Skip to content

Commit e174ceb

Browse files
authored
Merge pull request #112 from aleivag/add-e2e-tests
Add E2E testing infrastructure using mkosi
2 parents df633ab + 7a92970 commit e174ceb

13 files changed

Lines changed: 674 additions & 5 deletions

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
strategy:
99
fail-fast: false
1010
matrix:
11-
python: ['3.11', '3.12', '3.13', '3.14']
11+
python: ['3.11', '3.12', '3.13', '3.14', '3.14t']
1212
runs-on: ubuntu-latest
1313
steps:
1414
- name: Checkout repository
@@ -42,6 +42,7 @@ jobs:
4242
pystemd
4343
examples
4444
tests
45+
e2e
4546
- name: Run isort
4647
uses: isort/isort-action@v1
4748

.github/workflows/e2e-tests.yml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
name: E2E Tests
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
e2e-tests:
11+
name: Python ${{ matrix.python-version }}
12+
runs-on: ubuntu-latest
13+
14+
strategy:
15+
matrix:
16+
python-version: ['3.11', '3.12', '3.13', '3.14', '3.14t']
17+
18+
env:
19+
PYTHON_VERSION: ${{ matrix.python-version }}
20+
UNIT_NAME: pystemd-e2e-py${{ matrix.python-version }}.service
21+
MACHINE_NAME: pystemd-test-py${{ matrix.python-version }}
22+
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v4
26+
27+
- name: Set up latest Python
28+
uses: actions/setup-python@v4
29+
with:
30+
python-version: '3.14'
31+
32+
- name: setup-mkosi
33+
uses: systemd/mkosi@v26
34+
35+
- name: Install systemd-container
36+
run: |
37+
sudo apt-get update
38+
sudo apt-get install -y systemd-container
39+
40+
- name: Generate mkosi keys
41+
run: |
42+
sudo mkosi genkey
43+
44+
- name: Build mkosi test image
45+
run: |
46+
sudo mkosi -E "$PYTHON_VERSION" build
47+
48+
- name: Boot container
49+
run: |
50+
sudo systemd-run --unit "$UNIT_NAME" --same-dir \
51+
systemd-nspawn \
52+
--machine="$MACHINE_NAME" \
53+
--boot \
54+
--directory=pystemd-test \
55+
--bind-ro=${{ github.workspace }}/e2e:/opt/pystemd/e2e
56+
57+
# Wait for container to be ready
58+
for i in {1..30}; do
59+
if sudo systemd-run --machine="$MACHINE_NAME" --wait --pipe /bin/true 2>/dev/null; then
60+
echo "Container is ready"
61+
break
62+
fi
63+
echo "Waiting for container to start... ($i/30)"
64+
sleep 1
65+
done
66+
sudo journalctl -u "$UNIT_NAME"
67+
sleep 1
68+
sudo systemd-run --machine="$MACHINE_NAME" --wait --pipe /bin/echo 'hello world' || exit 1
69+
70+
- name: Run E2E tests
71+
run: |
72+
sudo systemd-run \
73+
--machine="$MACHINE_NAME" \
74+
--wait \
75+
--pipe \
76+
--setenv=PYSTEMD_E2E_CONTAINER=1\
77+
--property=PrivateTmp=true \
78+
-- \
79+
/opt/pystemd/venv/bin/pytest \
80+
-o cache_dir=/tmp/pytest_cache \
81+
/opt/pystemd/e2e/ -v
82+
83+
- name: Stop container
84+
if: always()
85+
run: |
86+
sudo machinectl terminate "$MACHINE_NAME" || true

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,10 @@ pystemd/RELEASE
9898

9999
# not a fan of keeping uv lock files around
100100
uv.lock
101+
102+
# mkosi generated files
103+
pystemd-test/
104+
pystemd-test-*/
105+
.#pystemd-test*.lck
106+
mkosi.key
107+
mkosi.crt

