Skip to content

Commit 686879d

Browse files
sirdeggenclaude
andcommitted
feat(conformance): JSON report output, Grafana dashboard v0, static HTML summary
- Add --json-report flag to Go runner (writeJSONReport func, Result.Category field) - Update conformance.yml CI to emit XML + JSON artifacts for both runners - Add Grafana importable dashboard (uid bsv-conformance-v0) with JSON API datasource - Add dashboard.mjs static HTML generator with CSS-only pass-rate gauges Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1aca3ed commit 686879d

4 files changed

Lines changed: 883 additions & 13 deletions

File tree

.github/workflows/conformance.yml

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,22 @@ jobs:
1414
- uses: actions/setup-go@v5
1515
with:
1616
go-version: '1.22'
17+
- name: Create reports directory
18+
run: mkdir -p conformance/reports
1719
- name: Run Go conformance runner
18-
run: go run . -vectors ../../vectors
20+
run: |
21+
go run . \
22+
-vectors ../../vectors \
23+
--report ../../reports/go-results.xml \
24+
--json-report ../../reports/go-results.json
1925
working-directory: ${{ github.workspace }}/conformance/runner/go
26+
- name: Upload Go conformance reports
27+
if: always()
28+
uses: actions/upload-artifact@v4
29+
with:
30+
name: go-conformance-reports
31+
path: conformance/reports/
32+
retention-days: 30
2033

2134
ts-runner:
2235
runs-on: ubuntu-latest
@@ -32,7 +45,19 @@ jobs:
3245
run: pnpm install --no-frozen-lockfile
3346
- name: Build TS SDK
3447
run: pnpm --filter @bsv/sdk run build
48+
- name: Create reports directory
49+
run: mkdir -p conformance/reports
3550
- name: Run TS conformance runner
36-
run: pnpm --filter @bsv/conformance-runner-ts test
51+
run: |
52+
pnpm --filter @bsv/conformance-runner-ts test -- \
53+
--json --outputFile=../../reports/ts-results.json
3754
env:
3855
NODE_OPTIONS: --experimental-vm-modules
56+
working-directory: ${{ github.workspace }}/conformance/runner/ts
57+
- name: Upload TS conformance reports
58+
if: always()
59+
uses: actions/upload-artifact@v4
60+
with:
61+
name: ts-conformance-reports
62+
path: conformance/reports/
63+
retention-days: 30

conformance/runner/go/main.go

Lines changed: 123 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,11 @@ const (
5353
)
5454

5555
type Result struct {
56-
ID string
57-
Status Status
58-
Message string
59-
Elapsed time.Duration
56+
ID string
57+
Status Status
58+
Message string
59+
Elapsed time.Duration
60+
Category string
6061
}
6162

6263
// ─── JUnit XML schema ─────────────────────────────────────────────────────────
@@ -2681,10 +2682,11 @@ func runVector(fileID string, filePath string, v map[string]interface{}, filePar
26812682
}
26822683

26832684
return Result{
2684-
ID: id,
2685-
Status: status,
2686-
Message: msg,
2687-
Elapsed: time.Since(start),
2685+
ID: id,
2686+
Status: status,
2687+
Message: msg,
2688+
Elapsed: time.Since(start),
2689+
Category: cat,
26882690
}
26892691
}
26902692

@@ -2763,14 +2765,112 @@ func writeJUnit(reportPath string, allResults []Result) error {
27632765
return os.WriteFile(reportPath, append([]byte(xml.Header), data...), 0644)
27642766
}
27652767

