The open source OpenXR runtime
at disable-ht-prediction 273 lines 8.2 kB view raw
1# Copyright 2024, Collabora, Ltd. 2# 3# SPDX-License-Identifier: BSL-1.0 4# 5# Author: Rylie Pavlik <rylie.pavlik@collabora.com> 6""" 7pytest configuration and helpers for adb/Android. 8 9Helper functions to run tests using adb on an Android device. 10 11pytest serves as the test runner and collects the results. 12 13See README.md in this directory for requirements and running instructions. 14""" 15 16import subprocess 17import time 18from pathlib import Path 19from typing import Optional 20 21import pytest 22 23_NATIVE_ACTIVITY = "android.app.NativeActivity" 24helloxr_vulkan_pkg = "org.khronos.openxr.hello_xr.vulkan" 25helloxr_vulkan_activity = f"{helloxr_vulkan_pkg}/{_NATIVE_ACTIVITY}" 26helloxr_gles_pkg = "org.khronos.openxr.hello_xr.opengles" 27helloxr_gles_activity = f"{helloxr_gles_pkg}/{_NATIVE_ACTIVITY}" 28 29RUNTIME = "org.freedesktop.monado.openxr_runtime.out_of_process" 30 31_PKGS_TO_STOP = [ 32 helloxr_gles_pkg, 33 helloxr_vulkan_pkg, 34 "org.khronos.openxr.cts", 35 "org.freedesktop.monado.openxr_runtime.in_process", 36 RUNTIME, 37] 38 39_REPO_ROOT = Path(__file__).resolve().parent.parent.parent 40 41 42class AdbTesting: 43 """pytest fixture for testing on an Android device with adb.""" 44 45 def __init__(self, tmp_path: Optional[Path]): 46 """Initialize members.""" 47 self.tmp_path = tmp_path 48 self.logcat_grabbed = False 49 self.logcat_echoed = False 50 self.log: Optional[str] = None 51 52 # TODO allow most of the following to be configured 53 self._ndk_ver = "26.3.11579264" 54 self._build_variant = "outOfProcessDebug" 55 self._sdk_root = Path.home() / "Android" / "Sdk" 56 self._arch = "arm64-v8a" 57 self._ndk_stack = self._sdk_root / "ndk" / self._ndk_ver / "ndk-stack" 58 self._caps_build_variant = ( 59 self._build_variant[0].upper() + self._build_variant[1:] 60 ) 61 self._sym_dir = ( 62 _REPO_ROOT 63 / "src" 64 / "xrt" 65 / "targets" 66 / "openxr_android" 67 / "build" 68 / "intermediates" 69 / "merged_native_libs" 70 / self._build_variant 71 / f"merge{self._caps_build_variant}NativeLibs" 72 / "out" 73 / "lib" 74 / self._arch 75 ) 76 77 def adb_call(self, cmd: list[str], **kwargs): 78 """Call ADB in a subprocess.""" 79 return subprocess.check_call(cmd, **kwargs) 80 81 def adb_check_output(self, cmd: list[str], **kwargs): 82 """Call ADB in a subprocess and get the output.""" 83 return subprocess.check_output(cmd, encoding="utf-8", **kwargs) 84 85 def get_device_state(self) -> Optional[str]: 86 try: 87 state = self.adb_check_output(["adb", "get-state"]).strip() 88 return state.strip() 89 90 except subprocess.CalledProcessError: 91 return None 92 93 def send_key(self, keyevent): 94 """Send a key event with `adb shell input keyevent`.""" 95 time.sleep(1) 96 print(f"*** {keyevent}") 97 self.adb_call(["adb", "shell", "input", "keyevent", keyevent]) 98 99 def send_tap(self, x, y): 100 """Send touchscreen tap at x, y.""" 101 # sleep 1 102 print(f"*** tap {x} {y}") 103 self.adb_call( 104 [ 105 "adb", 106 "shell", 107 "input", 108 "touchscreen", 109 "tap", 110 str(x), 111 str(y), 112 ] 113 ) 114 115 def start_activity(self, activity): 116 """Start an activity and wait with `adb shell am start-activity -W`.""" 117 time.sleep(1) 118 print(f"*** starting {activity} and waiting") 119 self.adb_call(["adb", "shell", "am", "start-activity", "-W", activity]) 120 121 def stop_and_clear_all(self): 122 """ 123 Stop relevant packages and clear their data. 124 125 This second step removes them from recents. 126 """ 127 print("*** stopping all relevant packages and clearing their data") 128 for pkg in _PKGS_TO_STOP: 129 self.adb_call(["adb", "shell", "am", "force-stop", pkg]) 130 self.adb_call(["adb", "shell", "pm", "clear", pkg]) 131 132 def start_test( 133 self, 134 ): 135 """Ensure a clean starting setup.""" 136 self.stop_and_clear_all() 137 self.adb_call(["adb", "logcat", "-c"]) 138 139 @property 140 def logcat_fn(self): 141 """Get computed path of logcat file.""" 142 assert self.tmp_path 143 return self.tmp_path / "logcat.txt" 144 145 def grab_logcat(self): 146 """Save logcat to the named file and return it as a string.""" 147 log = self.adb_check_output(["adb", "logcat", "-d"]) 148 log_fn = self.logcat_fn 149 with open(log_fn, "w", encoding="utf-8") as fp: 150 fp.write(log) 151 print(f"Logcat saved to {log_fn}") 152 self.logcat_grabbed = True 153 self.log = log 154 return log 155 156 def dump_logcat(self): 157 """Print logcat to stdout.""" 158 if self.logcat_echoed: 159 return 160 if not self.logcat_grabbed: 161 self.grab_logcat() 162 print("*** Logcat:") 163 print(self.log) 164 self.logcat_echoed = True 165 166 def check_for_crash(self): 167 """Dump logcat, and look for a crash.""" 168 __tracebackhide__ = True 169 log = self.grab_logcat() 170 log_fn = self.logcat_fn 171 crash_name = log_fn.with_name("crashes.txt") 172 if "backtrace:" in log: 173 subprocess.check_call( 174 " ".join( 175 ( 176 str(x) 177 for x in ( 178 self._ndk_stack, 179 "-sym", 180 self._sym_dir, 181 "-i", 182 log_fn, 183 ">", 184 crash_name, 185 ) 186 ) 187 ), 188 shell=True, 189 ) 190 self.dump_logcat() 191 192 with open(crash_name, "r", encoding="utf-8") as fp: 193 print("*** Crash Backtrace(s):") 194 print(fp.read()) 195 print(f"*** Crash backtrace in {crash_name}") 196 pytest.fail("Native crash backtrace detected in test") 197 198 if "FATAL EXCEPTION" in log: 199 self.dump_logcat() 200 pytest.fail("Java exception detected in test") 201 202 if "WindowManager: ANR" in log: 203 self.dump_logcat() 204 pytest.fail("ANR detected in test") 205 206 self.stop_and_clear_all() 207 208 209@pytest.fixture(scope="function") 210def adb(tmp_path): 211 """Provide AdbTesting instance and cleanup as pytest fixture.""" 212 adb_testing = AdbTesting(tmp_path) 213 214 adb_testing.start_test() 215 216 # Body of test happens here 217 yield adb_testing 218 219 # Grab logcat in case it wasn't grabbed, and dump to stdout 220 adb_testing.dump_logcat() 221 222 # Cleanup 223 adb_testing.stop_and_clear_all() 224 225 226@pytest.fixture(scope="session", autouse=True) 227def log_device_and_build(record_testsuite_property): 228 """Session fixture, autoused, to record device data.""" 229 adb = AdbTesting(None) 230 231 try: 232 device = adb.adb_check_output( 233 ["adb", "shell", "getprop", "ro.product.vendor.device"] 234 ) 235 build = adb.adb_check_output( 236 ["adb", "shell", "getprop", "ro.product.build.fingerprint"] 237 ) 238 except subprocess.CalledProcessError: 239 pytest.skip("adb shell getprop failed - no device connected?") 240 241 record_testsuite_property("device", device.strip()) 242 record_testsuite_property("build", build.strip()) 243 244 245def device_is_available(): 246 """Return True if an ADB device is available.""" 247 adb = AdbTesting(None) 248 249 try: 250 state = adb.adb_check_output(["adb", "get-state"]).strip() 251 return state == "device" 252 253 except subprocess.CalledProcessError: 254 return False 255 256 257def pytest_addoption(parser: pytest.Parser): 258 """Pytest addoption function.""" 259 parser.addoption( 260 "--adb", 261 action="store_true", 262 default=False, 263 help="run ADB/Android device tests", 264 ) 265 266 267def pytest_configure(config): 268 """Configure function for pytest.""" 269 config.addinivalue_line( 270 "markers", 271 "adb: mark test as needing adb and thus only run when an " 272 "Android device is available", 273 )