Addon Unit Tests #263
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |