Skip to content

Commit e3749a1

Browse files
authored
feature: Add support for sysman python bindings (#420)
Signed-off-by: shubham kumar <shubham.kumar@intel.com>
1 parent a6208a3 commit e3749a1

21 files changed

Lines changed: 4811 additions & 0 deletions
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
##
2+
# Copyright (C) 2026 Intel Corporation
3+
#
4+
# SPDX-License-Identifier: MIT
5+
#
6+
##
7+
8+
name: Python Bindings - Security Scan
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+
jobs:
26+
bandit:
27+
name: Bandit Security Analysis
28+
runs-on: ubuntu-latest
29+
defaults:
30+
run:
31+
working-directory: bindings/sysman/python
32+
permissions:
33+
contents: read
34+
35+
steps:
36+
- name: Checkout code
37+
uses: actions/checkout@v4
38+
39+
- name: Set up Python
40+
uses: actions/setup-python@v5
41+
with:
42+
python-version: '3.x'
43+
44+
- name: Install Bandit
45+
run: |
46+
python -m pip install --upgrade pip
47+
pip install bandit
48+
49+
- name: Run Bandit security scan
50+
run: |
51+
bandit -r source/ test/ -f json -o bandit-report.json
52+
bandit -r source/ test/ -ll -f screen
53+
54+
- name: Upload Bandit results as artifact
55+
uses: actions/upload-artifact@v4
56+
if: always()
57+
with:
58+
name: bandit-security-report
59+
path: bandit-report.json
60+
retention-days: 30
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
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

bindings/sysman/python/.flake8

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[flake8]
2+
max-line-length = 140
3+
extend-ignore =
4+
# F403: 'from module import *' used; unable to detect undefined names
5+
# F405: name may be undefined, or defined from star imports
6+
# Acceptable for ctypes bindings where wildcard imports are idiomatic
7+
F403,
8+
F405,
9+
# E266: too many leading '#' for block comment
10+
# Used for visual section separators
11+
E266
12+
per-file-ignores =
13+
# Allow module-level imports not at top of file in examples
14+
source/examples/pyzes_example.py:E402

0 commit comments

Comments
 (0)