Merge pull request #240 from ThomasBenjaminCook/issue-105-upstream-clean
Closes #105 — Linux KiCad 8/9/10 real-pcbnew smoke matrix. Thanks @ThomasBenjaminCook.
This commit is contained in:
216
.github/workflows/ci.yml
vendored
216
.github/workflows/ci.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-24.04, ubuntu-22.04, windows-latest, macos-latest]
|
os: [ubuntu-24.04, ubuntu-22.04, windows-latest, macos-latest]
|
||||||
node-version: [18.x, 20.x, 22.x]
|
node-version: [20.x, 22.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -33,6 +33,7 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
|
shell: bash
|
||||||
run: npm run lint || echo "Linter not configured yet"
|
run: npm run lint || echo "Linter not configured yet"
|
||||||
|
|
||||||
- name: Run TypeScript tests
|
- name: Run TypeScript tests
|
||||||
@@ -88,46 +89,225 @@ jobs:
|
|||||||
integration-tests:
|
integration-tests:
|
||||||
name: Integration Tests (Linux + KiCAD)
|
name: Integration Tests (Linux + KiCAD)
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
kicad: ["8.0", "9.0", "10.0"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20.x"
|
|
||||||
cache: "npm"
|
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
cache: "pip"
|
cache: "pip"
|
||||||
|
|
||||||
- name: Add KiCAD PPA and Install KiCAD 9.0
|
- name: Add KiCAD PPA and Install KiCAD ${{ matrix.kicad }}
|
||||||
run: |
|
run: |
|
||||||
sudo add-apt-repository --yes ppa:kicad/kicad-9.0-releases
|
sudo add-apt-repository --yes ppa:kicad/kicad-${{ matrix.kicad }}-releases
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y kicad kicad-libraries
|
sudo apt-get install -y kicad kicad-libraries
|
||||||
|
|
||||||
|
mapfile -t KICAD_PYTHON_PACKAGES < <(
|
||||||
|
{
|
||||||
|
apt-cache search kicad
|
||||||
|
apt-cache search pcbnew
|
||||||
|
} | awk '
|
||||||
|
BEGIN { IGNORECASE = 1 }
|
||||||
|
{
|
||||||
|
name = $1
|
||||||
|
line = tolower($0)
|
||||||
|
if (name !~ /nightly/ && (line ~ /kicad/ || line ~ /pcbnew/) && line ~ /python/) print name
|
||||||
|
}
|
||||||
|
' | sort -u
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "${#KICAD_PYTHON_PACKAGES[@]}" -gt 0 ]; then
|
||||||
|
echo "Installing KiCad Python packages: ${KICAD_PYTHON_PACKAGES[*]}"
|
||||||
|
sudo apt-get install -y "${KICAD_PYTHON_PACKAGES[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
KICAD_PYTHONPATH="$(python3 - <<'PY'
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
|
||||||
|
def add_candidate(root: Path) -> None:
|
||||||
|
if not root.exists():
|
||||||
|
return
|
||||||
|
root_str = str(root)
|
||||||
|
if root_str not in seen:
|
||||||
|
seen.add(root_str)
|
||||||
|
candidates.append(root_str)
|
||||||
|
|
||||||
|
|
||||||
|
def add_candidate_for_file(file_path: Path) -> None:
|
||||||
|
if file_path.name == "__init__.py" and file_path.parent.name == "pcbnew":
|
||||||
|
add_candidate(file_path.parent.parent)
|
||||||
|
return
|
||||||
|
if file_path.parent.name == "pcbnew":
|
||||||
|
add_candidate(file_path.parent.parent)
|
||||||
|
return
|
||||||
|
add_candidate(file_path.parent)
|
||||||
|
|
||||||
|
|
||||||
|
def looks_like_pcbnew(file_path: Path) -> bool:
|
||||||
|
name = file_path.name
|
||||||
|
if name == "pcbnew.py":
|
||||||
|
return True
|
||||||
|
if name == "__init__.py" and file_path.parent.name == "pcbnew":
|
||||||
|
return True
|
||||||
|
return name.endswith(".so") and (
|
||||||
|
name.startswith("pcbnew") or name.startswith("_pcbnew")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_from_package(package: str) -> None:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["dpkg", "-L", package],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return
|
||||||
|
|
||||||
|
for raw_path in result.stdout.splitlines():
|
||||||
|
path = Path(raw_path)
|
||||||
|
if path.is_file() and looks_like_pcbnew(path):
|
||||||
|
add_candidate_for_file(path)
|
||||||
|
|
||||||
|
|
||||||
|
package_result = subprocess.run(
|
||||||
|
["dpkg-query", "-W", "-f=${Package}\n"],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
installed_packages = [
|
||||||
|
package
|
||||||
|
for package in package_result.stdout.splitlines()
|
||||||
|
if "kicad" in package or "pcbnew" in package
|
||||||
|
]
|
||||||
|
|
||||||
|
for package in installed_packages:
|
||||||
|
collect_from_package(package)
|
||||||
|
|
||||||
|
file_patterns = [
|
||||||
|
"/usr/lib/python3/dist-packages/pcbnew.py",
|
||||||
|
"/usr/lib/python3/dist-packages/pcbnew*.so",
|
||||||
|
"/usr/lib/python3/dist-packages/_pcbnew*.so",
|
||||||
|
"/usr/lib/python3/dist-packages/pcbnew/__init__.py",
|
||||||
|
"/usr/lib/python3/dist-packages/pcbnew/_pcbnew*.so",
|
||||||
|
"/usr/lib/kicad*/lib/python3/dist-packages/pcbnew.py",
|
||||||
|
"/usr/lib/kicad*/lib/python3/dist-packages/pcbnew*.so",
|
||||||
|
"/usr/lib/kicad*/lib/python3/dist-packages/_pcbnew*.so",
|
||||||
|
"/usr/lib/kicad*/lib/python3/dist-packages/pcbnew/__init__.py",
|
||||||
|
"/usr/lib/kicad*/lib/python3/dist-packages/pcbnew/_pcbnew*.so",
|
||||||
|
"/usr/lib/*/dist-packages/pcbnew.py",
|
||||||
|
"/usr/lib/*/dist-packages/pcbnew*.so",
|
||||||
|
"/usr/lib/*/dist-packages/_pcbnew*.so",
|
||||||
|
"/usr/lib/*/dist-packages/pcbnew/__init__.py",
|
||||||
|
"/usr/lib/*/dist-packages/pcbnew/_pcbnew*.so",
|
||||||
|
"/usr/local/lib/python*/dist-packages/pcbnew.py",
|
||||||
|
"/usr/local/lib/python*/dist-packages/pcbnew*.so",
|
||||||
|
"/usr/local/lib/python*/dist-packages/_pcbnew*.so",
|
||||||
|
"/usr/local/lib/python*/dist-packages/pcbnew/__init__.py",
|
||||||
|
"/usr/local/lib/python*/dist-packages/pcbnew/_pcbnew*.so",
|
||||||
|
"/usr/share/kicad*/scripting/pcbnew.py",
|
||||||
|
"/usr/share/kicad*/scripting/pcbnew*.so",
|
||||||
|
"/usr/share/kicad*/scripting/_pcbnew*.so",
|
||||||
|
"/usr/share/kicad*/scripting/pcbnew/__init__.py",
|
||||||
|
"/usr/share/kicad*/scripting/pcbnew/_pcbnew*.so",
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in file_patterns:
|
||||||
|
for raw_path in glob.glob(pattern):
|
||||||
|
path = Path(raw_path)
|
||||||
|
if path.is_file() and looks_like_pcbnew(path):
|
||||||
|
add_candidate_for_file(path)
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
for file_path in Path("/usr").rglob("*"):
|
||||||
|
if file_path.is_file() and looks_like_pcbnew(file_path):
|
||||||
|
add_candidate_for_file(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
def import_succeeds(candidate: str) -> bool:
|
||||||
|
env = os.environ.copy()
|
||||||
|
existing = env.get("PYTHONPATH", "")
|
||||||
|
env["PYTHONPATH"] = (
|
||||||
|
f"{candidate}:{existing}" if existing else candidate
|
||||||
|
)
|
||||||
|
probe = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-c",
|
||||||
|
(
|
||||||
|
"import pcbnew; "
|
||||||
|
"print(getattr(pcbnew, '__file__', '')); "
|
||||||
|
"print(pcbnew.GetBuildVersion())"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
if probe.returncode == 0:
|
||||||
|
print(
|
||||||
|
f"Using pcbnew from {probe.stdout.splitlines()[0]}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
working_candidates = [candidate for candidate in candidates if import_succeeds(candidate)]
|
||||||
|
print(":".join(working_candidates))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [ -z "$KICAD_PYTHONPATH" ]; then
|
||||||
|
echo "Unable to locate pcbnew Python bindings after installing KiCad ${{ matrix.kicad }}" >&2
|
||||||
|
{
|
||||||
|
apt-cache search kicad
|
||||||
|
apt-cache search pcbnew
|
||||||
|
} | awk 'BEGIN { IGNORECASE = 1 } tolower($0) ~ /python/ { print }' || true
|
||||||
|
dpkg-query -W -f='${Package}\n' | grep -Ei 'kicad|pcbnew' || true
|
||||||
|
find /usr -path '*dist-packages*' \( -name 'pcbnew.py' -o -name 'pcbnew*.so' -o -name '_pcbnew*.so' \) -print || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "KICAD_PYTHONPATH=$KICAD_PYTHONPATH" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Verify KiCAD installation
|
- name: Verify KiCAD installation
|
||||||
run: |
|
run: |
|
||||||
kicad-cli version || echo "kicad-cli not found"
|
export KICAD_CLI="kicad-cli"
|
||||||
python3 -c "import pcbnew; print(f'pcbnew version: {pcbnew.GetBuildVersion()}')" || echo "pcbnew module not found"
|
export PYTHONPATH="$KICAD_PYTHONPATH:$PYTHONPATH"
|
||||||
|
"$KICAD_CLI" version
|
||||||
|
python3 -c "import pcbnew; print(f'pcbnew version: {pcbnew.GetBuildVersion()}')"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
python -m pip install --upgrade pip
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
|
||||||
|
|
||||||
- name: Build TypeScript
|
- name: Run real pcbnew smoke tests
|
||||||
run: npm run build
|
env:
|
||||||
|
KICAD_USE_REAL_PCBNEW: "1"
|
||||||
- name: Run integration tests
|
|
||||||
run: |
|
run: |
|
||||||
echo "Integration tests not yet configured"
|
export PYTHONPATH="$KICAD_PYTHONPATH:$PYTHONPATH"
|
||||||
# pytest tests/integration/
|
pytest tests/test_real_pcbnew_matrix.py -m "integration and real_pcbnew"
|
||||||
|
|
||||||
# Docker build test
|
# Docker build test
|
||||||
docker-build:
|
docker-build:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ addopts =
|
|||||||
markers =
|
markers =
|
||||||
unit: Unit tests (fast, no external dependencies)
|
unit: Unit tests (fast, no external dependencies)
|
||||||
integration: Integration tests (requires KiCAD)
|
integration: Integration tests (requires KiCAD)
|
||||||
|
real_pcbnew: Headless tests that exercise real KiCad SWIG bindings
|
||||||
slow: Slow-running tests
|
slow: Slow-running tests
|
||||||
linux: Linux-specific tests
|
linux: Linux-specific tests
|
||||||
windows: Windows-specific tests
|
windows: Windows-specific tests
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ test module can trigger their import, preventing crashes on systems where the
|
|||||||
real KiCAD environment is not fully initialised for testing.
|
real KiCAD environment is not fully initialised for testing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
@@ -16,12 +17,13 @@ from unittest.mock import MagicMock
|
|||||||
# attribute access (pcbnew.BOARD, pcbnew.PCB_TRACK, …) returns a mock
|
# attribute access (pcbnew.BOARD, pcbnew.PCB_TRACK, …) returns a mock
|
||||||
# rather than raising AttributeError.
|
# rather than raising AttributeError.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
_pcbnew = MagicMock(name="pcbnew")
|
if os.environ.get("KICAD_USE_REAL_PCBNEW") != "1":
|
||||||
_pcbnew.__file__ = "/fake/pcbnew.cpython-313-x86_64-linux-gnu.so"
|
_pcbnew = MagicMock(name="pcbnew")
|
||||||
_pcbnew.__name__ = "pcbnew"
|
_pcbnew.__file__ = "/fake/pcbnew.cpython-313-x86_64-linux-gnu.so"
|
||||||
_pcbnew.__spec__ = None
|
_pcbnew.__name__ = "pcbnew"
|
||||||
_pcbnew.GetBuildVersion.return_value = "9.0.0-stub"
|
_pcbnew.__spec__ = None
|
||||||
sys.modules["pcbnew"] = _pcbnew
|
_pcbnew.GetBuildVersion.return_value = "9.0.0-stub"
|
||||||
|
sys.modules["pcbnew"] = _pcbnew
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Stub: skip (kicad-skip — use real module if available, stub otherwise)
|
# Stub: skip (kicad-skip — use real module if available, stub otherwise)
|
||||||
|
|||||||
43
tests/test_real_pcbnew_matrix.py
Normal file
43
tests/test_real_pcbnew_matrix.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
PYTHON_DIR = Path(__file__).parent.parent / "python"
|
||||||
|
sys.path.insert(0, str(PYTHON_DIR))
|
||||||
|
|
||||||
|
pytestmark = [
|
||||||
|
pytest.mark.integration,
|
||||||
|
pytest.mark.real_pcbnew,
|
||||||
|
pytest.mark.linux,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def require_real_pcbnew() -> None:
|
||||||
|
if os.environ.get("KICAD_USE_REAL_PCBNEW") != "1":
|
||||||
|
pytest.skip("real pcbnew smoke tests require KICAD_USE_REAL_PCBNEW=1")
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_commands_create_and_load_board_with_real_pcbnew(tmp_path: Path) -> None:
|
||||||
|
import pcbnew
|
||||||
|
|
||||||
|
from commands.project import ProjectCommands
|
||||||
|
|
||||||
|
version = pcbnew.GetBuildVersion()
|
||||||
|
assert version
|
||||||
|
assert not str(version).endswith("-stub")
|
||||||
|
|
||||||
|
commands = ProjectCommands()
|
||||||
|
result = commands.create_project({"name": "matrix_smoke", "path": str(tmp_path)})
|
||||||
|
|
||||||
|
assert result["success"] is True, result
|
||||||
|
|
||||||
|
board_path = Path(result["project"]["boardPath"])
|
||||||
|
assert board_path.exists()
|
||||||
|
|
||||||
|
board = pcbnew.LoadBoard(str(board_path))
|
||||||
|
assert board is not None
|
||||||
|
assert hasattr(board, "GetFileName")
|
||||||
|
assert board.GetFileName().endswith(".kicad_pcb")
|
||||||
Reference in New Issue
Block a user