2768+
// ─── JSON report output ───────────────────────────────────────────────────────
2769+
2770+
type jsonVectorEntry struct {
2771+
ID string `json:"id"`
2772+
Status string `json:"status"`
2773+
Category string `json:"category"`
2774+
DurationMS float64 `json:"duration_ms"`
2775+
Message string `json:"message,omitempty"`
2776+
}
2777+
2778+
type jsonCategoryEntry struct {
2779+
Category string `json:"category"`
2780+
Passed int `json:"passed"`
2781+
Failed int `json:"failed"`
2782+
Skipped int `json:"skipped"`
2783+
Total int `json:"total"`
2784+
}
2785+
2786+
type jsonReport struct {
2787+
GeneratedAt string `json:"generated_at"`
2788+
Runner string `json:"runner"`
2789+
Total int `json:"total"`
2790+
Passed int `json:"passed"`
2791+
Failed int `json:"failed"`
2792+
Skipped int `json:"skipped"`
2793+
PassRate float64 `json:"pass_rate"`
2794+
Categories []jsonCategoryEntry `json:"categories"`
2795+
Vectors []jsonVectorEntry `json:"vectors"`
2796+
}
2797+
2798+
func writeJSONReport(reportPath string, allResults []Result) error {
2799+
var passed, failed, skipped int
2800+
catStats := map[string]*jsonCategoryEntry{}
2801+
2802+
vectors := make([]jsonVectorEntry, 0, len(allResults))
2803+
for _, r := range allResults {
2804+
statusStr := strings.ToUpper(string(r.Status))
2805+
vectors = append(vectors, jsonVectorEntry{
2806+
ID: r.ID,
2807+
Status: statusStr,
2808+
Category: r.Category,
2809+
DurationMS: float64(r.Elapsed.Microseconds()) / 1000.0,
2810+
Message: r.Message,
2811+
})
2812+
2813+
if _, ok := catStats[r.Category]; !ok {
2814+
catStats[r.Category] = &jsonCategoryEntry{Category: r.Category}
2815+
}
2816+
entry := catStats[r.Category]
2817+
entry.Total++
2818+
2819+
switch r.Status {
2820+
case StatusPass:
2821+
passed++
2822+
entry.Passed++
2823+
case StatusFail:
2824+
failed++
2825+
entry.Failed++
2826+
default:
2827+
skipped++
2828+
entry.Skipped++
2829+
}
2830+
}
2831+
2832+
// Build ordered category slice.
2833+
categories := make([]jsonCategoryEntry, 0, len(catStats))
2834+
for _, v := range catStats {
2835+
categories = append(categories, *v)
2836+
}
2837+
2838+
total := len(allResults)
2839+
var passRate float64
2840+
if total > 0 {
2841+
passRate = float64(passed) / float64(total)
2842+
// Round to 4 decimal places.
2843+
passRate = float64(int(passRate*10000+0.5)) / 10000
2844+
}
2845+
2846+
report := jsonReport{
2847+
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
2848+
Runner: "go",
2849+
Total: total,
2850+
Passed: passed,
2851+
Failed: failed,
2852+
Skipped: skipped,
2853+
PassRate: passRate,
2854+
Categories: categories,
2855+
Vectors: vectors,
2856+
}
2857+
2858+
data, err := json.MarshalIndent(report, "", " ")
2859+
if err != nil {
2860+
return err
2861+
}
2862+
return os.WriteFile(reportPath, data, 0644)
2863+
}
2864+
27662865
// ─── Main ─────────────────────────────────────────────────────────────────────
27672866

27682867
func main() {
27692868
// Default vectors path relative to the runner binary location.
27702869
defaultVectors := filepath.Join(filepath.Dir(os.Args[0]), "..", "..", "vectors")
2771-
vectorsDir := flag.String("vectors", defaultVectors, "path to vectors directory")
2772-
reportPath := flag.String("report", "", "JUnit XML report output path (optional)")
2773-
validateOnly := flag.Bool("validate-only", false, "validate JSON format only, do not execute vectors")
2870+
vectorsDir := flag.String("vectors", defaultVectors, "path to vectors directory")
2871+
reportPath := flag.String("report", "", "JUnit XML report output path (optional)")
2872+
jsonReportPath := flag.String("json-report", "", "JSON summary report output path (optional)")
2873+
validateOnly := flag.Bool("validate-only", false, "validate JSON format only, do not execute vectors")
27742874
flag.Parse()
27752875

27762876
files, err := findJSONFiles(*vectorsDir)
@@ -2825,6 +2925,18 @@ func main() {
28252925
fmt.Printf("JUnit report written to %s\n", *reportPath)
28262926
}
28272927

2928+
if *jsonReportPath != "" {
2929+
if err := os.MkdirAll(filepath.Dir(*jsonReportPath), 0755); err != nil {
2930+
fmt.Fprintf(os.Stderr, "create report dir: %v\n", err)
2931+
os.Exit(1)
2932+
}
2933+
if err := writeJSONReport(*jsonReportPath, allResults); err != nil {
2934+
fmt.Fprintf(os.Stderr, "write JSON report: %v\n", err)
2935+
os.Exit(1)
2936+
}
2937+
fmt.Printf("JSON report written to %s\n", *jsonReportPath)
2938+
}
2939+
28282940
if fail > 0 {
28292941
os.Exit(1)
28302942
}

0 commit comments

Comments
 (0)