Skip to content

Commit 740fb4a

Browse files
committed
!squash tests/config(test[loader]): Add property-based
1 parent 1553fe4 commit 740fb4a

File tree

1 file changed

+143
-163
lines changed

1 file changed

+143
-163
lines changed

tests/unit/config/test_loader_property.py

Lines changed: 143 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,20 @@
88
from __future__ import annotations
99

1010
import json
11-
import tempfile
12-
from pathlib import Path
13-
from typing import Any, Callable
11+
import pathlib
12+
import typing as t
1413

1514
import hypothesis.strategies as st
1615
import yaml
17-
from hypothesis import given, settings
16+
from hypothesis import HealthCheck, given, settings
1817

1918
from vcspull.config.loader import load_config, resolve_includes, save_config
2019
from vcspull.config.models import Repository, Settings, VCSPullConfig
2120

2221

2322
# Reuse strategies from test_models_property.py
2423
@st.composite
25-
def valid_url_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> str:
24+
def valid_url_strategy(draw: t.Callable[[st.SearchStrategy[t.Any]], t.Any]) -> str:
2625
"""Generate valid URLs for repositories."""
2726
protocols = ["https://", "http://", "git://", "ssh://git@"]
2827
domains = ["github.com", "gitlab.com", "bitbucket.org", "example.com"]
@@ -53,7 +52,7 @@ def valid_url_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> str:
5352

5453

