2 Commits
Author SHA1 Message Date
grabowski dfba5561bb CI: run the suite on every platform the README now advertises
Build PCM package / build (push) Successful in 5s
tests / ubuntu-latest · py3.11 (push) Successful in 1m32s
tests / ubuntu-latest · py3.13 (push) Successful in 1m25s
tests / archlinux:latest (push) Failing after 14s
tests / debian:12 (push) Failing after 7s
tests / fedora:latest (push) Failing after 8s
tests / ubuntu:24.04 (push) Failing after 9s
tests / NixOS (FHS wrapper from docs/NIXOS.md) (push) Has been skipped
tests / macos-latest · py3.11 (push) Has been cancelled
tests / macos-latest · py3.13 (push) Has been cancelled
tests / windows-latest · py3.11 (push) Has been cancelled
tests / windows-latest · py3.13 (push) Has been cancelled
Gitea Actions matrix: ubuntu-latest (py3.11/3.13) plus Debian 12,
Ubuntu 24.04, Fedora and Arch containers, each installing the system
libraries PySide6 wheels dlopen - all four validated locally in
Docker (Debian 141/141, QtWidgets import probe green everywhere).
Windows/macOS and a NixOS-FHS job (tools/ci-fhs.nix, the library set
from docs/NIXOS.md) are gated to github.com runners so they skip
instead of queueing forever here. README gets a Platform support
table with the workflow badge; KiCad-in-the-loop stays field-tested,
CI covers the solver and fallback suite.
2026-07-23 17:25:26 +07:00
grabowski de7a4f8641 Probe QtWidgets, not QtCore: the qt backend needs the full GUI stack
QtCore's libraries (glib, icu) can load while QtWidgets/QtGui still
miss libGL - matplotlib's qt backend imports all three, so the old
probe could promise QtAgg and lose even the error figure at
switch_backend, one library layer deeper than the NixOS fix. The
regression test's mock now mirrors the real failure (bare package and
QtCore import fine, GUI modules raise): both the bare-import and the
QtCore-probe variants now fail it, verified by mutation. Also drop two
redundant f-prefixes on placeholder-less strings in report.py.
2026-07-23 17:25:19 +07:00
6 changed files with 217 additions and 16 deletions
+143
View File
@@ -0,0 +1,143 @@
# Runs the full pytest suite (solver, raster, platform fallbacks)
# against real pip wheels on every platform row advertised in the
# README's "Platform support" table. What it cannot do is launch KiCad
# itself — the plugin-inside-KiCad integration stays field-tested.
#
# Gitea/Forgejo Actions syntax is GitHub-Actions-compatible; if the
# repo is ever mirrored to github.com, copy this file to
# .github/workflows/ (GitHub does not read .gitea/). The
# windows/macos and NixOS jobs need runners that only github.com
# provides for free, so they are gated on the server URL instead of
# queueing forever on a self-hosted instance; drop the `if:` lines if
# you register your own runners with those labels.
# Third-party actions pinned to commit SHAs, matching build-pcm.yml:
# mutable tags could be repointed at malicious code.
name: tests
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
env:
UV_PYTHON: "3.12"
# No test opens a window, but if one ever creates a figure the Qt
# backend must not try to reach a display on a headless runner.
QT_QPA_PLATFORM: offscreen
jobs:
linux:
name: ubuntu-latest · py${{ matrix.python }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python: ["3.11", "3.13"]
env:
UV_PYTHON: ${{ matrix.python }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Qt runtime libraries (PySide6 wheels dlopen these)
run: >
sudo apt-get update -qq && sudo apt-get install -y -qq
libglib2.0-0 libgl1 libegl1 libopengl0 libfontconfig1
libfreetype6 libdbus-1-3 libxkbcommon0 libzstd1 libxcb1
libx11-6 libxext6 libxrender1 libsm6 libice6
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- run: uv sync --frozen
- run: uv run python -c "import PySide6.QtWidgets"
- run: uv run pytest -q
distros:
name: ${{ matrix.distro }}
runs-on: ubuntu-latest
container: ${{ matrix.distro }}
strategy:
fail-fast: false
matrix:
include:
- distro: debian:12
setup: >
apt-get update -qq && apt-get install -y -qq
curl ca-certificates
libglib2.0-0 libgl1 libegl1 libopengl0 libfontconfig1
libfreetype6 libdbus-1-3 libxkbcommon0 libzstd1 libxcb1
libx11-6 libxext6 libxrender1 libsm6 libice6
- distro: ubuntu:24.04
setup: >
apt-get update -qq && apt-get install -y -qq
curl ca-certificates
libglib2.0-0 libgl1 libegl1 libopengl0 libfontconfig1
libfreetype6 libdbus-1-3 libxkbcommon0 libzstd1 libxcb1
libx11-6 libxext6 libxrender1 libsm6 libice6
- distro: fedora:latest
setup: >
dnf install -y -q
glib2 mesa-libGL mesa-libEGL libglvnd-opengl fontconfig
freetype dbus-libs libxkbcommon libzstd libxcb libX11
libXext libXrender libSM libICE
- distro: archlinux:latest
setup: >
pacman -Syu --noconfirm --needed
curl glib2 libglvnd fontconfig freetype2 dbus
libxkbcommon zstd libxcb libx11 libxext libxrender
libsm libice
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Distro Qt runtime libraries
run: ${{ matrix.setup }}
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- run: uv sync --frozen
- run: uv run python -c "import PySide6.QtWidgets"
- run: uv run pytest -q
desktop:
name: ${{ matrix.os }} · py${{ matrix.python }}
# github.com only: no free Windows/macOS runners on self-hosted
# Gitea/Forgejo. Skipped (not queued) elsewhere.
if: github.server_url == 'https://github.com'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
python: ["3.11", "3.13"]
env:
UV_PYTHON: ${{ matrix.python }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- run: uv sync --frozen
- run: uv run python -c "import PySide6.QtWidgets"
- run: uv run pytest -q
nixos:
name: NixOS (FHS wrapper from docs/NIXOS.md)
# Needs unprivileged user namespaces for bubblewrap, which the
# GitHub ubuntu VM allows but a docker-based act_runner does not.
if: github.server_url == 'https://github.com'
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: DeterminateSystems/nix-installer-action@e50d5f73bfe71c2dd0aa4218de8f4afa59f8f81d # v16
- name: Build the FHS environment (docs/NIXOS.md library set)
run: nix-build tools/ci-fhs.nix -o ci-fhs
- name: Run the suite inside the FHS env
# shellcheck disable=SC2016 -- $HOME/$PATH expand inside the
# FHS bash, not the runner shell; single quotes are the point.
run: |
# shellcheck disable=SC2016
./ci-fhs/bin/fill-resistance-ci -c '
set -e
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
export UV_PROJECT_ENVIRONMENT=/tmp/venv
uv sync --frozen
uv run python -c "import PySide6.QtWidgets"
uv run pytest -q
'
+21
View File
@@ -28,6 +28,27 @@ happens around the notch on F.Cu.*
Uses the KiCad **IPC API** (`kicad-python` / `kipy`), not the deprecated Uses the KiCad **IPC API** (`kicad-python` / `kipy`), not the deprecated
SWIG API. Requires KiCad **10.0.1+**. SWIG API. Requires KiCad **10.0.1+**.
## Platform support
[![tests](https://git.b4l.co.th/B4L/kicad-zone-resistance/actions/workflows/ci.yml/badge.svg?branch=main)](https://git.b4l.co.th/B4L/kicad-zone-resistance/actions)
| Platform | Status | Verified by |
|-----------------------------|:------:|-------------|
| Windows | ✅ | development platform; CI test suite |
| macOS | ✅ | field-tested in KiCad 10; CI test suite |
| NixOS | ✅ | field-tested in KiCad 10 ([setup](docs/NIXOS.md)); CI runs the suite inside the documented FHS env |
| Debian 12 | ✅ | CI test suite in container |
| Ubuntu 24.04 | ✅ | CI test suite in container |
| Fedora (latest) | ✅ | CI test suite in container |
| Arch (latest) | ✅ | CI test suite in container |
CI (`.gitea/workflows/ci.yml`) runs the full pytest suite — solver,
rasterizer, and the platform-fallback regressions — headless against
the real pip wheels of each row, including the `PySide6.QtWidgets`
import probe that decides the matplotlib backend. What CI *cannot* do
is launch KiCad itself, so "runs inside KiCad" remains field-tested
(Windows continuously, macOS and NixOS per release).
## Setup (one-time) ## Setup (one-time)
The plugin is developed and tested on **Windows**; **macOS works** The plugin is developed and tested on **Windows**; **macOS works**
+10 -6
View File
@@ -26,14 +26,18 @@ def _pick_backend():
("Cannot load backend 'TkAgg' ... as 'qt' is currently running") - ("Cannot load backend 'TkAgg' ... as 'qt' is currently running") -
exactly what happened on macOS, whose bundled Python ships tkinter. exactly what happened on macOS, whose bundled Python ships tkinter.
The probe must import the binding's native core, not just the The probe must import QtWidgets, not just the package or QtCore:
package: on NixOS `import PySide6` succeeds (pure __init__) while on NixOS `import PySide6` succeeds (pure __init__) while QtCore's
QtCore's .so cannot find the system libraries pip wheels expect .so cannot find the system libraries pip wheels expect
("libgthread-2.0.so.0: cannot open shared object file") - promising ("libgthread-2.0.so.0: cannot open shared object file"), and on a
QtAgg then kills even the error figure at switch_backend time.""" partially provisioned system QtCore's deps (glib, icu) can be
present while QtWidgets/QtGui still miss libGL/libEGL. matplotlib's
qt backend imports QtCore, QtGui and QtWidgets, so probe the widest
one - promising QtAgg then kills even the error figure at
switch_backend time."""
for qt in ("PySide6", "PyQt6", "PyQt5", "PySide2"): for qt in ("PySide6", "PyQt6", "PyQt5", "PySide2"):
try: try:
__import__(qt + ".QtCore") __import__(qt + ".QtWidgets")
return "QtAgg" if qt in ("PySide6", "PyQt6") else "Qt5Agg" return "QtAgg" if qt in ("PySide6", "PyQt6") else "Qt5Agg"
except Exception: except Exception:
continue continue
+2 -2
View File
@@ -68,7 +68,7 @@ def write_summary(outdir: Path, problem: Problem, stack: RasterStack,
f"resistivity: {problem.rho_ohm_m:.3e} ohm*m", f"resistivity: {problem.rho_ohm_m:.3e} ohm*m",
f"via plating: {problem.plating_nm / 1000:.0f} um", f"via plating: {problem.plating_nm / 1000:.0f} um",
"", "",
(f"frequency: " ("frequency: "
+ (f"{result.freq_hz:g} Hz (skin depth {result.skin_depth_um:.0f} um)" + (f"{result.freq_hz:g} Hz (skin depth {result.skin_depth_um:.0f} um)"
if result.freq_hz > 0 else "DC")), if result.freq_hz > 0 else "DC")),
f"RESISTANCE: {result.R_ohm * 1000:.6g} mOhm" f"RESISTANCE: {result.R_ohm * 1000:.6g} mOhm"
@@ -127,7 +127,7 @@ def write_summary(outdir: Path, problem: Problem, stack: RasterStack,
f"contact model: {result.contact_model}" f"contact model: {result.contact_model}"
+ (" (uniform orthogonal injection; R is the upper contact bound)" + (" (uniform orthogonal injection; R is the upper contact bound)"
if result.contact_model == "uniform" else " (ideal bonded lug)"), if result.contact_model == "uniform" else " (ideal bonded lug)"),
f"terminals:", "terminals:",
f" V+ ({len(problem.electrodes1)} injection area(s)):", f" V+ ({len(problem.electrodes1)} injection area(s)):",
*(f" {_electrode_line(e)}" for e in problem.electrodes1), *(f" {_electrode_line(e)}" for e in problem.electrodes1),
f" V- ({len(problem.electrodes2)} injection area(s)):", f" V- ({len(problem.electrodes2)} injection area(s)):",
+16 -8
View File
@@ -20,19 +20,27 @@ def test_backend_prefers_qt_over_tk():
assert plots._pick_backend() in ("QtAgg", "Qt5Agg") assert plots._pick_backend() in ("QtAgg", "Qt5Agg")
def test_backend_probe_requires_working_qtcore(monkeypatch): def test_backend_probe_requires_working_qt_gui_stack(monkeypatch):
# NixOS: `import PySide6` succeeds (a pure-Python __init__) while # NixOS: `import PySide6` succeeds (a pure-Python __init__) while
# QtCore's .so cannot load the FHS system libraries pip wheels # the native .so's cannot load the FHS system libraries pip wheels
# expect. The probe must import the native core and fall through - # expect; on a partially provisioned system even QtCore loads
# promising QtAgg kills even the error figure at switch_backend # (glib, icu present) while QtWidgets/QtGui still miss libGL. The
# time, and the failure report with it. # probe must import QtWidgets and fall through - promising QtAgg
# kills even the error figure at switch_backend time, and the
# failure report with it. The mock mirrors that faithfully (bare
# package and QtCore succeed, GUI modules fail) so a probe reverted
# to `__import__(qt)` or `.QtCore` would wrongly return QtAgg here.
import builtins import builtins
import types
real_import = builtins.__import__ real_import = builtins.__import__
def broken_qt(name, *args, **kwargs): def broken_qt(name, *args, **kwargs):
if name.split(".")[0] in ("PySide6", "PyQt6", "PyQt5", "PySide2"): root, _, sub = name.partition(".")
raise ImportError("libgthread-2.0.so.0: cannot open shared " if root in ("PySide6", "PyQt6", "PyQt5", "PySide2"):
"object file: No such file or directory") if sub in ("", "QtCore"):
return types.ModuleType(name)
raise ImportError("libGL.so.1: cannot open shared object "
"file: No such file or directory")
return real_import(name, *args, **kwargs) return real_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", broken_qt) monkeypatch.setattr(builtins, "__import__", broken_qt)
+25
View File
@@ -0,0 +1,25 @@
# CI-only: the FHS environment from docs/NIXOS.md minus KiCad itself.
# The suite runs against pip wheels exactly as the plugin's venv does
# inside the wrapped KiCad: PySide6 dlopens these libraries at FHS
# paths. Keep this library list in sync with the buildFHSEnv recipe in
# docs/NIXOS.md — CI failing here means the documented recipe broke.
{ pkgs ? import <nixpkgs> { } }:
pkgs.buildFHSEnv {
name = "fill-resistance-ci";
targetPkgs = p: with p; [
bashInteractive curl cacert
glib fontconfig freetype dbus libGL libxkbcommon
xcb-util-cursor wayland zlib zstd.out
xorg.libX11 xorg.libxcb xorg.libXext xorg.libXrender
xorg.libSM xorg.libICE xorg.libXrandr xorg.libXi
xorg.libXcursor xorg.libXfixes
# xcb-util family needed by PySide6's bundled xcb platform plugin
xorg.xcbutil xorg.xcbutilwm xorg.xcbutilimage
xorg.xcbutilkeysyms xorg.xcbutilrenderutil
];
# See docs/NIXOS.md failure layer 3: the desktop's QT_PLUGIN_PATH
# must not leak into the wheel's bundled Qt.
profile = "unset QT_PLUGIN_PATH";
runScript = "bash";
}