Thanks for using Compiler Explorer
Sponsors
Jakt
C++
Ada
Analysis
Android Java
Android Kotlin
Assembly
C
C3
Carbon
C++ (Circle)
CIRCT
Clean
CMake
CMakeScript
COBOL
C++ for OpenCL
MLIR
Cppx
Cppx-Blue
Cppx-Gold
Cpp2-cppfront
Crystal
C#
CUDA C++
D
Dart
Elixir
Erlang
Fortran
F#
Go
Haskell
HLSL
Hook
Hylo
ispc
Java
Julia
Kotlin
LLVM IR
LLVM MIR
Modula-2
Nim
Objective-C
Objective-C++
OCaml
OpenCL C
Pascal
Pony
Python
Racket
Ruby
Rust
Snowball
Scala
Solidity
Spice
Swift
LLVM TableGen
Toit
TypeScript Native
V
Vala
Visual Basic
WASM
Zig
Javascript
GIMPLE
c++ source #1
Output
Compile to binary object
Link to binary
Execute the code
Intel asm syntax
Demangle identifiers
Verbose demangling
Filters
Unused labels
Library functions
Directives
Comments
Horizontal whitespace
Debug intrinsics
Compiler
6502-c++ 11.1.0
ARM GCC 10.2.0
ARM GCC 10.3.0
ARM GCC 10.4.0
ARM GCC 10.5.0
ARM GCC 11.1.0
ARM GCC 11.2.0
ARM GCC 11.3.0
ARM GCC 11.4.0
ARM GCC 12.1.0
ARM GCC 12.2.0
ARM GCC 12.3.0
ARM GCC 12.4.0
ARM GCC 13.1.0
ARM GCC 13.2.0
ARM GCC 13.2.0 (unknown-eabi)
ARM GCC 13.3.0
ARM GCC 13.3.0 (unknown-eabi)
ARM GCC 14.1.0
ARM GCC 14.1.0 (unknown-eabi)
ARM GCC 14.2.0
ARM GCC 14.2.0 (unknown-eabi)
ARM GCC 4.5.4
ARM GCC 4.6.4
ARM GCC 5.4
ARM GCC 6.3.0
ARM GCC 6.4.0
ARM GCC 7.3.0
ARM GCC 7.5.0
ARM GCC 8.2.0
ARM GCC 8.5.0
ARM GCC 9.3.0
ARM GCC 9.4.0
ARM GCC 9.5.0
ARM GCC trunk
ARM gcc 10.2.1 (none)
ARM gcc 10.3.1 (2021.07 none)
ARM gcc 10.3.1 (2021.10 none)
ARM gcc 11.2.1 (none)
ARM gcc 5.4.1 (none)
ARM gcc 7.2.1 (none)
ARM gcc 8.2 (WinCE)
ARM gcc 8.3.1 (none)
ARM gcc 9.2.1 (none)
ARM msvc v19.0 (WINE)
ARM msvc v19.10 (WINE)
ARM msvc v19.14 (WINE)
ARM64 Morello gcc 10.1 Alpha 2
ARM64 gcc 10.2
ARM64 gcc 10.3
ARM64 gcc 10.4
ARM64 gcc 10.5.0
ARM64 gcc 11.1
ARM64 gcc 11.2
ARM64 gcc 11.3
ARM64 gcc 11.4.0
ARM64 gcc 12.1
ARM64 gcc 12.2.0
ARM64 gcc 12.3.0
ARM64 gcc 12.4.0
ARM64 gcc 13.1.0
ARM64 gcc 13.2.0
ARM64 gcc 13.3.0
ARM64 gcc 14.1.0
ARM64 gcc 14.2.0
ARM64 gcc 4.9.4
ARM64 gcc 5.4
ARM64 gcc 5.5.0
ARM64 gcc 6.3
ARM64 gcc 6.4
ARM64 gcc 7.3
ARM64 gcc 7.5
ARM64 gcc 8.2
ARM64 gcc 8.5
ARM64 gcc 9.3
ARM64 gcc 9.4
ARM64 gcc 9.5
ARM64 gcc trunk
ARM64 msvc v19.14 (WINE)
AVR gcc 10.3.0
AVR gcc 11.1.0
AVR gcc 12.1.0
AVR gcc 12.2.0
AVR gcc 12.3.0
AVR gcc 12.4.0
AVR gcc 13.1.0
AVR gcc 13.2.0
AVR gcc 13.3.0
AVR gcc 14.1.0
AVR gcc 14.2.0
AVR gcc 4.5.4
AVR gcc 4.6.4
AVR gcc 5.4.0
AVR gcc 9.2.0
AVR gcc 9.3.0
Arduino Mega (1.8.9)
Arduino Uno (1.8.9)
BPF clang (trunk)
BPF clang 13.0.0
BPF clang 14.0.0
BPF clang 15.0.0
BPF clang 16.0.0
BPF clang 17.0.1
BPF clang 18.1.0
BPF clang 19.1.0
BPF gcc 13.1.0
BPF gcc 13.2.0
BPF gcc 13.3.0
BPF gcc trunk
EDG (experimental reflection)
EDG 6.5
EDG 6.5 (GNU mode gcc 13)
EDG 6.6
EDG 6.6 (GNU mode gcc 13)
FRC 2019
FRC 2020
FRC 2023
KVX ACB 4.1.0 (GCC 7.5.0)
KVX ACB 4.1.0-cd1 (GCC 7.5.0)
KVX ACB 4.10.0 (GCC 10.3.1)
KVX ACB 4.11.1 (GCC 10.3.1)
KVX ACB 4.12.0 (GCC 11.3.0)
KVX ACB 4.2.0 (GCC 7.5.0)
KVX ACB 4.3.0 (GCC 7.5.0)
KVX ACB 4.4.0 (GCC 7.5.0)
KVX ACB 4.6.0 (GCC 9.4.1)
KVX ACB 4.8.0 (GCC 9.4.1)
KVX ACB 4.9.0 (GCC 9.4.1)
KVX ACB 5.0.0 (GCC 12.2.1)
LoongArch64 clang (trunk)
LoongArch64 clang 17.0.1
LoongArch64 clang 18.1.0
LoongArch64 clang 19.1.0
M68K gcc 13.1.0
M68K gcc 13.2.0
M68K gcc 13.3.0
M68K gcc 14.1.0
M68K gcc 14.2.0
M68k clang (trunk)
MRISC32 gcc (trunk)
MSP430 gcc 4.5.3
MSP430 gcc 5.3.0
MSP430 gcc 6.2.1
MinGW clang 14.0.3
MinGW clang 14.0.6
MinGW clang 15.0.7
MinGW clang 16.0.0
MinGW clang 16.0.2
MinGW gcc 11.3.0
MinGW gcc 12.1.0
MinGW gcc 12.2.0
MinGW gcc 13.1.0
RISC-V (32-bits) gcc (trunk)
RISC-V (32-bits) gcc 10.2.0
RISC-V (32-bits) gcc 10.3.0
RISC-V (32-bits) gcc 11.2.0
RISC-V (32-bits) gcc 11.3.0
RISC-V (32-bits) gcc 11.4.0
RISC-V (32-bits) gcc 12.1.0
RISC-V (32-bits) gcc 12.2.0
RISC-V (32-bits) gcc 12.3.0
RISC-V (32-bits) gcc 12.4.0
RISC-V (32-bits) gcc 13.1.0
RISC-V (32-bits) gcc 13.2.0
RISC-V (32-bits) gcc 13.3.0
RISC-V (32-bits) gcc 14.1.0
RISC-V (32-bits) gcc 14.2.0
RISC-V (32-bits) gcc 8.2.0
RISC-V (32-bits) gcc 8.5.0
RISC-V (32-bits) gcc 9.4.0
RISC-V (64-bits) gcc (trunk)
RISC-V (64-bits) gcc 10.2.0
RISC-V (64-bits) gcc 10.3.0
RISC-V (64-bits) gcc 11.2.0
RISC-V (64-bits) gcc 11.3.0
RISC-V (64-bits) gcc 11.4.0
RISC-V (64-bits) gcc 12.1.0
RISC-V (64-bits) gcc 12.2.0
RISC-V (64-bits) gcc 12.3.0
RISC-V (64-bits) gcc 12.4.0
RISC-V (64-bits) gcc 13.1.0
RISC-V (64-bits) gcc 13.2.0
RISC-V (64-bits) gcc 13.3.0
RISC-V (64-bits) gcc 14.1.0
RISC-V (64-bits) gcc 14.2.0
RISC-V (64-bits) gcc 8.2.0
RISC-V (64-bits) gcc 8.5.0
RISC-V (64-bits) gcc 9.4.0
RISC-V rv32gc clang (trunk)
RISC-V rv32gc clang 10.0.0
RISC-V rv32gc clang 10.0.1
RISC-V rv32gc clang 11.0.0
RISC-V rv32gc clang 11.0.1
RISC-V rv32gc clang 12.0.0
RISC-V rv32gc clang 12.0.1
RISC-V rv32gc clang 13.0.0
RISC-V rv32gc clang 13.0.1
RISC-V rv32gc clang 14.0.0
RISC-V rv32gc clang 15.0.0
RISC-V rv32gc clang 16.0.0
RISC-V rv32gc clang 17.0.1
RISC-V rv32gc clang 18.1.0
RISC-V rv32gc clang 19.1.0
RISC-V rv32gc clang 9.0.0
RISC-V rv32gc clang 9.0.1
RISC-V rv64gc clang (trunk)
RISC-V rv64gc clang 10.0.0
RISC-V rv64gc clang 10.0.1
RISC-V rv64gc clang 11.0.0
RISC-V rv64gc clang 11.0.1
RISC-V rv64gc clang 12.0.0
RISC-V rv64gc clang 12.0.1
RISC-V rv64gc clang 13.0.0
RISC-V rv64gc clang 13.0.1
RISC-V rv64gc clang 14.0.0
RISC-V rv64gc clang 15.0.0
RISC-V rv64gc clang 16.0.0
RISC-V rv64gc clang 17.0.1
RISC-V rv64gc clang 18.1.0
RISC-V rv64gc clang 19.1.0
RISC-V rv64gc clang 9.0.0
RISC-V rv64gc clang 9.0.1
Raspbian Buster
Raspbian Stretch
SPARC LEON gcc 12.2.0
SPARC LEON gcc 12.3.0
SPARC LEON gcc 12.4.0
SPARC LEON gcc 13.1.0
SPARC LEON gcc 13.2.0
SPARC LEON gcc 13.3.0
SPARC LEON gcc 14.1.0
SPARC LEON gcc 14.2.0
SPARC gcc 12.2.0
SPARC gcc 12.3.0
SPARC gcc 12.4.0
SPARC gcc 13.1.0
SPARC gcc 13.2.0
SPARC gcc 13.3.0
SPARC gcc 14.1.0
SPARC gcc 14.2.0
SPARC64 gcc 12.2.0
SPARC64 gcc 12.3.0
SPARC64 gcc 12.4.0
SPARC64 gcc 13.1.0
SPARC64 gcc 13.2.0
SPARC64 gcc 13.3.0
SPARC64 gcc 14.1.0
SPARC64 gcc 14.2.0
TI C6x gcc 12.2.0
TI C6x gcc 12.3.0
TI C6x gcc 12.4.0
TI C6x gcc 13.1.0
TI C6x gcc 13.2.0
TI C6x gcc 13.3.0
TI C6x gcc 14.1.0
TI C6x gcc 14.2.0
TI CL430 21.6.1
VAX gcc NetBSDELF 10.4.0
VAX gcc NetBSDELF 10.5.0 (Nov 15 03:50:22 2023)
WebAssembly clang (trunk)
Xtensa ESP32 gcc 11.2.0 (2022r1)
Xtensa ESP32 gcc 12.2.0 (20230208)
Xtensa ESP32 gcc 8.2.0 (2019r2)
Xtensa ESP32 gcc 8.2.0 (2020r1)
Xtensa ESP32 gcc 8.2.0 (2020r2)
Xtensa ESP32 gcc 8.4.0 (2020r3)
Xtensa ESP32 gcc 8.4.0 (2021r1)
Xtensa ESP32 gcc 8.4.0 (2021r2)
Xtensa ESP32-S2 gcc 11.2.0 (2022r1)
Xtensa ESP32-S2 gcc 12.2.0 (20230208)
Xtensa ESP32-S2 gcc 8.2.0 (2019r2)
Xtensa ESP32-S2 gcc 8.2.0 (2020r1)
Xtensa ESP32-S2 gcc 8.2.0 (2020r2)
Xtensa ESP32-S2 gcc 8.4.0 (2020r3)
Xtensa ESP32-S2 gcc 8.4.0 (2021r1)
Xtensa ESP32-S2 gcc 8.4.0 (2021r2)
Xtensa ESP32-S3 gcc 11.2.0 (2022r1)
Xtensa ESP32-S3 gcc 12.2.0 (20230208)
Xtensa ESP32-S3 gcc 8.4.0 (2020r3)
Xtensa ESP32-S3 gcc 8.4.0 (2021r1)
Xtensa ESP32-S3 gcc 8.4.0 (2021r2)
arm64 msvc v19.20 VS16.0
arm64 msvc v19.21 VS16.1
arm64 msvc v19.22 VS16.2
arm64 msvc v19.23 VS16.3
arm64 msvc v19.24 VS16.4
arm64 msvc v19.25 VS16.5
arm64 msvc v19.27 VS16.7
arm64 msvc v19.28 VS16.8
arm64 msvc v19.28 VS16.9
arm64 msvc v19.29 VS16.10
arm64 msvc v19.29 VS16.11
arm64 msvc v19.30 VS17.0
arm64 msvc v19.31 VS17.1
arm64 msvc v19.32 VS17.2
arm64 msvc v19.33 VS17.3
arm64 msvc v19.34 VS17.4
arm64 msvc v19.35 VS17.5
arm64 msvc v19.36 VS17.6
arm64 msvc v19.37 VS17.7
arm64 msvc v19.38 VS17.8
arm64 msvc v19.39 VS17.9
arm64 msvc v19.40 VS17.10
arm64 msvc v19.latest
armv7-a clang (trunk)
armv7-a clang 10.0.0
armv7-a clang 10.0.1
armv7-a clang 11.0.0
armv7-a clang 11.0.1
armv7-a clang 12.0.0
armv7-a clang 12.0.1
armv7-a clang 13.0.0
armv7-a clang 13.0.1
armv7-a clang 14.0.0
armv7-a clang 15.0.0
armv7-a clang 16.0.0
armv7-a clang 17.0.1
armv7-a clang 18.1.0
armv7-a clang 19.1.0
armv7-a clang 9.0.0
armv7-a clang 9.0.1
armv8-a clang (all architectural features, trunk)
armv8-a clang (trunk)
armv8-a clang 10.0.0
armv8-a clang 10.0.1
armv8-a clang 11.0.0
armv8-a clang 11.0.1
armv8-a clang 12.0.0
armv8-a clang 13.0.0
armv8-a clang 14.0.0
armv8-a clang 15.0.0
armv8-a clang 16.0.0
armv8-a clang 17.0.1
armv8-a clang 18.1.0
armv8-a clang 19.1.0
armv8-a clang 9.0.0
armv8-a clang 9.0.1
ellcc 0.1.33
ellcc 0.1.34
ellcc 2017-07-16
hexagon-clang 16.0.5
llvm-mos atari2600-3e
llvm-mos atari2600-4k
llvm-mos atari2600-common
llvm-mos atari5200-supercart
llvm-mos atari8-cart-megacart
llvm-mos atari8-cart-std
llvm-mos atari8-cart-xegs
llvm-mos atari8-common
llvm-mos atari8-dos
llvm-mos c128
llvm-mos c64
llvm-mos commodore
llvm-mos cpm65
llvm-mos cx16
llvm-mos dodo
llvm-mos eater
llvm-mos mega65
llvm-mos nes
llvm-mos nes-action53
llvm-mos nes-cnrom
llvm-mos nes-gtrom
llvm-mos nes-mmc1
llvm-mos nes-mmc3
llvm-mos nes-nrom
llvm-mos nes-unrom
llvm-mos nes-unrom-512
llvm-mos osi-c1p
llvm-mos pce
llvm-mos pce-cd
llvm-mos pce-common
llvm-mos pet
llvm-mos rp6502
llvm-mos rpc8e
llvm-mos supervision
llvm-mos vic20
loongarch64 gcc 12.2.0
loongarch64 gcc 12.3.0
loongarch64 gcc 12.4.0
loongarch64 gcc 13.1.0
loongarch64 gcc 13.2.0
loongarch64 gcc 13.3.0
loongarch64 gcc 14.1.0
loongarch64 gcc 14.2.0
mips clang 13.0.0
mips clang 14.0.0
mips clang 15.0.0
mips clang 16.0.0
mips clang 17.0.1
mips clang 18.1.0
mips clang 19.1.0
mips gcc 11.2.0
mips gcc 12.1.0
mips gcc 12.2.0
mips gcc 12.3.0
mips gcc 12.4.0
mips gcc 13.1.0
mips gcc 13.2.0
mips gcc 13.3.0
mips gcc 14.1.0
mips gcc 14.2.0
mips gcc 4.9.4
mips gcc 5.4
mips gcc 5.5.0
mips gcc 9.3.0 (codescape)
mips gcc 9.5.0
mips64 (el) gcc 12.1.0
mips64 (el) gcc 12.2.0
mips64 (el) gcc 12.3.0
mips64 (el) gcc 12.4.0
mips64 (el) gcc 13.1.0
mips64 (el) gcc 13.2.0
mips64 (el) gcc 13.3.0
mips64 (el) gcc 14.1.0
mips64 (el) gcc 14.2.0
mips64 (el) gcc 4.9.4
mips64 (el) gcc 5.4.0
mips64 (el) gcc 5.5.0
mips64 (el) gcc 9.5.0
mips64 clang 13.0.0
mips64 clang 14.0.0
mips64 clang 15.0.0
mips64 clang 16.0.0
mips64 clang 17.0.1
mips64 clang 18.1.0
mips64 clang 19.1.0
mips64 gcc 11.2.0
mips64 gcc 12.1.0
mips64 gcc 12.2.0
mips64 gcc 12.3.0
mips64 gcc 12.4.0
mips64 gcc 13.1.0
mips64 gcc 13.2.0
mips64 gcc 13.3.0
mips64 gcc 14.1.0
mips64 gcc 14.2.0
mips64 gcc 4.9.4
mips64 gcc 5.4.0
mips64 gcc 5.5.0
mips64 gcc 9.5.0
mips64el clang 13.0.0
mips64el clang 14.0.0
mips64el clang 15.0.0
mips64el clang 16.0.0
mips64el clang 17.0.1
mips64el clang 18.1.0
mips64el clang 19.1.0
mipsel clang 13.0.0
mipsel clang 14.0.0
mipsel clang 15.0.0
mipsel clang 16.0.0
mipsel clang 17.0.1
mipsel clang 18.1.0
mipsel clang 19.1.0
mipsel gcc 12.1.0
mipsel gcc 12.2.0
mipsel gcc 12.3.0
mipsel gcc 12.4.0
mipsel gcc 13.1.0
mipsel gcc 13.2.0
mipsel gcc 13.3.0
mipsel gcc 14.1.0
mipsel gcc 14.2.0
mipsel gcc 4.9.4
mipsel gcc 5.4.0
mipsel gcc 5.5.0
mipsel gcc 9.5.0
nanoMIPS gcc 6.3.0 (mtk)
power gcc 11.2.0
power gcc 12.1.0
power gcc 12.2.0
power gcc 12.3.0
power gcc 12.4.0
power gcc 13.1.0
power gcc 13.2.0
power gcc 13.3.0
power gcc 14.1.0
power gcc 14.2.0
power gcc 4.8.5
power64 AT12.0 (gcc8)
power64 AT13.0 (gcc9)
power64 gcc 11.2.0
power64 gcc 12.1.0
power64 gcc 12.2.0
power64 gcc 12.3.0
power64 gcc 12.4.0
power64 gcc 13.1.0
power64 gcc 13.2.0
power64 gcc 13.3.0
power64 gcc 14.1.0
power64 gcc 14.2.0
power64 gcc trunk
power64le AT12.0 (gcc8)
power64le AT13.0 (gcc9)
power64le clang (trunk)
power64le gcc 11.2.0
power64le gcc 12.1.0
power64le gcc 12.2.0
power64le gcc 12.3.0
power64le gcc 12.4.0
power64le gcc 13.1.0
power64le gcc 13.2.0
power64le gcc 13.3.0
power64le gcc 14.1.0
power64le gcc 14.2.0
power64le gcc 6.3.0
power64le gcc trunk
powerpc64 clang (trunk)
s390x gcc 11.2.0
s390x gcc 12.1.0
s390x gcc 12.2.0
s390x gcc 12.3.0
s390x gcc 12.4.0
s390x gcc 13.1.0
s390x gcc 13.2.0
s390x gcc 13.3.0
s390x gcc 14.1.0
s390x gcc 14.2.0
sh gcc 12.2.0
sh gcc 12.3.0
sh gcc 12.4.0
sh gcc 13.1.0
sh gcc 13.2.0
sh gcc 13.3.0
sh gcc 14.1.0
sh gcc 14.2.0
sh gcc 4.9.4
sh gcc 9.5.0
vast (trunk)
x64 msvc v19.0 (WINE)
x64 msvc v19.10 (WINE)
x64 msvc v19.14 (WINE)
x64 msvc v19.20 VS16.0
x64 msvc v19.21 VS16.1
x64 msvc v19.22 VS16.2
x64 msvc v19.23 VS16.3
x64 msvc v19.24 VS16.4
x64 msvc v19.25 VS16.5
x64 msvc v19.27 VS16.7
x64 msvc v19.28 VS16.8
x64 msvc v19.28 VS16.9
x64 msvc v19.29 VS16.10
x64 msvc v19.29 VS16.11
x64 msvc v19.30 VS17.0
x64 msvc v19.31 VS17.1
x64 msvc v19.32 VS17.2
x64 msvc v19.33 VS17.3
x64 msvc v19.34 VS17.4
x64 msvc v19.35 VS17.5
x64 msvc v19.36 VS17.6
x64 msvc v19.37 VS17.7
x64 msvc v19.38 VS17.8
x64 msvc v19.39 VS17.9
x64 msvc v19.40 VS17.10
x64 msvc v19.latest
x86 djgpp 4.9.4
x86 djgpp 5.5.0
x86 djgpp 6.4.0
x86 djgpp 7.2.0
x86 msvc v19.0 (WINE)
x86 msvc v19.10 (WINE)
x86 msvc v19.14 (WINE)
x86 msvc v19.20 VS16.0
x86 msvc v19.21 VS16.1
x86 msvc v19.22 VS16.2
x86 msvc v19.23 VS16.3
x86 msvc v19.24 VS16.4
x86 msvc v19.25 VS16.5
x86 msvc v19.27 VS16.7
x86 msvc v19.28 VS16.8
x86 msvc v19.28 VS16.9
x86 msvc v19.29 VS16.10
x86 msvc v19.29 VS16.11
x86 msvc v19.30 VS17.0
x86 msvc v19.31 VS17.1
x86 msvc v19.32 VS17.2
x86 msvc v19.33 VS17.3
x86 msvc v19.34 VS17.4
x86 msvc v19.35 VS17.5
x86 msvc v19.36 VS17.6
x86 msvc v19.37 VS17.7
x86 msvc v19.38 VS17.8
x86 msvc v19.39 VS17.9
x86 msvc v19.40 VS17.10
x86 msvc v19.latest
x86 nvc++ 22.11
x86 nvc++ 22.7
x86 nvc++ 22.9
x86 nvc++ 23.1
x86 nvc++ 23.11
x86 nvc++ 23.3
x86 nvc++ 23.5
x86 nvc++ 23.7
x86 nvc++ 23.9
x86 nvc++ 24.1
x86 nvc++ 24.3
x86 nvc++ 24.5
x86 nvc++ 24.7
x86-64 Zapcc 190308
x86-64 clang (EricWF contracts)
x86-64 clang (amd-staging)
x86-64 clang (assertions trunk)
x86-64 clang (clangir)
x86-64 clang (dascandy contracts)
x86-64 clang (experimental -Wlifetime)
x86-64 clang (experimental P1061)
x86-64 clang (experimental P1144)
x86-64 clang (experimental P1221)
x86-64 clang (experimental P2996)
x86-64 clang (experimental P3068)
x86-64 clang (experimental P3309)
x86-64 clang (experimental P3367)
x86-64 clang (experimental P3372)
x86-64 clang (experimental metaprogramming - P2632)
x86-64 clang (old concepts branch)
x86-64 clang (p1974)
x86-64 clang (pattern matching - P2688)
x86-64 clang (reflection)
x86-64 clang (resugar)
x86-64 clang (thephd.dev)
x86-64 clang (trunk)
x86-64 clang (variadic friends - P2893)
x86-64 clang (widberg)
x86-64 clang 10.0.0
x86-64 clang 10.0.0 (assertions)
x86-64 clang 10.0.1
x86-64 clang 11.0.0
x86-64 clang 11.0.0 (assertions)
x86-64 clang 11.0.1
x86-64 clang 12.0.0
x86-64 clang 12.0.0 (assertions)
x86-64 clang 12.0.1
x86-64 clang 13.0.0
x86-64 clang 13.0.0 (assertions)
x86-64 clang 13.0.1
x86-64 clang 14.0.0
x86-64 clang 14.0.0 (assertions)
x86-64 clang 15.0.0
x86-64 clang 15.0.0 (assertions)
x86-64 clang 16.0.0
x86-64 clang 16.0.0 (assertions)
x86-64 clang 17.0.1
x86-64 clang 17.0.1 (assertions)
x86-64 clang 18.1.0
x86-64 clang 18.1.0 (assertions)
x86-64 clang 19.1.0
x86-64 clang 19.1.0 (assertions)
x86-64 clang 2.6.0 (assertions)
x86-64 clang 2.7.0 (assertions)
x86-64 clang 2.8.0 (assertions)
x86-64 clang 2.9.0 (assertions)
x86-64 clang 3.0.0
x86-64 clang 3.0.0 (assertions)
x86-64 clang 3.1
x86-64 clang 3.1 (assertions)
x86-64 clang 3.2
x86-64 clang 3.2 (assertions)
x86-64 clang 3.3
x86-64 clang 3.3 (assertions)
x86-64 clang 3.4 (assertions)
x86-64 clang 3.4.1
x86-64 clang 3.5
x86-64 clang 3.5 (assertions)
x86-64 clang 3.5.1
x86-64 clang 3.5.2
x86-64 clang 3.6
x86-64 clang 3.6 (assertions)
x86-64 clang 3.7
x86-64 clang 3.7 (assertions)
x86-64 clang 3.7.1
x86-64 clang 3.8
x86-64 clang 3.8 (assertions)
x86-64 clang 3.8.1
x86-64 clang 3.9.0
x86-64 clang 3.9.0 (assertions)
x86-64 clang 3.9.1
x86-64 clang 4.0.0
x86-64 clang 4.0.0 (assertions)
x86-64 clang 4.0.1
x86-64 clang 5.0.0
x86-64 clang 5.0.0 (assertions)
x86-64 clang 5.0.1
x86-64 clang 5.0.2
x86-64 clang 6.0.0
x86-64 clang 6.0.0 (assertions)
x86-64 clang 6.0.1
x86-64 clang 7.0.0
x86-64 clang 7.0.0 (assertions)
x86-64 clang 7.0.1
x86-64 clang 7.1.0
x86-64 clang 8.0.0
x86-64 clang 8.0.0 (assertions)
x86-64 clang 8.0.1
x86-64 clang 9.0.0
x86-64 clang 9.0.0 (assertions)
x86-64 clang 9.0.1
x86-64 clang rocm-4.5.2
x86-64 clang rocm-5.0.2
x86-64 clang rocm-5.1.3
x86-64 clang rocm-5.2.3
x86-64 clang rocm-5.3.3
x86-64 clang rocm-5.7.0
x86-64 clang rocm-6.0.2
x86-64 clang rocm-6.1.2
x86-64 gcc (contract labels)
x86-64 gcc (contracts natural syntax)
x86-64 gcc (contracts)
x86-64 gcc (coroutines)
x86-64 gcc (modules)
x86-64 gcc (trunk)
x86-64 gcc 10.1
x86-64 gcc 10.2
x86-64 gcc 10.3
x86-64 gcc 10.4
x86-64 gcc 10.5
x86-64 gcc 11.1
x86-64 gcc 11.2
x86-64 gcc 11.3
x86-64 gcc 11.4
x86-64 gcc 12.1
x86-64 gcc 12.2
x86-64 gcc 12.3
x86-64 gcc 12.4
x86-64 gcc 13.1
x86-64 gcc 13.2
x86-64 gcc 13.3
x86-64 gcc 14.1
x86-64 gcc 14.2
x86-64 gcc 3.4.6
x86-64 gcc 4.0.4
x86-64 gcc 4.1.2
x86-64 gcc 4.4.7
x86-64 gcc 4.5.3
x86-64 gcc 4.6.4
x86-64 gcc 4.7.1
x86-64 gcc 4.7.2
x86-64 gcc 4.7.3
x86-64 gcc 4.7.4
x86-64 gcc 4.8.1
x86-64 gcc 4.8.2
x86-64 gcc 4.8.3
x86-64 gcc 4.8.4
x86-64 gcc 4.8.5
x86-64 gcc 4.9.0
x86-64 gcc 4.9.1
x86-64 gcc 4.9.2
x86-64 gcc 4.9.3
x86-64 gcc 4.9.4
x86-64 gcc 5.1
x86-64 gcc 5.2
x86-64 gcc 5.3
x86-64 gcc 5.4
x86-64 gcc 5.5
x86-64 gcc 6.1
x86-64 gcc 6.2
x86-64 gcc 6.3
x86-64 gcc 6.4
x86-64 gcc 6.5
x86-64 gcc 7.1
x86-64 gcc 7.2
x86-64 gcc 7.3
x86-64 gcc 7.4
x86-64 gcc 7.5
x86-64 gcc 8.1
x86-64 gcc 8.2
x86-64 gcc 8.3
x86-64 gcc 8.4
x86-64 gcc 8.5
x86-64 gcc 9.1
x86-64 gcc 9.2
x86-64 gcc 9.3
x86-64 gcc 9.4
x86-64 gcc 9.5
x86-64 icc 13.0.1
x86-64 icc 16.0.3
x86-64 icc 17.0.0
x86-64 icc 18.0.0
x86-64 icc 19.0.0
x86-64 icc 19.0.1
x86-64 icc 2021.1.2
x86-64 icc 2021.10.0
x86-64 icc 2021.2.0
x86-64 icc 2021.3.0
x86-64 icc 2021.4.0
x86-64 icc 2021.5.0
x86-64 icc 2021.6.0
x86-64 icc 2021.7.0
x86-64 icc 2021.7.1
x86-64 icc 2021.8.0
x86-64 icc 2021.9.0
x86-64 icx (latest)
x86-64 icx 2021.1.2
x86-64 icx 2021.2.0
x86-64 icx 2021.3.0
x86-64 icx 2021.4.0
x86-64 icx 2022.0.0
x86-64 icx 2022.1.0
x86-64 icx 2022.2.0
x86-64 icx 2022.2.1
x86-64 icx 2023.0.0
x86-64 icx 2023.1.0
x86-64 icx 2023.2.1
x86-64 icx 2024.0.0
x86-64 icx 2024.1.0
x86-64 icx 2024.2.0
zig c++ 0.10.0
zig c++ 0.11.0
zig c++ 0.12.0
zig c++ 0.12.1
zig c++ 0.13.0
zig c++ 0.6.0
zig c++ 0.7.0
zig c++ 0.7.1
zig c++ 0.8.0
zig c++ 0.9.0
zig c++ trunk
Options
Source code
#define SNITCH_DEFINE_MAIN 1 #define SNITCH_IMPLEMENTATION #ifndef SNITCH_CONFIG_HPP #define SNITCH_CONFIG_HPP // These are defined from build-time configuration. // clang-format off #define SNITCH_VERSION "1.0.0" #define SNITCH_FULL_VERSION "1.0.0.ea200a0" #define SNITCH_VERSION_MAJOR 1 #define SNITCH_VERSION_MINOR 0 #define SNITCH_VERSION_PATCH 0 #if !defined(SNITCH_MAX_TEST_CASES) # define SNITCH_MAX_TEST_CASES 5000 #endif #if !defined(SNITCH_MAX_NESTED_SECTIONS) # define SNITCH_MAX_NESTED_SECTIONS 8 #endif #if !defined(SNITCH_MAX_EXPR_LENGTH) # define SNITCH_MAX_EXPR_LENGTH 1024 #endif #if !defined(SNITCH_MAX_MESSAGE_LENGTH) # define SNITCH_MAX_MESSAGE_LENGTH 1024 #endif #if !defined(SNITCH_MAX_TEST_NAME_LENGTH) # define SNITCH_MAX_TEST_NAME_LENGTH 1024 #endif #if !defined(SNITCH_MAX_TAG_LENGTH) # define SNITCH_MAX_TAG_LENGTH 256 #endif #if !defined(SNITCH_MAX_CAPTURES) # define SNITCH_MAX_CAPTURES 8 #endif #if !defined(SNITCH_MAX_CAPTURE_LENGTH) # define SNITCH_MAX_CAPTURE_LENGTH 256 #endif #if !defined(SNITCH_MAX_UNIQUE_TAGS) # define SNITCH_MAX_UNIQUE_TAGS 1024 #endif #if !defined(SNITCH_MAX_COMMAND_LINE_ARGS) # define SNITCH_MAX_COMMAND_LINE_ARGS 1024 #endif #if !defined(SNITCH_DEFINE_MAIN) # define SNITCH_DEFINE_MAIN 1 #endif #if !defined(SNITCH_WITH_EXCEPTIONS) # define SNITCH_WITH_EXCEPTIONS 1 #endif #if !defined(SNITCH_WITH_TIMINGS) # define SNITCH_WITH_TIMINGS 1 #endif #if !defined(SNITCH_WITH_SHORTHAND_MACROS) # define SNITCH_WITH_SHORTHAND_MACROS 1 #endif #if !defined(SNITCH_DEFAULT_WITH_COLOR) # define SNITCH_DEFAULT_WITH_COLOR 1 #endif // clang-format on #if defined(_MSC_VER) # if defined(_KERNEL_MODE) || (defined(_HAS_EXCEPTIONS) && !_HAS_EXCEPTIONS) # define SNITCH_EXCEPTIONS_NOT_AVAILABLE # endif #elif defined(__clang__) || defined(__GNUC__) # if !defined(__EXCEPTIONS) # define SNITCH_EXCEPTIONS_NOT_AVAILABLE # endif #endif #if defined(SNITCH_EXCEPTIONS_NOT_AVAILABLE) # undef SNITCH_WITH_EXCEPTIONS # define SNITCH_WITH_EXCEPTIONS 0 #endif #endif #ifndef SNITCH_HPP #define SNITCH_HPP #include <array> // for small_vector #include <cstddef> // for std::size_t #if SNITCH_WITH_EXCEPTIONS # include <exception> // for std::exception #endif #include <compare> // for std::partial_ordering #include <initializer_list> // for std::initializer_list #include <optional> // for cli #include <string_view> // for all strings #include <type_traits> // for std::is_nothrow_* #include <variant> // for events and small_function // Testing framework configuration. // -------------------------------- namespace snitch { // Maximum number of test cases in the whole program. // A "test case" is created for each uses of the `*_TEST_CASE` macros, // and for each type for the `TEMPLATE_LIST_TEST_CASE` macro. constexpr std::size_t max_test_cases = SNITCH_MAX_TEST_CASES; // Maximum depth of nested sections in a test case (section in section in section ...). constexpr std::size_t max_nested_sections = SNITCH_MAX_NESTED_SECTIONS; // Maximum length of a `CHECK(...)` or `REQUIRE(...)` expression, // beyond which automatic variable printing is disabled. constexpr std::size_t max_expr_length = SNITCH_MAX_EXPR_LENGTH; // Maximum length of error messages. constexpr std::size_t max_message_length = SNITCH_MAX_MESSAGE_LENGTH; // Maximum length of a full test case name. // The full test case name includes the base name, plus any type. constexpr std::size_t max_test_name_length = SNITCH_MAX_TEST_NAME_LENGTH; // Maximum length of a tag, including brackets. constexpr std::size_t max_tag_length = SNITCH_MAX_TAG_LENGTH; // Maximum number of captured expressions in a test case. constexpr std::size_t max_captures = SNITCH_MAX_CAPTURES; // Maximum length of a captured expression. constexpr std::size_t max_capture_length = SNITCH_MAX_CAPTURE_LENGTH; // Maximum number of unique tags in the whole program. constexpr std::size_t max_unique_tags = SNITCH_MAX_UNIQUE_TAGS; // Maximum number of command line arguments. constexpr std::size_t max_command_line_args = SNITCH_MAX_COMMAND_LINE_ARGS; } // namespace snitch // Forward declarations and public utilities. // ------------------------------------------ namespace snitch { class registry; struct test_id { std::string_view name = {}; std::string_view tags = {}; std::string_view type = {}; }; struct section_id { std::string_view name = {}; std::string_view description = {}; }; } // namespace snitch namespace snitch::matchers { enum class match_status { failed, matched }; } // namespace snitch::matchers // Implementation details. // ----------------------- namespace snitch::impl { template<typename T> constexpr std::string_view get_type_name() noexcept { #if defined(__clang__) constexpr auto prefix = std::string_view{"[T = "}; constexpr auto suffix = "]"; constexpr auto function = std::string_view{__PRETTY_FUNCTION__}; #elif defined(__GNUC__) constexpr auto prefix = std::string_view{"with T = "}; constexpr auto suffix = "; "; constexpr auto function = std::string_view{__PRETTY_FUNCTION__}; #elif defined(_MSC_VER) constexpr auto prefix = std::string_view{"get_type_name<"}; constexpr auto suffix = ">(void)"; constexpr auto function = std::string_view{__FUNCSIG__}; #else # error Unsupported compiler #endif const auto start = function.find(prefix) + prefix.size(); const auto end = function.find(suffix); const auto size = end - start; return function.substr(start, size); } } // namespace snitch::impl // Public utilities. // ------------------------------------------------ namespace snitch { template<typename T> constexpr std::string_view type_name = impl::get_type_name<T>(); template<typename... Args> struct type_list {}; [[noreturn]] void terminate_with(std::string_view msg) noexcept; } // namespace snitch // Public utilities: small_vector. // ------------------------------- namespace snitch { template<typename ElemType> class small_vector_span { ElemType* buffer_ptr = nullptr; std::size_t buffer_size = 0; std::size_t* data_size = nullptr; public: constexpr explicit small_vector_span(ElemType* b, std::size_t bl, std::size_t* s) noexcept : buffer_ptr(b), buffer_size(bl), data_size(s) {} constexpr std::size_t capacity() const noexcept { return buffer_size; } constexpr std::size_t available() const noexcept { return capacity() - size(); } constexpr std::size_t size() const noexcept { return *data_size; } constexpr bool empty() const noexcept { return *data_size == 0; } constexpr void clear() noexcept { *data_size = 0; } constexpr void resize(std::size_t size) noexcept { if (size > buffer_size) { terminate_with("small vector is full"); } *data_size = size; } constexpr void grow(std::size_t elem) noexcept { if (*data_size + elem > buffer_size) { terminate_with("small vector is full"); } *data_size += elem; } constexpr ElemType& push_back(const ElemType& t) noexcept(std::is_nothrow_copy_assignable_v<ElemType>) { if (*data_size == buffer_size) { terminate_with("small vector is full"); } ++*data_size; ElemType& elem = buffer_ptr[*data_size - 1]; elem = t; return elem; } constexpr ElemType& push_back(ElemType&& t) noexcept(std::is_nothrow_move_assignable_v<ElemType>) { if (*data_size == buffer_size) { terminate_with("small vector is full"); } ++*data_size; ElemType& elem = buffer_ptr[*data_size - 1]; elem = std::move(t); return elem; } constexpr void pop_back() noexcept { if (*data_size == 0) { terminate_with("pop_back() called on empty vector"); } --*data_size; } constexpr ElemType& back() noexcept { if (*data_size == 0) { terminate_with("back() called on empty vector"); } return buffer_ptr[*data_size - 1]; } constexpr const ElemType& back() const noexcept { if (*data_size == 0) { terminate_with("back() called on empty vector"); } return buffer_ptr[*data_size - 1]; } constexpr ElemType* data() noexcept { return buffer_ptr; } constexpr const ElemType* data() const noexcept { return buffer_ptr; } constexpr ElemType* begin() noexcept { return data(); } constexpr ElemType* end() noexcept { return begin() + size(); } constexpr const ElemType* begin() const noexcept { return data(); } constexpr const ElemType* end() const noexcept { return begin() + size(); } constexpr const ElemType* cbegin() const noexcept { return data(); } constexpr const ElemType* cend() const noexcept { return begin() + size(); } constexpr ElemType& operator[](std::size_t i) noexcept { if (i >= size()) { terminate_with("operator[] called with incorrect index"); } return buffer_ptr[i]; } constexpr const ElemType& operator[](std::size_t i) const noexcept { if (i >= size()) { terminate_with("operator[] called with incorrect index"); } return buffer_ptr[i]; } }; template<typename ElemType> class small_vector_span<const ElemType> { const ElemType* buffer_ptr = nullptr; std::size_t buffer_size = 0; const std::size_t* data_size = nullptr; public: constexpr small_vector_span() noexcept = default; constexpr explicit small_vector_span( const ElemType* b, std::size_t bl, const std::size_t* s) noexcept : buffer_ptr(b), buffer_size(bl), data_size(s) {} constexpr std::size_t capacity() const noexcept { return buffer_size; } constexpr std::size_t available() const noexcept { return capacity() - size(); } constexpr std::size_t size() const noexcept { return *data_size; } constexpr bool empty() const noexcept { return *data_size == 0; } constexpr const ElemType& back() const noexcept { if (*data_size == 0) { terminate_with("back() called on empty vector"); } return buffer_ptr[*data_size - 1]; } constexpr const ElemType* data() const noexcept { return buffer_ptr; } constexpr const ElemType* begin() const noexcept { return data(); } constexpr const ElemType* end() const noexcept { return begin() + size(); } constexpr const ElemType* cbegin() const noexcept { return data(); } constexpr const ElemType* cend() const noexcept { return begin() + size(); } constexpr const ElemType& operator[](std::size_t i) const noexcept { if (i >= size()) { terminate_with("operator[] called with incorrect index"); } return buffer_ptr[i]; } }; template<typename ElemType, std::size_t MaxLength> class small_vector { std::array<ElemType, MaxLength> data_buffer = {}; std::size_t data_size = 0; public: constexpr small_vector() noexcept = default; constexpr small_vector(const small_vector& other) noexcept = default; constexpr small_vector(small_vector&& other) noexcept = default; constexpr small_vector(std::initializer_list<ElemType> list) noexcept( noexcept(span().push_back(std::declval<ElemType>()))) { for (const auto& e : list) { span().push_back(e); } } constexpr small_vector& operator=(const small_vector& other) noexcept = default; constexpr small_vector& operator=(small_vector&& other) noexcept = default; constexpr std::size_t capacity() const noexcept { return MaxLength; } constexpr std::size_t available() const noexcept { return MaxLength - data_size; } constexpr std::size_t size() const noexcept { return data_size; } constexpr bool empty() const noexcept { return data_size == 0u; } constexpr void clear() noexcept { span().clear(); } constexpr void resize(std::size_t size) noexcept { span().resize(size); } constexpr void grow(std::size_t elem) noexcept { span().grow(elem); } template<typename U = const ElemType&> constexpr ElemType& push_back(U&& t) noexcept(noexcept(this->span().push_back(t))) { return this->span().push_back(t); } constexpr void pop_back() noexcept { return span().pop_back(); } constexpr ElemType& back() noexcept { return span().back(); } constexpr const ElemType& back() const noexcept { return const_cast<small_vector*>(this)->span().back(); } constexpr ElemType* data() noexcept { return data_buffer.data(); } constexpr const ElemType* data() const noexcept { return data_buffer.data(); } constexpr ElemType* begin() noexcept { return data(); } constexpr ElemType* end() noexcept { return begin() + size(); } constexpr const ElemType* begin() const noexcept { return data(); } constexpr const ElemType* end() const noexcept { return begin() + size(); } constexpr const ElemType* cbegin() const noexcept { return data(); } constexpr const ElemType* cend() const noexcept { return begin() + size(); } constexpr small_vector_span<ElemType> span() noexcept { return small_vector_span<ElemType>(data_buffer.data(), MaxLength, &data_size); } constexpr small_vector_span<const ElemType> span() const noexcept { return small_vector_span<const ElemType>(data_buffer.data(), MaxLength, &data_size); } constexpr operator small_vector_span<ElemType>() noexcept { return span(); } constexpr operator small_vector_span<const ElemType>() const noexcept { return span(); } constexpr ElemType& operator[](std::size_t i) noexcept { return span()[i]; } constexpr const ElemType& operator[](std::size_t i) const noexcept { return const_cast<small_vector*>(this)->span()[i]; } }; } // namespace snitch // Public utilities: small_string. // ------------------------------- namespace snitch { using small_string_span = small_vector_span<char>; using small_string_view = small_vector_span<const char>; template<std::size_t MaxLength> class small_string { std::array<char, MaxLength> data_buffer = {}; std::size_t data_size = 0u; public: constexpr small_string() noexcept = default; constexpr small_string(const small_string& other) noexcept = default; constexpr small_string(small_string&& other) noexcept = default; constexpr small_string(std::string_view str) noexcept { resize(str.size()); for (std::size_t i = 0; i < str.size(); ++i) { data_buffer[i] = str[i]; } } constexpr small_string& operator=(const small_string& other) noexcept = default; constexpr small_string& operator=(small_string&& other) noexcept = default; constexpr std::string_view str() const noexcept { return std::string_view(data(), length()); } constexpr std::size_t capacity() const noexcept { return MaxLength; } constexpr std::size_t available() const noexcept { return MaxLength - data_size; } constexpr std::size_t size() const noexcept { return data_size; } constexpr std::size_t length() const noexcept { return data_size; } constexpr bool empty() const noexcept { return data_size == 0u; } constexpr void clear() noexcept { span().clear(); } constexpr void resize(std::size_t length) noexcept { span().resize(length); } constexpr void grow(std::size_t chars) noexcept { span().grow(chars); } constexpr char& push_back(char t) noexcept { return span().push_back(t); } constexpr void pop_back() noexcept { return span().pop_back(); } constexpr char& back() noexcept { return span().back(); } constexpr const char& back() const noexcept { return span().back(); } constexpr char* data() noexcept { return data_buffer.data(); } constexpr const char* data() const noexcept { return data_buffer.data(); } constexpr char* begin() noexcept { return data(); } constexpr char* end() noexcept { return begin() + length(); } constexpr const char* begin() const noexcept { return data(); } constexpr const char* end() const noexcept { return begin() + length(); } constexpr const char* cbegin() const noexcept { return data(); } constexpr const char* cend() const noexcept { return begin() + length(); } constexpr small_string_span span() noexcept { return small_string_span(data_buffer.data(), MaxLength, &data_size); } constexpr small_string_view span() const noexcept { return small_string_view(data_buffer.data(), MaxLength, &data_size); } constexpr operator small_string_span() noexcept { return span(); } constexpr operator small_string_view() const noexcept { return span(); } constexpr char& operator[](std::size_t i) noexcept { return span()[i]; } constexpr char operator[](std::size_t i) const noexcept { return const_cast<small_string*>(this)->span()[i]; } constexpr operator std::string_view() const noexcept { return std::string_view(data(), length()); } }; [[nodiscard]] bool append(small_string_span ss, std::string_view value) noexcept; [[nodiscard]] bool append(small_string_span ss, const void* ptr) noexcept; [[nodiscard]] bool append(small_string_span ss, std::nullptr_t) noexcept; [[nodiscard]] bool append(small_string_span ss, std::size_t i) noexcept; [[nodiscard]] bool append(small_string_span ss, std::ptrdiff_t i) noexcept; [[nodiscard]] bool append(small_string_span ss, float f) noexcept; [[nodiscard]] bool append(small_string_span ss, double f) noexcept; [[nodiscard]] bool append(small_string_span ss, bool value) noexcept; template<typename T> [[nodiscard]] bool append(small_string_span ss, T* ptr) noexcept { if constexpr (std::is_same_v<std::remove_cv_t<T>, char>) { return append(ss, std::string_view(ptr)); } else if constexpr (std::is_function_v<T>) { if (ptr != nullptr) { return append(ss, std::string_view("0x????????")); } else { return append(ss, std::string_view("nullptr")); } } else { return append(ss, static_cast<const void*>(ptr)); } } template<std::size_t N> [[nodiscard]] bool append(small_string_span ss, const char str[N]) noexcept { return append(ss, std::string_view(str)); } template<typename T> concept signed_integral = std::is_signed_v<T>; template<typename T> concept unsigned_integral = std::is_unsigned_v<T>; template<typename T, typename U> concept convertible_to = std::is_convertible_v<T, U>; template<typename T> concept enumeration = std::is_enum_v<T>; template<signed_integral T> [[nodiscard]] bool append(small_string_span ss, T value) noexcept { return snitch::append(ss, static_cast<std::ptrdiff_t>(value)); } template<unsigned_integral T> [[nodiscard]] bool append(small_string_span ss, T value) noexcept { return snitch::append(ss, static_cast<std::size_t>(value)); } template<enumeration T> [[nodiscard]] bool append(small_string_span ss, T value) noexcept { return append(ss, static_cast<std::underlying_type_t<T>>(value)); } template<convertible_to<std::string_view> T> [[nodiscard]] bool append(small_string_span ss, const T& value) noexcept { return snitch::append(ss, std::string_view(value)); } template<typename T> concept string_appendable = requires(small_string_span ss, T value) { append(ss, value); }; template<string_appendable T, string_appendable U, string_appendable... Args> [[nodiscard]] bool append(small_string_span ss, T&& t, U&& u, Args&&... args) noexcept { return append(ss, std::forward<T>(t)) && append(ss, std::forward<U>(u)) && (append(ss, std::forward<Args>(args)) && ...); } void truncate_end(small_string_span ss) noexcept; template<string_appendable... Args> bool append_or_truncate(small_string_span ss, Args&&... args) noexcept { if (!append(ss, std::forward<Args>(args)...)) { truncate_end(ss); return false; } return true; } [[nodiscard]] bool replace_all( small_string_span string, std::string_view pattern, std::string_view replacement) noexcept; [[nodiscard]] bool is_match(std::string_view string, std::string_view regex) noexcept; enum class filter_result { included, excluded, not_included, not_excluded }; [[nodiscard]] filter_result is_filter_match_name(std::string_view name, std::string_view filter) noexcept; [[nodiscard]] filter_result is_filter_match_tags(std::string_view tags, std::string_view filter) noexcept; [[nodiscard]] filter_result is_filter_match_id(const test_id& id, std::string_view filter) noexcept; template<typename T, typename U> concept matcher_for = requires(const T& m, const U& value) { { m.match(value) } -> convertible_to<bool>; { m.describe_match(value, matchers::match_status{}) } -> convertible_to<std::string_view>; }; } // namespace snitch // Public utilities: small_function. // --------------------------------- namespace snitch { template<typename... Args> struct overload : Args... { using Args::operator()...; }; template<typename... Args> overload(Args...) -> overload<Args...>; template<auto T> struct constant { static constexpr auto value = T; }; template<typename T> class small_function { static_assert(!std::is_same_v<T, T>, "incorrect template parameter for small_function"); }; template<typename Ret, typename... Args> class small_function<Ret(Args...) noexcept> { using function_ptr = Ret (*)(Args...) noexcept; using function_data_ptr = Ret (*)(void*, Args...) noexcept; using function_const_data_ptr = Ret (*)(const void*, Args...) noexcept; struct function_and_data_ptr { void* data = nullptr; function_data_ptr ptr; }; struct function_and_const_data_ptr { const void* data = nullptr; function_const_data_ptr ptr; }; using data_type = std:: variant<std::monostate, function_ptr, function_and_data_ptr, function_and_const_data_ptr>; data_type data; public: constexpr small_function() = default; constexpr small_function(function_ptr ptr) noexcept : data{ptr} {} template<convertible_to<function_ptr> T> constexpr small_function(T&& obj) noexcept : data{static_cast<function_ptr>(obj)} {} template<typename T, auto M> constexpr small_function(T& obj, constant<M>) noexcept : data{function_and_data_ptr{ &obj, [](void* ptr, Args... args) noexcept { if constexpr (std::is_same_v<Ret, void>) { (static_cast<T*>(ptr)->*constant<M>::value)(std::move(args)...); } else { return (static_cast<T*>(ptr)->*constant<M>::value)(std::move(args)...); } }}} {} template<typename T, auto M> constexpr small_function(const T& obj, constant<M>) noexcept : data{function_and_const_data_ptr{ &obj, [](const void* ptr, Args... args) noexcept { if constexpr (std::is_same_v<Ret, void>) { (static_cast<const T*>(ptr)->*constant<M>::value)(std::move(args)...); } else { return (static_cast<const T*>(ptr)->*constant<M>::value)(std::move(args)...); } }}} {} template<typename T> constexpr small_function(T& obj) noexcept : small_function(obj, constant<&T::operator()>{}) {} template<typename T> constexpr small_function(const T& obj) noexcept : small_function(obj, constant<&T::operator()>{}) {} // Prevent inadvertently using temporary stateful lambda; not supported at the moment. template<typename T> constexpr small_function(T&& obj) noexcept = delete; // Prevent inadvertently using temporary object; not supported at the moment. template<typename T, auto M> constexpr small_function(T&& obj, constant<M>) noexcept = delete; template<typename... CArgs> constexpr Ret operator()(CArgs&&... args) const noexcept { if constexpr (std::is_same_v<Ret, void>) { std::visit( overload{ [](std::monostate) { terminate_with("small_function called without an implementation"); }, [&](function_ptr f) { (*f)(std::forward<CArgs>(args)...); }, [&](const function_and_data_ptr& f) { (*f.ptr)(f.data, std::forward<CArgs>(args)...); }, [&](const function_and_const_data_ptr& f) { (*f.ptr)(f.data, std::forward<CArgs>(args)...); }}, data); } else { return std::visit( overload{ [](std::monostate) -> Ret { terminate_with("small_function called without an implementation"); }, [&](function_ptr f) { return (*f)(std::forward<CArgs>(args)...); }, [&](const function_and_data_ptr& f) { return (*f.ptr)(f.data, std::forward<CArgs>(args)...); }, [&](const function_and_const_data_ptr& f) { return (*f.ptr)(f.data, std::forward<CArgs>(args)...); }}, data); } } constexpr bool empty() const noexcept { return std::holds_alternative<std::monostate>(data); } }; } // namespace snitch // Implementation details. // ----------------------- namespace snitch::impl { struct test_state; using test_ptr = void (*)(); template<typename T, typename F> constexpr test_ptr to_test_case_ptr(const F&) noexcept { return []() { F{}.template operator()<T>(); }; } enum class test_case_state { not_run, success, skipped, failed }; struct test_case { test_id id = {}; test_ptr func = nullptr; test_case_state state = test_case_state::not_run; }; struct section_nesting_level { std::size_t current_section_id = 0; std::size_t previous_section_id = 0; std::size_t max_section_id = 0; }; struct section_state { small_vector<section_id, max_nested_sections> current_section = {}; small_vector<section_nesting_level, max_nested_sections> levels = {}; std::size_t depth = 0; bool leaf_executed = false; }; using capture_state = small_vector<small_string<max_capture_length>, max_captures>; struct test_state { registry& reg; test_case& test; section_state sections = {}; capture_state captures = {}; std::size_t asserts = 0; bool may_fail = false; bool should_fail = false; #if SNITCH_WITH_TIMINGS float duration = 0.0f; #endif }; test_state& get_current_test() noexcept; test_state* try_get_current_test() noexcept; void set_current_test(test_state* current) noexcept; struct section_entry_checker { section_id section = {}; test_state& state; bool entered = false; ~section_entry_checker() noexcept; explicit operator bool() noexcept; }; #define DEFINE_OPERATOR(OP, NAME, DISP, DISP_INV) \ struct operator_##NAME { \ static constexpr std::string_view actual = DISP; \ static constexpr std::string_view inverse = DISP_INV; \ \ template<typename T, typename U> \ constexpr bool operator()(const T& lhs, const U& rhs) const noexcept \ requires requires(const T& lhs, const U& rhs) { lhs OP rhs; } \ { \ return lhs OP rhs; \ } \ } DEFINE_OPERATOR(<, less, " < ", " >= "); DEFINE_OPERATOR(>, greater, " > ", " <= "); DEFINE_OPERATOR(<=, less_equal, " <= ", " > "); DEFINE_OPERATOR(>=, greater_equal, " >= ", " < "); DEFINE_OPERATOR(==, equal, " == ", " != "); DEFINE_OPERATOR(!=, not_equal, " != ", " == "); #undef DEFINE_OPERATOR struct expression { std::string_view expected = {}; small_string<max_expr_length> actual = {}; template<string_appendable T> [[nodiscard]] bool append_value(T&& value) noexcept { return append(actual, std::forward<T>(value)); } template<typename T> [[nodiscard]] bool append_value(T&&) noexcept { return append(actual, "?"); } }; template<bool CheckMode> struct invalid_expression { // This is an invalid expression; any further operator should produce another invalid // expression. We don't want to decompose these operators, but we need to declare them // so the expression compiles until cast to bool. This enable conditional decomposition. #define EXPR_OPERATOR_INVALID(OP) \ template<typename V> \ invalid_expression<CheckMode> operator OP(const V&) noexcept { \ return {}; \ } EXPR_OPERATOR_INVALID(<=) EXPR_OPERATOR_INVALID(<) EXPR_OPERATOR_INVALID(>=) EXPR_OPERATOR_INVALID(>) EXPR_OPERATOR_INVALID(==) EXPR_OPERATOR_INVALID(!=) EXPR_OPERATOR_INVALID(&&) EXPR_OPERATOR_INVALID(||) EXPR_OPERATOR_INVALID(=) EXPR_OPERATOR_INVALID(+=) EXPR_OPERATOR_INVALID(-=) EXPR_OPERATOR_INVALID(*=) EXPR_OPERATOR_INVALID(/=) EXPR_OPERATOR_INVALID(%=) EXPR_OPERATOR_INVALID(^=) EXPR_OPERATOR_INVALID(&=) EXPR_OPERATOR_INVALID(|=) EXPR_OPERATOR_INVALID(<<=) EXPR_OPERATOR_INVALID(>>=) EXPR_OPERATOR_INVALID(^) EXPR_OPERATOR_INVALID(|) EXPR_OPERATOR_INVALID(&) #undef EXPR_OPERATOR_INVALID explicit operator bool() const noexcept requires(!CheckMode) { // This should be unreachable, because we check if an expression is decomposable // before calling the decomposed expression, but just in case: static_assert(CheckMode != CheckMode, "snitch bug: cannot decompose chained expression"); return false; } }; template<bool CheckMode, bool Expected, typename T, typename O, typename U> struct extracted_binary_expression { expression& expr; const T& lhs; const U& rhs; // This is a binary expression; any further operator should produce an invalid // expression, since we can't/won't decompose complex expressions. We don't want to decompose // these operators, but we need to declare them so the expression compiles until cast to bool. // This enable conditional decomposition. #define EXPR_OPERATOR_INVALID(OP) \ template<typename V> \ invalid_expression<CheckMode> operator OP(const V&) noexcept { \ return {}; \ } EXPR_OPERATOR_INVALID(<=) EXPR_OPERATOR_INVALID(<) EXPR_OPERATOR_INVALID(>=) EXPR_OPERATOR_INVALID(>) EXPR_OPERATOR_INVALID(==) EXPR_OPERATOR_INVALID(!=) EXPR_OPERATOR_INVALID(&&) EXPR_OPERATOR_INVALID(||) EXPR_OPERATOR_INVALID(=) EXPR_OPERATOR_INVALID(+=) EXPR_OPERATOR_INVALID(-=) EXPR_OPERATOR_INVALID(*=) EXPR_OPERATOR_INVALID(/=) EXPR_OPERATOR_INVALID(%=) EXPR_OPERATOR_INVALID(^=) EXPR_OPERATOR_INVALID(&=) EXPR_OPERATOR_INVALID(|=) EXPR_OPERATOR_INVALID(<<=) EXPR_OPERATOR_INVALID(>>=) EXPR_OPERATOR_INVALID(^) EXPR_OPERATOR_INVALID(|) EXPR_OPERATOR_INVALID(&) #undef EXPR_OPERATOR_INVALID explicit operator bool() const noexcept requires(!CheckMode || requires(const T& lhs, const U& rhs) { O{}(lhs, rhs); }) { if (O{}(lhs, rhs) != Expected) { if constexpr (matcher_for<T, U>) { using namespace snitch::matchers; constexpr auto status = std::is_same_v<O, operator_equal> == Expected ? match_status::failed : match_status::matched; if (!expr.append_value(lhs.describe_match(rhs, status))) { expr.actual.clear(); } } else if constexpr (matcher_for<U, T>) { using namespace snitch::matchers; constexpr auto status = std::is_same_v<O, operator_equal> == Expected ? match_status::failed : match_status::matched; if (!expr.append_value(rhs.describe_match(lhs, status))) { expr.actual.clear(); } } else { if (!expr.append_value(lhs) || !(Expected ? expr.append_value(O::inverse) : expr.append_value(O::actual)) || !expr.append_value(rhs)) { expr.actual.clear(); } } return true; } return false; } }; template<bool CheckMode, bool Expected, typename T> struct extracted_unary_expression { expression& expr; const T& lhs; // Operators we want to decompose. #define EXPR_OPERATOR(OP, OP_TYPE) \ template<typename U> \ constexpr extracted_binary_expression<CheckMode, Expected, T, OP_TYPE, U> operator OP( \ const U& rhs) const noexcept { \ return {expr, lhs, rhs}; \ } EXPR_OPERATOR(<, operator_less) EXPR_OPERATOR(>, operator_greater) EXPR_OPERATOR(<=, operator_less_equal) EXPR_OPERATOR(>=, operator_greater_equal) EXPR_OPERATOR(==, operator_equal) EXPR_OPERATOR(!=, operator_not_equal) #undef EXPR_OPERATOR // We don't want to decompose the following operators, but we need to declare them so the // expression compiles until cast to bool. This enable conditional decomposition. #define EXPR_OPERATOR_INVALID(OP) \ template<typename V> \ invalid_expression<CheckMode> operator OP(const V&) noexcept { \ return {}; \ } EXPR_OPERATOR_INVALID(&&) EXPR_OPERATOR_INVALID(||) EXPR_OPERATOR_INVALID(=) EXPR_OPERATOR_INVALID(+=) EXPR_OPERATOR_INVALID(-=) EXPR_OPERATOR_INVALID(*=) EXPR_OPERATOR_INVALID(/=) EXPR_OPERATOR_INVALID(%=) EXPR_OPERATOR_INVALID(^=) EXPR_OPERATOR_INVALID(&=) EXPR_OPERATOR_INVALID(|=) EXPR_OPERATOR_INVALID(<<=) EXPR_OPERATOR_INVALID(>>=) EXPR_OPERATOR_INVALID(^) EXPR_OPERATOR_INVALID(|) EXPR_OPERATOR_INVALID(&) #undef EXPR_OPERATOR_INVALID explicit operator bool() const noexcept requires(!CheckMode || requires(const T& lhs) { static_cast<bool>(lhs); }) { if (static_cast<bool>(lhs) != Expected) { if (!expr.append_value(lhs)) { expr.actual.clear(); } return true; } return false; } }; template<bool CheckMode, bool Expected> struct expression_extractor { expression& expr; template<typename T> constexpr extracted_unary_expression<CheckMode, Expected, T> operator<=(const T& lhs) const noexcept { return {expr, lhs}; } }; template<typename T> constexpr bool is_decomposable = requires(const T& t) { static_cast<bool>(t); }; struct scoped_capture { capture_state& captures; std::size_t count = 0; ~scoped_capture() noexcept { captures.resize(captures.size() - count); } }; std::string_view extract_next_name(std::string_view& names) noexcept; small_string<max_capture_length>& add_capture(test_state& state) noexcept; template<string_appendable T> void add_capture(test_state& state, std::string_view& names, const T& arg) noexcept { auto& capture = add_capture(state); append_or_truncate(capture, extract_next_name(names), " := ", arg); } template<string_appendable... Args> scoped_capture add_captures(test_state& state, std::string_view names, const Args&... args) noexcept { (add_capture(state, names, args), ...); return {state.captures, sizeof...(args)}; } template<string_appendable... Args> scoped_capture add_info(test_state& state, const Args&... args) noexcept { auto& capture = add_capture(state); append_or_truncate(capture, args...); return {state.captures, 1}; } void stdout_print(std::string_view message) noexcept; struct abort_exception {}; template<typename T> concept exception_with_what = requires(const T& e) { { e.what() } -> convertible_to<std::string_view>; }; } // namespace snitch::impl // Sections and captures. // --------- namespace snitch { using section_info = small_vector_span<const section_id>; using capture_info = small_vector_span<const std::string_view>; } // namespace snitch // Events. // ------- namespace snitch { struct assertion_location { std::string_view file = {}; std::size_t line = 0u; }; enum class test_case_state { success, failed, skipped }; namespace event { struct test_run_started { std::string_view name = {}; }; struct test_run_ended { std::string_view name = {}; bool success = true; std::size_t run_count = 0; std::size_t fail_count = 0; std::size_t skip_count = 0; std::size_t assertion_count = 0; #if SNITCH_WITH_TIMINGS float duration = 0.0f; #endif }; struct test_case_started { const test_id& id; }; struct test_case_ended { const test_id& id; test_case_state state = test_case_state::success; std::size_t assertion_count = 0; #if SNITCH_WITH_TIMINGS float duration = 0.0f; #endif }; struct assertion_failed { const test_id& id; section_info sections = {}; capture_info captures = {}; const assertion_location& location; std::string_view message = {}; bool expected = false; bool allowed = false; }; struct test_case_skipped { const test_id& id; section_info sections = {}; capture_info captures = {}; const assertion_location& location; std::string_view message = {}; }; using data = std::variant< test_run_started, test_run_ended, test_case_started, test_case_ended, assertion_failed, test_case_skipped>; }; // namespace event } // namespace snitch // Command line interface. // ----------------------- namespace snitch::cli { struct argument { std::string_view name = {}; std::optional<std::string_view> value_name = {}; std::optional<std::string_view> value = {}; }; struct input { std::string_view executable = {}; small_vector<argument, max_command_line_args> arguments = {}; }; extern small_function<void(std::string_view) noexcept> console_print; std::optional<input> parse_arguments(int argc, const char* const argv[]) noexcept; std::optional<cli::argument> get_option(const cli::input& args, std::string_view name) noexcept; std::optional<cli::argument> get_positional_argument(const cli::input& args, std::string_view name) noexcept; void for_each_positional_argument( const cli::input& args, std::string_view name, const small_function<void(std::string_view) noexcept>& callback) noexcept; } // namespace snitch::cli // Test registry. // -------------- namespace snitch { class registry { small_vector<impl::test_case, max_test_cases> test_list; void print_location( const impl::test_case& current_case, const impl::section_state& sections, const impl::capture_state& captures, const assertion_location& location) const noexcept; void print_failure() const noexcept; void print_expected_failure() const noexcept; void print_skip() const noexcept; void print_details(std::string_view message) const noexcept; void print_details_expr(const impl::expression& exp) const noexcept; public: enum class verbosity { quiet, normal, high } verbose = verbosity::normal; bool with_color = true; using print_function = small_function<void(std::string_view) noexcept>; using report_function = small_function<void(const registry&, const event::data&) noexcept>; print_function print_callback = &snitch::impl::stdout_print; report_function report_callback; template<typename... Args> void print(Args&&... args) const noexcept { small_string<max_message_length> message; append_or_truncate(message, std::forward<Args>(args)...); this->print_callback(message); } const char* add(const test_id& id, impl::test_ptr func) noexcept; template<typename... Args, typename F> const char* add_with_types(std::string_view name, std::string_view tags, const F& func) noexcept { return ( add({name, tags, impl::get_type_name<Args>()}, impl::to_test_case_ptr<Args>(func)), ...); } template<typename T, typename F> const char* add_with_type_list(std::string_view name, std::string_view tags, const F& func) noexcept { return [&]<template<typename...> typename TL, typename... Args>(type_list<TL<Args...>>) { return this->add_with_types<Args...>(name, tags, func); }(type_list<T>{}); } void report_failure( impl::test_state& state, const assertion_location& location, std::string_view message) const noexcept; void report_failure( impl::test_state& state, const assertion_location& location, std::string_view message1, std::string_view message2) const noexcept; void report_failure( impl::test_state& state, const assertion_location& location, const impl::expression& exp) const noexcept; void report_skipped( impl::test_state& state, const assertion_location& location, std::string_view message) const noexcept; impl::test_state run(impl::test_case& test) noexcept; bool run_tests(std::string_view run_name) noexcept; bool run_selected_tests( std::string_view run_name, const small_function<bool(const test_id&) noexcept>& filter) noexcept; bool run_tests(const cli::input& args) noexcept; void configure(const cli::input& args) noexcept; void list_all_tests() const noexcept; void list_all_tags() const noexcept; void list_tests_with_tag(std::string_view tag) const noexcept; impl::test_case* begin() noexcept; impl::test_case* end() noexcept; const impl::test_case* begin() const noexcept; const impl::test_case* end() const noexcept; }; extern constinit registry tests; } // namespace snitch // Matchers. // --------- namespace snitch::matchers { struct contains_substring { std::string_view substring_pattern; explicit contains_substring(std::string_view pattern) noexcept; bool match(std::string_view message) const noexcept; small_string<max_message_length> describe_match(std::string_view message, match_status status) const noexcept; }; template<typename T, std::size_t N> struct is_any_of { small_vector<T, N> list; template<typename... Args> explicit is_any_of(const Args&... args) noexcept : list({args...}) {} bool match(const T& value) const noexcept { for (const auto& v : list) { if (v == value) { return true; } } return false; } small_string<max_message_length> describe_match(const T& value, match_status status) const noexcept { small_string<max_message_length> description_buffer; append_or_truncate( description_buffer, "'", value, "' was ", (status == match_status::failed ? "not " : ""), "found in {"); bool first = true; for (const auto& v : list) { if (!first) { append_or_truncate(description_buffer, ", '", v, "'"); } else { append_or_truncate(description_buffer, "'", v, "'"); } first = false; } append_or_truncate(description_buffer, "}"); return description_buffer; } }; template<typename T, typename... Args> is_any_of(T, Args...) -> is_any_of<T, sizeof...(Args) + 1>; struct with_what_contains : private contains_substring { explicit with_what_contains(std::string_view pattern) noexcept; template<snitch::impl::exception_with_what E> bool match(const E& e) const noexcept { return contains_substring::match(e.what()); } template<snitch::impl::exception_with_what E> small_string<max_message_length> describe_match(const E& e, match_status status) const noexcept { return contains_substring::describe_match(e.what(), status); } }; template<typename T, matcher_for<T> M> bool operator==(const T& value, const M& m) noexcept { return m.match(value); } template<typename T, matcher_for<T> M> bool operator==(const M& m, const T& value) noexcept { return m.match(value); } } // namespace snitch::matchers // Compiler warning handling. // -------------------------- // clang-format off #if defined(__clang__) # define SNITCH_WARNING_PUSH _Pragma("clang diagnostic push") # define SNITCH_WARNING_POP _Pragma("clang diagnostic pop") # define SNITCH_WARNING_DISABLE_PARENTHESES _Pragma("clang diagnostic ignored \"-Wparentheses\"") # define SNITCH_WARNING_DISABLE_CONSTANT_COMPARISON #elif defined(__GNUC__) # define SNITCH_WARNING_PUSH _Pragma("GCC diagnostic push") # define SNITCH_WARNING_POP _Pragma("GCC diagnostic pop") # define SNITCH_WARNING_DISABLE_PARENTHESES _Pragma("GCC diagnostic ignored \"-Wparentheses\"") # define SNITCH_WARNING_DISABLE_CONSTANT_COMPARISON #elif defined(_MSC_VER) # define SNITCH_WARNING_PUSH _Pragma("warning(push)") # define SNITCH_WARNING_POP _Pragma("warning(pop)") # define SNITCH_WARNING_DISABLE_PARENTHESES # define SNITCH_WARNING_DISABLE_CONSTANT_COMPARISON _Pragma("warning(disable: 4127)") #else # define SNITCH_WARNING_PUSH # define SNITCH_WARNING_POP # define SNITCH_WARNING_DISABLE_PARENTHESES # define SNITCH_WARNING_DISABLE_CONSTANT_COMPARISON #endif // clang-format on // Internal test macros. // --------------------- #if SNITCH_WITH_EXCEPTIONS # define SNITCH_TESTING_ABORT \ throw snitch::impl::abort_exception {} #else # define SNITCH_TESTING_ABORT std::terminate() #endif #define SNITCH_CONCAT_IMPL(x, y) x##y #define SNITCH_MACRO_CONCAT(x, y) SNITCH_CONCAT_IMPL(x, y) #define SNITCH_EXPR_TRUE(TYPE, EXP) \ auto SNITCH_CURRENT_EXPRESSION = snitch::impl::expression{TYPE "(" #EXP ")"}; \ snitch::impl::expression_extractor<false, true>{SNITCH_CURRENT_EXPRESSION} <= EXP #define SNITCH_EXPR_FALSE(TYPE, EXP) \ auto SNITCH_CURRENT_EXPRESSION = snitch::impl::expression{TYPE "(" #EXP ")"}; \ snitch::impl::expression_extractor<false, false>{SNITCH_CURRENT_EXPRESSION} <= EXP #define SNITCH_DECOMPOSABLE(EXP) \ snitch::impl::is_decomposable< \ decltype(snitch::impl::expression_extractor<true, true>{std::declval<snitch::impl::expression&>()} <= EXP)> // Public test macros: test cases. // ------------------------------- #define SNITCH_TEST_CASE_IMPL(ID, ...) \ static void ID(); \ static const char* SNITCH_MACRO_CONCAT(test_id_, __COUNTER__) [[maybe_unused]] = \ snitch::tests.add({__VA_ARGS__}, &ID); \ void ID() #define SNITCH_TEST_CASE(...) \ SNITCH_TEST_CASE_IMPL(SNITCH_MACRO_CONCAT(test_fun_, __COUNTER__), __VA_ARGS__) #define SNITCH_TEMPLATE_LIST_TEST_CASE_IMPL(ID, NAME, TAGS, TYPES) \ template<typename TestType> \ static void ID(); \ static const char* SNITCH_MACRO_CONCAT(test_id_, __COUNTER__) [[maybe_unused]] = \ snitch::tests.add_with_type_list<TYPES>( \ NAME, TAGS, []<typename TestType>() { ID<TestType>(); }); \ template<typename TestType> \ void ID() #define SNITCH_TEMPLATE_LIST_TEST_CASE(NAME, TAGS, TYPES) \ SNITCH_TEMPLATE_LIST_TEST_CASE_IMPL( \ SNITCH_MACRO_CONCAT(test_fun_, __COUNTER__), NAME, TAGS, TYPES) #define SNITCH_TEMPLATE_TEST_CASE_IMPL(ID, NAME, TAGS, ...) \ template<typename TestType> \ static void ID(); \ static const char* SNITCH_MACRO_CONCAT(test_id_, __COUNTER__) [[maybe_unused]] = \ snitch::tests.add_with_types<__VA_ARGS__>( \ NAME, TAGS, []<typename TestType>() { ID<TestType>(); }); \ template<typename TestType> \ void ID() #define SNITCH_TEMPLATE_TEST_CASE(NAME, TAGS, ...) \ SNITCH_TEMPLATE_TEST_CASE_IMPL( \ SNITCH_MACRO_CONCAT(test_fun_, __COUNTER__), NAME, TAGS, __VA_ARGS__) #define SNITCH_TEST_CASE_METHOD_IMPL(ID, FIXTURE, ...) \ namespace { \ struct ID : FIXTURE { \ void test_fun(); \ }; \ } \ static const char* SNITCH_MACRO_CONCAT(test_id_, __COUNTER__) [[maybe_unused]] = \ snitch::tests.add({__VA_ARGS__}, []() { ID{}.test_fun(); }); \ void ID::test_fun() #define SNITCH_TEST_CASE_METHOD(FIXTURE, ...) \ SNITCH_TEST_CASE_METHOD_IMPL( \ SNITCH_MACRO_CONCAT(test_fixture_, __COUNTER__), FIXTURE, __VA_ARGS__) #define SNITCH_TEMPLATE_LIST_TEST_CASE_METHOD_IMPL(ID, FIXTURE, NAME, TAGS, TYPES) \ namespace { \ template<typename TestType> \ struct ID : FIXTURE<TestType> { \ void test_fun(); \ }; \ } \ static const char* SNITCH_MACRO_CONCAT(test_id_, __COUNTER__) [[maybe_unused]] = \ snitch::tests.add_with_types<TYPES>( \ NAME, TAGS, []() < typename TestType > { ID<TestType>{}.test_fun(); }); \ template<typename TestType> \ void ID<TestType>::test_fun() #define SNITCH_TEMPLATE_LIST_TEST_CASE_METHOD(FIXTURE, NAME, TAGS, TYPES) \ SNITCH_TEMPLATE_LIST_TEST_CASE_METHOD_IMPL( \ SNITCH_MACRO_CONCAT(test_fixture_, __COUNTER__), FIXTURE, NAME, TAGS, TYPES) #define SNITCH_TEMPLATE_TEST_CASE_METHOD_IMPL(ID, FIXTURE, NAME, TAGS, ...) \ namespace { \ template<typename TestType> \ struct ID : FIXTURE<TestType> { \ void test_fun(); \ }; \ } \ static const char* SNITCH_MACRO_CONCAT(test_id_, __COUNTER__) [[maybe_unused]] = \ snitch::tests.add_with_types<__VA_ARGS__>( \ NAME, TAGS, []() < typename TestType > { ID<TestType>{}.test_fun(); }); \ template<typename TestType> \ void ID<TestType>::test_fun() #define SNITCH_TEMPLATE_TEST_CASE_METHOD(FIXTURE, NAME, TAGS, ...) \ SNITCH_TEMPLATE_TEST_CASE_METHOD_IMPL( \ SNITCH_MACRO_CONCAT(test_fixture_, __COUNTER__), FIXTURE, NAME, TAGS, __VA_ARGS__) // Public test macros: utilities. // ------------------------------ #define SNITCH_SECTION(...) \ if (snitch::impl::section_entry_checker SNITCH_MACRO_CONCAT(section_id_, __COUNTER__){ \ {__VA_ARGS__}, snitch::impl::get_current_test()}) #define SNITCH_CAPTURE(...) \ auto SNITCH_MACRO_CONCAT(capture_id_, __COUNTER__) = \ snitch::impl::add_captures(snitch::impl::get_current_test(), #__VA_ARGS__, __VA_ARGS__) #define SNITCH_INFO(...) \ auto SNITCH_MACRO_CONCAT(capture_id_, __COUNTER__) = \ snitch::impl::add_info(snitch::impl::get_current_test(), __VA_ARGS__) // Public test macros: checks. // ------------------------------ #define SNITCH_REQUIRE(EXP) \ do { \ auto& SNITCH_CURRENT_TEST = snitch::impl::get_current_test(); \ ++SNITCH_CURRENT_TEST.asserts; \ SNITCH_WARNING_PUSH \ SNITCH_WARNING_DISABLE_PARENTHESES \ SNITCH_WARNING_DISABLE_CONSTANT_COMPARISON \ if constexpr (SNITCH_DECOMPOSABLE(EXP)) { \ if (SNITCH_EXPR_TRUE("REQUIRE", EXP)) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, SNITCH_CURRENT_EXPRESSION); \ SNITCH_TESTING_ABORT; \ } \ } else { \ if (!(EXP)) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, "REQUIRE(" #EXP ")"); \ SNITCH_TESTING_ABORT; \ } \ } \ SNITCH_WARNING_POP \ } while (0) #define SNITCH_CHECK(EXP) \ do { \ auto& SNITCH_CURRENT_TEST = snitch::impl::get_current_test(); \ ++SNITCH_CURRENT_TEST.asserts; \ SNITCH_WARNING_PUSH \ SNITCH_WARNING_DISABLE_PARENTHESES \ SNITCH_WARNING_DISABLE_CONSTANT_COMPARISON \ if constexpr (SNITCH_DECOMPOSABLE(EXP)) { \ if (SNITCH_EXPR_TRUE("CHECK", EXP)) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, SNITCH_CURRENT_EXPRESSION); \ } \ } else { \ if (!(EXP)) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, "CHECK(" #EXP ")"); \ } \ } \ SNITCH_WARNING_POP \ } while (0) #define SNITCH_REQUIRE_FALSE(EXP) \ do { \ auto& SNITCH_CURRENT_TEST = snitch::impl::get_current_test(); \ ++SNITCH_CURRENT_TEST.asserts; \ SNITCH_WARNING_PUSH \ SNITCH_WARNING_DISABLE_PARENTHESES \ SNITCH_WARNING_DISABLE_CONSTANT_COMPARISON \ if constexpr (SNITCH_DECOMPOSABLE(EXP)) { \ if (SNITCH_EXPR_FALSE("REQUIRE_FALSE", EXP)) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, SNITCH_CURRENT_EXPRESSION); \ SNITCH_TESTING_ABORT; \ } \ } else { \ if (!(EXP)) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, "REQUIRE_FALSE(" #EXP ")"); \ SNITCH_TESTING_ABORT; \ } \ } \ SNITCH_WARNING_POP \ } while (0) #define SNITCH_CHECK_FALSE(EXP) \ do { \ auto& SNITCH_CURRENT_TEST = snitch::impl::get_current_test(); \ ++SNITCH_CURRENT_TEST.asserts; \ SNITCH_WARNING_PUSH \ SNITCH_WARNING_DISABLE_PARENTHESES \ SNITCH_WARNING_DISABLE_CONSTANT_COMPARISON \ if constexpr (SNITCH_DECOMPOSABLE(EXP)) { \ if (SNITCH_EXPR_FALSE("CHECK_FALSE", EXP)) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, SNITCH_CURRENT_EXPRESSION); \ } \ } else { \ if (!(EXP)) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, "CHECK_FALSE(" #EXP ")"); \ } \ } \ SNITCH_WARNING_POP \ } while (0) #define SNITCH_FAIL(MESSAGE) \ do { \ auto& SNITCH_CURRENT_TEST = snitch::impl::get_current_test(); \ ++SNITCH_CURRENT_TEST.asserts; \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, (MESSAGE)); \ SNITCH_TESTING_ABORT; \ } while (0) #define SNITCH_FAIL_CHECK(MESSAGE) \ do { \ auto& SNITCH_CURRENT_TEST = snitch::impl::get_current_test(); \ ++SNITCH_CURRENT_TEST.asserts; \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, (MESSAGE)); \ } while (0) #define SNITCH_SKIP(MESSAGE) \ do { \ auto& SNITCH_CURRENT_TEST = snitch::impl::get_current_test(); \ SNITCH_CURRENT_TEST.reg.report_skipped( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, (MESSAGE)); \ SNITCH_TESTING_ABORT; \ } while (0) #define SNITCH_REQUIRE_THAT(EXPR, MATCHER) \ do { \ auto& SNITCH_CURRENT_TEST = snitch::impl::get_current_test(); \ ++SNITCH_CURRENT_TEST.asserts; \ const auto& SNITCH_TEMP_VALUE = (EXPR); \ const auto& SNITCH_TEMP_MATCHER = (MATCHER); \ if (!SNITCH_TEMP_MATCHER.match(SNITCH_TEMP_VALUE)) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ SNITCH_TEMP_MATCHER.describe_fail(SNITCH_TEMP_VALUE)); \ SNITCH_TESTING_ABORT; \ } \ } while (0) #define SNITCH_CHECK_THAT(EXPR, MATCHER) \ do { \ auto& SNITCH_CURRENT_TEST = snitch::impl::get_current_test(); \ ++SNITCH_CURRENT_TEST.asserts; \ const auto& SNITCH_TEMP_VALUE = (EXPR); \ const auto& SNITCH_TEMP_MATCHER = (MATCHER); \ if (!SNITCH_TEMP_MATCHER.match(SNITCH_TEMP_VALUE)) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ SNITCH_TEMP_MATCHER.describe_fail(SNITCH_TEMP_VALUE)); \ } \ } while (0) // clang-format off #if SNITCH_WITH_SHORTHAND_MACROS # define TEST_CASE(NAME, ...) SNITCH_TEST_CASE(NAME, __VA_ARGS__) # define TEMPLATE_LIST_TEST_CASE(NAME, TAGS, TYPES) SNITCH_TEMPLATE_LIST_TEST_CASE(NAME, TAGS, TYPES) # define TEMPLATE_TEST_CASE(NAME, TAGS, ...) SNITCH_TEMPLATE_TEST_CASE(NAME, TAGS, __VA_ARGS__) # define TEST_CASE_METHOD(FIXTURE, NAME, ...) SNITCH_TEST_CASE_METHOD(FIXTURE, NAME, __VA_ARGS__) # define TEMPLATE_LIST_TEST_CASE_METHOD(FIXTURE, NAME, TAGS, TYPES) SNITCH_TEMPLATE_LIST_TEST_CASE_METHOD(FIXTURE, NAME, TAGS, TYPES) # define TEMPLATE_TEST_CASE_METHOD(FIXTURE, NAME, TAGS, ...) SNITCH_TEMPLATE_TEST_CASE_METHOD(FIXTURE, NAME, TAGS, __VA_ARGS__) # define SECTION(NAME, ...) SNITCH_SECTION(NAME, __VA_ARGS__) # define CAPTURE(...) SNITCH_CAPTURE(__VA_ARGS__) # define INFO(...) SNITCH_INFO(__VA_ARGS__) # define REQUIRE(EXP) SNITCH_REQUIRE(EXP) # define CHECK(EXP) SNITCH_CHECK(EXP) # define REQUIRE_FALSE(EXP) SNITCH_REQUIRE_FALSE(EXP) # define CHECK_FALSE(EXP) SNITCH_CHECK_FALSE(EXP) # define FAIL(MESSAGE) SNITCH_FAIL(MESSAGE) # define FAIL_CHECK(MESSAGE) SNITCH_FAIL_CHECK(MESSAGE) # define SKIP(MESSAGE) SNITCH_SKIP(MESSAGE) # define REQUIRE_THAT(EXP, MATCHER) SNITCH_REQUIRE(EXP, MATCHER) # define CHECK_THAT(EXP, MATCHER) SNITCH_CHECK(EXP, MATCHER) #endif // clang-format on #if SNITCH_WITH_EXCEPTIONS # define SNITCH_REQUIRE_THROWS_AS(EXPRESSION, EXCEPTION) \ do { \ auto& SNITCH_CURRENT_TEST = snitch::impl::get_current_test(); \ try { \ ++SNITCH_CURRENT_TEST.asserts; \ EXPRESSION; \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ #EXCEPTION " expected but no exception thrown"); \ SNITCH_TESTING_ABORT; \ } catch (const EXCEPTION&) { \ /* success */ \ } catch (...) { \ try { \ throw; \ } catch (const std::exception& e) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ #EXCEPTION " expected but other std::exception thrown; message: ", \ e.what()); \ } catch (...) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ #EXCEPTION " expected but other unknown exception thrown"); \ } \ SNITCH_TESTING_ABORT; \ } \ } while (0) # define SNITCH_CHECK_THROWS_AS(EXPRESSION, EXCEPTION) \ do { \ auto& SNITCH_CURRENT_TEST = snitch::impl::get_current_test(); \ try { \ ++SNITCH_CURRENT_TEST.asserts; \ EXPRESSION; \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ #EXCEPTION " expected but no exception thrown"); \ } catch (const EXCEPTION&) { \ /* success */ \ } catch (...) { \ try { \ throw; \ } catch (const std::exception& e) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ #EXCEPTION " expected but other std::exception thrown; message: ", \ e.what()); \ } catch (...) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ #EXCEPTION " expected but other unknown exception thrown"); \ } \ } \ } while (0) # define SNITCH_REQUIRE_THROWS_MATCHES(EXPRESSION, EXCEPTION, MATCHER) \ do { \ auto& SNITCH_CURRENT_TEST = snitch::impl::get_current_test(); \ try { \ ++SNITCH_CURRENT_TEST.asserts; \ EXPRESSION; \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ #EXCEPTION " expected but no exception thrown"); \ SNITCH_TESTING_ABORT; \ } catch (const EXCEPTION& e) { \ if (!(MATCHER).match(e)) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ "could not match caught " #EXCEPTION " with expected content: ", \ (MATCHER).describe_match(e, snitch::matchers::match_status::failed)); \ SNITCH_TESTING_ABORT; \ } \ } catch (...) { \ try { \ throw; \ } catch (const std::exception& e) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ #EXCEPTION " expected but other std::exception thrown; message: ", \ e.what()); \ } catch (...) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ #EXCEPTION " expected but other unknown exception thrown"); \ } \ SNITCH_TESTING_ABORT; \ } \ } while (0) # define SNITCH_CHECK_THROWS_MATCHES(EXPRESSION, EXCEPTION, MATCHER) \ do { \ auto& SNITCH_CURRENT_TEST = snitch::impl::get_current_test(); \ try { \ ++SNITCH_CURRENT_TEST.asserts; \ EXPRESSION; \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ #EXCEPTION " expected but no exception thrown"); \ } catch (const EXCEPTION& e) { \ if (!(MATCHER).match(e)) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ "could not match caught " #EXCEPTION " with expected content: ", \ (MATCHER).describe_match(e, snitch::matchers::match_status::failed)); \ } \ } catch (...) { \ try { \ throw; \ } catch (const std::exception& e) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ #EXCEPTION " expected but other std::exception thrown; message: ", \ e.what()); \ } catch (...) { \ SNITCH_CURRENT_TEST.reg.report_failure( \ SNITCH_CURRENT_TEST, {__FILE__, __LINE__}, \ #EXCEPTION " expected but other unknown exception thrown"); \ } \ } \ } while (0) // clang-format off #if SNITCH_WITH_SHORTHAND_MACROS # define REQUIRE_THROWS_AS(EXPRESSION, EXCEPTION) SNITCH_REQUIRE_THROWS_AS(EXPRESSION, EXCEPTION) # define CHECK_THROWS_AS(EXPRESSION, EXCEPTION) SNITCH_CHECK_THROWS_AS(EXPRESSION, EXCEPTION) # define REQUIRE_THROWS_MATCHES(EXPRESSION, EXCEPTION, MATCHER) SNITCH_REQUIRE_THROWS_MATCHES(EXPRESSION, EXCEPTION, MATCHER) # define CHECK_THROWS_MATCHES(EXPRESSION, EXCEPTION, MATCHER) SNITCH_CHECK_THROWS_MATCHES(EXPRESSION, EXCEPTION, MATCHER) #endif // clang-format on #endif #endif #if defined(SNITCH_IMPLEMENTATION) #include <algorithm> // for std::sort #include <cstdio> // for std::printf, std::snprintf #include <cstring> // for std::memcpy #include <optional> // for std::optional #if SNITCH_WITH_TIMINGS # include <chrono> // for measuring test time #endif // Testing framework implementation utilities. // ------------------------------------------- namespace { using namespace std::literals; using color_t = const char*; namespace color { constexpr color_t error [[maybe_unused]] = "\x1b[1;31m"; constexpr color_t warning [[maybe_unused]] = "\x1b[1;33m"; constexpr color_t status [[maybe_unused]] = "\x1b[1;36m"; constexpr color_t fail [[maybe_unused]] = "\x1b[1;31m"; constexpr color_t skipped [[maybe_unused]] = "\x1b[1;33m"; constexpr color_t pass [[maybe_unused]] = "\x1b[1;32m"; constexpr color_t highlight1 [[maybe_unused]] = "\x1b[1;35m"; constexpr color_t highlight2 [[maybe_unused]] = "\x1b[1;36m"; constexpr color_t reset [[maybe_unused]] = "\x1b[0m"; } // namespace color template<typename T> struct colored { const T& value; color_t color_start; color_t color_end; }; template<typename T> colored<T> make_colored(const T& t, bool with_color, color_t start) { return {t, with_color ? start : "", with_color ? color::reset : ""}; } thread_local snitch::impl::test_state* thread_current_test = nullptr; } // namespace namespace { using snitch::small_string_span; template<typename T> constexpr const char* get_format_code() noexcept { if constexpr (std::is_same_v<T, const void*>) { return "%p"; } else if constexpr (std::is_same_v<T, std::size_t>) { return "%zu"; } else if constexpr (std::is_same_v<T, std::ptrdiff_t>) { return "%td"; } else if constexpr (std::is_same_v<T, float>) { return "%f"; } else if constexpr (std::is_same_v<T, double>) { return "%lf"; } else { static_assert(std::is_same_v<T, T>, "unsupported type"); } } template<typename T> bool append_fmt(small_string_span ss, T value) noexcept { if (ss.available() <= 1) { // snprintf will always print a null-terminating character, // so abort early if only space for one or zero character, as // this would clobber the original string. return false; } // Calculate required length. const int return_code = std::snprintf(nullptr, 0, get_format_code<T>(), value); if (return_code < 0) { return false; } // 'return_code' holds the number of characters that are required, // excluding the null-terminating character, which always gets appended, // so we need to +1. const std::size_t length = static_cast<std::size_t>(return_code) + 1; const bool could_fit = length <= ss.available(); const std::size_t offset = ss.size(); const std::size_t prev_space = ss.available(); ss.resize(std::min(ss.size() + length, ss.capacity())); std::snprintf(ss.begin() + offset, prev_space, get_format_code<T>(), value); // Pop the null-terminating character, always printed unfortunately. ss.pop_back(); return could_fit; } } // namespace namespace snitch { bool append(small_string_span ss, std::string_view str) noexcept { if (str.empty()) { return true; } const bool could_fit = str.size() <= ss.available(); const std::size_t copy_count = std::min(str.size(), ss.available()); const std::size_t offset = ss.size(); ss.grow(copy_count); std::memmove(ss.begin() + offset, str.data(), copy_count); return could_fit; } bool append(small_string_span ss, const void* ptr) noexcept { return append_fmt(ss, ptr); } bool append(small_string_span ss, std::nullptr_t) noexcept { return append(ss, "nullptr"); } bool append(small_string_span ss, std::size_t i) noexcept { return append_fmt(ss, i); } bool append(small_string_span ss, std::ptrdiff_t i) noexcept { return append_fmt(ss, i); } bool append(small_string_span ss, float f) noexcept { return append_fmt(ss, f); } bool append(small_string_span ss, double d) noexcept { return append_fmt(ss, d); } bool append(small_string_span ss, bool value) noexcept { return append(ss, value ? "true" : "false"); } void truncate_end(small_string_span ss) noexcept { std::size_t num_dots = 3; std::size_t final_length = std::min(ss.capacity(), ss.size() + num_dots); std::size_t offset = final_length >= num_dots ? final_length - num_dots : 0; num_dots = final_length - offset; ss.resize(final_length); std::memcpy(ss.begin() + offset, "...", num_dots); } bool replace_all( small_string_span string, std::string_view pattern, std::string_view replacement) noexcept { if (replacement.size() == pattern.size()) { std::string_view sv(string.begin(), string.size()); auto pos = sv.find(pattern); while (pos != sv.npos) { // Replace pattern by replacement std::memcpy(string.data() + pos, replacement.data(), replacement.size()); pos += replacement.size(); // Find next occurrence pos = sv.find(pattern, pos); } return true; } else if (replacement.size() < pattern.size()) { const std::size_t char_diff = pattern.size() - replacement.size(); std::string_view sv(string.begin(), string.size()); auto pos = sv.find(pattern); while (pos != sv.npos) { // Shift data after the replacement to the left to fill the gap std::rotate(string.begin() + pos, string.begin() + pos + char_diff, string.end()); string.resize(string.size() - char_diff); // Replace pattern by replacement std::memcpy(string.data() + pos, replacement.data(), replacement.size()); pos += replacement.size(); // Find next occurrence sv = {string.begin(), string.size()}; pos = sv.find(pattern, pos); } return true; } else { const std::size_t char_diff = replacement.size() - pattern.size(); std::string_view sv(string.begin(), string.size()); auto pos = sv.find(pattern); bool overflow = false; while (pos != sv.npos) { // Shift data after the pattern to the right to make room for the replacement const std::size_t char_growth = std::min(char_diff, string.available()); if (char_growth != char_diff) { overflow = true; } string.grow(char_growth); if (char_diff <= string.size() && string.size() - char_diff > pos) { std::rotate(string.begin() + pos, string.end() - char_diff, string.end()); } // Replace pattern by replacement const std::size_t max_chars = std::min(replacement.size(), string.size() - pos); std::memcpy(string.data() + pos, replacement.data(), max_chars); pos += max_chars; // Find next occurrence sv = {string.begin(), string.size()}; pos = sv.find(pattern, pos); } return !overflow; } } bool is_match(std::string_view string, std::string_view regex) noexcept { // An empty regex matches any string; early exit. // An empty string matches an empty regex (exit here) or any regex containing // only wildcards (exit later). if ((string.empty() && regex.empty()) || regex.empty()) { return true; } const std::size_t regex_size = regex.size(); const std::size_t string_size = string.size(); // Iterate characters of the regex string and exit at first non-match. std::size_t js = 0; for (std::size_t jr = 0; jr < regex_size; ++jr, ++js) { bool escaped = false; if (regex[jr] == '\\') { // Escaped character, look ahead ignoring special characters. ++jr; if (jr >= regex_size) { // Nothing left to escape; the regex is ill-formed. return false; } escaped = true; } if (!escaped && regex[jr] == '*') { // Wildcard is found; if this is the last character of the regex // then any further content will be a match; early exit. if (jr == regex_size - 1) { return true; } // Discard what has already been matched. regex = regex.substr(jr + 1); // If there are no more characters in the string after discarding, then we only match if // the regex contains only wildcards from there on. const std::size_t remaining = string_size >= js ? string_size - js : 0u; if (remaining == 0u) { return regex.find_first_not_of('*') == regex.npos; } // Otherwise, we loop over all remaining characters of the string and look // for a match when starting from each of them. for (std::size_t o = 0; o < remaining; ++o) { if (is_match(string.substr(js + o), regex)) { return true; } } return false; } else if (js >= string_size || regex[jr] != string[js]) { // Regular character is found; not a match if not an exact match in the string. return false; } } // We have finished reading the regex string and did not find either a definite non-match // or a definite match. This means we did not have any wildcard left, hence that we need // an exact match. Therefore, only match if the string size is the same as the regex. return js == string_size; } } // namespace snitch namespace snitch::impl { void stdout_print(std::string_view message) noexcept { // TODO: replace this with std::print? std::printf("%.*s", static_cast<int>(message.length()), message.data()); } test_state& get_current_test() noexcept { test_state* current = thread_current_test; if (current == nullptr) { terminate_with("no test case is currently running on this thread"); } return *current; } test_state* try_get_current_test() noexcept { return thread_current_test; } void set_current_test(test_state* current) noexcept { thread_current_test = current; } } // namespace snitch::impl namespace snitch::cli { small_function<void(std::string_view) noexcept> console_print = &snitch::impl::stdout_print; } // namespace snitch::cli namespace { using snitch::max_message_length; using snitch::small_string; template<typename T> bool append(small_string_span ss, const colored<T>& colored_value) noexcept { return append(ss, colored_value.color_start, colored_value.value, colored_value.color_end); } template<typename... Args> void console_print(Args&&... args) noexcept { small_string<max_message_length> message; append_or_truncate(message, std::forward<Args>(args)...); snitch::cli::console_print(message); } bool is_at_least(snitch::registry::verbosity verbose, snitch::registry::verbosity required) { using underlying_type = std::underlying_type_t<snitch::registry::verbosity>; return static_cast<underlying_type>(verbose) >= static_cast<underlying_type>(required); } void trim(std::string_view& str, std::string_view patterns) noexcept { std::size_t start = str.find_first_not_of(patterns); if (start == str.npos) return; str.remove_prefix(start); std::size_t end = str.find_last_not_of(patterns); if (end != str.npos) str.remove_suffix(str.size() - end - 1); } } // namespace namespace snitch { [[noreturn]] void terminate_with(std::string_view msg) noexcept { impl::stdout_print("terminate called with message: "); impl::stdout_print(msg); impl::stdout_print("\n"); std::terminate(); } } // namespace snitch // Sections implementation. // ------------------------ namespace snitch::impl { section_entry_checker::~section_entry_checker() noexcept { if (entered) { if (state.sections.levels.size() == state.sections.depth) { state.sections.leaf_executed = true; } else { auto& child = state.sections.levels[state.sections.depth]; if (child.previous_section_id == child.max_section_id) { state.sections.levels.pop_back(); } } state.sections.current_section.pop_back(); } --state.sections.depth; } section_entry_checker::operator bool() noexcept { ++state.sections.depth; if (state.sections.depth > state.sections.levels.size()) { if (state.sections.depth > max_nested_sections) { state.reg.print( make_colored("error:", state.reg.with_color, color::fail), " max number of nested sections reached; " "please increase 'SNITCH_MAX_NESTED_SECTIONS' (currently ", max_nested_sections, ")\n."); std::terminate(); } state.sections.levels.push_back({}); } auto& level = state.sections.levels[state.sections.depth - 1]; ++level.current_section_id; if (level.max_section_id < level.current_section_id) { level.max_section_id = level.current_section_id; } if (!state.sections.leaf_executed && (level.previous_section_id + 1 == level.current_section_id || (level.previous_section_id == level.current_section_id && state.sections.levels.size() > state.sections.depth))) { level.previous_section_id = level.current_section_id; state.sections.current_section.push_back(section); entered = true; return true; } return false; } } // namespace snitch::impl // Captures implementation. // ------------------------ namespace snitch::impl { std::string_view extract_next_name(std::string_view& names) noexcept { std::string_view result; auto pos = names.find_first_of(",()\"\"''"); bool in_string = false; bool in_char = false; int parens = 0; while (pos != names.npos) { switch (names[pos]) { case '"': if (!in_char) { in_string = !in_string; } break; case '\'': if (!in_string) { in_char = !in_char; } break; case '(': if (!in_string && !in_char) { ++parens; } break; case ')': if (!in_string && !in_char) { --parens; } break; case ',': if (!in_string && !in_char && parens == 0) { result = names.substr(0, pos); trim(result, " \t\n\r"); names.remove_prefix(pos + 1); return result; } break; } pos = names.find_first_of(",()\"\"''", pos + 1); } std::swap(result, names); trim(result, " \t\n\r"); return result; } small_string<max_capture_length>& add_capture(test_state& state) noexcept { if (state.captures.available() == 0) { state.reg.print( make_colored("error:", state.reg.with_color, color::fail), " max number of captures reached; " "please increase 'SNITCH_MAX_CAPTURES' (currently ", max_captures, ")\n."); std::terminate(); } state.captures.grow(1); state.captures.back().clear(); return state.captures.back(); } } // namespace snitch::impl // Matcher implementation. // ----------------------- namespace snitch::matchers { contains_substring::contains_substring(std::string_view pattern) noexcept : substring_pattern(pattern) {} bool contains_substring::match(std::string_view message) const noexcept { return message.find(substring_pattern) != message.npos; } small_string<max_message_length> contains_substring::describe_match(std::string_view message, match_status status) const noexcept { small_string<max_message_length> description_buffer; append_or_truncate( description_buffer, (status == match_status::matched ? "found" : "could not find"), " '", substring_pattern, "' in '", message, "'"); return description_buffer; } with_what_contains::with_what_contains(std::string_view pattern) noexcept : contains_substring(pattern) {} } // namespace snitch::matchers // Testing framework implementation. // --------------------------------- namespace { using namespace snitch; using namespace snitch::impl; template<typename F> void for_each_raw_tag(std::string_view s, F&& callback) noexcept { if (s.empty()) { return; } if (s.find_first_of("[") == std::string_view::npos || s.find_first_of("]") == std::string_view::npos) { terminate_with("incorrectly formatted tag; please use \"[tag1][tag2][...]\""); } std::string_view delim = "]["; std::size_t pos = s.find(delim); std::size_t last_pos = 0u; while (pos != std::string_view::npos) { std::size_t cur_size = pos - last_pos; if (cur_size != 0) { callback(s.substr(last_pos, cur_size + 1)); } last_pos = pos + 1; pos = s.find(delim, last_pos); } callback(s.substr(last_pos)); } namespace tags { struct ignored {}; struct may_fail {}; struct should_fail {}; using parsed_tag = std::variant<std::string_view, ignored, may_fail, should_fail>; } // namespace tags template<typename F> void for_each_tag(std::string_view s, F&& callback) noexcept { small_string<max_tag_length> buffer; for_each_raw_tag(s, [&](std::string_view t) { // Look for "ignore" tags, which is either "[.]" // or a a tag starting with ".", like "[.integration]". if (t == "[.]"sv) { // This is a pure "ignore" tag, add this to the list of special tags. callback(tags::parsed_tag{tags::ignored{}}); } else if (t.starts_with("[."sv)) { // This is a combined "ignore" + normal tag, add the "ignore" to the list of special // tags, and continue with the normal tag. callback(tags::parsed_tag{tags::ignored{}}); callback(tags::parsed_tag{std::string_view("[.]")}); buffer.clear(); if (!append(buffer, "[", t.substr(2u))) { terminate_with("tag is too long"); } t = buffer; } if (t == "[!mayfail]") { callback(tags::parsed_tag{tags::may_fail{}}); } if (t == "[!shouldfail]") { callback(tags::parsed_tag{tags::should_fail{}}); } callback(tags::parsed_tag(t)); }); } std::string_view make_full_name(small_string<max_test_name_length>& buffer, const test_id& id) noexcept { buffer.clear(); if (id.type.length() != 0) { if (!append(buffer, id.name, " <", id.type, ">")) { return {}; } } else { if (!append(buffer, id.name)) { return {}; } } return buffer.str(); } template<typename F> void list_tests(const registry& r, F&& predicate) noexcept { small_string<max_test_name_length> buffer; for (const test_case& t : r) { if (!predicate(t)) { continue; } r.print(make_full_name(buffer, t.id), "\n"); } } void set_state(test_case& t, impl::test_case_state s) noexcept { if (static_cast<std::underlying_type_t<impl::test_case_state>>(t.state) < static_cast<std::underlying_type_t<impl::test_case_state>>(s)) { t.state = s; } } snitch::test_case_state convert_to_public_state(impl::test_case_state s) noexcept { switch (s) { case impl::test_case_state::success: return snitch::test_case_state::success; case impl::test_case_state::failed: return snitch::test_case_state::failed; case impl::test_case_state::skipped: return snitch::test_case_state::skipped; default: terminate_with("test case state cannot be exposed to the public"); } } small_vector<std::string_view, max_captures> make_capture_buffer(const capture_state& captures) { small_vector<std::string_view, max_captures> captures_buffer; for (const auto& c : captures) { captures_buffer.push_back(c); } return captures_buffer; } } // namespace namespace snitch { filter_result is_filter_match_name(std::string_view name, std::string_view filter) noexcept { filter_result match_action = filter_result::included; filter_result no_match_action = filter_result::not_included; if (filter.starts_with('~')) { filter = filter.substr(1); match_action = filter_result::excluded; no_match_action = filter_result::not_excluded; } return is_match(name, filter) ? match_action : no_match_action; } filter_result is_filter_match_tags(std::string_view tags, std::string_view filter) noexcept { filter_result match_action = filter_result::included; filter_result no_match_action = filter_result::not_included; if (filter.starts_with('~')) { filter = filter.substr(1); match_action = filter_result::excluded; no_match_action = filter_result::not_excluded; } bool match = false; for_each_tag(tags, [&](const tags::parsed_tag& v) { if (auto* vs = std::get_if<std::string_view>(&v); vs != nullptr && is_match(*vs, filter)) { match = true; } }); return match ? match_action : no_match_action; } filter_result is_filter_match_id(const test_id& id, std::string_view filter) noexcept { if (filter.starts_with('[') || filter.starts_with("~[")) { return is_filter_match_tags(id.tags, filter); } else { return is_filter_match_name(id.name, filter); } } const char* registry::add(const test_id& id, test_ptr func) noexcept { if (test_list.size() == test_list.capacity()) { print( make_colored("error:", with_color, color::fail), " max number of test cases reached; " "please increase 'SNITCH_MAX_TEST_CASES' (currently ", max_test_cases, ")\n."); std::terminate(); } test_list.push_back(test_case{id, func}); small_string<max_test_name_length> buffer; if (make_full_name(buffer, test_list.back().id).empty()) { print( make_colored("error:", with_color, color::fail), " max length of test name reached; " "please increase 'SNITCH_MAX_TEST_NAME_LENGTH' (currently ", max_test_name_length, ")\n."); std::terminate(); } return id.name.data(); } void registry::print_location( const impl::test_case& current_case, const impl::section_state& sections, const impl::capture_state& captures, const assertion_location& location) const noexcept { print( "running test case \"", make_colored(current_case.id.name, with_color, color::highlight1), "\"\n"); for (auto& section : sections.current_section) { print( " in section \"", make_colored(section.name, with_color, color::highlight1), "\"\n"); } print(" at ", location.file, ":", location.line, "\n"); if (!current_case.id.type.empty()) { print( " for type ", make_colored(current_case.id.type, with_color, color::highlight1), "\n"); } for (auto& capture : captures) { print(" with ", make_colored(capture, with_color, color::highlight1), "\n"); } } void registry::print_failure() const noexcept { print(make_colored("failed: ", with_color, color::fail)); } void registry::print_expected_failure() const noexcept { print(make_colored("expected failure: ", with_color, color::pass)); } void registry::print_skip() const noexcept { print(make_colored("skipped: ", with_color, color::skipped)); } void registry::print_details(std::string_view message) const noexcept { print(" ", make_colored(message, with_color, color::highlight2), "\n"); } void registry::print_details_expr(const expression& exp) const noexcept { print(" ", make_colored(exp.expected, with_color, color::highlight2)); if (!exp.actual.empty()) { print(", got ", make_colored(exp.actual, with_color, color::highlight2)); } print("\n"); } void registry::report_failure( impl::test_state& state, const assertion_location& location, std::string_view message) const noexcept { if (!state.may_fail) { set_state(state.test, impl::test_case_state::failed); } if (!report_callback.empty()) { const auto captures_buffer = make_capture_buffer(state.captures); report_callback( *this, event::assertion_failed{ state.test.id, state.sections.current_section, captures_buffer.span(), location, message, state.should_fail, state.may_fail}); } else { if (state.should_fail) { print_expected_failure(); } else { print_failure(); } print_location(state.test, state.sections, state.captures, location); print_details(message); } } void registry::report_failure( impl::test_state& state, const assertion_location& location, std::string_view message1, std::string_view message2) const noexcept { if (!state.may_fail) { set_state(state.test, impl::test_case_state::failed); } small_string<max_message_length> message; append_or_truncate(message, message1, message2); if (!report_callback.empty()) { const auto captures_buffer = make_capture_buffer(state.captures); report_callback( *this, event::assertion_failed{ state.test.id, state.sections.current_section, captures_buffer.span(), location, message, state.should_fail, state.may_fail}); } else { if (state.should_fail) { print_expected_failure(); } else { print_failure(); } print_location(state.test, state.sections, state.captures, location); print_details(message); } } void registry::report_failure( impl::test_state& state, const assertion_location& location, const impl::expression& exp) const noexcept { if (!state.may_fail) { set_state(state.test, impl::test_case_state::failed); } if (!report_callback.empty()) { const auto captures_buffer = make_capture_buffer(state.captures); if (!exp.actual.empty()) { small_string<max_message_length> message; append_or_truncate(message, exp.expected, ", got ", exp.actual); report_callback( *this, event::assertion_failed{ state.test.id, state.sections.current_section, captures_buffer.span(), location, message, state.should_fail, state.may_fail}); } else { report_callback( *this, event::assertion_failed{ state.test.id, state.sections.current_section, captures_buffer.span(), location, exp.expected, state.should_fail, state.may_fail}); } } else { if (state.should_fail) { print_expected_failure(); } else { print_failure(); } print_location(state.test, state.sections, state.captures, location); print_details_expr(exp); } } void registry::report_skipped( impl::test_state& state, const assertion_location& location, std::string_view message) const noexcept { set_state(state.test, impl::test_case_state::skipped); if (!report_callback.empty()) { const auto captures_buffer = make_capture_buffer(state.captures); report_callback( *this, event::test_case_skipped{ state.test.id, state.sections.current_section, captures_buffer.span(), location, message}); } else { print_skip(); print_location(state.test, state.sections, state.captures, location); print_details(message); } } test_state registry::run(test_case& test) noexcept { small_string<max_test_name_length> full_name; if (!report_callback.empty()) { report_callback(*this, event::test_case_started{test.id}); } else if (is_at_least(verbose, verbosity::high)) { make_full_name(full_name, test.id); print( make_colored("starting:", with_color, color::status), " ", make_colored(full_name, with_color, color::highlight1), "\n"); } test.state = impl::test_case_state::success; bool may_fail = false; bool should_fail = false; for_each_tag(test.id.tags, [&](const tags::parsed_tag& v) { if (std::holds_alternative<tags::may_fail>(v)) { may_fail = true; } else if (std::holds_alternative<tags::should_fail>(v)) { should_fail = true; } }); test_state state{.reg = *this, .test = test, .may_fail = may_fail, .should_fail = should_fail}; // Store previously running test, to restore it later. // This should always be a null pointer, except when testing snitch itself. test_state* previous_run = thread_current_test; thread_current_test = &state; #if SNITCH_WITH_TIMINGS using clock = std::chrono::steady_clock; auto time_start = clock::now(); #endif do { for (std::size_t i = 0; i < state.sections.levels.size(); ++i) { state.sections.levels[i].current_section_id = 0; } state.sections.leaf_executed = false; #if SNITCH_WITH_EXCEPTIONS try { test.func(); } catch (const impl::abort_exception&) { // Test aborted, assume its state was already set accordingly. } catch (const std::exception& e) { report_failure( state, {__FILE__, __LINE__}, "unhandled std::exception caught; message:", e.what()); } catch (...) { report_failure(state, {__FILE__, __LINE__}, "unhandled unknown exception caught"); } #else test.func(); #endif if (state.sections.levels.size() == 1) { auto& child = state.sections.levels[0]; if (child.previous_section_id == child.max_section_id) { state.sections.levels.clear(); state.sections.current_section.clear(); } } } while (!state.sections.levels.empty()); if (state.should_fail) { if (state.test.state == impl::test_case_state::success) { state.should_fail = false; report_failure(state, {__FILE__, __LINE__}, "expected test to fail, but it passed"); state.should_fail = true; } else if (state.test.state == impl::test_case_state::failed) { state.test.state = impl::test_case_state::success; } } #if SNITCH_WITH_TIMINGS auto time_end = clock::now(); state.duration = std::chrono::duration<float>(time_end - time_start).count(); #endif if (!report_callback.empty()) { #if SNITCH_WITH_TIMINGS report_callback( *this, event::test_case_ended{ .id = test.id, .state = convert_to_public_state(state.test.state), .assertion_count = state.asserts, .duration = state.duration}); #else report_callback( *this, event::test_case_ended{ .id = test.id, .state = convert_to_public_state(state.test.state), .assertion_count = state.asserts}); #endif } else if (is_at_least(verbose, verbosity::high)) { #if SNITCH_WITH_TIMINGS print( make_colored("finished:", with_color, color::status), " ", make_colored(full_name, with_color, color::highlight1), " (", state.duration, "s)\n"); #else print( make_colored("finished:", with_color, color::status), " ", make_colored(full_name, with_color, color::highlight1), "\n"); #endif } thread_current_test = previous_run; return state; } bool registry::run_selected_tests( std::string_view run_name, const small_function<bool(const test_id&) noexcept>& predicate) noexcept { if (!report_callback.empty()) { report_callback(*this, event::test_run_started{run_name}); } else if (is_at_least(verbose, registry::verbosity::normal)) { print( make_colored("starting tests with ", with_color, color::highlight2), make_colored("snitch v" SNITCH_FULL_VERSION "\n", with_color, color::highlight1)); print("==========================================\n"); } bool success = true; std::size_t run_count = 0; std::size_t fail_count = 0; std::size_t skip_count = 0; std::size_t assertion_count = 0; #if SNITCH_WITH_TIMINGS using clock = std::chrono::steady_clock; auto time_start = clock::now(); #endif for (test_case& t : *this) { if (!predicate(t.id)) { continue; } auto state = run(t); ++run_count; assertion_count += state.asserts; switch (t.state) { case impl::test_case_state::success: { // Nothing to do break; } case impl::test_case_state::failed: { ++fail_count; success = false; break; } case impl::test_case_state::skipped: { ++skip_count; break; } case impl::test_case_state::not_run: { // Unreachable break; } } } #if SNITCH_WITH_TIMINGS auto time_end = clock::now(); float duration = std::chrono::duration<float>(time_end - time_start).count(); #endif if (!report_callback.empty()) { #if SNITCH_WITH_TIMINGS report_callback( *this, event::test_run_ended{ .name = run_name, .success = success, .run_count = run_count, .fail_count = fail_count, .skip_count = skip_count, .assertion_count = assertion_count, .duration = duration}); #else report_callback( *this, event::test_run_ended{ .name = run_name, .success = success, .run_count = run_count, .fail_count = fail_count, .skip_count = skip_count, .assertion_count = assertion_count}); #endif } else if (is_at_least(verbose, registry::verbosity::normal)) { print("==========================================\n"); if (success) { print( make_colored("success:", with_color, color::pass), " all tests passed (", run_count, " test cases, ", assertion_count, " assertions"); } else { print( make_colored("error:", with_color, color::fail), " some tests failed (", fail_count, " out of ", run_count, " test cases, ", assertion_count, " assertions"); } if (skip_count > 0) { print(", ", skip_count, " test cases skipped"); } #if SNITCH_WITH_TIMINGS print(", ", duration, " seconds"); #endif print(")\n"); } return success; } bool registry::run_tests(std::string_view run_name) noexcept { const auto filter = [](const test_id& id) { bool selected = true; for_each_tag(id.tags, [&](const tags::parsed_tag& s) { if (std::holds_alternative<tags::ignored>(s)) { selected = false; } }); return selected; }; return run_selected_tests(run_name, filter); } void registry::list_all_tags() const noexcept { small_vector<std::string_view, max_unique_tags> tags; for (const auto& t : test_list) { for_each_tag(t.id.tags, [&](const tags::parsed_tag& v) { if (auto* vs = std::get_if<std::string_view>(&v); vs != nullptr) { if (std::find(tags.begin(), tags.end(), *vs) == tags.end()) { if (tags.size() == tags.capacity()) { print( make_colored("error:", with_color, color::fail), " max number of tags reached; " "please increase 'SNITCH_MAX_UNIQUE_TAGS' (currently ", max_unique_tags, ")\n."); std::terminate(); } tags.push_back(*vs); } } }); } std::sort(tags.begin(), tags.end()); for (const auto& t : tags) { print("[", t, "]\n"); } } void registry::list_all_tests() const noexcept { list_tests(*this, [](const test_case&) { return true; }); } void registry::list_tests_with_tag(std::string_view tag) const noexcept { list_tests(*this, [&](const test_case& t) { const auto result = is_filter_match_tags(t.id.tags, tag); return result == filter_result::included || result == filter_result::not_excluded; }); } test_case* registry::begin() noexcept { return test_list.begin(); } test_case* registry::end() noexcept { return test_list.end(); } const test_case* registry::begin() const noexcept { return test_list.begin(); } const test_case* registry::end() const noexcept { return test_list.end(); } constinit registry tests = []() { registry r; r.with_color = SNITCH_DEFAULT_WITH_COLOR == 1; return r; }(); } // namespace snitch // Main entry point utilities. // --------------------------- namespace { using namespace std::literals; constexpr std::size_t max_arg_names = 2; namespace argument_type { enum type { optional = 0b00, mandatory = 0b01, repeatable = 0b10 }; } struct expected_argument { small_vector<std::string_view, max_arg_names> names; std::optional<std::string_view> value_name; std::string_view description; argument_type::type type = argument_type::optional; }; using expected_arguments = small_vector<expected_argument, max_command_line_args>; struct parser_settings { bool with_color = true; }; std::string_view extract_executable(std::string_view path) { if (auto folder_end = path.find_last_of("\\/"); folder_end != path.npos) { path.remove_prefix(folder_end + 1); } if (auto extension_start = path.find_last_of('.'); extension_start != path.npos) { path.remove_suffix(path.size() - extension_start); } return path; } bool is_option(const expected_argument& e) { return !e.names.empty(); } bool is_option(const cli::argument& a) { return !a.name.empty(); } bool has_value(const expected_argument& e) { return e.value_name.has_value(); } bool is_mandatory(const expected_argument& e) { return (e.type & argument_type::mandatory) != 0; } bool is_repeatable(const expected_argument& e) { return (e.type & argument_type::repeatable) != 0; } std::optional<cli::input> parse_arguments( int argc, const char* const argv[], const expected_arguments& expected, const parser_settings& settings = parser_settings{}) noexcept { std::optional<cli::input> ret(std::in_place); ret->executable = extract_executable(argv[0]); auto& args = ret->arguments; bool bad = false; // Check validity of inputs small_vector<bool, max_command_line_args> expected_found; for (const auto& e : expected) { expected_found.push_back(false); if (is_option(e)) { if (e.names.size() == 1) { if (!e.names[0].starts_with('-')) { terminate_with("option name must start with '-' or '--'"); } } else { if (!(e.names[0].starts_with('-') && e.names[1].starts_with("--"))) { terminate_with("option names must be given with '-' first and '--' second"); } } } else { if (!has_value(e)) { terminate_with("positional argument must have a value name"); } } } // Parse for (int argi = 1; argi < argc; ++argi) { std::string_view arg(argv[argi]); if (arg.starts_with('-')) { // Options start with dashes. bool found = false; for (std::size_t arg_index = 0; arg_index < expected.size(); ++arg_index) { const auto& e = expected[arg_index]; if (!is_option(e)) { continue; } if (std::find(e.names.cbegin(), e.names.cend(), arg) == e.names.cend()) { continue; } found = true; if (expected_found[arg_index] && !is_repeatable(e)) { console_print( make_colored("error:", settings.with_color, color::error), " duplicate command line argument '", arg, "'\n"); bad = true; break; } expected_found[arg_index] = true; if (has_value(e)) { if (argi + 1 == argc) { console_print( make_colored("error:", settings.with_color, color::error), " missing value '<", *e.value_name, ">' for command line argument '", arg, "'\n"); bad = true; break; } argi += 1; args.push_back(cli::argument{ e.names.back(), e.value_name, {std::string_view(argv[argi])}}); } else { args.push_back(cli::argument{e.names.back()}); } break; } if (!found) { console_print( make_colored("warning:", settings.with_color, color::warning), " unknown command line argument '", arg, "'\n"); } } else { // If no dash, this is a positional argument. bool found = false; for (std::size_t arg_index = 0; arg_index < expected.size(); ++arg_index) { const auto& e = expected[arg_index]; if (is_option(e)) { continue; } if (expected_found[arg_index] && !is_repeatable(e)) { continue; } found = true; args.push_back(cli::argument{""sv, e.value_name, {arg}}); expected_found[arg_index] = true; break; } if (!found) { console_print( make_colored("error:", settings.with_color, color::error), " too many positional arguments\n"); bad = true; } } } for (std::size_t arg_index = 0; arg_index < expected.size(); ++arg_index) { const auto& e = expected[arg_index]; if (!expected_found[arg_index] && is_mandatory(e)) { if (!is_option(e)) { console_print( make_colored("error:", settings.with_color, color::error), " missing positional argument '<", *e.value_name, ">'\n"); } else { console_print( make_colored("error:", settings.with_color, color::error), " missing option '<", e.names.back(), ">'\n"); } bad = true; } } if (bad) { ret.reset(); } return ret; } struct print_help_settings { bool with_color = true; }; void print_help( std::string_view program_name, std::string_view program_description, const expected_arguments& expected, const print_help_settings& settings = print_help_settings{}) { // Print program desription console_print(make_colored(program_description, settings.with_color, color::highlight2), "\n"); // Print command line usage example console_print(make_colored("Usage:", settings.with_color, color::pass), "\n"); console_print(" ", program_name); if (std::any_of(expected.cbegin(), expected.cend(), [](auto& e) { return is_option(e); })) { console_print(" [options...]"); } for (const auto& e : expected) { if (!is_option(e)) { if (!is_mandatory(e) && !is_repeatable(e)) { console_print(" [<", *e.value_name, ">]"); } else if (is_mandatory(e) && !is_repeatable(e)) { console_print(" <", *e.value_name, ">"); } else if (!is_mandatory(e) && is_repeatable(e)) { console_print(" [<", *e.value_name, ">...]"); } else if (is_mandatory(e) && is_repeatable(e)) { console_print(" <", *e.value_name, ">..."); } else { terminate_with("unhandled argument type"); } } } console_print("\n\n"); // List arguments small_string<max_message_length> heading; for (const auto& e : expected) { heading.clear(); bool success = true; if (is_option(e)) { if (e.names[0].starts_with("--")) { success = success && append(heading, " "); } success = success && append(heading, e.names[0]); if (e.names.size() == 2) { success = success && append(heading, ", ", e.names[1]); } if (has_value(e)) { success = success && append(heading, " <", *e.value_name, ">"); } } else { success = success && append(heading, "<", *e.value_name, ">"); } if (!success) { terminate_with("argument name is too long"); } console_print( " ", make_colored(heading, settings.with_color, color::highlight1), " ", e.description, "\n"); } } // clang-format off constexpr expected_arguments expected_args = { {{"-l", "--list-tests"}, {}, "List tests by name"}, {{"--list-tags"}, {}, "List tags by name"}, {{"--list-tests-with-tag"}, {"[tag]"}, "List tests by name with a given tag"}, {{"-v", "--verbosity"}, {"quiet|normal|high"}, "Define how much gets sent to the standard output"}, {{"--color"}, {"always|never"}, "Enable/disable color in output"}, {{"-h", "--help"}, {}, "Print help"}, {{}, {"test regex"}, "A regex to select which test cases to run", argument_type::repeatable}}; // clang-format on constexpr bool with_color_default = SNITCH_DEFAULT_WITH_COLOR == 1; constexpr const char* program_description = "Test runner (snitch v" SNITCH_FULL_VERSION ")"; } // namespace namespace snitch::cli { std::optional<cli::input> parse_arguments(int argc, const char* const argv[]) noexcept { std::optional<cli::input> ret_args = parse_arguments(argc, argv, expected_args, {.with_color = with_color_default}); if (!ret_args) { console_print("\n"); print_help(argv[0], program_description, expected_args, {.with_color = with_color_default}); } return ret_args; } std::optional<cli::argument> get_option(const cli::input& args, std::string_view name) noexcept { std::optional<cli::argument> ret; auto iter = std::find_if(args.arguments.cbegin(), args.arguments.cend(), [&](const auto& arg) { return arg.name == name; }); if (iter != args.arguments.cend()) { ret = *iter; } return ret; } std::optional<cli::argument> get_positional_argument(const cli::input& args, std::string_view name) noexcept { std::optional<cli::argument> ret; auto iter = std::find_if(args.arguments.cbegin(), args.arguments.cend(), [&](const auto& arg) { return !is_option(arg) && arg.value_name == name; }); if (iter != args.arguments.cend()) { ret = *iter; } return ret; } void for_each_positional_argument( const cli::input& args, std::string_view name, const small_function<void(std::string_view) noexcept>& callback) noexcept { auto iter = args.arguments.cbegin(); while (iter != args.arguments.cend()) { iter = std::find_if(iter, args.arguments.cend(), [&](const auto& arg) { return !is_option(arg) && arg.value_name == name; }); if (iter != args.arguments.cend()) { callback(*iter->value); ++iter; } } } } // namespace snitch::cli namespace snitch { void registry::configure(const cli::input& args) noexcept { if (auto opt = get_option(args, "--color")) { if (*opt->value == "always") { with_color = true; } else if (*opt->value == "never") { with_color = false; } else { print( make_colored("warning:", with_color, color::warning), "unknown color directive; please use one of always|never\n"); } } if (auto opt = get_option(args, "--verbosity")) { if (*opt->value == "quiet") { verbose = snitch::registry::verbosity::quiet; } else if (*opt->value == "normal") { verbose = snitch::registry::verbosity::normal; } else if (*opt->value == "high") { verbose = snitch::registry::verbosity::high; } else { print( make_colored("warning:", with_color, color::warning), "unknown verbosity level; please use one of quiet|normal|high\n"); } } } bool registry::run_tests(const cli::input& args) noexcept { if (get_option(args, "--help")) { console_print("\n"); print_help( args.executable, program_description, expected_args, {.with_color = with_color_default}); return true; } if (get_option(args, "--list-tests")) { list_all_tests(); return true; } if (auto opt = get_option(args, "--list-tests-with-tag")) { list_tests_with_tag(*opt->value); return true; } if (get_option(args, "--list-tags")) { list_all_tags(); return true; } if (get_positional_argument(args, "test regex").has_value()) { const auto filter = [&](const test_id& id) noexcept { std::optional<bool> selected; const auto callback = [&](std::string_view filter) noexcept { switch (is_filter_match_id(id, filter)) { case filter_result::included: selected = true; break; case filter_result::excluded: selected = false; break; case filter_result::not_included: if (!selected.has_value()) { selected = false; } break; case filter_result::not_excluded: if (!selected.has_value()) { selected = true; } break; } }; for_each_positional_argument(args, "test regex", callback); return selected.value(); }; return run_selected_tests(args.executable, filter); } else { return run_tests(args.executable); } } } // namespace snitch #if SNITCH_DEFINE_MAIN // Main entry point. // ----------------- int main(int argc, char* argv[]) { std::optional<snitch::cli::input> args = snitch::cli::parse_arguments(argc, argv); if (!args) { return 1; } snitch::tests.configure(*args); return snitch::tests.run_tests(*args) ? 0 : 1; } #endif #endif
Become a Patron
Sponsor on GitHub
Donate via PayPal
Source on GitHub
Mailing list
Installed libraries
Wiki
Report an issue
How it works
Contact the author
CE on Mastodon
About the author
Statistics
Changelog
Version tree