Skip to content

Commit 5b459bb

Browse files
sirdeggenclaude
andcommitted
feat: expand SDK vectors and update conformance runner for subdirectory layout
- Move flat sdk/*.json vectors into typed subdirs: crypto/, keys/, scripts/, transactions/ - Expand each vector file from ~4-8 vectors to 15-24 vectors by extracting real input/output pairs from ECDSA, BRC42, Script, and Transaction test suites - Add required envelope fields ($schema, id, name, brc, version, reference_impl, parity_class) and per-vector id/tags to all new files - Rewrite runner to recursively discover *.json, validate envelope + vector fields, emit per-suite JUnit XML (testsuites wrapper) and JSON report - Add validate and report npm scripts to conformance/runner/package.json - Runner exits 0 when all files parse cleanly, 1 on any structural errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 043f877 commit 5b459bb

16 files changed

Lines changed: 2013 additions & 379 deletions

File tree

conformance/runner/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"description": "BSV ts-stack conformance test vector runner",
66
"type": "module",
77
"scripts": {
8-
"test": "node --experimental-vm-modules src/runner.js",
8+
"test": "node src/runner.js",
9+
"validate": "node src/runner.js --validate-only",
10+
"report": "node src/runner.js --report conformance/reports/results.xml",
911
"build": "echo 'no build step'"
1012
},
1113
"devDependencies": {

conformance/runner/src/runner.js

Lines changed: 264 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,134 @@
22
* BSV Conformance Runner
33
*
44
* Loads test vectors from conformance/vectors/**\/*.json,
5-
* executes them against the TypeScript implementations,
6-
* and emits JUnit XML + JSON reports.
5+
* validates their structure, and emits JUnit XML + JSON reports.
76
*
87
* MBGA §8.5: per-language vector runner.
98
*
109
* Usage:
11-
* node src/runner.js [--vectors <glob>] [--output <dir>]
10+
* node src/runner.js [--validate-only] [--report <path>] [--vectors <dir>]
11+
*
12+
* Exit codes:
13+
* 0 all vector files parsed cleanly
14+
* 1 one or more parse / validation errors
1215
*/
1316

1417
import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises'
15-
import { join, resolve } from 'node:path'
18+
import { join, resolve, dirname } from 'node:path'
19+
import { fileURLToPath } from 'node:url'
20+
21+
// ---------------------------------------------------------------------------
22+
// Paths
23+
// ---------------------------------------------------------------------------
24+
25+
const __filename = fileURLToPath(import.meta.url)
26+
const __dirname = dirname(__filename)
27+
28+
const VECTORS_DIR = resolve(__dirname, '../../vectors')
29+
const REPORT_DIR = resolve(__dirname, '../reports')
30+
31+
// ---------------------------------------------------------------------------
32+
// CLI args
33+
// ---------------------------------------------------------------------------
1634

17-
const VECTORS_DIR = resolve(import.meta.dirname, '../../vectors')
18-
const REPORT_DIR = resolve(import.meta.dirname, '../reports')
35+
function parseArgs (argv) {
36+
const args = argv.slice(2)
37+
const opts = {
38+
validateOnly: false,
39+
reportPath: null,
40+
vectorsDir: VECTORS_DIR
41+
}
42+
for (let i = 0; i < args.length; i++) {
43+
if (args[i] === '--validate-only') opts.validateOnly = true
44+
else if (args[i] === '--report' && args[i + 1]) opts.reportPath = resolve(args[++i])
45+
else if (args[i] === '--vectors' && args[i + 1]) opts.vectorsDir = resolve(args[++i])
46+
}
47+
return opts
48+
}
1949

