eyeling 1.26.3 → 1.26.5

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.
@@ -52,20 +52,53 @@ function normalizeMarkdownForCompare(text) {
52
52
  // Normalize N3 output for comparison.
53
53
  // Eyeling (and other N3 tools) may emit the same closure with different
54
54
  // triple ordering. Examples tests should verify content, not presentation.
55
- function normalizeN3ForCompare(n3Text) {
56
- return stripTrailingWhitespace(n3Text)
55
+ function normalizeN3ForCompare(n3Text, sourceText = '', expectedPath = '') {
56
+ let value = stripTrailingWhitespace(n3Text);
57
+
58
+ // get-uuid.n3 intentionally uses log:skolem and relative IRIs, so its
59
+ // output depends on the checkout path and generated skolem seed. Compare the
60
+ // shape of the generated triples rather than those environment-specific bits.
61
+ if (/\blog:skolem\b/.test(sourceText) || path.basename(expectedPath) === 'get-uuid.n3') {
62
+ value = value
63
+ .replace(/<urn:uuid:[^>]+>/g, '<urn:uuid:__UUID__>')
64
+ .replace(/<file:\/\/[^>\s]*\/examples\/([^/>]+)>/g, '<file://__EXAMPLES__/$1>');
65
+ }
66
+
67
+ return value
57
68
  .split('\n')
58
69
  .filter((l) => l.length > 0)
59
70
  .sort((a, b) => a.localeCompare(b))
60
71
  .join('\n');
61
72
  }
62
73
 
63
- function normalizeForCompare(text, expectedPath) {
74
+ function normalizeForCompare(text, expectedPath, sourceText = '') {
64
75
  const ext = path.extname(expectedPath);
65
76
  const value = text;
66
77
  if (ext === '.md') return normalizeMarkdownForCompare(value);
67
78
  if (ext === '.txt') return normalizeTextForCompare(value);
68
- return normalizeN3ForCompare(value);
79
+ return normalizeN3ForCompare(value, sourceText, expectedPath);
80
+ }
81
+
82
+ function normalizeProofForCompare(text, expectedPath, sourceText) {
83
+ let value = normalizeForCompare(text, expectedPath, sourceText);
84
+
85
+ // Some proof goldens intentionally cover volatile builtins such as
86
+ // time:localTime. Keep those examples useful by comparing proof structure
87
+ // while masking only the volatile literal values.
88
+ if (/\btime:localTime\b/.test(sourceText)) {
89
+ value = value
90
+ .replace(/"[^"\n]*"\^\^xsd:dateTime/g, '"__DATETIME__"^^xsd:dateTime')
91
+ .replace(/"PT[0-9]+(?:\.[0-9]+)?S"\^\^xsd:duration/g, '"__DURATION__"^^xsd:duration');
92
+ }
93
+
94
+ return value;
95
+ }
96
+
97
+ function compareGeneratedOutput({ expectedPath, generatedPath, sourceText, proof }) {
98
+ const expectedText = fs.readFileSync(expectedPath, 'utf8');
99
+ const generatedText = fs.readFileSync(generatedPath, 'utf8');
100
+ const normalize = proof ? normalizeProofForCompare : normalizeForCompare;
101
+ return normalize(expectedText, expectedPath, sourceText) === normalize(generatedText, expectedPath, sourceText);
69
102
  }
70
103
 
71
104
  // Expectation logic (robust, long-term):
@@ -141,6 +174,42 @@ function resolveExampleBuiltinPath(root, inputFile) {
141
174
  return { abs, rel };
142
175
  }
143
176
 
177
+ function runExampleToFile({ root, examplesDir, eyelingJsPath, nodePath, file, generatedPath, proof = false }) {
178
+ const builtin = resolveExampleBuiltinPath(root, file);
179
+ const trigInput = resolveExampleTrigInput(root, file);
180
+ const rdfMode = !!trigInput;
181
+ const modeFlag = proof ? '-p' : '-d';
182
+ const outFd = fs.openSync(generatedPath, 'w');
183
+
184
+ try {
185
+ if (builtin) {
186
+ const args = [eyelingJsPath, modeFlag];
187
+ if (rdfMode) args.push('-r');
188
+ args.push('--builtin', builtin.rel, path.join('examples', file));
189
+ if (trigInput) args.push(path.join('examples', trigInput.rel));
190
+ return cp.spawnSync(nodePath, args, {
191
+ cwd: root,
192
+ stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
193
+ maxBuffer: 200 * 1024 * 1024,
194
+ encoding: 'utf8',
195
+ });
196
+ }
197
+
198
+ const args = [eyelingJsPath, modeFlag];
199
+ if (rdfMode) args.push('-r');
200
+ args.push(file);
201
+ if (trigInput) args.push(trigInput.rel);
202
+ return cp.spawnSync(nodePath, args, {
203
+ cwd: examplesDir,
204
+ stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
205
+ maxBuffer: 200 * 1024 * 1024,
206
+ encoding: 'utf8',
207
+ });
208
+ } finally {
209
+ fs.closeSync(outFd);
210
+ }
211
+ }
212
+
144
213
  function main() {
145
214
  const suiteStart = Date.now();
146
215
 
@@ -148,6 +217,7 @@ function main() {
148
217
  const root = path.resolve(__dirname, '..');
149
218
  const examplesDir = path.join(root, 'examples');
150
219
  const outputDir = path.join(examplesDir, 'output');
220
+ const proofDir = path.join(examplesDir, 'proof');
151
221
  const eyelingJsPath = path.join(root, 'eyeling.js');
152
222
  const nodePath = process.execPath;
153
223
 
@@ -164,8 +234,15 @@ function main() {
164
234
  .readdirSync(examplesDir)
165
235
  .filter((f) => f.endsWith('.n3'))
166
236
  .sort((a, b) => a.localeCompare(b));
167
-
168
- info(`Running ${files.length} examples tests`);
237
+ const proofFiles = fs.existsSync(proofDir)
238
+ ? fs
239
+ .readdirSync(proofDir)
240
+ .filter((f) => f.endsWith('.n3') && fs.existsSync(path.join(examplesDir, f)))
241
+ .sort((a, b) => a.localeCompare(b))
242
+ : [];
243
+ const totalTests = files.length + proofFiles.length;
244
+
245
+ info(`Running ${files.length} examples tests and ${proofFiles.length} proof golden tests`);
169
246
  console.log(`${C.dim}${getEyelingVersion(nodePath, eyelingJsPath, root)}; node ${process.version}${C.n}`);
170
247
 
171
248
  if (files.length === 0) {
@@ -178,6 +255,7 @@ function main() {
178
255
 
179
256
  // Pretty, stable numbering (e.g., 001..100 when running 100 tests)
180
257
  const idxWidth = String(files.length).length;
258
+ const proofIdxWidth = String(Math.max(proofFiles.length, 1)).length;
181
259
 
182
260
  for (let i = 0; i < files.length; i++) {
183
261
  const idx = String(i + 1).padStart(idxWidth, '0');
@@ -221,38 +299,7 @@ function main() {
221
299
  // node eyeling.js --builtin examples/builtin/foo.js examples/foo.n3
222
300
  // A matching examples/input/<stem>.trig sidecar is external RDF/TriG
223
301
  // evidence for this example, so include it and run in -r mode automatically.
224
- const builtin = resolveExampleBuiltinPath(root, file);
225
- const trigInput = resolveExampleTrigInput(root, file);
226
- const rdfMode = !!trigInput;
227
- const outFd = fs.openSync(generatedPath, 'w');
228
- let r;
229
- try {
230
- if (builtin) {
231
- const args = [eyelingJsPath, '-d'];
232
- if (rdfMode) args.push('-r');
233
- args.push('--builtin', builtin.rel, path.join('examples', file));
234
- if (trigInput) args.push(path.join('examples', trigInput.rel));
235
- r = cp.spawnSync(nodePath, args, {
236
- cwd: root,
237
- stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
238
- maxBuffer: 200 * 1024 * 1024,
239
- encoding: 'utf8',
240
- });
241
- } else {
242
- const args = [eyelingJsPath, '-d'];
243
- if (rdfMode) args.push('-r');
244
- args.push(file);
245
- if (trigInput) args.push(trigInput.rel);
246
- r = cp.spawnSync(nodePath, args, {
247
- cwd: examplesDir,
248
- stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
249
- maxBuffer: 200 * 1024 * 1024,
250
- encoding: 'utf8',
251
- });
252
- }
253
- } finally {
254
- fs.closeSync(outFd);
255
- }
302
+ const r = runExampleToFile({ root, examplesDir, eyelingJsPath, nodePath, file, generatedPath });
256
303
 
257
304
  const rc = r.status == null ? 1 : r.status;
258
305
 
@@ -261,12 +308,7 @@ function main() {
261
308
  // Compare output. N3 outputs are order-insensitive; Markdown outputs are order-sensitive.
262
309
  let diffOk = false;
263
310
  try {
264
- const expectedText = fs.readFileSync(expectedPath, 'utf8');
265
- const generatedText = fs.readFileSync(generatedPath, 'utf8');
266
- if (expectedText == null) throw new Error('missing expected output');
267
- diffOk =
268
- normalizeForCompare(expectedText, expectedPath) ===
269
- normalizeForCompare(generatedText, expectedPath);
311
+ diffOk = compareGeneratedOutput({ expectedPath, generatedPath, sourceText: n3Text, proof: false });
270
312
  } catch {
271
313
  diffOk = false;
272
314
  }
@@ -302,15 +344,76 @@ function main() {
302
344
  if (tmpDir) rmrf(tmpDir);
303
345
  }
304
346
 
347
+
348
+ for (let i = 0; i < proofFiles.length; i++) {
349
+ const idx = String(i + 1).padStart(proofIdxWidth, '0');
350
+ const file = proofFiles[i];
351
+ const start = Date.now();
352
+ const filePath = path.join(examplesDir, file);
353
+ const expectedPath = path.join(proofDir, file);
354
+
355
+ let n3Text;
356
+ try {
357
+ n3Text = fs.readFileSync(filePath, 'utf8');
358
+ } catch (e) {
359
+ const ms = Date.now() - start;
360
+ fail(`proof ${idx} ${file} ${msTag(ms)}`);
361
+ fail(`Cannot read proof input: ${e.message}`);
362
+ failed++;
363
+ continue;
364
+ }
365
+
366
+ const expectedRc = expectedExitCode(n3Text);
367
+ const tmpDir = mkTmpDir();
368
+ const generatedPath = path.join(tmpDir, 'generated.n3');
369
+ const r = runExampleToFile({ root, examplesDir, eyelingJsPath, nodePath, file, generatedPath, proof: true });
370
+ const rc = r.status == null ? 1 : r.status;
371
+ const ms = Date.now() - start;
372
+
373
+ let diffOk = false;
374
+ try {
375
+ diffOk = compareGeneratedOutput({ expectedPath, generatedPath, sourceText: n3Text, proof: true });
376
+ } catch {
377
+ diffOk = false;
378
+ }
379
+
380
+ const rcOk = rc === expectedRc;
381
+
382
+ if (diffOk && rcOk) {
383
+ if (expectedRc === 0) {
384
+ ok(`proof ${idx} ${file} ${msTag(ms)}`);
385
+ } else {
386
+ ok(`proof ${idx} ${file} (expected exit ${expectedRc}) ${msTag(ms)}`);
387
+ }
388
+ passed++;
389
+ } else {
390
+ fail(`proof ${idx} ${file} ${msTag(ms)}`);
391
+ if (!rcOk) {
392
+ fail(`Exit code ${rc}, expected ${expectedRc}`);
393
+ }
394
+ if (!diffOk) {
395
+ fail('Proof output differs');
396
+ }
397
+ showDiff({
398
+ examplesDir,
399
+ expectedPath,
400
+ generatedPath,
401
+ });
402
+ failed++;
403
+ }
404
+
405
+ if (tmpDir) rmrf(tmpDir);
406
+ }
407
+
305
408
  console.log('');
306
409
  const suiteMs = Date.now() - suiteStart;
307
410
  info(`Total elapsed: ${suiteMs} ms (${(suiteMs / 1000).toFixed(2)} s)`);
308
411
 
309
412
  if (failed === 0) {
310
- ok(`All examples tests passed (${passed}/${files.length})`);
413
+ ok(`All examples tests passed (${passed}/${totalTests})`);
311
414
  process.exit(0);
312
415
  } else {
313
- fail(`Some examples tests failed (${passed}/${files.length})`);
416
+ fail(`Some examples tests failed (${passed}/${totalTests})`);
314
417
  // keep exit code 2 (matches historical behavior of examples/test)
315
418
  process.exit(2);
316
419
  }