Skip to content

Commit 41c6522

Browse files
committed
Final updates for no-popups
1 parent 5bfc3fe commit 41c6522

13 files changed

Lines changed: 513 additions & 14 deletions

File tree

Build/scripts/Invoke-CppTest.ps1

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,13 @@ function Invoke-Run {
551551
}
552552

553553
$process.WaitForExit()
554+
$nativeExitCode = $null
555+
try {
556+
$nativeExitCode = $process.ExitCode
557+
}
558+
catch {
559+
$nativeExitCode = $null
560+
}
554561

555562
$logTail = @()
556563
if (Test-Path $LogPath) {
@@ -564,15 +571,34 @@ function Invoke-Run {
564571
Write-Host "--- end output ---" -ForegroundColor Yellow
565572
}
566573

567-
# Determine exit code: parse the Unit++ summary line from the log as the authoritative
568-
# source. Start-Process -RedirectStandardOutput in PowerShell 5.1 can return a null
569-
# ExitCode even after WaitForExit(), so the process exit code is not reliable here.
574+
# Determine exit code using both the real process exit code and the Unit++ summary.
575+
# The process exit code is authoritative for crashes/teardown failures that occur after
576+
# the Unit++ summary has already been written.
570577
$exitCode = -1
578+
$summaryExitCode = $null
571579
if (-not $timedOut) {
572580
$summaryLine = $logTail | Where-Object { $_ -match 'Tests \[Ok-Fail-Error\]: \[\d+-\d+-\d+\]' } | Select-Object -Last 1
573581
if ($summaryLine) {
574582
$m = [regex]::Match($summaryLine, 'Tests \[Ok-Fail-Error\]: \[(\d+)-(\d+)-(\d+)\]')
575-
$exitCode = [int]$m.Groups[2].Value + [int]$m.Groups[3].Value
583+
$summaryExitCode = [int]$m.Groups[2].Value + [int]$m.Groups[3].Value
584+
}
585+
586+
if ($terminatedAfterCompletion) {
587+
if ($null -ne $nativeExitCode -and $nativeExitCode -ne 0) {
588+
$exitCode = $nativeExitCode
589+
}
590+
else {
591+
$exitCode = 1
592+
}
593+
}
594+
elseif ($null -ne $nativeExitCode -and $nativeExitCode -ne 0) {
595+
$exitCode = $nativeExitCode
596+
}
597+
elseif ($null -ne $summaryExitCode) {
598+
$exitCode = $summaryExitCode
599+
}
600+
elseif ($null -ne $nativeExitCode) {
601+
$exitCode = $nativeExitCode
576602
}
577603
}
578604

Lib/src/unit++/main.cc

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,86 @@
22
// Terms of use are in the file COPYING
33
#include "main.h"
44
#include <algorithm>
5+
#include <signal.h>
56
#include <stdio.h>
7+
#include <stdlib.h>
8+
#if defined(WIN32) || defined(WIN64)
9+
#define WINDOWS_LEAN_AND_MEAN
10+
#include <Windows.h>
11+
#include <crtdbg.h>
12+
#endif
613
using namespace std;
714
using namespace unitpp;
815

16+
#if defined(WIN32) || defined(WIN64)
17+
namespace
18+
{
19+
void TerminateOnSigAbrt(int)
20+
{
21+
_exit(3);
22+
}
23+
24+
typedef HRESULT (WINAPI * PfnWerGetFlags)(HANDLE, PDWORD);
25+
typedef HRESULT (WINAPI * PfnWerSetFlags)(DWORD);
26+
27+
const DWORD kWerFaultReportingNoUi = 0x00000004;
28+
const DWORD kWerFaultReportingAlwaysShowUi = 0x00000010;
29+
30+
void ConfigureWindowsErrorReportingUi()
31+
{
32+
DWORD errorMode = GetErrorMode();
33+
errorMode |= SEM_FAILCRITICALERRORS;
34+
errorMode |= SEM_NOGPFAULTERRORBOX;
35+
errorMode |= SEM_NOOPENFILEERRORBOX;
36+
SetErrorMode(errorMode);
37+
38+
HMODULE hWer = LoadLibraryA("wer.dll");
39+
if (!hWer)
40+
return;
41+
42+
PfnWerGetFlags pfnWerGetFlags = reinterpret_cast<PfnWerGetFlags>(
43+
GetProcAddress(hWer, "WerGetFlags")
44+
);
45+
PfnWerSetFlags pfnWerSetFlags = reinterpret_cast<PfnWerSetFlags>(
46+
GetProcAddress(hWer, "WerSetFlags")
47+
);
48+
49+
if (pfnWerSetFlags)
50+
{
51+
DWORD flags = 0;
52+
if (pfnWerGetFlags)
53+
pfnWerGetFlags(GetCurrentProcess(), &flags);
54+
55+
flags |= kWerFaultReportingNoUi;
56+
flags &= ~kWerFaultReportingAlwaysShowUi;
57+
pfnWerSetFlags(flags);
58+
}
59+
60+
FreeLibrary(hWer);
61+
}
62+
63+
void ConfigureCrtReportUi()
64+
{
65+
_set_error_mode(_OUT_TO_STDERR);
66+
67+
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
68+
_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
69+
_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE);
70+
_CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR);
71+
_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
72+
_CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
73+
74+
_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
75+
}
76+
77+
void SuppressInteractiveCrashUi()
78+
{
79+
ConfigureWindowsErrorReportingUi();
80+
ConfigureCrtReportUi();
81+
}
82+
}
83+
#endif
84+
985
bool unitpp::verbose = false;
1086
int unitpp::verbose_lvl = 0;
1187
bool unitpp::line_fmt = false;
@@ -25,6 +101,9 @@ void unitpp::set_tester(test_runner* tr)
25101

