Skip to content

Commit e90f566

Browse files
fix(ci): unbreak the python downstream gate and lint
The python job selected scenarios with a negative-lookahead regex (python(?!.*_3\.15$).*), but the matcher is Go's RE2 which rejects lookahead, so list-scenarios errored and the entire Python gate was skipped on push CI. Add an -exclude flag to cmd/list-scenarios (RE2, unanchored) and a matching test_scenarios_exclude input to test.yml, then have ci.yml run 'python.*' excluding the wheel-only scenarios that can't run against PyPI ddtrace: every *_3.15, python_live_heap_3.14 (persistent live-heap unreleased), and the python_downstream_gate index dir. Also fix ruff: drop a stray blank line in the live_heap main.py import blocks (I001) and ignore EM101/SIM105 for the exceptions scenarios, whose deliberate string-literal raise + try/except/pass is the profiled workload.
1 parent ceb3bc1 commit e90f566

7 files changed

Lines changed: 87 additions & 18 deletions

File tree

.github/workflows/ci.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,14 @@ jobs:
5858
python:
5959
uses: ./.github/workflows/test.yml
6060
with:
61-
# 3.15 migration scenarios require DDTRACE_INSTALL_URL (downstream only).
62-
test_scenarios: 'python(?!.*_3\.15$).*'
61+
test_scenarios: 'python.*'
62+
# Wheel-only scenarios need DDTRACE_INSTALL_URL (dd-trace-py downstream
63+
# gate), so they can't run here against PyPI ddtrace: every *_3.15, plus
64+
# python_live_heap_3.14 (persistent live-heap is not in a release yet).
65+
# python_downstream_gate is an index README, not a runnable scenario.
66+
# Go's regexp is RE2 (no negative lookahead), so this is an explicit
67+
# exclude rather than a lookahead baked into test_scenarios.
68+
test_scenarios_exclude: '_3\.15$|^python_live_heap_3\.14$|^python_downstream_gate$'
6369
secrets: inherit
6470
full_host:
6571
uses: ./.github/workflows/test.yml

.github/workflows/test.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ on:
99
required: false
1010
type: string
1111
default: '.*'
12+
test_scenarios_exclude:
13+
description: 'A regexp dropping scenarios matched by test_scenarios (RE2, unanchored)'
14+
required: false
15+
type: string
16+
default: ''
1217
ddtrace_install_url:
1318
description: 'URL to a ddtrace install script (e.g. from S3 builds)'
1419
required: false
@@ -35,7 +40,7 @@ jobs:
3540
# - free local-daemon layer caching: buildBaseImages runs once per chunk,
3641
# and the chunk's scenarios reuse the built base image.
3742
run: |
38-
matrix=$(go run ./cmd/list-scenarios -pattern '${{ inputs.test_scenarios }}' -chunk-size 3)
43+
matrix=$(go run ./cmd/list-scenarios -pattern '${{ inputs.test_scenarios }}' -exclude '${{ inputs.test_scenarios_exclude }}' -chunk-size 3)
3944
echo "scenarios=$matrix" >> "$GITHUB_OUTPUT"
4045
4146
docker-scenarios:

cmd/list-scenarios/main.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// Usage:
1414
//
1515
// go run ./cmd/list-scenarios -pattern 'python.*' -chunk-size 3
16+
// go run ./cmd/list-scenarios -pattern 'python.*' -exclude '_3\.15$' -chunk-size 3
1617
package main
1718

