1   
  2   
  3   
  4   
  5   
  6   
  7   
  8   
  9   
 10   
 11   
 12   
 13   
 14   
 15   
 16   
 17   
 18   
 19   
 20   
 21   
 22   
 23   
 24   
 25   
 26   
 27   
 28   
 29   
 30   
 31   
 32   
 33   
 34   
 35   
 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   
 64   
 65   
 66  import sys 
 67  import os 
 68  import tarfile 
 69  import time 
 70  import getpass 
 71  import random 
 72  import string  
 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   
 85   
 86   
 87   
 88   
 89   
 90   
 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)     
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   
112   
113   
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   
139   
140   
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   
163   
164   
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   
181   
182   
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   
199   
200   
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   
227   
228   
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      
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   
250   
251   
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   
278   
279   
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)   
 289   
290   
291   
292   
293   
294   
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   
305   
306   
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   
325   
326   
327   
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     
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   
382   
383   
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   
411   
412   
428   
429   
430   
431   
432   
433   
439   
440   
441   
442   
443   
444   
450   
451   
452   
453   
454   
455   
461   
462   
463   
464   
465   
466   
472   
473   
474   
475   
476   
477   
485   
486   
487   
488   
489   
490   
498   
499   
500   
501   
502   
503   
511   
512   
513   
514   
515   
516   
523   
524   
525   
526   
527   
528   
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   
542   
543   
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   
558   
559   
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