eyeprolog 1.3.88 → 1.3.89

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.88",
6
+ "version": "1.3.89",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/test/run-all.mjs CHANGED
@@ -21,7 +21,7 @@ await runStandalone(async (reporter) => {
21
21
  runOpenRuleBenchChecks(reporter);
22
22
  runArchitecture(reporter);
23
23
  runCleanup(reporter);
24
- runRegression(reporter);
24
+ await runRegression(reporter);
25
25
  await runPlayground(reporter);
26
26
  await runExamples(reporter);
27
27
  runBookExamples(reporter);
@@ -87,13 +87,7 @@ export async function runExamples(reporter = new TestReporter()) {
87
87
  .sort();
88
88
 
89
89
  reporter.section('Examples');
90
- const deepTaxonomyIndex = files.indexOf('deep-taxonomy-100000.pl');
91
- if (deepTaxonomyIndex >= 0) {
92
- await runExampleTasks(files.slice(0, deepTaxonomyIndex + 1), (name, result) => reporter.testResult(name, result), 2);
93
- await runExampleTasks(files.slice(deepTaxonomyIndex + 1), (name, result) => reporter.testResult(name, result), 3);
94
- } else {
95
- await runExampleTasks(files, (name, result) => reporter.testResult(name, result), 3);
96
- }
90
+ await runExampleTasks(files, (name, result) => reporter.testResult(name, result), 3);
97
91
  reporter.sectionTotal('examples');
98
92
 
99
93
  reporter.section('Proof examples');
@@ -7,6 +7,7 @@ import fs from 'node:fs';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { spawnSync } from 'node:child_process';
10
+ import { Worker, parentPort, workerData } from 'node:worker_threads';
10
11
  import { fileURLToPath } from 'node:url';
11
12
  import publicDefaultApi from '../index.js';
12
13
  import * as publicApi from '../src/index.js';
@@ -84,13 +85,15 @@ function sourceAtom(value) {
84
85
  return `'${String(value).replaceAll('\\', '\\\\').replaceAll("'", "''")}'`;
85
86
  }
86
87
 
87
- export function runRegression(reporter = new TestReporter(), requestedSection = null) {
88
- const sections = [
88
+ const regressionSections = [
89
89
  { key: 'regression', name: 'Regression', cases: regressionCases },
90
90
  { key: 'docs', name: 'Documentation sync', cases: documentationSyncCases },
91
91
  { key: 'api', name: 'API', cases: apiCases },
92
92
  { key: 'white-box', name: 'White-box', cases: whiteBoxCases },
93
- ];
93
+ ];
94
+
95
+ export async function runRegression(reporter = new TestReporter(), requestedSection = null) {
96
+ const sections = regressionSections;
94
97
  const selected = requestedSection == null
95
98
  ? sections
96
99
  : sections.filter((section) => section.key === requestedSection);
@@ -98,16 +101,167 @@ export function runRegression(reporter = new TestReporter(), requestedSection =
98
101
  throw new Error(`unknown regression section: ${requestedSection}; expected ${sections.map(({ key }) => key).join(', ')}`);
99
102
  }
100
103
 
104
+ const prepared = selected.map((section) => ({ ...section, tests: section.cases() }));
105
+ const testCount = prepared.reduce((sum, section) => sum + section.tests.length, 0);
106
+ const parallelism = os.availableParallelism?.() ?? os.cpus().length;
107
+ // Several cases start bounded-heap child processes, so leave CPU and memory
108
+ // headroom instead of matching the host's full logical-core count.
109
+ const workerCount = Math.min(testCount, Math.max(1, Math.min(4, parallelism - 1)));
110
+ if (workerCount === 1) {
111
+ withRegressionTemp(() => {
112
+ for (const section of prepared) runSection(reporter, section.name, section.tests);
113
+ });
114
+ return;
115
+ }
116
+
117
+ const results = new Map();
118
+ let reportSection = 0;
119
+ let reportTest = 0;
120
+ reporter.section(prepared[0].name);
121
+ const reportReady = () => {
122
+ while (reportSection < prepared.length) {
123
+ const section = prepared[reportSection];
124
+ const result = results.get(testKey(section.key, reportTest));
125
+ if (result == null) return;
126
+ reporter.testResult(section.tests[reportTest].name, result);
127
+ reportTest++;
128
+ if (reportTest !== section.tests.length) continue;
129
+ reporter.sectionTotal(sectionLabel(section.name), sectionElapsedMs(section, results));
130
+ reportSection++;
131
+ reportTest = 0;
132
+ if (reportSection < prepared.length) reporter.section(prepared[reportSection].name);
133
+ }
134
+ };
135
+ await runRegressionWorkers(prepared, workerCount, results, reportReady);
136
+ }
137
+
138
+ function withRegressionTemp(runTests) {
101
139
  tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeprolog-regression.'));
102
140
  tmpCounter = 0;
103
141
  try {
104
- for (const section of selected) runSection(reporter, section.name, section.cases());
142
+ return runTests();
105
143
  } finally {
106
144
  fs.rmSync(tmp, { recursive: true, force: true });
107
145
  tmp = null;
108
146
  }
109
147
  }
110
148
 
149
+ async function runRegressionWorkers(sections, workerCount, results, onResult) {
150
+ const tasks = sections.flatMap((section) => section.tests.map((_, index) => ({
151
+ sectionKey: section.key,
152
+ index,
153
+ })));
154
+ let nextTask = 0;
155
+ let completed = 0;
156
+ let settled = false;
157
+
158
+ return new Promise((resolve, reject) => {
159
+ const workers = Array.from({ length: workerCount }, () => new Worker(new URL(import.meta.url), {
160
+ workerData: { regressionWorker: true },
161
+ }));
162
+
163
+ const closeWorkers = () => {
164
+ for (const worker of workers) worker.postMessage({ close: true });
165
+ };
166
+ const fail = (error) => {
167
+ if (settled) return;
168
+ settled = true;
169
+ Promise.all(workers.map((worker) => worker.terminate())).finally(() => reject(error));
170
+ };
171
+ const assign = (worker) => {
172
+ if (settled) return;
173
+ const task = tasks[nextTask++];
174
+ if (task != null) worker.postMessage(task);
175
+ };
176
+
177
+ for (const worker of workers) {
178
+ worker.on('message', (message) => {
179
+ if (settled) return;
180
+ if (message.ready) {
181
+ assign(worker);
182
+ return;
183
+ }
184
+ results.set(testKey(message.sectionKey, message.index), {
185
+ ms: message.ms,
186
+ error: deserializeWorkerError(message.error),
187
+ startedAt: message.startedAt,
188
+ finishedAt: message.finishedAt,
189
+ });
190
+ try {
191
+ onResult();
192
+ } catch (error) {
193
+ fail(error);
194
+ return;
195
+ }
196
+ completed++;
197
+ if (completed === tasks.length) {
198
+ settled = true;
199
+ const exits = Promise.all(workers.map((candidate) => new Promise((done, failed) => {
200
+ candidate.once('exit', done);
201
+ candidate.once('error', failed);
202
+ })));
203
+ closeWorkers();
204
+ exits.then(() => resolve(results), reject);
205
+ } else {
206
+ assign(worker);
207
+ }
208
+ });
209
+ worker.on('error', fail);
210
+ worker.on('exit', (code) => {
211
+ if (!settled) fail(new Error(`regression worker exited unexpectedly with status ${code}`));
212
+ });
213
+ }
214
+ });
215
+ }
216
+
217
+ function testKey(sectionKey, index) {
218
+ return `${sectionKey}:${index}`;
219
+ }
220
+
221
+ function sectionElapsedMs(section, results) {
222
+ const timings = section.tests.map((_, index) => results.get(testKey(section.key, index)));
223
+ return Math.max(...timings.map(({ finishedAt }) => finishedAt)) -
224
+ Math.min(...timings.map(({ startedAt }) => startedAt));
225
+ }
226
+
227
+ function deserializeWorkerError(error) {
228
+ if (error == null) return null;
229
+ return Object.assign(new Error(error.message), { stack: error.stack });
230
+ }
231
+
232
+ function startRegressionWorker() {
233
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeprolog-regression-worker.'));
234
+ tmpCounter = 0;
235
+ const casesBySection = new Map(regressionSections.map((section) => [section.key, section.cases()]));
236
+ parentPort.on('message', ({ sectionKey, index, close }) => {
237
+ if (close) {
238
+ fs.rmSync(tmp, { recursive: true, force: true });
239
+ tmp = null;
240
+ parentPort.close();
241
+ return;
242
+ }
243
+ const testCase = casesBySection.get(sectionKey)?.[index];
244
+ const startedAt = Date.now();
245
+ const started = performance.now();
246
+ let error = null;
247
+ try {
248
+ if (testCase == null) throw new Error(`unknown regression test ${sectionKey}:${index}`);
249
+ testCase.run();
250
+ } catch (caught) {
251
+ error = { message: caught?.message ?? String(caught), stack: caught?.stack ?? String(caught) };
252
+ }
253
+ parentPort.postMessage({
254
+ sectionKey,
255
+ index,
256
+ ms: Math.round(performance.now() - started),
257
+ startedAt,
258
+ finishedAt: Date.now(),
259
+ error,
260
+ });
261
+ });
262
+ parentPort.postMessage({ ready: true });
263
+ }
264
+
111
265
  function regressionCases() {
112
266
  return [
113
267
  {
@@ -7185,6 +7339,8 @@ function format(value) {
7185
7339
  return typeof value === 'string' ? JSON.stringify(value) : String(value);
7186
7340
  }
7187
7341
 
7188
- if (isMainModule(import.meta.url)) {
7342
+ if (workerData?.regressionWorker) {
7343
+ startRegressionWorker();
7344
+ } else if (isMainModule(import.meta.url)) {
7189
7345
  await runStandalone((reporter) => runRegression(reporter, process.argv[2] ?? null));
7190
7346
  }
@@ -41,12 +41,12 @@ export class TestReporter {
41
41
  this.sectionCount++;
42
42
  }
43
43
 
44
- sectionTotal(label = null) {
44
+ sectionTotal(label = null, elapsedMs = null) {
45
45
  if (this.currentSection == null) return;
46
46
 
47
47
  const ok = this.ok - this.currentSection.okAtStart;
48
48
  const total = this.total - this.currentSection.totalAtStart;
49
- const ms = nowMs() - this.currentSection.startedAt;
49
+ const ms = elapsedMs ?? nowMs() - this.currentSection.startedAt;
50
50
  const suite = label ?? defaultSectionLabel(this.currentSection.name);
51
51
  this.stdout.write(`${colors.green}OK${colors.reset} ${ok}/${total} ${suite} tests passed ${colors.dim}(${ms} ms)${colors.reset}\n`);
52
52
  }