Package CedarBackup3 :: Module testutil
[hide private]
[frames] | no frames]

Source Code for Module CedarBackup3.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,2015 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 3 (>= 3.4) 
 29  # Project  : Cedar Backup, release 3 
 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 
 55   
 56  @author: Kenneth J. Pronovici <pronovic@ieee.org> 
 57  """ 
 58   
 59   
 60  ######################################################################## 
 61  # Imported modules 
 62  ######################################################################## 
 63   
 64  import sys 
 65  import os 
 66  import tarfile 
 67  import time 
 68  import getpass 
 69  import random 
 70  import string # pylint: disable=W0402 
 71  import platform 
 72  import logging 
 73  from io import StringIO 
 74   
 75  from CedarBackup3.util import encodePath, executeCommand 
 76  from CedarBackup3.config import Config, OptionsConfig 
 77  from CedarBackup3.customize import customizeOverrides 
 78  from CedarBackup3.cli import setupPathResolver 
 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("CedarBackup3") 100 logger.setLevel(logging.DEBUG) # let the logger see all messages 101 formatter = logging.Formatter(fmt="%(message)s") 102 handler = logging.StreamHandler(stream=sys.stdout) 103 handler.setFormatter(formatter) 104 handler.setLevel(logging.DEBUG) 105 logger.addHandler(handler)
106 107 108 ################# 109 # setupOverrides 110 ################# 111
112 -def setupOverrides():
113 """ 114 Set up any platform-specific overrides that might be required. 115 116 When packages are built, this is done manually (hardcoded) in customize.py 117 and the overrides are set up in cli.cli(). This way, no runtime checks need 118 to be done. This is safe, because the package maintainer knows exactly 119 which platform (Debian or not) the package is being built for. 120 121 Unit tests are different, because they might be run anywhere. So, we 122 attempt to make a guess about plaform using platformDebian(), and use that 123 to set up the custom overrides so that platform-specific unit tests continue 124 to work. 125 """ 126 config = Config() 127 config.options = OptionsConfig() 128 if platformDebian(): 129 customizeOverrides(config, platform="debian") 130 else: 131 customizeOverrides(config, platform="standard") 132 setupPathResolver(config)
133 134 135 ########################### 136 # findResources() function 137 ########################### 138
139 -def findResources(resources, dataDirs):
140 """ 141 Returns a dictionary of locations for various resources. 142 @param resources: List of required resources. 143 @param dataDirs: List of data directories to search within for resources. 144 @return: Dictionary mapping resource name to resource path. 145 @raise Exception: If some resource cannot be found. 146 """ 147 mapping = { } 148 for resource in resources: 149 for resourceDir in dataDirs: 150 path = os.path.join(resourceDir, resource) 151 if os.path.exists(path): 152 mapping[resource] = path 153 break 154 else: 155 raise Exception("Unable to find resource [%s]." % resource) 156 return mapping
157 158 159 ############################## 160 # commandAvailable() function 161 ############################## 162
163 -def commandAvailable(command):
164 """ 165 Indicates whether a command is available on $PATH somewhere. 166 This should work on both Windows and UNIX platforms. 167 @param command: Commang to search for 168 @return: Boolean true/false depending on whether command is available. 169 """ 170 if "PATH" in os.environ: 171 for path in os.environ["PATH"].split(os.sep): 172 if os.path.exists(os.path.join(path, command)): 173 return True 174 return False
175 176 177 ####################### 178 # buildPath() function 179 ####################### 180
181 -def buildPath(components):
182 """ 183 Builds a complete path from a list of components. 184 For instance, constructs C{"/a/b/c"} from C{["/a", "b", "c",]}. 185 @param components: List of components. 186 @returns: String path constructed from components. 187 @raise ValueError: If a path cannot be encoded properly. 188 """ 189 path = components[0] 190 for component in components[1:]: 191 path = os.path.join(path, component) 192 return encodePath(path)
193 194 195 ####################### 196 # removedir() function 197 ####################### 198
199 -def removedir(tree):
200 """ 201 Recursively removes an entire directory. 202 This is basically taken from an example on python.com. 203 @param tree: Directory tree to remove. 204 @raise ValueError: If a path cannot be encoded properly. 205 """ 206 tree = encodePath(tree) 207 for root, dirs, files in os.walk(tree, topdown=False): 208 for name in files: 209 path = os.path.join(root, name) 210 if os.path.islink(path): 211 os.remove(path) 212 elif os.path.isfile(path): 213 os.remove(path) 214 for name in dirs: 215 path = os.path.join(root, name) 216 if os.path.islink(path): 217 os.remove(path) 218 elif os.path.isdir(path): 219 os.rmdir(path) 220 os.rmdir(tree)
221 222 223 ######################## 224 # extractTar() function 225 ######################## 226
227 -def extractTar(tmpdir, filepath):
228 """ 229 Extracts the indicated tar file to the indicated tmpdir. 230 @param tmpdir: Temp directory to extract to. 231 @param filepath: Path to tarfile to extract. 232 @raise ValueError: If a path cannot be encoded properly. 233 """ 234 # pylint: disable=E1101 235 tmpdir = encodePath(tmpdir) 236 filepath = encodePath(filepath) 237 with tarfile.open(filepath) as tar: 238 try: 239 tar.format = tarfile.GNU_FORMAT 240 except AttributeError: 241 tar.posix = False 242 for tarinfo in tar: 243 tar.extract(tarinfo, tmpdir)
244 245 246 ########################### 247 # changeFileAge() function 248 ########################### 249
250 -def changeFileAge(filename, subtract=None):
251 """ 252 Changes a file age using the C{os.utime} function. 253 254 @note: Some platforms don't seem to be able to set an age precisely. As a 255 result, whereas we might have intended to set an age of 86400 seconds, we 256 actually get an age of 86399.375 seconds. When util.calculateFileAge() 257 looks at that the file, it calculates an age of 0.999992766204 days, which 258 then gets truncated down to zero whole days. The tests get very confused. 259 To work around this, I always subtract off one additional second as a fudge 260 factor. That way, the file age will be I{at least} as old as requested 261 later on. 262 263 @param filename: File to operate on. 264 @param subtract: Number of seconds to subtract from the current time. 265 @raise ValueError: If a path cannot be encoded properly. 266 """ 267 filename = encodePath(filename) 268 newTime = time.time() - 1 269 if subtract is not None: 270 newTime -= subtract 271 os.utime(filename, (newTime, newTime))
272 273 274 ########################### 275 # getMaskAsMode() function 276 ########################### 277
278 -def getMaskAsMode():
279 """ 280 Returns the user's current umask inverted to a mode. 281 A mode is mostly a bitwise inversion of a mask, i.e. mask 002 is mode 775. 282 @return: Umask converted to a mode, as an integer. 283 """ 284 umask = os.umask(0o777) 285 os.umask(umask) 286 return int(~umask & 0o777) # invert, then use only lower bytes
287 288 289 ###################### 290 # getLogin() function 291 ###################### 292
293 -def getLogin():
294 """ 295 Returns the name of the currently-logged in user. This might fail under 296 some circumstances - but if it does, our tests would fail anyway. 297 """ 298 return getpass.getuser()
299 300 301 ############################ 302 # randomFilename() function 303 ############################ 304
305 -def randomFilename(length, prefix=None, suffix=None):
306 """ 307 Generates a random filename with the given length. 308 @param length: Length of filename. 309 @return Random filename. 310 """ 311 characters = [None] * length 312 for i in range(length): 313 characters[i] = random.choice(string.ascii_uppercase) 314 if prefix is None: 315 prefix = "" 316 if suffix is None: 317 suffix = "" 318 return "%s%s%s" % (prefix, "".join(characters), suffix)
319 320 321 #################################### 322 # failUnlessAssignRaises() function 323 #################################### 324 325 # pylint: disable=W0613
326 -def failUnlessAssignRaises(testCase, exception, obj, prop, value):
327 """ 328 Equivalent of C{failUnlessRaises}, but used for property assignments instead. 329 330 It's nice to be able to use C{failUnlessRaises} to check that a method call 331 raises the exception that you expect. Unfortunately, this method can't be 332 used to check Python propery assignments, even though these property 333 assignments are actually implemented underneath as methods. 334 335 This function (which can be easily called by unit test classes) provides an 336 easy way to wrap the assignment checks. It's not pretty, or as intuitive as 337 the original check it's modeled on, but it does work. 338 339 Let's assume you make this method call:: 340 341 testCase.failUnlessAssignRaises(ValueError, collectDir, "absolutePath", absolutePath) 342 343 If you do this, a test case failure will be raised unless the assignment:: 344 345 collectDir.absolutePath = absolutePath 346 347 fails with a C{ValueError} exception. The failure message differentiates 348 between the case where no exception was raised and the case where the wrong 349 exception was raised. 350 351 @note: Internally, the C{missed} and C{instead} variables are used rather 352 than directly calling C{testCase.fail} upon noticing a problem because the 353 act of "failure" itself generates an exception that would be caught by the 354 general C{except} clause. 355 356 @param testCase: PyUnit test case object (i.e. self). 357 @param exception: Exception that is expected to be raised. 358 @param obj: Object whose property is to be assigned to. 359 @param prop: Name of the property, as a string. 360 @param value: Value that is to be assigned to the property. 361 362 @see: C{unittest.TestCase.failUnlessRaises} 363 """ 364 missed = False 365 instead = None 366 try: 367 exec("obj.%s = value" % prop) # pylint: disable=W0122 368 missed = True 369 except exception: pass 370 except Exception as e: 371 instead = e 372 if missed: 373 testCase.fail("Expected assignment to raise %s, but got no exception." % (exception.__name__)) 374 if instead is not None: 375 testCase.fail("Expected assignment to raise %s, but got %s instead." % (ValueError, instead.__class__.__name__))
376 377 378 ########################### 379 # captureOutput() function 380 ########################### 381
382 -def captureOutput(c):
383 """ 384 Captures the output (stdout, stderr) of a function or a method. 385 386 Some of our functions don't do anything other than just print output. We 387 need a way to test these functions (at least nominally) but we don't want 388 any of the output spoiling the test suite output. 389 390 This function just creates a dummy file descriptor that can be used as a 391 target by the callable function, rather than C{stdout} or C{stderr}. 392 393 @note: This method assumes that C{callable} doesn't take any arguments 394 besides keyword argument C{fd} to specify the file descriptor. 395 396 @param c: Callable function or method. 397 398 @return: Output of function, as one big string. 399 """ 400 fd = StringIO() 401 c(fd=fd) 402 result = fd.getvalue() 403 fd.close() 404 return result
405 406 407 ######################### 408 # _isPlatform() function 409 ######################### 410
411 -def _isPlatform(name):
412 """ 413 Returns boolean indicating whether we're running on the indicated platform. 414 @param name: Platform name to check, currently one of "windows" or "macosx" 415 """ 416 if name == "windows": 417 return platform.platform(True, True).startswith("Windows") 418 elif name == "macosx": 419 return sys.platform == "darwin" 420 elif name == "debian": 421 return platform.platform(False, False).find("debian") > 0 422 elif name == "cygwin": 423 return platform.platform(True, True).startswith("CYGWIN") 424 else: 425 raise ValueError("Unknown platform [%s]." % name)
426 427 428 ############################ 429 # platformDebian() function 430 ############################ 431
432 -def platformDebian():
433 """ 434 Returns boolean indicating whether this is the Debian platform. 435 """ 436 return _isPlatform("debian")
437 438 439 ############################ 440 # platformMacOsX() function 441 ############################ 442
443 -def platformMacOsX():
444 """ 445 Returns boolean indicating whether this is the Mac OS X platform. 446 """ 447 return _isPlatform("macosx")
448 449 450 ########################### 451 # runningAsRoot() function 452 ########################### 453
454 -def runningAsRoot():
455 """ 456 Returns boolean indicating whether the effective user id is root. 457 """ 458 return os.geteuid() == 0
459 460 461 ############################## 462 # availableLocales() function 463 ############################## 464
465 -def availableLocales():
466 """ 467 Returns a list of available locales on the system 468 @return: List of string locale names 469 """ 470 locales = [] 471 output = executeCommand(["locale"], [ "-a", ], returnOutput=True, ignoreStderr=True)[1] 472 for line in output: 473 locales.append(line.rstrip()) 474 return locales
475