-
Notifications
You must be signed in to change notification settings - Fork 176
fix: improve validation for custom benchmarks #323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
* add Zod schema for BenchmarkResult * parse custom benchmark output to make sure it conforms to schema * allow for coercion
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Assessment against linked issues
Out-of-scope changes
✨ Finishing Touches🧪 Generate unit tests
Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
There was a problem hiding this 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 casingMake 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 pathsCurrent 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 errorsReport 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
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsontest/__snapshots__/extract.spec.ts.snapis 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 tomodule: "commonjs", jest.config.js uses ts-jest with tsconfig.spec.json, imports use ESM syntax (import { z } from 'zod'), and there are no runtimerequire('zod')calls. No changes needed.src/extract.ts (1)
5-5: Confirm import style matches Zod v4 distributionIf 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.
There was a problem hiding this 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 declaresusing: '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
⛔ Files ignored due to path filters (1)
package-lock.jsonis 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 interopNode 20 is declared in action.yml and tsconfig.json uses
"module": "commonjs","moduleResolution": "node"and"esModuleInterop": true. Sincesrc/extract.tsimports Zod, keeping it independenciesis correct—but you must ensure your build or bundle (dist/src/index.js) includes Zod (e.g. via @vercel/ncc or by checking innode_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.
fixes #322