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