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
88from __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
1414import hypothesis .strategies as st
15- from hypothesis import given
15+ import pytest
16+ from hypothesis import given , settings
1617
1718from 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
144144def 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