Skip to content

Commit dd72fc7

Browse files
committed
Merge main: combine Rust and C++ test workflow support
Resolved merge conflicts to include both: - Rust/Cargo test support (from this branch) - C++/CMake test support (from main) Both frameworks now run in parallel with pytest when detected.
2 parents 249e70c + 93bb9b3 commit dd72fc7

5 files changed

Lines changed: 1189 additions & 2030 deletions

File tree

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

Lines changed: 177 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,50 @@ 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+
# Rust/cargo options
1517
rust-version:
1618
description: "Rust toolchain version for cargo test."
1719
required: false
1820
type: string
1921
default: "stable"
22+
# C++/CMake options
23+
cmake-version:
24+
description: "CMake version for C++ tests."
25+
required: false
26+
type: string
27+
default: "3.28"
28+
cpp-compiler:
29+
description: "C++ compiler (gcc, clang). Auto-detects if empty."
30+
required: false
31+
type: string
32+
default: ""
33+
cpp-build-type:
34+
description: "CMake build type (Debug, Release, RelWithDebInfo, MinSizeRel)."
35+
required: false
36+
type: string
37+
default: "Release"
38+
cpp-build-dir:
39+
description: "Build directory for C++ projects."
40+
required: false
41+
type: string
42+
default: "build"
43+
cpp-cmake-args:
44+
description: "Additional CMake configuration arguments."
45+
required: false
46+
type: string
47+
default: ""
48+
cpp-test-args:
49+
description: "Additional CTest arguments."
50+
required: false
51+
type: string
52+
default: ""
53+
# Common options
2054
runs_on:
2155
description: "Runner label."
2256
required: false
@@ -27,6 +61,11 @@ on:
2761
required: false
2862
type: string
2963
default: ""
64+
use_target_cache:
65+
description: "Whether to use caching for target branch test results. When false, always runs fresh tests."
66+
required: false
67+
type: boolean
68+
default: false
3069
secrets:
3170
DISCORD_WEBHOOK_URL:
3271
required: false
@@ -45,6 +84,12 @@ on:
4584
regression_count_cargo:
4685
description: "Number of regressions (cargo)"
4786
value: ${{ jobs.compare-cargo.outputs.regression_count }}
87+
cpp_has_regressions:
88+
description: "Whether regressions were detected (C++)"
89+
value: ${{ jobs.compare-cpp.outputs.has_regressions }}
90+
cpp_regression_count:
91+
description: "Number of regressions (C++)"
92+
value: ${{ jobs.compare-cpp.outputs.regression_count }}
4893

