From 70101637ea946b5ac5749d1c27d45ec910cb912f Mon Sep 17 00:00:00 2001 From: matteo-cristino Date: Wed, 3 Jul 2024 13:00:14 +0200 Subject: [PATCH 1/4] test(core): cover all branches in visitor.ts with tests --- pkg/core/test/visitor.ts | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/pkg/core/test/visitor.ts b/pkg/core/test/visitor.ts index 65d2ee2f..24ca6c9c 100644 --- a/pkg/core/test/visitor.ts +++ b/pkg/core/test/visitor.ts @@ -155,6 +155,54 @@ test('visitor works', (t) => { test('visitor throws error correctly', (t) => { ( [ + [ + { data: {}, keys: {} }, + { + givenThen: 'given', + errors: [ + { + message: new Error('at 1\n missing one of: Given I, Then I'), + lineNo: 1 + } + ], + matches: [], + }, + 'cst must not have any general errors' + ], + [ + { data: {}, keys: {} }, + { + givenThen: 'given', + errors: [], + matches: [], + }, + 'cst must have only one match' + ], + [ + { data: {}, keys: {} }, + { + givenThen: 'given', + errors: [], + matches: [ + { + key: { + phrase: 'a b c' + }, + bindings: new Map(), + err: [ + { + message: new Error('at 1:19.22\n save may be and'), + lineNo: 1, + start: 19, + end: 22 + } + ], + lineNo: 1 + } + ] + }, + 'cst\'s match must not have any errors' + ], [ { data: {}, keys: {} }, { @@ -215,6 +263,25 @@ test('visitor throws error correctly', (t) => { }, 'the array referenced by myFile must solely composed of strings' ], + [ + { data: {}, keys: {} }, + { + givenThen: 'given', + errors: [], + matches: [ + { + key: { + params: ['object'], + phrase: 'a b c' + }, + bindings: new Map([['object', 'myObj']]), + err: [], + lineNo: 1 + }, + ], + }, + 'Can\'t find myObj in DATA or KEYS' + ], ] as [ZenParams, Cst, string][] ).forEach(([params, cst, errMessage]) => { const err = t.throws(() => { From 2a27a8199075607a1f2c6611dd3fb5ad15470675 Mon Sep 17 00:00:00 2001 From: matteo-cristino Date: Wed, 3 Jul 2024 15:08:50 +0200 Subject: [PATCH 2/4] test(core): cover all branches in plugin.ts with tests --- pkg/core/src/plugin.ts | 3 +- pkg/core/test/plugin.ts | 87 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/pkg/core/src/plugin.ts b/pkg/core/src/plugin.ts index ea9db30a..5ff70f68 100644 --- a/pkg/core/src/plugin.ts +++ b/pkg/core/src/plugin.ts @@ -328,8 +328,7 @@ export class Plugin { ) { phrase = phraseOrParamsOrOpenconnect; executor = executorOrPhraseOrParams; - } else { - /* c8 ignore next 3 */ + } /* c8 ignore next 4 */ else { // This should be unreachable. throw new Error('unreachable'); } diff --git a/pkg/core/test/plugin.ts b/pkg/core/test/plugin.ts index 867cac9f..01d9be1b 100644 --- a/pkg/core/test/plugin.ts +++ b/pkg/core/test/plugin.ts @@ -3,7 +3,14 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import test from 'ava'; -import { DuplicatePluginError, Plugin, isSane, PluginContextTest, type PluginExecutor } from '@slangroom/core'; +import { + DuplicatePluginError, + Plugin, + isSane, + PluginContextTest, + PluginContextImpl, + type PluginExecutor +} from '@slangroom/core'; const insanes = [ '', @@ -118,6 +125,19 @@ test('Plugin.new() duplicates detected', (t) => { ); }); +test('Plugin.new() duplicated params detected', (t) => { + t.throws( + () => { + const p = new Plugin(); + p.new(['foo', 'foo'], 'a', (ctx) => ctx.pass(null)); + }, + { + instanceOf: Error, + message: `params must not have duplicate values: foo`, + }, + ); +}); + test('Plugin.new() clauses work', (t) => { const f: PluginExecutor = (ctx) => ctx.pass(null); { @@ -157,7 +177,7 @@ test('Plugin.new() clauses work', (t) => { } }); -test('PluginContext', async (t) => { +test('PluginContextTest', async (t) => { const p = new Plugin(); // ctxs const emptyCtx = new PluginContextTest([], {}); @@ -193,3 +213,66 @@ test('PluginContext', async (t) => { const err = await t.throwsAsync(async() => await fetch(emptyCtx)); t.is(err.message, 'the parameter isn\'t provided: obj'); }); + +test('PluginContextImpl', async (t) => { + const p = new Plugin(); + const emptyCtx = new PluginContextImpl({ + key: { + phrase: 'abc' + }, + params: new Map([]), + }); + const openCtx = new PluginContextImpl({ + key: { + openconnect: 'open', + phrase: 'abc' + }, + params: new Map([]), + open: ['some_path'], + }); + const connectCtx = new PluginContextImpl({ + key: { + openconnect: 'connect', + phrase: 'abc' + }, + params: new Map([]), + connect: ['some_path'], + }); + const objCtx = new PluginContextImpl({ + key: { + params: ['obj'], + phrase: 'abc' + }, + params: new Map([['obj', 'obj']]), + }) + + // pass and fail + const pass = p.new('test pass', (ctx) => ctx.pass('done')); + const fail = p.new('test fail', (ctx) => ctx.fail('failed')); + t.deepEqual(await pass(emptyCtx), { ok: true, value: 'done' }); + t.deepEqual(await fail(emptyCtx), { ok: false, error: 'failed' }); + // connect + const getConnect = p.new('connect', 'test get connect', (ctx) => ctx.pass(ctx.getConnect())); + const fetchConnect = p.new('connect', 'test fetch connect', (ctx) => ctx.pass(ctx.fetchConnect())); + t.deepEqual(await getConnect(connectCtx), { ok: true, value: ['some_path']}); + t.deepEqual(await fetchConnect(connectCtx), { ok: true, value: ['some_path']}); + t.deepEqual(await getConnect(emptyCtx), { ok: true, value: []}); + const fetchConnectErr = await t.throwsAsync(async() => await fetchConnect(emptyCtx)); + t.is(fetchConnectErr.message, 'a connect is required'); + // open + const getOpen = p.new('open', 'test get open', (ctx) => ctx.pass(ctx.getOpen())); + const fetchOpen = p.new('open', 'test fetch open', (ctx) => ctx.pass(ctx.fetchOpen())); + t.deepEqual(await getOpen(openCtx), { ok: true, value: ['some_path']}); + t.deepEqual(await fetchOpen(openCtx), { ok: true, value: ['some_path']}); + t.deepEqual(await getOpen(emptyCtx), { ok: true, value: []}); + const fetchOpenErr = await t.throwsAsync(async() => await fetchOpen(emptyCtx)); + t.is(fetchOpenErr.message, 'a open is required'); + // get and fetch + const get = p.new(['obj'],'test get', (ctx) => ctx.pass(ctx.get('obj') || null)); + const fetch = p.new(['obj'], 'test fetch', (ctx) => ctx.pass(ctx.fetch('obj') || null)); + t.deepEqual(await get(objCtx), { ok: true, value: 'obj' }); + t.deepEqual(await fetch(objCtx), { ok: true, value: 'obj' }); + t.deepEqual(await get(emptyCtx), { ok: true, value: null }); + const err = await t.throwsAsync(async() => await fetch(emptyCtx)); + t.is(err.message, 'the parameter isn\'t provided: obj'); +}); From fd4038f9ca75ee7482f3645de118fb6781e31deb Mon Sep 17 00:00:00 2001 From: matteo-cristino Date: Wed, 3 Jul 2024 15:47:55 +0200 Subject: [PATCH 3/4] test(core): cover some branches in slangroom.ts with tests The other branches in core package seems unreachable to me, but left there for further analysis --- pkg/core/test/errors.ts | 77 ++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/pkg/core/test/errors.ts b/pkg/core/test/errors.ts index e791fe59..61886cab 100644 --- a/pkg/core/test/errors.ts +++ b/pkg/core/test/errors.ts @@ -6,6 +6,8 @@ import { Plugin, Slangroom } from '@slangroom/core'; import test from 'ava'; // read the version from the package.json import packageJson from '@slangroom/core/package.json' with { type: 'json' }; +import ignoredPackageJson from '@slangroom/ignored/package.json' with { type: 'json' }; + import { sentenceHighlight, textHighlight, @@ -16,6 +18,13 @@ import { lineNoColor } from '@slangroom/shared'; +const errorColorDef = ` +Error colors: + - ${errorColor('error')} + - ${suggestedColor('suggested words')} + - ${missingColor('missing words')} + - ${extraColor('extra words')} +` test('@slangroom/core errors are shown and context is shown with line number', async (t) => { const plugin = new Plugin(); @@ -32,13 +41,7 @@ ${lineNoColor('1 | ')}${sentenceHighlight(` Given I ${textHighlight('gibberis ${errorColor('^^^^^^^^^')} ${lineNoColor('2 | ')} Given nothing ${lineNoColor('3 | ')} Then print data - -Error colors: - - ${errorColor('error')} - - ${suggestedColor('suggested words')} - - ${missingColor('missing words')} - - ${extraColor('extra words')} - +${errorColorDef} ParseError @slangroom/core@${packageJson.version}: at 2:9-17 ${errorColor('gibberish')} may be ${suggestedColor('send')} @@ -80,13 +83,7 @@ ${lineNoColor('1 | ')}${sentenceHighlight(` Given I send param ${textHighligh ${errorColor('^^^^^^^^^^^^^^^^^^^^^^^^^')} ${lineNoColor('2 | ')} Given nothing ${lineNoColor('3 | ')} Then print data - -Error colors: - - ${errorColor('error')} - - ${suggestedColor('suggested words')} - - ${missingColor('missing words')} - - ${extraColor('extra words')} - +${errorColorDef} LexError @slangroom/core@${packageJson.version}: at 2:20-44 unclosed single-quote ${errorColor('\'param and do some action')} ` @@ -111,13 +108,7 @@ ${lineNoColor('1 | ')}${sentenceHighlight(` ${textHighlight('Gibberish')} con ${errorColor('^^^^^^^^^')} ${lineNoColor('2 | ')} Given nothing ${lineNoColor('3 | ')} Then print data - -Error colors: - - ${errorColor('error')} - - ${suggestedColor('suggested words')} - - ${missingColor('missing words')} - - ${extraColor('extra words')} - +${errorColorDef} ParseError @slangroom/core@${packageJson.version}: at 2:1-9 ${errorColor('Gibberish')} may be ${suggestedColor('given')} or ${suggestedColor('then')} @@ -164,3 +155,47 @@ ParseError @slangroom/core@${packageJson.version}: at 2:74-84 const err = await t.throwsAsync(fn); t.is(err?.message, expected); }); + +test('@slangroom/core invalid line error', async (t) => { + const plugin = new Plugin(); + plugin.new('connect', ['param'], 'do some action', (_) => _.pass(null)); + + const slang = new Slangroom(plugin); + const fn = slang.execute(`Rule unknown ignore + Given I connect to 'url' and send param 'param' and do some action + Given nothing + Gibberish + Then print data`) + + const expected = `${lineNoColor('2 | ')} Given nothing +${lineNoColor('3 | ')}${sentenceHighlight(` ${textHighlight('Gibberish')}`)} + ${errorColor('^^^^^^^^^')} +${lineNoColor('4 | ')} Then print data +${errorColorDef} +Zencode Invalid Statement @slangroom/ignored@${ignoredPackageJson.version} Error: Invalid Zencode line +` + + const err = await t.throwsAsync(fn); + t.is(err?.message, expected); +}); + +test('@slangroom/core error in the then phase', async (t) => { + const plugin = new Plugin(); + plugin.new('do some action', (_) => _.fail(new Error('failed'))); + + const slang = new Slangroom(plugin); + const fn = slang.execute(`Rule unknown ignore + Given nothing + Then print data + Then I do some action`) + + const expected = `${lineNoColor('2 | ')} Then print data +${lineNoColor('3 | ')}${sentenceHighlight(` ${textHighlight('Then I do some action')}`)} + ${errorColor('^^^^^^^^^^^^^^^^^^^^^')} +${errorColorDef} +Error: failed +` + + const err = await t.throwsAsync(fn); + t.is(err?.message, expected); +}); From 93e737070b9b29a45605f5b862b9d40f6dc45c83 Mon Sep 17 00:00:00 2001 From: matteo-cristino Date: Wed, 3 Jul 2024 15:50:25 +0200 Subject: [PATCH 4/4] test(core): fix type in input to context fail --- pkg/core/test/plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/core/test/plugin.ts b/pkg/core/test/plugin.ts index 01d9be1b..f2eaa8d7 100644 --- a/pkg/core/test/plugin.ts +++ b/pkg/core/test/plugin.ts @@ -248,9 +248,9 @@ test('PluginContextImpl', async (t) => { // pass and fail const pass = p.new('test pass', (ctx) => ctx.pass('done')); - const fail = p.new('test fail', (ctx) => ctx.fail('failed')); + const fail = p.new('test fail', (ctx) => ctx.fail(new Error('failed'))); t.deepEqual(await pass(emptyCtx), { ok: true, value: 'done' }); - t.deepEqual(await fail(emptyCtx), { ok: false, error: 'failed' }); + t.deepEqual(await fail(emptyCtx), { ok: false, error: new Error('failed') }); // connect const getConnect = p.new('connect', 'test get connect', (ctx) => ctx.pass(ctx.getConnect())); const fetchConnect = p.new('connect', 'test fetch connect', (ctx) => ctx.pass(ctx.fetchConnect()));