From 5a2828d23f14f0559a26b260dcbe02dac2b089ee Mon Sep 17 00:00:00 2001 From: Andrew Nichols Date: Sun, 2 Nov 2025 12:38:21 -0500 Subject: [PATCH] chore: Add property tests for formatters --- .gitignore | 1 + bough/formatters.py | 2 +- pyproject.toml | 1 + tests/test_formatters.py | 109 +++++++++++++++++++++++++++++++++++++++ uv.lock | 35 ++++++++++++- 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 tests/test_formatters.py diff --git a/.gitignore b/.gitignore index e1129a9..e1dc878 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.swp __pycache__ .pyc +.hypothesis # sample repo tests/fixtures/sample-workspace diff --git a/bough/formatters.py b/bough/formatters.py index 0975fb1..9824a36 100644 --- a/bough/formatters.py +++ b/bough/formatters.py @@ -113,7 +113,7 @@ def dependency_graph(analyzer: BoughAnalyzer) -> str: lines.extend(_render_graph(buildable, "🚀 Buildable Packages:", warning=True)) if libraries: - lines.extend(_render_graph(buildable, "📚 Library Packages:")) + lines.extend(_render_graph(libraries, "📚 Library Packages:")) if not buildable and not libraries: lines.append("No packages found in workspace.") diff --git a/pyproject.toml b/pyproject.toml index 0aaeffe..ae95621 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ source = "vcs" [dependency-groups] dev = [ + "hypothesis>=6.143.1", "ipdb>=0.13.13", "pytest>=8.4.1", "pytest-cov>=7.0.0", diff --git a/tests/test_formatters.py b/tests/test_formatters.py new file mode 100644 index 0000000..7de1022 --- /dev/null +++ b/tests/test_formatters.py @@ -0,0 +1,109 @@ +from hypothesis import given, strategies as st +from unittest.mock import Mock +from pathlib import Path +import json +import bough.formatters as sut + +package_name = st.text( + min_size=1, + max_size=20, + alphabet=st.characters(blacklist_characters='\n\r') +) + +packages = st.sets(package_name, min_size=1) +files = st.sets(package_name, min_size=1) + +@given(packages) +def test_github_matrix_always_valid_json(package_names): + """GitHub matrix output must always be valid JSON.""" + analyzer = Mock() + analyzer.packages = { + name: Mock(directory=Path(f"/root/{name}")) + for name in package_names + } + analyzer.workspace_root = Path("/root") + + result = sut.github_matrix(analyzer, package_names) + parsed = json.loads(result) + + assert len(parsed["include"]) == len(package_names) + assert all("package" in item for item in parsed["include"]) + + +@given(packages) +def test_quiet_output_has_correct_line_count(package_names): + """Quiet mode outputs exactly one line per package.""" + analyzer = Mock() + analyzer.packages = { + name: Mock(directory=Path(f"/root/{name}")) + for name in package_names + } + + result = sut.quiet(analyzer, package_names) + + assert len(result.split("\n")) == len(package_names) + + +@given(packages, files) +def test_human_readable_contains_all_packages(packages, files): + """All package names must appear in human readable output.""" + analyzer = Mock() + analyzer.packages = { + name: Mock(directory=Path(f"/root/{name}")) + for name in packages + } + analyzer.workspace_root = Path("/root") + + result = sut.human_readable(analyzer, packages, files) + + for pkg in packages: + assert pkg in result + + + +@st.composite +def package_graph(draw): + """Generate a set of packages with dependencies referencing each other.""" + num_packages = draw(st.integers(min_value=1, max_value=10)) + names = [draw(package_name) for _ in range(num_packages)] + names = list(set(names)) + + packages = [] + for name in names: + deps = draw(st.sets(st.sampled_from(names), max_size=3)) - {name} + is_buildable = draw(st.booleans()) + packages.append({ + "name": name, + "dependencies": deps, + "is_buildable": is_buildable + }) + + return packages + +@given(package_graph()) +def test_dependency_graph_contains_all_packages(packages): + """All package names must appear in dependency graph output.""" + analyzer = Mock() + analyzer.packages = { + pkg["name"]: Mock( + directory=Path(f"/root/{pkg['name']}"), + dependencies=pkg["dependencies"] + ) + for pkg in packages + } + analyzer.workspace_root = Path("/root") + + # Build reverse dependency graph + analyzer.dependency_graph = {name: set() for name in analyzer.packages} + for pkg in packages: + for dep in pkg["dependencies"]: + if dep in analyzer.dependency_graph: + analyzer.dependency_graph[dep].add(pkg["name"]) + + buildable_names = {pkg["name"] for pkg in packages if pkg["is_buildable"]} + analyzer._is_buildable_package = lambda p: p.directory.name in buildable_names + + result = sut.dependency_graph(analyzer) + + for pkg in packages: + assert pkg["name"] in result diff --git a/uv.lock b/uv.lock index 92f0e8e..53021a7 100644 --- a/uv.lock +++ b/uv.lock @@ -10,9 +10,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, ] +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, +] + [[package]] name = "bough" -version = "0.2.2.dev4+g48476bd3f.d20251102" +version = "0.2.2.dev6+g5444609bb.d20251102" source = { editable = "." } dependencies = [ { name = "gitpython" }, @@ -22,6 +31,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "hypothesis" }, { name = "ipdb" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -39,6 +49,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "hypothesis", specifier = ">=6.143.1" }, { name = "ipdb", specifier = ">=0.13.13" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-cov", specifier = ">=7.0.0" }, @@ -172,6 +183,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168 }, ] +[[package]] +name = "hypothesis" +version = "6.143.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/7e/7217747127792254cef8e7f0b82bc3fe659f46b4efbd2e33abb76a8a6978/hypothesis-6.143.1.tar.gz", hash = "sha256:27954227d85057c0456c0b4e5953fdf6a0eff989874dcda21e20b5a41b4f1a59", size = 466406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/c6/b6d3e96fa2367edb3f30f6b3ccb7cd061b1603f210311985253099d13b55/hypothesis-6.143.1-py3-none-any.whl", hash = "sha256:7974544d0d5b7041688b544aa2dbeac69a28def90e010ff5d2b2cbc85bd40dd2", size = 533720 }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -420,6 +444,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, +] + [[package]] name = "stack-data" version = "0.6.3"