mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-11 18:17:46 +00:00
302 lines
17 KiB
Python
302 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""Target, failure and timeout controls for the compiler diagnostic launcher."""
|
|
|
|
import contextlib
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import signal
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
|
|
SPEC = importlib.util.spec_from_file_location("intel_trace", Path(__file__).with_name("trace-intel-ghc.py"))
|
|
TRACE = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(TRACE)
|
|
DRV = "/nix/store/" + "a" * 32 + "-simplex-chat-lib-simplex-chat-7.1.0.11.drv"
|
|
FAILURE = ("Failed to build simplex-chat-7.1.0.11-unit. The build process terminated with exit code -10\n"
|
|
+ "error: Cannot build '" + DRV + "'.\n")
|
|
|
|
|
|
class LauncherTests(unittest.TestCase):
|
|
def apple_installation(self, root, *, command_line=False):
|
|
developer = Path(root) / ("CommandLineTools" if command_line else "Xcode.app/Contents/Developer")
|
|
compiler = developer / ("usr/bin/clang" if command_line
|
|
else "Toolchains/XcodeDefault.xctoolchain/usr/bin/clang")
|
|
sdk = developer / ("SDKs/MacOSX.sdk" if command_line
|
|
else "Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk")
|
|
compiler.parent.mkdir(parents=True, exist_ok=True)
|
|
compiler.write_text("fixture Apple tool; never executed\n")
|
|
compiler.chmod(0o755)
|
|
sdk.mkdir(parents=True, exist_ok=True)
|
|
return developer, compiler, sdk
|
|
|
|
def test_unique_observed_failure(self):
|
|
self.assertEqual(TRACE.select_failure(FAILURE + FAILURE), DRV)
|
|
self.assertIsNone(TRACE.select_failure(FAILURE.replace("exit code -10", "exit code 1")))
|
|
self.assertIsNone(TRACE.select_failure(FAILURE.replace("simplex-chat-lib-simplex-chat", "dependency-lib-dependency")))
|
|
with self.assertRaises(ValueError):
|
|
TRACE.select_failure(FAILURE + FAILURE.replace("a" * 32, "b" * 32))
|
|
|
|
def invoke(self, folder, *, actions="true", machine="x86_64", target=None, failure=FAILURE,
|
|
reject_build=False, reject_cache=False, reject_xcrun=False):
|
|
developer, compiler, sdk = self.apple_installation(folder)
|
|
log = Path(folder) / "nix-build.log"
|
|
log.write_text(failure)
|
|
calls = []
|
|
def step(arguments, deadline, seconds, capture=False, *, phase="command"):
|
|
values = list(map(str, arguments))
|
|
calls.append((values, seconds, capture))
|
|
if values[0] == "git":
|
|
return "c" * 40
|
|
if values[0] == "xcrun" and reject_xcrun:
|
|
raise TimeoutError("Diagnostic command reached its wall-clock limit")
|
|
if values[:3] == ["xcrun", "--find", "clang"]:
|
|
return str(compiler)
|
|
if values[0] == "xcrun":
|
|
return str(sdk)
|
|
if (reject_build and "--file" in values) or (reject_cache and "--expr" in values):
|
|
raise RuntimeError("Diagnostic command returned 1")
|
|
return None
|
|
argv = ["trace-intel-ghc.py", "--repo", folder, "--attribute", target or TRACE.ATTRIBUTE, "--log", str(log)]
|
|
with patch.dict(os.environ, {"GITHUB_ACTIONS": actions, "DEVELOPER_DIR": str(developer), "TOOLCHAINS": ""}), \
|
|
patch.object(TRACE.platform, "system", return_value="Darwin"), \
|
|
patch.object(TRACE.platform, "machine", return_value=machine), \
|
|
patch.object(TRACE.shutil, "which", side_effect=lambda tool: None if tool in ("timeout", "gtimeout") else "/existing/tool"), \
|
|
patch.object(TRACE, "run_step", side_effect=step), patch("sys.argv", argv), \
|
|
contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
|
result = TRACE.main()
|
|
return result, calls
|
|
|
|
def test_excluded_contexts_never_launch(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
for options in ({"actions": "false"}, {"machine": "arm64"}, {"target": "hydraJobs.x86_64-darwin.lib:simplex-chat"},
|
|
{"failure": FAILURE.replace("exit code -10", "exit code 1")}):
|
|
result, calls = self.invoke(folder, **options)
|
|
self.assertEqual((result, calls), (0, []))
|
|
|
|
def test_one_scoped_replay_with_bounds_and_no_sandbox_override(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
result, calls = self.invoke(folder)
|
|
self.assertEqual(result, 0)
|
|
builds = [call for call in calls if call[0][:2] == ["nix", "build"]]
|
|
self.assertEqual(len(builds), 2)
|
|
preparation = builds[0][0]
|
|
self.assertIn("--expr", preparation)
|
|
self.assertEqual(preparation[preparation.index("--max-jobs") + 1], "0")
|
|
self.assertEqual(preparation[preparation.index("--builders") + 1], "")
|
|
self.assertEqual(preparation[preparation.index("external-builders") + 1], "[]")
|
|
self.assertEqual(builds[0][1], 30)
|
|
arguments, seconds, _ = builds[1]
|
|
self.assertEqual(seconds, 300)
|
|
self.assertEqual(arguments[arguments.index("expectedDrv") + 1], DRV)
|
|
self.assertIn("--timeout", arguments)
|
|
self.assertEqual(arguments[-1], "300")
|
|
self.assertNotIn("--option", arguments)
|
|
self.assertIn("--keep-failed", arguments)
|
|
original = arguments[arguments.index("original") + 1]
|
|
self.assertIn('f.hydraJobs.x86_64-darwin."x86_64-darwin-ios:lib:simplex-chat"', original)
|
|
self.assertNotIn(".project.hsPkgs", original)
|
|
compiler = str(Path(folder) / "Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang")
|
|
self.assertEqual(sum(call[0][:1] == [compiler] for call in calls), 1)
|
|
|
|
def test_observed_xcrun_timeout_does_not_block_selected_tools(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
result, calls = self.invoke(folder, reject_xcrun=True)
|
|
self.assertEqual(result, 0)
|
|
self.assertEqual(sum("--file" in call[0] for call in calls), 1)
|
|
self.assertFalse(any(call[0][0] == "xcrun" for call in calls))
|
|
|
|
def test_selected_xcode_and_command_line_tools_need_no_discovery_process(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
for command_line in (False, True):
|
|
developer, compiler, sdk = self.apple_installation(folder, command_line=command_line)
|
|
with patch.dict(os.environ, {"DEVELOPER_DIR": str(developer), "TOOLCHAINS": "", "SDKROOT": "/unused/iPhoneOS.sdk"}), \
|
|
patch.object(TRACE, "run_step") as step, contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertEqual(TRACE.apple_tool_paths(time.monotonic() + 20), (compiler, sdk))
|
|
step.assert_not_called()
|
|
|
|
def test_xcode_application_selection_is_supported(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
developer, compiler, sdk = self.apple_installation(folder)
|
|
with patch.dict(os.environ, {"DEVELOPER_DIR": str(developer.parent.parent), "TOOLCHAINS": ""}), \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertEqual(TRACE.apple_tool_paths(time.monotonic() + 20), (compiler, sdk))
|
|
|
|
def test_system_developer_selection_is_bounded(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
developer, compiler, sdk = self.apple_installation(folder)
|
|
deadline = time.monotonic() + 20
|
|
with patch.dict(os.environ, {}, clear=True), \
|
|
patch.object(TRACE.shutil, "which", return_value="/usr/bin/xcode-select"), \
|
|
patch.object(TRACE, "run_step", return_value=str(developer)) as step, \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertEqual(TRACE.apple_tool_paths(deadline), (compiler, sdk))
|
|
step.assert_called_once_with(["xcode-select", "--print-path"], deadline, 10, True,
|
|
phase="apple-developer-path")
|
|
|
|
def test_invalid_developer_selection_never_uses_another_installation(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
for selection in ("", "relative", "bad\npath", str(Path(folder) / "missing")):
|
|
with self.subTest(selection=selection), \
|
|
patch.dict(os.environ, {"DEVELOPER_DIR": selection, "TOOLCHAINS": ""}), \
|
|
patch.object(TRACE, "run_step") as step:
|
|
with self.assertRaisesRegex(ValueError, "developer directory"):
|
|
TRACE.apple_tool_paths(time.monotonic() + 20)
|
|
step.assert_not_called()
|
|
|
|
def test_missing_selected_clang_or_sdk_is_named(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
developer, compiler, sdk = self.apple_installation(folder)
|
|
with patch.dict(os.environ, {"DEVELOPER_DIR": str(developer), "TOOLCHAINS": ""}):
|
|
compiler.chmod(0o644)
|
|
with self.assertRaisesRegex(ValueError, "Clang.*not executable"):
|
|
TRACE.apple_tool_paths(time.monotonic() + 20)
|
|
compiler.chmod(0o755)
|
|
sdk.rmdir()
|
|
with self.assertRaisesRegex(ValueError, "macOS SDK is unavailable"):
|
|
TRACE.apple_tool_paths(time.monotonic() + 20)
|
|
|
|
def test_custom_toolchain_uses_original_bounded_discovery(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
developer, compiler, sdk = self.apple_installation(folder)
|
|
custom = Path(folder) / "Selected.xctoolchain/usr/bin/clang"
|
|
custom.parent.mkdir(parents=True)
|
|
custom.write_text("fixture custom tool; never executed\n")
|
|
custom.chmod(0o755)
|
|
deadline = time.monotonic() + 30
|
|
with patch.dict(os.environ, {"DEVELOPER_DIR": str(developer), "TOOLCHAINS": "selected.custom.toolchain"}), \
|
|
patch.object(TRACE.shutil, "which", return_value="/usr/bin/xcrun"), \
|
|
patch.object(TRACE, "run_step", side_effect=[str(custom), str(sdk)]) as step, \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertEqual(TRACE.apple_tool_paths(deadline), (custom, sdk))
|
|
self.assertEqual(step.call_args_list[0].args, (["xcrun", "--find", "clang"], deadline, 10, True))
|
|
self.assertEqual(step.call_args_list[1].args, (["xcrun", "--sdk", "macosx", "--show-sdk-path"], deadline, 10, True))
|
|
|
|
def test_failed_diagnostic_returns_without_replay_loop(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
result, calls = self.invoke(folder, reject_build=True)
|
|
self.assertEqual(result, 0)
|
|
self.assertEqual(sum("--file" in call[0] for call in calls), 1)
|
|
|
|
def test_cache_miss_never_launches_replay(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
result, calls = self.invoke(folder, reject_cache=True)
|
|
self.assertEqual(result, 0)
|
|
self.assertEqual(sum("--expr" in call[0] for call in calls), 1)
|
|
self.assertFalse(any("--file" in call[0] for call in calls))
|
|
|
|
@unittest.skipUnless(shutil.which("nix") and shutil.which("git"), "requires Nix and Git")
|
|
def test_pure_preparation_uses_pinned_flake_source(self):
|
|
with tempfile.TemporaryDirectory(prefix="intel-python-source-") as folder:
|
|
root = Path(folder)
|
|
factory = root / "scripts/nix/native-packages.nix"
|
|
factory.parent.mkdir(parents=True)
|
|
(root / "flake.nix").write_text('{ outputs = { self }: {}; }\n')
|
|
# The factory ignores its input: this fixture checks source access,
|
|
# without evaluating haskell.nix or building Python or a compiler.
|
|
factory.write_text('{ haskellNix }: system: { python3 = "pinned:${system}"; }\n')
|
|
def call(argv):
|
|
return subprocess.run(argv, cwd=root, text=True, capture_output=True, timeout=30)
|
|
call(["git", "init", "-q", "-b", "master"]).check_returncode()
|
|
call(["git", "add", "flake.nix", "scripts/nix/native-packages.nix"]).check_returncode()
|
|
call(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid",
|
|
"commit", "-qm", "pinned fixture"]).check_returncode()
|
|
revision = call(["git", "rev-parse", "HEAD"])
|
|
revision.check_returncode()
|
|
flake = "builtins.getFlake " + TRACE.nix_string("git+" + root.as_uri() + "?rev=" + revision.stdout.strip())
|
|
factory.write_text('throw "read live workspace"\n')
|
|
original = ("let f = " + flake + "; in (import " + TRACE.nix_string(factory)
|
|
+ ' { haskellNix = f.inputs.haskellNix; } "x86_64-darwin").python3')
|
|
argv = ["nix", "eval", "--offline", "--option", "pure-eval", "true", "--json", "--expr"]
|
|
rejected = call([*argv, original])
|
|
self.assertNotEqual(rejected.returncode, 0)
|
|
self.assertIn("forbidden in pure evaluation mode", rejected.stderr)
|
|
fixed = call([*argv, TRACE.python_expression(flake)])
|
|
self.assertEqual(fixed.returncode, 0, fixed.stderr)
|
|
self.assertEqual(json.loads(fixed.stdout), "pinned:x86_64-darwin")
|
|
|
|
def test_actual_metadata_output_is_bounded(self):
|
|
with self.assertRaises(ValueError):
|
|
TRACE.run_step([sys.executable, "-c", "print('x' * 65536)"], time.monotonic() + 10, 5, True)
|
|
self.assertEqual(TRACE.run_step([sys.executable, "-c", "print('revision')"],
|
|
time.monotonic() + 10, 5, True), "revision")
|
|
|
|
def test_phase_and_elapsed_time_survive_metadata_timeout(self):
|
|
output = io.StringIO()
|
|
with contextlib.redirect_stdout(output), \
|
|
patch.object(TRACE, "command_status", side_effect=TimeoutError("Diagnostic command reached its wall-clock limit")):
|
|
with self.assertRaisesRegex(TimeoutError, "wall-clock limit"):
|
|
TRACE.run_step(["git", "rev-parse", "HEAD"], time.monotonic() + 20, 10, True,
|
|
phase="checkout-revision")
|
|
events = [json.loads(line) for line in output.getvalue().splitlines()]
|
|
self.assertEqual([event["intelGHCPhase"] for event in events],
|
|
["checkout-revision", "checkout-revision"])
|
|
self.assertEqual([event["event"] for event in events], ["start", "end"])
|
|
self.assertLessEqual(events[0]["limitSeconds"], 10)
|
|
self.assertEqual(events[1]["outcome"], "failure")
|
|
self.assertGreaterEqual(events[1]["elapsedSeconds"], 0)
|
|
|
|
def test_successful_metadata_phase_is_reported(self):
|
|
output = io.StringIO()
|
|
with contextlib.redirect_stdout(output):
|
|
result = TRACE.run_step([sys.executable, "-c", "print('revision')"],
|
|
time.monotonic() + 10, 5, True, phase="checkout-revision")
|
|
self.assertEqual(result, "revision")
|
|
events = [json.loads(line) for line in output.getvalue().splitlines()]
|
|
self.assertEqual(events[-1]["outcome"], "success")
|
|
self.assertEqual(events[-1]["intelGHCPhase"], "checkout-revision")
|
|
|
|
def test_actual_timeout_kills_owned_command_and_its_child(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
pidfile = Path(folder) / "owned-pids"
|
|
grandchild = "import signal,time; signal.signal(signal.SIGTERM,signal.SIG_IGN); time.sleep(60)"
|
|
program = ("import os,signal,subprocess,sys,time; "
|
|
"signal.signal(signal.SIGTERM,signal.SIG_IGN); "
|
|
"p=subprocess.Popen([sys.executable,'-c',sys.argv[2]]); "
|
|
"open(sys.argv[1],'w').write(str(os.getpid())+' '+str(p.pid)); time.sleep(60)")
|
|
before = time.monotonic()
|
|
with self.assertRaisesRegex(TimeoutError, "wall-clock limit"):
|
|
TRACE.command_status([sys.executable, "-c", program, str(pidfile), grandchild], 1)
|
|
self.assertLess(time.monotonic() - before, 7)
|
|
parent, child = map(int, pidfile.read_text().split())
|
|
# The direct child is reaped. An orphaned grandchild can briefly
|
|
# remain a zombie on Linux; it must not be running in either case.
|
|
for pid in (parent, child):
|
|
stopped = False
|
|
until = time.monotonic() + 3
|
|
while time.monotonic() < until:
|
|
try:
|
|
os.kill(pid, 0)
|
|
except ProcessLookupError:
|
|
stopped = True
|
|
break
|
|
stat = Path("/proc") / str(pid) / "stat"
|
|
if stat.exists() and stat.read_text().split(")", 1)[1].split()[0] == "Z":
|
|
stopped = True
|
|
break
|
|
time.sleep(0.02)
|
|
self.assertTrue(stopped, "Owned fixture PID still running: " + str(pid))
|
|
|
|
def test_nonzero_actual_command_remains_a_failure(self):
|
|
with self.assertRaisesRegex(RuntimeError, "returned 7"):
|
|
TRACE.run_step([sys.executable, "-c", "raise SystemExit(7)"], time.monotonic() + 10, 5)
|
|
|
|
def test_deadline_and_nix_literal(self):
|
|
with patch.object(TRACE.time, "monotonic", return_value=2), patch.object(TRACE.subprocess, "Popen") as run:
|
|
with self.assertRaises(TimeoutError):
|
|
TRACE.run_step(["nix", "build"], 1, 300)
|
|
run.assert_not_called()
|
|
self.assertEqual(TRACE.nix_string('path${value}\\"'), '"path\\${value}\\\\\\\""')
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|