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 -def failUnlessAssignRaises(testCase, exception, obj, prop, value):
    328     """
    329     Equivalent of C{failUnlessRaises}, but used for property assignments instead.
    330
    331     It's nice to be able to use C{failUnlessRaises} to check that a method call
    332     raises the exception that you expect.  Unfortunately, this method can't be
    333     used to check Python propery assignments, even though these property
    334     assignments are actually implemented underneath as methods.
    335
    336     This function (which can be easily called by unit test classes) provides an
    337     easy way to wrap the assignment checks.  It's not pretty, or as intuitive as
    338     the original check it's modeled on, but it does work.
    339
    340     Let's assume you make this method call::
    341
    342        testCase.failUnlessAssignRaises(ValueError, collectDir, "absolutePath", absolutePath)
    343
    344     If you do this, a test case failure will be raised unless the assignment::
    345
    346        collectDir.absolutePath = absolutePath
    347
    348     fails with a C{ValueError} exception.  The failure message differentiates
    349     between the case where no exception was raised and the case where the wrong
    350     exception was raised.
    351
    352     @note: Internally, the C{missed} and C{instead} variables are used rather
    353     than directly calling C{testCase.fail} upon noticing a problem because the
    354     act of "failure" itself generates an exception that would be caught by the
    355     general C{except} clause.
    356
    357     @param testCase: PyUnit test case object (i.e. self).
    358     @param exception: Exception that is expected to be raised.
    359     @param obj: Object whose property is to be assigned to.
    360     @param prop: Name of the property, as a string.
    361     @param value: Value that is to be assigned to the property.
    362
    363     @see: C{unittest.TestCase.failUnlessRaises}
    364     """
    365     missed = False
    366     instead = None
    367     try:
    368        exec "obj.%s = value" % prop    # pylint: disable=W0122
    369        missed = True
    370     except exception: pass
    371     except Exception, e:
    372        instead = e
    373     if missed:
    374        testCase.fail("Expected assignment to raise %s, but got no exception." % (exception.__name__))
    375     if instead is not None:
    376        testCase.fail("Expected assignment to raise %s, but got %s instead." % (ValueError, instead.__class__.__name__))
    377
    378
    379  ###########################
    380  # captureOutput() function
    381  ###########################
    382
    383 -def captureOutput(c):
    384     """
    385     Captures the output (stdout, stderr) of a function or a method.
    386
    387     Some of our functions don't do anything other than just print output.  We
    388     need a way to test these functions (at least nominally) but we don't want
    389     any of the output spoiling the test suite output.
    390
    391     This function just creates a dummy file descriptor that can be used as a
    392     target by the callable function, rather than C{stdout} or C{stderr}.
    393
    394     @note: This method assumes that C{callable} doesn't take any arguments
    395     besides keyword argument C{fd} to specify the file descriptor.
    396
    397     @param c: Callable function or method.
    398
    399     @return: Output of function, as one big string.
    400     """
    401     fd = StringIO()
    402     c(fd=fd)
    403     result = fd.getvalue()
    404     fd.close()
    405     return result
    406
    407
    408  #########################
    409  # _isPlatform() function
    410  #########################
    411
    412 -def _isPlatform(name):
    413     """
    414     Returns boolean indicating whether we're running on the indicated platform.
    415     @param name: Platform name to check, currently one of "windows" or "macosx"
    416     """
    417     if name == "windows":
    418        return platform.platform(True, True).startswith("Windows")
    419     elif name == "macosx":
    420        return sys.platform == "darwin"
    421     elif name == "debian":
    422        return platform.platform(False, False).find("debian") > 0
    423     elif name == "cygwin":
    424        return platform.platform(True, True).startswith("CYGWIN")
    425     else:
    426        raise ValueError("Unknown platform [%s]." % name)
    427
    428
    429  ############################
    430  # platformDebian() function
    431  ############################
    432
    433 -def platformDebian():
    434     """
    435     Returns boolean indicating whether this is the Debian platform.
    436     """
    437     return _isPlatform("debian")
    438
    439
    440  ############################
    441  # platformMacOsX() function
    442  ############################
    443
    444 -def platformMacOsX():
    445     """
    446     Returns boolean indicating whether this is the Mac OS X platform.
    447     """
    448     return _isPlatform("macosx")
    449
    450
    451  #############################
    452  # platformWindows() function
    453  #############################
    454
    455 -def platformWindows():
    456     """
    457     Returns boolean indicating whether this is the Windows platform.
    458     """
    459     return _isPlatform("windows")
    460
    461
    462  ############################
    463  # platformCygwin() function
    464  ############################
    465
    466 -def platformCygwin():
    467     """
    468     Returns boolean indicating whether this is the Cygwin platform.
    469     """
    470     return _isPlatform("cygwin")
    471
    472
    473  ###################################
    474  # platformSupportsLinks() function
    475  ###################################
    476
    477 -def platformSupportsLinks():
    478     """
    479     Returns boolean indicating whether the platform supports soft-links.
    480     Some platforms, like Windows, do not support links, and tests need to take
    481     this into account.
    482     """
    483     return not platformWindows()
    484
    485
    486  #########################################
    487  # platformSupportsPermissions() function
    488  #########################################
    489
    490 -def platformSupportsPermissions():
    491     """
    492     Returns boolean indicating whether the platform supports UNIX-style file permissions.
    493     Some platforms, like Windows, do not support permissions, and tests need to take
    494     this into account.
    495     """
    496     return not platformWindows()
    497
    498
    499  ########################################
    500  # platformRequiresBinaryRead() function
    501  ########################################
    502
    503 -def platformRequiresBinaryRead():
    504     """
    505     Returns boolean indicating whether the platform requires binary reads.
    506     Some platforms, like Windows, require a special flag to read binary data
    507     from files.
    508     """
    509     return platformWindows()
    510
    511
    512  #############################
    513  # platformHasEcho() function
    514  #############################
    515
    516 -def platformHasEcho():
    517     """
    518     Returns boolean indicating whether the platform has a sensible echo command.
    519     On some platforms, like Windows, echo doesn't really work for tests.
    520     """
    521     return not platformWindows()
    522
    523
    524  ###########################
    525  # runningAsRoot() function
    526  ###########################
    527
    528 -def runningAsRoot():
    529     """
    530     Returns boolean indicating whether the effective user id is root.
    531     This is always true on platforms that have no concept of root, like Windows.
    532     """
    533     if platformWindows():
    534        return True
    535     else:
    536        return os.geteuid() == 0
    537
    538
    539  ##############################
    540  # availableLocales() function
    541  ##############################
    542
    543 -def availableLocales():
    544     """
    545     Returns a list of available locales on the system
    546     @return: List of string locale names
    547     """
    548     locales = []
    549     output = executeCommand(["locale"], [ "-a", ], returnOutput=True, ignoreStderr=True)[1]
    550     for line in output:
    551        locales.append(line.rstrip())
    552     return locales
    553
    554
    555  ####################################
    556  # hexFloatLiteralAllowed() function
    557  ####################################
    558
    559 -def hexFloatLiteralAllowed():
    560     """
    561     Indicates whether hex float literals are allowed by the interpreter.
    562
    563     As far back as 2004, some Python documentation indicated that octal and hex
    564     notation applied only to integer literals.  However, prior to Python 2.5, it
    565     was legal to construct a float with an argument like 0xAC on some platforms.
    566     This check provides a an indication of whether the current interpreter
    567     supports that behavior.
    568
    569     This check exists so that unit tests can continue to test the same thing as
    570     always for pre-2.5 interpreters (i.e. making sure backwards compatibility
    571     doesn't break) while still continuing to work for later interpreters.
    572
    573     The returned value is True if hex float literals are allowed, False otherwise.
    574     """
    575     if map(int, [sys.version_info[0], sys.version_info[1]]) < [2, 5] and not platformWindows():
    576        return True
    577     return False
    578