A C++ calculator project demonstrating how to use Sentinel for mutation testing.
The project implements a stack-based expression evaluator using the Shunting-yard algorithm (Dijkstra). It parses and evaluates mathematical expressions like 1 + 2 * (1 + 2) with correct operator precedence and parentheses support.
| Class | Description |
|---|---|
Stack |
Integer stack (push, pop, peek) |
Tokenizer |
Breaks expression strings into tokens (numbers, operators, parentheses) |
Calculator |
Evaluates expressions using Shunting-yard with two stacks |
- Arithmetic:
+,-,*,/ - Parentheses:
(,) - Negative numbers:
-5,3 * -2
./build/calc "1 + 2 * (1 + 2)"
# Output: 7
./build/calc "100 / (2 * 5) + 3 * (4 - 1)"
# Output: 19| Tool | Version | Notes |
|---|---|---|
| CMake | 3.13+ | |
| C++ compiler | C++17 support | GCC or Clang |
| sentinel | any | Must be on PATH |
| gcovr | any | For coverage report (optional) |
| Internet access | — | GoogleTest is downloaded via FetchContent on first build |
From the sample/ directory:
cmake -B build
cmake --build buildVerify the tests pass:
cd build && ctest --output-on-failure -j$(nproc)cmake -DCMAKE_TESTING_ENABLED=ON -B build
cmake --build build
cd build && ctest --output-on-failure -j$(nproc)After running the tests, .gcda / .gcno files are generated in the build tree. Use gcovr to produce an HTML report:
gcovr --html-details coverage.html -r ../src .From the sample/ directory:
sentinelSentinel reads sentinel.yaml automatically. It will:
- Build the project and run the test suite once (baseline)
- Generate mutants from
src/(pass--limit=Non the command line to cap the number of mutants) - Apply each mutant, rebuild, and check whether the tests catch it
- Write an HTML report to
./sentinel_output/
xdg-open sentinel_output/index.html # Linux
open sentinel_output/index.html # macOSSentinel produces output at three stages. Here is what you will see when running it on this sample project.
After generation, Sentinel shows what was created:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Mutant Generation Summary
Target: all sources (3 files, 200 lines)
Generator: uniform (seed: 1234567890)
Mutants: 30
────────────────────────────────────────────────────────────────────────────────────
File Mutants Lines
────────────────────────────────────────────────────────────────────────────────────
src/Calculator.cpp 12 85
src/Stack.cpp 8 52
src/Tokenizer.cpp 10 63
────────────────────────────────────────────────────────────────────────────────────
Operator
────────────────────────────────────────────────────────────────────────────────────
AOR (Arithmetic Operator Replacement) 12
ROR (Relational Operator Replacement) 10
SDL (Statement Deletion) 8
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
This tells you the mutation scope, how many mutants were generated per file and per operator, which generator strategy was used, and the random seed (use --seed to reproduce the same mutant set).
Each mutant is evaluated one by one, with results printed in real time:
Evaluating 30 mutants...
[ 1/30] ✗ KILLED AOR src/Calculator.cpp:42:5 (+) [1s/1s]
← CalculatorTest.AddTwoNumbers
[ 2/30] ✓ SURVIVED ROR src/Stack.cpp:18:12 (<) [0s/1s]
[ 3/30] ⚠ BUILD_FAILURE SDL src/Tokenizer.cpp:31:3 (DELETE) [0s/0s]
↪ .sentinel_workspace/mutants/3/build.log
- ✗ KILLED — a test caught the mutation (the
←line shows which test) - ✓ SURVIVED — no test caught it; this is a test coverage gap
- ⚠ BUILD_FAILURE / TIMEOUT / RUNTIME_ERROR — skipped from the score (the
↪line points to the log file)
After all evaluations, the final summary:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Mutation Score Report
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
File Killed Survived Total Score
────────────────────────────────────────────────────────────────────────────────────
src/Calculator.cpp 8 2 10 80.0%
src/Stack.cpp 5 1 6 83.3%
src/Tokenizer.cpp 9 3 12 75.0%
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TOTAL 22 6 28 78.6%
────────────────────────────────────────────────────────────────────────────────────
Skipped: 2 build failures
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
The mutation score (Killed / Total) tells you how effective your tests are at detecting faults. Only Killed and Survived mutants count; build failures, timeouts, and runtime errors are listed under "Skipped".
| State | Icon | Meaning |
|---|---|---|
| Killed | ✗ | A test failed on the mutant — desired outcome |
| Survived | ✓ | All tests passed despite the mutation — test gap |
| Build Failure | ⚠ | Mutant did not compile — excluded from score |
| Timeout | ⚠ | Tests exceeded the time limit — excluded from score |
| Runtime Error | ⚠ | Tests crashed or exited abnormally — excluded from score |
Open the HTML report (sentinel_output/index.html) to see exactly which mutations survived. Each survived mutant shows the file, line, and the change that was made. Use this to write a targeted test case, then re-run Sentinel to confirm the score improves.