From 94c5e361fbb229b0cd6853e2102a7d95f1f358b3 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Mon, 12 Oct 2026 22:12:02 +0300 Subject: better public interface and doxygen --- tests/helpers/check_exports.py | 11 ++++-- tests/helpers/package_cocoa_tests.py | 70 ++++++++++++++++++++++++++++++++++++ tests/helpers/run_backends.py | 8 ++++- tests/helpers/run_cocoa_bundle.py | 68 +++++++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 tests/helpers/package_cocoa_tests.py create mode 100644 tests/helpers/run_cocoa_bundle.py (limited to 'tests/helpers') diff --git a/tests/helpers/check_exports.py b/tests/helpers/check_exports.py index 0d10014..acb727d 100644 --- a/tests/helpers/check_exports.py +++ b/tests/helpers/check_exports.py @@ -9,6 +9,7 @@ import subprocess def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--allow-export', action='append', default=[]) + parser.add_argument('--platform', choices=['linux', 'darwin'], default='linux') parser.add_argument('nm') parser.add_argument('library') parser.add_argument('headers', nargs='+') @@ -23,10 +24,14 @@ def main(): if not expected: raise SystemExit("No public function declarations found") - output = subprocess.check_output( - [args.nm, "-D", "--defined-only", "--format=posix", args.library], text=True - ) + nm_options = ["-g", "-U", "-P"] if args.platform == 'darwin' else [ + "-D", "--defined-only", "--format=posix" + ] + output = subprocess.check_output([args.nm, *nm_options, args.library], text=True) exported = {line.split()[0] for line in output.splitlines() if line.strip()} + if args.platform == 'darwin': + # Mach-O decorates C symbols with one leading underscore. + exported = {name[1:] if name.startswith('_') else name for name in exported} missing = expected - exported leaked = exported - expected - set(args.allow_export) if missing or leaked: diff --git a/tests/helpers/package_cocoa_tests.py b/tests/helpers/package_cocoa_tests.py new file mode 100644 index 0000000..8f801f8 --- /dev/null +++ b/tests/helpers/package_cocoa_tests.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Package compiled Cocoa tests for execution on a matching macOS desktop.""" + +import argparse +import hashlib +import json +from pathlib import Path +import shutil +import tarfile +import tempfile + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('build', type=Path) + parser.add_argument('output', type=Path) + args = parser.parse_args() + build = args.build.resolve() + info = build / 'meson-info' + host = json.loads((info / 'intro-machines.json').read_text())['host'] + if host['system'] != 'darwin': + parser.error('Use a configured and compiled Cocoa build.') + tests = json.loads((info / 'intro-tests.json').read_text()) + required = {'native', 'native-ownership', 'keyboard-cocoa', 'tablet-cocoa', + 'cursor-cocoa', 'hello-cocoa-window', 'tablet-cocoa-window'} + missing = required - {test['name'] for test in tests} + if missing: + parser.error('Enable integration_tests; missing: ' + ', '.join(sorted(missing))) + options = json.loads((info / 'intro-buildoptions.json').read_text()) + sanitizer = next(option['value'] for option in options if option['name'] == 'b_sanitize') + if sanitizer not in ('none', [], ['none']): + parser.error('Use an unsanitized build; sanitizer runtimes are not bundled.') + + with tempfile.TemporaryDirectory(prefix='yasl-cocoa-bundle-') as directory: + root = Path(directory) / 'yasl-cocoa-tests' + root.mkdir() + files = {} + + def copy(source, relative): + destination = root / relative + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + files[str(relative)] = hashlib.sha256(destination.read_bytes()).hexdigest() + + copy(build / 'libyasl.dylib', Path('libyasl.dylib')) + source_root = Path(__file__).resolve().parents[2] + copy(source_root / 'LICENSE', Path('LICENSE')) + copy(Path(__file__).with_name('run_cocoa_bundle.py'), Path('run.py')) + commands = [] + for test in tests: + if test['name'] == 'public-exports': + continue # Check the dylib with nm on the build machine first. + executable = Path(test['cmd'][0]).resolve() + try: + relative = executable.relative_to(build) + except ValueError: + parser.error('Unexpected external test command: ' + str(executable)) + copy(executable, relative) + commands.append({'name': test['name'], 'cmd': [str(relative), *test['cmd'][1:]], + 'timeout': test['timeout']}) + manifest = {'architecture': host['cpu_family'], 'minimum_macos': 14, + 'tests': commands, 'sha256': files} + (root / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n') + with tarfile.open(args.output, 'w:gz') as archive: + archive.add(root, arcname=root.name) + print(f'Packaged {len(commands)} runtime tests for {host["cpu_family"]}: {args.output}') + + +if __name__ == '__main__': + main() diff --git a/tests/helpers/run_backends.py b/tests/helpers/run_backends.py index 57cc9ab..70c82fb 100644 --- a/tests/helpers/run_backends.py +++ b/tests/helpers/run_backends.py @@ -21,7 +21,7 @@ def wait_for(process, ready): def main(): - weston, xwayland, test, timers, tablet = sys.argv[1:] + weston, xwayland, test, timers, tablet, hello, input_x11 = sys.argv[1:] servers = [] with tempfile.TemporaryDirectory(prefix="yasl-backends-") as directory: root = Path(directory) @@ -80,10 +80,16 @@ def main(): x_env["XDG_SESSION_TYPE"] = "x11" print("Test: X11 surface timers", flush=True) subprocess.run([timers], env=x_env, check=True, timeout=8) + print("Test: X11 UTF-8 keyboard fallback and clipboard", flush=True) + subprocess.run([input_x11], env=x_env, check=True, timeout=8) for backend in ("wayland", "x11"): print("Test: tablet example on", backend, flush=True) subprocess.run([tablet, "--backend", backend, "--smoke-test"], env=env, check=True, timeout=8) + print("Test: hello example on", backend, flush=True) + subprocess.run([hello, "--smoke-test"], + env=env if backend == "wayland" else x_env, + check=True, timeout=8) except BaseException: for log in (wl_log, x_log): log.flush() diff --git a/tests/helpers/run_cocoa_bundle.py b/tests/helpers/run_cocoa_bundle.py new file mode 100644 index 0000000..338f38e --- /dev/null +++ b/tests/helpers/run_cocoa_bundle.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Run a packaged Cocoa suite, preserving output and a machine-readable report.""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import platform +import re +import subprocess +import sys + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--list', action='store_true', help='List tests without executing them.') + args = parser.parse_args() + root = Path(__file__).resolve().parent + manifest = json.loads((root / 'manifest.json').read_text()) + if args.list: + print('Architecture:', manifest['architecture']) + for test in manifest['tests']: + print(test['name']) + return + if sys.platform != 'darwin': + parser.error('Run this suite in a macOS desktop session.') + if int(platform.mac_ver()[0].split('.')[0]) < manifest['minimum_macos']: + parser.error('This suite requires macOS 14 or newer.') + architecture = {'arm64': 'aarch64'}.get(platform.machine(), platform.machine()) + if architecture != manifest['architecture']: + parser.error('Use the bundle matching this Python process architecture.') + for relative, expected in manifest['sha256'].items(): + if hashlib.sha256((root / relative).read_bytes()).hexdigest() != expected: + parser.error('Bundle file changed: ' + relative) + + logs = root / 'logs' + logs.mkdir(exist_ok=True) + env = os.environ.copy() + env['DYLD_LIBRARY_PATH'] = str(root) + results = [] + for test in manifest['tests']: + name = test['name'] + log = logs / (re.sub(r'[^A-Za-z0-9_-]', '_', name) + '.log') + command = [str(root / test['cmd'][0]), *test['cmd'][1:]] + with log.open('w') as output: + try: + process = subprocess.run(command, cwd=root, env=env, stdout=output, + stderr=subprocess.STDOUT, timeout=test['timeout'] or None) + result = {'name': name, 'returncode': process.returncode, + 'status': 'pass' if process.returncode == 0 else 'fail'} + except subprocess.TimeoutExpired: + output.write('\nTest timed out.\n') + result = {'name': name, 'status': 'timeout'} + except OSError as error: + output.write(str(error) + '\n') + result = {'name': name, 'status': 'error'} + results.append(result) + print(f'{name}: {result["status"]}', flush=True) + report = root / 'results.json' + report.write_text(json.dumps(results, indent=2) + '\n') + failed = sum(result['status'] != 'pass' for result in results) + print(f'{len(results) - failed} passed, {failed} failed. Logs: {logs}') + sys.exit(1 if failed else 0) + + +if __name__ == '__main__': + main() -- cgit v1.2.3