Files
MobileGL/tools/cts/scripts/run_cts_local.py
BZLZHH 1c5f6c0986 [Chore] (tools/cts): pick a run config the suite does not contradict
Two harness settings were producing failures that say nothing about the backend:

- dEQP's FboRenderContext picks the first entry of its own depth/stencil format
  list, GL_DEPTH32F_STENCIL8, when the config leaves the bit counts DONT_CARE.
  framebuffer_blit meanwhile hardcodes GL_DEPTH24_STENCIL8 for its own buffers
  as soon as it detects an FBO surface, and then blits depth between the two -
  which the spec forbids for mismatched formats, so a conformant driver has no
  choice but to fail it. Default to --deqp-gl-config-name=rgba8888d24s8 so the
  wrapper framebuffer and the test agree.

- --deqp-watchdog aborts the whole process when one case exceeds a hardcoded 30
  seconds (framework/common/tcuApp.hpp). That is not a hang on a CPU rasterizer:
  several texture_swizzle cases take ~17s each standalone and cross the limit
  once the process is warm, which came back as ten spurious Timeouts. dEQP's own
  default is off, and --chunk-timeout is what actually rescues a genuinely
  wedged case, so default it off too and leave it selectable.
2026-08-01 13:48:58 -04:00

228 lines
10 KiB
Python

