Skip to content

Commit 99ad9a0

Browse files
craig[bot]spilchendt
committed
154520: roachtest: add Mocha test parser for node-postgres tests r=spilchen a=spilchen This commit introduces Mocha test output parsing for the node-postgres roachtest. The parser handles hierarchical test suites (suite => subsuite => test) and properly categorizes test results against blocklists and ignorelists. With this ignorelist in place, this also ignores a test flake as outlined here: #152728 (comment) Fixes #152728 Release note: None Epic: None 154522: backup: re-apply SECONDARY zone config during RESTORE DATABASE r=dt a=dt Previously during restoration of a DATABASE with a SECONDARY REGION, the region was correctly configured on the database but its *zone config* was not updated to reflect the region, so the second lease preference was not set, at least until some other schema change or operation reconcilled the descriptors config to a zone config. This updates the creation of the DB during RESTORE to also trigger the zone configuration update. Fixes #154523. Release note (bug fix): RESTORE of a database with a SECONDARY REGION now applies the lease-preference for said region. Epic: none. Co-authored-by: Matt Spilchen <[email protected]> Co-authored-by: David Taylor <[email protected]>
3 parents 5a4758c + 9dbae89 + 2b1d0a2 commit 99ad9a0

File tree

7 files changed

+387
-24
lines changed

7 files changed

+387
-24
lines changed

pkg/backup/restore_job.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,6 +1389,11 @@ func createImportingDescriptors(
13891389

13901390
return nil
13911391
})
1392+
1393+
var opts []multiregion.MakeRegionConfigOption
1394+
if desc.RegionConfig.SecondaryRegion != "" {
1395+
opts = append(opts, multiregion.WithSecondaryRegion(desc.RegionConfig.SecondaryRegion))
1396+
}
13921397
regionConfig := multiregion.MakeRegionConfig(
13931398
regionNames,
13941399
desc.RegionConfig.PrimaryRegion,
@@ -1397,6 +1402,7 @@ func createImportingDescriptors(
13971402
desc.RegionConfig.Placement,
13981403
regionTypeDesc.TypeDesc().RegionConfig.SuperRegions,
13991404
regionTypeDesc.TypeDesc().RegionConfig.ZoneConfigExtensions,
1405+
opts...,
14001406
)
14011407
if err := sql.ApplyZoneConfigFromDatabaseRegionConfig(
14021408
ctx,

pkg/backup/testdata/backup-restore/multiregion

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ set-cluster-setting setting=sql.multiregion.system_database_multiregion.enabled
1111

1212
exec-sql
1313
ALTER DATABASE system SET PRIMARY REGION "us-east-1";
14-
CREATE DATABASE d PRIMARY REGION "us-east-1" REGIONS "us-west-1", "eu-central-1" SURVIVE REGION FAILURE;
14+
CREATE DATABASE d PRIMARY REGION "us-east-1" REGIONS "us-west-1", "eu-central-1" SURVIVE REGION FAILURE SECONDARY REGION "us-west-1";
1515
CREATE TABLE d.t (x INT);
1616
INSERT INTO d.t VALUES (1), (2), (3);
1717
----
@@ -23,6 +23,19 @@ eu-central-1
2323
us-east-1
2424
us-west-1
2525

26+
query-sql
27+
SHOW ZONE CONFIGURATION FROM DATABASE d;
28+
----
29+
DATABASE d ALTER DATABASE d CONFIGURE ZONE USING
30+
range_min_bytes = 134217728,
31+
range_max_bytes = 536870912,
32+
gc.ttlseconds = 14400,
33+
num_replicas = 5,
34+
num_voters = 5,
35+
constraints = '{+region=eu-central-1: 1, +region=us-east-1: 1, +region=us-west-1: 1}',
36+
voter_constraints = '{+region=us-east-1: 2, +region=us-west-1: 2}',
37+
lease_preferences = '[[+region=us-east-1], [+region=us-west-1]]'
38+
2639
exec-sql
2740
BACKUP DATABASE d INTO 'nodelocal://1/database_backup/';
2841
----
@@ -50,12 +63,25 @@ RESTORE DATABASE d FROM LATEST IN 'nodelocal://1/database_backup/';
5063
query-sql
5164
SHOW DATABASES;
5265
----
53-
d root us-east-1 {eu-central-1,us-east-1,us-west-1} region
66+
d root us-east-1 us-west-1 {eu-central-1,us-east-1,us-west-1} region
5467
data root <nil> <nil> {} <nil>
5568
defaultdb root <nil> <nil> {} <nil>
5669
postgres root <nil> <nil> {} <nil>
5770
system node <nil> <nil> {} <nil>
5871

72+
query-sql
73+
SHOW ZONE CONFIGURATION FROM DATABASE d;
74+
----
75+
DATABASE d ALTER DATABASE d CONFIGURE ZONE USING
76+
range_min_bytes = 134217728,
77+
range_max_bytes = 536870912,
78+
gc.ttlseconds = 14400,
79+
num_replicas = 5,
80+
num_voters = 5,
81+
constraints = '{+region=eu-central-1: 1, +region=us-east-1: 1, +region=us-west-1: 1}',
82+
voter_constraints = '{+region=us-east-1: 2, +region=us-west-1: 2}',
83+
lease_preferences = '[[+region=us-east-1], [+region=us-west-1]]'
84+
5985
# A new cluster with different localities settings.
6086
new-cluster name=s3 share-io-dir=s1 allow-implicit-access disable-tenant localities=eu-central-1,eu-north-1
6187
----
@@ -85,6 +111,7 @@ SET enable_multiregion_placement_policy='true';
85111
ALTER DATABASE d SURVIVE ZONE FAILURE;
86112
ALTER DATABASE d PLACEMENT RESTRICTED;
87113
ALTER DATABASE d SET PRIMARY REGION 'eu-central-1';
114+
ALTER DATABASE d DROP SECONDARY REGION;
88115
ALTER DATABASE d DROP REGION 'us-east-1';
89116
ALTER DATABASE d DROP REGION 'us-west-1';
90117
ALTER DATABASE d ADD REGION 'eu-north-1';
@@ -104,6 +131,7 @@ SET enable_multiregion_placement_policy='true';
104131
ALTER DATABASE d_new SURVIVE ZONE FAILURE;
105132
ALTER DATABASE d PLACEMENT RESTRICTED;
106133
ALTER DATABASE d_new SET PRIMARY REGION 'eu-central-1';
134+
ALTER DATABASE d_new DROP SECONDARY REGION;
107135
ALTER DATABASE d_new DROP REGION 'us-east-1';
108136
ALTER DATABASE d_new DROP REGION 'us-west-1';
109137
ALTER DATABASE d_new ADD REGION 'eu-north-1';

pkg/cmd/roachtest/tests/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ go_library(
141141
"multitenant_upgrade.go",
142142
"mvcc_gc.go",
143143
"network.go",
144+
"nodejs_helpers.go",
144145
"nodejs_postgres.go",
146+
"nodejs_postgres_blocklist.go",
145147
"npgsql.go",
146148
"npgsql_blocklist.go",
147149
"online_restore.go",
@@ -366,6 +368,7 @@ go_test(
366368
"blocklist_test.go",
367369
"cdc_helper_test.go",
368370
"drt_test.go",
371+
"nodejs_helpers_test.go",
369372
"query_comparison_util_test.go",
370373
"restore_test.go",
371374
"sysbench_test.go",
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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

Comments
 (0)