mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
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.
228 lines
10 KiB
Python
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())
|