#!/usr/bin/env python
"""Drive a glcts run on the local host, resuming across crashes.
Local-host counterpart of run_cts.py: MobileGL crashes on some cases and glcts
takes the whole process down with it, so a single invocation stops at the first
crash. This runner re-invokes glcts with only the cases that have no result
yet, records the case that was open when the process died as "Crash" (or
"Hang" on a timeout), and repeats until the list is exhausted.
Usage:
python run_cts_local.py --backend DirectVulkan \\
--glcts <path-to-glcts-binary> --lib <path-to-libMobileGL.so> \\
--caselist <mustpass.txt> --outdir <dir> [--env K=V ...]
"""
import argparse
import glob
import os
import re
import resource
import subprocess
import sys
import time
CASE_START = re.compile(r"^#beginTestCaseResult\s+(\S+)")
CASE_END = re.compile(r"^#endTestCaseResult")
CASE_TERM = re.compile(r"^#terminateTestCaseResult")
def completed_cases(qpa_path):
"""Return (finished_case_names, last_started_case_or_None)."""
finished = []
current = None
if not os.path.exists(qpa_path):
return finished, None
with open(qpa_path, "r", encoding="utf-8", errors="replace") as fh:
for line in fh:
m = CASE_START.match(line)
if m:
current = m.group(1)
continue
if current is not None and (CASE_END.match(line) or CASE_TERM.match(line)):
finished.append(current)
current = None
return finished, current
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--backend", required=True, choices=["DirectGLES", "DirectVulkan"])
ap.add_argument("--glcts", required=True, help="path to the glcts binary")
ap.add_argument("--lib", required=True, help="path to libMobileGL.so")
ap.add_argument("--caselist", required=True)
ap.add_argument("--outdir", required=True)
ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value")
# Without an explicit size, dEQP's FboRenderContext sizes the wrapper FBO to
# GL_MAX_RENDERBUFFER_SIZE (16384^2 here) and size-derived test allocations
# explode (a 4-sample 16K depth texture alone is 4 GiB).
ap.add_argument("--surface-size", type=int, default=256,
help="--deqp-surface-width/height value")
# With DONT_CARE depth/stencil bits dEQP's FboRenderContext picks the first entry of
# its own format list, GL_DEPTH32F_STENCIL8. framebuffer_blit meanwhile hardcodes
# GL_DEPTH24_STENCIL8 for its own buffers whenever it detects an FBO surface, then
# blits depth between the two - which the spec forbids for mismatched formats, so a
# conformant driver has to fail it. Asking for a config the test agrees with avoids
# the contradiction instead of papering over it.
ap.add_argument("--gl-config-name", default="rgba8888d24s8",
help="--deqp-gl-config-name value (empty string to leave it unset)")
ap.add_argument("--max-rounds", type=int, default=4000)
ap.add_argument("--max-empty-streak", type=int, default=64,
help="abort after this many consecutive chunks that produce no log at all")
ap.add_argument("--chunk-timeout", type=int, default=1800,
help="seconds before killing one glcts invocation (a wedged case never returns)")
# dEQP's watchdog aborts the process when a single case exceeds a hardcoded 30s
# (framework/common/tcuApp.hpp), which is not a hang on a CPU rasterizer - some
# texture_swizzle cases legitimately take ~17s each and cross it once the process is
# warm, so they came back as spurious Timeouts. dEQP's own default is off; the
# chunk-timeout above is what actually rescues a genuinely wedged case.
ap.add_argument("--watchdog", default="disable", choices=["enable", "disable"],
help="--deqp-watchdog value")
ap.add_argument("--skip-file", default=None,
help="file of case names to exclude, e.g. cases known to wedge the host")
ap.add_argument("--waiver-file", default=None,
help="--deqp-waiver-file value, e.g. tools/cts/waivers/mobilegl-fbo-harness.xml")
ap.add_argument("--env", action="append", default=[], metavar="K=V",
help="extra environment variable for glcts (repeatable)")
args = ap.parse_args()
os.makedirs(args.outdir, exist_ok=True)
glcts = os.path.abspath(args.glcts)
lib = os.path.abspath(args.lib)
# glcts resolves its gl_cts data tree relative to the binary's directory.
workdir = os.path.dirname(glcts)
with open(args.caselist, "r", encoding="utf-8") as fh:
remaining = [l.strip() for l in fh if l.strip() and not l.strip().startswith("#")]
skipped = []
if args.skip_file and os.path.isfile(args.skip_file):
with open(args.skip_file, "r", encoding="utf-8") as fh:
skip = {l.strip() for l in fh if l.strip() and not l.strip().startswith("#")}
skipped = [c for c in remaining if c in skip]
remaining = [c for c in remaining if c not in skip]
print(f"[run_cts_local] skipping {len(skipped)} case(s) from {args.skip_file}")
total = len(remaining)
print(f"[run_cts_local] {args.backend}: {total} cases")
env = dict(os.environ)
env["MOBILEGL_BACKEND_TYPE"] = args.backend
env["MOBILEGL_CTS_LIB"] = lib
for kv in args.env:
k, _, v = kv.partition("=")
env[k] = v
crashed = []
hung = []
done = set()
chunk = 0
started = time.time()
empty_streak = 0
# Resume: results in chunk files from an interrupted run still count. The
# case that was open when that run died is re-tried rather than assumed bad.
prior_chunks = sorted(glob.glob(os.path.join(args.outdir, "chunk*.qpa")))
for prior in prior_chunks:
finished, _ = completed_cases(prior)
done.update(finished)
if prior_chunks:
chunk = int(re.search(r"chunk(\d+)\.qpa$", prior_chunks[-1]).group(1)) + 1
remaining = [c for c in remaining if c not in done]
print(f"[run_cts_local] resuming: {len(done)} case(s) already measured, "
f"{len(remaining)} to go")
while remaining and chunk < args.max_rounds:
listfile = os.path.abspath(os.path.join(args.outdir, "remaining.txt"))
with open(listfile, "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(remaining) + "\n")
qpa = os.path.abspath(os.path.join(args.outdir, f"chunk{chunk:04d}.qpa"))
cmd = [
glcts,
f"--deqp-caselist-file={listfile}",
f"--deqp-surface-type={args.surface}",
f"--deqp-surface-width={args.surface_size}",
f"--deqp-surface-height={args.surface_size}",
"--deqp-terminate-on-device-lost=disable",
f"--deqp-watchdog={args.watchdog}",
"--deqp-log-images=disable",
"--deqp-log-shader-sources=disable",
f"--deqp-log-filename={qpa}",
]
if args.gl_config_name:
cmd.append(f"--deqp-gl-config-name={args.gl_config_name}")
if args.waiver_file:
cmd.append(f"--deqp-waiver-file={os.path.abspath(args.waiver_file)}")
timed_out = False
try:
# RLIMIT_CORE=0: MobileGL asserts abort with a core dump, and writing
# a multi-GB glcts core image after every crash dominates wall time.
subprocess.run(cmd, cwd=workdir, env=env, timeout=args.chunk_timeout,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
start_new_session=True,
preexec_fn=lambda: resource.setrlimit(resource.RLIMIT_CORE, (0, 0)))
except subprocess.TimeoutExpired:
timed_out = True
print(f"[run_cts_local] chunk {chunk:04d} timed out after {args.chunk_timeout}s",
file=sys.stderr)
finished, in_flight = completed_cases(qpa)
for c in finished:
done.add(c)
progressed = len(finished)
if progressed > 0:
empty_streak = 0
if in_flight is not None:
if timed_out:
print(f"[run_cts_local] HANG in {in_flight} - quarantining it")
hung.append(in_flight)
else:
crashed.append(in_flight)
done.add(in_flight)
progressed += 1
elif progressed == 0:
empty_streak += 1
if empty_streak >= args.max_empty_streak:
print(f"[run_cts_local] ABORTING: {empty_streak} consecutive chunks produced no "
f"output. Something systemic is wrong; refusing to label the rest of the "
f"suite as crashes.", file=sys.stderr)
break
victim = remaining[0]
label = "Hang" if timed_out else "Crash"
print(f"[run_cts_local] no output at all; recording {victim} as {label}")
(hung if timed_out else crashed).append(victim)
done.add(victim)
progressed = 1
remaining = [c for c in remaining if c not in done]
elapsed = time.time() - started
print(
f"[run_cts_local] chunk {chunk:04d}: +{progressed} (done {len(done)}/{total}, "
f"crashes {len(crashed)}, hangs {len(hung)}, {elapsed / 60:.1f} min)"
)
chunk += 1
with open(os.path.join(args.outdir, "crashed.txt"), "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(crashed) + ("\n" if crashed else ""))
with open(os.path.join(args.outdir, "hung.txt"), "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(hung) + ("\n" if hung else ""))
with open(os.path.join(args.outdir, "unrun.txt"), "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(remaining) + ("\n" if remaining else ""))
if skipped:
with open(os.path.join(args.outdir, "skipped.txt"), "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(skipped) + "\n")
if remaining:
print(f"[run_cts_local] WARNING: {len(remaining)} cases were never run (see unrun.txt)",
file=sys.stderr)
print(f"[run_cts_local] finished: {len(done)}/{total} cases, {len(crashed)} crashes, "
f"{len(hung)} hangs, {chunk} invocations")
print(f"[run_cts_local] qpa chunks in {args.outdir}")
return 0
if __name__ == "__main__":
sys.exit(main())