Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,25 @@ on:
- cron: "0 4 * * *"

jobs:
lint:
name: lint / format / types
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: python -m pip install -e .[dev]
# Runs the same hooks contributors get from `pre-commit install`, at the
# same pinned tool versions, so CI and local checks cannot disagree.
# pytest is skipped here because the jobs below already run the suite
# across the full matrix rather than once.
- name: Run pre-commit hooks
env:
SKIP: pytest
run: pre-commit run --all-files --show-diff-on-failure

tests-unit:
name: unit / py${{ matrix.python-version }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -135,9 +154,16 @@ jobs:
run: python -m pip install -e .[dev]
- name: Run tests with coverage
run: coverage run -m pytest -m "not soak"
# Fails when package coverage drops below the fail_under floor in
# pyproject.toml. Previously coverage was measured and uploaded but never
# checked, so it could regress without CI noticing.
- name: Check coverage against the floor
run: coverage report
- name: Generate coverage report
if: always()
run: coverage xml -o coverage.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-report
path: coverage.xml
16 changes: 16 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,19 @@ markers = [

[tool.isort]
profile = "black"


[tool.coverage.run]
# Scope measurement to the package. Without this, coverage also reports the
# test files themselves, which are ~100% by construction and inflate the total
# into something that cannot regress meaningfully.
source = ["pyisolate"]


[tool.coverage.report]
show_missing = true
# A floor, not a target. The suite sits above this; the point is that a change
# which drops package coverage substantially fails CI instead of being noticed
# months later. Kernel-gated tests (Landlock, BPF, cgroup) skip on hosts without
# those features, so the floor is set below the observed value rather than at it.
fail_under = 70
14 changes: 11 additions & 3 deletions tests/test_ebpf_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ def _kernel_map_names() -> set[str]:
def _kernel_deny_defines() -> dict[str, int]:
text = _BPF_SRC.read_text(encoding="utf-8")
found = {}
for name, shift in re.findall(r"#define\s+(PYI_DENY_\w+)\s+\(1U\s*<<\s*(\d+)\)", text):
for name, shift in re.findall(
r"#define\s+(PYI_DENY_\w+)\s+\(1U\s*<<\s*(\d+)\)", text
):
found[name] = 1 << int(shift)
return found

Expand All @@ -51,12 +53,18 @@ def test_deny_mask_bits_match_kernel_defines():
def test_compile_deny_mask_denies_everything_without_a_policy():
mask = contract.compile_deny_mask(None)
assert mask == (
contract.DENY_FS | contract.DENY_NET | contract.DENY_PROCESS | contract.DENY_RISKY
contract.DENY_FS
| contract.DENY_NET
| contract.DENY_PROCESS
| contract.DENY_RISKY
)


def test_compile_deny_mask_always_denies_process_and_risky():
for policy in (None, iso.policy.Policy().allow_fs("/tmp").allow_tcp("127.0.0.1:80")):
for policy in (
None,
iso.policy.Policy().allow_fs("/tmp").allow_tcp("127.0.0.1:80"),
):
mask = contract.compile_deny_mask(policy)
assert mask & contract.DENY_PROCESS
assert mask & contract.DENY_RISKY
Expand Down
5 changes: 1 addition & 4 deletions tests/test_landlock.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@
import pyisolate as iso
from pyisolate.runtime import landlock
from pyisolate.runtime.child import _net_connect_ports
from pyisolate.runtime.process_backend import (
_extract_fs_read_write,
_extract_fs_tcp,
)
from pyisolate.runtime.process_backend import _extract_fs_read_write, _extract_fs_tcp

requires_landlock = pytest.mark.skipif(
not landlock.landlock_supported(),
Expand Down
Loading