1# 2# Gramps - a GTK+/GNOME based genealogy program 3# 4# Copyright (C) 2000-2007 Donald N. Allingham 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# test/test_util.py 22 23"""unittest support utility module""" 24 25import os 26import sys 27import traceback 28import tempfile 29import shutil 30import logging 31import contextlib 32from io import TextIOWrapper, BytesIO, StringIO 33 34from gramps.gen.dbstate import DbState 35from gramps.gen.user import User 36from gramps.cli.grampscli import CLIManager 37from gramps.cli.argparser import ArgParser 38from gramps.cli.arghandler import ArgHandler 39from gramps.gen.const import USER_DIRLIST 40from gramps.gen.filters import reload_custom_filters 41reload_custom_filters() # so reports with filter options don't fail 42 43# _caller_context is primarily here to support and document the process 44# of determining the test-module's directory. 45# 46# NB: the traceback 0-element is 'limit'-levels back, or earliest calling 47# context if that is less than limit. 48# The -1 element is this very function; -2 is its caller, etc. 49# A traceback context tuple is: 50# (file, line, active function, text of the call-line) 51def _caller_context(): 52 """Return context of first caller outside this module""" 53 lim = 5 # 1 for this function, plus futrher chain within this module 54 st = traceback.extract_stack(limit=lim) 55 thisfile = __file__.rstrip("co") # eg, in ".py[co] 56 while st and st[-1][0] == thisfile: 57 del(st[-1]) 58 if not st: 59 raise TestError("Unexpected function call chain length!") 60 return st[-1] 61 62 63# NB: tb[0] differs between running 'XYZ_test.py' and './XYZ_test.py' 64# so, always take the abspath. 65def _caller_dir(): 66 """Return directory of caller function (caller outside this module)""" 67 tb = _caller_context() 68 return os.path.dirname(os.path.abspath(tb[0])) 69 70 71class TestError(Exception): 72 """Exception for use by test modules 73 74 Use this, for example, to distuinguish testing errors. 75 76 """ 77 def __init__(self, value): 78 self.value = value 79 def __str__(self): 80 return repr(self.value) 81 82 83def msg(got, exp, msg, pfx=""): 84 """Error-report message formatting utility 85 86 This improves unittest failure messages by showing data values 87 Usage: 88 assertEqual(got,exp, msg(got,exp,"mess" [,prefix]) 89 The failure message will show as 90 [prefix: ] mess 91 .....got:repr(value-of-got) 92 expected:repr(value-of-exp) 93 94 """ 95 if pfx: 96 pfx += ": " 97 return "%s%s\n .....got:%r\n expected:%r" % (pfx, msg, got, exp) 98 99 100def absdir(path=None): 101 """Return absolute dir of the specified path 102 103 The path parm may be dir or file or missing. 104 If a file, the dir of the file is used. 105 If missing, the dir of test-module caller is used 106 107 Common usage is 108 here = absdir() 109 here = absdir(__file__) 110 These 2 return the same result 111 112 """ 113 if not path: 114 path = _caller_dir() 115 loc = os.path.abspath(path) 116 if os.path.isfile(loc): 117 loc = os.path.dirname(loc) 118 return loc 119 120 121def path_append_parent(path=None): 122 """Append (if required) the parent of a path to the python system path, 123 and return the abspath to the parent as a possible convenience 124 125 The path parm may be a dir or a file or missing. 126 If a file, the dir of the file is used. 127 If missing the test-module caller's dir is used. 128 And then the parent of that dir is appended (if not already present) 129 130 Common usage is 131 path_append_parent() 132 path_append_parent(__file__) 133 These 2 produce the same result 134 135 """ 136 pdir = os.path.dirname(absdir(path)) 137 if not pdir in sys.path: 138 sys.path.append(pdir) 139 return pdir 140 141 142def make_subdir(dir, parent=None): 143 """Make (if required) a subdir to a given parent and return its path 144 145 The parent parm may be dir or file or missing 146 If a file, the dir of the file us used 147 If missing, the test-module caller's dir is used 148 Then the subdir dir in the parent dir is created if not already present 149 150 """ 151 if not parent: 152 parent = _caller_dir() 153 sdir = os.path.join(parent,dir) 154 if not os.path.exists(sdir): 155 os.mkdir(sdir) 156 return sdir 157 158def delete_tree(dir): 159 """Recursively delete directory and content 160 161 WARNING: this is clearly dangerous 162 it will only operate on subdirs of the test module dir or of /tmp 163 164 Test writers may explicitly use shutil.rmtree if really needed 165 """ 166 167 if not os.path.isdir(dir): 168 raise TestError("%r is not a dir" % dir) 169 sdir = os.path.abspath(dir) 170 here = _caller_dir() + os.path.sep 171 tmp = tempfile.gettempdir() + os.path.sep 172 if not (sdir.startswith(here) or sdir.startswith(tmp)): 173 raise TestError("%r is not a subdir of here (%r) or %r" 174 % (dir, here, tmp)) 175 shutil.rmtree(sdir) 176 177### Support for testing CLI 178 179def new_exit(edit_code=None): 180 raise SystemExit() 181 182@contextlib.contextmanager 183def capture(stdin, bytesio=False): 184 oldout, olderr = sys.stdout, sys.stderr 185 oldexit = sys.exit 186 if stdin: 187 oldin = sys.stdin 188 sys.stdin = stdin 189 logger = logging.getLogger() 190 old_level = logger.getEffectiveLevel() 191 logger.setLevel(logging.CRITICAL) 192 try: 193 output = [BytesIO() if bytesio else StringIO(), StringIO()] 194 sys.stdout, sys.stderr = output 195 sys.exit = new_exit 196 yield output 197 except SystemExit: 198 pass 199 finally: 200 logger.setLevel(old_level) 201 sys.stdout, sys.stderr = oldout, olderr 202 sys.exit = oldexit 203 if stdin: 204 sys.stdin = oldin 205 output[0] = output[0].getvalue() 206 output[1] = output[1].getvalue() 207 208class Gramps: 209 def __init__(self, user=None, dbstate=None): 210 ## Setup: 211 from gramps.cli.clidbman import CLIDbManager 212 self.dbstate = dbstate or DbState() 213 #we need a manager for the CLI session 214 self.user = user or User() 215 self.climanager = CLIManager(self.dbstate, setloader=True, user=self.user) 216 self.clidbmanager = CLIDbManager(self.dbstate) 217 218 def run(self, *args, stdin=None, bytesio=False): 219 with capture(stdin, bytesio=bytesio) as output: 220 try: 221 try: # make sure we have user directories 222 for path in USER_DIRLIST: 223 if not os.path.isdir(path): 224 os.makedirs(path) 225 except OSError as msg: 226 print("Error creating user directories: " + str(msg)) 227 except: 228 print("Error reading configuration.", exc_info=True) 229 #load the plugins 230 self.climanager.do_reg_plugins(self.dbstate, uistate=None) 231 # handle the arguments 232 args = [sys.executable] + list(args) 233 argparser = ArgParser(args) 234 argparser.need_gui() # initializes some variables 235 if argparser.errors: 236 print(argparser.errors, file=sys.stderr) 237 argparser.print_help() 238 argparser.print_usage() 239 handler = ArgHandler(self.dbstate, argparser, self.climanager) 240 # create a manager to manage the database 241 handler.handle_args_cli() 242 if handler.dbstate.is_open(): 243 handler.dbstate.db.close() 244 except: 245 print("Exception in test:") 246 print("-" * 60) 247 traceback.print_exc(file=sys.stdout) 248 print("-" * 60) 249 250 return output 251 252#===eof=== 253