Skip to content

test: shared test harness + plugin-registration tests#980

Merged
GaryGriffin merged 4 commits into
gramps-project:maintenance/gramps60from
eduralph:test/shared-test-harness
Jul 9, 2026
Merged

test: shared test harness + plugin-registration tests#980
GaryGriffin merged 4 commits into
gramps-project:maintenance/gramps60from
eduralph:test/shared-test-harness

Conversation

@eduralph

@eduralph eduralph commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Carved out of #820 so the addon-facing tests can land before the CI pipeline itself. These four items only need Gramps — they have zero dependency on the .github/scripts/ CI code — so they are reviewable and mergeable on their own. Splitting them out also shrinks #820 toward the "one concern per PR" shape the maintainers asked for.

What's in this PR

  • tests/gramps_test_env.pyGrampsTestCase / GrampsDbTestCase base classes (stdlib unittest, mirroring upstream Gramps' own test style). Boots the plugin manager once per process and hands out fresh in-memory DBs.
  • tests/test_plugin_registration.py — registers every addon through the real Gramps PluginRegister / BasePluginManager, verifies gramps_target_version, valid id/name/version, and smoke-tests import/export entry functions.
  • tests/test_plugin_load_gate.py — regression test that drives the plugin-load gating path (a hard, non-dependency load failure must fail the test rather than pass silently).
  • make.py — a new test command that runs the repo-root tests/ suite via unittest discovery (the exact discovery the CI job uses), gated on GRAMPSPATH like the other commands.

tests/__init__.py (the GI version pin) is already on maintenance/gramps60, so it isn't part of this diff.

Running the tests

GRAMPSPATH=/path/to/gramps python3 make.py gramps60 test

GRAMPSPATH must point at a Gramps 6.0 checkout so the addon target versions match. test_plugin_registration boots the full plugin registry and needs the GTK typelibs present; test_plugin_load_gate is import-light and runs anywhere.

Relationship to #820

The three remaining root tests in #820test_addon_system_deps.py, test_requires_mod_dedup.py, test_run_addon_tests_paths.py — stay there because they import modules out of .github/scripts/, which doesn't exist until the CI PR merges. Once this PR lands, #820 rebases and sheds these files from its diff, leaving it strictly CI infrastructure.

🤖 Generated with Claude Code

@GaryGriffin

Copy link
Copy Markdown
Member

Tried make.py gramps60 test on my Mac. Failed but not sure what is the issue to fix. I think it is lxml gramplet, but not sure. That gramplet does not have a requires in the gpr, so why time out? Was an env variable undefined?

All listed addons must target the Gramps series they're running against. ... ok

======================================================================
ERROR: test_load_all_addon_modules (tests.test_plugin_registration.TestPluginLoading.test_load_all_addon_modules)
Load every addon plugin; gate on non-dependency load failures.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Volumes/Storage/Development/addons-source/tests/test_plugin_registration.py", line 187, in test_load_all_addon_modules
    result = subprocess.run(
        [
    ...<17 lines>...
        env={**os.environ, "PYTHONPATH": ADDONS_ROOT},
    )
  File "/Users/GrampsTest/gtk/inst/lib/python3.13/subprocess.py", line 558, in run
    stdout, stderr = process.communicate(input, timeout=timeout)
                     ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/GrampsTest/gtk/inst/lib/python3.13/subprocess.py", line 1221, in communicate
    stdout, stderr = self._communicate(input, endtime, timeout)
                     ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/GrampsTest/gtk/inst/lib/python3.13/subprocess.py", line 2131, in _communicate
    self._check_timeout(endtime, orig_timeout, stdout, stderr)
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/GrampsTest/gtk/inst/lib/python3.13/subprocess.py", line 1265, in _check_timeout
    raise TimeoutExpired(
    ...<2 lines>...
            stderr=b''.join(stderr_seq) if stderr_seq else None)
subprocess.TimeoutExpired: Command '['/Users/GrampsTest/gtk/inst/bin/python3', '-c', "import sys; sys.path.insert(0, '/Volumes/Storage/Development/addons-source');from gramps.gen.plug import BasePluginManager;from gramps.gen.const import PLUGINS_DIR;pmgr = BasePluginManager.get_instance();pmgr.reg_plugins(PLUGINS_DIR, None, None);pmgr.reg_plugins('/Volumes/Storage/Development/addons-source', None, None);from gramps.gen.plug import PluginRegister;preg = PluginRegister.get_instance();pdata = preg.get_plugin('lxml Gramplet');mod = pmgr.load_plugin(pdata);sys.exit(0 if mod else 1)"]' timed out after 30 seconds

----------------------------------------------------------------------
Ran 16 tests in 129.029s

FAILED (errors=1)

@GaryGriffin

Copy link
Copy Markdown
Member

Reran and the output was much more understandable. Great progress - this was sorta what I was looking for when we started - a way to test the addons from my local machine.

Something weird on the Gexiv2. I see the following but only one generated error. The former passes, the latter fails. Is there a syntax issue if there is a comma string in the versions?

EditExifMetadata/editexifmetadata.gpr.py:    requires_gi=[("GExiv2", "0.10")],
PhotoTaggingGramplet/PhotoTaggingGramplet.gpr.py:    requires_gi=[("GExiv2", "0.10,0.12,0.14,0.16")],

My current error log is below. Do you get the same as I get running locally? I want to set my expectations for Mac usability. Did this actually load the requires_mod/requires_gi packages?


Skipped for unmet dependencies (2):
  graphview (missing: gi:GooCanvas-2.0,3.0)
  Photo Tagging (missing: gi:GExiv2-0.10,0.12,0.14,0.16)
Failed for environmental reasons — signal crash or GTK/display init, likely need a display server (1):
  Collections Clipboard Gramplet (gi.repository.GLib.GError: gtk-icon-theme-error-quark: Icon 'stock_link' not present in theme Adwaita (0))
FAIL
test_addons_discovered (tests.test_plugin_registration.TestPluginRegistration.test_addons_discovered)
At least some plugins should be registered. ... ok
test_all_plugins_have_valid_metadata (tests.test_plugin_registration.TestPluginRegistration.test_all_plugins_have_valid_metadata)
Every registered plugin must have id, name, and version. ... ok
test_target_version_matches_gramps_install (tests.test_plugin_registration.TestPluginRegistration.test_target_version_matches_gramps_install)
All listed addons must target the Gramps series they're running against. ... ok

======================================================================
FAIL: test_load_all_addon_modules (tests.test_plugin_registration.TestPluginLoading.test_load_all_addon_modules)
Load every addon plugin; gate on non-dependency load failures.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Volumes/Storage/Development/addons-source/tests/test_plugin_registration.py", line 347, in test_load_all_addon_modules
    self.fail("\n".join(lines))
    ~~~~~~~~~^^^^^^^^^^^^^^^^^^
AssertionError: 1 of 174 addon(s) failed to load. These are real load failures and must be fixed:

  ✗ lxml Gramplet (hung during import, >120s)

Not failing this test (advisory only; full lists in the log above):
  1 could not initialise GTK/a display — run under Xvfb to load these; not an addon bug
  2 skipped for an unmet optional dependency (requires_mod/gi/exe)

----------------------------------------------------------------------
Ran 24 tests in 224.865s

FAILED (failures=1)

eduralph and others added 2 commits July 9, 2026 03:49
…py runner

Adds a repo-root test suite for addons, runnable via `make.py <series> test`:

- tests/gramps_test_env.py — GrampsTestCase / GrampsDbTestCase base classes
  that boot the real Gramps plugin manager and registry once per process.
- tests/test_plugin_registration.py — registers every addon through Gramps'
  PluginRegister, validates id/name/version and the target Gramps series, loads
  each addon in a crash-isolated subprocess, and smoke-tests import/export
  entry points.
- tests/test_plugin_load_gate.py — regression test that the load check gates
  (self.fail) on a genuine load failure rather than only logging it.
- tests/test_addon_dependency_loading.py — exercises Gramps' depends_on
  mechanism over the tree: every declared dependency resolves to a registered
  plugin, the dependency graph is acyclic, and each declared dependency is
  load-bearing — proven by an isolated subprocess import so sys.modules caching
  can't mask a missing/undeclared dependency. Verified against Gramps 6.0 and 6.1.
- make.py — new `test` command that runs the suite via unittest discovery,
  gated on GRAMPSPATH like the other commands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-ups from repeated adversarial review of the suite:

- Load test: display/GTK/environment failures are classified consistently
  (anchored signatures, unit-tested) and advisory by default; a genuine hard
  failure — or an import hang, detected via a REGISTRY_READY marker rather than
  a slow registry scan — always gates. One slow addon load can no longer abort
  the whole test.
- Strict mode (GRAMPS_ADDON_TEST_STRICT=1) turns the smoke run into a full
  gate: dependency skips, environment failures and unverified addon
  dependencies become hard failures; only slow registry-scan timeouts stay
  advisory. Covered by synthetic regression tests that don't boot the registry.
- Import/export smoke tests fail (not silently skip) when a dependency-satisfied
  plugin fails to load.
- Unlisted-addon manifest: source-based and per-registration (parses each
  .gpr.py with ast), so an accidental include_in_listing=False — even on one
  plugin inside a mixed directory like TMGimporter — fails the test.
- Neutralise blocking modal dialogs an addon may pop at import (e.g. lxml
  without python3-lxml) so a load can't hang or scatter UI.
- Silence expected locale / PyGI noise so results are readable.
- Failure messages lead with the real failures; make.py's test docstring and
  the load-test docstring document default (smoke) vs strict (full gate) modes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@eduralph
eduralph force-pushed the test/shared-test-harness branch from 01f184a to 6b83be2 Compare July 9, 2026 01:50
@eduralph
eduralph marked this pull request as ready for review July 9, 2026 01:53
@eduralph

eduralph commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@GaryGriffin - it was abit more work than I originally thought but it should be clean now. There were some tricky items to differentiate between loading hangs and other things, but it should all be good. Let me know what you think about it.

@eduralph

eduralph commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@dsblank - I took your idea and ran with it. The PR 820 already had some tests in place, I just needed to wire up your addition and change it a little bit.

@GaryGriffin

Copy link
Copy Markdown
Member

Confused. I ran make.py gramps60 tests (without merging PR981). I expected to still choke on lxml issue but it did not. Is this now hidden?

Also, still get warning on Photo Tagging but not EditExIfMetadata, both checking Gexiv2 (but different strings). Seems strange there are Warnings for exactly the ones that have a comma-separated version in the requires_gi but not for the ones without.

eduralph and others added 2 commits July 9, 2026 20:40
…heck

Gramps accepts a comma-separated list of acceptable versions in a plugin's
requires_gi: Requirements.check_gi() in gramps/gen/utils/requirements.py splits
on "," and takes the first version that resolves. _check_dependencies() passed
the raw string straight to gi.require_version(), which raises ValueError — so
the two addons in the tree that use the form, GraphView (GooCanvas "2.0,3.0")
and PhotoTaggingGramplet (GExiv2 "0.10,0.12,0.14,0.16"), were reported as
having an unmet dependency on hosts that in fact satisfy it. Advisory in the
default smoke run; a false hard failure under GRAMPS_ADDON_TEST_STRICT=1.
Neither .gpr.py is wrong. Reported by Gary Griffin on PR 980.

Delegate to Gramps' own Requirements rather than re-implement the checks, so
the test agrees with what Gramps does at runtime. That also drops two smaller
deviations: requires_mod was import_module()'d where Gramps uses find_spec (no
import side effects), and requires_exe hand-rolled a PATH scan where Gramps'
search_for also tries name + ".exe" for Windows. The gi.repository import probe
is kept, now run after check_gi() has pinned the matching version, so a typelib
that resolves but fails to import stays a dependency skip instead of a hard
failure blamed on the addon.

TestDependencyCheck covers the comma form (red before this change: reports a
spurious gi:Gtk-3.0,4.0), the single-version form, an absent namespace, and an
absent module/executable. It boots no registry and needs no display.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
neutralize_gui_dialogs() replaced gramps.gui.dialog's dialogs with silent
no-ops, so an addon that constructs one at import time — e.g. lxmlGramplet on a
host without python3-lxml — loaded clean and the suite said nothing. That kept
the load from hanging on a modal (Gramps' ErrorDialog.__init__ calls .run()),
but it also hid the addon bug the stub exists to survive: before the stub the
same addon was reported as "hung during import", after it, as nothing at all.
Raised by Gary Griffin on PR 980 ("Is this now hidden?"). It was.

Each stub now records its construction, on gramps_test_env.dialogs_raised and as
a DIALOG_MARKER line on stdout. The isolated load already captures the
subprocess' stdout, so test_load_all_addon_modules scans it and reports a dialog
at import as a hard failure in both modes, naming the addon and the dialog
class. The marker is matched only at the start of a line, so a traceback quoting
it cannot forge a hit.

The load-failure header now reads "failed to load cleanly ... real defects"
rather than "failed to load ... real load failures": an addon caught this way
did load, and the message must not claim otherwise.

Verified against the real addon: with lxml unimportable, lxml/lxmlGramplet.py
raises ErrorDialog at import (dialogs_raised == ['ErrorDialog'], hard failure);
the same probe against PR 981's lxmlGramplet raises none. The gate itself is
covered synthetically by test_dialog_raised_at_import_gates, which is red
without the stdout scan even though the load exits 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@eduralph

eduralph commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Confused. I ran make.py gramps60 tests (without merging PR981). I expected to still choke on lxml issue but it did not. Is this now hidden?

Also, still get warning on Photo Tagging but not EditExIfMetadata, both checking Gexiv2 (but different strings). Seems strange there are Warnings for exactly the ones that have a comma-separated version in the requires_gi but not for the ones without.

Good points ... I buried the GUI issues (including lxml) and that is probably not the right approach to take. It protects the test from choking but also starts hiding issues. It now records the dialog, and the load test reports it as a hard failure in both modes, naming the addon and the dialog class. So lxml goes back to red — with an accurate message this time, "raised ErrorDialog at import" rather than "hung" — stays red until #981 merges, and any future addon that does the same gets caught rather than silently neutralised.

GExiv2 — that's my bug, Gramps supports a comma-separated version list in requires_gi. It splits, and accepts if any listed version resolves. My _check_dependencies() in tests/test_plugin_registration.py didn't.

To your earlier question — "did this actually load the requires_mod/requires_gi packages?" — no. The dependency check only probes (find_spec / gi.require_version / PATH lookup) to decide whether it's fair to attempt the load; the actual import happens in the isolated subprocess afterwards. So a missing typelib is a skip, never a failure, outside strict mode.

@GaryGriffin
GaryGriffin merged commit 8bb8984 into gramps-project:maintenance/gramps60 Jul 9, 2026
@GaryGriffin

Copy link
Copy Markdown
Member

Not cherry picking to gramps61 unless you explicitly tell me to check it.

@GaryGriffin

Copy link
Copy Markdown
Member

One suggestion - running this creates a file debug.log . If at the end of the run, it is zero length, suggest you delete it. Only leave it if it has messages.

@GaryGriffin

Copy link
Copy Markdown
Member

FYI - I ran this PR (make.py gramps61 test) against gramps61 build. The following two errors occurred.

  • GrampsAssistant has a dependency on the addon Grampy Script. Both exist in the 61 branch. What should change to make this pass? Adding @dsblank to confirm since these are both new addons. And both work correctly.
  • Gexiv2 - It is made as a dylib as part of the Mac bundle for 6.1 . lib/libgexiv2-<version>.dylib . Adding @jralls in case I am mis-speaking. This is different and why it is no longer a requires_gi in the GPR.

======================================================================
FAIL: test_declared_dependencies_resolve_to_registered_plugins (tests.test_addon_dependency_loading.TestAddonDependencyLoading.test_declared_dependencies_resolve_to_registered_plugins)
Every ``depends_on`` id must name a plugin Gramps actually registered.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Volumes/Storage/Development/addons-source/tests/test_addon_dependency_loading.py", line 201, in test_declared_dependencies_resolve_to_registered_plugins
    self.fail(
    ~~~~~~~~~^
        "Addons declaring depends_on ids that are not registered "
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        "(the Gramps loader cannot satisfy these):\n  "
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        + "\n  ".join(sorted(unresolved))
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
AssertionError: Addons declaring depends_on ids that are not registered (the Gramps loader cannot satisfy these):
  grampsassistant -> Grampy Script

======================================================================
FAIL: test_load_all_addon_modules (tests.test_plugin_registration.TestPluginLoading.test_load_all_addon_modules)
Load every addon plugin; gate on non-dependency load failures.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Volumes/Storage/Development/addons-source/tests/test_plugin_registration.py", line 435, in test_load_all_addon_modules
    self.fail("\n".join(lines))
    ~~~~~~~~~^^^^^^^^^^^^^^^^^^
AssertionError: 2 of 173 addon(s) failed to load cleanly. These are real defects and must be fixed:

  ✗ Edit Image Exif Metadata (Plugin error (from 'editexifmetadata'): GExiv2 is not installed)
  ✗ Photo Tagging (Plugin error (from 'PhotoTaggingGramplet'): GExiv2 is not installed)

Not failing this test (advisory only; full lists in the log above):
  1 could not initialise GTK/a display — run under Xvfb to load these; not an addon bug

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants