Skip to content

Commit cfd36ed

Browse files
committed
feat(ci): Add GitHub Actions CI workflow with code coverage support
Add continuous integration infrastructure for automated testing and coverage: GitHub Actions workflow (.github/workflows/ci.yml): - Triggers on push and pull request to main branch - Concurrency control to cancel duplicate runs - Caches CMake dependencies and ccache for faster builds - Installs build dependencies (cmake, libeigen3-dev, lcov) - Builds with coverage enabled using GCC - Runs test suite and generates lcov coverage report - Uploads coverage to Codecov integration - Saves coverage HTML report and build logs as artifacts CMake coverage support (CMakeLists.txt): - Add ENABLE_COVERAGE option for coverage instrumentation - Configure --coverage flags for GCC/Clang compilers - Add coverage custom target to run tests and generate HTML report - Add coverage-clean target to remove coverage data Makefile convenience targets: - coverage-build: Configure and build with coverage enabled - coverage: Full coverage workflow (build, test, generate report) - coverage-clean: Remove coverage artifacts
1 parent c6c8dab commit cfd36ed

46 files changed

Lines changed: 30469 additions & 7 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
linux-build-test-coverage:
15+
name: Linux Build, Test and Coverage
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- name: Checkout code
20+
uses: actions/checkout@v4
21+
22+
- name: Cache CMake dependencies
23+
uses: actions/cache@v4
24+
with:
25+
path: build/_deps
26+
key: ${{ runner.os }}-cmake-deps-${{ hashFiles('CMakeLists.txt', 'tests/CMakeLists.txt') }}
27+
restore-keys: |
28+
${{ runner.os }}-cmake-deps-
29+
30+
- name: Cache ccache
31+
uses: actions/cache@v4
32+
with:
33+
path: ~/.cache/ccache
34+
key: ${{ runner.os }}-ccache-${{ github.sha }}
35+
restore-keys: |
36+
${{ runner.os }}-ccache-
37+
38+
- name: Install dependencies
39+
run: |
40+
sudo apt-get update
41+
sudo apt-get install -y \
42+
cmake \
43+
build-essential \
44+
libeigen3-dev \
45+
lcov \
46+
ccache
47+
48+
- name: Setup ccache
49+
run: |
50+
ccache --set-config=max_size=500M
51+
ccache --zero-stats
52+
53+
- name: Configure CMake with coverage
54+
env:
55+
CC: ccache gcc
56+
CXX: ccache g++
57+
run: |
58+
cmake -B build \
59+
-DCMAKE_BUILD_TYPE=Debug \
60+
-DBUILD_TESTING=ON \
61+
-DENABLE_COVERAGE=ON
62+
63+
- name: Build
64+
run: cmake --build build --parallel
65+
66+
- name: Show ccache statistics
67+
run: ccache --show-stats
68+
69+
- name: Run tests
70+
run: ctest --test-dir build --output-on-failure --parallel
71+
continue-on-error: true
72+
73+
- name: Generate coverage report
74+
run: |
75+
mkdir -p build/coverage
76+
lcov --directory build --capture --output-file build/coverage/coverage.info --ignore-errors source,gcov
77+
lcov --extract build/coverage/coverage.info "${PWD}/src/*" --output-file build/coverage/coverage_filtered.info --ignore-errors unused
78+
genhtml build/coverage/coverage_filtered.info --output-directory build/coverage/html --ignore-errors source
79+
80+
- name: Upload coverage to Codecov
81+
uses: codecov/codecov-action@v4
82+
with:
83+
token: ${{ secrets.CODECOV_TOKEN }}
84+
files: ./build/coverage/coverage_filtered.info
85+
flags: unittests
86+
name: codecov-libsonare
87+
fail_ci_if_error: false
88+
verbose: true
89+
90+
- name: Upload coverage HTML report
91+
uses: actions/upload-artifact@v4
92+
with:
93+
name: coverage-report
94+
path: build/coverage/html/
95+
96+
- name: Upload build artifacts on failure
97+
if: failure()
98+
uses: actions/upload-artifact@v4
99+
with:
100+
name: build-logs
101+
path: |
102+
build/CMakeFiles/*.log
103+
build/Testing/Temporary/

.gitignore

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,13 @@ analysis.json
6666
# Dictionary cleanup artifacts
6767
data
6868

69-
# Dependencies
70-
third_party/*
71-
!third_party/CMakeLists.txt
69+
# Dependencies (keep required libraries)
70+
!third_party/
71+
third_party/**/build/
72+
third_party/**/.git/
73+
third_party/r8brain/bench/
74+
third_party/r8brain/DLL/
75+
third_party/r8brain/other/
7276

