Merge pull request #237 from prculley/check
Update Check & Repair tool & Test Case Generator tool to fix bad alternative Place names
This commit is contained in:
commit
819929f073
168
gramps/plugins/test/test_tools.py
Normal file
168
gramps/plugins/test/test_tools.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
#! /usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Gramps - a GTK+/GNOME based genealogy program
|
||||||
|
|
||||||
|
Copyright (c) 2016 Gramps Development Team
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import codecs
|
||||||
|
import unittest
|
||||||
|
import random
|
||||||
|
|
||||||
|
from gramps.test.test_util import Gramps
|
||||||
|
from gramps.gen.const import DATA_DIR
|
||||||
|
from gramps.cli.user import User
|
||||||
|
from gramps.gen.utils.id import set_det_id
|
||||||
|
from gramps.gen import const
|
||||||
|
from gramps.gen.utils.config import config
|
||||||
|
|
||||||
|
TREE_NAME = "Test_tooltest"
|
||||||
|
TEST_DIR = os.path.abspath(os.path.join(DATA_DIR, "tests"))
|
||||||
|
const.myrand = random.Random()
|
||||||
|
|
||||||
|
|
||||||
|
def call(*args):
|
||||||
|
""" Call Gramps to perform the action with out and err captured """
|
||||||
|
if __debug__:
|
||||||
|
print ("call: %s", args)
|
||||||
|
gramps = Gramps(user=User(auto_accept=True, quiet=True))
|
||||||
|
# gramps = Gramps(user=User(auto_accept=True))
|
||||||
|
out, err = gramps.run(*args)
|
||||||
|
# print("out:", out, "err:", err)
|
||||||
|
return out, err
|
||||||
|
|
||||||
|
|
||||||
|
def check_res(out, err, expected, do_out=True):
|
||||||
|
"""
|
||||||
|
compare the output with the expected
|
||||||
|
"""
|
||||||
|
# return all([(line in out) for line in expect])
|
||||||
|
retval = True
|
||||||
|
for line in expected:
|
||||||
|
if line in (out if do_out else err):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print("*** Expected: '%s', not found" % line)
|
||||||
|
retval = False
|
||||||
|
if not retval:
|
||||||
|
if do_out:
|
||||||
|
print("*** The following was the actual stdout:\n%s"
|
||||||
|
"*** The following was the stderr:\n%s"
|
||||||
|
"*** End of stderr" % (out, err))
|
||||||
|
else:
|
||||||
|
print("*** The following was the actual stderr:\n%s"
|
||||||
|
"*** The following was the stdout:\n%s"
|
||||||
|
"*** End of stdout" % (err, out))
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
class ToolControl(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
These tests run some of the tools used to maintain Gramps db
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
self.db_backend = config.get('database.backend')
|
||||||
|
call("--config=database.backend:bsddb", "-y", "-q", "--remove", TREE_NAME)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
config.set('database.backend', self.db_backend)
|
||||||
|
call("-y", "-q", "--remove", TREE_NAME)
|
||||||
|
|
||||||
|
def test_tcg_and_check_and_repair(self):
|
||||||
|
"""
|
||||||
|
Run a 'Test Case Generator' and 'Check & Repair Database' test.
|
||||||
|
Note that the 'Test Case Generator" uses a lot of random numbers to
|
||||||
|
generate its test cases. This makes it less than ideal for a
|
||||||
|
predictable unit test. Still it contains good code for testing the
|
||||||
|
'Check and Repair' tool. So I force the random functions to be
|
||||||
|
predictable by seeding it with a fixed number. I also used the
|
||||||
|
'Deterministic ID' function to make the usual db handle generation
|
||||||
|
stop using random numbers and potentially reduce Gramps version to
|
||||||
|
version issues.
|
||||||
|
"""
|
||||||
|
# the TCG creates bad strings with illegal characters, so we need to
|
||||||
|
# ignore them when we print the results
|
||||||
|
try:
|
||||||
|
sys.stderr = codecs.getwriter(sys.getdefaultencoding()) \
|
||||||
|
(sys.stderr.buffer, 'replace')
|
||||||
|
sys.stdout = codecs.getwriter(sys.getdefaultencoding()) \
|
||||||
|
(sys.stdout.buffer, 'replace')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
tst_file = os.path.join(TEST_DIR, "data.gramps")
|
||||||
|
set_det_id(True)
|
||||||
|
# the following line assumes that TCG has run through init code, where
|
||||||
|
# it puts 'myrand', a 'Random' class object, into the 'const' module so
|
||||||
|
# we can access it here.
|
||||||
|
const.myrand.seed(1234, version=1)
|
||||||
|
# print(const.myrand.random())
|
||||||
|
# out, err = call("-s")
|
||||||
|
# expect = ["bsddb"]
|
||||||
|
# check_res(out, err, expect, do_out=True)
|
||||||
|
out, err = call("-C", TREE_NAME, "-q",
|
||||||
|
"--import", tst_file)
|
||||||
|
expect = ["Opened successfully!",
|
||||||
|
"data.gramps, format gramps.",
|
||||||
|
"Cleaning up."]
|
||||||
|
self.assertTrue(check_res(out, err, expect, do_out=False))
|
||||||
|
self.assertEqual(out, "")
|
||||||
|
out, err = call("-O", TREE_NAME,
|
||||||
|
"-y", "-q", "-a", "tool", "-p",
|
||||||
|
"name=testcasegenerator,bugs=1,persons=0,"
|
||||||
|
"add_linebreak=0,add_serial=0,"
|
||||||
|
"long_names=0,lowlevel=0,person_count=20,"
|
||||||
|
"specialchars=0")
|
||||||
|
expect = ["Opened successfully!",
|
||||||
|
"Performing action: tool.",
|
||||||
|
"Using options string: name=testcasegenerator,bugs=1",
|
||||||
|
"Cleaning up."]
|
||||||
|
self.assertTrue(check_res(out, err, expect, do_out=False))
|
||||||
|
expect = ["person count 41"]
|
||||||
|
self.assertTrue(check_res(out, err, expect, do_out=True))
|
||||||
|
out, err = call("-O", TREE_NAME,
|
||||||
|
"-y", "-a", "tool", "-p", "name=check")
|
||||||
|
expect = ["7 broken child/family links were fixed",
|
||||||
|
"4 broken spouse/family links were fixed",
|
||||||
|
"1 place alternate names fixed",
|
||||||
|
"10 media objects were referenced, but not found",
|
||||||
|
"References to 10 media objects were kept",
|
||||||
|
"3 events were referenced, but not found",
|
||||||
|
"1 invalid birth event name was fixed",
|
||||||
|
"1 invalid death event name was fixed",
|
||||||
|
"2 places were referenced, but not found",
|
||||||
|
"13 citations were referenced, but not found",
|
||||||
|
"16 sources were referenced, but not found",
|
||||||
|
"7 empty objects removed",
|
||||||
|
"1 person objects",
|
||||||
|
"1 family objects",
|
||||||
|
"1 event objects",
|
||||||
|
"1 source objects",
|
||||||
|
"0 media objects",
|
||||||
|
"0 place objects",
|
||||||
|
"1 repository objects",
|
||||||
|
"1 note objects"]
|
||||||
|
self.assertTrue(check_res(out, err, expect, do_out=True))
|
||||||
|
expect = ["Opened successfully!",
|
||||||
|
"Performing action: tool.",
|
||||||
|
"Using options string: name=check",
|
||||||
|
"Cleaning up."]
|
||||||
|
self.assertTrue(check_res(out, err, expect, do_out=False))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
@ -171,6 +171,7 @@ class Check(tool.BatchTool):
|
|||||||
# then going to be deleted.
|
# then going to be deleted.
|
||||||
checker.cleanup_empty_objects()
|
checker.cleanup_empty_objects()
|
||||||
checker.fix_encoding()
|
checker.fix_encoding()
|
||||||
|
checker.fix_alt_place_names()
|
||||||
checker.fix_ctrlchars_in_notes()
|
checker.fix_ctrlchars_in_notes()
|
||||||
checker.cleanup_missing_photos(cli)
|
checker.cleanup_missing_photos(cli)
|
||||||
checker.cleanup_deleted_name_formats()
|
checker.cleanup_deleted_name_formats()
|
||||||
@ -246,6 +247,7 @@ class CheckIntegrity:
|
|||||||
self.removed_name_format = []
|
self.removed_name_format = []
|
||||||
self.empty_objects = defaultdict(list)
|
self.empty_objects = defaultdict(list)
|
||||||
self.replaced_sourceref = []
|
self.replaced_sourceref = []
|
||||||
|
self.place_errors = 0
|
||||||
self.last_img_dir = config.get('behavior.addmedia-image-dir')
|
self.last_img_dir = config.get('behavior.addmedia-image-dir')
|
||||||
self.progress = ProgressMeter(_('Checking Database'), '',
|
self.progress = ProgressMeter(_('Checking Database'), '',
|
||||||
parent=self.parent_window)
|
parent=self.parent_window)
|
||||||
@ -410,6 +412,38 @@ class CheckIntegrity:
|
|||||||
if error_count == 0:
|
if error_count == 0:
|
||||||
logging.info(' OK: no ctrl characters in notes found')
|
logging.info(' OK: no ctrl characters in notes found')
|
||||||
|
|
||||||
|
def fix_alt_place_names(self):
|
||||||
|
"""
|
||||||
|
This scans all places and cleans up alternative names. It removes
|
||||||
|
Blank names, names that are duplicates of the primary name, and
|
||||||
|
duplicates in the alt_names list.
|
||||||
|
"""
|
||||||
|
self.progress.set_pass(_('Looking for bad alternate place names'),
|
||||||
|
self.db.get_number_of_places())
|
||||||
|
logging.info('Looking for bad alternate place names')
|
||||||
|
for bhandle in self.db.place_map.keys():
|
||||||
|
handle = bhandle.decode('utf-8')
|
||||||
|
place = self.db.get_place_from_handle(handle)
|
||||||
|
fixed_alt_names = []
|
||||||
|
fixup = False
|
||||||
|
for name in place.get_alternative_names():
|
||||||
|
if not name.value or \
|
||||||
|
name == place.name or \
|
||||||
|
name in fixed_alt_names:
|
||||||
|
fixup = True
|
||||||
|
continue
|
||||||
|
fixed_alt_names.append(name)
|
||||||
|
if fixup:
|
||||||
|
place.set_alternative_names(fixed_alt_names)
|
||||||
|
self.db.commit_place(place, self.trans)
|
||||||
|
self.place_errors += 1
|
||||||
|
self.progress.step()
|
||||||
|
if self.place_errors == 0:
|
||||||
|
logging.info(' OK: no bad alternate places found')
|
||||||
|
else:
|
||||||
|
logging.info(' %d bad alternate places found and fixed',
|
||||||
|
self.place_errors)
|
||||||
|
|
||||||
def check_for_broken_family_links(self):
|
def check_for_broken_family_links(self):
|
||||||
# Check persons referenced by the family objects
|
# Check persons referenced by the family objects
|
||||||
|
|
||||||
@ -717,9 +751,8 @@ class CheckIntegrity:
|
|||||||
photo_desc = obj.get_description()
|
photo_desc = obj.get_description()
|
||||||
if photo_name is not None and photo_name != "" and not find_file(photo_name):
|
if photo_name is not None and photo_name != "" and not find_file(photo_name):
|
||||||
if cl:
|
if cl:
|
||||||
fn = os.path.basename(photo_name)
|
|
||||||
logging.warning(" FAIL: media file %s was not found." %
|
logging.warning(" FAIL: media file %s was not found." %
|
||||||
fn)
|
photo_name)
|
||||||
self.bad_photo.append(ObjectId)
|
self.bad_photo.append(ObjectId)
|
||||||
else:
|
else:
|
||||||
if missmedia_action == 0:
|
if missmedia_action == 0:
|
||||||
@ -2116,7 +2149,7 @@ class CheckIntegrity:
|
|||||||
empty_objs = sum(len(obj) for obj in self.empty_objects.values())
|
empty_objs = sum(len(obj) for obj in self.empty_objects.values())
|
||||||
|
|
||||||
errors = (photos + efam + blink + plink + slink + rel +
|
errors = (photos + efam + blink + plink + slink + rel +
|
||||||
event_invalid + person +
|
event_invalid + person + self.place_errors +
|
||||||
person_references + family_references + place_references +
|
person_references + family_references + place_references +
|
||||||
citation_references + repo_references + media_references +
|
citation_references + repo_references + media_references +
|
||||||
note_references + tag_references + name_format + empty_objs +
|
note_references + tag_references + name_format + empty_objs +
|
||||||
@ -2233,6 +2266,14 @@ class CheckIntegrity:
|
|||||||
rel).format(quantity=rel)
|
rel).format(quantity=rel)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.place_errors:
|
||||||
|
self.text.write(
|
||||||
|
# translators: leave all/any {...} untranslated
|
||||||
|
ngettext("{quantity} place alternate name fixed\n",
|
||||||
|
"{quantity} place alternate names fixed\n",
|
||||||
|
rel).format(quantity=self.place_errors)
|
||||||
|
)
|
||||||
|
|
||||||
if person_references:
|
if person_references:
|
||||||
self.text.write(
|
self.text.write(
|
||||||
# translators: leave all/any {...} untranslated
|
# translators: leave all/any {...} untranslated
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -260,6 +260,8 @@ class Gramps:
|
|||||||
args = [sys.executable] + list(args)
|
args = [sys.executable] + list(args)
|
||||||
argparser = ArgParser(args)
|
argparser = ArgParser(args)
|
||||||
argparser.need_gui() # initializes some variables
|
argparser.need_gui() # initializes some variables
|
||||||
|
if argparser.errors:
|
||||||
|
print(argparser.errors, file=sys.stderr)
|
||||||
argparser.print_help()
|
argparser.print_help()
|
||||||
argparser.print_usage()
|
argparser.print_usage()
|
||||||
handler = ArgHandler(self.dbstate, argparser, self.climanager)
|
handler = ArgHandler(self.dbstate, argparser, self.climanager)
|
||||||
|
Loading…
Reference in New Issue
Block a user