|
| 1 | +// Copyright 2025 The Cockroach Authors. |
| 2 | +// |
| 3 | +// Use of this software is governed by the CockroachDB Software License |
| 4 | +// included in the /LICENSE file. |
| 5 | + |
| 6 | +package tests |
| 7 | + |
| 8 | +import ( |
| 9 | + "context" |
| 10 | + "fmt" |
| 11 | + "regexp" |
| 12 | + "strings" |
| 13 | + |
| 14 | + "github.com/cockroachdb/cockroach/pkg/cmd/roachtest/cluster" |
| 15 | + "github.com/cockroachdb/cockroach/pkg/cmd/roachtest/option" |
| 16 | + "github.com/cockroachdb/cockroach/pkg/cmd/roachtest/test" |
| 17 | +) |
| 18 | + |
| 19 | +const ( |
| 20 | + // mochaDetailedFailureIndent is the indentation string used by Mocha for |
| 21 | + // lines in the detailed failure section (after "X failing"). |
| 22 | + // This distinguishes them from the test output section which uses 2-4 spaces. |
| 23 | + mochaDetailedFailureIndent = " " // 7 spaces |
| 24 | +) |
| 25 | + |
| 26 | +// mochaTestResult represents the result of a single test from Mocha output |
| 27 | +type mochaTestResult struct { |
| 28 | + name string |
| 29 | + status status |
| 30 | +} |
| 31 | + |
| 32 | +// parseMochaOutput parses Mocha test output and extracts test results. |
| 33 | +// Mocha output format: |
| 34 | +// |
| 35 | +// ✓ passing test name |
| 36 | +// 1) failing test name |
| 37 | +// - skipped test name |
| 38 | +// |
| 39 | +// Returns a slice of test results. |
| 40 | +func parseMochaOutput(output string) []mochaTestResult { |
| 41 | + var results []mochaTestResult |
| 42 | + seen := make(map[string]bool) // Track seen test names to avoid duplicates. |
| 43 | + lines := strings.Split(output, "\n") |
| 44 | + |
| 45 | + // Regex patterns for different test result types. |
| 46 | + passingPattern := regexp.MustCompile(`^\s*✓\s+(.+)$`) |
| 47 | + failingPattern := regexp.MustCompile(`^\s*\d+\)\s+(.+?)(?::.*)?$`) |
| 48 | + skippedPattern := regexp.MustCompile(`^\s*-\s+(.+)$`) |
| 49 | + |
| 50 | + for i, line := range lines { |
| 51 | + line = strings.TrimSpace(line) |
| 52 | + if line == "" { |
| 53 | + continue |
| 54 | + } |
| 55 | + |
| 56 | + if match := passingPattern.FindStringSubmatch(line); match != nil { |
| 57 | + testName := strings.TrimSpace(match[1]) |
| 58 | + if !seen[testName] { |
| 59 | + seen[testName] = true |
| 60 | + results = append(results, mochaTestResult{ |
| 61 | + name: testName, |
| 62 | + status: statusPass, |
| 63 | + }) |
| 64 | + } |
| 65 | + } else if match := failingPattern.FindStringSubmatch(line); match != nil { |
| 66 | + // Look ahead to see if this is a nested test failure. |
| 67 | + // Check next few lines to build the full test description. |
| 68 | + suiteName := strings.TrimSpace(match[1]) |
| 69 | + var hierarchy []string |
| 70 | + foundDetailedFormat := false |
| 71 | + |
| 72 | + // Look for indented continuation lines that describe the full test path. |
| 73 | + // Mocha can have multiple levels: suite -> subsuite -> test. |
| 74 | + for j := i + 1; j < len(lines) && j < i+10; j++ { |
| 75 | + nextLine := lines[j] |
| 76 | + trimmed := strings.TrimSpace(nextLine) |
| 77 | + |
| 78 | + // Check if this is an indented line (part of the test hierarchy). |
| 79 | + // Use mochaDetailedFailureIndent to distinguish detailed failure section |
| 80 | + // from the test output section which has only 2-4 spaces. |
| 81 | + if strings.HasPrefix(nextLine, mochaDetailedFailureIndent) && len(trimmed) > 0 { |
| 82 | + // If it ends with :, it's the final test name. |
| 83 | + if strings.HasSuffix(trimmed, ":") { |
| 84 | + hierarchy = append(hierarchy, strings.TrimSuffix(trimmed, ":")) |
| 85 | + foundDetailedFormat = true |
| 86 | + break |
| 87 | + } else { |
| 88 | + // It's an intermediate level (subsuite). |
| 89 | + hierarchy = append(hierarchy, trimmed) |
| 90 | + } |
| 91 | + } else if trimmed != "" && !strings.HasPrefix(nextLine, " ") { |
| 92 | + // Non-indented non-empty line means we've left the failure description. |
| 93 | + break |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + // Only process entries from the detailed failure section (which have indented continuation). |
| 98 | + // Skip the summary section entries that appear inline with test output. |
| 99 | + if !foundDetailedFormat { |
| 100 | + continue |
| 101 | + } |
| 102 | + |
| 103 | + // Build the full hierarchical test name. |
| 104 | + // Format: "suite => subsuite => ... => test" |
| 105 | + var combinedName string |
| 106 | + if len(hierarchy) > 0 { |
| 107 | + // Start with the suite name, then add all hierarchy levels. |
| 108 | + parts := []string{suiteName} |
| 109 | + parts = append(parts, hierarchy...) |
| 110 | + combinedName = strings.Join(parts, " => ") |
| 111 | + } else { |
| 112 | + // No hierarchy found, just use the suite name. |
| 113 | + combinedName = suiteName |
| 114 | + } |
| 115 | + |
| 116 | + if !seen[combinedName] { |
| 117 | + seen[combinedName] = true |
| 118 | + results = append(results, mochaTestResult{ |
| 119 | + name: combinedName, |
| 120 | + status: statusFail, |
| 121 | + }) |
| 122 | + } |
| 123 | + } else if match := skippedPattern.FindStringSubmatch(line); match != nil { |
| 124 | + testName := strings.TrimSpace(match[1]) |
| 125 | + if !seen[testName] { |
| 126 | + seen[testName] = true |
| 127 | + results = append(results, mochaTestResult{ |
| 128 | + name: testName, |
| 129 | + status: statusSkip, |
| 130 | + }) |
| 131 | + } |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + return results |
| 136 | +} |
| 137 | + |
| 138 | +// parseAndSummarizeNodeJSTestResults parses Node.js/Mocha test output and |
| 139 | +// summarizes it. This is based off of parseAndSummarizeJavaORMTestsResults. |
| 140 | +func parseAndSummarizeNodeJSTestResults( |
| 141 | + ctx context.Context, |
| 142 | + t test.Test, |
| 143 | + c cluster.Cluster, |
| 144 | + node option.NodeListOption, |
| 145 | + testName string, |
| 146 | + testOutput string, |
| 147 | + blocklistName string, |
| 148 | + expectedFailures blocklist, |
| 149 | + ignorelist blocklist, |
| 150 | + version string, |
| 151 | +) { |
| 152 | + results := newORMTestsResults() |
| 153 | + |
| 154 | + // Parse the Mocha output. |
| 155 | + mochaResults := parseMochaOutput(testOutput) |
| 156 | + |
| 157 | + for _, testResult := range mochaResults { |
| 158 | + currentTestName := testResult.name |
| 159 | + |
| 160 | + // Check if test is in ignore or block lists. |
| 161 | + ignoredIssue, expectedIgnored := ignorelist[currentTestName] |
| 162 | + issue, expectedFailure := expectedFailures[currentTestName] |
| 163 | + |
| 164 | + if len(issue) == 0 || issue == "unknown" { |
| 165 | + issue = "unknown" |
| 166 | + } |
| 167 | + |
| 168 | + // Categorize the test result. |
| 169 | + switch { |
| 170 | + case expectedIgnored: |
| 171 | + results.results[currentTestName] = fmt.Sprintf("--- IGNORE: %s due to %s (expected)", currentTestName, ignoredIssue) |
| 172 | + results.ignoredCount++ |
| 173 | + case testResult.status == statusSkip: |
| 174 | + results.results[currentTestName] = fmt.Sprintf("--- SKIP: %s", currentTestName) |
| 175 | + results.skipCount++ |
| 176 | + case testResult.status == statusPass && !expectedFailure: |
| 177 | + results.results[currentTestName] = fmt.Sprintf("--- PASS: %s (expected)", currentTestName) |
| 178 | + results.passExpectedCount++ |
| 179 | + case testResult.status == statusPass && expectedFailure: |
| 180 | + results.results[currentTestName] = fmt.Sprintf("--- PASS: %s - %s (unexpected)", |
| 181 | + currentTestName, maybeAddGithubLink(issue), |
| 182 | + ) |
| 183 | + results.passUnexpectedCount++ |
| 184 | + case testResult.status == statusFail && expectedFailure: |
| 185 | + results.results[currentTestName] = fmt.Sprintf("--- FAIL: %s - %s (expected)", |
| 186 | + currentTestName, maybeAddGithubLink(issue), |
| 187 | + ) |
| 188 | + results.failExpectedCount++ |
| 189 | + results.currentFailures = append(results.currentFailures, currentTestName) |
| 190 | + case testResult.status == statusFail && !expectedFailure: |
| 191 | + results.results[currentTestName] = fmt.Sprintf("--- FAIL: %s - %s (unexpected)", |
| 192 | + currentTestName, maybeAddGithubLink(issue)) |
| 193 | + results.failUnexpectedCount++ |
| 194 | + results.currentFailures = append(results.currentFailures, currentTestName) |
| 195 | + } |
| 196 | + |
| 197 | + results.runTests[currentTestName] = struct{}{} |
| 198 | + results.allTests = append(results.allTests, currentTestName) |
| 199 | + } |
| 200 | + |
| 201 | + results.summarizeAll( |
| 202 | + t, testName, blocklistName, expectedFailures, version, "N/A", |
| 203 | + ) |
| 204 | +} |
0 commit comments