diff --git a/.github/workflows/nodejs.yaml b/.github/workflows/nodejs.yaml index 36567cf9..e50476f4 100644 --- a/.github/workflows/nodejs.yaml +++ b/.github/workflows/nodejs.yaml @@ -28,5 +28,6 @@ jobs: - name: Install dependencies run: npm ci - name: Check formatting - run: | - npm run check + run: npm run check + - name: Run tests + run: npm test diff --git a/report/package-lock.json b/report/package-lock.json index 37f31c38..964ea075 100644 --- a/report/package-lock.json +++ b/report/package-lock.json @@ -34,7 +34,8 @@ "prettier": "^3.5.3", "typescript": "^5.3.3", "vite": "^5.1.4", - "vite-plugin-static-copy": "^2.3.1" + "vite-plugin-static-copy": "^2.3.1", + "vitest": "^3.1.2" } }, "node_modules/@ampproject/remapping": { @@ -2066,6 +2067,119 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@vitest/expect": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.2.tgz", + "integrity": "sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.2", + "@vitest/utils": "3.1.2", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.2.tgz", + "integrity": "sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz", + "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.2.tgz", + "integrity": "sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.2.tgz", + "integrity": "sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.2", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.2.tgz", + "integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz", + "integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.2", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -2311,6 +2425,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -2414,6 +2538,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2495,6 +2629,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2512,6 +2663,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3084,6 +3245,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3316,6 +3487,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -3804,6 +3982,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3814,6 +4002,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5193,6 +5391,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5203,6 +5408,16 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5573,6 +5788,23 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6198,6 +6430,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6207,6 +6446,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -6402,6 +6655,95 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6723,6 +7065,29 @@ } } }, + "node_modules/vite-node": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.2.tgz", + "integrity": "sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite-plugin-static-copy": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.1.tgz", @@ -6743,6 +7108,77 @@ "vite": "^5.0.0 || ^6.0.0" } }, + "node_modules/vitest": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.2.tgz", + "integrity": "sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.1.2", + "@vitest/mocker": "3.1.2", + "@vitest/pretty-format": "^3.1.2", + "@vitest/runner": "3.1.2", + "@vitest/snapshot": "3.1.2", + "@vitest/spy": "3.1.2", + "@vitest/utils": "3.1.2", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.13", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.1.2", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.1.2", + "@vitest/ui": "3.1.2", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6848,6 +7284,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/report/package.json b/report/package.json index e6d80858..b84e1152 100644 --- a/report/package.json +++ b/report/package.json @@ -8,7 +8,8 @@ "preview": "vite preview", "start": "vite build && vite preview", "check": "eslint . --ext .ts,.tsx --fix && prettier --check .", - "lint": "eslint . --ext .ts,.tsx --fix && prettier --write ." + "lint": "eslint . --ext .ts,.tsx --fix && prettier --write .", + "test": "vitest" }, "keywords": [], "author": "", @@ -40,6 +41,7 @@ "prettier": "^3.5.3", "typescript": "^5.3.3", "vite": "^5.1.4", - "vite-plugin-static-copy": "^2.3.1" + "vite-plugin-static-copy": "^2.3.1", + "vitest": "^3.1.2" } } diff --git a/report/src/components/ChartSelector.tsx b/report/src/components/ChartSelector.tsx index bf0063c0..dd57a11d 100644 --- a/report/src/components/ChartSelector.tsx +++ b/report/src/components/ChartSelector.tsx @@ -1,6 +1,5 @@ import { useEffect, useMemo, useRef } from "react"; -import { BenchmarkRun } from "../types"; -import { BenchmarkRuns, getBenchmarkVariables } from "../types"; +import { BenchmarkRuns } from "../types"; import { isEqual } from "lodash"; import { camelToTitleCase, @@ -8,7 +7,7 @@ import { formatLabel, } from "../utils/formatters"; import { interpolateWarm } from "d3"; -import { useSearchParamsState } from "../utils/useSearchParamsState"; +import { useBenchmarkFilters } from "../hooks/useBenchmarkFilters"; export interface DataFileRequest { outputDir: string; @@ -22,111 +21,83 @@ interface ChartSelectorProps { onChangeDataQuery: (data: DataFileRequest[]) => void; } -interface BenchmarkRunWithRole extends BenchmarkRun { - testConfig: BenchmarkRun["testConfig"] & { - role: string; - }; -} - const ChartSelector = ({ benchmarkRuns, onChangeDataQuery, }: ChartSelectorProps) => { - const variables = useMemo((): Record< - string, - (string | number | boolean)[] - > => { - return { - ...getBenchmarkVariables(benchmarkRuns.runs), - role: ["sequencer", "validator"], - }; - }, [benchmarkRuns]); - - const [filterSelections, setFilterSelections] = useSearchParamsState<{ - params: { [key: string]: string }; - byMetric: string; - }>("filters", { params: {}, byMetric: "role" }); - - const validFilterSelections = useMemo(() => { - return Object.fromEntries( - Object.keys(variables) - .filter((key) => { - return key !== filterSelections.byMetric; - }) - .map( - (key) => - [key, filterSelections.params[key] ?? variables[key][0]] as const, - ), - ); - }, [filterSelections.params, filterSelections.byMetric]); - - const matchedRuns = useMemo(() => { - return benchmarkRuns.runs - .flatMap((r): BenchmarkRunWithRole[] => [ - { - ...r, - testConfig: { - ...r.testConfig, - role: "sequencer", - }, - }, - { - ...r, - testConfig: { - ...r.testConfig, - role: "validator", - }, - }, - ]) - .filter((run) => { - return Object.entries(validFilterSelections).every(([key, value]) => { - return ( - `${(run.testConfig as Record)[key]}` === - `${value}` - ); - }); - }); - }, [validFilterSelections, benchmarkRuns.runs]); + const runsWithRoles = useMemo( + () => + benchmarkRuns.runs.flatMap((r) => [ + { ...r, testConfig: { ...r.testConfig, role: "sequencer" } }, + { ...r, testConfig: { ...r.testConfig, role: "validator" } }, + ]), + [benchmarkRuns.runs], + ); + + const { + variables, + filterOptions, + matchedRuns, + filterSelections, + setFilters, + setByMetric, + } = useBenchmarkFilters(runsWithRoles, "role"); const lastSentDataRef = useRef([]); useEffect(() => { let colorMap: ((val: number) => string) | undefined = undefined; - if (filterSelections.byMetric === "GasLimit") { - const min = matchedRuns.reduce((a, b) => { - return Math.min(a, Number(b.testConfig.GasLimit)); - }, 0); - const max = matchedRuns.reduce((a, b) => { - return Math.max(a, Number(b.testConfig.GasLimit)); - }, 0); + if (filterSelections.byMetric === "GasLimit" && matchedRuns.length > 0) { + const gasLimits = matchedRuns.map((r) => Number(r.testConfig.GasLimit)); + const min = Math.min(...gasLimits); + const max = Math.max(...gasLimits); colorMap = (val: number) => - interpolateWarm(1 - (max > 0 ? (val - min) / max : 0)); + interpolateWarm(max - min > 0 ? 1 - (val - min) / (max - min) : 0.5); } - const dataToSend: DataFileRequest[] = matchedRuns.map((run) => { - let seriesName = `${run.testConfig[filterSelections.byMetric ?? "role"]}`; - let color = undefined; - - if (filterSelections.byMetric === "GasLimit") { - seriesName = formatValue(Number(run.testConfig.GasLimit), "gas"); - color = colorMap?.(Number(run.testConfig.GasLimit)); - } - - return { - outputDir: run.outputDir, - role: run.testConfig.role, - name: seriesName, - color, - }; - }); + const dataToSend: DataFileRequest[] = matchedRuns + .map((run): DataFileRequest | null => { + if (!run.testConfig || !run.outputDir) { + console.warn("Skipping run with missing data:", run); + return null; + } + + let seriesName: string; + let color: string | undefined = undefined; + const byMetricValue = run.testConfig[filterSelections.byMetric]; + + if (filterSelections.byMetric === "GasLimit") { + const gasLimitNum = Number(byMetricValue); + seriesName = formatValue(gasLimitNum, "gas"); + color = colorMap?.(gasLimitNum); + } else { + seriesName = + byMetricValue !== undefined + ? formatLabel(String(byMetricValue)) + : "Unknown"; + } + + const role = run.testConfig.role ?? "unknown"; + + const request: DataFileRequest = { + outputDir: run.outputDir, + role: String(role), + name: seriesName, + }; + if (color !== undefined) { + request.color = color; + } + return request; + }) + .filter((item): item is DataFileRequest => item !== null); if (!isEqual(dataToSend, lastSentDataRef.current)) { lastSentDataRef.current = dataToSend; onChangeDataQuery(dataToSend); } - }, [filterSelections, matchedRuns, onChangeDataQuery]); + }, [matchedRuns, filterSelections.byMetric, onChangeDataQuery]); return (
@@ -134,47 +105,46 @@ const ChartSelector = ({
Show Line Per
- {Object.entries(variables) + {Object.entries(filterOptions) .sort((a, b) => a[0].localeCompare(b[0])) .filter(([k]) => k !== filterSelections.byMetric) - .map(([key, value]) => { + .map(([key, availableValues]) => { + const currentValue = + filterSelections.params[key] ?? availableValues[0]; return (
{camelToTitleCase(key)}
); })} + {matchedRuns.length === 0 && ( +
+ No benchmark runs match the current filter combination. +
+ )} ); }; diff --git a/report/src/filter.test.ts b/report/src/filter.test.ts new file mode 100644 index 00000000..44577085 --- /dev/null +++ b/report/src/filter.test.ts @@ -0,0 +1,252 @@ +import { describe, it, expect } from "vitest"; +import { getBenchmarkVariables } from "./filter"; +import { BenchmarkRun } from "./types"; + +// Sample data for testing - This data now aligns with the ActualBenchmarkRun type +const sampleRuns: BenchmarkRun[] = [ + { + sourceFile: "test1.json", + testName: "A", + testDescription: "Desc A", + outputDir: "/tmp/a", + testConfig: { NodeType: "geth", GasLimit: 100, ExtraParam: "true" }, // Use boolean true + result: { success: true }, + }, + { + sourceFile: "test2.json", + testName: "B", + testDescription: "Desc B", + outputDir: "/tmp/b", + testConfig: { NodeType: "reth", GasLimit: 100, ExtraParam: "true" }, // Use boolean true + result: { success: true }, + }, + { + sourceFile: "test3.json", + testName: "C", + testDescription: "Desc C", + outputDir: "/tmp/c", + testConfig: { NodeType: "geth", GasLimit: 200, ExtraParam: "true" }, // Use boolean true + result: { success: true }, + }, + { + sourceFile: "test4.json", + testName: "D", + testDescription: "Desc D", + outputDir: "/tmp/d", + testConfig: { NodeType: "reth", GasLimit: 200, ExtraParam: "false" }, // Use boolean false + result: { success: true }, + }, + { + sourceFile: "test5.json", + testName: "E", + testDescription: "Desc E", + outputDir: "/tmp/e", + testConfig: { NodeType: "geth", GasLimit: 200, ExtraParam: "false" }, // Use boolean false + result: { success: true }, + }, +]; + +describe("BenchmarkVariables", () => { + it("should update options and matched runs when a filter changes", () => { + const result = getBenchmarkVariables(sampleRuns, { + params: { GasLimit: 200, ExtraParam: "true" }, // Change GasLimit from default 100 to 200 + byMetric: "NodeType", + }); + + // Matched runs based on { GasLimit: 200, ExtraParam: true } + expect(result.matchedRuns).toHaveLength(1); + expect(result.matchedRuns[0].testName).toBe("C"); // geth, 200, true + + // Filter options available with GasLimit=200 + expect(result.filterOptions).toEqual({ + // GasLimit: [100, 200] + // Check GasLimit=100 with other filters { ExtraParam: true }: run 'A' matches. + // Check GasLimit=200 with other filters { ExtraParam: true }: run 'C' matches. + GasLimit: [100, 200], + // ExtraParam: [true, false] + // Check ExtraParam=true with other filters { GasLimit: 200 }: run 'C' matches. + // Check ExtraParam=false with other filters { GasLimit: 200 }: runs 'D', 'E' match. + ExtraParam: ["false", "true"], + }); + }); + + it("should provide options even if the current selection has no matches", () => { + const result = getBenchmarkVariables(sampleRuns, { + params: { GasLimit: 100, ExtraParam: false }, // This specific combo has no runs + byMetric: "NodeType", + }); + + // Active filters: { GasLimit: 100, ExtraParam: false } + + // Matched runs based on { GasLimit: 100, ExtraParam: false } + expect(result.matchedRuns).toHaveLength(0); + + // Filter options should still be calculated based on *other* filters + expect(result.filterOptions).toEqual({ + // GasLimit: [100, 200] + // Check GasLimit=100 with other filter { ExtraParam: false }: No match. + // Check GasLimit=200 with other filter { ExtraParam: false }: Runs 'D', 'E' match. + GasLimit: [200], + // ExtraParam: [true, false] + // Check ExtraParam=true with other filter { GasLimit: 100 }: Run 'A', 'B' matches. + // Check ExtraParam=false with other filter { GasLimit: 100 }: No match. + ExtraParam: ["true"], + }); + }); + + it("should handle byMetric correctly, excluding it from filters and options", () => { + const result = getBenchmarkVariables(sampleRuns, { + params: { GasLimit: 200, NodeType: "geth" }, + byMetric: "ExtraParam", // Group by ExtraParam this time + }); + + // Active filters: { NodeType: 'geth', GasLimit: 200 } (default for NodeType) + + // Matched runs based on { NodeType: 'geth', GasLimit: 200 } + expect(result.matchedRuns).toHaveLength(2); + expect(result.matchedRuns.map((r) => r.testName).sort()).toEqual([ + "C", + "E", + ]); // geth, 200, true AND geth, 200, false + + // Filter options available (excluding byMetric 'ExtraParam') + expect(result.filterOptions).toEqual({ + // NodeType: ['geth', 'reth'] + // Check NodeType='geth' with other { GasLimit: 200 }: Runs 'C', 'E' match. + // Check NodeType='reth' with other { GasLimit: 200 }: Run 'D' matches. + NodeType: ["geth", "reth"], + // GasLimit: [100, 200] + // Check GasLimit=100 with other { NodeType: 'geth' }: Run 'A' matches. + // Check GasLimit=200 with other { NodeType: 'geth' }: Runs 'C', 'E' match. + GasLimit: [100, 200], + }); + }); + + it("should handle scenario with only one variable", () => { + const singleVarRuns: BenchmarkRun[] = [ + { + sourceFile: "f1", + testName: "T1", + testDescription: "", + outputDir: "", + testConfig: { X: "a" }, + result: { success: true }, + }, + { + sourceFile: "f2", + testName: "T2", + testDescription: "", + outputDir: "", + testConfig: { X: "b" }, + result: { success: true }, + }, + ]; + const result = getBenchmarkVariables(singleVarRuns, { + params: {}, // No initial params + byMetric: "X", // Group by the only variable + }); + + expect(result.variables).toEqual({ X: ["a", "b"] }); + // No active filters because the only variable is the byMetric + expect(result.matchedRuns).toHaveLength(2); // All runs match initially + // No filter options because the only variable is the byMetric + expect(result.filterOptions).toEqual({}); + }); + + it("should correctly filter options based on other selected filters, even with sparse data", () => { + // Renamed test description for clarity + // Runs where combinations don't overlap well for filtering + const sparseRuns: BenchmarkRun[] = [ + { + sourceFile: "s1", + testName: "S1", + testDescription: "", + outputDir: "", + testConfig: { P1: "A", P2: "X" }, + result: { success: true }, + }, + { + sourceFile: "s2", + testName: "S2", + testDescription: "", + outputDir: "", + testConfig: { P1: "B", P2: "Y" }, + result: { success: true }, + }, + { + sourceFile: "s3", + testName: "S3", + testDescription: "", + outputDir: "", + testConfig: { P1: "A", P2: "Z" }, + result: { success: true }, + }, // Added A/Z + ]; + + // Scenario 1: Select P1='A', group by P2 + const result1 = getBenchmarkVariables(sparseRuns, { + params: { P1: "A" }, + byMetric: "P2", + }); + + expect(result1.variables).toEqual({ P1: ["A", "B"], P2: ["X", "Y", "Z"] }); + // Active filter: { P1: 'A' } + expect(result1.matchedRuns).toHaveLength(2); // S1 (A,X), S3 (A,Z) + expect(result1.matchedRuns.map((r) => r.testName).sort()).toEqual([ + "S1", + "S3", + ]); + // Filter Options: + // P1: Check P1='A' with other {}: Match S1, S3. Check P1='B' with other {}: Match S2. + expect(result1.filterOptions).toEqual({ P1: ["A", "B"] }); + + // Scenario 2: Select P1='B', group by P2 + const result2 = getBenchmarkVariables(sparseRuns, { + params: { P1: "B" }, + byMetric: "P2", + }); + // Active filter: { P1: 'B' } + expect(result2.matchedRuns).toHaveLength(1); // S2 (B, Y) + expect(result2.matchedRuns[0].testName).toBe("S2"); + // Filter Options: + // P1: Check P1='A' with other {}: Match S1, S3. Check P1='B' with other {}: Match S2. + expect(result2.filterOptions).toEqual({ P1: ["A", "B"] }); + }); +}); + +describe("getBenchmarkVariables", () => { + // Test for initial state (add this back if it was removed) + it("should correctly identify variables, initial options, and matched runs with defaults", () => { + const result = getBenchmarkVariables(sampleRuns, { + params: { + ExtraParam: "true", + GasLimit: 100, + }, + byMetric: "NodeType", // Group by NodeType + }); + + // Variables identified (more than one value) + expect(result.variables).toEqual({ + NodeType: ["geth", "reth"], + GasLimit: [100, 200], + ExtraParam: ["false", "true"], + }); + + // Matched runs based on initial active filters + expect(result.matchedRuns).toHaveLength(2); + expect(result.matchedRuns.map((r) => r.testName).sort()).toEqual([ + "A", + "B", + ]); // geth, 200, false AND reth, 200, false + + // Filter options available initially (excluding byMetric) + expect(result.filterOptions).toEqual({ + // GasLimit: Should check based on { ExtraParam: true } + // -> GasLimit=100 matches A, B. GasLimit=200 matches C. + GasLimit: [100, 200], + // ExtraParam: Should check based on { GasLimit: 100 } + // -> ExtraParam=true matches A, B. ExtraParam=false has no match with GasLimit=100. + ExtraParam: ["true"], // Only true should be available based on default GasLimit=100 + }); + }); +}); diff --git a/report/src/filter.ts b/report/src/filter.ts new file mode 100644 index 00000000..c7966aba --- /dev/null +++ b/report/src/filter.ts @@ -0,0 +1,95 @@ +import { BenchmarkRun } from "./types"; + +// Export this type for use elsewhere +export type FilterValue = string | number | boolean; +type FilterSelectionsParams = Record; + +/** + * Matches runs against a given set of filter criteria. + */ +function matchRuns( + runs: BenchmarkRun[], + filterSelections: FilterSelectionsParams, +): BenchmarkRun[] { + return runs.filter((run) => { + return Object.entries(filterSelections).every(([key, value]) => { + return `${run.testConfig[key]}` === `${value}`; + }); + }); +} + +/** + * Extracts variables, calculates available filter options, and filters runs based on selections. + * Ensures that filter options remain available even if the current selection yields no results. + */ +export function getBenchmarkVariables( + runs: BenchmarkRun[], + filterSelections: { + params: FilterSelectionsParams; + byMetric: string; + }, + // Add optional argument for pre-calculated variables + precalculatedVariables?: Record | undefined, + defaultBehavior: "first" | "any" = "first", // if a filter is not set, should it be any or first +) { + const variables = + precalculatedVariables ?? + (() => { + const allPossibleValues: Record> = {}; + for (const run of runs) { + for (const [key, value] of Object.entries(run.testConfig)) { + if (!allPossibleValues[key]) { + allPossibleValues[key] = new Set(); + } + allPossibleValues[key].add(value); + } + } + return Object.fromEntries( + Object.entries(allPossibleValues) + .filter(([, values]) => values.size > 1) + .map(([key, values]) => [key, [...values].sort()]), + ); + })(); // Immediately invoke the IIFE if needed + + const filterOptions: Record = {}; + for (const key of Object.keys(variables)) { + // Skip the metric used for grouping series + if (key === filterSelections.byMetric) { + continue; + } + + // Determine the base filters to use when checking options for `key` + // This includes all *other* active filters. + const otherActiveFilters = { ...filterSelections.params }; + delete otherActiveFilters[key]; + delete otherActiveFilters[filterSelections.byMetric]; + + // Check which values for `key` yield results when combined with `otherActiveFilters` + const validValuesForKey = variables[key].filter((value) => { + const potentialFilters = { ...otherActiveFilters, [key]: value }; + return matchRuns(runs, potentialFilters).length > 0; + }); + + if (validValuesForKey.length > 0) { + filterOptions[key] = validValuesForKey; + } + } + + // 3. Calculate active filters with defaults + const activeFilters: FilterSelectionsParams = {}; + for (const [key, values] of Object.entries(filterOptions)) { + if (key === filterSelections.byMetric) continue; + + const selectedValue = filterSelections.params[key]; + if (selectedValue !== undefined) { + activeFilters[key] = selectedValue; + } else if (defaultBehavior === "first" && values.length > 0) { + activeFilters[key] = values[0]; + } + } + + // 4. Calculate the final matched runs based on the current activeFilters + const matchedRuns = matchRuns(runs, activeFilters); + + return { variables, filterOptions, matchedRuns }; +} diff --git a/report/src/hooks/useBenchmarkFilters.ts b/report/src/hooks/useBenchmarkFilters.ts new file mode 100644 index 00000000..517b7d1b --- /dev/null +++ b/report/src/hooks/useBenchmarkFilters.ts @@ -0,0 +1,107 @@ +import { useCallback, useMemo } from "react"; +import { isEqual } from "lodash"; +import { type BenchmarkRun } from "../types"; +import { useSearchParamsState } from "../utils/useSearchParamsState"; +import { getBenchmarkVariables } from "../filter"; +type FilterValue = string | number | boolean; +type FilterSelectionsParams = Record; +type FilterSelections = { + params: FilterSelectionsParams; + byMetric: string; +}; + +/** + * Custom hook to manage benchmark filter selections and derived data. + * Encapsulates state logic and ensures filter consistency after updates. + * + * @param benchmarkRuns - The raw benchmark runs data. + * @param defaultMetric - The default metric to group by ('role' if not specified). + * @returns An object containing derived data and state management functions. + */ +export function useBenchmarkFilters( + runsWithRoles: BenchmarkRun[], + defaultMetric: string = "role", +) { + const [filterSelections, setRawFilterSelections] = + useSearchParamsState("filters", { + params: {}, + byMetric: defaultMetric, + }); + + // Memoize variables once, as they don't depend on selections + const variables = useMemo(() => { + const allPossibleValues: Record> = {}; + for (const run of runsWithRoles) { + for (const [key, value] of Object.entries(run.testConfig)) { + if (!allPossibleValues[key]) { + allPossibleValues[key] = new Set(); + } + allPossibleValues[key].add(value); + } + } + return Object.fromEntries( + Object.entries(allPossibleValues) + .filter(([, values]) => values.size > 1) + .map(([key, values]) => [key, [...values].sort()]), + ); + }, [runsWithRoles]); + + // Calculate current options and matched runs based on current selections + variables + const { filterOptions, matchedRuns } = useMemo(() => { + // Ensure a default byMetric if somehow cleared + const currentSelections = { + ...filterSelections, + byMetric: filterSelections.byMetric || defaultMetric, + }; + // Pass memoized variables to avoid recalculating them inside + return getBenchmarkVariables( + runsWithRoles, + currentSelections, + variables, + "first", + ); + }, [runsWithRoles, filterSelections, defaultMetric, variables]); + + // Define the setter function (simplified: no adjustment logic) + const setFilters = useCallback( + (name: string, value: FilterValue) => { + const prevState = filterSelections; + + const targetParams = { + ...prevState.params, + [name]: value, + }; + + const targetFilterSelections = { + ...prevState, + params: targetParams, + }; + + if (!isEqual(targetFilterSelections, prevState)) { + setRawFilterSelections(targetFilterSelections); + } + }, + [filterSelections, setRawFilterSelections], + ); + + const setByMetric = useCallback( + (metric: string) => { + // when by metric changes, reset all other filters + + setRawFilterSelections({ + params: {}, + byMetric: metric, + }); + }, + [setRawFilterSelections], + ); + + return { + variables, + filterOptions, + matchedRuns, + filterSelections, // Return current selections for UI binding + setFilters, // Return the simplified setter + setByMetric, + }; +} diff --git a/report/src/pages/RunIndex.tsx b/report/src/pages/RunIndex.tsx index 553cb89e..d248adde 100644 --- a/report/src/pages/RunIndex.tsx +++ b/report/src/pages/RunIndex.tsx @@ -5,21 +5,49 @@ import { formatValue, } from "../utils/formatters"; import { useTestMetadata } from "../utils/useDataSeries"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; +import { getBenchmarkVariables } from "../filter"; function RunIndex() { const { data: benchmarkRuns, isLoading: isLoadingBenchmarkRuns } = useTestMetadata(); + // State for filter selections + const [filterSelections, setFilterSelections] = useState< + Record + >({}); + + // Calculate filter options and filtered runs + const { filterOptions, matchedRuns } = useMemo(() => { + if (!benchmarkRuns) { + return { filterOptions: {}, matchedRuns: [] }; + } + + // Only include non-"any" filters in the params + const activeFilters = Object.fromEntries( + Object.entries(filterSelections).filter(([, value]) => value !== "any"), + ); + + return getBenchmarkVariables( + benchmarkRuns.runs, + { + params: activeFilters, + byMetric: "N/A", + }, + undefined, + "any", + ); + }, [benchmarkRuns, filterSelections]); + // Calculate only configs that differ across runs const diffConfigKeys = useMemo(() => { - if (!benchmarkRuns) { + if (!matchedRuns) { return []; } const configKeyToValues: Record> = {}; - benchmarkRuns.runs.forEach((run) => { + matchedRuns.forEach((run) => { const runConfig = run.testConfig || {}; Object.entries(runConfig).forEach(([key, value]) => { if (!configKeyToValues[key]) { @@ -30,16 +58,18 @@ function RunIndex() { }); const differingKeys = Object.entries(configKeyToValues) - .filter(([, values]) => values.size > 1) + .filter( + ([key, values]) => values.size > 1 || filterSelections[key] !== "any", + ) .map(([key]) => key); - return benchmarkRuns.runs.map((run) => { + return matchedRuns.map((run) => { const runConfig = run.testConfig || {}; return Object.entries(runConfig).filter(([key]) => differingKeys.includes(key), ); }); - }, [benchmarkRuns]); + }, [matchedRuns, filterSelections]); if (!benchmarkRuns || isLoadingBenchmarkRuns) { return
Loading...
; @@ -47,6 +77,45 @@ function RunIndex() { return (
+ {/* Filter Interface */} +
+ {Object.entries(filterOptions) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([key, availableValues]) => { + const currentValue = filterSelections[key] ?? "any"; + return ( +
+
+ {camelToTitleCase(key)} +
+ +
+ ); + })} +
+ @@ -113,7 +182,7 @@ function RunIndex() { - {benchmarkRuns?.runs.map((run, i) => ( + {matchedRuns.map((run, i) => (
{run.testName} diff --git a/report/src/types.ts b/report/src/types.ts index bc3b3b75..997c8ea7 100644 --- a/report/src/types.ts +++ b/report/src/types.ts @@ -64,24 +64,3 @@ export interface BenchmarkRuns { runs: BenchmarkRun[]; createdAt: string; } - -export function getBenchmarkVariables(runs: BenchmarkRun[]) { - const inferredConfig: Record> = {}; - - for (const run of runs) { - for (const [key, value] of Object.entries(run.testConfig)) { - if (!inferredConfig[key]) { - inferredConfig[key] = []; - } - inferredConfig[key].push(value); - } - } - - return Object.fromEntries( - Object.entries(inferredConfig) - .filter(([, values]) => values.length > 1) - .map(([key, values]) => { - return [key, [...new Set(values)]]; - }), - ); -}