- Expand blender_downloader with better error handling and platform support - Add test_main.py for main module tests - Expand test coverage for introspect and generate_stubs - Remove reportUnknownMemberType suppression from conformance config - Exclude conformance/ from ruff linting (docs examples have E402) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
166 lines
6.1 KiB
Python
166 lines
6.1 KiB
Python
"""Tests for the Blender downloader module."""
|
|
|
|
import io
|
|
import tarfile
|
|
import tempfile
|
|
import unittest
|
|
import zipfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from blender_downloader import (
|
|
extract_tar_archive,
|
|
extract_zip_archive,
|
|
get_archive_extension,
|
|
get_blender_executable,
|
|
get_download_url,
|
|
get_extracted_dir_name,
|
|
)
|
|
|
|
|
|
class TestGetDownloadUrl(unittest.TestCase):
|
|
def test_builds_correct_url(self) -> None:
|
|
with patch("blender_downloader.get_platform_suffix", return_value="linux-x64"):
|
|
with patch(
|
|
"blender_downloader.get_archive_extension", return_value=".tar.xz"
|
|
):
|
|
url = get_download_url("5.0.1")
|
|
self.assertEqual(
|
|
url,
|
|
"https://download.blender.org/release/Blender5.0/blender-5.0.1-linux-x64.tar.xz",
|
|
)
|
|
|
|
def test_major_minor_extraction(self) -> None:
|
|
with patch("blender_downloader.get_platform_suffix", return_value="linux-x64"):
|
|
with patch(
|
|
"blender_downloader.get_archive_extension", return_value=".tar.xz"
|
|
):
|
|
url = get_download_url("4.3.2")
|
|
self.assertEqual(
|
|
url,
|
|
"https://download.blender.org/release/Blender4.3/blender-4.3.2-linux-x64.tar.xz",
|
|
)
|
|
|
|
|
|
class TestGetExtractedDirName(unittest.TestCase):
|
|
def test_dir_name_uses_platform_suffix(self) -> None:
|
|
with patch(
|
|
"blender_downloader.get_platform_suffix", return_value="windows-x64"
|
|
):
|
|
name = get_extracted_dir_name("5.0.1")
|
|
self.assertEqual(name, "blender-5.0.1-windows-x64")
|
|
|
|
|
|
class TestGetArchiveExtension(unittest.TestCase):
|
|
def test_windows_extension(self) -> None:
|
|
with patch("blender_downloader.platform.system", return_value="Windows"):
|
|
self.assertEqual(get_archive_extension(), ".zip")
|
|
|
|
def test_non_windows_extension(self) -> None:
|
|
with patch("blender_downloader.platform.system", return_value="Linux"):
|
|
self.assertEqual(get_archive_extension(), ".tar.xz")
|
|
|
|
|
|
class TestGetBlenderExecutable(unittest.TestCase):
|
|
def _touch_executable(self, path: Path) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text("")
|
|
|
|
def test_cache_match_is_exact_for_major_minor(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
downloads = Path(td)
|
|
target_dir = downloads / "blender-5.1.2-linux-x64"
|
|
self._touch_executable(target_dir / "blender")
|
|
# Must not match when requesting 5.1
|
|
wrong_dir = downloads / "blender-5.10.1-linux-x64"
|
|
self._touch_executable(wrong_dir / "blender")
|
|
|
|
with patch("blender_downloader.DOWNLOADS_DIR", downloads):
|
|
with patch(
|
|
"blender_downloader.get_platform_suffix", return_value="linux-x64"
|
|
):
|
|
with patch(
|
|
"blender_downloader.platform.system", return_value="Linux"
|
|
):
|
|
with patch("blender_downloader.find_latest_patch") as latest:
|
|
executable = get_blender_executable("5.1")
|
|
|
|
self.assertEqual(executable, target_dir / "blender")
|
|
latest.assert_not_called()
|
|
|
|
def test_uses_windows_executable_name(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
downloads = Path(td)
|
|
target_dir = downloads / "blender-5.0.3-windows-x64"
|
|
self._touch_executable(target_dir / "blender.exe")
|
|
|
|
with patch("blender_downloader.DOWNLOADS_DIR", downloads):
|
|
with patch(
|
|
"blender_downloader.get_platform_suffix", return_value="windows-x64"
|
|
):
|
|
with patch(
|
|
"blender_downloader.platform.system", return_value="Windows"
|
|
):
|
|
executable = get_blender_executable("5.0")
|
|
|
|
self.assertEqual(executable, target_dir / "blender.exe")
|
|
|
|
def test_uses_macos_bundle_executable_path(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
downloads = Path(td)
|
|
target_dir = downloads / "blender-5.1.1-macos-arm64"
|
|
self._touch_executable(
|
|
target_dir / "Blender.app" / "Contents" / "MacOS" / "Blender"
|
|
)
|
|
|
|
with patch("blender_downloader.DOWNLOADS_DIR", downloads):
|
|
with patch(
|
|
"blender_downloader.get_platform_suffix", return_value="macos-arm64"
|
|
):
|
|
with patch(
|
|
"blender_downloader.platform.system", return_value="Darwin"
|
|
):
|
|
executable = get_blender_executable("5.1")
|
|
|
|
self.assertEqual(
|
|
executable,
|
|
target_dir / "Blender.app" / "Contents" / "MacOS" / "Blender",
|
|
)
|
|
|
|
|
|
class TestSafeExtraction(unittest.TestCase):
|
|
def test_tar_extraction_blocks_path_traversal(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
temp_dir = Path(td)
|
|
archive = temp_dir / "bad.tar.xz"
|
|
out_dir = temp_dir / "extract"
|
|
out_dir.mkdir()
|
|
|
|
info = tarfile.TarInfo("../evil.txt")
|
|
data = b"malicious"
|
|
info.size = len(data)
|
|
with tarfile.open(archive, "w:xz") as tar:
|
|
tar.addfile(info, io.BytesIO(data))
|
|
|
|
with self.assertRaises(RuntimeError):
|
|
extract_tar_archive(archive, out_dir)
|
|
self.assertFalse((temp_dir / "evil.txt").exists())
|
|
|
|
def test_zip_extraction_blocks_path_traversal(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
temp_dir = Path(td)
|
|
archive = temp_dir / "bad.zip"
|
|
out_dir = temp_dir / "extract"
|
|
out_dir.mkdir()
|
|
|
|
with zipfile.ZipFile(archive, "w") as zf:
|
|
zf.writestr("../evil.txt", "malicious")
|
|
|
|
with self.assertRaises(RuntimeError):
|
|
extract_zip_archive(archive, out_dir)
|
|
self.assertFalse((temp_dir / "evil.txt").exists())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|