Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 111 additions & 8 deletions Build/scripts/Invoke-CppTest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
.PARAMETER WorktreePath
Path to the worktree root. Defaults to current directory.

.PARAMETER AllowAssertDialogs
Allow FieldWorks Abort/Retry/Ignore assertion dialogs during this local test run.
Equivalent environment variable: FW_TEST_ALLOW_ASSERT_DIALOGS=1.

.EXAMPLE
.\Invoke-CppTest.ps1 -TestProject TestGeneric
Build and run TestGeneric using MSBuild.
Expand All @@ -33,6 +37,10 @@
.EXAMPLE
.\Invoke-CppTest.ps1 -BuildSystem NMake -TestProject TestGeneric
Build TestGeneric using legacy nmake (requires VsDevCmd).

.EXAMPLE
.\Invoke-CppTest.ps1 -Action Run -TestProject TestGeneric -AllowAssertDialogs
Run TestGeneric with interactive FieldWorks assertion dialogs enabled for debugger attachment.
#>
[CmdletBinding()]
param(
Expand All @@ -56,7 +64,9 @@ param(

[string[]]$TestArguments,

[string]$LogPath
[string]$LogPath,

[switch]$AllowAssertDialogs
)

Set-StrictMode -Version Latest
Expand All @@ -74,16 +84,89 @@ if (-not (Test-Path $helpersPath)) {
}
Import-Module $helpersPath -Force

function Test-EnvironmentSwitchEnabled {
param(
[string]$Name
)

$value = [Environment]::GetEnvironmentVariable($Name)
if ([string]::IsNullOrWhiteSpace($value)) {
return $false
}

return @('1', 'true', 'yes', 'on') -contains $value.Trim().ToLowerInvariant()
}

function Set-AssertDialogEnvironment {
param(
[bool]$AllowDialogs
)

if ($AllowDialogs) {
$env:AssertUiEnabled = 'true'
$env:AssertExceptionEnabled = 'false'
$env:FW_TEST_MODE = '0'
$env:FW_TEST_ALLOW_ASSERT_DIALOGS = '1'
Write-Host "[WARN] Interactive assertion dialogs are enabled for this local native test run." -ForegroundColor Yellow
return
}

# Suppress assertion dialog boxes (DebugProcs.dll checks these env vars).
# This prevents unattended tests from blocking on MessageBox popups.
$env:AssertUiEnabled = 'false'
$env:AssertExceptionEnabled = 'true'
# Unconditional test-mode override: bypasses registry AssertMessageBox key in DebugProcs.dll.
$env:FW_TEST_MODE = '1'
}

function Resolve-NativeTestExitCode {
param(
[bool]$TimedOut,
[bool]$TerminatedAfterCompletion,
$NativeExitCode,
$SummaryExitCode
)

if ($TimedOut) {
return -1
}

if ($null -ne $SummaryExitCode -and $SummaryExitCode -ne 0) {
return $SummaryExitCode
}

if ($TerminatedAfterCompletion) {
if ($null -ne $NativeExitCode -and $NativeExitCode -ne 0) {
return $NativeExitCode
}

return 1
}

if ($null -ne $NativeExitCode -and $NativeExitCode -ne 0) {
return $NativeExitCode
}

if ($null -ne $SummaryExitCode) {
return $SummaryExitCode
}

if ($null -ne $NativeExitCode) {
return $NativeExitCode
}

return -1
}

# =============================================================================
# Environment Setup
# =============================================================================

# Initialize VS environment (needed for NMake and MSBuild)
Initialize-VsDevEnvironment

# Suppress assertion dialog boxes (DebugProcs.dll checks this env var)
# This prevents tests from blocking on MessageBox popups
$env:AssertUiEnabled = 'false'
$allowAssertDialogsForRun = $AllowAssertDialogs -or (Test-EnvironmentSwitchEnabled -Name 'FW_TEST_ALLOW_ASSERT_DIALOGS')
Set-AssertDialogEnvironment -AllowDialogs $allowAssertDialogsForRun

# Suppress Windows Error Reporting and crash dialogs
# SEM_FAILCRITICALERRORS = 0x0001
Expand Down Expand Up @@ -549,6 +632,13 @@ function Invoke-Run {
}

$process.WaitForExit()
$nativeExitCode = $null
try {
$nativeExitCode = $process.ExitCode
}
catch {
$nativeExitCode = $null
}

$logTail = @()
if (Test-Path $LogPath) {
Expand All @@ -562,17 +652,30 @@ function Invoke-Run {
Write-Host "--- end output ---" -ForegroundColor Yellow
}

# Determine exit code: parse the Unit++ summary line from the log as the authoritative
# source. Start-Process -RedirectStandardOutput in PowerShell 5.1 can return a null
# ExitCode even after WaitForExit(), so the process exit code is not reliable here.
# Determine exit code using both the Unit++ summary and the real process exit code.
# When Unit++ reports failures/errors, prefer that count so the user sees the actionable
# test failure total. Fall back to the process exit code for crashes/teardown failures
# that happen without a non-zero Unit++ summary.
$exitCode = -1
$summaryExitCode = $null
if (-not $timedOut) {
$summaryLine = $logTail | Where-Object { $_ -match 'Tests \[Ok-Fail-Error\]: \[\d+-\d+-\d+\]' } | Select-Object -Last 1
if ($summaryLine) {
$m = [regex]::Match($summaryLine, 'Tests \[Ok-Fail-Error\]: \[(\d+)-(\d+)-(\d+)\]')
$exitCode = [int]$m.Groups[2].Value + [int]$m.Groups[3].Value
$summaryExitCode = [int]$m.Groups[2].Value + [int]$m.Groups[3].Value
}
}
$exitCode = Resolve-NativeTestExitCode `
-TimedOut $timedOut `
-TerminatedAfterCompletion $terminatedAfterCompletion `
-NativeExitCode $nativeExitCode `
-SummaryExitCode $summaryExitCode

if ($null -ne $summaryExitCode -and $summaryExitCode -ne 0 -and
$null -ne $nativeExitCode -and $nativeExitCode -ne 0 -and
$nativeExitCode -ne $summaryExitCode) {
Write-Host "Native process exit code was $nativeExitCode; reporting Unit++ failure/error count $summaryExitCode." -ForegroundColor Yellow
}

Write-Host ""
if ($timedOut) {
Expand Down
26 changes: 26 additions & 0 deletions Docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,32 @@ Build FieldWorks using the PowerShell build script:

For more build options, see [.github/instructions/build.instructions.md](../.github/instructions/build.instructions.md).

### Run tests from the command line

Use `test.ps1` for local test runs:

```powershell
.\test.ps1
.\test.ps1 -TestProject "Src/Common/FwUtils/FwUtilsTests"
.\test.ps1 -SkipManaged -TestProject TestGeneric
```

By default, test runs suppress FieldWorks assertion dialogs so unattended runs cannot hang on Abort/Retry/Ignore UI. For a local debugger session where you intentionally want the previous interactive assertion dialog behavior, use the command-line switch:

```powershell
.\test.ps1 -SkipManaged -TestProject TestGeneric -AllowAssertDialogs
```

The environment-variable equivalent is `FW_TEST_ALLOW_ASSERT_DIALOGS=1`:

```powershell
$env:FW_TEST_ALLOW_ASSERT_DIALOGS = '1'
.\test.ps1 -SkipManaged -TestProject TestGeneric
Remove-Item Env:FW_TEST_ALLOW_ASSERT_DIALOGS
```

Only use this opt-in for attended local debugging. CI and normal local runs should leave it unset.

### 5. VS Code and Visual Studio usage

Default recommendation:
Expand Down
121 changes: 115 additions & 6 deletions Lib/src/unit++/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,89 @@
// Terms of use are in the file COPYING
#include "main.h"
#include <algorithm>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#if defined(WIN32) || defined(_WIN32) || defined(WIN64) || defined(_WIN64) || defined(_M_X64)
#define UNITPP_WINDOWS 1
#endif
#if defined(UNITPP_WINDOWS)
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <crtdbg.h>
#endif
using namespace std;
using namespace unitpp;

#if defined(UNITPP_WINDOWS)
namespace
{
void TerminateOnSigAbrt(int)
{
_exit(3);
}

typedef HRESULT (WINAPI * PfnWerGetFlags)(HANDLE, PDWORD);
typedef HRESULT (WINAPI * PfnWerSetFlags)(DWORD);

const DWORD kWerFaultReportingNoUi = 0x00000004;
const DWORD kWerFaultReportingAlwaysShowUi = 0x00000010;

void ConfigureWindowsErrorReportingUi()
{
DWORD errorMode = GetErrorMode();
errorMode |= SEM_FAILCRITICALERRORS;
errorMode |= SEM_NOGPFAULTERRORBOX;
errorMode |= SEM_NOOPENFILEERRORBOX;
SetErrorMode(errorMode);

HMODULE hWer = LoadLibraryA("wer.dll");
if (!hWer)
return;

PfnWerGetFlags pfnWerGetFlags = reinterpret_cast<PfnWerGetFlags>(
GetProcAddress(hWer, "WerGetFlags")
);
PfnWerSetFlags pfnWerSetFlags = reinterpret_cast<PfnWerSetFlags>(
GetProcAddress(hWer, "WerSetFlags")
);

if (pfnWerSetFlags)
{
DWORD flags = 0;
if (pfnWerGetFlags)
pfnWerGetFlags(GetCurrentProcess(), &flags);

flags |= kWerFaultReportingNoUi;
flags &= ~kWerFaultReportingAlwaysShowUi;
pfnWerSetFlags(flags);
}

FreeLibrary(hWer);
}

void ConfigureCrtReportUi()
{
_set_error_mode(_OUT_TO_STDERR);

_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE);
_CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR);
_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
_CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);

_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
}

void SuppressInteractiveCrashUi()
{
ConfigureWindowsErrorReportingUi();
ConfigureCrtReportUi();
}
}
#endif

