Why I Started Using This Tool
By now my little C project does a lot with one command: it builds, runs my Unity unit tests and
measures code coverage with gcov. At the end, CTest
prints a green 100% tests passed, and I feel great.
Then I close the terminal and it's gone.
It sounds minor, but it bugged me. When someone asked whether the tests passed yesterday, I had no answer except "I think so." I couldn't attach anything to a message. And everything I read about continuous integration (CI) said the same thing: build servers don't read your terminal, they read report files. If I ever want GitHub Actions or Jenkins to run my tests, I need CTest to write results somewhere.
It turns out CTest has had this built in all along. I just had to switch it on.
What It Does
CTest can write two kinds of report file:
- A log file: a plain text copy of everything CTest prints. Good for people.
- A JUnit XML file: a structured report in a standard format almost every CI tool understands. Good for machines.
Because I already run my tests through CMakePresets.json, turning both on was two lines in the
test preset's output block:
"output": {
"outputOnFailure": true,
"outputLogFile": "${sourceDir}/reports/ctest.log",
"outputJUnitFile": "${sourceDir}/reports/ctest_junit.xml"
}
${sourceDir} points to the project's top folder, so the reports always land in
reports/. These two settings do the same job as the --output-log and
--output-junit command-line flags, but I never have to remember them. One thing to know:
outputJUnitFile needs preset file version 6, which means CMake 3.25 or newer.
My verbose test preset inherits from default, so it picked up both
reports without any changes. I also added two small things around it:
file(MAKE_DIRECTORY ${CMAKE_SOURCE_DIR}/reports)
in CMakeLists.txt, so the reports/ folder is always created when the project is
configured, and reports/ in .gitignore, because generated files don't belong in
Git.
Then I ran the same command as always, cmake --workflow --preset test, and two files
appeared. The log is my terminal output, saved (with a few color codes mixed in, which look like junk in a
plain editor):
Test project C:/git/09_cmake/build
Start 1: test_src
1/2 Test #1: test_src ......................... Passed 0.02 sec
Start 2: coverage
2/2 Test #2: coverage ......................... Passed 0.02 sec
100% tests passed, 0 tests failed out of 2
The XML was the interesting one. At the top, a <testsuite> with tests="2",
failures="0" and a timestamp. Below it, one <testcase> for each CTest test,
and inside each one a <system-out> with the full output:
<testsuite name="(empty)" tests="2" failures="0" skipped="0"
timestamp="2026-10-11T16:19:49">
<testcase name="test_src" classname="test_src" time="0.0157419" status="run">
<system-out>...
C:/git/09_cmake/tests/test_arithmetic.c:32:test_add_two_plus_three:PASS
...
2 Tests 0 Failures 0 Ignored
OK
</system-out>
</testcase>
<testcase name="coverage" classname="coverage" time="0.0201083" status="run">
<system-out>...
Lines executed:4.76% of 42
</system-out>
</testcase>
</testsuite>
Every Unity PASS line and every gcov coverage number is saved, even though
outputOnFailure hides them in the terminal when tests pass.
Final Verdict
For a two-line change, this is one of the best upgrades I've made to the project. My test results now survive closing the terminal, they have a timestamp, and they're in a format any CI server can read.
Some honest caveats. The files are overwritten on every run, so this gives you the latest
result, not a history. The log contains color codes that look odd in Notepad. The suite name in the XML says
(empty), which is harmless but looks strange. And CTest only knows about whole test programs: my
two Unity tests show up as one <testcase> called test_src, with the details
inside as text. Splitting them into separate CTest entries is a job for later.
If you're already using CMake presets for your tests, there's no reason not to do this. My takeaway: a test that passes but leaves no record is only half done. Next step is letting a CI server read that XML on every push.