eyeling 1.26.7 → 1.27.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.
Files changed (31) hide show
  1. package/HANDBOOK.md +32 -1
  2. package/dist/browser/eyeling.browser.js +519 -3
  3. package/examples/deck/act-barley-seed-lineage.md +1 -1
  4. package/examples/deck/faltings-genus2-finiteness.md +1 -1
  5. package/examples/deck/high-trust-rdf-bloom-envelope.md +1 -1
  6. package/examples/deck/odrl-dpv-risk-ranked.md +1 -1
  7. package/examples/deck/rdf-message-cold-chain-recall.md +71 -0
  8. package/examples/deck/rdf-message-flow.md +1 -1
  9. package/examples/deck/rdf-message-ldes-incremental.md +94 -0
  10. package/examples/deck/rdf-message-window-repair.md +1 -1
  11. package/examples/deck/schema-foaf-mapping.md +2 -0
  12. package/examples/input/rdf-message-cold-chain-recall.trig +668 -0
  13. package/examples/input/rdf-message-flow.trig +3 -0
  14. package/examples/input/rdf-message-ldes-incremental.trig +564 -0
  15. package/examples/input/rdf-message-microgrid.trig +3 -0
  16. package/examples/input/rdf-message-window-repair.trig +3 -0
  17. package/examples/input/rdf-messages.trig +3 -0
  18. package/examples/output/rdf-message-cold-chain-recall.md +13 -0
  19. package/examples/output/rdf-message-ldes-incremental.md +13 -0
  20. package/examples/rdf-message-cold-chain-recall.n3 +229 -0
  21. package/examples/rdf-message-flow.n3 +3 -0
  22. package/examples/rdf-message-ldes-incremental.n3 +231 -0
  23. package/examples/rdf-message-microgrid.n3 +3 -0
  24. package/examples/rdf-message-window-repair.n3 +3 -0
  25. package/examples/rdf-messages.n3 +3 -0
  26. package/eyeling.js +519 -3
  27. package/lib/cli.js +519 -3
  28. package/package.json +5 -3
  29. package/test/examples.test.js +25 -14
  30. package/test/fixtures/marc-rules-stream-messages.n3 +192 -0
  31. package/test/stream_messages.test.js +284 -0
