Skip to content

Commit 2bc8f71

Browse files
committed
feat: add psutil compatibility smoke test to CI pipelines
- Introduced a new Python script for testing psutil compatibility with musl. - Updated GitLab CI and GitHub Actions workflows to include a psutil smoke test. - The smoke test verifies major surface areas of psutil, ensuring proper functionality in musl environments.
1 parent a7a044b commit 2bc8f71

3 files changed

Lines changed: 210 additions & 0 deletions

File tree

.github/workflows/build.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ jobs: {
3030
"run": "docker run --rm release-${{ github.ref_name }}:${{ matrix.version }}-${{ matrix.architecture }} /opt/python/bin/python3 -c 'import ssl, sqlite3, ctypes, lzma; print(\"ok\")' && \
3131
docker run --rm release-${{ github.ref_name }}:${{ matrix.version }}-${{ matrix.architecture }} /opt/python/bin/pip3 --version",
3232
},
33+
{
34+
"name": "psutil compatibility smoke test",
35+
"run": "docker run --rm \
36+
-v \"${{ github.workspace }}/ci/smoke_psutil.py:/tmp/smoke_psutil.py:ro\" \
37+
release-${{ github.ref_name }}:${{ matrix.version }}-${{ matrix.architecture }} \
38+
sh -c '/opt/python/bin/pip3 install --quiet --no-cache-dir psutil && /opt/python/bin/python3 /tmp/smoke_psutil.py'",
39+
},
3340
{
3441
"name": "Save docker image as tar file",
3542
"run": "docker save release-${{ github.ref_name }}:${{ matrix.version }}-${{ matrix.architecture }} > release-${{ matrix.version }}-${{ matrix.architecture }}.tar",

.gitlab-ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ build:
2020
- docker build -f ./${version}/${architecture}/Dockerfile -t release-${PACKAGE_NAME}:${version}-${architecture} . 1>${version}-${architecture}.log 2>&1
2121
- docker run --rm release-${PACKAGE_NAME}:${version}-${architecture} /opt/python/bin/python3 -c 'import ssl, sqlite3, ctypes, lzma; print("ok")'
2222
- docker run --rm release-${PACKAGE_NAME}:${version}-${architecture} /opt/python/bin/pip3 --version
23+
# End-to-end compatibility check: install a native-extension wheel from
24+
# PyPI (exercises the musllinux tag hook) and run a broad surface-area
25+
# smoke test against the shipped libc / /proc / subprocess plumbing.
26+
- |
27+
docker run --rm \
28+
-v "${CI_PROJECT_DIR}/ci/smoke_psutil.py:/tmp/smoke_psutil.py:ro" \
29+
release-${PACKAGE_NAME}:${version}-${architecture} \
30+
sh -c '/opt/python/bin/pip3 install --quiet --no-cache-dir psutil && /opt/python/bin/python3 /tmp/smoke_psutil.py'
2331
- docker save release-${PACKAGE_NAME}:${version}-${architecture} > release-${version}-${architecture}.tar
2432
- bash ci/packing_release_tar.sh release-${version}-${architecture}.tar
2533
- gzip -9 build/release-${version}-${architecture}.tar

ci/smoke_psutil.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#!/usr/bin/env python3
2+
"""psutil compatibility smoke test for standalone-python.
3+
4+
Exercises the major surface areas that touch platform-specific code
5+
paths: /proc parsing, libc wrappers via ctypes, subprocess spawn, socket
6+
enumeration, and the loadable _psutil_linux / _psutil_posix C extensions.
7+
A successful run means the musl runtime can satisfy every symbol psutil
8+
links against, and /proc is visible and parseable.
9+
10+
Exit status: 0 if every section either succeeded or was explicitly
11+
skipped (e.g. sensors on a bare container). Non-zero if any section
12+
raised an unexpected exception.
13+
14+
Run:
15+
./python3 ci/smoke_psutil.py
16+
"""
17+
18+
import os
19+
import socket
20+
import sys
21+
import time
22+
import traceback
23+
24+
25+
# (section label, callable). Each callable prints its findings. Wrapping
26+
# them as lambdas defers work until we actually enter the section — so a
27+
# failing early import doesn't hide later sections.
28+
SECTIONS = []
29+
30+
31+
def section(label):
32+
def _decorator(fn):
33+
SECTIONS.append((label, fn))
34+
return fn
35+
return _decorator
36+
37+
38+
# ---------------------------------------------------------------------------
39+
# Sections
40+
# ---------------------------------------------------------------------------
41+
42+
43+
@section("version / build")
44+
def s_version():
45+
import psutil
46+
print(f"psutil {psutil.__version__}")
47+
print(f"PROCFS {psutil.PROCFS_PATH}")
48+
print(f"python {sys.version.splitlines()[0]}")
49+
print(f"platform {sys.platform} {os.uname().machine}")
50+
51+
52+
@section("cpu")
53+
def s_cpu():
54+
import psutil
55+
print(f"logical : {psutil.cpu_count()}")
56+
print(f"physical : {psutil.cpu_count(logical=False)}")
57+
print(f"percent : {psutil.cpu_percent(interval=0.5, percpu=True)}")
58+
print(f"times : {psutil.cpu_times()}")
59+
print(f"freq : {psutil.cpu_freq()}")
60+
print(f"stats : {psutil.cpu_stats()}")
61+
try:
62+
print(f"loadavg : {psutil.getloadavg()}")
63+
except (OSError, AttributeError) as e:
64+
print(f"loadavg : unavailable ({e})")
65+
66+
67+
@section("memory")
68+
def s_memory():
69+
import psutil
70+
print(f"virtual : {psutil.virtual_memory()}")
71+
print(f"swap : {psutil.swap_memory()}")
72+
73+
74+
@section("disk")
75+
def s_disk():
76+
import psutil
77+
parts = psutil.disk_partitions(all=False)
78+
print(f"parts : {len(parts)} partitions")
79+
for p in parts[:3]:
80+
print(f" {p}")
81+
print(f"usage / : {psutil.disk_usage('/')}")
82+
io = psutil.disk_io_counters(perdisk=False)
83+
print(f"io : {io}")
84+
85+
86+
@section("network")
87+
def s_network():
88+
import psutil
89+
print(f"io : {psutil.net_io_counters()}")
90+
addrs = psutil.net_if_addrs()
91+
print(f"if_addrs : {list(addrs)}")
92+
stats = psutil.net_if_stats()
93+
print(f"if_stats : {list(stats)}")
94+
for kind in ("inet", "inet4", "tcp", "udp"):
95+
try:
96+
conns = psutil.net_connections(kind=kind)
97+
print(f"conns {kind:5}: {len(conns)}")
98+
except (psutil.AccessDenied, PermissionError) as e:
99+
print(f"conns {kind:5}: access denied ({e})")
100+
101+
102+
@section("sensors (optional)")
103+
def s_sensors():
104+
import psutil
105+
temps = psutil.sensors_temperatures()
106+
print(f"temps : {temps or 'n/a (no /sys/class/hwmon sensors)'}")
107+
fans = psutil.sensors_fans()
108+
print(f"fans : {fans or 'n/a'}")
109+
print(f"battery : {psutil.sensors_battery()}")
110+
111+
112+
@section("system")
113+
def s_system():
114+
import psutil
115+
print(f"boot : {time.ctime(psutil.boot_time())}")
116+
users = psutil.users()
117+
print(f"users : {len(users)} logged in")
118+
for u in users[:3]:
119+
print(f" {u}")
120+
121+
122+
@section("current process")
123+
def s_current_process():
124+
import psutil
125+
p = psutil.Process(os.getpid())
126+
print(f"pid : {p.pid}")
127+
print(f"name : {p.name()}")
128+
print(f"exe : {p.exe()}")
129+
print(f"cwd : {p.cwd()}")
130+
print(f"cmdline : {p.cmdline()}")
131+
print(f"status : {p.status()}")
132+
print(f"username : {p.username()}")
133+
print(f"created : {time.ctime(p.create_time())}")
134+
print(f"threads : {p.num_threads()}")
135+
print(f"nice : {p.nice()}")
136+
print(f"ionice : {p.ionice()}")
137+
print(f"num_fds : {p.num_fds()}")
138+
print(f"mem_info : {p.memory_info()}")
139+
print(f"mem_full : {p.memory_full_info()}")
140+
print(f"cpu_times: {p.cpu_times()}")
141+
opened = p.open_files()
142+
print(f"open_fds : {len(opened)} open files")
143+
env = p.environ()
144+
print(f"environ : {len(env)} entries")
145+
146+
147+
@section("process iteration")
148+
def s_process_iter():
149+
import psutil
150+
rows = 0
151+
for proc in psutil.process_iter(["pid", "name", "username", "memory_info"]):
152+
rows += 1
153+
if rows <= 5:
154+
print(f" {proc.info}")
155+
print(f"iterated : {rows} processes")
156+
157+
158+
@section("spawn + monitor child")
159+
def s_spawn():
160+
import psutil
161+
child = psutil.Popen(
162+
[sys.executable, "-c", "import time; time.sleep(1)"],
163+
stdout=None, stderr=None,
164+
)
165+
print(f"child pid: {child.pid}")
166+
print(f"cmdline : {child.cmdline()}")
167+
rc = child.wait(timeout=5)
168+
print(f"exit rc : {rc}")
169+
assert rc == 0, f"unexpected child exit: {rc}"
170+
171+
172+
# ---------------------------------------------------------------------------
173+
# Driver
174+
# ---------------------------------------------------------------------------
175+
176+
177+
def main():
178+
passed = failed = 0
179+
for label, fn in SECTIONS:
180+
print(f"\n=== {label} ===")
181+
try:
182+
fn()
183+
except Exception:
184+
failed += 1
185+
traceback.print_exc()
186+
else:
187+
passed += 1
188+
189+
print("\n" + "=" * 40)
190+
print(f"summary: {passed} passed, {failed} failed of {len(SECTIONS)} sections")
191+
return 0 if failed == 0 else 1
192+
193+
194+
if __name__ == "__main__":
195+
sys.exit(main())

0 commit comments

Comments
 (0)