Skip to content

Conversation

@ktrz
Copy link
Member

@ktrz ktrz commented Sep 6, 2025

  • add Zod schema for BenchmarkResult
  • parse custom benchmark output to make sure it conforms to schema
  • allow for coercion

fixes #322

* add Zod schema for BenchmarkResult
* parse custom benchmark output to make sure it conforms to schema
* allow for coercion
@ktrz ktrz self-assigned this Sep 6, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 6, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Stricter runtime validation for benchmark results, improving robustness.
  • Bug Fixes
    • Improved parsing for custom benchmarks with clearer error messages.
  • Tests
    • Added additional test coverage for custom benchmark parsing scenarios.
  • Documentation
    • Updated changelog with an Unreleased entry noting the parsing fix.
  • Chores
    • Updated dependencies and development tooling to latest versions.
    • Integrated a validation library to standardize and enforce input formats.

Walkthrough

Adds Zod runtime schemas for benchmark results, validates/parses custom benchmark JSON via those schemas with improved error messages, adds zod as a dependency, adds a test fixture and test case, and updates CHANGELOG with an Unreleased entry.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Added Unreleased entry: “improve parsing for custom benchmarks (#323)” and inserted an extra blank line.
Dependencies
package.json
Added runtime dependency "zod": "^4.1.5"; updated devDependencies (@types/node, typescript) versions.
Extraction logic & types
src/extract.ts
Replaced exported BenchmarkResult interface with Zod schema (export const BenchmarkResult = z.object(...)), added export type BenchmarkResult = z.infer<typeof BenchmarkResult> and export const BenchmarkResults = z.array(BenchmarkResult); switched custom benchmark parsing to BenchmarkResults.parse(JSON.parse(output)); improved error handling and removed a stray output; line; added import { z } from 'zod'.
Tests: spec
test/extract.spec.ts
Added a new normal case for customSmallerIsBetter referencing the new fixture.
Tests: data fixture
test/data/extract/customSmallerIsBetter_output2.json
New JSON fixture with four benchmark records (name, unit: ns, value, range).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant Extract as extractResult(...)
  participant JSON as JSON.parse
  participant Zod as BenchmarkResults.parse

  Caller->>Extract: extractResult(output, tool="customSmallerIsBetter")
  Extract->>JSON: JSON.parse(output)
  JSON-->>Extract: JS value (array)
  Extract->>Zod: BenchmarkResults.parse(array)
  alt parse success
    Zod-->>Extract: validated/coerced BenchmarkResult[]
    Extract-->>Caller: return results
  else parse failure
    Zod-->>Extract: throws ZodError
    Extract-->>Caller: throw Error(errMessage)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective (issue) Addressed Explanation
Ensure parsing no longer fails with "range.match is not a function" by handling non-string range values ( #322 )
Improve robustness of custom benchmark parsing and error reporting ( #322 )

Out-of-scope changes

Code Change Explanation
Dev dependency upgrades (package.json) — @types/node and typescript updated (package.json) Version bumps of devDependencies are unrelated to the parsing fix objective; they are broader maintenance changes not required to resolve the linked issue.
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/322/unsafe-parsing-for-custom-benchmarks

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Sep 6, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.14%. Comparing base (65914f2) to head (d024af2).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #323      +/-   ##
==========================================
+ Coverage   89.13%   89.14%   +0.01%     
==========================================
  Files          16       16              
  Lines         948      949       +1     
  Branches      198      199       +1     
==========================================
+ Hits          845      846       +1     
- Misses         99      103       +4     
+ Partials        4        0       -4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
CHANGELOG.md (1)

3-4: Nit: Link the PR and use consistent prefix casing

Make this entry consistent with others and link the PR.

-- **fix** improve parsing for custom benchmarks (#323)
+- **fix:** improve parsing for custom benchmarks ([#323](https://github.com/benchmark-action/github-action-benchmark/pull/323))
test/data/extract/customSmallerIsBetter_output2.json (1)

1-26: Add a coercion-focused fixture (stringified numbers) to exercise z.coerce paths

Current fixture has numeric value/range. Add a case where value is a string and range is numeric to prove coercion.

Example new file: test/data/extract/customSmallerIsBetter_output3.json

[
  { "name": "No instrumentation", "unit": "ns", "value": "90.9439", "range": 0.519165 },
  { "name": "Deactivated probe",  "unit": "ns", "value": "445.661",  "range": 8.21371  }
]

And include it in test/extract.spec.ts normalCases.

src/extract.ts (2)

7-13: Tighten schema and avoid type/value name collision

  • Ensure non-empty strings and finite numbers.
  • Avoid over-coercion turning objects into "[object Object]".
  • Rename runtime schema to avoid confusion with the type alias.
-export const BenchmarkResult = z.object({
-    name: z.coerce.string(),
-    value: z.coerce.number(),
-    range: z.coerce.string().optional(),
-    unit: z.coerce.string(),
-    extra: z.coerce.string().optional(),
-});
-
-export type BenchmarkResult = z.infer<typeof BenchmarkResult>;
-
-export const BenchmarkResults = z.array(BenchmarkResult);
+export const BenchmarkResultSchema = z
+    .object({
+        name: z.string().min(1).transform((s) => s.trim()),
+        value: z.coerce.number().finite(),
+        // Accept number|string|null; avoid coercing objects/arrays to "[object Object]"
+        range: z
+            .union([z.string(), z.number(), z.null()])
+            .optional()
+            .transform((v) => (v == null ? undefined : String(v))),
+        unit: z.string().min(1).transform((s) => s.trim()),
+        extra: z
+            .union([z.string(), z.number()])
+            .optional()
+            .transform((v) => (typeof v === 'number' ? String(v) : v?.trim())),
+    })
+    .strip();
+
+export type BenchmarkResult = z.infer<typeof BenchmarkResultSchema>;
+
+export const BenchmarkResults = z.array(BenchmarkResultSchema);

Also applies to: 15-17


664-671: Surface Zod issues (paths/messages) for actionable errors

Report first few schema errors with paths; keep JSON.parse errors as-is.

-function extractCustomBenchmarkResult(output: string): BenchmarkResult[] {
-    try {
-        return BenchmarkResults.parse(JSON.parse(output));
-    } catch (err: unknown) {
-        const errMessage = err instanceof Error ? err.message : String(err);
-        throw new Error(
-            `Output file for 'custom-(bigger|smaller)-is-better' must be JSON file containing an array of entries in BenchmarkResult format: ${errMessage}`,
-        );
-    }
-}
+function extractCustomBenchmarkResult(output: string): BenchmarkResult[] {
+    try {
+        const parsed = JSON.parse(output);
+        const res = BenchmarkResults.safeParse(parsed);
+        if (!res.success) {
+            const details = res.error.issues
+                .slice(0, 3)
+                .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
+                .join('; ');
+            throw new Error(details);
+        }
+        return res.data;
+    } catch (err: unknown) {
+        const errMessage = err instanceof Error ? err.message : String(err);
+        throw new Error(
+            `Output file for 'custom-(bigger|smaller)-is-better' must be JSON file containing an array of entries in BenchmarkResult format: ${errMessage}`,
+        );
+    }
+}
test/extract.spec.ts (1)

152-166: Add targeted tests for Zod coercion and invalid-shape errors (beyond snapshot).

To exercise the new schema behavior, consider:

  • a coercion case (stringified numeric values),
  • and an invalid-shape case (missing required fields), asserting rejection.

TypeScript tests to add:

it('coerces custom JSON fields via Zod (value as string)', async () => {
  const outputFilePath = path.join(__dirname, 'data', 'extract', 'customSmallerIsBetter_output_coercion.json');
  const config = { tool: 'customSmallerIsBetter', outputFilePath } as Config;
  const bench = await extractResult(config);
  expect(bench).toMatchSnapshot();
});

it('fails with a clear error for invalid custom JSON shape', async () => {
  const outputFilePath = path.join(__dirname, 'data', 'extract', 'customSmallerIsBetter_output_invalid.json');
  const config = { tool: 'customSmallerIsBetter', outputFilePath } as Config;
  await expect(extractResult(config)).rejects.toThrow();
});

Fixture files to add:

test/data/extract/customSmallerIsBetter_output_coercion.json

[
  { "name": "No instrumentation", "unit": "ns", "value": "101.5", "range": "R1" },
  { "name": "Deactivated probe", "unit": "ns", "value": "98.2" }
]

test/data/extract/customSmallerIsBetter_output_invalid.json

[
  { "name": "Missing value", "unit": "ns" }
]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65914f2 and 5b84773.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • test/__snapshots__/extract.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • CHANGELOG.md (1 hunks)
  • package.json (1 hunks)
  • src/extract.ts (2 hunks)
  • test/data/extract/customSmallerIsBetter_output2.json (1 hunks)
  • test/extract.spec.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
test/data/extract/customSmallerIsBetter_output2.json (2)
test/write.spec.ts (3)
  • dataJson (168-933)
  • t (1336-1416)
  • t (828-932)
src/write.ts (1)
  • addBenchmarkToDataJson (320-335)
src/extract.ts (2)
test/normalizeBenchmarkResult.spec.ts (1)
  • describe (3-176)
scripts/ci_validate_modification.ts (1)
  • validateBenchmarkResultMod (132-188)
🔇 Additional comments (3)
package.json (1)

37-39: Zod v4 compatibility with CommonJS/ts-jest confirmed
tsconfig.json is set to module: "commonjs", jest.config.js uses ts-jest with tsconfig.spec.json, imports use ESM syntax (import { z } from 'zod'), and there are no runtime require('zod') calls. No changes needed.

src/extract.ts (1)

5-5: Confirm import style matches Zod v4 distribution

If Zod v4 is ESM-only and your output is CJS, this import can still compile but may fail at runtime. See package.json comment for verification.

test/extract.spec.ts (1)

146-149: LGTM: Expanded coverage for customSmallerIsBetter.

Adding the second fixture increases confidence in the new Zod-backed parsing. No issues spotted.

@ktrz ktrz merged commit 5cc7864 into master Sep 6, 2025
28 of 29 checks passed
@ktrz ktrz deleted the fix/322/unsafe-parsing-for-custom-benchmarks branch September 6, 2025 11:28
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
package.json (1)

47-47: Align @types/node version with Node20 runtime
action.yml declares using: 'node20', so downgrade @types/node to the matching Node.js 20 types to avoid accidental use of newer APIs.

-    "@types/node": "^24.3.1",
+    "@types/node": "^20.12.12",
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b84773 and d024af2.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • package.json (3 hunks)
🔇 Additional comments (2)
package.json (2)

37-39: Verify Zod runtime inclusion and interop

Node 20 is declared in action.yml and tsconfig.json uses "module": "commonjs", "moduleResolution": "node" and "esModuleInterop": true. Since src/extract.ts imports Zod, keeping it in dependencies is correct—but you must ensure your build or bundle (dist/src/index.js) includes Zod (e.g. via @vercel/ncc or by checking in node_modules) so it’s available at runtime.

Optional: pin Zod to an exact version to avoid unexpected minor upgrades:

-  "zod": "^4.1.5"
+  "zod": "4.1.5"

65-65: Verify TS 5.9 compatibility — ensure [email protected], [email protected] and @typescript-eslint/*@5.4.0 work with TypeScript 5.9 by running your CI (build, lint, tests); if you hit parser or lint errors, upgrade ESLint to ^9.x and @typescript-eslint/parser & plugin to ^7.x.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Execution Fails due to Missing range.match

2 participants