From a69885459dffb6b8bf7e820cadda8f3074c5f3fe Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Sat, 17 Oct 2026 11:17:20 +0200 Subject: [PATCH] smoke-tests: Share code in smoketest.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The five smoke tests carry copies of the same helpers: report() and the PASS/FAIL summary in all of them, run() and last_line() in the llvm and bare-metal Arm tests, the --prefix unpack and overlay re-exec in those two and the Mesa test, start_xvfb() in the Mesa and WebKitGTK tests, and the vm.overcommit_memory check in the llvm and WebKitGTK tests. The copies already drifted: report() counts only fail and warn in the FreeType test, but anything not ok elsewhere. Move them into scripts/smoke-tests/smoketest.py. The scripts have no .py extension, so each puts its own directory on sys.path and imports the module, with sys.dont_write_bytecode set so that no __pycache__ shows up in the checkout. What differs between the copies becomes an argument: the pattern last_line() looks for, the timeout of run() (120 s for the bare-metal Arm test), the Xvfb arguments, and which directories --prefix overlays (/usr and /etc for Mesa, which does not require usr/). This is a refactoring only. freetype-smoke-test and webkitgtk-smoke-test keep their own --prefix, which takes one image directory, and only share the reporting, Xvfb, the namespace command and the overcommit check. The one change in behaviour: webkitgtk-smoke-test without $DISPLAY and without xdpyinfo now exits with a message instead of a traceback. Every test, from master and from this commit, on the same MXQ node (galah, vm.overcommit_memory=2, TMPDIR=$MXQ_JOB_TMPDIR), output compared after normalizing the random temporary directory names: SAME llvm: PASS (0 failed, 2 warned) exit 0 SAME llvm-prefix: […] exit 32 SAME arm: PASS (0 failed, 0 warned) exit 0 SAME arm-prefix: PASS (0 failed, 0 warned) exit 0 SAME mesa: PASS (0 failed, 2 warned) exit 0 SAME mesa-prefix: […] exit 32 DIFFER webkit-6.0 DIFFER webkit-4.1 SAME freetype: PASS (0 failed, 0 warned), files in […] exit 0 The WebKitGTK runs differ only in the free commit memory: -OK strict overcommit, but 1039.0 GB of the 1039.2 GB commit limit is free (a session needs 22 GB at the least) +OK strict overcommit, but 1031.1 GB of the 1039.2 GB commit limit is free (a session needs 22 GB at the least) llvm and mesa with five --prefix packages fail on master the same way, before any test runs: mount: /usr: wrong fs type, bad option, bad superblock on overlay, missing codepage or helper program, or other error. So instead, the overlay code of master and of this commit was run on the same two dummy layers, lower/0 with usr/ and lower/1 with usr/ and etc/, with subprocess.run replaced by a stub that only records its command. The recorded `unshare` commands differ only in the path of the script, also for Mesa's /etc overlay. pyflakes reports nothing, before and after. Assisted-by: claude-opus-5.5 (Claude Code) --- scripts/smoke-tests/baremetal-arm-smoke-test | 112 +++--------- scripts/smoke-tests/freetype-smoke-test | 17 +- scripts/smoke-tests/llvm-smoke-test | 115 ++----------- scripts/smoke-tests/mesa-smoke-test | 110 ++---------- scripts/smoke-tests/smoketest.py | 171 +++++++++++++++++++ scripts/smoke-tests/webkitgtk-smoke-test | 64 +++---- 6 files changed, 245 insertions(+), 344 deletions(-) create mode 100644 scripts/smoke-tests/smoketest.py diff --git a/scripts/smoke-tests/baremetal-arm-smoke-test b/scripts/smoke-tests/baremetal-arm-smoke-test index b18c9ba99..f34c346c5 100755 --- a/scripts/smoke-tests/baremetal-arm-smoke-test +++ b/scripts/smoke-tests/baremetal-arm-smoke-test @@ -41,17 +41,26 @@ # not run, and that is reported as WARN. Failures exit non-zero. import argparse +import functools import os import re import shlex import shutil -import subprocess import sys import tempfile +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) +sys.dont_write_bytecode = True +import smoketest # noqa: E402 +from smoketest import (last_line, overlay_prefixes, report, # noqa: E402 + summary) + # How --prefix survives the re-exec into the mount namespace. PREFIX_VARIABLE = "BAREMETAL_ARM_SMOKE_TEST_PREFIX" +# What says why a link or a QEMU run failed, for last_line. +ERROR = r"error|cannot find|undefined symbol|unrecognised|fault" + RUNTIMES = "/usr/lib/clang-runtimes" HELLO = r""" @@ -138,80 +147,8 @@ PROGRAMS = [ MPS2_AN505), ] -status = {"fail": 0, "warn": 0} - - -def report(state, message): - if state != "ok": - status[state] += 1 - print("%-5s %s" % (state.upper(), message)) - - -def run(command, **kwargs): - """Run command, capturing its output, never raising for its exit status.""" - try: - return subprocess.run(command, capture_output=True, text=True, - timeout=120, **kwargs) - except (OSError, subprocess.TimeoutExpired) as error: - return subprocess.CompletedProcess(command, 127, "", str(error)) - - -def last_line(process): - """The line of the output that says what went wrong.""" - lines = (process.stderr or process.stdout).strip().splitlines() - for line in lines: - # The linker says why before clang says that it failed. - if re.search(r"error|cannot find|undefined symbol|unrecognised|fault", - line) and \ - "linker command failed" not in line: - return line.strip() - return lines[-1] if lines else "exit status %d" % process.returncode - - -# ---------------------------------------------------------------- --prefix - -def unpack(items, workdir): - """Return the directories to overlay, unpacking package archives.""" - lowers = [] - for number, item in enumerate(items): - if os.path.isdir(item): - path = os.path.abspath(item.rstrip("/")) - elif os.path.isfile(item): - path = os.path.join(workdir, str(number)) - os.mkdir(path) - print(" unpacking %s" % item, flush=True) - process = subprocess.run(["tar", "-C", path, "-xf", item], - capture_output=True, text=True) - if process.returncode: - sys.exit("cannot unpack %s: %s" % (item, process.stderr)) - else: - sys.exit("no such directory or package: %s" % item) - if ":" in path or "," in path: - sys.exit("cannot overlay %s: overlayfs separates layers by ':'" - " and options by ','" % path) - if not os.path.isdir(path + "/usr"): - sys.exit("%s has no usr/" % item) - lowers.append(path) - return lowers - - -def run_in_overlay(lowers): - """Run this script again with lowers mounted over /usr.""" - layers = ":".join([lower + "/usr" for lower in lowers] + ["/usr"]) - setup = "set -e\nmount -t overlay overlay -o lowerdir=%s /usr\nexec \"$@\"" \ - % shlex.quote(layers) - environment = dict(os.environ) - environment[PREFIX_VARIABLE] = ":".join(lowers) - command = ["unshare", "--user", "--map-root-user", "--mount", - "--propagation", "slave", - "sh", "-c", setup, "sh", sys.executable, - os.path.abspath(__file__)] + sys.argv[1:] - try: - return subprocess.run(command, env=environment).returncode - except OSError as error: - sys.exit("cannot re-run in a mount namespace (%s); install the" - " packages and run without --prefix" % error) - +# Shorter than smoketest's default: nothing here compiles for long. +run = functools.partial(smoketest.run, timeout=120) # ----------------------------------------------------------------- checks @@ -278,7 +215,8 @@ def check_program(name, flags, machine, memory, qemu, machines, directory): os.path.join(RUNTIMES, variant, "lib/crt0-semihost.o"), "-o", output, "hello.c", "-lsemihost"], cwd=directory) if process.returncode: - report("fail", "%s: link failed: %s" % (name, last_line(process))) + report("fail", "%s: link failed: %s" + % (name, last_line(process, ERROR))) return if machine not in machines: report("warn", "%s: linked, but not run: %s has no %s board" @@ -293,7 +231,7 @@ def check_program(name, flags, machine, memory, qemu, machines, directory): if process.returncode or EXPECT not in printed: report("fail", "%s on %s: exit status %d: %s" % (name, machine, process.returncode, - last_line(process) if process.returncode else + last_line(process, ERROR) if process.returncode else printed.strip())) else: report("ok", "%s runs on %s with %s" % (name, machine, variant)) @@ -307,7 +245,8 @@ def check_default_link(directory): ["-o", "default.elf", "hello.c", "-lsemihost"] process = run(command, cwd=directory) if process.returncode: - report("fail", "%s: link failed: %s" % (name, last_line(process))) + report("fail", "%s: link failed: %s" + % (name, last_line(process, ERROR))) return variant = os.path.join(RUNTIMES, VARIANTS[3], "lib") # clang spells the paths /usr/bin/../lib/clang-runtimes/... @@ -337,15 +276,9 @@ def main(): " (default: from PATH)") arguments = parser.parse_args() - if arguments.prefix and PREFIX_VARIABLE not in os.environ: - workdir = tempfile.mkdtemp(prefix="baremetal-arm-smoke-") - try: - return run_in_overlay(unpack(arguments.prefix, workdir)) - finally: - shutil.rmtree(workdir, ignore_errors=True) - if PREFIX_VARIABLE in os.environ: - print(" testing, over /usr: %s" - % os.environ[PREFIX_VARIABLE].replace(":", " ")) + returncode = overlay_prefixes(arguments.prefix, PREFIX_VARIABLE) + if returncode is not None: + return returncode machines = qemu_machines(arguments.qemu) if arguments.qemu: @@ -367,10 +300,7 @@ def main(): finally: shutil.rmtree(directory, ignore_errors=True) - print("%s (%d failed, %d warned)" - % ("FAIL" if status["fail"] else "PASS", status["fail"], - status["warn"])) - return 1 if status["fail"] else 0 + return summary() if __name__ == "__main__": diff --git a/scripts/smoke-tests/freetype-smoke-test b/scripts/smoke-tests/freetype-smoke-test index b7158a0cf..f55e1fa0a 100755 --- a/scripts/smoke-tests/freetype-smoke-test +++ b/scripts/smoke-tests/freetype-smoke-test @@ -47,6 +47,10 @@ import subprocess import sys import tempfile +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) +sys.dont_write_bytecode = True +from smoketest import report, summary # noqa: E402 + # configure links these when it finds them, and 2.10.4 linked all of them. NEEDED = ["libbz2.so.1.0", "libpng16.so.16", "libz.so.1", "libharfbuzz.so.0", "libbrotlidec.so."] @@ -425,13 +429,6 @@ int main(int argc, char **argv) """ root = "" -status = {"fail": 0, "warn": 0} - - -def report(state, message): - if state in status: - status[state] += 1 - print("%-5s %s" % (state.upper(), message)) def environment(tree=None): @@ -1041,16 +1038,14 @@ def main(): comparisons = check_rendering(binary, directory, fonts, arguments) check_fcscan(fonts) - print("%s (%d failed, %d warned), files in %s" - % ("FAIL" if status["fail"] else "PASS", - status["fail"], status["warn"], directory)) + returncode = summary(directory) if comparisons and os.environ.get("DISPLAY") and not arguments.no_view: try: view(comparisons) # ImportError without tkinter, tkinter.TclError without a display. except Exception as error: print("cannot show the rendering: %s" % error) - return 1 if status["fail"] else 0 + return returncode if __name__ == "__main__": diff --git a/scripts/smoke-tests/llvm-smoke-test b/scripts/smoke-tests/llvm-smoke-test index 23141297c..d88958bc7 100755 --- a/scripts/smoke-tests/llvm-smoke-test +++ b/scripts/smoke-tests/llvm-smoke-test @@ -40,15 +40,21 @@ import argparse import os import re -import shlex import shutil -import subprocess import sys import tempfile +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) +sys.dont_write_bytecode = True +from smoketest import (last_line, overlay_prefixes, report, run, # noqa: E402 + strict_overcommit, summary) + # How --prefix survives the re-exec into the mount namespace. PREFIX_VARIABLE = "LLVM_SMOKE_TEST_PREFIX" +# What says why a compile failed, for last_line. +ERROR = r"error|cannot find|undefined reference" + SOURCES = { "hello.c": r""" #include @@ -109,80 +115,6 @@ kernel void k(global float *out) { """, } -status = {"fail": 0, "warn": 0} - - -def report(state, message): - if state != "ok": - status[state] += 1 - print("%-5s %s" % (state.upper(), message)) - - -def run(command, **kwargs): - """Run command, capturing its output, never raising for its exit status.""" - try: - return subprocess.run(command, capture_output=True, text=True, - timeout=600, **kwargs) - except (OSError, subprocess.TimeoutExpired) as error: - return subprocess.CompletedProcess(command, 127, "", str(error)) - - -def last_line(process): - """The line of the output that says what went wrong.""" - lines = (process.stderr or process.stdout).strip().splitlines() - for line in lines: - # The linker says why before clang says that it failed. - if re.search(r"error|cannot find|undefined reference", line) and \ - "linker command failed" not in line: - return line.strip() - return lines[-1] if lines else "exit status %d" % process.returncode - - -# ---------------------------------------------------------------- --prefix - -def unpack(items, workdir): - """Return the directories to overlay, unpacking package archives.""" - lowers = [] - for number, item in enumerate(items): - if os.path.isdir(item): - path = os.path.abspath(item.rstrip("/")) - elif os.path.isfile(item): - path = os.path.join(workdir, str(number)) - os.mkdir(path) - print(" unpacking %s" % item, flush=True) - process = subprocess.run(["tar", "-C", path, "-xf", item], - capture_output=True, text=True) - if process.returncode: - sys.exit("cannot unpack %s: %s" % (item, process.stderr)) - else: - sys.exit("no such directory or package: %s" % item) - if ":" in path or "," in path: - sys.exit("cannot overlay %s: overlayfs separates layers by ':'" - " and options by ','" % path) - if not os.path.isdir(path + "/usr"): - sys.exit("%s has no usr/" % item) - lowers.append(path) - return lowers - - -def run_in_overlay(lowers): - """Run this script again with lowers mounted over /usr.""" - layers = ":".join([lower + "/usr" for lower in lowers] + ["/usr"]) - setup = "set -e\nmount -t overlay overlay -o lowerdir=%s /usr\nexec \"$@\"" \ - % shlex.quote(layers) - environment = dict(os.environ) - environment[PREFIX_VARIABLE] = ":".join(lowers) - command = ["unshare", "--user", "--map-root-user", "--mount", - "--propagation", "slave", - "sh", "-c", setup, "sh", sys.executable, - os.path.abspath(__file__)] + sys.argv[1:] - try: - return subprocess.run(command, env=environment).returncode - except OSError as error: - sys.exit("cannot re-run in a mount namespace (%s); install the" - " packages and run without --prefix" % error) - - # ----------------------------------------------------------------- checks def major(version): @@ -240,7 +172,7 @@ def build(name, command, directory): process = run(command, cwd=directory) if process.returncode: report("fail", "%s: %s failed: %s" - % (name, " ".join(command[:1]), last_line(process))) + % (name, " ".join(command[:1]), last_line(process, ERROR))) return False return True @@ -272,14 +204,6 @@ def check_lld(version, directory): report("ok", "%s, .comment names LLD %s" % (name, version)) -def strict_overcommit(): - try: - with open("/proc/sys/vm/overcommit_memory") as sysctl: - return sysctl.read().strip() == "2" - except OSError: - return False - - def check_sanitizer(name, flags, source, expect, directory): output = name.lower() if not build(name, ["clang", "-g"] + flags + ["-o", output, source], @@ -294,7 +218,7 @@ def check_sanitizer(name, flags, source, expect, directory): report("ok", "%s reports %s" % (name, expect)) else: report("fail", "%s: expected %r, got exit status %d: %s" - % (name, expect, process.returncode, last_line(process))) + % (name, expect, process.returncode, last_line(process, ERROR))) def check_profile(directory): @@ -313,7 +237,7 @@ def check_profile(directory): env=dict(os.environ, **environment)) if process.returncode: report("fail", "%s: %s failed: %s" - % (name, command[0], last_line(process))) + % (name, command[0], last_line(process, ERROR))) return if "TOTAL" in process.stdout: report("ok", "%s: compiler-rt profile, llvm-profdata, llvm-cov" % name) @@ -358,15 +282,9 @@ def main(): " installed; may be repeated") arguments = parser.parse_args() - if arguments.prefix and PREFIX_VARIABLE not in os.environ: - workdir = tempfile.mkdtemp(prefix="llvm-smoke-") - try: - return run_in_overlay(unpack(arguments.prefix, workdir)) - finally: - shutil.rmtree(workdir, ignore_errors=True) - if PREFIX_VARIABLE in os.environ: - print(" testing, over /usr: %s" - % os.environ[PREFIX_VARIABLE].replace(":", " ")) + returncode = overlay_prefixes(arguments.prefix, PREFIX_VARIABLE) + if returncode is not None: + return returncode version = check_versions() if not version: @@ -398,10 +316,7 @@ def main(): finally: shutil.rmtree(directory, ignore_errors=True) - print("%s (%d failed, %d warned)" - % ("FAIL" if status["fail"] else "PASS", status["fail"], - status["warn"])) - return 1 if status["fail"] else 0 + return summary() if __name__ == "__main__": diff --git a/scripts/smoke-tests/mesa-smoke-test b/scripts/smoke-tests/mesa-smoke-test index dbe645413..47cbf06a6 100755 --- a/scripts/smoke-tests/mesa-smoke-test +++ b/scripts/smoke-tests/mesa-smoke-test @@ -43,12 +43,14 @@ import glob import json import math import os -import shlex import shutil import subprocess import sys -import tempfile -import time + +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) +sys.dont_write_bytecode = True +from smoketest import (overlay_prefixes, report, start_xvfb, # noqa: E402 + summary) # How --prefix survives the re-exec into the mount namespace. PREFIX_VARIABLE = "MESA_SMOKE_TEST_PREFIX" @@ -63,64 +65,6 @@ VULKAN_ICD_DIRS = ("/usr/share/vulkan/icd.d", "/etc/vulkan/icd.d") LIBCLC_MISSING = "Libclc failed to load" LIBCLC_UPSTREAM = "Patched Mesa libclc not detected" -status = {"fail": 0, "warn": 0} - - -def report(state, message): - if state != "ok": - status[state] += 1 - print("%-5s %s" % (state.upper(), message)) - - -# ---------------------------------------------------------------- --prefix - -def unpack(items, workdir): - """Return the directories to overlay, unpacking package archives.""" - lowers = [] - for number, item in enumerate(items): - if os.path.isdir(item): - path = os.path.abspath(item.rstrip("/")) - elif os.path.isfile(item): - path = os.path.join(workdir, str(number)) - os.mkdir(path) - print(" unpacking %s" % item, flush=True) - process = subprocess.run(["tar", "-C", path, "-xf", item], - capture_output=True, text=True) - if process.returncode: - sys.exit("cannot unpack %s: %s" % (item, process.stderr)) - else: - sys.exit("no such directory or package: %s" % item) - if ":" in path or "," in path: - sys.exit("cannot overlay %s: overlayfs separates layers by ':'" - " and options by ','" % path) - lowers.append(path) - return lowers - - -def run_in_overlay(lowers): - """Run this script again with lowers mounted over /usr and /etc.""" - setup = ["set -e"] - for directory in ("usr", "etc"): - layers = [lower + "/" + directory for lower in lowers - if os.path.isdir(lower + "/" + directory)] - if layers: - setup.append("mount -t overlay overlay -o lowerdir=%s /%s" - % (shlex.quote(":".join(layers + ["/" + directory])), - directory)) - setup.append('exec "$@"') - environment = dict(os.environ) - environment[PREFIX_VARIABLE] = ":".join(lowers) - command = ["unshare", "--user", "--map-root-user", "--mount", - "--propagation", "slave", - "sh", "-c", "\n".join(setup), "sh", sys.executable, - os.path.abspath(__file__)] + sys.argv[1:] - try: - return subprocess.run(command, env=environment).returncode - except OSError as error: - sys.exit("cannot re-run in a mount namespace (%s); install the" - " packages and run without --prefix" % error) - - # --------------------------------------------------------------- children # # Every check that loads a driver runs in a child process of this script, so @@ -653,34 +597,12 @@ def check_gl(): % (label, name)) -def start_xvfb(): - """Return (display, process) for a private X server, or (None, None).""" - if not shutil.which("Xvfb") or not shutil.which("xdpyinfo"): - return None, None - for number in range(77, 97): - if os.path.exists("/tmp/.X%d-lock" % number): - continue - display = ":%d" % number - process = subprocess.Popen( - ["Xvfb", display, "-nolisten", "tcp", "-screen", "0", "640x480x24"], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - for _ in range(50): - time.sleep(.2) - if process.poll() is not None: - break - if subprocess.run(["xdpyinfo", "-display", display], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL).returncode == 0: - return display, process - process.kill() - return None, None - - def check_glx(): if not shutil.which("glxinfo"): report("warn", "GLX: no glxinfo") return - display, xvfb = start_xvfb() + display, xvfb = start_xvfb(["-nolisten", "tcp", "-screen", "0", + "640x480x24"]) if not display: report("warn", "GLX: cannot start Xvfb") return @@ -792,15 +714,10 @@ def main(): print(json.dumps(CHILDREN[arguments.child]())) return 0 - if arguments.prefix and PREFIX_VARIABLE not in os.environ: - workdir = tempfile.mkdtemp(prefix="mesa-smoke-") - try: - return run_in_overlay(unpack(arguments.prefix, workdir)) - finally: - shutil.rmtree(workdir, ignore_errors=True) - if PREFIX_VARIABLE in os.environ: - print(" testing, over /usr and /etc: %s" - % os.environ[PREFIX_VARIABLE].replace(":", " ")) + returncode = overlay_prefixes(arguments.prefix, PREFIX_VARIABLE, + directories=("usr", "etc"), required=None) + if returncode is not None: + return returncode check_libraries() check_libclc() @@ -809,10 +726,7 @@ def main(): check_vulkan() check_opencl(arguments.rusticl_enable) - print("%s (%d failed, %d warned)" - % ("FAIL" if status["fail"] else "PASS", status["fail"], - status["warn"])) - return 1 if status["fail"] else 0 + return summary() if __name__ == "__main__": diff --git a/scripts/smoke-tests/smoketest.py b/scripts/smoke-tests/smoketest.py new file mode 100644 index 000000000..7426ffb0a --- /dev/null +++ b/scripts/smoke-tests/smoketest.py @@ -0,0 +1,171 @@ +# What the smoke tests in this directory share: reporting, running commands, +# --prefix in a mount namespace, and a private Xvfb. +# +# The scripts have no .py extension, so they cannot be imported from each +# other; each puts this directory on sys.path and imports this module instead. + +import os +import re +import shlex +import shutil +import subprocess +import sys +import tempfile +import time + +# The script that imported this module, which the namespace runs again. +SCRIPT = os.path.abspath(sys.modules["__main__"].__file__) + +status = {"fail": 0, "warn": 0} + + +def report(state, message): + """Print a result; fail and warn are counted, ok and info are not.""" + if state in status: + status[state] += 1 + print("%-5s %s" % (state.upper(), message)) + + +def summary(directory=None): + """Print the verdict, naming directory if the files are kept; return the + exit status.""" + print("%s (%d failed, %d warned)%s" + % ("FAIL" if status["fail"] else "PASS", status["fail"], + status["warn"], ", files in %s" % directory if directory else "")) + return 1 if status["fail"] else 0 + + +def run(command, timeout=600, **kwargs): + """Run command, capturing its output, never raising for its exit status.""" + try: + return subprocess.run(command, capture_output=True, text=True, + timeout=timeout, **kwargs) + except (OSError, subprocess.TimeoutExpired) as error: + return subprocess.CompletedProcess(command, 127, "", str(error)) + + +def last_line(process, pattern): + """The line of the output that says what went wrong: the first one + matching pattern, or else the last one.""" + lines = (process.stderr or process.stdout).strip().splitlines() + for line in lines: + # The linker says why before clang says that it failed. + if re.search(pattern, line) and "linker command failed" not in line: + return line.strip() + return lines[-1] if lines else "exit status %d" % process.returncode + + +def strict_overcommit(): + """Whether vm.overcommit_memory=2 accounts every reserved byte.""" + try: + with open("/proc/sys/vm/overcommit_memory") as sysctl: + return sysctl.read().strip() == "2" + except OSError: + return False + + +def start_xvfb(arguments): + """Return (display, process) for a private X server started with + arguments, or (None, None).""" + if not shutil.which("Xvfb") or not shutil.which("xdpyinfo"): + return None, None + for number in range(77, 97): + if os.path.exists("/tmp/.X%d-lock" % number): + continue + display = ":%d" % number + process = subprocess.Popen(["Xvfb", display] + arguments, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + for _ in range(50): + time.sleep(.2) + if process.poll() is not None: + break + if subprocess.run(["xdpyinfo", "-display", display], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL).returncode == 0: + return display, process + process.kill() + return None, None + + +# ---------------------------------------------------------------- --prefix + +def namespace_command(setup): + """The command running the script again, with its arguments, after the + shell commands setup, in a user and mount namespace of its own. + + Mount propagation is slave, so that automounts from the host, such as /pkg, + reach the namespace, but nothing mounted inside leaks out.""" + return ["unshare", "--user", "--map-root-user", "--mount", + "--propagation", "slave", + "sh", "-c", setup, "sh", sys.executable, SCRIPT] + sys.argv[1:] + + +def unpack(items, workdir, required="usr"): + """Return the directories to overlay, unpacking package archives into + workdir. Each has to have the directory required, unless that is None.""" + lowers = [] + for number, item in enumerate(items): + if os.path.isdir(item): + path = os.path.abspath(item.rstrip("/")) + elif os.path.isfile(item): + path = os.path.join(workdir, str(number)) + os.mkdir(path) + print(" unpacking %s" % item, flush=True) + process = subprocess.run(["tar", "-C", path, "-xf", item], + capture_output=True, text=True) + if process.returncode: + sys.exit("cannot unpack %s: %s" % (item, process.stderr)) + else: + sys.exit("no such directory or package: %s" % item) + if ":" in path or "," in path: + sys.exit("cannot overlay %s: overlayfs separates layers by ':'" + " and options by ','" % path) + if required and not os.path.isdir(path + "/" + required): + sys.exit("%s has no %s/" % (item, required)) + lowers.append(path) + return lowers + + +def run_in_overlay(lowers, variable, directories): + """Run the script again with lowers mounted over directories, and their + paths in the environment variable.""" + setup = ["set -e"] + for directory in directories: + layers = [lower + "/" + directory for lower in lowers + if os.path.isdir(lower + "/" + directory)] + if layers: + setup.append("mount -t overlay overlay -o lowerdir=%s /%s" + % (shlex.quote(":".join(layers + ["/" + directory])), + directory)) + setup.append('exec "$@"') + environment = dict(os.environ) + environment[variable] = ":".join(lowers) + try: + return subprocess.run(namespace_command("\n".join(setup)), + env=environment).returncode + except OSError as error: + sys.exit("cannot re-run in a mount namespace (%s); install the" + " packages and run without --prefix" % error) + + +def overlay_prefixes(items, variable, directories=("usr",), required="usr"): + """Implement --prefix: run the script again with the image directories or + .bee.tar.bz2 packages items, unpacked below $TMPDIR, mounted read-only over + directories, and return its exit status. variable tells the run inside + that it is; there, and without items, return None to carry on testing.""" + if variable in os.environ: + print(" testing, over %s: %s" + % (" and ".join("/" + directory for directory in directories), + os.environ[variable].replace(":", " "))) + return None + if not items: + return None + # llvm-smoke-test unpacks into llvm-smoke-XXXXXXXX. + workdir = tempfile.mkdtemp( + prefix=re.sub(r"(-test)?$", "-", os.path.basename(SCRIPT), count=1)) + try: + return run_in_overlay(unpack(items, workdir, required), variable, + directories) + finally: + shutil.rmtree(workdir, ignore_errors=True) diff --git a/scripts/smoke-tests/webkitgtk-smoke-test b/scripts/smoke-tests/webkitgtk-smoke-test index c885650b1..e999ed2bd 100755 --- a/scripts/smoke-tests/webkitgtk-smoke-test +++ b/scripts/smoke-tests/webkitgtk-smoke-test @@ -35,6 +35,12 @@ import sys import tempfile import time +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) +sys.dont_write_bytecode = True +import smoketest # noqa: E402 +from smoketest import (namespace_command, report, # noqa: E402 + strict_overcommit, summary) + # The two flavours built from the same sources: webkitgtk.be0 builds the GTK 4 # one, which upstream calls API version 6.0, and webkit2gtk.be0 the GTK 3 one, # API version 4.1. Everything is named after the API version, so both can be @@ -139,15 +145,6 @@ OPTIONAL = ["webgl"] COMMIT_PER_PROCESS_KB = 5.5 * 1024 * 1024 SESSION_PROCESSES = 4 -status = {"fail": 0, "warn": 0} - - -def report(state, message): - if state != "ok": - status[state] += 1 - print("%-5s %s" % (state.upper(), message)) - - def libexecdir(): """Where the package installs MiniBrowser, jsc and the child processes.""" for candidate in ("/usr/libexec/", "/usr/lib/"): @@ -238,12 +235,8 @@ def check_overcommit(): Nothing here is a packaging bug, hence WARN: the machine is too small for WebKit's reservations, whatever its `MemTotal' says. """ - try: - with open("/proc/sys/vm/overcommit_memory") as sysctl: - if int(sysctl.read().strip()) != 2: - return True # MAP_NORESERVE is honoured; nothing to check. - except (OSError, ValueError): - return True + if not strict_overcommit(): + return True # MAP_NORESERVE is honoured; nothing to check. meminfo = {} try: @@ -274,29 +267,18 @@ def check_overcommit(): def start_xvfb(): - """Return (display, process) for a private X server, or (None, None).""" + """Return (display, process) for $DISPLAY, without a process, or else for + a private X server, which $DISPLAY then names.""" if os.environ.get("DISPLAY"): return os.environ["DISPLAY"], None - if not shutil.which("Xvfb"): - sys.exit("no $DISPLAY and no Xvfb -- run this on a machine with X") - for number in range(77, 97): - if os.path.exists("/tmp/.X%d-lock" % number): - continue - display = ":%d" % number - process = subprocess.Popen( - ["Xvfb", display, "-screen", "0", "1280x1024x24"], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - for _ in range(50): - time.sleep(.2) - if process.poll() is not None: - break - if subprocess.run(["xdpyinfo", "-display", display], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL).returncode == 0: - os.environ["DISPLAY"] = display - return display, process - process.kill() - sys.exit("could not start Xvfb") + if not shutil.which("Xvfb") or not shutil.which("xdpyinfo"): + sys.exit("no $DISPLAY and no Xvfb or xdpyinfo -- run this on a machine" + " with X") + display, process = smoketest.start_xvfb(["-screen", "0", "1280x1024x24"]) + if not display: + sys.exit("could not start Xvfb") + os.environ["DISPLAY"] = display + return display, process def load(uri, snapshot=None, script="JSON.stringify(window.smoke)"): @@ -477,10 +459,7 @@ mount -t overlay overlay \ ln -s %s /usr/libexec/%s exec "$@" """ % (shlex.quote(libexec), api["subdir"]) - command = ["unshare", "--user", "--map-root-user", "--mount", - "--propagation", "slave", - "sh", "-c", setup, "sh", sys.executable, - os.path.abspath(__file__)] + sys.argv[1:] + command = namespace_command(setup) try: os.execvpe(command[0], command, environment) except OSError as error: @@ -550,10 +529,7 @@ def main(): xvfb.terminate() xvfb.wait() - print("%s (%d failed, %d warned), files in %s" - % ("FAIL" if status["fail"] else "PASS", - status["fail"], status["warn"], directory)) - return 1 if status["fail"] else 0 + return summary(directory) if __name__ == "__main__":