Files
qbz/scripts/test_qt_build_gates.py
T
vicrodh 6794e15257 fix(build): invalidate native objects when the Qt SDK changes
Fingerprint Qt SDK headers for C++ cache inputs and share bounded debug/release startup gates between local tooling and CI.

cx
2026-09-06 03:53:05 -06:00

169 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""SDK-cache and crash-aware smoke regressions; no Qt or Cargo build needed."""
import importlib.util
import os
from pathlib import Path
import tempfile
import unittest
import sys
from unittest.mock import patch
sys.dont_write_bytecode = True
def load(name, filename):
spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(filename))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
qt = load("qt_cargo", "qt-cargo.py")
smoke = load("qt_smoke", "qt-smoke.py")
class SdkTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.headers = self.root / "include" / "QtCore"
self.headers.mkdir(parents=True)
self.header = self.headers / "qarraydataops.h"
self.header.write_text("old-layout")
self.answers = {"QT_VERSION": "6.11.2", "QT_INSTALL_HEADERS": str(self.headers.parent),
"QT_INSTALL_LIBS": str(self.root / "lib")}
mock = patch.object(qt, "query", side_effect=lambda _, key: self.answers[key])
mock.start()
self.addCleanup(mock.stop)
def fingerprint(self):
return qt.sdk_fingerprint("/sdk/bin/qmake", self.answers["QT_VERSION"])
def test_identical_headers_are_stable_after_touch(self):
before = self.fingerprint()
os.utime(self.header, None)
self.assertEqual(before, self.fingerprint())
def test_header_change_invalidates_even_with_same_version_size_and_mtime(self):
before = self.fingerprint()
stat = self.header.stat()
self.header.write_text("new-layout")
os.utime(self.header, ns=(stat.st_atime_ns, stat.st_mtime_ns))
self.assertNotEqual(before, self.fingerprint())
def test_patch_version_invalidates(self):
before = self.fingerprint()
self.answers["QT_VERSION"] = "6.11.3"
self.assertNotEqual(before, self.fingerprint())
def test_library_root_invalidates(self):
before = self.fingerprint()
self.answers["QT_INSTALL_LIBS"] = "/other-sdk/lib"
self.assertNotEqual(before, self.fingerprint())
def test_added_and_removed_headers_invalidate(self):
before = self.fingerprint()
extra = self.headers / "qnew.h"
extra.write_text("new type")
self.assertNotEqual(before, self.fingerprint())
extra.unlink()
self.assertEqual(before, self.fingerprint())
@unittest.skipIf(os.name == "nt", "unprivileged Windows symlinks may be unavailable")
def test_framework_symlink_and_cycle_are_bounded(self):
framework = self.root / "framework"
framework.mkdir()
header = framework / "qframework.h"
header.write_text("old")
(self.headers / "framework").symlink_to(framework, target_is_directory=True)
(framework / "cycle").symlink_to(self.headers, target_is_directory=True)
before = self.fingerprint()
header.write_text("new")
self.assertNotEqual(before, self.fingerprint())
def test_missing_sdk_fails_closed(self):
self.answers["QT_INSTALL_HEADERS"] = str(self.root / "missing")
with self.assertRaisesRegex(RuntimeError, "missing"):
self.fingerprint()
def test_empty_sdk_fails_closed(self):
self.header.unlink()
with self.assertRaisesRegex(RuntimeError, "empty"):
self.fingerprint()
def test_homebrew_framework_only_headers_are_hashed(self):
self.header.unlink()
headers = self.root / "lib" / "QtCore.framework" / "Headers"
headers.mkdir(parents=True)
header = headers / "qarraydataops.h"
header.write_text("old")
before = self.fingerprint()
header.write_text("new")
self.assertNotEqual(before, self.fingerprint())
def test_shared_include_root_ignores_non_qt_packages(self):
before = self.fingerprint()
unrelated = self.headers.parent / "other-package"
unrelated.mkdir()
(unrelated / "other.h").write_text("not part of Qt")
self.assertEqual(before, self.fingerprint())
def test_existing_flags_survive_and_nested_calls_are_stable(self):
original = {"CXXFLAGS": "/Zc:__cplusplus /permissive- -include arm_acle.h",
"RUSTFLAGS": "-C link-arg=-fuse-ld=lld", "CARGO_TARGET_DIR": "/cache"}
with patch.object(qt, "find_qmake", return_value=("/sdk/bin/qmake", "6.11.2")):
env, _, _ = qt.cargo_environment(original)
again, _, _ = qt.cargo_environment(env)
self.assertEqual(env, again)
self.assertTrue(env["CXXFLAGS"].startswith(original["CXXFLAGS"] + " "))
self.assertEqual(env["RUSTFLAGS"], original["RUSTFLAGS"])
self.assertEqual(env["CARGO_TARGET_DIR"], "/cache")
self.assertNotIn("QMAKE", original)
def test_broken_explicit_qmake_does_not_fall_back(self):
with patch.object(qt.shutil, "which", return_value=None) as which:
with self.assertRaises(RuntimeError):
qt.find_qmake({"QMAKE": "/missing/sdk/qmake"})
which.assert_called_once_with("/missing/sdk/qmake", path=None)
class SmokeTests(unittest.TestCase):
output = "QbzCore initialized\n" + "normal startup log\n" * 10
def test_success_requires_liveness(self):
smoke.check_result(self.output, True, -15)
def test_early_exit_is_never_green_even_after_success_marker(self):
for status in (0, 1, 139, -11, -6):
with self.subTest(status=status), self.assertRaisesRegex(RuntimeError, "exited"):
smoke.check_result(self.output, False, status)
def test_crash_at_deadline_is_not_our_intentional_shutdown(self):
for status in (139, -11, 134, -6, 1):
with self.subTest(status=status), self.assertRaisesRegex(RuntimeError, "status"):
smoke.check_result(self.output, True, status)
def test_silent_hang_is_not_startup(self):
with self.assertRaises(RuntimeError):
smoke.check_result("", True, -15)
def test_missing_core_marker_fails(self):
with self.assertRaisesRegex(RuntimeError, "initialization"):
smoke.check_result("log\n" * 12, True, -15)
def test_qml_error_after_init_fails(self):
with self.assertRaisesRegex(RuntimeError, "QML complaints"):
smoke.check_result(self.output + "ReferenceError: missing binding", True, -15)
def test_known_property_cache_warning_does_not_hide_real_error(self):
warning = "qt.qml.propertyCache: has no property\n"
smoke.check_result(self.output + warning, True, -15)
with self.assertRaises(RuntimeError):
smoke.check_result(self.output + warning + "TypeError: broken", True, -15)
if __name__ == "__main__":
unittest.main()