Skip to content

Commit bb2cfe9

Browse files
committed
Update the schema test coverage tool to use an EvaluationPlugin
1 parent 7f70839 commit bb2cfe9

1 file changed

Lines changed: 66 additions & 63 deletions

File tree

scripts/schema-test-coverage.mjs

Lines changed: 66 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,14 @@ import { readdir, readFile } from "node:fs/promises";
22
import YAML from "yaml";
33
import { join } from "node:path";
44
import { argv } from "node:process";
5-
import "@hyperjump/json-schema/draft-2020-12";
5+
import { validate } from "@hyperjump/json-schema/draft-2020-12";
66
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";
7+
import { BASIC } from "@hyperjump/json-schema/experimental";
158

169
/**
17-
* @import { AST } from "@hyperjump/json-schema/experimental"
18-
* @import { Json } from "@hyperjump/json-schema"
10+
* @import { SchemaObject } from "@hyperjump/json-schema/draft-2020-12"
11+
* @import { EvaluationPlugin } from "@hyperjump/json-schema/experimental"
12+
* @import { Json } from "@hyperjump/json-pointer"
1913
*/
2014

2115
import contentTypeParser from "content-type";
@@ -30,12 +24,48 @@ addMediaTypePlugin("application/schema+yaml", {
3024
const contextDialectId =
3125
contentType.parameters.schema ?? contentType.parameters.profile;
3226

27+
/** @type SchemaObject */
3328
const foo = YAML.parse(await response.text());
3429
return buildSchemaDocument(foo, response.url, contextDialectId);
3530
},
3631
fileMatcher: (path) => path.endsWith(".yaml"),
3732
});
3833

34+
/** @implements EvaluationPlugin */
35+
class TestCoveragePlugin {
36+
constructor() {
37+
/** @type Set<string> */
38+
this.visitedLocations = new Set();
39+
}
40+
41+
beforeSchema(_schemaUri, _instance, context) {
42+
if (this.allLocations) {
43+
return;
44+
}
45+
46+
/** @type Set<string> */
47+
this.allLocations = [];
48+
49+
for (const schemaLocation in context.ast) {
50+
if (schemaLocation === "metaData") {
51+
continue;
52+
}
53+
54+
if (Array.isArray(context.ast[schemaLocation])) {
55+
for (const keyword of context.ast[schemaLocation]) {
56+
if (Array.isArray(keyword)) {
57+
this.allLocations.push(keyword[1]);
58+
}
59+
}
60+
}
61+
}
62+
}
63+
64+
beforeKeyword([, schemaUri]) {
65+
this.visitedLocations.add(schemaUri);
66+
}
67+
}
68+
3969
/** @type (testDirectory: string) => AsyncGenerator<[string,Json]> */
4070
const tests = async function* (testDirectory) {
4171
for (const file of await readdir(testDirectory, {
@@ -53,70 +83,43 @@ const tests = async function* (testDirectory) {
5383
}
5484
};
5585

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);
86+
/**
87+
* @typedef {{
88+
* allLocations: string[];
89+
* visitedLocations: Set<string>;
90+
* }} Coverage
91+
*/
6092

61-
const result = interpret(compiled, instance, BASIC);
93+
/** @type (schemaUri: string, testDirectory: string) => Promise<Coverage> */
94+
const runTests = async (schemaUri, testDirectory) => {
95+
const testCoveragePlugin = new TestCoveragePlugin();
96+
const validateOpenApi = await validate(schemaUri);
97+
98+
for await (const [name, test] of tests(testDirectory)) {
99+
const result = validateOpenApi(test, {
100+
outputFormat: BASIC,
101+
plugins: [testCoveragePlugin],
102+
});
62103

63104
if (!result.valid) {
64105
console.log("Failed:", name, result.errors);
65106
}
66107
}
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-
}
86108

87-
return locations;
109+
return {
110+
allLocations: testCoveragePlugin.allLocations ?? new Set(),
111+
visitedLocations: testCoveragePlugin.visitedLocations
112+
};
88113
};
89114

90115
///////////////////////////////////////////////////////////////////////////////
91116

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, context) => {
99-
if (Array.isArray(context.ast[url])) {
100-
for (const keywordNode of context.ast[url]) {
101-
if (Array.isArray(keywordNode)) {
102-
visitedLocations.add(keywordNode[1]);
103-
}
104-
}
105-
}
106-
return baseInterpret(url, instance, context);
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(
117+
const { allLocations, visitedLocations } = await runTests(argv[2], argv[3]);
118+
const notCovered = allLocations.filter(
116119
(location) => !visitedLocations.has(location),
117120
);
118121
if (notCovered.length > 0) {
119-
console.log("NOT Covered:", notCovered.length, "of", allKeywords.length);
122+
console.log("NOT Covered:", notCovered.length, "of", allLocations.length);
120123
const maxNotCovered = 20;
121124
const firstNotCovered = notCovered.slice(0, maxNotCovered);
122125
if (notCovered.length > maxNotCovered) firstNotCovered.push("...");
@@ -127,6 +130,6 @@ console.log(
127130
"Covered:",
128131
visitedLocations.size,
129132
"of",
130-
allKeywords.length,
131-
"(" + Math.floor((visitedLocations.size / allKeywords.length) * 100) + "%)",
133+
allLocations.length,
134+
"(" + Math.floor((visitedLocations.size / allLocations.length) * 100) + "%)"
132135
);

0 commit comments

Comments
 (0)