opentia 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opentia-1.0.0/LICENSE +21 -0
- opentia-1.0.0/PKG-INFO +284 -0
- opentia-1.0.0/README.md +261 -0
- opentia-1.0.0/assess_impact.py +2049 -0
- opentia-1.0.0/opentia.egg-info/PKG-INFO +284 -0
- opentia-1.0.0/opentia.egg-info/SOURCES.txt +9 -0
- opentia-1.0.0/opentia.egg-info/dependency_links.txt +1 -0
- opentia-1.0.0/opentia.egg-info/entry_points.txt +2 -0
- opentia-1.0.0/opentia.egg-info/top_level.txt +1 -0
- opentia-1.0.0/pyproject.toml +34 -0
- opentia-1.0.0/setup.cfg +4 -0
opentia-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 byterey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
opentia-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opentia
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Test Impact Analysis — selects only tests affected by a git diff
|
|
5
|
+
Author-email: byterey <reymund.lapera@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: testing,test-impact-analysis,dotnet,csharp,java,nodejs,tia
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Test Impact Analysis — C# / .NET
|
|
25
|
+
|
|
26
|
+
Analyses a git diff and selects only the tests whose execution path could have been affected by the change. Skips the full suite on every push.
|
|
27
|
+
|
|
28
|
+
**No external dependencies** — Python 3.8+ stdlib only.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
| Tool | Version | Purpose |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| Python | 3.8+ | Run the script |
|
|
37
|
+
| Git | any | Diff source |
|
|
38
|
+
| .NET SDK | 8.0+ | Build and run `sample-app` tests |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Analyse the last commit against the one before it
|
|
46
|
+
python assess_impact.py --base HEAD~1 --root <path-to-your-solution>
|
|
47
|
+
|
|
48
|
+
# Analyse uncommitted (staged + unstaged) changes — no commit needed
|
|
49
|
+
python assess_impact.py --unstaged --root <path-to-your-solution>
|
|
50
|
+
|
|
51
|
+
# Analyse and immediately run the selected tests
|
|
52
|
+
python assess_impact.py --base HEAD~1 --root <path-to-your-solution> --run
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`--root` is where the `.sln` / `.csproj` files live. It does not need to be the git root — the script locates the actual git root automatically via `git rev-parse --show-toplevel`.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Validating the script with sample-app
|
|
60
|
+
|
|
61
|
+
`sample-app/` is a self-contained C# solution purpose-built to verify the script against realistic dependency scenarios.
|
|
62
|
+
|
|
63
|
+
### Dependency graph
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
SampleApp.Core (no deps) ← 92 tests in SampleApp.Core.Tests
|
|
67
|
+
↑
|
|
68
|
+
SampleApp.Services (depends on Core) ← 60 tests in SampleApp.Services.Tests
|
|
69
|
+
↑
|
|
70
|
+
SampleApp.Api (depends on Services) [no test project]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Key implication:** changing anything in `Core` triggers **both** test projects because `Services` depends on `Core` — the BFS propagates transitively.
|
|
74
|
+
|
|
75
|
+
### Step 1 — Confirm baseline
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cd sample-app
|
|
79
|
+
dotnet test SampleApp.sln
|
|
80
|
+
# Expected: 152 passed, 0 failed
|
|
81
|
+
cd ..
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Step 2 — Run validation scenarios
|
|
85
|
+
|
|
86
|
+
Each scenario tests a distinct behaviour. Use `--unstaged` to avoid committing:
|
|
87
|
+
|
|
88
|
+
**Scenario 1 — Ignored file → no tests selected**
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
echo "# change" >> sample-app/.gitignore
|
|
92
|
+
python assess_impact.py --unstaged --root sample-app
|
|
93
|
+
# Expected: no tests to run (file type is ignored)
|
|
94
|
+
git checkout sample-app/.gitignore
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Scenario 2 — Services-only change → only `Services.Tests`**
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
echo "// change" >> sample-app/src/SampleApp.Services/PricingService.cs
|
|
101
|
+
python assess_impact.py --unstaged --root sample-app
|
|
102
|
+
# Expected: SampleApp.Services.Tests only, filtered to PricingServiceTests
|
|
103
|
+
git checkout sample-app/src/SampleApp.Services/PricingService.cs
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Scenario 3 — Core change → both test projects (transitive)**
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
echo "// change" >> sample-app/src/SampleApp.Core/Utilities/MathHelper.cs
|
|
110
|
+
python assess_impact.py --unstaged --root sample-app
|
|
111
|
+
# Expected: SampleApp.Core.Tests AND SampleApp.Services.Tests
|
|
112
|
+
# Services.Tests is included because Services depends on Core (BFS)
|
|
113
|
+
git checkout sample-app/src/SampleApp.Core/Utilities/MathHelper.cs
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Scenario 4 — Infrastructure change → all tests forced**
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
echo " " >> sample-app/SampleApp.sln
|
|
120
|
+
python assess_impact.py --unstaged --root sample-app
|
|
121
|
+
# Expected: run_all = true, all test projects
|
|
122
|
+
git checkout sample-app/SampleApp.sln
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Step 3 — Read the output
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
──────────────────────────────────────────────────────────────────
|
|
129
|
+
TEST IMPACT ANALYSIS
|
|
130
|
+
──────────────────────────────────────────────────────────────────
|
|
131
|
+
Status : Targeted run ← "RUN ALL TESTS" means a fallback was triggered
|
|
132
|
+
Affected test projects (1):
|
|
133
|
+
• SampleApp.Services.Tests
|
|
134
|
+
Affected test classes (1):
|
|
135
|
+
• PricingServiceTests
|
|
136
|
+
dotnet command:
|
|
137
|
+
dotnet test "...SampleApp.Services.Tests.csproj" --filter "FullyQualifiedName~PricingServiceTests"
|
|
138
|
+
──────────────────────────────────────────────────────────────────
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`Status: Targeted run` means the script selected a subset. `Status: RUN ALL TESTS` means it fell back to running everything (expected for Scenario 4).
|
|
142
|
+
|
|
143
|
+
### Additional scenarios
|
|
144
|
+
|
|
145
|
+
| File to change | Expected result |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `src/SampleApp.Core/Models/Product.cs` | Both test projects, filtered to `ProductTests` + dependent service tests |
|
|
148
|
+
| `tests/SampleApp.Core.Tests/Utilities/StringHelperTests.cs` | `Core.Tests` only, filtered to `StringHelperTests` |
|
|
149
|
+
| `src/SampleApp.Services/appsettings.json` | `Services.Tests`, no class filter (config file) |
|
|
150
|
+
| `src/SampleApp.Services/SampleApp.Services.csproj` | All tests, `run_all = true` (infra file) |
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Usage reference
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
python assess_impact.py [OPTIONS] [-- DOTNET_ARGS]
|
|
158
|
+
|
|
159
|
+
--base REF Git ref to diff against (e.g. HEAD~1, main, origin/main)
|
|
160
|
+
--head REF Head ref to diff from (default: HEAD)
|
|
161
|
+
--root DIR Directory containing .sln / .csproj files (default: .)
|
|
162
|
+
--strategy project | convention | symbol | hybrid (default: hybrid)
|
|
163
|
+
--output, -o human | json | github-actions | azure-devops (default: human)
|
|
164
|
+
--run Execute dotnet test after analysis
|
|
165
|
+
--unstaged Analyse working-tree changes instead of a git diff
|
|
166
|
+
-- Everything after this is forwarded to dotnet test
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Strategies
|
|
170
|
+
|
|
171
|
+
| Strategy | What it does |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `project` | Parse `.sln`/`.csproj` to find which test projects reference the changed source project (BFS-transitive through the dependency graph) |
|
|
174
|
+
| `convention` | `FooService.cs` → look for `FooServiceTests.cs`, `FooServiceTest.cs`, `TestFooService.cs` |
|
|
175
|
+
| `symbol` | Extract `public class/interface/enum` names from the changed file; grep all test `.cs` files for references |
|
|
176
|
+
| `hybrid` | All three combined (default) |
|
|
177
|
+
|
|
178
|
+
### Output formats
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Human-readable (default)
|
|
182
|
+
python assess_impact.py --base HEAD~1 --root sample-app
|
|
183
|
+
|
|
184
|
+
# JSON — pipe into scripts or CI steps
|
|
185
|
+
python assess_impact.py --base HEAD~1 --root sample-app --output json
|
|
186
|
+
|
|
187
|
+
# GitHub Actions — prints `echo "key=value" >> $GITHUB_OUTPUT` lines
|
|
188
|
+
python assess_impact.py --base HEAD~1 --root sample-app --output github-actions
|
|
189
|
+
|
|
190
|
+
# Azure DevOps — prints `##vso[task.setvariable ...]` lines
|
|
191
|
+
python assess_impact.py --base HEAD~1 --root sample-app --output azure-devops
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### JSON output fields
|
|
195
|
+
|
|
196
|
+
```jsonc
|
|
197
|
+
{
|
|
198
|
+
"run_all": false, // true = targeted selection was abandoned
|
|
199
|
+
"test_filter": "FullyQualifiedName~PricingServiceTests",
|
|
200
|
+
"test_project_paths": ["...SampleApp.Services.Tests.csproj"],
|
|
201
|
+
"affected_test_projects": ["SampleApp.Services.Tests"],
|
|
202
|
+
"affected_test_classes": ["PricingServiceTests"],
|
|
203
|
+
"dotnet_command": "dotnet test \"...\" --filter \"...\"",
|
|
204
|
+
"reason": "Analysis complete",
|
|
205
|
+
"strategy_notes": [] // warnings / fallback explanations
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## CI integration
|
|
212
|
+
|
|
213
|
+
### GitHub Actions
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
- name: Test Impact Analysis
|
|
217
|
+
id: tia
|
|
218
|
+
run: python assess_impact.py --base ${{ github.event.before }} --root sample-app --output github-actions
|
|
219
|
+
|
|
220
|
+
- name: Run affected tests
|
|
221
|
+
if: steps.tia.outputs.has_tests == 'true'
|
|
222
|
+
run: ${{ steps.tia.outputs.dotnet_command }}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Available outputs: `test_filter`, `run_all`, `has_tests`, `test_project_paths`, `dotnet_command`.
|
|
226
|
+
|
|
227
|
+
### Azure DevOps
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
- script: python assess_impact.py --base $(System.PullRequest.TargetBranch) --root sample-app --output azure-devops
|
|
231
|
+
displayName: Test Impact Analysis
|
|
232
|
+
|
|
233
|
+
- script: $(dotnetCommand)
|
|
234
|
+
condition: eq(variables['hasTests'], 'true')
|
|
235
|
+
displayName: Run affected tests
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Available variables: `testFilter`, `runAllTests`, `hasTests`, `testProjectPaths`, `dotnetCommand`.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## How it works
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
git diff --name-status base..head
|
|
246
|
+
↓
|
|
247
|
+
classify each file → INFRA | IGNORED | CS_SOURCE | CONFIG | UNKNOWN
|
|
248
|
+
↓
|
|
249
|
+
INFRA → run all tests
|
|
250
|
+
IGNORED→ skip
|
|
251
|
+
UNKNOWN→ run all tests (safe fallback)
|
|
252
|
+
CS_SOURCE / CONFIG → run 3 strategies ↓
|
|
253
|
+
|
|
254
|
+
discover_projects() parse .sln + glob .csproj (excludes obj/ bin/)
|
|
255
|
+
build_reverse_deps() BFS: {source_project → set of test projects that cover it}
|
|
256
|
+
↓ per CS_SOURCE file:
|
|
257
|
+
strategy 1: project dependency graph (ownership → reverse dep lookup)
|
|
258
|
+
strategy 2: convention mapping (FooService → FooServiceTests)
|
|
259
|
+
strategy 3: symbol search (public types → grep cached test files)
|
|
260
|
+
↓ per CONFIG file:
|
|
261
|
+
find owning project → run its test projects (no class filter)
|
|
262
|
+
↓
|
|
263
|
+
build_filter() FullyQualifiedName~A|FullyQualifiedName~B
|
|
264
|
+
capped at 40 classes; drops to project-level if exceeded
|
|
265
|
+
↓
|
|
266
|
+
ImpactResult → formatter → stdout
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Fallback escalation** — rather than silently skipping tests, the script escalates:
|
|
270
|
+
1. `.cs` file not owned by any known project → run all test projects
|
|
271
|
+
2. Source project changed but dependency graph finds no covering tests → run all test projects
|
|
272
|
+
3. Config file not owned by any project → run all test projects
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Extending to other languages
|
|
277
|
+
|
|
278
|
+
To add Python, Node.js, or Java support, provide:
|
|
279
|
+
|
|
280
|
+
1. A `discover_projects()` equivalent that returns `List[Project]` for that ecosystem
|
|
281
|
+
2. Three strategy functions matching the signatures of `strategy_dependency_graph`, `strategy_convention`, `strategy_symbol_search`
|
|
282
|
+
3. New entries in `INFRA_EXTENSIONS`, `IGNORED_EXTENSIONS`, and `CONFIG_EXTENSIONS` for that ecosystem's file types
|
|
283
|
+
|
|
284
|
+
The git analysis, file classification pipeline, output formatters, and CI integration are language-agnostic and require no changes.
|
opentia-1.0.0/README.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Test Impact Analysis — C# / .NET
|
|
2
|
+
|
|
3
|
+
Analyses a git diff and selects only the tests whose execution path could have been affected by the change. Skips the full suite on every push.
|
|
4
|
+
|
|
5
|
+
**No external dependencies** — Python 3.8+ stdlib only.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
| Tool | Version | Purpose |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Python | 3.8+ | Run the script |
|
|
14
|
+
| Git | any | Diff source |
|
|
15
|
+
| .NET SDK | 8.0+ | Build and run `sample-app` tests |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Analyse the last commit against the one before it
|
|
23
|
+
python assess_impact.py --base HEAD~1 --root <path-to-your-solution>
|
|
24
|
+
|
|
25
|
+
# Analyse uncommitted (staged + unstaged) changes — no commit needed
|
|
26
|
+
python assess_impact.py --unstaged --root <path-to-your-solution>
|
|
27
|
+
|
|
28
|
+
# Analyse and immediately run the selected tests
|
|
29
|
+
python assess_impact.py --base HEAD~1 --root <path-to-your-solution> --run
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`--root` is where the `.sln` / `.csproj` files live. It does not need to be the git root — the script locates the actual git root automatically via `git rev-parse --show-toplevel`.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Validating the script with sample-app
|
|
37
|
+
|
|
38
|
+
`sample-app/` is a self-contained C# solution purpose-built to verify the script against realistic dependency scenarios.
|
|
39
|
+
|
|
40
|
+
### Dependency graph
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
SampleApp.Core (no deps) ← 92 tests in SampleApp.Core.Tests
|
|
44
|
+
↑
|
|
45
|
+
SampleApp.Services (depends on Core) ← 60 tests in SampleApp.Services.Tests
|
|
46
|
+
↑
|
|
47
|
+
SampleApp.Api (depends on Services) [no test project]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Key implication:** changing anything in `Core` triggers **both** test projects because `Services` depends on `Core` — the BFS propagates transitively.
|
|
51
|
+
|
|
52
|
+
### Step 1 — Confirm baseline
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
cd sample-app
|
|
56
|
+
dotnet test SampleApp.sln
|
|
57
|
+
# Expected: 152 passed, 0 failed
|
|
58
|
+
cd ..
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Step 2 — Run validation scenarios
|
|
62
|
+
|
|
63
|
+
Each scenario tests a distinct behaviour. Use `--unstaged` to avoid committing:
|
|
64
|
+
|
|
65
|
+
**Scenario 1 — Ignored file → no tests selected**
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
echo "# change" >> sample-app/.gitignore
|
|
69
|
+
python assess_impact.py --unstaged --root sample-app
|
|
70
|
+
# Expected: no tests to run (file type is ignored)
|
|
71
|
+
git checkout sample-app/.gitignore
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Scenario 2 — Services-only change → only `Services.Tests`**
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
echo "// change" >> sample-app/src/SampleApp.Services/PricingService.cs
|
|
78
|
+
python assess_impact.py --unstaged --root sample-app
|
|
79
|
+
# Expected: SampleApp.Services.Tests only, filtered to PricingServiceTests
|
|
80
|
+
git checkout sample-app/src/SampleApp.Services/PricingService.cs
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Scenario 3 — Core change → both test projects (transitive)**
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
echo "// change" >> sample-app/src/SampleApp.Core/Utilities/MathHelper.cs
|
|
87
|
+
python assess_impact.py --unstaged --root sample-app
|
|
88
|
+
# Expected: SampleApp.Core.Tests AND SampleApp.Services.Tests
|
|
89
|
+
# Services.Tests is included because Services depends on Core (BFS)
|
|
90
|
+
git checkout sample-app/src/SampleApp.Core/Utilities/MathHelper.cs
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Scenario 4 — Infrastructure change → all tests forced**
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
echo " " >> sample-app/SampleApp.sln
|
|
97
|
+
python assess_impact.py --unstaged --root sample-app
|
|
98
|
+
# Expected: run_all = true, all test projects
|
|
99
|
+
git checkout sample-app/SampleApp.sln
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Step 3 — Read the output
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
──────────────────────────────────────────────────────────────────
|
|
106
|
+
TEST IMPACT ANALYSIS
|
|
107
|
+
──────────────────────────────────────────────────────────────────
|
|
108
|
+
Status : Targeted run ← "RUN ALL TESTS" means a fallback was triggered
|
|
109
|
+
Affected test projects (1):
|
|
110
|
+
• SampleApp.Services.Tests
|
|
111
|
+
Affected test classes (1):
|
|
112
|
+
• PricingServiceTests
|
|
113
|
+
dotnet command:
|
|
114
|
+
dotnet test "...SampleApp.Services.Tests.csproj" --filter "FullyQualifiedName~PricingServiceTests"
|
|
115
|
+
──────────────────────────────────────────────────────────────────
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`Status: Targeted run` means the script selected a subset. `Status: RUN ALL TESTS` means it fell back to running everything (expected for Scenario 4).
|
|
119
|
+
|
|
120
|
+
### Additional scenarios
|
|
121
|
+
|
|
122
|
+
| File to change | Expected result |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `src/SampleApp.Core/Models/Product.cs` | Both test projects, filtered to `ProductTests` + dependent service tests |
|
|
125
|
+
| `tests/SampleApp.Core.Tests/Utilities/StringHelperTests.cs` | `Core.Tests` only, filtered to `StringHelperTests` |
|
|
126
|
+
| `src/SampleApp.Services/appsettings.json` | `Services.Tests`, no class filter (config file) |
|
|
127
|
+
| `src/SampleApp.Services/SampleApp.Services.csproj` | All tests, `run_all = true` (infra file) |
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Usage reference
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
python assess_impact.py [OPTIONS] [-- DOTNET_ARGS]
|
|
135
|
+
|
|
136
|
+
--base REF Git ref to diff against (e.g. HEAD~1, main, origin/main)
|
|
137
|
+
--head REF Head ref to diff from (default: HEAD)
|
|
138
|
+
--root DIR Directory containing .sln / .csproj files (default: .)
|
|
139
|
+
--strategy project | convention | symbol | hybrid (default: hybrid)
|
|
140
|
+
--output, -o human | json | github-actions | azure-devops (default: human)
|
|
141
|
+
--run Execute dotnet test after analysis
|
|
142
|
+
--unstaged Analyse working-tree changes instead of a git diff
|
|
143
|
+
-- Everything after this is forwarded to dotnet test
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Strategies
|
|
147
|
+
|
|
148
|
+
| Strategy | What it does |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `project` | Parse `.sln`/`.csproj` to find which test projects reference the changed source project (BFS-transitive through the dependency graph) |
|
|
151
|
+
| `convention` | `FooService.cs` → look for `FooServiceTests.cs`, `FooServiceTest.cs`, `TestFooService.cs` |
|
|
152
|
+
| `symbol` | Extract `public class/interface/enum` names from the changed file; grep all test `.cs` files for references |
|
|
153
|
+
| `hybrid` | All three combined (default) |
|
|
154
|
+
|
|
155
|
+
### Output formats
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Human-readable (default)
|
|
159
|
+
python assess_impact.py --base HEAD~1 --root sample-app
|
|
160
|
+
|
|
161
|
+
# JSON — pipe into scripts or CI steps
|
|
162
|
+
python assess_impact.py --base HEAD~1 --root sample-app --output json
|
|
163
|
+
|
|
164
|
+
# GitHub Actions — prints `echo "key=value" >> $GITHUB_OUTPUT` lines
|
|
165
|
+
python assess_impact.py --base HEAD~1 --root sample-app --output github-actions
|
|
166
|
+
|
|
167
|
+
# Azure DevOps — prints `##vso[task.setvariable ...]` lines
|
|
168
|
+
python assess_impact.py --base HEAD~1 --root sample-app --output azure-devops
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### JSON output fields
|
|
172
|
+
|
|
173
|
+
```jsonc
|
|
174
|
+
{
|
|
175
|
+
"run_all": false, // true = targeted selection was abandoned
|
|
176
|
+
"test_filter": "FullyQualifiedName~PricingServiceTests",
|
|
177
|
+
"test_project_paths": ["...SampleApp.Services.Tests.csproj"],
|
|
178
|
+
"affected_test_projects": ["SampleApp.Services.Tests"],
|
|
179
|
+
"affected_test_classes": ["PricingServiceTests"],
|
|
180
|
+
"dotnet_command": "dotnet test \"...\" --filter \"...\"",
|
|
181
|
+
"reason": "Analysis complete",
|
|
182
|
+
"strategy_notes": [] // warnings / fallback explanations
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## CI integration
|
|
189
|
+
|
|
190
|
+
### GitHub Actions
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
- name: Test Impact Analysis
|
|
194
|
+
id: tia
|
|
195
|
+
run: python assess_impact.py --base ${{ github.event.before }} --root sample-app --output github-actions
|
|
196
|
+
|
|
197
|
+
- name: Run affected tests
|
|
198
|
+
if: steps.tia.outputs.has_tests == 'true'
|
|
199
|
+
run: ${{ steps.tia.outputs.dotnet_command }}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Available outputs: `test_filter`, `run_all`, `has_tests`, `test_project_paths`, `dotnet_command`.
|
|
203
|
+
|
|
204
|
+
### Azure DevOps
|
|
205
|
+
|
|
206
|
+
```yaml
|
|
207
|
+
- script: python assess_impact.py --base $(System.PullRequest.TargetBranch) --root sample-app --output azure-devops
|
|
208
|
+
displayName: Test Impact Analysis
|
|
209
|
+
|
|
210
|
+
- script: $(dotnetCommand)
|
|
211
|
+
condition: eq(variables['hasTests'], 'true')
|
|
212
|
+
displayName: Run affected tests
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Available variables: `testFilter`, `runAllTests`, `hasTests`, `testProjectPaths`, `dotnetCommand`.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## How it works
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
git diff --name-status base..head
|
|
223
|
+
↓
|
|
224
|
+
classify each file → INFRA | IGNORED | CS_SOURCE | CONFIG | UNKNOWN
|
|
225
|
+
↓
|
|
226
|
+
INFRA → run all tests
|
|
227
|
+
IGNORED→ skip
|
|
228
|
+
UNKNOWN→ run all tests (safe fallback)
|
|
229
|
+
CS_SOURCE / CONFIG → run 3 strategies ↓
|
|
230
|
+
|
|
231
|
+
discover_projects() parse .sln + glob .csproj (excludes obj/ bin/)
|
|
232
|
+
build_reverse_deps() BFS: {source_project → set of test projects that cover it}
|
|
233
|
+
↓ per CS_SOURCE file:
|
|
234
|
+
strategy 1: project dependency graph (ownership → reverse dep lookup)
|
|
235
|
+
strategy 2: convention mapping (FooService → FooServiceTests)
|
|
236
|
+
strategy 3: symbol search (public types → grep cached test files)
|
|
237
|
+
↓ per CONFIG file:
|
|
238
|
+
find owning project → run its test projects (no class filter)
|
|
239
|
+
↓
|
|
240
|
+
build_filter() FullyQualifiedName~A|FullyQualifiedName~B
|
|
241
|
+
capped at 40 classes; drops to project-level if exceeded
|
|
242
|
+
↓
|
|
243
|
+
ImpactResult → formatter → stdout
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Fallback escalation** — rather than silently skipping tests, the script escalates:
|
|
247
|
+
1. `.cs` file not owned by any known project → run all test projects
|
|
248
|
+
2. Source project changed but dependency graph finds no covering tests → run all test projects
|
|
249
|
+
3. Config file not owned by any project → run all test projects
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Extending to other languages
|
|
254
|
+
|
|
255
|
+
To add Python, Node.js, or Java support, provide:
|
|
256
|
+
|
|
257
|
+
1. A `discover_projects()` equivalent that returns `List[Project]` for that ecosystem
|
|
258
|
+
2. Three strategy functions matching the signatures of `strategy_dependency_graph`, `strategy_convention`, `strategy_symbol_search`
|
|
259
|
+
3. New entries in `INFRA_EXTENSIONS`, `IGNORED_EXTENSIONS`, and `CONFIG_EXTENSIONS` for that ecosystem's file types
|
|
260
|
+
|
|
261
|
+
The git analysis, file classification pipeline, output formatters, and CI integration are language-agnostic and require no changes.
|