Skip to content

Commit 30baf54

Browse files
authored
chore: Add property tests for formatters (#12)
1 parent 5444609 commit 30baf54

5 files changed

Lines changed: 146 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*.swp
55
__pycache__
66
.pyc
7+
.hypothesis
78

89
# sample repo
910
tests/fixtures/sample-workspace

bough/formatters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def dependency_graph(analyzer: BoughAnalyzer) -> str:
113113
lines.extend(_render_graph(buildable, "🚀 Buildable Packages:", warning=True))
114114

115115
if libraries:
116-
lines.extend(_render_graph(buildable, "📚 Library Packages:"))
116+
lines.extend(_render_graph(libraries, "📚 Library Packages:"))
117117

118118
if not buildable and not libraries:
119119
lines.append("No packages found in workspace.")

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ source = "vcs"
2525

2626
[dependency-groups]
2727
dev = [
28+
"hypothesis>=6.143.1",
2829
"ipdb>=0.13.13",
2930
"pytest>=8.4.1",
3031
"pytest-cov>=7.0.0",

tests/test_formatters.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from hypothesis import given, strategies as st
2+
from unittest.mock import Mock
3+
from pathlib import Path
4+
import json
5+
import bough.formatters as sut
6+
7+
package_name = st.text(
8+
min_size=1,
9+
max_size=20,
10+
alphabet=st.characters(blacklist_characters='\n\r')
11+
)
12+
13+
packages = st.sets(package_name, min_size=1)
14+
files = st.sets(package_name, min_size=1)
15+
16+
@given(packages)
17+
def test_github_matrix_always_valid_json(package_names):
18+
"""GitHub matrix output must always be valid JSON."""
19+
analyzer = Mock()
20+
analyzer.packages = {
21+
name: Mock(directory=Path(f"/root/{name}"))
22+
for name in package_names
23+
}
24+
analyzer.workspace_root = Path("/root")
25+
26+
result = sut.github_matrix(analyzer, package_names)
27+
parsed = json.loads(result)
28+
29+
assert len(parsed["include"]) == len(package_names)
30+
assert all("package" in item for item in parsed["include"])
31+
32+
33+
@given(packages)
34+
def test_quiet_output_has_correct_line_count(package_names):
35+
"""Quiet mode outputs exactly one line per package."""
36+
analyzer = Mock()
37+
analyzer.packages = {
38+
name: Mock(directory=Path(f"/root/{name}"))
39+
for name in package_names
40+
}
41+
42+
result = sut.quiet(analyzer, package_names)
43+
44+
assert len(result.split("\n")) == len(package_names)
45+
46+
47+
@given(packages, files)
48+
def test_human_readable_contains_all_packages(packages, files):
49+
"""All package names must appear in human readable output."""
50+
analyzer = Mock()
51+
analyzer.packages = {
52+
name: Mock(directory=Path(f"/root/{name}"))
53+
for name in packages
54+
}
55+
analyzer.workspace_root = Path("/root")
56+
57+
result = sut.human_readable(analyzer, packages, files)
58+
59+
for pkg in packages:
60+
assert pkg in result
61+
62+
63+
64+
@st.composite
65+
def package_graph(draw):
66+
"""Generate a set of packages with dependencies referencing each other."""
67+
num_packages = draw(st.integers(min_value=1, max_value=10))
68+
names = [draw(package_name) for _ in range(num_packages)]
69+
names = list(set(names))
70+
71+
packages = []
72+
for name in names:
73+
deps = draw(st.sets(st.sampled_from(names), max_size=3)) - {name}
74+
is_buildable = draw(st.booleans())
75+
packages.append({
76+
"name": name,
77+
"dependencies": deps,
78+
"is_buildable": is_buildable
79+
})
80+
81+
return packages
82+
83+
@given(package_graph())
84+
def test_dependency_graph_contains_all_packages(packages):
85+
"""All package names must appear in dependency graph output."""
86+
analyzer = Mock()
87+
analyzer.packages = {
88+
pkg["name"]: Mock(
89+
directory=Path(f"/root/{pkg['name']}"),
90+
dependencies=pkg["dependencies"]
91+
)
92+
for pkg in packages
93+
}
94+
analyzer.workspace_root = Path("/root")
95+
96+
# Build reverse dependency graph
97+
analyzer.dependency_graph = {name: set() for name in analyzer.packages}
98+
for pkg in packages:
99+
for dep in pkg["dependencies"]:
100+
if dep in analyzer.dependency_graph:
101+
analyzer.dependency_graph[dep].add(pkg["name"])
102+
103+
buildable_names = {pkg["name"] for pkg in packages if pkg["is_buildable"]}
104+
analyzer._is_buildable_package = lambda p: p.directory.name in buildable_names
105+
106+
result = sut.dependency_graph(analyzer)
107+
108+
for pkg in packages:
109+
assert pkg["name"] in result

uv.lock

Lines changed: 34 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)