149 lines
5.2 KiB
TypeScript
149 lines
5.2 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|||
|
|
import { writeFile } from "node:fs/promises";
|
||
|
|
import { join } from "node:path";
|
||
|
|
|
||
|
|
import { expect } from "vitest";
|
||
|
|
|
||
|
|
import { readZipView } from "../../src/application/desktop/DesktopZipView.js";
|
||
|
|
import type { DesktopTaskCheck } from "../../src/domain/desktop/appTask.js";
|
||
|
|
import {
|
||
|
|
evaluateTaskChecks,
|
||
|
|
type TaskCheckOutcome,
|
||
|
|
type TaskFileView,
|
||
|
|
} from "../../src/domain/desktop/appTaskChecks.js";
|
||
|
|
import {
|
||
|
|
PYTHON_HELPERS,
|
||
|
|
PYTHON_IMPORTS,
|
||
|
|
} from "../../src/domain/desktop/appExportPython.js";
|
||
|
|
import { PYTHON_TASK_EVALUATOR } from "../../src/domain/desktop/appTaskPython.js";
|
||
|
|
import {
|
||
|
|
detectStateFormat,
|
||
|
|
textualView,
|
||
|
|
} from "../../src/domain/desktop/stateFormats.js";
|
||
|
|
import type { JsonValue } from "../../src/domain/jsonValue.js";
|
||
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||
|
|
|
||
|
|
/** A file as an app saved it: its name under the home and its bytes. */
|
||
|
|
export interface SavedFile {
|
||
|
|
readonly name: string;
|
||
|
|
readonly bytes: Uint8Array;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The exported module's standard library imports, its helpers and the verifier's evaluator, as one program. */
|
||
|
|
const PYTHON = `${PYTHON_IMPORTS.split("\n")
|
||
|
|
.filter((line) => !line.includes("agentenv_protocol"))
|
||
|
|
.join("\n")}${PYTHON_HELPERS}${PYTHON_TASK_EVALUATOR}`;
|
||
|
|
|
||
|
|
/** Run a driver after the exported module's helpers and evaluator, with JSON on its stdin; its stdout. */
|
||
|
|
export const exportedPython = (driver: string, input: unknown): string =>
|
||
|
|
execFileSync("python3", ["-I", "-B", "-c", `${PYTHON}\n${driver}`], {
|
||
|
|
input: JSON.stringify(input),
|
||
|
|
encoding: "utf8",
|
||
|
|
maxBuffer: 64 * 1024 * 1024,
|
||
|
|
});
|
||
|
|
|
||
|
|
/** One JSON value per line the driver printed. */
|
||
|
|
const printed = <Value>(output: string): Value[] =>
|
||
|
|
output
|
||
|
|
.trim()
|
||
|
|
.split("\n")
|
||
|
|
.map((line) => JSON.parse(line) as Value);
|
||
|
|
|
||
|
|
/** Write each file into a fresh folder, as data/get finds it under the home, keeping its suffix; their paths. */
|
||
|
|
export const writeFiles = async (files: readonly SavedFile[]) => {
|
||
|
|
const folder = await createTestTempDirectory("desktop-export-python-");
|
||
|
|
return Promise.all(
|
||
|
|
files.map(async ({ name, bytes }, index) => {
|
||
|
|
const path = join(folder, `${String(index)}-${name}`);
|
||
|
|
await writeFile(path, bytes);
|
||
|
|
return path;
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
/** The exported module's data/get entry for each file: its format and view, or its error. */
|
||
|
|
export const pythonFileViews = async (files: readonly SavedFile[]) =>
|
||
|
|
printed<Record<string, JsonValue>>(
|
||
|
|
exportedPython(
|
||
|
|
`for path in json.load(sys.stdin):
|
||
|
|
print(json.dumps(_view(Path(path))))`,
|
||
|
|
await writeFiles(files),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
/** The verifier's results for each file, read by the exported module as the home path `home`. */
|
||
|
|
const pythonCheckResults = async (
|
||
|
|
home: string,
|
||
|
|
checks: readonly DesktopTaskCheck[],
|
||
|
|
files: readonly SavedFile[],
|
||
|
|
) =>
|
||
|
|
printed<{ id: string; result: boolean; description: string }[]>(
|
||
|
|
exportedPython(
|
||
|
|
`request = json.load(sys.stdin)
|
||
|
|
for path in request["paths"]:
|
||
|
|
print(json.dumps(evaluate(request["checks"], {request["home"]: _view(Path(path))})))`,
|
||
|
|
{ home, checks, paths: await writeFiles(files) },
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
/** The exported module's XML view of each text, or null where it refuses one. */
|
||
|
|
export const pythonXmlViews = (texts: readonly string[]) =>
|
||
|
|
printed<JsonValue>(
|
||
|
|
exportedPython(
|
||
|
|
`for text in json.load(sys.stdin):
|
||
|
|
print(json.dumps(_xml_view(text)))`,
|
||
|
|
texts,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
/** REA's state view of a zip, text, JSON, key file or XML file, in the shape data/get serves it. */
|
||
|
|
export const typescriptFileView = async ({
|
||
|
|
name,
|
||
|
|
bytes,
|
||
|
|
}: SavedFile): Promise<TaskFileView> => {
|
||
|
|
const format = detectStateFormat(name, bytes);
|
||
|
|
if (format === "sqlite" || format === "dconf" || format === "binary")
|
||
|
|
throw new Error(`${name} is ${format}, which these tests do not read`);
|
||
|
|
const read =
|
||
|
|
format === "zip" ? await readZipView(bytes) : textualView(format, bytes);
|
||
|
|
return read.ok
|
||
|
|
? { format, view: read.value }
|
||
|
|
: { format, view: null, error: read.error };
|
||
|
|
};
|
||
|
|
|
||
|
|
/** Both readers give each file the same format and view, or the same error. */
|
||
|
|
export const expectSameFileViews = async (files: readonly SavedFile[]) => {
|
||
|
|
const fromPython = await pythonFileViews(files);
|
||
|
|
for (const [index, file] of files.entries()) {
|
||
|
|
const ours = await typescriptFileView(file);
|
||
|
|
const theirs = fromPython[index] ?? {};
|
||
|
|
expect(theirs.format).toBe(ours.format);
|
||
|
|
expect(theirs.view).toEqual(ours.view);
|
||
|
|
expect(theirs.error).toBe(ours.error);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/** Both evaluators give the same results and descriptions on each file read as the home path `home`; REA's. */
|
||
|
|
export const expectSameCheckResults = async (
|
||
|
|
home: string,
|
||
|
|
checks: readonly DesktopTaskCheck[],
|
||
|
|
files: readonly SavedFile[],
|
||
|
|
): Promise<(readonly TaskCheckOutcome[])[]> => {
|
||
|
|
const fromPython = await pythonCheckResults(home, checks, files);
|
||
|
|
return Promise.all(
|
||
|
|
files.map(async (file, index) => {
|
||
|
|
const ours = evaluateTaskChecks(
|
||
|
|
checks,
|
||
|
|
new Map([[home, await typescriptFileView(file)]]),
|
||
|
|
);
|
||
|
|
expect(fromPython[index]).toEqual(
|
||
|
|
ours.map(({ id, passed, description }) => ({
|
||
|
|
id,
|
||
|
|
result: passed,
|
||
|
|
description,
|
||
|
|
})),
|
||
|
|
);
|
||
|
|
return ours;
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
};
|