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>
This commit is contained in:
2026-04-02 11:49:53 +02:00
co-authored by Claude Opus 4.6
parent 7e1e18d845
commit 673b9e54bc
7 changed files with 420 additions and 44 deletions
+140 -17
View File
@@ -1,9 +1,18 @@
"""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,
)
@@ -11,31 +20,145 @@ from blender_downloader import (
class TestGetDownloadUrl(unittest.TestCase):
def test_builds_correct_url(self) -> None:
url = get_download_url("5.0.1")
self.assertIn("https://download.blender.org/release/Blender5.0/", url)
self.assertIn("blender-5.0.1-", url)
self.assertTrue(url.endswith(get_archive_extension()))
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:
url = get_download_url("4.3.2")
self.assertIn("/Blender4.3/", url)
self.assertIn("blender-4.3.2-", url)
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_linux_dir_name(self) -> None:
name = get_extracted_dir_name("5.0.1")
self.assertEqual(name, "blender-5.0.1-linux-x64")
def test_different_version(self) -> None:
name = get_extracted_dir_name("4.3.2")
self.assertEqual(name, "blender-4.3.2-linux-x64")
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_linux_extension(self) -> None:
ext = get_archive_extension()
self.assertEqual(ext, ".tar.xz")
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__":