E2E_TESTING.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# pystemd E2E Testing Setup
2+
3+
## Overview
4+
5+
This setup provides comprehensive end-to-end testing for pystemd using mkosi and systemd-nspawn. Tests run in a real systemd environment inside a container to ensure accurate testing of systemd integration.
6+
7+
## Quick Start
8+
9+
```bash
10+
# Install dependencies (one-time setup)
11+
sudo dnf install mkosi systemd-container # Fedora/RHEL
12+
# or
13+
sudo apt install mkosi systemd-container # Debian/Ubuntu
14+
15+
# generate keys if they dont exists
16+
mkosi genkey
17+
18+
# build container, there steps do not need to be run as root.
19+
mkosi clean && mkosi build
20+
21+
# Start container
22+
UNIT_NAME=pystemd-e2e-local.service
23+
MACHINE_NAME=pystemd-test-local
24+
sudo systemd-run --unit "$UNIT_NAME" --same-dir \
25+
systemd-nspawn \
26+
--machine="$MACHINE_NAME" \
27+
--boot \
28+
--directory=pystemd-test \
29+
--bind-ro=`pwd`/e2e:/opt/pystemd/e2e
30+
31+
# Run all E2E tests
32+
sudo systemd-run \
33+
--machine="$MACHINE_NAME" \
34+
--wait \
35+
--pipe \
36+
--setenv=PYSTEMD_E2E_CONTAINER=1\
37+
--property=PrivateTmp=true \
38+
-- \
39+
/opt/pystemd/venv/bin/pytest \
40+
-o cache_dir=/tmp/pytest_cache \
41+
/opt/pystemd/e2e/ -v
42+
```
43+
44+
45+
## How It Works
46+
47+
### Manual Container Management
48+
49+
The E2E testing workflow uses a straightforward approach:
50+
51+
1. **Build the container image** using mkosi - this creates a Fedora environment with pystemd installed
52+
2. **Boot the container** using systemd-nspawn as a background service
53+
3. **Run tests inside the container** using `systemd-run --machine`
54+
4. **Stop the container** when done
55+
56+
The container runs with `--boot` which starts a full systemd init inside, providing a realistic systemd environment for testing.
57+
58+
### Container Boot Command
59+
60+
The container is booted as a systemd service using systemd-nspawn:
61+
62+
```bash
63+
UNIT_NAME=pystemd-e2e-local.service
64+
MACHINE_NAME=pystemd-test
65+
66+
sudo systemd-run --unit "$UNIT_NAME" --same-dir \
67+
systemd-nspawn \
68+
--machine="$MACHINE_NAME" \
69+
--boot \
70+
--directory=pystemd-test \
71+
--bind-ro=`pwd`/e2e:/opt/pystemd/e2e
72+
```
73+
74+
Tests are then executed inside the container using:
75+
76+
```bash
77+
sudo systemd-run \
78+
--machine="$MACHINE_NAME" \
79+
--wait \
80+
--pipe \
81+
--setenv=PYSTEMD_E2E_CONTAINER=1 \
82+
--property=PrivateTmp=true \
83+
-- \
84+
/opt/pystemd/venv/bin/pytest \
85+
-o cache_dir=/tmp/pytest_cache \
86+
/opt/pystemd/e2e/ -v
87+
```
88+
89+
90+
## Test Suite
91+
92+
Tests are located in the `e2e/` directory:
93+
94+
- **`test_pystemd_run.py`** - Tests for `pystemd.run()`
95+
- **`test_manager.py`** - Tests for systemd Manager API
96+
- **`test_unit.py`** - Tests for Unit operations
97+
- **`test_transient_units.py`** - Tests for transient unit creation
98+
- **`test_dbus.py`** - Tests for D-Bus connections
99+
100+
## Adding New Tests
101+
102+
Create a new test file in the `e2e/` directory:
103+
104+
```python
105+
# e2e/test_my_feature.py
106+
import pystemd.run
107+
108+
def test_my_feature():
109+
"""Test my new feature"""
110+
unit = pystemd.run([b'/bin/echo', b'hello'], wait=True)
111+
assert unit.Service.ExecMainStatus == 0
112+
```
113+
114+
```bash
115+
sudo systemd-run \
116+
--machine="$MACHINE_NAME" \
117+
--wait \
118+
--pipe \
119+
--setenv=PYSTEMD_E2E_CONTAINER=1 \
120+
--property=PrivateTmp=true \
121+
-- \
122+
/opt/pystemd/venv/bin/pytest \
123+
-o cache_dir=/tmp/pytest_cache \
124+
/opt/pystemd/e2e/test_my_feature.py -v
125+
```
126+
127+
128+
And run it using
129+
130+
## CI/CD Integration
131+
132+
The GitHub Actions workflow (`.github/workflows/e2e-tests.yml`) automates E2E testing:
133+
134+
**Triggers:**
135+
- Pushes to `main` or `develop` branches
136+
- Pull requests targeting `main`
137+
138+
**Matrix Testing:**
139+
- Tests across multiple Python versions: 3.11, 3.12, 3.13, 3.14, 3.14t (free-threaded)
140+
- Each Python version runs in its own container instance
141+
142+
143+
## Troubleshooting
144+
145+
### Container fails to start
146+
```bash
147+
# Check if another container is running
148+
sudo machinectl list
149+
150+
# SSH into the container
151+
sudo machinectl shell pystemd-test
152+
153+
# Terminate stale container
154+
sudo machinectl terminate pystemd-test
155+
```
156+
157+
### Image not found
158+
```bash
159+
# Rebuild the mkosi image
160+
sudo mkosi --force build
161+
```
162+
163+
### Tests hang
164+
```bash
165+
# Check container status
166+
sudo machinectl status pystemd-test
167+
168+
# View container logs
169+
sudo journalctl -M pystemd-test
170+
```
171+
172+
### Building with a different Python version
173+
174+
By default, the container is built with Python 3.14. To build with a different Python version, set the `PYTHON_VERSION` environment variable before building:
175+
176+
```bash
177+
# Build with Python 3.12
178+
mkosi clean && mkosi -E PYTHON_VERSION=3.12 build
179+
180+
# Build with Python 3.14 free-threaded
181+
mkosi clean && mkosi-E PYTHON_VERSION=3.14t build
182+
```
183+
184+
The `-E` flag passes the `PYTHON_VERSION` environment variable to mkosi, which is then used by `mkosi.build.chroot` to install the specified Python version using `uv python install`.
185+

