eyeprolog 1.3.62 → 1.3.64
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/test/run-all.mjs +1 -1
- package/test/run-examples.mjs +102 -3
- package/test/run-regression.mjs +115 -33
- package/test/test-style.mjs +15 -0
package/package.json
CHANGED
package/test/run-all.mjs
CHANGED
package/test/run-examples.mjs
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
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';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
|
|
7
9
|
import { Program, run } from '../src/index.js';
|
|
8
10
|
import { fileURLToPath } from 'node:url';
|
|
9
11
|
import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
|
|
@@ -79,13 +81,13 @@ export const proofExamples = [
|
|
|
79
81
|
'partial-evaluator.pl',
|
|
80
82
|
];
|
|
81
83
|
|
|
82
|
-
export function runExamples(reporter = new TestReporter()) {
|
|
84
|
+
export async function runExamples(reporter = new TestReporter()) {
|
|
83
85
|
const files = fs.readdirSync(examplesDir)
|
|
84
86
|
.filter((name) => exampleIsRunnable(name))
|
|
85
87
|
.sort();
|
|
86
88
|
|
|
87
89
|
reporter.section('Examples');
|
|
88
|
-
|
|
90
|
+
await runExampleTasks(files, (name, result) => reporter.testResult(name, result));
|
|
89
91
|
reporter.sectionTotal('examples');
|
|
90
92
|
|
|
91
93
|
reporter.section('Proof examples');
|
|
@@ -94,6 +96,101 @@ export function runExamples(reporter = new TestReporter()) {
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
|
|
99
|
+
async function runExampleTasks(tasks, onResult) {
|
|
100
|
+
if (tasks.length === 0) return;
|
|
101
|
+
const parallelism = os.availableParallelism?.() ?? os.cpus().length;
|
|
102
|
+
const workerCount = Math.min(tasks.length, Math.max(1, Math.min(3, parallelism - 1)));
|
|
103
|
+
if (workerCount === 1) {
|
|
104
|
+
for (const name of tasks) {
|
|
105
|
+
const startedAt = performance.now();
|
|
106
|
+
try {
|
|
107
|
+
runExample(name);
|
|
108
|
+
onResult(name, { ms: Math.round(performance.now() - startedAt) });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
onResult(name, { ms: Math.round(performance.now() - startedAt), error });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const completedResults = new Map();
|
|
117
|
+
let nextTask = 0;
|
|
118
|
+
let nextReport = 0;
|
|
119
|
+
let completed = 0;
|
|
120
|
+
let settled = false;
|
|
121
|
+
|
|
122
|
+
await new Promise((resolve, reject) => {
|
|
123
|
+
const workers = Array.from({ length: workerCount }, () => new Worker(new URL(import.meta.url), {
|
|
124
|
+
workerData: { exampleWorker: true },
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
const stopWorkers = () => Promise.all(workers.map((worker) => worker.terminate()));
|
|
128
|
+
|
|
129
|
+
const fail = (error) => {
|
|
130
|
+
if (settled) return;
|
|
131
|
+
settled = true;
|
|
132
|
+
stopWorkers().finally(() => reject(error));
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const finish = () => {
|
|
136
|
+
if (settled || completed !== tasks.length || nextReport !== tasks.length) return;
|
|
137
|
+
settled = true;
|
|
138
|
+
stopWorkers().then(() => resolve(), reject);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const reportReady = () => {
|
|
142
|
+
try {
|
|
143
|
+
while (completedResults.has(nextReport)) {
|
|
144
|
+
const result = completedResults.get(nextReport);
|
|
145
|
+
completedResults.delete(nextReport);
|
|
146
|
+
onResult(tasks[nextReport], result);
|
|
147
|
+
nextReport++;
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
fail(error);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const assign = (worker) => {
|
|
155
|
+
if (nextTask >= tasks.length || settled) return;
|
|
156
|
+
worker.postMessage({ id: nextTask, name: tasks[nextTask++] });
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
for (const worker of workers) {
|
|
160
|
+
worker.on('message', ({ id, ms, error }) => {
|
|
161
|
+
if (settled) return;
|
|
162
|
+
completedResults.set(id, {
|
|
163
|
+
ms,
|
|
164
|
+
error: error == null ? null : Object.assign(new Error(error.message), { stack: error.stack }),
|
|
165
|
+
});
|
|
166
|
+
completed++;
|
|
167
|
+
reportReady();
|
|
168
|
+
assign(worker);
|
|
169
|
+
finish();
|
|
170
|
+
});
|
|
171
|
+
worker.on('error', fail);
|
|
172
|
+
assign(worker);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function runExampleWorker() {
|
|
178
|
+
parentPort.on('message', ({ id, name }) => {
|
|
179
|
+
const startedAt = performance.now();
|
|
180
|
+
try {
|
|
181
|
+
runExample(name);
|
|
182
|
+
parentPort.postMessage({ id, ms: Math.round(performance.now() - startedAt), error: null });
|
|
183
|
+
} catch (error) {
|
|
184
|
+
parentPort.postMessage({
|
|
185
|
+
id,
|
|
186
|
+
ms: Math.round(performance.now() - startedAt),
|
|
187
|
+
error: { message: error?.message ?? String(error), stack: error?.stack ?? String(error) },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
97
194
|
function exampleIsRunnable(name) {
|
|
98
195
|
return name.endsWith('.pl');
|
|
99
196
|
}
|
|
@@ -155,6 +252,8 @@ function diffText(expected, actualText) {
|
|
|
155
252
|
return 'outputs differ';
|
|
156
253
|
}
|
|
157
254
|
|
|
158
|
-
if (
|
|
255
|
+
if (!isMainThread && workerData?.exampleWorker) {
|
|
256
|
+
runExampleWorker();
|
|
257
|
+
} else if (isMainModule(import.meta.url)) {
|
|
159
258
|
await runStandalone(runExamples);
|
|
160
259
|
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -2221,22 +2221,16 @@ c4 ?- call((!;1)).
|
|
|
2221
2221
|
{
|
|
2222
2222
|
name: 'REPL term input is on demand in conjunctions and Ctrl-D does not exit the top level',
|
|
2223
2223
|
run: () => {
|
|
2224
|
-
if (
|
|
2225
|
-
const
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
`script -qefc ${shellQuote(command)} /dev/null`;
|
|
2235
|
-
const result = spawnSync('sh', ['-c', scriptCommand], {
|
|
2236
|
-
cwd: packageRoot,
|
|
2237
|
-
encoding: 'utf8',
|
|
2238
|
-
timeout: 5000,
|
|
2239
|
-
});
|
|
2224
|
+
if (!hasUtilLinuxScript()) return;
|
|
2225
|
+
const result = runScriptedRepl([
|
|
2226
|
+
{ waitFor: '?- ', send: 'read(X), read(Y).\n' },
|
|
2227
|
+
{ waitFor: ' |: ', send: 'foo.\n' },
|
|
2228
|
+
{ waitFor: '|: ', send: 'bar.\n' },
|
|
2229
|
+
{ waitFor: 'X = foo, Y = bar.', send: 'read(Z).\n' },
|
|
2230
|
+
{ waitFor: ' |: ', send: '\u0004' },
|
|
2231
|
+
{ waitFor: 'Z = end_of_file.', send: 'true.\n' },
|
|
2232
|
+
{ waitFor: ' true.', send: 'halt.\n' },
|
|
2233
|
+
]);
|
|
2240
2234
|
assertEqual(result.error?.code, undefined, 'interactive read timeout');
|
|
2241
2235
|
assertEqual(result.status, 0, 'exit status');
|
|
2242
2236
|
assertIncludes(result.stdout, 'X = foo, Y = bar.', 'conjunction reads');
|
|
@@ -2248,23 +2242,18 @@ c4 ?- call((!;1)).
|
|
|
2248
2242
|
{
|
|
2249
2243
|
name: 'REPL character input is on demand and Ctrl-D stays local (issue #55)',
|
|
2250
2244
|
run: () => {
|
|
2251
|
-
if (
|
|
2252
|
-
const
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
const result = spawnSync('sh', ['-c', scriptCommand], {
|
|
2264
|
-
cwd: packageRoot,
|
|
2265
|
-
encoding: 'utf8',
|
|
2266
|
-
timeout: 5000,
|
|
2267
|
-
});
|
|
2245
|
+
if (!hasUtilLinuxScript()) return;
|
|
2246
|
+
const result = runScriptedRepl([
|
|
2247
|
+
{ waitFor: '?- ', send: 'peek_char(P), get_char(C), get_code(K).\n' },
|
|
2248
|
+
{ waitFor: ' |: ', send: 'ab\n' },
|
|
2249
|
+
{ waitFor: 'P = a, C = a, K = 98.', send: 'get_char(N).\n' },
|
|
2250
|
+
{ waitFor: ' |: ', send: 'z\n' },
|
|
2251
|
+
{ waitFor: 'N = z.', send: 'get_char(E).\n' },
|
|
2252
|
+
{ waitFor: ' |: ', send: '\u0004' },
|
|
2253
|
+
{ waitFor: 'E = end_of_file.', send: 'get_char(D).\n' },
|
|
2254
|
+
{ waitFor: ' |: ', send: 'q\n' },
|
|
2255
|
+
{ waitFor: 'D = q.', send: 'halt.\n' },
|
|
2256
|
+
]);
|
|
2268
2257
|
assertEqual(result.error?.code, undefined, 'interactive character input timeout');
|
|
2269
2258
|
assertEqual(result.status, 0, 'exit status');
|
|
2270
2259
|
assertIncludes(result.stdout, 'P = a, C = a, K = 98.', 'peek/get char and code input');
|
|
@@ -6112,6 +6101,99 @@ function shellQuote(value) {
|
|
|
6112
6101
|
return `'${String(value).replaceAll("'", "'\"'\"'")}'`;
|
|
6113
6102
|
}
|
|
6114
6103
|
|
|
6104
|
+
let utilLinuxScriptAvailable = null;
|
|
6105
|
+
|
|
6106
|
+
function hasUtilLinuxScript() {
|
|
6107
|
+
if (process.platform === 'win32') return false;
|
|
6108
|
+
if (utilLinuxScriptAvailable == null) {
|
|
6109
|
+
const available = spawnSync('sh', ['-c',
|
|
6110
|
+
'command -v script >/dev/null 2>&1 && script --version 2>/dev/null | grep -qi util-linux']);
|
|
6111
|
+
utilLinuxScriptAvailable = available.status === 0;
|
|
6112
|
+
}
|
|
6113
|
+
return utilLinuxScriptAvailable;
|
|
6114
|
+
}
|
|
6115
|
+
|
|
6116
|
+
function runScriptedRepl(steps, { timeout = 5000 } = {}) {
|
|
6117
|
+
const command = `${shellQuote(process.execPath)} ${shellQuote(bin)}`;
|
|
6118
|
+
const payload = Buffer.from(JSON.stringify({
|
|
6119
|
+
command,
|
|
6120
|
+
cwd: packageRoot,
|
|
6121
|
+
steps,
|
|
6122
|
+
timeout,
|
|
6123
|
+
})).toString('base64');
|
|
6124
|
+
const helper = String.raw`
|
|
6125
|
+
const { spawn } = require('node:child_process');
|
|
6126
|
+
const config = JSON.parse(Buffer.from(process.env.EYEPROLOG_REPL_SCRIPT, 'base64').toString('utf8'));
|
|
6127
|
+
const child = spawn('script', ['-qefc', config.command, '/dev/null'], {
|
|
6128
|
+
cwd: config.cwd,
|
|
6129
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
6130
|
+
});
|
|
6131
|
+
let stdout = '';
|
|
6132
|
+
let stderr = '';
|
|
6133
|
+
let cursor = 0;
|
|
6134
|
+
let step = 0;
|
|
6135
|
+
let finished = false;
|
|
6136
|
+
|
|
6137
|
+
function advance() {
|
|
6138
|
+
while (step < config.steps.length) {
|
|
6139
|
+
const current = config.steps[step];
|
|
6140
|
+
const index = stdout.indexOf(current.waitFor, cursor);
|
|
6141
|
+
if (index === -1) return;
|
|
6142
|
+
cursor = index + current.waitFor.length;
|
|
6143
|
+
child.stdin.write(current.send);
|
|
6144
|
+
step++;
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6147
|
+
|
|
6148
|
+
child.stdout.on('data', (chunk) => {
|
|
6149
|
+
stdout += chunk;
|
|
6150
|
+
advance();
|
|
6151
|
+
});
|
|
6152
|
+
child.stderr.on('data', (chunk) => {
|
|
6153
|
+
stderr += chunk;
|
|
6154
|
+
});
|
|
6155
|
+
|
|
6156
|
+
const timer = setTimeout(() => {
|
|
6157
|
+
if (finished) return;
|
|
6158
|
+
finished = true;
|
|
6159
|
+
child.kill('SIGKILL');
|
|
6160
|
+
process.stdout.write(JSON.stringify({ status: null, stdout, stderr, timedOut: true, step }));
|
|
6161
|
+
}, config.timeout);
|
|
6162
|
+
|
|
6163
|
+
child.on('close', (status, signal) => {
|
|
6164
|
+
if (finished) return;
|
|
6165
|
+
finished = true;
|
|
6166
|
+
clearTimeout(timer);
|
|
6167
|
+
process.stdout.write(JSON.stringify({ status, signal, stdout, stderr, timedOut: false, step }));
|
|
6168
|
+
});
|
|
6169
|
+
`;
|
|
6170
|
+
const helperResult = spawnSync(process.execPath, ['-e', helper], {
|
|
6171
|
+
cwd: packageRoot,
|
|
6172
|
+
encoding: 'utf8',
|
|
6173
|
+
env: { ...process.env, EYEPROLOG_REPL_SCRIPT: payload },
|
|
6174
|
+
timeout: timeout + 1000,
|
|
6175
|
+
});
|
|
6176
|
+
if (helperResult.error) return helperResult;
|
|
6177
|
+
if (helperResult.status !== 0) return helperResult;
|
|
6178
|
+
let result;
|
|
6179
|
+
try {
|
|
6180
|
+
result = JSON.parse(helperResult.stdout);
|
|
6181
|
+
} catch (error) {
|
|
6182
|
+
return {
|
|
6183
|
+
...helperResult,
|
|
6184
|
+
error: new Error(`interactive REPL helper returned invalid JSON: ${error.message}`),
|
|
6185
|
+
};
|
|
6186
|
+
}
|
|
6187
|
+
return {
|
|
6188
|
+
status: result.status,
|
|
6189
|
+
stdout: result.stdout,
|
|
6190
|
+
stderr: `${result.stderr ?? ''}${helperResult.stderr ?? ''}`,
|
|
6191
|
+
error: result.timedOut
|
|
6192
|
+
? Object.assign(new Error(`interactive REPL timed out after step ${result.step}`), { code: 'ETIMEDOUT' })
|
|
6193
|
+
: undefined,
|
|
6194
|
+
};
|
|
6195
|
+
}
|
|
6196
|
+
|
|
6115
6197
|
function runCli(args, options = {}) {
|
|
6116
6198
|
return spawnSync(process.execPath, [bin, ...args], {
|
|
6117
6199
|
cwd: packageRoot,
|
package/test/test-style.mjs
CHANGED
|
@@ -87,6 +87,21 @@ export class TestReporter {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
testResult(name, { ms = 0, error = null } = {}) {
|
|
91
|
+
this.total++;
|
|
92
|
+
const nr = String(this.total).padStart(3, '0');
|
|
93
|
+
|
|
94
|
+
if (error == null) {
|
|
95
|
+
this.ok++;
|
|
96
|
+
this.stdout.write(`${colors.green}OK${colors.reset} ${nr} ${name} ${colors.dim}(${ms} ms)${colors.reset}\n`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.stderr.write(`${colors.red}FAIL${colors.reset} ${nr} ${name} ${colors.dim}(${ms} ms)${colors.reset}\n`);
|
|
101
|
+
this.stderr.write(`${error?.stack ?? String(error)}\n`);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
|
|
90
105
|
totalLine() {
|
|
91
106
|
const ms = nowMs() - this.startedAt;
|
|
92
107
|
this.stdout.write(`\n${colors.yellow}== Total${colors.reset}\n`);
|