blender-python-stubs/tests/test_blender_downloader.py
Joseph HENRY 673b9e54bc Improve tests, downloader, and project config
- 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>
2026-04-02 11:49:53 +02:00

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()