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 1006 2010-07-07 21:03:57Z 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-msg=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-msg=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-msg=W0122 370 missed = True 371 except exception: pass 372 except Exception, e: 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 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