@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 +32 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +51 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -0
- package/dist/diff.d.ts +43 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +81 -0
- package/dist/diff.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/render.d.ts +7 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +78 -0
- package/dist/render.js.map +1 -0
- package/dist/runner.d.ts +21 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +305 -0
- package/dist/runner.js.map +1 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +46 -0
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 @@
|
|
|
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
|
package/dist/cli.js.map
ADDED
|
@@ -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"}
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
package/dist/diff.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,SAAS,EAAU,MAAM,MAAM,CAAC;AA4CxE,SAAS,gBAAgB,CAAC,KAAa;IACrC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,uBAAuB,CAAC,KAAe;IAC9C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,CAAC,GAAG,CAAC,CAAC;IAEV,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAE1B,IACE,IAAI,CAAC,OAAO;YACZ,IAAI,EAAE,KAAK;YACX,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC;YAC5B,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,EAC5B,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;YACnD,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QAED,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC,IAAI,CAAC,CAAC;IACT,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB,EAAE,MAAc;IAC1D,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAgB,EAAE,CAAC;IAE/B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAE1B,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,EAAE,KAAK,EAAE,CAAC;YAChC,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,SAAS;gBACf,YAAY,EAAE,IAAI,CAAC,KAAK;gBACxB,UAAU,EAAE,IAAI,CAAC,KAAK;gBACtB,QAAQ,EAAE,uBAAuB,CAAC,kBAAkB,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;aAC9E,CAAC,CAAC;YACH,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,EAAE,OAAO,EAAE,CAAC;YAChC,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,SAAS;gBACf,YAAY,EAAE,IAAI,CAAC,KAAK;gBACxB,UAAU,EAAE,IAAI,CAAC,KAAK;gBACtB,QAAQ,EAAE,uBAAuB,CAAC,kBAAkB,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;aAC9E,CAAC,CAAC;YACH,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QACnD,CAAC;aAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QACrD,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QAClD,CAAC;QACD,CAAC,IAAI,CAAC,CAAC;IACT,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAmB;IAC7C,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;AACxD,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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"}
|
package/dist/render.d.ts
ADDED
|
@@ -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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"}
|
package/dist/runner.d.ts
ADDED
|
@@ -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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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|