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
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)
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") -
exactly what happened on macOS, whose bundled Python ships tkinter.
The probe must import the binding's native core, not just the
package: on NixOS `import PySide6` succeeds (pure __init__) while
QtCore's .so cannot find the system libraries pip wheels expect
("libgthread-2.0.so.0: cannot open shared object file") - promising
QtAgg then kills even the error figure at switch_backend time."""
The probe must import QtWidgets, not just the package or QtCore:
on NixOS `import PySide6` succeeds (pure __init__) while QtCore's
.so cannot find the system libraries pip wheels expect
("libgthread-2.0.so.0: cannot open shared object file"), and on a
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"):
try:
__import__(qt + ".QtCore")
__import__(qt + ".QtWidgets")
return "QtAgg" if qt in ("PySide6", "PyQt6") else "Qt5Agg"
except Exception:
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"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)"
if result.freq_hz > 0 else "DC")),
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}"
+ (" (uniform orthogonal injection; R is the upper contact bound)"
if result.contact_model == "uniform" else " (ideal bonded lug)"),
f"terminals:",
"terminals:",
f" V+ ({len(problem.electrodes1)} injection area(s)):",
*(f" {_electrode_line(e)}" for e in problem.electrodes1),
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")
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
# QtCore's .so cannot load the FHS system libraries pip wheels
# expect. The probe must import the native core and fall through -
# promising QtAgg kills even the error figure at switch_backend
# time, and the failure report with it.
# the native .so's cannot load the FHS system libraries pip wheels
# expect; on a partially provisioned system even QtCore loads
# (glib, icu present) while QtWidgets/QtGui still miss libGL. The
# 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 types
real_import = builtins.__import__
def broken_qt(name, *args, **kwargs):
if name.split(".")[0] in ("PySide6", "PyQt6", "PyQt5", "PySide2"):
raise ImportError("libgthread-2.0.so.0: cannot open shared "
"object file: No such file or directory")
root, _, sub = name.partition(".")
if root in ("PySide6", "PyQt6", "PyQt5", "PySide2"):
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)
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";
}