#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2007  Donald N. Allingham
#
# 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.
#

# test/test_util.py

"""unittest support utility module"""

import os
import sys
import traceback
import tempfile
import shutil
import logging


# _caller_context is primarily here to support and document the process
# of determining the test-module's directory.
# 
# NB: the traceback 0-element is 'limit'-levels back, or earliest calling
# context if that is less than limit.
#  The -1 element is this very function; -2 is its caller, etc.
# A traceback context tuple is: 
#  (file, line, active function, text of the call-line)
def _caller_context():
    """Return context of first caller outside this module"""
    lim = 5  #  1 for this function, plus futrher chain within this module
    st = traceback.extract_stack(limit=lim)
    thisfile = __file__.rstrip("co")  # eg, in ".py[co] 
    while st and st[-1][0] == thisfile:
        del(st[-1])
    if not st:
        raise TestError("Unexpected function call chain length!")
    return st[-1]


# NB: tb[0] differs between running 'XYZ_test.py' and './XYZ_test.py'
#     so, always take the abspath.
def _caller_dir():
    """Return directory of caller function (caller outside this module)"""
    tb =  _caller_context()
    return os.path.dirname(os.path.abspath(tb[0]))


class TestError(Exception):
    """Exception for use by test modules
    
    Use this, for example, to distuinguish testing errors.

    """
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)


def msg(got, exp, msg, pfx=""):
    """Error-report message formatting utility

    This improves unittest failure messages by showing data values
    Usage:
      assertEqual(got,exp, msg(got,exp,"mess" [,prefix])
    The failure message will show as 
      [prefix: ] mess
        .....got:repr(value-of-got)
        expected:repr(value-of-exp)

    """
    if pfx:
        pfx += ": "
    return "%s%s\n .....got:%r\n expected:%r" % (pfx, msg, got, exp)


def absdir(path=None):
    """Return absolute dir of the specified path
    
    The path parm may be dir or file  or missing.
    If a file, the dir of the file is used.
    If missing, the dir of test-module caller is used

    Common usage is 
      here = absdir()
      here = absdir(__file__)
    These 2 return the same result

    """
    if not path:
        path = _caller_dir()
    loc = os.path.abspath(path)
    if os.path.isfile(loc):
        loc = os.path.dirname(loc)
    return loc


def path_append_parent(path=None):
    """Append (if required) the parent of a path to the python system path,
    and return the abspath to the parent as a possible convenience

    The path parm may be a dir or a file or missing.
      If a file, the dir of the file is used. 
      If missing the test-module caller's dir is used.
    And then the parent of that dir is appended (if not already present)

    Common usage is
      path_append_parent()
      path_append_parent(__file__)
    These 2 produce the same result

    """
    pdir = os.path.dirname(absdir(path))
    if not pdir in sys.path:
        sys.path.append(pdir)
    return pdir


def make_subdir(dir, parent=None):
    """Make (if required) a subdir to a given parent and return its path
    
    The parent parm may be dir or file or missing
      If a file, the dir of the file us used
      If missing, the test-module caller's dir is used
    Then the subdir dir in the parent dir is created if not already present

    """
    if not parent:
        parent = _caller_dir()
    sdir = os.path.join(parent,dir)
    if not os.path.exists(sdir):
        os.mkdir(sdir)
    return sdir

def delete_tree(dir):
    """Recursively delete directory and content
    
    WARNING: this is clearly dangerous
      it will only operate on subdirs of the test module dir or of /tmp

    Test writers may explicitly use shutil.rmtree if really needed
    """

    if not os.path.isdir(dir):
        raise TestError("%r is not a dir" % dir)
    sdir = os.path.abspath(dir)
    here = _caller_dir() + os.path.sep
    tmp = tempfile.gettempdir() + os.path.sep
    if not (sdir.startswith(here) or sdir.startswith(tmp)):
        raise TestError("%r is not a subdir of here (%r) or %r" 
            % (dir, here, tmp))
    shutil.rmtree(sdir)

# simplified logging
# gramps-independent but gramps-compatible
#
# I don't see any need to inherit from logging.Logger
# (at present, test code needs nothing fancy)
# but that might be considered for future needs
# NB: current code reflects limited expertise on the 
# uses of the logging module
# ---------------------------------------------------------
class TestLogger():
    """this class mainly just encapsulates some globals
    namely lfname, lfh  for a file log name and handle

    provides simplified logging setup for test modules
    that need to setup logging for modules under test
    (just instantiate a TestLogger to avoid error 
     messages about logging handlers not available)

    There is also a simple logfile capability, to allow
    test modules to capture gramps logging output

    Note that existing logging will still occur, possibly
    resulting in console messages and popup dialogs
    """
    def __init__(self, lvl=logging.WARN):
        logging.basicConfig(level=lvl)
        
    def logfile_init(self, lfname):
        """init or re-init a logfile"""
        if getattr(self, "lfh", None):
            logging.getLogger().handlers.remove(self.lfh)
        if os.path.isfile(lfname):
            os.unlink(lfname)
        self.lfh = logging.FileHandler(lfname)
        logging.getLogger().addHandler(self.lfh)
        self.lfname = lfname
    
    def logfile_getlines(self):
        """get current content of logfile as list of lines"""
        txt = []
        if self.lfname and os.path.isfile(self.lfname):
            txt = open(self.lfname).readlines() 
        return txt

#===eof===