Spaces:
Running on Zero
Running on Zero
Download tests/test_config_uploads.py from cpuai/blender-render-studio: direct link, hf CLI and curl.
- Browser
- Download file 9.83 kB
-
https://huggingface.co/spaces/cpuai/blender-render-studio/resolve/main/tests/test_config_uploads.py
- Command line
-
hf download hf://spaces/cpuai/blender-render-studio/tests/test_config_uploads.py
-
curl -L -o test_config_uploads.py https://huggingface.co/spaces/cpuai/blender-render-studio/resolve/main/tests/test_config_uploads.py
9.83 kB
| """配置和资源导入安全边界;不需要 Blender 或 GPU。""" | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import stat | |
| import sys | |
| import zipfile | |
| from contextlib import nullcontext | |
| from pathlib import Path | |
| import pytest | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | |
| from render_config import RenderConfig, json_schema, parse_config | |
| from uploads import UploadError, stage_uploads | |
| def make_zip(path: Path, entries: list[tuple[str | zipfile.ZipInfo, bytes]], compression=zipfile.ZIP_STORED) -> Path: | |
| with zipfile.ZipFile(path, "w", compression=compression) as archive: | |
| for name, contents in entries: | |
| archive.writestr(name, contents) | |
| return path | |
| def test_rejects_wrong_types_and_unknown_keys(payload): | |
| with pytest.raises(ValueError): | |
| parse_config(json.dumps(payload)) | |
| def test_config_rejects_unsafe_blend_path(path): | |
| with pytest.raises(ValueError): | |
| parse_config(json.dumps({"blend_file": path})) | |
| def test_json_syntax_and_size(text): | |
| with pytest.raises(ValueError): | |
| parse_config(text) | |
| def test_defaults_schema_and_frame_sequence(): | |
| default = parse_config("{}") | |
| assert default.frame_numbers() == [1] | |
| assert (default.output_width, default.output_height) == (1280, 720) | |
| config = parse_config('{"mode":"frames","frame_start":3,"frame_end":10,"frame_step":3}') | |
| assert config.frame_numbers() == [3, 6, 9] | |
| assert json_schema()["additionalProperties"] is False | |
| assert json_schema()["properties"]["device"]["enum"] == ["AUTO", "CUDA", "OPTIX", "CPU"] | |
| def test_format_and_video_constraints(payload): | |
| with pytest.raises(ValueError): | |
| parse_config(json.dumps(payload)) | |
| def test_server_limits_count_rendered_frames_and_effective_pixels(monkeypatch): | |
| monkeypatch.setenv("MAX_FRAMES", "3") | |
| monkeypatch.setenv("MAX_PIXELS", "1000000") | |
| assert len(RenderConfig(mode="frames", frame_start=1, frame_end=5, frame_step=2).frame_numbers()) == 3 | |
| assert RenderConfig(width=1920, height=1080, resolution_percentage=50).output_width == 960 | |
| with pytest.raises(ValueError, match="MAX_FRAMES"): | |
| RenderConfig(mode="frames", frame_end=4) | |
| with pytest.raises(ValueError, match="MAX_PIXELS"): | |
| RenderConfig(width=1920, height=1080) | |
| def test_blend_with_resource_zip_and_nested_zip(tmp_path): | |
| blend = tmp_path / "scene.blend" | |
| # 不限制 .blend 头:兼容 Blender 自身的压缩格式。 | |
| blend.write_bytes(b"compressed-placeholder") | |
| resources = make_zip(tmp_path / "resources.zip", [ | |
| ("textures/albedo.png", b"png"), ("textures/", b""), | |
| ("cache/nested.zip", b"not recursively expanded"), | |
| ]) | |
| destination = tmp_path / "project" | |
| selected = stage_uploads([str(blend), str(resources)], destination, None) | |
| assert selected == destination / "scene.blend" | |
| assert selected.is_absolute() | |
| assert (destination / "textures/albedo.png").read_bytes() == b"png" | |
| assert (destination / "cache/nested.zip").read_bytes() == b"not recursively expanded" | |
| def test_multiple_blends_require_explicit_selection_and_preserve_paths(tmp_path): | |
| archive = make_zip(tmp_path / "project.zip", [("scenes/a.blend", b"a"), ("scenes/b.blend", b"b")]) | |
| with pytest.raises(UploadError, match="多个"): | |
| stage_uploads([str(archive)], tmp_path / "failed", None) | |
| assert not (tmp_path / "failed").exists() | |
| selected = stage_uploads([str(archive)], tmp_path / "valid", "scenes/b.blend") | |
| assert selected.read_bytes() == b"b" | |
| def test_zip_rejects_unsafe_entries_without_partial_output(tmp_path, malicious_name): | |
| archive = make_zip(tmp_path / "attack.zip", [("good.blend", b"blend"), (malicious_name, b"bad")]) | |
| destination = tmp_path / "project" | |
| destination.mkdir() | |
| with pytest.raises(UploadError): | |
| stage_uploads([str(archive)], destination, None) | |
| assert list(destination.iterdir()) == [] | |
| assert not list(tmp_path.glob(".project.upload-*")) | |
| assert not (tmp_path / "escape").exists() | |
| def test_zip_rejects_collisions(tmp_path, names): | |
| with pytest.warns(UserWarning) if names[0] == names[1] else nullcontext(): | |
| archive = make_zip(tmp_path / "conflict.zip", [(name, b"x") for name in names]) | |
| with pytest.raises(UploadError, match="冲突|重复"): | |
| stage_uploads([str(archive)], tmp_path / "project", None) | |
| def test_nul_zip_filename_is_not_silently_truncated(tmp_path): | |
| archive = make_zip(tmp_path / "nul.zip", [("scene.blend", b"blend"), ("evilQname", b"bad")]) | |
| # zipfile.writestr 自身会截断 NUL,因此修改 local header 和 central directory。 | |
| archive.write_bytes(archive.read_bytes().replace(b"evilQname", b"evil\x00name")) | |
| with pytest.raises(UploadError, match="NUL"): | |
| stage_uploads([str(archive)], tmp_path / "project", None) | |
| def test_zip_special_files_are_rejected(tmp_path, kind): | |
| entry = zipfile.ZipInfo("malicious") | |
| entry.create_system = 3 | |
| entry.external_attr = (kind | 0o777) << 16 | |
| archive = make_zip(tmp_path / "special.zip", [("scene.blend", b"blend"), (entry, b"../outside")]) | |
| with pytest.raises(UploadError, match="符号链接|特殊"): | |
| stage_uploads([str(archive)], tmp_path / "project", None) | |
| def test_source_symlink_and_fifo_rejected(tmp_path): | |
| source = tmp_path / "actual.blend" | |
| source.write_bytes(b"blend") | |
| link = tmp_path / "link.blend" | |
| link.symlink_to(source) | |
| with pytest.raises(UploadError, match="符号链接"): | |
| stage_uploads([str(link)], tmp_path / "project", None) | |
| if hasattr(os, "mkfifo"): | |
| pipe = tmp_path / "pipe.blend" | |
| os.mkfifo(pipe) | |
| with pytest.raises(UploadError, match="普通文件"): | |
| stage_uploads([str(pipe)], tmp_path / "project", None) | |
| def test_zip_bomb_rejected_before_writing(tmp_path): | |
| archive = make_zip(tmp_path / "bomb.zip", [("scene.blend", b"0" * 2_000_000)], compression=zipfile.ZIP_DEFLATED) | |
| with pytest.raises(UploadError, match="压缩比"): | |
| stage_uploads([str(archive)], tmp_path / "project", None) | |
| assert not (tmp_path / "project").exists() | |
| def test_extraction_size_and_entry_limits(tmp_path, monkeypatch): | |
| archive = make_zip(tmp_path / "large.zip", [("scene.blend", b"s"), ("large.bin", b"x" * 1_048_576)]) | |
| monkeypatch.setenv("MAX_EXTRACT_MB", "1") | |
| with pytest.raises(UploadError, match="MAX_EXTRACT_MB"): | |
| stage_uploads([str(archive)], tmp_path / "project", None) | |
| assert not (tmp_path / "project").exists() | |
| monkeypatch.setenv("MAX_ARCHIVE_FILES", "1") | |
| with pytest.raises(UploadError, match="MAX_ARCHIVE_FILES"): | |
| stage_uploads([str(archive)], tmp_path / "project", None) | |
| def test_upload_size_limit_and_script_rejection(tmp_path, monkeypatch): | |
| source = tmp_path / "scene.blend" | |
| source.write_bytes(b"x" * 1_048_577) | |
| monkeypatch.setenv("MAX_UPLOAD_MB", "1") | |
| with pytest.raises(UploadError, match="MAX_UPLOAD_MB"): | |
| stage_uploads([str(source)], tmp_path / "project", None) | |
| script = tmp_path / "helper.py" | |
| script.write_text("raise RuntimeError('must never execute')") | |
| with pytest.raises(UploadError, match="Python"): | |
| stage_uploads([str(script)], tmp_path / "project", None) | |
| def test_preexisting_data_is_never_removed_on_failure(tmp_path): | |
| destination = tmp_path / "project" | |
| destination.mkdir() | |
| keep = destination / "keep.txt" | |
| keep.write_text("important") | |
| source = tmp_path / "scene.blend" | |
| source.write_bytes(b"blend") | |
| with pytest.raises(UploadError, match="覆盖"): | |
| stage_uploads([str(source)], destination, None) | |
| assert keep.read_text() == "important" | |
| def test_separate_zip_and_file_cannot_overwrite_each_other(tmp_path): | |
| source = tmp_path / "scene.blend" | |
| source.write_bytes(b"original") | |
| archive = make_zip(tmp_path / "resources.zip", [("scene.blend", b"replace")]) | |
| with pytest.raises(UploadError, match="冲突|重复"): | |
| stage_uploads([str(source), str(archive)], tmp_path / "project", None) | |
| assert source.read_bytes() == b"original" | |