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 """
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
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
83
84
85
86
87
88
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)
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
110
111
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
134
135
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
152
153
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
170
171
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
198
199
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
220
221
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
248
249
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)
259
260
261
262
263
264
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
275
276
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
295
296
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
350
351
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
379
380
392
393
394
395
396
397
403
404
405
406
407
408
414
415
416
417
418
419
427
428
429
430
431
432
440
441
442
443
444
445
453
454
455
456
457
458
465
466
467
468
469
470
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
484
485
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
500
501
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