woolly 0.3.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {woolly-0.3.0 → woolly-0.5.0}/PKG-INFO +3 -1
- woolly-0.5.0/examples/template/example_template.md +16 -0
- {woolly-0.3.0 → woolly-0.5.0}/pyproject.toml +5 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/conftest.py +49 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/functional/test_cli.py +83 -0
- woolly-0.5.0/tests/unit/test_commands/test_check.py +440 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_languages/test_base.py +312 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_languages/test_python.py +266 -9
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_languages/test_rust.py +119 -10
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_optional_dependencies.py +39 -45
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_reporters/test_json.py +86 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_reporters/test_markdown.py +65 -0
- woolly-0.5.0/tests/unit/test_reporters/test_stdout.py +187 -0
- woolly-0.5.0/tests/unit/test_reporters/test_template.py +358 -0
- {woolly-0.3.0 → woolly-0.5.0}/uv.lock +104 -0
- {woolly-0.3.0 → woolly-0.5.0}/woolly/cache.py +21 -6
- woolly-0.5.0/woolly/commands/check.py +604 -0
- woolly-0.5.0/woolly/http.py +53 -0
- {woolly-0.3.0 → woolly-0.5.0}/woolly/languages/base.py +187 -10
- woolly-0.5.0/woolly/languages/python.py +413 -0
- {woolly-0.3.0 → woolly-0.5.0}/woolly/languages/rust.py +59 -1
- {woolly-0.3.0 → woolly-0.5.0}/woolly/reporters/__init__.py +15 -1
- {woolly-0.3.0 → woolly-0.5.0}/woolly/reporters/base.py +25 -0
- {woolly-0.3.0 → woolly-0.5.0}/woolly/reporters/json.py +102 -3
- {woolly-0.3.0 → woolly-0.5.0}/woolly/reporters/markdown.py +83 -9
- woolly-0.5.0/woolly/reporters/stdout.py +208 -0
- woolly-0.5.0/woolly/reporters/template.py +245 -0
- woolly-0.3.0/tests/unit/test_commands/test_check.py +0 -285
- woolly-0.3.0/tests/unit/test_reporters/test_stdout.py +0 -104
- woolly-0.3.0/woolly/commands/check.py +0 -422
- woolly-0.3.0/woolly/http.py +0 -34
- woolly-0.3.0/woolly/languages/python.py +0 -200
- woolly-0.3.0/woolly/reporters/stdout.py +0 -75
- {woolly-0.3.0 → woolly-0.5.0}/.coverage +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/.github/workflows/lint.yml +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/.github/workflows/publish.yml +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/.github/workflows/tests.yml +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/.gitignore +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/.python-version +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/LICENSE +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/README.md +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/__init__.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/functional/__init__.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/functional/test_optional_flag.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/__init__.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_cache.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_commands/__init__.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_commands/test_other_commands.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_debug.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_languages/__init__.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_languages/test_registry.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_progress.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_reporters/__init__.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_reporters/test_base.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/tests/unit/test_reporters/test_registry.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/woolly/__init__.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/woolly/__main__.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/woolly/commands/__init__.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/woolly/commands/clear_cache.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/woolly/commands/list_formats.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/woolly/commands/list_languages.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/woolly/debug.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/woolly/languages/__init__.py +0 -0
- {woolly-0.3.0 → woolly-0.5.0}/woolly/progress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: woolly
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Check if package dependencies are available in Fedora. Supports Rust, Python, and more.
|
|
5
5
|
Author-email: Rodolfo Olivieri <rodolfo.olivieri3@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -26,6 +26,8 @@ Requires-Dist: cyclopts>=4.3.0
|
|
|
26
26
|
Requires-Dist: httpx>=0.28.0
|
|
27
27
|
Requires-Dist: pydantic>=2.10.0
|
|
28
28
|
Requires-Dist: rich>=14.2.0
|
|
29
|
+
Provides-Extra: template
|
|
30
|
+
Requires-Dist: jinja2>=3.1.0; extra == 'template'
|
|
29
31
|
Description-Content-Type: text/markdown
|
|
30
32
|
|
|
31
33
|
# 🐑 Woolly
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Report for {{ root_package }}
|
|
2
|
+
|
|
3
|
+
Generated: {{ timestamp }}
|
|
4
|
+
Language: {{ language }}
|
|
5
|
+
|
|
6
|
+
## Summary
|
|
7
|
+
- Total: {{ total_dependencies }}
|
|
8
|
+
- Packaged: {{ packaged_count }}
|
|
9
|
+
- Missing: {{ missing_count }}
|
|
10
|
+
|
|
11
|
+
{% if missing_packages %}
|
|
12
|
+
## Missing Packages
|
|
13
|
+
{% for pkg in missing_packages %}
|
|
14
|
+
- {{ pkg }}
|
|
15
|
+
{% endfor %}
|
|
16
|
+
{% endif %}
|
|
@@ -37,6 +37,11 @@ classifiers = [
|
|
|
37
37
|
"Topic :: Utilities",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
template = [
|
|
42
|
+
"jinja2>=3.1.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
40
45
|
[project.url]
|
|
41
46
|
"Source code" = "https://github.com/r0x0d/woolly"
|
|
42
47
|
"Bug Tracker" = "https://github.com/r0x0d/woolly/issues"
|
|
@@ -24,6 +24,10 @@ def temp_cache_dir(tmp_path, monkeypatch):
|
|
|
24
24
|
cache_dir = tmp_path / "cache"
|
|
25
25
|
cache_dir.mkdir()
|
|
26
26
|
monkeypatch.setattr("woolly.cache.CACHE_DIR", cache_dir)
|
|
27
|
+
# Reset the ensured-namespaces set so that the new CACHE_DIR is used
|
|
28
|
+
import woolly.cache as _cache_mod
|
|
29
|
+
|
|
30
|
+
_cache_mod._ensured_namespaces.clear()
|
|
27
31
|
return cache_dir
|
|
28
32
|
|
|
29
33
|
|
|
@@ -119,6 +123,7 @@ def mock_crates_io_response():
|
|
|
119
123
|
"description": "A serialization framework for Rust",
|
|
120
124
|
"homepage": "https://serde.rs",
|
|
121
125
|
"repository": "https://github.com/serde-rs/serde",
|
|
126
|
+
"license": "MIT OR Apache-2.0",
|
|
122
127
|
}
|
|
123
128
|
}
|
|
124
129
|
|
|
@@ -154,12 +159,55 @@ def mock_pypi_response():
|
|
|
154
159
|
"summary": "Python HTTP for Humans",
|
|
155
160
|
"home_page": "https://requests.readthedocs.io",
|
|
156
161
|
"project_url": "https://github.com/psf/requests",
|
|
162
|
+
"license": "Apache-2.0",
|
|
163
|
+
"license_expression": None,
|
|
164
|
+
"provides_extra": ["socks"],
|
|
165
|
+
"requires_dist": [
|
|
166
|
+
"charset-normalizer<4,>=2",
|
|
167
|
+
"idna<4,>=2.5",
|
|
168
|
+
"urllib3<3,>=1.21.1",
|
|
169
|
+
"certifi>=2017.4.17",
|
|
170
|
+
"PySocks!=1.5.7,>=1.5.6; extra == 'socks'",
|
|
171
|
+
],
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.fixture
|
|
177
|
+
def mock_crates_io_version_response():
|
|
178
|
+
"""Create a mock crates.io version API response (for features)."""
|
|
179
|
+
return {
|
|
180
|
+
"version": {
|
|
181
|
+
"num": "1.0.200",
|
|
182
|
+
"features": {
|
|
183
|
+
"default": ["std", "derive"],
|
|
184
|
+
"std": [],
|
|
185
|
+
"derive": ["serde_derive"],
|
|
186
|
+
"alloc": [],
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@pytest.fixture
|
|
193
|
+
def mock_pypi_version_response():
|
|
194
|
+
"""Create a mock PyPI version API response (for extras/features)."""
|
|
195
|
+
return {
|
|
196
|
+
"info": {
|
|
197
|
+
"name": "requests",
|
|
198
|
+
"version": "2.31.0",
|
|
199
|
+
"summary": "Python HTTP for Humans",
|
|
200
|
+
"license": "Apache-2.0",
|
|
201
|
+
"license_expression": None,
|
|
202
|
+
"provides_extra": ["socks", "security"],
|
|
157
203
|
"requires_dist": [
|
|
158
204
|
"charset-normalizer<4,>=2",
|
|
159
205
|
"idna<4,>=2.5",
|
|
160
206
|
"urllib3<3,>=1.21.1",
|
|
161
207
|
"certifi>=2017.4.17",
|
|
162
208
|
"PySocks!=1.5.7,>=1.5.6; extra == 'socks'",
|
|
209
|
+
"pyOpenSSL>=0.14; extra == 'security'",
|
|
210
|
+
"cryptography>=1.3.4; extra == 'security'",
|
|
163
211
|
],
|
|
164
212
|
}
|
|
165
213
|
}
|
|
@@ -174,6 +222,7 @@ def mock_pypi_response():
|
|
|
174
222
|
def mock_console():
|
|
175
223
|
"""Create a mock Rich console."""
|
|
176
224
|
console = MagicMock(spec=Console)
|
|
225
|
+
console.width = 120 # Wide enough for side-by-side layout
|
|
177
226
|
return console
|
|
178
227
|
|
|
179
228
|
|
|
@@ -34,6 +34,7 @@ class TestCliHelp:
|
|
|
34
34
|
assert result.returncode == 0
|
|
35
35
|
assert "package" in result.stdout.lower()
|
|
36
36
|
assert "--lang" in result.stdout
|
|
37
|
+
assert "--missing-only" in result.stdout
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
class TestListLanguages:
|
|
@@ -263,6 +264,32 @@ class TestCheckCommandPython:
|
|
|
263
264
|
class TestCheckCommandOptions:
|
|
264
265
|
"""Tests for check command options."""
|
|
265
266
|
|
|
267
|
+
@pytest.mark.functional
|
|
268
|
+
@pytest.mark.slow
|
|
269
|
+
def test_missing_only_option(self, cli_runner, tmp_path, monkeypatch):
|
|
270
|
+
"""Good path: missing-only option is accepted."""
|
|
271
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
272
|
+
|
|
273
|
+
result = cli_runner(
|
|
274
|
+
"check", "cfg-if", "--lang", "rust", "--missing-only", "--no-progress"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
assert result.returncode == 0
|
|
278
|
+
# Should not show the dependency tree when --missing-only is used
|
|
279
|
+
assert "Dependency Tree:" not in result.stdout
|
|
280
|
+
|
|
281
|
+
@pytest.mark.functional
|
|
282
|
+
@pytest.mark.slow
|
|
283
|
+
def test_missing_only_short_option(self, cli_runner, tmp_path, monkeypatch):
|
|
284
|
+
"""Good path: missing-only short option (-m) is accepted."""
|
|
285
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
286
|
+
|
|
287
|
+
result = cli_runner("check", "cfg-if", "--lang", "rust", "-m", "--no-progress")
|
|
288
|
+
|
|
289
|
+
assert result.returncode == 0
|
|
290
|
+
# Should not show the dependency tree when -m is used
|
|
291
|
+
assert "Dependency Tree:" not in result.stdout
|
|
292
|
+
|
|
266
293
|
@pytest.mark.functional
|
|
267
294
|
@pytest.mark.slow
|
|
268
295
|
def test_max_depth_option(self, cli_runner, tmp_path, monkeypatch):
|
|
@@ -275,6 +302,62 @@ class TestCheckCommandOptions:
|
|
|
275
302
|
|
|
276
303
|
assert result.returncode == 0
|
|
277
304
|
|
|
305
|
+
@pytest.mark.functional
|
|
306
|
+
@pytest.mark.slow
|
|
307
|
+
def test_exclude_option(self, cli_runner, tmp_path, monkeypatch):
|
|
308
|
+
"""Good path: exclude option is accepted and displayed."""
|
|
309
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
310
|
+
|
|
311
|
+
result = cli_runner(
|
|
312
|
+
"check",
|
|
313
|
+
"cfg-if",
|
|
314
|
+
"--lang",
|
|
315
|
+
"rust",
|
|
316
|
+
"--exclude",
|
|
317
|
+
"windows*",
|
|
318
|
+
"--no-progress",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
assert result.returncode == 0
|
|
322
|
+
assert "Excluding dependencies matching" in result.stdout
|
|
323
|
+
assert "windows*" in result.stdout
|
|
324
|
+
|
|
325
|
+
@pytest.mark.functional
|
|
326
|
+
@pytest.mark.slow
|
|
327
|
+
def test_exclude_multiple_patterns(self, cli_runner, tmp_path, monkeypatch):
|
|
328
|
+
"""Good path: multiple exclude patterns are accepted."""
|
|
329
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
330
|
+
|
|
331
|
+
result = cli_runner(
|
|
332
|
+
"check",
|
|
333
|
+
"cfg-if",
|
|
334
|
+
"--lang",
|
|
335
|
+
"rust",
|
|
336
|
+
"--exclude",
|
|
337
|
+
"windows*",
|
|
338
|
+
"--exclude",
|
|
339
|
+
"*-sys",
|
|
340
|
+
"--no-progress",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
assert result.returncode == 0
|
|
344
|
+
assert "Excluding dependencies matching" in result.stdout
|
|
345
|
+
assert "windows*" in result.stdout
|
|
346
|
+
assert "*-sys" in result.stdout
|
|
347
|
+
|
|
348
|
+
@pytest.mark.functional
|
|
349
|
+
@pytest.mark.slow
|
|
350
|
+
def test_exclude_short_option(self, cli_runner, tmp_path, monkeypatch):
|
|
351
|
+
"""Good path: -e short option works."""
|
|
352
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
353
|
+
|
|
354
|
+
result = cli_runner(
|
|
355
|
+
"check", "cfg-if", "--lang", "rust", "-e", "win*", "--no-progress"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
assert result.returncode == 0
|
|
359
|
+
assert "Excluding dependencies matching" in result.stdout
|
|
360
|
+
|
|
278
361
|
@pytest.mark.functional
|
|
279
362
|
@pytest.mark.slow
|
|
280
363
|
def test_debug_option(self, cli_runner, tmp_path, monkeypatch):
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for woolly.commands.check module.
|
|
3
|
+
|
|
4
|
+
Tests cover:
|
|
5
|
+
- Good path: tree building, stats collection
|
|
6
|
+
- Critical path: dependency traversal, visited tracking
|
|
7
|
+
- Bad path: unknown languages, max depth
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from unittest.mock import MagicMock
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
from rich.tree import Tree
|
|
14
|
+
|
|
15
|
+
from woolly.commands.check import TreeStats, _compute_stats_from_visited, build_tree
|
|
16
|
+
from woolly.languages.base import (
|
|
17
|
+
Dependency,
|
|
18
|
+
FedoraPackageStatus,
|
|
19
|
+
LanguageProvider,
|
|
20
|
+
PackageInfo,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MockProvider(LanguageProvider):
|
|
25
|
+
"""Mock provider for testing."""
|
|
26
|
+
|
|
27
|
+
name = "mock"
|
|
28
|
+
display_name = "Mock"
|
|
29
|
+
registry_name = "mock.io"
|
|
30
|
+
fedora_provides_prefix = "mock"
|
|
31
|
+
cache_namespace = "mock"
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.packages = {}
|
|
35
|
+
self.dependencies = {}
|
|
36
|
+
self.fedora_status = {}
|
|
37
|
+
|
|
38
|
+
def fetch_package_info(self, package_name: str):
|
|
39
|
+
return self.packages.get(package_name)
|
|
40
|
+
|
|
41
|
+
def fetch_dependencies(self, package_name: str, version: str):
|
|
42
|
+
return self.dependencies.get(f"{package_name}:{version}", [])
|
|
43
|
+
|
|
44
|
+
def fetch_features(self, package_name: str, version: str):
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
def check_fedora_packaging(self, package_name: str):
|
|
48
|
+
return self.fedora_status.get(
|
|
49
|
+
package_name, FedoraPackageStatus(is_packaged=False)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestBuildTree:
|
|
54
|
+
"""Tests for build_tree function."""
|
|
55
|
+
|
|
56
|
+
@pytest.fixture
|
|
57
|
+
def provider(self):
|
|
58
|
+
"""Create a mock provider."""
|
|
59
|
+
p = MockProvider()
|
|
60
|
+
# Set up a simple package
|
|
61
|
+
p.packages["root"] = PackageInfo(name="root", latest_version="1.0.0")
|
|
62
|
+
p.fedora_status["root"] = FedoraPackageStatus(
|
|
63
|
+
is_packaged=True, versions=["1.0.0"], package_names=["mock-root"]
|
|
64
|
+
)
|
|
65
|
+
return p
|
|
66
|
+
|
|
67
|
+
@pytest.mark.unit
|
|
68
|
+
def test_returns_tree_for_packaged_package(self, provider):
|
|
69
|
+
"""Good path: returns Tree for packaged package."""
|
|
70
|
+
tree = build_tree(provider, "root")
|
|
71
|
+
|
|
72
|
+
assert isinstance(tree, Tree)
|
|
73
|
+
label = str(tree.label)
|
|
74
|
+
assert "root" in label
|
|
75
|
+
assert "packaged" in label
|
|
76
|
+
|
|
77
|
+
@pytest.mark.unit
|
|
78
|
+
def test_returns_tree_for_not_packaged_package(self, provider):
|
|
79
|
+
"""Good path: returns Tree for not packaged package."""
|
|
80
|
+
provider.packages["missing"] = PackageInfo(
|
|
81
|
+
name="missing", latest_version="1.0.0"
|
|
82
|
+
)
|
|
83
|
+
provider.fedora_status["missing"] = FedoraPackageStatus(is_packaged=False)
|
|
84
|
+
|
|
85
|
+
tree = build_tree(provider, "missing")
|
|
86
|
+
|
|
87
|
+
assert isinstance(tree, Tree)
|
|
88
|
+
label = str(tree.label)
|
|
89
|
+
assert "not packaged" in label
|
|
90
|
+
|
|
91
|
+
@pytest.mark.unit
|
|
92
|
+
def test_returns_string_for_not_found_package(self, provider):
|
|
93
|
+
"""Bad path: returns string for package not in registry."""
|
|
94
|
+
result = build_tree(provider, "nonexistent")
|
|
95
|
+
|
|
96
|
+
assert isinstance(result, str)
|
|
97
|
+
assert "not found" in result
|
|
98
|
+
|
|
99
|
+
@pytest.mark.unit
|
|
100
|
+
def test_tracks_visited_packages(self, provider):
|
|
101
|
+
"""Critical path: tracks visited packages."""
|
|
102
|
+
visited = {}
|
|
103
|
+
|
|
104
|
+
build_tree(provider, "root", visited=visited)
|
|
105
|
+
|
|
106
|
+
assert "root" in visited
|
|
107
|
+
assert visited["root"][0] is True # is_packaged
|
|
108
|
+
|
|
109
|
+
@pytest.mark.unit
|
|
110
|
+
def test_returns_visited_marker_for_duplicate(self, provider):
|
|
111
|
+
"""Critical path: returns visited marker for already-visited packages."""
|
|
112
|
+
visited = {"root": (True, "1.0.0", False)}
|
|
113
|
+
|
|
114
|
+
result = build_tree(provider, "root", visited=visited)
|
|
115
|
+
|
|
116
|
+
assert isinstance(result, str)
|
|
117
|
+
assert "already visited" in result
|
|
118
|
+
|
|
119
|
+
@pytest.mark.unit
|
|
120
|
+
def test_respects_max_depth(self, provider):
|
|
121
|
+
"""Critical path: respects max depth limit."""
|
|
122
|
+
result = build_tree(provider, "root", depth=100, max_depth=50)
|
|
123
|
+
|
|
124
|
+
assert isinstance(result, str)
|
|
125
|
+
assert "max depth" in result
|
|
126
|
+
|
|
127
|
+
@pytest.mark.unit
|
|
128
|
+
def test_recurses_into_dependencies(self, provider):
|
|
129
|
+
"""Critical path: recurses into dependencies."""
|
|
130
|
+
# Set up package with dependency
|
|
131
|
+
provider.packages["parent"] = PackageInfo(name="parent", latest_version="1.0.0")
|
|
132
|
+
provider.packages["child"] = PackageInfo(name="child", latest_version="2.0.0")
|
|
133
|
+
provider.dependencies["parent:1.0.0"] = [
|
|
134
|
+
Dependency(name="child", version_requirement="^2.0", kind="normal")
|
|
135
|
+
]
|
|
136
|
+
provider.fedora_status["parent"] = FedoraPackageStatus(
|
|
137
|
+
is_packaged=True, versions=["1.0.0"]
|
|
138
|
+
)
|
|
139
|
+
provider.fedora_status["child"] = FedoraPackageStatus(
|
|
140
|
+
is_packaged=True, versions=["2.0.0"]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
tree = build_tree(provider, "parent")
|
|
144
|
+
|
|
145
|
+
# Should have a child
|
|
146
|
+
assert isinstance(tree, Tree)
|
|
147
|
+
assert len(tree.children) > 0
|
|
148
|
+
|
|
149
|
+
@pytest.mark.unit
|
|
150
|
+
def test_uses_provided_version(self, provider):
|
|
151
|
+
"""Good path: uses provided version instead of latest."""
|
|
152
|
+
provider.packages["pkg"] = PackageInfo(name="pkg", latest_version="2.0.0")
|
|
153
|
+
provider.fedora_status["pkg"] = FedoraPackageStatus(
|
|
154
|
+
is_packaged=True, versions=["1.0.0"]
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
tree = build_tree(provider, "pkg", version="1.0.0")
|
|
158
|
+
|
|
159
|
+
assert isinstance(tree, Tree)
|
|
160
|
+
label = str(tree.label)
|
|
161
|
+
assert "1.0.0" in label
|
|
162
|
+
|
|
163
|
+
@pytest.mark.unit
|
|
164
|
+
def test_includes_license_in_label(self, provider):
|
|
165
|
+
"""Good path: license is shown in tree label."""
|
|
166
|
+
provider.packages["licensed-pkg"] = PackageInfo(
|
|
167
|
+
name="licensed-pkg", latest_version="1.0.0", license="MIT"
|
|
168
|
+
)
|
|
169
|
+
provider.fedora_status["licensed-pkg"] = FedoraPackageStatus(
|
|
170
|
+
is_packaged=True, versions=["1.0.0"]
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
tree = build_tree(provider, "licensed-pkg")
|
|
174
|
+
|
|
175
|
+
assert isinstance(tree, Tree)
|
|
176
|
+
label = str(tree.label)
|
|
177
|
+
assert "MIT" in label
|
|
178
|
+
|
|
179
|
+
@pytest.mark.unit
|
|
180
|
+
def test_no_license_marker_when_no_license(self, provider):
|
|
181
|
+
"""Good path: no license marker when package has no license."""
|
|
182
|
+
tree = build_tree(provider, "root")
|
|
183
|
+
|
|
184
|
+
assert isinstance(tree, Tree)
|
|
185
|
+
label = str(tree.label)
|
|
186
|
+
# License info from mock_package_info is None, so no license marker
|
|
187
|
+
assert "(None)" not in label
|
|
188
|
+
|
|
189
|
+
@pytest.mark.unit
|
|
190
|
+
def test_updates_progress_tracker(self, provider):
|
|
191
|
+
"""Good path: updates progress tracker when provided."""
|
|
192
|
+
tracker = MagicMock()
|
|
193
|
+
provider.packages["pkg"] = PackageInfo(name="pkg", latest_version="1.0.0")
|
|
194
|
+
provider.fedora_status["pkg"] = FedoraPackageStatus(
|
|
195
|
+
is_packaged=True, versions=["1.0.0"]
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
build_tree(provider, "pkg", tracker=tracker)
|
|
199
|
+
|
|
200
|
+
tracker.update.assert_called()
|
|
201
|
+
|
|
202
|
+
@pytest.mark.unit
|
|
203
|
+
def test_excludes_dependencies_matching_pattern(self, provider):
|
|
204
|
+
"""Good path: excludes dependencies matching glob patterns."""
|
|
205
|
+
# Set up package with dependencies
|
|
206
|
+
provider.packages["parent"] = PackageInfo(name="parent", latest_version="1.0.0")
|
|
207
|
+
provider.packages["child"] = PackageInfo(name="child", latest_version="2.0.0")
|
|
208
|
+
provider.packages["windows-sys"] = PackageInfo(
|
|
209
|
+
name="windows-sys", latest_version="0.52.0"
|
|
210
|
+
)
|
|
211
|
+
provider.dependencies["parent:1.0.0"] = [
|
|
212
|
+
Dependency(name="child", version_requirement="^2.0", kind="normal"),
|
|
213
|
+
Dependency(name="windows-sys", version_requirement="^0.52", kind="normal"),
|
|
214
|
+
]
|
|
215
|
+
provider.fedora_status["parent"] = FedoraPackageStatus(
|
|
216
|
+
is_packaged=True, versions=["1.0.0"]
|
|
217
|
+
)
|
|
218
|
+
provider.fedora_status["child"] = FedoraPackageStatus(
|
|
219
|
+
is_packaged=True, versions=["2.0.0"]
|
|
220
|
+
)
|
|
221
|
+
provider.fedora_status["windows-sys"] = FedoraPackageStatus(is_packaged=False)
|
|
222
|
+
|
|
223
|
+
tree = build_tree(provider, "parent", exclude_patterns=["windows*"])
|
|
224
|
+
|
|
225
|
+
# Should only have child, not windows-sys
|
|
226
|
+
assert isinstance(tree, Tree)
|
|
227
|
+
assert len(tree.children) == 1
|
|
228
|
+
child_label = str(tree.children[0].label)
|
|
229
|
+
assert "child" in child_label
|
|
230
|
+
# windows-sys should be excluded
|
|
231
|
+
for child in tree.children:
|
|
232
|
+
label = str(child.label) if hasattr(child, "label") else str(child)
|
|
233
|
+
assert "windows" not in label
|
|
234
|
+
|
|
235
|
+
@pytest.mark.unit
|
|
236
|
+
def test_excludes_multiple_patterns(self, provider):
|
|
237
|
+
"""Good path: excludes dependencies matching multiple glob patterns."""
|
|
238
|
+
provider.packages["parent"] = PackageInfo(name="parent", latest_version="1.0.0")
|
|
239
|
+
provider.packages["good-dep"] = PackageInfo(
|
|
240
|
+
name="good-dep", latest_version="1.0.0"
|
|
241
|
+
)
|
|
242
|
+
provider.packages["win-dep"] = PackageInfo(
|
|
243
|
+
name="win-dep", latest_version="1.0.0"
|
|
244
|
+
)
|
|
245
|
+
provider.packages["macos-dep"] = PackageInfo(
|
|
246
|
+
name="macos-dep", latest_version="1.0.0"
|
|
247
|
+
)
|
|
248
|
+
provider.dependencies["parent:1.0.0"] = [
|
|
249
|
+
Dependency(name="good-dep", version_requirement="^1.0", kind="normal"),
|
|
250
|
+
Dependency(name="win-dep", version_requirement="^1.0", kind="normal"),
|
|
251
|
+
Dependency(name="macos-dep", version_requirement="^1.0", kind="normal"),
|
|
252
|
+
]
|
|
253
|
+
provider.fedora_status["parent"] = FedoraPackageStatus(
|
|
254
|
+
is_packaged=True, versions=["1.0.0"]
|
|
255
|
+
)
|
|
256
|
+
provider.fedora_status["good-dep"] = FedoraPackageStatus(
|
|
257
|
+
is_packaged=True, versions=["1.0.0"]
|
|
258
|
+
)
|
|
259
|
+
provider.fedora_status["win-dep"] = FedoraPackageStatus(is_packaged=False)
|
|
260
|
+
provider.fedora_status["macos-dep"] = FedoraPackageStatus(is_packaged=False)
|
|
261
|
+
|
|
262
|
+
tree = build_tree(provider, "parent", exclude_patterns=["win*", "macos*"])
|
|
263
|
+
|
|
264
|
+
# Should only have good-dep
|
|
265
|
+
assert isinstance(tree, Tree)
|
|
266
|
+
assert len(tree.children) == 1
|
|
267
|
+
child_label = str(tree.children[0].label)
|
|
268
|
+
assert "good-dep" in child_label
|
|
269
|
+
|
|
270
|
+
@pytest.mark.unit
|
|
271
|
+
def test_exclude_patterns_applied_recursively(self, provider):
|
|
272
|
+
"""Good path: exclude patterns are applied at all depth levels."""
|
|
273
|
+
provider.packages["root"] = PackageInfo(name="root", latest_version="1.0.0")
|
|
274
|
+
provider.packages["child"] = PackageInfo(name="child", latest_version="1.0.0")
|
|
275
|
+
provider.packages["windows-inner"] = PackageInfo(
|
|
276
|
+
name="windows-inner", latest_version="1.0.0"
|
|
277
|
+
)
|
|
278
|
+
provider.dependencies["root:1.0.0"] = [
|
|
279
|
+
Dependency(name="child", version_requirement="^1.0", kind="normal"),
|
|
280
|
+
]
|
|
281
|
+
provider.dependencies["child:1.0.0"] = [
|
|
282
|
+
Dependency(name="windows-inner", version_requirement="^1.0", kind="normal"),
|
|
283
|
+
]
|
|
284
|
+
provider.fedora_status["root"] = FedoraPackageStatus(
|
|
285
|
+
is_packaged=True, versions=["1.0.0"]
|
|
286
|
+
)
|
|
287
|
+
provider.fedora_status["child"] = FedoraPackageStatus(
|
|
288
|
+
is_packaged=True, versions=["1.0.0"]
|
|
289
|
+
)
|
|
290
|
+
provider.fedora_status["windows-inner"] = FedoraPackageStatus(is_packaged=False)
|
|
291
|
+
|
|
292
|
+
tree = build_tree(provider, "root", exclude_patterns=["windows*"])
|
|
293
|
+
|
|
294
|
+
# root -> child (no windows-inner)
|
|
295
|
+
assert isinstance(tree, Tree)
|
|
296
|
+
assert len(tree.children) == 1
|
|
297
|
+
child_tree = tree.children[0]
|
|
298
|
+
assert isinstance(child_tree, Tree)
|
|
299
|
+
# child should have no children (windows-inner was filtered)
|
|
300
|
+
assert len(child_tree.children) == 0
|
|
301
|
+
|
|
302
|
+
@pytest.mark.unit
|
|
303
|
+
def test_no_exclusion_when_patterns_none(self, provider):
|
|
304
|
+
"""Good path: no exclusion when patterns is None."""
|
|
305
|
+
provider.packages["parent"] = PackageInfo(name="parent", latest_version="1.0.0")
|
|
306
|
+
provider.packages["child"] = PackageInfo(name="child", latest_version="1.0.0")
|
|
307
|
+
provider.dependencies["parent:1.0.0"] = [
|
|
308
|
+
Dependency(name="child", version_requirement="^1.0", kind="normal"),
|
|
309
|
+
]
|
|
310
|
+
provider.fedora_status["parent"] = FedoraPackageStatus(
|
|
311
|
+
is_packaged=True, versions=["1.0.0"]
|
|
312
|
+
)
|
|
313
|
+
provider.fedora_status["child"] = FedoraPackageStatus(
|
|
314
|
+
is_packaged=True, versions=["1.0.0"]
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
tree = build_tree(provider, "parent", exclude_patterns=None)
|
|
318
|
+
|
|
319
|
+
assert isinstance(tree, Tree)
|
|
320
|
+
assert len(tree.children) == 1
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class TestComputeStatsFromVisited:
|
|
324
|
+
"""Tests for _compute_stats_from_visited function."""
|
|
325
|
+
|
|
326
|
+
@pytest.mark.unit
|
|
327
|
+
def test_returns_tree_stats_model(self):
|
|
328
|
+
"""Good path: returns TreeStats model."""
|
|
329
|
+
visited = {"root": (True, "1.0.0", False)}
|
|
330
|
+
|
|
331
|
+
stats = _compute_stats_from_visited(visited)
|
|
332
|
+
|
|
333
|
+
assert isinstance(stats, TreeStats)
|
|
334
|
+
|
|
335
|
+
@pytest.mark.unit
|
|
336
|
+
def test_counts_packaged(self):
|
|
337
|
+
"""Good path: counts packaged packages."""
|
|
338
|
+
visited = {"root": (True, "1.0.0", False)}
|
|
339
|
+
|
|
340
|
+
stats = _compute_stats_from_visited(visited)
|
|
341
|
+
|
|
342
|
+
assert stats.packaged == 1
|
|
343
|
+
|
|
344
|
+
@pytest.mark.unit
|
|
345
|
+
def test_counts_missing(self):
|
|
346
|
+
"""Good path: counts missing packages."""
|
|
347
|
+
visited = {"root": (False, "1.0.0", False)}
|
|
348
|
+
|
|
349
|
+
stats = _compute_stats_from_visited(visited)
|
|
350
|
+
|
|
351
|
+
assert stats.missing == 1
|
|
352
|
+
|
|
353
|
+
@pytest.mark.unit
|
|
354
|
+
def test_collects_missing_list(self):
|
|
355
|
+
"""Good path: collects list of missing packages."""
|
|
356
|
+
visited = {"missing-pkg": (False, "1.0.0", False)}
|
|
357
|
+
|
|
358
|
+
stats = _compute_stats_from_visited(visited)
|
|
359
|
+
|
|
360
|
+
assert len(stats.missing_list) == 1
|
|
361
|
+
assert "missing-pkg" in stats.missing_list
|
|
362
|
+
|
|
363
|
+
@pytest.mark.unit
|
|
364
|
+
def test_collects_packaged_list(self):
|
|
365
|
+
"""Good path: collects list of packaged packages."""
|
|
366
|
+
visited = {"pkg": (True, "1.0.0", False)}
|
|
367
|
+
|
|
368
|
+
stats = _compute_stats_from_visited(visited)
|
|
369
|
+
|
|
370
|
+
assert len(stats.packaged_list) == 1
|
|
371
|
+
assert "pkg" in stats.packaged_list
|
|
372
|
+
|
|
373
|
+
@pytest.mark.unit
|
|
374
|
+
def test_counts_not_found_as_missing(self):
|
|
375
|
+
"""Good path: counts not-found packages (version=None) as missing."""
|
|
376
|
+
visited = {"unknown": (False, None, False)}
|
|
377
|
+
|
|
378
|
+
stats = _compute_stats_from_visited(visited)
|
|
379
|
+
|
|
380
|
+
assert stats.missing == 1
|
|
381
|
+
assert "unknown" in stats.missing_list
|
|
382
|
+
|
|
383
|
+
@pytest.mark.unit
|
|
384
|
+
def test_multiple_packages(self):
|
|
385
|
+
"""Critical path: counts all packages from visited dict."""
|
|
386
|
+
visited = {
|
|
387
|
+
"root": (True, "1.0.0", False),
|
|
388
|
+
"child1": (True, "1.0.0", False),
|
|
389
|
+
"child2": (False, "1.0.0", False),
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
stats = _compute_stats_from_visited(visited)
|
|
393
|
+
|
|
394
|
+
assert stats.total == 3
|
|
395
|
+
assert stats.packaged == 2
|
|
396
|
+
assert stats.missing == 1
|
|
397
|
+
|
|
398
|
+
@pytest.mark.unit
|
|
399
|
+
def test_empty_visited(self):
|
|
400
|
+
"""Good path: handles empty visited dict."""
|
|
401
|
+
stats = _compute_stats_from_visited({})
|
|
402
|
+
|
|
403
|
+
assert stats.total == 0
|
|
404
|
+
assert stats.packaged == 0
|
|
405
|
+
assert stats.missing == 0
|
|
406
|
+
assert stats.missing_list == []
|
|
407
|
+
assert stats.packaged_list == []
|
|
408
|
+
|
|
409
|
+
@pytest.mark.unit
|
|
410
|
+
def test_has_dev_build_stats(self):
|
|
411
|
+
"""Good path: stats model has dev/build dependency stats (zeroed by default)."""
|
|
412
|
+
visited = {"pkg": (True, "1.0.0", False)}
|
|
413
|
+
|
|
414
|
+
stats = _compute_stats_from_visited(visited)
|
|
415
|
+
|
|
416
|
+
assert hasattr(stats, "dev_total")
|
|
417
|
+
assert hasattr(stats, "dev_packaged")
|
|
418
|
+
assert hasattr(stats, "dev_missing")
|
|
419
|
+
assert hasattr(stats, "build_total")
|
|
420
|
+
assert hasattr(stats, "build_packaged")
|
|
421
|
+
assert hasattr(stats, "build_missing")
|
|
422
|
+
assert stats.dev_total == 0
|
|
423
|
+
assert stats.build_total == 0
|
|
424
|
+
|
|
425
|
+
@pytest.mark.unit
|
|
426
|
+
def test_optional_dependency_tracking(self):
|
|
427
|
+
"""Good path: tracks optional dependency statistics."""
|
|
428
|
+
visited = {
|
|
429
|
+
"required": (True, "1.0.0", False),
|
|
430
|
+
"opt-packaged": (True, "2.0.0", True),
|
|
431
|
+
"opt-missing": (False, "3.0.0", True),
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
stats = _compute_stats_from_visited(visited)
|
|
435
|
+
|
|
436
|
+
assert stats.total == 3
|
|
437
|
+
assert stats.optional_total == 2
|
|
438
|
+
assert stats.optional_packaged == 1
|
|
439
|
+
assert stats.optional_missing == 1
|
|
440
|
+
assert "opt-missing" in stats.optional_missing_list
|