import importlib import os import sys from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from render_config import RenderConfig, parse_config from blender_runtime import _worker_environment def test_explicit_cpu_bypasses_zerogpu_even_when_deployment_is_zerogpu(monkeypatch): import service monkeypatch.setenv('RENDER_BACKEND', 'zerogpu') cfg = parse_config('{"device":"CPU"}') assert (cfg.engine, cfg.device) == ('EEVEE', 'CPU') assert service._backend(cfg) == 'cpu' calls = [] monkeypatch.setattr(service, 'run_task', lambda *args: calls.append('cpu') or {'ok': True}) monkeypatch.setattr(service, '_zero_render', lambda *args: pytest.fail('CPU must not allocate a GPU')) service._execute({}, Path('/tmp'), service._backend(cfg), cfg, Path('/tmp/mode-test.log')) assert calls == ['cpu'] def test_default_gpu_and_cycles_only_devices_are_validated(monkeypatch): import service from spaces.config import Config monkeypatch.setenv('RENDER_BACKEND', 'auto') monkeypatch.setattr(Config, 'zero_gpu', True) assert (RenderConfig().engine, RenderConfig().device) == ('EEVEE', 'AUTO') assert service._backend(RenderConfig()) == 'zerogpu' assert service._backend(RenderConfig(engine='CYCLES', device='AUTO')) == 'zerogpu' assert service._backend(RenderConfig(engine='CYCLES', device='CPU')) == 'cpu' with pytest.raises(ValueError, match='CUDA / OPTIX'): RenderConfig(engine='EEVEE', device='CUDA') def test_software_gl_environment_does_not_leak_into_gpu_jobs(monkeypatch): monkeypatch.setenv('LIBGL_ALWAYS_SOFTWARE', 'true') monkeypatch.setenv('GALLIUM_DRIVER', 'llvmpipe') cpu = _worker_environment({'engine': 'EEVEE', 'device': 'CPU'}) gpu = _worker_environment({'engine': 'EEVEE', 'device': 'AUTO'}) assert cpu['LIBGL_ALWAYS_SOFTWARE'] == 'true' assert cpu['GALLIUM_DRIVER'] == 'llvmpipe' assert 'LIBGL_ALWAYS_SOFTWARE' not in gpu assert 'GALLIUM_DRIVER' not in gpu @pytest.mark.skipif(not hasattr(os, 'fork'), reason='Linux fork regression') def test_lock_released_while_forked_worker_still_holds_descriptor(tmp_path): import service r, w = os.pipe() child = None try: with service._lock(tmp_path): child = os.fork() if child == 0: os.close(w) os.read(r, 1) os._exit(0) # Child still owns an inherited descriptor, but must not own the lock. with service._lock(tmp_path): pass finally: if child: os.write(w, b'x') os.waitpid(child, 0) os.close(r) os.close(w) def test_legacy_jobs_resume_with_cycles(): import service assert service._config({'config': {'device': 'AUTO'}}).engine == 'CYCLES' assert service._config({'config': {'engine': 'EEVEE', 'device': 'CPU'}}).engine == 'EEVEE' def test_zerogpu_preserves_worker_error_details(monkeypatch): import service def failed_worker(*args): raise RuntimeError('EGL initialization failed: no graphics device') monkeypatch.setattr(service, 'run_task', failed_worker) report = service._allocated_render({}, '/tmp/probe.log', '', 30) assert not report['ok'] assert 'no graphics device' in report['error'] def test_eevee_gpu_probe_is_explicit_and_truthful(monkeypatch): import service configs = [] def probe(cfg): configs.append(cfg) return {'ok': False, 'gpu_verified': False, 'error': 'No EGL device'}, [], None monkeypatch.setattr(service, '_self_test', probe) report, _, _ = service.eevee_gpu_self_test() assert configs[0].engine == 'EEVEE' and configs[0].device == 'AUTO' assert not report['gpu_eevee_usable'] assert 'No EGL device' == report['error'] def test_requested_zerogpu_size_routes_exactly_once(monkeypatch): import service calls = [] monkeypatch.setattr(service, '_zero_render', lambda *a: calls.append('large') or {'ok': True}) monkeypatch.setattr(service, '_zero_render_xlarge', lambda *a: calls.append('xlarge') or {'ok': True}) for size in ('large', 'xlarge'): cfg = RenderConfig(gpu_size=size) service._execute({}, Path('/tmp'), 'zerogpu', cfg, Path('/tmp/probe.log')) assert calls == ['large', 'xlarge'] def test_graphics_probe_preserves_requested_backend_and_size(monkeypatch): import service configs = [] monkeypatch.setattr(service, '_self_test', lambda cfg: (configs.append(cfg) or {'ok': True, 'gpu_verified': True}, [], None)) report, _, _ = service.eevee_gpu_self_test('xlarge', 'vulkan') assert configs[0].gpu_size == 'xlarge' and configs[0].graphics_backend == 'vulkan' assert report['gpu_eevee_usable'] with pytest.raises(ValueError): RenderConfig(graphics_backend='opengl --python unsafe.py')