diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 86297485..b34d32e6 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -2,9 +2,9 @@ name: Build
on:
push:
- branches: [master]
+ branches: [ master ]
pull_request:
- branches: [master]
+ branches: [ master ]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -21,107 +21,110 @@ jobs:
fail-fast: false
matrix:
include:
- - arch: x86-64
- runner: ubuntu-24.04
- deb_arch: amd64
- build_jobs: $(nproc)
- - arch: arm64
- runner: ubuntu-24.04-arm
- deb_arch: arm64
- build_jobs: 2
+ - arch: x86-64
+ runner: ubuntu-24.04
+ deb_arch: amd64
+ build_jobs: $(nproc)
+ - arch: arm64
+ runner: ubuntu-24.04-arm
+ deb_arch: arm64
+ build_jobs: 2
steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- submodules: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: true
- - name: Cache apt packages
- uses: actions/cache@v4
- with:
- path: /var/cache/apt/archives/*.deb
- key: apt-${{ matrix.arch }}-${{ hashFiles('.github/workflows/build.yml') }}
- restore-keys: apt-${{ matrix.arch }}-
+ - name: Cache apt packages
+ uses: actions/cache@v4
+ with:
+ path: /var/cache/apt/archives/*.deb
+ key: apt-${{ matrix.arch }}-${{ hashFiles('.github/workflows/build.yml') }}
+ restore-keys: apt-${{ matrix.arch }}-
- - name: Install dependencies
- run: |
- sudo apt-get update
- sudo apt-get install -y \
- cmake \
- build-essential \
- pkg-config \
- libsdl2-dev \
- libglew-dev \
- libglm-dev \
- libssl-dev \
- zlib1g-dev \
- libvulkan-dev \
- vulkan-tools \
- glslc \
- libavformat-dev \
- libavcodec-dev \
- libswscale-dev \
- libavutil-dev \
- libunicorn-dev \
- libx11-dev
- sudo apt-get install -y libstorm-dev || true
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ cmake \
+ build-essential \
+ pkg-config \
+ libsdl2-dev \
+ libglew-dev \
+ libglm-dev \
+ libssl-dev \
+ zlib1g-dev \
+ libvulkan-dev \
+ vulkan-tools \
+ glslc \
+ libavformat-dev \
+ libavcodec-dev \
+ libswscale-dev \
+ libavutil-dev \
+ libunicorn-dev \
+ libx11-dev
+ sudo apt-get install -y libstorm-dev || true
- - name: Fetch AMD FSR2 SDK
- run: |
- rm -rf extern/FidelityFX-FSR2
- git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
- rm -rf extern/FidelityFX-SDK
- git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
+ - name: Fetch AMD FSR2 SDK
+ run: |
+ rm -rf extern/FidelityFX-FSR2
+ git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
+ rm -rf extern/FidelityFX-SDK
+ git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
- - name: Check AMD FSR2 Vulkan permutation headers
- run: |
- set -euo pipefail
- SDK_DIR="$PWD/extern/FidelityFX-FSR2"
- OUT_DIR="$SDK_DIR/src/ffx-fsr2-api/vk/shaders"
- if [ -f "$OUT_DIR/ffx_fsr2_accumulate_pass_permutations.h" ]; then
- echo "AMD FSR2 Vulkan permutation headers detected."
- else
- echo "AMD FSR2 Vulkan permutation headers not found in SDK checkout."
- echo "WoWee CMake will bootstrap vendored headers."
- fi
+ - name: Check AMD FSR2 Vulkan permutation headers
+ run: |
+ set -euo pipefail
+ SDK_DIR="$PWD/extern/FidelityFX-FSR2"
+ OUT_DIR="$SDK_DIR/src/ffx-fsr2-api/vk/shaders"
+ if [ -f "$OUT_DIR/ffx_fsr2_accumulate_pass_permutations.h" ]; then
+ echo "AMD FSR2 Vulkan permutation headers detected."
+ else
+ echo "AMD FSR2 Vulkan permutation headers not found in SDK checkout."
+ echo "WoWee CMake will bootstrap vendored headers."
+ fi
- - name: Check FidelityFX-SDK Kits framegen headers
- run: |
- set -euo pipefail
- KITS_DIR="$PWD/extern/FidelityFX-SDK/Kits/FidelityFX"
- test -f "$KITS_DIR/upscalers/fsr3/include/ffx_fsr3upscaler.h"
- test -f "$KITS_DIR/framegeneration/fsr3/include/ffx_frameinterpolation.h"
- test -f "$KITS_DIR/framegeneration/fsr3/include/ffx_opticalflow.h"
- test -f "$KITS_DIR/backend/vk/ffx_vk.h"
- echo "FidelityFX-SDK Kits framegen headers detected."
+ - name: Check FidelityFX-SDK Kits framegen headers
+ run: |
+ set -euo pipefail
+ KITS_DIR="$PWD/extern/FidelityFX-SDK/Kits/FidelityFX"
+ test -f "$KITS_DIR/upscalers/fsr3/include/ffx_fsr3upscaler.h"
+ test -f "$KITS_DIR/framegeneration/fsr3/include/ffx_frameinterpolation.h"
+ test -f "$KITS_DIR/framegeneration/fsr3/include/ffx_opticalflow.h"
+ test -f "$KITS_DIR/backend/vk/ffx_vk.h"
+ echo "FidelityFX-SDK Kits framegen headers detected."
- - name: Configure (AMD ON)
- run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DWOWEE_ENABLE_AMD_FSR2=ON
+ - name: Configure (AMD ON)
+ run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DWOWEE_ENABLE_AMD_FSR2=ON -DWOWEE_BUILD_TESTS=ON
- - name: Assert AMD FSR2 target
- run: cmake --build build --target wowee_fsr2_amd_vk --parallel ${{ matrix.build_jobs }}
+ - name: Assert AMD FSR2 target
+ run: cmake --build build --target wowee_fsr2_amd_vk --parallel ${{ matrix.build_jobs }}
- - name: Assert AMD FSR3 framegen probe target (if present)
- run: |
- set -euo pipefail
- if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
- cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel ${{ matrix.build_jobs }}
- else
- echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
- fi
+ - name: Assert AMD FSR3 framegen probe target (if present)
+ run: |
+ set -euo pipefail
+ if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
+ cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel ${{ matrix.build_jobs }}
+ else
+ echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
+ fi
- - name: Build
- run: cmake --build build --parallel ${{ matrix.build_jobs }}
+ - name: Build
+ run: cmake --build build --parallel ${{ matrix.build_jobs }}
- - name: Package (DEB)
- run: cd build && cpack -G DEB
+ - name: Run tests
+ run: cd build && ctest --output-on-failure
- - name: Upload DEB
- uses: actions/upload-artifact@v4
- with:
- name: wowee-linux-${{ matrix.arch }}-deb
- path: build/wowee-*.deb
- if-no-files-found: error
+ - name: Package (DEB)
+ run: cd build && cpack -G DEB
+
+ - name: Upload DEB
+ uses: actions/upload-artifact@v4
+ with:
+ name: wowee-linux-${{ matrix.arch }}-deb
+ path: build/wowee-*.deb
+ if-no-files-found: error
build-macos:
name: Build (macOS ${{ matrix.arch }})
@@ -130,325 +133,329 @@ jobs:
fail-fast: false
matrix:
include:
- - arch: arm64
- runner: macos-15
+ - arch: arm64
+ runner: macos-15
steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- submodules: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: true
- - name: Install dependencies
- run: |
- brew install cmake pkg-config sdl2 glew glm openssl@3 zlib ffmpeg unicorn \
- stormlib vulkan-loader vulkan-headers shaderc dylibbundler || true
- # dylibbundler may not be in all brew mirrors; install separately to not block others
- brew install dylibbundler 2>/dev/null || true
+ - name: Install dependencies
+ run: |
+ brew install cmake pkg-config sdl2 glew glm openssl@3 zlib ffmpeg unicorn \
+ stormlib vulkan-loader vulkan-headers shaderc dylibbundler || true
+ # dylibbundler may not be in all brew mirrors; install separately to not block others
+ brew install dylibbundler 2>/dev/null || true
- - name: Fetch AMD FSR2 SDK
- run: |
- rm -rf extern/FidelityFX-FSR2
- git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
- rm -rf extern/FidelityFX-SDK
- git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
+ - name: Fetch AMD FSR2 SDK
+ run: |
+ rm -rf extern/FidelityFX-FSR2
+ git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
+ rm -rf extern/FidelityFX-SDK
+ git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
- - name: Configure
- run: |
- BREW=$(brew --prefix)
- export PKG_CONFIG_PATH="$BREW/lib/pkgconfig:$(brew --prefix ffmpeg)/lib/pkgconfig:$(brew --prefix openssl@3)/lib/pkgconfig:$(brew --prefix vulkan-loader)/lib/pkgconfig:$(brew --prefix shaderc)/lib/pkgconfig"
- cmake -S . -B build \
- -DCMAKE_BUILD_TYPE=Release \
- -DCMAKE_PREFIX_PATH="$BREW" \
- -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" \
- -DWOWEE_ENABLE_AMD_FSR2=ON
+ - name: Configure
+ run: |
+ BREW=$(brew --prefix)
+ export PKG_CONFIG_PATH="$BREW/lib/pkgconfig:$(brew --prefix ffmpeg)/lib/pkgconfig:$(brew --prefix openssl@3)/lib/pkgconfig:$(brew --prefix vulkan-loader)/lib/pkgconfig:$(brew --prefix shaderc)/lib/pkgconfig"
+ cmake -S . -B build \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DCMAKE_PREFIX_PATH="$BREW" \
+ -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" \
+ -DWOWEE_ENABLE_AMD_FSR2=ON \
+ -DWOWEE_BUILD_TESTS=ON
- - name: Assert AMD FSR2 target
- run: cmake --build build --target wowee_fsr2_amd_vk --parallel $(sysctl -n hw.logicalcpu)
+ - name: Assert AMD FSR2 target
+ run: cmake --build build --target wowee_fsr2_amd_vk --parallel $(sysctl -n hw.logicalcpu)
- - name: Assert AMD FSR3 framegen probe target (if present)
- run: |
- if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
- cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel $(sysctl -n hw.logicalcpu)
- else
- echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
+ - name: Assert AMD FSR3 framegen probe target (if present)
+ run: |
+ if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
+ cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel $(sysctl -n hw.logicalcpu)
+ else
+ echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
+ fi
+
+ - name: Build
+ run: cmake --build build --parallel $(sysctl -n hw.logicalcpu)
+
+ - name: Run tests
+ run: cd build && ctest --output-on-failure
+
+ - name: Create .app bundle
+ run: |
+ mkdir -p Wowee.app/Contents/{MacOS,Frameworks,Resources}
+
+ # Wrapper launch script — cd to MacOS/ so ./assets/ resolves correctly
+ printf '#!/bin/bash\ncd "$(dirname "$0")"\nexec ./wowee_bin "$@"\n' \
+ > Wowee.app/Contents/MacOS/wowee
+ chmod +x Wowee.app/Contents/MacOS/wowee
+
+ # Actual binary
+ cp build/bin/wowee Wowee.app/Contents/MacOS/wowee_bin
+
+ # Assets (exclude proprietary music)
+ rsync -a --exclude='Original Music' build/bin/assets/ \
+ Wowee.app/Contents/MacOS/assets/
+
+ # Bundle dylibs (if dylibbundler available)
+ if command -v dylibbundler &>/dev/null; then
+ dylibbundler -od -b \
+ -x Wowee.app/Contents/MacOS/wowee_bin \
+ -d Wowee.app/Contents/Frameworks/ \
+ -p @executable_path/../Frameworks/
+ fi
+
+ # dylibbundler may miss Homebrew's Vulkan loader on some runner images.
+ # Copy all vulkan-loader dylib names so wowee_bin can resolve whichever
+ # install_name it was linked against (e.g. libvulkan.1.4.341.dylib).
+ VULKAN_LIB_DIR="$(brew --prefix vulkan-loader)/lib"
+ for lib in "${VULKAN_LIB_DIR}"/libvulkan*.dylib; do
+ [ -e "${lib}" ] || continue
+ cp -f "${lib}" Wowee.app/Contents/Frameworks/
+ done
+
+ if ! ls Wowee.app/Contents/Frameworks/libvulkan*.dylib >/dev/null 2>&1; then
+ echo "Missing Vulkan loader dylib(s) in app bundle Frameworks/" >&2
+ exit 1
+ fi
+
+ # Info.plist
+ cat > Wowee.app/Contents/Info.plist << 'EOF'
+
+
+
+ CFBundleExecutablewowee
+ CFBundleIdentifiercom.wowee.app
+ CFBundleNameWowee
+ CFBundleVersion1.0.0
+ CFBundleShortVersionString1.0.0
+ CFBundlePackageTypeAPPL
+
+ EOF
+
+ # Ad-hoc codesign (allows running on the local machine)
+ codesign --force --deep --sign - Wowee.app
+
+ - name: Create DMG
+ run: |
+ set -euo pipefail
+ rm -f Wowee.dmg
+ # CI runners can occasionally leave a mounted volume around; detach if present.
+ if [ -d "/Volumes/Wowee" ]; then
+ hdiutil detach "/Volumes/Wowee" -force || true
+ sleep 2
+ fi
+
+ ok=0
+ for attempt in 1 2 3 4 5; do
+ if hdiutil create -volname Wowee -srcfolder Wowee.app -ov -format UDZO Wowee.dmg; then
+ ok=1
+ break
fi
-
- - name: Build
- run: cmake --build build --parallel $(sysctl -n hw.logicalcpu)
-
- - name: Create .app bundle
- run: |
- mkdir -p Wowee.app/Contents/{MacOS,Frameworks,Resources}
-
- # Wrapper launch script — cd to MacOS/ so ./assets/ resolves correctly
- printf '#!/bin/bash\ncd "$(dirname "$0")"\nexec ./wowee_bin "$@"\n' \
- > Wowee.app/Contents/MacOS/wowee
- chmod +x Wowee.app/Contents/MacOS/wowee
-
- # Actual binary
- cp build/bin/wowee Wowee.app/Contents/MacOS/wowee_bin
-
- # Assets (exclude proprietary music)
- rsync -a --exclude='Original Music' build/bin/assets/ \
- Wowee.app/Contents/MacOS/assets/
-
- # Bundle dylibs (if dylibbundler available)
- if command -v dylibbundler &>/dev/null; then
- dylibbundler -od -b \
- -x Wowee.app/Contents/MacOS/wowee_bin \
- -d Wowee.app/Contents/Frameworks/ \
- -p @executable_path/../Frameworks/
- fi
-
- # dylibbundler may miss Homebrew's Vulkan loader on some runner images.
- # Copy all vulkan-loader dylib names so wowee_bin can resolve whichever
- # install_name it was linked against (e.g. libvulkan.1.4.341.dylib).
- VULKAN_LIB_DIR="$(brew --prefix vulkan-loader)/lib"
- for lib in "${VULKAN_LIB_DIR}"/libvulkan*.dylib; do
- [ -e "${lib}" ] || continue
- cp -f "${lib}" Wowee.app/Contents/Frameworks/
- done
-
- if ! ls Wowee.app/Contents/Frameworks/libvulkan*.dylib >/dev/null 2>&1; then
- echo "Missing Vulkan loader dylib(s) in app bundle Frameworks/" >&2
- exit 1
- fi
-
- # Info.plist
- cat > Wowee.app/Contents/Info.plist << 'EOF'
-
-
-
- CFBundleExecutablewowee
- CFBundleIdentifiercom.wowee.app
- CFBundleNameWowee
- CFBundleVersion1.0.0
- CFBundleShortVersionString1.0.0
- CFBundlePackageTypeAPPL
-
- EOF
-
- # Ad-hoc codesign (allows running on the local machine)
- codesign --force --deep --sign - Wowee.app
-
- - name: Create DMG
- run: |
- set -euo pipefail
- rm -f Wowee.dmg
- # CI runners can occasionally leave a mounted volume around; detach if present.
+ echo "hdiutil create failed on attempt ${attempt}; retrying..."
if [ -d "/Volumes/Wowee" ]; then
hdiutil detach "/Volumes/Wowee" -force || true
- sleep 2
fi
+ sleep 3
+ done
- ok=0
- for attempt in 1 2 3 4 5; do
- if hdiutil create -volname Wowee -srcfolder Wowee.app -ov -format UDZO Wowee.dmg; then
- ok=1
- break
- fi
- echo "hdiutil create failed on attempt ${attempt}; retrying..."
- if [ -d "/Volumes/Wowee" ]; then
- hdiutil detach "/Volumes/Wowee" -force || true
- fi
- sleep 3
- done
+ if [ "$ok" -ne 1 ] || [ ! -f Wowee.dmg ]; then
+ echo "Failed to create Wowee.dmg after retries."
+ exit 1
+ fi
- if [ "$ok" -ne 1 ] || [ ! -f Wowee.dmg ]; then
- echo "Failed to create Wowee.dmg after retries."
- exit 1
- fi
-
- - name: Upload DMG
- uses: actions/upload-artifact@v4
- with:
- name: wowee-macos-${{ matrix.arch }}-dmg
- path: Wowee.dmg
- if-no-files-found: error
+ - name: Upload DMG
+ uses: actions/upload-artifact@v4
+ with:
+ name: wowee-macos-${{ matrix.arch }}-dmg
+ path: Wowee.dmg
+ if-no-files-found: error
build-windows-arm:
name: Build (windows-arm64)
runs-on: windows-11-arm
steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- submodules: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: true
- - name: Set up MSYS2
- uses: msys2/setup-msys2@v2
- with:
- msystem: CLANGARM64
- update: true
- install: >-
- mingw-w64-clang-aarch64-cmake
- mingw-w64-clang-aarch64-clang
- mingw-w64-clang-aarch64-ninja
- mingw-w64-clang-aarch64-pkgconf
- mingw-w64-clang-aarch64-SDL2
- mingw-w64-clang-aarch64-glew
- mingw-w64-clang-aarch64-glm
- mingw-w64-clang-aarch64-openssl
- mingw-w64-clang-aarch64-zlib
- mingw-w64-clang-aarch64-ffmpeg
- mingw-w64-clang-aarch64-unicorn
- mingw-w64-clang-aarch64-vulkan-loader
- mingw-w64-clang-aarch64-vulkan-headers
- mingw-w64-clang-aarch64-shaderc
- git
+ - name: Set up MSYS2
+ uses: msys2/setup-msys2@v2
+ with:
+ msystem: CLANGARM64
+ update: true
+ install: >-
+ mingw-w64-clang-aarch64-cmake
+ mingw-w64-clang-aarch64-clang
+ mingw-w64-clang-aarch64-ninja
+ mingw-w64-clang-aarch64-pkgconf
+ mingw-w64-clang-aarch64-SDL2
+ mingw-w64-clang-aarch64-glew
+ mingw-w64-clang-aarch64-glm
+ mingw-w64-clang-aarch64-openssl
+ mingw-w64-clang-aarch64-zlib
+ mingw-w64-clang-aarch64-ffmpeg
+ mingw-w64-clang-aarch64-unicorn
+ mingw-w64-clang-aarch64-vulkan-loader
+ mingw-w64-clang-aarch64-vulkan-headers
+ mingw-w64-clang-aarch64-shaderc
+ git
- - name: Build StormLib from source
- shell: msys2 {0}
- run: |
- git clone --depth 1 https://github.com/ladislav-zezula/StormLib.git /tmp/StormLib
- # Disable x86 inline asm in bundled libtomcrypt (bswapl/movl) —
- # __MINGW32__ is defined on CLANGARM64 which incorrectly enables x86 asm
- cmake -S /tmp/StormLib -B /tmp/StormLib/build -G Ninja \
- -DCMAKE_BUILD_TYPE=Release \
- -DCMAKE_INSTALL_PREFIX="$MINGW_PREFIX" \
- -DBUILD_SHARED_LIBS=OFF \
- -DCMAKE_C_FLAGS="-DLTC_NO_BSWAP" \
- -DCMAKE_CXX_FLAGS="-DLTC_NO_BSWAP"
- cmake --build /tmp/StormLib/build --parallel $(nproc)
- cmake --install /tmp/StormLib/build
+ - name: Build StormLib from source
+ shell: msys2 {0}
+ run: |
+ git clone --depth 1 https://github.com/ladislav-zezula/StormLib.git /tmp/StormLib
+ # Disable x86 inline asm in bundled libtomcrypt (bswapl/movl) —
+ # __MINGW32__ is defined on CLANGARM64 which incorrectly enables x86 asm
+ cmake -S /tmp/StormLib -B /tmp/StormLib/build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DCMAKE_INSTALL_PREFIX="$MINGW_PREFIX" \
+ -DBUILD_SHARED_LIBS=OFF \
+ -DCMAKE_C_FLAGS="-DLTC_NO_BSWAP" \
+ -DCMAKE_CXX_FLAGS="-DLTC_NO_BSWAP"
+ cmake --build /tmp/StormLib/build --parallel $(nproc)
+ cmake --install /tmp/StormLib/build
- - name: Fetch AMD FSR2 SDK
- shell: msys2 {0}
- run: |
- rm -rf extern/FidelityFX-FSR2
- git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
- rm -rf extern/FidelityFX-SDK
- git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
+ - name: Fetch AMD FSR2 SDK
+ shell: msys2 {0}
+ run: |
+ rm -rf extern/FidelityFX-FSR2
+ git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
+ rm -rf extern/FidelityFX-SDK
+ git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
- - name: Configure
- shell: msys2 {0}
- run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DWOWEE_ENABLE_AMD_FSR2=ON
+ - name: Configure
+ shell: msys2 {0}
+ run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DWOWEE_ENABLE_AMD_FSR2=ON
- - name: Assert AMD FSR2 target
- shell: msys2 {0}
- run: cmake --build build --target wowee_fsr2_amd_vk --parallel $(nproc)
+ - name: Assert AMD FSR2 target
+ shell: msys2 {0}
+ run: cmake --build build --target wowee_fsr2_amd_vk --parallel $(nproc)
- - name: Assert AMD FSR3 framegen probe target (if present)
- shell: msys2 {0}
- run: |
- if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
- cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel $(nproc)
- else
- echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
- fi
+ - name: Assert AMD FSR3 framegen probe target (if present)
+ shell: msys2 {0}
+ run: |
+ if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
+ cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel $(nproc)
+ else
+ echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
+ fi
- - name: Build
- shell: msys2 {0}
- run: cmake --build build --parallel $(nproc)
+ - name: Build
+ shell: msys2 {0}
+ run: cmake --build build --parallel $(nproc)
- - name: Bundle DLLs
- shell: msys2 {0}
- run: |
- ldd build/bin/wowee.exe \
- | awk '/=> \// { print $3 }' \
- | grep -iv '^/c/Windows' \
- | xargs -I{} sh -c 'cp -n "{}" build/bin/ 2>/dev/null || true'
+ - name: Bundle DLLs
+ shell: msys2 {0}
+ run: |
+ ldd build/bin/wowee.exe \
+ | awk '/=> \// { print $3 }' \
+ | grep -iv '^/c/Windows' \
+ | xargs -I{} sh -c 'cp -n "{}" build/bin/ 2>/dev/null || true'
- - name: Package (ZIP)
- shell: msys2 {0}
- run: cd build && cpack -G ZIP
+ - name: Package (ZIP)
+ shell: msys2 {0}
+ run: cd build && cpack -G ZIP
- - name: Upload ZIP
- uses: actions/upload-artifact@v4
- with:
- name: wowee-windows-arm64-zip
- path: build/wowee-*.zip
- if-no-files-found: error
+ - name: Upload ZIP
+ uses: actions/upload-artifact@v4
+ with:
+ name: wowee-windows-arm64-zip
+ path: build/wowee-*.zip
+ if-no-files-found: error
build-windows:
name: Build (windows-x86-64)
runs-on: windows-latest
steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- submodules: true
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: true
- - name: Set up MSYS2
- uses: msys2/setup-msys2@v2
- with:
- msystem: MINGW64
- update: false
- install: >-
- mingw-w64-x86_64-cmake
- mingw-w64-x86_64-gcc
- mingw-w64-x86_64-ninja
- mingw-w64-x86_64-pkgconf
- mingw-w64-x86_64-SDL2
- mingw-w64-x86_64-glew
- mingw-w64-x86_64-glm
- mingw-w64-x86_64-openssl
- mingw-w64-x86_64-zlib
- mingw-w64-x86_64-ffmpeg
- mingw-w64-x86_64-unicorn
- mingw-w64-x86_64-vulkan-loader
- mingw-w64-x86_64-vulkan-headers
- mingw-w64-x86_64-shaderc
- mingw-w64-x86_64-nsis
- git
+ - name: Set up MSYS2
+ uses: msys2/setup-msys2@v2
+ with:
+ msystem: MINGW64
+ update: false
+ install: >-
+ mingw-w64-x86_64-cmake
+ mingw-w64-x86_64-gcc
+ mingw-w64-x86_64-ninja
+ mingw-w64-x86_64-pkgconf
+ mingw-w64-x86_64-SDL2
+ mingw-w64-x86_64-glew
+ mingw-w64-x86_64-glm
+ mingw-w64-x86_64-openssl
+ mingw-w64-x86_64-zlib
+ mingw-w64-x86_64-ffmpeg
+ mingw-w64-x86_64-unicorn
+ mingw-w64-x86_64-vulkan-loader
+ mingw-w64-x86_64-vulkan-headers
+ mingw-w64-x86_64-shaderc
+ mingw-w64-x86_64-nsis
+ git
- - name: Build StormLib from source
- shell: msys2 {0}
- run: |
- git clone --depth 1 https://github.com/ladislav-zezula/StormLib.git /tmp/StormLib
- cmake -S /tmp/StormLib -B /tmp/StormLib/build -G Ninja \
- -DCMAKE_BUILD_TYPE=Release \
- -DCMAKE_INSTALL_PREFIX="$MINGW_PREFIX" \
- -DBUILD_SHARED_LIBS=OFF
- cmake --build /tmp/StormLib/build --parallel $(nproc)
- cmake --install /tmp/StormLib/build
+ - name: Build StormLib from source
+ shell: msys2 {0}
+ run: |
+ git clone --depth 1 https://github.com/ladislav-zezula/StormLib.git /tmp/StormLib
+ cmake -S /tmp/StormLib -B /tmp/StormLib/build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DCMAKE_INSTALL_PREFIX="$MINGW_PREFIX" \
+ -DBUILD_SHARED_LIBS=OFF
+ cmake --build /tmp/StormLib/build --parallel $(nproc)
+ cmake --install /tmp/StormLib/build
- - name: Fetch AMD FSR2 SDK
- shell: msys2 {0}
- run: |
- rm -rf extern/FidelityFX-FSR2
- git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
- rm -rf extern/FidelityFX-SDK
- git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
+ - name: Fetch AMD FSR2 SDK
+ shell: msys2 {0}
+ run: |
+ rm -rf extern/FidelityFX-FSR2
+ git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
+ rm -rf extern/FidelityFX-SDK
+ git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
- - name: Configure
- shell: msys2 {0}
- run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DWOWEE_ENABLE_AMD_FSR2=ON
+ - name: Configure
+ shell: msys2 {0}
+ run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DWOWEE_ENABLE_AMD_FSR2=ON
- - name: Assert AMD FSR2 target
- shell: msys2 {0}
- run: cmake --build build --target wowee_fsr2_amd_vk --parallel $(nproc)
+ - name: Assert AMD FSR2 target
+ shell: msys2 {0}
+ run: cmake --build build --target wowee_fsr2_amd_vk --parallel $(nproc)
- - name: Assert AMD FSR3 framegen probe target (if present)
- shell: msys2 {0}
- run: |
- if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
- cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel $(nproc)
- else
- echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
- fi
+ - name: Assert AMD FSR3 framegen probe target (if present)
+ shell: msys2 {0}
+ run: |
+ if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
+ cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel $(nproc)
+ else
+ echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
+ fi
- - name: Build
- shell: msys2 {0}
- run: cmake --build build --parallel $(nproc)
+ - name: Build
+ shell: msys2 {0}
+ run: cmake --build build --parallel $(nproc)
- - name: Bundle DLLs
- shell: msys2 {0}
- run: |
- ldd build/bin/wowee.exe \
- | awk '/=> \// { print $3 }' \
- | grep -iv '^/c/Windows' \
- | xargs -I{} sh -c 'cp -n "{}" build/bin/ 2>/dev/null || true'
+ - name: Bundle DLLs
+ shell: msys2 {0}
+ run: |
+ ldd build/bin/wowee.exe \
+ | awk '/=> \// { print $3 }' \
+ | grep -iv '^/c/Windows' \
+ | xargs -I{} sh -c 'cp -n "{}" build/bin/ 2>/dev/null || true'
- - name: Package (NSIS)
- shell: msys2 {0}
- run: cd build && cpack -G NSIS
+ - name: Package (NSIS)
+ shell: msys2 {0}
+ run: cd build && cpack -G NSIS
- - name: Upload installer
- uses: actions/upload-artifact@v4
- with:
- name: wowee-windows-x86-64-installer
- path: build/wowee-*.exe
- if-no-files-found: error
+ - name: Upload installer
+ uses: actions/upload-artifact@v4
+ with:
+ name: wowee-windows-x86-64-installer
+ path: build/wowee-*.exe
+ if-no-files-found: error
diff --git a/.gitignore b/.gitignore
index b06d404c..3f84b1f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# Build directories
build/
+build_asan/
build-debug/
build-sanitize/
bin/
@@ -63,6 +64,7 @@ cscope.out
# External dependencies (except submodules and vendored headers)
extern/*
!extern/.gitkeep
+!extern/catch2
!extern/imgui
!extern/vk-bootstrap
!extern/vk_mem_alloc.h
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f55bba09..7d308863 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -25,8 +25,9 @@ endif()
# Options
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
-option(WOWEE_BUILD_TESTS "Build tests" OFF)
+option(WOWEE_BUILD_TESTS "Build tests" ON)
option(WOWEE_ENABLE_ASAN "Enable AddressSanitizer (Debug builds)" OFF)
+option(WOWEE_ENABLE_TRACY "Enable Tracy profiler instrumentation" OFF)
option(WOWEE_ENABLE_AMD_FSR2 "Enable AMD FidelityFX FSR2 backend when SDK is present" ON)
option(WOWEE_ENABLE_AMD_FSR3_FRAMEGEN "Enable AMD FidelityFX SDK FSR3 frame generation interface probe when SDK is present" ON)
option(WOWEE_BUILD_AMD_FSR3_RUNTIME "Build native AMD FidelityFX VK runtime (Path A) from extern/FidelityFX-SDK/Kits" ON)
@@ -788,6 +789,15 @@ endif()
# Create executable
add_executable(wowee ${WOWEE_SOURCES} ${WOWEE_HEADERS} ${WOWEE_PLATFORM_SOURCES})
+
+# Tracy profiler — zero overhead when WOWEE_ENABLE_TRACY is OFF
+if(WOWEE_ENABLE_TRACY)
+ target_sources(wowee PRIVATE ${CMAKE_SOURCE_DIR}/extern/tracy/public/TracyClient.cpp)
+ target_compile_definitions(wowee PRIVATE TRACY_ENABLE)
+ target_include_directories(wowee SYSTEM PRIVATE ${CMAKE_SOURCE_DIR}/extern/tracy/public)
+ message(STATUS "Tracy profiler: ENABLED")
+endif()
+
if(TARGET opcodes-generate)
add_dependencies(wowee opcodes-generate)
endif()
@@ -931,6 +941,12 @@ else()
)
endif()
+# ── Unit tests (Catch2) ──────────────────────────────────────
+if(WOWEE_BUILD_TESTS)
+ enable_testing()
+ add_subdirectory(tests)
+endif()
+
# AddressSanitizer — catch buffer overflows, use-after-free, etc.
# Enable with: cmake ... -DWOWEE_ENABLE_ASAN=ON -DCMAKE_BUILD_TYPE=Debug
if(WOWEE_ENABLE_ASAN)
@@ -942,10 +958,10 @@ if(WOWEE_ENABLE_ASAN)
$<$:/MD>
)
else()
- target_compile_options(wowee PRIVATE -fsanitize=address -fno-omit-frame-pointer)
- target_link_options(wowee PRIVATE -fsanitize=address)
+ target_compile_options(wowee PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
+ target_link_options(wowee PRIVATE -fsanitize=address,undefined)
endif()
- message(STATUS "AddressSanitizer: ENABLED")
+ message(STATUS "AddressSanitizer + UBSan: ENABLED")
endif()
# Release build optimizations
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 00000000..3846b15d
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,390 @@
+# WoWee Testing Guide
+
+This document covers everything needed to build, run, lint, and extend the WoWee test suite.
+
+---
+
+## Table of Contents
+
+1. [Overview](#overview)
+2. [Prerequisites](#prerequisites)
+3. [Test Suite Layout](#test-suite-layout)
+4. [Building the Tests](#building-the-tests)
+ - [Release Build (normal)](#release-build-normal)
+ - [Debug + ASAN/UBSan Build](#debug--asanubsan-build)
+5. [Running Tests](#running-tests)
+ - [test.sh — the unified entry point](#testsh--the-unified-entry-point)
+ - [Running directly with ctest](#running-directly-with-ctest)
+6. [Lint (clang-tidy)](#lint-clang-tidy)
+ - [Running lint](#running-lint)
+ - [Applying auto-fixes](#applying-auto-fixes)
+ - [Configuration (.clang-tidy)](#configuration-clang-tidy)
+7. [ASAN / UBSan](#asan--ubsan)
+8. [Adding New Tests](#adding-new-tests)
+9. [CI Reference](#ci-reference)
+
+---
+
+## Overview
+
+WoWee uses **Catch2 v3** (amalgamated) for unit testing and **clang-tidy** for static analysis. The `test.sh` script is the single entry point for both.
+
+| Command | What it does |
+|---|---|
+| `./test.sh` | Runs both unit tests (Release) and lint |
+| `./test.sh --test` | Runs unit tests only (Release build) |
+| `./test.sh --lint` | Runs clang-tidy only |
+| `./test.sh --asan` | Runs unit tests under ASAN + UBSan (Debug build) |
+| `FIX=1 ./test.sh --lint` | Applies clang-tidy auto-fixes in-place |
+
+All commands exit non-zero on any failure.
+
+---
+
+## Prerequisites
+
+The test suite requires the same base toolchain used to build the project. See [BUILD_INSTRUCTIONS.md](BUILD_INSTRUCTIONS.md) for platform-specific dependency installation.
+
+### Linux (Ubuntu / Debian)
+
+```bash
+sudo apt update
+sudo apt install -y \
+ build-essential cmake pkg-config git \
+ libssl-dev \
+ clang-tidy
+```
+
+### Linux (Arch)
+
+```bash
+sudo pacman -S --needed base-devel cmake pkgconf git openssl clang
+```
+
+### macOS
+
+```bash
+brew install cmake openssl@3 llvm
+# Add LLVM tools to PATH so clang-tidy is found:
+export PATH="$(brew --prefix llvm)/bin:$PATH"
+```
+
+### Windows (MSYS2)
+
+Install the full toolchain as described in `BUILD_INSTRUCTIONS.md`, then add:
+
+```bash
+pacman -S --needed mingw-w64-x86_64-clang-tools-extra
+```
+
+---
+
+## Test Suite Layout
+
+```
+tests/
+ CMakeLists.txt — CMake test configuration
+ test_packet.cpp — Network packet encode/decode
+ test_srp.cpp — SRP-6a authentication math (requires OpenSSL)
+ test_opcode_table.cpp — Opcode registry lookup
+ test_entity.cpp — ECS entity basics
+ test_dbc_loader.cpp — DBC binary file parsing
+ test_m2_structs.cpp — M2 model struct layout / alignment
+ test_blp_loader.cpp — BLP texture file parsing
+ test_frustum.cpp — View-frustum culling math
+```
+
+The Catch2 v3 amalgamated source lives at:
+
+```
+extern/catch2/
+ catch_amalgamated.hpp
+ catch_amalgamated.cpp
+```
+
+---
+
+## Building the Tests
+
+Tests are _not_ built by default. Enable them with `-DWOWEE_BUILD_TESTS=ON`.
+
+### Release Build (normal)
+
+> **Note:** Per project rules, always use `rebuild.sh` for a full clean build. Direct `cmake --build` is fine for test-only incremental builds.
+
+```bash
+# Configure (only needed once)
+cmake -B build -DCMAKE_BUILD_TYPE=Release -DWOWEE_BUILD_TESTS=ON
+
+# Build test targets (fast — only compiles tests and Catch2)
+cmake --build build --target \
+ test_packet test_srp test_opcode_table test_entity \
+ test_dbc_loader test_m2_structs test_blp_loader test_frustum
+```
+
+Or simply run a full rebuild (builds everything including the main binary):
+
+```bash
+./rebuild.sh # ~10 minutes — see BUILD_INSTRUCTIONS.md
+```
+
+### Debug + ASAN/UBSan Build
+
+A separate CMake build directory is used so ASAN flags do not pollute the Release binary.
+
+```bash
+cmake -B build_asan \
+ -DCMAKE_BUILD_TYPE=Debug \
+ -DWOWEE_ENABLE_ASAN=ON \
+ -DWOWEE_BUILD_TESTS=ON
+
+cmake --build build_asan --target \
+ test_packet test_srp test_opcode_table test_entity \
+ test_dbc_loader test_m2_structs test_blp_loader test_frustum
+```
+
+CMake will print: `Test targets: ASAN + UBSan ENABLED` when configured correctly.
+
+---
+
+## Running Tests
+
+### test.sh — the unified entry point
+
+`test.sh` is the recommended way to run tests and/or lint. It handles build-directory discovery, dependency checking, and exit-code aggregation across both steps.
+
+```bash
+# Run everything (tests + lint) — default when no flags are given
+./test.sh
+
+# Tests only (Release build)
+./test.sh --test
+
+# Tests only under ASAN+UBSan (Debug build — requires build_asan/)
+./test.sh --asan
+
+# Lint only
+./test.sh --lint
+
+# Both tests and lint explicitly
+./test.sh --test --lint
+
+# Usage summary
+./test.sh --help
+```
+
+**Exit codes:**
+
+| Outcome | Exit code |
+|---|---|
+| All tests passed, lint clean | `0` |
+| Any test failed | `1` |
+| Any lint diagnostic | `1` |
+| Both test failure and lint issues | `1` |
+
+### Running directly with ctest
+
+```bash
+# Release build
+cd build
+ctest --output-on-failure
+
+# ASAN build
+cd build_asan
+ctest --output-on-failure
+
+# Run one specific test suite by name
+ctest --output-on-failure -R srp
+
+# Verbose output (shows every SECTION and REQUIRE)
+ctest --output-on-failure -V
+```
+
+You can also run a test binary directly for detailed Catch2 output:
+
+```bash
+./build/bin/test_srp
+./build/bin/test_srp --reporter console
+./build/bin/test_srp "[authentication]" # run only tests tagged [authentication]
+```
+
+---
+
+## Lint (clang-tidy)
+
+The project uses clang-tidy to enforce C++20 best practices on all first-party sources under `src/`. Third-party code (anything in `extern/`) and generated files are excluded.
+
+### Running lint
+
+```bash
+./test.sh --lint
+```
+
+Under the hood the script:
+
+1. Locates `clang-tidy` (tries versions 14–18, then `clang-tidy`).
+2. Uses `run-clang-tidy` for parallel execution when available; falls back to sequential.
+3. Reads `build/compile_commands.json` (generated by CMake) for compiler flags.
+4. Feeds GCC stdlib include paths as `-isystem` extras so clang-tidy can resolve ``, ``, etc. when the compile-commands were generated with GCC.
+
+`compile_commands.json` is regenerated automatically by any CMake configure step. If you only want to update it without rebuilding:
+
+```bash
+cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
+```
+
+### Applying auto-fixes
+
+Some clang-tidy checks can apply fixes automatically (e.g. `modernize-*`, `readability-*`):
+
+```bash
+FIX=1 ./test.sh --lint
+```
+
+> **Caution:** Review the diff before committing — automatic fixes occasionally produce non-idiomatic results in complex template code.
+
+### Configuration (.clang-tidy)
+
+The active check set is defined in [.clang-tidy](.clang-tidy) at the repository root.
+
+**Enabled check categories:**
+
+| Category | What it catches |
+|---|---|
+| `bugprone-*` | Common bug patterns (signed overflow, misplaced `=`, etc.) |
+| `clang-analyzer-*` | Deep flow-analysis: null dereferences, memory leaks, dead stores |
+| `performance-*` | Unnecessary copies, inefficient STL usage |
+| `modernize-*` (subset) | Pre-C++11 patterns that should use modern equivalents |
+| `readability-*` (subset) | Control-flow simplification, redundant code |
+
+**Notable suppressions** (see `.clang-tidy` for details):
+
+| Suppressed check | Reason |
+|---|---|
+| `bugprone-easily-swappable-parameters` | High false-positive rate in graphics/math APIs |
+| `clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling` | Intentional low-level buffer code in rendering |
+| `performance-avoid-endl` | `std::endl` is used intentionally for logger flushing |
+
+To suppress a specific warning inline, use:
+
+```cpp
+// NOLINT(bugprone-narrowing-conversions)
+uint8_t byte = static_cast(value); // NOLINT
+```
+
+---
+
+## ASAN / UBSan
+
+AddressSanitizer (ASAN) and Undefined Behaviour Sanitizer (UBSan) are applied to all test targets when `WOWEE_ENABLE_ASAN=ON`.
+
+Both the test executables **and** the `catch2_main` static library are recompiled with:
+
+```
+-fsanitize=address,undefined -fno-omit-frame-pointer
+```
+
+This means any heap overflow, stack buffer overflow, use-after-free, null dereference, signed integer overflow, or misaligned access detected during a test will abort the process and print a human-readable report to stderr.
+
+### Workflow
+
+```bash
+# 1. Configure once (only needs to be re-run when CMakeLists.txt changes)
+cmake -B build_asan \
+ -DCMAKE_BUILD_TYPE=Debug \
+ -DWOWEE_ENABLE_ASAN=ON \
+ -DWOWEE_BUILD_TESTS=ON
+
+# 2. Build test binaries (fast incremental after the first build)
+cmake --build build_asan --target test_packet test_srp # etc.
+
+# 3. Run
+./test.sh --asan
+```
+
+### Interpreting ASAN output
+
+A failing ASAN report looks like:
+
+```
+==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000010
+READ of size 4 at 0x602000000010 thread T0
+ #0 0x... in PacketBuffer::read_uint32 src/network/packet.cpp:42
+ #1 0x... in test_packet tests/test_packet.cpp:88
+```
+
+Address the issue in the source file and re-run. Do **not** suppress ASAN reports without a code fix.
+
+---
+
+## Adding New Tests
+
+1. **Create** `tests/test_.cpp` with a standard Catch2 v3 structure:
+
+```cpp
+#include "catch_amalgamated.hpp"
+
+TEST_CASE("SomeFeature does X", "[tag]") {
+ REQUIRE(1 + 1 == 2);
+}
+```
+
+2. **Register** the test in `tests/CMakeLists.txt` following the existing pattern:
+
+```cmake
+# ── test_ ──────────────────────────────────────────────
+add_executable(test_
+ test_.cpp
+ ${TEST_COMMON_SOURCES}
+ ${CMAKE_SOURCE_DIR}/src//.cpp # source under test
+)
+target_include_directories(test_ PRIVATE ${TEST_INCLUDE_DIRS})
+target_include_directories(test_ SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
+target_link_libraries(test_ PRIVATE catch2_main)
+add_test(NAME COMMAND test_)
+register_test_target(test_) # required — enables ASAN propagation
+```
+
+3. **Build** and verify:
+
+```bash
+cmake --build build --target test_
+./test.sh --test
+```
+
+The `register_test_target()` macro call is **mandatory** — without it the new test will not receive ASAN/UBSan flags when `WOWEE_ENABLE_ASAN=ON`.
+
+---
+
+## CI Reference
+
+The following commands map to typical CI jobs:
+
+| Job | Command |
+|---|---|
+| Unit tests (Release) | `./test.sh --test` |
+| Unit tests (ASAN+UBSan) | `./test.sh --asan` |
+| Lint | `./test.sh --lint` |
+| Full check (tests + lint) | `./test.sh` |
+
+**Configuring the ASAN job in CI:**
+
+```yaml
+- name: Configure ASAN build
+ run: |
+ cmake -B build_asan \
+ -DCMAKE_BUILD_TYPE=Debug \
+ -DWOWEE_ENABLE_ASAN=ON \
+ -DWOWEE_BUILD_TESTS=ON
+
+- name: Build test targets
+ run: |
+ cmake --build build_asan --target \
+ test_packet test_srp test_opcode_table test_entity \
+ test_dbc_loader test_m2_structs test_blp_loader test_frustum
+
+- name: Run ASAN tests
+ run: ./test.sh --asan
+```
+
+> See [BUILD_INSTRUCTIONS.md](BUILD_INSTRUCTIONS.md) for full platform dependency installation steps required before any CI job.
diff --git a/docs/perf_baseline.md b/docs/perf_baseline.md
new file mode 100644
index 00000000..88bdc743
--- /dev/null
+++ b/docs/perf_baseline.md
@@ -0,0 +1,79 @@
+# Performance Baseline — WoWee
+
+> Phase 0.3 deliverable. Measurements taken before any optimization work.
+> Re-run after each phase to quantify improvement.
+
+## Tracy Profiler Integration
+
+Tracy v0.11.1 integrated under `WOWEE_ENABLE_TRACY` CMake option (default: OFF).
+When enabled, zero-cost zone markers instrument the following critical paths:
+
+### Instrumented Zones
+
+| Zone Name | File | Purpose |
+|-----------|------|---------|
+| `Application::run` | src/core/application.cpp | Main loop entry |
+| `Application::update` | src/core/application.cpp | Per-frame game logic |
+| `Renderer::beginFrame` | src/rendering/renderer.cpp | Vulkan frame begin |
+| `Renderer::endFrame` | src/rendering/renderer.cpp | Post-process + present |
+| `Renderer::update` | src/rendering/renderer.cpp | Renderer per-frame update |
+| `Renderer::renderWorld` | src/rendering/renderer.cpp | Main world draw call |
+| `Renderer::renderShadowPass` | src/rendering/renderer.cpp | Shadow depth pass |
+| `PostProcess::execute` | src/rendering/post_process_pipeline.cpp | FSR/FXAA post-process |
+| `M2::computeBoneMatrices` | src/rendering/m2_renderer.cpp | CPU skeletal animation |
+| `M2Renderer::update` | src/rendering/m2_renderer.cpp | M2 instance update + culling |
+| `TerrainManager::update` | src/rendering/terrain_manager.cpp | Terrain streaming logic |
+| `TerrainManager::processReadyTiles` | src/rendering/terrain_manager.cpp | GPU tile uploads |
+| `ADTLoader::load` | src/pipeline/adt_loader.cpp | ADT binary parsing |
+| `AssetManager::loadTexture` | src/pipeline/asset_manager.cpp | BLP texture loading |
+| `AssetManager::loadDBC` | src/pipeline/asset_manager.cpp | DBC data file loading |
+| `WorldSocket::update` | src/network/world_socket.cpp | Network packet dispatch |
+
+`FrameMark` placed at frame boundary in Application::update to track FPS.
+
+### How to Profile
+
+```bash
+# Build with Tracy enabled
+mkdir -p build_tracy && cd build_tracy
+cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo -DWOWEE_ENABLE_TRACY=ON
+cmake --build . --parallel $(nproc)
+
+# Run the client — Tracy will broadcast on default port (8086)
+cd bin && ./wowee
+
+# Connect with Tracy profiler GUI (separate download from https://github.com/wolfpld/tracy/releases)
+# Or capture from CLI: tracy-capture -o trace.tracy
+```
+
+## Baseline Scenarios
+
+> **TODO:** Record measurements once profiler is connected to a running instance.
+> Each scenario should record: avg FPS, frame time (p50/p95/p99), and per-zone timings.
+
+### Scenario 1: Stormwind (Heavy M2/WMO)
+- **Location:** Stormwind City center
+- **Load:** Dense M2 models (NPCs, doodads), multiple WMO interiors
+- **Avg FPS:** _pending_
+- **Frame time (p50/p95/p99):** _pending_
+- **Top zones:** _pending_
+
+### Scenario 2: The Barrens (Heavy Terrain)
+- **Location:** Central Barrens
+- **Load:** Many terrain tiles loaded, sparse M2, large draw distance
+- **Avg FPS:** _pending_
+- **Frame time (p50/p95/p99):** _pending_
+- **Top zones:** _pending_
+
+### Scenario 3: Dungeon Instance (WMO-only)
+- **Location:** Any dungeon instance (e.g., Deadmines entrance)
+- **Load:** WMO interior rendering, no terrain
+- **Avg FPS:** _pending_
+- **Frame time (p50/p95/p99):** _pending_
+- **Top zones:** _pending_
+
+## Notes
+
+- When `WOWEE_ENABLE_TRACY` is OFF (default), all `ZoneScopedN` / `FrameMark` macros expand to nothing — zero runtime overhead.
+- Tracy requires a network connection to capture traces. Run the Tracy profiler GUI or `tracy-capture` CLI alongside the client.
+- Debug builds are significantly slower due to -Og and no LTO; use RelWithDebInfo for representative measurements.
diff --git a/extern/catch2/catch_amalgamated.cpp b/extern/catch2/catch_amalgamated.cpp
new file mode 100644
index 00000000..f45c18a0
--- /dev/null
+++ b/extern/catch2/catch_amalgamated.cpp
@@ -0,0 +1,11811 @@
+
+// Copyright Catch2 Authors
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at
+// https://www.boost.org/LICENSE_1_0.txt)
+
+// SPDX-License-Identifier: BSL-1.0
+
+// Catch v3.7.1
+// Generated: 2024-09-17 10:36:45.608896
+// ----------------------------------------------------------
+// This file is an amalgamation of multiple different files.
+// You probably shouldn't edit it directly.
+// ----------------------------------------------------------
+
+#include "catch_amalgamated.hpp"
+
+
+#ifndef CATCH_WINDOWS_H_PROXY_HPP_INCLUDED
+#define CATCH_WINDOWS_H_PROXY_HPP_INCLUDED
+
+
+#if defined(CATCH_PLATFORM_WINDOWS)
+
+// We might end up with the define made globally through the compiler,
+// and we don't want to trigger warnings for this
+#if !defined(NOMINMAX)
+# define NOMINMAX
+#endif
+#if !defined(WIN32_LEAN_AND_MEAN)
+# define WIN32_LEAN_AND_MEAN
+#endif
+
+#include
+
+#endif // defined(CATCH_PLATFORM_WINDOWS)
+
+#endif // CATCH_WINDOWS_H_PROXY_HPP_INCLUDED
+
+
+
+
+namespace Catch {
+ namespace Benchmark {
+ namespace Detail {
+ ChronometerConcept::~ChronometerConcept() = default;
+ } // namespace Detail
+ } // namespace Benchmark
+} // namespace Catch
+
+
+// Adapted from donated nonius code.
+
+
+#include
+
+namespace Catch {
+ namespace Benchmark {
+ namespace Detail {
+ SampleAnalysis analyse(const IConfig &cfg, FDuration* first, FDuration* last) {
+ if (!cfg.benchmarkNoAnalysis()) {
+ std::vector samples;
+ samples.reserve(static_cast(last - first));
+ for (auto current = first; current != last; ++current) {
+ samples.push_back( current->count() );
+ }
+
+ auto analysis = Catch::Benchmark::Detail::analyse_samples(
+ cfg.benchmarkConfidenceInterval(),
+ cfg.benchmarkResamples(),
+ samples.data(),
+ samples.data() + samples.size() );
+ auto outliers = Catch::Benchmark::Detail::classify_outliers(
+ samples.data(), samples.data() + samples.size() );
+
+ auto wrap_estimate = [](Estimate e) {
+ return Estimate {
+ FDuration(e.point),
+ FDuration(e.lower_bound),
+ FDuration(e.upper_bound),
+ e.confidence_interval,
+ };
+ };
+ std::vector samples2;
+ samples2.reserve(samples.size());
+ for (auto s : samples) {
+ samples2.push_back( FDuration( s ) );
+ }
+
+ return {
+ CATCH_MOVE(samples2),
+ wrap_estimate(analysis.mean),
+ wrap_estimate(analysis.standard_deviation),
+ outliers,
+ analysis.outlier_variance,
+ };
+ } else {
+ std::vector samples;
+ samples.reserve(static_cast(last - first));
+
+ FDuration mean = FDuration(0);
+ int i = 0;
+ for (auto it = first; it < last; ++it, ++i) {
+ samples.push_back(*it);
+ mean += *it;
+ }
+ mean /= i;
+
+ return SampleAnalysis{
+ CATCH_MOVE(samples),
+ Estimate{ mean, mean, mean, 0.0 },
+ Estimate{ FDuration( 0 ),
+ FDuration( 0 ),
+ FDuration( 0 ),
+ 0.0 },
+ OutlierClassification{},
+ 0.0
+ };
+ }
+ }
+ } // namespace Detail
+ } // namespace Benchmark
+} // namespace Catch
+
+
+
+
+namespace Catch {
+ namespace Benchmark {
+ namespace Detail {
+ struct do_nothing {
+ void operator()() const {}
+ };
+
+ BenchmarkFunction::callable::~callable() = default;
+ BenchmarkFunction::BenchmarkFunction():
+ f( new model{ {} } ){}
+ } // namespace Detail
+ } // namespace Benchmark
+} // namespace Catch
+
+
+
+
+#include
+
+namespace Catch {
+ namespace Benchmark {
+ namespace Detail {
+ struct optimized_away_error : std::exception {
+ const char* what() const noexcept override;
+ };
+
+ const char* optimized_away_error::what() const noexcept {
+ return "could not measure benchmark, maybe it was optimized away";
+ }
+
+ void throw_optimized_away_error() {
+ Catch::throw_exception(optimized_away_error{});
+ }
+
+ } // namespace Detail
+ } // namespace Benchmark
+} // namespace Catch
+
+
+// Adapted from donated nonius code.
+
+
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+
+#if defined(CATCH_CONFIG_USE_ASYNC)
+#include
+#endif
+
+namespace Catch {
+ namespace Benchmark {
+ namespace Detail {
+ namespace {
+
+ template
+ static sample
+ resample( URng& rng,
+ unsigned int resamples,
+ double const* first,
+ double const* last,
+ Estimator& estimator ) {
+ auto n = static_cast( last - first );
+ Catch::uniform_integer_distribution dist( 0, n - 1 );
+
+ sample out;
+ out.reserve( resamples );
+ std::vector resampled;
+ resampled.reserve( n );
+ for ( size_t i = 0; i < resamples; ++i ) {
+ resampled.clear();
+ for ( size_t s = 0; s < n; ++s ) {
+ resampled.push_back( first[dist( rng )] );
+ }
+ const auto estimate =
+ estimator( resampled.data(), resampled.data() + resampled.size() );
+ out.push_back( estimate );
+ }
+ std::sort( out.begin(), out.end() );
+ return out;
+ }
+
+ static double outlier_variance( Estimate mean,
+ Estimate stddev,
+ int n ) {
+ double sb = stddev.point;
+ double mn = mean.point / n;
+ double mg_min = mn / 2.;
+ double sg = (std::min)( mg_min / 4., sb / std::sqrt( n ) );
+ double sg2 = sg * sg;
+ double sb2 = sb * sb;
+
+ auto c_max = [n, mn, sb2, sg2]( double x ) -> double {
+ double k = mn - x;
+ double d = k * k;
+ double nd = n * d;
+ double k0 = -n * nd;
+ double k1 = sb2 - n * sg2 + nd;
+ double det = k1 * k1 - 4 * sg2 * k0;
+ return static_cast( -2. * k0 /
+ ( k1 + std::sqrt( det ) ) );
+ };
+
+ auto var_out = [n, sb2, sg2]( double c ) {
+ double nc = n - c;
+ return ( nc / n ) * ( sb2 - nc * sg2 );
+ };
+
+ return (std::min)( var_out( 1 ),
+ var_out(
+ (std::min)( c_max( 0. ),
+ c_max( mg_min ) ) ) ) /
+ sb2;
+ }
+
+ static double erf_inv( double x ) {
+ // Code accompanying the article "Approximating the erfinv
+ // function" in GPU Computing Gems, Volume 2
+ double w, p;
+
+ w = -log( ( 1.0 - x ) * ( 1.0 + x ) );
+
+ if ( w < 6.250000 ) {
+ w = w - 3.125000;
+ p = -3.6444120640178196996e-21;
+ p = -1.685059138182016589e-19 + p * w;
+ p = 1.2858480715256400167e-18 + p * w;
+ p = 1.115787767802518096e-17 + p * w;
+ p = -1.333171662854620906e-16 + p * w;
+ p = 2.0972767875968561637e-17 + p * w;
+ p = 6.6376381343583238325e-15 + p * w;
+ p = -4.0545662729752068639e-14 + p * w;
+ p = -8.1519341976054721522e-14 + p * w;
+ p = 2.6335093153082322977e-12 + p * w;
+ p = -1.2975133253453532498e-11 + p * w;
+ p = -5.4154120542946279317e-11 + p * w;
+ p = 1.051212273321532285e-09 + p * w;
+ p = -4.1126339803469836976e-09 + p * w;
+ p = -2.9070369957882005086e-08 + p * w;
+ p = 4.2347877827932403518e-07 + p * w;
+ p = -1.3654692000834678645e-06 + p * w;
+ p = -1.3882523362786468719e-05 + p * w;
+ p = 0.0001867342080340571352 + p * w;
+ p = -0.00074070253416626697512 + p * w;
+ p = -0.0060336708714301490533 + p * w;
+ p = 0.24015818242558961693 + p * w;
+ p = 1.6536545626831027356 + p * w;
+ } else if ( w < 16.000000 ) {
+ w = sqrt( w ) - 3.250000;
+ p = 2.2137376921775787049e-09;
+ p = 9.0756561938885390979e-08 + p * w;
+ p = -2.7517406297064545428e-07 + p * w;
+ p = 1.8239629214389227755e-08 + p * w;
+ p = 1.5027403968909827627e-06 + p * w;
+ p = -4.013867526981545969e-06 + p * w;
+ p = 2.9234449089955446044e-06 + p * w;
+ p = 1.2475304481671778723e-05 + p * w;
+ p = -4.7318229009055733981e-05 + p * w;
+ p = 6.8284851459573175448e-05 + p * w;
+ p = 2.4031110387097893999e-05 + p * w;
+ p = -0.0003550375203628474796 + p * w;
+ p = 0.00095328937973738049703 + p * w;
+ p = -0.0016882755560235047313 + p * w;
+ p = 0.0024914420961078508066 + p * w;
+ p = -0.0037512085075692412107 + p * w;
+ p = 0.005370914553590063617 + p * w;
+ p = 1.0052589676941592334 + p * w;
+ p = 3.0838856104922207635 + p * w;
+ } else {
+ w = sqrt( w ) - 5.000000;
+ p = -2.7109920616438573243e-11;
+ p = -2.5556418169965252055e-10 + p * w;
+ p = 1.5076572693500548083e-09 + p * w;
+ p = -3.7894654401267369937e-09 + p * w;
+ p = 7.6157012080783393804e-09 + p * w;
+ p = -1.4960026627149240478e-08 + p * w;
+ p = 2.9147953450901080826e-08 + p * w;
+ p = -6.7711997758452339498e-08 + p * w;
+ p = 2.2900482228026654717e-07 + p * w;
+ p = -9.9298272942317002539e-07 + p * w;
+ p = 4.5260625972231537039e-06 + p * w;
+ p = -1.9681778105531670567e-05 + p * w;
+ p = 7.5995277030017761139e-05 + p * w;
+ p = -0.00021503011930044477347 + p * w;
+ p = -0.00013871931833623122026 + p * w;
+ p = 1.0103004648645343977 + p * w;
+ p = 4.8499064014085844221 + p * w;
+ }
+ return p * x;
+ }
+
+ static double
+ standard_deviation( double const* first, double const* last ) {
+ auto m = Catch::Benchmark::Detail::mean( first, last );
+ double variance =
+ std::accumulate( first,
+ last,
+ 0.,
+ [m]( double a, double b ) {
+ double diff = b - m;
+ return a + diff * diff;
+ } ) /
+ ( last - first );
+ return std::sqrt( variance );
+ }
+
+ static sample jackknife( double ( *estimator )( double const*,
+ double const* ),
+ double* first,
+ double* last ) {
+ const auto second = first + 1;
+ sample results;
+ results.reserve( static_cast( last - first ) );
+
+ for ( auto it = first; it != last; ++it ) {
+ std::iter_swap( it, first );
+ results.push_back( estimator( second, last ) );
+ }
+
+ return results;
+ }
+
+
+ } // namespace
+ } // namespace Detail
+ } // namespace Benchmark
+} // namespace Catch
+
+namespace Catch {
+ namespace Benchmark {
+ namespace Detail {
+
+ double weighted_average_quantile( int k,
+ int q,
+ double* first,
+ double* last ) {
+ auto count = last - first;
+ double idx = (count - 1) * k / static_cast(q);
+ int j = static_cast(idx);
+ double g = idx - j;
+ std::nth_element(first, first + j, last);
+ auto xj = first[j];
+ if ( Catch::Detail::directCompare( g, 0 ) ) {
+ return xj;
+ }
+
+ auto xj1 = *std::min_element(first + (j + 1), last);
+ return xj + g * (xj1 - xj);
+ }
+
+ OutlierClassification
+ classify_outliers( double const* first, double const* last ) {
+ std::vector copy( first, last );
+
+ auto q1 = weighted_average_quantile( 1, 4, copy.data(), copy.data() + copy.size() );
+ auto q3 = weighted_average_quantile( 3, 4, copy.data(), copy.data() + copy.size() );
+ auto iqr = q3 - q1;
+ auto los = q1 - ( iqr * 3. );
+ auto lom = q1 - ( iqr * 1.5 );
+ auto him = q3 + ( iqr * 1.5 );
+ auto his = q3 + ( iqr * 3. );
+
+ OutlierClassification o;
+ for ( ; first != last; ++first ) {
+ const double t = *first;
+ if ( t < los ) {
+ ++o.low_severe;
+ } else if ( t < lom ) {
+ ++o.low_mild;
+ } else if ( t > his ) {
+ ++o.high_severe;
+ } else if ( t > him ) {
+ ++o.high_mild;
+ }
+ ++o.samples_seen;
+ }
+ return o;
+ }
+
+ double mean( double const* first, double const* last ) {
+ auto count = last - first;
+ double sum = 0.;
+ while (first != last) {
+ sum += *first;
+ ++first;
+ }
+ return sum / static_cast(count);
+ }
+
+ double normal_cdf( double x ) {
+ return std::erfc( -x / std::sqrt( 2.0 ) ) / 2.0;
+ }
+
+ double erfc_inv(double x) {
+ return erf_inv(1.0 - x);
+ }
+
+ double normal_quantile(double p) {
+ static const double ROOT_TWO = std::sqrt(2.0);
+
+ double result = 0.0;
+ assert(p >= 0 && p <= 1);
+ if (p < 0 || p > 1) {
+ return result;
+ }
+
+ result = -erfc_inv(2.0 * p);
+ // result *= normal distribution standard deviation (1.0) * sqrt(2)
+ result *= /*sd * */ ROOT_TWO;
+ // result += normal disttribution mean (0)
+ return result;
+ }
+
+ Estimate
+ bootstrap( double confidence_level,
+ double* first,
+ double* last,
+ sample const& resample,
+ double ( *estimator )( double const*, double const* ) ) {
+ auto n_samples = last - first;
+
+ double point = estimator( first, last );
+ // Degenerate case with a single sample
+ if ( n_samples == 1 )
+ return { point, point, point, confidence_level };
+
+ sample jack = jackknife( estimator, first, last );
+ double jack_mean =
+ mean( jack.data(), jack.data() + jack.size() );
+ double sum_squares = 0, sum_cubes = 0;
+ for ( double x : jack ) {
+ auto difference = jack_mean - x;
+ auto square = difference * difference;
+ auto cube = square * difference;
+ sum_squares += square;
+ sum_cubes += cube;
+ }
+
+ double accel = sum_cubes / ( 6 * std::pow( sum_squares, 1.5 ) );
+ long n = static_cast( resample.size() );
+ double prob_n =
+ std::count_if( resample.begin(),
+ resample.end(),
+ [point]( double x ) { return x < point; } ) /
+ static_cast( n );
+ // degenerate case with uniform samples
+ if ( Catch::Detail::directCompare( prob_n, 0. ) ) {
+ return { point, point, point, confidence_level };
+ }
+
+ double bias = normal_quantile( prob_n );
+ double z1 = normal_quantile( ( 1. - confidence_level ) / 2. );
+
+ auto cumn = [n]( double x ) -> long {
+ return std::lround( normal_cdf( x ) *
+ static_cast( n ) );
+ };
+ auto a = [bias, accel]( double b ) {
+ return bias + b / ( 1. - accel * b );
+ };
+ double b1 = bias + z1;
+ double b2 = bias - z1;
+ double a1 = a( b1 );
+ double a2 = a( b2 );
+ auto lo = static_cast( (std::max)( cumn( a1 ), 0l ) );
+ auto hi =
+ static_cast( (std::min)( cumn( a2 ), n - 1 ) );
+
+ return { point, resample[lo], resample[hi], confidence_level };
+ }
+
+ bootstrap_analysis analyse_samples(double confidence_level,
+ unsigned int n_resamples,
+ double* first,
+ double* last) {
+ auto mean = &Detail::mean;
+ auto stddev = &standard_deviation;
+
+#if defined(CATCH_CONFIG_USE_ASYNC)
+ auto Estimate = [=](double(*f)(double const*, double const*)) {
+ std::random_device rd;
+ auto seed = rd();
+ return std::async(std::launch::async, [=] {
+ SimplePcg32 rng( seed );
+ auto resampled = resample(rng, n_resamples, first, last, f);
+ return bootstrap(confidence_level, first, last, resampled, f);
+ });
+ };
+
+ auto mean_future = Estimate(mean);
+ auto stddev_future = Estimate(stddev);
+
+ auto mean_estimate = mean_future.get();
+ auto stddev_estimate = stddev_future.get();
+#else
+ auto Estimate = [=](double(*f)(double const* , double const*)) {
+ std::random_device rd;
+ auto seed = rd();
+ SimplePcg32 rng( seed );
+ auto resampled = resample(rng, n_resamples, first, last, f);
+ return bootstrap(confidence_level, first, last, resampled, f);
+ };
+
+ auto mean_estimate = Estimate(mean);
+ auto stddev_estimate = Estimate(stddev);
+#endif // CATCH_USE_ASYNC
+
+ auto n = static_cast(last - first); // seriously, one can't use integral types without hell in C++
+ double outlier_variance = Detail::outlier_variance(mean_estimate, stddev_estimate, n);
+
+ return { mean_estimate, stddev_estimate, outlier_variance };
+ }
+ } // namespace Detail
+ } // namespace Benchmark
+} // namespace Catch
+
+
+
+#include
+#include
+
+namespace {
+
+// Performs equivalent check of std::fabs(lhs - rhs) <= margin
+// But without the subtraction to allow for INFINITY in comparison
+bool marginComparison(double lhs, double rhs, double margin) {
+ return (lhs + margin >= rhs) && (rhs + margin >= lhs);
+}
+
+}
+
+namespace Catch {
+
+ Approx::Approx ( double value )
+ : m_epsilon( static_cast(std::numeric_limits::epsilon())*100. ),
+ m_margin( 0.0 ),
+ m_scale( 0.0 ),
+ m_value( value )
+ {}
+
+ Approx Approx::custom() {
+ return Approx( 0 );
+ }
+
+ Approx Approx::operator-() const {
+ auto temp(*this);
+ temp.m_value = -temp.m_value;
+ return temp;
+ }
+
+
+ std::string Approx::toString() const {
+ ReusableStringStream rss;
+ rss << "Approx( " << ::Catch::Detail::stringify( m_value ) << " )";
+ return rss.str();
+ }
+
+ bool Approx::equalityComparisonImpl(const double other) const {
+ // First try with fixed margin, then compute margin based on epsilon, scale and Approx's value
+ // Thanks to Richard Harris for his help refining the scaled margin value
+ return marginComparison(m_value, other, m_margin)
+ || marginComparison(m_value, other, m_epsilon * (m_scale + std::fabs(std::isinf(m_value)? 0 : m_value)));
+ }
+
+ void Approx::setMargin(double newMargin) {
+ CATCH_ENFORCE(newMargin >= 0,
+ "Invalid Approx::margin: " << newMargin << '.'
+ << " Approx::Margin has to be non-negative.");
+ m_margin = newMargin;
+ }
+
+ void Approx::setEpsilon(double newEpsilon) {
+ CATCH_ENFORCE(newEpsilon >= 0 && newEpsilon <= 1.0,
+ "Invalid Approx::epsilon: " << newEpsilon << '.'
+ << " Approx::epsilon has to be in [0, 1]");
+ m_epsilon = newEpsilon;
+ }
+
+namespace literals {
+ Approx operator ""_a(long double val) {
+ return Approx(val);
+ }
+ Approx operator ""_a(unsigned long long val) {
+ return Approx(val);
+ }
+} // end namespace literals
+
+std::string StringMaker::convert(Catch::Approx const& value) {
+ return value.toString();
+}
+
+} // end namespace Catch
+
+
+
+namespace Catch {
+
+ AssertionResultData::AssertionResultData(ResultWas::OfType _resultType, LazyExpression const& _lazyExpression):
+ lazyExpression(_lazyExpression),
+ resultType(_resultType) {}
+
+ std::string AssertionResultData::reconstructExpression() const {
+
+ if( reconstructedExpression.empty() ) {
+ if( lazyExpression ) {
+ ReusableStringStream rss;
+ rss << lazyExpression;
+ reconstructedExpression = rss.str();
+ }
+ }
+ return reconstructedExpression;
+ }
+
+ AssertionResult::AssertionResult( AssertionInfo const& info, AssertionResultData&& data )
+ : m_info( info ),
+ m_resultData( CATCH_MOVE(data) )
+ {}
+
+ // Result was a success
+ bool AssertionResult::succeeded() const {
+ return Catch::isOk( m_resultData.resultType );
+ }
+
+ // Result was a success, or failure is suppressed
+ bool AssertionResult::isOk() const {
+ return Catch::isOk( m_resultData.resultType ) || shouldSuppressFailure( m_info.resultDisposition );
+ }
+
+ ResultWas::OfType AssertionResult::getResultType() const {
+ return m_resultData.resultType;
+ }
+
+ bool AssertionResult::hasExpression() const {
+ return !m_info.capturedExpression.empty();
+ }
+
+ bool AssertionResult::hasMessage() const {
+ return !m_resultData.message.empty();
+ }
+
+ std::string AssertionResult::getExpression() const {
+ // Possibly overallocating by 3 characters should be basically free
+ std::string expr; expr.reserve(m_info.capturedExpression.size() + 3);
+ if (isFalseTest(m_info.resultDisposition)) {
+ expr += "!(";
+ }
+ expr += m_info.capturedExpression;
+ if (isFalseTest(m_info.resultDisposition)) {
+ expr += ')';
+ }
+ return expr;
+ }
+
+ std::string AssertionResult::getExpressionInMacro() const {
+ if ( m_info.macroName.empty() ) {
+ return static_cast( m_info.capturedExpression );
+ }
+ std::string expr;
+ expr.reserve( m_info.macroName.size() + m_info.capturedExpression.size() + 4 );
+ expr += m_info.macroName;
+ expr += "( ";
+ expr += m_info.capturedExpression;
+ expr += " )";
+ return expr;
+ }
+
+ bool AssertionResult::hasExpandedExpression() const {
+ return hasExpression() && getExpandedExpression() != getExpression();
+ }
+
+ std::string AssertionResult::getExpandedExpression() const {
+ std::string expr = m_resultData.reconstructExpression();
+ return expr.empty()
+ ? getExpression()
+ : expr;
+ }
+
+ StringRef AssertionResult::getMessage() const {
+ return m_resultData.message;
+ }
+ SourceLineInfo AssertionResult::getSourceInfo() const {
+ return m_info.lineInfo;
+ }
+
+ StringRef AssertionResult::getTestMacroName() const {
+ return m_info.macroName;
+ }
+
+} // end namespace Catch
+
+
+
+#include
+
+namespace Catch {
+
+ namespace {
+ static bool enableBazelEnvSupport() {
+#if defined( CATCH_CONFIG_BAZEL_SUPPORT )
+ return true;
+#else
+ return Detail::getEnv( "BAZEL_TEST" ) != nullptr;
+#endif
+ }
+
+ struct bazelShardingOptions {
+ unsigned int shardIndex, shardCount;
+ std::string shardFilePath;
+ };
+
+ static Optional readBazelShardingOptions() {
+ const auto bazelShardIndex = Detail::getEnv( "TEST_SHARD_INDEX" );
+ const auto bazelShardTotal = Detail::getEnv( "TEST_TOTAL_SHARDS" );
+ const auto bazelShardInfoFile = Detail::getEnv( "TEST_SHARD_STATUS_FILE" );
+
+
+ const bool has_all =
+ bazelShardIndex && bazelShardTotal && bazelShardInfoFile;
+ if ( !has_all ) {
+ // We provide nice warning message if the input is
+ // misconfigured.
+ auto warn = []( const char* env_var ) {
+ Catch::cerr()
+ << "Warning: Bazel shard configuration is missing '"
+ << env_var << "'. Shard configuration is skipped.\n";
+ };
+ if ( !bazelShardIndex ) {
+ warn( "TEST_SHARD_INDEX" );
+ }
+ if ( !bazelShardTotal ) {
+ warn( "TEST_TOTAL_SHARDS" );
+ }
+ if ( !bazelShardInfoFile ) {
+ warn( "TEST_SHARD_STATUS_FILE" );
+ }
+ return {};
+ }
+
+ auto shardIndex = parseUInt( bazelShardIndex );
+ if ( !shardIndex ) {
+ Catch::cerr()
+ << "Warning: could not parse 'TEST_SHARD_INDEX' ('" << bazelShardIndex
+ << "') as unsigned int.\n";
+ return {};
+ }
+ auto shardTotal = parseUInt( bazelShardTotal );
+ if ( !shardTotal ) {
+ Catch::cerr()
+ << "Warning: could not parse 'TEST_TOTAL_SHARD' ('"
+ << bazelShardTotal << "') as unsigned int.\n";
+ return {};
+ }
+
+ return bazelShardingOptions{
+ *shardIndex, *shardTotal, bazelShardInfoFile };
+
+ }
+ } // end namespace
+
+
+ bool operator==( ProcessedReporterSpec const& lhs,
+ ProcessedReporterSpec const& rhs ) {
+ return lhs.name == rhs.name &&
+ lhs.outputFilename == rhs.outputFilename &&
+ lhs.colourMode == rhs.colourMode &&
+ lhs.customOptions == rhs.customOptions;
+ }
+
+ Config::Config( ConfigData const& data ):
+ m_data( data ) {
+ // We need to trim filter specs to avoid trouble with superfluous
+ // whitespace (esp. important for bdd macros, as those are manually
+ // aligned with whitespace).
+
+ for (auto& elem : m_data.testsOrTags) {
+ elem = trim(elem);
+ }
+ for (auto& elem : m_data.sectionsToRun) {
+ elem = trim(elem);
+ }
+
+ // Insert the default reporter if user hasn't asked for a specific one
+ if ( m_data.reporterSpecifications.empty() ) {
+#if defined( CATCH_CONFIG_DEFAULT_REPORTER )
+ const auto default_spec = CATCH_CONFIG_DEFAULT_REPORTER;
+#else
+ const auto default_spec = "console";
+#endif
+ auto parsed = parseReporterSpec(default_spec);
+ CATCH_ENFORCE( parsed,
+ "Cannot parse the provided default reporter spec: '"
+ << default_spec << '\'' );
+ m_data.reporterSpecifications.push_back( std::move( *parsed ) );
+ }
+
+ if ( enableBazelEnvSupport() ) {
+ readBazelEnvVars();
+ }
+
+ // Bazel support can modify the test specs, so parsing has to happen
+ // after reading Bazel env vars.
+ TestSpecParser parser( ITagAliasRegistry::get() );
+ if ( !m_data.testsOrTags.empty() ) {
+ m_hasTestFilters = true;
+ for ( auto const& testOrTags : m_data.testsOrTags ) {
+ parser.parse( testOrTags );
+ }
+ }
+ m_testSpec = parser.testSpec();
+
+
+ // We now fixup the reporter specs to handle default output spec,
+ // default colour spec, etc
+ bool defaultOutputUsed = false;
+ for ( auto const& reporterSpec : m_data.reporterSpecifications ) {
+ // We do the default-output check separately, while always
+ // using the default output below to make the code simpler
+ // and avoid superfluous copies.
+ if ( reporterSpec.outputFile().none() ) {
+ CATCH_ENFORCE( !defaultOutputUsed,
+ "Internal error: cannot use default output for "
+ "multiple reporters" );
+ defaultOutputUsed = true;
+ }
+
+ m_processedReporterSpecs.push_back( ProcessedReporterSpec{
+ reporterSpec.name(),
+ reporterSpec.outputFile() ? *reporterSpec.outputFile()
+ : data.defaultOutputFilename,
+ reporterSpec.colourMode().valueOr( data.defaultColourMode ),
+ reporterSpec.customOptions() } );
+ }
+ }
+
+ Config::~Config() = default;
+
+
+ bool Config::listTests() const { return m_data.listTests; }
+ bool Config::listTags() const { return m_data.listTags; }
+ bool Config::listReporters() const { return m_data.listReporters; }
+ bool Config::listListeners() const { return m_data.listListeners; }
+
+ std::vector const& Config::getTestsOrTags() const { return m_data.testsOrTags; }
+ std::vector const& Config::getSectionsToRun() const { return m_data.sectionsToRun; }
+
+ std::vector const& Config::getReporterSpecs() const {
+ return m_data.reporterSpecifications;
+ }
+
+ std::vector const&
+ Config::getProcessedReporterSpecs() const {
+ return m_processedReporterSpecs;
+ }
+
+ TestSpec const& Config::testSpec() const { return m_testSpec; }
+ bool Config::hasTestFilters() const { return m_hasTestFilters; }
+
+ bool Config::showHelp() const { return m_data.showHelp; }
+
+ // IConfig interface
+ bool Config::allowThrows() const { return !m_data.noThrow; }
+ StringRef Config::name() const { return m_data.name.empty() ? m_data.processName : m_data.name; }
+ bool Config::includeSuccessfulResults() const { return m_data.showSuccessfulTests; }
+ bool Config::warnAboutMissingAssertions() const {
+ return !!( m_data.warnings & WarnAbout::NoAssertions );
+ }
+ bool Config::warnAboutUnmatchedTestSpecs() const {
+ return !!( m_data.warnings & WarnAbout::UnmatchedTestSpec );
+ }
+ bool Config::zeroTestsCountAsSuccess() const { return m_data.allowZeroTests; }
+ ShowDurations Config::showDurations() const { return m_data.showDurations; }
+ double Config::minDuration() const { return m_data.minDuration; }
+ TestRunOrder Config::runOrder() const { return m_data.runOrder; }
+ uint32_t Config::rngSeed() const { return m_data.rngSeed; }
+ unsigned int Config::shardCount() const { return m_data.shardCount; }
+ unsigned int Config::shardIndex() const { return m_data.shardIndex; }
+ ColourMode Config::defaultColourMode() const { return m_data.defaultColourMode; }
+ bool Config::shouldDebugBreak() const { return m_data.shouldDebugBreak; }
+ int Config::abortAfter() const { return m_data.abortAfter; }
+ bool Config::showInvisibles() const { return m_data.showInvisibles; }
+ Verbosity Config::verbosity() const { return m_data.verbosity; }
+
+ bool Config::skipBenchmarks() const { return m_data.skipBenchmarks; }
+ bool Config::benchmarkNoAnalysis() const { return m_data.benchmarkNoAnalysis; }
+ unsigned int Config::benchmarkSamples() const { return m_data.benchmarkSamples; }
+ double Config::benchmarkConfidenceInterval() const { return m_data.benchmarkConfidenceInterval; }
+ unsigned int Config::benchmarkResamples() const { return m_data.benchmarkResamples; }
+ std::chrono::milliseconds Config::benchmarkWarmupTime() const { return std::chrono::milliseconds(m_data.benchmarkWarmupTime); }
+
+ void Config::readBazelEnvVars() {
+ // Register a JUnit reporter for Bazel. Bazel sets an environment
+ // variable with the path to XML output. If this file is written to
+ // during test, Bazel will not generate a default XML output.
+ // This allows the XML output file to contain higher level of detail
+ // than what is possible otherwise.
+ const auto bazelOutputFile = Detail::getEnv( "XML_OUTPUT_FILE" );
+
+ if ( bazelOutputFile ) {
+ m_data.reporterSpecifications.push_back(
+ { "junit", std::string( bazelOutputFile ), {}, {} } );
+ }
+
+ const auto bazelTestSpec = Detail::getEnv( "TESTBRIDGE_TEST_ONLY" );
+ if ( bazelTestSpec ) {
+ // Presumably the test spec from environment should overwrite
+ // the one we got from CLI (if we got any)
+ m_data.testsOrTags.clear();
+ m_data.testsOrTags.push_back( bazelTestSpec );
+ }
+
+ const auto bazelShardOptions = readBazelShardingOptions();
+ if ( bazelShardOptions ) {
+ std::ofstream f( bazelShardOptions->shardFilePath,
+ std::ios_base::out | std::ios_base::trunc );
+ if ( f.is_open() ) {
+ f << "";
+ m_data.shardIndex = bazelShardOptions->shardIndex;
+ m_data.shardCount = bazelShardOptions->shardCount;
+ }
+ }
+ }
+
+} // end namespace Catch
+
+
+
+
+
+namespace Catch {
+ std::uint32_t getSeed() {
+ return getCurrentContext().getConfig()->rngSeed();
+ }
+}
+
+
+
+#include
+#include
+
+namespace Catch {
+
+ ////////////////////////////////////////////////////////////////////////////
+
+
+ ScopedMessage::ScopedMessage( MessageBuilder&& builder ):
+ m_info( CATCH_MOVE(builder.m_info) ) {
+ m_info.message = builder.m_stream.str();
+ getResultCapture().pushScopedMessage( m_info );
+ }
+
+ ScopedMessage::ScopedMessage( ScopedMessage&& old ) noexcept:
+ m_info( CATCH_MOVE( old.m_info ) ) {
+ old.m_moved = true;
+ }
+
+ ScopedMessage::~ScopedMessage() {
+ if ( !uncaught_exceptions() && !m_moved ){
+ getResultCapture().popScopedMessage(m_info);
+ }
+ }
+
+
+ Capturer::Capturer( StringRef macroName,
+ SourceLineInfo const& lineInfo,
+ ResultWas::OfType resultType,
+ StringRef names ):
+ m_resultCapture( getResultCapture() ) {
+ auto trimmed = [&] (size_t start, size_t end) {
+ while (names[start] == ',' || isspace(static_cast(names[start]))) {
+ ++start;
+ }
+ while (names[end] == ',' || isspace(static_cast(names[end]))) {
+ --end;
+ }
+ return names.substr(start, end - start + 1);
+ };
+ auto skipq = [&] (size_t start, char quote) {
+ for (auto i = start + 1; i < names.size() ; ++i) {
+ if (names[i] == quote)
+ return i;
+ if (names[i] == '\\')
+ ++i;
+ }
+ CATCH_INTERNAL_ERROR("CAPTURE parsing encountered unmatched quote");
+ };
+
+ size_t start = 0;
+ std::stack openings;
+ for (size_t pos = 0; pos < names.size(); ++pos) {
+ char c = names[pos];
+ switch (c) {
+ case '[':
+ case '{':
+ case '(':
+ // It is basically impossible to disambiguate between
+ // comparison and start of template args in this context
+// case '<':
+ openings.push(c);
+ break;
+ case ']':
+ case '}':
+ case ')':
+// case '>':
+ openings.pop();
+ break;
+ case '"':
+ case '\'':
+ pos = skipq(pos, c);
+ break;
+ case ',':
+ if (start != pos && openings.empty()) {
+ m_messages.emplace_back(macroName, lineInfo, resultType);
+ m_messages.back().message = static_cast(trimmed(start, pos));
+ m_messages.back().message += " := ";
+ start = pos;
+ }
+ break;
+ default:; // noop
+ }
+ }
+ assert(openings.empty() && "Mismatched openings");
+ m_messages.emplace_back(macroName, lineInfo, resultType);
+ m_messages.back().message = static_cast(trimmed(start, names.size() - 1));
+ m_messages.back().message += " := ";
+ }
+ Capturer::~Capturer() {
+ if ( !uncaught_exceptions() ){
+ assert( m_captured == m_messages.size() );
+ for( size_t i = 0; i < m_captured; ++i )
+ m_resultCapture.popScopedMessage( m_messages[i] );
+ }
+ }
+
+ void Capturer::captureValue( size_t index, std::string const& value ) {
+ assert( index < m_messages.size() );
+ m_messages[index].message += value;
+ m_resultCapture.pushScopedMessage( m_messages[index] );
+ m_captured++;
+ }
+
+} // end namespace Catch
+
+
+
+
+#include
+
+namespace Catch {
+
+ namespace {
+
+ class RegistryHub : public IRegistryHub,
+ public IMutableRegistryHub,
+ private Detail::NonCopyable {
+
+ public: // IRegistryHub
+ RegistryHub() = default;
+ ReporterRegistry const& getReporterRegistry() const override {
+ return m_reporterRegistry;
+ }
+ ITestCaseRegistry const& getTestCaseRegistry() const override {
+ return m_testCaseRegistry;
+ }
+ IExceptionTranslatorRegistry const& getExceptionTranslatorRegistry() const override {
+ return m_exceptionTranslatorRegistry;
+ }
+ ITagAliasRegistry const& getTagAliasRegistry() const override {
+ return m_tagAliasRegistry;
+ }
+ StartupExceptionRegistry const& getStartupExceptionRegistry() const override {
+ return m_exceptionRegistry;
+ }
+
+ public: // IMutableRegistryHub
+ void registerReporter( std::string const& name, IReporterFactoryPtr factory ) override {
+ m_reporterRegistry.registerReporter( name, CATCH_MOVE(factory) );
+ }
+ void registerListener( Detail::unique_ptr factory ) override {
+ m_reporterRegistry.registerListener( CATCH_MOVE(factory) );
+ }
+ void registerTest( Detail::unique_ptr&& testInfo, Detail::unique_ptr&& invoker ) override {
+ m_testCaseRegistry.registerTest( CATCH_MOVE(testInfo), CATCH_MOVE(invoker) );
+ }
+ void registerTranslator( Detail::unique_ptr&& translator ) override {
+ m_exceptionTranslatorRegistry.registerTranslator( CATCH_MOVE(translator) );
+ }
+ void registerTagAlias( std::string const& alias, std::string const& tag, SourceLineInfo const& lineInfo ) override {
+ m_tagAliasRegistry.add( alias, tag, lineInfo );
+ }
+ void registerStartupException() noexcept override {
+#if !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+ m_exceptionRegistry.add(std::current_exception());
+#else
+ CATCH_INTERNAL_ERROR("Attempted to register active exception under CATCH_CONFIG_DISABLE_EXCEPTIONS!");
+#endif
+ }
+ IMutableEnumValuesRegistry& getMutableEnumValuesRegistry() override {
+ return m_enumValuesRegistry;
+ }
+
+ private:
+ TestRegistry m_testCaseRegistry;
+ ReporterRegistry m_reporterRegistry;
+ ExceptionTranslatorRegistry m_exceptionTranslatorRegistry;
+ TagAliasRegistry m_tagAliasRegistry;
+ StartupExceptionRegistry m_exceptionRegistry;
+ Detail::EnumValuesRegistry m_enumValuesRegistry;
+ };
+ }
+
+ using RegistryHubSingleton = Singleton;
+
+ IRegistryHub const& getRegistryHub() {
+ return RegistryHubSingleton::get();
+ }
+ IMutableRegistryHub& getMutableRegistryHub() {
+ return RegistryHubSingleton::getMutable();
+ }
+ void cleanUp() {
+ cleanupSingletons();
+ cleanUpContext();
+ }
+ std::string translateActiveException() {
+ return getRegistryHub().getExceptionTranslatorRegistry().translateActiveException();
+ }
+
+
+} // end namespace Catch
+
+
+
+#include
+#include
+#include
+#include
+#include
+
+namespace Catch {
+
+ namespace {
+ static constexpr int TestFailureExitCode = 42;
+ static constexpr int UnspecifiedErrorExitCode = 1;
+ static constexpr int AllTestsSkippedExitCode = 4;
+ static constexpr int NoTestsRunExitCode = 2;
+ static constexpr int UnmatchedTestSpecExitCode = 3;
+ static constexpr int InvalidTestSpecExitCode = 5;
+
+
+ IEventListenerPtr createReporter(std::string const& reporterName, ReporterConfig&& config) {
+ auto reporter = Catch::getRegistryHub().getReporterRegistry().create(reporterName, CATCH_MOVE(config));
+ CATCH_ENFORCE(reporter, "No reporter registered with name: '" << reporterName << '\'');
+
+ return reporter;
+ }
+
+ IEventListenerPtr prepareReporters(Config const* config) {
+ if (Catch::getRegistryHub().getReporterRegistry().getListeners().empty()
+ && config->getProcessedReporterSpecs().size() == 1) {
+ auto const& spec = config->getProcessedReporterSpecs()[0];
+ return createReporter(
+ spec.name,
+ ReporterConfig( config,
+ makeStream( spec.outputFilename ),
+ spec.colourMode,
+ spec.customOptions ) );
+ }
+
+ auto multi = Detail::make_unique(config);
+
+ auto const& listeners = Catch::getRegistryHub().getReporterRegistry().getListeners();
+ for (auto const& listener : listeners) {
+ multi->addListener(listener->create(config));
+ }
+
+ for ( auto const& reporterSpec : config->getProcessedReporterSpecs() ) {
+ multi->addReporter( createReporter(
+ reporterSpec.name,
+ ReporterConfig( config,
+ makeStream( reporterSpec.outputFilename ),
+ reporterSpec.colourMode,
+ reporterSpec.customOptions ) ) );
+ }
+
+ return multi;
+ }
+
+ class TestGroup {
+ public:
+ explicit TestGroup(IEventListenerPtr&& reporter, Config const* config):
+ m_reporter(reporter.get()),
+ m_config{config},
+ m_context{config, CATCH_MOVE(reporter)} {
+
+ assert( m_config->testSpec().getInvalidSpecs().empty() &&
+ "Invalid test specs should be handled before running tests" );
+
+ auto const& allTestCases = getAllTestCasesSorted(*m_config);
+ auto const& testSpec = m_config->testSpec();
+ if ( !testSpec.hasFilters() ) {
+ for ( auto const& test : allTestCases ) {
+ if ( !test.getTestCaseInfo().isHidden() ) {
+ m_tests.emplace( &test );
+ }
+ }
+ } else {
+ m_matches =
+ testSpec.matchesByFilter( allTestCases, *m_config );
+ for ( auto const& match : m_matches ) {
+ m_tests.insert( match.tests.begin(),
+ match.tests.end() );
+ }
+ }
+
+ m_tests = createShard(m_tests, m_config->shardCount(), m_config->shardIndex());
+ }
+
+ Totals execute() {
+ Totals totals;
+ for (auto const& testCase : m_tests) {
+ if (!m_context.aborting())
+ totals += m_context.runTest(*testCase);
+ else
+ m_reporter->skipTest(testCase->getTestCaseInfo());
+ }
+
+ for (auto const& match : m_matches) {
+ if (match.tests.empty()) {
+ m_unmatchedTestSpecs = true;
+ m_reporter->noMatchingTestCases( match.name );
+ }
+ }
+
+ return totals;
+ }
+
+ bool hadUnmatchedTestSpecs() const {
+ return m_unmatchedTestSpecs;
+ }
+
+
+ private:
+ IEventListener* m_reporter;
+ Config const* m_config;
+ RunContext m_context;
+ std::set m_tests;
+ TestSpec::Matches m_matches;
+ bool m_unmatchedTestSpecs = false;
+ };
+
+ void applyFilenamesAsTags() {
+ for (auto const& testInfo : getRegistryHub().getTestCaseRegistry().getAllInfos()) {
+ testInfo->addFilenameTag();
+ }
+ }
+
+ } // anon namespace
+
+ Session::Session() {
+ static bool alreadyInstantiated = false;
+ if( alreadyInstantiated ) {
+ CATCH_TRY { CATCH_INTERNAL_ERROR( "Only one instance of Catch::Session can ever be used" ); }
+ CATCH_CATCH_ALL { getMutableRegistryHub().registerStartupException(); }
+ }
+
+ // There cannot be exceptions at startup in no-exception mode.
+#if !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+ const auto& exceptions = getRegistryHub().getStartupExceptionRegistry().getExceptions();
+ if ( !exceptions.empty() ) {
+ config();
+ getCurrentMutableContext().setConfig(m_config.get());
+
+ m_startupExceptions = true;
+ auto errStream = makeStream( "%stderr" );
+ auto colourImpl = makeColourImpl(
+ ColourMode::PlatformDefault, errStream.get() );
+ auto guard = colourImpl->guardColour( Colour::Red );
+ errStream->stream() << "Errors occurred during startup!" << '\n';
+ // iterate over all exceptions and notify user
+ for ( const auto& ex_ptr : exceptions ) {
+ try {
+ std::rethrow_exception(ex_ptr);
+ } catch ( std::exception const& ex ) {
+ errStream->stream() << TextFlow::Column( ex.what() ).indent(2) << '\n';
+ }
+ }
+ }
+#endif
+
+ alreadyInstantiated = true;
+ m_cli = makeCommandLineParser( m_configData );
+ }
+ Session::~Session() {
+ Catch::cleanUp();
+ }
+
+ void Session::showHelp() const {
+ Catch::cout()
+ << "\nCatch2 v" << libraryVersion() << '\n'
+ << m_cli << '\n'
+ << "For more detailed usage please see the project docs\n\n" << std::flush;
+ }
+ void Session::libIdentify() {
+ Catch::cout()
+ << std::left << std::setw(16) << "description: " << "A Catch2 test executable\n"
+ << std::left << std::setw(16) << "category: " << "testframework\n"
+ << std::left << std::setw(16) << "framework: " << "Catch2\n"
+ << std::left << std::setw(16) << "version: " << libraryVersion() << '\n' << std::flush;
+ }
+
+ int Session::applyCommandLine( int argc, char const * const * argv ) {
+ if ( m_startupExceptions ) { return UnspecifiedErrorExitCode; }
+
+ auto result = m_cli.parse( Clara::Args( argc, argv ) );
+
+ if( !result ) {
+ config();
+ getCurrentMutableContext().setConfig(m_config.get());
+ auto errStream = makeStream( "%stderr" );
+ auto colour = makeColourImpl( ColourMode::PlatformDefault, errStream.get() );
+
+ errStream->stream()
+ << colour->guardColour( Colour::Red )
+ << "\nError(s) in input:\n"
+ << TextFlow::Column( result.errorMessage() ).indent( 2 )
+ << "\n\n";
+ errStream->stream() << "Run with -? for usage\n\n" << std::flush;
+ return UnspecifiedErrorExitCode;
+ }
+
+ if( m_configData.showHelp )
+ showHelp();
+ if( m_configData.libIdentify )
+ libIdentify();
+
+ m_config.reset();
+ return 0;
+ }
+
+#if defined(CATCH_CONFIG_WCHAR) && defined(_WIN32) && defined(UNICODE)
+ int Session::applyCommandLine( int argc, wchar_t const * const * argv ) {
+
+ char **utf8Argv = new char *[ argc ];
+
+ for ( int i = 0; i < argc; ++i ) {
+ int bufSize = WideCharToMultiByte( CP_UTF8, 0, argv[i], -1, nullptr, 0, nullptr, nullptr );
+
+ utf8Argv[ i ] = new char[ bufSize ];
+
+ WideCharToMultiByte( CP_UTF8, 0, argv[i], -1, utf8Argv[i], bufSize, nullptr, nullptr );
+ }
+
+ int returnCode = applyCommandLine( argc, utf8Argv );
+
+ for ( int i = 0; i < argc; ++i )
+ delete [] utf8Argv[ i ];
+
+ delete [] utf8Argv;
+
+ return returnCode;
+ }
+#endif
+
+ void Session::useConfigData( ConfigData const& configData ) {
+ m_configData = configData;
+ m_config.reset();
+ }
+
+ int Session::run() {
+ if( ( m_configData.waitForKeypress & WaitForKeypress::BeforeStart ) != 0 ) {
+ Catch::cout() << "...waiting for enter/ return before starting\n" << std::flush;
+ static_cast(std::getchar());
+ }
+ int exitCode = runInternal();
+ if( ( m_configData.waitForKeypress & WaitForKeypress::BeforeExit ) != 0 ) {
+ Catch::cout() << "...waiting for enter/ return before exiting, with code: " << exitCode << '\n' << std::flush;
+ static_cast(std::getchar());
+ }
+ return exitCode;
+ }
+
+ Clara::Parser const& Session::cli() const {
+ return m_cli;
+ }
+ void Session::cli( Clara::Parser const& newParser ) {
+ m_cli = newParser;
+ }
+ ConfigData& Session::configData() {
+ return m_configData;
+ }
+ Config& Session::config() {
+ if( !m_config )
+ m_config = Detail::make_unique( m_configData );
+ return *m_config;
+ }
+
+ int Session::runInternal() {
+ if ( m_startupExceptions ) { return UnspecifiedErrorExitCode; }
+
+ if (m_configData.showHelp || m_configData.libIdentify) {
+ return 0;
+ }
+
+ if ( m_configData.shardIndex >= m_configData.shardCount ) {
+ Catch::cerr() << "The shard count (" << m_configData.shardCount
+ << ") must be greater than the shard index ("
+ << m_configData.shardIndex << ")\n"
+ << std::flush;
+ return UnspecifiedErrorExitCode;
+ }
+
+ CATCH_TRY {
+ config(); // Force config to be constructed
+
+ seedRng( *m_config );
+
+ if (m_configData.filenamesAsTags) {
+ applyFilenamesAsTags();
+ }
+
+ // Set up global config instance before we start calling into other functions
+ getCurrentMutableContext().setConfig(m_config.get());
+
+ // Create reporter(s) so we can route listings through them
+ auto reporter = prepareReporters(m_config.get());
+
+ auto const& invalidSpecs = m_config->testSpec().getInvalidSpecs();
+ if ( !invalidSpecs.empty() ) {
+ for ( auto const& spec : invalidSpecs ) {
+ reporter->reportInvalidTestSpec( spec );
+ }
+ return InvalidTestSpecExitCode;
+ }
+
+
+ // Handle list request
+ if (list(*reporter, *m_config)) {
+ return 0;
+ }
+
+ TestGroup tests { CATCH_MOVE(reporter), m_config.get() };
+ auto const totals = tests.execute();
+
+ if ( tests.hadUnmatchedTestSpecs()
+ && m_config->warnAboutUnmatchedTestSpecs() ) {
+ // UnmatchedTestSpecExitCode
+ return UnmatchedTestSpecExitCode;
+ }
+
+ if ( totals.testCases.total() == 0
+ && !m_config->zeroTestsCountAsSuccess() ) {
+ return NoTestsRunExitCode;
+ }
+
+ if ( totals.testCases.total() > 0 &&
+ totals.testCases.total() == totals.testCases.skipped
+ && !m_config->zeroTestsCountAsSuccess() ) {
+ return AllTestsSkippedExitCode;
+ }
+
+ if ( totals.assertions.failed ) { return TestFailureExitCode; }
+ return 0;
+
+ }
+#if !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+ catch( std::exception& ex ) {
+ Catch::cerr() << ex.what() << '\n' << std::flush;
+ return UnspecifiedErrorExitCode;
+ }
+#endif
+ }
+
+} // end namespace Catch
+
+
+
+
+namespace Catch {
+
+ RegistrarForTagAliases::RegistrarForTagAliases(char const* alias, char const* tag, SourceLineInfo const& lineInfo) {
+ CATCH_TRY {
+ getMutableRegistryHub().registerTagAlias(alias, tag, lineInfo);
+ } CATCH_CATCH_ALL {
+ // Do not throw when constructing global objects, instead register the exception to be processed later
+ getMutableRegistryHub().registerStartupException();
+ }
+ }
+
+}
+
+
+
+#include
+#include
+#include
+
+namespace Catch {
+
+ namespace {
+ using TCP_underlying_type = uint8_t;
+ static_assert(sizeof(TestCaseProperties) == sizeof(TCP_underlying_type),
+ "The size of the TestCaseProperties is different from the assumed size");
+
+ constexpr TestCaseProperties operator|(TestCaseProperties lhs, TestCaseProperties rhs) {
+ return static_cast(
+ static_cast(lhs) | static_cast(rhs)
+ );
+ }
+
+ constexpr TestCaseProperties& operator|=(TestCaseProperties& lhs, TestCaseProperties rhs) {
+ lhs = static_cast(
+ static_cast(lhs) | static_cast(rhs)
+ );
+ return lhs;
+ }
+
+ constexpr TestCaseProperties operator&(TestCaseProperties lhs, TestCaseProperties rhs) {
+ return static_cast(
+ static_cast(lhs) & static_cast(rhs)
+ );
+ }
+
+ constexpr bool applies(TestCaseProperties tcp) {
+ static_assert(static_cast(TestCaseProperties::None) == 0,
+ "TestCaseProperties::None must be equal to 0");
+ return tcp != TestCaseProperties::None;
+ }
+
+ TestCaseProperties parseSpecialTag( StringRef tag ) {
+ if( !tag.empty() && tag[0] == '.' )
+ return TestCaseProperties::IsHidden;
+ else if( tag == "!throws"_sr )
+ return TestCaseProperties::Throws;
+ else if( tag == "!shouldfail"_sr )
+ return TestCaseProperties::ShouldFail;
+ else if( tag == "!mayfail"_sr )
+ return TestCaseProperties::MayFail;
+ else if( tag == "!nonportable"_sr )
+ return TestCaseProperties::NonPortable;
+ else if( tag == "!benchmark"_sr )
+ return TestCaseProperties::Benchmark | TestCaseProperties::IsHidden;
+ else
+ return TestCaseProperties::None;
+ }
+ bool isReservedTag( StringRef tag ) {
+ return parseSpecialTag( tag ) == TestCaseProperties::None
+ && tag.size() > 0
+ && !std::isalnum( static_cast(tag[0]) );
+ }
+ void enforceNotReservedTag( StringRef tag, SourceLineInfo const& _lineInfo ) {
+ CATCH_ENFORCE( !isReservedTag(tag),
+ "Tag name: [" << tag << "] is not allowed.\n"
+ << "Tag names starting with non alphanumeric characters are reserved\n"
+ << _lineInfo );
+ }
+
+ std::string makeDefaultName() {
+ static size_t counter = 0;
+ return "Anonymous test case " + std::to_string(++counter);
+ }
+
+ constexpr StringRef extractFilenamePart(StringRef filename) {
+ size_t lastDot = filename.size();
+ while (lastDot > 0 && filename[lastDot - 1] != '.') {
+ --lastDot;
+ }
+ // In theory we could have filename without any extension in it
+ if ( lastDot == 0 ) { return StringRef(); }
+
+ --lastDot;
+ size_t nameStart = lastDot;
+ while (nameStart > 0 && filename[nameStart - 1] != '/' && filename[nameStart - 1] != '\\') {
+ --nameStart;
+ }
+
+ return filename.substr(nameStart, lastDot - nameStart);
+ }
+
+ // Returns the upper bound on size of extra tags ([#file]+[.])
+ constexpr size_t sizeOfExtraTags(StringRef filepath) {
+ // [.] is 3, [#] is another 3
+ const size_t extras = 3 + 3;
+ return extractFilenamePart(filepath).size() + extras;
+ }
+ } // end unnamed namespace
+
+ bool operator<( Tag const& lhs, Tag const& rhs ) {
+ Detail::CaseInsensitiveLess cmp;
+ return cmp( lhs.original, rhs.original );
+ }
+ bool operator==( Tag const& lhs, Tag const& rhs ) {
+ Detail::CaseInsensitiveEqualTo cmp;
+ return cmp( lhs.original, rhs.original );
+ }
+
+ Detail::unique_ptr
+ makeTestCaseInfo(StringRef _className,
+ NameAndTags const& nameAndTags,
+ SourceLineInfo const& _lineInfo ) {
+ return Detail::make_unique(_className, nameAndTags, _lineInfo);
+ }
+
+ TestCaseInfo::TestCaseInfo(StringRef _className,
+ NameAndTags const& _nameAndTags,
+ SourceLineInfo const& _lineInfo):
+ name( _nameAndTags.name.empty() ? makeDefaultName() : _nameAndTags.name ),
+ className( _className ),
+ lineInfo( _lineInfo )
+ {
+ StringRef originalTags = _nameAndTags.tags;
+ // We need to reserve enough space to store all of the tags
+ // (including optional hidden tag and filename tag)
+ auto requiredSize = originalTags.size() + sizeOfExtraTags(_lineInfo.file);
+ backingTags.reserve(requiredSize);
+
+ // We cannot copy the tags directly, as we need to normalize
+ // some tags, so that [.foo] is copied as [.][foo].
+ size_t tagStart = 0;
+ size_t tagEnd = 0;
+ bool inTag = false;
+ for (size_t idx = 0; idx < originalTags.size(); ++idx) {
+ auto c = originalTags[idx];
+ if (c == '[') {
+ CATCH_ENFORCE(
+ !inTag,
+ "Found '[' inside a tag while registering test case '"
+ << _nameAndTags.name << "' at " << _lineInfo );
+
+ inTag = true;
+ tagStart = idx;
+ }
+ if (c == ']') {
+ CATCH_ENFORCE(
+ inTag,
+ "Found unmatched ']' while registering test case '"
+ << _nameAndTags.name << "' at " << _lineInfo );
+
+ inTag = false;
+ tagEnd = idx;
+ assert(tagStart < tagEnd);
+
+ // We need to check the tag for special meanings, copy
+ // it over to backing storage and actually reference the
+ // backing storage in the saved tags
+ StringRef tagStr = originalTags.substr(tagStart+1, tagEnd - tagStart - 1);
+ CATCH_ENFORCE( !tagStr.empty(),
+ "Found an empty tag while registering test case '"
+ << _nameAndTags.name << "' at "
+ << _lineInfo );
+
+ enforceNotReservedTag(tagStr, lineInfo);
+ properties |= parseSpecialTag(tagStr);
+ // When copying a tag to the backing storage, we need to
+ // check if it is a merged hide tag, such as [.foo], and
+ // if it is, we need to handle it as if it was [foo].
+ if (tagStr.size() > 1 && tagStr[0] == '.') {
+ tagStr = tagStr.substr(1, tagStr.size() - 1);
+ }
+ // We skip over dealing with the [.] tag, as we will add
+ // it later unconditionally and then sort and unique all
+ // the tags.
+ internalAppendTag(tagStr);
+ }
+ }
+ CATCH_ENFORCE( !inTag,
+ "Found an unclosed tag while registering test case '"
+ << _nameAndTags.name << "' at " << _lineInfo );
+
+
+ // Add [.] if relevant
+ if (isHidden()) {
+ internalAppendTag("."_sr);
+ }
+
+ // Sort and prepare tags
+ std::sort(begin(tags), end(tags));
+ tags.erase(std::unique(begin(tags), end(tags)),
+ end(tags));
+ }
+
+ bool TestCaseInfo::isHidden() const {
+ return applies( properties & TestCaseProperties::IsHidden );
+ }
+ bool TestCaseInfo::throws() const {
+ return applies( properties & TestCaseProperties::Throws );
+ }
+ bool TestCaseInfo::okToFail() const {
+ return applies( properties & (TestCaseProperties::ShouldFail | TestCaseProperties::MayFail ) );
+ }
+ bool TestCaseInfo::expectedToFail() const {
+ return applies( properties & (TestCaseProperties::ShouldFail) );
+ }
+
+ void TestCaseInfo::addFilenameTag() {
+ std::string combined("#");
+ combined += extractFilenamePart(lineInfo.file);
+ internalAppendTag(combined);
+ }
+
+ std::string TestCaseInfo::tagsAsString() const {
+ std::string ret;
+ // '[' and ']' per tag
+ std::size_t full_size = 2 * tags.size();
+ for (const auto& tag : tags) {
+ full_size += tag.original.size();
+ }
+ ret.reserve(full_size);
+ for (const auto& tag : tags) {
+ ret.push_back('[');
+ ret += tag.original;
+ ret.push_back(']');
+ }
+
+ return ret;
+ }
+
+ void TestCaseInfo::internalAppendTag(StringRef tagStr) {
+ backingTags += '[';
+ const auto backingStart = backingTags.size();
+ backingTags += tagStr;
+ const auto backingEnd = backingTags.size();
+ backingTags += ']';
+ tags.emplace_back(StringRef(backingTags.c_str() + backingStart, backingEnd - backingStart));
+ }
+
+ bool operator<( TestCaseInfo const& lhs, TestCaseInfo const& rhs ) {
+ // We want to avoid redoing the string comparisons multiple times,
+ // so we store the result of a three-way comparison before using
+ // it in the actual comparison logic.
+ const auto cmpName = lhs.name.compare( rhs.name );
+ if ( cmpName != 0 ) {
+ return cmpName < 0;
+ }
+ const auto cmpClassName = lhs.className.compare( rhs.className );
+ if ( cmpClassName != 0 ) {
+ return cmpClassName < 0;
+ }
+ return lhs.tags < rhs.tags;
+ }
+
+} // end namespace Catch
+
+
+
+#include
+#include
+#include
+#include
+
+namespace Catch {
+
+ TestSpec::Pattern::Pattern( std::string const& name )
+ : m_name( name )
+ {}
+
+ TestSpec::Pattern::~Pattern() = default;
+
+ std::string const& TestSpec::Pattern::name() const {
+ return m_name;
+ }
+
+
+ TestSpec::NamePattern::NamePattern( std::string const& name, std::string const& filterString )
+ : Pattern( filterString )
+ , m_wildcardPattern( toLower( name ), CaseSensitive::No )
+ {}
+
+ bool TestSpec::NamePattern::matches( TestCaseInfo const& testCase ) const {
+ return m_wildcardPattern.matches( testCase.name );
+ }
+
+ void TestSpec::NamePattern::serializeTo( std::ostream& out ) const {
+ out << '"' << name() << '"';
+ }
+
+
+ TestSpec::TagPattern::TagPattern( std::string const& tag, std::string const& filterString )
+ : Pattern( filterString )
+ , m_tag( tag )
+ {}
+
+ bool TestSpec::TagPattern::matches( TestCaseInfo const& testCase ) const {
+ return std::find( begin( testCase.tags ),
+ end( testCase.tags ),
+ Tag( m_tag ) ) != end( testCase.tags );
+ }
+
+ void TestSpec::TagPattern::serializeTo( std::ostream& out ) const {
+ out << name();
+ }
+
+ bool TestSpec::Filter::matches( TestCaseInfo const& testCase ) const {
+ bool should_use = !testCase.isHidden();
+ for (auto const& pattern : m_required) {
+ should_use = true;
+ if (!pattern->matches(testCase)) {
+ return false;
+ }
+ }
+ for (auto const& pattern : m_forbidden) {
+ if (pattern->matches(testCase)) {
+ return false;
+ }
+ }
+ return should_use;
+ }
+
+ void TestSpec::Filter::serializeTo( std::ostream& out ) const {
+ bool first = true;
+ for ( auto const& pattern : m_required ) {
+ if ( !first ) {
+ out << ' ';
+ }
+ out << *pattern;
+ first = false;
+ }
+ for ( auto const& pattern : m_forbidden ) {
+ if ( !first ) {
+ out << ' ';
+ }
+ out << *pattern;
+ first = false;
+ }
+ }
+
+
+ std::string TestSpec::extractFilterName( Filter const& filter ) {
+ Catch::ReusableStringStream sstr;
+ sstr << filter;
+ return sstr.str();
+ }
+
+ bool TestSpec::hasFilters() const {
+ return !m_filters.empty();
+ }
+
+ bool TestSpec::matches( TestCaseInfo const& testCase ) const {
+ return std::any_of( m_filters.begin(), m_filters.end(), [&]( Filter const& f ){ return f.matches( testCase ); } );
+ }
+
+ TestSpec::Matches TestSpec::matchesByFilter( std::vector const& testCases, IConfig const& config ) const {
+ Matches matches;
+ matches.reserve( m_filters.size() );
+ for ( auto const& filter : m_filters ) {
+ std::vector currentMatches;
+ for ( auto const& test : testCases )
+ if ( isThrowSafe( test, config ) &&
+ filter.matches( test.getTestCaseInfo() ) )
+ currentMatches.emplace_back( &test );
+ matches.push_back(
+ FilterMatch{ extractFilterName( filter ), currentMatches } );
+ }
+ return matches;
+ }
+
+ const TestSpec::vectorStrings& TestSpec::getInvalidSpecs() const {
+ return m_invalidSpecs;
+ }
+
+ void TestSpec::serializeTo( std::ostream& out ) const {
+ bool first = true;
+ for ( auto const& filter : m_filters ) {
+ if ( !first ) {
+ out << ',';
+ }
+ out << filter;
+ first = false;
+ }
+ }
+
+}
+
+
+
+#include
+
+namespace Catch {
+
+ namespace {
+ static auto getCurrentNanosecondsSinceEpoch() -> uint64_t {
+ return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count();
+ }
+ } // end unnamed namespace
+
+ void Timer::start() {
+ m_nanoseconds = getCurrentNanosecondsSinceEpoch();
+ }
+ auto Timer::getElapsedNanoseconds() const -> uint64_t {
+ return getCurrentNanosecondsSinceEpoch() - m_nanoseconds;
+ }
+ auto Timer::getElapsedMicroseconds() const -> uint64_t {
+ return getElapsedNanoseconds()/1000;
+ }
+ auto Timer::getElapsedMilliseconds() const -> unsigned int {
+ return static_cast(getElapsedMicroseconds()/1000);
+ }
+ auto Timer::getElapsedSeconds() const -> double {
+ return getElapsedMicroseconds()/1000000.0;
+ }
+
+
+} // namespace Catch
+
+
+
+
+#include
+#include
+
+namespace Catch {
+
+namespace Detail {
+
+ namespace {
+ const int hexThreshold = 255;
+
+ struct Endianness {
+ enum Arch { Big, Little };
+
+ static Arch which() {
+ int one = 1;
+ // If the lowest byte we read is non-zero, we can assume
+ // that little endian format is used.
+ auto value = *reinterpret_cast(&one);
+ return value ? Little : Big;
+ }
+ };
+
+ template
+ std::string fpToString(T value, int precision) {
+ if (Catch::isnan(value)) {
+ return "nan";
+ }
+
+ ReusableStringStream rss;
+ rss << std::setprecision(precision)
+ << std::fixed
+ << value;
+ std::string d = rss.str();
+ std::size_t i = d.find_last_not_of('0');
+ if (i != std::string::npos && i != d.size() - 1) {
+ if (d[i] == '.')
+ i++;
+ d = d.substr(0, i + 1);
+ }
+ return d;
+ }
+ } // end unnamed namespace
+
+ std::string convertIntoString(StringRef string, bool escapeInvisibles) {
+ std::string ret;
+ // This is enough for the "don't escape invisibles" case, and a good
+ // lower bound on the "escape invisibles" case.
+ ret.reserve(string.size() + 2);
+
+ if (!escapeInvisibles) {
+ ret += '"';
+ ret += string;
+ ret += '"';
+ return ret;
+ }
+
+ ret += '"';
+ for (char c : string) {
+ switch (c) {
+ case '\r':
+ ret.append("\\r");
+ break;
+ case '\n':
+ ret.append("\\n");
+ break;
+ case '\t':
+ ret.append("\\t");
+ break;
+ case '\f':
+ ret.append("\\f");
+ break;
+ default:
+ ret.push_back(c);
+ break;
+ }
+ }
+ ret += '"';
+
+ return ret;
+ }
+
+ std::string convertIntoString(StringRef string) {
+ return convertIntoString(string, getCurrentContext().getConfig()->showInvisibles());
+ }
+
+ std::string rawMemoryToString( const void *object, std::size_t size ) {
+ // Reverse order for little endian architectures
+ int i = 0, end = static_cast( size ), inc = 1;
+ if( Endianness::which() == Endianness::Little ) {
+ i = end-1;
+ end = inc = -1;
+ }
+
+ unsigned char const *bytes = static_cast(object);
+ ReusableStringStream rss;
+ rss << "0x" << std::setfill('0') << std::hex;
+ for( ; i != end; i += inc )
+ rss << std::setw(2) << static_cast(bytes[i]);
+ return rss.str();
+ }
+} // end Detail namespace
+
+
+
+//// ======================================================= ////
+//
+// Out-of-line defs for full specialization of StringMaker
+//
+//// ======================================================= ////
+
+std::string StringMaker::convert(const std::string& str) {
+ return Detail::convertIntoString( str );
+}
+
+#ifdef CATCH_CONFIG_CPP17_STRING_VIEW
+std::string StringMaker::convert(std::string_view str) {
+ return Detail::convertIntoString( StringRef( str.data(), str.size() ) );
+}
+#endif
+
+std::string StringMaker::convert(char const* str) {
+ if (str) {
+ return Detail::convertIntoString( str );
+ } else {
+ return{ "{null string}" };
+ }
+}
+std::string StringMaker::convert(char* str) { // NOLINT(readability-non-const-parameter)
+ if (str) {
+ return Detail::convertIntoString( str );
+ } else {
+ return{ "{null string}" };
+ }
+}
+
+#ifdef CATCH_CONFIG_WCHAR
+std::string StringMaker::convert(const std::wstring& wstr) {
+ std::string s;
+ s.reserve(wstr.size());
+ for (auto c : wstr) {
+ s += (c <= 0xff) ? static_cast(c) : '?';
+ }
+ return ::Catch::Detail::stringify(s);
+}
+
+# ifdef CATCH_CONFIG_CPP17_STRING_VIEW
+std::string StringMaker::convert(std::wstring_view str) {
+ return StringMaker::convert(std::wstring(str));
+}
+# endif
+
+std::string StringMaker::convert(wchar_t const * str) {
+ if (str) {
+ return ::Catch::Detail::stringify(std::wstring{ str });
+ } else {
+ return{ "{null string}" };
+ }
+}
+std::string StringMaker::convert(wchar_t * str) {
+ if (str) {
+ return ::Catch::Detail::stringify(std::wstring{ str });
+ } else {
+ return{ "{null string}" };
+ }
+}
+#endif
+
+#if defined(CATCH_CONFIG_CPP17_BYTE)
+#include
+std::string StringMaker::convert(std::byte value) {
+ return ::Catch::Detail::stringify(std::to_integer(value));
+}
+#endif // defined(CATCH_CONFIG_CPP17_BYTE)
+
+std::string StringMaker::convert(int value) {
+ return ::Catch::Detail::stringify(static_cast(value));
+}
+std::string StringMaker::convert(long value) {
+ return ::Catch::Detail::stringify(static_cast(value));
+}
+std::string StringMaker::convert(long long value) {
+ ReusableStringStream rss;
+ rss << value;
+ if (value > Detail::hexThreshold) {
+ rss << " (0x" << std::hex << value << ')';
+ }
+ return rss.str();
+}
+
+std::string StringMaker::convert(unsigned int value) {
+ return ::Catch::Detail::stringify(static_cast(value));
+}
+std::string StringMaker::convert(unsigned long value) {
+ return ::Catch::Detail::stringify(static_cast(value));
+}
+std::string StringMaker::convert(unsigned long long value) {
+ ReusableStringStream rss;
+ rss << value;
+ if (value > Detail::hexThreshold) {
+ rss << " (0x" << std::hex << value << ')';
+ }
+ return rss.str();
+}
+
+std::string StringMaker::convert(signed char value) {
+ if (value == '\r') {
+ return "'\\r'";
+ } else if (value == '\f') {
+ return "'\\f'";
+ } else if (value == '\n') {
+ return "'\\n'";
+ } else if (value == '\t') {
+ return "'\\t'";
+ } else if ('\0' <= value && value < ' ') {
+ return ::Catch::Detail::stringify(static_cast(value));
+ } else {
+ char chstr[] = "' '";
+ chstr[1] = value;
+ return chstr;
+ }
+}
+std::string StringMaker::convert(char c) {
+ return ::Catch::Detail::stringify(static_cast(c));
+}
+std::string StringMaker::convert(unsigned char value) {
+ return ::Catch::Detail::stringify(static_cast(value));
+}
+
+int StringMaker::precision = std::numeric_limits::max_digits10;
+
+std::string StringMaker::convert(float value) {
+ return Detail::fpToString(value, precision) + 'f';
+}
+
+int StringMaker::precision = std::numeric_limits::max_digits10;
+
+std::string StringMaker::convert(double value) {
+ return Detail::fpToString(value, precision);
+}
+
+} // end namespace Catch
+
+
+
+namespace Catch {
+
+ Counts Counts::operator - ( Counts const& other ) const {
+ Counts diff;
+ diff.passed = passed - other.passed;
+ diff.failed = failed - other.failed;
+ diff.failedButOk = failedButOk - other.failedButOk;
+ diff.skipped = skipped - other.skipped;
+ return diff;
+ }
+
+ Counts& Counts::operator += ( Counts const& other ) {
+ passed += other.passed;
+ failed += other.failed;
+ failedButOk += other.failedButOk;
+ skipped += other.skipped;
+ return *this;
+ }
+
+ std::uint64_t Counts::total() const {
+ return passed + failed + failedButOk + skipped;
+ }
+ bool Counts::allPassed() const {
+ return failed == 0 && failedButOk == 0 && skipped == 0;
+ }
+ bool Counts::allOk() const {
+ return failed == 0;
+ }
+
+ Totals Totals::operator - ( Totals const& other ) const {
+ Totals diff;
+ diff.assertions = assertions - other.assertions;
+ diff.testCases = testCases - other.testCases;
+ return diff;
+ }
+
+ Totals& Totals::operator += ( Totals const& other ) {
+ assertions += other.assertions;
+ testCases += other.testCases;
+ return *this;
+ }
+
+ Totals Totals::delta( Totals const& prevTotals ) const {
+ Totals diff = *this - prevTotals;
+ if( diff.assertions.failed > 0 )
+ ++diff.testCases.failed;
+ else if( diff.assertions.failedButOk > 0 )
+ ++diff.testCases.failedButOk;
+ else if ( diff.assertions.skipped > 0 )
+ ++ diff.testCases.skipped;
+ else
+ ++diff.testCases.passed;
+ return diff;
+ }
+
+}
+
+
+
+
+namespace Catch {
+ namespace Detail {
+ void registerTranslatorImpl(
+ Detail::unique_ptr&& translator ) {
+ getMutableRegistryHub().registerTranslator(
+ CATCH_MOVE( translator ) );
+ }
+ } // namespace Detail
+} // namespace Catch
+
+
+#include
+
+namespace Catch {
+
+ Version::Version
+ ( unsigned int _majorVersion,
+ unsigned int _minorVersion,
+ unsigned int _patchNumber,
+ char const * const _branchName,
+ unsigned int _buildNumber )
+ : majorVersion( _majorVersion ),
+ minorVersion( _minorVersion ),
+ patchNumber( _patchNumber ),
+ branchName( _branchName ),
+ buildNumber( _buildNumber )
+ {}
+
+ std::ostream& operator << ( std::ostream& os, Version const& version ) {
+ os << version.majorVersion << '.'
+ << version.minorVersion << '.'
+ << version.patchNumber;
+ // branchName is never null -> 0th char is \0 if it is empty
+ if (version.branchName[0]) {
+ os << '-' << version.branchName
+ << '.' << version.buildNumber;
+ }
+ return os;
+ }
+
+ Version const& libraryVersion() {
+ static Version version( 3, 7, 1, "", 0 );
+ return version;
+ }
+
+}
+
+
+
+
+namespace Catch {
+
+ const char* GeneratorException::what() const noexcept {
+ return m_msg;
+ }
+
+} // end namespace Catch
+
+
+
+
+namespace Catch {
+
+ IGeneratorTracker::~IGeneratorTracker() = default;
+
+namespace Generators {
+
+namespace Detail {
+
+ [[noreturn]]
+ void throw_generator_exception(char const* msg) {
+ Catch::throw_exception(GeneratorException{ msg });
+ }
+} // end namespace Detail
+
+ GeneratorUntypedBase::~GeneratorUntypedBase() = default;
+
+ IGeneratorTracker* acquireGeneratorTracker(StringRef generatorName, SourceLineInfo const& lineInfo ) {
+ return getResultCapture().acquireGeneratorTracker( generatorName, lineInfo );
+ }
+
+ IGeneratorTracker* createGeneratorTracker( StringRef generatorName,
+ SourceLineInfo lineInfo,
+ GeneratorBasePtr&& generator ) {
+ return getResultCapture().createGeneratorTracker(
+ generatorName, lineInfo, CATCH_MOVE( generator ) );
+ }
+
+} // namespace Generators
+} // namespace Catch
+
+
+
+
+#include
+
+namespace Catch {
+ namespace Generators {
+ namespace Detail {
+ std::uint32_t getSeed() { return sharedRng()(); }
+ } // namespace Detail
+
+ struct RandomFloatingGenerator::PImpl {
+ PImpl( long double a, long double b, uint32_t seed ):
+ rng( seed ), dist( a, b ) {}
+
+ Catch::SimplePcg32 rng;
+ std::uniform_real_distribution dist;
+ };
+
+ RandomFloatingGenerator::RandomFloatingGenerator(
+ long double a, long double b, std::uint32_t seed) :
+ m_pimpl(Catch::Detail::make_unique(a, b, seed)) {
+ static_cast( next() );
+ }
+
+ RandomFloatingGenerator::~RandomFloatingGenerator() =
+ default;
+ bool RandomFloatingGenerator::next() {
+ m_current_number = m_pimpl->dist( m_pimpl->rng );
+ return true;
+ }
+ } // namespace Generators
+} // namespace Catch
+
+
+
+
+namespace Catch {
+ IResultCapture::~IResultCapture() = default;
+}
+
+
+
+
+namespace Catch {
+ IConfig::~IConfig() = default;
+}
+
+
+
+
+namespace Catch {
+ IExceptionTranslator::~IExceptionTranslator() = default;
+ IExceptionTranslatorRegistry::~IExceptionTranslatorRegistry() = default;
+}
+
+
+
+#include
+
+namespace Catch {
+ namespace Generators {
+
+ bool GeneratorUntypedBase::countedNext() {
+ auto ret = next();
+ if ( ret ) {
+ m_stringReprCache.clear();
+ ++m_currentElementIndex;
+ }
+ return ret;
+ }
+
+ StringRef GeneratorUntypedBase::currentElementAsString() const {
+ if ( m_stringReprCache.empty() ) {
+ m_stringReprCache = stringifyImpl();
+ }
+ return m_stringReprCache;
+ }
+
+ } // namespace Generators
+} // namespace Catch
+
+
+
+
+namespace Catch {
+ IRegistryHub::~IRegistryHub() = default;
+ IMutableRegistryHub::~IMutableRegistryHub() = default;
+}
+
+
+
+#include
+
+namespace Catch {
+
+ ReporterConfig::ReporterConfig(
+ IConfig const* _fullConfig,
+ Detail::unique_ptr _stream,
+ ColourMode colourMode,
+ std::map customOptions ):
+ m_stream( CATCH_MOVE(_stream) ),
+ m_fullConfig( _fullConfig ),
+ m_colourMode( colourMode ),
+ m_customOptions( CATCH_MOVE( customOptions ) ) {}
+
+ Detail::unique_ptr ReporterConfig::takeStream() && {
+ assert( m_stream );
+ return CATCH_MOVE( m_stream );
+ }
+ IConfig const * ReporterConfig::fullConfig() const { return m_fullConfig; }
+ ColourMode ReporterConfig::colourMode() const { return m_colourMode; }
+
+ std::map const&
+ ReporterConfig::customOptions() const {
+ return m_customOptions;
+ }
+
+ ReporterConfig::~ReporterConfig() = default;
+
+ AssertionStats::AssertionStats( AssertionResult const& _assertionResult,
+ std::vector const& _infoMessages,
+ Totals const& _totals )
+ : assertionResult( _assertionResult ),
+ infoMessages( _infoMessages ),
+ totals( _totals )
+ {
+ if( assertionResult.hasMessage() ) {
+ // Copy message into messages list.
+ // !TBD This should have been done earlier, somewhere
+ MessageBuilder builder( assertionResult.getTestMacroName(), assertionResult.getSourceInfo(), assertionResult.getResultType() );
+ builder.m_info.message = static_cast(assertionResult.getMessage());
+
+ infoMessages.push_back( CATCH_MOVE(builder.m_info) );
+ }
+ }
+
+ SectionStats::SectionStats( SectionInfo&& _sectionInfo,
+ Counts const& _assertions,
+ double _durationInSeconds,
+ bool _missingAssertions )
+ : sectionInfo( CATCH_MOVE(_sectionInfo) ),
+ assertions( _assertions ),
+ durationInSeconds( _durationInSeconds ),
+ missingAssertions( _missingAssertions )
+ {}
+
+
+ TestCaseStats::TestCaseStats( TestCaseInfo const& _testInfo,
+ Totals const& _totals,
+ std::string&& _stdOut,
+ std::string&& _stdErr,
+ bool _aborting )
+ : testInfo( &_testInfo ),
+ totals( _totals ),
+ stdOut( CATCH_MOVE(_stdOut) ),
+ stdErr( CATCH_MOVE(_stdErr) ),
+ aborting( _aborting )
+ {}
+
+
+ TestRunStats::TestRunStats( TestRunInfo const& _runInfo,
+ Totals const& _totals,
+ bool _aborting )
+ : runInfo( _runInfo ),
+ totals( _totals ),
+ aborting( _aborting )
+ {}
+
+ IEventListener::~IEventListener() = default;
+
+} // end namespace Catch
+
+
+
+
+namespace Catch {
+ IReporterFactory::~IReporterFactory() = default;
+ EventListenerFactory::~EventListenerFactory() = default;
+}
+
+
+
+
+namespace Catch {
+ ITestCaseRegistry::~ITestCaseRegistry() = default;
+}
+
+
+
+namespace Catch {
+
+ AssertionHandler::AssertionHandler
+ ( StringRef macroName,
+ SourceLineInfo const& lineInfo,
+ StringRef capturedExpression,
+ ResultDisposition::Flags resultDisposition )
+ : m_assertionInfo{ macroName, lineInfo, capturedExpression, resultDisposition },
+ m_resultCapture( getResultCapture() )
+ {
+ m_resultCapture.notifyAssertionStarted( m_assertionInfo );
+ }
+
+ void AssertionHandler::handleExpr( ITransientExpression const& expr ) {
+ m_resultCapture.handleExpr( m_assertionInfo, expr, m_reaction );
+ }
+ void AssertionHandler::handleMessage(ResultWas::OfType resultType, std::string&& message) {
+ m_resultCapture.handleMessage( m_assertionInfo, resultType, CATCH_MOVE(message), m_reaction );
+ }
+
+ auto AssertionHandler::allowThrows() const -> bool {
+ return getCurrentContext().getConfig()->allowThrows();
+ }
+
+ void AssertionHandler::complete() {
+ m_completed = true;
+ if( m_reaction.shouldDebugBreak ) {
+
+ // If you find your debugger stopping you here then go one level up on the
+ // call-stack for the code that caused it (typically a failed assertion)
+
+ // (To go back to the test and change execution, jump over the throw, next)
+ CATCH_BREAK_INTO_DEBUGGER();
+ }
+ if (m_reaction.shouldThrow) {
+ throw_test_failure_exception();
+ }
+ if ( m_reaction.shouldSkip ) {
+ throw_test_skip_exception();
+ }
+ }
+
+ void AssertionHandler::handleUnexpectedInflightException() {
+ m_resultCapture.handleUnexpectedInflightException( m_assertionInfo, Catch::translateActiveException(), m_reaction );
+ }
+
+ void AssertionHandler::handleExceptionThrownAsExpected() {
+ m_resultCapture.handleNonExpr(m_assertionInfo, ResultWas::Ok, m_reaction);
+ }
+ void AssertionHandler::handleExceptionNotThrownAsExpected() {
+ m_resultCapture.handleNonExpr(m_assertionInfo, ResultWas::Ok, m_reaction);
+ }
+
+ void AssertionHandler::handleUnexpectedExceptionNotThrown() {
+ m_resultCapture.handleUnexpectedExceptionNotThrown( m_assertionInfo, m_reaction );
+ }
+
+ void AssertionHandler::handleThrowingCallSkipped() {
+ m_resultCapture.handleNonExpr(m_assertionInfo, ResultWas::Ok, m_reaction);
+ }
+
+ // This is the overload that takes a string and infers the Equals matcher from it
+ // The more general overload, that takes any string matcher, is in catch_capture_matchers.cpp
+ void handleExceptionMatchExpr( AssertionHandler& handler, std::string const& str ) {
+ handleExceptionMatchExpr( handler, Matchers::Equals( str ) );
+ }
+
+} // namespace Catch
+
+
+
+
+#include
+
+namespace Catch {
+ namespace Detail {
+
+ bool CaseInsensitiveLess::operator()( StringRef lhs,
+ StringRef rhs ) const {
+ return std::lexicographical_compare(
+ lhs.begin(), lhs.end(),
+ rhs.begin(), rhs.end(),
+ []( char l, char r ) { return toLower( l ) < toLower( r ); } );
+ }
+
+ bool
+ CaseInsensitiveEqualTo::operator()( StringRef lhs,
+ StringRef rhs ) const {
+ return std::equal(
+ lhs.begin(), lhs.end(),
+ rhs.begin(), rhs.end(),
+ []( char l, char r ) { return toLower( l ) == toLower( r ); } );
+ }
+
+ } // namespace Detail
+} // namespace Catch
+
+
+
+
+#include
+#include
+
+namespace {
+ bool isOptPrefix( char c ) {
+ return c == '-'
+#ifdef CATCH_PLATFORM_WINDOWS
+ || c == '/'
+#endif
+ ;
+ }
+
+ Catch::StringRef normaliseOpt( Catch::StringRef optName ) {
+ if ( optName[0] == '-'
+#if defined(CATCH_PLATFORM_WINDOWS)
+ || optName[0] == '/'
+#endif
+ ) {
+ return optName.substr( 1, optName.size() );
+ }
+
+ return optName;
+ }
+
+ static size_t find_first_separator(Catch::StringRef sr) {
+ auto is_separator = []( char c ) {
+ return c == ' ' || c == ':' || c == '=';
+ };
+ size_t pos = 0;
+ while (pos < sr.size()) {
+ if (is_separator(sr[pos])) { return pos; }
+ ++pos;
+ }
+
+ return Catch::StringRef::npos;
+ }
+
+} // namespace
+
+namespace Catch {
+ namespace Clara {
+ namespace Detail {
+
+ void TokenStream::loadBuffer() {
+ m_tokenBuffer.clear();
+
+ // Skip any empty strings
+ while ( it != itEnd && it->empty() ) {
+ ++it;
+ }
+
+ if ( it != itEnd ) {
+ StringRef next = *it;
+ if ( isOptPrefix( next[0] ) ) {
+ auto delimiterPos = find_first_separator(next);
+ if ( delimiterPos != StringRef::npos ) {
+ m_tokenBuffer.push_back(
+ { TokenType::Option,
+ next.substr( 0, delimiterPos ) } );
+ m_tokenBuffer.push_back(
+ { TokenType::Argument,
+ next.substr( delimiterPos + 1, next.size() ) } );
+ } else {
+ if ( next.size() > 1 && next[1] != '-' && next.size() > 2 ) {
+ // Combined short args, e.g. "-ab" for "-a -b"
+ for ( size_t i = 1; i < next.size(); ++i ) {
+ m_tokenBuffer.push_back(
+ { TokenType::Option,
+ next.substr( i, 1 ) } );
+ }
+ } else {
+ m_tokenBuffer.push_back(
+ { TokenType::Option, next } );
+ }
+ }
+ } else {
+ m_tokenBuffer.push_back(
+ { TokenType::Argument, next } );
+ }
+ }
+ }
+
+ TokenStream::TokenStream( Args const& args ):
+ TokenStream( args.m_args.begin(), args.m_args.end() ) {}
+
+ TokenStream::TokenStream( Iterator it_, Iterator itEnd_ ):
+ it( it_ ), itEnd( itEnd_ ) {
+ loadBuffer();
+ }
+
+ TokenStream& TokenStream::operator++() {
+ if ( m_tokenBuffer.size() >= 2 ) {
+ m_tokenBuffer.erase( m_tokenBuffer.begin() );
+ } else {
+ if ( it != itEnd )
+ ++it;
+ loadBuffer();
+ }
+ return *this;
+ }
+
+ ParserResult convertInto( std::string const& source,
+ std::string& target ) {
+ target = source;
+ return ParserResult::ok( ParseResultType::Matched );
+ }
+
+ ParserResult convertInto( std::string const& source,
+ bool& target ) {
+ std::string srcLC = toLower( source );
+
+ if ( srcLC == "y" || srcLC == "1" || srcLC == "true" ||
+ srcLC == "yes" || srcLC == "on" ) {
+ target = true;
+ } else if ( srcLC == "n" || srcLC == "0" || srcLC == "false" ||
+ srcLC == "no" || srcLC == "off" ) {
+ target = false;
+ } else {
+ return ParserResult::runtimeError(
+ "Expected a boolean value but did not recognise: '" +
+ source + '\'' );
+ }
+ return ParserResult::ok( ParseResultType::Matched );
+ }
+
+ size_t ParserBase::cardinality() const { return 1; }
+
+ InternalParseResult ParserBase::parse( Args const& args ) const {
+ return parse( static_cast(args.exeName()), TokenStream( args ) );
+ }
+
+ ParseState::ParseState( ParseResultType type,
+ TokenStream remainingTokens ):
+ m_type( type ), m_remainingTokens( CATCH_MOVE(remainingTokens) ) {}
+
+ ParserResult BoundFlagRef::setFlag( bool flag ) {
+ m_ref = flag;
+ return ParserResult::ok( ParseResultType::Matched );
+ }
+
+ ResultBase::~ResultBase() = default;
+
+ bool BoundRef::isContainer() const { return false; }
+
+ bool BoundRef::isFlag() const { return false; }
+
+ bool BoundFlagRefBase::isFlag() const { return true; }
+
+} // namespace Detail
+
+ Detail::InternalParseResult Arg::parse(std::string const&,
+ Detail::TokenStream tokens) const {
+ auto validationResult = validate();
+ if (!validationResult)
+ return Detail::InternalParseResult(validationResult);
+
+ auto token = *tokens;
+ if (token.type != Detail::TokenType::Argument)
+ return Detail::InternalParseResult::ok(Detail::ParseState(
+ ParseResultType::NoMatch, CATCH_MOVE(tokens)));
+
+ assert(!m_ref->isFlag());
+ auto valueRef =
+ static_cast(m_ref.get());
+
+ auto result = valueRef->setValue(static_cast(token.token));
+ if ( !result )
+ return Detail::InternalParseResult( result );
+ else
+ return Detail::InternalParseResult::ok(
+ Detail::ParseState( ParseResultType::Matched,
+ CATCH_MOVE( ++tokens ) ) );
+ }
+
+ Opt::Opt(bool& ref) :
+ ParserRefImpl(std::make_shared(ref)) {}
+
+ Detail::HelpColumns Opt::getHelpColumns() const {
+ ReusableStringStream oss;
+ bool first = true;
+ for (auto const& opt : m_optNames) {
+ if (first)
+ first = false;
+ else
+ oss << ", ";
+ oss << opt;
+ }
+ if (!m_hint.empty())
+ oss << " <" << m_hint << '>';
+ return { oss.str(), m_description };
+ }
+
+ bool Opt::isMatch(StringRef optToken) const {
+ auto normalisedToken = normaliseOpt(optToken);
+ for (auto const& name : m_optNames) {
+ if (normaliseOpt(name) == normalisedToken)
+ return true;
+ }
+ return false;
+ }
+
+ Detail::InternalParseResult Opt::parse(std::string const&,
+ Detail::TokenStream tokens) const {
+ auto validationResult = validate();
+ if (!validationResult)
+ return Detail::InternalParseResult(validationResult);
+
+ if (tokens &&
+ tokens->type == Detail::TokenType::Option) {
+ auto const& token = *tokens;
+ if (isMatch(token.token)) {
+ if (m_ref->isFlag()) {
+ auto flagRef =
+ static_cast(
+ m_ref.get());
+ auto result = flagRef->setFlag(true);
+ if (!result)
+ return Detail::InternalParseResult(result);
+ if (result.value() ==
+ ParseResultType::ShortCircuitAll)
+ return Detail::InternalParseResult::ok(Detail::ParseState(
+ result.value(), CATCH_MOVE(tokens)));
+ } else {
+ auto valueRef =
+ static_cast(
+ m_ref.get());
+ ++tokens;
+ if (!tokens)
+ return Detail::InternalParseResult::runtimeError(
+ "Expected argument following " +
+ token.token);
+ auto const& argToken = *tokens;
+ if (argToken.type != Detail::TokenType::Argument)
+ return Detail::InternalParseResult::runtimeError(
+ "Expected argument following " +
+ token.token);
+ const auto result = valueRef->setValue(static_cast(argToken.token));
+ if (!result)
+ return Detail::InternalParseResult(result);
+ if (result.value() ==
+ ParseResultType::ShortCircuitAll)
+ return Detail::InternalParseResult::ok(Detail::ParseState(
+ result.value(), CATCH_MOVE(tokens)));
+ }
+ return Detail::InternalParseResult::ok(Detail::ParseState(
+ ParseResultType::Matched, CATCH_MOVE(++tokens)));
+ }
+ }
+ return Detail::InternalParseResult::ok(
+ Detail::ParseState(ParseResultType::NoMatch, CATCH_MOVE(tokens)));
+ }
+
+ Detail::Result Opt::validate() const {
+ if (m_optNames.empty())
+ return Detail::Result::logicError("No options supplied to Opt");
+ for (auto const& name : m_optNames) {
+ if (name.empty())
+ return Detail::Result::logicError(
+ "Option name cannot be empty");
+#ifdef CATCH_PLATFORM_WINDOWS
+ if (name[0] != '-' && name[0] != '/')
+ return Detail::Result::logicError(
+ "Option name must begin with '-' or '/'");
+#else
+ if (name[0] != '-')
+ return Detail::Result::logicError(
+ "Option name must begin with '-'");
+#endif
+ }
+ return ParserRefImpl::validate();
+ }
+
+ ExeName::ExeName() :
+ m_name(std::make_shared("")) {}
+
+ ExeName::ExeName(std::string& ref) : ExeName() {
+ m_ref = std::make_shared>(ref);
+ }
+
+ Detail::InternalParseResult
+ ExeName::parse(std::string const&,
+ Detail::TokenStream tokens) const {
+ return Detail::InternalParseResult::ok(
+ Detail::ParseState(ParseResultType::NoMatch, CATCH_MOVE(tokens)));
+ }
+
+ ParserResult ExeName::set(std::string const& newName) {
+ auto lastSlash = newName.find_last_of("\\/");
+ auto filename = (lastSlash == std::string::npos)
+ ? newName
+ : newName.substr(lastSlash + 1);
+
+ *m_name = filename;
+ if (m_ref)
+ return m_ref->setValue(filename);
+ else
+ return ParserResult::ok(ParseResultType::Matched);
+ }
+
+
+
+
+ Parser& Parser::operator|=( Parser const& other ) {
+ m_options.insert( m_options.end(),
+ other.m_options.begin(),
+ other.m_options.end() );
+ m_args.insert(
+ m_args.end(), other.m_args.begin(), other.m_args.end() );
+ return *this;
+ }
+
+ std::vector Parser::getHelpColumns() const {
+ std::vector cols;
+ cols.reserve( m_options.size() );
+ for ( auto const& o : m_options ) {
+ cols.push_back(o.getHelpColumns());
+ }
+ return cols;
+ }
+
+ void Parser::writeToStream( std::ostream& os ) const {
+ if ( !m_exeName.name().empty() ) {
+ os << "usage:\n"
+ << " " << m_exeName.name() << ' ';
+ bool required = true, first = true;
+ for ( auto const& arg : m_args ) {
+ if ( first )
+ first = false;
+ else
+ os << ' ';
+ if ( arg.isOptional() && required ) {
+ os << '[';
+ required = false;
+ }
+ os << '<' << arg.hint() << '>';
+ if ( arg.cardinality() == 0 )
+ os << " ... ";
+ }
+ if ( !required )
+ os << ']';
+ if ( !m_options.empty() )
+ os << " options";
+ os << "\n\nwhere options are:\n";
+ }
+
+ auto rows = getHelpColumns();
+ size_t consoleWidth = CATCH_CONFIG_CONSOLE_WIDTH;
+ size_t optWidth = 0;
+ for ( auto const& cols : rows )
+ optWidth = ( std::max )( optWidth, cols.left.size() + 2 );
+
+ optWidth = ( std::min )( optWidth, consoleWidth / 2 );
+
+ for ( auto& cols : rows ) {
+ auto row = TextFlow::Column( CATCH_MOVE(cols.left) )
+ .width( optWidth )
+ .indent( 2 ) +
+ TextFlow::Spacer( 4 ) +
+ TextFlow::Column( static_cast(cols.descriptions) )
+ .width( consoleWidth - 7 - optWidth );
+ os << row << '\n';
+ }
+ }
+
+ Detail::Result Parser::validate() const {
+ for ( auto const& opt : m_options ) {
+ auto result = opt.validate();
+ if ( !result )
+ return result;
+ }
+ for ( auto const& arg : m_args ) {
+ auto result = arg.validate();
+ if ( !result )
+ return result;
+ }
+ return Detail::Result::ok();
+ }
+
+ Detail::InternalParseResult
+ Parser::parse( std::string const& exeName,
+ Detail::TokenStream tokens ) const {
+
+ struct ParserInfo {
+ ParserBase const* parser = nullptr;
+ size_t count = 0;
+ };
+ std::vector parseInfos;
+ parseInfos.reserve( m_options.size() + m_args.size() );
+ for ( auto const& opt : m_options ) {
+ parseInfos.push_back( { &opt, 0 } );
+ }
+ for ( auto const& arg : m_args ) {
+ parseInfos.push_back( { &arg, 0 } );
+ }
+
+ m_exeName.set( exeName );
+
+ auto result = Detail::InternalParseResult::ok(
+ Detail::ParseState( ParseResultType::NoMatch, CATCH_MOVE(tokens) ) );
+ while ( result.value().remainingTokens() ) {
+ bool tokenParsed = false;
+
+ for ( auto& parseInfo : parseInfos ) {
+ if ( parseInfo.parser->cardinality() == 0 ||
+ parseInfo.count < parseInfo.parser->cardinality() ) {
+ result = parseInfo.parser->parse(
+ exeName, CATCH_MOVE(result).value().remainingTokens() );
+ if ( !result )
+ return result;
+ if ( result.value().type() !=
+ ParseResultType::NoMatch ) {
+ tokenParsed = true;
+ ++parseInfo.count;
+ break;
+ }
+ }
+ }
+
+ if ( result.value().type() == ParseResultType::ShortCircuitAll )
+ return result;
+ if ( !tokenParsed )
+ return Detail::InternalParseResult::runtimeError(
+ "Unrecognised token: " +
+ result.value().remainingTokens()->token );
+ }
+ // !TBD Check missing required options
+ return result;
+ }
+
+ Args::Args(int argc, char const* const* argv) :
+ m_exeName(argv[0]), m_args(argv + 1, argv + argc) {}
+
+ Args::Args(std::initializer_list args) :
+ m_exeName(*args.begin()),
+ m_args(args.begin() + 1, args.end()) {}
+
+
+ Help::Help( bool& showHelpFlag ):
+ Opt( [&]( bool flag ) {
+ showHelpFlag = flag;
+ return ParserResult::ok( ParseResultType::ShortCircuitAll );
+ } ) {
+ static_cast ( *this )(
+ "display usage information" )["-?"]["-h"]["--help"]
+ .optional();
+ }
+
+ } // namespace Clara
+} // namespace Catch
+
+
+
+
+#include
+#include
+
+namespace Catch {
+
+ Clara::Parser makeCommandLineParser( ConfigData& config ) {
+
+ using namespace Clara;
+
+ auto const setWarning = [&]( std::string const& warning ) {
+ if ( warning == "NoAssertions" ) {
+ config.warnings = static_cast(config.warnings | WarnAbout::NoAssertions);
+ return ParserResult::ok( ParseResultType::Matched );
+ } else if ( warning == "UnmatchedTestSpec" ) {
+ config.warnings = static_cast(config.warnings | WarnAbout::UnmatchedTestSpec);
+ return ParserResult::ok( ParseResultType::Matched );
+ }
+
+ return ParserResult ::runtimeError(
+ "Unrecognised warning option: '" + warning + '\'' );
+ };
+ auto const loadTestNamesFromFile = [&]( std::string const& filename ) {
+ std::ifstream f( filename.c_str() );
+ if( !f.is_open() )
+ return ParserResult::runtimeError( "Unable to load input file: '" + filename + '\'' );
+
+ std::string line;
+ while( std::getline( f, line ) ) {
+ line = trim(line);
+ if( !line.empty() && !startsWith( line, '#' ) ) {
+ if( !startsWith( line, '"' ) )
+ line = '"' + CATCH_MOVE(line) + '"';
+ config.testsOrTags.push_back( line );
+ config.testsOrTags.emplace_back( "," );
+ }
+ }
+ //Remove comma in the end
+ if(!config.testsOrTags.empty())
+ config.testsOrTags.erase( config.testsOrTags.end()-1 );
+
+ return ParserResult::ok( ParseResultType::Matched );
+ };
+ auto const setTestOrder = [&]( std::string const& order ) {
+ if( startsWith( "declared", order ) )
+ config.runOrder = TestRunOrder::Declared;
+ else if( startsWith( "lexical", order ) )
+ config.runOrder = TestRunOrder::LexicographicallySorted;
+ else if( startsWith( "random", order ) )
+ config.runOrder = TestRunOrder::Randomized;
+ else
+ return ParserResult::runtimeError( "Unrecognised ordering: '" + order + '\'' );
+ return ParserResult::ok( ParseResultType::Matched );
+ };
+ auto const setRngSeed = [&]( std::string const& seed ) {
+ if( seed == "time" ) {
+ config.rngSeed = generateRandomSeed(GenerateFrom::Time);
+ return ParserResult::ok(ParseResultType::Matched);
+ } else if (seed == "random-device") {
+ config.rngSeed = generateRandomSeed(GenerateFrom::RandomDevice);
+ return ParserResult::ok(ParseResultType::Matched);
+ }
+
+ // TODO: ideally we should be parsing uint32_t directly
+ // fix this later when we add new parse overload
+ auto parsedSeed = parseUInt( seed, 0 );
+ if ( !parsedSeed ) {
+ return ParserResult::runtimeError( "Could not parse '" + seed + "' as seed" );
+ }
+ config.rngSeed = *parsedSeed;
+ return ParserResult::ok( ParseResultType::Matched );
+ };
+ auto const setDefaultColourMode = [&]( std::string const& colourMode ) {
+ Optional maybeMode = Catch::Detail::stringToColourMode(toLower( colourMode ));
+ if ( !maybeMode ) {
+ return ParserResult::runtimeError(
+ "colour mode must be one of: default, ansi, win32, "
+ "or none. '" +
+ colourMode + "' is not recognised" );
+ }
+ auto mode = *maybeMode;
+ if ( !isColourImplAvailable( mode ) ) {
+ return ParserResult::runtimeError(
+ "colour mode '" + colourMode +
+ "' is not supported in this binary" );
+ }
+ config.defaultColourMode = mode;
+ return ParserResult::ok( ParseResultType::Matched );
+ };
+ auto const setWaitForKeypress = [&]( std::string const& keypress ) {
+ auto keypressLc = toLower( keypress );
+ if (keypressLc == "never")
+ config.waitForKeypress = WaitForKeypress::Never;
+ else if( keypressLc == "start" )
+ config.waitForKeypress = WaitForKeypress::BeforeStart;
+ else if( keypressLc == "exit" )
+ config.waitForKeypress = WaitForKeypress::BeforeExit;
+ else if( keypressLc == "both" )
+ config.waitForKeypress = WaitForKeypress::BeforeStartAndExit;
+ else
+ return ParserResult::runtimeError( "keypress argument must be one of: never, start, exit or both. '" + keypress + "' not recognised" );
+ return ParserResult::ok( ParseResultType::Matched );
+ };
+ auto const setVerbosity = [&]( std::string const& verbosity ) {
+ auto lcVerbosity = toLower( verbosity );
+ if( lcVerbosity == "quiet" )
+ config.verbosity = Verbosity::Quiet;
+ else if( lcVerbosity == "normal" )
+ config.verbosity = Verbosity::Normal;
+ else if( lcVerbosity == "high" )
+ config.verbosity = Verbosity::High;
+ else
+ return ParserResult::runtimeError( "Unrecognised verbosity, '" + verbosity + '\'' );
+ return ParserResult::ok( ParseResultType::Matched );
+ };
+ auto const setReporter = [&]( std::string const& userReporterSpec ) {
+ if ( userReporterSpec.empty() ) {
+ return ParserResult::runtimeError( "Received empty reporter spec." );
+ }
+
+ Optional parsed =
+ parseReporterSpec( userReporterSpec );
+ if ( !parsed ) {
+ return ParserResult::runtimeError(
+ "Could not parse reporter spec '" + userReporterSpec +
+ "'" );
+ }
+
+ auto const& reporterSpec = *parsed;
+
+ auto const& factories =
+ getRegistryHub().getReporterRegistry().getFactories();
+ auto result = factories.find( reporterSpec.name() );
+
+ if ( result == factories.end() ) {
+ return ParserResult::runtimeError(
+ "Unrecognized reporter, '" + reporterSpec.name() +
+ "'. Check available with --list-reporters" );
+ }
+
+
+ const bool hadOutputFile = reporterSpec.outputFile().some();
+ config.reporterSpecifications.push_back( CATCH_MOVE( *parsed ) );
+ // It would be enough to check this only once at the very end, but
+ // there is not a place where we could call this check, so do it
+ // every time it could fail. For valid inputs, this is still called
+ // at most once.
+ if (!hadOutputFile) {
+ int n_reporters_without_file = 0;
+ for (auto const& spec : config.reporterSpecifications) {
+ if (spec.outputFile().none()) {
+ n_reporters_without_file++;
+ }
+ }
+ if (n_reporters_without_file > 1) {
+ return ParserResult::runtimeError( "Only one reporter may have unspecified output file." );
+ }
+ }
+
+ return ParserResult::ok( ParseResultType::Matched );
+ };
+ auto const setShardCount = [&]( std::string const& shardCount ) {
+ auto parsedCount = parseUInt( shardCount );
+ if ( !parsedCount ) {
+ return ParserResult::runtimeError(
+ "Could not parse '" + shardCount + "' as shard count" );
+ }
+ if ( *parsedCount == 0 ) {
+ return ParserResult::runtimeError(
+ "Shard count must be positive" );
+ }
+ config.shardCount = *parsedCount;
+ return ParserResult::ok( ParseResultType::Matched );
+ };
+
+ auto const setShardIndex = [&](std::string const& shardIndex) {
+ auto parsedIndex = parseUInt( shardIndex );
+ if ( !parsedIndex ) {
+ return ParserResult::runtimeError(
+ "Could not parse '" + shardIndex + "' as shard index" );
+ }
+ config.shardIndex = *parsedIndex;
+ return ParserResult::ok( ParseResultType::Matched );
+ };
+
+ auto cli
+ = ExeName( config.processName )
+ | Help( config.showHelp )
+ | Opt( config.showSuccessfulTests )
+ ["-s"]["--success"]
+ ( "include successful tests in output" )
+ | Opt( config.shouldDebugBreak )
+ ["-b"]["--break"]
+ ( "break into debugger on failure" )
+ | Opt( config.noThrow )
+ ["-e"]["--nothrow"]
+ ( "skip exception tests" )
+ | Opt( config.showInvisibles )
+ ["-i"]["--invisibles"]
+ ( "show invisibles (tabs, newlines)" )
+ | Opt( config.defaultOutputFilename, "filename" )
+ ["-o"]["--out"]
+ ( "default output filename" )
+ | Opt( accept_many, setReporter, "name[::key=value]*" )
+ ["-r"]["--reporter"]
+ ( "reporter to use (defaults to console)" )
+ | Opt( config.name, "name" )
+ ["-n"]["--name"]
+ ( "suite name" )
+ | Opt( [&]( bool ){ config.abortAfter = 1; } )
+ ["-a"]["--abort"]
+ ( "abort at first failure" )
+ | Opt( [&]( int x ){ config.abortAfter = x; }, "no. failures" )
+ ["-x"]["--abortx"]
+ ( "abort after x failures" )
+ | Opt( accept_many, setWarning, "warning name" )
+ ["-w"]["--warn"]
+ ( "enable warnings" )
+ | Opt( [&]( bool flag ) { config.showDurations = flag ? ShowDurations::Always : ShowDurations::Never; }, "yes|no" )
+ ["-d"]["--durations"]
+ ( "show test durations" )
+ | Opt( config.minDuration, "seconds" )
+ ["-D"]["--min-duration"]
+ ( "show test durations for tests taking at least the given number of seconds" )
+ | Opt( loadTestNamesFromFile, "filename" )
+ ["-f"]["--input-file"]
+ ( "load test names to run from a file" )
+ | Opt( config.filenamesAsTags )
+ ["-#"]["--filenames-as-tags"]
+ ( "adds a tag for the filename" )
+ | Opt( config.sectionsToRun, "section name" )
+ ["-c"]["--section"]
+ ( "specify section to run" )
+ | Opt( setVerbosity, "quiet|normal|high" )
+ ["-v"]["--verbosity"]
+ ( "set output verbosity" )
+ | Opt( config.listTests )
+ ["--list-tests"]
+ ( "list all/matching test cases" )
+ | Opt( config.listTags )
+ ["--list-tags"]
+ ( "list all/matching tags" )
+ | Opt( config.listReporters )
+ ["--list-reporters"]
+ ( "list all available reporters" )
+ | Opt( config.listListeners )
+ ["--list-listeners"]
+ ( "list all listeners" )
+ | Opt( setTestOrder, "decl|lex|rand" )
+ ["--order"]
+ ( "test case order (defaults to decl)" )
+ | Opt( setRngSeed, "'time'|'random-device'|number" )
+ ["--rng-seed"]
+ ( "set a specific seed for random numbers" )
+ | Opt( setDefaultColourMode, "ansi|win32|none|default" )
+ ["--colour-mode"]
+ ( "what color mode should be used as default" )
+ | Opt( config.libIdentify )
+ ["--libidentify"]
+ ( "report name and version according to libidentify standard" )
+ | Opt( setWaitForKeypress, "never|start|exit|both" )
+ ["--wait-for-keypress"]
+ ( "waits for a keypress before exiting" )
+ | Opt( config.skipBenchmarks)
+ ["--skip-benchmarks"]
+ ( "disable running benchmarks")
+ | Opt( config.benchmarkSamples, "samples" )
+ ["--benchmark-samples"]
+ ( "number of samples to collect (default: 100)" )
+ | Opt( config.benchmarkResamples, "resamples" )
+ ["--benchmark-resamples"]
+ ( "number of resamples for the bootstrap (default: 100000)" )
+ | Opt( config.benchmarkConfidenceInterval, "confidence interval" )
+ ["--benchmark-confidence-interval"]
+ ( "confidence interval for the bootstrap (between 0 and 1, default: 0.95)" )
+ | Opt( config.benchmarkNoAnalysis )
+ ["--benchmark-no-analysis"]
+ ( "perform only measurements; do not perform any analysis" )
+ | Opt( config.benchmarkWarmupTime, "benchmarkWarmupTime" )
+ ["--benchmark-warmup-time"]
+ ( "amount of time in milliseconds spent on warming up each test (default: 100)" )
+ | Opt( setShardCount, "shard count" )
+ ["--shard-count"]
+ ( "split the tests to execute into this many groups" )
+ | Opt( setShardIndex, "shard index" )
+ ["--shard-index"]
+ ( "index of the group of tests to execute (see --shard-count)" )
+ | Opt( config.allowZeroTests )
+ ["--allow-running-no-tests"]
+ ( "Treat 'No tests run' as a success" )
+ | Arg( config.testsOrTags, "test name|pattern|tags" )
+ ( "which test or tests to use" );
+
+ return cli;
+ }
+
+} // end namespace Catch
+
+
+#if defined(__clang__)
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wexit-time-destructors"
+#endif
+
+
+
+#include
+#include
+#include
+
+namespace Catch {
+
+ ColourImpl::~ColourImpl() = default;
+
+ ColourImpl::ColourGuard ColourImpl::guardColour( Colour::Code colourCode ) {
+ return ColourGuard(colourCode, this );
+ }
+
+ void ColourImpl::ColourGuard::engageImpl( std::ostream& stream ) {
+ assert( &stream == &m_colourImpl->m_stream->stream() &&
+ "Engaging colour guard for different stream than used by the "
+ "parent colour implementation" );
+ static_cast( stream );
+
+ m_engaged = true;
+ m_colourImpl->use( m_code );
+ }
+
+ ColourImpl::ColourGuard::ColourGuard( Colour::Code code,
+ ColourImpl const* colour ):
+ m_colourImpl( colour ), m_code( code ) {
+ }
+ ColourImpl::ColourGuard::ColourGuard( ColourGuard&& rhs ) noexcept:
+ m_colourImpl( rhs.m_colourImpl ),
+ m_code( rhs.m_code ),
+ m_engaged( rhs.m_engaged ) {
+ rhs.m_engaged = false;
+ }
+ ColourImpl::ColourGuard&
+ ColourImpl::ColourGuard::operator=( ColourGuard&& rhs ) noexcept {
+ using std::swap;
+ swap( m_colourImpl, rhs.m_colourImpl );
+ swap( m_code, rhs.m_code );
+ swap( m_engaged, rhs.m_engaged );
+
+ return *this;
+ }
+ ColourImpl::ColourGuard::~ColourGuard() {
+ if ( m_engaged ) {
+ m_colourImpl->use( Colour::None );
+ }
+ }
+
+ ColourImpl::ColourGuard&
+ ColourImpl::ColourGuard::engage( std::ostream& stream ) & {
+ engageImpl( stream );
+ return *this;
+ }
+
+ ColourImpl::ColourGuard&&
+ ColourImpl::ColourGuard::engage( std::ostream& stream ) && {
+ engageImpl( stream );
+ return CATCH_MOVE(*this);
+ }
+
+ namespace {
+ //! A do-nothing implementation of colour, used as fallback for unknown
+ //! platforms, and when the user asks to deactivate all colours.
+ class NoColourImpl final : public ColourImpl {
+ public:
+ NoColourImpl( IStream* stream ): ColourImpl( stream ) {}
+
+ private:
+ void use( Colour::Code ) const override {}
+ };
+ } // namespace
+
+
+} // namespace Catch
+
+
+#if defined ( CATCH_CONFIG_COLOUR_WIN32 ) /////////////////////////////////////////
+
+namespace Catch {
+namespace {
+
+ class Win32ColourImpl final : public ColourImpl {
+ public:
+ Win32ColourImpl(IStream* stream):
+ ColourImpl(stream) {
+ CONSOLE_SCREEN_BUFFER_INFO csbiInfo;
+ GetConsoleScreenBufferInfo( GetStdHandle( STD_OUTPUT_HANDLE ),
+ &csbiInfo );
+ originalForegroundAttributes = csbiInfo.wAttributes & ~( BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_BLUE | BACKGROUND_INTENSITY );
+ originalBackgroundAttributes = csbiInfo.wAttributes & ~( FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY );
+ }
+
+ static bool useImplementationForStream(IStream const& stream) {
+ // Win32 text colour APIs can only be used on console streams
+ // We cannot check that the output hasn't been redirected,
+ // so we just check that the original stream is console stream.
+ return stream.isConsole();
+ }
+
+ private:
+ void use( Colour::Code _colourCode ) const override {
+ switch( _colourCode ) {
+ case Colour::None: return setTextAttribute( originalForegroundAttributes );
+ case Colour::White: return setTextAttribute( FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE );
+ case Colour::Red: return setTextAttribute( FOREGROUND_RED );
+ case Colour::Green: return setTextAttribute( FOREGROUND_GREEN );
+ case Colour::Blue: return setTextAttribute( FOREGROUND_BLUE );
+ case Colour::Cyan: return setTextAttribute( FOREGROUND_BLUE | FOREGROUND_GREEN );
+ case Colour::Yellow: return setTextAttribute( FOREGROUND_RED | FOREGROUND_GREEN );
+ case Colour::Grey: return setTextAttribute( 0 );
+
+ case Colour::LightGrey: return setTextAttribute( FOREGROUND_INTENSITY );
+ case Colour::BrightRed: return setTextAttribute( FOREGROUND_INTENSITY | FOREGROUND_RED );
+ case Colour::BrightGreen: return setTextAttribute( FOREGROUND_INTENSITY | FOREGROUND_GREEN );
+ case Colour::BrightWhite: return setTextAttribute( FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE );
+ case Colour::BrightYellow: return setTextAttribute( FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_GREEN );
+
+ case Colour::Bright: CATCH_INTERNAL_ERROR( "not a colour" );
+
+ default:
+ CATCH_ERROR( "Unknown colour requested" );
+ }
+ }
+
+ void setTextAttribute( WORD _textAttribute ) const {
+ SetConsoleTextAttribute( GetStdHandle( STD_OUTPUT_HANDLE ),
+ _textAttribute |
+ originalBackgroundAttributes );
+ }
+ WORD originalForegroundAttributes;
+ WORD originalBackgroundAttributes;
+ };
+
+} // end anon namespace
+} // end namespace Catch
+
+#endif // Windows/ ANSI/ None
+
+
+#if defined( CATCH_PLATFORM_LINUX ) || defined( CATCH_PLATFORM_MAC )
+# define CATCH_INTERNAL_HAS_ISATTY
+# include
+#endif
+
+namespace Catch {
+namespace {
+
+ class ANSIColourImpl final : public ColourImpl {
+ public:
+ ANSIColourImpl( IStream* stream ): ColourImpl( stream ) {}
+
+ static bool useImplementationForStream(IStream const& stream) {
+ // This is kinda messy due to trying to support a bunch of
+ // different platforms at once.
+ // The basic idea is that if we are asked to do autodetection (as
+ // opposed to being told to use posixy colours outright), then we
+ // only want to use the colours if we are writing to console.
+ // However, console might be redirected, so we make an attempt at
+ // checking for that on platforms where we know how to do that.
+ bool useColour = stream.isConsole();
+#if defined( CATCH_INTERNAL_HAS_ISATTY ) && \
+ !( defined( __DJGPP__ ) && defined( __STRICT_ANSI__ ) )
+ ErrnoGuard _; // for isatty
+ useColour = useColour && isatty( STDOUT_FILENO );
+# endif
+# if defined( CATCH_PLATFORM_MAC ) || defined( CATCH_PLATFORM_IPHONE )
+ useColour = useColour && !isDebuggerActive();
+# endif
+
+ return useColour;
+ }
+
+ private:
+ void use( Colour::Code _colourCode ) const override {
+ auto setColour = [&out =
+ m_stream->stream()]( char const* escapeCode ) {
+ // The escape sequence must be flushed to console, otherwise
+ // if stdin and stderr are intermixed, we'd get accidentally
+ // coloured output.
+ out << '\033' << escapeCode << std::flush;
+ };
+ switch( _colourCode ) {
+ case Colour::None:
+ case Colour::White: return setColour( "[0m" );
+ case Colour::Red: return setColour( "[0;31m" );
+ case Colour::Green: return setColour( "[0;32m" );
+ case Colour::Blue: return setColour( "[0;34m" );
+ case Colour::Cyan: return setColour( "[0;36m" );
+ case Colour::Yellow: return setColour( "[0;33m" );
+ case Colour::Grey: return setColour( "[1;30m" );
+
+ case Colour::LightGrey: return setColour( "[0;37m" );
+ case Colour::BrightRed: return setColour( "[1;31m" );
+ case Colour::BrightGreen: return setColour( "[1;32m" );
+ case Colour::BrightWhite: return setColour( "[1;37m" );
+ case Colour::BrightYellow: return setColour( "[1;33m" );
+
+ case Colour::Bright: CATCH_INTERNAL_ERROR( "not a colour" );
+ default: CATCH_INTERNAL_ERROR( "Unknown colour requested" );
+ }
+ }
+ };
+
+} // end anon namespace
+} // end namespace Catch
+
+namespace Catch {
+
+ Detail::unique_ptr makeColourImpl( ColourMode colourSelection,
+ IStream* stream ) {
+#if defined( CATCH_CONFIG_COLOUR_WIN32 )
+ if ( colourSelection == ColourMode::Win32 ) {
+ return Detail::make_unique( stream );
+ }
+#endif
+ if ( colourSelection == ColourMode::ANSI ) {
+ return Detail::make_unique( stream );
+ }
+ if ( colourSelection == ColourMode::None ) {
+ return Detail::make_unique( stream );
+ }
+
+ if ( colourSelection == ColourMode::PlatformDefault) {
+#if defined( CATCH_CONFIG_COLOUR_WIN32 )
+ if ( Win32ColourImpl::useImplementationForStream( *stream ) ) {
+ return Detail::make_unique( stream );
+ }
+#endif
+ if ( ANSIColourImpl::useImplementationForStream( *stream ) ) {
+ return Detail::make_unique( stream );
+ }
+ return Detail::make_unique( stream );
+ }
+
+ CATCH_ERROR( "Could not create colour impl for selection " << static_cast(colourSelection) );
+ }
+
+ bool isColourImplAvailable( ColourMode colourSelection ) {
+ switch ( colourSelection ) {
+#if defined( CATCH_CONFIG_COLOUR_WIN32 )
+ case ColourMode::Win32:
+#endif
+ case ColourMode::ANSI:
+ case ColourMode::None:
+ case ColourMode::PlatformDefault:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+
+} // end namespace Catch
+
+#if defined(__clang__)
+# pragma clang diagnostic pop
+#endif
+
+
+
+
+namespace Catch {
+
+ Context* Context::currentContext = nullptr;
+
+ void cleanUpContext() {
+ delete Context::currentContext;
+ Context::currentContext = nullptr;
+ }
+ void Context::createContext() {
+ currentContext = new Context();
+ }
+
+ Context& getCurrentMutableContext() {
+ if ( !Context::currentContext ) { Context::createContext(); }
+ // NOLINTNEXTLINE(clang-analyzer-core.uninitialized.UndefReturn)
+ return *Context::currentContext;
+ }
+
+ SimplePcg32& sharedRng() {
+ static SimplePcg32 s_rng;
+ return s_rng;
+ }
+
+}
+
+
+
+
+
+#include
+
+#if defined(CATCH_CONFIG_ANDROID_LOGWRITE)
+#include
+
+ namespace Catch {
+ void writeToDebugConsole( std::string const& text ) {
+ __android_log_write( ANDROID_LOG_DEBUG, "Catch", text.c_str() );
+ }
+ }
+
+#elif defined(CATCH_PLATFORM_WINDOWS)
+
+ namespace Catch {
+ void writeToDebugConsole( std::string const& text ) {
+ ::OutputDebugStringA( text.c_str() );
+ }
+ }
+
+#else
+
+ namespace Catch {
+ void writeToDebugConsole( std::string const& text ) {
+ // !TBD: Need a version for Mac/ XCode and other IDEs
+ Catch::cout() << text;
+ }
+ }
+
+#endif // Platform
+
+
+
+#if defined(CATCH_PLATFORM_MAC) || defined(CATCH_PLATFORM_IPHONE)
+
+# include
+# include
+# include
+# include
+# include
+
+#ifdef __apple_build_version__
+ // These headers will only compile with AppleClang (XCode)
+ // For other compilers (Clang, GCC, ... ) we need to exclude them
+# include
+#endif
+
+ namespace Catch {
+ #ifdef __apple_build_version__
+ // The following function is taken directly from the following technical note:
+ // https://developer.apple.com/library/archive/qa/qa1361/_index.html
+
+ // Returns true if the current process is being debugged (either
+ // running under the debugger or has a debugger attached post facto).
+ bool isDebuggerActive(){
+ int mib[4];
+ struct kinfo_proc info;
+ std::size_t size;
+
+ // Initialize the flags so that, if sysctl fails for some bizarre
+ // reason, we get a predictable result.
+
+ info.kp_proc.p_flag = 0;
+
+ // Initialize mib, which tells sysctl the info we want, in this case
+ // we're looking for information about a specific process ID.
+
+ mib[0] = CTL_KERN;
+ mib[1] = KERN_PROC;
+ mib[2] = KERN_PROC_PID;
+ mib[3] = getpid();
+
+ // Call sysctl.
+
+ size = sizeof(info);
+ if( sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, nullptr, 0) != 0 ) {
+ Catch::cerr() << "\n** Call to sysctl failed - unable to determine if debugger is active **\n\n" << std::flush;
+ return false;
+ }
+
+ // We're being debugged if the P_TRACED flag is set.
+
+ return ( (info.kp_proc.p_flag & P_TRACED) != 0 );
+ }
+ #else
+ bool isDebuggerActive() {
+ // We need to find another way to determine this for non-appleclang compilers on macOS
+ return false;
+ }
+ #endif
+ } // namespace Catch
+
+#elif defined(CATCH_PLATFORM_LINUX)
+ #include
+ #include
+
+ namespace Catch{
+ // The standard POSIX way of detecting a debugger is to attempt to
+ // ptrace() the process, but this needs to be done from a child and not
+ // this process itself to still allow attaching to this process later
+ // if wanted, so is rather heavy. Under Linux we have the PID of the
+ // "debugger" (which doesn't need to be gdb, of course, it could also
+ // be strace, for example) in /proc/$PID/status, so just get it from
+ // there instead.
+ bool isDebuggerActive(){
+ // Libstdc++ has a bug, where std::ifstream sets errno to 0
+ // This way our users can properly assert over errno values
+ ErrnoGuard guard;
+ std::ifstream in("/proc/self/status");
+ for( std::string line; std::getline(in, line); ) {
+ static const int PREFIX_LEN = 11;
+ if( line.compare(0, PREFIX_LEN, "TracerPid:\t") == 0 ) {
+ // We're traced if the PID is not 0 and no other PID starts
+ // with 0 digit, so it's enough to check for just a single
+ // character.
+ return line.length() > PREFIX_LEN && line[PREFIX_LEN] != '0';
+ }
+ }
+
+ return false;
+ }
+ } // namespace Catch
+#elif defined(_MSC_VER)
+ extern "C" __declspec(dllimport) int __stdcall IsDebuggerPresent();
+ namespace Catch {
+ bool isDebuggerActive() {
+ return IsDebuggerPresent() != 0;
+ }
+ }
+#elif defined(__MINGW32__)
+ extern "C" __declspec(dllimport) int __stdcall IsDebuggerPresent();
+ namespace Catch {
+ bool isDebuggerActive() {
+ return IsDebuggerPresent() != 0;
+ }
+ }
+#else
+ namespace Catch {
+ bool isDebuggerActive() { return false; }
+ }
+#endif // Platform
+
+
+
+
+namespace Catch {
+
+ void ITransientExpression::streamReconstructedExpression(
+ std::ostream& os ) const {
+ // We can't make this function pure virtual to keep ITransientExpression
+ // constexpr, so we write error message instead
+ os << "Some class derived from ITransientExpression without overriding streamReconstructedExpression";
+ }
+
+ void formatReconstructedExpression( std::ostream &os, std::string const& lhs, StringRef op, std::string const& rhs ) {
+ if( lhs.size() + rhs.size() < 40 &&
+ lhs.find('\n') == std::string::npos &&
+ rhs.find('\n') == std::string::npos )
+ os << lhs << ' ' << op << ' ' << rhs;
+ else
+ os << lhs << '\n' << op << '\n' << rhs;
+ }
+}
+
+
+
+#include
+
+
+namespace Catch {
+#if defined(CATCH_CONFIG_DISABLE_EXCEPTIONS) && !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS_CUSTOM_HANDLER)
+ [[noreturn]]
+ void throw_exception(std::exception const& e) {
+ Catch::cerr() << "Catch will terminate because it needed to throw an exception.\n"
+ << "The message was: " << e.what() << '\n';
+ std::terminate();
+ }
+#endif
+
+ [[noreturn]]
+ void throw_logic_error(std::string const& msg) {
+ throw_exception(std::logic_error(msg));
+ }
+
+ [[noreturn]]
+ void throw_domain_error(std::string const& msg) {
+ throw_exception(std::domain_error(msg));
+ }
+
+ [[noreturn]]
+ void throw_runtime_error(std::string const& msg) {
+ throw_exception(std::runtime_error(msg));
+ }
+
+
+
+} // namespace Catch;
+
+
+
+#include
+
+namespace Catch {
+
+ IMutableEnumValuesRegistry::~IMutableEnumValuesRegistry() = default;
+
+ namespace Detail {
+
+ namespace {
+ // Extracts the actual name part of an enum instance
+ // In other words, it returns the Blue part of Bikeshed::Colour::Blue
+ StringRef extractInstanceName(StringRef enumInstance) {
+ // Find last occurrence of ":"
+ size_t name_start = enumInstance.size();
+ while (name_start > 0 && enumInstance[name_start - 1] != ':') {
+ --name_start;
+ }
+ return enumInstance.substr(name_start, enumInstance.size() - name_start);
+ }
+ }
+
+ std::vector parseEnums( StringRef enums ) {
+ auto enumValues = splitStringRef( enums, ',' );
+ std::vector parsed;
+ parsed.reserve( enumValues.size() );
+ for( auto const& enumValue : enumValues ) {
+ parsed.push_back(trim(extractInstanceName(enumValue)));
+ }
+ return parsed;
+ }
+
+ EnumInfo::~EnumInfo() = default;
+
+ StringRef EnumInfo::lookup( int value ) const {
+ for( auto const& valueToName : m_values ) {
+ if( valueToName.first == value )
+ return valueToName.second;
+ }
+ return "{** unexpected enum value **}"_sr;
+ }
+
+ Catch::Detail::unique_ptr makeEnumInfo( StringRef enumName, StringRef allValueNames, std::vector const& values ) {
+ auto enumInfo = Catch::Detail::make_unique();
+ enumInfo->m_name = enumName;
+ enumInfo->m_values.reserve( values.size() );
+
+ const auto valueNames = Catch::Detail::parseEnums( allValueNames );
+ assert( valueNames.size() == values.size() );
+ std::size_t i = 0;
+ for( auto value : values )
+ enumInfo->m_values.emplace_back(value, valueNames[i++]);
+
+ return enumInfo;
+ }
+
+ EnumInfo const& EnumValuesRegistry::registerEnum( StringRef enumName, StringRef allValueNames, std::vector const& values ) {
+ m_enumInfos.push_back(makeEnumInfo(enumName, allValueNames, values));
+ return *m_enumInfos.back();
+ }
+
+ } // Detail
+} // Catch
+
+
+
+
+
+#include
+
+namespace Catch {
+ ErrnoGuard::ErrnoGuard():m_oldErrno(errno){}
+ ErrnoGuard::~ErrnoGuard() { errno = m_oldErrno; }
+}
+
+
+
+#include
+
+namespace Catch {
+
+#if !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+ namespace {
+ static std::string tryTranslators(
+ std::vector<
+ Detail::unique_ptr> const& translators ) {
+ if ( translators.empty() ) {
+ std::rethrow_exception( std::current_exception() );
+ } else {
+ return translators[0]->translate( translators.begin() + 1,
+ translators.end() );
+ }
+ }
+
+ }
+#endif //!defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+
+ ExceptionTranslatorRegistry::~ExceptionTranslatorRegistry() = default;
+
+ void ExceptionTranslatorRegistry::registerTranslator( Detail::unique_ptr&& translator ) {
+ m_translators.push_back( CATCH_MOVE( translator ) );
+ }
+
+#if !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+ std::string ExceptionTranslatorRegistry::translateActiveException() const {
+ // Compiling a mixed mode project with MSVC means that CLR
+ // exceptions will be caught in (...) as well. However, these do
+ // do not fill-in std::current_exception and thus lead to crash
+ // when attempting rethrow.
+ // /EHa switch also causes structured exceptions to be caught
+ // here, but they fill-in current_exception properly, so
+ // at worst the output should be a little weird, instead of
+ // causing a crash.
+ if ( std::current_exception() == nullptr ) {
+ return "Non C++ exception. Possibly a CLR exception.";
+ }
+
+ // First we try user-registered translators. If none of them can
+ // handle the exception, it will be rethrown handled by our defaults.
+ try {
+ return tryTranslators(m_translators);
+ }
+ // To avoid having to handle TFE explicitly everywhere, we just
+ // rethrow it so that it goes back up the caller.
+ catch( TestFailureException& ) {
+ std::rethrow_exception(std::current_exception());
+ }
+ catch( TestSkipException& ) {
+ std::rethrow_exception(std::current_exception());
+ }
+ catch( std::exception const& ex ) {
+ return ex.what();
+ }
+ catch( std::string const& msg ) {
+ return msg;
+ }
+ catch( const char* msg ) {
+ return msg;
+ }
+ catch(...) {
+ return "Unknown exception";
+ }
+ }
+
+#else // ^^ Exceptions are enabled // Exceptions are disabled vv
+ std::string ExceptionTranslatorRegistry::translateActiveException() const {
+ CATCH_INTERNAL_ERROR("Attempted to translate active exception under CATCH_CONFIG_DISABLE_EXCEPTIONS!");
+ }
+#endif
+
+}
+
+
+
+/** \file
+ * This file provides platform specific implementations of FatalConditionHandler
+ *
+ * This means that there is a lot of conditional compilation, and platform
+ * specific code. Currently, Catch2 supports a dummy handler (if no
+ * handler is desired), and 2 platform specific handlers:
+ * * Windows' SEH
+ * * POSIX signals
+ *
+ * Consequently, various pieces of code below are compiled if either of
+ * the platform specific handlers is enabled, or if none of them are
+ * enabled. It is assumed that both cannot be enabled at the same time,
+ * and doing so should cause a compilation error.
+ *
+ * If another platform specific handler is added, the compile guards
+ * below will need to be updated taking these assumptions into account.
+ */
+
+
+
+#include
+
+#if !defined( CATCH_CONFIG_WINDOWS_SEH ) && !defined( CATCH_CONFIG_POSIX_SIGNALS )
+
+namespace Catch {
+
+ // If neither SEH nor signal handling is required, the handler impls
+ // do not have to do anything, and can be empty.
+ void FatalConditionHandler::engage_platform() {}
+ void FatalConditionHandler::disengage_platform() noexcept {}
+ FatalConditionHandler::FatalConditionHandler() = default;
+ FatalConditionHandler::~FatalConditionHandler() = default;
+
+} // end namespace Catch
+
+#endif // !CATCH_CONFIG_WINDOWS_SEH && !CATCH_CONFIG_POSIX_SIGNALS
+
+#if defined( CATCH_CONFIG_WINDOWS_SEH ) && defined( CATCH_CONFIG_POSIX_SIGNALS )
+#error "Inconsistent configuration: Windows' SEH handling and POSIX signals cannot be enabled at the same time"
+#endif // CATCH_CONFIG_WINDOWS_SEH && CATCH_CONFIG_POSIX_SIGNALS
+
+#if defined( CATCH_CONFIG_WINDOWS_SEH ) || defined( CATCH_CONFIG_POSIX_SIGNALS )
+
+namespace {
+ //! Signals fatal error message to the run context
+ void reportFatal( char const * const message ) {
+ Catch::getCurrentContext().getResultCapture()->handleFatalErrorCondition( message );
+ }
+
+ //! Minimal size Catch2 needs for its own fatal error handling.
+ //! Picked empirically, so it might not be sufficient on all
+ //! platforms, and for all configurations.
+ constexpr std::size_t minStackSizeForErrors = 32 * 1024;
+} // end unnamed namespace
+
+#endif // CATCH_CONFIG_WINDOWS_SEH || CATCH_CONFIG_POSIX_SIGNALS
+
+#if defined( CATCH_CONFIG_WINDOWS_SEH )
+
+namespace Catch {
+
+ struct SignalDefs { DWORD id; const char* name; };
+
+ // There is no 1-1 mapping between signals and windows exceptions.
+ // Windows can easily distinguish between SO and SigSegV,
+ // but SigInt, SigTerm, etc are handled differently.
+ static SignalDefs signalDefs[] = {
+ { EXCEPTION_ILLEGAL_INSTRUCTION, "SIGILL - Illegal instruction signal" },
+ { EXCEPTION_STACK_OVERFLOW, "SIGSEGV - Stack overflow" },
+ { EXCEPTION_ACCESS_VIOLATION, "SIGSEGV - Segmentation violation signal" },
+ { EXCEPTION_INT_DIVIDE_BY_ZERO, "Divide by zero error" },
+ };
+
+ static LONG CALLBACK topLevelExceptionFilter(PEXCEPTION_POINTERS ExceptionInfo) {
+ for (auto const& def : signalDefs) {
+ if (ExceptionInfo->ExceptionRecord->ExceptionCode == def.id) {
+ reportFatal(def.name);
+ }
+ }
+ // If its not an exception we care about, pass it along.
+ // This stops us from eating debugger breaks etc.
+ return EXCEPTION_CONTINUE_SEARCH;
+ }
+
+ // Since we do not support multiple instantiations, we put these
+ // into global variables and rely on cleaning them up in outlined
+ // constructors/destructors
+ static LPTOP_LEVEL_EXCEPTION_FILTER previousTopLevelExceptionFilter = nullptr;
+
+
+ // For MSVC, we reserve part of the stack memory for handling
+ // memory overflow structured exception.
+ FatalConditionHandler::FatalConditionHandler() {
+ ULONG guaranteeSize = static_cast(minStackSizeForErrors);
+ if (!SetThreadStackGuarantee(&guaranteeSize)) {
+ // We do not want to fully error out, because needing
+ // the stack reserve should be rare enough anyway.
+ Catch::cerr()
+ << "Failed to reserve piece of stack."
+ << " Stack overflows will not be reported successfully.";
+ }
+ }
+
+ // We do not attempt to unset the stack guarantee, because
+ // Windows does not support lowering the stack size guarantee.
+ FatalConditionHandler::~FatalConditionHandler() = default;
+
+
+ void FatalConditionHandler::engage_platform() {
+ // Register as a the top level exception filter.
+ previousTopLevelExceptionFilter = SetUnhandledExceptionFilter(topLevelExceptionFilter);
+ }
+
+ void FatalConditionHandler::disengage_platform() noexcept {
+ if (SetUnhandledExceptionFilter(previousTopLevelExceptionFilter) != topLevelExceptionFilter) {
+ Catch::cerr()
+ << "Unexpected SEH unhandled exception filter on disengage."
+ << " The filter was restored, but might be rolled back unexpectedly.";
+ }
+ previousTopLevelExceptionFilter = nullptr;
+ }
+
+} // end namespace Catch
+
+#endif // CATCH_CONFIG_WINDOWS_SEH
+
+#if defined( CATCH_CONFIG_POSIX_SIGNALS )
+
+#include
+
+namespace Catch {
+
+ struct SignalDefs {
+ int id;
+ const char* name;
+ };
+
+ static SignalDefs signalDefs[] = {
+ { SIGINT, "SIGINT - Terminal interrupt signal" },
+ { SIGILL, "SIGILL - Illegal instruction signal" },
+ { SIGFPE, "SIGFPE - Floating point error signal" },
+ { SIGSEGV, "SIGSEGV - Segmentation violation signal" },
+ { SIGTERM, "SIGTERM - Termination request signal" },
+ { SIGABRT, "SIGABRT - Abort (abnormal termination) signal" }
+ };
+
+// Older GCCs trigger -Wmissing-field-initializers for T foo = {}
+// which is zero initialization, but not explicit. We want to avoid
+// that.
+#if defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#endif
+
+ static char* altStackMem = nullptr;
+ static std::size_t altStackSize = 0;
+ static stack_t oldSigStack{};
+ static struct sigaction oldSigActions[sizeof(signalDefs) / sizeof(SignalDefs)]{};
+
+ static void restorePreviousSignalHandlers() noexcept {
+ // We set signal handlers back to the previous ones. Hopefully
+ // nobody overwrote them in the meantime, and doesn't expect
+ // their signal handlers to live past ours given that they
+ // installed them after ours..
+ for (std::size_t i = 0; i < sizeof(signalDefs) / sizeof(SignalDefs); ++i) {
+ sigaction(signalDefs[i].id, &oldSigActions[i], nullptr);
+ }
+ // Return the old stack
+ sigaltstack(&oldSigStack, nullptr);
+ }
+
+ static void handleSignal( int sig ) {
+ char const * name = "";
+ for (auto const& def : signalDefs) {
+ if (sig == def.id) {
+ name = def.name;
+ break;
+ }
+ }
+ // We need to restore previous signal handlers and let them do
+ // their thing, so that the users can have the debugger break
+ // when a signal is raised, and so on.
+ restorePreviousSignalHandlers();
+ reportFatal( name );
+ raise( sig );
+ }
+
+ FatalConditionHandler::FatalConditionHandler() {
+ assert(!altStackMem && "Cannot initialize POSIX signal handler when one already exists");
+ if (altStackSize == 0) {
+ altStackSize = std::max(static_cast(SIGSTKSZ), minStackSizeForErrors);
+ }
+ altStackMem = new char[altStackSize]();
+ }
+
+ FatalConditionHandler::~FatalConditionHandler() {
+ delete[] altStackMem;
+ // We signal that another instance can be constructed by zeroing
+ // out the pointer.
+ altStackMem = nullptr;
+ }
+
+ void FatalConditionHandler::engage_platform() {
+ stack_t sigStack;
+ sigStack.ss_sp = altStackMem;
+ sigStack.ss_size = altStackSize;
+ sigStack.ss_flags = 0;
+ sigaltstack(&sigStack, &oldSigStack);
+ struct sigaction sa = { };
+
+ sa.sa_handler = handleSignal;
+ sa.sa_flags = SA_ONSTACK;
+ for (std::size_t i = 0; i < sizeof(signalDefs)/sizeof(SignalDefs); ++i) {
+ sigaction(signalDefs[i].id, &sa, &oldSigActions[i]);
+ }
+ }
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+
+ void FatalConditionHandler::disengage_platform() noexcept {
+ restorePreviousSignalHandlers();
+ }
+
+} // end namespace Catch
+
+#endif // CATCH_CONFIG_POSIX_SIGNALS
+
+
+
+
+#include
+
+namespace Catch {
+ namespace Detail {
+
+ uint32_t convertToBits(float f) {
+ static_assert(sizeof(float) == sizeof(uint32_t), "Important ULP matcher assumption violated");
+ uint32_t i;
+ std::memcpy(&i, &f, sizeof(f));
+ return i;
+ }
+
+ uint64_t convertToBits(double d) {
+ static_assert(sizeof(double) == sizeof(uint64_t), "Important ULP matcher assumption violated");
+ uint64_t i;
+ std::memcpy(&i, &d, sizeof(d));
+ return i;
+ }
+
+#if defined( __GNUC__ ) || defined( __clang__ )
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wfloat-equal"
+#endif
+ bool directCompare( float lhs, float rhs ) { return lhs == rhs; }
+ bool directCompare( double lhs, double rhs ) { return lhs == rhs; }
+#if defined( __GNUC__ ) || defined( __clang__ )
+# pragma GCC diagnostic pop
+#endif
+
+
+ } // end namespace Detail
+} // end namespace Catch
+
+
+
+
+
+
+#include
+
+namespace Catch {
+ namespace Detail {
+
+#if !defined (CATCH_CONFIG_GETENV)
+ char const* getEnv( char const* ) { return nullptr; }
+#else
+
+ char const* getEnv( char const* varName ) {
+# if defined( _MSC_VER )
+# pragma warning( push )
+# pragma warning( disable : 4996 ) // use getenv_s instead of getenv
+# endif
+
+ return std::getenv( varName );
+
+# if defined( _MSC_VER )
+# pragma warning( pop )
+# endif
+ }
+#endif
+} // namespace Detail
+} // namespace Catch
+
+
+
+
+#include
+#include
+#include
+#include
+
+namespace Catch {
+
+ Catch::IStream::~IStream() = default;
+
+namespace Detail {
+ namespace {
+ template
+ class StreamBufImpl final : public std::streambuf {
+ char data[bufferSize];
+ WriterF m_writer;
+
+ public:
+ StreamBufImpl() {
+ setp( data, data + sizeof(data) );
+ }
+
+ ~StreamBufImpl() noexcept override {
+ StreamBufImpl::sync();
+ }
+
+ private:
+ int overflow( int c ) override {
+ sync();
+
+ if( c != EOF ) {
+ if( pbase() == epptr() )
+ m_writer( std::string( 1, static_cast( c ) ) );
+ else
+ sputc( static_cast( c ) );
+ }
+ return 0;
+ }
+
+ int sync() override {
+ if( pbase() != pptr() ) {
+ m_writer( std::string( pbase(), static_cast( pptr() - pbase() ) ) );
+ setp( pbase(), epptr() );
+ }
+ return 0;
+ }
+ };
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ struct OutputDebugWriter {
+
+ void operator()( std::string const& str ) {
+ if ( !str.empty() ) {
+ writeToDebugConsole( str );
+ }
+ }
+ };
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ class FileStream final : public IStream {
+ std::ofstream m_ofs;
+ public:
+ FileStream( std::string const& filename ) {
+ m_ofs.open( filename.c_str() );
+ CATCH_ENFORCE( !m_ofs.fail(), "Unable to open file: '" << filename << '\'' );
+ m_ofs << std::unitbuf;
+ }
+ public: // IStream
+ std::ostream& stream() override {
+ return m_ofs;
+ }
+ };
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ class CoutStream final : public IStream {
+ std::ostream m_os;
+ public:
+ // Store the streambuf from cout up-front because
+ // cout may get redirected when running tests
+ CoutStream() : m_os( Catch::cout().rdbuf() ) {}
+
+ public: // IStream
+ std::ostream& stream() override { return m_os; }
+ bool isConsole() const override { return true; }
+ };
+
+ class CerrStream : public IStream {
+ std::ostream m_os;
+
+ public:
+ // Store the streambuf from cerr up-front because
+ // cout may get redirected when running tests
+ CerrStream(): m_os( Catch::cerr().rdbuf() ) {}
+
+ public: // IStream
+ std::ostream& stream() override { return m_os; }
+ bool isConsole() const override { return true; }
+ };
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ class DebugOutStream final : public IStream {
+ Detail::unique_ptr> m_streamBuf;
+ std::ostream m_os;
+ public:
+ DebugOutStream()
+ : m_streamBuf( Detail::make_unique>() ),
+ m_os( m_streamBuf.get() )
+ {}
+
+ public: // IStream
+ std::ostream& stream() override { return m_os; }
+ };
+
+ } // unnamed namespace
+} // namespace Detail
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ auto makeStream( std::string const& filename ) -> Detail::unique_ptr {
+ if ( filename.empty() || filename == "-" ) {
+ return Detail::make_unique