From 56ae4c1aceaa5d3dc2676d209e27f5df81e1bf26 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 12 Dec 2024 17:03:30 +0100 Subject: [PATCH 1/8] feat: add support for embedded runner (#6) * Remove peer dependency and add runner * Add new runner implementation * Move cli to a directory * Update the cli to support file spec options * Add support for custom options * Fix unit tests * Fix the build for node 18 * Fix type issue with resolution * Skip lib check for the old version of a package * Add comment * Downgrade rimraf to support node 18 * Add ts as default extension list to not break compatibility * Update reame file * Update all mocha references * Remove extra sanitization option * Update color keys * Organize the exports * Remove it from the exports * Update the benchmark to reflect the changes in export --- README.md | 12 +- bin/index.cjs | 2 +- package.json | 14 +- scripts/writeOptionsMd.ts | 2 +- src/benchmark/benchmarkFn.ts | 127 +++++++++ src/{mochaPlugin => benchmark}/format.ts | 0 src/benchmark/globalState.ts | 44 ++++ src/benchmark/index.ts | 1 + src/benchmark/reporter.ts | 107 ++++++++ .../runBenchmarkFn.ts} | 0 src/benchmark/runner.ts | 81 ++++++ src/{ => cli}/cli.ts | 30 +-- src/{ => cli}/options.ts | 93 +++++-- src/cli/run.ts | 81 ++++++ src/compare/compute.ts | 6 +- src/compare/index.ts | 6 +- src/github/comment.ts | 4 +- src/history/append.ts | 4 +- src/history/index.ts | 6 +- src/history/location.ts | 6 +- src/history/shouldPersist.ts | 4 +- src/index.ts | 10 +- src/mochaPlugin/globalState.ts | 17 -- src/mochaPlugin/index.ts | 182 ------------- src/mochaPlugin/mochaRunner.ts | 50 ---- src/mochaPlugin/reporter.ts | 102 -------- src/mochaPlugin/utils.ts | 31 --- src/run.ts | 68 ----- src/types.ts | 71 ++--- src/utils/defaultBranch.ts | 4 +- src/utils/file.ts | 37 +++ src/utils/mochaCliExports.ts | 44 ---- src/utils/output.ts | 42 +++ src/utils/render.ts | 8 +- test/perf/iteration.test.ts | 25 +- test/unit/history/local.test.ts | 4 +- tsconfig.build.cjs.json | 3 + tsconfig.build.esm.json | 3 + yarn.lock | 247 +++++++++++++++--- 39 files changed, 902 insertions(+), 676 deletions(-) create mode 100644 src/benchmark/benchmarkFn.ts rename src/{mochaPlugin => benchmark}/format.ts (100%) create mode 100644 src/benchmark/globalState.ts create mode 100644 src/benchmark/index.ts create mode 100644 src/benchmark/reporter.ts rename src/{mochaPlugin/runBenchFn.ts => benchmark/runBenchmarkFn.ts} (100%) create mode 100644 src/benchmark/runner.ts rename src/{ => cli}/cli.ts (60%) rename src/{ => cli}/options.ts (68%) create mode 100644 src/cli/run.ts delete mode 100644 src/mochaPlugin/globalState.ts delete mode 100644 src/mochaPlugin/index.ts delete mode 100644 src/mochaPlugin/mochaRunner.ts delete mode 100644 src/mochaPlugin/reporter.ts delete mode 100644 src/mochaPlugin/utils.ts delete mode 100644 src/run.ts delete mode 100644 src/utils/mochaCliExports.ts create mode 100644 src/utils/output.ts diff --git a/README.md b/README.md index 943b42d..bb38113 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ This tooling provides both a easy to use runner for benchmarking and easy integr ## Quick start -Create a test mocha test file but use `itBench` instead of `it` +Create a benchmark test file but use `itBench` instead of `it` ```ts -import {itBench, setBenchOpts} from "../../src"; +import {itBench, setBenchOpts, describe} from "../../src"; describe("Sum array benchmark", () => { itBench("sum array with reduce", () => { @@ -21,7 +21,7 @@ describe("Sum array benchmark", () => { }); ``` -Then run the CLI, compatible with all mocha options. +Then run the CLI. ``` benchmark 'test/perf/**/*.perf.ts' --local @@ -36,7 +36,7 @@ Inspect benchmark results in the terminal ## How does it work? -This tool is a CLI wrapper around mocha, example usage: +This tool is a CLI tool, example usage: ``` benchmark 'test/perf/**/*.perf.ts' --s3 @@ -47,9 +47,7 @@ The above command will: - Read benchmark history from the specified provider (AWS S3) - Figure out the prev benchmark based on your option (defaults to latest commit in main branch) - Run benchmark comparing with previous - - Runs mocha programatically against the file globs - - Collect benchmark data in-memory while streaming results with a familiar mocha reporter - - Note: also runs any test that would regularly be run with mocha + - Collect benchmark data in-memory while streaming results - Add result to benchmark history and persist them to the specified provider (AWS S3) - If in CI, post a PR or commit comment with an expandable summary of the benchmark results comparision - If a performance regression was detected, exit 1 diff --git a/bin/index.cjs b/bin/index.cjs index 3f43cca..8b259be 100755 --- a/bin/index.cjs +++ b/bin/index.cjs @@ -1,3 +1,3 @@ #!/usr/bin/env node -require('../lib/cjs/cli.js'); +require("../lib/cjs/cli/cli.js"); diff --git a/package.json b/package.json index dc2f986..e4849c3 100644 --- a/package.json +++ b/package.json @@ -33,14 +33,13 @@ "test:unit": "mocha test/unit/**/*.test.ts", "lint": "eslint --color src/ test/", "prepublishOnly": "yarn build", - "benchmark": "node --loader ts-node/esm ./src/cli.ts 'test/perf/**/*.test.ts'", + "benchmark": "node --loader ts-node/esm ./src/cli/cli.ts 'test/perf/**/*.test.ts'", "writeDocs": "node --loader ts-node/esm scripts/writeOptionsMd.ts" }, "devDependencies": { "@types/chai": "^4.2.19", "@types/mocha": "^10.0.9", "@types/node": "^18.15.3", - "@types/rimraf": "^3.0.0", "@types/yargs": "^17.0.33", "chai": "^4.5.0", "dotenv": "^10.0.0", @@ -52,7 +51,7 @@ "eslint-config-prettier": "^9.1.0", "mocha": "^10.8.2", "prettier": "^3.4.0", - "rimraf": "^3.0.2", + "rimraf": "^5.0.10", "ts-node": "^10.9.2", "typescript": "^5.6.3", "typescript-eslint": "^8.16.0" @@ -60,13 +59,16 @@ "dependencies": { "@actions/cache": "^1.0.7", "@actions/github": "^5.0.0", + "@vitest/runner": "^2.1.6", "ajv": "^8.6.0", "aws-sdk": "^2.932.0", "csv-parse": "^4.16.0", "csv-stringify": "^5.6.2", - "yargs": "^17.7.2" + "glob": "^10.4.5", + "yargs": "^17.7.2", + "log-symbols": "^7.0.0" }, - "peerDependencies": { - "mocha": ">10.0.0" + "resolutions": { + "lru-cache": "10.4.3" } } diff --git a/scripts/writeOptionsMd.ts b/scripts/writeOptionsMd.ts index 48f80ee..3ec3d73 100644 --- a/scripts/writeOptionsMd.ts +++ b/scripts/writeOptionsMd.ts @@ -1,4 +1,4 @@ -import {options} from "../src/options.js"; +import {options} from "../src/cli/options.js"; const sections: string[] = []; diff --git a/src/benchmark/benchmarkFn.ts b/src/benchmark/benchmarkFn.ts new file mode 100644 index 0000000..f6b5f53 --- /dev/null +++ b/src/benchmark/benchmarkFn.ts @@ -0,0 +1,127 @@ +import fs from "node:fs"; +import path from "node:path"; +import {getCurrentSuite} from "@vitest/runner"; +import {createChainable} from "@vitest/runner/utils"; +import {store} from "./globalState.js"; +import {BenchApi, BenchmarkOpts, BenchmarkRunOptsWithFn, PartialBy} from "../types.js"; +import {runBenchFn} from "./runBenchmarkFn.js"; +import {optionsDefault} from "../cli/options.js"; + +export const bench: BenchApi = createBenchmarkFunction(function ( + this: Record<"skip" | "only", boolean | undefined>, + idOrOpts: string | PartialBy, "fn">, + fn?: (arg: T) => void | Promise +) { + const {fn: benchTask, ...opts} = coerceToOptsObj(idOrOpts, fn); + const currentSuite = getCurrentSuite(); + + const globalOptions = store.getGlobalOptions() ?? {}; + const parentOptions = store.getOptions(getCurrentSuite()) ?? {}; + const options = {...globalOptions, ...parentOptions, ...opts}; + const {timeoutBench, maxMs, minMs} = options; + + let timeout = timeoutBench ?? optionsDefault.timeoutBench; + if (maxMs && maxMs > timeout) { + timeout = maxMs * 1.5; + } + + if (minMs && minMs > timeout) { + timeout = minMs * 1.5; + } + + const task = currentSuite.task(opts.id, { + skip: opts.skip ?? this.skip, + only: opts.only ?? this.only, + sequential: true, + concurrent: false, + timeout, + meta: { + "chainsafe/benchmark": true, + }, + async handler() { + // Ensure bench id is unique + if (store.getResult(opts.id) && !opts.skip) { + throw Error(`test titles must be unique, duplicated: '${opts.id}'`); + } + + // Persist full results if requested. dir is created in `beforeAll` + const benchmarkResultsCsvDir = process.env.BENCHMARK_RESULTS_CSV_DIR; + const persistRunsNs = Boolean(benchmarkResultsCsvDir); + + const {result, runsNs} = await runBenchFn({...options, fn: benchTask}, persistRunsNs); + + // Store result for: + // - to persist benchmark data latter + // - to render with the custom reporter + store.setResult(opts.id, result); + + if (benchmarkResultsCsvDir) { + fs.mkdirSync(benchmarkResultsCsvDir, {recursive: true}); + const filename = `${result.id}.csv`; + const filepath = path.join(benchmarkResultsCsvDir, filename); + fs.writeFileSync(filepath, runsNs.join("\n")); + } + }, + }); + + store.setOptions(task, opts); +}); + +function createBenchmarkFunction( + fn: ( + this: Record<"skip" | "only", boolean | undefined>, + idOrOpts: string | PartialBy, "fn">, + fn?: (arg: T) => void | Promise + ) => void +): BenchApi { + return createChainable(["skip", "only"], fn) as BenchApi; +} + +function coerceToOptsObj( + idOrOpts: string | PartialBy, "fn">, + fn?: (arg: T) => void | Promise +): BenchmarkRunOptsWithFn { + let opts: BenchmarkRunOptsWithFn; + + if (typeof idOrOpts === "string") { + if (!fn) throw Error("fn arg must be set"); + opts = {id: idOrOpts, fn, threshold: optionsDefault.threshold}; + } else { + if (fn) { + opts = {...idOrOpts, fn}; + } else { + const optsWithFn = idOrOpts as BenchmarkRunOptsWithFn; + if (!optsWithFn.fn) throw Error("opts.fn arg must be set"); + opts = optsWithFn; + } + } + + return opts; +} + +/** + * Customize benchmark opts for a describe block + * ```ts + * describe("suite A1", function () { + * setBenchOpts({runs: 100}); + * // 100 runs + * itBench("bench A1.1", function() {}); + * itBench("bench A1.2", function() {}); + * // 300 runs + * itBench({id: "bench A1.3", runs: 300}, function() {}); + * + * // Supports nesting, child has priority over parent. + * // Arrow functions can be used, won't break it. + * describe("suite A2", () => { + * setBenchOpts({runs: 200}); + * // 200 runs. + * itBench("bench A2.1", () => {}); + * }) + * }) + * ``` + */ +export function setBenchOpts(opts: BenchmarkOpts): void { + store.setOptions(getCurrentSuite(), opts); +} + +export const setBenchmarkOptions = setBenchOpts; diff --git a/src/mochaPlugin/format.ts b/src/benchmark/format.ts similarity index 100% rename from src/mochaPlugin/format.ts rename to src/benchmark/format.ts diff --git a/src/benchmark/globalState.ts b/src/benchmark/globalState.ts new file mode 100644 index 0000000..1443af9 --- /dev/null +++ b/src/benchmark/globalState.ts @@ -0,0 +1,44 @@ +import type {Suite, SuiteCollector, Task} from "@vitest/runner"; +import {BenchmarkResult, BenchmarkOpts, BenchmarkResults} from "../types.js"; + +/**t + * Map of results by root suite. + */ +const results = new Map(); + +/** + * Global opts from CLI + */ +let globalOpts: BenchmarkOpts | undefined; + +/** + * Map to persist options set in describe blocks + */ +const optsMap = new WeakMap(); + +export const store = { + getResult(id: string): BenchmarkResult | undefined { + return results.get(id); + }, + setResult(id: string, result: BenchmarkResult): void { + results.set(id, result); + }, + getAllResults(): BenchmarkResults { + return [...results.values()]; + }, + getOptions(suite: Task | Suite | SuiteCollector): BenchmarkOpts | undefined { + return optsMap.get(suite); + }, + setOptions(suite: Task | Suite | SuiteCollector, opts: BenchmarkOpts): void { + optsMap.set(suite, opts); + }, + removeOptions(suite: Task | Suite): void { + optsMap.delete(suite); + }, + setGlobalOptions(opts: Partial): void { + globalOpts = opts; + }, + getGlobalOptions(): BenchmarkOpts | undefined { + return globalOpts; + }, +}; diff --git a/src/benchmark/index.ts b/src/benchmark/index.ts new file mode 100644 index 0000000..99c72e3 --- /dev/null +++ b/src/benchmark/index.ts @@ -0,0 +1 @@ +export * from "./benchmarkFn.js"; diff --git a/src/benchmark/reporter.ts b/src/benchmark/reporter.ts new file mode 100644 index 0000000..8388663 --- /dev/null +++ b/src/benchmark/reporter.ts @@ -0,0 +1,107 @@ +import {Task, Suite, File} from "@vitest/runner"; +import {color, consoleLog, symbols} from "../utils/output.js"; +import {store} from "./globalState.js"; +import {Benchmark, BenchmarkOpts, BenchmarkResult} from "../types.js"; +import {formatResultRow} from "./format.js"; +import {optionsDefault} from "../cli/options.js"; + +export class BenchmarkReporter { + indents = 0; + failed = 0; + passed = 0; + skipped = 0; + + readonly prevResults: Map; + readonly threshold: number; + + constructor({prevBench, benchmarkOpts}: {prevBench: Benchmark | null; benchmarkOpts: BenchmarkOpts}) { + this.prevResults = new Map(); + this.threshold = benchmarkOpts.threshold ?? optionsDefault.threshold; + + if (prevBench) { + for (const bench of prevBench.results) { + this.prevResults.set(bench.id, bench); + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onTestStarted(_task: Task): void { + // this.log(task.name, "started"); + } + + onTestFinished(task: Task): void { + const {result} = task; + + if (!result) { + consoleLog(`${this.indent()}${color("pending", " - %s")}`, `${task.name} - can not find result`); + return; + } + + switch (result.state) { + case "skip": { + this.skipped++; + consoleLog(`${this.indent()}${color("pending", " - %s")}`, task.name); + break; + } + case "fail": { + this.failed++; + consoleLog(this.indent() + color("fail", " %d) %s"), ++this.failed, task.name); + consoleLog(task.result?.errors); + break; + } + case "pass": { + try { + const result = store.getResult(task.name); + + if (result) { + // Render benchmark + const prevResult = this.prevResults.get(result.id) ?? null; + + const resultRow = formatResultRow(result, prevResult, result.threshold ?? this.threshold); + const fmt = this.indent() + color("checkmark", " " + symbols.ok) + " " + resultRow; + consoleLog(fmt); + } else { + // Render regular test + const fmt = this.indent() + color("checkmark", " " + symbols.ok) + color("pass", " %s"); + consoleLog(fmt, task.name); + } + this.passed++; + } catch (e) { + this.failed++; + consoleLog(e); + process.exitCode = 1; + throw e; + } + } + } + } + + onSuiteStarted(suite: Suite): void { + this.indents++; + consoleLog(color("suite", "%s%s"), this.indent(), suite.name); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onSuiteFinished(_suite: Suite): void { + --this.indents; + + if (this.indents === 1) { + consoleLog(); + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onComplete(_files: File[]): void { + consoleLog(); + this.indents += 2; + consoleLog(color("checkmark", "%s%s"), this.indent(), `${this.passed} passing`); + consoleLog(color("fail", "%s%s"), this.indent(), `${this.failed} failed`); + consoleLog(color("pending", "%s%s"), this.indent(), `${this.skipped} pending`); + consoleLog(); + } + + protected indent(): string { + return Array(this.indents).join(" "); + } +} diff --git a/src/mochaPlugin/runBenchFn.ts b/src/benchmark/runBenchmarkFn.ts similarity index 100% rename from src/mochaPlugin/runBenchFn.ts rename to src/benchmark/runBenchmarkFn.ts diff --git a/src/benchmark/runner.ts b/src/benchmark/runner.ts new file mode 100644 index 0000000..1897226 --- /dev/null +++ b/src/benchmark/runner.ts @@ -0,0 +1,81 @@ +import { + File, + startTests, + Suite, + Task, + VitestRunner, + VitestRunnerConfig, + VitestRunnerImportSource, +} from "@vitest/runner"; +import {Benchmark, BenchmarkOpts, BenchmarkResults} from "../types.js"; +import {BenchmarkReporter} from "./reporter.js"; +import {store} from "./globalState.js"; + +export class BenchmarkRunner implements VitestRunner { + readonly config: VitestRunnerConfig; + readonly reporter: BenchmarkReporter; + readonly prevBench: Benchmark | null; + readonly benchmarkOpts: BenchmarkOpts; + + constructor({prevBench, benchmarkOpts}: {prevBench: Benchmark | null; benchmarkOpts: BenchmarkOpts}) { + this.config = { + root: "", + sequence: {seed: 1234, hooks: "list", setupFiles: "list"}, + passWithNoTests: false, + maxConcurrency: 1, + hookTimeout: 10_0000, + testTimeout: 10_0000, + setupFiles: [], + retry: 0, + }; + this.prevBench = prevBench; + this.benchmarkOpts = benchmarkOpts; + this.reporter = new BenchmarkReporter({prevBench, benchmarkOpts}); + } + + onBeforeRunSuite(suite: Suite): void { + this.reporter.onSuiteStarted(suite); + } + + onAfterRunSuite(suite: Suite): void { + this.reporter.onSuiteFinished(suite); + } + + onBeforeRunTask(task: Task): void { + this.reporter.onTestStarted(task); + } + + onAfterRunTask(task: Task): void { + this.reporter.onTestFinished(task); + } + + onAfterRunFiles(files: File[]): void { + this.reporter.onComplete(files); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async importFile(filepath: string, _source: VitestRunnerImportSource): Promise { + // TODO: Implement file caching mechanism later + await import(filepath); + } + + async process(files: string[]): Promise { + store.setGlobalOptions(this.benchmarkOpts); + + const res = await startTests(files, this); + + const passed = res.filter((r) => r.result?.state == "pass"); + const skipped = res.filter((r) => r.result?.state == "skip"); + const failed = res.filter((r) => r.result?.state == "fail"); + + if (failed.length > 0) { + throw failed[0].result?.errors; + } + + if (passed.length + skipped.length === res.length) { + return store.getAllResults(); + } + + throw new Error("Some tests cause returned with unknown state"); + } +} diff --git a/src/cli.ts b/src/cli/cli.ts similarity index 60% rename from src/cli.ts rename to src/cli/cli.ts index c02649d..87e865b 100644 --- a/src/cli.ts +++ b/src/cli/cli.ts @@ -1,32 +1,20 @@ // Must not use `* as yargs`, see https://github.com/yargs/yargs/issues/1131 import yargs from "yargs"; -import {loadOptions, handleRequires} from "./utils/mochaCliExports.js"; -import {options, optionsDefault} from "./options.js"; -import {run} from "./run.js"; -import {Opts} from "./types.js"; - -/** - * Common factory for running the CLI and running integration tests - * The CLI must actually be executed in a different script - */ -const argv = process.argv.slice(2); +import {hideBin} from "yargs/helpers"; -const args = loadOptions(argv); +import {benchmarkOptions, CLIOptions, fileCollectionOptions, storageOptions} from "./options.js"; +import {run} from "./run.js"; -void yargs() +void yargs(hideBin(process.argv)) .env("BENCHMARK") .scriptName("benchmark") .command({ command: ["$0 [spec..]", "inspect"], describe: "Run benchmarks", handler: async (argv) => { - // Copied from mocha source to load ts-node properly. - // It's on the CLI middleware of mocha so it does not get run when calling mocha programatically - // https://github.com/mochajs/mocha/blob/014e47a8b07809e73b1598c7abeafe7a3b57a8f7/lib/cli/run.js#L353 - const plugins = await handleRequires(argv.require as string[]); - Object.assign(argv, plugins); + const cliOpts = {...argv} as unknown as CLIOptions & {spec: string[]}; - await run({threshold: optionsDefault.threshold, ...argv} as Opts); + await run(cliOpts); }, }) @@ -34,12 +22,11 @@ void yargs() // As of yargs v16.1.0 dot-notation breaks strictOptions() // Manually processing options is typesafe tho more verbose "dot-notation": false, - // From mocha "combine-arrays": true, "short-option-groups": false, "strip-aliased": true, }) - .options(options) + .options({...fileCollectionOptions, ...storageOptions, ...benchmarkOptions}) .usage( `Benchmark runner to track performance. @@ -67,5 +54,4 @@ void yargs() console.error(` ✖ ${errorMessage}\n`); process.exit(1); }) - .config(args) - .parse(args._); + .parse(); diff --git a/src/options.ts b/src/cli/options.ts similarity index 68% rename from src/options.ts rename to src/cli/options.ts index 8569a6b..f51c27e 100644 --- a/src/options.ts +++ b/src/cli/options.ts @@ -1,64 +1,95 @@ import {Options} from "yargs"; -import {Opts, BenchmarkOpts} from "./types.js"; -import {FileCollectionOptions} from "./utils/mochaCliExports.js"; +import {StorageOptions, BenchmarkOpts, FileCollectionOptions} from "../types.js"; export const optionsDefault = { threshold: 2, + timeoutBench: 10_000, historyLocalPath: "./benchmark_data", historyCacheKey: "benchmark_data", }; +type CLIFileCollectionOptions = Omit; +type CLIStorageOptions = StorageOptions; +type CLIBenchmarkOptions = Omit; type ICliCommandOptions = Required<{[key in keyof OwnArgs]: Options}>; -type CliOpts = Omit & - Omit; -export const options: ICliCommandOptions = { +export type CLIOptions = CLIFileCollectionOptions & CLIStorageOptions & CLIBenchmarkOptions; + +const fileGroup = "Files options:"; +const storageGroup = "Storage options:"; +const benchmarkGroup = "Benchmark options:"; + +export const fileCollectionOptions: ICliCommandOptions = { + extension: { + description: "File extension(s) to load", + type: "array", + alias: "ext", + default: ["js", "cjs", "mjs", "ts"], + group: fileGroup, + }, + ignore: { + description: "Ignore file(s) or glob pattern(s)", + type: "array", + alias: "exclude", + group: fileGroup, + }, + recursive: { + description: "Look for tests in subdirectories", + type: "boolean", + default: false, + group: fileGroup, + }, +}; + +export const storageOptions: ICliCommandOptions = { defaultBranch: { description: "Provide the default branch of this repository to prevent fetching from Github", type: "string", - group: "Options:", + group: storageGroup, }, persistBranches: { description: "Choose what branches to persist benchmark data", type: "array", defaultDescription: "default-branch", + group: storageGroup, }, benchmarksPerBranch: { description: "Limit number of benchmarks persisted per branch", type: "number", defaultDescription: "Infinity", - }, - threshold: { - description: - "Ratio of new average time per run vs previos time per run to consider a failure. Set to 'Infinity' to disable it.", - type: "number", - default: optionsDefault.threshold, + group: storageGroup, }, compareBranch: { description: "Compare new benchmark data against the latest available benchmark in this branch", type: "string", defaultDescription: "default-branch", + group: storageGroup, }, compareCommit: { description: "Compare new benchmark data against the benchmark data associated with a specific commit", type: "string", + group: storageGroup, }, prune: { description: "When persisting history, delete benchmark data associated with commits that are no longer in the current git history", type: "boolean", + group: storageGroup, }, persist: { description: "Force persisting benchmark data in history", type: "boolean", + group: storageGroup, }, noThrow: { description: "Exit cleanly even if a preformance regression was found", type: "boolean", + group: storageGroup, }, skipPostComment: { description: "Skip post Github comment step if run on Github CI", type: "boolean", + group: storageGroup, }, historyLocal: { alias: ["local"], @@ -66,6 +97,7 @@ export const options: ICliCommandOptions = { "Persist benchmark history locally. May specify just a boolean to use a default path, or provide a path", type: "string", defaultDescription: optionsDefault.historyLocalPath, + group: storageGroup, }, historyGaCache: { alias: ["ga-cache"], @@ -73,67 +105,78 @@ export const options: ICliCommandOptions = { "Persist benchmark history in Github Actions cache. Requires Github authentication. May specify just a boolean to use a default cache key or provide a custom key", type: "string", defaultDescription: optionsDefault.historyCacheKey, + group: storageGroup, }, historyS3: { alias: ["s3"], description: "Persist benchmark history in an Amazon S3 bucket. Requires Github authentication", type: "string", + group: storageGroup, }, +}; - // BenchmarkOpts - +// BenchmarkOpts +export const benchmarkOptions: ICliCommandOptions = { + threshold: { + description: + "Ratio of new average time per run vs previos time per run to consider a failure. Set to 'Infinity' to disable it.", + type: "number", + default: optionsDefault.threshold, + group: benchmarkGroup, + }, maxRuns: { type: "number", description: "Max number of fn() runs, after which the benchmark stops", - group: "itBench() options", + group: benchmarkGroup, }, minRuns: { type: "number", description: "Min number of fn() runs before considering stopping the benchmark after converging", - group: "itBench() options", + group: benchmarkGroup, }, maxMs: { type: "number", description: "Max total miliseconds of runs, after which the benchmark stops", - group: "itBench() options", + group: benchmarkGroup, }, minMs: { type: "number", description: "Min total miiliseconds of runs before considering stopping the benchmark after converging", - group: "itBench() options", + group: benchmarkGroup, }, maxWarmUpMs: { type: "number", description: "Maximum real benchmark function run time before starting to count towards results. Set to 0 to not warm-up. May warm up for less ms if the `maxWarmUpRuns` condition is met first.", - group: "itBench() options", + group: benchmarkGroup, }, maxWarmUpRuns: { type: "number", description: "Maximum benchmark function runs before starting to count towards results. Set to 0 to not warm-up. May warm up for less ms if the `maxWarmUpMs` condition is met first.", - group: "itBench() options", + group: benchmarkGroup, }, convergeFactor: { type: "number", description: "Convergance factor (0,1) at which the benchmark automatically stops. Set to 1 to disable", - group: "itBench() options", + group: benchmarkGroup, }, runsFactor: { type: "number", description: "If fn() contains a foor loop repeating a task N times, you may set runsFactor = N to scale down the results.", - group: "itBench() options", + group: benchmarkGroup, }, yieldEventLoopAfterEach: { type: "boolean", description: "Run `sleep(0)` after each fn() call. Use when the event loop needs to tick to free resources created by fn()", - group: "itBench() options", + group: benchmarkGroup, }, timeoutBench: { type: "number", - description: "Hard timeout, enforced by mocha.", - group: "itBench() options", + description: "Hard timeout for each benchmark", + default: optionsDefault.timeoutBench, + group: benchmarkGroup, }, }; diff --git a/src/cli/run.ts b/src/cli/run.ts new file mode 100644 index 0000000..7a9ef35 --- /dev/null +++ b/src/cli/run.ts @@ -0,0 +1,81 @@ +import * as github from "@actions/github"; +import {getHistoryProvider} from "../history/index.js"; +import {resolveShouldPersist} from "../history/shouldPersist.js"; +import {validateBenchmark} from "../history/schema.js"; +import {Benchmark, BenchmarkOpts, FileCollectionOptions, StorageOptions} from "../types.js"; +import {renderCompareWith, resolveCompareWith, resolvePrevBenchmark} from "../compare/index.js"; +import {parseBranchFromRef, getCurrentCommitInfo, shell, getCurrentBranch, collectFiles} from "../utils/index.js"; +import {computeBenchComparision} from "../compare/compute.js"; +import {postGaComment} from "../github/comment.js"; +import {isGaRun} from "../github/context.js"; +import {BenchmarkRunner} from "../benchmark/runner.js"; +import {optionsDefault} from "./options.js"; +import {consoleLog} from "../utils/output.js"; + +export async function run(opts_: FileCollectionOptions & StorageOptions & BenchmarkOpts): Promise { + const opts = Object.assign({}, optionsDefault, opts_); + + // Retrieve history + const historyProvider = getHistoryProvider(opts); + consoleLog(`Connected to historyProvider: ${historyProvider.providerInfo()}`); + + // Select prev benchmark to compare against + const compareWith = await resolveCompareWith(opts); + const prevBench = await resolvePrevBenchmark(compareWith, historyProvider); + if (prevBench) { + consoleLog(`Found previous benchmark for ${renderCompareWith(compareWith)}, at commit ${prevBench.commitSha}`); + validateBenchmark(prevBench); + } else { + consoleLog(`No previous benchmark found for ${renderCompareWith(compareWith)}`); + } + + const {files, unmatchedFiles} = await collectFiles(opts).catch((err) => { + consoleLog("Error loading up spec patterns"); + throw err; + }); + + if (unmatchedFiles.length > 0) { + consoleLog(`Found unmatched files: \n${unmatchedFiles.join("\n")}\n`); + } + + if (files.length === 0) { + consoleLog(`Can not find any matching spec file for ${opts.spec.join(",")}\n`); + process.exit(1); + } + + const runner = new BenchmarkRunner({prevBench, benchmarkOpts: opts}); + const results = await runner.process(files); + + if (results.length === 0) { + throw Error("No benchmark result was produced"); + } + + const currentCommit = await getCurrentCommitInfo(); + const currBench: Benchmark = { + commitSha: currentCommit.commitSha, + results, + }; + + // Persist new benchmark data + const currentBranch = await getCurrentBranch(); + const shouldPersist = await resolveShouldPersist(opts, currentBranch); + if (shouldPersist === true) { + const refStr = github.context.ref || (await shell("git symbolic-ref HEAD")); + const branch = parseBranchFromRef(refStr); + consoleLog(`Persisting new benchmark data for branch '${branch}' commit '${currBench.commitSha}'`); + // TODO: prune and limit total entries + // appendBenchmarkToHistoryAndPrune(history, currBench, branch, opts); + await historyProvider.writeLatestInBranch(branch, currBench); + await historyProvider.writeToHistory(currBench); + } + + const resultsComp = computeBenchComparision(currBench, prevBench, opts.threshold); + + if (!opts.skipPostComment && isGaRun()) { + await postGaComment(resultsComp); + } + + if (resultsComp.someFailed && !opts.noThrow) { + throw Error("Performance regression"); + } +} diff --git a/src/compare/compute.ts b/src/compare/compute.ts index 84e7ea1..32513ad 100644 --- a/src/compare/compute.ts +++ b/src/compare/compute.ts @@ -1,10 +1,10 @@ -import {ResultComparision, BenchmarkComparision, Benchmark, BenchmarkResult} from "../types.js"; +import {ResultComparison, BenchmarkComparison, Benchmark, BenchmarkResult} from "../types.js"; export function computeBenchComparision( currBench: Benchmark, prevBench: Benchmark | null, threshold: number -): BenchmarkComparision { +): BenchmarkComparison { const prevResults = new Map(); if (prevBench) { for (const bench of prevBench.results) { @@ -12,7 +12,7 @@ export function computeBenchComparision( } } - const results = currBench.results.map((currBench): ResultComparision => { + const results = currBench.results.map((currBench): ResultComparison => { const {id} = currBench; const prevBench = prevResults.get(id); const thresholdBench = currBench.threshold ?? threshold; diff --git a/src/compare/index.ts b/src/compare/index.ts index c9615ed..2fd0d6a 100644 --- a/src/compare/index.ts +++ b/src/compare/index.ts @@ -1,5 +1,5 @@ import * as github from "@actions/github"; -import {Benchmark, Opts} from "../types.js"; +import {Benchmark, StorageOptions} from "../types.js"; import {getGithubEventData, GithubActionsEventData, parseBranchFromRef, getDefaultBranch} from "../utils/index.js"; import {isGaRun} from "../github/context.js"; import {IHistoryProvider} from "../history/provider.js"; @@ -14,7 +14,7 @@ export type CompareWith = | {type: CompareWithType.latestCommitInBranch; branch: string; before?: string} | {type: CompareWithType.exactCommit; commitSha: string}; -export async function resolveCompare(provider: IHistoryProvider, opts: Opts): Promise { +export async function resolveCompare(provider: IHistoryProvider, opts: StorageOptions): Promise { const compareWith = await resolveCompareWith(opts); const prevBench = await resolvePrevBenchmark(compareWith, provider); if (!prevBench) return null; @@ -54,7 +54,7 @@ export function renderCompareWith(compareWith: CompareWith): string { } } -export async function resolveCompareWith(opts: Opts): Promise { +export async function resolveCompareWith(opts: StorageOptions): Promise { // compare may be a branch or commit if (opts.compareBranch) { return {type: CompareWithType.latestCommitInBranch, branch: opts.compareBranch}; diff --git a/src/github/comment.ts b/src/github/comment.ts index f668ad2..5eb04ad 100644 --- a/src/github/comment.ts +++ b/src/github/comment.ts @@ -1,9 +1,9 @@ import * as github from "@actions/github"; -import {BenchmarkComparision} from "../types.js"; +import {BenchmarkComparison} from "../types.js"; import {commetToPrUpdatable, commentToCommit} from "./octokit.js"; import {getGithubEventData, GithubActionsEventData, renderComment} from "../utils/index.js"; -export async function postGaComment(resultsComp: BenchmarkComparision): Promise { +export async function postGaComment(resultsComp: BenchmarkComparison): Promise { switch (github.context.eventName) { case "pull_request": { const eventData = getGithubEventData(); diff --git a/src/history/append.ts b/src/history/append.ts index 6d6d571..6234c27 100644 --- a/src/history/append.ts +++ b/src/history/append.ts @@ -1,10 +1,10 @@ -import {Benchmark, BenchmarkHistory, Opts} from "../types.js"; +import {Benchmark, BenchmarkHistory, StorageOptions} from "../types.js"; export function appendBenchmarkToHistoryAndPrune( history: BenchmarkHistory, newBench: Benchmark, branch: string, - opts: Opts + opts: StorageOptions ): void { if (opts.benchmarksPerBranch) { limitBenchmarksPerBranch(history, opts.benchmarksPerBranch); diff --git a/src/history/index.ts b/src/history/index.ts index cab1900..2f8db67 100644 --- a/src/history/index.ts +++ b/src/history/index.ts @@ -1,13 +1,13 @@ -import {Opts} from "../types.js"; +import {StorageOptions} from "../types.js"; import {resolveHistoryLocation} from "./location.js"; import {LocalHistoryProvider} from "./local.js"; import {getGaCacheHistoryProvider} from "./gaCache.js"; import {IHistoryProvider} from "./provider.js"; -import {optionsDefault} from "../options.js"; +import {optionsDefault} from "../cli/options.js"; import {S3HistoryProvider} from "./s3.js"; export {resolveHistoryLocation}; -export function getHistoryProvider(opts: Opts): IHistoryProvider { +export function getHistoryProvider(opts: StorageOptions): IHistoryProvider { if (opts.historyGaCache) { const cacheKey = typeof opts.historyGaCache === "string" ? opts.historyGaCache : optionsDefault.historyCacheKey; return getGaCacheHistoryProvider(cacheKey); diff --git a/src/history/location.ts b/src/history/location.ts index cef5c0a..e064130 100644 --- a/src/history/location.ts +++ b/src/history/location.ts @@ -1,10 +1,10 @@ import {isGaRun} from "../github/context.js"; -import {Opts} from "../types.js"; -import {optionsDefault} from "../options.js"; +import {StorageOptions} from "../types.js"; +import {optionsDefault} from "../cli/options.js"; export type HistoryLocation = {type: "local"; path: string} | {type: "ga-cache"; key: string}; -export function resolveHistoryLocation(opts: Opts): HistoryLocation { +export function resolveHistoryLocation(opts: StorageOptions): HistoryLocation { if (opts.historyLocal && opts.historyGaCache) { throw Error("Must not set 'historyLocal' and 'historyGaCache'"); } diff --git a/src/history/shouldPersist.ts b/src/history/shouldPersist.ts index b4a8507..280c333 100644 --- a/src/history/shouldPersist.ts +++ b/src/history/shouldPersist.ts @@ -1,7 +1,7 @@ -import {Opts} from "../types.js"; +import {StorageOptions} from "../types.js"; import {getDefaultBranch} from "../utils/defaultBranch.js"; -export async function resolveShouldPersist(opts: Opts, branch: string): Promise { +export async function resolveShouldPersist(opts: StorageOptions, branch: string): Promise { // Force persist if (opts.persist === true) return true; // Do not persist diff --git a/src/index.ts b/src/index.ts index 12b30d0..ef216f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,9 @@ -export * from "./mochaPlugin/index.js"; +export {suite as describe, beforeEach, beforeAll} from "@vitest/runner"; +export {bench, setBenchOpts, setBenchmarkOptions} from "./benchmark/index.js"; + +import {bench} from "./benchmark/index.js"; + +/** + * @deprecated We recommend to use `bench` instead. + */ +export const itBench = bench; diff --git a/src/mochaPlugin/globalState.ts b/src/mochaPlugin/globalState.ts deleted file mode 100644 index edb3e50..0000000 --- a/src/mochaPlugin/globalState.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {BenchmarkResult, BenchmarkOpts} from "../types"; - -/**t - * Map of results by root suie. - * Before running mocha, you must register the root suite here - */ -export const resultsByRootSuite = new WeakMap>(); - -/** - * Global opts from CLI - */ -export const optsByRootSuite = new WeakMap(); - -/** - * Map to persist options set in describe blocks - */ -export const optsMap = new Map(); diff --git a/src/mochaPlugin/index.ts b/src/mochaPlugin/index.ts deleted file mode 100644 index a1e9cef..0000000 --- a/src/mochaPlugin/index.ts +++ /dev/null @@ -1,182 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import {BenchmarkOpts} from "../types.js"; -import {optsByRootSuite, optsMap, resultsByRootSuite} from "./globalState.js"; -import {BenchmarkRunOptsWithFn, runBenchFn} from "./runBenchFn.js"; -import {getRootSuite, getParentSuite} from "./utils.js"; - -type PartialBy = Omit & Partial>; - -const itBenchFn: ItBenchFn = function itBench( - idOrOpts: string | PartialBy, "fn">, - fn?: (arg: T) => void | Promise -): void { - // TODO: - // Implement reporter - // Implement grouping functionality - - // if (this.averageNs === null) this.averageNs = result.averageNs; - // result.factor = result.averageNs / this.averageNs; - - let opts = coerceToOptsObj(idOrOpts, fn); - - // Apply mocha it opts - const itFn = opts.only ? it.only : opts.skip ? it.skip : it; - - itFn(opts.id, async function () { - const parent = getParentSuite(this); - const optsParent = getOptsFromParent(parent); - - // Get results array from root suite - const rootSuite = getRootSuite(parent); - const results = resultsByRootSuite.get(rootSuite); - const rootOpts = optsByRootSuite.get(rootSuite); - if (!results || !rootOpts) throw Error("root suite not found"); - - opts = Object.assign({}, rootOpts, optsParent, opts); - - // Ensure bench id is unique - if (results.has(opts.id)) { - throw Error(`test titles must be unique, duplicated: '${opts.id}'`); - } - - // Extend timeout if maxMs is set - if (opts.timeoutBench !== undefined) { - this.timeout(opts.timeoutBench); - } else { - const timeout = this.timeout(); - if (opts.maxMs && opts.maxMs > timeout) { - this.timeout(opts.maxMs * 1.5); - } else if (opts.minMs && opts.minMs > timeout) { - this.timeout(opts.minMs * 1.5); - } - } - - // Persist full results if requested. dir is created in `beforeAll` - const benchmarkResultsCsvDir = process.env.BENCHMARK_RESULTS_CSV_DIR; - const persistRunsNs = Boolean(benchmarkResultsCsvDir); - - const {result, runsNs} = await runBenchFn(opts, persistRunsNs); - - // Store result for: - // - to persist benchmark data latter - // - to render with the custom reporter - results.set(opts.id, result); - - if (benchmarkResultsCsvDir) { - fs.mkdirSync(benchmarkResultsCsvDir, {recursive: true}); - const filename = `${result.id}.csv`; - const filepath = path.join(benchmarkResultsCsvDir, filename); - fs.writeFileSync(filepath, runsNs.join("\n")); - } - }); -}; - -interface ItBenchFn { - (opts: BenchmarkRunOptsWithFn): void; - (idOrOpts: string | Omit, "fn">, fn: (arg: T) => void): void; - ( - idOrOpts: string | PartialBy, "fn">, - fn?: (arg: T) => void | Promise - ): void; -} - -interface ItBench extends ItBenchFn { - only: ItBenchFn; - skip: ItBenchFn; -} - -export const itBench = itBenchFn as ItBench; - -itBench.only = function itBench(idOrOpts, fn): void { - const opts = coerceToOptsObj(idOrOpts, fn); - opts.only = true; - itBenchFn(opts); -} as ItBenchFn; - -itBench.skip = function itBench(idOrOpts, fn): void { - const opts = coerceToOptsObj(idOrOpts, fn); - opts.skip = true; - itBenchFn(opts); -} as ItBenchFn; - -function coerceToOptsObj( - idOrOpts: string | PartialBy, "fn">, - fn?: (arg: T) => void | Promise -): BenchmarkRunOptsWithFn { - let opts: BenchmarkRunOptsWithFn; - - if (typeof idOrOpts === "string") { - if (!fn) throw Error("fn arg must be set"); - opts = {id: idOrOpts, fn}; - } else { - if (fn) { - opts = {...idOrOpts, fn}; - } else { - const optsWithFn = idOrOpts as BenchmarkRunOptsWithFn; - if (!optsWithFn.fn) throw Error("opts.fn arg must be set"); - opts = optsWithFn; - } - } - - return opts; -} - -/** - * Customize benchmark opts for a describe block. Affects only tests within that Mocha.Suite - * ```ts - * describe("suite A1", function () { - * setBenchOpts({runs: 100}); - * // 100 runs - * itBench("bench A1.1", function() {}); - * itBench("bench A1.2", function() {}); - * // 300 runs - * itBench({id: "bench A1.3", runs: 300}, function() {}); - * - * // Supports nesting, child has priority over parent. - * // Arrow functions can be used, won't break it. - * describe("suite A2", () => { - * setBenchOpts({runs: 200}); - * // 200 runs. - * itBench("bench A2.1", () => {}); - * }) - * }) - * ``` - */ -export function setBenchOpts(opts: BenchmarkOpts): void { - before(function () { - if (this.currentTest?.parent) { - optsMap.set(this.currentTest?.parent, opts); - } - }); - - after(function () { - // Clean-up to allow garbage collection - if (this.currentTest?.parent) { - optsMap.delete(this.currentTest?.parent); - } - }); -} - -function getOptsFromParent(parent: Mocha.Suite): BenchmarkOpts { - const optsArr: BenchmarkOpts[] = []; - getOptsFromSuite(parent, optsArr); - // Merge opts, highest parent = lowest priority - return Object.assign({}, ...optsArr.reverse()) as BenchmarkOpts; -} - -/** - * Recursively append suite opts from child to parent. - * - * @returns `[suiteChildOpts, suiteParentOpts, suiteParentParentOpts]` - */ -function getOptsFromSuite(suite: Mocha.Suite, optsArr: BenchmarkOpts[]): void { - const suiteOpts = optsMap.get(suite); - if (suiteOpts) { - optsArr.push(suiteOpts); - } - - if (suite.parent) { - getOptsFromSuite(suite.parent, optsArr); - } -} diff --git a/src/mochaPlugin/mochaRunner.ts b/src/mochaPlugin/mochaRunner.ts deleted file mode 100644 index 1a2b53c..0000000 --- a/src/mochaPlugin/mochaRunner.ts +++ /dev/null @@ -1,50 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import Mocha from "mocha"; -import {optsByRootSuite, resultsByRootSuite} from "./globalState.js"; -import {collectFiles, FileCollectionOptions} from "../utils/mochaCliExports.js"; -import {benchmarkReporterWithPrev} from "./reporter.js"; -import {Benchmark, BenchmarkOpts, BenchmarkResult, onlyBenchmarkOpts, Opts} from "../types.js"; - -export async function runMochaBenchmark( - opts: Opts & BenchmarkOpts, - prevBench: Benchmark | null -): Promise { - const mocha = new Mocha({ - // Pass all options to mocha, from upstream CLI - ...opts, - reporter: benchmarkReporterWithPrev(prevBench, opts.threshold), - // rootHooks: {beforeAll}, - }); - - // Register mocha root suite to append results on it() blocks - const results = new Map(); - resultsByRootSuite.set(mocha.suite, results); - optsByRootSuite.set(mocha.suite, onlyBenchmarkOpts(opts)); - - // Recreate `singleRun()` function - https://github.com/mochajs/mocha/blob/dcad90ad6e79864c871e2bc55b22c79ac6952991/lib/cli/run-helpers.js#L120 - const fileCollectParams: FileCollectionOptions = { - ignore: opts.ignore ?? [], - extension: opts.extension ?? [], - file: opts.file ?? [], - recursive: opts.recursive ?? false, - sort: opts.sort ?? false, - spec: opts.spec ?? [], - }; - const files = collectFiles(fileCollectParams); - mocha.files = files.files; - await mocha.loadFilesAsync(); - - // Run the tests. - await new Promise((resolve, reject) => { - mocha.run(function (failures) { - // process.exitCode = failures ? 1 : 0; // exit with non-zero status if there were failures - if (failures > 0) { - reject(Error("Some tests failed")); - } else { - resolve(); - } - }); - }); - - return Array.from(results.values()); -} diff --git a/src/mochaPlugin/reporter.ts b/src/mochaPlugin/reporter.ts deleted file mode 100644 index f1771dd..0000000 --- a/src/mochaPlugin/reporter.ts +++ /dev/null @@ -1,102 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import Mocha from "mocha"; -import {Benchmark, BenchmarkResult} from "../types.js"; -import {resultsByRootSuite} from "./globalState.js"; -import {formatResultRow} from "./format.js"; -import {getRootSuite} from "./utils.js"; - -const { - EVENT_RUN_BEGIN, - EVENT_RUN_END, - EVENT_TEST_FAIL, - EVENT_TEST_PASS, - EVENT_TEST_PENDING, - EVENT_SUITE_BEGIN, - EVENT_SUITE_END, -} = Mocha.Runner.constants; - -interface ReporterConstructor { - new (runner: Mocha.Runner, options: Mocha.MochaOptions): Mocha.reporters.Base; -} - -export function benchmarkReporterWithPrev(prevBench: Benchmark | null, threshold: number): ReporterConstructor { - const prevResults = new Map(); - if (prevBench) { - for (const bench of prevBench.results) { - prevResults.set(bench.id, bench); - } - } - - return class BenchmarkReporter extends Mocha.reporters.Base { - constructor(runner: Mocha.Runner, options?: Mocha.MochaOptions) { - super(runner, options); - - // eslint-disable-next-line no-console - const consoleLog = console.log; - const color = Mocha.reporters.Base.color; - const symbols = Mocha.reporters.Base.symbols; - - let indents = 0; - let n = 0; - - function indent(): string { - return Array(indents).join(" "); - } - - runner.on(EVENT_RUN_BEGIN, function () { - consoleLog(); - }); - - runner.on(EVENT_SUITE_BEGIN, function (suite) { - ++indents; - consoleLog(color("suite", "%s%s"), indent(), suite.title); - }); - - runner.on(EVENT_SUITE_END, function () { - --indents; - if (indents === 1) { - consoleLog(); - } - }); - - runner.on(EVENT_TEST_PENDING, function (test) { - const fmt = indent() + color("pending", " - %s"); - consoleLog(fmt, test.title); - }); - - runner.on(EVENT_TEST_PASS, function (test) { - try { - if (!test.parent) throw Error("test has no parent"); - const rootSuite = getRootSuite(test.parent); - const results = resultsByRootSuite.get(rootSuite); - if (!results) throw Error("root suite not found"); - - const result = results.get(test.title); - if (result) { - // Render benchmark - const prevResult = prevResults.get(result.id) ?? null; - - const resultRow = formatResultRow(result, prevResult, result.threshold ?? threshold); - const fmt = indent() + color("checkmark", " " + symbols.ok) + " " + resultRow; - consoleLog(fmt); - } else { - // Render regular test - const fmt = indent() + color("checkmark", " " + symbols.ok) + color("pass", " %s"); - consoleLog(fmt, test.title); - } - } catch (e) { - // Log error manually since mocha doesn't log errors thrown here - consoleLog(e); - process.exitCode = 1; - throw e; - } - }); - - runner.on(EVENT_TEST_FAIL, function (test) { - consoleLog(indent() + color("fail", " %d) %s"), ++n, test.title); - }); - - runner.once(EVENT_RUN_END, this.epilogue.bind(this)); - } - }; -} diff --git a/src/mochaPlugin/utils.ts b/src/mochaPlugin/utils.ts deleted file mode 100644 index db7aa04..0000000 --- a/src/mochaPlugin/utils.ts +++ /dev/null @@ -1,31 +0,0 @@ -export function getParentSuite(ctx: Mocha.Context): Mocha.Suite { - const test = ctx.currentTest ?? ctx.test; - if (!test) throw Error("this.test not set"); - if (!test.parent) throw Error("this.test.parent not set"); - return test.parent; -} - -export function getRootSuite(suite: Mocha.Suite): Mocha.Suite { - if (!suite.parent) return suite; - return getRootSuite(suite.parent); -} - -export function getAllTestsInRootSuite(ctx: Mocha.Context): Mocha.Test[] { - const parent = getParentSuite(ctx); - const rootSuite = getRootSuite(parent); - - const tests: Mocha.Test[] = []; - - function getTests(suite: Mocha.Suite): void { - for (const test of suite.tests) { - tests.push(test); - } - for (const childSuite of suite.suites) { - getTests(childSuite); - } - } - - getTests(rootSuite); - - return tests; -} diff --git a/src/run.ts b/src/run.ts deleted file mode 100644 index e5b6aa7..0000000 --- a/src/run.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as github from "@actions/github"; -import {getHistoryProvider} from "./history/index.js"; -import {resolveShouldPersist} from "./history/shouldPersist.js"; -import {validateBenchmark} from "./history/schema.js"; -import {Benchmark, BenchmarkOpts, Opts} from "./types.js"; -import {renderCompareWith, resolveCompareWith, resolvePrevBenchmark} from "./compare/index.js"; -import {parseBranchFromRef, getCurrentCommitInfo, shell, getCurrentBranch} from "./utils/index.js"; -import {runMochaBenchmark} from "./mochaPlugin/mochaRunner.js"; -import {computeBenchComparision} from "./compare/compute.js"; -import {postGaComment} from "./github/comment.js"; -import {isGaRun} from "./github/context.js"; - -/* eslint-disable no-console */ - -export async function run(opts: Opts & BenchmarkOpts): Promise { - // Sanitize opts - if (isNaN(opts.threshold)) throw Error("opts.threshold is not a number"); - - // Retrieve history - const historyProvider = getHistoryProvider(opts); - console.log(`Connected to historyProvider: ${historyProvider.providerInfo()}`); - - // Select prev benchmark to compare against - const compareWith = await resolveCompareWith(opts); - const prevBench = await resolvePrevBenchmark(compareWith, historyProvider); - if (prevBench) { - console.log(`Found previous benchmark for ${renderCompareWith(compareWith)}, at commit ${prevBench.commitSha}`); - validateBenchmark(prevBench); - } else { - console.log(`No previous bencharmk found for ${renderCompareWith(compareWith)}`); - } - - // TODO: Forward all options to mocha - // Run benchmarks with mocha programatically - const results = await runMochaBenchmark(opts, prevBench); - if (results.length === 0) { - throw Error("No benchmark result was produced"); - } - - const currentCommit = await getCurrentCommitInfo(); - const currBench: Benchmark = { - commitSha: currentCommit.commitSha, - results, - }; - - // Persist new benchmark data - const currentBranch = await getCurrentBranch(); - const shouldPersist = await resolveShouldPersist(opts, currentBranch); - if (shouldPersist === true) { - const refStr = github.context.ref || (await shell("git symbolic-ref HEAD")); - const branch = parseBranchFromRef(refStr); - console.log(`Persisting new benchmark data for branch '${branch}' commit '${currBench.commitSha}'`); - // TODO: prune and limit total entries - // appendBenchmarkToHistoryAndPrune(history, currBench, branch, opts); - await historyProvider.writeLatestInBranch(branch, currBench); - await historyProvider.writeToHistory(currBench); - } - - const resultsComp = computeBenchComparision(currBench, prevBench, opts.threshold); - - if (!opts.skipPostComment && isGaRun()) { - await postGaComment(resultsComp); - } - - if (resultsComp.someFailed && !opts.noThrow) { - throw Error("Performance regression"); - } -} diff --git a/src/types.ts b/src/types.ts index f3f7cd4..7b35d8a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,18 @@ -import {FileCollectionOptions} from "./utils/mochaCliExports.js"; +export interface FileCollectionOptions { + /** File extensions to use */ + extension: string[]; + /** Files, dirs, globs to ignore */ + ignore: string[]; + /** Find files recursively */ + recursive: boolean; + /** Glob pattern to load spec */ + spec: string[]; +} -export type Opts = Partial & { +export type StorageOptions = { defaultBranch?: string; persistBranches?: string[]; benchmarksPerBranch?: number; - threshold: number; compareBranch?: string; compareCommit?: string; prune?: boolean; @@ -41,8 +49,7 @@ export type BenchmarkOpts = { runsFactor?: number; /** Run `sleep(0)` after each fn() call. Use when the event loop needs to tick to free resources created by fn() */ yieldEventLoopAfterEach?: boolean; - /** Hard timeout, enforced by mocha. */ - // NOTE: Must not use `.timeout` or it collisions with mocha's .timeout option. It defaults to 2000 and messed up everything + /** Hard timeout */ timeoutBench?: number; // For reporter /** Customize the threshold for this specific benchmark. Set to Infinity to disable it */ @@ -50,38 +57,32 @@ export type BenchmarkOpts = { /** Equivalent to setting threshold = Infinity */ noThreshold?: boolean; - // For mocha only?: boolean; skip?: boolean; }; -/** Manual lodash.pick() function. Ensure no unwanted options end up in optsByRootSuite */ -export function onlyBenchmarkOpts(opts: BenchmarkOpts): BenchmarkOpts { - // Define in this way so Typescript guarantees all keys are considered - const keysObj: Record = { - maxRuns: true, - minRuns: true, - maxMs: true, - minMs: true, - maxWarmUpMs: true, - maxWarmUpRuns: true, - convergeFactor: true, - runsFactor: true, - yieldEventLoopAfterEach: true, - timeoutBench: true, - threshold: true, - noThreshold: true, - only: true, - skip: true, - }; +// Create partial only for specific keys +export type PartialBy = Omit & Partial>; + +export type BenchmarkRunOptsWithFn = BenchmarkOpts & { + id: string; + fn: (arg: T) => void | Promise; + before?: () => T2 | Promise; + beforeEach?: (arg: T2, i: number) => T | Promise; +}; + +export interface BenchFuncApi { + (opts: BenchmarkRunOptsWithFn): void; + (idOrOpts: string | Omit, "fn">, fn: (arg: T) => void): void; + ( + idOrOpts: string | PartialBy, "fn">, + fn?: (arg: T) => void | Promise + ): void; +} - const optsOut = {} as Record; - for (const key of Object.keys(keysObj) as (keyof BenchmarkOpts)[]) { - if (opts[key] !== undefined) { - optsOut[key] = opts[key]; - } - } - return optsOut as BenchmarkOpts; +export interface BenchApi extends BenchFuncApi { + only: BenchFuncApi; + skip: BenchFuncApi; } export type BenchmarkResults = BenchmarkResult[]; @@ -109,14 +110,14 @@ export type BenchmarkHistory = { }; }; -export type BenchmarkComparision = { +export type BenchmarkComparison = { currCommitSha: string; prevCommitSha: string | null; someFailed: boolean; - results: ResultComparision[]; + results: ResultComparison[]; }; -export type ResultComparision = { +export type ResultComparison = { id: string; currAverageNs: number; prevAverageNs: number | null; diff --git a/src/utils/defaultBranch.ts b/src/utils/defaultBranch.ts index fbae6cf..20c0fd0 100644 --- a/src/utils/defaultBranch.ts +++ b/src/utils/defaultBranch.ts @@ -1,6 +1,6 @@ import {isGaRun} from "../github/context.js"; import {getGithubDefaultBranch} from "../github/octokit.js"; -import {Opts} from "../types.js"; +import {StorageOptions} from "../types.js"; import {shell} from "./shell.js"; let defaultBranch: string | null = null; @@ -8,7 +8,7 @@ let defaultBranch: string | null = null; /** * Return a cached value of a best guess of the repo's default branch */ -export async function getDefaultBranch(opts?: Pick): Promise { +export async function getDefaultBranch(opts?: Pick): Promise { if (opts?.defaultBranch) { return opts.defaultBranch; } diff --git a/src/utils/file.ts b/src/utils/file.ts index 91ce55d..1c46267 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -1,6 +1,9 @@ import fs from "node:fs"; +import path from "node:path"; +import {glob} from "glob"; import csvParse from "csv-parse/lib/sync.js"; import csvStringify from "csv-stringify/lib/sync.js"; +import {FileCollectionOptions} from "../types.js"; type CsvMetadata = Record; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -72,3 +75,37 @@ function splitCsvMetadata(str: string): {csv: string; metadata: Record { + const matchedFiles = new Set(); + const unmatchedFiles: string[] = []; + + // Normalize extensions to ensure leading dots + const normalizedExtensions = extension.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`)); + + for (const pattern of spec) { + const files = await glob(pattern, { + ignore, + nodir: true, + cwd: process.cwd(), + absolute: true, + follow: recursive, + }); + + for (const file of files) { + if (normalizedExtensions.includes(path.extname(file))) { + matchedFiles.add(file); + } else { + unmatchedFiles.push(file); + } + } + } + + return { + files: Array.from(matchedFiles), + unmatchedFiles, + }; +} diff --git a/src/utils/mochaCliExports.ts b/src/utils/mochaCliExports.ts deleted file mode 100644 index cb6fb6d..0000000 --- a/src/utils/mochaCliExports.ts +++ /dev/null @@ -1,44 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -// eslint-disable-next-line import/no-extraneous-dependencies -import {lookupFiles as lookupFilesMocha, loadOptions as loadOptionsMocha} from "mocha/lib/cli/index.js"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -// eslint-disable-next-line import/no-extraneous-dependencies -import collectFilesMocha from "mocha/lib/cli/collect-files.js"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -// eslint-disable-next-line import/no-extraneous-dependencies -import {handleRequires as handleRequiresMocha} from "mocha/lib/cli/run-helpers.js"; - -export const lookupFiles = lookupFilesMocha as ( - filepath: string, - extensions?: string[], - recursive?: boolean -) => string[]; - -export const loadOptions = loadOptionsMocha as (argv: string[]) => Record & {_: string[]}; - -export interface FileCollectionOptions { - /** File extensions to use */ - extension: string[]; - /** Files, dirs, globs to run */ - spec: string[]; - /** Files, dirs, globs to ignore */ - ignore: string[]; - /** List of additional files to include */ - file: string[]; - /** Find files recursively */ - recursive: boolean; - /** Sort test files */ - sort: boolean; -} - -export interface FileCollectionResponse { - files: string[]; - unmatchedFiles: string[]; -} - -export const collectFiles = collectFilesMocha as (fileCollectParams: FileCollectionOptions) => FileCollectionResponse; - -export const handleRequires = handleRequiresMocha as (requires?: string[]) => Promise; diff --git a/src/utils/output.ts b/src/utils/output.ts new file mode 100644 index 0000000..8ef9e97 --- /dev/null +++ b/src/utils/output.ts @@ -0,0 +1,42 @@ +import logSymbols from "log-symbols"; + +const colors = { + pass: 90, + fail: 31, + brightPass: 92, + brightFail: 91, + brightYellow: 93, + pending: 36, + suite: 0, + errorTitle: 0, + errorMessage: 31, + errorStack: 90, + checkmark: 32, + fast: 90, + medium: 33, + slow: 31, + green: 32, + light: 90, + diffGutter: 90, + diffAdded: 32, + diffRemoved: 31, + diffAddedInline: "30;42", + diffRemovedInline: "30;41", +}; + +export const symbols = { + ok: logSymbols.success, + err: logSymbols.error, + dot: ".", + comma: ",", + bang: "!", +}; + +export function color(type: keyof typeof colors, str: string): string { + return "\u001b[" + colors[type] + "m" + str + "\u001b[0m"; +} + +export function consoleLog(...args: unknown[]): void { + // eslint-disable-next-line no-console + console.log(...args); +} diff --git a/src/utils/render.ts b/src/utils/render.ts index 04b8b85..d287b5e 100644 --- a/src/utils/render.ts +++ b/src/utils/render.ts @@ -1,8 +1,8 @@ -import {BenchmarkComparision, ResultComparision} from "../types.js"; +import {BenchmarkComparison, ResultComparison} from "../types.js"; -type CommitsSha = Pick; +type CommitsSha = Pick; -export function renderComment(benchComp: BenchmarkComparision): string { +export function renderComment(benchComp: BenchmarkComparison): string { const isFailedResults = benchComp.results.filter((r) => r.isFailed); const isImprovedResults = benchComp.results.filter((r) => r.isImproved); @@ -42,7 +42,7 @@ ${renderBenchmarkTable(benchComp.results, benchComp)} `; } -function renderBenchmarkTable(benchComp: ResultComparision[], {currCommitSha, prevCommitSha}: CommitsSha): string { +function renderBenchmarkTable(benchComp: ResultComparison[], {currCommitSha, prevCommitSha}: CommitsSha): string { function toRow(arr: (number | string)[]): string { // Don't surround string items with \`, it doesn't look great rendered in Github comments const row = arr.map((e) => `${e}`).join(" | "); diff --git a/test/perf/iteration.test.ts b/test/perf/iteration.test.ts index 135eaeb..0ee1e6f 100644 --- a/test/perf/iteration.test.ts +++ b/test/perf/iteration.test.ts @@ -1,5 +1,4 @@ -import assert from "node:assert"; -import {itBench, setBenchOpts} from "../../src/index.js"; +import {bench, describe, setBenchOpts} from "../../src/index.js"; // As of Jun 17 2021 // Compare state root @@ -11,15 +10,11 @@ import {itBench, setBenchOpts} from "../../src/index.js"; describe("Array iteration", () => { setBenchOpts({maxMs: 60 * 1000, convergeFactor: 0.1 / 100}); - it("Regular test", () => { - assert.strictEqual(1 + 2, 3); - }); - // nonce = 5 const n = 1e6; const arr = Array.from({length: n}, (_, i) => i); - itBench("sum array with raw for loop", () => { + bench("sum array with raw for loop", () => { let sum = 0; for (let i = 0, len = arr.length; i < len; i++) { sum += i; @@ -27,7 +22,7 @@ describe("Array iteration", () => { return sum; }); - itBench("sum array with reduce", () => { + bench("sum array with reduce", () => { arr.reduce((total, curr) => total + curr, 0); // Uncomment below to cause a guaranteed performance regression @@ -37,7 +32,7 @@ describe("Array iteration", () => { // Test before and beforeEach hooks - itBench({ + bench({ id: "sum array with reduce beforeEach", beforeEach: () => Array.from({length: 1e4}, (_, i) => i), fn: () => { @@ -49,7 +44,7 @@ describe("Array iteration", () => { }, }); - itBench({ + bench({ id: "sum array with reduce before beforeEach", before: () => Array.from({length: 1e4}, (_, i) => i), beforeEach: (arr) => arr.slice(0), @@ -62,19 +57,17 @@ describe("Array iteration", () => { }, }); - // Test mocha skip and only - - itBench.skip("sum array with reduce", () => { + bench.skip("sum array with reduce", () => { arr.reduce((total, curr) => total + curr, 0); }); - // itBench.only("sum array with reduce", () => { + // bench.only("sum array with reduce", () => { // arr.reduce((total, curr) => total + curr, 0); // }); // Reporter options - itBench({ + bench({ id: "sum array with reduce high threshold", threshold: 5, fn: () => { @@ -82,7 +75,7 @@ describe("Array iteration", () => { }, }); - itBench({ + bench({ id: "sum array with reduce no threshold", threshold: Infinity, fn: () => { diff --git a/test/unit/history/local.test.ts b/test/unit/history/local.test.ts index 0b58df0..ac8c9dd 100644 --- a/test/unit/history/local.test.ts +++ b/test/unit/history/local.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import {expect} from "chai"; -import rimraf from "rimraf"; +import {rimrafSync} from "rimraf"; import {Benchmark} from "../../../src/types.js"; import {LocalHistoryProvider} from "../../../src/history/local.js"; @@ -15,7 +15,7 @@ describe("benchmark history local", () => { const historyProvider = new LocalHistoryProvider(testDir); after(() => { - rimraf.sync(testDir); + rimrafSync(testDir); }); it("Should write and read history", async () => { diff --git a/tsconfig.build.cjs.json b/tsconfig.build.cjs.json index 5c299e9..2946c10 100644 --- a/tsconfig.build.cjs.json +++ b/tsconfig.build.cjs.json @@ -5,5 +5,8 @@ "outDir": "./lib/cjs", "esModuleInterop": true, "module": "commonjs", + + // To build for Node 18 the lru-cache package has conflicting types + "skipLibCheck": true, } } diff --git a/tsconfig.build.esm.json b/tsconfig.build.esm.json index 4079686..913b549 100644 --- a/tsconfig.build.esm.json +++ b/tsconfig.build.esm.json @@ -5,5 +5,8 @@ "outDir": "./lib/esm", "esModuleInterop": true, "module": "es2020", + + // To build for Node 18 the lru-cache package has conflicting types + "skipLibCheck": true, } } diff --git a/yarn.lock b/yarn.lock index 6657bab..470b505 100644 --- a/yarn.lock +++ b/yarn.lock @@ -256,6 +256,18 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.1.tgz#9a96ce501bc62df46c4031fbd970e3cc6b10f07b" integrity sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@jridgewell/resolve-uri@^3.0.3": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" @@ -396,6 +408,11 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.0.0-rc.0.tgz#0c7c3f5e1285f99cedb563d74ad1adb9822b5144" integrity sha512-iXKByCMfrlO5S6Oh97BuM56tM2cIBB0XsL/vWF/AtJrJEKx4MC/Xdu0xDsGXMGcNWpqF7ujMsjjnp0+UHBwnDQ== +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@pkgr/core@^0.1.0": version "0.1.1" resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" @@ -436,14 +453,6 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== -"@types/glob@*": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" - integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - "@types/json-schema@^7.0.15": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -454,11 +463,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/minimatch@*": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21" - integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA== - "@types/mocha@^10.0.9": version "10.0.9" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.9.tgz#101e9da88d2c02e5ac8952982c23b224524d662a" @@ -484,14 +488,6 @@ dependencies: undici-types "~5.26.4" -"@types/rimraf@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.0.tgz#b9d03f090ece263671898d57bb7bb007023ac19f" - integrity sha512-7WhJ0MdpFgYQPXlF4Dx+DhgvlPCfz/x5mHaeDQAKhcenvQP1KCpLQ18JklAqeGMYSAT2PxLpzd0g2/HE7fj7hQ== - dependencies: - "@types/glob" "*" - "@types/node" "*" - "@types/tunnel@^0.0.1": version "0.0.1" resolved "https://registry.yarnpkg.com/@types/tunnel/-/tunnel-0.0.1.tgz#0d72774768b73df26f25df9184273a42da72b19c" @@ -592,6 +588,30 @@ "@typescript-eslint/types" "8.16.0" eslint-visitor-keys "^4.2.0" +"@vitest/pretty-format@2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.6.tgz#9bc642047a3efc637b41492b1f222c43be3822e4" + integrity sha512-exZyLcEnHgDMKc54TtHca4McV4sKT+NKAe9ix/yhd/qkYb/TP8HTyXRFDijV19qKqTZM0hPL4753zU/U8L/gAA== + dependencies: + tinyrainbow "^1.2.0" + +"@vitest/runner@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.6.tgz#948cad2cccfe2e56be5b3f9979cf9a417ca59737" + integrity sha512-SjkRGSFyrA82m5nz7To4CkRSEVWn/rwQISHoia/DB8c6IHIhaE/UNAo+7UfeaeJRE979XceGl00LNkIz09RFsA== + dependencies: + "@vitest/utils" "2.1.6" + pathe "^1.1.2" + +"@vitest/utils@2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.6.tgz#2af6a82c5c45da35ecd322d0568247a6e9c95c5f" + integrity sha512-ixNkFy3k4vokOUTU2blIUvOgKq/N2PW8vKIjZZYsGJCMX69MRa9J2sKqX5hY/k5O5Gty3YJChepkqZ3KM9LyIQ== + dependencies: + "@vitest/pretty-format" "2.1.6" + loupe "^3.1.2" + tinyrainbow "^1.2.0" + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -651,6 +671,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -658,6 +683,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -956,7 +986,7 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.5: +cross-spawn@^7.0.0, cross-spawn@^7.0.5: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -1090,11 +1120,21 @@ dotenv@^10.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + enhanced-resolve@^5.15.0: version "5.17.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" @@ -1475,6 +1515,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + form-data@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" @@ -1588,17 +1636,17 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^7.1.3: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== +glob@^10.3.7, glob@^10.4.5: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" glob@^8.1.0: version "8.1.0" @@ -1950,6 +1998,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-unicode-supported@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz#09f0ab0de6d3744d48d265ebb98f65d11f2a9b3a" + integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ== + is-weakmap@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" @@ -1985,6 +2038,15 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jmespath@0.15.0: version "0.15.0" resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" @@ -2059,6 +2121,14 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +log-symbols@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-7.0.0.tgz#953999bb9cec27a09049c8f45e1154ec81163061" + integrity sha512-zrc91EDk2M+2AXo/9BTvK91pqb7qrPg2nX/Hy+u8a5qQlbaOflCKO+6SqgZ+M+xUFxGdKTgwnGiL96b1W3ikRA== + dependencies: + is-unicode-supported "^2.0.0" + yoctocolors "^2.1.1" + loupe@^2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" @@ -2066,6 +2136,16 @@ loupe@^2.3.6: dependencies: get-func-name "^2.0.1" +loupe@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.2.tgz#c86e0696804a02218f2206124c45d8b15291a240" + integrity sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg== + +lru-cache@10.4.3, lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" @@ -2134,6 +2214,11 @@ minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + mocha@^10.8.2: version "10.8.2" resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.8.2.tgz#8d8342d016ed411b12a429eb731b825f961afb96" @@ -2266,6 +2351,11 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -2278,11 +2368,6 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -2293,6 +2378,19 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + pathval@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" @@ -2431,12 +2529,12 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== +rimraf@^5.0.10: + version "5.0.10" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.10.tgz#23b9843d3dc92db71f96e1a2ce92e39fd2a8221c" + integrity sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ== dependencies: - glob "^7.1.3" + glob "^10.3.7" run-parallel@^1.1.9: version "1.2.0" @@ -2545,6 +2643,20 @@ side-channel@^1.0.4: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^4.1.0, string-width@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" @@ -2563,6 +2675,15 @@ string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string.prototype.trim@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" @@ -2591,6 +2712,13 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" @@ -2605,6 +2733,13 @@ strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -2647,6 +2782,11 @@ tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tinyrainbow@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5" + integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -2925,6 +3065,15 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -2934,6 +3083,15 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -3030,3 +3188,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yoctocolors@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/yoctocolors/-/yoctocolors-2.1.1.tgz#e0167474e9fbb9e8b3ecca738deaa61dd12e56fc" + integrity sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ== From d2a1b811e0ac62ed05e40ced2776a2595f5e5373 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Wed, 18 Dec 2024 11:24:19 +0100 Subject: [PATCH 2/8] test: add support to run tests in multiple runtimes (#8) * Update the node prefix to imports * Migrate test runner to vitest * Add test workflow for other runtimes * Update the unit tests * Fix the deno version * Update all unit tests * Update import prefix for node packages --- .github/workflows/test.yml | 42 +- .mocharc.yaml | 5 - package.json | 23 +- test/unit/history/gaCache.test.ts | 18 +- test/unit/history/local.test.ts | 8 +- test/unit/history/s3.test.ts | 14 +- test/unit/utils/file.test.ts | 18 +- vitest.config.ts | 36 ++ yarn.lock | 810 ++++++++++++++++++++---------- 9 files changed, 647 insertions(+), 327 deletions(-) delete mode 100644 .mocharc.yaml create mode 100644 vitest.config.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e86bfd7..5508a46 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,8 +3,8 @@ name: Tests on: [pull_request, push] jobs: - test: - name: Test + test-node: + name: Test Node strategy: fail-fast: false matrix: @@ -25,3 +25,41 @@ jobs: run: yarn lint - name: Unit tests run: yarn test:unit + + test-deno: + name: Test Deno + runs-on: ubuntu-latest + steps: + # - Uses YAML anchors in the future + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2-beta + with: + node-version: "22" + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + - name: Install + run: yarn install --frozen-lockfile + - name: Build + run: deno run build + - name: Unit tests + run: deno run -A test:unit + + test-bun: + name: Test Bun + runs-on: ubuntu-latest + steps: + # - Uses YAML anchors in the future + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2-beta + with: + node-version: "22" + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install + run: yarn install --frozen-lockfile + - name: Build + run: bun run --bun build + - name: Unit tests + run: bun run --bun test:unit diff --git a/.mocharc.yaml b/.mocharc.yaml deleted file mode 100644 index 0ec947f..0000000 --- a/.mocharc.yaml +++ /dev/null @@ -1,5 +0,0 @@ -loader: - - ts-node/esm -colors: true -timeout: 5000 -exit: true diff --git a/package.json b/package.json index e4849c3..b4c4b79 100644 --- a/package.json +++ b/package.json @@ -30,43 +30,42 @@ "build": "yarn build:esm && yarn build:cjs", "build:esm": "tsc -p tsconfig.build.esm.json && echo '{\"type\": \"module\"}' > ./lib/esm/package.json", "build:cjs": "tsc -p tsconfig.build.cjs.json && echo '{\"type\": \"commonjs\"}' > ./lib/cjs/package.json", - "test:unit": "mocha test/unit/**/*.test.ts", + "test:unit": "vitest run test/unit/**/*.test.ts", "lint": "eslint --color src/ test/", "prepublishOnly": "yarn build", "benchmark": "node --loader ts-node/esm ./src/cli/cli.ts 'test/perf/**/*.test.ts'", "writeDocs": "node --loader ts-node/esm scripts/writeOptionsMd.ts" }, "devDependencies": { - "@types/chai": "^4.2.19", - "@types/mocha": "^10.0.9", - "@types/node": "^18.15.3", + "@eslint/js": "^9.15.0", + "@types/node": "^22.10.2", "@types/yargs": "^17.0.33", "chai": "^4.5.0", "dotenv": "^10.0.0", "eslint": "^9.15.0", - "@eslint/js": "^9.15.0", - "eslint-plugin-import": "^2.31.0", + "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-prettier": "^5.2.1", - "eslint-config-prettier": "^9.1.0", - "mocha": "^10.8.2", "prettier": "^3.4.0", "rimraf": "^5.0.10", "ts-node": "^10.9.2", "typescript": "^5.6.3", - "typescript-eslint": "^8.16.0" + "typescript-eslint": "^8.16.0", + "vitest": "^2.1.8", + "vitest-in-process-pool": "^1.0.0" }, "dependencies": { "@actions/cache": "^1.0.7", "@actions/github": "^5.0.0", - "@vitest/runner": "^2.1.6", + "@vitest/runner": "^2.1.6", "ajv": "^8.6.0", "aws-sdk": "^2.932.0", "csv-parse": "^4.16.0", "csv-stringify": "^5.6.2", "glob": "^10.4.5", - "yargs": "^17.7.2", - "log-symbols": "^7.0.0" + "log-symbols": "^7.0.0", + "yargs": "^17.7.2" }, "resolutions": { "lru-cache": "10.4.3" diff --git a/test/unit/history/gaCache.test.ts b/test/unit/history/gaCache.test.ts index 1bfad1c..53f2395 100644 --- a/test/unit/history/gaCache.test.ts +++ b/test/unit/history/gaCache.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {expect, describe, it, vi, beforeAll} from "vitest"; import {Benchmark} from "../../../src/types.js"; import {getGaCacheHistoryProvider} from "../../../src/history/gaCache.js"; import {isGaRun} from "../../../src/github/context.js"; @@ -11,7 +11,7 @@ import {isGaRun} from "../../../src/github/context.js"; // - https://github.com/nektos/act/issues/329 // - https://github.com/nektos/act/issues/285 describe.skip("benchmark history gaCache", function () { - this.timeout(60 * 1000); + vi.setConfig({testTimeout: 60 * 1000}); const branch = "main"; const benchmark: Benchmark = { @@ -21,20 +21,20 @@ describe.skip("benchmark history gaCache", function () { const cacheKey = "ga-cache-testing"; let historyProvider: ReturnType; - before(() => { + beforeAll(() => { historyProvider = getGaCacheHistoryProvider(cacheKey); }); - it("writeLatestInBranch", async function () { - if (!isGaRun()) this.skip(); + it("writeLatestInBranch", async ({skip}) => { + if (!isGaRun()) return skip(); - await historyProvider.writeLatestInBranch(branch, benchmark); + await expect(historyProvider.writeLatestInBranch(branch, benchmark)).resolves.toBeDefined(); }); - it("readLatestInBranch", async function () { - if (!isGaRun()) this.skip(); + it("readLatestInBranch", async ({skip}) => { + if (!isGaRun()) return skip(); const benchRead = await historyProvider.readLatestInBranch(branch); - expect(benchRead).to.deep.equal(benchmark, "Wrong bench read from disk"); + expect(benchRead).toEqual(benchmark); }); }); diff --git a/test/unit/history/local.test.ts b/test/unit/history/local.test.ts index ac8c9dd..35cacbd 100644 --- a/test/unit/history/local.test.ts +++ b/test/unit/history/local.test.ts @@ -1,5 +1,5 @@ import fs from "node:fs"; -import {expect} from "chai"; +import {expect, describe, it, afterAll} from "vitest"; import {rimrafSync} from "rimraf"; import {Benchmark} from "../../../src/types.js"; import {LocalHistoryProvider} from "../../../src/history/local.js"; @@ -14,7 +14,7 @@ describe("benchmark history local", () => { const testDir = fs.mkdtempSync("test_files_"); const historyProvider = new LocalHistoryProvider(testDir); - after(() => { + afterAll(() => { rimrafSync(testDir); }); @@ -22,13 +22,13 @@ describe("benchmark history local", () => { await historyProvider.writeToHistory(benchmark); const benchmarks = await historyProvider.readHistory(); - expect(benchmarks).to.deep.equal([benchmark], "Wrong history"); + expect(benchmarks).toEqual([benchmark]); }); it("Should write and read latest in branch", async () => { await historyProvider.writeLatestInBranch(branch, benchmark); const benchRead = await historyProvider.readLatestInBranch(branch); - expect(benchRead).to.deep.equal(benchmark, "Wrong bench read from disk"); + expect(benchRead).toEqual(benchmark); }); }); diff --git a/test/unit/history/s3.test.ts b/test/unit/history/s3.test.ts index cf8d2cd..5ff3115 100644 --- a/test/unit/history/s3.test.ts +++ b/test/unit/history/s3.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {expect, describe, it, vi, beforeAll, afterAll} from "vitest"; import S3 from "aws-sdk/clients/s3.js"; import {Benchmark} from "../../../src/types.js"; import {S3HistoryProvider} from "../../../src/history/s3.js"; @@ -10,7 +10,7 @@ describe("benchmark history S3 paths", () => { const keyPrefix = "myorg/myproject/Linux"; let historyProvider: S3HistoryProvider; - before(() => { + beforeAll(() => { historyProvider = new S3HistoryProvider({Bucket, keyPrefix}); }); @@ -32,7 +32,7 @@ describe("benchmark history S3 paths", () => { }); describe.skip("benchmark history S3", function () { - this.timeout(60 * 1000); + vi.setConfig({testTimeout: 60 * 1000}); const branch = "main"; const benchmark: Benchmark = { @@ -41,7 +41,7 @@ describe.skip("benchmark history S3", function () { }; let historyProvider: S3HistoryProvider; - before(() => { + beforeAll(() => { historyProvider = S3HistoryProvider.fromEnv(); }); @@ -51,7 +51,7 @@ describe.skip("benchmark history S3", function () { it("readLatestInBranch", async function () { const benchRead = await historyProvider.readLatestInBranch(branch); - expect(benchRead).to.deep.equal(benchmark, "Wrong bench read from disk"); + expect(benchRead).toEqual(benchmark); }); it("writeToHistory", async function () { @@ -60,10 +60,10 @@ describe.skip("benchmark history S3", function () { it("readHistory", async function () { const benchmarks = await historyProvider.readHistory(); - expect(benchmarks).to.deep.equal([benchmark], "Wrong history"); + expect(benchmarks).toEqual([benchmark]); }); - after("Delete uploaded artifacts", async () => { + afterAll(async () => { const config = historyProvider["config"]; const s3 = new S3(config); const keys = [ diff --git a/test/unit/utils/file.test.ts b/test/unit/utils/file.test.ts index 6672917..e57fa68 100644 --- a/test/unit/utils/file.test.ts +++ b/test/unit/utils/file.test.ts @@ -1,4 +1,4 @@ -import {expect} from "chai"; +import {expect, describe, it} from "vitest"; import {toCsv, fromCsv} from "../../../src/utils/index.js"; describe("utils / file - csv", () => { @@ -15,13 +15,13 @@ describe("utils / file - csv", () => { `); const dataRev = fromCsv(csv).data; - expect(dataRev).to.deep.equal(data); + expect(dataRev).toEqual(data); }); it("Handle comma in value", () => { const data = [{id: "1,2,3"}]; const csv = toCsv(data); - expect(csv).to.equal(`id + expect(csv).toEqual(`id "1,2,3" `); }); @@ -36,15 +36,15 @@ describe("utils / file - csv", () => { }; const csv = toCsv(data, metadata); - expect(csv).to.equal(`#,commit,4b235978fa5227dae61a6bed6d73461eeb550dac + expect(csv).toEqual(`#,commit,4b235978fa5227dae61a6bed6d73461eeb550dac a,b 1,x 3,y `); const dataRev = fromCsv(csv); - expect(dataRev.data).to.deep.equal(data, "Wrong data"); - expect(dataRev.metadata).to.deep.equal(metadata, "Wrong metadata"); + expect(dataRev.data).toEqual(data); + expect(dataRev.metadata).toEqual(metadata); }); it("Parse CSV with only embedded metadata", () => { @@ -54,11 +54,11 @@ a,b }; const csv = toCsv(data, metadata); - expect(csv).to.equal(`#,commit,4b235978fa5227dae61a6bed6d73461eeb550dac + expect(csv).toEqual(`#,commit,4b235978fa5227dae61a6bed6d73461eeb550dac `); const dataRev = fromCsv(csv); - expect(dataRev.data).to.deep.equal(data, "Wrong data"); - expect(dataRev.metadata).to.deep.equal(metadata, "Wrong metadata"); + expect(dataRev.data).toEqual(data); + expect(dataRev.metadata).toEqual(metadata); }); }); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..475850b --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,36 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import {defineConfig, ViteUserConfig} from "vitest/config"; + +type Runtime = "node" | "deno" | "bun"; + +function getRuntime(): Runtime { + if ("bun" in process.versions) return "bun"; + if ("deno" in process.versions) return "deno"; + + return "node"; +} + +function getPoolOptions(runtime: Runtime): ViteUserConfig["test"] { + if (runtime === "node") { + return { + pool: "threads", + poolOptions: { + threads: { + singleThread: true, + minThreads: 2, + maxThreads: 10, + }, + }, + }; + } + + return { + pool: "vitest-in-process-pool", + }; +} + +export default defineConfig({ + test: { + ...getPoolOptions(getRuntime()), + }, +}); diff --git a/yarn.lock b/yarn.lock index 470b505..8878667 100644 --- a/yarn.lock +++ b/yarn.lock @@ -170,6 +170,121 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" @@ -273,7 +388,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/sourcemap-codec@^1.4.10": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== @@ -418,6 +533,101 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== +"@rollup/rollup-android-arm-eabi@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz#7f4c4d8cd5ccab6e95d6750dbe00321c1f30791e" + integrity sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ== + +"@rollup/rollup-android-arm64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz#17ea71695fb1518c2c324badbe431a0bd1879f2d" + integrity sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA== + +"@rollup/rollup-darwin-arm64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz#dac0f0d0cfa73e7d5225ae6d303c13c8979e7999" + integrity sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ== + +"@rollup/rollup-darwin-x64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz#8f63baa1d31784904a380d2e293fa1ddf53dd4a2" + integrity sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ== + +"@rollup/rollup-freebsd-arm64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz#30ed247e0df6e8858cdc6ae4090e12dbeb8ce946" + integrity sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA== + +"@rollup/rollup-freebsd-x64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz#57846f382fddbb508412ae07855b8a04c8f56282" + integrity sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ== + +"@rollup/rollup-linux-arm-gnueabihf@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz#378ca666c9dae5e6f94d1d351e7497c176e9b6df" + integrity sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA== + +"@rollup/rollup-linux-arm-musleabihf@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz#a692eff3bab330d5c33a5d5813a090c15374cddb" + integrity sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg== + +"@rollup/rollup-linux-arm64-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz#6b1719b76088da5ac1ae1feccf48c5926b9e3db9" + integrity sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA== + +"@rollup/rollup-linux-arm64-musl@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz#865baf5b6f5ff67acb32e5a359508828e8dc5788" + integrity sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A== + +"@rollup/rollup-linux-loongarch64-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz#23c6609ba0f7fa7a7f2038b6b6a08555a5055a87" + integrity sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA== + +"@rollup/rollup-linux-powerpc64le-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz#652ef0d9334a9f25b9daf85731242801cb0fc41c" + integrity sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A== + +"@rollup/rollup-linux-riscv64-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz#1eb6651839ee6ebca64d6cc64febbd299e95e6bd" + integrity sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA== + +"@rollup/rollup-linux-s390x-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz#015c52293afb3ff2a293cf0936b1d43975c1e9cd" + integrity sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg== + +"@rollup/rollup-linux-x64-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz#b83001b5abed2bcb5e2dbeec6a7e69b194235c1e" + integrity sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw== + +"@rollup/rollup-linux-x64-musl@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz#6cc7c84cd4563737f8593e66f33b57d8e228805b" + integrity sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g== + +"@rollup/rollup-win32-arm64-msvc@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz#631ffeee094d71279fcd1fe8072bdcf25311bc11" + integrity sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A== + +"@rollup/rollup-win32-ia32-msvc@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz#06d1d60d5b9f718e8a6c4a43f82e3f9e3254587f" + integrity sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA== + +"@rollup/rollup-win32-x64-msvc@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz#4dff5c4259ebe6c5b4a8f2c5bc3829b7a8447ff0" + integrity sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA== + "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" @@ -443,12 +653,7 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== -"@types/chai@^4.2.19": - version "4.3.20" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.20.tgz#cb291577ed342ca92600430841a00329ba05cecc" - integrity sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ== - -"@types/estree@^1.0.6": +"@types/estree@1.0.6", "@types/estree@^1.0.0", "@types/estree@^1.0.6": version "1.0.6" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== @@ -463,11 +668,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/mocha@^10.0.9": - version "10.0.9" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.9.tgz#101e9da88d2c02e5ac8952982c23b224524d662a" - integrity sha512-sicdRoWtYevwxjOHNMPTl3vSfJM6oyW8o1wXeI7uww6b6xHg8eBznQDNSGBCDJmsE8UMxP05JgZRtsKbTqt//Q== - "@types/node-fetch@^2.5.0": version "2.5.10" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.10.tgz#9b4d4a0425562f9fcea70b12cb3fcdd946ca8132" @@ -481,12 +681,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26" integrity sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA== -"@types/node@^18.15.3": - version "18.19.67" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.67.tgz#77c4b01641a1e3e1509aff7e10d39e4afd5ae06d" - integrity sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ== +"@types/node@^22.10.2": + version "22.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9" + integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ== dependencies: - undici-types "~5.26.4" + undici-types "~6.20.0" "@types/tunnel@^0.0.1": version "0.0.1" @@ -588,6 +788,25 @@ "@typescript-eslint/types" "8.16.0" eslint-visitor-keys "^4.2.0" +"@vitest/expect@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.8.tgz#13fad0e8d5a0bf0feb675dcf1d1f1a36a1773bc1" + integrity sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw== + dependencies: + "@vitest/spy" "2.1.8" + "@vitest/utils" "2.1.8" + chai "^5.1.2" + tinyrainbow "^1.2.0" + +"@vitest/mocker@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.8.tgz#51dec42ac244e949d20009249e033e274e323f73" + integrity sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA== + dependencies: + "@vitest/spy" "2.1.8" + estree-walker "^3.0.3" + magic-string "^0.30.12" + "@vitest/pretty-format@2.1.6": version "2.1.6" resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.6.tgz#9bc642047a3efc637b41492b1f222c43be3822e4" @@ -595,6 +814,21 @@ dependencies: tinyrainbow "^1.2.0" +"@vitest/pretty-format@2.1.8", "@vitest/pretty-format@^2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.8.tgz#88f47726e5d0cf4ba873d50c135b02e4395e2bca" + integrity sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ== + dependencies: + tinyrainbow "^1.2.0" + +"@vitest/runner@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.8.tgz#b0e2dd29ca49c25e9323ea2a45a5125d8729759f" + integrity sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg== + dependencies: + "@vitest/utils" "2.1.8" + pathe "^1.1.2" + "@vitest/runner@^2.1.6": version "2.1.6" resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.6.tgz#948cad2cccfe2e56be5b3f9979cf9a417ca59737" @@ -603,6 +837,22 @@ "@vitest/utils" "2.1.6" pathe "^1.1.2" +"@vitest/snapshot@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.8.tgz#d5dc204f4b95dc8b5e468b455dfc99000047d2de" + integrity sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg== + dependencies: + "@vitest/pretty-format" "2.1.8" + magic-string "^0.30.12" + pathe "^1.1.2" + +"@vitest/spy@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.8.tgz#bc41af3e1e6a41ae3b67e51f09724136b88fa447" + integrity sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg== + dependencies: + tinyspy "^3.0.2" + "@vitest/utils@2.1.6": version "2.1.6" resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.6.tgz#2af6a82c5c45da35ecd322d0568247a6e9c95c5f" @@ -612,6 +862,15 @@ loupe "^3.1.2" tinyrainbow "^1.2.0" +"@vitest/utils@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.8.tgz#f8ef85525f3362ebd37fd25d268745108d6ae388" + integrity sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA== + dependencies: + "@vitest/pretty-format" "2.1.8" + loupe "^3.1.2" + tinyrainbow "^1.2.0" + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -656,11 +915,6 @@ ajv@^8.6.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ansi-colors@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - ansi-regex@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" @@ -688,14 +942,6 @@ ansi-styles@^6.1.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -777,6 +1023,11 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -819,11 +1070,6 @@ before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -846,18 +1092,6 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -browser-stdout@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== - buffer@4.9.2: version "4.9.2" resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" @@ -867,6 +1101,11 @@ buffer@4.9.2: ieee754 "^1.1.4" isarray "^1.0.0" +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -891,11 +1130,6 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^6.0.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" - integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== - chai@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" @@ -909,7 +1143,18 @@ chai@^4.5.0: pathval "^1.1.1" type-detect "^4.1.0" -chalk@^4.0.0, chalk@^4.1.0: +chai@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.2.tgz#3afbc340b994ae3610ca519a6c70ace77ad4378d" + integrity sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw== + dependencies: + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" + +chalk@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== @@ -924,29 +1169,10 @@ check-error@^1.0.3: dependencies: get-func-name "^2.0.2" -chokidar@^3.5.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" +check-error@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" + integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== cliui@^8.0.1: version "8.0.1" @@ -1053,10 +1279,12 @@ debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: dependencies: ms "^2.1.3" -decamelize@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" - integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== +debug@^4.3.7: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" deep-eql@^4.1.3: version "4.1.4" @@ -1065,6 +1293,11 @@ deep-eql@^4.1.3: dependencies: type-detect "^4.0.0" +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== + deep-is@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -1103,11 +1336,6 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -diff@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" - integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -1207,6 +1435,11 @@ es-errors@^1.2.1, es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== +es-module-lexer@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + es-object-atoms@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" @@ -1239,6 +1472,35 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -1403,6 +1665,13 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -1423,6 +1692,11 @@ events@^3.0.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +expect-type@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.1.0.tgz#a146e414250d13dfc49eafcfd1344a4060fa4c75" + integrity sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -1468,13 +1742,6 @@ file-entry-cache@^8.0.0: dependencies: flat-cache "^4.0.0" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -1498,11 +1765,6 @@ flat-cache@^4.0.0: flatted "^3.2.9" keyv "^4.5.4" -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - flatted@^3.2.9: version "3.3.2" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27" @@ -1541,12 +1803,7 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@~2.3.2: +fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -1622,7 +1879,7 @@ get-tsconfig@^4.7.5: dependencies: resolve-pkg-maps "^1.0.0" -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -1648,17 +1905,6 @@ glob@^10.3.7, glob@^10.4.5: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - globals@^14.0.0: version "14.0.0" resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" @@ -1742,11 +1988,6 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: dependencies: function-bind "^1.1.2" -he@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - ieee754@1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" @@ -1775,19 +2016,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - internal-slot@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" @@ -1822,13 +2050,6 @@ is-bigint@^1.0.1: resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a" integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA== -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - is-boolean-object@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.1.tgz#3c0878f035cb821228d350d2e1e36719716a3de8" @@ -1903,7 +2124,7 @@ is-generator-function@^1.0.10: dependencies: has-tostringtag "^1.0.0" -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== @@ -1937,11 +2158,6 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-plain-obj@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - is-plain-object@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" @@ -1993,11 +2209,6 @@ is-typed-array@^1.1.13: dependencies: which-typed-array "^1.1.14" -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - is-unicode-supported@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz#09f0ab0de6d3744d48d265ebb98f65d11f2a9b3a" @@ -2113,14 +2324,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -log-symbols@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - log-symbols@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-7.0.0.tgz#953999bb9cec27a09049c8f45e1154ec81163061" @@ -2136,7 +2339,7 @@ loupe@^2.3.6: dependencies: get-func-name "^2.0.1" -loupe@^3.1.2: +loupe@^3.1.0, loupe@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.2.tgz#c86e0696804a02218f2206124c45d8b15291a240" integrity sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg== @@ -2146,6 +2349,13 @@ lru-cache@10.4.3, lru-cache@^10.2.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== +magic-string@^0.30.12: + version "0.30.15" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.15.tgz#d5474a2c4c5f35f041349edaba8a5cb02733ed3c" + integrity sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" @@ -2156,7 +2366,7 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^4.0.4: +micromatch@^4.0.4, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -2190,13 +2400,6 @@ minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1, minimatch@^5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - minimatch@^9.0.4: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" @@ -2219,32 +2422,6 @@ minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== -mocha@^10.8.2: - version "10.8.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.8.2.tgz#8d8342d016ed411b12a429eb731b825f961afb96" - integrity sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg== - dependencies: - ansi-colors "^4.1.3" - browser-stdout "^1.3.1" - chokidar "^3.5.3" - debug "^4.3.5" - diff "^5.2.0" - escape-string-regexp "^4.0.0" - find-up "^5.0.0" - glob "^8.1.0" - he "^1.2.0" - js-yaml "^4.1.0" - log-symbols "^4.1.0" - minimatch "^5.1.6" - ms "^2.1.3" - serialize-javascript "^6.0.2" - strip-json-comments "^3.1.1" - supports-color "^8.1.1" - workerpool "^6.5.1" - yargs "^16.2.0" - yargs-parser "^20.2.9" - yargs-unparser "^2.0.0" - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -2255,6 +2432,11 @@ ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +nanoid@^3.3.7: + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2265,11 +2447,6 @@ node-fetch@^2.6.0, node-fetch@^2.6.1: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - object-inspect@^1.13.1, object-inspect@^1.13.3: version "1.13.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" @@ -2318,7 +2495,7 @@ object.values@^1.2.0: define-properties "^1.2.1" es-object-atoms "^1.0.0" -once@^1.3.0, once@^1.4.0: +once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -2396,10 +2573,15 @@ pathval@^1.1.1: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== +pathval@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" + integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== picomatch@^2.3.1: version "2.3.1" @@ -2411,6 +2593,15 @@ possible-typed-array-names@^1.0.0: resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== +postcss@^8.4.43: + version "8.4.49" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" + integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.1" + source-map-js "^1.2.1" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -2458,20 +2649,6 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - reflect.getprototypeof@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz#04311b33a1b713ca5eb7b5aed9950a86481858e5" @@ -2536,6 +2713,34 @@ rimraf@^5.0.10: dependencies: glob "^10.3.7" +rollup@^4.20.0: + version "4.28.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.28.1.tgz#7718ba34d62b449dfc49adbfd2f312b4fe0df4de" + integrity sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg== + dependencies: + "@types/estree" "1.0.6" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.28.1" + "@rollup/rollup-android-arm64" "4.28.1" + "@rollup/rollup-darwin-arm64" "4.28.1" + "@rollup/rollup-darwin-x64" "4.28.1" + "@rollup/rollup-freebsd-arm64" "4.28.1" + "@rollup/rollup-freebsd-x64" "4.28.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.28.1" + "@rollup/rollup-linux-arm-musleabihf" "4.28.1" + "@rollup/rollup-linux-arm64-gnu" "4.28.1" + "@rollup/rollup-linux-arm64-musl" "4.28.1" + "@rollup/rollup-linux-loongarch64-gnu" "4.28.1" + "@rollup/rollup-linux-powerpc64le-gnu" "4.28.1" + "@rollup/rollup-linux-riscv64-gnu" "4.28.1" + "@rollup/rollup-linux-s390x-gnu" "4.28.1" + "@rollup/rollup-linux-x64-gnu" "4.28.1" + "@rollup/rollup-linux-x64-musl" "4.28.1" + "@rollup/rollup-win32-arm64-msvc" "4.28.1" + "@rollup/rollup-win32-ia32-msvc" "4.28.1" + "@rollup/rollup-win32-x64-msvc" "4.28.1" + fsevents "~2.3.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -2553,11 +2758,6 @@ safe-array-concat@^1.1.2: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@^5.1.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - safe-regex-test@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" @@ -2592,13 +2792,6 @@ semver@^7.6.0, semver@^7.6.3: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== -serialize-javascript@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" - integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== - dependencies: - randombytes "^2.1.0" - set-function-length@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -2643,11 +2836,31 @@ side-channel@^1.0.4: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + +std-env@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" + integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w== + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -2757,13 +2970,6 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -2782,11 +2988,31 @@ tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.1.tgz#0ab0daf93b43e2c211212396bdb836b468c97c98" + integrity sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ== + +tinypool@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" + integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== + tinyrainbow@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5" integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== +tinyspy@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" + integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -2947,10 +3173,10 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== universal-user-agent@^6.0.0: version "6.0.0" @@ -2997,6 +3223,62 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== +vite-node@2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.8.tgz#9495ca17652f6f7f95ca7c4b568a235e0c8dbac5" + integrity sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg== + dependencies: + cac "^6.7.14" + debug "^4.3.7" + es-module-lexer "^1.5.4" + pathe "^1.1.2" + vite "^5.0.0" + +vite@^5.0.0: + version "5.4.11" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5" + integrity sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" + optionalDependencies: + fsevents "~2.3.3" + +vitest-in-process-pool@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/vitest-in-process-pool/-/vitest-in-process-pool-1.0.0.tgz#f4f140d0648877e69429c0de4f1f867b69666f6c" + integrity sha512-WeXUIcXZ94CGgqGpSk0XeDi5+c5N8b3aRFSJppYYUxF9D8c+qIjGbMf6JGRXc4tJML5Ad5MuvZNgyfkjcXwCGA== + dependencies: + micromatch "^4.0.8" + vitest "^2.1.8" + +vitest@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.8.tgz#2e6a00bc24833574d535c96d6602fb64163092fa" + integrity sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ== + dependencies: + "@vitest/expect" "2.1.8" + "@vitest/mocker" "2.1.8" + "@vitest/pretty-format" "^2.1.8" + "@vitest/runner" "2.1.8" + "@vitest/snapshot" "2.1.8" + "@vitest/spy" "2.1.8" + "@vitest/utils" "2.1.8" + chai "^5.1.2" + debug "^4.3.7" + expect-type "^1.1.0" + magic-string "^0.30.12" + pathe "^1.1.2" + std-env "^3.8.0" + tinybench "^2.9.0" + tinyexec "^0.3.1" + tinypool "^1.0.1" + tinyrainbow "^1.2.0" + vite "^5.0.0" + vite-node "2.1.8" + why-is-node-running "^2.3.0" + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -3055,16 +3337,19 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -workerpool@^6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" - integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -3128,44 +3413,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yargs-parser@^20.2.2: - version "20.2.7" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" - integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== - -yargs-parser@^20.2.9: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs-unparser@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" - integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== - dependencies: - camelcase "^6.0.0" - decamelize "^4.0.0" - flat "^5.0.2" - is-plain-obj "^2.1.0" - -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" From 2343c44eaa268a0ba43d5864dfa1ffd34812ca80 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 6 Jan 2025 11:50:02 +0100 Subject: [PATCH 3/8] Move bin to esm module --- bin/index.cjs | 3 --- bin/index.js | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) delete mode 100755 bin/index.cjs create mode 100755 bin/index.js diff --git a/bin/index.cjs b/bin/index.cjs deleted file mode 100755 index 8b259be..0000000 --- a/bin/index.cjs +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node - -require("../lib/cjs/cli/cli.js"); diff --git a/bin/index.js b/bin/index.js new file mode 100755 index 0000000..72f357b --- /dev/null +++ b/bin/index.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require("../lib/esm/cli/cli.js"); From b31cfe68a483e631c294ad82916e974763eab7d8 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 6 Jan 2025 11:50:19 +0100 Subject: [PATCH 4/8] Add cli-table3 package dependency --- package.json | 3 ++- yarn.lock | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b4c4b79..9700ed6 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "dapplion <35266934+dapplion@users.noreply.github.com>", "license": "MIT", "bin": { - "benchmark": "./bin/index.cjs" + "benchmark": "./bin/index.js" }, "files": [ "lib/**/*.d.ts", @@ -61,6 +61,7 @@ "@vitest/runner": "^2.1.6", "ajv": "^8.6.0", "aws-sdk": "^2.932.0", + "cli-table3": "^0.6.5", "csv-parse": "^4.16.0", "csv-stringify": "^5.6.2", "glob": "^10.4.5", diff --git a/yarn.lock b/yarn.lock index 8878667..048d862 100644 --- a/yarn.lock +++ b/yarn.lock @@ -163,6 +163,11 @@ events "^3.0.0" tslib "^2.0.0" +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -1174,6 +1179,15 @@ check-error@^2.1.1: resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== +cli-table3@^0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" + integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" From c4f28d148bf21484976fe08b29bff7c751ff528c Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 6 Jan 2025 11:50:37 +0100 Subject: [PATCH 5/8] Add CLI command to compare benchmarks --- src/cli/cli.ts | 19 +++++++++-- src/cli/compare.ts | 42 ++++++++++++++++++++++++ src/cli/run.ts | 4 +-- src/compare/compute.ts | 72 ++++++++++++++++++++++++++++++++++++++--- src/github/comment.ts | 4 +-- src/history/gaCache.ts | 2 -- src/history/local.ts | 2 +- src/types.ts | 42 ++++++++++++++++++------ src/utils/render.ts | 73 +++++++++++++++++++++++++++++++++++++++--- 9 files changed, 233 insertions(+), 27 deletions(-) create mode 100644 src/cli/compare.ts diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 87e865b..e5d9a59 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -4,6 +4,7 @@ import {hideBin} from "yargs/helpers"; import {benchmarkOptions, CLIOptions, fileCollectionOptions, storageOptions} from "./options.js"; import {run} from "./run.js"; +import {compare} from "./compare.js"; void yargs(hideBin(process.argv)) .env("BENCHMARK") @@ -11,13 +12,28 @@ void yargs(hideBin(process.argv)) .command({ command: ["$0 [spec..]", "inspect"], describe: "Run benchmarks", + builder: function (yar) { + return yar.options({...fileCollectionOptions, ...storageOptions, ...benchmarkOptions}); + }, handler: async (argv) => { const cliOpts = {...argv} as unknown as CLIOptions & {spec: string[]}; await run(cliOpts); }, }) + .command({ + command: "compare ", + aliases: ["cmp"], + describe: "Compare multiple benchmark outputs", + builder: function (yar) { + return yar.option("dir", {type: "string", array: true, normalize: true, desc: "List of directories to compare"}); + }, + handler: async (argv) => { + const cliOpts = {...argv} as unknown as {dirs: string[]}; + await compare(cliOpts); + }, + }) .parserConfiguration({ // As of yargs v16.1.0 dot-notation breaks strictOptions() // Manually processing options is typesafe tho more verbose @@ -26,7 +42,6 @@ void yargs(hideBin(process.argv)) "short-option-groups": false, "strip-aliased": true, }) - .options({...fileCollectionOptions, ...storageOptions, ...benchmarkOptions}) .usage( `Benchmark runner to track performance. @@ -38,11 +53,11 @@ void yargs(hideBin(process.argv)) // .alias("h", "help") // .alias("v", "version") .recommendCommands() + .showHelpOnFail(true) .fail((msg, err) => { if (msg) { // Show command help message when no command is provided if (msg.includes("Not enough non-option arguments")) { - yargs.showHelp(); // eslint-disable-next-line no-console console.log("\n"); } diff --git a/src/cli/compare.ts b/src/cli/compare.ts new file mode 100644 index 0000000..f7ddff6 --- /dev/null +++ b/src/cli/compare.ts @@ -0,0 +1,42 @@ +import path from "node:path"; +import fs from "node:fs"; +import {LocalHistoryProvider} from "../history/local.js"; +import {consoleLog} from "../utils/output.js"; +import {compareBenchmarks} from "../compare/compute.js"; +import {renderBenchmarkComparisonTable} from "../utils/render.js"; +// import {isGaRun} from "../github/context.js"; +// import {postGaComment} from "../github/comment.js"; + +export async function compare({dirs}: {dirs: string[]}): Promise { + consoleLog("Comparing benchmarks:"); + for (const dir of dirs) { + consoleLog(`- ${dir}`); + } + + const benchmarks = []; + + for (const dir of dirs) { + const dirPath = path.resolve(dir); + if (!fs.existsSync(dirPath)) { + throw Error(`Benchmark directory ${dirPath} does not exits`); + } + const provider = new LocalHistoryProvider(dirPath); + try { + const history = await provider.readHistory(); + if (history.length === 0) { + throw Error(`Benchmark directory ${dirPath} does not contain nay history.`); + } + benchmarks.push(history[0]); + } catch { + throw Error(`Benchmark directory ${dirPath} does not contain nay history. Or not a valid benchmark.`); + } + } + + const resultsComp = compareBenchmarks(benchmarks); + + consoleLog(renderBenchmarkComparisonTable(resultsComp)); + + // if (isGaRun()) { + // await postGaComment(resultsComp); + // } +} diff --git a/src/cli/run.ts b/src/cli/run.ts index 7a9ef35..951dc4f 100644 --- a/src/cli/run.ts +++ b/src/cli/run.ts @@ -5,7 +5,7 @@ import {validateBenchmark} from "../history/schema.js"; import {Benchmark, BenchmarkOpts, FileCollectionOptions, StorageOptions} from "../types.js"; import {renderCompareWith, resolveCompareWith, resolvePrevBenchmark} from "../compare/index.js"; import {parseBranchFromRef, getCurrentCommitInfo, shell, getCurrentBranch, collectFiles} from "../utils/index.js"; -import {computeBenchComparision} from "../compare/compute.js"; +import {computeBenchComparison} from "../compare/compute.js"; import {postGaComment} from "../github/comment.js"; import {isGaRun} from "../github/context.js"; import {BenchmarkRunner} from "../benchmark/runner.js"; @@ -69,7 +69,7 @@ export async function run(opts_: FileCollectionOptions & StorageOptions & Benchm await historyProvider.writeToHistory(currBench); } - const resultsComp = computeBenchComparision(currBench, prevBench, opts.threshold); + const resultsComp = computeBenchComparison(currBench, prevBench, opts.threshold); if (!opts.skipPostComment && isGaRun()) { await postGaComment(resultsComp); diff --git a/src/compare/compute.ts b/src/compare/compute.ts index 32513ad..82a2f8a 100644 --- a/src/compare/compute.ts +++ b/src/compare/compute.ts @@ -1,10 +1,74 @@ -import {ResultComparison, BenchmarkComparison, Benchmark, BenchmarkResult} from "../types.js"; +import { + ResultSelfComparison, + BenchmarkSelfComparison, + Benchmark, + BenchmarkResult, + BenchmarkCrossComparison, + ResultCrossComparison, +} from "../types.js"; -export function computeBenchComparision( +export function compareBenchmarks(benchmarks: Benchmark[]): BenchmarkCrossComparison { + const originBenchmark = benchmarks[0]; + const targetBenchmarks = benchmarks.slice(1); + + const results = new Map(); + for (const res of originBenchmark.results) { + results.set(res.id, [ + {...res, targetAverageNs: null, originAverageNs: res.averageNs, isFailed: false, isImproved: false, ratio: 1.0}, + ]); + } + + let someFailed = false; + + for (const bench of targetBenchmarks) { + for (const currBench of bench.results) { + const {id} = currBench; + const result = results.get(id); + if (!result) continue; + + const refBench = result[0]; + const thresholdBench = currBench.threshold ?? 0; + + if (refBench && refBench.originAverageNs) { + const ratio = currBench.averageNs / refBench.originAverageNs; + const isFailed = ratio > thresholdBench; + result.push({ + id, + targetAverageNs: currBench.averageNs, + originAverageNs: refBench.originAverageNs, + ratio, + isFailed: isFailed, + isImproved: ratio < 1 / thresholdBench, + }); + if (!someFailed && isFailed) { + someFailed = true; + } + } else { + result.push({ + id, + targetAverageNs: currBench.averageNs, + originAverageNs: null, + ratio: null, + isFailed: false, + isImproved: false, + }); + } + } + } + + return { + commitsShas: benchmarks.map((b) => b.commitSha), + dirNames: benchmarks.map((b) => b.dirName ?? ""), + someFailed, + results, + }; +} + +export function computeBenchComparison( currBench: Benchmark, prevBench: Benchmark | null, threshold: number -): BenchmarkComparison { +): BenchmarkSelfComparison { const prevResults = new Map(); if (prevBench) { for (const bench of prevBench.results) { @@ -12,7 +76,7 @@ export function computeBenchComparision( } } - const results = currBench.results.map((currBench): ResultComparison => { + const results = currBench.results.map((currBench): ResultSelfComparison => { const {id} = currBench; const prevBench = prevResults.get(id); const thresholdBench = currBench.threshold ?? threshold; diff --git a/src/github/comment.ts b/src/github/comment.ts index 5eb04ad..a2f4b2d 100644 --- a/src/github/comment.ts +++ b/src/github/comment.ts @@ -1,9 +1,9 @@ import * as github from "@actions/github"; -import {BenchmarkComparison} from "../types.js"; +import {BenchmarkSelfComparison} from "../types.js"; import {commetToPrUpdatable, commentToCommit} from "./octokit.js"; import {getGithubEventData, GithubActionsEventData, renderComment} from "../utils/index.js"; -export async function postGaComment(resultsComp: BenchmarkComparison): Promise { +export async function postGaComment(resultsComp: BenchmarkSelfComparison): Promise { switch (github.context.eventName) { case "pull_request": { const eventData = getGithubEventData(); diff --git a/src/history/gaCache.ts b/src/history/gaCache.ts index 26bcf6c..6703b9a 100644 --- a/src/history/gaCache.ts +++ b/src/history/gaCache.ts @@ -63,8 +63,6 @@ class GaCacheHistoryProvider extends LocalHistoryProvider implements IHistoryPro } private async initialize(): Promise { - // eslint-disable-next-line no-console - console.log("ENV", process.env); if (this.initializePromise === null) { this.initializePromise = cache.restoreCache([this.tmpDir], this.cacheKey); } diff --git a/src/history/local.ts b/src/history/local.ts index 73cf942..c2b627a 100644 --- a/src/history/local.ts +++ b/src/history/local.ts @@ -87,7 +87,7 @@ export class LocalHistoryProvider implements IHistoryProvider { const str = fs.readFileSync(filepath, "utf8"); const {data, metadata} = fromCsv(str); const csvMeta = metadata as unknown as CsvMeta; - return {commitSha: csvMeta.commit, results: data}; + return {commitSha: csvMeta.commit, dirName: path.basename(path.dirname(this.getHistoryDirpath())), results: data}; } /** Write result to CSV + metadata as Embedded Metadata */ diff --git a/src/types.ts b/src/types.ts index 7b35d8a..b083588 100644 --- a/src/types.ts +++ b/src/types.ts @@ -64,8 +64,11 @@ export type BenchmarkOpts = { // Create partial only for specific keys export type PartialBy = Omit & Partial>; +type BenchId = string; +type CommitSha = string; + export type BenchmarkRunOptsWithFn = BenchmarkOpts & { - id: string; + id: BenchId; fn: (arg: T) => void | Promise; before?: () => T2 | Promise; beforeEach?: (arg: T2, i: number) => T | Promise; @@ -75,7 +78,7 @@ export interface BenchFuncApi { (opts: BenchmarkRunOptsWithFn): void; (idOrOpts: string | Omit, "fn">, fn: (arg: T) => void): void; ( - idOrOpts: string | PartialBy, "fn">, + idOrOpts: BenchId | PartialBy, "fn">, fn?: (arg: T) => void | Promise ): void; } @@ -89,7 +92,7 @@ export type BenchmarkResults = BenchmarkResult[]; /** Time results for a single benchmark item */ export type BenchmarkResult = { - id: string; + id: BenchId; averageNs: number; runsDone: number; totalMs: number; @@ -99,7 +102,8 @@ export type BenchmarkResult = { /** Time results for a single benchmark (all items) */ export type Benchmark = { - commitSha: string; + commitSha: CommitSha; + dirName?: string; results: BenchmarkResults; }; @@ -110,18 +114,36 @@ export type BenchmarkHistory = { }; }; -export type BenchmarkComparison = { - currCommitSha: string; - prevCommitSha: string | null; +export type BenchmarkSelfComparison = { + currCommitSha: CommitSha; + prevCommitSha: CommitSha | null; someFailed: boolean; - results: ResultComparison[]; + results: ResultSelfComparison[]; }; -export type ResultComparison = { - id: string; +export type ResultSelfComparison = { + id: BenchId; currAverageNs: number; prevAverageNs: number | null; ratio: number | null; isFailed: boolean; isImproved: boolean; }; + +export type BenchmarkCrossComparison = { + someFailed: boolean; + // The first element will always contain origin which is used to compare + commitsShas: (CommitSha | null)[]; + dirNames: string[]; + // The result array contains the origin commit first and then targets + results: Map; +}; + +export type ResultCrossComparison = { + id: BenchId; + originAverageNs: number | null; + targetAverageNs: number | null; + ratio: number | null; + isFailed: boolean; + isImproved: boolean; +}; diff --git a/src/utils/render.ts b/src/utils/render.ts index d287b5e..46d4d4d 100644 --- a/src/utils/render.ts +++ b/src/utils/render.ts @@ -1,8 +1,9 @@ -import {BenchmarkComparison, ResultComparison} from "../types.js"; +import CliTable, {Table, CellValue, CellOptions} from "cli-table3"; +import {BenchmarkCrossComparison, BenchmarkSelfComparison, ResultSelfComparison} from "../types.js"; -type CommitsSha = Pick; +type CommitsSha = Pick; -export function renderComment(benchComp: BenchmarkComparison): string { +export function renderComment(benchComp: BenchmarkSelfComparison): string { const isFailedResults = benchComp.results.filter((r) => r.isFailed); const isImprovedResults = benchComp.results.filter((r) => r.isImproved); @@ -42,7 +43,7 @@ ${renderBenchmarkTable(benchComp.results, benchComp)} `; } -function renderBenchmarkTable(benchComp: ResultComparison[], {currCommitSha, prevCommitSha}: CommitsSha): string { +function renderBenchmarkTable(benchComp: ResultSelfComparison[], {currCommitSha, prevCommitSha}: CommitsSha): string { function toRow(arr: (number | string)[]): string { // Don't surround string items with \`, it doesn't look great rendered in Github comments const row = arr.map((e) => `${e}`).join(" | "); @@ -65,6 +66,70 @@ ${rows.join("\n")} `; } +export function toMarkdownTable(table: Table): string { + const chars = { + middle: "|", + + mid: " ", + "mid-mid": "", + + right: "|", + "right-mid": " ", + + top: "", + "top-left": "", + "top-right": "", + "top-mid": "", + + left: "|", + "left-mid": " ", + + bottom: "-", + "bottom-left": "|", + "bottom-right": "|", + "bottom-mid": "-", + }; + table.options.chars = chars; + + return table.toString(); +} + +export function renderBenchmarkComparisonTable(benchComp: BenchmarkCrossComparison): string { + const keys = [...benchComp.results.keys()]; + const benchmarkSize = benchComp.commitsShas.length; + + const heads: (CellValue | CellOptions)[] = [{rowSpan: 2, content: "Benchmark suite"}, benchComp.dirNames[0]]; + const secondaryHead = ["Average Ns"]; + + for (let s = 1; s < benchmarkSize; s++) { + heads.push({colSpan: 2, content: benchComp.dirNames[s]}); + secondaryHead.push(...["Average Ns", "Ratio"]); + } + + const table = new CliTable({ + head: [], + }); + table.push(heads); + table.push(secondaryHead); + + for (const id of keys) { + const row = [id]; + + for (const [index, res] of (benchComp.results.get(id) ?? []).entries()) { + if (index === 0) { + row.push(prettyTimeStr(res.originAverageNs ?? 0)); + } else { + row.push(prettyTimeStr(res.targetAverageNs ?? 0)); + row.push((res.ratio ?? 1).toFixed(2)); + } + } + + table.push(row); + } + + return table.toString(); +} + function prettyTimeStr(nanoSec: number): string { const [value, unit] = prettyTime(nanoSec); return `${value.toPrecision(5)} ${unit}/op`; From 5bc341b4e31c550b616b2cf60ccfde7cd0d1603e Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 6 Jan 2025 12:14:29 +0100 Subject: [PATCH 6/8] Add a new workflow to compare performance --- .github/workflows/benchmark-compare.yml | 116 ++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 .github/workflows/benchmark-compare.yml diff --git a/.github/workflows/benchmark-compare.yml b/.github/workflows/benchmark-compare.yml new file mode 100644 index 0000000..267539c --- /dev/null +++ b/.github/workflows/benchmark-compare.yml @@ -0,0 +1,116 @@ +name: Benchmark + +# only one can tun at a time. +# Actions access a common cache entry and may corrupt it. +concurrency: cd-benchmark-compare-${{ github.ref }} + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + nodejs: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + # Get the entire git history to walk commit history on head branch + fetch-depth: 0 + # Do not checkout merge commit + ref: ${{ github.event.pull_request.head.sha }} + + - uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Install + run: yarn install --frozen-lockfile + + - name: Run performance tests on node + run: yarn benchmark --local ./nodejs --noThrow ${{ github.event_name == 'push' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Archive nodejs benchmark artifacts + uses: actions/upload-artifact@v4 + with: + name: nodejs-benchmark + path: nodejs + + bun: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install Dependencies + run: bun install + + - name: Run performance tests on bun + run: bun run --bun benchmark --local ./bun --noThrow ${{ github.event_name == 'push' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Archive bun benchmark artifacts + uses: actions/upload-artifact@v4 + with: + name: bun-benchmark + path: bun + + deno: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Install Dependencies + run: deno install + + - name: Run performance tests on bun + run: deno run -A benchmark --local ./deno --noThrow ${{ github.event_name == 'push' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Archive deno benchmark artifacts + uses: actions/upload-artifact@v4 + with: + name: deno-benchmark + path: deno + + compare: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Install + run: yarn install --frozen-lockfile + + - name: Download all benchmark artifacts + uses: actions/download-artifact@v4 + + - name: Run comparison + run: node --loader ts-node/esm ./src/cli/cli.ts cmp nodejs-benchmark deno-benchmark bun-benchmark From 7c4abab040bde731aba9554d2da0e7aa523b6a3f Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 6 Jan 2025 12:18:08 +0100 Subject: [PATCH 7/8] Fix unit tests --- test/unit/history/local.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/history/local.test.ts b/test/unit/history/local.test.ts index 35cacbd..4c1e022 100644 --- a/test/unit/history/local.test.ts +++ b/test/unit/history/local.test.ts @@ -5,13 +5,14 @@ import {Benchmark} from "../../../src/types.js"; import {LocalHistoryProvider} from "../../../src/history/local.js"; describe("benchmark history local", () => { + const testDir = fs.mkdtempSync("test_files_"); const branch = "main"; const benchmark: Benchmark = { commitSha: "010101010101010101010101", + dirName: testDir, results: [{id: "for loop", averageNs: 16573, runsDone: 1024, totalMs: 465, threshold: 2}], }; - const testDir = fs.mkdtempSync("test_files_"); const historyProvider = new LocalHistoryProvider(testDir); afterAll(() => { From c368d96b427c1eb2e371d0a431923527e6175aaa Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 6 Jan 2025 12:23:35 +0100 Subject: [PATCH 8/8] Update the workflow --- .github/workflows/benchmark-compare.yml | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/.github/workflows/benchmark-compare.yml b/.github/workflows/benchmark-compare.yml index 267539c..ddb9c13 100644 --- a/.github/workflows/benchmark-compare.yml +++ b/.github/workflows/benchmark-compare.yml @@ -13,20 +13,14 @@ on: - main jobs: - nodejs: + benchmark-nodejs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - # Get the entire git history to walk commit history on head branch - fetch-depth: 0 - # Do not checkout merge commit - ref: ${{ github.event.pull_request.head.sha }} - - uses: actions/setup-node@v4 with: - node-version: "18" + node-version: "22" - name: Install run: yarn install --frozen-lockfile @@ -42,14 +36,10 @@ jobs: name: nodejs-benchmark path: nodejs - bun: + benchmark-bun: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - uses: oven-sh/setup-bun@v2 with: bun-version: latest @@ -68,14 +58,10 @@ jobs: name: bun-benchmark path: bun - deno: + benchmark-deno: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - uses: denoland/setup-deno@v2 with: deno-version: v2.x @@ -94,7 +80,7 @@ jobs: name: deno-benchmark path: deno - compare: + benchmark-compare: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4