From 8d0bca8f7811f065bbdbbc4a1c61aa1b27d57719 Mon Sep 17 00:00:00 2001 From: tolauwae Date: Thu, 5 Dec 2024 16:53:04 +0100 Subject: [PATCH] Add style abstraction --- src/framework/Framework.ts | 19 ++- src/framework/Testee.ts | 3 +- src/index.ts | 1 + src/{framework => reporter}/Reporter.ts | 183 ++++-------------------- src/reporter/Result.ts | 115 +++++++++++++++ src/reporter/Style.ts | 67 +++++++++ src/reporter/index.ts | 17 +++ src/util/printing.ts | 3 + 8 files changed, 244 insertions(+), 164 deletions(-) rename src/{framework => reporter}/Reporter.ts (53%) create mode 100644 src/reporter/Result.ts create mode 100644 src/reporter/Style.ts create mode 100644 src/reporter/index.ts create mode 100644 src/util/printing.ts diff --git a/src/framework/Framework.ts b/src/framework/Framework.ts index d9e9012..83d6459 100644 --- a/src/framework/Framework.ts +++ b/src/framework/Framework.ts @@ -3,7 +3,10 @@ import {HybridScheduler, Scheduler} from './Scheduler'; import {TestScenario} from './scenario/TestScenario'; import {TestbedSpecification} from '../testbeds/TestbedSpecification'; -import {Reporter, SuiteResults} from './Reporter'; +import {Reporter, SuiteResults} from '../reporter/Reporter'; + +import {StyleType} from '../reporter'; +import {styling} from '../reporter/Style'; export interface Suite { @@ -20,12 +23,6 @@ export interface TesteeOptions { connectionTimout?: number; } -export enum OutputStyle { - silent, // TODO - plain, - github -} - export class Suite { public title: string; public scenarios: TestScenario[] = []; @@ -61,11 +58,11 @@ export class Framework { public runs: number = 1; - private outputStyle: OutputStyle = OutputStyle.plain; + private outputStyle: StyleType = StyleType.plain; private scheduled: Suite[] = []; - public readonly reporter: Reporter = new Reporter(); + public readonly reporter: Reporter = new Reporter(styling(this.outputStyle)); private constructor() { } @@ -78,11 +75,11 @@ export class Framework { return this.scheduled; } - public style(style: OutputStyle): void { + public style(style: StyleType): void { this.outputStyle = style; } - public styling(): OutputStyle { + public styling(): StyleType { return this.outputStyle; } diff --git a/src/framework/Testee.ts b/src/framework/Testee.ts index 164684d..e25b911 100644 --- a/src/framework/Testee.ts +++ b/src/framework/Testee.ts @@ -9,9 +9,10 @@ import {TestScenario} from './scenario/TestScenario'; import {OutofPlaceSpecification, PlatformType, TestbedSpecification} from '../testbeds/TestbedSpecification'; import {CompileOutput, CompilerFactory} from '../manage/Compiler'; import {WABT} from '../util/env'; -import {Completion, expect, Result, ScenarioResult, SuiteResults} from './Reporter'; +import {Completion, expect, ScenarioResult, SuiteResults} from '../reporter/Reporter'; import {WASM} from '../sourcemap/Wasm'; import {DummyProxy} from '../testbeds/Emulator'; +import {Result} from '../reporter/Result'; export function timeout(label: string, time: number, promise: Promise): Promise { if (time === 0) { diff --git a/src/index.ts b/src/index.ts index fe202cb..b30a277 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,5 +15,6 @@ export * from './framework/scenario/Step'; export * from './framework/scenario/Invoker'; export * from './testbeds/TestbedSpecification'; export * from './debug/Breakpoint'; +export * from './reporter/index'; export const latch = Framework.getImplementation(); diff --git a/src/framework/Reporter.ts b/src/reporter/Reporter.ts similarity index 53% rename from src/framework/Reporter.ts rename to src/reporter/Reporter.ts index 5482f21..b765369 100644 --- a/src/framework/Reporter.ts +++ b/src/reporter/Reporter.ts @@ -1,10 +1,14 @@ -import {OutputStyle, Suite} from './Framework'; -import {Behaviour, Description, Step} from './scenario/Step'; -import {getValue, Testee} from './Testee'; -import {blue, bold, green, red, yellow, reset, inverse, grey} from 'ansi-colors'; -import {Archiver} from './Archiver'; -import {TestScenario} from './scenario/TestScenario'; +import {Suite} from '../framework/Framework'; +import {Step} from '../framework/scenario/Step'; +import {getValue, Testee} from '../framework/Testee'; +import {blue, bold, green, inverse, red, yellow} from 'ansi-colors'; +import {Archiver} from '../framework/Archiver'; +import {TestScenario} from '../framework/scenario/TestScenario'; import {version} from '../../package.json'; +import {indent} from '../util/printing'; +import {Result} from './Result'; +import {Verbosity} from './index'; +import {Style} from './Style'; export enum Completion { uncommenced = 'not started', // test hasn't started @@ -15,20 +19,6 @@ export enum Completion { skipped = 'skipped' // test has failing dependencies } -export enum Verbosity { - none, - minimal, - short, - normal, - more, - all, - debug -} - -function indent(level: number, size: number = 2): string { - return ' '.repeat(level * size); -} - export function expect(step: Step, actual: Object | void, previous?: Object): Result { const result: Result = new Result(step.title, ''); result.completion = Completion.succeeded; @@ -133,113 +123,6 @@ export class ScenarioResult { } } -export class Result { - public completion: Completion; // completion status of the step - public name: string; // name of the step - public description: string; - - constructor(name: string, description: string, completion?: Completion) { - this.name = name; - this.description = description; - this.completion = completion ?? Completion.uncommenced; - } - - report(level: number) { - console.log(reset(`${indent(level)}${this}`)); - } - - toString(): string { - switch (this.completion) { - case Completion.succeeded: - return `${bold(inverse(green(' PASS ')))} ${this.name}`; - case Completion.uncommenced: - return `${bold(inverse(yellow(' SKIP ')))} ${this.name}`; - case Completion.error: - case Completion.failed: - default: - return `${bold(inverse(red(' FAIL ')))} ${this.name}\n ${red(this.completion)}${red(this.description)}`; - - } - } - - error(description: string) { - this.completion = Completion.error; - this.description = description; - } - - public expectPrimitive(actual: T, expected: T): void { - // this.completion = deepEqual(actual, expected) ? Completion.succeeded : Completion.failed; - if (deepEqual(actual, expected)) { - this.completion = Completion.succeeded; - } else { - this.completion = Completion.failed; - this.description = `Expected ${bold(`${expected}`)} got ${bold(`${actual}`)}`; - } - } - - public expectDescription(actual: T, value: Description): void { - if ((value === Description.defined && actual !== undefined) || - value === Description.notDefined && actual === undefined) { - this.completion = Completion.succeeded; - } else { - this.completion = Completion.failed; - this.description = value === Description.defined ? 'Should exist' : 'Unexpected field'; - } - } - - public expectComparison(state: Object | void, actual: T, comparator: (state: Object, value: T) => boolean, message?: string): void { - if (state === undefined) { - this.completion = Completion.failed; - this.description = `Got unexpected ${state}`; - return; - } - - if (comparator(state, actual)) { - this.completion = Completion.succeeded; - } else { - this.completion = Completion.failed; - this.description = 'custom comparator failed'; - } - } - - public expectBehaviour(actual: any, previous: any, behaviour: Behaviour): void { - switch (behaviour) { - case Behaviour.unchanged: - if (deepEqual(actual, previous)) { - this.completion = Completion.succeeded; - } else { - this.completion = Completion.failed; - this.description = `Expected ${actual} to equal ${previous}` - } - break; - case Behaviour.changed: - if (!deepEqual(actual, previous)) { - this.completion = Completion.succeeded; - } else { - this.completion = Completion.failed; - this.description = `Expected ${actual} to be different from ${previous}` - } - break; - case Behaviour.increased: - if (actual > previous) { - this.completion = Completion.succeeded; - } else { - this.completion = Completion.failed; - this.description = `Expected ${actual} to be greater than ${previous}` - } - break; - case Behaviour.decreased: - if (actual < previous) { - this.completion = Completion.succeeded; - } else { - this.completion = Completion.failed; - this.description = `Expected ${actual} to be less than ${previous}` - } - break; - } - } -} - // const r Result = expect(e: Expected) <-- replaces the function in Testee todo export class Reporter { @@ -247,39 +130,42 @@ export class Reporter { private indentationLevel: number = 2; + private style: Style; + private suites: SuiteResults[] = []; private archiver: Archiver; private verbosity: Verbosity = Verbosity.short; - constructor() { + constructor(style: Style) { + this.style = style; this.archiver = new Archiver(`${process.env.TESTFILE?.replace('.asserts.wast', '.wast') ?? 'suite'}.${Date.now()}.log`); this.archiver.set('date', new Date(Date.now()).toISOString()); } private indent(override?: number) { - return indent(override ?? this.indentationLevel); + return indent(override ?? this.indentationLevel, this.style.indentation); } general() { - console.log(this.indent() + blue(bold('● latch')) + bold(' General information')); + console.log(this.indent() + this.style.colors.highlight(this.style.bullet) + this.style.colors.highlight('latch') + this.style.emph(' General information')); // console.log(blue(`${this.indent()}===================`)); - console.log(this.indent() + ' '.repeat(2) + bold('version') + ' '.repeat(5) + version); - console.log(this.indent() + ' '.repeat(2) + bold('archive') + ' '.repeat(5) + this.archiver.archive); + console.log(this.indent() + ' '.repeat(2) + this.style.emph('version') + ' '.repeat(5) + version); + console.log(this.indent() + ' '.repeat(2) + this.style.emph('archive') + ' '.repeat(5) + this.archiver.archive); console.log(); } report(suiteResult: SuiteResults) { this.suites.push(suiteResult); - const status = (suiteResult.error ? bold(inverse(red(' ERROR '))) : - (suiteResult.passing() ? bold(inverse(green(' PASSED '))) : bold(inverse(red(' FAIL '))))); - console.log(this.indent() + blue(bold('● suite')) + ` ${bold(suiteResult.title())}${(this.verbosity === Verbosity.minimal) ? ' ' + status : ''}`); + const status = (suiteResult.error ? this.style.colors.error(this.style.labels.error) : + (suiteResult.passing() ? this.style.colors.success(this.style.labels.suiteSuccess) : this.style.colors.failure(this.style.labels.failure))); + console.log(this.indent() + this.style.colors.highlight(this.style.bullet) + this.style.colors.highlight('suite') + ` ${this.style.emph(suiteResult.title())}${(this.verbosity === Verbosity.minimal) ? ' ' + status : ''}`); if (this.verbosity > Verbosity.minimal) { - console.log(this.indent() + ' '.repeat(2) + bold('testbed') + ' '.repeat(5) + suiteResult.testee.name); - console.log(this.indent() + ' '.repeat(2) + bold('scenarios') + ' '.repeat(3) + suiteResult.scenarios.length); - console.log(this.indent() + ' '.repeat(2) + bold('actions') + ' '.repeat(5) + suiteResult.suite.scenarios.flatMap((scenario) => scenario.steps ?? []).flat().length); //.reduce((total, count) => total + count)); - console.log(this.indent() + ' '.repeat(2) + bold('status') + ' '.repeat(6) + status); + console.log(this.indent() + ' '.repeat(2) + this.style.emph('testbed') + ' '.repeat(5) + suiteResult.testee.name); + console.log(this.indent() + ' '.repeat(2) + this.style.emph('scenarios') + ' '.repeat(3) + suiteResult.scenarios.length); + console.log(this.indent() + ' '.repeat(2) + this.style.emph('actions') + ' '.repeat(5) + suiteResult.suite.scenarios.flatMap((scenario) => scenario.steps ?? []).flat().length); //.reduce((total, count) => total + count)); + console.log(this.indent() + ' '.repeat(2) + this.style.emph('status') + ' '.repeat(6) + status); } console.log(); if (this.verbosity >= Verbosity.normal) { @@ -310,7 +196,7 @@ export class Reporter { this.archiver.set('skipped scenarios', skipped); this.archiver.set('failed scenarios', failing); - console.log(this.indent() + blue(bold('● results')) + bold(' Overview')); + console.log(this.indent() + this.style.colors.highlight(this.style.bullet) + this.style.colors.highlight('results') + this.style.emph(' Overview')); console.log(); this.indentationLevel += 1; @@ -331,12 +217,12 @@ export class Reporter { const len: number = 12; const pss = [`${sc} passing`, `${passing} passing`, `${psa} passing`] - console.log(this.indent() + bold('Test suites:') + ' '.repeat(len - pss[0].length) + bold((sc === tl ? green : red)(pss[0])) + `, ${tl} total` + bold(` (${time.toFixed(0)}ms)`)); + console.log(this.indent() + this.style.emph('Test suites:') + ' '.repeat(len - pss[0].length) + this.style.emph((sc === tl ? green : red)(pss[0])) + `, ${tl} total` + this.style.emph(` (${time.toFixed(0)}ms)`)); if (this.verbosity > Verbosity.minimal) { - console.log(this.indent() + bold('Scenarios:') + - ' '.repeat(2 + len - pss[1].length) + bold((passing === scs.length ? green : red)(pss[1])) + - (skipped > 0 ? ', ' + bold(yellow(`${skipped} skipped`)) : '') + `, ${scs.length} total`); - console.log(this.indent() + bold('Actions:') + ' '.repeat(4 + len - pss[2].length) + bold((passing === scs.length ? green : red)(pss[2])) + (timeouts > 0 ? `, ${timeouts} timeouts` : '') + `, ${total} total`); + console.log(this.indent() + this.style.emph('Scenarios:') + + ' '.repeat(2 + len - pss[1].length) + this.style.emph((passing === scs.length ? green : red)(pss[1])) + + (skipped > 0 ? ', ' + this.style.emph(yellow(`${skipped} skipped`)) : '') + `, ${scs.length} total`); + console.log(this.indent() + this.style.emph('Actions:') + ' '.repeat(4 + len - pss[2].length) + this.style.emph((passing === scs.length ? green : red)(pss[2])) + (timeouts > 0 ? `, ${timeouts} timeouts` : '') + `, ${total} total`); } this.indentationLevel -= 1; @@ -344,9 +230,6 @@ export class Reporter { this.archiver.write(); } - style(style: OutputStyle) { - } - info(text: string) { this.output += `info: ${text}\n`; } @@ -367,7 +250,3 @@ export class Reporter { this.output += ` ${result.toString()}\n`; } } - -function deepEqual(a: any, b: any): boolean { - return a === b || (isNaN(a) && isNaN(b)); -} diff --git a/src/reporter/Result.ts b/src/reporter/Result.ts new file mode 100644 index 0000000..daeee34 --- /dev/null +++ b/src/reporter/Result.ts @@ -0,0 +1,115 @@ +import {bold, green, inverse, red, reset, yellow} from 'ansi-colors'; +import {indent} from '../util/printing'; +import {Behaviour, Description} from '../framework/scenario/Step'; +import {Completion} from './Reporter'; + +export class Result { + public completion: Completion; // completion status of the step + public name: string; // name of the step + public description: string; + + constructor(name: string, description: string, completion?: Completion) { + this.name = name; + this.description = description; + this.completion = completion ?? Completion.uncommenced; + } + + report(level: number) { + console.log(reset(`${indent(level)}${this}`)); + } + + toString(): string { + switch (this.completion) { + case Completion.succeeded: + return `${bold(inverse(green(' PASS ')))} ${this.name}`; + case Completion.uncommenced: + return `${bold(inverse(yellow(' SKIP ')))} ${this.name}`; + case Completion.error: + case Completion.failed: + default: + return `${bold(inverse(red(' FAIL ')))} ${this.name}\n ${red(this.completion)}${red(this.description)}`; + + } + } + + error(description: string) { + this.completion = Completion.error; + this.description = description; + } + + public expectPrimitive(actual: T, expected: T): void { + // this.completion = deepEqual(actual, expected) ? Completion.succeeded : Completion.failed; + if (deepEqual(actual, expected)) { + this.completion = Completion.succeeded; + } else { + this.completion = Completion.failed; + this.description = `Expected ${bold(`${expected}`)} got ${bold(`${actual}`)}`; + } + } + + public expectDescription(actual: T, value: Description): void { + if ((value === Description.defined && actual !== undefined) || + value === Description.notDefined && actual === undefined) { + this.completion = Completion.succeeded; + } else { + this.completion = Completion.failed; + this.description = value === Description.defined ? 'Should exist' : 'Unexpected field'; + } + } + + public expectComparison(state: Object | void, actual: T, comparator: (state: Object, value: T) => boolean, message?: string): void { + if (state === undefined) { + this.completion = Completion.failed; + this.description = `Got unexpected ${state}`; + return; + } + + if (comparator(state, actual)) { + this.completion = Completion.succeeded; + } else { + this.completion = Completion.failed; + this.description = 'custom comparator failed'; + } + } + + public expectBehaviour(actual: any, previous: any, behaviour: Behaviour): void { + switch (behaviour) { + case Behaviour.unchanged: + if (deepEqual(actual, previous)) { + this.completion = Completion.succeeded; + } else { + this.completion = Completion.failed; + this.description = `Expected ${actual} to equal ${previous}` + } + break; + case Behaviour.changed: + if (!deepEqual(actual, previous)) { + this.completion = Completion.succeeded; + } else { + this.completion = Completion.failed; + this.description = `Expected ${actual} to be different from ${previous}` + } + break; + case Behaviour.increased: + if (actual > previous) { + this.completion = Completion.succeeded; + } else { + this.completion = Completion.failed; + this.description = `Expected ${actual} to be greater than ${previous}` + } + break; + case Behaviour.decreased: + if (actual < previous) { + this.completion = Completion.succeeded; + } else { + this.completion = Completion.failed; + this.description = `Expected ${actual} to be less than ${previous}` + } + break; + } + } +} + +function deepEqual(a: any, b: any): boolean { + return a === b || (isNaN(a) && isNaN(b)); +} diff --git a/src/reporter/Style.ts b/src/reporter/Style.ts new file mode 100644 index 0000000..747355a --- /dev/null +++ b/src/reporter/Style.ts @@ -0,0 +1,67 @@ +import {blue, bold, green, inverse, red, yellow} from 'ansi-colors'; +import {StyleType} from './index'; + +interface Styler { + (s: string): string; +} + +interface Colors { + highlight: Styler; + success: Styler; + skipped: Styler; + failure: Styler; + error: Styler; +} + +interface Labels { + suiteSuccess: string; + + success: string; + skipped: string; + failure: string; + error: string; +} + +// strategy factory +export function styling(type: StyleType): Style { + switch (type) { + case StyleType.github: + case StyleType.plain: + default: + return new Plain(); + } +} + +// strategy pattern +export interface Style { + indentation: number; + + bullet: string; + end: string; + + emph: Styler; + + colors: Colors; + labels: Labels; +} + +export class Plain implements Style { + indentation = 2; + bullet = '● '; + end = ''; + emph = (s: string) => bold(s); + colors: Colors = { + highlight: (s: string) => blue(s), + success: (s: string) => inverse(bold(green(s))), + skipped: (s: string) => inverse(bold(yellow(s))), + failure: (s: string) => inverse(bold(red(s))), + error: (s: string) => inverse(bold(red(s))) + }; + labels: Labels = { + suiteSuccess: ' PASSED ', + success: ' PASS ', + skipped: ' SKIP ', + failure: ' FAIL ', + error: ' ERROR ' + } +} \ No newline at end of file diff --git a/src/reporter/index.ts b/src/reporter/index.ts new file mode 100644 index 0000000..8eff848 --- /dev/null +++ b/src/reporter/index.ts @@ -0,0 +1,17 @@ +import {MinimalReporter} from './verbosity/minimal'; +import {announcer} from './Announcer'; + +export enum StyleType { + plain, + github +} + +export enum Verbosity { + none, + minimal, + short, + normal, + more, + all, + debug +} \ No newline at end of file diff --git a/src/util/printing.ts b/src/util/printing.ts new file mode 100644 index 0000000..af3b32d --- /dev/null +++ b/src/util/printing.ts @@ -0,0 +1,3 @@ +export function indent(level: number, size: number = 2): string { + return ' '.repeat(level * size); +} \ No newline at end of file