Skip to content

Commit 722abb4

Browse files
committed
Add imiv RGB input regression test
Add a new RGB-input regression test for imiv: register a ctest (imiv_rgb_input_regression) in CMake with labels and timeout, update README and imiv_backends docs to reference the new check, and extend imiv_backend_verify.py to run the rgb checks and print rgb-related log paths. Introduce tools/imiv_rgb_input_regression.py which generates a 3-channel RGB fixture with oiiotool, runs the imiv GUI test runner, captures layout/state/log outputs, scans for runtime/graphics error patterns, and validates that an RGB image was loaded correctly (matching path and image size). The test exits non‑zero on failures to catch regressions in RGB input handling and related GPU/preview issues.
1 parent b2f17e5 commit 722abb4

5 files changed

Lines changed: 340 additions & 0 deletions

File tree

src/imiv/CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,6 +1332,23 @@ if (TARGET imiv
13321332
LABELS "imiv;gui"
13331333
TIMEOUT 360)
13341334

1335+
add_test (
1336+
NAME imiv_rgb_input_regression
1337+
COMMAND
1338+
"${Python3_EXECUTABLE}"
1339+
"${CMAKE_CURRENT_SOURCE_DIR}/tools/imiv_rgb_input_regression.py"
1340+
--bin "$<TARGET_FILE:imiv>"
1341+
--cwd "$<TARGET_FILE_DIR:imiv>"
1342+
${_imiv_ctest_default_backend_args}
1343+
--oiiotool "$<TARGET_FILE:oiiotool>"
1344+
--env-script "${CMAKE_BINARY_DIR}/imiv_env.sh"
1345+
--out-dir "${CMAKE_BINARY_DIR}/imiv_captures/rgb_input_regression"
1346+
--source-image "${PROJECT_SOURCE_DIR}/ASWF/logos/openimageio-stacked-gradient.png")
1347+
set_tests_properties (
1348+
imiv_rgb_input_regression PROPERTIES
1349+
LABELS "imiv;imiv_rgb;gui"
1350+
TIMEOUT 120)
1351+
13351352
if (_imiv_enabled_backend_count GREATER 1)
13361353
add_test (
13371354
NAME imiv_backend_preferences_regression

src/imiv/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ ctest --test-dir build_u -V -R '^imiv_backend_preferences_regression$'
117117
Main output logs:
118118

119119
- `verify_smoke.log`
120+
- `verify_rgb.log`
120121
- `verify_ux.log`
121122
- `verify_screenshot.log`
122123
- `verify_sampling.log`

src/imiv/imiv_backends.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ Manual verification:
207207

208208
- canonical cross-platform backend verifier:
209209
- [imiv_backend_verify.py](/mnt/f/gh/openimageio/src/imiv/tools/imiv_backend_verify.py)
210+
- shared RGB-input regression:
211+
- [imiv_rgb_input_regression.py](/mnt/f/gh/openimageio/src/imiv/tools/imiv_rgb_input_regression.py)
210212
- compatibility frontends:
211213
- [imiv_macos_backend_verify.sh](/mnt/f/gh/openimageio/src/imiv/tools/imiv_macos_backend_verify.sh)
212214
- [imiv_linux_backend_verify.sh](/mnt/f/gh/openimageio/src/imiv/tools/imiv_linux_backend_verify.sh)

src/imiv/tools/imiv_backend_verify.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,36 @@ def _ux_checks(
422422
return [("ux", cmd, out_dir / "verify_ux.log", None)]
423423

424424

425+
def _rgb_checks(
426+
repo_root: Path,
427+
backend: str,
428+
exe: Path,
429+
run_cwd: Path,
430+
oiiotool: Path,
431+
out_dir: Path,
432+
source_image: Path,
433+
env_script: Path | None,
434+
trace: bool,
435+
) -> list[tuple[str, list[str], Path, dict[str, str] | None]]:
436+
script = repo_root / "src" / "imiv" / "tools" / "imiv_rgb_input_regression.py"
437+
cmd = _script_cmd(
438+
script,
439+
backend=backend,
440+
exe=exe,
441+
run_cwd=run_cwd,
442+
out_dir=out_dir / "runtime_rgb",
443+
trace=trace,
444+
extra=[
445+
"--oiiotool",
446+
str(oiiotool),
447+
"--source-image",
448+
str(source_image),
449+
],
450+
env_script=env_script,
451+
)
452+
return [("rgb", cmd, out_dir / "verify_rgb.log", None)]
453+
454+
425455
def _ocio_checks(
426456
repo_root: Path,
427457
backend: str,
@@ -776,6 +806,19 @@ def main() -> int:
776806
args.trace,
777807
)
778808
)
809+
checks.extend(
810+
_rgb_checks(
811+
repo_root,
812+
args.backend,
813+
imiv,
814+
run_cwd,
815+
oiiotool,
816+
out_dir,
817+
image_path,
818+
env_script,
819+
args.trace,
820+
)
821+
)
779822
checks.extend(
780823
_ux_checks(
781824
repo_root,
@@ -827,6 +870,8 @@ def main() -> int:
827870
print(f" build: {build_log}")
828871
print(f" smoke: {out_dir / 'verify_smoke.log'}")
829872
print(f" runtime+s: {out_dir / 'runtime'}")
873+
print(f" rgb: {out_dir / 'verify_rgb.log'}")
874+
print(f" runtime+rgb: {out_dir / 'runtime_rgb'}")
830875
print(f" ux: {out_dir / 'verify_ux.log'}")
831876
print(f" runtime+ux: {out_dir / 'runtime_ux'}")
832877
if args.backend == "metal":
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
#!/usr/bin/env python3
2+
"""Regression check for loading a true RGB input image in imiv."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import json
8+
import os
9+
import shlex
10+
import shutil
11+
import subprocess
12+
import sys
13+
from pathlib import Path
14+
15+
16+
ERROR_PATTERNS = (
17+
"error: imiv exited with code",
18+
"OpenGL texture upload failed",
19+
"OpenGL preview draw failed",
20+
"OpenGL OCIO preview draw failed",
21+
"failed to create Metal source texture",
22+
"failed to create Metal upload pipeline",
23+
"failed to create Metal source upload buffer",
24+
"Metal source upload compute dispatch failed",
25+
"failed to create Metal preview texture",
26+
"Metal preview render failed",
27+
)
28+
29+
30+
def _repo_root() -> Path:
31+
return Path(__file__).resolve().parents[3]
32+
33+
34+
def _default_binary(repo_root: Path) -> Path:
35+
candidates = [
36+
repo_root / "build_u" / "bin" / "imiv",
37+
repo_root / "build" / "bin" / "imiv",
38+
repo_root / "build_u" / "src" / "imiv" / "imiv",
39+
repo_root / "build" / "src" / "imiv" / "imiv",
40+
repo_root / "build" / "Debug" / "imiv.exe",
41+
repo_root / "build" / "Release" / "imiv.exe",
42+
]
43+
for candidate in candidates:
44+
if candidate.exists():
45+
return candidate
46+
return candidates[0]
47+
48+
49+
def _default_oiiotool(repo_root: Path) -> Path:
50+
candidates = [
51+
repo_root / "build_u" / "bin" / "oiiotool",
52+
repo_root / "build" / "bin" / "oiiotool",
53+
repo_root / "build_u" / "src" / "oiiotool" / "oiiotool",
54+
repo_root / "build" / "src" / "oiiotool" / "oiiotool",
55+
repo_root / "build" / "Debug" / "oiiotool.exe",
56+
repo_root / "build" / "Release" / "oiiotool.exe",
57+
]
58+
for candidate in candidates:
59+
if candidate.exists():
60+
return candidate
61+
which = shutil.which("oiiotool")
62+
return Path(which) if which else candidates[0]
63+
64+
65+
def _default_env_script(repo_root: Path, exe: Path | None = None) -> Path:
66+
candidates: list[Path] = []
67+
if exe is not None:
68+
exe = exe.resolve()
69+
candidates.extend([exe.parent / "imiv_env.sh", exe.parent.parent / "imiv_env.sh"])
70+
candidates.extend([repo_root / "build" / "imiv_env.sh", repo_root / "build_u" / "imiv_env.sh"])
71+
for candidate in candidates:
72+
if candidate.exists():
73+
return candidate
74+
return candidates[0]
75+
76+
77+
def _load_env_from_script(script_path: Path) -> dict[str, str]:
78+
env = dict(os.environ)
79+
if not script_path.exists() or shutil.which("bash") is None:
80+
return env
81+
82+
quoted = shlex.quote(str(script_path))
83+
proc = subprocess.run(
84+
["bash", "-lc", f"source {quoted} >/dev/null 2>&1; env -0"],
85+
check=True,
86+
stdout=subprocess.PIPE,
87+
)
88+
for item in proc.stdout.split(b"\0"):
89+
if not item:
90+
continue
91+
key, _, value = item.partition(b"=")
92+
if not key:
93+
continue
94+
env[key.decode("utf-8", errors="ignore")] = value.decode(
95+
"utf-8", errors="ignore"
96+
)
97+
return env
98+
99+
100+
def _run_checked(cmd: list[str], *, cwd: Path) -> None:
101+
print("run:", " ".join(cmd))
102+
subprocess.run(cmd, cwd=str(cwd), check=True)
103+
104+
105+
def _generate_rgb_fixture(oiiotool: Path, source_path: Path, out_path: Path) -> None:
106+
out_path.parent.mkdir(parents=True, exist_ok=True)
107+
_run_checked(
108+
[
109+
str(oiiotool),
110+
str(source_path),
111+
"--ch",
112+
"R,G,B",
113+
"-d",
114+
"uint8",
115+
"-o",
116+
str(out_path),
117+
],
118+
cwd=out_path.parent,
119+
)
120+
121+
122+
def main() -> int:
123+
repo_root = _repo_root()
124+
runner = repo_root / "src" / "imiv" / "tools" / "imiv_gui_test_run.py"
125+
default_source = repo_root / "ASWF" / "logos" / "openimageio-stacked-gradient.png"
126+
127+
ap = argparse.ArgumentParser(description=__doc__)
128+
ap.add_argument("--bin", default=str(_default_binary(repo_root)), help="imiv executable")
129+
ap.add_argument("--cwd", default="", help="Working directory for imiv")
130+
ap.add_argument(
131+
"--backend",
132+
default="",
133+
help="Optional runtime backend override passed through to imiv",
134+
)
135+
ap.add_argument(
136+
"--oiiotool", default=str(_default_oiiotool(repo_root)), help="oiiotool executable"
137+
)
138+
ap.add_argument("--env-script", default="", help="Optional shell env setup script")
139+
ap.add_argument("--out-dir", default="", help="Output directory")
140+
ap.add_argument(
141+
"--source-image",
142+
default=str(default_source),
143+
help="Source image used to generate a 3-channel RGB fixture",
144+
)
145+
ap.add_argument("--trace", action="store_true", help="Enable runner tracing")
146+
args = ap.parse_args()
147+
148+
exe = Path(args.bin).resolve()
149+
if not exe.exists():
150+
print(f"error: binary not found: {exe}", file=sys.stderr)
151+
return 2
152+
153+
oiiotool = Path(args.oiiotool).resolve()
154+
if not oiiotool.exists():
155+
print(f"error: oiiotool not found: {oiiotool}", file=sys.stderr)
156+
return 2
157+
158+
cwd = Path(args.cwd).resolve() if args.cwd else exe.parent.resolve()
159+
if args.out_dir:
160+
out_dir = Path(args.out_dir).resolve()
161+
else:
162+
out_dir = exe.parent.parent / "imiv_captures" / "rgb_input_regression"
163+
out_dir.mkdir(parents=True, exist_ok=True)
164+
165+
source_path = Path(args.source_image).resolve()
166+
if not source_path.exists():
167+
print(f"error: source image not found: {source_path}", file=sys.stderr)
168+
return 2
169+
170+
env_script = (
171+
Path(args.env_script).resolve()
172+
if args.env_script
173+
else _default_env_script(repo_root, exe)
174+
)
175+
env = _load_env_from_script(env_script)
176+
env["IMIV_CONFIG_HOME"] = str(out_dir / "cfg")
177+
178+
rgb_fixture = out_dir / "rgb_input_fixture_u8.tif"
179+
_generate_rgb_fixture(oiiotool, source_path, rgb_fixture)
180+
181+
layout_path = out_dir / "rgb_input.layout.json"
182+
state_path = out_dir / "rgb_input.state.json"
183+
log_path = out_dir / "rgb_input.log"
184+
185+
cmd = [
186+
sys.executable,
187+
str(runner),
188+
"--bin",
189+
str(exe),
190+
"--cwd",
191+
str(cwd),
192+
]
193+
if args.backend:
194+
cmd.extend(["--backend", args.backend])
195+
cmd.extend(
196+
[
197+
"--open",
198+
str(rgb_fixture),
199+
"--layout-json-out",
200+
str(layout_path),
201+
"--layout-items",
202+
"--state-json-out",
203+
str(state_path),
204+
"--post-action-delay-frames",
205+
"2",
206+
]
207+
)
208+
if args.trace:
209+
cmd.append("--trace")
210+
211+
with log_path.open("w", encoding="utf-8") as log_handle:
212+
proc = subprocess.run(
213+
cmd,
214+
cwd=str(repo_root),
215+
env=env,
216+
check=False,
217+
stdout=log_handle,
218+
stderr=subprocess.STDOUT,
219+
timeout=90,
220+
)
221+
if proc.returncode != 0:
222+
print(f"error: runner exited with code {proc.returncode}", file=sys.stderr)
223+
return 1
224+
225+
for required in (layout_path, state_path):
226+
if not required.exists():
227+
print(f"error: missing output: {required}", file=sys.stderr)
228+
return 1
229+
230+
log_text = log_path.read_text(encoding="utf-8", errors="ignore")
231+
for pattern in ERROR_PATTERNS:
232+
if pattern in log_text:
233+
print(f"error: found runtime error pattern: {pattern}", file=sys.stderr)
234+
return 1
235+
236+
layout = json.loads(layout_path.read_text(encoding="utf-8"))
237+
if not any(window.get("name") == "Image" for window in layout.get("windows", [])):
238+
print("error: layout dump missing Image window", file=sys.stderr)
239+
return 1
240+
241+
state = json.loads(state_path.read_text(encoding="utf-8"))
242+
if not state.get("image_loaded"):
243+
print("error: state dump says image is not loaded", file=sys.stderr)
244+
return 1
245+
246+
current_path = state.get("image_path") or ""
247+
if not current_path:
248+
print("error: state dump missing image_path", file=sys.stderr)
249+
return 1
250+
try:
251+
current_resolved = Path(current_path).resolve()
252+
except Exception:
253+
current_resolved = Path(current_path)
254+
if current_resolved != rgb_fixture.resolve():
255+
print(
256+
f"error: loaded path mismatch: expected {rgb_fixture}, got {current_path}",
257+
file=sys.stderr,
258+
)
259+
return 1
260+
261+
image_size = state.get("image_size", [0, 0])
262+
if (
263+
not isinstance(image_size, list)
264+
or len(image_size) != 2
265+
or int(image_size[0]) <= 0
266+
or int(image_size[1]) <= 0
267+
):
268+
print(f"error: invalid image_size in state dump: {image_size}", file=sys.stderr)
269+
return 1
270+
271+
return 0
272+
273+
274+
if __name__ == "__main__":
275+
raise SystemExit(main())

0 commit comments

Comments
 (0)