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