From 40397f5bf90bfd7d60a0df96d6fd520d10b0b3d7 Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Thu, 8 Oct 2026 22:30:00 +0800 Subject: [PATCH] Capture expected application test diagnostics Signed-off-by: Loren Eteval --- tests/README.md | 2 +- tests/test_application_process.py | 63 +++++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/tests/README.md b/tests/README.md index fa8c0cc1..aef24e60 100644 --- a/tests/README.md +++ b/tests/README.md @@ -100,7 +100,7 @@ worker. Choose tests by the changed contract rather than by filename alone. | Module | Coverage | | --- | --- | -| [test_application_process.py](test_application_process.py) | Exact application-child ownership, spawned factory/runner failure classification and crash reporting, partial Qt startup, crash-write failure, intentional exits, exception/signal handling and command-line dispatch. | +| [test_application_process.py](test_application_process.py) | Exact application-child ownership, spawned factory/runner failure classification and crash reporting, captured and asserted expected child diagnostics, partial Qt startup, crash-write failure, intentional exits, exception/signal handling and command-line dispatch. | | [test_architecture_refactors.py](test_architecture_refactors.py) | Startup acquisition/rollback including partial controller construction and cleanup retry, singleton election and real isolated IPC race, tray/exit policy, host integration ownership, bounded core-log transport, connection transactions, stylesheet composition. | | [test_connection_startup_async.py](test_connection_startup_async.py) | Real local-listener readiness, timeout/cancel/replacement, semantic exits, DNS reply lifetime, mocked platform-specific TUN sequencing. | | [test_controllers.py](test_controllers.py) | Connection state/error/reconnect transitions, reentrant cancellation/replacement before launch and during completion, startup restoration, shared settings, routing fallback persistence and tray/selector agreement after custom-routing disable/re-enable. | diff --git a/tests/test_application_process.py b/tests/test_application_process.py index 32d8448a..d8cd89e4 100644 --- a/tests/test_application_process.py +++ b/tests/test_application_process.py @@ -24,6 +24,7 @@ from Furious.Utility.AppMainProcess import AppMainProcess from tests.support import application +from contextlib import redirect_stderr from pathlib import Path from types import SimpleNamespace from unittest import mock @@ -39,6 +40,22 @@ import unittest AppMainProcessModule = importlib.import_module('Furious.Utility.AppMainProcess') +class _CapturedAppMainProcess(AppMainProcess): + """Retain expected child tracebacks without creating CI error annotations.""" + + def __init__(self, func, diagnosticPath): + super().__init__(func) + + self.diagnosticPath = diagnosticPath + + def run(self): + # Capture inside the spawned child and keep the real supervision path. + # SystemExit unwinds this context and flushes the file before join returns. + with open(self.diagnosticPath, 'w', encoding='utf-8') as diagnostic: + with redirect_stderr(diagnostic): + super().run() + + class _SuccessfulApplication: """Provide one picklable process-boundary application fixture.""" @@ -109,20 +126,22 @@ class AppMainProcessTest(unittest.TestCase): with self.subTest(stage=stage, exception=exceptionType.__name__): with tempfile.TemporaryDirectory() as directory: crashDirectory = Path(directory) / 'crashes' + diagnosticPath = Path(directory) / 'stderr.log' if failCrashWrite: crashDirectory.write_text( 'keep existing file', encoding='utf-8' ) - process = AppMainProcess( + process = _CapturedAppMainProcess( functools.partial( _failingApplication, stage, exceptionType, str(crashDirectory), createQt=createQt, - ) + ), + str(diagnosticPath), ) try: @@ -130,9 +149,32 @@ class AppMainProcessTest(unittest.TestCase): process.join(15) self.assertFalse(process.is_alive(), 'child did not finish') - self.assertEqual(process.exitcode, expected.value) + + diagnostic = diagnosticPath.read_text(encoding='utf-8') + + # repr keeps diagnostics visible on failure without emitting + # traceback-shaped lines that CI mistakes for another error. + self.assertEqual( + process.exitcode, expected.value, repr(diagnostic) + ) self.assertEqual(process.fileWritten.value, not failCrashWrite) + message = ( + 'factory fixture failure' + if stage == 'factory' + else 'runner fixture failure' + ) + + self.assertEqual( + diagnostic.count('Traceback (most recent call last):'), + 1, + repr(diagnostic), + ) + self.assertIn( + f'{exceptionType.__name__}: {message}', diagnostic + ) + self.assertNotIn('unexpected Qt exit request', diagnostic) + if failCrashWrite: self.assertEqual( crashDirectory.read_text(encoding='utf-8'), @@ -143,20 +185,17 @@ class AppMainProcessTest(unittest.TestCase): self.assertEqual(len(logs), 1) - diagnostic = logs[0].read_text(encoding='utf-8') - message = ( - 'factory fixture failure' - if stage == 'factory' - else 'runner fixture failure' - ) + crashDiagnostic = logs[0].read_text(encoding='utf-8') self.assertIn( - 'Traceback (most recent call last):', diagnostic + 'Traceback (most recent call last):', crashDiagnostic ) self.assertIn( - f'{exceptionType.__name__}: {message}', diagnostic + f'{exceptionType.__name__}: {message}', crashDiagnostic + ) + self.assertNotIn( + 'unexpected Qt exit request', crashDiagnostic ) - self.assertNotIn('unexpected Qt exit request', diagnostic) finally: if process.is_alive(): process.terminate()