]>
Commit | Line | Data |
---|---|---|
7c673cae FG |
1 | # Copyright 2002-2005 Vladimir Prus. |
2 | # Copyright 2002-2003 Dave Abrahams. | |
3 | # Copyright 2006 Rene Rivera. | |
4 | # Distributed under the Boost Software License, Version 1.0. | |
5 | # (See accompanying file LICENSE_1_0.txt or copy at | |
6 | # http://www.boost.org/LICENSE_1_0.txt) | |
7 | ||
8 | import TestCmd | |
9 | ||
10 | import copy | |
11 | import fnmatch | |
12 | import glob | |
13 | import math | |
14 | import os | |
15 | import os.path | |
16 | import re | |
17 | import shutil | |
18 | import StringIO | |
19 | import subprocess | |
20 | import sys | |
21 | import tempfile | |
22 | import time | |
23 | import traceback | |
24 | import tree | |
25 | import types | |
26 | ||
27 | from xml.sax.saxutils import escape | |
28 | ||
29 | ||
30 | class TestEnvironmentError(Exception): | |
31 | pass | |
32 | ||
33 | ||
34 | annotations = [] | |
35 | ||
36 | ||
37 | def print_annotation(name, value, xml): | |
38 | """Writes some named bits of information about the current test run.""" | |
39 | if xml: | |
40 | print escape(name) + " {{{" | |
41 | print escape(value) | |
42 | print "}}}" | |
43 | else: | |
44 | print name + " {{{" | |
45 | print value | |
46 | print "}}}" | |
47 | ||
48 | ||
49 | def flush_annotations(xml=0): | |
50 | global annotations | |
51 | for ann in annotations: | |
52 | print_annotation(ann[0], ann[1], xml) | |
53 | annotations = [] | |
54 | ||
55 | ||
56 | def clear_annotations(): | |
57 | global annotations | |
58 | annotations = [] | |
59 | ||
60 | ||
61 | defer_annotations = 0 | |
62 | ||
63 | def set_defer_annotations(n): | |
64 | global defer_annotations | |
65 | defer_annotations = n | |
66 | ||
67 | ||
68 | def annotate_stack_trace(tb=None): | |
69 | if tb: | |
70 | trace = TestCmd.caller(traceback.extract_tb(tb), 0) | |
71 | else: | |
72 | trace = TestCmd.caller(traceback.extract_stack(), 1) | |
73 | annotation("stacktrace", trace) | |
74 | ||
75 | ||
76 | def annotation(name, value): | |
77 | """Records an annotation about the test run.""" | |
78 | annotations.append((name, value)) | |
79 | if not defer_annotations: | |
80 | flush_annotations() | |
81 | ||
82 | ||
83 | def get_toolset(): | |
84 | toolset = None | |
85 | for arg in sys.argv[1:]: | |
86 | if not arg.startswith("-"): | |
87 | toolset = arg | |
88 | return toolset or "gcc" | |
89 | ||
90 | ||
91 | # Detect the host OS. | |
92 | cygwin = hasattr(os, "uname") and os.uname()[0].lower().startswith("cygwin") | |
93 | windows = cygwin or os.environ.get("OS", "").lower().startswith("windows") | |
94 | ||
95 | ||
96 | def prepare_prefixes_and_suffixes(toolset): | |
97 | prepare_suffix_map(toolset) | |
98 | prepare_library_prefix(toolset) | |
99 | ||
100 | ||
101 | def prepare_suffix_map(toolset): | |
102 | """ | |
103 | Set up suffix translation performed by the Boost Build testing framework | |
104 | to accomodate different toolsets generating targets of the same type using | |
105 | different filename extensions (suffixes). | |
106 | ||
107 | """ | |
108 | global suffixes | |
109 | suffixes = {} | |
110 | if windows: | |
111 | if toolset == "gcc": | |
112 | suffixes[".lib"] = ".a" # mingw static libs use suffix ".a". | |
113 | suffixes[".obj"] = ".o" | |
114 | if cygwin: | |
115 | suffixes[".implib"] = ".lib.a" | |
116 | else: | |
117 | suffixes[".implib"] = ".lib" | |
118 | else: | |
119 | suffixes[".exe"] = "" | |
120 | suffixes[".dll"] = ".so" | |
121 | suffixes[".lib"] = ".a" | |
122 | suffixes[".obj"] = ".o" | |
123 | suffixes[".implib"] = ".no_implib_files_on_this_platform" | |
124 | ||
125 | if hasattr(os, "uname") and os.uname()[0] == "Darwin": | |
126 | suffixes[".dll"] = ".dylib" | |
127 | ||
128 | ||
129 | def prepare_library_prefix(toolset): | |
130 | """ | |
131 | Setup whether Boost Build is expected to automatically prepend prefixes | |
132 | to its built library targets. | |
133 | ||
134 | """ | |
135 | global lib_prefix | |
136 | lib_prefix = "lib" | |
137 | ||
138 | global dll_prefix | |
139 | if cygwin: | |
140 | dll_prefix = "cyg" | |
141 | elif windows and toolset != "gcc": | |
142 | dll_prefix = None | |
143 | else: | |
144 | dll_prefix = "lib" | |
145 | ||
146 | ||
147 | def re_remove(sequence, regex): | |
148 | me = re.compile(regex) | |
149 | result = filter(lambda x: me.match(x), sequence) | |
150 | if not result: | |
151 | raise ValueError() | |
152 | for r in result: | |
153 | sequence.remove(r) | |
154 | ||
155 | ||
156 | def glob_remove(sequence, pattern): | |
157 | result = fnmatch.filter(sequence, pattern) | |
158 | if not result: | |
159 | raise ValueError() | |
160 | for r in result: | |
161 | sequence.remove(r) | |
162 | ||
163 | ||
164 | class Tester(TestCmd.TestCmd): | |
165 | """Main tester class for Boost Build. | |
166 | ||
167 | Optional arguments: | |
168 | ||
169 | `arguments` - Arguments passed to the run executable. | |
170 | `executable` - Name of the executable to invoke. | |
171 | `match` - Function to use for compating actual and | |
172 | expected file contents. | |
173 | `boost_build_path` - Boost build path to be passed to the run | |
174 | executable. | |
175 | `translate_suffixes` - Whether to update suffixes on the the file | |
176 | names passed from the test script so they | |
177 | match those actually created by the current | |
178 | toolset. For example, static library files | |
179 | are specified by using the .lib suffix but | |
180 | when the "gcc" toolset is used it actually | |
181 | creates them using the .a suffix. | |
182 | `pass_toolset` - Whether the test system should pass the | |
183 | specified toolset to the run executable. | |
184 | `use_test_config` - Whether the test system should tell the run | |
185 | executable to read in the test_config.jam | |
186 | configuration file. | |
187 | `ignore_toolset_requirements` - Whether the test system should tell the run | |
188 | executable to ignore toolset requirements. | |
189 | `workdir` - Absolute directory where the test will be | |
190 | run from. | |
191 | `pass_d0` - If set, when tests are not explicitly run | |
192 | in verbose mode, they are run as silent | |
193 | (-d0 & --quiet Boost Jam options). | |
194 | ||
195 | Optional arguments inherited from the base class: | |
196 | ||
197 | `description` - Test description string displayed in case | |
198 | of a failed test. | |
199 | `subdir` - List of subdirectories to automatically | |
200 | create under the working directory. Each | |
201 | subdirectory needs to be specified | |
202 | separately, parent coming before its child. | |
203 | `verbose` - Flag that may be used to enable more | |
204 | verbose test system output. Note that it | |
205 | does not also enable more verbose build | |
206 | system output like the --verbose command | |
207 | line option does. | |
208 | """ | |
209 | def __init__(self, arguments=None, executable="bjam", | |
210 | match=TestCmd.match_exact, boost_build_path=None, | |
211 | translate_suffixes=True, pass_toolset=True, use_test_config=True, | |
b32b8144 | 212 | ignore_toolset_requirements=False, workdir="", pass_d0=True, |
7c673cae FG |
213 | **keywords): |
214 | ||
215 | assert arguments.__class__ is not str | |
216 | self.original_workdir = os.getcwd() | |
217 | if workdir and not os.path.isabs(workdir): | |
218 | raise ("Parameter workdir <%s> must point to an absolute " | |
219 | "directory: " % workdir) | |
220 | ||
221 | self.last_build_timestamp = 0 | |
222 | self.translate_suffixes = translate_suffixes | |
223 | self.use_test_config = use_test_config | |
224 | ||
225 | self.toolset = get_toolset() | |
226 | self.pass_toolset = pass_toolset | |
227 | self.ignore_toolset_requirements = ignore_toolset_requirements | |
228 | ||
229 | prepare_prefixes_and_suffixes(pass_toolset and self.toolset or "gcc") | |
230 | ||
231 | use_default_bjam = "--default-bjam" in sys.argv | |
232 | ||
233 | if not use_default_bjam: | |
234 | jam_build_dir = "" | |
235 | if os.name == "nt": | |
236 | jam_build_dir = "bin.ntx86" | |
237 | elif (os.name == "posix") and os.__dict__.has_key("uname"): | |
238 | if os.uname()[0].lower().startswith("cygwin"): | |
239 | jam_build_dir = "bin.cygwinx86" | |
240 | if ("TMP" in os.environ and | |
241 | os.environ["TMP"].find("~") != -1): | |
242 | print("Setting $TMP to /tmp to get around problem " | |
243 | "with short path names") | |
244 | os.environ["TMP"] = "/tmp" | |
245 | elif os.uname()[0] == "Linux": | |
246 | cpu = os.uname()[4] | |
247 | if re.match("i.86", cpu): | |
248 | jam_build_dir = "bin.linuxx86" | |
249 | else: | |
250 | jam_build_dir = "bin.linux" + os.uname()[4] | |
251 | elif os.uname()[0] == "SunOS": | |
252 | jam_build_dir = "bin.solaris" | |
253 | elif os.uname()[0] == "Darwin": | |
254 | if os.uname()[4] == "i386": | |
255 | jam_build_dir = "bin.macosxx86" | |
256 | elif os.uname()[4] == "x86_64": | |
257 | jam_build_dir = "bin.macosxx86_64" | |
258 | else: | |
259 | jam_build_dir = "bin.macosxppc" | |
260 | elif os.uname()[0] == "AIX": | |
261 | jam_build_dir = "bin.aix" | |
262 | elif os.uname()[0] == "IRIX64": | |
263 | jam_build_dir = "bin.irix" | |
264 | elif os.uname()[0] == "FreeBSD": | |
265 | jam_build_dir = "bin.freebsd" | |
266 | elif os.uname()[0] == "OSF1": | |
267 | jam_build_dir = "bin.osf" | |
268 | else: | |
269 | raise ("Do not know directory where Jam is built for this " | |
270 | "system: %s/%s" % (os.name, os.uname()[0])) | |
271 | else: | |
272 | raise ("Do not know directory where Jam is built for this " | |
273 | "system: %s" % os.name) | |
274 | ||
275 | # Find where jam_src is located. Try for the debug version if it is | |
276 | # lying around. | |
277 | dirs = [os.path.join("..", "src", "engine", jam_build_dir + ".debug"), | |
278 | os.path.join("..", "src", "engine", jam_build_dir)] | |
279 | for d in dirs: | |
280 | if os.path.exists(d): | |
281 | jam_build_dir = d | |
282 | break | |
283 | else: | |
284 | print("Cannot find built Boost.Jam") | |
285 | sys.exit(1) | |
286 | ||
287 | verbosity = ["-d0", "--quiet"] | |
288 | if not pass_d0: | |
289 | verbosity = [] | |
290 | if "--verbose" in sys.argv: | |
291 | keywords["verbose"] = True | |
292 | verbosity = ["-d+2"] | |
293 | ||
294 | if boost_build_path is None: | |
295 | boost_build_path = self.original_workdir + "/.." | |
296 | ||
297 | program_list = [] | |
298 | if use_default_bjam: | |
299 | program_list.append(executable) | |
300 | else: | |
301 | program_list.append(os.path.join(jam_build_dir, executable)) | |
302 | program_list.append('-sBOOST_BUILD_PATH="' + boost_build_path + '"') | |
303 | if verbosity: | |
304 | program_list += verbosity | |
305 | if arguments: | |
306 | program_list += arguments | |
307 | ||
308 | TestCmd.TestCmd.__init__(self, program=program_list, match=match, | |
309 | workdir=workdir, inpath=use_default_bjam, **keywords) | |
310 | ||
311 | os.chdir(self.workdir) | |
312 | ||
313 | def cleanup(self): | |
314 | try: | |
315 | TestCmd.TestCmd.cleanup(self) | |
316 | os.chdir(self.original_workdir) | |
317 | except AttributeError: | |
318 | # When this is called during TestCmd.TestCmd.__del__ we can have | |
319 | # both 'TestCmd' and 'os' unavailable in our scope. Do nothing in | |
320 | # this case. | |
321 | pass | |
322 | ||
323 | # | |
324 | # Methods that change the working directory's content. | |
325 | # | |
326 | def set_tree(self, tree_location): | |
327 | # It is not possible to remove the current directory. | |
328 | d = os.getcwd() | |
329 | os.chdir(os.path.dirname(self.workdir)) | |
330 | shutil.rmtree(self.workdir, ignore_errors=False) | |
331 | ||
332 | if not os.path.isabs(tree_location): | |
333 | tree_location = os.path.join(self.original_workdir, tree_location) | |
334 | shutil.copytree(tree_location, self.workdir) | |
335 | ||
336 | os.chdir(d) | |
337 | def make_writable(unused, dir, entries): | |
338 | for e in entries: | |
339 | name = os.path.join(dir, e) | |
340 | os.chmod(name, os.stat(name).st_mode | 0222) | |
341 | os.path.walk(".", make_writable, None) | |
342 | ||
343 | def write(self, file, content, wait=True): | |
344 | nfile = self.native_file_name(file) | |
345 | self.__makedirs(os.path.dirname(nfile), wait) | |
346 | f = open(nfile, "wb") | |
347 | try: | |
348 | f.write(content) | |
349 | finally: | |
350 | f.close() | |
351 | self.__ensure_newer_than_last_build(nfile) | |
352 | ||
353 | def copy(self, src, dst): | |
354 | try: | |
b32b8144 | 355 | self.write(dst, self.read(src, binary=True)) |
7c673cae FG |
356 | except: |
357 | self.fail_test(1) | |
358 | ||
359 | def copy_preserving_timestamp(self, src, dst): | |
360 | src_name = self.native_file_name(src) | |
361 | dst_name = self.native_file_name(dst) | |
362 | stats = os.stat(src_name) | |
b32b8144 | 363 | self.write(dst, self.__read(src, binary=True)) |
7c673cae FG |
364 | os.utime(dst_name, (stats.st_atime, stats.st_mtime)) |
365 | ||
366 | def touch(self, names, wait=True): | |
367 | if names.__class__ is str: | |
368 | names = [names] | |
369 | for name in names: | |
370 | path = self.native_file_name(name) | |
371 | if wait: | |
372 | self.__ensure_newer_than_last_build(path) | |
373 | else: | |
374 | os.utime(path, None) | |
375 | ||
376 | def rm(self, names): | |
377 | if not type(names) == types.ListType: | |
378 | names = [names] | |
379 | ||
380 | if names == ["."]: | |
381 | # If we are deleting the entire workspace, there is no need to wait | |
382 | # for a clock tick. | |
383 | self.last_build_timestamp = 0 | |
384 | ||
385 | # Avoid attempts to remove the current directory. | |
386 | os.chdir(self.original_workdir) | |
387 | for name in names: | |
388 | n = glob.glob(self.native_file_name(name)) | |
389 | if n: n = n[0] | |
390 | if not n: | |
391 | n = self.glob_file(name.replace("$toolset", self.toolset + "*") | |
392 | ) | |
393 | if n: | |
394 | if os.path.isdir(n): | |
395 | shutil.rmtree(n, ignore_errors=False) | |
396 | else: | |
397 | os.unlink(n) | |
398 | ||
399 | # Create working dir root again in case we removed it. | |
400 | if not os.path.exists(self.workdir): | |
401 | os.mkdir(self.workdir) | |
402 | os.chdir(self.workdir) | |
403 | ||
404 | def expand_toolset(self, name): | |
405 | """ | |
406 | Expands $toolset placeholder in the given file to the name of the | |
407 | toolset currently being tested. | |
408 | ||
409 | """ | |
410 | self.write(name, self.read(name).replace("$toolset", self.toolset)) | |
411 | ||
412 | def dump_stdio(self): | |
413 | annotation("STDOUT", self.stdout()) | |
414 | annotation("STDERR", self.stderr()) | |
415 | ||
416 | def run_build_system(self, extra_args=None, subdir="", stdout=None, | |
417 | stderr="", status=0, match=None, pass_toolset=None, | |
418 | use_test_config=None, ignore_toolset_requirements=None, | |
419 | expected_duration=None, **kw): | |
420 | ||
421 | assert extra_args.__class__ is not str | |
422 | ||
423 | if os.path.isabs(subdir): | |
424 | print("You must pass a relative directory to subdir <%s>." % subdir | |
425 | ) | |
426 | return | |
427 | ||
428 | self.previous_tree, dummy = tree.build_tree(self.workdir) | |
429 | ||
430 | if match is None: | |
431 | match = self.match | |
432 | ||
433 | if pass_toolset is None: | |
434 | pass_toolset = self.pass_toolset | |
435 | ||
436 | if use_test_config is None: | |
437 | use_test_config = self.use_test_config | |
438 | ||
439 | if ignore_toolset_requirements is None: | |
440 | ignore_toolset_requirements = self.ignore_toolset_requirements | |
441 | ||
442 | try: | |
443 | kw["program"] = [] | |
444 | kw["program"] += self.program | |
445 | if extra_args: | |
446 | kw["program"] += extra_args | |
447 | if pass_toolset: | |
448 | kw["program"].append("toolset=" + self.toolset) | |
449 | if use_test_config: | |
450 | kw["program"].append('--test-config="%s"' % os.path.join( | |
451 | self.original_workdir, "test-config.jam")) | |
452 | if ignore_toolset_requirements: | |
453 | kw["program"].append("--ignore-toolset-requirements") | |
454 | if "--python" in sys.argv: | |
455 | # -z disables Python optimization mode. | |
456 | # this enables type checking (all assert | |
457 | # and if __debug__ statements). | |
458 | kw["program"].extend(["--python", "-z"]) | |
459 | if "--stacktrace" in sys.argv: | |
460 | kw["program"].append("--stacktrace") | |
461 | kw["chdir"] = subdir | |
462 | self.last_program_invocation = kw["program"] | |
463 | build_time_start = time.time() | |
464 | apply(TestCmd.TestCmd.run, [self], kw) | |
465 | build_time_finish = time.time() | |
466 | except: | |
467 | self.dump_stdio() | |
468 | raise | |
469 | ||
470 | old_last_build_timestamp = self.last_build_timestamp | |
471 | self.tree, self.last_build_timestamp = tree.build_tree(self.workdir) | |
472 | self.difference = tree.tree_difference(self.previous_tree, self.tree) | |
473 | if self.difference.empty(): | |
474 | # If nothing has been changed by this build and sufficient time has | |
475 | # passed since the last build that actually changed something, | |
476 | # there is no need to wait for touched or newly created files to | |
477 | # start getting newer timestamps than the currently existing ones. | |
478 | self.last_build_timestamp = old_last_build_timestamp | |
479 | ||
480 | self.difference.ignore_directories() | |
481 | self.unexpected_difference = copy.deepcopy(self.difference) | |
482 | ||
483 | if (status and self.status) is not None and self.status != status: | |
484 | expect = "" | |
485 | if status != 0: | |
486 | expect = " (expected %d)" % status | |
487 | ||
488 | annotation("failure", '"%s" returned %d%s' % (kw["program"], | |
489 | self.status, expect)) | |
490 | ||
491 | annotation("reason", "unexpected status returned by bjam") | |
492 | self.fail_test(1) | |
493 | ||
494 | if stdout is not None and not match(self.stdout(), stdout): | |
b32b8144 | 495 | stdout_test = match(self.stdout(), stdout) |
7c673cae FG |
496 | annotation("failure", "Unexpected stdout") |
497 | annotation("Expected STDOUT", stdout) | |
498 | annotation("Actual STDOUT", self.stdout()) | |
499 | stderr = self.stderr() | |
500 | if stderr: | |
501 | annotation("STDERR", stderr) | |
b32b8144 | 502 | self.maybe_do_diff(self.stdout(), stdout, stdout_test) |
7c673cae FG |
503 | self.fail_test(1, dump_stdio=False) |
504 | ||
505 | # Intel tends to produce some messages to stderr which make tests fail. | |
506 | intel_workaround = re.compile("^xi(link|lib): executing.*\n", re.M) | |
507 | actual_stderr = re.sub(intel_workaround, "", self.stderr()) | |
508 | ||
509 | if stderr is not None and not match(actual_stderr, stderr): | |
b32b8144 | 510 | stderr_test = match(actual_stderr, stderr) |
7c673cae FG |
511 | annotation("failure", "Unexpected stderr") |
512 | annotation("Expected STDERR", stderr) | |
513 | annotation("Actual STDERR", self.stderr()) | |
514 | annotation("STDOUT", self.stdout()) | |
b32b8144 | 515 | self.maybe_do_diff(actual_stderr, stderr, stderr_test) |
7c673cae FG |
516 | self.fail_test(1, dump_stdio=False) |
517 | ||
518 | if expected_duration is not None: | |
519 | actual_duration = build_time_finish - build_time_start | |
520 | if actual_duration > expected_duration: | |
521 | print("Test run lasted %f seconds while it was expected to " | |
522 | "finish in under %f seconds." % (actual_duration, | |
523 | expected_duration)) | |
524 | self.fail_test(1, dump_stdio=False) | |
525 | ||
b32b8144 FG |
526 | self.__ignore_junk() |
527 | ||
7c673cae | 528 | def glob_file(self, name): |
b32b8144 | 529 | name = self.adjust_name(name) |
7c673cae FG |
530 | result = None |
531 | if hasattr(self, "difference"): | |
532 | for f in (self.difference.added_files + | |
533 | self.difference.modified_files + | |
534 | self.difference.touched_files): | |
535 | if fnmatch.fnmatch(f, name): | |
b32b8144 | 536 | result = self.__native_file_name(f) |
7c673cae FG |
537 | break |
538 | if not result: | |
b32b8144 | 539 | result = glob.glob(self.__native_file_name(name)) |
7c673cae FG |
540 | if result: |
541 | result = result[0] | |
542 | return result | |
543 | ||
b32b8144 | 544 | def __read(self, name, binary=False): |
7c673cae | 545 | try: |
7c673cae FG |
546 | openMode = "r" |
547 | if binary: | |
548 | openMode += "b" | |
549 | else: | |
550 | openMode += "U" | |
551 | f = open(name, openMode) | |
552 | result = f.read() | |
553 | f.close() | |
554 | return result | |
555 | except: | |
556 | annotation("failure", "Could not open '%s'" % name) | |
557 | self.fail_test(1) | |
558 | return "" | |
559 | ||
b32b8144 FG |
560 | def read(self, name, binary=False): |
561 | name = self.glob_file(name) | |
562 | return self.__read(name, binary=binary) | |
563 | ||
7c673cae FG |
564 | def read_and_strip(self, name): |
565 | if not self.glob_file(name): | |
566 | return "" | |
567 | f = open(self.glob_file(name), "rb") | |
568 | lines = f.readlines() | |
569 | f.close() | |
570 | result = "\n".join(x.rstrip() for x in lines) | |
571 | if lines and lines[-1][-1] != "\n": | |
572 | return result + "\n" | |
573 | return result | |
574 | ||
575 | def fail_test(self, condition, dump_difference=True, dump_stdio=True, | |
576 | dump_stack=True): | |
577 | if not condition: | |
578 | return | |
579 | ||
580 | if dump_difference and hasattr(self, "difference"): | |
581 | f = StringIO.StringIO() | |
582 | self.difference.pprint(f) | |
583 | annotation("changes caused by the last build command", | |
584 | f.getvalue()) | |
585 | ||
586 | if dump_stdio: | |
587 | self.dump_stdio() | |
588 | ||
589 | if "--preserve" in sys.argv: | |
590 | ||
591 | print "*** Copying the state of working dir into 'failed_test' ***" | |
592 | ||
593 | path = os.path.join(self.original_workdir, "failed_test") | |
594 | if os.path.isdir(path): | |
595 | shutil.rmtree(path, ignore_errors=False) | |
596 | elif os.path.exists(path): | |
597 | raise "Path " + path + " already exists and is not a directory" | |
598 | shutil.copytree(self.workdir, path) | |
599 | print "The failed command was:" | |
600 | print " ".join(self.last_program_invocation) | |
601 | ||
602 | if dump_stack: | |
603 | annotate_stack_trace() | |
604 | sys.exit(1) | |
605 | ||
606 | # A number of methods below check expectations with actual difference | |
607 | # between directory trees before and after a build. All the 'expect*' | |
608 | # methods require exact names to be passed. All the 'ignore*' methods allow | |
609 | # wildcards. | |
610 | ||
611 | # All names can be either a string or a list of strings. | |
612 | def expect_addition(self, names): | |
613 | for name in self.adjust_names(names): | |
614 | try: | |
615 | glob_remove(self.unexpected_difference.added_files, name) | |
616 | except: | |
617 | annotation("failure", "File %s not added as expected" % name) | |
618 | self.fail_test(1) | |
619 | ||
620 | def ignore_addition(self, wildcard): | |
621 | self.__ignore_elements(self.unexpected_difference.added_files, | |
622 | wildcard) | |
623 | ||
624 | def expect_removal(self, names): | |
625 | for name in self.adjust_names(names): | |
626 | try: | |
627 | glob_remove(self.unexpected_difference.removed_files, name) | |
628 | except: | |
629 | annotation("failure", "File %s not removed as expected" % name) | |
630 | self.fail_test(1) | |
631 | ||
632 | def ignore_removal(self, wildcard): | |
633 | self.__ignore_elements(self.unexpected_difference.removed_files, | |
634 | wildcard) | |
635 | ||
636 | def expect_modification(self, names): | |
637 | for name in self.adjust_names(names): | |
638 | try: | |
639 | glob_remove(self.unexpected_difference.modified_files, name) | |
640 | except: | |
641 | annotation("failure", "File %s not modified as expected" % | |
642 | name) | |
643 | self.fail_test(1) | |
644 | ||
645 | def ignore_modification(self, wildcard): | |
646 | self.__ignore_elements(self.unexpected_difference.modified_files, | |
647 | wildcard) | |
648 | ||
649 | def expect_touch(self, names): | |
650 | d = self.unexpected_difference | |
651 | for name in self.adjust_names(names): | |
652 | # We need to check both touched and modified files. The reason is | |
653 | # that: | |
654 | # (1) Windows binaries such as obj, exe or dll files have slight | |
655 | # differences even with identical inputs due to Windows PE | |
656 | # format headers containing an internal timestamp. | |
657 | # (2) Intel's compiler for Linux has the same behaviour. | |
658 | filesets = [d.modified_files, d.touched_files] | |
659 | ||
660 | while filesets: | |
661 | try: | |
662 | glob_remove(filesets[-1], name) | |
663 | break | |
664 | except ValueError: | |
665 | filesets.pop() | |
666 | ||
667 | if not filesets: | |
668 | annotation("failure", "File %s not touched as expected" % name) | |
669 | self.fail_test(1) | |
670 | ||
671 | def ignore_touch(self, wildcard): | |
672 | self.__ignore_elements(self.unexpected_difference.touched_files, | |
673 | wildcard) | |
674 | ||
675 | def ignore(self, wildcard): | |
676 | self.ignore_addition(wildcard) | |
677 | self.ignore_removal(wildcard) | |
678 | self.ignore_modification(wildcard) | |
679 | self.ignore_touch(wildcard) | |
680 | ||
681 | def expect_nothing(self, names): | |
682 | for name in self.adjust_names(names): | |
683 | if name in self.difference.added_files: | |
684 | annotation("failure", | |
685 | "File %s added, but no action was expected" % name) | |
686 | self.fail_test(1) | |
687 | if name in self.difference.removed_files: | |
688 | annotation("failure", | |
689 | "File %s removed, but no action was expected" % name) | |
690 | self.fail_test(1) | |
691 | pass | |
692 | if name in self.difference.modified_files: | |
693 | annotation("failure", | |
694 | "File %s modified, but no action was expected" % name) | |
695 | self.fail_test(1) | |
696 | if name in self.difference.touched_files: | |
697 | annotation("failure", | |
698 | "File %s touched, but no action was expected" % name) | |
699 | self.fail_test(1) | |
700 | ||
b32b8144 | 701 | def __ignore_junk(self): |
7c673cae FG |
702 | # Not totally sure about this change, but I do not see a good |
703 | # alternative. | |
704 | if windows: | |
705 | self.ignore("*.ilk") # MSVC incremental linking files. | |
706 | self.ignore("*.pdb") # MSVC program database files. | |
707 | self.ignore("*.rsp") # Response files. | |
708 | self.ignore("*.tds") # Borland debug symbols. | |
709 | self.ignore("*.manifest") # MSVC DLL manifests. | |
710 | ||
711 | # Debug builds of bjam built with gcc produce this profiling data. | |
712 | self.ignore("gmon.out") | |
713 | self.ignore("*/gmon.out") | |
714 | ||
715 | # Boost Build's 'configure' functionality (unfinished at the time) | |
716 | # produces this file. | |
717 | self.ignore("bin/config.log") | |
718 | self.ignore("bin/project-cache.jam") | |
719 | ||
720 | # Compiled Python files created when running Python based Boost Build. | |
721 | self.ignore("*.pyc") | |
722 | ||
b32b8144 FG |
723 | # OSX/Darwin files and dirs. |
724 | self.ignore("*.dSYM/*") | |
725 | ||
726 | def expect_nothing_more(self): | |
7c673cae FG |
727 | if not self.unexpected_difference.empty(): |
728 | annotation("failure", "Unexpected changes found") | |
729 | output = StringIO.StringIO() | |
730 | self.unexpected_difference.pprint(output) | |
731 | annotation("unexpected changes", output.getvalue()) | |
732 | self.fail_test(1) | |
733 | ||
734 | def expect_output_lines(self, lines, expected=True): | |
735 | self.__expect_lines(self.stdout(), lines, expected) | |
736 | ||
737 | def expect_content_lines(self, filename, line, expected=True): | |
b32b8144 | 738 | self.__expect_lines(self.read_and_strip(filename), line, expected) |
7c673cae FG |
739 | |
740 | def expect_content(self, name, content, exact=False): | |
b32b8144 | 741 | actual = self.read(name) |
7c673cae FG |
742 | content = content.replace("$toolset", self.toolset + "*") |
743 | ||
744 | matched = False | |
745 | if exact: | |
746 | matched = fnmatch.fnmatch(actual, content) | |
747 | else: | |
748 | def sorted_(x): | |
b32b8144 | 749 | x.sort(lambda x, y: cmp(x.lower().replace("\\","/"), y.lower().replace("\\","/"))) |
7c673cae FG |
750 | return x |
751 | actual_ = map(lambda x: sorted_(x.split()), actual.splitlines()) | |
752 | content_ = map(lambda x: sorted_(x.split()), content.splitlines()) | |
753 | if len(actual_) == len(content_): | |
754 | matched = map( | |
755 | lambda x, y: map(lambda n, p: fnmatch.fnmatch(n, p), x, y), | |
756 | actual_, content_) | |
757 | matched = reduce( | |
758 | lambda x, y: x and reduce( | |
759 | lambda a, b: a and b, | |
760 | y), | |
761 | matched) | |
762 | ||
763 | if not matched: | |
764 | print "Expected:\n" | |
765 | print content | |
766 | print "Got:\n" | |
767 | print actual | |
768 | self.fail_test(1) | |
769 | ||
b32b8144 | 770 | def maybe_do_diff(self, actual, expected, result=None): |
7c673cae FG |
771 | if os.environ.get("DO_DIFF"): |
772 | e = tempfile.mktemp("expected") | |
773 | a = tempfile.mktemp("actual") | |
774 | f = open(e, "w") | |
775 | f.write(expected) | |
776 | f.close() | |
777 | f = open(a, "w") | |
778 | f.write(actual) | |
779 | f.close() | |
780 | print("DIFFERENCE") | |
781 | # Current diff should return 1 to indicate 'different input files' | |
782 | # but some older diff versions may return 0 and depending on the | |
783 | # exact Python/OS platform version, os.system() call may gobble up | |
784 | # the external process's return code and return 0 itself. | |
785 | if os.system('diff -u "%s" "%s"' % (e, a)) not in [0, 1]: | |
786 | print('Unable to compute difference: diff -u "%s" "%s"' % (e, a | |
787 | )) | |
788 | os.unlink(e) | |
789 | os.unlink(a) | |
b32b8144 FG |
790 | elif type(result) is TestCmd.MatchError: |
791 | print(result.message) | |
7c673cae FG |
792 | else: |
793 | print("Set environmental variable 'DO_DIFF' to examine the " | |
794 | "difference.") | |
795 | ||
796 | # Internal methods. | |
797 | def adjust_lib_name(self, name): | |
798 | global lib_prefix | |
799 | global dll_prefix | |
800 | result = name | |
801 | ||
802 | pos = name.rfind(".") | |
803 | if pos != -1: | |
804 | suffix = name[pos:] | |
805 | if suffix == ".lib": | |
806 | (head, tail) = os.path.split(name) | |
807 | if lib_prefix: | |
808 | tail = lib_prefix + tail | |
809 | result = os.path.join(head, tail) | |
810 | elif suffix == ".dll": | |
811 | (head, tail) = os.path.split(name) | |
812 | if dll_prefix: | |
813 | tail = dll_prefix + tail | |
814 | result = os.path.join(head, tail) | |
815 | # If we want to use this name in a Jamfile, we better convert \ to /, | |
816 | # as otherwise we would have to quote \. | |
817 | result = result.replace("\\", "/") | |
818 | return result | |
819 | ||
820 | def adjust_suffix(self, name): | |
821 | if not self.translate_suffixes: | |
822 | return name | |
823 | pos = name.rfind(".") | |
824 | if pos == -1: | |
825 | return name | |
826 | suffix = name[pos:] | |
827 | return name[:pos] + suffixes.get(suffix, suffix) | |
828 | ||
829 | # Acceps either a string or a list of strings and returns a list of | |
830 | # strings. Adjusts suffixes on all names. | |
831 | def adjust_names(self, names): | |
832 | if names.__class__ is str: | |
833 | names = [names] | |
834 | r = map(self.adjust_lib_name, names) | |
835 | r = map(self.adjust_suffix, r) | |
836 | r = map(lambda x, t=self.toolset: x.replace("$toolset", t + "*"), r) | |
837 | return r | |
b32b8144 FG |
838 | |
839 | def adjust_name(self, name): | |
840 | return self.adjust_names(name)[0] | |
7c673cae | 841 | |
b32b8144 | 842 | def __native_file_name(self, name): |
7c673cae FG |
843 | return os.path.normpath(os.path.join(self.workdir, *name.split("/"))) |
844 | ||
b32b8144 FG |
845 | def native_file_name(self, name): |
846 | return self.__native_file_name(self.adjust_name(name)) | |
847 | ||
7c673cae FG |
848 | def wait_for_time_change(self, path, touch): |
849 | """ | |
850 | Wait for newly assigned file system modification timestamps for the | |
851 | given path to become large enough for the timestamp difference to be | |
852 | correctly recognized by both this Python based testing framework and | |
853 | the Boost Jam executable being tested. May optionally touch the given | |
854 | path to set its modification timestamp to the new value. | |
855 | ||
856 | """ | |
857 | self.__wait_for_time_change(path, touch, last_build_time=False) | |
858 | ||
859 | def __build_timestamp_resolution(self): | |
860 | """ | |
861 | Returns the minimum path modification timestamp resolution supported | |
862 | by the used Boost Jam executable. | |
863 | ||
864 | """ | |
865 | dir = tempfile.mkdtemp("bjam_version_info") | |
866 | try: | |
867 | jam_script = "timestamp_resolution.jam" | |
868 | f = open(os.path.join(dir, jam_script), "w") | |
869 | try: | |
870 | f.write("EXIT $(JAM_TIMESTAMP_RESOLUTION) : 0 ;") | |
871 | finally: | |
872 | f.close() | |
873 | p = subprocess.Popen([self.program[0], "-d0", "-f%s" % jam_script], | |
874 | stdout=subprocess.PIPE, cwd=dir, universal_newlines=True) | |
875 | out, err = p.communicate() | |
876 | finally: | |
877 | shutil.rmtree(dir, ignore_errors=False) | |
878 | ||
879 | if p.returncode != 0: | |
880 | raise TestEnvironmentError("Unexpected return code (%s) when " | |
881 | "detecting Boost Jam's minimum supported path modification " | |
882 | "timestamp resolution version information." % p.returncode) | |
883 | if err: | |
884 | raise TestEnvironmentError("Unexpected error output (%s) when " | |
885 | "detecting Boost Jam's minimum supported path modification " | |
886 | "timestamp resolution version information." % err) | |
887 | ||
888 | r = re.match("([0-9]{2}):([0-9]{2}):([0-9]{2}\\.[0-9]{9})$", out) | |
889 | if not r: | |
890 | # Older Boost Jam versions did not report their minimum supported | |
891 | # path modification timestamp resolution and did not actually | |
892 | # support path modification timestamp resolutions finer than 1 | |
893 | # second. | |
894 | # TODO: Phase this support out to avoid such fallback code from | |
895 | # possibly covering up other problems. | |
896 | return 1 | |
897 | if r.group(1) != "00" or r.group(2) != "00": # hours, minutes | |
898 | raise TestEnvironmentError("Boost Jam with too coarse minimum " | |
899 | "supported path modification timestamp resolution (%s:%s:%s)." | |
900 | % (r.group(1), r.group(2), r.group(3))) | |
901 | return float(r.group(3)) # seconds.nanoseconds | |
902 | ||
903 | def __ensure_newer_than_last_build(self, path): | |
904 | """ | |
905 | Updates the given path's modification timestamp after waiting for the | |
906 | newly assigned file system modification timestamp to become large | |
907 | enough for the timestamp difference between it and the last build | |
908 | timestamp to be correctly recognized by both this Python based testing | |
909 | framework and the Boost Jam executable being tested. Does nothing if | |
910 | there is no 'last build' information available. | |
911 | ||
912 | """ | |
913 | if self.last_build_timestamp: | |
914 | self.__wait_for_time_change(path, touch=True, last_build_time=True) | |
915 | ||
916 | def __expect_lines(self, data, lines, expected): | |
917 | """ | |
918 | Checks whether the given data contains the given lines. | |
919 | ||
920 | Data may be specified as a single string containing text lines | |
921 | separated by newline characters. | |
922 | ||
923 | Lines may be specified in any of the following forms: | |
924 | * Single string containing text lines separated by newlines - the | |
925 | given lines are searched for in the given data without any extra | |
926 | data lines between them. | |
927 | * Container of strings containing text lines separated by newlines | |
928 | - the given lines are searched for in the given data with extra | |
929 | data lines allowed between lines belonging to different strings. | |
930 | * Container of strings containing text lines separated by newlines | |
931 | and containers containing strings - the same as above with the | |
932 | internal containers containing strings being interpreted as if | |
933 | all their content was joined together into a single string | |
934 | separated by newlines. | |
935 | ||
936 | A newline at the end of any multi-line lines string is interpreted as | |
937 | an expected extra trailig empty line. | |
938 | """ | |
939 | # str.splitlines() trims at most one trailing newline while we want the | |
940 | # trailing newline to indicate that there should be an extra empty line | |
941 | # at the end. | |
942 | splitlines = lambda x : (x + "\n").splitlines() | |
943 | ||
944 | if data is None: | |
945 | data = [] | |
946 | elif data.__class__ is str: | |
947 | data = splitlines(data) | |
948 | ||
949 | if lines.__class__ is str: | |
950 | lines = [splitlines(lines)] | |
951 | else: | |
952 | expanded = [] | |
953 | for x in lines: | |
954 | if x.__class__ is str: | |
955 | x = splitlines(x) | |
956 | expanded.append(x) | |
957 | lines = expanded | |
958 | ||
959 | if _contains_lines(data, lines) != bool(expected): | |
960 | output = [] | |
961 | if expected: | |
962 | output = ["Did not find expected lines:"] | |
963 | else: | |
964 | output = ["Found unexpected lines:"] | |
965 | first = True | |
966 | for line_sequence in lines: | |
967 | if line_sequence: | |
968 | if first: | |
969 | first = False | |
970 | else: | |
971 | output.append("...") | |
972 | output.extend(" > " + line for line in line_sequence) | |
973 | output.append("in output:") | |
974 | output.extend(" > " + line for line in data) | |
975 | annotation("failure", "\n".join(output)) | |
976 | self.fail_test(1) | |
977 | ||
978 | def __ignore_elements(self, list, wildcard): | |
979 | """Removes in-place 'list' elements matching the given 'wildcard'.""" | |
980 | list[:] = filter(lambda x, w=wildcard: not fnmatch.fnmatch(x, w), list) | |
981 | ||
982 | def __makedirs(self, path, wait): | |
983 | """ | |
984 | Creates a folder with the given path, together with any missing | |
985 | parent folders. If WAIT is set, makes sure any newly created folders | |
986 | have modification timestamps newer than the ones left behind by the | |
987 | last build run. | |
988 | ||
989 | """ | |
990 | try: | |
991 | if wait: | |
992 | stack = [] | |
993 | while path and path not in stack and not os.path.isdir(path): | |
994 | stack.append(path) | |
995 | path = os.path.dirname(path) | |
996 | while stack: | |
997 | path = stack.pop() | |
998 | os.mkdir(path) | |
999 | self.__ensure_newer_than_last_build(path) | |
1000 | else: | |
1001 | os.makedirs(path) | |
1002 | except Exception: | |
1003 | pass | |
1004 | ||
1005 | def __python_timestamp_resolution(self, path, minimum_resolution): | |
1006 | """ | |
1007 | Returns the modification timestamp resolution for the given path | |
1008 | supported by the used Python interpreter/OS/filesystem combination. | |
1009 | Will not check for resolutions less than the given minimum value. Will | |
1010 | change the path's modification timestamp in the process. | |
1011 | ||
1012 | Return values: | |
1013 | 0 - nanosecond resolution supported | |
1014 | positive decimal - timestamp resolution in seconds | |
1015 | ||
1016 | """ | |
1017 | # Note on Python's floating point timestamp support: | |
1018 | # Python interpreter versions prior to Python 2.3 did not support | |
1019 | # floating point timestamps. Versions 2.3 through 3.3 may or may not | |
1020 | # support it depending on the configuration (may be toggled by calling | |
1021 | # os.stat_float_times(True/False) at program startup, disabled by | |
1022 | # default prior to Python 2.5 and enabled by default since). Python 3.3 | |
1023 | # deprecated this configuration and 3.4 removed support for it after | |
1024 | # which floating point timestamps are always supported. | |
1025 | ver = sys.version_info[0:2] | |
1026 | python_nanosecond_support = ver >= (3, 4) or (ver >= (2, 3) and | |
1027 | os.stat_float_times()) | |
1028 | ||
1029 | # Minimal expected floating point difference used to account for | |
1030 | # possible imprecise floating point number representations. We want | |
1031 | # this number to be small (at least smaller than 0.0001) but still | |
1032 | # large enough that we can be sure that increasing a floating point | |
1033 | # value by 2 * eta guarantees the value read back will be increased by | |
1034 | # at least eta. | |
1035 | eta = 0.00005 | |
1036 | ||
1037 | stats_orig = os.stat(path) | |
1038 | def test_time(diff): | |
1039 | """Returns whether a timestamp difference is detectable.""" | |
1040 | os.utime(path, (stats_orig.st_atime, stats_orig.st_mtime + diff)) | |
1041 | return os.stat(path).st_mtime > stats_orig.st_mtime + eta | |
1042 | ||
1043 | # Test for nanosecond timestamp resolution support. | |
1044 | if not minimum_resolution and python_nanosecond_support: | |
1045 | if test_time(2 * eta): | |
1046 | return 0 | |
1047 | ||
1048 | # Detect the filesystem timestamp resolution. Note that there is no | |
1049 | # need to make this code 'as fast as possible' as, this function gets | |
1050 | # called before having to sleep until the next detectable modification | |
1051 | # timestamp value and that, since we already know nanosecond resolution | |
1052 | # is not supported, will surely take longer than whatever we do here to | |
1053 | # detect this minimal detectable modification timestamp resolution. | |
1054 | step = 0.1 | |
1055 | if not python_nanosecond_support: | |
1056 | # If Python does not support nanosecond timestamp resolution we | |
1057 | # know the minimum possible supported timestamp resolution is 1 | |
1058 | # second. | |
1059 | minimum_resolution = max(1, minimum_resolution) | |
1060 | index = max(1, int(minimum_resolution / step)) | |
1061 | while step * index < minimum_resolution: | |
1062 | # Floating point number representation errors may cause our | |
1063 | # initially calculated start index to be too small if calculated | |
1064 | # directly. | |
1065 | index += 1 | |
1066 | while True: | |
1067 | # Do not simply add up the steps to avoid cumulative floating point | |
1068 | # number representation errors. | |
1069 | next = step * index | |
1070 | if next > 10: | |
1071 | raise TestEnvironmentError("File systems with too coarse " | |
1072 | "modification timestamp resolutions not supported.") | |
1073 | if test_time(next): | |
1074 | return next | |
1075 | index += 1 | |
1076 | ||
7c673cae FG |
1077 | def __wait_for_time_change(self, path, touch, last_build_time): |
1078 | """ | |
1079 | Wait until a newly assigned file system modification timestamp for | |
1080 | the given path is large enough for the timestamp difference between it | |
1081 | and the last build timestamp or the path's original file system | |
1082 | modification timestamp (depending on the last_build_time flag) to be | |
1083 | correctly recognized by both this Python based testing framework and | |
1084 | the Boost Jam executable being tested. May optionally touch the given | |
1085 | path to set its modification timestamp to the new value. | |
1086 | ||
1087 | """ | |
1088 | assert self.last_build_timestamp or not last_build_time | |
1089 | stats_orig = os.stat(path) | |
1090 | ||
1091 | if last_build_time: | |
1092 | start_time = self.last_build_timestamp | |
1093 | else: | |
1094 | start_time = stats_orig.st_mtime | |
1095 | ||
1096 | build_resolution = self.__build_timestamp_resolution() | |
1097 | assert build_resolution >= 0 | |
1098 | ||
1099 | # Check whether the current timestamp is already new enough. | |
1100 | if stats_orig.st_mtime > start_time and (not build_resolution or | |
1101 | stats_orig.st_mtime >= start_time + build_resolution): | |
1102 | return | |
1103 | ||
1104 | resolution = self.__python_timestamp_resolution(path, build_resolution) | |
1105 | assert resolution >= build_resolution | |
1106 | ||
1107 | # Implementation notes: | |
1108 | # * Theoretically time.sleep() API might get interrupted too soon | |
1109 | # (never actually encountered). | |
1110 | # * We encountered cases where we sleep just long enough for the | |
1111 | # filesystem's modifiction timestamp to change to the desired value, | |
1112 | # but after waking up, the read timestamp is still just a tiny bit | |
1113 | # too small (encountered on Windows). This is most likely caused by | |
1114 | # imprecise floating point timestamp & sleep interval representation | |
1115 | # used by Python. Note though that we never encountered a case where | |
1116 | # more than one additional tiny sleep() call was needed to remedy | |
1117 | # the situation. | |
1118 | # * We try to wait long enough for the timestamp to change, but do not | |
1119 | # want to waste processing time by waiting too long. The main | |
1120 | # problem is that when we have a coarse resolution, the actual times | |
1121 | # get rounded and we do not know the exact sleep time needed for the | |
1122 | # difference between two such times to pass. E.g. if we have a 1 | |
1123 | # second resolution and the original and the current file timestamps | |
1124 | # are both 10 seconds then it could be that the current time is | |
1125 | # 10.99 seconds and that we can wait for just one hundredth of a | |
1126 | # second for the current file timestamp to reach its next value, and | |
1127 | # using a longer sleep interval than that would just be wasting | |
1128 | # time. | |
1129 | while True: | |
1130 | os.utime(path, None) | |
1131 | c = os.stat(path).st_mtime | |
1132 | if resolution: | |
1133 | if c > start_time and (not build_resolution or c >= start_time | |
1134 | + build_resolution): | |
1135 | break | |
1136 | if c <= start_time - resolution: | |
1137 | # Move close to the desired timestamp in one sleep, but not | |
1138 | # close enough for timestamp rounding to potentially cause | |
1139 | # us to wait too long. | |
1140 | if start_time - c > 5: | |
1141 | if last_build_time: | |
1142 | error_message = ("Last build time recorded as " | |
1143 | "being a future event, causing a too long " | |
1144 | "wait period. Something must have played " | |
1145 | "around with the system clock.") | |
1146 | else: | |
1147 | error_message = ("Original path modification " | |
1148 | "timestamp set to far into the future or " | |
1149 | "something must have played around with the " | |
1150 | "system clock, causing a too long wait " | |
1151 | "period.\nPath: '%s'" % path) | |
1152 | raise TestEnvironmentError(message) | |
1153 | _sleep(start_time - c) | |
1154 | else: | |
1155 | # We are close to the desired timestamp so take baby sleeps | |
1156 | # to avoid sleeping too long. | |
1157 | _sleep(max(0.01, resolution / 10)) | |
1158 | else: | |
1159 | if c > start_time: | |
1160 | break | |
1161 | _sleep(max(0.01, start_time - c)) | |
1162 | ||
1163 | if not touch: | |
1164 | os.utime(path, (stats_orig.st_atime, stats_orig.st_mtime)) | |
1165 | ||
1166 | ||
1167 | class List: | |
1168 | def __init__(self, s=""): | |
1169 | elements = [] | |
1170 | if s.__class__ is str: | |
1171 | # Have to handle escaped spaces correctly. | |
1172 | elements = s.replace("\ ", "\001").split() | |
1173 | else: | |
1174 | elements = s | |
1175 | self.l = [e.replace("\001", " ") for e in elements] | |
1176 | ||
1177 | def __len__(self): | |
1178 | return len(self.l) | |
1179 | ||
1180 | def __getitem__(self, key): | |
1181 | return self.l[key] | |
1182 | ||
1183 | def __setitem__(self, key, value): | |
1184 | self.l[key] = value | |
1185 | ||
1186 | def __delitem__(self, key): | |
1187 | del self.l[key] | |
1188 | ||
1189 | def __str__(self): | |
1190 | return str(self.l) | |
1191 | ||
1192 | def __repr__(self): | |
1193 | return "%s.List(%r)" % (self.__module__, " ".join(self.l)) | |
1194 | ||
1195 | def __mul__(self, other): | |
1196 | result = List() | |
1197 | if not isinstance(other, List): | |
1198 | other = List(other) | |
1199 | for f in self: | |
1200 | for s in other: | |
1201 | result.l.append(f + s) | |
1202 | return result | |
1203 | ||
1204 | def __rmul__(self, other): | |
1205 | if not isinstance(other, List): | |
1206 | other = List(other) | |
1207 | return List.__mul__(other, self) | |
1208 | ||
1209 | def __add__(self, other): | |
1210 | result = List() | |
1211 | result.l = self.l[:] + other.l[:] | |
1212 | return result | |
1213 | ||
1214 | ||
1215 | def _contains_lines(data, lines): | |
1216 | data_line_count = len(data) | |
1217 | expected_line_count = reduce(lambda x, y: x + len(y), lines, 0) | |
1218 | index = 0 | |
1219 | for expected in lines: | |
1220 | if expected_line_count > data_line_count - index: | |
1221 | return False | |
1222 | expected_line_count -= len(expected) | |
1223 | index = _match_line_sequence(data, index, data_line_count - | |
1224 | expected_line_count, expected) | |
1225 | if index < 0: | |
1226 | return False | |
1227 | return True | |
1228 | ||
1229 | ||
1230 | def _match_line_sequence(data, start, end, lines): | |
1231 | if not lines: | |
1232 | return start | |
1233 | for index in xrange(start, end - len(lines) + 1): | |
1234 | data_index = index | |
1235 | for expected in lines: | |
1236 | if not fnmatch.fnmatch(data[data_index], expected): | |
1237 | break; | |
1238 | data_index += 1 | |
1239 | else: | |
1240 | return data_index | |
1241 | return -1 | |
1242 | ||
1243 | ||
1244 | def _sleep(delay): | |
1245 | if delay > 5: | |
1246 | raise TestEnvironmentError("Test environment error: sleep period of " | |
1247 | "more than 5 seconds requested. Most likely caused by a file with " | |
1248 | "its modification timestamp set to sometime in the future.") | |
1249 | time.sleep(delay) | |
1250 | ||
1251 | ||
1252 | ############################################################################### | |
1253 | # | |
1254 | # Initialization. | |
1255 | # | |
1256 | ############################################################################### | |
1257 | ||
1258 | # Make os.stat() return file modification times as floats instead of integers | |
1259 | # to get the best possible file timestamp resolution available. The exact | |
1260 | # resolution depends on the underlying file system and the Python os.stat() | |
1261 | # implementation. The better the resolution we achieve, the shorter we need to | |
1262 | # wait for files we create to start getting new timestamps. | |
1263 | # | |
1264 | # Additional notes: | |
1265 | # * os.stat_float_times() function first introduced in Python 2.3. and | |
1266 | # suggested for deprecation in Python 3.3. | |
1267 | # * On Python versions 2.5+ we do not need to do this as there os.stat() | |
1268 | # returns floating point file modification times by default. | |
1269 | # * Windows CPython implementations prior to version 2.5 do not support file | |
1270 | # modification timestamp resolutions of less than 1 second no matter whether | |
1271 | # these timestamps are returned as integer or floating point values. | |
1272 | # * Python documentation states that this should be set in a program's | |
1273 | # __main__ module to avoid affecting other libraries that might not be ready | |
1274 | # to support floating point timestamps. Since we use no such external | |
1275 | # libraries, we ignore this warning to make it easier to enable this feature | |
1276 | # in both our single & multiple-test scripts. | |
1277 | if (2, 3) <= sys.version_info < (2, 5) and not os.stat_float_times(): | |
1278 | os.stat_float_times(True) | |
1279 | ||
1280 | ||
1281 | # Quickie tests. Should use doctest instead. | |
1282 | if __name__ == "__main__": | |
1283 | assert str(List("foo bar") * "/baz") == "['foo/baz', 'bar/baz']" | |
1284 | assert repr("foo/" * List("bar baz")) == "__main__.List('foo/bar foo/baz')" | |
1285 | ||
1286 | assert _contains_lines([], []) | |
1287 | assert _contains_lines([], [[]]) | |
1288 | assert _contains_lines([], [[], []]) | |
1289 | assert _contains_lines([], [[], [], []]) | |
1290 | assert not _contains_lines([], [[""]]) | |
1291 | assert not _contains_lines([], [["a"]]) | |
1292 | ||
1293 | assert _contains_lines([""], []) | |
1294 | assert _contains_lines(["a"], []) | |
1295 | assert _contains_lines(["a", "b"], []) | |
1296 | assert _contains_lines(["a", "b"], [[], [], []]) | |
1297 | ||
1298 | assert _contains_lines([""], [[""]]) | |
1299 | assert not _contains_lines([""], [["a"]]) | |
1300 | assert not _contains_lines(["a"], [[""]]) | |
1301 | assert _contains_lines(["a", "", "b", ""], [["a"]]) | |
1302 | assert _contains_lines(["a", "", "b", ""], [[""]]) | |
1303 | assert _contains_lines(["a", "", "b"], [["b"]]) | |
1304 | assert not _contains_lines(["a", "b"], [[""]]) | |
1305 | assert not _contains_lines(["a", "", "b", ""], [["c"]]) | |
1306 | assert _contains_lines(["a", "", "b", "x"], [["x"]]) | |
1307 | ||
1308 | data = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] | |
1309 | assert _contains_lines(data, [["1", "2"]]) | |
1310 | assert not _contains_lines(data, [["2", "1"]]) | |
1311 | assert not _contains_lines(data, [["1", "3"]]) | |
1312 | assert not _contains_lines(data, [["1", "3"]]) | |
1313 | assert _contains_lines(data, [["1"], ["2"]]) | |
1314 | assert _contains_lines(data, [["1"], [], [], [], ["2"]]) | |
1315 | assert _contains_lines(data, [["1"], ["3"]]) | |
1316 | assert not _contains_lines(data, [["3"], ["1"]]) | |
1317 | assert _contains_lines(data, [["3"], ["7"], ["8"]]) | |
1318 | assert not _contains_lines(data, [["1"], ["3", "5"]]) | |
1319 | assert not _contains_lines(data, [["1"], [""], ["5"]]) | |
1320 | assert not _contains_lines(data, [["1"], ["5"], ["3"]]) | |
1321 | assert not _contains_lines(data, [["1"], ["5", "3"]]) | |
1322 | ||
1323 | assert not _contains_lines(data, [[" 3"]]) | |
1324 | assert not _contains_lines(data, [["3 "]]) | |
1325 | assert not _contains_lines(data, [["3", ""]]) | |
1326 | assert not _contains_lines(data, [["", "3"]]) | |
1327 | ||
1328 | print("tests passed") |