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

1from pathlib import Path 1abcdefg

2 

3import pytest 1abcdefg

4 

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) 

11 

12 

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

19 

20 

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

45 

46 

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!

65 

66 

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#$%'()*

100 

101 

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+,-./:;

107 

108 

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 ) 

119 

120 assert deployment.status is DeploymentStatus.unknown 1QRSTUVW

121 

122 

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=?@[]^_

142 

143 

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

174 

175 assert str(exc_info.value) == expected_message 1JKLMNOP

176 

177 

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

181 

182 assert _get_large_files(tmp_path, threshold_mb=1) == [] 1CDEFGHI

183 

184 

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

190 

191 result = _get_large_files(tmp_path, threshold_mb=1) 1hijklmn

192 

193 assert result == [ 1hijklmn

194 (Path("subdir") / "huge.bin", 5 * 1024 * 1024), 

195 (Path("big.bin"), 2 * 1024 * 1024), 

196 ] 

197 

198 

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

204 

205 assert _get_large_files(tmp_path, threshold_mb=1) == [ 1oqsuwyA

206 (Path("main.py"), 5 * 1024 * 1024) 

207 ] 

208 

209 

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

215 

216 assert _get_large_files(tmp_path, threshold_mb=1) == [ 1prtvxzB

217 (Path("main.bin"), 5 * 1024 * 1024) 

218 ]