26102
int main(int argc, const char* argv[])
27103
{
104+
#if defined(WIN32) || defined(WIN64)
105+
SuppressInteractiveCrashUi();
106+
#endif
28107
printf("DEBUG: unit++ main start\n"); fflush(stdout);
29108
options().add("v", new options_utils::opt_flag(verbose));
30109
options().alias("verbose", "v");
@@ -42,14 +121,41 @@ int main(int argc, const char* argv[])
42121
if (!runner)
43122
runner = &plain;
44123

45-
printf("DEBUG: Calling GlobalSetup\n"); fflush(stdout);
46-
GlobalSetup(verbose);
47-
printf("DEBUG: Returned from GlobalSetup\n"); fflush(stdout);
124+
int retval = 0;
48125

49-
int retval = runner->run_tests(argc, argv) ? 0 : 1;
126+
try {
127+
printf("DEBUG: Calling GlobalSetup\n"); fflush(stdout);
128+
GlobalSetup(verbose);
129+
printf("DEBUG: Returned from GlobalSetup\n"); fflush(stdout);
130+
}
131+
catch (const std::exception& e) {
132+
fprintf(stderr, "GlobalSetup threw std::exception: %s\n", e.what());
133+
fflush(stderr);
134+
return 1;
135+
}
136+
catch (...) {
137+
fprintf(stderr, "GlobalSetup threw an unknown exception\n");
138+
fflush(stderr);
139+
return 1;
140+
}
141+
142+
retval = runner->run_tests(argc, argv) ? 0 : 1;
143+
signal(SIGABRT, TerminateOnSigAbrt);
50144

51-
printf("DEBUG: Calling GlobalTeardown\n"); fflush(stdout);
52-
GlobalTeardown();
145+
try {
146+
printf("DEBUG: Calling GlobalTeardown\n"); fflush(stdout);
147+
GlobalTeardown();
148+
}
149+
catch (const std::exception& e) {
150+
fprintf(stderr, "GlobalTeardown threw std::exception: %s\n", e.what());
151+
fflush(stderr);
152+
retval = 1;
153+
}
154+
catch (...) {
155+
fprintf(stderr, "GlobalTeardown threw an unknown exception\n");
156+
fflush(stderr);
157+
retval = 1;
158+
}
53159
printf("DEBUG: unit++ main end (retval=%d)\n", retval); fflush(stdout);
54160
return retval;
55161
}

