Skip to content

Commit 6483ea6

Browse files
dsblankGaryGriffin
authored andcommitted
Add regression tests for IsFamilyFilterMatchEvent
Covers the AttributeError fixed in the previous commit: prepare() crashed because it updated the never-defined self.events instead of self.selected_handles. Tests build a small in-memory database with two families/events, register a custom Family filter matching one of them, and assert prepare()/apply_to_one()/GenericFilter.apply() all behave correctly without raising.
1 parent 2215697 commit 6483ea6

2 files changed

Lines changed: 178 additions & 0 deletions

File tree

FilterRules/tests/__init__.py

Whitespace-only changes.
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#
2+
# Gramps - a GTK+/GNOME based genealogy program
3+
#
4+
# Copyright (C) 2026 Doug Blank
5+
#
6+
# This program is free software; you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation; either version 2 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program; if not, write to the Free Software
18+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19+
#
20+
21+
"""
22+
Regression tests for the ``IsFamilyFilterMatchEvent`` filter rule.
23+
24+
``prepare()`` used to update ``self.events``, an attribute never
25+
defined anywhere on the class, instead of ``self.selected_handles``
26+
(the attribute it initializes and that ``apply_to_one()`` actually
27+
checks). That raised ``AttributeError: 'IsFamilyFilterMatchEvent'
28+
object has no attribute 'events'`` whenever the "Events of families
29+
matching a <family filter>" rule was applied, crashing the filter
30+
entirely. See:
31+
https://gramps.discourse.group/t/crash-of-an-event-filter-using-a-functional-family-filter/9733
32+
"""
33+
34+
# -------------------------------------------------------------------------
35+
#
36+
# Standard Python modules
37+
#
38+
# -------------------------------------------------------------------------
39+
import os
40+
import shutil
41+
import sys
42+
import tempfile
43+
import unittest
44+
45+
# The addon imports Gtk at module load (via
46+
# gramps.gui.editors.filtereditor). Pin Gtk to 3.0 before any gramps
47+
# import (mirrors what gramps.grampsapp does at startup); otherwise
48+
# PyGObject loads GTK4 and the gramps.gui import chain crashes on
49+
# Gtk.IconSize.MENU (a GTK3-only enum). Skip cleanly if GTK 3 / PyGObject
50+
# aren't available.
51+
try:
52+
import gi
53+
54+
gi.require_version("Gtk", "3.0")
55+
gi.require_version("Gdk", "3.0")
56+
except (ImportError, ValueError, AttributeError) as err:
57+
raise unittest.SkipTest("GTK 3.0 / PyGObject not available: %s" % err)
58+
59+
# Addon root goes on sys.path so ``FilterRules.isfamilyfiltermatchevent``
60+
# resolves. The ``FilterRules`` directory lacks an __init__.py, so this
61+
# relies on Python 3 namespace packages.
62+
ADDON_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
63+
if ADDON_DIR not in sys.path:
64+
sys.path.insert(0, ADDON_DIR)
65+
66+
try:
67+
import gramps
68+
except ImportError as err:
69+
raise unittest.SkipTest("gramps package not available: %s" % err)
70+
71+
if "GRAMPS_RESOURCES" not in os.environ:
72+
os.environ["GRAMPS_RESOURCES"] = os.path.dirname(os.path.dirname(gramps.__file__))
73+
74+
# -------------------------------------------------------------------------
75+
#
76+
# Gramps modules
77+
#
78+
# -------------------------------------------------------------------------
79+
from gramps.gen.db import DbTxn
80+
from gramps.gen.db.utils import make_database
81+
from gramps.gen.lib import Event, EventRef, EventType, Family, FamilyRelType
82+
83+
# CustomFilters starts out as None; it must be initialized before any
84+
# module imports the name by value, since reload_custom_filters()
85+
# rebinds the module-level global rather than mutating it in place.
86+
from gramps.gen.filters import reload_custom_filters
87+
88+
reload_custom_filters()
89+
from gramps.gen.filters import CustomFilters, GenericFilterFactory
90+
from gramps.gen.filters.rules.family import HasIdOf as FamilyHasIdOf
91+
from gramps.cli.user import User
92+
93+
from FilterRules.isfamilyfiltermatchevent import IsFamilyFilterMatchEvent
94+
95+
FAMILY_FILTER_NAME = "_test_isfamilyfiltermatchevent_family_filter"
96+
97+
98+
class IsFamilyFilterMatchEventTest(unittest.TestCase):
99+
"""Regression tests for IsFamilyFilterMatchEvent.prepare()."""
100+
101+
def setUp(self):
102+
"""Build a database with two families, each with one event, and
103+
register a custom Family filter matching only the first."""
104+
self.db_dir = tempfile.mkdtemp(prefix="isfamilyfiltermatchevent_")
105+
self.db = make_database("sqlite")
106+
self.db.load(self.db_dir)
107+
108+
with DbTxn("build test db", self.db) as txn:
109+
matched_event = Event()
110+
matched_event.set_type(EventType(EventType.MARRIAGE))
111+
self.db.add_event(matched_event, txn)
112+
self.matched_event_handle = matched_event.handle
113+
114+
unmatched_event = Event()
115+
unmatched_event.set_type(EventType(EventType.MARRIAGE))
116+
self.db.add_event(unmatched_event, txn)
117+
self.unmatched_event_handle = unmatched_event.handle
118+
119+
matched_family = Family()
120+
matched_family.set_relationship(FamilyRelType(FamilyRelType.MARRIED))
121+
matched_ref = EventRef()
122+
matched_ref.set_reference_handle(self.matched_event_handle)
123+
matched_family.add_event_ref(matched_ref)
124+
self.db.add_family(matched_family, txn)
125+
self.matched_family_gramps_id = matched_family.gramps_id
126+
127+
unmatched_family = Family()
128+
unmatched_family.set_relationship(FamilyRelType(FamilyRelType.MARRIED))
129+
unmatched_ref = EventRef()
130+
unmatched_ref.set_reference_handle(self.unmatched_event_handle)
131+
unmatched_family.add_event_ref(unmatched_ref)
132+
self.db.add_family(unmatched_family, txn)
133+
134+
family_filter = GenericFilterFactory("Family")()
135+
family_filter.set_name(FAMILY_FILTER_NAME)
136+
family_filter.add_rule(FamilyHasIdOf([self.matched_family_gramps_id]))
137+
CustomFilters.get_filters_dict("Family")[FAMILY_FILTER_NAME] = family_filter
138+
139+
def tearDown(self):
140+
del CustomFilters.get_filters_dict("Family")[FAMILY_FILTER_NAME]
141+
self.db.close()
142+
shutil.rmtree(self.db_dir, ignore_errors=True)
143+
144+
def test_prepare_does_not_raise_attributeerror(self):
145+
"""prepare() must populate selected_handles, not crash on the
146+
undefined self.events."""
147+
rule = IsFamilyFilterMatchEvent([FAMILY_FILTER_NAME])
148+
rule.requestprepare(self.db, User())
149+
self.assertEqual(rule.selected_handles, {self.matched_event_handle})
150+
151+
def test_apply_to_one_matches_only_expected_event(self):
152+
"""apply_to_one() must accept the matched family's event and
153+
reject the unmatched family's event."""
154+
rule = IsFamilyFilterMatchEvent([FAMILY_FILTER_NAME])
155+
rule.requestprepare(self.db, User())
156+
matched_event = self.db.get_event_from_handle(self.matched_event_handle)
157+
unmatched_event = self.db.get_event_from_handle(self.unmatched_event_handle)
158+
self.assertTrue(rule.apply_to_one(self.db, matched_event))
159+
self.assertFalse(rule.apply_to_one(self.db, unmatched_event))
160+
161+
def test_full_filter_apply(self):
162+
"""Running the rule through a GenericFilter must return exactly
163+
the matched family's event."""
164+
event_filter = GenericFilterFactory("Event")()
165+
event_filter.add_rule(IsFamilyFilterMatchEvent([FAMILY_FILTER_NAME]))
166+
results = set(event_filter.apply(self.db))
167+
self.assertEqual(results, {self.matched_event_handle})
168+
169+
def test_missing_family_filter(self):
170+
"""A rule referencing a nonexistent family filter must not
171+
crash and must match nothing."""
172+
rule = IsFamilyFilterMatchEvent(["_no_such_filter_"])
173+
rule.requestprepare(self.db, User())
174+
self.assertEqual(rule.selected_handles, set())
175+
176+
177+
if __name__ == "__main__":
178+
unittest.main()

0 commit comments

Comments
 (0)