Skip to content

Commit ca788cd

Browse files
committed
!squash tests(feat[property]): Add property-based testin
1 parent a73a780 commit ca788cd

File tree

1 file changed

+85
-82
lines changed

1 file changed

+85
-82
lines changed

tests/unit/config/test_models_property.py

Lines changed: 85 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
"""Property-based tests for configuration models.
22
3-
This module contains property-based tests using Hypothesis for the
4-
VCSPull configuration models to ensure they meet invariants and
5-
handle edge cases properly.
3+
This module contains property-based tests using Hypothesis
4+
for the VCSPull configuration models to ensure they handle
5+
various inputs correctly and maintain their invariants.
66
"""
77

88
from __future__ import annotations
99

10-
import re
11-
from pathlib import Path
12-
from typing import Any, Callable
10+
import os
11+
import pathlib
12+
import typing as t
1313

1414
import hypothesis.strategies as st
15-
from hypothesis import given
15+
import pytest
16+
from hypothesis import given, settings
1617

1718
from vcspull.config.models import Repository, Settings, VCSPullConfig
1819

1920

20-
# Define strategies for generating test data
2121
@st.composite
22-
def valid_url_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> str:
22+
def valid_url_strategy(draw: t.Callable[[st.SearchStrategy[t.Any]], t.Any]) -> str:
2323
"""Generate valid URLs for repositories."""
2424
protocols = ["https://", "http://", "git://", "ssh://git@"]
2525
domains = ["github.com", "gitlab.com", "bitbucket.org", "example.com"]
@@ -50,7 +50,7 @@ def valid_url_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> str:
5050

5151

