eyelang 1.4.0 → 1.4.1
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/package.json +1 -1
- package/src/builtins/registry.js +7 -0
- package/src/cli.js +49 -29
- package/src/index.js +9 -7
- package/test/run-conformance.js +21 -44
- package/test/run-examples.js +42 -50
package/package.json
CHANGED
package/src/builtins/registry.js
CHANGED
package/src/cli.js
CHANGED
|
@@ -3,17 +3,13 @@
|
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import process from 'node:process';
|
|
6
|
-
import { Env, copyResolved, termIsGround, termToString } from './term.js';
|
|
7
|
-
import { Program } from './program.js';
|
|
8
|
-
import { Solver } from './solver.js';
|
|
9
|
-
import { parseQueryGoal } from './parser.js';
|
|
10
|
-
import { whyNoProof, whyProof } from './explain.js';
|
|
11
6
|
|
|
12
|
-
|
|
7
|
+
let engineModule = null;
|
|
8
|
+
let explanationModule = null;
|
|
13
9
|
|
|
14
10
|
export async function main(argv) {
|
|
15
11
|
if (argv.length === 0) {
|
|
16
|
-
usage(process.stdout);
|
|
12
|
+
await usage(process.stdout);
|
|
17
13
|
return;
|
|
18
14
|
}
|
|
19
15
|
|
|
@@ -35,7 +31,7 @@ export async function main(argv) {
|
|
|
35
31
|
} else if (!endOptions && (arg === '--version' || arg === '-v')) {
|
|
36
32
|
options.version = true;
|
|
37
33
|
} else if (!endOptions && (arg === '--help' || arg === '-h')) {
|
|
38
|
-
usage(process.stdout);
|
|
34
|
+
await usage(process.stdout);
|
|
39
35
|
return;
|
|
40
36
|
} else if (!endOptions && (arg === '--proof' || arg === '-p')) {
|
|
41
37
|
options.proof = true;
|
|
@@ -52,7 +48,7 @@ export async function main(argv) {
|
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
if (options.version) {
|
|
55
|
-
process.stdout.write(`eyelang ${
|
|
51
|
+
process.stdout.write(`eyelang ${await packageVersion()}\n`);
|
|
56
52
|
return;
|
|
57
53
|
}
|
|
58
54
|
|
|
@@ -77,45 +73,69 @@ export async function main(argv) {
|
|
|
77
73
|
}
|
|
78
74
|
}
|
|
79
75
|
|
|
80
|
-
const
|
|
76
|
+
const engine = await loadEngine();
|
|
77
|
+
const program = engine.Program.parseSources(sourceParts, { sourceMetadata: options.proof, markRecursive: options.proof });
|
|
81
78
|
|
|
82
|
-
if (options.query != null) runQuery(program, options.query, options);
|
|
83
|
-
else runDefault(program, options);
|
|
79
|
+
if (options.query != null) await runQuery(engine, program, options.query, options);
|
|
80
|
+
else await runDefault(engine, program, options);
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
async function loadEngine() {
|
|
84
|
+
if (engineModule == null) {
|
|
85
|
+
const [term, program, solver, parser, registry] = await Promise.all([
|
|
86
|
+
import('./term.js'),
|
|
87
|
+
import('./program.js'),
|
|
88
|
+
import('./solver.js'),
|
|
89
|
+
import('./parser.js'),
|
|
90
|
+
import('./builtins/registry.js'),
|
|
91
|
+
]);
|
|
92
|
+
engineModule = { ...term, ...program, ...solver, ...parser, ...registry };
|
|
93
|
+
}
|
|
94
|
+
return engineModule;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function loadExplanation() {
|
|
98
|
+
if (explanationModule == null) explanationModule = await import('./explain.js');
|
|
99
|
+
return explanationModule;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function runQuery(engine, program, query, options) {
|
|
103
|
+
const goal = engine.parseQueryGoal(query);
|
|
104
|
+
const registry = engine.getDefaultRegistry();
|
|
105
|
+
const solver = new engine.Solver(program, { registry });
|
|
106
|
+
const explanation = options.proof ? await loadExplanation() : null;
|
|
89
107
|
|
|
90
|
-
for (const env of solver.solve([goal], new Env(), 0)) {
|
|
91
|
-
process.stdout.write(`${termToString(goal, env, true)}.\n`);
|
|
108
|
+
for (const env of solver.solve([goal], new engine.Env(), 0)) {
|
|
109
|
+
process.stdout.write(`${engine.termToString(goal, env, true)}.\n`);
|
|
92
110
|
|
|
93
|
-
if (options.proof) writeExplanation(program, copyResolved(goal, env));
|
|
111
|
+
if (options.proof) writeExplanation(explanation, program, engine.copyResolved(goal, env), registry);
|
|
94
112
|
}
|
|
95
113
|
|
|
96
114
|
if (options.stats) printStats(solver.stats);
|
|
97
115
|
}
|
|
98
116
|
|
|
99
|
-
function runDefault(program, options) {
|
|
117
|
+
async function runDefault(engine, program, options) {
|
|
100
118
|
const goals = program.materializationGoals();
|
|
101
119
|
const materializedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
|
|
102
120
|
const facts = program.sourceFactLines(materializedKeys);
|
|
103
121
|
const lines = new Set();
|
|
104
122
|
let lastStats = null;
|
|
123
|
+
const registry = engine.getDefaultRegistry();
|
|
124
|
+
const explanation = options.proof ? await loadExplanation() : null;
|
|
105
125
|
|
|
106
126
|
for (const goal of goals) {
|
|
107
|
-
const solver = new Solver(program);
|
|
127
|
+
const solver = new engine.Solver(program, { registry });
|
|
108
128
|
|
|
109
|
-
for (const env of solver.solve([goal], new Env(), 0)) {
|
|
110
|
-
if (!termIsGround(goal, env)) continue;
|
|
129
|
+
for (const env of solver.solve([goal], new engine.Env(), 0)) {
|
|
130
|
+
if (!engine.termIsGround(goal, env)) continue;
|
|
111
131
|
|
|
112
|
-
const line = `${termToString(goal, env, true)}.\n`;
|
|
132
|
+
const line = `${engine.termToString(goal, env, true)}.\n`;
|
|
113
133
|
if (facts.has(line) || lines.has(line)) continue;
|
|
114
134
|
|
|
115
135
|
lines.add(line);
|
|
116
136
|
|
|
117
137
|
process.stdout.write(line);
|
|
118
|
-
if (options.proof) writeExplanation(program, copyResolved(goal, env));
|
|
138
|
+
if (options.proof) writeExplanation(explanation, program, engine.copyResolved(goal, env), registry);
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
lastStats = solver.stats;
|
|
@@ -124,14 +144,14 @@ function runDefault(program, options) {
|
|
|
124
144
|
if (options.stats && lastStats) printStats(lastStats);
|
|
125
145
|
}
|
|
126
146
|
|
|
127
|
-
function writeExplanation(program, resolved) {
|
|
128
|
-
const proof = whyProof(program, resolved);
|
|
147
|
+
function writeExplanation(explanation, program, resolved, registry) {
|
|
148
|
+
const proof = explanation.whyProof(program, resolved, { registry });
|
|
129
149
|
process.stdout.write(proof.text);
|
|
130
|
-
if (!proof.ok) process.stdout.write(whyNoProof(resolved));
|
|
150
|
+
if (!proof.ok) process.stdout.write(explanation.whyNoProof(resolved));
|
|
131
151
|
}
|
|
132
152
|
|
|
133
|
-
function usage(stream) {
|
|
134
|
-
stream.write(`eyelang ${
|
|
153
|
+
async function usage(stream) {
|
|
154
|
+
stream.write(`eyelang ${await packageVersion()}
|
|
135
155
|
|
|
136
156
|
Usage:
|
|
137
157
|
eyelang [options] [file-or-url.pl|- ...]
|
package/src/index.js
CHANGED
|
@@ -4,25 +4,27 @@ export { Program, makeProgram } from './program.js';
|
|
|
4
4
|
export { parseClauses, parseProgramText, parseQueryGoal } from './parser.js';
|
|
5
5
|
export { Solver } from './solver.js';
|
|
6
6
|
export * from './term.js';
|
|
7
|
-
export { BuiltinRegistry, createDefaultRegistry } from './builtins/registry.js';
|
|
7
|
+
export { BuiltinRegistry, createDefaultRegistry, getDefaultRegistry } from './builtins/registry.js';
|
|
8
8
|
|
|
9
9
|
import { Env, copyResolved, termIsGround, termToString } from './term.js';
|
|
10
10
|
import { Program } from './program.js';
|
|
11
11
|
import { Solver } from './solver.js';
|
|
12
12
|
import { parseQueryGoal } from './parser.js';
|
|
13
13
|
import { whyNoProof, whyProof } from './explain.js';
|
|
14
|
+
import { getDefaultRegistry } from './builtins/registry.js';
|
|
14
15
|
|
|
15
16
|
export function run(source, options = {}) {
|
|
16
17
|
const includeWhy = options.proof === true || options.why === true || options.explain === true;
|
|
17
18
|
const parseOptions = { ...options, sourceMetadata: includeWhy, markRecursive: includeWhy };
|
|
18
19
|
const program = source instanceof Program ? source : Program.parse(source, parseOptions);
|
|
19
|
-
const
|
|
20
|
+
const runOptions = options.registry ? options : { ...options, registry: getDefaultRegistry() };
|
|
21
|
+
const solver = new Solver(program, runOptions);
|
|
20
22
|
const output = [];
|
|
21
23
|
if (options.query) {
|
|
22
24
|
const goal = typeof options.query === 'string' ? parseQueryGoal(options.query) : options.query;
|
|
23
25
|
for (const env of solver.solve([goal], new Env(), 0)) {
|
|
24
26
|
output.push(`${termToString(goal, env, true)}.\n`);
|
|
25
|
-
if (includeWhy) appendExplanation(output, program, copyResolved(goal, env));
|
|
27
|
+
if (includeWhy) appendExplanation(output, program, copyResolved(goal, env), runOptions.registry);
|
|
26
28
|
}
|
|
27
29
|
} else {
|
|
28
30
|
const goals = program.materializationGoals();
|
|
@@ -30,7 +32,7 @@ export function run(source, options = {}) {
|
|
|
30
32
|
const facts = program.sourceFactLines(materializedKeys);
|
|
31
33
|
const seen = new Set();
|
|
32
34
|
for (const goal of goals) {
|
|
33
|
-
const localSolver = new Solver(program,
|
|
35
|
+
const localSolver = new Solver(program, runOptions);
|
|
34
36
|
for (const env of localSolver.solve([goal], new Env(), 0)) {
|
|
35
37
|
const resolved = copyResolved(goal, env);
|
|
36
38
|
if (!termIsGround(resolved)) continue;
|
|
@@ -38,15 +40,15 @@ export function run(source, options = {}) {
|
|
|
38
40
|
if (facts.has(line) || seen.has(line)) continue;
|
|
39
41
|
seen.add(line);
|
|
40
42
|
output.push(line);
|
|
41
|
-
if (includeWhy) appendExplanation(output, program, resolved);
|
|
43
|
+
if (includeWhy) appendExplanation(output, program, resolved, runOptions.registry);
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
return { stdout: output.join(''), stats: solver.stats };
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
function appendExplanation(output, program, resolved) {
|
|
49
|
-
const proof = whyProof(program, resolved);
|
|
50
|
+
function appendExplanation(output, program, resolved, registry) {
|
|
51
|
+
const proof = whyProof(program, resolved, { registry });
|
|
50
52
|
output.push(proof.text);
|
|
51
53
|
if (!proof.ok) output.push(whyNoProof(resolved));
|
|
52
54
|
}
|
package/test/run-conformance.js
CHANGED
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Conformance test runner.
|
|
3
|
-
// It executes
|
|
3
|
+
// It executes cases in-process so the conformance corpus measures engine behavior instead of Node process startup.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
-
import os from 'node:os';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { Program, run } from '../src/index.js';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { TestReporter, isMainModule } from './test-style.js';
|
|
10
10
|
|
|
11
11
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
-
const bin = path.join(root, 'bin', 'eyelang');
|
|
13
12
|
const profileArg = process.argv[2] ?? 'conformance';
|
|
14
13
|
|
|
15
14
|
export function runConformance(reporter = new TestReporter(), requestedProfiles = null) {
|
|
16
15
|
const profiles = requestedProfiles ?? (profileArg === 'conformance' ? ['core', 'extension'] : [profileArg]);
|
|
17
|
-
const
|
|
18
|
-
const actualFile = path.join(tmp, 'actual.out');
|
|
19
|
-
const errFile = path.join(tmp, 'stderr.out');
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
for (const profile of profiles) runProfile(reporter, profile, actualFile, errFile);
|
|
23
|
-
} finally {
|
|
24
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
25
|
-
}
|
|
16
|
+
for (const profile of profiles) runProfile(reporter, profile);
|
|
26
17
|
}
|
|
27
18
|
|
|
28
|
-
function runProfile(reporter, profile
|
|
19
|
+
function runProfile(reporter, profile) {
|
|
29
20
|
const casesDir = path.join(root, 'conformance', 'cases', profile);
|
|
30
21
|
const expectedDir = path.join(root, 'conformance', 'expected', profile);
|
|
31
22
|
|
|
@@ -38,56 +29,42 @@ function runProfile(reporter, profile, actualFile, errFile) {
|
|
|
38
29
|
for (const file of files) {
|
|
39
30
|
const name = file.slice(0, -3);
|
|
40
31
|
const label = `${profile}/${name}`;
|
|
41
|
-
reporter.test(label, () => runCase(profile, name, file, casesDir, expectedDir
|
|
32
|
+
reporter.test(label, () => runCase(profile, name, file, casesDir, expectedDir));
|
|
42
33
|
}
|
|
43
34
|
|
|
44
35
|
reporter.sectionTotal(`conformance ${profile}`);
|
|
45
36
|
}
|
|
46
37
|
|
|
47
|
-
function runCase(profile, name, file, casesDir, expectedDir
|
|
48
|
-
const
|
|
38
|
+
function runCase(profile, name, file, casesDir, expectedDir) {
|
|
39
|
+
const programFile = path.join(casesDir, file);
|
|
49
40
|
const queryFile = path.join(casesDir, `${name}.query`);
|
|
50
41
|
const expected = path.join(expectedDir, `${name}.out`);
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const outFd = fs.openSync(actualFile, 'w');
|
|
56
|
-
const errFd = fs.openSync(errFile, 'w');
|
|
57
|
-
const result = spawnSync(process.execPath, args, {
|
|
58
|
-
cwd: root,
|
|
59
|
-
stdio: ['ignore', outFd, errFd],
|
|
60
|
-
});
|
|
61
|
-
fs.closeSync(outFd);
|
|
62
|
-
fs.closeSync(errFd);
|
|
63
|
-
|
|
64
|
-
if (result.status !== 0) {
|
|
65
|
-
const stderr = fs.readFileSync(errFile, 'utf8');
|
|
66
|
-
const stdout = fs.readFileSync(actualFile, 'utf8');
|
|
67
|
-
throw new Error(`case ${profile}/${name} exited with ${result.status}\n${stderr}${stdout}`.trimEnd());
|
|
68
|
-
}
|
|
42
|
+
const text = fs.readFileSync(programFile, 'utf8');
|
|
43
|
+
const program = Program.parseSources([{ text, filename: file }], { sourceMetadata: false, markRecursive: false });
|
|
44
|
+
const options = fs.existsSync(queryFile) ? { query: fs.readFileSync(queryFile, 'utf8').trim() } : {};
|
|
45
|
+
const actual = run(program, options).stdout;
|
|
69
46
|
|
|
70
47
|
if (!fs.existsSync(expected)) {
|
|
71
48
|
throw new Error(`missing expected file: ${path.relative(root, expected)}`);
|
|
72
49
|
}
|
|
73
50
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
51
|
+
const expectedText = fs.readFileSync(expected, 'utf8');
|
|
52
|
+
if (expectedText !== actual) {
|
|
53
|
+
throw new Error(`output mismatch for ${profile}/${name}
|
|
54
|
+
${diffText(expected, actual)}`.trimEnd());
|
|
78
55
|
}
|
|
79
56
|
}
|
|
80
57
|
|
|
81
|
-
function diffText(expected,
|
|
82
|
-
const diff = spawnSync('diff', ['-u', expected,
|
|
58
|
+
function diffText(expected, actualText) {
|
|
59
|
+
const diff = spawnSync('diff', ['-u', expected, '-'], { input: actualText, encoding: 'utf8' });
|
|
83
60
|
if (diff.stdout) return diff.stdout;
|
|
84
61
|
|
|
85
62
|
const expectedText = fs.readFileSync(expected, 'utf8').split('\n');
|
|
86
|
-
const
|
|
87
|
-
const limit = Math.max(expectedText.length,
|
|
63
|
+
const actualLines = actualText.split('\n');
|
|
64
|
+
const limit = Math.max(expectedText.length, actualLines.length);
|
|
88
65
|
for (let i = 0; i < limit; i++) {
|
|
89
|
-
if (expectedText[i] !==
|
|
90
|
-
return `first difference at line ${i + 1}\nexpected: ${expectedText[i] ?? '<missing>'}\nactual: ${
|
|
66
|
+
if (expectedText[i] !== actualLines[i]) {
|
|
67
|
+
return `first difference at line ${i + 1}\nexpected: ${expectedText[i] ?? '<missing>'}\nactual: ${actualLines[i] ?? '<missing>'}`;
|
|
91
68
|
}
|
|
92
69
|
}
|
|
93
70
|
|
package/test/run-examples.js
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
// Example-output test runner.
|
|
3
3
|
// It compares examples byte-for-byte against golden output so answer and proof changes cannot silently alter results.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
-
import os from 'node:os';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { Program, run } from '../src/index.js';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { TestReporter, isMainModule } from './test-style.js';
|
|
10
10
|
|
|
11
11
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
-
const bin = path.join(root, 'bin', 'eyelang');
|
|
13
12
|
const examplesDir = path.join(root, 'examples');
|
|
14
13
|
const expectedDir = path.join(examplesDir, 'output');
|
|
15
14
|
const expectedProofDir = path.join(examplesDir, 'proof');
|
|
@@ -39,77 +38,70 @@ const proofExamples = [
|
|
|
39
38
|
];
|
|
40
39
|
|
|
41
40
|
export function runExamples(reporter = new TestReporter()) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
const files = fs.readdirSync(examplesDir)
|
|
42
|
+
.filter((name) => name.endsWith('.pl'))
|
|
43
|
+
.sort();
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.sort();
|
|
50
|
-
|
|
51
|
-
reporter.section('Examples');
|
|
52
|
-
for (const name of files) reporter.test(name, () => runExample(name, actualFile, errFile));
|
|
53
|
-
reporter.sectionTotal('examples');
|
|
45
|
+
reporter.section('Examples');
|
|
46
|
+
for (const name of files) reporter.test(name, () => runExample(name));
|
|
47
|
+
reporter.sectionTotal('examples');
|
|
54
48
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} finally {
|
|
59
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
60
|
-
}
|
|
49
|
+
reporter.section('Proof examples');
|
|
50
|
+
for (const name of proofExamples) reporter.test(name, () => runProofExample(name));
|
|
51
|
+
reporter.sectionTotal('proof examples');
|
|
61
52
|
}
|
|
62
53
|
|
|
63
|
-
function runExample(name
|
|
64
|
-
const
|
|
54
|
+
function runExample(name) {
|
|
55
|
+
const programFile = path.join(examplesDir, name);
|
|
65
56
|
const expected = path.join(expectedDir, name);
|
|
66
|
-
|
|
57
|
+
const actual = runProgramExample(programFile, name, { proof: false });
|
|
58
|
+
compareOutput(name, expected, actual, 'output');
|
|
67
59
|
}
|
|
68
60
|
|
|
69
|
-
function runProofExample(name
|
|
70
|
-
const
|
|
61
|
+
function runProofExample(name) {
|
|
62
|
+
const programFile = path.join(examplesDir, name);
|
|
71
63
|
const expected = path.join(expectedProofDir, name);
|
|
72
|
-
|
|
64
|
+
const actual = runProgramExample(programFile, name, { proof: true });
|
|
65
|
+
compareOutput(name, expected, actual, 'proof output');
|
|
73
66
|
}
|
|
74
67
|
|
|
75
|
-
function
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const stdout = fs.readFileSync(actualFile, 'utf8');
|
|
89
|
-
throw new Error(`example ${name} ${label} exited with ${result.status}\n${stderr}${stdout}`.trimEnd());
|
|
68
|
+
function runProgramExample(programFile, filename, options) {
|
|
69
|
+
const oldLocalTime = process.env.EYELANG_LOCAL_TIME;
|
|
70
|
+
process.env.EYELANG_LOCAL_TIME = fixedExampleDate;
|
|
71
|
+
try {
|
|
72
|
+
const text = fs.readFileSync(programFile, 'utf8');
|
|
73
|
+
const program = Program.parseSources([{ text, filename }], {
|
|
74
|
+
sourceMetadata: options.proof,
|
|
75
|
+
markRecursive: options.proof,
|
|
76
|
+
});
|
|
77
|
+
return run(program, options).stdout;
|
|
78
|
+
} finally {
|
|
79
|
+
if (oldLocalTime == null) delete process.env.EYELANG_LOCAL_TIME;
|
|
80
|
+
else process.env.EYELANG_LOCAL_TIME = oldLocalTime;
|
|
90
81
|
}
|
|
82
|
+
}
|
|
91
83
|
|
|
84
|
+
function compareOutput(name, expected, actual, label) {
|
|
92
85
|
if (!fs.existsSync(expected)) {
|
|
93
86
|
throw new Error(`missing expected ${label} file: ${path.relative(root, expected)}`);
|
|
94
87
|
}
|
|
95
88
|
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
throw new Error(`${label} mismatch for ${name}\n${diffText(expected, actualFile)}`.trimEnd());
|
|
89
|
+
const expectedText = fs.readFileSync(expected, 'utf8');
|
|
90
|
+
if (expectedText !== actual) {
|
|
91
|
+
throw new Error(`${label} mismatch for ${name}\n${diffText(expected, actual)}`.trimEnd());
|
|
100
92
|
}
|
|
101
93
|
}
|
|
102
94
|
|
|
103
|
-
function diffText(expected,
|
|
104
|
-
const diff = spawnSync('diff', ['-u', expected,
|
|
95
|
+
function diffText(expected, actualText) {
|
|
96
|
+
const diff = spawnSync('diff', ['-u', expected, '-'], { input: actualText, encoding: 'utf8' });
|
|
105
97
|
if (diff.stdout) return diff.stdout;
|
|
106
98
|
|
|
107
99
|
const expectedText = fs.readFileSync(expected, 'utf8').split('\n');
|
|
108
|
-
const
|
|
109
|
-
const limit = Math.max(expectedText.length,
|
|
100
|
+
const actualLines = actualText.split('\n');
|
|
101
|
+
const limit = Math.max(expectedText.length, actualLines.length);
|
|
110
102
|
for (let i = 0; i < limit; i++) {
|
|
111
|
-
if (expectedText[i] !==
|
|
112
|
-
return `first difference at line ${i + 1}\nexpected: ${expectedText[i] ?? '<missing>'}\nactual: ${
|
|
103
|
+
if (expectedText[i] !== actualLines[i]) {
|
|
104
|
+
return `first difference at line ${i + 1}\nexpected: ${expectedText[i] ?? '<missing>'}\nactual: ${actualLines[i] ?? '<missing>'}`;
|
|
113
105
|
}
|
|
114
106
|
}
|
|
115
107
|
|