Src/Common/FwUtils/FwUtilsTests/LogUnhandledExceptionsAttributeTests.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,16 @@ public void ThrowIfCapturedUnobservedTaskExceptions_ThrowsAssertionExceptionWith
5757
Assert.That(exception.Message, Does.Contain("second failure"));
5858
Assert.That(exception.Message, Does.Contain("fails deterministically"));
5959
}
60+
61+
[Test]
62+
public void OnUnobservedTaskException_MarksExceptionObserved()
63+
{
64+
var aggregateException = new AggregateException(new InvalidOperationException("boom"));
65+
var eventArgs = new UnobservedTaskExceptionEventArgs(aggregateException);
66+
67+
LogUnhandledExceptionsAttribute.OnUnobservedTaskException(this, eventArgs);
68+
69+
Assert.That(eventArgs.Observed, Is.True);
70+
}
6071
}
6172
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright (c) 2026 SIL International
2+
// This software is licensed under the LGPL, version 2.1 or later
3+
// (http://www.gnu.org/licenses/lgpl-2.1.html)
4+
5+
using System;
6+
using System.Diagnostics;
7+
using System.IO;
8+
using NUnit.Framework;
9+
using SIL.FieldWorks.Common.FwUtils.Attributes;
10+
11+
namespace SIL.FieldWorks.Common.FwUtils
12+
{
13+
[TestFixture]
14+
public class SuppressAssertDialogsAttributeTests
15+
{
16+
private TraceListener[] m_originalListeners;
17+
private TextWriter m_originalError;
18+
19+
[SetUp]
20+
public void SetUp()
21+
{
22+
m_originalListeners = new TraceListener[Trace.Listeners.Count];
23+
Trace.Listeners.CopyTo(m_originalListeners, 0);
24+
Trace.Listeners.Clear();
25+
m_originalError = Console.Error;
26+
27+
Environment.SetEnvironmentVariable("AssertUiEnabled", null);
28+
Environment.SetEnvironmentVariable("AssertExceptionEnabled", null);
29+
Environment.SetEnvironmentVariable("FW_TEST_MODE", null);
30+
}
31+
32+
[TearDown]
33+
public void TearDown()
34+
{
35+
Trace.Listeners.Clear();
36+
Trace.Listeners.AddRange(m_originalListeners);
37+
Console.SetError(m_originalError);
38+
39+
Environment.SetEnvironmentVariable("AssertUiEnabled", null);
40+
Environment.SetEnvironmentVariable("AssertExceptionEnabled", null);
41+
Environment.SetEnvironmentVariable("FW_TEST_MODE", null);
42+
}
43+
44+
[Test]
45+
public void ConsoleErrorTraceListener_Fail_ThrowsAssertionDialogExceptionAndWritesToStderr()
46+
{
47+
var stderr = new StringWriter();
48+
Console.SetError(stderr);
49+
50+
var listener = new ConsoleErrorTraceListener(throwOnFail: true);
51+
52+
var exception = Assert.Throws<AssertionDialogException>(
53+
() => listener.Fail("boom", "detail")
54+
);
55+
56+
Assert.That(exception.Message, Does.Contain("boom"));
57+
Assert.That(exception.Message, Does.Contain("detail"));
58+
Assert.That(stderr.ToString(), Does.Contain("Debug.Fail/Assert fired during test:"));
59+
}
60+
61+
[Test]
62+
public void SuppressAssertDialogsAttribute_BeforeAndAfterTest_RestoresEnvironmentVariables()
63+
{
64+
Environment.SetEnvironmentVariable("AssertUiEnabled", "true");
65+
Environment.SetEnvironmentVariable("AssertExceptionEnabled", "false");
66+
Environment.SetEnvironmentVariable("FW_TEST_MODE", "0");
67+
68+
var defaultListener = new DefaultTraceListener();
69+
Trace.Listeners.Add(defaultListener);
70+
71+
var attribute = new SuppressAssertDialogsAttribute();
72+
73+
attribute.BeforeTest(null);
74+
75+
Assert.That(Environment.GetEnvironmentVariable("AssertUiEnabled"), Is.EqualTo("false"));
76+
Assert.That(
77+
Environment.GetEnvironmentVariable("AssertExceptionEnabled"),
78+
Is.EqualTo("true")
79+
);
80+
Assert.That(Environment.GetEnvironmentVariable("FW_TEST_MODE"), Is.EqualTo("1"));
81+
Assert.That(defaultListener.AssertUiEnabled, Is.False);
82+
Assert.That(CountListeners<ConsoleErrorTraceListener>(), Is.EqualTo(1));
83+
84+
attribute.AfterTest(null);
85+
86+
Assert.That(Environment.GetEnvironmentVariable("AssertUiEnabled"), Is.EqualTo("true"));
87+
Assert.That(
88+
Environment.GetEnvironmentVariable("AssertExceptionEnabled"),
89+
Is.EqualTo("false")
90+
);
91+
Assert.That(Environment.GetEnvironmentVariable("FW_TEST_MODE"), Is.EqualTo("0"));
92+
Assert.That(CountListeners<ConsoleErrorTraceListener>(), Is.EqualTo(0));
93+
}
94+
95+
[Test]
96+
public void SuppressAssertDialogsAttribute_DoesNotAddDuplicateConsoleListener()
97+
{
98+
Trace.Listeners.Add(new ConsoleErrorTraceListener(throwOnFail: false));
99+
var attribute = new SuppressAssertDialogsAttribute();
100+
101+
attribute.BeforeTest(null);
102+
103+
Assert.That(CountListeners<ConsoleErrorTraceListener>(), Is.EqualTo(1));
104+
105+
attribute.AfterTest(null);
106+
}
107+
108+
[Test]
109+
public void SuppressAssertDialogsAttribute_WithEnvVarTraceListener_LeavesFailAsLoggingOnly()
110+
{
111+
Trace.Listeners.Add(new EnvVarTraceListener());
112+
var stderr = new StringWriter();
113+
Console.SetError(stderr);
114+
115+
var attribute = new SuppressAssertDialogsAttribute();
116+
attribute.BeforeTest(null);
117+
118+
var listener = FindConsoleErrorTraceListener();
119+
Assert.That(listener, Is.Not.Null);
120+
121+
Assert.DoesNotThrow(() => listener.Fail("boom", null));
122+
Assert.That(stderr.ToString(), Does.Contain("Debug.Fail/Assert fired during test:"));
123+
124+
attribute.AfterTest(null);
125+
}
126+
127+
private static int CountListeners<T>()
128+
where T : TraceListener
129+
{
130+
int count = 0;
131+
foreach (TraceListener listener in Trace.Listeners)
132+
{
133+
if (listener is T)
134+
count++;
135+
}
136+
137+
return count;
138+
}
139+
140+
private static ConsoleErrorTraceListener FindConsoleErrorTraceListener()
141+
{
142+
foreach (TraceListener listener in Trace.Listeners)
143+
{
144+
if (listener is ConsoleErrorTraceListener consoleListener)
145+
return consoleListener;
146+
}
147+
148+
return null;
149+
}
150+
151+
private sealed class EnvVarTraceListener : TraceListener
152+
{
153+
public override void Write(string message) { }
154+
155+
public override void WriteLine(string message) { }
156+
}
157+
}
158+
}

Src/DebugProcs/DebugProcs.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -677,11 +677,14 @@ void __cdecl SilAssert (
677677
else // !g_fShowMessageBox
678678
{
679679
// if we don't show a message box we should at least abort (and output the assertion
680-
// text if we haven't done that already). Note that we don't call _exit(3) as above
681-
// so that we can trap the signal and ignore it in unit tests
680+
// text if we haven't done that already). In FW_TEST_MODE we must not continue after
681+
// a post-summary assert even if SIGABRT is ignored or intercepted by the native test
682+
// runner, so force process termination if raise(SIGABRT) returns.
682683
if (g_ReportHook)
683684
OutputDebugString(assertbuf);
684685
raise(SIGABRT);
686+
if (IsTestModeEnabled())
687+
_exit(3);
685688
}
686689

687690
/* Ignore: continue execution */

0 commit comments

Comments
 (0)