@@ -53,10 +53,11 @@ const (
5353)
5454
5555type 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
27682867func 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