eyeling 1.28.6 → 1.28.8

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/lib/builtins.js CHANGED
@@ -1168,7 +1168,7 @@ function parseIntegerDatatypeValue(lexical, dt) {
1168
1168
  }
1169
1169
 
1170
1170
  function parseDecimalDatatypeValue(lexical) {
1171
- const parsed = parseXsdDecimalToBigIntScale(String(lexical));
1171
+ const parsed = parseXsdDecimalToBigIntScale(String(lexical), { trim: false });
1172
1172
  if (!parsed) return null;
1173
1173
  const canonicalLex = canonicalDecimalLexFromParts(parsed.num, parsed.scale);
1174
1174
  return {
@@ -1294,58 +1294,93 @@ function parseAnyUriDatatypeValue(lexical) {
1294
1294
  };
1295
1295
  }
1296
1296
 
1297
- function isLeapYearNumber(year) {
1298
- if (year % 400 === 0) return true;
1299
- if (year % 100 === 0) return false;
1300
- return year % 4 === 0;
1297
+ function isLeapYearBigInt(year) {
1298
+ if (year % 400n === 0n) return true;
1299
+ if (year % 100n === 0n) return false;
1300
+ return year % 4n === 0n;
1301
1301
  }
1302
1302
 
1303
- function daysInMonthNumber(year, month) {
1304
- if (month === 2) return isLeapYearNumber(year) ? 29 : 28;
1303
+ function daysInMonthBigInt(year, month) {
1304
+ if (month === 2) return isLeapYearBigInt(year) ? 29 : 28;
1305
1305
  return [4, 6, 9, 11].includes(month) ? 30 : 31;
1306
1306
  }
1307
1307
 
1308
- function floorDivNumber(a, b) {
1309
- return Math.floor(a / b);
1308
+ function floorDivBigInt(a, b) {
1309
+ let q = a / b;
1310
+ const r = a % b;
1311
+ if (r !== 0n && ((r > 0n) !== (b > 0n))) q -= 1n;
1312
+ return q;
1310
1313
  }
1311
1314
 
1312
- function daysFromCivilNumber(year, month, day) {
1315
+ function daysFromCivilBigInt(year, month, day) {
1313
1316
  let y = year;
1314
- y -= month <= 2 ? 1 : 0;
1315
- const era = floorDivNumber(y, 400);
1316
- const yoe = y - era * 400;
1317
- const mp = month + (month > 2 ? -3 : 9);
1318
- const doy = Math.floor((153 * mp + 2) / 5) + day - 1;
1319
- const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;
1320
- return era * 146097 + doe - 719468;
1317
+ if (month <= 2) y -= 1n;
1318
+ const era = floorDivBigInt(y, 400n);
1319
+ const yoe = y - era * 400n;
1320
+ const mp = BigInt(month + (month > 2 ? -3 : 9));
1321
+ const doy = (153n * mp + 2n) / 5n + BigInt(day) - 1n;
1322
+ const doe = yoe * 365n + yoe / 4n - yoe / 100n + doy;
1323
+ return era * 146097n + doe - 719468n;
1324
+ }
1325
+
1326
+ function civilFromDaysBigInt(days) {
1327
+ const z = days + 719468n;
1328
+ const era = floorDivBigInt(z, 146097n);
1329
+ const doe = z - era * 146097n;
1330
+ const yoe = (doe - doe / 1460n + doe / 36524n - doe / 146096n) / 365n;
1331
+ let year = yoe + era * 400n;
1332
+ const doy = doe - (365n * yoe + yoe / 4n - yoe / 100n);
1333
+ const mp = (5n * doy + 2n) / 153n;
1334
+ const day = doy - (153n * mp + 2n) / 5n + 1n;
1335
+ const month = mp < 10n ? mp + 3n : mp - 9n;
1336
+ if (month <= 2n) year += 1n;
1337
+ return { year, month: Number(month), day: Number(day) };
1338
+ }
1339
+
1340
+ function isValidXsdYearLex(s) {
1341
+ const body = s.startsWith('-') ? s.slice(1) : s;
1342
+ if (!/^\d{4,}$/.test(body)) return false;
1343
+ return body.length === 4 || body[0] !== '0';
1344
+ }
1345
+
1346
+ function canonicalYearLex(year) {
1347
+ if (year < 0n) return `-${(-year).toString().padStart(4, '0')}`;
1348
+ return year.toString().padStart(4, '0');
1349
+ }
1350
+
1351
+ function formatDateTimeLocalKey(year, month, day, hour, minute, second, frac) {
1352
+ return `${canonicalYearLex(year)}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}${frac ? `.${frac}` : ''}`;
1321
1353
  }
1322
1354
 
1323
1355
  function parseDateTimeDatatypeValue(lexical, dt) {
1324
1356
  const s = String(lexical);
1325
1357
  const m = /^(-?\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?$/.exec(s);
1326
- if (!m) return null;
1358
+ if (!m || !isValidXsdYearLex(m[1])) return null;
1327
1359
 
1328
- const year = Number(m[1]);
1329
- const month = Number(m[2]);
1360
+ const year = BigInt(m[1]);
1361
+ let month = Number(m[2]);
1330
1362
  let day = Number(m[3]);
1331
1363
  let hour = Number(m[4]);
1332
1364
  const minute = Number(m[5]);
1333
1365
  const second = Number(m[6]);
1334
1366
  let frac = m[7] || '';
1335
1367
  const tz = m[8] || null;
1336
- if (!Number.isSafeInteger(year)) return null;
1337
1368
  if (dt === XSD_DATETIME_STAMP_DT && !tz) return null;
1338
1369
  if (!(month >= 1 && month <= 12)) return null;
1339
- if (!(day >= 1 && day <= daysInMonthNumber(year, month))) return null;
1370
+ if (!(day >= 1 && day <= daysInMonthBigInt(year, month))) return null;
1340
1371
  if (!(minute >= 0 && minute <= 59)) return null;
1341
1372
  if (!(second >= 0 && second <= 59)) return null;
1373
+
1374
+ let dayCount = daysFromCivilBigInt(year, month, day);
1375
+ let normalizedYear = year;
1342
1376
  if (hour === 24) {
1343
1377
  if (minute !== 0 || second !== 0 || /[1-9]/.test(frac)) return null;
1344
1378
  hour = 0;
1345
- const d = new Date(Date.UTC(year, month - 1, day));
1346
- d.setUTCFullYear(year);
1347
- d.setUTCDate(d.getUTCDate() + 1);
1348
- day = d.getUTCDate();
1379
+ dayCount += 1n;
1380
+ const civil = civilFromDaysBigInt(dayCount);
1381
+ normalizedYear = civil.year;
1382
+ month = civil.month;
1383
+ day = civil.day;
1349
1384
  } else if (!(hour >= 0 && hour <= 23)) {
1350
1385
  return null;
1351
1386
  }
@@ -1368,11 +1403,10 @@ function parseDateTimeDatatypeValue(lexical, dt) {
1368
1403
  }
1369
1404
  }
1370
1405
 
1371
- const dayCount = daysFromCivilNumber(year, month, day);
1372
- let totalSeconds = BigInt(dayCount) * 86400n + BigInt(hour * 3600 + minute * 60 + second);
1406
+ let totalSeconds = dayCount * 86400n + BigInt(hour * 3600 + minute * 60 + second);
1373
1407
  if (offsetMinutes !== null) totalSeconds -= BigInt(offsetMinutes * 60);
1374
1408
 
1375
- const localKey = `${m[1]}-${m[2]}-${String(day).padStart(2, '0')}T${String(hour).padStart(2, '0')}:${m[5]}:${m[6]}${frac ? `.${frac}` : ''}`;
1409
+ const localKey = formatDateTimeLocalKey(normalizedYear, month, day, hour, minute, second, frac);
1376
1410
  const canonicalLex = canonicalDateTimeLex(totalSeconds, fracNum, fracScale, offsetMinutes !== null, localKey);
1377
1411
  return {
1378
1412
  family: 'dateTime',
@@ -1392,15 +1426,14 @@ function canonicalDateTimeLex(totalSeconds, fracNum, fracScale, hasTimezone, loc
1392
1426
  const frac = fracScale > 0 ? `.${fracNum.toString().padStart(fracScale, '0')}` : '';
1393
1427
  if (!hasTimezone) return localKey;
1394
1428
 
1395
- const minSafeSeconds = BigInt(Math.ceil(Number.MIN_SAFE_INTEGER / 1000));
1396
- const maxSafeSeconds = BigInt(Math.floor(Number.MAX_SAFE_INTEGER / 1000));
1397
- if (totalSeconds < minSafeSeconds || totalSeconds > maxSafeSeconds) return `${localKey}Z`;
1398
-
1399
- const d = new Date(Number(totalSeconds) * 1000);
1400
- if (Number.isNaN(d.getTime())) return `${localKey}Z`;
1401
- let iso = d.toISOString().replace(/\.000Z$/, 'Z');
1402
- if (frac) iso = iso.replace('Z', `${frac}Z`);
1403
- return iso;
1429
+ const secondsPerDay = 86400n;
1430
+ const dayCount = floorDivBigInt(totalSeconds, secondsPerDay);
1431
+ const secondOfDay = totalSeconds - dayCount * secondsPerDay;
1432
+ const civil = civilFromDaysBigInt(dayCount);
1433
+ const hour = Number(secondOfDay / 3600n);
1434
+ const minute = Number((secondOfDay % 3600n) / 60n);
1435
+ const second = Number(secondOfDay % 60n);
1436
+ return formatDateTimeLocalKey(civil.year, civil.month, civil.day, hour, minute, second, frac) + 'Z';
1404
1437
  }
1405
1438
 
1406
1439
  function parseLangStringDatatypeValue(t, lexical) {
@@ -1518,7 +1551,31 @@ function evalDatatypeInspectionBuiltin(g, subst, kind) {
1518
1551
  return evalBindBuiltinObject(g.o, valueTerm, subst);
1519
1552
  }
1520
1553
 
1554
+ function booleanTermValue(t) {
1555
+ const info = parseBooleanLiteralInfo(t);
1556
+ return info ? info.value : null;
1557
+ }
1558
+
1559
+ function makeBooleanLiteral(value) {
1560
+ return internLiteral(value ? 'true' : 'false');
1561
+ }
1562
+
1563
+ function evalDatatypeValidityBooleanResult(outTerm, result, subst) {
1564
+ if (outTerm instanceof Var || outTerm instanceof Blank) return evalBindBuiltinObject(outTerm, makeBooleanLiteral(result), subst);
1565
+ const expected = booleanTermValue(outTerm);
1566
+ return expected === result ? [__cloneSubst(subst)] : [];
1567
+ }
1568
+
1521
1569
  function evalDatatypeValidityBuiltin(g, subst, positive) {
1570
+ if (g.s instanceof ListTerm && g.s.elems.length === 2) {
1571
+ const [lit, dtTerm] = g.s.elems;
1572
+ if (!(lit instanceof Literal) || !(dtTerm instanceof Iri)) return [];
1573
+ const dt = dtTerm.value;
1574
+ if (!isSupportedDatatypeIri(dt)) return [];
1575
+ const ok = parseDatatypeValueForDatatype(lit, dt) !== null;
1576
+ return evalDatatypeValidityBooleanResult(g.o, positive ? ok : !ok, subst);
1577
+ }
1578
+
1522
1579
  if (!(g.s instanceof Literal)) return [];
1523
1580
 
1524
1581
  let dt = null;
@@ -1712,8 +1769,9 @@ function formatNum(n) {
1712
1769
  return String(n);
1713
1770
  }
1714
1771
 
1715
- function parseXsdDecimalToBigIntScale(s) {
1716
- let t = String(s).trim();
1772
+ function parseXsdDecimalToBigIntScale(s, options = null) {
1773
+ let t = String(s);
1774
+ if (!options || options.trim !== false) t = t.trim();
1717
1775
  let sign = 1n;
1718
1776
 
1719
1777
  if (t.startsWith('+')) t = t.slice(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.28.6",
3
+ "version": "1.28.8",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -66,29 +66,12 @@ function reasonQuiet(opt, input) {
66
66
  throw err;
67
67
  }
68
68
 
69
- const TTY = process.stdout.isTTY;
70
- const C = TTY
71
- ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
72
- : { g: '', r: '', y: '', dim: '', n: '' };
73
-
74
- function ok(msg) {
75
- console.log(`${C.g}OK ${C.n} ${msg}`);
76
- }
77
- function info(msg) {
78
- console.log(`${C.y}==${C.n} ${msg}`);
79
- }
80
- function fail(msg) {
81
- console.error(`${C.r}FAIL${C.n} ${msg}`);
82
- }
69
+ const { detail, failResult, info, pass } = require('./report');
83
70
 
84
71
  function unnumberedName(name) {
85
72
  return String(name).replace(/^\d+[a-z]*\s+/i, '');
86
73
  }
87
74
 
88
- function numberedName(index, name) {
89
- return `${String(index + 1).padStart(3, '0')} ${unnumberedName(name)}`;
90
- }
91
-
92
75
  function msNow() {
93
76
  return Date.now();
94
77
  }
@@ -3172,7 +3155,8 @@ let failed = 0;
3172
3155
  info(`Running ${cases.length} API tests (independent of examples/)`);
3173
3156
 
3174
3157
  for (const [index, tc] of cases.entries()) {
3175
- const testName = numberedName(index, tc.name);
3158
+ const testNr = index + 1;
3159
+ const testName = unnumberedName(tc.name);
3176
3160
  const start = msNow();
3177
3161
  try {
3178
3162
  const out = typeof tc.run === 'function' ? await tc.run() : reasonQuiet(tc.opt, tc.input);
@@ -3187,19 +3171,19 @@ let failed = 0;
3187
3171
  if (typeof tc.check === 'function') tc.check(out, tc);
3188
3172
 
3189
3173
  const dur = msNow() - start;
3190
- ok(`${testName} ${C.dim}(${dur} ms)${C.n}`);
3174
+ pass(testNr, testName, dur);
3191
3175
  passed++;
3192
3176
  } catch (e) {
3193
3177
  const dur = msNow() - start;
3194
3178
 
3195
3179
  if (tc.expectErrorCode != null) {
3196
3180
  if (e && typeof e === 'object' && 'code' in e && e.code === tc.expectErrorCode) {
3197
- ok(`${testName} ${C.dim}(expected exit ${tc.expectErrorCode}, ${dur} ms)${C.n}`);
3181
+ pass(testNr, `${testName} (expected exit ${tc.expectErrorCode})`, dur);
3198
3182
  passed++;
3199
3183
  continue;
3200
3184
  }
3201
- fail(`${testName} ${C.dim}(${dur} ms)${C.n}`);
3202
- fail(
3185
+ failResult(testNr, testName, dur);
3186
+ detail(
3203
3187
  `Expected exit code ${tc.expectErrorCode}, got: ${e && e.code != null ? e.code : 'unknown'}\n${
3204
3188
  e && e.stderr ? e.stderr : e && e.stack ? e.stack : String(e)
3205
3189
  }`,
@@ -3209,26 +3193,26 @@ let failed = 0;
3209
3193
  }
3210
3194
 
3211
3195
  if (tc.expectError) {
3212
- ok(`${testName} ${C.dim}(expected error, ${dur} ms)${C.n}`);
3196
+ pass(testNr, `${testName} (expected error)`, dur);
3213
3197
  passed++;
3214
3198
  continue;
3215
3199
  }
3216
3200
 
3217
- fail(`${testName} ${C.dim}(${dur} ms)${C.n}`);
3218
- fail(e && e.stack ? e.stack : String(e));
3201
+ failResult(testNr, testName, dur);
3202
+ detail(e && e.stack ? e.stack : String(e));
3219
3203
  failed++;
3220
3204
  }
3221
3205
  }
3222
3206
 
3223
3207
  console.log('');
3224
3208
  const suiteMs = Date.now() - suiteStart;
3225
- console.log(`${C.y}==${C.n} Total elapsed: ${suiteMs} ms (${(suiteMs / 1000).toFixed(2)} s)`);
3209
+ info(`Total elapsed: ${suiteMs} ms (${(suiteMs / 1000).toFixed(2)} s)`);
3226
3210
 
3227
3211
  if (failed === 0) {
3228
- ok(`All API tests passed (${passed}/${cases.length})`);
3212
+ info(`All API tests passed (${passed}/${cases.length})`);
3229
3213
  process.exit(0);
3230
3214
  } else {
3231
- fail(`Some API tests failed (${passed}/${cases.length})`);
3215
+ info(`Some API tests failed (${passed}/${cases.length})`);
3232
3216
  process.exit(1);
3233
3217
  }
3234
3218
  })();
@@ -2,20 +2,7 @@
2
2
 
3
3
  const assert = require('node:assert/strict');
4
4
 
5
- const TTY = process.stdout.isTTY;
6
- const C = TTY
7
- ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
8
- : { g: '', r: '', y: '', dim: '', n: '' };
9
-
10
- function ok(msg) {
11
- console.log(`${C.g}OK ${C.n} ${msg}`);
12
- }
13
- function info(msg) {
14
- console.log(`${C.y}==${C.n} ${msg}`);
15
- }
16
- function fail(msg) {
17
- console.error(`${C.r}FAIL${C.n} ${msg}`);
18
- }
5
+ const { detail, failResult, info, pass } = require('./report');
19
6
 
20
7
  const builtins = require('../lib/builtins');
21
8
  require('../lib/engine');
@@ -183,17 +170,25 @@ const cases = [
183
170
  { "2147483648"^^xsd:int dt:invalidForDatatype xsd:int . } => { :invalid :int true } .
184
171
  { "2"^^xsd:boolean dt:invalidForDatatype xsd:boolean . } => { :invalid :boolean true } .
185
172
  { "2026-02-31T00:00:00Z"^^xsd:dateTime dt:invalidForDatatype xsd:dateTime . } => { :invalid :dateTime true } .
173
+ { " 1.0 "^^xsd:decimal dt:invalidForDatatype xsd:decimal . } => { :invalid :decimalWhitespace true } .
174
+ { "02026-06-10T12:00:00Z"^^xsd:dateTime dt:invalidForDatatype xsd:dateTime . } => { :invalid :dateTimeYear true } .
186
175
 
187
176
  { "01"^^xsd:integer dt:sameValueAs "1.0"^^xsd:decimal . } => { :same :numeric true } .
188
177
  { "true"^^xsd:boolean dt:sameValueAs "1"^^xsd:boolean . } => { :same :boolean true } .
189
178
  { "2026-06-10T12:00:00Z"^^xsd:dateTime dt:sameValueAs "2026-06-10T14:00:00+02:00"^^xsd:dateTime . } => { :same :dateTime true } .
179
+ { "2026-12-31T24:00:00Z"^^xsd:dateTime dt:sameValueAs "2027-01-01T00:00:00Z"^^xsd:dateTime . } => { :same :midnightRollover true } .
190
180
  { "AQID"^^xsd:base64Binary dt:sameValueAs "010203"^^xsd:hexBinary . } => { :same :binary true } .
191
181
  { "11"^^xsd:integer dt:differentValueFrom "12"^^xsd:integer . } => { :different :numeric true } .
192
182
 
183
+ { ("1"^^xsd:integer xsd:integer) dt:validForDatatype true . } => { :tuple :valid true } .
184
+ { ("abc"^^xsd:integer xsd:integer) dt:validForDatatype false . } => { :tuple :invalidBoolean true } .
185
+ { ("abc"^^xsd:integer xsd:integer) dt:invalidForDatatype ?invalid . } => { :tuple :invalidResult ?invalid } .
186
+
193
187
  { "01"^^xsd:integer dt:canonicalLiteral ?ci . } => { :canonical :integer ?ci } .
194
188
  { "1"^^xsd:boolean dt:canonicalLiteral ?cb . } => { :canonical :boolean ?cb } .
195
189
  { " a\t b "^^xsd:token dt:canonicalLiteral ?ct . } => { :canonical :token ?ct } .
196
190
  { "2026-06-10T14:00:00+02:00"^^xsd:dateTime dt:canonicalLiteral ?cd . } => { :canonical :dateTime ?cd } .
191
+ { "2026-12-31T24:00:00Z"^^xsd:dateTime dt:canonicalLiteral ?cm . } => { :canonical :midnightRollover ?cm } .
197
192
  `);
198
193
 
199
194
  assert.match(out, /:integer :datatype xsd:integer \./);
@@ -205,15 +200,22 @@ const cases = [
205
200
  assert.match(out, /:invalid :int true \./);
206
201
  assert.match(out, /:invalid :boolean true \./);
207
202
  assert.match(out, /:invalid :dateTime true \./);
203
+ assert.match(out, /:invalid :decimalWhitespace true \./);
204
+ assert.match(out, /:invalid :dateTimeYear true \./);
208
205
  assert.match(out, /:same :numeric true \./);
209
206
  assert.match(out, /:same :boolean true \./);
210
207
  assert.match(out, /:same :dateTime true \./);
208
+ assert.match(out, /:same :midnightRollover true \./);
211
209
  assert.match(out, /:same :binary true \./);
212
210
  assert.match(out, /:different :numeric true \./);
211
+ assert.match(out, /:tuple :valid true \./);
212
+ assert.match(out, /:tuple :invalidBoolean true \./);
213
+ assert.match(out, /:tuple :invalidResult true \./);
213
214
  assert.match(out, /:canonical :integer "1"\^\^xsd:integer \./);
214
215
  assert.match(out, /:canonical :boolean true \./);
215
216
  assert.match(out, /:canonical :token "a b"\^\^xsd:token \./);
216
217
  assert.match(out, /:canonical :dateTime "2026-06-10T12:00:00Z"\^\^xsd:dateTime \./);
218
+ assert.match(out, /:canonical :midnightRollover "2027-01-01T00:00:00Z"\^\^xsd:dateTime \./);
217
219
  },
218
220
  },
219
221
 
@@ -226,25 +228,25 @@ let failed = 0;
226
228
  const suiteStart = Date.now();
227
229
  info(`Running ${cases.length} builtin contract tests`);
228
230
 
229
- for (const tc of cases) {
231
+ for (const [index, tc] of cases.entries()) {
230
232
  const start = Date.now();
231
233
  try {
232
234
  tc.run();
233
- ok(`${tc.name} ${C.dim}(${Date.now() - start} ms)${C.n}`);
235
+ pass(index + 1, tc.name, Date.now() - start);
234
236
  passed++;
235
237
  } catch (e) {
236
- fail(`${tc.name} ${C.dim}(${Date.now() - start} ms)${C.n}`);
237
- fail(e && e.stack ? e.stack : String(e));
238
+ failResult(index + 1, tc.name, Date.now() - start);
239
+ detail(e && e.stack ? e.stack : String(e));
238
240
  failed++;
239
241
  }
240
242
  }
241
243
 
242
244
  console.log('');
243
- console.log(`${C.y}==${C.n} Total elapsed: ${Date.now() - suiteStart} ms`);
245
+ info(`Total elapsed: ${Date.now() - suiteStart} ms`);
244
246
  if (failed === 0) {
245
- ok(`All builtin contract tests passed (${passed}/${cases.length})`);
247
+ info(`All builtin contract tests passed (${passed}/${cases.length})`);
246
248
  process.exit(0);
247
249
  }
248
- fail(`Some builtin contract tests failed (${passed}/${cases.length})`);
250
+ info(`Some builtin contract tests failed (${passed}/${cases.length})`);
249
251
  process.exit(1);
250
252
  })();
@@ -6,21 +6,7 @@ const os = require('node:os');
6
6
  const path = require('node:path');
7
7
  const cp = require('node:child_process');
8
8
 
9
- const TTY = process.stdout.isTTY;
10
- const C = TTY
11
- ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
12
- : { g: '', r: '', y: '', dim: '', n: '' };
13
- const msTag = (ms) => `${C.dim}(${ms} ms)${C.n}`;
14
-
15
- function ok(msg) {
16
- console.log(`${C.g}OK${C.n} ${msg}`);
17
- }
18
- function fail(msg) {
19
- console.error(`${C.r}FAIL${C.n} ${msg}`);
20
- }
21
- function info(msg) {
22
- console.log(`${C.y}==${C.n} ${msg}`);
23
- }
9
+ const { C, detail, failResult, info, pass } = require('./report');
24
10
 
25
11
  function run(cmd, args, opts = {}) {
26
12
  return cp.spawnSync(cmd, args, {
@@ -224,11 +210,11 @@ function main() {
224
210
  const nodePath = process.execPath;
225
211
 
226
212
  if (!fs.existsSync(examplesDir)) {
227
- fail(`Cannot find examples directory: ${examplesDir}`);
213
+ failResult(1, `Cannot find examples directory: ${examplesDir}`, 0);
228
214
  process.exit(1);
229
215
  }
230
216
  if (!fs.existsSync(eyelingJsPath)) {
231
- fail(`Cannot find eyeling.js: ${eyelingJsPath}`);
217
+ failResult(1, `Cannot find eyeling.js: ${eyelingJsPath}`, 0);
232
218
  process.exit(1);
233
219
  }
234
220
 
@@ -252,21 +238,19 @@ function main() {
252
238
  console.log(`${C.dim}${getEyelingVersion(nodePath, eyelingJsPath, root)}; node ${process.version}${C.n}`);
253
239
 
254
240
  if (totalTests === 0) {
255
- ok(proofOnly ? 'No proof goldens found in examples/proof/' : 'No .n3 files found in examples/');
241
+ info(proofOnly ? 'No proof goldens found in examples/proof/' : 'No .n3 files found in examples/');
256
242
  process.exit(0);
257
243
  }
258
244
 
259
245
  let passed = 0;
260
246
  let failed = 0;
261
247
 
262
- // Pretty, stable numbering (e.g., 001..100 when running 100 tests)
263
- const idxWidth = String(files.length).length;
264
- const proofIdxWidth = String(Math.max(proofFiles.length, 1)).length;
265
- const proofLabel = proofOnly ? '' : 'proof ';
248
+ let sequence = 0;
266
249
 
267
250
  for (let i = 0; i < files.length; i++) {
268
- const idx = String(i + 1).padStart(idxWidth, '0');
251
+ const testNr = ++sequence;
269
252
  const file = files[i];
253
+ const testName = file;
270
254
 
271
255
  const start = Date.now();
272
256
 
@@ -278,8 +262,8 @@ function main() {
278
262
  n3Text = fs.readFileSync(filePath, 'utf8');
279
263
  } catch (e) {
280
264
  const ms = Date.now() - start;
281
- fail(`${idx} ${file} ${msTag(ms)}`);
282
- fail(`Cannot read input: ${e.message}`);
265
+ failResult(testNr, testName, ms);
266
+ detail(`Cannot read input: ${e.message}`);
283
267
  failed++;
284
268
  continue;
285
269
  }
@@ -291,8 +275,8 @@ function main() {
291
275
  // comparable across environments.
292
276
  if (!fs.existsSync(expectedPath)) {
293
277
  const ms = Date.now() - start;
294
- fail(`${idx} ${file} ${msTag(ms)}`);
295
- fail(`Missing expected output for ${path.basename(file, path.extname(file))}.*`);
278
+ failResult(testNr, testName, ms);
279
+ detail(`Missing expected output for ${path.basename(file, path.extname(file))}.*`);
296
280
  failed++;
297
281
  continue;
298
282
  }
@@ -324,18 +308,18 @@ function main() {
324
308
 
325
309
  if (diffOk && rcOk) {
326
310
  if (expectedRc === 0) {
327
- ok(`${idx} ${file} ${msTag(ms)}`);
311
+ pass(testNr, testName, ms);
328
312
  } else {
329
- ok(`${idx} ${file} (expected exit ${expectedRc}) ${msTag(ms)}`);
313
+ pass(testNr, `${testName} (expected exit ${expectedRc})`, ms);
330
314
  }
331
315
  passed++;
332
316
  } else {
333
- fail(`${idx} ${file} ${msTag(ms)}`);
317
+ failResult(testNr, testName, ms);
334
318
  if (!rcOk) {
335
- fail(`Exit code ${rc}, expected ${expectedRc}`);
319
+ detail(`Exit code ${rc}, expected ${expectedRc}`);
336
320
  }
337
321
  if (!diffOk) {
338
- fail('Output differs');
322
+ detail('Output differs');
339
323
  }
340
324
 
341
325
  // Show diffs, because this is a test runner
@@ -353,8 +337,9 @@ function main() {
353
337
 
354
338
 
355
339
  for (let i = 0; i < proofFiles.length; i++) {
356
- const idx = String(i + 1).padStart(proofIdxWidth, '0');
340
+ const testNr = ++sequence;
357
341
  const file = proofFiles[i];
342
+ const testName = proofOnly ? file : `proof ${file}`;
358
343
  const start = Date.now();
359
344
  const filePath = path.join(examplesDir, file);
360
345
  const expectedPath = path.join(proofDir, file);
@@ -364,8 +349,8 @@ function main() {
364
349
  n3Text = fs.readFileSync(filePath, 'utf8');
365
350
  } catch (e) {
366
351
  const ms = Date.now() - start;
367
- fail(`${proofLabel}${idx} ${file} ${msTag(ms)}`);
368
- fail(`Cannot read proof input: ${e.message}`);
352
+ failResult(testNr, testName, ms);
353
+ detail(`Cannot read proof input: ${e.message}`);
369
354
  failed++;
370
355
  continue;
371
356
  }
@@ -388,18 +373,18 @@ function main() {
388
373
 
389
374
  if (diffOk && rcOk) {
390
375
  if (expectedRc === 0) {
391
- ok(`${proofLabel}${idx} ${file} ${msTag(ms)}`);
376
+ pass(testNr, testName, ms);
392
377
  } else {
393
- ok(`${proofLabel}${idx} ${file} (expected exit ${expectedRc}) ${msTag(ms)}`);
378
+ pass(testNr, `${testName} (expected exit ${expectedRc})`, ms);
394
379
  }
395
380
  passed++;
396
381
  } else {
397
- fail(`${proofLabel}${idx} ${file} ${msTag(ms)}`);
382
+ failResult(testNr, testName, ms);
398
383
  if (!rcOk) {
399
- fail(`Exit code ${rc}, expected ${expectedRc}`);
384
+ detail(`Exit code ${rc}, expected ${expectedRc}`);
400
385
  }
401
386
  if (!diffOk) {
402
- fail('Proof output differs');
387
+ detail('Proof output differs');
403
388
  }
404
389
  showDiff({
405
390
  examplesDir,
@@ -417,12 +402,12 @@ function main() {
417
402
  info(`Total elapsed: ${suiteMs} ms (${(suiteMs / 1000).toFixed(2)} s)`);
418
403
 
419
404
  if (failed === 0) {
420
- ok(proofOnly
405
+ info(proofOnly
421
406
  ? `All proof golden tests passed (${passed}/${totalTests})`
422
407
  : `All examples tests passed (${passed}/${totalTests})`);
423
408
  process.exit(0);
424
409
  } else {
425
- fail(proofOnly
410
+ info(proofOnly
426
411
  ? `Some proof golden tests failed (${passed}/${totalTests})`
427
412
  : `Some examples tests failed (${passed}/${totalTests})`);
428
413
  // keep exit code 2 (matches historical behavior of examples/test)
@@ -5,21 +5,7 @@ const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
  const cp = require('node:child_process');
7
7
 
8
- const TTY = process.stdout.isTTY;
9
- const C = TTY
10
- ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
11
- : { g: '', r: '', y: '', dim: '', n: '' };
12
- const msTag = (ms) => `${C.dim}(${ms} ms)${C.n}`;
13
-
14
- function ok(msg) {
15
- console.log(`${C.g}OK${C.n} ${msg}`);
16
- }
17
- function fail(msg) {
18
- console.error(`${C.r}FAIL${C.n} ${msg}`);
19
- }
20
- function info(msg) {
21
- console.log(`${C.y}==${C.n} ${msg}`);
22
- }
8
+ const { C, detail, failResult, info, pass } = require('./report');
23
9
 
24
10
  function main() {
25
11
  const suiteStart = Date.now();
@@ -29,7 +15,7 @@ function main() {
29
15
  const nodePath = process.execPath;
30
16
 
31
17
  if (!fs.existsSync(extraDir)) {
32
- fail(`Cannot find examples/extra directory: ${extraDir}`);
18
+ failResult(1, `Cannot find examples/extra directory: ${extraDir}`, 0);
33
19
  process.exit(1);
34
20
  }
35
21
 
@@ -44,16 +30,14 @@ function main() {
44
30
  console.log(`${C.dim}node ${process.version}${C.n}`);
45
31
 
46
32
  if (files.length === 0) {
47
- ok('No .js files found in examples/extra/');
33
+ info('No .js files found in examples/extra/');
48
34
  process.exit(0);
49
35
  }
50
36
 
51
37
  let passed = 0;
52
38
  let failed = 0;
53
- const idxWidth = String(files.length).length;
54
-
55
39
  for (let i = 0; i < files.length; i += 1) {
56
- const idx = String(i + 1).padStart(idxWidth, '0');
40
+ const testNr = i + 1;
57
41
  const file = files[i];
58
42
  const start = Date.now();
59
43
 
@@ -74,11 +58,11 @@ function main() {
74
58
  const ms = Date.now() - start;
75
59
 
76
60
  if (rc === 0) {
77
- ok(`${idx} ${file} -> output/${path.basename(outputPath)} ${msTag(ms)}`);
61
+ pass(testNr, `${file} -> output/${path.basename(outputPath)}`, ms);
78
62
  passed += 1;
79
63
  } else {
80
- fail(`${idx} ${file} ${msTag(ms)}`);
81
- fail(`Exit code ${rc}`);
64
+ failResult(testNr, file, ms);
65
+ detail(`Exit code ${rc}`);
82
66
  if (r.stderr) process.stderr.write(r.stderr);
83
67
  failed += 1;
84
68
  }
@@ -89,11 +73,11 @@ function main() {
89
73
  info(`Total elapsed: ${suiteMs} ms (${(suiteMs / 1000).toFixed(2)} s)`);
90
74
 
91
75
  if (failed === 0) {
92
- ok(`All extra examples passed (${passed}/${files.length})`);
76
+ info(`All extra examples passed (${passed}/${files.length})`);
93
77
  process.exit(0);
94
78
  }
95
79
 
96
- fail(`Some extra examples failed (${passed}/${files.length})`);
80
+ info(`Some extra examples failed (${passed}/${files.length})`);
97
81
  process.exit(2);
98
82
  }
99
83