@@ -212,6 +212,8 @@ function runExampleToFile({ root, examplesDir, eyelingJsPath, nodePath, file, ge
212
212
 
213
213
  function main() {
214
214
  const suiteStart = Date.now();
215
+ const proofOnly = process.argv.includes('--proof-only');
216
+
215
217
 
216
218
  // test/examples.test.js -> repo root is one level up
217
219
  const root = path.resolve(__dirname, '..');
@@ -230,11 +232,13 @@ function main() {
230
232
  process.exit(1);
231
233
  }
232
234
 
233
- const files = fs
234
- .readdirSync(examplesDir)
235
- .filter((f) => f.endsWith('.n3'))
236
- .sort((a, b) => a.localeCompare(b));
237
- const proofFiles = fs.existsSync(proofDir)
235
+ const files = proofOnly
236
+ ? []
237
+ : fs
238
+ .readdirSync(examplesDir)
239
+ .filter((f) => f.endsWith('.n3'))
240
+ .sort((a, b) => a.localeCompare(b));
241
+ const proofFiles = proofOnly && fs.existsSync(proofDir)
238
242
  ? fs
239
243
  .readdirSync(proofDir)
240
244
  .filter((f) => f.endsWith('.n3') && fs.existsSync(path.join(examplesDir, f)))
@@ -242,11 +246,13 @@ function main() {
242
246
  : [];
243
247
  const totalTests = files.length + proofFiles.length;
244
248
 
245
- info(`Running ${files.length} examples tests and ${proofFiles.length} proof golden tests`);
249
+ info(proofOnly
250
+ ? `Running ${proofFiles.length} proof golden tests`
251
+ : `Running ${files.length} examples tests`);
246
252
  console.log(`${C.dim}${getEyelingVersion(nodePath, eyelingJsPath, root)}; node ${process.version}${C.n}`);
247
253
 
248
- if (files.length === 0) {
249
- ok('No .n3 files found in examples/');
254
+ if (totalTests === 0) {
255
+ ok(proofOnly ? 'No proof goldens found in examples/proof/' : 'No .n3 files found in examples/');
250
256
  process.exit(0);
251
257
  }
252
258
 
@@ -256,6 +262,7 @@ function main() {
256
262
  // Pretty, stable numbering (e.g., 001..100 when running 100 tests)
257
263
  const idxWidth = String(files.length).length;
258
264
  const proofIdxWidth = String(Math.max(proofFiles.length, 1)).length;
265
+ const proofLabel = proofOnly ? '' : 'proof ';
259
266
 
260
267
  for (let i = 0; i < files.length; i++) {
261
268
  const idx = String(i + 1).padStart(idxWidth, '0');
@@ -357,7 +364,7 @@ function main() {
357
364
  n3Text = fs.readFileSync(filePath, 'utf8');
358
365
  } catch (e) {
359
366
  const ms = Date.now() - start;
360
- fail(`proof ${idx} ${file} ${msTag(ms)}`);
367
+ fail(`${proofLabel}${idx} ${file} ${msTag(ms)}`);
361
368
  fail(`Cannot read proof input: ${e.message}`);
362
369
  failed++;
363
370
  continue;
@@ -381,13 +388,13 @@ function main() {
381
388
 
382
389
  if (diffOk && rcOk) {
383
390
  if (expectedRc === 0) {
384
- ok(`proof ${idx} ${file} ${msTag(ms)}`);
391
+ ok(`${proofLabel}${idx} ${file} ${msTag(ms)}`);
385
392
  } else {
386
- ok(`proof ${idx} ${file} (expected exit ${expectedRc}) ${msTag(ms)}`);
393
+ ok(`${proofLabel}${idx} ${file} (expected exit ${expectedRc}) ${msTag(ms)}`);
387
394
  }
388
395
  passed++;
389
396
  } else {
390
- fail(`proof ${idx} ${file} ${msTag(ms)}`);
397
+ fail(`${proofLabel}${idx} ${file} ${msTag(ms)}`);
391
398
  if (!rcOk) {
392
399
  fail(`Exit code ${rc}, expected ${expectedRc}`);
393
400
  }
@@ -410,10 +417,14 @@ function main() {
410
417
  info(`Total elapsed: ${suiteMs} ms (${(suiteMs / 1000).toFixed(2)} s)`);
411
418
 
412
419
  if (failed === 0) {
413
- ok(`All examples tests passed (${passed}/${totalTests})`);
420
+ ok(proofOnly
421
+ ? `All proof golden tests passed (${passed}/${totalTests})`
422
+ : `All examples tests passed (${passed}/${totalTests})`);
414
423
  process.exit(0);
415
424
  } else {
416
- fail(`Some examples tests failed (${passed}/${totalTests})`);
425
+ fail(proofOnly
426
+ ? `Some proof golden tests failed (${passed}/${totalTests})`
427
+ : `Some examples tests failed (${passed}/${totalTests})`);
417
428
  // keep exit code 2 (matches historical behavior of examples/test)
418
429
  process.exit(2);
419
430
  }
@@ -0,0 +1,192 @@
1
+ # MARC extraction rules for the RDF Messages demo (see marc-sample.messages.nt).
2
+ #
3
+ # Each message is one bibliographic record shaped as a nested list of MARC
4
+ # fields: <record> :record (("245" "1" "0" "a" "Title…") ("650" …) …), where an
5
+ # inner list is (tag ind1 ind2 code value code value …). These backward helper
6
+ # rules (marc:field / marc:subf / marc:map / marc:join / marc:id) walk that
7
+ # structure to pull a subfield's value by (tag, subfield-code); the forward
8
+ # rules at the bottom emit :title / :subject / :type per record.
9
+ #
10
+
11
+ @prefix : <http://example.org/ns#> .
12
+ @prefix list: <http://www.w3.org/2000/10/swap/list#> .
13
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
14
+ @prefix eymsg: <https://eyereasoner.github.io/eyeling/vocab/message#> .
15
+ @prefix string: <http://www.w3.org/2000/10/swap/string#> .
16
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
17
+ @prefix marc: <https://codeberg.org/phochste/marcattacks#> .
18
+
19
+ ## ---- Helper functions (backward rules) ----
20
+
21
+ # marc:splice - drop the first ?Idx members of a list (here: the tag + indicators)
22
+ { (?List ?Idx) marc:splice ?Result }
23
+ <=
24
+ {
25
+ ( ?X {
26
+ ?List list:iterate (?Num ?X).
27
+ ?Num math:notLessThan ?Idx.
28
+ } ?Result ) log:collectAllIn _:x.
29
+ }.
30
+
31
+ # marc:join - join a list with a separator
32
+ { (?List ?Sep) marc:join ?Result }
33
+ <=
34
+ {
35
+ (?List ?Sep "") marc:join ?Result.
36
+ }.
37
+
38
+ { ( () ?Sep ?Acc ) marc:join ?Acc }
39
+ <= true.
40
+
41
+ { ( ?List ?Sep ?Acc ) marc:join ?Result }
42
+ <=
43
+ {
44
+ ?List list:firstRest (?H ?T).
45
+ ?Acc log:equalTo "".
46
+ ( ?T ?Sep ?H ) marc:join ?Result .
47
+ }.
48
+
49
+ { ( ?List ?Sep ?Acc ) marc:join ?Result }
50
+ <=
51
+ {
52
+ ?List list:firstRest (?H ?T).
53
+ ?Acc log:notEqualTo "".
54
+ ( ?Acc ?Sep ?H) string:concatenation ?AccNew.
55
+ ( ?T ?Sep ?AccNew ) marc:join ?Result .
56
+ }.
57
+
58
+ # marc:id - return the record id as an IRI (from the 001 control field)
59
+ { ?Record marc:id ?Result }
60
+ <=
61
+ {
62
+ (?Record "001") marc:field0 ?F001.
63
+ ?F001 marc:ctrl ?ID.
64
+ ( "http://lib.ugent.be/record/" ?ID ) string:concatenation ?IRI_ID.
65
+ ?Result log:uri ?IRI_ID.
66
+ }.
67
+
68
+ # marc:ctrl - return the control value of a field
69
+ { ?Field marc:ctrl ?Result }
70
+ <=
71
+ {
72
+ (?Field 3) list:memberAt "_" .
73
+ (?Field 4) list:memberAt ?Result.
74
+ }.
75
+
76
+ # marc:subf - return all values matching a subfield regex
77
+ { ( ?Field ?Regex) marc:subf ?Result }
78
+ <=
79
+ {
80
+ ( ?Field 3) marc:splice ?FieldData.
81
+ ( ?FieldData ?Regex ()) marc:subf ?Result.
82
+ } .
83
+
84
+ { ( () ?Regex ?Acc ) marc:subf ?Acc }
85
+ <= true.
86
+
87
+ { ( ?FieldData ?Regex ?Acc ) marc:subf ?Result }
88
+ <=
89
+ {
90
+ ?FieldData list:firstRest (?Subf ?Rest).
91
+ ?Rest list:firstRest (?Value ?Tail).
92
+ ?Subf string:matches ?Regex.
93
+ ( ?Acc (?Value)) list:append ?Acc2.
94
+ ( ?Tail ?Regex ?Acc2 ) marc:subf ?Result.
95
+ }.
96
+
97
+ { ( ?FieldData ?Regex ?Acc ) marc:subf ?Result }
98
+ <=
99
+ {
100
+ ?FieldData list:firstRest (?Subf ?Rest).
101
+ ?Rest list:firstRest (?Value ?Tail).
102
+ ?Subf string:notMatches ?Regex.
103
+ ( ?Tail ?Regex ?Acc ) marc:subf ?Result.
104
+ }.
105
+
106
+ # marc:field0 - collect the first row of a marc field
107
+ { ( ?Record ?Field) marc:field0 ?Result }
108
+ <=
109
+ {
110
+ ( ?Record ?Field) marc:field ?F.
111
+ ?F list:first ?Result.
112
+ }.
113
+
114
+ # marc:field - collect all data for a marc field
115
+ { ( ?Record ?Field) marc:field ?Result}
116
+ <=
117
+ {
118
+ ( ?Record ?Field ()) marc:field ?Result.
119
+ }.
120
+
121
+ { ( () ?Field ?Acc ) marc:field ?Acc}
122
+ <= true.
123
+
124
+ { ( ?L ?Field ?Acc ) marc:field ?Result }
125
+ <=
126
+ {
127
+ ?L list:firstRest (?H ?T).
128
+ ( ?H 0 ) list:memberAt ?Field.
129
+ ( ?Acc (?H) ) list:append ?AccNew.
130
+ (?T ?Field ?AccNew) marc:field ?Result.
131
+ }.
132
+
133
+ { (?L ?Field ?Acc) marc:field ?Result }
134
+ <=
135
+ {
136
+ ?L list:firstRest (?H ?T).
137
+ ( ?H 0 ) list:memberAt ?X.
138
+ ?Field log:notEqualTo ?X.
139
+ (?T ?Field ?Acc) marc:field ?Result.
140
+ }.
141
+
142
+ # marc:map - join all values of (tag, subfield) into one string
143
+ { ( ?Record ?Tag ?Subfield ) marc:map ?Result }
144
+ <=
145
+ {
146
+ (?Record ?Tag) marc:field ?FL.
147
+ ?FL list:member ?F.
148
+ (?F ?Subfield) marc:subf ?T .
149
+ (?T " ") marc:join ?Result.
150
+ }.
151
+
152
+ ## ---- Extraction rules (forward) ----
153
+
154
+ # In --stream-messages mode the current RDF Message payload is exposed as a
155
+ # quoted graph via eymsg:payloadGraph/log:nameOf. Match :record inside that
156
+ # payload graph, then reuse the ordinary MARC helper rules on the bound list.
157
+
158
+ {
159
+ ?Envelope eymsg:payloadGraph ?Payload.
160
+ ?Payload log:nameOf ?PayloadContext.
161
+ ?PayloadContext log:includes { ?X :record ?Record. }.
162
+ ?Record marc:id ?ID.
163
+ (?Record "245" "a") marc:map ?Val.
164
+ }
165
+ =>
166
+ {
167
+ ?ID :title ?Val.
168
+ }.
169
+
170
+ {
171
+ ?Envelope eymsg:payloadGraph ?Payload.
172
+ ?Payload log:nameOf ?PayloadContext.
173
+ ?PayloadContext log:includes { ?X :record ?Record. }.
174
+ ?Record marc:id ?ID.
175
+ (?Record "650" "a") marc:map ?Val.
176
+ }
177
+ =>
178
+ {
179
+ ?ID :subject ?Val.
180
+ }.
181
+
182
+ {
183
+ ?Envelope eymsg:payloadGraph ?Payload.
184
+ ?Payload log:nameOf ?PayloadContext.
185
+ ?PayloadContext log:includes { ?X :record ?Record. }.
186
+ ?Record marc:id ?ID.
187
+ (?Record "920" "a") marc:map ?Val.
188
+ }
189
+ =>
190
+ {
191
+ ?ID :type ?Val.
192
+ }.
@@ -0,0 +1,284 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const cp = require('node:child_process');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+
9
+ const root = path.resolve(__dirname, '..');
10
+ const eyelingJsPath = path.join(root, 'eyeling.js');
11
+
12
+ const TTY = process.stdout.isTTY;
13
+ const C = TTY
14
+ ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
15
+ : { g: '', r: '', y: '', dim: '', n: '' };
16
+
17
+ function ok(msg) {
18
+ console.log(`${C.g}OK ${C.n} ${msg}`);
19
+ }
20
+ function info(msg) {
21
+ console.log(`${C.y}==${C.n} ${msg}`);
22
+ }
23
+ function fail(msg) {
24
+ console.error(`${C.r}FAIL${C.n} ${msg}`);
25
+ }
26
+ function numberedName(index, name) {
27
+ return `${String(index + 1).padStart(3, '0')} ${name}`;
28
+ }
29
+ function msNow() {
30
+ return Date.now();
31
+ }
32
+ function mkTmpDir() {
33
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'eyeling-stream-messages-'));
34
+ }
35
+ function rmrf(p) {
36
+ try {
37
+ fs.rmSync(p, { recursive: true, force: true });
38
+ } catch {}
39
+ }
40
+
41
+ function runEyeling(args, opts = {}) {
42
+ return cp.spawnSync(process.execPath, [eyelingJsPath, ...args], {
43
+ cwd: root,
44
+ encoding: 'utf8',
45
+ maxBuffer: opts.maxBuffer || 20 * 1024 * 1024,
46
+ });
47
+ }
48
+ function expectEyelingOk(args, opts = {}) {
49
+ const r = runEyeling(args, opts);
50
+ if (r.status === 0) return r.stdout;
51
+ throw new Error(
52
+ `eyeling failed with exit ${r.status}\n` +
53
+ `STDOUT:\n${r.stdout || ''}\n` +
54
+ `STDERR:\n${r.stderr || ''}`,
55
+ );
56
+ }
57
+
58
+ function writeScopedPayloadRules(file) {
59
+ fs.writeFileSync(
60
+ file,
61
+ `@prefix : <urn:test#>.\n` +
62
+ `@prefix eymsg: <https://eyereasoner.github.io/eyeling/vocab/message#>.\n` +
63
+ `@prefix log: <http://www.w3.org/2000/10/swap/log#>.\n` +
64
+ `{\n` +
65
+ ` ?Envelope eymsg:payloadGraph ?Payload.\n` +
66
+ ` ?Payload log:nameOf ?PayloadContext.\n` +
67
+ ` ?PayloadContext log:includes { ?Subject :line ?Line. }.\n` +
68
+ `} => {\n` +
69
+ ` ?Envelope log:outputString ?Line.\n` +
70
+ `}.\n`,
71
+ 'utf8',
72
+ );
73
+ }
74
+ function writeBasicMessageLog(file) {
75
+ fs.writeFileSync(
76
+ file,
77
+ `VERSION "1.2-messages"\n` +
78
+ `PREFIX : <urn:test#>\n` +
79
+ `\n` +
80
+ `:a :line "one\\n".\n` +
81
+ `MESSAGE\n` +
82
+ `\n` +
83
+ `:b :line "two\\n".\n` +
84
+ `MESSAGE\n` +
85
+ `# empty heartbeat\n` +
86
+ `MESSAGE\n` +
87
+ `:c :line "three\\n".\n`,
88
+ 'utf8',
89
+ );
90
+ }
91
+ function writeLargeMessageLog(file, count) {
92
+ let text = 'VERSION "1.2-messages"\nPREFIX : <urn:test#>\n';
93
+ for (let i = 1; i <= count; i += 1) {
94
+ text += `:m${i} :line "${i}\\n".\n`;
95
+ if (i < count) text += 'MESSAGE\n';
96
+ }
97
+ fs.writeFileSync(file, text, 'utf8');
98
+ }
99
+ function writeMarcMessageLog(file) {
100
+ fs.writeFileSync(
101
+ file,
102
+ `VERSION "1.2-messages"\n` +
103
+ `PREFIX : <http://example.org/ns#>\n` +
104
+ `\n` +
105
+ `:record1 :record (\n` +
106
+ ` ("001" "_" "_" "_" "42")\n` +
107
+ ` ("245" "1" "0" "a" "Streaming RDF Messages")\n` +
108
+ ` ("650" "_" "0" "a" "Semantic Web")\n` +
109
+ ` ("920" "_" "_" "a" "book")\n` +
110
+ `).\n` +
111
+ `MESSAGE\n` +
112
+ `:record2 :record (\n` +
113
+ ` ("001" "_" "_" "_" "43")\n` +
114
+ ` ("245" "1" "0" "a" "Incremental MARC Extraction")\n` +
115
+ ` ("650" "_" "0" "a" "Linked data")\n` +
116
+ ` ("920" "_" "_" "a" "article")\n` +
117
+ `).\n`,
118
+ 'utf8',
119
+ );
120
+ }
121
+
122
+ function startFileServer(file) {
123
+ const dir = path.dirname(file);
124
+ const script = path.join(dir, 'server.js');
125
+ const portFile = path.join(dir, 'server.port');
126
+ fs.writeFileSync(
127
+ script,
128
+ `const fs = require('node:fs');\n` +
129
+ `const http = require('node:http');\n` +
130
+ `const file = process.argv[2];\n` +
131
+ `const portFile = process.argv[3];\n` +
132
+ `const size = fs.statSync(file).size;\n` +
133
+ `const server = http.createServer((req, res) => {\n` +
134
+ ` const headers = { 'content-type': 'text/plain', 'accept-ranges': 'bytes' };\n` +
135
+ ` if (req.method === 'HEAD') { res.writeHead(200, { ...headers, 'content-length': size }); res.end(); return; }\n` +
136
+ ` const range = req.headers.range;\n` +
137
+ ` if (range) {\n` +
138
+ ` const m = /^bytes=(\\d+)-(\\d*)$/.exec(range);\n` +
139
+ ` const start = m ? Number(m[1]) : 0;\n` +
140
+ ` const end = m && m[2] ? Math.min(Number(m[2]), size - 1) : size - 1;\n` +
141
+ ` res.writeHead(206, { ...headers, 'content-range': 'bytes ' + start + '-' + end + '/' + size, 'content-length': end - start + 1 });\n` +
142
+ ` fs.createReadStream(file, { start, end }).pipe(res);\n` +
143
+ ` return;\n` +
144
+ ` }\n` +
145
+ ` res.writeHead(200, { ...headers, 'content-length': size });\n` +
146
+ ` fs.createReadStream(file).pipe(res);\n` +
147
+ `});\n` +
148
+ `server.listen(0, '127.0.0.1', () => fs.writeFileSync(portFile, String(server.address().port)));\n`,
149
+ 'utf8',
150
+ );
151
+ const child = cp.spawn(process.execPath, [script, file, portFile], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] });
152
+ const deadline = Date.now() + 5000;
153
+ while (Date.now() < deadline) {
154
+ if (fs.existsSync(portFile)) {
155
+ const port = fs.readFileSync(portFile, 'utf8').trim();
156
+ return { url: `http://127.0.0.1:${port}/messages.txt`, stop: () => child.kill() };
157
+ }
158
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
159
+ }
160
+ child.kill();
161
+ throw new Error('test HTTP server did not start');
162
+ }
163
+
164
+ const cases = [
165
+ {
166
+ name: 'scoped payload rules run once per RDF Message',
167
+ run(tmp) {
168
+ const rules = path.join(tmp, 'rules.n3');
169
+ const log = path.join(tmp, 'messages.trig');
170
+ writeScopedPayloadRules(rules);
171
+ writeBasicMessageLog(log);
172
+ const out = expectEyelingOk(['-r', '--stream-messages', rules, log]);
173
+ assert.equal(out, 'one\ntwo\nthree\n');
174
+ },
175
+ },
176
+ {
177
+ name: 'empty heartbeat messages do not leak previous payloads',
178
+ run(tmp) {
179
+ const rules = path.join(tmp, 'rules.n3');
180
+ const log = path.join(tmp, 'messages.trig');
181
+ writeScopedPayloadRules(rules);
182
+ writeBasicMessageLog(log);
183
+ const out = expectEyelingOk(['-r', '--stream-messages', rules, log]);
184
+ assert.equal(out.trim().split('\n').length, 3);
185
+ assert.ok(!out.includes('one\none'));
186
+ },
187
+ },
188
+ {
189
+ name: 'large message logs stream without requiring one monolithic dataset',
190
+ run(tmp) {
191
+ const rules = path.join(tmp, 'rules.n3');
192
+ const log = path.join(tmp, 'large.trig');
193
+ writeScopedPayloadRules(rules);
194
+ writeLargeMessageLog(log, 1000);
195
+ const out = expectEyelingOk(['-r', '--stream-messages', rules, log]);
196
+ const lines = out.trim().split('\n');
197
+ assert.equal(lines.length, 1000);
198
+ assert.equal(new Set(lines).size, 1000);
199
+ assert.ok(lines.includes('1'));
200
+ assert.ok(lines.includes('999'));
201
+ assert.ok(lines.includes('1000'));
202
+ },
203
+ },
204
+ {
205
+ name: 'remote text/plain RDF Message Logs are streamed via HTTP',
206
+ run(tmp) {
207
+ const rules = path.join(tmp, 'rules.n3');
208
+ const log = path.join(tmp, 'remote.trig');
209
+ writeScopedPayloadRules(rules);
210
+ writeLargeMessageLog(log, 25);
211
+ const server = startFileServer(log);
212
+ try {
213
+ const out = expectEyelingOk(['-r', '--stream-messages', server.url, rules]);
214
+ const lines = out.trim().split('\n');
215
+ assert.equal(lines.length, 25);
216
+ assert.equal(lines[0], '1');
217
+ assert.equal(lines[24], '25');
218
+ } finally {
219
+ server.stop();
220
+ }
221
+ },
222
+ },
223
+ {
224
+ name: 'MARC extraction rules fire over each streamed payload graph',
225
+ run(tmp) {
226
+ const log = path.join(tmp, 'marc.messages.txt');
227
+ writeMarcMessageLog(log);
228
+ const fixture = path.join(root, 'test', 'fixtures', 'marc-rules-stream-messages.n3');
229
+ const out = expectEyelingOk(['-r', '--stream-messages', fixture, log]);
230
+ assert.deepEqual(out.trim().split('\n').sort(), [
231
+ '<http://lib.ugent.be/record/42> <http://example.org/ns#subject> "Semantic Web" .',
232
+ '<http://lib.ugent.be/record/42> <http://example.org/ns#title> "Streaming RDF Messages" .',
233
+ '<http://lib.ugent.be/record/42> <http://example.org/ns#type> "book" .',
234
+ '<http://lib.ugent.be/record/43> <http://example.org/ns#subject> "Linked data" .',
235
+ '<http://lib.ugent.be/record/43> <http://example.org/ns#title> "Incremental MARC Extraction" .',
236
+ '<http://lib.ugent.be/record/43> <http://example.org/ns#type> "article" .',
237
+ ]);
238
+ },
239
+ },
240
+ {
241
+ name: '--stream-messages requires RDF mode',
242
+ run(tmp) {
243
+ const rules = path.join(tmp, 'rules.n3');
244
+ const log = path.join(tmp, 'messages.trig');
245
+ writeScopedPayloadRules(rules);
246
+ writeBasicMessageLog(log);
247
+ const r = runEyeling(['--stream-messages', rules, log]);
248
+ assert.notEqual(r.status, 0);
249
+ assert.match(r.stderr, /requires -r\/--rdf/);
250
+ },
251
+ },
252
+ ];
253
+
254
+ (function main() {
255
+ const suiteStart = msNow();
256
+ info(`Running ${cases.length} stream-message tests`);
257
+ let passed = 0;
258
+ let failed = 0;
259
+ for (const [index, tc] of cases.entries()) {
260
+ const tmp = mkTmpDir();
261
+ const testName = numberedName(index, tc.name);
262
+ const start = msNow();
263
+ try {
264
+ tc.run(tmp);
265
+ ok(`${testName} ${C.dim}(${msNow() - start} ms)${C.n}`);
266
+ passed++;
267
+ } catch (e) {
268
+ fail(`${testName} ${C.dim}(${msNow() - start} ms)${C.n}`);
269
+ fail(e && e.stack ? e.stack : String(e));
270
+ failed++;
271
+ } finally {
272
+ rmrf(tmp);
273
+ }
274
+ }
275
+ console.log('');
276
+ const suiteMs = msNow() - suiteStart;
277
+ console.log(`${C.y}==${C.n} Total elapsed: ${suiteMs} ms (${(suiteMs / 1000).toFixed(2)} s)`);
278
+ if (failed === 0) {
279
+ ok(`All stream-message tests passed (${passed}/${cases.length})`);
280
+ process.exit(0);
281
+ }
282
+ fail(`Some stream-message tests failed (${passed}/${cases.length})`);
283
+ process.exit(1);
284
+ })();