Skip to content

Commit 379bfb2

Browse files
committed
Enforce 90 percent Rust coverage
Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent 590d5c7 commit 379bfb2

62 files changed

Lines changed: 1271 additions & 4231 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci-rust-python-package.yaml

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,13 @@ jobs:
122122
working-directory: plugins/rust/python-package/${{ matrix.plugin }}
123123
run: make sync
124124

125+
- name: Plugin CI build verification
126+
if: matrix.os == 'ubuntu-latest'
127+
working-directory: plugins/rust/python-package/${{ matrix.plugin }}
128+
run: make ci-build
129+
125130
- name: Plugin CI verification
131+
if: matrix.os != 'ubuntu-latest'
126132
working-directory: plugins/rust/python-package/${{ matrix.plugin }}
127133
run: make ci
128134

@@ -163,22 +169,41 @@ jobs:
163169
rustup component add llvm-tools-preview
164170
cargo install cargo-llvm-cov --version 0.8.4 --locked
165171
172+
- name: Install Python build tooling
173+
run: python -m pip install uv==0.9.30 maturin==1.12.6
174+
166175
- name: Generate Rust coverage report
167176
env:
168177
CARGO_PACKAGES: ${{ needs.validate-and-detect.outputs.cargo_packages }}
178+
PLUGINS: ${{ needs.validate-and-detect.outputs.plugins }}
179+
PYO3_PYTHON: python
169180
run: |
170181
mkdir -p coverage
171182
mapfile -t cargo_packages < <(python3 -c 'import json, os; [print(package) for package in json.loads(os.environ["CARGO_PACKAGES"])]')
183+
mapfile -t plugins < <(python3 -c 'import json, os; [print(plugin) for plugin in json.loads(os.environ["PLUGINS"])]')
172184
cargo_args=()
173185
for package in "${cargo_packages[@]}"; do
174186
cargo_args+=("-p" "${package}")
175187
done
176-
cargo llvm-cov "${cargo_args[@]}" --cobertura --output-path coverage/cobertura.xml
188+
cargo llvm-cov clean --workspace
189+
eval "$(cargo llvm-cov show-env --sh)"
190+
export CARGO_TARGET_DIR="${CARGO_LLVM_COV_TARGET_DIR}/llvm-cov-target"
191+
export CARGO_LLVM_COV_BUILD_DIR="${CARGO_TARGET_DIR}"
192+
export LLVM_PROFILE_FILE="${CARGO_TARGET_DIR}/cpex-plugins-%p-%10m.profraw"
193+
mkdir -p "${CARGO_TARGET_DIR}"
194+
for plugin in "${plugins[@]}"; do
195+
(cd "plugins/rust/python-package/${plugin}" && make sync && uv run maturin develop)
196+
done
197+
cargo test "${cargo_args[@]}"
198+
for plugin in "${plugins[@]}"; do
199+
(cd "plugins/rust/python-package/${plugin}" && make test-integration)
200+
done
201+
env -u CARGO_TARGET_DIR -u CARGO_LLVM_COV_BUILD_DIR -u CARGO_LLVM_COV_TARGET_DIR -u LLVM_PROFILE_FILE cargo llvm-cov report "${cargo_args[@]}" --cobertura --output-path coverage/cobertura.xml
177202
178203
- name: Enforce per-plugin coverage floor
179204
env:
180205
PLUGINS: ${{ needs.validate-and-detect.outputs.plugins }}
181-
run: python3 tools/plugin_catalog.py coverage-check . coverage/cobertura.xml 50.00 "${PLUGINS}"
206+
run: python3 tools/plugin_catalog.py coverage-check . coverage/cobertura.xml 90.00 "${PLUGINS}"
182207

183208
- name: Upload coverage to Codecov
184209
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238

