Bug 1753424 - Add SandboxingKind for UtilityProcess crash annotations r=gsvelto
[gecko.git] / testing / remotecppunittests.py
blobb391d312f0528e627af635649d9d4dcfe0987c3a
1 #!/usr/bin/env python
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/.
7 from __future__ import absolute_import, print_function
8 import os
9 import posixpath
10 import sys
11 import subprocess
12 import traceback
13 from zipfile import ZipFile
14 import runcppunittests as cppunittests
15 import mozcrash
16 import mozfile
17 import mozinfo
18 import mozlog
19 from mozdevice import ADBDeviceFactory, ADBProcessError, ADBTimeoutError
21 try:
22 from mozbuild.base import MozbuildObject
24 build_obj = MozbuildObject.from_environment()
25 except ImportError:
26 build_obj = None
29 class RemoteCPPUnitTests(cppunittests.CPPUnitTests):
30 def __init__(self, options, progs):
31 cppunittests.CPPUnitTests.__init__(self)
32 self.options = options
33 self.device = ADBDeviceFactory(
34 adb=options.adb_path or "adb",
35 device=options.device_serial,
36 test_root=options.remote_test_root,
38 self.remote_test_root = posixpath.join(self.device.test_root, "cppunittests")
39 self.remote_bin_dir = posixpath.join(self.remote_test_root, "b")
40 self.remote_tmp_dir = posixpath.join(self.remote_test_root, "tmp")
41 self.remote_home_dir = posixpath.join(self.remote_test_root, "h")
42 if options.setup:
43 self.setup_bin(progs)
45 def setup_bin(self, progs):
46 self.device.rm(self.remote_test_root, force=True, recursive=True)
47 self.device.mkdir(self.remote_home_dir, parents=True)
48 self.device.mkdir(self.remote_tmp_dir)
49 self.device.mkdir(self.remote_bin_dir)
50 self.push_libs()
51 self.push_progs(progs)
52 self.device.chmod(self.remote_bin_dir, recursive=True)
54 def push_libs(self):
55 if self.options.local_apk:
56 with mozfile.TemporaryDirectory() as tmpdir:
57 apk_contents = ZipFile(self.options.local_apk)
59 for info in apk_contents.infolist():
60 if info.filename.endswith(".so"):
61 print("Pushing %s.." % info.filename, file=sys.stderr)
62 remote_file = posixpath.join(
63 self.remote_bin_dir, os.path.basename(info.filename)
65 apk_contents.extract(info, tmpdir)
66 local_file = os.path.join(tmpdir, info.filename)
67 with open(local_file, "rb") as f:
68 # Decompress xz-compressed file.
69 if f.read(5)[1:] == "7zXZ":
70 cmd = ["xz", "-df", "--suffix", ".so", local_file]
71 subprocess.check_output(cmd)
72 # xz strips the ".so" file suffix.
73 os.rename(local_file[:-3], local_file)
74 self.device.push(local_file, remote_file)
76 elif self.options.local_lib:
77 for path in os.listdir(self.options.local_lib):
78 if path.endswith(".so"):
79 print("Pushing {}..".format(path), file=sys.stderr)
80 remote_file = posixpath.join(self.remote_bin_dir, path)
81 local_file = os.path.join(self.options.local_lib, path)
82 self.device.push(local_file, remote_file)
83 # Additional libraries may be found in a sub-directory such as
84 # "lib/armeabi-v7a"
85 for subdir in ["assets", "lib"]:
86 local_arm_lib = os.path.join(self.options.local_lib, subdir)
87 if os.path.isdir(local_arm_lib):
88 for root, dirs, paths in os.walk(local_arm_lib):
89 for path in paths:
90 if path.endswith(".so"):
91 print("Pushing {}..".format(path), file=sys.stderr)
92 remote_file = posixpath.join(self.remote_bin_dir, path)
93 local_file = os.path.join(root, path)
94 self.device.push(local_file, remote_file)
96 def push_progs(self, progs):
97 for local_file in progs:
98 remote_file = posixpath.join(
99 self.remote_bin_dir, os.path.basename(local_file)
101 self.device.push(local_file, remote_file)
103 def build_environment(self):
104 env = self.build_core_environment({})
105 env["LD_LIBRARY_PATH"] = self.remote_bin_dir
106 env["TMPDIR"] = self.remote_tmp_dir
107 env["HOME"] = self.remote_home_dir
108 env["MOZ_XRE_DIR"] = self.remote_bin_dir
109 if self.options.add_env:
110 for envdef in self.options.add_env:
111 envdef_parts = envdef.split("=", 1)
112 if len(envdef_parts) == 2:
113 env[envdef_parts[0]] = envdef_parts[1]
114 elif len(envdef_parts) == 1:
115 env[envdef_parts[0]] = ""
116 else:
117 self.log.warning("invalid --addEnv option skipped: %s" % envdef)
119 return env
121 def run_one_test(
122 self, prog, env, symbols_path=None, interactive=False, timeout_factor=1
125 Run a single C++ unit test program remotely.
127 Arguments:
128 * prog: The path to the test program to run.
129 * env: The environment to use for running the program.
130 * symbols_path: A path to a directory containing Breakpad-formatted
131 symbol files for producing stack traces on crash.
132 * timeout_factor: An optional test-specific timeout multiplier.
134 Return True if the program exits with a zero status, False otherwise.
136 basename = os.path.basename(prog)
137 remote_bin = posixpath.join(self.remote_bin_dir, basename)
138 self.log.test_start(basename)
139 test_timeout = cppunittests.CPPUnitTests.TEST_PROC_TIMEOUT * timeout_factor
141 try:
142 output = self.device.shell_output(
143 remote_bin, env=env, cwd=self.remote_home_dir, timeout=test_timeout
145 returncode = 0
146 except ADBTimeoutError:
147 raise
148 except ADBProcessError as e:
149 output = e.adb_process.stdout
150 returncode = e.adb_process.exitcode
152 self.log.process_output(basename, "\n%s" % output, command=[remote_bin])
153 with mozfile.TemporaryDirectory() as tempdir:
154 self.device.pull(self.remote_home_dir, tempdir)
155 if mozcrash.check_for_crashes(tempdir, symbols_path, test_name=basename):
156 self.log.test_end(basename, status="CRASH", expected="PASS")
157 return False
158 result = returncode == 0
159 if not result:
160 self.log.test_end(
161 basename,
162 status="FAIL",
163 expected="PASS",
164 message=("test failed with return code %s" % returncode),
166 else:
167 self.log.test_end(basename, status="PASS", expected="PASS")
168 return result
171 class RemoteCPPUnittestOptions(cppunittests.CPPUnittestOptions):
172 def __init__(self):
173 cppunittests.CPPUnittestOptions.__init__(self)
174 defaults = {}
176 self.add_option(
177 "--deviceSerial",
178 action="store",
179 type="string",
180 dest="device_serial",
181 help="adb serial number of remote device. This is required "
182 "when more than one device is connected to the host. "
183 "Use 'adb devices' to see connected devices.",
185 defaults["device_serial"] = None
187 self.add_option(
188 "--adbPath",
189 action="store",
190 type="string",
191 dest="adb_path",
192 help="Path to adb binary.",
194 defaults["adb_path"] = None
196 self.add_option(
197 "--noSetup",
198 action="store_false",
199 dest="setup",
200 help="Do not copy any files to device (to be used only if "
201 "device is already setup).",
203 defaults["setup"] = True
205 self.add_option(
206 "--localLib",
207 action="store",
208 type="string",
209 dest="local_lib",
210 help="Location of libraries to push -- preferably stripped.",
212 defaults["local_lib"] = None
214 self.add_option(
215 "--apk",
216 action="store",
217 type="string",
218 dest="local_apk",
219 help="Local path to Firefox for Android APK.",
221 defaults["local_apk"] = None
223 self.add_option(
224 "--localBinDir",
225 action="store",
226 type="string",
227 dest="local_bin",
228 help="Local path to bin directory.",
230 defaults["local_bin"] = build_obj.bindir if build_obj is not None else None
232 self.add_option(
233 "--remoteTestRoot",
234 action="store",
235 type="string",
236 dest="remote_test_root",
237 help="Remote directory to use as test root "
238 "(eg. /data/local/tmp/test_root).",
241 # /data/local/tmp/test_root is used because it is usually not
242 # possible to set +x permissions on binaries on /mnt/sdcard
243 # and since scope storage on Android 10 causes permission
244 # errors on the sdcard.
245 defaults["remote_test_root"] = "/data/local/tmp/test_root"
247 self.add_option(
248 "--addEnv",
249 action="append",
250 type="string",
251 dest="add_env",
252 help="additional remote environment variable definitions "
253 '(eg. --addEnv "somevar=something")',
255 defaults["add_env"] = None
257 self.set_defaults(**defaults)
260 def run_test_harness(options, args):
261 options.xre_path = os.path.abspath(options.xre_path)
262 cppunittests.update_mozinfo()
263 progs = cppunittests.extract_unittests_from_args(
264 args, mozinfo.info, options.manifest_path
266 tester = RemoteCPPUnitTests(options, [item[0] for item in progs])
267 result = tester.run_tests(
268 progs,
269 options.xre_path,
270 options.symbols_path,
272 return result
275 def main():
276 parser = RemoteCPPUnittestOptions()
277 mozlog.commandline.add_logging_group(parser)
278 options, args = parser.parse_args()
279 if not args:
280 print(
281 """Usage: %s <test binary> [<test binary>...]""" % sys.argv[0],
282 file=sys.stderr,
284 sys.exit(1)
285 if options.local_lib is not None and not os.path.isdir(options.local_lib):
286 print(
287 """Error: --localLib directory %s not found""" % options.local_lib,
288 file=sys.stderr,
290 sys.exit(1)
291 if options.local_apk is not None and not os.path.isfile(options.local_apk):
292 print("""Error: --apk file %s not found""" % options.local_apk, file=sys.stderr)
293 sys.exit(1)
294 if not options.xre_path:
295 print("""Error: --xre-path is required""", file=sys.stderr)
296 sys.exit(1)
298 log = mozlog.commandline.setup_logging(
299 "remotecppunittests", options, {"tbpl": sys.stdout}
301 try:
302 result = run_test_harness(options, args)
303 except Exception as e:
304 log.error(str(e))
305 traceback.print_exc()
306 result = False
307 sys.exit(0 if result else 1)
310 if __name__ == "__main__":
311 main()