Commit 5081bc925c7d7e228a389eb2c7fd4e60d1ad475a
Committed by
GitHub
1 parent
5d0b9e40
feat: add regression tests and coverage report (#18)
Showing
12 changed files
with
1152 additions
and
0 deletions
.github/workflows/regression.yaml
0 → 100644
| 1 | +name: Regression | |
| 2 | + | |
| 3 | +on: | |
| 4 | + push: | |
| 5 | + branches: | |
| 6 | + - main | |
| 7 | + pull_request: | |
| 8 | + branches: | |
| 9 | + - main | |
| 10 | + | |
| 11 | +jobs: | |
| 12 | + regression: | |
| 13 | + name: Regression | |
| 14 | + runs-on: ubuntu-latest | |
| 15 | + | |
| 16 | + strategy: | |
| 17 | + fail-fast: false | |
| 18 | + matrix: | |
| 19 | + include: | |
| 20 | + - broker: Mosquitto | |
| 21 | + command: docker run --rm -d -p 1883:1883 -v $(pwd)/regression/configs/mosquitto:/mosquitto/config eclipse-mosquitto | |
| 22 | + # NATS doesn't deduplicate subscriptions, and this library doesn't either; in result, the regression fails | |
| 23 | + # - broker: NATS | |
| 24 | + # command: docker run --rm -d -v $(pwd)/regression/configs/nats:/config -p 1883:1883 nats -c /config/nats.conf | |
| 25 | + # FlashMQ doesn't deduplicate subscriptions, and this library doesn't either; in result, the regression fails | |
| 26 | + # - broker: FlashMQ | |
| 27 | + # command: docker run --rm -d -v $(pwd)/regression/configs/flashmq:/etc/flashmq -p 1883:1883 ghcr.io/truebrain/containers/flashmq | |
| 28 | + | |
| 29 | + steps: | |
| 30 | + - name: Checkout | |
| 31 | + uses: actions/checkout@v3 | |
| 32 | + | |
| 33 | + - name: Install conan & gcovr | |
| 34 | + run: | | |
| 35 | + pip install conan gcovr | |
| 36 | + conan profile new default --detect | |
| 37 | + conan profile update settings.compiler.libcxx=libstdc++11 default | |
| 38 | + | |
| 39 | + - name: Start ${{ matrix.broker }} | |
| 40 | + run: | | |
| 41 | + ${{ matrix.command }} | |
| 42 | + | |
| 43 | + - name: Build library | |
| 44 | + run: | | |
| 45 | + mkdir build | |
| 46 | + cd build | |
| 47 | + conan install .. | |
| 48 | + cmake .. -DCODE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug | |
| 49 | + make coverage | |
| 50 | + make coverage-html | |
| 51 | + | |
| 52 | + - uses: actions/upload-artifact@v3 | |
| 53 | + with: | |
| 54 | + name: coverage-report-${{ matrix.broker }} | |
| 55 | + path: build/coverage-html | ... | ... |
.github/workflows/sonarcloud.yaml
CMakeLists.txt
| ... | ... | @@ -13,7 +13,10 @@ set(CMAKE_CXX_STANDARD 17) |
| 13 | 13 | set(CMAKE_CXX_STANDARD_REQUIRED True) |
| 14 | 14 | set(THREADS_PREFER_PTHREAD_FLAG ON) |
| 15 | 15 | |
| 16 | +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/vendor/cmake-modules") | |
| 17 | + | |
| 16 | 18 | set(MIN_LOGGER_LEVEL "INFO" CACHE STRING "Set minimal logger level (TRACE, DEBUG, INFO, WARNING, ERROR). No logs below this level will be omitted.") |
| 19 | +set(CODE_COVERAGE "OFF" CACHE STRING "Enable code coverage.") | |
| 17 | 20 | |
| 18 | 21 | include(GNUInstallDirs) |
| 19 | 22 | |
| ... | ... | @@ -51,6 +54,25 @@ endif() |
| 51 | 54 | |
| 52 | 55 | target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror) |
| 53 | 56 | |
| 57 | +if(CODE_COVERAGE) | |
| 58 | + include(CodeCoverage) | |
| 59 | + | |
| 60 | + add_subdirectory(regression) | |
| 61 | + | |
| 62 | + target_compile_options(${PROJECT_NAME} PRIVATE -Og -g --coverage) | |
| 63 | + target_link_libraries(${PROJECT_NAME} PRIVATE gcov) | |
| 64 | + | |
| 65 | + setup_target_for_coverage_gcovr_xml( | |
| 66 | + NAME coverage | |
| 67 | + EXECUTABLE truemqtt_regression | |
| 68 | + BASE_DIRECTORY "${PROJECT_SOURCE_DIR}/src") | |
| 69 | + | |
| 70 | + setup_target_for_coverage_gcovr_html( | |
| 71 | + NAME coverage-html | |
| 72 | + EXECUTABLE truemqtt_regression | |
| 73 | + BASE_DIRECTORY "${PROJECT_SOURCE_DIR}/src") | |
| 74 | +endif() | |
| 75 | + | |
| 54 | 76 | install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) |
| 55 | 77 | install(FILES ${CMAKE_BINARY_DIR}/truemqtt.pc DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig) |
| 56 | 78 | ... | ... |
README.md
| ... | ... | @@ -28,6 +28,13 @@ make -j$(nproc) |
| 28 | 28 | example/pubsub/truemqtt_pubsub |
| 29 | 29 | ``` |
| 30 | 30 | |
| 31 | +Or in case you want to run the regression / code coverage: | |
| 32 | + | |
| 33 | +```bash | |
| 34 | +cmake .. -DBUILD_SHARED_LIBS=ON -DMIN_LOGGER_LEVEL=INFO -DCMAKE_BUILD_TYPE=Debug -DCODE_COVERAGE=ON | |
| 35 | +make -j$(nproc) coverage-html | |
| 36 | +``` | |
| 37 | + | |
| 31 | 38 | ## Design choices |
| 32 | 39 | |
| 33 | 40 | ### MQTT v3 only | ... | ... |
regression/CMakeLists.txt
0 → 100644
| 1 | +# | |
| 2 | +# Copyright (c) TrueBrain | |
| 3 | +# | |
| 4 | +# This source code is licensed under the MIT license found in the | |
| 5 | +# LICENSE file in the root directory of this source tree. | |
| 6 | +# | |
| 7 | + | |
| 8 | +cmake_minimum_required(VERSION 3.16) | |
| 9 | + | |
| 10 | +project(truemqtt_regression) | |
| 11 | + | |
| 12 | +set(CMAKE_CXX_STANDARD 17) | |
| 13 | +set(CMAKE_CXX_STANDARD_REQUIRED ON) | |
| 14 | + | |
| 15 | +include_directories(SYSTEM ${CMAKE_CURRENT_SOURCE_DIR}/../include) | |
| 16 | + | |
| 17 | +add_executable(${PROJECT_NAME} main.cpp) | |
| 18 | +target_link_libraries(${PROJECT_NAME} truemqtt) | ... | ... |
regression/configs/flashmq/flashmq.conf
0 → 100644
| 1 | +allow_anonymous true | ... | ... |
regression/configs/mosquitto/mosquitto.conf
0 → 100644
regression/configs/nats/nats.conf
0 → 100644
regression/main.cpp
0 → 100644
| 1 | +/* | |
| 2 | + * Copyright (c) TrueBrain | |
| 3 | + * | |
| 4 | + * This source code is licensed under the MIT license found in the | |
| 5 | + * LICENSE file in the root directory of this source tree. | |
| 6 | + */ | |
| 7 | + | |
| 8 | +#include <TrueMQTT.h> | |
| 9 | + | |
| 10 | +#include <catch2/catch_test_macros.hpp> | |
| 11 | +#include <mutex> | |
| 12 | +#include <thread> | |
| 13 | + | |
| 14 | +#include <iostream> | |
| 15 | + | |
| 16 | +TEST_CASE("regression", "[regression]") | |
| 17 | +{ | |
| 18 | + // last_message / logger_mutex needs to outlive client1, as it is captured in a lambda. | |
| 19 | + std::string last_message = {}; | |
| 20 | + std::mutex logger_mutex; | |
| 21 | + | |
| 22 | + TrueMQTT::Client client1("localhost", 1883, "client-1"); | |
| 23 | + TrueMQTT::Client client2("localhost", 1883, "client-2"); | |
| 24 | + | |
| 25 | + client1.setLogger( | |
| 26 | + TrueMQTT::Client::LogLevel::INFO, | |
| 27 | + [&last_message, &logger_mutex](TrueMQTT::Client::LogLevel level, const std::string_view message) | |
| 28 | + { | |
| 29 | + std::scoped_lock lock(logger_mutex); | |
| 30 | + last_message = message; | |
| 31 | + }); | |
| 32 | + | |
| 33 | + client2.setPublishQueue(TrueMQTT::Client::PublishQueueType::FIFO, 10); | |
| 34 | + | |
| 35 | + uint8_t connected = 0x0; | |
| 36 | + client1.setStateChangeCallback( | |
| 37 | + [&connected](TrueMQTT::Client::State state) | |
| 38 | + { | |
| 39 | + if (state == TrueMQTT::Client::State::CONNECTED) | |
| 40 | + { | |
| 41 | + connected |= 0x1; | |
| 42 | + } | |
| 43 | + }); | |
| 44 | + client2.setStateChangeCallback( | |
| 45 | + [&connected](TrueMQTT::Client::State state) | |
| 46 | + { | |
| 47 | + if (state == TrueMQTT::Client::State::CONNECTED) | |
| 48 | + { | |
| 49 | + connected |= 0x2; | |
| 50 | + } | |
| 51 | + }); | |
| 52 | + | |
| 53 | + // TC: Cannot publish/subscribe/unsubscribe when disconnected | |
| 54 | + { | |
| 55 | + CHECK(client1.publish("regression/topic1", "Hello World!", false) == false); | |
| 56 | + CHECK(last_message == "Cannot publish when disconnected"); | |
| 57 | + | |
| 58 | + client1.subscribe("regression/topic1", [](const std::string_view topic, const std::string_view message) { /* empty */ }); | |
| 59 | + CHECK(last_message == "Cannot subscribe when disconnected"); | |
| 60 | + | |
| 61 | + client1.unsubscribe("regression/topic1"); | |
| 62 | + CHECK(last_message == "Cannot unsubscribe when disconnected"); | |
| 63 | + } | |
| 64 | + | |
| 65 | + uint8_t received = 0; | |
| 66 | + uint8_t received2 = 0; | |
| 67 | + | |
| 68 | + // TC: Connect the subscribing client first and instantly make a subscription; subscription should be created after connect. | |
| 69 | + { | |
| 70 | + client1.connect(); | |
| 71 | + CHECK(last_message == "Connecting to localhost:1883"); | |
| 72 | + client1.subscribe( | |
| 73 | + "regression/topic1", | |
| 74 | + [&received](const std::string_view topic, const std::string_view payload) | |
| 75 | + { | |
| 76 | + CHECK(std::string(topic) == "regression/topic1"); | |
| 77 | + received++; | |
| 78 | + }); | |
| 79 | + | |
| 80 | + auto start = std::chrono::steady_clock::now(); | |
| 81 | + while ((connected & 0x1) == 0) | |
| 82 | + { | |
| 83 | + std::this_thread::sleep_for(std::chrono::milliseconds(10)); | |
| 84 | + if (std::chrono::steady_clock::now() - start > std::chrono::seconds(5)) | |
| 85 | + { | |
| 86 | + FAIL("Timeout waiting for client to connect"); | |
| 87 | + } | |
| 88 | + } | |
| 89 | + | |
| 90 | + // Give some time for the subscription to actually be made. | |
| 91 | + std::this_thread::sleep_for(std::chrono::milliseconds(50)); | |
| 92 | + } | |
| 93 | + | |
| 94 | + // TC: Connect the publishing client next and instantly publish a message; message should be published after connect. | |
| 95 | + { | |
| 96 | + client2.connect(); | |
| 97 | + CHECK(client2.publish("regression/topic1", "Hello World!", false) == true); | |
| 98 | + | |
| 99 | + auto start = std::chrono::steady_clock::now(); | |
| 100 | + while ((connected & 0x2) == 0) | |
| 101 | + { | |
| 102 | + std::this_thread::sleep_for(std::chrono::milliseconds(10)); | |
| 103 | + if (std::chrono::steady_clock::now() - start > std::chrono::seconds(1)) | |
| 104 | + { | |
| 105 | + FAIL("Timeout waiting for client to connect"); | |
| 106 | + } | |
| 107 | + } | |
| 108 | + } | |
| 109 | + | |
| 110 | + // TC: Publish before connect happened and check message arrives after connection is made | |
| 111 | + { | |
| 112 | + // Wait for the pending publish to arrive. | |
| 113 | + auto start = std::chrono::steady_clock::now(); | |
| 114 | + while (received != 1) | |
| 115 | + { | |
| 116 | + std::this_thread::sleep_for(std::chrono::milliseconds(10)); | |
| 117 | + if (std::chrono::steady_clock::now() - start > std::chrono::seconds(1)) | |
| 118 | + { | |
| 119 | + FAIL("Timeout waiting for message to arrive"); | |
| 120 | + } | |
| 121 | + } | |
| 122 | + received = 0; | |
| 123 | + } | |
| 124 | + | |
| 125 | + // TC: Cannot connect when already connected | |
| 126 | + { | |
| 127 | + client1.connect(); | |
| 128 | + CHECK(last_message == "Can't connect when already connecting / connected"); | |
| 129 | + } | |
| 130 | + | |
| 131 | + // TC: Publish to delayed subscription must work | |
| 132 | + { | |
| 133 | + CHECK(client2.publish("regression/topic1", "Hello World!", false) == true); | |
| 134 | + CHECK(client2.publish("regression/topic1", "Hello World!", false) == true); | |
| 135 | + CHECK(client2.publish("regression/topic1", "Hello World!", false) == true); | |
| 136 | + CHECK(client2.publish("regression/topic1", "Hello World!", false) == true); | |
| 137 | + | |
| 138 | + auto start = std::chrono::steady_clock::now(); | |
| 139 | + while (received != 4) | |
| 140 | + { | |
| 141 | + std::this_thread::sleep_for(std::chrono::milliseconds(10)); | |
| 142 | + if (std::chrono::steady_clock::now() - start > std::chrono::seconds(1)) | |
| 143 | + { | |
| 144 | + FAIL("Timeout waiting for message to arrive"); | |
| 145 | + } | |
| 146 | + } | |
| 147 | + received = 0; | |
| 148 | + } | |
| 149 | + | |
| 150 | + // TC: Subscribe to new topic and publish on it | |
| 151 | + { | |
| 152 | + client1.subscribe( | |
| 153 | + "regression/topic2", | |
| 154 | + [&received2](const std::string_view topic, const std::string_view payload) | |
| 155 | + { | |
| 156 | + CHECK(std::string(topic) == "regression/topic2"); | |
| 157 | + received2++; | |
| 158 | + }); | |
| 159 | + | |
| 160 | + // Give some time for the subscription to actually be made. | |
| 161 | + std::this_thread::sleep_for(std::chrono::milliseconds(50)); | |
| 162 | + | |
| 163 | + CHECK(client2.publish("regression/topic2", "Hello World!", false) == true); | |
| 164 | + | |
| 165 | + // Wait for the pending publish to arrive. | |
| 166 | + auto start = std::chrono::steady_clock::now(); | |
| 167 | + while (received2 != 1) | |
| 168 | + { | |
| 169 | + std::this_thread::sleep_for(std::chrono::milliseconds(10)); | |
| 170 | + if (std::chrono::steady_clock::now() - start > std::chrono::seconds(1)) | |
| 171 | + { | |
| 172 | + FAIL("Timeout waiting for message to arrive"); | |
| 173 | + } | |
| 174 | + } | |
| 175 | + received2 = 0; | |
| 176 | + } | |
| 177 | + | |
| 178 | + // TC: Unsubscribe and check a publish to the topic no longer arrives | |
| 179 | + { | |
| 180 | + client1.unsubscribe("regression/topic1"); | |
| 181 | + client1.unsubscribe("regression/topic2"); | |
| 182 | + | |
| 183 | + CHECK(client2.publish("regression/topic1", "Hello World!", false) == true); | |
| 184 | + CHECK(client2.publish("regression/topic2", "Hello World!", false) == true); | |
| 185 | + | |
| 186 | + std::this_thread::sleep_for(std::chrono::milliseconds(50)); | |
| 187 | + | |
| 188 | + REQUIRE(received == 0); | |
| 189 | + REQUIRE(received2 == 0); | |
| 190 | + } | |
| 191 | + | |
| 192 | + // TC: Check wildcard subscriptions | |
| 193 | + { | |
| 194 | + client1.subscribe( | |
| 195 | + "regression/topic3/+", | |
| 196 | + [&received](const std::string_view topic, const std::string_view payload) | |
| 197 | + { | |
| 198 | + CHECK(std::string(topic) == "regression/topic3/1"); | |
| 199 | + received++; | |
| 200 | + }); | |
| 201 | + client1.subscribe( | |
| 202 | + "regression/#", | |
| 203 | + [&received2](const std::string_view topic, const std::string_view payload) | |
| 204 | + { | |
| 205 | + if (received2 == 0) | |
| 206 | + { | |
| 207 | + CHECK(std::string(topic) == "regression/topic3/1"); | |
| 208 | + } | |
| 209 | + else | |
| 210 | + { | |
| 211 | + CHECK(std::string(topic) == "regression/topic4/2"); | |
| 212 | + } | |
| 213 | + received2++; | |
| 214 | + }); | |
| 215 | + | |
| 216 | + std::this_thread::sleep_for(std::chrono::milliseconds(50)); | |
| 217 | + | |
| 218 | + CHECK(client2.publish("regression/topic3/1", "Hello World!", false) == true); | |
| 219 | + CHECK(client2.publish("regression/topic4/2", "Hello World!", false) == true); | |
| 220 | + | |
| 221 | + auto start = std::chrono::steady_clock::now(); | |
| 222 | + while (received != 1 || received2 != 2) | |
| 223 | + { | |
| 224 | + std::this_thread::sleep_for(std::chrono::milliseconds(10)); | |
| 225 | + if (std::chrono::steady_clock::now() - start > std::chrono::seconds(1)) | |
| 226 | + { | |
| 227 | + FAIL("Timeout waiting for message to arrive"); | |
| 228 | + } | |
| 229 | + } | |
| 230 | + | |
| 231 | + received = 0; | |
| 232 | + received2 = 0; | |
| 233 | + } | |
| 234 | + | |
| 235 | + // TC: Cannot unsubscribe from topic not subscribed to | |
| 236 | + { | |
| 237 | + client1.unsubscribe("regression/topic1"); | |
| 238 | + CHECK(last_message == "Cannot unsubscribe from topic 'regression/topic1' because we are not subscribed to it"); | |
| 239 | + } | |
| 240 | + | |
| 241 | + // TC: Validate disconnect is seen | |
| 242 | + { | |
| 243 | + client1.disconnect(); | |
| 244 | + CHECK(last_message == "Disconnecting from broker"); | |
| 245 | + client2.disconnect(); | |
| 246 | + } | |
| 247 | + | |
| 248 | + // TC: Cannot disconnect when not connected | |
| 249 | + { | |
| 250 | + client1.disconnect(); | |
| 251 | + CHECK(last_message == "Can't disconnect when already disconnected"); | |
| 252 | + } | |
| 253 | + | |
| 254 | + // TC: Connect to a non-existing broker | |
| 255 | + { | |
| 256 | + TrueMQTT::Client client3("localhost", 1884, "client-3"); | |
| 257 | + | |
| 258 | + client3.setLogger( | |
| 259 | + TrueMQTT::Client::LogLevel::INFO, | |
| 260 | + [&last_message, &logger_mutex](TrueMQTT::Client::LogLevel level, const std::string_view message) | |
| 261 | + { | |
| 262 | + std::scoped_lock lock(logger_mutex); | |
| 263 | + last_message = message; | |
| 264 | + }); | |
| 265 | + client3.setStateChangeCallback( | |
| 266 | + [&connected](TrueMQTT::Client::State state) | |
| 267 | + { | |
| 268 | + if (state == TrueMQTT::Client::State::CONNECTED) | |
| 269 | + { | |
| 270 | + connected |= 0x4; | |
| 271 | + } | |
| 272 | + }); | |
| 273 | + | |
| 274 | + client3.connect(); | |
| 275 | + | |
| 276 | + CHECK(last_message == "Connecting to localhost:1884"); | |
| 277 | + | |
| 278 | + bool failed = false; | |
| 279 | + auto start = std::chrono::steady_clock::now(); | |
| 280 | + while ((connected & 0x4) == 0) | |
| 281 | + { | |
| 282 | + std::this_thread::sleep_for(std::chrono::milliseconds(10)); | |
| 283 | + if (std::chrono::steady_clock::now() - start > std::chrono::milliseconds(100)) | |
| 284 | + { | |
| 285 | + failed = true; | |
| 286 | + break; | |
| 287 | + } | |
| 288 | + } | |
| 289 | + REQUIRE(failed == true); | |
| 290 | + | |
| 291 | + client3.disconnect(); | |
| 292 | + CHECK(last_message == "Disconnecting from broker"); | |
| 293 | + } | |
| 294 | +} | ... | ... |
src/Connection.cpp
| ... | ... | @@ -87,6 +87,11 @@ void TrueMQTT::Client::Impl::Connection::runRead() |
| 87 | 87 | |
| 88 | 88 | std::this_thread::sleep_for(m_backoff); |
| 89 | 89 | |
| 90 | + if (m_state == State::STOP) | |
| 91 | + { | |
| 92 | + break; | |
| 93 | + } | |
| 94 | + | |
| 90 | 95 | // Calculate the next backoff time, slowly reducing how often we retry. |
| 91 | 96 | m_backoff *= 2; |
| 92 | 97 | if (m_backoff > m_impl.m_connection_backoff_max) | ... | ... |
vendor/cmake-modules/CodeCoverage.cmake
0 → 100644
| 1 | +# Copyright (c) 2012 - 2017, Lars Bilke | |
| 2 | +# All rights reserved. | |
| 3 | +# | |
| 4 | +# Redistribution and use in source and binary forms, with or without modification, | |
| 5 | +# are permitted provided that the following conditions are met: | |
| 6 | +# | |
| 7 | +# 1. Redistributions of source code must retain the above copyright notice, this | |
| 8 | +# list of conditions and the following disclaimer. | |
| 9 | +# | |
| 10 | +# 2. Redistributions in binary form must reproduce the above copyright notice, | |
| 11 | +# this list of conditions and the following disclaimer in the documentation | |
| 12 | +# and/or other materials provided with the distribution. | |
| 13 | +# | |
| 14 | +# 3. Neither the name of the copyright holder nor the names of its contributors | |
| 15 | +# may be used to endorse or promote products derived from this software without | |
| 16 | +# specific prior written permission. | |
| 17 | +# | |
| 18 | +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
| 19 | +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
| 20 | +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| 21 | +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR | |
| 22 | +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
| 23 | +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
| 24 | +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | |
| 25 | +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 26 | +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
| 27 | +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 28 | +# | |
| 29 | +# CHANGES: | |
| 30 | +# | |
| 31 | +# 2012-01-31, Lars Bilke | |
| 32 | +# - Enable Code Coverage | |
| 33 | +# | |
| 34 | +# 2013-09-17, Joakim Söderberg | |
| 35 | +# - Added support for Clang. | |
| 36 | +# - Some additional usage instructions. | |
| 37 | +# | |
| 38 | +# 2016-02-03, Lars Bilke | |
| 39 | +# - Refactored functions to use named parameters | |
| 40 | +# | |
| 41 | +# 2017-06-02, Lars Bilke | |
| 42 | +# - Merged with modified version from github.com/ufz/ogs | |
| 43 | +# | |
| 44 | +# 2019-05-06, Anatolii Kurotych | |
| 45 | +# - Remove unnecessary --coverage flag | |
| 46 | +# | |
| 47 | +# 2019-12-13, FeRD (Frank Dana) | |
| 48 | +# - Deprecate COVERAGE_LCOVR_EXCLUDES and COVERAGE_GCOVR_EXCLUDES lists in favor | |
| 49 | +# of tool-agnostic COVERAGE_EXCLUDES variable, or EXCLUDE setup arguments. | |
| 50 | +# - CMake 3.4+: All excludes can be specified relative to BASE_DIRECTORY | |
| 51 | +# - All setup functions: accept BASE_DIRECTORY, EXCLUDE list | |
| 52 | +# - Set lcov basedir with -b argument | |
| 53 | +# - Add automatic --demangle-cpp in lcovr, if 'c++filt' is available (can be | |
| 54 | +# overridden with NO_DEMANGLE option in setup_target_for_coverage_lcovr().) | |
| 55 | +# - Delete output dir, .info file on 'make clean' | |
| 56 | +# - Remove Python detection, since version mismatches will break gcovr | |
| 57 | +# - Minor cleanup (lowercase function names, update examples...) | |
| 58 | +# | |
| 59 | +# 2019-12-19, FeRD (Frank Dana) | |
| 60 | +# - Rename Lcov outputs, make filtered file canonical, fix cleanup for targets | |
| 61 | +# | |
| 62 | +# 2020-01-19, Bob Apthorpe | |
| 63 | +# - Added gfortran support | |
| 64 | +# | |
| 65 | +# 2020-02-17, FeRD (Frank Dana) | |
| 66 | +# - Make all add_custom_target()s VERBATIM to auto-escape wildcard characters | |
| 67 | +# in EXCLUDEs, and remove manual escaping from gcovr targets | |
| 68 | +# | |
| 69 | +# 2021-01-19, Robin Mueller | |
| 70 | +# - Add CODE_COVERAGE_VERBOSE option which will allow to print out commands which are run | |
| 71 | +# - Added the option for users to set the GCOVR_ADDITIONAL_ARGS variable to supply additional | |
| 72 | +# flags to the gcovr command | |
| 73 | +# | |
| 74 | +# 2020-05-04, Mihchael Davis | |
| 75 | +# - Add -fprofile-abs-path to make gcno files contain absolute paths | |
| 76 | +# - Fix BASE_DIRECTORY not working when defined | |
| 77 | +# - Change BYPRODUCT from folder to index.html to stop ninja from complaining about double defines | |
| 78 | +# | |
| 79 | +# 2021-05-10, Martin Stump | |
| 80 | +# - Check if the generator is multi-config before warning about non-Debug builds | |
| 81 | +# | |
| 82 | +# 2022-02-22, Marko Wehle | |
| 83 | +# - Change gcovr output from -o <filename> for --xml <filename> and --html <filename> output respectively. | |
| 84 | +# This will allow for Multiple Output Formats at the same time by making use of GCOVR_ADDITIONAL_ARGS, e.g. GCOVR_ADDITIONAL_ARGS "--txt". | |
| 85 | +# | |
| 86 | +# USAGE: | |
| 87 | +# | |
| 88 | +# 1. Copy this file into your cmake modules path. | |
| 89 | +# | |
| 90 | +# 2. Add the following line to your CMakeLists.txt (best inside an if-condition | |
| 91 | +# using a CMake option() to enable it just optionally): | |
| 92 | +# include(CodeCoverage) | |
| 93 | +# | |
| 94 | +# 3. Append necessary compiler flags for all supported source files: | |
| 95 | +# append_coverage_compiler_flags() | |
| 96 | +# Or for specific target: | |
| 97 | +# append_coverage_compiler_flags_to_target(YOUR_TARGET_NAME) | |
| 98 | +# | |
| 99 | +# 3.a (OPTIONAL) Set appropriate optimization flags, e.g. -O0, -O1 or -Og | |
| 100 | +# | |
| 101 | +# 4. If you need to exclude additional directories from the report, specify them | |
| 102 | +# using full paths in the COVERAGE_EXCLUDES variable before calling | |
| 103 | +# setup_target_for_coverage_*(). | |
| 104 | +# Example: | |
| 105 | +# set(COVERAGE_EXCLUDES | |
| 106 | +# '${PROJECT_SOURCE_DIR}/src/dir1/*' | |
| 107 | +# '/path/to/my/src/dir2/*') | |
| 108 | +# Or, use the EXCLUDE argument to setup_target_for_coverage_*(). | |
| 109 | +# Example: | |
| 110 | +# setup_target_for_coverage_lcov( | |
| 111 | +# NAME coverage | |
| 112 | +# EXECUTABLE testrunner | |
| 113 | +# EXCLUDE "${PROJECT_SOURCE_DIR}/src/dir1/*" "/path/to/my/src/dir2/*") | |
| 114 | +# | |
| 115 | +# 4.a NOTE: With CMake 3.4+, COVERAGE_EXCLUDES or EXCLUDE can also be set | |
| 116 | +# relative to the BASE_DIRECTORY (default: PROJECT_SOURCE_DIR) | |
| 117 | +# Example: | |
| 118 | +# set(COVERAGE_EXCLUDES "dir1/*") | |
| 119 | +# setup_target_for_coverage_gcovr_html( | |
| 120 | +# NAME coverage | |
| 121 | +# EXECUTABLE testrunner | |
| 122 | +# BASE_DIRECTORY "${PROJECT_SOURCE_DIR}/src" | |
| 123 | +# EXCLUDE "dir2/*") | |
| 124 | +# | |
| 125 | +# 5. Use the functions described below to create a custom make target which | |
| 126 | +# runs your test executable and produces a code coverage report. | |
| 127 | +# | |
| 128 | +# 6. Build a Debug build: | |
| 129 | +# cmake -DCMAKE_BUILD_TYPE=Debug .. | |
| 130 | +# make | |
| 131 | +# make my_coverage_target | |
| 132 | +# | |
| 133 | + | |
| 134 | +include(CMakeParseArguments) | |
| 135 | + | |
| 136 | +option(CODE_COVERAGE_VERBOSE "Verbose information" FALSE) | |
| 137 | + | |
| 138 | +# Check prereqs | |
| 139 | +find_program( GCOV_PATH gcov ) | |
| 140 | +find_program( LCOV_PATH NAMES lcov lcov.bat lcov.exe lcov.perl) | |
| 141 | +find_program( FASTCOV_PATH NAMES fastcov fastcov.py ) | |
| 142 | +find_program( GENHTML_PATH NAMES genhtml genhtml.perl genhtml.bat ) | |
| 143 | +find_program( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test) | |
| 144 | +find_program( CPPFILT_PATH NAMES c++filt ) | |
| 145 | + | |
| 146 | +if(NOT GCOV_PATH) | |
| 147 | + message(FATAL_ERROR "gcov not found! Aborting...") | |
| 148 | +endif() # NOT GCOV_PATH | |
| 149 | + | |
| 150 | +# Check supported compiler (Clang, GNU and Flang) | |
| 151 | +get_property(LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES) | |
| 152 | +foreach(LANG ${LANGUAGES}) | |
| 153 | + if("${CMAKE_${LANG}_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang") | |
| 154 | + if("${CMAKE_${LANG}_COMPILER_VERSION}" VERSION_LESS 3) | |
| 155 | + message(FATAL_ERROR "Clang version must be 3.0.0 or greater! Aborting...") | |
| 156 | + endif() | |
| 157 | + elseif(NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES "GNU" | |
| 158 | + AND NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES "(LLVM)?[Ff]lang") | |
| 159 | + message(FATAL_ERROR "Compiler is not GNU or Flang! Aborting...") | |
| 160 | + endif() | |
| 161 | +endforeach() | |
| 162 | + | |
| 163 | +set(COVERAGE_COMPILER_FLAGS "-g -fprofile-arcs -ftest-coverage" | |
| 164 | + CACHE INTERNAL "") | |
| 165 | +if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") | |
| 166 | + include(CheckCXXCompilerFlag) | |
| 167 | + check_cxx_compiler_flag(-fprofile-abs-path HAVE_fprofile_abs_path) | |
| 168 | + if(HAVE_fprofile_abs_path) | |
| 169 | + set(COVERAGE_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path") | |
| 170 | + endif() | |
| 171 | +endif() | |
| 172 | + | |
| 173 | +set(CMAKE_Fortran_FLAGS_COVERAGE | |
| 174 | + ${COVERAGE_COMPILER_FLAGS} | |
| 175 | + CACHE STRING "Flags used by the Fortran compiler during coverage builds." | |
| 176 | + FORCE ) | |
| 177 | +set(CMAKE_CXX_FLAGS_COVERAGE | |
| 178 | + ${COVERAGE_COMPILER_FLAGS} | |
| 179 | + CACHE STRING "Flags used by the C++ compiler during coverage builds." | |
| 180 | + FORCE ) | |
| 181 | +set(CMAKE_C_FLAGS_COVERAGE | |
| 182 | + ${COVERAGE_COMPILER_FLAGS} | |
| 183 | + CACHE STRING "Flags used by the C compiler during coverage builds." | |
| 184 | + FORCE ) | |
| 185 | +set(CMAKE_EXE_LINKER_FLAGS_COVERAGE | |
| 186 | + "" | |
| 187 | + CACHE STRING "Flags used for linking binaries during coverage builds." | |
| 188 | + FORCE ) | |
| 189 | +set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE | |
| 190 | + "" | |
| 191 | + CACHE STRING "Flags used by the shared libraries linker during coverage builds." | |
| 192 | + FORCE ) | |
| 193 | +mark_as_advanced( | |
| 194 | + CMAKE_Fortran_FLAGS_COVERAGE | |
| 195 | + CMAKE_CXX_FLAGS_COVERAGE | |
| 196 | + CMAKE_C_FLAGS_COVERAGE | |
| 197 | + CMAKE_EXE_LINKER_FLAGS_COVERAGE | |
| 198 | + CMAKE_SHARED_LINKER_FLAGS_COVERAGE ) | |
| 199 | + | |
| 200 | +get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) | |
| 201 | +if(NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG)) | |
| 202 | + message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading") | |
| 203 | +endif() # NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG) | |
| 204 | + | |
| 205 | +if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") | |
| 206 | + link_libraries(gcov) | |
| 207 | +endif() | |
| 208 | + | |
| 209 | +# Defines a target for running and collection code coverage information | |
| 210 | +# Builds dependencies, runs the given executable and outputs reports. | |
| 211 | +# NOTE! The executable should always have a ZERO as exit code otherwise | |
| 212 | +# the coverage generation will not complete. | |
| 213 | +# | |
| 214 | +# setup_target_for_coverage_lcov( | |
| 215 | +# NAME testrunner_coverage # New target name | |
| 216 | +# EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR | |
| 217 | +# DEPENDENCIES testrunner # Dependencies to build first | |
| 218 | +# BASE_DIRECTORY "../" # Base directory for report | |
| 219 | +# # (defaults to PROJECT_SOURCE_DIR) | |
| 220 | +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative | |
| 221 | +# # to BASE_DIRECTORY, with CMake 3.4+) | |
| 222 | +# NO_DEMANGLE # Don't demangle C++ symbols | |
| 223 | +# # even if c++filt is found | |
| 224 | +# ) | |
| 225 | +function(setup_target_for_coverage_lcov) | |
| 226 | + | |
| 227 | + set(options NO_DEMANGLE SONARQUBE) | |
| 228 | + set(oneValueArgs BASE_DIRECTORY NAME) | |
| 229 | + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES LCOV_ARGS GENHTML_ARGS) | |
| 230 | + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) | |
| 231 | + | |
| 232 | + if(NOT LCOV_PATH) | |
| 233 | + message(FATAL_ERROR "lcov not found! Aborting...") | |
| 234 | + endif() # NOT LCOV_PATH | |
| 235 | + | |
| 236 | + if(NOT GENHTML_PATH) | |
| 237 | + message(FATAL_ERROR "genhtml not found! Aborting...") | |
| 238 | + endif() # NOT GENHTML_PATH | |
| 239 | + | |
| 240 | + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR | |
| 241 | + if(DEFINED Coverage_BASE_DIRECTORY) | |
| 242 | + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) | |
| 243 | + else() | |
| 244 | + set(BASEDIR ${PROJECT_SOURCE_DIR}) | |
| 245 | + endif() | |
| 246 | + | |
| 247 | + # Collect excludes (CMake 3.4+: Also compute absolute paths) | |
| 248 | + set(LCOV_EXCLUDES "") | |
| 249 | + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_LCOV_EXCLUDES}) | |
| 250 | + if(CMAKE_VERSION VERSION_GREATER 3.4) | |
| 251 | + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) | |
| 252 | + endif() | |
| 253 | + list(APPEND LCOV_EXCLUDES "${EXCLUDE}") | |
| 254 | + endforeach() | |
| 255 | + list(REMOVE_DUPLICATES LCOV_EXCLUDES) | |
| 256 | + | |
| 257 | + # Conditional arguments | |
| 258 | + if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE}) | |
| 259 | + set(GENHTML_EXTRA_ARGS "--demangle-cpp") | |
| 260 | + endif() | |
| 261 | + | |
| 262 | + # Setting up commands which will be run to generate coverage data. | |
| 263 | + # Cleanup lcov | |
| 264 | + set(LCOV_CLEAN_CMD | |
| 265 | + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -directory . | |
| 266 | + -b ${BASEDIR} --zerocounters | |
| 267 | + ) | |
| 268 | + # Create baseline to make sure untouched files show up in the report | |
| 269 | + set(LCOV_BASELINE_CMD | |
| 270 | + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -c -i -d . -b | |
| 271 | + ${BASEDIR} -o ${Coverage_NAME}.base | |
| 272 | + ) | |
| 273 | + # Run tests | |
| 274 | + set(LCOV_EXEC_TESTS_CMD | |
| 275 | + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} | |
| 276 | + ) | |
| 277 | + # Capturing lcov counters and generating report | |
| 278 | + set(LCOV_CAPTURE_CMD | |
| 279 | + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --directory . -b | |
| 280 | + ${BASEDIR} --capture --output-file ${Coverage_NAME}.capture | |
| 281 | + ) | |
| 282 | + # add baseline counters | |
| 283 | + set(LCOV_BASELINE_COUNT_CMD | |
| 284 | + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -a ${Coverage_NAME}.base | |
| 285 | + -a ${Coverage_NAME}.capture --output-file ${Coverage_NAME}.total | |
| 286 | + ) | |
| 287 | + # filter collected data to final coverage report | |
| 288 | + set(LCOV_FILTER_CMD | |
| 289 | + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --remove | |
| 290 | + ${Coverage_NAME}.total ${LCOV_EXCLUDES} --output-file ${Coverage_NAME}.info | |
| 291 | + ) | |
| 292 | + # Generate HTML output | |
| 293 | + set(LCOV_GEN_HTML_CMD | |
| 294 | + ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS} -o | |
| 295 | + ${Coverage_NAME} ${Coverage_NAME}.info | |
| 296 | + ) | |
| 297 | + if(${Coverage_SONARQUBE}) | |
| 298 | + # Generate SonarQube output | |
| 299 | + set(GCOVR_XML_CMD | |
| 300 | + ${GCOVR_PATH} --sonarqube ${Coverage_NAME}_sonarqube.xml -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} | |
| 301 | + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} | |
| 302 | + ) | |
| 303 | + set(GCOVR_XML_CMD_COMMAND | |
| 304 | + COMMAND ${GCOVR_XML_CMD} | |
| 305 | + ) | |
| 306 | + set(GCOVR_XML_CMD_BYPRODUCTS ${Coverage_NAME}_sonarqube.xml) | |
| 307 | + set(GCOVR_XML_CMD_COMMENT COMMENT "SonarQube code coverage info report saved in ${Coverage_NAME}_sonarqube.xml.") | |
| 308 | + endif() | |
| 309 | + | |
| 310 | + | |
| 311 | + if(CODE_COVERAGE_VERBOSE) | |
| 312 | + message(STATUS "Executed command report") | |
| 313 | + message(STATUS "Command to clean up lcov: ") | |
| 314 | + string(REPLACE ";" " " LCOV_CLEAN_CMD_SPACED "${LCOV_CLEAN_CMD}") | |
| 315 | + message(STATUS "${LCOV_CLEAN_CMD_SPACED}") | |
| 316 | + | |
| 317 | + message(STATUS "Command to create baseline: ") | |
| 318 | + string(REPLACE ";" " " LCOV_BASELINE_CMD_SPACED "${LCOV_BASELINE_CMD}") | |
| 319 | + message(STATUS "${LCOV_BASELINE_CMD_SPACED}") | |
| 320 | + | |
| 321 | + message(STATUS "Command to run the tests: ") | |
| 322 | + string(REPLACE ";" " " LCOV_EXEC_TESTS_CMD_SPACED "${LCOV_EXEC_TESTS_CMD}") | |
| 323 | + message(STATUS "${LCOV_EXEC_TESTS_CMD_SPACED}") | |
| 324 | + | |
| 325 | + message(STATUS "Command to capture counters and generate report: ") | |
| 326 | + string(REPLACE ";" " " LCOV_CAPTURE_CMD_SPACED "${LCOV_CAPTURE_CMD}") | |
| 327 | + message(STATUS "${LCOV_CAPTURE_CMD_SPACED}") | |
| 328 | + | |
| 329 | + message(STATUS "Command to add baseline counters: ") | |
| 330 | + string(REPLACE ";" " " LCOV_BASELINE_COUNT_CMD_SPACED "${LCOV_BASELINE_COUNT_CMD}") | |
| 331 | + message(STATUS "${LCOV_BASELINE_COUNT_CMD_SPACED}") | |
| 332 | + | |
| 333 | + message(STATUS "Command to filter collected data: ") | |
| 334 | + string(REPLACE ";" " " LCOV_FILTER_CMD_SPACED "${LCOV_FILTER_CMD}") | |
| 335 | + message(STATUS "${LCOV_FILTER_CMD_SPACED}") | |
| 336 | + | |
| 337 | + message(STATUS "Command to generate lcov HTML output: ") | |
| 338 | + string(REPLACE ";" " " LCOV_GEN_HTML_CMD_SPACED "${LCOV_GEN_HTML_CMD}") | |
| 339 | + message(STATUS "${LCOV_GEN_HTML_CMD_SPACED}") | |
| 340 | + | |
| 341 | + if(${Coverage_SONARQUBE}) | |
| 342 | + message(STATUS "Command to generate SonarQube XML output: ") | |
| 343 | + string(REPLACE ";" " " GCOVR_XML_CMD_SPACED "${GCOVR_XML_CMD}") | |
| 344 | + message(STATUS "${GCOVR_XML_CMD_SPACED}") | |
| 345 | + endif() | |
| 346 | + endif() | |
| 347 | + | |
| 348 | + # Setup target | |
| 349 | + add_custom_target(${Coverage_NAME} | |
| 350 | + COMMAND ${LCOV_CLEAN_CMD} | |
| 351 | + COMMAND ${LCOV_BASELINE_CMD} | |
| 352 | + COMMAND ${LCOV_EXEC_TESTS_CMD} | |
| 353 | + COMMAND ${LCOV_CAPTURE_CMD} | |
| 354 | + COMMAND ${LCOV_BASELINE_COUNT_CMD} | |
| 355 | + COMMAND ${LCOV_FILTER_CMD} | |
| 356 | + COMMAND ${LCOV_GEN_HTML_CMD} | |
| 357 | + ${GCOVR_XML_CMD_COMMAND} | |
| 358 | + | |
| 359 | + # Set output files as GENERATED (will be removed on 'make clean') | |
| 360 | + BYPRODUCTS | |
| 361 | + ${Coverage_NAME}.base | |
| 362 | + ${Coverage_NAME}.capture | |
| 363 | + ${Coverage_NAME}.total | |
| 364 | + ${Coverage_NAME}.info | |
| 365 | + ${GCOVR_XML_CMD_BYPRODUCTS} | |
| 366 | + ${Coverage_NAME}/index.html | |
| 367 | + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} | |
| 368 | + DEPENDS ${Coverage_DEPENDENCIES} | |
| 369 | + VERBATIM # Protect arguments to commands | |
| 370 | + COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report." | |
| 371 | + ) | |
| 372 | + | |
| 373 | + # Show where to find the lcov info report | |
| 374 | + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD | |
| 375 | + COMMAND ; | |
| 376 | + COMMENT "Lcov code coverage info report saved in ${Coverage_NAME}.info." | |
| 377 | + ${GCOVR_XML_CMD_COMMENT} | |
| 378 | + ) | |
| 379 | + | |
| 380 | + # Show info where to find the report | |
| 381 | + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD | |
| 382 | + COMMAND ; | |
| 383 | + COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report." | |
| 384 | + ) | |
| 385 | + | |
| 386 | +endfunction() # setup_target_for_coverage_lcov | |
| 387 | + | |
| 388 | +# Defines a target for running and collection code coverage information | |
| 389 | +# Builds dependencies, runs the given executable and outputs reports. | |
| 390 | +# NOTE! The executable should always have a ZERO as exit code otherwise | |
| 391 | +# the coverage generation will not complete. | |
| 392 | +# | |
| 393 | +# setup_target_for_coverage_gcovr_xml( | |
| 394 | +# NAME ctest_coverage # New target name | |
| 395 | +# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR | |
| 396 | +# DEPENDENCIES executable_target # Dependencies to build first | |
| 397 | +# BASE_DIRECTORY "../" # Base directory for report | |
| 398 | +# # (defaults to PROJECT_SOURCE_DIR) | |
| 399 | +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative | |
| 400 | +# # to BASE_DIRECTORY, with CMake 3.4+) | |
| 401 | +# ) | |
| 402 | +# The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the | |
| 403 | +# GCVOR command. | |
| 404 | +function(setup_target_for_coverage_gcovr_xml) | |
| 405 | + | |
| 406 | + set(options NONE) | |
| 407 | + set(oneValueArgs BASE_DIRECTORY NAME) | |
| 408 | + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) | |
| 409 | + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) | |
| 410 | + | |
| 411 | + if(NOT GCOVR_PATH) | |
| 412 | + message(FATAL_ERROR "gcovr not found! Aborting...") | |
| 413 | + endif() # NOT GCOVR_PATH | |
| 414 | + | |
| 415 | + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR | |
| 416 | + if(DEFINED Coverage_BASE_DIRECTORY) | |
| 417 | + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) | |
| 418 | + else() | |
| 419 | + set(BASEDIR ${PROJECT_SOURCE_DIR}) | |
| 420 | + endif() | |
| 421 | + | |
| 422 | + # Collect excludes (CMake 3.4+: Also compute absolute paths) | |
| 423 | + set(GCOVR_EXCLUDES "") | |
| 424 | + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES}) | |
| 425 | + if(CMAKE_VERSION VERSION_GREATER 3.4) | |
| 426 | + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) | |
| 427 | + endif() | |
| 428 | + list(APPEND GCOVR_EXCLUDES "${EXCLUDE}") | |
| 429 | + endforeach() | |
| 430 | + list(REMOVE_DUPLICATES GCOVR_EXCLUDES) | |
| 431 | + | |
| 432 | + # Combine excludes to several -e arguments | |
| 433 | + set(GCOVR_EXCLUDE_ARGS "") | |
| 434 | + foreach(EXCLUDE ${GCOVR_EXCLUDES}) | |
| 435 | + list(APPEND GCOVR_EXCLUDE_ARGS "-e") | |
| 436 | + list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}") | |
| 437 | + endforeach() | |
| 438 | + | |
| 439 | + # Set up commands which will be run to generate coverage data | |
| 440 | + # Run tests | |
| 441 | + set(GCOVR_XML_EXEC_TESTS_CMD | |
| 442 | + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} | |
| 443 | + ) | |
| 444 | + # Running gcovr | |
| 445 | + set(GCOVR_XML_CMD | |
| 446 | + ${GCOVR_PATH} --xml ${Coverage_NAME}.xml -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} | |
| 447 | + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} | |
| 448 | + ) | |
| 449 | + | |
| 450 | + if(CODE_COVERAGE_VERBOSE) | |
| 451 | + message(STATUS "Executed command report") | |
| 452 | + | |
| 453 | + message(STATUS "Command to run tests: ") | |
| 454 | + string(REPLACE ";" " " GCOVR_XML_EXEC_TESTS_CMD_SPACED "${GCOVR_XML_EXEC_TESTS_CMD}") | |
| 455 | + message(STATUS "${GCOVR_XML_EXEC_TESTS_CMD_SPACED}") | |
| 456 | + | |
| 457 | + message(STATUS "Command to generate gcovr XML coverage data: ") | |
| 458 | + string(REPLACE ";" " " GCOVR_XML_CMD_SPACED "${GCOVR_XML_CMD}") | |
| 459 | + message(STATUS "${GCOVR_XML_CMD_SPACED}") | |
| 460 | + endif() | |
| 461 | + | |
| 462 | + add_custom_target(${Coverage_NAME} | |
| 463 | + COMMAND ${GCOVR_XML_EXEC_TESTS_CMD} | |
| 464 | + COMMAND ${GCOVR_XML_CMD} | |
| 465 | + | |
| 466 | + BYPRODUCTS ${Coverage_NAME}.xml | |
| 467 | + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} | |
| 468 | + DEPENDS ${Coverage_DEPENDENCIES} | |
| 469 | + VERBATIM # Protect arguments to commands | |
| 470 | + COMMENT "Running gcovr to produce Cobertura code coverage report." | |
| 471 | + ) | |
| 472 | + | |
| 473 | + # Show info where to find the report | |
| 474 | + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD | |
| 475 | + COMMAND ; | |
| 476 | + COMMENT "Cobertura code coverage report saved in ${Coverage_NAME}.xml." | |
| 477 | + ) | |
| 478 | +endfunction() # setup_target_for_coverage_gcovr_xml | |
| 479 | + | |
| 480 | +# Defines a target for running and collection code coverage information | |
| 481 | +# Builds dependencies, runs the given executable and outputs reports. | |
| 482 | +# NOTE! The executable should always have a ZERO as exit code otherwise | |
| 483 | +# the coverage generation will not complete. | |
| 484 | +# | |
| 485 | +# setup_target_for_coverage_gcovr_html( | |
| 486 | +# NAME ctest_coverage # New target name | |
| 487 | +# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR | |
| 488 | +# DEPENDENCIES executable_target # Dependencies to build first | |
| 489 | +# BASE_DIRECTORY "../" # Base directory for report | |
| 490 | +# # (defaults to PROJECT_SOURCE_DIR) | |
| 491 | +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative | |
| 492 | +# # to BASE_DIRECTORY, with CMake 3.4+) | |
| 493 | +# ) | |
| 494 | +# The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the | |
| 495 | +# GCVOR command. | |
| 496 | +function(setup_target_for_coverage_gcovr_html) | |
| 497 | + | |
| 498 | + set(options NONE) | |
| 499 | + set(oneValueArgs BASE_DIRECTORY NAME) | |
| 500 | + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) | |
| 501 | + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) | |
| 502 | + | |
| 503 | + if(NOT GCOVR_PATH) | |
| 504 | + message(FATAL_ERROR "gcovr not found! Aborting...") | |
| 505 | + endif() # NOT GCOVR_PATH | |
| 506 | + | |
| 507 | + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR | |
| 508 | + if(DEFINED Coverage_BASE_DIRECTORY) | |
| 509 | + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) | |
| 510 | + else() | |
| 511 | + set(BASEDIR ${PROJECT_SOURCE_DIR}) | |
| 512 | + endif() | |
| 513 | + | |
| 514 | + # Collect excludes (CMake 3.4+: Also compute absolute paths) | |
| 515 | + set(GCOVR_EXCLUDES "") | |
| 516 | + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES}) | |
| 517 | + if(CMAKE_VERSION VERSION_GREATER 3.4) | |
| 518 | + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) | |
| 519 | + endif() | |
| 520 | + list(APPEND GCOVR_EXCLUDES "${EXCLUDE}") | |
| 521 | + endforeach() | |
| 522 | + list(REMOVE_DUPLICATES GCOVR_EXCLUDES) | |
| 523 | + | |
| 524 | + # Combine excludes to several -e arguments | |
| 525 | + set(GCOVR_EXCLUDE_ARGS "") | |
| 526 | + foreach(EXCLUDE ${GCOVR_EXCLUDES}) | |
| 527 | + list(APPEND GCOVR_EXCLUDE_ARGS "-e") | |
| 528 | + list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}") | |
| 529 | + endforeach() | |
| 530 | + | |
| 531 | + # Set up commands which will be run to generate coverage data | |
| 532 | + # Run tests | |
| 533 | + set(GCOVR_HTML_EXEC_TESTS_CMD | |
| 534 | + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} | |
| 535 | + ) | |
| 536 | + # Create folder | |
| 537 | + set(GCOVR_HTML_FOLDER_CMD | |
| 538 | + ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/${Coverage_NAME} | |
| 539 | + ) | |
| 540 | + # Running gcovr | |
| 541 | + set(GCOVR_HTML_CMD | |
| 542 | + ${GCOVR_PATH} --html ${Coverage_NAME}/index.html --html-details -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} | |
| 543 | + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} | |
| 544 | + ) | |
| 545 | + | |
| 546 | + if(CODE_COVERAGE_VERBOSE) | |
| 547 | + message(STATUS "Executed command report") | |
| 548 | + | |
| 549 | + message(STATUS "Command to run tests: ") | |
| 550 | + string(REPLACE ";" " " GCOVR_HTML_EXEC_TESTS_CMD_SPACED "${GCOVR_HTML_EXEC_TESTS_CMD}") | |
| 551 | + message(STATUS "${GCOVR_HTML_EXEC_TESTS_CMD_SPACED}") | |
| 552 | + | |
| 553 | + message(STATUS "Command to create a folder: ") | |
| 554 | + string(REPLACE ";" " " GCOVR_HTML_FOLDER_CMD_SPACED "${GCOVR_HTML_FOLDER_CMD}") | |
| 555 | + message(STATUS "${GCOVR_HTML_FOLDER_CMD_SPACED}") | |
| 556 | + | |
| 557 | + message(STATUS "Command to generate gcovr HTML coverage data: ") | |
| 558 | + string(REPLACE ";" " " GCOVR_HTML_CMD_SPACED "${GCOVR_HTML_CMD}") | |
| 559 | + message(STATUS "${GCOVR_HTML_CMD_SPACED}") | |
| 560 | + endif() | |
| 561 | + | |
| 562 | + add_custom_target(${Coverage_NAME} | |
| 563 | + COMMAND ${GCOVR_HTML_EXEC_TESTS_CMD} | |
| 564 | + COMMAND ${GCOVR_HTML_FOLDER_CMD} | |
| 565 | + COMMAND ${GCOVR_HTML_CMD} | |
| 566 | + | |
| 567 | + BYPRODUCTS ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html # report directory | |
| 568 | + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} | |
| 569 | + DEPENDS ${Coverage_DEPENDENCIES} | |
| 570 | + VERBATIM # Protect arguments to commands | |
| 571 | + COMMENT "Running gcovr to produce HTML code coverage report." | |
| 572 | + ) | |
| 573 | + | |
| 574 | + # Show info where to find the report | |
| 575 | + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD | |
| 576 | + COMMAND ; | |
| 577 | + COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report." | |
| 578 | + ) | |
| 579 | + | |
| 580 | +endfunction() # setup_target_for_coverage_gcovr_html | |
| 581 | + | |
| 582 | +# Defines a target for running and collection code coverage information | |
| 583 | +# Builds dependencies, runs the given executable and outputs reports. | |
| 584 | +# NOTE! The executable should always have a ZERO as exit code otherwise | |
| 585 | +# the coverage generation will not complete. | |
| 586 | +# | |
| 587 | +# setup_target_for_coverage_fastcov( | |
| 588 | +# NAME testrunner_coverage # New target name | |
| 589 | +# EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR | |
| 590 | +# DEPENDENCIES testrunner # Dependencies to build first | |
| 591 | +# BASE_DIRECTORY "../" # Base directory for report | |
| 592 | +# # (defaults to PROJECT_SOURCE_DIR) | |
| 593 | +# EXCLUDE "src/dir1/" "src/dir2/" # Patterns to exclude. | |
| 594 | +# NO_DEMANGLE # Don't demangle C++ symbols | |
| 595 | +# # even if c++filt is found | |
| 596 | +# SKIP_HTML # Don't create html report | |
| 597 | +# POST_CMD perl -i -pe s!${PROJECT_SOURCE_DIR}/!!g ctest_coverage.json # E.g. for stripping source dir from file paths | |
| 598 | +# ) | |
| 599 | +function(setup_target_for_coverage_fastcov) | |
| 600 | + | |
| 601 | + set(options NO_DEMANGLE SKIP_HTML) | |
| 602 | + set(oneValueArgs BASE_DIRECTORY NAME) | |
| 603 | + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES FASTCOV_ARGS GENHTML_ARGS POST_CMD) | |
| 604 | + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) | |
| 605 | + | |
| 606 | + if(NOT FASTCOV_PATH) | |
| 607 | + message(FATAL_ERROR "fastcov not found! Aborting...") | |
| 608 | + endif() | |
| 609 | + | |
| 610 | + if(NOT Coverage_SKIP_HTML AND NOT GENHTML_PATH) | |
| 611 | + message(FATAL_ERROR "genhtml not found! Aborting...") | |
| 612 | + endif() | |
| 613 | + | |
| 614 | + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR | |
| 615 | + if(Coverage_BASE_DIRECTORY) | |
| 616 | + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) | |
| 617 | + else() | |
| 618 | + set(BASEDIR ${PROJECT_SOURCE_DIR}) | |
| 619 | + endif() | |
| 620 | + | |
| 621 | + # Collect excludes (Patterns, not paths, for fastcov) | |
| 622 | + set(FASTCOV_EXCLUDES "") | |
| 623 | + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_FASTCOV_EXCLUDES}) | |
| 624 | + list(APPEND FASTCOV_EXCLUDES "${EXCLUDE}") | |
| 625 | + endforeach() | |
| 626 | + list(REMOVE_DUPLICATES FASTCOV_EXCLUDES) | |
| 627 | + | |
| 628 | + # Conditional arguments | |
| 629 | + if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE}) | |
| 630 | + set(GENHTML_EXTRA_ARGS "--demangle-cpp") | |
| 631 | + endif() | |
| 632 | + | |
| 633 | + # Set up commands which will be run to generate coverage data | |
| 634 | + set(FASTCOV_EXEC_TESTS_CMD ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}) | |
| 635 | + | |
| 636 | + set(FASTCOV_CAPTURE_CMD ${FASTCOV_PATH} ${Coverage_FASTCOV_ARGS} --gcov ${GCOV_PATH} | |
| 637 | + --search-directory ${BASEDIR} | |
| 638 | + --process-gcno | |
| 639 | + --output ${Coverage_NAME}.json | |
| 640 | + --exclude ${FASTCOV_EXCLUDES} | |
| 641 | + --exclude ${FASTCOV_EXCLUDES} | |
| 642 | + ) | |
| 643 | + | |
| 644 | + set(FASTCOV_CONVERT_CMD ${FASTCOV_PATH} | |
| 645 | + -C ${Coverage_NAME}.json --lcov --output ${Coverage_NAME}.info | |
| 646 | + ) | |
| 647 | + | |
| 648 | + if(Coverage_SKIP_HTML) | |
| 649 | + set(FASTCOV_HTML_CMD ";") | |
| 650 | + else() | |
| 651 | + set(FASTCOV_HTML_CMD ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS} | |
| 652 | + -o ${Coverage_NAME} ${Coverage_NAME}.info | |
| 653 | + ) | |
| 654 | + endif() | |
| 655 | + | |
| 656 | + set(FASTCOV_POST_CMD ";") | |
| 657 | + if(Coverage_POST_CMD) | |
| 658 | + set(FASTCOV_POST_CMD ${Coverage_POST_CMD}) | |
| 659 | + endif() | |
| 660 | + | |
| 661 | + if(CODE_COVERAGE_VERBOSE) | |
| 662 | + message(STATUS "Code coverage commands for target ${Coverage_NAME} (fastcov):") | |
| 663 | + | |
| 664 | + message(" Running tests:") | |
| 665 | + string(REPLACE ";" " " FASTCOV_EXEC_TESTS_CMD_SPACED "${FASTCOV_EXEC_TESTS_CMD}") | |
| 666 | + message(" ${FASTCOV_EXEC_TESTS_CMD_SPACED}") | |
| 667 | + | |
| 668 | + message(" Capturing fastcov counters and generating report:") | |
| 669 | + string(REPLACE ";" " " FASTCOV_CAPTURE_CMD_SPACED "${FASTCOV_CAPTURE_CMD}") | |
| 670 | + message(" ${FASTCOV_CAPTURE_CMD_SPACED}") | |
| 671 | + | |
| 672 | + message(" Converting fastcov .json to lcov .info:") | |
| 673 | + string(REPLACE ";" " " FASTCOV_CONVERT_CMD_SPACED "${FASTCOV_CONVERT_CMD}") | |
| 674 | + message(" ${FASTCOV_CONVERT_CMD_SPACED}") | |
| 675 | + | |
| 676 | + if(NOT Coverage_SKIP_HTML) | |
| 677 | + message(" Generating HTML report: ") | |
| 678 | + string(REPLACE ";" " " FASTCOV_HTML_CMD_SPACED "${FASTCOV_HTML_CMD}") | |
| 679 | + message(" ${FASTCOV_HTML_CMD_SPACED}") | |
| 680 | + endif() | |
| 681 | + if(Coverage_POST_CMD) | |
| 682 | + message(" Running post command: ") | |
| 683 | + string(REPLACE ";" " " FASTCOV_POST_CMD_SPACED "${FASTCOV_POST_CMD}") | |
| 684 | + message(" ${FASTCOV_POST_CMD_SPACED}") | |
| 685 | + endif() | |
| 686 | + endif() | |
| 687 | + | |
| 688 | + # Setup target | |
| 689 | + add_custom_target(${Coverage_NAME} | |
| 690 | + | |
| 691 | + # Cleanup fastcov | |
| 692 | + COMMAND ${FASTCOV_PATH} ${Coverage_FASTCOV_ARGS} --gcov ${GCOV_PATH} | |
| 693 | + --search-directory ${BASEDIR} | |
| 694 | + --zerocounters | |
| 695 | + | |
| 696 | + COMMAND ${FASTCOV_EXEC_TESTS_CMD} | |
| 697 | + COMMAND ${FASTCOV_CAPTURE_CMD} | |
| 698 | + COMMAND ${FASTCOV_CONVERT_CMD} | |
| 699 | + COMMAND ${FASTCOV_HTML_CMD} | |
| 700 | + COMMAND ${FASTCOV_POST_CMD} | |
| 701 | + | |
| 702 | + # Set output files as GENERATED (will be removed on 'make clean') | |
| 703 | + BYPRODUCTS | |
| 704 | + ${Coverage_NAME}.info | |
| 705 | + ${Coverage_NAME}.json | |
| 706 | + ${Coverage_NAME}/index.html # report directory | |
| 707 | + | |
| 708 | + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} | |
| 709 | + DEPENDS ${Coverage_DEPENDENCIES} | |
| 710 | + VERBATIM # Protect arguments to commands | |
| 711 | + COMMENT "Resetting code coverage counters to zero. Processing code coverage counters and generating report." | |
| 712 | + ) | |
| 713 | + | |
| 714 | + set(INFO_MSG "fastcov code coverage info report saved in ${Coverage_NAME}.info and ${Coverage_NAME}.json.") | |
| 715 | + if(NOT Coverage_SKIP_HTML) | |
| 716 | + string(APPEND INFO_MSG " Open ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html in your browser to view the coverage report.") | |
| 717 | + endif() | |
| 718 | + # Show where to find the fastcov info report | |
| 719 | + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD | |
| 720 | + COMMAND ${CMAKE_COMMAND} -E echo ${INFO_MSG} | |
| 721 | + ) | |
| 722 | + | |
| 723 | +endfunction() # setup_target_for_coverage_fastcov | |
| 724 | + | |
| 725 | +function(append_coverage_compiler_flags) | |
| 726 | + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) | |
| 727 | + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) | |
| 728 | + set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) | |
| 729 | + message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}") | |
| 730 | +endfunction() # append_coverage_compiler_flags | |
| 731 | + | |
| 732 | +# Setup coverage for specific library | |
| 733 | +function(append_coverage_compiler_flags_to_target name) | |
| 734 | + target_compile_options(${name} | |
| 735 | + PRIVATE ${COVERAGE_COMPILER_FLAGS}) | |
| 736 | +endfunction() | ... | ... |