diff --git a/.gitattributes b/.gitattributes index 51ddc4a..90f3e68 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,4 +16,5 @@ *.jpg -text *.png -text *.gif -text +*.ico -text *.pdf -text diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e4d362..1b9fb81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,11 +35,13 @@ jobs: generator: "" build_args: "--config Release" exe_path: build/bin/Release/ASWUnitTests.exe + gui_exe_path: build/bin/Release/ASWUnitTests_Win32_GUI.exe - name: Windows (MinGW) os: windows-latest generator: "MinGW Makefiles" build_args: "" exe_path: build/bin/ASWUnitTests.exe + gui_exe_path: build/bin/ASWUnitTests_Win32_GUI.exe - name: Linux (GCC) os: ubuntu-latest generator: "" @@ -79,8 +81,11 @@ jobs: - name: Build shell: bash + # Four jobs, one per core of a hosted runner. The Windows jobs build the Win32 GUI runner too, which + # compiles the self-tests again; built one file at a time, that would take the MinGW job close to its + # timeout. A bare --parallel would let make start a compiler for every file at once. run: | - cmake --build build ${{ matrix.build_args }} + cmake --build build ${{ matrix.build_args }} --parallel 4 if [ $? -ne 0 ]; then echo "Build failed!" exit 1 @@ -142,3 +147,52 @@ jobs: run: | ./${{ matrix.exe_path }} --project-name "ASWUnitTests-CI-Partition1" --partition-index 1 --partition-count 2 ./${{ matrix.exe_path }} --project-name "ASWUnitTests-CI-Partition2" --partition-index 2 --partition-count 2 + + - name: Run self-tests in the Win32 GUI + # Smoke test for the Win32 GUI runner (Windows jobs only): it opens its window, runs every test (--run), + # writes the same JUnit report as the console runner, then closes and exits with the console runner's + # exit code (--exit). Bash waits for a GUI program to end, unlike a Windows command prompt. The step's + # own timeout ends a GUI left waiting on a dialog that nobody can close, rather than leaving it to the + # job's. + if: matrix.gui_exe_path != '' + id: gui_tests + timeout-minutes: 3 + shell: bash + run: | + if [ ! -f "${{ matrix.gui_exe_path }}" ]; then + echo "ERROR: Executable not found at ${{ matrix.gui_exe_path }}" + exit 1 + fi + ./${{ matrix.gui_exe_path }} --project-name "ASWUnitTests-CI-Win32GUI" --run --exit \ + --report-junit gui-results.xml --test-timeout-seconds 60 --catch-crashes + + - name: Check the Win32 GUI's report + # The GUI ran the same tests as the console runner, so both reports list the same number. + if: matrix.gui_exe_path != '' + shell: bash + run: | + console=$(grep -o '.png` files. - `README.md` contains usage and integration examples. - `.uncrustify.cfg` and `.githooks/` define the repository formatting workflow. @@ -51,13 +55,25 @@ Keep all source files (`.cpp`/`.h`) plain ASCII, including comments. MSVC reads - Insert new class methods in alphabetical order within their existing section. If the class has named or visibly separated sections, preserve those sections and alphabetize only within the relevant section. - Insert new free functions in alphabetical order within their existing section. Apply the same section rule when functions are grouped. +- Lay out a new class's members by kind, each kind in a section of its own with its own access label, even when the label repeats: nested types first, then static data, then data members, then the methods, grouped by access (private, then protected, then public), with each group's static methods in a section before its other methods. See `TWin32GUIMainWindow` (`win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.h`) and `TStringCheckProbe` (`tests/UT_Helper_StringChecks.h`). - Follow the surrounding naming, indentation, brace, include, and comment style. Avoid unrelated formatting changes. -- Keep declarations and definitions consistent, and update all relevant project or build files when adding, removing or renaming source files, including `Deploy.bat`, which copies an explicit list of files. +- Keep declarations and definitions consistent, and update all relevant project or build files when adding, removing or renaming source files, including `Deploy.bat`, which copies an explicit list of files, and the `*_Sources.cmake` files (`src/`, `gui/src/`, `win32/gui/src/`), which list each folder's sources for projects that use this repository as a submodule. - Add or update focused tests for behavior changes, including error and platform-specific cases where applicable. +- Each unit under test has exactly one test module: `ASWUnitTests_X` is tested by `Test_ASWUnitTests_X.cpp`/`.h` (class `TTest_ASWUnitTests_X`, group name `"ASWUnitTests_X_Tests"`), in the matching tests folder, and its tests go there, however large it grows. Supporting code that isn't a test itself, such as a fixture group or a probe whose templates are instantiated for the tests, may go in a helper module named `UT_Helper_.cpp`/`.h` in the same folder (e.g. `tests/UT_Helper_StringChecks`), listed in the projects and build files like any other source file. +- RAD Studio's Win32 linker (ilink32) limits the line-number debug information of each module (its "Line number cache"), and `tests/Test_ASWUnitTests_TestBase.cpp` is close to that limit. A Win32 Debug build then fails with "Fatal: Exceeded memory limit for block Line number cache in module ...". Every distinct lambda registered as a test costs a `std::function` instantiation, so prefer a few tests that each check many cases (or parameterized tests, which share one callback) to hundreds of registered lambdas, and move heavy template instantiations into a `UT_Helper_` module, defined and explicitly instantiated in its `.cpp`. Check with the VCL console's Win32 Debug build, which links the most modules. + +## Naming and Style + +- New enums take an `E` prefix (`EWin32GUICommand`); classes and structs take `T`. Existing enums that use `T` (e.g. `TLogKind`, `TTestOutcome`) keep it. +- Write acronyms in identifiers and file names in uppercase: `GUI`, `CLI`, `RTL`, `UTF8`, `XML` (e.g. `TCLIParser`, `ToUTF8`, `ASWUnitTests_GUI_TestList`). `JUnit` is a name, not an acronym. Check an existing name against this rule before copying its casing. +- Compare a pointer, `std::function` or other nullable value with `nullptr` explicitly (`if (filter != nullptr)`), rather than relying on its conversion to `bool` (`if (filter)`). +- In a `.cpp` file, describe a function in a block comment between its `//----` separator line and its definition (`/*`, the function's name, a blank line, the description, `*/`), not in `//` lines above it; a function that needs no explanation gets no comment. Declarations in a header keep `//` comments. See `win32/gui/src/ASWUnitTests_Win32GUI_System.cpp`. +- Suppress an intentional MSVC warning with `#pragma warning(push)`, `#pragma warning(disable: N)` and `#pragma warning(pop)` around the whole function or block, guarded by `#if defined(_MSC_VER)`, never with `#pragma warning(suppress: N)`: MSVC checks some warnings (e.g. C4702) against the state at the end of the function, after a `suppress` has lapsed. +- Don't name an identifier `assert`, even a parameter: `assert()` is a macro wherever `` is included, and the VCL headers include it. ## Building and Testing -The RAD Studio debug build can be run from `rad370/Build_Win64x_Debug.bat` when the `Rad370` environment variable points to the RAD Studio installation. The release script is `rad370/Build_Win64x_Release.bat`. Neither pauses before exiting by default; pass `--pause` to have the console prompt "press enter to continue" first, e.g. when running from an IDE that would otherwise close the console immediately. +The RAD Studio debug build can be built with `rad370/Build_Win64x_Debug.bat`, run from `rad370/`, when the `Rad370` environment variable points to the RAD Studio installation. The release script is `rad370/Build_Win64x_Release.bat`. The scripts only build; the executable, `build/bin//ASWUnitTests.exe`, doesn't pause before exiting by default. Pass it `--pause` to have the console prompt "press enter to continue" first, e.g. when running from an IDE that would otherwise close the console immediately (the RAD Studio projects' debugger parameters do). For other environments, use the repository's CMake configuration when present or the IDE's native project configuration. Tests should be run outside the debugger unless debugging an expected exception is intentional. Before considering a change complete: @@ -66,7 +82,8 @@ For other environments, use the repository's CMake configuration when present or - Check compiler warnings and errors. - Validate both Windows-specific and portable paths when the change touches platform code. - When the change touches code guarded by `ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED`, or the exception handling around it, also build and run the VCL console project (`vcl/console/rad370/Build_Win64x_Debug.bat` and the Win32 script), since no other build compiles that code. -- When the change touches `vcl/gui/src/`, or framework code the GUI runner uses (such as `ITestRunObserver` or `ASWUnitTests_CLI`), also build the VCL console project (which runs the GUI's unit tests) and the GUI project (`vcl/gui/rad370/Build_Win64x_Debug.bat` and the Win32 script), then run the GUI and check the affected behavior by hand. +- When the change touches `vcl/gui/src/` or `gui/src/`, or framework code the GUI runners use (such as `ITestRunObserver` or `ASWUnitTests_CLI`), also build the VCL console project (which runs the GUI's unit tests) and the VCL GUI project (`vcl/gui/rad370/Build_Win64x_Debug.bat` and the Win32 script), then run the GUI and check the affected behavior by hand. +- When the change touches `win32/gui/src/` or `gui/src/`, or framework code the GUI runners use, also build the CMake `ASWUnitTests_Win32_GUI` target with MinGW, with Clang (llvm-mingw) and, when available, with MSVC, and the RAD Studio Win32 GUI project (`win32/gui/rad370/Build_Win64x_Debug.bat` and the Win32 script), then run the GUI and check the affected behavior by hand. A full `--run --exit` of each build (as CI does) checks that it still runs every test. ## Changelog @@ -87,4 +104,6 @@ git config --local core.hooksPath .githooks/ Wrap Markdown prose at 100 columns in `README.md`, `CONTRIBUTING.md` and `CHANGELOG.md`, re-wrapping the edited paragraph when a change makes a line too long. Don't split inline code or a link's text across lines. Code blocks and tables are exempt, and so is a line that only overflows because a single link or URL can't be split. +Don't use em dashes in anything written for this repository (documentation, comments, messages): use a hyphen with a space on each side (" - "), or restructure the sentence. + Review the final diff for accidental file changes, ordering regressions, missing includes, platform assumptions, and tests that do not actually exercise the changed behavior. Do not commit changes unless the user explicitly requests a commit. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b85114..f4d5a98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,160 @@ versioned baseline. ## [Unreleased] +## [1.5.0] - 2026-10-30 + +### Added + +- A Win32 GUI runner, `ASWUnitTests_Win32_GUI`, with the same window, features and command line + options as the VCL GUI runner, built on the Windows API alone, so it needs neither the VCL nor + RAD Studio: it builds with CMake (MSVC, MinGW, Clang) as well as with RAD Studio, for Win32 and + Win64x. It needs Windows 10 version 1703 or later. This repository's CMake project builds it on + Windows, and a project of your own can build it with your tests by including + `win32\gui\src\ASWUnitTests_Win32GUI_Sources.cmake`; the README's "Win32 GUI Runner" section + covers both, and RAD Studio. +- Themes in both GUI runners, chosen in **View > Theme**: **Use Windows Setting** (the default) + follows Windows' app mode setting, switching as soon as it changes, and **Light** and **Dark** + override it. **View > Theme > Dark Theme** chooses the dark look's colors: one of five of RAD + Studio's dark VCL styles (Carbon, the default, Charcoal Dark Slate, Cobalt XEMedia, Onyx Blue and + Tablet Dark), in the Win32 GUI runner also one of four tints of Carbon (Ember, Forest, Midnight + and Plum), or a theme file of your own in `%APPDATA%\ASWUnitTests\Themes`, shared by every test + app, which **Open Themes Folder** opens. The Win32 GUI runner's theme files are INI files that set + any of its colors by role, and **Save Theme Template...** saves Carbon's as one to start from; the + VCL GUI runner's are VCL style files (`.vsf`). The choice applies without a restart and is saved + in the layout file, `--layout-ignore` and `--layout-reset` cover it as they do the layout, and a + theme file's problems are reported in the log. The Win32 GUI runner paints the themes itself, so + it needs nothing more. The VCL GUI runner uses the VCL styles, so a project of your own that runs + its tests in it needs them among its custom styles (Project > Options > Application > + Appearance), Carbon at least, without which Dark shows the light look, and a `WinMain()` that + applies the saved theme (see Changed). The README's "GUI Runners" section has the details. +- An ASWUnitTests icon, `resource\ASWUnitTests.ico`, which this repository's console and GUI + runners have on Windows, with the PNGs it's made from and `resource\make_icon.ps1`, which draws + them. `ASWUNITTESTS_WIN32_GUI_SOURCES` includes `resource\ASWUnitTests_Icon.rc`, so a Win32 GUI + runner of your own built with CMake gets the icon too, in Explorer, its title bar and the taskbar; + to use your own, remove that script from the list (the README's "Win32 GUI Runner" shows how) and + name your icon `MAINICON`. `ASWUNITTESTS_RESOURCE_DIR` is the `resource` folder, which + `ASWUNITTESTS_WIN32_GUI_INCLUDE_DIRS` includes. +- `ASWUNITTESTS_CORE_SOURCES` in `src\ASWUnitTests_Sources.cmake`: the framework's sources without + `main.cpp`, for a runner with its own entry point (`ASWUNITTESTS_SOURCES` lists the same files as + before), and `gui\src\ASWUnitTests_GUI_Sources.cmake`, which lists the GUI runners' shared units + as `ASWUNITTESTS_GUI_SOURCES`. +- The string checks (`Equals`, `Contains`, `StartsWith`, `EndsWith` and `Matches`, with their `Not` + and `IC` forms) take two texts of different types: a `std::string` with a C string, and narrow + text with wide text, which didn't compile before. Two narrow or two wide texts are compared as + they are; any other pair as UTF-8. The README's "Comparing Strings of Different Types" has the + details. +- `Matches` and `NotMatches` take two C strings, narrow or wide, and in RTL builds a + `System::String`. +- In RTL builds, the string checks also take an `AnsiString`, a `UTF8String` or another + `AnsiStringT`, and two RTL strings or an RTL string with any other text. +- `CheckEmpty`/`AssertEmpty` and `CheckNotEmpty`/`AssertNotEmpty` take a C string, narrow or wide, + including a character array, which is checked as the text it holds rather than by its size. A + null C string fails both, shown as `(null)`. In RTL builds they also take an `AnsiString`, a + `UTF8String` or another `AnsiStringT`. +- A `TExceptNotEmpty` constructor that takes a detail, which `AssertNotEmpty` uses for a null C + string. +- `CheckNear`/`AssertNear` and `CheckNotNear`/`AssertNotNear` take any three integer or + floating-point types except `bool`, in any mix, e.g. `CheckNear(2.0, aFloat, 0.01, ...)` or + `CheckNear(10, count, 1, ...)`, which were ambiguous before, and `long double`. With any + floating-point value, all three are compared as their common type; three integers are compared + exactly and shown as integers. +- `SetExceptionExpected()`, `CheckThrows` and `AssertThrows` + take any type that can be thrown, e.g. `int`, `char const*` or a class that doesn't derive from + `std::exception`. Before, the Throws methods didn't compile with one, and + `SetExceptionExpected()` didn't compile with `int` and never matched a class. + Expecting a message substring from a type with no message (neither a `std::exception` nor an RTL + exception) fails, naming the type that was caught, e.g. `an object of type "int"`. A class also + matches a `std::exception` subclass derived from it, whose `what()` is then checked. +- A `TCLIParser::ParseArguments()` overload that takes the arguments as a + `std::vector`, the program name first, as in `argv`. +- `UTF8ToWide()` in `ASWUnitTests_Utils.h`, which converts UTF-8 to wide text (UTF-16 on Windows, + UTF-32 on Linux), and, on Windows, `CommandLineArguments()` and `SystemErrorText()`: a command + line split into UTF-8 arguments, and a `GetLastError()` code's description. +- `TConsoleUTF8Output` (`ASWUnitTests_Console.h`), which sets the Windows console's output code page + to UTF-8 while it exists and then restores it. The console runner uses it for the whole run. +- `TTestHandler::GetAppBitsStr()`, which returns `"64-bit"` or `"32-bit"`, and + `TTestHandler::GetBuildOS()`, which returns `"Windows"` or `"Linux"` (`"Unknown"` for any other + operating system), describing the build the framework was compiled for. + +### Changed + +- The first line of the banner `TTestHandler::Initialize()` logs now also shows the app bits and + build OS, e.g. `ASWUnitTests - Version 1.5.0 - 64-bit - Windows`. +- The VCL GUI runner's units that don't need the VCL moved from `vcl\gui\src` to a new `gui\src` + folder, shared with the Win32 GUI runner: `ASWUnitTests_GUI_CommandLine`, + `ASWUnitTests_GUI_Layout`, `ASWUnitTests_GUI_RunObserver`, `ASWUnitTests_GUI_Selection`, + `ASWUnitTests_GUI_Shuffle` and `ASWUnitTests_GUI_TestList`, beside the new + `ASWUnitTests_GUI_Commands`, `ASWUnitTests_GUI_INIDocument`, `ASWUnitTests_GUI_Theme`, + `ASWUnitTests_GUI_Tree` and `ASWUnitTests_GUI_View`. A project that builds the VCL GUI runner + must compile them from `gui\src` and add that folder to its include path. Their VCL-only parts + moved to the new `vcl\gui\src\ASWUnitTests_GUI_VCLSystem.cpp`, which the project must also + compile: `GUILayoutFilePath()`, `GUISelectionFilePath()`, and + `TGUICommandLine::ProcessArguments()`, now the free function `ProcessArguments()`. A `WinMain()` + modeled on `vcl\gui\rad370\ASWUnitTests_VCL_GUI.cpp` must follow the new one: it includes + `ASWUnitTests_GUI_Theme.h` and `ASWUnitTests_GUI_VCLSystem.h`, calls + `ASWUnitTests::ProcessArguments()`, applies the saved theme before creating the main form, and + passes the theme to the form's `Start()`, which now takes it as well as the options. The layout + functions now take a `TGUIINIDocument` instead of a `TCustomIniFile`, and a `TGUIRect` instead of + a `TRect`. +- The VCL GUI runner reads and writes its layout file (`%APPDATA%\ASWUnitTests\.ini`) + itself, the same way `TMemIniFile` did, so files saved by earlier versions load as before and are + written the same way. A size or position written in hexadecimal by hand is now ignored, as an + invalid value. The selection file's first line, a comment, now reads "ASWUnitTests GUI test + selection" instead of "ASWUnitTests VCL GUI test selection"; files saved before still load. +- On Windows, the console runner reads its arguments from the UTF-16 command line instead of + `argv`, which holds them in the ANSI code page, so a `--filter` pattern, `--project-name` or + `--report-junit` path can hold characters outside that code page; before, those became `?` or a + lookalike. While it runs, it also sets the console's output code page to UTF-8, so non-ASCII test + names and messages show correctly, and restores it afterwards. +- On Windows, `main.cpp` defines `_tmain` instead of `main`, so a project whose `TCHAR` maps to + `wchar_t` (in RAD Studio, "TCHAR maps to" set to `wchar_t`) links too. It stays `main` elsewhere. +- `TJUnitReportWriter::Write()` now takes its path as UTF-8 on Windows too, as the console runner + passes it. Code that calls it with a path in the ANSI code page that has non-ASCII characters must + convert the path to UTF-8 first. +- In RTL builds, a null C string compared with a `System::String` follows the same rules as with any + other text, instead of being empty text: it equals only another null C string, so it no longer + equals an empty `System::String`, and it matches nothing in `Contains`, `StartsWith` and + `EndsWith`. A test that relied on it being empty text must compare with an empty string instead. +- A negative or NaN tolerance now fails `CheckNotNear`/`AssertNotNear` as well as + `CheckNear`/`AssertNear`, with "Invalid tolerance ...: it must be zero or more". Before, the + NotNear methods passed with one, whatever the values. A test that passes a negative tolerance must + pass its absolute value instead. +- When a test expects a specific type with `SetExceptionExpected()` and throws an object + of another type that is neither a `std::exception` nor an RTL exception, the failure now reads + "expected exception type was not thrown (caught a different exception): a non-std::exception + object", as for any other wrong type, instead of "expected a specific exception type, but a + non-std::exception object was thrown instead." +- In RTL builds, `SetExceptionExpected()` no longer compiles with an RTL class that + isn't an `Exception` or a subclass (e.g. `TStringList`), which could never match, as the Throws + methods already didn't. Expect `Exception` or the class that's actually raised instead. + +### Fixed + +- The VCL GUI runner's progress bar kept showing the progress it had when a test first failed and it + turned red, instead of following the rest of the run, and the next run could then start with it + full. It now shows every finished test, and starts each run empty. +- The VCL GUI runner's tooltips for Run Selected and Copy Details showed their keyboard shortcut + twice, e.g. "(F9) (F9)". +- The VCL GUI runner's status bar cut off its first panel's text, e.g. "Checked: 44 / 44 show", and + now has room for up to "Checked: 9999 / 9999 shown". +- With `--catch-crashes` and `--test-timeout-seconds` together, a crash that came while a test was + logging or recording a result, such as one in a run observer's `OnLog()`, left the run waiting + forever at the next test. The run now continues with the next test, as after any other crash. +- A null C string compared with a `std::string` or `std::wstring` (e.g. + `CheckEquals(expectedText, nameOrNull, ...)`), or given to `Matches`, was converted to a string, + which is undefined behavior and usually crashed. It now follows the same rules as two C strings: + it equals only another null C string, matches nothing in the other checks, and a failure shows it + as `(null)`. +- With `--test-timeout-seconds`, on Windows, a test ran with the default floating-point environment + instead of the one the run started with: rounding to nearest, whatever rounding mode was set, and + with llvm-mingw, `long double` arithmetic at `double` precision. Each test now starts with the + floating-point environment of the thread that runs the tests, as it does without a timeout. +- With RAD Studio's 32-bit compiler (`bcc32c`), type names in failure messages, such as + `CheckIsType`'s, were shown as the compiler mangles them, e.g. `$N@TShape` or `$i`. They're now + shown as written, e.g. `N::TShape` or `int`, as with the other compilers. A pointer to a `const` + or `volatile` class, and a template whose argument is in an anonymous namespace, are still shown + as mangled. + ## [1.4.0] - 2026-10-14 ### Added @@ -521,7 +675,8 @@ Initial versioned release, switching the project to semantic versioning and addi (test group registration, `Check`/`Assert` methods, RAD Studio and CMake build support, etc.) is treated as the baseline and is not itemized commit-by-commit. -[Unreleased]: https://github.com/ASWSoftware/asw-unit-tests/compare/v1.4.0...HEAD +[Unreleased]: https://github.com/ASWSoftware/asw-unit-tests/compare/v1.5.0...HEAD +[1.5.0]: https://github.com/ASWSoftware/asw-unit-tests/compare/v1.4.0...v1.5.0 [1.4.0]: https://github.com/ASWSoftware/asw-unit-tests/compare/v1.3.0...v1.4.0 [1.3.0]: https://github.com/ASWSoftware/asw-unit-tests/compare/v1.2.1...v1.3.0 [1.2.1]: https://github.com/ASWSoftware/asw-unit-tests/compare/v1.2.0...v1.2.1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 193d271..baae966 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,11 @@ framework, and contributions are expected to keep it that way. 5. Check compiler warnings and errors; a change should not introduce new ones. 6. If you touched platform-specific code (`#if defined(_WIN32)` or similar), validate both the Windows and the portable path if you can. +7. If you touched the RTL support (`ASWUNITTESTS_RTL_EXCEPTIONS`), the GUI runners (`gui/src/`, + `vcl/gui/src/`, `win32/gui/src/`) or framework code they use, also build and run the projects + [AGENTS.md](AGENTS.md#building-and-testing) lists for them: the VCL console and VCL GUI projects, + the CMake `ASWUnitTests_Win32_GUI` target and the Win32 GUI's RAD Studio project. Check the + affected GUI behavior by hand. ## Formatting diff --git a/Deploy.bat b/Deploy.bat index fa6d836..b17efb9 100644 --- a/Deploy.bat +++ b/Deploy.bat @@ -22,7 +22,7 @@ if not exist "%TARGET_DIR%\." ( ) ) -for %%D in (build cmake rad370 src tests toTest vcl\console\rad370 vcl\gui\rad370 vcl\gui\src vcl\tests) do ( +for %%D in (build cmake gui\src gui\tests rad370 resource src tests toTest vcl\console\rad370 vcl\gui\rad370 vcl\gui\src vcl\tests win32\gui\rad370 win32\gui\src win32\tests) do ( if not exist "%TARGET_DIR%\%%D\." ( echo Creating %%D folder... mkdir "%TARGET_DIR%\%%D" @@ -43,6 +43,96 @@ call :CopyFile "README.md" "README.md" if errorlevel 1 exit /b 1 call :CopyFile "cmake\CMakeLists.txt" "cmake\CMakeLists.txt" if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_CommandLine.cpp" "gui\src\ASWUnitTests_GUI_CommandLine.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_CommandLine.h" "gui\src\ASWUnitTests_GUI_CommandLine.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_Commands.cpp" "gui\src\ASWUnitTests_GUI_Commands.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_Commands.h" "gui\src\ASWUnitTests_GUI_Commands.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_INIDocument.cpp" "gui\src\ASWUnitTests_GUI_INIDocument.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_INIDocument.h" "gui\src\ASWUnitTests_GUI_INIDocument.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_Layout.cpp" "gui\src\ASWUnitTests_GUI_Layout.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_Layout.h" "gui\src\ASWUnitTests_GUI_Layout.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_RunObserver.cpp" "gui\src\ASWUnitTests_GUI_RunObserver.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_RunObserver.h" "gui\src\ASWUnitTests_GUI_RunObserver.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_Selection.cpp" "gui\src\ASWUnitTests_GUI_Selection.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_Selection.h" "gui\src\ASWUnitTests_GUI_Selection.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_Shuffle.cpp" "gui\src\ASWUnitTests_GUI_Shuffle.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_Shuffle.h" "gui\src\ASWUnitTests_GUI_Shuffle.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_Sources.cmake" "gui\src\ASWUnitTests_GUI_Sources.cmake" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_TestList.cpp" "gui\src\ASWUnitTests_GUI_TestList.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_TestList.h" "gui\src\ASWUnitTests_GUI_TestList.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_Theme.cpp" "gui\src\ASWUnitTests_GUI_Theme.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_Theme.h" "gui\src\ASWUnitTests_GUI_Theme.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_Tree.cpp" "gui\src\ASWUnitTests_GUI_Tree.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_Tree.h" "gui\src\ASWUnitTests_GUI_Tree.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_View.cpp" "gui\src\ASWUnitTests_GUI_View.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\src\ASWUnitTests_GUI_View.h" "gui\src\ASWUnitTests_GUI_View.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_CommandLine.cpp" "gui\tests\Test_ASWUnitTests_GUI_CommandLine.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_CommandLine.h" "gui\tests\Test_ASWUnitTests_GUI_CommandLine.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_Commands.cpp" "gui\tests\Test_ASWUnitTests_GUI_Commands.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_Commands.h" "gui\tests\Test_ASWUnitTests_GUI_Commands.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_INIDocument.cpp" "gui\tests\Test_ASWUnitTests_GUI_INIDocument.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_INIDocument.h" "gui\tests\Test_ASWUnitTests_GUI_INIDocument.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_Layout.cpp" "gui\tests\Test_ASWUnitTests_GUI_Layout.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_Layout.h" "gui\tests\Test_ASWUnitTests_GUI_Layout.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_RunObserver.cpp" "gui\tests\Test_ASWUnitTests_GUI_RunObserver.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_RunObserver.h" "gui\tests\Test_ASWUnitTests_GUI_RunObserver.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_Selection.cpp" "gui\tests\Test_ASWUnitTests_GUI_Selection.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_Selection.h" "gui\tests\Test_ASWUnitTests_GUI_Selection.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_Shuffle.cpp" "gui\tests\Test_ASWUnitTests_GUI_Shuffle.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_Shuffle.h" "gui\tests\Test_ASWUnitTests_GUI_Shuffle.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_TestList.cpp" "gui\tests\Test_ASWUnitTests_GUI_TestList.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_TestList.h" "gui\tests\Test_ASWUnitTests_GUI_TestList.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_Theme.cpp" "gui\tests\Test_ASWUnitTests_GUI_Theme.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_Theme.h" "gui\tests\Test_ASWUnitTests_GUI_Theme.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_Tree.cpp" "gui\tests\Test_ASWUnitTests_GUI_Tree.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_Tree.h" "gui\tests\Test_ASWUnitTests_GUI_Tree.h" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_View.cpp" "gui\tests\Test_ASWUnitTests_GUI_View.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "gui\tests\Test_ASWUnitTests_GUI_View.h" "gui\tests\Test_ASWUnitTests_GUI_View.h" +if errorlevel 1 exit /b 1 call :CopyFile "rad370\ASWUnitTests.cbproj" "rad370\ASWUnitTests.cbproj" if errorlevel 1 exit /b 1 call :CopyFile "rad370\ASWUnitTestsPCH1.h" "rad370\ASWUnitTestsPCH1.h" @@ -55,6 +145,10 @@ call :CopyFile "rad370\Build_Win32_Debug.bat" "rad370\Build_Win32_Debug.bat" if errorlevel 1 exit /b 1 call :CopyFile "rad370\Build_Win32_Release.bat" "rad370\Build_Win32_Release.bat" if errorlevel 1 exit /b 1 +call :CopyFile "resource\ASWUnitTests.ico" "resource\ASWUnitTests.ico" +if errorlevel 1 exit /b 1 +call :CopyFile "resource\ASWUnitTests_Icon.rc" "resource\ASWUnitTests_Icon.rc" +if errorlevel 1 exit /b 1 call :CopyFile "src\ASWUnitTests_CLI.cpp" "src\ASWUnitTests_CLI.cpp" if errorlevel 1 exit /b 1 call :CopyFile "src\ASWUnitTests_CLI.h" "src\ASWUnitTests_CLI.h" @@ -155,6 +249,14 @@ call :CopyFile "tests\Test_ASWUnitTests_Version.cpp" "tests\Test_ASWUnitTests_Ve if errorlevel 1 exit /b 1 call :CopyFile "tests\Test_ASWUnitTests_Version.h" "tests\Test_ASWUnitTests_Version.h" if errorlevel 1 exit /b 1 +call :CopyFile "tests\UT_Helper_ExceptionTypes.cpp" "tests\UT_Helper_ExceptionTypes.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "tests\UT_Helper_ExceptionTypes.h" "tests\UT_Helper_ExceptionTypes.h" +if errorlevel 1 exit /b 1 +call :CopyFile "tests\UT_Helper_StringChecks.cpp" "tests\UT_Helper_StringChecks.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "tests\UT_Helper_StringChecks.h" "tests\UT_Helper_StringChecks.h" +if errorlevel 1 exit /b 1 call :CopyFile "toTest\ASWTools_Common.cpp" "toTest\ASWTools_Common.cpp" if errorlevel 1 exit /b 1 call :CopyFile "toTest\ASWTools_Common.h" "toTest\ASWTools_Common.h" @@ -195,83 +297,137 @@ call :CopyFile "vcl\gui\rad370\Build_Win32_Debug.bat" "vcl\gui\rad370\Build_Win3 if errorlevel 1 exit /b 1 call :CopyFile "vcl\gui\rad370\Build_Win32_Release.bat" "vcl\gui\rad370\Build_Win32_Release.bat" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_CommandLine.cpp" "vcl\gui\src\ASWUnitTests_GUI_CommandLine.cpp" +call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_MainForm.cpp" "vcl\gui\src\ASWUnitTests_GUI_MainForm.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_MainForm.dfm" "vcl\gui\src\ASWUnitTests_GUI_MainForm.dfm" +if errorlevel 1 exit /b 1 +call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_MainForm.h" "vcl\gui\src\ASWUnitTests_GUI_MainForm.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_CommandLine.h" "vcl\gui\src\ASWUnitTests_GUI_CommandLine.h" +call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_ShuffleSeedDialog.cpp" "vcl\gui\src\ASWUnitTests_GUI_ShuffleSeedDialog.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_Layout.cpp" "vcl\gui\src\ASWUnitTests_GUI_Layout.cpp" +call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_ShuffleSeedDialog.h" "vcl\gui\src\ASWUnitTests_GUI_ShuffleSeedDialog.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_Layout.h" "vcl\gui\src\ASWUnitTests_GUI_Layout.h" +call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_Strings.cpp" "vcl\gui\src\ASWUnitTests_GUI_Strings.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_MainForm.cpp" "vcl\gui\src\ASWUnitTests_GUI_MainForm.cpp" +call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_Strings.h" "vcl\gui\src\ASWUnitTests_GUI_Strings.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_MainForm.dfm" "vcl\gui\src\ASWUnitTests_GUI_MainForm.dfm" +call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_TextDialog.cpp" "vcl\gui\src\ASWUnitTests_GUI_TextDialog.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_MainForm.h" "vcl\gui\src\ASWUnitTests_GUI_MainForm.h" +call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_TextDialog.h" "vcl\gui\src\ASWUnitTests_GUI_TextDialog.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_RunObserver.cpp" "vcl\gui\src\ASWUnitTests_GUI_RunObserver.cpp" +call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_VCLSystem.cpp" "vcl\gui\src\ASWUnitTests_GUI_VCLSystem.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_RunObserver.h" "vcl\gui\src\ASWUnitTests_GUI_RunObserver.h" +call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_VCLSystem.h" "vcl\gui\src\ASWUnitTests_GUI_VCLSystem.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_Selection.cpp" "vcl\gui\src\ASWUnitTests_GUI_Selection.cpp" +call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_Strings.cpp" "vcl\tests\Test_ASWUnitTests_GUI_Strings.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_Selection.h" "vcl\gui\src\ASWUnitTests_GUI_Selection.h" +call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_Strings.h" "vcl\tests\Test_ASWUnitTests_GUI_Strings.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_Shuffle.cpp" "vcl\gui\src\ASWUnitTests_GUI_Shuffle.cpp" +call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_VCLSystem.cpp" "vcl\tests\Test_ASWUnitTests_GUI_VCLSystem.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_Shuffle.h" "vcl\gui\src\ASWUnitTests_GUI_Shuffle.h" +call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_VCLSystem.h" "vcl\tests\Test_ASWUnitTests_GUI_VCLSystem.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_ShuffleSeedDialog.cpp" "vcl\gui\src\ASWUnitTests_GUI_ShuffleSeedDialog.cpp" +call :CopyFile "vcl\tests\Test_ASWUnitTests_RTLExceptions.cpp" "vcl\tests\Test_ASWUnitTests_RTLExceptions.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_ShuffleSeedDialog.h" "vcl\gui\src\ASWUnitTests_GUI_ShuffleSeedDialog.h" +call :CopyFile "vcl\tests\Test_ASWUnitTests_RTLExceptions.h" "vcl\tests\Test_ASWUnitTests_RTLExceptions.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_Strings.cpp" "vcl\gui\src\ASWUnitTests_GUI_Strings.cpp" +call :CopyFile "vcl\tests\Test_ASWUnitTests_RTLStrings.cpp" "vcl\tests\Test_ASWUnitTests_RTLStrings.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_Strings.h" "vcl\gui\src\ASWUnitTests_GUI_Strings.h" +call :CopyFile "vcl\tests\Test_ASWUnitTests_RTLStrings.h" "vcl\tests\Test_ASWUnitTests_RTLStrings.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_TestList.cpp" "vcl\gui\src\ASWUnitTests_GUI_TestList.cpp" +call :CopyFile "win32\gui\rad370\ASWUnitTests_Win32_GUI.cbproj" "win32\gui\rad370\ASWUnitTests_Win32_GUI.cbproj" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_TestList.h" "vcl\gui\src\ASWUnitTests_GUI_TestList.h" +call :CopyFile "win32\gui\rad370\ASWUnitTests_Win32_GUIPCH1.h" "win32\gui\rad370\ASWUnitTests_Win32_GUIPCH1.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_TextDialog.cpp" "vcl\gui\src\ASWUnitTests_GUI_TextDialog.cpp" +call :CopyFile "win32\gui\rad370\Build_Win64x_Debug.bat" "win32\gui\rad370\Build_Win64x_Debug.bat" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\gui\src\ASWUnitTests_GUI_TextDialog.h" "vcl\gui\src\ASWUnitTests_GUI_TextDialog.h" +call :CopyFile "win32\gui\rad370\Build_Win64x_Release.bat" "win32\gui\rad370\Build_Win64x_Release.bat" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_CommandLine.cpp" "vcl\tests\Test_ASWUnitTests_GUI_CommandLine.cpp" +call :CopyFile "win32\gui\rad370\Build_Win32_Debug.bat" "win32\gui\rad370\Build_Win32_Debug.bat" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_CommandLine.h" "vcl\tests\Test_ASWUnitTests_GUI_CommandLine.h" +call :CopyFile "win32\gui\rad370\Build_Win32_Release.bat" "win32\gui\rad370\Build_Win32_Release.bat" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_Layout.cpp" "vcl\tests\Test_ASWUnitTests_GUI_Layout.cpp" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI.manifest" "win32\gui\src\ASWUnitTests_Win32GUI.manifest" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_Layout.h" "vcl\tests\Test_ASWUnitTests_GUI_Layout.h" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_DialogTemplate.cpp" "win32\gui\src\ASWUnitTests_Win32GUI_DialogTemplate.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_RunObserver.cpp" "vcl\tests\Test_ASWUnitTests_GUI_RunObserver.cpp" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_DialogTemplate.h" "win32\gui\src\ASWUnitTests_Win32GUI_DialogTemplate.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_RunObserver.h" "vcl\tests\Test_ASWUnitTests_GUI_RunObserver.h" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_DPI.cpp" "win32\gui\src\ASWUnitTests_Win32GUI_DPI.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_Selection.cpp" "vcl\tests\Test_ASWUnitTests_GUI_Selection.cpp" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_DPI.h" "win32\gui\src\ASWUnitTests_Win32GUI_DPI.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_Selection.h" "vcl\tests\Test_ASWUnitTests_GUI_Selection.h" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_Glyphs.cpp" "win32\gui\src\ASWUnitTests_Win32GUI_Glyphs.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_Shuffle.cpp" "vcl\tests\Test_ASWUnitTests_GUI_Shuffle.cpp" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_Glyphs.h" "win32\gui\src\ASWUnitTests_Win32GUI_Glyphs.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_Shuffle.h" "vcl\tests\Test_ASWUnitTests_GUI_Shuffle.h" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_MainLayout.cpp" "win32\gui\src\ASWUnitTests_Win32GUI_MainLayout.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_Strings.cpp" "vcl\tests\Test_ASWUnitTests_GUI_Strings.cpp" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_MainLayout.h" "win32\gui\src\ASWUnitTests_Win32GUI_MainLayout.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_Strings.h" "vcl\tests\Test_ASWUnitTests_GUI_Strings.h" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_MainWindow.cpp" "win32\gui\src\ASWUnitTests_Win32GUI_MainWindow.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_TestList.cpp" "vcl\tests\Test_ASWUnitTests_GUI_TestList.cpp" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_MainWindow.h" "win32\gui\src\ASWUnitTests_Win32GUI_MainWindow.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_GUI_TestList.h" "vcl\tests\Test_ASWUnitTests_GUI_TestList.h" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_Manifest.rc" "win32\gui\src\ASWUnitTests_Win32GUI_Manifest.rc" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_RTLExceptions.cpp" "vcl\tests\Test_ASWUnitTests_RTLExceptions.cpp" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_MessageDialog.cpp" "win32\gui\src\ASWUnitTests_Win32GUI_MessageDialog.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_RTLExceptions.h" "vcl\tests\Test_ASWUnitTests_RTLExceptions.h" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_MessageDialog.h" "win32\gui\src\ASWUnitTests_Win32GUI_MessageDialog.h" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_RTLStrings.cpp" "vcl\tests\Test_ASWUnitTests_RTLStrings.cpp" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_Palette.cpp" "win32\gui\src\ASWUnitTests_Win32GUI_Palette.cpp" if errorlevel 1 exit /b 1 -call :CopyFile "vcl\tests\Test_ASWUnitTests_RTLStrings.h" "vcl\tests\Test_ASWUnitTests_RTLStrings.h" +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_Palette.h" "win32\gui\src\ASWUnitTests_Win32GUI_Palette.h" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_ShuffleSeedDialog.cpp" "win32\gui\src\ASWUnitTests_Win32GUI_ShuffleSeedDialog.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_ShuffleSeedDialog.h" "win32\gui\src\ASWUnitTests_Win32GUI_ShuffleSeedDialog.h" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_Sources.cmake" "win32\gui\src\ASWUnitTests_Win32GUI_Sources.cmake" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_Splitter.cpp" "win32\gui\src\ASWUnitTests_Win32GUI_Splitter.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_Splitter.h" "win32\gui\src\ASWUnitTests_Win32GUI_Splitter.h" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_System.cpp" "win32\gui\src\ASWUnitTests_Win32GUI_System.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_System.h" "win32\gui\src\ASWUnitTests_Win32GUI_System.h" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_TextDialog.cpp" "win32\gui\src\ASWUnitTests_Win32GUI_TextDialog.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_TextDialog.h" "win32\gui\src\ASWUnitTests_Win32GUI_TextDialog.h" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_Theme.cpp" "win32\gui\src\ASWUnitTests_Win32GUI_Theme.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_Theme.h" "win32\gui\src\ASWUnitTests_Win32GUI_Theme.h" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_WinMain.cpp" "win32\gui\src\ASWUnitTests_Win32GUI_WinMain.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\gui\src\ASWUnitTests_Win32GUI_Windows.h" "win32\gui\src\ASWUnitTests_Win32GUI_Windows.h" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\tests\Test_ASWUnitTests_Win32GUI_DialogTemplate.cpp" "win32\tests\Test_ASWUnitTests_Win32GUI_DialogTemplate.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\tests\Test_ASWUnitTests_Win32GUI_DialogTemplate.h" "win32\tests\Test_ASWUnitTests_Win32GUI_DialogTemplate.h" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\tests\Test_ASWUnitTests_Win32GUI_DPI.cpp" "win32\tests\Test_ASWUnitTests_Win32GUI_DPI.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\tests\Test_ASWUnitTests_Win32GUI_DPI.h" "win32\tests\Test_ASWUnitTests_Win32GUI_DPI.h" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\tests\Test_ASWUnitTests_Win32GUI_Glyphs.cpp" "win32\tests\Test_ASWUnitTests_Win32GUI_Glyphs.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\tests\Test_ASWUnitTests_Win32GUI_Glyphs.h" "win32\tests\Test_ASWUnitTests_Win32GUI_Glyphs.h" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\tests\Test_ASWUnitTests_Win32GUI_MainLayout.cpp" "win32\tests\Test_ASWUnitTests_Win32GUI_MainLayout.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\tests\Test_ASWUnitTests_Win32GUI_MainLayout.h" "win32\tests\Test_ASWUnitTests_Win32GUI_MainLayout.h" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\tests\Test_ASWUnitTests_Win32GUI_Palette.cpp" "win32\tests\Test_ASWUnitTests_Win32GUI_Palette.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\tests\Test_ASWUnitTests_Win32GUI_Palette.h" "win32\tests\Test_ASWUnitTests_Win32GUI_Palette.h" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\tests\Test_ASWUnitTests_Win32GUI_System.cpp" "win32\tests\Test_ASWUnitTests_Win32GUI_System.cpp" +if errorlevel 1 exit /b 1 +call :CopyFile "win32\tests\Test_ASWUnitTests_Win32GUI_System.h" "win32\tests\Test_ASWUnitTests_Win32GUI_System.h" if errorlevel 1 exit /b 1 echo Deployment completed successfully. diff --git a/README.md b/README.md index 002ca6f..765c2b1 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Requires C++17 or higher; the project itself is built and tested at C++20. method and registers it with its group, so a test can't be written but left out of the group's constructor. - **[Test suites](#organizing-groups-into-suites)** - `TSuitePath` puts groups in nested suites, so - `--filter "Framework.*"` selects a whole suite, and the VCL GUI runner shows suites in its tree. + `--filter "Framework.*"` selects a whole suite, and the GUI runners show suites in their tree. - **Check and Assert methods** - `Check*` records a failure and lets the test continue; `Assert*` fails the test immediately. Covers `Equals`/`NotEquals`, `True`/`False`, `Null`/`NotNull`, `Same`/`NotSame`, `Empty`/`NotEmpty`, `IsType`/`IsNotType`, `Near`/`NotNear`, @@ -26,7 +26,7 @@ Requires C++17 or higher; the project itself is built and tested at C++20. `std::source_location` report the caller's function and line, without passing `__func__, __LINE__`. - **[Floating-point comparison](#comparing-floating-point-values)** - `CheckNear()`/`AssertNear()` - compare `float` and `double` values within a tolerance. + compare two values within a tolerance: floating point, integers, or a mix. - **[Expected exceptions](#expecting-a-specific-exception-type-or-message)** - `SetExceptionExpected()`, with an optional check of the exception's type and message, and [`CheckThrows()`/`CheckNoThrow()`](#checking-that-a-call-throws) for a single call. @@ -53,8 +53,10 @@ Requires C++17 or higher; the project itself is built and tested at C++20. failed test instead of losing the whole process. - **Unexpected exceptions** - An exception a test throws without expecting it fails only that test, with the exception's message, and the run carries on. -- **[VCL GUI runner](#vcl-gui-runner)** - A RAD Studio VCL app that runs the same tests, with a - checkbox tree to choose them, live pass/fail/skip status, and each failure's details and log. +- **[GUI runners](#gui-runners)** - A Windows app that runs the same tests, with a checkbox tree to + choose them, live pass/fail/skip status, and each failure's details and log: the + [Win32 GUI runner](#win32-gui-runner), built with CMake or RAD Studio, or the + [VCL GUI runner](#vcl-gui-runner), a RAD Studio VCL app. ## Reporting @@ -111,17 +113,19 @@ cmake --build build --config Release ``` The executable is written to `build/bin/Release/ASWUnitTests.exe` with multi-configuration -generators. The RAD Studio project writes its final executable to the same `build/bin/` -directory. +generators. On Windows it also builds the [Win32 GUI runner](#win32-gui-runner), +`ASWUnitTests_Win32_GUI.exe`, in the same directory. The RAD Studio project writes its final +executable to the same `build/bin/` directory. The CMake project does not need a separate version declaration (see [Versions](#versions)) because it currently builds the test executable directly rather than packaging or installing it. ## Versions -ASWUnitTests follows [Semantic Versioning](https://semver.org), shown by `--version` and the VCL -GUI's caption. Releases are tagged on `main` (e.g. `v1.1.0`). Between releases, the `develop` branch -carries the next planned version with a pre-release, e.g. `1.2.0-dev.1`, which comes before `1.2.0`. +ASWUnitTests follows [Semantic Versioning](https://semver.org), shown by `--version` and the GUI +runners' caption. Releases are tagged on `main` (e.g. `v1.1.0`). Between releases, the `develop` +branch carries the next planned version with a pre-release, e.g. `1.2.0-dev.1`, which comes before +`1.2.0`. `src/ASWUnitTests_Version.h` has the version as macros, for code that supports several ASWUnitTests versions, and as constants in the `ASWUnitTests` namespace: @@ -206,6 +210,12 @@ listing tests, and `--list` reports how many tests/groups matched out of the tot when output is redirected to a file, there's a record of why fewer tests ran or were listed than expected. Likewise, it always states whether shuffle is enabled and, if so, the seed in use. +On Windows, the console runner reads its arguments from the UTF-16 command line rather than the C +runtime's `argv`, which holds them in the ANSI code page, so a `--filter` pattern, a +`--project-name` or a `--report-junit` path can hold any character. While it runs, it also sets the +console's output code page to UTF-8, and restores it afterwards, so non-ASCII test names and +messages show correctly. Output redirected to a file or a pipe is UTF-8 either way. + `--shuffle` is a sanity check against hidden inter-test/inter-group coupling. The deterministic alphabetical default (see [Registering Tests](#registering-tests)) is for readable, reproducible output day-to-day, while `--shuffle` deliberately breaks that to surface tests that secretly depend @@ -232,9 +242,10 @@ partition, the same way `--list` can preview `--filter`. `--test-timeout-seconds` guards against a single test hanging (e.g. an infinite loop) forever. There is no default; without it, a hung test blocks the run indefinitely. When given, each test runs on its own worker thread while the main thread waits with that timeout; a test that finishes normally -(whether it passes, fails, or throws) is completely unaffected. A test that doesn't finish in time -is recorded as failed, with a message naming it and the timeout that was exceeded, and the run stops -there: no further tests or groups run, and the process exits with a dedicated exit code (`5`) +(whether it passes, fails, or throws) is completely unaffected. The worker thread starts with the +main thread's floating-point environment, such as its rounding mode. A test that doesn't finish in +time is recorded as failed, with a message naming it and the timeout that was exceeded, and the run +stops there: no further tests or groups run, and the process exits with a dedicated exit code (`5`) distinct from an ordinary test failure (`1`). If `--report-junit` was also given, the report still gets written, covering every test that completed before the timeout, plus the timed-out test's own synthetic failure entry. The abandoned worker thread itself is never joined or forcibly stopped, @@ -314,13 +325,14 @@ that would have run fails, with `SetUp_Group() exception: ` and the exception's detail, `TearDown_Group()` still runs, and the run carries on with the next group (exit code `1`). Skipped tests never affect the exit code. -## VCL GUI Runner +## GUI Runners -`vcl\gui\rad370\ASWUnitTests_VCL_GUI.cbproj` builds a RAD Studio VCL application that runs the same -tests as the console runner, in a window for choosing tests and reading their results. It runs this -repository's full self-test suite, and its `Build_*.bat` scripts write the executable to -`vcl\gui\rad370\\`. `vcl\gui\rad370\ASWUnitTests_VCL_Group.groupproj` opens it -together with the VCL console project. +Two GUI runners run the same tests as the console runner, in a window for choosing tests and +reading their results: the [VCL GUI runner](#vcl-gui-runner), a RAD Studio VCL application, and the +[Win32 GUI runner](#win32-gui-runner), which uses only what ships with Windows and builds with CMake +(MSVC, MinGW, Clang) or RAD Studio. They look and work the same, so this section describes both, and +their own sections cover building them and running your own tests in them. Each of this +repository's builds of them runs its full self-test suite. The window shows: @@ -346,8 +358,10 @@ The toolbar, whose commands are also in the Run and Tests menus: - **Select All** and **Select None** check or uncheck every shown test, and **Select Failed** checks only the shown tests that failed in the latest run. - **Copy Details** (Ctrl+Shift+C) copies the detail pane to the clipboard. -- **Filter** (Ctrl+F) shows only the tests whose `Group.Test` name contains its text, ignoring case, - with the same `*` and `?` wildcards as `--filter`. While it hides tests, checking them and Run +- **Filter** (Ctrl+F) shows only the tests whose `Group.Test` name contains its text, with the same + `*` and `?` wildcards as `--filter`. It ignores the case of the ASCII letters, as + [`CheckEqualsIC`](#comparing-strings-ignoring-case) does, so other letters, such as an accented + capital and small E, still have to match exactly. While it hides tests, checking them and Run Selected only apply to the shown ones, and hidden tests keep their check marks. **File > Export JUnit Report** saves the latest run's results as the same JUnit XML report @@ -365,7 +379,51 @@ When the window closes, it saves its size, position, maximized state, and panel `%APPDATA%\ASWUnitTests\.ini`, and restores them the next time it opens. A position on a monitor that's no longer connected is ignored. An `--exit` run doesn't save the layout. **View > Reset Layout** restores the default layout right away; `--layout-ignore` and `--layout-reset` -(below) control this from the command line. +(below) control this from the command line. Since the file is named after the executable, the two +GUI runners, and each project's own build of them, keep separate layouts. + +In both GUI runners, **View > Theme** chooses a light or dark look. **Use Windows Setting**, the +default, follows Windows' app mode setting (Settings > Personalization > Colors), and switches as +soon as it changes, but stays light while high contrast is on, so the window shows high contrast's +own colors. **Light** and **Dark** keep that look whatever Windows is set to. Light is the standard +Windows look. Dark is RAD Studio's Carbon VCL style in the VCL GUI runner, and the Win32 GUI runner +paints the same colors itself. A choice applies right away and is saved in the layout file at once, +so the next session starts with it; **View > Reset Layout** leaves it as it is. The Theme commands +are disabled during a run, and a change of the Windows setting during one applies when it ends. In +the Win32 GUI runner's dark look, the title bar is dark unless Windows shows the accent color on +title bars, which it then keeps while the window is active, and the Export JUnit Report dialog, +Windows' own, stays light. + +**View > Theme > Dark Theme** chooses the dark look's colors: a built-in theme or a theme file of +your own, used whenever the look is dark, with Dark or with Use Windows Setting (high contrast keeps +its own colors). Both runners have five of RAD Studio's dark VCL styles built in: Carbon, Charcoal +Dark Slate, Cobalt XEMedia, Onyx Blue and Tablet Dark, which the Win32 GUI runner paints in their +colors. The Win32 GUI runner also has four tints of Carbon: Ember (warm brown), Forest (green), +Midnight (blue) and Plum (purple), which keep Carbon's colors for how tests did. Theme files go in +`%APPDATA%\ASWUnitTests\Themes`, which **Open Themes Folder** opens, and are shared by every test +app; the menu lists each one by its file name, below the built-in themes, and leaves out a file +named like one of them. The choice is saved in the layout file with the theme, and a problem with a +theme file, such as a misspelled role, a color that isn't one or a missing file, is reported in the +Log tab, with Carbon's colors used in its place. + +The Win32 GUI runner's theme files are INI files, `.ini`, and **Save Theme Template...** saves +Carbon's colors as one to start from, every role listed under a comment on what its group colors: + +```ini +[Theme] +Base=Dark + +[Colors] +Window=#0F2238 +Panel=#1B3350 +``` + +`Base=Dark` is required. Each line under `[Colors]` sets a role's color as `#RRGGBB`, and a role +left out keeps Carbon's color. Choosing the theme again reads its file again, so a change shows at +once. The VCL GUI runner's theme files are VCL style files, `.vsf`, made with RAD Studio's +Bitmap Style Designer. Each needs a style name of its own, not that of a style the project already +has, such as Carbon. A style file is loaded once per session, so a change to it shows after a +restart. The window also remembers which tests are checked, in `%APPDATA%\ASWUnitTests\.selection`, as long as the command line doesn't choose them itself (no `--run`, `--filter`, or partition @@ -380,9 +438,9 @@ box itself starts empty. Since tests come and go between sessions, it adapts: The log notes what was restored. So **Select Failed**, closing the window, fixing the code, and reopening it leaves just those tests checked for Run Selected. -Tests run on the main (VCL) thread, so a test can create forms and controls. -`--test-timeout-seconds` runs each test on a worker thread instead, so it can't be combined with -tests like that; the log says so when it's given. +Tests run on the GUI's main thread, the one that runs its window, so a test can create windows (or, +in the VCL GUI runner, forms and controls). `--test-timeout-seconds` runs each test on a worker +thread instead, so it can't be combined with tests like that; the log says so when it's given. A run that a test's timeout or crash abandoned leaves **Run Selected** and **Run Failed** disabled until the GUI is restarted, since the timed-out test may still be running, and a crash may have left @@ -407,31 +465,107 @@ these differences: - `--exit` (GUI-only, requires `--run`) closes the window when that run finishes, and exits with the same code the console runner would; a run ended with Stop exits with `1`. Without `--exit`, the GUI always exits with `0`. -- `--layout-ignore` (GUI-only) neither loads nor saves the window layout, so the window opens with - the defaults and the saved layout is left as it is. -- `--layout-reset` (GUI-only) deletes the saved window layout and opens with the defaults, which is - a way to recover if a saved layout ever causes a problem. The layout is saved again when the - window closes. +- `--layout-ignore` (GUI-only) neither loads nor saves the window layout, or the theme, so the + window opens with the defaults and the saved ones are left as they are. A theme chosen in the View + menu then lasts only until the window closes. +- `--layout-reset` (GUI-only) deletes the saved window layout, and with it the theme, and opens with + the defaults, which is a way to recover if a saved layout ever causes a problem. The layout is + saved again when the window closes. A GUI process doesn't make a Windows command prompt wait for it, so a script that needs `--exit`'s -exit code should start it with `start /wait` (or its own language's equivalent): +exit code should start it with `start /wait` (or its own language's equivalent; Bash, including Git +Bash, waits for it anyway): ``` start /wait ASWUnitTests_VCL_GUI.exe --filter "*String*" --run --exit --report-junit results.xml echo %ERRORLEVEL% ``` -To run your own tests in the GUI, create a RAD Studio VCL application that defines -`ASWUNITTESTS_RTL_EXCEPTIONS`, and add to it: the framework's `src` files except `main.cpp`; the -files in `vcl\gui\src`, including `ASWUnitTests_GUI_MainForm.dfm`; your own test modules; and a -`WinMain()` modeled on `vcl\gui\rad370\ASWUnitTests_VCL_GUI.cpp`, which parses the command line and -starts the main form. - -The GUI is built on `ITestRunObserver` (in `src\ASWUnitTests_TestBase.h`), set with +The GUI runners are built on `ITestRunObserver` (in `src\ASWUnitTests_TestBase.h`), set with `TTestHandler::SetRunObserver()`. An observer receives the log output that would otherwise go to `std::cout`, as plain text without the console's color codes, is told as each test starts and finishes, and can stop a run between tests, so it can drive other kinds of runners too. +### VCL GUI Runner + +`vcl\gui\rad370\ASWUnitTests_VCL_GUI.cbproj` builds the VCL GUI runner, for Win32 and Win64x, and +its `Build_*.bat` scripts write the executable to `vcl\gui\rad370\\`. +`vcl\gui\rad370\ASWUnitTests_VCL_Group.groupproj` opens it together with the VCL console project. +It defines `ASWUNITTESTS_RTL_EXCEPTIONS`, so it has +[RTL exception support](#rad-studio-rtl-exceptions-vclfmx), and its tests can use the VCL. + +To run your own tests in it, create a RAD Studio VCL application that defines +`ASWUNITTESTS_RTL_EXCEPTIONS`, and add to it: the framework's `src` files except `main.cpp`; the +files in `gui\src` (shared by the GUI runners) and `vcl\gui\src`, including +`ASWUnitTests_GUI_MainForm.dfm`, with both folders on the include path; your own test modules; and +a `WinMain()` modeled on `vcl\gui\rad370\ASWUnitTests_VCL_GUI.cpp`, which parses the command line, +applies the saved theme and starts the main form. For the dark theme, also add RAD Studio's Carbon +style to the application (Project > Options > Application > Appearance, which this repository's +project has as `Custom_Styles`); without it, Dark shows the light look. The other built-in dark +themes (Charcoal Dark Slate, Cobalt XEMedia, Onyx Blue and Tablet Dark) are listed only if their +styles are added too. For the ASWUnitTests icon, set the application's icon to +`resource\ASWUnitTests.ico` there too, as this repository's project does. + +### Win32 GUI Runner + +The Win32 GUI runner uses only the Windows API, with the common controls and dialogs that ship with +Windows, and no VCL or third-party code, so it builds with the [CMake](#cmake) toolchains on Windows +(MSVC, MinGW, Clang) as well as with RAD Studio. It needs Windows 10 version 1703 or later. Win32 is +the name of the API: it builds as a 32-bit or a 64-bit program. Since it doesn't link the VCL, it +has no [RTL exception support](#rad-studio-rtl-exceptions-vclfmx); tests of VCL code belong in the +[VCL GUI runner](#vcl-gui-runner). + +On Windows, this repository's CMake project builds it as the `ASWUnitTests_Win32_GUI` target, next +to the console runner (e.g. `build/bin/Release/ASWUnitTests_Win32_GUI.exe`). +`win32\gui\rad370\ASWUnitTests_Win32_GUI.cbproj` builds it with RAD Studio, for Win32 and Win64x, +and its `Build_*.bat` scripts write the executable to `win32\gui\rad370\\`. + +To run your own tests in it with CMake, include `win32\gui\src\ASWUnitTests_Win32GUI_Sources.cmake` +after `project()`. Its `ASWUNITTESTS_WIN32_GUI_SOURCES` lists the framework's `src` files except +`main.cpp`, the files in `gui\src` (shared by the GUI runners) and `win32\gui\src`, including the +runner's own `WinMain()`, its manifest, and `resource\ASWUnitTests_Icon.rc`, which gives the +executable the ASWUnitTests icon. It also exports the include path (which has the `resource` folder, +for the icon), libraries and linker options they need: + +```cmake +include(third_party/asw-unit-tests/win32/gui/src/ASWUnitTests_Win32GUI_Sources.cmake) + +add_executable(MyTestsGUI WIN32 + ${ASWUNITTESTS_WIN32_GUI_SOURCES} + tests/Test_MyClass.cpp +) + +target_include_directories(MyTestsGUI PRIVATE + ${ASWUNITTESTS_WIN32_GUI_INCLUDE_DIRS} + tests +) + +target_link_libraries(MyTestsGUI PRIVATE ${ASWUNITTESTS_WIN32_GUI_LIBRARIES}) +target_link_options(MyTestsGUI PRIVATE ${ASWUNITTESTS_WIN32_GUI_LINK_OPTIONS}) +``` + +Link `Threads::Threads` too, as for the console runner (see [CMake](#cmake-1) under Submodule +Integration). With MinGW, also link its runtime statically +(`-static-libgcc -static-libstdc++ -static`, as this repository's `cmake\CMakeLists.txt` does) or +put its DLLs next to the executable, or the GUI may load another program's older `libstdc++-6.dll` +from `PATH` and fail to start. + +For an icon of your own, remove the ASWUnitTests one from the list before `add_executable()`, and +add a resource script of your own whose icon is named `MAINICON`, which the window then shows: + +```cmake +list(REMOVE_ITEM ASWUNITTESTS_WIN32_GUI_SOURCES "${ASWUNITTESTS_RESOURCE_DIR}/ASWUnitTests_Icon.rc") +``` + +With RAD Studio, the simplest start is a copy of `win32\gui\rad370\ASWUnitTests_Win32_GUI.cbproj`, +with its paths and test files changed. It's a C++ application with no framework +(`FrameworkType None`), with `TCHAR` mapped to `char`, since the runner's entry point is +`WinMain()`. It compiles the framework's `src` files except `main.cpp`, the files in `gui\src` and +`win32\gui\src`, with all three folders on the include path, and the test modules. Its manifest is +`win32\gui\src\ASWUnitTests_Win32GUI.manifest`, with DPI awareness set to None, so RAD Studio uses +the manifest as it is (the manifest declares per-monitor DPI awareness itself). Its icon is +`resource\ASWUnitTests.ico`, which RAD Studio embeds as `MAINICON`. + ## Registering Tests Test modules self-register with `TTestHandler` using the `ASW_REGISTER_TEST_GROUP` macro (declared @@ -497,8 +631,8 @@ group's qualified name, `Framework.Console.TTest_TConsole`, starts all of its te - `--filter "Framework.*"` runs every test in suite `Framework`, including those in `Console`, and `--filter "Framework.Console.*"` runs just those in `Console`. - `--list`, the console log and the `--report-junit` report show its tests as - `Framework.Console.TTest_TConsole.TestName`, and the [VCL GUI runner](#vcl-gui-runner)'s test tree - shows each suite as a node, whose check box checks or unchecks all of its tests. + `Framework.Console.TTest_TConsole.TestName`, and the [GUI runners](#gui-runners)' test tree shows + each suite as a node, whose check box checks or unchecks all of its tests. The registration can name the suite instead, leaving the constructor as it is: @@ -533,11 +667,21 @@ int32_t i = TStrTool::StrToInt32(invalid); ``` If the wrong exception type is thrown, or its message doesn't contain the given substring, the test -fails with a message showing what was actually caught. This only works for exceptions deriving from -`std::exception`, or from RAD Studio's RTL `Exception` class when -[RTL exception support](#rad-studio-rtl-exceptions-vclfmx) is enabled. A thrown object that doesn't -can't be inspected, so a type/message expectation against it fails with an explanatory message -rather than silently passing. +fails with a message showing what was actually caught. The type can be any type that can be thrown: +a `std::exception` class, RAD Studio's RTL `Exception` class or a subclass when +[RTL exception support](#rad-studio-rtl-exceptions-vclfmx) is enabled, a class of your own, or even +`int` or `char const*` (the type a string literal is thrown as): + +``` +SetExceptionExpected(__func__, __LINE__, "the legacy parser throws its error code"); +LegacyParse("x"); +``` + +Only a `std::exception` (its `what()`) and an RTL exception (its `Message`) have a message, so +expecting a substring from any other type fails, naming the type that was caught (e.g. +`caught an exception without a message: an object of type "int"`). A class that doesn't derive from +`std::exception` also matches a `std::exception` subclass derived from it, whose `what()` is then +checked. An expected exception never hides a failure elsewhere in the test. A failed `Assert*` still fails the test rather than counting as the expected exception, and so does a failed `Check*` earlier in @@ -557,11 +701,11 @@ CheckEquals(3u, list.Count(), __func__, __LINE__, "a failed call leaves the list CheckNoThrow([&] { list.At(2); }, __func__, __LINE__, "last index"); ``` -The type is matched like `SetExceptionExpected()`'s: polymorphically, and with -[RTL exception support](#rad-studio-rtl-exceptions-vclfmx), RTL exception types too. A failed -`Assert*`, `Fail()` or `Skip()` inside the lambda isn't taken as the thrown exception; it ends the -test as usual, unless the requested type is that framework exception (e.g. -`CheckThrows`). +The type is matched like `SetExceptionExpected()`'s: polymorphically, of any type (e.g. +`CheckThrows`), and with [RTL exception support](#rad-studio-rtl-exceptions-vclfmx), RTL +exception types too. A failed `Assert*`, `Fail()` or `Skip()` inside the lambda isn't taken as the +thrown exception; it ends the test as usual, unless the requested type is that framework exception +(e.g. `CheckThrows`). ### RAD Studio RTL Exceptions (VCL/FMX) @@ -617,10 +761,10 @@ writes its executable to `vcl\console\rad370\\`. The ### Comparing System::String (VCL/FMX) The same `ASWUNITTESTS_RTL_EXCEPTIONS` define (see [above](#rad-studio-rtl-exceptions-vclfmx)) also -lets the string `Check`/`Assert` methods take a `System::String` (`UnicodeString`): `Equals`, -`Contains`, `StartsWith`, `EndsWith`, and their `Not` and `IC` forms. At least one of the two texts -must be a `System::String`. The other may also be a `std::string`, `std::wstring` or C string, -including a literal: +lets the string `Check`/`Assert` methods take a `System::String` (`UnicodeString`), an `AnsiString`, +a `UTF8String` or another `AnsiStringT`: `Equals`, `Contains`, `StartsWith`, `EndsWith`, `Matches`, +and their `Not` and `IC` forms. The other text may be any of those, or a `std::string`, +`std::wstring` or C string, including a literal: ``` CheckEquals("Ready", Label1->Caption, __func__, __LINE__, "status after loading"); @@ -628,11 +772,13 @@ CheckStartsWithIC(Edit1->Text, L"https://", __func__, __LINE__, "a secure URL"); ``` Both texts are converted to UTF-8 and compared as two `std::string` values, so a failure shows them -the same way. Narrow text is read as UTF-8, as elsewhere in the framework, and a null C string is -empty text, as it is for `System::String`. A `System::String` converts implicitly from a number or a -character, but these methods don't accept either, so `CheckEquals(Edit1->Text, 5, ...)` is a compile -error rather than a comparison with `"5"`. Other RTL string types, such as `AnsiString`, need -converting to `System::String` first, and the message is still a `std::string`. +the same way, as for [other strings of different types](#comparing-strings-of-different-types). An +`AnsiString` is converted from its code page, and narrow text is read as UTF-8, as elsewhere in the +framework. `CheckMatches` matches with `std::wregex`, so `.` matches one UTF-16 code unit. A null C +string isn't empty text: it follows the [C string rules](#comparing-c-strings), so it doesn't equal +an empty `System::String`. A `System::String` converts implicitly from a number or a character, but +these methods don't accept either, so `CheckEquals(Edit1->Text, 5, ...)` is a compile error rather +than a comparison with `"5"`. The message is still a `std::string`. ### Omitting the Method and Line (C++20) @@ -688,18 +834,44 @@ A failed `CheckEquals` shows both strings, and a failed `CheckNotEquals` the val strings, `std::string` and `std::wstring` alike. Wide text is converted to UTF-8, and a null pointer is shown as `(null)`. -The [case-insensitive](#comparing-strings-ignoring-case), [substring](#checking-for-a-substring) -and [prefix and suffix](#checking-the-start-or-end-of-a-string) checks take two C strings too. In +The [case-insensitive](#comparing-strings-ignoring-case), [substring](#checking-for-a-substring), +[prefix and suffix](#checking-the-start-or-end-of-a-string) and +[regular expression](#matching-a-regular-expression) checks take two C strings too. In `CheckEqualsIC` and `CheckNotEqualsIC`, a null pointer only matches another null pointer, as in -`CheckEquals`. In `CheckContains`, `CheckStartsWith` and `CheckEndsWith`, and their `IC` forms, a -null pointer, as either the text or the part, matches nothing, so they fail and their `Not` forms -pass: +`CheckEquals`. In `CheckContains`, `CheckStartsWith`, `CheckEndsWith` and `CheckMatches`, and their +`IC` forms, a null pointer, as either the text or the part, matches nothing, so they fail and their +`Not` forms pass: ``` char const* const name = FindName(); // null if not found CheckStartsWith(name, "ASW", __func__, __LINE__, "fails, rather than crashing, when not found"); ``` +The same rules apply when a C string is compared with +[a string of another type](#comparing-strings-of-different-types). + +### Comparing Strings of Different Types + +The string checks (`Equals`, `Contains`, `StartsWith`, `EndsWith` and `Matches`, with their `Not` +and `IC` forms) also take two texts of different types: a `std::string` with a C string, narrow text +(`std::string`, `char const*`) with wide text (`std::wstring`, `wchar_t const*`), and, in +[RTL builds](#comparing-systemstring-vclfmx), the RTL string types. Narrow text is read as UTF-8, as +elsewhere in the framework. Two narrow texts, or two wide ones, are compared as they are. Any other +pair is converted to UTF-8 and compared as two `std::string` values, so a failure shows both texts +the same way: + +``` +std::wstring const title = window.Title(); +CheckEquals("Settings", title, __func__, __LINE__, "the window's title"); +``` + +`CheckMatches` and `CheckNotMatches` match two narrow texts with `std::regex`, where `.` matches one +byte of UTF-8, and any other pair with `std::wregex`, where `.` matches one wide character. On +Windows, that's one UTF-16 code unit, so a character beyond U+FFFF is two. + +A null C string follows the [same rules](#comparing-c-strings) whatever it's compared with: it +equals only another null C string, narrow or wide, and matches nothing in the other checks. + ### Comparing Pointers `CheckEquals`/`AssertEquals` and `CheckNotEquals`/`AssertNotEquals` compare any other two pointers @@ -735,8 +907,8 @@ CheckIsType(shape, __func__, __LINE__, "the factory makes a circle"); // Check failed for: "Test_Create" (42): Expected type "TCircle" but was "TSquare". the factory makes a circle ``` -Type names come from `std::type_info::name()`, demangled with GCC and Clang, so their exact form -(e.g. a namespace or `class ` prefix) depends on the compiler. With +Type names come from `std::type_info::name()`, demangled with GCC, Clang and RAD Studio's +compilers, so their exact form (e.g. a namespace or `class ` prefix) depends on the compiler. With [RTL exception support](#rad-studio-rtl-exceptions-vclfmx) enabled, a `TObject` descendant is named by its `ClassName()` instead (e.g. `TStringList`), the same with every compiler. @@ -818,7 +990,8 @@ CheckMatches(stamp, R"(\d{4}-\d{2}-\d{2})", __func__, __LINE__, "an ISO 8601 dat Wide text is matched with `std::wregex`, so `.` matches one wide character, and a failure shows it as UTF-8. An invalid pattern fails the check, showing `std::regex_error`'s description, rather than -throwing. +throwing. The text and the pattern may also be [C strings](#comparing-c-strings), or +[of different types](#comparing-strings-of-different-types). ### Comparing Integers of Different Types @@ -884,6 +1057,20 @@ sensible tolerance depends heavily on the magnitude and accumulated error of the A NaN is near nothing, not even another NaN, so `CheckNear` fails and `CheckNotNear` passes when either value is a NaN, whatever the tolerance. Equal infinities are near each other. +A negative or NaN tolerance is a mistake in the test, so both `CheckNear` and `CheckNotNear` fail +with it, showing the tolerance (e.g. `Invalid tolerance -0.100000: it must be zero or more`). + +The three values can be of any integer or floating-point types except `bool`, in any mix, such as +`CheckNear(2.0, aFloat, 0.01, ...)` or `CheckNear(10, count, 1, ...)`. When any of them is floating +point, all three are compared as their common type, as the built-in operators do, so an all-`float` +call keeps `float` arithmetic. Three integers are compared exactly, whatever their types, so +`-1` and `1u` are 2 apart, and a failure shows them as integers: + +``` +CheckNear(10, count, 1, __func__, __LINE__, "about ten"); +// Check failed for: "Test_Count" (42): Expected "10 (tolerance 1)" but was "12 (diff 2)". about ten +``` + ### Comparing Greater Than and Less Than `CheckGreaterThan`, `CheckGreaterThanOrEqual`, `CheckLessThan` and `CheckLessThanOrEqual`, and their @@ -904,10 +1091,11 @@ built-in operators do, and a NaN fails every check. ### Checking for Empty `CheckEmpty`/`AssertEmpty` and `CheckNotEmpty`/`AssertNotEmpty` take anything with an `empty()` -member, such as a `std::string`, `std::wstring`, `std::vector` or `std::map`, and with -[RTL support](#comparing-systemstring-vclfmx), a `System::String`. Unlike -`CheckTrue(list.empty(), ...)`, a failure shows what was there: the text, or the element count of a -container with a `size()`: +member, such as a `std::string`, `std::wstring`, `std::vector` or `std::map`, a C string +(`char const*` or `wchar_t const*`, including character arrays), and with +[RTL support](#comparing-systemstring-vclfmx), a `System::String`, `AnsiString` or `UTF8String`. +Unlike `CheckTrue(list.empty(), ...)`, a failure shows what was there: the text, or the element +count of a container with a `size()`: ``` CheckEmpty(errors, __func__, __LINE__, "no errors reported"); @@ -916,6 +1104,15 @@ CheckEmpty(GetWarning(), __func__, __LINE__, "no warning"); // Check failed for: "Test_Load" (43): Expected empty but was "low disk space". no warning ``` +A character array is checked as the text it holds, whatever its size, so `char name[32] = ""` is +empty. A null C string is neither empty nor not empty: both checks fail, showing it as `(null)`: + +``` +char const* name = nullptr; +CheckNotEmpty(name, __func__, __LINE__, "has a name"); +// Check failed for: "Test_Load" (44): Expected not empty but was "(null)". has a name +``` + ### Checking for Null `CheckNull`/`AssertNull` and `CheckNotNull`/`AssertNotNull` compare a value with `nullptr`. They @@ -1022,7 +1219,7 @@ void TTest_TMyClassToTest::TearDown_Test(ITestCase& /*testCase*/) A test name can be registered only once per group. Registering one again (with `RegisterTest()`, `RegisterTestCases()` or `ASW_TEST`) throws `TExceptDuplicateTestName` from the group's constructor, which stops the run at startup with a message naming the group and the test, since `--filter`, -`--list`, the JUnit report and the VCL GUI runner couldn't tell the two tests apart. +`--list`, the JUnit report and the GUI runners couldn't tell the two tests apart. ### Self-Registering Test Methods @@ -1076,7 +1273,7 @@ doesn't derive from `TAutoTestGroup`. The order is deterministic: a group runs its `ASW_TEST` tests first, in alphabetical order by name (whatever order they're defined in), then any tests its constructor registers with `RegisterTest()` or `RegisterTestCases()`, in the order it registers them, so the two styles can be mixed in one -group. `--shuffle`, `--filter`, `--list`, the JUnit report and the VCL GUI runner treat `ASW_TEST` +group. `--shuffle`, `--filter`, `--list`, the JUnit report and the GUI runners treat `ASW_TEST` tests exactly like any other. Parameterized tests are still registered in the constructor with `RegisterTestCases()`, next to their data. @@ -1127,18 +1324,21 @@ especially useful for reading `--filter` matches or a failing row's name in CI o ## Submodule Integration This repository's own `cmake\CMakeLists.txt`, `rad370\ASWUnitTests.cbproj`, and the RAD Studio -projects under `vcl` only build *this* repo's own example tests and `toTest` code for its own -development and CI. Don't use them from a consuming project, and don't modify them. Doing either -means your changes live inside the submodule and get lost or conflict the next time you update it. -Instead, add ASWUnitTests as a git submodule (e.g. into `third_party\asw-unit-tests`) and reference -its `src` files from your own project's build file, alongside your own test modules: +projects under `vcl` and `win32` only build *this* repo's own example tests and `toTest` code for +its own development and CI. Don't use them from a consuming project, and don't modify them. Doing +either means your changes live inside the submodule and get lost or conflict the next time you +update it. Instead, add ASWUnitTests as a git submodule (e.g. into `third_party\asw-unit-tests`) and +reference its `src` files from your own project's build file, alongside your own test modules: ``` my-project/ +-- third_party/asw-unit-tests/ <- git submodule, never modified, freely updated | +-- src/ <- framework core (never touched) +| +-- gui/src/ <- shared by the GUI runners, if you use one (never touched) | +-- vcl/gui/src/ <- VCL GUI runner, if you use it (never touched) -| `-- cmake/, rad370/, tests/, toTest/, other vcl/ folders <- this framework's own example build, unused by you +| +-- win32/gui/src/ <- Win32 GUI runner, if you use it (never touched) +| +-- resource/ <- the ASWUnitTests icon, for a runner's executable (never touched) +| `-- cmake/, rad370/, tests/, toTest/, other vcl/ and win32/ folders <- this framework's own example build, unused by you +-- tests/ <- your own test modules (Test_MyClass.cpp/.h), self-registered `-- CMakeLists.txt / .cbproj <- your own build file, in your own repo ``` @@ -1176,16 +1376,26 @@ find_package(Threads REQUIRED) target_link_libraries(MyTests PRIVATE Threads::Threads) ``` +`ASWUNITTESTS_CORE_SOURCES` lists the same files without `main.cpp`, for a runner with an entry +point of its own. For a GUI target, `win32\gui\src\ASWUnitTests_Win32GUI_Sources.cmake` does the +same for the [Win32 GUI runner](#win32-gui-runner); see that section. + ### RAD Studio / Visual Studio / other IDE projects Add the same files listed in `ASWUNITTESTS_SOURCES` from the submodule's `src` folder to your own project, plus your own test modules. Check `src\ASWUnitTests_Sources.cmake` for added files after updating the submodule. +On Windows, `main.cpp` defines `_tmain` (`main` elsewhere), so a project that maps `TCHAR` to +`wchar_t`, such as a RAD Studio project with "TCHAR maps to" set to `wchar_t`, links as well as one +that maps it to `char`. On Windows, the framework also uses `CommandLineToArgvW()`, from `shell32`, +which MSVC, MinGW and RAD Studio link by default. + For a C++Builder project that links the VCL or FMX, also define `ASWUNITTESTS_RTL_EXCEPTIONS` to enable [RTL exception support](#rad-studio-rtl-exceptions-vclfmx) and [`System::String` comparisons](#comparing-systemstring-vclfmx). To run your tests in the -[VCL GUI runner](#vcl-gui-runner) instead of a console, see that section for the files it needs. +[VCL GUI runner](#vcl-gui-runner) or the [Win32 GUI runner](#win32-gui-runner) instead of a console, +see its section for the files it needs. # Coding Standards diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index deac8dd..038ee0f 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -7,9 +7,22 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) include(${CMAKE_CURRENT_SOURCE_DIR}/../src/ASWUnitTests_Sources.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/../gui/src/ASWUnitTests_GUI_Sources.cmake) -add_executable(ASWUnitTests - ${ASWUNITTESTS_SOURCES} +# The framework's self-tests, the shared GUI core's tests and the example code they test, which every runner +# compiles. +set(ASWUNITTESTS_SELF_TEST_SOURCES + ../gui/tests/Test_ASWUnitTests_GUI_CommandLine.cpp + ../gui/tests/Test_ASWUnitTests_GUI_Commands.cpp + ../gui/tests/Test_ASWUnitTests_GUI_INIDocument.cpp + ../gui/tests/Test_ASWUnitTests_GUI_Layout.cpp + ../gui/tests/Test_ASWUnitTests_GUI_RunObserver.cpp + ../gui/tests/Test_ASWUnitTests_GUI_Selection.cpp + ../gui/tests/Test_ASWUnitTests_GUI_Shuffle.cpp + ../gui/tests/Test_ASWUnitTests_GUI_TestList.cpp + ../gui/tests/Test_ASWUnitTests_GUI_Theme.cpp + ../gui/tests/Test_ASWUnitTests_GUI_Tree.cpp + ../gui/tests/Test_ASWUnitTests_GUI_View.cpp ../tests/Test_ASWTools_Random.cpp ../tests/Test_ASWTools_String.cpp ../tests/Test_ASWUnitTests_AutoTest.cpp @@ -23,12 +36,22 @@ add_executable(ASWUnitTests ../tests/Test_ASWUnitTests_TestBase.cpp ../tests/Test_ASWUnitTests_Utils.cpp ../tests/Test_ASWUnitTests_Version.cpp + ../tests/UT_Helper_ExceptionTypes.cpp + ../tests/UT_Helper_StringChecks.cpp ../toTest/ASWTools_Common.cpp ../toTest/ASWTools_Random.cpp ../toTest/ASWTools_String.cpp ) +add_executable(ASWUnitTests + ${ASWUNITTESTS_SOURCES} + ${ASWUNITTESTS_GUI_SOURCES} + ${ASWUNITTESTS_SELF_TEST_SOURCES} +) + target_include_directories(ASWUnitTests PRIVATE + ../gui/src + ../gui/tests ../src ../tests ../toTest @@ -41,7 +64,10 @@ find_package(Threads REQUIRED) target_link_libraries(ASWUnitTests PRIVATE Threads::Threads) if(MSVC) - target_compile_options(ASWUnitTests PRIVATE /W4 /Zc:__cplusplus) + # /MP compiles a target's files in parallel when MSBuild runs cl.exe, which otherwise compiles them one at a + # time (MSBuild's own parallelism only builds projects side by side). Other generators, such as Ninja, + # already run one compiler per file, and clang-cl ignores the option, so it's given to cl.exe only. + target_compile_options(ASWUnitTests PRIVATE /W4 /Zc:__cplusplus $<$:/MP>) else() target_compile_options(ASWUnitTests PRIVATE -Wall -Wextra -Wpedantic) endif() @@ -50,6 +76,33 @@ if(WIN32) # toTest/ASWTools_String.cpp uses the Win32 UUID/RPC string APIs. MSVC picks up # rpcrt4.lib via a #pragma comment, but MinGW/Clang need it linked explicitly. target_link_libraries(ASWUnitTests PRIVATE rpcrt4) + + # The Win32 GUI runner's units that need no window, and their tests, so they also run without the GUI. + target_sources(ASWUnitTests PRIVATE + ../win32/gui/src/ASWUnitTests_Win32GUI_DialogTemplate.cpp + ../win32/gui/src/ASWUnitTests_Win32GUI_DPI.cpp + ../win32/gui/src/ASWUnitTests_Win32GUI_Glyphs.cpp + ../win32/gui/src/ASWUnitTests_Win32GUI_MainLayout.cpp + ../win32/gui/src/ASWUnitTests_Win32GUI_Palette.cpp + ../win32/gui/src/ASWUnitTests_Win32GUI_System.cpp + ../win32/tests/Test_ASWUnitTests_Win32GUI_DialogTemplate.cpp + ../win32/tests/Test_ASWUnitTests_Win32GUI_DPI.cpp + ../win32/tests/Test_ASWUnitTests_Win32GUI_Glyphs.cpp + ../win32/tests/Test_ASWUnitTests_Win32GUI_MainLayout.cpp + ../win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp + ../win32/tests/Test_ASWUnitTests_Win32GUI_System.cpp + ) + target_include_directories(ASWUnitTests PRIVATE + ../win32/gui/src + ../win32/tests + ) + + # The ASWUnitTests icon, which the resource compiler finds through the include path. + target_sources(ASWUnitTests PRIVATE ../resource/ASWUnitTests_Icon.rc) + target_include_directories(ASWUnitTests PRIVATE ../resource) + # CommandLineToArgvW(), used by src/ASWUnitTests_Utils.cpp and the Win32 GUI's units. MSVC and MinGW link + # shell32 by default; naming it doesn't depend on that. + target_link_libraries(ASWUnitTests PRIVATE shell32) endif() if(MINGW) @@ -65,3 +118,53 @@ endif() set_target_properties(ASWUnitTests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) + +if(WIN32) + # The Win32 GUI runner: the same tests as the console runner, in a window. Its WinMain takes the place of + # the console runner's main.cpp. It's built from the same sources file a project using it would include, + # so this build also checks that file. + include(${CMAKE_CURRENT_SOURCE_DIR}/../win32/gui/src/ASWUnitTests_Win32GUI_Sources.cmake) + + add_executable(ASWUnitTests_Win32_GUI WIN32 + ${ASWUNITTESTS_WIN32_GUI_SOURCES} + ${ASWUNITTESTS_SELF_TEST_SOURCES} + ../win32/tests/Test_ASWUnitTests_Win32GUI_DialogTemplate.cpp + ../win32/tests/Test_ASWUnitTests_Win32GUI_DPI.cpp + ../win32/tests/Test_ASWUnitTests_Win32GUI_Glyphs.cpp + ../win32/tests/Test_ASWUnitTests_Win32GUI_MainLayout.cpp + ../win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp + ../win32/tests/Test_ASWUnitTests_Win32GUI_System.cpp + ) + + target_include_directories(ASWUnitTests_Win32_GUI PRIVATE + ${ASWUNITTESTS_WIN32_GUI_INCLUDE_DIRS} + ../gui/tests + ../tests + ../toTest + ../win32/tests + ) + + target_link_libraries(ASWUnitTests_Win32_GUI PRIVATE + ${ASWUNITTESTS_WIN32_GUI_LIBRARIES} + Threads::Threads + rpcrt4 + ) + + target_link_options(ASWUnitTests_Win32_GUI PRIVATE ${ASWUNITTESTS_WIN32_GUI_LINK_OPTIONS}) + + if(MSVC) + # /MP as for the console runner. + target_compile_options(ASWUnitTests_Win32_GUI PRIVATE /W4 /Zc:__cplusplus $<$:/MP>) + else() + target_compile_options(ASWUnitTests_Win32_GUI PRIVATE -Wall -Wextra -Wpedantic) + endif() + + if(MINGW) + # Statically linked, as the console runner is. + target_link_options(ASWUnitTests_Win32_GUI PRIVATE -static-libgcc -static-libstdc++ -static) + endif() + + set_target_properties(ASWUnitTests_Win32_GUI PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + ) +endif() diff --git a/vcl/gui/src/ASWUnitTests_GUI_CommandLine.cpp b/gui/src/ASWUnitTests_GUI_CommandLine.cpp similarity index 80% rename from vcl/gui/src/ASWUnitTests_GUI_CommandLine.cpp rename to gui/src/ASWUnitTests_GUI_CommandLine.cpp index e77dea9..f970480 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_CommandLine.cpp +++ b/gui/src/ASWUnitTests_GUI_CommandLine.cpp @@ -24,12 +24,8 @@ limitations under the License. // Module header #include "ASWUnitTests_GUI_CommandLine.h" //--------------------------------------------------------------------------- -#include -//--------------------------------------------------------------------------- #include "ASWUnitTests_StdOutRedirect.h" //--------------------------------------------------------------------------- -#include "ASWUnitTests_GUI_Strings.h" -//--------------------------------------------------------------------------- namespace ASWUnitTests { @@ -49,9 +45,10 @@ std::string TGUICommandLine::GUIOptionsHelp() " returning the same exit code the console runner would.\n" " Requires --run.\n" " --layout-ignore Neither load nor save the window layout (its size,\n" - " position, and panel sizes); open with the defaults.\n" - " --layout-reset Delete the saved window layout and open with the\n" - " defaults; the layout is saved again on closing.\n" + " position, and panel sizes) or the theme; open with the\n" + " defaults.\n" + " --layout-reset Delete the saved window layout (and the theme) and open\n" + " with the defaults; the layout is saved again on closing.\n" "\n" "Ignored by the GUI: --list, --pause, and the color options.\n"; } @@ -68,7 +65,7 @@ TGUICommandLineResult TGUICommandLine::Parse(std::vector const& arg // TCLIParser rejects options it doesn't know, so the GUI-only ones are taken out first. Its argv[0] is // the program name, which it skips. - std::vector cliArgs{ "ASWUnitTests_VCL_GUI" }; + std::vector cliArgs{ "ASWUnitTests_GUI" }; for (std::string const& arg : args) { @@ -84,13 +81,9 @@ TGUICommandLineResult TGUICommandLine::Parse(std::vector const& arg cliArgs.push_back(arg); } - std::vector argv; - for (std::string& arg : cliArgs) - argv.push_back(&arg[0]); - { TStdOutRedirect const capture; - result.EarlyExitCode = TCLIParser::ParseArguments(static_cast(argv.size()), argv.data(), options.CLI); + result.EarlyExitCode = TCLIParser::ParseArguments(cliArgs, options.CLI); result.Message = capture.Str(); } @@ -124,15 +117,5 @@ TGUICommandLineResult TGUICommandLine::Parse(std::vector const& arg return result; } //--------------------------------------------------------------------------- -std::vector TGUICommandLine::ProcessArguments() -{ - std::vector args; - - for (int i = 1; i <= System::ParamCount(); ++i) - args.push_back(ToUTF8(System::ParamStr(i))); - - return args; -} -//--------------------------------------------------------------------------- } // namespace ASWUnitTests diff --git a/vcl/gui/src/ASWUnitTests_GUI_CommandLine.h b/gui/src/ASWUnitTests_GUI_CommandLine.h similarity index 82% rename from vcl/gui/src/ASWUnitTests_GUI_CommandLine.h rename to gui/src/ASWUnitTests_GUI_CommandLine.h index 720a0fd..f727a82 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_CommandLine.h +++ b/gui/src/ASWUnitTests_GUI_CommandLine.h @@ -35,7 +35,7 @@ namespace ASWUnitTests ///////////////////////////////////////////////////////////////////////////// // TGUIOptions // -// The VCL GUI runner's command-line options: everything the console runner +// The GUI runners' command-line options: everything the console runner // accepts (in CLI), plus the GUI-only options. ///////////////////////////////////////////////////////////////////////////// struct TGUIOptions @@ -68,13 +68,15 @@ struct TGUICommandLineResult ///////////////////////////////////////////////////////////////////////////// // TGUICommandLine // -// Parses the VCL GUI runner's command line. The GUI-only options are handled +// Parses a GUI runner's command line. The GUI-only options are handled // here; everything else goes through the console runner's own TCLIParser, so -// both runners accept the same options with the same validation and help -// text. TCLIParser writes its messages to std::cout, which a GUI app has no -// console for, so Parse() captures them into the result's Message instead. -// Contains no VCL UI code, so it's unit tested from the VCL console runner -// (see vcl/tests/Test_ASWUnitTests_GUI_CommandLine.cpp). +// the GUI and console runners accept the same options with the same +// validation and help text. TCLIParser writes its messages to std::cout, +// which a GUI app has no console for, so Parse() captures them into the +// result's Message instead. Contains no UI code, so it's unit tested from the +// console runners (see gui/tests/Test_ASWUnitTests_GUI_CommandLine.cpp). Each +// GUI gets its process's arguments its own way, e.g. the VCL GUI's +// ProcessArguments() in vcl/gui/src/ASWUnitTests_GUI_VCLSystem.h. ///////////////////////////////////////////////////////////////////////////// class TGUICommandLine { @@ -84,11 +86,8 @@ class TGUICommandLine // The full help text, exactly as --help shows it: the console runner's usage text plus the GUI-only // options. static std::string HelpText(); - // Parses 'args' (the arguments without the program name, as ProcessArguments() returns them) into - // 'options'. + // Parses 'args' (the process's arguments without the program name, in UTF-8) into 'options'. static TGUICommandLineResult Parse(std::vector const& args, TGUIOptions& options); - // This process's command-line arguments, without the program name, converted to UTF-8. - static std::vector ProcessArguments(); }; } // namespace ASWUnitTests diff --git a/gui/src/ASWUnitTests_GUI_Commands.cpp b/gui/src/ASWUnitTests_GUI_Commands.cpp new file mode 100644 index 0000000..b5570ff --- /dev/null +++ b/gui/src/ASWUnitTests_GUI_Commands.cpp @@ -0,0 +1,64 @@ +/* ************************************************************************** +ASWUnitTests_GUI_Commands.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_GUI_Commands.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +//--------------------------------------------------------------------------- +TGUICommandStates GUICommandStates(TGUICommandContext const& context) +{ + bool const idle = !context.Running; + bool const canRun = idle && !context.RunAborted; + bool const anyFailed = context.FailedCount > 0; + + TGUICommandStates states; + + states.About = idle; + states.CollapseAll = context.TreeHasNodes; + states.CommandLineOptions = idle; + states.CopyDetails = context.HasSelection; + states.Exit = true; + states.ExpandAll = context.TreeHasNodes; + states.ExportJUnitReport = idle && context.HasReport; + states.FilterBox = idle; + states.FocusFilter = idle; + states.ResetLayout = true; + states.RunFailed = canRun && anyFailed; + states.RunSelected = canRun; + states.SelectAll = idle; + states.SelectFailed = idle && anyFailed; + states.SelectNone = idle; + states.ShuffleOrder = idle; + states.ShuffleSeed = idle; + states.Stop = context.Running; + states.Theme = idle; + + return states; +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/gui/src/ASWUnitTests_GUI_Commands.h b/gui/src/ASWUnitTests_GUI_Commands.h new file mode 100644 index 0000000..3889c77 --- /dev/null +++ b/gui/src/ASWUnitTests_GUI_Commands.h @@ -0,0 +1,105 @@ +/* ************************************************************************** +ASWUnitTests_GUI_Commands.h +Author: Anthony S. West - ASW Software + +Which of a GUI runner's commands are enabled, independent of the menus and +buttons that offer them. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_GUI_CommandsH +#define ASWUnitTests_GUI_CommandsH +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TGUICommandContext +// +// What decides which of a GUI runner's commands are enabled; see +// GUICommandStates(). +///////////////////////////////////////////////////////////////////////////// +struct TGUICommandContext +{ + // Number of tests that failed in the latest run, visible or not. + size_t FailedCount = 0; + // Whether a finished run's JUnit report can be exported. + bool HasReport = false; + // Whether a test tree node is selected, so the detail pane shows something. + bool HasSelection = false; + // Whether a run has ended with a test that timed out or crashed, after which no further run can start. + bool RunAborted = false; + bool Running = false; + // Whether the test tree has any nodes, i.e. the filter box doesn't hide every test. + bool TreeHasNodes = false; +}; + + +///////////////////////////////////////////////////////////////////////////// +// TGUICommandStates +// +// Whether each of a GUI runner's commands is enabled, one member per +// command, named as in the VCL GUI runner's actions (e.g. RunSelected for +// Act_RunSelected), plus the filter box. Theme is the GUI runners' View > +// Theme menu, its commands together, its Dark Theme submenu's included. +///////////////////////////////////////////////////////////////////////////// +struct TGUICommandStates +{ + bool About = false; + bool CollapseAll = false; + bool CommandLineOptions = false; + bool CopyDetails = false; + bool Exit = false; + bool ExpandAll = false; + bool ExportJUnitReport = false; + bool FilterBox = false; + bool FocusFilter = false; + bool ResetLayout = false; + bool RunFailed = false; + bool RunSelected = false; + bool SelectAll = false; + bool SelectFailed = false; + bool SelectNone = false; + bool ShuffleOrder = false; + bool ShuffleSeed = false; + bool Stop = false; + bool Theme = false; +}; + +//--------------------------------------------------------------------------- + +// Which of a GUI runner's commands are enabled in 'context'. Contains no UI code, so it's unit tested from the +// console runners (see gui/tests/Test_ASWUnitTests_GUI_Commands.cpp). The rules: +// - During a run, only Stop and the commands that can't affect it (Exit, which stops the run first, Copy +// Details, Expand All, Collapse All, and Reset Layout) are enabled. Tests run on the GUI's main thread, so a +// modal dialog (About, Command Line Options, Shuffle Seed, Export JUnit Report) would hold up the run until +// it closed; the run's tests and order are already decided, so checking tests and changing the shuffle +// would only mislead; changing the filter would rebuild the tree the run is updating; and changing the theme +// would restyle (in the VCL GUI runner, recreate) the windows the run is updating. +// - Run Selected and Run Failed stay disabled once a run has been aborted. +// - Run Failed and Select Failed need a failed test, Export JUnit Report a finished run's report, Copy Details a +// selected node, and Expand All and Collapse All a tree with nodes. +TGUICommandStates GUICommandStates(TGUICommandContext const& context); + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_GUI_CommandsH diff --git a/gui/src/ASWUnitTests_GUI_INIDocument.cpp b/gui/src/ASWUnitTests_GUI_INIDocument.cpp new file mode 100644 index 0000000..92e8292 --- /dev/null +++ b/gui/src/ASWUnitTests_GUI_INIDocument.cpp @@ -0,0 +1,248 @@ +/* ************************************************************************** +ASWUnitTests_GUI_INIDocument.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_GUI_INIDocument.h" +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- + +namespace +{ + +//--------------------------------------------------------------------------- + +bool SameName(std::string const& a, std::string const& b); +char ToLowerASCII(char c); +std::string Trim(std::string const& text); + +//--------------------------------------------------------------------------- + +// Whether two section or key names are the same, ignoring ASCII case, as TMemIniFile matches them. +bool SameName(std::string const& a, std::string const& b) +{ + return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) + { + return ToLowerASCII(x) == ToLowerASCII(y); + }); +} +//--------------------------------------------------------------------------- +char ToLowerASCII(char c) +{ + return (c >= 'A' && c <= 'Z') ? static_cast(c - 'A' + 'a') : c; +} +//--------------------------------------------------------------------------- +// 'text' without the spaces, tabs and other control characters at either end, as Delphi's Trim() removes. +std::string Trim(std::string const& text) +{ + auto const isTrimmed = [](char c) + { + return static_cast(c) <= ' '; + }; + + size_t start = 0; + while (start < text.size() && isTrimmed(text[start])) + ++start; + + size_t end = text.size(); + while (end > start && isTrimmed(text[end - 1])) + --end; + + return text.substr(start, end - start); +} + +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TGUIINIDocument +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TGUIINIDocument::TGUIINIDocument() +{ +} +//--------------------------------------------------------------------------- +TGUIINIDocument::TGUIINIDocument(std::string const& text) +{ + // Past a UTF-8 byte order mark, in case the file was edited with something that adds one. + size_t start = (text.compare(0, 3, "\xEF\xBB\xBF") == 0) ? 3 : 0; + + // The section the lines being read belong to; nullptr before the first one, and in a section that + // appeared before, whose lines are skipped. + TSection* section = nullptr; + + while (start < text.size()) + { + size_t end = text.find('\n', start); + if (end == std::string::npos) + end = text.size(); + + std::string const line = Trim(text.substr(start, end - start)); + start = end + 1; + + if (line.empty() || line[0] == ';') + continue; + + if (line[0] == '[' && line.back() == ']') + { + std::string const name = Trim(line.substr(1, line.size() - 2)); + + if (FindSection(name) != nullptr) + { + section = nullptr; + } + else + { + m_Sections.push_back(TSection{ std::vector(), name }); + section = &m_Sections.back(); + } + + continue; + } + + size_t const equals = line.find('='); + if (section == nullptr || equals == std::string::npos) + continue; + + section->Keys.push_back(TKey{ Trim(line.substr(0, equals)), Trim(line.substr(equals + 1)) }); + } +} +//--------------------------------------------------------------------------- +void TGUIINIDocument::DeleteKey(std::string const& section, std::string const& key) +{ + TSection* const found = FindSection(section); + if (found == nullptr) + return; + + auto const keyFound = std::find_if(found->Keys.begin(), found->Keys.end(), [&key](TKey const& candidate) + { + return SameName(candidate.Name, key); + }); + + if (keyFound != found->Keys.end()) + found->Keys.erase(keyFound); +} +//--------------------------------------------------------------------------- +TGUIINIDocument::TSection const* TGUIINIDocument::FindSection(std::string const& section) const +{ + for (TSection const& candidate : m_Sections) + { + if (SameName(candidate.Name, section)) + return &candidate; + } + + return nullptr; +} +//--------------------------------------------------------------------------- +TGUIINIDocument::TSection* TGUIINIDocument::FindSection(std::string const& section) +{ + return const_cast(static_cast(this)->FindSection(section)); +} +//--------------------------------------------------------------------------- +std::vector TGUIINIDocument::KeyNames(std::string const& section) const +{ + std::vector names; + TSection const* const found = FindSection(section); + + if (found != nullptr) + { + for (TKey const& key : found->Keys) + names.push_back(key.Name); + } + + return names; +} +//--------------------------------------------------------------------------- +std::optional TGUIINIDocument::ReadString(std::string const& section, std::string const& key) const +{ + TSection const* const found = FindSection(section); + if (found == nullptr) + return std::nullopt; + + for (TKey const& candidate : found->Keys) + { + if (SameName(candidate.Name, key)) + return candidate.Value; + } + + return std::nullopt; +} +//--------------------------------------------------------------------------- +std::vector TGUIINIDocument::SectionNames() const +{ + std::vector names; + + for (TSection const& section : m_Sections) + names.push_back(section.Name); + + return names; +} +//--------------------------------------------------------------------------- +std::string TGUIINIDocument::Text() const +{ + std::string text; + + for (TSection const& section : m_Sections) + { + text += "[" + section.Name + "]\r\n"; + + for (TKey const& key : section.Keys) + text += key.Name + "=" + key.Value + "\r\n"; + + text += "\r\n"; + } + + return text; +} +//--------------------------------------------------------------------------- +void TGUIINIDocument::WriteString(std::string const& section, std::string const& key, std::string const& value) +{ + TSection* found = FindSection(section); + if (found == nullptr) + { + m_Sections.push_back(TSection{ std::vector(), section }); + found = &m_Sections.back(); + } + + for (TKey& candidate : found->Keys) + { + if (SameName(candidate.Name, key)) + { + candidate.Value = value; + return; + } + } + + found->Keys.push_back(TKey{ key, value }); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/gui/src/ASWUnitTests_GUI_INIDocument.h b/gui/src/ASWUnitTests_GUI_INIDocument.h new file mode 100644 index 0000000..f19d0df --- /dev/null +++ b/gui/src/ASWUnitTests_GUI_INIDocument.h @@ -0,0 +1,101 @@ +/* ************************************************************************** +ASWUnitTests_GUI_INIDocument.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_GUI_INIDocumentH +#define ASWUnitTests_GUI_INIDocumentH +//--------------------------------------------------------------------------- +#include +#include +#include +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TGUIINIDocument +// +// The text of an INI file, such as a GUI runner's layout file, read and +// written the way the VCL's TMemIniFile does, so files saved by either one +// read the same with the other: +// - Section and key names are matched ignoring (ASCII) case. +// - Each line is trimmed; a key is the text before its line's first '=', +// and its value the text after it, each trimmed. +// - Blank lines, comment lines (starting with ';'), lines before the first +// section, and lines in a section with no '=' are skipped. So is a +// section that appears again after its first occurrence, with its keys. +// - A key that appears twice in a section reads as its first value. +// - Text() writes each section as "[Name]", its keys as "Name=Value", then a +// blank line, with CRLF line breaks and no byte order mark. Sections and +// keys keep the order they were read or first written in, so unknown ones +// are kept, but skipped lines aren't. +// Contains no UI code, so it's unit tested from the console runners (see +// gui/tests/Test_ASWUnitTests_GUI_INIDocument.cpp). +///////////////////////////////////////////////////////////////////////////// +class TGUIINIDocument +{ +private: + struct TKey + { + std::string Name; + std::string Value; + }; + + struct TSection + { + std::vector Keys; + std::string Name; + }; + +private: + std::vector m_Sections; + +private: + TSection const* FindSection(std::string const& section) const; + TSection* FindSection(std::string const& section); + +public: + // An empty document. + TGUIINIDocument(); + // Reads 'text', the file's content in UTF-8, past a UTF-8 byte order mark if it has one. Either CRLF or LF + // line breaks. + explicit TGUIINIDocument(std::string const& text); + + // Removes the key, if there is one. Its section stays, even if it's then empty. + void DeleteKey(std::string const& section, std::string const& key); + // The names of the section's keys, as written and in order, a repeated key's each time, as TMemIniFile's + // ReadSection() gives them; empty if the section doesn't exist. + std::vector KeyNames(std::string const& section) const; + // The key's value, or empty if the section or key doesn't exist. + std::optional ReadString(std::string const& section, std::string const& key) const; + // The names of the sections, as written and in order. + std::vector SectionNames() const; + // The document's text, as described above; empty for an empty document. + std::string Text() const; + // Sets the key's value, adding the key at the end of its section, and the section at the end of the + // document, if they don't exist yet. + void WriteString(std::string const& section, std::string const& key, std::string const& value); +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_GUI_INIDocumentH diff --git a/vcl/gui/src/ASWUnitTests_GUI_Layout.cpp b/gui/src/ASWUnitTests_GUI_Layout.cpp similarity index 61% rename from vcl/gui/src/ASWUnitTests_GUI_Layout.cpp rename to gui/src/ASWUnitTests_GUI_Layout.cpp index e08f41f..f858a8c 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_Layout.cpp +++ b/gui/src/ASWUnitTests_GUI_Layout.cpp @@ -25,10 +25,9 @@ limitations under the License. #include "ASWUnitTests_GUI_Layout.h" //--------------------------------------------------------------------------- #include +#include #include -//--------------------------------------------------------------------------- -#include -#include +#include //--------------------------------------------------------------------------- namespace @@ -47,27 +46,76 @@ int const MinWindowWidth = 200; int const MinGrabWidth = 100; int const TitleBarHeight = 30; -std::optional ReadInt(System::Inifiles::TCustomIniFile& ini, char const* section, char const* ident); -std::optional ReadSize(System::Inifiles::TCustomIniFile& ini, char const* section, char const* ident, int minimum); -void WriteOrDelete(System::Inifiles::TCustomIniFile& ini, char const* section, char const* ident, +//--------------------------------------------------------------------------- + +std::optional ParseInt(std::string const& text); +std::optional ReadInt(ASWUnitTests::TGUIINIDocument const& ini, char const* section, char const* ident); +std::optional ReadSize(ASWUnitTests::TGUIINIDocument const& ini, char const* section, char const* ident, + int minimum); +void WriteOrDelete(ASWUnitTests::TGUIINIDocument& ini, char const* section, char const* ident, std::optional const& value); //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- -// The value, or empty if it's missing or isn't a whole number. -std::optional ReadInt(System::Inifiles::TCustomIniFile& ini, char const* section, char const* ident) +/* + ParseInt + + 'text' as a whole decimal number, with an optional sign, or empty if it isn't one or doesn't fit in an int. +*/ +std::optional ParseInt(std::string const& text) { - int value = 0; + size_t start = 0; + bool const negative = !text.empty() && text[0] == '-'; + + if (!text.empty() && (text[0] == '-' || text[0] == '+')) + start = 1; - if (!System::Sysutils::TryStrToInt(ini.ReadString(section, ident, System::UnicodeString()), value)) + if (start == text.size()) return std::nullopt; - return value; + // Accumulated as a negative number, whose range includes INT_MIN. + long long value = 0; + + for (size_t i = start; i < text.size(); ++i) + { + if (text[i] < '0' || text[i] > '9') + return std::nullopt; + + value = value * 10 - (text[i] - '0'); + + if (value < INT_MIN) + return std::nullopt; + } + + if (!negative && -value > INT_MAX) + return std::nullopt; + + return static_cast(negative ? value : -value); } //--------------------------------------------------------------------------- -// The value, or empty if it's missing, isn't a whole number, or is less than 'minimum'. -std::optional ReadSize(System::Inifiles::TCustomIniFile& ini, char const* section, char const* ident, int minimum) +/* + ReadInt + + The value, or empty if it's missing or isn't a whole number. +*/ +std::optional ReadInt(ASWUnitTests::TGUIINIDocument const& ini, char const* section, char const* ident) +{ + std::optional const text = ini.ReadString(section, ident); + + if (!text.has_value()) + return std::nullopt; + + return ParseInt(*text); +} +//--------------------------------------------------------------------------- +/* + ReadSize + + The value, or empty if it's missing, isn't a whole number, or is less than 'minimum'. +*/ +std::optional ReadSize(ASWUnitTests::TGUIINIDocument const& ini, char const* section, char const* ident, + int minimum) { std::optional const value = ReadInt(ini, section, ident); @@ -77,11 +125,11 @@ std::optional ReadSize(System::Inifiles::TCustomIniFile& ini, char const* s return value; } //--------------------------------------------------------------------------- -void WriteOrDelete(System::Inifiles::TCustomIniFile& ini, char const* section, char const* ident, +void WriteOrDelete(ASWUnitTests::TGUIINIDocument& ini, char const* section, char const* ident, std::optional const& value) { if (value.has_value()) - ini.WriteInteger(section, ident, *value); + ini.WriteString(section, ident, std::to_string(*value)); else ini.DeleteKey(section, ident); } @@ -89,23 +137,13 @@ void WriteOrDelete(System::Inifiles::TCustomIniFile& ini, char const* section, c } // namespace +//--------------------------------------------------------------------------- namespace ASWUnitTests { //--------------------------------------------------------------------------- -System::UnicodeString GUILayoutFilePath() -{ - // GetHomePath() is %APPDATA% on Windows. - System::UnicodeString const folder = System::Ioutils::TPath::Combine(System::Ioutils::TPath::GetHomePath(), - "ASWUnitTests"); - System::UnicodeString const fileName = System::Sysutils::ChangeFileExt( - System::Sysutils::ExtractFileName(System::ParamStr(0)), ".ini"); - - return System::Ioutils::TPath::Combine(folder, fileName); -} -//--------------------------------------------------------------------------- -bool IsWindowReachable(System::Types::TRect const& bounds, std::vector const& workAreas) +bool IsWindowReachable(TGUIRect const& bounds, std::vector const& workAreas) { if (bounds.Width() <= 0 || bounds.Height() <= 0) return false; @@ -113,7 +151,7 @@ bool IsWindowReachable(System::Types::TRect const& bounds, std::vector= workArea.Top && titleBarBottom <= workArea.Bottom; @@ -125,12 +163,12 @@ bool IsWindowReachable(System::Types::TRect const& bounds, std::vector const height = ReadSize(ini, WindowSection, "Height", MinWindowHeight); @@ -152,7 +190,7 @@ TGUILayout LoadGUILayout(System::Inifiles::TCustomIniFile& ini) return layout; } //--------------------------------------------------------------------------- -void SaveGUILayout(System::Inifiles::TCustomIniFile& ini, TGUILayout const& layout) +void SaveGUILayout(TGUIINIDocument& ini, TGUILayout const& layout) { WriteOrDelete(ini, PanelsSection, "DetailHeight", layout.DetailHeight); WriteOrDelete(ini, PanelsSection, "TestTreeWidth", layout.TestTreeWidth); @@ -175,7 +213,7 @@ void SaveGUILayout(System::Inifiles::TCustomIniFile& ini, TGUILayout const& layo WriteOrDelete(ini, WindowSection, "Top", top); WriteOrDelete(ini, WindowSection, "Width", width); - ini.WriteBool(WindowSection, "Maximized", layout.Maximized); + ini.WriteString(WindowSection, "Maximized", layout.Maximized ? "1" : "0"); } //--------------------------------------------------------------------------- int ScaleFrom96(int value, int pixelsPerInch) @@ -191,22 +229,49 @@ int ScaleTo96(int value, int pixelsPerInch) return static_cast(std::lround(static_cast(value) * 96.0 / pixelsPerInch)); } //--------------------------------------------------------------------------- -System::Types::TRect ScreenToWorkspace(System::Types::TRect const& rect, System::Types::TRect const& primaryMonitor, - System::Types::TRect const& primaryWorkArea) +TGUIRect ScreenToWorkspace(TGUIRect const& rect, TGUIRect const& primaryMonitor, TGUIRect const& primaryWorkArea) { int const dx = primaryWorkArea.Left - primaryMonitor.Left; int const dy = primaryWorkArea.Top - primaryMonitor.Top; - return System::Types::TRect(rect.Left - dx, rect.Top - dy, rect.Right - dx, rect.Bottom - dy); + return TGUIRect(rect.Left - dx, rect.Top - dy, rect.Right - dx, rect.Bottom - dy); } //--------------------------------------------------------------------------- -System::Types::TRect WorkspaceToScreen(System::Types::TRect const& rect, System::Types::TRect const& primaryMonitor, - System::Types::TRect const& primaryWorkArea) +TGUIRect WorkspaceToScreen(TGUIRect const& rect, TGUIRect const& primaryMonitor, TGUIRect const& primaryWorkArea) { int const dx = primaryWorkArea.Left - primaryMonitor.Left; int const dy = primaryWorkArea.Top - primaryMonitor.Top; - return System::Types::TRect(rect.Left + dx, rect.Top + dy, rect.Right + dx, rect.Bottom + dy); + return TGUIRect(rect.Left + dx, rect.Top + dy, rect.Right + dx, rect.Bottom + dy); +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TGUIRect +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TGUIRect::TGUIRect() +{ +} +//--------------------------------------------------------------------------- +TGUIRect::TGUIRect(int left, int top, int right, int bottom) + : Bottom(bottom), + Left(left), + Right(right), + Top(top) +{ +} +//--------------------------------------------------------------------------- +int TGUIRect::Height() const +{ + return Bottom - Top; +} +//--------------------------------------------------------------------------- +int TGUIRect::Width() const +{ + return Right - Left; } //--------------------------------------------------------------------------- diff --git a/vcl/gui/src/ASWUnitTests_GUI_Layout.h b/gui/src/ASWUnitTests_GUI_Layout.h similarity index 61% rename from vcl/gui/src/ASWUnitTests_GUI_Layout.h rename to gui/src/ASWUnitTests_GUI_Layout.h index 17fcd6e..f646628 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_Layout.h +++ b/gui/src/ASWUnitTests_GUI_Layout.h @@ -25,14 +25,33 @@ limitations under the License. #include #include //--------------------------------------------------------------------------- -#include -#include -#include +#include "ASWUnitTests_GUI_INIDocument.h" //--------------------------------------------------------------------------- namespace ASWUnitTests { +///////////////////////////////////////////////////////////////////////////// +// TGUIRect +// +// A rectangle in pixels, like the Windows API's RECT: Right and Bottom are +// just outside it, so Width() is Right - Left. +///////////////////////////////////////////////////////////////////////////// +struct TGUIRect +{ + int Bottom = 0; + int Left = 0; + int Right = 0; + int Top = 0; + + TGUIRect(); + TGUIRect(int left, int top, int right, int bottom); + + int Height() const; + int Width() const; +}; + + ///////////////////////////////////////////////////////////////////////////// // TGUIWindowBounds // @@ -52,13 +71,15 @@ struct TGUIWindowBounds ///////////////////////////////////////////////////////////////////////////// // TGUILayout // -// The VCL GUI runner's window layout, saved when the window closes and -// restored when it next opens. Sizes are kept at 96 DPI (100% display -// scaling), converted with ScaleFrom96() and ScaleTo96(), so a layout saved -// at one display scaling still fits at another. A value that's missing, or -// wasn't valid when loaded, is empty, and the window keeps its default for -// it. Contains no VCL UI code, so it's unit tested from the VCL console -// runner (see vcl/tests/Test_ASWUnitTests_GUI_Layout.cpp). +// A GUI runner's window layout, saved when the window closes and restored +// when it next opens. Sizes are kept at 96 DPI (100% display scaling), +// converted with ScaleFrom96() and ScaleTo96(), so a layout saved at one +// display scaling still fits at another. A value that's missing, or wasn't +// valid when loaded, is empty, and the window keeps its default for it. +// Contains no UI code, so it's unit tested from the console runners (see +// gui/tests/Test_ASWUnitTests_GUI_Layout.cpp). Each GUI reads and writes the +// file itself, e.g. the VCL GUI's at GUILayoutFilePath() in +// vcl/gui/src/ASWUnitTests_GUI_VCLSystem.h. ///////////////////////////////////////////////////////////////////////////// struct TGUILayout { @@ -70,29 +91,26 @@ struct TGUILayout //--------------------------------------------------------------------------- -// This executable's layout file, %APPDATA%\ASWUnitTests\.ini, so each test app built on the GUI -// keeps its own layout, shared by all of its build configurations. -System::UnicodeString GUILayoutFilePath(); // Whether enough of a window's top edge, where its title bar is, lies on one of 'workAreas' for the window to // be dragged; e.g. false once the monitor it was on has been disconnected. All in screen pixels. -bool IsWindowReachable(System::Types::TRect const& bounds, std::vector const& workAreas); -// Reads the layout SaveGUILayout() wrote to 'ini'. -TGUILayout LoadGUILayout(System::Inifiles::TCustomIniFile& ini); -// Writes 'layout' to 'ini', removing each value it doesn't have. For a TMemIniFile, the file itself is only -// written by a later UpdateFile(). -void SaveGUILayout(System::Inifiles::TCustomIniFile& ini, TGUILayout const& layout); +bool IsWindowReachable(TGUIRect const& bounds, std::vector const& workAreas); +// Reads the layout SaveGUILayout() wrote to 'ini'. A size or position is read as a whole decimal number, and +// the maximized flag as maximized when it's a whole number other than 0, as the VCL's TCustomIniFile read them +// (though it also read hexadecimal, which a layout file never has). +TGUILayout LoadGUILayout(TGUIINIDocument const& ini); +// Writes 'layout' to 'ini', removing each value it doesn't have. The file itself is written from the +// document's Text() afterward. +void SaveGUILayout(TGUIINIDocument& ini, TGUILayout const& layout); // Converts a size at 96 DPI to one at 'pixelsPerInch', rounded to the nearest pixel. int ScaleFrom96(int value, int pixelsPerInch); // Converts a size at 'pixelsPerInch' to one at 96 DPI, rounded to the nearest pixel. int ScaleTo96(int value, int pixelsPerInch); // Converts 'rect' from screen coordinates to workspace coordinates, which SetWindowPlacement() uses; the // reverse of WorkspaceToScreen(). -System::Types::TRect ScreenToWorkspace(System::Types::TRect const& rect, System::Types::TRect const& primaryMonitor, - System::Types::TRect const& primaryWorkArea); +TGUIRect ScreenToWorkspace(TGUIRect const& rect, TGUIRect const& primaryMonitor, TGUIRect const& primaryWorkArea); // Converts 'rect' from workspace coordinates, which GetWindowPlacement() uses and which are relative to the // primary monitor's work area, to screen coordinates. -System::Types::TRect WorkspaceToScreen(System::Types::TRect const& rect, System::Types::TRect const& primaryMonitor, - System::Types::TRect const& primaryWorkArea); +TGUIRect WorkspaceToScreen(TGUIRect const& rect, TGUIRect const& primaryMonitor, TGUIRect const& primaryWorkArea); } // namespace ASWUnitTests diff --git a/vcl/gui/src/ASWUnitTests_GUI_RunObserver.cpp b/gui/src/ASWUnitTests_GUI_RunObserver.cpp similarity index 100% rename from vcl/gui/src/ASWUnitTests_GUI_RunObserver.cpp rename to gui/src/ASWUnitTests_GUI_RunObserver.cpp diff --git a/vcl/gui/src/ASWUnitTests_GUI_RunObserver.h b/gui/src/ASWUnitTests_GUI_RunObserver.h similarity index 93% rename from vcl/gui/src/ASWUnitTests_GUI_RunObserver.h rename to gui/src/ASWUnitTests_GUI_RunObserver.h index 93aeff7..5f694b7 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_RunObserver.h +++ b/gui/src/ASWUnitTests_GUI_RunObserver.h @@ -36,16 +36,16 @@ namespace ASWUnitTests ///////////////////////////////////////////////////////////////////////////// // TGUIRunObserver // -// The VCL GUI runner's ITestRunObserver. It contains no VCL code itself: it -// hands each event to the std::function callbacks below, which the main form -// sets, so its behavior can be unit tested without a window (see -// vcl/tests/Test_ASWUnitTests_GUI_RunObserver.cpp). +// The GUI runners' ITestRunObserver. It contains no UI code itself: it +// hands each event to the std::function callbacks below, which the main +// window sets, so its behavior can be unit tested without a window (see +// gui/tests/Test_ASWUnitTests_GUI_RunObserver.cpp). // // Log text can arrive from a test's worker thread (see ITestRunObserver), so // OnLog() only appends it to a buffer under a lock. The buffer is handed to // LogTextCallback when a test starts or finishes, and by Flush(), so every // callback runs on the thread that called TTestHandler::Run(), which for the -// GUI is its main (VCL) thread. +// GUI is its main (UI) thread. // // Text logged between a test's start and finish is also collected as that // test's own log and passed to TestFinishedCallback, for the detail pane. diff --git a/vcl/gui/src/ASWUnitTests_GUI_Selection.cpp b/gui/src/ASWUnitTests_GUI_Selection.cpp similarity index 94% rename from vcl/gui/src/ASWUnitTests_GUI_Selection.cpp rename to gui/src/ASWUnitTests_GUI_Selection.cpp index 5414075..f104146 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_Selection.cpp +++ b/gui/src/ASWUnitTests_GUI_Selection.cpp @@ -27,19 +27,17 @@ limitations under the License. #include #include //--------------------------------------------------------------------------- -#include -//--------------------------------------------------------------------------- -#include "ASWUnitTests_GUI_Layout.h" -//--------------------------------------------------------------------------- namespace { // The selection file's first line, describing the lines after it for anyone who opens it. char const SelectionFileComment[] = - "; ASWUnitTests VCL GUI test selection: \"+\" checked or \"-\" unchecked, then the group and test names, " + "; ASWUnitTests GUI test selection: \"+\" checked or \"-\" unchecked, then the group and test names, " "separated by a tab"; +//--------------------------------------------------------------------------- + bool IsStorableName(std::string const& name); //--------------------------------------------------------------------------- @@ -54,6 +52,7 @@ bool IsStorableName(std::string const& name) } // namespace +//--------------------------------------------------------------------------- namespace ASWUnitTests { @@ -109,11 +108,6 @@ std::string FormatGUISelection(std::vector const& tests) return text; } //--------------------------------------------------------------------------- -System::UnicodeString GUISelectionFilePath() -{ - return System::Sysutils::ChangeFileExt(GUILayoutFilePath(), ".selection"); -} -//--------------------------------------------------------------------------- std::vector ParseGUISelection(std::string const& text) { std::vector tests; diff --git a/vcl/gui/src/ASWUnitTests_GUI_Selection.h b/gui/src/ASWUnitTests_GUI_Selection.h similarity index 84% rename from vcl/gui/src/ASWUnitTests_GUI_Selection.h rename to gui/src/ASWUnitTests_GUI_Selection.h index 06175ea..8553823 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_Selection.h +++ b/gui/src/ASWUnitTests_GUI_Selection.h @@ -26,8 +26,6 @@ limitations under the License. #include #include //--------------------------------------------------------------------------- -#include -//--------------------------------------------------------------------------- #include "ASWUnitTests_GUI_TestList.h" //--------------------------------------------------------------------------- @@ -37,7 +35,7 @@ namespace ASWUnitTests ///////////////////////////////////////////////////////////////////////////// // TGUISavedTest // -// A test's check state, as the VCL GUI runner saves it between sessions. +// A test's check state, as a GUI runner saves it between sessions. ///////////////////////////////////////////////////////////////////////////// struct TGUISavedTest { @@ -64,10 +62,11 @@ struct TGUISelectionRestore //--------------------------------------------------------------------------- -// The VCL GUI runner's test selection, saved when the window closes and restored when it next opens, so the -// tests checked in one session stay checked in the next, even as tests are added, removed, or renamed in -// between. Contains no VCL UI code, so it's unit tested from the VCL console runner (see -// vcl/tests/Test_ASWUnitTests_GUI_Selection.cpp). +// A GUI runner's test selection, saved when the window closes and restored when it next opens, so the tests +// checked in one session stay checked in the next, even as tests are added, removed, or renamed in between. +// Contains no UI code, so it's unit tested from the console runners (see +// gui/tests/Test_ASWUnitTests_GUI_Selection.cpp). Each GUI reads and writes the file itself, e.g. the VCL +// GUI's at GUISelectionFilePath() in vcl/gui/src/ASWUnitTests_GUI_VCLSystem.h. // The selection to save: every test, checked only if it's both checked and shown, i.e. if Run Selected would // run it. @@ -78,9 +77,6 @@ std::string DescribeGUISelectionRestore(TGUISelectionRestore const& restore); // group name, a tab, and its test name. A test whose name has a tab or line break is left out, since its line // couldn't be read back; it's then treated as new when restored. std::string FormatGUISelection(std::vector const& tests); -// This executable's selection file, beside its layout file (see GUILayoutFilePath()): its name with a -// ".selection" extension. -System::UnicodeString GUISelectionFilePath(); // Reads the tests FormatGUISelection() wrote, skipping any line it can't read. std::vector ParseGUISelection(std::string const& text); // Checks 'testList's tests to match 'saved', best-guessing for tests the saved selection doesn't have. A saved diff --git a/vcl/gui/src/ASWUnitTests_GUI_Shuffle.cpp b/gui/src/ASWUnitTests_GUI_Shuffle.cpp similarity index 100% rename from vcl/gui/src/ASWUnitTests_GUI_Shuffle.cpp rename to gui/src/ASWUnitTests_GUI_Shuffle.cpp diff --git a/vcl/gui/src/ASWUnitTests_GUI_Shuffle.h b/gui/src/ASWUnitTests_GUI_Shuffle.h similarity index 95% rename from vcl/gui/src/ASWUnitTests_GUI_Shuffle.h rename to gui/src/ASWUnitTests_GUI_Shuffle.h index 4c31e8d..12dbf67 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_Shuffle.h +++ b/gui/src/ASWUnitTests_GUI_Shuffle.h @@ -2,7 +2,7 @@ ASWUnitTests_GUI_Shuffle.h Author: Anthony S. West - ASW Software -Whether the VCL GUI runner shuffles its runs, and with which seed. +Whether a GUI runner shuffles its runs, and with which seed. Copyright 2026 ASW Software @@ -40,8 +40,8 @@ namespace ASWUnitTests // seed, every run uses it, so each repeats the same order. Without one, each // run gets a new random seed, and the last one used is kept, so Shuffle // Seed... can fix it to repeat that run's order. -// Contains no VCL code, so it's unit tested from the VCL console runner -// (see vcl/tests/Test_ASWUnitTests_GUI_Shuffle.cpp). +// Contains no UI code, so it's unit tested from the console runners +// (see gui/tests/Test_ASWUnitTests_GUI_Shuffle.cpp). ///////////////////////////////////////////////////////////////////////////// class TGUIShuffle { diff --git a/gui/src/ASWUnitTests_GUI_Sources.cmake b/gui/src/ASWUnitTests_GUI_Sources.cmake new file mode 100644 index 0000000..9441808 --- /dev/null +++ b/gui/src/ASWUnitTests_GUI_Sources.cmake @@ -0,0 +1,25 @@ +# ASWUnitTests_GUI_Sources.cmake +# +# Exports the source files of the GUI runners' shared core, the units in this +# folder, which need neither Windows nor the VCL, as the +# ASWUNITTESTS_GUI_SOURCES list, and their directory as +# ASWUNITTESTS_GUI_SOURCE_DIR. A GUI runner compiles them together with the +# framework's core sources (src/ASWUnitTests_Sources.cmake) and its own +# units; win32/gui/src/ASWUnitTests_Win32GUI_Sources.cmake includes this +# file, so a project building the Win32 GUI runner needn't include it itself. + +set(ASWUNITTESTS_GUI_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}") + +set(ASWUNITTESTS_GUI_SOURCES + "${ASWUNITTESTS_GUI_SOURCE_DIR}/ASWUnitTests_GUI_CommandLine.cpp" + "${ASWUNITTESTS_GUI_SOURCE_DIR}/ASWUnitTests_GUI_Commands.cpp" + "${ASWUNITTESTS_GUI_SOURCE_DIR}/ASWUnitTests_GUI_INIDocument.cpp" + "${ASWUNITTESTS_GUI_SOURCE_DIR}/ASWUnitTests_GUI_Layout.cpp" + "${ASWUNITTESTS_GUI_SOURCE_DIR}/ASWUnitTests_GUI_RunObserver.cpp" + "${ASWUNITTESTS_GUI_SOURCE_DIR}/ASWUnitTests_GUI_Selection.cpp" + "${ASWUNITTESTS_GUI_SOURCE_DIR}/ASWUnitTests_GUI_Shuffle.cpp" + "${ASWUNITTESTS_GUI_SOURCE_DIR}/ASWUnitTests_GUI_TestList.cpp" + "${ASWUNITTESTS_GUI_SOURCE_DIR}/ASWUnitTests_GUI_Theme.cpp" + "${ASWUNITTESTS_GUI_SOURCE_DIR}/ASWUnitTests_GUI_Tree.cpp" + "${ASWUNITTESTS_GUI_SOURCE_DIR}/ASWUnitTests_GUI_View.cpp" +) diff --git a/vcl/gui/src/ASWUnitTests_GUI_TestList.cpp b/gui/src/ASWUnitTests_GUI_TestList.cpp similarity index 94% rename from vcl/gui/src/ASWUnitTests_GUI_TestList.cpp rename to gui/src/ASWUnitTests_GUI_TestList.cpp index 29909f1..41a8226 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_TestList.cpp +++ b/gui/src/ASWUnitTests_GUI_TestList.cpp @@ -54,9 +54,14 @@ TGUITestStatus StatusFromOutcome(TTestOutcome outcome) { switch (outcome) { - case TTestOutcome::Pass: return TGUITestStatus::Passed; - case TTestOutcome::Fail: return TGUITestStatus::Failed; - case TTestOutcome::Skip: return TGUITestStatus::Skipped; + case TTestOutcome::Pass: + return TGUITestStatus::Passed; + + case TTestOutcome::Fail: + return TGUITestStatus::Failed; + + case TTestOutcome::Skip: + return TGUITestStatus::Skipped; } return TGUITestStatus::Failed; @@ -66,11 +71,20 @@ std::string StatusName(TGUITestStatus status) { switch (status) { - case TGUITestStatus::NotRun: return "Not run"; - case TGUITestStatus::Running: return "Running"; - case TGUITestStatus::Passed: return "Passed"; - case TGUITestStatus::Failed: return "Failed"; - case TGUITestStatus::Skipped: return "Skipped"; + case TGUITestStatus::NotRun: + return "Not run"; + + case TGUITestStatus::Running: + return "Running"; + + case TGUITestStatus::Passed: + return "Passed"; + + case TGUITestStatus::Failed: + return "Failed"; + + case TGUITestStatus::Skipped: + return "Skipped"; } return "Unknown"; @@ -388,11 +402,24 @@ TGUITestStatus TGUITestList::StatusOf(std::vector const& indexes) const { switch (m_Statuses[index]) { - case TGUITestStatus::Running: anyRunning = true; break; - case TGUITestStatus::Failed: anyFailed = true; break; - case TGUITestStatus::Skipped: anySkipped = true; break; - case TGUITestStatus::Passed: anyPassed = true; break; - case TGUITestStatus::NotRun: break; + case TGUITestStatus::Running: + anyRunning = true; + break; + + case TGUITestStatus::Failed: + anyFailed = true; + break; + + case TGUITestStatus::Skipped: + anySkipped = true; + break; + + case TGUITestStatus::Passed: + anyPassed = true; + break; + + case TGUITestStatus::NotRun: + break; } } diff --git a/vcl/gui/src/ASWUnitTests_GUI_TestList.h b/gui/src/ASWUnitTests_GUI_TestList.h similarity index 95% rename from vcl/gui/src/ASWUnitTests_GUI_TestList.h rename to gui/src/ASWUnitTests_GUI_TestList.h index 7a0e68d..0b37e4c 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_TestList.h +++ b/gui/src/ASWUnitTests_GUI_TestList.h @@ -38,21 +38,21 @@ namespace ASWUnitTests ///////////////////////////////////////////////////////////////////////////// // TGUICheckState // -// A group's check state in the VCL GUI runner's test tree: all, none, or +// A group's check state in a GUI runner's test tree: all, none, or // only some of its tests checked. ///////////////////////////////////////////////////////////////////////////// enum class TGUICheckState { Unchecked, Checked, - Partial + Partial, }; ///////////////////////////////////////////////////////////////////////////// // TGUITestStatus // -// A test's status in the VCL GUI runner's current or latest run, in the +// A test's status in a GUI runner's current or latest run, in the // order of the tree's status image list, so a status converts directly to // its image index. ///////////////////////////////////////////////////////////////////////////// @@ -62,7 +62,7 @@ enum class TGUITestStatus Running, Passed, Failed, - Skipped + Skipped, }; // 'qualifiedName' (a group's qualified name, or a suite's path) without the 'suitePath' it's in and the '.' @@ -81,7 +81,7 @@ std::string StatusName(TGUITestStatus status); ///////////////////////////////////////////////////////////////////////////// // TGUITestResult // -// What the VCL GUI runner keeps about a test that finished in the current +// What a GUI runner keeps about a test that finished in the current // or latest run: its record, and the text it logged while running. ///////////////////////////////////////////////////////////////////////////// struct TGUITestResult @@ -94,15 +94,15 @@ struct TGUITestResult ///////////////////////////////////////////////////////////////////////////// // TGUITestList // -// The VCL GUI runner's list of registered tests and which of them are +// A GUI runner's list of registered tests and which of them are // checked to run, and each one's status and result, independent of how the // tree displays them. Tests are identified by their index, in the canonical order // TTestHandler::GetTests() returns them, and groups (by qualified name) and // suites (by path, including suites that only hold other suites) appear in // the order their first test does. A suite's tests are those of every group -// in it or in the suites inside it. Contains no VCL code, so it's unit -// tested from the VCL console runner (see -// vcl/tests/Test_ASWUnitTests_GUI_TestList.cpp). +// in it or in the suites inside it. Contains no UI code, so it's unit +// tested from the console runners (see +// gui/tests/Test_ASWUnitTests_GUI_TestList.cpp). // // Tests can be hidden by the filter box (see SetVisibleFilter()). While // some are, everything about checking and grouping applies only to the diff --git a/gui/src/ASWUnitTests_GUI_Theme.cpp b/gui/src/ASWUnitTests_GUI_Theme.cpp new file mode 100644 index 0000000..aeaa49c --- /dev/null +++ b/gui/src/ASWUnitTests_GUI_Theme.cpp @@ -0,0 +1,333 @@ +/* ************************************************************************** +ASWUnitTests_GUI_Theme.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_GUI_Theme.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +#include +//--------------------------------------------------------------------------- + +namespace +{ + +char const BaseKey[] = "Base"; +char const ColorsSection[] = "Colors"; +char const DarkThemeKey[] = "DarkTheme"; +char const ThemeKey[] = "Theme"; +char const ThemeSection[] = "Theme"; +char const ViewSection[] = "View"; + +//--------------------------------------------------------------------------- + +int HexDigitValue(char c); +bool LessText(std::string const& a, std::string const& b); +bool SameText(std::string const& a, std::string const& b); +char ToLowerASCII(char c); + +//--------------------------------------------------------------------------- +/* + HexDigitValue + + The value of 'c' as a hexadecimal digit, in either case; -1 if it isn't one. +*/ +int HexDigitValue(char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + + return -1; +} +//--------------------------------------------------------------------------- +/* + LessText + + Whether 'a' sorts before 'b', ignoring ASCII case. +*/ +bool LessText(std::string const& a, std::string const& b) +{ + return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(), [](char x, char y) + { + return ToLowerASCII(x) < ToLowerASCII(y); + }); +} +//--------------------------------------------------------------------------- +/* + SameText + + Whether 'a' and 'b' are the same, ignoring ASCII case, as the INI document matches section and key names. +*/ +bool SameText(std::string const& a, std::string const& b) +{ + return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) + { + return ToLowerASCII(x) == ToLowerASCII(y); + }); +} +//--------------------------------------------------------------------------- +char ToLowerASCII(char c) +{ + return (c >= 'A' && c <= 'Z') ? static_cast(c - 'A' + 'a') : c; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +//--------------------------------------------------------------------------- +std::string FormatGUIThemeColor(std::uint32_t rgb) +{ + char const digits[] = "0123456789ABCDEF"; + std::string text = "#"; + + for (int shift = 20; shift >= 0; shift -= 4) + text += digits[(rgb >> shift) & 0xF]; + + return text; +} +//--------------------------------------------------------------------------- +std::string GUIDefaultDarkThemeName() +{ + return "Carbon"; +} +//--------------------------------------------------------------------------- +bool GUIThemeIsDark(EGUITheme theme, bool windowsAppsUseDark, bool highContrast) +{ + switch (theme) + { + case EGUITheme::Dark: + return true; + + case EGUITheme::Light: + return false; + + case EGUITheme::Windows: + break; + } + + return windowsAppsUseDark && !highContrast; +} +//--------------------------------------------------------------------------- +std::vector GUIThemeNames(std::vector const& fileNames, std::string const& extension, + std::vector const& builtInNames) +{ + std::vector names; + + for (std::string const& fileName : fileNames) + { + if (fileName.size() <= extension.size() || + !SameText(fileName.substr(fileName.size() - extension.size()), extension)) + { + continue; + } + + std::string const name = fileName.substr(0, fileName.size() - extension.size()); + auto const sameName = [&name](std::string const& other) + { + return SameText(other, name); + }; + + if (std::none_of(names.begin(), names.end(), sameName) && + std::none_of(builtInNames.begin(), builtInNames.end(), sameName)) + { + names.push_back(name); + } + } + + std::stable_sort(names.begin(), names.end(), &LessText); + return names; +} +//--------------------------------------------------------------------------- +std::string GUIThemeProblemLine(std::string const& themeName, std::string const& problem) +{ + return "Dark theme \"" + themeName + "\": " + problem; +} +//--------------------------------------------------------------------------- +std::vector GUIVCLStyleDarkThemeNames() +{ + return { "Charcoal Dark Slate", "Cobalt XEMedia", "Onyx Blue", "Tablet Dark" }; +} +//--------------------------------------------------------------------------- +std::string LoadGUIDarkTheme(TGUIINIDocument const& ini) +{ + std::optional const name = ini.ReadString(ViewSection, DarkThemeKey); + + if (!name.has_value() || name->empty() || SameText(*name, GUIDefaultDarkThemeName())) + return GUIDefaultDarkThemeName(); + + return *name; +} +//--------------------------------------------------------------------------- +EGUITheme LoadGUITheme(TGUIINIDocument const& ini) +{ + std::optional const name = ini.ReadString(ViewSection, ThemeKey); + + if (name.has_value()) + { + if (SameText(*name, "Dark")) + return EGUITheme::Dark; + + if (SameText(*name, "Light")) + return EGUITheme::Light; + } + + return EGUITheme::Windows; +} +//--------------------------------------------------------------------------- +std::optional ParseGUIThemeColor(std::string const& text) +{ + if (text.size() != 7 || text[0] != '#') + return std::nullopt; + + std::uint32_t rgb = 0; + + for (std::size_t i = 1; i < text.size(); ++i) + { + int const digit = HexDigitValue(text[i]); + + if (digit < 0) + return std::nullopt; + + rgb = (rgb << 4) | static_cast(digit); + } + + return rgb; +} +//--------------------------------------------------------------------------- +/* + ReadGUIThemeFile + + Every problem is reported, even in a file that can't be used, so one look at the log shows them all. +*/ +TGUIThemeFile ReadGUIThemeFile(std::string const& text, std::vector const& roles) +{ + TGUIINIDocument const ini(text); + TGUIThemeFile file; + + for (std::string const& section : ini.SectionNames()) + { + if (!SameText(section, ThemeSection) && !SameText(section, ColorsSection)) + file.Problems.push_back("[" + section + "] isn't a section of a theme file, so it's ignored."); + } + + for (std::string const& key : ini.KeyNames(ThemeSection)) + { + if (!SameText(key, BaseKey)) + file.Problems.push_back("[Theme] " + key + " isn't a setting of a theme file, so it's ignored."); + } + + std::optional const base = ini.ReadString(ThemeSection, BaseKey); + + if (!base.has_value()) + { + file.Problems.push_back("[Theme] has no Base=Dark, which a theme file needs, so " + GUIDefaultDarkThemeName() + + " is used instead."); + } + else if (!SameText(*base, "Dark")) + { + file.Problems.push_back("[Theme] Base=" + *base + " isn't supported, only Base=Dark, so " + + GUIDefaultDarkThemeName() + " is used instead."); + } + + // The roles met so far, as 'roles' names them, so a repeat is reported, even of an invalid color. + std::vector met; + + for (std::string const& key : ini.KeyNames(ColorsSection)) + { + std::vector::const_iterator const role = std::find_if(roles.begin(), roles.end(), + [&key](std::string const& candidate) + { + return SameText(candidate, key); + }); + + if (role == roles.end()) + { + file.Problems.push_back("[Colors] " + key + " isn't a role a theme can color, so it's ignored."); + continue; + } + + if (std::find(met.begin(), met.end(), *role) != met.end()) + { + file.Problems.push_back("[Colors] " + key + " is set more than once, so only the first is used."); + continue; + } + + met.push_back(*role); + + // A repeated key reads as its first value, as TMemIniFile reads it. + std::string const value = ini.ReadString(ColorsSection, key).value_or(std::string()); + std::optional const rgb = ParseGUIThemeColor(value); + + if (!rgb.has_value()) + { + file.Problems.push_back("[Colors] " + key + "=" + value + " isn't a color written as #RRGGBB, so it's " + "ignored."); + continue; + } + + TGUIThemeColor color; + color.RGB = *rgb; + color.Role = *role; + file.Colors.push_back(color); + } + + file.Usable = base.has_value() && SameText(*base, "Dark"); + + if (!file.Usable) + file.Colors.clear(); + + return file; +} +//--------------------------------------------------------------------------- +void SaveGUIDarkTheme(TGUIINIDocument& ini, std::string const& name) +{ + ini.WriteString(ViewSection, DarkThemeKey, name); +} +//--------------------------------------------------------------------------- +void SaveGUITheme(TGUIINIDocument& ini, EGUITheme theme) +{ + char const* name = "Windows"; + + if (theme == EGUITheme::Dark) + name = "Dark"; + else if (theme == EGUITheme::Light) + name = "Light"; + + ini.WriteString(ViewSection, ThemeKey, name); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/gui/src/ASWUnitTests_GUI_Theme.h b/gui/src/ASWUnitTests_GUI_Theme.h new file mode 100644 index 0000000..0e4fce8 --- /dev/null +++ b/gui/src/ASWUnitTests_GUI_Theme.h @@ -0,0 +1,136 @@ +/* ************************************************************************** +ASWUnitTests_GUI_Theme.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_GUI_ThemeH +#define ASWUnitTests_GUI_ThemeH +//--------------------------------------------------------------------------- +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_INIDocument.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// EGUITheme +// +// A GUI runner's theme, as chosen in its View > Theme menu and saved in its +// layout file. Contains no UI code, so it's unit tested from the console +// runners (see gui/tests/Test_ASWUnitTests_GUI_Theme.cpp). +///////////////////////////////////////////////////////////////////////////// +enum class EGUITheme +{ + Windows, // Follows Windows' setting for apps, light or dark; the default. + Light, + Dark +}; + + +///////////////////////////////////////////////////////////////////////////// +// TGUIThemeColor +// +// One color a theme file sets: the role it colors, as the GUI runner names +// it, and the color, as 0xRRGGBB. +///////////////////////////////////////////////////////////////////////////// +struct TGUIThemeColor +{ + std::uint32_t RGB = 0; + std::string Role; +}; + + +///////////////////////////////////////////////////////////////////////////// +// TGUIThemeFile +// +// A theme file as ReadGUIThemeFile() read it. A theme file is an INI file +// in the GUI runners' Themes folder, named for its theme, that sets the +// dark look's colors (the Win32 GUI runner's; the VCL GUI runner's dark +// themes are VCL style files instead): +// [Theme] +// Base=Dark +// [Colors] +// Window=#202020 +// Base=Dark is required, so that a later version can add light themes. Each +// [Colors] key is a role's name, ignoring (ASCII) case, and its value a +// color as #RRGGBB, in either case; a role left out keeps the built-in +// dark theme's color. +///////////////////////////////////////////////////////////////////////////// +struct TGUIThemeFile +{ + // The colors the file sets, in its order, each role once, named as the GUI runner names it. + std::vector Colors; + // What's wrong with the file, one sentence each, for the log (see GUIThemeProblemLine()): an unknown + // section, setting or role, a color that isn't one, a role set twice, or a missing or unknown Base. + std::vector Problems; + // Whether the file can be used, which it can't without Base=Dark; Colors is then empty. + bool Usable = false; +}; + +//--------------------------------------------------------------------------- + +// 'rgb', 0xRRGGBB, as a theme file writes a color: "#RRGGBB", in uppercase. +std::string FormatGUIThemeColor(std::uint32_t rgb); +// The name of the built-in dark theme both runners have, "Carbon": the default for LoadGUIDarkTheme(), and one +// of the built-in names GUIThemeNames() leaves out, so a theme file can't take it. +std::string GUIDefaultDarkThemeName(); +// Whether 'theme' is dark: Light and Dark as chosen, and Windows as 'windowsAppsUseDark' (Windows' app mode +// setting) says, except that it's light while 'highContrast' is on, so the window shows high contrast's own +// colors. +bool GUIThemeIsDark(EGUITheme theme, bool windowsAppsUseDark, bool highContrast); +// The names of the themes among 'fileNames', the files in the Themes folder: each one that ends in +// 'extension' (e.g. ".ini"), ignoring (ASCII) case, without it, sorted ignoring case, and each name once. +// Leaves out 'builtInNames', the runner's built-in dark themes (GUIDefaultDarkThemeName() among them), ignoring +// case, which a file can't replace, and a file named just 'extension'. +std::vector GUIThemeNames(std::vector const& fileNames, std::string const& extension, + std::vector const& builtInNames); +// A line for the log about one of the dark theme 'themeName''s problems (e.g. one of TGUIThemeFile's), as both +// GUI runners word it: Dark theme "": +std::string GUIThemeProblemLine(std::string const& themeName, std::string const& problem); +// The dark themes both runners have built in besides Carbon, sorted: RAD Studio's dark VCL styles, by their +// style names, which the VCL GUI runner links in and the Win32 GUI runner has the colors of +// (BuiltInDarkThemes()). +std::vector GUIVCLStyleDarkThemeNames(); +// Reads the dark theme SaveGUIDarkTheme() wrote to 'ini', its [View] section's DarkTheme key: the name of a +// theme file, or GUIDefaultDarkThemeName() when it's missing or empty, or is that name in another case. +std::string LoadGUIDarkTheme(TGUIINIDocument const& ini); +// Reads the theme SaveGUITheme() wrote to 'ini', its [View] section's Theme key, ignoring (ASCII) case. +// Windows when it's missing or isn't a theme's name. +EGUITheme LoadGUITheme(TGUIINIDocument const& ini); +// The color 'text' is, as a theme file writes it (see TGUIThemeFile), as 0xRRGGBB; nullopt if it isn't one. +std::optional ParseGUIThemeColor(std::string const& text); +// Reads a theme file's 'text' (see TGUIThemeFile), UTF-8, whose roles are 'roles', the names the GUI runner +// gives them. +TGUIThemeFile ReadGUIThemeFile(std::string const& text, std::vector const& roles); +// Writes 'name', a dark theme's (see LoadGUIDarkTheme()), to 'ini'. The file itself is written from the +// document's Text() afterward. +void SaveGUIDarkTheme(TGUIINIDocument& ini, std::string const& name); +// Writes 'theme' to 'ini', as "Windows", "Light" or "Dark". The file itself is written from the document's +// Text() afterward. +void SaveGUITheme(TGUIINIDocument& ini, EGUITheme theme); + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_GUI_ThemeH diff --git a/gui/src/ASWUnitTests_GUI_Tree.cpp b/gui/src/ASWUnitTests_GUI_Tree.cpp new file mode 100644 index 0000000..fa64cff --- /dev/null +++ b/gui/src/ASWUnitTests_GUI_Tree.cpp @@ -0,0 +1,166 @@ +/* ************************************************************************** +ASWUnitTests_GUI_Tree.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_GUI_Tree.h" +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +//--------------------------------------------------------------------------- +std::vector BuildGUITree(TGUITestList const& testList) +{ + std::vector nodes; + + // By index into testList.SuitePaths(): each suite's node's position in 'nodes', once added. A suite's + // node, and those of the suites holding it, are added just before its first group's, so suites and groups + // keep the list's order, where each suite's groups are together. + std::vector > suiteNodes(testList.SuitePaths().size()); + std::function(std::string const&)> suiteNode = + [&](std::string const& suitePath) -> std::optional + { + if (suitePath.empty()) + return std::nullopt; + + size_t const suiteIndex = testList.SuiteIndexOf(suitePath).value(); + + if (!suiteNodes[suiteIndex].has_value()) + { + std::string const parentPath = ParentSuitePath(suitePath); + std::optional const parent = suiteNode(parentPath); + nodes.push_back(TGUITreeNode{ parent, TGUINodeRef{ suiteIndex, TGUINodeKind::Suite }, + NameInSuite(suitePath, parentPath) }); + suiteNodes[suiteIndex] = nodes.size() - 1; + } + + return suiteNodes[suiteIndex]; + }; + + // Only visible tests get nodes, and a group or suite only gets one if any of its tests are visible. + std::vector const& groupNames = testList.GroupNames(); + for (size_t groupIndex = 0; groupIndex < groupNames.size(); ++groupIndex) + { + std::vector const testIndexes = testList.TestsInGroup(groupNames[groupIndex]); + if (testIndexes.empty()) + continue; + + std::string const suitePath = testList.GroupSuitePath(groupNames[groupIndex]); + std::optional const parent = suiteNode(suitePath); + nodes.push_back(TGUITreeNode{ parent, TGUINodeRef{ groupIndex, TGUINodeKind::Group }, + NameInSuite(groupNames[groupIndex], suitePath) }); + size_t const groupNode = nodes.size() - 1; + + for (size_t testIndex : testIndexes) + { + nodes.push_back(TGUITreeNode{ groupNode, TGUINodeRef{ testIndex, TGUINodeKind::Test }, + testList.Test(testIndex).TestName }); + } + } + + return nodes; +} +//--------------------------------------------------------------------------- +TGUINodeRef DecodeGUINodeRef(std::uintptr_t data) +{ + return TGUINodeRef{ static_cast(data / 4), static_cast(data % 4) }; +} +//--------------------------------------------------------------------------- +std::uintptr_t EncodeGUINodeRef(TGUINodeRef const& ref) +{ + return static_cast(ref.Index) * 4 + static_cast(ref.Kind); +} +//--------------------------------------------------------------------------- +TGUICheckState GUINodeCheckState(TGUITestList const& testList, TGUINodeRef const& ref) +{ + switch (ref.Kind) + { + case TGUINodeKind::Suite: + return testList.SuiteCheckState(testList.SuitePaths()[ref.Index]); + + case TGUINodeKind::Group: + return testList.GroupCheckState(testList.GroupNames()[ref.Index]); + + case TGUINodeKind::Test: + return testList.IsChecked(ref.Index) ? TGUICheckState::Checked : TGUICheckState::Unchecked; + } + + return TGUICheckState::Unchecked; +} +//--------------------------------------------------------------------------- +std::string GUINodeDetailText(TGUITestList const& testList, TGUINodeRef const& ref) +{ + switch (ref.Kind) + { + case TGUINodeKind::Suite: + return testList.SuiteDetailText(testList.SuitePaths()[ref.Index]); + + case TGUINodeKind::Group: + return testList.GroupDetailText(testList.GroupNames()[ref.Index]); + + case TGUINodeKind::Test: + return testList.DetailText(ref.Index); + } + + return std::string(); +} +//--------------------------------------------------------------------------- +TGUITestStatus GUINodeStatus(TGUITestList const& testList, TGUINodeRef const& ref) +{ + switch (ref.Kind) + { + case TGUINodeKind::Suite: + return testList.SuiteStatus(testList.SuitePaths()[ref.Index]); + + case TGUINodeKind::Group: + return testList.GroupStatus(testList.GroupNames()[ref.Index]); + + case TGUINodeKind::Test: + return testList.Status(ref.Index); + } + + return TGUITestStatus::NotRun; +} +//--------------------------------------------------------------------------- +void SetGUINodeChecked(TGUITestList& testList, TGUINodeRef const& ref, bool checked) +{ + switch (ref.Kind) + { + case TGUINodeKind::Suite: + testList.SetSuiteChecked(testList.SuitePaths()[ref.Index], checked); + break; + + case TGUINodeKind::Group: + testList.SetGroupChecked(testList.GroupNames()[ref.Index], checked); + break; + + case TGUINodeKind::Test: + testList.SetChecked(ref.Index, checked); + break; + } +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/gui/src/ASWUnitTests_GUI_Tree.h b/gui/src/ASWUnitTests_GUI_Tree.h new file mode 100644 index 0000000..854117b --- /dev/null +++ b/gui/src/ASWUnitTests_GUI_Tree.h @@ -0,0 +1,110 @@ +/* ************************************************************************** +ASWUnitTests_GUI_Tree.h +Author: Anthony S. West - ASW Software + +What a GUI runner's test tree shows, independent of the tree control that +shows it. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_GUI_TreeH +#define ASWUnitTests_GUI_TreeH +//--------------------------------------------------------------------------- +#include +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_TestList.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TGUINodeKind +// +// What a test tree node stands for. +///////////////////////////////////////////////////////////////////////////// +enum class TGUINodeKind +{ + Suite, + Group, + Test, +}; + + +///////////////////////////////////////////////////////////////////////////// +// TGUINodeRef +// +// Which suite, group or test a test tree node stands for: its kind, and an +// index, a suite's into TGUITestList::SuitePaths(), a group's into its +// GroupNames(), and a test's into the list itself. A tree control keeps it +// in each node's data, encoded by EncodeGUINodeRef(). +///////////////////////////////////////////////////////////////////////////// +struct TGUINodeRef +{ + size_t Index = 0; + TGUINodeKind Kind = TGUINodeKind::Test; +}; + + +///////////////////////////////////////////////////////////////////////////// +// TGUITreeNode +// +// One node of the test tree, as BuildGUITree() returns it. +///////////////////////////////////////////////////////////////////////////// +struct TGUITreeNode +{ + // The parent node's position in BuildGUITree()'s result, which is always before this one's; empty for a + // top-level node. + std::optional Parent; + TGUINodeRef Ref; + // The node's caption: a suite's or group's own name within the suite holding it, or a test's name. + std::string Text; +}; + +//--------------------------------------------------------------------------- + +// A GUI runner's test tree, independent of the tree control that shows it. Contains no UI code, so it's unit +// tested from the console runners (see gui/tests/Test_ASWUnitTests_GUI_Tree.cpp). + +// The test tree's nodes for 'testList's visible tests, each after its parent, so a tree control can add them in +// order: one node per group, in the list's order, holding one node per test, and one node per suite, holding +// the nodes of the groups and suites in it, added just before its first group's. A group or suite only gets a +// node if any of its tests are visible. +std::vector BuildGUITree(TGUITestList const& testList); +// The node reference EncodeGUINodeRef() encoded. +TGUINodeRef DecodeGUINodeRef(std::uintptr_t data); +// 'ref' as one number, for a tree node's data: its index times four, plus its kind. +std::uintptr_t EncodeGUINodeRef(TGUINodeRef const& ref); +// A node's check box: a test's from whether it's checked, and a group's or suite's from its visible tests. +TGUICheckState GUINodeCheckState(TGUITestList const& testList, TGUINodeRef const& ref); +// The detail pane's text for a node's test, group, or suite. +std::string GUINodeDetailText(TGUITestList const& testList, TGUINodeRef const& ref); +// The status shown for a node: a test's own, or a group's or suite's from its visible tests. +TGUITestStatus GUINodeStatus(TGUITestList const& testList, TGUINodeRef const& ref); +// Checks or unchecks a node's test, or every visible test of its group or suite, as a click on its check box +// does. +void SetGUINodeChecked(TGUITestList& testList, TGUINodeRef const& ref, bool checked); + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_GUI_TreeH diff --git a/gui/src/ASWUnitTests_GUI_View.cpp b/gui/src/ASWUnitTests_GUI_View.cpp new file mode 100644 index 0000000..ed7a76f --- /dev/null +++ b/gui/src/ASWUnitTests_GUI_View.cpp @@ -0,0 +1,293 @@ +/* ************************************************************************** +ASWUnitTests_GUI_View.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_GUI_View.h" +//--------------------------------------------------------------------------- +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_CLI.h" +//--------------------------------------------------------------------------- + +namespace +{ + +//--------------------------------------------------------------------------- + +bool Contains(std::string const& text, char const* part); +std::string Trim(std::string const& text); + +//--------------------------------------------------------------------------- + +bool Contains(std::string const& text, char const* part) +{ + return text.find(part) != std::string::npos; +} +//--------------------------------------------------------------------------- +// 'text' without the spaces, tabs and other control characters at either end. +std::string Trim(std::string const& text) +{ + auto const isTrimmed = [](char c) + { + return static_cast(c) <= ' '; + }; + + size_t start = 0; + while (start < text.size() && isTrimmed(text[start])) + ++start; + + size_t end = text.size(); + while (end > start && isTrimmed(text[end - 1])) + --end; + + return text.substr(start, end - start); +} + +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +//--------------------------------------------------------------------------- +TGUILogLineKind ClassifyGUILogLine(std::string const& line) +{ + if (Contains(line, "***Test failed") || Contains(line, "**Check failed") || Contains(line, "\" - failed (") || + Contains(line, "Unhandled exception") || Contains(line, "!!FATAL ERROR!!") || Contains(line, "!!ERROR!!") || + Contains(line, "!!TIMEOUT!!") || Contains(line, "!!CRASH!!")) + return TGUILogLineKind::Failure; + + if (Contains(line, "***Test skipped") || Contains(line, "\" - skipped (")) + return TGUILogLineKind::Skip; + + return TGUILogLineKind::Normal; +} +//--------------------------------------------------------------------------- +std::string FormatGUISeconds(double seconds) +{ + std::ostringstream text; + text << std::fixed << std::setprecision(2) << seconds << " s"; + return text.str(); +} +//--------------------------------------------------------------------------- +std::string GUIAboutText(std::string const& runnerName) +{ + return TTestHandler::GetVersionFullStr() + "\n" + + runnerName + "\n" + "\n" + "App bits: " + std::to_string(sizeof(void*) * 8) + "\n" + "\n" + "https://github.com/ASWSoftware/asw-unit-tests\n" + "\n" + "Copyright 2026 ASW Software\n" + "Licensed under the Apache License, Version 2.0.\n"; +} +//--------------------------------------------------------------------------- +std::string GUICaption(std::string const& projectName) +{ + std::string caption = "ASWUnitTests - Framework version: " + TTestHandler::GetVersionStr(); + + if (projectName != TCLIOptions().ProjectName) + caption += " - " + projectName; + + return caption; +} +//--------------------------------------------------------------------------- +std::vector GUICommandLineNotes(TGUIOptions const& options, std::string const& uiObjects) +{ + std::vector notes; + + for (std::string const& ignored : options.IgnoredOptions) + notes.push_back("Note: " + ignored + " has no effect in the GUI, so it was ignored."); + + if (options.CLI.TestTimeoutSeconds.has_value()) + { + notes.push_back("Note: --test-timeout-seconds runs each test on a worker thread, so a test that creates " + + uiObjects + " won't work with it."); + } + + if (options.LayoutIgnore) + notes.push_back("Note: --layout-ignore: the window layout isn't loaded or saved, so it opens with the " + "defaults."); + + return notes; +} +//--------------------------------------------------------------------------- +std::string GUIDefaultJUnitReportFileName(std::string const& exeFileName) +{ + size_t const dot = exeFileName.rfind('.'); + return (dot == std::string::npos ? exeFileName : exeFileName.substr(0, dot)) + "_JUnit.xml"; +} +//--------------------------------------------------------------------------- +TestFilter GUIFilterBoxFilter(std::string const& text) +{ + std::string const trimmed = Trim(text); + if (trimmed.empty()) + return TestFilter(); + + std::string const pattern = "*" + trimmed + "*"; + return [pattern](std::string const& fullTestName) + { + return TTestHandler::WildcardMatch(pattern, fullTestName, true); + }; +} +//--------------------------------------------------------------------------- +std::string GUIFirstLine(std::string const& text) +{ + return text.substr(0, text.find('\n')); +} +//--------------------------------------------------------------------------- +std::string GUIMenuItemText(std::string const& text) +{ + std::string caption; + + for (char const c : text) + { + if (c == '&') + caption += '&'; + + caption += c; + } + + return caption; +} +//--------------------------------------------------------------------------- +std::vector GUIMenuItemTextsWithAccelerators(std::vector const& texts, + std::string const& takenLetters) +{ + auto const lower = [](char c) + { + return c >= 'A' && c <= 'Z' ? static_cast(c - 'A' + 'a') : c; + }; + auto const isAccelerator = [](char c) + { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); + }; + + std::string taken; + + for (char const c : takenLetters) + taken += lower(c); + + std::vector captions; + + for (std::string const& text : texts) + { + std::size_t chosen = std::string::npos; + + // A letter that starts a word first, then any. + for (int pass = 0; pass < 2 && chosen == std::string::npos; ++pass) + { + for (std::size_t i = 0; i < text.size() && chosen == std::string::npos; ++i) + { + bool const startsWord = i == 0 || text[i - 1] == ' '; + + if (isAccelerator(text[i]) && (pass == 1 || startsWord) && taken.find(lower(text[i])) == + std::string::npos) + { + chosen = i; + } + } + } + + if (chosen != std::string::npos) + taken += lower(text[chosen]); + + std::string caption; + + for (std::size_t i = 0; i < text.size(); ++i) + { + if (i == chosen) + caption += '&'; + + caption += GUIMenuItemText(text.substr(i, 1)); + } + + captions.push_back(caption); + } + + return captions; +} +//--------------------------------------------------------------------------- +std::vector GUIStatusPanels(TGUITestList const& testList, size_t runFinishedCount, size_t runTotalCount, + std::optional elapsedSeconds, std::string const& shuffleText, std::string const& message) +{ + // Counts only the tests the filter box shows, since those are the ones Run Selected runs. + std::string checked = "Checked: " + std::to_string(testList.CheckedCount()) + " / " + + std::to_string(testList.VisibleCount()); + if (testList.VisibleCount() != testList.Count()) + checked += " shown"; + + return { + checked, + "Run: " + std::to_string(runFinishedCount) + " / " + std::to_string(runTotalCount), + "Passed: " + std::to_string(testList.StatusCount(TGUITestStatus::Passed)), + "Failed: " + std::to_string(testList.StatusCount(TGUITestStatus::Failed)), + "Skipped: " + std::to_string(testList.StatusCount(TGUITestStatus::Skipped)), + elapsedSeconds.has_value() ? FormatGUISeconds(*elapsedSeconds) : std::string(), + shuffleText, + message, + }; +} +//--------------------------------------------------------------------------- +std::vector GUIStatusPanelWidths() +{ + return { 170, 100, 90, 90, 90, 70, 190, 300 }; +} +//--------------------------------------------------------------------------- +TTestCaseRecord GUIUnhandledExceptionRecord(TTestId const& test, std::string const& description) +{ + return TTestCaseRecord{ test.GroupName, test.TestName, 0.0, TTestOutcome::Fail, + "Unhandled exception: " + description, test.SuitePath }; +} +//--------------------------------------------------------------------------- +std::string GUIUnhandledExceptionStatus(std::string const& description) +{ + return "The run was ended by an unhandled exception: " + description; +} +//--------------------------------------------------------------------------- +TGUIRunSummary SummarizeGUIRun(TTestResults const& results) +{ + TGUIRunSummary summary; + summary.Aborted = results.TimedOut || results.Crashed; + summary.ExitCode = ExitCodeForResults(results); + + if (results.Stopped) + summary.StatusMessage = "Stopped before every test ran."; + else if (results.TimedOut) + summary.StatusMessage = "A test timed out, so the rest of the run was abandoned. Restart to run tests again."; + else if (results.Crashed) + summary.StatusMessage = "A test crashed, so the rest of the run was abandoned. Restart to run tests again."; + else + summary.StatusMessage = "Done."; + + return summary; +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/gui/src/ASWUnitTests_GUI_View.h b/gui/src/ASWUnitTests_GUI_View.h new file mode 100644 index 0000000..7e911aa --- /dev/null +++ b/gui/src/ASWUnitTests_GUI_View.h @@ -0,0 +1,131 @@ +/* ************************************************************************** +ASWUnitTests_GUI_View.h +Author: Anthony S. West - ASW Software + +The text a GUI runner's window shows, and how it classifies what it shows, +independent of the controls that show it. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_GUI_ViewH +#define ASWUnitTests_GUI_ViewH +//--------------------------------------------------------------------------- +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Handler.h" +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_CommandLine.h" +#include "ASWUnitTests_GUI_TestList.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TGUILogLineKind +// +// How a GUI runner's log shows one line of the framework's log output; see +// ClassifyGUILogLine(). +///////////////////////////////////////////////////////////////////////////// +enum class TGUILogLineKind +{ + Normal, + Failure, + Skip, +}; + + +///////////////////////////////////////////////////////////////////////////// +// TGUIRunSummary +// +// What a finished run means for a GUI runner; see SummarizeGUIRun(). +///////////////////////////////////////////////////////////////////////////// +struct TGUIRunSummary +{ + // Set when a test timed out or crashed, after which no further run can start (a timed-out test's abandoned + // thread may still be running, and a crash may have left the tests' state corrupted). + bool Aborted = false; + // The exit code the console runner would return for this run. + int ExitCode = 0; + // The status bar's message, e.g. "Done.". + std::string StatusMessage; +}; + +//--------------------------------------------------------------------------- + +// The text a GUI runner's window shows, shared so every GUI runner shows the same. Contains no UI code, so it's +// unit tested from the console runners (see gui/tests/Test_ASWUnitTests_GUI_View.cpp). + +// How the log shows one line of the framework's log output: its failure lines (a failed test or check, an +// unhandled exception, a fatal SetUp_Test()/TearDown_Test() error, a group whose constructor or SetUp_Group() +// threw, or a test that timed out or crashed) and its skip lines stand out. +TGUILogLineKind ClassifyGUILogLine(std::string const& line); +// A number of seconds as the status bar shows it, e.g. "1.35 s". +std::string FormatGUISeconds(double seconds); +// The About dialog's text: the framework's version, 'runnerName' (e.g. "VCL GUI runner"), the app's bits, the +// project's URL, and the copyright and license. +std::string GUIAboutText(std::string const& runnerName); +// The window's caption: "ASWUnitTests - Framework version: ", plus " - " when +// --project-name gave one other than the default. +std::string GUICaption(std::string const& projectName); +// The notes a GUI runner logs as it starts, about command line options it handles differently from the console +// runner: one for each ignored option, one for --test-timeout-seconds, which runs each test on a worker thread +// where a test can't create 'uiObjects' (e.g. "VCL forms or controls"), and one for --layout-ignore. +std::vector GUICommandLineNotes(TGUIOptions const& options, std::string const& uiObjects); +// The file name Export JUnit Report suggests when --report-junit gave none: 'exeFileName' (without its folder) +// with "_JUnit.xml" in place of its extension, e.g. "ASWUnitTests_VCL_GUI_JUnit.xml". +std::string GUIDefaultJUnitReportFileName(std::string const& exeFileName); +// The filter box's filter: matches anywhere in a test's "Group.Test" full name, ignoring case, with the same '*' +// and '?' wildcards as --filter. Unlike --filter, it's never anchored to the whole name, so "Parse?rg" finds +// "...ParseArguments...", just as plain text would. Empty (no filter) when 'text' is empty or only spaces. +TestFilter GUIFilterBoxFilter(std::string const& text); +// The first line of 'text', e.g. for the Failures & Skips list's Detail column. +std::string GUIFirstLine(std::string const& text); +// 'text', e.g. a theme file's name, as a menu item's caption: each '&' doubled, so it shows rather than making +// the next letter the item's accelerator. +std::string GUIMenuItemText(std::string const& text); +// 'texts', a menu's items in order, as captions (see GUIMenuItemText()), each with an accelerator of its own: the +// first ASCII letter or digit, ignoring case, that's neither in 'takenLetters' (the menu's other items') nor an +// earlier text's, preferring one that starts a word. A text with none left gets none. +std::vector GUIMenuItemTextsWithAccelerators(std::vector const& texts, + std::string const& takenLetters); +// The status bar's panels' text, in order: the checked and shown tests, the run's progress, the passed, failed +// and skipped counts, the run's elapsed time (empty if no run has started), the shuffle setting ('shuffleText', +// from TGUIShuffle::StatusText()), and 'message'. +std::vector GUIStatusPanels(TGUITestList const& testList, size_t runFinishedCount, size_t runTotalCount, + std::optional elapsedSeconds, std::string const& shuffleText, std::string const& message); +// The status bar's panel widths at 96 DPI, in order, one per panel GUIStatusPanels() fills, as the VCL GUI +// runner's form has them. The last panel, the status message's, takes the rest of the bar in both runners. +std::vector GUIStatusPanelWidths(); +// The result a test is given when an unhandled exception, described by 'description', ended the run while it +// was running. +TTestCaseRecord GUIUnhandledExceptionRecord(TTestId const& test, std::string const& description); +// The status bar's message when an unhandled exception, described by 'description', ended the run. +std::string GUIUnhandledExceptionStatus(std::string const& description); +// What a finished run's results mean for the GUI: whether it was aborted, its exit code, and the status bar's +// message. +TGUIRunSummary SummarizeGUIRun(TTestResults const& results); + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_GUI_ViewH diff --git a/vcl/tests/Test_ASWUnitTests_GUI_CommandLine.cpp b/gui/tests/Test_ASWUnitTests_GUI_CommandLine.cpp similarity index 94% rename from vcl/tests/Test_ASWUnitTests_GUI_CommandLine.cpp rename to gui/tests/Test_ASWUnitTests_GUI_CommandLine.cpp index ecc1202..ca2ce2c 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_CommandLine.cpp +++ b/gui/tests/Test_ASWUnitTests_GUI_CommandLine.cpp @@ -28,8 +28,6 @@ limitations under the License. #include #include //--------------------------------------------------------------------------- -#include -//--------------------------------------------------------------------------- #include "ASWUnitTests_Registry.h" #include "ASWUnitTests_StdOutRedirect.h" //--------------------------------------------------------------------------- @@ -41,6 +39,8 @@ namespace using namespace ASWUnitTests; +//--------------------------------------------------------------------------- + bool Contains(std::vector const& list, std::string const& item); //--------------------------------------------------------------------------- @@ -54,6 +54,7 @@ bool Contains(std::vector const& list, std::string const& item) } // namespace +//--------------------------------------------------------------------------- namespace ASWUnitTests { @@ -77,7 +78,6 @@ TTest_ASWUnitTests_GUI_CommandLine::TTest_ASWUnitTests_GUI_CommandLine() RegisterTest(&TTest_ASWUnitTests_GUI_CommandLine::Test_Parse_PassesConsoleOptionsThrough, "Parse_PassesConsoleOptionsThrough"); RegisterTest(&TTest_ASWUnitTests_GUI_CommandLine::Test_Parse_RunAndExitAreGUIOnlyOptions, "Parse_RunAndExitAreGUIOnlyOptions"); RegisterTest(&TTest_ASWUnitTests_GUI_CommandLine::Test_Parse_UnrecognizedOptionIsInvalid, "Parse_UnrecognizedOptionIsInvalid"); - RegisterTest(&TTest_ASWUnitTests_GUI_CommandLine::Test_ProcessArguments_MatchesParamCount, "ProcessArguments_MatchesParamCount"); } //--------------------------------------------------------------------------- TTest_ASWUnitTests_GUI_CommandLine::~TTest_ASWUnitTests_GUI_CommandLine() @@ -124,10 +124,6 @@ void TTest_ASWUnitTests_GUI_CommandLine::Test_HelpText_MatchesHelpOption() CheckContains(text, "--run ", __func__, __LINE__, "including the GUI-only options"); CheckEmpty(consoleOutput, __func__, __LINE__, "nothing was written to std::cout"); } -//--------------------------------------------------------------------------- - -// /////// Begin tests after this line /////////////////////// - //--------------------------------------------------------------------------- void TTest_ASWUnitTests_GUI_CommandLine::Test_Parse_ConsoleOnlyOptionsAreReportedAsIgnored() { @@ -281,20 +277,6 @@ void TTest_ASWUnitTests_GUI_CommandLine::Test_Parse_UnrecognizedOptionIsInvalid( "and the usage text that follows it lists the GUI-only options too"); } //--------------------------------------------------------------------------- -void TTest_ASWUnitTests_GUI_CommandLine::Test_ProcessArguments_MatchesParamCount() -{ - // Arrange - // The runner's own arguments vary with how it was started, so only their count can be checked here. - int const expectedCount = System::ParamCount(); - - // Act - std::vector const args = TGUICommandLine::ProcessArguments(); - - // Assert - CheckEquals(static_cast(expectedCount), args.size(), __func__, __LINE__, - "one entry per argument, without the program name"); -} -//--------------------------------------------------------------------------- } // namespace ASWUnitTests diff --git a/vcl/tests/Test_ASWUnitTests_GUI_CommandLine.h b/gui/tests/Test_ASWUnitTests_GUI_CommandLine.h similarity index 91% rename from vcl/tests/Test_ASWUnitTests_GUI_CommandLine.h rename to gui/tests/Test_ASWUnitTests_GUI_CommandLine.h index 191e873..6898a19 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_CommandLine.h +++ b/gui/tests/Test_ASWUnitTests_GUI_CommandLine.h @@ -31,9 +31,9 @@ namespace ASWUnitTests ///////////////////////////////////////////////////////////////////////////// // TTest_ASWUnitTests_GUI_CommandLine // -// Exercises the VCL GUI runner's command-line parsing -// (vcl/gui/src/ASWUnitTests_GUI_CommandLine.h). Listed in both vcl/ -// projects, so it also runs from the VCL console runner. +// Exercises the GUI runners' command-line parsing +// (gui/src/ASWUnitTests_GUI_CommandLine.h). Listed in every project, so it +// also runs from the console runners. ///////////////////////////////////////////////////////////////////////////// class TTest_ASWUnitTests_GUI_CommandLine : public TTestGroupBase { @@ -50,7 +50,6 @@ class TTest_ASWUnitTests_GUI_CommandLine : public TTestGroupBase void Test_Parse_PassesConsoleOptionsThrough(); void Test_Parse_RunAndExitAreGUIOnlyOptions(); void Test_Parse_UnrecognizedOptionIsInvalid(); - void Test_ProcessArguments_MatchesParamCount(); public: TTest_ASWUnitTests_GUI_CommandLine(); diff --git a/gui/tests/Test_ASWUnitTests_GUI_Commands.cpp b/gui/tests/Test_ASWUnitTests_GUI_Commands.cpp new file mode 100644 index 0000000..1e97637 --- /dev/null +++ b/gui/tests/Test_ASWUnitTests_GUI_Commands.cpp @@ -0,0 +1,215 @@ +/* ************************************************************************** +Test_ASWUnitTests_GUI_Commands.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "Test_ASWUnitTests_GUI_Commands.h" +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_Commands.h" +//--------------------------------------------------------------------------- + +namespace +{ + +using namespace ASWUnitTests; + +//--------------------------------------------------------------------------- + +std::string EnabledCommands(TGUICommandStates const& states); + +//--------------------------------------------------------------------------- +/* + EnabledCommands + + The names of the enabled commands, in TGUICommandStates' order, separated by spaces. Compared whole, so a + failure shows every command's state. +*/ +std::string EnabledCommands(TGUICommandStates const& states) +{ + std::string text; + + auto const add = [&text](bool enabled, char const* name) + { + if (enabled) + text += (text.empty() ? "" : " ") + std::string(name); + }; + + add(states.About, "About"); + add(states.CollapseAll, "CollapseAll"); + add(states.CommandLineOptions, "CommandLineOptions"); + add(states.CopyDetails, "CopyDetails"); + add(states.Exit, "Exit"); + add(states.ExpandAll, "ExpandAll"); + add(states.ExportJUnitReport, "ExportJUnitReport"); + add(states.FilterBox, "FilterBox"); + add(states.FocusFilter, "FocusFilter"); + add(states.ResetLayout, "ResetLayout"); + add(states.RunFailed, "RunFailed"); + add(states.RunSelected, "RunSelected"); + add(states.SelectAll, "SelectAll"); + add(states.SelectFailed, "SelectFailed"); + add(states.SelectNone, "SelectNone"); + add(states.ShuffleOrder, "ShuffleOrder"); + add(states.ShuffleSeed, "ShuffleSeed"); + add(states.Stop, "Stop"); + add(states.Theme, "Theme"); + + return text; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_GUI_Commands +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_GUI_Commands::TTest_ASWUnitTests_GUI_Commands() + : inherited("ASWUnitTests_GUI_Commands_Tests") +{ + RegisterTest(&TTest_ASWUnitTests_GUI_Commands::Test_GUICommandStates_AbortedRunCannotRunAgain, + "GUICommandStates_AbortedRunCannotRunAgain"); + RegisterTest(&TTest_ASWUnitTests_GUI_Commands::Test_GUICommandStates_FailuresEnableRunAndSelectFailed, + "GUICommandStates_FailuresEnableRunAndSelectFailed"); + RegisterTest(&TTest_ASWUnitTests_GUI_Commands::Test_GUICommandStates_IdleWithNothingRunYet, + "GUICommandStates_IdleWithNothingRunYet"); + RegisterTest(&TTest_ASWUnitTests_GUI_Commands::Test_GUICommandStates_ReportSelectionAndTreeEnableTheirCommands, + "GUICommandStates_ReportSelectionAndTreeEnableTheirCommands"); + RegisterTest(&TTest_ASWUnitTests_GUI_Commands::Test_GUICommandStates_RunningLeavesOnlyHarmlessCommands, + "GUICommandStates_RunningLeavesOnlyHarmlessCommands"); +} +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_GUI_Commands::~TTest_ASWUnitTests_GUI_Commands() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Commands::SetUp_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Commands::SetUp_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Commands::TearDown_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Commands::TearDown_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- + +// /////// Begin tests after this line /////////////////////// + +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Commands::Test_GUICommandStates_AbortedRunCannotRunAgain() +{ + // Arrange: a run that a test's timeout or crash ended, with failures and a report. + TGUICommandContext context; + context.FailedCount = 1; + context.HasReport = true; + context.RunAborted = true; + + // Act and Assert + CheckEquals(std::string("About CommandLineOptions Exit ExportJUnitReport FilterBox FocusFilter ResetLayout " + "SelectAll SelectFailed SelectNone ShuffleOrder ShuffleSeed Theme"), EnabledCommands(GUICommandStates(context)), + __func__, __LINE__, "neither Run Selected nor Run Failed, but everything else that doesn't run tests"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Commands::Test_GUICommandStates_FailuresEnableRunAndSelectFailed() +{ + // Arrange + TGUICommandContext context; + context.FailedCount = 2; + + // Act and Assert + CheckEquals(std::string("About CommandLineOptions Exit FilterBox FocusFilter ResetLayout RunFailed RunSelected " + "SelectAll SelectFailed SelectNone ShuffleOrder ShuffleSeed Theme"), EnabledCommands(GUICommandStates(context)), + __func__, __LINE__, "Run Failed and Select Failed, once a test has failed"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Commands::Test_GUICommandStates_IdleWithNothingRunYet() +{ + // Act and Assert: as the window opens, before its tree is built. + CheckEquals(std::string("About CommandLineOptions Exit FilterBox FocusFilter ResetLayout RunSelected SelectAll " + "SelectNone ShuffleOrder ShuffleSeed Theme"), EnabledCommands(GUICommandStates(TGUICommandContext())), __func__, + __LINE__, "nothing that needs a failure, a report, a selection, or tree nodes, and not Stop"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Commands::Test_GUICommandStates_ReportSelectionAndTreeEnableTheirCommands() +{ + // Arrange + TGUICommandContext report; + report.HasReport = true; + TGUICommandContext selection; + selection.HasSelection = true; + TGUICommandContext tree; + tree.TreeHasNodes = true; + + // Act + TGUICommandStates const reportStates = GUICommandStates(report); + TGUICommandStates const selectionStates = GUICommandStates(selection); + TGUICommandStates const treeStates = GUICommandStates(tree); + + // Assert + CheckTrue(reportStates.ExportJUnitReport, __func__, __LINE__, "a report can be exported"); + CheckFalse(reportStates.CopyDetails || reportStates.ExpandAll, __func__, __LINE__, "but nothing else changes"); + CheckTrue(selectionStates.CopyDetails, __func__, __LINE__, "a selected node's details can be copied"); + CheckFalse(selectionStates.ExportJUnitReport || selectionStates.ExpandAll, __func__, __LINE__, + "nothing else changes"); + CheckTrue(treeStates.CollapseAll && treeStates.ExpandAll, __func__, __LINE__, + "a tree can be expanded and collapsed"); + CheckFalse(treeStates.ExportJUnitReport || treeStates.CopyDetails, __func__, __LINE__, "nothing else changes"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Commands::Test_GUICommandStates_RunningLeavesOnlyHarmlessCommands() +{ + // Arrange: every other condition met, so only the run itself disables commands. + TGUICommandContext context; + context.FailedCount = 3; + context.HasReport = true; + context.HasSelection = true; + context.Running = true; + context.TreeHasNodes = true; + + // Act and Assert + CheckEquals(std::string("CollapseAll CopyDetails Exit ExpandAll ResetLayout Stop"), + EnabledCommands(GUICommandStates(context)), __func__, __LINE__, + "Stop, and only the commands that can't change the run or hold it up with a dialog"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +ASW_REGISTER_TEST_GROUP(ASWUnitTests::TTest_ASWUnitTests_GUI_Commands) diff --git a/gui/tests/Test_ASWUnitTests_GUI_Commands.h b/gui/tests/Test_ASWUnitTests_GUI_Commands.h new file mode 100644 index 0000000..66f43ce --- /dev/null +++ b/gui/tests/Test_ASWUnitTests_GUI_Commands.h @@ -0,0 +1,63 @@ +/* ************************************************************************** +Test_ASWUnitTests_GUI_Commands.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef Test_ASWUnitTests_GUI_CommandsH +#define Test_ASWUnitTests_GUI_CommandsH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_GUI_Commands +// +// Exercises which of the GUI runners' commands are enabled +// (gui/src/ASWUnitTests_GUI_Commands.h). Listed in every project, so it also +// runs from the console runners. +///////////////////////////////////////////////////////////////////////////// +class TTest_ASWUnitTests_GUI_Commands : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: // Test methods + void Test_GUICommandStates_AbortedRunCannotRunAgain(); + void Test_GUICommandStates_FailuresEnableRunAndSelectFailed(); + void Test_GUICommandStates_IdleWithNothingRunYet(); + void Test_GUICommandStates_ReportSelectionAndTreeEnableTheirCommands(); + void Test_GUICommandStates_RunningLeavesOnlyHarmlessCommands(); + +public: + TTest_ASWUnitTests_GUI_Commands(); + ~TTest_ASWUnitTests_GUI_Commands() override; + + void SetUp_Group() override; + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override; + void TearDown_Test(ITestCase& testCase) override; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef Test_ASWUnitTests_GUI_CommandsH diff --git a/gui/tests/Test_ASWUnitTests_GUI_INIDocument.cpp b/gui/tests/Test_ASWUnitTests_GUI_INIDocument.cpp new file mode 100644 index 0000000..367c7ac --- /dev/null +++ b/gui/tests/Test_ASWUnitTests_GUI_INIDocument.cpp @@ -0,0 +1,300 @@ +/* ************************************************************************** +Test_ASWUnitTests_GUI_INIDocument.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "Test_ASWUnitTests_GUI_INIDocument.h" +//--------------------------------------------------------------------------- +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_INIDocument.h" +//--------------------------------------------------------------------------- + +namespace +{ + +//--------------------------------------------------------------------------- + +std::string JoinNames(std::vector const& names); + +//--------------------------------------------------------------------------- +/* + JoinNames + + The names separated by commas, compared whole, so a failure shows the whole list. +*/ +std::string JoinNames(std::vector const& names) +{ + std::string text; + + for (std::string const& name : names) + text += (text.empty() ? "" : ",") + name; + + return text; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_GUI_INIDocument +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_GUI_INIDocument::TTest_ASWUnitTests_GUI_INIDocument() + : inherited("ASWUnitTests_GUI_INIDocument_Tests") +{ + RegisterTest(&TTest_ASWUnitTests_GUI_INIDocument::Test_Constructor_IgnoresARepeatedSection, + "Constructor_IgnoresARepeatedSection"); + RegisterTest(&TTest_ASWUnitTests_GUI_INIDocument::Test_Constructor_ReadsTheFirstOfARepeatedKey, + "Constructor_ReadsTheFirstOfARepeatedKey"); + RegisterTest(&TTest_ASWUnitTests_GUI_INIDocument::Test_Constructor_ReadsTrimmedNamesAndValues, + "Constructor_ReadsTrimmedNamesAndValues"); + RegisterTest(&TTest_ASWUnitTests_GUI_INIDocument::Test_Constructor_SkipsLinesTMemIniFileSkips, + "Constructor_SkipsLinesTMemIniFileSkips"); + RegisterTest(&TTest_ASWUnitTests_GUI_INIDocument::Test_DeleteKey_RemovesOnlyThatKey, "DeleteKey_RemovesOnlyThatKey"); + RegisterTest(&TTest_ASWUnitTests_GUI_INIDocument::Test_KeyNames_ListsEachKeyAsWritten, + "KeyNames_ListsEachKeyAsWritten"); + RegisterTest(&TTest_ASWUnitTests_GUI_INIDocument::Test_ReadString_IgnoresCase, "ReadString_IgnoresCase"); + RegisterTest(&TTest_ASWUnitTests_GUI_INIDocument::Test_ReadString_MissingIsEmpty, "ReadString_MissingIsEmpty"); + RegisterTest(&TTest_ASWUnitTests_GUI_INIDocument::Test_SectionNames_ListsEachSectionOnce, + "SectionNames_ListsEachSectionOnce"); + RegisterTest(&TTest_ASWUnitTests_GUI_INIDocument::Test_Text_EmptyDocumentIsEmpty, "Text_EmptyDocumentIsEmpty"); + RegisterTest(&TTest_ASWUnitTests_GUI_INIDocument::Test_Text_RoundTripsWhatItRead, "Text_RoundTripsWhatItRead"); + RegisterTest(&TTest_ASWUnitTests_GUI_INIDocument::Test_WriteString_AddsNewKeysAndSectionsAtTheEnd, + "WriteString_AddsNewKeysAndSectionsAtTheEnd"); + RegisterTest(&TTest_ASWUnitTests_GUI_INIDocument::Test_WriteString_ReplacesAValueInPlace, + "WriteString_ReplacesAValueInPlace"); +} +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_GUI_INIDocument::~TTest_ASWUnitTests_GUI_INIDocument() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::SetUp_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::SetUp_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::TearDown_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::TearDown_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- + +// /////// Begin tests after this line /////////////////////// + +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::Test_Constructor_IgnoresARepeatedSection() +{ + // Act + TGUIINIDocument const ini("[A]\r\nOne=1\r\n[B]\r\nTwo=2\r\n[a]\r\nOne=changed\r\nThree=3\r\n"); + + // Assert + CheckEquals(std::string("1"), ini.ReadString("A", "One").value_or("missing"), __func__, __LINE__, + "the first occurrence's value is kept"); + CheckFalse(ini.ReadString("A", "Three").has_value(), __func__, __LINE__, + "and the repeated section's own keys are skipped too, as TMemIniFile skips them"); + CheckEquals(std::string("[A]\r\nOne=1\r\n\r\n[B]\r\nTwo=2\r\n\r\n"), ini.Text(), __func__, __LINE__, + "nor is it written back"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::Test_Constructor_ReadsTheFirstOfARepeatedKey() +{ + // Act + TGUIINIDocument const ini("[A]\r\nKey=first\r\nKEY=second\r\n"); + + // Assert + CheckEquals(std::string("first"), ini.ReadString("A", "Key").value_or("missing"), __func__, __LINE__, + "the first one is read, as with TMemIniFile"); + CheckEquals(std::string("[A]\r\nKey=first\r\nKEY=second\r\n\r\n"), ini.Text(), __func__, __LINE__, + "and both are written back"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::Test_Constructor_ReadsTrimmedNamesAndValues() +{ + // Act + TGUIINIDocument const ini(" [ Spaced Section ] \r\n\tName = some value \t\r\nEquation=a=b\r\nEmpty=\r\n"); + + // Assert + CheckEquals(std::string("some value"), ini.ReadString("Spaced Section", "Name").value_or("missing"), __func__, + __LINE__, "the section and key names and the value are trimmed, but not spaces inside them"); + CheckEquals(std::string("a=b"), ini.ReadString("Spaced Section", "Equation").value_or("missing"), __func__, + __LINE__, "a value is everything after the first '='"); + CheckEquals(std::string(), ini.ReadString("Spaced Section", "Empty").value_or("missing"), __func__, __LINE__, + "an empty value is a value"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::Test_Constructor_SkipsLinesTMemIniFileSkips() +{ + // Arrange: a byte order mark, a key before any section, a comment, blank lines, a line with no '=', and LF + // line breaks mixed with CRLF ones. + std::string const text = "\xEF\xBB\xBF" "Before=section\r\n" + "[A]\n" + "; Key=commented\r\n" + "\r\n" + " \n" + "NoEquals\r\n" + "Key=value"; + + // Act + TGUIINIDocument const ini(text); + + // Assert + CheckEquals(std::string("value"), ini.ReadString("A", "Key").value_or("missing"), __func__, __LINE__, + "the one key, whose line has no line break after it"); + CheckFalse(ini.ReadString("A", "; Key").has_value(), __func__, __LINE__, "a comment isn't a key"); + CheckFalse(ini.ReadString("A", "NoEquals").has_value(), __func__, __LINE__, "nor a line with no '='"); + CheckEquals(std::string("[A]\r\nKey=value\r\n\r\n"), ini.Text(), __func__, __LINE__, + "only the section and its key are written back, without the byte order mark"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::Test_DeleteKey_RemovesOnlyThatKey() +{ + // Arrange + TGUIINIDocument ini("[A]\r\nOne=1\r\nTwo=2\r\n[B]\r\nOne=1\r\n"); + + // Act + ini.DeleteKey("a", "ONE"); + ini.DeleteKey("B", "One"); + ini.DeleteKey("A", "Missing"); + ini.DeleteKey("Missing", "One"); + + // Assert + CheckEquals(std::string("[A]\r\nTwo=2\r\n\r\n[B]\r\n\r\n"), ini.Text(), __func__, __LINE__, + "a key is found ignoring case, and its emptied section stays, as with TMemIniFile"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::Test_KeyNames_ListsEachKeyAsWritten() +{ + // Arrange + TGUIINIDocument ini("[A]\r\n One = 1\r\nNoEquals\r\nTwo=2\r\nONE=again\r\n[B]\r\n[C]\r\nThree=3\r\n"); + + // Act and Assert + CheckEquals(std::string("One,Two,ONE"), JoinNames(ini.KeyNames("a")), __func__, __LINE__, + "trimmed, in order, a repeated key each time, found ignoring the section's case"); + CheckEmpty(JoinNames(ini.KeyNames("B")), __func__, __LINE__, "an empty section"); + CheckEmpty(JoinNames(ini.KeyNames("Missing")), __func__, __LINE__, "a missing section"); + + ini.WriteString("C", "Four", "4"); + CheckEquals(std::string("Three,Four"), JoinNames(ini.KeyNames("C")), __func__, __LINE__, + "with a key written since"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::Test_ReadString_IgnoresCase() +{ + // Arrange + TGUIINIDocument const ini("[Window]\r\nHeight=600\r\n"); + + // Act and Assert + CheckEquals(std::string("600"), ini.ReadString("WINDOW", "height").value_or("missing"), __func__, __LINE__, + "section and key names match ignoring case"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::Test_ReadString_MissingIsEmpty() +{ + // Arrange + TGUIINIDocument const ini("[Window]\r\nHeight=600\r\n"); + + // Act and Assert + CheckFalse(ini.ReadString("Window", "Width").has_value(), __func__, __LINE__, "a missing key"); + CheckFalse(ini.ReadString("Panels", "Height").has_value(), __func__, __LINE__, "a missing section"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::Test_SectionNames_ListsEachSectionOnce() +{ + // Arrange + TGUIINIDocument ini("Before=section\r\n[ Colors ]\r\nA=1\r\n[Theme]\r\n[colors]\r\nB=2\r\n"); + + // Act and Assert + CheckEquals(std::string("Colors,Theme"), JoinNames(ini.SectionNames()), __func__, __LINE__, + "trimmed, in order, a repeated one once, as it's skipped"); + CheckEmpty(JoinNames(TGUIINIDocument().SectionNames()), __func__, __LINE__, "an empty document"); + + ini.WriteString("View", "Theme", "Dark"); + CheckEquals(std::string("Colors,Theme,View"), JoinNames(ini.SectionNames()), __func__, __LINE__, + "with a section written since"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::Test_Text_EmptyDocumentIsEmpty() +{ + // Act and Assert + CheckEmpty(TGUIINIDocument().Text(), __func__, __LINE__, "a new document"); + CheckEmpty(TGUIINIDocument("; only a comment\r\n").Text(), __func__, __LINE__, "a file with no sections"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::Test_Text_RoundTripsWhatItRead() +{ + // Arrange + std::string const text = "[Panels]\r\nDetailHeight=288\r\n\r\n[Window]\r\nMaximized=1\r\n\r\n"; + + // Act and Assert + CheckEquals(text, TGUIINIDocument(text).Text(), __func__, __LINE__, "text in Text()'s own form is unchanged"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::Test_WriteString_AddsNewKeysAndSectionsAtTheEnd() +{ + // Arrange + TGUIINIDocument ini("[A]\r\nOne=1\r\n[B]\r\nTwo=2\r\n"); + + // Act + ini.WriteString("A", "Three", "3"); + ini.WriteString("C", "Four", "4"); + + // Assert + CheckEquals(std::string("[A]\r\nOne=1\r\nThree=3\r\n\r\n[B]\r\nTwo=2\r\n\r\n[C]\r\nFour=4\r\n\r\n"), ini.Text(), + __func__, __LINE__, "a new key at the end of its section, and a new section at the end"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_INIDocument::Test_WriteString_ReplacesAValueInPlace() +{ + // Arrange + TGUIINIDocument ini("[A]\r\nOne=1\r\nTwo=2\r\n"); + + // Act + ini.WriteString("a", "ONE", "changed"); + + // Assert + CheckEquals(std::string("[A]\r\nOne=changed\r\nTwo=2\r\n\r\n"), ini.Text(), __func__, __LINE__, + "the key keeps its place and its name as written, as with TMemIniFile"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +ASW_REGISTER_TEST_GROUP(ASWUnitTests::TTest_ASWUnitTests_GUI_INIDocument) diff --git a/gui/tests/Test_ASWUnitTests_GUI_INIDocument.h b/gui/tests/Test_ASWUnitTests_GUI_INIDocument.h new file mode 100644 index 0000000..7aa186f --- /dev/null +++ b/gui/tests/Test_ASWUnitTests_GUI_INIDocument.h @@ -0,0 +1,72 @@ +/* ************************************************************************** +Test_ASWUnitTests_GUI_INIDocument.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef Test_ASWUnitTests_GUI_INIDocumentH +#define Test_ASWUnitTests_GUI_INIDocumentH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_GUI_INIDocument +// +// Exercises the GUI runners' INI file document +// (gui/src/ASWUnitTests_GUI_INIDocument.h), which reads and writes files the +// way the VCL's TMemIniFile does. Listed in every project, so it also runs +// from the console runners. +///////////////////////////////////////////////////////////////////////////// +class TTest_ASWUnitTests_GUI_INIDocument : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: // Test methods + void Test_Constructor_IgnoresARepeatedSection(); + void Test_Constructor_ReadsTheFirstOfARepeatedKey(); + void Test_Constructor_ReadsTrimmedNamesAndValues(); + void Test_Constructor_SkipsLinesTMemIniFileSkips(); + void Test_DeleteKey_RemovesOnlyThatKey(); + void Test_KeyNames_ListsEachKeyAsWritten(); + void Test_ReadString_IgnoresCase(); + void Test_ReadString_MissingIsEmpty(); + void Test_SectionNames_ListsEachSectionOnce(); + void Test_Text_EmptyDocumentIsEmpty(); + void Test_Text_RoundTripsWhatItRead(); + void Test_WriteString_AddsNewKeysAndSectionsAtTheEnd(); + void Test_WriteString_ReplacesAValueInPlace(); + +public: + TTest_ASWUnitTests_GUI_INIDocument(); + ~TTest_ASWUnitTests_GUI_INIDocument() override; + + void SetUp_Group() override; + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override; + void TearDown_Test(ITestCase& testCase) override; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef Test_ASWUnitTests_GUI_INIDocumentH diff --git a/vcl/tests/Test_ASWUnitTests_GUI_Layout.cpp b/gui/tests/Test_ASWUnitTests_GUI_Layout.cpp similarity index 56% rename from vcl/tests/Test_ASWUnitTests_GUI_Layout.cpp rename to gui/tests/Test_ASWUnitTests_GUI_Layout.cpp index b673926..c35e383 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_Layout.cpp +++ b/gui/tests/Test_ASWUnitTests_GUI_Layout.cpp @@ -25,39 +25,35 @@ limitations under the License. #include "Test_ASWUnitTests_GUI_Layout.h" //--------------------------------------------------------------------------- #include -#include #include #include //--------------------------------------------------------------------------- -#include -#include -#include -//--------------------------------------------------------------------------- #include "ASWUnitTests_Registry.h" //--------------------------------------------------------------------------- #include "ASWUnitTests_GUI_Layout.h" -#include "ASWUnitTests_GUI_Strings.h" //--------------------------------------------------------------------------- namespace { -using System::Types::TRect; - -// An empty, in-memory INI file; nothing is read from or written to disk. -std::unique_ptr NewMemIniFile(); - -//--------------------------------------------------------------------------- - -//--------------------------------------------------------------------------- -std::unique_ptr NewMemIniFile() -{ - return std::unique_ptr(new System::Inifiles::TMemIniFile(System::UnicodeString())); -} -//--------------------------------------------------------------------------- +// A layout file exactly as the VCL GUI runner saved it with the VCL's TMemIniFile, before 1.5 (copied from a +// real one), so files saved before then still load, and the file is still written the same way. +char const TMemIniFileLayout[] = + "[Panels]\r\n" + "DetailHeight=288\r\n" + "TestTreeWidth=446\r\n" + "\r\n" + "[Window]\r\n" + "Height=969\r\n" + "Left=365\r\n" + "Top=232\r\n" + "Width=1285\r\n" + "Maximized=0\r\n" + "\r\n"; } // namespace +//--------------------------------------------------------------------------- namespace ASWUnitTests { @@ -70,16 +66,21 @@ namespace ASWUnitTests TTest_ASWUnitTests_GUI_Layout::TTest_ASWUnitTests_GUI_Layout() : inherited("ASWUnitTests_GUI_Layout_Tests") { - RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_GUILayoutFilePath_IsPerExeUnderAppData, - "GUILayoutFilePath_IsPerExeUnderAppData"); RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_IsWindowReachable_FalseOffScreen, "IsWindowReachable_FalseOffScreen"); RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_IsWindowReachable_FalseWhenTitleBarIsHidden, "IsWindowReachable_FalseWhenTitleBarIsHidden"); RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_IsWindowReachable_TrueOnAWorkArea, "IsWindowReachable_TrueOnAWorkArea"); RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_LoadGUILayout_EmptyIniHasNoValues, "LoadGUILayout_EmptyIniHasNoValues"); RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_LoadGUILayout_IgnoresInvalidValues, "LoadGUILayout_IgnoresInvalidValues"); + RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_LoadGUILayout_ReadsAFileTMemIniFileSaved, + "LoadGUILayout_ReadsAFileTMemIniFileSaved"); + RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_LoadGUILayout_ReadsWholeDecimalNumbers, + "LoadGUILayout_ReadsWholeDecimalNumbers"); + RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_SaveGUILayout_KeepsOtherValues, "SaveGUILayout_KeepsOtherValues"); RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_SaveGUILayout_RemovesMissingValues, "SaveGUILayout_RemovesMissingValues"); RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_SaveGUILayout_RoundTrips, "SaveGUILayout_RoundTrips"); + RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_SaveGUILayout_WritesWhatTMemIniFileWrote, + "SaveGUILayout_WritesWhatTMemIniFileWrote"); RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_ScaleFrom96_ScalesAndRounds, "ScaleFrom96_ScalesAndRounds"); RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_ScaleTo96_ReversesScaleFrom96, "ScaleTo96_ReversesScaleFrom96"); RegisterTest(&TTest_ASWUnitTests_GUI_Layout::Test_ScreenToWorkspace_ReversesWorkspaceToScreen, @@ -111,76 +112,56 @@ void TTest_ASWUnitTests_GUI_Layout::TearDown_Test(ITestCase& /*testCase*/) // /////// Begin tests after this line /////////////////////// -//--------------------------------------------------------------------------- -void TTest_ASWUnitTests_GUI_Layout::Test_GUILayoutFilePath_IsPerExeUnderAppData() -{ - // Arrange - System::UnicodeString const exeName = System::Sysutils::ExtractFileName(System::ParamStr(0)); - System::UnicodeString const appData = System::Sysutils::GetEnvironmentVariable("APPDATA"); - - // Act - System::UnicodeString const path = GUILayoutFilePath(); - - // Assert - System::UnicodeString const folder = System::Sysutils::ExtractFileDir(path); - CheckEquals(ToUTF8(System::Sysutils::ChangeFileExt(exeName, ".ini")), - ToUTF8(System::Sysutils::ExtractFileName(path)), __func__, __LINE__, "named after this executable"); - CheckEquals(std::string("ASWUnitTests"), ToUTF8(System::Sysutils::ExtractFileName(folder)), __func__, __LINE__, - "in an ASWUnitTests folder"); - AssertFalse(appData.IsEmpty(), __func__, __LINE__, "%APPDATA% is set"); - CheckTrue(System::Sysutils::SameText(System::Sysutils::ExtractFileDir(folder), - System::Sysutils::ExcludeTrailingPathDelimiter(appData)), __func__, __LINE__, "under %APPDATA%"); -} //--------------------------------------------------------------------------- void TTest_ASWUnitTests_GUI_Layout::Test_IsWindowReachable_FalseOffScreen() { // Arrange - std::vector const workAreas{ TRect(0, 0, 1920, 1040) }; + std::vector const workAreas{ TGUIRect(0, 0, 1920, 1040) }; // Act and Assert - CheckFalse(IsWindowReachable(TRect(2000, 100, 2800, 700), workAreas), __func__, __LINE__, + CheckFalse(IsWindowReachable(TGUIRect(2000, 100, 2800, 700), workAreas), __func__, __LINE__, "a window past a disconnected monitor's edge"); - CheckFalse(IsWindowReachable(TRect(1870, 100, 2670, 700), workAreas), __func__, __LINE__, + CheckFalse(IsWindowReachable(TGUIRect(1870, 100, 2670, 700), workAreas), __func__, __LINE__, "only 50 pixels of its title bar on screen"); - CheckFalse(IsWindowReachable(TRect(100, 100, 900, 700), std::vector()), __func__, __LINE__, + CheckFalse(IsWindowReachable(TGUIRect(100, 100, 900, 700), std::vector()), __func__, __LINE__, "no work areas at all"); - CheckFalse(IsWindowReachable(TRect(100, 100, 100, 700), workAreas), __func__, __LINE__, "an empty rectangle"); + CheckFalse(IsWindowReachable(TGUIRect(100, 100, 100, 700), workAreas), __func__, __LINE__, "an empty rectangle"); } //--------------------------------------------------------------------------- void TTest_ASWUnitTests_GUI_Layout::Test_IsWindowReachable_FalseWhenTitleBarIsHidden() { // Arrange - std::vector const workAreas{ TRect(0, 0, 1920, 1040) }; + std::vector const workAreas{ TGUIRect(0, 0, 1920, 1040) }; // Act and Assert - CheckFalse(IsWindowReachable(TRect(100, -20, 900, 580), workAreas), __func__, __LINE__, + CheckFalse(IsWindowReachable(TGUIRect(100, -20, 900, 580), workAreas), __func__, __LINE__, "its top above the work area"); - CheckFalse(IsWindowReachable(TRect(100, 1020, 900, 1620), workAreas), __func__, __LINE__, + CheckFalse(IsWindowReachable(TGUIRect(100, 1020, 900, 1620), workAreas), __func__, __LINE__, "its title bar below the work area's bottom, e.g. behind the taskbar"); } //--------------------------------------------------------------------------- void TTest_ASWUnitTests_GUI_Layout::Test_IsWindowReachable_TrueOnAWorkArea() { // Arrange: a second monitor to the left of the primary one, at negative screen coordinates. - std::vector const workAreas{ TRect(0, 0, 1920, 1040), TRect(-1600, 0, 0, 860) }; + std::vector const workAreas{ TGUIRect(0, 0, 1920, 1040), TGUIRect(-1600, 0, 0, 860) }; // Act and Assert - CheckTrue(IsWindowReachable(TRect(100, 100, 900, 700), workAreas), __func__, __LINE__, "on the primary monitor"); - CheckTrue(IsWindowReachable(TRect(-1200, 100, -400, 700), workAreas), __func__, __LINE__, + CheckTrue(IsWindowReachable(TGUIRect(100, 100, 900, 700), workAreas), __func__, __LINE__, "on the primary monitor"); + CheckTrue(IsWindowReachable(TGUIRect(-1200, 100, -400, 700), workAreas), __func__, __LINE__, "on the second monitor"); - CheckTrue(IsWindowReachable(TRect(1770, 100, 2570, 700), workAreas), __func__, __LINE__, + CheckTrue(IsWindowReachable(TGUIRect(1770, 100, 2570, 700), workAreas), __func__, __LINE__, "150 pixels of its title bar on screen"); - CheckTrue(IsWindowReachable(TRect(100, 800, 900, 1400), workAreas), __func__, __LINE__, + CheckTrue(IsWindowReachable(TGUIRect(100, 800, 900, 1400), workAreas), __func__, __LINE__, "only its title bar and a little more on screen"); } //--------------------------------------------------------------------------- void TTest_ASWUnitTests_GUI_Layout::Test_LoadGUILayout_EmptyIniHasNoValues() { // Arrange - std::unique_ptr const ini = NewMemIniFile(); + TGUIINIDocument ini; // Act - TGUILayout const layout = LoadGUILayout(*ini); + TGUILayout const layout = LoadGUILayout(ini); // Assert CheckFalse(layout.DetailHeight.has_value(), __func__, __LINE__, "no detail pane height"); @@ -192,17 +173,17 @@ void TTest_ASWUnitTests_GUI_Layout::Test_LoadGUILayout_EmptyIniHasNoValues() void TTest_ASWUnitTests_GUI_Layout::Test_LoadGUILayout_IgnoresInvalidValues() { // Arrange - std::unique_ptr const ini = NewMemIniFile(); - ini->WriteString("Panels", "DetailHeight", "0"); - ini->WriteString("Panels", "TestTreeWidth", "wide"); - ini->WriteString("Window", "Height", "600"); - ini->WriteString("Window", "Left", "10"); - ini->WriteString("Window", "Maximized", "yes"); - ini->WriteString("Window", "Top", ""); - ini->WriteString("Window", "Width", "800"); + TGUIINIDocument ini; + ini.WriteString("Panels", "DetailHeight", "0"); + ini.WriteString("Panels", "TestTreeWidth", "wide"); + ini.WriteString("Window", "Height", "600"); + ini.WriteString("Window", "Left", "10"); + ini.WriteString("Window", "Maximized", "yes"); + ini.WriteString("Window", "Top", ""); + ini.WriteString("Window", "Width", "800"); // Act - TGUILayout const layout = LoadGUILayout(*ini); + TGUILayout const layout = LoadGUILayout(ini); // Assert CheckFalse(layout.DetailHeight.has_value(), __func__, __LINE__, "a zero size is ignored"); @@ -211,17 +192,101 @@ void TTest_ASWUnitTests_GUI_Layout::Test_LoadGUILayout_IgnoresInvalidValues() CheckFalse(layout.Window.has_value(), __func__, __LINE__, "the window isn't placed without all four values"); // Arrange: every window value valid except a height below the minimum. - ini->WriteString("Window", "Height", "40"); - ini->WriteString("Window", "Top", "20"); + ini.WriteString("Window", "Height", "40"); + ini.WriteString("Window", "Top", "20"); // Act and Assert - CheckFalse(LoadGUILayout(*ini).Window.has_value(), __func__, __LINE__, "nor with a height too small to use"); + CheckFalse(LoadGUILayout(ini).Window.has_value(), __func__, __LINE__, "nor with a height too small to use"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Layout::Test_LoadGUILayout_ReadsAFileTMemIniFileSaved() +{ + // Arrange + TGUIINIDocument const ini(TMemIniFileLayout); + + // Act + TGUILayout const layout = LoadGUILayout(ini); + + // Assert + AssertTrue(layout.DetailHeight.has_value(), __func__, __LINE__, "the detail pane height is read"); + CheckEquals(288, *layout.DetailHeight, __func__, __LINE__, "its value"); + CheckFalse(layout.Maximized, __func__, __LINE__, "not maximized"); + AssertTrue(layout.TestTreeWidth.has_value(), __func__, __LINE__, "the test tree width is read"); + CheckEquals(446, *layout.TestTreeWidth, __func__, __LINE__, "its value"); + AssertTrue(layout.Window.has_value(), __func__, __LINE__, "the window bounds are read"); + CheckEquals(969, layout.Window->Height, __func__, __LINE__, "height"); + CheckEquals(365, layout.Window->Left, __func__, __LINE__, "left"); + CheckEquals(232, layout.Window->Top, __func__, __LINE__, "top"); + CheckEquals(1285, layout.Window->Width, __func__, __LINE__, "width"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Layout::Test_LoadGUILayout_ReadsWholeDecimalNumbers() +{ + // Arrange: the window's values with a sign, at the ends of int's range, and just past them. + TGUIINIDocument ini; + ini.WriteString("Window", "Height", "+600"); + ini.WriteString("Window", "Left", "-2147483648"); + ini.WriteString("Window", "Top", "2147483647"); + ini.WriteString("Window", "Width", "800"); + + // Act + TGUILayout const layout = LoadGUILayout(ini); + + // Assert + AssertTrue(layout.Window.has_value(), __func__, __LINE__, "the window bounds are read"); + CheckEquals(600, layout.Window->Height, __func__, __LINE__, "a leading plus sign is allowed"); + CheckEquals(-2147483647 - 1, layout.Window->Left, __func__, __LINE__, "the smallest int"); + CheckEquals(2147483647, layout.Window->Top, __func__, __LINE__, "the largest int"); + + for (char const* tooBig : { "2147483648", "-2147483649", "99999999999999999999" }) + { + ini.WriteString("Window", "Top", tooBig); + CheckFalse(LoadGUILayout(ini).Window.has_value(), __func__, __LINE__, + std::string("a number that doesn't fit in an int is invalid: ") + tooBig); + } + + for (char const* notWhole : { "-", "+", "12px", "1.5", "1e3", "0x10", "$10", "1 2" }) + { + ini.WriteString("Window", "Top", notWhole); + CheckFalse(LoadGUILayout(ini).Window.has_value(), __func__, __LINE__, + std::string("only a whole decimal number is valid, not: ") + notWhole); + } + + // Act and Assert: the maximized flag is set by any whole number other than 0, as TCustomIniFile::ReadBool() + // read it. + for (char const* maximized : { "1", "2", "-1" }) + { + ini.WriteString("Window", "Maximized", maximized); + CheckTrue(LoadGUILayout(ini).Maximized, __func__, __LINE__, std::string("maximized: ") + maximized); + } + + for (char const* notMaximized : { "0", "-0", "" }) + { + ini.WriteString("Window", "Maximized", notMaximized); + CheckFalse(LoadGUILayout(ini).Maximized, __func__, __LINE__, std::string("not maximized: ") + notMaximized); + } +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Layout::Test_SaveGUILayout_KeepsOtherValues() +{ + // Arrange: a file with a key and a section the layout doesn't use, e.g. from a later version. + TGUIINIDocument ini("[Window]\r\nState=Docked\r\n\r\n[Other]\r\nName=Value\r\n\r\n"); + TGUILayout layout; + layout.DetailHeight = 170; + + // Act + SaveGUILayout(ini, layout); + + // Assert + CheckEquals(std::string("[Window]\r\nState=Docked\r\nMaximized=0\r\n\r\n[Other]\r\nName=Value\r\n\r\n" + "[Panels]\r\nDetailHeight=170\r\n\r\n"), ini.Text(), __func__, __LINE__, + "the other values stay, with the layout's added after them"); } //--------------------------------------------------------------------------- void TTest_ASWUnitTests_GUI_Layout::Test_SaveGUILayout_RemovesMissingValues() { // Arrange - std::unique_ptr const ini = NewMemIniFile(); + TGUIINIDocument ini; TGUILayout full; full.DetailHeight = 170; full.TestTreeWidth = 360; @@ -231,25 +296,28 @@ void TTest_ASWUnitTests_GUI_Layout::Test_SaveGUILayout_RemovesMissingValues() window.Top = 60; window.Width = 1000; full.Window = window; - SaveGUILayout(*ini, full); + SaveGUILayout(ini, full); // Act - SaveGUILayout(*ini, TGUILayout()); + SaveGUILayout(ini, TGUILayout()); // Assert - CheckFalse(ini->ValueExists("Panels", "DetailHeight"), __func__, __LINE__, "the detail pane height is removed"); - CheckFalse(ini->ValueExists("Panels", "TestTreeWidth"), __func__, __LINE__, "the test tree width is removed"); - CheckFalse(ini->ValueExists("Window", "Height"), __func__, __LINE__, "and so are the window's height,"); - CheckFalse(ini->ValueExists("Window", "Left"), __func__, __LINE__, "left,"); - CheckFalse(ini->ValueExists("Window", "Top"), __func__, __LINE__, "top,"); - CheckFalse(ini->ValueExists("Window", "Width"), __func__, __LINE__, "and width"); - CheckTrue(ini->ValueExists("Window", "Maximized"), __func__, __LINE__, "the maximized flag is always written"); + CheckFalse(ini.ReadString("Panels", "DetailHeight").has_value(), __func__, __LINE__, + "the detail pane height is removed"); + CheckFalse(ini.ReadString("Panels", "TestTreeWidth").has_value(), __func__, __LINE__, + "the test tree width is removed"); + CheckFalse(ini.ReadString("Window", "Height").has_value(), __func__, __LINE__, "and so are the window's height,"); + CheckFalse(ini.ReadString("Window", "Left").has_value(), __func__, __LINE__, "left,"); + CheckFalse(ini.ReadString("Window", "Top").has_value(), __func__, __LINE__, "top,"); + CheckFalse(ini.ReadString("Window", "Width").has_value(), __func__, __LINE__, "and width"); + CheckTrue(ini.ReadString("Window", "Maximized").has_value(), __func__, __LINE__, + "the maximized flag is always written"); } //--------------------------------------------------------------------------- void TTest_ASWUnitTests_GUI_Layout::Test_SaveGUILayout_RoundTrips() { // Arrange: a window on a monitor left of the primary one, so its position is negative. - std::unique_ptr const ini = NewMemIniFile(); + TGUIINIDocument ini; TGUILayout saved; saved.DetailHeight = 170; saved.Maximized = true; @@ -262,8 +330,8 @@ void TTest_ASWUnitTests_GUI_Layout::Test_SaveGUILayout_RoundTrips() saved.Window = window; // Act - SaveGUILayout(*ini, saved); - TGUILayout const loaded = LoadGUILayout(*ini); + SaveGUILayout(ini, saved); + TGUILayout const loaded = LoadGUILayout(ini); // Assert AssertTrue(loaded.DetailHeight.has_value(), __func__, __LINE__, "the detail pane height is kept"); @@ -278,6 +346,34 @@ void TTest_ASWUnitTests_GUI_Layout::Test_SaveGUILayout_RoundTrips() CheckEquals(1024, loaded.Window->Width, __func__, __LINE__, "width"); } //--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Layout::Test_SaveGUILayout_WritesWhatTMemIniFileWrote() +{ + // Arrange: the layout TMemIniFileLayout holds, saved to a new file. + TGUIINIDocument ini; + TGUILayout layout; + layout.DetailHeight = 288; + layout.TestTreeWidth = 446; + TGUIWindowBounds window; + window.Height = 969; + window.Left = 365; + window.Top = 232; + window.Width = 1285; + layout.Window = window; + + // Act + SaveGUILayout(ini, layout); + std::string const newFile = ini.Text(); + + TGUIINIDocument existing(TMemIniFileLayout); + SaveGUILayout(existing, layout); + + // Assert + CheckEquals(std::string(TMemIniFileLayout), newFile, __func__, __LINE__, + "a new file has the same text, values, and order as TMemIniFile wrote"); + CheckEquals(std::string(TMemIniFileLayout), existing.Text(), __func__, __LINE__, + "and saving the same layout over that file doesn't change it"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_GUI_Layout::Test_ScaleFrom96_ScalesAndRounds() { // Act and Assert @@ -306,13 +402,13 @@ void TTest_ASWUnitTests_GUI_Layout::Test_ScaleTo96_ReversesScaleFrom96() void TTest_ASWUnitTests_GUI_Layout::Test_ScreenToWorkspace_ReversesWorkspaceToScreen() { // Arrange: a taskbar docked at the left of the primary monitor pushes its work area right 60 pixels. - TRect const primaryMonitor(0, 0, 1920, 1080); - TRect const leftTaskbarWorkArea(60, 0, 1920, 1080); - TRect const screenRect(150, 90, 1350, 910); + TGUIRect const primaryMonitor(0, 0, 1920, 1080); + TGUIRect const leftTaskbarWorkArea(60, 0, 1920, 1080); + TGUIRect const screenRect(150, 90, 1350, 910); // Act - TRect const workspaceRect = ScreenToWorkspace(screenRect, primaryMonitor, leftTaskbarWorkArea); - TRect const roundTrip = WorkspaceToScreen(workspaceRect, primaryMonitor, leftTaskbarWorkArea); + TGUIRect const workspaceRect = ScreenToWorkspace(screenRect, primaryMonitor, leftTaskbarWorkArea); + TGUIRect const roundTrip = WorkspaceToScreen(workspaceRect, primaryMonitor, leftTaskbarWorkArea); // Assert CheckEquals(90, workspaceRect.Left, __func__, __LINE__, "left moves by the work area's offset"); @@ -328,12 +424,12 @@ void TTest_ASWUnitTests_GUI_Layout::Test_ScreenToWorkspace_ReversesWorkspaceToSc void TTest_ASWUnitTests_GUI_Layout::Test_WorkspaceToScreen_OffsetsByPrimaryWorkArea() { // Arrange: a taskbar docked at the top of the primary monitor pushes its work area down 40 pixels. - TRect const primaryMonitor(0, 0, 1920, 1080); - TRect const topTaskbarWorkArea(0, 40, 1920, 1080); + TGUIRect const primaryMonitor(0, 0, 1920, 1080); + TGUIRect const topTaskbarWorkArea(0, 40, 1920, 1080); // Act - TRect const shifted = WorkspaceToScreen(TRect(10, 10, 810, 610), primaryMonitor, topTaskbarWorkArea); - TRect const unshifted = WorkspaceToScreen(TRect(10, 10, 810, 610), primaryMonitor, TRect(0, 0, 1920, 1040)); + TGUIRect const shifted = WorkspaceToScreen(TGUIRect(10, 10, 810, 610), primaryMonitor, topTaskbarWorkArea); + TGUIRect const unshifted = WorkspaceToScreen(TGUIRect(10, 10, 810, 610), primaryMonitor, TGUIRect(0, 0, 1920, 1040)); // Assert CheckEquals(10, shifted.Left, __func__, __LINE__, "left is unchanged"); diff --git a/vcl/tests/Test_ASWUnitTests_GUI_Layout.h b/gui/tests/Test_ASWUnitTests_GUI_Layout.h similarity index 86% rename from vcl/tests/Test_ASWUnitTests_GUI_Layout.h rename to gui/tests/Test_ASWUnitTests_GUI_Layout.h index 3e6c3e6..646d217 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_Layout.h +++ b/gui/tests/Test_ASWUnitTests_GUI_Layout.h @@ -31,9 +31,9 @@ namespace ASWUnitTests ///////////////////////////////////////////////////////////////////////////// // TTest_ASWUnitTests_GUI_Layout // -// Exercises the VCL GUI runner's saved window layout -// (vcl/gui/src/ASWUnitTests_GUI_Layout.h). Listed in both vcl/ projects, so -// it also runs from the VCL console runner. +// Exercises the GUI runners' saved window layout +// (gui/src/ASWUnitTests_GUI_Layout.h). Listed in every project, so it also +// runs from the console runners. ///////////////////////////////////////////////////////////////////////////// class TTest_ASWUnitTests_GUI_Layout : public TTestGroupBase { @@ -41,14 +41,17 @@ class TTest_ASWUnitTests_GUI_Layout : public TTestGroupBase typedef TTestGroupBase inherited; private: // Test methods - void Test_GUILayoutFilePath_IsPerExeUnderAppData(); void Test_IsWindowReachable_FalseOffScreen(); void Test_IsWindowReachable_FalseWhenTitleBarIsHidden(); void Test_IsWindowReachable_TrueOnAWorkArea(); void Test_LoadGUILayout_EmptyIniHasNoValues(); void Test_LoadGUILayout_IgnoresInvalidValues(); + void Test_LoadGUILayout_ReadsAFileTMemIniFileSaved(); + void Test_LoadGUILayout_ReadsWholeDecimalNumbers(); + void Test_SaveGUILayout_KeepsOtherValues(); void Test_SaveGUILayout_RemovesMissingValues(); void Test_SaveGUILayout_RoundTrips(); + void Test_SaveGUILayout_WritesWhatTMemIniFileWrote(); void Test_ScaleFrom96_ScalesAndRounds(); void Test_ScaleTo96_ReversesScaleFrom96(); void Test_ScreenToWorkspace_ReversesWorkspaceToScreen(); diff --git a/vcl/tests/Test_ASWUnitTests_GUI_RunObserver.cpp b/gui/tests/Test_ASWUnitTests_GUI_RunObserver.cpp similarity index 99% rename from vcl/tests/Test_ASWUnitTests_GUI_RunObserver.cpp rename to gui/tests/Test_ASWUnitTests_GUI_RunObserver.cpp index 5465673..711f0a8 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_RunObserver.cpp +++ b/gui/tests/Test_ASWUnitTests_GUI_RunObserver.cpp @@ -132,6 +132,7 @@ void TFixture_GUIObserved::Test_Passes() } // namespace +//--------------------------------------------------------------------------- namespace ASWUnitTests { diff --git a/vcl/tests/Test_ASWUnitTests_GUI_RunObserver.h b/gui/tests/Test_ASWUnitTests_GUI_RunObserver.h similarity index 88% rename from vcl/tests/Test_ASWUnitTests_GUI_RunObserver.h rename to gui/tests/Test_ASWUnitTests_GUI_RunObserver.h index 491f6e1..7e7b2d1 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_RunObserver.h +++ b/gui/tests/Test_ASWUnitTests_GUI_RunObserver.h @@ -31,11 +31,10 @@ namespace ASWUnitTests ///////////////////////////////////////////////////////////////////////////// // TTest_ASWUnitTests_GUI_RunObserver // -// Exercises the VCL GUI runner's ITestRunObserver -// (vcl/gui/src/ASWUnitTests_GUI_RunObserver.h), both with hand-made events -// and by running a small unregistered fixture group through it (see the -// .cpp). Listed in both vcl/ projects, so it also runs from the VCL -// console runner. +// Exercises the GUI runners' ITestRunObserver +// (gui/src/ASWUnitTests_GUI_RunObserver.h), both with hand-made events and +// by running a small unregistered fixture group through it (see the .cpp). +// Listed in every project, so it also runs from the console runners. ///////////////////////////////////////////////////////////////////////////// class TTest_ASWUnitTests_GUI_RunObserver : public TTestGroupBase { diff --git a/vcl/tests/Test_ASWUnitTests_GUI_Selection.cpp b/gui/tests/Test_ASWUnitTests_GUI_Selection.cpp similarity index 96% rename from vcl/tests/Test_ASWUnitTests_GUI_Selection.cpp rename to gui/tests/Test_ASWUnitTests_GUI_Selection.cpp index 660d031..5dd7386 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_Selection.cpp +++ b/gui/tests/Test_ASWUnitTests_GUI_Selection.cpp @@ -27,13 +27,9 @@ limitations under the License. #include #include //--------------------------------------------------------------------------- -#include -//--------------------------------------------------------------------------- #include "ASWUnitTests_Registry.h" //--------------------------------------------------------------------------- -#include "ASWUnitTests_GUI_Layout.h" #include "ASWUnitTests_GUI_Selection.h" -#include "ASWUnitTests_GUI_Strings.h" #include "ASWUnitTests_GUI_TestList.h" //--------------------------------------------------------------------------- @@ -42,6 +38,8 @@ namespace using namespace ASWUnitTests; +//--------------------------------------------------------------------------- + std::vector SampleTests(); //--------------------------------------------------------------------------- @@ -56,6 +54,7 @@ std::vector SampleTests() } // namespace +//--------------------------------------------------------------------------- namespace ASWUnitTests { @@ -78,8 +77,6 @@ TTest_ASWUnitTests_GUI_Selection::TTest_ASWUnitTests_GUI_Selection() "FormatGUISelection_LeavesOutNamesItCannotStore"); RegisterTest(&TTest_ASWUnitTests_GUI_Selection::Test_FormatGUISelection_RoundTripsThroughParse, "FormatGUISelection_RoundTripsThroughParse"); - RegisterTest(&TTest_ASWUnitTests_GUI_Selection::Test_GUISelectionFilePath_IsBesideTheLayoutFile, - "GUISelectionFilePath_IsBesideTheLayoutFile"); RegisterTest(&TTest_ASWUnitTests_GUI_Selection::Test_ParseGUISelection_SkipsLinesItCannotRead, "ParseGUISelection_SkipsLinesItCannotRead"); RegisterTest(&TTest_ASWUnitTests_GUI_Selection::Test_RestoreGUISelection_ChecksEveryTestIfNoSavedCheckedTestExists, @@ -218,17 +215,6 @@ void TTest_ASWUnitTests_GUI_Selection::Test_FormatGUISelection_RoundTripsThrough } } //--------------------------------------------------------------------------- -void TTest_ASWUnitTests_GUI_Selection::Test_GUISelectionFilePath_IsBesideTheLayoutFile() -{ - // Act - System::UnicodeString const selectionPath = GUISelectionFilePath(); - System::UnicodeString const layoutPath = GUILayoutFilePath(); - - // Assert - CheckEquals(ToUTF8(System::Sysutils::ChangeFileExt(layoutPath, ".selection")), ToUTF8(selectionPath), __func__, - __LINE__, "the layout file's path with a .selection extension"); -} -//--------------------------------------------------------------------------- void TTest_ASWUnitTests_GUI_Selection::Test_ParseGUISelection_SkipsLinesItCannotRead() { // Arrange: a byte order mark, CRLF and LF line ends, and lines that aren't tests. diff --git a/vcl/tests/Test_ASWUnitTests_GUI_Selection.h b/gui/tests/Test_ASWUnitTests_GUI_Selection.h similarity index 97% rename from vcl/tests/Test_ASWUnitTests_GUI_Selection.h rename to gui/tests/Test_ASWUnitTests_GUI_Selection.h index 2b8ec31..ced5b21 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_Selection.h +++ b/gui/tests/Test_ASWUnitTests_GUI_Selection.h @@ -42,7 +42,6 @@ class TTest_ASWUnitTests_GUI_Selection : public TTestGroupBase void Test_DescribeGUISelectionRestore_NotesWhenEveryTestWasChecked(); void Test_FormatGUISelection_LeavesOutNamesItCannotStore(); void Test_FormatGUISelection_RoundTripsThroughParse(); - void Test_GUISelectionFilePath_IsBesideTheLayoutFile(); void Test_ParseGUISelection_SkipsLinesItCannotRead(); void Test_RestoreGUISelection_ChecksEveryTestIfNoSavedCheckedTestExists(); void Test_RestoreGUISelection_ChecksNewTestsLikeTheirGroup(); diff --git a/vcl/tests/Test_ASWUnitTests_GUI_Shuffle.cpp b/gui/tests/Test_ASWUnitTests_GUI_Shuffle.cpp similarity index 97% rename from vcl/tests/Test_ASWUnitTests_GUI_Shuffle.cpp rename to gui/tests/Test_ASWUnitTests_GUI_Shuffle.cpp index 6142fd9..9de9051 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_Shuffle.cpp +++ b/gui/tests/Test_ASWUnitTests_GUI_Shuffle.cpp @@ -38,12 +38,17 @@ namespace using namespace ASWUnitTests; +//--------------------------------------------------------------------------- + TGUIShuffle::TSeedSource SeedSequence(std::vector const& seeds, size_t& callCount); //--------------------------------------------------------------------------- +/* + SeedSequence -// New seeds that come from 'seeds' in turn (repeating the last one once they run out), counting each call in -// 'callCount', which must outlive the returned source. + New seeds that come from 'seeds' in turn (repeating the last one once they run out), counting each call in + 'callCount', which must outlive the returned source. +*/ TGUIShuffle::TSeedSource SeedSequence(std::vector const& seeds, size_t& callCount) { callCount = 0; @@ -54,11 +59,11 @@ TGUIShuffle::TSeedSource SeedSequence(std::vector const& seeds, si return seed; }; } - //--------------------------------------------------------------------------- } // namespace +//--------------------------------------------------------------------------- namespace ASWUnitTests { diff --git a/vcl/tests/Test_ASWUnitTests_GUI_Shuffle.h b/gui/tests/Test_ASWUnitTests_GUI_Shuffle.h similarity index 93% rename from vcl/tests/Test_ASWUnitTests_GUI_Shuffle.h rename to gui/tests/Test_ASWUnitTests_GUI_Shuffle.h index 70155b8..04e013f 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_Shuffle.h +++ b/gui/tests/Test_ASWUnitTests_GUI_Shuffle.h @@ -31,9 +31,9 @@ namespace ASWUnitTests ///////////////////////////////////////////////////////////////////////////// // TTest_ASWUnitTests_GUI_Shuffle // -// Exercises the VCL GUI runner's shuffle state -// (vcl/gui/src/ASWUnitTests_GUI_Shuffle.h). Listed in both vcl/ projects, -// so it also runs from the VCL console runner. +// Exercises the GUI runners' shuffle state +// (gui/src/ASWUnitTests_GUI_Shuffle.h). Listed in every project, so it also +// runs from the console runners. ///////////////////////////////////////////////////////////////////////////// class TTest_ASWUnitTests_GUI_Shuffle : public TTestGroupBase { diff --git a/vcl/tests/Test_ASWUnitTests_GUI_TestList.cpp b/gui/tests/Test_ASWUnitTests_GUI_TestList.cpp similarity index 97% rename from vcl/tests/Test_ASWUnitTests_GUI_TestList.cpp rename to gui/tests/Test_ASWUnitTests_GUI_TestList.cpp index ed709ae..3c6578a 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_TestList.cpp +++ b/gui/tests/Test_ASWUnitTests_GUI_TestList.cpp @@ -38,32 +38,40 @@ namespace using namespace ASWUnitTests; +//--------------------------------------------------------------------------- + std::vector SampleTests(); std::vector SuiteTests(); //--------------------------------------------------------------------------- +/* + SampleTests -// Four tests in three groups, one with a '.' in its name: indexes 0 and 1 in "Alpha", 2 in "Beta", 3 in -// "Alpha.Sub". + Four tests in three groups, one with a '.' in its name: indexes 0 and 1 in "Alpha", 2 in "Beta", 3 in "Alpha.Sub". +*/ std::vector SampleTests() { return { { "Alpha", "One" }, { "Alpha", "Two" }, { "Beta", "Three" }, { "Alpha.Sub", "Four" } }; } +//--------------------------------------------------------------------------- +/* + SuiteTests -// Six tests, with groups in suites: index 0 in top-level group "Top"; 1 and 2 in group -// "Deep" in suite "Outer.Inner"; 3 in group "Mid" in suite "Outer"; 4 in group "Solo" in suite "Other"; and -// 5 in group "Near" in suite "Outermost", whose path starts with "Outer" but isn't inside it. + Six tests, with groups in suites: index 0 in top-level group "Top"; 1 and 2 in group + "Deep" in suite "Outer.Inner"; 3 in group "Mid" in suite "Outer"; 4 in group "Solo" in suite "Other"; and + 5 in group "Near" in suite "Outermost", whose path starts with "Outer" but isn't inside it. +*/ std::vector SuiteTests() { return { { "Top", "One", "" }, { "Outer.Inner.Deep", "Two", "Outer.Inner" }, { "Outer.Inner.Deep", "Three", "Outer.Inner" }, { "Outer.Mid", "Four", "Outer" }, { "Other.Solo", "Five", "Other" }, { "Outermost.Near", "Six", "Outermost" } }; } - //--------------------------------------------------------------------------- } // namespace +//--------------------------------------------------------------------------- namespace ASWUnitTests { diff --git a/vcl/tests/Test_ASWUnitTests_GUI_TestList.h b/gui/tests/Test_ASWUnitTests_GUI_TestList.h similarity index 94% rename from vcl/tests/Test_ASWUnitTests_GUI_TestList.h rename to gui/tests/Test_ASWUnitTests_GUI_TestList.h index 4e4a664..bcacf0c 100644 --- a/vcl/tests/Test_ASWUnitTests_GUI_TestList.h +++ b/gui/tests/Test_ASWUnitTests_GUI_TestList.h @@ -31,9 +31,9 @@ namespace ASWUnitTests ///////////////////////////////////////////////////////////////////////////// // TTest_ASWUnitTests_GUI_TestList // -// Exercises the VCL GUI runner's test list and check state model -// (vcl/gui/src/ASWUnitTests_GUI_TestList.h). Listed in both vcl/ projects, -// so it also runs from the VCL console runner. +// Exercises the GUI runners' test list and check state model +// (gui/src/ASWUnitTests_GUI_TestList.h). Listed in every project, so it +// also runs from the console runners. ///////////////////////////////////////////////////////////////////////////// class TTest_ASWUnitTests_GUI_TestList : public TTestGroupBase { diff --git a/gui/tests/Test_ASWUnitTests_GUI_Theme.cpp b/gui/tests/Test_ASWUnitTests_GUI_Theme.cpp new file mode 100644 index 0000000..d20b92b --- /dev/null +++ b/gui/tests/Test_ASWUnitTests_GUI_Theme.cpp @@ -0,0 +1,419 @@ +/* ************************************************************************** +Test_ASWUnitTests_GUI_Theme.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "Test_ASWUnitTests_GUI_Theme.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_Layout.h" +#include "ASWUnitTests_GUI_Theme.h" +//--------------------------------------------------------------------------- + +namespace +{ + +using namespace ASWUnitTests; + +//--------------------------------------------------------------------------- + +std::string ColorsText(TGUIThemeFile const& file); +std::string JoinLines(std::vector const& lines); + +//--------------------------------------------------------------------------- +/* + ColorsText + + The file's colors as "Role=#RRGGBB", separated by commas, compared whole, so a failure shows them all. +*/ +std::string ColorsText(TGUIThemeFile const& file) +{ + std::string text; + + for (TGUIThemeColor const& color : file.Colors) + text += (text.empty() ? "" : ",") + color.Role + "=" + FormatGUIThemeColor(color.RGB); + + return text; +} +//--------------------------------------------------------------------------- +/* + JoinLines + + The lines, each followed by a line break, compared whole, so a failure shows them all. +*/ +std::string JoinLines(std::vector const& lines) +{ + std::string text; + + for (std::string const& line : lines) + text += line + "\n"; + + return text; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_GUI_Theme +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_GUI_Theme::TTest_ASWUnitTests_GUI_Theme() + : inherited("ASWUnitTests_GUI_Theme_Tests") +{ + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_FormatGUIThemeColor_RoundTripsThroughParse, + "FormatGUIThemeColor_RoundTripsThroughParse"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_GUIThemeIsDark_FollowsTheChoiceOrWindows, + "GUIThemeIsDark_FollowsTheChoiceOrWindows"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_GUIThemeNames_ListsTheFilesOfOneKindSorted, + "GUIThemeNames_ListsTheFilesOfOneKindSorted"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_GUIVCLStyleDarkThemeNames_AreSortedWithoutCarbon, + "GUIVCLStyleDarkThemeNames_AreSortedWithoutCarbon"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_LoadGUIDarkTheme_DefaultsToCarbon, + "LoadGUIDarkTheme_DefaultsToCarbon"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_LoadGUITheme_DefaultsToWindows, "LoadGUITheme_DefaultsToWindows"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_LoadGUITheme_ReadsEachThemeIgnoringCase, + "LoadGUITheme_ReadsEachThemeIgnoringCase"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_ParseGUIThemeColor_RejectsOtherForms, + "ParseGUIThemeColor_RejectsOtherForms"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_ReadGUIThemeFile_NeedsBaseDark, "ReadGUIThemeFile_NeedsBaseDark"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_ReadGUIThemeFile_ReadsTheColorsItSets, + "ReadGUIThemeFile_ReadsTheColorsItSets"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_ReadGUIThemeFile_ReportsEachProblem, + "ReadGUIThemeFile_ReportsEachProblem"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_SaveGUIDarkTheme_RoundTripsBesideTheTheme, + "SaveGUIDarkTheme_RoundTripsBesideTheTheme"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_SaveGUITheme_KeepsTheLayout, "SaveGUITheme_KeepsTheLayout"); + RegisterTest(&TTest_ASWUnitTests_GUI_Theme::Test_SaveGUITheme_RoundTrips, "SaveGUITheme_RoundTrips"); +} +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_GUI_Theme::~TTest_ASWUnitTests_GUI_Theme() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::SetUp_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::SetUp_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::TearDown_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::TearDown_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- + +// /////// Begin tests after this line /////////////////////// + +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_FormatGUIThemeColor_RoundTripsThroughParse() +{ + // Act and Assert + CheckEquals(std::string("#1A2B3C"), FormatGUIThemeColor(0x1A2B3C), __func__, __LINE__, "red, green, blue"); + CheckEquals(std::string("#000000"), FormatGUIThemeColor(0), __func__, __LINE__, "with leading zeros"); + CheckEquals(std::string("#FFFFFF"), FormatGUIThemeColor(0xFFFFFF), __func__, __LINE__, "in uppercase"); + CheckEquals(std::string("#ABCDEF"), FormatGUIThemeColor(0xFFABCDEF), __func__, __LINE__, + "ignoring bits above the blue, green and red bytes"); + + for (std::uint32_t const rgb : { 0x000000u, 0x000001u, 0x0F1E2Du, 0x808080u, 0xC0FFEEu, 0xFFFFFFu }) + { + CheckTrue(ParseGUIThemeColor(FormatGUIThemeColor(rgb)) == rgb, __func__, __LINE__, + "read back: " + FormatGUIThemeColor(rgb)); + } + + CheckTrue(ParseGUIThemeColor("#c0ffee") == 0xC0FFEEu, __func__, __LINE__, "lowercase digits"); + CheckTrue(ParseGUIThemeColor("#C0ffEE") == 0xC0FFEEu, __func__, __LINE__, "mixed case digits"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_GUIThemeIsDark_FollowsTheChoiceOrWindows() +{ + // Act and Assert: GUIThemeIsDark(theme, windowsAppsUseDark, highContrast). + CheckTrue(GUIThemeIsDark(EGUITheme::Dark, false, false), __func__, __LINE__, "Dark, with Windows light"); + CheckTrue(GUIThemeIsDark(EGUITheme::Dark, true, true), __func__, __LINE__, "Dark, even with high contrast"); + CheckFalse(GUIThemeIsDark(EGUITheme::Light, true, false), __func__, __LINE__, "Light, with Windows dark"); + CheckFalse(GUIThemeIsDark(EGUITheme::Light, false, true), __func__, __LINE__, "Light, with high contrast"); + CheckTrue(GUIThemeIsDark(EGUITheme::Windows, true, false), __func__, __LINE__, "Windows, set to dark"); + CheckFalse(GUIThemeIsDark(EGUITheme::Windows, false, false), __func__, __LINE__, "Windows, set to light"); + CheckFalse(GUIThemeIsDark(EGUITheme::Windows, true, true), __func__, __LINE__, + "Windows, set to dark, with high contrast"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_GUIThemeNames_ListsTheFilesOfOneKindSorted() +{ + // Arrange: the Themes folder's files, in the order a listing might give them. + std::vector const files = { + "ocean.ini", "Zest.ini", "Notes.txt", "Carbon.ini", "OCEAN.INI", ".ini", "apple.Ini", "carbon.INI", + "Ocean.ini.bak", "Forest.vsf", "ini", "Two Words.ini", "plum.ini", + }; + std::vector const carbon = { "Carbon" }; + std::vector const builtIns = { "Carbon", "Plum", "Zest" }; + + // Act and Assert + CheckEquals(std::string("apple\nocean\nplum\nTwo Words\nZest\n"), JoinLines(GUIThemeNames(files, ".ini", carbon)), + __func__, __LINE__, "the .ini files' names, sorted ignoring case, a name once, without Carbon or a bare " + "extension"); + CheckEquals(std::string("apple\nocean\nTwo Words\n"), JoinLines(GUIThemeNames(files, ".ini", builtIns)), __func__, + __LINE__, "without any built-in theme's name, ignoring case"); + CheckEquals(std::string("Forest\n"), JoinLines(GUIThemeNames(files, ".vsf", carbon)), __func__, __LINE__, + "the VCL GUI runner's .vsf files"); + CheckEmpty(JoinLines(GUIThemeNames(std::vector(), ".ini", carbon)), __func__, __LINE__, + "an empty or missing folder"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_GUIVCLStyleDarkThemeNames_AreSortedWithoutCarbon() +{ + // Act + std::vector const names = GUIVCLStyleDarkThemeNames(); + + // Assert: RAD Studio's style names, which the VCL GUI runner's project links in. + CheckEquals(std::string("Charcoal Dark Slate\nCobalt XEMedia\nOnyx Blue\nTablet Dark\n"), JoinLines(names), __func__, + __LINE__, "the picked styles, sorted, Carbon not among them"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_LoadGUIDarkTheme_DefaultsToCarbon() +{ + // Act and Assert + CheckEquals(std::string("Carbon"), GUIDefaultDarkThemeName(), __func__, __LINE__, "the built-in dark theme"); + CheckEquals(std::string("Carbon"), LoadGUIDarkTheme(TGUIINIDocument()), __func__, __LINE__, "an empty file"); + CheckEquals(std::string("Carbon"), LoadGUIDarkTheme(TGUIINIDocument("[View]\r\nTheme=Dark\r\n")), __func__, + __LINE__, "a layout file from before theme files"); + CheckEquals(std::string("Carbon"), LoadGUIDarkTheme(TGUIINIDocument("[View]\r\nDarkTheme=\r\n")), __func__, + __LINE__, "an empty value"); + CheckEquals(std::string("Carbon"), LoadGUIDarkTheme(TGUIINIDocument("[View]\r\nDarkTheme=CARBON\r\n")), __func__, + __LINE__, "Carbon in another case is the built-in one"); + CheckEquals(std::string("Carbon"), LoadGUIDarkTheme(TGUIINIDocument("[Window]\r\nDarkTheme=Ocean\r\n")), + __func__, __LINE__, "a DarkTheme key in another section"); + CheckEquals(std::string("Deep Ocean"), LoadGUIDarkTheme(TGUIINIDocument("[view]\r\ndarktheme = Deep Ocean \r\n")), + __func__, __LINE__, "a theme file's name, as written, trimmed by the INI document"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_LoadGUITheme_DefaultsToWindows() +{ + // Act and Assert + CheckTrue(LoadGUITheme(TGUIINIDocument()) == EGUITheme::Windows, __func__, __LINE__, "an empty file"); + CheckTrue(LoadGUITheme(TGUIINIDocument("[Window]\r\nMaximized=1\r\n")) == EGUITheme::Windows, __func__, + __LINE__, "a layout file from before 1.5"); + CheckTrue(LoadGUITheme(TGUIINIDocument("[View]\r\nOther=Dark\r\n")) == EGUITheme::Windows, __func__, + __LINE__, "no Theme key"); + CheckTrue(LoadGUITheme(TGUIINIDocument("[Window]\r\nTheme=Dark\r\n")) == EGUITheme::Windows, __func__, + __LINE__, "a Theme key in another section"); + CheckTrue(LoadGUITheme(TGUIINIDocument("[View]\r\nTheme=Carbon\r\n")) == EGUITheme::Windows, __func__, + __LINE__, "an unknown theme"); + CheckTrue(LoadGUITheme(TGUIINIDocument("[View]\r\nTheme=\r\n")) == EGUITheme::Windows, __func__, __LINE__, + "an empty value"); + CheckTrue(LoadGUITheme(TGUIINIDocument("[View]\r\nTheme=Darker\r\n")) == EGUITheme::Windows, __func__, + __LINE__, "a theme's name followed by more text"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_LoadGUITheme_ReadsEachThemeIgnoringCase() +{ + // Act and Assert + CheckTrue(LoadGUITheme(TGUIINIDocument("[View]\r\nTheme=Dark\r\n")) == EGUITheme::Dark, __func__, __LINE__, + "Dark"); + CheckTrue(LoadGUITheme(TGUIINIDocument("[View]\r\nTheme=Light\r\n")) == EGUITheme::Light, __func__, __LINE__, + "Light"); + CheckTrue(LoadGUITheme(TGUIINIDocument("[View]\r\nTheme=Windows\r\n")) == EGUITheme::Windows, __func__, + __LINE__, "Windows"); + CheckTrue(LoadGUITheme(TGUIINIDocument("[view]\r\ntheme=DARK\r\n")) == EGUITheme::Dark, __func__, __LINE__, + "names and value in another case"); + CheckTrue(LoadGUITheme(TGUIINIDocument("[View]\r\nTheme = light \r\n")) == EGUITheme::Light, __func__, + __LINE__, "spaces around the value, which the INI document trims"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_ParseGUIThemeColor_RejectsOtherForms() +{ + // Act and Assert + for (char const* const text : { "", "#", "#12345", "#1234567", "123456", "0x123456", "#12345G", " #123456", + "#123456 ", "#-12345", "#+12345", "#12 456", "rgb(1,2,3)", "1, 2, 3", "#ABC" }) + { + CheckFalse(ParseGUIThemeColor(text).has_value(), __func__, __LINE__, std::string("not a color: \"") + text + + "\""); + } +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_ReadGUIThemeFile_NeedsBaseDark() +{ + // Arrange + std::vector const roles = { "Window" }; + std::string const colors = "[Colors]\r\nWindow=#102030\r\n"; + + // Act + TGUIThemeFile const missing = ReadGUIThemeFile(colors, roles); + TGUIThemeFile const light = ReadGUIThemeFile("[Theme]\r\nBase=Light\r\n" + colors, roles); + TGUIThemeFile const empty = ReadGUIThemeFile(std::string(), roles); + TGUIThemeFile const dark = ReadGUIThemeFile("[theme]\r\nbase=DARK\r\n" + colors, roles); + + // Assert + CheckFalse(missing.Usable, __func__, __LINE__, "no Base"); + CheckEmpty(ColorsText(missing), __func__, __LINE__, "so no colors"); + CheckEquals(std::string("[Theme] has no Base=Dark, which a theme file needs, so Carbon is used instead.\n"), + JoinLines(missing.Problems), __func__, __LINE__, "the problem"); + + CheckFalse(light.Usable, __func__, __LINE__, "Base=Light"); + CheckEmpty(ColorsText(light), __func__, __LINE__, "so no colors either"); + CheckEquals(std::string("[Theme] Base=Light isn't supported, only Base=Dark, so Carbon is used instead.\n"), + JoinLines(light.Problems), __func__, __LINE__, "its problem"); + + CheckFalse(empty.Usable, __func__, __LINE__, "an empty file"); + CheckEquals(std::string("[Theme] has no Base=Dark, which a theme file needs, so Carbon is used instead.\n"), + JoinLines(empty.Problems), __func__, __LINE__, "its problem"); + + CheckTrue(dark.Usable, __func__, __LINE__, "Base=Dark, ignoring case"); + CheckEquals(std::string("Window=#102030"), ColorsText(dark), __func__, __LINE__, "its color"); + CheckEmpty(JoinLines(dark.Problems), __func__, __LINE__, "no problems"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_ReadGUIThemeFile_ReadsTheColorsItSets() +{ + // Arrange: comments and blank lines, as the template has, a byte order mark from an editor, LF line breaks + // and roles written in another case. + std::vector const roles = { "Panel", "Window", "WindowText" }; + std::string const text = "\xEF\xBB\xBF; A theme\n" + "\n" + "[Theme]\n" + "Base=Dark\n" + "\n" + "[Colors]\n" + "; The window\n" + "window=#102030\n" + " PANEL = #a0B0c0 \n"; + + // Act + TGUIThemeFile const file = ReadGUIThemeFile(text, roles); + + // Assert + CheckTrue(file.Usable, __func__, __LINE__, "usable"); + CheckEquals(std::string("Window=#102030,Panel=#A0B0C0"), ColorsText(file), __func__, __LINE__, + "in the file's order, each role named as the GUI names it, and WindowText left out"); + CheckEmpty(JoinLines(file.Problems), __func__, __LINE__, "no problems"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_ReadGUIThemeFile_ReportsEachProblem() +{ + // Arrange + std::vector const roles = { "Panel", "Window", "WindowText" }; + std::string const text = "[Theme]\r\n" + "Base=Dark\r\n" + "Name=Ocean\r\n" + "[Colours]\r\n" + "Window=#000000\r\n" + "[Colors]\r\n" + "Window=#102030\r\n" + "Windw=#111111\r\n" + "WindowText=white\r\n" + "Panel=#123456 ; a comment\r\n" + "WINDOW=#203040\r\n" + "windowtext=#FFFFFF\r\n"; + + // Act + TGUIThemeFile const file = ReadGUIThemeFile(text, roles); + + // Assert + CheckTrue(file.Usable, __func__, __LINE__, "still usable, with what it got right"); + CheckEquals(std::string("Window=#102030"), ColorsText(file), __func__, __LINE__, "the one valid color"); + CheckEquals(std::string("[Colours] isn't a section of a theme file, so it's ignored.\n" + "[Theme] Name isn't a setting of a theme file, so it's ignored.\n" + "[Colors] Windw isn't a role a theme can color, so it's ignored.\n" + "[Colors] WindowText=white isn't a color written as #RRGGBB, so it's ignored.\n" + "[Colors] Panel=#123456 ; a comment isn't a color written as #RRGGBB, so it's ignored.\n" + "[Colors] WINDOW is set more than once, so only the first is used.\n" + "[Colors] windowtext is set more than once, so only the first is used.\n"), JoinLines(file.Problems), __func__, + __LINE__, "each problem, sections first, then the settings, then the colors in the file's order"); + CheckEquals(std::string("Dark theme \"Ocean\": a problem."), GUIThemeProblemLine("Ocean", "a problem."), __func__, + __LINE__, "a problem as the log shows it"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_SaveGUIDarkTheme_RoundTripsBesideTheTheme() +{ + // Arrange + TGUIINIDocument ini; + SaveGUITheme(ini, EGUITheme::Dark); + + // Act and Assert: each overwrites the one before, and the theme stays. + for (char const* const name : { "Ocean", "Two Words", "Carbon" }) + { + SaveGUIDarkTheme(ini, name); + TGUIINIDocument const readBack(ini.Text()); + CheckEquals(std::string(name), LoadGUIDarkTheme(readBack), __func__, __LINE__, "read back"); + CheckTrue(LoadGUITheme(readBack) == EGUITheme::Dark, __func__, __LINE__, "and the theme with it"); + } + + CheckEquals(std::string("[View]\r\nTheme=Dark\r\nDarkTheme=Carbon\r\n\r\n"), ini.Text(), __func__, __LINE__, + "both in [View]"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_SaveGUITheme_KeepsTheLayout() +{ + // Arrange: the theme is saved as soon as it's chosen, and the layout when the window closes, each keeping + // the other. + TGUIINIDocument ini; + TGUILayout layout; + layout.DetailHeight = 170; + SaveGUILayout(ini, layout); + + // Act + SaveGUITheme(ini, EGUITheme::Dark); + SaveGUILayout(ini, layout); + + // Assert + CheckEquals(std::string("[Panels]\r\nDetailHeight=170\r\n\r\n[Window]\r\nMaximized=0\r\n\r\n" + "[View]\r\nTheme=Dark\r\n\r\n"), ini.Text(), __func__, __LINE__, "the theme after the layout"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Theme::Test_SaveGUITheme_RoundTrips() +{ + // Arrange + TGUIINIDocument ini; + + // Act and Assert: each overwrites the one before. + for (EGUITheme const theme : { EGUITheme::Dark, EGUITheme::Light, EGUITheme::Windows }) + { + SaveGUITheme(ini, theme); + CheckTrue(LoadGUITheme(TGUIINIDocument(ini.Text())) == theme, __func__, __LINE__, "read back"); + } + + CheckEquals(std::string("[View]\r\nTheme=Windows\r\n\r\n"), ini.Text(), __func__, __LINE__, + "one key, holding the last theme saved"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +ASW_REGISTER_TEST_GROUP(ASWUnitTests::TTest_ASWUnitTests_GUI_Theme) diff --git a/gui/tests/Test_ASWUnitTests_GUI_Theme.h b/gui/tests/Test_ASWUnitTests_GUI_Theme.h new file mode 100644 index 0000000..745a115 --- /dev/null +++ b/gui/tests/Test_ASWUnitTests_GUI_Theme.h @@ -0,0 +1,72 @@ +/* ************************************************************************** +Test_ASWUnitTests_GUI_Theme.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef Test_ASWUnitTests_GUI_ThemeH +#define Test_ASWUnitTests_GUI_ThemeH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_GUI_Theme +// +// Exercises the GUI runners' theme choice +// (gui/src/ASWUnitTests_GUI_Theme.h). Listed in every project, so it also +// runs from the console runners. +///////////////////////////////////////////////////////////////////////////// +class TTest_ASWUnitTests_GUI_Theme : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: // Test methods + void Test_FormatGUIThemeColor_RoundTripsThroughParse(); + void Test_GUIThemeIsDark_FollowsTheChoiceOrWindows(); + void Test_GUIThemeNames_ListsTheFilesOfOneKindSorted(); + void Test_GUIVCLStyleDarkThemeNames_AreSortedWithoutCarbon(); + void Test_LoadGUIDarkTheme_DefaultsToCarbon(); + void Test_LoadGUITheme_DefaultsToWindows(); + void Test_LoadGUITheme_ReadsEachThemeIgnoringCase(); + void Test_ParseGUIThemeColor_RejectsOtherForms(); + void Test_ReadGUIThemeFile_NeedsBaseDark(); + void Test_ReadGUIThemeFile_ReadsTheColorsItSets(); + void Test_ReadGUIThemeFile_ReportsEachProblem(); + void Test_SaveGUIDarkTheme_RoundTripsBesideTheTheme(); + void Test_SaveGUITheme_KeepsTheLayout(); + void Test_SaveGUITheme_RoundTrips(); + +public: + TTest_ASWUnitTests_GUI_Theme(); + ~TTest_ASWUnitTests_GUI_Theme() override; + + void SetUp_Group() override; + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override; + void TearDown_Test(ITestCase& testCase) override; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef Test_ASWUnitTests_GUI_ThemeH diff --git a/gui/tests/Test_ASWUnitTests_GUI_Tree.cpp b/gui/tests/Test_ASWUnitTests_GUI_Tree.cpp new file mode 100644 index 0000000..e1cf54d --- /dev/null +++ b/gui/tests/Test_ASWUnitTests_GUI_Tree.cpp @@ -0,0 +1,330 @@ +/* ************************************************************************** +Test_ASWUnitTests_GUI_Tree.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "Test_ASWUnitTests_GUI_Tree.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_TestList.h" +#include "ASWUnitTests_GUI_Tree.h" +//--------------------------------------------------------------------------- + +namespace +{ + +using namespace ASWUnitTests; + +//--------------------------------------------------------------------------- + +std::string DescribeTree(std::vector const& nodes); +std::vector SuiteTests(); + +//--------------------------------------------------------------------------- +/* + DescribeTree + + One line per node, in order: its parent's position ("-" for none), its kind and index, and its text, e.g. + "0 Test 1 Two". Compared whole, so a failure shows the entire tree. +*/ +std::string DescribeTree(std::vector const& nodes) +{ + std::string text; + + for (TGUITreeNode const& node : nodes) + { + char const* const kind = (node.Ref.Kind == TGUINodeKind::Suite) ? "Suite" : + (node.Ref.Kind == TGUINodeKind::Group) ? "Group" : "Test"; + text += (node.Parent.has_value() ? std::to_string(*node.Parent) : std::string("-")) + " " + kind + " " + + std::to_string(node.Ref.Index) + " " + node.Text + "\n"; + } + + return text; +} +//--------------------------------------------------------------------------- +/* + SuiteTests + + Seven tests: index 0 in top-level group "Top"; 1 and 2 in group "Deep" in suite "Outer.Inner"; 3 in group + "Mid" in suite "Outer"; 4 in group "Solo" in suite "Other"; 5 in group "Near" in suite "Outermost", whose + path starts with "Outer" but isn't inside it; and 6 in top-level group "Last". +*/ +std::vector SuiteTests() +{ + return { { "Top", "One", "" }, { "Outer.Inner.Deep", "Two", "Outer.Inner" }, + { "Outer.Inner.Deep", "Three", "Outer.Inner" }, { "Outer.Mid", "Four", "Outer" }, + { "Other.Solo", "Five", "Other" }, { "Outermost.Near", "Six", "Outermost" }, { "Last", "Seven", "" } }; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_GUI_Tree +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_GUI_Tree::TTest_ASWUnitTests_GUI_Tree() + : inherited("ASWUnitTests_GUI_Tree_Tests") +{ + RegisterTest(&TTest_ASWUnitTests_GUI_Tree::Test_BuildGUITree_EmptyListHasNoNodes, "BuildGUITree_EmptyListHasNoNodes"); + RegisterTest(&TTest_ASWUnitTests_GUI_Tree::Test_BuildGUITree_GroupsWithoutSuitesAreTopLevel, + "BuildGUITree_GroupsWithoutSuitesAreTopLevel"); + RegisterTest(&TTest_ASWUnitTests_GUI_Tree::Test_BuildGUITree_LeavesOutHiddenTests, "BuildGUITree_LeavesOutHiddenTests"); + RegisterTest(&TTest_ASWUnitTests_GUI_Tree::Test_BuildGUITree_NestsSuites, "BuildGUITree_NestsSuites"); + RegisterTest(&TTest_ASWUnitTests_GUI_Tree::Test_EncodeGUINodeRef_RoundTrips, "EncodeGUINodeRef_RoundTrips"); + RegisterTest(&TTest_ASWUnitTests_GUI_Tree::Test_GUINodeCheckState_FollowsTheList, "GUINodeCheckState_FollowsTheList"); + RegisterTest(&TTest_ASWUnitTests_GUI_Tree::Test_GUINodeDetailText_FollowsTheList, "GUINodeDetailText_FollowsTheList"); + RegisterTest(&TTest_ASWUnitTests_GUI_Tree::Test_GUINodeStatus_FollowsTheList, "GUINodeStatus_FollowsTheList"); + RegisterTest(&TTest_ASWUnitTests_GUI_Tree::Test_SetGUINodeChecked_ChecksTheNodesTests, + "SetGUINodeChecked_ChecksTheNodesTests"); +} +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_GUI_Tree::~TTest_ASWUnitTests_GUI_Tree() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Tree::SetUp_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Tree::SetUp_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Tree::TearDown_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Tree::TearDown_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- + +// /////// Begin tests after this line /////////////////////// + +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Tree::Test_BuildGUITree_EmptyListHasNoNodes() +{ + // Arrange + TGUITestList list; + list.Load(std::vector()); + + // Act and Assert + CheckTrue(BuildGUITree(list).empty(), __func__, __LINE__, "no tests, no nodes"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Tree::Test_BuildGUITree_GroupsWithoutSuitesAreTopLevel() +{ + // Arrange: a group whose name has a '.' but isn't in a suite. + TGUITestList list; + list.Load({ { "Alpha", "One" }, { "Alpha", "Two" }, { "Beta", "Three" }, { "Alpha.Sub", "Four" } }); + + // Act + std::vector const nodes = BuildGUITree(list); + + // Assert + CheckEquals(std::string("- Group 0 Alpha\n" + "0 Test 0 One\n" + "0 Test 1 Two\n" + "- Group 1 Beta\n" + "3 Test 2 Three\n" + "- Group 2 Alpha.Sub\n" + "5 Test 3 Four\n"), DescribeTree(nodes), __func__, __LINE__, + "each group at the top level, with its full name, holding its tests, in the list's order"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Tree::Test_BuildGUITree_LeavesOutHiddenTests() +{ + // Arrange: only test 3 (group "Mid" in suite "Outer") and test 6 (top-level group "Last") are visible. + TGUITestList list; + list.Load(SuiteTests()); + list.SetVisibleFilter([](std::string const& fullTestName) + { + return fullTestName == "Outer.Mid.Four" || fullTestName == "Last.Seven"; + }); + std::string const outer = std::to_string(list.SuiteIndexOf("Outer").value()); + + // Act + std::vector const nodes = BuildGUITree(list); + + // Assert + CheckEquals("- Suite " + outer + " Outer\n" + "0 Group 2 Mid\n" + "1 Test 3 Four\n" + "- Group 5 Last\n" + "3 Test 6 Seven\n", DescribeTree(nodes), __func__, __LINE__, + "groups and suites with no visible tests get no node, including suite Outer.Inner inside a shown suite"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Tree::Test_BuildGUITree_NestsSuites() +{ + // Arrange + TGUITestList list; + list.Load(SuiteTests()); + auto const suite = [&list](std::string const& path) + { + return std::to_string(list.SuiteIndexOf(path).value()); + }; + + // Act + std::vector const nodes = BuildGUITree(list); + + // Assert + CheckEquals("- Group 0 Top\n" + "0 Test 0 One\n" + "- Suite " + suite("Outer") + " Outer\n" + "2 Suite " + suite("Outer.Inner") + " Inner\n" + "3 Group 1 Deep\n" + "4 Test 1 Two\n" + "4 Test 2 Three\n" + "2 Group 2 Mid\n" + "7 Test 3 Four\n" + "- Suite " + suite("Other") + " Other\n" + "9 Group 3 Solo\n" + "10 Test 4 Five\n" + "- Suite " + suite("Outermost") + " Outermost\n" + "12 Group 4 Near\n" + "13 Test 5 Six\n" + "- Group 5 Last\n" + "15 Test 6 Seven\n", DescribeTree(nodes), __func__, __LINE__, + "each suite just before its first group, holding its suites and groups by their own names"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Tree::Test_EncodeGUINodeRef_RoundTrips() +{ + // Arrange + std::vector const refs{ { 0, TGUINodeKind::Suite }, { 0, TGUINodeKind::Group }, + { 0, TGUINodeKind::Test }, { 5, TGUINodeKind::Group }, { 123456, TGUINodeKind::Test } }; + + for (TGUINodeRef const& ref : refs) + { + // Act + TGUINodeRef const decoded = DecodeGUINodeRef(EncodeGUINodeRef(ref)); + + // Assert + std::string const which = std::to_string(ref.Index) + "/" + std::to_string(static_cast(ref.Kind)); + CheckEquals(ref.Index, decoded.Index, __func__, __LINE__, "index survives: " + which); + CheckTrue(ref.Kind == decoded.Kind, __func__, __LINE__, "kind survives: " + which); + } + + CheckEquals(std::uintptr_t(21), EncodeGUINodeRef(TGUINodeRef{ 5, TGUINodeKind::Group }), __func__, __LINE__, + "the index times four, plus the kind"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Tree::Test_GUINodeCheckState_FollowsTheList() +{ + // Arrange: test 1 unchecked, so group Deep (tests 1 and 2) and suite Outer (1 to 3) are partly checked. + TGUITestList list; + list.Load(SuiteTests()); + list.SetChecked(1, false); + size_t const outer = list.SuiteIndexOf("Outer").value(); + size_t const other = list.SuiteIndexOf("Other").value(); + + // Act and Assert + CheckTrue(GUINodeCheckState(list, { 0, TGUINodeKind::Test }) == TGUICheckState::Checked, __func__, __LINE__, + "a checked test"); + CheckTrue(GUINodeCheckState(list, { 1, TGUINodeKind::Test }) == TGUICheckState::Unchecked, __func__, __LINE__, + "an unchecked test"); + CheckTrue(GUINodeCheckState(list, { 1, TGUINodeKind::Group }) == TGUICheckState::Partial, __func__, __LINE__, + "a partly checked group"); + CheckTrue(GUINodeCheckState(list, { outer, TGUINodeKind::Suite }) == TGUICheckState::Partial, __func__, __LINE__, + "a partly checked suite"); + CheckTrue(GUINodeCheckState(list, { other, TGUINodeKind::Suite }) == TGUICheckState::Checked, __func__, __LINE__, + "a fully checked suite"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Tree::Test_GUINodeDetailText_FollowsTheList() +{ + // Arrange + TGUITestList list; + list.Load(SuiteTests()); + size_t const outer = list.SuiteIndexOf("Outer").value(); + + // Act and Assert + CheckEquals(list.DetailText(3), GUINodeDetailText(list, { 3, TGUINodeKind::Test }), __func__, __LINE__, + "a test's own detail"); + CheckEquals(list.GroupDetailText("Outer.Inner.Deep"), GUINodeDetailText(list, { 1, TGUINodeKind::Group }), + __func__, __LINE__, "a group's"); + CheckEquals(list.SuiteDetailText("Outer"), GUINodeDetailText(list, { outer, TGUINodeKind::Suite }), __func__, + __LINE__, "a suite's"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Tree::Test_GUINodeStatus_FollowsTheList() +{ + // Arrange: test 1 passed and test 2 failed, in group Deep in suite Outer.Inner, so the group and suite each + // show a status other than test 1's, though their indexes are 1 too. + TGUITestList list; + list.Load(SuiteTests()); + list.SetStatus(1, TGUITestStatus::Passed); + list.SetStatus(2, TGUITestStatus::Failed); + size_t const inner = list.SuiteIndexOf("Outer.Inner").value(); + + // Act and Assert + CheckTrue(GUINodeStatus(list, { 1, TGUINodeKind::Test }) == TGUITestStatus::Passed, __func__, __LINE__, + "a test's own status"); + CheckTrue(GUINodeStatus(list, { 1, TGUINodeKind::Group }) == TGUITestStatus::Failed, __func__, __LINE__, + "a group's, from its tests"); + CheckTrue(GUINodeStatus(list, { inner, TGUINodeKind::Suite }) == TGUITestStatus::Failed, __func__, __LINE__, + "a suite's, from its tests"); + CheckTrue(GUINodeStatus(list, { 0, TGUINodeKind::Group }) == TGUITestStatus::NotRun, __func__, __LINE__, + "a group none of whose tests ran"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_Tree::Test_SetGUINodeChecked_ChecksTheNodesTests() +{ + // Arrange + TGUITestList list; + list.Load(SuiteTests()); + size_t const outer = list.SuiteIndexOf("Outer").value(); + + // Act and Assert + SetGUINodeChecked(list, { outer, TGUINodeKind::Suite }, false); + CheckFalse(list.IsChecked(1) || list.IsChecked(2) || list.IsChecked(3), __func__, __LINE__, + "unchecking suite Outer unchecks every test in it and in the suites inside it"); + CheckTrue(list.IsChecked(0) && list.IsChecked(5), __func__, __LINE__, + "but not the tests outside it, even in suite Outermost"); + + SetGUINodeChecked(list, { 1, TGUINodeKind::Group }, true); + CheckTrue(list.IsChecked(1) && list.IsChecked(2), __func__, __LINE__, "checking group Deep checks its tests"); + CheckFalse(list.IsChecked(3), __func__, __LINE__, "and only them"); + + SetGUINodeChecked(list, { 3, TGUINodeKind::Test }, true); + CheckTrue(list.IsChecked(3), __func__, __LINE__, "checking a test checks just it"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +ASW_REGISTER_TEST_GROUP(ASWUnitTests::TTest_ASWUnitTests_GUI_Tree) diff --git a/gui/tests/Test_ASWUnitTests_GUI_Tree.h b/gui/tests/Test_ASWUnitTests_GUI_Tree.h new file mode 100644 index 0000000..8ad3ca6 --- /dev/null +++ b/gui/tests/Test_ASWUnitTests_GUI_Tree.h @@ -0,0 +1,66 @@ +/* ************************************************************************** +Test_ASWUnitTests_GUI_Tree.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef Test_ASWUnitTests_GUI_TreeH +#define Test_ASWUnitTests_GUI_TreeH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_GUI_Tree +// +// Exercises the GUI runners' test tree (gui/src/ASWUnitTests_GUI_Tree.h). +// Listed in every project, so it also runs from the console runners. +///////////////////////////////////////////////////////////////////////////// +class TTest_ASWUnitTests_GUI_Tree : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: // Test methods + void Test_BuildGUITree_EmptyListHasNoNodes(); + void Test_BuildGUITree_GroupsWithoutSuitesAreTopLevel(); + void Test_BuildGUITree_LeavesOutHiddenTests(); + void Test_BuildGUITree_NestsSuites(); + void Test_EncodeGUINodeRef_RoundTrips(); + void Test_GUINodeCheckState_FollowsTheList(); + void Test_GUINodeDetailText_FollowsTheList(); + void Test_GUINodeStatus_FollowsTheList(); + void Test_SetGUINodeChecked_ChecksTheNodesTests(); + +public: + TTest_ASWUnitTests_GUI_Tree(); + ~TTest_ASWUnitTests_GUI_Tree() override; + + void SetUp_Group() override; + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override; + void TearDown_Test(ITestCase& testCase) override; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef Test_ASWUnitTests_GUI_TreeH diff --git a/gui/tests/Test_ASWUnitTests_GUI_View.cpp b/gui/tests/Test_ASWUnitTests_GUI_View.cpp new file mode 100644 index 0000000..0f01b4e --- /dev/null +++ b/gui/tests/Test_ASWUnitTests_GUI_View.cpp @@ -0,0 +1,367 @@ +/* ************************************************************************** +Test_ASWUnitTests_GUI_View.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "Test_ASWUnitTests_GUI_View.h" +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_CLI.h" +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_View.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_GUI_View +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_GUI_View::TTest_ASWUnitTests_GUI_View() + : inherited("ASWUnitTests_GUI_View_Tests") +{ + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_ClassifyGUILogLine_FailuresAndSkipsStandOut, + "ClassifyGUILogLine_FailuresAndSkipsStandOut"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_FormatGUISeconds_TwoDecimals, "FormatGUISeconds_TwoDecimals"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUIAboutText_NamesTheRunnerAndVersion, + "GUIAboutText_NamesTheRunnerAndVersion"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUICaption_AddsAProjectNameOtherThanTheDefault, + "GUICaption_AddsAProjectNameOtherThanTheDefault"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUICommandLineNotes_OnePerOptionHandledDifferently, + "GUICommandLineNotes_OnePerOptionHandledDifferently"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUIDefaultJUnitReportFileName_ReplacesTheExtension, + "GUIDefaultJUnitReportFileName_ReplacesTheExtension"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUIFilterBoxFilter_MatchesAnywhereIgnoringCase, + "GUIFilterBoxFilter_MatchesAnywhereIgnoringCase"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUIFirstLine_StopsAtTheFirstLineBreak, + "GUIFirstLine_StopsAtTheFirstLineBreak"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUIMenuItemText_DoublesEachAmpersand, + "GUIMenuItemText_DoublesEachAmpersand"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUIMenuItemTextsWithAccelerators_GiveEachALetterOfItsOwn, + "GUIMenuItemTextsWithAccelerators_GiveEachALetterOfItsOwn"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUIStatusPanels_ShowCountsAndMessage, + "GUIStatusPanels_ShowCountsAndMessage"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUIStatusPanelWidths_OnePerPanel, "GUIStatusPanelWidths_OnePerPanel"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_GUIUnhandledExceptionRecord_FailsTheRunningTest, + "GUIUnhandledExceptionRecord_FailsTheRunningTest"); + RegisterTest(&TTest_ASWUnitTests_GUI_View::Test_SummarizeGUIRun_DescribesHowTheRunEnded, + "SummarizeGUIRun_DescribesHowTheRunEnded"); +} +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_GUI_View::~TTest_ASWUnitTests_GUI_View() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::SetUp_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::SetUp_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::TearDown_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::TearDown_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- + +// /////// Begin tests after this line /////////////////////// + +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_ClassifyGUILogLine_FailuresAndSkipsStandOut() +{ + // Arrange: each kind of failure and skip line the framework logs. + std::vector const failures{ + " ***Test failed: Run_X", + " **Check failed for: \"Test_X\" (12)", + "Finished test: \"G.T\" - failed (0.1 ms)", + "Unhandled exception: boom", + "!!FATAL ERROR!! in SetUp_Test()", + "!!ERROR!! SetUp_Group() threw", + "!!TIMEOUT!! G.T", + "!!CRASH!! G.T" + }; + std::vector const skips{ " ***Test skipped: Run_X", "Finished test: \"G.T\" - skipped (0.1 ms)" }; + + // Act and Assert + for (std::string const& line : failures) + CheckTrue(ClassifyGUILogLine(line) == TGUILogLineKind::Failure, __func__, __LINE__, "a failure: " + line); + + for (std::string const& line : skips) + CheckTrue(ClassifyGUILogLine(line) == TGUILogLineKind::Skip, __func__, __LINE__, "a skip: " + line); + + CheckTrue(ClassifyGUILogLine("Finished test: \"G.T\" - passed (0.1 ms)") == TGUILogLineKind::Normal, __func__, + __LINE__, "a passing test's line"); + CheckTrue(ClassifyGUILogLine(std::string()) == TGUILogLineKind::Normal, __func__, __LINE__, "an empty line"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_FormatGUISeconds_TwoDecimals() +{ + // Act and Assert + CheckEquals(std::string("1.35 s"), FormatGUISeconds(1.346), __func__, __LINE__, "rounded to two decimals"); + CheckEquals(std::string("0.00 s"), FormatGUISeconds(0.0), __func__, __LINE__, "zero"); + CheckEquals(std::string("125.50 s"), FormatGUISeconds(125.5), __func__, __LINE__, "not split into minutes"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_GUIAboutText_NamesTheRunnerAndVersion() +{ + // Act + std::string const text = GUIAboutText("Example GUI runner"); + + // Assert + std::string const bits = (sizeof(void*) == 8) ? "64" : "32"; + CheckEquals(TTestHandler::GetVersionFullStr() + "\n" + "Example GUI runner\n" + "\n" + "App bits: " + bits + "\n" + "\n" + "https://github.com/ASWSoftware/asw-unit-tests\n" + "\n" + "Copyright 2026 ASW Software\n" + "Licensed under the Apache License, Version 2.0.\n", text, __func__, __LINE__, + "the VCL GUI runner's About text, with the runner's own name"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_GUICaption_AddsAProjectNameOtherThanTheDefault() +{ + // Arrange + std::string const plain = "ASWUnitTests - Framework version: " + TTestHandler::GetVersionStr(); + + // Act and Assert + CheckEquals(plain, GUICaption(TCLIOptions().ProjectName), __func__, __LINE__, + "the default project name isn't shown"); + CheckEquals(plain + " - My Project", GUICaption("My Project"), __func__, __LINE__, "another one is"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_GUICommandLineNotes_OnePerOptionHandledDifferently() +{ + // Arrange + TGUIOptions options; + options.IgnoredOptions = { "--list", "--pause" }; + options.CLI.TestTimeoutSeconds = 30; + options.LayoutIgnore = true; + + // Act + std::vector const notes = GUICommandLineNotes(options, "widgets"); + + // Assert + AssertEquals(size_t(4), notes.size(), __func__, __LINE__, "one note each"); + CheckEquals(std::string("Note: --list has no effect in the GUI, so it was ignored."), notes[0], __func__, __LINE__, + "the first ignored option"); + CheckEquals(std::string("Note: --pause has no effect in the GUI, so it was ignored."), notes[1], __func__, + __LINE__, "the second"); + CheckEquals(std::string("Note: --test-timeout-seconds runs each test on a worker thread, so a test that creates " + "widgets won't work with it."), notes[2], __func__, __LINE__, "the timeout, naming the GUI's own UI objects"); + CheckEquals(std::string("Note: --layout-ignore: the window layout isn't loaded or saved, so it opens with the " + "defaults."), notes[3], __func__, __LINE__, "--layout-ignore"); + CheckTrue(GUICommandLineNotes(TGUIOptions(), "widgets").empty(), __func__, __LINE__, + "no notes without such options"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_GUIDefaultJUnitReportFileName_ReplacesTheExtension() +{ + // Act and Assert + CheckEquals(std::string("ASWUnitTests_VCL_GUI_JUnit.xml"), GUIDefaultJUnitReportFileName("ASWUnitTests_VCL_GUI.exe"), + __func__, __LINE__, "the extension is replaced"); + CheckEquals(std::string("my.app_JUnit.xml"), GUIDefaultJUnitReportFileName("my.app.exe"), __func__, __LINE__, + "only the last one"); + CheckEquals(std::string("runner_JUnit.xml"), GUIDefaultJUnitReportFileName("runner"), __func__, __LINE__, + "a name without one gets the suffix added"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_GUIFilterBoxFilter_MatchesAnywhereIgnoringCase() +{ + // Arrange + TestFilter const filter = GUIFilterBoxFilter(" parse?rg\t"); + + // Act and Assert + AssertTrue(static_cast(filter), __func__, __LINE__, "text gives a filter"); + CheckTrue(filter("Framework.TTest_TCLIParser.ParseArguments_Help"), __func__, __LINE__, + "matches anywhere in the name, ignoring case, with a wildcard, once trimmed"); + CheckFalse(filter("Framework.TTest_TConsole.Color"), __func__, __LINE__, "and not a name without it"); + CheckFalse(static_cast(GUIFilterBoxFilter(std::string())), __func__, __LINE__, "no text, no filter"); + CheckFalse(static_cast(GUIFilterBoxFilter(" \t ")), __func__, __LINE__, "nor only spaces"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_GUIFirstLine_StopsAtTheFirstLineBreak() +{ + // Act and Assert + CheckEquals(std::string("first"), GUIFirstLine("first\nsecond\nthird"), __func__, __LINE__, "the first line"); + CheckEquals(std::string("only"), GUIFirstLine("only"), __func__, __LINE__, "a single line"); + CheckEmpty(GUIFirstLine("\nsecond"), __func__, __LINE__, "an empty first line"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_GUIMenuItemText_DoublesEachAmpersand() +{ + // Act and Assert + CheckEquals(std::string("Ocean"), GUIMenuItemText("Ocean"), __func__, __LINE__, "plain text as it is"); + CheckEquals(std::string("Salt && Pepper"), GUIMenuItemText("Salt & Pepper"), __func__, __LINE__, "one '&'"); + CheckEquals(std::string("&&&&a&&"), GUIMenuItemText("&&a&"), __func__, __LINE__, "each of several"); + CheckEquals(std::string("\xC3\x89" "cume && co"), GUIMenuItemText("\xC3\x89" "cume & co"), __func__, __LINE__, + "UTF-8 left as it is"); + CheckEmpty(GUIMenuItemText(std::string()), __func__, __LINE__, "empty"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_GUIMenuItemTextsWithAccelerators_GiveEachALetterOfItsOwn() +{ + // Arrange + auto const join = [](std::vector const& captions) + { + std::string text; + + for (std::string const& caption : captions) + text += caption + "\n"; + + return text; + }; + + std::vector const themes = { + "Carbon", "Charcoal Dark Slate", "Cobalt XEMedia", "Ember", "Forest", "Midnight", "Onyx Blue", "Plum", + "Tablet Dark" + }; + + // Act and Assert + CheckEquals(std::string("&Carbon\nCharcoal &Dark Slate\nCobalt &XEMedia\n&Ember\n&Forest\n&Midnight\nOnyx &Blue\n" + "&Plum\n&Tablet Dark\n"), join(GUIMenuItemTextsWithAccelerators(themes, "so")), __func__, __LINE__, + "a word's first letter not taken before, by the menu's other items or an earlier text"); + CheckEquals(std::string("S&ave\n"), join(GUIMenuItemTextsWithAccelerators({ "Save" }, "S")), __func__, __LINE__, + "else any letter, ignoring case"); + CheckEquals(std::string("&A\nA&&&B\n"), join(GUIMenuItemTextsWithAccelerators({ "A", "A&B" }, "")), __func__, + __LINE__, "an '&' doubled, not an accelerator"); + CheckEquals(std::string("\xC3\x89&cume\n"), join(GUIMenuItemTextsWithAccelerators({ "\xC3\x89" "cume" }, "")), + __func__, __LINE__, "an ASCII letter only"); + CheckEquals(std::string("&x\nx\n"), join(GUIMenuItemTextsWithAccelerators({ "x", "x" }, "")), __func__, __LINE__, + "none when none is left"); + CheckEmpty(join(GUIMenuItemTextsWithAccelerators(std::vector(), "so")), __func__, __LINE__, + "no texts, no captions"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_GUIStatusPanels_ShowCountsAndMessage() +{ + // Arrange: three tests, one hidden by the filter box, one unchecked, one passed and one failed. + TGUITestList list; + list.Load({ { "Alpha", "One" }, { "Alpha", "Two" }, { "Beta", "Three" } }); + list.SetChecked(1, false); + list.SetStatus(0, TGUITestStatus::Passed); + list.SetStatus(1, TGUITestStatus::Failed); + list.SetVisibleFilter([](std::string const& fullTestName) + { + return fullTestName != "Beta.Three"; + }); + + // Act + std::vector const panels = GUIStatusPanels(list, 2, 3, 1.346, "Shuffle: off", "Running..."); + std::vector const beforeRun = GUIStatusPanels(list, 0, 0, std::nullopt, "Shuffle: off", "Ready."); + + // Assert + AssertEquals(size_t(8), panels.size(), __func__, __LINE__, "eight panels"); + CheckEquals(std::string("Checked: 1 / 2 shown"), panels[0], __func__, __LINE__, + "checked of the shown tests, noting that some are hidden"); + CheckEquals(std::string("Run: 2 / 3"), panels[1], __func__, __LINE__, "the run's progress"); + CheckEquals(std::string("Passed: 1"), panels[2], __func__, __LINE__, "passed"); + CheckEquals(std::string("Failed: 1"), panels[3], __func__, __LINE__, "failed"); + CheckEquals(std::string("Skipped: 0"), panels[4], __func__, __LINE__, "skipped"); + CheckEquals(std::string("1.35 s"), panels[5], __func__, __LINE__, "elapsed time"); + CheckEquals(std::string("Shuffle: off"), panels[6], __func__, __LINE__, "the shuffle setting"); + CheckEquals(std::string("Running..."), panels[7], __func__, __LINE__, "the message"); + CheckEmpty(beforeRun[5], __func__, __LINE__, "no elapsed time before a run"); + + list.SetVisibleFilter(TestFilter()); + CheckEquals(std::string("Checked: 2 / 3"), GUIStatusPanels(list, 0, 0, std::nullopt, "", "")[0], __func__, + __LINE__, "no \"shown\" while every test is"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_GUIStatusPanelWidths_OnePerPanel() +{ + // Arrange + TGUITestList list; + list.Load(std::vector()); + + // Act and Assert + CheckEquals(GUIStatusPanels(list, 0, 0, std::nullopt, "", "").size(), GUIStatusPanelWidths().size(), __func__, + __LINE__, "one width per panel"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_GUIUnhandledExceptionRecord_FailsTheRunningTest() +{ + // Act + TTestCaseRecord const record = GUIUnhandledExceptionRecord(TTestId{ "Group", "Test", "Suite" }, "boom"); + + // Assert + CheckEquals(std::string("Group"), record.GroupName, __func__, __LINE__, "the test's group"); + CheckEquals(std::string("Test"), record.TestName, __func__, __LINE__, "its name"); + CheckEquals(std::string("Suite"), record.SuitePath, __func__, __LINE__, "its suite"); + CheckTrue(record.Outcome == TTestOutcome::Fail, __func__, __LINE__, "failed"); + CheckEquals(std::string("Unhandled exception: boom"), record.Message, __func__, __LINE__, "with the description"); + CheckEquals(std::string("The run was ended by an unhandled exception: boom"), GUIUnhandledExceptionStatus("boom"), + __func__, __LINE__, "and the status bar says why the run ended"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_View::Test_SummarizeGUIRun_DescribesHowTheRunEnded() +{ + // Arrange + TTestResults done; + done.SuccessCount = 2; + TTestResults stopped; + stopped.Stopped = true; + TTestResults timedOut; + timedOut.TimedOut = true; + TTestResults crashed; + crashed.Crashed = true; + + // Act + TGUIRunSummary const doneSummary = SummarizeGUIRun(done); + TGUIRunSummary const stoppedSummary = SummarizeGUIRun(stopped); + TGUIRunSummary const timedOutSummary = SummarizeGUIRun(timedOut); + TGUIRunSummary const crashedSummary = SummarizeGUIRun(crashed); + + // Assert + CheckEquals(std::string("Done."), doneSummary.StatusMessage, __func__, __LINE__, "a run that finished"); + CheckFalse(doneSummary.Aborted, __func__, __LINE__, "isn't aborted"); + CheckEquals(ExitCodeForResults(done), doneSummary.ExitCode, __func__, __LINE__, "and has the console's exit code"); + CheckEquals(std::string("Stopped before every test ran."), stoppedSummary.StatusMessage, __func__, __LINE__, + "a stopped run"); + CheckFalse(stoppedSummary.Aborted, __func__, __LINE__, "can be run again"); + CheckEquals(ExitCodeForResults(stopped), stoppedSummary.ExitCode, __func__, __LINE__, "its exit code"); + CheckEquals(std::string("A test timed out, so the rest of the run was abandoned. Restart to run tests again."), + timedOutSummary.StatusMessage, __func__, __LINE__, "a timed-out run"); + CheckTrue(timedOutSummary.Aborted, __func__, __LINE__, "is aborted"); + CheckEquals(std::string("A test crashed, so the rest of the run was abandoned. Restart to run tests again."), + crashedSummary.StatusMessage, __func__, __LINE__, "a crashed run"); + CheckTrue(crashedSummary.Aborted, __func__, __LINE__, "is aborted"); + CheckEquals(ExitCodeForResults(crashed), crashedSummary.ExitCode, __func__, __LINE__, "its exit code"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +ASW_REGISTER_TEST_GROUP(ASWUnitTests::TTest_ASWUnitTests_GUI_View) diff --git a/gui/tests/Test_ASWUnitTests_GUI_View.h b/gui/tests/Test_ASWUnitTests_GUI_View.h new file mode 100644 index 0000000..002e15c --- /dev/null +++ b/gui/tests/Test_ASWUnitTests_GUI_View.h @@ -0,0 +1,71 @@ +/* ************************************************************************** +Test_ASWUnitTests_GUI_View.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef Test_ASWUnitTests_GUI_ViewH +#define Test_ASWUnitTests_GUI_ViewH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_GUI_View +// +// Exercises the text the GUI runners show (gui/src/ASWUnitTests_GUI_View.h). +// Listed in every project, so it also runs from the console runners. +///////////////////////////////////////////////////////////////////////////// +class TTest_ASWUnitTests_GUI_View : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: // Test methods + void Test_ClassifyGUILogLine_FailuresAndSkipsStandOut(); + void Test_FormatGUISeconds_TwoDecimals(); + void Test_GUIAboutText_NamesTheRunnerAndVersion(); + void Test_GUICaption_AddsAProjectNameOtherThanTheDefault(); + void Test_GUICommandLineNotes_OnePerOptionHandledDifferently(); + void Test_GUIDefaultJUnitReportFileName_ReplacesTheExtension(); + void Test_GUIFilterBoxFilter_MatchesAnywhereIgnoringCase(); + void Test_GUIFirstLine_StopsAtTheFirstLineBreak(); + void Test_GUIMenuItemText_DoublesEachAmpersand(); + void Test_GUIMenuItemTextsWithAccelerators_GiveEachALetterOfItsOwn(); + void Test_GUIStatusPanels_ShowCountsAndMessage(); + void Test_GUIStatusPanelWidths_OnePerPanel(); + void Test_GUIUnhandledExceptionRecord_FailsTheRunningTest(); + void Test_SummarizeGUIRun_DescribesHowTheRunEnded(); + +public: + TTest_ASWUnitTests_GUI_View(); + ~TTest_ASWUnitTests_GUI_View() override; + + void SetUp_Group() override; + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override; + void TearDown_Test(ITestCase& testCase) override; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef Test_ASWUnitTests_GUI_ViewH diff --git a/rad370/ASWUnitTests.cbproj b/rad370/ASWUnitTests.cbproj index f02eb20..3ad7c16 100644 --- a/rad370/ASWUnitTests.cbproj +++ b/rad370/ASWUnitTests.cbproj @@ -59,12 +59,13 @@ true $(BDSLIB)\$(PLATFORM)\release\$(LANGDIR);$(ILINK_TranslatedLibraryPath) CppConsoleApplication + ..\resource\ASWUnitTests.ico System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) true ASWUnitTests - ..\src\;..\tests\;..\toTest\;$(IncludePath) + ..\src\;..\tests\;..\toTest\;..\gui\src\;..\gui\tests\;..\win32\gui\src\;..\win32\tests\;$(IncludePath) <_TCHARMapping>char - ..\src\;..\tests\;..\toTest\;$(ILINK_LibraryPath) + ..\src\;..\tests\;..\toTest\;..\gui\src\;..\gui\tests\;..\win32\gui\src\;..\win32\tests\;$(ILINK_LibraryPath) true false false @@ -144,6 +145,94 @@ 0 true + + ..\gui\src\ASWUnitTests_GUI_CommandLine.h + 28 + + + ..\gui\src\ASWUnitTests_GUI_Commands.h + 44 + + + ..\gui\src\ASWUnitTests_GUI_INIDocument.h + 40 + + + ..\gui\src\ASWUnitTests_GUI_Layout.h + 38 + + + ..\gui\src\ASWUnitTests_GUI_RunObserver.h + 29 + + + ..\gui\src\ASWUnitTests_GUI_Selection.h + 36 + + + ..\gui\src\ASWUnitTests_GUI_Shuffle.h + 30 + + + ..\gui\src\ASWUnitTests_GUI_TestList.h + 31 + + + ..\gui\src\ASWUnitTests_GUI_Theme.h + 58 + + + ..\gui\src\ASWUnitTests_GUI_Tree.h + 42 + + + ..\gui\src\ASWUnitTests_GUI_View.h + 46 + + + ..\gui\tests\Test_ASWUnitTests_GUI_CommandLine.h + 32 + + + ..\gui\tests\Test_ASWUnitTests_GUI_Commands.h + 45 + + + ..\gui\tests\Test_ASWUnitTests_GUI_INIDocument.h + 41 + + + ..\gui\tests\Test_ASWUnitTests_GUI_Layout.h + 39 + + + ..\gui\tests\Test_ASWUnitTests_GUI_RunObserver.h + 33 + + + ..\gui\tests\Test_ASWUnitTests_GUI_Selection.h + 37 + + + ..\gui\tests\Test_ASWUnitTests_GUI_Shuffle.h + 34 + + + ..\gui\tests\Test_ASWUnitTests_GUI_TestList.h + 35 + + + ..\gui\tests\Test_ASWUnitTests_GUI_Theme.h + 59 + + + ..\gui\tests\Test_ASWUnitTests_GUI_Tree.h + 43 + + + ..\gui\tests\Test_ASWUnitTests_GUI_View.h + 47 + ..\src\ASWUnitTests_CLI.h 2 @@ -243,6 +332,14 @@ ..\tests\Test_ASWUnitTests_Version.h 23 + + ..\tests\UT_Helper_ExceptionTypes.h + 57 + + + ..\tests\UT_Helper_StringChecks.h + 56 + ..\toTest\ASWTools_Random.h 3 @@ -251,6 +348,54 @@ ..\toTest\ASWTools_String.h 4 + + ..\win32\gui\src\ASWUnitTests_Win32GUI_DialogTemplate.h + 54 + + + ..\win32\gui\src\ASWUnitTests_Win32GUI_DPI.h + 50 + + + ..\win32\gui\src\ASWUnitTests_Win32GUI_Glyphs.h + 60 + + + ..\win32\gui\src\ASWUnitTests_Win32GUI_MainLayout.h + 52 + + + ..\win32\gui\src\ASWUnitTests_Win32GUI_Palette.h + 62 + + + ..\win32\gui\src\ASWUnitTests_Win32GUI_System.h + 48 + + + ..\win32\tests\Test_ASWUnitTests_Win32GUI_DialogTemplate.h + 55 + + + ..\win32\tests\Test_ASWUnitTests_Win32GUI_DPI.h + 51 + + + ..\win32\tests\Test_ASWUnitTests_Win32GUI_Glyphs.h + 61 + + + ..\win32\tests\Test_ASWUnitTests_Win32GUI_MainLayout.h + 53 + + + ..\win32\tests\Test_ASWUnitTests_Win32GUI_Palette.h + 63 + + + ..\win32\tests\Test_ASWUnitTests_Win32GUI_System.h + 49 + Base diff --git a/resource/ASWUnitTests.ico b/resource/ASWUnitTests.ico new file mode 100644 index 0000000..7904744 Binary files /dev/null and b/resource/ASWUnitTests.ico differ diff --git a/resource/ASWUnitTests_Icon.rc b/resource/ASWUnitTests_Icon.rc new file mode 100644 index 0000000..b56d59e --- /dev/null +++ b/resource/ASWUnitTests_Icon.rc @@ -0,0 +1,27 @@ +/* ************************************************************************** +ASWUnitTests_Icon.rc +Author: Anthony S. West - ASW Software + +Embeds the ASWUnitTests icon in the CMake builds of the console runner and +the Win32 GUI runner (MSVC, MinGW and Clang). The RAD Studio projects embed +the same .ico themselves (Icon_MainIcon), also as MAINICON, which is the +name the Win32 GUI runner loads it by. The .ico is found through the +include path, which has this folder. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +MAINICON ICON "ASWUnitTests.ico" diff --git a/resource/icon_16.png b/resource/icon_16.png new file mode 100644 index 0000000..91adc46 Binary files /dev/null and b/resource/icon_16.png differ diff --git a/resource/icon_20.png b/resource/icon_20.png new file mode 100644 index 0000000..749d600 Binary files /dev/null and b/resource/icon_20.png differ diff --git a/resource/icon_24.png b/resource/icon_24.png new file mode 100644 index 0000000..ae92fae Binary files /dev/null and b/resource/icon_24.png differ diff --git a/resource/icon_256.png b/resource/icon_256.png new file mode 100644 index 0000000..f439c30 Binary files /dev/null and b/resource/icon_256.png differ diff --git a/resource/icon_32.png b/resource/icon_32.png new file mode 100644 index 0000000..64af3ae Binary files /dev/null and b/resource/icon_32.png differ diff --git a/resource/icon_40.png b/resource/icon_40.png new file mode 100644 index 0000000..98fd06d Binary files /dev/null and b/resource/icon_40.png differ diff --git a/resource/icon_48.png b/resource/icon_48.png new file mode 100644 index 0000000..42cd3d0 Binary files /dev/null and b/resource/icon_48.png differ diff --git a/resource/icon_64.png b/resource/icon_64.png new file mode 100644 index 0000000..8241b52 Binary files /dev/null and b/resource/icon_64.png differ diff --git a/resource/make_icon.ps1 b/resource/make_icon.ps1 new file mode 100644 index 0000000..3b9cc0b --- /dev/null +++ b/resource/make_icon.ps1 @@ -0,0 +1,138 @@ +# Draws the ASWUnitTests icon (a blue rounded badge with a green check mark and, from 32 px up, a green and red +# progress bar) at each icon size, and writes icon_.png and ASWUnitTests.ico (BMP entries below 256 px, PNG +# at 256) beside this script, replacing them; the same drawing gives the same files. ImageMagick's magick packs +# the .ico, so it must be on the PATH. Also writes a preview sheet of every size on light and dark backgrounds to +# %TEMP%\ASWUnitTests_icon_preview.png. +# Usage (Windows PowerShell): & resource\make_icon.ps1 +Add-Type -AssemblyName System.Drawing +$out = $PSScriptRoot + +$sizes = 16, 20, 24, 32, 40, 48, 64, 256 + +function C([string]$hex) { return [System.Drawing.ColorTranslator]::FromHtml($hex) } +$blueTop = C '#2D66B5' +$blueBottom = C '#173B72' +$edge = C '#0E2850' +$green = C '#4BC55E' +$greenDark = C '#2E8F3E' +$red = C '#E5484D' +$track = C '#0E2850' + +function RoundedRect([float]$x, [float]$y, [float]$w, [float]$h, [float]$r) +{ + $p = New-Object System.Drawing.Drawing2D.GraphicsPath + $d = 2 * $r + $p.AddArc($x, $y, $d, $d, 180, 90) + $p.AddArc($x + $w - $d, $y, $d, $d, 270, 90) + $p.AddArc($x + $w - $d, $y + $h - $d, $d, $d, 0, 90) + $p.AddArc($x, $y + $h - $d, $d, $d, 90, 90) + $p.CloseFigure() + return $p +} + +function Draw([int]$size) +{ + $s = $size / 256.0 + $bmp = New-Object System.Drawing.Bitmap($size, $size, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb) + $g = [System.Drawing.Graphics]::FromImage($bmp) + $g.SmoothingMode = 'AntiAlias' + $g.PixelOffsetMode = 'HighQuality' + $g.Clear([System.Drawing.Color]::Transparent) + + $withBar = ($size -ge 32) + + # Badge: the full square at 16-24 px, so the check mark gets every pixel. + $margin = if ($size -lt 32) { 0 } else { [Math]::Floor(8 * $s) } + $radius = [Math]::Max(2.5, 44 * $s) + $side = $size - 2 * $margin + $badge = RoundedRect $margin $margin $side $side $radius + $bottom = $margin + $side + $brush = New-Object System.Drawing.Drawing2D.LinearGradientBrush( + (New-Object System.Drawing.PointF(0, $margin)), (New-Object System.Drawing.PointF(0, $bottom)), + $blueTop, $blueBottom) + $g.FillPath($brush, $badge) + if ($size -ge 48) + { + $pen = New-Object System.Drawing.Pen($edge, [Math]::Max(1, 4 * $s)) + $g.DrawPath($pen, $badge) + } + + # Check mark: thicker, relative to the size, when it's small. + if ($withBar) + { + $pts = @((64, 120), (108, 164), (194, 76)); $width = 36 + } + else + { + $pts = @((54, 132), (104, 182), (204, 80)); $width = 48 + } + $points = [System.Drawing.PointF[]]@($pts | ForEach-Object { New-Object System.Drawing.PointF(($_[0] * $s), ($_[1] * $s)) }) + if ($size -ge 48) + { + # A darker green rim, for definition against the blue. + $rim = New-Object System.Drawing.Pen($greenDark, (($width + 8) * $s)) + $rim.StartCap = 'Round'; $rim.EndCap = 'Round'; $rim.LineJoin = 'Round' + $g.DrawLines($rim, $points) + } + $checkPen = New-Object System.Drawing.Pen($green, ($width * $s)) + $checkPen.StartCap = 'Round'; $checkPen.EndCap = 'Round'; $checkPen.LineJoin = 'Round' + $g.DrawLines($checkPen, $points) + + # Progress bar: mostly green, ending red, as the GUI's shows passes and a failure. + if ($withBar) + { + $bx = 52 * $s; $bw = 152 * $s + $bh = [Math]::Max(3, 20 * $s); $by = 196 * $s + $br = $bh / 2 + $g.FillPath((New-Object System.Drawing.SolidBrush($track)), (RoundedRect $bx $by $bw $bh $br)) + $split = $bx + $bw * 0.72 + $g.SetClip((New-Object System.Drawing.RectangleF($bx, $by, ($split - $bx), $bh))) + $g.FillPath((New-Object System.Drawing.SolidBrush($green)), (RoundedRect $bx $by $bw $bh $br)) + $g.SetClip((New-Object System.Drawing.RectangleF($split, $by, ($bx + $bw - $split), $bh))) + $g.FillPath((New-Object System.Drawing.SolidBrush($red)), (RoundedRect $bx $by $bw $bh $br)) + $g.ResetClip() + } + + $g.Dispose() + $bmp.Save("$out\icon_$size.png", [System.Drawing.Imaging.ImageFormat]::Png) + return $bmp +} + +$images = @{} +foreach ($size in $sizes) { $images[$size] = Draw $size } + +# Preview sheet: each size at 1x on a light and a dark band, then 16-48 px magnified 6x on both, then 256 px. +$sheetW = 1400; $sheetH = 700 +$sheet = New-Object System.Drawing.Bitmap($sheetW, $sheetH) +$g = [System.Drawing.Graphics]::FromImage($sheet) +$g.Clear((C '#FFFFFF')) +$g.FillRectangle((New-Object System.Drawing.SolidBrush((C '#F3F3F3'))), 0, 0, 700, 400) +$g.FillRectangle((New-Object System.Drawing.SolidBrush((C '#202020'))), 700, 0, 700, 400) +$x = 20 +foreach ($size in 16, 20, 24, 32, 40, 48, 64) +{ + $g.DrawImageUnscaled($images[$size], $x, 20) + $g.DrawImageUnscaled($images[$size], $x + 700, 20) + $x += $size + 20 +} +$g.InterpolationMode = 'NearestNeighbor' +$g.PixelOffsetMode = 'Half' +$x = 20 +foreach ($size in 16, 20, 24, 32) +{ + $g.DrawImage($images[$size], $x, 110, $size * 6, $size * 6) + $g.DrawImage($images[$size], $x + 700, 110, $size * 6, $size * 6) + $x += $size * 6 + 16 +} +$g.InterpolationMode = 'HighQualityBicubic' +$g.DrawImage($images[256], 20, 420, 256, 256) +$g.DrawImage($images[48], 300, 420, 288, 288) +$g.FillRectangle((New-Object System.Drawing.SolidBrush((C '#202020'))), 700, 400, 700, 300) +$g.DrawImage($images[256], 720, 420, 256, 256) +$g.Dispose() +$sheet.Save("$env:TEMP\ASWUnitTests_icon_preview.png", [System.Drawing.Imaging.ImageFormat]::Png) + +$pngs = $sizes | ForEach-Object { "$out\icon_$_.png" } +& magick @pngs "$out\ASWUnitTests.ico" +& magick identify "$out\ASWUnitTests.ico" +"wrote the PNGs and ASWUnitTests.ico to $out, and the preview to $env:TEMP\ASWUnitTests_icon_preview.png" diff --git a/src/ASWUnitTests_CLI.cpp b/src/ASWUnitTests_CLI.cpp index 6d1f3a5..8c4a3f3 100644 --- a/src/ASWUnitTests_CLI.cpp +++ b/src/ASWUnitTests_CLI.cpp @@ -34,10 +34,11 @@ limitations under the License. namespace { +//--------------------------------------------------------------------------- + void PrintUsage(); //--------------------------------------------------------------------------- - void PrintUsage() { std::cout @@ -191,9 +192,14 @@ TestFilter TCLIParser::BuildTestFilter(TTestHandler& tester, TCLIOptions const& //--------------------------------------------------------------------------- std::optional TCLIParser::ParseArguments(int argc, char* argv[], TCLIOptions& options) { - for (int i = 1; i < argc; ++i) + return ParseArguments(std::vector(argv, argv + argc), options); +} +//--------------------------------------------------------------------------- +std::optional TCLIParser::ParseArguments(std::vector const& args, TCLIOptions& options) +{ + for (std::size_t i = 1; i < args.size(); ++i) { - std::string_view arg = argv[i]; + std::string_view const arg = args[i]; if (arg == "--version") { @@ -211,13 +217,13 @@ std::optional TCLIParser::ParseArguments(int argc, char* argv[], TCLIOption } else if (arg == "--filter") { - if (i + 1 >= argc) + if (i + 1 >= args.size()) { std::cout << "Error: --filter requires a pattern argument.\n"; return ExitCode_InvalidArguments; } - options.FilterPattern = argv[++i]; + options.FilterPattern = args[++i]; options.HasFilter = true; } else if (arg.rfind("--filter=", 0) == 0) @@ -235,13 +241,13 @@ std::optional TCLIParser::ParseArguments(int argc, char* argv[], TCLIOption } else if (arg == "--shuffle-seed") { - if (i + 1 >= argc) + if (i + 1 >= args.size()) { std::cout << "Error: --shuffle-seed requires a numeric argument.\n"; return ExitCode_InvalidArguments; } - options.ShuffleSeed = ParseUnsignedInt(argv[++i]); + options.ShuffleSeed = ParseUnsignedInt(args[++i]); if (!options.ShuffleSeed.has_value()) { std::cout << "Error: --shuffle-seed requires a non-negative integer argument.\n"; @@ -263,13 +269,13 @@ std::optional TCLIParser::ParseArguments(int argc, char* argv[], TCLIOption } else if (arg == "--partition-index") { - if (i + 1 >= argc) + if (i + 1 >= args.size()) { std::cout << "Error: --partition-index requires a numeric argument.\n"; return ExitCode_InvalidArguments; } - std::optional const parsed = ParseUnsignedInt(argv[++i]); + std::optional const parsed = ParseUnsignedInt(args[++i]); if (!parsed.has_value()) { std::cout << "Error: --partition-index requires a positive integer argument.\n"; @@ -293,13 +299,13 @@ std::optional TCLIParser::ParseArguments(int argc, char* argv[], TCLIOption } else if (arg == "--partition-count") { - if (i + 1 >= argc) + if (i + 1 >= args.size()) { std::cout << "Error: --partition-count requires a numeric argument.\n"; return ExitCode_InvalidArguments; } - std::optional const parsed = ParseUnsignedInt(argv[++i]); + std::optional const parsed = ParseUnsignedInt(args[++i]); if (!parsed.has_value()) { std::cout << "Error: --partition-count requires a positive integer argument.\n"; @@ -323,13 +329,13 @@ std::optional TCLIParser::ParseArguments(int argc, char* argv[], TCLIOption } else if (arg == "--test-timeout-seconds") { - if (i + 1 >= argc) + if (i + 1 >= args.size()) { std::cout << "Error: --test-timeout-seconds requires a numeric argument.\n"; return ExitCode_InvalidArguments; } - std::optional const parsed = ParseUnsignedInt(argv[++i]); + std::optional const parsed = ParseUnsignedInt(args[++i]); if (!parsed.has_value() || *parsed == 0) { std::cout << "Error: --test-timeout-seconds requires a positive integer argument.\n"; @@ -359,13 +365,13 @@ std::optional TCLIParser::ParseArguments(int argc, char* argv[], TCLIOption } else if (arg == "--color") { - if (i + 1 >= argc) + if (i + 1 >= args.size()) { std::cout << "Error: --color requires \"auto\", \"always\", or \"never\".\n"; return ExitCode_InvalidArguments; } - std::optional const parsed = ParseColorMode(argv[++i]); + std::optional const parsed = ParseColorMode(args[++i]); if (!parsed.has_value()) { std::cout << "Error: --color requires \"auto\", \"always\", or \"never\".\n"; @@ -387,13 +393,13 @@ std::optional TCLIParser::ParseArguments(int argc, char* argv[], TCLIOption } else if (arg == "--color-pass" || arg == "--color-fail" || arg == "--color-skip") { - if (i + 1 >= argc) + if (i + 1 >= args.size()) { std::cout << "Error: " << arg << " requires a color name argument.\n"; return ExitCode_InvalidArguments; } - std::optional const parsed = TConsole::ParseColorName(argv[++i]); + std::optional const parsed = TConsole::ParseColorName(args[++i]); if (!parsed.has_value()) { std::cout << "Error: " << arg << " was given an unrecognized color name.\n"; @@ -429,13 +435,13 @@ std::optional TCLIParser::ParseArguments(int argc, char* argv[], TCLIOption } else if (arg == "--report-junit") { - if (i + 1 >= argc) + if (i + 1 >= args.size()) { std::cout << "Error: --report-junit requires a file path argument.\n"; return ExitCode_InvalidArguments; } - options.JunitReportPath = argv[++i]; + options.JunitReportPath = args[++i]; } else if (arg.rfind("--report-junit=", 0) == 0) { @@ -443,13 +449,13 @@ std::optional TCLIParser::ParseArguments(int argc, char* argv[], TCLIOption } else if (arg == "--project-name") { - if (i + 1 >= argc) + if (i + 1 >= args.size()) { std::cout << "Error: --project-name requires a name argument.\n"; return ExitCode_InvalidArguments; } - options.ProjectName = argv[++i]; + options.ProjectName = args[++i]; } else if (arg.rfind("--project-name=", 0) == 0) { diff --git a/src/ASWUnitTests_CLI.h b/src/ASWUnitTests_CLI.h index 49e279c..a172fe3 100644 --- a/src/ASWUnitTests_CLI.h +++ b/src/ASWUnitTests_CLI.h @@ -96,6 +96,9 @@ class TCLIParser // parse/validation error), or std::nullopt if 'options' was parsed successfully and the program // should proceed to run tests. static std::optional ParseArguments(int argc, char* argv[], TCLIOptions& options); + // The same, with the arguments as strings, in UTF-8 for non-ASCII text. Like argv, 'args' starts with the + // program name, which is skipped. + static std::optional ParseArguments(std::vector const& args, TCLIOptions& options); // Parses "auto"/"always"/"never" as accepted by --color; returns std::nullopt if 'text' isn't // recognized. diff --git a/src/ASWUnitTests_Console.cpp b/src/ASWUnitTests_Console.cpp index 934b781..0ad1ff1 100644 --- a/src/ASWUnitTests_Console.cpp +++ b/src/ASWUnitTests_Console.cpp @@ -141,11 +141,15 @@ TConsoleColor GetColorFor(TLogKind kind) { case TLogKind::Pass: return g_PassColor; + case TLogKind::Fail: return g_FailColor; + case TLogKind::Skip: return g_SkipColor; + case TLogKind::Info: + default: return TConsoleColor::Default; } @@ -243,13 +247,17 @@ void TConsole::SetColor(TLogKind kind, TConsoleColor color) case TLogKind::Pass: g_PassColor = color; break; + case TLogKind::Fail: g_FailColor = color; break; + case TLogKind::Skip: g_SkipColor = color; break; + case TLogKind::Info: + default: break; // Info is never colored } @@ -290,4 +298,43 @@ std::string TConsole::StripColor(std::string const& text) } //--------------------------------------------------------------------------- + +///////////////////////////////////////////////////////////////////////////// +// TConsoleUTF8Output +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +/* + TConsoleUTF8Output::TConsoleUTF8Output + + SetConsoleOutputCP() fails without a console, and then nothing is changed or restored. The console is the one + the process shares with whatever started it (cmd.exe, say), so its code page must be put back afterwards. +*/ +TConsoleUTF8Output::TConsoleUTF8Output() + : m_OriginalCodePage(0) +{ +#if defined(_WIN32) + UINT const original = GetConsoleOutputCP(); + + if (original != 0 && original != CP_UTF8 && SetConsoleOutputCP(CP_UTF8)) + m_OriginalCodePage = original; +#endif +} +//--------------------------------------------------------------------------- +TConsoleUTF8Output::~TConsoleUTF8Output() +{ + Restore(); +} +//--------------------------------------------------------------------------- +void TConsoleUTF8Output::Restore() +{ +#if defined(_WIN32) + if (m_OriginalCodePage != 0) + SetConsoleOutputCP(m_OriginalCodePage); +#endif + + m_OriginalCodePage = 0; +} +//--------------------------------------------------------------------------- + } // namespace ASWUnitTests diff --git a/src/ASWUnitTests_Console.h b/src/ASWUnitTests_Console.h index db3985b..16c9635 100644 --- a/src/ASWUnitTests_Console.h +++ b/src/ASWUnitTests_Console.h @@ -51,7 +51,7 @@ enum class TConsoleColor BrightBlue, BrightMagenta, BrightCyan, - BrightWhite + BrightWhite, }; @@ -62,7 +62,7 @@ enum class TColorMode { Auto, // color only if stdout is an interactive terminal that supports it Always, - Never + Never, }; @@ -77,7 +77,7 @@ enum class TLogKind Info, Pass, Fail, - Skip + Skip, }; @@ -115,6 +115,33 @@ class TConsole static std::string StripColor(std::string const& text); }; + +///////////////////////////////////////////////////////////////////////////// +// TConsoleUTF8Output +// +// Sets the Windows console's output code page to UTF-8 for as long as it +// exists, so the UTF-8 text the framework writes (such as a non-ASCII test +// name or message) shows correctly in a console that uses another code +// page, then restores the console's own. Output redirected to a file or a +// pipe gets the same bytes either way. Does nothing without a console, or +// on Linux, where terminals already expect UTF-8. +///////////////////////////////////////////////////////////////////////////// +class TConsoleUTF8Output +{ +private: + unsigned int m_OriginalCodePage; // The code page to restore, or 0 when there's nothing to restore. + +public: + TConsoleUTF8Output(); + TConsoleUTF8Output(TConsoleUTF8Output const&) = delete; + TConsoleUTF8Output& operator=(TConsoleUTF8Output const&) = delete; + ~TConsoleUTF8Output(); + + // Restores the console's own code page now, e.g. before std::_Exit(), which skips destructors. Does nothing + // once it's restored. + void Restore(); +}; + } // namespace ASWUnitTests #endif // #ifndef ASWUnitTests_ConsoleH diff --git a/src/ASWUnitTests_CrashGuard.cpp b/src/ASWUnitTests_CrashGuard.cpp index 5959fab..df5947a 100644 --- a/src/ASWUnitTests_CrashGuard.cpp +++ b/src/ASWUnitTests_CrashGuard.cpp @@ -61,16 +61,20 @@ using namespace ASWUnitTests; thread_local std::jmp_buf* g_JumpBuf = nullptr; thread_local unsigned long g_FaultCode = 0; +//--------------------------------------------------------------------------- + LONG WINAPI CrashVectoredHandler(PEXCEPTION_POINTERS info); std::string DescribeFault(unsigned long code); bool IsRecognizedFault(unsigned long code); //--------------------------------------------------------------------------- +/* + CrashVectoredHandler -//--------------------------------------------------------------------------- -// MSVC's C4702 (unreachable code) is a code-generation warning, evaluated against the warning state -// in effect at the end of the function rather than at the offending line, so it has to be disabled -// around the whole function; the unreachable return below is intentional (see its comment). + MSVC's C4702 (unreachable code) is a code-generation warning, evaluated against the warning state + in effect at the end of the function rather than at the offending line, so it has to be disabled + around the whole function; the unreachable return below is intentional (see its comment). +*/ #if defined(_MSC_VER) # pragma warning(push) # pragma warning(disable: 4702) @@ -89,22 +93,41 @@ LONG WINAPI CrashVectoredHandler(PEXCEPTION_POINTERS info) #if defined(_MSC_VER) # pragma warning(pop) #endif - //--------------------------------------------------------------------------- std::string DescribeFault(unsigned long code) { switch (code) { - case EXCEPTION_ACCESS_VIOLATION: return "access violation"; - case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "array bounds exceeded"; - case EXCEPTION_DATATYPE_MISALIGNMENT: return "data misalignment"; - case EXCEPTION_FLT_DIVIDE_BY_ZERO: return "floating point divide by zero"; - case EXCEPTION_FLT_INVALID_OPERATION: return "floating point invalid operation"; - case EXCEPTION_ILLEGAL_INSTRUCTION: return "illegal instruction"; - case EXCEPTION_INT_DIVIDE_BY_ZERO: return "integer divide by zero"; - case EXCEPTION_INT_OVERFLOW: return "integer overflow"; - case EXCEPTION_PRIV_INSTRUCTION: return "privileged instruction"; - case EXCEPTION_STACK_OVERFLOW: return "stack overflow"; + case EXCEPTION_ACCESS_VIOLATION: + return "access violation"; + + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + return "array bounds exceeded"; + + case EXCEPTION_DATATYPE_MISALIGNMENT: + return "data misalignment"; + + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + return "floating point divide by zero"; + + case EXCEPTION_FLT_INVALID_OPERATION: + return "floating point invalid operation"; + + case EXCEPTION_ILLEGAL_INSTRUCTION: + return "illegal instruction"; + + case EXCEPTION_INT_DIVIDE_BY_ZERO: + return "integer divide by zero"; + + case EXCEPTION_INT_OVERFLOW: + return "integer overflow"; + + case EXCEPTION_PRIV_INSTRUCTION: + return "privileged instruction"; + + case EXCEPTION_STACK_OVERFLOW: + return "stack overflow"; + default: { char buffer[40]; @@ -115,9 +138,13 @@ std::string DescribeFault(unsigned long code) } //--------------------------------------------------------------------------- -// Only these codes are ever treated as "the test crashed"; anything else, including a C++ -// exception's own internal dispatch codes, is left completely alone (see the comment above -// g_JumpBuf), so ordinary C++ exception handling is unaffected by this handler being installed. +/* + IsRecognizedFault + + Only these codes are ever treated as "the test crashed"; anything else, including a C++ + exception's own internal dispatch codes, is left completely alone (see the comment above + g_JumpBuf), so ordinary C++ exception handling is unaffected by this handler being installed. +*/ bool IsRecognizedFault(unsigned long code) { switch (code) @@ -139,6 +166,7 @@ bool IsRecognizedFault(unsigned long code) case EXCEPTION_PRIV_INSTRUCTION: case EXCEPTION_STACK_OVERFLOW: return true; + default: return false; } @@ -167,13 +195,13 @@ thread_local int g_FaultSignal = 0; // mirroring why SetThreadStackGuarantee() on the Windows side below is also called every time. thread_local char g_AltStackBuffer[64 * 1024]; +//--------------------------------------------------------------------------- + void CrashSignalHandler(int signalNumber); std::string DescribeFault(int signalNumber); void EnsureAlternateSignalStack(); void InstallSignalHandlersOnce(); -//--------------------------------------------------------------------------- - //--------------------------------------------------------------------------- void CrashSignalHandler(int signalNumber) { @@ -196,11 +224,20 @@ std::string DescribeFault(int signalNumber) { switch (signalNumber) { - case SIGSEGV: return "segmentation fault"; - case SIGFPE: return "floating point exception"; - case SIGILL: return "illegal instruction"; - case SIGBUS: return "bus error"; - default: return "signal " + std::to_string(signalNumber); + case SIGSEGV: + return "segmentation fault"; + + case SIGFPE: + return "floating point exception"; + + case SIGILL: + return "illegal instruction"; + + case SIGBUS: + return "bus error"; + + default: + return "signal " + std::to_string(signalNumber); } } @@ -238,6 +275,8 @@ void InstallSignalHandlersOnce() } // namespace +//--------------------------------------------------------------------------- + namespace ASWUnitTests { @@ -376,5 +415,6 @@ TCrashGuardResult TCrashGuard::Run(std::function const& body) return result; } +//--------------------------------------------------------------------------- } // namespace ASWUnitTests diff --git a/src/ASWUnitTests_Exception.cpp b/src/ASWUnitTests_Exception.cpp index b810be3..a945d34 100644 --- a/src/ASWUnitTests_Exception.cpp +++ b/src/ASWUnitTests_Exception.cpp @@ -34,6 +34,8 @@ namespace ASWUnitTests namespace { +//--------------------------------------------------------------------------- + std::string ToUTF8(System::UnicodeString const& text); //--------------------------------------------------------------------------- @@ -323,6 +325,12 @@ TExceptNotEmpty::TExceptNotEmpty(std::string const& method, int line, std::strin m_Message = "Empty: " + method + " (" + std::to_string(line) + "): Expected not empty but was empty. " + msg; } //--------------------------------------------------------------------------- +TExceptNotEmpty::TExceptNotEmpty(std::string const& method, int line, std::string const& detail, + std::string const& msg) +{ + m_Message = "Empty: " + method + " (" + std::to_string(line) + "): " + detail + ". " + msg; +} +//--------------------------------------------------------------------------- ///////////////////////////////////////////////////////////////////////////// diff --git a/src/ASWUnitTests_Exception.h b/src/ASWUnitTests_Exception.h index 3538b91..27d54f5 100644 --- a/src/ASWUnitTests_Exception.h +++ b/src/ASWUnitTests_Exception.h @@ -43,16 +43,6 @@ limitations under the License. namespace ASWUnitTests { -#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) -// Returns "ClassName: Message" for 'ex' (e.g. "EConvertError: 'abc' is not a -// valid integer value"), converted to UTF-8 so non-ASCII text survives. -std::string DescribeRTLException(System::Sysutils::Exception& ex); -// Returns just 'ex.Message', converted to UTF-8. -std::string RTLExceptionMessage(System::Sysutils::Exception& ex); -#endif - -//--------------------------------------------------------------------------- - ///////////////////////////////////////////////////////////////////////////// // TTestException ///////////////////////////////////////////////////////////////////////////// @@ -345,6 +335,8 @@ class TExceptNotEmpty : public TTestException { public: TExceptNotEmpty(std::string const& method, int line, std::string const& msg); + // With a detail in place of "Expected not empty but was empty", e.g. for a null C string. + TExceptNotEmpty(std::string const& method, int line, std::string const& detail, std::string const& msg); }; @@ -463,12 +455,13 @@ class TExceptInvalidSuitePath : public std::logic_error TExceptInvalidSuitePath(std::string const& msg, bool isMessage); public: - explicit TExceptInvalidSuitePath(std::string const& suitePath); - // A group registered in suite 'registeredPath' whose own suite path (from its constructor, or its // GetSuitePath() for an ITestGroup that doesn't derive from TTestGroupBase) is 'groupPath'. static TExceptInvalidSuitePath Conflicting(std::string const& groupName, std::string const& registeredPath, std::string const& groupPath); + +public: + explicit TExceptInvalidSuitePath(std::string const& suitePath); }; // /////// Compiler specific exceptions after this line ///////////////////// @@ -499,6 +492,16 @@ class TExceptRTLException : public std::runtime_error #endif +//--------------------------------------------------------------------------- + +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) +// Returns "ClassName: Message" for 'ex' (e.g. "EConvertError: 'abc' is not a +// valid integer value"), converted to UTF-8 so non-ASCII text survives. +std::string DescribeRTLException(System::Sysutils::Exception& ex); +// Returns just 'ex.Message', converted to UTF-8. +std::string RTLExceptionMessage(System::Sysutils::Exception& ex); +#endif + } // namespace ASWUnitTests //--------------------------------------------------------------------------- diff --git a/src/ASWUnitTests_Handler.cpp b/src/ASWUnitTests_Handler.cpp index c83c329..30ee172 100644 --- a/src/ASWUnitTests_Handler.cpp +++ b/src/ASWUnitTests_Handler.cpp @@ -51,40 +51,6 @@ limitations under the License. namespace { -// Calls 'callable' and returns a description of what it threw: what() for a std::exception (this framework's own -// failure signals, such as an Assert* failure, included), or "ClassName: Message" for an RTL exception (see -// DescribeRTLException()); or nullopt when it threw nothing. Each exception is caught exactly once, never rethrown -// and caught again, since an RTL exception object is freed when the first handler that catches it ends. -std::optional CallAndDescribeException(std::function const& callable) -{ - try - { - callable(); - } - catch (std::exception const& ex) - { - return std::string(ex.what()); - } -#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) - catch (System::Sysutils::Exception& ex) - { - return ASWUnitTests::DescribeRTLException(ex); - } - catch (...) - { - return std::string("an object that is neither a std::exception nor an RTL Exception"); - } -#else - catch (...) - { - return std::string("a non-std::exception object"); - } -#endif - - return std::nullopt; -} - - ///////////////////////////////////////////////////////////////////////////// // TUncreatableTestGroup // @@ -116,8 +82,53 @@ class TUncreatableTestGroup : public ASWUnitTests::TTestGroupBase } }; +//--------------------------------------------------------------------------- + +std::optional CallAndDescribeException(std::function const& callable); + +//--------------------------------------------------------------------------- +/* + CallAndDescribeException + + Calls 'callable' and returns a description of what it threw: what() for a std::exception (this framework's own + failure signals, such as an Assert* failure, included), or "ClassName: Message" for an RTL exception (see + DescribeRTLException()); or nullopt when it threw nothing. Each exception is caught exactly once, never rethrown + and caught again, since an RTL exception object is freed when the first handler that catches it ends. +*/ +std::optional CallAndDescribeException(std::function const& callable) +{ + try + { + callable(); + } + catch (std::exception const& ex) + { + return std::string(ex.what()); + } +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) + catch (System::Sysutils::Exception& ex) + { + return ASWUnitTests::DescribeRTLException(ex); + } + catch (...) + { + return std::string("an object that is neither a std::exception nor an RTL Exception"); + } +#else + catch (...) + { + return std::string("a non-std::exception object"); + } +#endif + + return std::nullopt; +} +//--------------------------------------------------------------------------- + } // namespace +//--------------------------------------------------------------------------- + namespace ASWUnitTests { @@ -302,6 +313,25 @@ std::vector TTestHandler::GetAllTestFullNames() return fullNames; } //--------------------------------------------------------------------------- +std::string TTestHandler::GetAppBitsStr() +{ + if constexpr (sizeof(void*) == 8) + return "64-bit"; + else + return "32-bit"; +} +//--------------------------------------------------------------------------- +std::string TTestHandler::GetBuildOS() +{ +#if defined(_WIN32) + return "Windows"; +#elif defined(__linux__) + return "Linux"; +#else + return "Unknown"; +#endif +} +//--------------------------------------------------------------------------- std::string const& TTestHandler::GetProjectName() const { return m_ProjectName; @@ -515,7 +545,7 @@ void TTestHandler::Initialize(std::string const& projectName) { m_ProjectName = projectName; - Log(GetVersionFullStr()); + Log(GetVersionFullStr() + " - " + GetAppBitsStr() + " - " + GetBuildOS()); Log("Initializing test handler and registering test groups for " + m_ProjectName + "..."); RegisterTestGroups(); diff --git a/src/ASWUnitTests_Handler.h b/src/ASWUnitTests_Handler.h index 285bed3..9a4926a 100644 --- a/src/ASWUnitTests_Handler.h +++ b/src/ASWUnitTests_Handler.h @@ -126,6 +126,10 @@ class TTestHandler // it, always run together. Groups that tie on both keep their order in 'groups'. Throws // TExceptInvalidSuitePath for an invalid suite path (see TSuitePath). static std::vector CanonicalGroupOrder(std::vector const& groups); + // Returns "64-bit" or "32-bit": the pointer size this framework was built for. + static std::string GetAppBitsStr(); + // Returns the operating system this framework was built for: "Windows" or "Linux" ("Unknown" for any other). + static std::string GetBuildOS(); static std::string GetUTCTimeISO8601(); static std::string GetVersionFullStr(); static std::string GetVersionStr(); diff --git a/src/ASWUnitTests_JUnitReport.cpp b/src/ASWUnitTests_JUnitReport.cpp index 27745ce..f88b280 100644 --- a/src/ASWUnitTests_JUnitReport.cpp +++ b/src/ASWUnitTests_JUnitReport.cpp @@ -26,16 +26,21 @@ limitations under the License. //--------------------------------------------------------------------------- #include #include +#include #include #include #include //--------------------------------------------------------------------------- +#include "ASWUnitTests_Utils.h" +//--------------------------------------------------------------------------- namespace { using namespace ASWUnitTests; +//--------------------------------------------------------------------------- + std::string FormatSeconds(double seconds); std::string FormatUtcTimestamp(); void WriteTestCase(std::ostringstream& body, TJUnitTestCase const& testCase); @@ -98,6 +103,8 @@ void WriteTestCase(std::ostringstream& body, TJUnitTestCase const& testCase) } // namespace +//--------------------------------------------------------------------------- + namespace ASWUnitTests { @@ -173,18 +180,23 @@ std::string TJUnitReportWriter::EscapeXml(std::string const& text) case '&': result += "&"; break; + case '<': result += "<"; break; + case '>': result += ">"; break; + case '"': result += """; break; + case '\'': result += "'"; break; + default: result += ch; break; @@ -199,8 +211,15 @@ bool TJUnitReportWriter::Write(std::string const& filePath, std::string const& s { std::string const xml = BuildXML(suitesName, testCases); + // On Windows, a std::string path is read in the ANSI code page, so 'filePath''s UTF-8 is converted first. +#if defined(_WIN32) + std::filesystem::path const path(UTF8ToWide(filePath)); +#else + std::filesystem::path const path(filePath); +#endif + // Binary, so the file holds BuildXML()'s bytes unchanged, with LF line ends, rather than CRLF on Windows. - std::ofstream file(filePath, std::ios::out | std::ios::trunc | std::ios::binary); + std::ofstream file(path, std::ios::out | std::ios::trunc | std::ios::binary); if (!file.is_open()) return false; diff --git a/src/ASWUnitTests_JUnitReport.h b/src/ASWUnitTests_JUnitReport.h index fd2f41a..11b5672 100644 --- a/src/ASWUnitTests_JUnitReport.h +++ b/src/ASWUnitTests_JUnitReport.h @@ -37,7 +37,7 @@ enum class TJUnitOutcome { Pass, Fail, - Skip + Skip, }; @@ -80,7 +80,8 @@ class TJUnitReportWriter static std::string EscapeXml(std::string const& text); // Writes the report BuildXML() builds to 'filePath', byte for byte (LF line ends on every - // platform). Returns false if 'filePath' could not be opened for writing. + // platform). 'filePath' is UTF-8, on Windows too. Returns false if 'filePath' could not be + // opened for writing. static bool Write(std::string const& filePath, std::string const& suitesName, std::vector const& testCases); }; diff --git a/src/ASWUnitTests_Sources.cmake b/src/ASWUnitTests_Sources.cmake index f8ab748..36b17e7 100644 --- a/src/ASWUnitTests_Sources.cmake +++ b/src/ASWUnitTests_Sources.cmake @@ -7,6 +7,10 @@ # core header has a matching .cpp, so listing ASWUNITTESTS_SOURCES is # enough; CMake/IDEs associate each header with its .cpp automatically. # +# ASWUNITTESTS_CORE_SOURCES is the same list without main.cpp, for a runner +# that brings its own entry point, such as the Win32 GUI runner +# (win32/gui/src/ASWUnitTests_Win32GUI_Sources.cmake includes this file). +# # Usage, from the consuming project's own CMakeLists.txt (never edit this # repository's own cmake/CMakeLists.txt or rad370/ASWUnitTests.cbproj to add # your tests): @@ -20,7 +24,7 @@ set(ASWUNITTESTS_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}") -set(ASWUNITTESTS_SOURCES +set(ASWUNITTESTS_CORE_SOURCES "${ASWUNITTESTS_SOURCE_DIR}/ASWUnitTests_CLI.cpp" "${ASWUNITTESTS_SOURCE_DIR}/ASWUnitTests_Console.cpp" "${ASWUNITTESTS_SOURCE_DIR}/ASWUnitTests_CrashGuard.cpp" @@ -32,5 +36,9 @@ set(ASWUNITTESTS_SOURCES "${ASWUNITTESTS_SOURCE_DIR}/ASWUnitTests_TestBase.cpp" "${ASWUNITTESTS_SOURCE_DIR}/ASWUnitTests_Utils.cpp" "${ASWUNITTESTS_SOURCE_DIR}/ASWUnitTests_Version.cpp" +) + +set(ASWUNITTESTS_SOURCES + ${ASWUNITTESTS_CORE_SOURCES} "${ASWUNITTESTS_SOURCE_DIR}/main.cpp" ) diff --git a/src/ASWUnitTests_TestBase.cpp b/src/ASWUnitTests_TestBase.cpp index 14f47f2..f73c234 100644 --- a/src/ASWUnitTests_TestBase.cpp +++ b/src/ASWUnitTests_TestBase.cpp @@ -25,6 +25,7 @@ limitations under the License. #include "ASWUnitTests_TestBase.h" //--------------------------------------------------------------------------- #include +#include #include #include #include @@ -51,6 +52,11 @@ limitations under the License. # define ASWUNITTESTS_CXXABI_DEMANGLE_ENABLED 1 # endif #endif +// _rtl_unmangle(), for TypeName() with bcc32c, whose std::type_info::name() is Borland-mangled (see +// BorlandTypeName()). +#if defined(__BORLANDC__) && !defined(_WIN64) +# include +#endif //--------------------------------------------------------------------------- namespace @@ -68,31 +74,212 @@ struct TWorkerState // The worker state of the test this thread runs, or nullptr on a thread that isn't a test's worker. thread_local TWorkerState* t_WorkerState = nullptr; -// Returns 'str' for display in a failure message, or "(null)" when it's nullptr. +//--------------------------------------------------------------------------- + +#if defined(__BORLANDC__) && !defined(_WIN64) +std::string BorlandTypeName(char const* name); +#endif + +std::string CStringDisplayText(char const* str); +std::string CStringDisplayText(wchar_t const* str); + +template +std::string CStringEmptyFailure(TChar const* text); + +template +std::string CStringNotEmptyFailure(TChar const* text); + +std::string CStringOrEmpty(char const* str); +std::wstring CStringOrEmpty(wchar_t const* str); + +template +bool CharEqualsIgnoringASCIICase(TChar a, TChar b); + +template +bool ContainsIgnoringASCIICase(TString const& text, TString const& substring); + +std::string DecimalSum(uint64_t a, uint64_t b); + +std::string DisplayText(std::string const& text); + +template +bool EndsWith(TString const& text, TString const& suffix); + +template +bool EndsWithIgnoringASCIICase(TString const& text, TString const& suffix); + +template +bool EqualsIgnoringASCIICase(TString const& a, TString const& b); + +std::string FormatDurationMs(std::chrono::steady_clock::time_point start); + +template +std::string FormatFloatingPoint(TFloat value, int precision); + +template +std::pair FormatFloatingPointPair(TFloat value, TFloat bound); + +std::string HexBytes(unsigned char const* bytes, std::size_t offset, std::size_t size); + +template +bool MatchesRegex(TString const& text, TString const& pattern, std::string& error); + +std::string MemoryFailure(void const* expected, void const* actual, std::size_t size, bool expectEqual); + +template +TFloat NearDifference(TFloat expected, TFloat actual); + +template +std::string NullRegexMatchFailure(TChar const* text, TChar const* pattern); + +template +std::string NullTextFailure(TChar const* text, TChar const* part, char const* relation, bool ignoreCase); + +template +std::string RegexMatchFailure(TString const& text, TString const& pattern, bool expectMatch); + +template +bool StartsWith(TString const& text, TString const& prefix); + +template +bool StartsWithIgnoringASCIICase(TString const& text, TString const& prefix); + +//--------------------------------------------------------------------------- +#if defined(__BORLANDC__) && !defined(_WIN64) +/* + BorlandTypeName + + Returns 'name', a std::type_info::name() from bcc32c, readably (e.g. "int" for "$i", "N::TNamed" for + "$N@TNamed"), or as it is when it can't be read. It's a Borland-mangled type, which _rtl_unmangle() reads as the + argument of a function f(): as it is for a built-in type ("@f$qi", "@f$qpxc"), or for a class, or a pointer to + one, with the class's length in front ("@f$q8N@TNamed", "@f$qp8N@TNamed"). An anonymous namespace, which + _rtl_unmangle() can't read, is given a name of its own first, then shown as "(anonymous namespace)". +*/ +std::string BorlandTypeName(char const* name) +{ + if ((name == nullptr) || (name[0] != '$')) + return (name == nullptr) ? std::string() : std::string(name); + + std::string const anonymous = "12_GLOBAL__N_1@"; + std::string const anonymousStandIn = "ASWUnitTestsAnonymousNamespace@"; + std::string code(name + 1); + + for (std::size_t at = code.find(anonymous); at != std::string::npos; + at = code.find(anonymous, at + anonymousStandIn.size())) + code.replace(at, anonymous.size(), anonymousStandIn); + + std::size_t const pointers = std::min(code.find_first_not_of('p'), code.size()); + std::string const symbols[2] = + { + "@f$q" + code, + "@f$q" + code.substr(0, pointers) + std::to_string(code.size() - pointers) + code.substr(pointers) + }; + // After a pointer, an 'x' or 'w' is const or volatile, which the second form would read as part of a class's + // name (e.g. "xN::TNamed *" for "$pxN@TNamed"), so only the first is tried. + bool const qualifiedPointee = (pointers > 0) && (pointers < code.size()) && + ((code[pointers] == 'x') || (code[pointers] == 'w')); + + for (std::string symbol : symbols) + { + if (qualifiedPointee && (symbol != symbols[0])) + break; + + char unmangled[1024] = {}; + _umKind const kind = _rtl_unmangle(&symbol[0], unmangled, sizeof(unmangled) - 1, nullptr, nullptr, 1); + std::string const text(unmangled); + std::size_t const arguments = text.find("f("); + + if (((kind & (UM_ERRMASK | UM_NOT_MANGLED)) != 0) || (arguments == std::string::npos) || + (text.back() != ')')) + continue; + + std::string type = text.substr(arguments + 2, text.size() - arguments - 3); + std::string const standInQualifier = anonymousStandIn.substr(0, anonymousStandIn.size() - 1) + "::"; + for (std::size_t at = type.find(standInQualifier); at != std::string::npos; + at = type.find(standInQualifier, at + 1)) + type.replace(at, standInQualifier.size(), "(anonymous namespace)::"); + + return type; + } + + return name; +} +//--------------------------------------------------------------------------- +#endif +/* + CStringDisplayText + + Returns 'str' for display in a failure message, or "(null)" when it's nullptr. +*/ std::string CStringDisplayText(char const* str) { return (str != nullptr) ? std::string(str) : std::string("(null)"); } +//--------------------------------------------------------------------------- +/* + CStringDisplayText -// Returns 'str' as UTF-8 for display in a failure message, or "(null)" when it's nullptr. + Returns 'str' as UTF-8 for display in a failure message, or "(null)" when it's nullptr. +*/ std::string CStringDisplayText(wchar_t const* str) { return (str != nullptr) ? ASWUnitTests::WideToUTF8(str) : std::string("(null)"); } +//--------------------------------------------------------------------------- +/* + CStringEmptyFailure + + Why 'text' fails an Empty method, e.g. Expected empty but was "abc" (or "(null)" for a null C string), or an + empty string when it's empty. +*/ +template +std::string CStringEmptyFailure(TChar const* text) +{ + if (text != nullptr && *text == TChar()) + return std::string(); + + return "Expected empty but was \"" + CStringDisplayText(text) + "\""; +} +//--------------------------------------------------------------------------- +/* + CStringNotEmptyFailure + + Why 'text' fails a NotEmpty method, Expected not empty but was empty (or was "(null)" for a null C string), or + an empty string when it isn't empty. +*/ +template +std::string CStringNotEmptyFailure(TChar const* text) +{ + if (text == nullptr) + return "Expected not empty but was \"(null)\""; + + if (*text == TChar()) + return "Expected not empty but was empty"; + + return std::string(); +} +//--------------------------------------------------------------------------- +/* + CStringOrEmpty -// Returns 'str' as a string, or an empty one when it's nullptr. + Returns 'str' as a string, or an empty one when it's nullptr. +*/ std::string CStringOrEmpty(char const* str) { return (str != nullptr) ? std::string(str) : std::string(); } - +//--------------------------------------------------------------------------- std::wstring CStringOrEmpty(wchar_t const* str) { return (str != nullptr) ? std::wstring(str) : std::wstring(); } +//--------------------------------------------------------------------------- +/* + CharEqualsIgnoringASCIICase -// True if 'a' and 'b' are the same character, ignoring the case of the ASCII letters A-Z only, so the result doesn't -// depend on the platform or the current locale (and a byte of a multi-byte UTF-8 sequence is never changed). + True if 'a' and 'b' are the same character, ignoring the case of the ASCII letters A-Z only, so the result doesn't + depend on the platform or the current locale (and a byte of a multi-byte UTF-8 sequence is never changed). +*/ template bool CharEqualsIgnoringASCIICase(TChar a, TChar b) { @@ -106,8 +293,12 @@ bool CharEqualsIgnoringASCIICase(TChar a, TChar b) return toLower(a) == toLower(b); } +//--------------------------------------------------------------------------- +/* + ContainsIgnoringASCIICase -// True if 'text' contains 'substring', comparing characters with CharEqualsIgnoringASCIICase(). + True if 'text' contains 'substring', comparing characters with CharEqualsIgnoringASCIICase(). +*/ template bool ContainsIgnoringASCIICase(TString const& text, TString const& substring) { @@ -115,41 +306,90 @@ bool ContainsIgnoringASCIICase(TString const& text, TString const& substring) return substring.empty() || (std::search(text.begin(), text.end(), substring.begin(), substring.end(), CharEqualsIgnoringASCIICase) != text.end()); } +//--------------------------------------------------------------------------- +/* + DecimalSum -// True if 'text' ends with 'suffix'. Every text ends with the empty string, and none with a longer suffix. -template -bool EndsWith(TString const& text, TString const& suffix) + Returns 'a' + 'b' in decimal, exactly, even when the sum doesn't fit in a uint64_t. +*/ +std::string DecimalSum(uint64_t a, uint64_t b) { - return text.size() >= suffix.size() && text.compare(text.size() - suffix.size(), suffix.size(), suffix) == 0; -} + if (a <= std::numeric_limits::max() - b) + return std::to_string(a + b); -// True if 'text' ends with 'suffix', comparing characters with CharEqualsIgnoringASCIICase(). -template -bool EndsWithIgnoringASCIICase(TString const& text, TString const& suffix) -{ - return text.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), text.rbegin(), - CharEqualsIgnoringASCIICase); + // Long addition of the two numbers' digits, from the last. + std::string const first = std::to_string(a); + std::string const second = std::to_string(b); + std::string sum; + int carry = 0; + + for (std::size_t i = 0; i < first.size() || i < second.size() || carry != 0; ++i) + { + int digit = carry; + + if (i < first.size()) + digit += first[first.size() - 1 - i] - '0'; + + if (i < second.size()) + digit += second[second.size() - 1 - i] - '0'; + + sum.insert(sum.begin(), static_cast('0' + digit % 10)); + carry = digit / 10; + } + + return sum; } +//--------------------------------------------------------------------------- +/* + DisplayText -// Returns 'text' for display in a failure message, as UTF-8. + Returns 'text' for display in a failure message, as UTF-8. +*/ std::string DisplayText(std::string const& text) { return text; } - +//--------------------------------------------------------------------------- std::string DisplayText(std::wstring const& text) { return ASWUnitTests::WideToUTF8(text); } +//--------------------------------------------------------------------------- +/* + EndsWith + + True if 'text' ends with 'suffix'. Every text ends with the empty string, and none with a longer suffix. +*/ +template +bool EndsWith(TString const& text, TString const& suffix) +{ + return text.size() >= suffix.size() && text.compare(text.size() - suffix.size(), suffix.size(), suffix) == 0; +} +//--------------------------------------------------------------------------- +/* + EndsWithIgnoringASCIICase -// True if 'a' and 'b' are the same text, comparing characters with CharEqualsIgnoringASCIICase(). + True if 'text' ends with 'suffix', comparing characters with CharEqualsIgnoringASCIICase(). +*/ +template +bool EndsWithIgnoringASCIICase(TString const& text, TString const& suffix) +{ + return text.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), text.rbegin(), + CharEqualsIgnoringASCIICase); +} +//--------------------------------------------------------------------------- +/* + EqualsIgnoringASCIICase + + True if 'a' and 'b' are the same text, comparing characters with CharEqualsIgnoringASCIICase(). +*/ template bool EqualsIgnoringASCIICase(TString const& a, TString const& b) { return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), CharEqualsIgnoringASCIICase); } - +//--------------------------------------------------------------------------- std::string FormatDurationMs(std::chrono::steady_clock::time_point start) { double const elapsedMs = std::chrono::duration( @@ -159,8 +399,12 @@ std::string FormatDurationMs(std::chrono::steady_clock::time_point start) oss << std::fixed << std::setprecision(3) << elapsedMs << " ms"; return oss.str(); } +//--------------------------------------------------------------------------- +/* + FormatFloatingPoint -// Returns 'value' with 'precision' significant digits, in the classic locale so it doesn't depend on the current one. + Returns 'value' with 'precision' significant digits, in the classic locale so it doesn't depend on the current one. +*/ template std::string FormatFloatingPoint(TFloat value, int precision) { @@ -169,9 +413,13 @@ std::string FormatFloatingPoint(TFloat value, int precision) oss << std::setprecision(precision) << value; return oss.str(); } +//--------------------------------------------------------------------------- +/* + FormatFloatingPointPair -// Returns 'value' and 'bound' with as many digits as TFloat reliably holds, or with all of them when that would make -// two different values look equal. + Returns 'value' and 'bound' with as many digits as TFloat reliably holds, or with all of them when that would make + two different values look equal. +*/ template std::pair FormatFloatingPointPair(TFloat value, TFloat bound) { @@ -186,9 +434,13 @@ std::pair FormatFloatingPointPair(TFloat value, TFloat return texts; } +//--------------------------------------------------------------------------- +/* + HexBytes -// Up to 16 of the 'size' bytes at 'bytes', from 'offset', as uppercase hex pairs (e.g. "0A 1B 2C"), followed by " ..." -// when more follow. + Up to 16 of the 'size' bytes at 'bytes', from 'offset', as uppercase hex pairs (e.g. "0A 1B 2C"), followed by " ..." + when more follow. +*/ std::string HexBytes(unsigned char const* bytes, std::size_t offset, std::size_t size) { std::size_t const maxShown = 16; @@ -204,9 +456,13 @@ std::string HexBytes(unsigned char const* bytes, std::size_t offset, std::size_t return oss.str(); } +//--------------------------------------------------------------------------- +/* + MatchesRegex -// True if the whole of 'text' matches the ECMAScript regular expression 'pattern'. When 'pattern' is invalid, or too -// complex to match, returns false and sets 'error' to std::regex_error's description; otherwise leaves 'error' alone. + True if the whole of 'text' matches the ECMAScript regular expression 'pattern'. When 'pattern' is invalid, or too + complex to match, returns false and sets 'error' to std::regex_error's description; otherwise leaves 'error' alone. +*/ template bool MatchesRegex(TString const& text, TString const& pattern, std::string& error) { @@ -221,10 +477,14 @@ bool MatchesRegex(TString const& text, TString const& pattern, std::string& erro return false; } } +//--------------------------------------------------------------------------- +/* + MemoryFailure -// The failure detail for an EqualsMem (when 'expectEqual') or NotEqualsMem method, or an empty string when it passes. -// Zero bytes, or the same pointer twice (even null), are equal without reading either. Exactly one null pointer is -// never equal to the other memory, and isn't read. + The failure detail for an EqualsMem (when 'expectEqual') or NotEqualsMem method, or an empty string when it passes. + Zero bytes, or the same pointer twice (even null), are equal without reading either. Exactly one null pointer is + never equal to the other memory, and isn't read. +*/ std::string MemoryFailure(void const* expected, void const* actual, std::size_t size, bool expectEqual) { unsigned char const* const expectedBytes = static_cast(expected); @@ -261,10 +521,14 @@ std::string MemoryFailure(void const* expected, void const* actual, std::size_t return "Expected the " + std::to_string(size) + " bytes to differ, but both are \"" + bytes + "\""; } +//--------------------------------------------------------------------------- +/* + NearDifference -// The absolute difference between 'expected' and 'actual' for the Near and NotNear methods: zero when they're equal, -// so equal infinities (whose difference is a NaN) are near each other, and a NaN when either is a NaN, which a -// "diff <= tolerance" test never accepts, so a NaN is near nothing, not even another NaN. + The absolute difference between 'expected' and 'actual' for the Near and NotNear methods: zero when they're equal, + so equal infinities (whose difference is a NaN) are near each other, and a NaN when either is a NaN, which a + "diff <= tolerance" test never accepts, so a NaN is near nothing, not even another NaN. +*/ template TFloat NearDifference(TFloat expected, TFloat actual) { @@ -273,18 +537,38 @@ TFloat NearDifference(TFloat expected, TFloat actual) return std::fabs(expected - actual); } +//--------------------------------------------------------------------------- +/* + NullTextFailure -// The detail for a text check that fails because 'text' or 'part' is a null C string, worded as the check's other -// failures are, e.g. Expected "(null)" to contain "abc". followed by the caller's message. + The detail for a text check that fails because 'text' or 'part' is a null C string, worded as the check's other + failures are, e.g. Expected "(null)" to contain "abc". followed by the caller's message. +*/ +/* + NullRegexMatchFailure + + The failure detail for a Matches method given a null C string as 'text' or 'pattern', worded as + RegexMatchFailure()'s, e.g. Expected "(null)" to match "a.c". +*/ +template +std::string NullRegexMatchFailure(TChar const* text, TChar const* pattern) +{ + return "Expected \"" + CStringDisplayText(text) + "\" to match \"" + CStringDisplayText(pattern) + "\""; +} +//--------------------------------------------------------------------------- template std::string NullTextFailure(TChar const* text, TChar const* part, char const* relation, bool ignoreCase) { return "Expected \"" + CStringDisplayText(text) + "\" " + relation + " \"" + CStringDisplayText(part) + "\"" + (ignoreCase ? " (ignoring case)" : "") + ". "; } +//--------------------------------------------------------------------------- +/* + RegexMatchFailure -// The failure detail for a Matches (when 'expectMatch') or NotMatches method, or an empty string when it passes. An -// invalid pattern always fails, saying why. + The failure detail for a Matches (when 'expectMatch') or NotMatches method, or an empty string when it passes. An + invalid pattern always fails, saying why. +*/ template std::string RegexMatchFailure(TString const& text, TString const& pattern, bool expectMatch) { @@ -300,24 +584,35 @@ std::string RegexMatchFailure(TString const& text, TString const& pattern, bool return "Expected \"" + DisplayText(text) + "\" " + (expectMatch ? "to match" : "not to match") + " \"" + DisplayText(pattern) + "\""; } +//--------------------------------------------------------------------------- +/* + StartsWith -// True if 'text' starts with 'prefix'. Every text starts with the empty string, and none with a longer prefix. + True if 'text' starts with 'prefix'. Every text starts with the empty string, and none with a longer prefix. +*/ template bool StartsWith(TString const& text, TString const& prefix) { return text.size() >= prefix.size() && text.compare(0, prefix.size(), prefix) == 0; } +//--------------------------------------------------------------------------- +/* + StartsWithIgnoringASCIICase -// True if 'text' starts with 'prefix', comparing characters with CharEqualsIgnoringASCIICase(). + True if 'text' starts with 'prefix', comparing characters with CharEqualsIgnoringASCIICase(). +*/ template bool StartsWithIgnoringASCIICase(TString const& text, TString const& prefix) { return text.size() >= prefix.size() && std::equal(prefix.begin(), prefix.end(), text.begin(), CharEqualsIgnoringASCIICase); } +//--------------------------------------------------------------------------- } // namespace +//--------------------------------------------------------------------------- + namespace ASWUnitTests { @@ -427,7 +722,8 @@ TTestGroupBase::TTestGroupBase(std::string const& name) } //--------------------------------------------------------------------------- TTestGroupBase::TTestGroupBase(TSuitePath const& suitePath, std::string const& name) - : m_ExceptionExpected(false), + : m_RecordingHolds(0), + m_ExceptionExpected(false), m_LogSuppressed(false), m_TestFailedCheck(false), m_Name(name), @@ -501,6 +797,20 @@ void TTestGroupBase::AssertContainsIC(wchar_t const* text, wchar_t const* substr AssertContainsIC(std::wstring(text), std::wstring(substring), method, line, msg); } //--------------------------------------------------------------------------- +void TTestGroupBase::AssertEmpty(char const* text, std::string const& method, int line, std::string const& msg) +{ + std::string const failure = CStringEmptyFailure(text); + if (!failure.empty()) + throw TExceptEmpty(method, line, failure, msg); +} +//--------------------------------------------------------------------------- +void TTestGroupBase::AssertEmpty(wchar_t const* text, std::string const& method, int line, std::string const& msg) +{ + std::string const failure = CStringEmptyFailure(text); + if (!failure.empty()) + throw TExceptEmpty(method, line, failure, msg); +} +//--------------------------------------------------------------------------- void TTestGroupBase::AssertEndsWith(std::string const& text, std::string const& suffix, std::string const& method, int line, std::string const& msg) { @@ -726,30 +1036,38 @@ void TTestGroupBase::AssertMatches(std::wstring const& text, std::wstring const& throw TExceptMatches(method, line, failure, msg); } //--------------------------------------------------------------------------- +void TTestGroupBase::AssertMatches(char const* text, char const* pattern, std::string const& method, int line, + std::string const& msg) +{ + if (text == nullptr || pattern == nullptr) + throw TExceptMatches(method, line, NullRegexMatchFailure(text, pattern), msg); + + AssertMatches(std::string(text), std::string(pattern), method, line, msg); +} +//--------------------------------------------------------------------------- +void TTestGroupBase::AssertMatches(wchar_t const* text, wchar_t const* pattern, std::string const& method, int line, + std::string const& msg) +{ + if (text == nullptr || pattern == nullptr) + throw TExceptMatches(method, line, NullRegexMatchFailure(text, pattern), msg); + + AssertMatches(std::wstring(text), std::wstring(pattern), method, line, msg); +} +//--------------------------------------------------------------------------- void TTestGroupBase::AssertNear( float expected, float actual, float tolerance, std::string const& method, int line, std::string const& msg) { - float const diff = NearDifference(expected, actual); - - if (!(diff <= tolerance)) - { - std::string expectedStr = std::to_string(expected) + " (tolerance " + std::to_string(tolerance) + ")"; - std::string actualStr = std::to_string(actual) + " (diff " + std::to_string(diff) + ")"; - throw TExceptEquals(method, line, expectedStr, actualStr, msg); - } + std::optional const failure = NearFailure(expected, actual, tolerance, true); + if (failure != std::nullopt) + ReportNearFailure(*failure, ENearMethod::AssertNear, method, line, msg); } //--------------------------------------------------------------------------- void TTestGroupBase::AssertNear( double expected, double actual, double tolerance, std::string const& method, int line, std::string const& msg) { - double const diff = NearDifference(expected, actual); - - if (!(diff <= tolerance)) - { - std::string expectedStr = std::to_string(expected) + " (tolerance " + std::to_string(tolerance) + ")"; - std::string actualStr = std::to_string(actual) + " (diff " + std::to_string(diff) + ")"; - throw TExceptEquals(method, line, expectedStr, actualStr, msg); - } + std::optional const failure = NearFailure(expected, actual, tolerance, true); + if (failure != std::nullopt) + ReportNearFailure(*failure, ENearMethod::AssertNear, method, line, msg); } //--------------------------------------------------------------------------- void TTestGroupBase::AssertNotContains(std::string const& text, std::string const& substring, @@ -816,6 +1134,20 @@ void TTestGroupBase::AssertNotContainsIC(wchar_t const* text, wchar_t const* sub AssertNotContainsIC(std::wstring(text), std::wstring(substring), method, line, msg); } //--------------------------------------------------------------------------- +void TTestGroupBase::AssertNotEmpty(char const* text, std::string const& method, int line, std::string const& msg) +{ + std::string const failure = CStringNotEmptyFailure(text); + if (!failure.empty()) + throw TExceptNotEmpty(method, line, failure, msg); +} +//--------------------------------------------------------------------------- +void TTestGroupBase::AssertNotEmpty(wchar_t const* text, std::string const& method, int line, std::string const& msg) +{ + std::string const failure = CStringNotEmptyFailure(text); + if (!failure.empty()) + throw TExceptNotEmpty(method, line, failure, msg); +} +//--------------------------------------------------------------------------- void TTestGroupBase::AssertNotEndsWith(std::string const& text, std::string const& suffix, std::string const& method, int line, std::string const& msg) { @@ -1046,30 +1378,38 @@ void TTestGroupBase::AssertNotMatches(std::wstring const& text, std::wstring con throw TExceptNotMatches(method, line, failure, msg); } //--------------------------------------------------------------------------- +void TTestGroupBase::AssertNotMatches(char const* text, char const* pattern, std::string const& method, int line, + std::string const& msg) +{ + if (text == nullptr || pattern == nullptr) // A null C string never matches, so it never fails. + return; + + AssertNotMatches(std::string(text), std::string(pattern), method, line, msg); +} +//--------------------------------------------------------------------------- +void TTestGroupBase::AssertNotMatches(wchar_t const* text, wchar_t const* pattern, std::string const& method, int line, + std::string const& msg) +{ + if (text == nullptr || pattern == nullptr) // A null C string never matches, so it never fails. + return; + + AssertNotMatches(std::wstring(text), std::wstring(pattern), method, line, msg); +} +//--------------------------------------------------------------------------- void TTestGroupBase::AssertNotNear( float expected, float actual, float tolerance, std::string const& method, int line, std::string const& msg) { - float const diff = NearDifference(expected, actual); - - if (diff <= tolerance) - { - std::string valueStr = std::to_string(actual) + " (diff " + std::to_string(diff) + " <= tolerance " + - std::to_string(tolerance) + ")"; - throw TExceptNotEquals(method, line, valueStr, msg); - } + std::optional const failure = NearFailure(expected, actual, tolerance, false); + if (failure != std::nullopt) + ReportNearFailure(*failure, ENearMethod::AssertNotNear, method, line, msg); } //--------------------------------------------------------------------------- void TTestGroupBase::AssertNotNear( double expected, double actual, double tolerance, std::string const& method, int line, std::string const& msg) { - double const diff = NearDifference(expected, actual); - - if (diff <= tolerance) - { - std::string valueStr = std::to_string(actual) + " (diff " + std::to_string(diff) + " <= tolerance " + - std::to_string(tolerance) + ")"; - throw TExceptNotEquals(method, line, valueStr, msg); - } + std::optional const failure = NearFailure(expected, actual, tolerance, false); + if (failure != std::nullopt) + ReportNearFailure(*failure, ENearMethod::AssertNotNear, method, line, msg); } //--------------------------------------------------------------------------- void TTestGroupBase::AssertNotStartsWith(std::string const& text, std::string const& prefix, @@ -1206,15 +1546,15 @@ void TTestGroupBase::AssertTrue(bool testVal, std::string const& method, int lin throw TExceptTrue(method, line, msg); } //--------------------------------------------------------------------------- -bool TTestGroupBase::BeginRecording(std::unique_lock& lock) +bool TTestGroupBase::BeginRecording(TRecordingLock& lock) { - if (!lock.owns_lock()) - lock = std::unique_lock(m_RecordingMutex); + if (!lock.OwnsLock()) + lock.Lock(); return t_WorkerState == nullptr || t_WorkerState->Group != this || !t_WorkerState->Abandoned; } //--------------------------------------------------------------------------- -bool TTestGroupBase::BeginRecordingOutcome(std::unique_lock& lock) +bool TTestGroupBase::BeginRecordingOutcome(TRecordingLock& lock) { if (!BeginRecording(lock)) return false; @@ -1310,6 +1650,20 @@ void TTestGroupBase::CheckContainsIC(wchar_t const* text, wchar_t const* substri CheckContainsIC(std::wstring(text), std::wstring(substring), method, line, msg); } //--------------------------------------------------------------------------- +void TTestGroupBase::CheckEmpty(char const* text, std::string const& method, int line, std::string const& msg) +{ + std::string const failure = CStringEmptyFailure(text); + if (!failure.empty()) + SetTestFailedCheck(method, line, failure + ". " + msg); +} +//--------------------------------------------------------------------------- +void TTestGroupBase::CheckEmpty(wchar_t const* text, std::string const& method, int line, std::string const& msg) +{ + std::string const failure = CStringEmptyFailure(text); + if (!failure.empty()) + SetTestFailedCheck(method, line, failure + ". " + msg); +} +//--------------------------------------------------------------------------- void TTestGroupBase::CheckEndsWith(std::string const& text, std::string const& suffix, std::string const& method, int line, std::string const& msg) { @@ -1577,30 +1931,44 @@ void TTestGroupBase::CheckMatches(std::wstring const& text, std::wstring const& SetTestFailedCheck(method, line, failure + ". " + msg); } //--------------------------------------------------------------------------- -void TTestGroupBase::CheckNear( - float expected, float actual, float tolerance, std::string const& method, int line, std::string const& msg) +void TTestGroupBase::CheckMatches(char const* text, char const* pattern, std::string const& method, int line, + std::string const& msg) { - float const diff = NearDifference(expected, actual); + if (text == nullptr || pattern == nullptr) + { + SetTestFailedCheck(method, line, NullRegexMatchFailure(text, pattern) + ". " + msg); + return; + } - if (!(diff <= tolerance)) + CheckMatches(std::string(text), std::string(pattern), method, line, msg); +} +//--------------------------------------------------------------------------- +void TTestGroupBase::CheckMatches(wchar_t const* text, wchar_t const* pattern, std::string const& method, int line, + std::string const& msg) +{ + if (text == nullptr || pattern == nullptr) { - std::string expectedStr = std::to_string(expected) + " (tolerance " + std::to_string(tolerance) + ")"; - std::string actualStr = std::to_string(actual) + " (diff " + std::to_string(diff) + ")"; - SetTestFailedCheck(method, line, expectedStr, actualStr, msg); + SetTestFailedCheck(method, line, NullRegexMatchFailure(text, pattern) + ". " + msg); + return; } + + CheckMatches(std::wstring(text), std::wstring(pattern), method, line, msg); +} +//--------------------------------------------------------------------------- +void TTestGroupBase::CheckNear( + float expected, float actual, float tolerance, std::string const& method, int line, std::string const& msg) +{ + std::optional const failure = NearFailure(expected, actual, tolerance, true); + if (failure != std::nullopt) + ReportNearFailure(*failure, ENearMethod::CheckNear, method, line, msg); } //--------------------------------------------------------------------------- void TTestGroupBase::CheckNear( double expected, double actual, double tolerance, std::string const& method, int line, std::string const& msg) { - double const diff = NearDifference(expected, actual); - - if (!(diff <= tolerance)) - { - std::string expectedStr = std::to_string(expected) + " (tolerance " + std::to_string(tolerance) + ")"; - std::string actualStr = std::to_string(actual) + " (diff " + std::to_string(diff) + ")"; - SetTestFailedCheck(method, line, expectedStr, actualStr, msg); - } + std::optional const failure = NearFailure(expected, actual, tolerance, true); + if (failure != std::nullopt) + ReportNearFailure(*failure, ENearMethod::CheckNear, method, line, msg); } //--------------------------------------------------------------------------- void TTestGroupBase::CheckNotContains(std::string const& text, std::string const& substring, @@ -1676,6 +2044,20 @@ void TTestGroupBase::CheckNotContainsIC(wchar_t const* text, wchar_t const* subs CheckNotContainsIC(std::wstring(text), std::wstring(substring), method, line, msg); } //--------------------------------------------------------------------------- +void TTestGroupBase::CheckNotEmpty(char const* text, std::string const& method, int line, std::string const& msg) +{ + std::string const failure = CStringNotEmptyFailure(text); + if (!failure.empty()) + SetTestFailedCheck(method, line, failure + ". " + msg); +} +//--------------------------------------------------------------------------- +void TTestGroupBase::CheckNotEmpty(wchar_t const* text, std::string const& method, int line, std::string const& msg) +{ + std::string const failure = CStringNotEmptyFailure(text); + if (!failure.empty()) + SetTestFailedCheck(method, line, failure + ". " + msg); +} +//--------------------------------------------------------------------------- void TTestGroupBase::CheckNotEndsWith(std::string const& text, std::string const& suffix, std::string const& method, int line, std::string const& msg) { @@ -1933,30 +2315,38 @@ void TTestGroupBase::CheckNotMatches(std::wstring const& text, std::wstring cons SetTestFailedCheck(method, line, failure + ". " + msg); } //--------------------------------------------------------------------------- +void TTestGroupBase::CheckNotMatches(char const* text, char const* pattern, std::string const& method, int line, + std::string const& msg) +{ + if (text == nullptr || pattern == nullptr) // A null C string never matches, so it never fails. + return; + + CheckNotMatches(std::string(text), std::string(pattern), method, line, msg); +} +//--------------------------------------------------------------------------- +void TTestGroupBase::CheckNotMatches(wchar_t const* text, wchar_t const* pattern, std::string const& method, int line, + std::string const& msg) +{ + if (text == nullptr || pattern == nullptr) // A null C string never matches, so it never fails. + return; + + CheckNotMatches(std::wstring(text), std::wstring(pattern), method, line, msg); +} +//--------------------------------------------------------------------------- void TTestGroupBase::CheckNotNear( float expected, float actual, float tolerance, std::string const& method, int line, std::string const& msg) { - float const diff = NearDifference(expected, actual); - - if (diff <= tolerance) - { - std::string valueStr = std::to_string(actual) + " (diff " + std::to_string(diff) + " <= tolerance " + - std::to_string(tolerance) + ")"; - SetTestFailedCheckNotEquals(method, line, valueStr, msg); - } + std::optional const failure = NearFailure(expected, actual, tolerance, false); + if (failure != std::nullopt) + ReportNearFailure(*failure, ENearMethod::CheckNotNear, method, line, msg); } //--------------------------------------------------------------------------- void TTestGroupBase::CheckNotNear( double expected, double actual, double tolerance, std::string const& method, int line, std::string const& msg) { - double const diff = NearDifference(expected, actual); - - if (diff <= tolerance) - { - std::string valueStr = std::to_string(actual) + " (diff " + std::to_string(diff) + " <= tolerance " + - std::to_string(tolerance) + ")"; - SetTestFailedCheckNotEquals(method, line, valueStr, msg); - } + std::optional const failure = NearFailure(expected, actual, tolerance, false); + if (failure != std::nullopt) + ReportNearFailure(*failure, ENearMethod::CheckNotNear, method, line, msg); } //--------------------------------------------------------------------------- void TTestGroupBase::CheckNotStartsWith(std::string const& text, std::string const& prefix, @@ -2148,6 +2538,15 @@ std::string TTestGroupBase::DescribeException(System::Sysutils::Exception& ex) } //--------------------------------------------------------------------------- #endif +std::string TTestGroupBase::DescribeUnknownException() +{ +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) + return "an object that is neither a std::exception nor an RTL Exception"; +#else + return "a non-std::exception object"; +#endif +} +//--------------------------------------------------------------------------- std::string TTestGroupBase::ExceptionMessage(std::exception const& ex) { return ex.what(); @@ -2167,7 +2566,7 @@ bool TTestGroupBase::ExceptionTypeExpected() const return true; #endif - return m_ExpectedExceptionTypeChecker != nullptr; + return (m_ExpectedExceptionTypeChecker != nullptr) || (m_ExpectedOtherExceptionTypeChecker != nullptr); } //--------------------------------------------------------------------------- void TTestGroupBase::Fail(std::string const& method, int line, std::string const& msg) @@ -2217,7 +2616,7 @@ void TTestGroupBase::Log(std::string const& msg) if (m_LogSuppressed) return; - std::unique_lock lock; + TRecordingLock lock(*this); if (!BeginRecording(lock)) return; @@ -2235,7 +2634,7 @@ void TTestGroupBase::LogAppend(std::string const& msg) if (m_LogSuppressed) return; - std::unique_lock lock; + TRecordingLock lock(*this); if (!BeginRecording(lock)) return; @@ -2248,6 +2647,67 @@ void TTestGroupBase::LogAppend(std::string const& msg) std::cout << msg; } //--------------------------------------------------------------------------- +template +std::optional TTestGroupBase::NearFailure(TFloat expected, TFloat actual, + TFloat tolerance, bool expectNear) +{ + TFloat const diff = NearDifference(expected, actual); + bool const invalidTolerance = !(tolerance >= 0); // Negative or NaN. + + if (!invalidTolerance && (diff <= tolerance) == expectNear) + return std::nullopt; + + TNearFailure failure; + failure.InvalidTolerance = invalidTolerance; + failure.Expected = std::to_string(expected); + failure.Actual = std::to_string(actual); + failure.Difference = std::to_string(diff); + failure.Tolerance = std::to_string(tolerance); + return failure; +} +//--------------------------------------------------------------------------- +template std::optional TTestGroupBase::NearFailure(float, float, float, + bool); +template std::optional TTestGroupBase::NearFailure(double, double, double, + bool); +template std::optional TTestGroupBase::NearFailure(long double, + long double, long double, bool); +//--------------------------------------------------------------------------- +std::optional TTestGroupBase::NearFailure(TNearInteger expected, + TNearInteger actual, TNearInteger tolerance, bool expectNear) +{ + // With the same sign, the difference is that of the magnitudes; otherwise their sum, which can exceed + // UINT64_MAX (one value negative, the other above INT64_MAX) and is then near nothing. + bool const sameSign = (expected.Negative == actual.Negative); + bool const sumOverflows = !sameSign && + (expected.Magnitude > std::numeric_limits::max() - actual.Magnitude); + uint64_t diff = expected.Magnitude + actual.Magnitude; + + if (sameSign) + { + diff = (expected.Magnitude >= actual.Magnitude) ? expected.Magnitude - actual.Magnitude : + actual.Magnitude - expected.Magnitude; + } + + bool const isNear = !sumOverflows && (diff <= tolerance.Magnitude); + + if (!tolerance.Negative && isNear == expectNear) + return std::nullopt; + + auto const text = [](TNearInteger value) + { + return std::string(value.Negative ? "-" : "") + std::to_string(value.Magnitude); + }; + + TNearFailure failure; + failure.InvalidTolerance = tolerance.Negative; + failure.Expected = text(expected); + failure.Actual = text(actual); + failure.Difference = sumOverflows ? DecimalSum(expected.Magnitude, actual.Magnitude) : std::to_string(diff); + failure.Tolerance = text(tolerance); + return failure; +} +//--------------------------------------------------------------------------- bool TTestGroupBase::OrderingHolds(std::optional order, TOrdering ordering) { if (!order.has_value()) @@ -2257,11 +2717,15 @@ bool TTestGroupBase::OrderingHolds(std::optional order, TOrdering ordering) { case TOrdering::GreaterThan: return *order > 0; + case TOrdering::GreaterThanOrEqual: return *order >= 0; + case TOrdering::LessThan: return *order < 0; + case TOrdering::LessThanOrEqual: + default: return *order <= 0; } @@ -2273,49 +2737,20 @@ char const* TTestGroupBase::OrderingSymbol(TOrdering ordering) { case TOrdering::GreaterThan: return ">"; + case TOrdering::GreaterThanOrEqual: return ">="; + case TOrdering::LessThan: return "<"; + case TOrdering::LessThanOrEqual: + default: return "<="; } } //--------------------------------------------------------------------------- -#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) -std::string TTestGroupBase::RTLTextToUTF8(std::string const& text) -{ - return text; -} -//--------------------------------------------------------------------------- -std::string TTestGroupBase::RTLTextToUTF8(std::wstring const& text) -{ - return WideToUTF8(text); -} -//--------------------------------------------------------------------------- -std::string TTestGroupBase::RTLTextToUTF8(char const* text) -{ - return CStringOrEmpty(text); -} -//--------------------------------------------------------------------------- -std::string TTestGroupBase::RTLTextToUTF8(wchar_t const* text) -{ - return WideToUTF8(CStringOrEmpty(text)); -} -//--------------------------------------------------------------------------- -std::string TTestGroupBase::RTLTextToUTF8(System::String const& text) -{ -#if defined(WIDECHAR_IS_WCHAR) - // The same conversion as wide text, so both give the same bytes even for an unpaired surrogate. - return WideToUTF8(std::wstring(text.c_str(), static_cast(text.Length()))); -#else - System::UTF8String const utf8(text); - return std::string(utf8.c_str(), static_cast(utf8.Length())); -#endif -} -//--------------------------------------------------------------------------- -#endif /* TTestGroupBase::RegisterTest @@ -2343,6 +2778,33 @@ void TTestGroupBase::RegisterTest(ITestCase::TestCallback callback, std::string RegisterTest(testCase); } //--------------------------------------------------------------------------- +/* + TTestGroupBase::ReleaseRecordingHolds + + Called by RunCatchingCrashes() when TCrashGuard::Run() has caught a crash on this thread. The + jump out of a crash skips the destructors of the TRecordingLocks it leaves, so a crash in code + run while m_RecordingMutex is held (a run observer's OnLog(), say) leaves this thread holding it. + That's harmless while one thread runs every test, as the mutex is recursive, but a test's worker + thread (see RunWithTimeout()) would end still holding it, and the next thread to record would + wait for it forever. + + Locks it once more, so that every hold m_RecordingHolds counts is this thread's, then releases + them all. No thread calls RunCatchingCrashes() while holding it, so every hold left is one the + crash skipped. +*/ +void TTestGroupBase::ReleaseRecordingHolds() +{ + m_RecordingMutex.lock(); + + unsigned int const holds = m_RecordingHolds; + m_RecordingHolds = 0; + + for (unsigned int i = 0; i < holds; ++i) + m_RecordingMutex.unlock(); + + m_RecordingMutex.unlock(); +} +//--------------------------------------------------------------------------- /* TTestGroupBase::ReportCrashedTest @@ -2363,7 +2825,7 @@ void TTestGroupBase::ReportCrashedTest(ITestCase& testCase, std::string const& d std::string const plainMsg = tag + ": \"" + testFullName + "\": " + detail; // On a worker thread, a crash that comes after its test timed out is ignored, like the rest of what it does then. - std::unique_lock lock; + TRecordingLock lock(*this); if (!BeginRecordingOutcome(lock)) return; @@ -2384,6 +2846,46 @@ void TTestGroupBase::ReportCrashedTest(ITestCase& testCase, std::string const& d Log("!!CRASH!!: \"" + testFullName + "\" crashed (" + description + "); continuing with the next test."); } //--------------------------------------------------------------------------- +void TTestGroupBase::ReportNearFailure(TNearFailure const& failure, ENearMethod which, std::string const& method, + int line, std::string const& msg) +{ + bool const isAssert = (which == ENearMethod::AssertNear || which == ENearMethod::AssertNotNear); + bool const expectNear = (which == ENearMethod::AssertNear || which == ENearMethod::CheckNear); + + if (failure.InvalidTolerance) + { + std::string const detail = "Invalid tolerance " + failure.Tolerance + ": it must be zero or more. " + msg; + + if (which == ENearMethod::AssertNear) + throw TExceptEquals(method, line, detail); + + if (which == ENearMethod::AssertNotNear) + throw TExceptNotEquals(method, line, detail); + + SetTestFailedCheck(method, line, detail); + } + else if (expectNear) + { + std::string const expectedStr = failure.Expected + " (tolerance " + failure.Tolerance + ")"; + std::string const actualStr = failure.Actual + " (diff " + failure.Difference + ")"; + + if (isAssert) + throw TExceptEquals(method, line, expectedStr, actualStr, msg); + + SetTestFailedCheck(method, line, expectedStr, actualStr, msg); + } + else + { + std::string const valueStr = failure.Actual + " (diff " + failure.Difference + " <= tolerance " + + failure.Tolerance + ")"; + + if (isAssert) + throw TExceptNotEquals(method, line, valueStr, msg); + + SetTestFailedCheckNotEquals(method, line, valueStr, msg); + } +} +//--------------------------------------------------------------------------- /* TTestGroupBase::ReportTimedOutTest @@ -2522,7 +3024,8 @@ void TTestGroupBase::Run(TestFilter const& filter, std::optional s normal completion, including via an ordinary C++ exception, passes through unaffected. A caught crash is handed to ReportCrashedTest(), which either records it as a failure and returns (the common case: the next test still runs) or additionally throws to abort the rest of the run, for - the specific crash types TCrashGuard::Run() considers too severe to continue past. + the specific crash types TCrashGuard::Run() considers too severe to continue past. First, though, + ReleaseRecordingHolds() releases m_RecordingMutex, in case the crash came while it was held. */ void TTestGroupBase::RunCatchingCrashes(ITestCase& testCase, bool catchCrashes) { @@ -2538,7 +3041,10 @@ void TTestGroupBase::RunCatchingCrashes(ITestCase& testCase, bool catchCrashes) }); if (result.Crashed) + { + ReleaseRecordingHolds(); ReportCrashedTest(testCase, result.Description, result.ShouldAbortRun); + } } //--------------------------------------------------------------------------- /* @@ -2552,7 +3058,10 @@ void TTestGroupBase::RunCatchingCrashes(ITestCase& testCase, bool catchCrashes) data race with the waiting thread here. Once the test has been abandoned (below), the worker records nothing more and logs nothing more, should it end after all (see BeginRecording()); a test that had begun recording its outcome when the timeout came counts as finished in time - instead. If it finishes in time, whatever it threw (if anything) + instead. The worker starts with the calling thread's floating-point environment (std::fesetenv()), + as the test would have on that thread: on Windows a new thread otherwise starts with the defaults, + such as rounding to nearest and, with llvm-mingw, x87 long double arithmetic at double precision. + If it finishes in time, whatever it threw (if anything) is rethrown by future.get(), preserving normal exception propagation exactly as if it had run directly. The one exception, with ASWUNITTESTS_RTL_EXCEPTIONS enabled: an RTL exception arrives as a TExceptRTLException carrying its description and class (see the worker's catch). If it @@ -2576,9 +3085,12 @@ void TTestGroupBase::RunWithTimeout(ITestCase& testCase, std::optional const state = std::make_shared(); state->Group = this; + std::fenv_t environment = {}; + std::fegetenv(&environment); - std::thread worker([this, &testCase, done, state, catchCrashes]() + std::thread worker([this, &testCase, done, state, catchCrashes, environment]() { + std::fesetenv(&environment); t_WorkerState = state.get(); try @@ -2605,11 +3117,12 @@ void TTestGroupBase::RunWithTimeout(ITestCase& testCase, std::optional lock(m_RecordingMutex); + TRecordingLock lock(*this); + lock.Lock(); if (!state->RecordingOutcome) { state->Abandoned = true; - lock.unlock(); + lock.Unlock(); worker.detach(); ReportTimedOutTest(testCase, *testTimeoutSeconds); @@ -2631,6 +3144,7 @@ void TTestGroupBase::SetExceptionExpected(bool expected, std::string const& meth m_ExceptionExpectedText = method + " (" + std::to_string(line) + "): " + msg; m_ExpectedExceptionMessage.clear(); m_ExpectedExceptionTypeChecker = nullptr; + m_ExpectedOtherExceptionTypeChecker = nullptr; #if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) m_ExpectedRTLExceptionTypeChecker = nullptr; #endif @@ -2654,7 +3168,7 @@ void TTestGroupBase::SetSuitePath(std::string const& suitePath) //--------------------------------------------------------------------------- void TTestGroupBase::SetTestFailedCheck(std::string const& method, int line, std::string const& msg) { - std::unique_lock lock; + TRecordingLock lock(*this); if (!BeginRecording(lock)) return; @@ -2778,8 +3292,9 @@ void TTestGroupBase::Test(ITestCase& testCase) }; // Scores an exception caught while one was expected. 'typeMatches' is whether it satisfies the requested - // type (true when no specific type was requested), 'message' is the text checked for the expected substring, - // and 'description' is how the exception is shown in a failure detail. + // type (true when no specific type was requested), 'message' is the text checked for the expected substring + // (nullopt for an exception that has none, which fails a message check), and 'description' is how the + // exception is shown in a failure detail. // Records a pass, unless a Check* failure earlier in the test means it failed after all, even if everything // after that (including an expected exception arriving) went as intended. Every path that would otherwise // pass the test goes through here, so none of them can overlook an earlier Check* failure. @@ -2797,10 +3312,11 @@ void TTestGroupBase::Test(ITestCase& testCase) finish(TTestOutcome::Pass, "passed", std::string()); }; - auto finishExpectedException = [&](bool typeMatches, std::string const& message, std::string const& description) + auto finishExpectedException = [&](bool typeMatches, std::optional const& message, + std::string const& description) { bool const messageMatches = m_ExpectedExceptionMessage.empty() || - (message.find(m_ExpectedExceptionMessage) != std::string::npos); + ((message != std::nullopt) && (message->find(m_ExpectedExceptionMessage) != std::string::npos)); if (typeMatches && messageMatches) { @@ -2813,6 +3329,9 @@ void TTestGroupBase::Test(ITestCase& testCase) std::string detail; if (!typeMatches) detail = "expected exception type was not thrown (caught a different exception): " + description; + else if (message == std::nullopt) + detail = "expected exception message to contain \"" + m_ExpectedExceptionMessage + + "\" but caught an exception without a message: " + description; else detail = "expected exception message to contain \"" + m_ExpectedExceptionMessage + "\" but caught: " + description; @@ -2828,7 +3347,7 @@ void TTestGroupBase::Test(ITestCase& testCase) // Held from when the test, its SetUp_Test() and its TearDown_Test() are over until Test() returns or throws, while // the outcome is recorded. On a worker thread whose test has timed out (see RunWithTimeout()), nothing is recorded: // Test() just returns, even for a fatal exception. - std::unique_lock outcomeLock; + TRecordingLock outcomeLock(*this); auto tearDown = [&]() { @@ -2921,7 +3440,8 @@ void TTestGroupBase::Test(ITestCase& testCase) { // A type/message check is only requested via the templated SetExceptionExpected() // overload; the plain bool overload leaves both null/empty, matching any exception (legacy behavior). - // A requested RTL exception type (see ExceptionTypeExpected()) never matches a std::exception. + // A requested RTL exception type or non-class type (see ExceptionTypeExpected()) never matches a + // std::exception. bool const typeMatches = (m_ExpectedExceptionTypeChecker != nullptr) ? m_ExpectedExceptionTypeChecker(ex) : !ExceptionTypeExpected(); finishExpectedException(typeMatches, ex.what(), ex.what()); @@ -2969,16 +3489,16 @@ void TTestGroupBase::Test(ITestCase& testCase) { if (ExceptionTypeExpected()) { - // A specific type was requested, but what was thrown isn't an exception class this framework - // can inspect to confirm the type (or message) matched. Treat that as a failure, not a pass. - m_Results.FailedCount++; -#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) - finish(TTestOutcome::Fail, "failed", "expected a specific exception type, but an object that is " - "neither a std::exception nor an RTL Exception was thrown instead."); -#else - finish(TTestOutcome::Fail, "failed", - "expected a specific exception type, but a non-std::exception object was thrown instead."); -#endif + // Only a requested type not derived from std::exception can match here, and it has no message. A + // requested std::exception or RTL type never matches. + std::optional caught; + if (m_ExpectedOtherExceptionTypeChecker != nullptr) + caught = m_ExpectedOtherExceptionTypeChecker(); + + if (caught != std::nullopt) + finishExpectedException(true, std::nullopt, *caught); + else + finishExpectedException(false, std::nullopt, DescribeUnknownException()); } else { @@ -2993,12 +3513,7 @@ void TTestGroupBase::Test(ITestCase& testCase) throw; // Fatal // The same description CallAndDescribeException() gives it. -#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) - finish(TTestOutcome::Fail, "failed", - "Unexpected exception: an object that is neither a std::exception nor an RTL Exception"); -#else - finish(TTestOutcome::Fail, "failed", "Unexpected exception: a non-std::exception object"); -#endif + finish(TTestOutcome::Fail, "failed", "Unexpected exception: " + DescribeUnknownException()); } } } @@ -3008,6 +3523,70 @@ bool TTestGroupBase::TestFailedOneOrMoreChecks() return m_TestFailedCheck; } //--------------------------------------------------------------------------- +std::string TTestGroupBase::TextToUTF8(std::string const& text) +{ + return text; +} +//--------------------------------------------------------------------------- +std::string TTestGroupBase::TextToUTF8(std::wstring const& text) +{ + return WideToUTF8(text); +} +//--------------------------------------------------------------------------- +std::string TTestGroupBase::TextToUTF8(char const* text) +{ + return CStringOrEmpty(text); +} +//--------------------------------------------------------------------------- +std::string TTestGroupBase::TextToUTF8(wchar_t const* text) +{ + return WideToUTF8(CStringOrEmpty(text)); +} +//--------------------------------------------------------------------------- +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) +std::string TTestGroupBase::TextToUTF8(System::String const& text) +{ +#if defined(WIDECHAR_IS_WCHAR) + // The same conversion as wide text, so both give the same bytes even for an unpaired surrogate. + return WideToUTF8(TextToWide(text)); +#else + System::UTF8String const utf8(text); + return std::string(utf8.c_str(), static_cast(utf8.Length())); +#endif +} +#endif +//--------------------------------------------------------------------------- +std::wstring TTestGroupBase::TextToWide(std::string const& text) +{ + return UTF8ToWide(text); +} +//--------------------------------------------------------------------------- +std::wstring TTestGroupBase::TextToWide(std::wstring const& text) +{ + return text; +} +//--------------------------------------------------------------------------- +std::wstring TTestGroupBase::TextToWide(char const* text) +{ + return UTF8ToWide(CStringOrEmpty(text)); +} +//--------------------------------------------------------------------------- +std::wstring TTestGroupBase::TextToWide(wchar_t const* text) +{ + return CStringOrEmpty(text); +} +//--------------------------------------------------------------------------- +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) +std::wstring TTestGroupBase::TextToWide(System::String const& text) +{ +#if defined(WIDECHAR_IS_WCHAR) + return std::wstring(text.c_str(), static_cast(text.Length())); +#else + return UTF8ToWide(TextToUTF8(text)); +#endif +} +#endif +//--------------------------------------------------------------------------- std::string TTestGroupBase::TypeName(std::type_info const& type) { #if defined(ASWUNITTESTS_CXXABI_DEMANGLE_ENABLED) @@ -3021,7 +3600,49 @@ std::string TTestGroupBase::TypeName(std::type_info const& type) } #endif +#if defined(__BORLANDC__) && !defined(_WIN64) + return BorlandTypeName(type.name()); +#else return type.name(); +#endif +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TTestGroupBase::TRecordingLock +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TTestGroupBase::TRecordingLock::TRecordingLock(TTestGroupBase& group) + : m_Group(group), + m_Locked(false) +{ +} +//--------------------------------------------------------------------------- +TTestGroupBase::TRecordingLock::~TRecordingLock() +{ + if (m_Locked) + Unlock(); +} +//--------------------------------------------------------------------------- +void TTestGroupBase::TRecordingLock::Lock() +{ + m_Group.m_RecordingMutex.lock(); + ++m_Group.m_RecordingHolds; + m_Locked = true; +} +//--------------------------------------------------------------------------- +bool TTestGroupBase::TRecordingLock::OwnsLock() const +{ + return m_Locked; +} +//--------------------------------------------------------------------------- +void TTestGroupBase::TRecordingLock::Unlock() +{ + m_Locked = false; + --m_Group.m_RecordingHolds; + m_Group.m_RecordingMutex.unlock(); } //--------------------------------------------------------------------------- diff --git a/src/ASWUnitTests_TestBase.h b/src/ASWUnitTests_TestBase.h index c21ce7f..24d7f3f 100644 --- a/src/ASWUnitTests_TestBase.h +++ b/src/ASWUnitTests_TestBase.h @@ -70,7 +70,7 @@ enum class TTestOutcome { Pass, Fail, - Skip + Skip, }; @@ -312,6 +312,7 @@ class TTestGroupBase : public ITestGroup friend class TTestHandler; // For SetSuitePath() +private: // The relation an ordering method (e.g. CheckGreaterThan()) checks 'value' has to 'bound'. enum class TOrdering { @@ -321,6 +322,34 @@ class TTestGroupBase : public ITestGroup LessThanOrEqual }; + // The Near or NotNear method a failure is reported for (see ReportNearFailure()). + enum class ENearMethod + { + AssertNear, + AssertNotNear, + CheckNear, + CheckNotNear + }; + + // Why a Near or NotNear method failed (see NearFailure()): an invalid tolerance (negative or NaN), or the + // values' texts, as std::to_string() shows them. + struct TNearFailure + { + bool InvalidTolerance = false; + std::string Expected; + std::string Actual; + std::string Difference; + std::string Tolerance; + }; + + // An integer for the Near methods, as its sign and magnitude, so two integers of any types can be subtracted + // exactly (see ToNearInteger()). + struct TNearInteger + { + bool Negative = false; + uint64_t Magnitude = 0; + }; + // True when TValue has a get() member returning a pointer, like std::unique_ptr and std::shared_ptr. template struct THasPointerGet : std::false_type @@ -342,6 +371,64 @@ class TTestGroupBase : public ITestGroup { }; + // The kind of text a string check's argument is (see TextKindOf()), which decides how it's compared with another. + enum class ETextKind + { + None, // Not text. + NarrowString, // std::string, as UTF-8. + NarrowCString, // char const*, char* or a char array, as UTF-8. + WideString, // std::wstring. + WideCString, // wchar_t const*, wchar_t* or a wchar_t array. + RTL // System::String, or an AnsiStringT such as AnsiString or UTF8String, in RTL builds. + }; + + // True for an AnsiStringT, such as AnsiString or UTF8String, in RTL builds. + template + struct TIsAnsiStringT : std::false_type + { + }; +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) + template + struct TIsAnsiStringT > : std::true_type + { + }; +#endif + + // A string check's std::string, char const*, std::wstring and wchar_t const* overloads, which ForwardAsUTF8() + // and ForwardAsWide() forward to. + using TUTF8TextCheck = void (TTestGroupBase::*)(std::string const&, std::string const&, std::string const&, int, + std::string const&); + using TUTF8CStringCheck = void (TTestGroupBase::*)(char const*, char const*, std::string const&, int, + std::string const&); + using TWideTextCheck = void (TTestGroupBase::*)(std::wstring const&, std::wstring const&, std::string const&, int, + std::string const&); + using TWideCStringCheck = void (TTestGroupBase::*)(wchar_t const*, wchar_t const*, std::string const&, int, + std::string const&); + + // A lock on a group's m_RecordingMutex, like a std::unique_lock, that also counts its holds in m_RecordingHolds, + // so ReleaseRecordingHolds() can release the ones a crash left behind. + class TRecordingLock + { + private: + TTestGroupBase& m_Group; + bool m_Locked; + + public: + explicit TRecordingLock(TTestGroupBase& group); + TRecordingLock(TRecordingLock const&) = delete; + TRecordingLock& operator=(TRecordingLock const&) = delete; + ~TRecordingLock(); + + void Lock(); + bool OwnsLock() const; + void Unlock(); + }; + +private: + // How many times m_RecordingMutex's owner holds it through TRecordingLocks. Only read or changed while holding it. + unsigned int m_RecordingHolds; + +private: // Used by the Assert ordering methods: throws TExceptOrdering unless 'value' has relation 'ordering' to 'bound'. template void AssertOrdering(TValue value, TBound bound, TOrdering ordering, std::string const& method, int line, @@ -354,13 +441,13 @@ class TTestGroupBase : public ITestGroup } } - // Locks m_RecordingMutex in 'lock', for a change to this group's results or log, and returns true, unless the - // calling thread runs a test of this group that timed out and was abandoned (see RunWithTimeout()), which must - // leave both alone; it then returns false, with 'lock' still locked. - bool BeginRecording(std::unique_lock& lock); + // Locks 'lock', for a change to this group's results or log, and returns true, unless the calling thread runs a + // test of this group that timed out and was abandoned (see RunWithTimeout()), which must leave both alone; it + // then returns false, with 'lock' still locked. + bool BeginRecording(TRecordingLock& lock); // The same, for recording a test's outcome, after which a test running on a worker thread counts as finished in // time, however long its thread then takes to end. - bool BeginRecordingOutcome(std::unique_lock& lock); + bool BeginRecordingOutcome(TRecordingLock& lock); // Runs 'callable' for the Throws/NoThrow methods and returns a description of what it threw (see // DescribeException()), or nullopt when it threw nothing. A failure signal from this framework (a TTestException, @@ -387,16 +474,11 @@ class TTestGroupBase : public ITestGroup { return DescribeException(ex); } +#endif catch (...) { - return std::string("an object that is neither a std::exception nor an RTL Exception"); - } -#else - catch (...) - { - return std::string("a non-std::exception object"); + return DescribeUnknownException(); } -#endif return std::nullopt; } @@ -478,6 +560,18 @@ class TTestGroupBase : public ITestGroup static std::string DescribeException(System::Sysutils::Exception& ex); #endif + // Describes 'ex', caught as a TException that is neither a std::exception nor an RTL exception, and so has no + // message, by its type (its dynamic type, for a polymorphic class), e.g. "an object of type \"int\"". + template + static std::string DescribeOtherException(TException const& ex) + { + return "an object of type \"" + TypeName(typeid(ex)) + "\""; + } + + // Describes an exception that is neither a std::exception nor (in RTL builds) an RTL exception, whose type isn't + // known, e.g. "a non-std::exception object". + static std::string DescribeUnknownException(); + // The message a Throws method checks for its expected substring: what(), or an RTL exception's Message. static std::string ExceptionMessage(std::exception const& ex); #if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) @@ -537,6 +631,46 @@ class TTestGroupBase : public ITestGroup } } + // Forwards a string check's two texts, of any text types (see TextKindOf()), to its std::string overload 'check' + // as UTF-8, or, when either is a null C string, to its char const* overload 'cStringCheck' with that one null, + // which applies the null C string rules. Used by the templates for mixed string types. The texts are taken by + // value, so an array arrives as a pointer, and one instantiation serves every length of literal. + template + void ForwardAsUTF8(TFirst first, TSecond second, TUTF8TextCheck check, TUTF8CStringCheck cStringCheck, + std::string const& method, int line, std::string const& msg) + { + std::string const firstText = TextToUTF8(first); + std::string const secondText = TextToUTF8(second); + + if (IsNullText(first) || IsNullText(second)) + { + (this->*cStringCheck)(IsNullText(first) ? nullptr : firstText.c_str(), + IsNullText(second) ? nullptr : secondText.c_str(), method, line, msg); + } + else + { + (this->*check)(firstText, secondText, method, line, msg); + } + } + // The same, as wide text, for two wide texts, and for the Matches methods unless both texts are narrow. + template + void ForwardAsWide(TFirst first, TSecond second, TWideTextCheck check, TWideCStringCheck cStringCheck, + std::string const& method, int line, std::string const& msg) + { + std::wstring const firstText = TextToWide(first); + std::wstring const secondText = TextToWide(second); + + if (IsNullText(first) || IsNullText(second)) + { + (this->*cStringCheck)(IsNullText(first) ? nullptr : firstText.c_str(), + IsNullText(second) ? nullptr : secondText.c_str(), method, line, msg); + } + else + { + (this->*check)(firstText, secondText, method, line, msg); + } + } + // Calls 'compare' with 'expected' and 'actual' as two int64_t when both fit, otherwise as two uint64_t when // neither is negative, otherwise (one negative, the other above INT64_MAX) as two decimal strings, so neither // value wraps around. Used by the integer template overloads of the Equals/NotEquals methods. @@ -551,6 +685,22 @@ class TTestGroupBase : public ITestGroup compare(std::to_string(expected), std::to_string(actual)); } + // True when TException is a type the Throws methods and SetExceptionExpected() can expect: any type + // that can be thrown and caught by reference (an object type, but not an array). In RTL builds, an RTL class + // (a System::TObject descendant) must be an Exception or a subclass. + template + static constexpr bool IsExpectableException() + { + if constexpr (!std::is_object::value || std::is_array::value) + return false; +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) + else if constexpr (std::is_base_of::value) + return std::is_base_of::value; +#endif + else + return true; + } + template static bool IsNonNegative(TInteger value) { @@ -560,6 +710,27 @@ class TTestGroupBase : public ITestGroup return true; } + // True when 'text' is a null C string. A char or wchar_t array never is. + template + static bool IsNullText(TText const& text) + { + static_cast(text); + + if constexpr (std::is_pointer::value) + return text == nullptr; + else + return false; + } + + // Why a Near method (or with 'expectNear' false, a NotNear method) fails for these values, or nullopt when it + // passes. A negative or NaN tolerance fails both. Two values are near when they're equal (so equal infinities + // are) or differ by at most 'tolerance'; a NaN is near nothing. The template is defined in the .cpp, for float, + // double and long double. + template + static std::optional NearFailure(TFloat expected, TFloat actual, TFloat tolerance, bool expectNear); + static std::optional NearFailure(TNearInteger expected, TNearInteger actual, TNearInteger tolerance, + bool expectNear); + // Runs 'callable' for the NoThrow methods (e.g. CheckNoThrow()) and returns why it failed, or an empty string when // it threw nothing. A failure signal from this framework (e.g. an Assert* failure inside 'callable') passes // straight through, failing the test as usual. @@ -578,15 +749,13 @@ class TTestGroupBase : public ITestGroup // The operator for 'ordering' in a failure message, e.g. ">=". static char const* OrderingSymbol(TOrdering ordering); -#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) - // Returns 'text' as UTF-8 for the System::String overloads, which forward to the std::string ones. A null C string - // is empty text, as it is for System::String. - static std::string RTLTextToUTF8(std::string const& text); - static std::string RTLTextToUTF8(std::wstring const& text); - static std::string RTLTextToUTF8(char const* text); - static std::string RTLTextToUTF8(wchar_t const* text); - static std::string RTLTextToUTF8(System::String const& text); -#endif + // Releases every hold of m_RecordingMutex this thread has left, after a crash caught on it skipped the + // TRecordingLocks' destructors. + void ReleaseRecordingHolds(); + + // Records 'failure' (from NearFailure()) as 'which' does, or throws it for an Assert method. + void ReportNearFailure(TNearFailure const& failure, ENearMethod which, std::string const& method, int line, + std::string const& msg); // The address the Same methods (e.g. CheckSame()) compare: a smart pointer's get(), or a raw pointer (or an // array, or a function) as it is. @@ -603,6 +772,50 @@ class TTestGroupBase : public ITestGroup return value; } + // The std::exception 'ex' is, for a Throws method's message check: 'ex' itself, or for a polymorphic class not + // derived from std::exception, the std::exception it's part of (e.g. when TException is a mixin base of a + // std::runtime_error subclass); nullptr when there's none. + template + static std::exception const* StdExceptionOf(TException const& ex) + { + static_cast(ex); + + if constexpr (std::is_base_of::value) + return std::addressof(ex); + else if constexpr (std::is_polymorphic::value) + return dynamic_cast(std::addressof(ex)); + else + return nullptr; + } + + // Returns 'text' as UTF-8, for the templates for mixed string types (see ForwardAsUTF8()). A null C string is + // empty text; see IsNullText(). + static std::string TextToUTF8(std::string const& text); + static std::string TextToUTF8(std::wstring const& text); + static std::string TextToUTF8(char const* text); + static std::string TextToUTF8(wchar_t const* text); +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) + static std::string TextToUTF8(System::String const& text); + template + static std::string TextToUTF8(System::AnsiStringT const& text) + { + return TextToUTF8(System::String(text)); + } +#endif + // The same, as wide text (see ForwardAsWide()). + static std::wstring TextToWide(std::string const& text); + static std::wstring TextToWide(std::wstring const& text); + static std::wstring TextToWide(char const* text); + static std::wstring TextToWide(wchar_t const* text); +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) + static std::wstring TextToWide(System::String const& text); + template + static std::wstring TextToWide(System::AnsiStringT const& text) + { + return TextToWide(System::String(text)); + } +#endif + template static int ThreeWayCompare(T a, T b) { @@ -610,19 +823,26 @@ class TTestGroupBase : public ITestGroup } // Runs 'callable' for the Throws methods (e.g. CheckThrows()) and returns why it failed, or an empty string when - // it threw a TException (or a subclass) whose message contains 'expectedMessage'. A failure signal from this - // framework (e.g. an Assert* failure inside 'callable') passes straight through, failing the test as usual, - // unless TException asks for one (e.g. TExceptTrue). + // it threw a TException (or a subclass) whose message contains 'expectedMessage', when that isn't empty (a + // TException with no message then fails). A failure signal from this framework (e.g. an Assert* failure inside + // 'callable') passes straight through, failing the test as usual, unless TException asks for one (e.g. + // TExceptTrue). template static std::string ThrowsFailure(TCallable&& callable, std::string const& expectedMessage) { + static_assert(IsExpectableException(), "TException must be a type that can be thrown and caught " + "by reference (not an array), and with ASWUNITTESTS_RTL_EXCEPTIONS an RTL class must be an Exception"); + + // A type that is neither a std::exception nor an RTL exception is caught by const reference, as Test() does + // (see m_ExpectedOtherExceptionTypeChecker): MSVC never matches a pointer to const (e.g. a thrown string + // literal, a char const*) with a handler that's a non-const reference to it, even for its exact type. #if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) constexpr bool isRTLException = std::is_base_of::value; #else constexpr bool isRTLException = false; #endif - static_assert(std::is_base_of::value || isRTLException, - "TException must be a std::exception, or with ASWUNITTESTS_RTL_EXCEPTIONS an RTL Exception, or a subclass"); + constexpr bool caughtAsIs = isRTLException || std::is_base_of::value; + typedef typename std::conditional::type TCaught; bool caught = false; std::string failure; @@ -635,7 +855,7 @@ class TTestGroupBase : public ITestGroup { static_cast(std::forward(callable)()); } - catch (TException& ex) + catch (TCaught& ex) { // A TException that is a base of TTestException (e.g. std::exception) also catches the // framework's own signals, which must still pass through. @@ -648,11 +868,8 @@ class TTestGroupBase : public ITestGroup caught = true; - if (!expectedMessage.empty() && (ExceptionMessage(ex).find(expectedMessage) == std::string::npos)) - { - failure = "Expected the exception message to contain \"" + expectedMessage + "\" but caught: " + - DescribeException(ex); - } + if (!expectedMessage.empty()) + failure = ThrowsMessageFailure(ex, expectedMessage); } }); @@ -665,6 +882,53 @@ class TTestGroupBase : public ITestGroup return failure; } + // Why 'ex', a TException a Throws method caught, fails its non-empty 'expectedMessage', or an empty string when + // its message contains it: an RTL exception's Message, or a std::exception's what() (see StdExceptionOf()). Any + // other type has no message, so it fails. + template + static std::string ThrowsMessageFailure(TException& ex, std::string const& expectedMessage) + { + std::string const expected = "Expected the exception message to contain \"" + expectedMessage + "\" but caught"; + +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) + if constexpr (std::is_base_of::value) + { + if (ExceptionMessage(ex).find(expectedMessage) == std::string::npos) + return expected + ": " + DescribeException(ex); + } + else +#endif + { + std::exception const* const stdEx = StdExceptionOf(ex); + if (stdEx == nullptr) + return expected + " an exception without a message: " + DescribeOtherException(ex); + if (ExceptionMessage(*stdEx).find(expectedMessage) == std::string::npos) + return expected + ": " + DescribeException(*stdEx); + } + + return std::string(); + } + + template + static TNearInteger ToNearInteger(TInteger value) + { + TNearInteger result; + + if constexpr (std::is_signed::value) + { + result.Negative = (value < 0); + // A negative value's magnitude, computed so the most negative int64_t doesn't overflow. + result.Magnitude = result.Negative ? static_cast(-(static_cast(value) + 1)) + 1 : + static_cast(value); + } + else + { + result.Magnitude = static_cast(value); + } + + return result; + } + // Puts the group in suite 'suitePath', for TTestHandler when the group's registration names its suite (see // ASW_REGISTER_TEST_GROUP_IN_SUITE). void SetSuitePath(std::string const& suitePath); @@ -699,7 +963,7 @@ class TTestGroupBase : public ITestGroup { #if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) if constexpr (std::is_base_of::value) - return RTLTextToUTF8(__classid(TType)->ClassName()); + return TextToUTF8(__classid(TType)->ClassName()); else #endif return TypeName(typeid(TType)); @@ -711,7 +975,7 @@ class TTestGroupBase : public ITestGroup { #if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) if constexpr (std::is_base_of::value) - return RTLTextToUTF8(const_cast(pointer)->ClassName()); + return TextToUTF8(const_cast(pointer)->ClassName()); else #endif return TypeName(typeid(*pointer)); @@ -729,9 +993,62 @@ class TTestGroupBase : public ITestGroup return std::addressof(value); } - // 'type''s name for a failure message, demangled where the compiler's name() is mangled (e.g. GCC and Clang). + // 'type''s name for a failure message, demangled where the compiler's name() is mangled (e.g. GCC, Clang and + // bcc32c). static std::string TypeName(std::type_info const& type); + // The work of the Near and NotNear templates for mixed types (see 'which'). A float or double common type goes to + // the virtual overload, so an override still sees the call; a long double one, or integers (compared exactly, as + // their sign and magnitude), to NearFailure(). + template + void VerifyMixedNear(ENearMethod which, TExpected expected, TActual actual, TTolerance tolerance, + std::string const& method, int line, std::string const& msg) + { + typedef typename std::common_type::type TCommon; + + if constexpr (std::is_same::value || std::is_same::value) + { + TCommon const commonExpected = static_cast(expected); + TCommon const commonActual = static_cast(actual); + TCommon const commonTolerance = static_cast(tolerance); + + switch (which) + { + case ENearMethod::AssertNear: + AssertNear(commonExpected, commonActual, commonTolerance, method, line, msg); + break; + case ENearMethod::AssertNotNear: + AssertNotNear(commonExpected, commonActual, commonTolerance, method, line, msg); + break; + case ENearMethod::CheckNear: + CheckNear(commonExpected, commonActual, commonTolerance, method, line, msg); + break; + case ENearMethod::CheckNotNear: + CheckNotNear(commonExpected, commonActual, commonTolerance, method, line, msg); + break; + } + } + else + { + bool const expectNear = (which == ENearMethod::AssertNear || which == ENearMethod::CheckNear); + std::optional failure; + + if constexpr (std::is_integral::value) + { + failure = NearFailure(ToNearInteger(expected), ToNearInteger(actual), ToNearInteger(tolerance), + expectNear); + } + else + { + failure = NearFailure(static_cast(expected), static_cast(actual), + static_cast(tolerance), expectNear); + } + + if (failure != std::nullopt) + ReportNearFailure(*failure, which, method, line, msg); + } + } + protected: bool m_ExceptionExpected; bool m_LogSuppressed; @@ -742,13 +1059,19 @@ class TTestGroupBase : public ITestGroup std::string m_ExceptionExpectedText; std::string m_ExpectedExceptionMessage; std::function m_ExpectedExceptionTypeChecker; + // For an expected type not derived from std::exception (see SetExceptionExpected()): called only from + // Test()'s catch (...) handler, which no std::exception or RTL exception reaches, it rethrows the exception being + // handled to match it against that type, and returns its description (see DescribeOtherException()) when it + // matches, or nullopt. + std::function ()> m_ExpectedOtherExceptionTypeChecker; #if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) std::function m_ExpectedRTLExceptionTypeChecker; #endif std::string m_Name; std::string m_QualifiedName; // See GetQualifiedTestGroupName(). // Held while a test records a result or logs, and while RunWithTimeout() decides to abandon a test that timed out, - // so an abandoned test can't change the results or the log; see BeginRecording(). + // so an abandoned test can't change the results or the log; see BeginRecording(). The framework locks it through + // TRecordingLocks, so a crash can't leave it locked (see ReleaseRecordingHolds()). std::recursive_mutex m_RecordingMutex; TTestResults m_Results; ITestRunObserver* m_RunObserver; // Not owned; nullptr when none is set. @@ -827,14 +1150,19 @@ class TTestGroupBase : public ITestGroup // ReportTimedOutTest() for what happens if it doesn't finish in time. virtual void RunWithTimeout(ITestCase& testCase, std::optional testTimeoutSeconds, bool catchCrashes); virtual void SetExceptionExpected(bool expected, std::string const& method, int line, std::string const& msg); - // Expects a specific exception type (matched polymorphically, so a base class also matches its subclasses). - // When 'expectedMessage' is non-empty, the caught exception's what() must also contain it as a substring. - // With ASWUNITTESTS_RTL_EXCEPTIONS enabled (see ASWUnitTests_Exception.h), 'TException' may also be an RTL - // exception class (System::Sysutils::Exception or a subclass), whose Message is checked instead of what(). + // Expects a specific exception type, of any kind (e.g. a std::exception class, another class, or int), matched + // polymorphically, so a base class also matches its subclasses. When 'expectedMessage' is non-empty, the caught + // exception's what() must also contain it as a substring; a type with no message (neither a std::exception nor + // an RTL exception) then fails. With ASWUNITTESTS_RTL_EXCEPTIONS enabled (see ASWUnitTests_Exception.h), + // 'TException' may also be an RTL exception class (System::Sysutils::Exception or a subclass), whose Message is + // checked instead of what(). template void SetExceptionExpected(std::string const& method, int line, std::string const& msg, std::string const& expectedMessage = std::string()) { + static_assert(IsExpectableException(), "TException must be a type that can be thrown and caught " + "by reference (not an array), and with ASWUNITTESTS_RTL_EXCEPTIONS an RTL class must be an Exception"); + SetExceptionExpected(true, method, line, msg); m_ExpectedExceptionMessage = expectedMessage; #if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) @@ -848,10 +1176,36 @@ class TTestGroupBase : public ITestGroup else #endif { - m_ExpectedExceptionTypeChecker = [](std::exception const& ex) - { - return dynamic_cast(&ex) != nullptr; - }; + // A class also matches a std::exception it's a base of (e.g. a mixin of a std::runtime_error subclass). + if constexpr (std::is_class_v) + { + m_ExpectedExceptionTypeChecker = [](std::exception const& ex) + { + return dynamic_cast(&ex) != nullptr; + }; + } + + if constexpr (!std::is_base_of_v) + { + m_ExpectedOtherExceptionTypeChecker = []() + { + std::optional description; + + try + { + throw; + } + catch (TException const& ex) + { + description = DescribeOtherException(ex); + } + catch (...) + { + } + + return description; + }; + } } } virtual void SetTestFailedCheck(std::string const& method, int line, std::string const& msg); @@ -908,16 +1262,19 @@ class TTestGroupBase : public ITestGroup AssertEquals(expectedValue, actualValue, method, line, msg); }); } - // Any two pointers, compared by address, except two C strings of the same character type, which the overloads - // above compare by content. Without these, two pointers would convert to bool and match the bool overload. Two - // pointers that can't be compared with each other (e.g. int* and long*) are a compile error. + // Any two pointers, compared by address, except two C strings, narrow or wide, which the overloads above and the + // templates for mixed string types (see TEnableIfMixedText) compare by content. Without these, two pointers would + // convert to bool and match the bool overload. Two pointers that can't be compared with each other (e.g. int* + // and long*) are a compile error. template using TPointsTo = std::is_same::type>::type, TChar>; + template + using TIsCStringPointer = std::integral_constant::value || + TPointsTo::value>; template using TEnableIfPointers = typename std::enable_if::value && std::is_pointer::value && - !(TPointsTo::value && TPointsTo::value) && - !(TPointsTo::value && TPointsTo::value), int>::type; + !(TIsCStringPointer::value && TIsCStringPointer::value), int>::type; template = 0> void AssertEquals(TExpected expected, TActual actual, std::string const& method, int line, std::string const& msg) @@ -1125,26 +1482,76 @@ class TTestGroupBase : public ITestGroup void CheckNotEqualsIC(wchar_t const* expected, wchar_t const* actual, std::string const& method, int line, std::string const& msg); -protected: // Assertion/Check methods - Near (floating point, absolute tolerance) +protected: // Assertion/Check methods - Near (absolute tolerance) + // 'actual' is near 'expected' when they're equal (so equal infinities are) or differ by at most 'tolerance'; a NaN + // is near nothing. A negative or NaN tolerance fails both Near and NotNear. The templates take any three integer + // or floating-point types except bool (all float or all double use the overloads): integers are compared + // exactly, and otherwise all three are converted to their common type, as the built-in operators do. + template + static constexpr bool IsNearArithmetic() + { + return std::is_arithmetic::value && !std::is_same::value; + } + template + static constexpr bool IsMixedNear() + { + constexpr bool allSame = std::is_same::value && std::is_same::value; + constexpr bool hasOverload = allSame && + (std::is_same::value || std::is_same::value); + + return IsNearArithmetic() && IsNearArithmetic() && IsNearArithmetic() && + !hasOverload; + } + template + using TEnableIfMixedNear = typename std::enable_if(), int>::type; + virtual void AssertNear(float expected, float actual, float tolerance, std::string const& method, int line, std::string const& msg); virtual void AssertNear(double expected, double actual, double tolerance, std::string const& method, int line, std::string const& msg); + template = 0> + void AssertNear(TExpected expected, TActual actual, TTolerance tolerance, std::string const& method, int line, + std::string const& msg) + { + VerifyMixedNear(ENearMethod::AssertNear, expected, actual, tolerance, method, line, msg); + } virtual void AssertNotNear(float expected, float actual, float tolerance, std::string const& method, int line, std::string const& msg); virtual void AssertNotNear(double expected, double actual, double tolerance, std::string const& method, int line, std::string const& msg); + template = 0> + void AssertNotNear(TExpected expected, TActual actual, TTolerance tolerance, std::string const& method, int line, + std::string const& msg) + { + VerifyMixedNear(ENearMethod::AssertNotNear, expected, actual, tolerance, method, line, msg); + } virtual void CheckNear(float expected, float actual, float tolerance, std::string const& method, int line, std::string const& msg); virtual void CheckNear(double expected, double actual, double tolerance, std::string const& method, int line, std::string const& msg); + template = 0> + void CheckNear(TExpected expected, TActual actual, TTolerance tolerance, std::string const& method, int line, + std::string const& msg) + { + VerifyMixedNear(ENearMethod::CheckNear, expected, actual, tolerance, method, line, msg); + } virtual void CheckNotNear(float expected, float actual, float tolerance, std::string const& method, int line, std::string const& msg); virtual void CheckNotNear(double expected, double actual, double tolerance, std::string const& method, int line, std::string const& msg); + template = 0> + void CheckNotNear(TExpected expected, TActual actual, TTolerance tolerance, std::string const& method, int line, + std::string const& msg) + { + VerifyMixedNear(ENearMethod::CheckNotNear, expected, actual, tolerance, method, line, msg); + } protected: // Assertion/Check methods - Boolean virtual void AssertFalse(bool testVal, std::string const& method, int line, std::string const& msg); @@ -1271,10 +1678,12 @@ class TTestGroupBase : public ITestGroup protected: // Assertion/Check methods - Throws (exception from a callable) // Each runs 'callable' (e.g. a lambda) and checks whether it throws. Unlike with SetExceptionExpected(), the test // carries on afterwards (unless an Assert method fails), so it can check several calls and the state after each. - // The Throws methods pass when 'callable' throws a TException or a subclass (TException being a std::exception, - // or with ASWUNITTESTS_RTL_EXCEPTIONS an RTL Exception) whose message contains 'expectedMessage', when that isn't - // empty. A failure signal from this framework inside 'callable' (e.g. an Assert* failure, Fail() or Skip()) still - // ends the test as usual, unless TException asks for one. + // The Throws methods pass when 'callable' throws a TException or a subclass (TException being of any kind, as + // with SetExceptionExpected(), e.g. a std::exception class, with ASWUNITTESTS_RTL_EXCEPTIONS an RTL + // Exception, another class, or int) whose message contains 'expectedMessage', when that isn't empty: what(), or an + // RTL exception's Message. A TException with no message then fails. A failure signal from this framework inside + // 'callable' (e.g. an Assert* failure, Fail() or Skip()) still ends the test as usual, unless TException asks for + // one. template void AssertNoThrow(TCallable&& callable, std::string const& method, int line, std::string const& msg) { @@ -1320,12 +1729,18 @@ class TTestGroupBase : public ITestGroup if (!value.empty()) throw TExceptEmpty(method, line, "Expected empty but " + DescribeContents(value), msg); } + // A C string, or a character array taken as one (so an array holding "" is empty, whatever its size). A null C + // string is neither empty nor not empty: both methods fail, showing it as "(null)". + void AssertEmpty(char const* text, std::string const& method, int line, std::string const& msg); + void AssertEmpty(wchar_t const* text, std::string const& method, int line, std::string const& msg); template = 0> void AssertNotEmpty(TValue const& value, std::string const& method, int line, std::string const& msg) { if (value.empty()) throw TExceptNotEmpty(method, line, msg); } + void AssertNotEmpty(char const* text, std::string const& method, int line, std::string const& msg); + void AssertNotEmpty(wchar_t const* text, std::string const& method, int line, std::string const& msg); template = 0> void CheckEmpty(TValue const& value, std::string const& method, int line, std::string const& msg) @@ -1333,12 +1748,16 @@ class TTestGroupBase : public ITestGroup if (!value.empty()) SetTestFailedCheck(method, line, "Expected empty but " + DescribeContents(value) + ". " + msg); } + void CheckEmpty(char const* text, std::string const& method, int line, std::string const& msg); + void CheckEmpty(wchar_t const* text, std::string const& method, int line, std::string const& msg); template = 0> void CheckNotEmpty(TValue const& value, std::string const& method, int line, std::string const& msg) { if (value.empty()) SetTestFailedCheck(method, line, "Expected not empty but was empty. " + msg); } + void CheckNotEmpty(char const* text, std::string const& method, int line, std::string const& msg); + void CheckNotEmpty(wchar_t const* text, std::string const& method, int line, std::string const& msg); protected: // Assertion/Check methods - Contains (substring) // A null C string, as the text or the substring, matches nothing: the Contains methods fail, showing it as @@ -1565,26 +1984,43 @@ class TTestGroupBase : public ITestGroup protected: // Assertion/Check methods - Matches (regular expression) // Each checks whether the whole of 'text' matches the ECMAScript regular expression 'pattern' (std::regex_match; // start or end a pattern with ".*" to match part of the text). Wide text is matched with std::wregex. An invalid - // pattern fails the check, showing why, rather than throwing std::regex_error. + // pattern fails the check, showing why, rather than throwing std::regex_error. A null C string, as the text or + // the pattern, matches nothing: the Matches methods fail, showing it as "(null)", and the NotMatches ones pass. virtual void AssertMatches(std::string const& text, std::string const& pattern, std::string const& method, int line, std::string const& msg); virtual void AssertMatches(std::wstring const& text, std::wstring const& pattern, std::string const& method, int line, std::string const& msg); + void AssertMatches(char const* text, char const* pattern, std::string const& method, int line, + std::string const& msg); + void AssertMatches(wchar_t const* text, wchar_t const* pattern, std::string const& method, int line, + std::string const& msg); virtual void AssertNotMatches(std::string const& text, std::string const& pattern, std::string const& method, int line, std::string const& msg); virtual void AssertNotMatches(std::wstring const& text, std::wstring const& pattern, std::string const& method, int line, std::string const& msg); + void AssertNotMatches(char const* text, char const* pattern, std::string const& method, int line, + std::string const& msg); + void AssertNotMatches(wchar_t const* text, wchar_t const* pattern, std::string const& method, int line, + std::string const& msg); virtual void CheckMatches(std::string const& text, std::string const& pattern, std::string const& method, int line, std::string const& msg); virtual void CheckMatches(std::wstring const& text, std::wstring const& pattern, std::string const& method, int line, std::string const& msg); + void CheckMatches(char const* text, char const* pattern, std::string const& method, int line, + std::string const& msg); + void CheckMatches(wchar_t const* text, wchar_t const* pattern, std::string const& method, int line, + std::string const& msg); virtual void CheckNotMatches(std::string const& text, std::string const& pattern, std::string const& method, int line, std::string const& msg); virtual void CheckNotMatches(std::wstring const& text, std::wstring const& pattern, std::string const& method, int line, std::string const& msg); + void CheckNotMatches(char const* text, char const* pattern, std::string const& method, int line, + std::string const& msg); + void CheckNotMatches(wchar_t const* text, wchar_t const* pattern, std::string const& method, int line, + std::string const& msg); protected: // Assertion/Check methods - Ordering (value compared with a bound) // Any two integer or floating-point types except bool. @@ -1652,239 +2088,505 @@ class TTestGroupBase : public ITestGroup virtual void CheckNotEqualsMem(void const* expected, void const* actual, std::size_t size, std::string const& method, int line, std::string const& msg); +protected: // Assertion/Check methods - Mixed string types + // The Equals, Contains, StartsWith/EndsWith and Matches methods, IC and Not forms included, for two texts that + // the overloads above don't take as they are: a std::string with a C string, narrow text with wide, or an RTL + // string (System::String, AnsiString, UTF8String or another AnsiStringT, in RTL builds) with any text. Both are + // converted to UTF-8 and compared as two std::string values, except that two wide texts are compared as two + // std::wstring values, and the Matches methods match any texts but two narrow ones as wide text (std::wregex). + // A null C string follows the same rules as in the C string overloads, whatever it's compared with. #if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) -protected: // Assertion/Check methods - System::String (RTL) - // Each takes two texts, at least one of them a System::String, and forwards both to the std::string overload as - // UTF-8. The other may also be a std::string, std::wstring or C string. The Empty methods take one System::String. template - using TIsRTLString = std::is_same::type, System::String>; + using TIsRTLText = std::integral_constant::value || + TIsAnsiStringT::value>; +#else template - using TIsRTLText = std::integral_constant::value || - std::is_same::type, std::string>::value || - std::is_same::type, std::wstring>::value || - std::is_same::type, char const*>::value || - std::is_same::type, char*>::value || - std::is_same::type, wchar_t const*>::value || - std::is_same::type, wchar_t*>::value>; - template - using TEnableIfRTLText = typename std::enable_if<(TIsRTLString::value || - TIsRTLString::value) && TIsRTLText::value && TIsRTLText::value, int>::type; - // Exactly a System::String, so nothing else converts to one implicitly (e.g. a C string, only in RTL builds). + using TIsRTLText = std::false_type; +#endif + // The kind of text TText is (see ETextKind). An array is a C string. + template + static constexpr ETextKind TextKindOf() + { + using TDecayed = typename std::decay::type; + + if constexpr (std::is_same::value) + return ETextKind::NarrowString; + else if constexpr (std::is_same::value || std::is_same::value) + return ETextKind::NarrowCString; + else if constexpr (std::is_same::value) + return ETextKind::WideString; + else if constexpr (std::is_same::value || std::is_same::value) + return ETextKind::WideCString; + else if constexpr (TIsRTLText::value) + return ETextKind::RTL; + else + return ETextKind::None; + } + template + static constexpr bool IsNarrowText() + { + return TextKindOf() == ETextKind::NarrowString || TextKindOf() == ETextKind::NarrowCString; + } template - using TEnableIfRTLString = typename std::enable_if::value, int>::type; + static constexpr bool IsWideText() + { + return TextKindOf() == ETextKind::WideString || TextKindOf() == ETextKind::WideCString; + } + // A character array, such as a literal, with a std::string or std::wstring of its width, which converts to it and + // goes to its overload as before: an array is never null, so it needs nothing these templates add, and leaving it + // out saves an instantiation for each length of literal. + template + static constexpr bool IsArrayWithItsString() + { + constexpr bool isArray = std::is_array::value; + constexpr ETextKind arrayKind = TextKindOf(); + constexpr ETextKind stringKind = TextKindOf(); + + return isArray && ((arrayKind == ETextKind::NarrowCString && stringKind == ETextKind::NarrowString) || + (arrayKind == ETextKind::WideCString && stringKind == ETextKind::WideString)); + } + // Two texts of different kinds, or an RTL string with any text, but not an array with its string (see + // IsArrayWithItsString()). + template + static constexpr bool IsMixedText() + { + constexpr ETextKind firstKind = TextKindOf(); + constexpr ETextKind secondKind = TextKindOf(); + constexpr bool bothText = firstKind != ETextKind::None && secondKind != ETextKind::None; + constexpr bool mixed = firstKind != secondKind || firstKind == ETextKind::RTL; + + return bothText && mixed && !IsArrayWithItsString() && + !IsArrayWithItsString(); + } + template + using TEnableIfMixedText = typename std::enable_if(), int>::type; - template = 0> + template = 0> void AssertContains(TText const& text, TSubstring const& substring, std::string const& method, int line, std::string const& msg) { - AssertContains(RTLTextToUTF8(text), RTLTextToUTF8(substring), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, substring, &TTestGroupBase::AssertContains, &TTestGroupBase::AssertContains, method, + line, msg); + else + ForwardAsUTF8(text, substring, &TTestGroupBase::AssertContains, &TTestGroupBase::AssertContains, method, + line, msg); } - template = 0> + template = 0> void AssertContainsIC(TText const& text, TSubstring const& substring, std::string const& method, int line, std::string const& msg) { - AssertContainsIC(RTLTextToUTF8(text), RTLTextToUTF8(substring), method, line, msg); - } - template = 0> - void AssertEmpty(TText const& text, std::string const& method, int line, std::string const& msg) - { - AssertEmpty(RTLTextToUTF8(text), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, substring, &TTestGroupBase::AssertContainsIC, &TTestGroupBase::AssertContainsIC, method, + line, msg); + else + ForwardAsUTF8(text, substring, &TTestGroupBase::AssertContainsIC, &TTestGroupBase::AssertContainsIC, method, + line, msg); } - template = 0> + template = 0> void AssertEndsWith(TText const& text, TSuffix const& suffix, std::string const& method, int line, std::string const& msg) { - AssertEndsWith(RTLTextToUTF8(text), RTLTextToUTF8(suffix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, suffix, &TTestGroupBase::AssertEndsWith, &TTestGroupBase::AssertEndsWith, method, line, + msg); + else + ForwardAsUTF8(text, suffix, &TTestGroupBase::AssertEndsWith, &TTestGroupBase::AssertEndsWith, method, line, + msg); } - template = 0> + template = 0> void AssertEndsWithIC(TText const& text, TSuffix const& suffix, std::string const& method, int line, std::string const& msg) { - AssertEndsWithIC(RTLTextToUTF8(text), RTLTextToUTF8(suffix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, suffix, &TTestGroupBase::AssertEndsWithIC, &TTestGroupBase::AssertEndsWithIC, method, + line, msg); + else + ForwardAsUTF8(text, suffix, &TTestGroupBase::AssertEndsWithIC, &TTestGroupBase::AssertEndsWithIC, method, + line, msg); } - template = 0> + template = 0> void AssertEquals(TExpected const& expected, TActual const& actual, std::string const& method, int line, std::string const& msg) { - AssertEquals(RTLTextToUTF8(expected), RTLTextToUTF8(actual), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(expected, actual, &TTestGroupBase::AssertEquals, &TTestGroupBase::AssertEquals, method, line, + msg); + else + ForwardAsUTF8(expected, actual, &TTestGroupBase::AssertEquals, &TTestGroupBase::AssertEquals, method, line, + msg); } - template = 0> + template = 0> void AssertEqualsIC(TExpected const& expected, TActual const& actual, std::string const& method, int line, std::string const& msg) { - AssertEqualsIC(RTLTextToUTF8(expected), RTLTextToUTF8(actual), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(expected, actual, &TTestGroupBase::AssertEqualsIC, &TTestGroupBase::AssertEqualsIC, method, + line, msg); + else + ForwardAsUTF8(expected, actual, &TTestGroupBase::AssertEqualsIC, &TTestGroupBase::AssertEqualsIC, method, + line, msg); } - template = 0> - void AssertNotContains(TText const& text, TSubstring const& substring, std::string const& method, int line, + template = 0> + void AssertMatches(TText const& text, TPattern const& pattern, std::string const& method, int line, std::string const& msg) { - AssertNotContains(RTLTextToUTF8(text), RTLTextToUTF8(substring), method, line, msg); + if constexpr (IsNarrowText() && IsNarrowText()) + ForwardAsUTF8(text, pattern, &TTestGroupBase::AssertMatches, &TTestGroupBase::AssertMatches, method, line, + msg); + else + ForwardAsWide(text, pattern, &TTestGroupBase::AssertMatches, &TTestGroupBase::AssertMatches, method, line, + msg); } - template = 0> - void AssertNotContainsIC(TText const& text, TSubstring const& substring, std::string const& method, int line, + template = 0> + void AssertNotContains(TText const& text, TSubstring const& substring, std::string const& method, int line, std::string const& msg) { - AssertNotContainsIC(RTLTextToUTF8(text), RTLTextToUTF8(substring), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, substring, &TTestGroupBase::AssertNotContains, &TTestGroupBase::AssertNotContains, + method, line, msg); + else + ForwardAsUTF8(text, substring, &TTestGroupBase::AssertNotContains, &TTestGroupBase::AssertNotContains, + method, line, msg); } - template = 0> - void AssertNotEmpty(TText const& text, std::string const& method, int line, std::string const& msg) + template = 0> + void AssertNotContainsIC(TText const& text, TSubstring const& substring, std::string const& method, int line, + std::string const& msg) { - AssertNotEmpty(RTLTextToUTF8(text), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, substring, &TTestGroupBase::AssertNotContainsIC, &TTestGroupBase::AssertNotContainsIC, + method, line, msg); + else + ForwardAsUTF8(text, substring, &TTestGroupBase::AssertNotContainsIC, &TTestGroupBase::AssertNotContainsIC, + method, line, msg); } - template = 0> + template = 0> void AssertNotEndsWith(TText const& text, TSuffix const& suffix, std::string const& method, int line, std::string const& msg) { - AssertNotEndsWith(RTLTextToUTF8(text), RTLTextToUTF8(suffix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, suffix, &TTestGroupBase::AssertNotEndsWith, &TTestGroupBase::AssertNotEndsWith, method, + line, msg); + else + ForwardAsUTF8(text, suffix, &TTestGroupBase::AssertNotEndsWith, &TTestGroupBase::AssertNotEndsWith, method, + line, msg); } - template = 0> + template = 0> void AssertNotEndsWithIC(TText const& text, TSuffix const& suffix, std::string const& method, int line, std::string const& msg) { - AssertNotEndsWithIC(RTLTextToUTF8(text), RTLTextToUTF8(suffix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, suffix, &TTestGroupBase::AssertNotEndsWithIC, &TTestGroupBase::AssertNotEndsWithIC, + method, line, msg); + else + ForwardAsUTF8(text, suffix, &TTestGroupBase::AssertNotEndsWithIC, &TTestGroupBase::AssertNotEndsWithIC, + method, line, msg); } - template = 0> + template = 0> void AssertNotEquals(TExpected const& expected, TActual const& actual, std::string const& method, int line, std::string const& msg) { - AssertNotEquals(RTLTextToUTF8(expected), RTLTextToUTF8(actual), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(expected, actual, &TTestGroupBase::AssertNotEquals, &TTestGroupBase::AssertNotEquals, method, + line, msg); + else + ForwardAsUTF8(expected, actual, &TTestGroupBase::AssertNotEquals, &TTestGroupBase::AssertNotEquals, method, + line, msg); } - template = 0> + template = 0> void AssertNotEqualsIC(TExpected const& expected, TActual const& actual, std::string const& method, int line, std::string const& msg) { - AssertNotEqualsIC(RTLTextToUTF8(expected), RTLTextToUTF8(actual), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(expected, actual, &TTestGroupBase::AssertNotEqualsIC, &TTestGroupBase::AssertNotEqualsIC, + method, line, msg); + else + ForwardAsUTF8(expected, actual, &TTestGroupBase::AssertNotEqualsIC, &TTestGroupBase::AssertNotEqualsIC, + method, line, msg); + } + template = 0> + void AssertNotMatches(TText const& text, TPattern const& pattern, std::string const& method, int line, + std::string const& msg) + { + if constexpr (IsNarrowText() && IsNarrowText()) + ForwardAsUTF8(text, pattern, &TTestGroupBase::AssertNotMatches, &TTestGroupBase::AssertNotMatches, method, + line, msg); + else + ForwardAsWide(text, pattern, &TTestGroupBase::AssertNotMatches, &TTestGroupBase::AssertNotMatches, method, + line, msg); } - template = 0> + template = 0> void AssertNotStartsWith(TText const& text, TPrefix const& prefix, std::string const& method, int line, std::string const& msg) { - AssertNotStartsWith(RTLTextToUTF8(text), RTLTextToUTF8(prefix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, prefix, &TTestGroupBase::AssertNotStartsWith, &TTestGroupBase::AssertNotStartsWith, + method, line, msg); + else + ForwardAsUTF8(text, prefix, &TTestGroupBase::AssertNotStartsWith, &TTestGroupBase::AssertNotStartsWith, + method, line, msg); } - template = 0> + template = 0> void AssertNotStartsWithIC(TText const& text, TPrefix const& prefix, std::string const& method, int line, std::string const& msg) { - AssertNotStartsWithIC(RTLTextToUTF8(text), RTLTextToUTF8(prefix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, prefix, &TTestGroupBase::AssertNotStartsWithIC, &TTestGroupBase::AssertNotStartsWithIC, + method, line, msg); + else + ForwardAsUTF8(text, prefix, &TTestGroupBase::AssertNotStartsWithIC, &TTestGroupBase::AssertNotStartsWithIC, + method, line, msg); } - template = 0> + template = 0> void AssertStartsWith(TText const& text, TPrefix const& prefix, std::string const& method, int line, std::string const& msg) { - AssertStartsWith(RTLTextToUTF8(text), RTLTextToUTF8(prefix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, prefix, &TTestGroupBase::AssertStartsWith, &TTestGroupBase::AssertStartsWith, method, + line, msg); + else + ForwardAsUTF8(text, prefix, &TTestGroupBase::AssertStartsWith, &TTestGroupBase::AssertStartsWith, method, + line, msg); } - template = 0> + template = 0> void AssertStartsWithIC(TText const& text, TPrefix const& prefix, std::string const& method, int line, std::string const& msg) { - AssertStartsWithIC(RTLTextToUTF8(text), RTLTextToUTF8(prefix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, prefix, &TTestGroupBase::AssertStartsWithIC, &TTestGroupBase::AssertStartsWithIC, + method, line, msg); + else + ForwardAsUTF8(text, prefix, &TTestGroupBase::AssertStartsWithIC, &TTestGroupBase::AssertStartsWithIC, + method, line, msg); } - template = 0> + template = 0> void CheckContains(TText const& text, TSubstring const& substring, std::string const& method, int line, std::string const& msg) { - CheckContains(RTLTextToUTF8(text), RTLTextToUTF8(substring), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, substring, &TTestGroupBase::CheckContains, &TTestGroupBase::CheckContains, method, line, + msg); + else + ForwardAsUTF8(text, substring, &TTestGroupBase::CheckContains, &TTestGroupBase::CheckContains, method, line, + msg); } - template = 0> + template = 0> void CheckContainsIC(TText const& text, TSubstring const& substring, std::string const& method, int line, std::string const& msg) { - CheckContainsIC(RTLTextToUTF8(text), RTLTextToUTF8(substring), method, line, msg); - } - template = 0> - void CheckEmpty(TText const& text, std::string const& method, int line, std::string const& msg) - { - CheckEmpty(RTLTextToUTF8(text), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, substring, &TTestGroupBase::CheckContainsIC, &TTestGroupBase::CheckContainsIC, method, + line, msg); + else + ForwardAsUTF8(text, substring, &TTestGroupBase::CheckContainsIC, &TTestGroupBase::CheckContainsIC, method, + line, msg); } - template = 0> + template = 0> void CheckEndsWith(TText const& text, TSuffix const& suffix, std::string const& method, int line, std::string const& msg) { - CheckEndsWith(RTLTextToUTF8(text), RTLTextToUTF8(suffix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, suffix, &TTestGroupBase::CheckEndsWith, &TTestGroupBase::CheckEndsWith, method, line, + msg); + else + ForwardAsUTF8(text, suffix, &TTestGroupBase::CheckEndsWith, &TTestGroupBase::CheckEndsWith, method, line, + msg); } - template = 0> + template = 0> void CheckEndsWithIC(TText const& text, TSuffix const& suffix, std::string const& method, int line, std::string const& msg) { - CheckEndsWithIC(RTLTextToUTF8(text), RTLTextToUTF8(suffix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, suffix, &TTestGroupBase::CheckEndsWithIC, &TTestGroupBase::CheckEndsWithIC, method, + line, msg); + else + ForwardAsUTF8(text, suffix, &TTestGroupBase::CheckEndsWithIC, &TTestGroupBase::CheckEndsWithIC, method, + line, msg); } - template = 0> + template = 0> void CheckEquals(TExpected const& expected, TActual const& actual, std::string const& method, int line, std::string const& msg) { - CheckEquals(RTLTextToUTF8(expected), RTLTextToUTF8(actual), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(expected, actual, &TTestGroupBase::CheckEquals, &TTestGroupBase::CheckEquals, method, line, + msg); + else + ForwardAsUTF8(expected, actual, &TTestGroupBase::CheckEquals, &TTestGroupBase::CheckEquals, method, line, + msg); } - template = 0> + template = 0> void CheckEqualsIC(TExpected const& expected, TActual const& actual, std::string const& method, int line, std::string const& msg) { - CheckEqualsIC(RTLTextToUTF8(expected), RTLTextToUTF8(actual), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(expected, actual, &TTestGroupBase::CheckEqualsIC, &TTestGroupBase::CheckEqualsIC, method, + line, msg); + else + ForwardAsUTF8(expected, actual, &TTestGroupBase::CheckEqualsIC, &TTestGroupBase::CheckEqualsIC, method, + line, msg); } - template = 0> - void CheckNotContains(TText const& text, TSubstring const& substring, std::string const& method, int line, + template = 0> + void CheckMatches(TText const& text, TPattern const& pattern, std::string const& method, int line, std::string const& msg) { - CheckNotContains(RTLTextToUTF8(text), RTLTextToUTF8(substring), method, line, msg); + if constexpr (IsNarrowText() && IsNarrowText()) + ForwardAsUTF8(text, pattern, &TTestGroupBase::CheckMatches, &TTestGroupBase::CheckMatches, method, line, + msg); + else + ForwardAsWide(text, pattern, &TTestGroupBase::CheckMatches, &TTestGroupBase::CheckMatches, method, line, + msg); } - template = 0> - void CheckNotContainsIC(TText const& text, TSubstring const& substring, std::string const& method, int line, + template = 0> + void CheckNotContains(TText const& text, TSubstring const& substring, std::string const& method, int line, std::string const& msg) { - CheckNotContainsIC(RTLTextToUTF8(text), RTLTextToUTF8(substring), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, substring, &TTestGroupBase::CheckNotContains, &TTestGroupBase::CheckNotContains, method, + line, msg); + else + ForwardAsUTF8(text, substring, &TTestGroupBase::CheckNotContains, &TTestGroupBase::CheckNotContains, method, + line, msg); } - template = 0> - void CheckNotEmpty(TText const& text, std::string const& method, int line, std::string const& msg) + template = 0> + void CheckNotContainsIC(TText const& text, TSubstring const& substring, std::string const& method, int line, + std::string const& msg) { - CheckNotEmpty(RTLTextToUTF8(text), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, substring, &TTestGroupBase::CheckNotContainsIC, &TTestGroupBase::CheckNotContainsIC, + method, line, msg); + else + ForwardAsUTF8(text, substring, &TTestGroupBase::CheckNotContainsIC, &TTestGroupBase::CheckNotContainsIC, + method, line, msg); } - template = 0> + template = 0> void CheckNotEndsWith(TText const& text, TSuffix const& suffix, std::string const& method, int line, std::string const& msg) { - CheckNotEndsWith(RTLTextToUTF8(text), RTLTextToUTF8(suffix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, suffix, &TTestGroupBase::CheckNotEndsWith, &TTestGroupBase::CheckNotEndsWith, method, + line, msg); + else + ForwardAsUTF8(text, suffix, &TTestGroupBase::CheckNotEndsWith, &TTestGroupBase::CheckNotEndsWith, method, + line, msg); } - template = 0> + template = 0> void CheckNotEndsWithIC(TText const& text, TSuffix const& suffix, std::string const& method, int line, std::string const& msg) { - CheckNotEndsWithIC(RTLTextToUTF8(text), RTLTextToUTF8(suffix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, suffix, &TTestGroupBase::CheckNotEndsWithIC, &TTestGroupBase::CheckNotEndsWithIC, + method, line, msg); + else + ForwardAsUTF8(text, suffix, &TTestGroupBase::CheckNotEndsWithIC, &TTestGroupBase::CheckNotEndsWithIC, + method, line, msg); } - template = 0> + template = 0> void CheckNotEquals(TExpected const& expected, TActual const& actual, std::string const& method, int line, std::string const& msg) { - CheckNotEquals(RTLTextToUTF8(expected), RTLTextToUTF8(actual), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(expected, actual, &TTestGroupBase::CheckNotEquals, &TTestGroupBase::CheckNotEquals, method, + line, msg); + else + ForwardAsUTF8(expected, actual, &TTestGroupBase::CheckNotEquals, &TTestGroupBase::CheckNotEquals, method, + line, msg); } - template = 0> + template = 0> void CheckNotEqualsIC(TExpected const& expected, TActual const& actual, std::string const& method, int line, std::string const& msg) { - CheckNotEqualsIC(RTLTextToUTF8(expected), RTLTextToUTF8(actual), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(expected, actual, &TTestGroupBase::CheckNotEqualsIC, &TTestGroupBase::CheckNotEqualsIC, + method, line, msg); + else + ForwardAsUTF8(expected, actual, &TTestGroupBase::CheckNotEqualsIC, &TTestGroupBase::CheckNotEqualsIC, + method, line, msg); + } + template = 0> + void CheckNotMatches(TText const& text, TPattern const& pattern, std::string const& method, int line, + std::string const& msg) + { + if constexpr (IsNarrowText() && IsNarrowText()) + ForwardAsUTF8(text, pattern, &TTestGroupBase::CheckNotMatches, &TTestGroupBase::CheckNotMatches, method, + line, msg); + else + ForwardAsWide(text, pattern, &TTestGroupBase::CheckNotMatches, &TTestGroupBase::CheckNotMatches, method, + line, msg); } - template = 0> + template = 0> void CheckNotStartsWith(TText const& text, TPrefix const& prefix, std::string const& method, int line, std::string const& msg) { - CheckNotStartsWith(RTLTextToUTF8(text), RTLTextToUTF8(prefix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, prefix, &TTestGroupBase::CheckNotStartsWith, &TTestGroupBase::CheckNotStartsWith, + method, line, msg); + else + ForwardAsUTF8(text, prefix, &TTestGroupBase::CheckNotStartsWith, &TTestGroupBase::CheckNotStartsWith, + method, line, msg); } - template = 0> + template = 0> void CheckNotStartsWithIC(TText const& text, TPrefix const& prefix, std::string const& method, int line, std::string const& msg) { - CheckNotStartsWithIC(RTLTextToUTF8(text), RTLTextToUTF8(prefix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, prefix, &TTestGroupBase::CheckNotStartsWithIC, &TTestGroupBase::CheckNotStartsWithIC, + method, line, msg); + else + ForwardAsUTF8(text, prefix, &TTestGroupBase::CheckNotStartsWithIC, &TTestGroupBase::CheckNotStartsWithIC, + method, line, msg); } - template = 0> + template = 0> void CheckStartsWith(TText const& text, TPrefix const& prefix, std::string const& method, int line, std::string const& msg) { - CheckStartsWith(RTLTextToUTF8(text), RTLTextToUTF8(prefix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, prefix, &TTestGroupBase::CheckStartsWith, &TTestGroupBase::CheckStartsWith, method, + line, msg); + else + ForwardAsUTF8(text, prefix, &TTestGroupBase::CheckStartsWith, &TTestGroupBase::CheckStartsWith, method, + line, msg); } - template = 0> + template = 0> void CheckStartsWithIC(TText const& text, TPrefix const& prefix, std::string const& method, int line, std::string const& msg) { - CheckStartsWithIC(RTLTextToUTF8(text), RTLTextToUTF8(prefix), method, line, msg); + if constexpr (IsWideText() && IsWideText()) + ForwardAsWide(text, prefix, &TTestGroupBase::CheckStartsWithIC, &TTestGroupBase::CheckStartsWithIC, method, + line, msg); + else + ForwardAsUTF8(text, prefix, &TTestGroupBase::CheckStartsWithIC, &TTestGroupBase::CheckStartsWithIC, method, + line, msg); + } + +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) +protected: // Assertion/Check methods - System::String (RTL) + // The Empty methods for an RTL string: a System::String, or an AnsiStringT such as AnsiString or UTF8String. + // Exactly those, so nothing else converts to one implicitly (e.g. a C string, only in RTL builds). + template + using TEnableIfRTLString = typename std::enable_if::type>::value, + int>::type; + + template = 0> + void AssertEmpty(TText const& text, std::string const& method, int line, std::string const& msg) + { + AssertEmpty(TextToUTF8(text), method, line, msg); + } + template = 0> + void AssertNotEmpty(TText const& text, std::string const& method, int line, std::string const& msg) + { + AssertNotEmpty(TextToUTF8(text), method, line, msg); + } + + template = 0> + void CheckEmpty(TText const& text, std::string const& method, int line, std::string const& msg) + { + CheckEmpty(TextToUTF8(text), method, line, msg); + } + template = 0> + void CheckNotEmpty(TText const& text, std::string const& method, int line, std::string const& msg) + { + CheckNotEmpty(TextToUTF8(text), method, line, msg); } #endif // #if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) diff --git a/src/ASWUnitTests_Utils.cpp b/src/ASWUnitTests_Utils.cpp index e9ca3f1..42297de 100644 --- a/src/ASWUnitTests_Utils.cpp +++ b/src/ASWUnitTests_Utils.cpp @@ -26,19 +26,47 @@ limitations under the License. //--------------------------------------------------------------------------- #include #include +#include #include #include //--------------------------------------------------------------------------- +#if defined(_WIN32) +# include +# include +# if !defined(NOMINMAX) +# define NOMINMAX +# endif +# if !defined(WIN32_LEAN_AND_MEAN) +# define WIN32_LEAN_AND_MEAN +# endif +# include +# include +#endif +//--------------------------------------------------------------------------- namespace { +#if defined(_WIN32) +// A buffer from CommandLineToArgvW() or FormatMessageW(), freed with LocalFree(). +template +using TLocalPtr = std::unique_ptr; +#endif + +//--------------------------------------------------------------------------- + void AppendUTF8(std::string& text, uint32_t codePoint); +void AppendWide(std::wstring& text, uint32_t codePoint); +std::size_t DecodeUTF8Sequence(std::string const& text, std::size_t start, uint32_t& codePoint); bool IsHighSurrogate(uint32_t codeUnit); bool IsLowSurrogate(uint32_t codeUnit); //--------------------------------------------------------------------------- -// Appends 'codePoint', which must be a valid code point, to 'text' as UTF-8. +/* + AppendUTF8 + + Appends 'codePoint', which must be a valid code point, to 'text' as UTF-8. +*/ void AppendUTF8(std::string& text, uint32_t codePoint) { if (codePoint < 0x80) @@ -64,26 +92,142 @@ void AppendUTF8(std::string& text, uint32_t codePoint) text += static_cast(0x80 | (codePoint & 0x3F)); } } +//--------------------------------------------------------------------------- +/* + AppendWide + + Appends 'codePoint', which must be a valid code point, to 'text': as is where wchar_t is 32 bits, and as a + surrogate pair, above U+FFFF, where it's 16. +*/ +void AppendWide(std::wstring& text, uint32_t codePoint) +{ +#if WCHAR_MAX > 0xFFFF + text += static_cast(codePoint); +#else + if (codePoint < 0x10000) + { + text += static_cast(codePoint); + return; + } + + text += static_cast(0xD800 + ((codePoint - 0x10000) >> 10)); + text += static_cast(0xDC00 + ((codePoint - 0x10000) & 0x3FF)); +#endif +} +//--------------------------------------------------------------------------- +/* + DecodeUTF8Sequence + + Decodes the UTF-8 sequence at 'start' in 'text', which must be within it, into 'codePoint', and returns how many + bytes it took. A maximal ill-formed sequence (a byte that can't start a sequence, or a sequence's valid start + before it breaks off or 'text' ends) gives U+FFFD and takes just those bytes, so the next sequence starts at the + byte that broke it off. The second byte's range depends on the first, which rules out overlong sequences, + surrogates and values above U+10FFFF (the Unicode standard's table 3-7, "Well-Formed UTF-8 Byte Sequences"). +*/ +std::size_t DecodeUTF8Sequence(std::string const& text, std::size_t start, uint32_t& codePoint) +{ + uint32_t const replacementCharacter = 0xFFFD; + unsigned char const lead = static_cast(text[start]); + + if (lead < 0x80) + { + codePoint = lead; + return 1; + } + + std::size_t length = 0; + unsigned char secondLow = 0x80; + unsigned char secondHigh = 0xBF; + + if (lead >= 0xC2 && lead <= 0xDF) + { + length = 2; + } + else if (lead >= 0xE0 && lead <= 0xEF) + { + length = 3; + secondLow = (lead == 0xE0) ? 0xA0 : 0x80; + secondHigh = (lead == 0xED) ? 0x9F : 0xBF; + } + else if (lead >= 0xF0 && lead <= 0xF4) + { + length = 4; + secondLow = (lead == 0xF0) ? 0x90 : 0x80; + secondHigh = (lead == 0xF4) ? 0x8F : 0xBF; + } + else + { + codePoint = replacementCharacter; // A continuation byte, or one never used in UTF-8. + return 1; + } + + codePoint = lead & (0xFFu >> (length + 1)); + + for (std::size_t i = 1; i < length; ++i) + { + if (start + i >= text.size()) + { + codePoint = replacementCharacter; + return i; + } + + unsigned char const next = static_cast(text[start + i]); + unsigned char const low = (i == 1) ? secondLow : 0x80; + unsigned char const high = (i == 1) ? secondHigh : 0xBF; + if (next < low || next > high) + { + codePoint = replacementCharacter; + return i; + } + + codePoint = (codePoint << 6) | (next & 0x3Fu); + } + + return length; +} //--------------------------------------------------------------------------- bool IsHighSurrogate(uint32_t codeUnit) { return codeUnit >= 0xD800 && codeUnit <= 0xDBFF; } - //--------------------------------------------------------------------------- bool IsLowSurrogate(uint32_t codeUnit) { return codeUnit >= 0xDC00 && codeUnit <= 0xDFFF; } - //--------------------------------------------------------------------------- } // namespace +//--------------------------------------------------------------------------- + namespace ASWUnitTests { +//--------------------------------------------------------------------------- +#if defined(_WIN32) +std::vector CommandLineArguments(std::wstring const& commandLine) +{ + // CommandLineToArgvW() splits an empty command line as if it were this executable's path, unquoted, so a path + // with a space in it would become a program name and an argument. + if (commandLine.empty()) + return std::vector(); + + int count = 0; + TLocalPtr const argv(CommandLineToArgvW(commandLine.c_str(), &count), &LocalFree); + + if (argv == nullptr) + throw std::runtime_error("Couldn't split the command line: " + SystemErrorText(GetLastError())); + + std::vector args; + + for (int i = 1; i < count; ++i) + args.push_back(WideToUTF8(argv.get()[i])); + + return args; +} +#endif //--------------------------------------------------------------------------- uint32_t FNV1aHash(std::string const& text) { @@ -130,6 +274,42 @@ std::vector ShuffledIndices(std::size_t count, unsigned int seed) return indices; } //--------------------------------------------------------------------------- +#if defined(_WIN32) +std::string SystemErrorText(unsigned long error) +{ + wchar_t* buffer = nullptr; + DWORD const length = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, error, 0, reinterpret_cast(&buffer), 0, nullptr); + + if (length == 0 || buffer == nullptr) + return "error " + std::to_string(error); + + TLocalPtr const owner(buffer, &LocalFree); + std::wstring text(buffer, length); + + // FormatMessageW() ends the text with a line break. + while (!text.empty() && (text.back() == L'\r' || text.back() == L'\n' || text.back() == L' ')) + text.pop_back(); + + return WideToUTF8(text); +} +#endif +//--------------------------------------------------------------------------- +std::wstring UTF8ToWide(std::string const& text) +{ + std::wstring wide; + wide.reserve(text.size()); + + for (std::size_t i = 0; i < text.size(); ) + { + uint32_t codePoint = 0; + i += DecodeUTF8Sequence(text, i, codePoint); + AppendWide(wide, codePoint); + } + + return wide; +} +//--------------------------------------------------------------------------- std::string WideToUTF8(std::wstring const& text) { uint32_t const replacementCharacter = 0xFFFD; diff --git a/src/ASWUnitTests_Utils.h b/src/ASWUnitTests_Utils.h index 9944c0c..89b0526 100644 --- a/src/ASWUnitTests_Utils.h +++ b/src/ASWUnitTests_Utils.h @@ -31,6 +31,13 @@ limitations under the License. namespace ASWUnitTests { +#if defined(_WIN32) +// Returns the arguments in a Windows command line such as GetCommandLineW()'s, without the program name, converted +// to UTF-8. Split by CommandLineToArgvW(), whose quoting rules are those of the C runtime's argv. Throws +// std::runtime_error if the command line can't be split. +std::vector CommandLineArguments(std::wstring const& commandLine); +#endif + // Returns the 32-bit FNV-1a hash of 'text''s bytes. Unlike std::hash, the result is the same with every compiler // and standard library. uint32_t FNV1aHash(std::string const& text); @@ -39,6 +46,18 @@ uint32_t FNV1aHash(std::string const& text); // given seed gives the same order with every compiler and standard library. 'count' must fit in 32 bits. std::vector ShuffledIndices(std::size_t count, unsigned int seed); +#if defined(_WIN32) +// Returns Windows' description of a GetLastError() code, e.g. "Access is denied.", or "error " when it has +// none. +std::string SystemErrorText(unsigned long error); +#endif + +// Converts UTF-8 text to wide text, UTF-16 where wchar_t is 16 bits (Windows) and UTF-32 where it's 32 bits +// (Linux). Each maximal ill-formed sequence (Unicode's term: a byte that can't start a sequence, or as much of a +// sequence as is valid before it breaks off) becomes one U+FFFD, the Unicode replacement character, as Unicode +// recommends. WideToUTF8() converts back. +std::wstring UTF8ToWide(std::string const& text); + // Converts wide text to UTF-8, reading it as UTF-16 where wchar_t is 16 bits (Windows) and as UTF-32 where it's // 32 bits (Linux). Anything that isn't a valid code point, such as an unpaired surrogate, becomes U+FFFD, the // Unicode replacement character. diff --git a/src/ASWUnitTests_Version.h b/src/ASWUnitTests_Version.h index c31f202..2117aa7 100644 --- a/src/ASWUnitTests_Version.h +++ b/src/ASWUnitTests_Version.h @@ -30,10 +30,10 @@ limitations under the License. // ASWUNITTESTS_VERSION_STRING is "MAJOR.MINOR.PATCH", followed by "-PRERELEASE" if the pre-release isn't empty; change // them together (a unit test checks that they match). #define ASWUNITTESTS_VERSION_MAJOR 1 -#define ASWUNITTESTS_VERSION_MINOR 4 +#define ASWUNITTESTS_VERSION_MINOR 5 #define ASWUNITTESTS_VERSION_PATCH 0 #define ASWUNITTESTS_VERSION_PRERELEASE "" -#define ASWUNITTESTS_VERSION_STRING "1.4.0" +#define ASWUNITTESTS_VERSION_STRING "1.5.0" namespace ASWUnitTests { diff --git a/src/main.cpp b/src/main.cpp index 947a46d..3a8e4fb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -34,6 +34,18 @@ limitations under the License. #include "ASWUnitTests_Exception.h" #include "ASWUnitTests_Handler.h" #include "ASWUnitTests_JUnitReport.h" +#include "ASWUnitTests_Utils.h" +//--------------------------------------------------------------------------- +#if defined(_WIN32) +# if !defined(NOMINMAX) +# define NOMINMAX +# endif +# if !defined(WIN32_LEAN_AND_MEAN) +# define WIN32_LEAN_AND_MEAN +# endif +# include +# include +#endif //--------------------------------------------------------------------------- using namespace ASWUnitTests; //--------------------------------------------------------------------------- @@ -54,8 +66,13 @@ int _dowildcard = 0; namespace { +//--------------------------------------------------------------------------- + void ApplyColorOptions(TCLIOptions const& options); void PauseIfRequested(bool pauseOnExit); +#if defined(_WIN32) +std::vector ProgramArguments(); +#endif int RunTests(TTestHandler& tester, TCLIOptions const& options, TestFilter const& filter, std::string const& filterDescription); //--------------------------------------------------------------------------- @@ -87,6 +104,22 @@ void PauseIfRequested(bool pauseOnExit) #endif } +//--------------------------------------------------------------------------- +#if defined(_WIN32) +/* + ProgramArguments + + The arguments for TCLIParser::ParseArguments(), in UTF-8, from the UTF-16 command line. The C runtime's argv + has them in the ANSI code page instead, which turns any character it lacks into a '?' or a lookalike. +*/ +std::vector ProgramArguments() +{ + std::vector args = CommandLineArguments(GetCommandLineW()); + args.insert(args.begin(), "ASWUnitTests"); // In place of the program name, which ParseArguments() skips. + return args; +} +#endif + //--------------------------------------------------------------------------- int RunTests(TTestHandler& tester, TCLIOptions const& options, TestFilter const& filter, std::string const& filterDescription) { @@ -119,11 +152,23 @@ int RunTests(TTestHandler& tester, TCLIOptions const& options, TestFilter const& //--------------------------------------------------------------------------- +#if defined(_WIN32) +// _tmain is main, or wmain where TCHAR is wchar_t, as in a RAD Studio project whose "TCHAR maps to" is wchar_t. It +// takes no arguments: on Windows they come from ProgramArguments(). +int _tmain() +#else int main(int argc, char* argv[]) +#endif { + // UTF-8 while the tests run, so non-ASCII test names and messages show correctly in the console. + TConsoleUTF8Output consoleOutput; TCLIOptions options; +#if defined(_WIN32) + std::optional const earlyExitCode = TCLIParser::ParseArguments(ProgramArguments(), options); +#else std::optional const earlyExitCode = TCLIParser::ParseArguments(argc, argv, options); +#endif if (earlyExitCode.has_value()) return *earlyExitCode; @@ -167,6 +212,9 @@ int main(int argc, char* argv[]) PauseIfRequested(options.PauseOnExit); + // Now, since the std::_Exit() calls below skip its destructor. + consoleOutput.Restore(); + if (returnCode == ExitCode_TestTimedOut) { // A test's worker thread may still be stuck (see TTestGroupBase::RunWithTimeout()) and was diff --git a/tests/Test_ASWTools_Random.cpp b/tests/Test_ASWTools_Random.cpp index 05eb1d8..71b057c 100644 --- a/tests/Test_ASWTools_Random.cpp +++ b/tests/Test_ASWTools_Random.cpp @@ -74,7 +74,6 @@ void TTest_TMersenneTwisterRandom::TearDown_Test(ITestCase& /*testCase*/) // /////// Begin tests after this line /////////////////////// //--------------------------------------------------------------------------- -// Test: Constructor sets default seed ASW_TEST(TTest_TMersenneTwisterRandom, Constructor_DefaultSeed) { // Arrange @@ -87,7 +86,11 @@ ASW_TEST(TTest_TMersenneTwisterRandom, Constructor_DefaultSeed) AssertEquals(static_cast(1), seed, __func__, __LINE__, "Default seed should be 1"); } //--------------------------------------------------------------------------- -// Test: Random() returns float in [0.0, 1.0] +/* + TTest_TMersenneTwisterRandom::Test_Random_FloatRange + + Test: Random() returns float in [0.0, 1.0] +*/ ASW_TEST(TTest_TMersenneTwisterRandom, Random_FloatRange) { // Arrange @@ -110,7 +113,11 @@ ASW_TEST(TTest_TMersenneTwisterRandom, Random_FloatRange) AssertTrue(inRange, __func__, __LINE__, "Random() should be in [0.0, 1.0]"); } //--------------------------------------------------------------------------- -// Test: Random(uint32_t n) returns values in [0, n) +/* + TTest_TMersenneTwisterRandom::Test_Random_UIntRange + + Test: Random(uint32_t n) returns values in [0, n) +*/ ASW_TEST(TTest_TMersenneTwisterRandom, Random_UIntRange) { // Arrange @@ -134,7 +141,6 @@ ASW_TEST(TTest_TMersenneTwisterRandom, Random_UIntRange) AssertTrue(inRange, __func__, __LINE__, "Random(n) should be in [0, n)"); } //--------------------------------------------------------------------------- -// Test: Randomize changes the seed ASW_TEST(TTest_TMersenneTwisterRandom, Randomize_ChangesSeed) { // Arrange @@ -150,7 +156,6 @@ ASW_TEST(TTest_TMersenneTwisterRandom, Randomize_ChangesSeed) AssertNotEquals(before, after, __func__, __LINE__, "Randomize should change the seed"); } //--------------------------------------------------------------------------- -// Test: Set and Get Seed ASW_TEST(TTest_TMersenneTwisterRandom, SetAndGetSeed) { // Arrange diff --git a/tests/Test_ASWTools_String.cpp b/tests/Test_ASWTools_String.cpp index e71d133..92be824 100644 --- a/tests/Test_ASWTools_String.cpp +++ b/tests/Test_ASWTools_String.cpp @@ -24,10 +24,10 @@ limitations under the License. // Module header #include "Test_ASWTools_String.h" //--------------------------------------------------------------------------- -#include "ASWUnitTests_Registry.h" -//--------------------------------------------------------------------------- #include //--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- #include "ASWTools_String.h" //--------------------------------------------------------------------------- using namespace ASWTools; diff --git a/tests/Test_ASWUnitTests_AutoTest.cpp b/tests/Test_ASWUnitTests_AutoTest.cpp index 7ceea1a..e0cf14b 100644 --- a/tests/Test_ASWUnitTests_AutoTest.cpp +++ b/tests/Test_ASWUnitTests_AutoTest.cpp @@ -24,14 +24,14 @@ limitations under the License. // Module header #include "Test_ASWUnitTests_AutoTest.h" //--------------------------------------------------------------------------- -#include "ASWUnitTests_Registry.h" -//--------------------------------------------------------------------------- #include #include #include #include #include //--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- #include "ASWUnitTests_Utils.h" //--------------------------------------------------------------------------- @@ -41,7 +41,17 @@ namespace ASWUnitTests namespace { -// 'names' joined with ", ", so a whole list can be compared, and shown on failure, as one string. +//--------------------------------------------------------------------------- + +std::string JoinNames(std::vector const& names); +std::vector RegisteredNames(ITestGroup& group); + +//--------------------------------------------------------------------------- +/* + JoinNames + + 'names' joined with ", ", so a whole list can be compared, and shown on failure, as one string. +*/ std::string JoinNames(std::vector const& names) { std::string joined; @@ -55,8 +65,12 @@ std::string JoinNames(std::vector const& names) return joined; } +//--------------------------------------------------------------------------- +/* + RegisteredNames -// The registered test names of 'group', in registration order. + The registered test names of 'group', in registration order. +*/ std::vector RegisteredNames(ITestGroup& group) { std::vector names; @@ -66,9 +80,33 @@ std::vector RegisteredNames(ITestGroup& group) return names; } +//--------------------------------------------------------------------------- } // namespace +//--------------------------------------------------------------------------- + +///////////////////////////////////////////////////////////////////////////// +// TFixture_AutoAndExplicitDuplicate +// +// A never-registered fixture group that registers the ASW_TEST test "Same" +// again in its constructor, so constructing it throws. +///////////////////////////////////////////////////////////////////////////// +class TFixture_AutoAndExplicitDuplicate : public TAutoTestGroup +{ +private: + typedef TAutoTestGroup inherited; + +public: + void Test_Same(); + +public: + TFixture_AutoAndExplicitDuplicate(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + ///////////////////////////////////////////////////////////////////////////// // TFixture_AutoTests @@ -100,6 +138,77 @@ class TFixture_AutoTests : public TAutoTestGroup void TearDown_Group() override {} }; + +///////////////////////////////////////////////////////////////////////////// +// TFixture_ExplicitDuplicate +// +// A never-registered fixture group, not a TAutoTestGroup, whose constructor +// registers two different methods under the same test name, so +// constructing it throws. +///////////////////////////////////////////////////////////////////////////// +class TFixture_ExplicitDuplicate : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_First() {} + void Test_Second() {} + +public: + TFixture_ExplicitDuplicate(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_TestCasesDuplicate +// +// A never-registered fixture group whose RegisterTestCases() name generator +// gives two rows the same label, so constructing it throws. +///////////////////////////////////////////////////////////////////////////// +class TFixture_TestCasesDuplicate : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_Case(int const& /*value*/) {} + +public: + TFixture_TestCasesDuplicate(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + +//--------------------------------------------------------------------------- + +///////////////////////////////////////////////////////////////////////////// +// TFixture_AutoAndExplicitDuplicate +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TFixture_AutoAndExplicitDuplicate::TFixture_AutoAndExplicitDuplicate() + : inherited("Fixture_AutoAndExplicitDuplicate") +{ + SetLogSuppressed(true); + + RegisterTest(&TFixture_AutoAndExplicitDuplicate::Test_Same, "Same"); +} +//--------------------------------------------------------------------------- +ASW_TEST(TFixture_AutoAndExplicitDuplicate, Same) +{ +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_AutoTests +///////////////////////////////////////////////////////////////////////////// + //--------------------------------------------------------------------------- TFixture_AutoTests::TFixture_AutoTests() : inherited("Fixture_AutoTests") @@ -137,64 +246,9 @@ void TFixture_AutoTests::Test_Explicit_B() //--------------------------------------------------------------------------- -///////////////////////////////////////////////////////////////////////////// -// TFixture_AutoAndExplicitDuplicate -// -// A never-registered fixture group that registers the ASW_TEST test "Same" -// again in its constructor, so constructing it throws. -///////////////////////////////////////////////////////////////////////////// -class TFixture_AutoAndExplicitDuplicate : public TAutoTestGroup -{ -private: - typedef TAutoTestGroup inherited; - -public: - void Test_Same(); - -public: - TFixture_AutoAndExplicitDuplicate(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; - -//--------------------------------------------------------------------------- -TFixture_AutoAndExplicitDuplicate::TFixture_AutoAndExplicitDuplicate() - : inherited("Fixture_AutoAndExplicitDuplicate") -{ - SetLogSuppressed(true); - - RegisterTest(&TFixture_AutoAndExplicitDuplicate::Test_Same, "Same"); -} -//--------------------------------------------------------------------------- -ASW_TEST(TFixture_AutoAndExplicitDuplicate, Same) -{ -} -//--------------------------------------------------------------------------- - - ///////////////////////////////////////////////////////////////////////////// // TFixture_ExplicitDuplicate -// -// A never-registered fixture group, not a TAutoTestGroup, whose constructor -// registers two different methods under the same test name, so -// constructing it throws. ///////////////////////////////////////////////////////////////////////////// -class TFixture_ExplicitDuplicate : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_First() {} - void Test_Second() {} - -public: - TFixture_ExplicitDuplicate(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_ExplicitDuplicate::TFixture_ExplicitDuplicate() @@ -210,24 +264,7 @@ TFixture_ExplicitDuplicate::TFixture_ExplicitDuplicate() ///////////////////////////////////////////////////////////////////////////// // TFixture_TestCasesDuplicate -// -// A never-registered fixture group whose RegisterTestCases() name generator -// gives two rows the same label, so constructing it throws. ///////////////////////////////////////////////////////////////////////////// -class TFixture_TestCasesDuplicate : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_Case(int const& /*value*/) {} - -public: - TFixture_TestCasesDuplicate(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_TestCasesDuplicate::TFixture_TestCasesDuplicate() diff --git a/tests/Test_ASWUnitTests_CLI.cpp b/tests/Test_ASWUnitTests_CLI.cpp index d153d41..f674729 100644 --- a/tests/Test_ASWUnitTests_CLI.cpp +++ b/tests/Test_ASWUnitTests_CLI.cpp @@ -27,8 +27,8 @@ limitations under the License. #include #include //--------------------------------------------------------------------------- -#include "ASWUnitTests_Handler.h" #include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- #include "ASWUnitTests_StdOutRedirect.h" //--------------------------------------------------------------------------- @@ -64,6 +64,8 @@ TTest_ASWUnitTests_CLI::TTest_ASWUnitTests_CLI() RegisterTest(&TTest_ASWUnitTests_CLI::Test_ParseArguments_Pause, "ParseArguments_Pause"); RegisterTest(&TTest_ASWUnitTests_CLI::Test_ParseArguments_ReportAndProjectName, "ParseArguments_ReportAndProjectName"); RegisterTest(&TTest_ASWUnitTests_CLI::Test_ParseArguments_Shuffle, "ParseArguments_Shuffle"); + RegisterTest(&TTest_ASWUnitTests_CLI::Test_ParseArguments_Strings_SkipProgramNameAndKeepUTF8, + "ParseArguments_Strings_SkipProgramNameAndKeepUTF8"); RegisterTest(&TTest_ASWUnitTests_CLI::Test_ParseArguments_TestTimeout, "ParseArguments_TestTimeout"); RegisterTest(&TTest_ASWUnitTests_CLI::Test_ParseColorMode, "ParseColorMode"); RegisterTest(&TTest_ASWUnitTests_CLI::Test_ParseUnsignedInt_Invalid, "ParseUnsignedInt_Invalid"); @@ -406,6 +408,28 @@ void TTest_ASWUnitTests_CLI::Test_ParseArguments_Shuffle() __func__, __LINE__, "--shuffle-seed with a non-numeric argument is rejected"); } //--------------------------------------------------------------------------- +void TTest_ASWUnitTests_CLI::Test_ParseArguments_Strings_SkipProgramNameAndKeepUTF8() +{ + // Arrange: a program name that looks like an option, then a filter and a report path with U+00E9 and U+03A9, + // which isn't in the Windows-1252 code page. + std::vector const args = { "--version", "--filter", "*\xC3\xA9*", "--report-junit", + "r\xCE\xA9.xml" }; + TCLIOptions options; + std::optional result; + + // Act + { + TStdOutRedirect const suppressOutput; + result = TCLIParser::ParseArguments(args, options); + } + + // Assert + CheckFalse(result.has_value(), __func__, __LINE__, "the program name isn't parsed, so --version doesn't exit"); + CheckEquals(std::string("*\xC3\xA9*"), options.FilterPattern, __func__, __LINE__, "the filter, byte for byte"); + CheckEquals(std::string("r\xCE\xA9.xml"), options.JunitReportPath, __func__, __LINE__, + "the report path, byte for byte"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_CLI::Test_ParseArguments_TestTimeout() { // Arrange diff --git a/tests/Test_ASWUnitTests_CLI.h b/tests/Test_ASWUnitTests_CLI.h index 4bd00ca..9b99ebb 100644 --- a/tests/Test_ASWUnitTests_CLI.h +++ b/tests/Test_ASWUnitTests_CLI.h @@ -62,6 +62,7 @@ class TTest_ASWUnitTests_CLI : public TTestGroupBase void Test_ParseArguments_Pause(); void Test_ParseArguments_ReportAndProjectName(); void Test_ParseArguments_Shuffle(); + void Test_ParseArguments_Strings_SkipProgramNameAndKeepUTF8(); void Test_ParseArguments_TestTimeout(); void Test_ParseColorMode(); void Test_ParseUnsignedInt_Invalid(); diff --git a/tests/Test_ASWUnitTests_Console.cpp b/tests/Test_ASWUnitTests_Console.cpp index 2f53d15..20549d5 100644 --- a/tests/Test_ASWUnitTests_Console.cpp +++ b/tests/Test_ASWUnitTests_Console.cpp @@ -24,9 +24,16 @@ limitations under the License. // Module header #include "Test_ASWUnitTests_Console.h" //--------------------------------------------------------------------------- -#include "ASWUnitTests_Registry.h" -//--------------------------------------------------------------------------- +#include #include +#if defined(_WIN32) +# if !defined(WIN32_LEAN_AND_MEAN) +# define WIN32_LEAN_AND_MEAN +# endif +# include +#endif +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" //--------------------------------------------------------------------------- #include "ASWUnitTests_Console.h" //--------------------------------------------------------------------------- @@ -42,6 +49,10 @@ namespace ASWUnitTests TTest_ASWUnitTests_Console::TTest_ASWUnitTests_Console() : inherited("ASWUnitTests_Console_Tests") { +#if defined(_WIN32) + RegisterTest(&TTest_ASWUnitTests_Console::Test_ConsoleUTF8Output_SetsUTF8UntilRestored, + "ConsoleUTF8Output_SetsUTF8UntilRestored"); +#endif RegisterTest(&TTest_ASWUnitTests_Console::Test_ParseColorName_RejectsInvalid, "ParseColorName_RejectsInvalid"); RegisterTest(&TTest_ASWUnitTests_Console::Test_ParseColorName_Recognizes, "ParseColorName_Recognizes"); RegisterTest(&TTest_ASWUnitTests_Console::Test_StripColor_RemovesColorCodesOnly, "StripColor_RemovesColorCodesOnly"); @@ -72,6 +83,49 @@ void TTest_ASWUnitTests_Console::TearDown_Test(ITestCase& /*testCase*/) // /////// Begin tests after this line /////////////////////// +//--------------------------------------------------------------------------- +#if defined(_WIN32) +void TTest_ASWUnitTests_Console::Test_ConsoleUTF8Output_SetsUTF8UntilRestored() +{ + // Arrange: the console runner has already set UTF-8 (see main()), so another code page is set first, and the + // original is put back at the end. + UINT const outerCodePage = GetConsoleOutputCP(); + if (outerCodePage == 0 || !SetConsoleOutputCP(437)) + Skip(__func__, __LINE__, "needs a console, which a GUI runner doesn't have"); + + // Act + UINT duringFirst = 0; + UINT afterRestore = 0; + UINT duringSecond = 0; + UINT afterDestructor = 0; + { + TConsoleUTF8Output first; + duringFirst = GetConsoleOutputCP(); + first.Restore(); + afterRestore = GetConsoleOutputCP(); + SetConsoleOutputCP(850); // Which the destructor, restoring again, mustn't undo. + } + UINT const afterFirstDestructor = GetConsoleOutputCP(); + { + TConsoleUTF8Output const second; + duringSecond = GetConsoleOutputCP(); + } + afterDestructor = GetConsoleOutputCP(); + + SetConsoleOutputCP(outerCodePage); + + // Assert + CheckEquals(static_cast(CP_UTF8), static_cast(duringFirst), __func__, __LINE__, + "UTF-8 while it exists"); + CheckEquals(437u, static_cast(afterRestore), __func__, __LINE__, "Restore() puts the original back"); + CheckEquals(850u, static_cast(afterFirstDestructor), __func__, __LINE__, + "and the destructor then leaves the console alone"); + CheckEquals(static_cast(CP_UTF8), static_cast(duringSecond), __func__, __LINE__, + "UTF-8 again for a second one"); + CheckEquals(850u, static_cast(afterDestructor), __func__, __LINE__, + "whose destructor puts the original back"); +} +#endif //--------------------------------------------------------------------------- void TTest_ASWUnitTests_Console::Test_ParseColorName_RejectsInvalid() { diff --git a/tests/Test_ASWUnitTests_Console.h b/tests/Test_ASWUnitTests_Console.h index ee21b9d..d6b6a14 100644 --- a/tests/Test_ASWUnitTests_Console.h +++ b/tests/Test_ASWUnitTests_Console.h @@ -37,6 +37,9 @@ class TTest_ASWUnitTests_Console : public TTestGroupBase typedef TTestGroupBase inherited; private: // Test methods +#if defined(_WIN32) + void Test_ConsoleUTF8Output_SetsUTF8UntilRestored(); +#endif void Test_ParseColorName_RejectsInvalid(); void Test_ParseColorName_Recognizes(); void Test_StripColor_RemovesColorCodesOnly(); diff --git a/tests/Test_ASWUnitTests_CrashGuard.cpp b/tests/Test_ASWUnitTests_CrashGuard.cpp index a51ce34..c5f0c9e 100644 --- a/tests/Test_ASWUnitTests_CrashGuard.cpp +++ b/tests/Test_ASWUnitTests_CrashGuard.cpp @@ -25,25 +25,44 @@ limitations under the License. #include "Test_ASWUnitTests_CrashGuard.h" //--------------------------------------------------------------------------- #include +#include +#include //--------------------------------------------------------------------------- -#include "ASWUnitTests_CrashGuard.h" #include "ASWUnitTests_Registry.h" //--------------------------------------------------------------------------- +#include "ASWUnitTests_CrashGuard.h" +//--------------------------------------------------------------------------- namespace { -// Written through by CrashInsideTryBlock() below. At namespace scope, not a local: GCC 13 and 14 at -// -O3 drop the write through a local 'int* volatile' initialized to nullptr in the same function, so -// nothing crashes (seen in the Linux CI build; GCC 15 and Clang keep it). +// Written through by CrashInsideTryBlock() and RecurseDeeplyThenCrash() below. volatile, so no +// compiler can prove the write is through a null pointer, which is undefined behavior it may assume +// never happens: Clang in a Release build (RAD Studio's bcc64x, llvm-mingw) treated the branch that +// wrote through a plain local null pointer in RecurseDeeplyThenCrash() as unreachable and dropped it +// with its depth check, so the recursion never stopped and overflowed the stack instead. At namespace +// scope, not a local: GCC 13 and 14 at -O3 drop the write through a local 'int* volatile' initialized +// to nullptr in the same function, so nothing crashes (seen in the Linux CI build; GCC 15 and Clang +// keep it). int* volatile g_NullPointer = nullptr; -// A try block's handler is linked into Win32's chain of exception handlers (NT_TIB::ExceptionList) -// while it's entered, so this crashes with the chain's head pointing into this function's stack -// frame, which the jump out of the crash abandons. A test run by the framework crashes like this, -// since the framework calls each test from inside its own try blocks. The crash is called through a -// volatile pointer so no compiler can see it can't throw and drop the try block. Used by -// Test_Run_AccessViolation_LaterExceptionStillCaught() below. +//--------------------------------------------------------------------------- + +void CrashInsideTryBlock(); +int RecurseDeeplyThenCrash(int depth); +int RecurseUntilStackOverflows(int depth); + +//--------------------------------------------------------------------------- +/* + CrashInsideTryBlock + + A try block's handler is linked into Win32's chain of exception handlers (NT_TIB::ExceptionList) + while it's entered, so this crashes with the chain's head pointing into this function's stack + frame, which the jump out of the crash abandons. A test run by the framework crashes like this, + since the framework calls each test from inside its own try blocks. The crash is called through a + volatile pointer so no compiler can see it can't throw and drop the try block. Used by + Test_Run_AccessViolation_LaterExceptionStillCaught() below. +*/ void CrashInsideTryBlock() { using TCrash = void (*)(); @@ -60,14 +79,42 @@ void CrashInsideTryBlock() { } } +//--------------------------------------------------------------------------- +/* + RecurseDeeplyThenCrash -// Not a tail call: 'buffer[0]' is read again after the recursive call returns, so no compiler can -// turn this into a loop with constant stack usage the way it could a simple tail-recursive -// version. Verified to genuinely overflow the stack (rather than hang) under -O2 and -O3 on GCC/ -// MinGW and under RAD Studio's bcc32c/bcc64, before this was trusted enough to commit as a -// permanent test - see the commit that added this file. The infinite recursion GCC/Clang/MSVC warn -// about here is the deliberate point, not a mistake, hence silencing just that one warning. -// bcc32c is Clang-based but, unlike bcc64x, doesn't define __GNUC__, so it needs its own branch. + A small per-frame footprint (unlike RecurseUntilStackOverflows() below), so 20 frames of this + use only a few tens of KB and never overflows the stack on its own; the crash at the bottom is + an ordinary access violation, deliberately reached from deep in the call stack. It writes through + g_NullPointer, not a local null pointer, so the optimizer can't drop it (see g_NullPointer). Used + by Test_Run_DeepAccessViolation_CaughtAndDoesNotAbort() below. +*/ +int RecurseDeeplyThenCrash(int depth) +{ + volatile char buffer[16]; + buffer[0] = static_cast(depth); + + if (depth >= 20) + { + *g_NullPointer = 1; + return buffer[0]; + } + + int const deeper = RecurseDeeplyThenCrash(depth + 1); + return deeper + buffer[0]; +} +//--------------------------------------------------------------------------- +/* + RecurseUntilStackOverflows + + Not a tail call: 'buffer[0]' is read again after the recursive call returns, so no compiler can + turn this into a loop with constant stack usage the way it could a simple tail-recursive + version. Verified to genuinely overflow the stack (rather than hang) under -O2 and -O3 on GCC/ + MinGW and under RAD Studio's bcc32c/bcc64, before this was trusted enough to commit as a + permanent test - see the commit that added this file. The infinite recursion GCC/Clang/MSVC warn + about here is the deliberate point, not a mistake, hence silencing just that one warning. + bcc32c is Clang-based but, unlike bcc64x, doesn't define __GNUC__, so it needs its own branch. +*/ #if defined(__GNUC__) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Winfinite-recursion" @@ -92,29 +139,12 @@ int RecurseUntilStackOverflows(int depth) #elif defined(_MSC_VER) # pragma warning(pop) #endif - -// A small per-frame footprint (unlike RecurseUntilStackOverflows() above), so 20 frames of this -// use only a few tens of KB and never overflows the stack on its own; the crash at the bottom is -// an ordinary access violation, deliberately reached from deep in the call stack. Used by -// Test_Run_DeepAccessViolation_CaughtAndDoesNotAbort() below. -int RecurseDeeplyThenCrash(int depth) -{ - volatile char buffer[16]; - buffer[0] = static_cast(depth); - - if (depth >= 20) - { - int* const nullPointer = nullptr; - *const_cast(nullPointer) = 1; - return buffer[0]; - } - - int const deeper = RecurseDeeplyThenCrash(depth + 1); - return deeper + buffer[0]; -} +//--------------------------------------------------------------------------- } // namespace +//--------------------------------------------------------------------------- + namespace ASWUnitTests { @@ -182,6 +212,8 @@ void TTest_ASWUnitTests_CrashGuard::Test_Run_AccessViolation_CaughtAndDoesNotAbo // Assert CheckTrue(result.Crashed, __func__, __LINE__, "a null-pointer write is caught"); #if defined(_WIN32) + CheckEquals(std::string("access violation"), result.Description, __func__, __LINE__, + "the null-pointer write itself crashed, not something the optimizer turned it into"); CheckFalse(result.ShouldAbortRun, __func__, __LINE__, "an ordinary access violation isn't severe enough to force an abort"); #else @@ -231,25 +263,15 @@ void TTest_ASWUnitTests_CrashGuard::Test_Run_AccessViolation_LaterExceptionStill /* TTest_ASWUnitTests_CrashGuard::Test_Run_DeepAccessViolation_CaughtAndDoesNotAbort - Not merely a handful of frames deep, deliberately: an ordinary crash a couple thousand frames - deep was found to be misclassified as abort-worthy specifically on RAD Studio's Clang-based - compilers (bcc32c/bcc64) in a Release build, while MinGW handled the exact same code correctly - at every depth tried. bcc64x remains unreliable at this specifically, in ways further tuning (a - larger SetThreadStackGuarantee(), a shallower depth) did not resolve cleanly - see Skip() below - and the commit that added it for the full investigation. This test is Skip()ped on that compiler - rather than deleted, since MinGW, bcc32c (and, unverified so far, MSVC) still need real coverage - of "an ordinary crash, meaningfully deep, still doesn't force an abort." bcc32c passes it in - Debug and Release builds, while bcc64x still fails it in a Release build (checked 2026-10-14, - RAD Studio 13.1). + Not merely a handful of frames deep, deliberately: an ordinary crash deep in the call stack is + still only an access violation, not a stack overflow, and mustn't force an abort. This test was + once skipped on RAD Studio's bcc64x, whose Release build reported a stack overflow: Clang had + dropped the helper's write through a plain null pointer as undefined behavior, and the depth + check with it, so the recursion never stopped (see g_NullPointer). The description is checked + so a crash that turns into a different fault fails with a clear message. */ void TTest_ASWUnitTests_CrashGuard::Test_Run_DeepAccessViolation_CaughtAndDoesNotAbort() { -#if defined(__BORLANDC__) && defined(_WIN64) - Skip(__func__, __LINE__, - "Unreliable on RAD Studio's bcc64x in a Release build: an ordinary crash this deep was " - "misclassified as abort-worthy. MinGW and bcc32c handle this correctly; investigation ongoing."); -#endif - // Arrange, Act TCrashGuardResult const result = TCrashGuard::Run([]() { @@ -259,6 +281,8 @@ void TTest_ASWUnitTests_CrashGuard::Test_Run_DeepAccessViolation_CaughtAndDoesNo // Assert CheckTrue(result.Crashed, __func__, __LINE__, "an access violation 20 stack frames deep is still caught"); #if defined(_WIN32) + CheckEquals(std::string("access violation"), result.Description, __func__, __LINE__, + "the null-pointer write at the bottom crashed, not a stack overflow from unbounded recursion"); CheckFalse(result.ShouldAbortRun, __func__, __LINE__, "an ordinary access violation, even this deep, still isn't severe enough to force an abort"); #else @@ -330,28 +354,28 @@ void TTest_ASWUnitTests_CrashGuard::Test_Run_NormalException_PropagatesUnaffecte /* TTest_ASWUnitTests_CrashGuard::Test_Run_StackOverflow_CaughtAndAbortsRun - Skip()ped on RAD Studio's Clang-based compilers (bcc32c/bcc64): in a Release build, a genuine - stack overflow here was found to not be caught at all, taking the whole process down for real, - despite the exact same mechanism working correctly for every other fault type on those same - compilers, and for this exact scenario on MinGW. A larger SetThreadStackGuarantee() didn't fix - it cleanly (see the commit that added this Skip() for the full investigation); this is Skip()ped - rather than deleted since MinGW (and, unverified so far, MSVC) still need real coverage of stack - overflow recovery, and because letting this run for real on an unfixed compiler would crash this - very test process rather than merely fail a test. + The stack overflows on a thread of its own, never on the thread running the tests. A thread's + stack takes one overflow at most: the first uses up its guard page, and a second one ends the + process. So on the thread running the tests, a second run of this test in one process, as a GUI + runner's Run Selected does, ended the GUI (0xC0000005 in the Win32 GUI runner). TCrashGuard::Run() + sets up its stack guarantee (Windows) and alternate signal stack (POSIX) on each thread it runs + on, so the new thread is guarded the same way. */ void TTest_ASWUnitTests_CrashGuard::Test_Run_StackOverflow_CaughtAndAbortsRun() { -#if defined(__BORLANDC__) - Skip(__func__, __LINE__, - "Unreliable on RAD Studio's bcc32c/bcc64 in a Release build: a genuine stack overflow was not " - "caught at all, crashing the process for real. MinGW handles this correctly; investigation ongoing."); -#endif - - // Arrange, Act - TCrashGuardResult const result = TCrashGuard::Run([]() + // Arrange + auto const overflow = []() { RecurseUntilStackOverflows(0); - }); + }; + TCrashGuardResult result; + auto const runGuarded = [&]() + { + result = TCrashGuard::Run(overflow); + }; + + // Act + std::thread(runGuarded).join(); // Assert CheckTrue(result.Crashed, __func__, __LINE__, "a genuine stack overflow is caught"); diff --git a/tests/Test_ASWUnitTests_Handler.cpp b/tests/Test_ASWUnitTests_Handler.cpp index e0259e6..b718918 100644 --- a/tests/Test_ASWUnitTests_Handler.cpp +++ b/tests/Test_ASWUnitTests_Handler.cpp @@ -24,15 +24,16 @@ limitations under the License. // Module header #include "Test_ASWUnitTests_Handler.h" //--------------------------------------------------------------------------- -#include "ASWUnitTests_Registry.h" -//--------------------------------------------------------------------------- #include +#include #include #include #include #include #include //--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- #include "ASWUnitTests_Console.h" #include "ASWUnitTests_Exception.h" #include "ASWUnitTests_Handler.h" @@ -45,39 +46,6 @@ namespace using namespace ASWUnitTests; -std::string JoinIndices(std::vector const& indices); -TestFilter WildcardMatchTestsFilter(bool alsoStdOutRedirectTests); - -//--------------------------------------------------------------------------- - -// 'indices' joined with ",", so a whole order can be compared, and shown on failure, as one string. -std::string JoinIndices(std::vector const& indices) -{ - std::string joined; - - for (size_t i = 0; i < indices.size(); ++i) - joined += (i == 0 ? std::string() : std::string(",")) + std::to_string(indices[i]); - - return joined; -} - -//--------------------------------------------------------------------------- - -// Matches only this group's own WildcardMatch_* tests (pure static-function checks, so safe to run from a -// nested TTestHandler inside this group, and never including the test doing the nesting), plus, when asked, -// every ASWUnitTests_StdOutRedirect_Tests test, a second group that runs after this one. -TestFilter WildcardMatchTestsFilter(bool alsoStdOutRedirectTests) -{ - return [alsoStdOutRedirectTests](std::string const& fullName) - { - return fullName.rfind("ASWUnitTests_Handler_Tests.WildcardMatch_", 0) == 0 || - (alsoStdOutRedirectTests && fullName.rfind("ASWUnitTests_StdOutRedirect_Tests.", 0) == 0); - }; -} - -//--------------------------------------------------------------------------- - - ///////////////////////////////////////////////////////////////////////////// // TFixture_GroupConstructorThrows // @@ -113,29 +81,6 @@ class TFixture_GroupConstructorThrows : public TTestGroupBase TFixture_GroupConstructorThrows::TThrow TFixture_GroupConstructorThrows::ConstructorThrows = TThrow::Nothing; -//--------------------------------------------------------------------------- -TFixture_GroupConstructorThrows::TFixture_GroupConstructorThrows() - : inherited("ASWUnitTests_Handler_ConstructorFixture") -{ - if (ConstructorThrows == TThrow::StdException) - throw std::runtime_error("deliberate constructor failure"); - -#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) - if (ConstructorThrows == TThrow::RTLException) - throw System::Sysutils::EConvertError("deliberate constructor failure"); -#endif - - RegisterTest(&TFixture_GroupConstructorThrows::Test_Passes, "Passes"); - - if (ConstructorThrows == TThrow::DuplicateTestName) - RegisterTest(&TFixture_GroupConstructorThrows::Test_Passes, "Passes"); -} -//--------------------------------------------------------------------------- -void TFixture_GroupConstructorThrows::Test_Passes() -{ -} -//--------------------------------------------------------------------------- - ///////////////////////////////////////////////////////////////////////////// // TFixture_GroupSetUpThrows @@ -177,41 +122,6 @@ TFixture_GroupSetUpThrows::TThrow TFixture_GroupSetUpThrows::SetUpGroupThrows = size_t TFixture_GroupSetUpThrows::TearDownGroupCalls = 0; size_t TFixture_GroupSetUpThrows::TestsRun = 0; -//--------------------------------------------------------------------------- -TFixture_GroupSetUpThrows::TFixture_GroupSetUpThrows() - : inherited("ASWUnitTests_Handler_GroupSetUpFixture") -{ - RegisterTest(&TFixture_GroupSetUpThrows::Test_First, "First"); - RegisterTest(&TFixture_GroupSetUpThrows::Test_Second, "Second"); -} -//--------------------------------------------------------------------------- -void TFixture_GroupSetUpThrows::SetUp_Group() -{ - if (SetUpGroupThrows == TThrow::StdException) - throw std::runtime_error("deliberate SetUp_Group() failure"); - -#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) - if (SetUpGroupThrows == TThrow::RTLException) - throw System::Sysutils::EConvertError("deliberate SetUp_Group() failure"); -#endif -} -//--------------------------------------------------------------------------- -void TFixture_GroupSetUpThrows::TearDown_Group() -{ - ++TearDownGroupCalls; -} -//--------------------------------------------------------------------------- -void TFixture_GroupSetUpThrows::Test_First() -{ - ++TestsRun; -} -//--------------------------------------------------------------------------- -void TFixture_GroupSetUpThrows::Test_Second() -{ - ++TestsRun; -} -//--------------------------------------------------------------------------- - ///////////////////////////////////////////////////////////////////////////// // TRecordingObserver @@ -249,8 +159,113 @@ class TRecordingObserver : public ITestRunObserver //--------------------------------------------------------------------------- +std::string JoinIndices(std::vector const& indices); +TestFilter WildcardMatchTestsFilter(bool alsoStdOutRedirectTests); + +//--------------------------------------------------------------------------- +/* + JoinIndices + + 'indices' joined with ",", so a whole order can be compared, and shown on failure, as one string. +*/ +std::string JoinIndices(std::vector const& indices) +{ + std::string joined; + + for (size_t i = 0; i < indices.size(); ++i) + joined += (i == 0 ? std::string() : std::string(",")) + std::to_string(indices[i]); + + return joined; +} +//--------------------------------------------------------------------------- +/* + WildcardMatchTestsFilter + + Matches only this group's own WildcardMatch_* tests (pure static-function checks, so safe to run from a + nested TTestHandler inside this group, and never including the test doing the nesting), plus, when asked, + every ASWUnitTests_StdOutRedirect_Tests test, a second group that runs after this one. +*/ +TestFilter WildcardMatchTestsFilter(bool alsoStdOutRedirectTests) +{ + return [alsoStdOutRedirectTests](std::string const& fullName) + { + return fullName.rfind("ASWUnitTests_Handler_Tests.WildcardMatch_", 0) == 0 || + (alsoStdOutRedirectTests && fullName.rfind("ASWUnitTests_StdOutRedirect_Tests.", 0) == 0); + }; +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_GroupConstructorThrows +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TFixture_GroupConstructorThrows::TFixture_GroupConstructorThrows() + : inherited("ASWUnitTests_Handler_ConstructorFixture") +{ + if (ConstructorThrows == TThrow::StdException) + throw std::runtime_error("deliberate constructor failure"); + +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) + if (ConstructorThrows == TThrow::RTLException) + throw System::Sysutils::EConvertError("deliberate constructor failure"); +#endif + + RegisterTest(&TFixture_GroupConstructorThrows::Test_Passes, "Passes"); + + if (ConstructorThrows == TThrow::DuplicateTestName) + RegisterTest(&TFixture_GroupConstructorThrows::Test_Passes, "Passes"); +} +//--------------------------------------------------------------------------- +void TFixture_GroupConstructorThrows::Test_Passes() +{ +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_GroupSetUpThrows +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TFixture_GroupSetUpThrows::TFixture_GroupSetUpThrows() + : inherited("ASWUnitTests_Handler_GroupSetUpFixture") +{ + RegisterTest(&TFixture_GroupSetUpThrows::Test_First, "First"); + RegisterTest(&TFixture_GroupSetUpThrows::Test_Second, "Second"); +} +//--------------------------------------------------------------------------- +void TFixture_GroupSetUpThrows::SetUp_Group() +{ + if (SetUpGroupThrows == TThrow::StdException) + throw std::runtime_error("deliberate SetUp_Group() failure"); + +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) + if (SetUpGroupThrows == TThrow::RTLException) + throw System::Sysutils::EConvertError("deliberate SetUp_Group() failure"); +#endif +} +//--------------------------------------------------------------------------- +void TFixture_GroupSetUpThrows::TearDown_Group() +{ + ++TearDownGroupCalls; +} +//--------------------------------------------------------------------------- +void TFixture_GroupSetUpThrows::Test_First() +{ + ++TestsRun; +} +//--------------------------------------------------------------------------- +void TFixture_GroupSetUpThrows::Test_Second() +{ + ++TestsRun; +} +//--------------------------------------------------------------------------- + } // namespace +//--------------------------------------------------------------------------- namespace ASWUnitTests { @@ -269,8 +284,11 @@ TTest_ASWUnitTests_Handler::TTest_ASWUnitTests_Handler() "CanonicalGroupOrder_RejectsInvalidSuitePath"); RegisterTest(&TTest_ASWUnitTests_Handler::Test_CanonicalGroupOrder_WithoutSuites_SortsByOrderThenName, "CanonicalGroupOrder_WithoutSuites_SortsByOrderThenName"); + RegisterTest(&TTest_ASWUnitTests_Handler::Test_GetAppBitsStr_MatchesPointerSize, "GetAppBitsStr_MatchesPointerSize"); + RegisterTest(&TTest_ASWUnitTests_Handler::Test_GetBuildOS_MatchesPlatform, "GetBuildOS_MatchesPlatform"); RegisterTest(&TTest_ASWUnitTests_Handler::Test_GetTests_MatchesGetAllTestFullNames, "GetTests_MatchesGetAllTestFullNames"); + RegisterTest(&TTest_ASWUnitTests_Handler::Test_Initialize_LogsVersionBitsAndOS, "Initialize_LogsVersionBitsAndOS"); RegisterTest(&TTest_ASWUnitTests_Handler::Test_Initialize_ReplacesGroupWhoseConstructorThrows, "Initialize_ReplacesGroupWhoseConstructorThrows"); RegisterTest(&TTest_ASWUnitTests_Handler::Test_Run_FailsGroupTestsWhenSetUpGroupThrows, @@ -374,6 +392,36 @@ void TTest_ASWUnitTests_Handler::Test_CanonicalGroupOrder_WithoutSuites_SortsByO "by order, then name, with tied groups kept in registration order, as before suites existed"); } //--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Handler::Test_GetAppBitsStr_MatchesPointerSize() +{ + // Arrange + std::string const expected = std::to_string(sizeof(void*) * CHAR_BIT) + "-bit"; + + // Act + std::string const bits = TTestHandler::GetAppBitsStr(); + + // Assert + CheckEquals(expected, bits, __func__, __LINE__, "the pointer size this test was built with"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Handler::Test_GetBuildOS_MatchesPlatform() +{ + // Arrange +#if defined(_WIN32) + std::string const expected = "Windows"; +#elif defined(__linux__) + std::string const expected = "Linux"; +#else + std::string const expected = "Unknown"; +#endif + + // Act + std::string const os = TTestHandler::GetBuildOS(); + + // Assert + CheckEquals(expected, os, __func__, __LINE__, "the operating system this test was built for"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_Handler::Test_GetTests_MatchesGetAllTestFullNames() { // Arrange @@ -406,6 +454,23 @@ void TTest_ASWUnitTests_Handler::Test_GetTests_MatchesGetAllTestFullNames() CheckTrue(foundThisTest, __func__, __LINE__, "this very test is listed, under its own group"); } //--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Handler::Test_Initialize_LogsVersionBitsAndOS() +{ + // Arrange + TTestHandler handler; + TRecordingObserver observer; + handler.SetRunObserver(&observer); + std::string const expected = TTestHandler::GetVersionFullStr() + " - " + TTestHandler::GetAppBitsStr() + " - " + + TTestHandler::GetBuildOS() + "\n"; + + // Act + handler.Initialize("Test_Initialize_LogsVersionBitsAndOS"); + + // Assert + CheckStartsWith(observer.LogText, expected, __func__, __LINE__, + "the banner's first line is the version, then the app bits, then the build OS"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_Handler::Test_Initialize_ReplacesGroupWhoseConstructorThrows() { typedef TFixture_GroupConstructorThrows::TThrow TThrow; diff --git a/tests/Test_ASWUnitTests_Handler.h b/tests/Test_ASWUnitTests_Handler.h index 710ad6a..c102765 100644 --- a/tests/Test_ASWUnitTests_Handler.h +++ b/tests/Test_ASWUnitTests_Handler.h @@ -40,7 +40,10 @@ class TTest_ASWUnitTests_Handler : public TTestGroupBase void Test_CanonicalGroupOrder_KeepsSuitesTogether(); void Test_CanonicalGroupOrder_RejectsInvalidSuitePath(); void Test_CanonicalGroupOrder_WithoutSuites_SortsByOrderThenName(); + void Test_GetAppBitsStr_MatchesPointerSize(); + void Test_GetBuildOS_MatchesPlatform(); void Test_GetTests_MatchesGetAllTestFullNames(); + void Test_Initialize_LogsVersionBitsAndOS(); void Test_Initialize_ReplacesGroupWhoseConstructorThrows(); void Test_Run_FailsGroupTestsWhenSetUpGroupThrows(); void Test_Run_ShuffleKeepsOrderWhenFiltered(); diff --git a/tests/Test_ASWUnitTests_JUnitReport.cpp b/tests/Test_ASWUnitTests_JUnitReport.cpp index f7eba0f..ae617a2 100644 --- a/tests/Test_ASWUnitTests_JUnitReport.cpp +++ b/tests/Test_ASWUnitTests_JUnitReport.cpp @@ -24,8 +24,6 @@ limitations under the License. // Module header #include "Test_ASWUnitTests_JUnitReport.h" //--------------------------------------------------------------------------- -#include "ASWUnitTests_Registry.h" -//--------------------------------------------------------------------------- #include #include #include @@ -36,15 +34,29 @@ limitations under the License. # include #endif //--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- #include "ASWUnitTests_JUnitReport.h" +#include "ASWUnitTests_Utils.h" //--------------------------------------------------------------------------- namespace { -// Portable enough for this one use: a per-process-unique suffix for the temp folder name below, so -// two OS processes running this suite concurrently (e.g. via --partition-index/--partition-count) -// never share, and can't race on, the same temp directory. +//--------------------------------------------------------------------------- + +unsigned long GetCurrentProcessIdPortable(); +std::string PathToUTF8(std::filesystem::path const& path); +std::string WithoutTimestamps(std::string xml); + +//--------------------------------------------------------------------------- +/* + GetCurrentProcessIdPortable + + Portable enough for this one use: a per-process-unique suffix for the temp folder name below, so + two OS processes running this suite concurrently (e.g. via --partition-index/--partition-count) + never share, and can't race on, the same temp directory. +*/ unsigned long GetCurrentProcessIdPortable() { #if defined(_WIN32) @@ -53,8 +65,27 @@ unsigned long GetCurrentProcessIdPortable() return static_cast(::getpid()); #endif } +//--------------------------------------------------------------------------- +/* + PathToUTF8 -// 'xml' with each timestamp attribute's value removed, so two reports built in different seconds compare equal. + The path in UTF-8, as TJUnitReportWriter::Write() takes it. On Windows, path::string() gives the ANSI code + page instead, and MSVC's throws for a character it lacks. +*/ +std::string PathToUTF8(std::filesystem::path const& path) +{ +#if defined(_WIN32) + return ASWUnitTests::WideToUTF8(path.wstring()); +#else + return path.string(); +#endif +} +//--------------------------------------------------------------------------- +/* + WithoutTimestamps + + 'xml' with each timestamp attribute's value removed, so two reports built in different seconds compare equal. +*/ std::string WithoutTimestamps(std::string xml) { std::string const attribute = "timestamp=\""; @@ -75,6 +106,8 @@ std::string WithoutTimestamps(std::string xml) } // namespace +//--------------------------------------------------------------------------- + namespace ASWUnitTests { @@ -92,6 +125,7 @@ TTest_ASWUnitTests_JUnitReport::TTest_ASWUnitTests_JUnitReport() RegisterTest(&TTest_ASWUnitTests_JUnitReport::Test_EscapeXml_PlainTextUnchanged, "EscapeXml_PlainTextUnchanged"); RegisterTest(&TTest_ASWUnitTests_JUnitReport::Test_EscapeXml_SpecialCharacters, "EscapeXml_SpecialCharacters"); RegisterTest(&TTest_ASWUnitTests_JUnitReport::Test_Write_ProducesReadableReport, "Write_ProducesReadableReport"); + RegisterTest(&TTest_ASWUnitTests_JUnitReport::Test_Write_TakesAUTF8Path, "Write_TakesAUTF8Path"); RegisterTest(&TTest_ASWUnitTests_JUnitReport::Test_Write_WritesBuildXMLUnchanged, "Write_WritesBuildXMLUnchanged"); } //--------------------------------------------------------------------------- @@ -101,26 +135,26 @@ TTest_ASWUnitTests_JUnitReport::~TTest_ASWUnitTests_JUnitReport() //--------------------------------------------------------------------------- void TTest_ASWUnitTests_JUnitReport::SetUp_Group() { - Log("Setting up temp group folder: " + m_GroupBaseTempDir.string()); + Log("Setting up temp group folder: " + PathToUTF8(m_GroupBaseTempDir)); std::filesystem::create_directories(m_GroupBaseTempDir); } //--------------------------------------------------------------------------- void TTest_ASWUnitTests_JUnitReport::SetUp_Test(ITestCase& testCase) { m_TestTempDir = m_GroupBaseTempDir / testCase.GetName(); - Log(" Setting up temp folder for " + testCase.GetName() + ": " + m_TestTempDir.string()); + Log(" Setting up temp folder for " + testCase.GetName() + ": " + PathToUTF8(m_TestTempDir)); std::filesystem::create_directories(m_TestTempDir); } //--------------------------------------------------------------------------- void TTest_ASWUnitTests_JUnitReport::TearDown_Group() { - Log("Cleaning up temp group folder: " + m_GroupBaseTempDir.string()); + Log("Cleaning up temp group folder: " + PathToUTF8(m_GroupBaseTempDir)); std::filesystem::remove_all(m_GroupBaseTempDir); } //--------------------------------------------------------------------------- void TTest_ASWUnitTests_JUnitReport::TearDown_Test(ITestCase& testCase) { - Log(" Cleaning up temp folder for " + testCase.GetName() + ": " + m_TestTempDir.string()); + Log(" Cleaning up temp folder for " + testCase.GetName() + ": " + PathToUTF8(m_TestTempDir)); std::filesystem::remove_all(m_TestTempDir); } //--------------------------------------------------------------------------- @@ -196,7 +230,7 @@ void TTest_ASWUnitTests_JUnitReport::Test_Write_ProducesReadableReport() }; // Act - bool const written = TJUnitReportWriter::Write(reportPath.string(), "MyProject", testCases); + bool const written = TJUnitReportWriter::Write(PathToUTF8(reportPath), "MyProject", testCases); // Assert AssertTrue(written, __func__, __LINE__, "Write() reports success"); @@ -214,6 +248,24 @@ void TTest_ASWUnitTests_JUnitReport::Test_Write_ProducesReadableReport() CheckContains(content, " const testCases = { { "GroupA", "TestPass", 0.0, TJUnitOutcome::Pass, "" } }; + + // Act + bool const written = TJUnitReportWriter::Write(PathToUTF8(reportPath), "MyProject", testCases); + + // Assert + CheckTrue(written, __func__, __LINE__, "Write() reports success"); + CheckTrue(std::filesystem::exists(reportPath), __func__, __LINE__, "the report has the name it was given"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_JUnitReport::Test_Write_WritesBuildXMLUnchanged() { // Arrange @@ -226,7 +278,7 @@ void TTest_ASWUnitTests_JUnitReport::Test_Write_WritesBuildXMLUnchanged() }; // Act - bool const written = TJUnitReportWriter::Write(reportPath.string(), "MyProject", testCases); + bool const written = TJUnitReportWriter::Write(PathToUTF8(reportPath), "MyProject", testCases); std::string const expected = TJUnitReportWriter::BuildXML("MyProject", testCases); // Assert diff --git a/tests/Test_ASWUnitTests_JUnitReport.h b/tests/Test_ASWUnitTests_JUnitReport.h index a0f3205..d89a848 100644 --- a/tests/Test_ASWUnitTests_JUnitReport.h +++ b/tests/Test_ASWUnitTests_JUnitReport.h @@ -47,6 +47,7 @@ class TTest_ASWUnitTests_JUnitReport : public TTestGroupBase void Test_EscapeXml_PlainTextUnchanged(); void Test_EscapeXml_SpecialCharacters(); void Test_Write_ProducesReadableReport(); + void Test_Write_TakesAUTF8Path(); void Test_Write_WritesBuildXMLUnchanged(); public: diff --git a/tests/Test_ASWUnitTests_StdOutRedirect.cpp b/tests/Test_ASWUnitTests_StdOutRedirect.cpp index c4738d0..0c7d997 100644 --- a/tests/Test_ASWUnitTests_StdOutRedirect.cpp +++ b/tests/Test_ASWUnitTests_StdOutRedirect.cpp @@ -24,10 +24,10 @@ limitations under the License. // Module header #include "Test_ASWUnitTests_StdOutRedirect.h" //--------------------------------------------------------------------------- -#include "ASWUnitTests_Registry.h" -//--------------------------------------------------------------------------- #include //--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- #include "ASWUnitTests_StdOutRedirect.h" //--------------------------------------------------------------------------- diff --git a/tests/Test_ASWUnitTests_Suite.cpp b/tests/Test_ASWUnitTests_Suite.cpp index c0b8652..963b9ea 100644 --- a/tests/Test_ASWUnitTests_Suite.cpp +++ b/tests/Test_ASWUnitTests_Suite.cpp @@ -24,14 +24,14 @@ limitations under the License. // Module header #include "Test_ASWUnitTests_Suite.h" //--------------------------------------------------------------------------- -#include "ASWUnitTests_Registry.h" -//--------------------------------------------------------------------------- #include #include #include #include #include //--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- #include "ASWUnitTests_Exception.h" #include "ASWUnitTests_Handler.h" //--------------------------------------------------------------------------- @@ -46,7 +46,17 @@ namespace char const* const ThisSuitePath = "Suites.Nested"; char const* const ThisQualifiedName = "Suites.Nested.ASWUnitTests_Suite_Tests"; -// 'names' joined with "|", so a whole list can be compared, and shown on failure, as one string. +//--------------------------------------------------------------------------- + +std::string JoinNames(std::vector const& names); +TestFilter WildcardFilter(std::string const& pattern); + +//--------------------------------------------------------------------------- +/* + JoinNames + + 'names' joined with "|", so a whole list can be compared, and shown on failure, as one string. +*/ std::string JoinNames(std::vector const& names) { std::string joined; @@ -56,8 +66,12 @@ std::string JoinNames(std::vector const& names) return joined; } +//--------------------------------------------------------------------------- +/* + WildcardFilter -// A filter matching full names against 'pattern' the way --filter does. + A filter matching full names against 'pattern' the way --filter does. +*/ TestFilter WildcardFilter(std::string const& pattern) { return [pattern](std::string const& fullTestName) @@ -65,9 +79,11 @@ TestFilter WildcardFilter(std::string const& pattern) return TTestHandler::WildcardMatch(pattern, fullTestName); }; } +//--------------------------------------------------------------------------- } // namespace +//--------------------------------------------------------------------------- ///////////////////////////////////////////////////////////////////////////// // TFixture_AutoSuiteGroup @@ -92,19 +108,6 @@ class TFixture_AutoSuiteGroup : public TAutoTestGroup void TearDown_Group() override {} }; -//--------------------------------------------------------------------------- -TFixture_AutoSuiteGroup::TFixture_AutoSuiteGroup() - : inherited(TSuitePath("Outer"), "Fixture_AutoSuiteGroup") -{ - SetLogSuppressed(true); -} -//--------------------------------------------------------------------------- -ASW_TEST(TFixture_AutoSuiteGroup, Auto) -{ - ++Calls; -} -//--------------------------------------------------------------------------- - ///////////////////////////////////////////////////////////////////////////// // TFixture_MinimalGroup @@ -222,6 +225,24 @@ class TFixture_SuiteGroup : public TTestGroupBase void TearDown_Group() override {} }; +//--------------------------------------------------------------------------- + +///////////////////////////////////////////////////////////////////////////// +// TFixture_AutoSuiteGroup +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TFixture_AutoSuiteGroup::TFixture_AutoSuiteGroup() + : inherited(TSuitePath("Outer"), "Fixture_AutoSuiteGroup") +{ + SetLogSuppressed(true); +} +//--------------------------------------------------------------------------- +ASW_TEST(TFixture_AutoSuiteGroup, Auto) +{ + ++Calls; +} +//--------------------------------------------------------------------------- namespace { @@ -262,6 +283,7 @@ class TRecordingObserver : public ITestRunObserver } // namespace +//--------------------------------------------------------------------------- ///////////////////////////////////////////////////////////////////////////// // TTest_ASWUnitTests_Suite diff --git a/tests/Test_ASWUnitTests_TestBase.cpp b/tests/Test_ASWUnitTests_TestBase.cpp index c073080..7266061 100644 --- a/tests/Test_ASWUnitTests_TestBase.cpp +++ b/tests/Test_ASWUnitTests_TestBase.cpp @@ -26,11 +26,13 @@ limitations under the License. //--------------------------------------------------------------------------- #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -46,6 +48,11 @@ limitations under the License. #if defined(__cpp_lib_string_view) # include #endif +#if defined(__BORLANDC__) && !defined(_WIN64) +# include // _control87(), for RoundingMode() +#endif +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" //--------------------------------------------------------------------------- #include "ASWUnitTests_CLI.h" #include "ASWUnitTests_Console.h" @@ -54,52 +61,1743 @@ limitations under the License. #include "ASWUnitTests_Registry.h" #include "ASWUnitTests_StdOutRedirect.h" //--------------------------------------------------------------------------- +#include "UT_Helper_ExceptionTypes.h" +#include "UT_Helper_StringChecks.h" +//--------------------------------------------------------------------------- + +namespace +{ + +using namespace ASWUnitTests; + +///////////////////////////////////////////////////////////////////////////// +// TFixture_BoolComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one deliberately failing test +// per bool overload of AssertEquals()/CheckEquals()/AssertNotEquals()/CheckNotEquals(), so +// Test_Equals_ShowsBoolValuesAsTrueOrFalse below can check that every failure message shows the +// values as "true"/"false" rather than "1"/"0". +///////////////////////////////////////////////////////////////////////////// +class TFixture_BoolComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertEquals_Different_Fails(); + void Test_AssertNotEquals_Same_Fails(); + void Test_CheckEquals_Different_Fails(); + void Test_CheckNotEquals_Same_Fails(); + +public: + TFixture_BoolComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_CStringComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing the C string overloads of +// AssertEquals()/CheckEquals()/AssertNotEquals()/CheckNotEquals(), narrow and wide. Without those +// overloads, two C strings convert to bool and match the bool overload, so the "DifferentLiterals" +// tests pass/fail the wrong way round. The "SameContentDifferentBuffers" tests compare a literal with +// a separate array holding the same text, so they fail if the pointers are compared instead of the +// content (two identical literals may share one address). Test names self-document expected outcome +// via NameEndsWith(), same as TFixture_ExceptionExpectations below. +///////////////////////////////////////////////////////////////////////////// +class TFixture_CStringComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertEquals_DifferentLiterals_Fails(); + void Test_AssertEquals_NullAndNonNull_Fails(); + void Test_AssertEquals_SameContentDifferentBuffers_Passes(); + void Test_AssertEquals_Wide_DifferentLiterals_Fails(); + void Test_AssertEquals_Wide_SameContentDifferentBuffers_Passes(); + void Test_AssertNotEquals_DifferentLiterals_Passes(); + void Test_AssertNotEquals_SameContentDifferentBuffers_Fails(); + void Test_AssertNotEquals_Wide_DifferentLiterals_Passes(); + void Test_AssertNotEquals_Wide_SameContentDifferentBuffers_Fails(); + void Test_CheckEquals_BothNull_Passes(); + void Test_CheckEquals_DifferentLiterals_Fails(); + void Test_CheckEquals_EmptyAndNull_Fails(); + void Test_CheckEquals_NullAndEmpty_Fails(); + void Test_CheckEquals_SameContentDifferentBuffers_Passes(); + void Test_CheckEquals_Wide_BothNull_Passes(); + void Test_CheckEquals_Wide_DifferentLiterals_Fails(); + void Test_CheckEquals_Wide_NullAndEmpty_Fails(); + void Test_CheckEquals_Wide_SameContentDifferentBuffers_Passes(); + void Test_CheckNotEquals_BothNull_Fails(); + void Test_CheckNotEquals_DifferentLiterals_Passes(); + void Test_CheckNotEquals_NullAndEmpty_Passes(); + void Test_CheckNotEquals_SameContentDifferentBuffers_Fails(); + void Test_CheckNotEquals_Wide_BothNull_Fails(); + void Test_CheckNotEquals_Wide_DifferentLiterals_Passes(); + void Test_CheckNotEquals_Wide_NullAndEmpty_Passes(); + void Test_CheckNotEquals_Wide_SameContentDifferentBuffers_Fails(); + +public: + TFixture_CStringComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_ContainsComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertContains()/CheckContains()/ +// AssertNotContains()/CheckNotContains(), narrow and wide. "Wide_NonASCII" checks that a wide failure shows +// its text as UTF-8. Test names self-document expected outcome via NameEndsWith(), same as +// TFixture_ExceptionExpectations below. +///////////////////////////////////////////////////////////////////////////// +class TFixture_ContainsComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertContains_Absent_Fails(); + void Test_AssertContains_Present_Passes(); + void Test_AssertContains_Wide_Absent_Fails(); + void Test_AssertContains_Wide_Present_Passes(); + void Test_AssertNotContains_Absent_Passes(); + void Test_AssertNotContains_Present_Fails(); + void Test_AssertNotContains_Wide_Absent_Passes(); + void Test_AssertNotContains_Wide_Present_Fails(); + void Test_CheckContains_Absent_Fails(); + void Test_CheckContains_DifferentCase_Fails(); + void Test_CheckContains_EmptySubstring_Passes(); + void Test_CheckContains_Present_Passes(); + void Test_CheckContains_SubstringLongerThanText_Fails(); + void Test_CheckContains_Wide_Absent_Fails(); + void Test_CheckContains_Wide_NonASCII_Fails(); + void Test_CheckContains_Wide_Present_Passes(); + void Test_CheckNotContains_Absent_Passes(); + void Test_CheckNotContains_EmptySubstring_Fails(); + void Test_CheckNotContains_Present_Fails(); + void Test_CheckNotContains_Wide_Absent_Passes(); + void Test_CheckNotContains_Wide_Present_Fails(); + +public: + TFixture_ContainsComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_ContainsICComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertContainsIC()/CheckContainsIC()/ +// AssertNotContainsIC()/CheckNotContainsIC(), narrow and wide. Only the ASCII letters A-Z are case-folded: the +// "NonLetters" tests use characters 0x20 apart, which a fold that just sets bit 0x20 would wrongly match, and the +// "NonASCII" tests show that accented letters keep their case. Test names self-document expected outcome via +// NameEndsWith(), same as TFixture_ExceptionExpectations below. +///////////////////////////////////////////////////////////////////////////// +class TFixture_ContainsICComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertContainsIC_Absent_Fails(); + void Test_AssertContainsIC_DifferentCase_Passes(); + void Test_AssertContainsIC_Wide_Absent_Fails(); + void Test_AssertContainsIC_Wide_DifferentCase_Passes(); + void Test_AssertNotContainsIC_Absent_Passes(); + void Test_AssertNotContainsIC_DifferentCase_Fails(); + void Test_AssertNotContainsIC_Wide_Absent_Passes(); + void Test_AssertNotContainsIC_Wide_DifferentCase_Fails(); + void Test_CheckContainsIC_Absent_Fails(); + void Test_CheckContainsIC_BothEmpty_Passes(); + void Test_CheckContainsIC_DifferentCase_Passes(); + void Test_CheckContainsIC_EmptySubstring_Passes(); + void Test_CheckContainsIC_EmptyText_Fails(); + void Test_CheckContainsIC_NonASCII_Fails(); + void Test_CheckContainsIC_NonLetters_Fails(); + void Test_CheckContainsIC_SubstringAtEnd_Passes(); + void Test_CheckContainsIC_SubstringLongerThanText_Fails(); + void Test_CheckContainsIC_Wide_Absent_Fails(); + void Test_CheckContainsIC_Wide_DifferentCase_Passes(); + void Test_CheckContainsIC_Wide_NonASCII_Fails(); + void Test_CheckNotContainsIC_Absent_Passes(); + void Test_CheckNotContainsIC_DifferentCase_Fails(); + void Test_CheckNotContainsIC_EmptySubstring_Fails(); + void Test_CheckNotContainsIC_Wide_Absent_Passes(); + void Test_CheckNotContainsIC_Wide_DifferentCase_Fails(); + +public: + TFixture_ContainsICComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_EmptyChecks +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEmpty()/CheckEmpty()/ +// AssertNotEmpty()/CheckNotEmpty() with strings, wide strings, containers with and without size(), C strings and +// character arrays (narrow and wide, null included), and (where the library has it) std::string_view, so +// Test_Empty_ShowsContentsOnFailure below can check what each failure shows. Test names self-document expected +// outcome via NameEndsWith(), same as TFixture_ExceptionExpectations below. +///////////////////////////////////////////////////////////////////////////// +class TFixture_EmptyChecks : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertEmpty_EmptyString_Passes(); + void Test_AssertEmpty_NullCString_Fails(); + void Test_AssertEmpty_String_Fails(); + void Test_AssertEmpty_WideArray_Passes(); + void Test_AssertNotEmpty_EmptyVector_Fails(); + void Test_AssertNotEmpty_EmptyWideCString_Fails(); + void Test_AssertNotEmpty_NullCString_Fails(); + void Test_AssertNotEmpty_Vector_Passes(); + void Test_AssertNotEmpty_WideCString_Passes(); + void Test_CheckEmpty_CharArray_Fails(); + void Test_CheckEmpty_EmptyCharArray_Passes(); + void Test_CheckEmpty_EmptyMap_Passes(); + void Test_CheckEmpty_ForwardList_Fails(); + void Test_CheckEmpty_NullWideCString_Fails(); + void Test_CheckEmpty_OneElement_Fails(); +#if defined(__cpp_lib_string_view) + void Test_CheckEmpty_StringView_Fails(); +#endif + void Test_CheckEmpty_Vector_Fails(); + void Test_CheckEmpty_WideCString_Fails(); + void Test_CheckEmpty_WideString_Fails(); + void Test_CheckNotEmpty_CString_Passes(); + void Test_CheckNotEmpty_EmptyCharPointer_Fails(); + void Test_CheckNotEmpty_EmptyString_Fails(); + void Test_CheckNotEmpty_NullCString_Fails(); + void Test_CheckNotEmpty_NullWideCString_Fails(); +#if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) + void Test_CheckNotEmpty_SourceLocation_Passes(); +#endif + void Test_CheckNotEmpty_String_Passes(); + void Test_CheckNotEmpty_WideArray_Passes(); + +public: + TFixture_EmptyChecks(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_EndsWithComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEndsWith()/CheckEndsWith()/ +// AssertNotEndsWith()/CheckNotEndsWith(), narrow and wide. "SuffixOnlyAtStart" and "SuffixOnlyInMiddle" catch a +// check that matches anywhere but the end, and "SuffixLongerThanText" one that doesn't check the length first. +// "Wide_NonASCII" checks that a wide failure shows its text as UTF-8. Test names self-document expected outcome via +// NameEndsWith(), same as TFixture_ExceptionExpectations below. +///////////////////////////////////////////////////////////////////////////// +class TFixture_EndsWithComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertEndsWith_Absent_Fails(); + void Test_AssertEndsWith_Present_Passes(); + void Test_AssertEndsWith_Wide_Absent_Fails(); + void Test_AssertEndsWith_Wide_Present_Passes(); + void Test_AssertNotEndsWith_Absent_Passes(); + void Test_AssertNotEndsWith_Present_Fails(); + void Test_AssertNotEndsWith_Wide_Absent_Passes(); + void Test_AssertNotEndsWith_Wide_Present_Fails(); + void Test_CheckEndsWith_Absent_Fails(); + void Test_CheckEndsWith_BothEmpty_Passes(); + void Test_CheckEndsWith_DifferentCase_Fails(); + void Test_CheckEndsWith_EmptySuffix_Passes(); + void Test_CheckEndsWith_EmptyText_Fails(); + void Test_CheckEndsWith_Present_Passes(); + void Test_CheckEndsWith_SuffixLongerThanText_Fails(); + void Test_CheckEndsWith_SuffixOnlyAtStart_Fails(); + void Test_CheckEndsWith_SuffixOnlyInMiddle_Fails(); + void Test_CheckEndsWith_Wide_Absent_Fails(); + void Test_CheckEndsWith_Wide_NonASCII_Fails(); + void Test_CheckEndsWith_Wide_Present_Passes(); + void Test_CheckNotEndsWith_Absent_Passes(); + void Test_CheckNotEndsWith_BothEmpty_Fails(); + void Test_CheckNotEndsWith_EmptySuffix_Fails(); + void Test_CheckNotEndsWith_Present_Fails(); + void Test_CheckNotEndsWith_SuffixLongerThanText_Passes(); + void Test_CheckNotEndsWith_SuffixOnlyAtStart_Passes(); + void Test_CheckNotEndsWith_SuffixOnlyInMiddle_Passes(); + void Test_CheckNotEndsWith_Wide_Absent_Passes(); + void Test_CheckNotEndsWith_Wide_Present_Fails(); + +public: + TFixture_EndsWithComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_EndsWithICComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEndsWithIC()/CheckEndsWithIC()/ +// AssertNotEndsWithIC()/CheckNotEndsWithIC(), narrow and wide. Only the ASCII letters A-Z are case-folded, as in +// TFixture_ContainsICComparisons above. Test names self-document expected outcome via NameEndsWith(), same as +// TFixture_ExceptionExpectations below. +///////////////////////////////////////////////////////////////////////////// +class TFixture_EndsWithICComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertEndsWithIC_Absent_Fails(); + void Test_AssertEndsWithIC_DifferentCase_Passes(); + void Test_AssertEndsWithIC_Wide_Absent_Fails(); + void Test_AssertEndsWithIC_Wide_DifferentCase_Passes(); + void Test_AssertNotEndsWithIC_Absent_Passes(); + void Test_AssertNotEndsWithIC_DifferentCase_Fails(); + void Test_AssertNotEndsWithIC_Wide_Absent_Passes(); + void Test_AssertNotEndsWithIC_Wide_DifferentCase_Fails(); + void Test_CheckEndsWithIC_Absent_Fails(); + void Test_CheckEndsWithIC_BothEmpty_Passes(); + void Test_CheckEndsWithIC_DifferentCase_Passes(); + void Test_CheckEndsWithIC_EmptySuffix_Passes(); + void Test_CheckEndsWithIC_EmptyText_Fails(); + void Test_CheckEndsWithIC_NonASCII_Fails(); + void Test_CheckEndsWithIC_NonLetters_Fails(); + void Test_CheckEndsWithIC_SuffixLongerThanText_Fails(); + void Test_CheckEndsWithIC_SuffixOnlyAtStart_Fails(); + void Test_CheckEndsWithIC_SuffixOnlyInMiddle_Fails(); + void Test_CheckEndsWithIC_Wide_Absent_Fails(); + void Test_CheckEndsWithIC_Wide_DifferentCase_Passes(); + void Test_CheckEndsWithIC_Wide_NonASCII_Fails(); + void Test_CheckNotEndsWithIC_Absent_Passes(); + void Test_CheckNotEndsWithIC_DifferentCase_Fails(); + void Test_CheckNotEndsWithIC_EmptySuffix_Fails(); + void Test_CheckNotEndsWithIC_SuffixLongerThanText_Passes(); + void Test_CheckNotEndsWithIC_SuffixOnlyAtStart_Passes(); + void Test_CheckNotEndsWithIC_Wide_Absent_Passes(); + void Test_CheckNotEndsWithIC_Wide_DifferentCase_Fails(); + +public: + TFixture_EndsWithICComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_EnumComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEquals()/CheckEquals()/ +// AssertNotEquals()/CheckNotEquals() with scoped enums (enum class), compared and shown by their underlying values, +// including char, signed and 64-bit unsigned underlying types; plus an unscoped enum with an int, which must still +// compile and compare as before. Test names self-document expected outcome via NameEndsWith(), same as +// TFixture_ExceptionExpectations below. +///////////////////////////////////////////////////////////////////////////// +class TFixture_EnumComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + + enum class TBig : uint64_t + { + One = 1, + Max = std::numeric_limits::max() + }; + + enum class TColor + { + Red, + Green, + Blue + }; + + enum class TLetter : char + { + A = 'A', + B = 'B' + }; + + enum TPlain + { + PlainA, + PlainB + }; + + enum class TSmall : int8_t + { + Negative = -1, + Positive = 1 + }; + +private: + void Test_AssertEquals_Different_Fails(); + void Test_AssertEquals_Same_Passes(); + void Test_AssertNotEquals_Different_Passes(); + void Test_AssertNotEquals_Same_Fails(); + void Test_CheckEquals_CharUnderlying_Fails(); + void Test_CheckEquals_Different_Fails(); + void Test_CheckEquals_NegativeUnderlying_Fails(); + void Test_CheckEquals_Same_Passes(); + void Test_CheckEquals_UInt64Underlying_Fails(); + void Test_CheckEquals_UnscopedEnumAndInt_Passes(); + void Test_CheckNotEquals_Different_Passes(); + void Test_CheckNotEquals_Same_Fails(); + +public: + TFixture_EnumComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_EqualsICComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEqualsIC()/CheckEqualsIC()/ +// AssertNotEqualsIC()/CheckNotEqualsIC(), narrow and wide. Only the ASCII letters A-Z are case-folded, as in +// TFixture_ContainsICComparisons above. "DifferentLength" catches a comparison that stops at the end of the shorter +// value. Test names self-document expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations below. +///////////////////////////////////////////////////////////////////////////// +class TFixture_EqualsICComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertEqualsIC_DifferentCase_Passes(); + void Test_AssertEqualsIC_DifferentText_Fails(); + void Test_AssertEqualsIC_Wide_DifferentCase_Passes(); + void Test_AssertEqualsIC_Wide_DifferentText_Fails(); + void Test_AssertNotEqualsIC_DifferentCase_Fails(); + void Test_AssertNotEqualsIC_DifferentText_Passes(); + void Test_AssertNotEqualsIC_Wide_DifferentCase_Fails(); + void Test_AssertNotEqualsIC_Wide_DifferentText_Passes(); + void Test_CheckEqualsIC_BothEmpty_Passes(); + void Test_CheckEqualsIC_DifferentCase_Passes(); + void Test_CheckEqualsIC_DifferentLength_Fails(); + void Test_CheckEqualsIC_DifferentText_Fails(); + void Test_CheckEqualsIC_EmptyAndNonEmpty_Fails(); + void Test_CheckEqualsIC_NonASCII_Fails(); + void Test_CheckEqualsIC_NonLetters_Fails(); + void Test_CheckEqualsIC_Wide_DifferentCase_Passes(); + void Test_CheckEqualsIC_Wide_DifferentText_Fails(); + void Test_CheckEqualsIC_Wide_NonASCII_Fails(); + void Test_CheckNotEqualsIC_DifferentCase_Fails(); + void Test_CheckNotEqualsIC_DifferentText_Passes(); + void Test_CheckNotEqualsIC_SameText_Fails(); + void Test_CheckNotEqualsIC_Wide_DifferentCase_Fails(); + void Test_CheckNotEqualsIC_Wide_DifferentText_Passes(); + +public: + TFixture_EqualsICComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixtureSpecificError +// +// Unrelated (sibling) exception type for TFixture_ExceptionExpectations +// below, so a "wrong type" scenario can throw a real type that isn't just a +// differently-worded same class. +///////////////////////////////////////////////////////////////////////////// +class TFixtureSpecificError : public std::runtime_error +{ +public: + explicit TFixtureSpecificError(std::string const& msg) + : std::runtime_error(msg) + { + } +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixtureOtherError +// +// Unrelated (sibling) exception type for TFixture_ExceptionExpectations +// below, so a "wrong type" scenario can throw a real type that isn't just a +// differently-worded same class. +///////////////////////////////////////////////////////////////////////////// +class TFixtureOtherError : public std::runtime_error +{ +public: + explicit TFixtureOtherError(std::string const& msg) + : std::runtime_error(msg) + { + } +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_ExceptionExpectations +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one test method per +// SetExceptionExpected()/Test() branch: no throw, a generic expectation satisfied by either a +// std::exception or a non-std::exception throw, and a specific-type expectation crossed with an +// exact type match, a polymorphic base-type match, a wrong sibling type, a non-std::exception +// throw, and a message substring that's present or absent; plus an Assert* failure while a generic +// or specific exception is expected, and an earlier Check* failure followed by the expected +// exception, on each path that would otherwise pass. Every test name ends with "_Passes" or +// "_Fails", read generically by Test_SetExceptionExpected_MatchesTypeAndMessage below via +// NameEndsWith() rather than a hand-maintained table. +///////////////////////////////////////////////////////////////////////////// +class TFixture_ExceptionExpectations : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_CheckFailedThenAssertFailedWhileExceptionExpected_Fails(); + void Test_CheckFailedThenGenericExpectedExceptionThrown_Fails(); + void Test_CheckFailedThenNonStdExpectedExceptionThrown_Fails(); + void Test_CheckFailedThenSpecificExpectedExceptionThrown_Fails(); + void Test_ExceptionExpectedButNoneThrown_Fails(); + void Test_GenericExceptionExpected_AssertFails_Fails(); + void Test_GenericExceptionExpected_NonStdExceptionThrown_Passes(); + void Test_GenericExceptionExpected_StdExceptionThrown_Passes(); + void Test_SpecificTypeExpected_AssertFails_Fails(); + void Test_SpecificTypeExpected_ExactTypeThrown_Passes(); + void Test_SpecificTypeExpected_MessageSubstringAbsent_Fails(); + void Test_SpecificTypeExpected_MessageSubstringPresent_Passes(); + void Test_SpecificTypeExpected_NonStdExceptionThrown_Fails(); + void Test_SpecificTypeExpected_PolymorphicBaseTypeThrown_Passes(); + void Test_SpecificTypeExpected_WrongSiblingTypeThrown_Fails(); + +public: + TFixture_ExceptionExpectations(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_FailCalls +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling Fail(), so Test_Fail_AbortsTestAsFailed +// below can check it fails the test, stops it (ReachedAfterFail stays false), still fails while an exception is +// expected, and keeps an earlier Check failure in the record. Test names self-document expected outcome via +// NameEndsWith(), same as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_FailCalls : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_Fail_AfterCheckFailure_Fails(); + void Test_Fail_Fails(); + void Test_Fail_WhileExceptionExpected_Fails(); + +public: + bool ReachedAfterFail = false; + + TFixture_FailCalls(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_FloatingPointEnvironment +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one test that records the floating-point +// environment it runs in: the rounding mode, and whether long double arithmetic keeps its precision. Used by +// Test_Run_GivesTimedTestTheCallersFloatingPointEnvironment below, which compares them with the calling thread's. +///////////////////////////////////////////////////////////////////////////// +class TFixture_FloatingPointEnvironment : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_RecordsEnvironment(); + +public: + bool LongDoublePrecisionInTest = false; // LongDoubleKeepsPrecision() in the test. + int RoundingModeInTest = -1; + + TFixture_FloatingPointEnvironment(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_IntegerComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing the integer template overloads +// of AssertEquals()/CheckEquals()/AssertNotEquals()/CheckNotEquals(), for two integer types that no +// fixed-width overload matches exactly. Without those overloads, most of these calls don't compile +// (ambiguous). The "NegativeAndUnsignedMax" tests catch a comparison that lets -1 wrap around to an +// unsigned maximum, and the "Int64Max" tests check the int64_t/uint64_t boundary. Test names +// self-document expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_IntegerComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertEquals_IntAndInt64Different_Fails(); + void Test_AssertEquals_IntAndInt64Equal_Passes(); + void Test_AssertNotEquals_IntAndInt64Equal_Fails(); + void Test_AssertNotEquals_NegativeAndUnsignedMax_Passes(); + void Test_CheckEquals_AboveInt64MaxAndInt64Max_Fails(); + void Test_CheckEquals_Int64MaxAndUint64_Passes(); + void Test_CheckEquals_IntAndInt64Different_Fails(); + void Test_CheckEquals_IntAndInt64Equal_Passes(); + void Test_CheckEquals_IntAndShort_Passes(); + void Test_CheckEquals_IntAndUnsigned_Passes(); + void Test_CheckEquals_LongAndLong_Passes(); + void Test_CheckEquals_LongLongAndLongLong_Passes(); + void Test_CheckEquals_NegativeAndUint64Max_Fails(); + void Test_CheckEquals_NegativeAndUnsignedMax_Fails(); + void Test_CheckEquals_UnsignedLongAndUnsignedLong_Passes(); + void Test_CheckNotEquals_IntAndInt64Equal_Fails(); + void Test_CheckNotEquals_NegativeAndUint64Max_Passes(); + void Test_CheckNotEquals_NegativeAndUnsignedMax_Passes(); + +public: + TFixture_IntegerComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_LateFinish +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one test that sleeps past a one second +// timeout, then fails a Check, logs a line and ends, followed by one that must never run. Logging is left on, for a +// run observer to collect. Used by Test_Run_IgnoresTestThatEndsAfterItsTimeout below to prove an abandoned test that +// ends after all changes neither the results nor the log. +///////////////////////////////////////////////////////////////////////////// +class TFixture_LateFinish : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_EndsJustAfterTimeout(); + void Test_NeverRuns(); + +public: + std::atomic Ended{ false }; // Set as the first test ends, after its Check failure and its log line. + +public: + TFixture_LateFinish(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_MatchesChecks +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertMatches()/CheckMatches()/ +// AssertNotMatches()/CheckNotMatches(), narrow and wide, so Test_Matches_MatchesWholeTextAndShowsPattern below can +// check their outcomes and messages. "PartialMatch" shows the whole text must match, and "InvalidPattern" that a bad +// pattern fails the check instead of throwing std::regex_error. Test names self-document expected outcome via +// NameEndsWith(), same as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_MatchesChecks : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertMatches_InvalidPattern_Fails(); + void Test_AssertMatches_Matching_Passes(); + void Test_AssertMatches_NotMatching_Fails(); + void Test_AssertNotMatches_Matching_Fails(); + void Test_AssertNotMatches_NotMatching_Passes(); + void Test_CheckMatches_InvalidPattern_Fails(); + void Test_CheckMatches_Matching_Passes(); + void Test_CheckMatches_PartialMatch_Fails(); + void Test_CheckMatches_Wide_Matching_Passes(); + void Test_CheckMatches_Wide_NotMatching_Fails(); + void Test_CheckNotMatches_InvalidPattern_Fails(); + void Test_CheckNotMatches_Matching_Fails(); + void Test_CheckNotMatches_NotMatching_Passes(); + +public: + TFixture_MatchesChecks(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_MemoryComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEqualsMem()/CheckEqualsMem()/ +// AssertNotEqualsMem()/CheckNotEqualsMem(), so Test_EqualsMem_ShowsFirstDifferingBytes below can check their outcomes +// and messages: a difference at the start, in the middle and past 16 bytes, zero bytes, the same pointer twice, and +// null pointers. Test names self-document expected outcome via NameEndsWith(), same as +// TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_MemoryComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertEqualsMem_Different_Fails(); + void Test_AssertEqualsMem_Same_Passes(); + void Test_AssertNotEqualsMem_Different_Passes(); + void Test_AssertNotEqualsMem_Same_Fails(); + void Test_CheckEqualsMem_ActualNull_Fails(); + void Test_CheckEqualsMem_BothNull_Passes(); + void Test_CheckEqualsMem_DifferentLater_Fails(); + void Test_CheckEqualsMem_LongDifference_Fails(); + void Test_CheckEqualsMem_SameBuffer_Passes(); + void Test_CheckEqualsMem_ZeroSize_Passes(); + void Test_CheckNotEqualsMem_Different_Passes(); + void Test_CheckNotEqualsMem_NullAndBuffer_Passes(); + void Test_CheckNotEqualsMem_Same_Fails(); + +public: + TFixture_MemoryComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_MixedOutcomes +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one test method per outcome +// TTestGroupBase can produce, plus one that fails twice in a row to prove a Check failure doesn't +// abort the rest of the test the way an Assert failure does, and one whose Check failure is +// followed by an Assert failure, so both end up in its record's detail. Constructed and run directly by +// TTest_ASWUnitTests_TestBase's own test methods below, so its deliberate failures/skip only ever +// affect this fixture's own Results() - never the real, self-registered suite running it. +///////////////////////////////////////////////////////////////////////////// +class TFixture_MixedOutcomes : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_ContinuesAfterCheckFailure(); + void Test_FailViaAssert(); + void Test_FailViaCheck(); + void Test_FailViaCheckThenAssert(); + void Test_Pass(); + void Test_Skip(); + +public: + bool ReachedSecondCheck = false; + +public: + explicit TFixture_MixedOutcomes(bool suppressLog = true); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_NearComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing CheckNear()/AssertNear()/ +// CheckNotNear()'s tolerance boundary: the implementation passes only when diff <= tolerance, so a +// diff exactly equal to tolerance must still count as "near". Test names self-document expected +// outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_NearComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertNear_JustOutsideTolerance_Fails(); + void Test_CheckNear_AtToleranceBoundary_Passes(); + void Test_CheckNear_Double_AtToleranceBoundary_Passes(); + void Test_CheckNear_JustOutsideTolerance_Fails(); + void Test_CheckNotNear_AtToleranceBoundary_Fails(); + void Test_CheckNotNear_JustOutsideTolerance_Passes(); + +public: + TFixture_NearComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_NearMixedTypes +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling the Near and NotNear methods with mixed +// types (integers, float, double, long double, mixed signedness) and with invalid tolerances, so +// Test_Near_ComparesMixedTypesAndRejectsInvalidTolerance below can check each outcome and message. It overrides +// CheckNear(double, ...) to count its calls: a call whose common type is double must still reach it. Test names +// self-document expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_NearMixedTypes : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertNear_Integers_Fails(); + void Test_AssertNear_NegativeTolerance_Fails(); + void Test_AssertNotNear_FloatNaNTolerance_Fails(); + void Test_AssertNotNear_MixedSigns_Passes(); + void Test_CheckNear_DoubleAndFloat_Fails(); + void Test_CheckNear_DoubleAndFloat_Passes(); + void Test_CheckNear_FloatsAndDoubleTolerance_Passes(); + void Test_CheckNear_IntegerBoundary_Passes(); + void Test_CheckNear_IntegerExtremes_Passes(); + void Test_CheckNear_IntegerSumOverflows_Fails(); + void Test_CheckNear_Integers_Fails(); + void Test_CheckNear_LongDouble_Fails(); + void Test_CheckNear_LongDouble_Passes(); + void Test_CheckNear_MixedSigns_Passes(); + void Test_CheckNear_NaNAndFloat_Fails(); + void Test_CheckNear_NaNTolerance_Fails(); + void Test_CheckNear_NegativeIntegerTolerance_Fails(); +#if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) + void Test_CheckNear_SourceLocation_Passes(); +#endif + void Test_CheckNotNear_DoubleAndFloat_Fails(); + void Test_CheckNotNear_Integers_Fails(); + void Test_CheckNotNear_LongDoublePrecision_Passes(); + void Test_CheckNotNear_NegativeTolerance_Fails(); + +protected: + using inherited::CheckNear; + + void CheckNear(double expected, double actual, double tolerance, std::string const& method, int line, + std::string const& msg) override; + +public: + int DoubleCheckNearCalls = 0; // Calls of CheckNear(double, ...), the override above. + + TFixture_NearMixedTypes(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_NearNonFiniteValues +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling the Near and NotNear methods, float and +// double, with NaNs and infinities, so Test_Near_HandlesNaNAndInfinity below can check that a NaN is near nothing (not +// even another NaN, however large the tolerance) and that equal infinities are near each other, though a NaN +// difference or an infinite one compares false with any tolerance. Test names self-document expected outcome via +// NameEndsWith(), same as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_NearNonFiniteValues : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertNear_NaN_Fails(); + void Test_AssertNotNear_EqualInfinities_Fails(); + void Test_AssertNotNear_NaN_Passes(); + void Test_CheckNear_BothNaN_Fails(); + void Test_CheckNear_EqualInfinities_Passes(); + void Test_CheckNear_EqualNegativeInfinities_Passes(); + void Test_CheckNear_Float_EqualInfinities_Passes(); + void Test_CheckNear_Float_NaN_Fails(); + void Test_CheckNear_InfinityAndFinite_Fails(); + void Test_CheckNear_NaNActual_Fails(); + void Test_CheckNear_NaNExpected_Fails(); + void Test_CheckNear_OppositeInfinities_Fails(); + void Test_CheckNotNear_BothNaN_Passes(); + void Test_CheckNotNear_EqualInfinities_Fails(); + void Test_CheckNotNear_Float_NaN_Passes(); + void Test_CheckNotNear_InfinityAndFinite_Passes(); + void Test_CheckNotNear_NaN_Passes(); + +public: + TFixture_NearNonFiniteValues(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_NullCStringTextChecks +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling the C string overloads of the IC Equals, +// Contains, StartsWith and EndsWith methods, Check and Assert, narrow and wide, and their Not forms, each with a null +// text, a null part, both null, and a matching non-null pair, so Test_NullCStrings_MatchNothingInTextChecks below +// can check that a null C string matches nothing in the text checks and equals only another null one in the IC +// Equals ones. Test names self-document expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations +// above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_NullCStringTextChecks : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + + // How a method treats a null C string, which decides each case's expected outcome. + enum class TNullRule + { + NeverMatches, // Contains, StartsWith, EndsWith: fails for any null + NotNeverMatches, // their Not forms: passes for any null + EqualsOnlyNull, // EqualsIC: fails for one null, passes for two + NotEqualsOnlyNull // NotEqualsIC: passes for one null, fails for two + }; + + char const* const m_NullNarrow = nullptr; + wchar_t const* const m_NullWide = nullptr; + + // Registers 'name''s eight cases: null text, null part, both null and a matching pair, narrow and wide. 'call' + // takes the text and the part, as two char const* or two wchar_t const*, and calls the method with them. The + // matching pair is "ABC" and "abc" for an IC method and "abc" and "abc" otherwise, which every method matches. + template + void RegisterCases(std::string const& name, TNullRule rule, bool ignoreCase, TCall call) + { + bool const negated = rule == TNullRule::NotNeverMatches || rule == TNullRule::NotEqualsOnlyNull; + char const* const outcomeWithOneNull = + (rule == TNullRule::NeverMatches || rule == TNullRule::EqualsOnlyNull) ? "_Fails" : "_Passes"; + char const* const outcomeWithBothNull = + (rule == TNullRule::NeverMatches || rule == TNullRule::NotEqualsOnlyNull) ? "_Fails" : "_Passes"; + char const* const outcomeWhenMatching = negated ? "_Fails" : "_Passes"; + char const* const text = ignoreCase ? "ABC" : "abc"; + wchar_t const* const wideText = ignoreCase ? L"ABC" : L"abc"; + + RegisterTest([this, call]() { call(m_NullNarrow, "abc"); }, name + "_Narrow_NullText" + outcomeWithOneNull); + RegisterTest([call, text]() { call(text, static_cast(nullptr)); }, + name + "_Narrow_NullPart" + outcomeWithOneNull); + RegisterTest([this, call]() { call(m_NullNarrow, m_NullNarrow); }, name + "_Narrow_BothNull" + outcomeWithBothNull); + RegisterTest([call, text]() { call(text, "abc"); }, name + "_Narrow_Matching" + outcomeWhenMatching); + RegisterTest([this, call]() { call(m_NullWide, L"abc"); }, name + "_Wide_NullText" + outcomeWithOneNull); + RegisterTest([call, wideText]() { call(wideText, static_cast(nullptr)); }, + name + "_Wide_NullPart" + outcomeWithOneNull); + RegisterTest([this, call]() { call(m_NullWide, m_NullWide); }, name + "_Wide_BothNull" + outcomeWithBothNull); + RegisterTest([call, wideText]() { call(wideText, L"abc"); }, name + "_Wide_Matching" + outcomeWhenMatching); + } + +public: + TFixture_NullCStringTextChecks(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_NullChecks +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling AssertNull()/CheckNull()/AssertNotNull()/ +// CheckNotNull() with null and non-null raw pointers, smart pointers, std::function objects, C strings and a nullptr +// literal, so Test_NullNotNull_FailureNamesTheExpectedValue below can check each is compared with nullptr and that +// each failure states the value that was expected. Test names self-document expected outcome via NameEndsWith(), same +// as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_NullChecks : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertNotNull_NonNull_Passes(); + void Test_AssertNotNull_Null_Fails(); + void Test_AssertNull_NonNull_Fails(); + void Test_AssertNull_Null_Passes(); + void Test_CheckNotNull_EmptyCString_Passes(); + void Test_CheckNotNull_EmptyFunction_Fails(); + void Test_CheckNotNull_NonNull_Passes(); + void Test_CheckNotNull_Null_Fails(); + void Test_CheckNotNull_SharedPtr_Passes(); + void Test_CheckNull_EmptyFunction_Passes(); + void Test_CheckNull_NonNull_Fails(); + void Test_CheckNull_NullCString_Passes(); + void Test_CheckNull_Null_Passes(); + void Test_CheckNull_NullptrLiteral_Passes(); + void Test_CheckNull_UniquePtr_Fails(); + +public: + TFixture_NullChecks(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_OrderRecorder +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group of 8 trivially-passing tests (A-H) +// that each record their own name into a caller-owned vector when run, for verifying Run()'s +// --shuffle ordering and TestFilter application. 8 tests keeps the odds of a shuffled order +// coincidentally matching the unshuffled one at 1-in-40320 or better, low enough that +// Test_Run_ShuffleSeedProducesDeterministicOrder below won't flake. +///////////////////////////////////////////////////////////////////////////// +class TFixture_OrderRecorder : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + std::vector& m_ExecutionOrder; + +private: + void Test_A(); + void Test_B(); + void Test_C(); + void Test_D(); + void Test_E(); + void Test_F(); + void Test_G(); + void Test_H(); + +public: + explicit TFixture_OrderRecorder(std::vector& executionOrder); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_OrderingComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing the GreaterThan/GreaterThanOrEqual/ +// LessThan/LessThanOrEqual methods, each called as (value, bound). The "Negative...Max" and "...MaxAndNegative" tests +// catch a comparison that lets -1 wrap around to an unsigned maximum (the built-in -1 < 4294967295u is false), the +// "Uint64Max" ones the case where neither value fits the other's type, and the NaN ones that a NaN fails every check. +// Test names self-document expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_OrderingComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertGreaterThanOrEqual_Equal_Passes(); + void Test_AssertGreaterThanOrEqual_Less_Fails(); + void Test_AssertGreaterThan_Equal_Fails(); + void Test_AssertGreaterThan_Greater_Passes(); + void Test_AssertLessThanOrEqual_Equal_Passes(); + void Test_AssertLessThanOrEqual_Greater_Fails(); + void Test_AssertLessThan_Equal_Fails(); + void Test_AssertLessThan_Less_Passes(); + void Test_CheckGreaterThanOrEqual_Equal_Passes(); + void Test_CheckGreaterThanOrEqual_Greater_Passes(); + void Test_CheckGreaterThanOrEqual_Less_Fails(); + void Test_CheckGreaterThanOrEqual_NaN_Fails(); + void Test_CheckGreaterThan_AboveInt64MaxAndInt64Max_Passes(); + void Test_CheckGreaterThan_DoubleAndInt_Passes(); + void Test_CheckGreaterThan_Double_Fails(); + void Test_CheckGreaterThan_Equal_Fails(); + void Test_CheckGreaterThan_Float_Fails(); + void Test_CheckGreaterThan_Greater_Passes(); + void Test_CheckGreaterThan_Infinity_Passes(); + void Test_CheckGreaterThan_LongDouble_Fails(); + void Test_CheckGreaterThan_Less_Fails(); + void Test_CheckGreaterThan_Uint64MaxAndNegative_Passes(); + void Test_CheckGreaterThan_UnsignedMaxAndNegative_Passes(); + void Test_CheckLessThanOrEqual_Equal_Passes(); + void Test_CheckLessThanOrEqual_Greater_Fails(); + void Test_CheckLessThanOrEqual_IntAndInt64_Passes(); + void Test_CheckLessThanOrEqual_Less_Passes(); + void Test_CheckLessThanOrEqual_NaNBound_Fails(); + void Test_CheckLessThan_CloseDoubles_Fails(); + void Test_CheckLessThan_Equal_Fails(); + void Test_CheckLessThan_Greater_Fails(); + void Test_CheckLessThan_Less_Passes(); + void Test_CheckLessThan_NaN_Fails(); + void Test_CheckLessThan_NegativeAndUint64Max_Passes(); + void Test_CheckLessThan_NegativeAndUnsignedMax_Passes(); + void Test_CheckLessThan_Uint64MaxAndNegative_Fails(); + +public: + TFixture_OrderingComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_PointerComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEquals()/CheckEquals()/ +// AssertNotEquals()/CheckNotEquals() with two pointers. Without the pointer overloads, two pointers convert to bool +// and match the bool overload, so two different non-null pointers compare equal and the "DifferentPointers" tests +// pass/fail the wrong way round. The tests compare the addresses of First and Second, so +// Test_Equals_ComparesPointersByAddress below can check the addresses a failure shows. Test names self-document +// expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_PointerComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + + struct TBase + { + int Value = 0; + }; + + struct TDerived : TBase + { + }; + + // Different bodies, so a linker that merges identical functions (e.g. MSVC's /OPT:ICF) can't give them one address. + static int FirstFunction() { return 1; } + static int SecondFunction() { return 2; } + +private: + void Test_AssertEquals_DifferentPointers_Fails(); + void Test_AssertEquals_SamePointer_Passes(); + void Test_AssertNotEquals_DifferentPointers_Passes(); + void Test_AssertNotEquals_SamePointer_Fails(); + void Test_CheckEquals_BaseAndDerived_Passes(); + void Test_CheckEquals_DifferentFunctions_Fails(); + void Test_CheckEquals_DifferentPointers_Fails(); + void Test_CheckEquals_MutableCStringBuffer_Passes(); + void Test_CheckEquals_NullAndNonNull_Fails(); + void Test_CheckEquals_SamePointer_Passes(); + void Test_CheckEquals_VoidAndTyped_Passes(); + void Test_CheckNotEquals_DifferentPointers_Passes(); + void Test_CheckNotEquals_SamePointer_Fails(); + +public: + int First = 0; + int Second = 0; + + TFixture_PointerComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_SameChecks +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertSame()/CheckSame()/AssertNotSame()/ +// CheckNotSame() with raw pointers, arrays, smart pointers and C strings, which unlike CheckEquals() are compared by +// address, not content. The basic tests compare the addresses of First and Second, so +// Test_Same_ComparesAddressesNotContent below can check the addresses a failure shows. Test names self-document +// expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_SameChecks : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertNotSame_DifferentObjects_Passes(); + void Test_AssertNotSame_SameObject_Fails(); + void Test_AssertSame_DifferentObjects_Fails(); + void Test_AssertSame_SameObject_Passes(); + void Test_CheckNotSame_CStringsSameContent_Passes(); + void Test_CheckNotSame_SameObject_Fails(); + void Test_CheckSame_ArrayAndPointer_Passes(); + void Test_CheckSame_BothNull_Passes(); + void Test_CheckSame_CStringsSameContent_Fails(); + void Test_CheckSame_DifferentObjects_Fails(); + void Test_CheckSame_RawAndSharedPtr_Passes(); + void Test_CheckSame_SameObject_Passes(); + void Test_CheckSame_SharedPtrCopies_Passes(); + void Test_CheckSame_UniquePtrs_Fails(); + +public: + int First = 0; + int Second = 0; + + TFixture_SameChecks(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_ShortTest +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one test that busy-waits for +// BusyWaitDuration, far less than a millisecond, timed with std::chrono::steady_clock. Logging is left on, for a +// run observer to collect. Used by Test_Run_RecordsDurationOfShortTest below to prove a short test's duration is +// measured, rather than read from a clock too coarse to see it (RAD Studio's 32-bit high_resolution_clock only +// advances about every 10 ms). +///////////////////////////////////////////////////////////////////////////// +class TFixture_ShortTest : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_BusyWaits(); + +public: + static constexpr std::chrono::microseconds BusyWaitDuration{ 200 }; + +public: + TFixture_ShortTest(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_SlowTest +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one test that hangs forever, +// followed by one that must never run if RunWithTimeout() correctly abandons the hung test and +// TTestGroupBase::Run() correctly aborts the rest of the group afterward. Used by +// Test_Run_AbandonsHungTestAndAbortsGroupOnTimeout below. +///////////////////////////////////////////////////////////////////////////// +class TFixture_SlowTest : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_HangsForever(); + void Test_NeverRuns(); + +public: + bool NeverRunReached = false; + +public: + TFixture_SlowTest(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +#if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) +///////////////////////////////////////////////////////////////////////////// +// TFixture_SourceLocations +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling each std::source_location +// overload. Each "_Fails"/"_Skips" test records the line of its call in ExpectedLines, so +// Test_SourceLocation_ReportsCallerFunctionAndLine below can check the failure names that test's +// function and line. "_Passes" tests check that arguments forward to the same overloads as the +// method/line form (C strings, mixed integers, wide substrings, exception expectations), and the "IC" ones that +// they reach the case-insensitive overloads. The StartsWith/EndsWith texts are chosen so that forwarding to the wrong +// overload (IC or not, start or end, Not or not) changes the outcome of at least one test. CheckIsEven() is a custom +// helper of the kind a test author might write, passing its caller's location through. +///////////////////////////////////////////////////////////////////////////// +class TFixture_SourceLocations : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void CheckIsEven(int value, std::source_location loc = std::source_location::current()); + + void Test_AssertContainsIC_Passes(); + void Test_AssertContains_Fails(); + void Test_AssertEmpty_Fails(); + void Test_AssertEndsWithIC_Passes(); + void Test_AssertEndsWith_Fails(); + void Test_AssertEqualsIC_Passes(); + void Test_AssertEqualsMem_Fails(); + void Test_AssertEquals_CStrings_Passes(); + void Test_AssertEquals_Fails(); + void Test_AssertFalse_Fails(); + void Test_AssertGreaterThanOrEqual_Passes(); + void Test_AssertGreaterThan_Fails(); + void Test_AssertIsNotType_Fails(); + void Test_AssertIsType_Fails(); + void Test_AssertLessThanOrEqual_Passes(); + void Test_AssertLessThan_Fails(); + void Test_AssertMatches_Fails(); + void Test_AssertNear_Fails(); + void Test_AssertNoThrow_Fails(); + void Test_AssertNotContainsIC_Fails(); + void Test_AssertNotContains_Fails(); + void Test_AssertNotEmpty_Fails(); + void Test_AssertNotEndsWithIC_Fails(); + void Test_AssertNotEndsWith_DifferentCase_Passes(); + void Test_AssertNotEndsWith_Fails(); + void Test_AssertNotEqualsIC_Fails(); + void Test_AssertNotEqualsMem_Fails(); + void Test_AssertNotEquals_Fails(); + void Test_AssertNotMatches_Fails(); + void Test_AssertNotNear_Fails(); + void Test_AssertNotNull_Fails(); + void Test_AssertNotSame_Fails(); + void Test_AssertNotStartsWithIC_Fails(); + void Test_AssertNotStartsWith_DifferentCase_Passes(); + void Test_AssertNotStartsWith_Fails(); + void Test_AssertNull_Fails(); + void Test_AssertSame_Fails(); + void Test_AssertStartsWithIC_Passes(); + void Test_AssertStartsWith_Fails(); + void Test_AssertThrows_Fails(); + void Test_AssertTrue_Fails(); + void Test_CheckContainsIC_Passes(); + void Test_CheckContains_Fails(); + void Test_CheckContains_Wide_Passes(); + void Test_CheckEmpty_Fails(); + void Test_CheckEndsWithIC_Passes(); + void Test_CheckEndsWith_Fails(); + void Test_CheckEndsWith_Wide_Passes(); + void Test_CheckEqualsIC_Passes(); + void Test_CheckEqualsMem_Fails(); + void Test_CheckEquals_EnumClass_Fails(); + void Test_CheckEquals_Fails(); + void Test_CheckEquals_MixedIntegers_Passes(); + void Test_CheckFalse_Fails(); + void Test_CheckGreaterThanOrEqual_Passes(); + void Test_CheckGreaterThan_Fails(); + void Test_CheckIsNotType_Fails(); + void Test_CheckIsType_Fails(); + void Test_CheckLessThanOrEqual_Passes(); + void Test_CheckLessThan_Fails(); + void Test_CheckMatches_Fails(); + void Test_CheckMatches_Wide_Passes(); + void Test_CheckNear_Fails(); + void Test_CheckNear_Passes(); + void Test_CheckNoThrow_Fails(); + void Test_CheckNotContainsIC_Fails(); + void Test_CheckNotContains_Fails(); + void Test_CheckNotEmpty_Fails(); + void Test_CheckNotEndsWithIC_Fails(); + void Test_CheckNotEndsWith_DifferentCase_Passes(); + void Test_CheckNotEndsWith_Fails(); + void Test_CheckNotEqualsIC_Fails(); + void Test_CheckNotEqualsMem_Fails(); + void Test_CheckNotEquals_Fails(); + void Test_CheckNotMatches_Fails(); + void Test_CheckNotNear_Fails(); + void Test_CheckNotNull_Fails(); + void Test_CheckNotSame_Fails(); + void Test_CheckNotSame_UniquePtrs_Passes(); + void Test_CheckNotStartsWithIC_Fails(); + void Test_CheckNotStartsWith_DifferentCase_Passes(); + void Test_CheckNotStartsWith_Fails(); + void Test_CheckNull_Fails(); + void Test_CheckNull_UniquePtr_Passes(); + void Test_CheckSame_Fails(); + void Test_CheckStartsWithIC_Passes(); + void Test_CheckStartsWith_Fails(); + void Test_CheckStartsWith_Wide_Passes(); + void Test_CheckThrows_Fails(); + void Test_CheckThrows_MessageMatches_Passes(); + void Test_CheckTrue_Fails(); + void Test_CheckTrue_ThroughHelper_Fails(); + void Test_Fail_Fails(); + void Test_SetExceptionExpected_Bool_NoneThrown_Fails(); + void Test_SetExceptionExpected_Bool_Passes(); + void Test_SetExceptionExpected_TypeAndMessage_Passes(); + void Test_SetExceptionExpected_Type_NoneThrown_Fails(); + void Test_Skip_Skips(); + +public: + std::map ExpectedLines; // Test name to the line of its failing/skipping call. + + TFixture_SourceLocations(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; +#endif // #if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_StartsWithComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertStartsWith()/CheckStartsWith()/ +// AssertNotStartsWith()/CheckNotStartsWith(), narrow and wide. "PrefixOnlyAtEnd" and "PrefixOnlyInMiddle" catch a +// check that matches anywhere but the start, and "PrefixLongerThanText" one that doesn't check the length first. +// "Wide_NonASCII" checks that a wide failure shows its text as UTF-8. Test names self-document expected outcome via +// NameEndsWith(), same as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_StartsWithComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertNotStartsWith_Absent_Passes(); + void Test_AssertNotStartsWith_Present_Fails(); + void Test_AssertNotStartsWith_Wide_Absent_Passes(); + void Test_AssertNotStartsWith_Wide_Present_Fails(); + void Test_AssertStartsWith_Absent_Fails(); + void Test_AssertStartsWith_Present_Passes(); + void Test_AssertStartsWith_Wide_Absent_Fails(); + void Test_AssertStartsWith_Wide_Present_Passes(); + void Test_CheckNotStartsWith_Absent_Passes(); + void Test_CheckNotStartsWith_BothEmpty_Fails(); + void Test_CheckNotStartsWith_EmptyPrefix_Fails(); + void Test_CheckNotStartsWith_PrefixLongerThanText_Passes(); + void Test_CheckNotStartsWith_PrefixOnlyAtEnd_Passes(); + void Test_CheckNotStartsWith_PrefixOnlyInMiddle_Passes(); + void Test_CheckNotStartsWith_Present_Fails(); + void Test_CheckNotStartsWith_Wide_Absent_Passes(); + void Test_CheckNotStartsWith_Wide_Present_Fails(); + void Test_CheckStartsWith_Absent_Fails(); + void Test_CheckStartsWith_BothEmpty_Passes(); + void Test_CheckStartsWith_DifferentCase_Fails(); + void Test_CheckStartsWith_EmptyPrefix_Passes(); + void Test_CheckStartsWith_EmptyText_Fails(); + void Test_CheckStartsWith_PrefixLongerThanText_Fails(); + void Test_CheckStartsWith_PrefixOnlyAtEnd_Fails(); + void Test_CheckStartsWith_PrefixOnlyInMiddle_Fails(); + void Test_CheckStartsWith_Present_Passes(); + void Test_CheckStartsWith_Wide_Absent_Fails(); + void Test_CheckStartsWith_Wide_NonASCII_Fails(); + void Test_CheckStartsWith_Wide_Present_Passes(); + +public: + TFixture_StartsWithComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_StartsWithICComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertStartsWithIC()/CheckStartsWithIC()/ +// AssertNotStartsWithIC()/CheckNotStartsWithIC(), narrow and wide. Only the ASCII letters A-Z are case-folded, as in +// TFixture_ContainsICComparisons above. Test names self-document expected outcome via NameEndsWith(), same as +// TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_StartsWithICComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertNotStartsWithIC_Absent_Passes(); + void Test_AssertNotStartsWithIC_DifferentCase_Fails(); + void Test_AssertNotStartsWithIC_Wide_Absent_Passes(); + void Test_AssertNotStartsWithIC_Wide_DifferentCase_Fails(); + void Test_AssertStartsWithIC_Absent_Fails(); + void Test_AssertStartsWithIC_DifferentCase_Passes(); + void Test_AssertStartsWithIC_Wide_Absent_Fails(); + void Test_AssertStartsWithIC_Wide_DifferentCase_Passes(); + void Test_CheckNotStartsWithIC_Absent_Passes(); + void Test_CheckNotStartsWithIC_DifferentCase_Fails(); + void Test_CheckNotStartsWithIC_EmptyPrefix_Fails(); + void Test_CheckNotStartsWithIC_PrefixLongerThanText_Passes(); + void Test_CheckNotStartsWithIC_PrefixOnlyAtEnd_Passes(); + void Test_CheckNotStartsWithIC_Wide_Absent_Passes(); + void Test_CheckNotStartsWithIC_Wide_DifferentCase_Fails(); + void Test_CheckStartsWithIC_Absent_Fails(); + void Test_CheckStartsWithIC_BothEmpty_Passes(); + void Test_CheckStartsWithIC_DifferentCase_Passes(); + void Test_CheckStartsWithIC_EmptyPrefix_Passes(); + void Test_CheckStartsWithIC_EmptyText_Fails(); + void Test_CheckStartsWithIC_NonASCII_Fails(); + void Test_CheckStartsWithIC_NonLetters_Fails(); + void Test_CheckStartsWithIC_PrefixLongerThanText_Fails(); + void Test_CheckStartsWithIC_PrefixOnlyAtEnd_Fails(); + void Test_CheckStartsWithIC_PrefixOnlyInMiddle_Fails(); + void Test_CheckStartsWithIC_Wide_Absent_Fails(); + void Test_CheckStartsWithIC_Wide_DifferentCase_Passes(); + void Test_CheckStartsWithIC_Wide_NonASCII_Fails(); + +public: + TFixture_StartsWithICComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_StringComparisons +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one deliberately failing test per string +// overload of AssertEquals()/CheckEquals()/AssertNotEquals()/CheckNotEquals() whose failure message didn't use to +// show the values: wide text (shown as UTF-8), the NotEquals overloads (which show the shared value), and null wide +// C strings (shown as "(null)", like narrow ones). Used by Test_Equals_ShowsStringValues below. +///////////////////////////////////////////////////////////////////////////// +class TFixture_StringComparisons : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertEquals_WideCStringNull_Fails(); + void Test_AssertEquals_Wide_Fails(); + void Test_AssertNotEquals_CStringBothNull_Fails(); + void Test_AssertNotEquals_String_Fails(); + void Test_AssertNotEquals_WideCStringBothNull_Fails(); + void Test_AssertNotEquals_Wide_Fails(); + void Test_CheckEquals_WideCStringNull_Fails(); + void Test_CheckEquals_WideCString_Fails(); + void Test_CheckEquals_Wide_Fails(); + void Test_CheckNotEquals_CStringBothNull_Fails(); + void Test_CheckNotEquals_String_Fails(); + void Test_CheckNotEquals_WideCStringBothNull_Fails(); + void Test_CheckNotEquals_Wide_Fails(); + +public: + TFixture_StringComparisons(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_ThrowingSetUpTearDown +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group whose SetUp_Test() or TearDown_Test(), as chosen +// by 'ThrowFrom', throws a std::runtime_error for its first test. That test expects an exception and throws one, +// so the exception from TearDown_Test() arrives while one is expected, and must not be taken for it. The second +// test records whether it ran. Used by Test_Run_EndsRunOnSetUpOrTearDownTestException below. +///////////////////////////////////////////////////////////////////////////// +class TFixture_ThrowingSetUpTearDown : public TTestGroupBase +{ +public: + enum class TThrowFrom + { + SetUp, + TearDown + }; -namespace +private: + typedef TTestGroupBase inherited; + +private: + void Test_RunsAfterThrowingTest(); + void Test_ThrowsExpectedException(); + +public: + bool RunsAfterThrowingTestReached = false; + TThrowFrom const ThrowFrom; + +public: + explicit TFixture_ThrowingSetUpTearDown(TThrowFrom throwFrom); + + void SetUp_Group() override {} + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override {} + void TearDown_Test(ITestCase& testCase) override; +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_ThrowsChecks +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertThrows()/CheckThrows()/ +// AssertNoThrow()/CheckNoThrow(), so Test_Throws_ChecksTypeAndMessageAndContinues below can check their outcomes +// and messages. ReachedAfterCheckNoThrow shows a failed Check lets the test continue; ReachedAfterInnerAssert shows +// an Assert failure inside the callable still ends the test instead of counting as the exception thrown. Test names +// self-document expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_ThrowsChecks : public TTestGroupBase { +private: + typedef TTestGroupBase inherited; -using namespace ASWUnitTests; +private: + void Test_AssertNoThrow_NoException_Passes(); + void Test_AssertNoThrow_Throws_Fails(); + void Test_AssertThrows_NoException_Fails(); + void Test_AssertThrows_RightType_Passes(); + void Test_CheckNoThrow_Throws_Fails(); + void Test_CheckThrows_AssertFailureInside_Fails(); + void Test_CheckThrows_FrameworkTypeRequested_Passes(); + void Test_CheckThrows_MessageMatches_Passes(); + void Test_CheckThrows_MessageMismatch_Fails(); + void Test_CheckThrows_NoException_Fails(); + void Test_CheckThrows_NonStdException_Fails(); + void Test_CheckThrows_SeveralCalls_Passes(); + void Test_CheckThrows_Subclass_Passes(); + void Test_CheckThrows_WrongType_Fails(); -size_t CountOccurrences(std::string const& text, std::string const& needle); -TTestCaseRecord const* FindRecord(TTestResults const& results, std::string const& testName); -bool NameEndsWith(std::string const& name, std::string const& suffix); +public: + bool ReachedAfterCheckNoThrow = false; + bool ReachedAfterInnerAssert = false; -//--------------------------------------------------------------------------- -// Number of non-overlapping occurrences of 'needle' in 'text'. -size_t CountOccurrences(std::string const& text, std::string const& needle) + TFixture_ThrowsChecks(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_TrueFalseChecks +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling AssertTrue()/CheckTrue()/AssertFalse()/ +// CheckFalse() with a value that passes and one that fails, so Test_TrueFalse_FailureNamesTheExpectedValue below can +// check that each failure states the value that was expected, not the opposite. Test names self-document expected +// outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_TrueFalseChecks : public TTestGroupBase { - size_t count = 0; +private: + typedef TTestGroupBase inherited; - for (size_t pos = text.find(needle); pos != std::string::npos; pos = text.find(needle, pos + needle.size())) - ++count; +private: + void Test_AssertFalse_False_Passes(); + void Test_AssertFalse_True_Fails(); + void Test_AssertTrue_False_Fails(); + void Test_AssertTrue_True_Passes(); + void Test_CheckFalse_False_Passes(); + void Test_CheckFalse_True_Fails(); + void Test_CheckTrue_False_Fails(); + void Test_CheckTrue_True_Passes(); - return count; -} +public: + TFixture_TrueFalseChecks(); -//--------------------------------------------------------------------------- -// The record for 'testName' in 'results', or nullptr if there isn't one. -TTestCaseRecord const* FindRecord(TTestResults const& results, std::string const& testName) + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_TypeChecks +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertIsType()/CheckIsType()/ +// AssertIsNotType()/CheckIsNotType() with a small class hierarchy, through raw and smart pointers and a reference, so +// Test_IsType_UsesDynamicTypeAndShowsNames below can check their outcomes and messages. A subclass counts as its base +// class's type, and a null pointer is never any type. Test names self-document expected outcome via NameEndsWith(), +// same as TFixture_ExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_TypeChecks : public TTestGroupBase { - for (TTestCaseRecord const& record : results.CaseRecords) +private: + typedef TTestGroupBase inherited; + + struct TShape { - if (record.TestName == testName) - return &record; - } + virtual ~TShape() = default; + }; - return nullptr; -} + struct TCircle : TShape + { + }; -//--------------------------------------------------------------------------- -// True if 'name' ends with 'suffix'. Used below so a fixture test's own name ("..._Passes"/ -// "..._Fails") documents its expected outcome, and the outer verifying test can check every -// registered test generically instead of hand-maintaining a separate expected-outcome table. Also -// used to check the end of a failure message. -bool NameEndsWith(std::string const& name, std::string const& suffix) + struct TBigCircle : TCircle + { + }; + + struct TSquare : TShape + { + }; + +private: + void Test_AssertIsNotType_OtherType_Passes(); + void Test_AssertIsNotType_SameType_Fails(); + void Test_AssertIsType_RightType_Passes(); + void Test_AssertIsType_WrongType_Fails(); + void Test_CheckIsNotType_Null_Passes(); + void Test_CheckIsNotType_OtherType_Passes(); + void Test_CheckIsNotType_Subclass_Fails(); + void Test_CheckIsType_BaseObject_Fails(); + void Test_CheckIsType_Null_Fails(); + void Test_CheckIsType_Reference_Passes(); + void Test_CheckIsType_RightType_Passes(); + void Test_CheckIsType_SharedPtr_Passes(); + void Test_CheckIsType_Subclass_Passes(); + void Test_CheckIsType_UniquePtr_Fails(); + +public: + TFixture_TypeChecks(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_UnexpectedExceptions +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group whose tests throw without expecting it: a +// std::exception, an object that isn't an exception at all, and a std::exception after a failed Check. Each of +// the first two is followed by a test that passes. Registration order matters: each throwing test must run before +// the test after it. Used by the Run_*UnexpectedException* tests below to prove an unexpected exception only fails +// the test that threw it, after which TearDown_Test() and the remaining tests still run. +///////////////////////////////////////////////////////////////////////////// +class TFixture_UnexpectedExceptions : public TTestGroupBase { - return name.size() >= suffix.size() && name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0; -} +private: + typedef TTestGroupBase inherited; -//--------------------------------------------------------------------------- +private: + void Test_CheckFailsThenThrowsStdException(); + void Test_RunsAfterNonException(); + void Test_RunsAfterStdException(); + void Test_ThrowsNonException(); + void Test_ThrowsStdException(); + +public: + std::vector TornDownTests; // The tests TearDown_Test() ran for, in order. + +public: + TFixture_UnexpectedExceptions(); + + void SetUp_Group() override {} + void TearDown_Group() override {} + void TearDown_Test(ITestCase& testCase) override; +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_CrashingTest +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one test that crashes (an +// integer divide by zero), followed by one that records whether it ran. Used by +// Test_Run_ContinuesAfterCrashWhenCatchCrashesIsSet below to prove --catch-crashes' central +// behavior: unlike a timeout, which can only abandon-and-abort, a caught crash lets the group +// continue running its remaining tests. Deliberately not a null-pointer write: that's a SIGSEGV on +// POSIX, and TCrashGuard::Run() treats every SIGSEGV there as abort-worthy (it can't tell an +// ordinary one apart from a stack overflow - see its comment), which would make this fixture's +// crashing test abort the group instead of letting it continue, defeating the point of this test. +// An integer divide by zero (SIGFPE on POSIX, EXCEPTION_INT_DIVIDE_BY_ZERO on Windows) is never +// abort-worthy on either platform, so it exercises "ordinary crash, group continues" portably. +///////////////////////////////////////////////////////////////////////////// +class TFixture_CrashingTest : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_CrashesButDoesNotAbort(); + void Test_RunsAfterTheCrash(); + +public: + bool RunsAfterTheCrashReached = false; + +public: + TFixture_CrashingTest(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; ///////////////////////////////////////////////////////////////////////////// @@ -164,7 +1862,40 @@ class TRecordingObserver : public ITestRunObserver } }; -//--------------------------------------------------------------------------- + +///////////////////////////////////////////////////////////////////////////// +// TCrashOnLogObserver +// +// A TRecordingObserver whose first OnLog() crashes, for the Run_*CrashInRunObserver* tests below: the crash +// comes while the group holds the lock it records and logs under. An integer divide by zero, like +// TFixture_CrashingTest's, so the run continues on POSIX too. It crashes before TRecordingObserver::OnLog() +// takes its own lock, which the crash would leave locked. +///////////////////////////////////////////////////////////////////////////// +class TCrashOnLogObserver : public TRecordingObserver +{ +private: + std::atomic m_Crashed; + +public: + TCrashOnLogObserver() + : m_Crashed(false) + { + } + + void OnLog(std::string const& text) override + { + if (!m_Crashed.exchange(true)) + { + volatile int numerator = 42; + volatile int denominator = 0; + volatile int const quotient = numerator / denominator; + (void)quotient; + } + + TRecordingObserver::OnLog(text); + } +}; + ///////////////////////////////////////////////////////////////////////////// // TSlowFinishObserver @@ -187,31 +1918,122 @@ class TSlowFinishObserver : public TRecordingObserver //--------------------------------------------------------------------------- -///////////////////////////////////////////////////////////////////////////// -// TFixture_BoolComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one deliberately failing test -// per bool overload of AssertEquals()/CheckEquals()/AssertNotEquals()/CheckNotEquals(), so -// Test_Equals_ShowsBoolValuesAsTrueOrFalse below can check that every failure message shows the -// values as "true"/"false" rather than "1"/"0". -///////////////////////////////////////////////////////////////////////////// -class TFixture_BoolComparisons : public TTestGroupBase +size_t CountOccurrences(std::string const& text, std::string const& needle); +int DownwardRoundingMode(); +TTestCaseRecord const* FindRecord(TTestResults const& results, std::string const& testName); +bool LongDoubleKeepsPrecision(); +bool NameEndsWith(std::string const& name, std::string const& suffix); +int RoundingMode(); +void SetRoundingMode(int mode); + +//--------------------------------------------------------------------------- +/* + CountOccurrences + + Number of non-overlapping occurrences of 'needle' in 'text'. +*/ +size_t CountOccurrences(std::string const& text, std::string const& needle) { -private: - typedef TTestGroupBase inherited; + size_t count = 0; -private: - void Test_AssertEquals_Different_Fails(); - void Test_AssertNotEquals_Same_Fails(); - void Test_CheckEquals_Different_Fails(); - void Test_CheckNotEquals_Same_Fails(); + for (size_t pos = text.find(needle); pos != std::string::npos; pos = text.find(needle, pos + needle.size())) + ++count; -public: - TFixture_BoolComparisons(); + return count; +} +//--------------------------------------------------------------------------- +/* + DownwardRoundingMode - void SetUp_Group() override {} - void TearDown_Group() override {} -}; + The rounding mode that rounds toward negative infinity, as RoundingMode() returns it. +*/ +int DownwardRoundingMode() +{ +#if defined(__BORLANDC__) && !defined(_WIN64) + return RC_DOWN; +#else + return FE_DOWNWARD; +#endif +} +//--------------------------------------------------------------------------- +/* + FindRecord + + The record for 'testName' in 'results', or nullptr if there isn't one. +*/ +TTestCaseRecord const* FindRecord(TTestResults const& results, std::string const& testName) +{ + for (TTestCaseRecord const& record : results.CaseRecords) + { + if (record.TestName == testName) + return &record; + } + + return nullptr; +} +//--------------------------------------------------------------------------- +/* + LongDoubleKeepsPrecision + + True if long double arithmetic on this thread keeps all of long double's digits: 1 plus its epsilon isn't 1. + Where long double is wider than double, x87 arithmetic set to double precision rounds that sum to 1, as on a + new llvm-mingw thread. The volatile load keeps the sum from being computed at compile time. +*/ +bool LongDoubleKeepsPrecision() +{ + long double volatile one = 1.0L; + long double const sum = one + std::numeric_limits::epsilon(); + return sum != one; +} +//--------------------------------------------------------------------------- +/* + NameEndsWith + + True if 'name' ends with 'suffix'. Used below so a fixture test's own name ("..._Passes"/ + "..._Fails") documents its expected outcome, and the outer verifying test can check every + registered test generically instead of hand-maintaining a separate expected-outcome table. Also + used to check the end of a failure message. +*/ +bool NameEndsWith(std::string const& name, std::string const& suffix) +{ + return name.size() >= suffix.size() && name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0; +} +//--------------------------------------------------------------------------- +/* + RoundingMode + + This thread's floating-point rounding mode, as std::fegetround() returns it. RAD Studio's 32-bit runtime + declares std::fegetround() and std::fesetround() but doesn't define them, so there it's the x87 control + word's rounding bits instead (see SetRoundingMode() and DownwardRoundingMode()). +*/ +int RoundingMode() +{ +#if defined(__BORLANDC__) && !defined(_WIN64) + return static_cast(_control87(0, 0) & MCW_RC); +#else + return std::fegetround(); +#endif +} +//--------------------------------------------------------------------------- +/* + SetRoundingMode + + Sets this thread's rounding mode to 'mode' (from RoundingMode() or DownwardRoundingMode()). +*/ +void SetRoundingMode(int mode) +{ +#if defined(__BORLANDC__) && !defined(_WIN64) + _control87(static_cast(mode), MCW_RC); +#else + std::fesetround(mode); +#endif +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_BoolComparisons +///////////////////////////////////////////////////////////////////////////// //--------------------------------------------------------------------------- TFixture_BoolComparisons::TFixture_BoolComparisons() @@ -249,54 +2071,7 @@ void TFixture_BoolComparisons::Test_CheckNotEquals_Same_Fails() ///////////////////////////////////////////////////////////////////////////// // TFixture_CStringComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing the C string overloads of -// AssertEquals()/CheckEquals()/AssertNotEquals()/CheckNotEquals(), narrow and wide. Without those -// overloads, two C strings convert to bool and match the bool overload, so the "DifferentLiterals" -// tests pass/fail the wrong way round. The "SameContentDifferentBuffers" tests compare a literal with -// a separate array holding the same text, so they fail if the pointers are compared instead of the -// content (two identical literals may share one address). Test names self-document expected outcome -// via NameEndsWith(), same as TFixture_ExceptionExpectations below. ///////////////////////////////////////////////////////////////////////////// -class TFixture_CStringComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertEquals_DifferentLiterals_Fails(); - void Test_AssertEquals_NullAndNonNull_Fails(); - void Test_AssertEquals_SameContentDifferentBuffers_Passes(); - void Test_AssertEquals_Wide_DifferentLiterals_Fails(); - void Test_AssertEquals_Wide_SameContentDifferentBuffers_Passes(); - void Test_AssertNotEquals_DifferentLiterals_Passes(); - void Test_AssertNotEquals_SameContentDifferentBuffers_Fails(); - void Test_AssertNotEquals_Wide_DifferentLiterals_Passes(); - void Test_AssertNotEquals_Wide_SameContentDifferentBuffers_Fails(); - void Test_CheckEquals_BothNull_Passes(); - void Test_CheckEquals_DifferentLiterals_Fails(); - void Test_CheckEquals_EmptyAndNull_Fails(); - void Test_CheckEquals_NullAndEmpty_Fails(); - void Test_CheckEquals_SameContentDifferentBuffers_Passes(); - void Test_CheckEquals_Wide_BothNull_Passes(); - void Test_CheckEquals_Wide_DifferentLiterals_Fails(); - void Test_CheckEquals_Wide_NullAndEmpty_Fails(); - void Test_CheckEquals_Wide_SameContentDifferentBuffers_Passes(); - void Test_CheckNotEquals_BothNull_Fails(); - void Test_CheckNotEquals_DifferentLiterals_Passes(); - void Test_CheckNotEquals_NullAndEmpty_Passes(); - void Test_CheckNotEquals_SameContentDifferentBuffers_Fails(); - void Test_CheckNotEquals_Wide_BothNull_Fails(); - void Test_CheckNotEquals_Wide_DifferentLiterals_Passes(); - void Test_CheckNotEquals_Wide_NullAndEmpty_Passes(); - void Test_CheckNotEquals_Wide_SameContentDifferentBuffers_Fails(); - -public: - TFixture_CStringComparisons(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_CStringComparisons::TFixture_CStringComparisons() @@ -506,46 +2281,7 @@ void TFixture_CStringComparisons::Test_CheckNotEquals_Wide_SameContentDifferentB ///////////////////////////////////////////////////////////////////////////// // TFixture_ContainsComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertContains()/CheckContains()/ -// AssertNotContains()/CheckNotContains(), narrow and wide. "Wide_NonASCII" checks that a wide failure shows -// its text as UTF-8. Test names self-document expected outcome via NameEndsWith(), same as -// TFixture_ExceptionExpectations below. ///////////////////////////////////////////////////////////////////////////// -class TFixture_ContainsComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertContains_Absent_Fails(); - void Test_AssertContains_Present_Passes(); - void Test_AssertContains_Wide_Absent_Fails(); - void Test_AssertContains_Wide_Present_Passes(); - void Test_AssertNotContains_Absent_Passes(); - void Test_AssertNotContains_Present_Fails(); - void Test_AssertNotContains_Wide_Absent_Passes(); - void Test_AssertNotContains_Wide_Present_Fails(); - void Test_CheckContains_Absent_Fails(); - void Test_CheckContains_DifferentCase_Fails(); - void Test_CheckContains_EmptySubstring_Passes(); - void Test_CheckContains_Present_Passes(); - void Test_CheckContains_SubstringLongerThanText_Fails(); - void Test_CheckContains_Wide_Absent_Fails(); - void Test_CheckContains_Wide_NonASCII_Fails(); - void Test_CheckContains_Wide_Present_Passes(); - void Test_CheckNotContains_Absent_Passes(); - void Test_CheckNotContains_EmptySubstring_Fails(); - void Test_CheckNotContains_Present_Fails(); - void Test_CheckNotContains_Wide_Absent_Passes(); - void Test_CheckNotContains_Wide_Present_Fails(); - -public: - TFixture_ContainsComparisons(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_ContainsComparisons::TFixture_ContainsComparisons() @@ -704,51 +2440,7 @@ void TFixture_ContainsComparisons::Test_CheckNotContains_Wide_Present_Fails() ///////////////////////////////////////////////////////////////////////////// // TFixture_ContainsICComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertContainsIC()/CheckContainsIC()/ -// AssertNotContainsIC()/CheckNotContainsIC(), narrow and wide. Only the ASCII letters A-Z are case-folded: the -// "NonLetters" tests use characters 0x20 apart, which a fold that just sets bit 0x20 would wrongly match, and the -// "NonASCII" tests show that accented letters keep their case. Test names self-document expected outcome via -// NameEndsWith(), same as TFixture_ExceptionExpectations below. ///////////////////////////////////////////////////////////////////////////// -class TFixture_ContainsICComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertContainsIC_Absent_Fails(); - void Test_AssertContainsIC_DifferentCase_Passes(); - void Test_AssertContainsIC_Wide_Absent_Fails(); - void Test_AssertContainsIC_Wide_DifferentCase_Passes(); - void Test_AssertNotContainsIC_Absent_Passes(); - void Test_AssertNotContainsIC_DifferentCase_Fails(); - void Test_AssertNotContainsIC_Wide_Absent_Passes(); - void Test_AssertNotContainsIC_Wide_DifferentCase_Fails(); - void Test_CheckContainsIC_Absent_Fails(); - void Test_CheckContainsIC_BothEmpty_Passes(); - void Test_CheckContainsIC_DifferentCase_Passes(); - void Test_CheckContainsIC_EmptySubstring_Passes(); - void Test_CheckContainsIC_EmptyText_Fails(); - void Test_CheckContainsIC_NonASCII_Fails(); - void Test_CheckContainsIC_NonLetters_Fails(); - void Test_CheckContainsIC_SubstringAtEnd_Passes(); - void Test_CheckContainsIC_SubstringLongerThanText_Fails(); - void Test_CheckContainsIC_Wide_Absent_Fails(); - void Test_CheckContainsIC_Wide_DifferentCase_Passes(); - void Test_CheckContainsIC_Wide_NonASCII_Fails(); - void Test_CheckNotContainsIC_Absent_Passes(); - void Test_CheckNotContainsIC_DifferentCase_Fails(); - void Test_CheckNotContainsIC_EmptySubstring_Fails(); - void Test_CheckNotContainsIC_Wide_Absent_Passes(); - void Test_CheckNotContainsIC_Wide_DifferentCase_Fails(); - -public: - TFixture_ContainsICComparisons(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_ContainsICComparisons::TFixture_ContainsICComparisons() @@ -936,42 +2628,10 @@ void TFixture_ContainsICComparisons::Test_CheckNotContainsIC_Wide_DifferentCase_ //--------------------------------------------------------------------------- -///////////////////////////////////////////////////////////////////////////// -// TFixture_EmptyChecks -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEmpty()/CheckEmpty()/ -// AssertNotEmpty()/CheckNotEmpty() with strings, wide strings, containers with and without size(), and (where the -// library has it) std::string_view, so Test_Empty_ShowsContentsOnFailure below can check what each failure shows. -// Test names self-document expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations below. -///////////////////////////////////////////////////////////////////////////// -class TFixture_EmptyChecks : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertEmpty_EmptyString_Passes(); - void Test_AssertEmpty_String_Fails(); - void Test_AssertNotEmpty_EmptyVector_Fails(); - void Test_AssertNotEmpty_Vector_Passes(); - void Test_CheckEmpty_EmptyMap_Passes(); - void Test_CheckEmpty_ForwardList_Fails(); - void Test_CheckEmpty_OneElement_Fails(); -#if defined(__cpp_lib_string_view) - void Test_CheckEmpty_StringView_Fails(); -#endif - void Test_CheckEmpty_Vector_Fails(); - void Test_CheckEmpty_WideString_Fails(); - void Test_CheckNotEmpty_EmptyString_Fails(); - void Test_CheckNotEmpty_String_Passes(); - -public: - TFixture_EmptyChecks(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; - +///////////////////////////////////////////////////////////////////////////// +// TFixture_EmptyChecks +///////////////////////////////////////////////////////////////////////////// + //--------------------------------------------------------------------------- TFixture_EmptyChecks::TFixture_EmptyChecks() : inherited("Fixture_EmptyChecks") @@ -979,19 +2639,39 @@ TFixture_EmptyChecks::TFixture_EmptyChecks() SetLogSuppressed(true); RegisterTest(&TFixture_EmptyChecks::Test_AssertEmpty_EmptyString_Passes, "AssertEmpty_EmptyString_Passes"); + RegisterTest(&TFixture_EmptyChecks::Test_AssertEmpty_NullCString_Fails, "AssertEmpty_NullCString_Fails"); RegisterTest(&TFixture_EmptyChecks::Test_AssertEmpty_String_Fails, "AssertEmpty_String_Fails"); + RegisterTest(&TFixture_EmptyChecks::Test_AssertEmpty_WideArray_Passes, "AssertEmpty_WideArray_Passes"); RegisterTest(&TFixture_EmptyChecks::Test_AssertNotEmpty_EmptyVector_Fails, "AssertNotEmpty_EmptyVector_Fails"); + RegisterTest(&TFixture_EmptyChecks::Test_AssertNotEmpty_EmptyWideCString_Fails, + "AssertNotEmpty_EmptyWideCString_Fails"); + RegisterTest(&TFixture_EmptyChecks::Test_AssertNotEmpty_NullCString_Fails, "AssertNotEmpty_NullCString_Fails"); RegisterTest(&TFixture_EmptyChecks::Test_AssertNotEmpty_Vector_Passes, "AssertNotEmpty_Vector_Passes"); + RegisterTest(&TFixture_EmptyChecks::Test_AssertNotEmpty_WideCString_Passes, "AssertNotEmpty_WideCString_Passes"); + RegisterTest(&TFixture_EmptyChecks::Test_CheckEmpty_CharArray_Fails, "CheckEmpty_CharArray_Fails"); + RegisterTest(&TFixture_EmptyChecks::Test_CheckEmpty_EmptyCharArray_Passes, "CheckEmpty_EmptyCharArray_Passes"); RegisterTest(&TFixture_EmptyChecks::Test_CheckEmpty_EmptyMap_Passes, "CheckEmpty_EmptyMap_Passes"); RegisterTest(&TFixture_EmptyChecks::Test_CheckEmpty_ForwardList_Fails, "CheckEmpty_ForwardList_Fails"); + RegisterTest(&TFixture_EmptyChecks::Test_CheckEmpty_NullWideCString_Fails, "CheckEmpty_NullWideCString_Fails"); RegisterTest(&TFixture_EmptyChecks::Test_CheckEmpty_OneElement_Fails, "CheckEmpty_OneElement_Fails"); #if defined(__cpp_lib_string_view) RegisterTest(&TFixture_EmptyChecks::Test_CheckEmpty_StringView_Fails, "CheckEmpty_StringView_Fails"); #endif RegisterTest(&TFixture_EmptyChecks::Test_CheckEmpty_Vector_Fails, "CheckEmpty_Vector_Fails"); + RegisterTest(&TFixture_EmptyChecks::Test_CheckEmpty_WideCString_Fails, "CheckEmpty_WideCString_Fails"); RegisterTest(&TFixture_EmptyChecks::Test_CheckEmpty_WideString_Fails, "CheckEmpty_WideString_Fails"); + RegisterTest(&TFixture_EmptyChecks::Test_CheckNotEmpty_CString_Passes, "CheckNotEmpty_CString_Passes"); + RegisterTest(&TFixture_EmptyChecks::Test_CheckNotEmpty_EmptyCharPointer_Fails, + "CheckNotEmpty_EmptyCharPointer_Fails"); RegisterTest(&TFixture_EmptyChecks::Test_CheckNotEmpty_EmptyString_Fails, "CheckNotEmpty_EmptyString_Fails"); + RegisterTest(&TFixture_EmptyChecks::Test_CheckNotEmpty_NullCString_Fails, "CheckNotEmpty_NullCString_Fails"); + RegisterTest(&TFixture_EmptyChecks::Test_CheckNotEmpty_NullWideCString_Fails, + "CheckNotEmpty_NullWideCString_Fails"); +#if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) + RegisterTest(&TFixture_EmptyChecks::Test_CheckNotEmpty_SourceLocation_Passes, "CheckNotEmpty_SourceLocation_Passes"); +#endif RegisterTest(&TFixture_EmptyChecks::Test_CheckNotEmpty_String_Passes, "CheckNotEmpty_String_Passes"); + RegisterTest(&TFixture_EmptyChecks::Test_CheckNotEmpty_WideArray_Passes, "CheckNotEmpty_WideArray_Passes"); } //--------------------------------------------------------------------------- void TFixture_EmptyChecks::Test_AssertEmpty_EmptyString_Passes() @@ -999,21 +2679,61 @@ void TFixture_EmptyChecks::Test_AssertEmpty_EmptyString_Passes() AssertEmpty(std::string(), __func__, __LINE__, "empty string"); } //--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_AssertEmpty_NullCString_Fails() +{ + AssertEmpty(static_cast(nullptr), __func__, __LINE__, "null C string"); +} +//--------------------------------------------------------------------------- void TFixture_EmptyChecks::Test_AssertEmpty_String_Fails() { AssertEmpty(std::string("abc"), __func__, __LINE__, "has text"); } //--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_AssertEmpty_WideArray_Passes() +{ + wchar_t const buffer[16] = L""; + AssertEmpty(buffer, __func__, __LINE__, "an array holding empty text"); +} +//--------------------------------------------------------------------------- void TFixture_EmptyChecks::Test_AssertNotEmpty_EmptyVector_Fails() { AssertNotEmpty(std::vector(), __func__, __LINE__, "no elements"); } //--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_AssertNotEmpty_EmptyWideCString_Fails() +{ + wchar_t const* const text = L""; + AssertNotEmpty(text, __func__, __LINE__, "empty wide C string"); +} +//--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_AssertNotEmpty_NullCString_Fails() +{ + AssertNotEmpty(static_cast(nullptr), __func__, __LINE__, "null C string"); +} +//--------------------------------------------------------------------------- void TFixture_EmptyChecks::Test_AssertNotEmpty_Vector_Passes() { AssertNotEmpty(std::vector{ 1 }, __func__, __LINE__, "one element"); } //--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_AssertNotEmpty_WideCString_Passes() +{ + wchar_t const* const text = L"x"; + AssertNotEmpty(text, __func__, __LINE__, "wide C string"); +} +//--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_CheckEmpty_CharArray_Fails() +{ + char const buffer[32] = "abc"; + CheckEmpty(buffer, __func__, __LINE__, "an array shows its text, not its size"); +} +//--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_CheckEmpty_EmptyCharArray_Passes() +{ + char const buffer[32] = ""; + CheckEmpty(buffer, __func__, __LINE__, "an array holding empty text, whatever its size"); +} +//--------------------------------------------------------------------------- void TFixture_EmptyChecks::Test_CheckEmpty_EmptyMap_Passes() { CheckEmpty(std::map(), __func__, __LINE__, "empty map"); @@ -1025,6 +2745,11 @@ void TFixture_EmptyChecks::Test_CheckEmpty_ForwardList_Fails() CheckEmpty(std::forward_list{ 1, 2 }, __func__, __LINE__, "no size to show"); } //--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_CheckEmpty_NullWideCString_Fails() +{ + CheckEmpty(static_cast(nullptr), __func__, __LINE__, "null wide C string"); +} +//--------------------------------------------------------------------------- void TFixture_EmptyChecks::Test_CheckEmpty_OneElement_Fails() { CheckEmpty(std::vector{ 7 }, __func__, __LINE__, "one element"); @@ -1042,75 +2767,71 @@ void TFixture_EmptyChecks::Test_CheckEmpty_Vector_Fails() CheckEmpty(std::vector{ 1, 2, 3 }, __func__, __LINE__, "three elements"); } //--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_CheckEmpty_WideCString_Fails() +{ + // U+00E9 (e with acute accent) is shown as its UTF-8 bytes. + wchar_t const* const text = L"caf\x00E9"; + CheckEmpty(text, __func__, __LINE__, "wide C string"); +} +//--------------------------------------------------------------------------- void TFixture_EmptyChecks::Test_CheckEmpty_WideString_Fails() { // U+00E9 (e with acute accent) is shown as its UTF-8 bytes. CheckEmpty(std::wstring(L"caf\x00E9"), __func__, __LINE__, "wide text"); } //--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_CheckNotEmpty_CString_Passes() +{ + char const* const text = "abc"; + CheckNotEmpty(text, __func__, __LINE__, "C string"); +} +//--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_CheckNotEmpty_EmptyCharPointer_Fails() +{ + char buffer[8] = ""; + char* const text = buffer; + CheckNotEmpty(text, __func__, __LINE__, "a char* to empty text"); +} +//--------------------------------------------------------------------------- void TFixture_EmptyChecks::Test_CheckNotEmpty_EmptyString_Fails() { CheckNotEmpty(std::string(), __func__, __LINE__, "empty string"); } //--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_CheckNotEmpty_NullCString_Fails() +{ + CheckNotEmpty(static_cast(nullptr), __func__, __LINE__, "null C string"); +} +//--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_CheckNotEmpty_NullWideCString_Fails() +{ + CheckNotEmpty(static_cast(nullptr), __func__, __LINE__, "null wide C string"); +} +//--------------------------------------------------------------------------- +#if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) +void TFixture_EmptyChecks::Test_CheckNotEmpty_SourceLocation_Passes() +{ + wchar_t buffer[8] = L"x"; + CheckNotEmpty(buffer, "a wchar_t array through the std::source_location form"); +} +//--------------------------------------------------------------------------- +#endif void TFixture_EmptyChecks::Test_CheckNotEmpty_String_Passes() { CheckNotEmpty(std::string("abc"), __func__, __LINE__, "has text"); } //--------------------------------------------------------------------------- +void TFixture_EmptyChecks::Test_CheckNotEmpty_WideArray_Passes() +{ + wchar_t const buffer[16] = L"abc"; + CheckNotEmpty(buffer, __func__, __LINE__, "an array holding text"); +} +//--------------------------------------------------------------------------- ///////////////////////////////////////////////////////////////////////////// // TFixture_EndsWithComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEndsWith()/CheckEndsWith()/ -// AssertNotEndsWith()/CheckNotEndsWith(), narrow and wide. "SuffixOnlyAtStart" and "SuffixOnlyInMiddle" catch a -// check that matches anywhere but the end, and "SuffixLongerThanText" one that doesn't check the length first. -// "Wide_NonASCII" checks that a wide failure shows its text as UTF-8. Test names self-document expected outcome via -// NameEndsWith(), same as TFixture_ExceptionExpectations below. ///////////////////////////////////////////////////////////////////////////// -class TFixture_EndsWithComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertEndsWith_Absent_Fails(); - void Test_AssertEndsWith_Present_Passes(); - void Test_AssertEndsWith_Wide_Absent_Fails(); - void Test_AssertEndsWith_Wide_Present_Passes(); - void Test_AssertNotEndsWith_Absent_Passes(); - void Test_AssertNotEndsWith_Present_Fails(); - void Test_AssertNotEndsWith_Wide_Absent_Passes(); - void Test_AssertNotEndsWith_Wide_Present_Fails(); - void Test_CheckEndsWith_Absent_Fails(); - void Test_CheckEndsWith_BothEmpty_Passes(); - void Test_CheckEndsWith_DifferentCase_Fails(); - void Test_CheckEndsWith_EmptySuffix_Passes(); - void Test_CheckEndsWith_EmptyText_Fails(); - void Test_CheckEndsWith_Present_Passes(); - void Test_CheckEndsWith_SuffixLongerThanText_Fails(); - void Test_CheckEndsWith_SuffixOnlyAtStart_Fails(); - void Test_CheckEndsWith_SuffixOnlyInMiddle_Fails(); - void Test_CheckEndsWith_Wide_Absent_Fails(); - void Test_CheckEndsWith_Wide_NonASCII_Fails(); - void Test_CheckEndsWith_Wide_Present_Passes(); - void Test_CheckNotEndsWith_Absent_Passes(); - void Test_CheckNotEndsWith_BothEmpty_Fails(); - void Test_CheckNotEndsWith_EmptySuffix_Fails(); - void Test_CheckNotEndsWith_Present_Fails(); - void Test_CheckNotEndsWith_SuffixLongerThanText_Passes(); - void Test_CheckNotEndsWith_SuffixOnlyAtStart_Passes(); - void Test_CheckNotEndsWith_SuffixOnlyInMiddle_Passes(); - void Test_CheckNotEndsWith_Wide_Absent_Passes(); - void Test_CheckNotEndsWith_Wide_Present_Fails(); - -public: - TFixture_EndsWithComparisons(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_EndsWithComparisons::TFixture_EndsWithComparisons() @@ -1321,53 +3042,7 @@ void TFixture_EndsWithComparisons::Test_CheckNotEndsWith_Wide_Present_Fails() ///////////////////////////////////////////////////////////////////////////// // TFixture_EndsWithICComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEndsWithIC()/CheckEndsWithIC()/ -// AssertNotEndsWithIC()/CheckNotEndsWithIC(), narrow and wide. Only the ASCII letters A-Z are case-folded, as in -// TFixture_ContainsICComparisons above. Test names self-document expected outcome via NameEndsWith(), same as -// TFixture_ExceptionExpectations below. ///////////////////////////////////////////////////////////////////////////// -class TFixture_EndsWithICComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertEndsWithIC_Absent_Fails(); - void Test_AssertEndsWithIC_DifferentCase_Passes(); - void Test_AssertEndsWithIC_Wide_Absent_Fails(); - void Test_AssertEndsWithIC_Wide_DifferentCase_Passes(); - void Test_AssertNotEndsWithIC_Absent_Passes(); - void Test_AssertNotEndsWithIC_DifferentCase_Fails(); - void Test_AssertNotEndsWithIC_Wide_Absent_Passes(); - void Test_AssertNotEndsWithIC_Wide_DifferentCase_Fails(); - void Test_CheckEndsWithIC_Absent_Fails(); - void Test_CheckEndsWithIC_BothEmpty_Passes(); - void Test_CheckEndsWithIC_DifferentCase_Passes(); - void Test_CheckEndsWithIC_EmptySuffix_Passes(); - void Test_CheckEndsWithIC_EmptyText_Fails(); - void Test_CheckEndsWithIC_NonASCII_Fails(); - void Test_CheckEndsWithIC_NonLetters_Fails(); - void Test_CheckEndsWithIC_SuffixLongerThanText_Fails(); - void Test_CheckEndsWithIC_SuffixOnlyAtStart_Fails(); - void Test_CheckEndsWithIC_SuffixOnlyInMiddle_Fails(); - void Test_CheckEndsWithIC_Wide_Absent_Fails(); - void Test_CheckEndsWithIC_Wide_DifferentCase_Passes(); - void Test_CheckEndsWithIC_Wide_NonASCII_Fails(); - void Test_CheckNotEndsWithIC_Absent_Passes(); - void Test_CheckNotEndsWithIC_DifferentCase_Fails(); - void Test_CheckNotEndsWithIC_EmptySuffix_Fails(); - void Test_CheckNotEndsWithIC_SuffixLongerThanText_Passes(); - void Test_CheckNotEndsWithIC_SuffixOnlyAtStart_Passes(); - void Test_CheckNotEndsWithIC_Wide_Absent_Passes(); - void Test_CheckNotEndsWithIC_Wide_DifferentCase_Fails(); - -public: - TFixture_EndsWithICComparisons(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_EndsWithICComparisons::TFixture_EndsWithICComparisons() @@ -1578,69 +3253,7 @@ void TFixture_EndsWithICComparisons::Test_CheckNotEndsWithIC_Wide_DifferentCase_ ///////////////////////////////////////////////////////////////////////////// // TFixture_EnumComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEquals()/CheckEquals()/ -// AssertNotEquals()/CheckNotEquals() with scoped enums (enum class), compared and shown by their underlying values, -// including char, signed and 64-bit unsigned underlying types; plus an unscoped enum with an int, which must still -// compile and compare as before. Test names self-document expected outcome via NameEndsWith(), same as -// TFixture_ExceptionExpectations below. ///////////////////////////////////////////////////////////////////////////// -class TFixture_EnumComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - - enum class TBig : uint64_t - { - One = 1, - Max = std::numeric_limits::max() - }; - - enum class TColor - { - Red, - Green, - Blue - }; - - enum class TLetter : char - { - A = 'A', - B = 'B' - }; - - enum TPlain - { - PlainA, - PlainB - }; - - enum class TSmall : int8_t - { - Negative = -1, - Positive = 1 - }; - -private: - void Test_AssertEquals_Different_Fails(); - void Test_AssertEquals_Same_Passes(); - void Test_AssertNotEquals_Different_Passes(); - void Test_AssertNotEquals_Same_Fails(); - void Test_CheckEquals_CharUnderlying_Fails(); - void Test_CheckEquals_Different_Fails(); - void Test_CheckEquals_NegativeUnderlying_Fails(); - void Test_CheckEquals_Same_Passes(); - void Test_CheckEquals_UInt64Underlying_Fails(); - void Test_CheckEquals_UnscopedEnumAndInt_Passes(); - void Test_CheckNotEquals_Different_Passes(); - void Test_CheckNotEquals_Same_Fails(); - -public: - TFixture_EnumComparisons(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_EnumComparisons::TFixture_EnumComparisons() @@ -1726,52 +3339,11 @@ void TFixture_EnumComparisons::Test_CheckNotEquals_Same_Fails() } //--------------------------------------------------------------------------- - -///////////////////////////////////////////////////////////////////////////// -// TFixture_EqualsICComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEqualsIC()/CheckEqualsIC()/ -// AssertNotEqualsIC()/CheckNotEqualsIC(), narrow and wide. Only the ASCII letters A-Z are case-folded, as in -// TFixture_ContainsICComparisons above. "DifferentLength" catches a comparison that stops at the end of the shorter -// value. Test names self-document expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations below. -///////////////////////////////////////////////////////////////////////////// -class TFixture_EqualsICComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertEqualsIC_DifferentCase_Passes(); - void Test_AssertEqualsIC_DifferentText_Fails(); - void Test_AssertEqualsIC_Wide_DifferentCase_Passes(); - void Test_AssertEqualsIC_Wide_DifferentText_Fails(); - void Test_AssertNotEqualsIC_DifferentCase_Fails(); - void Test_AssertNotEqualsIC_DifferentText_Passes(); - void Test_AssertNotEqualsIC_Wide_DifferentCase_Fails(); - void Test_AssertNotEqualsIC_Wide_DifferentText_Passes(); - void Test_CheckEqualsIC_BothEmpty_Passes(); - void Test_CheckEqualsIC_DifferentCase_Passes(); - void Test_CheckEqualsIC_DifferentLength_Fails(); - void Test_CheckEqualsIC_DifferentText_Fails(); - void Test_CheckEqualsIC_EmptyAndNonEmpty_Fails(); - void Test_CheckEqualsIC_NonASCII_Fails(); - void Test_CheckEqualsIC_NonLetters_Fails(); - void Test_CheckEqualsIC_Wide_DifferentCase_Passes(); - void Test_CheckEqualsIC_Wide_DifferentText_Fails(); - void Test_CheckEqualsIC_Wide_NonASCII_Fails(); - void Test_CheckNotEqualsIC_DifferentCase_Fails(); - void Test_CheckNotEqualsIC_DifferentText_Passes(); - void Test_CheckNotEqualsIC_SameText_Fails(); - void Test_CheckNotEqualsIC_Wide_DifferentCase_Fails(); - void Test_CheckNotEqualsIC_Wide_DifferentText_Passes(); - -public: - TFixture_EqualsICComparisons(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; - + +///////////////////////////////////////////////////////////////////////////// +// TFixture_EqualsICComparisons +///////////////////////////////////////////////////////////////////////////// + //--------------------------------------------------------------------------- TFixture_EqualsICComparisons::TFixture_EqualsICComparisons() : inherited("Fixture_EqualsICComparisons") @@ -1948,81 +3520,9 @@ void TFixture_EqualsICComparisons::Test_CheckNotEqualsIC_Wide_DifferentText_Pass //--------------------------------------------------------------------------- -///////////////////////////////////////////////////////////////////////////// -// TFixtureSpecificError -// -// Unrelated (sibling) exception type for TFixture_ExceptionExpectations -// below, so a "wrong type" scenario can throw a real type that isn't just a -// differently-worded same class. -///////////////////////////////////////////////////////////////////////////// -class TFixtureSpecificError : public std::runtime_error -{ -public: - explicit TFixtureSpecificError(std::string const& msg) - : std::runtime_error(msg) - { - } -}; - - -///////////////////////////////////////////////////////////////////////////// -// TFixtureOtherError -// -// Unrelated (sibling) exception type for TFixture_ExceptionExpectations -// below, so a "wrong type" scenario can throw a real type that isn't just a -// differently-worded same class. -///////////////////////////////////////////////////////////////////////////// -class TFixtureOtherError : public std::runtime_error -{ -public: - explicit TFixtureOtherError(std::string const& msg) - : std::runtime_error(msg) - { - } -}; - - ///////////////////////////////////////////////////////////////////////////// // TFixture_ExceptionExpectations -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one test method per -// SetExceptionExpected()/Test() branch: no throw, a generic expectation satisfied by either a -// std::exception or a non-std::exception throw, and a specific-type expectation crossed with an -// exact type match, a polymorphic base-type match, a wrong sibling type, a non-std::exception -// throw, and a message substring that's present or absent; plus an Assert* failure while a generic -// or specific exception is expected, and an earlier Check* failure followed by the expected -// exception, on each path that would otherwise pass. Every test name ends with "_Passes" or -// "_Fails", read generically by Test_SetExceptionExpected_MatchesTypeAndMessage below via -// NameEndsWith() rather than a hand-maintained table. ///////////////////////////////////////////////////////////////////////////// -class TFixture_ExceptionExpectations : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_CheckFailedThenAssertFailedWhileExceptionExpected_Fails(); - void Test_CheckFailedThenGenericExpectedExceptionThrown_Fails(); - void Test_CheckFailedThenNonStdExpectedExceptionThrown_Fails(); - void Test_CheckFailedThenSpecificExpectedExceptionThrown_Fails(); - void Test_ExceptionExpectedButNoneThrown_Fails(); - void Test_GenericExceptionExpected_AssertFails_Fails(); - void Test_GenericExceptionExpected_NonStdExceptionThrown_Passes(); - void Test_GenericExceptionExpected_StdExceptionThrown_Passes(); - void Test_SpecificTypeExpected_AssertFails_Fails(); - void Test_SpecificTypeExpected_ExactTypeThrown_Passes(); - void Test_SpecificTypeExpected_MessageSubstringAbsent_Fails(); - void Test_SpecificTypeExpected_MessageSubstringPresent_Passes(); - void Test_SpecificTypeExpected_NonStdExceptionThrown_Fails(); - void Test_SpecificTypeExpected_PolymorphicBaseTypeThrown_Passes(); - void Test_SpecificTypeExpected_WrongSiblingTypeThrown_Fails(); - -public: - TFixture_ExceptionExpectations(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_ExceptionExpectations::TFixture_ExceptionExpectations() @@ -2062,10 +3562,14 @@ TFixture_ExceptionExpectations::TFixture_ExceptionExpectations() "SpecificTypeExpected_WrongSiblingTypeThrown_Fails"); } //--------------------------------------------------------------------------- -// The next four each fail a Check* and then let an exception arrive while one is expected. A std::exception -// matching a generic or a specific expectation, and a non-std::exception object, are each a way Test() can -// otherwise reach a pass, and the earlier Check* failure must still fail the test on every one of them. An -// Assert* failure never counts as the expected exception; that case checks both failures reach the record. +/* + TFixture_ExceptionExpectations::Test_CheckFailedThenAssertFailedWhileExceptionExpected_Fails + + The next four each fail a Check* and then let an exception arrive while one is expected. A std::exception + matching a generic or a specific expectation, and a non-std::exception object, are each a way Test() can + otherwise reach a pass, and the earlier Check* failure must still fail the test on every one of them. An + Assert* failure never counts as the expected exception; that case checks both failures reach the record. +*/ void TFixture_ExceptionExpectations::Test_CheckFailedThenAssertFailedWhileExceptionExpected_Fails() { SetExceptionExpected(true, __func__, __LINE__, "generic expectation, then a failed Check and a failed Assert"); @@ -2167,30 +3671,7 @@ void TFixture_ExceptionExpectations::Test_SpecificTypeExpected_WrongSiblingTypeT ///////////////////////////////////////////////////////////////////////////// // TFixture_FailCalls -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling Fail(), so Test_Fail_AbortsTestAsFailed -// below can check it fails the test, stops it (ReachedAfterFail stays false), still fails while an exception is -// expected, and keeps an earlier Check failure in the record. Test names self-document expected outcome via -// NameEndsWith(), same as TFixture_ExceptionExpectations above. ///////////////////////////////////////////////////////////////////////////// -class TFixture_FailCalls : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_Fail_AfterCheckFailure_Fails(); - void Test_Fail_Fails(); - void Test_Fail_WhileExceptionExpected_Fails(); - -public: - bool ReachedAfterFail = false; - - TFixture_FailCalls(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_FailCalls::TFixture_FailCalls() @@ -2224,46 +3705,29 @@ void TFixture_FailCalls::Test_Fail_WhileExceptionExpected_Fails() ///////////////////////////////////////////////////////////////////////////// -// TFixture_IntegerComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing the integer template overloads -// of AssertEquals()/CheckEquals()/AssertNotEquals()/CheckNotEquals(), for two integer types that no -// fixed-width overload matches exactly. Without those overloads, most of these calls don't compile -// (ambiguous). The "NegativeAndUnsignedMax" tests catch a comparison that lets -1 wrap around to an -// unsigned maximum, and the "Int64Max" tests check the int64_t/uint64_t boundary. Test names -// self-document expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. +// TFixture_FloatingPointEnvironment ///////////////////////////////////////////////////////////////////////////// -class TFixture_IntegerComparisons : public TTestGroupBase + +//--------------------------------------------------------------------------- +TFixture_FloatingPointEnvironment::TFixture_FloatingPointEnvironment() + : inherited("Fixture_FloatingPointEnvironment") { -private: - typedef TTestGroupBase inherited; + SetLogSuppressed(true); -private: - void Test_AssertEquals_IntAndInt64Different_Fails(); - void Test_AssertEquals_IntAndInt64Equal_Passes(); - void Test_AssertNotEquals_IntAndInt64Equal_Fails(); - void Test_AssertNotEquals_NegativeAndUnsignedMax_Passes(); - void Test_CheckEquals_AboveInt64MaxAndInt64Max_Fails(); - void Test_CheckEquals_Int64MaxAndUint64_Passes(); - void Test_CheckEquals_IntAndInt64Different_Fails(); - void Test_CheckEquals_IntAndInt64Equal_Passes(); - void Test_CheckEquals_IntAndShort_Passes(); - void Test_CheckEquals_IntAndUnsigned_Passes(); - void Test_CheckEquals_LongAndLong_Passes(); - void Test_CheckEquals_LongLongAndLongLong_Passes(); - void Test_CheckEquals_NegativeAndUint64Max_Fails(); - void Test_CheckEquals_NegativeAndUnsignedMax_Fails(); - void Test_CheckEquals_UnsignedLongAndUnsignedLong_Passes(); - void Test_CheckNotEquals_IntAndInt64Equal_Fails(); - void Test_CheckNotEquals_NegativeAndUint64Max_Passes(); - void Test_CheckNotEquals_NegativeAndUnsignedMax_Passes(); + RegisterTest(&TFixture_FloatingPointEnvironment::Test_RecordsEnvironment, "RecordsEnvironment"); +} +//--------------------------------------------------------------------------- +void TFixture_FloatingPointEnvironment::Test_RecordsEnvironment() +{ + LongDoublePrecisionInTest = LongDoubleKeepsPrecision(); + RoundingModeInTest = RoundingMode(); +} +//--------------------------------------------------------------------------- -public: - TFixture_IntegerComparisons(); - void SetUp_Group() override {} - void TearDown_Group() override {} -}; +///////////////////////////////////////////////////////////////////////////// +// TFixture_IntegerComparisons +///////////////////////////////////////////////////////////////////////////// //--------------------------------------------------------------------------- TFixture_IntegerComparisons::TFixture_IntegerComparisons() @@ -2407,30 +3871,7 @@ void TFixture_IntegerComparisons::Test_CheckNotEquals_NegativeAndUnsignedMax_Pas ///////////////////////////////////////////////////////////////////////////// // TFixture_LateFinish -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one test that sleeps past a one second -// timeout, then fails a Check, logs a line and ends, followed by one that must never run. Logging is left on, for a -// run observer to collect. Used by Test_Run_IgnoresTestThatEndsAfterItsTimeout below to prove an abandoned test that -// ends after all changes neither the results nor the log. ///////////////////////////////////////////////////////////////////////////// -class TFixture_LateFinish : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_EndsJustAfterTimeout(); - void Test_NeverRuns(); - -public: - std::atomic Ended{ false }; // Set as the first test ends, after its Check failure and its log line. - -public: - TFixture_LateFinish(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_LateFinish::TFixture_LateFinish() @@ -2457,39 +3898,7 @@ void TFixture_LateFinish::Test_NeverRuns() ///////////////////////////////////////////////////////////////////////////// // TFixture_MatchesChecks -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertMatches()/CheckMatches()/ -// AssertNotMatches()/CheckNotMatches(), narrow and wide, so Test_Matches_MatchesWholeTextAndShowsPattern below can -// check their outcomes and messages. "PartialMatch" shows the whole text must match, and "InvalidPattern" that a bad -// pattern fails the check instead of throwing std::regex_error. Test names self-document expected outcome via -// NameEndsWith(), same as TFixture_ExceptionExpectations above. ///////////////////////////////////////////////////////////////////////////// -class TFixture_MatchesChecks : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertMatches_InvalidPattern_Fails(); - void Test_AssertMatches_Matching_Passes(); - void Test_AssertMatches_NotMatching_Fails(); - void Test_AssertNotMatches_Matching_Fails(); - void Test_AssertNotMatches_NotMatching_Passes(); - void Test_CheckMatches_InvalidPattern_Fails(); - void Test_CheckMatches_Matching_Passes(); - void Test_CheckMatches_PartialMatch_Fails(); - void Test_CheckMatches_Wide_Matching_Passes(); - void Test_CheckMatches_Wide_NotMatching_Fails(); - void Test_CheckNotMatches_InvalidPattern_Fails(); - void Test_CheckNotMatches_Matching_Fails(); - void Test_CheckNotMatches_NotMatching_Passes(); - -public: - TFixture_MatchesChecks(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_MatchesChecks::TFixture_MatchesChecks() @@ -2589,39 +3998,7 @@ void TFixture_MatchesChecks::Test_CheckNotMatches_NotMatching_Passes() ///////////////////////////////////////////////////////////////////////////// // TFixture_MemoryComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEqualsMem()/CheckEqualsMem()/ -// AssertNotEqualsMem()/CheckNotEqualsMem(), so Test_EqualsMem_ShowsFirstDifferingBytes below can check their outcomes -// and messages: a difference at the start, in the middle and past 16 bytes, zero bytes, the same pointer twice, and -// null pointers. Test names self-document expected outcome via NameEndsWith(), same as -// TFixture_ExceptionExpectations above. ///////////////////////////////////////////////////////////////////////////// -class TFixture_MemoryComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertEqualsMem_Different_Fails(); - void Test_AssertEqualsMem_Same_Passes(); - void Test_AssertNotEqualsMem_Different_Passes(); - void Test_AssertNotEqualsMem_Same_Fails(); - void Test_CheckEqualsMem_ActualNull_Fails(); - void Test_CheckEqualsMem_BothNull_Passes(); - void Test_CheckEqualsMem_DifferentLater_Fails(); - void Test_CheckEqualsMem_LongDifference_Fails(); - void Test_CheckEqualsMem_SameBuffer_Passes(); - void Test_CheckEqualsMem_ZeroSize_Passes(); - void Test_CheckNotEqualsMem_Different_Passes(); - void Test_CheckNotEqualsMem_NullAndBuffer_Passes(); - void Test_CheckNotEqualsMem_Same_Fails(); - -public: - TFixture_MemoryComparisons(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_MemoryComparisons::TFixture_MemoryComparisons() @@ -2736,47 +4113,18 @@ void TFixture_MemoryComparisons::Test_CheckNotEqualsMem_NullAndBuffer_Passes() CheckNotEqualsMem(nullptr, actual, sizeof(actual), __func__, __LINE__, "null isn't equal to any memory"); } //--------------------------------------------------------------------------- -void TFixture_MemoryComparisons::Test_CheckNotEqualsMem_Same_Fails() -{ - unsigned char const expected[] = { 0xDE, 0xAD, 0xBE, 0xEF }; - unsigned char const actual[] = { 0xDE, 0xAD, 0xBE, 0xEF }; - CheckNotEqualsMem(expected, actual, sizeof(expected), __func__, __LINE__, "same bytes"); -} -//--------------------------------------------------------------------------- - - -///////////////////////////////////////////////////////////////////////////// -// TFixture_MixedOutcomes -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one test method per outcome -// TTestGroupBase can produce, plus one that fails twice in a row to prove a Check failure doesn't -// abort the rest of the test the way an Assert failure does, and one whose Check failure is -// followed by an Assert failure, so both end up in its record's detail. Constructed and run directly by -// TTest_ASWUnitTests_TestBase's own test methods below, so its deliberate failures/skip only ever -// affect this fixture's own Results() - never the real, self-registered suite running it. -///////////////////////////////////////////////////////////////////////////// -class TFixture_MixedOutcomes : public TTestGroupBase +void TFixture_MemoryComparisons::Test_CheckNotEqualsMem_Same_Fails() { -private: - typedef TTestGroupBase inherited; - -private: - void Test_ContinuesAfterCheckFailure(); - void Test_FailViaAssert(); - void Test_FailViaCheck(); - void Test_FailViaCheckThenAssert(); - void Test_Pass(); - void Test_Skip(); - -public: - bool ReachedSecondCheck = false; + unsigned char const expected[] = { 0xDE, 0xAD, 0xBE, 0xEF }; + unsigned char const actual[] = { 0xDE, 0xAD, 0xBE, 0xEF }; + CheckNotEqualsMem(expected, actual, sizeof(expected), __func__, __LINE__, "same bytes"); +} +//--------------------------------------------------------------------------- -public: - explicit TFixture_MixedOutcomes(bool suppressLog = true); - void SetUp_Group() override {} - void TearDown_Group() override {} -}; +///////////////////////////////////////////////////////////////////////////// +// TFixture_MixedOutcomes +///////////////////////////////////////////////////////////////////////////// //--------------------------------------------------------------------------- TFixture_MixedOutcomes::TFixture_MixedOutcomes(bool suppressLog) @@ -2829,31 +4177,7 @@ void TFixture_MixedOutcomes::Test_Skip() ///////////////////////////////////////////////////////////////////////////// // TFixture_NearComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing CheckNear()/AssertNear()/ -// CheckNotNear()'s tolerance boundary: the implementation passes only when diff <= tolerance, so a -// diff exactly equal to tolerance must still count as "near". Test names self-document expected -// outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. ///////////////////////////////////////////////////////////////////////////// -class TFixture_NearComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertNear_JustOutsideTolerance_Fails(); - void Test_CheckNear_AtToleranceBoundary_Passes(); - void Test_CheckNear_Double_AtToleranceBoundary_Passes(); - void Test_CheckNear_JustOutsideTolerance_Fails(); - void Test_CheckNotNear_AtToleranceBoundary_Fails(); - void Test_CheckNotNear_JustOutsideTolerance_Passes(); - -public: - TFixture_NearComparisons(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_NearComparisons::TFixture_NearComparisons() @@ -2908,44 +4232,183 @@ void TFixture_NearComparisons::Test_CheckNotNear_JustOutsideTolerance_Passes() ///////////////////////////////////////////////////////////////////////////// -// TFixture_NearNonFiniteValues -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling the Near and NotNear methods, float and -// double, with NaNs and infinities, so Test_Near_HandlesNaNAndInfinity below can check that a NaN is near nothing (not -// even another NaN, however large the tolerance) and that equal infinities are near each other, though a NaN -// difference or an infinite one compares false with any tolerance. Test names self-document expected outcome via -// NameEndsWith(), same as TFixture_ExceptionExpectations above. +// TFixture_NearMixedTypes ///////////////////////////////////////////////////////////////////////////// -class TFixture_NearNonFiniteValues : public TTestGroupBase + +//--------------------------------------------------------------------------- +TFixture_NearMixedTypes::TFixture_NearMixedTypes() + : inherited("Fixture_NearMixedTypes") { -private: - typedef TTestGroupBase inherited; + SetLogSuppressed(true); -private: - void Test_AssertNear_NaN_Fails(); - void Test_AssertNotNear_EqualInfinities_Fails(); - void Test_AssertNotNear_NaN_Passes(); - void Test_CheckNear_BothNaN_Fails(); - void Test_CheckNear_EqualInfinities_Passes(); - void Test_CheckNear_EqualNegativeInfinities_Passes(); - void Test_CheckNear_Float_EqualInfinities_Passes(); - void Test_CheckNear_Float_NaN_Fails(); - void Test_CheckNear_InfinityAndFinite_Fails(); - void Test_CheckNear_NaNActual_Fails(); - void Test_CheckNear_NaNExpected_Fails(); - void Test_CheckNear_OppositeInfinities_Fails(); - void Test_CheckNotNear_BothNaN_Passes(); - void Test_CheckNotNear_EqualInfinities_Fails(); - void Test_CheckNotNear_Float_NaN_Passes(); - void Test_CheckNotNear_InfinityAndFinite_Passes(); - void Test_CheckNotNear_NaN_Passes(); + RegisterTest(&TFixture_NearMixedTypes::Test_AssertNear_Integers_Fails, "AssertNear_Integers_Fails"); + RegisterTest(&TFixture_NearMixedTypes::Test_AssertNear_NegativeTolerance_Fails, + "AssertNear_NegativeTolerance_Fails"); + RegisterTest(&TFixture_NearMixedTypes::Test_AssertNotNear_FloatNaNTolerance_Fails, + "AssertNotNear_FloatNaNTolerance_Fails"); + RegisterTest(&TFixture_NearMixedTypes::Test_AssertNotNear_MixedSigns_Passes, "AssertNotNear_MixedSigns_Passes"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_DoubleAndFloat_Fails, "CheckNear_DoubleAndFloat_Fails"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_DoubleAndFloat_Passes, "CheckNear_DoubleAndFloat_Passes"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_FloatsAndDoubleTolerance_Passes, + "CheckNear_FloatsAndDoubleTolerance_Passes"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_IntegerBoundary_Passes, "CheckNear_IntegerBoundary_Passes"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_IntegerExtremes_Passes, "CheckNear_IntegerExtremes_Passes"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_IntegerSumOverflows_Fails, + "CheckNear_IntegerSumOverflows_Fails"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_Integers_Fails, "CheckNear_Integers_Fails"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_LongDouble_Fails, "CheckNear_LongDouble_Fails"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_LongDouble_Passes, "CheckNear_LongDouble_Passes"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_MixedSigns_Passes, "CheckNear_MixedSigns_Passes"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_NaNAndFloat_Fails, "CheckNear_NaNAndFloat_Fails"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_NaNTolerance_Fails, "CheckNear_NaNTolerance_Fails"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_NegativeIntegerTolerance_Fails, + "CheckNear_NegativeIntegerTolerance_Fails"); +#if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNear_SourceLocation_Passes, "CheckNear_SourceLocation_Passes"); +#endif + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNotNear_DoubleAndFloat_Fails, "CheckNotNear_DoubleAndFloat_Fails"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNotNear_Integers_Fails, "CheckNotNear_Integers_Fails"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNotNear_LongDoublePrecision_Passes, + "CheckNotNear_LongDoublePrecision_Passes"); + RegisterTest(&TFixture_NearMixedTypes::Test_CheckNotNear_NegativeTolerance_Fails, + "CheckNotNear_NegativeTolerance_Fails"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::CheckNear(double expected, double actual, double tolerance, std::string const& method, + int line, std::string const& msg) +{ + ++DoubleCheckNearCalls; + inherited::CheckNear(expected, actual, tolerance, method, line, msg); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_AssertNear_Integers_Fails() +{ + AssertNear(10, 13, 2, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_AssertNear_NegativeTolerance_Fails() +{ + AssertNear(1.0, 1.0, -0.5, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_AssertNotNear_FloatNaNTolerance_Fails() +{ + AssertNotNear(1.0f, 5.0f, std::numeric_limits::quiet_NaN(), __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_AssertNotNear_MixedSigns_Passes() +{ + // -1 and 1u differ by 2: as their common type (unsigned int), -1 would wrap to UINT_MAX. + AssertNotNear(-1, 1u, 1, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNear_DoubleAndFloat_Fails() +{ + CheckNear(2.0, 2.5f, 0.01, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNear_DoubleAndFloat_Passes() +{ + float const actual = 2.005f; + CheckNear(2.0, actual, 0.01, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNear_FloatsAndDoubleTolerance_Passes() +{ + CheckNear(1.0f, 1.5f, 0.5, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNear_IntegerBoundary_Passes() +{ + std::size_t const count = 11; + CheckNear(10, count, 1, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNear_IntegerExtremes_Passes() +{ + CheckNear(std::numeric_limits::min(), std::numeric_limits::max(), + std::numeric_limits::max(), __func__, __LINE__, "a difference of exactly UINT64_MAX"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNear_IntegerSumOverflows_Fails() +{ + CheckNear(static_cast(-1), std::numeric_limits::max(), std::numeric_limits::max(), + __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNear_Integers_Fails() +{ + CheckNear(10, static_cast(12), 1, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNear_LongDouble_Fails() +{ + CheckNear(1.0L, 1.5L, 0.25L, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNear_LongDouble_Passes() +{ + CheckNear(1.0L, 1.25L, 0.25L, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNear_MixedSigns_Passes() +{ + CheckNear(-1, 1u, 2, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNear_NaNAndFloat_Fails() +{ + CheckNear(std::numeric_limits::quiet_NaN(), 1.0f, 1, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNear_NaNTolerance_Fails() +{ + CheckNear(1.0, 1.0, std::numeric_limits::quiet_NaN(), __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNear_NegativeIntegerTolerance_Fails() +{ + CheckNear(5, 5, -1, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +#if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) +void TFixture_NearMixedTypes::Test_CheckNear_SourceLocation_Passes() +{ + int const count = 11; + CheckNear(10, count, 1, "integers through the std::source_location form"); +} +//--------------------------------------------------------------------------- +#endif +void TFixture_NearMixedTypes::Test_CheckNotNear_DoubleAndFloat_Fails() +{ + CheckNotNear(2.0, 2.005f, 0.01, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNotNear_Integers_Fails() +{ + CheckNotNear(10, 12, 5, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNotNear_LongDoublePrecision_Passes() +{ + // Computed as long double: where it's wider than double, these differ, though as doubles they'd be equal. The + // sum is a constant, since one computed at run time can be rounded to double precision: a llvm-mingw std::thread + // (where --test-timeout-seconds runs a test) starts with the x87 FPU at 53-bit precision. + constexpr long double actual = 1.0L + std::numeric_limits::epsilon(); + CheckNotNear(1.0L, actual, 0.0L, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- +void TFixture_NearMixedTypes::Test_CheckNotNear_NegativeTolerance_Fails() +{ + // Passed before 1.5: no difference is at most a negative tolerance. + CheckNotNear(1.0, 5.0, -1.0, __func__, __LINE__, "msg"); +} +//--------------------------------------------------------------------------- -public: - TFixture_NearNonFiniteValues(); - void SetUp_Group() override {} - void TearDown_Group() override {} -}; +///////////////////////////////////////////////////////////////////////////// +// TFixture_NearNonFiniteValues +///////////////////////////////////////////////////////////////////////////// //--------------------------------------------------------------------------- TFixture_NearNonFiniteValues::TFixture_NearNonFiniteValues() @@ -3041,104 +4504,47 @@ void TFixture_NearNonFiniteValues::Test_CheckNear_NaNExpected_Fails() { CheckNear(std::numeric_limits::quiet_NaN(), 1.0, 0.1, __func__, __LINE__, "a NaN as the expected value"); } -//--------------------------------------------------------------------------- -void TFixture_NearNonFiniteValues::Test_CheckNear_OppositeInfinities_Fails() -{ - double const inf = std::numeric_limits::infinity(); - CheckNear(inf, -inf, 0.1, __func__, __LINE__, "opposite infinities are far apart"); -} -//--------------------------------------------------------------------------- -void TFixture_NearNonFiniteValues::Test_CheckNotNear_BothNaN_Passes() -{ - double const nan = std::numeric_limits::quiet_NaN(); - CheckNotNear(nan, nan, 0.1, __func__, __LINE__, "a NaN isn't near another NaN"); -} -//--------------------------------------------------------------------------- -void TFixture_NearNonFiniteValues::Test_CheckNotNear_EqualInfinities_Fails() -{ - double const inf = std::numeric_limits::infinity(); - CheckNotNear(inf, inf, 0.1, __func__, __LINE__, "equal infinities are near each other"); -} -//--------------------------------------------------------------------------- -void TFixture_NearNonFiniteValues::Test_CheckNotNear_Float_NaN_Passes() -{ - CheckNotNear(1.0f, std::numeric_limits::quiet_NaN(), 0.1f, __func__, __LINE__, "float overload, NaN"); -} -//--------------------------------------------------------------------------- -void TFixture_NearNonFiniteValues::Test_CheckNotNear_InfinityAndFinite_Passes() -{ - double const inf = std::numeric_limits::infinity(); - CheckNotNear(inf, 1.0, std::numeric_limits::max(), __func__, __LINE__, "an infinite difference"); -} -//--------------------------------------------------------------------------- -void TFixture_NearNonFiniteValues::Test_CheckNotNear_NaN_Passes() -{ - CheckNotNear(1.0, std::numeric_limits::quiet_NaN(), std::numeric_limits::max(), __func__, __LINE__, - "no tolerance makes a NaN near"); -} -//--------------------------------------------------------------------------- - - -///////////////////////////////////////////////////////////////////////////// -// TFixture_NullCStringTextChecks -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling the C string overloads of the IC Equals, -// Contains, StartsWith and EndsWith methods, Check and Assert, narrow and wide, and their Not forms, each with a null -// text, a null part, both null, and a matching non-null pair, so Test_NullCStrings_MatchNothingInTextChecks below -// can check that a null C string matches nothing in the text checks and equals only another null one in the IC -// Equals ones. Test names self-document expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations -// above. -///////////////////////////////////////////////////////////////////////////// -class TFixture_NullCStringTextChecks : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - - // How a method treats a null C string, which decides each case's expected outcome. - enum class TNullRule - { - NeverMatches, // Contains, StartsWith, EndsWith: fails for any null - NotNeverMatches, // their Not forms: passes for any null - EqualsOnlyNull, // EqualsIC: fails for one null, passes for two - NotEqualsOnlyNull // NotEqualsIC: passes for one null, fails for two - }; - - char const* const m_NullNarrow = nullptr; - wchar_t const* const m_NullWide = nullptr; - - // Registers 'name''s eight cases: null text, null part, both null and a matching pair, narrow and wide. 'call' - // takes the text and the part, as two char const* or two wchar_t const*, and calls the method with them. The - // matching pair is "ABC" and "abc" for an IC method and "abc" and "abc" otherwise, which every method matches. - template - void RegisterCases(std::string const& name, TNullRule rule, bool ignoreCase, TCall call) - { - bool const negated = rule == TNullRule::NotNeverMatches || rule == TNullRule::NotEqualsOnlyNull; - char const* const outcomeWithOneNull = - (rule == TNullRule::NeverMatches || rule == TNullRule::EqualsOnlyNull) ? "_Fails" : "_Passes"; - char const* const outcomeWithBothNull = - (rule == TNullRule::NeverMatches || rule == TNullRule::NotEqualsOnlyNull) ? "_Fails" : "_Passes"; - char const* const outcomeWhenMatching = negated ? "_Fails" : "_Passes"; - char const* const text = ignoreCase ? "ABC" : "abc"; - wchar_t const* const wideText = ignoreCase ? L"ABC" : L"abc"; - - RegisterTest([this, call]() { call(m_NullNarrow, "abc"); }, name + "_Narrow_NullText" + outcomeWithOneNull); - RegisterTest([call, text]() { call(text, static_cast(nullptr)); }, - name + "_Narrow_NullPart" + outcomeWithOneNull); - RegisterTest([this, call]() { call(m_NullNarrow, m_NullNarrow); }, name + "_Narrow_BothNull" + outcomeWithBothNull); - RegisterTest([call, text]() { call(text, "abc"); }, name + "_Narrow_Matching" + outcomeWhenMatching); - RegisterTest([this, call]() { call(m_NullWide, L"abc"); }, name + "_Wide_NullText" + outcomeWithOneNull); - RegisterTest([call, wideText]() { call(wideText, static_cast(nullptr)); }, - name + "_Wide_NullPart" + outcomeWithOneNull); - RegisterTest([this, call]() { call(m_NullWide, m_NullWide); }, name + "_Wide_BothNull" + outcomeWithBothNull); - RegisterTest([call, wideText]() { call(wideText, L"abc"); }, name + "_Wide_Matching" + outcomeWhenMatching); - } +//--------------------------------------------------------------------------- +void TFixture_NearNonFiniteValues::Test_CheckNear_OppositeInfinities_Fails() +{ + double const inf = std::numeric_limits::infinity(); + CheckNear(inf, -inf, 0.1, __func__, __LINE__, "opposite infinities are far apart"); +} +//--------------------------------------------------------------------------- +void TFixture_NearNonFiniteValues::Test_CheckNotNear_BothNaN_Passes() +{ + double const nan = std::numeric_limits::quiet_NaN(); + CheckNotNear(nan, nan, 0.1, __func__, __LINE__, "a NaN isn't near another NaN"); +} +//--------------------------------------------------------------------------- +void TFixture_NearNonFiniteValues::Test_CheckNotNear_EqualInfinities_Fails() +{ + double const inf = std::numeric_limits::infinity(); + CheckNotNear(inf, inf, 0.1, __func__, __LINE__, "equal infinities are near each other"); +} +//--------------------------------------------------------------------------- +void TFixture_NearNonFiniteValues::Test_CheckNotNear_Float_NaN_Passes() +{ + CheckNotNear(1.0f, std::numeric_limits::quiet_NaN(), 0.1f, __func__, __LINE__, "float overload, NaN"); +} +//--------------------------------------------------------------------------- +void TFixture_NearNonFiniteValues::Test_CheckNotNear_InfinityAndFinite_Passes() +{ + double const inf = std::numeric_limits::infinity(); + CheckNotNear(inf, 1.0, std::numeric_limits::max(), __func__, __LINE__, "an infinite difference"); +} +//--------------------------------------------------------------------------- +void TFixture_NearNonFiniteValues::Test_CheckNotNear_NaN_Passes() +{ + CheckNotNear(1.0, std::numeric_limits::quiet_NaN(), std::numeric_limits::max(), __func__, __LINE__, + "no tolerance makes a NaN near"); +} +//--------------------------------------------------------------------------- -public: - TFixture_NullCStringTextChecks(); - void SetUp_Group() override {} - void TearDown_Group() override {} -}; +///////////////////////////////////////////////////////////////////////////// +// TFixture_NullCStringTextChecks +///////////////////////////////////////////////////////////////////////////// //--------------------------------------------------------------------------- TFixture_NullCStringTextChecks::TFixture_NullCStringTextChecks() @@ -3272,41 +4678,7 @@ TFixture_NullCStringTextChecks::TFixture_NullCStringTextChecks() ///////////////////////////////////////////////////////////////////////////// // TFixture_NullChecks -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling AssertNull()/CheckNull()/AssertNotNull()/ -// CheckNotNull() with null and non-null raw pointers, smart pointers, std::function objects, C strings and a nullptr -// literal, so Test_NullNotNull_FailureNamesTheExpectedValue below can check each is compared with nullptr and that -// each failure states the value that was expected. Test names self-document expected outcome via NameEndsWith(), same -// as TFixture_ExceptionExpectations above. ///////////////////////////////////////////////////////////////////////////// -class TFixture_NullChecks : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertNotNull_NonNull_Passes(); - void Test_AssertNotNull_Null_Fails(); - void Test_AssertNull_NonNull_Fails(); - void Test_AssertNull_Null_Passes(); - void Test_CheckNotNull_EmptyCString_Passes(); - void Test_CheckNotNull_EmptyFunction_Fails(); - void Test_CheckNotNull_NonNull_Passes(); - void Test_CheckNotNull_Null_Fails(); - void Test_CheckNotNull_SharedPtr_Passes(); - void Test_CheckNull_EmptyFunction_Passes(); - void Test_CheckNull_NonNull_Fails(); - void Test_CheckNull_NullCString_Passes(); - void Test_CheckNull_Null_Passes(); - void Test_CheckNull_NullptrLiteral_Passes(); - void Test_CheckNull_UniquePtr_Fails(); - -public: - TFixture_NullChecks(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_NullChecks::TFixture_NullChecks() @@ -3424,37 +4796,7 @@ void TFixture_NullChecks::Test_CheckNull_UniquePtr_Fails() ///////////////////////////////////////////////////////////////////////////// // TFixture_OrderRecorder -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group of 8 trivially-passing tests (A-H) -// that each record their own name into a caller-owned vector when run, for verifying Run()'s -// --shuffle ordering and TestFilter application. 8 tests keeps the odds of a shuffled order -// coincidentally matching the unshuffled one at 1-in-40320 or better, low enough that -// Test_Run_ShuffleSeedProducesDeterministicOrder below won't flake. ///////////////////////////////////////////////////////////////////////////// -class TFixture_OrderRecorder : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - std::vector& m_ExecutionOrder; - -private: - void Test_A(); - void Test_B(); - void Test_C(); - void Test_D(); - void Test_E(); - void Test_F(); - void Test_G(); - void Test_H(); - -public: - explicit TFixture_OrderRecorder(std::vector& executionOrder); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_OrderRecorder::TFixture_OrderRecorder(std::vector& executionOrder) @@ -3517,62 +4859,7 @@ void TFixture_OrderRecorder::Test_H() ///////////////////////////////////////////////////////////////////////////// // TFixture_OrderingComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing the GreaterThan/GreaterThanOrEqual/ -// LessThan/LessThanOrEqual methods, each called as (value, bound). The "Negative...Max" and "...MaxAndNegative" tests -// catch a comparison that lets -1 wrap around to an unsigned maximum (the built-in -1 < 4294967295u is false), the -// "Uint64Max" ones the case where neither value fits the other's type, and the NaN ones that a NaN fails every check. -// Test names self-document expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. ///////////////////////////////////////////////////////////////////////////// -class TFixture_OrderingComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertGreaterThanOrEqual_Equal_Passes(); - void Test_AssertGreaterThanOrEqual_Less_Fails(); - void Test_AssertGreaterThan_Equal_Fails(); - void Test_AssertGreaterThan_Greater_Passes(); - void Test_AssertLessThanOrEqual_Equal_Passes(); - void Test_AssertLessThanOrEqual_Greater_Fails(); - void Test_AssertLessThan_Equal_Fails(); - void Test_AssertLessThan_Less_Passes(); - void Test_CheckGreaterThanOrEqual_Equal_Passes(); - void Test_CheckGreaterThanOrEqual_Greater_Passes(); - void Test_CheckGreaterThanOrEqual_Less_Fails(); - void Test_CheckGreaterThanOrEqual_NaN_Fails(); - void Test_CheckGreaterThan_AboveInt64MaxAndInt64Max_Passes(); - void Test_CheckGreaterThan_DoubleAndInt_Passes(); - void Test_CheckGreaterThan_Double_Fails(); - void Test_CheckGreaterThan_Equal_Fails(); - void Test_CheckGreaterThan_Float_Fails(); - void Test_CheckGreaterThan_Greater_Passes(); - void Test_CheckGreaterThan_Infinity_Passes(); - void Test_CheckGreaterThan_LongDouble_Fails(); - void Test_CheckGreaterThan_Less_Fails(); - void Test_CheckGreaterThan_Uint64MaxAndNegative_Passes(); - void Test_CheckGreaterThan_UnsignedMaxAndNegative_Passes(); - void Test_CheckLessThanOrEqual_Equal_Passes(); - void Test_CheckLessThanOrEqual_Greater_Fails(); - void Test_CheckLessThanOrEqual_IntAndInt64_Passes(); - void Test_CheckLessThanOrEqual_Less_Passes(); - void Test_CheckLessThanOrEqual_NaNBound_Fails(); - void Test_CheckLessThan_CloseDoubles_Fails(); - void Test_CheckLessThan_Equal_Fails(); - void Test_CheckLessThan_Greater_Fails(); - void Test_CheckLessThan_Less_Passes(); - void Test_CheckLessThan_NaN_Fails(); - void Test_CheckLessThan_NegativeAndUint64Max_Passes(); - void Test_CheckLessThan_NegativeAndUnsignedMax_Passes(); - void Test_CheckLessThan_Uint64MaxAndNegative_Fails(); - -public: - TFixture_OrderingComparisons(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_OrderingComparisons::TFixture_OrderingComparisons() @@ -3831,56 +5118,7 @@ void TFixture_OrderingComparisons::Test_CheckLessThan_Uint64MaxAndNegative_Fails ///////////////////////////////////////////////////////////////////////////// // TFixture_PointerComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertEquals()/CheckEquals()/ -// AssertNotEquals()/CheckNotEquals() with two pointers. Without the pointer overloads, two pointers convert to bool -// and match the bool overload, so two different non-null pointers compare equal and the "DifferentPointers" tests -// pass/fail the wrong way round. The tests compare the addresses of First and Second, so -// Test_Equals_ComparesPointersByAddress below can check the addresses a failure shows. Test names self-document -// expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. ///////////////////////////////////////////////////////////////////////////// -class TFixture_PointerComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - - struct TBase - { - int Value = 0; - }; - - struct TDerived : TBase - { - }; - - // Different bodies, so a linker that merges identical functions (e.g. MSVC's /OPT:ICF) can't give them one address. - static int FirstFunction() { return 1; } - static int SecondFunction() { return 2; } - -private: - void Test_AssertEquals_DifferentPointers_Fails(); - void Test_AssertEquals_SamePointer_Passes(); - void Test_AssertNotEquals_DifferentPointers_Passes(); - void Test_AssertNotEquals_SamePointer_Fails(); - void Test_CheckEquals_BaseAndDerived_Passes(); - void Test_CheckEquals_DifferentFunctions_Fails(); - void Test_CheckEquals_DifferentPointers_Fails(); - void Test_CheckEquals_MutableCStringBuffer_Passes(); - void Test_CheckEquals_NullAndNonNull_Fails(); - void Test_CheckEquals_SamePointer_Passes(); - void Test_CheckEquals_VoidAndTyped_Passes(); - void Test_CheckNotEquals_DifferentPointers_Passes(); - void Test_CheckNotEquals_SamePointer_Fails(); - -public: - int First = 0; - int Second = 0; - - TFixture_PointerComparisons(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_PointerComparisons::TFixture_PointerComparisons() @@ -3975,56 +5213,20 @@ void TFixture_PointerComparisons::Test_CheckEquals_VoidAndTyped_Passes() } //--------------------------------------------------------------------------- void TFixture_PointerComparisons::Test_CheckNotEquals_DifferentPointers_Passes() -{ - CheckNotEquals(&First, &Second, __func__, __LINE__, "different objects"); -} -//--------------------------------------------------------------------------- -void TFixture_PointerComparisons::Test_CheckNotEquals_SamePointer_Fails() -{ - CheckNotEquals(&First, &First, __func__, __LINE__, "same object"); -} -//--------------------------------------------------------------------------- - - -///////////////////////////////////////////////////////////////////////////// -// TFixture_SameChecks -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertSame()/CheckSame()/AssertNotSame()/ -// CheckNotSame() with raw pointers, arrays, smart pointers and C strings, which unlike CheckEquals() are compared by -// address, not content. The basic tests compare the addresses of First and Second, so -// Test_Same_ComparesAddressesNotContent below can check the addresses a failure shows. Test names self-document -// expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. -///////////////////////////////////////////////////////////////////////////// -class TFixture_SameChecks : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertNotSame_DifferentObjects_Passes(); - void Test_AssertNotSame_SameObject_Fails(); - void Test_AssertSame_DifferentObjects_Fails(); - void Test_AssertSame_SameObject_Passes(); - void Test_CheckNotSame_CStringsSameContent_Passes(); - void Test_CheckNotSame_SameObject_Fails(); - void Test_CheckSame_ArrayAndPointer_Passes(); - void Test_CheckSame_BothNull_Passes(); - void Test_CheckSame_CStringsSameContent_Fails(); - void Test_CheckSame_DifferentObjects_Fails(); - void Test_CheckSame_RawAndSharedPtr_Passes(); - void Test_CheckSame_SameObject_Passes(); - void Test_CheckSame_SharedPtrCopies_Passes(); - void Test_CheckSame_UniquePtrs_Fails(); - -public: - int First = 0; - int Second = 0; +{ + CheckNotEquals(&First, &Second, __func__, __LINE__, "different objects"); +} +//--------------------------------------------------------------------------- +void TFixture_PointerComparisons::Test_CheckNotEquals_SamePointer_Fails() +{ + CheckNotEquals(&First, &First, __func__, __LINE__, "same object"); +} +//--------------------------------------------------------------------------- - TFixture_SameChecks(); - void SetUp_Group() override {} - void TearDown_Group() override {} -}; +///////////////////////////////////////////////////////////////////////////// +// TFixture_SameChecks +///////////////////////////////////////////////////////////////////////////// //--------------------------------------------------------------------------- TFixture_SameChecks::TFixture_SameChecks() @@ -4136,30 +5338,7 @@ void TFixture_SameChecks::Test_CheckSame_UniquePtrs_Fails() ///////////////////////////////////////////////////////////////////////////// // TFixture_ShortTest -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one test that busy-waits for -// BusyWaitDuration, far less than a millisecond, timed with std::chrono::steady_clock. Logging is left on, for a -// run observer to collect. Used by Test_Run_RecordsDurationOfShortTest below to prove a short test's duration is -// measured, rather than read from a clock too coarse to see it (RAD Studio's 32-bit high_resolution_clock only -// advances about every 10 ms). ///////////////////////////////////////////////////////////////////////////// -class TFixture_ShortTest : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_BusyWaits(); - -public: - static constexpr std::chrono::microseconds BusyWaitDuration{ 200 }; - -public: - TFixture_ShortTest(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_ShortTest::TFixture_ShortTest() @@ -4181,30 +5360,7 @@ void TFixture_ShortTest::Test_BusyWaits() ///////////////////////////////////////////////////////////////////////////// // TFixture_SlowTest -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one test that hangs forever, -// followed by one that must never run if RunWithTimeout() correctly abandons the hung test and -// TTestGroupBase::Run() correctly aborts the rest of the group afterward. Used by -// Test_Run_AbandonsHungTestAndAbortsGroupOnTimeout below. ///////////////////////////////////////////////////////////////////////////// -class TFixture_SlowTest : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_HangsForever(); - void Test_NeverRuns(); - -public: - bool NeverRunReached = false; - -public: - TFixture_SlowTest(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_SlowTest::TFixture_SlowTest() @@ -4234,131 +5390,7 @@ void TFixture_SlowTest::Test_NeverRuns() #if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) ///////////////////////////////////////////////////////////////////////////// // TFixture_SourceLocations -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling each std::source_location -// overload. Each "_Fails"/"_Skips" test records the line of its call in ExpectedLines, so -// Test_SourceLocation_ReportsCallerFunctionAndLine below can check the failure names that test's -// function and line. "_Passes" tests check that arguments forward to the same overloads as the -// method/line form (C strings, mixed integers, wide substrings, exception expectations), and the "IC" ones that -// they reach the case-insensitive overloads. The StartsWith/EndsWith texts are chosen so that forwarding to the wrong -// overload (IC or not, start or end, Not or not) changes the outcome of at least one test. CheckIsEven() is a custom -// helper of the kind a test author might write, passing its caller's location through. ///////////////////////////////////////////////////////////////////////////// -class TFixture_SourceLocations : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void CheckIsEven(int value, std::source_location loc = std::source_location::current()); - - void Test_AssertContainsIC_Passes(); - void Test_AssertContains_Fails(); - void Test_AssertEmpty_Fails(); - void Test_AssertEndsWithIC_Passes(); - void Test_AssertEndsWith_Fails(); - void Test_AssertEqualsIC_Passes(); - void Test_AssertEqualsMem_Fails(); - void Test_AssertEquals_CStrings_Passes(); - void Test_AssertEquals_Fails(); - void Test_AssertFalse_Fails(); - void Test_AssertGreaterThanOrEqual_Passes(); - void Test_AssertGreaterThan_Fails(); - void Test_AssertIsNotType_Fails(); - void Test_AssertIsType_Fails(); - void Test_AssertLessThanOrEqual_Passes(); - void Test_AssertLessThan_Fails(); - void Test_AssertMatches_Fails(); - void Test_AssertNear_Fails(); - void Test_AssertNoThrow_Fails(); - void Test_AssertNotContainsIC_Fails(); - void Test_AssertNotContains_Fails(); - void Test_AssertNotEmpty_Fails(); - void Test_AssertNotEndsWithIC_Fails(); - void Test_AssertNotEndsWith_DifferentCase_Passes(); - void Test_AssertNotEndsWith_Fails(); - void Test_AssertNotEqualsIC_Fails(); - void Test_AssertNotEqualsMem_Fails(); - void Test_AssertNotEquals_Fails(); - void Test_AssertNotMatches_Fails(); - void Test_AssertNotNear_Fails(); - void Test_AssertNotNull_Fails(); - void Test_AssertNotSame_Fails(); - void Test_AssertNotStartsWithIC_Fails(); - void Test_AssertNotStartsWith_DifferentCase_Passes(); - void Test_AssertNotStartsWith_Fails(); - void Test_AssertNull_Fails(); - void Test_AssertSame_Fails(); - void Test_AssertStartsWithIC_Passes(); - void Test_AssertStartsWith_Fails(); - void Test_AssertThrows_Fails(); - void Test_AssertTrue_Fails(); - void Test_CheckContainsIC_Passes(); - void Test_CheckContains_Fails(); - void Test_CheckContains_Wide_Passes(); - void Test_CheckEmpty_Fails(); - void Test_CheckEndsWithIC_Passes(); - void Test_CheckEndsWith_Fails(); - void Test_CheckEndsWith_Wide_Passes(); - void Test_CheckEqualsIC_Passes(); - void Test_CheckEqualsMem_Fails(); - void Test_CheckEquals_EnumClass_Fails(); - void Test_CheckEquals_Fails(); - void Test_CheckEquals_MixedIntegers_Passes(); - void Test_CheckFalse_Fails(); - void Test_CheckGreaterThanOrEqual_Passes(); - void Test_CheckGreaterThan_Fails(); - void Test_CheckIsNotType_Fails(); - void Test_CheckIsType_Fails(); - void Test_CheckLessThanOrEqual_Passes(); - void Test_CheckLessThan_Fails(); - void Test_CheckMatches_Fails(); - void Test_CheckMatches_Wide_Passes(); - void Test_CheckNear_Fails(); - void Test_CheckNear_Passes(); - void Test_CheckNoThrow_Fails(); - void Test_CheckNotContainsIC_Fails(); - void Test_CheckNotContains_Fails(); - void Test_CheckNotEmpty_Fails(); - void Test_CheckNotEndsWithIC_Fails(); - void Test_CheckNotEndsWith_DifferentCase_Passes(); - void Test_CheckNotEndsWith_Fails(); - void Test_CheckNotEqualsIC_Fails(); - void Test_CheckNotEqualsMem_Fails(); - void Test_CheckNotEquals_Fails(); - void Test_CheckNotMatches_Fails(); - void Test_CheckNotNear_Fails(); - void Test_CheckNotNull_Fails(); - void Test_CheckNotSame_Fails(); - void Test_CheckNotSame_UniquePtrs_Passes(); - void Test_CheckNotStartsWithIC_Fails(); - void Test_CheckNotStartsWith_DifferentCase_Passes(); - void Test_CheckNotStartsWith_Fails(); - void Test_CheckNull_Fails(); - void Test_CheckNull_UniquePtr_Passes(); - void Test_CheckSame_Fails(); - void Test_CheckStartsWithIC_Passes(); - void Test_CheckStartsWith_Fails(); - void Test_CheckStartsWith_Wide_Passes(); - void Test_CheckThrows_Fails(); - void Test_CheckThrows_MessageMatches_Passes(); - void Test_CheckTrue_Fails(); - void Test_CheckTrue_ThroughHelper_Fails(); - void Test_Fail_Fails(); - void Test_SetExceptionExpected_Bool_NoneThrown_Fails(); - void Test_SetExceptionExpected_Bool_Passes(); - void Test_SetExceptionExpected_TypeAndMessage_Passes(); - void Test_SetExceptionExpected_Type_NoneThrown_Fails(); - void Test_Skip_Skips(); - -public: - std::map ExpectedLines; // Test name to the line of its failing/skipping call. - - TFixture_SourceLocations(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_SourceLocations::TFixture_SourceLocations() @@ -5061,83 +6093,35 @@ void TFixture_SourceLocations::Test_SetExceptionExpected_Bool_NoneThrown_Fails() } //--------------------------------------------------------------------------- void TFixture_SourceLocations::Test_SetExceptionExpected_Bool_Passes() -{ - SetExceptionExpected(true, "any exception"); - throw std::runtime_error("boom"); -} -//--------------------------------------------------------------------------- -void TFixture_SourceLocations::Test_SetExceptionExpected_TypeAndMessage_Passes() -{ - SetExceptionExpected("matching type and message", "boom"); - throw std::invalid_argument("boom"); -} -//--------------------------------------------------------------------------- -void TFixture_SourceLocations::Test_SetExceptionExpected_Type_NoneThrown_Fails() -{ - ExpectedLines["SetExceptionExpected_Type_NoneThrown_Fails"] = __LINE__ + 1; - SetExceptionExpected("deliberately nothing thrown"); -} -//--------------------------------------------------------------------------- -void TFixture_SourceLocations::Test_Skip_Skips() -{ - ExpectedLines["Skip_Skips"] = __LINE__ + 1; - Skip("deliberate skip"); -} -//--------------------------------------------------------------------------- -#endif // #if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) - - -///////////////////////////////////////////////////////////////////////////// -// TFixture_StartsWithComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertStartsWith()/CheckStartsWith()/ -// AssertNotStartsWith()/CheckNotStartsWith(), narrow and wide. "PrefixOnlyAtEnd" and "PrefixOnlyInMiddle" catch a -// check that matches anywhere but the start, and "PrefixLongerThanText" one that doesn't check the length first. -// "Wide_NonASCII" checks that a wide failure shows its text as UTF-8. Test names self-document expected outcome via -// NameEndsWith(), same as TFixture_ExceptionExpectations above. -///////////////////////////////////////////////////////////////////////////// -class TFixture_StartsWithComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertNotStartsWith_Absent_Passes(); - void Test_AssertNotStartsWith_Present_Fails(); - void Test_AssertNotStartsWith_Wide_Absent_Passes(); - void Test_AssertNotStartsWith_Wide_Present_Fails(); - void Test_AssertStartsWith_Absent_Fails(); - void Test_AssertStartsWith_Present_Passes(); - void Test_AssertStartsWith_Wide_Absent_Fails(); - void Test_AssertStartsWith_Wide_Present_Passes(); - void Test_CheckNotStartsWith_Absent_Passes(); - void Test_CheckNotStartsWith_BothEmpty_Fails(); - void Test_CheckNotStartsWith_EmptyPrefix_Fails(); - void Test_CheckNotStartsWith_PrefixLongerThanText_Passes(); - void Test_CheckNotStartsWith_PrefixOnlyAtEnd_Passes(); - void Test_CheckNotStartsWith_PrefixOnlyInMiddle_Passes(); - void Test_CheckNotStartsWith_Present_Fails(); - void Test_CheckNotStartsWith_Wide_Absent_Passes(); - void Test_CheckNotStartsWith_Wide_Present_Fails(); - void Test_CheckStartsWith_Absent_Fails(); - void Test_CheckStartsWith_BothEmpty_Passes(); - void Test_CheckStartsWith_DifferentCase_Fails(); - void Test_CheckStartsWith_EmptyPrefix_Passes(); - void Test_CheckStartsWith_EmptyText_Fails(); - void Test_CheckStartsWith_PrefixLongerThanText_Fails(); - void Test_CheckStartsWith_PrefixOnlyAtEnd_Fails(); - void Test_CheckStartsWith_PrefixOnlyInMiddle_Fails(); - void Test_CheckStartsWith_Present_Passes(); - void Test_CheckStartsWith_Wide_Absent_Fails(); - void Test_CheckStartsWith_Wide_NonASCII_Fails(); - void Test_CheckStartsWith_Wide_Present_Passes(); +{ + SetExceptionExpected(true, "any exception"); + throw std::runtime_error("boom"); +} +//--------------------------------------------------------------------------- +void TFixture_SourceLocations::Test_SetExceptionExpected_TypeAndMessage_Passes() +{ + SetExceptionExpected("matching type and message", "boom"); + throw std::invalid_argument("boom"); +} +//--------------------------------------------------------------------------- +void TFixture_SourceLocations::Test_SetExceptionExpected_Type_NoneThrown_Fails() +{ + ExpectedLines["SetExceptionExpected_Type_NoneThrown_Fails"] = __LINE__ + 1; + SetExceptionExpected("deliberately nothing thrown"); +} +//--------------------------------------------------------------------------- +void TFixture_SourceLocations::Test_Skip_Skips() +{ + ExpectedLines["Skip_Skips"] = __LINE__ + 1; + Skip("deliberate skip"); +} +//--------------------------------------------------------------------------- +#endif // #if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) -public: - TFixture_StartsWithComparisons(); - void SetUp_Group() override {} - void TearDown_Group() override {} -}; +///////////////////////////////////////////////////////////////////////////// +// TFixture_StartsWithComparisons +///////////////////////////////////////////////////////////////////////////// //--------------------------------------------------------------------------- TFixture_StartsWithComparisons::TFixture_StartsWithComparisons() @@ -5354,53 +6338,7 @@ void TFixture_StartsWithComparisons::Test_CheckStartsWith_Wide_Present_Passes() ///////////////////////////////////////////////////////////////////////////// // TFixture_StartsWithICComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertStartsWithIC()/CheckStartsWithIC()/ -// AssertNotStartsWithIC()/CheckNotStartsWithIC(), narrow and wide. Only the ASCII letters A-Z are case-folded, as in -// TFixture_ContainsICComparisons above. Test names self-document expected outcome via NameEndsWith(), same as -// TFixture_ExceptionExpectations above. ///////////////////////////////////////////////////////////////////////////// -class TFixture_StartsWithICComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertNotStartsWithIC_Absent_Passes(); - void Test_AssertNotStartsWithIC_DifferentCase_Fails(); - void Test_AssertNotStartsWithIC_Wide_Absent_Passes(); - void Test_AssertNotStartsWithIC_Wide_DifferentCase_Fails(); - void Test_AssertStartsWithIC_Absent_Fails(); - void Test_AssertStartsWithIC_DifferentCase_Passes(); - void Test_AssertStartsWithIC_Wide_Absent_Fails(); - void Test_AssertStartsWithIC_Wide_DifferentCase_Passes(); - void Test_CheckNotStartsWithIC_Absent_Passes(); - void Test_CheckNotStartsWithIC_DifferentCase_Fails(); - void Test_CheckNotStartsWithIC_EmptyPrefix_Fails(); - void Test_CheckNotStartsWithIC_PrefixLongerThanText_Passes(); - void Test_CheckNotStartsWithIC_PrefixOnlyAtEnd_Passes(); - void Test_CheckNotStartsWithIC_Wide_Absent_Passes(); - void Test_CheckNotStartsWithIC_Wide_DifferentCase_Fails(); - void Test_CheckStartsWithIC_Absent_Fails(); - void Test_CheckStartsWithIC_BothEmpty_Passes(); - void Test_CheckStartsWithIC_DifferentCase_Passes(); - void Test_CheckStartsWithIC_EmptyPrefix_Passes(); - void Test_CheckStartsWithIC_EmptyText_Fails(); - void Test_CheckStartsWithIC_NonASCII_Fails(); - void Test_CheckStartsWithIC_NonLetters_Fails(); - void Test_CheckStartsWithIC_PrefixLongerThanText_Fails(); - void Test_CheckStartsWithIC_PrefixOnlyAtEnd_Fails(); - void Test_CheckStartsWithIC_PrefixOnlyInMiddle_Fails(); - void Test_CheckStartsWithIC_Wide_Absent_Fails(); - void Test_CheckStartsWithIC_Wide_DifferentCase_Passes(); - void Test_CheckStartsWithIC_Wide_NonASCII_Fails(); - -public: - TFixture_StartsWithICComparisons(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_StartsWithICComparisons::TFixture_StartsWithICComparisons() @@ -5615,38 +6553,7 @@ void TFixture_StartsWithICComparisons::Test_CheckStartsWithIC_Wide_NonASCII_Fail ///////////////////////////////////////////////////////////////////////////// // TFixture_StringComparisons -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one deliberately failing test per string -// overload of AssertEquals()/CheckEquals()/AssertNotEquals()/CheckNotEquals() whose failure message didn't use to -// show the values: wide text (shown as UTF-8), the NotEquals overloads (which show the shared value), and null wide -// C strings (shown as "(null)", like narrow ones). Used by Test_Equals_ShowsStringValues below. ///////////////////////////////////////////////////////////////////////////// -class TFixture_StringComparisons : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertEquals_WideCStringNull_Fails(); - void Test_AssertEquals_Wide_Fails(); - void Test_AssertNotEquals_CStringBothNull_Fails(); - void Test_AssertNotEquals_String_Fails(); - void Test_AssertNotEquals_WideCStringBothNull_Fails(); - void Test_AssertNotEquals_Wide_Fails(); - void Test_CheckEquals_WideCStringNull_Fails(); - void Test_CheckEquals_WideCString_Fails(); - void Test_CheckEquals_Wide_Fails(); - void Test_CheckNotEquals_CStringBothNull_Fails(); - void Test_CheckNotEquals_String_Fails(); - void Test_CheckNotEquals_WideCStringBothNull_Fails(); - void Test_CheckNotEquals_Wide_Fails(); - -public: - TFixture_StringComparisons(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_StringComparisons::TFixture_StringComparisons() @@ -5756,40 +6663,7 @@ void TFixture_StringComparisons::Test_CheckNotEquals_Wide_Fails() ///////////////////////////////////////////////////////////////////////////// // TFixture_ThrowingSetUpTearDown -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group whose SetUp_Test() or TearDown_Test(), as chosen -// by 'ThrowFrom', throws a std::runtime_error for its first test. That test expects an exception and throws one, -// so the exception from TearDown_Test() arrives while one is expected, and must not be taken for it. The second -// test records whether it ran. Used by Test_Run_EndsRunOnSetUpOrTearDownTestException below. ///////////////////////////////////////////////////////////////////////////// -class TFixture_ThrowingSetUpTearDown : public TTestGroupBase -{ -public: - enum class TThrowFrom - { - SetUp, - TearDown - }; - -private: - typedef TTestGroupBase inherited; - -private: - void Test_RunsAfterThrowingTest(); - void Test_ThrowsExpectedException(); - -public: - bool RunsAfterThrowingTestReached = false; - TThrowFrom const ThrowFrom; - -public: - explicit TFixture_ThrowingSetUpTearDown(TThrowFrom throwFrom); - - void SetUp_Group() override {} - void SetUp_Test(ITestCase& testCase) override; - void TearDown_Group() override {} - void TearDown_Test(ITestCase& testCase) override; -}; //--------------------------------------------------------------------------- TFixture_ThrowingSetUpTearDown::TFixture_ThrowingSetUpTearDown(TThrowFrom throwFrom) @@ -5830,43 +6704,7 @@ void TFixture_ThrowingSetUpTearDown::Test_ThrowsExpectedException() ///////////////////////////////////////////////////////////////////////////// // TFixture_ThrowsChecks -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertThrows()/CheckThrows()/ -// AssertNoThrow()/CheckNoThrow(), so Test_Throws_ChecksTypeAndMessageAndContinues below can check their outcomes -// and messages. ReachedAfterCheckNoThrow shows a failed Check lets the test continue; ReachedAfterInnerAssert shows -// an Assert failure inside the callable still ends the test instead of counting as the exception thrown. Test names -// self-document expected outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. ///////////////////////////////////////////////////////////////////////////// -class TFixture_ThrowsChecks : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertNoThrow_NoException_Passes(); - void Test_AssertNoThrow_Throws_Fails(); - void Test_AssertThrows_NoException_Fails(); - void Test_AssertThrows_RightType_Passes(); - void Test_CheckNoThrow_Throws_Fails(); - void Test_CheckThrows_AssertFailureInside_Fails(); - void Test_CheckThrows_FrameworkTypeRequested_Passes(); - void Test_CheckThrows_MessageMatches_Passes(); - void Test_CheckThrows_MessageMismatch_Fails(); - void Test_CheckThrows_NoException_Fails(); - void Test_CheckThrows_NonStdException_Fails(); - void Test_CheckThrows_SeveralCalls_Passes(); - void Test_CheckThrows_Subclass_Passes(); - void Test_CheckThrows_WrongType_Fails(); - -public: - bool ReachedAfterCheckNoThrow = false; - bool ReachedAfterInnerAssert = false; - - TFixture_ThrowsChecks(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_ThrowsChecks::TFixture_ThrowsChecks() @@ -6003,33 +6841,7 @@ void TFixture_ThrowsChecks::Test_CheckThrows_WrongType_Fails() ///////////////////////////////////////////////////////////////////////////// // TFixture_TrueFalseChecks -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling AssertTrue()/CheckTrue()/AssertFalse()/ -// CheckFalse() with a value that passes and one that fails, so Test_TrueFalse_FailureNamesTheExpectedValue below can -// check that each failure states the value that was expected, not the opposite. Test names self-document expected -// outcome via NameEndsWith(), same as TFixture_ExceptionExpectations above. ///////////////////////////////////////////////////////////////////////////// -class TFixture_TrueFalseChecks : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertFalse_False_Passes(); - void Test_AssertFalse_True_Fails(); - void Test_AssertTrue_False_Fails(); - void Test_AssertTrue_True_Passes(); - void Test_CheckFalse_False_Passes(); - void Test_CheckFalse_True_Fails(); - void Test_CheckTrue_False_Fails(); - void Test_CheckTrue_True_Passes(); - -public: - TFixture_TrueFalseChecks(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_TrueFalseChecks::TFixture_TrueFalseChecks() @@ -6083,65 +6895,15 @@ void TFixture_TrueFalseChecks::Test_CheckTrue_False_Fails() } //--------------------------------------------------------------------------- void TFixture_TrueFalseChecks::Test_CheckTrue_True_Passes() -{ - CheckTrue(true, __func__, __LINE__, "value is true"); -} -//--------------------------------------------------------------------------- - - -///////////////////////////////////////////////////////////////////////////// -// TFixture_TypeChecks -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing AssertIsType()/CheckIsType()/ -// AssertIsNotType()/CheckIsNotType() with a small class hierarchy, through raw and smart pointers and a reference, so -// Test_IsType_UsesDynamicTypeAndShowsNames below can check their outcomes and messages. A subclass counts as its base -// class's type, and a null pointer is never any type. Test names self-document expected outcome via NameEndsWith(), -// same as TFixture_ExceptionExpectations above. -///////////////////////////////////////////////////////////////////////////// -class TFixture_TypeChecks : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - - struct TShape - { - virtual ~TShape() = default; - }; - - struct TCircle : TShape - { - }; - - struct TBigCircle : TCircle - { - }; - - struct TSquare : TShape - { - }; - -private: - void Test_AssertIsNotType_OtherType_Passes(); - void Test_AssertIsNotType_SameType_Fails(); - void Test_AssertIsType_RightType_Passes(); - void Test_AssertIsType_WrongType_Fails(); - void Test_CheckIsNotType_Null_Passes(); - void Test_CheckIsNotType_OtherType_Passes(); - void Test_CheckIsNotType_Subclass_Fails(); - void Test_CheckIsType_BaseObject_Fails(); - void Test_CheckIsType_Null_Fails(); - void Test_CheckIsType_Reference_Passes(); - void Test_CheckIsType_RightType_Passes(); - void Test_CheckIsType_SharedPtr_Passes(); - void Test_CheckIsType_Subclass_Passes(); - void Test_CheckIsType_UniquePtr_Fails(); +{ + CheckTrue(true, __func__, __LINE__, "value is true"); +} +//--------------------------------------------------------------------------- -public: - TFixture_TypeChecks(); - void SetUp_Group() override {} - void TearDown_Group() override {} -}; +///////////////////////////////////////////////////////////////////////////// +// TFixture_TypeChecks +///////////////////////////////////////////////////////////////////////////// //--------------------------------------------------------------------------- TFixture_TypeChecks::TFixture_TypeChecks() @@ -6262,35 +7024,7 @@ void TFixture_TypeChecks::Test_CheckIsType_UniquePtr_Fails() ///////////////////////////////////////////////////////////////////////////// // TFixture_UnexpectedExceptions -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group whose tests throw without expecting it: a -// std::exception, an object that isn't an exception at all, and a std::exception after a failed Check. Each of -// the first two is followed by a test that passes. Registration order matters: each throwing test must run before -// the test after it. Used by the Run_*UnexpectedException* tests below to prove an unexpected exception only fails -// the test that threw it, after which TearDown_Test() and the remaining tests still run. ///////////////////////////////////////////////////////////////////////////// -class TFixture_UnexpectedExceptions : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_CheckFailsThenThrowsStdException(); - void Test_RunsAfterNonException(); - void Test_RunsAfterStdException(); - void Test_ThrowsNonException(); - void Test_ThrowsStdException(); - -public: - std::vector TornDownTests; // The tests TearDown_Test() ran for, in order. - -public: - TFixture_UnexpectedExceptions(); - - void SetUp_Group() override {} - void TearDown_Group() override {} - void TearDown_Test(ITestCase& testCase) override; -}; //--------------------------------------------------------------------------- TFixture_UnexpectedExceptions::TFixture_UnexpectedExceptions() @@ -6342,36 +7076,7 @@ void TFixture_UnexpectedExceptions::Test_ThrowsStdException() ///////////////////////////////////////////////////////////////////////////// // TFixture_CrashingTest -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group with one test that crashes (an -// integer divide by zero), followed by one that records whether it ran. Used by -// Test_Run_ContinuesAfterCrashWhenCatchCrashesIsSet below to prove --catch-crashes' central -// behavior: unlike a timeout, which can only abandon-and-abort, a caught crash lets the group -// continue running its remaining tests. Deliberately not a null-pointer write: that's a SIGSEGV on -// POSIX, and TCrashGuard::Run() treats every SIGSEGV there as abort-worthy (it can't tell an -// ordinary one apart from a stack overflow - see its comment), which would make this fixture's -// crashing test abort the group instead of letting it continue, defeating the point of this test. -// An integer divide by zero (SIGFPE on POSIX, EXCEPTION_INT_DIVIDE_BY_ZERO on Windows) is never -// abort-worthy on either platform, so it exercises "ordinary crash, group continues" portably. ///////////////////////////////////////////////////////////////////////////// -class TFixture_CrashingTest : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_CrashesButDoesNotAbort(); - void Test_RunsAfterTheCrash(); - -public: - bool RunsAfterTheCrashReached = false; - -public: - TFixture_CrashingTest(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_CrashingTest::TFixture_CrashingTest() @@ -6444,6 +7149,8 @@ TTest_ASWUnitTests_TestBase::TTest_ASWUnitTests_TestBase() "IsType_UsesDynamicTypeAndShowsNames"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Matches_MatchesWholeTextAndShowsPattern, "Matches_MatchesWholeTextAndShowsPattern"); + RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Near_ComparesMixedTypesAndRejectsInvalidTolerance, + "Near_ComparesMixedTypesAndRejectsInvalidTolerance"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Near_HandlesNaNAndInfinity, "Near_HandlesNaNAndInfinity"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_NullCStrings_MatchNothingInTextChecks, "NullCStrings_MatchNothingInTextChecks"); @@ -6455,12 +7162,18 @@ TTest_ASWUnitTests_TestBase::TTest_ASWUnitTests_TestBase() "Run_AbandonsHungTestAndAbortsGroupOnTimeout"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Run_AppliesFilterToSkipNonMatchingTests, "Run_AppliesFilterToSkipNonMatchingTests"); + RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Run_ContinuesAfterCrashInRunObserverUnderTimeout, + "Run_ContinuesAfterCrashInRunObserverUnderTimeout"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Run_ContinuesAfterCrashWhenCatchCrashesIsSet, "Run_ContinuesAfterCrashWhenCatchCrashesIsSet"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Run_CountsTestRecordingItsOutcomeAtTimeoutAsFinished, "Run_CountsTestRecordingItsOutcomeAtTimeoutAsFinished"); + RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Run_CrashInRunObserverDoesNotBlockLaterRunOnAnotherThread, + "Run_CrashInRunObserverDoesNotBlockLaterRunOnAnotherThread"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Run_EndsRunOnSetUpOrTearDownTestException, "Run_EndsRunOnSetUpOrTearDownTestException"); + RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Run_GivesTimedTestTheCallersFloatingPointEnvironment, + "Run_GivesTimedTestTheCallersFloatingPointEnvironment"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Run_IgnoresTestThatEndsAfterItsTimeout, "Run_IgnoresTestThatEndsAfterItsTimeout"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Run_LogsEachCheckFailureOnce, "Run_LogsEachCheckFailureOnce"); @@ -6493,6 +7206,8 @@ TTest_ASWUnitTests_TestBase::TTest_ASWUnitTests_TestBase() "SetExceptionExpected_AssertFailureStillFailsAndIsRecorded"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_SetExceptionExpected_EarlierCheckFailureStillFailsAndIsRecorded, "SetExceptionExpected_EarlierCheckFailureStillFailsAndIsRecorded"); + RegisterTest(&TTest_ASWUnitTests_TestBase::Test_SetExceptionExpected_MatchesAnyType, + "SetExceptionExpected_MatchesAnyType"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_SetExceptionExpected_MatchesTypeAndMessage, "SetExceptionExpected_MatchesTypeAndMessage"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_SetLogSuppressed_SilencesFixtureOutput, @@ -6504,8 +7219,11 @@ TTest_ASWUnitTests_TestBase::TTest_ASWUnitTests_TestBase() RegisterTest(&TTest_ASWUnitTests_TestBase::Test_StartsWithIC_IgnoresASCIICaseOnly, "StartsWithIC_IgnoresASCIICaseOnly"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_StartsWith_ShowsTextAndPrefix, "StartsWith_ShowsTextAndPrefix"); + RegisterTest(&TTest_ASWUnitTests_TestBase::Test_StringChecks_CompareMixedTypesAsTextAndKeepNullRules, + "StringChecks_CompareMixedTypesAsTextAndKeepNullRules"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Throws_ChecksTypeAndMessageAndContinues, "Throws_ChecksTypeAndMessageAndContinues"); + RegisterTest(&TTest_ASWUnitTests_TestBase::Test_Throws_MatchesAnyType, "Throws_MatchesAnyType"); RegisterTest(&TTest_ASWUnitTests_TestBase::Test_TrueFalse_FailureNamesTheExpectedValue, "TrueFalse_FailureNamesTheExpectedValue"); } @@ -6747,10 +7465,12 @@ void TTest_ASWUnitTests_TestBase::Test_Empty_ShowsContentsOnFailure() // Assert TTestResults const& results = fixture.Results(); + size_t expectedCount = 25; #if defined(__cpp_lib_string_view) - size_t const expectedCount = 12; -#else - size_t const expectedCount = 11; + ++expectedCount; +#endif +#if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) + ++expectedCount; #endif CheckEquals(expectedCount, results.CaseRecords.size(), __func__, __LINE__, "one record per registered test"); @@ -6773,12 +7493,22 @@ void TTest_ASWUnitTests_TestBase::Test_Empty_ShowsContentsOnFailure() }; std::vector const expectedFailures = { + { "AssertEmpty_NullCString_Fails", "Not empty: Test_AssertEmpty_NullCString_Fails (", + "): Expected empty but was \"(null)\". null C string" }, { "AssertEmpty_String_Fails", "Not empty: Test_AssertEmpty_String_Fails (", "): Expected empty but was \"abc\". has text" }, { "AssertNotEmpty_EmptyVector_Fails", "Empty: Test_AssertNotEmpty_EmptyVector_Fails (", "): Expected not empty but was empty. no elements" }, + { "AssertNotEmpty_EmptyWideCString_Fails", "Empty: Test_AssertNotEmpty_EmptyWideCString_Fails (", + "): Expected not empty but was empty. empty wide C string" }, + { "AssertNotEmpty_NullCString_Fails", "Empty: Test_AssertNotEmpty_NullCString_Fails (", + "): Expected not empty but was \"(null)\". null C string" }, + { "CheckEmpty_CharArray_Fails", "Check failed for: \"Test_CheckEmpty_CharArray_Fails\" (", + "): Expected empty but was \"abc\". an array shows its text, not its size" }, { "CheckEmpty_ForwardList_Fails", "Check failed for: \"Test_CheckEmpty_ForwardList_Fails\" (", "): Expected empty but was not empty. no size to show" }, + { "CheckEmpty_NullWideCString_Fails", "Check failed for: \"Test_CheckEmpty_NullWideCString_Fails\" (", + "): Expected empty but was \"(null)\". null wide C string" }, { "CheckEmpty_OneElement_Fails", "Check failed for: \"Test_CheckEmpty_OneElement_Fails\" (", "): Expected empty but had 1 element. one element" }, #if defined(__cpp_lib_string_view) @@ -6791,10 +7521,18 @@ void TTest_ASWUnitTests_TestBase::Test_Empty_ShowsContentsOnFailure() "CheckEmpty_Vector_Fails", "Check failed for: \"Test_CheckEmpty_Vector_Fails\" (", "): Expected empty but had 3 elements. three elements" }, + { "CheckEmpty_WideCString_Fails", "Check failed for: \"Test_CheckEmpty_WideCString_Fails\" (", + "): Expected empty but was \"caf\xC3\xA9\". wide C string" }, { "CheckEmpty_WideString_Fails", "Check failed for: \"Test_CheckEmpty_WideString_Fails\" (", "): Expected empty but was \"caf\xC3\xA9\". wide text" }, + { "CheckNotEmpty_EmptyCharPointer_Fails", "Check failed for: \"Test_CheckNotEmpty_EmptyCharPointer_Fails\" (", + "): Expected not empty but was empty. a char* to empty text" }, { "CheckNotEmpty_EmptyString_Fails", "Check failed for: \"Test_CheckNotEmpty_EmptyString_Fails\" (", "): Expected not empty but was empty. empty string" }, + { "CheckNotEmpty_NullCString_Fails", "Check failed for: \"Test_CheckNotEmpty_NullCString_Fails\" (", + "): Expected not empty but was \"(null)\". null C string" }, + { "CheckNotEmpty_NullWideCString_Fails", "Check failed for: \"Test_CheckNotEmpty_NullWideCString_Fails\" (", + "): Expected not empty but was \"(null)\". null wide C string" }, }; for (TExpectedFailure const& expected : expectedFailures) @@ -7474,6 +8212,94 @@ void TTest_ASWUnitTests_TestBase::Test_Matches_MatchesWholeTextAndShowsPattern() } } //--------------------------------------------------------------------------- +void TTest_ASWUnitTests_TestBase::Test_Near_ComparesMixedTypesAndRejectsInvalidTolerance() +{ + // Arrange + TFixture_NearMixedTypes fixture; + + // Act + fixture.Run(TestFilter(), std::nullopt, std::nullopt, false); + + // Assert + TTestResults const& results = fixture.Results(); +#if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) + size_t const expectedCount = 22; +#else + size_t const expectedCount = 21; +#endif + CheckEquals(expectedCount, results.CaseRecords.size(), __func__, __LINE__, "one record per registered test"); + + for (TTestCaseRecord const& record : results.CaseRecords) + { + if (NameEndsWith(record.TestName, "_Passes")) + { + CheckEquals(TTestOutcome::Pass, record.Outcome, __func__, __LINE__, + record.TestName + " should pass: " + record.Message); + } + else if (NameEndsWith(record.TestName, "_Fails")) + { + CheckEquals(TTestOutcome::Fail, record.Outcome, __func__, __LINE__, record.TestName + " should fail"); + } + else + { + Fail(__func__, __LINE__, record.TestName + " name must end with _Passes or _Fails"); + } + } + + // Every call whose common type is double reaches the CheckNear(double, ...) override: DoubleAndFloat (2), + // FloatsAndDoubleTolerance, NaNAndFloat and NaNTolerance. Integer and long double calls don't. + CheckEquals(5, fixture.DoubleCheckNearCalls, __func__, __LINE__, "mixed calls still reach the virtual overload"); + + // Each failure's start (up to the varying line number) and end. A NaN's text varies by compiler. + struct TExpectedFailure + { + std::string TestName; + std::string Prefix; + std::string Detail; + }; + + std::vector const expectedFailures = { + { "AssertNear_Integers_Fails", "Values not equal: Test_AssertNear_Integers_Fails (", + "): Expected: \"10 (tolerance 2)\" but was \"13 (diff 3)\". msg" }, + { "AssertNear_NegativeTolerance_Fails", "Values not equal: Test_AssertNear_NegativeTolerance_Fails (", + "): Invalid tolerance -0.500000: it must be zero or more. msg" }, + { "AssertNotNear_FloatNaNTolerance_Fails", "Values are equal: Test_AssertNotNear_FloatNaNTolerance_Fails (", + ": it must be zero or more. msg" }, + { "CheckNear_DoubleAndFloat_Fails", "Check failed for: \"Test_CheckNear_DoubleAndFloat_Fails\" (", + "): Expected \"2.000000 (tolerance 0.010000)\" but was \"2.500000 (diff 0.500000)\". msg" }, + { "CheckNear_IntegerSumOverflows_Fails", "Check failed for: \"Test_CheckNear_IntegerSumOverflows_Fails\" (", + "): Expected \"-1 (tolerance 18446744073709551615)\" but was \"18446744073709551615 " + "(diff 18446744073709551616)\". msg" }, + { "CheckNear_Integers_Fails", "Check failed for: \"Test_CheckNear_Integers_Fails\" (", + "): Expected \"10 (tolerance 1)\" but was \"12 (diff 2)\". msg" }, + { "CheckNear_LongDouble_Fails", "Check failed for: \"Test_CheckNear_LongDouble_Fails\" (", + "): Expected \"1.000000 (tolerance 0.250000)\" but was \"1.500000 (diff 0.500000)\". msg" }, + { "CheckNear_NaNTolerance_Fails", "Check failed for: \"Test_CheckNear_NaNTolerance_Fails\" (", + ": it must be zero or more. msg" }, + { "CheckNear_NegativeIntegerTolerance_Fails", + "Check failed for: \"Test_CheckNear_NegativeIntegerTolerance_Fails\" (", + "): Invalid tolerance -1: it must be zero or more. msg" }, + { "CheckNotNear_DoubleAndFloat_Fails", "Check failed for: \"Test_CheckNotNear_DoubleAndFloat_Fails\" (", + "): Both values equal: \"2.005000 (diff 0.005000 <= tolerance 0.010000)\". msg" }, + { "CheckNotNear_Integers_Fails", "Check failed for: \"Test_CheckNotNear_Integers_Fails\" (", + "): Both values equal: \"12 (diff 2 <= tolerance 5)\". msg" }, + { "CheckNotNear_NegativeTolerance_Fails", "Check failed for: \"Test_CheckNotNear_NegativeTolerance_Fails\" (", + "): Invalid tolerance -1.000000: it must be zero or more. msg" }, + }; + + for (TExpectedFailure const& expected : expectedFailures) + { + TTestCaseRecord const* const record = FindRecord(results, expected.TestName); + AssertNotNull(record, __func__, __LINE__, expected.TestName + " has a record"); + CheckStartsWith(record->Message, expected.Prefix, __func__, __LINE__, expected.TestName + " names the test"); + CheckEndsWith(record->Message, expected.Detail, __func__, __LINE__, expected.TestName + " shows the values"); + } + + TTestCaseRecord const* const nanTolerance = FindRecord(results, "CheckNear_NaNTolerance_Fails"); + AssertNotNull(nanTolerance, __func__, __LINE__, "the NaN tolerance test has a record"); + CheckContains(nanTolerance->Message, "): Invalid tolerance ", __func__, __LINE__, "a NaN tolerance is invalid"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_TestBase::Test_Near_HandlesNaNAndInfinity() { // Arrange @@ -7713,6 +8539,39 @@ void TTest_ASWUnitTests_TestBase::Test_Run_AppliesFilterToSkipNonMatchingTests() "CaseRecords has an entry only for the tests that ran, not the full registered set"); } //--------------------------------------------------------------------------- +/* + TTest_ASWUnitTests_TestBase::Test_Run_ContinuesAfterCrashInRunObserverUnderTimeout + + The first test crashes in its "Running test" log line, on its worker thread, while that thread + holds the group's recording lock. The jump out of the crash skips the lock's destructor, so + unless the framework releases it, the worker thread ends still holding it: the next test then + waits for it forever at its first log line, and so does Run() once that test times out, so + Run() never returns (under --test-timeout-seconds, this test times out instead). +*/ +void TTest_ASWUnitTests_TestBase::Test_Run_ContinuesAfterCrashInRunObserverUnderTimeout() +{ + // Arrange + TFixture_MixedOutcomes fixture(false); // Logs, so the observer's OnLog() is called. + TCrashOnLogObserver observer; + fixture.SetRunObserver(&observer); + + // Act + fixture.Run(TestFilter(), std::nullopt, 30u, true); // Only long enough never to fire; catchCrashes = true. + + // Assert + TTestResults const& results = fixture.Results(); + CheckEquals(static_cast(6), results.CaseRecords.size(), __func__, __LINE__, "every test recorded"); + CheckEquals(static_cast(6), observer.FinishedRecords.size(), __func__, __LINE__, "every test reported"); + CheckEquals(1u, results.SuccessCount, __func__, __LINE__, "the one passing test, after the crash, passed"); + + TTestCaseRecord const& crashedRecord = results.CaseRecords.front(); + CheckEquals(std::string("ContinuesAfterCheckFailure"), crashedRecord.TestName, __func__, __LINE__, + "the first test is the one that crashed"); + CheckContains(crashedRecord.Message, "crashed", __func__, __LINE__, "and its record says so"); + CheckContains(observer.LogText(), "Running test: Fixture_MixedOutcomes.Skip", __func__, __LINE__, + "the last test still logged"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_TestBase::Test_Run_ContinuesAfterCrashWhenCatchCrashesIsSet() { // Arrange @@ -7769,6 +8628,40 @@ void TTest_ASWUnitTests_TestBase::Test_Run_CountsTestRecordingItsOutcomeAtTimeou CheckEquals(0u, results.FailedCount, __func__, __LINE__, "and nothing failed"); } //--------------------------------------------------------------------------- +/* + TTest_ASWUnitTests_TestBase::Test_Run_CrashInRunObserverDoesNotBlockLaterRunOnAnotherThread + + Without a timeout, the crash in the first test's "Running test" log line comes on this thread, + while it holds the group's recording lock. Unless the framework releases the lock, this thread + keeps holding it (harmless to this thread, as it's recursive), and a later run on another thread + waits for it forever at its first log line, so the join below never returns. +*/ +void TTest_ASWUnitTests_TestBase::Test_Run_CrashInRunObserverDoesNotBlockLaterRunOnAnotherThread() +{ + // Arrange + TFixture_MixedOutcomes fixture(false); // Logs, so the observer's OnLog() is called. + TCrashOnLogObserver observer; + fixture.SetRunObserver(&observer); + fixture.Run(TestFilter(), std::nullopt, std::nullopt, true); // catchCrashes = true; the first test crashes. + + auto runAgain = [&fixture]() + { + fixture.Run(TestFilter(), std::nullopt, std::nullopt, false); + }; + + // Act + std::thread other(runAgain); + other.join(); + + // Assert + TTestResults const& results = fixture.Results(); + CheckEquals(static_cast(6), results.CaseRecords.size(), __func__, __LINE__, + "the run on the other thread recorded every test"); + CheckEquals(1u, results.SuccessCount, __func__, __LINE__, "the passing test passed"); + CheckNotContains(results.CaseRecords.front().Message, "crashed", __func__, __LINE__, + "the first test didn't crash this time"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_TestBase::Test_Run_EndsRunOnSetUpOrTearDownTestException() { // Unlike an unexpected exception from a test itself, one from SetUp_Test() or TearDown_Test() still escapes @@ -7811,6 +8704,35 @@ void TTest_ASWUnitTests_TestBase::Test_Run_EndsRunOnSetUpOrTearDownTestException } } //--------------------------------------------------------------------------- +void TTest_ASWUnitTests_TestBase::Test_Run_GivesTimedTestTheCallersFloatingPointEnvironment() +{ + // Arrange: this thread rounds downward for the run, and as before afterwards, whatever happens. On Windows + // a new thread starts rounding to nearest, so a test run on one without this thread's environment would too. + struct TRoundingModeRestorer + { + int Mode = RoundingMode(); + + ~TRoundingModeRestorer() + { + SetRoundingMode(Mode); + } + }; + + TFixture_FloatingPointEnvironment fixture; + TRoundingModeRestorer const restorer; + SetRoundingMode(DownwardRoundingMode()); + AssertEquals(DownwardRoundingMode(), RoundingMode(), __func__, __LINE__, "this thread now rounds downward"); + + // Act + fixture.Run(TestFilter(), std::nullopt, 30u, false); // Only long enough never to fire. + + // Assert + CheckEquals(DownwardRoundingMode(), fixture.RoundingModeInTest, __func__, __LINE__, + "the test, on a worker thread, rounds as the calling thread does"); + CheckEquals(LongDoubleKeepsPrecision(), fixture.LongDoublePrecisionInTest, __func__, __LINE__, + "and computes long double with the calling thread's precision"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_TestBase::Test_Run_IgnoresTestThatEndsAfterItsTimeout() { // Arrange @@ -8373,6 +9295,71 @@ void TTest_ASWUnitTests_TestBase::Test_SetExceptionExpected_EarlierCheckFailureS "all four Check-then-expected-exception tests ran"); } //--------------------------------------------------------------------------- +void TTest_ASWUnitTests_TestBase::Test_SetExceptionExpected_MatchesAnyType() +{ + // Arrange + TFixture_AnyTypeExceptionExpectations fixture; +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) + std::string const unknown = "an object that is neither a std::exception nor an RTL Exception"; +#else + std::string const unknown = "a non-std::exception object"; +#endif + std::string const wrongType = "expected exception type was not thrown (caught a different exception): "; + std::string const noMessage = "expected exception message to contain \"needle\" but caught an exception without a " + "message: an object of type \""; + // A part of each failed test's record (a class name's full form differs by compiler). + std::vector > const failures = + { + { "CheckFailedThenIntThrown_Fails", "deliberate Check failure before the expected exception" }, + { "ClassExpected_MessageExpected_Fails", noMessage }, + { "ClassExpected_MessageExpected_Fails", "TFixturePlainError\"" }, + { "ClassExpected_SubclassThrownMessageExpected_Fails", noMessage }, + { "ClassExpected_SubclassThrownMessageExpected_Fails", "TFixtureNonStdSubError\"" }, + { "IntExpected_AssertFails_Fails", "deliberate Assert failure while an int is expected" }, + { "IntExpected_DoubleThrown_Fails", wrongType + unknown }, + { "IntExpected_MessageExpected_Fails", noMessage + "int\"" }, + { "IntExpected_NoneThrown_Fails", "expected an int that never comes" }, + { "IntExpected_StdExceptionThrown_Fails", wrongType + "boom" }, + { "IntThenStdTypeExpected_IntThrown_Fails", wrongType + unknown }, + { "MixinExpected_StdExceptionWithoutMixinThrown_Fails", wrongType + "boom" }, + { "MixinExpected_TaggedErrorMessageAbsent_Fails", "expected exception message to contain \"needle\" but caught: boom" }, + { "StdTypeExpected_IntThrown_Fails", wrongType + unknown }, + }; + + // Act + fixture.Run(TestFilter(), std::nullopt, std::nullopt, false); + + // Assert + TTestResults const& results = fixture.Results(); + CheckEquals(static_cast(17), results.CaseRecords.size(), __func__, __LINE__, "one record per registered test"); + + for (TTestCaseRecord const& record : results.CaseRecords) + { + if (NameEndsWith(record.TestName, "_Passes")) + CheckEquals(TTestOutcome::Pass, record.Outcome, __func__, __LINE__, record.TestName + " should pass: " + record.Message); + else if (NameEndsWith(record.TestName, "_Fails")) + CheckEquals(TTestOutcome::Fail, record.Outcome, __func__, __LINE__, record.TestName + " should fail"); + else + Fail(__func__, __LINE__, record.TestName + " name must end with _Passes or _Fails"); + } + + for (std::pair const& failure : failures) + { + TTestCaseRecord const* const record = FindRecord(results, failure.first); + AssertTrue(record != nullptr, __func__, __LINE__, std::string(failure.first) + " has a record"); + CheckContains(record->Message, failure.second, __func__, __LINE__, std::string(failure.first) + "'s message"); + } + + // A type name still mangled, as bcc32c's type_info::name() is (e.g. "$i"), would start with '$'. + for (char const* const testName : { "ClassExpected_MessageExpected_Fails", "IntExpected_MessageExpected_Fails" }) + CheckNotContains(FindRecord(results, testName)->Message, "$", __func__, __LINE__, testName); +#if !defined(_MSC_VER) + // MSVC writes it "`anonymous namespace'". + CheckContains(FindRecord(results, "ClassExpected_MessageExpected_Fails")->Message, + "(anonymous namespace)::TFixturePlainError\"", __func__, __LINE__, "a class in an anonymous namespace"); +#endif +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_TestBase::Test_SetExceptionExpected_MatchesTypeAndMessage() { // Arrange @@ -8622,6 +9609,186 @@ void TTest_ASWUnitTests_TestBase::Test_StartsWith_ShowsTextAndPrefix() "a wide failure shows its text as UTF-8: " + checkNonASCII->Message); } //--------------------------------------------------------------------------- +/* + TTest_ASWUnitTests_TestBase::Test_StringChecks_CompareMixedTypesAsTextAndKeepNullRules + + Every string check with two texts of different types: each ordered pairing of std::string, char const*, + std::wstring and wchar_t const*, non-ASCII text, texts that differ in case only, and null C strings. The texts + must compare as text, whatever their types, and a null C string must follow the same rules whatever it's + compared with. +*/ +void TTest_ASWUnitTests_TestBase::Test_StringChecks_CompareMixedTypesAsTextAndKeepNullRules() +{ + // Arrange + typedef TStringCheckProbe::ERule ERule; + enum class ECase + { + Matching, // The same text: matches in every method. + CaseDiffers, // "ABC" and "abc": matches only in an IC method. + OneNull, + BothNull + }; + struct TCase + { + std::string Name; + ECase Kind; + std::vector > CheckFailures; + std::vector > AssertFailures; + }; + + TStringCheckProbe probe; + char const* const nullNarrow = nullptr; + wchar_t const* const nullWide = nullptr; + std::string const nonASCII = "\xC3\xA9x\xCE\xA9\xE4\xB8\xAD"; // U+00E9, an x, U+03A9 and U+4E2D. + std::wstring const wideNonASCII = L"\x00E9x\x03A9\x4E2D"; + + // Act + std::vector const cases = + { + { "StringAndCString", ECase::Matching, + probe.RunChecks(std::string("abc"), "abc"), + probe.RunAsserts(std::string("abc"), "abc") }, + { "StringAndWString", ECase::Matching, + probe.RunChecks(std::string("abc"), std::wstring(L"abc")), + probe.RunAsserts(std::string("abc"), std::wstring(L"abc")) }, + { "StringAndWCString", ECase::Matching, + probe.RunChecks(std::string("abc"), L"abc"), + probe.RunAsserts(std::string("abc"), L"abc") }, + { "CStringAndString", ECase::Matching, + probe.RunChecks("abc", std::string("abc")), + probe.RunAsserts("abc", std::string("abc")) }, + { "CStringAndWString", ECase::Matching, + probe.RunChecks("abc", std::wstring(L"abc")), + probe.RunAsserts("abc", std::wstring(L"abc")) }, + { "CStringAndWCString", ECase::Matching, probe.RunChecks("abc", L"abc"), probe.RunAsserts("abc", L"abc") }, + { "WStringAndString", ECase::Matching, + probe.RunChecks(std::wstring(L"abc"), std::string("abc")), + probe.RunAsserts(std::wstring(L"abc"), std::string("abc")) }, + { "WStringAndCString", ECase::Matching, + probe.RunChecks(std::wstring(L"abc"), "abc"), + probe.RunAsserts(std::wstring(L"abc"), "abc") }, + { "WStringAndWCString", ECase::Matching, + probe.RunChecks(std::wstring(L"abc"), L"abc"), + probe.RunAsserts(std::wstring(L"abc"), L"abc") }, + { "WCStringAndString", ECase::Matching, + probe.RunChecks(L"abc", std::string("abc")), + probe.RunAsserts(L"abc", std::string("abc")) }, + { "WCStringAndCString", ECase::Matching, probe.RunChecks(L"abc", "abc"), probe.RunAsserts(L"abc", "abc") }, + { "WCStringAndWString", ECase::Matching, + probe.RunChecks(L"abc", std::wstring(L"abc")), + probe.RunAsserts(L"abc", std::wstring(L"abc")) }, + { "NonASCII_StringAndWString", ECase::Matching, + probe.RunChecks(nonASCII, wideNonASCII), + probe.RunAsserts(nonASCII, wideNonASCII) }, + { "NonASCII_WCStringAndCString", ECase::Matching, + probe.RunChecks(wideNonASCII.c_str(), nonASCII.c_str()), + probe.RunAsserts(wideNonASCII.c_str(), nonASCII.c_str()) }, + { "CaseDiffers_StringAndWCString", ECase::CaseDiffers, + probe.RunChecks(std::string("ABC"), L"abc"), + probe.RunAsserts(std::string("ABC"), L"abc") }, + { "CaseDiffers_WStringAndWCString", ECase::CaseDiffers, + probe.RunChecks(std::wstring(L"ABC"), L"abc"), + probe.RunAsserts(std::wstring(L"ABC"), L"abc") }, + { "CaseDiffers_CStringAndWString", ECase::CaseDiffers, + probe.RunChecks("ABC", std::wstring(L"abc")), + probe.RunAsserts("ABC", std::wstring(L"abc")) }, + { "CaseDiffers_NonASCII_StringAndWString", ECase::CaseDiffers, + probe.RunChecks(std::string("\xC3\xA9X\xCE\xA9\xE4\xB8\xAD"), wideNonASCII), + probe.RunAsserts(std::string("\xC3\xA9X\xCE\xA9\xE4\xB8\xAD"), wideNonASCII) }, + { "NullCStringAndString", ECase::OneNull, + probe.RunChecks(nullNarrow, std::string("abc")), + probe.RunAsserts(nullNarrow, std::string("abc")) }, + { "StringAndNullCString", ECase::OneNull, + probe.RunChecks(std::string("abc"), nullNarrow), + probe.RunAsserts(std::string("abc"), nullNarrow) }, + { "NullWCStringAndString", ECase::OneNull, + probe.RunChecks(nullWide, std::string("abc")), + probe.RunAsserts(nullWide, std::string("abc")) }, + { "WStringAndNullWCString", ECase::OneNull, + probe.RunChecks(std::wstring(L"abc"), nullWide), + probe.RunAsserts(std::wstring(L"abc"), nullWide) }, + { "NullCStringAndWString", ECase::OneNull, + probe.RunChecks(nullNarrow, std::wstring(L"abc")), + probe.RunAsserts(nullNarrow, std::wstring(L"abc")) }, + { "WCStringAndNullCString", ECase::OneNull, + probe.RunChecks(L"abc", nullNarrow), + probe.RunAsserts(L"abc", nullNarrow) }, + { "NullCStringAndNullWCString", ECase::BothNull, + probe.RunChecks(nullNarrow, nullWide), + probe.RunAsserts(nullNarrow, nullWide) }, + { "NullWCStringAndNullCString", ECase::BothNull, + probe.RunChecks(nullWide, nullNarrow), + probe.RunAsserts(nullWide, nullNarrow) } + }; + // '.' matches U+00E9 as wide text, but not in two narrow strings, where U+00E9 is two bytes. + std::vector > const wideDot = + probe.RunChecks(std::string("\xC3\xA9"), std::wstring(L".")); + std::vector > const narrowDot = probe.RunChecks(std::string("\xC3\xA9"), "."); + // Two wide texts are compared as they are, not as UTF-8, where both unpaired surrogates would be U+FFFD. + std::vector > const surrogates = + probe.RunChecks(std::wstring(L"\xD800"), L"\xDC00"); + std::vector > const mismatch = probe.RunChecks(std::wstring(L"\x03A9x"), "y"); + + // Assert + for (TCase const& testCase : cases) + { + AssertEquals(std::size(TStringCheckProbe::Checks), testCase.CheckFailures.size(), __func__, __LINE__, + testCase.Name + ": a result for each Check method"); + AssertEquals(std::size(TStringCheckProbe::Checks), testCase.AssertFailures.size(), __func__, __LINE__, + testCase.Name + ": a result for each Assert method"); + + for (std::size_t i = 0; i < std::size(TStringCheckProbe::Checks); ++i) + { + TStringCheckProbe::TCheck const& check = TStringCheckProbe::Checks[i]; + bool const negated = check.Rule == ERule::NotEquals || check.Rule == ERule::NotMatches; + bool const equalsRule = check.Rule == ERule::Equals || check.Rule == ERule::NotEquals; + bool const matches = testCase.Kind == ECase::Matching || + (testCase.Kind == ECase::CaseDiffers && check.IgnoresCase) || + (testCase.Kind == ECase::BothNull && equalsRule); + bool const shouldPass = matches != negated; + bool const withNull = testCase.Kind == ECase::OneNull || testCase.Kind == ECase::BothNull; + + char const* const kinds[] = { "Check", "Assert" }; + std::vector > const* const results[] = + { + &testCase.CheckFailures, &testCase.AssertFailures + }; + + for (std::size_t k = 0; k < std::size(kinds); ++k) + { + std::vector > const& failures = *results[k]; + std::string const what = std::string(kinds[k]) + check.Name + " with " + testCase.Name; + CheckEquals(shouldPass, !failures[i].has_value(), __func__, __LINE__, + what + (shouldPass ? " passes" : " fails")); + + if (withNull && failures[i].has_value()) + { + CheckContains(*failures[i], "\"(null)\"", __func__, __LINE__, + what + " shows the null C string as (null)"); + } + } + } + } + + std::size_t const matchesIndex = TStringCheckProbe::IndexOf("Matches"); + CheckFalse(wideDot[matchesIndex].has_value(), __func__, __LINE__, "'.' matches U+00E9 as wide text"); + CheckTrue(narrowDot[matchesIndex].has_value(), __func__, __LINE__, "but not in two narrow strings"); + CheckFalse(surrogates[TStringCheckProbe::IndexOf("NotEquals")].has_value(), __func__, __LINE__, + "two different unpaired surrogates aren't equal as wide text"); + + std::optional const equalsFailure = mismatch[TStringCheckProbe::IndexOf("Equals")]; + std::optional const containsFailure = mismatch[TStringCheckProbe::IndexOf("Contains")]; + AssertTrue(equalsFailure.has_value() && containsFailure.has_value(), __func__, __LINE__, "both fail"); + CheckContains(*equalsFailure, "Expected \"\xCE\xA9x\" but was \"y\". msg", __func__, __LINE__, + "both texts shown, the wide one in UTF-8"); + CheckContains(*containsFailure, "Expected \"\xCE\xA9x\" to contain \"y\"", __func__, __LINE__, + "both texts shown, the wide one in UTF-8"); +#if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) + CheckFalse(probe.CheckEqualsThroughSourceLocation("abc", L"abc").has_value(), __func__, __LINE__, + "through the std::source_location overload too"); +#endif +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_TestBase::Test_Throws_ChecksTypeAndMessageAndContinues() { // Arrange @@ -8681,6 +9848,61 @@ void TTest_ASWUnitTests_TestBase::Test_Throws_ChecksTypeAndMessageAndContinues() __func__, __LINE__, "a wrong type shows what was caught"); } //--------------------------------------------------------------------------- +void TTest_ASWUnitTests_TestBase::Test_Throws_MatchesAnyType() +{ + // Arrange + TFixture_AnyTypeThrowsChecks fixture; +#if defined(ASWUNITTESTS_RTL_EXCEPTIONS_ENABLED) + std::string const unknown = "an object that is neither a std::exception nor an RTL Exception"; +#else + std::string const unknown = "a non-std::exception object"; +#endif + std::string const noMessage = "): Expected the exception message to contain \"needle\" but caught an exception " + "without a message: an object of type \""; + // A part of each failed test's record (a class name's full form differs by compiler). + std::vector > const failures = + { + { "AssertThrows_IntMessageExpected_Fails", noMessage + "int\". an int has no message" }, + { "CheckThrows_ClassSubclassMessageExpected_Fails", noMessage }, + { "CheckThrows_ClassSubclassMessageExpected_Fails", "TFixtureNonStdSubError\". a subclass with no message" }, + { "CheckThrows_IntExpectedAssertFailsInside_Fails", "inner assert" }, + { "CheckThrows_IntExpectedDoubleThrown_Fails", "): Expected a different exception type but caught: " + unknown + + ". int expected, double thrown" }, + { "CheckThrows_IntExpectedStdExceptionThrown_Fails", + "): Expected a different exception type but caught: boom. int expected, std::exception thrown" }, + { "CheckThrows_MixinMessageAbsent_Fails", + "): Expected the exception message to contain \"needle\" but caught: boom. the subclass's what() is checked" }, + { "CheckThrows_PlainClassMessageExpected_Fails", noMessage }, + { "CheckThrows_PlainClassMessageExpected_Fails", "TFixturePlainError\". a class with no message" }, + { "CheckThrows_StdStringMessageExpected_Fails", noMessage }, + { "CheckThrows_StdStringMessageExpected_Fails", "basic_string(12), results.CaseRecords.size(), __func__, __LINE__, "one record per registered test"); + + for (TTestCaseRecord const& record : results.CaseRecords) + { + if (NameEndsWith(record.TestName, "_Passes")) + CheckEquals(TTestOutcome::Pass, record.Outcome, __func__, __LINE__, record.TestName + " should pass: " + record.Message); + else if (NameEndsWith(record.TestName, "_Fails")) + CheckEquals(TTestOutcome::Fail, record.Outcome, __func__, __LINE__, record.TestName + " should fail"); + else + Fail(__func__, __LINE__, record.TestName + " name must end with _Passes or _Fails"); + } + + for (std::pair const& failure : failures) + { + TTestCaseRecord const* const record = FindRecord(results, failure.first); + AssertTrue(record != nullptr, __func__, __LINE__, std::string(failure.first) + " has a record"); + CheckContains(record->Message, failure.second, __func__, __LINE__, std::string(failure.first) + "'s message"); + } +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_TestBase::Test_TrueFalse_FailureNamesTheExpectedValue() { // Arrange diff --git a/tests/Test_ASWUnitTests_TestBase.h b/tests/Test_ASWUnitTests_TestBase.h index 7318b7d..5023137 100644 --- a/tests/Test_ASWUnitTests_TestBase.h +++ b/tests/Test_ASWUnitTests_TestBase.h @@ -64,15 +64,19 @@ class TTest_ASWUnitTests_TestBase : public TTestGroupBase void Test_Fail_AbortsTestAsFailed(); void Test_IsType_UsesDynamicTypeAndShowsNames(); void Test_Matches_MatchesWholeTextAndShowsPattern(); + void Test_Near_ComparesMixedTypesAndRejectsInvalidTolerance(); void Test_Near_HandlesNaNAndInfinity(); void Test_NullCStrings_MatchNothingInTextChecks(); void Test_NullNotNull_FailureNamesTheExpectedValue(); void Test_Ordering_ComparesByValueAndShowsBoth(); void Test_Run_AbandonsHungTestAndAbortsGroupOnTimeout(); void Test_Run_AppliesFilterToSkipNonMatchingTests(); + void Test_Run_ContinuesAfterCrashInRunObserverUnderTimeout(); void Test_Run_ContinuesAfterCrashWhenCatchCrashesIsSet(); void Test_Run_CountsTestRecordingItsOutcomeAtTimeoutAsFinished(); + void Test_Run_CrashInRunObserverDoesNotBlockLaterRunOnAnotherThread(); void Test_Run_EndsRunOnSetUpOrTearDownTestException(); + void Test_Run_GivesTimedTestTheCallersFloatingPointEnvironment(); void Test_Run_IgnoresTestThatEndsAfterItsTimeout(); void Test_Run_LogsEachCheckFailureOnce(); void Test_Run_RecordsCheckFailuresInFailedTestDetail(); @@ -90,6 +94,7 @@ class TTest_ASWUnitTests_TestBase : public TTestGroupBase void Test_Same_ComparesAddressesNotContent(); void Test_SetExceptionExpected_AssertFailureStillFailsAndIsRecorded(); void Test_SetExceptionExpected_EarlierCheckFailureStillFailsAndIsRecorded(); + void Test_SetExceptionExpected_MatchesAnyType(); void Test_SetExceptionExpected_MatchesTypeAndMessage(); void Test_SetLogSuppressed_SilencesFixtureOutput(); #if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) @@ -97,7 +102,9 @@ class TTest_ASWUnitTests_TestBase : public TTestGroupBase #endif void Test_StartsWithIC_IgnoresASCIICaseOnly(); void Test_StartsWith_ShowsTextAndPrefix(); + void Test_StringChecks_CompareMixedTypesAsTextAndKeepNullRules(); void Test_Throws_ChecksTypeAndMessageAndContinues(); + void Test_Throws_MatchesAnyType(); void Test_TrueFalse_FailureNamesTheExpectedValue(); public: diff --git a/tests/Test_ASWUnitTests_Utils.cpp b/tests/Test_ASWUnitTests_Utils.cpp index dfc6375..f2dbdf7 100644 --- a/tests/Test_ASWUnitTests_Utils.cpp +++ b/tests/Test_ASWUnitTests_Utils.cpp @@ -24,20 +24,50 @@ limitations under the License. // Module header #include "Test_ASWUnitTests_Utils.h" //--------------------------------------------------------------------------- -#include "ASWUnitTests_Registry.h" -//--------------------------------------------------------------------------- #include #include #include #include #include //--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- #include "ASWUnitTests_Utils.h" //--------------------------------------------------------------------------- namespace ASWUnitTests { +#if defined(_WIN32) +namespace +{ + +//--------------------------------------------------------------------------- + +std::string Joined(std::vector const& items); + +//--------------------------------------------------------------------------- +/* + Joined + + The items separated by '|', to compare a list with one CheckEquals(). +*/ +std::string Joined(std::vector const& items) +{ + std::string joined; + + for (std::size_t i = 0; i < items.size(); ++i) + joined += (i == 0 ? "" : "|") + items[i]; + + return joined; +} +//--------------------------------------------------------------------------- + +} // namespace +#endif + +//--------------------------------------------------------------------------- + ///////////////////////////////////////////////////////////////////////////// // TTest_ASWUnitTests_Utils ///////////////////////////////////////////////////////////////////////////// @@ -46,11 +76,29 @@ namespace ASWUnitTests TTest_ASWUnitTests_Utils::TTest_ASWUnitTests_Utils() : inherited("ASWUnitTests_Utils_Tests") { +#if defined(_WIN32) + RegisterTest(&TTest_ASWUnitTests_Utils::Test_CommandLineArguments_ConvertsNonASCIIToUTF8, + "CommandLineArguments_ConvertsNonASCIIToUTF8"); + RegisterTest(&TTest_ASWUnitTests_Utils::Test_CommandLineArguments_FollowsTheCRuntimeQuotingRules, + "CommandLineArguments_FollowsTheCRuntimeQuotingRules"); + RegisterTest(&TTest_ASWUnitTests_Utils::Test_CommandLineArguments_SkipsTheProgramName, + "CommandLineArguments_SkipsTheProgramName"); +#endif RegisterTest(&TTest_ASWUnitTests_Utils::Test_FNV1aHash_MatchesReferenceValues, "FNV1aHash_MatchesReferenceValues"); RegisterTest(&TTest_ASWUnitTests_Utils::Test_ShuffledIndices_GivesSameOrderEverywhere, "ShuffledIndices_GivesSameOrderEverywhere"); RegisterTest(&TTest_ASWUnitTests_Utils::Test_ShuffledIndices_ReturnsEachIndexOnce, "ShuffledIndices_ReturnsEachIndexOnce"); +#if defined(_WIN32) + RegisterTest(&TTest_ASWUnitTests_Utils::Test_SystemErrorText_DescribesAKnownCode, + "SystemErrorText_DescribesAKnownCode"); + RegisterTest(&TTest_ASWUnitTests_Utils::Test_SystemErrorText_GivesTheNumberForAnUnknownCode, + "SystemErrorText_GivesTheNumberForAnUnknownCode"); +#endif + RegisterTest(&TTest_ASWUnitTests_Utils::Test_UTF8ToWide_ConvertsEveryPlane, "UTF8ToWide_ConvertsEveryPlane"); + RegisterTest(&TTest_ASWUnitTests_Utils::Test_UTF8ToWide_ReplacesEachMaximalIllFormedSequence, + "UTF8ToWide_ReplacesEachMaximalIllFormedSequence"); + RegisterTest(&TTest_ASWUnitTests_Utils::Test_UTF8ToWide_ReplacesInvalidUTF8, "UTF8ToWide_ReplacesInvalidUTF8"); RegisterTest(&TTest_ASWUnitTests_Utils::Test_WideToUTF8_EncodesEachSequenceLength, "WideToUTF8_EncodesEachSequenceLength"); RegisterTest(&TTest_ASWUnitTests_Utils::Test_WideToUTF8_KeepsASCIIUnchanged, "WideToUTF8_KeepsASCIIUnchanged"); @@ -85,6 +133,54 @@ void TTest_ASWUnitTests_Utils::TearDown_Test(ITestCase& /*testCase*/) // /////// Begin tests after this line /////////////////////// +//--------------------------------------------------------------------------- +#if defined(_WIN32) +void TTest_ASWUnitTests_Utils::Test_CommandLineArguments_ConvertsNonASCIIToUTF8() +{ + // Arrange: U+00E9 (two UTF-8 bytes), U+4E2D (three) and U+1F600 (four; a surrogate pair in UTF-16). + std::wstring const commandLine = std::wstring(L"prog.exe \x00E9t\x00E9 \x4E2D ") + L"\xD83D\xDE00"; + + // Act + std::vector const args = CommandLineArguments(commandLine); + + // Assert + CheckEquals(std::string("\xC3\xA9t\xC3\xA9|\xE4\xB8\xAD|\xF0\x9F\x98\x80"), Joined(args), __func__, __LINE__, + "each argument in UTF-8"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Utils::Test_CommandLineArguments_FollowsTheCRuntimeQuotingRules() +{ + // Arrange: a quoted program name with a space, a quoted argument with a space, an empty quoted argument, an + // escaped quote, an escaped backslash before a closing quote, and backslashes that precede no quote. + std::wstring const commandLine = + L"\"C:\\Program Files\\prog.exe\" --filter \"a b\" \"\" a\\\"b \"c\\\\\" d\\\\e"; + + // Act + std::vector const args = CommandLineArguments(commandLine); + + // Assert + CheckEquals(std::string("--filter|a b||a\"b|c\\|d\\\\e"), Joined(args), __func__, __LINE__, + "split and unquoted as the C runtime splits argv"); + CheckEquals(6u, args.size(), __func__, __LINE__, "the empty argument is kept"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Utils::Test_CommandLineArguments_SkipsTheProgramName() +{ + // Act + std::vector const programOnly = CommandLineArguments(L"prog.exe"); + std::vector const spaced = CommandLineArguments(L"prog.exe --run --exit "); + // CommandLineToArgvW() gives an empty command line the executable's path, unquoted, as its program name, which + // it then splits at any space in the path; so this checks the fix only when the path has one. + std::vector const empty = CommandLineArguments(L""); + std::vector const blank = CommandLineArguments(L" "); + + // Assert + CheckEmpty(programOnly, __func__, __LINE__, "no arguments after the program name"); + CheckEquals(std::string("--run|--exit"), Joined(spaced), __func__, __LINE__, "extra spaces separate only"); + CheckEmpty(empty, __func__, __LINE__, "an empty command line has no arguments"); + CheckEmpty(blank, __func__, __LINE__, "nor has a blank one"); +} +#endif //--------------------------------------------------------------------------- void TTest_ASWUnitTests_Utils::Test_FNV1aHash_MatchesReferenceValues() { @@ -122,6 +218,77 @@ void TTest_ASWUnitTests_Utils::Test_ShuffledIndices_ReturnsEachIndexOnce() CheckTrue(shuffled != identity, __func__, __LINE__, "and the order is actually shuffled"); } //--------------------------------------------------------------------------- +#if defined(_WIN32) +void TTest_ASWUnitTests_Utils::Test_SystemErrorText_DescribesAKnownCode() +{ + // Act: ERROR_FILE_NOT_FOUND, described in the system's language. + std::string const text = SystemErrorText(2); + + // Assert + CheckNotEmpty(text, __func__, __LINE__, "described"); + CheckNotEquals(std::string("error 2"), text, __func__, __LINE__, "by Windows, not by its number"); + CheckFalse(text.back() == '\n' || text.back() == '\r' || text.back() == ' ', __func__, __LINE__, + "without FormatMessageW()'s line break at the end"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Utils::Test_SystemErrorText_GivesTheNumberForAnUnknownCode() +{ + // Act: a code with the customer bit (bit 29) set, which Windows never describes. + std::string const text = SystemErrorText(0x2FFFFFFFul); + + // Assert + CheckEquals(std::string("error 805306367"), text, __func__, __LINE__, "the code, in decimal"); +} +#endif +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Utils::Test_UTF8ToWide_ConvertsEveryPlane() +{ + // Arrange: ASCII, U+00E9, U+4E2D and U+1F600 (a surrogate pair where wchar_t is 16 bits), and a NUL, which + // ends neither. + std::string const utf8 = std::string("a\xC3\xA9\xE4\xB8\xAD\xF0\x9F\x98\x80") + std::string("\0z", 2); + std::wstring const expected = std::wstring(L"a\x00E9\x4E2D\U0001F600") + std::wstring(L"\0z", 2); + + // Act + std::wstring const wide = UTF8ToWide(utf8); + + // Assert + CheckEquals(expected, wide, __func__, __LINE__, "every character converted"); + CheckEquals(utf8, WideToUTF8(wide), __func__, __LINE__, "and back again"); + CheckEmpty(UTF8ToWide(std::string()), __func__, __LINE__, "empty stays empty"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Utils::Test_UTF8ToWide_ReplacesEachMaximalIllFormedSequence() +{ + // Act: sequences cut short, at the end and before another character; a lead byte UTF-8 never uses; and an + // overlong form, a surrogate and a value above U+10FFFF, whose second byte is out of its lead byte's range. + std::wstring const cutShort = UTF8ToWide("\xE4\xB8" "x\xF0\x9F\x98"); + std::wstring const neverUsed = UTF8ToWide("\xC0\x80"); + std::wstring const overlong = UTF8ToWide("\xE0\x80\x80"); + std::wstring const surrogate = UTF8ToWide("\xED\xA0\x80"); + std::wstring const tooLarge = UTF8ToWide("\xF4\x90\x80\x80"); + + // Assert + CheckEquals(std::wstring(L"\xFFFD" L"x\xFFFD"), cutShort, __func__, __LINE__, + "a sequence cut short is one U+FFFD, and the character after it is kept"); + CheckEquals(std::wstring(L"\xFFFD\xFFFD"), neverUsed, __func__, __LINE__, + "0xC0 can't start a sequence, so it and the continuation byte after it are one each"); + CheckEquals(std::wstring(L"\xFFFD\xFFFD\xFFFD"), overlong, __func__, __LINE__, "an overlong form, one per byte"); + CheckEquals(std::wstring(L"\xFFFD\xFFFD\xFFFD"), surrogate, __func__, __LINE__, "a surrogate, one per byte"); + CheckEquals(std::wstring(L"\xFFFD\xFFFD\xFFFD\xFFFD"), tooLarge, __func__, __LINE__, + "above U+10FFFF, one per byte"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Utils::Test_UTF8ToWide_ReplacesInvalidUTF8() +{ + // Act: a byte that never appears in UTF-8, and a continuation byte with no lead byte. + std::wstring const invalid = UTF8ToWide("a\xFF" "b"); + std::wstring const continuation = UTF8ToWide("\x80" "c"); + + // Assert + CheckEquals(std::wstring(L"a\xFFFD" L"b"), invalid, __func__, __LINE__, "0xFF becomes U+FFFD"); + CheckEquals(std::wstring(L"\xFFFD" L"c"), continuation, __func__, __LINE__, "a lone 0x80 becomes U+FFFD"); +} +//--------------------------------------------------------------------------- void TTest_ASWUnitTests_Utils::Test_WideToUTF8_EncodesEachSequenceLength() { // The first and last code point of each UTF-8 sequence length. The four-byte ones are a surrogate pair where diff --git a/tests/Test_ASWUnitTests_Utils.h b/tests/Test_ASWUnitTests_Utils.h index 8f33aad..8ffbd01 100644 --- a/tests/Test_ASWUnitTests_Utils.h +++ b/tests/Test_ASWUnitTests_Utils.h @@ -39,9 +39,21 @@ class TTest_ASWUnitTests_Utils : public TTestGroupBase typedef TTestGroupBase inherited; private: // Test methods +#if defined(_WIN32) + void Test_CommandLineArguments_ConvertsNonASCIIToUTF8(); + void Test_CommandLineArguments_FollowsTheCRuntimeQuotingRules(); + void Test_CommandLineArguments_SkipsTheProgramName(); +#endif void Test_FNV1aHash_MatchesReferenceValues(); void Test_ShuffledIndices_GivesSameOrderEverywhere(); void Test_ShuffledIndices_ReturnsEachIndexOnce(); +#if defined(_WIN32) + void Test_SystemErrorText_DescribesAKnownCode(); + void Test_SystemErrorText_GivesTheNumberForAnUnknownCode(); +#endif + void Test_UTF8ToWide_ConvertsEveryPlane(); + void Test_UTF8ToWide_ReplacesEachMaximalIllFormedSequence(); + void Test_UTF8ToWide_ReplacesInvalidUTF8(); void Test_WideToUTF8_EncodesEachSequenceLength(); void Test_WideToUTF8_KeepsASCIIUnchanged(); #if WCHAR_MAX > 0xFFFF diff --git a/tests/Test_ASWUnitTests_Version.cpp b/tests/Test_ASWUnitTests_Version.cpp index 01429ed..b8ef63e 100644 --- a/tests/Test_ASWUnitTests_Version.cpp +++ b/tests/Test_ASWUnitTests_Version.cpp @@ -24,12 +24,12 @@ limitations under the License. // Module header #include "Test_ASWUnitTests_Version.h" //--------------------------------------------------------------------------- -#include "ASWUnitTests_Registry.h" -//--------------------------------------------------------------------------- #include #include #include //--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- #include "ASWUnitTests_Handler.h" #include "ASWUnitTests_Version.h" //--------------------------------------------------------------------------- @@ -37,7 +37,16 @@ limitations under the License. namespace { -// A SemVer pre-release identifier: not empty, only [0-9A-Za-z-], and no leading zero if it's all digits. +//--------------------------------------------------------------------------- + +bool IsValidPreReleaseIdentifier(std::string_view identifier); + +//--------------------------------------------------------------------------- +/* + IsValidPreReleaseIdentifier + + A SemVer pre-release identifier: not empty, only [0-9A-Za-z-], and no leading zero if it's all digits. +*/ bool IsValidPreReleaseIdentifier(std::string_view identifier) { if (identifier.empty()) @@ -56,9 +65,12 @@ bool IsValidPreReleaseIdentifier(std::string_view identifier) return !isNumeric || identifier.size() == 1 || identifier.front() != '0'; } +//--------------------------------------------------------------------------- } // namespace +//--------------------------------------------------------------------------- + namespace ASWUnitTests { diff --git a/tests/UT_Helper_ExceptionTypes.cpp b/tests/UT_Helper_ExceptionTypes.cpp new file mode 100644 index 0000000..cc90ade --- /dev/null +++ b/tests/UT_Helper_ExceptionTypes.cpp @@ -0,0 +1,375 @@ +/* ************************************************************************** +UT_Helper_ExceptionTypes.cpp +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "UT_Helper_ExceptionTypes.h" +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- + +namespace +{ + +///////////////////////////////////////////////////////////////////////////// +// TFixtureErrorTag +// +// A polymorphic class not derived from std::exception, used as a mixin base of a std::exception subclass +// (TFixtureTaggedError), so expecting it matches that subclass and finds its what(). +///////////////////////////////////////////////////////////////////////////// +class TFixtureErrorTag +{ +public: + virtual ~TFixtureErrorTag() = default; +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixtureNonStdError +// +// A polymorphic exception class not derived from std::exception, with a subclass, TFixtureNonStdSubError, so a +// failure shows the dynamic type of what was caught. +///////////////////////////////////////////////////////////////////////////// +class TFixtureNonStdError +{ +public: + virtual ~TFixtureNonStdError() = default; +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixtureNonStdSubError +///////////////////////////////////////////////////////////////////////////// +class TFixtureNonStdSubError : public TFixtureNonStdError +{ +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixturePlainError +// +// An exception class with no virtual functions, so nothing but its type is known about a caught one. +///////////////////////////////////////////////////////////////////////////// +struct TFixturePlainError +{ + int Code; +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixtureTaggedError +///////////////////////////////////////////////////////////////////////////// +class TFixtureTaggedError : public std::runtime_error, public TFixtureErrorTag +{ +public: + explicit TFixtureTaggedError(std::string const& msg) + : std::runtime_error(msg) + { + } +}; + +} // namespace + + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TFixture_AnyTypeExceptionExpectations +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TFixture_AnyTypeExceptionExpectations::TFixture_AnyTypeExceptionExpectations() + : inherited("Fixture_AnyTypeExceptionExpectations") +{ + SetLogSuppressed(true); + + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_CheckFailedThenIntThrown_Fails, + "CheckFailedThenIntThrown_Fails"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_ClassExpected_MessageExpected_Fails, + "ClassExpected_MessageExpected_Fails"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_ClassExpected_SubclassThrown_Passes, + "ClassExpected_SubclassThrown_Passes"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_ClassExpected_SubclassThrownMessageExpected_Fails, + "ClassExpected_SubclassThrownMessageExpected_Fails"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_IntExpected_AssertFails_Fails, + "IntExpected_AssertFails_Fails"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_IntExpected_DoubleThrown_Fails, + "IntExpected_DoubleThrown_Fails"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_IntExpected_IntThrown_Passes, + "IntExpected_IntThrown_Passes"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_IntExpected_MessageExpected_Fails, + "IntExpected_MessageExpected_Fails"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_IntExpected_NoneThrown_Fails, + "IntExpected_NoneThrown_Fails"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_IntExpected_StdExceptionThrown_Fails, + "IntExpected_StdExceptionThrown_Fails"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_IntThenGenericExpected_DoubleThrown_Passes, + "IntThenGenericExpected_DoubleThrown_Passes"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_IntThenStdTypeExpected_IntThrown_Fails, + "IntThenStdTypeExpected_IntThrown_Fails"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_MixinExpected_StdExceptionWithoutMixinThrown_Fails, + "MixinExpected_StdExceptionWithoutMixinThrown_Fails"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_MixinExpected_TaggedErrorMessageAbsent_Fails, + "MixinExpected_TaggedErrorMessageAbsent_Fails"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_MixinExpected_TaggedErrorThrown_Passes, + "MixinExpected_TaggedErrorThrown_Passes"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_PointerExpected_StringLiteralThrown_Passes, + "PointerExpected_StringLiteralThrown_Passes"); + RegisterTest(&TFixture_AnyTypeExceptionExpectations::Test_StdTypeExpected_IntThrown_Fails, + "StdTypeExpected_IntThrown_Fails"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_CheckFailedThenIntThrown_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "int expected, then a failed Check"); + CheckTrue(false, __func__, __LINE__, "deliberate Check failure before the expected exception"); + throw 42; +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_ClassExpected_MessageExpected_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "a class with no message", "needle"); + throw TFixturePlainError{ 1 }; +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_ClassExpected_SubclassThrown_Passes() +{ + SetExceptionExpected(__func__, __LINE__, "base class expected, subclass thrown"); + throw TFixtureNonStdSubError(); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_ClassExpected_SubclassThrownMessageExpected_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "a subclass with no message", "needle"); + throw TFixtureNonStdSubError(); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_IntExpected_AssertFails_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "int expected, then a failed Assert"); + AssertTrue(false, __func__, __LINE__, "deliberate Assert failure while an int is expected"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_IntExpected_DoubleThrown_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "int expected, double thrown"); + throw 4.2; +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_IntExpected_IntThrown_Passes() +{ + SetExceptionExpected(__func__, __LINE__, "int expected and thrown"); + throw 42; +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_IntExpected_MessageExpected_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "an int has no message", "needle"); + throw 42; +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_IntExpected_NoneThrown_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "expected an int that never comes"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_IntExpected_StdExceptionThrown_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "int expected, std::exception thrown"); + throw std::runtime_error("boom"); +} +//--------------------------------------------------------------------------- +/* + TFixture_AnyTypeExceptionExpectations::Test_IntThenGenericExpected_DoubleThrown_Passes + + This and the next replace an expectation of int with another, which must replace its type check too. +*/ +void TFixture_AnyTypeExceptionExpectations::Test_IntThenGenericExpected_DoubleThrown_Passes() +{ + SetExceptionExpected(__func__, __LINE__, "int expected first"); + SetExceptionExpected(true, __func__, __LINE__, "then any exception"); + throw 4.2; +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_IntThenStdTypeExpected_IntThrown_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "int expected first"); + SetExceptionExpected(__func__, __LINE__, "then a std::runtime_error"); + throw 42; +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_MixinExpected_StdExceptionWithoutMixinThrown_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "mixin expected, a std::exception without it thrown"); + throw std::runtime_error("boom"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_MixinExpected_TaggedErrorMessageAbsent_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "the subclass's what() is checked", "needle"); + throw TFixtureTaggedError("boom"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_MixinExpected_TaggedErrorThrown_Passes() +{ + SetExceptionExpected(__func__, __LINE__, "the subclass's what() is checked", "needle"); + throw TFixtureTaggedError("hay needle stack"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_PointerExpected_StringLiteralThrown_Passes() +{ + SetExceptionExpected(__func__, __LINE__, "a string literal is thrown as a char const*"); + throw "boom"; +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeExceptionExpectations::Test_StdTypeExpected_IntThrown_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "std::runtime_error expected, int thrown"); + throw 42; +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_AnyTypeThrowsChecks +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TFixture_AnyTypeThrowsChecks::TFixture_AnyTypeThrowsChecks() + : inherited("Fixture_AnyTypeThrowsChecks") +{ + SetLogSuppressed(true); + + RegisterTest(&TFixture_AnyTypeThrowsChecks::Test_AssertThrows_IntMessageExpected_Fails, + "AssertThrows_IntMessageExpected_Fails"); + RegisterTest(&TFixture_AnyTypeThrowsChecks::Test_AssertThrows_IntThrown_Passes, "AssertThrows_IntThrown_Passes"); + RegisterTest(&TFixture_AnyTypeThrowsChecks::Test_CheckThrows_ClassSubclassMessageExpected_Fails, + "CheckThrows_ClassSubclassMessageExpected_Fails"); + RegisterTest(&TFixture_AnyTypeThrowsChecks::Test_CheckThrows_ClassSubclassThrown_Passes, + "CheckThrows_ClassSubclassThrown_Passes"); + RegisterTest(&TFixture_AnyTypeThrowsChecks::Test_CheckThrows_IntExpectedAssertFailsInside_Fails, + "CheckThrows_IntExpectedAssertFailsInside_Fails"); + RegisterTest(&TFixture_AnyTypeThrowsChecks::Test_CheckThrows_IntExpectedDoubleThrown_Fails, + "CheckThrows_IntExpectedDoubleThrown_Fails"); + RegisterTest(&TFixture_AnyTypeThrowsChecks::Test_CheckThrows_IntExpectedStdExceptionThrown_Fails, + "CheckThrows_IntExpectedStdExceptionThrown_Fails"); + RegisterTest(&TFixture_AnyTypeThrowsChecks::Test_CheckThrows_MixinMessageAbsent_Fails, + "CheckThrows_MixinMessageAbsent_Fails"); + RegisterTest(&TFixture_AnyTypeThrowsChecks::Test_CheckThrows_MixinMessagePresent_Passes, + "CheckThrows_MixinMessagePresent_Passes"); + RegisterTest(&TFixture_AnyTypeThrowsChecks::Test_CheckThrows_PlainClassMessageExpected_Fails, + "CheckThrows_PlainClassMessageExpected_Fails"); + RegisterTest(&TFixture_AnyTypeThrowsChecks::Test_CheckThrows_PointerStringLiteralThrown_Passes, + "CheckThrows_PointerStringLiteralThrown_Passes"); + RegisterTest(&TFixture_AnyTypeThrowsChecks::Test_CheckThrows_StdStringMessageExpected_Fails, + "CheckThrows_StdStringMessageExpected_Fails"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeThrowsChecks::Test_AssertThrows_IntMessageExpected_Fails() +{ + AssertThrows([] { + throw 42; + }, __func__, __LINE__, "an int has no message", "needle"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeThrowsChecks::Test_AssertThrows_IntThrown_Passes() +{ + AssertThrows([] { + throw 42; + }, __func__, __LINE__, "int expected and thrown"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeThrowsChecks::Test_CheckThrows_ClassSubclassMessageExpected_Fails() +{ + CheckThrows([] { + throw TFixtureNonStdSubError(); + }, __func__, __LINE__, "a subclass with no message", "needle"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeThrowsChecks::Test_CheckThrows_ClassSubclassThrown_Passes() +{ + CheckThrows([] { + throw TFixtureNonStdSubError(); + }, __func__, __LINE__, "base class expected, subclass thrown"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeThrowsChecks::Test_CheckThrows_IntExpectedAssertFailsInside_Fails() +{ + CheckThrows([this] { + AssertTrue(false, __func__, __LINE__, "inner assert"); + }, __func__, __LINE__, "an Assert failure isn't the int being checked for"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeThrowsChecks::Test_CheckThrows_IntExpectedDoubleThrown_Fails() +{ + CheckThrows([] { + throw 4.2; + }, __func__, __LINE__, "int expected, double thrown"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeThrowsChecks::Test_CheckThrows_IntExpectedStdExceptionThrown_Fails() +{ + CheckThrows([] { + throw std::runtime_error("boom"); + }, __func__, __LINE__, "int expected, std::exception thrown"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeThrowsChecks::Test_CheckThrows_MixinMessageAbsent_Fails() +{ + CheckThrows([] { + throw TFixtureTaggedError("boom"); + }, __func__, __LINE__, "the subclass's what() is checked", "needle"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeThrowsChecks::Test_CheckThrows_MixinMessagePresent_Passes() +{ + CheckThrows([] { + throw TFixtureTaggedError("hay needle stack"); + }, __func__, __LINE__, "the subclass's what() is checked", "needle"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeThrowsChecks::Test_CheckThrows_PlainClassMessageExpected_Fails() +{ + CheckThrows([] { + throw TFixturePlainError{ 1 }; + }, __func__, __LINE__, "a class with no message", "needle"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeThrowsChecks::Test_CheckThrows_PointerStringLiteralThrown_Passes() +{ + CheckThrows([] { + throw "boom"; + }, __func__, __LINE__, "a string literal is thrown as a char const*"); +} +//--------------------------------------------------------------------------- +void TFixture_AnyTypeThrowsChecks::Test_CheckThrows_StdStringMessageExpected_Fails() +{ + // A std::string isn't a std::exception, so even one holding the text has no message. + CheckThrows([] { + throw std::string("needle"); + }, __func__, __LINE__, "a std::string has no message", "needle"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- diff --git a/tests/UT_Helper_ExceptionTypes.h b/tests/UT_Helper_ExceptionTypes.h new file mode 100644 index 0000000..304f3b1 --- /dev/null +++ b/tests/UT_Helper_ExceptionTypes.h @@ -0,0 +1,106 @@ +/* ************************************************************************** +UT_Helper_ExceptionTypes.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +#ifndef UT_Helper_ExceptionTypesH +#define UT_Helper_ExceptionTypesH +#include "ASWUnitTests_TestBase.h" + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TFixture_AnyTypeExceptionExpectations +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group expecting, with SetExceptionExpected(), +// types that aren't std::exception classes: int, a C string pointer, classes of its own (polymorphic or not), and a +// mixin base of a std::exception subclass. Each is thrown, not thrown, crossed with another kind, or expected with a +// message, so Test_SetExceptionExpected_MatchesAnyType (Test_ASWUnitTests_TestBase.cpp) can check the outcomes and +// messages. Test names end with "_Passes" or "_Fails", the expected outcome. +///////////////////////////////////////////////////////////////////////////// +class TFixture_AnyTypeExceptionExpectations : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_CheckFailedThenIntThrown_Fails(); + void Test_ClassExpected_MessageExpected_Fails(); + void Test_ClassExpected_SubclassThrown_Passes(); + void Test_ClassExpected_SubclassThrownMessageExpected_Fails(); + void Test_IntExpected_AssertFails_Fails(); + void Test_IntExpected_DoubleThrown_Fails(); + void Test_IntExpected_IntThrown_Passes(); + void Test_IntExpected_MessageExpected_Fails(); + void Test_IntExpected_NoneThrown_Fails(); + void Test_IntExpected_StdExceptionThrown_Fails(); + void Test_IntThenGenericExpected_DoubleThrown_Passes(); + void Test_IntThenStdTypeExpected_IntThrown_Fails(); + void Test_MixinExpected_StdExceptionWithoutMixinThrown_Fails(); + void Test_MixinExpected_TaggedErrorMessageAbsent_Fails(); + void Test_MixinExpected_TaggedErrorThrown_Passes(); + void Test_PointerExpected_StringLiteralThrown_Passes(); + void Test_StdTypeExpected_IntThrown_Fails(); + +public: + TFixture_AnyTypeExceptionExpectations(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_AnyTypeThrowsChecks +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group calling AssertThrows()/CheckThrows() with the same +// kinds of types as TFixture_AnyTypeExceptionExpectations, so Test_Throws_MatchesAnyType +// (Test_ASWUnitTests_TestBase.cpp) can check the outcomes and messages. Test names end with "_Passes" or "_Fails", +// the expected outcome. +///////////////////////////////////////////////////////////////////////////// +class TFixture_AnyTypeThrowsChecks : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertThrows_IntMessageExpected_Fails(); + void Test_AssertThrows_IntThrown_Passes(); + void Test_CheckThrows_ClassSubclassMessageExpected_Fails(); + void Test_CheckThrows_ClassSubclassThrown_Passes(); + void Test_CheckThrows_IntExpectedAssertFailsInside_Fails(); + void Test_CheckThrows_IntExpectedDoubleThrown_Fails(); + void Test_CheckThrows_IntExpectedStdExceptionThrown_Fails(); + void Test_CheckThrows_MixinMessageAbsent_Fails(); + void Test_CheckThrows_MixinMessagePresent_Passes(); + void Test_CheckThrows_PlainClassMessageExpected_Fails(); + void Test_CheckThrows_PointerStringLiteralThrown_Passes(); + void Test_CheckThrows_StdStringMessageExpected_Fails(); + +public: + TFixture_AnyTypeThrowsChecks(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef UT_Helper_ExceptionTypesH diff --git a/tests/UT_Helper_StringChecks.cpp b/tests/UT_Helper_StringChecks.cpp new file mode 100644 index 0000000..66a236e --- /dev/null +++ b/tests/UT_Helper_StringChecks.cpp @@ -0,0 +1,297 @@ +/* ************************************************************************** +UT_Helper_StringChecks.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "UT_Helper_StringChecks.h" +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TStringCheckProbe +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TStringCheckProbe::TCheck const TStringCheckProbe::Checks[18] = +{ + { "Contains", ERule::Matches, false }, + { "ContainsIC", ERule::Matches, true }, + { "EndsWith", ERule::Matches, false }, + { "EndsWithIC", ERule::Matches, true }, + { "Equals", ERule::Equals, false }, + { "EqualsIC", ERule::Equals, true }, + { "Matches", ERule::Matches, false }, + { "NotContains", ERule::NotMatches, false }, + { "NotContainsIC", ERule::NotMatches, true }, + { "NotEndsWith", ERule::NotMatches, false }, + { "NotEndsWithIC", ERule::NotMatches, true }, + { "NotEquals", ERule::NotEquals, false }, + { "NotEqualsIC", ERule::NotEquals, true }, + { "NotMatches", ERule::NotMatches, false }, + { "NotStartsWith", ERule::NotMatches, false }, + { "NotStartsWithIC", ERule::NotMatches, true }, + { "StartsWith", ERule::Matches, false }, + { "StartsWithIC", ERule::Matches, true }, +}; + +//--------------------------------------------------------------------------- +TStringCheckProbe::TStringCheckProbe() + : inherited("Helper_StringCheckProbe") +{ + SetLogSuppressed(true); +} +//--------------------------------------------------------------------------- +/* + TStringCheckProbe::AssertFailure + + Calls 'assertion' and returns its failure's message, or nullopt when it passes. +*/ +template +std::optional TStringCheckProbe::AssertFailure(TAssertion assertion) +{ + try + { + assertion(); + } + catch (TTestException const& ex) + { + return std::string(ex.what()); + } + + return std::nullopt; +} +//--------------------------------------------------------------------------- +#if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) +/* + TStringCheckProbe::CheckEqualsThroughSourceLocation + + Calls CheckEquals() through its std::source_location overload, which forwards to the method/line one, and + returns its failure message, or nullopt when it passes. +*/ +std::optional TStringCheckProbe::CheckEqualsThroughSourceLocation(std::string const& expected, + wchar_t const* actual) +{ + CheckEquals(expected, actual, "msg"); + return TakeCheckFailure(); +} +//--------------------------------------------------------------------------- +#endif +/* + TStringCheckProbe::IndexOf + + The position of the method family named 'name' in Checks, and in RunAsserts()'s and RunChecks()'s results. +*/ +std::size_t TStringCheckProbe::IndexOf(std::string const& name) +{ + for (std::size_t i = 0; i < std::size(Checks); ++i) + { + if (name == Checks[i].Name) + return i; + } + + throw std::logic_error("no string check named " + name); +} +//--------------------------------------------------------------------------- +template +std::vector > TStringCheckProbe::RunAsserts(TFirst first, TSecond second) +{ + std::vector > results; + + results.push_back(AssertFailure([&]() { + AssertContains(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertContainsIC(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertEndsWith(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertEndsWithIC(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertEquals(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertEqualsIC(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertMatches(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertNotContains(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertNotContainsIC(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertNotEndsWith(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertNotEndsWithIC(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertNotEquals(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertNotEqualsIC(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertNotMatches(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertNotStartsWith(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertNotStartsWithIC(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertStartsWith(first, second, __func__, __LINE__, "msg"); + })); + results.push_back(AssertFailure([&]() { + AssertStartsWithIC(first, second, __func__, __LINE__, "msg"); + })); + + return results; +} +//--------------------------------------------------------------------------- +template +std::vector > TStringCheckProbe::RunChecks(TFirst first, TSecond second) +{ + std::vector > results; + + CheckContains(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckContainsIC(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckEndsWith(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckEndsWithIC(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckEquals(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckEqualsIC(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckMatches(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckNotContains(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckNotContainsIC(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckNotEndsWith(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckNotEndsWithIC(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckNotEquals(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckNotEqualsIC(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckNotMatches(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckNotStartsWith(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckNotStartsWithIC(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckStartsWith(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + CheckStartsWithIC(first, second, __func__, __LINE__, "msg"); + results.push_back(TakeCheckFailure()); + + return results; +} +//--------------------------------------------------------------------------- +/* + TStringCheckProbe::TakeCheckFailure + + The message of the Check failure since the last call, or nullopt if there was none, then forgets it, for the + next Check method. +*/ +std::optional TStringCheckProbe::TakeCheckFailure() +{ + std::optional failure; + + if (TestFailedOneOrMoreChecks()) + failure = m_CheckFailureMessages.back(); + + ResetTestFailedOneOrMoreChecks(); + return failure; +} +//--------------------------------------------------------------------------- + +// The pairs of types the tests use. +template std::vector > TStringCheckProbe::RunAsserts( + std::string, char const*); +template std::vector > TStringCheckProbe::RunAsserts( + std::string, std::wstring); +template std::vector > TStringCheckProbe::RunAsserts( + std::string, wchar_t const*); +template std::vector > TStringCheckProbe::RunAsserts( + char const*, std::string); +template std::vector > TStringCheckProbe::RunAsserts( + char const*, std::wstring); +template std::vector > TStringCheckProbe::RunAsserts( + char const*, wchar_t const*); +template std::vector > TStringCheckProbe::RunAsserts( + std::wstring, std::string); +template std::vector > TStringCheckProbe::RunAsserts( + std::wstring, char const*); +template std::vector > TStringCheckProbe::RunAsserts( + std::wstring, wchar_t const*); +template std::vector > TStringCheckProbe::RunAsserts( + wchar_t const*, std::string); +template std::vector > TStringCheckProbe::RunAsserts( + wchar_t const*, char const*); +template std::vector > TStringCheckProbe::RunAsserts( + wchar_t const*, std::wstring); +template std::vector > TStringCheckProbe::RunChecks( + std::string, char const*); +template std::vector > TStringCheckProbe::RunChecks( + std::string, std::wstring); +template std::vector > TStringCheckProbe::RunChecks( + std::string, wchar_t const*); +template std::vector > TStringCheckProbe::RunChecks( + char const*, std::string); +template std::vector > TStringCheckProbe::RunChecks( + char const*, std::wstring); +template std::vector > TStringCheckProbe::RunChecks( + char const*, wchar_t const*); +template std::vector > TStringCheckProbe::RunChecks( + std::wstring, std::string); +template std::vector > TStringCheckProbe::RunChecks( + std::wstring, char const*); +template std::vector > TStringCheckProbe::RunChecks( + std::wstring, wchar_t const*); +template std::vector > TStringCheckProbe::RunChecks( + wchar_t const*, std::string); +template std::vector > TStringCheckProbe::RunChecks( + wchar_t const*, char const*); +template std::vector > TStringCheckProbe::RunChecks( + wchar_t const*, std::wstring); + +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/tests/UT_Helper_StringChecks.h b/tests/UT_Helper_StringChecks.h new file mode 100644 index 0000000..46bc36c --- /dev/null +++ b/tests/UT_Helper_StringChecks.h @@ -0,0 +1,106 @@ +/* ************************************************************************** +UT_Helper_StringChecks.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef UT_Helper_StringChecksH +#define UT_Helper_StringChecksH +//--------------------------------------------------------------------------- +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TStringCheckProbe +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) group whose RunChecks() calls every Check method of the +// Equals, Contains, StartsWith, EndsWith and Matches families, IC and Not forms included, with the same two +// texts, and returns what each did, and whose RunAsserts() does the same with the Assert methods, so +// TTest_ASWUnitTests_TestBase can check them with texts of any two types. Their templates are defined, and +// instantiated for the pairs of types the tests use, in this module rather than in the test module: there, +// they make it too big for RAD Studio's Win32 linker (its line number cache, in a Debug build). +///////////////////////////////////////////////////////////////////////////// +class TStringCheckProbe : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +public: + // How a string check treats its texts, which decides each case's expected outcome. + enum class ERule + { + Equals, // Equals and EqualsIC: a null C string equals only another one. + NotEquals, // NotEquals and NotEqualsIC. + Matches, // Contains, StartsWith, EndsWith and Matches: a null C string matches nothing. + NotMatches // Their Not forms. + }; + + struct TCheck + { + char const* Name; + ERule Rule; + bool IgnoresCase; + }; + +public: + // The method families RunAsserts() and RunChecks() call, in the order of their results, e.g. "ContainsIC" + // for AssertContainsIC() and CheckContainsIC(). + static TCheck const Checks[18]; + +private: + template + static std::optional AssertFailure(TAssertion assertion); + +private: + std::optional TakeCheckFailure(); + +public: + static std::size_t IndexOf(std::string const& name); + +public: + TStringCheckProbe(); + +#if defined(ASWUNITTESTS_SOURCE_LOCATION_ENABLED) + std::optional CheckEqualsThroughSourceLocation(std::string const& expected, wchar_t const* actual); +#endif + // Calls the Assert method of each of Checks with 'first' and 'second' and returns, in the same order, each one's + // failure message, or nullopt for each that passed. The texts are taken by value, so an array is a C string. + // Instantiated in the .cpp for each two different types of std::string, char const*, std::wstring and + // wchar_t const*. + template + std::vector > RunAsserts(TFirst first, TSecond second); + // The same, with the Check methods. + template + std::vector > RunChecks(TFirst first, TSecond second); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef UT_Helper_StringChecksH diff --git a/vcl/console/rad370/ASWUnitTests_VCL_Console.cbproj b/vcl/console/rad370/ASWUnitTests_VCL_Console.cbproj index c14eedf..a05e556 100644 --- a/vcl/console/rad370/ASWUnitTests_VCL_Console.cbproj +++ b/vcl/console/rad370/ASWUnitTests_VCL_Console.cbproj @@ -59,11 +59,12 @@ true $(BDSLIB)\$(PLATFORM)\release\$(LANGDIR);$(ILINK_TranslatedLibraryPath) CppConsoleApplication + ..\..\..\resource\ASWUnitTests.ico System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace) rtl.lib;vcl.lib true - ..\..\..\src\;..\..\..\tests\;..\..\..\toTest\;..\..\tests\;..\..\gui\src\;$(IncludePath) - ..\..\..\src\;..\..\..\tests\;..\..\..\toTest\;..\..\tests\;..\..\gui\src\;$(ILINK_LibraryPath) + ..\..\..\src\;..\..\..\tests\;..\..\..\toTest\;..\..\tests\;..\..\gui\src\;..\..\..\gui\src\;..\..\..\gui\tests\;$(IncludePath) + ..\..\..\src\;..\..\..\tests\;..\..\..\toTest\;..\..\tests\;..\..\gui\src\;..\..\..\gui\src\;..\..\..\gui\tests\;$(ILINK_LibraryPath) <_TCHARMapping>char true false @@ -246,6 +247,14 @@ ..\..\..\tests\Test_ASWUnitTests_Version.h 33 + + ..\..\..\tests\UT_Helper_ExceptionTypes.h + 54 + + + ..\..\..\tests\UT_Helper_StringChecks.h + 53 + ..\..\..\toTest\ASWTools_Random.h 20 @@ -258,62 +267,110 @@ 0 true - - ..\..\gui\src\ASWUnitTests_GUI_CommandLine.h + + ..\..\..\gui\src\ASWUnitTests_GUI_CommandLine.h 22 - - ..\..\gui\src\ASWUnitTests_GUI_Layout.h + + ..\..\..\gui\src\ASWUnitTests_GUI_Commands.h + 49 + + + ..\..\..\gui\src\ASWUnitTests_GUI_INIDocument.h + 45 + + + ..\..\..\gui\src\ASWUnitTests_GUI_Layout.h 31 - - ..\..\gui\src\ASWUnitTests_GUI_RunObserver.h + + ..\..\..\gui\src\ASWUnitTests_GUI_RunObserver.h 23 - - ..\..\gui\src\ASWUnitTests_GUI_Selection.h + + ..\..\..\gui\src\ASWUnitTests_GUI_Selection.h 34 - - ..\..\gui\src\ASWUnitTests_GUI_Shuffle.h + + ..\..\..\gui\src\ASWUnitTests_GUI_Shuffle.h 39 ..\..\gui\src\ASWUnitTests_GUI_Strings.h 24 - - ..\..\gui\src\ASWUnitTests_GUI_TestList.h + + ..\..\..\gui\src\ASWUnitTests_GUI_TestList.h 25 - - ..\..\tests\Test_ASWUnitTests_GUI_CommandLine.h + + ..\..\..\gui\src\ASWUnitTests_GUI_Theme.h + 55 + + + ..\..\..\gui\src\ASWUnitTests_GUI_Tree.h + 47 + + + ..\..\..\gui\src\ASWUnitTests_GUI_View.h + 51 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_CommandLine.h 27 - - ..\..\tests\Test_ASWUnitTests_GUI_Layout.h + + ..\..\gui\src\ASWUnitTests_GUI_VCLSystem.h + 43 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Commands.h + 50 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_INIDocument.h + 46 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Layout.h 32 - - ..\..\tests\Test_ASWUnitTests_GUI_RunObserver.h + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_RunObserver.h 28 - - ..\..\tests\Test_ASWUnitTests_GUI_Selection.h + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Selection.h 35 - - ..\..\tests\Test_ASWUnitTests_GUI_Shuffle.h + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Shuffle.h 40 ..\..\tests\Test_ASWUnitTests_GUI_Strings.h 29 - - ..\..\tests\Test_ASWUnitTests_GUI_TestList.h + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_TestList.h 30 + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Theme.h + 56 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Tree.h + 48 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_View.h + 52 + + + ..\..\tests\Test_ASWUnitTests_GUI_VCLSystem.h + 44 + ..\..\tests\Test_ASWUnitTests_RTLExceptions.h 26 diff --git a/vcl/gui/rad370/ASWUnitTests_VCL_GUI.cbproj b/vcl/gui/rad370/ASWUnitTests_VCL_GUI.cbproj index d3aa5c1..fec860c 100644 --- a/vcl/gui/rad370/ASWUnitTests_VCL_GUI.cbproj +++ b/vcl/gui/rad370/ASWUnitTests_VCL_GUI.cbproj @@ -88,12 +88,12 @@ rtl.lib;vcl.lib <_TCHARMapping>wchar_t true - $(BDS)\bin\cbuilder_PROJECTICON.ico + ..\..\..\resource\ASWUnitTests.ico $(BDS)\bin\Artwork\Windows\UWP\cppreg_UwpDefault_44.png $(BDS)\bin\Artwork\Windows\UWP\cppreg_UwpDefault_150.png ASWUnitTests_VCL_GUI - ..\src\;..\..\..\src\;..\..\..\tests\;..\..\..\toTest\;..\..\tests\;$(IncludePath) - ..\src\;..\..\..\src\;..\..\..\tests\;..\..\..\toTest\;..\..\tests\;$(ILINK_LibraryPath) + ..\src\;..\..\..\src\;..\..\..\tests\;..\..\..\toTest\;..\..\tests\;..\..\..\gui\src\;..\..\..\gui\tests\;$(IncludePath) + ..\src\;..\..\..\src\;..\..\..\tests\;..\..\..\toTest\;..\..\tests\;..\..\..\gui\src\;..\..\..\gui\tests\;$(ILINK_LibraryPath) ASWUNITTESTS_RTL_EXCEPTIONS;$(Defines) true false @@ -101,6 +101,7 @@ 1033 CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= false + Carbon|VCLSTYLE|$(BDSCOMMONDIR)\Styles\Carbon.vsf;"Charcoal Dark Slate|VCLSTYLE|$(BDSCOMMONDIR)\Styles\CharcoalDarkSlate.vsf";"Cobalt XEMedia|VCLSTYLE|$(BDSCOMMONDIR)\Styles\CobaltXEMedia.vsf";"Onyx Blue|VCLSTYLE|$(BDSCOMMONDIR)\Styles\OnyxBlue.vsf";"Tablet Dark|VCLSTYLE|$(BDSCOMMONDIR)\Styles\TabletDark.vsf" adortl;bcbie;bcbsmp;bindcomp;bindcompdbx;bindcompfmx;bindcompvcl;bindcompvclsmp;bindcompvclwinx;bindengine;CloudService;CustomIPTransport;dbexpress;dbrtl;dbxcds;DbxClientDriver;DbxCommonDriver;DBXInterBaseDriver;DBXMySQLDriver;DBXSqliteDriver;dsnap;dsnapcon;dsnapxml;FireDAC;FireDACADSDriver;FireDACCommon;FireDACCommonDriver;FireDACCommonODBC;FireDACIBDriver;FireDACMSAccDriver;FireDACMySQLDriver;FireDACPgDriver;FireDACSqliteDriver;fmx;fmxase;fmxdae;fmxFireDAC;fmxobj;FMXTee;FmxTeeUI;IndyCore;IndyIPClient;IndyIPCommon;IndyIPServer;IndyProtocols;IndySystem;inet;inetdb;inetdbxpress;inetstn;RESTBackendComponents;RESTComponents;rtl;Skia;soapmidas;soaprtl;soapserver;Tee;TeeDB;TeeUI;tethering;vcl;vclactnband;vcldb;vcldsnap;vcledge;vclFireDAC;vclie;vclimg;VCLRESTComponents;VclSmp;vcltouch;vclwinx;vclx;xmlrtl;$(PackageImports) @@ -283,6 +284,14 @@ ..\..\..\tests\Test_ASWUnitTests_Version.h 43 + + ..\..\..\tests\UT_Helper_ExceptionTypes.h + 57 + + + ..\..\..\tests\UT_Helper_StringChecks.h + 56 + ..\..\..\toTest\ASWTools_Random.h 26 @@ -298,12 +307,20 @@ 1 true - - ..\src\ASWUnitTests_GUI_CommandLine.h + + ..\..\..\gui\src\ASWUnitTests_GUI_CommandLine.h 2 - - ..\src\ASWUnitTests_GUI_Layout.h + + ..\..\..\gui\src\ASWUnitTests_GUI_Commands.h + 52 + + + ..\..\..\gui\src\ASWUnitTests_GUI_INIDocument.h + 48 + + + ..\..\..\gui\src\ASWUnitTests_GUI_Layout.h 33 @@ -312,16 +329,16 @@ ..\src\ASWUnitTests_GUI_MainForm.h 1 - - ..\src\ASWUnitTests_GUI_RunObserver.h + + ..\..\..\gui\src\ASWUnitTests_GUI_RunObserver.h 3 - - ..\src\ASWUnitTests_GUI_Selection.h + + ..\..\..\gui\src\ASWUnitTests_GUI_Selection.h 35 - - ..\src\ASWUnitTests_GUI_Shuffle.h + + ..\..\..\gui\src\ASWUnitTests_GUI_Shuffle.h 39 @@ -332,42 +349,82 @@ ..\src\ASWUnitTests_GUI_Strings.h 4 - - ..\src\ASWUnitTests_GUI_TestList.h + + ..\..\..\gui\src\ASWUnitTests_GUI_TestList.h 5 + + ..\..\..\gui\src\ASWUnitTests_GUI_Theme.h + 58 + + + ..\..\..\gui\src\ASWUnitTests_GUI_Tree.h + 50 + + + ..\..\..\gui\src\ASWUnitTests_GUI_View.h + 54 + ..\src\ASWUnitTests_GUI_TextDialog.h 6 - - ..\..\tests\Test_ASWUnitTests_GUI_CommandLine.h + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_CommandLine.h 28 - - ..\..\tests\Test_ASWUnitTests_GUI_Layout.h + + ..\src\ASWUnitTests_GUI_VCLSystem.h + 46 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Commands.h + 53 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_INIDocument.h + 49 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Layout.h 34 - - ..\..\tests\Test_ASWUnitTests_GUI_RunObserver.h + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_RunObserver.h 29 - - ..\..\tests\Test_ASWUnitTests_GUI_Selection.h + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Selection.h 36 - - ..\..\tests\Test_ASWUnitTests_GUI_Shuffle.h + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Shuffle.h 41 ..\..\tests\Test_ASWUnitTests_GUI_Strings.h 30 - - ..\..\tests\Test_ASWUnitTests_GUI_TestList.h + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_TestList.h 31 + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Theme.h + 59 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Tree.h + 51 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_View.h + 55 + + + ..\..\tests\Test_ASWUnitTests_GUI_VCLSystem.h + 47 + ..\..\tests\Test_ASWUnitTests_RTLExceptions.h 32 diff --git a/vcl/gui/rad370/ASWUnitTests_VCL_GUI.cpp b/vcl/gui/rad370/ASWUnitTests_VCL_GUI.cpp index a55a345..880d0e9 100644 --- a/vcl/gui/rad370/ASWUnitTests_VCL_GUI.cpp +++ b/vcl/gui/rad370/ASWUnitTests_VCL_GUI.cpp @@ -27,9 +27,12 @@ limitations under the License. //--------------------------------------------------------------------------- #include #include +#include //--------------------------------------------------------------------------- #include "ASWUnitTests_GUI_CommandLine.h" #include "ASWUnitTests_GUI_TextDialog.h" +#include "ASWUnitTests_GUI_Theme.h" +#include "ASWUnitTests_GUI_VCLSystem.h" //--------------------------------------------------------------------------- USEFORM("..\src\ASWUnitTests_GUI_MainForm.cpp", FormASWUnitTestsGUIMain); //--------------------------------------------------------------------------- @@ -49,7 +52,21 @@ int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int) // and the process exits with the same code the console runner would use, without opening the GUI. ASWUnitTests::TGUIOptions options; ASWUnitTests::TGUICommandLineResult const commandLine = - ASWUnitTests::TGUICommandLine::Parse(ASWUnitTests::TGUICommandLine::ProcessArguments(), options); + ASWUnitTests::TGUICommandLine::Parse(ASWUnitTests::ProcessArguments(), options); + + // Before any form exists, so each one, the --help text's included, is created in its style rather than + // restyled. --layout-reset's deletion of the layout file, which also holds the theme and the dark theme, + // comes later. + bool const savedTheme = !options.LayoutIgnore && !options.LayoutReset; + System::UnicodeString const layoutPath = ASWUnitTests::GUILayoutFilePath(); + ASWUnitTests::EGUITheme const theme = savedTheme ? ASWUnitTests::LoadGUIThemeFromLayoutFile(layoutPath) : + ASWUnitTests::EGUITheme::Windows; + std::string const darkTheme = savedTheme ? ASWUnitTests::LoadGUIDarkThemeFromLayoutFile(layoutPath) : + ASWUnitTests::GUIDefaultDarkThemeName(); + std::vector themeProblems; + System::UnicodeString const darkStyle = ASWUnitTests::LoadGUIDarkStyle(ASWUnitTests::GUIThemesFolderPath(), + darkTheme, themeProblems); + ASWUnitTests::ApplyGUITheme(theme, darkStyle); if (commandLine.EarlyExitCode.has_value()) { @@ -58,7 +75,7 @@ int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int) } Application->CreateForm(__classid(TFormASWUnitTestsGUIMain), &FormASWUnitTestsGUIMain); - FormASWUnitTestsGUIMain->Start(options); + FormASWUnitTestsGUIMain->Start(options, theme, darkTheme, darkStyle, themeProblems); Application->Run(); // The form outlives Run(); Application frees it later, during shutdown. diff --git a/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp b/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp index bbdf32c..a4b4b85 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp +++ b/vcl/gui/src/ASWUnitTests_GUI_MainForm.cpp @@ -29,28 +29,33 @@ limitations under the License. #pragma resource "*.dfm" //--------------------------------------------------------------------------- #include +#include #include +#include #include #include -#include #include -#include +#include #include #include #include //--------------------------------------------------------------------------- -#include #include #include +#include //--------------------------------------------------------------------------- #include "ASWUnitTests_CLI.h" #include "ASWUnitTests_Exception.h" #include "ASWUnitTests_JUnitReport.h" //--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_Commands.h" #include "ASWUnitTests_GUI_Selection.h" #include "ASWUnitTests_GUI_ShuffleSeedDialog.h" #include "ASWUnitTests_GUI_Strings.h" #include "ASWUnitTests_GUI_TextDialog.h" +#include "ASWUnitTests_GUI_Tree.h" +#include "ASWUnitTests_GUI_VCLSystem.h" +#include "ASWUnitTests_GUI_View.h" //--------------------------------------------------------------------------- namespace @@ -58,35 +63,31 @@ namespace using namespace ASWUnitTests; -TColor const FailureColor = static_cast(RGB(200, 0, 0)); -TColor const SkipColor = static_cast(RGB(190, 120, 0)); - -// What a test tree node stands for. A node's Data holds its kind and an index (see NodeData()): a suite -// node's into TGUITestList::SuitePaths(), a group node's into its GroupNames(), and a test node's into the -// list itself. -enum class TNodeKind -{ - Suite, - Group, - Test, -}; +//--------------------------------------------------------------------------- TTreeNode* AddTreeNode(TTreeNodes* nodes, TTreeNode* parent, std::string const& text, void* data); size_t DataToIndex(void* data); -std::string FirstLine(std::string const& text); -std::string FormatSeconds(double seconds); +TColor FailureColor(); void* IndexToData(size_t index); +bool IsDarkBackground(); +bool IsThemeSettingChange(WPARAM wParam, LPARAM lParam); TColor LogLineColor(std::string const& line); -std::vector MonitorWorkAreas(); -void* NodeData(TNodeKind kind, size_t index); -size_t NodeIndex(void* data); -TNodeKind NodeKind(void* data); +TColor LogTextColor(); +std::vector MonitorWorkAreas(); +void* NodeData(TGUINodeRef const& ref); +TGUINodeRef NodeRef(void* data); +TColor SkipColor(); +TGUIRect ToGUIRect(TRect const& rect); TNodeCheckState ToNodeCheckState(TGUICheckState state); //--------------------------------------------------------------------------- //--------------------------------------------------------------------------- -// Adds a node as the last child of 'parent', or as the last top-level node when 'parent' is nullptr. +/* + AddTreeNode + + Adds a node as the last child of 'parent', or as the last top-level node when 'parent' is nullptr. +*/ TTreeNode* AddTreeNode(TTreeNodes* nodes, TTreeNode* parent, std::string const& text, void* data) { if (parent == nullptr) @@ -95,81 +96,151 @@ TTreeNode* AddTreeNode(TTreeNodes* nodes, TTreeNode* parent, std::string const& return nodes->AddChildObject(parent, FromUTF8(text), data); } //--------------------------------------------------------------------------- -// A Failures & Skips list item's Data holds its test's index into TGUITestList. +/* + DataToIndex + + A Failures & Skips list item's Data holds its test's index into TGUITestList. +*/ size_t DataToIndex(void* data) { return static_cast(reinterpret_cast(data)); } //--------------------------------------------------------------------------- -std::string FirstLine(std::string const& text) +/* + FailureColor + + The text color of a failure in the log and the Failures & Skips list, lighter on a dark background. +*/ +TColor FailureColor() { - return text.substr(0, text.find('\n')); + return IsDarkBackground() ? static_cast(RGB(255, 100, 100)) : static_cast(RGB(200, 0, 0)); } //--------------------------------------------------------------------------- -// e.g. "0.84 s" -std::string FormatSeconds(double seconds) +void* IndexToData(size_t index) { - std::ostringstream text; - text << std::fixed << std::setprecision(2) << seconds << " s"; - return text.str(); + return reinterpret_cast(static_cast(index)); } //--------------------------------------------------------------------------- -void* IndexToData(size_t index) +/* + IsDarkBackground + + Whether the active style's window color is dark, by its luma, so text colors that read well on white need + lighter variants. Also true for a dark high contrast theme, whose window color the unstyled look uses. +*/ +bool IsDarkBackground() { - return reinterpret_cast(static_cast(index)); + int const rgb = ColorToRGB(Vcl::Themes::TStyleManager::ActiveStyle->GetSystemColor(clWindow)); + int const luma = (GetRValue(rgb) * 299 + GetGValue(rgb) * 587 + GetBValue(rgb) * 114) / 1000; + + return luma < 128; +} +//--------------------------------------------------------------------------- +/* + IsThemeSettingChange + + Whether a WM_SETTINGCHANGE message's parameters are about a setting that Use Windows Setting follows: + the app mode, whose change Windows names "ImmersiveColorSet", or high contrast. +*/ +bool IsThemeSettingChange(WPARAM wParam, LPARAM lParam) +{ + if (wParam == SPI_SETHIGHCONTRAST) + return true; + + wchar_t const* const area = reinterpret_cast(lParam); + + return area != nullptr && std::wcscmp(area, L"ImmersiveColorSet") == 0; } //--------------------------------------------------------------------------- -// The Log tab's color for one line of the framework's log output: its failure lines (a failed test or -// check, an unhandled exception, a fatal SetUp_Test()/TearDown_Test() error, a group whose constructor -// or SetUp_Group() threw, or a test that timed out or crashed) and its skip lines stand out. +/* + LogLineColor + + The Log tab's color for one line of the framework's log output; see ClassifyGUILogLine(). +*/ TColor LogLineColor(std::string const& line) { - if (line.find("***Test failed") != std::string::npos || line.find("**Check failed") != std::string::npos || - line.find("\" - failed (") != std::string::npos || line.find("Unhandled exception") != std::string::npos || - line.find("!!FATAL ERROR!!") != std::string::npos || line.find("!!ERROR!!") != std::string::npos || - line.find("!!TIMEOUT!!") != std::string::npos || line.find("!!CRASH!!") != std::string::npos) - return FailureColor; + switch (ClassifyGUILogLine(line)) + { + case TGUILogLineKind::Failure: + return FailureColor(); - if (line.find("***Test skipped") != std::string::npos || line.find("\" - skipped (") != std::string::npos) - return SkipColor; + case TGUILogLineKind::Skip: + return SkipColor(); - return clWindowText; + case TGUILogLineKind::Normal: + break; + } + + return LogTextColor(); } //--------------------------------------------------------------------------- -// Every monitor's work area (the part the taskbar and other docked bars leave free), in screen pixels. -std::vector MonitorWorkAreas() +/* + LogTextColor + + The Log tab's color for a line that isn't a failure or a skip: the active style's text color. Unstyled, + that's clWindowText, which the rich edit shows in its automatic color (see TTextAttributes::SetColor()). +*/ +TColor LogTextColor() { - std::vector workAreas; + return Vcl::Themes::TStyleManager::ActiveStyle->GetSystemColor(clWindowText); +} +//--------------------------------------------------------------------------- +/* + MonitorWorkAreas + + Every monitor's work area (the part the taskbar and other docked bars leave free), in screen pixels. +*/ +std::vector MonitorWorkAreas() +{ + std::vector workAreas; for (int i = 0; i < Screen->MonitorCount; ++i) - workAreas.push_back(Screen->Monitors[i]->WorkareaRect); + workAreas.push_back(ToGUIRect(Screen->Monitors[i]->WorkareaRect)); return workAreas; } //--------------------------------------------------------------------------- -// A test tree node's Data, from its kind and index (see TNodeKind). The kind takes the low two bits. -void* NodeData(TNodeKind kind, size_t index) +/* + NodeData + + A test tree node's Data: which suite, group or test it stands for, encoded by EncodeGUINodeRef(). +*/ +void* NodeData(TGUINodeRef const& ref) { - return reinterpret_cast(static_cast(index) * 4 + static_cast(kind)); + return reinterpret_cast(EncodeGUINodeRef(ref)); } //--------------------------------------------------------------------------- -size_t NodeIndex(void* data) +TGUINodeRef NodeRef(void* data) { - return static_cast(reinterpret_cast(data) / 4); + return DecodeGUINodeRef(reinterpret_cast(data)); } //--------------------------------------------------------------------------- -TNodeKind NodeKind(void* data) +/* + SkipColor + + The text color of a skip in the log and the Failures & Skips list, lighter on a dark background. +*/ +TColor SkipColor() +{ + return IsDarkBackground() ? static_cast(RGB(255, 190, 70)) : static_cast(RGB(190, 120, 0)); +} +//--------------------------------------------------------------------------- +TGUIRect ToGUIRect(TRect const& rect) { - return static_cast(reinterpret_cast(data) % 4); + return TGUIRect(rect.Left, rect.Top, rect.Right, rect.Bottom); } //--------------------------------------------------------------------------- TNodeCheckState ToNodeCheckState(TGUICheckState state) { switch (state) { - case TGUICheckState::Checked: return ncsChecked; - case TGUICheckState::Partial: return ncsPartial; - case TGUICheckState::Unchecked: return ncsUnchecked; + case TGUICheckState::Checked: + return ncsChecked; + + case TGUICheckState::Partial: + return ncsPartial; + + case TGUICheckState::Unchecked: + return ncsUnchecked; } return ncsUnchecked; @@ -237,6 +308,7 @@ class TTreeUpdateScope } // namespace +//--------------------------------------------------------------------------- ///////////////////////////////////////////////////////////////////////////// // TFormASWUnitTestsGUIMain @@ -254,10 +326,21 @@ __fastcall TFormASWUnitTestsGUIMain::TFormASWUnitTestsGUIMain(TComponent* Owner) m_RunFinishedCount(0), m_Running(false), m_RunTotalCount(0), + m_SettingFilterText(false), m_StatusMessage("Ready."), m_SyncingChecks(false), + m_Theme(EGUITheme::Windows), + m_ThemeChangePending(false), m_TreeNodesStale(false) { + m_FormWindowProc = WindowProc; + WindowProc = FormWindowProc; + + // A VCL style's rich edit hook gives every color set in the log's text the style's text color instead, so + // failures and skips would lose theirs. Without seFont it leaves them, and LogLineColor() chooses colors + // that suit the style. + RE_Log->StyleElements = RE_Log->StyleElements >> seFont; + m_TreeViewWindowProc = TV_Tests->WindowProc; TV_Tests->WindowProc = TV_TestsWindowProc; } @@ -268,10 +351,11 @@ __fastcall TFormASWUnitTestsGUIMain::TFormASWUnitTestsGUIMain(TComponent* Owner) This form's own members (m_TestList, m_TestNodes, and so on) are destroyed before the VCL base class destroys its controls, and destroying a control can fire its events. So every event that reads those members is detached first, along with anything still queued for this form, so none can run against - members that no longer exist. The tree's own WindowProc is restored for the same reason. + members that no longer exist. The form's and the tree's own WindowProc are restored for the same reason. */ __fastcall TFormASWUnitTestsGUIMain::~TFormASWUnitTestsGUIMain() { + WindowProc = m_FormWindowProc; TV_Tests->WindowProc = m_TreeViewWindowProc; Edt_Filter->OnChange = nullptr; @@ -287,21 +371,7 @@ void __fastcall TFormASWUnitTestsGUIMain::Act_AboutExecute(TObject* /*Sender*/) { RunGuarded("About", []() { - ShowTextDialog(System::UnicodeString("About ") + Application->Title, - TTestHandler::GetVersionFullStr() + "\n" - "VCL GUI runner\n" - "\n" - "App bits: " -#if defined (_WIN64) - "64" -#else - "32" -#endif - "\n\n" - "https://github.com/ASWSoftware/asw-unit-tests\n" - "\n" - "Copyright 2026 ASW Software\n" - "Licensed under the Apache License, Version 2.0.\n"); + ShowTextDialog(System::UnicodeString("About ") + Application->Title, GUIAboutText("VCL GUI runner")); }); } //--------------------------------------------------------------------------- @@ -382,8 +452,8 @@ void __fastcall TFormASWUnitTestsGUIMain::Act_ExportJUnitReportExecute(TObject* else if (SD_JUnitReport->FileName.IsEmpty()) { // e.g. "ASWUnitTests_VCL_GUI_JUnit.xml", in the dialog's default folder. - SD_JUnitReport->FileName = System::Sysutils::ChangeFileExt( - System::Sysutils::ExtractFileName(System::ParamStr(0)), "_JUnit.xml"); + SD_JUnitReport->FileName = FromUTF8(GUIDefaultJUnitReportFileName( + ToUTF8(System::Sysutils::ExtractFileName(System::ParamStr(0))))); } if (!SD_JUnitReport->Execute(Handle)) @@ -415,6 +485,16 @@ void __fastcall TFormASWUnitTestsGUIMain::Act_FocusFilterExecute(TObject* /*Send }); } //--------------------------------------------------------------------------- +void __fastcall TFormASWUnitTestsGUIMain::Act_OpenThemesFolderExecute(TObject* /*Sender*/) +{ + RunGuarded("Theme > Dark Theme > Open Themes Folder", []() + { + System::UnicodeString const folder = GUIThemesFolderPath(); + System::Sysutils::ForceDirectories(folder); + OpenFolderInExplorer(folder); + }); +} +//--------------------------------------------------------------------------- void __fastcall TFormASWUnitTestsGUIMain::Act_ResetLayoutExecute(TObject* /*Sender*/) { RunGuarded("Reset Layout", [this]() @@ -514,6 +594,30 @@ void __fastcall TFormASWUnitTestsGUIMain::Act_StopExecute(TObject* /*Sender*/) }); } //--------------------------------------------------------------------------- +void __fastcall TFormASWUnitTestsGUIMain::Act_ThemeDarkExecute(TObject* /*Sender*/) +{ + RunGuarded("Theme > Dark", [this]() + { + SetTheme(EGUITheme::Dark); + }); +} +//--------------------------------------------------------------------------- +void __fastcall TFormASWUnitTestsGUIMain::Act_ThemeLightExecute(TObject* /*Sender*/) +{ + RunGuarded("Theme > Light", [this]() + { + SetTheme(EGUITheme::Light); + }); +} +//--------------------------------------------------------------------------- +void __fastcall TFormASWUnitTestsGUIMain::Act_ThemeWindowsExecute(TObject* /*Sender*/) +{ + RunGuarded("Theme > Use Windows Setting", [this]() + { + SetTheme(EGUITheme::Windows); + }); +} +//--------------------------------------------------------------------------- void TFormASWUnitTestsGUIMain::AddFailureItem(size_t testIndex) { TTestId const& test = m_TestList.Test(testIndex); @@ -522,7 +626,7 @@ void TFormASWUnitTestsGUIMain::AddFailureItem(size_t testIndex) TListItem* const item = LV_Failures->Items->Add(); item->Caption = FromUTF8(StatusName(m_TestList.Status(testIndex))); item->SubItems->Add(FromUTF8(test.GroupName + "." + test.TestName)); - item->SubItems->Add(FromUTF8(result != nullptr ? FirstLine(result->Record.Message) : std::string())); + item->SubItems->Add(FromUTF8(result != nullptr ? GUIFirstLine(result->Record.Message) : std::string())); item->Data = IndexToData(testIndex); } //--------------------------------------------------------------------------- @@ -553,26 +657,11 @@ void TFormASWUnitTestsGUIMain::AppendLog(std::string const& text) /* TFormASWUnitTestsGUIMain::ApplyFilterBox - The text matches anywhere in a test's "Group.Test" full name, ignoring case, and may use the same '*' - and '?' wildcards as --filter. Unlike --filter, it's never anchored to the whole name, so "Parse?rg" - finds "...ParseArguments...", just as plain text would. + The text matches as GUIFilterBoxFilter() describes. */ void TFormASWUnitTestsGUIMain::ApplyFilterBox() { - std::string const text = ToUTF8(Edt_Filter->Text.Trim()); - - if (text.empty()) - { - m_TestList.SetVisibleFilter(TestFilter()); - } - else - { - std::string const pattern = "*" + text + "*"; - m_TestList.SetVisibleFilter([pattern](std::string const& fullTestName) - { - return TTestHandler::WildcardMatch(pattern, fullTestName, true); - }); - } + m_TestList.SetVisibleFilter(GUIFilterBoxFilter(ToUTF8(Edt_Filter->Text))); BuildTestTree(); UpdateDetail(); @@ -597,6 +686,33 @@ void TFormASWUnitTestsGUIMain::ApplyPanelSizes(TGUILayout const& layout) } } //--------------------------------------------------------------------------- +/* + TFormASWUnitTestsGUIMain::ApplyTheme + + Queued rather than at once: under a VCL style, a menu item's click comes from the form's style hook, which a + style change frees, and which uses itself again once the click returns (an access violation in + TFormStyleHook::TMainMenuBarStyleHook::Invalidate(), switching dark themes with the mouse). Not during a run, + since restyling recreates the windows the run is updating; SetRunning() calls it again once the run has + ended. The window is restyled once it handles the message the style change posts; see FormWindowProc(). +*/ +void TFormASWUnitTestsGUIMain::ApplyTheme() +{ + TThread::ForceQueue(nullptr, [this]() + { + RunGuarded("changing the theme", [this]() + { + if (m_Running) + { + m_ThemeChangePending = true; + return; + } + + m_ThemeChangePending = false; + ApplyGUITheme(m_Theme, m_DarkStyle); + }); + }); +} +//--------------------------------------------------------------------------- /* TFormASWUnitTestsGUIMain::BuildTestTree @@ -612,44 +728,18 @@ void TFormASWUnitTestsGUIMain::BuildTestTree() TV_Tests->Items->Clear(); m_TestNodes.assign(m_TestList.Count(), nullptr); - // By index into m_TestList.SuitePaths(). A suite's node, and those of the suites holding it, are added - // just before its first group's, so suites and groups keep m_TestList's order, where each suite's - // groups are together. - std::vector suiteNodes(m_TestList.SuitePaths().size(), nullptr); - std::function suiteNode = [&](std::string const& suitePath) -> TTreeNode* - { - if (suitePath.empty()) - return nullptr; - - size_t const suiteIndex = m_TestList.SuiteIndexOf(suitePath).value(); + // Each node comes after its parent, so its parent's tree node already exists. + std::vector const nodes = BuildGUITree(m_TestList); + std::vector treeNodes; + treeNodes.reserve(nodes.size()); - if (suiteNodes[suiteIndex] == nullptr) - { - std::string const parentPath = ParentSuitePath(suitePath); - suiteNodes[suiteIndex] = AddTreeNode(TV_Tests->Items, suiteNode(parentPath), - NameInSuite(suitePath, parentPath), NodeData(TNodeKind::Suite, suiteIndex)); - } - - return suiteNodes[suiteIndex]; - }; - - // Only visible tests get nodes, and a group or suite only gets one if any of its tests are visible. - std::vector const& groupNames = m_TestList.GroupNames(); - for (size_t groupIndex = 0; groupIndex < groupNames.size(); ++groupIndex) + for (TGUITreeNode const& node : nodes) { - std::vector const testIndexes = m_TestList.TestsInGroup(groupNames[groupIndex]); - if (testIndexes.empty()) - continue; + TTreeNode* const parent = node.Parent.has_value() ? treeNodes[*node.Parent] : nullptr; + treeNodes.push_back(AddTreeNode(TV_Tests->Items, parent, node.Text, NodeData(node.Ref))); - std::string const suitePath = m_TestList.GroupSuitePath(groupNames[groupIndex]); - TTreeNode* const groupNode = AddTreeNode(TV_Tests->Items, suiteNode(suitePath), - NameInSuite(groupNames[groupIndex], suitePath), NodeData(TNodeKind::Group, groupIndex)); - - for (size_t testIndex : testIndexes) - { - m_TestNodes[testIndex] = AddTreeNode(TV_Tests->Items, groupNode, m_TestList.Test(testIndex).TestName, - NodeData(TNodeKind::Test, testIndex)); - } + if (node.Ref.Kind == TGUINodeKind::Test) + m_TestNodes[node.Ref.Index] = treeNodes.back(); } // The nodes just added are the tree's current ones, even if its window was recreated before this. @@ -661,9 +751,8 @@ void TFormASWUnitTestsGUIMain::BuildTestTree() RefreshAllNodeImages(); SyncTreeChecks(); - // e.g. while the filter hides every test. - Act_CollapseAll->Enabled = (TV_Tests->Items->Count > 0); - Act_ExpandAll->Enabled = (TV_Tests->Items->Count > 0); + // Expand All and Collapse All, e.g. while the filter hides every test. + UpdateCommands(); if (TV_Tests->Items->Count > 0) TV_Tests->Items->GetFirstNode()->MakeVisible(); @@ -732,8 +821,8 @@ TGUILayout TFormASWUnitTestsGUIMain::CurrentLayout() if (::GetWindowPlacement(Handle, &placement)) { - TRect const bounds = WorkspaceToScreen(TRect(placement.rcNormalPosition), Screen->PrimaryMonitor->BoundsRect, - Screen->PrimaryMonitor->WorkareaRect); + TGUIRect const bounds = WorkspaceToScreen(ToGUIRect(TRect(placement.rcNormalPosition)), + ToGUIRect(Screen->PrimaryMonitor->BoundsRect), ToGUIRect(Screen->PrimaryMonitor->WorkareaRect)); TGUIWindowBounds window; window.Height = ScaleTo96(bounds.Height(), pixelsPerInch); @@ -751,12 +840,36 @@ TGUILayout TFormASWUnitTestsGUIMain::CurrentLayout() //--------------------------------------------------------------------------- void __fastcall TFormASWUnitTestsGUIMain::Edt_FilterChange(TObject* /*Sender*/) { + if (m_SettingFilterText) + return; // Start() builds the tree once it has set the text. + RunGuarded("the filter box", [this]() { ApplyFilterBox(); }); } //--------------------------------------------------------------------------- +/* + TFormASWUnitTestsGUIMain::DarkThemeItemClick + + Not while the Theme actions are disabled, during a run, as for them. +*/ +void __fastcall TFormASWUnitTestsGUIMain::DarkThemeItemClick(TObject* Sender) +{ + RunGuarded("Theme > Dark Theme", [this, Sender]() + { + TMenuItem const* const item = dynamic_cast(Sender); + + if (item == nullptr || !Act_ThemeDark->Enabled || item->Tag < 0 || + static_cast(item->Tag) >= m_DarkThemeNames.size()) + { + return; + } + + SetDarkTheme(m_DarkThemeNames[static_cast(item->Tag)]); + }); +} +//--------------------------------------------------------------------------- int TFormASWUnitTestsGUIMain::ExitCode() const { return m_Options.ExitAfterRun ? m_LastRunExitCode : ExitCode_Success; @@ -769,21 +882,81 @@ void TFormASWUnitTestsGUIMain::FailRunOnUnhandledException(std::string const& de // The test that was running when the exception escaped never finished, so it's the one that failed. if (m_CurrentTestIndex.has_value()) { - TTestId const& test = m_TestList.Test(*m_CurrentTestIndex); - m_TestList.SetResult(*m_CurrentTestIndex, TTestCaseRecord{ test.GroupName, test.TestName, 0.0, - TTestOutcome::Fail, "Unhandled exception: " + description, - test.SuitePath }, std::string()); + m_TestList.SetResult(*m_CurrentTestIndex, + GUIUnhandledExceptionRecord(m_TestList.Test(*m_CurrentTestIndex), description), std::string()); UpdateTestNodeImages(*m_CurrentTestIndex); AddFailureItem(*m_CurrentTestIndex); m_CurrentTestIndex.reset(); } PB_Progress->State = pbsError; - m_StatusMessage = "The run was ended by an unhandled exception: " + description; + m_StatusMessage = GUIUnhandledExceptionStatus(description); AppendLog(m_StatusMessage + "\n"); UpdateDetail(); } //--------------------------------------------------------------------------- +/* + TFormASWUnitTestsGUIMain::FillDarkThemeMenu + + The built-in themes first (see BuiltInGUIDarkThemeNames()), Carbon at the top, then a line and the VCL style + files in GUIThemesFolderPath() (see GUIThemeNames()), so a file added or removed since shows at once, above + Open Themes Folder; the chosen one is checked. A chosen theme whose file is missing is still listed, so + choosing it again reports why it isn't used. +*/ +void TFormASWUnitTestsGUIMain::FillDarkThemeMenu() +{ + for (TMenuItem* const item : m_DarkThemeItems) + delete item; + + m_DarkThemeItems.clear(); + + std::vector names = BuiltInGUIDarkThemeNames(); + size_t const builtInCount = names.size(); + std::vector const files = GUIThemeNames(FileNamesInFolder(GUIThemesFolderPath()), ".vsf", names); + names.insert(names.end(), files.begin(), files.end()); + + // Windows' file names ignore case, so a name saved in another case is still that file's. + std::vector::const_iterator chosen = std::find_if(names.begin(), names.end(), + [this](std::string const& name) + { + return System::Sysutils::AnsiSameText(FromUTF8(name), FromUTF8(m_DarkTheme)); + }); + + if (chosen == names.end()) + chosen = names.insert(names.end(), m_DarkTheme); + + size_t const chosenIndex = static_cast(chosen - names.begin()); + // A built-in theme's accelerator, other than Open Themes Folder's O; a style file's name has none. + std::vector const builtInCaptions = GUIMenuItemTextsWithAccelerators( + std::vector(names.begin(), names.begin() + static_cast(builtInCount)), "o"); + int position = 0; + + for (size_t i = 0; i < names.size(); ++i) + { + // A line between the built-in themes and the files. + if (i == builtInCount) + { + TMenuItem* const line = new TMenuItem(this); + m_DarkThemeItems.push_back(line); + line->Caption = "-"; + MI_ViewDarkTheme->Insert(position++, line); + } + + TMenuItem* const item = new TMenuItem(this); + m_DarkThemeItems.push_back(item); + + item->Caption = FromUTF8(i < builtInCount ? builtInCaptions[i] : GUIMenuItemText(names[i])); + item->Checked = i == chosenIndex; + item->Enabled = Act_ThemeDark->Enabled; + item->OnClick = DarkThemeItemClick; + item->RadioItem = true; + item->Tag = static_cast(i); + MI_ViewDarkTheme->Insert(position++, item); + } + + m_DarkThemeNames = names; +} +//--------------------------------------------------------------------------- void __fastcall TFormASWUnitTestsGUIMain::FormClose(TObject* /*Sender*/, TCloseAction& /*Action*/) { RunGuarded("closing the window", [this]() @@ -818,6 +991,45 @@ void __fastcall TFormASWUnitTestsGUIMain::FormCloseQuery(TObject* /*Sender*/, bo }); } //--------------------------------------------------------------------------- +/* + TFormASWUnitTestsGUIMain::FormWindowProc + + Follows the theme as it changes: + - TStyleManager posts CM_CUSTOMSTYLECHANGED to each visible form when the style changes, and the form + handles it by recreating its windows in the new style. Its log text and tree nodes are kept, but the + log's lines keep the colors chosen for the old style, so they're colored again, and the tree's check + boxes and status images are set again. VCL changes the message to CM_STYLECHANGED as it handles it, so + it's noted first. + - With Use Windows Setting, a WM_SETTINGCHANGE about the app mode or high contrast applies the theme + again. +*/ +void __fastcall TFormASWUnitTestsGUIMain::FormWindowProc(TMessage& message) +{ + bool const styleChanged = (message.Msg == CM_CUSTOMSTYLECHANGED); + bool const settingChanged = (message.Msg == WM_SETTINGCHANGE) && + IsThemeSettingChange(message.WParam, message.LParam); + + m_FormWindowProc(message); + + if (styleChanged) + { + RunGuarded("changing the theme", [this]() + { + RecolorLog(); + SyncTreeChecks(); + RefreshAllNodeImages(); + LV_Failures->Invalidate(); + }); + } + else if (settingChanged && m_Theme == EGUITheme::Windows) + { + RunGuarded("following Windows' theme setting", [this]() + { + ApplyTheme(); + }); + } +} +//--------------------------------------------------------------------------- /* TFormASWUnitTestsGUIMain::LoadLayout @@ -834,8 +1046,7 @@ void TFormASWUnitTestsGUIMain::LoadLayout() if (!System::Sysutils::FileExists(path)) return; - std::unique_ptr const ini(new TMemIniFile(path)); - layout = LoadGUILayout(*ini); + layout = LoadGUILayout(TGUIINIDocument(ReadFileBytes(path))); } catch (...) { @@ -845,7 +1056,7 @@ void TFormASWUnitTestsGUIMain::LoadLayout() if (layout.Window.has_value()) { TGUIWindowBounds const& window = *layout.Window; - TRect const bounds(window.Left, window.Top, window.Left + ScaleFrom96(window.Width, CurrentPPI), + TGUIRect const bounds(window.Left, window.Top, window.Left + ScaleFrom96(window.Width, CurrentPPI), window.Top + ScaleFrom96(window.Height, CurrentPPI)); // Not where it can't be reached, e.g. on a monitor that's since been disconnected. Set as the window @@ -856,8 +1067,8 @@ void TFormASWUnitTestsGUIMain::LoadLayout() if (IsWindowReachable(bounds, MonitorWorkAreas()) && ::GetWindowPlacement(Handle, &placement)) { - TRect const normal = ScreenToWorkspace(bounds, Screen->PrimaryMonitor->BoundsRect, - Screen->PrimaryMonitor->WorkareaRect); + TGUIRect const normal = ScreenToWorkspace(bounds, ToGUIRect(Screen->PrimaryMonitor->BoundsRect), + ToGUIRect(Screen->PrimaryMonitor->WorkareaRect)); placement.rcNormalPosition.left = normal.Left; placement.rcNormalPosition.top = normal.Top; placement.rcNormalPosition.right = normal.Right; @@ -901,7 +1112,7 @@ void TFormASWUnitTestsGUIMain::LoadSelection() } //--------------------------------------------------------------------------- void __fastcall TFormASWUnitTestsGUIMain::LV_FailuresCustomDrawItem(TCustomListView* Sender, TListItem* Item, - TCustomDrawState /*State*/, bool& /*DefaultDraw*/) + TCustomDrawState State, bool& /*DefaultDraw*/) { // Not RunGuarded(): this runs while painting, where showing an error would only cause more painting. // An item it can't match to a test is simply drawn in the default color. @@ -910,7 +1121,12 @@ void __fastcall TFormASWUnitTestsGUIMain::LV_FailuresCustomDrawItem(TCustomListV return; bool const failed = m_TestList.Status(index) == TGUITestStatus::Failed; - Sender->Canvas->Font->Color = failed ? FailureColor : SkipColor; + Sender->Canvas->Font->Color = failed ? FailureColor() : SkipColor(); + + // Otherwise a styled list view paints an item that's neither selected nor hot in the canvas's default, + // light, color. + if (!State.Contains(cdsSelected) && !State.Contains(cdsHot)) + Sender->Canvas->Brush->Color = Vcl::Themes::TStyleManager::ActiveStyle->GetSystemColor(clWindow); } //--------------------------------------------------------------------------- /* @@ -944,6 +1160,19 @@ void __fastcall TFormASWUnitTestsGUIMain::LV_FailuresDblClick(TObject* /*Sender* }); } //--------------------------------------------------------------------------- +/* + TFormASWUnitTestsGUIMain::MI_ViewDarkThemeClick + + The VCL calls a parent item's OnClick as its submenu opens. +*/ +void __fastcall TFormASWUnitTestsGUIMain::MI_ViewDarkThemeClick(TObject* /*Sender*/) +{ + RunGuarded("the Dark Theme menu", [this]() + { + FillDarkThemeMenu(); + }); +} +//--------------------------------------------------------------------------- void TFormASWUnitTestsGUIMain::OnTestFinished(TTestCaseRecord const& record, std::string const& testLog) { std::optional const index = m_TestList.IndexOf(record.GroupName, record.TestName); @@ -958,7 +1187,7 @@ void TFormASWUnitTestsGUIMain::OnTestFinished(TTestCaseRecord const& record, std m_CurrentTestIndex.reset(); ++m_RunFinishedCount; - PB_Progress->Position = static_cast(m_RunFinishedCount); + SetProgressPosition(static_cast(m_RunFinishedCount)); if (record.Outcome == TTestOutcome::Fail) PB_Progress->State = pbsError; @@ -994,15 +1223,60 @@ void TFormASWUnitTestsGUIMain::RebuildNodeCache() for (TTreeNode* node = TV_Tests->Items->GetFirstNode(); node != nullptr; node = node->GetNext()) { - size_t const index = NodeIndex(node->Data); + TGUINodeRef const ref = NodeRef(node->Data); - if (NodeKind(node->Data) == TNodeKind::Test && index < m_TestNodes.size()) - m_TestNodes[index] = node; + if (ref.Kind == TGUINodeKind::Test && ref.Index < m_TestNodes.size()) + m_TestNodes[ref.Index] = node; } m_TreeNodesStale = false; } //--------------------------------------------------------------------------- +/* + TFormASWUnitTestsGUIMain::RecolorLog + + AppendLog() colored each line for the style active then. Every line is given the text color first, in one + step, then each failure and skip line its own color. Lines are found by their index, which counts a line + break as one character, as SelStart does. +*/ +void TFormASWUnitTestsGUIMain::RecolorLog() +{ + struct TEndUpdate + { + TStrings* const Lines; + + ~TEndUpdate() + { + Lines->EndUpdate(); + } + }; + + int const selStart = RE_Log->SelStart; + int const selLength = RE_Log->SelLength; + + RE_Log->Lines->BeginUpdate(); + TEndUpdate const endUpdate{ RE_Log->Lines }; + + RE_Log->SelectAll(); + RE_Log->SelAttributes->Color = LogTextColor(); + + for (int i = 0; i < RE_Log->Lines->Count; ++i) + { + System::UnicodeString const text = RE_Log->Lines->Strings[i]; + std::string const line = ToUTF8(text); + + if (ClassifyGUILogLine(line) == TGUILogLineKind::Normal) + continue; + + RE_Log->SelStart = static_cast(::SendMessage(RE_Log->Handle, EM_LINEINDEX, static_cast(i), 0)); + RE_Log->SelLength = text.Length(); + RE_Log->SelAttributes->Color = LogLineColor(line); + } + + RE_Log->SelStart = selStart; + RE_Log->SelLength = selLength; +} +//--------------------------------------------------------------------------- void TFormASWUnitTestsGUIMain::RefreshAllNodeImages() { TTreeUpdateScope const updating(TV_Tests->Items); @@ -1113,9 +1387,11 @@ void TFormASWUnitTestsGUIMain::RunTests(TestFilter const& filter, std::string co m_RunEnd.reset(); m_LastRunJUnitTestCases.reset(); - PB_Progress->Max = static_cast(std::max(m_RunTotalCount, 1)); - PB_Progress->Position = 0; + // Back to the normal state first, since the error state wouldn't draw the new position; see + // SetProgressPosition(). PB_Progress->State = pbsNormal; + PB_Progress->Max = static_cast(std::max(m_RunTotalCount, 1)); + SetProgressPosition(0); m_Observer.ResetStop(); m_StatusMessage = "Running..."; @@ -1128,20 +1404,11 @@ void TFormASWUnitTestsGUIMain::RunTests(TestFilter const& filter, std::string co m_Options.CLI.TestTimeoutSeconds, m_Options.CLI.CatchCrashes); m_Observer.Flush(); - // A timed-out test's abandoned thread may still be running one of the groups' tests, and a crash may have - // left their state corrupted, so nothing runs again until the GUI is restarted (see WasRunAborted()). - m_RunAborted = results.TimedOut || results.Crashed; - - if (results.Stopped) - m_StatusMessage = "Stopped before every test ran."; - else if (results.TimedOut) - m_StatusMessage = "A test timed out, so the rest of the run was abandoned. Restart to run tests again."; - else if (results.Crashed) - m_StatusMessage = "A test crashed, so the rest of the run was abandoned. Restart to run tests again."; - else - m_StatusMessage = "Done."; - - m_LastRunExitCode = ExitCodeForResults(results); + // After an aborted run, nothing runs again until the GUI is restarted (see WasRunAborted()). + TGUIRunSummary const summary = SummarizeGUIRun(results); + m_RunAborted = summary.Aborted; + m_StatusMessage = summary.StatusMessage; + m_LastRunExitCode = summary.ExitCode; m_LastRunJUnitTestCases = ToJUnitTestCases(results.CaseRecords); // A failure is only noted in the log, so it can't hold up --exit. @@ -1190,9 +1457,10 @@ void TFormASWUnitTestsGUIMain::SaveLayout() System::UnicodeString const path = GUILayoutFilePath(); System::Sysutils::ForceDirectories(System::Sysutils::ExtractFileDir(path)); - std::unique_ptr const ini(new TMemIniFile(path)); - SaveGUILayout(*ini, CurrentLayout()); - ini->UpdateFile(); + // Starting from the file's current text keeps any other values in it. + TGUIINIDocument ini(System::Sysutils::FileExists(path) ? ReadFileBytes(path) : std::string()); + SaveGUILayout(ini, CurrentLayout()); + WriteFileBytes(path, ini.Text()); } catch (...) { @@ -1216,58 +1484,84 @@ void TFormASWUnitTestsGUIMain::SaveSelection() } } //--------------------------------------------------------------------------- -void TFormASWUnitTestsGUIMain::SetNodeImage(TTreeNode* node) +/* + TFormASWUnitTestsGUIMain::SetDarkTheme + + Applied at once while dark; while light, its file is still loaded, so its problems show in the log, and the + status bar says it shows once the theme is dark. A VCL style file already loaded isn't read again (see + LoadGUIDarkStyle()). +*/ +void TFormASWUnitTestsGUIMain::SetDarkTheme(std::string const& name) { - size_t const index = NodeIndex(node->Data); - TGUITestStatus status = TGUITestStatus::NotRun; + m_DarkTheme = name; - switch (NodeKind(node->Data)) - { - case TNodeKind::Suite: - status = m_TestList.SuiteStatus(m_TestList.SuitePaths()[index]); - break; + if (!m_Options.LayoutIgnore) + SaveGUIDarkThemeToLayoutFile(GUILayoutFilePath(), name); - case TNodeKind::Group: - status = m_TestList.GroupStatus(m_TestList.GroupNames()[index]); - break; + std::vector problems; + m_DarkStyle = LoadGUIDarkStyle(GUIThemesFolderPath(), name, problems); + ApplyTheme(); - case TNodeKind::Test: - status = m_TestList.Status(index); - break; - } + if (GUIThemeIsDark(m_Theme, WindowsAppsUseDarkTheme(), WindowsHighContrast())) + m_StatusMessage = "Dark theme: " + name + "."; + else + m_StatusMessage = "Dark theme: " + name + ", shown while the theme is dark."; - int const image = static_cast(status); + for (std::string const& problem : problems) + AppendLog(problem + "\n"); + + UpdateStatusBar(); +} +//--------------------------------------------------------------------------- +void TFormASWUnitTestsGUIMain::SetNodeImage(TTreeNode* node) +{ + int const image = static_cast(GUINodeStatus(m_TestList, NodeRef(node->Data))); node->ImageIndex = image; node->SelectedIndex = image; } //--------------------------------------------------------------------------- +/* + TFormASWUnitTestsGUIMain::SetProgressPosition + + In the error state (red), a themed progress bar takes a new position without drawing it, so it stays where + it was when it turned red, and back in the normal state it then shows the position it last took. Setting the + position a second time draws it. Found by experiment on Windows 10 with Common Controls 6; the VCL passes + Position straight to the control. +*/ +void TFormASWUnitTestsGUIMain::SetProgressPosition(int position) +{ + PB_Progress->Position = position; + + if (PB_Progress->State != pbsNormal) + PB_Progress->Position = position; +} +//--------------------------------------------------------------------------- void TFormASWUnitTestsGUIMain::SetRunning(bool running) { m_Running = running; + UpdateCommands(); + UpdateStatusBar(); - // A modal dialog opened mid-run would hold up the rest of the run until it was closed, since the tests - // run on this thread. - Act_About->Enabled = !running; - Act_CommandLineOptions->Enabled = !running; - Act_ExportJUnitReport->Enabled = !running && m_LastRunJUnitTestCases.has_value(); - - // Nor once a run has been aborted; see RunTests(). - Act_RunFailed->Enabled = !running && !m_RunAborted && m_TestList.StatusCount(TGUITestStatus::Failed) > 0; - Act_RunSelected->Enabled = !running && !m_RunAborted; - Act_SelectAll->Enabled = !running; - Act_SelectFailed->Enabled = !running && m_TestList.StatusCount(TGUITestStatus::Failed) > 0; - Act_SelectNone->Enabled = !running; - Act_Stop->Enabled = running; + // Windows' setting changed during the run; see ApplyTheme(). + if (!running && m_ThemeChangePending) + ApplyTheme(); +} +//--------------------------------------------------------------------------- +/* + TFormASWUnitTestsGUIMain::SetTheme - // The run in progress already has its order, so a change now would only mislead. - Act_ShuffleOrder->Enabled = !running; - Act_ShuffleSeed->Enabled = !running; + Saved at once, rather than with the layout as the window closes, so the choice is kept even if the window + doesn't get to close normally. +*/ +void TFormASWUnitTestsGUIMain::SetTheme(EGUITheme theme) +{ + m_Theme = theme; + ShowThemeState(); - // Rebuilding the tree mid-run would discard the nodes the run is updating. - Act_FocusFilter->Enabled = !running; - Edt_Filter->Enabled = !running; + if (!m_Options.LayoutIgnore) + SaveGUIThemeToLayoutFile(GUILayoutFilePath(), theme); - UpdateStatusBar(); + ApplyTheme(); } //--------------------------------------------------------------------------- void TFormASWUnitTestsGUIMain::ShowShuffleState() @@ -1276,6 +1570,13 @@ void TFormASWUnitTestsGUIMain::ShowShuffleState() UpdateStatusBar(); } //--------------------------------------------------------------------------- +void TFormASWUnitTestsGUIMain::ShowThemeState() +{ + Act_ThemeDark->Checked = (m_Theme == EGUITheme::Dark); + Act_ThemeLight->Checked = (m_Theme == EGUITheme::Light); + Act_ThemeWindows->Checked = (m_Theme == EGUITheme::Windows); +} +//--------------------------------------------------------------------------- /* TFormASWUnitTestsGUIMain::Start @@ -1284,9 +1585,14 @@ void TFormASWUnitTestsGUIMain::ShowShuffleState() and --shuffle and --shuffle-seed to the Options menu. Also restores the window layout saved when it last closed. */ -void TFormASWUnitTestsGUIMain::Start(TGUIOptions const& options) +void TFormASWUnitTestsGUIMain::Start(TGUIOptions const& options, EGUITheme theme, std::string const& darkTheme, + System::UnicodeString const& darkStyle, std::vector const& themeProblems) { m_Options = options; + m_Theme = theme; + m_DarkTheme = darkTheme; + m_DarkStyle = darkStyle; + ShowThemeState(); m_Observer.LogTextCallback = [this](std::string const& text) { @@ -1327,17 +1633,11 @@ void TFormASWUnitTestsGUIMain::Start(TGUIOptions const& options) if (m_KeepSelection) LoadSelection(); - for (std::string const& ignored : m_Options.IgnoredOptions) - AppendLog("Note: " + ignored + " has no effect in the GUI, so it was ignored.\n"); + for (std::string const& note : GUICommandLineNotes(m_Options, "VCL forms or controls")) + AppendLog(note + "\n"); - if (m_Options.CLI.TestTimeoutSeconds.has_value()) - { - AppendLog("Note: --test-timeout-seconds runs each test on a worker thread, so a test that creates VCL " - "forms or controls won't work with it.\n"); - } - - if (m_Options.LayoutIgnore) - AppendLog("Note: --layout-ignore: the window layout isn't loaded or saved, so it opens with the defaults.\n"); + for (std::string const& problem : themeProblems) + AppendLog(problem + "\n"); if (m_Options.LayoutReset) ResetSavedLayout(); @@ -1351,10 +1651,14 @@ void TFormASWUnitTestsGUIMain::Start(TGUIOptions const& options) // The filter box's case-insensitive contains-match shows every test --filter's pattern matches (and // maybe a few more, left unchecked), so seeding it with the pattern never hides a checked test. if (m_Options.CLI.HasFilter) + { + // Without OnChange's rebuild, which may or may not happen before the window is showing, so the tree + // is built once either way, just below. + TScopedFlag const settingFilterText(m_SettingFilterText); Edt_Filter->Text = FromUTF8(m_Options.CLI.FilterPattern); + } - // Called directly, since setting the text above doesn't necessarily raise OnChange before the window - // is showing. Builds the tree. + // Builds the tree. ApplyFilterBox(); // Centered here rather than with the DFM's Position, which is poDesigned because LoadLayout() would @@ -1397,24 +1701,7 @@ void TFormASWUnitTestsGUIMain::SyncTreeChecks() TScopedFlag const syncing(m_SyncingChecks); for (TTreeNode* node = TV_Tests->Items->GetFirstNode(); node != nullptr; node = node->GetNext()) - { - size_t const index = NodeIndex(node->Data); - - switch (NodeKind(node->Data)) - { - case TNodeKind::Suite: - node->CheckState = ToNodeCheckState(m_TestList.SuiteCheckState(m_TestList.SuitePaths()[index])); - break; - - case TNodeKind::Group: - node->CheckState = ToNodeCheckState(m_TestList.GroupCheckState(m_TestList.GroupNames()[index])); - break; - - case TNodeKind::Test: - node->CheckState = m_TestList.IsChecked(index) ? ncsChecked : ncsUnchecked; - break; - } - } + node->CheckState = ToNodeCheckState(GUINodeCheckState(m_TestList, NodeRef(node->Data))); UpdateStatusBar(); // For its checked count. } @@ -1457,23 +1744,7 @@ void __fastcall TFormASWUnitTestsGUIMain::TV_TestsCheckStateChanging(TCustomTree if (m_Running) return; - bool const check = (OldCheckState != ncsChecked); - size_t const index = NodeIndex(Node->Data); - - switch (NodeKind(Node->Data)) - { - case TNodeKind::Suite: - m_TestList.SetSuiteChecked(m_TestList.SuitePaths()[index], check); - break; - - case TNodeKind::Group: - m_TestList.SetGroupChecked(m_TestList.GroupNames()[index], check); - break; - - case TNodeKind::Test: - m_TestList.SetChecked(index, check); - break; - } + SetGUINodeChecked(m_TestList, NodeRef(Node->Data), OldCheckState != ncsChecked); // Changing check boxes from inside the tree's own change notification isn't safe, so the update // waits until this click has been fully handled. @@ -1502,26 +1773,51 @@ void __fastcall TFormASWUnitTestsGUIMain::TV_TestsWindowProc(TMessage& message) m_TreeViewWindowProc(message); } //--------------------------------------------------------------------------- -/* - TFormASWUnitTestsGUIMain::UpdateCaption - - "ASWUnitTests - Framework version: ", plus " - " when --project-name gave one - other than the default. -*/ void TFormASWUnitTestsGUIMain::UpdateCaption() { - std::string caption = "ASWUnitTests - Framework version: " + TTestHandler::GetVersionStr(); - - if (m_Options.CLI.ProjectName != TCLIOptions().ProjectName) - caption += " - " + m_Options.CLI.ProjectName; - - Caption = FromUTF8(caption); + Caption = FromUTF8(GUICaption(m_Options.CLI.ProjectName)); +} +//--------------------------------------------------------------------------- +void TFormASWUnitTestsGUIMain::UpdateCommands() +{ + TGUICommandContext context; + context.FailedCount = m_TestList.StatusCount(TGUITestStatus::Failed); + context.HasReport = m_LastRunJUnitTestCases.has_value(); + context.HasSelection = (TV_Tests->Selected != nullptr); + context.RunAborted = m_RunAborted; + context.Running = m_Running; + context.TreeHasNodes = (TV_Tests->Items->Count > 0); + + TGUICommandStates const states = GUICommandStates(context); + + Act_About->Enabled = states.About; + Act_CollapseAll->Enabled = states.CollapseAll; + Act_CommandLineOptions->Enabled = states.CommandLineOptions; + Act_CopyDetails->Enabled = states.CopyDetails; + Act_Exit->Enabled = states.Exit; + Act_ExpandAll->Enabled = states.ExpandAll; + Act_ExportJUnitReport->Enabled = states.ExportJUnitReport; + Act_FocusFilter->Enabled = states.FocusFilter; + Act_ResetLayout->Enabled = states.ResetLayout; + Act_RunFailed->Enabled = states.RunFailed; + Act_RunSelected->Enabled = states.RunSelected; + Act_SelectAll->Enabled = states.SelectAll; + Act_SelectFailed->Enabled = states.SelectFailed; + Act_SelectNone->Enabled = states.SelectNone; + Act_ShuffleOrder->Enabled = states.ShuffleOrder; + Act_ShuffleSeed->Enabled = states.ShuffleSeed; + Act_Stop->Enabled = states.Stop; + Act_OpenThemesFolder->Enabled = states.Theme; + Act_ThemeDark->Enabled = states.Theme; + Act_ThemeLight->Enabled = states.Theme; + Act_ThemeWindows->Enabled = states.Theme; + Edt_Filter->Enabled = states.FilterBox; } //--------------------------------------------------------------------------- void TFormASWUnitTestsGUIMain::UpdateDetail() { TTreeNode* const node = TV_Tests->Selected; - Act_CopyDetails->Enabled = (node != nullptr); + UpdateCommands(); // Copy Details, with or without a selection. if (node == nullptr) { @@ -1529,56 +1825,25 @@ void TFormASWUnitTestsGUIMain::UpdateDetail() return; } - size_t const index = NodeIndex(node->Data); - std::string text; - - switch (NodeKind(node->Data)) - { - case TNodeKind::Suite: - text = m_TestList.SuiteDetailText(m_TestList.SuitePaths()[index]); - break; - - case TNodeKind::Group: - text = m_TestList.GroupDetailText(m_TestList.GroupNames()[index]); - break; - - case TNodeKind::Test: - text = m_TestList.DetailText(index); - break; - } - - Mem_Detail->Text = System::Sysutils::AdjustLineBreaks(FromUTF8(text)); + Mem_Detail->Text = System::Sysutils::AdjustLineBreaks(FromUTF8(GUINodeDetailText(m_TestList, + NodeRef(node->Data)))); } //--------------------------------------------------------------------------- void TFormASWUnitTestsGUIMain::UpdateStatusBar() { - auto setPanel = [this](int panel, std::string const& text) - { - SB_Status->Panels->Items[panel]->Text = FromUTF8(text); - }; - - // Counts only the tests the filter box shows, since those are the ones Run Selected runs. - std::string checked = "Checked: " + std::to_string(m_TestList.CheckedCount()) + " / " + - std::to_string(m_TestList.VisibleCount()); - if (m_TestList.VisibleCount() != m_TestList.Count()) - checked += " shown"; - setPanel(0, checked); - setPanel(1, "Run: " + std::to_string(m_RunFinishedCount) + " / " + std::to_string(m_RunTotalCount)); - setPanel(2, "Passed: " + std::to_string(m_TestList.StatusCount(TGUITestStatus::Passed))); - setPanel(3, "Failed: " + std::to_string(m_TestList.StatusCount(TGUITestStatus::Failed))); - setPanel(4, "Skipped: " + std::to_string(m_TestList.StatusCount(TGUITestStatus::Skipped))); - - std::string elapsed; + std::optional elapsedSeconds; if (m_RunStart.has_value()) { std::chrono::steady_clock::time_point const end = m_RunEnd.value_or(std::chrono::steady_clock::now()); - elapsed = FormatSeconds(std::chrono::duration(end - *m_RunStart).count()); + elapsedSeconds = std::chrono::duration(end - *m_RunStart).count(); } - setPanel(5, elapsed); - setPanel(6, m_Shuffle.StatusText()); - setPanel(7, m_StatusMessage); + std::vector const panels = GUIStatusPanels(m_TestList, m_RunFinishedCount, m_RunTotalCount, + elapsedSeconds, m_Shuffle.StatusText(), m_StatusMessage); + + for (size_t i = 0; i < panels.size() && static_cast(i) < SB_Status->Panels->Count; ++i) + SB_Status->Panels->Items[static_cast(i)]->Text = FromUTF8(panels[i]); } //--------------------------------------------------------------------------- void TFormASWUnitTestsGUIMain::UpdateTestNodeImages(size_t testIndex) @@ -1600,8 +1865,8 @@ bool TFormASWUnitTestsGUIMain::WasRunAborted() const /* TFormASWUnitTestsGUIMain::WriteJUnitReport - Writes the file itself, rather than with TJUnitReportWriter::Write(), whose std::ofstream can't open a - path with characters outside the system code page (e.g. a user folder's name) from a std::string. + Writes the file itself, rather than with TJUnitReportWriter::Write(), so a failure can say why: Write() only + returns false. */ bool TFormASWUnitTestsGUIMain::WriteJUnitReport(System::UnicodeString const& path, std::string& error) { diff --git a/vcl/gui/src/ASWUnitTests_GUI_MainForm.dfm b/vcl/gui/src/ASWUnitTests_GUI_MainForm.dfm index cea4519..6c0adc3 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_MainForm.dfm +++ b/vcl/gui/src/ASWUnitTests_GUI_MainForm.dfm @@ -238,7 +238,7 @@ object FormASWUnitTestsGUIMain: TFormASWUnitTestsGUIMain Height = 19 Panels = < item - Width = 120 + Width = 170 end item Width = 100 @@ -267,7 +267,7 @@ object FormASWUnitTestsGUIMain: TFormASWUnitTestsGUIMain Top = 80 object Act_RunSelected: TAction Caption = '&Run Selected' - Hint = 'Run the checked tests that are shown (F9)' + Hint = 'Run the checked tests that are shown' ShortCut = 120 OnExecute = Act_RunSelectedExecute end @@ -296,7 +296,7 @@ object FormASWUnitTestsGUIMain: TFormASWUnitTestsGUIMain object Act_CopyDetails: TAction Caption = '&Copy Details' Enabled = False - Hint = 'Copy the selected test'#39's details to the clipboard (Ctrl+Shift+C)' + Hint = 'Copy the selected test'#39's details to the clipboard' ShortCut = 24643 OnExecute = Act_CopyDetailsExecute end @@ -313,7 +313,7 @@ object FormASWUnitTestsGUIMain: TFormASWUnitTestsGUIMain end object Act_FocusFilter: TAction Caption = '&Filter' - Hint = 'Move to the filter box (Ctrl+F)' + Hint = 'Move to the filter box' ShortCut = 16454 OnExecute = Act_FocusFilterExecute end @@ -340,14 +340,15 @@ object FormASWUnitTestsGUIMain: TFormASWUnitTestsGUIMain end object Act_ShuffleOrder: TAction Caption = 'Run in &Shuffled Order' - Hint = - 'Run the groups, and each group'#39's tests, in a random order, to f' + - 'ind tests that depend on running in a particular order (--shuffle)' + Hint = + 'Run the groups, and each group'#39's tests, in a random order, to fi' + + 'nd tests that depend on running in a particular order (--shuffle' + + ')' OnExecute = Act_ShuffleOrderExecute end object Act_ShuffleSeed: TAction Caption = 'Shuffle S&eed...' - Hint = + Hint = 'Choose a new random seed for each shuffled run, or one seed for ' + 'every run to repeat its order (--shuffle-seed)' OnExecute = Act_ShuffleSeedExecute @@ -362,6 +363,32 @@ object FormASWUnitTestsGUIMain: TFormASWUnitTestsGUIMain Hint = 'Collapse the test tree to its top-level suites and groups' OnExecute = Act_CollapseAllExecute end + object Act_ThemeWindows: TAction + Caption = 'Use &Windows Setting' + Checked = True + GroupIndex = 1 + Hint = 'Use the light or dark theme, as Windows'#39' app mode is set' + OnExecute = Act_ThemeWindowsExecute + end + object Act_ThemeLight: TAction + Caption = '&Light' + GroupIndex = 1 + Hint = 'Use the light theme, whatever Windows is set to' + OnExecute = Act_ThemeLightExecute + end + object Act_ThemeDark: TAction + Caption = '&Dark' + GroupIndex = 1 + Hint = 'Use the dark theme, whatever Windows is set to' + OnExecute = Act_ThemeDarkExecute + end + object Act_OpenThemesFolder: TAction + Caption = '&Open Themes Folder' + Hint = + 'Open the folder of the VCL style files View > Theme > Dark Theme' + + ' lists' + OnExecute = Act_OpenThemesFolderExecute + end end object IL_Status: TImageList Left = 120 @@ -429,6 +456,37 @@ object FormASWUnitTestsGUIMain: TFormASWUnitTestsGUIMain object MI_ViewResetLayout: TMenuItem Action = Act_ResetLayout end + object MI_ViewSeparator2: TMenuItem + Caption = '-' + end + object MI_ViewTheme: TMenuItem + Caption = '&Theme' + object MI_ViewThemeWindows: TMenuItem + Action = Act_ThemeWindows + RadioItem = True + end + object MI_ViewThemeLight: TMenuItem + Action = Act_ThemeLight + RadioItem = True + end + object MI_ViewThemeDark: TMenuItem + Action = Act_ThemeDark + RadioItem = True + end + object MI_ViewThemeSeparator1: TMenuItem + Caption = '-' + end + object MI_ViewDarkTheme: TMenuItem + Caption = 'Dark &Theme' + OnClick = MI_ViewDarkThemeClick + object MI_ViewDarkThemeSeparator1: TMenuItem + Caption = '-' + end + object MI_ViewDarkThemeOpenFolder: TMenuItem + Action = Act_OpenThemesFolder + end + end + end end object MI_Options: TMenuItem Caption = '&Options' diff --git a/vcl/gui/src/ASWUnitTests_GUI_MainForm.h b/vcl/gui/src/ASWUnitTests_GUI_MainForm.h index b933b5c..1d4d504 100644 --- a/vcl/gui/src/ASWUnitTests_GUI_MainForm.h +++ b/vcl/gui/src/ASWUnitTests_GUI_MainForm.h @@ -54,6 +54,7 @@ limitations under the License. #include "ASWUnitTests_GUI_RunObserver.h" #include "ASWUnitTests_GUI_Shuffle.h" #include "ASWUnitTests_GUI_TestList.h" +#include "ASWUnitTests_GUI_Theme.h" //--------------------------------------------------------------------------- ///////////////////////////////////////////////////////////////////////////// @@ -73,6 +74,7 @@ class TFormASWUnitTestsGUIMain : public TForm TAction* Act_ExpandAll; TAction* Act_ExportJUnitReport; TAction* Act_FocusFilter; + TAction* Act_OpenThemesFolder; TAction* Act_ResetLayout; TAction* Act_RunFailed; TAction* Act_RunSelected; @@ -82,6 +84,9 @@ class TFormASWUnitTestsGUIMain : public TForm TAction* Act_ShuffleOrder; TAction* Act_ShuffleSeed; TAction* Act_Stop; + TAction* Act_ThemeDark; + TAction* Act_ThemeLight; + TAction* Act_ThemeWindows; TActionList* AL_Main; TButton* Btn_CopyDetails; TButton* Btn_RunFailed; @@ -118,9 +123,18 @@ class TFormASWUnitTestsGUIMain : public TForm TMenuItem* MI_TestsSeparator1; TMenuItem* MI_View; TMenuItem* MI_ViewCollapseAll; + TMenuItem* MI_ViewDarkTheme; + TMenuItem* MI_ViewDarkThemeOpenFolder; + TMenuItem* MI_ViewDarkThemeSeparator1; TMenuItem* MI_ViewExpandAll; TMenuItem* MI_ViewResetLayout; TMenuItem* MI_ViewSeparator1; + TMenuItem* MI_ViewSeparator2; + TMenuItem* MI_ViewTheme; + TMenuItem* MI_ViewThemeDark; + TMenuItem* MI_ViewThemeLight; + TMenuItem* MI_ViewThemeSeparator1; + TMenuItem* MI_ViewThemeWindows; TMainMenu* MM_Main; TProgressBar* PB_Progress; TPageControl* PC_Results; @@ -142,6 +156,7 @@ class TFormASWUnitTestsGUIMain : public TForm void __fastcall Act_ExpandAllExecute(TObject* Sender); void __fastcall Act_ExportJUnitReportExecute(TObject* Sender); void __fastcall Act_FocusFilterExecute(TObject* Sender); + void __fastcall Act_OpenThemesFolderExecute(TObject* Sender); void __fastcall Act_ResetLayoutExecute(TObject* Sender); void __fastcall Act_RunFailedExecute(TObject* Sender); void __fastcall Act_RunSelectedExecute(TObject* Sender); @@ -151,12 +166,16 @@ class TFormASWUnitTestsGUIMain : public TForm void __fastcall Act_ShuffleOrderExecute(TObject* Sender); void __fastcall Act_ShuffleSeedExecute(TObject* Sender); void __fastcall Act_StopExecute(TObject* Sender); + void __fastcall Act_ThemeDarkExecute(TObject* Sender); + void __fastcall Act_ThemeLightExecute(TObject* Sender); + void __fastcall Act_ThemeWindowsExecute(TObject* Sender); void __fastcall Edt_FilterChange(TObject* Sender); void __fastcall FormClose(TObject* Sender, TCloseAction& Action); void __fastcall FormCloseQuery(TObject* Sender, bool& CanClose); void __fastcall LV_FailuresCustomDrawItem(TCustomListView* Sender, TListItem* Item, TCustomDrawState State, bool& DefaultDraw); void __fastcall LV_FailuresDblClick(TObject* Sender); + void __fastcall MI_ViewDarkThemeClick(TObject* Sender); void __fastcall TV_TestsChange(TObject* Sender, TTreeNode* Node); void __fastcall TV_TestsCheckStateChanging(TCustomTreeView* Sender, TTreeNode* Node, TNodeCheckState NewCheckState, TNodeCheckState OldCheckState, bool& AllowChange); @@ -171,8 +190,17 @@ class TFormASWUnitTestsGUIMain : public TForm bool m_CloseAfterRun; // The test that has started but not yet finished, if any. std::optional m_CurrentTestIndex; + // The VCL style m_DarkTheme is (see LoadGUIDarkStyle()), which ApplyTheme() uses while the theme is dark. + System::UnicodeString m_DarkStyle; + // The View > Theme > Dark Theme choice: GUIDefaultDarkThemeName() or a VCL style file's name. + std::string m_DarkTheme; + // The items FillDarkThemeMenu() added to View > Theme > Dark Theme, one per dark theme, by index into + // m_DarkThemeNames, which each one's Tag is. + std::vector m_DarkThemeItems; + std::vector m_DarkThemeNames; // The layout from the DFM, at 96 DPI, captured before the saved one is applied, for Reset Layout. ASWUnitTests::TGUILayout m_DefaultLayout; + TWndMethod m_FormWindowProc; // The form's own WindowProc, which FormWindowProc() passes messages on to. // Set when the command line doesn't choose the tests itself (no --run, --filter, or partition options), so // the saved test selection is restored at start and saved again at close. bool m_KeepSelection; @@ -189,6 +217,9 @@ class TFormASWUnitTestsGUIMain : public TForm bool m_Running; std::optional m_RunStart; size_t m_RunTotalCount; + // True while Start() puts --filter's pattern in the filter box, so Edt_FilterChange() leaves building the + // tree to Start(), which then builds it once. + bool m_SettingFilterText; // Whether runs are shuffled, and with which seed; starts from --shuffle and --shuffle-seed. ASWUnitTests::TGUIShuffle m_Shuffle; std::string m_StatusMessage; @@ -198,6 +229,10 @@ class TFormASWUnitTestsGUIMain : public TForm // By index into m_TestList; nullptr while hidden. Read through TestNode(), which rebuilds it when the // tree's window has been recreated. std::vector m_TestNodes; + // The View > Theme menu's choice. + ASWUnitTests::EGUITheme m_Theme; + // Set when Windows' setting changed during a run, so ApplyTheme() applies it once the run has ended. + bool m_ThemeChangePending; // Set when the tree's window is destroyed, which frees every TTreeNode, so m_TestNodes points at freed // nodes until RebuildNodeCache() finds the ones the recreated window loaded in their place. bool m_TreeNodesStale; @@ -212,13 +247,21 @@ class TFormASWUnitTestsGUIMain : public TForm void ApplyFilterBox(); // Sets the test tree width and detail pane height that 'layout' has, fitted to the window's current size. void ApplyPanelSizes(ASWUnitTests::TGUILayout const& layout); + // Sets the style for m_Theme and Windows' current setting; see the definition. + void ApplyTheme(); void BuildTestTree(); void CenterOnMonitor(int width, int height); void CreateStatusImages(); // The window's layout as it is now, for saving. ASWUnitTests::TGUILayout CurrentLayout(); + // Chooses the dark theme of the View > Theme > Dark Theme item 'Sender'. + void __fastcall DarkThemeItemClick(TObject* Sender); // Ends a run that an unhandled exception escaped from, described by 'description'. void FailRunOnUnhandledException(std::string const& description); + // Lists Carbon and the VCL style files in View > Theme > Dark Theme, as it opens; see the definition. + void FillDarkThemeMenu(); + // Follows the theme as it changes; see the definition. + void __fastcall FormWindowProc(TMessage& message); // Restores the layout SaveLayout() saved, if any; see the definition. void LoadLayout(); // Restores the test selection SaveSelection() saved, if any, noting what it restored in the log. @@ -227,6 +270,8 @@ class TFormASWUnitTestsGUIMain : public TForm void OnTestStarted(std::string const& groupName, std::string const& testName); // Refills m_TestNodes from the tree's test nodes, by the index each one holds in its Data. void RebuildNodeCache(); + // Colors the log's lines again, for the style now active. + void RecolorLog(); void RefreshAllNodeImages(); // Deletes the saved layout, for --layout-reset, noting the outcome in the log. void ResetSavedLayout(); @@ -240,18 +285,29 @@ class TFormASWUnitTestsGUIMain : public TForm void SaveLayout(); // Saves which tests are checked and shown for the next start, ignoring any error. void SaveSelection(); + // Chooses View > Theme > Dark Theme's 'name', saving it, unless --layout-ignore, and applies it; see the + // definition. + void SetDarkTheme(std::string const& name); // Sets a tree node's status image from m_TestList: a test's own status, or a group's or suite's, from its // tests. void SetNodeImage(TTreeNode* node); + // Sets the progress bar's position so it's also drawn in the error state; see the definition. + void SetProgressPosition(int position); void SetRunning(bool running); + // Chooses the theme, as the View > Theme menu does: saves it, unless --layout-ignore, and applies it. + void SetTheme(ASWUnitTests::EGUITheme theme); // Shows m_Shuffle's state: Run in Shuffled Order's check mark, and the status bar. void ShowShuffleState(); + // Shows m_Theme as the View > Theme menu's checked item. + void ShowThemeState(); void SyncTreeChecks(); // The test's tree node, or nullptr while it's hidden. TTreeNode* TestNode(size_t testIndex); // Watches for the tree's window being destroyed; see m_TreeNodesStale. void __fastcall TV_TestsWindowProc(TMessage& message); void UpdateCaption(); + // Enables or disables every action, and the filter box, by GUICommandStates()'s rules. + void UpdateCommands(); // Shows the selected test's, group's or suite's details in the detail pane. void UpdateDetail(); void UpdateStatusBar(); @@ -268,8 +324,11 @@ class TFormASWUnitTestsGUIMain : public TForm // The process exit code: with --exit, what the console runner would have returned for the --run run; // otherwise ExitCode_Success, since the GUI was used interactively. int ExitCode() const; - // Loads the registered tests and applies 'options' (see WinMain()). Call once, right after creating the form. - void Start(ASWUnitTests::TGUIOptions const& options); + // Loads the registered tests and applies 'options' (see WinMain()), and shows 'theme' and 'darkTheme', the + // choices WinMain() applied, in the View > Theme menu, with 'darkStyle', the VCL style LoadGUIDarkStyle() gave + // for 'darkTheme', and the problems it found in the log. Call once, right after creating the form. + void Start(ASWUnitTests::TGUIOptions const& options, ASWUnitTests::EGUITheme theme, std::string const& darkTheme, + System::UnicodeString const& darkStyle, std::vector const& themeProblems); // True once a run has ended with a test that timed out or crashed. A timed-out test's abandoned thread may then // still be running, using its group, so WinMain() ends the process without destroying the groups, as the console // runner does. diff --git a/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.cpp b/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.cpp new file mode 100644 index 0000000..0f2e2b7 --- /dev/null +++ b/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.cpp @@ -0,0 +1,335 @@ +/* ************************************************************************** +ASWUnitTests_GUI_VCLSystem.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_GUI_VCLSystem.h" +//--------------------------------------------------------------------------- +#include +#include +#include +//--------------------------------------------------------------------------- +#include +#include +#include +#include // Links the unit that registers the engine loading VCL style resources, e.g. Carbon. +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_INIDocument.h" +#include "ASWUnitTests_GUI_Strings.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +//--------------------------------------------------------------------------- +/* + ApplyGUITheme + + TrySetStyle() leaves the style as it is if the dark style's resource isn't linked in (the GUI project's + Custom_Styles), as in the VCL console runner, or the Vcl.Styles unit isn't, which loads it. +*/ +bool ApplyGUITheme(EGUITheme theme, System::UnicodeString const& darkStyle) +{ + bool const dark = GUIThemeIsDark(theme, WindowsAppsUseDarkTheme(), WindowsHighContrast()); + System::UnicodeString const styleName = dark ? darkStyle : System::UnicodeString("Windows"); + + if (System::Sysutils::SameText(Vcl::Themes::TStyleManager::ActiveStyle->Name, styleName)) + return false; + + return Vcl::Themes::TStyleManager::TrySetStyle(styleName, false); +} +//--------------------------------------------------------------------------- +std::vector BuiltInGUIDarkThemeNames() +{ + std::vector names = { GUIDefaultDarkThemeName() }; + System::DynamicArray const styles = Vcl::Themes::TStyleManager::StyleNames; + + for (std::string const& name : GUIVCLStyleDarkThemeNames()) + { + for (int i = 0; i < styles.Length; ++i) + { + if (System::Sysutils::SameText(styles[i], FromUTF8(name))) + { + names.push_back(name); + break; + } + } + } + + return names; +} +//--------------------------------------------------------------------------- +std::vector FileNamesInFolder(System::UnicodeString const& folderPath) +{ + std::vector names; + + try + { + if (!System::Sysutils::DirectoryExists(folderPath)) + return names; + + System::DynamicArray const files = System::Ioutils::TDirectory::GetFiles(folderPath); + + for (int i = 0; i < files.Length; ++i) + names.push_back(ToUTF8(System::Sysutils::ExtractFileName(files[i]))); + } + catch (...) + { + names.clear(); + } + + return names; +} +//--------------------------------------------------------------------------- +System::UnicodeString GUILayoutFilePath() +{ + // GetHomePath() is %APPDATA% on Windows. + System::UnicodeString const folder = System::Ioutils::TPath::Combine(System::Ioutils::TPath::GetHomePath(), + "ASWUnitTests"); + System::UnicodeString const fileName = System::Sysutils::ChangeFileExt( + System::Sysutils::ExtractFileName(System::ParamStr(0)), ".ini"); + + return System::Ioutils::TPath::Combine(folder, fileName); +} +//--------------------------------------------------------------------------- +System::UnicodeString GUISelectionFilePath() +{ + return System::Sysutils::ChangeFileExt(GUILayoutFilePath(), ".selection"); +} +//--------------------------------------------------------------------------- +System::UnicodeString GUIThemesFolderPath() +{ + // GetHomePath() is %APPDATA% on Windows, as for GUILayoutFilePath(). + return System::Ioutils::TPath::Combine(System::Ioutils::TPath::Combine(System::Ioutils::TPath::GetHomePath(), + "ASWUnitTests"), "Themes"); +} +//--------------------------------------------------------------------------- +/* + LoadGUIDarkStyle + + A style, once loaded, stays registered for the process, under the name the file gives it, and can't be + loaded again, so a file loaded before gives the style it loaded, and a change to it shows after a restart. + A file whose style has the name of one already registered, e.g. Carbon's own file renamed, can't be loaded + either. Every error gives Carbon, with a line in 'problems'. A built-in style that isn't linked in is looked + for as a file. +*/ +System::UnicodeString LoadGUIDarkStyle(System::UnicodeString const& themesFolder, std::string const& darkTheme, + std::vector& problems) +{ + // Each file loaded so far, by its path in lowercase, and the name of its style. + static std::vector > loaded; + + System::UnicodeString const carbon = "Carbon"; + + for (std::string const& name : BuiltInGUIDarkThemeNames()) + { + if (System::Sysutils::SameText(FromUTF8(name), FromUTF8(darkTheme))) + return FromUTF8(name); + } + + std::string problem; + + try + { + System::UnicodeString const path = System::Ioutils::TPath::Combine(themesFolder, FromUTF8(darkTheme) + ".vsf"); + System::UnicodeString const key = System::Sysutils::AnsiLowerCase(path); + + for (std::pair const& entry : loaded) + { + if (entry.first == key) + return entry.second; + } + + Vcl::Themes::TStyleInfo info; + + if (!System::Sysutils::FileExists(path)) + { + problem = "There's no style file " + ToUTF8(path) + ", so " + GUIDefaultDarkThemeName() + + " is used instead."; + } + else if (!Vcl::Themes::TStyleManager::IsValidStyle(path, info)) + { + problem = "Its file isn't a VCL style file, so " + GUIDefaultDarkThemeName() + " is used instead."; + } + else + { + System::DynamicArray const names = Vcl::Themes::TStyleManager::StyleNames; + + for (int i = 0; i < names.Length && problem.empty(); ++i) + { + if (System::Sysutils::SameText(names[i], info.Name)) + { + problem = "Its style is named " + ToUTF8(info.Name) + ", as a style already loaded is, so " + + GUIDefaultDarkThemeName() + " is used instead. Give it a name of its own in RAD Studio's " + + "Bitmap Style Designer."; + } + } + + if (problem.empty()) + { + Vcl::Themes::TStyleManager::LoadFromFile(path); + loaded.emplace_back(key, info.Name); + return info.Name; + } + } + } + catch (System::Sysutils::Exception const& exception) + { + problem = "Its style file couldn't be loaded, so " + GUIDefaultDarkThemeName() + " is used instead: " + + ToUTF8(exception.Message); + } + + problems.push_back(GUIThemeProblemLine(darkTheme, problem)); + return carbon; +} +//--------------------------------------------------------------------------- +std::string LoadGUIDarkThemeFromLayoutFile(System::UnicodeString const& layoutPath) +{ + try + { + if (!System::Sysutils::FileExists(layoutPath)) + return GUIDefaultDarkThemeName(); + + return LoadGUIDarkTheme(TGUIINIDocument(ReadFileBytes(layoutPath))); + } + catch (...) + { + return GUIDefaultDarkThemeName(); + } +} +//--------------------------------------------------------------------------- +EGUITheme LoadGUIThemeFromLayoutFile(System::UnicodeString const& layoutPath) +{ + try + { + if (!System::Sysutils::FileExists(layoutPath)) + return EGUITheme::Windows; + + return LoadGUITheme(TGUIINIDocument(ReadFileBytes(layoutPath))); + } + catch (...) + { + return EGUITheme::Windows; + } +} +//--------------------------------------------------------------------------- +/* + OpenFolderInExplorer + + ShellExecuteW() returns a value above 32 on success. +*/ +void OpenFolderInExplorer(System::UnicodeString const& folderPath) +{ + HINSTANCE const result = ::ShellExecuteW(nullptr, L"open", folderPath.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + + if (reinterpret_cast(result) <= 32) + throw System::Sysutils::Exception(System::UnicodeString("Couldn't open the folder ") + folderPath); +} +//--------------------------------------------------------------------------- +std::vector ProcessArguments() +{ + std::vector args; + + for (int i = 1; i <= System::ParamCount(); ++i) + args.push_back(ToUTF8(System::ParamStr(i))); + + return args; +} +//--------------------------------------------------------------------------- +std::string ReadFileBytes(System::UnicodeString const& path) +{ + System::Sysutils::TBytes const bytes = System::Ioutils::TFile::ReadAllBytes(path); + + if (bytes.Length == 0) + return std::string(); + + return std::string(reinterpret_cast(&bytes[0]), static_cast(bytes.Length)); +} +//--------------------------------------------------------------------------- +void SaveGUIDarkThemeToLayoutFile(System::UnicodeString const& layoutPath, std::string const& name) +{ + try + { + System::Sysutils::ForceDirectories(System::Sysutils::ExtractFileDir(layoutPath)); + + // Starting from the file's current text keeps the layout and any other values in it. + TGUIINIDocument ini(System::Sysutils::FileExists(layoutPath) ? ReadFileBytes(layoutPath) : std::string()); + SaveGUIDarkTheme(ini, name); + WriteFileBytes(layoutPath, ini.Text()); + } + catch (...) + { + // Not saving it (e.g. a read-only profile) only means the next start uses the default dark theme. + } +} +//--------------------------------------------------------------------------- +void SaveGUIThemeToLayoutFile(System::UnicodeString const& layoutPath, EGUITheme theme) +{ + try + { + System::Sysutils::ForceDirectories(System::Sysutils::ExtractFileDir(layoutPath)); + + // Starting from the file's current text keeps the layout and any other values in it. + TGUIINIDocument ini(System::Sysutils::FileExists(layoutPath) ? ReadFileBytes(layoutPath) : std::string()); + SaveGUITheme(ini, theme); + WriteFileBytes(layoutPath, ini.Text()); + } + catch (...) + { + // Not saving it (e.g. a read-only profile) only means the next start uses the default theme. + } +} +//--------------------------------------------------------------------------- +bool WindowsAppsUseDarkTheme() +{ + DWORD value = 1; + DWORD size = sizeof(value); + + LSTATUS const status = ::RegGetValueW(HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"AppsUseLightTheme", + RRF_RT_REG_DWORD, nullptr, &value, &size); + + return status == ERROR_SUCCESS && value == 0; +} +//--------------------------------------------------------------------------- +bool WindowsHighContrast() +{ + HIGHCONTRASTW highContrast{}; + highContrast.cbSize = sizeof(highContrast); + + return ::SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(highContrast), &highContrast, 0) && + (highContrast.dwFlags & HCF_HIGHCONTRASTON) != 0; +} +//--------------------------------------------------------------------------- +void WriteFileBytes(System::UnicodeString const& path, std::string const& bytes) +{ + std::unique_ptr const file(new System::Classes::TFileStream(path, fmCreate)); + + if (!bytes.empty()) + file->WriteBuffer(bytes.data(), static_cast(bytes.size())); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.h b/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.h new file mode 100644 index 0000000..c5439a0 --- /dev/null +++ b/vcl/gui/src/ASWUnitTests_GUI_VCLSystem.h @@ -0,0 +1,99 @@ +/* ************************************************************************** +ASWUnitTests_GUI_VCLSystem.h +Author: Anthony S. West - ASW Software + +What the VCL GUI runner needs from its process and system through the +VCL/RTL, kept out of the shared units in gui/src, which contain no VCL code. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_GUI_VCLSystemH +#define ASWUnitTests_GUI_VCLSystemH +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_Theme.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +// Sets the VCL style for 'theme' (see GUIThemeIsDark()): 'darkStyle' (see LoadGUIDarkStyle()) when it's dark, +// otherwise the default unstyled look. Returns whether the active style changed. Call it on the main thread; +// before any form exists, each form is created in that style, and afterward it restyles the open ones. +bool ApplyGUITheme(EGUITheme theme, System::UnicodeString const& darkStyle); +// The dark themes this runner has built in, for the View > Theme > Dark Theme menu: Carbon (see +// GUIDefaultDarkThemeName()), then each of GUIVCLStyleDarkThemeNames() whose VCL style is linked in (the GUI +// project's Custom_Styles), by its style name. +std::vector BuiltInGUIDarkThemeNames(); +// The names of the files (not folders) in the folder, without the folder, converted to UTF-8; empty if the +// folder doesn't exist or can't be listed. +std::vector FileNamesInFolder(System::UnicodeString const& folderPath); +// This executable's layout file, %APPDATA%\ASWUnitTests\.ini, so each test app built on the GUI +// keeps its own layout, shared by all of its build configurations. +System::UnicodeString GUILayoutFilePath(); +// This executable's selection file, beside its layout file (see GUILayoutFilePath()): its name with a +// ".selection" extension. +System::UnicodeString GUISelectionFilePath(); +// The folder that holds the theme files both GUI runners share, whatever executable runs them: +// %APPDATA%\ASWUnitTests\Themes. The VCL GUI runner's are VCL style files, ".vsf". It may not exist yet. +System::UnicodeString GUIThemesFolderPath(); +// The VCL style the dark theme 'darkTheme' is, for ApplyGUITheme(): the built-in style of that name, ignoring +// case (see BuiltInGUIDarkThemeNames()), Carbon included; otherwise the style in ".vsf" in +// 'themesFolder', e.g. GUIThemesFolderPath(), loaded the first time it's needed. Adds a line for the log to +// 'problems' (see GUIThemeProblemLine()), and gives Carbon, when the file is missing, isn't a VCL style, or has +// the name of a style already loaded from elsewhere. +System::UnicodeString LoadGUIDarkStyle(System::UnicodeString const& themesFolder, std::string const& darkTheme, + std::vector& problems); +// The dark theme SaveGUIDarkThemeToLayoutFile() saved in the layout file at 'layoutPath', e.g. +// GUILayoutFilePath(), or GUIDefaultDarkThemeName() if there's none or it can't be read. +std::string LoadGUIDarkThemeFromLayoutFile(System::UnicodeString const& layoutPath); +// The theme SaveGUIThemeToLayoutFile() saved in the layout file at 'layoutPath', e.g. GUILayoutFilePath(), or +// Windows if there's none or it can't be read. +EGUITheme LoadGUIThemeFromLayoutFile(System::UnicodeString const& layoutPath); +// Opens the folder in a File Explorer window. Throws an RTL exception if it can't, e.g. because the folder +// doesn't exist. +void OpenFolderInExplorer(System::UnicodeString const& folderPath); +// This process's command-line arguments, without the program name, converted to UTF-8, for +// TGUICommandLine::Parse(). +std::vector ProcessArguments(); +// The file's bytes, unchanged, e.g. UTF-8 text for TGUIINIDocument. Throws an RTL exception if it can't be +// read. +std::string ReadFileBytes(System::UnicodeString const& path); +// Saves 'name', the dark theme's, in the layout file at 'layoutPath', e.g. GUILayoutFilePath(), keeping its other +// values, and creating it and its folder if needed. Ignores any error, which only means the next start uses the +// default dark theme. +void SaveGUIDarkThemeToLayoutFile(System::UnicodeString const& layoutPath, std::string const& name); +// Saves 'theme' in the layout file at 'layoutPath', e.g. GUILayoutFilePath(), keeping its other values, and +// creating it and its folder if needed. Ignores any error, which only means the next start uses the default theme. +void SaveGUIThemeToLayoutFile(System::UnicodeString const& layoutPath, EGUITheme theme); +// Whether Windows' app mode setting is dark; false if it can't be read, as on a Windows version without it. +bool WindowsAppsUseDarkTheme(); +// Whether Windows' high contrast is on. +bool WindowsHighContrast(); +// Replaces the file's content with 'bytes', unchanged, creating the file if needed (but not its folder). +// Throws an RTL exception if it can't be written. +void WriteFileBytes(System::UnicodeString const& path, std::string const& bytes); + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_GUI_VCLSystemH diff --git a/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.cpp b/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.cpp new file mode 100644 index 0000000..ce3ad92 --- /dev/null +++ b/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.cpp @@ -0,0 +1,398 @@ +/* ************************************************************************** +Test_ASWUnitTests_GUI_VCLSystem.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "Test_ASWUnitTests_GUI_VCLSystem.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include +#include +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_Strings.h" +#include "ASWUnitTests_GUI_Theme.h" +#include "ASWUnitTests_GUI_VCLSystem.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_GUI_VCLSystem +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_GUI_VCLSystem::TTest_ASWUnitTests_GUI_VCLSystem() + : inherited("ASWUnitTests_GUI_VCLSystem_Tests") +{ + RegisterTest(&TTest_ASWUnitTests_GUI_VCLSystem::Test_BuiltInGUIDarkThemeNames_AreCarbonAndTheLinkedStyles, + "BuiltInGUIDarkThemeNames_AreCarbonAndTheLinkedStyles"); + RegisterTest(&TTest_ASWUnitTests_GUI_VCLSystem::Test_FileNamesInFolder_ListsOnlyTheFiles, + "FileNamesInFolder_ListsOnlyTheFiles"); + RegisterTest(&TTest_ASWUnitTests_GUI_VCLSystem::Test_GUILayoutFilePath_IsPerExeUnderAppData, + "GUILayoutFilePath_IsPerExeUnderAppData"); + RegisterTest(&TTest_ASWUnitTests_GUI_VCLSystem::Test_GUISelectionFilePath_IsBesideTheLayoutFile, + "GUISelectionFilePath_IsBesideTheLayoutFile"); + RegisterTest(&TTest_ASWUnitTests_GUI_VCLSystem::Test_GUIThemesFolderPath_IsSharedBesideTheLayoutFiles, + "GUIThemesFolderPath_IsSharedBesideTheLayoutFiles"); + RegisterTest(&TTest_ASWUnitTests_GUI_VCLSystem::Test_LoadGUIDarkStyle_GivesCarbonForAnyProblem, + "LoadGUIDarkStyle_GivesCarbonForAnyProblem"); + RegisterTest(&TTest_ASWUnitTests_GUI_VCLSystem::Test_LoadGUIThemeFromLayoutFile_ReadsWhatSaveWrote, + "LoadGUIThemeFromLayoutFile_ReadsWhatSaveWrote"); + RegisterTest(&TTest_ASWUnitTests_GUI_VCLSystem::Test_ProcessArguments_MatchesParamCount, "ProcessArguments_MatchesParamCount"); + RegisterTest(&TTest_ASWUnitTests_GUI_VCLSystem::Test_ReadFileBytes_ReadsWhatWriteFileBytesWrote, + "ReadFileBytes_ReadsWhatWriteFileBytesWrote"); + RegisterTest(&TTest_ASWUnitTests_GUI_VCLSystem::Test_WindowsAppsUseDarkTheme_MatchesTheRegistry, + "WindowsAppsUseDarkTheme_MatchesTheRegistry"); +} +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_GUI_VCLSystem::~TTest_ASWUnitTests_GUI_VCLSystem() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::SetUp_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::SetUp_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::TearDown_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::TearDown_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- + +// /////// Begin tests after this line /////////////////////// + +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::Test_BuiltInGUIDarkThemeNames_AreCarbonAndTheLinkedStyles() +{ + // Arrange: the styles this runner links in, none in the console runner, all of them in the GUI. + System::DynamicArray const styles = Vcl::Themes::TStyleManager::StyleNames; + std::string expected = GUIDefaultDarkThemeName() + "\n"; + + for (std::string const& name : GUIVCLStyleDarkThemeNames()) + { + for (int i = 0; i < styles.Length; ++i) + { + if (System::Sysutils::SameText(styles[i], FromUTF8(name))) + expected += name + "\n"; + } + } + + // Act + std::string names; + + for (std::string const& name : BuiltInGUIDarkThemeNames()) + names += name + "\n"; + + // Assert + CheckEquals(expected, names, __func__, __LINE__, "Carbon first, then each linked style, in order"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::Test_FileNamesInFolder_ListsOnlyTheFiles() +{ + // Arrange: two files, one with a non-ASCII name (U+4E2D), and a folder named like a style file, in a folder + // deleted however the test ends. + System::UnicodeString const folder = System::Ioutils::TPath::Combine(System::Ioutils::TPath::GetTempPath(), + System::UnicodeString("ASWUnitTests_VCLSystem_") + System::Sysutils::IntToStr(static_cast( + GetCurrentProcessId()))); + + struct TDeleteFolder + { + System::UnicodeString const Folder; + + ~TDeleteFolder() + { + try + { + System::Ioutils::TDirectory::Delete(Folder, true); + } + catch (...) + { + } + } + } const deleteFolder{ folder }; + + System::Sysutils::ForceDirectories(System::Ioutils::TPath::Combine(folder, "Folder.vsf")); + WriteFileBytes(System::Ioutils::TPath::Combine(folder, "Ocean.vsf"), "x"); + WriteFileBytes(System::Ioutils::TPath::Combine(folder, System::UnicodeString(L"\x4E2D.vsf")), std::string()); + WriteFileBytes(System::Ioutils::TPath::Combine(folder, "Folder.vsf\\Inside.vsf"), "x"); + + // Act + std::vector names = FileNamesInFolder(folder); + std::vector const missing = FileNamesInFolder(System::Ioutils::TPath::Combine(folder, "Missing")); + + // Assert: both sorted the same way, as where a non-ASCII byte sorts depends on whether char is signed. + std::vector expected = { "Ocean.vsf", "\xE4\xB8\xAD.vsf" }; + std::sort(expected.begin(), expected.end()); + std::sort(names.begin(), names.end()); + std::string expectedText; + std::string listed; + + for (std::string const& name : expected) + expectedText += name + "\n"; + + for (std::string const& name : names) + listed += name + "\n"; + + CheckEquals(expectedText, listed, __func__, __LINE__, + "the files' names, in UTF-8, without the folder, its subfolder or the files in it"); + CheckTrue(missing.empty(), __func__, __LINE__, "none for a missing folder"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::Test_GUILayoutFilePath_IsPerExeUnderAppData() +{ + // Arrange + System::UnicodeString const exeName = System::Sysutils::ExtractFileName(System::ParamStr(0)); + System::UnicodeString const appData = System::Sysutils::GetEnvironmentVariable("APPDATA"); + + // Act + System::UnicodeString const path = GUILayoutFilePath(); + + // Assert + System::UnicodeString const folder = System::Sysutils::ExtractFileDir(path); + CheckEquals(ToUTF8(System::Sysutils::ChangeFileExt(exeName, ".ini")), + ToUTF8(System::Sysutils::ExtractFileName(path)), __func__, __LINE__, "named after this executable"); + CheckEquals(std::string("ASWUnitTests"), ToUTF8(System::Sysutils::ExtractFileName(folder)), __func__, __LINE__, + "in an ASWUnitTests folder"); + AssertFalse(appData.IsEmpty(), __func__, __LINE__, "%APPDATA% is set"); + CheckTrue(System::Sysutils::SameText(System::Sysutils::ExtractFileDir(folder), + System::Sysutils::ExcludeTrailingPathDelimiter(appData)), __func__, __LINE__, "under %APPDATA%"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::Test_GUISelectionFilePath_IsBesideTheLayoutFile() +{ + // Act + System::UnicodeString const selectionPath = GUISelectionFilePath(); + System::UnicodeString const layoutPath = GUILayoutFilePath(); + + // Assert + CheckEquals(ToUTF8(System::Sysutils::ChangeFileExt(layoutPath, ".selection")), ToUTF8(selectionPath), __func__, + __LINE__, "the layout file's path with a .selection extension"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::Test_GUIThemesFolderPath_IsSharedBesideTheLayoutFiles() +{ + // Act and Assert: the same for every executable, unlike the layout file. + CheckEquals(ToUTF8(System::Ioutils::TPath::Combine(System::Sysutils::ExtractFileDir(GUILayoutFilePath()), + "Themes")), ToUTF8(GUIThemesFolderPath()), __func__, __LINE__, "a Themes folder beside the layout file"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::Test_LoadGUIDarkStyle_GivesCarbonForAnyProblem() +{ + // Arrange: a missing theme, and a file that isn't a style, in a Themes folder of the test's own, not the real + // one, which a GUI running these tests lists. Each name is the process's, as a style file, once loaded, isn't + // read again. The folder is deleted however the test ends. + std::string const suffix = std::to_string(GetCurrentProcessId()); + std::string const missingName = "NoSuchTheme_" + suffix; + std::string const invalidName = "NotAStyle_" + suffix; + System::UnicodeString const folder = System::Ioutils::TPath::Combine(System::Ioutils::TPath::GetTempPath(), + System::UnicodeString("ASWUnitTests_VCLSystem_Themes_") + FromUTF8(suffix)); + + struct TDeleteFolder + { + System::UnicodeString const Folder; + + ~TDeleteFolder() + { + try + { + System::Ioutils::TDirectory::Delete(Folder, true); + } + catch (...) + { + } + } + } const deleteFolder{ folder }; + + System::Sysutils::ForceDirectories(folder); + WriteFileBytes(System::Ioutils::TPath::Combine(folder, FromUTF8(invalidName) + ".vsf"), "not a VCL style"); + + // Act + std::vector carbonProblems; + System::UnicodeString const carbon = LoadGUIDarkStyle(folder, "Carbon", carbonProblems); + std::vector missingProblems; + System::UnicodeString const missing = LoadGUIDarkStyle(folder, missingName, missingProblems); + std::vector invalidProblems; + System::UnicodeString const invalid = LoadGUIDarkStyle(folder, invalidName, invalidProblems); + + // Assert + CheckEquals(std::string("Carbon"), ToUTF8(carbon), __func__, __LINE__, "the built-in theme's style"); + CheckTrue(carbonProblems.empty(), __func__, __LINE__, "with no problems"); + CheckEquals(std::string("Carbon"), ToUTF8(missing), __func__, __LINE__, "Carbon for a missing file"); + AssertEquals(static_cast(1), missingProblems.size(), __func__, __LINE__, "with one problem"); + std::string const missingStart = "Dark theme \"" + missingName + "\": There's no style file "; + CheckEquals(missingStart, missingProblems.front().substr(0, missingStart.size()), __func__, __LINE__, + "saying so"); + CheckEquals(std::string("Carbon"), ToUTF8(invalid), __func__, __LINE__, "Carbon for a file that isn't a style"); + AssertEquals(static_cast(1), invalidProblems.size(), __func__, __LINE__, "with one problem"); + CheckEquals("Dark theme \"" + invalidName + "\": Its file isn't a VCL style file, so Carbon is used instead.", + invalidProblems.front(), __func__, __LINE__, "saying so"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::Test_LoadGUIThemeFromLayoutFile_ReadsWhatSaveWrote() +{ + // Arrange: a layout file in a folder that doesn't exist yet, not this runner's own, which a GUI running these + // tests is using. The folder is deleted however the test ends. + System::UnicodeString const folder = System::Ioutils::TPath::Combine(System::Ioutils::TPath::GetTempPath(), + System::UnicodeString("ASWUnitTests_VCLSystem_Layout_") + System::Sysutils::IntToStr(static_cast( + GetCurrentProcessId()))); + System::UnicodeString const path = System::Ioutils::TPath::Combine(System::Ioutils::TPath::Combine(folder, + "Settings"), "Layout.ini"); + + struct TDeleteFolder + { + System::UnicodeString const Folder; + + ~TDeleteFolder() + { + try + { + System::Ioutils::TDirectory::Delete(Folder, true); + } + catch (...) + { + } + } + } const deleteFolder{ folder }; + + EGUITheme const noFile = LoadGUIThemeFromLayoutFile(path); + std::string const noFileDarkTheme = LoadGUIDarkThemeFromLayoutFile(path); + + // Act + SaveGUIThemeToLayoutFile(path, EGUITheme::Dark); + EGUITheme const dark = LoadGUIThemeFromLayoutFile(path); + + WriteFileBytes(path, "[Window]\r\nMaximized=1\r\n"); + SaveGUIThemeToLayoutFile(path, EGUITheme::Light); + EGUITheme const light = LoadGUIThemeFromLayoutFile(path); + SaveGUIDarkThemeToLayoutFile(path, "Deep \xC3\x89" "cume"); + std::string const darkTheme = LoadGUIDarkThemeFromLayoutFile(path); + std::string const text = ReadFileBytes(path); + + System::Sysutils::DeleteFile(path); + SaveGUIDarkThemeToLayoutFile(path, "Ocean"); + std::string const darkThemeInNewFile = LoadGUIDarkThemeFromLayoutFile(path); + + // Assert + CheckTrue(noFile == EGUITheme::Windows, __func__, __LINE__, "Windows without a layout file"); + CheckEquals(std::string("Carbon"), noFileDarkTheme, __func__, __LINE__, "and the dark theme Carbon"); + CheckTrue(dark == EGUITheme::Dark, __func__, __LINE__, "the theme saved in a new file, and its new folder"); + CheckTrue(light == EGUITheme::Light, __func__, __LINE__, "the theme saved in an existing file"); + CheckEquals(std::string("Deep \xC3\x89" "cume"), darkTheme, __func__, __LINE__, + "the dark theme saved beside it, in UTF-8"); + CheckEquals(std::string("[Window]\r\nMaximized=1\r\n\r\n[View]\r\nTheme=Light\r\nDarkTheme=Deep \xC3\x89" "cume\r\n" + "\r\n"), text, __func__, __LINE__, "the layout stays"); + CheckEquals(std::string("Ocean"), darkThemeInNewFile, __func__, __LINE__, "the dark theme saved in a new file"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::Test_ProcessArguments_MatchesParamCount() +{ + // Arrange + // The runner's own arguments vary with how it was started, so only their count can be checked here. + int const expectedCount = System::ParamCount(); + + // Act + std::vector const args = ProcessArguments(); + + // Assert + CheckEquals(static_cast(expectedCount), args.size(), __func__, __LINE__, + "one entry per argument, without the program name"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::Test_ReadFileBytes_ReadsWhatWriteFileBytesWrote() +{ + // Arrange: a file name with a character outside every ANSI code page but UTF-8's (U+4E2D), and bytes a + // text conversion would change: CRLF and LF line breaks, a NUL, and UTF-8 for U+00E9. + System::UnicodeString const path = System::Ioutils::TPath::Combine(System::Ioutils::TPath::GetTempPath(), + System::UnicodeString(L"ASWUnitTests_VCLSystem_\x4E2D.tmp")); + std::string const bytes("a\r\nb\nc\0d\xC3\xA9", 10); + + // Deletes the file however the test ends. + struct TDeleteFile + { + System::UnicodeString const Path; + + ~TDeleteFile() + { + System::Sysutils::DeleteFile(Path); + } + } const deleteFile{ path }; + + // Act + WriteFileBytes(path, bytes); + std::string const read = ReadFileBytes(path); + + WriteFileBytes(path, "xy"); + std::string const shorter = ReadFileBytes(path); + + WriteFileBytes(path, std::string()); + std::string const empty = ReadFileBytes(path); + + // Assert + CheckEquals(bytes, read, __func__, __LINE__, "every byte is read back unchanged"); + CheckEquals(std::string("xy"), shorter, __func__, __LINE__, "writing again replaces the whole file"); + CheckEmpty(empty, __func__, __LINE__, "an empty file reads as empty"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_GUI_VCLSystem::Test_WindowsAppsUseDarkTheme_MatchesTheRegistry() +{ + // Arrange: the setting read another way, through the RTL. This machine's setting can't be changed here, + // so only agreement with it can be checked. + bool expected = false; + std::unique_ptr const registry(new System::Win::Registry::TRegistry(KEY_READ)); + registry->RootKey = HKEY_CURRENT_USER; + + if (registry->OpenKeyReadOnly("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize") && + registry->ValueExists("AppsUseLightTheme")) + { + expected = (registry->ReadInteger("AppsUseLightTheme") == 0); + } + + // Act + bool const dark = WindowsAppsUseDarkTheme(); + + // Assert + CheckEquals(expected, dark, __func__, __LINE__, "dark when AppsUseLightTheme is 0"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +ASW_REGISTER_TEST_GROUP(ASWUnitTests::TTest_ASWUnitTests_GUI_VCLSystem) diff --git a/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.h b/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.h new file mode 100644 index 0000000..9a2bb14 --- /dev/null +++ b/vcl/tests/Test_ASWUnitTests_GUI_VCLSystem.h @@ -0,0 +1,68 @@ +/* ************************************************************************** +Test_ASWUnitTests_GUI_VCLSystem.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef Test_ASWUnitTests_GUI_VCLSystemH +#define Test_ASWUnitTests_GUI_VCLSystemH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_GUI_VCLSystem +// +// Exercises the VCL GUI runner's process and system access +// (vcl/gui/src/ASWUnitTests_GUI_VCLSystem.h). Listed in both vcl/ projects, +// so it also runs from the VCL console runner. +///////////////////////////////////////////////////////////////////////////// +class TTest_ASWUnitTests_GUI_VCLSystem : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: // Test methods + void Test_BuiltInGUIDarkThemeNames_AreCarbonAndTheLinkedStyles(); + void Test_FileNamesInFolder_ListsOnlyTheFiles(); + void Test_GUILayoutFilePath_IsPerExeUnderAppData(); + void Test_GUISelectionFilePath_IsBesideTheLayoutFile(); + void Test_GUIThemesFolderPath_IsSharedBesideTheLayoutFiles(); + void Test_LoadGUIDarkStyle_GivesCarbonForAnyProblem(); + void Test_LoadGUIThemeFromLayoutFile_ReadsWhatSaveWrote(); + void Test_ProcessArguments_MatchesParamCount(); + void Test_ReadFileBytes_ReadsWhatWriteFileBytesWrote(); + void Test_WindowsAppsUseDarkTheme_MatchesTheRegistry(); + +public: + TTest_ASWUnitTests_GUI_VCLSystem(); + ~TTest_ASWUnitTests_GUI_VCLSystem() override; + + void SetUp_Group() override; + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override; + void TearDown_Test(ITestCase& testCase) override; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef Test_ASWUnitTests_GUI_VCLSystemH diff --git a/vcl/tests/Test_ASWUnitTests_RTLExceptions.cpp b/vcl/tests/Test_ASWUnitTests_RTLExceptions.cpp index ee79e36..35a0fc2 100644 --- a/vcl/tests/Test_ASWUnitTests_RTLExceptions.cpp +++ b/vcl/tests/Test_ASWUnitTests_RTLExceptions.cpp @@ -45,21 +45,6 @@ namespace using namespace ASWUnitTests; -bool NameEndsWith(std::string const& name, std::string const& suffix); - -//--------------------------------------------------------------------------- - -// True if 'name' ends with 'suffix'. Used below so a fixture test's own name ("..._Passes"/ -// "..._Fails") documents its expected outcome, and the outer verifying test can check every -// registered test generically instead of hand-maintaining a separate expected-outcome table. -bool NameEndsWith(std::string const& name, std::string const& suffix) -{ - return name.size() >= suffix.size() && name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0; -} - -//--------------------------------------------------------------------------- - - ///////////////////////////////////////////////////////////////////////////// // TFixtureObject // @@ -75,8 +60,6 @@ class TFixtureObject : public System::TObject } }; -//--------------------------------------------------------------------------- - ///////////////////////////////////////////////////////////////////////////// // TFixture_RTLExceptionExpectations @@ -86,9 +69,10 @@ class TFixtureObject : public System::TObject // with an exact match (thrown from C++ and raised from Delphi RTL code), a polymorphic base-type match, a // wrong sibling type, a message substring that's present, absent, non-ASCII, or only in the class name, // a std::exception, and a non-exception throw; plus a std::exception type expected but an RTL exception -// thrown, and an earlier Check* failure followed by the expected RTL exception; plus, where supported, the -// std::source_location form of SetExceptionExpected(). Every test name ends with "_Passes" or "_Fails", -// read generically by CheckFixtureOutcomes(). +// thrown, and an earlier Check* failure followed by the expected RTL exception; plus int expected and an int, +// an RTL exception or one raised from Delphi RTL code thrown, which must reach the RTL handler without ever +// being rethrown; plus, where supported, the std::source_location form of SetExceptionExpected(). Every test +// name ends with "_Passes" or "_Fails", read generically by CheckFixtureOutcomes(). ///////////////////////////////////////////////////////////////////////////// class TFixture_RTLExceptionExpectations : public TTestGroupBase { @@ -99,6 +83,9 @@ class TFixture_RTLExceptionExpectations : public TTestGroupBase void Test_CheckFailedThenExpectedRTLExceptionThrown_Fails(); void Test_ExceptionExpectedButNoneThrown_Fails(); void Test_GenericExceptionExpected_RTLExceptionThrown_Passes(); + void Test_IntTypeExpected_IntThrown_Passes(); + void Test_IntTypeExpected_RaisedFromDelphiCode_Fails(); + void Test_IntTypeExpected_RTLExceptionThrown_Fails(); void Test_RTLTypeExpected_ExactTypeThrown_Passes(); void Test_RTLTypeExpected_MessageSubstringAbsent_Fails(); void Test_RTLTypeExpected_MessageSubstringOnlyInClassName_Fails(); @@ -122,6 +109,129 @@ class TFixture_RTLExceptionExpectations : public TTestGroupBase void TearDown_Group() override {} }; +///////////////////////////////////////////////////////////////////////////// +// TFixture_RTLThrowsChecks +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing CheckThrows()/AssertThrows()/ +// CheckNoThrow() with RTL exceptions: an exact type (thrown from C++ and raised from Delphi RTL code), a base +// type, a message substring that's absent, a wrong sibling type, RTL and std types crossed either way, and int +// expected but an RTL exception raised from Delphi RTL code. Test names self-document expected outcome via +// NameEndsWith(), as in TFixture_RTLExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_RTLThrowsChecks : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_AssertThrows_ExactType_Passes(); + void Test_CheckNoThrow_RTLExceptionThrown_Fails(); + void Test_CheckThrows_BaseType_Passes(); + void Test_CheckThrows_IntTypeExpected_RaisedFromDelphiCode_Fails(); + void Test_CheckThrows_MessageSubstringAbsent_Fails(); + void Test_CheckThrows_RaisedFromDelphiCode_Passes(); + void Test_CheckThrows_RTLTypeExpected_StdExceptionThrown_Fails(); + void Test_CheckThrows_StdTypeExpected_RTLExceptionThrown_Fails(); + void Test_CheckThrows_WrongSiblingType_Fails(); + +public: + TFixture_RTLThrowsChecks(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_RTLTypeChecks +// +// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing CheckIsType()/CheckIsNotType() with RTL +// (Delphi) classes, held as their base class System::Sysutils::Exception: a subclass, a sibling, and a reference; +// and a C++-declared TObject descendant, held as a TObject, with a Delphi class as the expected type, and the reverse. +// Test names self-document expected outcome via NameEndsWith(), as in TFixture_RTLExceptionExpectations above. +///////////////////////////////////////////////////////////////////////////// +class TFixture_RTLTypeChecks : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + void Test_CheckIsNotType_Sibling_Passes(); + void Test_CheckIsNotType_Subclass_Fails(); + void Test_CheckIsType_CppDeclaredClass_Fails(); + void Test_CheckIsType_DelphiClass_Fails(); + void Test_CheckIsType_Reference_Passes(); + void Test_CheckIsType_Sibling_Fails(); + void Test_CheckIsType_Subclass_Passes(); + +public: + TFixture_RTLTypeChecks(); + + void SetUp_Group() override {} + void TearDown_Group() override {} +}; + +///////////////////////////////////////////////////////////////////////////// +// TFixture_UnexpectedRTLException +// +// A never-registered fixture group whose first test meets an RTL exception it doesn't expect, from where +// 'ThrowFrom' says: the test itself, SetUp_Test(), or TearDown_Test() after the test threw the RTL exception it +// expects. The second test records whether it ran. The first only fails its own test; the other two escape +// Run() intact, ending the run. +///////////////////////////////////////////////////////////////////////////// +class TFixture_UnexpectedRTLException : public TTestGroupBase +{ +public: + enum class TThrowFrom + { + Test, + SetUp, + TearDownAfterExpectedException + }; + +private: + typedef TTestGroupBase inherited; + +private: + void Test_RunsAfterThrowingTest(); + void Test_Throws(); + +public: + bool RunsAfterThrowingTestReached = false; + TThrowFrom const ThrowFrom; + +public: + explicit TFixture_UnexpectedRTLException(TThrowFrom throwFrom); + + void SetUp_Group() override {} + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override {} + void TearDown_Test(ITestCase& testCase) override; +}; + +//--------------------------------------------------------------------------- + +bool NameEndsWith(std::string const& name, std::string const& suffix); + +//--------------------------------------------------------------------------- +/* + NameEndsWith + + True if 'name' ends with 'suffix'. Used below so a fixture test's own name ("..._Passes"/ + "..._Fails") documents its expected outcome, and the outer verifying test can check every + registered test generically instead of hand-maintaining a separate expected-outcome table. +*/ +bool NameEndsWith(std::string const& name, std::string const& suffix) +{ + return name.size() >= suffix.size() && name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0; +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_RTLExceptionExpectations +///////////////////////////////////////////////////////////////////////////// + //--------------------------------------------------------------------------- TFixture_RTLExceptionExpectations::TFixture_RTLExceptionExpectations() : inherited("Fixture_RTLExceptionExpectations") @@ -134,6 +244,12 @@ TFixture_RTLExceptionExpectations::TFixture_RTLExceptionExpectations() "ExceptionExpectedButNoneThrown_Fails"); RegisterTest(&TFixture_RTLExceptionExpectations::Test_GenericExceptionExpected_RTLExceptionThrown_Passes, "GenericExceptionExpected_RTLExceptionThrown_Passes"); + RegisterTest(&TFixture_RTLExceptionExpectations::Test_IntTypeExpected_IntThrown_Passes, + "IntTypeExpected_IntThrown_Passes"); + RegisterTest(&TFixture_RTLExceptionExpectations::Test_IntTypeExpected_RaisedFromDelphiCode_Fails, + "IntTypeExpected_RaisedFromDelphiCode_Fails"); + RegisterTest(&TFixture_RTLExceptionExpectations::Test_IntTypeExpected_RTLExceptionThrown_Fails, + "IntTypeExpected_RTLExceptionThrown_Fails"); RegisterTest(&TFixture_RTLExceptionExpectations::Test_RTLTypeExpected_ExactTypeThrown_Passes, "RTLTypeExpected_ExactTypeThrown_Passes"); RegisterTest(&TFixture_RTLExceptionExpectations::Test_RTLTypeExpected_MessageSubstringAbsent_Fails, @@ -183,6 +299,24 @@ void TFixture_RTLExceptionExpectations::Test_GenericExceptionExpected_RTLExcepti throw System::Sysutils::EConvertError(L"whatever"); } //--------------------------------------------------------------------------- +void TFixture_RTLExceptionExpectations::Test_IntTypeExpected_IntThrown_Passes() +{ + SetExceptionExpected(__func__, __LINE__, "int expected and thrown"); + throw 42; +} +//--------------------------------------------------------------------------- +void TFixture_RTLExceptionExpectations::Test_IntTypeExpected_RaisedFromDelphiCode_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "int expected, StrToInt raises EConvertError"); + static_cast(System::Sysutils::StrToInt(L"not a number")); +} +//--------------------------------------------------------------------------- +void TFixture_RTLExceptionExpectations::Test_IntTypeExpected_RTLExceptionThrown_Fails() +{ + SetExceptionExpected(__func__, __LINE__, "int expected, RTL exception thrown"); + throw System::Sysutils::EConvertError(L"boom"); +} +//--------------------------------------------------------------------------- void TFixture_RTLExceptionExpectations::Test_RTLTypeExpected_ExactTypeThrown_Passes() { SetExceptionExpected(__func__, __LINE__, "exact type match"); @@ -274,33 +408,7 @@ void TFixture_RTLExceptionExpectations::Test_StdTypeExpected_RTLExceptionThrown_ ///////////////////////////////////////////////////////////////////////////// // TFixture_RTLThrowsChecks -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing CheckThrows()/AssertThrows()/ -// CheckNoThrow() with RTL exceptions: an exact type (thrown from C++ and raised from Delphi RTL code), a base -// type, a message substring that's absent, a wrong sibling type, and RTL and std types crossed either way. Test -// names self-document expected outcome via NameEndsWith(), as in TFixture_RTLExceptionExpectations above. ///////////////////////////////////////////////////////////////////////////// -class TFixture_RTLThrowsChecks : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_AssertThrows_ExactType_Passes(); - void Test_CheckNoThrow_RTLExceptionThrown_Fails(); - void Test_CheckThrows_BaseType_Passes(); - void Test_CheckThrows_MessageSubstringAbsent_Fails(); - void Test_CheckThrows_RaisedFromDelphiCode_Passes(); - void Test_CheckThrows_RTLTypeExpected_StdExceptionThrown_Fails(); - void Test_CheckThrows_StdTypeExpected_RTLExceptionThrown_Fails(); - void Test_CheckThrows_WrongSiblingType_Fails(); - -public: - TFixture_RTLThrowsChecks(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_RTLThrowsChecks::TFixture_RTLThrowsChecks() @@ -312,6 +420,8 @@ TFixture_RTLThrowsChecks::TFixture_RTLThrowsChecks() RegisterTest(&TFixture_RTLThrowsChecks::Test_CheckNoThrow_RTLExceptionThrown_Fails, "CheckNoThrow_RTLExceptionThrown_Fails"); RegisterTest(&TFixture_RTLThrowsChecks::Test_CheckThrows_BaseType_Passes, "CheckThrows_BaseType_Passes"); + RegisterTest(&TFixture_RTLThrowsChecks::Test_CheckThrows_IntTypeExpected_RaisedFromDelphiCode_Fails, + "CheckThrows_IntTypeExpected_RaisedFromDelphiCode_Fails"); RegisterTest(&TFixture_RTLThrowsChecks::Test_CheckThrows_MessageSubstringAbsent_Fails, "CheckThrows_MessageSubstringAbsent_Fails"); RegisterTest(&TFixture_RTLThrowsChecks::Test_CheckThrows_RaisedFromDelphiCode_Passes, @@ -347,6 +457,14 @@ void TFixture_RTLThrowsChecks::Test_CheckThrows_BaseType_Passes() __LINE__, "base type expected, derived type thrown"); } //--------------------------------------------------------------------------- +void TFixture_RTLThrowsChecks::Test_CheckThrows_IntTypeExpected_RaisedFromDelphiCode_Fails() +{ + CheckThrows([] { + static_cast(System::Sysutils::StrToInt(L"not a number")); + }, + __func__, __LINE__, "int expected, StrToInt raises EConvertError"); +} +//--------------------------------------------------------------------------- void TFixture_RTLThrowsChecks::Test_CheckThrows_MessageSubstringAbsent_Fails() { CheckThrows([] { @@ -392,32 +510,7 @@ void TFixture_RTLThrowsChecks::Test_CheckThrows_WrongSiblingType_Fails() ///////////////////////////////////////////////////////////////////////////// // TFixture_RTLTypeChecks -// -// A never-registered (no ASW_REGISTER_TEST_GROUP) fixture group testing CheckIsType()/CheckIsNotType() with RTL -// (Delphi) classes, held as their base class System::Sysutils::Exception: a subclass, a sibling, and a reference; -// and a C++-declared TObject descendant, held as a TObject, with a Delphi class as the expected type, and the reverse. -// Test names self-document expected outcome via NameEndsWith(), as in TFixture_RTLExceptionExpectations above. ///////////////////////////////////////////////////////////////////////////// -class TFixture_RTLTypeChecks : public TTestGroupBase -{ -private: - typedef TTestGroupBase inherited; - -private: - void Test_CheckIsNotType_Sibling_Passes(); - void Test_CheckIsNotType_Subclass_Fails(); - void Test_CheckIsType_CppDeclaredClass_Fails(); - void Test_CheckIsType_DelphiClass_Fails(); - void Test_CheckIsType_Reference_Passes(); - void Test_CheckIsType_Sibling_Fails(); - void Test_CheckIsType_Subclass_Passes(); - -public: - TFixture_RTLTypeChecks(); - - void SetUp_Group() override {} - void TearDown_Group() override {} -}; //--------------------------------------------------------------------------- TFixture_RTLTypeChecks::TFixture_RTLTypeChecks() @@ -482,41 +575,7 @@ void TFixture_RTLTypeChecks::Test_CheckIsType_Subclass_Passes() ///////////////////////////////////////////////////////////////////////////// // TFixture_UnexpectedRTLException -// -// A never-registered fixture group whose first test meets an RTL exception it doesn't expect, from where -// 'ThrowFrom' says: the test itself, SetUp_Test(), or TearDown_Test() after the test threw the RTL exception it -// expects. The second test records whether it ran. The first only fails its own test; the other two escape -// Run() intact, ending the run. ///////////////////////////////////////////////////////////////////////////// -class TFixture_UnexpectedRTLException : public TTestGroupBase -{ -public: - enum class TThrowFrom - { - Test, - SetUp, - TearDownAfterExpectedException - }; - -private: - typedef TTestGroupBase inherited; - -private: - void Test_RunsAfterThrowingTest(); - void Test_Throws(); - -public: - bool RunsAfterThrowingTestReached = false; - TThrowFrom const ThrowFrom; - -public: - explicit TFixture_UnexpectedRTLException(TThrowFrom throwFrom); - - void SetUp_Group() override {} - void SetUp_Test(ITestCase& testCase) override; - void TearDown_Group() override {} - void TearDown_Test(ITestCase& testCase) override; -}; //--------------------------------------------------------------------------- TFixture_UnexpectedRTLException::TFixture_UnexpectedRTLException(TThrowFrom throwFrom) @@ -623,9 +682,9 @@ void TTest_ASWUnitTests_RTLExceptions::CheckFixtureOutcomes(std::optional(8), results.CaseRecords.size(), __func__, __LINE__, + CheckEquals(static_cast(9), results.CaseRecords.size(), __func__, __LINE__, "one record per registered test"); for (TTestCaseRecord const& record : results.CaseRecords) @@ -976,6 +1035,9 @@ void TTest_ASWUnitTests_RTLExceptions::Test_Throws_MatchesRTLTypeAndMessage() CheckEndsWith(findMessage("CheckNoThrow_RTLExceptionThrown_Fails"), "): Expected no exception but caught: EConvertError: boom. must not throw", __func__, __LINE__, "CheckNoThrow shows the RTL exception's class and message"); + CheckContains(findMessage("CheckThrows_IntTypeExpected_RaisedFromDelphiCode_Fails"), + "): Expected a different exception type but caught: EConvertError: ", __func__, __LINE__, + "an RTL exception raised from Delphi code is a wrong type for int"); CheckEndsWith(findMessage("CheckThrows_MessageSubstringAbsent_Fails"), "): Expected the exception message to contain \"needle\" but caught: EConvertError: boom. " "message must contain 'needle'", __func__, __LINE__, "a message mismatch shows the RTL exception"); diff --git a/vcl/tests/Test_ASWUnitTests_RTLStrings.cpp b/vcl/tests/Test_ASWUnitTests_RTLStrings.cpp index ab00cab..8745922 100644 --- a/vcl/tests/Test_ASWUnitTests_RTLStrings.cpp +++ b/vcl/tests/Test_ASWUnitTests_RTLStrings.cpp @@ -42,44 +42,6 @@ namespace using namespace ASWUnitTests; -TTestCaseRecord const* FindRecord(TTestResults const& results, std::string const& testName); -bool NameEndsWith(std::string const& name, std::string const& suffix); -System::String Text(); - -//--------------------------------------------------------------------------- - -// The record for 'testName' in 'results', or nullptr if there isn't one. -TTestCaseRecord const* FindRecord(TTestResults const& results, std::string const& testName) -{ - for (TTestCaseRecord const& record : results.CaseRecords) - { - if (record.TestName == testName) - return &record; - } - - return nullptr; -} - -//--------------------------------------------------------------------------- -// True if 'name' ends with 'suffix'. Used below so a fixture test's own name ("..._Passes"/ -// "..._Fails") documents its expected outcome, and the outer verifying test can check every -// registered test generically instead of hand-maintaining a separate expected-outcome table. -bool NameEndsWith(std::string const& name, std::string const& suffix) -{ - return name.size() >= suffix.size() && name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0; -} - -//--------------------------------------------------------------------------- -// The text most tests below compare with: "Hello" at the start, "hello" at the end and "World" in the middle, so a -// prefix, suffix or substring check matches at one place only, and only with the right case. -System::String Text() -{ - return System::String(L"Hello World hello"); -} - -//--------------------------------------------------------------------------- - - ///////////////////////////////////////////////////////////////////////////// // TFixture_RTLStringComparisons // @@ -184,6 +146,58 @@ class TFixture_RTLStringComparisons : public TTestGroupBase void TearDown_Group() override {} }; +//--------------------------------------------------------------------------- + +TTestCaseRecord const* FindRecord(TTestResults const& results, std::string const& testName); +bool NameEndsWith(std::string const& name, std::string const& suffix); +System::String Text(); + +//--------------------------------------------------------------------------- +/* + FindRecord + + The record for 'testName' in 'results', or nullptr if there isn't one. +*/ +TTestCaseRecord const* FindRecord(TTestResults const& results, std::string const& testName) +{ + for (TTestCaseRecord const& record : results.CaseRecords) + { + if (record.TestName == testName) + return &record; + } + + return nullptr; +} +//--------------------------------------------------------------------------- +/* + NameEndsWith + + True if 'name' ends with 'suffix'. Used below so a fixture test's own name ("..._Passes"/ + "..._Fails") documents its expected outcome, and the outer verifying test can check every + registered test generically instead of hand-maintaining a separate expected-outcome table. +*/ +bool NameEndsWith(std::string const& name, std::string const& suffix) +{ + return name.size() >= suffix.size() && name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0; +} +//--------------------------------------------------------------------------- +/* + Text + + The text most tests below compare with: "Hello" at the start, "hello" at the end and "World" in the middle, so a + prefix, suffix or substring check matches at one place only, and only with the right case. +*/ +System::String Text() +{ + return System::String(L"Hello World hello"); +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TFixture_RTLStringComparisons +///////////////////////////////////////////////////////////////////////////// + //--------------------------------------------------------------------------- TFixture_RTLStringComparisons::TFixture_RTLStringComparisons() : inherited("Fixture_RTLStringComparisons") @@ -727,10 +741,32 @@ void TTest_ASWUnitTests_RTLStrings::Test_Overloads_AcceptEveryTextKind() CheckStartsWith(text, narrowBuffer, __func__, __LINE__, "System::String and a char buffer"); CheckEndsWith(text, wideBuffer, __func__, __LINE__, "System::String and a wchar_t buffer"); - CheckEquals(System::String(), static_cast(nullptr), __func__, __LINE__, - "a null C string is empty text"); - CheckEquals(static_cast(nullptr), System::String(), __func__, __LINE__, - "a null wide C string is empty text"); + CheckNotEquals(System::String(), static_cast(nullptr), __func__, __LINE__, + "a null C string isn't empty text: it equals only another null C string"); + CheckNotEquals(static_cast(nullptr), System::String(), __func__, __LINE__, + "nor is a null wide C string"); + CheckNotContains(text, static_cast(nullptr), __func__, __LINE__, + "a null C string matches nothing, so the Not form passes"); + + // AnsiString with ASCII text only, which every ANSI code page has; UTF8String with any. + CheckEquals(System::AnsiString("Hello World hello"), text, __func__, __LINE__, "AnsiString and System::String"); + CheckEquals(narrow, System::AnsiString("Hello World hello"), __func__, __LINE__, "std::string and AnsiString"); + CheckContains(System::AnsiString("Hello World hello"), L"World", __func__, __LINE__, + "AnsiString and a wide literal"); + CheckEquals(System::UTF8String(nonASCII), nonASCII, __func__, __LINE__, "UTF8String and System::String"); + CheckEquals(System::UTF8String(nonASCII), "caf\xC3\xA9 \xF0\x9F\x98\x80", __func__, __LINE__, + "UTF8String and its UTF-8 bytes"); + CheckEndsWith(L"caf" L"\x00E9", System::UTF8String(L"\x00E9"), __func__, __LINE__, "a wide literal and UTF8String"); + CheckEmpty(System::AnsiString(), __func__, __LINE__, "an empty AnsiString"); + CheckNotEmpty(System::AnsiString("Hello"), __func__, __LINE__, "an AnsiString with text"); + AssertEmpty(System::UTF8String(), __func__, __LINE__, "an empty UTF8String"); + AssertNotEmpty(System::UTF8String(nonASCII), __func__, __LINE__, "a UTF8String with text"); + + // Matched as wide text, where '.' is one UTF-16 code unit. + CheckMatches(System::String(L"caf" L"\x00E9"), "caf.", __func__, __LINE__, "'.' matches the e-acute"); + CheckMatches(System::String(L"\x00E9"), System::UTF8String("."), __func__, __LINE__, + "'.' in a UTF8String pattern matches one character too"); + CheckNotMatches(System::String(L"abc"), "a.", __func__, __LINE__, "the whole text must match"); CheckEquals(nonASCII, "caf\xC3\xA9 \xF0\x9F\x98\x80", __func__, __LINE__, "non-ASCII text and its UTF-8 bytes"); CheckEquals(nonASCII, L"caf" L"\x00E9" L" \U0001F600", __func__, __LINE__, "non-ASCII text and its wide form"); @@ -889,15 +925,23 @@ void TTest_ASWUnitTests_RTLStrings::Test_Overloads_RejectNonText() static_assert(TCanCheckEquals::value, "a wide C string and System::String"); static_assert(TCanCheckEquals::value, "std::string and System::String"); static_assert(TCanCheckContains::value, "System::String and std::wstring"); + static_assert(TCanCheckContains::value, "narrow and wide text without System::String"); + static_assert(TCanCheckEquals::value, "AnsiString and UTF8String"); + static_assert(TCanCheckEquals::value, "a C string and AnsiString"); static_assert(TCanCheckEmpty::value, "a System::String"); + static_assert(TCanCheckEmpty::value, "an AnsiString"); + static_assert(TCanCheckEmpty::value, "a UTF8String"); + static_assert(TCanCheckEmpty::value, + "a C string, through its own overload rather than as a System::String (a null one isn't empty)"); static_assert(!TCanCheckEquals::value, "System::String and a number"); static_assert(!TCanCheckEquals::value, "a number and System::String"); static_assert(!TCanCheckEquals::value, "System::String and a character"); static_assert(!TCanCheckEquals::value, "two floating-point values, as without System::String"); static_assert(!TCanCheckContains::value, "System::String and a number"); - static_assert(!TCanCheckContains::value, "narrow and wide text without System::String"); - static_assert(!TCanCheckEmpty::value, "a C string, which mustn't convert to System::String"); + static_assert(!TCanCheckEquals::value, "AnsiString and a number"); + static_assert(!TCanCheckContains::value, "std::string and a number"); + static_assert(!TCanCheckEmpty::value, "a character, which mustn't convert to System::String"); static_assert(!TCanCheckEmpty::value, "a number"); } //--------------------------------------------------------------------------- diff --git a/win32/gui/rad370/ASWUnitTests_Win32_GUI.cbproj b/win32/gui/rad370/ASWUnitTests_Win32_GUI.cbproj new file mode 100644 index 0000000..e9937e1 --- /dev/null +++ b/win32/gui/rad370/ASWUnitTests_Win32_GUI.cbproj @@ -0,0 +1,451 @@ + + + {7C3B1E52-2F0A-4C61-9D3E-5A8B4F6E2D71} + 20.4 + None + ..\src\ASWUnitTests_Win32GUI_WinMain.cpp + True + Debug + Win64x + ASWUnitTests_Win32_GUI + 1048577 + Application + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + JPHNE + .\$(Platform)\$(Config) + .\$(Platform)\$(Config) + false + true + true + $(BDSLIB)\$(PLATFORM)\release\$(LANGDIR);$(ILINK_TranslatedLibraryPath) + CppVCLApplication + ..\..\..\resource\ASWUnitTests.ico + System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + true + ASWUnitTests_Win32_GUI + ..\src\;..\..\..\src\;..\..\..\tests\;..\..\..\toTest\;..\..\tests\;..\..\..\gui\src\;..\..\..\gui\tests\;$(IncludePath) + <_TCHARMapping>char + ..\src\;..\..\..\src\;..\..\..\tests\;..\..\..\toTest\;..\..\tests\;..\..\..\gui\src\;..\..\..\gui\tests\;$(ILINK_LibraryPath) + true + false + false + 1033 + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + + + adortl;appanalytics;bcbie;bcbsmp;bindcomp;bindcompdbx;bindcompfmx;bindcompvcl;bindcompvclsmp;bindcompvclwinx;bindengine;CloudService;CustomIPTransport;dbexpress;dbrtl;dbxcds;DbxClientDriver;DbxCommonDriver;DBXInterBaseDriver;DBXMySQLDriver;DBXSqliteDriver;dsnap;dsnapcon;dsnapxml;FireDAC;FireDACADSDriver;FireDACCommon;FireDACCommonDriver;FireDACCommonODBC;FireDACIBDriver;FireDACMSAccDriver;FireDACMySQLDriver;FireDACPgDriver;FireDACSqliteDriver;fmx;fmxase;fmxdae;fmxFireDAC;fmxobj;IndyCore;IndyIPClient;IndyIPCommon;IndyIPServer;IndyProtocols;IndySystem;inet;inetdb;inetdbxpress;inetstn;RESTBackendComponents;RESTComponents;rtl;Skia;soapmidas;soaprtl;soapserver;tethering;vcl;vclactnband;vcldb;vcldsnap;vcledge;vclFireDAC;vclie;vclimg;VCLRESTComponents;VclSmp;vcltouch;vclwinx;vclx;xmlrtl;$(PackageImports) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + Debug + $(BDS)\bin\Artwork\Windows\UWP\cppreg_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\cppreg_UwpDefault_150.png + ASWUnitTests_Win32_GUIPCH1.h + ..\src\ASWUnitTests_Win32GUI.manifest + none + false + -Wno-padded -Wno-format-nonliteral + true + + + adortl;appanalytics;bcbie;bcbsmp;bindcomp;bindcompdbx;bindcompfmx;bindcompvcl;bindcompvclsmp;bindcompvclwinx;bindengine;CloudService;CustomIPTransport;dbexpress;dbrtl;dbxcds;DbxClientDriver;DbxCommonDriver;DBXInterBaseDriver;DBXMySQLDriver;DBXSqliteDriver;dsnap;dsnapcon;dsnapxml;FireDAC;FireDACADSDriver;FireDACCommon;FireDACCommonDriver;FireDACCommonODBC;FireDACIBDriver;FireDACMSAccDriver;FireDACMySQLDriver;FireDACPgDriver;FireDACSqliteDriver;fmx;fmxase;fmxdae;fmxFireDAC;fmxobj;IndyCore;IndyIPClient;IndyIPCommon;IndyIPServer;IndyProtocols;IndySystem;inet;inetdb;inetdbxpress;inetstn;RESTBackendComponents;RESTComponents;rtl;Skia;soapmidas;soaprtl;soapserver;tethering;vcl;vclactnband;vcldb;vcldsnap;vcledge;vclFireDAC;vclie;vclimg;VCLRESTComponents;VclSmp;vcltouch;vclwinx;vclx;xmlrtl;$(PackageImports) + $(BDS)\bin\Artwork\Windows\UWP\cppreg_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\cppreg_UwpDefault_150.png + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + Debug + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + ASWUnitTests_Win32_GUIPCH1.h + + + adortl;bindcomp;bindcompdbx;bindcompfmx;bindcompvcl;bindcompvclsmp;bindcompvclwinx;bindengine;CloudService;CustomIPTransport;dbexpress;dbrtl;dbxcds;DbxClientDriver;DbxCommonDriver;DBXInterBaseDriver;DBXMySQLDriver;DBXSqliteDriver;dsnap;dsnapcon;dsnapxml;FireDAC;FireDACADSDriver;FireDACCommon;FireDACCommonDriver;FireDACCommonODBC;FireDACIBDriver;FireDACMSAccDriver;FireDACMySQLDriver;FireDACPgDriver;FireDACSqliteDriver;fmx;fmxase;fmxdae;fmxFireDAC;fmxobj;IndyCore;IndyIPClient;IndyIPCommon;IndyIPServer;IndyProtocols;IndySystem;inet;inetdb;inetdbxpress;inetstn;RESTBackendComponents;RESTComponents;rtl;Skia;soapmidas;soaprtl;soapserver;tethering;vcl;vclactnband;vcldb;vcldsnap;vcledge;vclFireDAC;vclie;vclimg;VCLRESTComponents;VclSmp;vcltouch;vclwinx;vclx;xmlrtl;$(PackageImports) + $(BDS)\bin\Artwork\Windows\UWP\cppreg_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\cppreg_UwpDefault_150.png + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + Debug + true + ASWUnitTests_Win32_GUIPCH1.h + ..\src\ASWUnitTests_Win32GUI.manifest + -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-padded -Wno-weak-vtables -Wno-used-but-marked-unused -Wno-format-nonliteral + none + + + false + true + false + true + _DEBUG;$(Defines) + false + None + DEBUG + true + true + true + true + Full + true + true + true + true + true + $(BDSLIB)\$(PLATFORM)\debug;$(ILINK_LibraryPath) + $(BDSLIB)\$(PLATFORM)\debug\$(LANGDIR);$(ILINK_TranslatedLibraryPath) + + + $(BDSLIB)\$(PLATFORM)$(CC_SUFFIX)\debug;$(ILINK_LibraryPath) + + + NDEBUG;$(Defines) + None + + + + ..\..\..\gui\src\ASWUnitTests_GUI_CommandLine.h + 1 + + + ..\..\..\gui\src\ASWUnitTests_GUI_Commands.h + 2 + + + ..\..\..\gui\src\ASWUnitTests_GUI_INIDocument.h + 3 + + + ..\..\..\gui\src\ASWUnitTests_GUI_Layout.h + 4 + + + ..\..\..\gui\src\ASWUnitTests_GUI_RunObserver.h + 5 + + + ..\..\..\gui\src\ASWUnitTests_GUI_Selection.h + 6 + + + ..\..\..\gui\src\ASWUnitTests_GUI_Shuffle.h + 7 + + + ..\..\..\gui\src\ASWUnitTests_GUI_TestList.h + 8 + + + ..\..\..\gui\src\ASWUnitTests_GUI_Theme.h + 62 + + + ..\..\..\gui\src\ASWUnitTests_GUI_Tree.h + 9 + + + ..\..\..\gui\src\ASWUnitTests_GUI_View.h + 10 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_CommandLine.h + 11 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Commands.h + 12 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_INIDocument.h + 13 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Layout.h + 14 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_RunObserver.h + 15 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Selection.h + 16 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Shuffle.h + 17 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_TestList.h + 18 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Theme.h + 63 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_Tree.h + 19 + + + ..\..\..\gui\tests\Test_ASWUnitTests_GUI_View.h + 20 + + + ..\..\..\src\ASWUnitTests_CLI.h + 21 + + + ..\..\..\src\ASWUnitTests_Console.h + 22 + + + ..\..\..\src\ASWUnitTests_CrashGuard.h + 23 + + + ..\..\..\src\ASWUnitTests_Exception.h + 24 + + + ..\..\..\src\ASWUnitTests_Handler.h + 25 + + + ..\..\..\src\ASWUnitTests_JUnitReport.h + 26 + + + ..\..\..\src\ASWUnitTests_Registry.h + 27 + + + ..\..\..\src\ASWUnitTests_StdOutRedirect.h + 28 + + + ..\..\..\src\ASWUnitTests_TestBase.h + 29 + + + ..\..\..\src\ASWUnitTests_Utils.h + 30 + + + ..\..\..\src\ASWUnitTests_Version.h + 31 + + + ..\..\..\tests\Test_ASWTools_Random.h + 32 + + + ..\..\..\tests\Test_ASWTools_String.h + 33 + + + ..\..\..\tests\Test_ASWUnitTests_AutoTest.h + 34 + + + ..\..\..\tests\Test_ASWUnitTests_CLI.h + 35 + + + ..\..\..\tests\Test_ASWUnitTests_Console.h + 36 + + + ..\..\..\tests\Test_ASWUnitTests_CrashGuard.h + 37 + + + ..\..\..\tests\Test_ASWUnitTests_Handler.h + 38 + + + ..\..\..\tests\Test_ASWUnitTests_JUnitReport.h + 39 + + + ..\..\..\tests\Test_ASWUnitTests_StdOutRedirect.h + 40 + + + ..\..\..\tests\Test_ASWUnitTests_Suite.h + 41 + + + ..\..\..\tests\Test_ASWUnitTests_TestBase.h + 42 + + + ..\..\..\tests\Test_ASWUnitTests_Utils.h + 43 + + + ..\..\..\tests\Test_ASWUnitTests_Version.h + 44 + + + ..\..\..\tests\UT_Helper_ExceptionTypes.h + 61 + + + ..\..\..\tests\UT_Helper_StringChecks.h + 60 + + + ..\..\..\toTest\ASWTools_Random.h + 45 + + + ..\..\..\toTest\ASWTools_String.h + 46 + + + 0 + true + + + ..\..\tests\Test_ASWUnitTests_Win32GUI_DialogTemplate.h + 58 + + + ..\..\tests\Test_ASWUnitTests_Win32GUI_Glyphs.h + 65 + + + ..\..\tests\Test_ASWUnitTests_Win32GUI_MainLayout.h + 54 + + + ..\..\tests\Test_ASWUnitTests_Win32GUI_Palette.h + 67 + + + ..\src\ASWUnitTests_Win32GUI_DialogTemplate.h + 57 + + + ..\src\ASWUnitTests_Win32GUI_DPI.h + 50 + + + ..\src\ASWUnitTests_Win32GUI_Glyphs.h + 64 + + + ..\src\ASWUnitTests_Win32GUI_MainLayout.h + 53 + + + ..\src\ASWUnitTests_Win32GUI_MainWindow.h + 55 + + + ..\src\ASWUnitTests_Win32GUI_MessageDialog.h + 68 + + + ..\src\ASWUnitTests_Win32GUI_Palette.h + 66 + + + ..\src\ASWUnitTests_Win32GUI_ShuffleSeedDialog.h + 59 + + + ..\src\ASWUnitTests_Win32GUI_Splitter.h + 56 + + + ..\src\ASWUnitTests_Win32GUI_System.h + 48 + + + ..\src\ASWUnitTests_Win32GUI_TextDialog.h + 51 + + + ..\src\ASWUnitTests_Win32GUI_Theme.h + 69 + + + 47 + + + ..\..\tests\Test_ASWUnitTests_Win32GUI_DPI.h + 52 + + + ..\..\tests\Test_ASWUnitTests_Win32GUI_System.h + 49 + + + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + + + + CPlusPlusBuilder.Personality.12 + CppVCLApplication + + + + ..\src\ASWUnitTests_Win32GUI_WinMain.cpp + + + False + True + True + False + + + + True + False + True + + + 12 + + + + diff --git a/win32/gui/rad370/ASWUnitTests_Win32_GUIPCH1.h b/win32/gui/rad370/ASWUnitTests_Win32_GUIPCH1.h new file mode 100644 index 0000000..b5e0f82 --- /dev/null +++ b/win32/gui/rad370/ASWUnitTests_Win32_GUIPCH1.h @@ -0,0 +1 @@ +#include "ASWUnitTests_Win32GUI_Windows.h" diff --git a/win32/gui/rad370/Build_Win32_Debug.bat b/win32/gui/rad370/Build_Win32_Debug.bat new file mode 100644 index 0000000..d7ec390 --- /dev/null +++ b/win32/gui/rad370/Build_Win32_Debug.bat @@ -0,0 +1,7 @@ +@echo off + +REM Temp script + +call "%Rad370%\bin\rsvars.bat" + +call cmd /c msbuild /t:Build /p:Config=Debug;Platform=Win32 ASWUnitTests_Win32_GUI.cbproj diff --git a/win32/gui/rad370/Build_Win32_Release.bat b/win32/gui/rad370/Build_Win32_Release.bat new file mode 100644 index 0000000..dc67570 --- /dev/null +++ b/win32/gui/rad370/Build_Win32_Release.bat @@ -0,0 +1,7 @@ +@echo off + +REM Temp script + +call "%Rad370%\bin\rsvars.bat" + +call cmd /c msbuild /t:Build /p:Config=Release;Platform=Win32 ASWUnitTests_Win32_GUI.cbproj diff --git a/win32/gui/rad370/Build_Win64x_Debug.bat b/win32/gui/rad370/Build_Win64x_Debug.bat new file mode 100644 index 0000000..177a535 --- /dev/null +++ b/win32/gui/rad370/Build_Win64x_Debug.bat @@ -0,0 +1,7 @@ +@echo off + +REM Temp script + +call "%Rad370%\bin\rsvars.bat" + +call cmd /c msbuild /t:Build /p:Config=Debug;Platform=Win64x ASWUnitTests_Win32_GUI.cbproj diff --git a/win32/gui/rad370/Build_Win64x_Release.bat b/win32/gui/rad370/Build_Win64x_Release.bat new file mode 100644 index 0000000..ca03b3b --- /dev/null +++ b/win32/gui/rad370/Build_Win64x_Release.bat @@ -0,0 +1,7 @@ +@echo off + +REM Temp script + +call "%Rad370%\bin\rsvars.bat" + +call cmd /c msbuild /t:Build /p:Config=Release;Platform=Win64x ASWUnitTests_Win32_GUI.cbproj diff --git a/win32/gui/src/ASWUnitTests_Win32GUI.manifest b/win32/gui/src/ASWUnitTests_Win32GUI.manifest new file mode 100644 index 0000000..a1a59e5 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI.manifest @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + true/pm + PerMonitorV2, PerMonitor + true + + + + + + + + + + diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_DPI.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_DPI.cpp new file mode 100644 index 0000000..b1b751d --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_DPI.cpp @@ -0,0 +1,129 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_DPI.cpp +Author: Anthony S. West - ASW Software + +Sizes and fonts for a DPI, for the Win32 GUI runner. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_Win32GUI_DPI.h" +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +//--------------------------------------------------------------------------- +TWin32GUIFont CreateMonospaceFont(UINT dpi) +{ + LOGFONTW font = {}; + font.lfHeight = -MulDiv(10, static_cast(dpi), 72); + font.lfWeight = FW_NORMAL; + font.lfCharSet = DEFAULT_CHARSET; + font.lfQuality = CLEARTYPE_QUALITY; + font.lfPitchAndFamily = FIXED_PITCH | FF_MODERN; + lstrcpynW(font.lfFaceName, L"Consolas", LF_FACESIZE); + + HFONT const handle = CreateFontIndirectW(&font); + + if (handle == nullptr) + throw std::runtime_error("Couldn't create the monospace font"); + + return TWin32GUIFont(handle); +} +//--------------------------------------------------------------------------- +TWin32GUIFont CreateUIFont(UINT dpi) +{ + NONCLIENTMETRICSW metrics = {}; + metrics.cbSize = sizeof(metrics); + + if (SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, sizeof(metrics), &metrics, 0, dpi) == 0) + throw std::runtime_error("Couldn't get the message font"); + + HFONT const handle = CreateFontIndirectW(&metrics.lfMessageFont); + + if (handle == nullptr) + throw std::runtime_error("Couldn't create the message font"); + + return TWin32GUIFont(handle); +} +//--------------------------------------------------------------------------- +int ScaleForDPI(int value, UINT dpi) +{ + return MulDiv(value, static_cast(dpi), USER_DEFAULT_SCREEN_DPI); +} +//--------------------------------------------------------------------------- +UINT WindowDPI(HWND window) +{ + UINT const dpi = GetDpiForWindow(window); + + return dpi != 0 ? dpi : USER_DEFAULT_SCREEN_DPI; +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUIFont +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TWin32GUIFont::TWin32GUIFont() + : m_Font(nullptr) +{ +} +//--------------------------------------------------------------------------- +TWin32GUIFont::TWin32GUIFont(HFONT font) + : m_Font(font) +{ +} +//--------------------------------------------------------------------------- +TWin32GUIFont::TWin32GUIFont(TWin32GUIFont&& other) noexcept + : m_Font(other.m_Font) +{ + other.m_Font = nullptr; +} +//--------------------------------------------------------------------------- +TWin32GUIFont::~TWin32GUIFont() +{ + if (m_Font != nullptr) + DeleteObject(m_Font); +} +//--------------------------------------------------------------------------- +TWin32GUIFont& TWin32GUIFont::operator=(TWin32GUIFont&& other) noexcept +{ + if (this != &other) + { + if (m_Font != nullptr) + DeleteObject(m_Font); + + m_Font = other.m_Font; + other.m_Font = nullptr; + } + + return *this; +} +//--------------------------------------------------------------------------- +HFONT TWin32GUIFont::Get() const +{ + return m_Font; +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_DPI.h b/win32/gui/src/ASWUnitTests_Win32GUI_DPI.h new file mode 100644 index 0000000..aeeb88f --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_DPI.h @@ -0,0 +1,75 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_DPI.h +Author: Anthony S. West - ASW Software + +Sizes and fonts for a DPI, for the Win32 GUI runner, which is per-monitor +DPI aware: its sizes are given at 96 DPI, as in the VCL GUI runner's .dfm, +and scaled to the DPI of the monitor its window is on. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_Win32GUI_DPIH +#define ASWUnitTests_Win32GUI_DPIH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUIFont +// +// Owns a font handle, deleting it when replaced or destroyed. +///////////////////////////////////////////////////////////////////////////// +class TWin32GUIFont +{ +private: + HFONT m_Font; + +public: + TWin32GUIFont(); + explicit TWin32GUIFont(HFONT font); + TWin32GUIFont(TWin32GUIFont&& other) noexcept; + ~TWin32GUIFont(); + + TWin32GUIFont(TWin32GUIFont const&) = delete; + TWin32GUIFont& operator=(TWin32GUIFont const&) = delete; + TWin32GUIFont& operator=(TWin32GUIFont&& other) noexcept; + + // The font, or nullptr if there's none. + HFONT Get() const; +}; + +//--------------------------------------------------------------------------- + +// The GUI's monospace font, Consolas 10 pt, for 'dpi', as the VCL GUI runner's detail pane, log and text +// dialog use. Throws std::runtime_error if it can't be created. +TWin32GUIFont CreateMonospaceFont(UINT dpi); +// Windows' message font (Segoe UI 9 pt by default) for 'dpi', the font of the GUI's controls. Throws +// std::runtime_error if it can't be created. +TWin32GUIFont CreateUIFont(UINT dpi); +// 'value', a size at 96 DPI, at 'dpi', rounded to the nearest pixel (halves away from zero). +int ScaleForDPI(int value, UINT dpi); +// The DPI 'window' is shown at: its monitor's, for a per-monitor DPI aware window. +UINT WindowDPI(HWND window); + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_Win32GUI_DPIH diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_DialogTemplate.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_DialogTemplate.cpp new file mode 100644 index 0000000..373decd --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_DialogTemplate.cpp @@ -0,0 +1,117 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_DialogTemplate.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_Win32GUI_DialogTemplate.h" +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +// The index of DLGTEMPLATE's cdit, the number of controls, after its style and extended style. +std::size_t const ControlCountIndex = 4; + +} // namespace + +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUIDialogTemplate +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TWin32GUIDialogTemplate::TWin32GUIDialogTemplate(DWORD style, std::wstring const& caption) +{ + AppendDWORD(style | DS_SETFONT); + AppendDWORD(0); // Extended style + m_Words.push_back(0); // Number of controls, counted by AddControl(). + m_Words.insert(m_Words.end(), { 0, 0, 200, 100 }); // x, y, cx, cy + m_Words.push_back(0); // No menu + m_Words.push_back(0); // The predefined dialog class + AppendString(caption); + m_Words.push_back(9); // Point size + AppendString(L"Segoe UI"); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIDialogTemplate::AddControl + + Appends a DLGITEMTEMPLATE, DWORD-aligned as each must be. +*/ +void TWin32GUIDialogTemplate::AddControl(EWin32GUIDialogControlClass controlClass, WORD id, DWORD style, + DWORD exStyle, std::wstring const& text) +{ + if (m_Words.size() % 2 != 0) + m_Words.push_back(0); + + AppendDWORD(style); + AppendDWORD(exStyle); + m_Words.insert(m_Words.end(), { 0, 0, 10, 10 }); // x, y, cx, cy + m_Words.push_back(id); + m_Words.push_back(0xFFFF); // A predefined class's atom follows. + m_Words.push_back(static_cast(controlClass)); + AppendString(text); + m_Words.push_back(0); // No creation data + + ++m_Words[ControlCountIndex]; +} +//--------------------------------------------------------------------------- +void TWin32GUIDialogTemplate::AppendDWORD(DWORD value) +{ + m_Words.push_back(LOWORD(value)); + m_Words.push_back(HIWORD(value)); +} +//--------------------------------------------------------------------------- +void TWin32GUIDialogTemplate::AppendString(std::wstring const& text) +{ + for (wchar_t const character : text) + m_Words.push_back(static_cast(character)); + + m_Words.push_back(0); +} +//--------------------------------------------------------------------------- +WORD TWin32GUIDialogTemplate::ControlCount() const +{ + return m_Words[ControlCountIndex]; +} +//--------------------------------------------------------------------------- +LPCDLGTEMPLATEW TWin32GUIDialogTemplate::Get() const +{ + // A std::vector's storage is aligned for any fundamental type, so the template starts on a DWORD boundary, + // as it must. + return reinterpret_cast(m_Words.data()); +} +//--------------------------------------------------------------------------- +std::vector const& TWin32GUIDialogTemplate::Words() const +{ + return m_Words; +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_DialogTemplate.h b/win32/gui/src/ASWUnitTests_Win32GUI_DialogTemplate.h new file mode 100644 index 0000000..bc383ed --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_DialogTemplate.h @@ -0,0 +1,90 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_DialogTemplate.h +Author: Anthony S. West - ASW Software + +Builds the in-memory dialog templates of the Win32 GUI runner's dialogs. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_Win32GUI_DialogTemplateH +#define ASWUnitTests_Win32GUI_DialogTemplateH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// EWin32GUIDialogControlClass +// +// The predefined window classes of a dialog template's controls, as the +// atoms a DLGITEMTEMPLATE names them by. +///////////////////////////////////////////////////////////////////////////// +enum class EWin32GUIDialogControlClass : WORD +{ + Button = 0x0080, + Edit = 0x0081, + Static = 0x0082, +}; + + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUIDialogTemplate +// +// An in-memory DLGTEMPLATE for DialogBoxIndirectParamW(): a dialog with the +// GUI's font, Segoe UI 9 pt, holding controls of the predefined classes. +// Its positions and sizes are placeholders, since the dialogs lay +// themselves out in pixels for their DPI once they're created. Windows +// scales a dialog and its controls to the DPI of the monitor it's on. +// Contains no window, so it's unit tested from the Windows console runners +// (see win32/tests/Test_ASWUnitTests_Win32GUI_DialogTemplate.cpp). +///////////////////////////////////////////////////////////////////////////// +class TWin32GUIDialogTemplate +{ +private: + std::vector m_Words; + +private: + void AppendDWORD(DWORD value); + // Appends 'text' and its terminating null. + void AppendString(std::wstring const& text); + +public: + // A dialog with 'style', to which DS_SETFONT is added, titled 'caption', with no controls yet. + TWin32GUIDialogTemplate(DWORD style, std::wstring const& caption); + + // Adds a control of 'controlClass' with the control ID 'id', 'style', 'exStyle' and 'text', after the + // controls already added, so also after them in the tab order. + void AddControl(EWin32GUIDialogControlClass controlClass, WORD id, DWORD style, DWORD exStyle, + std::wstring const& text); + // The number of controls added. + WORD ControlCount() const; + // The template, for DialogBoxIndirectParamW(); valid until this object is changed or destroyed. + LPCDLGTEMPLATEW Get() const; + // The template's contents, a WORD at a time. + std::vector const& Words() const; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_Win32GUI_DialogTemplateH diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Glyphs.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_Glyphs.cpp new file mode 100644 index 0000000..481294a --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Glyphs.cpp @@ -0,0 +1,172 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_Glyphs.cpp +Author: Anthony S. West - ASW Software + +The small shapes the Win32 GUI runner's dark theme draws. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_Win32GUI_Glyphs.h" +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +// Each pixel is sampled at SamplesPerSide x SamplesPerSide points. +int const SamplesPerSide = 4; + +//--------------------------------------------------------------------------- + +bool Covers(EWin32GUIGlyph glyph, double x, double y); +bool InTriangle(double x, double y, double ax, double ay, double bx, double by, double cx, double cy); +bool NearSegment(double x, double y, double ax, double ay, double bx, double by, double halfWidth); + +//--------------------------------------------------------------------------- +/* + Covers + + Whether the point (x, y) of a cell one unit wide, from its top left, is inside 'glyph'. The shapes' + proportions come from Carbon's glyphs: its 13-pixel check box's cross spans pixels 3 to 9 in strokes about + 1.7 pixels wide, its partial square pixels 4 to 8, its radio dot about 4.6 pixels across, and its 16-pixel + submenu and spin button arrows are 4 pixels wide and 7 tall. +*/ +bool Covers(EWin32GUIGlyph glyph, double x, double y) +{ + double const dx = x - 0.5; + double const dy = y - 0.5; + double const squaredDistance = dx * dx + dy * dy; + + switch (glyph) + { + case EWin32GUIGlyph::ArrowLeft: + return InTriangle(x, y, 0.625, 0.28, 0.625, 0.72, 0.375, 0.5); + + case EWin32GUIGlyph::ArrowRight: + return InTriangle(x, y, 0.375, 0.28, 0.375, 0.72, 0.625, 0.5); + + case EWin32GUIGlyph::Circle: + return squaredDistance <= 0.5 * 0.5; + + case EWin32GUIGlyph::Cross: + return NearSegment(x, y, 0.285, 0.285, 0.715, 0.715, 0.066) || NearSegment(x, y, 0.715, 0.285, 0.285, + 0.715, 0.066); + + case EWin32GUIGlyph::Dot: + return squaredDistance <= 0.18 * 0.18; + + case EWin32GUIGlyph::InnerCircle: + return squaredDistance <= (5.5 / 13) * (5.5 / 13); + + case EWin32GUIGlyph::Square: + return x >= 0.34 && x <= 0.66 && y >= 0.34 && y <= 0.66; + + case EWin32GUIGlyph::Tick: + return NearSegment(x, y, 0.25, 0.52, 0.42, 0.69, 0.07) || NearSegment(x, y, 0.42, 0.69, 0.76, 0.33, + 0.07); + } + + return false; +} +//--------------------------------------------------------------------------- +/* + InTriangle + + Whether (x, y) is inside the triangle a, b, c (in either winding), or on its edge. +*/ +bool InTriangle(double x, double y, double ax, double ay, double bx, double by, double cx, double cy) +{ + auto const side = [x, y](double x1, double y1, double x2, double y2) + { + return (x2 - x1) * (y - y1) - (y2 - y1) * (x - x1); + }; + + double const ab = side(ax, ay, bx, by); + double const bc = side(bx, by, cx, cy); + double const ca = side(cx, cy, ax, ay); + + return (ab >= 0 && bc >= 0 && ca >= 0) || (ab <= 0 && bc <= 0 && ca <= 0); +} +//--------------------------------------------------------------------------- +/* + NearSegment + + Whether (x, y) is within 'halfWidth' of the segment from a to b: inside a stroke along it with round ends. +*/ +bool NearSegment(double x, double y, double ax, double ay, double bx, double by, double halfWidth) +{ + double const lengthX = bx - ax; + double const lengthY = by - ay; + double const squaredLength = lengthX * lengthX + lengthY * lengthY; + double const along = squaredLength > 0 ? (std::max)(0.0, (std::min)(1.0, ((x - ax) * lengthX + (y - ay) * + lengthY) / squaredLength)) : 0.0; + double const offsetX = x - (ax + along * lengthX); + double const offsetY = y - (ay + along * lengthY); + + return offsetX * offsetX + offsetY * offsetY <= halfWidth * halfWidth; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +//--------------------------------------------------------------------------- +std::vector RenderWin32GUIGlyph(EWin32GUIGlyph glyph, int size) +{ + if (size <= 0) + return std::vector(); + + std::vector coverage(static_cast(size) * static_cast(size), 0); + int const samples = SamplesPerSide * SamplesPerSide; + + for (int row = 0; row < size; ++row) + { + for (int column = 0; column < size; ++column) + { + int inside = 0; + + for (int sampleY = 0; sampleY < SamplesPerSide; ++sampleY) + { + for (int sampleX = 0; sampleX < SamplesPerSide; ++sampleX) + { + // Each sample at the middle of its part of the pixel. + double const x = (column + (sampleX + 0.5) / SamplesPerSide) / size; + double const y = (row + (sampleY + 0.5) / SamplesPerSide) / size; + + if (Covers(glyph, x, y)) + ++inside; + } + } + + coverage[static_cast(row) * static_cast(size) + static_cast(column)] = + static_cast((inside * 255 + samples / 2) / samples); + } + } + + return coverage; +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Glyphs.h b/win32/gui/src/ASWUnitTests_Win32GUI_Glyphs.h new file mode 100644 index 0000000..564e5d2 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Glyphs.h @@ -0,0 +1,77 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_Glyphs.h +Author: Anthony S. West - ASW Software + +The small shapes the Win32 GUI runner's dark theme draws, such as a check +box's cross and a menu's submenu arrow, as Carbon (the VCL GUI runner's dark +style) has them, antialiased at any size. Contains no window code, so it's +unit tested from the Windows console runners (see +win32/tests/Test_ASWUnitTests_Win32GUI_Glyphs.cpp). + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_Win32GUI_GlyphsH +#define ASWUnitTests_Win32GUI_GlyphsH +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// EWin32GUIGlyph +// +// A shape RenderWin32GUIGlyph() draws, in a square cell. The check box's +// and radio button's are Carbon's at 13 pixels; the menus' and the spin +// buttons' arrows at 16. +///////////////////////////////////////////////////////////////////////////// +enum class EWin32GUIGlyph +{ + // A spin button's arrow pointing left: ArrowRight mirrored. + ArrowLeft, + // A menu's submenu arrow, and a spin button's pointing right: a triangle pointing right, in the cell's + // middle. + ArrowRight, + // A radio button's circle, filling the cell; drawn in the border color, with InnerCircle over it in the + // fill color. + Circle, + // A checked check box's mark: a cross. + Cross, + // A checked radio button's dot, and a checked radio menu item's. + Dot, + // The inside of a radio button's circle, one pixel in from Circle at 13 pixels. + InnerCircle, + // A check box's partial state: a square in the middle. + Square, + // A checked menu item's mark: a check mark. + Tick, +}; + +//--------------------------------------------------------------------------- + +// How much of each pixel 'glyph' covers in a 'size' x 'size' cell, row by row from the top left: 0 where it's +// absent, up to 255 where it covers the whole pixel, antialiased by sampling each pixel at 4 x 4 points. Empty +// if 'size' isn't positive. +std::vector RenderWin32GUIGlyph(EWin32GUIGlyph glyph, int size); + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_Win32GUI_GlyphsH diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_MainLayout.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_MainLayout.cpp new file mode 100644 index 0000000..8844f66 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_MainLayout.cpp @@ -0,0 +1,108 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_MainLayout.cpp +Author: Anthony S. West - ASW Software + +Where the Win32 GUI runner's main window puts its controls. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_Win32GUI_MainLayout.h" +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_DPI.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +//--------------------------------------------------------------------------- + +int ClampSplitPosition(int size, int minimum, int available); +int ResultsHeight(int clientHeight, UINT dpi); + +//--------------------------------------------------------------------------- +/* + ClampSplitPosition + + 'size' kept at least 'minimum' and at most what leaves 'minimum' of 'available' on the other side, with + the minimum winning when 'available' is too small for both. +*/ +int ClampSplitPosition(int size, int minimum, int available) +{ + return (std::max)(minimum, (std::min)(size, available - minimum)); +} +//--------------------------------------------------------------------------- +/* + ResultsHeight + + The height between the toolbar and the progress bar, which the detail pane, its splitter and the results + tabs share. +*/ +int ResultsHeight(int clientHeight, UINT dpi) +{ + return clientHeight - ScaleForDPI(TWin32GUIMainSizes::StatusBarHeight, dpi) - ScaleForDPI(TWin32GUIMainSizes::ProgressBarHeight, dpi) - + ScaleForDPI(TWin32GUIMainSizes::ToolbarHeight, dpi); +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +//--------------------------------------------------------------------------- +int ClampDetailHeight(int height, int clientHeight, UINT dpi) +{ + return ClampSplitPosition(height, ScaleForDPI(TWin32GUIMainSizes::DetailMinHeight, dpi), + ResultsHeight(clientHeight, dpi) - ScaleForDPI(TWin32GUIMainSizes::SplitterSize, dpi)); +} +//--------------------------------------------------------------------------- +int ClampTestTreeWidth(int width, int clientWidth, UINT dpi) +{ + return ClampSplitPosition(width, ScaleForDPI(TWin32GUIMainSizes::TestTreeMinWidth, dpi), + clientWidth - ScaleForDPI(TWin32GUIMainSizes::SplitterSize, dpi)); +} +//--------------------------------------------------------------------------- +TWin32GUIMainLayout LayOutMainWindow(int clientWidth, int clientHeight, int testTreeWidth, int detailHeight, UINT dpi) +{ + int const splitter = ScaleForDPI(TWin32GUIMainSizes::SplitterSize, dpi); + int const statusTop = clientHeight - ScaleForDPI(TWin32GUIMainSizes::StatusBarHeight, dpi); + int const progressTop = statusTop - ScaleForDPI(TWin32GUIMainSizes::ProgressBarHeight, dpi); + int const top = ScaleForDPI(TWin32GUIMainSizes::ToolbarHeight, dpi); + int const bottom = (std::max)(top, progressTop); + int const resultsLeft = testTreeWidth + splitter; + int const right = (std::max)(resultsLeft, clientWidth); + int const detailBottom = top + detailHeight; + + TWin32GUIMainLayout layout; + layout.TestTree = TGUIRect(0, top, testTreeWidth, bottom); + layout.TestsSplitter = TGUIRect(testTreeWidth, top, resultsLeft, bottom); + layout.Detail = TGUIRect(resultsLeft, top, right, detailBottom); + layout.DetailSplitter = TGUIRect(resultsLeft, detailBottom, right, detailBottom + splitter); + layout.Results = TGUIRect(resultsLeft, detailBottom + splitter, right, (std::max)(detailBottom + splitter, bottom)); + layout.ProgressBar = TGUIRect(0, progressTop, clientWidth, statusTop); + layout.StatusBar = TGUIRect(0, statusTop, clientWidth, clientHeight); + return layout; +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_MainLayout.h b/win32/gui/src/ASWUnitTests_Win32GUI_MainLayout.h new file mode 100644 index 0000000..d712e72 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_MainLayout.h @@ -0,0 +1,100 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_MainLayout.h +Author: Anthony S. West - ASW Software + +Where the Win32 GUI runner's main window puts its controls: the VCL GUI +runner's form layout, from its .dfm, at any DPI and window size. Contains no +window code, so it's unit tested from the Windows console runners (see +win32/tests/Test_ASWUnitTests_Win32GUI_MainLayout.cpp). + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_Win32GUI_MainLayoutH +#define ASWUnitTests_Win32GUI_MainLayoutH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_Layout.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUIMainSizes +// +// The main window's sizes at 96 DPI, from the VCL GUI runner's .dfm. +///////////////////////////////////////////////////////////////////////////// +struct TWin32GUIMainSizes +{ + static constexpr int DefaultClientHeight = 661; + static constexpr int DefaultClientWidth = 1008; + static constexpr int DefaultDetailHeight = 170; + static constexpr int DefaultTestTreeWidth = 380; + // Spl_Detail's MinSize: the least height of the detail pane, and of the results tabs below it. + static constexpr int DetailMinHeight = 60; + static constexpr int ProgressBarHeight = 20; + static constexpr int SplitterSize = 5; + // Fixed, as the VCL's TStatusBar is, rather than following its font as a Windows status bar does. + static constexpr int StatusBarHeight = 19; + // Spl_Tests' MinSize: the least width of the test tree, and of the results to its right. + static constexpr int TestTreeMinWidth = 150; + static constexpr int ToolbarHeight = 42; +}; + + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUIMainLayout +// +// The main window's controls' bounds, in client pixels; see +// LayOutMainWindow(). The toolbar's controls have fixed positions, so +// aren't here. +///////////////////////////////////////////////////////////////////////////// +struct TWin32GUIMainLayout +{ + TGUIRect Detail; // Mem_Detail + TGUIRect DetailSplitter; // Spl_Detail + TGUIRect ProgressBar; // PB_Progress + TGUIRect Results; // PC_Results + TGUIRect StatusBar; // SB_Status + TGUIRect TestTree; // TV_Tests + TGUIRect TestsSplitter; // Spl_Tests +}; + +//--------------------------------------------------------------------------- + +// The detail pane's height kept within what its splitter allows, as the VCL's TSplitter keeps it: at least +// DetailMinHeight, and leaving at least that much for the results tabs below it, in a window with the given +// client height. In a window too small for both, the detail pane's minimum wins. +int ClampDetailHeight(int height, int clientHeight, UINT dpi); +// The test tree's width kept within what its splitter allows, as the VCL's TSplitter keeps it: at least +// TestTreeMinWidth, and leaving at least that much for the results to its right, in a window with the given +// client width. In a window too small for both, the tree's minimum wins. +int ClampTestTreeWidth(int width, int clientWidth, UINT dpi); +// The controls' bounds for a client area of the given size, as the VCL lays out the .dfm's aligned controls: +// the toolbar strip at the top, the status bar and the progress bar above it at the bottom, the test tree at +// the left ('testTreeWidth' wide) with its splitter, and to its right the detail pane ('detailHeight' tall), its +// splitter and the results tabs. The tree's width and the detail pane's height are used as given, as the VCL +// keeps an aligned control's size when the window is resized; only what's left for the results shrinks, never +// below zero. +TWin32GUIMainLayout LayOutMainWindow(int clientWidth, int clientHeight, int testTreeWidth, int detailHeight, UINT dpi); + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_Win32GUI_MainLayoutH diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.cpp new file mode 100644 index 0000000..f1d810f --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.cpp @@ -0,0 +1,3105 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_MainWindow.cpp +Author: Anthony S. West - ASW Software + +The Win32 GUI runner's main window. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_Win32GUI_MainWindow.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_CLI.h" +#include "ASWUnitTests_Utils.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_INIDocument.h" +#include "ASWUnitTests_GUI_Selection.h" +#include "ASWUnitTests_GUI_Theme.h" +#include "ASWUnitTests_GUI_Tree.h" +#include "ASWUnitTests_GUI_View.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_MainLayout.h" +#include "ASWUnitTests_Win32GUI_MessageDialog.h" +#include "ASWUnitTests_Win32GUI_ShuffleSeedDialog.h" +#include "ASWUnitTests_Win32GUI_System.h" +#include "ASWUnitTests_Win32GUI_TextDialog.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +// The command ID of View > Theme > Dark Theme's first theme, Carbon, after the EWin32GUICommand IDs; each of the +// others' is the next (see TWin32GUIMainWindow::m_DarkThemeNames), up to MaxDarkThemes of them. +WORD const FirstDarkThemeCommand = 2000; +wchar_t const MainWindowClassName[] = L"ASWUnitTests_Win32GUI_Main"; +// The most themes View > Theme > Dark Theme lists, Carbon included. +std::size_t const MaxDarkThemes = 1000; +// Posted by PostToUI(), so the message loop runs the queued actions. +UINT const RunQueuedMessage = WM_APP + 1; +// The title of the GUI's windows, as the VCL GUI runner's Application->Title. +char const Title[] = "ASWUnitTests"; + +// Every command, for enabling them all. +EWin32GUICommand const AllCommands[] = { + EWin32GUICommand::About, + EWin32GUICommand::CollapseAll, + EWin32GUICommand::CommandLineOptions, + EWin32GUICommand::CopyDetails, + EWin32GUICommand::Exit, + EWin32GUICommand::ExpandAll, + EWin32GUICommand::ExportJUnitReport, + EWin32GUICommand::FocusFilter, + EWin32GUICommand::OpenThemesFolder, + EWin32GUICommand::ResetLayout, + EWin32GUICommand::RunFailed, + EWin32GUICommand::RunSelected, + EWin32GUICommand::SaveThemeTemplate, + EWin32GUICommand::SelectAll, + EWin32GUICommand::SelectFailed, + EWin32GUICommand::SelectNone, + EWin32GUICommand::ShuffleOrder, + EWin32GUICommand::ShuffleSeed, + EWin32GUICommand::Stop, + EWin32GUICommand::ThemeDark, + EWin32GUICommand::ThemeLight, + EWin32GUICommand::ThemeWindows, +}; + +//--------------------------------------------------------------------------- + +///////////////////////////////////////////////////////////////////////////// +// TRedrawScope +// +// Stops a control from redrawing for the lifetime of this object, e.g. +// while the test tree is rebuilt, then redraws it once, even if an +// exception leaves the scope. +///////////////////////////////////////////////////////////////////////////// +class TRedrawScope +{ +private: + HWND const m_Control; + +public: + explicit TRedrawScope(HWND control); + ~TRedrawScope(); + + TRedrawScope(TRedrawScope const&) = delete; + TRedrawScope& operator=(TRedrawScope const&) = delete; +}; + + +///////////////////////////////////////////////////////////////////////////// +// TScopedFlag +// +// Sets a bool for the lifetime of this object, clearing it again even if +// an exception leaves the scope. +///////////////////////////////////////////////////////////////////////////// +class TScopedFlag +{ +private: + bool& m_Flag; + +public: + explicit TScopedFlag(bool& flag); + ~TScopedFlag(); + + TScopedFlag(TScopedFlag const&) = delete; + TScopedFlag& operator=(TScopedFlag const&) = delete; +}; + +//--------------------------------------------------------------------------- + +BOOL CALLBACK AddMonitorWorkArea(HMONITOR monitor, HDC context, LPRECT bounds, LPARAM workAreas); +void AppendCommand(HMENU menu, EWin32GUICommand command, wchar_t const* caption); +HMENU AppendSubmenu(HMENU menuBar, wchar_t const* caption); +std::optional AskSaveFilePath(HWND owner, wchar_t const* title, wchar_t const* filter, + wchar_t const* defaultExtension, std::string const& initialPath); +char const* CommandName(EWin32GUICommand command); +bool IsCommandEnabled(TGUICommandStates const& states, EWin32GUICommand command); +COLORREF LogLineColor(std::string const& line, TWin32GUITheme const& theme); +std::vector MonitorWorkAreas(); +HTREEITEM NextTreeItem(HWND tree, HTREEITEM item); +MONITORINFO PrimaryMonitorInfo(); +HTREEITEM RelatedTreeItem(HWND tree, HTREEITEM item, UINT relation); +UINT StateImageIndex(TGUICheckState state); +TGUIRect ToGUIRect(RECT const& rect); +TGUINodeRef TreeItemRef(HWND tree, HTREEITEM item); + +//--------------------------------------------------------------------------- +/* + AddMonitorWorkArea + + EnumDisplayMonitors()'s callback for MonitorWorkAreas(): adds the monitor's work area to 'workAreas', a + std::vector. No exception may leave it, so one that would ends the enumeration. +*/ +BOOL CALLBACK AddMonitorWorkArea(HMONITOR monitor, HDC /*context*/, LPRECT /*bounds*/, LPARAM workAreas) +{ + try + { + MONITORINFO info = {}; + info.cbSize = sizeof(info); + + if (GetMonitorInfoW(monitor, &info) != 0) + reinterpret_cast*>(workAreas)->push_back(ToGUIRect(info.rcWork)); + + return TRUE; + } + catch (...) + { + return FALSE; + } +} +//--------------------------------------------------------------------------- +void AppendCommand(HMENU menu, EWin32GUICommand command, wchar_t const* caption) +{ + AppendMenuW(menu, MF_STRING, static_cast(command), caption); +} +//--------------------------------------------------------------------------- +HMENU AppendSubmenu(HMENU menuBar, wchar_t const* caption) +{ + HMENU const submenu = CreatePopupMenu(); + + if (submenu == nullptr) + throw std::runtime_error("Couldn't create the menu"); + + AppendMenuW(menuBar, MF_POPUP, reinterpret_cast(submenu), caption); + return submenu; +} +//--------------------------------------------------------------------------- +/* + AskSaveFilePath + + The path chosen in Windows' Save dialog, or nullopt if it was canceled. The dialog starts at 'initialPath', + given its folder and its file name separately, so a folder that doesn't exist (any more) only means it starts + in its default folder. Throws std::runtime_error if the dialog can't be shown. +*/ +std::optional AskSaveFilePath(HWND owner, wchar_t const* title, wchar_t const* filter, + wchar_t const* defaultExtension, std::string const& initialPath) +{ + std::wstring const initial = UTF8ToWide(initialPath); + std::size_t const separator = initial.find_last_of(L"\\/"); + std::wstring const initialFolder = separator == std::wstring::npos ? std::wstring() : + initial.substr(0, separator + 1); + std::wstring const initialName = separator == std::wstring::npos ? initial : initial.substr(separator + 1); + + // Room for the longest path Windows allows, plus the terminating null. + std::vector fileName(32768, L'\0'); + std::copy(initialName.begin(), initialName.begin() + + static_cast((std::min)(initialName.size(), fileName.size() - 1)), fileName.begin()); + + OPENFILENAMEW dialog = {}; + dialog.lStructSize = sizeof(dialog); + dialog.hwndOwner = owner; + dialog.lpstrFilter = filter; + dialog.lpstrFile = fileName.data(); + dialog.nMaxFile = static_cast(fileName.size()); + dialog.lpstrInitialDir = initialFolder.empty() ? nullptr : initialFolder.c_str(); + dialog.lpstrTitle = title; + dialog.Flags = OFN_ENABLESIZING | OFN_EXPLORER | OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST; + dialog.lpstrDefExt = defaultExtension; + + if (GetSaveFileNameW(&dialog) == 0) + { + DWORD const error = CommDlgExtendedError(); + + if (error != 0) + { + throw std::runtime_error("Couldn't show the " + WideToUTF8(title) + " dialog (error " + + std::to_string(error) + ")"); + } + + return std::nullopt; // Canceled. + } + + return WideToUTF8(fileName.data()); +} +//--------------------------------------------------------------------------- +/* + CommandName + + The command's name in an error report, as the VCL GUI runner's RunGuarded() calls it. +*/ +char const* CommandName(EWin32GUICommand command) +{ + switch (command) + { + case EWin32GUICommand::About: + return "About"; + + case EWin32GUICommand::CollapseAll: + return "Collapse All"; + + case EWin32GUICommand::CommandLineOptions: + return "Command Line Options"; + + case EWin32GUICommand::CopyDetails: + return "Copy Details"; + + case EWin32GUICommand::Exit: + return "Exit"; + + case EWin32GUICommand::ExpandAll: + return "Expand All"; + + case EWin32GUICommand::ExportJUnitReport: + return "Export JUnit Report"; + + case EWin32GUICommand::FocusFilter: + return "Filter"; + + case EWin32GUICommand::OpenThemesFolder: + return "Theme > Dark Theme > Open Themes Folder"; + + case EWin32GUICommand::ResetLayout: + return "Reset Layout"; + + case EWin32GUICommand::RunFailed: + return "Run Failed"; + + case EWin32GUICommand::RunSelected: + return "Run Selected"; + + case EWin32GUICommand::SaveThemeTemplate: + return "Theme > Dark Theme > Save Theme Template"; + + case EWin32GUICommand::SelectAll: + return "Select All"; + + case EWin32GUICommand::SelectFailed: + return "Select Failed"; + + case EWin32GUICommand::SelectNone: + return "Select None"; + + case EWin32GUICommand::ShuffleOrder: + return "Run in Shuffled Order"; + + case EWin32GUICommand::ShuffleSeed: + return "Shuffle Seed"; + + case EWin32GUICommand::Stop: + return "Stop"; + + case EWin32GUICommand::ThemeDark: + return "Theme > Dark"; + + case EWin32GUICommand::ThemeLight: + return "Theme > Light"; + + case EWin32GUICommand::ThemeWindows: + return "Theme > Use Windows Setting"; + } + + return "a command"; +} +//--------------------------------------------------------------------------- +bool IsCommandEnabled(TGUICommandStates const& states, EWin32GUICommand command) +{ + switch (command) + { + case EWin32GUICommand::About: + return states.About; + + case EWin32GUICommand::CollapseAll: + return states.CollapseAll; + + case EWin32GUICommand::CommandLineOptions: + return states.CommandLineOptions; + + case EWin32GUICommand::CopyDetails: + return states.CopyDetails; + + case EWin32GUICommand::Exit: + return states.Exit; + + case EWin32GUICommand::ExpandAll: + return states.ExpandAll; + + case EWin32GUICommand::ExportJUnitReport: + return states.ExportJUnitReport; + + case EWin32GUICommand::FocusFilter: + return states.FocusFilter; + + case EWin32GUICommand::OpenThemesFolder: + return states.Theme; + + case EWin32GUICommand::ResetLayout: + return states.ResetLayout; + + case EWin32GUICommand::RunFailed: + return states.RunFailed; + + case EWin32GUICommand::RunSelected: + return states.RunSelected; + + case EWin32GUICommand::SaveThemeTemplate: + return states.Theme; + + case EWin32GUICommand::SelectAll: + return states.SelectAll; + + case EWin32GUICommand::SelectFailed: + return states.SelectFailed; + + case EWin32GUICommand::SelectNone: + return states.SelectNone; + + case EWin32GUICommand::ShuffleOrder: + return states.ShuffleOrder; + + case EWin32GUICommand::ShuffleSeed: + return states.ShuffleSeed; + + case EWin32GUICommand::Stop: + return states.Stop; + + case EWin32GUICommand::ThemeDark: + case EWin32GUICommand::ThemeLight: + case EWin32GUICommand::ThemeWindows: + return states.Theme; + } + + return false; +} +//--------------------------------------------------------------------------- +/* + LogLineColor + + The log's color for one line of the framework's log output (see ClassifyGUILogLine()) in 'theme', as the + VCL GUI runner's: normal lines in the text color, explicitly, so a dark log's aren't black. +*/ +COLORREF LogLineColor(std::string const& line, TWin32GUITheme const& theme) +{ + switch (ClassifyGUILogLine(line)) + { + case TGUILogLineKind::Failure: + return theme.FailureTextColor(); + + case TGUILogLineKind::Skip: + return theme.SkipTextColor(); + + case TGUILogLineKind::Normal: + break; + } + + return theme.WindowTextColor(); +} +//--------------------------------------------------------------------------- +/* + MonitorWorkAreas + + Every monitor's work area (the part the taskbar and other docked bars leave free), in screen pixels. +*/ +std::vector MonitorWorkAreas() +{ + std::vector workAreas; + EnumDisplayMonitors(nullptr, nullptr, &AddMonitorWorkArea, reinterpret_cast(&workAreas)); + + return workAreas; +} +//--------------------------------------------------------------------------- +/* + NextTreeItem + + The node after 'item' in the tree's full order, expanded or not: its first child, or else the next sibling + of it or of the nearest of its parents that has one; nullptr after the last node. The first node is + RelatedTreeItem(tree, nullptr, TVGN_ROOT). +*/ +HTREEITEM NextTreeItem(HWND tree, HTREEITEM item) +{ + HTREEITEM const child = RelatedTreeItem(tree, item, TVGN_CHILD); + + if (child != nullptr) + return child; + + for (HTREEITEM node = item; node != nullptr; node = RelatedTreeItem(tree, node, TVGN_PARENT)) + { + HTREEITEM const sibling = RelatedTreeItem(tree, node, TVGN_NEXT); + + if (sibling != nullptr) + return sibling; + } + + return nullptr; +} +//--------------------------------------------------------------------------- +/* + PrimaryMonitorInfo + + The primary monitor's bounds and work area, which window placements' workspace coordinates are relative to. +*/ +MONITORINFO PrimaryMonitorInfo() +{ + MONITORINFO info = {}; + info.cbSize = sizeof(info); + POINT const origin = { 0, 0 }; + GetMonitorInfoW(MonitorFromPoint(origin, MONITOR_DEFAULTTOPRIMARY), &info); + + return info; +} +//--------------------------------------------------------------------------- +/* + RelatedTreeItem + + TVM_GETNEXTITEM: the node 'relation' (e.g. TVGN_CHILD) names, relative to 'item'; nullptr if there's none. +*/ +HTREEITEM RelatedTreeItem(HWND tree, HTREEITEM item, UINT relation) +{ + return reinterpret_cast(SendMessageW(tree, TVM_GETNEXTITEM, relation, reinterpret_cast(item))); +} +//--------------------------------------------------------------------------- +/* + StateImageIndex + + A test tree check box's state image, as TVS_EX_PARTIALCHECKBOXES numbers them. +*/ +UINT StateImageIndex(TGUICheckState state) +{ + switch (state) + { + case TGUICheckState::Checked: + return 2; + + case TGUICheckState::Partial: + return 3; + + case TGUICheckState::Unchecked: + break; + } + + return 1; +} +//--------------------------------------------------------------------------- +TGUIRect ToGUIRect(RECT const& rect) +{ + return TGUIRect(rect.left, rect.top, rect.right, rect.bottom); +} +//--------------------------------------------------------------------------- +/* + TreeItemRef + + Which suite, group or test a test tree node stands for, from the data BuildTestTree() gave it. +*/ +TGUINodeRef TreeItemRef(HWND tree, HTREEITEM item) +{ + TVITEMW data = {}; + data.mask = TVIF_HANDLE | TVIF_PARAM; + data.hItem = item; + SendMessageW(tree, TVM_GETITEMW, 0, reinterpret_cast(&data)); + + return DecodeGUINodeRef(static_cast(data.lParam)); +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TRedrawScope +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TRedrawScope::TRedrawScope(HWND control) + : m_Control(control) +{ + SendMessageW(m_Control, WM_SETREDRAW, FALSE, 0); +} +//--------------------------------------------------------------------------- +TRedrawScope::~TRedrawScope() +{ + SendMessageW(m_Control, WM_SETREDRAW, TRUE, 0); + RedrawWindow(m_Control, nullptr, nullptr, RDW_ERASE | RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN); +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TScopedFlag +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TScopedFlag::TScopedFlag(bool& flag) + : m_Flag(flag) +{ + m_Flag = true; +} +//--------------------------------------------------------------------------- +TScopedFlag::~TScopedFlag() +{ + m_Flag = false; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUIMainWindow +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +// The hints are the actions', with their shortcut added as the VCL adds it to an action's control's hint. +TWin32GUIMainWindow::TToolbarButton const TWin32GUIMainWindow::ToolbarButtons[7] = { + { EWin32GUICommand::RunSelected, L"R&un Selected", 8, 110, L"Run the checked tests that are shown (F9)", + &TWin32GUIMainWindow::m_Btn_RunSelected }, + { EWin32GUICommand::RunFailed, L"Run Faile&d", 124, 100, L"Rerun the tests that failed in the last run", + &TWin32GUIMainWindow::m_Btn_RunFailed }, + { EWin32GUICommand::Stop, L"&Stop", 230, 80, L"Stop the run after the current test", + &TWin32GUIMainWindow::m_Btn_Stop }, + { EWin32GUICommand::SelectAll, L"Select &All", 328, 90, L"Check every shown test", + &TWin32GUIMainWindow::m_Btn_SelectAll }, + { EWin32GUICommand::SelectNone, L"Select &None", 424, 90, L"Uncheck every shown test", + &TWin32GUIMainWindow::m_Btn_SelectNone }, + { EWin32GUICommand::SelectFailed, L"Select Fai&led", 520, 90, + L"Check only the shown tests that failed in the last run", &TWin32GUIMainWindow::m_Btn_SelectFailed }, + { EWin32GUICommand::CopyDetails, L"&Copy Details", 628, 110, + L"Copy the selected test's details to the clipboard (Ctrl+Shift+C)", &TWin32GUIMainWindow::m_Btn_CopyDetails }, +}; + +//--------------------------------------------------------------------------- +TWin32GUIMainWindow::TWin32GUIMainWindow(TWin32GUITheme& theme) + : m_Btn_CopyDetails(nullptr), + m_Btn_RunFailed(nullptr), + m_Btn_RunSelected(nullptr), + m_Btn_SelectAll(nullptr), + m_Btn_SelectFailed(nullptr), + m_Btn_SelectNone(nullptr), + m_Btn_Stop(nullptr), + m_Edt_Filter(nullptr), + m_Lbl_Filter(nullptr), + m_LV_Failures(nullptr), + m_Mem_Detail(nullptr), + m_MI_ViewDarkTheme(nullptr), + m_MM_Main(nullptr), + m_PB_Progress(nullptr), + m_PC_Results(nullptr), + m_RE_Log(nullptr), + m_SB_Status(nullptr), + m_TT_Hints(nullptr), + m_TV_Tests(nullptr), + m_Accelerators(nullptr), + m_CloseAfterRun(false), + m_Closing(false), + m_DarkTheme(GUIDefaultDarkThemeName()), + m_DarkThemeNames(1, GUIDefaultDarkThemeName()), + m_DetailHeight(TWin32GUIMainSizes::DefaultDetailHeight), + m_DPI(USER_DEFAULT_SCREEN_DPI), + m_FocusBeforeRun(nullptr), + m_FocusedControl(nullptr), + m_KeepSelection(false), + m_LastRunExitCode(ExitCode_Success), + m_OpenMaximized(false), + m_RichEditLibrary(nullptr), + m_RunAborted(false), + m_RunFinishedCount(0), + m_Running(false), + m_RunTotalCount(0), + m_SettingFilterText(false), + m_StatusImages(nullptr), + m_StatusMessage("Ready."), + m_SyncingChecks(false), + m_TestTreeWidth(TWin32GUIMainSizes::DefaultTestTreeWidth), + m_Theme(theme), + m_ThemeChangePending(false), + m_ThemeChoice(EGUITheme::Windows), + m_Window(nullptr) +{ +} +//--------------------------------------------------------------------------- +TWin32GUIMainWindow::~TWin32GUIMainWindow() +{ + // Its tests' groups are destroyed while m_Observer, declared after it, still exists. + m_Handler.reset(); + + if (m_Window != nullptr) + DestroyWindow(m_Window); + else if (m_MM_Main != nullptr) + DestroyMenu(m_MM_Main); // Created, but never given to a window, which would have destroyed it. + + // After the tree that used it. + if (m_StatusImages != nullptr) + ImageList_Destroy(m_StatusImages); + + if (m_Accelerators != nullptr) + DestroyAcceleratorTable(m_Accelerators); + + if (m_RichEditLibrary != nullptr) + FreeLibrary(m_RichEditLibrary); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::AddFailureItem + + Each row keeps its test's index into m_TestList, for its color and for a double-click. +*/ +void TWin32GUIMainWindow::AddFailureItem(std::size_t testIndex) +{ + TTestId const& test = m_TestList.Test(testIndex); + TGUITestResult const* const result = m_TestList.Result(testIndex); + + std::wstring const columns[] = { + UTF8ToWide(StatusName(m_TestList.Status(testIndex))), + UTF8ToWide(test.GroupName + "." + test.TestName), + UTF8ToWide(result != nullptr ? GUIFirstLine(result->Record.Message) : std::string()), + }; + + LVITEMW item = {}; + item.mask = LVIF_TEXT | LVIF_PARAM; + item.iItem = static_cast(SendMessageW(m_LV_Failures, LVM_GETITEMCOUNT, 0, 0)); + item.pszText = const_cast(columns[0].c_str()); + item.lParam = static_cast(testIndex); + + int const row = static_cast(SendMessageW(m_LV_Failures, LVM_INSERTITEMW, 0, reinterpret_cast(&item))); + + if (row < 0) + throw std::runtime_error("Couldn't add a test to the Failures & Skips list"); + + for (int column = 1; column < 3; ++column) + { + LVITEMW text = {}; + text.iSubItem = column; + text.pszText = const_cast(columns[column].c_str()); + SendMessageW(m_LV_Failures, LVM_SETITEMTEXTW, static_cast(row), reinterpret_cast(&text)); + } +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::AppendLog(std::string const& text) +{ + // One line at a time, so each can have its own color. A trailing piece with no line break (from + // LogAppend()) is appended as-is, and the next piece continues its line. + std::size_t start = 0; + + while (start < text.size()) + { + std::size_t const end = text.find('\n', start); + bool const endsLine = end != std::string::npos; + std::string const line = text.substr(start, endsLine ? end - start : std::string::npos); + + // Past the end is clamped to the end, which sidesteps the rich edit counting a line break as one + // character here but two in GetWindowTextLengthW(). + int const length = GetWindowTextLengthW(m_RE_Log); + SendMessageW(m_RE_Log, EM_SETSEL, static_cast(length), length); + + CHARFORMAT2W format = {}; + format.cbSize = sizeof(format); + format.dwMask = CFM_COLOR; + format.crTextColor = LogLineColor(line, m_Theme); + SendMessageW(m_RE_Log, EM_SETCHARFORMAT, SCF_SELECTION, reinterpret_cast(&format)); + + SendMessageW(m_RE_Log, EM_REPLACESEL, FALSE, reinterpret_cast(UTF8ToWide(line + + (endsLine ? "\r\n" : "")).c_str())); + + start = endsLine ? end + 1 : text.size(); + } + + SendMessageW(m_RE_Log, WM_VSCROLL, SB_BOTTOM, 0); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::ApplyDPI() +{ + m_UIFont = CreateUIFont(m_DPI); + m_MonospaceFont = CreateMonospaceFont(m_DPI); + + for (HWND child = GetWindow(m_Window, GW_CHILD); child != nullptr; child = GetWindow(child, GW_HWNDNEXT)) + { + HFONT const font = child == m_Mem_Detail || child == m_RE_Log ? m_MonospaceFont.Get() : m_UIFont.Get(); + SendMessageW(child, WM_SETFONT, reinterpret_cast(font), FALSE); + } + + CreateStatusImages(); + SendMessageW(m_TV_Tests, TVM_SETINDENT, static_cast(ScaleForDPI(19, m_DPI)), 0); + // e.g. the dark tree's check boxes and the menus' sizes, for the new DPI. + m_Theme.Refresh(); + + // The status bar's parts' widths, from the .dfm's panels; the last, the status message's, takes the rest of + // the bar, as VCL's TStatusBar gives its last panel. + std::vector rightEdges; + int right = 0; + + for (int const width : GUIStatusPanelWidths()) + { + right += ScaleForDPI(width, m_DPI); + rightEdges.push_back(right); + } + + if (!rightEdges.empty()) + rightEdges.back() = -1; + + SendMessageW(m_SB_Status, SB_SETPARTS, rightEdges.size(), reinterpret_cast(rightEdges.data())); + + // The Detail column's width is set by LayOut(), to fill the list. + SendMessageW(m_LV_Failures, LVM_SETCOLUMNWIDTH, 0, ScaleForDPI(70, m_DPI)); + SendMessageW(m_LV_Failures, LVM_SETCOLUMNWIDTH, 1, ScaleForDPI(240, m_DPI)); + + LayOut(); + // Every control too, in its new font: resizing one keeps what it had drawn and redraws only the part it + // gained, which would leave e.g. the filter label's old, smaller text showing. + RedrawWindow(m_Window, nullptr, nullptr, RDW_ERASE | RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::ApplyFilterBox + + The text matches as GUIFilterBoxFilter() describes. +*/ +void TWin32GUIMainWindow::ApplyFilterBox() +{ + m_TestList.SetVisibleFilter(GUIFilterBoxFilter(WindowText(m_Edt_Filter))); + + BuildTestTree(); + UpdateDetail(); + UpdateStatusBar(); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::ApplyPanelSizes(TGUILayout const& layout) +{ + // Kept within what the splitters allow, so each can still be dragged and nothing is squeezed out of view. + RECT client; + GetClientRect(m_Window, &client); + int const pixelsPerInch = static_cast(m_DPI); + + if (layout.TestTreeWidth.has_value()) + m_TestTreeWidth = ClampTestTreeWidth(ScaleFrom96(*layout.TestTreeWidth, pixelsPerInch), client.right, m_DPI); + + if (layout.DetailHeight.has_value()) + m_DetailHeight = ClampDetailHeight(ScaleFrom96(*layout.DetailHeight, pixelsPerInch), client.bottom, m_DPI); + + LayOut(); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::ApplyTheme + + Dark or light as View > Theme says, or with Use Windows Setting, as Windows' app mode does, but light while + a high contrast theme is on (see GUIThemeIsDark()). Not during a run, as in the VCL GUI runner; SetRunning() + calls it again once the run has ended. +*/ +void TWin32GUIMainWindow::ApplyTheme() +{ + if (m_Running) + { + m_ThemeChangePending = true; + return; + } + + m_ThemeChangePending = false; + bool const dark = GUIThemeIsDark(m_ThemeChoice, WindowsAppsUseDarkTheme(), WindowsHighContrast()); + + if (dark == m_Theme.IsDark()) + return; + + m_Theme.SetDark(dark); + // The tree's status dots are the dark theme's own colors while dark. + CreateStatusImages(); + RecolorLog(); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::BuildTestTree + + Rebuilds the tree from m_TestList: one node per group, in order, each with one child node per test, and + one node per suite, holding the nodes of the groups and suites in it. A group's and a suite's node show + just its own name. +*/ +void TWin32GUIMainWindow::BuildTestTree() +{ + { + // Each step redraws the tree once, when it's done: WM_SETREDRAW doesn't nest, so one step can't + // bracket the others. Nothing is painted until the window processes messages again anyway. + TRedrawScope const updating(m_TV_Tests); + // Each new node starts unchecked, which SyncTreeChecks() below then corrects. + TScopedFlag const syncing(m_SyncingChecks); + + SendMessageW(m_TV_Tests, TVM_DELETEITEM, 0, reinterpret_cast(TVI_ROOT)); + m_TestNodes.assign(m_TestList.Count(), nullptr); + + // Each node comes after its parent, so its parent's tree node already exists. + std::vector const nodes = BuildGUITree(m_TestList); + std::vector treeItems; + treeItems.reserve(nodes.size()); + + for (TGUITreeNode const& node : nodes) + { + std::wstring text = UTF8ToWide(node.Text); + + TVINSERTSTRUCTW insert = {}; + insert.hParent = node.Parent.has_value() ? treeItems[*node.Parent] : TVI_ROOT; + insert.hInsertAfter = TVI_LAST; + insert.item.mask = TVIF_TEXT | TVIF_PARAM; + insert.item.pszText = &text[0]; + insert.item.lParam = static_cast(EncodeGUINodeRef(node.Ref)); + + HTREEITEM const item = reinterpret_cast(SendMessageW(m_TV_Tests, TVM_INSERTITEMW, 0, + reinterpret_cast(&insert))); + + if (item == nullptr) + throw std::runtime_error("Couldn't add a node to the test tree"); + + treeItems.push_back(item); + + if (node.Ref.Kind == TGUINodeKind::Test) + m_TestNodes[node.Ref.Index] = item; + } + } + + SetTreeExpanded(true); + RefreshAllNodeImages(); + SyncTreeChecks(); + + // Expand All and Collapse All, e.g. while the filter hides every test. + UpdateCommands(); + + HTREEITEM const first = RelatedTreeItem(m_TV_Tests, nullptr, TVGN_ROOT); + + if (first != nullptr) + SendMessageW(m_TV_Tests, TVM_ENSUREVISIBLE, 0, reinterpret_cast(first)); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::CenterOnMonitor + + Gives the window the .dfm's client size at its DPI, centered on its monitor's work area, as the VCL GUI + runner's form opens when it has no saved layout. +*/ +void TWin32GUIMainWindow::CenterOnMonitor() +{ + RECT frame = { 0, 0, ScaleForDPI(TWin32GUIMainSizes::DefaultClientWidth, m_DPI), + ScaleForDPI(TWin32GUIMainSizes::DefaultClientHeight, m_DPI) }; + AdjustWindowRectExForDpi(&frame, static_cast(GetWindowLongPtrW(m_Window, GWL_STYLE)), TRUE, + static_cast(GetWindowLongPtrW(m_Window, GWL_EXSTYLE)), m_DPI); + + MONITORINFO monitor = {}; + monitor.cbSize = sizeof(monitor); + GetMonitorInfoW(MonitorFromWindow(m_Window, MONITOR_DEFAULTTONEAREST), &monitor); + + RECT const& work = monitor.rcWork; + int const width = frame.right - frame.left; + int const height = frame.bottom - frame.top; + int const left = work.left + (work.right - work.left - width) / 2; + int const top = work.top + (work.bottom - work.top - height) / 2; + + SetWindowPos(m_Window, nullptr, left > work.left ? left : work.left, top > work.top ? top : work.top, width, + height, SWP_NOZORDER | SWP_NOACTIVATE); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::Create() +{ + HINSTANCE const instance = GetModuleHandleW(nullptr); + WNDCLASSEXW windowClass = {}; + + if (GetClassInfoExW(instance, MainWindowClassName, &windowClass) == 0) + { + windowClass.cbSize = sizeof(windowClass); + windowClass.lpfnWndProc = &WindowProc; + windowClass.hInstance = instance; + // With no small icon given, Windows takes the title bar's from the same resource, at the size it needs. + windowClass.hIcon = MainIcon(); + windowClass.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512)); // IDC_ARROW + windowClass.hbrBackground = reinterpret_cast(COLOR_BTNFACE + 1); + windowClass.lpszClassName = MainWindowClassName; + + if (RegisterClassExW(&windowClass) == 0) + throw std::runtime_error("Couldn't register the main window class"); + } + + // The rich edit control the log uses comes from this library. + m_RichEditLibrary = LoadLibraryW(L"Msftedit.dll"); + + if (m_RichEditLibrary == nullptr) + throw std::runtime_error("Couldn't load Msftedit.dll, which has the log's rich edit control"); + + CreateMenuBar(); + + if (CreateWindowExW(WS_EX_CONTROLPARENT, MainWindowClassName, UTF8ToWide(Title).c_str(), + WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, + m_MM_Main, instance, this) == nullptr) + { + throw std::runtime_error("Couldn't create the main window"); + } + + m_Theme.AttachWindow(m_Window, m_MM_Main); + m_DPI = WindowDPI(m_Window); + m_TestTreeWidth = ScaleForDPI(TWin32GUIMainSizes::DefaultTestTreeWidth, m_DPI); + m_DetailHeight = ScaleForDPI(TWin32GUIMainSizes::DefaultDetailHeight, m_DPI); + + CreateControls(); + CreateTooltips(); + CenterOnMonitor(); + ApplyDPI(); + UpdateCommands(); + + // The .dfm's action shortcuts: Run Selected F9, Copy Details Ctrl+Shift+C, Filter Ctrl+F. + ACCEL accelerators[] = { + { FVIRTKEY, VK_F9, static_cast(EWin32GUICommand::RunSelected) }, + { FVIRTKEY | FCONTROL | FSHIFT, 'C', static_cast(EWin32GUICommand::CopyDetails) }, + { FVIRTKEY | FCONTROL, 'F', static_cast(EWin32GUICommand::FocusFilter) }, + }; + m_Accelerators = CreateAcceleratorTableW(accelerators, static_cast(sizeof(accelerators) / sizeof(accelerators[0]))); + + if (m_Accelerators == nullptr) + throw std::runtime_error("Couldn't create the hot keys"); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::CreateChild + + Creates a child control of the main window, at no size until LayOut() places it. Throws + std::runtime_error if it can't be created. +*/ +HWND TWin32GUIMainWindow::CreateChild(DWORD exStyle, wchar_t const* className, wchar_t const* text, DWORD style, + WORD id) +{ + HWND const control = CreateWindowExW(exStyle, className, text, WS_CHILD | WS_VISIBLE | style, 0, 0, 0, 0, m_Window, + reinterpret_cast(static_cast(id)), GetModuleHandleW(nullptr), nullptr); + + if (control == nullptr) + throw std::runtime_error("Couldn't create a control"); + + return control; +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::CreateControls + + Creates the .dfm's controls, empty. They're created in tab order, which is also their z-order, the first + on top; so the results pages come before the tab control they sit on, which would otherwise hide them. +*/ +void TWin32GUIMainWindow::CreateControls() +{ + for (TToolbarButton const& button : ToolbarButtons) + { + this->*button.Control = CreateChild(0, L"BUTTON", button.Caption, WS_TABSTOP | BS_PUSHBUTTON, + static_cast(button.Command)); + } + + // Created just before the filter box, so its hot key moves the focus there, as FocusControl does. + m_Lbl_Filter = CreateChild(0, L"STATIC", L"F&ilter:", SS_LEFT); + m_Edt_Filter = CreateChild(WS_EX_CLIENTEDGE, L"EDIT", L"", WS_TABSTOP | ES_AUTOHSCROLL); + SendMessageW(m_Edt_Filter, EM_SETCUEBANNER, FALSE, reinterpret_cast(L"e.g. String or *Handler*.Run_*")); + m_Theme.Attach(m_Edt_Filter, EWin32GUIThemedControl::Edit); + + m_TV_Tests = CreateChild(WS_EX_CLIENTEDGE, WC_TREEVIEWW, L"", WS_TABSTOP | TVS_HASBUTTONS | TVS_HASLINES | + TVS_LINESATROOT | TVS_SHOWSELALWAYS); + // The partial state, for a group or suite with only some of its tests checked, has to be asked for before + // the check boxes, which create the check box images. The check boxes only once the tree exists, as + // TVS_CHECKBOXES' documentation says, or they may not show their state. Painted through a buffer, so it + // doesn't flicker. + SendMessageW(m_TV_Tests, TVM_SETEXTENDEDSTYLE, TVS_EX_PARTIALCHECKBOXES | TVS_EX_DOUBLEBUFFER, + TVS_EX_PARTIALCHECKBOXES | TVS_EX_DOUBLEBUFFER); + SetWindowLongPtrW(m_TV_Tests, GWL_STYLE, GetWindowLongPtrW(m_TV_Tests, GWL_STYLE) | TVS_CHECKBOXES); + // Drawn like File Explorer's tree (arrows to expand, a light selection) while light, as the VCL draws its + // trees; see TWin32GUITheme. + m_Theme.Attach(m_TV_Tests, EWin32GUIThemedControl::TreeView); + m_Spl_Tests.Create(m_Window, TWin32GUISplitter::EOrientation::Vertical, [this](int position) + { + OnTestsSplitterDrag(position); + }); + m_Theme.Attach(m_Spl_Tests.Handle(), EWin32GUIThemedControl::Splitter); + + m_Mem_Detail = CreateChild(WS_EX_CLIENTEDGE, L"EDIT", L"", WS_TABSTOP | WS_VSCROLL | ES_MULTILINE | ES_READONLY | + ES_AUTOVSCROLL); + SendMessageW(m_Mem_Detail, EM_SETLIMITTEXT, 0, 0); // Otherwise 32K characters at most. + m_Theme.Attach(m_Mem_Detail, EWin32GUIThemedControl::Edit); + m_Spl_Detail.Create(m_Window, TWin32GUISplitter::EOrientation::Horizontal, [this](int position) + { + OnDetailSplitterDrag(position); + }); + m_Theme.Attach(m_Spl_Detail.Handle(), EWin32GUIThemedControl::Splitter); + + m_LV_Failures = CreateChild(WS_EX_CLIENTEDGE, WC_LISTVIEWW, L"", WS_TABSTOP | LVS_REPORT | LVS_SINGLESEL | + LVS_SHOWSELALWAYS); + SendMessageW(m_LV_Failures, LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER, + LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER); + + wchar_t const* const columns[] = { L"Result", L"Test", L"Detail" }; + + for (int i = 0; i < 3; ++i) + { + LVCOLUMNW column = {}; + column.mask = LVCF_TEXT; + column.pszText = const_cast(columns[i]); + SendMessageW(m_LV_Failures, LVM_INSERTCOLUMNW, static_cast(i), reinterpret_cast(&column)); + } + + // As the VCL draws its list views while light; it also keeps a selected row's text in its result's color. + m_Theme.Attach(m_LV_Failures, EWin32GUIThemedControl::ListView); + + m_RE_Log = CreateChild(WS_EX_CLIENTEDGE, MSFTEDIT_CLASS, L"", WS_TABSTOP | WS_VSCROLL | WS_HSCROLL | + ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_NOHIDESEL); + SendMessageW(m_RE_Log, EM_EXLIMITTEXT, 0, 0x7FFFFFFE); // Otherwise 32K characters at most. + SendMessageW(m_RE_Log, EM_SETTARGETDEVICE, 0, 1); // No word wrap. + ShowWindow(m_RE_Log, SW_HIDE); + m_Theme.Attach(m_RE_Log, EWin32GUIThemedControl::RichEdit); + + m_PC_Results = CreateChild(0, WC_TABCONTROLW, L"", WS_TABSTOP | WS_CLIPSIBLINGS); + // "&&" for a literal '&', as in the .dfm: a tab control takes '&' as a hot key's prefix. + wchar_t const* const pages[] = { L"Failures && Skips", L"Log" }; + + for (int i = 0; i < 2; ++i) + { + TCITEMW item = {}; + item.mask = TCIF_TEXT; + item.pszText = const_cast(pages[i]); + SendMessageW(m_PC_Results, TCM_INSERTITEMW, static_cast(i), reinterpret_cast(&item)); + } + + m_Theme.Attach(m_PC_Results, EWin32GUIThemedControl::TabControl); + + m_PB_Progress = CreateChild(0, PROGRESS_CLASSW, L"", 0); + m_Theme.Attach(m_PB_Progress, EWin32GUIThemedControl::ProgressBar); + // Placed and sized by LayOut(), at the .dfm's height, rather than by itself at its font's. + m_SB_Status = CreateChild(0, STATUSCLASSNAMEW, L"", SBARS_SIZEGRIP | CCS_NORESIZE | CCS_NOPARENTALIGN); + m_Theme.Attach(m_SB_Status, EWin32GUIThemedControl::StatusBar); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::CreateMenuBar + + The .dfm's main menu, with each action's caption and shortcut. +*/ +void TWin32GUIMainWindow::CreateMenuBar() +{ + m_MM_Main = CreateMenu(); + + if (m_MM_Main == nullptr) + throw std::runtime_error("Couldn't create the menu"); + + HMENU const file = AppendSubmenu(m_MM_Main, L"&File"); + AppendCommand(file, EWin32GUICommand::ExportJUnitReport, L"&Export JUnit Report..."); + AppendMenuW(file, MF_SEPARATOR, 0, nullptr); + AppendCommand(file, EWin32GUICommand::Exit, L"E&xit"); + + HMENU const run = AppendSubmenu(m_MM_Main, L"&Run"); + AppendCommand(run, EWin32GUICommand::RunSelected, L"&Run Selected\tF9"); + AppendCommand(run, EWin32GUICommand::RunFailed, L"Run &Failed"); + AppendCommand(run, EWin32GUICommand::Stop, L"&Stop"); + + HMENU const tests = AppendSubmenu(m_MM_Main, L"&Tests"); + AppendCommand(tests, EWin32GUICommand::SelectAll, L"Select &All"); + AppendCommand(tests, EWin32GUICommand::SelectNone, L"Select &None"); + AppendCommand(tests, EWin32GUICommand::SelectFailed, L"Select Fai&led"); + AppendCommand(tests, EWin32GUICommand::FocusFilter, L"&Filter\tCtrl+F"); + AppendMenuW(tests, MF_SEPARATOR, 0, nullptr); + AppendCommand(tests, EWin32GUICommand::CopyDetails, L"&Copy Details\tCtrl+Shift+C"); + + HMENU const view = AppendSubmenu(m_MM_Main, L"&View"); + AppendCommand(view, EWin32GUICommand::ExpandAll, L"&Expand All"); + AppendCommand(view, EWin32GUICommand::CollapseAll, L"&Collapse All"); + AppendMenuW(view, MF_SEPARATOR, 0, nullptr); + AppendCommand(view, EWin32GUICommand::ResetLayout, L"&Reset Layout"); + AppendMenuW(view, MF_SEPARATOR, 0, nullptr); + HMENU const theme = AppendSubmenu(view, L"&Theme"); + AppendCommand(theme, EWin32GUICommand::ThemeWindows, L"Use &Windows Setting"); + AppendCommand(theme, EWin32GUICommand::ThemeLight, L"&Light"); + AppendCommand(theme, EWin32GUICommand::ThemeDark, L"&Dark"); + AppendMenuW(theme, MF_SEPARATOR, 0, nullptr); + // Filled with the theme files each time it opens; Carbon until then. + m_MI_ViewDarkTheme = AppendSubmenu(theme, L"Dark &Theme"); + AppendMenuW(m_MI_ViewDarkTheme, MF_STRING, FirstDarkThemeCommand, L"&Carbon"); + AppendMenuW(m_MI_ViewDarkTheme, MF_SEPARATOR, 0, nullptr); + AppendCommand(m_MI_ViewDarkTheme, EWin32GUICommand::SaveThemeTemplate, L"&Save Theme Template..."); + AppendCommand(m_MI_ViewDarkTheme, EWin32GUICommand::OpenThemesFolder, L"&Open Themes Folder"); + + HMENU const options = AppendSubmenu(m_MM_Main, L"&Options"); + AppendCommand(options, EWin32GUICommand::ShuffleOrder, L"Run in &Shuffled Order"); + AppendCommand(options, EWin32GUICommand::ShuffleSeed, L"Shuffle S&eed..."); + + HMENU const help = AppendSubmenu(m_MM_Main, L"&Help"); + AppendCommand(help, EWin32GUICommand::CommandLineOptions, L"&Command Line Options..."); + AppendCommand(help, EWin32GUICommand::About, L"&About..."); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::CreateStatusImages + + Draws one colored dot per TGUITestStatus into a new image list for the test tree, in the enum's order, so + a status converts directly to its image index. Drawn at runtime, scaled to m_DPI, as the VCL GUI runner + draws its own, rather than stored as images. In the dark theme's colors while dark, and else in Carbon's, + which are the light look's too. +*/ +void TWin32GUIMainWindow::CreateStatusImages() +{ + TWin32GUIPalette const palette = m_Theme.IsDark() ? m_Theme.Palette() : CarbonPalette(); + + // In TGUITestStatus order: NotRun, Running, Passed, Failed, Skipped. + COLORREF const colors[] = { + palette.TreeStatusNotRun, + palette.TreeStatusRunning, + palette.TreeStatusPassed, + palette.TreeStatusFailed, + palette.TreeStatusSkipped, + }; + // Each dot's background, left out by ImageList_AddMasked(): a color none of the dots has, as a theme file may + // give one any color. + COLORREF transparent = RGB(255, 0, 255); + + for (COLORREF const candidate : { RGB(255, 0, 255), RGB(0, 255, 255), RGB(255, 255, 0), RGB(1, 2, 3), + RGB(254, 0, 254), RGB(0, 254, 254) }) + { + transparent = candidate; + + if (std::find(std::begin(colors), std::end(colors), candidate) == std::end(colors)) + break; + } + + int const size = ScaleForDPI(16, m_DPI); + int const inset = ScaleForDPI(3, m_DPI); + + HIMAGELIST const images = ImageList_Create(size, size, ILC_COLOR24 | ILC_MASK, 5, 0); + + if (images == nullptr) + throw std::runtime_error("Couldn't create the test tree's status images"); + + HDC const screen = GetDC(nullptr); + HDC const context = CreateCompatibleDC(screen); + HBITMAP const bitmap = CreateCompatibleBitmap(screen, size, size); + ReleaseDC(nullptr, screen); + + bool drawn = context != nullptr && bitmap != nullptr; + + if (drawn) + { + HGDIOBJ const oldPen = SelectObject(context, GetStockObject(DC_PEN)); + HGDIOBJ const oldBrush = SelectObject(context, GetStockObject(DC_BRUSH)); + RECT const whole = { 0, 0, size, size }; + + for (COLORREF const color : colors) + { + HGDIOBJ const oldBitmap = SelectObject(context, bitmap); + + SetDCBrushColor(context, transparent); + FillRect(context, &whole, static_cast(GetStockObject(DC_BRUSH))); + SetDCBrushColor(context, color); + SetDCPenColor(context, color); + Ellipse(context, inset, inset, size - inset, size - inset); + + // A bitmap selected into a device context can't be added to an image list. + SelectObject(context, oldBitmap); + drawn = ImageList_AddMasked(images, bitmap, transparent) >= 0 && drawn; + } + + SelectObject(context, oldBrush); + SelectObject(context, oldPen); + } + + if (bitmap != nullptr) + DeleteObject(bitmap); + + if (context != nullptr) + DeleteDC(context); + + if (!drawn) + { + ImageList_Destroy(images); + throw std::runtime_error("Couldn't draw the test tree's status images"); + } + + SendMessageW(m_TV_Tests, TVM_SETIMAGELIST, TVSIL_NORMAL, reinterpret_cast(images)); + + if (m_StatusImages != nullptr) + ImageList_Destroy(m_StatusImages); + + m_StatusImages = images; +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::CreateTooltips + + The VCL shows a control's Hint as its tooltip while the window is active, and not for a disabled control, + which a Windows tooltip that subclasses its control doesn't either, since the control gets no mouse + messages. +*/ +void TWin32GUIMainWindow::CreateTooltips() +{ + m_TT_Hints = CreateWindowExW(WS_EX_TOPMOST, TOOLTIPS_CLASSW, nullptr, WS_POPUP | TTS_NOPREFIX, CW_USEDEFAULT, + CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, m_Window, nullptr, GetModuleHandleW(nullptr), nullptr); + + if (m_TT_Hints == nullptr) + throw std::runtime_error("Couldn't create the tooltips"); + + m_Theme.Attach(m_TT_Hints, EWin32GUIThemedControl::Tooltip); + + auto const addTool = [this](HWND control, wchar_t const* text) + { + TTTOOLINFOW tool = {}; + tool.cbSize = sizeof(tool); + tool.uFlags = TTF_IDISHWND | TTF_SUBCLASS; + tool.hwnd = m_Window; + tool.uId = reinterpret_cast(control); + tool.lpszText = const_cast(text); + SendMessageW(m_TT_Hints, TTM_ADDTOOLW, 0, reinterpret_cast(&tool)); + }; + + for (TToolbarButton const& button : ToolbarButtons) + addTool(this->*button.Control, button.Hint); + + addTool(m_Edt_Filter, L"Show only tests whose \"Group.Test\" name contains this text, which may use the * and ? " + L"wildcards of --filter; not case-sensitive"); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::CurrentLayout + + The window's restored bounds come from GetWindowPlacement(), so a maximized (or minimized) window still + saves the size it restores to. +*/ +TGUILayout TWin32GUIMainWindow::CurrentLayout() const +{ + TGUILayout layout; + int const pixelsPerInch = static_cast(m_DPI); + + layout.DetailHeight = ScaleTo96(m_DetailHeight, pixelsPerInch); + layout.TestTreeWidth = ScaleTo96(m_TestTreeWidth, pixelsPerInch); + + WINDOWPLACEMENT placement = {}; + placement.length = sizeof(placement); + + if (GetWindowPlacement(m_Window, &placement) != 0) + { + MONITORINFO const primary = PrimaryMonitorInfo(); + TGUIRect const bounds = WorkspaceToScreen(ToGUIRect(placement.rcNormalPosition), ToGUIRect(primary.rcMonitor), + ToGUIRect(primary.rcWork)); + + TGUIWindowBounds window; + window.Height = ScaleTo96(bounds.Height(), pixelsPerInch); + window.Left = bounds.Left; + window.Top = bounds.Top; + window.Width = ScaleTo96(bounds.Width(), pixelsPerInch); + layout.Window = window; + + layout.Maximized = placement.showCmd == SW_SHOWMAXIMIZED || + (placement.showCmd == SW_SHOWMINIMIZED && (placement.flags & WPF_RESTORETOMAXIMIZED) != 0); + } + + return layout; +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::DoCommand + + A disabled command does nothing, since a hot key still sends it when its menu item and button are disabled. +*/ +void TWin32GUIMainWindow::DoCommand(EWin32GUICommand command) +{ + if (!IsCommandEnabled(m_CommandStates, command)) + return; + + RunGuarded(CommandName(command), [this, command]() + { + switch (command) + { + case EWin32GUICommand::About: + ShowTextDialog(m_Window, m_Theme, std::string("About ") + Title, GUIAboutText("Win32 GUI runner")); + break; + + case EWin32GUICommand::CollapseAll: + SetTreeExpanded(false); + // A selected node that's now hidden passes the selection to the top-level node above it. + UpdateDetail(); + break; + + case EWin32GUICommand::CommandLineOptions: + ShowTextDialog(m_Window, m_Theme, std::string(Title) + " - Command Line Options", + TGUICommandLine::HelpText()); + break; + + case EWin32GUICommand::CopyDetails: + SetClipboardText(m_Window, WindowText(m_Mem_Detail)); + m_StatusMessage = "Details copied to the clipboard."; + UpdateStatusBar(); + break; + + case EWin32GUICommand::Exit: + PostMessageW(m_Window, WM_CLOSE, 0, 0); + break; + + case EWin32GUICommand::ExpandAll: + { + SetTreeExpanded(true); + HTREEITEM const selected = SelectedTreeItem(); + + if (selected != nullptr) + SendMessageW(m_TV_Tests, TVM_ENSUREVISIBLE, 0, reinterpret_cast(selected)); + + break; + } + + case EWin32GUICommand::ExportJUnitReport: + ExportJUnitReport(); + break; + + case EWin32GUICommand::FocusFilter: + SetFocus(m_Edt_Filter); + SendMessageW(m_Edt_Filter, EM_SETSEL, 0, -1); + break; + + case EWin32GUICommand::OpenThemesFolder: + CreateFolderPath(GUIThemesFolderPath()); + OpenFolderInExplorer(m_Window, GUIThemesFolderPath()); + break; + + case EWin32GUICommand::ResetLayout: + ResetLayout(); + break; + + case EWin32GUICommand::RunFailed: + if (m_TestList.StatusCount(TGUITestStatus::Failed) == 0) + break; + + // Built before RunTests() resets every result, while the failures are still known. + RunTests(m_TestList.FailedFilter(), "the previous run's failures"); + break; + + case EWin32GUICommand::RunSelected: + if (m_TestList.CheckedCount() == 0) + { + m_StatusMessage = "No tests are checked."; + UpdateStatusBar(); + break; + } + + RunCheckedTests(); + break; + + case EWin32GUICommand::SaveThemeTemplate: + SaveThemeTemplate(); + break; + + case EWin32GUICommand::SelectAll: + m_TestList.SetAllChecked(true); + SyncTreeChecks(); + break; + + case EWin32GUICommand::SelectFailed: + m_TestList.CheckOnlyFailed(); + SyncTreeChecks(); + break; + + case EWin32GUICommand::SelectNone: + m_TestList.SetAllChecked(false); + SyncTreeChecks(); + break; + + case EWin32GUICommand::ShuffleOrder: + m_Shuffle.SetEnabled(!m_Shuffle.Enabled()); + ShowShuffleState(); + break; + + case EWin32GUICommand::ShuffleSeed: + if (ShowShuffleSeedDialog(m_Window, m_Theme, m_Shuffle)) + ShowShuffleState(); + + break; + + case EWin32GUICommand::Stop: + m_Observer.RequestStop(); + m_StatusMessage = "Stopping after the current test..."; + UpdateStatusBar(); + break; + + case EWin32GUICommand::ThemeDark: + SetTheme(EGUITheme::Dark); + break; + + case EWin32GUICommand::ThemeLight: + SetTheme(EGUITheme::Light); + break; + + case EWin32GUICommand::ThemeWindows: + SetTheme(EGUITheme::Windows); + break; + + default: + break; + } + }); +} +//--------------------------------------------------------------------------- +int TWin32GUIMainWindow::ExitCode() const +{ + return m_Options.ExitAfterRun ? m_LastRunExitCode : ExitCode_Success; +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::ExportJUnitReport + + Saves the latest run's JUnit report, the same one --report-junit writes, e.g. when --report-junit wasn't + given. The dialog starts at --report-junit's path if there is one, and then at the last one chosen. +*/ +void TWin32GUIMainWindow::ExportJUnitReport() +{ + if (m_Running || !m_LastRunJUnitTestCases.has_value()) + return; + + if (m_ExportJUnitReportPath.empty() && !m_Options.CLI.JunitReportPath.empty()) + { + m_ExportJUnitReportPath = ExpandFileName(m_Options.CLI.JunitReportPath); + } + else if (m_ExportJUnitReportPath.empty()) + { + // e.g. "ASWUnitTests_Win32_GUI_JUnit.xml", in the dialog's default folder. + m_ExportJUnitReportPath = GUIDefaultJUnitReportFileName(ExeFileName()); + } + + // The .dfm's SD_JUnitReport: its title, filter and default extension. + std::optional const path = AskSaveFilePath(m_Window, L"Export JUnit Report", + L"JUnit XML reports (*.xml)\0*.xml\0All files (*.*)\0*.*\0", L"xml", m_ExportJUnitReportPath); + + if (!path.has_value()) + return; // Canceled. + + m_ExportJUnitReportPath = *path; + + std::string error; + + if (!WriteJUnitReport(m_ExportJUnitReportPath, error)) + { + std::string const message = "The JUnit report couldn't be written to:\n" + m_ExportJUnitReportPath + "\n\n" + + error; + ShowMessageDialog(m_Window, m_Theme, Title, message, EWin32GUIMessageIcon::Error); + return; + } + + m_StatusMessage = "JUnit report exported."; + UpdateStatusBar(); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::FailRunOnUnhandledException(std::string const& description) +{ + m_Observer.Flush(); + + // The test that was running when the exception escaped never finished, so it's the one that failed. + if (m_CurrentTestIndex.has_value()) + { + m_TestList.SetResult(*m_CurrentTestIndex, + GUIUnhandledExceptionRecord(m_TestList.Test(*m_CurrentTestIndex), description), std::string()); + UpdateTestNodeImages(*m_CurrentTestIndex); + AddFailureItem(*m_CurrentTestIndex); + m_CurrentTestIndex.reset(); + } + + SendMessageW(m_PB_Progress, PBM_SETSTATE, PBST_ERROR, 0); + m_StatusMessage = GUIUnhandledExceptionStatus(description); + AppendLog(m_StatusMessage + "\n"); + UpdateDetail(); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::FillDarkThemeMenu + + The built-in themes first, Carbon at the top, then the theme files in GUIThemesFolderPath() (see + GUIThemeNames()), so a file added or removed since shows at once, then Save Theme Template and Open Themes + Folder; the chosen one has the radio check. A chosen theme whose file is missing is still listed, so choosing + it again reports why it isn't used. +*/ +void TWin32GUIMainWindow::FillDarkThemeMenu() +{ + std::vector names; + + for (TWin32GUIBuiltInTheme const& theme : BuiltInDarkThemes()) + names.push_back(theme.Name); + + std::size_t const builtInCount = names.size(); + std::vector const files = GUIThemeNames(FileNamesInFolder(GUIThemesFolderPath()), ".ini", names); + names.insert(names.end(), files.begin(), files.end()); + + // Windows' file names ignore case, so a name saved in another case is still that file's. + auto const sameName = [](std::string const& a, std::string const& b) + { + std::wstring const wideA = UTF8ToWide(a); + std::wstring const wideB = UTF8ToWide(b); + + return CompareStringOrdinal(wideA.c_str(), static_cast(wideA.size()), wideB.c_str(), + static_cast(wideB.size()), TRUE) == CSTR_EQUAL; + }; + + std::vector::const_iterator chosen = std::find_if(names.begin(), names.end(), + [this, &sameName](std::string const& name) + { + return sameName(name, m_DarkTheme); + }); + + if (chosen == names.end()) + chosen = names.insert(names.end(), m_DarkTheme); + + std::size_t const chosenIndex = static_cast(chosen - names.begin()); + + if (names.size() > MaxDarkThemes) + names.erase(names.begin() + static_cast(MaxDarkThemes), names.end()); + + while (GetMenuItemCount(m_MI_ViewDarkTheme) > 0) + DeleteMenu(m_MI_ViewDarkTheme, 0, MF_BYPOSITION); + + UINT const enabled = m_CommandStates.Theme ? MF_ENABLED : MF_GRAYED; + // A built-in theme's accelerator, other than Save Theme Template's S and Open Themes Folder's O; a theme + // file's name has none. + std::vector const builtInCaptions = GUIMenuItemTextsWithAccelerators( + std::vector(names.begin(), names.begin() + static_cast(builtInCount)), "so"); + + for (std::size_t i = 0; i < names.size(); ++i) + { + // A line between the built-in themes and the files. + if (i == builtInCount) + AppendMenuW(m_MI_ViewDarkTheme, MF_SEPARATOR, 0, nullptr); + + UINT const id = static_cast(FirstDarkThemeCommand + i); + std::wstring const caption = UTF8ToWide(i < builtInCount ? builtInCaptions[i] : GUIMenuItemText(names[i])); + AppendMenuW(m_MI_ViewDarkTheme, MF_STRING | enabled, id, caption.c_str()); + + // Only it, as nothing else is checked yet, so the range needn't span the line above the files. + if (i == chosenIndex) + CheckMenuRadioItem(m_MI_ViewDarkTheme, id, id, id, MF_BYCOMMAND); + } + + AppendMenuW(m_MI_ViewDarkTheme, MF_SEPARATOR, 0, nullptr); + AppendMenuW(m_MI_ViewDarkTheme, MF_STRING | enabled, static_cast(EWin32GUICommand::SaveThemeTemplate), + L"&Save Theme Template..."); + AppendMenuW(m_MI_ViewDarkTheme, MF_STRING | enabled, static_cast(EWin32GUICommand::OpenThemesFolder), + L"&Open Themes Folder"); + + m_DarkThemeNames = names; + m_Theme.RefreshMenu(m_MI_ViewDarkTheme); +} +//--------------------------------------------------------------------------- +HWND TWin32GUIMainWindow::Handle() const +{ + return m_Window; +} +//--------------------------------------------------------------------------- +LRESULT TWin32GUIMainWindow::HandleMessage(UINT message, WPARAM wParam, LPARAM lParam) +{ + HWND const window = m_Window; // Still needed after WM_NCDESTROY clears m_Window. + + switch (message) + { + case WM_ACTIVATE: + // The focus goes back to the control that had it, as in a dialog. + if (LOWORD(wParam) == WA_INACTIVE) + { + HWND const focus = GetFocus(); + + if (focus != nullptr && IsChild(m_Window, focus)) + m_FocusedControl = focus; + } + else if (HIWORD(wParam) == 0) // Not minimized + { + // Unless it's been disabled since, e.g. by a run, when the next control takes it. + HWND focus = m_FocusedControl; + + if (focus == nullptr || IsWindowEnabled(focus) == FALSE) + focus = NextTabStop(focus, true); + + SetFocus(focus); + } + + return 0; + + case WM_CLOSE: + OnClose(); + return 0; + + case WM_COMMAND: + { + WORD const id = LOWORD(wParam); + + if (id >= static_cast(EWin32GUICommand::About) && + id <= static_cast(EWin32GUICommand::ThemeWindows)) + { + DoCommand(static_cast(id)); + return 0; + } + + std::size_t const darkTheme = static_cast(id) - FirstDarkThemeCommand; + + if (id >= FirstDarkThemeCommand && darkTheme < m_DarkThemeNames.size()) + { + // As DoCommand() does, nothing while the Theme commands are disabled. + if (!m_CommandStates.Theme) + return 0; + + std::string const name = m_DarkThemeNames[darkTheme]; + + RunGuarded("Theme > Dark Theme", [this, &name]() + { + SetDarkTheme(name); + }); + return 0; + } + + if (reinterpret_cast(lParam) == m_Edt_Filter && HIWORD(wParam) == EN_CHANGE) + { + if (m_SettingFilterText) + return 0; // Start() builds the tree once it has set the text. + + RunGuarded("the filter box", [this]() + { + ApplyFilterBox(); + }); + return 0; + } + + break; + } + + case WM_DESTROY: + if (m_Closing) + PostQuitMessage(0); + + return 0; + + case WM_DPICHANGED: + OnDPIChanged(HIWORD(wParam), *reinterpret_cast(lParam)); + return 0; + + case WM_INITMENUPOPUP: + if (reinterpret_cast(wParam) == m_MI_ViewDarkTheme) + { + RunGuarded("the Dark Theme menu", [this]() + { + FillDarkThemeMenu(); + }); + return 0; + } + + break; + + case WM_NCDESTROY: + m_Window = nullptr; + break; + + case WM_NOTIFY: + { + NMHDR const* const header = reinterpret_cast(lParam); + + if (header->hwndFrom == m_PC_Results && header->code == TCN_SELCHANGE) + { + ShowResultsPage(); + return 0; + } + + if (header->hwndFrom == m_LV_Failures) + { + // Not RunGuarded(): this runs while painting, where showing an error would only cause more + // painting. + if (header->code == NM_CUSTOMDRAW) + return OnFailuresCustomDraw(*reinterpret_cast(lParam)); + + if (header->code == NM_DBLCLK) + { + RunGuarded("the Failures & Skips list", [this]() + { + OnFailuresDoubleClick(); + }); + return 0; + } + } + + if (header->hwndFrom == m_TV_Tests) + { + if (header->code == TVN_ITEMCHANGINGW) + return OnTreeItemChanging(*reinterpret_cast(lParam)) ? TRUE : FALSE; + + if (header->code == TVN_SELCHANGEDW) + { + RunGuarded("the test tree", [this]() + { + UpdateDetail(); + }); + return 0; + } + } + + break; + } + + case WM_SETTINGCHANGE: + // With Use Windows Setting, Windows' app mode or high contrast changing changes the theme. + if (IsThemeSettingChange(wParam, lParam)) + { + RunGuarded("following Windows' theme", [this]() + { + ApplyTheme(); + }); + } + + break; + + case WM_SIZE: + if (wParam != SIZE_MINIMIZED) + LayOut(); + + return 0; + + case WM_SYSCOLORCHANGE: + // The common controls need it passed on, and the light theme's colors are the system's. + for (HWND child = GetWindow(m_Window, GW_CHILD); child != nullptr; child = GetWindow(child, GW_HWNDNEXT)) + SendMessageW(child, WM_SYSCOLORCHANGE, wParam, lParam); + + RunGuarded("the system colors' change", [this]() + { + m_Theme.Refresh(); + RecolorLog(); + }); + return 0; + + default: + if (message == RunQueuedMessage) + { + RunQueued(); + return 0; + } + + break; + } + + return DefWindowProcW(window, message, wParam, lParam); +} +//--------------------------------------------------------------------------- +bool TWin32GUIMainWindow::IsToolbarButton(HWND control) const +{ + for (TToolbarButton const& button : ToolbarButtons) + { + if (control != nullptr && control == this->*button.Control) + return true; + } + + return false; +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::LayOut + + Places every control for the window's client size: the toolbar's at the .dfm's positions, the rest by + LayOutMainWindow(), all at once. +*/ +void TWin32GUIMainWindow::LayOut() +{ + if (m_SB_Status == nullptr) + return; // WM_SIZE during CreateWindowExW(), before the controls exist. + + RECT client; + GetClientRect(m_Window, &client); + + TWin32GUIMainLayout const layout = LayOutMainWindow(client.right, client.bottom, m_TestTreeWidth, m_DetailHeight, + m_DPI); + + // The results pages fill the tab control's display area. + RECT pages = { layout.Results.Left, layout.Results.Top, layout.Results.Right, layout.Results.Bottom }; + SendMessageW(m_PC_Results, TCM_ADJUSTRECT, FALSE, reinterpret_cast(&pages)); + + auto const scaled = [this](int value) + { + return ScaleForDPI(value, m_DPI); + }; + HDWP positions = BeginDeferWindowPos(20); + auto const place = [&positions](HWND control, int left, int top, int width, int height) + { + if (positions != nullptr) + { + positions = DeferWindowPos(positions, control, nullptr, left, top, width < 0 ? 0 : width, + height < 0 ? 0 : height, SWP_NOZORDER | SWP_NOACTIVATE); + } + }; + auto const placeRect = [&place](HWND control, TGUIRect const& rect) + { + place(control, rect.Left, rect.Top, rect.Width(), rect.Height()); + }; + + for (TToolbarButton const& button : ToolbarButtons) + place(this->*button.Control, scaled(button.Left), scaled(8), scaled(button.Width), scaled(26)); + + place(m_Lbl_Filter, scaled(762), scaled(13), scaled(29), scaled(15)); + place(m_Edt_Filter, scaled(800), scaled(9), scaled(192), scaled(23)); + placeRect(m_TV_Tests, layout.TestTree); + placeRect(m_Spl_Tests.Handle(), layout.TestsSplitter); + placeRect(m_Mem_Detail, layout.Detail); + placeRect(m_Spl_Detail.Handle(), layout.DetailSplitter); + placeRect(m_PC_Results, layout.Results); + place(m_LV_Failures, pages.left, pages.top, pages.right - pages.left, pages.bottom - pages.top); + place(m_RE_Log, pages.left, pages.top, pages.right - pages.left, pages.bottom - pages.top); + placeRect(m_PB_Progress, layout.ProgressBar); + placeRect(m_SB_Status, layout.StatusBar); + + if (positions != nullptr) + EndDeferWindowPos(positions); + + // The Detail column fills the rest of the list, as its AutoSize does in the .dfm. + SendMessageW(m_LV_Failures, LVM_SETCOLUMNWIDTH, 2, LVSCW_AUTOSIZE_USEHEADER); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::LoadLayout + + Called from Start(), before the window is first shown. The panel sizes are applied once it's showing, + since a window about to open maximized doesn't have the size they have to fit into until then. +*/ +void TWin32GUIMainWindow::LoadLayout() +{ + TGUILayout layout; + + try + { + std::string const path = GUILayoutFilePath(); + + if (!FileExists(path)) + return; + + layout = LoadGUILayout(TGUIINIDocument(ReadFileBytes(path))); + } + catch (...) + { + return; // A layout that can't be read only means the window opens with its defaults. + } + + if (layout.Window.has_value()) + { + TGUIWindowBounds const& window = *layout.Window; + int const pixelsPerInch = static_cast(m_DPI); + TGUIRect const bounds(window.Left, window.Top, window.Left + ScaleFrom96(window.Width, pixelsPerInch), + window.Top + ScaleFrom96(window.Height, pixelsPerInch)); + + // Not where it can't be reached, e.g. on a monitor that's since been disconnected. Set as the window + // placement's normal position, which is also where a window that opens maximized restores to. + WINDOWPLACEMENT placement = {}; + placement.length = sizeof(placement); + + if (IsWindowReachable(bounds, MonitorWorkAreas()) && GetWindowPlacement(m_Window, &placement) != 0) + { + MONITORINFO const primary = PrimaryMonitorInfo(); + TGUIRect const normal = ScreenToWorkspace(bounds, ToGUIRect(primary.rcMonitor), ToGUIRect(primary.rcWork)); + placement.rcNormalPosition.left = normal.Left; + placement.rcNormalPosition.top = normal.Top; + placement.rcNormalPosition.right = normal.Right; + placement.rcNormalPosition.bottom = normal.Bottom; + placement.showCmd = SW_HIDE; // Show() shows it. + SetWindowPlacement(m_Window, &placement); + } + } + + m_OpenMaximized = layout.Maximized; + + PostToUI([this, layout]() + { + RunGuarded("restoring the layout", [this, &layout]() + { + ApplyPanelSizes(layout); + }); + }); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::LoadSelection() +{ + std::vector saved; + + try + { + std::string const path = GUISelectionFilePath(); + + if (!FileExists(path)) + return; + + saved = ParseGUISelection(ReadFileBytes(path)); + } + catch (...) + { + return; // A selection that can't be read only means every test starts out checked. + } + + if (!saved.empty()) + AppendLog(DescribeGUISelectionRestore(RestoreGUISelection(m_TestList, saved)) + "\n"); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::MoveFocusTo + + Also selects the filter box's text, as the VCL's TEdit does when it's tabbed to (AutoSelect), and shows the + focus rectangle, which Windows hides until the keyboard is used. +*/ +void TWin32GUIMainWindow::MoveFocusTo(HWND control) +{ + SetFocus(control); + + if (control == m_Edt_Filter) + SendMessageW(m_Edt_Filter, EM_SETSEL, 0, -1); + + SendMessageW(m_Window, WM_CHANGEUISTATE, MAKEWPARAM(UIS_CLEAR, UISF_HIDEFOCUS), 0); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::NextTabStop + + The VCL GUI runner's tab order: the toolbar's buttons, the filter box, the test tree, the detail pane, the + results' tabs, and the results' pages, skipping a control that's disabled or hidden. Windows' own order + would put the results' pages before their tabs, since they're above them in z-order to get the mouse's + clicks. From a control that isn't in the order (e.g. nullptr), the first or, going back, the last. nullptr + if no control can take the focus. +*/ +HWND TWin32GUIMainWindow::NextTabStop(HWND from, bool forward) const +{ + std::vector order; + + for (TToolbarButton const& button : ToolbarButtons) + order.push_back(this->*button.Control); + + order.insert(order.end(), { m_Edt_Filter, m_TV_Tests, m_Mem_Detail, m_PC_Results, m_LV_Failures, m_RE_Log }); + + std::size_t const count = order.size(); + std::vector::const_iterator const found = std::find(order.begin(), order.end(), from); + // Stepping once from here reaches the first control, or going back, the last. + std::size_t const start = found != order.end() ? static_cast(found - order.begin()) : + (forward ? count - 1 : 0); + + for (std::size_t step = 1; step <= count; ++step) + { + HWND const control = order[forward ? (start + step) % count : (start + count - step) % count]; + + if (IsWindowVisible(control) != FALSE && IsWindowEnabled(control) != FALSE) + return control; + } + + return nullptr; +} +//--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::OnClose + + A run can't be interrupted partway through a test, so closing during one asks it to stop after the + current test, and closes the window once it has (see RunTests()). +*/ +void TWin32GUIMainWindow::OnClose() +{ + if (m_Running) + { + RunGuarded("closing the window", [this]() + { + m_CloseAfterRun = true; + m_Observer.RequestStop(); + m_StatusMessage = "Closing after the current test..."; + UpdateStatusBar(); + }); + return; + } + + RunGuarded("closing the window", [this]() + { + // An --exit run is automated, so nobody arranged its window. + if (!m_Options.ExitAfterRun && !m_Options.LayoutIgnore) + SaveLayout(); + + if (m_KeepSelection) + SaveSelection(); + }); + + m_Closing = true; + DestroyWindow(m_Window); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::OnDetailSplitterDrag(int position) +{ + RunGuarded("the detail pane's splitter", [this, position]() + { + RECT client; + GetClientRect(m_Window, &client); + m_DetailHeight = ClampDetailHeight(position - ScaleForDPI(TWin32GUIMainSizes::ToolbarHeight, m_DPI), + client.bottom, m_DPI); + LayOut(); + }); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::OnDPIChanged + + The window has moved to a monitor with another DPI. Windows rescales dialogs, but not a window's own + controls, so everything sized by the DPI is redone here, and the panel sizes keep their proportion. +*/ +void TWin32GUIMainWindow::OnDPIChanged(UINT dpi, RECT const& suggested) +{ + RunGuarded("moving to another monitor", [this, dpi, &suggested]() + { + m_TestTreeWidth = MulDiv(m_TestTreeWidth, static_cast(dpi), static_cast(m_DPI)); + m_DetailHeight = MulDiv(m_DetailHeight, static_cast(dpi), static_cast(m_DPI)); + m_DPI = dpi; + + SetWindowPos(m_Window, nullptr, suggested.left, suggested.top, suggested.right - suggested.left, + suggested.bottom - suggested.top, SWP_NOZORDER | SWP_NOACTIVATE); + ApplyDPI(); + }); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::OnFailuresCustomDraw + + A row it can't match to a test is simply drawn in the text color. The theme draws the row; see + TWin32GUITheme::OnListCustomDraw(). +*/ +LRESULT TWin32GUIMainWindow::OnFailuresCustomDraw(NMLVCUSTOMDRAW& draw) const +{ + COLORREF color = m_Theme.WindowTextColor(); + + if (draw.nmcd.dwDrawStage == CDDS_ITEMPREPAINT) + { + std::size_t const index = static_cast(draw.nmcd.lItemlParam); + + if (index < m_TestList.Count()) + { + color = m_TestList.Status(index) == TGUITestStatus::Failed ? m_Theme.FailureTextColor() : + m_Theme.SkipTextColor(); + } + } + + return m_Theme.OnListCustomDraw(draw, color); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::OnFailuresDoubleClick + + Selects the double-clicked failure's test in the tree, which shows its details. +*/ +void TWin32GUIMainWindow::OnFailuresDoubleClick() +{ + int const row = static_cast(SendMessageW(m_LV_Failures, LVM_GETNEXTITEM, static_cast(-1), + LVNI_SELECTED)); + + if (row < 0) + return; + + LVITEMW item = {}; + item.mask = LVIF_PARAM; + item.iItem = row; + SendMessageW(m_LV_Failures, LVM_GETITEMW, 0, reinterpret_cast(&item)); + + std::size_t const testIndex = static_cast(item.lParam); + + if (testIndex >= m_TestList.Count()) + return; + + // A failure the filter box is hiding can't be selected, so the filter is cleared to show it. + if (TestNode(testIndex) == nullptr) + SetWindowTextW(m_Edt_Filter, L""); // Its EN_CHANGE rebuilds the tree. + + HTREEITEM const node = TestNode(testIndex); + + if (node == nullptr) + return; + + SendMessageW(m_TV_Tests, TVM_SELECTITEM, TVGN_CARET, reinterpret_cast(node)); + SendMessageW(m_TV_Tests, TVM_ENSUREVISIBLE, 0, reinterpret_cast(node)); + SetFocus(m_TV_Tests); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::OnTabKey + + Tab and Shift+Tab move through NextTabStop()'s order, rather than IsDialogMessageW()'s, which would also + let the detail pane and the log keep Tab, as multiline edit controls do. Ctrl+Tab and Ctrl+Shift+Tab switch + the results' pages while the focus is on them, as the VCL's TPageControl does, and do nothing elsewhere. + True if 'message' was one of these keys, which it then handled. +*/ +bool TWin32GUIMainWindow::OnTabKey(MSG const& message) +{ + if (message.message != WM_KEYDOWN || message.wParam != VK_TAB || (GetKeyState(VK_MENU) & 0x8000) != 0 || + IsChild(m_Window, message.hwnd) == FALSE) + { + return false; + } + + bool const back = (GetKeyState(VK_SHIFT) & 0x8000) != 0; + HWND const focus = GetFocus(); + + if ((GetKeyState(VK_CONTROL) & 0x8000) != 0) + { + if (focus == m_PC_Results || focus == m_LV_Failures || focus == m_RE_Log) + { + int const count = static_cast(SendMessageW(m_PC_Results, TCM_GETITEMCOUNT, 0, 0)); + int const current = static_cast(SendMessageW(m_PC_Results, TCM_GETCURSEL, 0, 0)); + SelectResultsPage((current + (back ? count - 1 : 1)) % count); + } + + return true; + } + + HWND const next = NextTabStop(focus, !back); + + if (next != nullptr) + MoveFocusTo(next); + + return true; +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::OnTestFinished(TTestCaseRecord const& record, std::string const& testLog) +{ + std::optional const index = m_TestList.IndexOf(record.GroupName, record.TestName); + + if (index.has_value()) + { + m_TestList.SetResult(*index, record, testLog); + UpdateTestNodeImages(*index); + + if (record.Outcome != TTestOutcome::Pass) + AddFailureItem(*index); + } + + m_CurrentTestIndex.reset(); + ++m_RunFinishedCount; + SetProgressPosition(static_cast(m_RunFinishedCount)); + + if (record.Outcome == TTestOutcome::Fail) + SendMessageW(m_PB_Progress, PBM_SETSTATE, PBST_ERROR, 0); + + UpdateDetail(); + UpdateStatusBar(); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::OnTestStarted(std::string const& groupName, std::string const& testName) +{ + m_CurrentTestIndex = m_TestList.IndexOf(groupName, testName); + + if (m_CurrentTestIndex.has_value()) + { + m_TestList.SetStatus(*m_CurrentTestIndex, TGUITestStatus::Running); + UpdateTestNodeImages(*m_CurrentTestIndex); + } + + m_StatusMessage = "Running " + groupName + "." + testName; + UpdateDetail(); + UpdateStatusBar(); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::OnTestsSplitterDrag(int position) +{ + RunGuarded("the test tree's splitter", [this, position]() + { + RECT client; + GetClientRect(m_Window, &client); + m_TestTreeWidth = ClampTestTreeWidth(position, client.right, m_DPI); + LayOut(); + }); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::OnTreeItemChanging + + With partial check boxes, a click (or Space) can cycle a check box into its partial state, which only a + group's or suite's own tests should decide. So every check box change the user makes is vetoed here, and + instead toggles the node's test, or all of its group's or suite's tests, in m_TestList; the tree is then + updated from m_TestList, which also works out each group's and suite's partial state. Other changes to a + node (selecting or expanding it), SyncTreeChecks()' own, and the tree giving an item its first check box + go through. The tree does that on its own, even for an item of its own with no text or data. +*/ +bool TWin32GUIMainWindow::OnTreeItemChanging(NMTVITEMCHANGE const& change) +{ + if (m_SyncingChecks || (change.uChanged & TVIF_STATE) == 0 || + ((change.uStateNew ^ change.uStateOld) & TVIS_STATEIMAGEMASK) == 0 || + (change.uStateOld & TVIS_STATEIMAGEMASK) == 0) + { + return false; + } + + RunGuarded("a test tree check box", [this, &change]() + { + // The run in progress already decided which tests it runs, so a change now would only mislead. + if (m_Running) + return; + + // From the notification: the item's own state already reads as the new one here. + bool const checked = (change.uStateOld & TVIS_STATEIMAGEMASK) == + INDEXTOSTATEIMAGEMASK(StateImageIndex(TGUICheckState::Checked)); + + SetGUINodeChecked(m_TestList, TreeItemRef(m_TV_Tests, change.hItem), !checked); + + // Changing check boxes from inside the tree's own notification isn't safe, so the update waits until + // this change has been fully handled. + PostToUI([this]() + { + RunGuarded("a test tree check box", [this]() + { + SyncTreeChecks(); + }); + }); + }); + + return true; +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::PostToUI(std::function action) +{ + { + std::lock_guard const lock(m_QueueMutex); + m_Queue.push_back(std::move(action)); + } + + PostMessageW(m_Window, RunQueuedMessage, 0, 0); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::ProcessMessage(MSG& message) +{ + if (m_Window != nullptr) + { + // Hot keys first, then Tab, then a dialog's keyboard handling: the controls' & hot keys and arrow keys. + if (TranslateAcceleratorW(m_Window, m_Accelerators, &message) != 0 || OnTabKey(message) || + IsDialogMessageW(m_Window, &message) != 0) + { + return; + } + } + + TranslateMessage(&message); + DispatchMessageW(&message); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::ProcessMessages + + The observer's YieldCallback while tests run on this thread, between tests: the counterpart of the VCL's + Application->ProcessMessages(). A WM_QUIT found here (e.g. one a test posted) would otherwise be lost, so + it's kept and posted again when the run ends, which then ends the GUI as the VCL GUI runner's does. +*/ +void TWin32GUIMainWindow::ProcessMessages() +{ + MSG message; + + while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE) != 0) + { + if (message.message == WM_QUIT) + m_PendingQuitCode = static_cast(message.wParam); + else + ProcessMessage(message); + } +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::RecolorLog + + AppendLog() colored each line for the theme then. Every line is given the text color first, in one step, + then each failure and skip line its own color, as the VCL GUI runner's does. The selection and the scroll + position are kept. +*/ +void TWin32GUIMainWindow::RecolorLog() +{ + CHARRANGE selection = {}; + SendMessageW(m_RE_Log, EM_EXGETSEL, 0, reinterpret_cast(&selection)); + int const firstVisible = static_cast(SendMessageW(m_RE_Log, EM_GETFIRSTVISIBLELINE, 0, 0)); + + // Only while it's showing: WM_SETREDRAW's TRUE shows a hidden window, e.g. the log behind the Failures & + // Skips page. (bcc32c's std::optional can't hold a TRedrawScope, which has no default constructor.) + std::unique_ptr const updating = IsWindowVisible(m_RE_Log) != FALSE ? + std::make_unique(m_RE_Log) : nullptr; + + CHARFORMAT2W format = {}; + format.cbSize = sizeof(format); + format.dwMask = CFM_COLOR; + format.crTextColor = m_Theme.WindowTextColor(); + SendMessageW(m_RE_Log, EM_SETCHARFORMAT, SCF_ALL, reinterpret_cast(&format)); + + int const lineCount = static_cast(SendMessageW(m_RE_Log, EM_GETLINECOUNT, 0, 0)); + + for (int i = 0; i < lineCount; ++i) + { + int const start = static_cast(SendMessageW(m_RE_Log, EM_LINEINDEX, static_cast(i), 0)); + int const length = static_cast(SendMessageW(m_RE_Log, EM_LINELENGTH, static_cast(start), 0)); + + if (start < 0 || length <= 0) + continue; + + // EM_GETLINE's buffer starts with its size, in characters. + std::wstring text(static_cast(length) + 1, L' '); + text[0] = static_cast((std::min)(length + 1, 0xFFFF)); + text.resize(static_cast(SendMessageW(m_RE_Log, EM_GETLINE, static_cast(i), + reinterpret_cast(&text[0])))); + + std::string const line = WideToUTF8(text); + + if (ClassifyGUILogLine(line) == TGUILogLineKind::Normal) + continue; + + CHARRANGE range = { start, start + length }; + SendMessageW(m_RE_Log, EM_EXSETSEL, 0, reinterpret_cast(&range)); + format.crTextColor = LogLineColor(line, m_Theme); + SendMessageW(m_RE_Log, EM_SETCHARFORMAT, SCF_SELECTION, reinterpret_cast(&format)); + } + + SendMessageW(m_RE_Log, EM_EXSETSEL, 0, reinterpret_cast(&selection)); + int const nowVisible = static_cast(SendMessageW(m_RE_Log, EM_GETFIRSTVISIBLELINE, 0, 0)); + SendMessageW(m_RE_Log, EM_LINESCROLL, 0, firstVisible - nowVisible); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::RefreshAllNodeImages() +{ + TRedrawScope const updating(m_TV_Tests); + + for (HTREEITEM item = RelatedTreeItem(m_TV_Tests, nullptr, TVGN_ROOT); item != nullptr; + item = NextTreeItem(m_TV_Tests, item)) + { + SetNodeImage(item); + } +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::ReportUnexpectedError(std::string const& where, std::string const& description) +{ + std::string const message = "An unexpected error occurred in " + where + ": " + description; + ShowMessageDialog(m_Window, m_Theme, Title, message, EWin32GUIMessageIcon::Error); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::ResetLayout() +{ + if (IsZoomed(m_Window) != FALSE) + ShowWindow(m_Window, SW_RESTORE); + + // The default size, centered on the monitor the window is on now. + CenterOnMonitor(); + + TGUILayout defaults; + defaults.DetailHeight = TWin32GUIMainSizes::DefaultDetailHeight; + defaults.TestTreeWidth = TWin32GUIMainSizes::DefaultTestTreeWidth; + ApplyPanelSizes(defaults); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::ResetSavedLayout + + For --layout-reset. The file is deleted now, rather than only not loaded, so a layout that caused a + problem is gone even if the window doesn't get to close normally this time. +*/ +void TWin32GUIMainWindow::ResetSavedLayout() +{ + std::string const path = GUILayoutFilePath(); + std::string outcome; + + if (!FileExists(path)) + { + outcome = "there was no saved window layout to delete"; + } + else + { + RemoveFile(path); // Reported below, from whether the file is still there. + outcome = FileExists(path) ? + "the saved window layout couldn't be deleted (" + path + "), so it's ignored this time" : + "the saved window layout was deleted, so the window opens with the defaults"; + } + + AppendLog("Note: --layout-reset: " + outcome + ".\n"); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::RunCheckedTests() +{ + RunTests(m_TestList.CheckedFilter(), "the checked tests"); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::RunGuarded + + Runs an event handler's body. No exception may leave a window procedure, since unwinding through + Windows' own frames is undefined, so one is caught here and reported with its message and 'where' it + happened, as the VCL GUI runner's RunGuarded() reports a C++ exception. +*/ +void TWin32GUIMainWindow::RunGuarded(std::string const& where, std::function const& body) +{ + try + { + body(); + } + catch (std::exception const& ex) + { + ReportUnexpectedError(where, ex.what()); + } + catch (...) + { + ReportUnexpectedError(where, "an object that isn't a std::exception"); + } +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::RunMessageLoop() +{ + MSG message; + + while (GetMessageW(&message, nullptr, 0, 0) > 0) + ProcessMessage(message); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::RunQueued() +{ + std::deque > actions; + + { + std::lock_guard const lock(m_QueueMutex); + actions.swap(m_Queue); + } + + for (std::function const& action : actions) + RunGuarded("a queued action", action); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::RunTests + + Runs on this (the main) thread, so tests that create windows work; the observer's YieldCallback lets the + window repaint and respond to Stop between tests. Every test's result, the Failures & Skips list, and the + log are cleared first, so they only ever show one run. + + A shuffled run's seed comes from m_Shuffle, rather than being left to TTestHandler::Run() to choose, so + the GUI knows it and can show it. +*/ +void TWin32GUIMainWindow::RunTests(TestFilter const& filter, std::string const& filterDescription) +{ + if (m_Running || m_RunAborted) + return; + + m_TestList.ResetResults(); + RefreshAllNodeImages(); + SendMessageW(m_LV_Failures, LVM_DELETEALLITEMS, 0, 0); + SetWindowTextW(m_RE_Log, L""); + UpdateDetail(); + + m_RunTotalCount = m_TestList.CountMatching(filter); + m_RunFinishedCount = 0; + m_CurrentTestIndex.reset(); + m_RunStart = std::chrono::steady_clock::now(); + m_RunEnd.reset(); + m_LastRunJUnitTestCases.reset(); + + // Back to the normal state first, since the error state wouldn't draw the new position; see + // SetProgressPosition(). + SendMessageW(m_PB_Progress, PBM_SETSTATE, PBST_NORMAL, 0); + SendMessageW(m_PB_Progress, PBM_SETRANGE32, 0, static_cast((std::max)(m_RunTotalCount, 1))); + SetProgressPosition(0); + + m_Observer.ResetStop(); + m_StatusMessage = "Running..."; + std::optional const shuffleSeed = m_Shuffle.NextRunSeed(); + SetRunning(true); // Also shows the seed just chosen. + + try + { + TTestResults const results = m_Handler->Run(filter, filterDescription, shuffleSeed.has_value(), shuffleSeed, + m_Options.CLI.TestTimeoutSeconds, m_Options.CLI.CatchCrashes); + m_Observer.Flush(); + + // After an aborted run, nothing runs again until the GUI is restarted (see WasRunAborted()). + TGUIRunSummary const summary = SummarizeGUIRun(results); + m_RunAborted = summary.Aborted; + m_StatusMessage = summary.StatusMessage; + m_LastRunExitCode = summary.ExitCode; + m_LastRunJUnitTestCases = ToJUnitTestCases(results.CaseRecords); + + // A failure is only noted in the log, so it can't hold up --exit. + if (!m_Options.CLI.JunitReportPath.empty()) + { + std::string error; + WriteJUnitReport(m_Options.CLI.JunitReportPath, error); + } + } + // As in the console runner, an unhandled exception ends the run without a report, with its own exit code. + catch (std::exception const& ex) + { + m_LastRunExitCode = ExitCode_UnhandledException; + FailRunOnUnhandledException(ex.what()); + } + catch (...) + { + m_LastRunExitCode = ExitCode_UnhandledExceptionUnknown; + FailRunOnUnhandledException("an object that isn't a std::exception"); + } + + m_RunEnd = std::chrono::steady_clock::now(); + SetRunning(false); + + if (m_PendingQuitCode.has_value()) + { + PostQuitMessage(*m_PendingQuitCode); + m_PendingQuitCode.reset(); + } + + // Posted, not closed directly: this may still be inside a button's click handling. + if (m_CloseAfterRun) + PostMessageW(m_Window, WM_CLOSE, 0, 0); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::SaveLayout() +{ + try + { + std::string const path = GUILayoutFilePath(); + CreateFolderPath(GUISettingsFolderPath()); + + // Starting from the file's current text keeps any other values in it. + TGUIINIDocument ini(FileExists(path) ? ReadFileBytes(path) : std::string()); + SaveGUILayout(ini, CurrentLayout()); + WriteFileBytes(path, ini.Text()); + } + catch (...) + { + // Not saving it (e.g. a read-only profile) only means the next start uses the defaults. + } +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::SaveSelection() +{ + try + { + std::string const path = GUISelectionFilePath(); + CreateFolderPath(GUISettingsFolderPath()); + WriteFileBytes(path, FormatGUISelection(CaptureGUISelection(m_TestList))); + } + catch (...) + { + // Not saving it (e.g. a read-only profile) only means the next start checks every test. + } +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::SaveThemeTemplate + + Writes CarbonThemeFileText() where the user chooses, starting in GUIThemesFolderPath(), created if needed, + where View > Theme > Dark Theme lists it once it's saved there. +*/ +void TWin32GUIMainWindow::SaveThemeTemplate() +{ + std::string const folder = GUIThemesFolderPath(); + CreateFolderPath(folder); + + std::optional const path = AskSaveFilePath(m_Window, L"Save Theme Template", + L"Theme files (*.ini)\0*.ini\0All files (*.*)\0*.*\0", L"ini", folder + "\\My Theme.ini"); + + if (!path.has_value()) + return; // Canceled. + + try + { + WriteFileBytes(*path, CarbonThemeFileText()); + } + catch (std::exception const& exception) + { + std::string const message = "The theme template couldn't be written to:\n" + *path + "\n\n" + + exception.what(); + ShowMessageDialog(m_Window, m_Theme, Title, message, EWin32GUIMessageIcon::Error); + return; + } + + m_StatusMessage = "Theme template saved."; + UpdateStatusBar(); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::SelectResultsPage(int page) +{ + // Unlike a click on a tab, this sends no TCN_SELCHANGE. + SendMessageW(m_PC_Results, TCM_SETCURSEL, static_cast(page), 0); + ShowResultsPage(); +} +//--------------------------------------------------------------------------- +HTREEITEM TWin32GUIMainWindow::SelectedTreeItem() const +{ + return RelatedTreeItem(m_TV_Tests, nullptr, TVGN_CARET); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::SetDarkTheme + + Read from its file again even if it's the current one, so a change to the file shows. Applied at once + while dark; while light, it's still read, so its problems show in the log, and the status bar says it shows + once the theme is dark. +*/ +void TWin32GUIMainWindow::SetDarkTheme(std::string const& name) +{ + m_DarkTheme = name; + + if (!m_Options.LayoutIgnore) + SaveGUIDarkThemeToLayoutFile(GUILayoutFilePath(), name); + + std::vector const problems = m_Theme.SetDarkTheme(name); + + if (m_Theme.IsDark()) + { + CreateStatusImages(); + RecolorLog(); + m_StatusMessage = "Dark theme: " + name + "."; + } + else + { + m_StatusMessage = "Dark theme: " + name + ", shown while the theme is dark."; + } + + for (std::string const& problem : problems) + AppendLog(problem + "\n"); + + UpdateStatusBar(); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::SetNodeImage(HTREEITEM item) +{ + int const image = static_cast(GUINodeStatus(m_TestList, TreeItemRef(m_TV_Tests, item))); + + TVITEMW data = {}; + data.mask = TVIF_HANDLE | TVIF_IMAGE | TVIF_SELECTEDIMAGE; + data.hItem = item; + data.iImage = image; + data.iSelectedImage = image; + SendMessageW(m_TV_Tests, TVM_SETITEMW, 0, reinterpret_cast(&data)); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::SetProgressPosition + + In the error state (red), a themed progress bar takes a new position without drawing it, so it stays where + it was when it turned red, and back in the normal state it then shows the position it last took. Setting the + position a second time draws it, as the VCL GUI runner found. +*/ +void TWin32GUIMainWindow::SetProgressPosition(int position) +{ + SendMessageW(m_PB_Progress, PBM_SETPOS, static_cast(position), 0); + + if (SendMessageW(m_PB_Progress, PBM_GETSTATE, 0, 0) != PBST_NORMAL) + SendMessageW(m_PB_Progress, PBM_SETPOS, static_cast(position), 0); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::SetRunning + + Disabling the control with the focus moves the focus on (see UpdateCommands()), e.g. from Run Selected to + Stop as a run starts. So when the run ends, the control that had the focus gets it back, as in the VCL GUI + runner, unless the user has put it somewhere other than a toolbar button since. +*/ +void TWin32GUIMainWindow::SetRunning(bool running) +{ + if (running) + m_FocusBeforeRun = GetFocus(); + + m_Running = running; + UpdateCommands(); + UpdateStatusBar(); + + if (!running && m_FocusBeforeRun != nullptr && IsWindowEnabled(m_FocusBeforeRun) != FALSE && + IsToolbarButton(GetFocus())) + { + SetFocus(m_FocusBeforeRun); + } + + // Windows' setting changed during the run; see ApplyTheme(). + if (!running && m_ThemeChangePending) + ApplyTheme(); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::SetTheme + + Saved at once, rather than with the layout as the window closes, so the choice is kept even if the window + doesn't get to close normally; but not with --layout-ignore, which saves nothing. Reset Layout leaves it + alone. +*/ +void TWin32GUIMainWindow::SetTheme(EGUITheme theme) +{ + m_ThemeChoice = theme; + ShowThemeState(); + + if (!m_Options.LayoutIgnore) + SaveGUIThemeToLayoutFile(GUILayoutFilePath(), theme); + + ApplyTheme(); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::SetTreeExpanded(bool expanded) +{ + TRedrawScope const updating(m_TV_Tests); + + for (HTREEITEM item = RelatedTreeItem(m_TV_Tests, nullptr, TVGN_ROOT); item != nullptr; + item = NextTreeItem(m_TV_Tests, item)) + { + if (RelatedTreeItem(m_TV_Tests, item, TVGN_CHILD) != nullptr) + SendMessageW(m_TV_Tests, TVM_EXPAND, expanded ? TVE_EXPAND : TVE_COLLAPSE, reinterpret_cast(item)); + } +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::Show + + WinMain()'s 'showCommand' is usually SW_SHOWNORMAL, unless e.g. a shortcut asks for the window minimized or + maximized. +*/ +void TWin32GUIMainWindow::Show(int showCommand) +{ + if (m_OpenMaximized && (showCommand == SW_SHOWNORMAL || showCommand == SW_SHOWDEFAULT || showCommand == SW_SHOW)) + showCommand = SW_SHOWMAXIMIZED; + + ShowWindow(m_Window, showCommand); + UpdateWindow(m_Window); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::ShowResultsPage + + The focus, if it's on the page being hidden, goes to the one shown, as in the VCL. +*/ +void TWin32GUIMainWindow::ShowResultsPage() +{ + bool const logSelected = SendMessageW(m_PC_Results, TCM_GETCURSEL, 0, 0) == 1; + HWND const shown = logSelected ? m_RE_Log : m_LV_Failures; + HWND const hidden = logSelected ? m_LV_Failures : m_RE_Log; + + ShowWindow(shown, SW_SHOW); + + if (GetFocus() == hidden) + SetFocus(shown); + + ShowWindow(hidden, SW_HIDE); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::ShowShuffleState() +{ + CheckMenuItem(m_MM_Main, static_cast(EWin32GUICommand::ShuffleOrder), + MF_BYCOMMAND | (m_Shuffle.Enabled() ? MF_CHECKED : MF_UNCHECKED)); + UpdateStatusBar(); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::ShowThemeState() +{ + EWin32GUICommand checked = EWin32GUICommand::ThemeWindows; + + if (m_ThemeChoice == EGUITheme::Dark) + checked = EWin32GUICommand::ThemeDark; + else if (m_ThemeChoice == EGUITheme::Light) + checked = EWin32GUICommand::ThemeLight; + + CheckMenuRadioItem(m_MM_Main, static_cast(EWin32GUICommand::ThemeDark), + static_cast(EWin32GUICommand::ThemeWindows), static_cast(checked), MF_BYCOMMAND); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::Start + + Loads the registered tests and applies the command line's options: --project-name to the caption, and + --filter and the partition options to which tests start out checked, and --filter to the filter box, + and --shuffle and --shuffle-seed to the Options menu. Also restores the window layout and the test + selection saved when it last closed. WinMain() set the theme up for 'theme' and 'darkTheme' before any + window existed, so the --help dialog had it too, and its problems are noted in the log, where they stay + until the first run clears it. +*/ +void TWin32GUIMainWindow::Start(TGUIOptions const& options, EGUITheme theme, std::string const& darkTheme, + std::vector const& themeProblems) +{ + m_Options = options; + m_ThemeChoice = theme; + m_DarkTheme = darkTheme; + ShowThemeState(); + // The dark theme's status dots, if WinMain() set up a theme file. + CreateStatusImages(); + + m_Observer.LogTextCallback = [this](std::string const& text) + { + AppendLog(text); + }; + m_Observer.TestStartedCallback = [this](std::string const& groupName, std::string const& testName) + { + OnTestStarted(groupName, testName); + }; + m_Observer.TestFinishedCallback = [this](TTestCaseRecord const& record, std::string const& testLog) + { + OnTestFinished(record, testLog); + }; + m_Observer.YieldCallback = [this]() + { + ProcessMessages(); + }; + + m_Handler = std::make_unique(); + m_Handler->SetRunObserver(&m_Observer); // Before Initialize(), so its output goes to the observer too. + m_Handler->Initialize(m_Options.CLI.ProjectName); + m_Observer.Flush(); + + m_TestList.Load(m_Handler->GetTests()); + + std::string filterDescription; + TestFilter const initialFilter = TCLIParser::BuildTestFilter(*m_Handler, m_Options.CLI, filterDescription); + m_TestList.CheckMatching(initialFilter); + + // Notes about the command line, in the log, where they stay until the first run clears it. + if (initialFilter != nullptr) + AppendLog("Initially checked: " + filterDescription + "\n"); + + // Only when the command line chooses no tests (see m_KeepSelection). Before the tree is built below. + m_KeepSelection = !m_Options.RunOnStart && initialFilter == nullptr; + + if (m_KeepSelection) + LoadSelection(); + + for (std::string const& note : GUICommandLineNotes(m_Options, "windows or controls")) + AppendLog(note + "\n"); + + for (std::string const& problem : themeProblems) + AppendLog(problem + "\n"); + + if (m_Options.LayoutReset) + ResetSavedLayout(); + + UpdateCaption(); + + m_Shuffle.ApplyCommandLine(m_Options.CLI.Shuffle, m_Options.CLI.ShuffleSeed); + ShowShuffleState(); + + // The filter box's case-insensitive contains-match shows every test --filter's pattern matches (and + // maybe a few more, left unchecked), so seeding it with the pattern never hides a checked test. + if (m_Options.CLI.HasFilter) + { + // Without EN_CHANGE's rebuild, which only happens if the text changes, so the tree is built once either + // way, just below. + TScopedFlag const settingFilterText(m_SettingFilterText); + SetWindowTextW(m_Edt_Filter, UTF8ToWide(m_Options.CLI.FilterPattern).c_str()); + } + + // Builds the tree. + ApplyFilterBox(); + + // Before --run is queued below, so the panel sizes LoadLayout() queues are applied before the run starts. + if (!m_Options.LayoutIgnore && !m_Options.LayoutReset) + LoadLayout(); + + if (m_Options.RunOnStart) + { + // Queued, so it starts once the message loop is running and the window is showing. + PostToUI([this]() + { + RunGuarded("--run", [this]() + { + // Unlike Run Selected, runs even if nothing is checked, so --exit still reports and exits + // the way the console runner does when its filter matches nothing. + RunCheckedTests(); + + if (m_Options.ExitAfterRun) + PostMessageW(m_Window, WM_CLOSE, 0, 0); + }); + }); + } +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::SyncTreeChecks + + Sets every node's check box from m_TestList: a test's to whether it's checked, and a group's or suite's + to whether all, none, or only some of its tests are. +*/ +void TWin32GUIMainWindow::SyncTreeChecks() +{ + { + TRedrawScope const updating(m_TV_Tests); + TScopedFlag const syncing(m_SyncingChecks); + + for (HTREEITEM item = RelatedTreeItem(m_TV_Tests, nullptr, TVGN_ROOT); item != nullptr; + item = NextTreeItem(m_TV_Tests, item)) + { + TVITEMW data = {}; + data.mask = TVIF_HANDLE | TVIF_STATE; + data.hItem = item; + data.stateMask = TVIS_STATEIMAGEMASK; + data.state = INDEXTOSTATEIMAGEMASK(StateImageIndex(GUINodeCheckState(m_TestList, + TreeItemRef(m_TV_Tests, item)))); + SendMessageW(m_TV_Tests, TVM_SETITEMW, 0, reinterpret_cast(&data)); + } + } + + UpdateStatusBar(); // For its checked count. +} +//--------------------------------------------------------------------------- +HTREEITEM TWin32GUIMainWindow::TestNode(std::size_t testIndex) const +{ + return testIndex < m_TestNodes.size() ? m_TestNodes[testIndex] : nullptr; +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::UpdateCaption() +{ + SetWindowTextW(m_Window, UTF8ToWide(GUICaption(m_Options.CLI.ProjectName)).c_str()); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::UpdateCommands() +{ + TGUICommandContext context; + context.FailedCount = m_TestList.StatusCount(TGUITestStatus::Failed); + context.HasReport = m_LastRunJUnitTestCases.has_value(); + context.HasSelection = SelectedTreeItem() != nullptr; + context.RunAborted = m_RunAborted; + context.Running = m_Running; + context.TreeHasNodes = SendMessageW(m_TV_Tests, TVM_GETCOUNT, 0, 0) > 0; + + m_CommandStates = GUICommandStates(context); + HWND const focus = GetFocus(); + + for (EWin32GUICommand const command : AllCommands) + { + EnableMenuItem(m_MM_Main, static_cast(command), + MF_BYCOMMAND | (IsCommandEnabled(m_CommandStates, command) ? MF_ENABLED : MF_GRAYED)); + } + + for (TToolbarButton const& button : ToolbarButtons) + EnableWindow(this->*button.Control, IsCommandEnabled(m_CommandStates, button.Command) ? TRUE : FALSE); + + EnableWindow(m_Edt_Filter, m_CommandStates.FilterBox ? TRUE : FALSE); + + // A disabled control can't keep the focus, e.g. Run Selected as a run starts, and with no control holding it, + // IsDialogMessageW() keeps Alt+letter from opening the menu. So it moves on to the next control, as a dialog's + // does. + if (focus != nullptr && IsChild(m_Window, focus) && IsWindowEnabled(focus) == FALSE) + { + HWND const next = NextTabStop(focus, true); + + if (next != nullptr) + SetFocus(next); + } +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::UpdateDetail() +{ + HTREEITEM const item = SelectedTreeItem(); + UpdateCommands(); // Copy Details, with or without a selection. + + std::string const text = item != nullptr ? AdjustLineBreaks(GUINodeDetailText(m_TestList, + TreeItemRef(m_TV_Tests, item))) : std::string(); + SetWindowTextW(m_Mem_Detail, UTF8ToWide(text).c_str()); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::UpdateStatusBar() +{ + std::optional elapsedSeconds; + + if (m_RunStart.has_value()) + { + std::chrono::steady_clock::time_point const end = m_RunEnd.value_or(std::chrono::steady_clock::now()); + elapsedSeconds = std::chrono::duration(end - *m_RunStart).count(); + } + + std::vector const panels = GUIStatusPanels(m_TestList, m_RunFinishedCount, m_RunTotalCount, + elapsedSeconds, m_Shuffle.StatusText(), m_StatusMessage); + + for (std::size_t i = 0; i < panels.size(); ++i) + SendMessageW(m_SB_Status, SB_SETTEXTW, i, reinterpret_cast(UTF8ToWide(panels[i]).c_str())); +} +//--------------------------------------------------------------------------- +void TWin32GUIMainWindow::UpdateTestNodeImages(std::size_t testIndex) +{ + HTREEITEM const testNode = TestNode(testIndex); + + if (testNode == nullptr) + return; // Hidden by the filter box, e.g. a hidden failure rerun by Run Failed. + + // The test's own image, then those of its group and the suites holding it, whose status depends on it. + for (HTREEITEM item = testNode; item != nullptr; item = RelatedTreeItem(m_TV_Tests, item, TVGN_PARENT)) + SetNodeImage(item); +} +//--------------------------------------------------------------------------- +bool TWin32GUIMainWindow::WasRunAborted() const +{ + return m_RunAborted; +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::WindowProc + + Passes each message to the window that created it, whose address CreateWindowExW() received. No exception + may leave a window procedure: one that escapes the message's handler is reported, and the message is left + to Windows. +*/ +LRESULT CALLBACK TWin32GUIMainWindow::WindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (message == WM_NCCREATE) + { + TWin32GUIMainWindow* const mainWindow = static_cast( + reinterpret_cast(lParam)->lpCreateParams); + mainWindow->m_Window = window; + SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast(mainWindow)); + } + + TWin32GUIMainWindow* const mainWindow = reinterpret_cast(GetWindowLongPtrW(window, + GWLP_USERDATA)); + + if (mainWindow != nullptr) + { + try + { + return mainWindow->HandleMessage(message, wParam, lParam); + } + catch (std::exception const& ex) + { + try + { + mainWindow->ReportUnexpectedError("the main window", ex.what()); + } + catch (...) + { + } + } + catch (...) + { + try + { + mainWindow->ReportUnexpectedError("the main window", "an object that isn't a std::exception"); + } + catch (...) + { + } + } + } + + return DefWindowProcW(window, message, wParam, lParam); +} +//--------------------------------------------------------------------------- +/* + TWin32GUIMainWindow::WriteJUnitReport + + Writes the file itself, rather than with TJUnitReportWriter::Write(), so a failure can say why: Write() only + returns false. +*/ +bool TWin32GUIMainWindow::WriteJUnitReport(std::string const& path, std::string& error) +{ + error.clear(); + + try + { + if (!m_LastRunJUnitTestCases.has_value()) + throw std::runtime_error("no run has finished"); + + WriteFileBytes(path, TJUnitReportWriter::BuildXML(m_Handler->GetProjectName(), *m_LastRunJUnitTestCases)); + } + catch (std::exception const& ex) + { + error = ex.what(); + } + + if (error.empty()) + AppendLog("JUnit report written to: " + path + "\n"); + else + AppendLog("Error: could not write JUnit report to: " + path + " (" + error + ")\n"); + + return error.empty(); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.h b/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.h new file mode 100644 index 0000000..f899be4 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_MainWindow.h @@ -0,0 +1,391 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_MainWindow.h +Author: Anthony S. West - ASW Software + +The Win32 GUI runner's main window, the counterpart of the VCL GUI runner's +main form (vcl/gui/src/ASWUnitTests_GUI_MainForm): the same controls, menu, +hot keys and layout, built with the Win32 API. Its controls are named as +the .dfm's components and its commands as the form's actions, so the two +can be compared side by side. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_Win32GUI_MainWindowH +#define ASWUnitTests_Win32GUI_MainWindowH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +#include +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Handler.h" +#include "ASWUnitTests_JUnitReport.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_CommandLine.h" +#include "ASWUnitTests_GUI_Commands.h" +#include "ASWUnitTests_GUI_Layout.h" +#include "ASWUnitTests_GUI_RunObserver.h" +#include "ASWUnitTests_GUI_Shuffle.h" +#include "ASWUnitTests_GUI_TestList.h" +#include "ASWUnitTests_GUI_Theme.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_DPI.h" +#include "ASWUnitTests_Win32GUI_Splitter.h" +#include "ASWUnitTests_Win32GUI_Theme.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// EWin32GUICommand +// +// The main window's commands, one per VCL GUI runner action, named as in +// TGUICommandStates (e.g. RunSelected for Act_RunSelected), the theme ones +// each a View > Theme item (TGUICommandStates::Theme). Each is also the +// control ID of its menu item and toolbar button; the Theme ones are +// consecutive, for CheckMenuRadioItem(). +///////////////////////////////////////////////////////////////////////////// +enum class EWin32GUICommand : WORD +{ + About = 1000, + CollapseAll, + CommandLineOptions, + CopyDetails, + Exit, + ExpandAll, + ExportJUnitReport, + FocusFilter, + OpenThemesFolder, + ResetLayout, + RunFailed, + RunSelected, + SaveThemeTemplate, + SelectAll, + SelectFailed, + SelectNone, + ShuffleOrder, + ShuffleSeed, + Stop, + ThemeDark, + ThemeLight, + ThemeWindows, +}; + + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUIMainWindow +///////////////////////////////////////////////////////////////////////////// +class TWin32GUIMainWindow +{ +private: + // A toolbar button: its command, caption, left edge and width at 96 DPI, and tooltip, from the .dfm. + struct TToolbarButton + { + EWin32GUICommand Command; + wchar_t const* Caption; + int Left; + int Width; + wchar_t const* Hint; + HWND TWin32GUIMainWindow::* Control; + }; + +private: + static TToolbarButton const ToolbarButtons[7]; + +private: + // Controls, named as the VCL GUI runner's .dfm components. + HWND m_Btn_CopyDetails; + HWND m_Btn_RunFailed; + HWND m_Btn_RunSelected; + HWND m_Btn_SelectAll; + HWND m_Btn_SelectFailed; + HWND m_Btn_SelectNone; + HWND m_Btn_Stop; + HWND m_Edt_Filter; + HWND m_Lbl_Filter; + HWND m_LV_Failures; + HWND m_Mem_Detail; + // View > Theme > Dark Theme, which FillDarkThemeMenu() fills each time it opens. + HMENU m_MI_ViewDarkTheme; + HMENU m_MM_Main; + HWND m_PB_Progress; + HWND m_PC_Results; + HWND m_RE_Log; + HWND m_SB_Status; + TWin32GUISplitter m_Spl_Detail; + TWin32GUISplitter m_Spl_Tests; + // The tooltips of the toolbar's buttons and the filter box, the .dfm's Hints. + HWND m_TT_Hints; + HWND m_TV_Tests; + + HACCEL m_Accelerators; + // Set when the window was asked to close during a run, which then closes it once the run has stopped. + bool m_CloseAfterRun; + // Set by WM_CLOSE, so destroying the window ends the message loop; destroying it otherwise (e.g. as this + // object is destroyed after an error) doesn't. + bool m_Closing; + TGUICommandStates m_CommandStates; + // The test that has started but not yet finished, if any. + std::optional m_CurrentTestIndex; + // The View > Theme > Dark Theme choice: GUIDefaultDarkThemeName() or a theme file's name. + std::string m_DarkTheme; + // The dark themes View > Theme > Dark Theme lists, in its order, each item's command ID being + // FirstDarkThemeCommand plus its index here. + std::vector m_DarkThemeNames; + // The detail pane's height and the test tree's width, in pixels at m_DPI. + int m_DetailHeight; + UINT m_DPI; + // The path last chosen in Export JUnit Report's dialog, where it starts next time. + std::string m_ExportJUnitReportPath; + // The control that had the focus as the current or latest run started; see SetRunning(). + HWND m_FocusBeforeRun; + // The control that had the focus when the window was last deactivated, given it back on activation. + HWND m_FocusedControl; + // Holds the registered tests; created by Start(). Reports to m_Observer, so the destructor destroys it first. + std::unique_ptr m_Handler; + // Set when the selection is restored at start and saved at close: only when the command line chooses no + // tests, so an automated --run or a --filter launch never replaces the selection saved interactively. + bool m_KeepSelection; + // The exit code the console runner would have returned for the latest run; see ExitCode(). + int m_LastRunExitCode; + // The latest run's JUnit report entries, for --report-junit and Export JUnit Report. Unset until a run + // finishes, and after a run an unhandled exception ended, which has no report, as in the console runner. + std::optional > m_LastRunJUnitTestCases; + TWin32GUIFont m_MonospaceFont; + TGUIRunObserver m_Observer; + // Set by LoadLayout() when the saved layout was maximized, so Show() opens the window maximized. + bool m_OpenMaximized; + TGUIOptions m_Options; + // The exit code of a WM_QUIT that ProcessMessages() took from the queue during a run, posted again when the + // run ends. + std::optional m_PendingQuitCode; + // Actions PostToUI() queued, run by RunQueued(). + std::deque > m_Queue; + std::mutex m_QueueMutex; + // Msftedit.dll, which provides the rich edit control the log uses. + HMODULE m_RichEditLibrary; + // Set when a run ends with a test that timed out or crashed, after which no further run can start; see + // WasRunAborted(). + bool m_RunAborted; + std::optional m_RunEnd; + std::size_t m_RunFinishedCount; + bool m_Running; + std::optional m_RunStart; + std::size_t m_RunTotalCount; + // Set while Start() puts --filter's pattern in the filter box, so its EN_CHANGE leaves building the tree to + // Start(), which then builds it once. + bool m_SettingFilterText; + TGUIShuffle m_Shuffle; + // The test tree's status dots, one per TGUITestStatus, drawn for m_DPI by CreateStatusImages(). + HIMAGELIST m_StatusImages; + // The status bar's last panel. + std::string m_StatusMessage; + // Set while SyncTreeChecks() sets the tree's check boxes, so OnTreeItemChanging() lets the changes through. + bool m_SyncingChecks; + TGUITestList m_TestList; + // Each test's tree node, by index into m_TestList; nullptr while the filter box hides it. Set by + // BuildTestTree(); a Win32 tree's nodes stay valid until they're deleted. + std::vector m_TestNodes; + int m_TestTreeWidth; + // The look of the window, its controls and dialogs, light or dark; see ApplyTheme(). + TWin32GUITheme& m_Theme; + // Set when the theme should change during a run, which then changes it once the run has ended. + bool m_ThemeChangePending; + // The View > Theme choice. + EGUITheme m_ThemeChoice; + TWin32GUIFont m_UIFont; + HWND m_Window; + +private: + static LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam); + +private: + // Adds a failed or skipped test to the Failures & Skips list. + void AddFailureItem(std::size_t testIndex); + // Appends the framework's log output to the log, each line colored as ClassifyGUILogLine() says. + void AppendLog(std::string const& text); + // Recreates the fonts and status images for m_DPI and applies them, with everything else sized by the DPI: + // the tree's indent, the status bar's parts and the failure list's columns. + void ApplyDPI(); + // Shows only the tests the filter box's text matches, and rebuilds the tree for them. + void ApplyFilterBox(); + // Sets the test tree's width and the detail pane's height that 'layout' has, within what the splitters + // allow. + void ApplyPanelSizes(TGUILayout const& layout); + // Makes the window light or dark as m_ThemeChoice and Windows' settings say; see the definition. + void ApplyTheme(); + // Rebuilds the test tree from m_TestList's visible tests, fully expanded; see the definition. + void BuildTestTree(); + void CenterOnMonitor(); + HWND CreateChild(DWORD exStyle, wchar_t const* className, wchar_t const* text, DWORD style, WORD id = 0); + void CreateControls(); + void CreateMenuBar(); + void CreateStatusImages(); + void CreateTooltips(); + // The window's layout as it would be saved; see the definition. + TGUILayout CurrentLayout() const; + // Runs a command from its menu item, toolbar button or hot key, if it's enabled; see the definition. + void DoCommand(EWin32GUICommand command); + // Saves the latest run's JUnit report where the user chooses; see the definition. + void ExportJUnitReport(); + // Ends the run on an unhandled exception that escaped TTestHandler::Run(), described by 'description'. + void FailRunOnUnhandledException(std::string const& description); + // Lists Carbon and the theme files in View > Theme > Dark Theme, as it opens; see the definition. + void FillDarkThemeMenu(); + LRESULT HandleMessage(UINT message, WPARAM wParam, LPARAM lParam); + bool IsToolbarButton(HWND control) const; + void LayOut(); + // Restores the layout saved when the window last closed; see the definition. + void LoadLayout(); + // Restores the test selection saved when the window last closed, noting what it did in the log. + void LoadSelection(); + // Moves the focus to 'control' as Tab does; see the definition. + void MoveFocusTo(HWND control); + // The control Tab (or Shift+Tab, if not 'forward') moves the focus to from 'from'; see the definition. + HWND NextTabStop(HWND from, bool forward) const; + // Handles WM_CLOSE; see the definition. + void OnClose(); + void OnDetailSplitterDrag(int position); + void OnDPIChanged(UINT dpi, RECT const& suggested); + // The Failures & Skips list's NM_CUSTOMDRAW: each row's text in its result's color. + LRESULT OnFailuresCustomDraw(NMLVCUSTOMDRAW& draw) const; + // Selects the double-clicked failure's test in the tree; see the definition. + void OnFailuresDoubleClick(); + // Handles Tab, Shift+Tab and Ctrl+Tab before the dialog keyboard handling does; see the definition. + bool OnTabKey(MSG const& message); + void OnTestFinished(TTestCaseRecord const& record, std::string const& testLog); + void OnTestStarted(std::string const& groupName, std::string const& testName); + void OnTestsSplitterDrag(int position); + // Handles TVN_ITEMCHANGINGW; true vetoes the change. See the definition. + bool OnTreeItemChanging(NMTVITEMCHANGE const& change); + // Handles one message from the queue, as the message loop does: hot keys first, then a dialog's keyboard + // handling, then the window it's for. + void ProcessMessage(MSG& message); + // Handles the messages waiting in the queue, so the window stays responsive while tests run; see the + // definition. + void ProcessMessages(); + // Colors the log's text again for the current theme; see the definition. + void RecolorLog(); + // Sets every test tree node's status image from m_TestList. + void RefreshAllNodeImages(); + // Reports an error that escaped from 'where' in a message box. + void ReportUnexpectedError(std::string const& where, std::string const& description); + // Gives the window its default size, centered on its monitor, and the panels their default sizes. + void ResetLayout(); + // Deletes the saved layout, for --layout-reset; see the definition. + void ResetSavedLayout(); + void RunCheckedTests(); + // Runs an event handler's body, reporting any exception from it by name; see the definition. + void RunGuarded(std::string const& where, std::function const& body); + // Runs the actions PostToUI() queued. + void RunQueued(); + // Runs the tests 'filter' matches; see the definition. + void RunTests(TestFilter const& filter, std::string const& filterDescription); + // Saves the window's layout to GUILayoutFilePath(), keeping any other values in the file. Not saving it + // (e.g. a read-only profile) only means the next start uses the defaults. + void SaveLayout(); + // Saves the test selection to GUISelectionFilePath(). Not saving it only means the next start checks every + // test. + void SaveSelection(); + // Saves Carbon as a theme file to start from, where the user chooses; see the definition. + void SaveThemeTemplate(); + // Shows the results page 'page' (0 Failures & Skips, 1 Log), as clicking its tab does. + void SelectResultsPage(int page); + // The test tree's selected node, or nullptr if none is. + HTREEITEM SelectedTreeItem() const; + // Chooses View > Theme > Dark Theme's 'name', saving it, and applies it; see the definition. + void SetDarkTheme(std::string const& name); + // Sets a test tree node's status image from m_TestList. + void SetNodeImage(HTREEITEM item); + // Sets the progress bar's position; see the definition. + void SetProgressPosition(int position); + // Enables or disables the commands for a run starting or ending; see the definition. + void SetRunning(bool running); + // Chooses View > Theme's 'theme', saving it, and applies it; see the definition. + void SetTheme(EGUITheme theme); + // Expands or collapses every node of the test tree. + void SetTreeExpanded(bool expanded); + // Shows the results page whose tab is selected; see the definition. + void ShowResultsPage(); + // Shows whether shuffling is on, in the Options menu's check mark and the status bar. + void ShowShuffleState(); + // Shows m_ThemeChoice in View > Theme's radio check. + void ShowThemeState(); + // Sets every test tree node's check box from m_TestList; see the definition. + void SyncTreeChecks(); + // A test's tree node, or nullptr while the filter box hides it. + HTREEITEM TestNode(std::size_t testIndex) const; + void UpdateCaption(); + // Enables or disables every command, and the filter box, by GUICommandStates()'s rules. + void UpdateCommands(); + // Shows the selected node's details in the detail pane. + void UpdateDetail(); + void UpdateStatusBar(); + // Sets the status images of a test's node and of the group and suite nodes holding it. + void UpdateTestNodeImages(std::size_t testIndex); + // Writes the latest run's JUnit report to 'path', noting the outcome in the log. False, with 'error' set to + // why, if it couldn't be written. See the definition. + bool WriteJUnitReport(std::string const& path, std::string& error); + +public: + // A window in 'theme''s look, which must outlive it. + explicit TWin32GUIMainWindow(TWin32GUITheme& theme); + ~TWin32GUIMainWindow(); + + TWin32GUIMainWindow(TWin32GUIMainWindow const&) = delete; + TWin32GUIMainWindow& operator=(TWin32GUIMainWindow const&) = delete; + + // Creates the window, hidden, at its default size, centered on its monitor. Throws std::runtime_error if it + // can't be created. + void Create(); + // The process exit code: with --exit, the one the console runner would have returned for the --run run; + // otherwise ExitCode_Success, since the GUI was used interactively. + int ExitCode() const; + HWND Handle() const; + // Queues 'action' to run on the window's thread from its message loop, after the messages already waiting: + // the counterpart of the VCL's TThread::ForceQueue(). Safe to call from any thread. + void PostToUI(std::function action); + // Runs the window's message loop until it's closed. + void RunMessageLoop(); + // Shows the window as WinMain()'s 'showCommand' says, but maximized if the restored layout was and nothing + // else was asked for. + void Show(int showCommand); + // Loads the registered tests, applies the command line's options, and restores the saved layout and selection; + // see the definition. 'theme' and 'darkTheme' are the View > Theme and Dark Theme choices the window's theme + // was set up for, and 'themeProblems' what TWin32GUITheme::SetDarkTheme() found wrong, noted in the log. Call + // after Create(), before Show(). + void Start(TGUIOptions const& options, EGUITheme theme, std::string const& darkTheme, + std::vector const& themeProblems); + // Whether a run ended with a test that timed out or crashed. A timed-out test's abandoned thread may still be + // running, using one of the test groups, and after a crash their state may be corrupted, so the caller should + // then end the process, as the console runner does, without destroying this window and its tests. + bool WasRunAborted() const; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_Win32GUI_MainWindowH diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Manifest.rc b/win32/gui/src/ASWUnitTests_Win32GUI_Manifest.rc new file mode 100644 index 0000000..a3c7439 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Manifest.rc @@ -0,0 +1,30 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_Manifest.rc +Author: Anthony S. West - ASW Software + +Embeds the Win32 GUI runner's manifest in MinGW-w64 builds (GCC and Clang), +which have no other way to embed one; MSVC and RAD Studio embed the same +.manifest file themselves. Resource 1 of type 24 is +CREATEPROCESS_MANIFEST_RESOURCE_ID of type RT_MANIFEST. + +No LANGUAGE statement: GNU ld silently drops a language-neutral manifest +when another one is linked, which would drop this one in favor of the +default manifest some MinGW-w64 distributions link into every program. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +1 24 "ASWUnitTests_Win32GUI.manifest" diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_MessageDialog.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_MessageDialog.cpp new file mode 100644 index 0000000..3e269e7 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_MessageDialog.cpp @@ -0,0 +1,355 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_MessageDialog.cpp +Author: Anthony S. West - ASW Software + +The Win32 GUI runner's message box. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_Win32GUI_MessageDialog.h" +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Utils.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_DialogTemplate.h" +#include "ASWUnitTests_Win32GUI_DPI.h" +#include "ASWUnitTests_Win32GUI_System.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +// Posted to the dialog when it moves to a monitor with another DPI, so its icon is reloaded and it's laid out +// again after Windows has rescaled it and its controls' fonts. +UINT const ApplyDPIMessage = WM_APP + 1; +// The controls' IDs; the OK button's is IDOK. +WORD const IconID = 100; +WORD const MessageID = 101; +// The OK button's subclass, for Ctrl+C. +UINT_PTR const OKButtonSubclassID = 1; + +///////////////////////////////////////////////////////////////////////////// +// TMessageDialogState +// +// What the dialog shows, and the icon it loads for its DPI. +///////////////////////////////////////////////////////////////////////////// +struct TMessageDialogState +{ + std::wstring Caption; + HICON Icon = nullptr; + EWin32GUIMessageIcon IconKind = EWin32GUIMessageIcon::Error; + std::wstring Text; + TWin32GUITheme* Theme = nullptr; +}; + +//--------------------------------------------------------------------------- + +void ApplyDPI(HWND dialog); +TWin32GUIDialogTemplate BuildDialogTemplate(TMessageDialogState const& state); +void CenterOnOwner(HWND dialog); +INT_PTR CALLBACK DialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM lParam); +INT_PTR HandleDialogMessage(HWND dialog, UINT message, WPARAM wParam, LPARAM lParam); +void LayOutDialog(HWND dialog); +LRESULT CALLBACK OKButtonProc(HWND button, UINT message, WPARAM wParam, LPARAM lParam, UINT_PTR id, + DWORD_PTR state); +UINT SoundOf(EWin32GUIMessageIcon icon); + +//--------------------------------------------------------------------------- +/* + ApplyDPI + + Loads the icon at the size Windows' icons have at the dialog's DPI, scaled down from a larger one if + needed, then lays the dialog out again. +*/ +void ApplyDPI(HWND dialog) +{ + TMessageDialogState* const state = reinterpret_cast(GetWindowLongPtrW(dialog, DWLP_USER)); + UINT const dpi = WindowDPI(dialog); + int const size = GetSystemMetricsForDpi(SM_CXICON, dpi); + + // IDI_ERROR and IDI_WARNING; the W versions of those macros need UNICODE. + HICON icon = nullptr; + LoadIconWithScaleDown(nullptr, MAKEINTRESOURCEW(state->IconKind == EWin32GUIMessageIcon::Error ? 32513 : 32515), + size, size, &icon); + + if (icon != nullptr) + { + SendDlgItemMessageW(dialog, IconID, STM_SETICON, reinterpret_cast(icon), 0); + + if (state->Icon != nullptr) + DestroyIcon(state->Icon); + + state->Icon = icon; + } + + LayOutDialog(dialog); +} +//--------------------------------------------------------------------------- +/* + BuildDialogTemplate + + LayOutDialog() sets the positions and sizes. +*/ +TWin32GUIDialogTemplate BuildDialogTemplate(TMessageDialogState const& state) +{ + TWin32GUIDialogTemplate dialogTemplate(WS_POPUP | WS_CAPTION | WS_SYSMENU | DS_MODALFRAME, state.Caption); + + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Static, IconID, WS_CHILD | WS_VISIBLE | SS_ICON | + SS_REALSIZECONTROL, 0, std::wstring()); + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Static, MessageID, WS_CHILD | WS_VISIBLE | SS_LEFT | + SS_NOPREFIX, 0, state.Text); + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Button, IDOK, WS_CHILD | WS_VISIBLE | WS_TABSTOP | + BS_DEFPUSHBUTTON, 0, L"OK"); + + return dialogTemplate; +} +//--------------------------------------------------------------------------- +/* + CenterOnOwner + + As MessageBoxW() places itself: centered on its owner, or on the work area of its monitor if it has none, + kept within that work area. +*/ +void CenterOnOwner(HWND dialog) +{ + HWND const owner = GetWindow(dialog, GW_OWNER); + MONITORINFO monitor = {}; + monitor.cbSize = sizeof(monitor); + GetMonitorInfoW(MonitorFromWindow(owner != nullptr ? owner : dialog, MONITOR_DEFAULTTONEAREST), &monitor); + + RECT const& work = monitor.rcWork; + RECT around = work; + + if (owner != nullptr) + GetWindowRect(owner, &around); + + RECT bounds; + GetWindowRect(dialog, &bounds); + int const width = bounds.right - bounds.left; + int const height = bounds.bottom - bounds.top; + + int const left = (std::max)(static_cast(work.left), (std::min)(static_cast(work.right) - width, + static_cast(around.left + (around.right - around.left - width) / 2))); + int const top = (std::max)(static_cast(work.top), (std::min)(static_cast(work.bottom) - height, + static_cast(around.top + (around.bottom - around.top - height) / 2))); + + SetWindowPos(dialog, nullptr, left, top, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); +} +//--------------------------------------------------------------------------- +/* + DialogProc + + No exception may leave a dialog procedure, since unwinding through Windows' own frames is undefined. One + that would ends the dialog with -1, which ShowMessageDialog() answers with a MessageBoxW(). +*/ +INT_PTR CALLBACK DialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM lParam) +{ + try + { + return HandleDialogMessage(dialog, message, wParam, lParam); + } + catch (...) + { + EndDialog(dialog, -1); + return TRUE; + } +} +//--------------------------------------------------------------------------- +INT_PTR HandleDialogMessage(HWND dialog, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { + case WM_INITDIALOG: + { + SetWindowLongPtrW(dialog, DWLP_USER, lParam); + TMessageDialogState* const state = reinterpret_cast(lParam); + + state->Theme->AttachWindow(dialog); + SetWindowSubclass(GetDlgItem(dialog, IDOK), &OKButtonProc, OKButtonSubclassID, + reinterpret_cast(state)); + + // Shown before the main window exists, it has a taskbar button of its own. + if (GetWindow(dialog, GW_OWNER) == nullptr) + SendMessageW(dialog, WM_SETICON, ICON_BIG, reinterpret_cast(MainIcon())); + + ApplyDPI(dialog); + CenterOnOwner(dialog); + MessageBeep(SoundOf(state->IconKind)); + + SetFocus(GetDlgItem(dialog, IDOK)); + return FALSE; // The focus is set. + } + + case WM_COMMAND: + if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) + { + EndDialog(dialog, IDOK); + return TRUE; + } + + return FALSE; + + case WM_DPICHANGED: + PostMessageW(dialog, ApplyDPIMessage, 0, 0); + return FALSE; // Windows moves and rescales the dialog. + + default: + if (message == ApplyDPIMessage) + { + ApplyDPI(dialog); + return TRUE; + } + + return FALSE; + } +} +//--------------------------------------------------------------------------- +/* + LayOutDialog + + As a MessageBoxW() at the dialog's DPI: the icon at the top left, the message to its right, wrapped at 400 + pixels at 96 DPI and centered on the icon if it's shorter, and the OK button centered below both, as the + VCL GUI runner's message dialogs have it. Then sizes the dialog to fit, keeping its position. +*/ +void LayOutDialog(HWND dialog) +{ + UINT const dpi = WindowDPI(dialog); + int const margin = ScaleForDPI(12, dpi); + int const iconSize = GetSystemMetricsForDpi(SM_CXICON, dpi); + HWND const messageLabel = GetDlgItem(dialog, MessageID); + + std::wstring const text = UTF8ToWide(WindowText(messageLabel)); + RECT bounds = { 0, 0, ScaleForDPI(400, dpi), 0 }; + HDC const context = GetDC(messageLabel); + + if (context == nullptr) + throw std::runtime_error("Couldn't measure the message"); + + HGDIOBJ const oldFont = SelectObject(context, reinterpret_cast(SendMessageW(messageLabel, WM_GETFONT, 0, + 0))); + DrawTextW(context, text.c_str(), -1, &bounds, DT_CALCRECT | DT_WORDBREAK | DT_NOPREFIX | DT_EXPANDTABS); + SelectObject(context, oldFont); + ReleaseDC(messageLabel, context); + + int const textWidth = bounds.right; + int const textHeight = bounds.bottom; + int const textLeft = margin + iconSize + margin; + int const textTop = margin + (std::max)(0, (iconSize - textHeight) / 2); + + MoveWindow(GetDlgItem(dialog, IconID), margin, margin, iconSize, iconSize, TRUE); + MoveWindow(messageLabel, textLeft, textTop, textWidth, textHeight, TRUE); + + int const buttonWidth = ScaleForDPI(80, dpi); + int const buttonHeight = ScaleForDPI(26, dpi); + int const width = (std::max)(textLeft + textWidth + margin, buttonWidth + 2 * margin); + int const buttonTop = (std::max)(margin + iconSize, textTop + textHeight) + margin + ScaleForDPI(4, dpi); + MoveWindow(GetDlgItem(dialog, IDOK), (width - buttonWidth) / 2, buttonTop, buttonWidth, buttonHeight, TRUE); + + RECT frame = { 0, 0, width, buttonTop + buttonHeight + margin }; + AdjustWindowRectExForDpi(&frame, static_cast(GetWindowLongPtrW(dialog, GWL_STYLE)), FALSE, + static_cast(GetWindowLongPtrW(dialog, GWL_EXSTYLE)), dpi); + SetWindowPos(dialog, nullptr, 0, 0, frame.right - frame.left, frame.bottom - frame.top, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); +} +//--------------------------------------------------------------------------- +/* + OKButtonProc + + The OK button, which has the focus, copies the dialog as text on Ctrl+C, laid out as a MessageBoxW() + copies itself. No exception may leave a window procedure; a failure to copy is ignored, as it would be in + a MessageBoxW(). +*/ +LRESULT CALLBACK OKButtonProc(HWND button, UINT message, WPARAM wParam, LPARAM lParam, UINT_PTR id, DWORD_PTR state) +{ + if (message == WM_NCDESTROY) + { + RemoveWindowSubclass(button, &OKButtonProc, id); + } + else if (message == WM_KEYDOWN && (wParam == 'C' || wParam == VK_INSERT) && + (GetKeyState(VK_CONTROL) & 0x8000) != 0) + { + try + { + TMessageDialogState const* const dialog = reinterpret_cast(state); + std::string const line = "---------------------------\r\n"; + SetClipboardText(button, line + WideToUTF8(dialog->Caption) + "\r\n" + line + WideToUTF8(dialog->Text) + + "\r\n" + line + "OK\r\n" + line); + } + catch (...) + { + } + + return 0; + } + + return DefSubclassProc(button, message, wParam, lParam); +} +//--------------------------------------------------------------------------- +UINT SoundOf(EWin32GUIMessageIcon icon) +{ + return icon == EWin32GUIMessageIcon::Error ? MB_ICONERROR : MB_ICONWARNING; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +//--------------------------------------------------------------------------- +void ShowMessageDialog(HWND owner, TWin32GUITheme& theme, std::string const& caption, std::string const& text, + EWin32GUIMessageIcon icon) +{ + try + { + TMessageDialogState state; + state.Caption = UTF8ToWide(caption); + state.IconKind = icon; + state.Text = UTF8ToWide(AdjustLineBreaks(text)); + state.Theme = &theme; + + TWin32GUIDialogTemplate const dialogTemplate = BuildDialogTemplate(state); + INT_PTR const result = DialogBoxIndirectParamW(GetModuleHandleW(nullptr), dialogTemplate.Get(), owner, + &DialogProc, reinterpret_cast(&state)); + + if (state.Icon != nullptr) + DestroyIcon(state.Icon); + + if (result != -1) + return; + } + catch (...) + { + } + + try + { + MessageBoxW(owner, UTF8ToWide(text).c_str(), UTF8ToWide(caption).c_str(), MB_OK | SoundOf(icon)); + } + catch (...) + { + } +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_MessageDialog.h b/win32/gui/src/ASWUnitTests_Win32GUI_MessageDialog.h new file mode 100644 index 0000000..17717c9 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_MessageDialog.h @@ -0,0 +1,62 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_MessageDialog.h +Author: Anthony S. West - ASW Software + +The Win32 GUI runner's message box: an icon, a message and an OK button, as +MessageBoxW() shows them, but in the GUI's theme. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_Win32GUI_MessageDialogH +#define ASWUnitTests_Win32GUI_MessageDialogH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Theme.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// EWin32GUIMessageIcon +// +// The icon ShowMessageDialog() shows, and the sound it plays, as +// MessageBoxW()'s MB_ICONERROR and MB_ICONWARNING. +///////////////////////////////////////////////////////////////////////////// +enum class EWin32GUIMessageIcon +{ + Error, + Warning, +}; + +//--------------------------------------------------------------------------- + +// Shows 'text' (UTF-8) in a modal dialog titled 'caption', owned by 'owner' (nullptr for none), with 'icon' and +// an OK button, in 'theme''s look, and waits until it's closed. As in a MessageBoxW(), its sound plays, the +// message wraps, and Ctrl+C copies the caption, the message and the button's text. Throws nothing: if the +// dialog can't be shown, a MessageBoxW() is. +void ShowMessageDialog(HWND owner, TWin32GUITheme& theme, std::string const& caption, std::string const& text, + EWin32GUIMessageIcon icon); + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_Win32GUI_MessageDialogH diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Palette.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_Palette.cpp new file mode 100644 index 0000000..12da01f --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Palette.cpp @@ -0,0 +1,949 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_Palette.cpp +Author: Anthony S. West - ASW Software + +The colors of the Win32 GUI runner's dark theme. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_Win32GUI_Palette.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +#include +#include +#include +//--------------------------------------------------------------------------- + +namespace +{ + +//--------------------------------------------------------------------------- + +COLORREF FromHSL(double hue, double saturation, double lightness); +COLORREF FromRGB(std::uint32_t rgb); +bool SameText(std::string const& a, std::string const& b); +void ToHSL(COLORREF color, double& hue, double& saturation, double& lightness); +std::uint32_t ToRGB(COLORREF color); + +//--------------------------------------------------------------------------- +/* + FromHSL + + The color with 'hue' (in degrees, any number of turns), 'saturation' and 'lightness' (0 to 1 each), each + channel rounded to the nearest. +*/ +COLORREF FromHSL(double hue, double saturation, double lightness) +{ + double const chroma = (1.0 - std::fabs(2.0 * lightness - 1.0)) * saturation; + double const sector = std::fmod(std::fmod(hue, 360.0) + 360.0, 360.0) / 60.0; + double const second = chroma * (1.0 - std::fabs(std::fmod(sector, 2.0) - 1.0)); + double red = 0.0; + double green = 0.0; + double blue = 0.0; + + switch (static_cast(sector)) + { + case 0: + red = chroma; + green = second; + break; + + case 1: + red = second; + green = chroma; + break; + + case 2: + green = chroma; + blue = second; + break; + + case 3: + green = second; + blue = chroma; + break; + + case 4: + red = second; + blue = chroma; + break; + + default: + red = chroma; + blue = second; + break; + } + + double const lowest = lightness - chroma / 2.0; + auto const channel = [lowest](double value) + { + return static_cast(std::lround((std::min)(1.0, (std::max)(0.0, value + lowest)) * 255.0)); + }; + + return RGB(channel(red), channel(green), channel(blue)); +} +//--------------------------------------------------------------------------- +/* + FromRGB + + A theme file's color, 0xRRGGBB, as a COLORREF, 0x00BBGGRR. +*/ +COLORREF FromRGB(std::uint32_t rgb) +{ + return RGB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); +} +//--------------------------------------------------------------------------- +/* + SameText + + Whether 'a' and 'b' are the same, ignoring the case of ASCII letters. +*/ +bool SameText(std::string const& a, std::string const& b) +{ + auto const lower = [](char c) + { + return c >= 'A' && c <= 'Z' ? static_cast(c - 'A' + 'a') : c; + }; + + if (a.size() != b.size()) + return false; + + for (std::size_t i = 0; i < a.size(); ++i) + { + if (lower(a[i]) != lower(b[i])) + return false; + } + + return true; +} +//--------------------------------------------------------------------------- +/* + ToHSL + + The color's hue (in degrees, 0 up to 360; 0 for a gray), saturation and lightness (0 to 1 each). +*/ +void ToHSL(COLORREF color, double& hue, double& saturation, double& lightness) +{ + double const red = GetRValue(color) / 255.0; + double const green = GetGValue(color) / 255.0; + double const blue = GetBValue(color) / 255.0; + double const highest = (std::max)(red, (std::max)(green, blue)); + double const lowest = (std::min)(red, (std::min)(green, blue)); + double const chroma = highest - lowest; + + lightness = (highest + lowest) / 2.0; + saturation = chroma <= 0.0 ? 0.0 : chroma / (1.0 - std::fabs(2.0 * lightness - 1.0)); + + if (chroma <= 0.0) + hue = 0.0; + else if (highest == red) + hue = 60.0 * std::fmod((green - blue) / chroma + 6.0, 6.0); + else if (highest == green) + hue = 60.0 * ((blue - red) / chroma + 2.0); + else + hue = 60.0 * ((red - green) / chroma + 4.0); +} +//--------------------------------------------------------------------------- +/* + ToRGB + + A COLORREF as a theme file's color, 0xRRGGBB. +*/ +std::uint32_t ToRGB(COLORREF color) +{ + return (static_cast(GetRValue(color)) << 16) | (static_cast(GetGValue(color)) << 8) | + static_cast(GetBValue(color)); +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +//--------------------------------------------------------------------------- +COLORREF BlendColors(COLORREF from, COLORREF to, int numerator, int denominator) +{ + if (denominator <= 0) + return from; + + int const part = (std::max)(0, (std::min)(numerator, denominator)); + auto const channel = [part, denominator](int a, int b) + { + return static_cast((a * (denominator - part) + b * part + denominator / 2) / denominator); + }; + + return RGB(channel(GetRValue(from), GetRValue(to)), channel(GetGValue(from), GetGValue(to)), + channel(GetBValue(from), GetBValue(to))); +} +//--------------------------------------------------------------------------- +/* + BuiltInDarkThemes + + Each tint's hue and strength, and its accent's hue, chosen by eye in the runner. The VCL styles' names are + GUIVCLStyleDarkThemeNames()'s. +*/ +std::vector const& BuiltInDarkThemes() +{ + static std::vector const themes = { + { GUIDefaultDarkThemeName(), CarbonPalette() }, + { "Charcoal Dark Slate", CharcoalDarkSlatePalette() }, + { "Cobalt XEMedia", CobaltXEMediaPalette() }, + { "Ember", TintedCarbonPalette(25.0, 0.09, 30.0) }, + { "Forest", TintedCarbonPalette(140.0, 0.08, 155.0) }, + { "Midnight", TintedCarbonPalette(220.0, 0.10, 205.0) }, + { "Onyx Blue", OnyxBluePalette() }, + { "Plum", TintedCarbonPalette(285.0, 0.09, 295.0) }, + { "Tablet Dark", TabletDarkPalette() } + }; + + return themes; +} +//--------------------------------------------------------------------------- +/* + CarbonPalette + + Most colors are Carbon.vsf's own, from TStyleManager::ActiveStyle's system, style and font colors and + from pixels of the elements it draws; the selection, the tree's lines, the failed progress bar and the + edit border are sampled from the VCL GUI runner under Carbon (plans/1.5/theme-win32-handoff.md). +*/ +TWin32GUIPalette CarbonPalette() +{ + TWin32GUIPalette palette; + + palette.ButtonBorder = RGB(0x49, 0x49, 0x49); + palette.ButtonDefaultFrame = RGB(0x24, 0x61, 0x7A); + palette.ButtonDisabledBorder = RGB(0x3A, 0x3A, 0x3A); + palette.ButtonDisabledFaceBottom = RGB(0x30, 0x30, 0x30); + palette.ButtonDisabledFaceTop = RGB(0x3C, 0x3C, 0x3C); + palette.ButtonDisabledOutline = RGB(0x2C, 0x2C, 0x2C); + palette.ButtonFaceBottom = RGB(0x31, 0x31, 0x31); + palette.ButtonFaceTop = RGB(0x43, 0x43, 0x43); + palette.ButtonHotBorder = RGB(0x57, 0x57, 0x57); + palette.ButtonHotFaceBottom = RGB(0x35, 0x35, 0x35); + palette.ButtonHotFaceTop = RGB(0x47, 0x47, 0x47); + palette.ButtonHotText = RGB(0xFF, 0xFF, 0xFF); + palette.ButtonOutline = RGB(0x19, 0x19, 0x19); + palette.ButtonPressedFaceBottom = RGB(0x48, 0x48, 0x48); + palette.ButtonPressedFaceTop = RGB(0x37, 0x37, 0x37); + palette.ButtonText = RGB(0xC0, 0xC0, 0xC0); + palette.CheckBoxBorder = RGB(0x47, 0x47, 0x47); + palette.CheckBoxFill = RGB(0x19, 0x19, 0x19); + palette.CheckBoxHotBorder = RGB(0x5F, 0x5F, 0x5F); + palette.CheckMark = RGB(0x84, 0x84, 0x84); + palette.CheckMarkDisabled = RGB(0x53, 0x53, 0x53); + palette.ControlText = RGB(0xC0, 0xC0, 0xC0); + palette.DisabledText = RGB(0x80, 0x80, 0x80); + palette.EditBorderInner = RGB(0x4C, 0x4C, 0x4C); + palette.EditBorderOuter = RGB(0x2E, 0x2E, 0x2E); + palette.GripDot = RGB(0x1A, 0x1A, 0x1A); + palette.HeaderBottom = RGB(0x35, 0x35, 0x35); + palette.HeaderDivider = RGB(0x2E, 0x2E, 0x2E); + palette.HeaderHot = RGB(0x4C, 0x4C, 0x4C); + palette.HeaderTop = RGB(0x3D, 0x3D, 0x3D); + palette.Highlight = RGB(0x26, 0x5A, 0x73); + palette.HighlightFrame = RGB(0x20, 0x20, 0x20); + palette.HotText = RGB(0xFF, 0xFF, 0xFF); + palette.MenuBackground = RGB(0x19, 0x19, 0x19); + palette.MenuBarDisabledText = RGB(0x59, 0x59, 0x59); + palette.MenuBorderInner = RGB(0x41, 0x41, 0x41); + palette.MenuBorderOuter = RGB(0x2E, 0x2E, 0x2E); + palette.MenuDisabledText = RGB(0x4F, 0x4F, 0x4F); + palette.MenuSeparator = RGB(0x84, 0x84, 0x84); + palette.Panel = RGB(0x33, 0x33, 0x33); + palette.ProgressBorder = RGB(0x2B, 0x2B, 0x2B); + palette.ProgressChunkBottom = RGB(0x27, 0x54, 0x6A); + palette.ProgressChunkTop = RGB(0x24, 0x61, 0x7A); + palette.ProgressFailedBottom = RGB(0x95, 0x29, 0x34); + palette.ProgressFailedTop = RGB(0x94, 0x30, 0x3C); + palette.ProgressTrack = RGB(0x19, 0x19, 0x19); + palette.Selection = RGB(0x62, 0x62, 0x62); + palette.SelectionFrame = RGB(0x38, 0x38, 0x38); + palette.SelectionText = RGB(0xFF, 0xFF, 0xFF); + palette.SpinArrow = RGB(0x1C, 0x1C, 0x1C); + palette.SpinDisabledArrow = RGB(0x2A, 0x2A, 0x2A); + palette.SpinDisabledFaceBottom = RGB(0x36, 0x38, 0x37); + palette.SpinDisabledFaceTop = RGB(0x42, 0x42, 0x42); + palette.SpinDisabledOutline = RGB(0x14, 0x14, 0x14); + palette.SpinFaceBottom = RGB(0x4B, 0x4D, 0x4C); + palette.SpinFaceTop = RGB(0x5B, 0x5B, 0x5B); + palette.SpinHotFaceBottom = RGB(0x5D, 0x5E, 0x5E); + palette.SpinHotFaceTop = RGB(0x6B, 0x6B, 0x6B); + palette.SpinOutline = RGB(0x1B, 0x1B, 0x1B); + palette.SpinPressedEdge = RGB(0x45, 0x45, 0x45); + palette.StatusLine = RGB(0x2E, 0x2E, 0x2E); + palette.TabActiveBottom = RGB(0x35, 0x35, 0x35); + palette.TabActiveTop = RGB(0x3C, 0x3C, 0x3C); + palette.TabBorder = RGB(0x41, 0x41, 0x41); + palette.TabHot = RGB(0x33, 0x33, 0x33); + palette.TabInactive = RGB(0x27, 0x27, 0x27); + palette.TabInactiveBorder = RGB(0x2B, 0x2B, 0x2B); + palette.TabOuter = RGB(0x14, 0x14, 0x14); + palette.TooltipBackground = RGB(0x51, 0x51, 0x51); + palette.TooltipText = RGB(0xEA, 0xEA, 0xEA); + palette.TreeLine = RGB(0x6D, 0x6D, 0x6D); + palette.Window = RGB(0x19, 0x19, 0x19); + palette.WindowText = RGB(0xEA, 0xEA, 0xEA); + // As the VCL GUI runner's on its dark window color. + palette.FailureText = FailureTextColorOn(palette.Window); + palette.SkipText = SkipTextColorOn(palette.Window); + // The Win32 GUI runner's own, which the light look has too. + palette.TreeStatusFailed = RGB(215, 40, 40); + palette.TreeStatusNotRun = RGB(190, 190, 190); + palette.TreeStatusPassed = RGB(16, 160, 16); + palette.TreeStatusRunning = RGB(0, 120, 215); + palette.TreeStatusSkipped = RGB(230, 165, 0); + + return palette; +} +//--------------------------------------------------------------------------- +/* + CarbonThemeFileText + + The roles' groups are separated by a blank line, each under its comment. +*/ +std::string CarbonThemeFileText() +{ + TWin32GUIPalette const palette = CarbonPalette(); + std::string text = "; An ASWUnitTests theme file: the colors of the Win32 GUI runner's dark look.\r\n" + ";\r\n" + "; In the Themes folder (View > Theme > Dark Theme > Open Themes Folder), it's listed in View > Theme >\r\n" + "; Dark Theme by its file name, and choosing it there uses it whenever the window is dark. Choosing it\r\n" + "; again reads it again, so a change shows at once.\r\n" + ";\r\n" + "; Each line under [Colors] sets a role's color, as #RRGGBB. These are Carbon's, the built-in dark\r\n" + "; theme's, and a role left out keeps Carbon's color, so the lines left as they are can be deleted.\r\n" + "; Problems, such as a misspelled role, are reported in the Log tab.\r\n" + "\r\n" + "[Theme]\r\n" + "Base=Dark\r\n" + "\r\n" + "[Colors]\r\n"; + bool first = true; + + for (TWin32GUIPaletteRole const& role : PaletteRoles()) + { + if (role.Group != nullptr) + { + if (!first) + text += "\r\n"; + + first = false; + std::string const group = role.Group; + std::size_t start = 0; + + while (start <= group.size()) + { + std::size_t end = group.find('\n', start); + + if (end == std::string::npos) + end = group.size(); + + text += "; " + group.substr(start, end - start) + "\r\n"; + start = end + 1; + } + } + + text += std::string(role.Name) + "=" + FormatGUIThemeColor(ToRGB(palette.*role.Color)) + "\r\n"; + } + + return text; +} +//--------------------------------------------------------------------------- +/* + CharcoalDarkSlatePalette + + The colors of RAD Studio's Charcoal Dark Slate style, read from its .vsf file as CarbonPalette()'s were + (plans/1.5/experiments/style_palette.dpr), with the values its elements don't give taken from the VCL GUI + runner under it; the selection and the colors that say how a test did are Carbon's, as the VCL GUI + runner's are under every style. +*/ +TWin32GUIPalette CharcoalDarkSlatePalette() +{ + TWin32GUIPalette palette = CarbonPalette(); + + palette.ButtonBorder = RGB(0x43, 0x43, 0x43); + palette.ButtonDefaultFrame = RGB(0x32, 0x32, 0x32); + palette.ButtonDisabledBorder = RGB(0x45, 0x45, 0x45); + palette.ButtonDisabledFaceBottom = RGB(0x39, 0x39, 0x39); + palette.ButtonDisabledFaceTop = RGB(0x4C, 0x4C, 0x4C); + palette.ButtonDisabledOutline = RGB(0x2F, 0x2F, 0x2F); + palette.ButtonFaceBottom = RGB(0x28, 0x28, 0x28); + palette.ButtonFaceTop = RGB(0x40, 0x40, 0x40); + palette.ButtonHotBorder = RGB(0x4A, 0x4A, 0x4A); + palette.ButtonHotFaceBottom = RGB(0x1F, 0x1F, 0x1F); + palette.ButtonHotFaceTop = RGB(0x32, 0x32, 0x32); + palette.ButtonHotText = RGB(0xFF, 0xFF, 0xFF); + palette.ButtonOutline = RGB(0x27, 0x27, 0x27); + palette.ButtonPressedFaceBottom = RGB(0x20, 0x20, 0x20); + palette.ButtonPressedFaceTop = RGB(0x14, 0x14, 0x14); + palette.ButtonText = RGB(0xA3, 0xA3, 0xA3); + palette.CheckBoxBorder = RGB(0x5D, 0x5D, 0x5D); + palette.CheckBoxFill = RGB(0x1F, 0x1F, 0x1F); + palette.CheckBoxHotBorder = RGB(0x70, 0x70, 0x70); + palette.CheckMark = RGB(0x7B, 0x7B, 0x7B); + palette.CheckMarkDisabled = RGB(0x4D, 0x4D, 0x4D); + palette.ControlText = RGB(0xC0, 0xC0, 0xC0); + palette.DisabledText = RGB(0x80, 0x80, 0x80); + palette.EditBorderInner = RGB(0x5E, 0x5E, 0x5E); + palette.EditBorderOuter = RGB(0x36, 0x36, 0x36); + palette.GripDot = RGB(0x41, 0x41, 0x41); + palette.HeaderBottom = RGB(0x3E, 0x3E, 0x3E); + palette.HeaderDivider = RGB(0x2D, 0x2D, 0x2D); + palette.HeaderHot = RGB(0x43, 0x43, 0x43); + palette.HeaderTop = RGB(0x49, 0x49, 0x49); + palette.Highlight = RGB(0x5F, 0x5F, 0x5F); + palette.HighlightFrame = RGB(0x8A, 0x8A, 0x8A); + palette.HotText = RGB(0xFF, 0xFF, 0xFF); + palette.MenuBackground = RGB(0x1F, 0x1F, 0x1F); + palette.MenuBarDisabledText = RGB(0x93, 0x93, 0x93); + palette.MenuBorderInner = RGB(0x58, 0x58, 0x58); + palette.MenuBorderOuter = RGB(0x36, 0x36, 0x36); + palette.MenuDisabledText = RGB(0x69, 0x69, 0x69); + palette.MenuSeparator = RGB(0x3B, 0x3B, 0x3B); + palette.Panel = RGB(0x3E, 0x3E, 0x3E); + palette.ProgressBorder = RGB(0x29, 0x29, 0x29); + palette.ProgressChunkBottom = RGB(0x5C, 0x5C, 0x5C); + palette.ProgressChunkTop = RGB(0x5C, 0x5C, 0x5C); + palette.ProgressTrack = RGB(0x31, 0x31, 0x31); + palette.SelectionText = RGB(0xFF, 0xFF, 0xFF); + palette.SpinArrow = RGB(0x7D, 0x7D, 0x7D); + palette.SpinDisabledArrow = RGB(0x40, 0x40, 0x40); + palette.SpinDisabledFaceBottom = RGB(0x23, 0x23, 0x23); + palette.SpinDisabledFaceTop = RGB(0x23, 0x23, 0x23); + palette.SpinDisabledOutline = RGB(0x23, 0x23, 0x23); + palette.SpinFaceBottom = RGB(0x23, 0x23, 0x23); + palette.SpinFaceTop = RGB(0x23, 0x23, 0x23); + palette.SpinHotFaceBottom = RGB(0x23, 0x23, 0x23); + palette.SpinHotFaceTop = RGB(0x23, 0x23, 0x23); + palette.SpinOutline = RGB(0x23, 0x23, 0x23); + palette.SpinPressedEdge = RGB(0x23, 0x23, 0x23); + palette.StatusLine = RGB(0x40, 0x40, 0x40); + palette.TabActiveBottom = RGB(0x3E, 0x3E, 0x3E); + palette.TabActiveTop = RGB(0x57, 0x57, 0x57); + palette.TabBorder = RGB(0x40, 0x40, 0x40); + palette.TabHot = RGB(0x25, 0x25, 0x25); + palette.TabInactive = RGB(0x25, 0x25, 0x25); + palette.TabInactiveBorder = RGB(0x23, 0x23, 0x23); + palette.TabOuter = RGB(0x23, 0x23, 0x23); + palette.TooltipBackground = RGB(0x5B, 0x5B, 0x5B); + palette.TooltipText = RGB(0xFF, 0xFF, 0xFF); + palette.Window = RGB(0x23, 0x23, 0x23); + palette.WindowText = RGB(0xC0, 0xC0, 0xC0); + + palette.FailureText = FailureTextColorOn(palette.Window); + palette.SkipText = SkipTextColorOn(palette.Window); + palette.TreeLine = BlendColors(palette.Window, palette.WindowText, 2, 5); + return palette; +} +//--------------------------------------------------------------------------- +/* + CobaltXEMediaPalette + + The colors of RAD Studio's Cobalt XEMedia style, read from its .vsf file as CarbonPalette()'s were + (plans/1.5/experiments/style_palette.dpr), with the values its elements don't give taken from the VCL GUI + runner under it; the selection and the colors that say how a test did are Carbon's, as the VCL GUI + runner's are under every style. +*/ +TWin32GUIPalette CobaltXEMediaPalette() +{ + TWin32GUIPalette palette = CarbonPalette(); + + palette.ButtonBorder = RGB(0x07, 0x0B, 0x11); + palette.ButtonDefaultFrame = RGB(0x5A, 0x77, 0x9B); + palette.ButtonDisabledBorder = RGB(0x09, 0x0A, 0x0B); + palette.ButtonDisabledFaceBottom = RGB(0x16, 0x18, 0x1C); + palette.ButtonDisabledFaceTop = RGB(0x2B, 0x2E, 0x32); + palette.ButtonDisabledOutline = RGB(0x14, 0x17, 0x1A); + palette.ButtonFaceBottom = RGB(0x1A, 0x33, 0x53); + palette.ButtonFaceTop = RGB(0x46, 0x53, 0x66); + palette.ButtonHotBorder = RGB(0x07, 0x0F, 0x1C); + palette.ButtonHotFaceBottom = RGB(0x2A, 0x79, 0xB0); + palette.ButtonHotFaceTop = RGB(0x5A, 0x77, 0x9B); + palette.ButtonHotText = RGB(0xFF, 0xFF, 0xFF); + palette.ButtonOutline = RGB(0x11, 0x1A, 0x28); + palette.ButtonPressedFaceBottom = RGB(0x15, 0x4D, 0x75); + palette.ButtonPressedFaceTop = RGB(0x38, 0x4C, 0x66); + palette.ButtonText = RGB(0xC0, 0xC0, 0xC0); + palette.CheckBoxBorder = RGB(0x39, 0x57, 0x8B); + palette.CheckBoxFill = RGB(0x17, 0x2E, 0x4D); + palette.CheckBoxHotBorder = RGB(0x54, 0x6E, 0x9C); + palette.CheckMark = RGB(0x27, 0x75, 0xB8); + palette.CheckMarkDisabled = RGB(0x1B, 0x49, 0x74); + palette.ControlText = RGB(0xC0, 0xC0, 0xC0); + palette.DisabledText = RGB(0x41, 0x41, 0x41); + palette.EditBorderInner = RGB(0x04, 0x06, 0x0C); + palette.EditBorderOuter = RGB(0x18, 0x31, 0x53); + palette.GripDot = RGB(0x1B, 0x2B, 0x45); + palette.HeaderBottom = RGB(0x10, 0x17, 0x24); + palette.HeaderDivider = RGB(0x00, 0x00, 0x00); + palette.HeaderHot = RGB(0x0B, 0x11, 0x19); + palette.HeaderTop = RGB(0x19, 0x21, 0x2D); + palette.Highlight = RGB(0x0B, 0x36, 0x65); + palette.HighlightFrame = RGB(0x29, 0x45, 0x69); + palette.HotText = RGB(0xFF, 0xFF, 0xFF); + palette.MenuBackground = RGB(0x00, 0x00, 0x00); + palette.MenuBarDisabledText = RGB(0x41, 0x41, 0x41); + palette.MenuBorderInner = RGB(0x0D, 0x18, 0x25); + palette.MenuBorderOuter = RGB(0x21, 0x39, 0x50); + palette.MenuDisabledText = RGB(0x41, 0x41, 0x41); + palette.MenuSeparator = RGB(0x0B, 0x36, 0x65); + palette.Panel = RGB(0x01, 0x01, 0x01); + palette.ProgressBorder = RGB(0x18, 0x31, 0x53); + palette.ProgressChunkBottom = RGB(0x24, 0x8B, 0xD1); + palette.ProgressChunkTop = RGB(0x86, 0xB1, 0xCC); + palette.ProgressTrack = RGB(0x00, 0x00, 0x00); + palette.SelectionText = RGB(0xFF, 0xFF, 0xFF); + palette.SpinArrow = RGB(0x4C, 0x6D, 0x8F); + palette.SpinDisabledArrow = RGB(0x2D, 0x41, 0x56); + palette.SpinDisabledFaceBottom = RGB(0x02, 0x04, 0x08); + palette.SpinDisabledFaceTop = RGB(0x02, 0x04, 0x08); + palette.SpinDisabledOutline = RGB(0x02, 0x04, 0x08); + palette.SpinFaceBottom = RGB(0x02, 0x04, 0x08); + palette.SpinFaceTop = RGB(0x02, 0x04, 0x08); + palette.SpinHotFaceBottom = RGB(0x02, 0x04, 0x08); + palette.SpinHotFaceTop = RGB(0x02, 0x04, 0x08); + palette.SpinOutline = RGB(0x02, 0x04, 0x08); + palette.SpinPressedEdge = RGB(0x02, 0x04, 0x08); + palette.StatusLine = RGB(0x03, 0x05, 0x07); + palette.TabActiveBottom = RGB(0x0E, 0x13, 0x1C); + palette.TabActiveTop = RGB(0x21, 0x2F, 0x45); + palette.TabBorder = RGB(0x0A, 0x0E, 0x15); + palette.TabHot = RGB(0x05, 0x07, 0x0A); + palette.TabInactive = RGB(0x05, 0x07, 0x0A); + palette.TabInactiveBorder = RGB(0x0D, 0x0D, 0x0D); + palette.TabOuter = RGB(0x32, 0x43, 0x5E); + palette.TooltipBackground = RGB(0x26, 0x46, 0x6C); + palette.TooltipText = RGB(0xC0, 0xC0, 0xC0); + palette.Window = RGB(0x04, 0x07, 0x0E); + palette.WindowText = RGB(0xFF, 0xFF, 0xFF); + + palette.FailureText = FailureTextColorOn(palette.Window); + palette.SkipText = SkipTextColorOn(palette.Window); + palette.TreeLine = BlendColors(palette.Window, palette.WindowText, 2, 5); + return palette; +} +//--------------------------------------------------------------------------- +COLORREF FailureTextColorOn(COLORREF background) +{ + return IsDarkColor(background) ? RGB(255, 100, 100) : RGB(200, 0, 0); +} +//--------------------------------------------------------------------------- +TWin32GUIBuiltInTheme const* FindBuiltInDarkTheme(std::string const& name) +{ + for (TWin32GUIBuiltInTheme const& theme : BuiltInDarkThemes()) + { + if (SameText(theme.Name, name)) + return &theme; + } + + return nullptr; +} +//--------------------------------------------------------------------------- +bool IsDarkColor(COLORREF color) +{ + int const luma = (GetRValue(color) * 299 + GetGValue(color) * 587 + GetBValue(color) * 114) / 1000; + + return luma < 128; +} +//--------------------------------------------------------------------------- +/* + OnyxBluePalette + + The colors of RAD Studio's Onyx Blue style, read from its .vsf file as CarbonPalette()'s were + (plans/1.5/experiments/style_palette.dpr), with the values its elements don't give taken from the VCL GUI + runner under it; the selection and the colors that say how a test did are Carbon's, as the VCL GUI + runner's are under every style. +*/ +TWin32GUIPalette OnyxBluePalette() +{ + TWin32GUIPalette palette = CarbonPalette(); + + palette.ButtonBorder = RGB(0xAD, 0xB0, 0xB4); + palette.ButtonDefaultFrame = RGB(0x74, 0xA0, 0xC4); + palette.ButtonDisabledBorder = RGB(0x36, 0x39, 0x3E); + palette.ButtonDisabledFaceBottom = RGB(0x23, 0x25, 0x28); + palette.ButtonDisabledFaceTop = RGB(0x42, 0x45, 0x49); + palette.ButtonDisabledOutline = RGB(0x2A, 0x2C, 0x30); + palette.ButtonFaceBottom = RGB(0x8C, 0x90, 0x95); + palette.ButtonFaceTop = RGB(0xD8, 0xDA, 0xDC); + palette.ButtonHotBorder = RGB(0x42, 0x72, 0x9A); + palette.ButtonHotFaceBottom = RGB(0x27, 0x41, 0x57); + palette.ButtonHotFaceTop = RGB(0x74, 0xA0, 0xC4); + palette.ButtonHotText = RGB(0xFF, 0xFF, 0xFF); + palette.ButtonOutline = RGB(0x2A, 0x2C, 0x30); + palette.ButtonPressedFaceBottom = RGB(0x3B, 0x5C, 0x77); + palette.ButtonPressedFaceTop = RGB(0x6D, 0x8C, 0xA6); + palette.ButtonText = RGB(0x18, 0x1A, 0x1D); + palette.CheckBoxBorder = RGB(0x6A, 0x6D, 0x72); + palette.CheckBoxFill = RGB(0x23, 0x25, 0x29); + palette.CheckBoxHotBorder = RGB(0x8C, 0x8E, 0x92); + palette.CheckMark = RGB(0xCC, 0xD0, 0xD4); + palette.CheckMarkDisabled = RGB(0x66, 0x69, 0x6E); + palette.ControlText = RGB(0xFF, 0xFF, 0xFF); + palette.DisabledText = RGB(0x80, 0x80, 0x80); + palette.EditBorderInner = RGB(0x29, 0x2C, 0x31); + palette.EditBorderOuter = RGB(0x1C, 0x1C, 0x1C); + palette.GripDot = RGB(0x4E, 0x4F, 0x57); + palette.HeaderBottom = RGB(0x3E, 0x41, 0x46); + palette.HeaderDivider = RGB(0x44, 0x47, 0x4B); + palette.HeaderHot = RGB(0x60, 0x63, 0x67); + palette.HeaderTop = RGB(0x4B, 0x4E, 0x52); + palette.Highlight = RGB(0x44, 0x75, 0x9D); + palette.HighlightFrame = RGB(0x41, 0x70, 0x96); + palette.HotText = RGB(0xFF, 0xFF, 0xFF); + palette.MenuBackground = RGB(0x31, 0x34, 0x39); + palette.MenuBarDisabledText = RGB(0x80, 0x80, 0x80); + palette.MenuBorderInner = RGB(0x3E, 0x41, 0x45); + palette.MenuBorderOuter = RGB(0x18, 0x1A, 0x1D); + palette.MenuDisabledText = RGB(0x80, 0x80, 0x80); + palette.MenuSeparator = RGB(0x3E, 0x40, 0x45); + palette.Panel = RGB(0x31, 0x34, 0x39); + palette.ProgressBorder = RGB(0x1C, 0x1C, 0x1C); + palette.ProgressChunkBottom = RGB(0x23, 0x3F, 0x57); + palette.ProgressChunkTop = RGB(0x69, 0x97, 0xBE); + palette.ProgressTrack = RGB(0x29, 0x2C, 0x31); + palette.SelectionText = RGB(0xFF, 0xFF, 0xFF); + palette.SpinArrow = RGB(0xDD, 0xDE, 0xDF); + palette.SpinDisabledArrow = RGB(0x63, 0x67, 0x6B); + palette.SpinDisabledFaceBottom = RGB(0x26, 0x29, 0x2D); + palette.SpinDisabledFaceTop = RGB(0x26, 0x29, 0x2D); + palette.SpinDisabledOutline = RGB(0x26, 0x29, 0x2D); + palette.SpinFaceBottom = RGB(0x26, 0x29, 0x2D); + palette.SpinFaceTop = RGB(0x26, 0x29, 0x2D); + palette.SpinHotFaceBottom = RGB(0x26, 0x29, 0x2D); + palette.SpinHotFaceTop = RGB(0x26, 0x29, 0x2D); + palette.SpinOutline = RGB(0x26, 0x29, 0x2D); + palette.SpinPressedEdge = RGB(0x26, 0x29, 0x2D); + palette.StatusLine = RGB(0x29, 0x2C, 0x33); + palette.TabActiveBottom = RGB(0x2B, 0x2E, 0x33); + palette.TabActiveTop = RGB(0x35, 0x38, 0x3D); + palette.TabBorder = RGB(0x2C, 0x2F, 0x34); + palette.TabHot = RGB(0x21, 0x24, 0x28); + palette.TabInactive = RGB(0x21, 0x24, 0x28); + palette.TabInactiveBorder = RGB(0x20, 0x23, 0x27); + palette.TabOuter = RGB(0x20, 0x23, 0x27); + palette.TooltipBackground = RGB(0x12, 0x12, 0x12); + palette.TooltipText = RGB(0xFF, 0xFF, 0xFF); + palette.Window = RGB(0x29, 0x2C, 0x31); + palette.WindowText = RGB(0xFF, 0xFF, 0xFF); + + palette.FailureText = FailureTextColorOn(palette.Window); + palette.SkipText = SkipTextColorOn(palette.Window); + palette.TreeLine = BlendColors(palette.Window, palette.WindowText, 2, 5); + return palette; +} +//--------------------------------------------------------------------------- +std::vector PaletteRoleNames() +{ + std::vector names; + + for (TWin32GUIPaletteRole const& role : PaletteRoles()) + names.push_back(role.Name); + + return names; +} +//--------------------------------------------------------------------------- +/* + PaletteRoles + + Each role is named by the macro as its member, so a name can't differ from its member's. The groups follow + TWin32GUIPalette's comments. +*/ +std::vector const& PaletteRoles() +{ +#define ASWUNITTESTS_WIN32GUI_ROLE(member, group) \ + TWin32GUIPaletteRole{ &TWin32GUIPalette::member, group, #member } + + static std::vector const roles = { + ASWUNITTESTS_WIN32GUI_ROLE(ButtonBorder, + "Push buttons: the border, the default (or focused) button's line inside it, the face's gradient\n" + "(bottom and top), normal, hot (under the mouse), pressed and disabled, the outline around them, and\n" + "the text, normal and hot or pressed."), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonDefaultFrame, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonDisabledBorder, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonDisabledFaceBottom, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonDisabledFaceTop, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonDisabledOutline, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonFaceBottom, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonFaceTop, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonHotBorder, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonHotFaceBottom, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonHotFaceTop, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonHotText, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonOutline, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonPressedFaceBottom, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonPressedFaceTop, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ButtonText, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(CheckBoxBorder, + "Check boxes (the test tree's too) and radio buttons: the box's or circle's border, its inside, its\n" + "border under the mouse, and the mark (a cross, the partial state's square, a radio button's dot),\n" + "normal and disabled."), + ASWUNITTESTS_WIN32GUI_ROLE(CheckBoxFill, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(CheckBoxHotBorder, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(CheckMark, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(CheckMarkDisabled, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ControlText, + "The text of labels, radio buttons and check boxes, menus, the inactive tabs, the list's header and\n" + "the status bar."), + ASWUNITTESTS_WIN32GUI_ROLE(DisabledText, "Disabled text, but the menus' (see MenuBarDisabledText)."), + ASWUNITTESTS_WIN32GUI_ROLE(EditBorderInner, + "The two-pixel border of the edit boxes, the tree, the list and the log: inner and outer."), + ASWUNITTESTS_WIN32GUI_ROLE(EditBorderOuter, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(FailureText, "A failed test's text in the log and the Failures & Skips list."), + ASWUNITTESTS_WIN32GUI_ROLE(GripDot, "The status bar's size grip."), + ASWUNITTESTS_WIN32GUI_ROLE(HeaderBottom, + "The list's header: its gradient's bottom, the lines between columns, a column under the mouse, and\n" + "the gradient's top."), + ASWUNITTESTS_WIN32GUI_ROLE(HeaderDivider, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(HeaderHot, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(HeaderTop, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(Highlight, "The menu item under the mouse or the keyboard, and its frame."), + ASWUNITTESTS_WIN32GUI_ROLE(HighlightFrame, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(HotText, "Text over Highlight."), + ASWUNITTESTS_WIN32GUI_ROLE(MenuBarDisabledText, "A disabled item's text, in the menu bar and in a menu."), + ASWUNITTESTS_WIN32GUI_ROLE(MenuDisabledText, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(MenuBackground, + "A menu's background, its two-pixel frame (inner and outer) and its separators."), + ASWUNITTESTS_WIN32GUI_ROLE(MenuBorderInner, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(MenuBorderOuter, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(MenuSeparator, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(Panel, + "The background of the window, the menu bar, the toolbar, the tab strip, the status bar, the splitters\n" + "and the dialogs."), + ASWUNITTESTS_WIN32GUI_ROLE(ProgressBorder, + "The progress bar: its border, its bar's gradient (bottom and top), normal and after a failure, and\n" + "its track."), + ASWUNITTESTS_WIN32GUI_ROLE(ProgressChunkBottom, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ProgressChunkTop, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ProgressFailedBottom, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ProgressFailedTop, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(ProgressTrack, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(Selection, + "The selected node or row in the tree and the list, its frame, and its text."), + ASWUNITTESTS_WIN32GUI_ROLE(SelectionFrame, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(SelectionText, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(SkipText, "A skipped test's text in the log and the Failures & Skips list."), + ASWUNITTESTS_WIN32GUI_ROLE(SpinArrow, + "The results tabs' scroll arrows, when the tabs don't fit: the arrow, normal and disabled, each\n" + "button's face gradient (bottom and top) and outline, disabled, normal and hot, and the edge a pressed\n" + "one shows at its top and left."), + ASWUNITTESTS_WIN32GUI_ROLE(SpinDisabledArrow, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(SpinDisabledFaceBottom, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(SpinDisabledFaceTop, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(SpinDisabledOutline, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(SpinFaceBottom, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(SpinFaceTop, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(SpinHotFaceBottom, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(SpinHotFaceTop, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(SpinOutline, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(SpinPressedEdge, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(StatusLine, "The line along the status bar's top."), + ASWUNITTESTS_WIN32GUI_ROLE(TabActiveBottom, + "The results tabs: the selected one's gradient (bottom and top), the borders, a tab under the mouse,\n" + "the other tabs and their borders, and the line around the strip and the pages."), + ASWUNITTESTS_WIN32GUI_ROLE(TabActiveTop, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(TabBorder, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(TabHot, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(TabInactive, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(TabInactiveBorder, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(TabOuter, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(TooltipBackground, "Tooltips."), + ASWUNITTESTS_WIN32GUI_ROLE(TooltipText, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(TreeLine, "The test tree's dotted lines between nodes."), + ASWUNITTESTS_WIN32GUI_ROLE(TreeStatusFailed, + "The test tree's status dots: failed, not run, passed, running and skipped. The light look keeps\n" + "Carbon's."), + ASWUNITTESTS_WIN32GUI_ROLE(TreeStatusNotRun, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(TreeStatusPassed, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(TreeStatusRunning, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(TreeStatusSkipped, nullptr), + ASWUNITTESTS_WIN32GUI_ROLE(Window, + "The background of the edit boxes, the detail pane, the tree, the list and the log, and their text."), + ASWUNITTESTS_WIN32GUI_ROLE(WindowText, nullptr), + }; + +#undef ASWUNITTESTS_WIN32GUI_ROLE + + return roles; +} +//--------------------------------------------------------------------------- +TWin32GUIPalette PaletteWithThemeColors(TWin32GUIPalette palette, std::vector const& colors) +{ + std::vector const& roles = PaletteRoles(); + + for (TGUIThemeColor const& color : colors) + { + std::vector::const_iterator const role = std::find_if(roles.begin(), roles.end(), + [&color](TWin32GUIPaletteRole const& candidate) + { + return color.Role == candidate.Name; + }); + + if (role != roles.end()) + palette.*(role->Color) = FromRGB(color.RGB); + } + + return palette; +} +//--------------------------------------------------------------------------- +COLORREF SkipTextColorOn(COLORREF background) +{ + return IsDarkColor(background) ? RGB(255, 190, 70) : RGB(190, 120, 0); +} +//--------------------------------------------------------------------------- +/* + TabletDarkPalette + + The colors of RAD Studio's Tablet Dark style, read from its .vsf file as CarbonPalette()'s were + (plans/1.5/experiments/style_palette.dpr), with the values its elements don't give taken from the VCL GUI + runner under it; the selection and the colors that say how a test did are Carbon's, as the VCL GUI + runner's are under every style. +*/ +TWin32GUIPalette TabletDarkPalette() +{ + TWin32GUIPalette palette = CarbonPalette(); + + palette.ButtonBorder = RGB(0x3D, 0x4A, 0x79); + palette.ButtonDefaultFrame = RGB(0x1A, 0xBC, 0x9C); + palette.ButtonDisabledBorder = RGB(0x30, 0x33, 0x4B); + palette.ButtonDisabledFaceBottom = RGB(0x30, 0x33, 0x4B); + palette.ButtonDisabledFaceTop = RGB(0x30, 0x33, 0x4B); + palette.ButtonDisabledOutline = RGB(0x30, 0x33, 0x4B); + palette.ButtonFaceBottom = RGB(0x3D, 0x4A, 0x79); + palette.ButtonFaceTop = RGB(0x3D, 0x4A, 0x79); + palette.ButtonHotBorder = RGB(0x1A, 0xBC, 0x9C); + palette.ButtonHotFaceBottom = RGB(0x1A, 0xBC, 0x9C); + palette.ButtonHotFaceTop = RGB(0x1A, 0xBC, 0x9C); + palette.ButtonHotText = RGB(0x00, 0x00, 0x00); + palette.ButtonOutline = RGB(0x3D, 0x4A, 0x79); + palette.ButtonPressedFaceBottom = RGB(0x1A, 0xBC, 0x9C); + palette.ButtonPressedFaceTop = RGB(0x1A, 0xBC, 0x9C); + palette.ButtonText = RGB(0xFF, 0xFF, 0xFF); + palette.CheckBoxBorder = RGB(0x6B, 0x73, 0x94); + palette.CheckBoxFill = RGB(0x2F, 0x36, 0x55); + palette.CheckBoxHotBorder = RGB(0x1A, 0xBC, 0x9C); + palette.CheckMark = RGB(0xE6, 0xE6, 0xE6); + palette.CheckMarkDisabled = RGB(0x6D, 0x74, 0x8B); + palette.ControlText = RGB(0xFF, 0xFF, 0xFF); + palette.DisabledText = RGB(0x80, 0x80, 0x80); + palette.EditBorderInner = RGB(0x0F, 0x0F, 0x11); + palette.EditBorderOuter = RGB(0x2B, 0x29, 0x37); + palette.GripDot = RGB(0x6B, 0x6B, 0x6B); + palette.HeaderBottom = RGB(0x2F, 0x2D, 0x3C); + palette.HeaderDivider = RGB(0x2F, 0x2D, 0x3C); + palette.HeaderHot = RGB(0x36, 0x34, 0x43); + palette.HeaderTop = RGB(0x2F, 0x2D, 0x3C); + palette.Highlight = RGB(0x1A, 0xBC, 0x9C); + palette.HighlightFrame = RGB(0x1A, 0xBC, 0x9C); + palette.HotText = RGB(0x00, 0x00, 0x00); + palette.MenuBackground = RGB(0x39, 0x42, 0x64); + palette.MenuBarDisabledText = RGB(0x80, 0x80, 0x80); + palette.MenuBorderInner = RGB(0x39, 0x42, 0x64); + palette.MenuBorderOuter = RGB(0x00, 0x00, 0x00); + palette.MenuDisabledText = RGB(0x80, 0x80, 0x80); + palette.MenuSeparator = RGB(0x1F, 0x1D, 0x28); + palette.Panel = RGB(0x2B, 0x29, 0x37); + palette.ProgressBorder = RGB(0x1A, 0x18, 0x22); + palette.ProgressChunkBottom = RGB(0x1A, 0xBC, 0x9C); + palette.ProgressChunkTop = RGB(0x1A, 0xBC, 0x9C); + palette.ProgressTrack = RGB(0x1A, 0x18, 0x22); + palette.SelectionText = RGB(0xFF, 0xFF, 0xFF); + palette.SpinArrow = RGB(0x30, 0x2F, 0x3D); + palette.SpinDisabledArrow = RGB(0x2E, 0x2C, 0x3A); + palette.SpinDisabledFaceBottom = RGB(0x2B, 0x29, 0x37); + palette.SpinDisabledFaceTop = RGB(0x2B, 0x29, 0x37); + palette.SpinDisabledOutline = RGB(0x2B, 0x29, 0x37); + palette.SpinFaceBottom = RGB(0x2B, 0x29, 0x37); + palette.SpinFaceTop = RGB(0x2B, 0x29, 0x37); + palette.SpinHotFaceBottom = RGB(0x1D, 0x9F, 0x88); + palette.SpinHotFaceTop = RGB(0x27, 0x4E, 0x50); + palette.SpinOutline = RGB(0x2B, 0x29, 0x37); + palette.SpinPressedEdge = RGB(0x27, 0x4E, 0x50); + palette.StatusLine = RGB(0x38, 0x37, 0x46); + palette.TabActiveBottom = RGB(0x39, 0x42, 0x64); + palette.TabActiveTop = RGB(0x39, 0x42, 0x64); + palette.TabBorder = RGB(0x39, 0x42, 0x64); + palette.TabHot = RGB(0x2B, 0x29, 0x37); + palette.TabInactive = RGB(0x2B, 0x29, 0x37); + palette.TabInactiveBorder = RGB(0x49, 0x4A, 0x5A); + palette.TabOuter = RGB(0x3D, 0x4A, 0x79); + palette.TooltipBackground = RGB(0x39, 0x42, 0x64); + palette.TooltipText = RGB(0xFF, 0xFF, 0xFF); + palette.Window = RGB(0x39, 0x42, 0x64); + palette.WindowText = RGB(0xFF, 0xFF, 0xFF); + + // Lighter than FailureTextColorOn()'s red, which is too dark to read on the indigo window. + palette.FailureText = RGB(255, 150, 150); + palette.SkipText = SkipTextColorOn(palette.Window); + palette.TreeLine = BlendColors(palette.Window, palette.WindowText, 2, 5); + return palette; +} +//--------------------------------------------------------------------------- +/* + TintedCarbonPalette + + A gray (a color whose HSL saturation is under 0.1) keeps its lightness and gets the chroma 'tint' times + (1 - its lightness); a colored one keeps its saturation and lightness and gets 'accentHue'. +*/ +TWin32GUIPalette TintedCarbonPalette(double hue, double tint, double accentHue) +{ + // The colors that say how a test did. + COLORREF TWin32GUIPalette::* const kept[] = { + &TWin32GUIPalette::FailureText, &TWin32GUIPalette::ProgressFailedBottom, &TWin32GUIPalette::ProgressFailedTop, + &TWin32GUIPalette::SkipText, &TWin32GUIPalette::TreeStatusFailed, &TWin32GUIPalette::TreeStatusNotRun, + &TWin32GUIPalette::TreeStatusPassed, &TWin32GUIPalette::TreeStatusRunning, &TWin32GUIPalette::TreeStatusSkipped + }; + + TWin32GUIPalette palette = CarbonPalette(); + + for (TWin32GUIPaletteRole const& role : PaletteRoles()) + { + if (std::find(std::begin(kept), std::end(kept), role.Color) != std::end(kept)) + continue; + + COLORREF& color = palette.*role.Color; + double colorHue = 0.0; + double saturation = 0.0; + double lightness = 0.0; + ToHSL(color, colorHue, saturation, lightness); + + if (saturation < 0.1) + { + // The chroma as HSL's saturation, which is relative to the most chroma the lightness allows. + double const room = 1.0 - std::fabs(2.0 * lightness - 1.0); + double const chroma = tint * (1.0 - lightness); + color = FromHSL(hue, room > 0.0 ? (std::min)(1.0, chroma / room) : 0.0, lightness); + } + else + { + color = FromHSL(accentHue, saturation, lightness); + } + } + + return palette; +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Palette.h b/win32/gui/src/ASWUnitTests_Win32GUI_Palette.h new file mode 100644 index 0000000..54fa5f8 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Palette.h @@ -0,0 +1,254 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_Palette.h +Author: Anthony S. West - ASW Software + +The colors of the Win32 GUI runner's dark theme, one per role, as RAD +Studio's Carbon style (the VCL GUI runner's dark theme) has them. Contains +no window code, so it's unit tested from the Windows console runners (see +win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp). + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_Win32GUI_PaletteH +#define ASWUnitTests_Win32GUI_PaletteH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_Theme.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUIPalette +// +// The colors the dark theme paints with, one per role, each named for what +// it colors. CarbonPalette() has Carbon's, and a theme file sets any of +// them by name (see PaletteRoles()), so a member's name is part of that +// file's format: renaming one breaks the theme files that set it. The light +// theme is Windows' standard look, in the system colors, so it has no +// palette. +///////////////////////////////////////////////////////////////////////////// +struct TWin32GUIPalette +{ + // Push buttons: the outline around them, the border, and the face's gradient from top to bottom, normal, + // hot (under the mouse), pressed and disabled; the default button's (and the focused one's) line inside + // its border; the text, and a hot or pressed button's. + COLORREF ButtonBorder; + COLORREF ButtonDefaultFrame; + COLORREF ButtonDisabledBorder; + COLORREF ButtonDisabledFaceBottom; + COLORREF ButtonDisabledFaceTop; + COLORREF ButtonDisabledOutline; + COLORREF ButtonFaceBottom; + COLORREF ButtonFaceTop; + COLORREF ButtonHotBorder; + COLORREF ButtonHotFaceBottom; + COLORREF ButtonHotFaceTop; + COLORREF ButtonHotText; + COLORREF ButtonOutline; + COLORREF ButtonPressedFaceBottom; + COLORREF ButtonPressedFaceTop; + COLORREF ButtonText; + // Check boxes (the test tree's too) and radio buttons: the box's or circle's border, its inside, and the + // mark (a cross, the partial state's square, the radio button's dot); the border under the mouse, and the + // mark of a disabled one. + COLORREF CheckBoxBorder; + COLORREF CheckBoxFill; + COLORREF CheckBoxHotBorder; + COLORREF CheckMark; + COLORREF CheckMarkDisabled; + // The text of labels, radio buttons and check boxes, menus, the inactive tab, the list's header and the + // status bar. + COLORREF ControlText; + // Disabled text, but the menus' (see MenuBarDisabledText and MenuDisabledText). + COLORREF DisabledText; + // The two-pixel border of the edit boxes, the tree, the list and the log: outer and inner. + COLORREF EditBorderInner; + COLORREF EditBorderOuter; + // A failed and a skipped test's text in the log and the Failures & Skips list. + COLORREF FailureText; + // The status bar's size grip. + COLORREF GripDot; + // The list's header: its gradient from top to bottom, under the mouse, and the lines between columns. + COLORREF HeaderBottom; + COLORREF HeaderDivider; + COLORREF HeaderHot; + COLORREF HeaderTop; + // The menu item under the mouse or the keyboard, and its frame. + COLORREF Highlight; + COLORREF HighlightFrame; + // Text over Highlight. + COLORREF HotText; + // A disabled item's text, in the menu bar and in a menu. + COLORREF MenuBarDisabledText; + COLORREF MenuDisabledText; + // A menu's background, its two-pixel frame (outer and inner) and its separators. + COLORREF MenuBackground; + COLORREF MenuBorderInner; + COLORREF MenuBorderOuter; + COLORREF MenuSeparator; + // The window's background, and that of the menu bar, the toolbar, the tab strip, the status bar, the + // splitters and the dialogs. + COLORREF Panel; + // The progress bar: its border, its track, and its bar's gradient from top to bottom, normal and failed. + COLORREF ProgressBorder; + COLORREF ProgressChunkBottom; + COLORREF ProgressChunkTop; + COLORREF ProgressFailedBottom; + COLORREF ProgressFailedTop; + COLORREF ProgressTrack; + // The selected node or row in the tree and the list, its frame, and its text. + COLORREF Selection; + COLORREF SelectionFrame; + COLORREF SelectionText; + COLORREF SkipText; + // Spin buttons, such as the results tabs' scroll arrows when the tabs don't fit: the outline around each, its + // face's gradient from top to bottom, normal, hot and disabled, the edge a pressed one shows at its top and + // left, and its arrow, normal and disabled. + COLORREF SpinArrow; + COLORREF SpinDisabledArrow; + COLORREF SpinDisabledFaceBottom; + COLORREF SpinDisabledFaceTop; + COLORREF SpinDisabledOutline; + COLORREF SpinFaceBottom; + COLORREF SpinFaceTop; + COLORREF SpinHotFaceBottom; + COLORREF SpinHotFaceTop; + COLORREF SpinOutline; + COLORREF SpinPressedEdge; + // The line along the status bar's top. + COLORREF StatusLine; + // The results tabs: the selected one's gradient from top to bottom, the others' color, one under the + // mouse, their borders, and the line around the strip and the pages. + COLORREF TabActiveBottom; + COLORREF TabActiveTop; + COLORREF TabBorder; + COLORREF TabHot; + COLORREF TabInactive; + COLORREF TabInactiveBorder; + COLORREF TabOuter; + // Tooltips. + COLORREF TooltipBackground; + COLORREF TooltipText; + // The test tree's dotted lines between nodes. + COLORREF TreeLine; + // The test tree's status dots: a test or group that failed, hasn't run, passed, is running, or was skipped. + // Light draws them in Carbon's colors. + COLORREF TreeStatusFailed; + COLORREF TreeStatusNotRun; + COLORREF TreeStatusPassed; + COLORREF TreeStatusRunning; + COLORREF TreeStatusSkipped; + // The background of the edit boxes, the detail pane, the tree, the list and the log, and their text. + COLORREF Window; + COLORREF WindowText; +}; + + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUIPaletteRole +// +// One of TWin32GUIPalette's colors, as a theme file sets it (see +// PaletteRoles()). +///////////////////////////////////////////////////////////////////////////// +struct TWin32GUIPaletteRole +{ + // The member it is. + COLORREF TWin32GUIPalette::* Color; + // What this role and the ones after it, up to the next one with a Group, color, one line or more, for + // CarbonThemeFileText()'s comments; nullptr for those after it. + char const* Group; + // The member's name, which the theme file uses. + char const* Name; +}; + + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUIBuiltInTheme +// +// A dark theme the Win32 GUI runner has built in, which View > Theme > +// Dark Theme lists before the theme files (see BuiltInDarkThemes()). +///////////////////////////////////////////////////////////////////////////// +struct TWin32GUIBuiltInTheme +{ + // Its name, as the menu lists it and the layout file saves it. + std::string Name; + TWin32GUIPalette Palette; +}; + +//--------------------------------------------------------------------------- + +// 'from' mixed with 'to' in the proportion 'numerator' / 'denominator', each channel rounded to the nearest: +// 'from' at 0, 'to' at 'denominator', e.g. one row of a gradient. 'from' if 'denominator' isn't positive; the +// proportion is kept within 0 and 1. +COLORREF BlendColors(COLORREF from, COLORREF to, int numerator, int denominator); +// The dark themes built in: Carbon (GUIDefaultDarkThemeName(), CarbonPalette()) first, then by name the +// others: RAD Studio's dark VCL styles the VCL GUI runner has too (GUIVCLStyleDarkThemeNames(), e.g. +// OnyxBluePalette()), and Carbon tinted (see TintedCarbonPalette()): Ember (warm brown), Forest (green), Midnight +// (blue) and Plum (purple). +std::vector const& BuiltInDarkThemes(); +// The colors of RAD Studio's Carbon style, the VCL GUI runner's dark theme, read from its .vsf file and from +// the VCL GUI runner as it draws under it. +TWin32GUIPalette CarbonPalette(); +// A theme file (see TGUIThemeFile) setting each of PaletteRoles() to Carbon's color, in their order, with +// comments saying what the file is and what each group of roles colors, and CRLF line breaks: the template +// View > Theme > Dark Theme > Save Theme Template writes. +std::string CarbonThemeFileText(); +// The colors of RAD Studio's Charcoal Dark Slate VCL style, as CarbonPalette() has Carbon's. +TWin32GUIPalette CharcoalDarkSlatePalette(); +// The colors of RAD Studio's Cobalt XEMedia VCL style, as CarbonPalette() has Carbon's. +TWin32GUIPalette CobaltXEMediaPalette(); +// The color of a failed test's text on 'background': RGB(200, 0, 0), or RGB(255, 100, 100) on a dark +// background (see IsDarkColor()), as the VCL GUI runner's. +COLORREF FailureTextColorOn(COLORREF background); +// The built-in dark theme named 'name', ignoring (ASCII) case, e.g. as a layout file saved it; nullptr if +// there's none (see BuiltInDarkThemes()). +TWin32GUIBuiltInTheme const* FindBuiltInDarkTheme(std::string const& name); +// Whether 'color' is dark, by its luma (Rec. 601 weights) being below 128, as the VCL GUI runner decides +// whether a background needs lighter text colors. +bool IsDarkColor(COLORREF color); +// The colors of RAD Studio's Onyx Blue VCL style, as CarbonPalette() has Carbon's. +TWin32GUIPalette OnyxBluePalette(); +// The names of PaletteRoles(), in their order, for ReadGUIThemeFile(). +std::vector PaletteRoleNames(); +// Every member of TWin32GUIPalette, once, in the struct's order, with its name and group. +std::vector const& PaletteRoles(); +// 'palette' with each of 'colors' (a theme file's, see ReadGUIThemeFile()) in place of its role's color; one +// whose role isn't one of PaletteRoles() is left out. +TWin32GUIPalette PaletteWithThemeColors(TWin32GUIPalette palette, std::vector const& colors); +// The color of a skipped test's text on 'background': RGB(190, 120, 0), or RGB(255, 190, 70) on a dark +// background (see IsDarkColor()), as the VCL GUI runner's. +COLORREF SkipTextColorOn(COLORREF background); +// The colors of RAD Studio's Tablet Dark VCL style, as CarbonPalette() has Carbon's. +TWin32GUIPalette TabletDarkPalette(); +// Carbon's palette tinted: each of its grays turned toward 'hue' (in degrees, 0 red, 120 green, 240 blue), by +// up to 'tint' of its chroma (0 to 1), most in the dark backgrounds and less as a gray gets lighter, so text +// stays nearly white; its accent (the highlight, the default button's frame and the progress bar) turned to +// 'accentHue'. The colors that say how a test did (the tree's status dots, the failed and skipped text and the +// failed progress bar) stay Carbon's. +TWin32GUIPalette TintedCarbonPalette(double hue, double tint, double accentHue); + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_Win32GUI_PaletteH diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_ShuffleSeedDialog.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_ShuffleSeedDialog.cpp new file mode 100644 index 0000000..1295d79 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_ShuffleSeedDialog.cpp @@ -0,0 +1,387 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_ShuffleSeedDialog.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_Win32GUI_ShuffleSeedDialog.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Utils.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_DialogTemplate.h" +#include "ASWUnitTests_Win32GUI_DPI.h" +#include "ASWUnitTests_Win32GUI_MessageDialog.h" +#include "ASWUnitTests_Win32GUI_System.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +// Posted to the dialog when it moves to a monitor with another DPI, so it's laid out again after Windows has +// rescaled it and its controls' fonts. +UINT const ApplyDPIMessage = WM_APP + 1; +// The dialog's title, and its error message's, as the VCL GUI runner's. +wchar_t const DialogCaption[] = L"Shuffle Seed"; +// The controls' IDs, in the dialog's order; the buttons' are IDOK and IDCANCEL. The radio buttons' must be +// consecutive, for CheckRadioButton(). +WORD const FixedSeedID = 102; +WORD const IntroLabelID = 100; +WORD const LastSeedLabelID = 104; +WORD const NewSeedID = 101; +WORD const SeedBoxID = 103; + +///////////////////////////////////////////////////////////////////////////// +// TShuffleSeedDialogState +// +// The choice the dialog edits, and the theme it's shown in. +///////////////////////////////////////////////////////////////////////////// +struct TShuffleSeedDialogState +{ + TGUIShuffle* Shuffle = nullptr; + TWin32GUITheme* Theme = nullptr; +}; + +//--------------------------------------------------------------------------- + +void ApplyChoice(HWND dialog); +TWin32GUIDialogTemplate BuildDialogTemplate(TGUIShuffle const& shuffle); +void CenterOnOwner(HWND dialog); +INT_PTR CALLBACK DialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM lParam); +INT_PTR HandleDialogMessage(HWND dialog, UINT message, WPARAM wParam, LPARAM lParam); +void LayOutDialog(HWND dialog); +void SelectSeedChoice(HWND dialog, WORD radioButtonID); +int TextHeight(HWND control, int width); + +//--------------------------------------------------------------------------- +/* + ApplyChoice + + OK's click: applies the choice and closes the dialog, or, if the seed isn't valid, says why and leaves the + dialog open with the seed selected, as the VCL GUI runner's does. +*/ +void ApplyChoice(HWND dialog) +{ + TShuffleSeedDialogState const* const state = reinterpret_cast( + GetWindowLongPtrW(dialog, DWLP_USER)); + HWND const seedBox = GetDlgItem(dialog, SeedBoxID); + std::string error; + + if (!state->Shuffle->ApplySeedChoice(IsDlgButtonChecked(dialog, NewSeedID) == BST_CHECKED, WindowText(seedBox), + error)) + { + ShowMessageDialog(dialog, *state->Theme, WideToUTF8(DialogCaption), error, EWin32GUIMessageIcon::Warning); + // Focuses the seed box as tabbing to it does, which also selects its text. + SendMessageW(dialog, WM_NEXTDLGCTL, reinterpret_cast(seedBox), TRUE); + return; + } + + EndDialog(dialog, IDOK); +} +//--------------------------------------------------------------------------- +/* + BuildDialogTemplate + + The VCL GUI runner's dialog, with its captions, & letters and tab order, and the seed box already holding + the seed to show, so only the user's own typing selects "same seed" (see HandleDialogMessage()). + LayOutDialog() sets the positions and sizes. +*/ +TWin32GUIDialogTemplate BuildDialogTemplate(TGUIShuffle const& shuffle) +{ + std::optional const lastSeed = shuffle.LastSeed(); + std::string const lastSeedText = lastSeed.has_value() ? + "The latest shuffled run's seed was " + std::to_string(*lastSeed) + "." : + std::string("No run has been shuffled yet."); + + TWin32GUIDialogTemplate dialogTemplate(WS_POPUP | WS_CAPTION | WS_SYSMENU | DS_MODALFRAME, DialogCaption); + + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Static, IntroLabelID, WS_CHILD | WS_VISIBLE | SS_LEFT | + SS_NOPREFIX, 0, L"A shuffled run runs the groups, and each group's tests, in an order its seed decides, so " + L"running again with the same seed repeats that order. Clicking OK also turns on Run in Shuffled Order."); + // SelectSeedChoice() gives the checked one the tab stop. + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Button, NewSeedID, WS_CHILD | WS_VISIBLE | WS_GROUP | + BS_AUTORADIOBUTTON, 0, L"Use a &new random seed for each run"); + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Button, FixedSeedID, WS_CHILD | WS_VISIBLE | + BS_AUTORADIOBUTTON, 0, L"Use the same &seed for every run:"); + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Edit, SeedBoxID, WS_CHILD | WS_VISIBLE | WS_GROUP | + WS_TABSTOP | ES_NUMBER | ES_AUTOHSCROLL, WS_EX_CLIENTEDGE, UTF8ToWide(std::to_string(shuffle.SeedToShow()))); + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Static, LastSeedLabelID, WS_CHILD | WS_VISIBLE | + WS_GROUP | SS_LEFT | SS_NOPREFIX, 0, UTF8ToWide(lastSeedText)); + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Button, IDCANCEL, WS_CHILD | WS_VISIBLE | WS_GROUP | + WS_TABSTOP | BS_PUSHBUTTON, 0, L"Cancel"); + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Button, IDOK, WS_CHILD | WS_VISIBLE | WS_TABSTOP | + BS_DEFPUSHBUTTON, 0, L"OK"); + + return dialogTemplate; +} +//--------------------------------------------------------------------------- +/* + CenterOnOwner + + As the VCL's poMainFormCenter, but kept within the work area of the owner's monitor. +*/ +void CenterOnOwner(HWND dialog) +{ + HWND const owner = GetWindow(dialog, GW_OWNER); + MONITORINFO monitor = {}; + monitor.cbSize = sizeof(monitor); + GetMonitorInfoW(MonitorFromWindow(owner != nullptr ? owner : dialog, MONITOR_DEFAULTTONEAREST), &monitor); + + RECT const& work = monitor.rcWork; + RECT around = work; + + if (owner != nullptr) + GetWindowRect(owner, &around); + + RECT bounds; + GetWindowRect(dialog, &bounds); + int const width = bounds.right - bounds.left; + int const height = bounds.bottom - bounds.top; + + int const left = (std::max)(static_cast(work.left), (std::min)(static_cast(work.right) - width, + static_cast(around.left + (around.right - around.left - width) / 2))); + int const top = (std::max)(static_cast(work.top), (std::min)(static_cast(work.bottom) - height, + static_cast(around.top + (around.bottom - around.top - height) / 2))); + + SetWindowPos(dialog, nullptr, left, top, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); +} +//--------------------------------------------------------------------------- +/* + DialogProc + + No exception may leave a dialog procedure, since unwinding through Windows' own frames is undefined. One + that would ends the dialog with -1, which ShowShuffleSeedDialog() reports as a failure. +*/ +INT_PTR CALLBACK DialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM lParam) +{ + try + { + return HandleDialogMessage(dialog, message, wParam, lParam); + } + catch (...) + { + EndDialog(dialog, -1); + return TRUE; + } +} +//--------------------------------------------------------------------------- +/* + HandleDialogMessage + + The seed box's EN_CHANGE selects "same seed". The template gives the box its first text, which sends none. +*/ +INT_PTR HandleDialogMessage(HWND dialog, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { + case WM_INITDIALOG: + { + SetWindowLongPtrW(dialog, DWLP_USER, lParam); + TShuffleSeedDialogState const* const state = reinterpret_cast(lParam); + + HWND const seedBox = GetDlgItem(dialog, SeedBoxID); + SendMessageW(seedBox, EM_SETLIMITTEXT, 10, 0); // 4294967295, the largest seed. + state->Theme->AttachWindow(dialog); + state->Theme->Attach(seedBox, EWin32GUIThemedControl::Edit); + + bool const fixedSeed = state->Shuffle->FixedSeed().has_value(); + SelectSeedChoice(dialog, fixedSeed ? FixedSeedID : NewSeedID); + + LayOutDialog(dialog); + CenterOnOwner(dialog); + + if (fixedSeed) + { + SetFocus(seedBox); + SendMessageW(seedBox, EM_SETSEL, 0, -1); + } + else + { + SetFocus(GetDlgItem(dialog, NewSeedID)); + } + + return FALSE; // The focus is set. + } + + case WM_COMMAND: + switch (LOWORD(wParam)) + { + case FixedSeedID: + case NewSeedID: + if (HIWORD(wParam) == BN_CLICKED) + SelectSeedChoice(dialog, LOWORD(wParam)); + + return TRUE; + + case IDCANCEL: + EndDialog(dialog, IDCANCEL); + return TRUE; + + case IDOK: + ApplyChoice(dialog); + return TRUE; + + case SeedBoxID: + if (HIWORD(wParam) == EN_CHANGE) + SelectSeedChoice(dialog, FixedSeedID); + + return TRUE; + + default: + return FALSE; + } + + case WM_DPICHANGED: + PostMessageW(dialog, ApplyDPIMessage, 0, 0); + return FALSE; // Windows moves and rescales the dialog. + + default: + if (message == ApplyDPIMessage) + { + LayOutDialog(dialog); + return TRUE; + } + + return FALSE; + } +} +//--------------------------------------------------------------------------- +/* + LayOutDialog + + Places the controls as the VCL GUI runner's dialog does, scaled to the dialog's DPI: 420 pixels wide at + 96 DPI, as tall as the wrapped introduction needs. Then sizes the dialog to fit, keeping its position. +*/ +void LayOutDialog(HWND dialog) +{ + UINT const dpi = WindowDPI(dialog); + int const margin = ScaleForDPI(12, dpi); + int const width = ScaleForDPI(420, dpi); + int const textWidth = width - 2 * margin; + + HWND const introLabel = GetDlgItem(dialog, IntroLabelID); + int const introHeight = TextHeight(introLabel, textWidth); + MoveWindow(introLabel, margin, margin, textWidth, introHeight, TRUE); + int top = margin + introHeight + margin; + + MoveWindow(GetDlgItem(dialog, NewSeedID), margin, top, textWidth, ScaleForDPI(20, dpi), TRUE); + top += ScaleForDPI(28, dpi); + + int const fixedSeedWidth = ScaleForDPI(220, dpi); + MoveWindow(GetDlgItem(dialog, FixedSeedID), margin, top, fixedSeedWidth, ScaleForDPI(20, dpi), TRUE); + MoveWindow(GetDlgItem(dialog, SeedBoxID), margin + fixedSeedWidth + ScaleForDPI(8, dpi), + top - ScaleForDPI(2, dpi), ScaleForDPI(120, dpi), ScaleForDPI(23, dpi), TRUE); + top += ScaleForDPI(32, dpi); + + HWND const lastSeedLabel = GetDlgItem(dialog, LastSeedLabelID); + int const lastSeedHeight = TextHeight(lastSeedLabel, textWidth); + MoveWindow(lastSeedLabel, margin, top, textWidth, lastSeedHeight, TRUE); + top += lastSeedHeight + margin + ScaleForDPI(4, dpi); + + int const buttonWidth = ScaleForDPI(80, dpi); + int const buttonHeight = ScaleForDPI(26, dpi); + int const cancelLeft = width - margin - buttonWidth; + MoveWindow(GetDlgItem(dialog, IDCANCEL), cancelLeft, top, buttonWidth, buttonHeight, TRUE); + MoveWindow(GetDlgItem(dialog, IDOK), cancelLeft - ScaleForDPI(8, dpi) - buttonWidth, top, buttonWidth, + buttonHeight, TRUE); + + RECT frame = { 0, 0, width, top + buttonHeight + margin }; + AdjustWindowRectExForDpi(&frame, static_cast(GetWindowLongPtrW(dialog, GWL_STYLE)), FALSE, + static_cast(GetWindowLongPtrW(dialog, GWL_EXSTYLE)), dpi); + SetWindowPos(dialog, nullptr, 0, 0, frame.right - frame.left, frame.bottom - frame.top, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); +} +//--------------------------------------------------------------------------- +/* + SelectSeedChoice + + Checks one of the two radio buttons and gives it the group's tab stop, so Tab goes to the checked one, as + in the VCL, whose radio buttons are tab stops only while checked. +*/ +void SelectSeedChoice(HWND dialog, WORD radioButtonID) +{ + CheckRadioButton(dialog, NewSeedID, FixedSeedID, radioButtonID); + + for (WORD const id : { NewSeedID, FixedSeedID }) + { + HWND const radioButton = GetDlgItem(dialog, id); + LONG_PTR const style = GetWindowLongPtrW(radioButton, GWL_STYLE); + SetWindowLongPtrW(radioButton, GWL_STYLE, id == radioButtonID ? (style | WS_TABSTOP) : (style & ~WS_TABSTOP)); + } +} +//--------------------------------------------------------------------------- +/* + TextHeight + + The height of the control's text in its font, wrapped at 'width' pixels, as a word-wrapping label's. +*/ +int TextHeight(HWND control, int width) +{ + std::wstring const text = UTF8ToWide(WindowText(control)); + RECT bounds = { 0, 0, width, 0 }; + + HDC const context = GetDC(control); + + if (context == nullptr) + throw std::runtime_error("Couldn't measure the Shuffle Seed dialog's text"); + + HGDIOBJ const oldFont = SelectObject(context, reinterpret_cast(SendMessageW(control, WM_GETFONT, 0, + 0))); + DrawTextW(context, text.c_str(), -1, &bounds, DT_CALCRECT | DT_WORDBREAK | DT_NOPREFIX | DT_EXPANDTABS); + SelectObject(context, oldFont); + ReleaseDC(control, context); + + return bounds.bottom; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +//--------------------------------------------------------------------------- +bool ShowShuffleSeedDialog(HWND owner, TWin32GUITheme& theme, TGUIShuffle& shuffle) +{ + TWin32GUIDialogTemplate const dialogTemplate = BuildDialogTemplate(shuffle); + + TShuffleSeedDialogState state; + state.Shuffle = &shuffle; + state.Theme = &theme; + + INT_PTR const result = DialogBoxIndirectParamW(GetModuleHandleW(nullptr), dialogTemplate.Get(), owner, + &DialogProc, reinterpret_cast(&state)); + + if (result == -1) + throw std::runtime_error("Couldn't show the \"Shuffle Seed\" dialog"); + + return result == IDOK; +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_ShuffleSeedDialog.h b/win32/gui/src/ASWUnitTests_Win32GUI_ShuffleSeedDialog.h new file mode 100644 index 0000000..8690c26 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_ShuffleSeedDialog.h @@ -0,0 +1,47 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_ShuffleSeedDialog.h +Author: Anthony S. West - ASW Software + +The Win32 GUI runner's Shuffle Seed dialog, the counterpart of the VCL GUI +runner's ASWUnitTests_GUI_ShuffleSeedDialog. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_Win32GUI_ShuffleSeedDialogH +#define ASWUnitTests_Win32GUI_ShuffleSeedDialogH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_Shuffle.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Theme.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +// Shows a modal dialog, owned by 'owner', for choosing between a new random seed for each shuffled run and one +// seed for every run, starting from 'shuffle's current choice. Clicking OK applies the choice to 'shuffle', which +// also turns shuffling on, and returns true; Cancel changes nothing and returns false. Throws std::runtime_error +// if the dialog can't be shown. The dialog has 'theme''s look. +bool ShowShuffleSeedDialog(HWND owner, TWin32GUITheme& theme, TGUIShuffle& shuffle); + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_Win32GUI_ShuffleSeedDialogH diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Sources.cmake b/win32/gui/src/ASWUnitTests_Win32GUI_Sources.cmake new file mode 100644 index 0000000..8624ca6 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Sources.cmake @@ -0,0 +1,122 @@ +# ASWUnitTests_Win32GUI_Sources.cmake +# +# Exports what a project needs to build the Win32 GUI runner (Windows only), +# so a project that adds this repository as a git submodule can run its tests +# in a window without tracking the GUI's filenames by hand across updates: +# +# ASWUNITTESTS_WIN32_GUI_SOURCES The framework's core sources (all +# but main.cpp), the GUI runners' +# shared core, this folder's units +# including WinMain, the +# manifest (the .manifest file +# itself for MSVC, or the resource +# script that embeds it for other +# compilers), and +# ASWUnitTests_Icon.rc, which +# embeds the ASWUnitTests icon. A +# project with an icon of its own +# removes that script from the list +# (see the usage below); the window +# shows its icon if it's named +# MAINICON. +# ASWUNITTESTS_WIN32_GUI_INCLUDE_DIRS Their directories, for the include +# path (the resource compiler finds +# the icon through it). +# ASWUNITTESTS_WIN32_GUI_LIBRARIES The Windows libraries it links. +# ASWUNITTESTS_WIN32_GUI_LINK_OPTIONS Linker options it needs: with MSVC, +# leaving the execution level to the +# manifest; with MinGW-w64, marking +# the executable for Windows Vista +# or later, which gives its windows +# the same frame as other programs. +# ASWUNITTESTS_WIN32_GUI_SOURCE_DIR This folder. +# ASWUNITTESTS_RESOURCE_DIR The repository's resource folder, +# which has the icon. +# +# It includes src/ASWUnitTests_Sources.cmake and +# gui/src/ASWUnitTests_GUI_Sources.cmake, so their variables are also set. +# Include it after project(), which tells it the compiler. +# +# Usage, from the consuming project's own CMakeLists.txt (never edit this +# repository's own cmake/CMakeLists.txt to add your tests): +# +# include(third_party/asw-unit-tests/win32/gui/src/ASWUnitTests_Win32GUI_Sources.cmake) +# add_executable(MyTestsGUI WIN32 +# ${ASWUNITTESTS_WIN32_GUI_SOURCES} +# tests/Test_MyClass.cpp +# ) +# target_include_directories(MyTestsGUI PRIVATE ${ASWUNITTESTS_WIN32_GUI_INCLUDE_DIRS} tests) +# target_link_libraries(MyTestsGUI PRIVATE ${ASWUNITTESTS_WIN32_GUI_LIBRARIES}) +# target_link_options(MyTestsGUI PRIVATE ${ASWUNITTESTS_WIN32_GUI_LINK_OPTIONS}) +# +# To use an icon of your own instead, before add_executable(): +# +# list(REMOVE_ITEM ASWUNITTESTS_WIN32_GUI_SOURCES "${ASWUNITTESTS_RESOURCE_DIR}/ASWUnitTests_Icon.rc") + +include("${CMAKE_CURRENT_LIST_DIR}/../../../src/ASWUnitTests_Sources.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/../../../gui/src/ASWUnitTests_GUI_Sources.cmake") + +set(ASWUNITTESTS_WIN32_GUI_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}") +get_filename_component(ASWUNITTESTS_RESOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../resource" ABSOLUTE) + +set(ASWUNITTESTS_WIN32_GUI_SOURCES + ${ASWUNITTESTS_CORE_SOURCES} + ${ASWUNITTESTS_GUI_SOURCES} + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_DialogTemplate.cpp" + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_DPI.cpp" + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_Glyphs.cpp" + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_MainLayout.cpp" + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_MainWindow.cpp" + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_MessageDialog.cpp" + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_Palette.cpp" + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_ShuffleSeedDialog.cpp" + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_Splitter.cpp" + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_System.cpp" + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_TextDialog.cpp" + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_Theme.cpp" + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_WinMain.cpp" + "${ASWUNITTESTS_RESOURCE_DIR}/ASWUnitTests_Icon.rc" +) + +set(ASWUNITTESTS_WIN32_GUI_INCLUDE_DIRS + "${ASWUNITTESTS_SOURCE_DIR}" + "${ASWUNITTESTS_GUI_SOURCE_DIR}" + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}" + "${ASWUNITTESTS_RESOURCE_DIR}" +) + +set(ASWUNITTESTS_WIN32_GUI_LIBRARIES + comctl32 + comdlg32 + gdi32 + shell32 + user32 + uxtheme +) + +set(ASWUNITTESTS_WIN32_GUI_LINK_OPTIONS) + +if(MSVC) + # The linker merges the manifest into the one it generates. /MANIFESTUAC:NO leaves the execution level to + # the manifest's own trustInfo, rather than adding a second one. + list(APPEND ASWUNITTESTS_WIN32_GUI_SOURCES + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI.manifest") + list(APPEND ASWUNITTESTS_WIN32_GUI_LINK_OPTIONS /MANIFESTUAC:NO) +else() + # The manifest as a resource. GCC also links its distribution's default manifest, if it has one; GNU ld + # drops a language-neutral default one silently, but WinLibs' has a language, so ld reports ".rsrc merge + # failure: multiple non-default manifests". The link still succeeds and Windows uses this manifest, which + # comes first. + list(APPEND ASWUNITTESTS_WIN32_GUI_SOURCES + "${ASWUNITTESTS_WIN32_GUI_SOURCE_DIR}/ASWUnitTests_Win32GUI_Manifest.rc") +endif() + +if(MINGW) + # GNU ld marks the executable for Windows subsystem 5.02 (XP) unless told otherwise, and Windows draws the + # windows of an executable older than 6.0 (Vista) without the padded border, so their frames and title bars + # would be thinner than in the other builds and the VCL GUI runner. lld already uses 6.0. + list(APPEND ASWUNITTESTS_WIN32_GUI_LINK_OPTIONS + -Wl,--major-subsystem-version,6 + -Wl,--minor-subsystem-version,0 + ) +endif() diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Splitter.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_Splitter.cpp new file mode 100644 index 0000000..d012942 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Splitter.cpp @@ -0,0 +1,171 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_Splitter.cpp +Author: Anthony S. West - ASW Software + +The Win32 GUI runner's splitter bars. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_Win32GUI_Splitter.h" +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +wchar_t const SplitterClassName[] = L"ASWUnitTests_Win32GUI_Splitter"; + +} // namespace + +//--------------------------------------------------------------------------- + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUISplitter +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TWin32GUISplitter::TWin32GUISplitter() + : m_GrabOffset(0), + m_Orientation(EOrientation::Vertical), + m_Window(nullptr) +{ +} +//--------------------------------------------------------------------------- +void TWin32GUISplitter::Create(HWND parent, EOrientation orientation, TDragHandler onDrag) +{ + HINSTANCE const instance = GetModuleHandleW(nullptr); + WNDCLASSEXW windowClass = {}; + + if (GetClassInfoExW(instance, SplitterClassName, &windowClass) == 0) + { + windowClass.cbSize = sizeof(windowClass); + windowClass.lpfnWndProc = &WindowProc; + windowClass.hInstance = instance; + windowClass.hbrBackground = reinterpret_cast(COLOR_BTNFACE + 1); + windowClass.lpszClassName = SplitterClassName; + + if (RegisterClassExW(&windowClass) == 0) + throw std::runtime_error("Couldn't register the splitter window class"); + } + + m_Orientation = orientation; + m_OnDrag = std::move(onDrag); + + if (CreateWindowExW(0, SplitterClassName, L"", WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, parent, nullptr, instance, + this) == nullptr) + { + throw std::runtime_error("Couldn't create a splitter"); + } +} +//--------------------------------------------------------------------------- +HWND TWin32GUISplitter::Handle() const +{ + return m_Window; +} +//--------------------------------------------------------------------------- +LRESULT TWin32GUISplitter::HandleMessage(UINT message, WPARAM wParam, LPARAM lParam) +{ + HWND const window = m_Window; // Still needed after WM_NCDESTROY clears m_Window. + + switch (message) + { + case WM_CAPTURECHANGED: + m_GrabOffset = 0; + return 0; + + case WM_LBUTTONDOWN: + m_GrabOffset = m_Orientation == EOrientation::Vertical ? static_cast(LOWORD(lParam)) : + static_cast(HIWORD(lParam)); + SetCapture(m_Window); + return 0; + + case WM_LBUTTONUP: + if (GetCapture() == m_Window) + ReleaseCapture(); + + return 0; + + case WM_MOUSEMOVE: + if (GetCapture() == m_Window && m_OnDrag) + { + // The mouse in the parent's client coordinates, less where the bar was grabbed. + POINT point = { static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam)) }; + MapWindowPoints(m_Window, GetParent(m_Window), &point, 1); + m_OnDrag((m_Orientation == EOrientation::Vertical ? point.x : point.y) - m_GrabOffset); + } + + return 0; + + case WM_NCDESTROY: + m_Window = nullptr; + break; + + case WM_SETCURSOR: + // IDC_SIZEWE and IDC_SIZENS; the W versions of those macros need UNICODE. + SetCursor(LoadCursorW(nullptr, MAKEINTRESOURCEW(m_Orientation == EOrientation::Vertical ? 32644 : 32645))); + return TRUE; + + default: + break; + } + + return DefWindowProcW(window, message, wParam, lParam); +} +//--------------------------------------------------------------------------- +/* + TWin32GUISplitter::WindowProc + + Passes each message to the splitter that created the window, whose address CreateWindowExW() received. + No exception may leave a window procedure, since unwinding through Windows' own frames is undefined; the + drag handler must not throw, and anything else that does is ignored, leaving the message to Windows. +*/ +LRESULT CALLBACK TWin32GUISplitter::WindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam) +{ + if (message == WM_NCCREATE) + { + TWin32GUISplitter* const splitter = static_cast( + reinterpret_cast(lParam)->lpCreateParams); + splitter->m_Window = window; + SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast(splitter)); + } + + TWin32GUISplitter* const splitter = reinterpret_cast(GetWindowLongPtrW(window, + GWLP_USERDATA)); + + if (splitter != nullptr) + { + try + { + return splitter->HandleMessage(message, wParam, lParam); + } + catch (...) + { + } + } + + return DefWindowProcW(window, message, wParam, lParam); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Splitter.h b/win32/gui/src/ASWUnitTests_Win32GUI_Splitter.h new file mode 100644 index 0000000..b546a08 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Splitter.h @@ -0,0 +1,87 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_Splitter.h +Author: Anthony S. West - ASW Software + +The Win32 GUI runner's splitter bars, the counterpart of the VCL's +TSplitter: a bar between two panes, dragged to resize them. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_Win32GUI_SplitterH +#define ASWUnitTests_Win32GUI_SplitterH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUISplitter +// +// A bar its parent window positions between two panes. Dragging it reports +// where the bar would go; the parent decides where it does go, keeping each +// pane at least its minimum size, and lays itself out again, as the VCL's +// TSplitter with ResizeStyle rsUpdate does. +///////////////////////////////////////////////////////////////////////////// +class TWin32GUISplitter +{ +public: + enum class EOrientation + { + // A tall bar between a left and a right pane, dragged left and right. + Vertical, + // A wide bar between a top and a bottom pane, dragged up and down. + Horizontal, + }; + + // Called while the bar is dragged, with where its left (Vertical) or top (Horizontal) edge would be, in its + // parent's client coordinates. Must not throw: it runs inside the splitter's window procedure. + using TDragHandler = std::function; + +private: + TDragHandler m_OnDrag; + // Where in the bar it was grabbed, from its left or top edge, while it's being dragged. + int m_GrabOffset; + EOrientation m_Orientation; + HWND m_Window; + +private: + static LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam); + +private: + LRESULT HandleMessage(UINT message, WPARAM wParam, LPARAM lParam); + +public: + TWin32GUISplitter(); + + TWin32GUISplitter(TWin32GUISplitter const&) = delete; + TWin32GUISplitter& operator=(TWin32GUISplitter const&) = delete; + + // Creates the bar as a child of 'parent', which positions it. Throws std::runtime_error if it can't be + // created. The bar is destroyed with its parent, which must outlive neither this object nor its handler. + void Create(HWND parent, EOrientation orientation, TDragHandler onDrag); + HWND Handle() const; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_Win32GUI_SplitterH diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_System.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_System.cpp new file mode 100644 index 0000000..f29db6c --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_System.cpp @@ -0,0 +1,553 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_System.cpp +Author: Anthony S. West - ASW Software + +What the Win32 GUI runner needs from its process and system: text +conversion between the framework's UTF-8 and Windows' UTF-16, its +command-line arguments, where it keeps its settings, and file access +through wide paths. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_Win32GUI_System.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Utils.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_INIDocument.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +// The most bytes one ReadFile() or WriteFile() call is asked to transfer. +DWORD const MaxFileChunk = 1u << 30; + +//--------------------------------------------------------------------------- + +///////////////////////////////////////////////////////////////////////////// +// TFileHandle +// +// Closes a file handle when it goes out of scope. +///////////////////////////////////////////////////////////////////////////// +class TFileHandle +{ +private: + HANDLE const m_Handle; + +public: + explicit TFileHandle(HANDLE handle); + ~TFileHandle(); + + TFileHandle(TFileHandle const&) = delete; + TFileHandle& operator=(TFileHandle const&) = delete; + + HANDLE Get() const; + bool IsValid() const; +}; + +//--------------------------------------------------------------------------- + +std::runtime_error FileError(std::string const& action, std::string const& path, DWORD error); +std::string FolderOf(std::string const& path); +std::string WithExtension(std::string const& path, std::string const& extension); + +//--------------------------------------------------------------------------- +/* + FileError + + The error to throw when a file operation fails, e.g. "Couldn't read C:\x.ini: Access is denied." +*/ +std::runtime_error FileError(std::string const& action, std::string const& path, DWORD error) +{ + return std::runtime_error("Couldn't " + action + " " + path + ": " + SystemErrorText(error)); +} +//--------------------------------------------------------------------------- +/* + FolderOf + + The folder the path's file is in, without a trailing separator, e.g. "a\b" for "a\b\c.ini"; empty if the + path has no folder. +*/ +std::string FolderOf(std::string const& path) +{ + std::size_t const separator = path.find_last_of("\\/"); + + return separator == std::string::npos ? std::string() : path.substr(0, separator); +} +//--------------------------------------------------------------------------- +/* + WithExtension + + The path with its file name's extension replaced, or added if it has none, e.g. "a\b.exe" and ".ini" give + "a\b.ini". +*/ +std::string WithExtension(std::string const& path, std::string const& extension) +{ + std::size_t const separator = path.find_last_of("\\/:"); + std::size_t const dot = path.find_last_of('.'); + + if (dot == std::string::npos || (separator != std::string::npos && dot < separator)) + return path + extension; + + return path.substr(0, dot) + extension; +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TFileHandle +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TFileHandle::TFileHandle(HANDLE handle) + : m_Handle(handle) +{ +} +//--------------------------------------------------------------------------- +TFileHandle::~TFileHandle() +{ + if (IsValid()) + CloseHandle(m_Handle); +} +//--------------------------------------------------------------------------- +HANDLE TFileHandle::Get() const +{ + return m_Handle; +} +//--------------------------------------------------------------------------- +bool TFileHandle::IsValid() const +{ + return m_Handle != INVALID_HANDLE_VALUE; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +//--------------------------------------------------------------------------- +std::string AdjustLineBreaks(std::string const& text) +{ + std::string adjusted; + adjusted.reserve(text.size() + text.size() / 16); + + for (std::size_t i = 0; i < text.size(); ++i) + { + if (text[i] == '\r' || text[i] == '\n') + { + adjusted += "\r\n"; + + // A CRLF pair is one line break. + if (text[i] == '\r' && i + 1 < text.size() && text[i + 1] == '\n') + ++i; + } + else + { + adjusted += text[i]; + } + } + + return adjusted; +} +//--------------------------------------------------------------------------- +void CreateFolderPath(std::string const& folderPath) +{ + std::wstring path = UTF8ToWide(folderPath); + std::replace(path.begin(), path.end(), L'/', L'\\'); + + // Each folder from the top down. Creating a drive or a share ("C:", "\\server") fails, as does creating a + // folder that exists, so only whether the whole path ends up a folder decides success. + DWORD lastError = ERROR_SUCCESS; + + for (std::size_t end = path.find(L'\\'); ; end = path.find(L'\\', end + 1)) + { + std::wstring const folder = path.substr(0, end); + + if (!folder.empty() && CreateDirectoryW(folder.c_str(), nullptr) == 0) + lastError = GetLastError(); + + if (end == std::wstring::npos) + break; + } + + DWORD const attributes = GetFileAttributesW(path.c_str()); + + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) + throw FileError("create the folder", folderPath, lastError != ERROR_SUCCESS ? lastError : ERROR_PATH_NOT_FOUND); +} +//--------------------------------------------------------------------------- +std::string ExeFileName() +{ + std::wstring path(MAX_PATH, L'\0'); + + for (;;) + { + DWORD const length = GetModuleFileNameW(nullptr, &path[0], static_cast(path.size())); + + if (length == 0) + throw std::runtime_error("Couldn't get the executable's path: " + SystemErrorText(GetLastError())); + + // A truncated path fills the whole buffer. + if (length < path.size()) + { + path.resize(length); + break; + } + + path.resize(path.size() * 2); + } + + std::size_t const separator = path.find_last_of(L"\\/"); + + return WideToUTF8(separator == std::wstring::npos ? path : path.substr(separator + 1)); +} +//--------------------------------------------------------------------------- +std::string ExpandFileName(std::string const& path) +{ + std::wstring const relative = UTF8ToWide(path); + // The length GetFullPathNameW() needs, including the terminating null; 0 if the path isn't valid. + DWORD const needed = GetFullPathNameW(relative.c_str(), 0, nullptr, nullptr); + + if (needed == 0) + throw FileError("expand the path", path, GetLastError()); + + std::wstring full(needed, L'\0'); + DWORD const length = GetFullPathNameW(relative.c_str(), needed, &full[0], nullptr); + + if (length == 0 || length >= needed) + throw FileError("expand the path", path, length == 0 ? GetLastError() : ERROR_INSUFFICIENT_BUFFER); + + full.resize(length); + return WideToUTF8(full); +} +//--------------------------------------------------------------------------- +bool FileExists(std::string const& path) +{ + DWORD const attributes = GetFileAttributesW(UTF8ToWide(path).c_str()); + + return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0; +} +//--------------------------------------------------------------------------- +std::vector FileNamesInFolder(std::string const& folderPath) +{ + std::vector names; + WIN32_FIND_DATAW found = {}; + HANDLE const search = FindFirstFileW(UTF8ToWide(folderPath + "\\*").c_str(), &found); + + if (search == INVALID_HANDLE_VALUE) + return names; + + for (bool more = true; more; more = FindNextFileW(search, &found) != 0) + { + if ((found.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) + names.push_back(WideToUTF8(found.cFileName)); + } + + FindClose(search); + return names; +} +//--------------------------------------------------------------------------- +std::string GUILayoutFilePath() +{ + return GUISettingsFolderPath() + "\\" + WithExtension(ExeFileName(), ".ini"); +} +//--------------------------------------------------------------------------- +std::string GUISelectionFilePath() +{ + return WithExtension(GUILayoutFilePath(), ".selection"); +} +//--------------------------------------------------------------------------- +std::string GUISettingsFolderPath() +{ + // The same folder the VCL GUI runner's System::Ioutils::TPath::GetHomePath() gives. + wchar_t appData[MAX_PATH] = {}; + HRESULT const result = SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, SHGFP_TYPE_CURRENT, appData); + + if (FAILED(result)) + throw std::runtime_error("Couldn't find the AppData folder: " + SystemErrorText(static_cast(result))); + + return WideToUTF8(appData) + "\\ASWUnitTests"; +} +//--------------------------------------------------------------------------- +std::string GUIThemesFolderPath() +{ + return GUISettingsFolderPath() + "\\Themes"; +} +//--------------------------------------------------------------------------- +bool IsThemeSettingChange(WPARAM wParam, LPARAM lParam) +{ + if (wParam == SPI_SETHIGHCONTRAST) + return true; + + wchar_t const* const area = reinterpret_cast(lParam); + + return area != nullptr && std::wcscmp(area, L"ImmersiveColorSet") == 0; +} +//--------------------------------------------------------------------------- +std::string LoadGUIDarkThemeFromLayoutFile(std::string const& layoutPath) +{ + try + { + if (!FileExists(layoutPath)) + return GUIDefaultDarkThemeName(); + + return LoadGUIDarkTheme(TGUIINIDocument(ReadFileBytes(layoutPath))); + } + catch (...) + { + return GUIDefaultDarkThemeName(); + } +} +//--------------------------------------------------------------------------- +EGUITheme LoadGUIThemeFromLayoutFile(std::string const& layoutPath) +{ + try + { + if (!FileExists(layoutPath)) + return EGUITheme::Windows; + + return LoadGUITheme(TGUIINIDocument(ReadFileBytes(layoutPath))); + } + catch (...) + { + return EGUITheme::Windows; + } +} +//--------------------------------------------------------------------------- +HICON MainIcon() +{ + return LoadIconW(GetModuleHandleW(nullptr), L"MAINICON"); +} +//--------------------------------------------------------------------------- +/* + OpenFolderInExplorer + + ShellExecuteW() returns a value above 32 on success, and otherwise an error code that, as documented, isn't + always GetLastError()'s, so the error is described from GetLastError() only when it has one. +*/ +void OpenFolderInExplorer(HWND owner, std::string const& folderPath) +{ + SetLastError(ERROR_SUCCESS); + INT_PTR const result = reinterpret_cast(ShellExecuteW(owner, L"open", UTF8ToWide(folderPath).c_str(), + nullptr, nullptr, SW_SHOWNORMAL)); + + if (result <= 32) + { + DWORD const error = GetLastError(); + throw FileError("open the folder", folderPath, error != ERROR_SUCCESS ? error : ERROR_PATH_NOT_FOUND); + } +} +//--------------------------------------------------------------------------- +std::vector ProcessArguments() +{ + return CommandLineArguments(GetCommandLineW()); +} +//--------------------------------------------------------------------------- +std::string ReadFileBytes(std::string const& path) +{ + TFileHandle const file(CreateFileW(UTF8ToWide(path).c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + + if (!file.IsValid()) + throw FileError("open", path, GetLastError()); + + LARGE_INTEGER size; + + if (GetFileSizeEx(file.Get(), &size) == 0) + throw FileError("read", path, GetLastError()); + + if (static_cast(size.QuadPart) > (std::numeric_limits::max)() / 2) + throw std::runtime_error("Couldn't read " + path + ": the file is too large"); + + std::string bytes(static_cast(size.QuadPart), '\0'); + std::size_t total = 0; + + while (total < bytes.size()) + { + DWORD const chunk = static_cast((std::min)(bytes.size() - total, static_cast(MaxFileChunk))); + DWORD read = 0; + + if (ReadFile(file.Get(), &bytes[total], chunk, &read, nullptr) == 0) + throw FileError("read", path, GetLastError()); + + // The file got shorter since its size was read. + if (read == 0) + break; + + total += read; + } + + bytes.resize(total); + return bytes; +} +//--------------------------------------------------------------------------- +bool RemoveFile(std::string const& path) +{ + return DeleteFileW(UTF8ToWide(path).c_str()) != 0; +} +//--------------------------------------------------------------------------- +void SaveGUIDarkThemeToLayoutFile(std::string const& layoutPath, std::string const& name) +{ + try + { + std::string const folder = FolderOf(layoutPath); + + if (!folder.empty()) + CreateFolderPath(folder); + + // Starting from the file's current text keeps the layout and any other values in it. + TGUIINIDocument ini(FileExists(layoutPath) ? ReadFileBytes(layoutPath) : std::string()); + SaveGUIDarkTheme(ini, name); + WriteFileBytes(layoutPath, ini.Text()); + } + catch (...) + { + // Not saving it (e.g. a read-only profile) only means the next start uses the default dark theme. + } +} +//--------------------------------------------------------------------------- +void SaveGUIThemeToLayoutFile(std::string const& layoutPath, EGUITheme theme) +{ + try + { + std::string const folder = FolderOf(layoutPath); + + if (!folder.empty()) + CreateFolderPath(folder); + + // Starting from the file's current text keeps the layout and any other values in it. + TGUIINIDocument ini(FileExists(layoutPath) ? ReadFileBytes(layoutPath) : std::string()); + SaveGUITheme(ini, theme); + WriteFileBytes(layoutPath, ini.Text()); + } + catch (...) + { + // Not saving it (e.g. a read-only profile) only means the next start uses the default theme. + } +} +//--------------------------------------------------------------------------- +/* + SetClipboardText + + Puts the text on the clipboard as CF_UNICODETEXT, which Windows also offers to programs that ask for + CF_TEXT. Once SetClipboardData() succeeds, the clipboard owns the memory. +*/ +void SetClipboardText(HWND owner, std::string const& text) +{ + std::wstring const wide = UTF8ToWide(text); + HGLOBAL const memory = GlobalAlloc(GMEM_MOVEABLE, (wide.size() + 1) * sizeof(wchar_t)); + + if (memory == nullptr) + throw std::runtime_error("Couldn't copy to the clipboard: " + SystemErrorText(GetLastError())); + + wchar_t* const target = static_cast(GlobalLock(memory)); + + if (target == nullptr) + { + DWORD const error = GetLastError(); + GlobalFree(memory); + throw std::runtime_error("Couldn't copy to the clipboard: " + SystemErrorText(error)); + } + + std::copy(wide.c_str(), wide.c_str() + wide.size() + 1, target); + GlobalUnlock(memory); + + if (OpenClipboard(owner) == 0) + { + DWORD const error = GetLastError(); + GlobalFree(memory); + throw std::runtime_error("Couldn't open the clipboard: " + SystemErrorText(error)); + } + + EmptyClipboard(); + bool const placed = SetClipboardData(CF_UNICODETEXT, memory) != nullptr; + DWORD const error = GetLastError(); + CloseClipboard(); + + if (!placed) + { + GlobalFree(memory); + throw std::runtime_error("Couldn't copy to the clipboard: " + SystemErrorText(error)); + } +} +//--------------------------------------------------------------------------- +std::string WindowText(HWND window) +{ + std::wstring text(static_cast(GetWindowTextLengthW(window)) + 1, L'\0'); + text.resize(static_cast(GetWindowTextW(window, &text[0], static_cast(text.size())))); + + return WideToUTF8(text); +} +//--------------------------------------------------------------------------- +bool WindowsAppsUseDarkTheme() +{ + DWORD value = 1; + DWORD size = sizeof(value); + + LSTATUS const status = RegGetValueW(HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", L"AppsUseLightTheme", RRF_RT_REG_DWORD, + nullptr, &value, &size); + + return status == ERROR_SUCCESS && value == 0; +} +//--------------------------------------------------------------------------- +bool WindowsHighContrast() +{ + HIGHCONTRASTW highContrast = {}; + highContrast.cbSize = sizeof(highContrast); + + return SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(highContrast), &highContrast, 0) != 0 && + (highContrast.dwFlags & HCF_HIGHCONTRASTON) != 0; +} +//--------------------------------------------------------------------------- +void WriteFileBytes(std::string const& path, std::string const& bytes) +{ + TFileHandle const file(CreateFileW(UTF8ToWide(path).c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, nullptr)); + + if (!file.IsValid()) + throw FileError("create", path, GetLastError()); + + std::size_t total = 0; + + while (total < bytes.size()) + { + DWORD const chunk = static_cast((std::min)(bytes.size() - total, static_cast(MaxFileChunk))); + DWORD written = 0; + + if (WriteFile(file.Get(), bytes.data() + total, chunk, &written, nullptr) == 0) + throw FileError("write", path, GetLastError()); + + total += written; + } +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_System.h b/win32/gui/src/ASWUnitTests_Win32GUI_System.h new file mode 100644 index 0000000..17d5c46 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_System.h @@ -0,0 +1,123 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_System.h +Author: Anthony S. West - ASW Software + +What the Win32 GUI runner needs from its process and system: text +conversion between the framework's UTF-8 and Windows' UTF-16, its +command-line arguments, where it keeps its settings, file access through +wide paths, and the clipboard. The Win32 counterpart of the VCL GUI runner's +ASWUnitTests_GUI_VCLSystem, with the same function names, so the two GUIs +can be compared side by side. + +Text and paths are UTF-8 std::string throughout, converted to UTF-16 only +where they reach Windows, so a path outside the ANSI code page works. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_Win32GUI_SystemH +#define ASWUnitTests_Win32GUI_SystemH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_Theme.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +// The text with every line break made CRLF, which an EDIT control needs: a lone LF or CR becomes CRLF, much as +// the VCL's AdjustLineBreaks() does for the VCL GUI runner. +std::string AdjustLineBreaks(std::string const& text); +// Creates the folder, and any of its parent folders that don't exist yet; succeeds if it already exists. Throws +// std::runtime_error if it can't be created. 'folderPath' must be absolute. +void CreateFolderPath(std::string const& folderPath); +// This executable's file name, without its folder, e.g. "ASWUnitTests_Win32_GUI.exe". +std::string ExeFileName(); +// The path made absolute, relative to the current folder, with any "." and ".." folders resolved, much as the +// VCL's ExpandFileName() does for the VCL GUI runner. The file needn't exist. Throws std::runtime_error if the +// path isn't valid. +std::string ExpandFileName(std::string const& path); +// Whether a file (not a folder) exists at the path. +bool FileExists(std::string const& path); +// The names of the files (not folders) in the folder, without the folder, in the order Windows lists them; +// empty if the folder doesn't exist or can't be listed. +std::vector FileNamesInFolder(std::string const& folderPath); +// This executable's layout file, %APPDATA%\ASWUnitTests\.ini, so each test app built on the GUI +// keeps its own layout, shared by all of its build configurations. +std::string GUILayoutFilePath(); +// This executable's selection file, beside its layout file (see GUILayoutFilePath()): its name with a +// ".selection" extension. +std::string GUISelectionFilePath(); +// The folder that holds the GUI runners' layout and selection files, %APPDATA%\ASWUnitTests. It may not exist +// yet (see CreateFolderPath()). +std::string GUISettingsFolderPath(); +// The folder that holds the theme files both GUI runners share (see TGUIThemeFile), whatever executable runs +// them: %APPDATA%\ASWUnitTests\Themes. It may not exist yet (see CreateFolderPath()). +std::string GUIThemesFolderPath(); +// Whether a WM_SETTINGCHANGE message's parameters are about a setting that Use Windows Setting follows: the +// app mode (Windows names its change "ImmersiveColorSet") or high contrast. +bool IsThemeSettingChange(WPARAM wParam, LPARAM lParam); +// The dark theme saved in the layout file at 'layoutPath', e.g. GUILayoutFilePath()'s (see LoadGUIDarkTheme()); +// GUIDefaultDarkThemeName() when there's no file or it can't be read. +std::string LoadGUIDarkThemeFromLayoutFile(std::string const& layoutPath); +// The theme saved in the layout file at 'layoutPath', e.g. GUILayoutFilePath()'s (see LoadGUITheme()); Windows +// when there's no file or it can't be read. +EGUITheme LoadGUIThemeFromLayoutFile(std::string const& layoutPath); +// This executable's icon, its MAINICON resource (resource/ASWUnitTests_Icon.rc, or Icon_MainIcon in a RAD Studio +// project), at the standard icon size; nullptr if it has none. Shared, so it's never destroyed. +HICON MainIcon(); +// Opens the folder in a File Explorer window, for 'owner' (a window of this process, or nullptr). Throws +// std::runtime_error if it can't, e.g. because the folder doesn't exist. +void OpenFolderInExplorer(HWND owner, std::string const& folderPath); +// This process's command-line arguments, without the program name, converted to UTF-8, for +// TGUICommandLine::Parse(). +std::vector ProcessArguments(); +// The file's bytes, unchanged, e.g. UTF-8 text for TGUIINIDocument. Throws std::runtime_error if it can't be +// read. +std::string ReadFileBytes(std::string const& path); +// Deletes the file. False if it couldn't be deleted, e.g. because it doesn't exist. +bool RemoveFile(std::string const& path); +// Saves 'name', the dark theme's, in the layout file at 'layoutPath', e.g. GUILayoutFilePath() (see +// SaveGUIDarkTheme()), keeping the layout and any other values in it, and creating the file and its folder if +// needed. Not saving it (e.g. a read-only profile) only means the next start uses the default. +void SaveGUIDarkThemeToLayoutFile(std::string const& layoutPath, std::string const& name); +// Saves 'theme' in the layout file at 'layoutPath', e.g. GUILayoutFilePath() (see SaveGUITheme()), keeping the +// layout and any other values in it, and creating the file and its folder if needed. Not saving it (e.g. a +// read-only profile) only means the next start uses the default. +void SaveGUIThemeToLayoutFile(std::string const& layoutPath, EGUITheme theme); +// Replaces the clipboard's content with the text, for 'owner' (a window of this process). Throws +// std::runtime_error if it can't, e.g. while another program has the clipboard open. +void SetClipboardText(HWND owner, std::string const& text); +// The window's text, e.g. a control's, converted to UTF-8. +std::string WindowText(HWND window); +// Whether Windows' setting for apps is dark: AppsUseLightTheme is 0 under +// HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize. False if it can't be read. +bool WindowsAppsUseDarkTheme(); +// Whether a high contrast theme is on. +bool WindowsHighContrast(); +// Replaces the file's content with 'bytes', unchanged, creating the file if needed (but not its folder). +// Throws std::runtime_error if it can't be written. +void WriteFileBytes(std::string const& path, std::string const& bytes); + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_Win32GUI_SystemH diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_TextDialog.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_TextDialog.cpp new file mode 100644 index 0000000..bce48b3 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_TextDialog.cpp @@ -0,0 +1,257 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_TextDialog.cpp +Author: Anthony S. West - ASW Software + +The Win32 GUI runner's text dialog. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_Win32GUI_TextDialog.h" +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Utils.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_DialogTemplate.h" +#include "ASWUnitTests_Win32GUI_DPI.h" +#include "ASWUnitTests_Win32GUI_System.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +// The text box's control ID; the OK button's is IDOK. +WORD const TextBoxID = 100; +// Posted to the dialog when it moves to a monitor with another DPI, so its monospace font is recreated after +// Windows has rescaled the dialog and its controls. +UINT const ApplyDPIMessage = WM_APP + 1; + +///////////////////////////////////////////////////////////////////////////// +// TTextDialogState +// +// What the dialog shows, and the monospace font it creates for its DPI. +///////////////////////////////////////////////////////////////////////////// +struct TTextDialogState +{ + std::wstring Text; + TWin32GUIFont MonospaceFont; + TWin32GUITheme* Theme = nullptr; +}; + +//--------------------------------------------------------------------------- + +void ApplyDPI(HWND dialog); +TWin32GUIDialogTemplate BuildDialogTemplate(std::wstring const& caption); +INT_PTR CALLBACK DialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM lParam); +INT_PTR HandleDialogMessage(HWND dialog, UINT message, WPARAM wParam, LPARAM lParam); +void LayOutDialog(HWND dialog); +void SizeAndCenterDialog(HWND dialog); + +//--------------------------------------------------------------------------- +/* + ApplyDPI + + Sets the text box's monospace font for the dialog's DPI, then lays the dialog out again. +*/ +void ApplyDPI(HWND dialog) +{ + TTextDialogState* const state = reinterpret_cast(GetWindowLongPtrW(dialog, DWLP_USER)); + + state->MonospaceFont = CreateMonospaceFont(WindowDPI(dialog)); + SendMessageW(GetDlgItem(dialog, TextBoxID), WM_SETFONT, reinterpret_cast(state->MonospaceFont.Get()), + TRUE); + LayOutDialog(dialog); +} +//--------------------------------------------------------------------------- +/* + BuildDialogTemplate + + A resizable window holding the OK button and the text box. SizeAndCenterDialog() and LayOutDialog() set + their positions and sizes in pixels. +*/ +TWin32GUIDialogTemplate BuildDialogTemplate(std::wstring const& caption) +{ + TWin32GUIDialogTemplate dialogTemplate(WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_CLIPCHILDREN, + caption); + + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Button, IDOK, WS_CHILD | WS_VISIBLE | WS_TABSTOP | + BS_DEFPUSHBUTTON, 0, L"OK"); + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Edit, TextBoxID, WS_CHILD | WS_VISIBLE | WS_TABSTOP | + WS_VSCROLL | WS_HSCROLL | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | ES_AUTOHSCROLL, WS_EX_CLIENTEDGE, + std::wstring()); + + return dialogTemplate; +} +//--------------------------------------------------------------------------- +/* + DialogProc + + No exception may leave a dialog procedure, since unwinding through Windows' own frames is undefined. One + that would ends the dialog with -1, which ShowTextDialog() reports as a failure. +*/ +INT_PTR CALLBACK DialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM lParam) +{ + try + { + return HandleDialogMessage(dialog, message, wParam, lParam); + } + catch (...) + { + EndDialog(dialog, -1); + return TRUE; + } +} +//--------------------------------------------------------------------------- +INT_PTR HandleDialogMessage(HWND dialog, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { + case WM_INITDIALOG: + { + SetWindowLongPtrW(dialog, DWLP_USER, lParam); + TTextDialogState const* const state = reinterpret_cast(lParam); + + // Without a limit, an EDIT control holds only 32K characters. + HWND const textBox = GetDlgItem(dialog, TextBoxID); + state->Theme->AttachWindow(dialog); + state->Theme->Attach(textBox, EWin32GUIThemedControl::Edit); + SendMessageW(textBox, EM_SETLIMITTEXT, 0, 0); + SetWindowTextW(textBox, state->Text.c_str()); + + // In its title bar, as the VCL GUI runner's sizeable text dialog has the application's icon, and on its + // own taskbar button when it's shown before the main window exists (e.g. for --help). + SendMessageW(dialog, WM_SETICON, ICON_BIG, reinterpret_cast(MainIcon())); + + SizeAndCenterDialog(dialog); + ApplyDPI(dialog); + + SetFocus(GetDlgItem(dialog, IDOK)); + return FALSE; // The focus is set. + } + + case WM_COMMAND: + if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) + { + EndDialog(dialog, LOWORD(wParam)); + return TRUE; + } + + return FALSE; + + case WM_DPICHANGED: + PostMessageW(dialog, ApplyDPIMessage, 0, 0); + return FALSE; // Windows moves and rescales the dialog. + + case WM_GETMINMAXINFO: + { + MINMAXINFO* const info = reinterpret_cast(lParam); + UINT const dpi = WindowDPI(dialog); + info->ptMinTrackSize.x = ScaleForDPI(320, dpi); + info->ptMinTrackSize.y = ScaleForDPI(200, dpi); + return TRUE; + } + + case WM_SIZE: + LayOutDialog(dialog); + return TRUE; + + default: + if (message == ApplyDPIMessage) + { + ApplyDPI(dialog); + return TRUE; + } + + return FALSE; + } +} +//--------------------------------------------------------------------------- +/* + LayOutDialog + + As the VCL GUI runner's text dialog: the text box fills the window above a 44-pixel strip (at 96 DPI) + that holds the OK button at its right. +*/ +void LayOutDialog(HWND dialog) +{ + RECT client; + GetClientRect(dialog, &client); + + UINT const dpi = WindowDPI(dialog); + int const stripHeight = ScaleForDPI(44, dpi); + int const textHeight = (std::max)(0, static_cast(client.bottom) - stripHeight); + + MoveWindow(GetDlgItem(dialog, TextBoxID), 0, 0, client.right, textHeight, TRUE); + MoveWindow(GetDlgItem(dialog, IDOK), client.right - ScaleForDPI(96, dpi), textHeight + ScaleForDPI(9, dpi), + ScaleForDPI(84, dpi), ScaleForDPI(26, dpi), TRUE); +} +//--------------------------------------------------------------------------- +/* + SizeAndCenterDialog + + Gives the dialog the VCL GUI runner's text dialog's client size, 760 by 520 at 96 DPI, centered on the + work area of its owner's monitor, or of the one it opened on if it has no owner. +*/ +void SizeAndCenterDialog(HWND dialog) +{ + UINT const dpi = WindowDPI(dialog); + RECT frame = { 0, 0, ScaleForDPI(760, dpi), ScaleForDPI(520, dpi) }; + AdjustWindowRectExForDpi(&frame, static_cast(GetWindowLongPtrW(dialog, GWL_STYLE)), FALSE, + static_cast(GetWindowLongPtrW(dialog, GWL_EXSTYLE)), dpi); + + HWND const owner = GetWindow(dialog, GW_OWNER); + MONITORINFO monitor = {}; + monitor.cbSize = sizeof(monitor); + GetMonitorInfoW(MonitorFromWindow(owner != nullptr ? owner : dialog, MONITOR_DEFAULTTONEAREST), &monitor); + + RECT const& work = monitor.rcWork; + int const width = (std::min)(static_cast(frame.right - frame.left), static_cast(work.right - work.left)); + int const height = (std::min)(static_cast(frame.bottom - frame.top), static_cast(work.bottom - work.top)); + + SetWindowPos(dialog, nullptr, work.left + (work.right - work.left - width) / 2, + work.top + (work.bottom - work.top - height) / 2, width, height, SWP_NOZORDER | SWP_NOACTIVATE); +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +//--------------------------------------------------------------------------- +void ShowTextDialog(HWND owner, TWin32GUITheme& theme, std::string const& caption, std::string const& text) +{ + TTextDialogState state; + state.Text = UTF8ToWide(AdjustLineBreaks(text)); + state.Theme = &theme; + + TWin32GUIDialogTemplate const dialogTemplate = BuildDialogTemplate(UTF8ToWide(caption)); + + INT_PTR const result = DialogBoxIndirectParamW(GetModuleHandleW(nullptr), dialogTemplate.Get(), owner, + &DialogProc, reinterpret_cast(&state)); + + if (result == -1) + throw std::runtime_error("Couldn't show the \"" + caption + "\" dialog"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_TextDialog.h b/win32/gui/src/ASWUnitTests_Win32GUI_TextDialog.h new file mode 100644 index 0000000..2dc263e --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_TextDialog.h @@ -0,0 +1,47 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_TextDialog.h +Author: Anthony S. West - ASW Software + +The Win32 GUI runner's text dialog, the counterpart of the VCL GUI runner's +ASWUnitTests_GUI_TextDialog. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_Win32GUI_TextDialogH +#define ASWUnitTests_Win32GUI_TextDialogH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Theme.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +// Shows 'text' (UTF-8) in a modal, resizable dialog with a read-only, scrollable, monospace text box, for +// console-style output such as --help, whose aligned columns a regular message box's proportional font would +// scramble. 'caption' (UTF-8) is the dialog's title. 'owner' may be nullptr, e.g. before the main window +// exists. The dialog has 'theme''s look. Throws std::runtime_error if the dialog can't be shown. +void ShowTextDialog(HWND owner, TWin32GUITheme& theme, std::string const& caption, std::string const& text); + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_Win32GUI_TextDialogH diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Theme.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_Theme.cpp new file mode 100644 index 0000000..069bb52 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Theme.cpp @@ -0,0 +1,2760 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_Theme.cpp +Author: Anthony S. West - ASW Software + +The Win32 GUI runner's light and dark looks. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "ASWUnitTests_Win32GUI_Theme.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_Theme.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Glyphs.h" +#include "ASWUnitTests_Win32GUI_System.h" +//--------------------------------------------------------------------------- + +// RAD Studio's Win32 (bcc32c) links its own Windows import libraries, which don't include SetWindowTheme()'s. +// CMake links uxtheme for the other compilers, and RAD Studio's Win64x links it already. +#if defined(__BORLANDC__) && !defined(_WIN64) +# pragma comment(lib, "uxtheme.lib") +#endif + +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +// The theme Windows' own dark controls (scroll bars, the tree's expand arrows) are drawn from. Not documented, +// but used widely; where it's missing, the controls keep their light theme. +wchar_t const DarkExplorerTheme[] = L"DarkMode_Explorer"; +// DwmSetWindowAttribute()'s attribute for a dark title bar: 20 from Windows 10 version 20H1 on, 19 before. +DWORD const DarkTitleBarAttribute = 20; +DWORD const DarkTitleBarAttributeBefore20H1 = 19; +// The theme the tree and the list have while light, as File Explorer draws them, as the VCL draws its own. +wchar_t const ExplorerTheme[] = L"Explorer"; +// The window property that holds which tab is under the mouse, plus one (0 for none), while dark. +wchar_t const HotTabProperty[] = L"ASWUnitTests_Win32GUI_HotTab"; +// The window property set on a control that had WS_EX_CLIENTEDGE, which it has in place of it while dark; see +// TWin32GUITheme::ApplyToBorder(). +wchar_t const OwnBorderProperty[] = L"ASWUnitTests_Win32GUI_OwnBorder"; +// The window property set on the tree or the list while its size changes, between WM_WINDOWPOSCHANGING and +// WM_WINDOWPOSCHANGED; see TWin32GUITheme::HandleControlMessage(). +wchar_t const ResizingProperty[] = L"ASWUnitTests_Win32GUI_Resizing"; +// The menu windows' class, documented as a system class. +wchar_t const MenuClassName[] = L"#32768"; +// The subclass IDs: an attached control's is ControlSubclassBase plus its kind. +UINT_PTR const ControlSubclassBase = 100; +UINT_PTR const MenuFrameSubclassID = 2; +UINT_PTR const WindowSubclassID = 1; + +///////////////////////////////////////////////////////////////////////////// +// TGlyphCanvas +// +// A small image built pixel by pixel, e.g. a check box from its border, its +// inside and its mark, then drawn or made into a bitmap. +///////////////////////////////////////////////////////////////////////////// +class TGlyphCanvas +{ +private: + int const m_Height; + // Top-down 32-bit pixels, 0xAARRGGBB, fully opaque. + std::vector m_Pixels; + int const m_Width; + +private: + static std::uint32_t ToPixel(COLORREF color); + +private: + BITMAPINFO BitmapInfo() const; + +public: + TGlyphCanvas(int width, int height, COLORREF background); + + // Mixes 'color' into the pixels 'glyph' covers in the square at 'left', 'top' of side 'size'. + void Blend(EWin32GUIGlyph glyph, int left, int top, int size, COLORREF color); + // Takes the pixels at 'left', 'top' of 'context' as the canvas's own, e.g. a gradient a glyph is then drawn + // over. Leaves the canvas as it was if they can't be read. + void CopyFrom(HDC context, int left, int top); + // A bitmap of the canvas, which the caller deletes; nullptr if it can't be created. + HBITMAP CreateBitmap() const; + void Draw(HDC context, int left, int top) const; + // Mixes every pixel toward 'toward' in the proportion 'numerator' / 'denominator' (see BlendColors()). + void Fade(COLORREF toward, int numerator, int denominator); + void FillRect(RECT const& rect, COLORREF color); + // The pixel at 'x', 'y', or black outside the canvas. + COLORREF PixelAt(int x, int y) const; +}; + +///////////////////////////////////////////////////////////////////////////// +// TPaintBuffer +// +// Paints through a bitmap the size of a rectangle, copied to the target in +// one step when this object is destroyed, so nothing painted in between is +// seen, and so nothing flickers. If the bitmap can't be created, painting +// goes straight to the target. +///////////////////////////////////////////////////////////////////////////// +class TPaintBuffer +{ +private: + HBITMAP m_Bitmap; + HDC m_Context; + HGDIOBJ m_OldBitmap; + RECT const m_Rect; + HDC const m_Target; + +public: + TPaintBuffer(HDC target, RECT const& rect); + ~TPaintBuffer(); + + TPaintBuffer(TPaintBuffer const&) = delete; + TPaintBuffer& operator=(TPaintBuffer const&) = delete; + + // The context to paint into, in the target's coordinates. + HDC Get() const; +}; + +//--------------------------------------------------------------------------- + +COLORREF BackgroundColor(HWND control, EWin32GUIThemedControl kind); +int BorderWidth(HWND control); +bool ClassNameIs(HWND window, wchar_t const* className); +void ExcludeRect(HRGN region, RECT const& rect); +void FillGradient(HDC context, RECT const& rect, COLORREF top, COLORREF bottom); +void FillSolid(HDC context, RECT const& rect, COLORREF color); +void FrameSolid(HDC context, RECT const& rect, COLORREF color); +bool HasThemedBorder(HWND control); +bool HidesAccelerators(HWND control); +bool HidesFocus(HWND control); +RECT Inset(RECT rect, int by); +bool IsOverDisabledScrollBar(HWND control, WPARAM hitTest); +bool MayRedrawScrollBars(UINT message); +void SetImmersiveDarkMode(HWND window, bool dark); +void SplitShortcut(std::wstring const& text, std::wstring& caption, std::wstring& shortcut); +SIZE TextSize(HDC context, std::wstring const& text, UINT format); +std::wstring WideWindowText(HWND window); + +//--------------------------------------------------------------------------- +/* + BackgroundColor + + The background the tree or the list paints, by asking it: the system's window color if it has none of its + own. +*/ +COLORREF BackgroundColor(HWND control, EWin32GUIThemedControl kind) +{ + // The tree's -1 for none is CLR_NONE too, as a COLORREF. + COLORREF const color = static_cast(kind == EWin32GUIThemedControl::TreeView ? + SendMessageW(control, TVM_GETBKCOLOR, 0, 0) : SendMessageW(control, LVM_GETBKCOLOR, 0, 0)); + + return color == CLR_NONE ? GetSysColor(COLOR_WINDOW) : color; +} +//--------------------------------------------------------------------------- +/* + BorderWidth + + WS_EX_CLIENTEDGE's width at the control's DPI, which the dark look's own border has too. +*/ +int BorderWidth(HWND control) +{ + return (std::max)(1, GetSystemMetricsForDpi(SM_CXEDGE, WindowDPI(control))); +} +//--------------------------------------------------------------------------- +bool ClassNameIs(HWND window, wchar_t const* className) +{ + wchar_t name[64] = {}; + GetClassNameW(window, name, 64); + + return lstrcmpiW(name, className) == 0; +} +//--------------------------------------------------------------------------- +void ExcludeRect(HRGN region, RECT const& rect) +{ + HRGN const excluded = CreateRectRgnIndirect(&rect); + + if (excluded == nullptr) + return; + + CombineRgn(region, region, excluded, RGN_DIFF); + DeleteObject(excluded); +} +//--------------------------------------------------------------------------- +/* + FillGradient + + A vertical gradient, one row at a time: 'top' at the first row and 'bottom' at the last. +*/ +void FillGradient(HDC context, RECT const& rect, COLORREF top, COLORREF bottom) +{ + int const height = rect.bottom - rect.top; + + for (int y = 0; y < height; ++y) + { + RECT const row = { rect.left, rect.top + y, rect.right, rect.top + y + 1 }; + FillSolid(context, row, BlendColors(top, bottom, y, height - 1)); + } +} +//--------------------------------------------------------------------------- +void FillSolid(HDC context, RECT const& rect, COLORREF color) +{ + COLORREF const old = SetDCBrushColor(context, color); + FillRect(context, &rect, static_cast(GetStockObject(DC_BRUSH))); + SetDCBrushColor(context, old); +} +//--------------------------------------------------------------------------- +void FrameSolid(HDC context, RECT const& rect, COLORREF color) +{ + COLORREF const old = SetDCBrushColor(context, color); + FrameRect(context, &rect, static_cast(GetStockObject(DC_BRUSH))); + SetDCBrushColor(context, old); +} +//--------------------------------------------------------------------------- +/* + HasThemedBorder + + Whether the dark look paints the control's border (see TWin32GUITheme::PaintBorder()): it has one of its own + in place of WS_EX_CLIENTEDGE, or still has WS_EX_CLIENTEDGE. +*/ +bool HasThemedBorder(HWND control) +{ + return GetPropW(control, OwnBorderProperty) != nullptr || + (GetWindowLongPtrW(control, GWL_EXSTYLE) & WS_EX_CLIENTEDGE) != 0; +} +//--------------------------------------------------------------------------- +bool HidesAccelerators(HWND control) +{ + return (SendMessageW(control, WM_QUERYUISTATE, 0, 0) & UISF_HIDEACCEL) != 0; +} +//--------------------------------------------------------------------------- +bool HidesFocus(HWND control) +{ + return (SendMessageW(control, WM_QUERYUISTATE, 0, 0) & UISF_HIDEFOCUS) != 0; +} +//--------------------------------------------------------------------------- +RECT Inset(RECT rect, int by) +{ + InflateRect(&rect, -by, -by); + return rect; +} +//--------------------------------------------------------------------------- +/* + IsOverDisabledScrollBar + + Whether 'hitTest', a WM_NCHITTEST result, is the control's vertical or horizontal scroll bar, and that bar is + disabled. +*/ +bool IsOverDisabledScrollBar(HWND control, WPARAM hitTest) +{ + if (hitTest != HTVSCROLL && hitTest != HTHSCROLL) + return false; + + SCROLLBARINFO info = {}; + info.cbSize = sizeof(info); + + return GetScrollBarInfo(control, hitTest == HTVSCROLL ? OBJID_VSCROLL : OBJID_HSCROLL, &info) != 0 && + (info.rgstate[0] & STATE_SYSTEM_UNAVAILABLE) != 0; +} +//--------------------------------------------------------------------------- +/* + MayRedrawScrollBars + + Whether a control with scroll bars (an edit box, the tree, the list, the log) may draw them straight away, + rather than in WM_NCPAINT, while it handles 'message': as its text changes (an edit box enables or disables + them), as it's resized (they may appear or go, and with them the corner between them), and as the mouse + enters or leaves it or its scroll bars (a themed scroll bar's arrows show whether the mouse is over the + window). +*/ +bool MayRedrawScrollBars(UINT message) +{ + switch (message) + { + case EM_REPLACESEL: + case WM_CAPTURECHANGED: + case WM_ENABLE: + case WM_KILLFOCUS: + case WM_MOUSELEAVE: + case WM_MOUSEMOVE: + case WM_NCLBUTTONDOWN: + case WM_NCLBUTTONUP: + case WM_NCMOUSELEAVE: + case WM_NCMOUSEMOVE: + case WM_SETFOCUS: + case WM_SETTEXT: + case WM_SIZE: + case WM_WINDOWPOSCHANGED: + return true; + + default: + return false; + } +} +//--------------------------------------------------------------------------- +/* + SetImmersiveDarkMode + + DwmSetWindowAttribute() is loaded from dwmapi.dll as it's needed, rather than linked, so the GUI needs no + import library of it (RAD Studio's Win32 and Win64x have different ones, and CMake would need it listed for + every project that builds the GUI). Windows before 10 version 1809 has neither attribute, and keeps the + title bar light. +*/ +void SetImmersiveDarkMode(HWND window, bool dark) +{ + using TDwmSetWindowAttribute = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD); + + static HMODULE const library = LoadLibraryW(L"dwmapi.dll"); + static TDwmSetWindowAttribute const setAttribute = library == nullptr ? nullptr : + reinterpret_cast(reinterpret_cast(GetProcAddress(library, + "DwmSetWindowAttribute"))); + + if (setAttribute == nullptr) + return; + + BOOL const value = dark ? TRUE : FALSE; + + if (FAILED(setAttribute(window, DarkTitleBarAttribute, &value, sizeof(value)))) + setAttribute(window, DarkTitleBarAttributeBefore20H1, &value, sizeof(value)); +} +//--------------------------------------------------------------------------- +/* + SplitShortcut + + A menu item's text, e.g. "&Run Selected\tF9", as its caption and its shortcut, which is empty if it has + none. +*/ +void SplitShortcut(std::wstring const& text, std::wstring& caption, std::wstring& shortcut) +{ + std::size_t const tab = text.find(L'\t'); + caption = text.substr(0, tab); + shortcut = tab == std::wstring::npos ? std::wstring() : text.substr(tab + 1); +} +//--------------------------------------------------------------------------- +SIZE TextSize(HDC context, std::wstring const& text, UINT format) +{ + RECT bounds = { 0, 0, 0, 0 }; + DrawTextW(context, text.c_str(), static_cast(text.size()), &bounds, format | DT_CALCRECT | DT_SINGLELINE); + + return { bounds.right - bounds.left, bounds.bottom - bounds.top }; +} +//--------------------------------------------------------------------------- +std::wstring WideWindowText(HWND window) +{ + std::wstring text(static_cast(GetWindowTextLengthW(window)) + 1, L'\0'); + text.resize(static_cast(GetWindowTextW(window, &text[0], static_cast(text.size())))); + + return text; +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TGlyphCanvas +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TGlyphCanvas::TGlyphCanvas(int width, int height, COLORREF background) + : m_Height((std::max)(height, 1)), + m_Pixels(static_cast((std::max)(width, 1)) * static_cast((std::max)(height, 1)), + ToPixel(background)), + m_Width((std::max)(width, 1)) +{ +} +//--------------------------------------------------------------------------- +BITMAPINFO TGlyphCanvas::BitmapInfo() const +{ + BITMAPINFO info = {}; + info.bmiHeader.biSize = sizeof(info.bmiHeader); + info.bmiHeader.biWidth = m_Width; + info.bmiHeader.biHeight = -m_Height; // Top-down. + info.bmiHeader.biPlanes = 1; + info.bmiHeader.biBitCount = 32; + info.bmiHeader.biCompression = BI_RGB; + + return info; +} +//--------------------------------------------------------------------------- +void TGlyphCanvas::Blend(EWin32GUIGlyph glyph, int left, int top, int size, COLORREF color) +{ + std::vector const coverage = RenderWin32GUIGlyph(glyph, size); + + for (int y = 0; y < size; ++y) + { + for (int x = 0; x < size; ++x) + { + int const canvasX = left + x; + int const canvasY = top + y; + std::uint8_t const amount = coverage[static_cast(y) * static_cast(size) + + static_cast(x)]; + + if (amount == 0 || canvasX < 0 || canvasY < 0 || canvasX >= m_Width || canvasY >= m_Height) + continue; + + std::uint32_t& pixel = m_Pixels[static_cast(canvasY) * static_cast(m_Width) + + static_cast(canvasX)]; + COLORREF const under = RGB((pixel >> 16) & 0xFF, (pixel >> 8) & 0xFF, pixel & 0xFF); + pixel = ToPixel(BlendColors(under, color, amount, 255)); + } + } +} +//--------------------------------------------------------------------------- +void TGlyphCanvas::CopyFrom(HDC context, int left, int top) +{ + BITMAPINFO const info = BitmapInfo(); + void* bits = nullptr; + HBITMAP const bitmap = CreateDIBSection(context, &info, DIB_RGB_COLORS, &bits, nullptr, 0); + HDC const copy = CreateCompatibleDC(context); + + if (bitmap != nullptr && bits != nullptr && copy != nullptr) + { + HGDIOBJ const old = SelectObject(copy, bitmap); + + if (BitBlt(copy, 0, 0, m_Width, m_Height, context, left, top, SRCCOPY) != 0) + { + GdiFlush(); + std::uint32_t const* const pixels = static_cast(bits); + + for (std::size_t i = 0; i < m_Pixels.size(); ++i) + m_Pixels[i] = 0xFF000000u | pixels[i]; + } + + SelectObject(copy, old); + } + + if (copy != nullptr) + DeleteDC(copy); + + if (bitmap != nullptr) + DeleteObject(bitmap); +} +//--------------------------------------------------------------------------- +HBITMAP TGlyphCanvas::CreateBitmap() const +{ + BITMAPINFO const info = BitmapInfo(); + void* bits = nullptr; + HBITMAP const bitmap = CreateDIBSection(nullptr, &info, DIB_RGB_COLORS, &bits, nullptr, 0); + + if (bitmap != nullptr && bits != nullptr) + std::copy(m_Pixels.begin(), m_Pixels.end(), static_cast(bits)); + + return bitmap; +} +//--------------------------------------------------------------------------- +void TGlyphCanvas::Draw(HDC context, int left, int top) const +{ + BITMAPINFO const info = BitmapInfo(); + SetDIBitsToDevice(context, left, top, static_cast(m_Width), static_cast(m_Height), 0, 0, 0, + static_cast(m_Height), m_Pixels.data(), &info, DIB_RGB_COLORS); +} +//--------------------------------------------------------------------------- +void TGlyphCanvas::Fade(COLORREF toward, int numerator, int denominator) +{ + for (std::uint32_t& pixel : m_Pixels) + { + COLORREF const color = RGB((pixel >> 16) & 0xFF, (pixel >> 8) & 0xFF, pixel & 0xFF); + pixel = ToPixel(BlendColors(color, toward, numerator, denominator)); + } +} +//--------------------------------------------------------------------------- +void TGlyphCanvas::FillRect(RECT const& rect, COLORREF color) +{ + std::uint32_t const pixel = ToPixel(color); + + for (int y = (std::max)(0, static_cast(rect.top)); y < (std::min)(m_Height, static_cast(rect.bottom)); + ++y) + { + for (int x = (std::max)(0, static_cast(rect.left)); x < (std::min)(m_Width, static_cast(rect.right)); + ++x) + { + m_Pixels[static_cast(y) * static_cast(m_Width) + static_cast(x)] = + pixel; + } + } +} +//--------------------------------------------------------------------------- +COLORREF TGlyphCanvas::PixelAt(int x, int y) const +{ + if (x < 0 || y < 0 || x >= m_Width || y >= m_Height) + return RGB(0, 0, 0); + + std::size_t const row = static_cast(y) * static_cast(m_Width); + std::uint32_t const pixel = m_Pixels[row + static_cast(x)]; + return RGB((pixel >> 16) & 0xFF, (pixel >> 8) & 0xFF, pixel & 0xFF); +} +//--------------------------------------------------------------------------- +std::uint32_t TGlyphCanvas::ToPixel(COLORREF color) +{ + return 0xFF000000u | (static_cast(GetRValue(color)) << 16) | + (static_cast(GetGValue(color)) << 8) | static_cast(GetBValue(color)); +} +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TPaintBuffer +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TPaintBuffer::TPaintBuffer(HDC target, RECT const& rect) + : m_Bitmap(nullptr), + m_Context(nullptr), + m_OldBitmap(nullptr), + m_Rect(rect), + m_Target(target) +{ + int const width = rect.right - rect.left; + int const height = rect.bottom - rect.top; + + if (width <= 0 || height <= 0) + return; + + m_Context = CreateCompatibleDC(target); + m_Bitmap = m_Context != nullptr ? CreateCompatibleBitmap(target, width, height) : nullptr; + + if (m_Bitmap == nullptr) + { + if (m_Context != nullptr) + DeleteDC(m_Context); + + m_Context = nullptr; + return; + } + + m_OldBitmap = SelectObject(m_Context, m_Bitmap); + SetViewportOrgEx(m_Context, -rect.left, -rect.top, nullptr); +} +//--------------------------------------------------------------------------- +TPaintBuffer::~TPaintBuffer() +{ + if (m_Context == nullptr) + return; + + SetViewportOrgEx(m_Context, 0, 0, nullptr); + BitBlt(m_Target, m_Rect.left, m_Rect.top, m_Rect.right - m_Rect.left, m_Rect.bottom - m_Rect.top, m_Context, 0, 0, + SRCCOPY); + SelectObject(m_Context, m_OldBitmap); + DeleteObject(m_Bitmap); + DeleteDC(m_Context); +} +//--------------------------------------------------------------------------- +HDC TPaintBuffer::Get() const +{ + return m_Context != nullptr ? m_Context : m_Target; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUITheme +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TWin32GUITheme* TWin32GUITheme::s_Instance = nullptr; + +//--------------------------------------------------------------------------- +TWin32GUITheme::TWin32GUITheme() + : m_Dark(false), + m_MenuBar(nullptr), + m_MenuBarWindow(nullptr), + m_MenuBrush(nullptr), + m_MenuFontDPI(0), + m_MenuHook(nullptr), + m_Palette(CarbonPalette()), + m_PanelBrush(nullptr), + m_WindowBrush(nullptr) +{ + m_MenuBrush = CreateSolidBrush(m_Palette.MenuBackground); + m_PanelBrush = CreateSolidBrush(m_Palette.Panel); + m_WindowBrush = CreateSolidBrush(m_Palette.Window); + + // Without the hook, the menus' frames stay light. + s_Instance = this; + m_MenuHook = SetWindowsHookExW(WH_CALLWNDPROC, &MenuHookProc, nullptr, GetCurrentThreadId()); +} +//--------------------------------------------------------------------------- +TWin32GUITheme::~TWin32GUITheme() +{ + if (m_MenuHook != nullptr) + UnhookWindowsHookEx(m_MenuHook); + + if (s_Instance == this) + s_Instance = nullptr; + + for (std::pair const& themed : m_ThemedCheckImages) + ImageList_Destroy(themed.second); + + for (std::pair const& dark : m_DarkCheckImages) + ImageList_Destroy(dark.second); + + for (HBRUSH const brush : { m_MenuBrush, m_PanelBrush, m_WindowBrush }) + { + if (brush != nullptr) + DeleteObject(brush); + } +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::ApplyToBorder + + A control with WS_EX_CLIENTEDGE paints that border itself, in a light color, whatever its theme, in each + WM_NCPAINT; painting a dark one over it afterwards shows the light one for a moment, e.g. while a splitter + is dragged. So while dark it has no WS_EX_CLIENTEDGE: WM_NCCALCSIZE keeps the same width for a border of + its own, around its scroll bars, which WM_NCPAINT paints (see HandleControlMessage()). Light puts + WS_EX_CLIENTEDGE back. +*/ +void TWin32GUITheme::ApplyToBorder(HWND control) const +{ + LONG_PTR const exStyle = GetWindowLongPtrW(control, GWL_EXSTYLE); + bool const ownBorder = GetPropW(control, OwnBorderProperty) != nullptr; + + if (m_Dark && (exStyle & WS_EX_CLIENTEDGE) != 0) + { + SetPropW(control, OwnBorderProperty, reinterpret_cast(static_cast(1))); + SetWindowLongPtrW(control, GWL_EXSTYLE, exStyle & ~static_cast(WS_EX_CLIENTEDGE)); + } + else if (!m_Dark && ownBorder) + { + RemovePropW(control, OwnBorderProperty); + SetWindowLongPtrW(control, GWL_EXSTYLE, exStyle | WS_EX_CLIENTEDGE); + } + else if (!ownBorder) + { + return; + } + + // Its non-client area is measured again, also for a new DPI while dark. + SetWindowPos(control, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | + SWP_FRAMECHANGED); +} +//--------------------------------------------------------------------------- +void TWin32GUITheme::ApplyToControl(TAttachedControl const& attached) +{ + HWND const control = attached.Control; + wchar_t const* const darkOrDefault = m_Dark ? DarkExplorerTheme : nullptr; + + switch (attached.Kind) + { + case EWin32GUIThemedControl::Edit: + // Its scroll bars; its colors come from WM_CTLCOLOREDIT and WM_CTLCOLORSTATIC. + SetWindowTheme(control, darkOrDefault, nullptr); + break; + + case EWin32GUIThemedControl::ListView: + { + SetWindowTheme(control, m_Dark ? DarkExplorerTheme : ExplorerTheme, nullptr); + HWND const header = reinterpret_cast(SendMessageW(control, LVM_GETHEADER, 0, 0)); + + if (header != nullptr) + { + SetWindowTheme(header, m_Dark ? L"DarkMode_ItemsView" : nullptr, nullptr); + } + + COLORREF const background = WindowColor(); + SendMessageW(control, LVM_SETBKCOLOR, 0, static_cast(background)); + SendMessageW(control, LVM_SETTEXTBKCOLOR, 0, static_cast(background)); + SendMessageW(control, LVM_SETTEXTCOLOR, 0, static_cast(WindowTextColor())); + break; + } + + case EWin32GUIThemedControl::RichEdit: + SetWindowTheme(control, darkOrDefault, nullptr); + SendMessageW(control, EM_SETBKGNDCOLOR, m_Dark ? FALSE : TRUE, static_cast(m_Palette.Window)); + break; + + case EWin32GUIThemedControl::Tooltip: + if (m_Dark) + { + // Without visual styles, so it takes the colors given. + SetWindowTheme(control, L" ", L" "); + SendMessageW(control, TTM_SETTIPBKCOLOR, static_cast(m_Palette.TooltipBackground), 0); + SendMessageW(control, TTM_SETTIPTEXTCOLOR, static_cast(m_Palette.TooltipText), 0); + } + else + { + SetWindowTheme(control, nullptr, nullptr); + SendMessageW(control, TTM_SETTIPBKCOLOR, static_cast(GetSysColor(COLOR_INFOBK)), 0); + SendMessageW(control, TTM_SETTIPTEXTCOLOR, static_cast(GetSysColor(COLOR_INFOTEXT)), 0); + } + + return; // A tooltip is drawn when it next shows. + + case EWin32GUIThemedControl::TreeView: + ApplyToTree(control); + break; + + case EWin32GUIThemedControl::ProgressBar: + case EWin32GUIThemedControl::Splitter: + case EWin32GUIThemedControl::StatusBar: + case EWin32GUIThemedControl::TabControl: + case EWin32GUIThemedControl::UpDown: + break; // Painted by ControlProc() while dark. + } + + if (attached.Kind == EWin32GUIThemedControl::Edit || attached.Kind == EWin32GUIThemedControl::ListView || + attached.Kind == EWin32GUIThemedControl::RichEdit || attached.Kind == EWin32GUIThemedControl::TreeView) + { + ApplyToBorder(control); + } + + RedrawWindow(control, nullptr, nullptr, RDW_ERASE | RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN); +} +//--------------------------------------------------------------------------- +void TWin32GUITheme::ApplyToMenu(HMENU menu, std::vector >& items) +{ + int const count = GetMenuItemCount(menu); + + for (int position = 0; position < count; ++position) + { + wchar_t text[256] = {}; + MENUITEMINFOW info = {}; + info.cbSize = sizeof(info); + info.fMask = MIIM_DATA | MIIM_FTYPE | MIIM_ID | MIIM_STATE | MIIM_STRING | MIIM_SUBMENU; + info.dwTypeData = text; + info.cch = 256; + + if (GetMenuItemInfoW(menu, static_cast(position), TRUE, &info) == 0) + continue; + + // An owner-drawn item's string can't be read back reliably, so it's taken from its item. + TMenuItem const* const current = FindMenuItem(info.dwItemData); + + if (current != nullptr) + text[current->Text.copy(text, 255)] = L'\0'; + + // Its string is set again too, which an owner-drawn item may have lost. + MENUITEMINFOW change = {}; + change.cbSize = sizeof(change); + change.fMask = MIIM_FTYPE | MIIM_DATA | ((info.fType & MFT_SEPARATOR) != 0 ? 0 : MIIM_STRING); + change.fType = info.fType & ~static_cast(MFT_OWNERDRAW); + change.dwTypeData = text; + + if (m_Dark) + { + std::unique_ptr item = std::make_unique(); + item->Menu = menu; + item->Position = static_cast(position); + item->Text = text; + item->Separator = (info.fType & MFT_SEPARATOR) != 0; + item->Submenu = info.hSubMenu != nullptr; + item->TopLevel = menu == m_MenuBar; + + // Its string stays, so Windows still finds its & letter. + change.fType |= MFT_OWNERDRAW; + change.dwItemData = reinterpret_cast(item.get()); + items.push_back(std::move(item)); + } + + // An item keeps what Windows worked out for it in the look it was last shown in, whatever changes: a menu + // bar item its width, and a menu's item, once only shown owner-drawn, has no caption in the standard look. + // So each is replaced by a new one. + change.fMask |= MIIM_ID | MIIM_STATE | MIIM_SUBMENU; + change.wID = info.wID; + change.fState = info.fState; + change.hSubMenu = info.hSubMenu; + RemoveMenu(menu, static_cast(position), MF_BYPOSITION); + InsertMenuItemW(menu, static_cast(position), TRUE, &change); + + if (info.hSubMenu != nullptr) + { + MENUINFO background = {}; + background.cbSize = sizeof(background); + background.fMask = MIM_BACKGROUND; + background.hbrBack = m_Dark ? m_MenuBrush : nullptr; + SetMenuInfo(info.hSubMenu, &background); + + ApplyToMenu(info.hSubMenu, items); + } + } +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::ApplyToMenus + + The new items replace the old ones only once every menu item points to its new one. +*/ +void TWin32GUITheme::ApplyToMenus() +{ + if (m_MenuBar == nullptr) + return; + + std::vector > items; + ApplyToMenu(m_MenuBar, items); + m_MenuItems.swap(items); + + // The menu bar's empty part, right of its items. + MENUINFO background = {}; + background.cbSize = sizeof(background); + background.fMask = MIM_BACKGROUND; + background.hbrBack = m_Dark ? m_PanelBrush : nullptr; + SetMenuInfo(m_MenuBar, &background); + + DrawMenuBar(m_MenuBarWindow); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::ApplyToTitleBar + + A visible window's frame is redrawn, so a live switch shows on its title bar too. +*/ +void TWin32GUITheme::ApplyToTitleBar(HWND window) const +{ + SetImmersiveDarkMode(window, m_Dark); + + if (IsWindowVisible(window) != FALSE) + { + SetWindowPos(window, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | + SWP_FRAMECHANGED); + } +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::ApplyToTree + + A tree with check boxes shows Carbon's while dark, keeping its own to give back when it turns light. Its own + are given back before its theme changes, and replaced after, since a theme change redraws the images of the + list it has. +*/ +void TWin32GUITheme::ApplyToTree(HWND tree) +{ + bool const checkBoxes = (GetWindowLongPtrW(tree, GWL_STYLE) & TVS_CHECKBOXES) != 0; + std::vector >::iterator const themed = std::find_if(m_ThemedCheckImages.begin(), + m_ThemedCheckImages.end(), [tree](std::pair const& entry) + { + return entry.first == tree; + }); + + if (!m_Dark && themed != m_ThemedCheckImages.end()) + { + SendMessageW(tree, TVM_SETIMAGELIST, TVSIL_STATE, reinterpret_cast(themed->second)); + m_ThemedCheckImages.erase(themed); + } + + SetWindowTheme(tree, m_Dark ? DarkExplorerTheme : ExplorerTheme, nullptr); + SendMessageW(tree, TVM_SETBKCOLOR, 0, m_Dark ? static_cast(m_Palette.Window) : -1); + SendMessageW(tree, TVM_SETTEXTCOLOR, 0, m_Dark ? static_cast(m_Palette.WindowText) : -1); + SendMessageW(tree, TVM_SETLINECOLOR, 0, m_Dark ? static_cast(m_Palette.TreeLine) : + static_cast(CLR_DEFAULT)); + + if (m_Dark && checkBoxes) + { + HIMAGELIST const previous = reinterpret_cast(SendMessageW(tree, TVM_SETIMAGELIST, TVSIL_STATE, + reinterpret_cast(DarkCheckImages(WindowDPI(tree))))); + bool const previousIsDark = std::any_of(m_DarkCheckImages.begin(), m_DarkCheckImages.end(), + [previous](std::pair const& entry) + { + return entry.second == previous; + }); + + if (previous != nullptr && !previousIsDark) + m_ThemedCheckImages.emplace_back(tree, previous); + } +} +//--------------------------------------------------------------------------- +void TWin32GUITheme::Attach(HWND control, EWin32GUIThemedControl kind) +{ + if (control == nullptr) + return; + + m_Controls.push_back({ control, kind }); + SetWindowSubclass(control, &ControlProc, ControlSubclassBase + static_cast(kind), + reinterpret_cast(this)); + ApplyToControl(m_Controls.back()); + + if (kind == EWin32GUIThemedControl::TabControl) + AttachUpDown(control); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::AttachUpDown + + A tab control creates its scroll arrows, a child up-down control, the first time its tabs don't fit, and + keeps them, hidden, once they fit again. It's looked for when the tab control is attached, created + (WM_PARENTNOTIFY) and resized, in case it doesn't notify. +*/ +void TWin32GUITheme::AttachUpDown(HWND tabControl) +{ + HWND const upDown = FindWindowExW(tabControl, nullptr, UPDOWN_CLASSW, nullptr); + + if (upDown == nullptr) + return; + + bool const attached = std::any_of(m_Controls.begin(), m_Controls.end(), [upDown](TAttachedControl const& entry) + { + return entry.Control == upDown; + }); + + if (!attached) + Attach(upDown, EWin32GUIThemedControl::UpDown); +} +//--------------------------------------------------------------------------- +void TWin32GUITheme::AttachWindow(HWND window, HMENU menuBar) +{ + if (window == nullptr) + return; + + m_Windows.push_back(window); + SetWindowSubclass(window, &WindowProc, WindowSubclassID, reinterpret_cast(this)); + + if (menuBar != nullptr) + { + m_MenuBar = menuBar; + m_MenuBarWindow = window; + ApplyToMenus(); + } + + ApplyToTitleBar(window); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::ControlProc + + An attached control's subclass. No exception may leave a window procedure; one that would leaves the + message to the control. +*/ +LRESULT CALLBACK TWin32GUITheme::ControlProc(HWND control, UINT message, WPARAM wParam, LPARAM lParam, UINT_PTR id, + DWORD_PTR theme) +{ + try + { + EWin32GUIThemedControl const kind = static_cast(id - ControlSubclassBase); + + if (message == WM_NCDESTROY) + { + reinterpret_cast(theme)->Unattach(control); + RemovePropW(control, HotTabProperty); + RemovePropW(control, OwnBorderProperty); + RemovePropW(control, ResizingProperty); + RemoveWindowSubclass(control, &ControlProc, id); + return DefSubclassProc(control, message, wParam, lParam); + } + + return reinterpret_cast(theme)->HandleControlMessage(control, kind, message, wParam, lParam); + } + catch (...) + { + return DefSubclassProc(control, message, wParam, lParam); + } +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::CreateDarkCheckImage + + Carbon's check box, 13 pixels at 96 DPI, in the middle of the tree's 16-pixel state image. +*/ +HBITMAP TWin32GUITheme::CreateDarkCheckImage(int state, UINT dpi) const +{ + int const cell = ScaleForDPI(16, dpi); + int const box = ScaleForDPI(13, dpi); + int const offset = (cell - box) / 2; + int const border = (std::max)(1, ScaleForDPI(1, dpi)); + + TGlyphCanvas canvas(cell, cell, m_Palette.Window); + + if (state > 0) + { + RECT const outside = { offset, offset, offset + box, offset + box }; + canvas.FillRect(outside, m_Palette.CheckBoxBorder); + canvas.FillRect(Inset(outside, border), m_Palette.CheckBoxFill); + + if (state == 2) + canvas.Blend(EWin32GUIGlyph::Cross, offset, offset, box, m_Palette.CheckMark); + else if (state == 3) + canvas.Blend(EWin32GUIGlyph::Square, offset, offset, box, m_Palette.CheckMark); + } + + return canvas.CreateBitmap(); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::DarkCheckImages + + In the order TVS_EX_PARTIALCHECKBOXES numbers them: none, unchecked, checked (a cross), partial (a square). +*/ +HIMAGELIST TWin32GUITheme::DarkCheckImages(UINT dpi) +{ + for (std::pair const& entry : m_DarkCheckImages) + { + if (entry.first == dpi) + return entry.second; + } + + int const cell = ScaleForDPI(16, dpi); + HIMAGELIST const images = ImageList_Create(cell, cell, ILC_COLOR32, 4, 0); + + if (images == nullptr) + return nullptr; + + for (int state = 0; state < 4; ++state) + { + HBITMAP const bitmap = CreateDarkCheckImage(state, dpi); + + if (bitmap != nullptr) + { + ImageList_Add(images, bitmap, nullptr); + DeleteObject(bitmap); + } + } + + m_DarkCheckImages.emplace_back(dpi, images); + return images; +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::DrawButton + + As Carbon draws a push button: an outline, a border, and a face shaded from top to bottom, its border lighter + under the mouse, and with a line inside it for the default button or the focused one, which is how the + focus shows, as in the VCL GUI runner. +*/ +void TWin32GUITheme::DrawButton(NMCUSTOMDRAW const& draw) const +{ + HWND const button = draw.hdr.hwndFrom; + HDC const context = draw.hdc; + UINT const dpi = WindowDPI(button); + int const line = (std::max)(1, ScaleForDPI(1, dpi)); + + bool const disabled = (draw.uItemState & CDIS_DISABLED) != 0; + bool const hot = (draw.uItemState & CDIS_HOT) != 0; + bool const pressed = (draw.uItemState & CDIS_SELECTED) != 0; + bool const emphasized = !disabled && ((draw.uItemState & (CDIS_DEFAULT | CDIS_FOCUS)) != 0 || + (GetWindowLongPtrW(button, GWL_STYLE) & BS_TYPEMASK) == BS_DEFPUSHBUTTON); + + RECT const outline = draw.rc; + RECT const border = Inset(outline, line); + RECT const face = Inset(border, line); + + FillSolid(context, outline, disabled ? m_Palette.ButtonDisabledOutline : m_Palette.ButtonOutline); + FillSolid(context, border, disabled ? m_Palette.ButtonDisabledBorder : hot || emphasized ? + m_Palette.ButtonHotBorder : m_Palette.ButtonBorder); + + if (disabled) + FillGradient(context, face, m_Palette.ButtonDisabledFaceTop, m_Palette.ButtonDisabledFaceBottom); + else if (pressed) + FillGradient(context, face, m_Palette.ButtonPressedFaceTop, m_Palette.ButtonPressedFaceBottom); + else if (hot) + FillGradient(context, face, m_Palette.ButtonHotFaceTop, m_Palette.ButtonHotFaceBottom); + else + FillGradient(context, face, m_Palette.ButtonFaceTop, m_Palette.ButtonFaceBottom); + + if (emphasized) + FrameSolid(context, face, m_Palette.ButtonDefaultFrame); + + std::wstring const text = WideWindowText(button); + RECT textRect = face; + int const oldMode = SetBkMode(context, TRANSPARENT); + COLORREF const oldColor = SetTextColor(context, disabled ? m_Palette.DisabledText : hot || pressed ? + m_Palette.ButtonHotText : m_Palette.ButtonText); + DrawTextW(context, text.c_str(), -1, &textRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE | + (HidesAccelerators(button) ? DT_HIDEPREFIX : 0)); + SetTextColor(context, oldColor); + SetBkMode(context, oldMode); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::DrawCheckButton + + As Carbon draws a radio button or a check box: its 13-pixel circle or box at the left, middle, and its text + after it, with a focus rectangle around the text when it has the focus and the keyboard is being used. +*/ +void TWin32GUITheme::DrawCheckButton(NMCUSTOMDRAW const& draw, bool radio) const +{ + HWND const button = draw.hdr.hwndFrom; + HDC const context = draw.hdc; + UINT const dpi = WindowDPI(button); + RECT const rect = draw.rc; + + bool const disabled = (draw.uItemState & CDIS_DISABLED) != 0; + bool const hot = (draw.uItemState & CDIS_HOT) != 0 || (draw.uItemState & CDIS_SELECTED) != 0; + LRESULT const check = SendMessageW(button, BM_GETCHECK, 0, 0); + + FillSolid(context, rect, m_Palette.Panel); + + int const box = ScaleForDPI(13, dpi); + int const top = (rect.top + rect.bottom - box) / 2; + COLORREF const borderColor = hot && !disabled ? m_Palette.CheckBoxHotBorder : m_Palette.CheckBoxBorder; + COLORREF const markColor = disabled ? m_Palette.CheckMarkDisabled : m_Palette.CheckMark; + TGlyphCanvas canvas(box, box, m_Palette.Panel); + + if (radio) + { + canvas.Blend(EWin32GUIGlyph::Circle, 0, 0, box, borderColor); + canvas.Blend(EWin32GUIGlyph::InnerCircle, 0, 0, box, m_Palette.CheckBoxFill); + + if (check == BST_CHECKED) + canvas.Blend(EWin32GUIGlyph::Dot, 0, 0, box, markColor); + } + else + { + RECT const outside = { 0, 0, box, box }; + canvas.FillRect(outside, borderColor); + canvas.FillRect(Inset(outside, (std::max)(1, ScaleForDPI(1, dpi))), m_Palette.CheckBoxFill); + + if (check == BST_CHECKED) + canvas.Blend(EWin32GUIGlyph::Cross, 0, 0, box, markColor); + else if (check == BST_INDETERMINATE) + canvas.Blend(EWin32GUIGlyph::Square, 0, 0, box, markColor); + } + + canvas.Draw(context, rect.left, top); + + std::wstring const text = WideWindowText(button); + UINT const format = DT_LEFT | DT_VCENTER | DT_SINGLELINE | (HidesAccelerators(button) ? DT_HIDEPREFIX : 0); + RECT textRect = { rect.left + box + ScaleForDPI(4, dpi), rect.top, rect.right, rect.bottom }; + int const oldMode = SetBkMode(context, TRANSPARENT); + COLORREF const oldColor = SetTextColor(context, disabled ? m_Palette.DisabledText : m_Palette.ControlText); + DrawTextW(context, text.c_str(), -1, &textRect, format); + SetTextColor(context, oldColor); + SetBkMode(context, oldMode); + + if ((draw.uItemState & CDIS_FOCUS) != 0 && !HidesFocus(button)) + { + SIZE const size = TextSize(context, text, format & ~static_cast(DT_VCENTER)); + RECT focus = { textRect.left - 1, (rect.top + rect.bottom - size.cy) / 2 - 1, textRect.left + size.cx + 1, + (rect.top + rect.bottom + size.cy) / 2 + 1 }; + DrawFocusRect(context, &focus); + } +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::DrawListRow + + The row's background, its selection as the tree shows one, then each column's text, clipped with an + ellipsis, and a focus rectangle around the focused row while the list has the focus and the keyboard is + being used. +*/ +void TWin32GUITheme::DrawListRow(NMLVCUSTOMDRAW const& draw, COLORREF textColor) const +{ + HWND const list = draw.nmcd.hdr.hwndFrom; + HDC const context = draw.nmcd.hdc; + UINT const dpi = WindowDPI(list); + WPARAM const index = static_cast(draw.nmcd.dwItemSpec); + UINT const state = static_cast(SendMessageW(list, LVM_GETITEMSTATE, index, LVIS_SELECTED | LVIS_FOCUSED)); + + RECT row = {}; + row.left = LVIR_BOUNDS; + SendMessageW(list, LVM_GETITEMRECT, index, reinterpret_cast(&row)); + + if ((state & LVIS_SELECTED) != 0) + { + FillSolid(context, row, m_Palette.Selection); + FrameSolid(context, row, m_Palette.SelectionFrame); + } + else + { + FillSolid(context, row, m_Palette.Window); + } + + int const oldMode = SetBkMode(context, TRANSPARENT); + COLORREF const oldColor = SetTextColor(context, textColor); + HWND const header = reinterpret_cast(SendMessageW(list, LVM_GETHEADER, 0, 0)); + int const columns = header != nullptr ? static_cast(SendMessageW(header, HDM_GETITEMCOUNT, 0, 0)) : 1; + + for (int column = 0; column < columns; ++column) + { + RECT cell = {}; + + if (column == 0) + { + cell.left = LVIR_LABEL; + SendMessageW(list, LVM_GETITEMRECT, index, reinterpret_cast(&cell)); + } + else + { + cell.top = column; + cell.left = LVIR_BOUNDS; + SendMessageW(list, LVM_GETSUBITEMRECT, index, reinterpret_cast(&cell)); + } + + std::wstring text(1024, L'\0'); + LVITEMW item = {}; + item.iSubItem = column; + item.pszText = &text[0]; + item.cchTextMax = static_cast(text.size()); + text.resize(static_cast(SendMessageW(list, LVM_GETITEMTEXTW, index, + reinterpret_cast(&item)))); + + // As the list's own text is placed: 2 pixels into the first column's label, 6 into the others. + cell.left += ScaleForDPI(column == 0 ? 2 : 6, dpi); + cell.right -= ScaleForDPI(6, dpi); + DrawTextW(context, text.c_str(), static_cast(text.size()), &cell, DT_LEFT | DT_VCENTER | DT_SINGLELINE | + DT_END_ELLIPSIS | DT_NOPREFIX); + } + + SetTextColor(context, oldColor); + SetBkMode(context, oldMode); + + if ((state & LVIS_FOCUSED) != 0 && GetFocus() == list && !HidesFocus(list)) + { + RECT focus = Inset(row, 1); + DrawFocusRect(context, &focus); + } +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::DrawMenuItem + + As Carbon draws the menus: the item under the mouse or the keyboard in the highlight, with a frame, unless + it's disabled; a separator as a line; a menu item's caption after a gutter for its check mark (a tick, or a + dot for a radio item), its shortcut at the right, and a submenu's arrow. The & letters are underlined only + once the keyboard has been used (ODS_NOACCEL). Windows draws its own submenu arrow after WM_DRAWITEM, + unless the item is clipped out of the context. +*/ +void TWin32GUITheme::DrawMenuItem(DRAWITEMSTRUCT const& draw) +{ + TMenuItem const* const item = FindMenuItem(draw.itemData); + + if (item == nullptr) + return; + + HDC const context = draw.hDC; + RECT const rect = draw.rcItem; + UINT const dpi = m_MenuBarWindow != nullptr ? WindowDPI(m_MenuBarWindow) : USER_DEFAULT_SCREEN_DPI; + bool const disabled = (draw.itemState & (ODS_DISABLED | ODS_GRAYED)) != 0; + // As Carbon: a disabled item under the mouse or the keyboard looks as it does otherwise. + bool const selected = !disabled && (draw.itemState & (ODS_SELECTED | ODS_HOTLIGHT)) != 0; + COLORREF const background = selected ? m_Palette.Highlight : item->TopLevel ? m_Palette.Panel : + m_Palette.MenuBackground; + + FillSolid(context, rect, background); + + if (selected) + FrameSolid(context, rect, m_Palette.HighlightFrame); + + if (item->Separator) + { + int const middle = (rect.top + rect.bottom) / 2; + RECT const separator = { rect.left + ScaleForDPI(4, dpi), middle, rect.right - ScaleForDPI(4, dpi), middle + + (std::max)(1, ScaleForDPI(1, dpi)) }; + FillSolid(context, separator, m_Palette.MenuSeparator); + return; + } + + COLORREF const textColor = disabled ? (item->TopLevel ? m_Palette.MenuBarDisabledText : + m_Palette.MenuDisabledText) : selected ? m_Palette.HotText : m_Palette.ControlText; + UINT const format = DT_SINGLELINE | DT_VCENTER | ((draw.itemState & ODS_NOACCEL) != 0 ? DT_HIDEPREFIX : 0); + std::wstring caption; + std::wstring shortcut; + SplitShortcut(item->Text, caption, shortcut); + + HGDIOBJ const oldFont = SelectObject(context, MenuFont(dpi)); + int const oldMode = SetBkMode(context, TRANSPARENT); + COLORREF const oldColor = SetTextColor(context, textColor); + + if (item->TopLevel) + { + RECT textRect = rect; + DrawTextW(context, caption.c_str(), -1, &textRect, format | DT_CENTER); + } + else + { + int const gutter = ScaleForDPI(28, dpi); + int const arrowWidth = ScaleForDPI(22, dpi); + RECT textRect = { rect.left + gutter, rect.top, rect.right - arrowWidth, rect.bottom }; + DrawTextW(context, caption.c_str(), -1, &textRect, format | DT_LEFT); + + if (!shortcut.empty()) + DrawTextW(context, shortcut.c_str(), -1, &textRect, format | DT_RIGHT); + + int const glyph = ScaleForDPI(16, dpi); + int const glyphTop = (rect.top + rect.bottom - glyph) / 2; + + if ((draw.itemState & ODS_CHECKED) != 0) + { + MENUITEMINFOW info = {}; + info.cbSize = sizeof(info); + info.fMask = MIIM_FTYPE; + GetMenuItemInfoW(item->Menu, item->Position, TRUE, &info); + + TGlyphCanvas canvas(glyph, glyph, background); + canvas.Blend((info.fType & MFT_RADIOCHECK) != 0 ? EWin32GUIGlyph::Dot : EWin32GUIGlyph::Tick, 0, 0, glyph, + textColor); + canvas.Draw(context, rect.left + (gutter - glyph) / 2, glyphTop); + } + + if (item->Submenu) + { + TGlyphCanvas canvas(glyph, glyph, background); + canvas.Blend(EWin32GUIGlyph::ArrowRight, 0, 0, glyph, textColor); + canvas.Draw(context, rect.right - arrowWidth + (arrowWidth - glyph) / 2, glyphTop); + ExcludeClipRect(context, rect.left, rect.top, rect.right, rect.bottom); + } + } + + SetTextColor(context, oldColor); + SetBkMode(context, oldMode); + SelectObject(context, oldFont); +} +//--------------------------------------------------------------------------- +COLORREF TWin32GUITheme::FailureTextColor() const +{ + return m_Dark ? m_Palette.FailureText : FailureTextColorOn(GetSysColor(COLOR_WINDOW)); +} +//--------------------------------------------------------------------------- +TWin32GUITheme::TMenuItem const* TWin32GUITheme::FindMenuItem(ULONG_PTR itemData) const +{ + for (std::unique_ptr const& item : m_MenuItems) + { + if (reinterpret_cast(item.get()) == itemData) + return item.get(); + } + + return nullptr; +} +//--------------------------------------------------------------------------- +LRESULT TWin32GUITheme::HandleControlMessage(HWND control, EWin32GUIThemedControl kind, UINT message, WPARAM wParam, + LPARAM lParam) +{ + bool const painted = kind == EWin32GUIThemedControl::ProgressBar || kind == EWin32GUIThemedControl::StatusBar || + kind == EWin32GUIThemedControl::TabControl || kind == EWin32GUIThemedControl::UpDown; + + bool const mayCreateArrows = message == WM_SIZE || (message == WM_PARENTNOTIFY && LOWORD(wParam) == WM_CREATE); + + if (kind == EWin32GUIThemedControl::TabControl && mayCreateArrows) + { + // Its scroll arrows, which it creates when it's too narrow for its tabs. + LRESULT const result = DefSubclassProc(control, message, wParam, lParam); + AttachUpDown(control); + + if (m_Dark && message == WM_SIZE) + InvalidateRect(control, nullptr, FALSE); + + return result; + } + + bool const doubleBuffered = kind == EWin32GUIThemedControl::ListView || kind == EWin32GUIThemedControl::TreeView; + + if (doubleBuffered && message == WM_WINDOWPOSCHANGING && + (reinterpret_cast(lParam)->flags & SWP_NOSIZE) == 0) + { + SetPropW(control, ResizingProperty, reinterpret_cast(static_cast(1))); + } + else if (doubleBuffered && (message == WM_WINDOWPOSCHANGED || message == WM_PAINT)) + { + RemovePropW(control, ResizingProperty); + } + else if (doubleBuffered && message == WM_ERASEBKGND && GetPropW(control, ResizingProperty) != nullptr) + { + // As the control grows, e.g. by a splitter, Windows erases the part it newly shows straight away, and the + // control paints it later. Being double-buffered, the tree and the list skip the erase, so that part + // showed what the screen had there before, e.g. the scroll bar at its old place, until they painted it. + // Filled with their background here, and only while resizing, so their other repaints don't flicker. + RECT client; + GetClientRect(control, &client); + FillSolid(reinterpret_cast(wParam), client, BackgroundColor(control, kind)); + return 1; + } + + if (!m_Dark) + { + // The control paints itself, through a buffer too, so it doesn't flicker while the window is resized: + // erasing and painting it straight onto the screen would show its background in between. Not the status + // bar, whose WM_PRINTCLIENT leaves out its background and text. + bool const buffered = kind == EWin32GUIThemedControl::ProgressBar || kind == EWin32GUIThemedControl::TabControl; + + if (buffered && message == WM_ERASEBKGND) + return 1; + + if (buffered && message == WM_PAINT) + { + PAINTSTRUCT paint = {}; + HDC const target = BeginPaint(control, &paint); + + { + TPaintBuffer const buffer(target, paint.rcPaint); + DefSubclassProc(control, WM_PRINTCLIENT, reinterpret_cast(buffer.Get()), PRF_CLIENT | + PRF_ERASEBKGND); + } + + EndPaint(control, &paint); + return 0; + } + + return DefSubclassProc(control, message, wParam, lParam); + } + + bool const bordered = kind == EWin32GUIThemedControl::Edit || kind == EWin32GUIThemedControl::ListView || + kind == EWin32GUIThemedControl::RichEdit || kind == EWin32GUIThemedControl::TreeView; + + if (bordered && message == WM_NCMOUSEMOVE && IsOverDisabledScrollBar(control, wParam)) + { + // Not passed on: over a scroll bar, Windows fades its arrows as the mouse comes and goes, painting them + // from a timer of its own, after PaintScrollBars() has, so a disabled bar's would end up light. A + // disabled bar does nothing with the mouse anyway. + return 0; + } + + if (bordered && MayRedrawScrollBars(message)) + { + LRESULT const result = DefSubclassProc(control, message, wParam, lParam); + PaintScrollBars(control, nullptr); + return result; + } + + switch (message) + { + case WM_ERASEBKGND: + if (kind == EWin32GUIThemedControl::Splitter) + { + RECT client; + GetClientRect(control, &client); + FillSolid(reinterpret_cast(wParam), client, m_Palette.Panel); + return 1; + } + + if (painted) + return 1; // WM_PAINT paints it all. + + break; + + case WM_MOUSELEAVE: + case WM_MOUSEMOVE: + if (kind == EWin32GUIThemedControl::TabControl) + { + // The tab under the mouse is drawn lighter. + TCHITTESTINFO hit = {}; + + if (message == WM_MOUSEMOVE) + { + hit.pt.x = static_cast(LOWORD(lParam)); + hit.pt.y = static_cast(HIWORD(lParam)); + } + else + { + hit.pt.x = -1; + hit.pt.y = -1; + } + + int const hot = static_cast(SendMessageW(control, TCM_HITTEST, 0, reinterpret_cast(&hit))) + + 1; + + if (reinterpret_cast(GetPropW(control, HotTabProperty)) != hot) + { + SetPropW(control, HotTabProperty, reinterpret_cast(static_cast(hot))); + InvalidateRect(control, nullptr, FALSE); + } + + if (message == WM_MOUSEMOVE) + { + TRACKMOUSEEVENT track = {}; + track.cbSize = sizeof(track); + track.dwFlags = TME_LEAVE; + track.hwndTrack = control; + TrackMouseEvent(&track); + } + } + + if (kind == EWin32GUIThemedControl::UpDown) + { + // The arrow under the mouse is drawn lighter. + InvalidateRect(control, nullptr, FALSE); + + if (message == WM_MOUSEMOVE) + { + TRACKMOUSEEVENT track = {}; + track.cbSize = sizeof(track); + track.dwFlags = TME_LEAVE; + track.hwndTrack = control; + TrackMouseEvent(&track); + } + } + + break; + + case WM_CAPTURECHANGED: + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + if (kind == EWin32GUIThemedControl::UpDown) + { + // Pressed, or not any more. + LRESULT const result = DefSubclassProc(control, message, wParam, lParam); + InvalidateRect(control, nullptr, FALSE); + return result; + } + + break; + + case WM_NCCALCSIZE: + if (bordered && GetPropW(control, OwnBorderProperty) != nullptr) + { + // The border's width taken off the window's rectangle first, so the control puts its scroll bars + // and client area inside it, as with WS_EX_CLIENTEDGE. + RECT* const proposed = wParam != FALSE ? &reinterpret_cast(lParam)->rgrc[0] : + reinterpret_cast(lParam); + int const edge = BorderWidth(control); + InflateRect(proposed, -edge, -edge); + return DefSubclassProc(control, message, wParam, lParam); + } + + break; + + case WM_NCPAINT: + if (bordered) + { + PaintNonClient(control, reinterpret_cast(wParam)); + return 0; + } + + break; + + case WM_NOTIFY: + if (kind == EWin32GUIThemedControl::ListView) + { + NMHDR const* const header = reinterpret_cast(lParam); + + if (header->code == NM_CUSTOMDRAW && + header->hwndFrom == reinterpret_cast(SendMessageW(control, LVM_GETHEADER, 0, 0))) + { + return OnHeaderCustomDraw(*reinterpret_cast(lParam)); + } + } + + break; + + case WM_PAINT: + case WM_PRINTCLIENT: + if (painted) + { + PAINTSTRUCT paint = {}; + HDC const target = message == WM_PAINT ? BeginPaint(control, &paint) : reinterpret_cast(wParam); + RECT client; + GetClientRect(control, &client); + + { + TPaintBuffer const buffer(target, client); + + if (kind == EWin32GUIThemedControl::ProgressBar) + PaintProgressBar(control, buffer.Get()); + else if (kind == EWin32GUIThemedControl::StatusBar) + PaintStatusBar(control, buffer.Get()); + else if (kind == EWin32GUIThemedControl::TabControl) + PaintTabControl(control, buffer.Get()); + else + PaintUpDown(control, buffer.Get()); + } + + if (message == WM_PAINT) + EndPaint(control, &paint); + + return 0; + } + + break; + + case WM_SIZE: + if (painted) + InvalidateRect(control, nullptr, FALSE); // e.g. the status bar's size grip, at its right. + + break; + + case PBM_DELTAPOS: + case PBM_SETPOS: + case PBM_SETRANGE: + case PBM_SETRANGE32: + case PBM_SETSTATE: + case PBM_STEPIT: + if (kind == EWin32GUIThemedControl::ProgressBar) + { + // The themed bar animates toward a new position by itself; this one shows it at once. Not on the + // themed bar's WM_TIMER, which only steps that animation, drawing nothing while dark. + LRESULT const result = DefSubclassProc(control, message, wParam, lParam); + InvalidateRect(control, nullptr, FALSE); + return result; + } + + break; + + default: + break; + } + + return DefSubclassProc(control, message, wParam, lParam); +} +//--------------------------------------------------------------------------- +LRESULT TWin32GUITheme::HandleWindowMessage(HWND window, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { + case WM_CTLCOLORBTN: + case WM_CTLCOLORDLG: + case WM_CTLCOLOREDIT: + case WM_CTLCOLORSTATIC: + { + LRESULT result = 0; + + if (OnCtlColor(message, reinterpret_cast(wParam), reinterpret_cast(lParam), result)) + return result; + + break; + } + + case WM_DRAWITEM: + { + DRAWITEMSTRUCT const* const draw = reinterpret_cast(lParam); + + if (draw->CtlType == ODT_MENU && FindMenuItem(draw->itemData) != nullptr) + { + DrawMenuItem(*draw); + return TRUE; + } + + break; + } + + case WM_ERASEBKGND: + if (m_Dark) + { + // Only between the children, which a window with WS_CLIPCHILDREN leaves out of the context. + RECT client; + GetClientRect(window, &client); + FillSolid(reinterpret_cast(wParam), client, m_Palette.Panel); + return 1; + } + + break; + + case WM_MEASUREITEM: + { + MEASUREITEMSTRUCT* const measure = reinterpret_cast(lParam); + + if (measure->CtlType == ODT_MENU && FindMenuItem(measure->itemData) != nullptr) + { + MeasureMenuItem(window, *measure); + return TRUE; + } + + break; + } + + case WM_NCACTIVATE: + case WM_NCPAINT: + if (m_Dark && window == m_MenuBarWindow) + { + LRESULT const result = DefSubclassProc(window, message, wParam, lParam); + PaintMenuBarLine(window); + return result; + } + + break; + + case WM_NOTIFY: + if (m_Dark) + { + NMHDR const* const header = reinterpret_cast(lParam); + + if (header->code == NM_CUSTOMDRAW && ClassNameIs(header->hwndFrom, WC_BUTTONW)) + return OnButtonCustomDraw(*reinterpret_cast(lParam)); + + if (header->code == NM_CUSTOMDRAW && ClassNameIs(header->hwndFrom, WC_TREEVIEWW)) + return OnTreeCustomDraw(*reinterpret_cast(lParam)); + } + + break; + + default: + break; + } + + return DefSubclassProc(window, message, wParam, lParam); +} +//--------------------------------------------------------------------------- +bool TWin32GUITheme::IsDark() const +{ + return m_Dark; +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::MeasureMenuItem + + Carbon's sizes: a menu item 21 pixels high at 96 DPI and a separator 9, with room for the check mark + gutter, the caption, the shortcut and the submenu arrow; a menu bar item its caption plus 6 pixels each + side, as tall as the menu bar. Windows adds the width of a check mark to a menu bar item's, which is taken + off here. +*/ +void TWin32GUITheme::MeasureMenuItem(HWND window, MEASUREITEMSTRUCT& measure) +{ + TMenuItem const* const item = FindMenuItem(measure.itemData); + UINT const dpi = WindowDPI(window); + + if (item->Separator) + { + measure.itemWidth = static_cast(ScaleForDPI(10, dpi)); + measure.itemHeight = static_cast(ScaleForDPI(9, dpi)); + return; + } + + std::wstring caption; + std::wstring shortcut; + SplitShortcut(item->Text, caption, shortcut); + + HDC const context = GetDC(window); + HGDIOBJ const oldFont = SelectObject(context, MenuFont(dpi)); + SIZE const captionSize = TextSize(context, caption, 0); + SIZE const shortcutSize = shortcut.empty() ? SIZE { 0, 0 } : TextSize(context, shortcut, 0); + SelectObject(context, oldFont); + ReleaseDC(window, context); + + if (item->TopLevel) + { + int const captionWidth = static_cast(captionSize.cx); + int const added = GetSystemMetricsForDpi(SM_CXMENUCHECK, dpi) - 1; + measure.itemWidth = static_cast((std::max)(captionWidth, captionWidth + ScaleForDPI(12, dpi) - added)); + measure.itemHeight = static_cast(GetSystemMetricsForDpi(SM_CYMENU, dpi)); + } + else + { + measure.itemWidth = static_cast(ScaleForDPI(28, dpi) + captionSize.cx + (shortcut.empty() ? 0 : + ScaleForDPI(32, dpi) + shortcutSize.cx) + ScaleForDPI(22, dpi)); + measure.itemHeight = static_cast(ScaleForDPI(21, dpi)); + } +} +//--------------------------------------------------------------------------- +HFONT TWin32GUITheme::MenuFont(UINT dpi) +{ + if (m_MenuFont.Get() == nullptr || m_MenuFontDPI != dpi) + { + NONCLIENTMETRICSW metrics = {}; + metrics.cbSize = sizeof(metrics); + + if (SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, sizeof(metrics), &metrics, 0, dpi) != 0) + { + m_MenuFont = TWin32GUIFont(CreateFontIndirectW(&metrics.lfMenuFont)); + m_MenuFontDPI = dpi; + } + } + + return m_MenuFont.Get() != nullptr ? m_MenuFont.Get() : static_cast(GetStockObject(DEFAULT_GUI_FONT)); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::MenuFrameProc + + A menu window's subclass while it exists: its frame (its non-client area) painted in Carbon's colors while + dark, also into the context WM_PRINT gives it for the menu's fade animation. +*/ +LRESULT CALLBACK TWin32GUITheme::MenuFrameProc(HWND menu, UINT message, WPARAM wParam, LPARAM lParam, UINT_PTR id, + DWORD_PTR theme) +{ + try + { + TWin32GUITheme const* const owner = reinterpret_cast(theme); + + switch (message) + { + case WM_NCDESTROY: + RemoveWindowSubclass(menu, &MenuFrameProc, id); + break; + + case WM_NCPAINT: + if (owner->m_Dark) + { + owner->PaintMenuFrame(menu, nullptr); + return 0; + } + + break; + + case WM_PRINT: + if (owner->m_Dark && (lParam & PRF_NONCLIENT) != 0) + { + LRESULT const result = DefSubclassProc(menu, message, wParam, lParam); + owner->PaintMenuFrame(menu, reinterpret_cast(wParam)); + return result; + } + + break; + + default: + break; + } + } + catch (...) + { + } + + return DefSubclassProc(menu, message, wParam, lParam); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::MenuHookProc + + This thread's WH_CALLWNDPROC hook: subclasses each menu window as it's created (see MenuFrameProc()). +*/ +LRESULT CALLBACK TWin32GUITheme::MenuHookProc(int code, WPARAM wParam, LPARAM lParam) +{ + TWin32GUITheme* const theme = s_Instance; + + if (code == HC_ACTION && theme != nullptr) + { + CWPSTRUCT const* const call = reinterpret_cast(lParam); + + if (call->message == WM_CREATE && ClassNameIs(call->hwnd, MenuClassName)) + SetWindowSubclass(call->hwnd, &MenuFrameProc, MenuFrameSubclassID, reinterpret_cast(theme)); + } + + return CallNextHookEx(theme != nullptr ? theme->m_MenuHook : nullptr, code, wParam, lParam); +} +//--------------------------------------------------------------------------- +LRESULT TWin32GUITheme::OnButtonCustomDraw(NMCUSTOMDRAW const& draw) const +{ + if (draw.dwDrawStage != CDDS_PREPAINT) + return CDRF_DODEFAULT; + + switch (GetWindowLongPtrW(draw.hdr.hwndFrom, GWL_STYLE) & BS_TYPEMASK) + { + case BS_DEFPUSHBUTTON: + case BS_PUSHBUTTON: + DrawButton(draw); + return CDRF_SKIPDEFAULT; + + case BS_AUTORADIOBUTTON: + case BS_RADIOBUTTON: + DrawCheckButton(draw, true); + return CDRF_SKIPDEFAULT; + + case BS_3STATE: + case BS_AUTO3STATE: + case BS_AUTOCHECKBOX: + case BS_CHECKBOX: + DrawCheckButton(draw, false); + return CDRF_SKIPDEFAULT; + + default: + return CDRF_DODEFAULT; + } +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::OnCtlColor + + A read-only edit box (WM_CTLCOLORSTATIC) is drawn like an editable one, in both looks, as the VCL GUI + runner's are, rather than gray. While dark, everything else gets the palette's colors; while light, the + system's. +*/ +bool TWin32GUITheme::OnCtlColor(UINT message, HDC context, HWND control, LRESULT& result) const +{ + bool const edit = ClassNameIs(control, WC_EDITW); + bool const enabled = IsWindowEnabled(control) != FALSE; + + if (message == WM_CTLCOLORSTATIC && edit && enabled) + { + SetTextColor(context, WindowTextColor()); + SetBkColor(context, WindowColor()); + result = reinterpret_cast(m_Dark ? m_WindowBrush : GetSysColorBrush(COLOR_WINDOW)); + return true; + } + + if (!m_Dark) + return false; + + switch (message) + { + case WM_CTLCOLOREDIT: + SetTextColor(context, m_Palette.WindowText); + SetBkColor(context, m_Palette.Window); + result = reinterpret_cast(m_WindowBrush); + return true; + + case WM_CTLCOLORSTATIC: + if (edit) + { + // A disabled edit box, e.g. the filter box during a run. + SetTextColor(context, m_Palette.DisabledText); + SetBkColor(context, m_Palette.Window); + result = reinterpret_cast(m_WindowBrush); + return true; + } + + SetTextColor(context, enabled ? m_Palette.ControlText : m_Palette.DisabledText); + SetBkColor(context, m_Palette.Panel); + result = reinterpret_cast(m_PanelBrush); + return true; + + case WM_CTLCOLORBTN: + case WM_CTLCOLORDLG: + result = reinterpret_cast(m_PanelBrush); + return true; + + default: + return false; + } +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::OnHeaderCustomDraw + + As Carbon draws a header: each column shaded from top to bottom (lighter under the mouse), with a line at + its right and along the bottom, and the part right of the last column the same. +*/ +LRESULT TWin32GUITheme::OnHeaderCustomDraw(NMCUSTOMDRAW const& draw) const +{ + HWND const header = draw.hdr.hwndFrom; + HDC const context = draw.hdc; + + if (draw.dwDrawStage == CDDS_PREPAINT) + return CDRF_NOTIFYITEMDRAW | CDRF_NOTIFYPOSTPAINT; + + if (draw.dwDrawStage == CDDS_ITEMPREPAINT) + { + RECT rect = draw.rc; + + if ((draw.uItemState & CDIS_HOT) != 0) + FillSolid(context, rect, m_Palette.HeaderHot); + else + FillGradient(context, rect, m_Palette.HeaderTop, m_Palette.HeaderBottom); + + RECT const divider = { rect.right - 1, rect.top, rect.right, rect.bottom }; + RECT const bottom = { rect.left, rect.bottom - 1, rect.right, rect.bottom }; + FillSolid(context, divider, m_Palette.HeaderDivider); + FillSolid(context, bottom, m_Palette.HeaderDivider); + + wchar_t text[256] = {}; + HDITEMW item = {}; + item.mask = HDI_TEXT; + item.pszText = text; + item.cchTextMax = 256; + SendMessageW(header, HDM_GETITEMW, draw.dwItemSpec, reinterpret_cast(&item)); + + rect.left += ScaleForDPI(6, WindowDPI(header)); + int const oldMode = SetBkMode(context, TRANSPARENT); + COLORREF const oldColor = SetTextColor(context, m_Palette.ControlText); + DrawTextW(context, text, -1, &rect, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOPREFIX); + SetTextColor(context, oldColor); + SetBkMode(context, oldMode); + return CDRF_SKIPDEFAULT; + } + + if (draw.dwDrawStage == CDDS_POSTPAINT) + { + int const count = static_cast(SendMessageW(header, HDM_GETITEMCOUNT, 0, 0)); + RECT last = {}; + + if (count > 0) + SendMessageW(header, HDM_GETITEMRECT, static_cast(count - 1), reinterpret_cast(&last)); + + RECT rest; + GetClientRect(header, &rest); + rest.left = last.right; + + if (rest.left < rest.right) + { + FillGradient(context, rest, m_Palette.HeaderTop, m_Palette.HeaderBottom); + RECT const bottom = { rest.left, rest.bottom - 1, rest.right, rest.bottom }; + FillSolid(context, bottom, m_Palette.HeaderDivider); + } + } + + return CDRF_DODEFAULT; +} +//--------------------------------------------------------------------------- +LRESULT TWin32GUITheme::OnListCustomDraw(NMLVCUSTOMDRAW& draw, COLORREF rowTextColor) const +{ + if (draw.nmcd.dwDrawStage == CDDS_PREPAINT) + return CDRF_NOTIFYITEMDRAW; + + if (draw.nmcd.dwDrawStage != CDDS_ITEMPREPAINT) + return CDRF_DODEFAULT; + + if (m_Dark) + { + DrawListRow(draw, rowTextColor); + return CDRF_SKIPDEFAULT; + } + + draw.clrText = rowTextColor; + return CDRF_DODEFAULT; +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::OnTreeCustomDraw + + The selected node as the VCL GUI runner shows it under Carbon: white text in a gray box with a darker frame, + whether the tree has the focus or not. Without the selected, hot and focus states, the theme draws neither + its own selection nor its highlight under the mouse. +*/ +LRESULT TWin32GUITheme::OnTreeCustomDraw(NMTVCUSTOMDRAW& draw) const +{ + if (draw.nmcd.dwDrawStage == CDDS_PREPAINT) + return CDRF_NOTIFYITEMDRAW; + + HTREEITEM const item = reinterpret_cast(draw.nmcd.dwItemSpec); + bool const selected = SendMessageW(draw.nmcd.hdr.hwndFrom, TVM_GETNEXTITEM, TVGN_CARET, 0) == + static_cast(draw.nmcd.dwItemSpec); + + if (draw.nmcd.dwDrawStage == CDDS_ITEMPREPAINT) + { + draw.nmcd.uItemState &= ~static_cast(CDIS_SELECTED | CDIS_HOT | CDIS_FOCUS); + draw.clrText = selected ? m_Palette.SelectionText : m_Palette.WindowText; + draw.clrTextBk = selected ? m_Palette.Selection : m_Palette.Window; + + return selected ? CDRF_NOTIFYPOSTPAINT : CDRF_DODEFAULT; + } + + if (draw.nmcd.dwDrawStage == CDDS_ITEMPOSTPAINT && selected) + { + // TVM_GETITEMRECT takes the item in the rectangle and gives back its text's bounds (wParam TRUE). + RECT text = {}; + std::memcpy(&text, &item, sizeof(item)); + + if (SendMessageW(draw.nmcd.hdr.hwndFrom, TVM_GETITEMRECT, TRUE, reinterpret_cast(&text)) != 0) + FrameSolid(draw.nmcd.hdc, text, m_Palette.SelectionFrame); + } + + return CDRF_DODEFAULT; +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::PaintBorder + + The border a bordered control has of its own while dark (see ApplyToBorder()), in Carbon's two edit border + colors, the outer one a pixel wide; or, on one that still has WS_EX_CLIENTEDGE, that border painted over. + Into 'context', in the window's coordinates. +*/ +void TWin32GUITheme::PaintBorder(HWND control, HDC context) const +{ + if (!HasThemedBorder(control)) + return; + + RECT rect; + GetWindowRect(control, &rect); + OffsetRect(&rect, -rect.left, -rect.top); + + int const edge = BorderWidth(control); + FrameSolid(context, rect, m_Palette.EditBorderOuter); + + for (int i = 1; i < edge; ++i) + FrameSolid(context, Inset(rect, i), m_Palette.EditBorderInner); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::PaintMenuBarLine + + The line DefWindowProc() draws under the menu bar, in a light system color, painted over in the + window's background. +*/ +void TWin32GUITheme::PaintMenuBarLine(HWND window) const +{ + MENUBARINFO bar = {}; + bar.cbSize = sizeof(bar); + + if (GetMenuBarInfo(window, OBJID_MENU, 0, &bar) == 0) + return; + + RECT windowRect; + GetWindowRect(window, &windowRect); + RECT line = bar.rcBar; + OffsetRect(&line, -windowRect.left, -windowRect.top); + line.top = line.bottom; + line.bottom = line.top + 1; + + HDC const context = GetWindowDC(window); + + if (context == nullptr) + return; + + FillSolid(context, line, m_Palette.Panel); + ReleaseDC(window, context); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::PaintMenuFrame + + A menu window's non-client area, its frame, as Carbon's: two one-pixel lines, then the menu's background. + Into 'given' if it isn't nullptr (WM_PRINT's context), or else the window's own. +*/ +void TWin32GUITheme::PaintMenuFrame(HWND menu, HDC given) const +{ + RECT window; + GetWindowRect(menu, &window); + RECT client; + GetClientRect(menu, &client); + MapWindowPoints(menu, nullptr, reinterpret_cast(&client), 2); + OffsetRect(&client, -window.left, -window.top); + OffsetRect(&window, -window.left, -window.top); + + HDC const context = given != nullptr ? given : GetWindowDC(menu); + + if (context == nullptr) + return; + + int const saved = SaveDC(context); + ExcludeClipRect(context, client.left, client.top, client.right, client.bottom); + FillSolid(context, window, m_Palette.MenuBackground); + FrameSolid(context, window, m_Palette.MenuBorderOuter); + FrameSolid(context, Inset(window, 1), m_Palette.MenuBorderInner); + RestoreDC(context, saved); + + if (given == nullptr) + ReleaseDC(menu, context); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::PaintNonClient + + Each part painted once: the control (and Windows) paint 'update', WM_NCPAINT's region, less what + PaintBorder() and PaintScrollBars() paint, which they then paint. Letting the control paint it all first, + as before, showed its light parts in between, e.g. a disabled scroll bar's arrows and a light border without + the scroll bars, which flickered while the control was resized, e.g. by a splitter. +*/ +void TWin32GUITheme::PaintNonClient(HWND control, HRGN update) const +{ + RECT window; + GetWindowRect(control, &window); + + // In screen coordinates, as WM_NCPAINT's region is: inside the border. + RECT const inside = HasThemedBorder(control) ? Inset(window, BorderWidth(control)) : window; + HRGN const rest = CreateRectRgnIndirect(&inside); + + if (rest != nullptr) + { + // 1 is the whole window. + if (update != reinterpret_cast(1)) + CombineRgn(rest, rest, update, RGN_AND); + + SCROLLBARINFO bars[2] = {}; + LONG const ids[2] = { OBJID_VSCROLL, OBJID_HSCROLL }; + bool visible[2] = { false, false }; + + for (int i = 0; i < 2; ++i) + { + bars[i].cbSize = sizeof(bars[i]); + visible[i] = GetScrollBarInfo(control, ids[i], &bars[i]) != 0 && + (bars[i].rgstate[0] & (STATE_SYSTEM_INVISIBLE | STATE_SYSTEM_OFFSCREEN)) == 0; + + if (visible[i] && (bars[i].rgstate[0] & STATE_SYSTEM_UNAVAILABLE) != 0) + ExcludeRect(rest, bars[i].rcScrollBar); + } + + if (visible[0] && visible[1]) + { + RECT const corner = { bars[0].rcScrollBar.left, bars[1].rcScrollBar.top, bars[0].rcScrollBar.right, + bars[1].rcScrollBar.bottom }; + ExcludeRect(rest, corner); + } + + DefSubclassProc(control, WM_NCPAINT, reinterpret_cast(rest), 0); + DeleteObject(rest); + } + + HDC const context = GetWindowDC(control); + + if (context == nullptr) + return; + + PaintScrollBars(control, context); + PaintBorder(control, context); + ReleaseDC(control, context); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::PaintProgressBar + + As Carbon draws it: a border around a dark track, and the bar shaded from top to bottom, red once a test + has failed (PBST_ERROR). +*/ +void TWin32GUITheme::PaintProgressBar(HWND progressBar, HDC context) const +{ + RECT client; + GetClientRect(progressBar, &client); + int const line = (std::max)(1, ScaleForDPI(1, WindowDPI(progressBar))); + + FillSolid(context, client, m_Palette.ProgressBorder); + RECT const track = Inset(client, line); + FillSolid(context, track, m_Palette.ProgressTrack); + + PBRANGE range = {}; + SendMessageW(progressBar, PBM_GETRANGE, TRUE, reinterpret_cast(&range)); + int const position = static_cast(SendMessageW(progressBar, PBM_GETPOS, 0, 0)); + int const span = range.iHigh - range.iLow; + + if (span <= 0) + return; + + RECT bar = track; + int const done = (std::max)(0, (std::min)(span, position - range.iLow)); + bar.right = track.left + MulDiv(track.right - track.left, done, span); + + if (bar.right <= bar.left) + return; + + bool const failed = SendMessageW(progressBar, PBM_GETSTATE, 0, 0) == PBST_ERROR; + FillGradient(context, bar, failed ? m_Palette.ProgressFailedTop : m_Palette.ProgressChunkTop, + failed ? m_Palette.ProgressFailedBottom : m_Palette.ProgressChunkBottom); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::PaintScrollBars + + What the dark scroll bar theme ("DarkMode_Explorer") leaves light, painted over: + - the corner between a vertical and a horizontal scroll bar, which Windows paints in the system colors + whatever the theme (a light edge around a gray square), painted as the theme's track; + - a disabled scroll bar's arrows, which the theme draws nearly white, e.g. the empty detail pane's, which + keeps its scroll bar while there's nothing to scroll. Such a bar is drawn again in the theme's normal + state, which has no thumb when there's nothing to scroll, with its arrows faded halfway toward the track, + so they read as disabled. + An enabled bar is left as it is. Called after each message that may draw the bars (see + MayRedrawScrollBars()), so it's quick when there's nothing to paint. Into 'given' if it isn't nullptr + (PaintNonClient()'s), in the window's coordinates, or else the window's own context. +*/ +void TWin32GUITheme::PaintScrollBars(HWND control, HDC given) const +{ + SCROLLBARINFO bars[2] = {}; + LONG const ids[2] = { OBJID_VSCROLL, OBJID_HSCROLL }; + bool visible[2] = { false, false }; + + for (int i = 0; i < 2; ++i) + { + bars[i].cbSize = sizeof(bars[i]); + visible[i] = GetScrollBarInfo(control, ids[i], &bars[i]) != 0 && + (bars[i].rgstate[0] & (STATE_SYSTEM_INVISIBLE | STATE_SYSTEM_OFFSCREEN)) == 0; + } + + bool const corner = visible[0] && visible[1]; + bool const disabled[2] = { + visible[0] && (bars[0].rgstate[0] & STATE_SYSTEM_UNAVAILABLE) != 0, + visible[1] && (bars[1].rgstate[0] & STATE_SYSTEM_UNAVAILABLE) != 0, + }; + + if (!corner && !disabled[0] && !disabled[1]) + return; + + HDC const context = given != nullptr ? given : GetWindowDC(control); + HTHEME const theme = OpenThemeData(control, L"ScrollBar"); + RECT window; + GetWindowRect(control, &window); + + if (context != nullptr && theme != nullptr) + { + if (corner) + { + // Below the vertical bar and right of the horizontal one. + RECT box = { bars[0].rcScrollBar.left, bars[1].rcScrollBar.top, bars[0].rcScrollBar.right, + bars[1].rcScrollBar.bottom }; + OffsetRect(&box, -window.left, -window.top); + DrawThemeBackground(theme, context, SBP_LOWERTRACKVERT, SCRBS_NORMAL, &box, nullptr); + } + + for (int i = 0; i < 2; ++i) + { + if (!disabled[i]) + continue; + + bool const vertical = ids[i] == OBJID_VSCROLL; + RECT bar = bars[i].rcScrollBar; + OffsetRect(&bar, -window.left, -window.top); + + // The arrow buttons at its two ends, each as long as a line button. + RECT first = bar; + RECT second = bar; + + if (vertical) + { + first.bottom = first.top + bars[i].dxyLineButton; + second.top = second.bottom - bars[i].dxyLineButton; + } + else + { + first.right = first.left + bars[i].dxyLineButton; + second.left = second.right - bars[i].dxyLineButton; + } + + TPaintBuffer const buffer(context, bar); + HDC const target = buffer.Get(); + DrawThemeBackground(theme, target, vertical ? SBP_UPPERTRACKVERT : SBP_UPPERTRACKHORZ, SCRBS_NORMAL, &bar, + nullptr); + DrawThemeBackground(theme, target, SBP_ARROWBTN, vertical ? ABS_UPNORMAL : ABS_LEFTNORMAL, &first, + nullptr); + DrawThemeBackground(theme, target, SBP_ARROWBTN, vertical ? ABS_DOWNNORMAL : ABS_RIGHTNORMAL, &second, + nullptr); + + for (RECT const& button : { first, second }) + { + TGlyphCanvas canvas(button.right - button.left, button.bottom - button.top, m_Palette.Window); + canvas.CopyFrom(target, button.left, button.top); + // Toward its corner's pixel, the button's background. + canvas.Fade(canvas.PixelAt(0, 0), 1, 2); + canvas.Draw(target, button.left, button.top); + } + } + } + + if (theme != nullptr) + CloseThemeData(theme); + + if (context != nullptr && given == nullptr) + ReleaseDC(control, context); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::PaintStatusBar + + As Carbon draws it: a line along the top, each part's text, and the size grip's dots at the bottom right + (unless its window is maximized), where Carbon's 16-pixel grip has them. +*/ +void TWin32GUITheme::PaintStatusBar(HWND statusBar, HDC context) const +{ + RECT client; + GetClientRect(statusBar, &client); + UINT const dpi = WindowDPI(statusBar); + int const line = (std::max)(1, ScaleForDPI(1, dpi)); + + FillSolid(context, client, m_Palette.Panel); + RECT const top = { client.left, client.top, client.right, client.top + line }; + FillSolid(context, top, m_Palette.StatusLine); + + HGDIOBJ const oldFont = SelectObject(context, reinterpret_cast(SendMessageW(statusBar, WM_GETFONT, 0, 0))); + int const oldMode = SetBkMode(context, TRANSPARENT); + COLORREF const oldColor = SetTextColor(context, m_Palette.ControlText); + int const parts = static_cast(SendMessageW(statusBar, SB_GETPARTS, 0, 0)); + + for (int part = 0; part < parts; ++part) + { + RECT rect = {}; + SendMessageW(statusBar, SB_GETRECT, static_cast(part), reinterpret_cast(&rect)); + + std::wstring text(static_cast(LOWORD(SendMessageW(statusBar, SB_GETTEXTLENGTHW, + static_cast(part), 0))) + 1, L'\0'); + text.resize(static_cast(LOWORD(SendMessageW(statusBar, SB_GETTEXTW, static_cast(part), + reinterpret_cast(&text[0]))))); + + rect.left += ScaleForDPI(4, dpi); + DrawTextW(context, text.c_str(), static_cast(text.size()), &rect, DT_LEFT | DT_VCENTER | DT_SINGLELINE | + DT_END_ELLIPSIS | DT_NOPREFIX); + } + + SetTextColor(context, oldColor); + SetBkMode(context, oldMode); + SelectObject(context, oldFont); + + HWND const parent = GetParent(statusBar); + bool const maximized = parent != nullptr && IsZoomed(parent) != FALSE; + + if ((GetWindowLongPtrW(statusBar, GWL_STYLE) & SBARS_SIZEGRIP) == 0 || maximized) + return; + + // Carbon's dots, at 96 DPI in its 16-pixel grip: a triangle of four rows, two pixels apart. + int const cell = ScaleForDPI(16, dpi); + int const left = client.right - cell; + int const gripTop = client.bottom - cell; + + for (int row = 0; row < 4; ++row) + { + for (int column = 0; column <= row; ++column) + { + int const x = left + ScaleForDPI(10 - 2 * column, dpi); + int const y = gripTop + ScaleForDPI(7 + 2 * row, dpi); + RECT const dot = { x, y, x + line, y + line }; + FillSolid(context, dot, m_Palette.GripDot); + } + } +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::PaintTabControl + + As Carbon draws the results' tabs: the strip in the window's background, the pages framed by two lines, + the selected tab shaded and joined to them, the others darker (lighter under the mouse), the selected tab's + text brighter. The pages themselves are separate windows above the tab control. +*/ +void TWin32GUITheme::PaintTabControl(HWND tabControl, HDC context) const +{ + RECT client; + GetClientRect(tabControl, &client); + UINT const dpi = WindowDPI(tabControl); + int const line = (std::max)(1, ScaleForDPI(1, dpi)); + + FillSolid(context, client, m_Palette.Panel); + + RECT pages = client; + SendMessageW(tabControl, TCM_ADJUSTRECT, FALSE, reinterpret_cast(&pages)); + pages = Inset(pages, -2 * line); + FillSolid(context, pages, m_Palette.TabOuter); + FillSolid(context, Inset(pages, line), m_Palette.TabBorder); + FillSolid(context, Inset(pages, 2 * line), m_Palette.Panel); + + int const count = static_cast(SendMessageW(tabControl, TCM_GETITEMCOUNT, 0, 0)); + int const selected = static_cast(SendMessageW(tabControl, TCM_GETCURSEL, 0, 0)); + int const hot = static_cast(reinterpret_cast(GetPropW(tabControl, HotTabProperty))) - 1; + bool const showFocus = GetFocus() == tabControl && !HidesFocus(tabControl); + UINT const format = DT_CENTER | DT_VCENTER | DT_SINGLELINE | (HidesAccelerators(tabControl) ? DT_HIDEPREFIX : 0); + + HGDIOBJ const oldFont = SelectObject(context, reinterpret_cast(SendMessageW(tabControl, WM_GETFONT, 0, + 0))); + int const oldMode = SetBkMode(context, TRANSPARENT); + + for (int i = 0; i < count; ++i) + { + RECT tab = {}; + SendMessageW(tabControl, TCM_GETITEMRECT, static_cast(i), reinterpret_cast(&tab)); + + if (i == selected) + { + // Raised and widened, and down over the pages' frame, which it opens into. + tab.left -= 2 * line; + tab.right += 2 * line; + tab.top -= 2 * line; + tab.bottom += 2 * line; + FillSolid(context, tab, m_Palette.TabOuter); + RECT const inside = { tab.left + line, tab.top + line, tab.right - line, tab.bottom }; + FillSolid(context, inside, m_Palette.TabBorder); + RECT const face = { inside.left + line, inside.top + line, inside.right - line, inside.bottom }; + FillGradient(context, face, m_Palette.TabActiveTop, m_Palette.TabActiveBottom); + } + else + { + FillSolid(context, tab, m_Palette.TabInactiveBorder); + FillSolid(context, Inset(tab, line), i == hot ? m_Palette.TabHot : m_Palette.TabInactive); + } + + wchar_t text[128] = {}; + TCITEMW item = {}; + item.mask = TCIF_TEXT; + item.pszText = text; + item.cchTextMax = 128; + SendMessageW(tabControl, TCM_GETITEMW, static_cast(i), reinterpret_cast(&item)); + + RECT textRect = tab; + + if (i == selected) + textRect.bottom -= 2 * line; + + COLORREF const oldColor = SetTextColor(context, i == selected ? m_Palette.WindowText : m_Palette.ControlText); + DrawTextW(context, text, -1, &textRect, format); + SetTextColor(context, oldColor); + + if (i == selected && showFocus) + { + RECT focus = Inset(textRect, 3 * line); + DrawFocusRect(context, &focus); + } + } + + SetBkMode(context, oldMode); + SelectObject(context, oldFont); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::PaintUpDown + + As Carbon draws a pair of horizontal spin buttons, e.g. the results tabs' scroll arrows: each an outline + around a face shaded from top to bottom, lighter under the mouse, with a darker edge at its top and left + while pressed, and an arrow pointing left (decrease) or right (increase). +*/ +void TWin32GUITheme::PaintUpDown(HWND upDown, HDC context) const +{ + RECT client; + GetClientRect(upDown, &client); + UINT const dpi = WindowDPI(upDown); + int const line = (std::max)(1, ScaleForDPI(1, dpi)); + bool const disabled = IsWindowEnabled(upDown) == FALSE; + + POINT cursor = {}; + GetCursorPos(&cursor); + bool const underMouse = WindowFromPoint(cursor) == upDown; + ScreenToClient(upDown, &cursor); + bool const pressing = GetCapture() == upDown && (GetKeyState(VK_LBUTTON) & 0x8000) != 0; + + int const middle = (client.left + client.right) / 2; + RECT const halves[] = { + { client.left, client.top, middle, client.bottom }, + { middle, client.top, client.right, client.bottom }, + }; + + for (int i = 0; i < 2; ++i) + { + RECT const& half = halves[i]; + bool const hot = !disabled && underMouse && PtInRect(&half, cursor) != FALSE; + + FillSolid(context, half, disabled ? m_Palette.SpinDisabledOutline : m_Palette.SpinOutline); + RECT const face = Inset(half, line); + + if (disabled) + FillGradient(context, face, m_Palette.SpinDisabledFaceTop, m_Palette.SpinDisabledFaceBottom); + else if (hot) + FillGradient(context, face, m_Palette.SpinHotFaceTop, m_Palette.SpinHotFaceBottom); + else + FillGradient(context, face, m_Palette.SpinFaceTop, m_Palette.SpinFaceBottom); + + if (hot && pressing) + { + RECT const top = { face.left, face.top, face.right, face.top + line }; + RECT const left = { face.left, face.top, face.left + line, face.bottom }; + FillSolid(context, top, m_Palette.SpinPressedEdge); + FillSolid(context, left, m_Palette.SpinPressedEdge); + } + + int const size = (std::min)(face.right - face.left, face.bottom - face.top); + + if (size <= 0) + continue; + + int const left = face.left + (face.right - face.left - size) / 2; + int const top = face.top + (face.bottom - face.top - size) / 2; + TGlyphCanvas canvas(size, size, m_Palette.SpinFaceTop); + canvas.CopyFrom(context, left, top); + canvas.Blend(i == 0 ? EWin32GUIGlyph::ArrowLeft : EWin32GUIGlyph::ArrowRight, 0, 0, size, disabled ? + m_Palette.SpinDisabledArrow : m_Palette.SpinArrow); + canvas.Draw(context, left, top); + } +} +//--------------------------------------------------------------------------- +TWin32GUIPalette const& TWin32GUITheme::Palette() const +{ + return m_Palette; +} +//--------------------------------------------------------------------------- +void TWin32GUITheme::Refresh() +{ + ApplyToMenus(); + + for (HWND const window : m_Windows) + ApplyToTitleBar(window); + + // By index, on a copy: applying the look can resize a tab control, which may then create and attach its + // scroll arrows. + for (std::size_t i = 0; i < m_Controls.size(); ++i) + { + TAttachedControl const attached = m_Controls[i]; + ApplyToControl(attached); + } + + for (HWND const window : m_Windows) + RedrawWindow(window, nullptr, nullptr, RDW_ERASE | RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::RefreshMenu + + Only the items of 'menu' and its submenus are made again, and the old ones forgotten, so the items of the + menu bar and of the other menus keep theirs. +*/ +void TWin32GUITheme::RefreshMenu(HMENU menu) +{ + if (m_MenuBar == nullptr || menu == nullptr) + return; + + std::vector > items; + ApplyToMenu(menu, items); + + // The menus just walked: 'menu', and those of its submenus' new items. + std::vector walked = { menu }; + + for (std::unique_ptr const& item : items) + walked.push_back(item->Menu); + + m_MenuItems.erase(std::remove_if(m_MenuItems.begin(), m_MenuItems.end(), + [&walked](std::unique_ptr const& item) + { + return std::find(walked.begin(), walked.end(), item->Menu) != walked.end(); + }), m_MenuItems.end()); + + for (std::unique_ptr& item : items) + m_MenuItems.push_back(std::move(item)); +} +//--------------------------------------------------------------------------- +void TWin32GUITheme::SetDark(bool dark) +{ + if (dark == m_Dark) + return; + + m_Dark = dark; + Refresh(); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::SetDarkTheme + + A built-in theme's name comes before a file's, so a theme file of the same name isn't read. A file that + can't be read leaves Carbon's colors, as does one ReadGUIThemeFile() finds unusable. +*/ +std::vector TWin32GUITheme::SetDarkTheme(std::string const& name) +{ + std::vector problems; + TWin32GUIPalette palette = CarbonPalette(); + TWin32GUIBuiltInTheme const* const builtIn = FindBuiltInDarkTheme(name); + + if (builtIn != nullptr) + { + palette = builtIn->Palette; + } + else + { + try + { + std::string const path = GUIThemesFolderPath() + "\\" + name + ".ini"; + + if (!FileExists(path)) + { + problems.push_back(GUIThemeProblemLine(name, "There's no theme file " + path + ", so " + + GUIDefaultDarkThemeName() + " is used instead.")); + } + else + { + TGUIThemeFile const file = ReadGUIThemeFile(ReadFileBytes(path), PaletteRoleNames()); + + for (std::string const& problem : file.Problems) + problems.push_back(GUIThemeProblemLine(name, problem)); + + palette = PaletteWithThemeColors(palette, file.Colors); + } + } + catch (std::exception const& exception) + { + problems.push_back(GUIThemeProblemLine(name, "Its theme file couldn't be read, so " + + GUIDefaultDarkThemeName() + " is used instead: " + exception.what())); + } + } + + SetPalette(palette); + return problems; +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::SetPalette + + The brushes are made again in the new colors, and the old ones deleted once the menus no longer use them. + The dark check boxes are drawn again into their image lists, which the trees keep. +*/ +void TWin32GUITheme::SetPalette(TWin32GUIPalette const& palette) +{ + m_Palette = palette; + + HBRUSH const oldBrushes[] = { m_MenuBrush, m_PanelBrush, m_WindowBrush }; + m_MenuBrush = CreateSolidBrush(m_Palette.MenuBackground); + m_PanelBrush = CreateSolidBrush(m_Palette.Panel); + m_WindowBrush = CreateSolidBrush(m_Palette.Window); + + for (std::pair const& entry : m_DarkCheckImages) + { + for (int state = 0; state < 4; ++state) + { + HBITMAP const bitmap = CreateDarkCheckImage(state, entry.first); + + if (bitmap != nullptr) + { + ImageList_Replace(entry.second, state, bitmap, nullptr); + DeleteObject(bitmap); + } + } + } + + if (m_Dark) + Refresh(); + + for (HBRUSH const brush : oldBrushes) + { + if (brush != nullptr) + DeleteObject(brush); + } +} +//--------------------------------------------------------------------------- +COLORREF TWin32GUITheme::SkipTextColor() const +{ + return m_Dark ? m_Palette.SkipText : SkipTextColorOn(GetSysColor(COLOR_WINDOW)); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::Unattach + + A tree's own check box images, kept while it was dark, are destroyed with it, as a tree with check boxes + leaves them to its owner. +*/ +void TWin32GUITheme::Unattach(HWND window) +{ + m_Controls.erase(std::remove_if(m_Controls.begin(), m_Controls.end(), [window](TAttachedControl const& attached) + { + return attached.Control == window; + }), m_Controls.end()); + m_Windows.erase(std::remove(m_Windows.begin(), m_Windows.end(), window), m_Windows.end()); + + for (std::vector >::iterator themed = m_ThemedCheckImages.begin(); + themed != m_ThemedCheckImages.end();) + { + if (themed->first == window) + { + ImageList_Destroy(themed->second); + themed = m_ThemedCheckImages.erase(themed); + } + else + { + ++themed; + } + } + + if (window == m_MenuBarWindow) + { + // The menu bar is destroyed with its window. + m_MenuBar = nullptr; + m_MenuBarWindow = nullptr; + m_MenuItems.clear(); + } +} +//--------------------------------------------------------------------------- +COLORREF TWin32GUITheme::WindowColor() const +{ + return m_Dark ? m_Palette.Window : GetSysColor(COLOR_WINDOW); +} +//--------------------------------------------------------------------------- +/* + TWin32GUITheme::WindowProc + + An attached window's subclass. No exception may leave a window procedure; one that would leaves the + message to the window. +*/ +LRESULT CALLBACK TWin32GUITheme::WindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam, UINT_PTR id, + DWORD_PTR theme) +{ + try + { + if (message == WM_NCDESTROY) + { + reinterpret_cast(theme)->Unattach(window); + RemoveWindowSubclass(window, &WindowProc, id); + return DefSubclassProc(window, message, wParam, lParam); + } + + return reinterpret_cast(theme)->HandleWindowMessage(window, message, wParam, lParam); + } + catch (...) + { + return DefSubclassProc(window, message, wParam, lParam); + } +} +//--------------------------------------------------------------------------- +COLORREF TWin32GUITheme::WindowTextColor() const +{ + return m_Dark ? m_Palette.WindowText : GetSysColor(COLOR_WINDOWTEXT); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Theme.h b/win32/gui/src/ASWUnitTests_Win32GUI_Theme.h new file mode 100644 index 0000000..ca97a5c --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Theme.h @@ -0,0 +1,257 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_Theme.h +Author: Anthony S. West - ASW Software + +The Win32 GUI runner's light and dark looks: Windows' standard one, and a +dark one painted in Carbon's colors (the VCL GUI runner's dark style), +drawing what Windows can't make dark itself. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_Win32GUI_ThemeH +#define ASWUnitTests_Win32GUI_ThemeH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_DPI.h" +#include "ASWUnitTests_Win32GUI_Palette.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// EWin32GUIThemedControl +// +// The kinds of child control TWin32GUITheme::Attach() themes, each its own +// way. Buttons, radio buttons, check boxes and labels need no attaching: +// their window does it (see TWin32GUITheme::AttachWindow()). +///////////////////////////////////////////////////////////////////////////// +enum class EWin32GUIThemedControl +{ + // An EDIT control, single-line or multiline, read-only or not. + Edit, + // A WC_LISTVIEWW control in report view, with its header. + ListView, + // A PROGRESS_CLASSW control. + ProgressBar, + // An MSFTEDIT_CLASS rich edit control. Its text keeps the colors it's given (see TWin32GUITheme's + // WindowTextColor()). + RichEdit, + // A window that only fills itself with its parent's background, e.g. a splitter. + Splitter, + // A STATUSCLASSNAMEW control. + StatusBar, + // A WC_TABCONTROLW control, whose pages are separate windows above it. + TabControl, + // A TOOLTIPS_CLASSW window. + Tooltip, + // A WC_TREEVIEWW control, with or without TVS_CHECKBOXES (and TVS_EX_PARTIALCHECKBOXES). + TreeView, + // An UPDOWN_CLASSW control with UDS_HORZ, such as a tab control's scroll arrows, which attaching the tab + // control attaches as it creates them. + UpDown, +}; + + +///////////////////////////////////////////////////////////////////////////// +// TWin32GUITheme +// +// Whether the GUI is light (Windows' standard look, in the system colors) or +// dark (painted in CarbonPalette()'s colors, or a theme file's; see +// SetDarkTheme()), applied to the windows and controls attached to it, and +// switched live by SetDark() and SetDarkTheme(). Dark has: +// - the title bar Windows draws dark (DWMWA_USE_IMMERSIVE_DARK_MODE); +// - owner-drawn menus, and the menus' frames painted through a hook on +// this thread's menu windows; +// - buttons, radio buttons and check boxes custom-drawn by their window; +// - the tab control, status bar and progress bar painted whole, through +// a buffer, so they don't flicker; +// - the tree's and list's items and the list's header drawn by us, and +// their scroll bars Windows' own dark ones ("DarkMode_Explorer"); +// - the edit boxes', tree's, list's and log's borders painted over. +// Make one per process, on the GUI's thread, before any window, and destroy +// it after the windows; dialogs attach themselves while they're open. +///////////////////////////////////////////////////////////////////////////// +class TWin32GUITheme +{ +private: + // An attached control and its kind. + struct TAttachedControl + { + HWND Control; + EWin32GUIThemedControl Kind; + }; + + // An item of an attached window's menu bar, or of one of its menus, while they're owner-drawn: the item's + // address is its MENUITEMINFOW::dwItemData. + struct TMenuItem + { + HMENU Menu; + UINT Position; + // Its caption, with any "\t" and shortcut. + std::wstring Text; + bool Separator; + bool Submenu; + bool TopLevel; + }; + +private: + // The theme whose hook paints the menus' frames; see MenuHookProc(). + static TWin32GUITheme* s_Instance; + +private: + std::vector m_Controls; + bool m_Dark; + // The dark tree check boxes, one image list per DPI they've been needed at, shared by the attached trees + // while dark. Kept until this object is destroyed, so a tree never shows a destroyed list. + std::vector > m_DarkCheckImages; + // The menu bar whose items, and its menus', are owner-drawn while dark, and the window it belongs to. + HMENU m_MenuBar; + HWND m_MenuBarWindow; + HBRUSH m_MenuBrush; + // The font for the menus' items, for m_MenuFontDPI. + TWin32GUIFont m_MenuFont; + UINT m_MenuFontDPI; + HHOOK m_MenuHook; + std::vector > m_MenuItems; + TWin32GUIPalette m_Palette; + HBRUSH m_PanelBrush; + // Each attached tree's own check box images, put back when it turns light. + std::vector > m_ThemedCheckImages; + HBRUSH m_WindowBrush; + // The windows attached by AttachWindow(). + std::vector m_Windows; + +private: + static LRESULT CALLBACK ControlProc(HWND control, UINT message, WPARAM wParam, LPARAM lParam, UINT_PTR id, + DWORD_PTR theme); + static LRESULT CALLBACK MenuFrameProc(HWND menu, UINT message, WPARAM wParam, LPARAM lParam, UINT_PTR id, + DWORD_PTR theme); + static LRESULT CALLBACK MenuHookProc(int code, WPARAM wParam, LPARAM lParam); + static LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam, UINT_PTR id, + DWORD_PTR theme); + +private: + // Gives a bordered control (an edit box, the tree, the list, the log) its own border while dark, in place + // of WS_EX_CLIENTEDGE's, or WS_EX_CLIENTEDGE back while light; see the definition. + void ApplyToBorder(HWND control) const; + // Applies the current look to one attached control. + void ApplyToControl(TAttachedControl const& attached); + // Makes 'menu''s items, and its submenus', owner-drawn while dark, adding each to 'items', or standard while + // light. + void ApplyToMenu(HMENU menu, std::vector >& items); + // Makes the menu bar's items and its menus' owner-drawn while dark, or standard while light. + void ApplyToMenus(); + void ApplyToTitleBar(HWND window) const; + void ApplyToTree(HWND tree); + // Attaches the scroll arrows a tab control creates when its tabs don't fit, if it has created them and they + // aren't attached yet. + void AttachUpDown(HWND tabControl); + // One of the dark tree check boxes' images, in the palette's colors, at 'dpi'; nullptr if it can't be made. + // 'state' is its index, as DarkCheckImages() numbers them. + HBITMAP CreateDarkCheckImage(int state, UINT dpi) const; + // The dark tree check boxes for 'dpi', created if needed. + HIMAGELIST DarkCheckImages(UINT dpi); + void DrawButton(NMCUSTOMDRAW const& draw) const; + void DrawCheckButton(NMCUSTOMDRAW const& draw, bool radio) const; + // OnListCustomDraw()'s item prepaint while dark. + void DrawListRow(NMLVCUSTOMDRAW const& draw, COLORREF textColor) const; + void DrawMenuItem(DRAWITEMSTRUCT const& draw); + // The owner-drawn menu item whose MENUITEMINFOW::dwItemData is 'itemData', or nullptr if it isn't one. + TMenuItem const* FindMenuItem(ULONG_PTR itemData) const; + LRESULT HandleControlMessage(HWND control, EWin32GUIThemedControl kind, UINT message, WPARAM wParam, + LPARAM lParam); + LRESULT HandleWindowMessage(HWND window, UINT message, WPARAM wParam, LPARAM lParam); + void MeasureMenuItem(HWND window, MEASUREITEMSTRUCT& measure); + // The font for the menus at 'dpi', created if needed. + HFONT MenuFont(UINT dpi); + // The custom draw of a button, radio button or check box, by its window; see AttachWindow(). + LRESULT OnButtonCustomDraw(NMCUSTOMDRAW const& draw) const; + // A WM_CTLCOLOR* message to an attached window; false leaves it to the window. + bool OnCtlColor(UINT message, HDC context, HWND control, LRESULT& result) const; + LRESULT OnHeaderCustomDraw(NMCUSTOMDRAW const& draw) const; + LRESULT OnTreeCustomDraw(NMTVCUSTOMDRAW& draw) const; + void PaintBorder(HWND control, HDC context) const; + void PaintMenuBarLine(HWND window) const; + void PaintMenuFrame(HWND menu, HDC given) const; + // A bordered control's WM_NCPAINT while dark, of 'update', its region: its scroll bars and border. + void PaintNonClient(HWND control, HRGN update) const; + void PaintProgressBar(HWND progressBar, HDC context) const; + // What the dark scroll bar theme leaves light on 'control''s scroll bars (see the definition). + void PaintScrollBars(HWND control, HDC given) const; + void PaintStatusBar(HWND statusBar, HDC context) const; + void PaintTabControl(HWND tabControl, HDC context) const; + void PaintUpDown(HWND upDown, HDC context) const; + // Forgets a control or window that's being destroyed. + void Unattach(HWND window); + +public: + // Light, with nothing attached. Hooks this thread's menu windows. + TWin32GUITheme(); + ~TWin32GUITheme(); + + TWin32GUITheme(TWin32GUITheme const&) = delete; + TWin32GUITheme& operator=(TWin32GUITheme const&) = delete; + + // Themes 'control', of 'kind', a child of an attached window, until it's destroyed. + void Attach(HWND control, EWin32GUIThemedControl kind); + // Themes 'window', a top-level window or dialog, until it's destroyed: its title bar and background, its + // labels, edit boxes' and read-only edit boxes' colors (WM_CTLCOLOR*), its buttons, radio buttons and check + // boxes (their NM_CUSTOMDRAW), and 'menuBar', its menu bar, if it has one. + void AttachWindow(HWND window, HMENU menuBar = nullptr); + // A failed test's text color on the window color (see WindowColor()). + COLORREF FailureTextColor() const; + bool IsDark() const; + // An attached list view's NM_CUSTOMDRAW, for its parent to return: each row drawn whole while dark, or in its + // default look while light, with its text in 'rowTextColor', which the item prepaint stage uses. + LRESULT OnListCustomDraw(NMLVCUSTOMDRAW& draw, COLORREF rowTextColor) const; + // The dark palette's colors. + TWin32GUIPalette const& Palette() const; + // Applies the current look again to everything attached, e.g. after a DPI change or a change of the system + // colors. + void Refresh(); + // Applies the current look to 'menu', one of the attached menu bar's menus, after its items were changed, + // e.g. in WM_INITMENUPOPUP, leaving the menu bar and the other menus, which may be open, as they are. + void RefreshMenu(HMENU menu); + // Switches to the dark look or back to the light one, and redraws everything attached. + void SetDark(bool dark); + // Makes 'name' the dark theme: one of BuiltInDarkThemes() (ignoring case), or a theme file in + // GUIThemesFolderPath(), ".ini", over Carbon's colors (see SetPalette()). Returns its problems as lines + // for the log (see GUIThemeProblemLine()): the file's (see ReadGUIThemeFile()), or that it's missing or can't + // be read, so Carbon is used. + std::vector SetDarkTheme(std::string const& name); + // Makes 'palette' the dark look's colors, redrawing everything attached while dark. + void SetPalette(TWin32GUIPalette const& palette); + // A skipped test's text color on the window color (see WindowColor()). + COLORREF SkipTextColor() const; + // The background of edit boxes, lists and the log. + COLORREF WindowColor() const; + // The text color of edit boxes, lists and the log. + COLORREF WindowTextColor() const; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef ASWUnitTests_Win32GUI_ThemeH diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_WinMain.cpp b/win32/gui/src/ASWUnitTests_Win32GUI_WinMain.cpp new file mode 100644 index 0000000..6043acd --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_WinMain.cpp @@ -0,0 +1,164 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_WinMain.cpp +Author: Anthony S. West - ASW Software + +Win32 GUI app that runs the ASWUnitTests framework's tests. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_CLI.h" +#include "ASWUnitTests_Utils.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_CommandLine.h" +#include "ASWUnitTests_GUI_Theme.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_MainWindow.h" +#include "ASWUnitTests_Win32GUI_System.h" +#include "ASWUnitTests_Win32GUI_TextDialog.h" +#include "ASWUnitTests_Win32GUI_Theme.h" +//--------------------------------------------------------------------------- + +#if defined(__MINGW32__) +// As in src/main.cpp: MinGW-w64's C runtime can expand wildcards in the command line into the names of matching +// files before WinMain() runs (WinLibs' builds do by default), and defining _dowildcard as 0 turns that off. The +// GUI reads its arguments from GetCommandLineW() rather than the runtime's argv, so they'd arrive as given anyway, +// but the expansion would still search the current folder at every start, and the self-tests check the setting. +extern "C" +{ +int _dowildcard = 0; +} +#endif + +//--------------------------------------------------------------------------- + +namespace +{ + +// The title of windows that have no other, as the VCL GUI runner's Application->Title. +char const ApplicationTitle[] = "ASWUnitTests"; + +//--------------------------------------------------------------------------- + +void ShowUnhandledException(ASWUnitTests::TWin32GUITheme& theme, std::string const& description); + +//--------------------------------------------------------------------------- +/* + ShowUnhandledException + + Reports an exception that escaped to WinMain(), in the GUI's theme. Called from its catch blocks, so it + throws nothing: if the text dialog can't be shown, a message box is. +*/ +void ShowUnhandledException(ASWUnitTests::TWin32GUITheme& theme, std::string const& description) +{ + try + { + std::string const text = "Unhandled exception: " + description; + + try + { + ASWUnitTests::ShowTextDialog(nullptr, theme, ApplicationTitle, text); + } + catch (...) + { + MessageBoxW(nullptr, ASWUnitTests::UTF8ToWide(text).c_str(), L"ASWUnitTests", MB_OK | MB_ICONERROR); + } + } + catch (...) + { + MessageBoxW(nullptr, L"Unhandled exception", L"ASWUnitTests", MB_OK | MB_ICONERROR); + } +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int showCommand) +{ + int exitCode = ASWUnitTests::ExitCode_Success; + // Before any window, and destroyed after them. + ASWUnitTests::TWin32GUITheme theme; + + try + { + INITCOMMONCONTROLSEX commonControls = {}; + commonControls.dwSize = sizeof(commonControls); + commonControls.dwICC = ICC_BAR_CLASSES | ICC_LISTVIEW_CLASSES | ICC_PROGRESS_CLASS | ICC_STANDARD_CLASSES | + ICC_TAB_CLASSES | ICC_TREEVIEW_CLASSES; + InitCommonControlsEx(&commonControls); + + // Parsed before the main window exists, so --help, --version, and argument errors are shown on their own + // and the process exits with the same code the console runner would use, without opening the GUI. + ASWUnitTests::TGUIOptions options; + ASWUnitTests::TGUICommandLineResult const commandLine = + ASWUnitTests::TGUICommandLine::Parse(ASWUnitTests::ProcessArguments(), options); + + // The saved theme and dark theme, before any window, so the --help dialog has them too: Use Windows + // Setting and Carbon with --layout-ignore, which ignores the saved layout file, and --layout-reset, which + // deletes it. + bool const savedTheme = !options.LayoutIgnore && !options.LayoutReset; + std::string const layoutPath = ASWUnitTests::GUILayoutFilePath(); + ASWUnitTests::EGUITheme const themeChoice = savedTheme ? ASWUnitTests::LoadGUIThemeFromLayoutFile(layoutPath) : + ASWUnitTests::EGUITheme::Windows; + std::string const darkTheme = savedTheme ? ASWUnitTests::LoadGUIDarkThemeFromLayoutFile(layoutPath) : + ASWUnitTests::GUIDefaultDarkThemeName(); + std::vector const themeProblems = theme.SetDarkTheme(darkTheme); + theme.SetDark(ASWUnitTests::GUIThemeIsDark(themeChoice, ASWUnitTests::WindowsAppsUseDarkTheme(), + ASWUnitTests::WindowsHighContrast())); + + if (commandLine.EarlyExitCode.has_value()) + { + ASWUnitTests::ShowTextDialog(nullptr, theme, ApplicationTitle, commandLine.Message); + return *commandLine.EarlyExitCode; + } + + ASWUnitTests::TWin32GUIMainWindow mainWindow(theme); + mainWindow.Create(); + mainWindow.Start(options, themeChoice, darkTheme, themeProblems); + mainWindow.Show(showCommand); + mainWindow.RunMessageLoop(); + + exitCode = mainWindow.ExitCode(); + + // After a timeout, the abandoned test's thread may still be running, using one of the window's test groups, + // and after a crash their state may be corrupted, so the process ends here, as the console runner's does, + // without destroying them. + if (mainWindow.WasRunAborted()) + ExitProcess(static_cast(exitCode)); + } + catch (std::exception const& exception) + { + ShowUnhandledException(theme, exception.what()); + exitCode = ASWUnitTests::ExitCode_UnhandledException; + } + catch (...) + { + ShowUnhandledException(theme, "Unknown"); + exitCode = ASWUnitTests::ExitCode_UnhandledExceptionUnknown; + } + + return exitCode; +} +//--------------------------------------------------------------------------- diff --git a/win32/gui/src/ASWUnitTests_Win32GUI_Windows.h b/win32/gui/src/ASWUnitTests_Win32GUI_Windows.h new file mode 100644 index 0000000..32de882 --- /dev/null +++ b/win32/gui/src/ASWUnitTests_Win32GUI_Windows.h @@ -0,0 +1,66 @@ +/* ************************************************************************** +ASWUnitTests_Win32GUI_Windows.h +Author: Anthony S. West - ASW Software + +The one place the Win32 GUI runner includes the Windows headers, after +setting the Windows version they target (Windows 10) and NOMINMAX. + +Include it first in every Win32 GUI source file, before any other header: +the MinGW-w64 C++ library headers set _WIN32_WINNT to an older version +(Windows 7) when it isn't already set, which would hide the Windows 10 +declarations the GUI uses. Including it later stops the build with an +error rather than failing in less obvious ways. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef ASWUnitTests_Win32GUI_WindowsH +#define ASWUnitTests_Win32GUI_WindowsH +//--------------------------------------------------------------------------- + +// Windows 10 (version 1703 or later at run time, for per-monitor v2 DPI awareness). +#if !defined(_WIN32_WINNT) +# define _WIN32_WINNT 0x0A00 +#elif _WIN32_WINNT < 0x0A00 +# error "_WIN32_WINNT is set below Windows 10: include ASWUnitTests_Win32GUI_Windows.h before any other header." +#endif + +#if !defined(WINVER) +# define WINVER 0x0A00 +#elif WINVER < 0x0A00 +# error "WINVER is set below Windows 10: include ASWUnitTests_Win32GUI_Windows.h before any other header." +#endif + +#if !defined(NOMINMAX) +# define NOMINMAX +#endif + +#if !defined(WIN32_LEAN_AND_MEAN) +# define WIN32_LEAN_AND_MEAN +#endif + +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- +#include +#include +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#endif diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_DPI.cpp b/win32/tests/Test_ASWUnitTests_Win32GUI_DPI.cpp new file mode 100644 index 0000000..4cb8b8e --- /dev/null +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_DPI.cpp @@ -0,0 +1,207 @@ +/* ************************************************************************** +Test_ASWUnitTests_Win32GUI_DPI.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +// Module header +#include "Test_ASWUnitTests_Win32GUI_DPI.h" +//--------------------------------------------------------------------------- +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_DPI.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +//--------------------------------------------------------------------------- + +LOGFONTW FontDescription(HFONT font); +bool IsFontHandle(HFONT font); +LOGFONTW MessageFont(UINT dpi); + +//--------------------------------------------------------------------------- +LOGFONTW FontDescription(HFONT font) +{ + LOGFONTW description = {}; + GetObjectW(font, sizeof(description), &description); + return description; +} +//--------------------------------------------------------------------------- +/* + IsFontHandle + + Whether 'font' is a font that hasn't been deleted. +*/ +bool IsFontHandle(HFONT font) +{ + return GetObjectType(font) == OBJ_FONT; +} +//--------------------------------------------------------------------------- +LOGFONTW MessageFont(UINT dpi) +{ + NONCLIENTMETRICSW metrics = {}; + metrics.cbSize = sizeof(metrics); + SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, sizeof(metrics), &metrics, 0, dpi); + return metrics.lfMessageFont; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_Win32GUI_DPI +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_Win32GUI_DPI::TTest_ASWUnitTests_Win32GUI_DPI() + : inherited("ASWUnitTests_Win32GUI_DPI_Tests") +{ + RegisterTest(&TTest_ASWUnitTests_Win32GUI_DPI::Test_CreateMonospaceFont_IsConsolas10PointForTheDPI, + "CreateMonospaceFont_IsConsolas10PointForTheDPI"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_DPI::Test_CreateUIFont_IsTheMessageFontForTheDPI, + "CreateUIFont_IsTheMessageFontForTheDPI"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_DPI::Test_ScaleForDPI_RoundsToTheNearestPixel, + "ScaleForDPI_RoundsToTheNearestPixel"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_DPI::Test_TWin32GUIFont_MoveTransfersOwnership, + "TWin32GUIFont_MoveTransfersOwnership"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_DPI::Test_WindowDPI_IsTheDefaultWithoutAWindow, + "WindowDPI_IsTheDefaultWithoutAWindow"); +} +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_Win32GUI_DPI::~TTest_ASWUnitTests_Win32GUI_DPI() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DPI::SetUp_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DPI::SetUp_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DPI::TearDown_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DPI::TearDown_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- + +// /////// Begin tests after this line /////////////////////// + +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DPI::Test_CreateMonospaceFont_IsConsolas10PointForTheDPI() +{ + // Act + TWin32GUIFont const at96 = CreateMonospaceFont(96); + TWin32GUIFont const at144 = CreateMonospaceFont(144); + TWin32GUIFont const at192 = CreateMonospaceFont(192); + + // Assert: 10 pt is 13.3 pixels at 96 DPI, 20 at 144 and 26.7 at 192; a negative height is the character + // height, without the internal leading. + CheckEquals(std::wstring(L"Consolas"), std::wstring(FontDescription(at96.Get()).lfFaceName), __func__, __LINE__, + "Consolas"); + CheckEquals(-13, FontDescription(at96.Get()).lfHeight, __func__, __LINE__, "at 96 DPI"); + CheckEquals(-20, FontDescription(at144.Get()).lfHeight, __func__, __LINE__, "at 144 DPI"); + CheckEquals(-27, FontDescription(at192.Get()).lfHeight, __func__, __LINE__, "at 192 DPI"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DPI::Test_CreateUIFont_IsTheMessageFontForTheDPI() +{ + // Arrange + LOGFONTW const expected96 = MessageFont(96); + LOGFONTW const expected192 = MessageFont(192); + + // Act + TWin32GUIFont const at96 = CreateUIFont(96); + TWin32GUIFont const at192 = CreateUIFont(192); + + // Assert + CheckEquals(std::wstring(expected96.lfFaceName), std::wstring(FontDescription(at96.Get()).lfFaceName), __func__, + __LINE__, "the message font's face"); + CheckEquals(expected96.lfHeight, FontDescription(at96.Get()).lfHeight, __func__, __LINE__, "its height at 96 DPI"); + CheckEquals(expected192.lfHeight, FontDescription(at192.Get()).lfHeight, __func__, __LINE__, + "its height at 192 DPI"); + CheckNotEquals(expected96.lfHeight, expected192.lfHeight, __func__, __LINE__, "which differ"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DPI::Test_ScaleForDPI_RoundsToTheNearestPixel() +{ + // Act, Assert + CheckEquals(100, ScaleForDPI(100, 96), __func__, __LINE__, "unchanged at 96 DPI"); + CheckEquals(150, ScaleForDPI(100, 144), __func__, __LINE__, "150% at 144 DPI"); + CheckEquals(9, ScaleForDPI(5, 168), __func__, __LINE__, "8.75 rounds up"); + CheckEquals(6, ScaleForDPI(5, 120), __func__, __LINE__, "6.25 rounds down"); + CheckEquals(3, ScaleForDPI(2, 120), __func__, __LINE__, "2.5 rounds away from zero"); + CheckEquals(-20, ScaleForDPI(-13, 144), __func__, __LINE__, "-19.5 rounds away from zero"); + CheckEquals(0, ScaleForDPI(0, 192), __func__, __LINE__, "zero stays zero"); +} +//--------------------------------------------------------------------------- +/* + Test_TWin32GUIFont_MoveTransfersOwnership + + Whether a font the owner deleted is gone can't be checked: GDI keeps recently deleted fonts' handles in a + cache, where GetObjectType() can still find them. +*/ +void TTest_ASWUnitTests_Win32GUI_DPI::Test_TWin32GUIFont_MoveTransfersOwnership() +{ + // Arrange + TWin32GUIFont source = CreateMonospaceFont(96); + HFONT const font = source.Get(); + TWin32GUIFont assigned; + + // Act + TWin32GUIFont moved(std::move(source)); + assigned = std::move(moved); + + // Assert + CheckTrue(assigned.Get() == font, __func__, __LINE__, "the last owner has the font"); + CheckNull(source.Get(), __func__, __LINE__, "the first owner has none"); + CheckNull(moved.Get(), __func__, __LINE__, "nor has the one it was moved through"); + CheckTrue(IsFontHandle(font), __func__, __LINE__, "and the font still exists"); + CheckNull(TWin32GUIFont().Get(), __func__, __LINE__, "an empty owner has no font"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DPI::Test_WindowDPI_IsTheDefaultWithoutAWindow() +{ + // Act, Assert + CheckEquals(96u, WindowDPI(nullptr), __func__, __LINE__, "96 DPI when there's no window to ask"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +ASW_REGISTER_TEST_GROUP(ASWUnitTests::TTest_ASWUnitTests_Win32GUI_DPI) diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_DPI.h b/win32/tests/Test_ASWUnitTests_Win32GUI_DPI.h new file mode 100644 index 0000000..ceebabf --- /dev/null +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_DPI.h @@ -0,0 +1,63 @@ +/* ************************************************************************** +Test_ASWUnitTests_Win32GUI_DPI.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef Test_ASWUnitTests_Win32GUI_DPIH +#define Test_ASWUnitTests_Win32GUI_DPIH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_Win32GUI_DPI +// +// Exercises the Win32 GUI runner's sizes and fonts for a DPI +// (win32/gui/src/ASWUnitTests_Win32GUI_DPI.h). Compiled by the Windows +// console runners as well as the Win32 GUI, so it runs without the GUI. +///////////////////////////////////////////////////////////////////////////// +class TTest_ASWUnitTests_Win32GUI_DPI : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: // Test methods + void Test_CreateMonospaceFont_IsConsolas10PointForTheDPI(); + void Test_CreateUIFont_IsTheMessageFontForTheDPI(); + void Test_ScaleForDPI_RoundsToTheNearestPixel(); + void Test_TWin32GUIFont_MoveTransfersOwnership(); + void Test_WindowDPI_IsTheDefaultWithoutAWindow(); + +public: + TTest_ASWUnitTests_Win32GUI_DPI(); + ~TTest_ASWUnitTests_Win32GUI_DPI() override; + + void SetUp_Group() override; + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override; + void TearDown_Test(ITestCase& testCase) override; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef Test_ASWUnitTests_Win32GUI_DPIH diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_DialogTemplate.cpp b/win32/tests/Test_ASWUnitTests_Win32GUI_DialogTemplate.cpp new file mode 100644 index 0000000..e506435 --- /dev/null +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_DialogTemplate.cpp @@ -0,0 +1,238 @@ +/* ************************************************************************** +Test_ASWUnitTests_Win32GUI_DialogTemplate.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +// Module header +#include "Test_ASWUnitTests_Win32GUI_DialogTemplate.h" +//--------------------------------------------------------------------------- +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +#include "ASWUnitTests_Utils.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_DialogTemplate.h" +#include "ASWUnitTests_Win32GUI_System.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +//--------------------------------------------------------------------------- + +std::string ClassName(HWND window); +INT_PTR CALLBACK IgnoringDialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM lParam); +std::string WordsText(std::vector const& words, std::size_t first, std::size_t count); + +//--------------------------------------------------------------------------- +std::string ClassName(HWND window) +{ + wchar_t name[64] = {}; + GetClassNameW(window, name, 64); + + return WideToUTF8(name); +} +//--------------------------------------------------------------------------- +/* + IgnoringDialogProc + + A dialog procedure that leaves every message to the dialog's defaults. +*/ +INT_PTR CALLBACK IgnoringDialogProc(HWND /*dialog*/, UINT /*message*/, WPARAM /*wParam*/, LPARAM /*lParam*/) +{ + return FALSE; +} +//--------------------------------------------------------------------------- +/* + WordsText + + 'count' of the words from 'first' as "w1,w2,...", in hex, to compare them with a single CheckEquals(). Stops + early at the end of 'words'. +*/ +std::string WordsText(std::vector const& words, std::size_t first, std::size_t count) +{ + static char const digits[] = "0123456789ABCDEF"; + std::string text; + + for (std::size_t i = first; i < first + count && i < words.size(); ++i) + { + if (!text.empty()) + text += ","; + + for (int shift = 12; shift >= 0; shift -= 4) + text += digits[(words[i] >> shift) & 0xF]; + } + + return text; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_Win32GUI_DialogTemplate +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_Win32GUI_DialogTemplate::TTest_ASWUnitTests_Win32GUI_DialogTemplate() + : inherited("ASWUnitTests_Win32GUI_DialogTemplate_Tests") +{ + RegisterTest(&TTest_ASWUnitTests_Win32GUI_DialogTemplate::Test_AddControl_AlignsEachControlOnADWORD, + "AddControl_AlignsEachControlOnADWORD"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_DialogTemplate::Test_AddControl_CountsTheControls, + "AddControl_CountsTheControls"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_DialogTemplate::Test_Constructor_WritesTheHeaderWithTheGUIFont, + "Constructor_WritesTheHeaderWithTheGUIFont"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_DialogTemplate::Test_Get_IsATemplateWindowsCreatesTheDialogFrom, + "Get_IsATemplateWindowsCreatesTheDialogFrom"); +} +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_Win32GUI_DialogTemplate::~TTest_ASWUnitTests_Win32GUI_DialogTemplate() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DialogTemplate::SetUp_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DialogTemplate::SetUp_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DialogTemplate::TearDown_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DialogTemplate::TearDown_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- + +// /////// Begin tests after this line /////////////////////// + +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DialogTemplate::Test_AddControl_AlignsEachControlOnADWORD() +{ + // Arrange: a three-letter caption leaves the header 25 words long, so the first control needs a padding word. + TWin32GUIDialogTemplate dialogTemplate(WS_POPUP, L"Abc"); + + // Act + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Button, 7, WS_CHILD | WS_VISIBLE, WS_EX_CLIENTEDGE, + L"OK"); + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Static, 8, WS_CHILD, 0, L"X"); + + // Assert + std::vector const& words = dialogTemplate.Words(); + CheckEquals(std::string("0000"), WordsText(words, 25, 1), __func__, __LINE__, "a padding word after the header"); + CheckEquals(std::string("0000,5000,0200,0000,0000,0000,000A,000A,0007,FFFF,0080,004F,004B,0000,0000"), + WordsText(words, 26, 15), __func__, __LINE__, + "the button: style, extended style, placeholder bounds, ID, class, text, no creation data"); + CheckEquals(std::string("0000"), WordsText(words, 41, 1), __func__, __LINE__, "a padding word after the button"); + CheckEquals(std::string("0000,4000,0000,0000,0000,0000,000A,000A,0008,FFFF,0082,0058,0000,0000"), + WordsText(words, 42, 14), __func__, __LINE__, "the static control, from the next DWORD"); + CheckEquals(56, static_cast(words.size()), __func__, __LINE__, "nothing after it"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DialogTemplate::Test_AddControl_CountsTheControls() +{ + // Arrange + TWin32GUIDialogTemplate dialogTemplate(WS_POPUP, L"A"); + + // Act, Assert + CheckEquals(0, static_cast(dialogTemplate.ControlCount()), __func__, __LINE__, "none at first"); + + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Button, IDOK, WS_CHILD, 0, L"OK"); + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Edit, 100, WS_CHILD, 0, std::wstring()); + + CheckEquals(2, static_cast(dialogTemplate.ControlCount()), __func__, __LINE__, "two added"); + CheckEquals(std::string("0002"), WordsText(dialogTemplate.Words(), 4, 1), __func__, __LINE__, + "in the header's cdit"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DialogTemplate::Test_Constructor_WritesTheHeaderWithTheGUIFont() +{ + // Act + TWin32GUIDialogTemplate const dialogTemplate(WS_POPUP | WS_CAPTION, L"Ab"); + + // Assert: WS_POPUP | WS_CAPTION | DS_SETFONT is 0x80C00040. + CheckEquals(std::string("0040,80C0,0000,0000,0000,0000,0000,00C8,0064,0000,0000,0041,0062,0000," + "0009,0053,0065,0067,006F,0065,0020,0055,0049,0000"), WordsText(dialogTemplate.Words(), 0, 100), __func__, + __LINE__, "style with DS_SETFONT, no extended style or controls, placeholder bounds, no menu, the default " + "class, the caption, then Segoe UI 9 pt"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_DialogTemplate::Test_Get_IsATemplateWindowsCreatesTheDialogFrom() +{ + // Arrange: a hidden dialog with a caption outside ASCII (U+00E9) and one control of each class, the last + // after a padding word. + TWin32GUIDialogTemplate dialogTemplate(WS_POPUP | WS_CAPTION, L"Caf\x00E9"); + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Button, IDOK, WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, + 0, L"OK!"); + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Edit, 101, WS_CHILD | WS_VISIBLE | ES_NUMBER, + WS_EX_CLIENTEDGE, L"42"); + dialogTemplate.AddControl(EWin32GUIDialogControlClass::Static, 102, WS_CHILD | WS_VISIBLE, 0, L"\x4E2D"); + + // Act + HWND const dialog = CreateDialogIndirectParamW(GetModuleHandleW(nullptr), dialogTemplate.Get(), nullptr, + &IgnoringDialogProc, 0); + + std::string const caption = WindowText(dialog); + HWND const button = GetDlgItem(dialog, IDOK); + HWND const edit = GetDlgItem(dialog, 101); + HWND const label = GetDlgItem(dialog, 102); + std::string const controls = ClassName(button) + " " + WindowText(button) + ", " + ClassName(edit) + " " + + WindowText(edit) + ", " + ClassName(label) + " " + WindowText(label); + bool const editStyled = (GetWindowLongPtrW(edit, GWL_STYLE) & ES_NUMBER) != 0 && + (GetWindowLongPtrW(edit, GWL_EXSTYLE) & WS_EX_CLIENTEDGE) != 0; + + LOGFONTW font = {}; + HFONT const fontHandle = reinterpret_cast(SendMessageW(dialog, WM_GETFONT, 0, 0)); + + if (fontHandle != nullptr) + GetObjectW(fontHandle, sizeof(font), &font); + + if (dialog != nullptr) + DestroyWindow(dialog); + + // Assert + CheckTrue(dialog != nullptr, __func__, __LINE__, "Windows created the dialog"); + CheckEquals(std::string("Caf\xC3\xA9"), caption, __func__, __LINE__, "its caption"); + CheckEquals(std::string("Button OK!, Edit 42, Static \xE4\xB8\xAD"), controls, __func__, __LINE__, + "each control's class and text"); + CheckTrue(editStyled, __func__, __LINE__, "the edit's style and extended style"); + CheckEquals(std::wstring(L"Segoe UI"), std::wstring(font.lfFaceName), __func__, __LINE__, "the dialog's font"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +ASW_REGISTER_TEST_GROUP(ASWUnitTests::TTest_ASWUnitTests_Win32GUI_DialogTemplate) diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_DialogTemplate.h b/win32/tests/Test_ASWUnitTests_Win32GUI_DialogTemplate.h new file mode 100644 index 0000000..c259e84 --- /dev/null +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_DialogTemplate.h @@ -0,0 +1,63 @@ +/* ************************************************************************** +Test_ASWUnitTests_Win32GUI_DialogTemplate.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef Test_ASWUnitTests_Win32GUI_DialogTemplateH +#define Test_ASWUnitTests_Win32GUI_DialogTemplateH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_Win32GUI_DialogTemplate +// +// Exercises the in-memory dialog templates of the Win32 GUI runner's +// dialogs (win32/gui/src/ASWUnitTests_Win32GUI_DialogTemplate.h). Compiled +// by the Windows console runners as well as the Win32 GUI, so it runs +// without the GUI. +///////////////////////////////////////////////////////////////////////////// +class TTest_ASWUnitTests_Win32GUI_DialogTemplate : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: // Test methods + void Test_AddControl_AlignsEachControlOnADWORD(); + void Test_AddControl_CountsTheControls(); + void Test_Constructor_WritesTheHeaderWithTheGUIFont(); + void Test_Get_IsATemplateWindowsCreatesTheDialogFrom(); + +public: + TTest_ASWUnitTests_Win32GUI_DialogTemplate(); + ~TTest_ASWUnitTests_Win32GUI_DialogTemplate() override; + + void SetUp_Group() override; + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override; + void TearDown_Test(ITestCase& testCase) override; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef Test_ASWUnitTests_Win32GUI_DialogTemplateH diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_Glyphs.cpp b/win32/tests/Test_ASWUnitTests_Win32GUI_Glyphs.cpp new file mode 100644 index 0000000..361b4d1 --- /dev/null +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_Glyphs.cpp @@ -0,0 +1,261 @@ +/* ************************************************************************** +Test_ASWUnitTests_Win32GUI_Glyphs.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +// Module header +#include "Test_ASWUnitTests_Win32GUI_Glyphs.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Glyphs.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +//--------------------------------------------------------------------------- + +int At(std::vector const& coverage, int size, int x, int y); +bool IsMirrored(std::vector const& coverage, int size, bool leftToRight); +int Total(std::vector const& coverage); + +//--------------------------------------------------------------------------- +int At(std::vector const& coverage, int size, int x, int y) +{ + return coverage[static_cast(y) * static_cast(size) + static_cast(x)]; +} +//--------------------------------------------------------------------------- +/* + IsMirrored + + Whether the glyph is the same mirrored left to right (or else top to bottom). +*/ +bool IsMirrored(std::vector const& coverage, int size, bool leftToRight) +{ + for (int y = 0; y < size; ++y) + { + for (int x = 0; x < size; ++x) + { + int const mirrored = leftToRight ? At(coverage, size, size - 1 - x, y) : At(coverage, size, x, size - 1 - y); + + if (At(coverage, size, x, y) != mirrored) + return false; + } + } + + return true; +} +//--------------------------------------------------------------------------- +int Total(std::vector const& coverage) +{ + int total = 0; + + for (std::uint8_t const value : coverage) + total += value; + + return total; +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_Win32GUI_Glyphs +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_Win32GUI_Glyphs::TTest_ASWUnitTests_Win32GUI_Glyphs() + : inherited("ASWUnitTests_Win32GUI_Glyphs_Tests") +{ + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_ArrowPointsRight, + "RenderWin32GUIGlyph_ArrowPointsRight"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_CirclesAreRoundAndNested, + "RenderWin32GUIGlyph_CirclesAreRoundAndNested"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_CoversOnePixelPerValue, + "RenderWin32GUIGlyph_CoversOnePixelPerValue"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_CrossIsCarbonsCheckMark, + "RenderWin32GUIGlyph_CrossIsCarbonsCheckMark"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_IsEmptyWithoutASize, + "RenderWin32GUIGlyph_IsEmptyWithoutASize"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_ScalesWithTheSize, + "RenderWin32GUIGlyph_ScalesWithTheSize"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_SquareAndDotAreInTheMiddle, + "RenderWin32GUIGlyph_SquareAndDotAreInTheMiddle"); +} +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_Win32GUI_Glyphs::~TTest_ASWUnitTests_Win32GUI_Glyphs() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Glyphs::SetUp_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Glyphs::SetUp_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Glyphs::TearDown_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Glyphs::TearDown_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- + +// /////// Begin tests after this line /////////////////////// + +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_ArrowPointsRight() +{ + // Act: the menus' 16-pixel cell, the triangle's flat side at pixel 6 and its point at 10. + std::vector const arrow = RenderWin32GUIGlyph(EWin32GUIGlyph::ArrowRight, 16); + + std::vector const left = RenderWin32GUIGlyph(EWin32GUIGlyph::ArrowLeft, 16); + + bool mirrored = true; + + for (int y = 0; y < 16; ++y) + { + for (int x = 0; x < 16; ++x) + mirrored = mirrored && At(left, 16, x, y) == At(arrow, 16, 15 - x, y); + } + + // Assert + CheckTrue(IsMirrored(arrow, 16, false), __func__, __LINE__, "the same above and below its middle"); + CheckEquals(255, At(arrow, 16, 6, 8), __func__, __LINE__, "full at its flat side, in the middle"); + CheckEquals(0, At(arrow, 16, 5, 8), __func__, __LINE__, "nothing left of it"); + CheckEquals(0, At(arrow, 16, 11, 8), __func__, __LINE__, "nothing right of its point"); + CheckTrue(At(arrow, 16, 6, 5) > At(arrow, 16, 9, 5), __func__, __LINE__, "narrowing to the right"); + CheckTrue(mirrored, __func__, __LINE__, "the left arrow is the right one mirrored"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_CirclesAreRoundAndNested() +{ + // Act: a radio button's circle and its inside, at Carbon's 13 pixels. + std::vector const circle = RenderWin32GUIGlyph(EWin32GUIGlyph::Circle, 13); + std::vector const inner = RenderWin32GUIGlyph(EWin32GUIGlyph::InnerCircle, 13); + + bool nested = true; + + for (std::size_t i = 0; i < inner.size(); ++i) + nested = nested && inner[i] <= circle[i]; + + // Assert + CheckTrue(IsMirrored(circle, 13, true) && IsMirrored(circle, 13, false), __func__, __LINE__, + "the circle is symmetric"); + CheckEquals(255, At(circle, 13, 6, 6), __func__, __LINE__, "the circle's middle"); + CheckEquals(255, At(circle, 13, 0, 6), __func__, __LINE__, "the circle reaches the cell's edge"); + CheckEquals(0, At(circle, 13, 0, 0), __func__, __LINE__, "the circle leaves the corners"); + CheckEquals(0, At(inner, 13, 0, 6), __func__, __LINE__, "the inside is a pixel in from the edge"); + CheckEquals(255, At(inner, 13, 1, 6), __func__, __LINE__, "the inside starts a pixel in"); + CheckTrue(nested, __func__, __LINE__, "the inside is within the circle"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_CoversOnePixelPerValue() +{ + // Arrange + EWin32GUIGlyph const glyphs[] = { + EWin32GUIGlyph::ArrowLeft, EWin32GUIGlyph::ArrowRight, EWin32GUIGlyph::Circle, EWin32GUIGlyph::Cross, EWin32GUIGlyph::Dot, + EWin32GUIGlyph::InnerCircle, EWin32GUIGlyph::Square, EWin32GUIGlyph::Tick, + }; + + for (EWin32GUIGlyph const glyph : glyphs) + { + // Act + std::vector const coverage = RenderWin32GUIGlyph(glyph, 13); + + // Assert + std::string const name = "glyph " + std::to_string(static_cast(glyph)); + CheckEquals(static_cast(13 * 13), coverage.size(), __func__, __LINE__, name + ": 13 x 13 values"); + CheckTrue(Total(coverage) > 0, __func__, __LINE__, name + ": covers something"); + CheckTrue(Total(coverage) < 13 * 13 * 255, __func__, __LINE__, name + ": doesn't cover everything"); + } +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_CrossIsCarbonsCheckMark() +{ + // Act: Carbon's 13-pixel check box's cross, whose arms end at pixels 3 and 9. + std::vector const cross = RenderWin32GUIGlyph(EWin32GUIGlyph::Cross, 13); + + // Assert + CheckTrue(IsMirrored(cross, 13, true) && IsMirrored(cross, 13, false), __func__, __LINE__, "symmetric"); + CheckEquals(255, At(cross, 13, 6, 6), __func__, __LINE__, "full where its arms cross"); + CheckTrue(At(cross, 13, 3, 3) >= 200, __func__, __LINE__, "nearly full at an arm's end"); + CheckEquals(0, At(cross, 13, 6, 3), __func__, __LINE__, "nothing between the upper arms"); + CheckEquals(0, At(cross, 13, 1, 1), __func__, __LINE__, "nothing past an arm's end"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_IsEmptyWithoutASize() +{ + // Act, Assert + CheckTrue(RenderWin32GUIGlyph(EWin32GUIGlyph::Cross, 0).empty(), __func__, __LINE__, "size 0"); + CheckTrue(RenderWin32GUIGlyph(EWin32GUIGlyph::Cross, -1).empty(), __func__, __LINE__, "a negative size"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_ScalesWithTheSize() +{ + // Act: 96 and 192 DPI. + int const at13 = Total(RenderWin32GUIGlyph(EWin32GUIGlyph::Cross, 13)); + int const at26 = Total(RenderWin32GUIGlyph(EWin32GUIGlyph::Cross, 26)); + + // Assert: twice the size, four times the area, within what the pixel grid changes. + CheckTrue(at26 > at13 * 7 / 2 && at26 < at13 * 9 / 2, __func__, __LINE__, "covers about four times as much"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Glyphs::Test_RenderWin32GUIGlyph_SquareAndDotAreInTheMiddle() +{ + // Act: the partial check box's square and the radio button's dot, at 13 pixels. + std::vector const square = RenderWin32GUIGlyph(EWin32GUIGlyph::Square, 13); + std::vector const dot = RenderWin32GUIGlyph(EWin32GUIGlyph::Dot, 13); + + // Assert + for (std::vector const* const glyph : { &square, &dot }) + { + std::string const name = glyph == &square ? "the square" : "the dot"; + CheckTrue(IsMirrored(*glyph, 13, true) && IsMirrored(*glyph, 13, false), __func__, __LINE__, + name + " is symmetric"); + CheckEquals(255, At(*glyph, 13, 6, 6), __func__, __LINE__, name + " is full in the middle"); + CheckEquals(0, At(*glyph, 13, 6, 2), __func__, __LINE__, name + " leaves the edges"); + } + + CheckTrue(At(dot, 13, 4, 4) < At(square, 13, 4, 4), __func__, __LINE__, "the dot is round where the square isn't"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +ASW_REGISTER_TEST_GROUP(ASWUnitTests::TTest_ASWUnitTests_Win32GUI_Glyphs) diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_Glyphs.h b/win32/tests/Test_ASWUnitTests_Win32GUI_Glyphs.h new file mode 100644 index 0000000..24757d6 --- /dev/null +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_Glyphs.h @@ -0,0 +1,65 @@ +/* ************************************************************************** +Test_ASWUnitTests_Win32GUI_Glyphs.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef Test_ASWUnitTests_Win32GUI_GlyphsH +#define Test_ASWUnitTests_Win32GUI_GlyphsH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_Win32GUI_Glyphs +// +// Exercises the Win32 GUI runner's dark theme shapes +// (win32/gui/src/ASWUnitTests_Win32GUI_Glyphs.h). Compiled by the Windows +// console runners as well as the Win32 GUI, so it runs without the GUI. +///////////////////////////////////////////////////////////////////////////// +class TTest_ASWUnitTests_Win32GUI_Glyphs : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: // Test methods + void Test_RenderWin32GUIGlyph_ArrowPointsRight(); + void Test_RenderWin32GUIGlyph_CirclesAreRoundAndNested(); + void Test_RenderWin32GUIGlyph_CoversOnePixelPerValue(); + void Test_RenderWin32GUIGlyph_CrossIsCarbonsCheckMark(); + void Test_RenderWin32GUIGlyph_IsEmptyWithoutASize(); + void Test_RenderWin32GUIGlyph_ScalesWithTheSize(); + void Test_RenderWin32GUIGlyph_SquareAndDotAreInTheMiddle(); + +public: + TTest_ASWUnitTests_Win32GUI_Glyphs(); + ~TTest_ASWUnitTests_Win32GUI_Glyphs() override; + + void SetUp_Group() override; + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override; + void TearDown_Test(ITestCase& testCase) override; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef Test_ASWUnitTests_Win32GUI_GlyphsH diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_MainLayout.cpp b/win32/tests/Test_ASWUnitTests_Win32GUI_MainLayout.cpp new file mode 100644 index 0000000..5995550 --- /dev/null +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_MainLayout.cpp @@ -0,0 +1,217 @@ +/* ************************************************************************** +Test_ASWUnitTests_Win32GUI_MainLayout.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +// Module header +#include "Test_ASWUnitTests_Win32GUI_MainLayout.h" +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_MainLayout.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +//--------------------------------------------------------------------------- + +std::string Bounds(TGUIRect const& rect); + +//--------------------------------------------------------------------------- +/* + Bounds + + The rectangle as "left,top,right,bottom", to compare one with a single CheckEquals(). +*/ +std::string Bounds(TGUIRect const& rect) +{ + return std::to_string(rect.Left) + "," + std::to_string(rect.Top) + "," + std::to_string(rect.Right) + "," + + std::to_string(rect.Bottom); +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_Win32GUI_MainLayout +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_Win32GUI_MainLayout::TTest_ASWUnitTests_Win32GUI_MainLayout() + : inherited("ASWUnitTests_Win32GUI_MainLayout_Tests") +{ + RegisterTest(&TTest_ASWUnitTests_Win32GUI_MainLayout::Test_ClampDetailHeight_LeavesRoomForTheResultsTabs, + "ClampDetailHeight_LeavesRoomForTheResultsTabs"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_MainLayout::Test_ClampDetailHeight_MinimumWinsInASmallWindow, + "ClampDetailHeight_MinimumWinsInASmallWindow"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_MainLayout::Test_ClampTestTreeWidth_LeavesRoomForTheResults, + "ClampTestTreeWidth_LeavesRoomForTheResults"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_MainLayout::Test_ClampTestTreeWidth_MinimumWinsInASmallWindow, + "ClampTestTreeWidth_MinimumWinsInASmallWindow"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_MainLayout::Test_LayOutMainWindow_GivesNoNegativeHeightInAVeryShortWindow, + "LayOutMainWindow_GivesNoNegativeHeightInAVeryShortWindow"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_MainLayout::Test_LayOutMainWindow_KeepsThePanelSizesWhenTheWindowShrinks, + "LayOutMainWindow_KeepsThePanelSizesWhenTheWindowShrinks"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_MainLayout::Test_LayOutMainWindow_MatchesTheDFMAt96DPI, + "LayOutMainWindow_MatchesTheDFMAt96DPI"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_MainLayout::Test_LayOutMainWindow_ScalesWithTheDPI, + "LayOutMainWindow_ScalesWithTheDPI"); +} +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_Win32GUI_MainLayout::~TTest_ASWUnitTests_Win32GUI_MainLayout() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_MainLayout::SetUp_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_MainLayout::SetUp_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_MainLayout::TearDown_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_MainLayout::TearDown_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- + +// /////// Begin tests after this line /////////////////////// + +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_MainLayout::Test_ClampDetailHeight_LeavesRoomForTheResultsTabs() +{ + // Arrange: the .dfm's window, with 580 pixels between the toolbar and the progress bar; 515 leaves the + // splitter (5) and the tabs' minimum (60) below the detail pane. + + // Act, Assert + CheckEquals(170, ClampDetailHeight(170, 661, 96), __func__, __LINE__, "a size that fits is kept"); + CheckEquals(60, ClampDetailHeight(10, 661, 96), __func__, __LINE__, "at least the minimum"); + CheckEquals(515, ClampDetailHeight(600, 661, 96), __func__, __LINE__, "leaving the tabs their minimum"); + CheckEquals(772, ClampDetailHeight(900, 992, 144), __func__, __LINE__, + "at 144 DPI: 870 between the bars, less the splitter (8) and the tabs' minimum (90)"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_MainLayout::Test_ClampDetailHeight_MinimumWinsInASmallWindow() +{ + // Act, Assert: 100 pixels between the toolbar and the progress bar can't fit both minimums. + CheckEquals(60, ClampDetailHeight(80, 181, 96), __func__, __LINE__, "the detail pane's minimum"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_MainLayout::Test_ClampTestTreeWidth_LeavesRoomForTheResults() +{ + // Act, Assert + CheckEquals(380, ClampTestTreeWidth(380, 1008, 96), __func__, __LINE__, "a size that fits is kept"); + CheckEquals(150, ClampTestTreeWidth(20, 1008, 96), __func__, __LINE__, "at least the minimum"); + CheckEquals(853, ClampTestTreeWidth(1000, 1008, 96), __func__, __LINE__, + "leaving the splitter (5) and the results' minimum (150)"); + CheckEquals(225, ClampTestTreeWidth(20, 1512, 144), __func__, __LINE__, "the minimum at 144 DPI"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_MainLayout::Test_ClampTestTreeWidth_MinimumWinsInASmallWindow() +{ + // Act, Assert + CheckEquals(150, ClampTestTreeWidth(200, 250, 96), __func__, __LINE__, "the tree's minimum"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_MainLayout::Test_LayOutMainWindow_GivesNoNegativeHeightInAVeryShortWindow() +{ + // Act: a client area so short that the progress bar (21 to 41) is above the toolbar's bottom (42). + TWin32GUIMainLayout const layout = LayOutMainWindow(100, 60, 380, 170, 96); + + // Assert + CheckEquals(std::string("0,42,380,42"), Bounds(layout.TestTree), __func__, __LINE__, "the tree has no height"); + CheckEquals(std::string("380,42,385,42"), Bounds(layout.TestsSplitter), __func__, __LINE__, + "nor has its splitter"); + CheckEquals(std::string("0,21,100,41"), Bounds(layout.ProgressBar), __func__, __LINE__, + "the progress bar stays above the status bar"); + CheckEquals(std::string("0,41,100,60"), Bounds(layout.StatusBar), __func__, __LINE__, + "the status bar stays at the bottom"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_MainLayout::Test_LayOutMainWindow_KeepsThePanelSizesWhenTheWindowShrinks() +{ + // Act: a window narrower than the tree and shorter than the detail pane. + TWin32GUIMainLayout const layout = LayOutMainWindow(300, 200, 380, 170, 96); + + // Assert + CheckEquals(std::string("0,42,380,161"), Bounds(layout.TestTree), __func__, __LINE__, "the tree keeps its width"); + CheckEquals(std::string("385,42,385,212"), Bounds(layout.Detail), __func__, __LINE__, + "the detail pane keeps its height, with no width left"); + CheckEquals(std::string("385,217,385,217"), Bounds(layout.Results), __func__, __LINE__, + "the results tabs get nothing, never a negative size"); + CheckEquals(std::string("0,161,300,181"), Bounds(layout.ProgressBar), __func__, __LINE__, + "the progress bar stays above the status bar"); + CheckEquals(std::string("0,181,300,200"), Bounds(layout.StatusBar), __func__, __LINE__, + "the status bar stays at the bottom"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_MainLayout::Test_LayOutMainWindow_MatchesTheDFMAt96DPI() +{ + // Act: the .dfm's client size, tree width and detail height. + TWin32GUIMainLayout const layout = LayOutMainWindow(1008, 661, 380, 170, 96); + + // Assert: each control's Left, Top, Left + Width and Top + Height in the .dfm (the results' within + // Pnl_Results, which starts at 385,42). + CheckEquals(std::string("0,42,380,622"), Bounds(layout.TestTree), __func__, __LINE__, "TV_Tests"); + CheckEquals(std::string("380,42,385,622"), Bounds(layout.TestsSplitter), __func__, __LINE__, "Spl_Tests"); + CheckEquals(std::string("385,42,1008,212"), Bounds(layout.Detail), __func__, __LINE__, "Mem_Detail"); + CheckEquals(std::string("385,212,1008,217"), Bounds(layout.DetailSplitter), __func__, __LINE__, "Spl_Detail"); + CheckEquals(std::string("385,217,1008,622"), Bounds(layout.Results), __func__, __LINE__, "PC_Results"); + CheckEquals(std::string("0,622,1008,642"), Bounds(layout.ProgressBar), __func__, __LINE__, "PB_Progress"); + CheckEquals(std::string("0,642,1008,661"), Bounds(layout.StatusBar), __func__, __LINE__, "SB_Status"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_MainLayout::Test_LayOutMainWindow_ScalesWithTheDPI() +{ + // Act: at 144 DPI, the toolbar is 63 tall, the progress bar 30, the status bar 29 (28.5, rounded) and the + // splitters 8 (7.5, rounded). + TWin32GUIMainLayout const layout = LayOutMainWindow(1512, 992, 570, 255, 144); + + // Assert + CheckEquals(std::string("0,63,570,933"), Bounds(layout.TestTree), __func__, __LINE__, "TV_Tests"); + CheckEquals(std::string("570,63,578,933"), Bounds(layout.TestsSplitter), __func__, __LINE__, "Spl_Tests"); + CheckEquals(std::string("578,63,1512,318"), Bounds(layout.Detail), __func__, __LINE__, "Mem_Detail"); + CheckEquals(std::string("578,318,1512,326"), Bounds(layout.DetailSplitter), __func__, __LINE__, "Spl_Detail"); + CheckEquals(std::string("578,326,1512,933"), Bounds(layout.Results), __func__, __LINE__, "PC_Results"); + CheckEquals(std::string("0,933,1512,963"), Bounds(layout.ProgressBar), __func__, __LINE__, "PB_Progress"); + CheckEquals(std::string("0,963,1512,992"), Bounds(layout.StatusBar), __func__, __LINE__, "SB_Status"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +ASW_REGISTER_TEST_GROUP(ASWUnitTests::TTest_ASWUnitTests_Win32GUI_MainLayout) diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_MainLayout.h b/win32/tests/Test_ASWUnitTests_Win32GUI_MainLayout.h new file mode 100644 index 0000000..178f9d5 --- /dev/null +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_MainLayout.h @@ -0,0 +1,67 @@ +/* ************************************************************************** +Test_ASWUnitTests_Win32GUI_MainLayout.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef Test_ASWUnitTests_Win32GUI_MainLayoutH +#define Test_ASWUnitTests_Win32GUI_MainLayoutH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_Win32GUI_MainLayout +// +// Exercises where the Win32 GUI runner's main window puts its controls +// (win32/gui/src/ASWUnitTests_Win32GUI_MainLayout.h). Compiled by the +// Windows console runners as well as the Win32 GUI, so it runs without the +// GUI. +///////////////////////////////////////////////////////////////////////////// +class TTest_ASWUnitTests_Win32GUI_MainLayout : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: // Test methods + void Test_ClampDetailHeight_LeavesRoomForTheResultsTabs(); + void Test_ClampDetailHeight_MinimumWinsInASmallWindow(); + void Test_ClampTestTreeWidth_LeavesRoomForTheResults(); + void Test_ClampTestTreeWidth_MinimumWinsInASmallWindow(); + void Test_LayOutMainWindow_GivesNoNegativeHeightInAVeryShortWindow(); + void Test_LayOutMainWindow_KeepsThePanelSizesWhenTheWindowShrinks(); + void Test_LayOutMainWindow_MatchesTheDFMAt96DPI(); + void Test_LayOutMainWindow_ScalesWithTheDPI(); + +public: + TTest_ASWUnitTests_Win32GUI_MainLayout(); + ~TTest_ASWUnitTests_Win32GUI_MainLayout() override; + + void SetUp_Group() override; + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override; + void TearDown_Test(ITestCase& testCase) override; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef Test_ASWUnitTests_Win32GUI_MainLayoutH diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp b/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp new file mode 100644 index 0000000..c5e66d6 --- /dev/null +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.cpp @@ -0,0 +1,501 @@ +/* ************************************************************************** +Test_ASWUnitTests_Win32GUI_Palette.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +// Module header +#include "Test_ASWUnitTests_Win32GUI_Palette.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_GUI_Theme.h" +#include "ASWUnitTests_GUI_View.h" +#include "ASWUnitTests_Win32GUI_Palette.h" +//--------------------------------------------------------------------------- + +namespace +{ + +//--------------------------------------------------------------------------- + +double ContrastRatio(COLORREF a, COLORREF b); +double RelativeLuminance(COLORREF color); + +//--------------------------------------------------------------------------- +/* + ContrastRatio + + WCAG 2's contrast ratio of two colors, 1 to 21; text needs at least 4.5 on its background to be readable. +*/ +double ContrastRatio(COLORREF a, COLORREF b) +{ + double const lighter = (std::max)(RelativeLuminance(a), RelativeLuminance(b)); + double const darker = (std::min)(RelativeLuminance(a), RelativeLuminance(b)); + + return (lighter + 0.05) / (darker + 0.05); +} +//--------------------------------------------------------------------------- +/* + RelativeLuminance + + WCAG 2's relative luminance of an sRGB color, 0 for black to 1 for white. +*/ +double RelativeLuminance(COLORREF color) +{ + auto const linear = [](BYTE channel) + { + double const value = channel / 255.0; + + return value <= 0.04045 ? value / 12.92 : std::pow((value + 0.055) / 1.055, 2.4); + }; + + return 0.2126 * linear(GetRValue(color)) + 0.7152 * linear(GetGValue(color)) + 0.0722 * linear(GetBValue(color)); +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_Win32GUI_Palette +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_Win32GUI_Palette::TTest_ASWUnitTests_Win32GUI_Palette() + : inherited("ASWUnitTests_Win32GUI_Palette_Tests") +{ + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_BlendColors_MixesEachChannelInProportion, + "BlendColors_MixesEachChannelInProportion"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_BuiltInDarkThemes_AreCarbonThenItsTints, + "BuiltInDarkThemes_AreCarbonThenItsTints"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_BuiltInDarkThemes_KeepTextReadable, + "BuiltInDarkThemes_KeepTextReadable"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_CarbonPalette_HasCarbonsColors, + "CarbonPalette_HasCarbonsColors"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_CarbonThemeFileText_ReadsBackAsCarbon, + "CarbonThemeFileText_ReadsBackAsCarbon"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_FailureAndSkipTextColorOn_AreLighterOnADarkBackground, + "FailureAndSkipTextColorOn_AreLighterOnADarkBackground"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_FindBuiltInDarkTheme_IgnoresCase, + "FindBuiltInDarkTheme_IgnoresCase"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_IsDarkColor_IsByTheLuma, "IsDarkColor_IsByTheLuma"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_PaletteRoles_ListEveryMemberOnce, + "PaletteRoles_ListEveryMemberOnce"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_PaletteWithThemeColors_SetsOnlyTheirRoles, + "PaletteWithThemeColors_SetsOnlyTheirRoles"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_Palette::Test_TintedCarbonPalette_TintsOnlyTheGraysAndTheAccent, + "TintedCarbonPalette_TintsOnlyTheGraysAndTheAccent"); +} +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_Win32GUI_Palette::~TTest_ASWUnitTests_Win32GUI_Palette() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::SetUp_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::SetUp_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::TearDown_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::TearDown_Test(ITestCase& /*testCase*/) +{ +} +//--------------------------------------------------------------------------- + +// /////// Begin tests after this line /////////////////////// + +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_BlendColors_MixesEachChannelInProportion() +{ + // Arrange + COLORREF const black = RGB(0, 0, 0); + COLORREF const white = RGB(255, 255, 255); + + // Act, Assert + CheckEquals(black, BlendColors(black, white, 0, 4), __func__, __LINE__, "'from' at 0"); + CheckEquals(white, BlendColors(black, white, 4, 4), __func__, __LINE__, "'to' at the denominator"); + CheckEquals(RGB(128, 128, 128), BlendColors(black, white, 1, 2), __func__, __LINE__, "halfway, rounded"); + CheckEquals(RGB(20, 40, 60), BlendColors(RGB(10, 20, 30), RGB(30, 60, 90), 1, 2), __func__, __LINE__, + "each channel on its own"); + CheckEquals(RGB(64, 64, 64), BlendColors(black, white, 1, 4), __func__, __LINE__, "a quarter, rounded"); + CheckEquals(white, BlendColors(black, white, 5, 4), __func__, __LINE__, "past the denominator is 'to'"); + CheckEquals(black, BlendColors(black, white, -1, 4), __func__, __LINE__, "below 0 is 'from'"); + CheckEquals(black, BlendColors(black, white, 1, 0), __func__, __LINE__, "'from' with no denominator"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_BuiltInDarkThemes_AreCarbonThenItsTints() +{ + // Act + std::vector const& themes = BuiltInDarkThemes(); + + // Assert + std::vector nameList; + std::string names; + + for (TWin32GUIBuiltInTheme const& theme : themes) + { + nameList.push_back(theme.Name); + names += theme.Name + "\n"; + } + + CheckEquals(std::string("Carbon\nCharcoal Dark Slate\nCobalt XEMedia\nEmber\nForest\nMidnight\nOnyx Blue\nPlum\n" + "Tablet Dark\n"), names, __func__, __LINE__, "Carbon first, then the VCL styles and the tints by name"); + + for (std::string const& style : GUIVCLStyleDarkThemeNames()) + { + CheckTrue(std::find(nameList.begin(), nameList.end(), style) != nameList.end(), __func__, __LINE__, + "the VCL GUI runner's built-in style " + style + ", by its name"); + } + + // The menu's other items' accelerators: Save Theme Template's and Open Themes Folder's. + std::set letters = { 's', 'o' }; + bool lettersDiffer = true; + + for (std::string const& caption : GUIMenuItemTextsWithAccelerators(nameList, "so")) + { + std::size_t const accelerator = caption.find('&'); + lettersDiffer = lettersDiffer && accelerator != std::string::npos && accelerator + 1 < caption.size() && + letters.insert(static_cast(std::tolower(static_cast(caption[accelerator + 1])))).second; + } + + CheckTrue(lettersDiffer, __func__, __LINE__, "each with an accelerator of its own in the menu"); + AssertFalse(themes.empty(), __func__, __LINE__, "Carbon is there"); + TWin32GUIPalette const carbon = CarbonPalette(); + bool sameAsCarbon = true; + + for (TWin32GUIPaletteRole const& role : PaletteRoles()) + sameAsCarbon = sameAsCarbon && themes.front().Palette.*role.Color == carbon.*role.Color; + + CheckEquals(GUIDefaultDarkThemeName(), themes.front().Name, __func__, __LINE__, "the default first"); + CheckTrue(sameAsCarbon, __func__, __LINE__, "in CarbonPalette()'s colors"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_BuiltInDarkThemes_KeepTextReadable() +{ + // Arrange: each text color the GUI draws on a background, with that background, a gradient's by its middle, + // where the text is. + struct TPair + { + COLORREF TWin32GUIPalette::* Text; + COLORREF TWin32GUIPalette::* Background; + COLORREF TWin32GUIPalette::* BackgroundBottom; + char const* Name; + }; + + TPair const pairs[] = { + { &TWin32GUIPalette::WindowText, &TWin32GUIPalette::Window, nullptr, "WindowText on Window" }, + { &TWin32GUIPalette::ControlText, &TWin32GUIPalette::Panel, nullptr, "ControlText on Panel" }, + { &TWin32GUIPalette::ControlText, &TWin32GUIPalette::MenuBackground, nullptr, "ControlText on MenuBackground" }, + { &TWin32GUIPalette::ButtonText, &TWin32GUIPalette::ButtonFaceTop, &TWin32GUIPalette::ButtonFaceBottom, + "ButtonText on the button's face" }, + { &TWin32GUIPalette::ButtonHotText, &TWin32GUIPalette::ButtonHotFaceTop, &TWin32GUIPalette::ButtonHotFaceBottom, + "ButtonHotText on the hot button's face" }, + { &TWin32GUIPalette::HotText, &TWin32GUIPalette::Highlight, nullptr, "HotText on Highlight" }, + { &TWin32GUIPalette::SelectionText, &TWin32GUIPalette::Selection, nullptr, "SelectionText on Selection" }, + { &TWin32GUIPalette::FailureText, &TWin32GUIPalette::Window, nullptr, "FailureText on Window" }, + { &TWin32GUIPalette::SkipText, &TWin32GUIPalette::Window, nullptr, "SkipText on Window" }, + { &TWin32GUIPalette::TooltipText, &TWin32GUIPalette::TooltipBackground, nullptr, + "TooltipText on TooltipBackground" }, + }; + + // Act + std::string unreadable; + + for (TWin32GUIBuiltInTheme const& theme : BuiltInDarkThemes()) + { + for (TPair const& pair : pairs) + { + COLORREF const background = pair.BackgroundBottom == nullptr ? theme.Palette.*pair.Background : + BlendColors(theme.Palette.*pair.Background, theme.Palette.*pair.BackgroundBottom, 1, 2); + + if (ContrastRatio(theme.Palette.*pair.Text, background) < 4.5) + unreadable += theme.Name + ": " + pair.Name + "\n"; + } + } + + // Assert + CheckEmpty(unreadable, __func__, __LINE__, "every text at WCAG's 4.5 to 1 or more on its background"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_CarbonPalette_HasCarbonsColors() +{ + // Act + TWin32GUIPalette const palette = CarbonPalette(); + + // Assert: a few of Carbon.vsf's own colors, and the VCL GUI runner's text colors on its window color. + CheckEquals(RGB(0x19, 0x19, 0x19), palette.Window, __func__, __LINE__, "clWindow"); + CheckEquals(RGB(0xEA, 0xEA, 0xEA), palette.WindowText, __func__, __LINE__, "clWindowText"); + CheckEquals(RGB(0x33, 0x33, 0x33), palette.Panel, __func__, __LINE__, "clBtnFace"); + CheckEquals(RGB(0xC0, 0xC0, 0xC0), palette.ControlText, __func__, __LINE__, "sfButtonTextNormal"); + CheckEquals(RGB(0x26, 0x5A, 0x73), palette.Highlight, __func__, __LINE__, "the hot menu item"); + CheckEquals(RGB(0x19, 0x19, 0x19), palette.MenuBackground, __func__, __LINE__, "the menus' background"); + CheckEquals(RGB(255, 100, 100), palette.FailureText, __func__, __LINE__, "a failure on the dark window"); + CheckEquals(RGB(255, 190, 70), palette.SkipText, __func__, __LINE__, "a skip on the dark window"); + CheckTrue(IsDarkColor(palette.Window) && IsDarkColor(palette.Panel) && !IsDarkColor(palette.WindowText) && + !IsDarkColor(palette.ControlText), __func__, __LINE__, "dark backgrounds, light text"); + CheckEquals(RGB(215, 40, 40), palette.TreeStatusFailed, __func__, __LINE__, "the tree's failed dot, as light's"); + CheckEquals(RGB(16, 160, 16), palette.TreeStatusPassed, __func__, __LINE__, "the tree's passed dot, as light's"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_CarbonThemeFileText_ReadsBackAsCarbon() +{ + // Act + std::string const text = CarbonThemeFileText(); + TGUIThemeFile const file = ReadGUIThemeFile(text, PaletteRoleNames()); + TWin32GUIPalette const blank = {}; + TWin32GUIPalette const readBack = PaletteWithThemeColors(blank, file.Colors); + + // Assert + CheckTrue(file.Usable, __func__, __LINE__, "a usable theme file"); + CheckEquals(std::string(), std::string(file.Problems.empty() ? "" : file.Problems.front()), __func__, __LINE__, + "with no problems"); + CheckEquals(PaletteRoles().size(), file.Colors.size(), __func__, __LINE__, "setting every role"); + + TWin32GUIPalette const carbon = CarbonPalette(); + std::string differences; + + for (TWin32GUIPaletteRole const& role : PaletteRoles()) + { + if (readBack.*role.Color != carbon.*role.Color) + differences += std::string(differences.empty() ? "" : ", ") + role.Name; + } + + CheckEmpty(differences, __func__, __LINE__, "to Carbon's colors"); + + CheckTrue(text.compare(0, 30, "; An ASWUnitTests theme file: ") == 0, __func__, __LINE__, + "starting with what it is"); + CheckTrue(text.find("\r\n[Theme]\r\nBase=Dark\r\n\r\n[Colors]\r\n; Push buttons: ") != std::string::npos, + __func__, __LINE__, "Base=Dark, then the colors, under their groups' comments"); + CheckTrue(text.find("\r\n\r\n; Tooltips.\r\nTooltipBackground=#515151\r\nTooltipText=#EAEAEA\r\n\r\n") != + std::string::npos, __func__, __LINE__, "a group, with a blank line on each side"); + + bool lineBreaksAreCRLF = true; + bool ascii = true; + std::size_t longest = 0; + std::size_t lineStart = 0; + + for (std::size_t i = 0; i < text.size(); ++i) + { + if (static_cast(text[i]) > 0x7F) + ascii = false; + + if (text[i] == '\n' && (i == 0 || text[i - 1] != '\r')) + lineBreaksAreCRLF = false; + + if (text[i] == '\n') + { + longest = (std::max)(longest, i - lineStart - 1); + lineStart = i + 1; + } + } + + CheckTrue(lineBreaksAreCRLF, __func__, __LINE__, "CRLF line breaks"); + CheckTrue(ascii, __func__, __LINE__, "plain ASCII"); + CheckTrue(longest <= 110, __func__, __LINE__, "no line longer than 110: " + std::to_string(longest)); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_FailureAndSkipTextColorOn_AreLighterOnADarkBackground() +{ + // Act, Assert: as the VCL GUI runner's colors, by the background's luma, so a dark high contrast theme's + // window color also gets the lighter ones. + CheckEquals(RGB(200, 0, 0), FailureTextColorOn(RGB(255, 255, 255)), __func__, __LINE__, "failure on white"); + CheckEquals(RGB(255, 100, 100), FailureTextColorOn(RGB(0x19, 0x19, 0x19)), __func__, __LINE__, + "failure on Carbon's window color"); + CheckEquals(RGB(255, 100, 100), FailureTextColorOn(RGB(0, 0, 0)), __func__, __LINE__, "failure on black"); + CheckEquals(RGB(190, 120, 0), SkipTextColorOn(RGB(255, 255, 255)), __func__, __LINE__, "skip on white"); + CheckEquals(RGB(255, 190, 70), SkipTextColorOn(RGB(0x19, 0x19, 0x19)), __func__, __LINE__, + "skip on Carbon's window color"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_FindBuiltInDarkTheme_IgnoresCase() +{ + // Act + TWin32GUIBuiltInTheme const* const midnight = FindBuiltInDarkTheme("mIDNIGHT"); + TWin32GUIBuiltInTheme const* const carbon = FindBuiltInDarkTheme("Carbon"); + + // Assert + AssertTrue(midnight != nullptr, __func__, __LINE__, "a built-in theme, in another case"); + CheckEquals(std::string("Midnight"), midnight->Name, __func__, __LINE__, "with its own name"); + AssertTrue(carbon != nullptr, __func__, __LINE__, "Carbon"); + CheckEquals(std::string("Carbon"), carbon->Name, __func__, __LINE__, "named so"); + CheckTrue(FindBuiltInDarkTheme("Midnight Blue") == nullptr, __func__, __LINE__, "none for a longer name"); + CheckTrue(FindBuiltInDarkTheme("Midnigh") == nullptr, __func__, __LINE__, "none for a shorter one"); + CheckTrue(FindBuiltInDarkTheme(std::string()) == nullptr, __func__, __LINE__, "none for an empty one"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_IsDarkColor_IsByTheLuma() +{ + // Act, Assert: Rec. 601 luma, (299 R + 587 G + 114 B) / 1000, below 128. + CheckTrue(IsDarkColor(RGB(127, 127, 127)), __func__, __LINE__, "gray 127"); + CheckFalse(IsDarkColor(RGB(128, 128, 128)), __func__, __LINE__, "gray 128"); + CheckTrue(IsDarkColor(RGB(0, 0, 255)), __func__, __LINE__, "blue, luma 29"); + CheckFalse(IsDarkColor(RGB(0, 255, 0)), __func__, __LINE__, "green, luma 149"); + CheckTrue(IsDarkColor(RGB(255, 0, 0)), __func__, __LINE__, "red, luma 76"); + CheckFalse(IsDarkColor(RGB(255, 255, 0)), __func__, __LINE__, "yellow, luma 225"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_PaletteRoles_ListEveryMemberOnce() +{ + // Arrange + std::vector const& roles = PaletteRoles(); + + // Act: each role's member set to its own number, so two roles of one member would leave the first's wrong. + TWin32GUIPalette palette = {}; + + for (std::size_t i = 0; i < roles.size(); ++i) + palette.*roles[i].Color = static_cast(i + 1); + + // Assert + CheckEquals(sizeof(TWin32GUIPalette) / sizeof(COLORREF), roles.size(), __func__, __LINE__, + "as many roles as the palette has members"); + + std::string wrong; + + for (std::size_t i = 0; i < roles.size(); ++i) + { + if (palette.*roles[i].Color != static_cast(i + 1)) + wrong += std::string(wrong.empty() ? "" : ", ") + roles[i].Name; + } + + CheckEmpty(wrong, __func__, __LINE__, "each a member of its own"); + + std::vector const names = PaletteRoleNames(); + std::string repeated; + + for (std::size_t i = 0; i < names.size(); ++i) + { + for (std::size_t j = i + 1; j < names.size(); ++j) + { + // Theme files match them ignoring case. + bool const same = names[i].size() == names[j].size() && std::equal(names[i].begin(), names[i].end(), + names[j].begin(), [](char a, char b) + { + return std::tolower(static_cast(a)) == std::tolower(static_cast(b)); + }); + + if (same) + repeated += names[j] + " "; + } + } + + CheckEmpty(repeated, __func__, __LINE__, "names that differ, ignoring case"); + CheckEquals(roles.size(), names.size(), __func__, __LINE__, "PaletteRoleNames(), one per role"); + CheckTrue(!roles.empty() && roles.front().Group != nullptr, __func__, __LINE__, "the first role starts a group"); + CheckEquals(std::string("ButtonBorder"), names.front(), __func__, __LINE__, "named as its member"); + CheckEquals(std::string("WindowText"), names.back(), __func__, __LINE__, "in the struct's order"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_PaletteWithThemeColors_SetsOnlyTheirRoles() +{ + // Arrange + TWin32GUIPalette const carbon = CarbonPalette(); + std::vector colors(3); + colors[0].Role = "Window"; + colors[0].RGB = 0x102030; + colors[1].Role = "NotARole"; + colors[1].RGB = 0xFFFFFF; + colors[2].Role = "TreeStatusPassed"; + colors[2].RGB = 0xA0B0C0; + + // Act + TWin32GUIPalette const palette = PaletteWithThemeColors(carbon, colors); + + // Assert + CheckEquals(RGB(0x10, 0x20, 0x30), palette.Window, __func__, __LINE__, "#RRGGBB as a COLORREF"); + CheckEquals(RGB(0xA0, 0xB0, 0xC0), palette.TreeStatusPassed, __func__, __LINE__, "a status dot"); + + std::string changed; + + for (TWin32GUIPaletteRole const& role : PaletteRoles()) + { + if (std::string(role.Name) != "Window" && std::string(role.Name) != "TreeStatusPassed" && + palette.*role.Color != carbon.*role.Color) + { + changed += std::string(changed.empty() ? "" : ", ") + role.Name; + } + } + + CheckEmpty(changed, __func__, __LINE__, "the others keep their colors, and an unknown role is left out"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_Palette::Test_TintedCarbonPalette_TintsOnlyTheGraysAndTheAccent() +{ + // Arrange + TWin32GUIPalette const carbon = CarbonPalette(); + + // Act: grays toward blue, the accent green; and no tint, with the accent's own hue. + TWin32GUIPalette const tinted = TintedCarbonPalette(240.0, 0.1, 120.0); + TWin32GUIPalette const untinted = TintedCarbonPalette(240.0, 0.0, 120.0); + + // Assert + CheckTrue(GetBValue(tinted.Window) > GetRValue(tinted.Window) && GetBValue(tinted.Window) > GetGValue(tinted.Window), + __func__, __LINE__, "a dark gray turns blue"); + // HSL's lightness, the highest channel and the lowest added, here blue and red. + CheckTrue(std::abs(GetBValue(tinted.Window) + GetRValue(tinted.Window) - 2 * GetRValue(carbon.Window)) <= 1, + __func__, __LINE__, "keeping its lightness"); + CheckTrue(GetBValue(tinted.WindowText) - GetRValue(tinted.WindowText) < GetBValue(tinted.Window) - + GetRValue(tinted.Window), __func__, __LINE__, "a light one less, so text stays nearly white"); + CheckEquals(RGB(255, 255, 255), tinted.HotText, __func__, __LINE__, "white stays white"); + CheckTrue(GetGValue(tinted.Highlight) > GetRValue(tinted.Highlight) && GetGValue(tinted.Highlight) > + GetBValue(tinted.Highlight), __func__, __LINE__, "the accent takes its own hue"); + CheckEquals(carbon.Window, untinted.Window, __func__, __LINE__, "no tint leaves a gray as it is"); + CheckEquals(carbon.Panel, untinted.Panel, __func__, __LINE__, "every gray"); + + // The colors that say how a test did. + COLORREF TWin32GUIPalette::* const kept[] = { + &TWin32GUIPalette::FailureText, &TWin32GUIPalette::ProgressFailedBottom, &TWin32GUIPalette::ProgressFailedTop, + &TWin32GUIPalette::SkipText, &TWin32GUIPalette::TreeStatusFailed, &TWin32GUIPalette::TreeStatusNotRun, + &TWin32GUIPalette::TreeStatusPassed, &TWin32GUIPalette::TreeStatusRunning, &TWin32GUIPalette::TreeStatusSkipped + }; + bool statusKept = true; + + for (COLORREF TWin32GUIPalette::* const color : kept) + statusKept = statusKept && tinted.*color == carbon.*color; + + CheckTrue(statusKept, __func__, __LINE__, "the status colors stay Carbon's"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +ASW_REGISTER_TEST_GROUP(ASWUnitTests::TTest_ASWUnitTests_Win32GUI_Palette) diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.h b/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.h new file mode 100644 index 0000000..de1e077 --- /dev/null +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_Palette.h @@ -0,0 +1,69 @@ +/* ************************************************************************** +Test_ASWUnitTests_Win32GUI_Palette.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef Test_ASWUnitTests_Win32GUI_PaletteH +#define Test_ASWUnitTests_Win32GUI_PaletteH +//--------------------------------------------------------------------------- +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_Win32GUI_Palette +// +// Exercises the Win32 GUI runner's dark theme colors +// (win32/gui/src/ASWUnitTests_Win32GUI_Palette.h). Compiled by the Windows +// console runners as well as the Win32 GUI, so it runs without the GUI. +///////////////////////////////////////////////////////////////////////////// +class TTest_ASWUnitTests_Win32GUI_Palette : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: // Test methods + void Test_BlendColors_MixesEachChannelInProportion(); + void Test_BuiltInDarkThemes_AreCarbonThenItsTints(); + void Test_BuiltInDarkThemes_KeepTextReadable(); + void Test_CarbonPalette_HasCarbonsColors(); + void Test_CarbonThemeFileText_ReadsBackAsCarbon(); + void Test_FailureAndSkipTextColorOn_AreLighterOnADarkBackground(); + void Test_FindBuiltInDarkTheme_IgnoresCase(); + void Test_IsDarkColor_IsByTheLuma(); + void Test_PaletteRoles_ListEveryMemberOnce(); + void Test_PaletteWithThemeColors_SetsOnlyTheirRoles(); + void Test_TintedCarbonPalette_TintsOnlyTheGraysAndTheAccent(); + +public: + TTest_ASWUnitTests_Win32GUI_Palette(); + ~TTest_ASWUnitTests_Win32GUI_Palette() override; + + void SetUp_Group() override; + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override; + void TearDown_Test(ITestCase& testCase) override; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef Test_ASWUnitTests_Win32GUI_PaletteH diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_System.cpp b/win32/tests/Test_ASWUnitTests_Win32GUI_System.cpp new file mode 100644 index 0000000..1d2b453 --- /dev/null +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_System.cpp @@ -0,0 +1,550 @@ +/* ************************************************************************** +Test_ASWUnitTests_Win32GUI_System.cpp +Author: Anthony S. West - ASW Software + +See header for info. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_Windows.h" +//--------------------------------------------------------------------------- +// Module header +#include "Test_ASWUnitTests_Win32GUI_System.h" +//--------------------------------------------------------------------------- +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Registry.h" +#include "ASWUnitTests_Utils.h" +//--------------------------------------------------------------------------- +#include "ASWUnitTests_Win32GUI_System.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +namespace +{ + +//--------------------------------------------------------------------------- + +bool IsFolder(std::string const& path); +void RemoveFolderTree(std::wstring const& folder); + +//--------------------------------------------------------------------------- +bool IsFolder(std::string const& path) +{ + DWORD const attributes = GetFileAttributesW(UTF8ToWide(path).c_str()); + + return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; +} +//--------------------------------------------------------------------------- +/* + RemoveFolderTree + + Deletes the folder and everything in it, as far as it can. +*/ +void RemoveFolderTree(std::wstring const& folder) +{ + WIN32_FIND_DATAW found; + HANDLE const search = FindFirstFileW((folder + L"\\*").c_str(), &found); + + if (search != INVALID_HANDLE_VALUE) + { + for (bool more = true; more; more = FindNextFileW(search, &found) != 0) + { + std::wstring const name = found.cFileName; + + if (name == L"." || name == L"..") + continue; + + std::wstring const path = folder + L"\\" + name; + + if ((found.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) + RemoveFolderTree(path); + else + DeleteFileW(path.c_str()); + } + + FindClose(search); + } + + RemoveDirectoryW(folder.c_str()); +} +//--------------------------------------------------------------------------- + +} // namespace + +//--------------------------------------------------------------------------- + + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_Win32GUI_System +///////////////////////////////////////////////////////////////////////////// + +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_Win32GUI_System::TTest_ASWUnitTests_Win32GUI_System() + : inherited("ASWUnitTests_Win32GUI_System_Tests"), + m_ScratchFolderCount(0) +{ + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_AdjustLineBreaks_MakesEveryLineBreakCRLF, + "AdjustLineBreaks_MakesEveryLineBreakCRLF"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_CreateFolderPath_CreatesEveryMissingFolder, + "CreateFolderPath_CreatesEveryMissingFolder"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_CreateFolderPath_ThrowsWhenAFileIsInTheWay, + "CreateFolderPath_ThrowsWhenAFileIsInTheWay"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_ExeFileName_IsThisExecutablesNameWithoutItsFolder, + "ExeFileName_IsThisExecutablesNameWithoutItsFolder"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_ExpandFileName_MakesARelativePathAbsolute, + "ExpandFileName_MakesARelativePathAbsolute"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_ExpandFileName_ResolvesDotFolders, + "ExpandFileName_ResolvesDotFolders"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_FileExists_IsTrueOnlyForAFile, "FileExists_IsTrueOnlyForAFile"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_FileNamesInFolder_ListsOnlyTheFiles, + "FileNamesInFolder_ListsOnlyTheFiles"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_GUILayoutFilePath_IsPerExeUnderAppData, + "GUILayoutFilePath_IsPerExeUnderAppData"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_GUISelectionFilePath_IsBesideTheLayoutFile, + "GUISelectionFilePath_IsBesideTheLayoutFile"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_GUIThemesFolderPath_IsSharedBesideTheLayoutFiles, + "GUIThemesFolderPath_IsSharedBesideTheLayoutFiles"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_IsThemeSettingChange_IsTrueForTheAppModeAndHighContrast, + "IsThemeSettingChange_IsTrueForTheAppModeAndHighContrast"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_LoadGUIThemeFromLayoutFile_ReadsWhatSaveWrote, + "LoadGUIThemeFromLayoutFile_ReadsWhatSaveWrote"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_MainIcon_IsThisExecutablesSharedIcon, + "MainIcon_IsThisExecutablesSharedIcon"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_ReadFileBytes_ReadsWhatWriteFileBytesWrote, + "ReadFileBytes_ReadsWhatWriteFileBytesWrote"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_ReadFileBytes_ThrowsForAMissingFile, + "ReadFileBytes_ThrowsForAMissingFile"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_RemoveFile_DeletesOnlyAnExistingFile, + "RemoveFile_DeletesOnlyAnExistingFile"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_WindowText_ConvertsNonASCIIToUTF8, + "WindowText_ConvertsNonASCIIToUTF8"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_WindowsAppsUseDarkTheme_MatchesTheRegistry, + "WindowsAppsUseDarkTheme_MatchesTheRegistry"); + RegisterTest(&TTest_ASWUnitTests_Win32GUI_System::Test_WriteFileBytes_ThrowsWhenTheFolderIsMissing, + "WriteFileBytes_ThrowsWhenTheFolderIsMissing"); +} +//--------------------------------------------------------------------------- +TTest_ASWUnitTests_Win32GUI_System::~TTest_ASWUnitTests_Win32GUI_System() +{ +} +//--------------------------------------------------------------------------- +std::string TTest_ASWUnitTests_Win32GUI_System::ScratchPath(std::string const& name) const +{ + return m_ScratchFolder + "\\" + name; +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::SetUp_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::SetUp_Test(ITestCase& /*testCase*/) +{ + wchar_t tempFolder[MAX_PATH + 1] = {}; + + if (GetTempPathW(MAX_PATH + 1, tempFolder) == 0) + throw std::runtime_error("no temp folder"); + + // e.g. "C:\Users\...\Temp\ASWUnitTests_Win32GUI_System_1234_5_" followed by U+4E2D. Numbered per test, since + // the previous test's folder may outlive its TearDown_Test(): another program with one of its files open, + // such as a virus scanner, keeps it from being deleted. + m_ScratchFolder = WideToUTF8(tempFolder) + "ASWUnitTests_Win32GUI_System_" + std::to_string(GetCurrentProcessId()) + + "_" + std::to_string(++m_ScratchFolderCount) + "_\xE4\xB8\xAD"; + + RemoveFolderTree(UTF8ToWide(m_ScratchFolder)); + + if (CreateDirectoryW(UTF8ToWide(m_ScratchFolder).c_str(), nullptr) == 0) + throw std::runtime_error("couldn't create " + m_ScratchFolder + " (error " + std::to_string(GetLastError()) + + ")"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::TearDown_Group() +{ +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::TearDown_Test(ITestCase& /*testCase*/) +{ + RemoveFolderTree(UTF8ToWide(m_ScratchFolder)); +} +//--------------------------------------------------------------------------- + +// /////// Begin tests after this line /////////////////////// + +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_AdjustLineBreaks_MakesEveryLineBreakCRLF() +{ + // Act, Assert + CheckEquals(std::string("a\r\nb\r\n"), AdjustLineBreaks("a\nb\n"), __func__, __LINE__, "LF becomes CRLF"); + CheckEquals(std::string("a\r\nb"), AdjustLineBreaks("a\r\nb"), __func__, __LINE__, "CRLF stays as it is"); + CheckEquals(std::string("a\r\nb"), AdjustLineBreaks("a\rb"), __func__, __LINE__, "a lone CR becomes CRLF"); + CheckEquals(std::string("\r\n\r\n\r\n"), AdjustLineBreaks("\r\n\n\r"), __func__, __LINE__, + "each break counts once: CRLF, LF, CR"); + CheckEquals(std::string("a\r\n\r\nb"), AdjustLineBreaks("a\n\r\nb"), __func__, __LINE__, + "an LF before a CRLF is a break of its own"); + CheckEmpty(AdjustLineBreaks(std::string()), __func__, __LINE__, "empty stays empty"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_CreateFolderPath_CreatesEveryMissingFolder() +{ + // Arrange: three new levels, one with a non-ASCII name (U+00E9), and both kinds of separator. + std::string const folder = ScratchPath("a\xC3\xA9" "/b\\c"); + + // Act + CreateFolderPath(folder); + + // Assert + CheckTrue(IsFolder(ScratchPath("a\xC3\xA9\\b\\c")), __func__, __LINE__, "every level is created"); + CheckNoThrow([&]() + { + CreateFolderPath(folder); + }, __func__, __LINE__, "an existing folder is fine"); + CheckNoThrow([&]() + { + CreateFolderPath(folder + "\\"); + }, __func__, __LINE__, "so is a trailing separator"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_CreateFolderPath_ThrowsWhenAFileIsInTheWay() +{ + // Arrange + std::string const file = ScratchPath("file"); + WriteFileBytes(file, "x"); + + // Act, Assert + CheckThrows([&]() + { + CreateFolderPath(file + "\\sub"); + }, __func__, __LINE__, "a file where a folder must go"); + CheckThrows([&]() + { + CreateFolderPath(file); + }, __func__, __LINE__, "a file where the folder itself must go"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_ExeFileName_IsThisExecutablesNameWithoutItsFolder() +{ + // Act + std::string const name = ExeFileName(); + + // Assert + AssertFalse(name.empty(), __func__, __LINE__, "has a name"); + CheckTrue(name.find_first_of("\\/:") == std::string::npos, __func__, __LINE__, "has no folder"); + // GetModuleHandleW() finds a loaded module by its file name alone. + CheckTrue(GetModuleHandleW(UTF8ToWide(name).c_str()) == GetModuleHandleW(nullptr), __func__, __LINE__, + "names this process's executable"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_ExpandFileName_MakesARelativePathAbsolute() +{ + // Arrange + wchar_t currentFolder[MAX_PATH + 1] = {}; + DWORD const currentLength = GetCurrentDirectoryW(MAX_PATH + 1, currentFolder); + AssertTrue(currentLength > 0 && currentLength <= MAX_PATH, __func__, __LINE__, "the current folder is known"); + + std::string folder = WideToUTF8(currentFolder); + + if (folder.back() != '\\') + folder += "\\"; // Unless it's a drive's root, such as "C:\". + + // Act + std::string const expanded = ExpandFileName("report\xC3\xA9.xml"); + + // Assert + CheckEquals(folder + "report\xC3\xA9.xml", expanded, __func__, __LINE__, "in the current folder"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_ExpandFileName_ResolvesDotFolders() +{ + // Act + std::string const expanded = ExpandFileName(ScratchPath("sub\xE4\xB8\xAD\\..\\.\\report.xml")); + + // Assert + CheckEquals(ScratchPath("report.xml"), expanded, __func__, __LINE__, "without the dot folders"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_FileExists_IsTrueOnlyForAFile() +{ + // Arrange + std::string const file = ScratchPath("file\xE4\xB8\xAD.txt"); + WriteFileBytes(file, std::string()); + + // Act, Assert + CheckTrue(FileExists(file), __func__, __LINE__, "an empty file exists"); + CheckFalse(FileExists(ScratchPath("missing.txt")), __func__, __LINE__, "a missing file doesn't"); + CheckFalse(FileExists(m_ScratchFolder), __func__, __LINE__, "a folder isn't a file"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_FileNamesInFolder_ListsOnlyTheFiles() +{ + // Arrange: two files, one with a non-ASCII name (U+4E2D), and a folder named like a theme file. + WriteFileBytes(ScratchPath("Ocean.ini"), "x"); + WriteFileBytes(ScratchPath("\xE4\xB8\xAD.ini"), std::string()); + CreateFolderPath(ScratchPath("Folder.ini")); + WriteFileBytes(ScratchPath("Folder.ini\\Inside.ini"), "x"); + + // Act + std::vector names = FileNamesInFolder(m_ScratchFolder); + std::vector const missing = FileNamesInFolder(ScratchPath("Missing")); + + // Assert: both sorted the same way, as where a non-ASCII byte sorts depends on whether char is signed. + std::vector expected = { "Ocean.ini", "\xE4\xB8\xAD.ini" }; + std::sort(expected.begin(), expected.end()); + std::sort(names.begin(), names.end()); + std::string expectedText; + std::string listed; + + for (std::string const& name : expected) + expectedText += name + "\n"; + + for (std::string const& name : names) + listed += name + "\n"; + + CheckEquals(expectedText, listed, __func__, __LINE__, + "the files' names, in UTF-8, without the folder, its subfolder or the files in it"); + CheckTrue(missing.empty(), __func__, __LINE__, "none for a missing folder"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_GUILayoutFilePath_IsPerExeUnderAppData() +{ + // Arrange + std::string const exeName = ExeFileName(); + std::string const expectedName = exeName.substr(0, exeName.find_last_of('.')) + ".ini"; + wchar_t appData[MAX_PATH + 1] = {}; + DWORD const appDataLength = GetEnvironmentVariableW(L"APPDATA", appData, MAX_PATH + 1); + + // Act + std::string const path = GUILayoutFilePath(); + std::string const folder = GUISettingsFolderPath(); + + // Assert + CheckEquals(folder + "\\" + expectedName, path, __func__, __LINE__, "named after this executable, in the folder"); + AssertTrue(appDataLength > 0 && appDataLength <= MAX_PATH, __func__, __LINE__, "%APPDATA% is set"); + CheckEqualsIC(WideToUTF8(appData) + "\\ASWUnitTests", folder, __func__, __LINE__, + "an ASWUnitTests folder under %APPDATA%"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_GUISelectionFilePath_IsBesideTheLayoutFile() +{ + // Arrange + std::string const layoutPath = GUILayoutFilePath(); + + // Act + std::string const selectionPath = GUISelectionFilePath(); + + // Assert + CheckEquals(layoutPath.substr(0, layoutPath.size() - 4) + ".selection", selectionPath, __func__, __LINE__, + "the layout file's path with a .selection extension"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_GUIThemesFolderPath_IsSharedBesideTheLayoutFiles() +{ + // Act and Assert: the same for every executable, unlike the layout file. + CheckEquals(GUISettingsFolderPath() + "\\Themes", GUIThemesFolderPath(), __func__, __LINE__, + "a Themes folder in the settings folder"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_IsThemeSettingChange_IsTrueForTheAppModeAndHighContrast() +{ + // Arrange: WM_SETTINGCHANGE's parameters, as Windows sends them. + wchar_t const appMode[] = L"ImmersiveColorSet"; + wchar_t const other[] = L"Policy"; + + // Act + bool const forAppMode = IsThemeSettingChange(0, reinterpret_cast(appMode)); + bool const forHighContrast = IsThemeSettingChange(SPI_SETHIGHCONTRAST, 0); + bool const forOther = IsThemeSettingChange(0, reinterpret_cast(other)); + bool const forOtherSetting = IsThemeSettingChange(SPI_SETWORKAREA, 0); + + // Assert + CheckTrue(forAppMode, __func__, __LINE__, "the app mode's change"); + CheckTrue(forHighContrast, __func__, __LINE__, "high contrast's change"); + CheckFalse(forOther, __func__, __LINE__, "another area's change"); + CheckFalse(forOtherSetting, __func__, __LINE__, "another setting's change, with no area"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_LoadGUIThemeFromLayoutFile_ReadsWhatSaveWrote() +{ + // Arrange: a layout file in a folder that doesn't exist yet, not this runner's own, which a GUI running these + // tests is using. + std::string const path = ScratchPath("Settings\\Layout.ini"); + + EGUITheme const noFile = LoadGUIThemeFromLayoutFile(path); + std::string const noFileDarkTheme = LoadGUIDarkThemeFromLayoutFile(path); + + // Act + SaveGUIThemeToLayoutFile(path, EGUITheme::Dark); + EGUITheme const dark = LoadGUIThemeFromLayoutFile(path); + + WriteFileBytes(path, "[Window]\r\nMaximized=1\r\n"); + SaveGUIThemeToLayoutFile(path, EGUITheme::Light); + EGUITheme const light = LoadGUIThemeFromLayoutFile(path); + SaveGUIDarkThemeToLayoutFile(path, "Deep \xC3\x89" "cume"); + std::string const darkTheme = LoadGUIDarkThemeFromLayoutFile(path); + std::string const text = ReadFileBytes(path); + + RemoveFile(path); + SaveGUIDarkThemeToLayoutFile(path, "Ocean"); + std::string const darkThemeInNewFile = LoadGUIDarkThemeFromLayoutFile(path); + + // Assert + CheckTrue(noFile == EGUITheme::Windows, __func__, __LINE__, "Windows without a layout file"); + CheckEquals(std::string("Carbon"), noFileDarkTheme, __func__, __LINE__, "and the dark theme Carbon"); + CheckTrue(dark == EGUITheme::Dark, __func__, __LINE__, "the theme saved in a new file, and its new folder"); + CheckTrue(light == EGUITheme::Light, __func__, __LINE__, "the theme saved in an existing file"); + CheckEquals(std::string("Deep \xC3\x89" "cume"), darkTheme, __func__, __LINE__, + "the dark theme saved beside it, in UTF-8"); + CheckEquals(std::string("[Window]\r\nMaximized=1\r\n\r\n[View]\r\nTheme=Light\r\nDarkTheme=Deep \xC3\x89" "cume\r\n" + "\r\n"), text, __func__, __LINE__, "the layout stays"); + CheckEquals(std::string("Ocean"), darkThemeInNewFile, __func__, __LINE__, "the dark theme saved in a new file"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_MainIcon_IsThisExecutablesSharedIcon() +{ + // Act: every runner that compiles this test embeds the ASWUnitTests icon. + HICON const icon = MainIcon(); + HICON const again = MainIcon(); + + // Assert + CheckTrue(icon != nullptr, __func__, __LINE__, "this executable has a MAINICON resource"); + CheckTrue(again == icon, __func__, __LINE__, "the same, shared, icon each time"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_ReadFileBytes_ReadsWhatWriteFileBytesWrote() +{ + // Arrange: a file name with a character outside every ANSI code page but UTF-8's (U+4E2D), and bytes a + // text conversion would change: CRLF and LF line breaks, a NUL, and UTF-8 for U+00E9. + std::string const path = ScratchPath("bytes_\xE4\xB8\xAD.tmp"); + std::string const bytes("a\r\nb\nc\0d\xC3\xA9", 10); + + // Act + WriteFileBytes(path, bytes); + std::string const read = ReadFileBytes(path); + + WriteFileBytes(path, "xy"); + std::string const shorter = ReadFileBytes(path); + + WriteFileBytes(path, std::string()); + std::string const empty = ReadFileBytes(path); + + // Assert + CheckEquals(bytes, read, __func__, __LINE__, "every byte is read back unchanged"); + CheckEquals(std::string("xy"), shorter, __func__, __LINE__, "writing again replaces the whole file"); + CheckEmpty(empty, __func__, __LINE__, "an empty file reads as empty"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_ReadFileBytes_ThrowsForAMissingFile() +{ + // Act, Assert + CheckThrows([&]() + { + ReadFileBytes(ScratchPath("missing.txt")); + }, __func__, __LINE__, "a missing file can't be read"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_RemoveFile_DeletesOnlyAnExistingFile() +{ + // Arrange + std::string const file = ScratchPath("file\xE4\xB8\xAD.txt"); + WriteFileBytes(file, "x"); + + // Act + bool const removed = RemoveFile(file); + bool const exists = FileExists(file); + bool const removedAgain = RemoveFile(file); + + // Assert + CheckTrue(removed, __func__, __LINE__, "an existing file is deleted"); + CheckFalse(exists, __func__, __LINE__, "and is gone"); + CheckFalse(removedAgain, __func__, __LINE__, "a missing file can't be deleted"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_WindowText_ConvertsNonASCIIToUTF8() +{ + // Arrange: a hidden window titled with ASCII, U+00E9, U+4E2D and U+1F600 (a surrogate pair in UTF-16), and + // one with no title. + HWND const titled = CreateWindowExW(0, L"STATIC", L"a\x00E9\x4E2D\xD83D\xDE00", WS_POPUP, 0, 0, 10, 10, nullptr, + nullptr, GetModuleHandleW(nullptr), nullptr); + HWND const untitled = CreateWindowExW(0, L"STATIC", L"", WS_POPUP, 0, 0, 10, 10, nullptr, nullptr, + GetModuleHandleW(nullptr), nullptr); + + // Act + std::string const titledText = titled != nullptr ? WindowText(titled) : std::string("(no window)"); + std::string const untitledText = untitled != nullptr ? WindowText(untitled) : std::string("(no window)"); + + if (titled != nullptr) + DestroyWindow(titled); + + if (untitled != nullptr) + DestroyWindow(untitled); + + // Assert + CheckEquals(std::string("a\xC3\xA9\xE4\xB8\xAD\xF0\x9F\x98\x80"), titledText, __func__, __LINE__, + "every character converted"); + CheckEquals(std::string(), untitledText, __func__, __LINE__, "no text"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_WindowsAppsUseDarkTheme_MatchesTheRegistry() +{ + // Arrange: the setting read another way, with RegQueryValueExW(). This machine's setting can't be changed + // here, so only agreement with it can be checked. + bool expected = false; + HKEY key = nullptr; + + if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", 0, + KEY_READ, &key) == ERROR_SUCCESS) + { + DWORD value = 1; + DWORD size = sizeof(value); + DWORD type = 0; + + if (RegQueryValueExW(key, L"AppsUseLightTheme", nullptr, &type, reinterpret_cast(&value), &size) == + ERROR_SUCCESS && type == REG_DWORD) + { + expected = value == 0; + } + + RegCloseKey(key); + } + + // Act + bool const dark = WindowsAppsUseDarkTheme(); + + // Assert + CheckEquals(expected, dark, __func__, __LINE__, "dark when AppsUseLightTheme is 0"); +} +//--------------------------------------------------------------------------- +void TTest_ASWUnitTests_Win32GUI_System::Test_WriteFileBytes_ThrowsWhenTheFolderIsMissing() +{ + // Act, Assert + CheckThrows([&]() + { + WriteFileBytes(ScratchPath("missing\\file.txt"), "x"); + }, __func__, __LINE__, "the folder isn't created"); + CheckFalse(IsFolder(ScratchPath("missing")), __func__, __LINE__, "nor left behind"); +} +//--------------------------------------------------------------------------- + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +ASW_REGISTER_TEST_GROUP(ASWUnitTests::TTest_ASWUnitTests_Win32GUI_System) diff --git a/win32/tests/Test_ASWUnitTests_Win32GUI_System.h b/win32/tests/Test_ASWUnitTests_Win32GUI_System.h new file mode 100644 index 0000000..0c83274 --- /dev/null +++ b/win32/tests/Test_ASWUnitTests_Win32GUI_System.h @@ -0,0 +1,89 @@ +/* ************************************************************************** +Test_ASWUnitTests_Win32GUI_System.h +Author: Anthony S. West - ASW Software + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +//--------------------------------------------------------------------------- +#ifndef Test_ASWUnitTests_Win32GUI_SystemH +#define Test_ASWUnitTests_Win32GUI_SystemH +//--------------------------------------------------------------------------- +#include +//--------------------------------------------------------------------------- +#include "ASWUnitTests_TestBase.h" +//--------------------------------------------------------------------------- + +namespace ASWUnitTests +{ + +///////////////////////////////////////////////////////////////////////////// +// TTest_ASWUnitTests_Win32GUI_System +// +// Exercises the Win32 GUI runner's process and system access +// (win32/gui/src/ASWUnitTests_Win32GUI_System.h). Compiled by the Windows +// console runners as well as the Win32 GUI, so it runs without the GUI. +///////////////////////////////////////////////////////////////////////////// +class TTest_ASWUnitTests_Win32GUI_System : public TTestGroupBase +{ +private: + typedef TTestGroupBase inherited; + +private: + // A folder of its own for each test, in the temp folder, with a name outside every ANSI code page; deleted + // with its content after the test. + std::string m_ScratchFolder; + // The scratch folders made so far, which numbers each test's own; see SetUp_Test(). + unsigned int m_ScratchFolderCount; + + std::string ScratchPath(std::string const& name) const; + +private: // Test methods + void Test_AdjustLineBreaks_MakesEveryLineBreakCRLF(); + void Test_CreateFolderPath_CreatesEveryMissingFolder(); + void Test_CreateFolderPath_ThrowsWhenAFileIsInTheWay(); + void Test_ExeFileName_IsThisExecutablesNameWithoutItsFolder(); + void Test_ExpandFileName_MakesARelativePathAbsolute(); + void Test_ExpandFileName_ResolvesDotFolders(); + void Test_FileExists_IsTrueOnlyForAFile(); + void Test_FileNamesInFolder_ListsOnlyTheFiles(); + void Test_GUILayoutFilePath_IsPerExeUnderAppData(); + void Test_GUISelectionFilePath_IsBesideTheLayoutFile(); + void Test_GUIThemesFolderPath_IsSharedBesideTheLayoutFiles(); + void Test_IsThemeSettingChange_IsTrueForTheAppModeAndHighContrast(); + void Test_LoadGUIThemeFromLayoutFile_ReadsWhatSaveWrote(); + void Test_MainIcon_IsThisExecutablesSharedIcon(); + void Test_ReadFileBytes_ReadsWhatWriteFileBytesWrote(); + void Test_ReadFileBytes_ThrowsForAMissingFile(); + void Test_RemoveFile_DeletesOnlyAnExistingFile(); + void Test_WindowText_ConvertsNonASCIIToUTF8(); + void Test_WindowsAppsUseDarkTheme_MatchesTheRegistry(); + void Test_WriteFileBytes_ThrowsWhenTheFolderIsMissing(); + +public: + TTest_ASWUnitTests_Win32GUI_System(); + ~TTest_ASWUnitTests_Win32GUI_System() override; + + void SetUp_Group() override; + void SetUp_Test(ITestCase& testCase) override; + void TearDown_Group() override; + void TearDown_Test(ITestCase& testCase) override; +}; + +} // namespace ASWUnitTests + +//--------------------------------------------------------------------------- +#endif // #ifndef Test_ASWUnitTests_Win32GUI_SystemH