1#!/usr/bin/env python3 2# 3# Copyright 2021 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""Unit tests for RemoteHostGoldfishDeviceFactory.""" 18 19import os 20import tempfile 21import unittest 22import zipfile 23 24from unittest import mock 25 26from acloud import errors 27from acloud.internal import constants 28from acloud.internal.lib import driver_test_lib 29from acloud.public.actions import remote_host_gf_device_factory as gf_factory 30 31 32class RemoteHostGoldfishDeviceFactoryTest(driver_test_lib.BaseDriverTest): 33 """Unit tests for RemoteHostGoldfishDeviceFactory.""" 34 35 _EMULATOR_INFO = "require version-emulator=111111\n" 36 _X86_64_BUILD_INFO = { 37 constants.BUILD_ID: "123456", 38 constants.BUILD_TARGET: "sdk_x86_64-sdk", 39 } 40 _X86_64_INSTANCE_NAME = ( 41 "host-goldfish-192.0.2.1-5554-123456-sdk_x86_64-sdk") 42 _ARM64_BUILD_INFO = { 43 constants.BUILD_ID: "123456", 44 constants.BUILD_TARGET: "sdk_arm64-sdk", 45 } 46 _ARM64_INSTANCE_NAME = ( 47 "host-goldfish-192.0.2.1-5554-123456-sdk_arm64-sdk") 48 _CFG_ATTRS = { 49 "ssh_private_key_path": "cfg_key_path", 50 "extra_args_ssh_tunnel": "extra args", 51 "emulator_build_target": "sdk_tools_linux", 52 } 53 _AVD_SPEC_ATTRS = { 54 "cfg": None, 55 "remote_image": _X86_64_BUILD_INFO, 56 "image_download_dir": None, 57 "host_user": "user", 58 "remote_host": "192.0.2.1", 59 "host_ssh_private_key_path": None, 60 "emulator_build_id": None, 61 "emulator_build_target": None, 62 "system_build_info": {}, 63 "kernel_build_info": {}, 64 "boot_timeout_secs": None, 65 "hw_customize": False, 66 "hw_property": {}, 67 "gpu": "auto", 68 } 69 _LOGS = [{"path": "acloud_gf/instance/kernel.log", "type": "KERNEL_LOG"}, 70 {"path": "acloud_gf/instance/logcat.txt", "type": "LOGCAT"}] 71 _SSH_COMMAND = ( 72 "'export ANDROID_PRODUCT_OUT=~/acloud_gf/image/x86_64 " 73 "ANDROID_TMP=~/acloud_gf/instance " 74 "ANDROID_BUILD_TOP=~/acloud_gf/instance ; " 75 "touch acloud_gf/instance/kernel.log ; " 76 "nohup acloud_gf/emulator/x86_64/emulator -verbose " 77 "-show-kernel -read-only -ports 5554,5555 -no-window " 78 "-logcat-output acloud_gf/instance/logcat.txt " 79 "-stdouterr-file acloud_gf/instance/kernel.log -gpu auto &'" 80 ) 81 82 def setUp(self): 83 super().setUp() 84 self._mock_ssh = mock.Mock() 85 self.Patch(gf_factory.ssh, "Ssh", return_value=self._mock_ssh) 86 self.Patch(gf_factory.goldfish_remote_host_client, 87 "GoldfishRemoteHostClient") 88 self.Patch(gf_factory.auth, "CreateCredentials") 89 # Emulator console 90 self._mock_console = mock.MagicMock() 91 self._mock_console.__enter__.return_value = self._mock_console 92 self._mock_console.Kill.side_effect = errors.DeviceConnectionError 93 ping_results = [False, True] 94 self._mock_poll_and_wait = self.Patch( 95 gf_factory.utils, 96 "PollAndWait", 97 side_effect=lambda func, **kwargs: [func() for _ in ping_results]) 98 self._mock_console.Ping.side_effect = ping_results 99 self.Patch(gf_factory.emulator_console, "RemoteEmulatorConsole", 100 return_value=self._mock_console) 101 # Android build client. 102 self._mock_android_build_client = mock.Mock() 103 self._mock_android_build_client.DownloadArtifact.side_effect = ( 104 self._MockDownloadArtifact) 105 self.Patch(gf_factory.android_build_client, "AndroidBuildClient", 106 return_value=self._mock_android_build_client) 107 # AVD spec. 108 mock_cfg = mock.Mock(spec=list(self._CFG_ATTRS.keys()), 109 **self._CFG_ATTRS) 110 self._mock_avd_spec = mock.Mock(spec=list(self._AVD_SPEC_ATTRS.keys()), 111 **self._AVD_SPEC_ATTRS) 112 self._mock_avd_spec.cfg = mock_cfg 113 114 @staticmethod 115 def _CreateSdkRepoZip(path): 116 """Create a zip file that contains a subdirectory.""" 117 with zipfile.ZipFile(path, "w") as zip_file: 118 zip_file.writestr("x86_64/build.prop", "") 119 zip_file.writestr("x86_64/test", "") 120 121 @staticmethod 122 def _CreateImageZip(path): 123 """Create a zip file containing images.""" 124 with zipfile.ZipFile(path, "w") as zip_file: 125 zip_file.writestr("system.img", "") 126 127 def _MockDownloadArtifact(self, _build_target, _build_id, resource_id, 128 local_path, _attempt): 129 if resource_id.endswith(".zip"): 130 if (resource_id.startswith("sdk-repo-") or 131 resource_id.startswith("emu-extra-")): 132 self._CreateSdkRepoZip(local_path) 133 else: 134 self._CreateImageZip(local_path) 135 elif resource_id == "emulator-info.txt": 136 with open(local_path, "w") as file: 137 file.write(self._EMULATOR_INFO) 138 else: 139 with open(local_path, "w") as file: 140 pass 141 142 def testCreateInstanceWithCfg(self): 143 """Test RemoteHostGoldfishDeviceFactory with default config.""" 144 factory = gf_factory.RemoteHostGoldfishDeviceFactory( 145 self._mock_avd_spec) 146 instance_name = factory.CreateInstance() 147 148 self.assertEqual(self._X86_64_INSTANCE_NAME, instance_name) 149 self.assertEqual(self._X86_64_BUILD_INFO, factory.GetBuildInfoDict()) 150 self.assertEqual({}, factory.GetFailures()) 151 self.assertEqual({instance_name: self._LOGS}, factory.GetLogs()) 152 # Artifacts. 153 self._mock_android_build_client.DownloadArtifact.assert_any_call( 154 "sdk_tools_linux", "111111", 155 "sdk-repo-linux-emulator-111111.zip", mock.ANY, mock.ANY) 156 self._mock_android_build_client.DownloadArtifact.assert_any_call( 157 "sdk_x86_64-sdk", "123456", 158 "sdk-repo-linux-system-images-123456.zip", mock.ANY, mock.ANY) 159 self._mock_android_build_client.DownloadArtifact.assert_any_call( 160 "sdk_x86_64-sdk", "123456", 161 "emulator-info.txt", mock.ANY, mock.ANY) 162 self.assertEqual( 163 3, self._mock_android_build_client.DownloadArtifact.call_count) 164 # Commands. 165 self._mock_ssh.Run.assert_called_with(self._SSH_COMMAND) 166 self._mock_console.Kill.assert_called_once() 167 self.assertEqual(self._mock_console.Ping.call_count, 168 self._mock_console.Reconnect.call_count + 1) 169 self._mock_console.Reconnect.assert_called() 170 171 def testCreateInstanceWithAvdSpec(self): 172 """Test RemoteHostGoldfishDeviceFactory with command options.""" 173 self._mock_avd_spec.remote_image = self._ARM64_BUILD_INFO 174 self._mock_avd_spec.host_ssh_private_key_path = "key_path" 175 self._mock_avd_spec.emulator_build_id = "999999" 176 self._mock_avd_spec.emulator_build_target = "aarch64_sdk_tools_mac" 177 self._mock_avd_spec.boot_timeout_secs = 1 178 self._mock_avd_spec.hw_customize = True 179 self._mock_avd_spec.hw_property = {"disk": "4096"} 180 self._mock_android_build_client.DownloadArtifact.side_effect = ( 181 AssertionError("DownloadArtifact should not be called.")) 182 # All artifacts are cached. 183 with tempfile.TemporaryDirectory() as download_dir: 184 self._mock_avd_spec.image_download_dir = download_dir 185 artifact_paths = ( 186 os.path.join(download_dir, "999999", 187 "aarch64_sdk_tools_mac", 188 "sdk-repo-darwin_aarch64-emulator-999999.zip"), 189 os.path.join(download_dir, "123456", 190 "sdk_arm64-sdk", 191 "sdk-repo-linux-system-images-123456.zip"), 192 ) 193 for artifact_path in artifact_paths: 194 os.makedirs(os.path.dirname(artifact_path), exist_ok=True) 195 self._CreateSdkRepoZip(artifact_path) 196 197 factory = gf_factory.RemoteHostGoldfishDeviceFactory( 198 self._mock_avd_spec) 199 instance_name = factory.CreateInstance() 200 201 self.assertEqual(self._ARM64_INSTANCE_NAME, instance_name) 202 self.assertEqual(self._ARM64_BUILD_INFO, factory.GetBuildInfoDict()) 203 self.assertEqual({}, factory.GetFailures()) 204 205 @mock.patch("acloud.public.actions.remote_host_gf_device_factory." 206 "goldfish_utils") 207 def testCreateInstanceWithSystemBuild(self, mock_gf_utils): 208 """Test RemoteHostGoldfishDeviceFactory with system build.""" 209 self._mock_avd_spec.system_build_info = { 210 constants.BUILD_ID: "111111", 211 constants.BUILD_TARGET: "aosp_x86_64-userdebug"} 212 mock_gf_utils.ConvertAvdSpecToArgs.return_value = ["-gpu", "auto"] 213 mock_gf_utils.MixWithSystemImage.return_value = "/mixed/disk" 214 mock_gf_utils.SYSTEM_QEMU_IMAGE_NAME = "system-qemu.img" 215 216 factory = gf_factory.RemoteHostGoldfishDeviceFactory( 217 self._mock_avd_spec) 218 instance_name = factory.CreateInstance() 219 # Artifacts. 220 self._mock_android_build_client.DownloadArtifact.assert_any_call( 221 "sdk_x86_64-sdk", "123456", 222 "emu-extra-linux-system-images-123456.zip", mock.ANY, mock.ANY) 223 self._mock_android_build_client.DownloadArtifact.assert_any_call( 224 "aosp_x86_64-userdebug", "111111", 225 "aosp_x86_64-img-111111.zip", mock.ANY, mock.ANY) 226 self._mock_android_build_client.DownloadArtifact.assert_any_call( 227 "sdk_x86_64-sdk", "123456", 228 "otatools.zip", mock.ANY, mock.ANY) 229 self.assertEqual( 230 5, self._mock_android_build_client.DownloadArtifact.call_count) 231 # Images. 232 mock_gf_utils.MixWithSystemImage.assert_called_once() 233 self._mock_ssh.ScpPushFile.assert_called_with( 234 "/mixed/disk", "acloud_gf/image/x86_64/system-qemu.img") 235 236 self.assertEqual(self._X86_64_INSTANCE_NAME, instance_name) 237 self.assertEqual(self._X86_64_BUILD_INFO, factory.GetBuildInfoDict()) 238 self.assertEqual({}, factory.GetFailures()) 239 240 @mock.patch("acloud.public.actions.remote_host_gf_device_factory." 241 "goldfish_utils") 242 def testCreateInstanceWithKernelBuild(self, mock_gf_utils): 243 """Test RemoteHostGoldfishDeviceFactory with kernel build.""" 244 self._mock_avd_spec.kernel_build_info = { 245 constants.BUILD_ID: "111111", 246 constants.BUILD_TARGET: "aosp_x86_64-userdebug", 247 constants.BUILD_ARTIFACT: "boot-5.10.img"} 248 mock_gf_utils.ConvertAvdSpecToArgs.return_value = ["-gpu", "auto"] 249 mock_gf_utils.MixWithBootImage.return_value = ( 250 "/path/to/kernel", "/path/to/ramdisk") 251 252 factory = gf_factory.RemoteHostGoldfishDeviceFactory( 253 self._mock_avd_spec) 254 instance_name = factory.CreateInstance() 255 # Artifacts. 256 self._mock_android_build_client.DownloadArtifact.assert_any_call( 257 "sdk_x86_64-sdk", "123456", 258 "sdk-repo-linux-system-images-123456.zip", mock.ANY, mock.ANY) 259 self._mock_android_build_client.DownloadArtifact.assert_any_call( 260 "aosp_x86_64-userdebug", "111111", 261 "boot-5.10.img", mock.ANY, mock.ANY) 262 self._mock_android_build_client.DownloadArtifact.assert_any_call( 263 "sdk_x86_64-sdk", "123456", 264 "otatools.zip", mock.ANY, mock.ANY) 265 self.assertEqual( 266 5, self._mock_android_build_client.DownloadArtifact.call_count) 267 # Images. 268 mock_gf_utils.MixWithBootImage.assert_called_once() 269 self._mock_ssh.ScpPushFile.assert_any_call( 270 "/path/to/kernel", "acloud_gf/kernel") 271 self._mock_ssh.ScpPushFile.assert_any_call( 272 "/path/to/ramdisk", "acloud_gf/mixed_ramdisk") 273 274 self.assertEqual(self._X86_64_INSTANCE_NAME, instance_name) 275 self.assertEqual(self._X86_64_BUILD_INFO, factory.GetBuildInfoDict()) 276 self.assertEqual({}, factory.GetFailures()) 277 278 def testCreateInstanceError(self): 279 """Test RemoteHostGoldfishDeviceFactory with boot error.""" 280 self._mock_console.Reconnect.side_effect = ( 281 errors.DeviceConnectionError) 282 283 factory = gf_factory.RemoteHostGoldfishDeviceFactory( 284 self._mock_avd_spec) 285 factory.CreateInstance() 286 287 failures = factory.GetFailures() 288 self.assertIsInstance(failures.get(self._X86_64_INSTANCE_NAME), 289 errors.DeviceBootError) 290 self.assertEqual({self._X86_64_INSTANCE_NAME: self._LOGS}, 291 factory.GetLogs()) 292 293 def testCreateInstanceTimeout(self): 294 """Test RemoteHostGoldfishDeviceFactory with timeout.""" 295 self._mock_avd_spec.boot_timeout_secs = 1 296 self._mock_poll_and_wait.side_effect = errors.DeviceBootTimeoutError() 297 298 factory = gf_factory.RemoteHostGoldfishDeviceFactory( 299 self._mock_avd_spec) 300 factory.CreateInstance() 301 302 self._mock_poll_and_wait.assert_called_once_with( 303 func=mock.ANY, 304 expected_return=True, 305 timeout_exception=errors.DeviceBootTimeoutError, 306 timeout_secs=1, 307 sleep_interval_secs=mock.ANY) 308 failures = factory.GetFailures() 309 self.assertIsInstance(failures.get(self._X86_64_INSTANCE_NAME), 310 errors.DeviceBootTimeoutError) 311 self.assertEqual({self._X86_64_INSTANCE_NAME: self._LOGS}, 312 factory.GetLogs()) 313 314 315if __name__ == "__main__": 316 unittest.main() 317