diff --git a/report-app/report-server.ts b/report-app/report-server.ts index d2a27f9..113a4a9 100644 --- a/report-app/report-server.ts +++ b/report-app/report-server.ts @@ -4,11 +4,13 @@ import { isMainModule, writeResponseToNodeResponse, } from '@angular/ssr/node'; -import { glob } from 'tinyglobby'; import express from 'express'; -import { readFile } from 'node:fs/promises'; import { dirname, isAbsolute, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { + FetchedLocalReports, + fetchReportsFromDisk, +} from '../runner/reporting/report-local-disk'; const app = express(); const reportsLoaderPromise = getReportLoader(); @@ -16,7 +18,7 @@ const options = getOptions(); const serverDistFolder = dirname(fileURLToPath(import.meta.url)); const browserDistFolder = resolve(serverDistFolder, '../browser'); const angularApp = new AngularNodeAppEngine(); -let localDataPromise: Promise | null = null; +let localDataPromise: Promise | null = null; // Endpoint for fetching all available report groups. app.get('/api/reports', async (_, res) => { @@ -26,8 +28,8 @@ app.get('/api/reports', async (_, res) => { ]); const results = remoteGroups.slice(); - for (const [, group] of localData) { - results.unshift(group.overview); + for (const [, data] of localData) { + results.unshift(data.group); } res.json(results); @@ -77,14 +79,6 @@ interface ReportLoader { getGroupsList: () => Promise<{ id: string }[]>; } -type LocalData = Map< - string, - { - overview: { id: string }; - run: { group: string }; - } ->; - /** Gets the server options from the command line. */ function getOptions() { const defaultPort = 4200; @@ -135,37 +129,9 @@ async function getReportLoader() { async function resolveLocalData(directory: string) { // Reuse the same promise so that concurrent requests get the same response. if (!localDataPromise) { - let resolveFn: (data: LocalData) => void; + let resolveFn: (data: FetchedLocalReports) => void; localDataPromise = new Promise((resolve) => (resolveFn = resolve)); - - const data: LocalData = new Map(); - const groupFiles = await glob('**/groups.json', { - cwd: directory, - absolute: true, - }); - - await Promise.all( - // Note: sort the groups so that the indexes stay consistent no matter how the files - // appear on disk. It appears to be non-deterministic when using the async glob. - groupFiles.sort().map(async (configPath, index) => { - const [groupContent, runContent] = await Promise.all([ - readFile(configPath, 'utf8'), - readFile(join(dirname(configPath), 'summary.json'), 'utf8'), - ]); - - // Note: Local reports only have one group. - const overview = (JSON.parse(groupContent) as { id: string }[])[0]; - const run = JSON.parse(runContent) as { group: string }; - - // Local runs should not be grouped by their group ID, but rather if they - // were part of the same invocation. Add a unique suffix to the ID to - // prevent further grouping. - run.group = overview.id = `${overview.id}-l${index}`; - data.set(overview.id, { overview, run }); - }) - ); - - resolveFn!(data); + resolveFn!(await fetchReportsFromDisk(directory)); } return localDataPromise; diff --git a/runner/index.ts b/runner/index.ts index 8a450bf..8cfbeed 100644 --- a/runner/index.ts +++ b/runner/index.ts @@ -29,3 +29,4 @@ export { getRunnerByName, type RunnerName } from './codegen/runner-creation.js'; export { getEnvironmentByPath } from './configuration/environment-resolution.js'; export { type Environment } from './configuration/environment.js'; export { autoRateFiles } from './ratings/autoraters/rate-files.js'; +export { fetchReportsFromDisk } from './reporting/report-local-disk.js'; diff --git a/runner/reporting/report-local-disk.ts b/runner/reporting/report-local-disk.ts new file mode 100644 index 0000000..e4621d5 --- /dev/null +++ b/runner/reporting/report-local-disk.ts @@ -0,0 +1,47 @@ +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { RunGroup, RunInfo } from '../shared-interfaces.js'; +import { glob } from 'tinyglobby'; + +/** Type describing a map from group report IDs to their runs. */ +export type FetchedLocalReports = Map< + /* groupId */ string, + { + group: RunGroup; + run: RunInfo; + } +>; + +/** Fetches local report data from the given directory. */ +export async function fetchReportsFromDisk( + directory: string +): Promise { + const data: FetchedLocalReports = new Map(); + const groupFiles = await glob('**/groups.json', { + cwd: directory, + absolute: true, + }); + + await Promise.all( + // Note: sort the groups so that the indexes stay consistent no matter how the files + // appear on disk. It appears to be non-deterministic when using the async glob. + groupFiles.sort().map(async (configPath, index) => { + const [groupContent, runContent] = await Promise.all([ + readFile(configPath, 'utf8'), + readFile(join(dirname(configPath), 'summary.json'), 'utf8'), + ]); + + // Note: Local reports only have one group. + const group = (JSON.parse(groupContent) as RunGroup[])[0]; + const run = JSON.parse(runContent) as RunInfo; + + // Local runs should not be grouped by their group ID, but rather if they + // were part of the same invocation. Add a unique suffix to the ID to + // prevent further grouping. + run.group = group.id = `${group.id}-l${index}`; + data.set(group.id, { group, run }); + }) + ); + + return data; +}