4994
jobs:
5095
# Detect which test frameworks are present
@@ -53,6 +98,7 @@ jobs:
5398
outputs:
5499
has_pytest: ${{ steps.detect.outputs.has_pytest }}
55100
has_cargo: ${{ steps.detect.outputs.has_cargo }}
101+
has_cpp: ${{ steps.detect.outputs.has_cpp }}
56102
steps:
57103
- uses: actions/checkout@v4.2.2
58104
- name: Detect test frameworks
@@ -74,6 +120,25 @@ jobs:
74120
echo "has_cargo=false" >> "$GITHUB_OUTPUT"
75121
fi
76122
123+
# Detect C++ with CMake and tests
124+
HAS_CPP="false"
125+
if [ -f "CMakeLists.txt" ]; then
126+
# Check for test-related CMake content
127+
if grep -rqE "(enable_testing|add_test|gtest|catch|boost.*test)" CMakeLists.txt 2>/dev/null || \
128+
find . -name "CMakeLists.txt" -exec grep -lE "(enable_testing|add_test|gtest|catch)" {} \; 2>/dev/null | head -1 | grep -q .; then
129+
HAS_CPP="true"
130+
echo "✅ Detected: C++ (CMake with tests)"
131+
fi
132+
fi
133+
# Check for test source files
134+
if [ "$HAS_CPP" = "false" ]; then
135+
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
136+
HAS_CPP="true"
137+
echo "✅ Detected: C++ test files"
138+
fi
139+
fi
140+
echo "has_cpp=$HAS_CPP" >> "$GITHUB_OUTPUT"
141+
77142
# Test source branch (always fresh, no caching)
78143
test-source:
79144
needs: detect-frameworks
@@ -111,6 +176,7 @@ jobs:
111176
# Define cache keys
112177
- name: Set cache keys
113178
id: cache-keys
179+
if: inputs.use_target_cache
114180
run: |
115181
# Version bump forces cache invalidation when extraction logic changes
116182
CACHE_VERSION="v4"
@@ -122,6 +188,7 @@ jobs:
122188
# Try to restore complete results first
123189
- name: Check for complete cache
124190
id: cache-complete
191+
if: inputs.use_target_cache
125192
uses: actions/cache/restore@v4
126193
with:
127194
path: cached_target
@@ -130,7 +197,7 @@ jobs:
130197
# If no complete cache, check for any pending cache (someone else is running)
131198
- name: Check for pending cache
132199
id: cache-pending
133-
if: steps.cache-complete.outputs.cache-hit != 'true'
200+
if: inputs.use_target_cache && steps.cache-complete.outputs.cache-hit != 'true'
134201
uses: actions/cache/restore@v4
135202
with:
136203
path: cached_pending
@@ -141,7 +208,10 @@ jobs:
141208
- name: Determine initial status
142209
id: initial-status
143210
run: |
144-
if [ "${{ steps.cache-complete.outputs.cache-hit }}" == "true" ]; then
211+
if [ "${{ inputs.use_target_cache }}" != "true" ]; then
212+
echo "status=disabled" >> $GITHUB_OUTPUT
213+
echo "🔄 Cache disabled - will run fresh tests"
214+
elif [ "${{ steps.cache-complete.outputs.cache-hit }}" == "true" ]; then
145215
echo "status=complete" >> $GITHUB_OUTPUT
146216
echo "✅ Found complete cache - will use it"
147217
elif [ "${{ steps.cache-pending.outputs.cache-hit }}" == "true" ]; then
@@ -154,15 +224,15 @@ jobs:
154224
155225
# If cache miss, immediately save a pending marker so others know to wait
156226
- name: Create pending marker
157-
if: steps.initial-status.outputs.status == 'miss'
227+
if: inputs.use_target_cache && steps.initial-status.outputs.status == 'miss'
158228
run: |
159229
mkdir -p cached_pending_marker
160230
echo "pending" > cached_pending_marker/status
161231
echo "started=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> cached_pending_marker/status
162232
echo "run_id=${{ github.run_id }}" >> cached_pending_marker/status
163233
164234
- name: Save pending marker
165-
if: steps.initial-status.outputs.status == 'miss'
235+
if: inputs.use_target_cache && steps.initial-status.outputs.status == 'miss'
166236
uses: actions/cache/save@v4
167237
with:
168238
path: cached_pending_marker
@@ -171,7 +241,7 @@ jobs:
171241
# If pending found, poll for complete cache with exponential backoff
172242
- name: Poll for complete cache
173243
id: poll-cache
174-
if: steps.initial-status.outputs.status == 'pending'
244+
if: inputs.use_target_cache && steps.initial-status.outputs.status == 'pending'
175245
env:
176246
GH_TOKEN: ${{ github.token }}
177247
run: |
@@ -209,7 +279,7 @@ jobs:
209279
# Restore complete cache after polling found it
210280
- name: Restore cache after poll
211281
id: cache-after-poll
212-
if: steps.poll-cache.outputs.found == 'true'
282+
if: inputs.use_target_cache && steps.poll-cache.outputs.found == 'true'
213283
uses: actions/cache/restore@v4
214284
with:
215285
path: cached_target
@@ -218,7 +288,10 @@ jobs:
218288
- name: Determine final status
219289
id: final-status
220290
run: |
221-
if [ "${{ steps.cache-complete.outputs.cache-hit }}" == "true" ]; then
291+
if [ "${{ inputs.use_target_cache }}" != "true" ]; then
292+
echo "cache_hit=false" >> $GITHUB_OUTPUT
293+
echo "🔄 Cache disabled - running fresh tests"
294+
elif [ "${{ steps.cache-complete.outputs.cache-hit }}" == "true" ]; then
222295
echo "cache_hit=true" >> $GITHUB_OUTPUT
223296
echo "✅ Using complete cache (found immediately)"
224297
elif [ "${{ steps.cache-after-poll.outputs.cache-hit }}" == "true" ]; then
@@ -231,15 +304,15 @@ jobs:
231304
232305
- name: Load cached results
233306
id: load-cache
234-
if: steps.final-status.outputs.cache_hit == 'true'
307+
if: inputs.use_target_cache && steps.final-status.outputs.cache_hit == 'true'
235308
run: |
236309
echo "✅ Loading cached target results (skipping test run)"
237310
if [ -f cached_target/outputs.env ]; then
238311
cat cached_target/outputs.env >> $GITHUB_OUTPUT
239312
fi
240313
241314
- name: Upload cached artifact
242-
if: steps.final-status.outputs.cache_hit == 'true'
315+
if: inputs.use_target_cache && steps.final-status.outputs.cache_hit == 'true'
243316
uses: actions/upload-artifact@v4
244317
with:
245318
name: pytest_target_${{ github.event.pull_request.number || github.run_id }}
@@ -480,7 +553,7 @@ jobs:
480553
"
481554
482555
- name: Save results to cache
483-
if: steps.final-status.outputs.cache_hit != 'true'
556+
if: inputs.use_target_cache && steps.final-status.outputs.cache_hit != 'true'
484557
run: |
485558
echo "💾 Saving results to cache..."
486559
mkdir -p cached_target
@@ -515,7 +588,7 @@ jobs:
515588
516589
# Save complete results so other PRs can find it
517590
- name: Upload to cache
518-
if: steps.final-status.outputs.cache_hit != 'true'
591+
if: inputs.use_target_cache && steps.final-status.outputs.cache_hit != 'true'
519592
uses: actions/cache/save@v4
520593
with:
521594
path: cached_target
@@ -644,6 +717,69 @@ jobs:
644717
current_no_tests_found: ${{ needs.test-source-cargo.outputs.no_tests_found }}
645718
artifact_name: regression_cargo_${{ github.event.pull_request.number || github.run_id }}
646719

