diffcone 0.1.0__py3-none-any.whl

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.
diffcone/testing.py ADDED
@@ -0,0 +1,200 @@
1
+ """Helpers for writing scenario tests against diffcone.
2
+
3
+ A scenario is a throwaway git repository built from dicts of file contents,
4
+ committed as before/after snapshots, planned, and then checked for exact
5
+ target sets and reasons. These helpers are what diffcone's own acceptance
6
+ tests use; projects integrating diffcone can use them the same way.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import cast
17
+
18
+ from diffcone.cache import IndexCache
19
+ from diffcone.evidence import Evidence
20
+ from diffcone.manifest import Manifest, Target, parse_manifest
21
+ from diffcone.planner import Plan, Reason, plan
22
+ from diffcone.report import to_dict
23
+
24
+ # A hermetic git environment: no user config, deterministic identity.
25
+ GIT_ENV = {
26
+ "GIT_AUTHOR_NAME": "fixture",
27
+ "GIT_AUTHOR_EMAIL": "fixture@example.invalid",
28
+ "GIT_COMMITTER_NAME": "fixture",
29
+ "GIT_COMMITTER_EMAIL": "fixture@example.invalid",
30
+ "GIT_CONFIG_GLOBAL": "/dev/null",
31
+ "GIT_CONFIG_NOSYSTEM": "1",
32
+ "HOME": "/nonexistent",
33
+ }
34
+
35
+
36
+ class FixtureRepo:
37
+ """A throwaway git repository built from dicts of file contents.
38
+
39
+ With ``check_cache`` (diffcone's own suite turns it on) every ``plan``
40
+ call is also planned through a cold and a warm cache in ``cache_dir``
41
+ (default: ``.fixture-cache`` beside the repository) and the reports are
42
+ asserted equal, which triples its cost."""
43
+
44
+ def __init__(
45
+ self, path: Path, *, check_cache: bool = False, cache_dir: Path | None = None
46
+ ) -> None:
47
+ self.path = Path(path)
48
+ self.path.mkdir(parents=True, exist_ok=True)
49
+ self.check_cache = check_cache
50
+ self.cache_dir = Path(cache_dir) if cache_dir else self.path.parent / ".fixture-cache"
51
+ self.git("init", "-q", "-b", "main")
52
+
53
+ def try_git(self, *args: str) -> subprocess.CompletedProcess[str]:
54
+ """Run git and return the completed process, whatever its exit code."""
55
+ return subprocess.run(
56
+ ["git", *args], cwd=self.path, env=GIT_ENV, capture_output=True, text=True
57
+ )
58
+
59
+ def git(self, *args: str) -> str:
60
+ """Run git and return stripped stdout; raises on a non-zero exit."""
61
+ proc = self.try_git(*args)
62
+ if proc.returncode != 0:
63
+ raise RuntimeError(proc.stderr)
64
+ return proc.stdout.strip()
65
+
66
+ def commit(self, files: dict[str, str | bytes | None], message: str = "snapshot") -> str:
67
+ """Write (or delete, for ``None``) files and commit. Returns the sha.
68
+ ``bytes`` content is written as given, for a file whose encoding is
69
+ the point (a PEP 263 coding cookie)."""
70
+ for rel, content in files.items():
71
+ target = self.path / rel
72
+ if content is None:
73
+ target.unlink()
74
+ else:
75
+ target.parent.mkdir(parents=True, exist_ok=True)
76
+ if isinstance(content, bytes):
77
+ target.write_bytes(content)
78
+ else:
79
+ target.write_text(content, "utf-8")
80
+ self.git("add", "-A")
81
+ self.git("commit", "-q", "--allow-empty", "-m", message)
82
+ return self.git("rev-parse", "HEAD")
83
+
84
+ def plan(
85
+ self,
86
+ base: str,
87
+ head: str,
88
+ targets: list[Target] | list[dict],
89
+ source_roots: list[str] | None = None,
90
+ **kwargs,
91
+ ) -> Plan:
92
+ manifest = (
93
+ parse_manifest(targets)
94
+ if targets and isinstance(targets[0], dict)
95
+ else Manifest(cast(list[Target], list(targets)))
96
+ )
97
+ result = plan(self.path, base, head, manifest, source_roots=source_roots, **kwargs)
98
+ if self.check_cache and "cache" not in kwargs:
99
+ self._check_cache_invisible(result, base, head, manifest, source_roots, kwargs)
100
+ return result
101
+
102
+ def _check_cache_invisible(self, result, base, head, manifest, source_roots, kwargs) -> None:
103
+ """The caches are an optimisation only: a plan served cold (module
104
+ facts and resolution computed and stored) and one served warm (every
105
+ module's facts and resolution loaded, whole-index entries removed so
106
+ the module cache is what answers) must equal the uncached plan."""
107
+ directory = self.cache_dir
108
+ shutil.rmtree(directory, ignore_errors=True)
109
+ expected = json.dumps(to_dict(result), sort_keys=True)
110
+ for warm in (False, True):
111
+ if warm:
112
+ shutil.rmtree(directory / "index", ignore_errors=True)
113
+ cache = IndexCache(directory)
114
+ again = plan(
115
+ self.path, base, head, manifest, source_roots=source_roots, cache=cache, **kwargs
116
+ )
117
+ got = json.dumps(to_dict(again), sort_keys=True)
118
+ assert got == expected, f"{'warm' if warm else 'cold'} cached plan differs"
119
+
120
+ def collect(
121
+ self,
122
+ rev: str | None = None,
123
+ *,
124
+ source_roots: list[str] | None = None,
125
+ reverse_check: bool = False,
126
+ command: str | None = None,
127
+ extra: list[str] | None = None,
128
+ env_variables: list[str] | None = None,
129
+ ) -> Evidence:
130
+ """Record execution evidence (``diffcone collect``) at ``rev`` (default:
131
+ the clean checkout), running the suite with this interpreter's pytest."""
132
+ from diffcone.execution import collect_evidence # runs project code
133
+
134
+ return collect_evidence(
135
+ self.path,
136
+ command=command or f"{sys.executable} -m pytest",
137
+ source_roots=source_roots or ["."],
138
+ rev=rev,
139
+ reverse_check=reverse_check,
140
+ extra=extra,
141
+ env_variables=env_variables,
142
+ ).evidence
143
+
144
+ def write_manifest(self, targets: list[dict], name: str = "targets.json") -> Path:
145
+ path = self.path.parent / name
146
+ path.write_text(json.dumps({"targets": targets}), "utf-8")
147
+ return path
148
+
149
+
150
+ def py_target(runner_id: str, entry: str, *deps: str) -> Target:
151
+ return Target("pytest", runner_id, entry, tuple(deps))
152
+
153
+
154
+ def asv_target(runner_id: str, entry: str, *deps: str) -> Target:
155
+ return Target("asv", runner_id, entry, tuple(deps))
156
+
157
+
158
+ def selected(plan: Plan) -> set[str]:
159
+ return {d.target.runner_id for d in plan.decisions if d.selected}
160
+
161
+
162
+ def unselected(plan: Plan) -> set[str]:
163
+ return {d.target.runner_id for d in plan.decisions if not d.selected}
164
+
165
+
166
+ def rules(plan: Plan, runner_id: str) -> set[str]:
167
+ for d in plan.decisions:
168
+ if d.target.runner_id == runner_id:
169
+ return {r.rule for r in d.reasons}
170
+ raise KeyError(runner_id)
171
+
172
+
173
+ def reason(plan: Plan, runner_id: str, rule: str = "dependency") -> Reason:
174
+ for d in plan.decisions:
175
+ if d.target.runner_id == runner_id:
176
+ for r in d.reasons:
177
+ if r.rule == rule:
178
+ return r
179
+ raise KeyError((runner_id, rule))
180
+
181
+
182
+ def path_ids(reason: Reason) -> list[str]:
183
+ """Node ids along a reason's dependency path, source first."""
184
+ if not reason.path:
185
+ return []
186
+ return [reason.path[0].source] + [s.target for s in reason.path]
187
+
188
+
189
+ def executed(evidence: Evidence, test_id: str) -> set[str]:
190
+ """The symbols a test executed in the evidence run."""
191
+ return evidence.executed(evidence.tests[test_id])
192
+
193
+
194
+ def touched(evidence: Evidence, test_id: str) -> set[str]:
195
+ """The repository paths a test opened, stat'ed or listed in the evidence run."""
196
+ return evidence.touched(evidence.tests[test_id])
197
+
198
+
199
+ def changes(plan: Plan) -> dict[str, tuple[str, ...]]:
200
+ return {c.id: c.changes for c in plan.changes}
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: diffcone
3
+ Version: 0.1.0
4
+ Summary: Static-first, function-level change-impact engine for Python: which tests and benchmarks a change can affect, and why
5
+ Keywords: test selection,test impact analysis,change impact,pytest,asv,ci
6
+ Author: Bearing Research
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Framework :: Pytest
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Requires-Python: >=3.11
23
+ Project-URL: Homepage, https://github.com/bearing-research/diffcone
24
+ Project-URL: Documentation, https://bearing-research.github.io/diffcone/
25
+ Project-URL: Repository, https://github.com/bearing-research/diffcone
26
+ Project-URL: Issues, https://github.com/bearing-research/diffcone/issues
27
+ Project-URL: Changelog, https://github.com/bearing-research/diffcone/blob/main/CHANGELOG.md
28
+ Description-Content-Type: text/markdown
29
+
30
+ # diffcone
31
+
32
+ [![CI](https://github.com/bearing-research/diffcone/actions/workflows/ci.yml/badge.svg)](https://github.com/bearing-research/diffcone/actions/workflows/ci.yml)
33
+ [![Docs](https://github.com/bearing-research/diffcone/actions/workflows/docs.yml/badge.svg)](https://bearing-research.github.io/diffcone/)
34
+
35
+ **Run the tests your change can affect, and know why each one was picked.**
36
+
37
+ diffcone compares two versions of a Python repository, finds the functions,
38
+ methods and classes that changed, and follows the code's dependencies back
39
+ to the tests and benchmarks that can observe them. Each selected test comes
40
+ with the reason it was selected.
41
+
42
+ ```console
43
+ $ diffcone plan --base main --head WORKTREE --discover pytest \
44
+ --source-root src --source-root . --format text
45
+ ...
46
+ changed symbols (1):
47
+ calc.ops.mul [function] body_changed
48
+
49
+ selected targets (2):
50
+ pytest: tests/test_calc.py::test_mul
51
+ - dependency: calc.ops.mul body_changed
52
+ pytest: tests/test_calc.py::test_square
53
+ - dependency: calc.ops.mul body_changed
54
+
55
+ unselected targets (1):
56
+ pytest: tests/test_calc.py::test_add
57
+ ```
58
+
59
+ - **Every selection is explained:** the report shows the chain of calls and
60
+ references from each test to the change.
61
+ - **It errs on the side of running more:** when diffcone can't tell what a
62
+ change reaches, it selects more tests and says why.
63
+ - **It never runs your code to plan:** planning reads files and git history
64
+ only.
65
+ - **pytest and ASV:** tests and benchmarks are found from your
66
+ configuration and source, the way each runner collects them.
67
+ - **Execution evidence:** for large codebases where nearly everything
68
+ imports everything, record once what each test ran and plan from that.
69
+
70
+ ## Install
71
+
72
+ Python 3.11 or later and git; no other dependencies.
73
+
74
+ ```bash
75
+ uv tool install diffcone # or: pipx install diffcone, pip install diffcone
76
+ ```
77
+
78
+ ## Use
79
+
80
+ ```bash
81
+ # Which tests can my uncommitted changes affect?
82
+ diffcone plan --base main --head WORKTREE --discover pytest --format text
83
+
84
+ # Run only those tests (arguments after -- go to pytest)
85
+ diffcone run --base main --head WORKTREE --discover pytest --command "uv run pytest" -- -x
86
+
87
+ # Record what each test runs, then plan from the recording (Python 3.12+)
88
+ diffcone collect --command "uv run pytest" -- -n 8
89
+ diffcone plan --base main --head HEAD --discover pytest --evidence auto -o plan.json
90
+
91
+ # Did the plan select every test that failed in a full run?
92
+ diffcone check --plan plan.json --full full.xml
93
+ ```
94
+
95
+ For a `src` layout, add `--source-root src --source-root .` so modules get
96
+ their import names.
97
+
98
+ ## Documentation
99
+
100
+ Read the documentation at
101
+ **[bearing-research.github.io/diffcone](https://bearing-research.github.io/diffcone/)**:
102
+
103
+ - [Getting started](https://bearing-research.github.io/diffcone/latest/getting-started/)
104
+ - [How it works](https://bearing-research.github.io/diffcone/latest/concepts/)
105
+ - Guides: [planning](https://bearing-research.github.io/diffcone/latest/guides/planning/),
106
+ [running and checking](https://bearing-research.github.io/diffcone/latest/guides/running/),
107
+ [execution evidence](https://bearing-research.github.io/diffcone/latest/guides/evidence/),
108
+ [CI with GitHub Actions](https://bearing-research.github.io/diffcone/latest/ci/)
109
+ - [Command reference](https://bearing-research.github.io/diffcone/latest/reference/cli/)
110
+ and [limitations](https://bearing-research.github.io/diffcone/latest/limitations/)
111
+
112
+ ## Status
113
+
114
+ diffcone is alpha software. Its selections have been checked against the
115
+ full test suites of 34 open-source projects. See the
116
+ [changelog](https://github.com/bearing-research/diffcone/blob/main/CHANGELOG.md)
117
+ for what each release contains.
118
+
119
+ ## Contributing
120
+
121
+ ```bash
122
+ uv sync
123
+ uv run pytest
124
+ uv run ruff check src tests scripts && uv run ruff format --check src tests scripts
125
+ uv run ty check
126
+ ```
127
+
128
+ See the [development guide](https://bearing-research.github.io/diffcone/latest/development/)
129
+ and [AGENTS.md](https://github.com/bearing-research/diffcone/blob/main/AGENTS.md).
130
+
131
+ ## License
132
+
133
+ MIT; see [LICENSE](https://github.com/bearing-research/diffcone/blob/main/LICENSE).
@@ -0,0 +1,39 @@
1
+ diffcone/__init__.py,sha256=qbazijAQfmvdP791Jriky0uZL6VMvS-qABvBK5QMBMM,477
2
+ diffcone/cache.py,sha256=ktkJKneiz4IDispcWoi9XCwoCLS5HgXUUqHvpHefne8,20828
3
+ diffcone/check.py,sha256=BqV4LAIweJfzMjcPfNgjPjbXw7hpjt9JdahBvN2o0Bs,14614
4
+ diffcone/classify.py,sha256=u4FMKNl-dhRBfb16ASqFhtXRO_XHCS8lKhPyiLNfPVQ,11283
5
+ diffcone/cli.py,sha256=tnYzvjJI0bW4-reHehlPH_szEWHTfQpGTIZDXhIFIZI,37202
6
+ diffcone/collect.py,sha256=v5b4j969hMuBH6qZjcAI0zy-MM6jGns2lCtkf4EC5ug,31346
7
+ diffcone/cython.py,sha256=nBuP7E1wEjkcBMu0sQX-JU01ryrNHvDwKBIeowVY5L8,30796
8
+ diffcone/declarations.py,sha256=x_A9B2zcHN5sc5BHrHxMVeUClgWAufitmJxCl1H-4cM,4392
9
+ diffcone/discovery/__init__.py,sha256=19K0VkGMyXogobN6EVSQRitKwxqcN7SELMBLgOUcc7c,4325
10
+ diffcone/discovery/asv_static.py,sha256=jT0w2wInSMtmnEW9VCQrquAabsRDWRA8FL2RrrMgk88,21186
11
+ diffcone/discovery/common.py,sha256=Km1jOQZBziVDFgpSLaLkt4Nw0CB8cqbOUARejij56Jg,4609
12
+ diffcone/discovery/pytest_static.py,sha256=A9Yu6xotz4f7IJTTYdglJ-ignXerBC40KVZE75bPEOk,114194
13
+ diffcone/evidence.py,sha256=p5fzQVzLlkNs0WSioGaW6fMc9_ZFqm-rKxA34qgpGcY,31677
14
+ diffcone/evidence_plan.py,sha256=ZplypghCuEJQdz-_4Oj2a4X0452micT1ojNq_f71Lmw,65291
15
+ diffcone/execution.py,sha256=aUU8Wqo_meAzl9YWCKxEpUqvvuJu_d8J3RcYpl8UOTA,68854
16
+ diffcone/indexer/__init__.py,sha256=Xn4Ryd1kXnv-xSYoAFWlO07xwgN3GDwv5_-F8N9fn90,1803
17
+ diffcone/indexer/core.py,sha256=YfpHteoZg8JZtqXVa5Whb3GzLJd0LhQwpwz4KnpoMKc,13870
18
+ diffcone/indexer/definitions.py,sha256=KdvixZbN_MuQ-HyEqzCRZQ5Kmr7Q6TXUO0s3wtkOYl8,12213
19
+ diffcone/indexer/dynamics.py,sha256=tgFwuVET0S9h7D1I3RyV-jgfxSo4R75VrEFqbc-YwMc,18549
20
+ diffcone/indexer/facts.py,sha256=ADPGaFPP0U71Ow1OHbO-1Q2RhOcT9fFQYYIQUA0Iyws,15445
21
+ diffcone/indexer/literals.py,sha256=VtT_oq2uva66gU2RC1-Vc6eqpGXUMZtSpXbtvuMFBwY,15149
22
+ diffcone/indexer/references.py,sha256=SnKp_-UnTFvxutoirSfRcNB4CT5ONqsr6l42_r-Y1qU,40764
23
+ diffcone/indexer/resolver.py,sha256=YH0V7lXKgoKEFOIgIiT9f4e7NWbLJKK7127SVWGXzjE,47786
24
+ diffcone/indexer/scopes.py,sha256=EmgLdevc6wonGpSmM1lBeZLj1xnwfsoxHhbHs7jj33w,11965
25
+ diffcone/indexer/state.py,sha256=bhwAVtfiyiG5m6F8N3o-_sOvVfjRs_DBZjFsjLexKwg,3656
26
+ diffcone/indexer/symbols.py,sha256=hwgs_OjCbykwKu7E2lIuIdoyOnVMzuL-w090xTJFcD0,19548
27
+ diffcone/indexer/syntax.py,sha256=X6e6zISgrXmJyPiQEW4c88KqxtyCfW9pDBqmJhxlxzw,5834
28
+ diffcone/manifest.py,sha256=fK-uslZe8hemg55tKwM8AFNsT-tXXB6YYvK0EQmtonA,6294
29
+ diffcone/model.py,sha256=y1Cy1JjG9OcI6XrurLgGj3F2bVOg9bEqMwGeFDQvi_c,8053
30
+ diffcone/planner.py,sha256=eEbNrbOm0UrkXLw2xzv0U3YJXIH0GRmsJ4qhe9XDsVw,59740
31
+ diffcone/report.py,sha256=8LY7MYeQ9zHnmHcvz4V0uwjncbj13u66A38fQbNnnf8,10174
32
+ diffcone/selection.py,sha256=n8jQPf2qzsSwwa20aKiJ7oCqs3ND8OfC4fapzGF3gWA,5560
33
+ diffcone/snapshot.py,sha256=LGHBT9jJZKXbH_0rry4Fyt_F4JJJDwLnaskVtRTA3jE,29371
34
+ diffcone/testing.py,sha256=THE6CT65nY_LewL1pFZgxrYbtN805abvdWxHKcFKsho,7588
35
+ diffcone-0.1.0.dist-info/licenses/LICENSE,sha256=jX-OPRzCDHug3I-cqFfFZqbcCFPNSc9Y9E5c7xcobf4,1073
36
+ diffcone-0.1.0.dist-info/WHEEL,sha256=cmC5s21ojypbVslldL7IJq3hjZH-tINy4rziKePFsG0,81
37
+ diffcone-0.1.0.dist-info/entry_points.txt,sha256=8egIaUvWcHazoYp6jTI0MLzpDqKtDRJju-07DXTayYg,48
38
+ diffcone-0.1.0.dist-info/METADATA,sha256=Rn5wKkqRAh3ltN8y47G1tiyhBfW8CChxN5MnIgAzwEU,5475
39
+ diffcone-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.23
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ diffcone = diffcone.cli:main
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bearing Research
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.