Coverage for tests/test_deploy_utils.py: 100%
52 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
1from pathlib import Path 1abcdefg
3import pytest 1abcdefg
5from fastapi_cloud_cli.api import Deployment, DeploymentStatus 1abcdefg
6from fastapi_cloud_cli.commands.deploy.archive import ( 1abcdefg
7 _get_large_files,
8 _should_exclude_entry,
9 validate_app_directory,
10)
13def _create_file(path: Path, size_bytes: int) -> None: 1abcdefg
14 path.parent.mkdir(parents=True, exist_ok=True) 1oCphqDrisEtjuFvkwGxlyHzmAIBn
15 with open(path, "wb") as f: 1oCphqDrisEtjuFvkwGxlyHzmAIBn
16 if size_bytes > 0: 1oCphqDrisEtjuFvkwGxlyHzmAIBn
17 f.seek(size_bytes - 1) 1oCphqDrisEtjuFvkwGxlyHzmAIBn
18 f.write(b"\0") 1oCphqDrisEtjuFvkwGxlyHzmAIBn
21@pytest.mark.parametrize( 1abcdefg
22 "path",
23 [
24 Path("/project/.venv/lib/python3.11/site-packages/some_package"),
25 Path("/project/src/__pycache__/module.cpython-311.pyc"),
26 Path("/project/.mypy_cache/3.11/module.meta.json"),
27 Path("/project/.pytest_cache/v/cache/lastfailed"),
28 Path("/project/src/module.pyc"),
29 Path("/project/src/subdir/another/module.pyc"),
30 Path("/project/subproject/.venv/lib/python3.11/site-packages"),
31 Path("/project/.venv/lib/__pycache__/module.pyc"),
32 Path(".venv"),
33 Path("__pycache__"),
34 Path("module.pyc"),
35 Path("/project/.env"),
36 Path("/project/.env.local"),
37 Path("/project/.env.production"),
38 Path(".env"),
39 Path(".env.development"),
40 ],
41)
42def test_excludes_paths(path: Path) -> None: 1abcdefg
43 """Should exclude paths that match exclusion criteria."""
44 assert _should_exclude_entry(path) is True 1XYZ0123
47@pytest.mark.parametrize( 1abcdefg
48 "path",
49 [
50 Path("/project/src/module.py"),
51 Path("/project/src/utils"),
52 Path("/project/src/my_cache_utils.py"),
53 Path("/project/venv/lib/python3.11/site-packages"), # no leading dot
54 Path("/project/pycache/some_file.py"), # no underscores
55 Path("/project/src/module.pyx"), # similar to .pyc but different
56 Path("/project/config.json"),
57 Path("/project/README.md"),
58 Path("/project/.envrc"), # not a .env file
59 Path("/project/env.py"), # not a .env file
60 ],
61)
62def test_includes_paths(path: Path) -> None: 1abcdefg
63 """Should not exclude paths that don't match exclusion criteria."""
64 assert _should_exclude_entry(path) is False 1456789!
67@pytest.mark.parametrize( 1abcdefg
68 "status,expected",
69 [
70 (DeploymentStatus.waiting_upload, "Awaiting Upload"),
71 (DeploymentStatus.ready_for_build, "Build Queued"),
72 (DeploymentStatus.build_skipped_filtered, "Build Skipped (Filtered)"),
73 (DeploymentStatus.build_skipped_superseded, "Build Skipped (Superseded)"),
74 (DeploymentStatus.building, "Building"),
75 (DeploymentStatus.extracting, "Extracting Upload"),
76 (DeploymentStatus.extracting_failed, "Extraction Failed"),
77 (DeploymentStatus.building_image, "Building Image"),
78 (DeploymentStatus.building_image_failed, "Build Failed"),
79 (DeploymentStatus.building_image_failed_timeout, "Build Timed Out"),
80 (DeploymentStatus.deploying, "Deploying Image"),
81 (DeploymentStatus.deploying_skipped, "Deployment Skipped"),
82 (DeploymentStatus.deploying_failed, "Deployment Failed"),
83 (DeploymentStatus.verifying, "Verifying Readiness"),
84 (DeploymentStatus.verifying_failed, "Verification Failed"),
85 (DeploymentStatus.verification_failed_oom, "Verification Failed (OOM)"),
86 (DeploymentStatus.verifying_skipped, "Verification Skipped"),
87 (DeploymentStatus.success, "Ready"),
88 (DeploymentStatus.degraded_oom, "Degraded (OOM)"),
89 (DeploymentStatus.degraded, "Degraded"),
90 (DeploymentStatus.expired, "Expired"),
91 (DeploymentStatus.failed, "Failed"),
92 (DeploymentStatus.unknown, "Unknown"),
93 ],
94)
95def test_deployment_status_to_human_readable( 1abcdefg
96 status: DeploymentStatus, expected: str
97) -> None:
98 """Should convert deployment status to human readable format."""
99 assert DeploymentStatus.to_human_readable(status) == expected 1#$%'()*
102@pytest.mark.parametrize("status", list(DeploymentStatus)) 1abcdefg
103def test_every_deployment_status_has_human_readable( 1abcdefg
104 status: DeploymentStatus,
105) -> None:
106 assert DeploymentStatus.to_human_readable(status) 1+,-./:;
109def test_deployment_model_accepts_unknown_status() -> None: 1abcdefg
110 deployment = Deployment.model_validate( 1QRSTUVW
111 {
112 "id": "dep-1",
113 "app_id": "app-1",
114 "slug": "my-app",
115 "status": "some_new_status",
116 "created_at": "2026-01-01T00:00:00Z",
117 }
118 )
120 assert deployment.status is DeploymentStatus.unknown 1QRSTUVW
123@pytest.mark.parametrize( 1abcdefg
124 "value,expected",
125 [
126 (None, None),
127 ("", None),
128 (" ", None),
129 ("src", "src"),
130 ("src/app", "src/app"),
131 (" src/app ", "src/app"),
132 ("my-app", "my-app"),
133 ("my_app", "my_app"),
134 ("my.app", "my.app"),
135 ("src/my app", "src/my app"),
136 ("a/b/c", "a/b/c"),
137 ],
138)
139def test_validate_app_directory_valid(value: str | None, expected: str | None) -> None: 1abcdefg
140 """Should accept valid directory values and normalize them."""
141 assert validate_app_directory(value) == expected 1=?@[]^_
144@pytest.mark.parametrize( 1abcdefg
145 "value,expected_message",
146 [
147 ("~/src", "cannot start with '~'"),
148 ("/absolute/path", "must be a relative path, not absolute"),
149 ("src/../etc", "cannot contain '..' path segments"),
150 ("..", "cannot contain '..' path segments"),
151 ("src/../../etc", "cannot contain '..' path segments"),
152 (
153 "src/@app",
154 "contains invalid characters (allowed: letters, numbers, space, / . _ -)",
155 ),
156 (
157 "src/$app",
158 "contains invalid characters (allowed: letters, numbers, space, / . _ -)",
159 ),
160 (
161 "src/app!",
162 "contains invalid characters (allowed: letters, numbers, space, / . _ -)",
163 ),
164 (
165 "src/app#1",
166 "contains invalid characters (allowed: letters, numbers, space, / . _ -)",
167 ),
168 ],
169)
170def test_validate_app_directory_invalid(value: str, expected_message: str) -> None: 1abcdefg
171 """Should reject invalid directory values with descriptive errors."""
172 with pytest.raises(ValueError) as exc_info: 1JKLMNOP
173 validate_app_directory(value) 1JKLMNOP
175 assert str(exc_info.value) == expected_message 1JKLMNOP
178def test_get_large_files_no_files_above_threshold(tmp_path: Path) -> None: 1abcdefg
179 """Should not return files smaller than the threshold."""
180 _create_file(tmp_path / "small.bin", 512 * 1024) # 0.5 MB 1CDEFGHI
182 assert _get_large_files(tmp_path, threshold_mb=1) == [] 1CDEFGHI
185def test_get_large_files_returns_files_at_or_above_threshold(tmp_path: Path) -> None: 1abcdefg
186 """Should return files at or above the threshold with sizes and relative paths."""
187 _create_file(tmp_path / "big.bin", 2 * 1024 * 1024) # 2 MB 1hijklmn
188 _create_file(tmp_path / "subdir" / "huge.bin", 5 * 1024 * 1024) # 5 MB 1hijklmn
189 _create_file(tmp_path / "small.bin", 100 * 1024) # 0.1 MB 1hijklmn
191 result = _get_large_files(tmp_path, threshold_mb=1) 1hijklmn
193 assert result == [ 1hijklmn
194 (Path("subdir") / "huge.bin", 5 * 1024 * 1024),
195 (Path("big.bin"), 2 * 1024 * 1024),
196 ]
199def test_get_large_files_excludes_default_exclusions(tmp_path: Path) -> None: 1abcdefg
200 """Should not count files in excluded directories like .venv or __pycache__."""
201 _create_file(tmp_path / ".venv" / "lib" / "huge.so", 5 * 1024 * 1024) 1oqsuwyA
202 _create_file(tmp_path / "__pycache__" / "module.cpython-311.pyc", 5 * 1024 * 1024) 1oqsuwyA
203 _create_file(tmp_path / "main.py", 5 * 1024 * 1024) 1oqsuwyA
205 assert _get_large_files(tmp_path, threshold_mb=1) == [ 1oqsuwyA
206 (Path("main.py"), 5 * 1024 * 1024)
207 ]
210def test_get_large_files_respects_fastapicloudignore(tmp_path: Path) -> None: 1abcdefg
211 """Should not count files matching .fastapicloudignore patterns."""
212 _create_file(tmp_path / "data" / "huge.bin", 5 * 1024 * 1024) 1prtvxzB
213 _create_file(tmp_path / "main.bin", 5 * 1024 * 1024) 1prtvxzB
214 (tmp_path / ".fastapicloudignore").write_text("data/\n") 1prtvxzB
216 assert _get_large_files(tmp_path, threshold_mb=1) == [ 1prtvxzB
217 (Path("main.bin"), 5 * 1024 * 1024)
218 ]