Skip to content

Commit 33f19ad

Browse files
Merge pull request #1196 from RonnyPfannschmidt/steamline-tests
refactor testing infra around configruation and assertions
2 parents 54fa4b1 + 0f78a7e commit 33f19ad

17 files changed

+756
-174
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ test = [
6767
"build",
6868
"pytest",
6969
"pytest-timeout", # Timeout protection for CI/CD
70+
"pytest-xdist",
7071
"rich",
7172
"ruff",
7273
"mypy~=1.13.0", # pinned to old for python 3.8

src/setuptools_scm/_cli.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
from setuptools_scm import Configuration
1212
from setuptools_scm._file_finders import find_files
1313
from setuptools_scm._get_version_impl import _get_version
14+
from setuptools_scm._integration.pyproject_reading import PyProjectData
1415
from setuptools_scm.discover import walk_potential_roots
1516

1617

17-
def main(args: list[str] | None = None) -> int:
18+
def main(
19+
args: list[str] | None = None, *, _given_pyproject_data: PyProjectData | None = None
20+
) -> int:
1821
opts = _get_cli_opts(args)
1922
inferred_root: str = opts.root or "."
2023

@@ -24,6 +27,7 @@ def main(args: list[str] | None = None) -> int:
2427
config = Configuration.from_file(
2528
pyproject,
2629
root=(os.path.abspath(opts.root) if opts.root is not None else None),
30+
pyproject_data=_given_pyproject_data,
2731
)
2832
except (LookupError, FileNotFoundError) as ex:
2933
# no pyproject.toml OR no [tool.setuptools_scm]

src/setuptools_scm/version.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
from typing_extensions import Concatenate
2929
from typing_extensions import ParamSpec
3030

31+
if sys.version_info >= (3, 11):
32+
from typing import Unpack
33+
else:
34+
from typing_extensions import Unpack
35+
3136
_P = ParamSpec("_P")
3237

3338
from typing import TypedDict
@@ -51,6 +56,52 @@ class _TagDict(TypedDict):
5156
suffix: str
5257

5358

59+
class VersionExpectations(TypedDict, total=False):
60+
"""Expected properties for ScmVersion matching."""
61+
62+
tag: str | _VersionT
63+
distance: int
64+
dirty: bool
65+
node_prefix: str # Prefix of the node/commit hash
66+
branch: str | None
67+
exact: bool
68+
preformatted: bool
69+
node_date: date | None
70+
time: datetime | None
71+
72+
73+
@dataclasses.dataclass
74+
class mismatches:
75+
"""Represents mismatches between expected and actual ScmVersion properties."""
76+
77+
expected: dict[str, Any]
78+
actual: dict[str, Any]
79+
80+
def __bool__(self) -> bool:
81+
"""mismatches is falsy to allow `if not version.matches(...)`."""
82+
return False
83+
84+
def __str__(self) -> str:
85+
"""Format mismatches for error reporting."""
86+
lines = []
87+
for key, exp_val in self.expected.items():
88+
if key == "node_prefix":
89+
# Special handling for node prefix matching
90+
actual_node = self.actual.get("node")
91+
if not actual_node or not actual_node.startswith(exp_val):
92+
lines.append(
93+
f" node: expected prefix '{exp_val}', got '{actual_node}'"
94+
)
95+
else:
96+
act_val = self.actual.get(key)
97+
if str(exp_val) != str(act_val):
98+
lines.append(f" {key}: expected {exp_val!r}, got {act_val!r}")
99+
return "\n".join(lines)
100+
101+
def __repr__(self) -> str:
102+
return f"mismatches(expected={self.expected!r}, actual={self.actual!r})"
103+
104+
54105
def _parse_version_tag(
55106
tag: str | object, config: _config.Configuration
56107
) -> _TagDict | None:
@@ -220,6 +271,58 @@ def format_next_version(
220271
guessed = guess_next(self, *k, **kw)
221272
return self.format_with(fmt, guessed=guessed)
222273

274+
def matches(self, **expectations: Unpack[VersionExpectations]) -> bool | mismatches:
275+
"""Check if this ScmVersion matches the given expectations.
276+
277+
Returns True if all specified properties match, or a mismatches
278+
object (which is falsy) containing details of what didn't match.
279+
280+
Args:
281+
**expectations: Properties to check, using VersionExpectations TypedDict
282+
"""
283+
# Map expectation keys to ScmVersion attributes
284+
attr_map: dict[str, Callable[[], Any]] = {
285+
"tag": lambda: str(self.tag),
286+
"node_prefix": lambda: self.node,
287+
"distance": lambda: self.distance,
288+
"dirty": lambda: self.dirty,
289+
"branch": lambda: self.branch,
290+
"exact": lambda: self.exact,
291+
"preformatted": lambda: self.preformatted,
292+
"node_date": lambda: self.node_date,
293+
"time": lambda: self.time,
294+
}
295+
296+
# Build actual values dict
297+
actual: dict[str, Any] = {
298+
key: attr_map[key]() for key in expectations if key in attr_map
299+
}
300+
301+
# Process expectations
302+
expected = {
303+
"tag" if k == "tag" else k: str(v) if k == "tag" else v
304+
for k, v in expectations.items()
305+
}
306+
307+
# Check for mismatches
308+
def has_mismatch() -> bool:
309+
for key, exp_val in expected.items():
310+
if key == "node_prefix":
311+
act_val = actual.get("node_prefix")
312+
if not act_val or not act_val.startswith(exp_val):
313+
return True
314+
else:
315+
if str(exp_val) != str(actual.get(key)):
316+
return True
317+
return False
318+
319+
if has_mismatch():
320+
# Rename node_prefix back to node for actual values in mismatch reporting
321+
if "node_prefix" in actual:
322+
actual["node"] = actual.pop("node_prefix")
323+
return mismatches(expected=expected, actual=actual)
324+
return True
325+
223326

224327
def _parse_tag(
225328
tag: _VersionT | str, preformatted: bool, config: _config.Configuration

testing/conftest.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import shutil
66
import sys
77

8+
from datetime import datetime
9+
from datetime import timezone
810
from pathlib import Path
911
from types import TracebackType
1012
from typing import Any
@@ -21,10 +23,18 @@
2123

2224
from .wd_wrapper import WorkDir
2325

26+
# Test time constants: 2009-02-13T23:31:30+00:00
27+
TEST_SOURCE_DATE = datetime(2009, 2, 13, 23, 31, 30, tzinfo=timezone.utc)
28+
TEST_SOURCE_DATE_EPOCH = int(TEST_SOURCE_DATE.timestamp())
29+
TEST_SOURCE_DATE_FORMATTED = "20090213" # As used in node-and-date local scheme
30+
TEST_SOURCE_DATE_TIMESTAMP = (
31+
"20090213233130" # As used in node-and-timestamp local scheme
32+
)
2433

25-
def pytest_configure() -> None:
34+
35+
def pytest_configure(config: pytest.Config) -> None:
2636
# 2009-02-13T23:31:30+00:00
27-
os.environ["SOURCE_DATE_EPOCH"] = "1234567890"
37+
os.environ["SOURCE_DATE_EPOCH"] = str(TEST_SOURCE_DATE_EPOCH)
2838
os.environ["SETUPTOOLS_SCM_DEBUG"] = "1"
2939

3040

@@ -42,10 +52,10 @@ def pytest_report_header() -> list[str]:
4252
# Replace everything up to and including site-packages with site::
4353
parts = path.split("site-packages", 1)
4454
if len(parts) > 1:
45-
path = "site:." + parts[1]
55+
path = "site::" + parts[1]
4656
elif path and str(Path.cwd()) in path:
4757
# Replace current working directory with CWD::
48-
path = path.replace(str(Path.cwd()), "CWD:.")
58+
path = path.replace(str(Path.cwd()), "CWD::")
4959
res.append(f"{pkg} version {pkg_version} from {path}")
5060
return res
5161

@@ -90,6 +100,10 @@ def debug_mode() -> Iterator[DebugMode]:
90100

91101
@pytest.fixture
92102
def wd(tmp_path: Path) -> WorkDir:
103+
"""Base WorkDir fixture that returns an unconfigured working directory.
104+
105+
Individual test modules should override this fixture to set up specific SCM configurations.
106+
"""
93107
target_wd = tmp_path.resolve() / "wd"
94108
target_wd.mkdir()
95109
return WorkDir(target_wd)
@@ -109,12 +123,7 @@ def repositories_hg_git(tmp_path: Path) -> tuple[WorkDir, WorkDir]:
109123
path_git = tmp_path / "repo_git"
110124
path_git.mkdir()
111125

112-
wd = WorkDir(path_git)
113-
wd("git init")
114-
wd("git config user.email test@example.com")
115-
wd('git config user.name "a test"')
116-
wd.add_command = "git add ."
117-
wd.commit_command = "git commit -m test-{reason}"
126+
wd = WorkDir(path_git).setup_git()
118127

119128
path_hg = tmp_path / "repo_hg"
120129
run(["hg", "clone", path_git, path_hg, "--config", "extensions.hggit="], tmp_path)
@@ -124,7 +133,6 @@ def repositories_hg_git(tmp_path: Path) -> tuple[WorkDir, WorkDir]:
124133
file.write("[extensions]\nhggit =\n")
125134

126135
wd_hg = WorkDir(path_hg)
127-
wd_hg.add_command = "hg add ."
128-
wd_hg.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"'
136+
wd_hg.configure_hg_commands()
129137

130138
return wd_hg, wd

testing/test_better_root_errors.py

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,13 @@
1616
from setuptools_scm._get_version_impl import _version_missing
1717
from testing.wd_wrapper import WorkDir
1818

19-
20-
def setup_git_repo(wd: WorkDir) -> WorkDir:
21-
"""Set up a git repository for testing."""
22-
wd("git init")
23-
wd("git config user.email test@example.com")
24-
wd('git config user.name "a test"')
25-
wd.add_command = "git add ."
26-
wd.commit_command = "git commit -m test-{reason}"
27-
return wd
28-
29-
30-
def setup_hg_repo(wd: WorkDir) -> WorkDir:
31-
"""Set up a mercurial repository for testing."""
32-
try:
33-
wd("hg init")
34-
wd.add_command = "hg add ."
35-
wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"'
36-
return wd
37-
except Exception:
38-
pytest.skip("hg not available")
19+
# No longer need to import setup functions - using WorkDir methods directly
3920

4021

4122
def test_find_scm_in_parents_finds_git(wd: WorkDir) -> None:
4223
"""Test that _find_scm_in_parents correctly finds git repositories in parent directories."""
4324
# Set up git repo in root
44-
setup_git_repo(wd)
25+
wd.setup_git()
4526

4627
# Create a subdirectory structure
4728
subdir = wd.cwd / "subproject" / "nested"
@@ -57,7 +38,7 @@ def test_find_scm_in_parents_finds_git(wd: WorkDir) -> None:
5738
def test_find_scm_in_parents_finds_hg(wd: WorkDir) -> None:
5839
"""Test that _find_scm_in_parents correctly finds mercurial repositories in parent directories."""
5940
# Set up hg repo in root
60-
setup_hg_repo(wd)
41+
wd.setup_hg()
6142

6243
# Create a subdirectory structure
6344
subdir = wd.cwd / "subproject" / "nested"
@@ -85,7 +66,7 @@ def test_find_scm_in_parents_returns_none(wd: WorkDir) -> None:
8566
def test_version_missing_with_scm_in_parent(wd: WorkDir) -> None:
8667
"""Test that _version_missing provides helpful error message when SCM is found in parent."""
8768
# Set up git repo in root
88-
setup_git_repo(wd)
69+
wd.setup_git()
8970

9071
# Create a subdirectory structure
9172
subdir = wd.cwd / "subproject" / "nested"
@@ -130,7 +111,7 @@ def test_version_missing_no_scm_found(wd: WorkDir) -> None:
130111
def test_version_missing_with_relative_to_set(wd: WorkDir) -> None:
131112
"""Test that when relative_to is set, we don't search parents for error messages."""
132113
# Set up git repo in root
133-
setup_git_repo(wd)
114+
wd.setup_git()
134115

135116
# Create a subdirectory structure
136117
subdir = wd.cwd / "subproject" / "nested"
@@ -161,7 +142,7 @@ def test_search_parent_directories_works_as_suggested(
161142
) -> None:
162143
"""Test that the suggested search_parent_directories=True solution actually works."""
163144
# Set up git repo
164-
setup_git_repo(wd)
145+
wd.setup_git()
165146
wd.commit_testfile() # Make sure there's a commit for version detection
166147

167148
# Create a subdirectory
@@ -182,7 +163,7 @@ def test_integration_better_error_from_nested_directory(
182163
) -> None:
183164
"""Integration test: get_version from nested directory should give helpful error."""
184165
# Set up git repo
185-
setup_git_repo(wd)
166+
wd.setup_git()
186167

187168
# Create a subdirectory
188169
subdir = wd.cwd / "subproject"

0 commit comments

Comments
 (0)