Fix the two macOS field-test failures: backend order, RO board dir
Build PCM package / build (push) Successful in 5s
Build PCM package / build (push) Successful in 5s
Second Mac run (KiCad demo board opened from the mounted installer image) got past the dialog and died twice: 1. make_output_dir crashed with OSError 30: the demos volume is a read-only filesystem. Now falls back to a temp directory named after the board, with the path printed (the Messages panel showed it is actually read there). 2. The error figure - and every other figure - could never render: matplotlib refused with Cannot load backend TkAgg ... as qt is currently running. macOS bundled Python ships tkinter, so the old tk-first probe picked TkAgg while the PySide6 dialog and progress window had already made this a Qt process. Windows never saw it because KiCad Python there has no tkinter. Qt now comes first - PySide6 is a hard dependency, so it is always there. Both covered by tests that fail on the old code: the dev env has both toolkits installed, so the backend-order assertion is exercised for real, and the RO fallback is simulated by denying mkdir under the board dir. 140 passed on the dev stack and on the Python 3.9 + numpy 2.0 / scipy 1.13 / matplotlib 3.9 / PySide6 6.10 stack that a Mac venv resolves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -123,7 +123,10 @@ notes* below.
|
|||||||
multi-layer pours at fine cell sizes may run for minutes (on our
|
multi-layer pours at fine cell sizes may run for minutes (on our
|
||||||
test setup a typical real-board run finishes in ≈ 8 s). Then read
|
test setup a typical real-board run finishes in ≈ 8 s). Then read
|
||||||
R / voltage drop / total power in the figure titles and status
|
R / voltage drop / total power in the figure titles and status
|
||||||
bar. Outputs land in `<board dir>/fill_res_results/<timestamp>/`:
|
bar. Outputs land in `<board dir>/fill_res_results/<timestamp>/`
|
||||||
|
(if the board directory is not writable — e.g. a demo project opened
|
||||||
|
straight from the mounted installer image — a temp directory is used
|
||||||
|
instead and its path printed to the Messages panel):
|
||||||
per-layer `1_raster_map` / `2_potential` / `3_current_density` /
|
per-layer `1_raster_map` / `2_potential` / `3_current_density` /
|
||||||
`4_power_density` PNGs, `summary.txt` (incl. the busiest vias with
|
`4_power_density` PNGs, `summary.txt` (incl. the busiest vias with
|
||||||
per-via current and dissipation, and the **current through each
|
per-via current and dissipation, and the **current through each
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"""Figures: per-layer rasterized maps, potential, current density, power
|
"""Figures: per-layer rasterized maps, potential, current density, power
|
||||||
density, and the error figure. PNGs are saved BEFORE any window opens.
|
density, and the error figure. PNGs are saved BEFORE any window opens.
|
||||||
|
|
||||||
Backend: interactive if a GUI toolkit exists (tkinter, else Qt), else Agg
|
Backend: interactive if a GUI toolkit exists (Qt first, tkinter as a
|
||||||
with os.startfile on the saved PNGs so results are never silent.
|
fallback), else Agg with the OS default viewer on the saved PNGs so
|
||||||
|
results are never silent.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -18,19 +19,23 @@ import numpy as np
|
|||||||
|
|
||||||
def _pick_backend():
|
def _pick_backend():
|
||||||
"""matplotlib.use() is lazy and 'succeeds' for backends whose GUI
|
"""matplotlib.use() is lazy and 'succeeds' for backends whose GUI
|
||||||
toolkit is missing (KiCad's Python has no tkinter), so probe the
|
toolkit is missing (KiCad's Windows Python has no tkinter), so probe
|
||||||
toolkits explicitly."""
|
the toolkits explicitly. Qt MUST come first: PySide6 is a hard
|
||||||
try:
|
dependency and the selection dialog / progress window put a Qt event
|
||||||
import tkinter # noqa: F401
|
loop in this process, after which matplotlib refuses TkAgg
|
||||||
return "TkAgg"
|
("Cannot load backend 'TkAgg' ... as 'qt' is currently running") -
|
||||||
except Exception:
|
exactly what happened on macOS, whose bundled Python ships tkinter."""
|
||||||
pass
|
|
||||||
for qt in ("PySide6", "PyQt6", "PyQt5", "PySide2"):
|
for qt in ("PySide6", "PyQt6", "PyQt5", "PySide2"):
|
||||||
try:
|
try:
|
||||||
__import__(qt)
|
__import__(qt)
|
||||||
return "QtAgg" if qt in ("PySide6", "PyQt6") else "Qt5Agg"
|
return "QtAgg" if qt in ("PySide6", "PyQt6") else "Qt5Agg"
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
try:
|
||||||
|
import tkinter # noqa: F401
|
||||||
|
return "TkAgg"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Output directory, summary.txt, geometry dump, stdout one-liner."""
|
"""Output directory, summary.txt, geometry dump, stdout one-liner."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -14,8 +15,20 @@ from .solver import Result
|
|||||||
|
|
||||||
def make_output_dir(board_dir: Path) -> Path:
|
def make_output_dir(board_dir: Path) -> Path:
|
||||||
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
out = Path(board_dir) / config.OUTPUT_DIRNAME / stamp
|
board_dir = Path(board_dir)
|
||||||
out.mkdir(parents=True, exist_ok=True)
|
out = board_dir / config.OUTPUT_DIRNAME / stamp
|
||||||
|
try:
|
||||||
|
out.mkdir(parents=True, exist_ok=True)
|
||||||
|
except OSError as e:
|
||||||
|
# The board can live somewhere unwritable - e.g. the demos
|
||||||
|
# folder on the mounted KiCad installer image (read-only, and
|
||||||
|
# how the first macOS field test was run). Results still have
|
||||||
|
# to land somewhere the figures/summary can be written.
|
||||||
|
out = (Path(tempfile.gettempdir()) / config.OUTPUT_DIRNAME
|
||||||
|
/ f"{board_dir.name}-{stamp}")
|
||||||
|
print(f"board directory not writable ({e}); saving results to "
|
||||||
|
f"{out}")
|
||||||
|
out.mkdir(parents=True, exist_ok=True)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Regressions from the first macOS field test.
|
||||||
|
|
||||||
|
Two failures that only a non-Windows KiCad could produce: the board
|
||||||
|
directory was read-only (demo project opened straight from the mounted
|
||||||
|
installer image), and matplotlib picked TkAgg - macOS' bundled Python
|
||||||
|
ships tkinter, unlike KiCad's Windows Python - then refused to create
|
||||||
|
any figure because the PySide6 dialog already had a Qt event loop in
|
||||||
|
the process.
|
||||||
|
"""
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
from fill_resistance import plots, report
|
||||||
|
|
||||||
|
|
||||||
|
def test_backend_prefers_qt_over_tk():
|
||||||
|
# The dev environment has both toolkits installed, so this asserts
|
||||||
|
# the preference order for real: Qt must win, because the selection
|
||||||
|
# dialog / progress window make the process a Qt process before the
|
||||||
|
# first figure exists.
|
||||||
|
assert plots._pick_backend() in ("QtAgg", "Qt5Agg")
|
||||||
|
|
||||||
|
|
||||||
|
def test_output_dir_falls_back_when_board_dir_unwritable(
|
||||||
|
tmp_path, monkeypatch, capsys):
|
||||||
|
board_dir = tmp_path / "board"
|
||||||
|
board_dir.mkdir()
|
||||||
|
real_mkdir = pathlib.Path.mkdir
|
||||||
|
|
||||||
|
def deny_under_board(self, *args, **kwargs):
|
||||||
|
if str(self).startswith(str(board_dir)):
|
||||||
|
raise OSError(30, "Read-only file system", str(self))
|
||||||
|
return real_mkdir(self, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(pathlib.Path, "mkdir", deny_under_board)
|
||||||
|
out = report.make_output_dir(board_dir)
|
||||||
|
assert out.is_dir()
|
||||||
|
assert not str(out).startswith(str(board_dir))
|
||||||
|
assert board_dir.name in out.name # traceable back to the board
|
||||||
|
assert "not writable" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
def test_output_dir_normal_case_unchanged(tmp_path):
|
||||||
|
out = report.make_output_dir(tmp_path)
|
||||||
|
assert out.is_dir()
|
||||||
|
assert out.parent.parent == tmp_path
|
||||||
Reference in New Issue
Block a user