Skip to content

Commit 1a9621b

Browse files
Merge pull request #58 from codefly-dev/codex/exact-python-selection
Python: make typed pytest selection authoritative
2 parents 69016fd + c599c1b commit 1a9621b

5 files changed

Lines changed: 63 additions & 4 deletions

File tree

‎agent.codefly.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
publisher: codefly.dev
22
kind: codefly:service
33
name: python
4-
version: 0.0.42
4+
version: 0.0.43

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.25.12
55
toolchain go1.26.4
66

77
require (
8-
github.com/codefly-dev/core v0.2.116
8+
github.com/codefly-dev/core v0.2.117
99
gopkg.in/yaml.v3 v3.0.1
1010
)
1111

‎go.sum‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
3737
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
3838
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
3939
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
40-
github.com/codefly-dev/core v0.2.116 h1:8+GM4qYyJOGfmRXgw50/n6r0WpGisJ6XBqWSUWAHoCU=
41-
github.com/codefly-dev/core v0.2.116/go.mod h1:e3xNBpBgMOzraxGaFbbiYtMYyhLAEoP7Cy2yaMrvV5U=
40+
github.com/codefly-dev/core v0.2.117 h1:wwzH/YE9gBk+Fn76JNYsdFORiqj6lUz5/szuQQi7f2s=
41+
github.com/codefly-dev/core v0.2.117/go.mod h1:e3xNBpBgMOzraxGaFbbiYtMYyhLAEoP7Cy2yaMrvV5U=
4242
github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ=
4343
github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg=
4444
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=

‎pkg/runtime/runtime.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ func (s *Runtime) Test(ctx context.Context, req *runtimev0.TestRequest) (*runtim
133133
if selectorErr != nil {
134134
return nil, fmt.Errorf("python runtime selection: %w", selectorErr)
135135
}
136+
// A typed selector is authoritative execution scope. Derived pytest
137+
// commands may carry broad project discovery operands (for example,
138+
// `pytest --pyargs astropy docs {posargs}`). Appending one node ID to
139+
// those operands still collects unrelated modules, allowing their
140+
// failures to masquerade as the selected case. Keep the real runner
141+
// prefix and replace broad pytest discovery before adding the exact
142+
// plugin-rendered selector.
143+
if req.GetSelection() != nil {
144+
fspec.Command = pythonhelpers.CommandForExactSelection(fspec.Command)
145+
}
136146
fspec.Selectors = selectors
137147
s.Wool.Info("running test formula via uv",
138148
wool.Field("command", fspec.Command), wool.Field("output", fspec.Output))

‎pkg/runtime/runtime_test.go‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,55 @@ func TestRuntimeHonorsTypedSelectionWithDefaultRunner(t *testing.T) {
9696
assertRuntimeTestLeftSourceClean(t, dir)
9797
}
9898

99+
// TestRuntimeTypedSelectionReplacesBroadPytestDiscovery proves that the exact
100+
// semantic selector is the whole collection scope. The project formula is
101+
// intentionally broad and contains an unrelated collection crash; retaining
102+
// that operand would produce a false red result even though the selected case
103+
// passes. Real uv + pytest exercise the production boundary without mocks.
104+
func TestRuntimeTypedSelectionReplacesBroadPytestDiscovery(t *testing.T) {
105+
if _, err := exec.LookPath("uv"); err != nil {
106+
t.Fatalf("uv is required for the production Python runtime: %v", err)
107+
}
108+
root := t.TempDir()
109+
if err := os.MkdirAll(filepath.Join(root, "broadpkg"), 0o755); err != nil {
110+
t.Fatal(err)
111+
}
112+
for path, content := range map[string]string{
113+
"tox.ini": `[testenv]
114+
commands = pytest --pyargs broadpkg {posargs}
115+
`,
116+
"broadpkg/__init__.py": "",
117+
"broadpkg/test_broken.py": "raise RuntimeError('unselected collection must not run')\n",
118+
"test_selected.py": "def test_selected_pass():\n assert 2 + 2 == 4\n",
119+
} {
120+
if err := os.WriteFile(filepath.Join(root, filepath.FromSlash(path)), []byte(content), 0o644); err != nil {
121+
t.Fatal(err)
122+
}
123+
}
124+
125+
svc := pythonservice.New(&resources.Agent{Kind: "codefly:service", Name: "python"})
126+
svc.SourceLocation = root
127+
rt := pythonruntime.New(svc)
128+
req := &runtimev0.TestRequest{
129+
Selection: &runtimev0.TestSelection{Scope: &runtimev0.TestSelection_TestCase{TestCase: &runtimev0.TestCaseSelection{
130+
Path: "test_selected.py",
131+
QualifiedName: []string{"test_selected_pass"},
132+
}}},
133+
SelectionId: "formula-exact-python-selected-case",
134+
}
135+
resp, err := rt.Test(context.Background(), req)
136+
if err != nil {
137+
t.Fatalf("Test: %v", err)
138+
}
139+
if err := selectioncontract.VerifyAcknowledgement(req, resp); err != nil {
140+
t.Fatalf("selection acknowledgement: %v", err)
141+
}
142+
if resp.GetResult().GetState() != runtimev0.TestRunResult_PASSED || resp.GetCounts().GetTotal() != 1 || resp.GetCounts().GetPassed() != 1 {
143+
t.Fatalf("selected result = %s counts=%+v, want only one passing case\n%s", resp.GetResult().GetState(), resp.GetCounts(), resp.GetOutput())
144+
}
145+
assertRuntimeTestLeftSourceClean(t, root)
146+
}
147+
99148
// TestRuntimeFormulaNormalizesUnittestDisplaySelection proves the production
100149
// agent boundary accepts the canonical case identity returned by unittest
101150
// (`method (module.Class)`) and routes it back through a formula that consumes

0 commit comments

Comments
 (0)