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 platformDebian, platformMacOsX, platformCygwin, platformWindows,
56 platformHasEcho, 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 from CedarBackup2.config import Config, OptionsConfig
80 from CedarBackup2.customize import customizeOverrides
81 from CedarBackup2.cli import setupPathResolver
82
83
84
85
86
87
88
89
90
91
93 """
94 Sets up a screen logger for debugging purposes.
95
96 Normally, the CLI functionality configures the logger so that
97 things get written to the right place. However, for debugging
98 it's sometimes nice to just get everything -- debug information
99 and output -- dumped to the screen. This function takes care
100 of that.
101 """
102 logger = logging.getLogger("CedarBackup2")
103 logger.setLevel(logging.DEBUG)
104 formatter = logging.Formatter(fmt="%(message)s")
105 handler = logging.StreamHandler(strm=sys.stdout)
106 handler.setFormatter(formatter)
107 handler.setLevel(logging.DEBUG)
108 logger.addHandler(handler)
109
110
111
112
113
114
116 """
117 Set up any platform-specific overrides that might be required.
118
119 When packages are built, this is done manually (hardcoded) in customize.py
120 and the overrides are set up in cli.cli(). This way, no runtime checks need
121 to be done. This is safe, because the package maintainer knows exactly
122 which platform (Debian or not) the package is being built for.
123
124 Unit tests are different, because they might be run anywhere. So, we
125 attempt to make a guess about plaform using platformDebian(), and use that
126 to set up the custom overrides so that platform-specific unit tests continue
127 to work.
128 """
129 config = Config()
130 config.options = OptionsConfig()
131 if platformDebian():
132 customizeOverrides(config, platform="debian")
133 else:
134 customizeOverrides(config, platform="standard")
135 setupPathResolver(config)
136
137
138
139
140
141
143 """
144 Returns a dictionary of locations for various resources.
145 @param resources: List of required resources.
146 @param dataDirs: List of data directories to search within for resources.
147 @return: Dictionary mapping resource name to resource path.
148 @raise Exception: If some resource cannot be found.
149 """
150 mapping = { }
151 for resource in resources:
152 for resourceDir in dataDirs:
153 path = os.path.join(resourceDir, resource)
154 if os.path.exists(path):
155 mapping[resource] = path
156 break
157 else:
158 raise Exception("Unable to find resource [%s]." % resource)
159 return mapping
160
161
162
163
164
165
167 """
168 Indicates whether a command is available on $PATH somewhere.
169 This should work on both Windows and UNIX platforms.
170 @param command: Commang to search for
171 @return: Boolean true/false depending on whether command is available.
172 """
173 if os.environ.has_key("PATH"):
174 for path in os.environ["PATH"].split(os.sep):
175 if os.path.exists(os.path.join(path, command)):
176 return True
177 return False
178
179
180
181
182
183
185 """
186 Builds a complete path from a list of components.
187 For instance, constructs C{"/a/b/c"} from C{["/a", "b", "c",]}.
188 @param components: List of components.
189 @returns: String path constructed from components.
190 @raise ValueError: If a path cannot be encoded properly.
191 """
192 path = components[0]
193 for component in components[1:]:
194 path = os.path.join(path, component)
195 return encodePath(path)
196
197
198
199
200
201
203 """
204 Recursively removes an entire directory.
205 This is basically taken from an example on python.com.
206 @param tree: Directory tree to remove.
207 @raise ValueError: If a path cannot be encoded properly.
208 """
209 tree = encodePath(tree)
210 for root, dirs, files in os.walk(tree, topdown=False):
211 for name in files:
212 path = os.path.join(root, name)
213 if os.path.islink(path):
214 os.remove(path)
215 elif os.path.isfile(path):
216 os.remove(path)
217 for name in dirs:
218 path = os.path.join(root, name)
219 if os.path.islink(path):
220 os.remove(path)
221 elif os.path.isdir(path):
222 os.rmdir(path)
223 os.rmdir(tree)
224
225
226
227
228
229
231 """
232 Extracts the indicated tar file to the indicated tmpdir.
233 @param tmpdir: Temp directory to extract to.
234 @param filepath: Path to tarfile to extract.
235 @raise ValueError: If a path cannot be encoded properly.
236 """
237
238 tmpdir = encodePath(tmpdir)
239 filepath = encodePath(filepath)
240 tar = tarfile.open(filepath)
241 try:
242 tar.format = tarfile.GNU_FORMAT
243 except AttributeError:
244 tar.posix = False
245 for tarinfo in tar:
246 tar.extract(tarinfo, tmpdir)
247
248
249
250
251
252
254 """
255 Changes a file age using the C{os.utime} function.
256
257 @note: Some platforms don't seem to be able to set an age precisely. As a
258 result, whereas we might have intended to set an age of 86400 seconds, we
259 actually get an age of 86399.375 seconds. When util.calculateFileAge()
260 looks at that the file, it calculates an age of 0.999992766204 days, which
261 then gets truncated down to zero whole days. The tests get very confused.
262 To work around this, I always subtract off one additional second as a fudge
263 factor. That way, the file age will be I{at least} as old as requested
264 later on.
265
266 @param filename: File to operate on.
267 @param subtract: Number of seconds to subtract from the current time.
268 @raise ValueError: If a path cannot be encoded properly.
269 """
270 filename = encodePath(filename)
271 newTime = time.time() - 1
272 if subtract is not None:
273 newTime -= subtract
274 os.utime(filename, (newTime, newTime))
275
276
277
278
279
280
282 """
283 Returns the user's current umask inverted to a mode.
284 A mode is mostly a bitwise inversion of a mask, i.e. mask 002 is mode 775.
285 @return: Umask converted to a mode, as an integer.
286 """
287 umask = os.umask(0777)
288 os.umask(umask)
289 return int(~umask & 0777)
290
291
292
293
294
295
297 """
298 Returns the name of the currently-logged in user. This might fail under
299 some circumstances - but if it does, our tests would fail anyway.
300 """
301 return getpass.getuser()
302
303
304
305
306
307
309 """
310 Generates a random filename with the given length.
311 @param length: Length of filename.
312 @return Random filename.
313 """
314 characters = [None] * length
315 for i in xrange(length):
316 characters[i] = random.choice(string.ascii_uppercase)
317 if prefix is None:
318 prefix = ""
319 if suffix is None:
320 suffix = ""
321 return "%s%s%s" % (prefix, "".join(characters), suffix)
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