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 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