1+ # #
2+ # Copyright (C) 2026 Intel Corporation
3+ #
4+ # SPDX-License-Identifier: MIT
5+ #
6+ # #
7+
8+ name : Python Bindings - Unit Tests & Coverage
9+
10+ on :
11+ pull_request :
12+ branches :
13+ - ' **'
14+ paths :
15+ - ' bindings/sysman/python/**'
16+ push :
17+ branches :
18+ - main
19+ - master
20+ - python_bindings
21+ paths :
22+ - ' bindings/sysman/python/**'
23+ workflow_dispatch :
24+
25+ env :
26+ PYTHON_VERSION : ' 3.10'
27+
28+ jobs :
29+ unit-tests-linux :
30+ name : Linux - Unit Tests & Coverage Analysis
31+ runs-on : ubuntu-latest
32+ defaults :
33+ run :
34+ working-directory : bindings/sysman/python
35+ permissions :
36+ contents : read
37+ checks : write # For check results
38+
39+ steps :
40+ - name : Checkout code
41+ uses : actions/checkout@v4
42+ with :
43+ fetch-depth : 0 # Full history for better coverage comparison
44+
45+ - name : Set up Python ${{ env.PYTHON_VERSION }}
46+ uses : actions/setup-python@v5
47+ with :
48+ python-version : ${{ env.PYTHON_VERSION }}
49+ cache : ' pip'
50+
51+ - name : Install dependencies
52+ run : |
53+ python -m pip install --upgrade pip
54+ pip install pytest pytest-cov pytest-html pytest-xdist
55+ pip install coverage[toml]
56+ # Linting, formatting, and type checking tools
57+ pip install flake8 black isort mypy
58+
59+ # Install any project-specific dependencies if requirements.txt exists
60+ if [ -f requirements.txt ]; then
61+ pip install -r requirements.txt
62+ fi
63+
64+ - name : Code Quality Checks
65+ run : |
66+ echo "Running code quality checks..."
67+
68+ # Check code formatting with black
69+ echo "Checking code formatting..."
70+ black --check --diff source/ test/ || {
71+ echo "❌ Code formatting issues found. Run 'black source/ test/' to fix."
72+ exit 1
73+ }
74+
75+ # Check import sorting
76+ echo "Checking import sorting..."
77+ isort --check-only --diff source/ test/ || {
78+ echo "❌ Import sorting issues found. Run 'isort source/ test/' to fix."
79+ exit 1
80+ }
81+
82+ # Run linting
83+ echo "Running flake8 linting..."
84+ flake8 source/ test/ || {
85+ echo "❌ Linting issues found. Check flake8 output above."
86+ exit 1
87+ }
88+
89+ # Run type checking
90+ echo "Running mypy type checking..."
91+ mypy source/ --ignore-missing-imports --no-strict-optional || {
92+ echo "❌ Type checking issues found. Check mypy output above."
93+ exit 1
94+ }
95+
96+ echo "✅ All code quality checks passed!"
97+
98+ - name : Run Unit Tests with Coverage
99+ run : |
100+ # Run tests with coverage and generate multiple report formats
101+ python -m pytest test/unit_tests/ \
102+ --cov=source \
103+ --cov-config=pyproject.toml \
104+ --cov-report=term-missing \
105+ --cov-report=html:htmlcov \
106+ --cov-report=xml:coverage.xml \
107+ --cov-report=json:coverage.json \
108+ --junit-xml=test-results.xml \
109+ --html=test-report.html \
110+ --self-contained-html \
111+ -v \
112+ --tb=short \
113+ --durations=10
114+
115+ - name : Extract Coverage Percentage
116+ id : coverage
117+ run : |
118+ # Extract coverage percentage from JSON report
119+ COVERAGE_PCT=$(python -c 'import json; data = json.load(open("coverage.json")); print("{:.1f}".format(data["totals"]["percent_covered"]))')
120+ echo "coverage_pct=$COVERAGE_PCT" >> $GITHUB_OUTPUT
121+ echo "Current Coverage: $COVERAGE_PCT%"
122+
123+ - name : Get Baseline Coverage from Target Branch
124+ id : baseline
125+ continue-on-error : true
126+ run : |
127+ # ============================================================================
128+ # TEMPORARY: First Baseline Handling
129+ # TODO: Remove the special handling below after first PR merge to master
130+ # Once master has test coverage baseline, this graceful fallback is no longer needed
131+ # ============================================================================
132+ SKIP_BASELINE_ON_MISSING=true # Set to false after first merge
133+
134+ # Get the target branch (base of the PR or parent commit for push events)
135+ if [ "${{ github.event_name }}" == "pull_request" ]; then
136+ TARGET_BRANCH="${{ github.event.pull_request.base.ref }}"
137+ echo "Pull request detected - comparing against base branch: $TARGET_BRANCH"
138+ else
139+ # For push events, compare against the parent commit
140+ TARGET_BRANCH="${{ github.ref_name }}"
141+ echo "Push event detected - comparing against parent commit on branch: $TARGET_BRANCH"
142+ # Use HEAD~1 to get the previous commit
143+ git checkout HEAD~1 2>/dev/null || {
144+ if [ "$SKIP_BASELINE_ON_MISSING" = true ]; then
145+ echo "⚠️ No parent commit found (possibly first commit). Skipping baseline comparison."
146+ echo "baseline_coverage=0" >> $GITHUB_OUTPUT
147+ exit 0
148+ else
149+ echo "❌ No parent commit found"
150+ exit 1
151+ fi
152+ }
153+ fi
154+
155+ echo "Target branch/commit: $TARGET_BRANCH"
156+
157+ # Checkout target branch/commit to get baseline coverage
158+ if [ "${{ github.event_name }}" == "pull_request" ]; then
159+ git fetch origin $TARGET_BRANCH
160+ git checkout origin/$TARGET_BRANCH 2>/dev/null || {
161+ if [ "$SKIP_BASELINE_ON_MISSING" = true ]; then
162+ echo "⚠️ Could not checkout target branch. Skipping baseline comparison."
163+ echo "baseline_coverage=0" >> $GITHUB_OUTPUT
164+ exit 0
165+ else
166+ echo "❌ Could not checkout target branch: $TARGET_BRANCH"
167+ exit 1
168+ fi
169+ }
170+ fi
171+
172+ # Check if tests exist in baseline
173+ if [ ! -d "bindings/sysman/python/test/unit_tests" ]; then
174+ if [ "$SKIP_BASELINE_ON_MISSING" = true ]; then
175+ echo "⚠️ No unit tests found in baseline. This is likely the first commit with tests."
176+ echo "Skipping baseline comparison."
177+ echo "baseline_coverage=0" >> $GITHUB_OUTPUT
178+ exit 0
179+ else
180+ echo "❌ No unit tests found in baseline branch"
181+ exit 1
182+ fi
183+ fi
184+
185+ # Install dependencies and run tests to get baseline coverage
186+ python -m pip install --upgrade pip
187+ pip install pytest pytest-cov coverage[toml]
188+
189+ # Run tests with coverage for baseline
190+ python -m pytest test/unit_tests/ \
191+ --cov=source \
192+ --cov-report=json:baseline-coverage.json \
193+ -q || {
194+ if [ "$SKIP_BASELINE_ON_MISSING" = true ]; then
195+ echo "⚠️ Baseline tests failed or not found. Skipping baseline comparison."
196+ echo "baseline_coverage=0" >> $GITHUB_OUTPUT
197+ exit 0
198+ else
199+ echo "❌ Baseline tests failed on $TARGET_BRANCH"
200+ echo "The target branch has broken tests. Fix the baseline before merging."
201+ exit 1
202+ fi
203+ }
204+
205+ # Extract baseline coverage
206+ BASELINE_COVERAGE=$(python -c 'import json; data = json.load(open("baseline-coverage.json")); print("{:.1f}".format(data["totals"]["percent_covered"]))')
207+ echo "baseline_coverage=$BASELINE_COVERAGE" >> $GITHUB_OUTPUT
208+ echo "Baseline Coverage: $BASELINE_COVERAGE%"
209+
210+ # Switch back to PR branch
211+ git checkout ${{ github.sha }}
212+
213+ - name : Check Coverage Threshold
214+ run : |
215+ CURRENT_COVERAGE="${{ steps.coverage.outputs.coverage_pct }}"
216+ BASELINE_COVERAGE="${{ steps.baseline.outputs.baseline_coverage }}"
217+
218+ echo "Current Coverage: $CURRENT_COVERAGE%"
219+ echo "Baseline Coverage: $BASELINE_COVERAGE%"
220+
221+ # ============================================================================
222+ # TEMPORARY: First Baseline Handling
223+ # TODO: Remove this section after first PR merge to master
224+ # After first merge, baseline should always exist and this check is unnecessary
225+ # ============================================================================
226+ # If baseline is 0, this is the first commit with tests - always pass
227+ if [ "$BASELINE_COVERAGE" == "0" ] || [ -z "$BASELINE_COVERAGE" ]; then
228+ echo "✅ No baseline coverage found (first commit with tests)."
229+ echo "Current coverage: $CURRENT_COVERAGE%"
230+ echo "Establishing baseline for future comparisons."
231+ exit 0
232+ fi
233+ # ============================================================================
234+ # END TEMPORARY SECTION
235+ # ============================================================================
236+
237+ # Use awk for floating point comparison (more portable than bc)
238+ if [ $(echo "$CURRENT_COVERAGE >= $BASELINE_COVERAGE" | awk '{print ($1 >= $3)}') -eq 1 ]; then
239+ DELTA=$(echo "$CURRENT_COVERAGE - $BASELINE_COVERAGE" | awk '{printf "%.1f", $1 - $3}')
240+ echo "✅ Coverage check passed: $CURRENT_COVERAGE% >= $BASELINE_COVERAGE% (Δ ${DELTA}%)"
241+ else
242+ REGRESSION=$(echo "$BASELINE_COVERAGE - $CURRENT_COVERAGE" | awk '{printf "%.1f", $1 - $3}')
243+ echo "❌ Coverage regression detected!"
244+ echo "Current coverage ($CURRENT_COVERAGE%) is below baseline coverage ($BASELINE_COVERAGE%)"
245+ echo "Regression: -${REGRESSION}%"
246+ echo "This would cause coverage to regress from the baseline."
247+ echo "Please add tests to maintain or improve coverage."
248+ exit 1
249+ fi
250+
251+ - name : Coverage Summary
252+ if : always()
253+ run : |
254+ echo "## 📊 Test Coverage Report" >> $GITHUB_STEP_SUMMARY
255+ echo "" >> $GITHUB_STEP_SUMMARY
256+ echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
257+ echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
258+ echo "| Current Coverage | ${{ steps.coverage.outputs.coverage_pct }}% |" >> $GITHUB_STEP_SUMMARY
259+ echo "| Target Branch Coverage | ${{ steps.baseline.outputs.baseline_coverage }}% |" >> $GITHUB_STEP_SUMMARY
260+
261+ CURRENT_COV="${{ steps.coverage.outputs.coverage_pct }}"
262+ BASELINE_COV="${{ steps.baseline.outputs.baseline_coverage }}"
263+
264+ # Use awk for comparison (handles empty/zero baseline gracefully)
265+ if [ -z "$BASELINE_COV" ] || [ "$BASELINE_COV" == "0" ]; then
266+ echo "| Status | ✅ PASSED (Baseline Established) |" >> $GITHUB_STEP_SUMMARY
267+ elif awk "BEGIN {exit !($CURRENT_COV >= $BASELINE_COV)}"; then
268+ echo "| Status | ✅ PASSED |" >> $GITHUB_STEP_SUMMARY
269+ else
270+ echo "| Status | ❌ FAILED |" >> $GITHUB_STEP_SUMMARY
271+ fi
272+ echo "" >> $GITHUB_STEP_SUMMARY
273+
274+ # Add detailed coverage report (only if .coverage file exists)
275+ if [ -f .coverage ]; then
276+ echo "### Detailed Coverage Report" >> $GITHUB_STEP_SUMMARY
277+ echo '```' >> $GITHUB_STEP_SUMMARY
278+ python -m coverage report >> $GITHUB_STEP_SUMMARY || echo "Coverage report generation failed" >> $GITHUB_STEP_SUMMARY
279+ echo '```' >> $GITHUB_STEP_SUMMARY
280+ else
281+ echo "### Detailed Coverage Report" >> $GITHUB_STEP_SUMMARY
282+ echo "Coverage data file not found. Report not available." >> $GITHUB_STEP_SUMMARY
283+ fi
284+
285+ - name : Upload Test Results
286+ uses : actions/upload-artifact@v4
287+ if : always()
288+ with :
289+ name : test-results-${{ github.run_number }}
290+ path : |
291+ test-results.xml
292+ test-report.html
293+ htmlcov/
294+ coverage.xml
295+ coverage.json
296+ retention-days : 30
297+
298+ - name : Upload Coverage Reports
299+ uses : actions/upload-artifact@v4
300+ if : always()
301+ with :
302+ name : coverage-report-${{ github.run_number }}
303+ path : |
304+ htmlcov/
305+ coverage.xml
306+ coverage.json
307+ retention-days : 30
0 commit comments