From 518de980af14f14bf357e9f1132fceb596e732c8 Mon Sep 17 00:00:00 2001 From: ivojawer Date: Fri, 7 Jun 2024 00:47:42 -0300 Subject: [PATCH 1/4] full file match + throw error when no file/describe is found --- src/commands/test.ts | 31 ++++++++++++++++++++++++++----- src/index.ts | 2 +- test/test.test.ts | 10 +++++----- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/commands/test.ts b/src/commands/test.ts index 2cc3d8c..a50394e 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -1,4 +1,4 @@ -import { bold } from 'chalk' +import { bold, red } from 'chalk' import { time, timeEnd } from 'console' import logger from 'loglevel' import { Entity, Environment, Node, Test, is, match, when, WRENatives as natives, interpret, Describe } from 'wollok-ts' @@ -17,16 +17,33 @@ export type Options = { skipValidations: boolean } +class TestSearchMissError extends Error{} + export function validateParameters(filter: string | undefined, { file, describe, test }: Options): void { if (filter && (file || describe || test)) throw new Error('You should either use filter by full name or file/describe/test.') } +function matchingTestDescription(filter: string | undefined, options: Options): string { + if(filter) return `matching ${valueDescription(filter)}` + if(options.file || options.describe || options.test) return `matching ${valueDescription(`${options.file ? `'${options.file}'` : '*'}.${options.describe ? `'${options.describe}'` : '*'}.${options.test ? `'${options.test}'` : '*'}`)}` + return '' +} + export function sanitize(value?: string): string | undefined { return value?.replaceAll('"', '') } export function getTarget(environment: Environment, filter: string | undefined, options: Options): Test[] { - const possibleTargets = getBaseNode(environment, filter, options).descendants.filter(getTestFilter(filter, options)) + let possibleTargets: Test[] + try { + possibleTargets = getBaseNode(environment, filter, options).descendants.filter(getTestFilter(filter, options)) + } catch(e: any){ + if(e instanceof TestSearchMissError){ + logger.error(red(bold(e.message))) + return [] + } + throw e + } const onlyTarget = possibleTargets.find((test: Test) => test.isOnly) const testMatches = (filter: string) => (test: Test) => !filter || sanitize(test.fullyQualifiedName)!.includes(filter) const filterTest = sanitize(filter) ?? '' @@ -37,12 +54,14 @@ function getBaseNode(environment: Environment, filter: string | undefined, optio if (filter) return environment const { file, describe } = options - let nodeToFilter: Environment | Package | Describe = environment + let nodeToFilter: Environment | Package | Describe | undefined = environment if (file) { - nodeToFilter = nodeToFilter.descendants.find(node => node.is(Package) && node.name === file) as Package | undefined ?? nodeToFilter + nodeToFilter = nodeToFilter.descendants.find(node => node.is(Package) && node.fileName === file) as Package | undefined + if(!nodeToFilter) throw new TestSearchMissError(`File '${file}' not found`) } if (describe) { nodeToFilter = nodeToFilter.descendants.find(node => node.is(Describe) && node.name === `"${describe}"`) as Describe | undefined ?? nodeToFilter + if(!nodeToFilter) throw new TestSearchMissError(`Describe '${describe}' not found`) } return nodeToFilter } @@ -62,7 +81,9 @@ export default async function (filter: string | undefined, options: Options): Pr const timeMeasurer = new TimeMeasurer() const { project, skipValidations } = options - const runAllTestsDescription = `${testIcon} Running all tests ${filter ? `matching ${valueDescription(filter)} ` : ''}on ${valueDescription(project)}` + + const matchLog = matchingTestDescription(filter, options) + const runAllTestsDescription = `${testIcon} Running all tests${matchLog ? ` ${matchLog} `: ' '}on ${valueDescription(project)}` logger.info(runAllTestsDescription) diff --git a/src/index.ts b/src/index.ts index 9bbb043..07e9f20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,7 @@ program.command('test') .description('Run Wollok tests') .argument('[filter]', 'filter pattern for a test, describe or package') .option('-p, --project [project]', 'path to project', process.cwd()) - .option('-f, --file [file]', 'path to file', '') + .option('-f, --file [file]', 'path to file relative to the project', '') .option('-d, --describe [describe]', 'describe to run', '') .option('-t, --test [test]', 'test to run', '') .option('--skipValidations', 'skip code validation', false) diff --git a/test/test.test.ts b/test/test.test.ts index a7a2f96..8f0c2f2 100644 --- a/test/test.test.ts +++ b/test/test.test.ts @@ -121,7 +121,7 @@ describe('Test', () => { it('should filter by file using file option', () => { const tests = getTarget(environment, undefined, { ...emptyOptions, - file: 'test-one', + file: 'test-one.wtest', }) expect(tests.length).to.equal(3) expect(tests[0].name).to.equal('"a test"') @@ -132,7 +132,7 @@ describe('Test', () => { it('should filter by file & describe using file & describe option', () => { const tests = getTarget(environment, undefined, { ...emptyOptions, - file: 'test-one', + file: 'test-one.wtest', describe: 'this describe', }) expect(tests.length).to.equal(3) @@ -144,7 +144,7 @@ describe('Test', () => { it('should filter by file & describe & test using file & describe & test option', () => { const tests = getTarget(environment, undefined, { ...emptyOptions, - file: 'test-one', + file: 'test-one.wtest', describe: 'this describe', test: 'another test', }) @@ -195,7 +195,7 @@ describe('Test', () => { it('should execute single test when running a file using file option', () => { const tests = getTarget(environment, undefined, { ...emptyOptions, - file: 'only-file', + file: 'only-file.wtest', }) expect(tests.length).to.equal(1) expect(tests[0].name).to.equal('"this is the one"') @@ -290,7 +290,7 @@ describe('Test', () => { it('passes all the tests successfully and exits normally', async () => { await test(undefined, { ...emptyOptions, - file: 'test-one', + file: 'test-one.wtest', }) expect(processExitSpy.callCount).to.equal(0) From 6e84f13145ef3547918eb9f7aa1a7b19d72786ed Mon Sep 17 00:00:00 2001 From: ivojawer Date: Fri, 7 Jun 2024 01:33:24 -0300 Subject: [PATCH 2/4] fix describe + tests --- src/commands/test.ts | 2 +- test/test.test.ts | 33 +++++++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/commands/test.ts b/src/commands/test.ts index a50394e..919272d 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -60,7 +60,7 @@ function getBaseNode(environment: Environment, filter: string | undefined, optio if(!nodeToFilter) throw new TestSearchMissError(`File '${file}' not found`) } if (describe) { - nodeToFilter = nodeToFilter.descendants.find(node => node.is(Describe) && node.name === `"${describe}"`) as Describe | undefined ?? nodeToFilter + nodeToFilter = nodeToFilter.descendants.find(node => node.is(Describe) && node.name === `"${describe}"`) as Describe | undefined if(!nodeToFilter) throw new TestSearchMissError(`Describe '${describe}' not found`) } return nodeToFilter diff --git a/test/test.test.ts b/test/test.test.ts index 8f0c2f2..f197fa7 100644 --- a/test/test.test.ts +++ b/test/test.test.ts @@ -1,11 +1,11 @@ import { expect } from 'chai' +import logger from 'loglevel' import { join } from 'path' -import { buildEnvironmentForProject } from '../src/utils' -import test, { getTarget, sanitize, tabulationForNode, validateParameters } from '../src/commands/test' +import sinon from 'sinon' import { Environment } from 'wollok-ts' -import logger from 'loglevel' +import test, { getTarget, sanitize, tabulationForNode, validateParameters } from '../src/commands/test' import { logger as fileLogger } from '../src/logger' -import sinon from 'sinon' +import { buildEnvironmentForProject } from '../src/utils' import { spyCalledWithSubstring } from './assertions' describe('Test', () => { @@ -265,6 +265,7 @@ describe('Test', () => { let fileLoggerInfoSpy: sinon.SinonStub let loggerInfoSpy: sinon.SinonStub + let loggerErrorSpy: sinon.SinonStub let processExitSpy: sinon.SinonStub const projectPath = join('examples', 'test-examples', 'normal-case') @@ -281,6 +282,7 @@ describe('Test', () => { loggerInfoSpy = sinon.stub(logger, 'info') fileLoggerInfoSpy = sinon.stub(fileLogger, 'info') processExitSpy = sinon.stub(process, 'exit') + loggerErrorSpy = sinon.stub(logger, 'error') }) afterEach(() => { @@ -301,6 +303,29 @@ describe('Test', () => { expect(fileLoggerInfoSpy.firstCall.firstArg.result).to.deep.equal({ ok: 3, failed: 0 }) }) + it('passing a wrong filename runs no tests and logs a warning', async () => { + await test(undefined, { + ...emptyOptions, + file: 'non-existing-file.wtest', + }) + + expect(processExitSpy.callCount).to.equal(0) + expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 0 tests')).to.be.true + expect(spyCalledWithSubstring(loggerErrorSpy, 'File \'non-existing-file.wtest\' not found')).to.be.true + }) + + it('passing a wrong describe runs no tests and logs a warning', async () => { + await test(undefined, { + ...emptyOptions, + file: 'test-one.wtest', + describe: 'non-existing-describe', + }) + + expect(processExitSpy.callCount).to.equal(0) + expect(spyCalledWithSubstring(loggerInfoSpy, 'Running 0 tests')).to.be.true + expect(spyCalledWithSubstring(loggerErrorSpy, 'Describe \'non-existing-describe\' not found')).to.be.true + }) + it('returns exit code 2 if one or more tests fail', async () => { await test(undefined, emptyOptions) From d4d2bc244ef8596ef48594965e5e638c0a2d8c9c Mon Sep 17 00:00:00 2001 From: ivojawer Date: Fri, 7 Jun 2024 21:01:56 -0300 Subject: [PATCH 3/4] refac + test test match log --- src/commands/test.ts | 7 +++++-- test/test.test.ts | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/commands/test.ts b/src/commands/test.ts index 919272d..ac96d7c 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -23,9 +23,12 @@ export function validateParameters(filter: string | undefined, { file, describe, if (filter && (file || describe || test)) throw new Error('You should either use filter by full name or file/describe/test.') } -function matchingTestDescription(filter: string | undefined, options: Options): string { +export function matchingTestDescription(filter: string | undefined, options: Options): string { if(filter) return `matching ${valueDescription(filter)}` - if(options.file || options.describe || options.test) return `matching ${valueDescription(`${options.file ? `'${options.file}'` : '*'}.${options.describe ? `'${options.describe}'` : '*'}.${options.test ? `'${options.test}'` : '*'}`)}` + if(options.file || options.describe || options.test) { + const stringifiedOrWildcard = (value?: string) => value ? `'${value}'` : '*' + return `matching ${valueDescription([options.file, options.describe, options.test].map(stringifiedOrWildcard).join('.'))}` + } return '' } diff --git a/test/test.test.ts b/test/test.test.ts index f197fa7..589ab67 100644 --- a/test/test.test.ts +++ b/test/test.test.ts @@ -3,7 +3,7 @@ import logger from 'loglevel' import { join } from 'path' import sinon from 'sinon' import { Environment } from 'wollok-ts' -import test, { getTarget, sanitize, tabulationForNode, validateParameters } from '../src/commands/test' +import test, { getTarget, matchingTestDescription, sanitize, tabulationForNode, validateParameters } from '../src/commands/test' import { logger as fileLogger } from '../src/logger' import { buildEnvironmentForProject } from '../src/utils' import { spyCalledWithSubstring } from './assertions' @@ -250,6 +250,43 @@ describe('Test', () => { }) + describe('matching test description', () => { + const emptyOptions = { + project: '', + skipValidations: false, + file: undefined, + describe: undefined, + test: undefined, + } + + + it('should return empty string if no filter or options are passed', () => { + expect(matchingTestDescription(undefined, emptyOptions)).to.equal('') + }) + + it('should return filter description if filter is passed', () => { + expect(matchingTestDescription('some test', emptyOptions)).to.include('some test') + }) + + it('should return options descriptions if options are passed', () => { + expect(matchingTestDescription(undefined, { + ...emptyOptions, + file: 'test-one.wtest', + describe: 'this describe', + test: 'another test', + })).to.include('\'test-one.wtest\'.\'this describe\'.\'another test\'') + }) + + it('should return options descriptions with wildcards if options are missing', () => { + expect(matchingTestDescription(undefined, { + ...emptyOptions, + file: undefined, + describe: 'this discribe', + test: undefined, + })).to.include('*.\'this discribe\'.*') + }) + }) + describe('tabulations for node', () => { it('should work for root package', () => { From e2a0e7278c887fd527c3473e69a3430c6ea6a627 Mon Sep 17 00:00:00 2001 From: ivojawer Date: Tue, 11 Jun 2024 21:26:27 -0300 Subject: [PATCH 4/4] try - catch the whole func --- src/commands/test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/test.ts b/src/commands/test.ts index ac96d7c..17bce55 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -40,6 +40,10 @@ export function getTarget(environment: Environment, filter: string | undefined, let possibleTargets: Test[] try { possibleTargets = getBaseNode(environment, filter, options).descendants.filter(getTestFilter(filter, options)) + const onlyTarget = possibleTargets.find((test: Test) => test.isOnly) + const testMatches = (filter: string) => (test: Test) => !filter || sanitize(test.fullyQualifiedName)!.includes(filter) + const filterTest = sanitize(filter) ?? '' + return onlyTarget ? [onlyTarget] : possibleTargets.filter(testMatches(filterTest)) } catch(e: any){ if(e instanceof TestSearchMissError){ logger.error(red(bold(e.message))) @@ -47,10 +51,6 @@ export function getTarget(environment: Environment, filter: string | undefined, } throw e } - const onlyTarget = possibleTargets.find((test: Test) => test.isOnly) - const testMatches = (filter: string) => (test: Test) => !filter || sanitize(test.fullyQualifiedName)!.includes(filter) - const filterTest = sanitize(filter) ?? '' - return onlyTarget ? [onlyTarget] : possibleTargets.filter(testMatches(filterTest)) } function getBaseNode(environment: Environment, filter: string | undefined, options: Options): Environment | Package | Describe {