You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by bc...@apache.org on 2026/01/21 03:32:57 UTC
(trafficserver) 01/02: WIP: Unit test coverage infrastructure
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 <bc...@apache.org>
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"