Skip to content

Addon Unit Tests

Addon Unit Tests #263

name: Addon Unit Tests
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:
description: 'Space-separated addon names (empty = all with tests/)'
default: ''
gramps_repo:
description: 'gramps repo (owner/name); empty = same-owner fork'
default: ''
gramps_ref:
description: 'gramps branch/tag/sha'
default: 'maintenance/gramps61'
addons_source_repo:
description: 'addons-source repo (owner/name); empty = same-owner fork'
default: ''
addons_ref:
description: 'addons-source branch/tag/sha'
# gramps60 by default — the branch addons are authored on. Paired
# with gramps_ref=gramps61 on purpose; see the Checkout step below.
default: 'maintenance/gramps60'
# See interface-tests.yml for rationale — dorny/test-reporter needs
# checks: write + pull-requests: write, and the repo's default token
# permissions are restrictive.
permissions:
contents: read
checks: write
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
addon-unit-tests:
name: '[upstream] Addon Unit Tests'
runs-on: ubuntu-24.04
timeout-minutes: 20
# 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
uses: actions/checkout@v4
with:
path: gramps-testbed
# Repo resolution (in order): workflow input → repository variable
# → same-owner fork. Matches unit-tests.yml / interface-tests.yml.
- name: Checkout gramps fork
uses: actions/checkout@v4
with:
repository: ${{ github.event.inputs.gramps_repo || vars.GRAMPS_REPO || format('{0}/gramps', github.repository_owner) }}
ref: ${{ github.event.inputs.gramps_ref || vars.GRAMPS_REF || 'maintenance/gramps61' }}
path: gramps
# DELIBERATE cross-version pairing: addons_ref defaults to
# maintenance/gramps60 while gramps_ref (above) stays gramps61. Addons
# are authored on gramps60 (addons-source's production branch; the
# maintainer cherry-picks forward to gramps61). Running the as-authored
# gramps60 addon against fresh gramps61 core is the forward-compat
# early-warning this job exists for — it catches pre-cherry-pick
# import/ABI breakage that addons-source's own CIs miss (their gramps60
# CI runs 60-addon/60-core; their gramps61 CI runs the already-ported
# 61 addon). Do NOT "fix" this to 61/61 — that only duplicates the
# gramps61 CI and tests the already-forward-ported addon, so it can no
# longer be an early warning.
- name: Checkout addons-source fork
uses: actions/checkout@v4
with:
repository: ${{ github.event.inputs.addons_source_repo || vars.ADDONS_SOURCE_REPO || format('{0}/addons-source', github.repository_owner) }}
ref: ${{ github.event.inputs.addons_ref || vars.ADDONS_SOURCE_REF || 'maintenance/gramps60' }}
path: addons-source
- name: Install system dependencies
# gramps.gen imports gi on load, so GTK introspection bindings must be
# present. xvfb is included because some addons (e.g. GraphView) create
# a Gtk style context at import time, which needs a display — the
# per-addon run below wraps in xvfb-run, mirroring run-addon-unit.sh.
#
# python3-venv / python3-pip are installed here so the next step
# can build a venv that inherits python3-gi from the system
# interpreter.
run: |
sudo apt-get update
# Base stack + addon system deps derived from the single-source map
# (agent-work/scripts/lib/addon_system_deps.py) — keeps this list from drifting
# from what addons declare. gir1.2-gtk-3.0 is gramps' own base dep.
sudo apt-get install -y --no-install-recommends \
python3 python3-venv python3-pip \
python3-gi python3-gi-cairo python3-cairo python3-icu \
gir1.2-gtk-3.0 \
libgirepository1.0-dev \
xvfb \
gettext intltool \
librsvg2-common \
$(python3 gramps-testbed/agent-work/scripts/lib/addon_system_deps.py --platform ubuntu)
- name: Set up Python venv (inherits python3-gi from apt)
# See unit-tests.yml for rationale — apt-installed PyGObject is
# only visible to the system Python, not to setup-python@v5's
# toolcache interpreter, so we use a system-site-packages venv.
uses: ./gramps-testbed/.github/actions/setup-gramps-venv
- name: Restore pip cache
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('gramps-testbed/requirements-test.txt') }}
restore-keys: |
pip-${{ runner.os }}-
- name: Install Python dependencies
run: |
pip install -r gramps-testbed/requirements-test.txt
# [testing] extras pulls in jsonschema/mock/lxml from gramps setup.py,
# 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 \
gramps-testbed/agent-work/scripts/lib/addon_python_deps.py addons-source)
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. Shared via the composite action.
uses: ./gramps-testbed/.github/actions/compile-gramps-mo
with:
gramps-path: gramps
- name: Run addon unit tests
# Mirrors agent-work/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 }}
run: |
mkdir -p "$GITHUB_WORKSPACE/gramps-testbed/test-results"
if [ -n "$ADDONS" ]; then
addons=( $ADDONS )
else
addons=()
for d in "$GITHUB_WORKSPACE"/addons-source/*/tests; do
[ -d "$d" ] || continue
parent_name=$(basename "$(dirname "$d")")
[ "$parent_name" = "addons-source" ] && continue
compgen -G "$d/test_*.py" >/dev/null || continue
addons+=( "$parent_name" )
done
fi
if [ ${#addons[@]} -eq 0 ]; then
echo "no addons with tests/test_*.py were found" >&2
exit 1
fi
echo "→ addon unit tests: ${addons[*]}"
fail=0
for addon in "${addons[@]}"; do
test_dir="$GITHUB_WORKSPACE/addons-source/$addon/tests"
if [ ! -d "$test_dir" ]; then
echo "× $addon: addons-source/$addon/tests/ not found" >&2
fail=1
continue
fi
echo
echo "=== $addon ==="
out_dir="$GITHUB_WORKSPACE/gramps-testbed/test-results/$addon"
mkdir -p "$out_dir"
# Filename convention (mirrors
# addons-source/.github/workflows/ci.yml):
# test_*.py general — every platform
# test_linux_*.py Linux-only
# test_windows_*.py Windows-only (skipped here)
# test_integration_*.py Linux-only, full-pipeline/DB-backed
# This job runs on ubuntu-24.04. A sibling Windows job would
# do the inverse (skip test_linux_*/test_integration_*).
modules=()
for f in "$GITHUB_WORKSPACE"/addons-source/"$addon"/tests/test_*.py; do
[ -f "$f" ] || continue
case "$(basename "$f")" in
test_windows_*) continue ;;
esac
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
rc=0
(
cd "$GITHUB_WORKSPACE/addons-source"
# xvfb-run: addons that create a Gtk style context at import need
# a display (else a hard Gtk-ERROR abort, not a clean skip).
# PYTHONPATH gi_bootstrap: pin the GI versions (Pango/PangoCairo/
# Gtk) like gramps' GUI launcher, so importing a gramps.gui.*
# module loads the GTK 3 stack rather than warning / risking
# GTK 4 (see agent-work/scripts/lib/gi_bootstrap + run-addon-unit.sh).
PYTHONPATH="$GITHUB_WORKSPACE/gramps-testbed/agent-work/scripts/lib/gi_bootstrap${PYTHONPATH:+:$PYTHONPATH}" \
xvfb-run -a --server-args="-screen 0 1920x1080x24" \
python -m xmlrunner "${modules[@]}" \
-o "$out_dir" \
-v
) || rc=$?
# Coverage accounting from the JUnit XML: a wholly-skipped module
# still exits 0, which exit-code-only would call PASS. Fail it.
read -r t s < <(python "$GITHUB_WORKSPACE/gramps-testbed/agent-work/scripts/lib/junit_coverage.py" "$out_dir")
if [ "$rc" -ne 0 ]; then
fail=1
elif [ "${t:-0}" -gt 0 ] && [ "${s:-0}" -eq "${t:-0}" ]; then
echo "::error::$addon: all ${t} tests skipped (degraded coverage)"
fail=1
fi
done
exit $fail
- name: Validate addon translation catalogs
# Every addons-source/<addon>/po/*.po must compile with msgfmt —
# the same step `make.py build` runs. A rejected catalog aborts
# the addon build (Mantis bug 14234), and addons-source has no
# unit-test CI of its own, so this testbed check is the only
# automated guard. `if: always()` runs it even when the addon
# suites above fail, so a catalog regression is never masked.
if: always()
working-directory: gramps-testbed
env:
ADDONS_SOURCE: ${{ github.workspace }}/addons-source
run: |
mkdir -p test-results/_po-catalogs
python -m xmlrunner tests.test_addon_po_catalogs \
-o test-results/_po-catalogs -v
- name: Validate addon system dependencies
# Every requires_gi / requires_exe an addon declares must be mapped in
# agent-work/scripts/lib/addon_system_deps.py (so the apt list above stays in
# sync), and every such dep of a *tested* addon must be present in this
# environment. Guards against the per-platform drift that previously
# left graphviz out of the image. GRAMPS_TESTBED enables the presence
# check (this is the official CI environment).
if: always()
working-directory: gramps-testbed
env:
ADDONS_SOURCE: ${{ github.workspace }}/addons-source
GRAMPS_TESTBED: ubuntu
run: |
mkdir -p test-results/_system-deps
python -m xmlrunner tests.test_addon_system_deps \
-o test-results/_system-deps -v
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: addon-unit-test-results
path: gramps-testbed/test-results/
- name: Publish test report
if: always()
uses: dorny/test-reporter@v1
with:
# See interface-tests.yml for the working-directory rationale —
# checkouts use `path:` subdirs so the workflow root isn't a
# git repo; anchor on the testbed checkout.
working-directory: gramps-testbed
name: '[upstream] Addon Unit Tests'
path: test-results/**/*.xml
reporter: java-junit
fail-on-error: false # report, don't gate — see interface-tests.yml