e2e/test_dbus.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""E2E tests for D-Bus integration"""
2+
3+
from pystemd.dbuslib import DBus
4+
5+
6+
def test_dbus_connection():
7+
"""Test basic D-Bus connection"""
8+
with DBus() as bus:
9+
assert bus is not None

e2e/test_manager.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""E2E tests for pystemd Manager functionality"""
2+
3+
from pystemd.systemd1 import Manager
4+
5+
6+
def test_manager_version():
7+
"""Test getting systemd version"""
8+
with Manager() as manager:
9+
version = manager.Manager.Version
10+
assert version is not None
11+
assert isinstance(version, bytes)
12+
13+
14+
def test_manager_architecture():
15+
"""Test getting system architecture"""
16+
with Manager() as manager:
17+
arch = manager.Manager.Architecture
18+
assert arch is not None
19+
assert isinstance(arch, bytes)
20+
21+
22+
def test_list_units():
23+
"""Test listing units"""
24+
with Manager() as manager:
25+
units = manager.Manager.ListUnits()
26+
assert len(units) > 0
27+
# Each unit should be a tuple with multiple fields
28+
assert isinstance(units[0], tuple)
29+
30+
31+
def test_list_unit_files():
32+
"""Test listing unit files"""
33+
with Manager() as manager:
34+
unit_files = manager.Manager.ListUnitFiles()
35+
assert len(unit_files) > 0
36+
# Each should be (name, state) tuple
37+
for name, state in unit_files:
38+
assert isinstance(name, bytes)
39+
assert isinstance(state, bytes)
40+
41+
42+
def test_get_unit():
43+
"""Test getting a unit by name"""
44+
with Manager() as manager:
45+
# Get a unit that should always exist
46+
unit_path = manager.Manager.GetUnit(b"dbus.service")
47+
assert unit_path is not None
48+
assert isinstance(unit_path, bytes)

0 commit comments

Comments
 (0)