@@ -19,6 +19,17 @@ import jsonlint from '@prantlf/jsonlint'
1919import * as jsoncParser from 'jsonc-parser'
2020import ora from 'ora'
2121import chalk from 'chalk'
22+ import {
23+ checkUnusedDefs ,
24+ checkDescriptionCoverage ,
25+ checkTestCompleteness ,
26+ checkEnumCoverage ,
27+ checkPatternCoverage ,
28+ checkRequiredCoverage ,
29+ checkDefaultCoverage ,
30+ checkNegativeIsolation ,
31+ printCoverageReport ,
32+ } from './src/helpers/coverage.js'
2233import minimist from 'minimist'
2334import fetch , { FetchError } from 'node-fetch'
2435import { execFile } from 'node:child_process'
@@ -144,6 +155,7 @@ if (argv.SchemaName) {
144155 * @property {string[] } highSchemaVersion
145156 * @property {string[] } missingCatalogUrl
146157 * @property {string[] } skiptest
158+ * @property {{schema: string, strict?: boolean}[] } coverage
147159 * @property {string[] } catalogEntryNoLintNameOrDescription
148160 * @property {Record<string, SchemaValidationJsonOption> } options
149161 */
@@ -1481,6 +1493,10 @@ async function assertSchemaValidationJsonReferencesNoNonexistentFiles() {
14811493 schemaNamesMustExist ( SchemaValidation . skiptest , 'skiptest' )
14821494 schemaNamesMustExist ( SchemaValidation . missingCatalogUrl , 'missingCatalogUrl' )
14831495 schemaNamesMustExist ( SchemaValidation . highSchemaVersion , 'highSchemaVersion' )
1496+ schemaNamesMustExist (
1497+ ( SchemaValidation . coverage ?? [ ] ) . map ( ( c ) => c . schema ) ,
1498+ 'coverage' ,
1499+ )
14841500 for ( const schemaName in SchemaValidation . options ) {
14851501 if ( ! SchemasToBeTested . includes ( schemaName ) ) {
14861502 printErrorAndExit ( new Error ( ) , [
@@ -2060,6 +2076,7 @@ TASKS:
20602076 check-remote: Run all build checks for remote schemas
20612077 maintenance: Run maintenance checks
20622078 build-xregistry: Build the xRegistry from the catalog.json
2079+ coverage: Run test coverage analysis on opted-in schemas
20632080
20642081EXAMPLES:
20652082 node ./cli.js check
@@ -2132,6 +2149,113 @@ EXAMPLES:
21322149 }
21332150 }
21342151
2152+ // ---------------------------------------------------------------------------
2153+ // Coverage task
2154+ // ---------------------------------------------------------------------------
2155+
2156+ async function taskCoverage ( ) {
2157+ const coverageSchemas = SchemaValidation . coverage ?? [ ]
2158+ if ( coverageSchemas . length === 0 ) {
2159+ console . info (
2160+ 'No schemas opted into coverage. Add schemas to "coverage" in schema-validation.jsonc' ,
2161+ )
2162+ return
2163+ }
2164+
2165+ const spinner = ora ( )
2166+ spinner . start ( )
2167+ let hasFailure = false
2168+ let hasMatch = false
2169+
2170+ for ( const entry of coverageSchemas ) {
2171+ const schemaName = entry . schema
2172+ const strict = entry . strict ?? false
2173+ if ( argv [ 'schema-name' ] && argv [ 'schema-name' ] !== schemaName ) {
2174+ continue
2175+ }
2176+ hasMatch = true
2177+
2178+ const schemaId = schemaName . replace ( '.json' , '' )
2179+ spinner . text = `Running coverage checks on "${ schemaName } "${ strict ? ' (strict)' : '' } `
2180+
2181+ // Load schema
2182+ const schemaFile = await toFile ( path . join ( SchemaDir , schemaName ) )
2183+ const schema = /** @type {Record<string, unknown> } */ ( schemaFile . json )
2184+
2185+ // Load positive test files
2186+ const positiveTests = new Map ( )
2187+ const posDir = path . join ( TestPositiveDir , schemaId )
2188+ for ( const testfile of await fs . readdir ( posDir ) . catch ( ( ) => [ ] ) ) {
2189+ if ( isIgnoredFile ( testfile ) ) continue
2190+ const file = await toFile ( path . join ( posDir , testfile ) )
2191+ positiveTests . set ( testfile , file . json )
2192+ }
2193+
2194+ // Load negative test files
2195+ const negativeTests = new Map ( )
2196+ const negDir = path . join ( TestNegativeDir , schemaId )
2197+ for ( const testfile of await fs . readdir ( negDir ) . catch ( ( ) => [ ] ) ) {
2198+ if ( isIgnoredFile ( testfile ) ) continue
2199+ const file = await toFile ( path . join ( negDir , testfile ) )
2200+ negativeTests . set ( testfile , file . json )
2201+ }
2202+
2203+ // Run all 8 checks
2204+ const results = [
2205+ { name : '1. Unused $defs' , result : checkUnusedDefs ( schema ) } ,
2206+ {
2207+ name : '2. Description Coverage' ,
2208+ result : checkDescriptionCoverage ( schema ) ,
2209+ } ,
2210+ {
2211+ name : '3. Test Completeness' ,
2212+ result : checkTestCompleteness ( schema , positiveTests ) ,
2213+ } ,
2214+ {
2215+ name : '4. Enum Coverage' ,
2216+ result : checkEnumCoverage ( schema , positiveTests , negativeTests ) ,
2217+ } ,
2218+ {
2219+ name : '5. Pattern Coverage' ,
2220+ result : checkPatternCoverage ( schema , positiveTests , negativeTests ) ,
2221+ } ,
2222+ {
2223+ name : '6. Required Field Coverage' ,
2224+ result : checkRequiredCoverage ( schema , negativeTests ) ,
2225+ } ,
2226+ {
2227+ name : '7. Default Value Coverage' ,
2228+ result : checkDefaultCoverage ( schema , positiveTests ) ,
2229+ } ,
2230+ {
2231+ name : '8. Negative Test Isolation' ,
2232+ result : checkNegativeIsolation ( schema , negativeTests ) ,
2233+ } ,
2234+ ]
2235+
2236+ spinner . stop ( )
2237+ printCoverageReport ( schemaName , results )
2238+ if ( strict && results . some ( ( r ) => r . result . status === 'fail' ) )
2239+ hasFailure = true
2240+
2241+ // Restart spinner for next schema
2242+ if ( coverageSchemas . indexOf ( entry ) < coverageSchemas . length - 1 ) {
2243+ spinner . start ( )
2244+ }
2245+ }
2246+
2247+ if ( ! hasMatch ) {
2248+ spinner . stop ( )
2249+ printErrorAndExit ( null , [
2250+ `Schema "${ argv [ 'schema-name' ] } " is not in the coverage list in "${ SchemaValidationFile } "` ,
2251+ ] )
2252+ }
2253+
2254+ if ( hasFailure ) {
2255+ process . exit ( 1 )
2256+ }
2257+ }
2258+
21352259 /** @type {Record<string, () => Promise<unknown>> } */
21362260 const taskMap = {
21372261 'new-schema' : taskNewSchema ,
@@ -2143,6 +2267,7 @@ EXAMPLES:
21432267 maintenance : taskMaintenance ,
21442268 'build-website' : taskBuildWebsite ,
21452269 'build-xregistry' : taskBuildXRegistry ,
2270+ coverage : taskCoverage ,
21462271 build : taskCheck , // Undocumented alias.
21472272 }
21482273 const taskOrFn = argv . _ [ 0 ]
0 commit comments