Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/scheduled_tasks.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"sessionId":"0f53409e-bc5c-4515-a840-714951478989","pid":8846,"acquiredAt":1776708261236}
95 changes: 82 additions & 13 deletions .github/workflows/addon-unit-tests.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
name: Addon Unit Tests

# No push/PR triggers: addon unit coverage is opt-in while the addon
# ecosystem stabilises. Run manually against a single addon, a list, or
# leave the input blank to run every addon that has tests/test_*.py.
on:
push:
branches: [main]
pull_request:
schedule:
# Nightly drift check at 06:30 UTC — 2.5h after upstream-sync (04:00)
# and 30min after docker-build (06:00), keeping the existing
# staggered-by-30min cadence. Surfaces addon-side import/ABI
# regressions against fresh upstream, which is exactly the class of
# bug the testbed catches while addons-source's own CI stays green.
- cron: '30 6 * * *'
workflow_dispatch:
inputs:
addons:
Expand Down Expand Up @@ -34,9 +41,12 @@ jobs:
addon-unit-tests:
runs-on: ubuntu-24.04
timeout-minutes: 20
# No gate policy (continue-on-error) needed: workflow_dispatch-only
# triggers mean this job never blocks a PR merge. It's a manual
# diagnostic run, not a required check.
# Advisory gate. Addon tests run upstream addon code, so failures
# here usually reflect addon/upstream bugs rather than testbed
# issues. continue-on-error keeps the workflow conclusion green
# while the per-addon check-run surfaces the failing addon — same
# shape as unit-tests.yml.
continue-on-error: true

steps:
- name: Checkout testbed
Expand Down Expand Up @@ -102,6 +112,49 @@ jobs:
# which some addon tests also import transitively.
pip install -e 'gramps[testing]'

- name: Install addon runtime deps (derived from requires_mod)
# Auto-derive the union of requires_mod across every .gpr.py in
# the checked-out addons-source/. Mirrors Gramps' Addon Manager
# install path (gramps/gui/plug/_windows.py __on_install_clicked
# → req.install → gen/utils/requirements.py). Keeps the .gpr.py
# files as the single source of truth for addon deps — no
# parallel list to drift from upstream. Best-effort: a package
# that needs exotic system deps (e.g. pygraphviz → graphviz-dev)
# may fail here; the affected addon's tests will skip or fail in
# isolation without blocking the rest.
shell: bash
run: |
addon_mods=$(python3 - <<'PY'
import ast, glob, re
pat = re.compile(r"requires_mod\s*=\s*(\[[^\]]*\])")
mods = set()
for f in glob.glob("addons-source/*/*.gpr.py"):
try:
text = open(f, encoding="utf-8").read()
except OSError:
continue
for m in pat.finditer(text):
try:
mods.update(ast.literal_eval(m.group(1)))
except (ValueError, SyntaxError):
pass
print(" ".join(sorted(mods)))
PY
)
if [ -n "$addon_mods" ]; then
echo "→ addon deps: $addon_mods"
# Install one at a time so a single failing build
# (pygraphviz without graphviz-dev, psycopg2 without
# libpq-dev, etc.) does not abort the batch. The affected
# addon's tests will skip or fail in isolation.
for mod in $addon_mods; do
pip install "$mod" || \
echo "× $mod failed to install (continuing)"
done
else
echo "no requires_mod declarations found"
fi

- name: Compile Gramps translations
# Same rationale as unit-tests.yml: keeps gramps.gen imports quiet
# during test collection.
Expand All @@ -115,10 +168,14 @@ jobs:
done