7377
# Node.js
7478
node_modules/

CMakeLists.txt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ option(BUILD_TESTING "Build tests" ON)
99
option(BUILD_WASM "Build for WebAssembly" OFF)
1010
option(BUILD_SHARED "Build shared library" OFF)
1111
option(BUILD_CLI "Build CLI tool" ON)
12+
option(ENABLE_COVERAGE "Enable code coverage" OFF)
1213

1314
# 出力ディレクトリ
1415
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
@@ -25,6 +26,19 @@ else()
2526
add_compile_options(-Wall -Wextra -Wpedantic -Werror)
2627
endif()
2728

29+
# Coverage settings
30+
if(ENABLE_COVERAGE)
31+
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
32+
message(STATUS "Enabling code coverage")
33+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage -O0 -g")
34+
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --coverage -O0 -g")
35+
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
36+
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --coverage")
37+
else()
38+
message(WARNING "Code coverage is only supported with GCC or Clang")
39+
endif()
40+
endif()
41+
2842
# Eigen3
2943
if(BUILD_WASM)
3044
# For WASM, use FetchContent to get Eigen3
@@ -51,3 +65,34 @@ endif()
5165
if(BUILD_CLI AND NOT BUILD_WASM)
5266
add_subdirectory(tools)
5367
endif()
68+
69+
# Coverage target
70+
if(ENABLE_COVERAGE)
71+
find_program(LCOV_PATH lcov)
72+
find_program(GENHTML_PATH genhtml)
73+
if(LCOV_PATH AND GENHTML_PATH)
74+
add_custom_target(coverage
75+
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/coverage
76+
COMMAND ${LCOV_PATH} --directory . --zerocounters
77+
COMMAND ctest --output-on-failure --parallel
78+
COMMAND ${LCOV_PATH} --directory . --capture --output-file ${CMAKE_BINARY_DIR}/coverage/coverage.info
79+
COMMAND ${LCOV_PATH} --extract ${CMAKE_BINARY_DIR}/coverage/coverage.info
80+
'${CMAKE_SOURCE_DIR}/src/*'
81+
--output-file ${CMAKE_BINARY_DIR}/coverage/coverage_filtered.info
82+
COMMAND ${GENHTML_PATH} ${CMAKE_BINARY_DIR}/coverage/coverage_filtered.info
83+
--output-directory ${CMAKE_BINARY_DIR}/coverage/html
84+
COMMAND ${CMAKE_COMMAND} -E echo "Coverage report generated: ${CMAKE_BINARY_DIR}/coverage/html/index.html"
85+
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
86+
COMMENT "Generating code coverage report"
87+
)
88+
add_custom_target(coverage-clean
89+
COMMAND find . -name '*.gcda' -delete
90+
COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/coverage
91+
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
92+
COMMENT "Cleaning coverage data"
93+
)
94+
else()
95+
message(WARNING "lcov and genhtml not found. Coverage report target will not be available.")
96+
message(WARNING "Install with: sudo apt-get install lcov (Linux)")
97+
endif()
98+
endif()

Makefile

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: all build test clean rebuild format wasm
1+
.PHONY: all build test clean rebuild format wasm coverage coverage-build coverage-clean
22

33
BUILD_DIR := build
44

@@ -26,3 +26,21 @@ rebuild: clean build
2626

