Skip to content

Commit 57e38ba

Browse files
eduralphclaude
authored andcommitted
ImportGramplet: pin death-fallback contract for clean-XML import (bug 13420)
Mantis 13420 reports that a person imported via the Text Import gramplet does NOT show their Death event as the death fallback in list views until the event ref is re-validated with OK in the editor. snoiraud established on the tracker (note 8) that the reporter's XML was generated by a no-code Android tool with UUID handles and longer-than-normal change-times; no developer ever reproduced the symptom on conformant Gramps XML. The fallback-setting logic lives in gramps CORE at `gramps/plugins/importer/importxml.py:1434-1473` (`start_eventref`) and is inherited by the addon's `AtomicGrampsParser` unchanged -- AtomicGrampsParser only overrides `parse()` to wrap the import in a non-batch DbTxn for atomic undo; it does not touch the per-element handlers. The relevant precondition is spelled out by the core parser itself, line 1451-1452: "We count here on events being already parsed prior to parsing people or families. This code will fail if this is not true." This test exercises the decisive scenario from the verdict in `triage/batches/batch-03-confirmed-velocity/issue_13420.md`: build CONFORMANT minimal Gramps XML (events listed before people, standard Gramps handles, both events role Primary), import via AtomicGrampsParser, and assert person.get_death_ref() resolves to the Death event WITHOUT manual editor re-save. Birth ref asserted in the same case as a control for the shared start_eventref path. Empirical result on `upstream/maintenance/gramps60`: 1 test, OK That is: the addon's atomic parser sets death_ref correctly on clean XML. The bug as filed does NOT reproduce on conformant input on the current gramps60 tree -- the cause was the reporter's malformed UUID-handle XML, not the gramplet. The Mantis ticket should close as cannot-reproduce / invalid input; the test stays here as a regression guard so a later refactor of the atomic parser cannot silently break the fallback wiring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c4aca96 commit 57e38ba

2 files changed

Lines changed: 187 additions & 0 deletions

File tree

ImportGramplet/tests/__init__.py

