File size: 4,909 Bytes
5d6dcc3
 
 
 
 
 
 
 
 
 
 
 
d443689
5d6dcc3
 
d443689
5d6dcc3
 
 
 
 
 
 
 
 
d443689
5d6dcc3
 
 
 
d443689
 
5d6dcc3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f135496
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d443689
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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')