|
1 #!/usr/bin/env python |
|
2 # |
|
3 # This Source Code Form is subject to the terms of the Mozilla Public |
|
4 # License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
|
6 |
|
7 from __future__ import with_statement |
|
8 import sys, os, tempfile, shutil |
|
9 from optparse import OptionParser |
|
10 import mozprocess, mozinfo, mozlog, mozcrash, mozfile |
|
11 from contextlib import contextmanager |
|
12 |
|
13 log = mozlog.getLogger('cppunittests') |
|
14 |
|
15 class CPPUnitTests(object): |
|
16 # Time (seconds) to wait for test process to complete |
|
17 TEST_PROC_TIMEOUT = 900 |
|
18 # Time (seconds) in which process will be killed if it produces no output. |
|
19 TEST_PROC_NO_OUTPUT_TIMEOUT = 300 |
|
20 |
|
21 def run_one_test(self, prog, env, symbols_path=None): |
|
22 """ |
|
23 Run a single C++ unit test program. |
|
24 |
|
25 Arguments: |
|
26 * prog: The path to the test program to run. |
|
27 * env: The environment to use for running the program. |
|
28 * symbols_path: A path to a directory containing Breakpad-formatted |
|
29 symbol files for producing stack traces on crash. |
|
30 |
|
31 Return True if the program exits with a zero status, False otherwise. |
|
32 """ |
|
33 basename = os.path.basename(prog) |
|
34 log.info("Running test %s", basename) |
|
35 with mozfile.TemporaryDirectory() as tempdir: |
|
36 proc = mozprocess.ProcessHandler([prog], |
|
37 cwd=tempdir, |
|
38 env=env) |
|
39 #TODO: After bug 811320 is fixed, don't let .run() kill the process, |
|
40 # instead use a timeout in .wait() and then kill to get a stack. |
|
41 proc.run(timeout=CPPUnitTests.TEST_PROC_TIMEOUT, |
|
42 outputTimeout=CPPUnitTests.TEST_PROC_NO_OUTPUT_TIMEOUT) |
|
43 proc.wait() |
|
44 if proc.timedOut: |
|
45 log.testFail("%s | timed out after %d seconds", |
|
46 basename, CPPUnitTests.TEST_PROC_TIMEOUT) |
|
47 return False |
|
48 if mozcrash.check_for_crashes(tempdir, symbols_path, |
|
49 test_name=basename): |
|
50 log.testFail("%s | test crashed", basename) |
|
51 return False |
|
52 result = proc.proc.returncode == 0 |
|
53 if not result: |
|
54 log.testFail("%s | test failed with return code %d", |
|
55 basename, proc.proc.returncode) |
|
56 return result |
|
57 |
|
58 def build_core_environment(self, env = {}): |
|
59 """ |
|
60 Add environment variables likely to be used across all platforms, including remote systems. |
|
61 """ |
|
62 env["MOZILLA_FIVE_HOME"] = self.xre_path |
|
63 env["MOZ_XRE_DIR"] = self.xre_path |
|
64 #TODO: switch this to just abort once all C++ unit tests have |
|
65 # been fixed to enable crash reporting |
|
66 env["XPCOM_DEBUG_BREAK"] = "stack-and-abort" |
|
67 env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" |
|
68 env["MOZ_CRASHREPORTER"] = "1" |
|
69 return env |
|
70 |
|
71 def build_environment(self): |
|
72 """ |
|
73 Create and return a dictionary of all the appropriate env variables and values. |
|
74 On a remote system, we overload this to set different values and are missing things like os.environ and PATH. |
|
75 """ |
|
76 if not os.path.isdir(self.xre_path): |
|
77 raise Exception("xre_path does not exist: %s", self.xre_path) |
|
78 env = dict(os.environ) |
|
79 env = self.build_core_environment(env) |
|
80 pathvar = "" |
|
81 if mozinfo.os == "linux": |
|
82 pathvar = "LD_LIBRARY_PATH" |
|
83 elif mozinfo.os == "mac": |
|
84 pathvar = "DYLD_LIBRARY_PATH" |
|
85 elif mozinfo.os == "win": |
|
86 pathvar = "PATH" |
|
87 if pathvar: |
|
88 if pathvar in env: |
|
89 env[pathvar] = "%s%s%s" % (self.xre_path, os.pathsep, env[pathvar]) |
|
90 else: |
|
91 env[pathvar] = self.xre_path |
|
92 |
|
93 # Use llvm-symbolizer for ASan if available/required |
|
94 llvmsym = os.path.join(self.xre_path, "llvm-symbolizer") |
|
95 if os.path.isfile(llvmsym): |
|
96 env["ASAN_SYMBOLIZER_PATH"] = llvmsym |
|
97 |
|
98 return env |
|
99 |
|
100 def run_tests(self, programs, xre_path, symbols_path=None): |
|
101 """ |
|
102 Run a set of C++ unit test programs. |
|
103 |
|
104 Arguments: |
|
105 * programs: An iterable containing paths to test programs. |
|
106 * xre_path: A path to a directory containing a XUL Runtime Environment. |
|
107 * symbols_path: A path to a directory containing Breakpad-formatted |
|
108 symbol files for producing stack traces on crash. |
|
109 |
|
110 Returns True if all test programs exited with a zero status, False |
|
111 otherwise. |
|
112 """ |
|
113 self.xre_path = xre_path |
|
114 env = self.build_environment() |
|
115 pass_count = 0 |
|
116 fail_count = 0 |
|
117 for prog in programs: |
|
118 single_result = self.run_one_test(prog, env, symbols_path) |
|
119 if single_result: |
|
120 pass_count += 1 |
|
121 else: |
|
122 fail_count += 1 |
|
123 |
|
124 log.info("Result summary:") |
|
125 log.info("Passed: %d" % pass_count) |
|
126 log.info("Failed: %d" % fail_count) |
|
127 return fail_count == 0 |
|
128 |
|
129 class CPPUnittestOptions(OptionParser): |
|
130 def __init__(self): |
|
131 OptionParser.__init__(self) |
|
132 self.add_option("--xre-path", |
|
133 action = "store", type = "string", dest = "xre_path", |
|
134 default = None, |
|
135 help = "absolute path to directory containing XRE (probably xulrunner)") |
|
136 self.add_option("--symbols-path", |
|
137 action = "store", type = "string", dest = "symbols_path", |
|
138 default = None, |
|
139 help = "absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols") |
|
140 self.add_option("--skip-manifest", |
|
141 action = "store", type = "string", dest = "manifest_file", |
|
142 default = None, |
|
143 help = "absolute path to a manifest file") |
|
144 |
|
145 def extract_unittests_from_args(args, manifest_file): |
|
146 """Extract unittests from args, expanding directories as needed""" |
|
147 progs = [] |
|
148 |
|
149 # Known files commonly packaged with the cppunittests that are not tests |
|
150 skipped_progs = set(['.mkdir.done', 'remotecppunittests.py', 'runcppunittests.py', 'runcppunittests.pyc']) |
|
151 |
|
152 if manifest_file: |
|
153 skipped_progs.add(os.path.basename(manifest_file)) |
|
154 with open(manifest_file) as f: |
|
155 for line in f: |
|
156 # strip out comment, if any |
|
157 prog = line.split('#')[0] |
|
158 if prog: |
|
159 skipped_progs.add(prog.strip()) |
|
160 |
|
161 for p in args: |
|
162 if os.path.isdir(p): |
|
163 progs.extend([os.path.abspath(os.path.join(p, x)) for x in os.listdir(p) if not x in skipped_progs]) |
|
164 elif p not in skipped_progs: |
|
165 progs.append(os.path.abspath(p)) |
|
166 |
|
167 return progs |
|
168 |
|
169 def main(): |
|
170 parser = CPPUnittestOptions() |
|
171 options, args = parser.parse_args() |
|
172 if not args: |
|
173 print >>sys.stderr, """Usage: %s <test binary> [<test binary>...]""" % sys.argv[0] |
|
174 sys.exit(1) |
|
175 if not options.xre_path: |
|
176 print >>sys.stderr, """Error: --xre-path is required""" |
|
177 sys.exit(1) |
|
178 |
|
179 progs = extract_unittests_from_args(args, options.manifest_file) |
|
180 options.xre_path = os.path.abspath(options.xre_path) |
|
181 tester = CPPUnitTests() |
|
182 try: |
|
183 result = tester.run_tests(progs, options.xre_path, options.symbols_path) |
|
184 except Exception, e: |
|
185 log.error(str(e)) |
|
186 result = False |
|
187 sys.exit(0 if result else 1) |
|
188 |
|
189 if __name__ == '__main__': |
|
190 main() |
|
191 |