.github/workflows/release-rust-python-package.yaml

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,19 @@ jobs:
197197
if [[ ! -f "${venv_python}" ]]; then
198198
venv_python="${tmpdir}/venv/Scripts/python.exe"
199199
fi
200-
"${venv_python}" -m pip install dist/*.whl pytest pytest-asyncio
201-
if [[ -d "${GITHUB_WORKSPACE}/${{ needs.resolve.outputs.plugin_path }}/tests" ]]; then
202-
cp -R "${GITHUB_WORKSPACE}/${{ needs.resolve.outputs.plugin_path }}/tests" "${tmpdir}/tests"
203-
printf "[pytest]\npythonpath = tests\nasyncio_mode = auto\n" > "${tmpdir}/pytest.ini"
200+
"${venv_python}" -m pip install dist/*.whl pytest pytest-asyncio PyYAML
201+
if [[ -d "${GITHUB_WORKSPACE}/plugins/tests/${{ needs.resolve.outputs.slug }}" ]]; then
202+
mkdir -p "${tmpdir}/tests"
203+
cp -R "${GITHUB_WORKSPACE}/plugins/tests/${{ needs.resolve.outputs.slug }}" "${tmpdir}/tests/${{ needs.resolve.outputs.slug }}"
204+
cp "${GITHUB_WORKSPACE}/plugins/tests/conftest.py" "${tmpdir}/tests/conftest.py"
205+
cp "${GITHUB_WORKSPACE}/plugins/tests/plugin_hooks.py" "${tmpdir}/tests/plugin_hooks.py"
206+
cp "${GITHUB_WORKSPACE}/plugins/tests/pytest.ini" "${tmpdir}/pytest.ini"
204207
cd "${tmpdir}"
208+
export CPEX_TEST_PLUGIN_HOOKS=1
209+
export PYTHONPATH="${tmpdir}/tests"
205210
"${venv_python}" -m pytest \
206211
-c "${tmpdir}/pytest.ini" \
207-
"${tmpdir}/tests" -v
212+
"${tmpdir}/tests/${{ needs.resolve.outputs.slug }}" -v
208213
fi
209214
210215
build-sdist:
@@ -244,14 +249,19 @@ jobs:
244249
if [[ ! -f "${venv_python}" ]]; then
245250
venv_python="${tmpdir}/venv/Scripts/python.exe"
246251
fi
247-
"${venv_python}" -m pip install dist/*.tar.gz pytest pytest-asyncio
248-
if [[ -d "${GITHUB_WORKSPACE}/${{ needs.resolve.outputs.plugin_path }}/tests" ]]; then
249-
cp -R "${GITHUB_WORKSPACE}/${{ needs.resolve.outputs.plugin_path }}/tests" "${tmpdir}/tests"
250-
printf "[pytest]\npythonpath = tests\nasyncio_mode = auto\n" > "${tmpdir}/pytest.ini"
252+
"${venv_python}" -m pip install dist/*.tar.gz pytest pytest-asyncio PyYAML
253+
if [[ -d "${GITHUB_WORKSPACE}/plugins/tests/${{ needs.resolve.outputs.slug }}" ]]; then
254+
mkdir -p "${tmpdir}/tests"
255+
cp -R "${GITHUB_WORKSPACE}/plugins/tests/${{ needs.resolve.outputs.slug }}" "${tmpdir}/tests/${{ needs.resolve.outputs.slug }}"
256+
cp "${GITHUB_WORKSPACE}/plugins/tests/conftest.py" "${tmpdir}/tests/conftest.py"
257+
cp "${GITHUB_WORKSPACE}/plugins/tests/plugin_hooks.py" "${tmpdir}/tests/plugin_hooks.py"
258+
cp "${GITHUB_WORKSPACE}/plugins/tests/pytest.ini" "${tmpdir}/pytest.ini"
251259
cd "${tmpdir}"
260+
export CPEX_TEST_PLUGIN_HOOKS=1
261+
export PYTHONPATH="${tmpdir}/tests"
252262
"${venv_python}" -m pytest \
253263
-c "${tmpdir}/pytest.ini" \
254-
"${tmpdir}/tests" -v
264+
"${tmpdir}/tests/${{ needs.resolve.outputs.slug }}" -v
255265
fi
256266
257267
publish:

DEVELOPING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ make plugins-validate
4040
make plugin-test PLUGIN=pii_filter
4141
```
4242

43-
`make plugins-validate` runs the same convention checks that CI uses before the per-plugin build jobs run.
43+
`make plugins-validate` runs the same convention checks that the repo contract CI workflow runs.
4444
It runs the catalog validator plus the shared repo contract test modules:
4545
`tests/test_plugin_catalog.py` and `tests/test_install_built_wheel.py`.
4646

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ Each managed plugin must include:
1717
- `Cargo.toml`
1818
- `Makefile`
1919
- `README.md`
20-
- `tests/`
2120
- `cpex_<slug>/__init__.py`
2221
- `cpex_<slug>/plugin-manifest.yaml`
2322

23+
Python integration tests live under `plugins/tests/<slug>/`; Rust unit tests live in the plugin crate.
24+
2425
Rust crates are owned by the top-level workspace in `Cargo.toml`. Python package names follow `cpex-<slug>`, Python modules follow `cpex_<slug>`, plugin manifests must declare a top-level `kind` in `module.object` form, and `pyproject.toml` must publish the matching `module:object` reference under `[project.entry-points."cpex.plugins"]`. Release tags use the hyphenated slug form `<slug-with-hyphens>-v<version>`, for example `rate-limiter-v0.0.2`.
2526

2627
## Helper Commands

TESTING.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,46 @@ Equivalent repo-level helper:
4040
make plugin-test PLUGIN=rate_limiter
4141
```
4242

43-
`make plugin-test` runs the selected plugin's `make ci` target, including stub verification, build, bench compilation without execution, install, and Python tests.
43+
`make plugin-test` runs the selected plugin's `make ci` target, including stub verification, build, bench compilation where configured, install, and Python tests.
44+
45+
## 3. Rust Coverage
46+
47+
CI enforces at least 90% line coverage for each Rust plugin selected by the plugin catalog. The coverage job instruments Rust, runs Rust unit tests, then runs each plugin's repo-level Python integration tests so PyO3 paths are counted.
48+
49+
To run the same coverage check locally for all managed Rust plugins:
50+
51+
```bash
52+
rustup component add llvm-tools-preview
53+
cargo install cargo-llvm-cov --version 0.8.4 --locked
54+
mkdir -p coverage
55+
CARGO_PACKAGES="$(python3 tools/plugin_catalog.py ci-selection-field . all '' '' cargo_packages)"
56+
PLUGINS="$(python3 tools/plugin_catalog.py ci-selection-field . all '' '' plugins)"
57+
mapfile -t cargo_packages < <(python3 -c 'import json, os; [print(package) for package in json.loads(os.environ["CARGO_PACKAGES"])]')
58+
mapfile -t plugins < <(python3 -c 'import json, os; [print(plugin) for plugin in json.loads(os.environ["PLUGINS"])]')
59+
cargo_args=()
60+
for package in "${cargo_packages[@]}"; do
61+
cargo_args+=("-p" "${package}")
62+
done
63+
cargo llvm-cov clean --workspace
64+
eval "$(cargo llvm-cov show-env --sh)"
65+
export CARGO_TARGET_DIR="${CARGO_LLVM_COV_TARGET_DIR}/llvm-cov-target"
66+
export CARGO_LLVM_COV_BUILD_DIR="${CARGO_TARGET_DIR}"
67+
export LLVM_PROFILE_FILE="${CARGO_TARGET_DIR}/cpex-plugins-%p-%10m.profraw"
68+
mkdir -p "${CARGO_TARGET_DIR}"
69+
for plugin in "${plugins[@]}"; do
70+
(cd "plugins/rust/python-package/${plugin}" && make sync && uv run maturin develop)
71+
done
72+
cargo test "${cargo_args[@]}"
73+
for plugin in "${plugins[@]}"; do
74+
(cd "plugins/rust/python-package/${plugin}" && make test-integration)
75+
done
76+
env -u CARGO_TARGET_DIR -u CARGO_LLVM_COV_BUILD_DIR -u CARGO_LLVM_COV_TARGET_DIR -u LLVM_PROFILE_FILE cargo llvm-cov report "${cargo_args[@]}" --cobertura --output-path coverage/cobertura.xml
77+
python3 tools/plugin_catalog.py coverage-check . coverage/cobertura.xml 90.00 "${PLUGINS}"
78+
```
4479

4580
## CI Behavior
4681

47-
Whenever the Rust plugin CI workflow is triggered, it runs the repo contract tests before any plugin build jobs.
82+
Repo contract tests run in their own CI workflow. The Rust plugin CI workflow uses the same plugin catalog to select affected plugin build, integration, and coverage jobs.
4883

4984
Per-plugin build/test jobs are then scoped by the plugin catalog:
5085

plugins/rust/python-package/encoded_exfil_detection/Makefile

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,29 @@ clippy:
2727
$(CARGO) clippy -- -D warnings
2828

2929
# help: sync - Install plugin development dependencies
30-
# help: test - Run Rust unit tests
31-
# help: test-python - Run Python plugin tests
32-
# help: test-integration - Run integration tests only
33-
# help: test-all - Run both Rust and Python tests
34-
.PHONY: sync test test-python test-integration test-all verify-stubs
30+
# help: test - Run Rust unit tests and Python integration tests
31+
# help: test-unit - Run Rust unit tests
32+
# help: test-integration - Run repo-level integration tests for encoded_exfil_detection
33+
# help: test-all - Alias for test
34+
.PHONY: sync test test-unit test-python test-integration test-all verify-stubs
3535

3636
sync:
3737
uv sync --dev
3838

39-
test:
39+
test-unit:
4040
@echo "$(GREEN)Running encoded_exfil_detection Rust tests...$(NC)"
4141
$(CARGO) test
4242

43+
test: test-unit test-integration
44+
4345
test-python:
44-
@echo "$(GREEN)Running Python tests...$(NC)"
45-
uv run pytest tests/ -v
46+
$(MAKE) test-integration
4647

4748
test-integration:
4849
@echo "$(GREEN)Running integration tests...$(NC)"
49-
uv run pytest tests/integration/ -v
50+
CPEX_TEST_PLUGIN_HOOKS=1 uv run pytest ../../../tests/encoded_exfil_detection -v --asyncio-mode=auto
5051

51-
test-all: test test-python
52+
test-all: test
5253

5354
verify-stubs: stub-gen
5455
@git diff --exit-code -- $(STUB_FILES)
@@ -106,16 +107,19 @@ clean-all: clean
106107

107108
# help: verify - Verify plugin installation
108109
# help: check-all - Run fmt-check + clippy + Rust tests
110+
# help: ci-build - Run CI build/static verification without integration tests
109111
# help: ci - Run the full CI-equivalent plugin verification flow
110-
.PHONY: verify check-all ci
112+
.PHONY: verify check-all ci-build ci
111113

112114
verify:
113115
@uv run python -c "from cpex_encoded_exfil_detection import encoded_exfil_detection_rust; print('encoded_exfil_detection_rust available')" || echo "encoded_exfil_detection_rust not installed — run: make install"
114116

115-
check-all: fmt-check clippy test
117+
check-all: fmt-check clippy test-unit
116118
@echo "$(GREEN)All checks passed$(NC)"
117119

118-
ci: check-all verify-stubs build bench-no-run install-wheel test-python
120+
ci-build: check-all verify-stubs build bench-no-run install-wheel
121+
122+
ci: ci-build test-integration
119123
@echo "$(GREEN)CI verification passed$(NC)"
120124

121125
.DEFAULT_GOAL := help

plugins/rust/python-package/encoded_exfil_detection/cpex_encoded_exfil_detection/encoded_exfil_detection.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,32 +28,18 @@
2828
from pydantic import BaseModel, Field, field_validator
2929

3030
# First-Party
31-
try:
32-
from mcpgateway.plugins.framework import (
33-
Plugin,
34-
PluginConfig,
35-
PluginContext,
36-
PluginViolation,
37-
PromptPrehookPayload,
38-
PromptPrehookResult,
39-
ResourcePostFetchPayload,
40-
ResourcePostFetchResult,
41-
ToolPostInvokePayload,
42-
ToolPostInvokeResult,
43-
)
44-
except ModuleNotFoundError:
45-
from mcpgateway_mock.plugins.framework import ( # type: ignore[no-redef]
46-
Plugin,
47-
PluginConfig,
48-
PluginContext,
49-
PluginViolation,
50-
PromptPrehookPayload,
51-
PromptPrehookResult,
52-
ResourcePostFetchPayload,
53-
ResourcePostFetchResult,
54-
ToolPostInvokePayload,
55-
ToolPostInvokeResult,
56-
)
31+
from mcpgateway.plugins.framework import (
32+
Plugin,
33+
PluginConfig,
34+
PluginContext,
35+
PluginViolation,
36+
PromptPrehookPayload,
37+
PromptPrehookResult,
38+
ResourcePostFetchPayload,
39+
ResourcePostFetchResult,
40+
ToolPostInvokePayload,
41+
ToolPostInvokeResult,
42+
)
5743

5844
logger = logging.getLogger(__name__)
5945

plugins/rust/python-package/encoded_exfil_detection/pyproject.toml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,6 @@ module-name = "cpex_encoded_exfil_detection.encoded_exfil_detection_rust"
2929
python-source = "."
3030
features = ["pyo3/extension-module"]
3131

32-
[tool.pytest.ini_options]
33-
testpaths = ["tests"]
34-
pythonpath = ["tests"]
35-
asyncio_mode = "auto"
36-
markers = [
37-
"integration: end-to-end tests exercising the full plugin pipeline",
38-
]
39-
4032
[dependency-groups]
4133
dev = [
4234
"maturin>=1.12.6",

plugins/rust/python-package/encoded_exfil_detection/tests/conftest.py

Lines changed: 0 additions & 9 deletions
This file was deleted.

plugins/rust/python-package/encoded_exfil_detection/tests/integration/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)