2727
format:
2828
find src tests -name '*.cpp' -o -name '*.h' | xargs clang-format -i
29+
30+
# Coverage targets
31+
coverage-build:
32+
cmake -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON
33+
cmake --build $(BUILD_DIR) -j
34+
35+
coverage: coverage-build
36+
@mkdir -p $(BUILD_DIR)/coverage
37+
cd $(BUILD_DIR) && lcov --directory . --zerocounters
38+
-cd $(BUILD_DIR) && ctest --output-on-failure --parallel
39+
cd $(BUILD_DIR) && lcov --directory . --capture --output-file coverage/coverage.info
40+
cd $(BUILD_DIR) && lcov --extract coverage/coverage.info '$(CURDIR)/src/*' --output-file coverage/coverage_filtered.info
41+
cd $(BUILD_DIR) && genhtml coverage/coverage_filtered.info --output-directory coverage/html
42+
@echo "Coverage report: $(BUILD_DIR)/coverage/html/index.html"
43+
44+
coverage-clean:
45+
find $(BUILD_DIR) -name '*.gcda' -delete 2>/dev/null || true
46+
rm -rf $(BUILD_DIR)/coverage

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# libsonare
22

3+
[![CI](https://img.shields.io/github/actions/workflow/status/libraz/libsonare/ci.yml?branch=main&label=CI)](https://github.com/libraz/libsonare/actions)
4+
[![codecov](https://codecov.io/gh/libraz/libsonare/branch/main/graph/badge.svg)](https://codecov.io/gh/libraz/libsonare)
35
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/libraz/libsonare/blob/master/LICENSE)
46
[![C++17](https://img.shields.io/badge/C%2B%2B-17-blue?logo=c%2B%2B)](https://en.cppreference.com/w/cpp/17)
57
[![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20WebAssembly-lightgrey)](https://github.com/libraz/libsonare)

README_ja.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# libsonare
22

3+
[![CI](https://img.shields.io/github/actions/workflow/status/libraz/libsonare/ci.yml?branch=main&label=CI)](https://github.com/libraz/libsonare/actions)
4+
[![codecov](https://codecov.io/gh/libraz/libsonare/branch/main/graph/badge.svg)](https://codecov.io/gh/libraz/libsonare)
35
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/libraz/libsonare/blob/master/LICENSE)
46
[![C++17](https://img.shields.io/badge/C%2B%2B-17-blue?logo=c%2B%2B)](https://en.cppreference.com/w/cpp/17)
57
[![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20WebAssembly-lightgrey)](https://github.com/libraz/libsonare)

src/analysis/chord_analyzer.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,10 @@ ChordAnalyzer::ChordMatch ChordAnalyzer::find_best_chord_with_confidence(const f
132132
ChordMatch result;
133133
if (best_tetrad_corr > best_triad_corr + chord_constants::kTetradThreshold) {
134134
result.index = best_tetrad_idx;
135-
result.confidence = best_tetrad_corr;
135+
result.confidence = std::min(1.0f, std::max(0.0f, best_tetrad_corr));
136136
} else {
137137
result.index = best_triad_idx;
138-
result.confidence = best_triad_corr;
138+
result.confidence = std::min(1.0f, std::max(0.0f, best_triad_corr));
139139
}
140140
return result;
141141
}

tests/cli/cli_test.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ void create_test_wav(const std::string& path, float duration = 3.0f, float frequ
3939
save_wav(path, samples, sample_rate);
4040
}
4141

42+
/// @brief Custom deleter for FILE* using pclose.
43+
struct PipeDeleter {
44+
void operator()(FILE* fp) const {
45+
if (fp) pclose(fp);
46+
}
47+
};
48+
4249
/// @brief Executes a shell command and returns output.
4350
/// @param cmd Command to execute
4451
/// @return Pair of (exit_code, output)
@@ -48,7 +55,7 @@ std::pair<int, std::string> exec_command(const std::string& cmd) {
4855

4956
// Redirect stderr to stdout
5057
std::string full_cmd = cmd + " 2>&1";
51-
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(full_cmd.c_str(), "r"), pclose);
58+
std::unique_ptr<FILE, PipeDeleter> pipe(popen(full_cmd.c_str(), "r"));
5259
if (!pipe) {
5360
return {-1, "popen failed"};
5461
}
@@ -379,6 +386,7 @@ TEST_CASE("CLI pitch command", "[cli]") {
379386

380387
SECTION("json output") {
381388
auto [code, output] = exec_command(CLI + " pitch " + TEST_WAV + " --json -q");
389+
INFO("CLI output: " << output);
382390
REQUIRE(code == 0);
383391
REQUIRE_THAT(output, ContainsSubstring("\"algorithm\""));
384392
REQUIRE_THAT(output, ContainsSubstring("\"n_frames\""));

0 commit comments

Comments
 (0)