From 34b0152e693b1aa3206c5dfd38509ee77d20b9b5 Mon Sep 17 00:00:00 2001 From: Johann Schopplich Date: Thu, 1 Oct 2026 21:07:52 +0200 Subject: [PATCH 1/6] test: validate fixtures against the fixture schema --- package.json | 2 ++ pnpm-lock.yaml | 29 +++++++++++++++++++++++++++++ scripts/validate-fixtures.mjs | 29 +++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 scripts/validate-fixtures.mjs diff --git a/package.json b/package.json index 7641466..b7c9239 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,12 @@ ], "scripts": { "lint": "eslint .", + "test": "node scripts/validate-fixtures.mjs", "release": "bumpp" }, "devDependencies": { "@antfu/eslint-config": "^9.5.1", + "ajv": "^8.20.0", "bumpp": "^12.3.0", "eslint": "^10.11.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55be21f..e53a3f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@antfu/eslint-config': specifier: ^9.5.1 version: 9.5.1(@typescript-eslint/typescript-estree@8.71.0(typescript@6.0.3))(@typescript-eslint/utils@8.71.0(eslint@10.11.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.43)(eslint@10.11.0(jiti@2.7.0))(typescript@6.0.3) + ajv: + specifier: ^8.20.0 + version: 8.20.0 bumpp: specifier: ^12.3.0 version: 12.3.0 @@ -390,6 +393,9 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansis@4.4.0: resolution: {integrity: sha512-9k3v7xcHwgdO/DruxGIg4HtjvlAZlcnsX/mzqUb1t3NkYnl9kK2UJ+Gq0io+vQf7iT//BD/HB/NBkUR1LWxoeA==} engines: {node: '>=14'} @@ -782,6 +788,9 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} + fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -922,6 +931,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -1240,6 +1252,10 @@ packages: resolution: {integrity: sha512-ycwFAS14Jw4mppvmK4GR/J6u3WpWpjkEApehuHtLc/8VpPNpDMbQ4WjqwplXifGeyKOzHSFLmSPqzksDQE2Sfg==} hasBin: true + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + reserved-identifiers@1.2.0: resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} engines: {node: '>=18'} @@ -1828,6 +1844,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.8 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansis@4.4.0: {} are-docs-informative@0.1.1: {} @@ -2252,6 +2275,8 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 + fast-uri@3.1.8: {} + fast-wrap-ansi@0.2.2: dependencies: fast-string-width: 3.0.2 @@ -2363,6 +2388,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} jsonc-eslint-parser@3.3.0: @@ -2881,6 +2908,8 @@ snapshots: dependencies: jsesc: 3.1.0 + require-from-string@2.0.2: {} + reserved-identifiers@1.2.0: {} resolve-pkg-maps@1.0.0: {} diff --git a/scripts/validate-fixtures.mjs b/scripts/validate-fixtures.mjs new file mode 100644 index 0000000..9693047 --- /dev/null +++ b/scripts/validate-fixtures.mjs @@ -0,0 +1,29 @@ +import { readdirSync, readFileSync } from 'node:fs' +import { join } from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import Ajv from 'ajv' + +const testsDir = fileURLToPath(new URL('../tests/', import.meta.url)) +const schema = JSON.parse(readFileSync(join(testsDir, 'fixtures.schema.json'), 'utf8')) +const validate = new Ajv({ allErrors: true }).compile(schema) + +let failed = 0 +for (const category of ['encode', 'decode']) { + const dir = join(testsDir, 'fixtures', category) + for (const file of readdirSync(dir).filter(name => name.endsWith('.json'))) { + const fixture = JSON.parse(readFileSync(join(dir, file), 'utf8')) + const problems = [] + if (!validate(fixture)) + problems.push(...validate.errors.map(error => `${error.instancePath || '/'} ${error.message}`)) + if (fixture.category !== category) + problems.push(`/category is "${fixture.category}", expected "${category}"`) + for (const problem of problems) + console.error(`${category}/${file}: ${problem}`) + failed += problems.length + } +} + +if (failed > 0) + process.exit(1) +console.log('All fixtures match tests/fixtures.schema.json') From bae6d86083dda827d4803eac68a0b23240219b84 Mon Sep 17 00:00:00 2001 From: Johann Schopplich Date: Thu, 1 Oct 2026 21:07:53 +0200 Subject: [PATCH 2/6] ci: run the fixture validation --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31923b5..15620d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,4 @@ jobs: cache: pnpm - run: pnpm install - run: pnpm run lint + - run: pnpm run test From fa0da03989fd5e3fbd4e7f895b589c0d38c71664 Mon Sep 17 00:00:00 2001 From: Johann Schopplich Date: Fri, 2 Oct 2026 07:39:13 +0200 Subject: [PATCH 3/6] docs: point fixture validation at `pnpm test` --- CONTRIBUTING.md | 2 +- tests/README.md | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index afd6005..4acf8d9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,7 @@ For major changes requiring RFC: ## Contributing Test Fixtures -Test fixtures validate TOON implementations across languages. Add your test to `tests/fixtures/encode/` or `tests/fixtures/decode/`, validate against `tests/fixtures.schema.json`, and submit a PR. +Test fixtures validate TOON implementations across languages. Add your test to `tests/fixtures/encode/` or `tests/fixtures/decode/`, run `pnpm test` to validate it against `tests/fixtures.schema.json`, and submit a PR. See [tests/README.md](./tests/README.md) for fixture structure and guidelines. diff --git a/tests/README.md b/tests/README.md index b06f6e0..6993848 100644 --- a/tests/README.md +++ b/tests/README.md @@ -148,17 +148,8 @@ Load each fixture file, run every entry in its `tests` array through your encode ## Validating Fixtures -All fixture files should validate against [`fixtures.schema.json`](./fixtures.schema.json). Run the commands below from the repository root: - -```bash -# Using ajv-cli -npx ajv-cli validate -s tests/fixtures.schema.json -d "tests/fixtures/**/*.json" - -# Using check-jsonschema (Python) -pip install check-jsonschema -check-jsonschema --schemafile tests/fixtures.schema.json tests/fixtures/**/*.json -``` +`pnpm test` validates every fixture against [`fixtures.schema.json`](./fixtures.schema.json) and checks that its `category` matches its directory. ## Contributing Test Cases -Add your test to the matching fixture file, reference the spec section it exercises, verify the expected output against SPEC.md, and validate the file against the schema before submitting a PR. See [CONTRIBUTING.md](../CONTRIBUTING.md) for guidelines. +Add your test to the matching fixture file, reference the spec section it exercises, verify the expected output against SPEC.md, and run `pnpm test` before submitting a PR. See [CONTRIBUTING.md](../CONTRIBUTING.md) for guidelines. From 92604a1564531df072cca3e3231e72143c6892a3 Mon Sep 17 00:00:00 2001 From: Johann Schopplich Date: Fri, 2 Oct 2026 08:34:25 +0200 Subject: [PATCH 4/6] refactor: read fixtures with `node:fs/promises` and `import.meta.dirname` --- scripts/validate-fixtures.mjs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/scripts/validate-fixtures.mjs b/scripts/validate-fixtures.mjs index 9693047..e903523 100644 --- a/scripts/validate-fixtures.mjs +++ b/scripts/validate-fixtures.mjs @@ -1,29 +1,29 @@ -import { readdirSync, readFileSync } from 'node:fs' +import { readdir, readFile } from 'node:fs/promises' import { join } from 'node:path' import process from 'node:process' -import { fileURLToPath } from 'node:url' import Ajv from 'ajv' -const testsDir = fileURLToPath(new URL('../tests/', import.meta.url)) -const schema = JSON.parse(readFileSync(join(testsDir, 'fixtures.schema.json'), 'utf8')) +const testsDir = join(import.meta.dirname, '../tests') +const schema = JSON.parse(await readFile(join(testsDir, 'fixtures.schema.json'), 'utf8')) const validate = new Ajv({ allErrors: true }).compile(schema) -let failed = 0 +const problems = [] for (const category of ['encode', 'decode']) { const dir = join(testsDir, 'fixtures', category) - for (const file of readdirSync(dir).filter(name => name.endsWith('.json'))) { - const fixture = JSON.parse(readFileSync(join(dir, file), 'utf8')) - const problems = [] + const files = (await readdir(dir)).filter(name => name.endsWith('.json')) + for (const file of files) { + const fixture = JSON.parse(await readFile(join(dir, file), 'utf8')) + const path = `${category}/${file}` if (!validate(fixture)) - problems.push(...validate.errors.map(error => `${error.instancePath || '/'} ${error.message}`)) + problems.push(...validate.errors.map(error => `${path}: ${error.instancePath || '/'} ${error.message}`)) if (fixture.category !== category) - problems.push(`/category is "${fixture.category}", expected "${category}"`) - for (const problem of problems) - console.error(`${category}/${file}: ${problem}`) - failed += problems.length + problems.push(`${path}: /category is "${fixture.category}", expected "${category}"`) } } -if (failed > 0) +if (problems.length > 0) { + console.error(problems.join('\n')) process.exit(1) +} + console.log('All fixtures match tests/fixtures.schema.json') From c47a07eeca40e714d16a129322caff8f87f3cbef Mon Sep 17 00:00:00 2001 From: Johann Schopplich Date: Fri, 2 Oct 2026 16:55:20 +0200 Subject: [PATCH 5/6] refactor: run the fixture validation as TypeScript --- package.json | 2 +- scripts/{validate-fixtures.mjs => validate-fixtures.ts} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename scripts/{validate-fixtures.mjs => validate-fixtures.ts} (87%) diff --git a/package.json b/package.json index b7c9239..9223570 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ ], "scripts": { "lint": "eslint .", - "test": "node scripts/validate-fixtures.mjs", + "test": "node scripts/validate-fixtures.ts", "release": "bumpp" }, "devDependencies": { diff --git a/scripts/validate-fixtures.mjs b/scripts/validate-fixtures.ts similarity index 87% rename from scripts/validate-fixtures.mjs rename to scripts/validate-fixtures.ts index e903523..12f552e 100644 --- a/scripts/validate-fixtures.mjs +++ b/scripts/validate-fixtures.ts @@ -7,7 +7,7 @@ const testsDir = join(import.meta.dirname, '../tests') const schema = JSON.parse(await readFile(join(testsDir, 'fixtures.schema.json'), 'utf8')) const validate = new Ajv({ allErrors: true }).compile(schema) -const problems = [] +const problems: string[] = [] for (const category of ['encode', 'decode']) { const dir = join(testsDir, 'fixtures', category) const files = (await readdir(dir)).filter(name => name.endsWith('.json')) @@ -15,7 +15,7 @@ for (const category of ['encode', 'decode']) { const fixture = JSON.parse(await readFile(join(dir, file), 'utf8')) const path = `${category}/${file}` if (!validate(fixture)) - problems.push(...validate.errors.map(error => `${path}: ${error.instancePath || '/'} ${error.message}`)) + problems.push(...validate.errors!.map(error => `${path}: ${error.instancePath || '/'} ${error.message}`)) if (fixture.category !== category) problems.push(`${path}: /category is "${fixture.category}", expected "${category}"`) } From 06f29d375de57c7127a0935ed1afa8321e8d41e1 Mon Sep 17 00:00:00 2001 From: Johann Schopplich Date: Fri, 2 Oct 2026 16:55:23 +0200 Subject: [PATCH 6/6] refactor: name the fixture validation variables precisely --- scripts/validate-fixtures.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/validate-fixtures.ts b/scripts/validate-fixtures.ts index 12f552e..045f11d 100644 --- a/scripts/validate-fixtures.ts +++ b/scripts/validate-fixtures.ts @@ -7,22 +7,22 @@ const testsDir = join(import.meta.dirname, '../tests') const schema = JSON.parse(await readFile(join(testsDir, 'fixtures.schema.json'), 'utf8')) const validate = new Ajv({ allErrors: true }).compile(schema) -const problems: string[] = [] +const fixtureErrors: string[] = [] for (const category of ['encode', 'decode']) { - const dir = join(testsDir, 'fixtures', category) - const files = (await readdir(dir)).filter(name => name.endsWith('.json')) - for (const file of files) { - const fixture = JSON.parse(await readFile(join(dir, file), 'utf8')) - const path = `${category}/${file}` + const categoryDir = join(testsDir, 'fixtures', category) + const fileNames = (await readdir(categoryDir)).filter(fileName => fileName.endsWith('.json')) + for (const fileName of fileNames) { + const fixture = JSON.parse(await readFile(join(categoryDir, fileName), 'utf8')) + const fixturePath = `${category}/${fileName}` if (!validate(fixture)) - problems.push(...validate.errors!.map(error => `${path}: ${error.instancePath || '/'} ${error.message}`)) + fixtureErrors.push(...validate.errors!.map(error => `${fixturePath}: ${error.instancePath || '/'} ${error.message}`)) if (fixture.category !== category) - problems.push(`${path}: /category is "${fixture.category}", expected "${category}"`) + fixtureErrors.push(`${fixturePath}: /category is "${fixture.category}", expected "${category}"`) } } -if (problems.length > 0) { - console.error(problems.join('\n')) +if (fixtureErrors.length > 0) { + console.error(fixtureErrors.join('\n')) process.exit(1) }