5252
@st.composite
53-
def valid_path_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> str:
53+
def valid_path_strategy(draw: t.Callable[[st.SearchStrategy[t.Any]], t.Any]) -> str:
5454
"""Generate valid paths for repositories."""
5555
base_dirs = ["~/code", "~/projects", "/tmp", "./projects"]
5656
sub_dirs = [
@@ -75,7 +75,7 @@ def valid_path_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> str:
7575

7676

7777
@st.composite
78-
def repository_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> Repository:
78+
def repository_strategy(draw: t.Callable[[st.SearchStrategy[t.Any]], t.Any]) -> Repository:
7979
"""Generate valid Repository instances."""
8080
name = draw(st.one_of(st.none(), st.text(min_size=1, max_size=20)))
8181
url = draw(valid_url_strategy())
@@ -127,7 +127,7 @@ def repository_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> Reposi
127127

128128

129129
@st.composite
130-
def settings_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> Settings:
130+
def settings_strategy(draw: t.Callable[[st.SearchStrategy[t.Any]], t.Any]) -> Settings:
131131
"""Generate valid Settings instances."""
132132
sync_remotes = draw(st.booleans())
133133
default_vcs = draw(st.one_of(st.none(), st.sampled_from(["git", "hg", "svn"])))
@@ -142,7 +142,7 @@ def settings_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> Settings
142142

143143
@st.composite
144144
def vcspull_config_strategy(
145-
draw: Callable[[st.SearchStrategy[Any]], Any],
145+
draw: t.Callable[[st.SearchStrategy[t.Any]], t.Any]
146146
) -> VCSPullConfig:
147147
"""Generate valid VCSPullConfig instances."""
148148
settings = draw(settings_strategy())
@@ -151,9 +151,9 @@ def vcspull_config_strategy(
151151
repo_count = draw(st.integers(min_value=0, max_value=5))
152152
repositories = [draw(repository_strategy()) for _ in range(repo_count)]
153153

154-
# Generate includes
154+
# Optionally generate includes (0 to 3)
155155
include_count = draw(st.integers(min_value=0, max_value=3))
156-
includes = [f"~/.config/vcspull/include{i}.yaml" for i in range(include_count)]
156+
includes = [f"include{i}.yaml" for i in range(include_count)]
157157

158158
return VCSPullConfig(
159159
settings=settings,
@@ -162,82 +162,85 @@ def vcspull_config_strategy(
162162
)
163163

164164

165-
class TestRepositoryProperties:
166-
"""Property-based tests for the Repository model."""
165+
class TestRepositoryModel:
166+
"""Property-based tests for Repository model."""
167167

168-
@given(url=valid_url_strategy(), path=valid_path_strategy())
169-
def test_minimal_repository_properties(self, url: str, path: str) -> None:
170-
"""Test properties of minimal repositories."""
171-
repo = Repository(url=url, path=path)
168+
@given(repository=repository_strategy())
169+
def test_repository_construction(self, repository: Repository) -> None:
170+
"""Test Repository model construction with varied inputs."""
171+
# Verify required fields are set
172+
assert repository.url is not None
173+
assert repository.path is not None
172174

173-
# Check invariants
174-
assert repo.url == url
175-
assert Path(repo.path).is_absolute()
176-
assert repo.path.startswith("/") # Path should be absolute after normalization
175+
# Check computed fields
176+
if repository.name is None:
177+
# Name should be derived from URL if not explicitly set
178+
assert repository.get_name() != ""
177179

178180
@given(url=valid_url_strategy())
179-
def test_valid_url_formats(self, url: str) -> None:
180-
"""Test that valid URL formats are accepted."""
181-
repo = Repository(url=url, path="~/repo")
182-
assert repo.url == url
183-
184-
# Check URL format matches expected pattern
185-
url_pattern = r"^(https?|git|ssh)://.+"
186-
assert re.match(url_pattern, repo.url) is not None
187-
188-
@given(repo=repository_strategy())
189-
def test_repository_roundtrip(self, repo: Repository) -> None:
190-
"""Test repository serialization and deserialization."""
191-
# Roundtrip test: convert to dict and back to model
192-
repo_dict = repo.model_dump()
193-
repo2 = Repository.model_validate(repo_dict)
194-
195-
# The resulting object should match the original
196-
assert repo2.url == repo.url
197-
assert repo2.path == repo.path
198-
assert repo2.name == repo.name
199-
assert repo2.vcs == repo.vcs
200-
assert repo2.remotes == repo.remotes
201-
assert repo2.rev == repo.rev
202-
assert repo2.web_url == repo.web_url
203-
204-
205-
class TestSettingsProperties:
206-
"""Property-based tests for the Settings model."""
181+
def test_repository_name_extraction(self, url: str) -> None:
182+
"""Test Repository can extract names from URLs."""
183+
repo = Repository(url=url, path="/tmp/repo")
184+
# Should be able to extract a name from any valid URL
185+
assert repo.get_name() != ""
186+
# The name shouldn't contain protocol or domain parts
187+
assert "://" not in repo.get_name()
188+
assert "github.com" not in repo.get_name()
207189

208-
@given(settings=settings_strategy())
209-
def test_settings_roundtrip(self, settings: Settings) -> None:
210-
"""Test settings serialization and deserialization."""
211-
# Roundtrip test: convert to dict and back to model
212-
settings_dict = settings.model_dump()
213-
settings2 = Settings.model_validate(settings_dict)
190+
@given(repository=repository_strategy())
191+
def test_repository_path_expansion(self, repository: Repository) -> None:
192+
"""Test path expansion in Repository model."""
193+
# Get the expanded path
194+
expanded_path = repository.get_path()
214195

215-
# The resulting object should match the original
216-
assert settings2.sync_remotes == settings.sync_remotes
217-
assert settings2.default_vcs == settings.default_vcs
218-
assert settings2.depth == settings.depth
196+
# Check for tilde expansion
197+
assert "~" not in str(expanded_path)
219198

199+
# If original path started with ~, expanded should be absolute
200+
if repository.path.startswith("~"):
201+
assert os.path.isabs(expanded_path)
220202

221-
class TestVCSPullConfigProperties:
222-
"""Property-based tests for the VCSPullConfig model."""
223203

224-
@given(config=vcspull_config_strategy())
225-
def test_config_roundtrip(self, config: VCSPullConfig) -> None:
226-
"""Test configuration serialization and deserialization."""
227-
# Roundtrip test: convert to dict and back to model
228-
config_dict = config.model_dump()
229-
config2 = VCSPullConfig.model_validate(config_dict)
204+
class TestSettingsModel:
205+
"""Property-based tests for Settings model."""
206+
207+
@given(settings=settings_strategy())
208+
def test_settings_construction(self, settings: Settings) -> None:
209+
"""Test Settings model construction with varied inputs."""
210+
# Check types
211+
assert isinstance(settings.sync_remotes, bool)
212+
if settings.default_vcs is not None:
213+
assert settings.default_vcs in ["git", "hg", "svn"]
214+
if settings.depth is not None:
215+
assert isinstance(settings.depth, int)
216+
assert settings.depth > 0
217+
230218

231-
# The resulting object should match the original
232-
assert config2.settings.model_dump() == config.settings.model_dump()
233-
assert len(config2.repositories) == len(config.repositories)
234-
assert config2.includes == config.includes
219+
class TestVCSPullConfigModel:
220+
"""Property-based tests for VCSPullConfig model."""
235221

236222
@given(config=vcspull_config_strategy())
237-
def test_repository_uniqueness(self, config: VCSPullConfig) -> None:
238-
"""Test that repositories with the same path are treated as unique."""
239-
# This checks that we don't have unintended object identity issues
240-
repo_paths = [repo.path for repo in config.repositories]
241-
# Path uniqueness isn't enforced by the model, so we're just checking
242-
# that the objects are distinct even if paths might be the same
243-
assert len(repo_paths) == len(config.repositories)
223+
def test_config_construction(self, config: VCSPullConfig) -> None:
224+
"""Test VCSPullConfig model construction with varied inputs."""
225+
# Verify nested models are properly initialized
226+
assert isinstance(config.settings, Settings)
227+
assert all(isinstance(repo, Repository) for repo in config.repositories)
228+
assert all(isinstance(include, str) for include in config.includes)
229+
230+
@given(
231+
repo1=repository_strategy(),
232+
repo2=repository_strategy(),
233+
repo3=repository_strategy(),
234+
)
235+
def test_config_with_multiple_repositories(
236+
self, repo1: Repository, repo2: Repository, repo3: Repository
237+
) -> None:
238+
"""Test VCSPullConfig with multiple repositories."""
239+
# Create a config with multiple repositories
240+
config = VCSPullConfig(repositories=[repo1, repo2, repo3])
241+
242+
# Verify all repositories are present
243+
assert len(config.repositories) == 3
244+
assert repo1 in config.repositories
245+
assert repo2 in config.repositories
246+
assert repo3 in config.repositories

0 commit comments

Comments
 (0)