Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion package.json
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"
}
}
73 changes: 73 additions & 0 deletions scripts/add-test-ids.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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 = {
"draft2020-12": "https://json-schema.org/draft/2020-12/schema",
"draft2019-09": "https://json-schema.org/draft/2019-09/schema",
"draft7": "http://json-schema.org/draft-07/schema#",
"draft6": "http://json-schema.org/draft-06/schema#",
"draft4": "http://json-schema.org/draft-04/schema#"
};

function generateTestId(normalizedSchema, testData, testValid) {
return crypto
.createHash("md5")
.update(jsonStringify(normalizedSchema) + jsonStringify(testData) + testValid)
.digest("hex");
}

async function addIdsToFile(filePath, dialectUri) {
console.log("Reading:", filePath);
const tests = JSON.parse(fs.readFileSync(filePath, "utf8"));
let added = 0;

for (const testCase of tests) {
// Pass dialectUri from directory, not from schema
// @hyperjump/json-schema handles the schema's $schema internally
const normalizedSchema = await normalize(testCase.schema, dialectUri);

for (const test of testCase.tests) {
if (!test.id) {
test.id = generateTestId(normalizedSchema, test.data, test.valid);
added++;
}
}
}

if (added > 0) {
fs.writeFileSync(filePath, JSON.stringify(tests, null, 4) + "\n");
console.log(` Added ${added} IDs`);
} else {
console.log(" All tests already have IDs");
}
}

// Get dialect from command line argument (e.g., "draft2020-12")
const dialectArg = process.argv[2];
if (!dialectArg || !DIALECT_MAP[dialectArg]) {
console.error("Usage: node add-test-ids.js <dialect> [file-path]");
console.error("Available dialects:", Object.keys(DIALECT_MAP).join(", "));
process.exit(1);
}

const dialectUri = DIALECT_MAP[dialectArg];
const filePath = process.argv[3];

// Load remotes only for the specified dialect
loadRemotes(dialectUri, "./remotes");

if (filePath) {
// Process single file
addIdsToFile(filePath, dialectUri);
} else {
// Process all files in the dialect directory
const testDir = `tests/${dialectArg}`;
const files = fs.readdirSync(testDir).filter(f => f.endsWith('.json'));

for (const file of files) {
await addIdsToFile(`${testDir}/${file}`, dialectUri);
}
}
143 changes: 143 additions & 0 deletions scripts/check-test-ids.js
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);
36 changes: 36 additions & 0 deletions scripts/load-remotes.js
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}`);
}
});
};
Loading
Loading