|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +import pathlib |
5 | 6 | import re |
6 | 7 | import subprocess |
7 | 8 | import typing as t |
8 | 9 |
|
9 | 10 | import pytest |
10 | 11 |
|
11 | 12 | from vcspull._internal.private_path import PrivatePath |
12 | | -from vcspull.cli.discover import discover_repos |
| 13 | +from vcspull.cli.discover import ConfigScope, _classify_config_scope, discover_repos |
13 | 14 |
|
14 | 15 | if t.TYPE_CHECKING: |
15 | 16 | import pathlib |
@@ -202,6 +203,55 @@ class DiscoverFixture(t.NamedTuple): |
202 | 203 | ] |
203 | 204 |
|
204 | 205 |
|
| 206 | +class ConfigScopeFixture(t.NamedTuple): |
| 207 | + """Fixture describing config scope classification scenarios.""" |
| 208 | + |
| 209 | + test_id: str |
| 210 | + config_template: str |
| 211 | + cwd_template: str |
| 212 | + env: dict[str, str] |
| 213 | + expected_scope: ConfigScope |
| 214 | + |
| 215 | + |
| 216 | +CONFIG_SCOPE_FIXTURES: list[ConfigScopeFixture] = [ |
| 217 | + ConfigScopeFixture( |
| 218 | + test_id="scope-user-home-default", |
| 219 | + config_template="{home}/.vcspull.yaml", |
| 220 | + cwd_template="{home}", |
| 221 | + env={}, |
| 222 | + expected_scope="user", |
| 223 | + ), |
| 224 | + ConfigScopeFixture( |
| 225 | + test_id="scope-user-xdg-home", |
| 226 | + config_template="{xdg_home}/vcspull/personal.yaml", |
| 227 | + cwd_template="{home}", |
| 228 | + env={"XDG_CONFIG_HOME": "{xdg_home}"}, |
| 229 | + expected_scope="user", |
| 230 | + ), |
| 231 | + ConfigScopeFixture( |
| 232 | + test_id="scope-system-xdg-dirs", |
| 233 | + config_template="{xdg_system}/vcspull/system.yaml", |
| 234 | + cwd_template="{home}", |
| 235 | + env={"XDG_CONFIG_DIRS": "{xdg_system}"}, |
| 236 | + expected_scope="system", |
| 237 | + ), |
| 238 | + ConfigScopeFixture( |
| 239 | + test_id="scope-project-relative", |
| 240 | + config_template="{project}/.vcspull.yaml", |
| 241 | + cwd_template="{project}", |
| 242 | + env={}, |
| 243 | + expected_scope="project", |
| 244 | + ), |
| 245 | + ConfigScopeFixture( |
| 246 | + test_id="scope-external-file", |
| 247 | + config_template="{external}/configs/custom.yaml", |
| 248 | + cwd_template="{project}", |
| 249 | + env={}, |
| 250 | + expected_scope="external", |
| 251 | + ), |
| 252 | +] |
| 253 | + |
| 254 | + |
205 | 255 | class DiscoverLoadEdgeFixture(t.NamedTuple): |
206 | 256 | """Fixture describing discover configuration loading edge cases.""" |
207 | 257 |
|
@@ -401,6 +451,67 @@ def test_discover_repos( |
401 | 451 | ) |
402 | 452 |
|
403 | 453 |
|
| 454 | +@pytest.mark.parametrize( |
| 455 | + list(ConfigScopeFixture._fields), |
| 456 | + CONFIG_SCOPE_FIXTURES, |
| 457 | + ids=[fixture.test_id for fixture in CONFIG_SCOPE_FIXTURES], |
| 458 | +) |
| 459 | +def test_classify_config_scope( |
| 460 | + test_id: str, |
| 461 | + config_template: str, |
| 462 | + cwd_template: str, |
| 463 | + env: dict[str, str], |
| 464 | + expected_scope: ConfigScope, |
| 465 | + tmp_path: pathlib.Path, |
| 466 | + monkeypatch: MonkeyPatch, |
| 467 | +) -> None: |
| 468 | + """Ensure _classify_config_scope handles user/system/project/external paths.""" |
| 469 | + base_home = tmp_path / "home" |
| 470 | + base_home.mkdir() |
| 471 | + project = tmp_path / "project" |
| 472 | + project.mkdir() |
| 473 | + xdg_home = tmp_path / "xdg-home" |
| 474 | + (xdg_home / "vcspull").mkdir(parents=True) |
| 475 | + xdg_system = tmp_path / "xdg-system" |
| 476 | + (xdg_system / "vcspull").mkdir(parents=True) |
| 477 | + external = tmp_path / "external" |
| 478 | + (external / "configs").mkdir(parents=True) |
| 479 | + |
| 480 | + replacements = { |
| 481 | + "home": base_home, |
| 482 | + "project": project, |
| 483 | + "xdg_home": xdg_home, |
| 484 | + "xdg_system": xdg_system, |
| 485 | + "external": external, |
| 486 | + } |
| 487 | + |
| 488 | + def _expand(template: str) -> pathlib.Path: |
| 489 | + expanded = template |
| 490 | + for key, path in replacements.items(): |
| 491 | + expanded = expanded.replace(f"{{{key}}}", str(path)) |
| 492 | + return pathlib.Path(expanded) |
| 493 | + |
| 494 | + config_path = _expand(config_template) |
| 495 | + config_path.parent.mkdir(parents=True, exist_ok=True) |
| 496 | + config_path.touch() |
| 497 | + |
| 498 | + cwd_path = _expand(cwd_template) |
| 499 | + cwd_path.mkdir(parents=True, exist_ok=True) |
| 500 | + |
| 501 | + monkeypatch.chdir(cwd_path) |
| 502 | + monkeypatch.setenv("HOME", str(base_home)) |
| 503 | + for var in ["XDG_CONFIG_HOME", "XDG_CONFIG_DIRS"]: |
| 504 | + monkeypatch.delenv(var, raising=False) |
| 505 | + for key, value in env.items(): |
| 506 | + expanded_value = value |
| 507 | + for name, path in replacements.items(): |
| 508 | + expanded_value = expanded_value.replace(f"{{{name}}}", str(path)) |
| 509 | + monkeypatch.setenv(key, expanded_value) |
| 510 | + |
| 511 | + scope = _classify_config_scope(config_path, cwd=cwd_path, home=base_home) |
| 512 | + assert scope == expected_scope |
| 513 | + |
| 514 | + |
404 | 515 | @pytest.mark.parametrize( |
405 | 516 | list(DiscoverLoadEdgeFixture._fields), |
406 | 517 | DISCOVER_LOAD_EDGE_FIXTURES, |
@@ -703,7 +814,9 @@ def test_discover_user_config_prefers_absolute_workspace_label( |
703 | 814 | monkeypatch: MonkeyPatch, |
704 | 815 | caplog: t.Any, |
705 | 816 | ) -> None: |
| 817 | + """User-level configs default to tilde-prefixed workspace labels.""" |
706 | 818 | import logging |
| 819 | + |
707 | 820 | import yaml |
708 | 821 |
|
709 | 822 | caplog.set_level(logging.INFO) |
@@ -759,6 +872,7 @@ def test_discover_project_config_retains_relative_workspace_label( |
759 | 872 | tmp_path: pathlib.Path, |
760 | 873 | monkeypatch: MonkeyPatch, |
761 | 874 | ) -> None: |
| 875 | + """Project-level configs keep relative './' workspace sections.""" |
762 | 876 | import yaml |
763 | 877 |
|
764 | 878 | home = tmp_path / "home" |
|
0 commit comments