From d718c907e767d2def890bba2b055e046d66a27ca Mon Sep 17 00:00:00 2001 From: Haowen Chen Date: Mon, 4 Dec 2023 11:20:36 +0800 Subject: [PATCH] Refine --- src/etc.ts | 48 ---------- src/index.ts | 5 +- src/plugin/jestReporter.ts | 2 +- src/plugin/jestReporterSetup.ts | 2 +- src/plugin/karmaReporter.ts | 2 +- src/plugin/karmaReporterSetup.ts | 2 +- src/plugin/mochaReporter.ts | 2 +- src/reporters.ts | 149 ------------------------------- src/reporters/jest.ts | 111 +++++++++++++++++++++++ src/reporters/karma.ts | 68 ++++++++++++++ src/reporters/mocha.ts | 40 +++++++++ src/reporters/utils.ts | 26 ++++++ tests/index.test.ts | 2 +- tests/reporters.test.ts | 7 +- 14 files changed, 257 insertions(+), 209 deletions(-) delete mode 100644 src/etc.ts delete mode 100644 src/reporters.ts create mode 100644 src/reporters/jest.ts create mode 100644 src/reporters/karma.ts create mode 100644 src/reporters/mocha.ts create mode 100644 src/reporters/utils.ts diff --git a/src/etc.ts b/src/etc.ts deleted file mode 100644 index 9fd7f89..0000000 --- a/src/etc.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as fsModule from "fs"; - -export const STATE_FILE = ".kelonio.state.json"; -const HEADER_SIDE = "- ".repeat(17).trim(); -export const HEADER = `${HEADER_SIDE} Performance ${HEADER_SIDE}`; -export const FOOTER = "- ".repeat(40).trim(); - -let fs: typeof fsModule; -let canUseFs = true; -try { - fs = require("fs"); -} catch { - canUseFs = false; -} - -export class BenchmarkFileState { - constructor() { - if (!canUseFs) { - throw new Error("Unable to access file system"); - } - } - - exists(): boolean { - return fs.existsSync(STATE_FILE); - } - - read(): any { - return JSON.parse(fs.readFileSync(STATE_FILE, "utf-8")); - } - - write(data: object): void { - fs.writeFileSync(STATE_FILE, JSON.stringify(data), "utf-8"); - } - - append(data: object): void { - let previousData; - try { - previousData = this.read(); - } catch { - previousData = {}; - } - this.write({ ...previousData, ...data }); - } - - delete(): void { - try { fs.unlinkSync(STATE_FILE); } catch { } - } -} diff --git a/src/index.ts b/src/index.ts index d0f44fe..111bcde 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,7 @@ import hrtime from "browser-process-hrtime"; import { EventEmitter } from "events"; import * as mathjs from "mathjs"; -import { FOOTER, HEADER } from "./etc"; -import { JestReporter, KarmaReporter, MochaReporter } from "./reporters"; - -export { JestReporter, KarmaReporter, MochaReporter }; +import { FOOTER, HEADER } from "./reporters/utils"; export declare interface BenchmarkEventEmitter { emit(event: "record", description: Array, measurement: Measurement): boolean; diff --git a/src/plugin/jestReporter.ts b/src/plugin/jestReporter.ts index c5528b9..db312d9 100644 --- a/src/plugin/jestReporter.ts +++ b/src/plugin/jestReporter.ts @@ -1,3 +1,3 @@ -import { JestReporter } from ".."; +import { JestReporter } from "../reporters/jest"; export = JestReporter; diff --git a/src/plugin/jestReporterSetup.ts b/src/plugin/jestReporterSetup.ts index 8b59329..f035de4 100644 --- a/src/plugin/jestReporterSetup.ts +++ b/src/plugin/jestReporterSetup.ts @@ -1,3 +1,3 @@ -import { JestReporter } from ".."; +import { JestReporter } from "../reporters/jest"; JestReporter.initializeKelonio(); diff --git a/src/plugin/karmaReporter.ts b/src/plugin/karmaReporter.ts index 2990082..fe29566 100644 --- a/src/plugin/karmaReporter.ts +++ b/src/plugin/karmaReporter.ts @@ -1,4 +1,4 @@ -import { KarmaReporter } from ".."; +import { KarmaReporter } from "../reporters/karma"; module.exports = { "reporter:kelonio": ["type", KarmaReporter], diff --git a/src/plugin/karmaReporterSetup.ts b/src/plugin/karmaReporterSetup.ts index 20c0cb6..7c49736 100644 --- a/src/plugin/karmaReporterSetup.ts +++ b/src/plugin/karmaReporterSetup.ts @@ -1,3 +1,3 @@ -import { KarmaReporter } from ".."; +import { KarmaReporter } from "../reporters/karma"; KarmaReporter.initializeKelonio(); diff --git a/src/plugin/mochaReporter.ts b/src/plugin/mochaReporter.ts index 48ca13b..eafebab 100644 --- a/src/plugin/mochaReporter.ts +++ b/src/plugin/mochaReporter.ts @@ -1,3 +1,3 @@ -import { MochaReporter } from ".."; +import { MochaReporter } from "../reporters/mocha"; export = MochaReporter; diff --git a/src/reporters.ts b/src/reporters.ts deleted file mode 100644 index 37080c2..0000000 --- a/src/reporters.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { benchmark, Benchmark, Measurement } from "."; -import { BenchmarkFileState } from "./etc"; - -const MOCHA_EVENT_TEST_BEGIN = "test"; -const MOCHA_EVENT_RUN_END = "end"; -type KarmaLoggedRecord = { description: Array, durations: Array, totalDuration: number }; -type Extension = { - extraReport?: (benchmark: Benchmark) => string | void; -}; -type ExtensionLookup = { module: string, extension: string }; - -interface MochaReporterOptions { - reporterOptions: { - inferDescriptions?: boolean; - printReportAtEnd?: boolean; - extensions?: Array; - }; -} - -interface KarmaReporterOptions { - inferBrowsers?: boolean; - printReportAtEnd?: boolean; - extensions?: Array; -} - -interface JestReporterOptions { - keepStateAtStart?: boolean; - keepStateAtEnd?: boolean; - printReportAtEnd?: boolean; - extensions?: Array; -} - -function handleExtraReports(lookups: Array | undefined, benchmark: Benchmark, print: (report: string) => void): void { - for (const lookup of (lookups ?? [])) { - const extension: Extension | undefined = require(lookup.module)?.[lookup.extension]; - const report = extension?.extraReport?.(benchmark); - if (report) { - print(report); - } - } -} - -export class JestReporter implements jest.Reporter { - options: JestReporterOptions = { keepStateAtStart: false, keepStateAtEnd: false, printReportAtEnd: true }; - constructor(testData?: any, options?: JestReporterOptions) { - if (options) { - this.options = { ...this.options, ...options }; - } - } - static initializeKelonio(): void { - const state = new BenchmarkFileState(); - benchmark.events.on("record", (description, measurement) => { - const b = new Benchmark(); - if (state.exists()) { - b.data = state.read(); - } - b.incorporate(description, measurement); - state.write(b.data); - }); - } - - onRunStart(): void { - const state = new BenchmarkFileState(); - if (this.options.keepStateAtStart) { - state.append({}); - } else { - state.write({}); - } - } - - onRunComplete(): void { - const state = new BenchmarkFileState(); - if (!state.exists()) { - throw new Error( - "The Kelonio reporter for Jest requires benchmark serialization." - + " Make sure to call `JestReporter.initializeKelonio()`." - ); - } - - const b = new Benchmark(); - b.data = state.read(); - - if (this.options.printReportAtEnd) { - console.log(`\n${b.report()}`); - handleExtraReports(this.options.extensions, b, console.log); - } - - if (!this.options.keepStateAtEnd) { - state.delete(); - } - } -} - -export class KarmaReporter { - protected onBrowserLog: (browser: string, log: string, type: string) => void; - protected onRunComplete: () => void; - - static initializeKelonio(): void { - benchmark.events.on("record", (description, measurement) => { - (window).__karma__.log("kelonio", [JSON.stringify({ description, durations: measurement.durations, totalDuration: measurement.totalDuration })]); - }); - } - - constructor(baseReporterDecorator: any, config: { kelonioReporter?: KarmaReporterOptions }, logger: unknown, helper: unknown, formatError: unknown) { - baseReporterDecorator(this); - const activeConfig = { ...{ inferBrowsers: true, printReportAtEnd: true }, ...config.kelonioReporter }; - const b = new Benchmark(); - - this.onBrowserLog = (browser: string, log: string, type: string) => { - if (type === "kelonio") { - const parsed: KarmaLoggedRecord = JSON.parse(log.slice(1, -1)); - const browserDescription = activeConfig.inferBrowsers ? [browser] : []; - b.incorporate([...browserDescription, ...parsed.description], new Measurement(parsed.durations, parsed.totalDuration)); - } - }; - - this.onRunComplete = () => { - if (activeConfig.printReportAtEnd) { - (this).write(`${b.report()}\n`); - handleExtraReports(activeConfig.extensions, b, msg => (this).write(`${msg}\n`)); - } - }; - } -} - -export class MochaReporter { - constructor(runner: Mocha.Runner, options: MochaReporterOptions) { - const b = new Benchmark(); - let baseDescription: Array = []; - const inferDescriptions = options.reporterOptions.inferDescriptions ?? true; - const printReportAtEnd = options.reporterOptions.printReportAtEnd ?? true; - const extensions = options.reporterOptions.extensions ?? []; - - benchmark.events.on("record", (description, measurement) => { - b.incorporate(baseDescription.concat(description), measurement); - }); - if (inferDescriptions) { - runner.on(MOCHA_EVENT_TEST_BEGIN, test => { - baseDescription = test.titlePath(); - }); - } - runner.once(MOCHA_EVENT_RUN_END, () => { - if (printReportAtEnd) { - console.log(`\n${b.report()}`); - handleExtraReports(extensions, b, console.log); - } - }); - } -} diff --git a/src/reporters/jest.ts b/src/reporters/jest.ts new file mode 100644 index 0000000..c98fe5e --- /dev/null +++ b/src/reporters/jest.ts @@ -0,0 +1,111 @@ +import * as fsModule from "fs"; +import { Benchmark, benchmark } from '../index' +import { ExtensionLookup, handleExtraReports } from "./utils"; + +export const STATE_FILE = ".kelonio.state.json"; + +let fs: typeof fsModule; +let canUseFs = true; +try { + fs = require("fs"); +} catch { + canUseFs = false; +} + +export class BenchmarkFileState { + constructor() { + if (!canUseFs) { + throw new Error("Unable to access file system"); + } + } + + exists(): boolean { + return fs.existsSync(STATE_FILE); + } + + read(): any { + return JSON.parse(fs.readFileSync(STATE_FILE, "utf-8")); + } + + write(data: object): void { + fs.writeFileSync(STATE_FILE, JSON.stringify(data), "utf-8"); + } + + append(data: object): void { + let previousData; + try { + previousData = this.read(); + } catch { + previousData = {}; + } + this.write({ ...previousData, ...data }); + } + + delete(): void { + try { + fs.unlinkSync(STATE_FILE); + } catch {} + } +} + +interface JestReporterOptions { + keepStateAtStart?: boolean; + keepStateAtEnd?: boolean; + printReportAtEnd?: boolean; + extensions?: Array; +} + +export class JestReporter implements jest.Reporter { + options: JestReporterOptions = { + keepStateAtStart: false, + keepStateAtEnd: false, + printReportAtEnd: true, + }; + constructor(testData?: any, options?: JestReporterOptions) { + if (options) { + this.options = { ...this.options, ...options }; + } + } + static initializeKelonio(): void { + const state = new BenchmarkFileState(); + benchmark.events.on("record", (description, measurement) => { + const b = new Benchmark(); + if (state.exists()) { + b.data = state.read(); + } + b.incorporate(description, measurement); + state.write(b.data); + }); + } + + onRunStart(): void { + const state = new BenchmarkFileState(); + if (this.options.keepStateAtStart) { + state.append({}); + } else { + state.write({}); + } + } + + onRunComplete(): void { + const state = new BenchmarkFileState(); + if (!state.exists()) { + throw new Error( + "The Kelonio reporter for Jest requires benchmark serialization." + + " Make sure to call `JestReporter.initializeKelonio()`." + ); + } + + const b = new Benchmark(); + b.data = state.read(); + + if (this.options.printReportAtEnd) { + console.log(`\n${b.report()}`); + handleExtraReports(this.options.extensions, b, console.log); + } + + if (!this.options.keepStateAtEnd) { + state.delete(); + } + } +} diff --git a/src/reporters/karma.ts b/src/reporters/karma.ts new file mode 100644 index 0000000..105ecdd --- /dev/null +++ b/src/reporters/karma.ts @@ -0,0 +1,68 @@ +import { benchmark, Benchmark, Measurement } from ".."; +import { ExtensionLookup, handleExtraReports } from "./utils"; + +type KarmaLoggedRecord = { description: Array, durations: Array, totalDuration: number }; + +interface KarmaReporterOptions { + inferBrowsers?: boolean; + printReportAtEnd?: boolean; + extensions?: Array; +} + +export class KarmaReporter { + protected onBrowserLog: ( + browser: string, + log: string, + type: string + ) => void; + protected onRunComplete: () => void; + + static initializeKelonio(): void { + benchmark.events.on("record", (description, measurement) => { + (window).__karma__.log("kelonio", [ + JSON.stringify({ + description, + durations: measurement.durations, + totalDuration: measurement.totalDuration, + }), + ]); + }); + } + + constructor( + baseReporterDecorator: any, + config: { kelonioReporter?: KarmaReporterOptions }, + logger: unknown, + helper: unknown, + formatError: unknown + ) { + baseReporterDecorator(this); + const activeConfig = { + ...{ inferBrowsers: true, printReportAtEnd: true }, + ...config.kelonioReporter, + }; + const b = new Benchmark(); + + this.onBrowserLog = (browser: string, log: string, type: string) => { + if (type === "kelonio") { + const parsed: KarmaLoggedRecord = JSON.parse(log.slice(1, -1)); + const browserDescription = activeConfig.inferBrowsers + ? [browser] + : []; + b.incorporate( + [...browserDescription, ...parsed.description], + new Measurement(parsed.durations, parsed.totalDuration) + ); + } + }; + + this.onRunComplete = () => { + if (activeConfig.printReportAtEnd) { + (this).write(`${b.report()}\n`); + handleExtraReports(activeConfig.extensions, b, (msg) => + (this).write(`${msg}\n`) + ); + } + }; + } +} diff --git a/src/reporters/mocha.ts b/src/reporters/mocha.ts new file mode 100644 index 0000000..560b1d4 --- /dev/null +++ b/src/reporters/mocha.ts @@ -0,0 +1,40 @@ +import { Benchmark, benchmark } from ".."; +import { ExtensionLookup, handleExtraReports } from "./utils"; + +const MOCHA_EVENT_TEST_BEGIN = "test"; +const MOCHA_EVENT_RUN_END = "end"; + +interface MochaReporterOptions { + reporterOptions: { + inferDescriptions?: boolean; + printReportAtEnd?: boolean; + extensions?: Array; + }; +} + +export class MochaReporter { + constructor(runner: Mocha.Runner, options: MochaReporterOptions) { + const b = new Benchmark(); + let baseDescription: Array = []; + const inferDescriptions = + options.reporterOptions.inferDescriptions ?? true; + const printReportAtEnd = + options.reporterOptions.printReportAtEnd ?? true; + const extensions = options.reporterOptions.extensions ?? []; + + benchmark.events.on("record", (description, measurement) => { + b.incorporate(baseDescription.concat(description), measurement); + }); + if (inferDescriptions) { + runner.on(MOCHA_EVENT_TEST_BEGIN, (test) => { + baseDescription = test.titlePath(); + }); + } + runner.once(MOCHA_EVENT_RUN_END, () => { + if (printReportAtEnd) { + console.log(`\n${b.report()}`); + handleExtraReports(extensions, b, console.log); + } + }); + } +} diff --git a/src/reporters/utils.ts b/src/reporters/utils.ts new file mode 100644 index 0000000..2e31d80 --- /dev/null +++ b/src/reporters/utils.ts @@ -0,0 +1,26 @@ +import { Benchmark } from ".."; + +const HEADER_SIDE = "- ".repeat(17).trim(); +export const HEADER = `${HEADER_SIDE} Performance ${HEADER_SIDE}`; +export const FOOTER = "- ".repeat(40).trim(); + +export type Extension = { + extraReport?: (benchmark: Benchmark) => string | void; +}; +export type ExtensionLookup = { module: string; extension: string }; + +export function handleExtraReports( + lookups: Array | undefined, + benchmark: Benchmark, + print: (report: string) => void +): void { + for (const lookup of lookups ?? []) { + const extension: Extension | undefined = require(lookup.module)?.[ + lookup.extension + ]; + const report = extension?.extraReport?.(benchmark); + if (report) { + print(report); + } + } +} diff --git a/tests/index.test.ts b/tests/index.test.ts index 655fb39..433f36f 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,6 +1,6 @@ import stripIndent from "strip-indent"; import { Benchmark, Criteria, measure, Measurement, PerformanceError } from "../src"; -import { FOOTER, HEADER } from "../src/etc"; +import { FOOTER, HEADER } from "../src/reporters/utils"; // Using `await util.promisify(setTimeout)(500)` leads to this error in some tests: // "Async callback was not invoked within the 5000ms timeout" diff --git a/tests/reporters.test.ts b/tests/reporters.test.ts index a6cb859..443bcdf 100644 --- a/tests/reporters.test.ts +++ b/tests/reporters.test.ts @@ -1,8 +1,11 @@ import { EventEmitter } from "events"; import fs from "fs"; import stripIndent from "strip-indent"; -import { benchmark, JestReporter, KarmaReporter, Measurement, MochaReporter } from "../src"; -import { FOOTER, HEADER, STATE_FILE } from "../src/etc"; +import { benchmark, Measurement } from "../src"; +import { JestReporter, STATE_FILE } from "../src/reporters/jest"; +import { KarmaReporter } from "../src/reporters/karma"; +import { MochaReporter } from "../src/reporters/mocha"; +import { FOOTER, HEADER } from "../src/reporters/utils"; function makeMochaReporter(reporterOptions: any = {}): [EventEmitter, MochaReporter] { const runner = new EventEmitter();