- name: Run addon unit tests
# Mirrors scripts/ubuntu/run-addon-unit.sh discovery. Each addon's
# tests/ dir is treated as the start+top dir because most addons
# do not ship tests/__init__.py; __file__-based sys.path hacks
# inside the tests resolve the addon module themselves.
# Mirrors scripts/ubuntu/run-addon-unit.sh. Each test module is
# loaded by its dotted path (<Addon>.tests.<module>) from
# addons-source/, matching the upstream invocation in
# addons-source/.github/workflows/ci.yml. Loading via dotted
# path — not discover-from-tests/ — puts the addon on
# sys.modules as a namespace package before the test body runs,
# which is the exact arrangement that exposes package-shadowing
# traps like bug 0012691. Discover inside tests/ hides them.
env:
GRAMPS_RESOURCES: ${{ github.workspace }}/gramps
ADDONS: ${{ github.event.inputs.addons }}
Expand Down Expand Up @@ -157,10 +214,22 @@ jobs:
echo "=== $addon ==="
out_dir="$GITHUB_WORKSPACE/gramps-testbed/test-results/$addon"
mkdir -p "$out_dir"
modules=()
for f in "$GITHUB_WORKSPACE"/addons-source/"$addon"/tests/test_*.py; do
[ -f "$f" ] || continue
rel="${f#$GITHUB_WORKSPACE/addons-source/}"
mod="${rel%.py}"
mod="${mod//\//.}"
modules+=( "$mod" )
done
if [ ${#modules[@]} -eq 0 ]; then
echo "× $addon: no test_*.py in tests/" >&2
fail=1
continue
fi
(
cd "$test_dir"
python -m xmlrunner discover \
-p 'test_*.py' \
cd "$GITHUB_WORKSPACE/addons-source"
python -m xmlrunner "${modules[@]}" \
-o "$out_dir" \
-v
) || fail=1
Expand Down
21 changes: 16 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ If `../addons-source/AGENTS.md` exists, it applies inside that repo:
- ./scripts/ubuntu/run-interface.sh — dogtail/AT-SPI GUI tests
- ./scripts/ubuntu/run-unit.sh — gramps' own *_test.py suite (no GUI)
- ./scripts/ubuntu/run-addon-unit.sh [addon ...] — per-addon tests/test_*.py (no GUI)
- Before pushing an addon-source change that touches tests or the addon
module itself, run `./scripts/ubuntu/run-addon-unit.sh <AddonName>` on
the PR branch. The runner loads each test via its dotted path
(`<Addon>.tests.<module>`) — the same form upstream's ci.yml uses —
which is what surfaces Python namespace-package traps that
`discover`-from-tests/ hides. Bug 0012691 was exactly this class of
bug: `from <Addon> import <Addon>` bound the submodule instead of the
class under dotted-path loading.
- Platform-specific scripts live under ./scripts/<platform>/; today only
scripts/ubuntu/ exists (Fedora/Arch/macOS/Windows equivalents are planned)
- Docker image: gramps-testbed:ubuntu-<gramps-version> (e.g. gramps-testbed:ubuntu-6.0.8),
Expand All @@ -34,9 +42,11 @@ If `../addons-source/AGENTS.md` exists, it applies inside that repo:

## Status
Interface smoke suite (tests/interface/test_smoke.py) passes locally and in CI
— the previous "smoke before all else" priority is cleared. No singular next
focus yet; natural candidates are expanding interface coverage, porting
scripts to other platforms, or scheduling the addon-unit suite.
— the previous "smoke before all else" priority is cleared. Addon-unit suite
is now scheduled (push/PR/nightly) and invokes tests via dotted path, so
namespace-package bugs surface in the testbed. No singular next focus yet;
natural candidates are expanding interface coverage or porting scripts to
other platforms.

## CI gates and branch protection
- `main` is protected by ruleset "main branch protection" (id 15262402):
Expand All @@ -55,7 +65,8 @@ scripts to other platforms, or scheduling the addon-unit suite.
apt-installed PyGObject and everything that imports `gi` dies on load.
- Adding a new Gramps minor: append to `matrix.gramps_ref` in
unit-tests.yml and interface-tests.yml (keep them in sync).
- Nightly drift crons fire 1-2h after upstream-sync (04:00 UTC): unit
tests 05:00, interface tests 05:30, docker build 06:00.
- Nightly drift crons fire 1-2.5h after upstream-sync (04:00 UTC): unit
tests 05:00, interface tests 05:30, docker build 06:00, addon unit
tests 06:30.
- `eduralph/gramps` and `eduralph/addons-source` carry a parallel
"PRFirst" ruleset on `master` + `maintenance/gramps60` (no bypass).
8 changes: 7 additions & 1 deletion docker/Dockerfile.ubuntu
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ ENV LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \
LANGUAGE=en_US:en

# dogtail + JUnit XML writer for unittest (pyatspi comes from python3-pyatspi apt package)
# dogtail + JUnit XML writer for unittest (pyatspi comes from python3-pyatspi apt package).
# Addon runtime deps (dbf, boto3, networkx, psycopg2, etc.) are NOT baked
# in here — the run-addon-unit.sh entrypoint auto-derives them at
# container start from requires_mod declarations in the currently
# checked-out addons-source/*/*.gpr.py files. That way the image stays
# generic and the addon dep list is single-sourced from the .gpr.py
# files (the same data Gramps' Addon Manager consumes for end users).
RUN pip3 install --break-system-packages --no-cache-dir \
dogtail \
unittest-xml-reporting
Expand Down
4 changes: 4 additions & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
dogtail
unittest-xml-reporting
# Addon runtime deps (dbf, boto3, networkx, psycopg2, etc.) are NOT
# listed here. The addon-unit workflow auto-derives them from
# requires_mod declarations in addons-source/*/*.gpr.py at runtime,
# matching what Gramps' Addon Manager installs for an end user.
79 changes: 65 additions & 14 deletions scripts/ubuntu/run-addon-unit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,45 @@ docker run --rm \
# extras syntax against absolute paths, so resolve via a relative path.
(cd /workspace && pip install --break-system-packages --user -e "./gramps[testing]")
export PATH="$HOME/.local/bin:$PATH"

# Auto-derive addon Python deps from requires_mod in every .gpr.py
# under addons-source/, then pip-install the union. Mirrors what
# Gramps Addon Manager does for an end user on Install click (see
# gramps/gui/plug/_windows.py __on_install_clicked → req.install →
# gen/utils/requirements.py). The .gpr.py files are the single
# source of truth — this keeps the test environment in sync with
# whatever the currently-checked-out addons-source declares, so
# new addon deps do not need a parallel update here.
echo "→ discovering addon deps from requires_mod declarations"
addon_mods=$(python3 - <<"PY"
import ast, glob, re
pat = re.compile(r"requires_mod\s*=\s*(\[[^\]]*\])")
mods = set()
for f in glob.glob("/workspace/addons-source/*/*.gpr.py"):
try:
text = open(f, encoding="utf-8").read()
except OSError:
continue
for m in pat.finditer(text):
try:
mods.update(ast.literal_eval(m.group(1)))
except (ValueError, SyntaxError):
pass
print(" ".join(sorted(mods)))
PY
)
if [ -n "$addon_mods" ]; then
echo "→ addon deps: $addon_mods"
# Install one at a time so a single failing build (pygraphviz
# without graphviz-dev, psycopg2 without libpq-dev, etc.) does
# not abort the batch. The affected addon''s tests will skip or
# fail in isolation without blocking the rest.
for mod in $addon_mods; do
pip install --break-system-packages --user "$mod" \
|| echo "× $mod failed to install (continuing)"
done
fi

# Compile .mo translations so gramps.gen imports do not emit
# "Missing or invalid localedir" during addon test collection.
if [ ! -f /workspace/gramps/build/mo/de/LC_MESSAGES/gramps.mo ]; then
Expand Down Expand Up @@ -108,22 +147,34 @@ docker run --rm \
# for any addon test that touches gramps.gen resource loading.
out_dir="/workspace/gramps-testbed/test-results/$addon"
mkdir -p "$out_dir"
# Run from the tests/ dir itself. Many addon tests/ dirs lack an
# __init__.py, so unittest cannot treat tests/ as an importable
# package; entering it makes tests/ the start+top dir, and the
# __file__-based sys.path hacks inside test_*.py still resolve the
# addon module (they use os.path.dirname(os.path.abspath(__file__))).
#
# PYTHONPATH pins /workspace/addons-source so tests that use
# package-style imports (from <Addon>.<mod> import ...) resolve via
# namespace-package lookup, matching the documented
# "PYTHONPATH=. python -m unittest ..." invocation used upstream.
# Collect dotted module paths (<Addon>.tests.<module>) and invoke
# unittest/xmlrunner with the module list, running from
# addons-source/. This mirrors how addons-source/.github/workflows/
# ci.yml calls the suite and how contributors invoke
# "python3 -m unittest <Addon>.tests.test_..." locally. Critically,
# loading a test via its dotted path — not via "discover" inside
# tests/ — puts the addon on sys.modules as a namespace package
# before the test body runs. That is the exact arrangement that
# surfaces package-shadowing bugs like bug 0012691, where
# "from <Addon> import <Addon>" binds the submodule instead of the
# class. Discover-from-tests/ hides the trap.
modules=()
for f in /workspace/addons-source/"$addon"/tests/test_*.py; do
[ -f "$f" ] || continue
rel="${f#/workspace/addons-source/}"
mod="${rel%.py}"
mod="${mod//\//.}"
modules+=( "$mod" )
done
if [ ${#modules[@]} -eq 0 ]; then
echo "× $addon: no test_*.py in tests/" >&2
fail=1
continue
fi
(
cd "$test_dir"
cd /workspace/addons-source
GRAMPS_RESOURCES=/workspace/gramps \
PYTHONPATH="/workspace/addons-source${PYTHONPATH:+:$PYTHONPATH}" \
python3 -m xmlrunner discover \
-p "test_*.py" \
python3 -m xmlrunner "${modules[@]}" \
-o "$out_dir" \
-v
) || fail=1
Expand Down
Loading