-
-
Notifications
You must be signed in to change notification settings - Fork 230
Add normalized hash-based test IDs to draft2020-12/enum.json (POC for #698) #796
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,21 @@ | ||
| { | ||
| "name": "json-schema-test-suite", | ||
| "version": "0.1.0", | ||
| "type": "module", | ||
| "description": "A language agnostic test suite for the JSON Schema specifications", | ||
| "repository": "github:json-schema-org/JSON-Schema-Test-Suite", | ||
| "keywords": [ | ||
| "json-schema", | ||
| "tests" | ||
| ], | ||
| "author": "http://json-schema.org", | ||
| "license": "MIT" | ||
| "license": "MIT", | ||
| "dependencies": { | ||
| "@hyperjump/browser": "^1.3.1", | ||
| "@hyperjump/json-pointer": "^1.1.1", | ||
| "@hyperjump/json-schema": "^1.17.2", | ||
| "@hyperjump/pact": "^1.4.0", | ||
| "@hyperjump/uri": "^1.3.2", | ||
| "json-stringify-deterministic": "^1.0.12" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,74 @@ | ||||||
| import * as fs from "node:fs"; | ||||||
| import * as crypto from "node:crypto"; | ||||||
| import jsonStringify from "json-stringify-deterministic"; | ||||||
| import { normalize } from "./normalize.js"; | ||||||
| import { loadRemotes } from "./load-remotes.js"; | ||||||
|
|
||||||
| const DIALECT_MAP = { | ||||||
| "https://json-schema.org/draft/2020-12/schema": "https://json-schema.org/draft/2020-12/schema", | ||||||
| "https://json-schema.org/draft/2019-09/schema": "https://json-schema.org/draft/2019-09/schema", | ||||||
| "http://json-schema.org/draft-07/schema#": "http://json-schema.org/draft-07/schema#", | ||||||
| "http://json-schema.org/draft-06/schema#": "http://json-schema.org/draft-06/schema#", | ||||||
| "http://json-schema.org/draft-04/schema#": "http://json-schema.org/draft-04/schema#" | ||||||
| }; | ||||||
|
|
||||||
| function getDialectUri(schema) { | ||||||
| if (schema.$schema && DIALECT_MAP[schema.$schema]) { | ||||||
| return DIALECT_MAP[schema.$schema]; | ||||||
| } | ||||||
| return "https://json-schema.org/draft/2020-12/schema"; | ||||||
| } | ||||||
|
|
||||||
| function generateTestId(normalizedSchema, testData, testValid) { | ||||||
| return crypto | ||||||
| .createHash("md5") | ||||||
| .update(jsonStringify(normalizedSchema) + jsonStringify(testData) + testValid) | ||||||
| .digest("hex"); | ||||||
| } | ||||||
|
|
||||||
| async function addIdsToFile(filePath) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should need to pass the dialect based on the directory to this function. |
||||||
| console.log("Reading:", filePath); | ||||||
| const tests = JSON.parse(fs.readFileSync(filePath, "utf8")); | ||||||
| let changed = false; | ||||||
| let added = 0; | ||||||
|
|
||||||
| if (!Array.isArray(tests)) { | ||||||
| console.log("Expected an array at top level, got:", typeof tests); | ||||||
| return; | ||||||
| } | ||||||
|
Comment on lines
+35
to
+38
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be necessary. There's already automation to check the test file against the schema. By the time we run this script, we should be confident that the test file is in the right format. |
||||||
|
|
||||||
| for (const testCase of tests) { | ||||||
| if (!Array.isArray(testCase.tests)) continue; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More unnecessary defensive programming. |
||||||
|
|
||||||
| const dialectUri = getDialectUri(testCase.schema || {}); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unnecessary defensive programming.
Suggested change
|
||||||
| const normalizedSchema = await normalize(testCase.schema || true, dialectUri); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unnecessary defensive programming.
Suggested change
|
||||||
|
|
||||||
| for (const test of testCase.tests) { | ||||||
| if (!test.id) { | ||||||
| test.id = generateTestId(normalizedSchema, test.data, test.valid); | ||||||
| changed = true; | ||||||
| added++; | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if (changed) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you really need a |
||||||
| fs.writeFileSync(filePath, JSON.stringify(tests, null, 2) + "\n"); | ||||||
| console.log(`✓ Added ${added} IDs`); | ||||||
| } else { | ||||||
| console.log("✓ All tests already have IDs"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Load remotes for all dialects | ||||||
| const remotesPaths = ["./remotes"]; | ||||||
| for (const dialectUri of Object.values(DIALECT_MAP)) { | ||||||
| for (const path of remotesPaths) { | ||||||
| if (fs.existsSync(path)) { | ||||||
| loadRemotes(dialectUri, path); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
Comment on lines
+63
to
+71
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're not going to be able to add remotes for all dialects at the same time. You're going to have to run this script separately for each dialect. Most remotes don't have I suggest making the first argument of the script the local draft identifier, then convert that to the right dialect URI to pass to |
||||||
|
|
||||||
| const filePath = process.argv[2] || "tests/draft2020-12/enum.json"; | ||||||
| addIdsToFile(filePath).catch(console.error); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The catch is unnecessary here. Just let it throw if there's an error. |
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| import * as fs from "node:fs"; | ||
| import * as path from "node:path"; | ||
| import * as crypto from "node:crypto"; | ||
| import jsonStringify from "json-stringify-deterministic"; | ||
| import { normalize } from "./normalize.js"; | ||
| import { loadRemotes } from "./load-remotes.js"; | ||
|
|
||
| const DIALECT_MAP = { | ||
| "https://json-schema.org/draft/2020-12/schema": "https://json-schema.org/draft/2020-12/schema", | ||
| "https://json-schema.org/draft/2019-09/schema": "https://json-schema.org/draft/2019-09/schema", | ||
| "http://json-schema.org/draft-07/schema#": "http://json-schema.org/draft-07/schema#", | ||
| "http://json-schema.org/draft-06/schema#": "http://json-schema.org/draft-06/schema#", | ||
| "http://json-schema.org/draft-04/schema#": "http://json-schema.org/draft-04/schema#" | ||
| }; | ||
|
|
||
| function* jsonFiles(dir) { | ||
| for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { | ||
| const full = path.join(dir, entry.name); | ||
| if (entry.isDirectory()) { | ||
| yield* jsonFiles(full); | ||
| } else if (entry.isFile() && entry.name.endsWith(".json")) { | ||
| yield full; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function getDialectUri(schema) { | ||
| if (schema.$schema && DIALECT_MAP[schema.$schema]) { | ||
| return DIALECT_MAP[schema.$schema]; | ||
| } | ||
| return "https://json-schema.org/draft/2020-12/schema"; | ||
| } | ||
|
|
||
| function generateTestId(normalizedSchema, testData, testValid) { | ||
| return crypto | ||
| .createHash("md5") | ||
| .update(jsonStringify(normalizedSchema) + jsonStringify(testData) + testValid) | ||
| .digest("hex"); | ||
| } | ||
|
|
||
| async function checkVersion(dir) { | ||
| const missingIdFiles = new Set(); | ||
| const duplicateIdFiles = new Set(); | ||
| const mismatchedIdFiles = new Set(); | ||
| const idMap = new Map(); | ||
|
|
||
| console.log(`Checking tests in ${dir}...`); | ||
|
|
||
| for (const file of jsonFiles(dir)) { | ||
| const tests = JSON.parse(fs.readFileSync(file, "utf8")); | ||
|
|
||
| for (let i = 0; i < tests.length; i++) { | ||
| const testCase = tests[i]; | ||
| if (!Array.isArray(testCase.tests)) continue; | ||
|
|
||
| const dialectUri = getDialectUri(testCase.schema || {}); | ||
| const normalizedSchema = await normalize(testCase.schema || true, dialectUri); | ||
|
|
||
| for (let j = 0; j < testCase.tests.length; j++) { | ||
| const test = testCase.tests[j]; | ||
|
|
||
| if (!test.id) { | ||
| missingIdFiles.add(file); | ||
| console.log(` ✗ Missing ID: ${file} | ${testCase.description} | ${test.description}`); | ||
| continue; | ||
| } | ||
|
|
||
| const expectedId = generateTestId(normalizedSchema, test.data, test.valid); | ||
|
|
||
| if (test.id !== expectedId) { | ||
| mismatchedIdFiles.add(file); | ||
| console.log(` ✗ Mismatched ID: ${file}`); | ||
| console.log(` Test: ${testCase.description} | ${test.description}`); | ||
| console.log(` Current ID: ${test.id}`); | ||
| console.log(` Expected ID: ${expectedId}`); | ||
| } | ||
|
|
||
| if (idMap.has(test.id)) { | ||
| const existing = idMap.get(test.id); | ||
| duplicateIdFiles.add(file); | ||
| duplicateIdFiles.add(existing.file); | ||
| console.log(` ✗ Duplicate ID: ${test.id}`); | ||
| console.log(` First: ${existing.file} | ${existing.testCase} | ${existing.test}`); | ||
| console.log(` Second: ${file} | ${testCase.description} | ${test.description}`); | ||
| } else { | ||
| idMap.set(test.id, { | ||
| file, | ||
| testCase: testCase.description, | ||
| test: test.description | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| console.log("\n" + "=".repeat(60)); | ||
| console.log("Summary:"); | ||
| console.log("=".repeat(60)); | ||
|
|
||
| console.log("\nFiles with missing IDs:"); | ||
| if (missingIdFiles.size === 0) { | ||
| console.log(" ✓ None"); | ||
| } else { | ||
| for (const f of missingIdFiles) console.log(` - ${f}`); | ||
| } | ||
|
|
||
| console.log("\nFiles with mismatched IDs:"); | ||
| if (mismatchedIdFiles.size === 0) { | ||
| console.log(" ✓ None"); | ||
| } else { | ||
| for (const f of mismatchedIdFiles) console.log(` - ${f}`); | ||
| } | ||
|
|
||
| console.log("\nFiles with duplicate IDs:"); | ||
| if (duplicateIdFiles.size === 0) { | ||
| console.log(" ✓ None"); | ||
| } else { | ||
| for (const f of duplicateIdFiles) console.log(` - ${f}`); | ||
| } | ||
|
|
||
| const hasErrors = missingIdFiles.size > 0 || mismatchedIdFiles.size > 0 || duplicateIdFiles.size > 0; | ||
|
|
||
| console.log("\n" + "=".repeat(60)); | ||
| if (hasErrors) { | ||
| console.log("❌ Check failed - issues found"); | ||
| process.exit(1); | ||
| } else { | ||
| console.log("✅ All checks passed!"); | ||
| } | ||
| } | ||
|
|
||
| // Load remotes | ||
| const remotesPaths = ["./remotes"]; | ||
| for (const dialectUri of Object.values(DIALECT_MAP)) { | ||
| for (const path of remotesPaths) { | ||
| if (fs.existsSync(path)) { | ||
| loadRemotes(dialectUri, path); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const dir = process.argv[2] || "tests/draft2020-12"; | ||
| checkVersion(dir).catch(console.error); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| // scripts/load-remotes.js | ||
| import * as fs from "node:fs"; | ||
| import { toAbsoluteIri } from "@hyperjump/uri"; | ||
| import { registerSchema } from "@hyperjump/json-schema/draft-2020-12"; | ||
|
|
||
| // Keep track of which remote URLs we've already registered | ||
| const loadedRemotes = new Set(); | ||
|
|
||
| export const loadRemotes = (dialectId, filePath, url = "") => { | ||
| if (!fs.existsSync(filePath)) { | ||
| console.warn(`Warning: Remotes path not found: ${filePath}`); | ||
| return; | ||
| } | ||
|
|
||
| fs.readdirSync(filePath, { withFileTypes: true }).forEach((entry) => { | ||
| if (entry.isFile() && entry.name.endsWith(".json")) { | ||
| const remotePath = `${filePath}/${entry.name}`; | ||
| const remoteUrl = `http://localhost:1234${url}/${entry.name}`; | ||
|
|
||
| // If we've already registered this URL once, skip it | ||
| if (loadedRemotes.has(remoteUrl)) { | ||
| return; | ||
| } | ||
|
|
||
| const remote = JSON.parse(fs.readFileSync(remotePath, "utf8")); | ||
|
|
||
| // Only register if $schema matches dialect OR there's no $schema | ||
| if (!remote.$schema || toAbsoluteIri(remote.$schema) === dialectId) { | ||
| registerSchema(remote, remoteUrl, dialectId); | ||
| loadedRemotes.add(remoteUrl); // ✅ Remember we've registered it | ||
| } | ||
| } else if (entry.isDirectory()) { | ||
| loadRemotes(dialectId, `${filePath}/${entry.name}`, `${url}/${entry.name}`); | ||
| } | ||
| }); | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.
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.
There are a couple of issues here.
First, you can't use 2020-12 as the default dialect. The default needs to be the dialect associated with the directory the test file is in. For example, the convention to use
$schemain every test case schema was only adopted starting in 2019-09. So, tests for draft-04/6/7 don't have$schema. That means this script will treat all of those tests as 2020-12 schemas, which would be very wrong.This whole function should be unnecessary anyway. This is already handled internally by
@hyperjump/json-schema. The result of this function is only used to pass to theregisterSchemafunction, which should be the dialect from the directory the test file is in, not from the schema.