From e6654c410eaf2e7a564c58dfe8766fe3f6859e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Weslley=20Ara=C3=BAjo?= <46850407+wellwelwel@users.noreply.github.com> Date: Thu, 25 Jul 2024 05:09:48 -0300 Subject: [PATCH] fix: ensure `afterEach` execution after a test failure (#598) * ci(tdd): include a failure case * ci: fix test failure case * chore: unify `it` and `test` * ci: improve tests * ci: fix tests * chore: better name `hasIt` to `hasItOrTest` --- .gitignore | 1 + fixtures/each-api/after-failure.test.ts | 68 +++++++++++++ fixtures/no-runner/basic-logs.test.ts | 54 +++++++++++ src/configs/indentation.ts | 3 +- src/modules/helpers/describe.ts | 1 - src/modules/helpers/it.ts | 95 +++++++++++-------- src/modules/helpers/test.ts | 78 +-------------- src/services/assert.ts | 11 +-- test/c8.test.ts | 14 +++ test/e2e/basic-logs.test.ts | 41 ++++++++ test/e2e/each-api/failure.test.ts | 24 +++++ test/e2e/each-api/order.test.ts | 1 + .../before-and-after-each/invalid.test.ts | 25 +++++ test/integration/it/it.test.ts | 3 + 14 files changed, 291 insertions(+), 128 deletions(-) create mode 100644 fixtures/each-api/after-failure.test.ts create mode 100644 fixtures/no-runner/basic-logs.test.ts create mode 100644 test/e2e/basic-logs.test.ts create mode 100644 test/e2e/each-api/failure.test.ts create mode 100644 test/integration/before-and-after-each/invalid.test.ts diff --git a/.gitignore b/.gitignore index 35044a06..8ac24017 100755 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ node_modules .env .nyc_output /.temp +/.temp2 /test-src /test-tests /test-before-and-after-each.json diff --git a/fixtures/each-api/after-failure.test.ts b/fixtures/each-api/after-failure.test.ts new file mode 100644 index 00000000..d701ce1d --- /dev/null +++ b/fixtures/each-api/after-failure.test.ts @@ -0,0 +1,68 @@ +import { readFile, writeFile, rm, mkdir } from 'node:fs/promises'; +import { + beforeEach, + afterEach, + log, + test, + describe, + assert, +} from '../../src/modules/index.js'; + +const testDir = '../../.temp2'; +const testFile = `${testDir}/each-hook`; + +const clearFixture = async () => { + try { + await rm(testFile); + await rm(testDir, { recursive: true, force: true }); + log(' - Cleaning'); + } catch {} +}; + +const writeFixture = async () => { + await mkdir(testDir); + await writeFile(testFile, 'test', 'utf-8'); + log(' - Writing'); +}; + +const toTest = async (message: string) => { + await readFile(testFile, 'utf-8'); + return message; +}; + +beforeEach(async () => { + log('- before beforeEach writeFixture'); + + await writeFixture(); + + log('- after beforeEach writeFixture'); +}); + +afterEach(async () => { + log('- before afterEach clearFixture'); + + await clearFixture(); + + log('- after afterEach clearFixture'); +}); + +describe(async () => { + await clearFixture(); + + await test('first test', async () => { + log(' before first test'); + + assert(true, await toTest('first test')); + + log(' after first test'); + }); + + await writeFixture(); + await test('Force failure', async () => { + log(' before first test'); + + assert(true, await toTest('first test')); + + log(' after first test'); + }); +}); diff --git a/fixtures/no-runner/basic-logs.test.ts b/fixtures/no-runner/basic-logs.test.ts new file mode 100644 index 00000000..065004c0 --- /dev/null +++ b/fixtures/no-runner/basic-logs.test.ts @@ -0,0 +1,54 @@ +import { assert } from '../../src/modules/essentials/assert.js'; +import { test } from '../../src/modules/helpers/test.js'; +import { describe } from '../../src/modules/helpers/describe.js'; +import { it } from '../../src/modules/helpers/it.js'; + +assert(true, 'Should emit a basic assetion log'); + +test('Should emit a basic test scope log', () => { + // ... +}); + +describe('Should emit a basic test scope log', () => { + // ... +}); + +it('Should emit a basic it scope log', () => { + // ... +}); + +test('Should emit a basic test scope log', () => { + assert(true, 'Should emit a basic assetion log'); +}); + +describe('Should emit a basic test scope log', () => { + assert(true, 'Should emit a basic assetion log'); +}); + +it('Should emit a basic test scope log', () => { + assert(true, 'Should emit a basic assetion log'); +}); + +describe('Should emit a basic test scope log', () => { + it('Should emit a basic it scope log', () => { + // ... + }); +}); + +describe('Should emit a basic test scope log', () => { + test('Should emit a basic test scope log', () => { + // ... + }); +}); + +describe('Should emit a basic test scope log', () => { + it('Should emit a basic it scope log', () => { + assert(true, 'Should emit a basic assetion log'); + }); +}); + +describe('Should emit a basic test scope log', () => { + test('Should emit a basic test scope log', () => { + assert(true, 'Should emit a basic assetion log'); + }); +}); diff --git a/src/configs/indentation.ts b/src/configs/indentation.ts index 65e1c83f..78bbd4f2 100644 --- a/src/configs/indentation.ts +++ b/src/configs/indentation.ts @@ -2,6 +2,5 @@ export const indentation = { test: ' ', stdio: ' ', hasDescribe: false, - hasTest: false, - hasIt: false, + hasItOrTest: false, }; diff --git a/src/modules/helpers/describe.ts b/src/modules/helpers/describe.ts index e345874c..b42015cc 100644 --- a/src/modules/helpers/describe.ts +++ b/src/modules/helpers/describe.ts @@ -43,7 +43,6 @@ export async function describe( indentation.hasDescribe = true; const { background, icon } = options || {}; - /* c8 ignore next */ const message = `${cb ? format('◌').dim() : icon || '☰'} ${cb ? format(isPoku ? `${title} › ${format(`${FILE}`).italic().gray()}` : title).dim() : format(title).bold() || ''}`; const noBackground = !background; diff --git a/src/modules/helpers/it.ts b/src/modules/helpers/it.ts index 5a8fd0ee..7b96eb9d 100644 --- a/src/modules/helpers/it.ts +++ b/src/modules/helpers/it.ts @@ -19,61 +19,74 @@ export async function it( (() => unknown | Promise)?, ] ): Promise { - let message: string | undefined; - let cb: () => unknown | Promise; + try { + let message: string | undefined; + let cb: () => unknown | Promise; - const isPoku = typeof env?.FILE === 'string' && env?.FILE.length > 0; - const FILE = env.FILE; + const isPoku = typeof env?.FILE === 'string' && env?.FILE.length > 0; + const FILE = env.FILE; - if (typeof args[0] === 'string') { - message = args[0]; - cb = args[1] as () => unknown | Promise; - } else { - cb = args[0] as () => unknown | Promise; - } + if (typeof args[0] === 'string') { + message = args[0]; + cb = args[1] as () => unknown | Promise; + } else { + cb = args[0] as () => unknown | Promise; + } - if (message) { - indentation.hasIt = true; + if (message) { + indentation.hasItOrTest = true; - Write.log( - isPoku && !indentation.hasDescribe - ? /* c8 ignore next 2 */ - `${indentation.hasDescribe ? ' ' : ''}${format(`◌ ${message} › ${format(`${FILE}`).italic().gray()}`).dim()}` - : `${indentation.hasDescribe ? ' ' : ''}${format(`◌ ${message}`).dim()}` - ); - } + Write.log( + isPoku && !indentation.hasDescribe + ? `${indentation.hasDescribe ? ' ' : ''}${format(`◌ ${message} › ${format(`${FILE}`).italic().gray()}`).dim()}` + : `${indentation.hasDescribe ? ' ' : ''}${format(`◌ ${message}`).dim()}` + ); + } - if (typeof each.before.cb === 'function') { - const beforeResult = each.before.cb(); + if (typeof each.before.cb === 'function') { + const beforeResult = each.before.cb(); - if (beforeResult instanceof Promise) { - await beforeResult; + if (beforeResult instanceof Promise) { + await beforeResult; + } } - } - const start = hrtime(); - const resultCb = cb(); + const start = hrtime(); + const resultCb = cb(); - if (resultCb instanceof Promise) { - await resultCb; - } + if (resultCb instanceof Promise) { + await resultCb; + } - const end = hrtime(start); + const end = hrtime(start); - if (typeof each.after.cb === 'function') { - const afterResult = each.after.cb(); + if (typeof each.after.cb === 'function') { + const afterResult = each.after.cb(); - if (afterResult instanceof Promise) { - await afterResult; + if (afterResult instanceof Promise) { + await afterResult; + } } - } - if (message) { - const total = (end[0] * 1e3 + end[1] / 1e6).toFixed(6); + if (message) { + const total = (end[0] * 1e3 + end[1] / 1e6).toFixed(6); + + indentation.hasItOrTest = false; + Write.log( + `${indentation.hasDescribe ? ' ' : ''}${format(`● ${message}`).success().bold()} ${format(`› ${total}ms`).success().dim()}` + ); + } + } catch (error) { + indentation.hasItOrTest = false; + + if (typeof each.after.cb === 'function') { + const afterResult = each.after.cb(); + + if (afterResult instanceof Promise) { + await afterResult; + } + } - indentation.hasIt = false; - Write.log( - `${indentation.hasDescribe ? ' ' : ''}${format(`● ${message}`).success().bold()} ${format(`› ${total}ms`).success().dim()}` - ); + throw error; } } diff --git a/src/modules/helpers/test.ts b/src/modules/helpers/test.ts index a6c1e8a8..a5e1bfa2 100644 --- a/src/modules/helpers/test.ts +++ b/src/modules/helpers/test.ts @@ -1,79 +1,5 @@ /* c8 ignore next */ // ? -import { hrtime, env } from 'node:process'; -import { each } from '../../configs/each.js'; -import { indentation } from '../../configs/indentation.js'; -import { format } from '../../services/format.js'; -import { Write } from '../../services/write.js'; +import { it } from './it.js'; -export async function test( - message: string, - cb: () => Promise -): Promise; -export function test(message: string, cb: () => unknown): void; -export async function test(cb: () => Promise): Promise; -export function test(cb: () => unknown): void; /* c8 ignore next */ // ? -export async function test( - ...args: [ - string | (() => unknown | Promise), - (() => unknown | Promise)?, - ] -): Promise { - let message: string | undefined; - let cb: () => unknown | Promise; - - const isPoku = typeof env?.FILE === 'string' && env?.FILE.length > 0; - const FILE = env.FILE; - - if (typeof args[0] === 'string') { - message = args[0]; - cb = args[1] as () => unknown | Promise; - } else { - cb = args[0] as () => unknown | Promise; - } - - if (message) { - indentation.hasTest = true; - - Write.log( - isPoku - ? /* c8 ignore next 2 */ - format(`◌ ${message} › ${format(`${FILE}`).italic().gray()}`).dim() - : format(`◌ ${message}`).dim() - ); - } - - if (typeof each.before.cb === 'function') { - const beforeResult = each.before.cb(); - - if (beforeResult instanceof Promise) { - await beforeResult; - } - } - - const start = hrtime(); - const resultCb = cb(); - - if (resultCb instanceof Promise) { - await resultCb; - } - - const end = hrtime(start); - - if (typeof each.after.cb === 'function') { - const afterResult = each.after.cb(); - - if (afterResult instanceof Promise) { - await afterResult; - } - } - - if (message) { - const total = (end[0] * 1e3 + end[1] / 1e6).toFixed(6); - - indentation.hasTest = false; - Write.log( - `${format(`● ${message}`).success().bold()} ${format(`› ${total}ms`).success().dim()}` - ); - } -} +export const test = it; diff --git a/src/services/assert.ts b/src/services/assert.ts index 9a57cfa5..a59833ce 100644 --- a/src/services/assert.ts +++ b/src/services/assert.ts @@ -23,11 +23,11 @@ export const processAssert = async ( const FILE = env.FILE; let preIdentation = ''; - if (indentation.hasDescribe || indentation.hasTest) { + if (indentation.hasDescribe) { preIdentation += ' '; } - if (indentation.hasIt) { + if (indentation.hasItOrTest) { preIdentation += ' '; } @@ -40,11 +40,7 @@ export const processAssert = async ( if (typeof options.message === 'string') { const message = - isPoku && - !indentation.hasDescribe && - !indentation.hasIt && - /* c8 ignore next 2 */ - !indentation.hasTest + isPoku && !indentation.hasDescribe && !indentation.hasItOrTest ? `${preIdentation}${format(`${format(`✔ ${options.message}`).bold()} ${format(`› ${FILE}`).success().dim()}`).success()}` : `${preIdentation}${format(`✔ ${options.message}`).success().bold()}`; @@ -115,7 +111,6 @@ export const processAssert = async ( // Non-assertion errors throw error; } - /* c8 ignore stop */ }; /* c8 ignore next */ // ? diff --git a/test/c8.test.ts b/test/c8.test.ts index facd7e90..75122097 100644 --- a/test/c8.test.ts +++ b/test/c8.test.ts @@ -2,8 +2,22 @@ import { env } from 'node:process'; import { poku, test, describe, it, assert } from '../src/modules/index.js'; import { isWindows } from '../src/parsers/get-runner.js'; import { inspectCLI } from './helpers/capture-cli.test.js'; +import { rmSync } from 'node:fs'; test(async () => { + const toRemove = [ + '/.temp', + '/test-src', + '/test-tests', + '/test-before-and-after-each.json', + ]; + + for (const path of toRemove) { + try { + rmSync(path, { force: true, recursive: true }); + } catch {} + } + await describe('CLI', async () => { await it('Sequential (Just Touch)', async () => { const results = await inspectCLI( diff --git a/test/e2e/basic-logs.test.ts b/test/e2e/basic-logs.test.ts new file mode 100644 index 00000000..e788d74e --- /dev/null +++ b/test/e2e/basic-logs.test.ts @@ -0,0 +1,41 @@ +import { describe } from '../../src/modules/helpers/describe.js'; +import { it } from '../../src/modules/helpers/it.js'; +import { assert } from '../../src/modules/essentials/assert.js'; +import { inspectCLI, isProduction } from '../helpers/capture-cli.test.js'; +import { skip } from '../../src/modules/helpers/skip.js'; + +if (isProduction) { + skip(); +} + +// const offset = 33; + +describe('Basic logs with Runner', async () => { + await it('Basic Logs without using Poku Runner', async () => { + const results = await inspectCLI( + 'npx tsx ./fixtures/no-runner/basic-logs.test.ts' + ); + + console.log(results.stdout); + console.log(results.stderr); + + assert.strictEqual(results.exitCode, 0, 'Exit Code needs to be 0'); + }); + + await it('Basic Logs without using Poku Runner + ENV', async () => { + const results = await inspectCLI( + 'npx tsx ./fixtures/no-runner/basic-logs.test.ts', + { + env: { + ...process.env, + FILE: 'path-file', + }, + } + ); + + console.log(results.stdout); + console.log(results.stderr); + + assert.strictEqual(results.exitCode, 0, 'Exit Code needs to be 0'); + }); +}); diff --git a/test/e2e/each-api/failure.test.ts b/test/e2e/each-api/failure.test.ts new file mode 100644 index 00000000..07a87da8 --- /dev/null +++ b/test/e2e/each-api/failure.test.ts @@ -0,0 +1,24 @@ +import { describe } from '../../../src/modules/helpers/describe.js'; +import { it } from '../../../src/modules/helpers/it.js'; +import { assert } from '../../../src/modules/essentials/assert.js'; +import { inspectCLI, isProduction } from '../../helpers/capture-cli.test.js'; +import { skip } from '../../../src/modules/helpers/skip.js'; +import { statSync } from 'node:fs'; + +if (isProduction) { + skip(); +} + +describe('Testing afterEach execution after a test failure', async () => { + await it(async () => { + const results = await inspectCLI( + 'npx tsx ../../src/bin/index.ts after-failure.test.ts -d', + { + cwd: 'fixtures/each-api', + } + ); + + assert.strictEqual(results.exitCode, 1, 'Exit Code needs to be 1'); + assert.throws(() => statSync('./.temp2')); + }); +}); diff --git a/test/e2e/each-api/order.test.ts b/test/e2e/each-api/order.test.ts index b2066d85..a91924bf 100644 --- a/test/e2e/each-api/order.test.ts +++ b/test/e2e/each-api/order.test.ts @@ -43,6 +43,7 @@ describe('Testing ', async () => { const actual = results.stdout.split('\n'); if (results.exitCode !== 0) { + console.log(results.stdout); console.log(results.stderr); } diff --git a/test/integration/before-and-after-each/invalid.test.ts b/test/integration/before-and-after-each/invalid.test.ts new file mode 100644 index 00000000..b46a94cd --- /dev/null +++ b/test/integration/before-and-after-each/invalid.test.ts @@ -0,0 +1,25 @@ +import { isProduction } from '../../helpers/capture-cli.test.js'; +import { skip } from '../../../src/modules/helpers/skip.js'; + +import { test } from '../../../src/modules/helpers/test.js'; +import { poku } from '../../../src/modules/essentials/poku.js'; +import { assert } from '../../../src/modules/essentials/assert.js'; + +if (isProduction) { + skip(); +} + +test('Before and After Each: updating an external file', async () => { + const prepareService = true; + const resetService = true; + + // @ts-expect-error + const exitCode = await poku('./test/integration/import.test.ts', { + noExit: true, + quiet: true, + beforeEach: prepareService, + afterEach: resetService, + }); + + assert.strictEqual(exitCode, 0, 'Should ignore if hooks are invalid'); +}); diff --git a/test/integration/it/it.test.ts b/test/integration/it/it.test.ts index 0aa501c2..1f0d025d 100644 --- a/test/integration/it/it.test.ts +++ b/test/integration/it/it.test.ts @@ -31,3 +31,6 @@ test('Testing "it" method', () => { it('it without describe', async () => await new Promise((resolve) => resolve(undefined))); }); + +it('it without scope', async () => + await new Promise((resolve) => resolve(undefined)));