CedarBackup2.testutil

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

# Source Code for Module CedarBackup2.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 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 (>= 2.5)
     29  # Project  : Cedar Backup, release 2
     30  # Revision : $Id: testutil.py 1023 2011-10-11 23:44:50Z pronovic $
     31  # Purpose  : Provides unit-testing utilities.
     32  #
     33  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
     34
     35  ########################################################################
     36  # Module documentation
     37  ########################################################################
     38
     39  """
     40  Provides unit-testing utilities.
     41
     42  These utilities are kept here, separate from util.py, because they provide
     43  common functionality that I do not want exported "publicly" once Cedar Backup
     44  is installed on a system.  They are only used for unit testing, and are only
     45  useful within the source tree.
     46
     47  Many of these functions are in here because they are "good enough" for unit
     48  test work but are not robust enough to be real public functions.  Others (like
     49  L{removedir}) do what they are supposed to, but I don't want responsibility for
     50  making them available to others.
     51
     52  @sort: findResources, commandAvailable,
     53         buildPath, removedir, extractTar, changeFileAge,
     54         getMaskAsMode, getLogin, failUnlessAssignRaises, runningAsRoot,
     55         platformDebian, platformMacOsX, platformCygwin, platformWindows,
     56         platformHasEcho, platformSupportsLinks, platformSupportsPermissions,
     57         platformRequiresBinaryRead
     58
     59  @author: Kenneth J. Pronovici <pronovic@ieee.org>
     60  """
     61
     62
     63  ########################################################################
     64  # Imported modules
     65  ########################################################################
     66
     67  import sys
     68  import os
     69  import tarfile
     70  import time
     71  import getpass
     72  import random
     73  import string # pylint: disable=W0402
     74  import platform
     75  import logging
     76  from StringIO import StringIO
     77
     78  from CedarBackup2.util import encodePath, executeCommand
     79  from CedarBackup2.config import Config, OptionsConfig
     80  from CedarBackup2.customize import customizeOverrides
     81  from CedarBackup2.cli import setupPathResolver
     82
     83
     84  ########################################################################
     85  # Public functions
     86  ########################################################################
     87
     88  ##############################
     89  # setupDebugLogger() function
     90  ##############################
     91
     92 -def setupDebugLogger():
     93     """
     94     Sets up a screen logger for debugging purposes.
     95
     96     Normally, the CLI functionality configures the logger so that
     97     things get written to the right place.  However, for debugging
     98     it's sometimes nice to just get everything -- debug information
     99     and output -- dumped to the screen.  This function takes care
    100     of that.
    101     """
    102     logger = logging.getLogger("CedarBackup2")
    103     logger.setLevel(logging.DEBUG)    # let the logger see all messages
    104     formatter = logging.Formatter(fmt="%(message)s")
    105     handler = logging.StreamHandler(strm=sys.stdout)
    106     handler.setFormatter(formatter)
    107     handler.setLevel(logging.DEBUG)
    108     logger.addHandler(handler)
    109
    110
    111  #################
    112  # setupOverrides
    113  #################
    114
    115 -def setupOverrides():
    116     """
    117     Set up any platform-specific overrides that might be required.
    118
    119     When packages are built, this is done manually (hardcoded) in customize.py
    120     and the overrides are set up in cli.cli().  This way, no runtime checks need
    121     to be done.  This is safe, because the package maintainer knows exactly
    122     which platform (Debian or not) the package is being built for.
    123
    124     Unit tests are different, because they might be run anywhere.  So, we
    125     attempt to make a guess about plaform using platformDebian(), and use that
    126     to set up the custom overrides so that platform-specific unit tests continue
    127     to work.
    128     """
    129     config = Config()
    130     config.options = OptionsConfig()
    131     if platformDebian():
    132        customizeOverrides(config, platform="debian")
    133     else:
    134        customizeOverrides(config, platform="standard")
    135     setupPathResolver(config)
    136
    137
    138  ###########################
    139  # findResources() function
    140  ###########################
    141
    142 -def findResources(resources, dataDirs):
    143     """
    144     Returns a dictionary of locations for various resources.
    145     @param resources: List of required resources.
    146     @param dataDirs: List of data directories to search within for resources.
    147     @return: Dictionary mapping resource name to resource path.
    148     @raise Exception: If some resource cannot be found.
    149     """
    150     mapping = { }
    151     for resource in resources:
    152        for resourceDir in dataDirs:
    153           path = os.path.join(resourceDir, resource)
    154           if os.path.exists(path):
    155              mapping[resource] = path
    156              break
    157        else:
    158           raise Exception("Unable to find resource [%s]." % resource)
    159     return mapping
    160
    161
    162  ##############################
    163  # commandAvailable() function
    164  ##############################
    165
    166 -def commandAvailable(command):
    167     """
    168     Indicates whether a command is available on $PATH somewhere.
    169     This should work on both Windows and UNIX platforms.
    170     @param command: Commang to search for
    171     @return: Boolean true/false depending on whether command is available.
    172     """
    173     if os.environ.has_key("PATH"):
    174        for path in os.environ["PATH"].split(os.sep):
    175           if os.path.exists(os.path.join(path, command)):
    176              return True
    177     return False
    178
    179
    180  #######################
    181  # buildPath() function
    182  #######################
    183
    184 -def buildPath(components):
    185     """
    186     Builds a complete path from a list of components.
    187     For instance, constructs C{"/a/b/c"} from C{["/a", "b", "c",]}.
    188     @param components: List of components.
    189     @returns: String path constructed from components.
    190     @raise ValueError: If a path cannot be encoded properly.
    191     """
    192     path = components[0]
    193     for component in components[1:]:
    194        path = os.path.join(path, component)
    195     return encodePath(path)
    196
    197
    198  #######################
    199  # removedir() function
    200  #######################
    201
    202 -def removedir(tree):
    203     """
    204     Recursively removes an entire directory.
    205     This is basically taken from an example on python.com.
    206     @param tree: Directory tree to remove.
    207     @raise ValueError: If a path cannot be encoded properly.
    208     """
    209     tree = encodePath(tree)
    210     for root, dirs, files in os.walk(tree, topdown=False):
    211        for name in files:
    212           path = os.path.join(root, name)
    213           if os.path.islink(path):
    214              os.remove(path)
    215           elif os.path.isfile(path):
    216              os.remove(path)
    217        for name in dirs:
    218           path = os.path.join(root, name)
    219           if os.path.islink(path):
    220              os.remove(path)
    221           elif os.path.isdir(path):
    222              os.rmdir(path)
    223     os.rmdir(tree)
    224
    225
    226  ########################
    227  # extractTar() function
    228  ########################
    229
    230 -def extractTar(tmpdir, filepath):
    231     """
    232     Extracts the indicated tar file to the indicated tmpdir.
    233     @param tmpdir: Temp directory to extract to.
    234     @param filepath: Path to tarfile to extract.
    235     @raise ValueError: If a path cannot be encoded properly.
    236     """
    237     # pylint: disable=E1101
    238     tmpdir = encodePath(tmpdir)
    239     filepath = encodePath(filepath)
    240     tar = tarfile.open(filepath)
    241     try:
    242        tar.format = tarfile.GNU_FORMAT
    243     except AttributeError:
    244        tar.posix = False
    245     for tarinfo in tar:
    246        tar.extract(tarinfo, tmpdir)
    247
    248
    249  ###########################
    250  # changeFileAge() function
    251  ###########################
    252
    253 -def changeFileAge(filename, subtract=None):
    254     """
    255     Changes a file age using the C{os.utime} function.
    256
    257     @note: Some platforms don't seem to be able to set an age precisely.  As a
    258     result, whereas we might have intended to set an age of 86400 seconds, we
    259     actually get an age of 86399.375 seconds.  When util.calculateFileAge()
    260     looks at that the file, it calculates an age of 0.999992766204 days, which
    261     then gets truncated down to zero whole days.  The tests get very confused.
    262     To work around this, I always subtract off one additional second as a fudge
    263     factor.  That way, the file age will be I{at least} as old as requested
    264     later on.
    265
    266     @param filename: File to operate on.
    267     @param subtract: Number of seconds to subtract from the current time.
    268     @raise ValueError: If a path cannot be encoded properly.
    269     """
    270     filename = encodePath(filename)
    271     newTime = time.time() - 1
    272     if subtract is not None:
    273        newTime -= subtract
    274     os.utime(filename, (newTime, newTime))
    275
    276
    277  ###########################
    278  # getMaskAsMode() function
    279  ###########################
    280
    281 -def getMaskAsMode():
    282     """
    283     Returns the user's current umask inverted to a mode.
    284     A mode is mostly a bitwise inversion of a mask, i.e. mask 002 is mode 775.
    285     @return: Umask converted to a mode, as an integer.
    286     """
    287     umask = os.umask(0777)
    288     os.umask(umask)
    289     return int(~umask & 0777)  # invert, then use only lower bytes
    290
    291
    292  ######################
    293  # getLogin() function
    294  ######################
    295
    296 -def getLogin():
    297     """
    298     Returns the name of the currently-logged in user.  This might fail under
    299     some circumstances - but if it does, our tests would fail anyway.
    300     """
    301     return getpass.getuser()
    302
    303
    304  ############################
    305  # randomFilename() function
    306  ############################
    307
    308 -def randomFilename(length, prefix=None, suffix=None):
    309     """
    310     Generates a random filename with the given length.
    311     @param length: Length of filename.
    312     @return Random filename.
    313     """
    314     characters = [None] * length
    315     for i in xrange(length):
    316        characters[i] = random.choice(string.ascii_uppercase)
    317     if prefix is None:
    318        prefix = ""
    319     if suffix is None:
    320        suffix = ""
    321     return "%s%s%s" % (prefix, "".join(characters), suffix)
    322
    323
    324  ####################################
    325  # failUnlessAssignRaises() function
    326  ####################################
    327
    328 -def failUnlessAssignRaises(testCase, exception, obj, prop, value):
    329     """
    330     Equivalent of C{failUnlessRaises}, but used for property assignments instead.
    331
    332     It's nice to be able to use C{failUnlessRaises} to check that a method call
    333     raises the exception that you expect.  Unfortunately, this method can't be
    334     used to check Python propery assignments, even though these property
    335     assignments are actually implemented underneath as methods.
    336
    337     This function (which can be easily called by unit test classes) provides an
    338     easy way to wrap the assignment checks.  It's not pretty, or as intuitive as
    339     the original check it's modeled on, but it does work.
    340
    341     Let's assume you make this method call::
    342
    343        testCase.failUnlessAssignRaises(ValueError, collectDir, "absolutePath", absolutePath)
    344
    345     If you do this, a test case failure will be raised unless the assignment::
    346
    347        collectDir.absolutePath = absolutePath
    348
    349     fails with a C{ValueError} exception.  The failure message differentiates
    350     between the case where no exception was raised and the case where the wrong
    351     exception was raised.
    352
    353     @note: Internally, the C{missed} and C{instead} variables are used rather
    354     than directly calling C{testCase.fail} upon noticing a problem because the
    355     act of "failure" itself generates an exception that would be caught by the
    356     general C{except} clause.
    357
    358     @param testCase: PyUnit test case object (i.e. self).
    359     @param exception: Exception that is expected to be raised.
    360     @param obj: Object whose property is to be assigned to.
    361     @param prop: Name of the property, as a string.
    362     @param value: Value that is to be assigned to the property.
    363
    364     @see: C{unittest.TestCase.failUnlessRaises}
    365     """
    366     missed = False
    367     instead = None
    368     try:
    369        exec "obj.%s = value" % prop    # pylint: disable=W0122
    370        missed = True
    371     except exception: pass
    372     except Exception, e:
    373        instead = e
    374     if missed:
    375        testCase.fail("Expected assignment to raise %s, but got no exception." % (exception.__name__))
    376     if instead is not None:
    377        testCase.fail("Expected assignment to raise %s, but got %s instead." % (ValueError, instead.__class__.__name__))
    378
    379
    380  ###########################
    381  # captureOutput() function
    382  ###########################
    383
    384 -def captureOutput(c):
    385     """
    386     Captures the output (stdout, stderr) of a function or a method.
    387
    388     Some of our functions don't do anything other than just print output.  We
    389     need a way to test these functions (at least nominally) but we don't want
    390     any of the output spoiling the test suite output.
    391
    392     This function just creates a dummy file descriptor that can be used as a
    393     target by the callable function, rather than C{stdout} or C{stderr}.
    394
    395     @note: This method assumes that C{callable} doesn't take any arguments
    396     besides keyword argument C{fd} to specify the file descriptor.
    397
    398     @param c: Callable function or method.
    399
    400     @return: Output of function, as one big string.
    401     """
    402     fd = StringIO()
    403     c(fd=fd)
    404     result = fd.getvalue()
    405     fd.close()
    406     return result
    407
    408
    409  #########################
    410  # _isPlatform() function
    411  #########################
    412
    413 -def _isPlatform(name):
    414     """
    415     Returns boolean indicating whether we're running on the indicated platform.
    416     @param name: Platform name to check, currently one of "windows" or "macosx"
    417     """
    418     if name == "windows":
    419        return platform.platform(True, True).startswith("Windows")
    420     elif name == "macosx":
    421        return sys.platform == "darwin"
    422     elif name == "debian":
    423        return platform.platform(False, False).find("debian") > 0
    424     elif name == "cygwin":
    425        return platform.platform(True, True).startswith("CYGWIN")
    426     else:
    427        raise ValueError("Unknown platform [%s]." % name)
    428
    429
    430  ############################
    431  # platformDebian() function
    432  ############################
    433
    434 -def platformDebian():
    435     """
    436     Returns boolean indicating whether this is the Debian platform.
    437     """
    438     return _isPlatform("debian")
    439
    440
    441  ############################
    442  # platformMacOsX() function
    443  ############################
    444
    445 -def platformMacOsX():
    446     """
    447     Returns boolean indicating whether this is the Mac OS X platform.
    448     """
    449     return _isPlatform("macosx")
    450
    451
    452  #############################
    453  # platformWindows() function
    454  #############################
    455
    456 -def platformWindows():
    457     """
    458     Returns boolean indicating whether this is the Windows platform.
    459     """
    460     return _isPlatform("windows")
    461
    462
    463  ############################
    464  # platformCygwin() function
    465  ############################
    466
    467 -def platformCygwin():
    468     """
    469     Returns boolean indicating whether this is the Cygwin platform.
    470     """
    471     return _isPlatform("cygwin")
    472
    473
    474  ###################################
    475  # platformSupportsLinks() function
    476  ###################################
    477
    478 -def platformSupportsLinks():
    479     """
    480     Returns boolean indicating whether the platform supports soft-links.
    481     Some platforms, like Windows, do not support links, and tests need to take
    482     this into account.
    483     """
    484     return not platformWindows()
    485
    486
    487  #########################################
    488  # platformSupportsPermissions() function
    489  #########################################
    490
    491 -def platformSupportsPermissions():
    492     """
    493     Returns boolean indicating whether the platform supports UNIX-style file permissions.
    494     Some platforms, like Windows, do not support permissions, and tests need to take
    495     this into account.
    496     """
    497     return not platformWindows()
    498
    499
    500  ########################################
    501  # platformRequiresBinaryRead() function
    502  ########################################
    503
    504 -def platformRequiresBinaryRead():
    505     """
    506     Returns boolean indicating whether the platform requires binary reads.
    507     Some platforms, like Windows, require a special flag to read binary data
    508     from files.
    509     """
    510     return platformWindows()
    511
    512
    513  #############################
    514  # platformHasEcho() function
    515  #############################
    516
    517 -def platformHasEcho():
    518     """
    519     Returns boolean indicating whether the platform has a sensible echo command.
    520     On some platforms, like Windows, echo doesn't really work for tests.
    521     """
    522     return not platformWindows()
    523
    524
    525  ###########################
    526  # runningAsRoot() function
    527  ###########################
    528
    529 -def runningAsRoot():
    530     """
    531     Returns boolean indicating whether the effective user id is root.
    532     This is always true on platforms that have no concept of root, like Windows.
    533     """
    534     if platformWindows():
    535        return True
    536     else:
    537        return os.geteuid() == 0
    538
    539
    540  ##############################
    541  # availableLocales() function
    542  ##############################
    543
    544 -def availableLocales():
    545     """
    546     Returns a list of available locales on the system
    547     @return: List of string locale names
    548     """
    549     locales = []
    550     output = executeCommand(["locale"], [ "-a", ], returnOutput=True, ignoreStderr=True)[1]
    551     for line in output:
    552        locales.append(line.rstrip())
    553     return locales
    554
    555
    556  ####################################
    557  # hexFloatLiteralAllowed() function
    558  ####################################
    559
    560 -def hexFloatLiteralAllowed():
    561     """
    562     Indicates whether hex float literals are allowed by the interpreter.
    563
    564     As far back as 2004, some Python documentation indicated that octal and hex
    565     notation applied only to integer literals.  However, prior to Python 2.5, it
    566     was legal to construct a float with an argument like 0xAC on some platforms.
    567     This check provides a an indication of whether the current interpreter
    568     supports that behavior.
    569
    570     This check exists so that unit tests can continue to test the same thing as
    571     always for pre-2.5 interpreters (i.e. making sure backwards compatibility
    572     doesn't break) while still continuing to work for later interpreters.
    573
    574     The returned value is True if hex float literals are allowed, False otherwise.
    575     """
    576     if map(int, [sys.version_info[0], sys.version_info[1]]) < [2, 5] and not platformWindows():
    577        return True
    578     return False
    579