Skip to content

Commit 70eac6a

Browse files
authored
test: cover config loading behavior (#72)
## Summary - add dedicated unit coverage for `config.py` per #65 - cover valid TOML loading, missing/invalid config fallback, and default copy isolation - cover packaged default rule loading, AI rule inclusion, and shared placeholder substitution ## Validation - `/tmp/pyspector-pdlc048-venv/bin/python -m pytest tests/unit/test_config.py -q` - `/tmp/pyspector-pdlc048-venv/bin/python -m py_compile src/pyspector/config.py tests/unit/test_config.py` - `/tmp/pyspector-pdlc048-venv/bin/python -m ruff check src/pyspector/config.py tests/unit/test_config.py` - `git diff --check` Fixes #65
1 parent 3806e2c commit 70eac6a

2 files changed

Lines changed: 102 additions & 5 deletions

File tree

src/pyspector/config.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import re
2+
from copy import deepcopy
23
from pathlib import Path
3-
import toml # type: ignore
4-
import click # type: ignore
4+
5+
import click # type: ignore
6+
import toml # type: ignore
7+
58
try:
69
# Python 3.9+
710
import importlib.resources as pkg_resources
811
except ImportError:
912
# Fallback for older Python versions
10-
import importlib_resources as pkg_resources # type: ignore
13+
import importlib_resources as pkg_resources # type: ignore
1114

1215
# Sentinel placed inside any rule's `exclude_pattern` to inherit the shared
1316
# placeholder regex declared at [defaults].exclude_pattern_placeholder. The
@@ -42,12 +45,12 @@ def load_config(config_path: Path) -> dict:
4245
try:
4346
with config_path.open('r') as f:
4447
user_config = toml.load(f).get('tool', {}).get('pyspector', {})
45-
config = DEFAULT_CONFIG.copy()
48+
config = deepcopy(DEFAULT_CONFIG)
4649
config.update(user_config)
4750
return config
4851
except Exception as e:
4952
click.echo(click.style(f"Warning: Could not parse config file '{config_path}'. Using defaults. Error: {e}", fg="yellow"))
50-
return DEFAULT_CONFIG
53+
return deepcopy(DEFAULT_CONFIG)
5154

5255
def get_default_rules(ai_scan: bool = False) -> str:
5356
"""Loads the built-in TOML rules file from package resources.

tests/unit/test_config.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import sys
2+
from pathlib import Path
3+
4+
import toml
5+
6+
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
7+
8+
from pyspector.config import DEFAULT_CONFIG, get_default_rules, load_config
9+
10+
11+
def test_load_config_merges_valid_pyspector_section(tmp_path):
12+
config_path = tmp_path / "pyspector.toml"
13+
config_path.write_text(
14+
"""
15+
[tool.pyspector]
16+
severity = "HIGH"
17+
exclude = ["custom"]
18+
extra_setting = "kept"
19+
""".strip(),
20+
encoding="utf-8",
21+
)
22+
23+
config = load_config(config_path)
24+
25+
assert config["severity"] == "HIGH"
26+
assert config["exclude"] == ["custom"]
27+
assert config["extra_setting"] == "kept"
28+
29+
30+
def test_load_config_uses_defaults_when_file_is_missing(tmp_path):
31+
config = load_config(tmp_path / "missing.toml")
32+
33+
assert config == DEFAULT_CONFIG
34+
assert config["severity"] == "LOW"
35+
assert "node_modules" in config["exclude"]
36+
assert "**/test_*.py" in config["exclude"]
37+
38+
39+
def test_load_config_uses_defaults_for_invalid_toml(tmp_path, capsys):
40+
config_path = tmp_path / "pyspector.toml"
41+
config_path.write_text("[tool.pyspector\nseverity = 'HIGH'\n", encoding="utf-8")
42+
43+
config = load_config(config_path)
44+
45+
assert config == DEFAULT_CONFIG
46+
assert "Could not parse config file" in capsys.readouterr().out
47+
48+
49+
def test_load_config_defaults_are_copied_before_user_updates(tmp_path):
50+
config_path = tmp_path / "pyspector.toml"
51+
config_path.write_text(
52+
"""
53+
[tool.pyspector]
54+
severity = "MEDIUM"
55+
""".strip(),
56+
encoding="utf-8",
57+
)
58+
59+
config = load_config(config_path)
60+
config["exclude"].append("local-only")
61+
62+
assert config["severity"] == "MEDIUM"
63+
assert "local-only" not in DEFAULT_CONFIG["exclude"]
64+
65+
66+
def test_get_default_rules_loads_parseable_builtin_rules():
67+
rules = toml.loads(get_default_rules())
68+
69+
rule_ids = {rule["id"] for rule in rules["rule"]}
70+
assert "PY001" in rule_ids
71+
assert "AI202" not in rule_ids
72+
73+
74+
def test_get_default_rules_includes_ai_rules_when_enabled(capsys):
75+
rules = toml.loads(get_default_rules(ai_scan=True))
76+
77+
rule_ids = {rule["id"] for rule in rules["rule"]}
78+
assert "PY001" in rule_ids
79+
assert "AI202" in rule_ids
80+
assert "AI scanning enabled" in capsys.readouterr().out
81+
82+
83+
def test_get_default_rules_substitutes_shared_placeholder_regex():
84+
rules_text = get_default_rules(ai_scan=True)
85+
rules = toml.loads(rules_text)
86+
87+
placeholder = rules["defaults"]["exclude_pattern_placeholder"]
88+
assert "__SHARED_PLACEHOLDERS__" not in rules_text
89+
assert placeholder
90+
91+
placeholder_backed_rules = [
92+
rule for rule in rules["rule"] if rule.get("exclude_pattern") == placeholder
93+
]
94+
assert placeholder_backed_rules

0 commit comments

Comments
 (0)