|
1 | 1 | """Tests for cdl_slides.marp_cli.""" |
2 | 2 |
|
3 | 3 | import platform |
| 4 | +import stat |
| 5 | +import subprocess |
| 6 | +import urllib.request |
| 7 | +from urllib.request import urlopen |
4 | 8 |
|
5 | 9 | import click.testing |
6 | 10 |
|
7 | 11 | from cdl_slides.cli import main |
8 | 12 | from cdl_slides.marp_cli import ( |
| 13 | + _PLATFORM_MAP, |
| 14 | + _RELEASE_BASE, |
9 | 15 | MARP_CLI_VERSION, |
10 | 16 | _get_cache_dir, |
| 17 | + _get_cached_marp_path, |
11 | 18 | _get_platform_key, |
12 | 19 | get_marp_version_info, |
13 | 20 | resolve_marp_cli, |
@@ -148,3 +155,148 @@ def test_setup_command_idempotent_on_repeated_calls(self): |
148 | 155 | # Both calls should succeed (exit code 0 or 1) |
149 | 156 | assert result1.exit_code in (0, 1) |
150 | 157 | assert result2.exit_code in (0, 1) |
| 158 | + |
| 159 | + |
| 160 | +class TestInstallerDownloadUrl: |
| 161 | + """Test the download URL construction for Marp CLI installer.""" |
| 162 | + |
| 163 | + def test_release_base_url_format(self): |
| 164 | + """Verify the release base URL is correctly formatted.""" |
| 165 | + assert _RELEASE_BASE.startswith("https://github.com/marp-team/marp-cli/releases/download/") |
| 166 | + assert MARP_CLI_VERSION in _RELEASE_BASE |
| 167 | + |
| 168 | + def test_download_url_is_accessible(self): |
| 169 | + """Verify the download URL for current platform is accessible (HEAD request).""" |
| 170 | + platform_key = _get_platform_key() |
| 171 | + assert platform_key is not None, "Platform should be supported" |
| 172 | + |
| 173 | + os_name, archive_ext = _PLATFORM_MAP[platform_key] |
| 174 | + filename = f"marp-cli-{MARP_CLI_VERSION}-{os_name}.{archive_ext}" |
| 175 | + url = f"{_RELEASE_BASE}/{filename}" |
| 176 | + |
| 177 | + # Use HEAD request to verify URL exists without downloading |
| 178 | + |
| 179 | + req = urllib.request.Request(url, method="HEAD") |
| 180 | + try: |
| 181 | + response = urlopen(req, timeout=30) |
| 182 | + assert response.status == 200, f"Expected 200, got {response.status}" |
| 183 | + except Exception as e: |
| 184 | + # If HEAD fails, try GET with small range |
| 185 | + req = urllib.request.Request(url) |
| 186 | + req.add_header("Range", "bytes=0-0") |
| 187 | + response = urlopen(req, timeout=30) |
| 188 | + assert response.status in (200, 206), f"URL not accessible: {e}" |
| 189 | + |
| 190 | + |
| 191 | +class TestInstallerPlatformMapping: |
| 192 | + """Test platform detection and mapping for the installer.""" |
| 193 | + |
| 194 | + def test_all_platform_keys_have_valid_structure(self): |
| 195 | + """Verify all platform map entries have correct structure.""" |
| 196 | + for key, value in _PLATFORM_MAP.items(): |
| 197 | + assert len(key) == 2, f"Key {key} should be (system, machine) tuple" |
| 198 | + assert len(value) == 2, f"Value {value} should be (os_name, archive_ext) tuple" |
| 199 | + os_name, archive_ext = value |
| 200 | + assert os_name in ("mac", "linux", "win"), f"Unknown OS: {os_name}" |
| 201 | + assert archive_ext in ("tar.gz", "zip"), f"Unknown archive: {archive_ext}" |
| 202 | + |
| 203 | + def test_current_platform_is_supported(self): |
| 204 | + """Verify the current platform is in the supported list.""" |
| 205 | + key = _get_platform_key() |
| 206 | + assert key is not None, "Current platform should be supported" |
| 207 | + assert key in _PLATFORM_MAP or key[0] in [k[0] for k in _PLATFORM_MAP] |
| 208 | + |
| 209 | + def test_windows_uses_zip(self): |
| 210 | + """Verify Windows platforms use zip archives.""" |
| 211 | + for key, value in _PLATFORM_MAP.items(): |
| 212 | + if key[0] == "Windows": |
| 213 | + assert value[1] == "zip", "Windows should use zip archives" |
| 214 | + |
| 215 | + def test_unix_uses_tar_gz(self): |
| 216 | + """Verify Unix platforms use tar.gz archives.""" |
| 217 | + for key, value in _PLATFORM_MAP.items(): |
| 218 | + if key[0] in ("Darwin", "Linux"): |
| 219 | + assert value[1] == "tar.gz", "Unix should use tar.gz archives" |
| 220 | + |
| 221 | + |
| 222 | +class TestInstallerCachedBinary: |
| 223 | + """Test the cached binary detection and execution.""" |
| 224 | + |
| 225 | + def test_cached_path_in_version_directory(self): |
| 226 | + """Verify cached binary path includes version directory.""" |
| 227 | + cached = _get_cached_marp_path() |
| 228 | + if cached is not None: |
| 229 | + assert MARP_CLI_VERSION in str(cached), "Cached path should include version" |
| 230 | + |
| 231 | + def test_cached_binary_is_executable(self): |
| 232 | + """Verify the cached binary has executable permissions.""" |
| 233 | + cached = _get_cached_marp_path() |
| 234 | + if cached is not None: |
| 235 | + assert cached.exists(), "Cached binary should exist" |
| 236 | + mode = cached.stat().st_mode |
| 237 | + assert mode & stat.S_IXUSR, "Binary should be user-executable" |
| 238 | + |
| 239 | + |
| 240 | +class TestInstallerBinaryExecution: |
| 241 | + """Test that the resolved Marp CLI binary actually works.""" |
| 242 | + |
| 243 | + def test_resolved_binary_executes_version_command(self): |
| 244 | + """Verify the resolved binary can execute --version.""" |
| 245 | + result = resolve_marp_cli() |
| 246 | + assert result is not None, "Should resolve to something" |
| 247 | + |
| 248 | + if isinstance(result, str): |
| 249 | + # Direct binary path |
| 250 | + proc = subprocess.run([result, "--version"], capture_output=True, text=True, timeout=30) |
| 251 | + else: |
| 252 | + # npx command list |
| 253 | + proc = subprocess.run(result + ["--version"], capture_output=True, text=True, timeout=60) |
| 254 | + |
| 255 | + assert proc.returncode == 0, f"--version failed: {proc.stderr}" |
| 256 | + assert "marp" in proc.stdout.lower() or len(proc.stdout) > 0, "Should output version info" |
| 257 | + |
| 258 | + def test_resolved_binary_help_command(self): |
| 259 | + """Verify the resolved binary can execute --help.""" |
| 260 | + result = resolve_marp_cli() |
| 261 | + assert result is not None |
| 262 | + |
| 263 | + if isinstance(result, str): |
| 264 | + proc = subprocess.run([result, "--help"], capture_output=True, text=True, timeout=30) |
| 265 | + else: |
| 266 | + proc = subprocess.run(result + ["--help"], capture_output=True, text=True, timeout=60) |
| 267 | + |
| 268 | + assert proc.returncode == 0, f"--help failed: {proc.stderr}" |
| 269 | + # Help output should mention common options |
| 270 | + output_lower = proc.stdout.lower() |
| 271 | + assert "html" in output_lower or "pdf" in output_lower or "output" in output_lower |
| 272 | + |
| 273 | + |
| 274 | +class TestInstallerCacheDirectory: |
| 275 | + """Test cache directory creation and structure.""" |
| 276 | + |
| 277 | + def test_cache_dir_is_created(self): |
| 278 | + """Verify cache directory is created if it doesn't exist.""" |
| 279 | + cache_dir = _get_cache_dir() |
| 280 | + assert cache_dir.exists(), "Cache directory should be created" |
| 281 | + assert cache_dir.is_dir(), "Cache directory should be a directory" |
| 282 | + |
| 283 | + def test_cache_dir_is_writable(self): |
| 284 | + """Verify cache directory is writable.""" |
| 285 | + cache_dir = _get_cache_dir() |
| 286 | + test_file = cache_dir / ".write_test" |
| 287 | + try: |
| 288 | + test_file.write_text("test") |
| 289 | + assert test_file.exists() |
| 290 | + finally: |
| 291 | + if test_file.exists(): |
| 292 | + test_file.unlink() |
| 293 | + |
| 294 | + def test_cache_dir_contains_version_subdir_after_resolve(self): |
| 295 | + """Verify version subdirectory exists after resolving.""" |
| 296 | + resolve_marp_cli() # Ensure binary is downloaded/cached |
| 297 | + cache_dir = _get_cache_dir() |
| 298 | + version_dir = cache_dir / MARP_CLI_VERSION |
| 299 | + # Only check if we're using cached binary (not system marp or npx) |
| 300 | + info = get_marp_version_info() |
| 301 | + if info["source"] == "cached": |
| 302 | + assert version_dir.exists(), "Version directory should exist for cached binary" |
0 commit comments