720+
# ============================================
721+
# C++ Tests (GTest/CTest)
722+
# ============================================
723+
724+
# Test C++ source branch
725+
test-source-cpp:
726+
needs: detect-frameworks
727+
if: needs.detect-frameworks.outputs.has_cpp == 'true'
728+
uses: ./.github/workflows/test-cpp-gtest.yml
729+
with:
730+
ref: "" # Default checkout = PR branch
731+
cmake-version: ${{ inputs.cmake-version }}
732+
compiler: ${{ inputs.cpp-compiler }}
733+
build-type: ${{ inputs.cpp-build-type }}
734+
build-dir: ${{ inputs.cpp-build-dir }}
735+
cmake-args: ${{ inputs.cpp-cmake-args }}
736+
test-args: ${{ inputs.cpp-test-args }}
737+
runs_on: ${{ inputs.runs_on }}
738+
artifact_name: cpp_source_${{ github.event.pull_request.number || github.run_id }}
739+
parallel_workers: ${{ inputs.parallel_workers }}
740+
741+
# Test C++ target branch
742+
test-target-cpp:
743+
needs: detect-frameworks
744+
if: needs.detect-frameworks.outputs.has_cpp == 'true'
745+
uses: ./.github/workflows/test-cpp-gtest.yml
746+
with:
747+
ref: ${{ inputs.target_branch }}
748+
cmake-version: ${{ inputs.cmake-version }}
749+
compiler: ${{ inputs.cpp-compiler }}
750+
build-type: ${{ inputs.cpp-build-type }}
751+
build-dir: ${{ inputs.cpp-build-dir }}
752+
cmake-args: ${{ inputs.cpp-cmake-args }}
753+
test-args: ${{ inputs.cpp-test-args }}
754+
runs_on: ${{ inputs.runs_on }}
755+
artifact_name: cpp_target_${{ github.event.pull_request.number || github.run_id }}
756+
parallel_workers: ${{ inputs.parallel_workers }}
757+
758+
# Compare C++ results
759+
compare-cpp:
760+
needs: [test-source-cpp, test-target-cpp]
761+
if: always() && needs.test-source-cpp.result == 'success'
762+
uses: ./.github/workflows/regression-test.yml
763+
with:
764+
runs_on: ${{ inputs.runs_on }}
765+
baseline_label: ${{ inputs.target_branch }}
766+
baseline_results_artifact: cpp_target_${{ github.event.pull_request.number || github.run_id }}
767+
baseline_results_filename: test_data.json
768+
current_label: ${{ github.head_ref || github.ref_name }}
769+
current_results_artifact: cpp_source_${{ github.event.pull_request.number || github.run_id }}
770+
current_results_filename: test_data.json
771+
baseline_passed: ${{ needs.test-target-cpp.outputs.passed }}
772+
baseline_total: ${{ needs.test-target-cpp.outputs.total }}
773+
baseline_percentage: ${{ needs.test-target-cpp.outputs.percentage }}
774+
current_passed: ${{ needs.test-source-cpp.outputs.passed }}
775+
current_total: ${{ needs.test-source-cpp.outputs.total }}
776+
current_percentage: ${{ needs.test-source-cpp.outputs.percentage }}
777+
baseline_collection_errors: ${{ needs.test-target-cpp.outputs.collection_errors }}
778+
baseline_no_tests_found: ${{ needs.test-target-cpp.outputs.no_tests_found }}
779+
current_collection_errors: ${{ needs.test-source-cpp.outputs.collection_errors }}
780+
current_no_tests_found: ${{ needs.test-source-cpp.outputs.no_tests_found }}
781+
artifact_name: regression_cpp_${{ github.event.pull_request.number || github.run_id }}
782+
647783
# ============== NOTIFICATIONS ==============
648784

