@@ -3,6 +3,7 @@ package main
33
44import (
55 "context"
6+ "encoding/json"
67 "fmt"
78 "io"
89 "io/fs"
3536 flagExamplesDir string
3637 flagTimeout int
3738 flagQuiet bool
39+ flagJSON bool
40+ flagCore string
3841)
3942
43+ // jsonCheck is the CI-friendly serialization of a single health check.
44+ type jsonCheck struct {
45+ Name string `json:"name"`
46+ OK bool `json:"ok"`
47+ Optional bool `json:"optional"`
48+ Extra string `json:"extra,omitempty"`
49+ Error string `json:"error,omitempty"`
50+ }
51+
52+ // jsonResult is the CI-friendly serialization of one variant's run.
53+ type jsonResult struct {
54+ Dir string `json:"dir"`
55+ Name string `json:"name"`
56+ Variant string `json:"variant,omitempty"`
57+ Core string `json:"core,omitempty"`
58+ CoreVersion string `json:"core_version,omitempty"`
59+ Pass bool `json:"pass"`
60+ Censor string `json:"censor,omitempty"`
61+ DurationMs int64 `json:"duration_ms"`
62+ Checks []jsonCheck `json:"checks"`
63+ Error string `json:"error,omitempty"`
64+ }
65+
66+ // jsonReport is the top-level CI output for run-all.
67+ type jsonReport struct {
68+ Passed int `json:"passed"`
69+ Failed int `json:"failed"`
70+ Results []jsonResult `json:"results"`
71+ }
72+
73+ func toJSONResult (dir , core string , res * runner.Result ) jsonResult {
74+ jr := jsonResult {
75+ Dir : dir , Name : res .Name , Variant : res .Variant , Core : core ,
76+ CoreVersion : res .CoreVersion , Pass : res .Pass ,
77+ Censor : res .Fingerprint .Verdict ,
78+ DurationMs : res .Duration .Milliseconds (),
79+ }
80+ if res .Err != nil {
81+ jr .Error = res .Err .Error ()
82+ }
83+ for _ , c := range res .Checks {
84+ jc := jsonCheck {Name : c .Name , OK : c .OK , Optional : c .Optional , Extra : c .Extra }
85+ if c .Err != nil {
86+ jc .Error = c .Err .Error ()
87+ }
88+ jr .Checks = append (jr .Checks , jc )
89+ }
90+ return jr
91+ }
92+
4093func rootCmd () * cobra.Command {
4194 root := & cobra.Command {
4295 Use : "hiddify-health" ,
@@ -62,7 +115,7 @@ func rootCmd() *cobra.Command {
62115// --- run ---
63116
64117func runCmd () * cobra.Command {
65- return & cobra.Command {
118+ c := & cobra.Command {
66119 Use : "run <example-dir>" ,
67120 Short : "Run one example test" ,
68121 Args : cobra .ExactArgs (1 ),
@@ -71,22 +124,43 @@ func runCmd() *cobra.Command {
71124 if db != nil {
72125 defer db .Close ()
73126 }
74- return runOne (cmd .Context (), args [0 ], db )
127+ jrs , anyFail := runOne (cmd .Context (), args [0 ], db )
128+ if flagJSON {
129+ printJSONReport (jrs )
130+ }
131+ if anyFail {
132+ return fmt .Errorf ("test failed" )
133+ }
134+ return nil
75135 },
76136 }
137+ c .Flags ().BoolVar (& flagJSON , "json" , false , "emit machine-readable JSON report (for CI)" )
138+ return c
77139}
78140
79- func runOne (ctx context.Context , dir string , db * store.DB ) error {
80- fmt .Printf ("▶ %s\n " , dir )
141+ // runOne runs every variant of one example, persists to db, prints the
142+ // human log/summary (unless --json), and returns the JSON results plus
143+ // whether any variant failed.
144+ func runOne (ctx context.Context , dir string , db * store.DB ) ([]jsonResult , bool ) {
81145 logOut := io .Writer (os .Stdout )
82- if flagQuiet {
146+ if flagQuiet || flagJSON {
83147 logOut = io .Discard
84148 }
149+ if ! flagJSON {
150+ fmt .Printf ("▶ %s\n " , dir )
151+ }
152+
153+ core := coreOf (dir )
85154 results , err := runner .Run (ctx , dir , logOut )
86- if err != nil {
87- fmt .Printf (" ERROR: %v\n " , err )
88- return err
155+ if err != nil && len (results ) == 0 {
156+ // Hard failure before any variant produced a result.
157+ if ! flagJSON {
158+ fmt .Printf (" ERROR: %v\n " , err )
159+ }
160+ return []jsonResult {{Dir : dir , Name : filepath .Base (dir ), Core : core , Pass : false , Error : err .Error ()}}, true
89161 }
162+
163+ var jrs []jsonResult
90164 anyFail := false
91165 for _ , res := range results {
92166 if db != nil {
@@ -104,34 +178,63 @@ func runOne(ctx context.Context, dir string, db *store.DB) error {
104178 }
105179 _ , _ = db .Save (rec )
106180 }
107- status := "PASS"
181+ jrs = append ( jrs , toJSONResult ( dir , core , res ))
108182 if ! res .Pass {
109- status = "FAIL"
110183 anyFail = true
111184 }
112- label := res .Name
113- if res .Variant != "" && res .Variant != res .Name {
114- label = res .Variant
115- }
116- fmt .Printf (" [%s] %s duration=%s censor=%s\n " ,
117- label , status , res .Duration .Round (time .Millisecond ), res .Fingerprint .Verdict )
118- if res .Err != nil {
119- fmt .Printf (" error: %v\n " , res .Err )
185+ if ! flagJSON {
186+ status := "PASS"
187+ if ! res .Pass {
188+ status = "FAIL"
189+ }
190+ label := res .Name
191+ if res .Variant != "" && res .Variant != res .Name {
192+ label = res .Variant
193+ }
194+ fmt .Printf (" [%s] %s duration=%s censor=%s\n " ,
195+ label , status , res .Duration .Round (time .Millisecond ), res .Fingerprint .Verdict )
196+ if res .Err != nil {
197+ fmt .Printf (" error: %v\n " , res .Err )
198+ }
120199 }
121200 }
122- if anyFail {
123- return fmt .Errorf ("test failed" )
201+ return jrs , anyFail
202+ }
203+
204+ // coreOf returns the core name declared in dir's run config ("" if unknown).
205+ func coreOf (dir string ) string {
206+ cfg , err := runner .LoadRunConfig (dir )
207+ if err != nil {
208+ return ""
124209 }
125- return nil
210+ return cfg .Core
211+ }
212+
213+ func printJSONReport (results []jsonResult ) {
214+ rep := jsonReport {Results : results }
215+ for _ , r := range results {
216+ if r .Pass {
217+ rep .Passed ++
218+ } else {
219+ rep .Failed ++
220+ }
221+ }
222+ enc := json .NewEncoder (os .Stdout )
223+ enc .SetIndent ("" , " " )
224+ _ = enc .Encode (rep )
126225}
127226
128227// --- run-all ---
129228
130229func runAllCmd () * cobra.Command {
131- return & cobra.Command {
230+ c := & cobra.Command {
132231 Use : "run-all [examples-dir]" ,
133232 Short : "Run all examples; exit 1 if any fail" ,
134- Args : cobra .MaximumNArgs (1 ),
233+ Long : "Run all examples under the given directory (default: ./examples).\n " +
234+ "Pass a subdirectory to test only that subtree, e.g.\n " +
235+ " hiddify-health run-all examples/xray\n " +
236+ "Filter by core with --core, e.g. --core sing-box." ,
237+ Args : cobra .MaximumNArgs (1 ),
135238 RunE : func (cmd * cobra.Command , args []string ) error {
136239 root := flagExamplesDir
137240 if len (args ) > 0 {
@@ -141,8 +244,15 @@ func runAllCmd() *cobra.Command {
141244 if err != nil {
142245 return err
143246 }
247+ if flagCore != "" {
248+ dirs = filterByCore (dirs , flagCore )
249+ }
144250 if len (dirs ) == 0 {
145- fmt .Println ("No run.json files found under" , root )
251+ if ! flagJSON {
252+ fmt .Println ("No matching examples found under" , root )
253+ } else {
254+ printJSONReport (nil )
255+ }
146256 return nil
147257 }
148258
@@ -151,21 +261,42 @@ func runAllCmd() *cobra.Command {
151261 defer db .Close ()
152262 }
153263
264+ var allJRS []jsonResult
154265 pass , fail := 0 , 0
155266 for _ , dir := range dirs {
156- if err := runOne (cmd .Context (), dir , db ); err != nil {
267+ jrs , anyFail := runOne (cmd .Context (), dir , db )
268+ allJRS = append (allJRS , jrs ... )
269+ if anyFail {
157270 fail ++
158271 } else {
159272 pass ++
160273 }
161274 }
162- fmt .Printf ("\n --- %d passed %d failed ---\n " , pass , fail )
275+ if flagJSON {
276+ printJSONReport (allJRS )
277+ } else {
278+ fmt .Printf ("\n --- %d passed %d failed ---\n " , pass , fail )
279+ }
163280 if fail > 0 {
164281 return fmt .Errorf ("%d test(s) failed" , fail )
165282 }
166283 return nil
167284 },
168285 }
286+ c .Flags ().BoolVar (& flagJSON , "json" , false , "emit machine-readable JSON report (for CI)" )
287+ c .Flags ().StringVar (& flagCore , "core" , "" , "only run examples for this core (e.g. sing-box, xray)" )
288+ return c
289+ }
290+
291+ // filterByCore keeps only example dirs whose run config declares the given core.
292+ func filterByCore (dirs []string , core string ) []string {
293+ var out []string
294+ for _ , dir := range dirs {
295+ if coreOf (dir ) == core {
296+ out = append (out , dir )
297+ }
298+ }
299+ return out
169300}
170301
171302// --- check ---
0 commit comments