Skip to content

Commit 13cc03c

Browse files
Improve coverage (#168)
--------- Co-authored-by: Sushant Gaurav <[email protected]>
1 parent 0f3e37d commit 13cc03c

7 files changed

Lines changed: 176 additions & 10 deletions

File tree

changelog.d/168.infra.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve test coverage for :file:`git.py`, :file:`env.py`, and
2+
:file:`lifecycle.py`. Raises coverage report by 1%.

src/docbuild/models/config/env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ class EnvGeneralConfig(BaseModel):
156156
)
157157
"The canonical domain for URLs."
158158

159-
# --- NEW: Custom Serialization for LanguageCode Models ---
159+
# --- Custom Serialization for LanguageCode Models ---
160160
@field_serializer("default_lang")
161161
def serialize_default_lang(self, lang_obj: LanguageCode) -> str:
162162
"""Serialize the LanguageCode model back to a simple string (e.g., 'en-us')."""

src/docbuild/models/doctype.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ def product_xpath_segment(self: Self) -> str:
268268
269269
Example: "product[@productid='sles']" or "product"
270270
"""
271-
if self.product != Product.ALL:
271+
if self.product != Product["ALL"]:
272272
return f"product[@productid={self.product.value!r}]"
273273
return "product"
274274

tests/models/config/test_env.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,14 @@ def test_envconfig_full_success(mock_valid_raw_env_data: dict[str, Any]):
174174
assert isinstance(config.xslt_params["external"], dict)
175175
assert config.xslt_params["show"]["edit"]["link"] == 1
176176

177+
# Test serialization of LanguageCode fields back to strings
178+
dumped_config = config.model_dump()
179+
assert isinstance(dumped_config["config"]["default_lang"], str)
180+
assert dumped_config["config"]["default_lang"] == "en-us"
181+
assert isinstance(dumped_config["config"]["languages"], list)
182+
assert all(isinstance(lang, str) for lang in dumped_config["config"]["languages"])
183+
assert set(dumped_config["config"]["languages"]) == {"en-us", "de-de"}
184+
177185

178186
def test_envconfig_type_coercion_ip_host(mock_valid_raw_env_data: dict[str, Any]):
179187
"""Test that the host field handles IPvAnyAddress correctly."""
@@ -267,3 +275,23 @@ def test_envconfig_invalid_role_fails(mock_valid_raw_env_data: dict[str, Any]):
267275

268276
locs = excinfo.value.errors()[0]["loc"]
269277
assert ("server", "role") == tuple(locs)
278+
279+
280+
def test_envconfig_unresolved_placeholder_fails(
281+
mock_valid_raw_env_data: dict[str, Any]
282+
):
283+
"""Test that an unresolved placeholder raises a ValueError."""
284+
data = mock_valid_raw_env_data.copy()
285+
# Introduce an unresolvable placeholder
286+
data["paths"]["jinja_dir"] = "/etc/docbuild/{UNRESOLVED_PLACEHOLDER}"
287+
288+
with pytest.raises(ValueError, match="Configuration placeholder error"):
289+
EnvConfig.from_dict(data)
290+
291+
292+
def test_envconfig_with_non_dict_input():
293+
"""Test that passing non-dict input to the validator is handled."""
294+
# This covers the `else: return data` path in `_resolve_placeholders`.
295+
# Pydantic will then raise a ValidationError because the input is not a dict.
296+
with pytest.raises(ValidationError, match="Input should be a valid dictionary"):
297+
EnvConfig.model_validate("not a dict")

tests/models/test_doctype.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,3 +309,17 @@ def test_xpath_in_doctype(string, xpath):
309309
"""Test the XPath extraction from a Doctype."""
310310
doctype = Doctype.from_str(string)
311311
assert xpath == doctype.xpath()
312+
313+
314+
def test_product_xpath_segment():
315+
"""Test the product_xpath_segment method."""
316+
# Test with all products (*)
317+
dt_all = Doctype.from_str("*/15-SP6/en-us")
318+
assert dt_all.product_xpath_segment() == "product"
319+
320+
321+
def test_docset_xpath_segment():
322+
"""Test the docset_xpath_segment method."""
323+
# Test with all docsets (*)
324+
dt_all = Doctype.from_str("sles/*/en-us")
325+
assert dt_all.docset_xpath_segment("*") == "docset"

tests/models/test_lifecycle.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ def test_unknown_lifecycle():
2020
assert instance.name == "unknown"
2121

2222

23+
def test_invalid_lifecycle_from_constructor():
24+
"""Creating a lifecycle from an invalid string via constructor raises ValueError."""
25+
with pytest.raises(ValueError, match="'invalid' is not a valid LifecycleFlag"):
26+
LifecycleFlag("invalid")
27+
28+
2329
def test_lifecycle_flag_from_str_with_empty_string():
2430
instance = LifecycleFlag.from_str("")
2531
assert instance == LifecycleFlag.unknown

tests/utils/test_git.py

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import asyncio
44
from pathlib import Path
55
from subprocess import CompletedProcess
6-
from unittest.mock import AsyncMock
6+
from unittest.mock import AsyncMock, patch
77

88
import pytest
99

@@ -26,13 +26,8 @@ def mock_subprocess_exec(monkeypatch) -> AsyncMock:
2626
@pytest.fixture
2727
def mock_execute_git(monkeypatch) -> AsyncMock:
2828
"""Fixture to mock execute_git_command."""
29-
30-
async def side_effect(*args, **kwargs):
31-
if args[0] == "clone":
32-
return CompletedProcess(args, 0, "Cloning...", "")
33-
return CompletedProcess(args, 0, "stdout success", "")
34-
35-
mock = AsyncMock(side_effect=side_effect)
29+
# Remove the side_effect to allow tests to set their own return_value
30+
mock = AsyncMock()
3631
monkeypatch.setattr(git_module, "execute_git_command", mock)
3732
return mock
3833

@@ -48,6 +43,9 @@ async def test_managed_repo_clone_bare_new(
4843
):
4944
"""Test clone_bare when the repository does not exist yet."""
5045
repo = ManagedGitRepo("http://a.b/org/c.git", tmp_path)
46+
mock_execute_git.return_value = CompletedProcess(
47+
args=[], returncode=0, stdout="", stderr=""
48+
)
5149
# Simulate repo does not exist
5250
monkeypatch.setattr(Path, "exists", lambda self: False)
5351

@@ -70,6 +68,9 @@ async def test_managed_repo_clone_bare_exists(
7068
):
7169
"""Test clone_bare when the repository already exists."""
7270
repo = ManagedGitRepo("http://a.b/org/c.git", tmp_path)
71+
mock_execute_git.return_value = CompletedProcess(
72+
args=[], returncode=0, stdout="", stderr=""
73+
)
7374
# Simulate repo exists
7475
monkeypatch.setattr(Path, "exists", lambda self: True)
7576

@@ -94,6 +95,9 @@ async def test_managed_repo_create_worktree_success(
9495
tmp_path: Path, mock_execute_git: AsyncMock, monkeypatch
9596
):
9697
"""Test create_worktree successfully creates a worktree."""
98+
mock_execute_git.return_value = CompletedProcess(
99+
args=[], returncode=0, stdout="", stderr=""
100+
)
97101
repo = ManagedGitRepo("http://a.b/org/c.git", tmp_path)
98102
# Simulate bare repo exists
99103
monkeypatch.setattr(Path, "exists", lambda self: True)
@@ -117,6 +121,9 @@ async def test_managed_repo_create_worktree_with_options(
117121
tmp_path: Path, mock_execute_git: AsyncMock, monkeypatch
118122
):
119123
"""Test create_worktree with additional clone options."""
124+
mock_execute_git.return_value = CompletedProcess(
125+
args=[], returncode=0, stdout="", stderr=""
126+
)
120127
repo = ManagedGitRepo("http://a.b/org/c.git", tmp_path)
121128
# Simulate bare repo exists
122129
monkeypatch.setattr(Path, "exists", lambda self: True)
@@ -157,6 +164,9 @@ async def test_managed_repo_create_worktree_not_local(
157164
tmp_path: Path, mock_execute_git: AsyncMock, monkeypatch
158165
):
159166
"""Test create_worktree without the --local flag."""
167+
mock_execute_git.return_value = CompletedProcess(
168+
args=[], returncode=0, stdout="", stderr=""
169+
)
160170
repo = ManagedGitRepo("http://a.b/org/c.git", tmp_path)
161171
# Simulate bare repo exists
162172
monkeypatch.setattr(Path, "exists", lambda self: True)
@@ -218,6 +228,9 @@ async def test_fetch_updates_success(
218228
tmp_path: Path, mock_execute_git: AsyncMock, monkeypatch
219229
):
220230
"""Test fetch_updates successfully fetches updates."""
231+
mock_execute_git.return_value = CompletedProcess(
232+
args=[], returncode=0, stdout="", stderr=""
233+
)
221234
repo = ManagedGitRepo("http://a.b/org/c.git", tmp_path)
222235
# Simulate bare repo exists
223236
monkeypatch.setattr(Path, "exists", lambda self: True)
@@ -260,6 +273,9 @@ async def test_managed_repo_clone_bare_already_processed(
260273
tmp_path: Path, mock_execute_git: AsyncMock, monkeypatch
261274
):
262275
"""Test clone_bare when the repository has been processed in this run."""
276+
mock_execute_git.return_value = CompletedProcess(
277+
args=[], returncode=0, stdout="", stderr=""
278+
)
263279
repo = ManagedGitRepo("http://a.b/org/c.git", tmp_path)
264280
# Simulate repo does not exist for the first call
265281
monkeypatch.setattr(Path, "exists", lambda self: False)
@@ -284,3 +300,103 @@ async def test_managed_repo_clone_bare_already_processed(
284300
assert result2 is True
285301
# The mock should still have only been called once
286302
mock_execute_git.assert_awaited_once()
303+
304+
305+
async def test_ls_tree_no_repo(
306+
tmp_path: Path, mock_execute_git: AsyncMock, monkeypatch
307+
):
308+
"""Test ls_tree when the bare repository does not exist."""
309+
repo = ManagedGitRepo("http://a.b/org/c.git", tmp_path)
310+
# Simulate bare repo does not exist
311+
monkeypatch.setattr(Path, "exists", lambda self: False)
312+
313+
with patch.object(git_module.log, "warning") as mock_log_warning:
314+
result = await repo.ls_tree("main")
315+
316+
assert result == []
317+
mock_log_warning.assert_called_once_with(
318+
"Cannot run ls-tree: Bare repository does not exist at %s",
319+
repo.bare_repo_path,
320+
)
321+
mock_execute_git.assert_not_awaited()
322+
323+
324+
async def test_ls_tree_success_recursive(
325+
tmp_path: Path, mock_execute_git: AsyncMock, monkeypatch
326+
):
327+
"""Test ls_tree successfully lists files recursively."""
328+
repo = ManagedGitRepo("http://a.b/org/c.git", tmp_path)
329+
monkeypatch.setattr(Path, "exists", lambda self: True)
330+
mock_execute_git.return_value = CompletedProcess(
331+
args=[], returncode=0, stdout="file1.txt\ndir/file2.txt", stderr=""
332+
)
333+
334+
result = await repo.ls_tree("main")
335+
336+
assert result == ["file1.txt", "dir/file2.txt"]
337+
mock_execute_git.assert_awaited_once_with(
338+
"ls-tree",
339+
"--name-only",
340+
"-r",
341+
"main",
342+
cwd=repo.bare_repo_path,
343+
gitconfig=None,
344+
)
345+
346+
347+
async def test_ls_tree_success_non_recursive(
348+
tmp_path: Path, mock_execute_git: AsyncMock, monkeypatch
349+
):
350+
"""Test ls_tree successfully lists files non-recursively."""
351+
repo = ManagedGitRepo("http://a.b/org/c.git", tmp_path)
352+
monkeypatch.setattr(Path, "exists", lambda self: True)
353+
mock_execute_git.return_value = CompletedProcess(
354+
args=[], returncode=0, stdout="file1.txt", stderr=""
355+
)
356+
357+
result = await repo.ls_tree("main", recursive=False)
358+
359+
assert result == ["file1.txt"]
360+
mock_execute_git.assert_awaited_once_with(
361+
"ls-tree",
362+
"--name-only",
363+
"main",
364+
cwd=repo.bare_repo_path,
365+
gitconfig=None,
366+
)
367+
368+
369+
async def test_ls_tree_empty_output(
370+
tmp_path: Path, mock_execute_git: AsyncMock, monkeypatch
371+
):
372+
"""Test ls_tree handles empty stdout correctly."""
373+
repo = ManagedGitRepo("http://a.b/org/c.git", tmp_path)
374+
monkeypatch.setattr(Path, "exists", lambda self: True)
375+
mock_execute_git.return_value = CompletedProcess(
376+
args=[], returncode=0, stdout="", stderr=""
377+
)
378+
379+
result = await repo.ls_tree("main")
380+
381+
assert result == []
382+
383+
384+
async def test_ls_tree_command_failure(
385+
tmp_path: Path, mock_execute_git: AsyncMock, monkeypatch
386+
):
387+
"""Test ls_tree handles a RuntimeError from execute_git_command."""
388+
repo = ManagedGitRepo("http://a.b/org/c.git", tmp_path)
389+
monkeypatch.setattr(Path, "exists", lambda self: True)
390+
error = RuntimeError("Git command failed")
391+
mock_execute_git.side_effect = error
392+
393+
with patch.object(git_module.log, "error") as mock_log_error:
394+
result = await repo.ls_tree("develop")
395+
396+
assert result == []
397+
mock_log_error.assert_called_once_with(
398+
"Failed to run ls-tree on branch '%s' in '%s': %s",
399+
"develop",
400+
repo.slug,
401+
error,
402+
)

0 commit comments

Comments
 (0)