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
55
56 @author: Kenneth J. Pronovici <pronovic@ieee.org>
57 """
58
59
60
61
62
63
64 import sys
65 import os
66 import tarfile
67 import time
68 import getpass
69 import random
70 import string
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
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("CedarBackup3")
100 logger.setLevel(logging.DEBUG)
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
110
111
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
137
138
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
161
162
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
179
180
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
197
198
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
225
226
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
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
248
249
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
276
277
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)
287
288
289
290
291
292
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
303
304
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
323
324
325
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)
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
380
381
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
409
410
426
427
428
429
430
431
437
438
439
440
441
442
448
449
450
451
452
453
455 """
456 Returns boolean indicating whether the effective user id is root.
457 """
458 return os.geteuid() == 0
459
460
461
462
463
464
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