From 56f08d00a925807b957585b5964497f1973388ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Weslley=20Ara=C3=BAjo?= <46850407+wellwelwel@users.noreply.github.com> Date: Mon, 10 Jun 2024 06:21:23 -0300 Subject: [PATCH] feat(cli): show individual and total test execution times (#359) * feat(cli): show individual and total test execution times * chore: adapt logs --- src/@types/list-files.ts | 4 +- src/@types/poku.ts | 5 +++ src/configs/files.ts | 7 +++- src/helpers/logs.ts | 4 +- src/helpers/runner.ts | 1 + src/helpers/time.ts | 20 +++++++++ src/modules/describe.ts | 11 ++++- src/modules/exit.ts | 14 ++++++- src/modules/it.ts | 9 +++- src/modules/poku.ts | 41 ++++++++++++++----- src/modules/test.ts | 11 ++++- src/services/run-test-file.ts | 17 ++++++-- src/services/run-tests.ts | 7 +++- test/unit/deno/allow.test.ts | 1 + .../docs/documentation/poku/options/deno.mdx | 2 +- 15 files changed, 124 insertions(+), 30 deletions(-) create mode 100644 src/helpers/time.ts diff --git a/src/@types/list-files.ts b/src/@types/list-files.ts index 39743f8d..041648de 100644 --- a/src/@types/list-files.ts +++ b/src/@types/list-files.ts @@ -16,8 +16,8 @@ export type Configs = { }; export type FileResults = { - success: string[]; - fail: string[]; + success: Map; + fail: Map; }; /* c8 ignore stop */ diff --git a/src/@types/poku.ts b/src/@types/poku.ts index 2d2b6d6d..1f27e09a 100644 --- a/src/@types/poku.ts +++ b/src/@types/poku.ts @@ -102,4 +102,9 @@ export type Configs = { deno?: DenoOptions; } & ListFilesConfigs; +export type FinalResults = { + time: string; + started: Date; +}; + /* c8 ignore stop */ diff --git a/src/configs/files.ts b/src/configs/files.ts index 0ecc3474..746a41c3 100644 --- a/src/configs/files.ts +++ b/src/configs/files.ts @@ -1,10 +1,13 @@ /* c8 ignore start */ import type { FileResults } from '../@types/list-files.js'; +import type { FinalResults } from '../@types/poku.js'; export const fileResults: FileResults = { - success: [], - fail: [], + success: new Map(), + fail: new Map(), }; +export const finalResults = {} as FinalResults; + /* c8 ignore stop */ diff --git a/src/helpers/logs.ts b/src/helpers/logs.ts index 1f9563dd..d2a6617a 100644 --- a/src/helpers/logs.ts +++ b/src/helpers/logs.ts @@ -1,6 +1,6 @@ /* c8 ignore start */ -import { stdout } from 'node:process'; +import process from 'node:process'; import { EOL } from 'node:os'; import type { Configs } from '../@types/poku.js'; @@ -10,7 +10,7 @@ export const isQuiet = (configs?: Configs): boolean => export const isDebug = (configs?: Configs): boolean => Boolean(configs?.debug); export const write = (data: string | Uint8Array) => - stdout.write(`${String(data)}\n`); + process.stdout.write(`${String(data)}\n`); export const printOutput = (options: { output: string; diff --git a/src/helpers/runner.ts b/src/helpers/runner.ts index 6ca1b8e6..a9f51196 100644 --- a/src/helpers/runner.ts +++ b/src/helpers/runner.ts @@ -26,6 +26,7 @@ export const runner = (filename: string, configs?: Configs): string[] => { '--allow-env', // Poku share the process.env with the `child_process` '--allow-run', // Poku CLI '--allow-net', // Create Service + '--allow-hrtime', // Precise performance execution time ]; const denoDeny = configs?.deno?.deny diff --git a/src/helpers/time.ts b/src/helpers/time.ts new file mode 100644 index 00000000..eb1a1fa3 --- /dev/null +++ b/src/helpers/time.ts @@ -0,0 +1,20 @@ +/* c8 ignore start */ + +import { padStart } from '../polyfills/pad.js'; + +export const setTime = (date: Date): string => { + const hours = padStart(date.getHours().toString(), 2, '0'); + const minutes = padStart(date.getMinutes().toString(), 2, '0'); + const seconds = padStart(date.getSeconds().toString(), 2, '0'); + + return `${hours}:${minutes}:${seconds}`; +}; + +export const toSecs = (milliseconds: string): string => { + const ms = parseFloat(milliseconds); + const seconds = (ms / 1000).toFixed(2); + + return seconds; +}; + +/* c8 ignore stop */ diff --git a/src/modules/describe.ts b/src/modules/describe.ts index 9a04ca69..f7fd540e 100644 --- a/src/modules/describe.ts +++ b/src/modules/describe.ts @@ -1,3 +1,4 @@ +import process from 'node:process'; import { format, backgroundColor } from '../helpers/format.js'; import { write } from '../helpers/logs.js'; /* c8 ignore next */ @@ -39,7 +40,7 @@ export async function describe( indentation.hasDescribe = true; const { background, icon } = options || {}; - const message = `${cb ? format.dim('◌') : icon || '☰'} ${format.bold(title) || ''}`; + const message = `${cb ? format.dim('◌') : icon || '☰'} ${cb ? format.dim(title) : format.bold(title) || ''}`; const noBackground = !background; if (noBackground) write(`${format.bold(message)}`); @@ -53,15 +54,21 @@ export async function describe( if (typeof cb !== 'function') return; + const start = process.hrtime(); const resultCb = cb(); /* c8 ignore next */ if (resultCb instanceof Promise) await resultCb; + const end = process.hrtime(start); /* c8 ignore start */ if (title) { + const total = (end[0] * 1e3 + end[1] / 1e6).toFixed(6); + indentation.hasDescribe = false; - write(`${format.bold(format.success('●'))} ${format.bold(title)}`); + write( + `${format.bold(format.success('●'))} ${format.bold(format.success(title))} ${format.dim(`› ${total}ms`)}` + ); } /* c8 ignore stop */ } diff --git a/src/modules/exit.ts b/src/modules/exit.ts index 4d1be25b..553bc732 100644 --- a/src/modules/exit.ts +++ b/src/modules/exit.ts @@ -6,6 +6,8 @@ import { results } from '../services/run-tests.js'; import { format } from '../helpers/format.js'; import type { Code } from '../@types/code.js'; import { write } from '../helpers/logs.js'; +import { fileResults, finalResults } from '../configs/files.js'; +import { setTime, toSecs } from '../helpers/time.js'; export const exit = (code: Code, quiet?: boolean) => { const isPoku = results.success > 0 || results.fail > 0; @@ -13,6 +15,16 @@ export const exit = (code: Code, quiet?: boolean) => { !quiet && process.on('exit', (code) => { if (isPoku) { + hr(); + write( + ` ${format.dim('Start at ›')} ${format.bold(format.dim(`${setTime(finalResults.started)}`))}` + ); + write( + ` ${format.dim('Duration ›')} ${format.bold(format.dim(`${finalResults.time}ms`))} ${`${format.dim(`(±${toSecs(finalResults.time)} seconds)`)}`}` + ); + write( + ` ${format.dim('Test Files ›')} ${format.bold(format.dim(`${fileResults.success.size + fileResults.fail.size}`))}` + ); hr(); write( format.bg(42, `PASS › ${results.success}`) + @@ -23,7 +35,7 @@ export const exit = (code: Code, quiet?: boolean) => { } write( - `${format.dim('Exited with code')} ${format.bold(format?.[code === 0 ? 'success' : 'fail'](String(code)))}` + `${format.dim('Exited with code')} ${format.bold(format?.[code === 0 ? 'success' : 'fail'](String(code)))}\n` ); }); diff --git a/src/modules/it.ts b/src/modules/it.ts index 99eab301..ddfd9559 100644 --- a/src/modules/it.ts +++ b/src/modules/it.ts @@ -1,3 +1,4 @@ +import process from 'node:process'; /* c8 ignore next */ import { each } from '../configs/each.js'; /* c8 ignore next */ @@ -37,15 +38,17 @@ export async function it( if (message) { indentation.hasIt = true; write( - `${indentation.hasDescribe ? ' ' : ''}${format.dim('◌')} ${format.bold(format.italic(format.dim(message)))}` + `${indentation.hasDescribe ? ' ' : ''}${format.dim('◌')} ${format.dim(message)}` ); } /* c8 ignore end */ + const start = process.hrtime(); const resultCb = cb(); /* c8 ignore next */ if (resultCb instanceof Promise) await resultCb; + const end = process.hrtime(start); if (typeof each.after.cb === 'function' && each.after.test) { const afterResult = each.after.cb(); @@ -55,9 +58,11 @@ export async function it( /* c8 ignore start */ if (message) { + const total = (end[0] * 1e3 + end[1] / 1e6).toFixed(6); + indentation.hasIt = false; write( - `${indentation.hasDescribe ? ' ' : ''}${format.bold(format.success('●'))} ${format.bold(format.italic(message))}` + `${indentation.hasDescribe ? ' ' : ''}${format.bold(format.success('●'))} ${format.bold(format.success(message))} ${format.dim(`› ${total}ms`)}` ); } /* c8 ignore stop */ diff --git a/src/modules/poku.ts b/src/modules/poku.ts index 249fc842..a244d94e 100644 --- a/src/modules/poku.ts +++ b/src/modules/poku.ts @@ -4,20 +4,20 @@ * Both CLI, API, noExit, sequential and parallel runs are strictly tested, but these tests use deep child process for it */ -import process, { stdout } from 'node:process'; +import process from 'node:process'; import { EOL } from 'node:os'; import { runTests, runTestsParallel } from '../services/run-tests.js'; import { exit } from './exit.js'; import { format } from '../helpers/format.js'; import { isQuiet, write } from '../helpers/logs.js'; import { hr } from '../helpers/hr.js'; -import { fileResults } from '../configs/files.js'; +import { fileResults, finalResults } from '../configs/files.js'; import { indentation } from '../configs/indentation.js'; import type { Code } from '../@types/code.js'; import type { Configs } from '../@types/poku.js'; process.once('SIGINT', () => { - stdout.write('\u001B[?25h'); + process.stdout.write('\u001B[?25h'); }); export async function poku( @@ -34,6 +34,9 @@ export async function poku( ): Promise { let code: Code = 0; + finalResults.started = new Date(); + + const start = process.hrtime(); const prepareDirs = Array.prototype.concat(targetPaths); const dirs = prepareDirs.length > 0 ? prepareDirs : ['.']; const showLogs = !isQuiet(configs); @@ -51,6 +54,11 @@ export async function poku( if (configs?.noExit) return code; + const end = process.hrtime(start); + const total = (end[0] * 1e3 + end[1] / 1e6).toFixed(6); + + finalResults.time = total; + exit(code, configs?.quiet); return; } @@ -73,26 +81,37 @@ export async function poku( const concurrency = await Promise.all(promises); if (concurrency.some((result) => !result)) code = 1; - } catch {} + } catch { + } finally { + const end = process.hrtime(start); + const total = (end[0] * 1e3 + end[1] / 1e6).toFixed(6); + + finalResults.time = total; + } showLogs && hr(); - if (showLogs && fileResults.success.length > 0) + if (showLogs && fileResults.success.size > 0) { write( - fileResults.success + Array.from(fileResults.success) .map( - (current) => - `${indentation.test}${format.success('✔')} ${format.dim(current)}` + ([file, time]) => + `${indentation.test}${format.success('✔')} ${format.dim(`${file} › ${time}ms`)}` ) .join(EOL) ); + } - if (showLogs && fileResults.fail.length > 0) + if (showLogs && fileResults.fail.size > 0) { write( - fileResults.fail - .map((current) => `${indentation.test}${format.fail('✘')} ${current}`) + Array.from(fileResults.success) + .map( + ([file, time]) => + `${indentation.test}${format.fail('✘')} ${format.dim(`${file} › ${time}ms`)}` + ) .join(EOL) ); + } if (configs?.noExit) return code; diff --git a/src/modules/test.ts b/src/modules/test.ts index f8ee9166..1bd2a050 100644 --- a/src/modules/test.ts +++ b/src/modules/test.ts @@ -1,3 +1,4 @@ +import process from 'node:process'; /* c8 ignore next */ import { each } from '../configs/each.js'; /* c8 ignore next */ @@ -37,14 +38,16 @@ export async function test( if (message) { indentation.hasTest = true; - write(`${format.dim('◌')} ${format.bold(message)}`); + write(`${format.dim('◌')} ${format.dim(message)}`); } /* c8 ignore stop */ + const start = process.hrtime(); const resultCb = cb(); /* c8 ignore next */ if (resultCb instanceof Promise) await resultCb; + const end = process.hrtime(start); if (typeof each.after.cb === 'function' && each.after.test) { const afterResult = each.after.cb(); @@ -54,8 +57,12 @@ export async function test( /* c8 ignore start */ if (message) { + const total = (end[0] * 1e3 + end[1] / 1e6).toFixed(6); + indentation.hasTest = false; - write(`${format.bold(format.success('●'))} ${format.bold(message)}`); + write( + `${format.bold(format.success('●'))} ${format.bold(format.success(message))} ${format.dim(`› ${total}ms`)}` + ); } /* c8 ignore stop */ } diff --git a/src/services/run-test-file.ts b/src/services/run-test-file.ts index 1ac51f53..b110b079 100644 --- a/src/services/run-test-file.ts +++ b/src/services/run-test-file.ts @@ -46,6 +46,9 @@ export const runTestFile = ( ); } + const start = process.hrtime(); + let end: ReturnType; + if (!(await beforeEach(fileRelative, configs))) return false; // Export spawn helper is not an option @@ -64,6 +67,8 @@ export const runTestFile = ( child.stderr.on('data', stdOut); child.on('close', async (code) => { + end = process.hrtime(start); + const result = code === 0; if (showLogs) @@ -75,16 +80,22 @@ export const runTestFile = ( if (!(await afterEach(fileRelative, configs))) return false; - if (result) fileResults.success.push(fileRelative); - else fileResults.fail.push(fileRelative); + const total = (end[0] * 1e3 + end[1] / 1e6).toFixed(6); + + if (result) fileResults.success.set(fileRelative, total); + else fileResults.fail.set(fileRelative, total); resolve(result); }); /* c8 ignore start */ child.on('error', (err) => { + end = process.hrtime(start); + + const total = (end[0] * 1e3 + end[1] / 1e6).toFixed(6); + console.error(`Failed to start test: ${filePath}`, err); - fileResults.fail.push(fileRelative); + fileResults.fail.set(fileRelative, total); resolve(false); }); diff --git a/src/services/run-tests.ts b/src/services/run-tests.ts index 6d1021af..a6ba3309 100644 --- a/src/services/run-tests.ts +++ b/src/services/run-tests.ts @@ -50,13 +50,16 @@ export const runTests = async ( const filePath = files[i]; const fileRelative = relative(cwd, filePath); + const start = process.hrtime(); const testPassed = await runTestFile(filePath, configs); + const end = process.hrtime(start); + const total = (end[0] * 1e3 + end[1] / 1e6).toFixed(6); const testNumber = i + 1; const counter = format.counter(testNumber, totalTests); - const command = `${runner(fileRelative, configs).join(' ')} ${fileRelative}`; + const command = `${runner(fileRelative, configs).join(' ')} ${fileRelative} ${format.dim(`› ${total}ms`)}`; const nextLine = i + 1 !== files.length ? EOL : ''; - const log = `${counter}/${totalTests} ${command}`; + const log = `${counter}/${totalTests} ${command} `; if (testPassed) { ++results.success; diff --git a/test/unit/deno/allow.test.ts b/test/unit/deno/allow.test.ts index 7234945f..e6f421ed 100644 --- a/test/unit/deno/allow.test.ts +++ b/test/unit/deno/allow.test.ts @@ -17,6 +17,7 @@ test(() => { '--allow-env', '--allow-run', '--allow-net', + '--allow-hrtime', ], 'Default Permissions' ); diff --git a/website/docs/documentation/poku/options/deno.mdx b/website/docs/documentation/poku/options/deno.mdx index 5a8058ea..68adef3b 100644 --- a/website/docs/documentation/poku/options/deno.mdx +++ b/website/docs/documentation/poku/options/deno.mdx @@ -12,7 +12,7 @@ sidebar_position: 98 Change permissions for **Deno**. -By default **Poku** uses `--allow-run`, `--allow-env`, `--allow-read` and `--allow-net`. +By default **Poku** uses `--allow-run`, `--allow-env`, `--allow-read`, `allow-hrtime` and `--allow-net`. ### API (_in-code_)