Skip to content

Commit 67439b6

Browse files
--wip-- [skip ci]
1 parent 1999ec9 commit 67439b6

17 files changed

Lines changed: 922 additions & 360 deletions

packages/core/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
"homepage": "https://codspeed.io",
3333
"license": "Apache-2.0",
3434
"devDependencies": {
35-
"@types/find-up": "^4.0.0",
3635
"@types/stack-trace": "^0.0.30",
3736
"node-addon-api": "^5.1.0",
3837
"node-gyp": "^12.2.0",
@@ -41,7 +40,6 @@
4140
},
4241
"dependencies": {
4342
"axios": "^1.4.0",
44-
"find-up": "^6.3.0",
4543
"form-data": "^4.0.4",
4644
"node-gyp-build": "^4.6.0",
4745
"stack-trace": "1.0.0-pre2"

packages/core/src/utils.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
1-
import { Options as FindupOptions, findUpSync } from "find-up";
2-
import path, { dirname } from "path";
1+
import { existsSync, statSync } from "fs";
2+
import path, { dirname, join } from "path";
33
import { get as getStackTrace } from "stack-trace";
44
import { fileURLToPath } from "url";
55

6-
export function getGitDir(path: string): string | undefined {
7-
const dotGitPath = findUpSync(".git", {
8-
cwd: path,
9-
type: "directory",
10-
} as FindupOptions);
11-
return dotGitPath ? dirname(dotGitPath) : undefined;
6+
export function getGitDir(fromPath: string): string | undefined {
7+
// Walk up from the starting path looking for the first ancestor that contains
8+
// a `.git` entry, accepting it whether it's a directory (regular checkout) or
9+
// a file (a gitlink, as used by git worktrees and submodules). Restricting to
10+
// directories would miss those layouts and either fail or wrongly resolve to a
11+
// parent repository.
12+
let current = directoryOf(fromPath);
13+
for (;;) {
14+
if (existsSync(join(current, ".git"))) {
15+
return current;
16+
}
17+
const parent = dirname(current);
18+
if (parent === current) {
19+
return undefined;
20+
}
21+
current = parent;
22+
}
23+
}
24+
25+
/** The starting directory for the walk: `p` itself if it's a directory, else its parent. */
26+
function directoryOf(p: string): string {
27+
try {
28+
return statSync(p).isDirectory() ? p : dirname(p);
29+
} catch {
30+
// Path doesn't exist (e.g. a not-yet-written file): treat it as a file and
31+
// start from its containing directory.
32+
return dirname(p);
33+
}
1234
}
1335

1436
/**

packages/vitest-plugin/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@
4141
"peerDependencies": {
4242
"tinybench": ">=2.9.0",
4343
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
44-
"vitest": "^3.2 || ^4"
44+
"vitest": "^3.2 || ^4 || ^5.0.0-beta"
4545
},
4646
"devDependencies": {
4747
"@total-typescript/shoehorn": "^0.1.1",
4848
"execa": "^8.0.1",
4949
"tinybench": "^2.9.0",
5050
"vite": "^7.0.0",
51-
"vitest": "^4.0.18"
51+
"vitest": "5.0.0-beta.5"
5252
}
5353
}

packages/vitest-plugin/rollup.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,10 @@ export default defineConfig([
3232
plugins: jsPlugins(pkg.version),
3333
external: ["@codspeed/core", /^vitest/],
3434
},
35+
{
36+
input: "src/v5/setup.ts",
37+
output: { file: "dist/v5-setup.mjs", format: "es" },
38+
plugins: jsPlugins(pkg.version),
39+
external: ["@codspeed/core", /^vitest/, "tinybench"],
40+
},
3541
]);

packages/vitest-plugin/src/__tests__/index.test.ts

Lines changed: 96 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@ vi.mock("fs", () => {
4343

4444
console.warn = vi.fn();
4545

46+
const EXPECTED_EXEC_ARGV = [
47+
"--interpreted-frames-native-stack",
48+
"--allow-natives-syntax",
49+
"--hash-seed=1",
50+
"--random-seed=1",
51+
"--no-opt",
52+
"--predictable",
53+
"--predictable-gc-schedule",
54+
"--expose-gc",
55+
"--no-concurrent-sweeping",
56+
"--max-old-space-size=4096",
57+
];
58+
4659
describe("codSpeedPlugin", () => {
4760
beforeAll(() => {
4861
// Set environment variables to trigger instrumented mode
@@ -54,6 +67,7 @@ describe("codSpeedPlugin", () => {
5467
// Clean up environment variables
5568
delete process.env.CODSPEED_ENV;
5669
delete process.env.CODSPEED_RUNNER_MODE;
70+
fsMocks.setMockVersion("4.0.18");
5771
});
5872

5973
it("should have a name", async () => {
@@ -65,7 +79,9 @@ describe("codSpeedPlugin", () => {
6579
});
6680

6781
describe("apply", () => {
68-
it("should not apply the plugin when the mode is not benchmark", async () => {
82+
it("should not apply the plugin when the mode is not benchmark (v3/v4)", async () => {
83+
fsMocks.setMockVersion("4.0.18");
84+
6985
const applyPlugin = applyPluginFunction(
7086
{},
7187
fromPartial({ mode: "test" }),
@@ -74,7 +90,8 @@ describe("codSpeedPlugin", () => {
7490
expect(applyPlugin).toBe(false);
7591
});
7692

77-
it("should apply the plugin when there is no instrumentation", async () => {
93+
it("should apply the plugin when there is no instrumentation (v3/v4)", async () => {
94+
fsMocks.setMockVersion("4.0.18");
7895
coreMocks.InstrumentHooks.isInstrumented.mockReturnValue(false);
7996

8097
const applyPlugin = applyPluginFunction(
@@ -88,7 +105,8 @@ describe("codSpeedPlugin", () => {
88105
expect(applyPlugin).toBe(true);
89106
});
90107

91-
it("should apply the plugin when there is instrumentation", async () => {
108+
it("should apply the plugin when there is instrumentation (v3/v4)", async () => {
109+
fsMocks.setMockVersion("4.0.18");
92110
coreMocks.InstrumentHooks.isInstrumented.mockReturnValue(true);
93111

94112
const applyPlugin = applyPluginFunction(
@@ -98,33 +116,40 @@ describe("codSpeedPlugin", () => {
98116

99117
expect(applyPlugin).toBe(true);
100118
});
119+
120+
it("should stay active regardless of mode on v5 (benchmark gating happens in config)", async () => {
121+
fsMocks.setMockVersion("5.0.0-beta.5");
122+
coreMocks.InstrumentHooks.isInstrumented.mockReturnValue(true);
123+
124+
const applyPlugin = applyPluginFunction(
125+
{},
126+
fromPartial({ mode: "test" }),
127+
);
128+
129+
expect(applyPlugin).toBe(true);
130+
fsMocks.setMockVersion("4.0.18");
131+
});
101132
});
102133

103134
it("should apply the codspeed config for v4", () => {
135+
fsMocks.setMockVersion("4.0.18");
104136
const config = resolvedCodSpeedPlugin.config;
105137
if (typeof config !== "function")
106138
throw new Error("config is not a function");
107139

108-
const result = config.call({} as never, {}, fromPartial({}));
140+
const result = config.call(
141+
{} as never,
142+
{},
143+
fromPartial({ mode: "benchmark" }),
144+
);
109145

110146
expect(result).toStrictEqual({
111147
test: {
112148
globalSetup: [
113149
expect.stringContaining("packages/vitest-plugin/src/globalSetup.ts"),
114150
],
115151
pool: "forks",
116-
execArgv: [
117-
"--interpreted-frames-native-stack",
118-
"--allow-natives-syntax",
119-
"--hash-seed=1",
120-
"--random-seed=1",
121-
"--no-opt",
122-
"--predictable",
123-
"--predictable-gc-schedule",
124-
"--expose-gc",
125-
"--no-concurrent-sweeping",
126-
"--max-old-space-size=4096",
127-
],
152+
execArgv: EXPECTED_EXEC_ARGV,
128153
runner: expect.stringContaining(
129154
"packages/vitest-plugin/src/analysis.ts",
130155
),
@@ -133,16 +158,18 @@ describe("codSpeedPlugin", () => {
133158
});
134159

135160
it("should apply the codspeed config for v3 with poolOptions", () => {
136-
// Set mock version to v3
137161
fsMocks.setMockVersion("3.2.0");
138162

139-
// Create a new plugin instance to pick up the mocked version
140163
const v3Plugin = codspeedPlugin();
141164
const config = v3Plugin.config;
142165
if (typeof config !== "function")
143166
throw new Error("config is not a function");
144167

145-
const result = config.call({} as never, {}, fromPartial({}));
168+
const result = config.call(
169+
{} as never,
170+
{},
171+
fromPartial({ mode: "benchmark" }),
172+
);
146173

147174
expect(result).toStrictEqual({
148175
test: {
@@ -152,18 +179,7 @@ describe("codSpeedPlugin", () => {
152179
pool: "forks",
153180
poolOptions: {
154181
forks: {
155-
execArgv: [
156-
"--interpreted-frames-native-stack",
157-
"--allow-natives-syntax",
158-
"--hash-seed=1",
159-
"--random-seed=1",
160-
"--no-opt",
161-
"--predictable",
162-
"--predictable-gc-schedule",
163-
"--expose-gc",
164-
"--no-concurrent-sweeping",
165-
"--max-old-space-size=4096",
166-
],
182+
execArgv: EXPECTED_EXEC_ARGV,
167183
},
168184
},
169185
runner: expect.stringContaining(
@@ -172,7 +188,55 @@ describe("codSpeedPlugin", () => {
172188
},
173189
});
174190

175-
// Reset mock version back to v4
176191
fsMocks.setMockVersion("4.0.18");
177192
});
193+
194+
describe("v5 config", () => {
195+
it("should not inject config when benchmarks are not enabled", () => {
196+
fsMocks.setMockVersion("5.0.0-beta.5");
197+
const v5Plugin = codspeedPlugin();
198+
const config = v5Plugin.config;
199+
if (typeof config !== "function")
200+
throw new Error("config is not a function");
201+
202+
const result = config.call({} as never, {}, fromPartial({ mode: "test" }));
203+
204+
expect(result).toBeUndefined();
205+
fsMocks.setMockVersion("4.0.18");
206+
});
207+
208+
it("should inject the v5 setup file (not a runner) when benchmarks are enabled", () => {
209+
fsMocks.setMockVersion("5.0.0-beta.5");
210+
const v5Plugin = codspeedPlugin();
211+
const config = v5Plugin.config;
212+
if (typeof config !== "function")
213+
throw new Error("config is not a function");
214+
215+
const result = config.call(
216+
{} as never,
217+
// `benchmark.enabled` is a Vitest 5 config field the v3/4 typings (which
218+
// this file may be compiled against) don't expose.
219+
{ test: { benchmark: { enabled: true } } } as never,
220+
fromPartial({ mode: "test" }),
221+
);
222+
223+
expect(result).toStrictEqual({
224+
test: {
225+
globalSetup: [
226+
expect.stringContaining(
227+
"packages/vitest-plugin/src/globalSetup.ts",
228+
),
229+
],
230+
pool: "forks",
231+
execArgv: EXPECTED_EXEC_ARGV,
232+
setupFiles: [
233+
expect.stringContaining("packages/vitest-plugin/src/v5/setup.ts"),
234+
],
235+
},
236+
});
237+
// The v5 path must not set a custom runner.
238+
expect((result as { test?: { runner?: unknown } })?.test?.runner).toBeUndefined();
239+
fsMocks.setMockVersion("4.0.18");
240+
});
241+
});
178242
});

packages/vitest-plugin/src/__tests__/instrumented.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { fromPartial } from "@total-typescript/shoehorn";
22
import { describe, expect, it, vi, type RunnerTestSuite } from "vitest";
3+
// `vitest/suite` only exists on Vitest 3/4; this file is excluded from the test
4+
// run under v5+ (see vitest.config.ts).
5+
// eslint-disable-next-line import/no-unresolved
36
import { getBenchFn } from "vitest/suite";
47
import { AnalysisRunner as CodSpeedRunner } from "../analysis";
58

9+
// The legacy AnalysisRunner targets the Vitest 3/4 benchmark backend
10+
// (`NodeBenchmarkRunner`, `vitest/suite`), which Vitest 5 removed. This whole
11+
// file is excluded from the test run under v5+ (see vitest.config.ts); the v5
12+
// path is covered separately.
13+
614
const coreMocks = vi.hoisted(() => {
715
return {
816
InstrumentHooks: {

packages/vitest-plugin/src/analysis.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
wrapWithRootFrame,
99
} from "@codspeed/core";
1010
import { Benchmark, type RunnerTestSuite } from "vitest";
11+
// `vitest/runners` and `vitest/suite` only exist on Vitest 3/4; this runner is
12+
// loaded only there.
13+
// eslint-disable-next-line import/no-unresolved
1114
import { NodeBenchmarkRunner } from "vitest/runners";
15+
// eslint-disable-next-line import/no-unresolved
1216
import { getBenchFn } from "vitest/suite";
1317
import {
1418
callSuiteHook,
@@ -36,11 +40,14 @@ async function runAnalysisBench(
3640
currentSuiteName: string,
3741
) {
3842
const uri = `${currentSuiteName}::${benchmark.name}`;
39-
const fn = getBenchFn(benchmark);
43+
// tinybench's bench fn carries a `this: Bench` requirement on Vitest 3/4 that
44+
// we don't need (the work under test is self-contained); call it as a plain
45+
// parameterless function. The cast also smooths over the typing differences
46+
// across supported Vitest versions.
47+
const fn = getBenchFn(benchmark) as () => unknown;
4048

4149
await optimizeFunction(async () => {
4250
await callSuiteHook(suite, benchmark, "beforeEach");
43-
// @ts-expect-error we do not need to bind the function to an instance of tinybench's Bench
4451
await fn();
4552
await callSuiteHook(suite, benchmark, "afterEach");
4653
});
@@ -50,7 +57,6 @@ async function runAnalysisBench(
5057
global.gc?.();
5158
await wrapWithRootFrame(async () => {
5259
InstrumentHooks.startBenchmark();
53-
// @ts-expect-error we do not need to bind the function to an instance of tinybench's Bench
5460
await fn();
5561
InstrumentHooks.stopBenchmark();
5662
InstrumentHooks.setExecutedBenchmark(process.pid, uri);

packages/vitest-plugin/src/common.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { getGitDir } from "@codspeed/core";
22
import path from "path";
33
import { Benchmark, type RunnerTask, type RunnerTestSuite } from "vitest";
4+
// `vitest/suite` only exists on Vitest 3/4; this module is used only there.
5+
// eslint-disable-next-line import/no-unresolved
46
import { getHooks } from "vitest/suite";
57
type SuiteHooks = ReturnType<typeof getHooks>;
68

@@ -19,8 +21,11 @@ export async function callSuiteHook<T extends keyof SuiteHooks>(
1921

2022
const hooks = getSuiteHooks(suite, name);
2123

22-
// @ts-expect-error TODO: add support for hooks parameters
23-
await Promise.all(hooks.map((fn) => fn()));
24+
// TODO: add support for hook parameters. The hook signature differs across
25+
// supported Vitest versions, so we call them through a parameterless cast.
26+
await Promise.all(
27+
(hooks as Array<() => unknown>).map((fn) => fn()),
28+
);
2429

2530
if (name === "afterEach" && suite?.suite) {
2631
await callSuiteHook(suite.suite, currentTask, name);

0 commit comments

Comments
 (0)