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