This is an automated email from the ASF dual-hosted git repository. bcall pushed a commit to branch unit-test-coverage in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit d43e14376ca5f0b1f1b1e71605c5a712a07eccdd Author: Bryan Call <[email protected]> AuthorDate: Tue Jan 20 19:24:52 2026 -0800 WIP: Unit test coverage infrastructure Phase 1 progress: - Add dev-coverage CMake preset for gcov instrumentation - Add tools/coverage-report.sh for generating coverage reports - Add src/test_utils/ with MockIOBuffer and TestEventProcessor Part of unit test coverage improvement plan. --- CMakeLists.txt | 3 + CMakePresets.json | 17 ++++ src/test_utils/CMakeLists.txt | 39 ++++++++ src/test_utils/MockIOBuffer.cc | 140 ++++++++++++++++++++++++++++ src/test_utils/MockIOBuffer.h | 146 ++++++++++++++++++++++++++++++ src/test_utils/TestEventProcessor.cc | 73 +++++++++++++++ src/test_utils/TestEventProcessor.h | 115 +++++++++++++++++++++++ tools/coverage-report.sh | 171 +++++++++++++++++++++++++++++++++++ 8 files changed, 704 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7df4f8b63c..817a0cc5b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -741,6 +741,9 @@ add_subdirectory(src/tsutil) add_subdirectory(src/tscore) add_subdirectory(src/records) add_subdirectory(src/iocore) +if(BUILD_TESTING) + add_subdirectory(src/test_utils) +endif() add_subdirectory(src/proxy) add_subdirectory(src/shared) add_subdirectory(src/mgmt/config) diff --git a/CMakePresets.json b/CMakePresets.json index 3fe7e010b8..996cc20af8 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -116,6 +116,23 @@ "description": "Development Presets with ASAN sanitizer", "inherits": ["dev", "asan"] }, + { + "name": "coverage", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "-fprofile-arcs -ftest-coverage -g -O0", + "CMAKE_C_FLAGS": "-fprofile-arcs -ftest-coverage -g -O0", + "CMAKE_EXE_LINKER_FLAGS": "-lgcov --coverage", + "CMAKE_SHARED_LINKER_FLAGS": "-lgcov --coverage" + } + }, + { + "name": "dev-coverage", + "displayName": "dev with coverage", + "description": "Development build with gcov code coverage instrumentation", + "inherits": ["dev", "coverage"], + "binaryDir": "${sourceDir}/build-coverage" + }, { "name": "ci", "displayName": "CI defaults", diff --git a/src/test_utils/CMakeLists.txt b/src/test_utils/CMakeLists.txt new file mode 100644 index 0000000000..339994e283 --- /dev/null +++ b/src/test_utils/CMakeLists.txt @@ -0,0 +1,39 @@ +####################### +# +# Licensed to the Apache Software Foundation (ASF) under one or more contributor license +# agreements. See the NOTICE file distributed with this work for additional information regarding +# copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License +# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing permissions and limitations under +# the License. +# +####################### + +# Test utilities library +# Provides mock objects and test infrastructure for unit testing + +add_library( + ats_test_utils STATIC + MockIOBuffer.cc + TestEventProcessor.cc +) + +target_include_directories( + ats_test_utils + PUBLIC ${PROJECT_SOURCE_DIR}/include + ${PROJECT_SOURCE_DIR}/src +) + +target_link_libraries( + ats_test_utils + PUBLIC ts::inkevent + ts::tscore +) + +add_library(ts::ats_test_utils ALIAS ats_test_utils) diff --git a/src/test_utils/MockIOBuffer.cc b/src/test_utils/MockIOBuffer.cc new file mode 100644 index 0000000000..05ab07d0fd --- /dev/null +++ b/src/test_utils/MockIOBuffer.cc @@ -0,0 +1,140 @@ +/** @file + + Mock IOBuffer implementation + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include "test_utils/MockIOBuffer.h" + +MockIOBuffer::MockIOBuffer(int64_t size_index) +{ + buffer_ = new_MIOBuffer(size_index); + reader_ = buffer_->alloc_reader(); +} + +MockIOBuffer::MockIOBuffer(std::string_view data, int64_t size_index) : MockIOBuffer(size_index) +{ + write(data); +} + +MockIOBuffer::~MockIOBuffer() +{ + if (buffer_) { + free_MIOBuffer(buffer_); + } +} + +MockIOBuffer::MockIOBuffer(MockIOBuffer &&other) noexcept : buffer_(other.buffer_), reader_(other.reader_) +{ + other.buffer_ = nullptr; + other.reader_ = nullptr; +} + +MockIOBuffer & +MockIOBuffer::operator=(MockIOBuffer &&other) noexcept +{ + if (this != &other) { + if (buffer_) { + free_MIOBuffer(buffer_); + } + buffer_ = other.buffer_; + reader_ = other.reader_; + other.buffer_ = nullptr; + other.reader_ = nullptr; + } + return *this; +} + +int64_t +MockIOBuffer::write(std::string_view data) +{ + return write(data.data(), data.size()); +} + +int64_t +MockIOBuffer::write(const void *data, int64_t len) +{ + return buffer_->write(data, len); +} + +IOBufferReader * +MockIOBuffer::reader() +{ + return reader_; +} + +std::string +MockIOBuffer::read_all() +{ + std::string result; + int64_t avail = reader_->read_avail(); + if (avail > 0) { + result.resize(avail); + reader_->read(result.data(), avail); + } + return result; +} + +int64_t +MockIOBuffer::available() const +{ + return reader_->read_avail(); +} + +void +MockIOBuffer::reset() +{ + reader_->consume(reader_->read_avail()); +} + +// MockIOBufferChain implementation + +MockIOBufferChain::MockIOBufferChain(std::string_view data, int64_t block_size) +{ + buffer_ = new_MIOBuffer(BUFFER_SIZE_INDEX_4K); + reader_ = buffer_->alloc_reader(); + + // Write data in chunks to create multiple blocks + size_t offset = 0; + while (offset < data.size()) { + size_t chunk_size = std::min(static_cast<size_t>(block_size), data.size() - offset); + buffer_->write(data.data() + offset, chunk_size); + offset += chunk_size; + } +} + +MockIOBufferChain::~MockIOBufferChain() +{ + if (buffer_) { + free_MIOBuffer(buffer_); + } +} + +IOBufferReader * +MockIOBufferChain::reader() +{ + return reader_; +} + +MIOBuffer * +MockIOBufferChain::buffer() +{ + return buffer_; +} diff --git a/src/test_utils/MockIOBuffer.h b/src/test_utils/MockIOBuffer.h new file mode 100644 index 0000000000..51e6db3008 --- /dev/null +++ b/src/test_utils/MockIOBuffer.h @@ -0,0 +1,146 @@ +/** @file + + Mock IOBuffer utilities for unit testing + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#pragma once + +#include "iocore/eventsystem/IOBuffer.h" +#include <string> +#include <string_view> + +/** + * @class MockIOBuffer + * @brief Helper class for creating and managing IOBuffers in tests + * + * Simplifies the creation and manipulation of IOBuffers for unit testing. + * Handles proper allocation and cleanup. + * + * Usage: + * @code + * MockIOBuffer buf("test data"); + * IOBufferReader *reader = buf.reader(); + * // ... use reader in tests ... + * @endcode + */ +class MockIOBuffer +{ +public: + /** + * Create an empty MockIOBuffer + * @param size_index Buffer size index (default: BUFFER_SIZE_INDEX_4K) + */ + explicit MockIOBuffer(int64_t size_index = BUFFER_SIZE_INDEX_4K); + + /** + * Create a MockIOBuffer with initial data + * @param data Initial data to write to the buffer + * @param size_index Buffer size index (default: BUFFER_SIZE_INDEX_4K) + */ + MockIOBuffer(std::string_view data, int64_t size_index = BUFFER_SIZE_INDEX_4K); + + ~MockIOBuffer(); + + // Non-copyable + MockIOBuffer(const MockIOBuffer &) = delete; + MockIOBuffer &operator=(const MockIOBuffer &) = delete; + + // Movable + MockIOBuffer(MockIOBuffer &&other) noexcept; + MockIOBuffer &operator=(MockIOBuffer &&other) noexcept; + + /** + * Write data to the buffer + * @param data Data to write + * @return Number of bytes written + */ + int64_t write(std::string_view data); + + /** + * Write data to the buffer + * @param data Pointer to data + * @param len Length of data + * @return Number of bytes written + */ + int64_t write(const void *data, int64_t len); + + /** + * Get a reader for this buffer + * @return Pointer to IOBufferReader (owned by the MIOBuffer) + */ + IOBufferReader *reader(); + + /** + * Get the underlying MIOBuffer + * @return Pointer to MIOBuffer + */ + MIOBuffer * + buffer() + { + return buffer_; + } + + /** + * Read all available data as a string + * @return String containing all data in the buffer + */ + std::string read_all(); + + /** + * Get the number of bytes available to read + */ + int64_t available() const; + + /** + * Reset the buffer (clear all data) + */ + void reset(); + +private: + MIOBuffer *buffer_ = nullptr; + IOBufferReader *reader_ = nullptr; +}; + +/** + * @class MockIOBufferChain + * @brief Helper for creating multi-block IOBuffer chains for testing + * + * Useful for testing code that handles data spanning multiple IOBuffer blocks. + */ +class MockIOBufferChain +{ +public: + /** + * Create a chain with data split across multiple blocks + * @param data Data to write + * @param block_size Maximum size per block + */ + MockIOBufferChain(std::string_view data, int64_t block_size); + + ~MockIOBufferChain(); + + IOBufferReader *reader(); + MIOBuffer *buffer(); + +private: + MIOBuffer *buffer_ = nullptr; + IOBufferReader *reader_ = nullptr; +}; diff --git a/src/test_utils/TestEventProcessor.cc b/src/test_utils/TestEventProcessor.cc new file mode 100644 index 0000000000..24db51a495 --- /dev/null +++ b/src/test_utils/TestEventProcessor.cc @@ -0,0 +1,73 @@ +/** @file + + Test Event Processor implementation + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include "test_utils/TestEventProcessor.h" +#include "iocore/eventsystem/IOBuffer.h" +#include "tscore/ink_memory.h" + +TestEventProcessor *TestEventProcessor::instance_ = nullptr; + +TestEventProcessor::TestEventProcessor() +{ + if (instance_ != nullptr) { + // Only one instance allowed at a time + ink_release_assert(!"TestEventProcessor already instantiated"); + } + instance_ = this; +} + +TestEventProcessor::~TestEventProcessor() +{ + if (running_.load()) { + stop(); + } + instance_ = nullptr; +} + +void +TestEventProcessor::start() +{ + if (running_.exchange(true)) { + return; // Already running + } + + // Initialize IOBuffer system + // This is the minimum required for most unit tests + init_buffer_allocators(0); // Use default thread ID +} + +void +TestEventProcessor::stop() +{ + if (!running_.exchange(false)) { + return; // Already stopped + } + + // Cleanup is handled by the IOBuffer system's static destructors +} + +TestEventProcessor * +TestEventProcessor::instance() +{ + return instance_; +} diff --git a/src/test_utils/TestEventProcessor.h b/src/test_utils/TestEventProcessor.h new file mode 100644 index 0000000000..febabd125a --- /dev/null +++ b/src/test_utils/TestEventProcessor.h @@ -0,0 +1,115 @@ +/** @file + + Test Event Processor - Controllable event loop for unit testing + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#pragma once + +#include "iocore/eventsystem/EventSystem.h" +#include "iocore/eventsystem/EThread.h" +#include "iocore/eventsystem/Tasks.h" + +#include <atomic> + +/** + * @class TestEventProcessor + * @brief A simplified event processor for unit testing + * + * This class provides a controllable event loop that can be used in unit tests + * without requiring the full ATS event system infrastructure. It supports: + * + * - Single-threaded event processing + * - Synchronous event dispatch for deterministic testing + * - IOBuffer operations without network I/O + * + * Usage: + * @code + * TestEventProcessor ep; + * ep.start(); + * // ... run tests ... + * ep.stop(); + * @endcode + */ +class TestEventProcessor +{ +public: + TestEventProcessor(); + ~TestEventProcessor(); + + /** + * Start the test event processor + * Initializes minimal infrastructure needed for IOBuffer operations + */ + void start(); + + /** + * Stop the test event processor + * Cleans up all resources + */ + void stop(); + + /** + * Check if the event processor is running + */ + bool + is_running() const + { + return running_.load(); + } + + /** + * Get the event processor singleton + * Ensures only one test event processor is active + */ + static TestEventProcessor *instance(); + +private: + std::atomic<bool> running_{false}; + static TestEventProcessor *instance_; +}; + +/** + * @class TestEventProcessorScope + * @brief RAII wrapper for TestEventProcessor + * + * Automatically starts the event processor on construction and stops on destruction. + * + * Usage: + * @code + * TEST_CASE("my test") { + * TestEventProcessorScope ep_scope; + * // Event processor is now running + * // ... test code ... + * } // Event processor automatically stopped + * @endcode + */ +class TestEventProcessorScope +{ +public: + TestEventProcessorScope() { ep_.start(); } + ~TestEventProcessorScope() { ep_.stop(); } + + TestEventProcessorScope(const TestEventProcessorScope &) = delete; + TestEventProcessorScope &operator=(const TestEventProcessorScope &) = delete; + +private: + TestEventProcessor ep_; +}; diff --git a/tools/coverage-report.sh b/tools/coverage-report.sh new file mode 100755 index 0000000000..23606316ce --- /dev/null +++ b/tools/coverage-report.sh @@ -0,0 +1,171 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Generate code coverage report using gcov/lcov +# +# Usage: ./tools/coverage-report.sh [--html] [--threshold N] +# +# Options: +# --html Generate HTML report (requires genhtml) +# --threshold N Fail if coverage is below N% (default: 0, disabled) +# --clean Clean build directory before building +# --help Show this help message +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +BUILD_DIR="${PROJECT_ROOT}/build-coverage" +COVERAGE_DIR="${PROJECT_ROOT}/coverage-report" + +GENERATE_HTML=0 +THRESHOLD=0 +CLEAN=0 + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --html) + GENERATE_HTML=1 + shift + ;; + --threshold) + THRESHOLD="$2" + shift 2 + ;; + --clean) + CLEAN=1 + shift + ;; + --help) + echo "Usage: $0 [--html] [--threshold N] [--clean]" + echo "" + echo "Options:" + echo " --html Generate HTML report (requires genhtml)" + echo " --threshold N Fail if coverage is below N% (default: 0, disabled)" + echo " --clean Clean build directory before building" + echo " --help Show this help message" + exit 0 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Check for required tools +check_tool() { + if ! command -v "$1" &> /dev/null; then + echo "Error: $1 is required but not installed." + echo "Install with: $2" + exit 1 + fi +} + +check_tool lcov "brew install lcov (macOS) or apt install lcov (Linux)" +if [[ $GENERATE_HTML -eq 1 ]]; then + check_tool genhtml "brew install lcov (macOS) or apt install lcov (Linux)" +fi + +cd "${PROJECT_ROOT}" + +# Clean if requested +if [[ $CLEAN -eq 1 ]] && [[ -d "${BUILD_DIR}" ]]; then + echo "==> Cleaning build directory..." + rm -rf "${BUILD_DIR}" +fi + +# Configure +echo "==> Configuring with coverage preset..." +cmake --preset dev-coverage + +# Build +echo "==> Building..." +cmake --build "${BUILD_DIR}" -j "$(nproc 2>/dev/null || sysctl -n hw.ncpu)" + +# Run tests +echo "==> Running tests..." +ctest --test-dir "${BUILD_DIR}" --output-on-failure || true + +# Capture coverage data +echo "==> Capturing coverage data..." +lcov --capture \ + --directory "${BUILD_DIR}" \ + --output-file "${BUILD_DIR}/coverage.info" \ + --ignore-errors mismatch + +# Remove system headers and test files from coverage +echo "==> Filtering coverage data..." +lcov --remove "${BUILD_DIR}/coverage.info" \ + '/usr/*' \ + '/opt/*' \ + '*/unit_tests/*' \ + '*/test_*' \ + '*/lib/Catch2/*' \ + '*/lib/yamlcpp/*' \ + '*/lib/swoc/*' \ + --output-file "${BUILD_DIR}/coverage-filtered.info" \ + --ignore-errors unused + +# Generate summary +echo "" +echo "==> Coverage Summary:" +lcov --summary "${BUILD_DIR}/coverage-filtered.info" 2>&1 | tee "${BUILD_DIR}/coverage-summary.txt" + +# Extract coverage percentage +COVERAGE=$(lcov --summary "${BUILD_DIR}/coverage-filtered.info" 2>&1 | grep "lines" | grep -oP '\d+\.\d+%' | head -1 | tr -d '%') + +if [[ -z "$COVERAGE" ]]; then + # Try alternative parsing for different lcov versions + COVERAGE=$(lcov --summary "${BUILD_DIR}/coverage-filtered.info" 2>&1 | grep -oE '[0-9]+\.[0-9]+%' | head -1 | tr -d '%') +fi + +echo "" +echo "Line coverage: ${COVERAGE:-unknown}%" + +# Generate HTML report if requested +if [[ $GENERATE_HTML -eq 1 ]]; then + echo "==> Generating HTML report..." + rm -rf "${COVERAGE_DIR}" + genhtml "${BUILD_DIR}/coverage-filtered.info" \ + --output-directory "${COVERAGE_DIR}" \ + --title "ATS Code Coverage" \ + --legend \ + --show-details + echo "" + echo "HTML report generated at: ${COVERAGE_DIR}/index.html" +fi + +# Check threshold +if [[ $THRESHOLD -gt 0 ]] && [[ -n "$COVERAGE" ]]; then + COVERAGE_INT=${COVERAGE%.*} + if [[ $COVERAGE_INT -lt $THRESHOLD ]]; then + echo "" + echo "ERROR: Coverage ${COVERAGE}% is below threshold ${THRESHOLD}%" + exit 1 + fi + echo "Coverage ${COVERAGE}% meets threshold ${THRESHOLD}%" +fi + +echo "" +echo "==> Coverage report complete!" +echo "Raw coverage data: ${BUILD_DIR}/coverage.info" +echo "Filtered coverage: ${BUILD_DIR}/coverage-filtered.info" +echo "Summary: ${BUILD_DIR}/coverage-summary.txt"
