package run import ( "bufio" "encoding/json" "encoding/xml" "errors" "strings" ) // exec_parsers.go 把常见测试输出(go test -json / JUnit XML / vitest --reporter=json) // 归一化为 TestSummary。所有解析器都是防御性的:永不 panic;无法解析时返回 error, // 由 RunTests 折成 ParseError(始终保留原始输出)。 // parseTestOutput 按 format 分派;format=="" 或 "auto" 时按内容嗅探。 func parseTestOutput(format, stdout, stderr string) (TestSummary, error) { combined := stdout if strings.TrimSpace(combined) == "" { combined = stderr } switch strings.ToLower(strings.TrimSpace(format)) { case "gotest": return parseGoTestJSON([]byte(combined)) case "junit": return parseJUnitXML([]byte(combined)) case "vitest": return parseVitestJSON([]byte(combined)) case "", "auto": return sniffAndParse(combined) default: return TestSummary{}, errors.New("unknown test format: " + format) } } // sniffAndParse 按首个非空 token 推断框架:JSON 行 + Action 字段 → gotest; // JSON 对象含 numPassedTests → vitest;'testsuite>testsuite)。 Nested []junitTestSuite `xml:"testsuite"` } type junitTestCase struct { Name string `xml:"name,attr"` ClassName string `xml:"classname,attr"` Time float64 `xml:"time,attr"` Failure *junitDetail `xml:"failure"` Error *junitDetail `xml:"error"` Skipped *junitSkipped `xml:"skipped"` } type junitDetail struct { Message string `xml:"message,attr"` Body string `xml:",chardata"` } type junitSkipped struct { Message string `xml:"message,attr"` } // parseJUnitXML 解析 JUnit XML(顶层 testsuites 或裸 testsuite)。按 testcase 逐个 // 统计,failure/error → failed,skipped → skipped,其余 → passed。 func parseJUnitXML(data []byte) (TestSummary, error) { if strings.TrimSpace(string(data)) == "" { return TestSummary{}, errors.New("empty junit output") } sum := TestSummary{Framework: "junit"} var suites []junitTestSuite var root junitTestSuites if err := xml.Unmarshal(data, &root); err == nil && len(root.Suites) > 0 { suites = root.Suites } else { var single junitTestSuite if err := xml.Unmarshal(data, &single); err != nil { return sum, err } suites = []junitTestSuite{single} } var totalTime float64 var walk func(s junitTestSuite) walk = func(s junitTestSuite) { totalTime += s.Time for _, tc := range s.TestCases { sum.Total++ pkg := tc.ClassName switch { case tc.Failure != nil || tc.Error != nil: sum.Failed++ d := tc.Failure if d == nil { d = tc.Error } msg := strings.TrimSpace(d.Message) if msg == "" { msg = strings.TrimSpace(d.Body) } sum.Failures = append(sum.Failures, TestFailure{Name: tc.Name, Package: pkg, Message: msg}) case tc.Skipped != nil: sum.Skipped++ default: sum.Passed++ } } for _, n := range s.Nested { walk(n) } } for _, s := range suites { walk(s) } if sum.Total == 0 { return sum, errors.New("no junit testcases found") } sum.DurationMs = int64(totalTime * 1000) return sum, nil } // ===== vitest --reporter=json ===== type vitestReport struct { NumTotalTests *int `json:"numTotalTests"` NumPassedTests *int `json:"numPassedTests"` NumFailedTests *int `json:"numFailedTests"` NumPendingTests *int `json:"numPendingTests"` StartTime *int64 `json:"startTime"` TestResults []vitestFileResult `json:"testResults"` } type vitestFileResult struct { Name string `json:"name"` AssertionResults []vitestAssertion `json:"assertionResults"` } type vitestAssertion struct { Title string `json:"title"` FullName string `json:"fullName"` Status string `json:"status"` // passed|failed|skipped|pending FailureMessages []string `json:"failureMessages"` Duration float64 `json:"duration"` } // parseVitestJSON 解析 vitest(jest 兼容)的 JSON 报告。优先使用顶层计数; // 同时遍历 testResults 收集失败用例名与首条 failureMessage。 func parseVitestJSON(data []byte) (TestSummary, error) { if strings.TrimSpace(string(data)) == "" { return TestSummary{}, errors.New("empty vitest output") } var rep vitestReport if err := json.Unmarshal(data, &rep); err != nil { return TestSummary{}, err } sum := TestSummary{Framework: "vitest"} var durMs float64 for _, fr := range rep.TestResults { for _, a := range fr.AssertionResults { switch a.Status { case "failed": sum.Failed++ msg := "" if len(a.FailureMessages) > 0 { msg = strings.TrimSpace(a.FailureMessages[0]) } name := a.FullName if name == "" { name = a.Title } sum.Failures = append(sum.Failures, TestFailure{Name: name, Package: fr.Name, Message: msg}) case "skipped", "pending", "todo": sum.Skipped++ case "passed": sum.Passed++ } durMs += a.Duration } } // 顶层计数优先(更可靠);没有就用遍历得到的计数。 if rep.NumPassedTests != nil { sum.Passed = *rep.NumPassedTests } if rep.NumFailedTests != nil { sum.Failed = *rep.NumFailedTests } if rep.NumPendingTests != nil { sum.Skipped = *rep.NumPendingTests } if rep.NumTotalTests != nil { sum.Total = *rep.NumTotalTests } else { sum.Total = sum.Passed + sum.Failed + sum.Skipped } if sum.Total == 0 && len(rep.TestResults) == 0 && rep.NumTotalTests == nil { return sum, errors.New("not a vitest report") } sum.DurationMs = int64(durMs) return sum, nil }