649785
# Notify on pytest regressions
@@ -705,3 +841,33 @@ jobs:
705841
curl -s -H "Content-Type: application/json" \
706842
-d "{\"content\": \"$(echo -e "$MSG")\"}" \
707843
"$WEBHOOK" || true
844+
845+
# Notify on C++ regressions
846+
notify-cpp:
847+
needs: [detect-frameworks, test-source-cpp, test-target-cpp, compare-cpp]
848+
if: |
849+
always() &&
850+
needs.detect-frameworks.outputs.has_cpp == 'true' &&
851+
(needs.compare-cpp.outputs.has_regressions == 'true' || needs.compare-cpp.result == 'failure')
852+
runs-on: ${{ fromJSON(inputs.runs_on) }}
853+
steps:
854+
- name: Send notification
855+
env:
856+
WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }}
857+
run: |
858+
if [ -z "$WEBHOOK" ]; then
859+
echo "No Discord webhook configured, skipping notification"
860+
exit 0
861+
fi
862+
863+
MSG="**C++ Test Regression Alert**\n"
864+
MSG+="PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}\n"
865+
MSG+="\`${{ github.head_ref }}\` → \`${{ inputs.target_branch }}\`\n\n"
866+
MSG+="Source: ${{ needs.test-source-cpp.outputs.passed }}/${{ needs.test-source-cpp.outputs.total }}\n"
867+
MSG+="Target: ${{ needs.test-target-cpp.outputs.passed }}/${{ needs.test-target-cpp.outputs.total }}\n"
868+
MSG+="Regressions: ${{ needs.compare-cpp.outputs.regression_count || '?' }}\n\n"
869+
MSG+="[View Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
870+
871+
curl -s -H "Content-Type: application/json" \
872+
-d "{\"content\": \"$(echo -e "$MSG")\"}" \
873+
"$WEBHOOK" || true

0 commit comments

Comments
 (0)