20-
async function loadVectors (dir) {
21-
const entries = await readdir(dir, { withFileTypes: true, recursive: true })
22-
const vectors = []
50+
// ---------------------------------------------------------------------------
51+
// Vector file loading — recursive glob over all *.json
52+
// ---------------------------------------------------------------------------
53+
54+
async function findJsonFiles (dir) {
55+
const results = []
56+
let entries
57+
try {
58+
entries = await readdir(dir, { withFileTypes: true })
59+
} catch {
60+
return results
61+
}
2362
for (const entry of entries) {
24-
if (entry.isFile() && entry.name.endsWith('.json')) {
25-
const path = join(entry.parentPath ?? entry.path, entry.name)
26-
const raw = await readFile(path, 'utf-8')
27-
vectors.push({ path, data: JSON.parse(raw) })
63+
const full = join(dir, entry.name)
64+
if (entry.isDirectory()) {
65+
const sub = await findJsonFiles(full)
66+
results.push(...sub)
67+
} else if (entry.isFile() && entry.name.endsWith('.json')) {
68+
results.push(full)
2869
}
2970
}
30-
return vectors
71+
return results
3172
}
3273

33-
function toJUnit (results) {
34-
const total = results.length
35-
const failed = results.filter(r => !r.pass).length
36-
const cases = results.map(r => {
37-
if (r.pass) {
38-
return ` <testcase name="${escXml(r.name)}" classname="${escXml(r.suite)}" time="0"/>`
74+
// ---------------------------------------------------------------------------
75+
// Validation helpers
76+
// ---------------------------------------------------------------------------
77+
78+
const REQUIRED_TOP_LEVEL = ['vectors']
79+
const RECOMMENDED_TOP_LEVEL = ['$schema', 'id', 'name', 'brc', 'version', 'reference_impl', 'parity_class']
80+
const REQUIRED_VECTOR_FIELDS = ['id', 'input', 'expected']
81+
82+
function validateFile (path, data) {
83+
const errors = []
84+
85+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
86+
// Legacy format: bare array of vectors (no top-level envelope)
87+
if (Array.isArray(data)) {
88+
errors.push(`WARN: ${path} uses legacy bare-array format — wrap in { "vectors": [...] } envelope`)
89+
return { errors, vectors: data }
3990
}
40-
return [
41-
` <testcase name="${escXml(r.name)}" classname="${escXml(r.suite)}" time="0">`,
42-
` <failure message="${escXml(r.error)}">${escXml(r.error)}</failure>`,
43-
' </testcase>'
44-
].join('\n')
45-
}).join('\n')
91+
errors.push(`ERROR: ${path} top level must be an object or array`)
92+
return { errors, vectors: [] }
93+
}
4694

47-
return [
48-
'<?xml version="1.0" encoding="UTF-8"?>',
49-
`<testsuite name="bsv-conformance" tests="${total}" failures="${failed}" errors="0" skipped="0">`,
50-
cases,
51-
'</testsuite>'
52-
].join('\n')
95+
// Check required top-level fields
96+
for (const field of REQUIRED_TOP_LEVEL) {
97+
if (!(field in data)) {
98+
errors.push(`ERROR: ${path} missing required top-level field "${field}"`)
99+
}
100+
}
101+
102+
// Warn on missing recommended fields
103+
for (const field of RECOMMENDED_TOP_LEVEL) {
104+
if (!(field in data)) {
105+
errors.push(`WARN: ${path} missing recommended top-level field "${field}"`)
106+
}
107+
}
108+
109+
const vectors = Array.isArray(data.vectors) ? data.vectors : []
110+
111+
if (!Array.isArray(data.vectors)) {
112+
errors.push(`ERROR: ${path} "vectors" must be an array`)
113+
return { errors, vectors: [] }
114+
}
115+
116+
return { errors, vectors }
53117
}
54118

119+
function validateVector (filePath, vec, index) {
120+
const errors = []
121+
for (const field of REQUIRED_VECTOR_FIELDS) {
122+
if (!(field in vec)) {
123+
errors.push(`ERROR: ${filePath} vector[${index}] missing required field "${field}"`)
124+
}
125+
}
126+
return errors
127+
}
128+
129+
// ---------------------------------------------------------------------------
130+
// JUnit XML helpers
131+
// ---------------------------------------------------------------------------
132+
55133
function escXml (s = '') {
56134
return String(s)
57135
.replace(/&/g, '&amp;')
@@ -60,42 +138,172 @@ function escXml (s = '') {
60138
.replace(/"/g, '&quot;')
61139
}
62140

141+
function toJUnit (suites) {
142+
const allCases = suites.flatMap(s => s.cases)
143+
const total = allCases.length
144+
const failed = allCases.filter(c => !c.pass).length
145+
146+
const suiteXml = suites.map(s => {
147+
const sTotal = s.cases.length
148+
const sFailed = s.cases.filter(c => !c.pass).length
149+
const casesXml = s.cases.map(c => {
150+
if (c.pass) {
151+
return ` <testcase name="${escXml(c.name)}" classname="${escXml(s.name)}" time="0"/>`
152+
}
153+
return [
154+
` <testcase name="${escXml(c.name)}" classname="${escXml(s.name)}" time="0">`,
155+
` <failure message="${escXml(c.error)}">${escXml(c.error)}</failure>`,
156+
' </testcase>'
157+
].join('\n')
158+
}).join('\n')
159+
160+
return [
161+
` <testsuite name="${escXml(s.name)}" tests="${sTotal}" failures="${sFailed}" errors="0" skipped="0">`,
162+
casesXml,
163+
' </testsuite>'
164+
].join('\n')
165+
}).join('\n')
166+
167+
return [
168+
'<?xml version="1.0" encoding="UTF-8"?>',
169+
`<testsuites name="bsv-conformance" tests="${total}" failures="${failed}" errors="0" skipped="0">`,
170+
suiteXml,
171+
'</testsuites>'
172+
].join('\n')
173+
}
174+
175+
// ---------------------------------------------------------------------------
176+
// Main
177+
// ---------------------------------------------------------------------------
178+
63179
async function run () {
64-
console.log(`Loading vectors from ${VECTORS_DIR}`)
65-
let vectorFiles
66-
try {
67-
vectorFiles = await loadVectors(VECTORS_DIR)
68-
} catch {
69-
console.log('No vectors found — nothing to run.')
180+
const opts = parseArgs(process.argv)
181+
const vectorsDir = opts.vectorsDir
182+
183+
console.log(`BSV Conformance Runner`)
184+
console.log(` Vectors dir : ${vectorsDir}`)
185+
console.log(` Mode : ${opts.validateOnly ? 'validate-only' : 'validate + report'}`)
186+
if (opts.reportPath) console.log(` Report path : ${opts.reportPath}`)
187+
console.log()
188+
189+
// Discover all JSON files
190+
const jsonFiles = await findJsonFiles(vectorsDir)
191+
192+
if (jsonFiles.length === 0) {
193+
console.log('No vector files found — nothing to validate.')
70194
process.exit(0)
71195
}
72196

73-
const results = []
197+
console.log(`Found ${jsonFiles.length} vector file(s)`)
74198

75-
for (const { path, data } of vectorFiles) {
76-
const suite = path.replace(VECTORS_DIR + '/', '').replace(/\.json$/, '')
77-
const cases = Array.isArray(data) ? data : data.vectors ?? []
199+
let totalVectors = 0
200+
let totalParseErrors = 0
201+
const suites = []
202+
const allErrors = []
78203

79-
for (const vec of cases) {
80-
const name = vec.description ?? vec.name ?? JSON.stringify(vec).slice(0, 60)
81-
// Vectors are self-validating: each has `input` and `expected` fields.
82-
// Domain-specific executors (imported per suite) do the actual check.
83-
// For now, mark as pending until executor is wired.
84-
results.push({ suite, name, pass: true, pending: true })
204+
for (const filePath of jsonFiles) {
205+
const relPath = filePath.replace(vectorsDir + '/', '')
206+
let raw, parsed
207+
208+
// Parse JSON
209+
try {
210+
raw = await readFile(filePath, 'utf-8')
211+
parsed = JSON.parse(raw)
212+
} catch (err) {
213+
const msg = `PARSE ERROR: ${filePath}: ${err.message}`
214+
allErrors.push(msg)
215+
totalParseErrors++
216+
console.log(` [FAIL] ${relPath}${err.message}`)
217+
continue
218+
}
219+
220+
// Validate file structure
221+
const { errors: fileErrors, vectors } = validateFile(filePath, parsed)
222+
223+
// Validate each vector
224+
const cases = []
225+
for (let i = 0; i < vectors.length; i++) {
226+
const vec = vectors[i]
227+
const vecErrors = validateVector(filePath, vec, i)
228+
const vecName = vec.id ?? vec.description ?? `vector[${i}]`
229+
230+
const isFatal = vecErrors.some(e => e.startsWith('ERROR:'))
231+
cases.push({
232+
name: vecName,
233+
pass: !isFatal,
234+
error: isFatal ? vecErrors.filter(e => e.startsWith('ERROR:')).join('; ') : null
235+
})
236+
if (isFatal) allErrors.push(...vecErrors.filter(e => e.startsWith('ERROR:')))
237+
}
238+
239+
const fatalFileErrors = fileErrors.filter(e => e.startsWith('ERROR:'))
240+
const warnFileErrors = fileErrors.filter(e => e.startsWith('WARN:'))
241+
242+
const suiteName = relPath.replace(/\.json$/, '')
243+
const suitePass = fatalFileErrors.length === 0
244+
245+
// If the file itself has fatal errors, add a synthetic failing case
246+
if (fatalFileErrors.length > 0) {
247+
cases.unshift({
248+
name: '_file_structure',
249+
pass: false,
250+
error: fatalFileErrors.join('; ')
251+
})
252+
allErrors.push(...fatalFileErrors)
253+
totalParseErrors++
85254
}
255+
256+
suites.push({ name: suiteName, cases })
257+
totalVectors += vectors.length
258+
259+
const status = (fatalFileErrors.length === 0 && cases.every(c => c.pass)) ? 'OK' : 'FAIL'
260+
const warnStr = warnFileErrors.length > 0 ? ` (${warnFileErrors.length} warn)` : ''
261+
console.log(` [${status}] ${relPath}${vectors.length} vector(s)${warnStr}`)
262+
263+
for (const w of warnFileErrors) console.log(` ${w}`)
264+
for (const e of fatalFileErrors) console.log(` ${e}`)
86265
}
87266

88-
await mkdir(REPORT_DIR, { recursive: true })
267+
console.log()
268+
console.log(`Summary`)
269+
console.log(` Total vector files : ${jsonFiles.length}`)
270+
console.log(` Total vectors : ${totalVectors}`)
271+
console.log(` Parse/structure errors : ${allErrors.filter(e => e.startsWith('ERROR:')).length}`)
272+
273+
// Write reports
274+
const reportDir = opts.reportPath ? dirname(opts.reportPath) : REPORT_DIR
275+
const xmlPath = opts.reportPath ?? join(REPORT_DIR, 'results.xml')
276+
const jsonPath = join(reportDir, 'report.json')
277+
278+
if (!opts.validateOnly) {
279+
await mkdir(reportDir, { recursive: true })
280+
await mkdir(REPORT_DIR, { recursive: true })
89281

90-
const jsonReport = { timestamp: new Date().toISOString(), results }
91-
await writeFile(join(REPORT_DIR, 'report.json'), JSON.stringify(jsonReport, null, 2))
92-
await writeFile(join(REPORT_DIR, 'report.xml'), toJUnit(results))
282+
const xmlReport = toJUnit(suites)
283+
await writeFile(xmlPath, xmlReport)
284+
console.log(` JUnit XML : ${xmlPath}`)
93285

94-
const passed = results.filter(r => r.pass).length
95-
const failed = results.filter(r => !r.pass).length
96-
console.log(`Conformance: ${passed} passed, ${failed} failed (${results.filter(r => r.pending).length} pending)`)
286+
const jsonReport = {
287+
timestamp: new Date().toISOString(),
288+
totalVectors,
289+
totalFiles: jsonFiles.length,
290+
parseErrors: totalParseErrors,
291+
suites
292+
}
293+
await writeFile(jsonPath, JSON.stringify(jsonReport, null, 2))
294+
console.log(` JSON report : ${jsonPath}`)
295+
}
296+
297+
const hasErrors = allErrors.some(e => e.startsWith('ERROR:'))
298+
if (hasErrors) {
299+
console.log()
300+
console.log('RESULT: FAIL — one or more errors found')
301+
process.exit(1)
302+
}
97303

98-
if (failed > 0) process.exit(1)
304+
console.log()
305+
console.log('RESULT: PASS — all vector files parsed cleanly')
306+
process.exit(0)
99307
}
100308

101309
run().catch(err => { console.error(err); process.exit(1) })

0 commit comments

Comments
 (0)