bool unitpp::verbose = false;
int unitpp::verbose_lvl = 0;
bool unitpp::line_fmt = false;
Expand All @@ -25,6 +104,10 @@ void unitpp::set_tester(test_runner* tr)

int main(int argc, const char* argv[])
{
#if defined(UNITPP_WINDOWS)
SuppressInteractiveCrashUi();
signal(SIGABRT, TerminateOnSigAbrt);
#endif
printf("DEBUG: unit++ main start\n"); fflush(stdout);
options().add("v", new options_utils::opt_flag(verbose));
options().alias("verbose", "v");
Expand All @@ -42,14 +125,40 @@ int main(int argc, const char* argv[])
if (!runner)
runner = &plain;

printf("DEBUG: Calling GlobalSetup\n"); fflush(stdout);
GlobalSetup(verbose);
printf("DEBUG: Returned from GlobalSetup\n"); fflush(stdout);
int retval = 0;

try {
printf("DEBUG: Calling GlobalSetup\n"); fflush(stdout);
GlobalSetup(verbose);
printf("DEBUG: Returned from GlobalSetup\n"); fflush(stdout);
}
catch (const std::exception& e) {
fprintf(stderr, "GlobalSetup threw std::exception: %s\n", e.what());
fflush(stderr);
return 1;
}
catch (...) {
fprintf(stderr, "GlobalSetup threw an unknown exception\n");
fflush(stderr);
return 1;
}

int retval = runner->run_tests(argc, argv) ? 0 : 1;
retval = runner->run_tests(argc, argv) ? 0 : 1;

printf("DEBUG: Calling GlobalTeardown\n"); fflush(stdout);
GlobalTeardown();
try {
printf("DEBUG: Calling GlobalTeardown\n"); fflush(stdout);
GlobalTeardown();
}
catch (const std::exception& e) {
fprintf(stderr, "GlobalTeardown threw std::exception: %s\n", e.what());
fflush(stderr);
retval = 1;
}
catch (...) {
fprintf(stderr, "GlobalTeardown threw an unknown exception\n");
fflush(stderr);
retval = 1;
}
printf("DEBUG: unit++ main end (retval=%d)\n", retval); fflush(stdout);
return retval;
}
Expand Down
6 changes: 6 additions & 0 deletions Src/AssemblyInfoForTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
// Set stub for messagebox so that we don't pop up a message box when running tests.
[assembly: SetMessageBoxAdapter]

// Log last-chance managed exceptions to console output before process termination.
[assembly: LogUnhandledExceptions]

// Suppress all assertion dialog boxes (native + managed) regardless of config file coverage
[assembly: SuppressAssertDialogs]

// Cleanup all singletons after running tests
[assembly: CleanupSingletons]

Expand Down
6 changes: 6 additions & 0 deletions Src/AssemblyInfoForUiIndependentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
// This file is for test fixtures for UI independent projects, i.e. projects that don't
// reference System.Windows.Forms et al.

// Log last-chance managed exceptions to console output before process termination.
[assembly: LogUnhandledExceptions]

// Suppress all assertion dialog boxes (native + managed) regardless of config file coverage
[assembly: SuppressAssertDialogs]

// Cleanup all singletons after running tests
[assembly: CleanupSingletons]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ public override void AfterTest(ITest test)

private void OnThreadException(object sender, ThreadExceptionEventArgs e)
{
Console.Error.WriteLine("Unhandled Windows Forms thread exception during test run:");
Console.Error.WriteLine(e.Exception.ToString());
Console.Error.Flush();

throw new ApplicationException(e.Exception.Message, e.Exception);
}
}
Expand Down
Loading
Loading