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:
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user