Skip to content

addon-unit: dotted-path invocation + scheduled runs #4

addon-unit: dotted-path invocation + scheduled runs

addon-unit: dotted-path invocation + scheduled runs #4

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/gramps60'
addons_source_repo:
description: 'addons-source repo (owner/name); empty = same-owner fork'
default: ''
addons_ref:
description: 'addons-source branch/tag/sha'
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
jobs:
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/gramps60' }}
path: gramps
- 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
# No xvfb/dbus/at-spi — addon unit tests do not launch the GUI.
# Same apt list as unit-tests.yml: gramps.gen still imports gi on
# load, so GTK introspection bindings must be present.
#
# 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
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 gir1.2-osmgpsmap-1.0 gir1.2-goocanvas-2.0 gir1.2-gexiv2-0.10 \
libgirepository1.0-dev \
gettext intltool \
librsvg2-common
- 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.
run: |
python3 -m venv --system-site-packages .venv
echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH"
.venv/bin/python -m pip install --upgrade pip
- 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 - <<'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.
working-directory: gramps
run: |
for po in po/*.po; do
lang=$(basename "$po" .po)
dest="build/mo/$lang/LC_MESSAGES"
mkdir -p "$dest"
msgfmt "$po" -o "$dest/gramps.mo"
done
- name: Run addon unit tests
# 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 }}
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"
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 "$GITHUB_WORKSPACE/addons-source"
python -m xmlrunner "${modules[@]}" \
-o "$out_dir" \
-v
) || fail=1
done
exit $fail
- 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: Addon Unit Tests
path: test-results/**/*.xml
reporter: java-junit