1+ name : Bindings - Sysman Python - Unit Tests & Coverage
2+
3+ on :
4+ pull_request :
5+ branches :
6+ - ' **'
7+ paths :
8+ - ' bindings/sysman/python/**.py'
9+ - ' bindings/sysman/python/test/**'
10+ - ' bindings/sysman/python/source/**'
11+ - ' .github/workflows/bindings-sysman-python-unit-tests-coverage.yml'
12+ - ' bindings/sysman/python/pytest.ini'
13+ push :
14+ branches :
15+ - main
16+ - master
17+ paths :
18+ - ' bindings/sysman/python/**.py'
19+ - ' bindings/sysman/python/test/**'
20+ - ' bindings/sysman/python/source/**'
21+ - ' .github/workflows/bindings-sysman-python-unit-tests-coverage.yml'
22+ - ' bindings/sysman/python/pytest.ini'
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+
33+ defaults :
34+ run :
35+ working-directory : bindings/sysman/python
36+
37+ permissions :
38+ contents : read
39+ pull-requests : write # For PR comments
40+ checks : write # For check results
41+
42+ steps :
43+ - name : Checkout code
44+ uses : actions/checkout@v4
45+ with :
46+ fetch-depth : 0 # Full history for better coverage comparison
47+
48+ - name : Set up Python ${{ env.PYTHON_VERSION }}
49+ uses : actions/setup-python@v5
50+ with :
51+ python-version : ${{ env.PYTHON_VERSION }}
52+ cache : ' pip'
53+
54+ - name : Install dependencies
55+ run : |
56+ python -m pip install --upgrade pip
57+ pip install pytest pytest-cov pytest-html pytest-xdist
58+ pip install coverage[toml]
59+ # Linting, formatting, and type checking tools
60+ pip install flake8 black isort mypy
61+
62+ # Install any project-specific dependencies if requirements.txt exists
63+ if [ -f requirements.txt ]; then
64+ pip install -r requirements.txt
65+ fi
66+
67+ - name : Code Quality Checks
68+ run : |
69+ echo "Running code quality checks..."
70+
71+ # Check code formatting with black
72+ echo "Checking code formatting..."
73+ black --check --diff source/ test/ || {
74+ echo "❌ Code formatting issues found. Run 'black source/ test/' to fix."
75+ exit 1
76+ }
77+
78+ # Check import sorting
79+ echo "Checking import sorting..."
80+ isort --check-only --diff source/ test/ || {
81+ echo "❌ Import sorting issues found. Run 'isort source/ test/' to fix."
82+ exit 1
83+ }
84+
85+ # Run linting
86+ echo "Running flake8 linting..."
87+ flake8 source/ test/ || {
88+ echo "❌ Linting issues found. Check flake8 output above."
89+ exit 1
90+ }
91+
92+ # Run type checking
93+ echo "Running mypy type checking..."
94+ mypy source/ --ignore-missing-imports --no-strict-optional || {
95+ echo "❌ Type checking issues found. Check mypy output above."
96+ exit 1
97+ }
98+
99+ echo "✅ All code quality checks passed!"
100+
101+ - name : Run Unit Tests with Coverage
102+ run : |
103+ # Run tests with coverage and generate multiple report formats
104+ python -m pytest test/unit_tests/ \
105+ --cov=source \
106+ --cov-report=term-missing \
107+ --cov-report=html:htmlcov \
108+ --cov-report=xml:coverage.xml \
109+ --cov-report=json:coverage.json \
110+ --junit-xml=test-results.xml \
111+ --html=test-report.html \
112+ --self-contained-html \
113+ -v \
114+ --tb=short \
115+ --durations=10
116+
117+ - name : Extract Coverage Percentage
118+ id : coverage
119+ run : |
120+ # Extract coverage percentage from JSON report
121+ COVERAGE_PCT=$(python -c 'import json; data = json.load(open("coverage.json")); print("{:.1f}".format(data["totals"]["percent_covered"]))')
122+ echo "coverage_pct=$COVERAGE_PCT" >> $GITHUB_OUTPUT
123+ echo "Current Coverage: $COVERAGE_PCT%"
124+
125+ - name : Get Baseline Coverage from Target Branch
126+ id : baseline
127+ run : |
128+ # Get the target branch (base of the PR or parent commit for push events)
129+ if [ "${{ github.event_name }}" == "pull_request" ]; then
130+ TARGET_BRANCH="${{ github.event.pull_request.base.ref }}"
131+ echo "Pull request detected - comparing against base branch: $TARGET_BRANCH"
132+ else
133+ # For push events, compare against the parent commit
134+ TARGET_BRANCH="${{ github.ref_name }}"
135+ echo "Push event detected - comparing against parent commit on branch: $TARGET_BRANCH"
136+ # Use HEAD~1 to get the previous commit
137+ git checkout HEAD~1 2>/dev/null || {
138+ echo "⚠️ No parent commit found (possibly first commit). Using current commit as baseline."
139+ echo "baseline_coverage=${{ steps.coverage.outputs.coverage_pct }}" >> $GITHUB_OUTPUT
140+ exit 0
141+ }
142+ fi
143+
144+ echo "Target branch/commit: $TARGET_BRANCH"
145+
146+ # Checkout target branch/commit to get baseline coverage
147+ if [ "${{ github.event_name }}" == "pull_request" ]; then
148+ git fetch origin $TARGET_BRANCH
149+ git checkout origin/$TARGET_BRANCH
150+ fi
151+
152+ # Install dependencies and run tests to get baseline coverage
153+ python -m pip install --upgrade pip
154+ pip install pytest pytest-cov coverage[toml]
155+
156+ # Run tests with coverage for baseline
157+ python -m pytest test/unit_tests/ \
158+ --cov=source \
159+ --cov-report=json:baseline-coverage.json \
160+ -q || {
161+ if [ "${{ github.event_name }}" == "pull_request" ]; then
162+ echo "❌ CRITICAL: Baseline tests failed on target branch"
163+ echo ""
164+ echo "This indicates the target branch has broken tests."
165+ echo "The PR cannot be properly validated against a broken baseline."
166+ echo ""
167+ echo "Action required:"
168+ echo " 1. Check if there's an ongoing incident with the target branch"
169+ echo " 2. Fix the target branch tests first"
170+ echo " 3. Then rebase and re-run this PR"
171+ else
172+ echo "❌ CRITICAL: Baseline tests failed on parent commit"
173+ echo ""
174+ echo "This indicates the previous commit had broken tests."
175+ echo "The current commit cannot be properly validated."
176+ fi
177+ echo ""
178+ exit 1
179+ }
180+
181+ # Extract baseline coverage
182+ BASELINE_COVERAGE=$(python -c 'import json; data = json.load(open("baseline-coverage.json")); print("{:.1f}".format(data["totals"]["percent_covered"]))')
183+ echo "baseline_coverage=$BASELINE_COVERAGE" >> $GITHUB_OUTPUT
184+ echo "Baseline Coverage: $BASELINE_COVERAGE%"
185+
186+ # Switch back to PR branch
187+ git checkout ${{ github.sha }}
188+
189+ - name : Check Coverage Threshold
190+ run : |
191+ CURRENT_COVERAGE="${{ steps.coverage.outputs.coverage_pct }}"
192+ BASELINE_COVERAGE="${{ steps.baseline.outputs.baseline_coverage }}"
193+
194+ echo "Current Coverage: $CURRENT_COVERAGE%"
195+ echo "Target Branch Coverage: $BASELINE_COVERAGE%"
196+
197+ # Use awk for floating point comparison (more portable than bc)
198+ if [ $(echo "$CURRENT_COVERAGE >= $BASELINE_COVERAGE" | awk '{print ($1 >= $3)}') -eq 1 ]; then
199+ DELTA=$(echo "$CURRENT_COVERAGE - $BASELINE_COVERAGE" | awk '{printf "%.1f", $1 - $3}')
200+ echo "✅ Coverage check passed: $CURRENT_COVERAGE% >= $BASELINE_COVERAGE% (Δ ${DELTA}%)"
201+ else
202+ REGRESSION=$(echo "$BASELINE_COVERAGE - $CURRENT_COVERAGE" | awk '{printf "%.1f", $1 - $3}')
203+ echo "❌ Coverage regression detected!"
204+ echo "Current coverage ($CURRENT_COVERAGE%) is below target branch coverage ($BASELINE_COVERAGE%)"
205+ echo "Regression: -${REGRESSION}%"
206+ echo "This PR would cause coverage to regress from the target branch."
207+ echo "Please add tests to maintain or improve coverage."
208+ exit 1
209+ fi
210+
211+ - name : Coverage Summary
212+ if : always()
213+ run : |
214+ echo "## 📊 Test Coverage Report" >> $GITHUB_STEP_SUMMARY
215+ echo "" >> $GITHUB_STEP_SUMMARY
216+ echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
217+ echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
218+ echo "| Current Coverage | ${{ steps.coverage.outputs.coverage_pct }}% |" >> $GITHUB_STEP_SUMMARY
219+ echo "| Target Branch Coverage | ${{ steps.baseline.outputs.baseline_coverage }}% |" >> $GITHUB_STEP_SUMMARY
220+
221+ THRESHOLD="${{ steps.baseline.outputs.baseline_coverage }}"
222+ if [ $(echo "${{ steps.coverage.outputs.coverage_pct }} >= $THRESHOLD" | awk '{print ($1 >= $3)}') -eq 1 ]; then
223+ echo "| Status | ✅ PASSED |" >> $GITHUB_STEP_SUMMARY
224+ else
225+ echo "| Status | ❌ FAILED |" >> $GITHUB_STEP_SUMMARY
226+ fi
227+ echo "" >> $GITHUB_STEP_SUMMARY
228+
229+ # Add detailed coverage report
230+ echo "### Detailed Coverage Report" >> $GITHUB_STEP_SUMMARY
231+ echo '```' >> $GITHUB_STEP_SUMMARY
232+ python -m coverage report >> $GITHUB_STEP_SUMMARY
233+ echo '```' >> $GITHUB_STEP_SUMMARY
234+
235+ - name : Comment PR with Coverage
236+ if : github.event_name == 'pull_request'
237+ uses : actions/github-script@v7
238+ with :
239+ script : |
240+ const coverage = '${{ steps.coverage.outputs.coverage_pct }}';
241+ const baselineCoverage = '${{ steps.baseline.outputs.baseline_coverage }}';
242+ const passed = parseFloat(coverage) >= parseFloat(baselineCoverage);
243+ const improvement = parseFloat(coverage) - parseFloat(baselineCoverage);
244+
245+ const body = `## 📊 Unit Tests & Coverage Report
246+
247+ ${passed ? '✅' : '❌'} **Coverage:** ${coverage}% vs ${baselineCoverage}% (target branch)
248+
249+ ### Test Results
250+ - **Status:** ${passed ? 'PASSED' : 'FAILED'}
251+ - **Current Coverage:** ${coverage}%
252+ - **Target Branch Coverage:** ${baselineCoverage}%
253+ - **Change:** ${improvement > 0 ? '+' : ''}${improvement.toFixed(1)}%
254+
255+ ${passed ?
256+ (improvement > 0 ?
257+ `🎉 Coverage improved by ${improvement.toFixed(1)}%! Great work on adding tests.` :
258+ '✅ Coverage maintained. No regression detected.') :
259+ '⚠️ Coverage regression detected. This PR would reduce test coverage. Please add more tests.'
260+ }
261+
262+ 📁 Detailed reports are available in the workflow artifacts.
263+ `;
264+
265+ github.rest.issues.createComment({
266+ issue_number: context.issue.number,
267+ owner: context.repo.owner,
268+ repo: context.repo.repo,
269+ body: body
270+ });
271+
272+ - name : Upload Test Results
273+ uses : actions/upload-artifact@v4
274+ if : always()
275+ with :
276+ name : bindings-sysman-python-test-results-${{ github.run_number }}
277+ path : |
278+ test-results.xml
279+ test-report.html
280+ htmlcov/
281+ coverage.xml
282+ coverage.json
283+ retention-days : 30
284+
285+ - name : Upload Coverage Reports
286+ uses : actions/upload-artifact@v4
287+ if : always()
288+ with :
289+ name : bindings-sysman-python-coverage-report-${{ github.run_number }}
290+ path : |
291+ htmlcov/
292+ coverage.xml
293+ coverage.json
294+ retention-days : 30
295+
296+
297+
298+ # Optional: Run tests on multiple Python versions
299+ compatibility-tests :
300+ name : Python ${{ matrix.python-version }} Compatibility
301+ runs-on : ubuntu-latest
302+
303+ defaults :
304+ run :
305+ working-directory : bindings/sysman/python
306+ if : github.event_name == 'pull_request' # Only on PRs to save resources
307+ strategy :
308+ matrix :
309+ python-version : ['3.10']
310+
311+ steps :
312+ - name : Checkout code
313+ uses : actions/checkout@v4
314+
315+ - name : Set up Python ${{ matrix.python-version }}
316+ uses : actions/setup-python@v5
317+ with :
318+ python-version : ${{ matrix.python-version }}
319+ cache : ' pip'
320+
321+ - name : Install dependencies
322+ run : |
323+ python -m pip install --upgrade pip
324+ pip install pytest pytest-cov
325+
326+ - name : Run Unit Tests
327+ run : |
328+ python -m pytest test/unit_tests/ \
329+ --cov=source \
330+ --cov-report=term-missing \
331+ -v \
332+ --tb=short
0 commit comments