CedarBackup3.testutil

Package CedarBackup3 :: Module testutil |
[hide private]
[frames] | no frames]

# Source Code for Module CedarBackup3.testutil

      1  # -*- coding: iso-8859-1 -*-
      2  # vim: set ft=python ts=3 sw=3 expandtab:
      3  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
      4  #
      5  #              C E D A R
      6  #          S O L U T I O N S       "Software done right."
      7  #           S O F T W A R E
      8  #
      9  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
     10  #
     11  # Copyright (c) 2004-2006,2008,2010,2015 Kenneth J. Pronovici.
     12  # All rights reserved.
     13  #
     14  # This program is free software; you can redistribute it and/or
     15  # modify it under the terms of the GNU General Public License,
     16  # Version 2, as published by the Free Software Foundation.
     17  #
     18  # This program is distributed in the hope that it will be useful,
     19  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     20  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
     21  #
     22  # Copies of the GNU General Public License are available from
     23  # the Free Software Foundation website, http://www.gnu.org/.
     24  #
     25  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
     26  #
     27  # Author   : Kenneth J. Pronovici <pronovic@ieee.org>
     28  # Language : Python 3 (>= 3.4)
     29  # Project  : Cedar Backup, release 3
     30  # Purpose  : Provides unit-testing utilities.
     31  #
     32  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
     33
     34  ########################################################################
     35  # Module documentation
     36  ########################################################################
     37
     38  """
     39  Provides unit-testing utilities.
     40
     41  These utilities are kept here, separate from util.py, because they provide
     42  common functionality that I do not want exported "publicly" once Cedar Backup
     43  is installed on a system.  They are only used for unit testing, and are only
     44  useful within the source tree.
     45
     46  Many of these functions are in here because they are "good enough" for unit
     47  test work but are not robust enough to be real public functions.  Others (like
     48  L{removedir}) do what they are supposed to, but I don't want responsibility for
     49  making them available to others.
     50
     51  @sort: findResources, commandAvailable,
     52         buildPath, removedir, extractTar, changeFileAge,
     53         getMaskAsMode, getLogin, failUnlessAssignRaises, runningAsRoot,
     54         platformDebian, platformMacOsX
     55
     56  @author: Kenneth J. Pronovici <pronovic@ieee.org>
     57  """
     58
     59
     60  ########################################################################
     61  # Imported modules
     62  ########################################################################
     63
     64  import sys
     65  import os
     66  import tarfile
     67  import time
     68  import getpass
     69  import random
     70  import string # pylint: disable=W0402
     71  import platform
     72  import logging
     73  from io import StringIO
     74
     75  from CedarBackup3.util import encodePath, executeCommand
     76  from CedarBackup3.config import Config, OptionsConfig
     77  from CedarBackup3.customize import customizeOverrides
     78  from CedarBackup3.cli import setupPathResolver
     79
     80
     81  ########################################################################
     82  # Public functions
     83  ########################################################################
     84
     85  ##############################
     86  # setupDebugLogger() function
     87  ##############################
     88
     89 -def setupDebugLogger():
     90     """
     91     Sets up a screen logger for debugging purposes.
     92
     93     Normally, the CLI functionality configures the logger so that
     94     things get written to the right place.  However, for debugging
     95     it's sometimes nice to just get everything -- debug information
     96     and output -- dumped to the screen.  This function takes care
     97     of that.
     98     """
     99     logger = logging.getLogger("CedarBackup3")
    100     logger.setLevel(logging.DEBUG)    # let the logger see all messages
    101     formatter = logging.Formatter(fmt="%(message)s")
    102     handler = logging.StreamHandler(stream=sys.stdout)
    103     handler.setFormatter(formatter)
    104     handler.setLevel(logging.DEBUG)
    105     logger.addHandler(handler)
    106
    107
    108  #################
    109  # setupOverrides
    110  #################
    111
    112 -def setupOverrides():
    113     """
    114     Set up any platform-specific overrides that might be required.
    115
    116     When packages are built, this is done manually (hardcoded) in customize.py
    117     and the overrides are set up in cli.cli().  This way, no runtime checks need
    118     to be done.  This is safe, because the package maintainer knows exactly
    119     which platform (Debian or not) the package is being built for.
    120
    121     Unit tests are different, because they might be run anywhere.  So, we
    122     attempt to make a guess about plaform using platformDebian(), and use that
    123     to set up the custom overrides so that platform-specific unit tests continue
    124     to work.
    125     """
    126     config = Config()
    127     config.options = OptionsConfig()
    128     if platformDebian():
    129        customizeOverrides(config, platform="debian")
    130     else:
    131        customizeOverrides(config, platform="standard")
    132     setupPathResolver(config)
    133
    134
    135  ###########################
    136  # findResources() function
    137  ###########################
    138
    139 -def findResources(resources, dataDirs):
    140     """
    141     Returns a dictionary of locations for various resources.
    142     @param resources: List of required resources.
    143     @param dataDirs: List of data directories to search within for resources.
    144     @return: Dictionary mapping resource name to resource path.
    145     @raise Exception: If some resource cannot be found.
    146     """
    147     mapping = { }
    148     for resource in resources:
    149        for resourceDir in dataDirs:
    150           path = os.path.join(resourceDir, resource)
    151           if os.path.exists(path):
    152              mapping[resource] = path
    153              break
    154        else:
    155           raise Exception("Unable to find resource [%s]." % resource)
    156     return mapping
    157
    158
    159  ##############################
    160  # commandAvailable() function
    161  ##############################
    162
    163 -def commandAvailable(command):
    164     """
    165     Indicates whether a command is available on $PATH somewhere.
    166     This should work on both Windows and UNIX platforms.
    167     @param command: Commang to search for
    168     @return: Boolean true/false depending on whether command is available.
    169     """
    170     if "PATH" in os.environ:
    171        for path in os.environ["PATH"].split(os.sep):
    172           if os.path.exists(os.path.join(path, command)):
    173              return True
    174     return False
    175
    176
    177  #######################
    178  # buildPath() function
    179  #######################
    180
    181 -def buildPath(components):
    182     """
    183     Builds a complete path from a list of components.
    184     For instance, constructs C{"/a/b/c"} from C{["/a", "b", "c",]}.
    185     @param components: List of components.
    186     @returns: String path constructed from components.
    187     @raise ValueError: If a path cannot be encoded properly.
    188     """
    189     path = components[0]
    190     for component in components[1:]:
    191        path = os.path.join(path, component)
    192     return encodePath(path)
    193
    194
    195  #######################
    196  # removedir() function
    197  #######################
    198
    199 -def removedir(tree):
    200     """
    201     Recursively removes an entire directory.
    202     This is basically taken from an example on python.com.
    203     @param tree: Directory tree to remove.
    204     @raise ValueError: If a path cannot be encoded properly.
    205     """
    206     tree = encodePath(tree)
    207     for root, dirs, files in os.walk(tree, topdown=False):
    208        for name in files:
    209           path = os.path.join(root, name)
    210           if os.path.islink(path):
    211              os.remove(path)
    212           elif os.path.isfile(path):
    213              os.remove(path)
    214        for name in dirs:
    215           path = os.path.join(root, name)
    216           if os.path.islink(path):
    217              os.remove(path)
    218           elif os.path.isdir(path):
    219              os.rmdir(path)
    220     os.rmdir(tree)
    221
    222
    223  ########################
    224  # extractTar() function
    225  ########################
    226
    227 -def extractTar(tmpdir, filepath):
    228     """
    229     Extracts the indicated tar file to the indicated tmpdir.
    230     @param tmpdir: Temp directory to extract to.
    231     @param filepath: Path to tarfile to extract.
    232     @raise ValueError: If a path cannot be encoded properly.
    233     """
    234     # pylint: disable=E1101
    235     tmpdir = encodePath(tmpdir)
    236     filepath = encodePath(filepath)
    237     with tarfile.open(filepath) as tar:
    238        try:
    239           tar.format = tarfile.GNU_FORMAT
    240        except AttributeError:
    241           tar.posix = False
    242        for tarinfo in tar:
    243           tar.extract(tarinfo, tmpdir)
    244
    245
    246  ###########################
    247  # changeFileAge() function
    248  ###########################
    249
    250 -def changeFileAge(filename, subtract=None):
    251     """
    252     Changes a file age using the C{os.utime} function.
    253
    254     @note: Some platforms don't seem to be able to set an age precisely.  As a
    255     result, whereas we might have intended to set an age of 86400 seconds, we
    256     actually get an age of 86399.375 seconds.  When util.calculateFileAge()
    257     looks at that the file, it calculates an age of 0.999992766204 days, which
    258     then gets truncated down to zero whole days.  The tests get very confused.
    259     To work around this, I always subtract off one additional second as a fudge
    260     factor.  That way, the file age will be I{at least} as old as requested
    261     later on.
    262
    263     @param filename: File to operate on.
    264     @param subtract: Number of seconds to subtract from the current time.
    265     @raise ValueError: If a path cannot be encoded properly.
    266     """
    267     filename = encodePath(filename)
    268     newTime = time.time() - 1
    269     if subtract is not None:
    270        newTime -= subtract
    271     os.utime(filename, (newTime, newTime))
    272
    273
    274  ###########################
    275  # getMaskAsMode() function
    276  ###########################
    277
    278 -def getMaskAsMode():
    279     """
    280     Returns the user's current umask inverted to a mode.
    281     A mode is mostly a bitwise inversion of a mask, i.e. mask 002 is mode 775.
    282     @return: Umask converted to a mode, as an integer.
    283     """
    284     umask = os.umask(0o777)
    285     os.umask(umask)
    286     return int(~umask & 0o777)  # invert, then use only lower bytes
    287
    288
    289  ######################
    290  # getLogin() function
    291  ######################
    292
    293 -def getLogin():
    294     """
    295     Returns the name of the currently-logged in user.  This might fail under
    296     some circumstances - but if it does, our tests would fail anyway.
    297     """
    298     return getpass.getuser()
    299
    300
    301  ############################
    302  # randomFilename() function
    303  ############################
    304
    305 -def randomFilename(length, prefix=None, suffix=None):
    306     """
    307     Generates a random filename with the given length.
    308     @param length: Length of filename.
    309     @return Random filename.
    310     """
    311     characters = [None] * length
    312     for i in range(length):
    313        characters[i] = random.choice(string.ascii_uppercase)
    314     if prefix is None:
    315        prefix = ""
    316     if suffix is None:
    317        suffix = ""
    318     return "%s%s%s" % (prefix, "".join(characters), suffix)
    319
    320
    321  ####################################
    322  # failUnlessAssignRaises() function
    323  ####################################
    324
    325 -def failUnlessAssignRaises(testCase, exception, obj, prop, value):
    326     """
    327     Equivalent of C{failUnlessRaises}, but used for property assignments instead.
    328
    329     It's nice to be able to use C{failUnlessRaises} to check that a method call
    330     raises the exception that you expect.  Unfortunately, this method can't be
    331     used to check Python propery assignments, even though these property
    332     assignments are actually implemented underneath as methods.
    333
    334     This function (which can be easily called by unit test classes) provides an
    335     easy way to wrap the assignment checks.  It's not pretty, or as intuitive as
    336     the original check it's modeled on, but it does work.
    337
    338     Let's assume you make this method call::
    339
    340        testCase.failUnlessAssignRaises(ValueError, collectDir, "absolutePath", absolutePath)
    341
    342     If you do this, a test case failure will be raised unless the assignment::
    343
    344        collectDir.absolutePath = absolutePath
    345
    346     fails with a C{ValueError} exception.  The failure message differentiates
    347     between the case where no exception was raised and the case where the wrong
    348     exception was raised.
    349
    350     @note: Internally, the C{missed} and C{instead} variables are used rather
    351     than directly calling C{testCase.fail} upon noticing a problem because the
    352     act of "failure" itself generates an exception that would be caught by the
    353     general C{except} clause.
    354
    355     @param testCase: PyUnit test case object (i.e. self).
    356     @param exception: Exception that is expected to be raised.
    357     @param obj: Object whose property is to be assigned to.
    358     @param prop: Name of the property, as a string.
    359     @param value: Value that is to be assigned to the property.
    360
    361     @see: C{unittest.TestCase.failUnlessRaises}
    362     """
    363     missed = False
    364     instead = None
    365     try:
    366        exec("obj.%s = value" % prop)    # pylint: disable=W0122
    367        missed = True
    368     except exception: pass
    369     except Exception as e:
    370        instead = e
    371     if missed:
    372        testCase.fail("Expected assignment to raise %s, but got no exception." % (exception.__name__))
    373     if instead is not None:
    374        testCase.fail("Expected assignment to raise %s, but got %s instead." % (ValueError, instead.__class__.__name__))
    375
    376
    377  ###########################
    378  # captureOutput() function
    379  ###########################
    380
    381 -def captureOutput(c):
    382     """
    383     Captures the output (stdout, stderr) of a function or a method.
    384
    385     Some of our functions don't do anything other than just print output.  We
    386     need a way to test these functions (at least nominally) but we don't want
    387     any of the output spoiling the test suite output.
    388
    389     This function just creates a dummy file descriptor that can be used as a
    390     target by the callable function, rather than C{stdout} or C{stderr}.
    391
    392     @note: This method assumes that C{callable} doesn't take any arguments
    393     besides keyword argument C{fd} to specify the file descriptor.
    394
    395     @param c: Callable function or method.
    396
    397     @return: Output of function, as one big string.
    398     """
    399     fd = StringIO()
    400     c(fd=fd)
    401     result = fd.getvalue()
    402     fd.close()
    403     return result
    404
    405
    406  #########################
    407  # _isPlatform() function
    408  #########################
    409
    410 -def _isPlatform(name):
    411     """
    412     Returns boolean indicating whether we're running on the indicated platform.
    413     @param name: Platform name to check, currently one of "windows" or "macosx"
    414     """
    415     if name == "windows":
    416        return platform.platform(True, True).startswith("Windows")
    417     elif name == "macosx":
    418        return sys.platform == "darwin"
    419     elif name == "debian":
    420        return platform.platform(False, False).find("debian") > 0
    421     elif name == "cygwin":
    422        return platform.platform(True, True).startswith("CYGWIN")
    423     else:
    424        raise ValueError("Unknown platform [%s]." % name)
    425
    426
    427  ############################
    428  # platformDebian() function
    429  ############################
    430
    431 -def platformDebian():
    432     """
    433     Returns boolean indicating whether this is the Debian platform.
    434     """
    435     return _isPlatform("debian")
    436
    437
    438  ############################
    439  # platformMacOsX() function
    440  ############################
    441
    442 -def platformMacOsX():
    443     """
    444     Returns boolean indicating whether this is the Mac OS X platform.
    445     """
    446     return _isPlatform("macosx")
    447
    448
    449  ###########################
    450  # runningAsRoot() function
    451  ###########################
    452
    453 -def runningAsRoot():
    454     """
    455     Returns boolean indicating whether the effective user id is root.
    456     """
    457     return os.geteuid() == 0
    458
    459
    460  ##############################
    461  # availableLocales() function
    462  ##############################
    463
    464 -def availableLocales():
    465     """
    466     Returns a list of available locales on the system
    467     @return: List of string locale names
    468     """
    469     locales = []
    470     output = executeCommand(["locale"], [ "-a", ], returnOutput=True, ignoreStderr=True)[1]
    471     for line in output:
    472        locales.append(line.rstrip())
    473     return locales
    474