Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions __tests__/cli-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const files = {
{ :Let :output ?out } => { 1 log:outputString ?out } .
`,
[path.join(__dirname, 'ask.n3')]: askQuery,
[path.join(__dirname, 'invalid.n3')]: 'this is not valid N3 {{{',
};

jest.mock('fs', () => ({
Expand All @@ -43,7 +44,7 @@ async function getConsoleOutput(args: string[]) {
const restoreConsole = mockConsole();
const stdin = new ReadStream();

await mainFunc({
const proc = {
argv: ['/bin/node', 'eyereasoner', ...args],
cwd: () => __dirname,
stdin,
Expand All @@ -54,7 +55,9 @@ async function getConsoleOutput(args: string[]) {
}
},
},
} as NodeJS.Process);
} as NodeJS.Process;

await mainFunc(proc);

// @ts-ignore
// eslint-disable-next-line no-console
Expand All @@ -68,7 +71,7 @@ async function getConsoleOutput(args: string[]) {
const stdout = calls.map((call) => call.join(' ')).join('\n');
const stderr = stderrCalls.map((call) => call.join(' ')).join('\n');

return { stdout, stderr };
return { stdout, stderr, exitCode: proc.exitCode };
}

describe('Testing convertToPosixPath', () => {
Expand Down Expand Up @@ -126,4 +129,16 @@ describe('Testing CLI', () => {
const { stdout } = await getConsoleOutput(['--nope', '--quiet', './ask.n3']);
expect(new Parser().parse(stdout)).toBeRdfIsomorphic(new Parser().parse(askResult));
});

it('Should not set an exit code on success', async () => {
const { exitCode } = await getConsoleOutput(['--nope', '--quiet', './socrates.n3', '--query', './socrates-query.n3']);
expect(exitCode).toBeUndefined();
});

it('Should set a non-zero exit code when the reasoner errors', async () => {
const { stdout, stderr, exitCode } = await getConsoleOutput(['--nope', '--quiet', './invalid.n3']);
expect(stdout).toEqual('');
expect(stderr).toContain('** ERROR **');
expect(exitCode).toEqual(1);
});
});
28 changes: 26 additions & 2 deletions lib/bin/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,16 @@
output: proc.stdout,
});

const Module = await SwiplEye();
// Capture the reasoner's stderr (in addition to echoing it) so that
// failures can be propagated to the process exit code below.
const errorLines: string[] = [];
const Module = await SwiplEye({
printErr: (str: string) => {
errorLines.push(str);
// eslint-disable-next-line no-console
console.error(str);
},
});

const posixArgv: string[] = [];
// Make any local files available to the reasoner
Expand All @@ -46,6 +55,21 @@
}
}

await qaQuery(Module, 'main', posixArgv, (q) => rl.question(`${q}\n|: `));
let failed = false;
try {
const res = await qaQuery(Module, 'main', posixArgv, (q) => rl.question(`${q}\n|: `));
failed = res.error === true;
} /* istanbul ignore next: defensive β€” EYE reports its errors on stderr instead */ catch (e) {
// eslint-disable-next-line no-console
console.error(e instanceof Error ? e.message : String(e));
failed = true;
}
rl.close();

// EYE reports reasoning failures (parse errors, resource errors, ...) on
// stderr with an `** ERROR **` marker while the underlying Prolog goal
// still succeeds, so a marker on stderr must also fail the process.
if (failed || errorLines.some((line) => line.includes('** ERROR **'))) {
proc.exitCode = 1;

Check failure on line 73 in lib/bin/main.ts

View workflow job for this annotation

GitHub Actions / eslint

Assignment to property of function parameter 'proc'
}
}
14 changes: 11 additions & 3 deletions lib/query.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import type { SWIPLModule } from 'swipl-wasm';

interface CallOutput {
export interface CallResult {
done: boolean;
/** Set when the query terminated with an uncaught Prolog exception */
error?: boolean;
/** The exception message, when `error` is set */
message?: string;
}

interface CallOutput extends CallResult {
resume: (res: string) => void; yield: string;
}

Expand All @@ -19,19 +26,20 @@ export function buildQuery(name: string, args: string | string[]) {
* @param name The name of the query function
* @param args The arguments of the query function
* @param cb The callback for question/answering
* @returns The result of the query
* @returns The final result of the query
*/
export async function qaQuery(
module: SWIPLModule,
queryString: string,
args: string | string[],
cb: (res: string) => Promise<string>,
) {
): Promise<CallResult> {
let res = module.prolog.call(buildQuery(queryString, args), { async: true }) as CallOutput;
while (!res.done) {
// eslint-disable-next-line no-await-in-loop
res = res.resume(await cb(res.yield)) as unknown as CallOutput;
}
return res;
}

/**
Expand Down
33 changes: 33 additions & 0 deletions perf/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,36 @@ const deepTaxonomyBenchmark100 = [
...(new Parser({ format: 'n3' })).parse('{ ?s a ?o . ?o <http://www.w3.org/2000/01/rdf-schema#subClassOf> ?o2 . } => { ?s a ?o2 . } .'),
]

// The extended deep taxonomy benchmark in its canonical shape (from the
// deep taxonomy benchmark at https://eulersharp.sourceforge.net/2009/12dtb/,
// see also #337/#338): an individual at the bottom of an N-level
// rdfs:subClassOf chain with 3-way branching, a single *backward* rule, and
// the target class membership asked via --query. EYE proves this in linear
// time, so even N=1000 reasons well under a second and the CI benchmark
// remains bounded.
//
// NOTE: unlike the forward-rule deep taxonomy cases above, this case must
// not reuse a pre-loaded module across runs: re-running main() re-asserts
// the backward rule, and the duplicated rule clauses make the backward
// search explode (a second run on the same module does not terminate in
// minutes, where the first takes <1s). n3reasoner boots a fresh module per
// call, which keeps every iteration independent.
function generateExtendedDeepTaxonomy(size: number): { data: string, query: string } {
const prefixes = '@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\n'
+ '@prefix : <http://eulersharp.sourceforge.net/2009/12dtb/test#>.\n';
const lines = [prefixes, `:i${size} a :N0.`];
for (let i = 0; i < size; i += 1) {
lines.push(`:N${i} rdfs:subClassOf :N${i + 1}, :I${i + 1}, :J${i + 1}.`);
}
lines.push('{?X a ?D} <= {?C rdfs:subClassOf ?D. ?X a ?C}.');
return {
data: lines.join('\n'),
query: `${prefixes}{:i${size} a :N${size}} => {:i${size} a :N${size}}.\n`,
};
}

const extendedDeepTaxonomy1000 = generateExtendedDeepTaxonomy(1000);

function deferred(fn: () => Promise<any>): Benchmark.Options {
return {
defer: true,
Expand Down Expand Up @@ -90,6 +120,9 @@ async function main() {
).add(
'Run deep taxonomy benchmark [100] [reasoning only]',
() => queryOnce(LoadedDeep100, 'main', ['--nope', '--quiet', './data.n3', '--pass-only-new']),
).add(
'Run extended deep taxonomy benchmark [1000]',
deferred(() => n3reasoner(extendedDeepTaxonomy1000.data, extendedDeepTaxonomy1000.query)),
).add(
'Run timbl + foaf + rdfs rules',
deferred(() => n3reasoner(timblFoafRdfs, undefined, { outputType: 'string' })),
Expand Down
Loading