1
1
"""Property-based tests for configuration models.
2
2
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 .
6
6
"""
7
7
8
8
from __future__ import annotations
9
9
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
13
13
14
14
import hypothesis .strategies as st
15
- from hypothesis import given
15
+ import pytest
16
+ from hypothesis import given , settings
16
17
17
18
from vcspull .config .models import Repository , Settings , VCSPullConfig
18
19
19
20
20
- # Define strategies for generating test data
21
21
@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 :
23
23
"""Generate valid URLs for repositories."""
24
24
protocols = ["https://" , "http://" , "git://" , "ssh://git@" ]
25
25
domains = ["github.com" , "gitlab.com" , "bitbucket.org" , "example.com" ]
@@ -50,7 +50,7 @@ def valid_url_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> str:
50
50
51
51
52
52
@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 :
54
54
"""Generate valid paths for repositories."""
55
55
base_dirs = ["~/code" , "~/projects" , "/tmp" , "./projects" ]
56
56
sub_dirs = [
@@ -75,7 +75,7 @@ def valid_path_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> str:
75
75
76
76
77
77
@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 :
79
79
"""Generate valid Repository instances."""
80
80
name = draw (st .one_of (st .none (), st .text (min_size = 1 , max_size = 20 )))
81
81
url = draw (valid_url_strategy ())
@@ -127,7 +127,7 @@ def repository_strategy(draw: Callable[[st.SearchStrategy[Any]], Any]) -> Reposi
127
127
128
128
129
129
@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 :
131
131
"""Generate valid Settings instances."""
132
132
sync_remotes = draw (st .booleans ())
133
133
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
142
142
143
143
@st .composite
144
144
def vcspull_config_strategy (
145
- draw : Callable [[st .SearchStrategy [Any ]], Any ],
145
+ draw : t . Callable [[st .SearchStrategy [t . Any ]], t . Any ]
146
146
) -> VCSPullConfig :
147
147
"""Generate valid VCSPullConfig instances."""
148
148
settings = draw (settings_strategy ())
@@ -151,9 +151,9 @@ def vcspull_config_strategy(
151
151
repo_count = draw (st .integers (min_value = 0 , max_value = 5 ))
152
152
repositories = [draw (repository_strategy ()) for _ in range (repo_count )]
153
153
154
- # Generate includes
154
+ # Optionally generate includes (0 to 3)
155
155
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 )]
157
157
158
158
return VCSPullConfig (
159
159
settings = settings ,
@@ -162,82 +162,85 @@ def vcspull_config_strategy(
162
162
)
163
163
164
164
165
- class TestRepositoryProperties :
166
- """Property-based tests for the Repository model."""
165
+ class TestRepositoryModel :
166
+ """Property-based tests for Repository model."""
167
167
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
172
174
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 () != ""
177
179
178
180
@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 ()
207
189
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 ()
214
195
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 )
219
198
199
+ # If original path started with ~, expanded should be absolute
200
+ if repository .path .startswith ("~" ):
201
+ assert os .path .isabs (expanded_path )
220
202
221
- class TestVCSPullConfigProperties :
222
- """Property-based tests for the VCSPullConfig model."""
223
203
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
+
230
218
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."""
235
221
236
222
@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