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 (>= 2.7)
     29  # Project  : Cedar Backup, release 2
     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, platformCygwin, platformWindows,
     55         platformHasEcho, platformSupportsLinks, platformSupportsPermissions,
     56         platformRequiresBinaryRead
     57
     58  @author: Kenneth J. Pronovici <pronovic@ieee.org>
     59  """
     60
     61
     62  ########################################################################
     63  # Imported modules
     64  ########################################################################
     65
     66  import sys
     67  import os
     68  import tarfile
     69  import time
     70  import getpass
     71  import random
     72  import string # pylint: disable=W0402
     73  import platform
     74  import logging
     75  from StringIO import StringIO
     76
     77  from CedarBackup2.util import encodePath, executeCommand
     78  from CedarBackup2.config import Config, OptionsConfig
     79  from CedarBackup2.customize import customizeOverrides
     80  from CedarBackup2.cli import setupPathResolver
     81
     82
     83  ########################################################################
     84  # Public functions
     85  ########################################################################
     86
     87  ##############################
     88  # setupDebugLogger() function
     89  ##############################
     90
     91 -def setupDebugLogger():
     92     """
     93     Sets up a screen logger for debugging purposes.
     94
     95     Normally, the CLI functionality configures the logger so that
     96     things get written to the right place.  However, for debugging
     97     it's sometimes nice to just get everything -- debug information
     98     and output -- dumped to the screen.  This function takes care
     99     of that.
    100     """
    101     logger = logging.getLogger("CedarBackup2")
    102     logger.setLevel(logging.DEBUG)    # let the logger see all messages
    103     formatter = logging.Formatter(fmt="%(message)s")
    104     handler = logging.StreamHandler(stream=sys.stdout)
    105     handler.setFormatter(formatter)
    106     handler.setLevel(logging.DEBUG)
    107     logger.addHandler(handler)
    108
    109
    110  #################
    111  # setupOverrides
    112  #################
    113
    114 -def setupOverrides():
    115     """
    116     Set up any platform-specific overrides that might be required.
    117
    118     When packages are built, this is done manually (hardcoded) in customize.py
    119     and the overrides are set up in cli.cli().  This way, no runtime checks need
    120     to be done.  This is safe, because the package maintainer knows exactly
    121     which platform (Debian or not) the package is being built for.
    122
    123     Unit tests are different, because they might be run anywhere.  So, we
    124     attempt to make a guess about plaform using platformDebian(), and use that
    125     to set up the custom overrides so that platform-specific unit tests continue
    126     to work.
    127     """
    128     config = Config()
    129     config.options = OptionsConfig()
    130     if platformDebian():
    131        customizeOverrides(config, platform="debian")
    132     else:
    133        customizeOverrides(config, platform="standard")
    134     setupPathResolver(config)
    135
    136
    137  ###########################
    138  # findResources() function
    139  ###########################
    140
    141 -def findResources(resources, dataDirs):
    142     """
    143     Returns a dictionary of locations for various resources.
    144     @param resources: List of required resources.
    145     @param dataDirs: List of data directories to search within for resources.
    146     @return: Dictionary mapping resource name to resource path.
    147     @raise Exception: If some resource cannot be found.
    148     """
    149     mapping = { }
    150     for resource in resources:
    151        for resourceDir in dataDirs:
    152           path = os.path.join(resourceDir, resource)
    153           if os.path.exists(path):
    154              mapping[resource] = path
    155              break
    156        else:
    157           raise Exception("Unable to find resource [%s]." % resource)
    158     return mapping
    159
    160
    161  ##############################
    162  # commandAvailable() function
    163  ##############################
    164
    165 -def commandAvailable(command):
    166     """
    167     Indicates whether a command is available on $PATH somewhere.
    168     This should work on both Windows and UNIX platforms.
    169     @param command: Commang to search for
    170     @return: Boolean true/false depending on whether command is available.
    171     """
    172     if os.environ.has_key("PATH"):
    173        for path in os.environ["PATH"].split(os.sep):
    174           if os.path.exists(os.path.join(path, command)):
    175              return True
    176     return False
    177
    178
    179  #######################
    180  # buildPath() function
    181  #######################
    182
    183 -def buildPath(components):
    184     """
    185     Builds a complete path from a list of components.
    186     For instance, constructs C{"/a/b/c"} from C{["/a", "b", "c",]}.
    187     @param components: List of components.
    188     @returns: String path constructed from components.
    189     @raise ValueError: If a path cannot be encoded properly.
    190     """
    191     path = components[0]
    192     for component in components[1:]:
    193        path = os.path.join(path, component)
    194     return encodePath(path)
    195
    196
    197  #######################
    198  # removedir() function
    199  #######################
    200
    201 -def removedir(tree):
    202     """
    203     Recursively removes an entire directory.
    204     This is basically taken from an example on python.com.
    205     @param tree: Directory tree to remove.
    206     @raise ValueError: If a path cannot be encoded properly.
    207     """
    208     tree = encodePath(tree)
    209     for root, dirs, files in os.walk(tree, topdown=False):
    210        for name in files:
    211           path = os.path.join(root, name)
    212           if os.path.islink(path):
    213              os.remove(path)
    214           elif os.path.isfile(path):
    215              os.remove(path)
    216        for name in dirs:
    217           path = os.path.join(root, name)
    218           if os.path.islink(path):
    219              os.remove(path)
    220           elif os.path.isdir(path):
    221              os.rmdir(path)
    222     os.rmdir(tree)
    223
    224
    225  ########################
    226  # extractTar() function
    227  ########################
    228
    229 -def extractTar(tmpdir, filepath):
    230     """
    231     Extracts the indicated tar file to the indicated tmpdir.
    232     @param tmpdir: Temp directory to extract to.
    233     @param filepath: Path to tarfile to extract.
    234     @raise ValueError: If a path cannot be encoded properly.
    235     """
    236     # pylint: disable=E1101
    237     tmpdir = encodePath(tmpdir)
    238     filepath = encodePath(filepath)
    239     tar = tarfile.open(filepath)
    240     try:
    241        tar.format = tarfile.GNU_FORMAT
    242     except AttributeError:
    243        tar.posix = False
    244     for tarinfo in tar:
    245        tar.extract(tarinfo, tmpdir)
    246
    247
    248  ###########################
    249  # changeFileAge() function
    250  ###########################
    251
    252 -def changeFileAge(filename, subtract=None):
    253     """
    254     Changes a file age using the C{os.utime} function.
    255
    256     @note: Some platforms don't seem to be able to set an age precisely.  As a
    257     result, whereas we might have intended to set an age of 86400 seconds, we
    258     actually get an age of 86399.375 seconds.  When util.calculateFileAge()
    259     looks at that the file, it calculates an age of 0.999992766204 days, which
    260     then gets truncated down to zero whole days.  The tests get very confused.
    261     To work around this, I always subtract off one additional second as a fudge
    262     factor.  That way, the file age will be I{at least} as old as requested
    263     later on.
    264
    265     @param filename: File to operate on.
    266     @param subtract: Number of seconds to subtract from the current time.
    267     @raise ValueError: If a path cannot be encoded properly.
    268     """
    269     filename = encodePath(filename)
    270     newTime = time.time() - 1
    271     if subtract is not None:
    272        newTime -= subtract
    273     os.utime(filename, (newTime, newTime))
    274
    275
    276  ###########################
    277  # getMaskAsMode() function
    278  ###########################
    279
    280 -def getMaskAsMode():
    281     """
    282     Returns the user's current umask inverted to a mode.
    283     A mode is mostly a bitwise inversion of a mask, i.e. mask 002 is mode 775.
    284     @return: Umask converted to a mode, as an integer.
    285     """
    286     umask = os.umask(0777)
    287     os.umask(umask)
    288     return int(~umask & 0777)  # invert, then use only lower bytes
    289
    290
    291  ######################
    292  # getLogin() function
    293  ######################
    294
    295 -def getLogin():
    296     """
    297     Returns the name of the currently-logged in user.  This might fail under
    298     some circumstances - but if it does, our tests would fail anyway.
    299     """
    300     return getpass.getuser()
    301
    302
    303  ############################
    304  # randomFilename() function
    305  ############################
    306
    307 -def randomFilename(length, prefix=None, suffix=None):
    308     """
    309     Generates a random filename with the given length.
    310     @param length: Length of filename.
    311     @return Random filename.
    312     """
    313     characters = [None] * length
    314     for i in xrange(length):
    315        characters[i] = random.choice(string.ascii_uppercase)
    316     if prefix is None:
    317        prefix = ""
    318     if suffix is None:
    319        suffix = ""
    320     return "%s%s%s" % (prefix, "".join(characters), suffix)
    321
    322
    323  ####################################
    324  # failUnlessAssignRaises() function
    325  ####################################
    326
    327  # pylint: disable=W0613
    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