From 195923648c899248798525e45d268f476de8d510 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sun, 5 Nov 2023 20:17:42 -0300 Subject: [PATCH] Common function, error handling & unit tests for repl/test --- .../fileWithParseErrors.wlk | 7 ++ .../fileWithValidationErrors.wlk | 5 + package-lock.json | 72 ++++++++++++ package.json | 3 + src/commands/repl.ts | 14 +-- src/commands/test.ts | 108 +++++++++--------- src/utils.ts | 20 ++-- test/utils.test.ts | 26 +++++ 8 files changed, 184 insertions(+), 71 deletions(-) create mode 100644 examples/bad-files-examples/fileWithParseErrors.wlk create mode 100644 examples/bad-files-examples/fileWithValidationErrors.wlk create mode 100644 test/utils.test.ts diff --git a/examples/bad-files-examples/fileWithParseErrors.wlk b/examples/bad-files-examples/fileWithParseErrors.wlk new file mode 100644 index 0000000..230af0b --- /dev/null +++ b/examples/bad-files-examples/fileWithParseErrors.wlk @@ -0,0 +1,7 @@ +object pepita { + var energia = 0 + var extra = 0 + + // method without parentheses does not parse ok + method energiaTotal = energia + extra +} \ No newline at end of file diff --git a/examples/bad-files-examples/fileWithValidationErrors.wlk b/examples/bad-files-examples/fileWithValidationErrors.wlk new file mode 100644 index 0000000..52e8159 --- /dev/null +++ b/examples/bad-files-examples/fileWithValidationErrors.wlk @@ -0,0 +1,5 @@ +class A { + method go() { + self = 1 + } +} diff --git a/package-lock.json b/package-lock.json index a8e79f8..316e26d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,14 +27,17 @@ }, "devDependencies": { "@types/chai": "^4.3.9", + "@types/chai-as-promised": "^7.1.7", "@types/cytoscape": "^3.19.7", "@types/express": "^4.17.20", "@types/mocha": "^10.0.3", "@types/node": "^18.14.5", "@types/p5": "^1.7.1", + "@types/sinon": "^17.0.0", "@typescript-eslint/eslint-plugin": "^6.9.1", "@typescript-eslint/parser": "^6.9.1", "chai": "^4.3.7", + "chai-as-promised": "^7.1.1", "eslint": "^8.52.0", "mocha": "^10.2.0", "nyc": "^15.1.0", @@ -1074,6 +1077,15 @@ "integrity": "sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg==", "dev": true }, + "node_modules/@types/chai-as-promised": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.7.tgz", + "integrity": "sha512-APucaP5rlmTRYKtRA6FE5QPP87x76ejw5t5guRJ4y5OgMnwtsvigw7HHhKZlx2MGXLeZd6R/GNZR/IqDHcbtQw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.35", "dev": true, @@ -1192,6 +1204,21 @@ "@types/node": "*" } }, + "node_modules/@types/sinon": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.0.tgz", + "integrity": "sha512-oN4AeDMFCeNZrAffCjhLcwwVymRZL2c9mljUmhPnd0eiM06d4ELDg0Q0TSvnZXrCIFlSA859qIdcfu1HapswPQ==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.4.tgz", + "integrity": "sha512-GDV68H0mBSN449sa5HEj51E0wfpVQb8xNSMzxf/PrypMFcLTMwJMOM/cgXiv71Mq5drkOQmUGvL1okOZcu6RrQ==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz", @@ -1832,6 +1859,18 @@ "node": ">=4" } }, + "node_modules/chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 5" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6437,6 +6476,15 @@ "integrity": "sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg==", "dev": true }, + "@types/chai-as-promised": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.7.tgz", + "integrity": "sha512-APucaP5rlmTRYKtRA6FE5QPP87x76ejw5t5guRJ4y5OgMnwtsvigw7HHhKZlx2MGXLeZd6R/GNZR/IqDHcbtQw==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, "@types/connect": { "version": "3.4.35", "dev": true, @@ -6549,6 +6597,21 @@ "@types/node": "*" } }, + "@types/sinon": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.0.tgz", + "integrity": "sha512-oN4AeDMFCeNZrAffCjhLcwwVymRZL2c9mljUmhPnd0eiM06d4ELDg0Q0TSvnZXrCIFlSA859qIdcfu1HapswPQ==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.4.tgz", + "integrity": "sha512-GDV68H0mBSN449sa5HEj51E0wfpVQb8xNSMzxf/PrypMFcLTMwJMOM/cgXiv71Mq5drkOQmUGvL1okOZcu6RrQ==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz", @@ -6935,6 +6998,15 @@ "type-detect": "^4.0.8" } }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "^1.0.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", diff --git a/package.json b/package.json index ba540ac..548fc70 100644 --- a/package.json +++ b/package.json @@ -52,14 +52,17 @@ }, "devDependencies": { "@types/chai": "^4.3.9", + "@types/chai-as-promised": "^7.1.7", "@types/cytoscape": "^3.19.7", "@types/express": "^4.17.20", "@types/mocha": "^10.0.3", "@types/node": "^18.14.5", "@types/p5": "^1.7.1", + "@types/sinon": "^17.0.0", "@typescript-eslint/eslint-plugin": "^6.9.1", "@typescript-eslint/parser": "^6.9.1", "chai": "^4.3.7", + "chai-as-promised": "^7.1.1", "eslint": "^8.52.0", "mocha": "^10.2.0", "nyc": "^15.1.0", diff --git a/src/commands/repl.ts b/src/commands/repl.ts index 1d53d9a..5547a7d 100644 --- a/src/commands/repl.ts +++ b/src/commands/repl.ts @@ -13,7 +13,7 @@ import link from 'wollok-ts/dist/linker' import { ParseError } from 'wollok-ts/dist/parser' import natives from 'wollok-ts/dist/wre/wre.natives' import { getDataDiagram } from '../services/diagram-generator' -import { buildEnvironmentForProject, failureDescription, getFQN, linkSentence, publicPath, successDescription, valueDescription, validateEnvironment } from '../utils' +import { buildEnvironmentForProject, failureDescription, getFQN, linkSentence, publicPath, successDescription, valueDescription, validateEnvironment, handleError } from '../utils' export const REPL = 'REPL' @@ -94,17 +94,11 @@ export async function initializeInterpreter(autoImportPath: string | undefined, const replPackage = new Package({ name: REPL }) environment = link([replPackage], environment) } + return new Interpreter(Evaluation.build(environment, natives)) } catch (error: any) { - if (error.level === 'error') { - logger.error(failureDescription('Exiting REPL due to validation errors!')) - } else { - logger.error(failureDescription('Uh-oh... Unexpected Error!')) - logger.debug(failureDescription('Stack trace:', error)) - } - process.exit() + handleError(error) + return process.exit(1) } - - return new Interpreter(Evaluation.build(environment, natives)) } // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ diff --git a/src/commands/test.ts b/src/commands/test.ts index bbe1f5d..327f831 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -5,7 +5,7 @@ import { Entity, Node, Test } from 'wollok-ts' import { is, match, when } from 'wollok-ts/dist/extensions' import interpret from 'wollok-ts/dist/interpreter/interpreter' import natives from 'wollok-ts/dist/wre/wre.natives' -import { buildEnvironmentForProject, failureDescription, successDescription, valueDescription, validateEnvironment } from '../utils' +import { buildEnvironmentForProject, failureDescription, successDescription, valueDescription, validateEnvironment, handleError } from '../utils' const { log } = console @@ -15,67 +15,73 @@ type Options = { } export default async function (filter: string | undefined, { project, skipValidations }: Options): Promise { - logger.info(`Running all tests ${filter ? `matching ${valueDescription(filter)} ` : ''}on ${valueDescription(project)}`) + try { - const environment = await buildEnvironmentForProject(project) + logger.info(`Running all tests ${filter ? `matching ${valueDescription(filter)} ` : ''}on ${valueDescription(project)}`) - validateEnvironment(environment, skipValidations) + const environment = await buildEnvironmentForProject(project) - const filterTest = filter?.replaceAll('"', '') ?? '' - const possibleTargets = environment.descendants.filter(is(Test)) - const onlyTarget = possibleTargets.find(test => test.isOnly) - const testMatches = (filter: string) => (test: Test) => !filter || test.fullyQualifiedName.replaceAll('"', '').includes(filterTest) - const targets = onlyTarget ? [onlyTarget] : possibleTargets.filter(testMatches(filterTest)) + validateEnvironment(environment, skipValidations) - logger.info(`Running ${targets.length} tests...`) + const filterTest = filter?.replaceAll('"', '') ?? '' + const possibleTargets = environment.descendants.filter(is(Test)) + const onlyTarget = possibleTargets.find(test => test.isOnly) + const testMatches = (filter: string) => (test: Test) => !filter || test.fullyQualifiedName.replaceAll('"', '').includes(filterTest) + const targets = onlyTarget ? [onlyTarget] : possibleTargets.filter(testMatches(filterTest)) - const debug = logger.getLevel() <= logger.levels.DEBUG - if (debug) time('Run finished') - const interpreter = interpret(environment, natives) - const failures: [Test, Error][] = [] - let successes = 0 + logger.info(`Running ${targets.length} tests...`) - environment.forEach(node => match(node)( - when(Test)(node => { - if (targets.includes(node)) { + const debug = logger.getLevel() <= logger.levels.DEBUG + if (debug) time('Run finished') + const interpreter = interpret(environment, natives) + const failures: [Test, Error][] = [] + let successes = 0 + + environment.forEach(node => match(node)( + when(Test)(node => { + if (targets.includes(node)) { + const tabulation = ' '.repeat(node.fullyQualifiedName.split('.').length - 1) + try { + interpreter.fork().exec(node) + logger.info(tabulation, successDescription(node.name)) + successes++ + } catch (error: any) { + logger.info(tabulation, failureDescription(node.name)) + failures.push([node, error]) + } + } + }), + + when(Entity)(node => { const tabulation = ' '.repeat(node.fullyQualifiedName.split('.').length - 1) - try { - interpreter.fork().exec(node) - logger.info(tabulation, successDescription(node.name)) - successes++ - } catch (error: any) { - logger.info(tabulation, failureDescription(node.name)) - failures.push([node, error]) + if(targets.some(target => node.descendants.includes(target))){ + logger.info(tabulation, node.name) } - } - }), + }), - when(Entity)(node => { - const tabulation = ' '.repeat(node.fullyQualifiedName.split('.').length - 1) - if(targets.some(target => node.descendants.includes(target))){ - logger.info(tabulation, node.name) - } - }), + when(Node)( _ => { }), + )) - when(Node)( _ => { }), - )) + log() + if (debug) timeEnd('Run finished') - log() - if (debug) timeEnd('Run finished') + failures.forEach(([test, error]) => { + log() + logger.error(failureDescription(bold(test.fullyQualifiedName), error)) + }) - failures.forEach(([test, error]) => { - log() - logger.error(failureDescription(bold(test.fullyQualifiedName), error)) - }) - - logger.info( - '\n', - successDescription(`${successes} passing`), - failures.length ? failureDescription(`${failures.length} failing`) : '', - '\n' - ) - - if (failures.length) { - process.exit(2) + logger.info( + '\n', + successDescription(`${successes} passing`), + failures.length ? failureDescription(`${failures.length} failing`) : '', + '\n' + ) + + if (failures.length) { + process.exit(2) + } + } catch (error: any) { + handleError(error) + return process.exit(1) } } \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 6f16270..0beaee0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { blue, bold, green, italic, red, yellowBright } from 'chalk' +import { blue, bold, green, italic, red, redBright, yellowBright } from 'chalk' import fs, { Dirent } from 'fs' import { readFile } from 'fs/promises' import globby from 'globby' @@ -44,11 +44,6 @@ export async function buildEnvironmentForProject(project: string, files: string[ if (debug) time('Building environment') try { return buildEnvironment(environmentFiles) } - catch (error: any) { - logger.error(failureDescription(`Fatal error while building the environment. ${error.message}`)) - logger.debug(error) - return process.exit(10) - } finally { if (debug) timeEnd('Building environment') } } @@ -61,16 +56,21 @@ export const validateEnvironment = (environment: Environment, skipValidations: b logger.info(successDescription('No problems found building the environment!')) } else if(problems.some(_ => _.level === 'error')) { - logger.error(failureDescription('Aborting run due to validation errors!')) - process.exit(1) + throw new Error('Aborting run due to validation errors!') } } catch (error: any) { - logger.error(failureDescription(`Fatal error while building the environment. ${error.message}`)) logger.debug(error) - process.exit(2) + throw new Error(`Fatal error while building the environment. ${error.message}`) } } } + +export const handleError = (error: any): void => { + logger.error(redBright('💥 Uh-oh... Unexpected Error!')) + logger.error(redBright(error.message)) + logger.debug(failureDescription('ℹ️ Stack trace:', error)) +} + // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // PRINTING // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ diff --git a/test/utils.test.ts b/test/utils.test.ts new file mode 100644 index 0000000..77e49ac --- /dev/null +++ b/test/utils.test.ts @@ -0,0 +1,26 @@ +import { join } from 'path' +import { buildEnvironmentForProject, validateEnvironment } from '../src/utils' +import chaiAsPromised from 'chai-as-promised' +import chai from 'chai' + +describe('build & validating environment', () => { + + const badProjectPath = join('examples', 'bad-files-examples') + + it('should throw an exception if parsing fails', async () => { + chai.use(chaiAsPromised) + const expect = chai.expect + await expect(buildEnvironmentForProject(badProjectPath, ['fileWithParseErrors.wlk'])).to.eventually.be.rejectedWith(/Failed to parse fileWithParseErrors.wlk/) + }) + + it('should throw an exception if validation fails', async () => { + const environment = await buildEnvironmentForProject(badProjectPath, ['fileWithValidationErrors.wlk']) + chai.expect(() => { validateEnvironment(environment, false) }).to.throw(/Fatal error while building the environment/) + }) + + it('should not throw an exception if validation fails but you want to skip validation', async () => { + const environment = await buildEnvironmentForProject(badProjectPath, ['fileWithValidationErrors.wlk']) + chai.expect(() => { validateEnvironment(environment, true) }).to.not.throw() + }) + +}) \ No newline at end of file