1819
import (
@@ -39,7 +40,7 @@ type matrixEntry struct {
3940
Names string `json:"names"`
4041
}
4142

42-
func run(pattern, scenariosDir string, chunkSize int) ([]matrixEntry, error) {
43+
func run(pattern, exclude, scenariosDir string, chunkSize int) ([]matrixEntry, error) {
4344
// Anchor the user pattern so e.g. "python" doesn't accidentally match
4445
// "python_basic_idle_3.10". The non-capturing group preserves precedence of
4546
// any alternation inside the user pattern.
@@ -48,21 +49,38 @@ func run(pattern, scenariosDir string, chunkSize int) ([]matrixEntry, error) {
4849
return nil, fmt.Errorf("invalid -pattern: %w", err)
4950
}
5051

52+
// Optional exclusion, applied to names that matched -pattern. Unlike
53+
// -pattern this is NOT anchored, so a suffix like `_3\.15$` drops every
54+
// name ending in that version. Go's regexp is RE2 (no lookahead), so
55+
// "match python but not the wheel-only variants" must be expressed as a
56+
// separate exclude rather than a negative lookahead in -pattern.
57+
var excludeRe *regexp.Regexp
58+
if exclude != "" {
59+
excludeRe, err = regexp.Compile(exclude)
60+
if err != nil {
61+
return nil, fmt.Errorf("invalid -exclude: %w", err)
62+
}
63+
}
64+
5165
entries, err := os.ReadDir(scenariosDir)
5266
if err != nil {
5367
return nil, fmt.Errorf("read %s: %w", scenariosDir, err)
5468
}
5569

5670
var names []string
5771
for _, e := range entries {
58-
if e.IsDir() && re.MatchString(e.Name()) {
59-
names = append(names, e.Name())
72+
if !e.IsDir() || !re.MatchString(e.Name()) {
73+
continue
74+
}
75+
if excludeRe != nil && excludeRe.MatchString(e.Name()) {
76+
continue
6077
}
78+
names = append(names, e.Name())
6179
}
6280
sort.Strings(names)
6381

6482
if len(names) == 0 {
65-
return nil, fmt.Errorf("no scenarios matched pattern %q in %s", pattern, scenariosDir)
83+
return nil, fmt.Errorf("no scenarios matched pattern %q (exclude %q) in %s", pattern, exclude, scenariosDir)
6684
}
6785

6886
// Pack into chunks of at most chunkSize, preserving sorted order.
@@ -88,6 +106,7 @@ func run(pattern, scenariosDir string, chunkSize int) ([]matrixEntry, error) {
88106

89107
func main() {
90108
pattern := flag.String("pattern", "", "regex selecting scenario directory names (anchored as ^pattern$)")
109+
exclude := flag.String("exclude", "", "regex dropping matched names (unanchored, RE2); e.g. '_3\\.15$'")
91110
scenariosDir := flag.String("scenarios-dir", "scenarios", "path to the scenarios directory")
92111
chunkSize := flag.Int("chunk-size", 3, "max scenarios per matrix entry")
93112
flag.Parse()
@@ -103,7 +122,7 @@ func main() {
103122
}
104123

105124
abs, _ := filepath.Abs(*scenariosDir)
106-
out, err := run(*pattern, *scenariosDir, *chunkSize)
125+
out, err := run(*pattern, *exclude, *scenariosDir, *chunkSize)
107126
if err != nil {
108127
fmt.Fprintf(os.Stderr, "error: %v (resolved scenarios dir: %s)\n", err, abs)
109128
os.Exit(1)

cmd/list-scenarios/main_test.go

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"path/filepath"
77
"reflect"
88
"regexp"
9+
"strings"
910
"testing"
1011
)
1112

@@ -37,7 +38,7 @@ func TestRun_ChunksAlphabetically(t *testing.T) {
3738
"node_heap", // must be filtered out by the pattern
3839
})
3940

40-
got, err := run("python.*", root, 3)
41+
got, err := run("python.*", "", root, 3)
4142
if err != nil {
4243
t.Fatal(err)
4344
}
@@ -62,7 +63,7 @@ func TestRun_ChunksAlphabetically(t *testing.T) {
6263
func TestRun_SingleChunkWhenSmall(t *testing.T) {
6364
root := mkScenarios(t, []string{"dotnet_wall", "dotnet_alloc"})
6465

65-
got, err := run("dotnet.*", root, 3)
66+
got, err := run("dotnet.*", "", root, 3)
6667
if err != nil {
6768
t.Fatal(err)
6869
}
@@ -87,7 +88,7 @@ func TestRun_AnchoringRejectsSubstringMatches(t *testing.T) {
8788
"python_cpu_sleep_sync_3.12",
8889
})
8990

90-
got, err := run("python_cpu", root, 3)
91+
got, err := run("python_cpu", "", root, 3)
9192
if err != nil {
9293
t.Fatal(err)
9394
}
@@ -101,7 +102,7 @@ func TestRun_ExactChunkSizeBoundary(t *testing.T) {
101102
root := mkScenarios(t, []string{
102103
"a", "b", "c", "d", "e", "f",
103104
})
104-
got, err := run(".*", root, 3)
105+
got, err := run(".*", "", root, 3)
105106
if err != nil {
106107
t.Fatal(err)
107108
}
@@ -113,10 +114,46 @@ func TestRun_ExactChunkSizeBoundary(t *testing.T) {
113114
}
114115
}
115116

117+
func TestRun_ExcludeDropsMatchedNames(t *testing.T) {
118+
// Mirrors the CI python gate: run everything python except the wheel-only
119+
// variants (every *_3.15 plus python_live_heap_3.14).
120+
root := mkScenarios(t, []string{
121+
"python_cpu",
122+
"python_lock_3.14",
123+
"python_lock_3.15",
124+
"python_mem_domain_3.14",
125+
"python_mem_domain_3.15",
126+
"python_live_heap_3.14",
127+
"python_live_heap_3.15",
128+
})
129+
130+
got, err := run("python.*", `_3\.15$|^python_live_heap_3\.14$`, root, 3)
131+
if err != nil {
132+
t.Fatal(err)
133+
}
134+
135+
var names []string
136+
for _, e := range got {
137+
names = append(names, e.Names)
138+
}
139+
joined := strings.Join(names, ", ")
140+
want := "python_cpu, python_lock_3.14, python_mem_domain_3.14"
141+
if joined != want {
142+
t.Fatalf("excluded set wrong:\n got %q\nwant %q", joined, want)
143+
}
144+
}
145+
146+
func TestRun_InvalidExcludeIsError(t *testing.T) {
147+
root := mkScenarios(t, []string{"python_cpu"})
148+
if _, err := run("python.*", "[invalid", root, 3); err == nil {
149+
t.Fatal("expected error on invalid exclude regex")
150+
}
151+
}
152+
116153
func TestRun_NoMatchIsError(t *testing.T) {
117154
root := mkScenarios(t, []string{"python_cpu"})
118155

119-
_, err := run("ruby.*", root, 3)
156+
_, err := run("ruby.*", "", root, 3)
120157
if err == nil {
121158
t.Fatal("expected error when no scenarios match")
122159
}
@@ -125,7 +162,7 @@ func TestRun_NoMatchIsError(t *testing.T) {
125162
func TestRun_InvalidPatternIsError(t *testing.T) {
126163
root := mkScenarios(t, []string{"python_cpu"})
127164

128-
if _, err := run("[invalid", root, 3); err == nil {
165+
if _, err := run("[invalid", "", root, 3); err == nil {
129166
t.Fatal("expected error on invalid regex")
130167
}
131168
}
@@ -140,7 +177,7 @@ func TestRun_RegexMatchesIntendedDirAndOnlyThat(t *testing.T) {
140177
"python_cpu_sleep_sync_3.12",
141178
"python_basic_idle_3.11",
142179
})
143-
got, err := run("python.*", root, 3)
180+
got, err := run("python.*", "", root, 3)
144181
if err != nil {
145182
t.Fatal(err)
146183
}
@@ -175,7 +212,7 @@ func TestRun_RegexMatchesIntendedDirAndOnlyThat(t *testing.T) {
175212
// verifies it round-trips to the expected shape.
176213
func TestRun_OutputIsValidJSON(t *testing.T) {
177214
root := mkScenarios(t, []string{"a", "b", "c", "d"})
178-
got, err := run(".*", root, 3)
215+
got, err := run(".*", "", root, 3)
179216
if err != nil {
180217
t.Fatal(err)
181218
}

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ ignore = [
5050
"**/__init__.py" = ["D104"] # missing docstring in public package
5151
"**/test_*.py" = ["S101", "D"] # allow assert in tests, skip docstrings
5252
"**/tests/**/*.py" = ["S101", "D"] # allow assert in tests, skip docstrings
53+
# The exceptions scenario deliberately raises + swallows an exception as its
54+
# profiled workload; the explicit string-literal raise and try/except/pass are
55+
# the point of the test, not style smells.
56+
"scenarios/python_exceptions_*/main.py" = ["EM101", "SIM105"]
5357

5458
[tool.ruff.lint.pydocstyle]
5559
convention = "google"

scenarios/python_live_heap_3.14/main.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
from ddtrace.profiling import Profiler
55

6-
76
# Allocations are held at module scope so they stay live for the whole process
87
# and therefore appear in every live-heap snapshot the profiler exports.
98
LIVE: list = []

scenarios/python_live_heap_3.15/main.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
from ddtrace.profiling import Profiler
55

6-
76
# Allocations are held at module scope so they stay live for the whole process
87
# and therefore appear in every live-heap snapshot the profiler exports.
98
LIVE: list = []

0 commit comments

Comments
 (0)