Whitespace-only changes.
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#
2+
# Gramps - a GTK+/GNOME based genealogy program
3+
#
4+
# Copyright (C) 2026 Gramps Development Team
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+
Repro-or-close test for bug 13420: Text Import gramplet death event
23+
is not shown as the person's death fallback until re-validated in the
24+
editor.
25+
26+
The reporter's input was XML produced by a no-code Android tool with
27+
UUID handles and longer-than-normal change-times; no developer
28+
reproduced the symptom on conformant Gramps XML. Per the triage
29+
verdict in `triage/batches/batch-03-confirmed-velocity/issue_13420.md`
30+
the decisive test is:
31+
32+
- Build CONFORMANT minimal Gramps XML (one person, Birth + Death
33+
events both role Primary, standard Gramps handles)
34+
- Import via the addon's `AtomicGrampsParser` (the wrapper the
35+
ImportGramplet uses for its XML branch)
36+
- Assert person.get_death_ref() returns the Death event ref
37+
WITHOUT manual editor re-save
38+
39+
If clean XML imports correctly → reporter's XML was malformed; close
40+
13420 as cannot-reproduce. If clean XML still fails → real addon bug,
41+
fix needed.
42+
43+
The fallback-setting logic lives in gramps CORE at
44+
`gramps/plugins/importer/importxml.py:1434-1473` (`start_eventref`)
45+
and is shared between the standard XML import and AtomicGrampsParser
46+
(AtomicGrampsParser only overrides `parse()` for an atomic DbTxn
47+
wrapper, not the per-element handlers). The note above
48+
`start_eventref` itself spells out the precondition: "We count here
49+
on events being already parsed prior to parsing people or families.
50+
This code will fail if this is not true." So the test fixture lists
51+
events BEFORE people.
52+
"""
53+
54+
import os
55+
import shutil
56+
import sys
57+
import tempfile
58+
import unittest
59+
60+
# AtomicGrampsParser pulls `from gi.repository import Gtk` at module
61+
# load (via ImportGramplet.py). Pin Gtk to 3.0 before importing; skip
62+
# cleanly if PyGObject / GTK 3 aren't available.
63+
try:
64+
import gi
65+
66+
gi.require_version("Gtk", "3.0")
67+
gi.require_version("Gdk", "3.0")
68+
except (ImportError, ValueError) as err:
69+
raise unittest.SkipTest("GTK 3.0 / PyGObject not available: %s" % err)
70+
71+
# Make sure addon modules are importable from the parent directory.
72+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
73+
74+
75+
# Minimal CONFORMANT Gramps XML. Events listed before people, both
76+
# events role Primary, standard Gramps handles (no UUIDs). Mirrors
77+
# the structure in `example/gramps/example.gramps`.
78+
CLEAN_XML = """<?xml version="1.0" encoding="UTF-8"?>
79+
<!DOCTYPE database PUBLIC "-//Gramps//DTD Gramps XML 1.7.2//EN"
80+
"http://gramps-project.org/xml/1.7.2/grampsxml.dtd">
81+
<database xmlns="http://gramps-project.org/xml/1.7.2/">
82+
<header>
83+
<created date="2026-05-25" version="6.0.0"/>
84+
<researcher>
85+
<resname>Bug 13420 Test</resname>
86+
</researcher>
87+
</header>
88+
<events>
89+
<event handle="_a5af0eb667015e355db" change="1284030602" id="E00001">
90+
<type>Birth</type>
91+
<dateval val="1900-01-01"/>
92+
<description>Birth of Test, Person</description>
93+
</event>
94+
<event handle="_a5af0eb696917232725" change="1284030602" id="E00002">
95+
<type>Death</type>
96+
<dateval val="1970-12-31"/>
97+
<description>Death of Test, Person</description>
98+
</event>
99+
</events>
100+
<people>
101+
<person handle="_aaa0bbb111ccc222ddd" change="1284030602" id="I00001">
102+
<gender>M</gender>
103+
<name type="Birth Name">
104+
<first>Person</first>
105+
<surname>Test</surname>
106+
</name>
107+
<eventref hlink="_a5af0eb667015e355db" role="Primary"/>
108+
<eventref hlink="_a5af0eb696917232725" role="Primary"/>
109+
</person>
110+
</people>
111+
</database>
112+
"""
113+
114+
115+
class TestImportGrampletDeathFallback(unittest.TestCase):
116+
"""Repro-or-close for bug 13420."""
117+
118+
def setUp(self):
119+
# Per Sqlite/tests/test_sqlite.py: create a sqlite in-memory-
120+
# equivalent db in a tempdir, import the XML, exercise.
121+
from gramps.gen.db.utils import make_database # pylint: disable=import-outside-toplevel
122+
123+
self.db_dir = tempfile.mkdtemp(prefix="bug13420_")
124+
self.database = make_database("sqlite")
125+
self.database.load(self.db_dir)
126+
127+
def tearDown(self):
128+
try:
129+
self.database.close()
130+
except Exception: # pylint: disable=broad-except
131+
pass
132+
shutil.rmtree(self.db_dir, ignore_errors=True)
133+
134+
def test_clean_xml_imports_set_death_ref_via_atomic_parser(self):
135+
"""Clean XML through AtomicGrampsParser must set the
136+
person's death_ref to the Death event.
137+
138+
This is the repro-or-close decision point for bug 13420.
139+
If it passes, the reporter's malformed XML (UUID handles)
140+
was the cause; if it fails, the addon's atomic parser is
141+
skipping the fallback wiring.
142+
"""
143+
from io import BytesIO # pylint: disable=import-outside-toplevel
144+
import time # pylint: disable=import-outside-toplevel
145+
from gramps.cli.user import User # pylint: disable=import-outside-toplevel
146+
147+
# Importing the addon transitively imports Gtk -- this is why
148+
# the test is gated above on gi.require_version("Gtk", "3.0").
149+
from ImportGramplet.ImportGramplet import AtomicGrampsParser # pylint: disable=import-outside-toplevel
150+
151+
user = User()
152+
change = int(time.time())
153+
parser = AtomicGrampsParser(self.database, user, change)
154+
ifile = BytesIO(CLEAN_XML.encode("utf-8"))
155+
parser.parse(ifile)
156+
157+
person = self.database.get_person_from_gramps_id("I00001")
158+
self.assertIsNotNone(
159+
person,
160+
"Person I00001 must exist after XML import",
161+
)
162+
163+
death_ref = person.get_death_ref()
164+
self.assertIsNotNone(
165+
death_ref,
166+
"Person.get_death_ref() must return the Death event ref "
167+
"after clean-XML import via AtomicGrampsParser. If this "
168+
"assertion fails, bug 13420 reproduces on conformant XML "
169+
"and is a real addon defect.",
170+
)
171+
172+
death_event = self.database.get_event_from_handle(death_ref.ref)
173+
self.assertIsNotNone(death_event)
174+
self.assertEqual(str(death_event.get_type()), "Death")
175+
176+
# Birth ref must also be wired up -- same logic in core
177+
# start_eventref. If birth is fine but death is not, the bug
178+
# is type-specific; if both fail, it's the whole fallback
179+
# path.
180+
birth_ref = person.get_birth_ref()
181+
self.assertIsNotNone(birth_ref)
182+
birth_event = self.database.get_event_from_handle(birth_ref.ref)
183+
self.assertEqual(str(birth_event.get_type()), "Birth")
184+
185+
186+
if __name__ == "__main__":
187+
unittest.main()

0 commit comments

Comments
 (0)