Skip to content

Commit 50103d8

Browse files
authored
Merge pull request #4173 from ralfhandl/main-schema-test-coverage
main: schema test coverage
2 parents 876e1b1 + 31f66e7 commit 50103d8

File tree

3 files changed

+151
-1
lines changed

3 files changed

+151
-1
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"license": "Apache-2.0",
1515
"scripts": {
1616
"build": "bash ./scripts/md2html/build.sh",
17-
"test": "c8 --100 vitest --watch=false"
17+
"test": "c8 --100 vitest --watch=false && bash scripts/schema-test-coverage.sh"
1818
},
1919
"readmeFilename": "README.md",
2020
"files": [

scripts/schema-test-coverage.mjs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { readdir, readFile } from "node:fs/promises";
2+
import YAML from "yaml";
3+
import { join } from "node:path";
4+
import { argv } from "node:process";
5+
import "@hyperjump/json-schema/draft-2020-12";
6+
import "@hyperjump/json-schema/draft-04";
7+
import {
8+
compile,
9+
getSchema,
10+
interpret,
11+
Validation,
12+
BASIC,
13+
} from "@hyperjump/json-schema/experimental";
14+
import * as Instance from "@hyperjump/json-schema/instance/experimental";
15+
16+
/**
17+
* @import { AST } from "@hyperjump/json-schema/experimental"
18+
* @import { Json } from "@hyperjump/json-schema"
19+
*/
20+
21+
import contentTypeParser from "content-type";
22+
import { addMediaTypePlugin } from "@hyperjump/browser";
23+
import { buildSchemaDocument } from "@hyperjump/json-schema/experimental";
24+
25+
addMediaTypePlugin("application/schema+yaml", {
26+
parse: async (response) => {
27+
const contentType = contentTypeParser.parse(
28+
response.headers.get("content-type") ?? "",
29+
);
30+
const contextDialectId =
31+
contentType.parameters.schema ?? contentType.parameters.profile;
32+
33+
const foo = YAML.parse(await response.text());
34+
return buildSchemaDocument(foo, response.url, contextDialectId);
35+
},
36+
fileMatcher: (path) => path.endsWith(".yaml"),
37+
});
38+
39+
/** @type (testDirectory: string) => AsyncGenerator<[string,Json]> */
40+
const tests = async function* (testDirectory) {
41+
for (const file of await readdir(testDirectory, {
42+
recursive: true,
43+
withFileTypes: true,
44+
})) {
45+
if (!file.isFile() || !file.name.endsWith(".yaml")) {
46+
continue;
47+
}
48+
49+
const testPath = join(file.parentPath, file.name);
50+
const testJson = await readFile(testPath, "utf8");
51+
52+
yield [testPath, YAML.parse(testJson)];
53+
}
54+
};
55+
56+
/** @type (testDirectory: string) => Promise<void> */
57+
const runTests = async (testDirectory) => {
58+
for await (const [name, test] of tests(testDirectory)) {
59+
const instance = Instance.fromJs(test);
60+
61+
const result = interpret(compiled, instance, BASIC);
62+
63+
if (!result.valid) {
64+
console.log("Failed:", name, result.errors);
65+
}
66+
}
67+
};
68+
69+
/** @type (ast: AST) => string[] */
70+
const keywordLocations = (ast) => {
71+
/** @type string[] */
72+
const locations = [];
73+
for (const schemaLocation in ast) {
74+
if (schemaLocation === "metaData") {
75+
continue;
76+
}
77+
78+
if (Array.isArray(ast[schemaLocation])) {
79+
for (const keyword of ast[schemaLocation]) {
80+
if (Array.isArray(keyword)) {
81+
locations.push(keyword[1]);
82+
}
83+
}
84+
}
85+
}
86+
87+
return locations;
88+
};
89+
90+
///////////////////////////////////////////////////////////////////////////////
91+
92+
const schema = await getSchema(argv[2]);
93+
const compiled = await compile(schema);
94+
95+
/** @type Set<string> */
96+
const visitedLocations = new Set();
97+
const baseInterpret = Validation.interpret;
98+
Validation.interpret = (url, instance, ast, dynamicAnchors, quiet) => {
99+
if (Array.isArray(ast[url])) {
100+
for (const keywordNode of ast[url]) {
101+
if (Array.isArray(keywordNode)) {
102+
visitedLocations.add(keywordNode[1]);
103+
}
104+
}
105+
}
106+
return baseInterpret(url, instance, ast, dynamicAnchors, quiet);
107+
};
108+
109+
await runTests(argv[3]);
110+
Validation.interpret = baseInterpret;
111+
112+
// console.log("Covered:", visitedLocations);
113+
114+
const allKeywords = keywordLocations(compiled.ast);
115+
const notCovered = allKeywords.filter(
116+
(location) => !visitedLocations.has(location),
117+
);
118+
if (notCovered.length > 0) {
119+
console.log("NOT Covered:", notCovered.length, "of", allKeywords.length);
120+
const maxNotCovered = 20;
121+
const firstNotCovered = notCovered.slice(0, maxNotCovered);
122+
if (notCovered.length > maxNotCovered) firstNotCovered.push("...");
123+
console.log(firstNotCovered);
124+
}
125+
126+
console.log(
127+
"Covered:",
128+
visitedLocations.size,
129+
"of",
130+
allKeywords.length,
131+
"(" + Math.floor((visitedLocations.size / allKeywords.length) * 100) + "%)",
132+
);

scripts/schema-test-coverage.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bash
2+
3+
# Author: @ralfhandl
4+
5+
# Run this script from the root of the repo
6+
7+
echo
8+
echo "Schema Test Coverage"
9+
echo
10+
11+
for schemaDir in schemas/v3* ; do
12+
version=$(basename "$schemaDir")
13+
echo $version
14+
15+
node scripts/schema-test-coverage.mjs $schemaDir/schema.yaml tests/$version/pass
16+
17+
echo
18+
done

0 commit comments

Comments
 (0)