Skip to content

Commit 93bb9b3

Browse files
Add C++ unit test workflow with GTest/CTest support (#40)
* Add C++ unit test workflow with GTest/CTest support Adds reusable C++ unit test workflows modeled after test-py-pytest.yml: - test-cpp-gtest.yml: Reusable workflow for CMake-based C++ projects - Supports GTest, Catch2, and CTest frameworks - Parallel test execution with cgroup-aware CPU detection - JUnit XML output parsing for structured results - Compiler auto-detection (gcc/clang) - Build error detection and reporting - run-branch-test-cpp.yml: Branch comparison workflow - Framework detection for CMake projects - Smart caching for target branch results (same pattern as pytest) - Full regression analysis integration via regression-test.yml - Discord notification support for regressions - triggers/cpp/: Template trigger files for PRs Output format is compatible with regression-test.yml for full regression matrix analysis including pass/fail/skip/disabled states. * Integrate C++ tests into run-branch-test.yml Instead of creating a separate workflow file, add C++ support directly to the existing run-branch-test.yml: - Add C++ framework detection (CMakeLists.txt with testing, test files) - Add C++ related inputs (cmake-version, cpp-compiler, cpp-build-type, etc.) - Add test-source-cpp and test-target-cpp jobs using test-cpp-gtest.yml - Add compare-cpp and notify-cpp jobs for regression analysis - Add cpp_has_regressions and cpp_regression_count outputs Both pytest and C++ tests run in parallel when detected, using the same regression analysis infrastructure. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d33563d commit 93bb9b3

2 files changed

Lines changed: 720 additions & 5 deletions

File tree

.github/workflows/run-branch-test.yml

Lines changed: 154 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,51 @@ on:
77
description: "Target branch to compare against (e.g., main)."
88
required: true
99
type: string
10+
# Python/pytest options
1011
python-version:
1112
description: "Python version for pytest."
1213
required: false
1314
type: string
1415
default: "3.10"
16+
# C++/CMake options
17+
cmake-version:
18+
description: "CMake version for C++ tests."
19+
required: false
20+
type: string
21+
default: "3.28"
22+
cpp-compiler:
23+
description: "C++ compiler (gcc, clang). Auto-detects if empty."
24+
required: false
25+
type: string
26+
default: ""
27+
cpp-build-type:
28+
description: "CMake build type (Debug, Release, RelWithDebInfo, MinSizeRel)."
29+
required: false
30+
type: string
31+
default: "Release"
32+
cpp-build-dir:
33+
description: "Build directory for C++ projects."
34+
required: false
35+
type: string
36+
default: "build"
37+
cpp-cmake-args:
38+
description: "Additional CMake configuration arguments."
39+
required: false
40+
type: string
41+
default: ""
42+
cpp-test-args:
43+
description: "Additional CTest arguments."
44+
required: false
45+
type: string
46+
default: ""
47+
# Common options
1548
runs_on:
1649
description: "Runner label."
1750
required: false
1851
type: string
1952
default: '["self-hosted", "multithreaded"]'
2053
parallel_workers:
21-
description: "Number of parallel pytest workers. Leave empty for auto-detect (6 for multithreaded, 1 for singlethreaded). Use 'auto' for CPU count (caution: detects host CPUs)."
54+
description: "Number of parallel workers. Leave empty for auto-detect (6 for multithreaded, 1 for singlethreaded). Use 'auto' for CPU count."
2255
required: false
2356
type: string
2457
default: ""
@@ -34,19 +67,25 @@ on:
3467
required: false
3568
outputs:
3669
has_regressions:
37-
description: "Whether regressions were detected"
70+
description: "Whether regressions were detected (pytest)"
3871
value: ${{ jobs.compare.outputs.has_regressions }}
3972
regression_count:
40-
description: "Number of regressions"
73+
description: "Number of regressions (pytest)"
4174
value: ${{ jobs.compare.outputs.regression_count }}
75+
cpp_has_regressions:
76+
description: "Whether regressions were detected (C++)"
77+
value: ${{ jobs.compare-cpp.outputs.has_regressions }}
78+
cpp_regression_count:
79+
description: "Number of regressions (C++)"
80+
value: ${{ jobs.compare-cpp.outputs.regression_count }}
4281

4382
jobs:
4483
# Detect which test frameworks are present
4584
detect-frameworks:
4685
runs-on: ${{ fromJSON(inputs.runs_on) }}
4786
outputs:
4887
has_pytest: ${{ steps.detect.outputs.has_pytest }}
49-
# Future: has_jest, has_xunit, etc.
88+
has_cpp: ${{ steps.detect.outputs.has_cpp }}
5089
steps:
5190
- uses: actions/checkout@v4.2.2
5291
- name: Detect test frameworks
@@ -59,7 +98,25 @@ jobs:
5998
else
6099
echo "has_pytest=false" >> $GITHUB_OUTPUT
61100
fi
62-
# Future: Add jest, xunit, etc. detection
101+
102+
# Detect C++ with CMake and tests
103+
HAS_CPP="false"
104+
if [ -f "CMakeLists.txt" ]; then
105+
# Check for test-related CMake content
106+
if grep -rqE "(enable_testing|add_test|gtest|catch|boost.*test)" CMakeLists.txt 2>/dev/null || \
107+
find . -name "CMakeLists.txt" -exec grep -lE "(enable_testing|add_test|gtest|catch)" {} \; 2>/dev/null | head -1 | grep -q .; then
108+
HAS_CPP="true"
109+
echo "✅ Detected: C++ (CMake with tests)"
110+
fi
111+
fi
112+
# Check for test source files
113+
if [ "$HAS_CPP" = "false" ]; then
114+
if find . \( -name "*_test.cpp" -o -name "*_test.cc" -o -name "test_*.cpp" -o -name "test_*.cc" \) 2>/dev/null | head -1 | grep -q .; then
115+
HAS_CPP="true"
116+
echo "✅ Detected: C++ test files"
117+
fi
118+
fi
119+
echo "has_cpp=$HAS_CPP" >> $GITHUB_OUTPUT
63120
64121
# Test source branch (always fresh, no caching)
65122
test-source:
@@ -616,3 +673,95 @@ jobs:
616673
curl -s -H "Content-Type: application/json" \
617674
-d "{\"content\": \"$(echo -e "$MSG")\"}" \
618675
"$WEBHOOK" || true
676+
677+
# ============================================
678+
# C++ Tests (GTest/CTest)
679+
# ============================================
680+
681+
# Test C++ source branch
682+
test-source-cpp:
683+
needs: detect-frameworks
684+
if: needs.detect-frameworks.outputs.has_cpp == 'true'
685+
uses: ./.github/workflows/test-cpp-gtest.yml
686+
with:
687+
ref: "" # Default checkout = PR branch
688+
cmake-version: ${{ inputs.cmake-version }}
689+
compiler: ${{ inputs.cpp-compiler }}
690+
build-type: ${{ inputs.cpp-build-type }}
691+
build-dir: ${{ inputs.cpp-build-dir }}
692+
cmake-args: ${{ inputs.cpp-cmake-args }}
693+
test-args: ${{ inputs.cpp-test-args }}
694+
runs_on: ${{ inputs.runs_on }}
695+
artifact_name: cpp_source_${{ github.event.pull_request.number || github.run_id }}
696+
parallel_workers: ${{ inputs.parallel_workers }}
697+
698+
# Test C++ target branch
699+
test-target-cpp:
700+
needs: detect-frameworks
701+
if: needs.detect-frameworks.outputs.has_cpp == 'true'
702+
uses: ./.github/workflows/test-cpp-gtest.yml
703+
with:
704+
ref: ${{ inputs.target_branch }}
705+
cmake-version: ${{ inputs.cmake-version }}
706+
compiler: ${{ inputs.cpp-compiler }}
707+
build-type: ${{ inputs.cpp-build-type }}
708+
build-dir: ${{ inputs.cpp-build-dir }}
709+
cmake-args: ${{ inputs.cpp-cmake-args }}
710+
test-args: ${{ inputs.cpp-test-args }}
711+
runs_on: ${{ inputs.runs_on }}
712+
artifact_name: cpp_target_${{ github.event.pull_request.number || github.run_id }}
713+
parallel_workers: ${{ inputs.parallel_workers }}
714+
715+
# Compare C++ results
716+
compare-cpp:
717+
needs: [test-source-cpp, test-target-cpp]
718+
if: always() && needs.test-source-cpp.result == 'success'
719+
uses: ./.github/workflows/regression-test.yml
720+
with:
721+
runs_on: ${{ inputs.runs_on }}
722+
baseline_label: ${{ inputs.target_branch }}
723+
baseline_results_artifact: cpp_target_${{ github.event.pull_request.number || github.run_id }}
724+
baseline_results_filename: test_data.json
725+
current_label: ${{ github.head_ref || github.ref_name }}
726+
current_results_artifact: cpp_source_${{ github.event.pull_request.number || github.run_id }}
727+
current_results_filename: test_data.json
728+
baseline_passed: ${{ needs.test-target-cpp.outputs.passed }}
729+
baseline_total: ${{ needs.test-target-cpp.outputs.total }}
730+
baseline_percentage: ${{ needs.test-target-cpp.outputs.percentage }}
731+
current_passed: ${{ needs.test-source-cpp.outputs.passed }}
732+
current_total: ${{ needs.test-source-cpp.outputs.total }}
733+
current_percentage: ${{ needs.test-source-cpp.outputs.percentage }}
734+
baseline_collection_errors: ${{ needs.test-target-cpp.outputs.collection_errors }}
735+
baseline_no_tests_found: ${{ needs.test-target-cpp.outputs.no_tests_found }}
736+
current_collection_errors: ${{ needs.test-source-cpp.outputs.collection_errors }}
737+
current_no_tests_found: ${{ needs.test-source-cpp.outputs.no_tests_found }}
738+
artifact_name: regression_cpp_${{ github.event.pull_request.number || github.run_id }}
739+
740+
# Notify on C++ regressions
741+
notify-cpp:
742+
needs: [test-source-cpp, test-target-cpp, compare-cpp]
743+
if: |
744+
always() &&
745+
(needs.compare-cpp.outputs.has_regressions == 'true' || needs.compare-cpp.result == 'failure')
746+
runs-on: ${{ fromJSON(inputs.runs_on) }}
747+
steps:
748+
- name: Send notification
749+
env:
750+
WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }}
751+
run: |
752+
if [ -z "$WEBHOOK" ]; then
753+
echo "No Discord webhook configured, skipping notification"
754+
exit 0
755+
fi
756+
757+
MSG="**C++ Test Regression Alert**\n"
758+
MSG+="PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}\n"
759+
MSG+="\`${{ github.head_ref }}\` → \`${{ inputs.target_branch }}\`\n\n"
760+
MSG+="Source: ${{ needs.test-source-cpp.outputs.passed }}/${{ needs.test-source-cpp.outputs.total }}\n"
761+
MSG+="Target: ${{ needs.test-target-cpp.outputs.passed }}/${{ needs.test-target-cpp.outputs.total }}\n"
762+
MSG+="Regressions: ${{ needs.compare-cpp.outputs.regression_count || '?' }}\n\n"
763+
MSG+="[View Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
764+
765+
curl -s -H "Content-Type: application/json" \
766+
-d "{\"content\": \"$(echo -e "$MSG")\"}" \
767+
"$WEBHOOK" || true

0 commit comments

Comments
 (0)