5554
@st.composite
56-
def valid_path_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> str:
55+
def valid_path_strategy(draw: t.Callable[[st.SearchStrategy[t.Any]], t.Any]) -> str:
5756
"""Generate valid paths for repositories."""
5857
base_dirs = ["~/code", "~/projects", "/tmp", "./projects"]
5958
sub_dirs = [
@@ -78,7 +77,9 @@ def valid_path_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> str:
7877

7978

8079
@st.composite
81-
def repository_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> Repository:
80+
def repository_strategy(
81+
draw: t.Callable[[st.SearchStrategy[t.Any]], t.Any],
82+
) -> Repository:
8283
"""Generate valid Repository instances."""
8384
name = draw(st.one_of(st.none(), st.text(min_size=1, max_size=20)))
8485
url = draw(valid_url_strategy())
@@ -130,7 +131,7 @@ def repository_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> Reposi
130131

131132

132133
@st.composite
133-
def settings_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> Settings:
134+
def settings_strategy(draw: t.Callable[[st.SearchStrategy[t.Any]], t.Any]) -> Settings:
134135
"""Generate valid Settings instances."""
135136
sync_remotes = draw(st.booleans())
136137
default_vcs = draw(st.one_of(st.none(), st.sampled_from(["git", "hg", "svn"])))
@@ -145,14 +146,14 @@ def settings_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> Settings
145146

146147
@st.composite
147148
def vcspull_config_strategy(
148-
draw: Callable[[st.SearchStrategy[Any]], Any],
149+
draw: t.Callable[[st.SearchStrategy[t.Any]], t.Any],
149150
with_includes: bool = False,
150151
) -> VCSPullConfig:
151152
"""Generate valid VCSPullConfig instances.
152153
153154
Parameters
154155
----------
155-
draw : Callable
156+
draw : t.Callable
156157
Hypothesis draw function
157158
with_includes : bool, optional
158159
Whether to add include files to the config, by default False
@@ -181,192 +182,171 @@ def vcspull_config_strategy(
181182
)
182183

183184

184-
# Helper function to save a config to a temporary file
185-
def save_temp_config(config: VCSPullConfig, suffix: str = ".yaml") -> Path:
186-
"""Save a config to a temporary file.
187-
188-
Parameters
189-
----------
190-
config : VCSPullConfig
191-
Configuration to save
192-
suffix : str, optional
193-
File suffix, by default ".yaml"
194-
195-
Returns
196-
-------
197-
Path
198-
Path to the saved file
199-
"""
200-
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
201-
temp_path = Path(f.name)
202-
203-
# Save the config to the temporary file
204-
format_type = "yaml" if suffix in (".yaml", ".yml") else "json"
205-
save_config(config, temp_path, format_type=format_type)
206-
207-
return temp_path
208-
209-
210185
class TestConfigLoaderProperties:
211186
"""Property-based tests for configuration loading."""
212187

213188
@given(config=vcspull_config_strategy())
214189
@settings(
215-
max_examples=10
216-
) # Limit the number of examples to avoid too many temp files
217-
def test_load_save_roundtrip(self, config: VCSPullConfig) -> None:
190+
max_examples=10, # Limit examples to avoid too many temp files
191+
suppress_health_check=[HealthCheck.function_scoped_fixture],
192+
)
193+
def test_load_save_roundtrip(
194+
self, config: VCSPullConfig, tmp_path: pathlib.Path
195+
) -> None:
218196
"""Test that saving and loading a configuration preserves its content."""
219197
# Save the config to a temporary YAML file
220-
yaml_path = save_temp_config(config, suffix=".yaml")
221-
try:
222-
# Load the config back
223-
loaded_config = load_config(yaml_path)
224-
225-
# Check that loaded config matches original
226-
assert loaded_config.settings.model_dump() == config.settings.model_dump()
227-
assert len(loaded_config.repositories) == len(config.repositories)
228-
for i, repo in enumerate(config.repositories):
229-
assert loaded_config.repositories[i].url == repo.url
230-
assert loaded_config.repositories[i].path == repo.path
231-
232-
# Also test with JSON format
233-
json_path = save_temp_config(config, suffix=".json")
234-
try:
235-
json_loaded_config = load_config(json_path)
236-
237-
# Check that JSON loaded config matches original
238-
assert (
239-
json_loaded_config.settings.model_dump()
240-
== config.settings.model_dump()
241-
)
242-
assert len(json_loaded_config.repositories) == len(config.repositories)
243-
finally:
244-
# Cleanup JSON temp file
245-
json_path.unlink(missing_ok=True)
198+
yaml_path = tmp_path / "config.yaml"
199+
save_config(config, yaml_path, format_type="yaml")
246200

247-
finally:
248-
# Cleanup YAML temp file
249-
yaml_path.unlink(missing_ok=True)
201+
# Load the config back
202+
loaded_config = load_config(yaml_path)
203+
204+
# Check that loaded config matches original
205+
assert loaded_config.settings.model_dump() == config.settings.model_dump()
206+
assert len(loaded_config.repositories) == len(config.repositories)
207+
for i, repo in enumerate(config.repositories):
208+
assert loaded_config.repositories[i].url == repo.url
209+
assert loaded_config.repositories[i].path == repo.path
210+
211+
# Also test with JSON format
212+
json_path = tmp_path / "config.json"
213+
save_config(config, json_path, format_type="json")
214+
215+
# Load JSON config
216+
json_loaded_config = load_config(json_path)
217+
218+
# Check that JSON loaded config matches original
219+
assert json_loaded_config.settings.model_dump() == config.settings.model_dump()
220+
assert len(json_loaded_config.repositories) == len(config.repositories)
250221

251222
@given(
252223
main_config=vcspull_config_strategy(with_includes=True),
253224
included_configs=st.lists(vcspull_config_strategy(), min_size=1, max_size=3),
254225
)
255-
@settings(max_examples=10) # Limit the number of examples
226+
@settings(
227+
max_examples=10, # Limit the number of examples
228+
suppress_health_check=[HealthCheck.function_scoped_fixture],
229+
)
256230
def test_include_resolution(
257-
self, main_config: VCSPullConfig, included_configs: list[VCSPullConfig]
231+
self,
232+
main_config: VCSPullConfig,
233+
included_configs: list[VCSPullConfig],
234+
tmp_path: pathlib.Path,
258235
) -> None:
259236
"""Test that include resolution properly merges configurations."""
260-
with tempfile.TemporaryDirectory() as temp_dir:
261-
temp_dir_path = Path(temp_dir)
262-
263-
# Create and save included configs
264-
included_paths = []
265-
for i, include_config in enumerate(included_configs):
266-
include_path = temp_dir_path / f"include{i}.yaml"
267-
save_config(include_config, include_path)
268-
included_paths.append(include_path)
237+
# Create and save included configs
238+
included_paths = []
239+
for i, include_config in enumerate(included_configs):
240+
include_path = tmp_path / f"include{i}.yaml"
241+
save_config(include_config, include_path)
242+
included_paths.append(include_path)
269243

270-
# Update main config's includes to point to the actual files
271-
main_config.includes = [str(path) for path in included_paths]
244+
# Update main config's includes to point to the actual files
245+
main_config.includes = [str(path) for path in included_paths]
272246

273-
# Save main config
274-
main_path = temp_dir_path / "main.yaml"
275-
save_config(main_config, main_path)
247+
# Save main config
248+
main_path = tmp_path / "main.yaml"
249+
save_config(main_config, main_path)
276250

277-
# Load and resolve includes
278-
loaded_config = load_config(main_path)
279-
resolved_config = resolve_includes(loaded_config, main_path.parent)
251+
# Load and resolve includes
252+
loaded_config = load_config(main_path)
253+
resolved_config = resolve_includes(loaded_config, main_path.parent)
280254

281-
# Verify all repositories are present in the resolved config
282-
all_repos = list(main_config.repositories)
283-
for include_config in included_configs:
284-
all_repos.extend(include_config.repositories)
255+
# Verify all repositories are present in the resolved config
256+
all_repos = list(main_config.repositories)
257+
for include_config in included_configs:
258+
all_repos.extend(include_config.repositories)
285259

286-
# Check that all repositories are present in the resolved config
287-
assert len(resolved_config.repositories) == len(all_repos)
260+
# Check that all repositories are present in the resolved config
261+
assert len(resolved_config.repositories) == len(all_repos)
288262

289-
# Check that includes are cleared
290-
assert len(resolved_config.includes) == 0
263+
# Check that includes are cleared
264+
assert len(resolved_config.includes) == 0
291265

292-
# Verify URLs of repositories match (as a basic check)
293-
resolved_urls = {repo.url for repo in resolved_config.repositories}
294-
original_urls = {repo.url for repo in all_repos}
295-
assert resolved_urls == original_urls
266+
# Verify URLs of repositories match (as a basic check)
267+
resolved_urls = {repo.url for repo in resolved_config.repositories}
268+
original_urls = {repo.url for repo in all_repos}
269+
assert resolved_urls == original_urls
296270

297271
@given(configs=st.lists(vcspull_config_strategy(), min_size=2, max_size=4))
298-
@settings(max_examples=10)
299-
def test_nested_includes_resolution(self, configs: list[VCSPullConfig]) -> None:
272+
@settings(
273+
max_examples=10,
274+
suppress_health_check=[HealthCheck.function_scoped_fixture],
275+
)
276+
def test_nested_includes_resolution(
277+
self,
278+
configs: list[VCSPullConfig],
279+
tmp_path: pathlib.Path,
280+
) -> None:
300281
"""Test that nested includes are resolved properly."""
301-
with tempfile.TemporaryDirectory() as temp_dir:
302-
temp_dir_path = Path(temp_dir)
303-
304-
# Save configs with nested includes
305-
# Last config has no includes
306-
paths = []
307-
for i, config in enumerate(configs):
308-
config_path = temp_dir_path / f"config{i}.yaml"
282+
# Save configs with nested includes
283+
# Last config has no includes
284+
paths = []
285+
for i, config in enumerate(configs):
286+
config_path = tmp_path / f"config{i}.yaml"
309287

310-
# Add includes to each config (except the last one)
311-
if i < len(configs) - 1:
312-
config.includes = [f"config{i + 1}.yaml"]
313-
else:
314-
config.includes = []
288+
# Add includes to each config (except the last one)
289+
if i < len(configs) - 1:
290+
config.includes = [f"config{i + 1}.yaml"]
291+
else:
292+
config.includes = []
315293

316-
save_config(config, config_path)
317-
paths.append(config_path)
294+
save_config(config, config_path)
295+
paths.append(config_path)
318296

319-
# Load and resolve includes for the first config
320-
first_config = load_config(paths[0])
321-
resolved_config = resolve_includes(first_config, temp_dir_path)
297+
# Load and resolve includes for the first config
298+
first_config = load_config(paths[0])
299+
resolved_config = resolve_includes(first_config, tmp_path)
322300

323-
# Gather all repositories from original configs
324-
all_repos = []
325-
for config in configs:
326-
all_repos.extend(config.repositories)
301+
# Gather all repositories from original configs
302+
all_repos = []
303+
for config in configs:
304+
all_repos.extend(config.repositories)
327305

328-
# Check repository count
329-
assert len(resolved_config.repositories) == len(all_repos)
306+
# Check repository count
307+
assert len(resolved_config.repositories) == len(all_repos)
330308

331-
# Check all repositories are included
332-
resolved_urls = {repo.url for repo in resolved_config.repositories}
333-
original_urls = {repo.url for repo in all_repos}
334-
assert resolved_urls == original_urls
309+
# Check all repositories are included
310+
resolved_urls = {repo.url for repo in resolved_config.repositories}
311+
original_urls = {repo.url for repo in all_repos}
312+
assert resolved_urls == original_urls
335313

336-
# Check no includes remain
337-
assert len(resolved_config.includes) == 0
314+
# Check no includes remain
315+
assert len(resolved_config.includes) == 0
338316

339317
@given(config=vcspull_config_strategy())
340-
@settings(max_examples=10)
341-
def test_save_config_formats(self, config: VCSPullConfig) -> None:
318+
@settings(
319+
max_examples=10,
320+
suppress_health_check=[HealthCheck.function_scoped_fixture],
321+
)
322+
def test_save_config_formats(
323+
self, config: VCSPullConfig, tmp_path: pathlib.Path
324+
) -> None:
342325
"""Test that configs can be saved in different formats."""
343-
with tempfile.TemporaryDirectory() as temp_dir:
344-
temp_dir_path = Path(temp_dir)
345-
346-
# Save in YAML format
347-
yaml_path = temp_dir_path / "config.yaml"
348-
saved_yaml_path = save_config(config, yaml_path, format_type="yaml")
349-
assert saved_yaml_path.exists()
350-
351-
# Verify YAML file is valid
352-
with saved_yaml_path.open() as f:
353-
yaml_content = yaml.safe_load(f)
354-
assert isinstance(yaml_content, dict)
355-
356-
# Save in JSON format
357-
json_path = temp_dir_path / "config.json"
358-
saved_json_path = save_config(config, json_path, format_type="json")
359-
assert saved_json_path.exists()
360-
361-
# Verify JSON file is valid
362-
with saved_json_path.open() as f:
363-
json_content = json.load(f)
364-
assert isinstance(json_content, dict)
365-
366-
# Load both formats and compare
367-
yaml_config = load_config(saved_yaml_path)
368-
json_config = load_config(saved_json_path)
369-
370-
# Check that both loaded configs match the original
371-
assert yaml_config.model_dump() == config.model_dump()
372-
assert json_config.model_dump() == config.model_dump()
326+
# Save in YAML format
327+
yaml_path = tmp_path / "config.yaml"
328+
saved_yaml_path = save_config(config, yaml_path, format_type="yaml")
329+
assert saved_yaml_path.exists()
330+
331+
# Verify YAML file is valid
332+
with saved_yaml_path.open() as f:
333+
yaml_content = yaml.safe_load(f)
334+
assert isinstance(yaml_content, dict)
335+
336+
# Save in JSON format
337+
json_path = tmp_path / "config.json"
338+
saved_json_path = save_config(config, json_path, format_type="json")
339+
assert saved_json_path.exists()
340+
341+
# Verify JSON file is valid
342+
with saved_json_path.open() as f:
343+
json_content = json.load(f)
344+
assert isinstance(json_content, dict)
345+
346+
# Load both formats and compare
347+
yaml_config = load_config(saved_yaml_path)
348+
json_config = load_config(saved_json_path)
349+
350+
# Check that both loaded configs match the original
351+
assert yaml_config.model_dump() == config.model_dump()
352+
assert json_config.model_dump() == config.model_dump()

0 commit comments

Comments
 (0)