@eu-cpsc/lecture-lab-core 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # @eu-cpsc/lecture-lab-core
2
+
3
+ Loads `lecture.yaml` configs, runs a test's command (pipe or pty mode), and
4
+ produces a structured diff between expected and actual output. No VS Code
5
+ dependency - this is the engine behind the
6
+ [Lecture Lab](https://github.com/eu-cpsc/lecture-lab) VS Code extension, and
7
+ also ships standalone as the `lecture-lab` CLI for use outside the editor
8
+ (e.g. grading automation).
9
+
10
+ > **Beta**: published under the `beta` npm dist-tag. Install with
11
+ > `npm install @eu-cpsc/lecture-lab-core@beta` or pin an exact version.
12
+
13
+ ## CLI usage
14
+
15
+ ```bash
16
+ npx @eu-cpsc/lecture-lab-core@beta test lecture.yaml
17
+ ```
18
+
19
+ Runs every test defined in the given config, printing a pass/fail summary and
20
+ a diff for any mismatched output.
21
+
22
+ ## Library usage
23
+
24
+ ```ts
25
+ import { loadLectureConfigFromYaml, runTest } from "@eu-cpsc/lecture-lab-core";
26
+
27
+ const config = await loadLectureConfigFromYaml("lecture.yaml");
28
+ const result = await runTest(config.items[0].tests[0]);
29
+ ```
30
+
31
+ See the [monorepo README](https://github.com/eu-cpsc/lecture-lab) for the
32
+ full project layout and contribution notes.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { createRequire } from "node:module";
4
+ import * as path from "path";
5
+ import { loadLectureConfigFromFile, loadLectureConfigFromYaml } from "./config.js";
6
+ import { runTest } from "./runner.js";
7
+ import { renderResultAsText } from "./render.js";
8
+ // Read the version from package.json at runtime instead of hardcoding it a
9
+ // second time here, so a version bump only ever touches one file.
10
+ const { version } = createRequire(import.meta.url)("../package.json");
11
+ const program = new Command();
12
+ program.version(version);
13
+ program.command("test [config-file]").action(async (file) => {
14
+ let config;
15
+ if (!file) {
16
+ config = await loadLectureConfigFromFile("lecture.json");
17
+ }
18
+ else if (file.endsWith(".yml") || file.endsWith(".yaml")) {
19
+ config = await loadLectureConfigFromYaml(file);
20
+ }
21
+ else {
22
+ config = await loadLectureConfigFromFile(file);
23
+ }
24
+ let failure = false;
25
+ let totalTests = 0;
26
+ let passedTests = 0;
27
+ const dir = file ? path.dirname(file) : "";
28
+ for (const item of config.items) {
29
+ for (const test of item.tests || []) {
30
+ totalTests++;
31
+ const result = await runTest(test, dir);
32
+ for (const line of renderResultAsText(result, test.cmd, test.compareToFile)) {
33
+ console.log(line);
34
+ }
35
+ console.log("");
36
+ if (result.pass) {
37
+ passedTests++;
38
+ }
39
+ else {
40
+ failure = true;
41
+ }
42
+ }
43
+ }
44
+ const noTestsSuffix = totalTests === 0 ? " (no tests found)." : ".";
45
+ console.log(`Summary: ${passedTests}/${totalTests} tests passed${noTestsSuffix}`);
46
+ if (failure) {
47
+ process.exit(1);
48
+ }
49
+ });
50
+ program.parse();
51
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,OAAO,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,2EAA2E;AAC3E,kEAAkE;AAClE,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAEnE,CAAC;AAEF,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;AAEzB,OAAO,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;IAClE,IAAI,MAAqB,CAAC;IAE1B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,GAAG,MAAM,yBAAyB,CAAC,cAAc,CAAC,CAAC;IAC3D,CAAC;SAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3D,MAAM,GAAG,MAAM,yBAAyB,CAAC,IAAI,CAAC,CAAC;IACjD,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,MAAM,yBAAyB,CAAC,IAAI,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,WAAW,GAAG,CAAC,CAAC;IAEpB,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAE3C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;YACpC,UAAU,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACxC,KAAK,MAAM,IAAI,IAAI,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;gBAC5E,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACpB,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAEhB,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;gBAChB,WAAW,EAAE,CAAC;YAChB,CAAC;iBAAM,CAAC;gBACN,OAAO,GAAG,IAAI,CAAC;YACjB,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,aAAa,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,GAAG,CAAC;IACpE,OAAO,CAAC,GAAG,CAAC,YAAY,WAAW,IAAI,UAAU,gBAAgB,aAAa,EAAE,CAAC,CAAC;IAElF,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,KAAK,EAAE,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { LectureConfig } from "./types.js";
2
+ export declare function loadLectureConfigFromFile(path: string): Promise<LectureConfig>;
3
+ export declare function loadLectureConfigFromText(text: string): Promise<LectureConfig>;
4
+ export declare function loadLectureConfigFromYaml(path: string): Promise<LectureConfig>;
5
+ export declare function loadLectureConfigFromYamlText(text: string): Promise<LectureConfig>;
6
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,aAAa,CAAC,CAGxB;AAED,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,aAAa,CAAC,CAGxB;AAED,wBAAsB,6BAA6B,CACjD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,aAAa,CAAC,CAExB"}
package/dist/config.js ADDED
@@ -0,0 +1,17 @@
1
+ import { readFile as readFileAsync } from "node:fs/promises";
2
+ import * as yaml from "js-yaml";
3
+ export async function loadLectureConfigFromFile(path) {
4
+ const text = await readFileAsync(path, "utf8");
5
+ return JSON.parse(text);
6
+ }
7
+ export async function loadLectureConfigFromText(text) {
8
+ return JSON.parse(text);
9
+ }
10
+ export async function loadLectureConfigFromYaml(path) {
11
+ const text = await readFileAsync(path, "utf8");
12
+ return yaml.load(text);
13
+ }
14
+ export async function loadLectureConfigFromYamlText(text) {
15
+ return yaml.load(text);
16
+ }
17
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,KAAK,IAAI,MAAM,SAAS,CAAC;AAGhC,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,IAAY;IAEZ,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAkB,CAAC;AAC3C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,IAAY;IAEZ,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAkB,CAAC;AAC3C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,IAAY;IAEZ,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC/C,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAkB,CAAC;AAC1C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,6BAA6B,CACjD,IAAY;IAEZ,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAkB,CAAC;AAC1C,CAAC"}
package/dist/diff.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { Change } from "diff";
2
+ /**
3
+ * A single line kept as-is on both sides.
4
+ */
5
+ export interface DiffBlockSame {
6
+ kind: "same";
7
+ text: string;
8
+ }
9
+ /**
10
+ * Line(s) present in the actual output but missing from the expected output.
11
+ */
12
+ export interface DiffBlockAdded {
13
+ kind: "added";
14
+ text: string;
15
+ }
16
+ /**
17
+ * Line(s) expected but missing from the actual output.
18
+ */
19
+ export interface DiffBlockRemoved {
20
+ kind: "removed";
21
+ text: string;
22
+ }
23
+ /**
24
+ * A line that exists on both sides but differs. `wordDiff` pinpoints exactly
25
+ * which words changed, so the UI can highlight just the divergent span
26
+ * instead of marking the whole line as different.
27
+ */
28
+ export interface DiffBlockChanged {
29
+ kind: "changed";
30
+ expectedText: string;
31
+ actualText: string;
32
+ wordDiff: Change[];
33
+ }
34
+ export type DiffBlock = DiffBlockSame | DiffBlockAdded | DiffBlockRemoved | DiffBlockChanged;
35
+ /**
36
+ * Line-diffs expected vs. actual output, then upgrades any adjacent
37
+ * removed+added pair into a single "changed" block with a word-level diff.
38
+ * This is what lets the UI say "you printed a B here instead of an A"
39
+ * instead of just flagging the whole line as wrong.
40
+ */
41
+ export declare function computeDiff(expected: string, actual: string): DiffBlock[];
42
+ export declare function diffIsClean(blocks: DiffBlock[]): boolean;
43
+ //# sourceMappingURL=diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4C,MAAM,EAAE,MAAM,MAAM,CAAC;AAExE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,MAAM,SAAS,GACjB,aAAa,GACb,cAAc,GACd,gBAAgB,GAChB,gBAAgB,CAAC;AAwCrB;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,CA0CzE;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAExD"}
package/dist/diff.js ADDED
@@ -0,0 +1,81 @@
1
+ import { diffLines, diffWordsWithSpace, diffChars } from "diff";
2
+ function isWhitespaceOnly(value) {
3
+ return value.length > 0 && /^\s+$/.test(value);
4
+ }
5
+ /**
6
+ * diffWordsWithSpace treats a run of whitespace as one atomic token, so two
7
+ * differing whitespace runs (e.g. one space vs. two) come back as a whole
8
+ * removed token plus a whole added token instead of the minimal edit within
9
+ * them - misleadingly implying every character changed when in fact only
10
+ * one space was inserted. Re-diffing just that pair at the character level
11
+ * finds the smaller edit a reader actually expects.
12
+ */
13
+ function refineWhitespaceChanges(parts) {
14
+ const refined = [];
15
+ let i = 0;
16
+ while (i < parts.length) {
17
+ const part = parts[i];
18
+ const next = parts[i + 1];
19
+ if (part.removed &&
20
+ next?.added &&
21
+ isWhitespaceOnly(part.value) &&
22
+ isWhitespaceOnly(next.value)) {
23
+ refined.push(...diffChars(part.value, next.value));
24
+ i += 2;
25
+ continue;
26
+ }
27
+ refined.push(part);
28
+ i += 1;
29
+ }
30
+ return refined;
31
+ }
32
+ /**
33
+ * Line-diffs expected vs. actual output, then upgrades any adjacent
34
+ * removed+added pair into a single "changed" block with a word-level diff.
35
+ * This is what lets the UI say "you printed a B here instead of an A"
36
+ * instead of just flagging the whole line as wrong.
37
+ */
38
+ export function computeDiff(expected, actual) {
39
+ const parts = diffLines(expected, actual);
40
+ const blocks = [];
41
+ let i = 0;
42
+ while (i < parts.length) {
43
+ const part = parts[i];
44
+ const next = parts[i + 1];
45
+ if (part.removed && next?.added) {
46
+ blocks.push({
47
+ kind: "changed",
48
+ expectedText: part.value,
49
+ actualText: next.value,
50
+ wordDiff: refineWhitespaceChanges(diffWordsWithSpace(part.value, next.value)),
51
+ });
52
+ i += 2;
53
+ continue;
54
+ }
55
+ if (part.added && next?.removed) {
56
+ blocks.push({
57
+ kind: "changed",
58
+ expectedText: next.value,
59
+ actualText: part.value,
60
+ wordDiff: refineWhitespaceChanges(diffWordsWithSpace(next.value, part.value)),
61
+ });
62
+ i += 2;
63
+ continue;
64
+ }
65
+ if (part.added) {
66
+ blocks.push({ kind: "added", text: part.value });
67
+ }
68
+ else if (part.removed) {
69
+ blocks.push({ kind: "removed", text: part.value });
70
+ }
71
+ else {
72
+ blocks.push({ kind: "same", text: part.value });
73
+ }
74
+ i += 1;
75
+ }
76
+ return blocks;
77
+ }
78
+ export function diffIsClean(blocks) {
79
+ return blocks.every((block) => block.kind === "same");
80
+ }
81
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./config.js";
3
+ export * from "./runner.js";
4
+ export * from "./diff.js";
5
+ export * from "./render.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./config.js";
3
+ export * from "./runner.js";
4
+ export * from "./diff.js";
5
+ export * from "./render.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { LectureTestResult } from "./runner.js";
2
+ /**
3
+ * Renders a test result as plain text lines, for the CLI / CI use case
4
+ * (no rich diff highlighting available in a terminal).
5
+ */
6
+ export declare function renderResultAsText(result: LectureTestResult, command: string, expectedFilePath?: string): string[];
7
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AA0BhD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,MAAM,EACf,gBAAgB,CAAC,EAAE,MAAM,GACxB,MAAM,EAAE,CA6DV"}
package/dist/render.js ADDED
@@ -0,0 +1,78 @@
1
+ function splitWithVisibleLineEndings(value) {
2
+ const lines = [];
3
+ const linePattern = /([^\r\n]*)(\r\n|\r|\n|$)/g;
4
+ let match;
5
+ while ((match = linePattern.exec(value)) !== null) {
6
+ const content = match[1] ?? "";
7
+ const ending = match[2] ?? "";
8
+ if (ending === "" && content === "") {
9
+ break;
10
+ }
11
+ const visibleEnding = ending.replace(/\r/g, "␍").replace(/\n/g, "␊");
12
+ lines.push(`${content}${visibleEnding}`);
13
+ if (ending === "") {
14
+ break;
15
+ }
16
+ }
17
+ return lines;
18
+ }
19
+ /**
20
+ * Renders a test result as plain text lines, for the CLI / CI use case
21
+ * (no rich diff highlighting available in a terminal).
22
+ */
23
+ export function renderResultAsText(result, command, expectedFilePath) {
24
+ const lines = [];
25
+ lines.push(`Command : ${command}`);
26
+ if (expectedFilePath) {
27
+ lines.push(`Expected : ${expectedFilePath}`);
28
+ }
29
+ if (result.stdin) {
30
+ lines.push(`Input : ${JSON.stringify(result.stdin)}`);
31
+ }
32
+ if (result.runError) {
33
+ lines.push(`FAIL: Could not run command: ${result.runError}`);
34
+ return lines;
35
+ }
36
+ if (result.pass) {
37
+ lines.push("PASS: Test passed successfully.");
38
+ return lines;
39
+ }
40
+ lines.push("FAIL: Test failed.");
41
+ if (result.actualExitCode !== result.expectedExitCode) {
42
+ lines.push(`Command exited with code ${result.actualExitCode}, but expected ${result.expectedExitCode}.`);
43
+ }
44
+ if (result.diff) {
45
+ lines.push("Output differs from expected file:");
46
+ lines.push("-".repeat(60));
47
+ for (const block of result.diff) {
48
+ if (block.kind === "same") {
49
+ for (const line of splitWithVisibleLineEndings(block.text)) {
50
+ lines.push(` ${line}`);
51
+ }
52
+ }
53
+ else if (block.kind === "added") {
54
+ for (const line of splitWithVisibleLineEndings(block.text)) {
55
+ lines.push(`+ ${line}`);
56
+ }
57
+ }
58
+ else if (block.kind === "removed") {
59
+ for (const line of splitWithVisibleLineEndings(block.text)) {
60
+ lines.push(`- ${line}`);
61
+ }
62
+ }
63
+ else {
64
+ for (const line of splitWithVisibleLineEndings(block.expectedText)) {
65
+ lines.push(`- ${line}`);
66
+ }
67
+ for (const line of splitWithVisibleLineEndings(block.actualText)) {
68
+ lines.push(`+ ${line}`);
69
+ }
70
+ }
71
+ }
72
+ lines.push("-".repeat(60));
73
+ lines.push("Legend: + actual output - expected output");
74
+ lines.push("Line endings: CR is shown as ␍, LF is shown as ␊");
75
+ }
76
+ return lines;
77
+ }
78
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { LectureItemTest } from "./types.js";
2
+ import { DiffBlock } from "./diff.js";
3
+ export interface LectureTestResult {
4
+ pass: boolean;
5
+ actualExitCode: number;
6
+ expectedExitCode: number;
7
+ actualOutput: string;
8
+ expectedOutput?: string;
9
+ /**
10
+ * The stdin text actually fed to the process, resolved from `stdin` or
11
+ * `stdinFile`. Piped stdin is never echoed into the captured output (that's
12
+ * correct - a real pipe doesn't echo), so the UI must show this
13
+ * explicitly or a student has no way to tell which input a result is for.
14
+ */
15
+ stdin?: string;
16
+ diff?: DiffBlock[];
17
+ /** Set when the command itself could not be run (e.g. bad cwd, spawn error). */
18
+ runError?: string;
19
+ }
20
+ export declare function runTest(test: LectureItemTest, cwd?: string): Promise<LectureTestResult>;
21
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,eAAe,EAAqB,MAAM,YAAY,CAAC;AAChE,OAAO,EAA4B,SAAS,EAAE,MAAM,WAAW,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC;IACnB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAqXD,wBAAsB,OAAO,CAC3B,IAAI,EAAE,eAAe,EACrB,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAoB5B"}
package/dist/runner.js ADDED
@@ -0,0 +1,305 @@
1
+ import * as cp from "child_process";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import stripAnsiCodes from "strip-ansi";
5
+ import { computeDiff, diffIsClean } from "./diff.js";
6
+ const DEFAULT_COMMAND_TIMEOUT_MS = 15_000;
7
+ function runCommandPipe(command, options) {
8
+ return new Promise((resolve, reject) => {
9
+ const child = cp.spawn(command, [], {
10
+ cwd: options.cwd,
11
+ shell: true,
12
+ stdio: ["pipe", "pipe", "pipe"],
13
+ });
14
+ if (options.stdinFilePath) {
15
+ const stdinStream = fs.createReadStream(options.stdinFilePath);
16
+ stdinStream.on("error", reject);
17
+ stdinStream.pipe(child.stdin);
18
+ }
19
+ else if (options.stdin !== undefined) {
20
+ child.stdin.write(options.stdin);
21
+ child.stdin.end();
22
+ }
23
+ else {
24
+ child.stdin.end();
25
+ }
26
+ let stdout = "";
27
+ let stderr = "";
28
+ let settled = false;
29
+ child.stdout.on("data", (chunk) => (stdout += chunk));
30
+ child.stderr.on("data", (chunk) => (stderr += chunk));
31
+ child.on("error", reject);
32
+ // A student's exercise can loop forever or block waiting for stdin it
33
+ // was never given; without a cap, that hangs this test - and anything
34
+ // awaiting it - forever, same as pty mode without its own timeout.
35
+ const timeoutId = DEFAULT_COMMAND_TIMEOUT_MS > 0
36
+ ? setTimeout(() => {
37
+ if (settled) {
38
+ return;
39
+ }
40
+ settled = true;
41
+ child.kill("SIGKILL");
42
+ // Follow GNU timeout convention: 124 indicates timeout expiry.
43
+ resolve({ output: stdout + stderr, exitCode: 124 });
44
+ }, DEFAULT_COMMAND_TIMEOUT_MS)
45
+ : undefined;
46
+ child.on("close", (code, signal) => {
47
+ if (settled) {
48
+ return;
49
+ }
50
+ settled = true;
51
+ if (timeoutId) {
52
+ clearTimeout(timeoutId);
53
+ }
54
+ resolve({
55
+ output: stdout + stderr,
56
+ exitCode: code ?? (signal ? 1 : 0),
57
+ });
58
+ });
59
+ });
60
+ }
61
+ function getStdinText(options) {
62
+ if (options.stdinFilePath) {
63
+ return fs.promises.readFile(options.stdinFilePath, "utf8");
64
+ }
65
+ return Promise.resolve(options.stdin ?? "");
66
+ }
67
+ function normalizeCrLfToLf(value) {
68
+ return value.replace(/\r\n/g, "\n");
69
+ }
70
+ /**
71
+ * Splits stdin text into the chunks a typed-line-at-a-time user would send,
72
+ * keeping each line's trailing "\n" attached (so it can be written to the
73
+ * pty as one Enter keypress) except for a final line with no trailing
74
+ * newline, which is left bare to simulate input ended by EOF rather than
75
+ * Enter.
76
+ */
77
+ function splitStdinIntoLines(input) {
78
+ if (input === "") {
79
+ return [];
80
+ }
81
+ const endsWithNewline = input.endsWith("\n");
82
+ const parts = input.split("\n");
83
+ const realParts = endsWithNewline ? parts.slice(0, -1) : parts;
84
+ return realParts.map((part, index) => {
85
+ const isLast = index === realParts.length - 1;
86
+ return isLast && !endsWithNewline ? part : `${part}\n`;
87
+ });
88
+ }
89
+ // A real terminal echoes each keystroke and only advances once the program
90
+ // actually asks for the next line, so a prompt printed just before a read
91
+ // always appears before that line's echo. A pty has no such handshake -
92
+ // writing is fire-and-forget - so instead we wait for output to go quiet
93
+ // (the program has presumably finished printing its prompt and is now
94
+ // blocked reading) before sending the next line. PTY_INPUT_MAX_WAIT_MS
95
+ // bounds that wait for prompt-less reads, where output may never go quiet
96
+ // on its own.
97
+ const PTY_INPUT_QUIET_MS = 30;
98
+ const PTY_INPUT_MAX_WAIT_MS = 300;
99
+ const DEFAULT_PTY_TIMEOUT_MS = 15_000;
100
+ function waitForPtyQuiet(isExited, getLastDataAt, getDataEventCount) {
101
+ return new Promise((resolve) => {
102
+ const start = Date.now();
103
+ const dataEventCountBefore = getDataEventCount();
104
+ const check = () => {
105
+ if (isExited()) {
106
+ resolve();
107
+ return;
108
+ }
109
+ const now = Date.now();
110
+ const sawNewOutput = getDataEventCount() > dataEventCountBefore;
111
+ const quietLongEnough = now - getLastDataAt() >= PTY_INPUT_QUIET_MS;
112
+ const timedOut = now - start >= PTY_INPUT_MAX_WAIT_MS;
113
+ if (timedOut || (sawNewOutput && quietLongEnough)) {
114
+ resolve();
115
+ return;
116
+ }
117
+ setTimeout(check, 5);
118
+ };
119
+ check();
120
+ });
121
+ }
122
+ async function feedPtyInputByLine(terminal, input, isExited, getLastDataAt, getDataEventCount) {
123
+ for (const line of splitStdinIntoLines(input)) {
124
+ if (isExited()) {
125
+ return;
126
+ }
127
+ await waitForPtyQuiet(isExited, getLastDataAt, getDataEventCount);
128
+ if (isExited()) {
129
+ return;
130
+ }
131
+ terminal.write(line);
132
+ }
133
+ }
134
+ function processEnvForPty() {
135
+ return Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
136
+ }
137
+ let ptyModulePromise;
138
+ function loadPtyModule() {
139
+ if (!ptyModulePromise) {
140
+ ptyModulePromise = import("node-pty").catch((error) => {
141
+ console.warn("[lecture-lab] node-pty is unavailable, falling back to pipe mode " +
142
+ "(interactive prompts and their input won't be interleaved):", error instanceof Error ? error.message : error);
143
+ return undefined;
144
+ });
145
+ }
146
+ return ptyModulePromise;
147
+ }
148
+ async function runCommandPty(command, options) {
149
+ const pty = await loadPtyModule();
150
+ if (!pty) {
151
+ return runCommandPipe(command, options);
152
+ }
153
+ return getStdinText(options).then((input) => new Promise((resolve, reject) => {
154
+ const shell = process.platform === "win32" ? "cmd.exe" : "/bin/sh";
155
+ const shellArgs = process.platform === "win32"
156
+ ? ["/d", "/s", "/c", command]
157
+ : ["-lc", command];
158
+ const terminal = pty.spawn(shell, shellArgs, {
159
+ name: "xterm-256color",
160
+ cols: options.pty?.cols ?? 80,
161
+ rows: options.pty?.rows ?? 24,
162
+ cwd: options.cwd ?? process.cwd(),
163
+ env: {
164
+ ...processEnvForPty(),
165
+ ...(options.pty?.env ?? {}),
166
+ },
167
+ });
168
+ let output = "";
169
+ let settled = false;
170
+ let exited = false;
171
+ let lastDataAt = Date.now();
172
+ let dataEventCount = 0;
173
+ let timeoutId;
174
+ const resolveResult = (exitCode) => {
175
+ if (settled) {
176
+ return;
177
+ }
178
+ settled = true;
179
+ if (timeoutId) {
180
+ clearTimeout(timeoutId);
181
+ }
182
+ if (options.pty?.stripAnsi !== false) {
183
+ output = stripAnsiCodes(output);
184
+ }
185
+ if (options.pty?.normalizeNewlines !== false) {
186
+ output = normalizeCrLfToLf(output);
187
+ }
188
+ resolve({ output, exitCode });
189
+ };
190
+ terminal.onData((chunk) => {
191
+ output += chunk;
192
+ lastDataAt = Date.now();
193
+ dataEventCount++;
194
+ });
195
+ terminal.onExit((event) => {
196
+ exited = true;
197
+ resolveResult(event.exitCode);
198
+ });
199
+ // Without a cap, a program that never reads all of its expected
200
+ // stdin (or otherwise never exits) would hang this test - and
201
+ // anything awaiting it - forever. Default this on for pty mode since
202
+ // it spawns a real subprocess with no other escape hatch; pass
203
+ // timeoutMs: 0 to opt out.
204
+ const timeoutMs = options.pty?.timeoutMs ?? DEFAULT_PTY_TIMEOUT_MS;
205
+ if (timeoutMs > 0) {
206
+ timeoutId = setTimeout(() => {
207
+ try {
208
+ terminal.kill();
209
+ }
210
+ finally {
211
+ // Follow GNU timeout convention: 124 indicates timeout expiry.
212
+ resolveResult(124);
213
+ }
214
+ }, timeoutMs);
215
+ }
216
+ feedPtyInputByLine(terminal, input, () => exited, () => lastDataAt, () => dataEventCount)
217
+ .then(() => {
218
+ if (!exited && process.platform !== "win32") {
219
+ // Send Ctrl-D to signal EOF in POSIX PTY sessions.
220
+ terminal.write("");
221
+ }
222
+ // On Windows, cmd.exe exits naturally after input is consumed.
223
+ })
224
+ .catch((error) => {
225
+ reject(error);
226
+ });
227
+ }));
228
+ }
229
+ function runCommand(command, options) {
230
+ // pty is the default: it's what makes interactive prompts and their
231
+ // input line up the way a student actually sees them in a real terminal.
232
+ // Pass ioMode: "pipe" to opt out (e.g. for a command with no stdin at all,
233
+ // where a plain pipe is simpler and slightly cheaper to spawn).
234
+ if (options.ioMode === "pipe") {
235
+ return runCommandPipe(command, options);
236
+ }
237
+ return runCommandPty(command, options);
238
+ }
239
+ function readExpectedFile(filePath) {
240
+ const resolved = path.resolve(filePath);
241
+ if (!fs.existsSync(resolved)) {
242
+ throw new Error(`Expected output file not found: ${resolved}`);
243
+ }
244
+ return fs.readFileSync(resolved, "utf8");
245
+ }
246
+ async function verifyOutput(command, expectedExitCode, options) {
247
+ let stdin = "";
248
+ try {
249
+ stdin = await getStdinText(options);
250
+ const result = await runCommand(command, options);
251
+ const exitCodesMatch = result.exitCode === expectedExitCode;
252
+ if (options.compareToFile || options.compareTo !== undefined) {
253
+ const expectedOutput = options.compareToFile
254
+ ? readExpectedFile(options.compareToFile)
255
+ : options.compareTo ?? "";
256
+ const diff = computeDiff(expectedOutput, result.output);
257
+ return {
258
+ pass: diffIsClean(diff) && exitCodesMatch,
259
+ actualExitCode: result.exitCode,
260
+ expectedExitCode,
261
+ actualOutput: result.output,
262
+ expectedOutput,
263
+ stdin,
264
+ diff,
265
+ };
266
+ }
267
+ return {
268
+ pass: exitCodesMatch,
269
+ actualExitCode: result.exitCode,
270
+ expectedExitCode,
271
+ actualOutput: result.output,
272
+ stdin,
273
+ };
274
+ }
275
+ catch (err) {
276
+ return {
277
+ pass: false,
278
+ actualExitCode: -1,
279
+ expectedExitCode,
280
+ actualOutput: "",
281
+ stdin,
282
+ runError: err instanceof Error ? err.message : String(err),
283
+ };
284
+ }
285
+ }
286
+ export async function runTest(test, cwd) {
287
+ let compareToFilePath = test.compareToFile;
288
+ if (compareToFilePath && !path.isAbsolute(compareToFilePath)) {
289
+ compareToFilePath = path.join(cwd || ".", compareToFilePath);
290
+ }
291
+ let stdinFilePath = test.stdinFile;
292
+ if (stdinFilePath && !path.isAbsolute(stdinFilePath)) {
293
+ stdinFilePath = path.join(cwd || ".", stdinFilePath);
294
+ }
295
+ return verifyOutput(test.cmd, test.exitCode ?? 0, {
296
+ cwd,
297
+ compareTo: test.compareTo,
298
+ compareToFile: compareToFilePath,
299
+ stdin: test.stdin,
300
+ stdinFilePath,
301
+ ioMode: test.ioMode,
302
+ pty: test.pty,
303
+ });
304
+ }
305
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ export interface LecturePtyOptions {
2
+ cols?: number;
3
+ rows?: number;
4
+ env?: Record<string, string>;
5
+ stripAnsi?: boolean;
6
+ normalizeNewlines?: boolean;
7
+ timeoutMs?: number;
8
+ }
9
+ export interface LectureItemTest {
10
+ cmd: string;
11
+ compareTo?: string;
12
+ compareToFile?: string;
13
+ exitCode?: number;
14
+ stdin?: string;
15
+ stdinFile?: string;
16
+ /** Defaults to "pty", which interleaves prompts with typed input the way
17
+ * a real terminal does. Pass "pipe" to opt out (e.g. a command with no
18
+ * stdin at all, where a plain pipe is simpler and slightly cheaper). */
19
+ ioMode?: "pipe" | "pty";
20
+ pty?: LecturePtyOptions;
21
+ }
22
+ export interface LectureItem {
23
+ name: string;
24
+ notesFile: string;
25
+ sourceFiles?: string[];
26
+ runCmd?: string;
27
+ tests?: LectureItemTest[];
28
+ }
29
+ export interface LectureConfig {
30
+ items: LectureItem[];
31
+ }
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;4EAEwE;IACxE,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACxB,GAAG,CAAC,EAAE,iBAAiB,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@eu-cpsc/lecture-lab-core",
3
+ "version": "0.3.0",
4
+ "description": "Loads lecture.yaml configs, runs exercise tests, and produces student-friendly diffs. Core logic for lecture-lab.",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "lecture-lab": "dist/cli.js"
11
+ },
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "import": "./dist/index.js",
17
+ "types": "./dist/index.d.ts"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "check-types": "tsc --noEmit",
29
+ "lint": "eslint src",
30
+ "test": "vitest run"
31
+ },
32
+ "dependencies": {
33
+ "commander": "^15.0.0",
34
+ "diff": "^9.0.0",
35
+ "js-yaml": "^4.2.0",
36
+ "node-pty": "^1.1.0",
37
+ "strip-ansi": "^7.2.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/js-yaml": "^4.0.9",
41
+ "@types/node": "^22.19.19",
42
+ "vitest": "^3.2.4"
43
+ },
44
+ "author": "eu-cpsc",
45
+ "type": "module"
46
+ }