• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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-5556-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": "emulator-linux_x64_nolocationui",
52    }
53    _AVD_SPEC_ATTRS = {
54        "cfg": None,
55        "image_source": constants.IMAGE_SRC_REMOTE,
56        "remote_image": _X86_64_BUILD_INFO,
57        "image_download_dir": None,
58        "host_user": "user",
59        "remote_host": "192.0.2.1",
60        "host_ssh_private_key_path": None,
61        "emulator_build_id": None,
62        "emulator_build_target": None,
63        "emulator_zip": None,
64        "system_build_info": {},
65        "boot_build_info": {},
66        "local_image_artifact": None,
67        "local_kernel_image": None,
68        "local_system_image": None,
69        "local_system_dlkm_image": None,
70        "local_tool_dirs": [],
71        "base_instance_num": None,
72        "boot_timeout_secs": None,
73        "hw_customize": False,
74        "hw_property": {},
75        "gpu": "auto",
76    }
77    _LOGS = [{"path": "acloud_gf_1/instance/kernel.log", "type": "KERNEL_LOG"},
78             {"path": "acloud_gf_1/instance/emu_stderr.txt", "type": "TEXT"},
79             {"path": "acloud_gf_1/instance/logcat.txt", "type": "LOGCAT"}]
80    _VERIFIED_BOOT_PARAMS_COMMAND = (
81        "'test -f acloud_gf_1/image/x86_64/VerifiedBootParams.textproto && "
82        r'echo -e \\nparam: \"androidboot.verifiedbootstate=orange\" >> '
83        "acloud_gf_1/image/x86_64/VerifiedBootParams.textproto'"
84    )
85    _SSH_COMMAND = (
86        "'export ANDROID_PRODUCT_OUT=~/acloud_gf_1/image/x86_64 "
87        "ANDROID_TMP=~/acloud_gf_1/instance "
88        "ANDROID_BUILD_TOP=~/acloud_gf_1/instance ; "
89        "nohup acloud_gf_1/emulator/emulator/emulator -verbose "
90        "-show-kernel -read-only -ports 5554,5555 -no-window "
91        "-logcat-output acloud_gf_1/instance/logcat.txt -gpu auto "
92        "1> acloud_gf_1/instance/kernel.log "
93        "2> acloud_gf_1/instance/emu_stderr.txt &'"
94    )
95
96    def setUp(self):
97        super().setUp()
98        self._mock_ssh = mock.Mock()
99        self.Patch(gf_factory.ssh, "Ssh", return_value=self._mock_ssh)
100        self._mock_remote_host_client = mock.Mock()
101        self.Patch(gf_factory.remote_host_client, "RemoteHostClient",
102                   return_value=self._mock_remote_host_client)
103        self._mock_create_credentials = self.Patch(
104            gf_factory.auth, "CreateCredentials")
105        # Emulator console
106        self._mock_console = mock.MagicMock()
107        self._mock_console.__enter__.return_value = self._mock_console
108        self._mock_console.Kill.side_effect = errors.DeviceConnectionError
109        ping_results = [False, True]
110        self._mock_poll_and_wait = self.Patch(
111            gf_factory.utils,
112            "PollAndWait",
113            side_effect=lambda func, **kwargs: [func() for _ in ping_results])
114        self._mock_console.Ping.side_effect = ping_results
115        self.Patch(gf_factory.emulator_console, "RemoteEmulatorConsole",
116                   return_value=self._mock_console)
117        # Android build client.
118        self._mock_android_build_client = mock.Mock()
119        self._mock_android_build_client.DownloadArtifact.side_effect = (
120            self._MockDownloadArtifact)
121        self.Patch(gf_factory.android_build_client, "AndroidBuildClient",
122                   return_value=self._mock_android_build_client)
123        # AVD spec.
124        mock_cfg = mock.Mock(spec=list(self._CFG_ATTRS.keys()),
125                             **self._CFG_ATTRS)
126        self._mock_avd_spec = mock.Mock(spec=list(self._AVD_SPEC_ATTRS.keys()),
127                                        **self._AVD_SPEC_ATTRS)
128        self._mock_avd_spec.cfg = mock_cfg
129
130    @staticmethod
131    def _CreateSdkRepoZip(path):
132        """Create a zip file that contains a subdirectory."""
133        with zipfile.ZipFile(path, "w") as zip_file:
134            zip_file.writestr("x86_64/build.prop", "")
135            zip_file.writestr("x86_64/test", "")
136
137    @staticmethod
138    def _CreateImageZip(path):
139        """Create a zip file containing images."""
140        with zipfile.ZipFile(path, "w") as zip_file:
141            zip_file.writestr("system.img", "")
142
143    def _MockDownloadArtifact(self, _build_target, _build_id, resource_id,
144                              local_path, _attempt):
145        if resource_id.endswith(".zip"):
146            if (resource_id.startswith("sdk-repo-") or
147                    resource_id.startswith("emu-extra-")):
148                self._CreateSdkRepoZip(local_path)
149            else:
150                self._CreateImageZip(local_path)
151        elif resource_id == "emulator-info.txt":
152            with open(local_path, "w", encoding="utf-8") as file:
153                file.write(self._EMULATOR_INFO)
154        else:
155            with open(local_path, "w", encoding="utf-8") as file:
156                pass
157
158    def testCreateInstanceWithCfg(self):
159        """Test RemoteHostGoldfishDeviceFactory with default config."""
160        factory = gf_factory.RemoteHostGoldfishDeviceFactory(
161            self._mock_avd_spec)
162        instance_name = factory.CreateInstance()
163
164        self.assertEqual(self._X86_64_INSTANCE_NAME, instance_name)
165        self.assertEqual(self._X86_64_BUILD_INFO, factory.GetBuildInfoDict())
166        self.assertEqual([5555], factory.GetAdbPorts())
167        self.assertEqual([None], factory.GetVncPorts())
168        self.assertEqual({}, factory.GetFailures())
169        self.assertEqual({instance_name: self._LOGS}, factory.GetLogs())
170        # Artifacts.
171        self._mock_android_build_client.DownloadArtifact.assert_any_call(
172            "emulator-linux_x64_nolocationui", "111111",
173            "sdk-repo-linux-emulator-111111.zip", mock.ANY, mock.ANY)
174        self._mock_android_build_client.DownloadArtifact.assert_any_call(
175            "sdk_x86_64-sdk", "123456",
176            "sdk-repo-linux-system-images-123456.zip", mock.ANY, mock.ANY)
177        self._mock_android_build_client.DownloadArtifact.assert_any_call(
178            "sdk_x86_64-sdk", "123456",
179            "emulator-info.txt", mock.ANY, mock.ANY)
180        self.assertEqual(
181            3, self._mock_android_build_client.DownloadArtifact.call_count)
182        # Commands.
183        self._mock_ssh.Run.assert_called_with(self._SSH_COMMAND)
184        self._mock_console.Kill.assert_called_once()
185        self.assertEqual(self._mock_console.Ping.call_count,
186                         self._mock_console.Reconnect.call_count + 1)
187        self._mock_console.Reconnect.assert_called()
188        # RemoteHostClient.
189        self._mock_remote_host_client.RecordTime.assert_has_calls([
190            mock.call(constants.TIME_GCE, mock.ANY),
191            mock.call(constants.TIME_ARTIFACT, mock.ANY),
192            mock.call(constants.TIME_LAUNCH, mock.ANY)])
193        self.assertEqual(3,
194                         self._mock_remote_host_client.RecordTime.call_count)
195        self._mock_remote_host_client.SetStage.assert_has_calls([
196            mock.call(constants.STAGE_SSH_CONNECT),
197            mock.call(constants.STAGE_ARTIFACT),
198            mock.call(constants.STAGE_BOOT_UP)])
199        self.assertEqual(3, self._mock_remote_host_client.SetStage.call_count)
200
201    def testCreateInstanceWithAvdSpec(self):
202        """Test RemoteHostGoldfishDeviceFactory with command options."""
203        self._mock_avd_spec.remote_image = self._ARM64_BUILD_INFO
204        self._mock_avd_spec.host_ssh_private_key_path = "key_path"
205        self._mock_avd_spec.emulator_build_id = "999999"
206        self._mock_avd_spec.emulator_build_target = "aarch64_sdk_tools_mac"
207        self._mock_avd_spec.base_instance_num = 2
208        self._mock_avd_spec.boot_timeout_secs = 1
209        self._mock_avd_spec.hw_customize = True
210        self._mock_avd_spec.hw_property = {"disk": "4096"}
211        self._mock_create_credentials.side_effect = AssertionError(
212            "CreateCredentials should not be called.")
213        # All artifacts are cached.
214        with tempfile.TemporaryDirectory() as download_dir:
215            self._mock_avd_spec.image_download_dir = download_dir
216            artifact_paths = (
217                os.path.join(download_dir, "999999",
218                             "aarch64_sdk_tools_mac",
219                             "sdk-repo-darwin_aarch64-emulator-999999.zip"),
220                os.path.join(download_dir, "123456",
221                             "sdk_arm64-sdk",
222                             "sdk-repo-linux-system-images-123456.zip"),
223            )
224            for artifact_path in artifact_paths:
225                os.makedirs(os.path.dirname(artifact_path), exist_ok=True)
226                self._CreateSdkRepoZip(artifact_path)
227
228            factory = gf_factory.RemoteHostGoldfishDeviceFactory(
229                self._mock_avd_spec)
230            instance_name = factory.CreateInstance()
231
232        self.assertEqual(self._ARM64_INSTANCE_NAME, instance_name)
233        self.assertEqual(self._ARM64_BUILD_INFO, factory.GetBuildInfoDict())
234        self.assertEqual([5557], factory.GetAdbPorts())
235        self.assertEqual([None], factory.GetVncPorts())
236        self.assertEqual({}, factory.GetFailures())
237
238    @mock.patch("acloud.public.actions.remote_host_gf_device_factory."
239                "goldfish_utils")
240    def testCreateInstanceWithSystemBuild(self, mock_gf_utils):
241        """Test RemoteHostGoldfishDeviceFactory with system build."""
242        self._mock_avd_spec.system_build_info = {
243            constants.BUILD_ID: "111111",
244            constants.BUILD_TARGET: "aosp_x86_64-userdebug"}
245        mock_gf_utils.ConvertAvdSpecToArgs.return_value = ["-gpu", "auto"]
246        mock_gf_utils.MixDiskImage.return_value = "/mixed/disk"
247        mock_gf_utils.SYSTEM_QEMU_IMAGE_NAME = "system-qemu.img"
248        mock_gf_utils.VERIFIED_BOOT_PARAMS_FILE_NAME = (
249            "VerifiedBootParams.textproto")
250
251        factory = gf_factory.RemoteHostGoldfishDeviceFactory(
252            self._mock_avd_spec)
253        factory.CreateInstance()
254        # Artifacts.
255        self._mock_android_build_client.DownloadArtifact.assert_any_call(
256            "sdk_x86_64-sdk", "123456",
257            "emu-extra-linux-system-images-123456.zip", mock.ANY, mock.ANY)
258        self._mock_android_build_client.DownloadArtifact.assert_any_call(
259            "aosp_x86_64-userdebug", "111111",
260            "aosp_x86_64-img-111111.zip", mock.ANY, mock.ANY)
261        self._mock_android_build_client.DownloadArtifact.assert_any_call(
262            "sdk_x86_64-sdk", "123456",
263            "otatools.zip", mock.ANY, mock.ANY)
264        self.assertEqual(
265            5, self._mock_android_build_client.DownloadArtifact.call_count)
266        # Images.
267        mock_gf_utils.MixDiskImage.assert_called_once()
268        self._mock_ssh.ScpPushFile.assert_called_with(
269            "/mixed/disk", "acloud_gf_1/image/x86_64/system-qemu.img")
270        self._mock_ssh.Run.assert_any_call(self._VERIFIED_BOOT_PARAMS_COMMAND)
271
272        mock_gf_utils.FormatRemoteHostInstanceName.assert_called()
273        self.assertEqual(self._X86_64_BUILD_INFO, factory.GetBuildInfoDict())
274        self.assertEqual([5555], factory.GetAdbPorts())
275        self.assertEqual([None], factory.GetVncPorts())
276        self.assertEqual({}, factory.GetFailures())
277
278    @mock.patch("acloud.public.actions.remote_host_gf_device_factory."
279                "goldfish_utils")
280    def testCreateInstanceWithBootBuild(self, mock_gf_utils):
281        """Test RemoteHostGoldfishDeviceFactory with boot build."""
282        self._mock_avd_spec.boot_build_info = {
283            constants.BUILD_ID: "111111",
284            constants.BUILD_TARGET: "gki_x86_64-userdebug",
285            constants.BUILD_ARTIFACT: "boot-5.10.img"}
286        mock_gf_utils.ConvertAvdSpecToArgs.return_value = ["-gpu", "auto"]
287        mock_gf_utils.MixWithBootImage.return_value = (
288            "/path/to/kernel", "/path/to/ramdisk")
289
290        factory = gf_factory.RemoteHostGoldfishDeviceFactory(
291            self._mock_avd_spec)
292        factory.CreateInstance()
293        # Artifacts.
294        self._mock_android_build_client.DownloadArtifact.assert_any_call(
295            "sdk_x86_64-sdk", "123456",
296            "sdk-repo-linux-system-images-123456.zip", mock.ANY, mock.ANY)
297        self._mock_android_build_client.DownloadArtifact.assert_any_call(
298            "gki_x86_64-userdebug", "111111",
299            "boot-5.10.img", mock.ANY, mock.ANY)
300        self._mock_android_build_client.DownloadArtifact.assert_any_call(
301            "sdk_x86_64-sdk", "123456",
302            "otatools.zip", mock.ANY, mock.ANY)
303        self.assertEqual(
304            5, self._mock_android_build_client.DownloadArtifact.call_count)
305        # Images.
306        mock_gf_utils.MixWithBootImage.assert_called_once()
307        self._mock_ssh.ScpPushFile.assert_any_call(
308            "/path/to/kernel", "acloud_gf_1/kernel")
309        self._mock_ssh.ScpPushFile.assert_any_call(
310            "/path/to/ramdisk", "acloud_gf_1/mixed_ramdisk")
311
312        mock_gf_utils.FormatRemoteHostInstanceName.assert_called()
313        self.assertEqual(self._X86_64_BUILD_INFO, factory.GetBuildInfoDict())
314        self.assertEqual([5555], factory.GetAdbPorts())
315        self.assertEqual([None], factory.GetVncPorts())
316        self.assertEqual({}, factory.GetFailures())
317
318    @mock.patch("acloud.public.actions.remote_host_gf_device_factory."
319                "ota_tools")
320    @mock.patch("acloud.public.actions.remote_host_gf_device_factory."
321                "goldfish_utils")
322    def testCreateInstanceWithLocalFiles(self, mock_gf_utils, mock_ota_tools):
323        """Test RemoteHostGoldfishDeviceFactory with local files."""
324        with tempfile.TemporaryDirectory() as temp_dir:
325            emulator_zip_path = os.path.join(temp_dir, "emulator.zip")
326            self._CreateSdkRepoZip(emulator_zip_path)
327            image_zip_path = os.path.join(temp_dir, "image.zip")
328            self._CreateSdkRepoZip(image_zip_path)
329            boot_image_path = os.path.join(temp_dir, "boot.img")
330            self.CreateFile(boot_image_path, b"ANDROID!")
331            system_image_path = os.path.join(temp_dir, "system.img")
332            self.CreateFile(system_image_path)
333            system_dlkm_image_path = os.path.join(temp_dir, "system_dlkm.img")
334            self.CreateFile(system_dlkm_image_path)
335            self._mock_avd_spec.emulator_zip = emulator_zip_path
336            self._mock_avd_spec.image_source = constants.IMAGE_SRC_LOCAL
337            self._mock_avd_spec.remote_image = {}
338            self._mock_avd_spec.local_image_artifact = image_zip_path
339            self._mock_avd_spec.local_kernel_image = boot_image_path
340            self._mock_avd_spec.local_system_image = system_image_path
341            self._mock_avd_spec.local_system_dlkm_image = (
342                system_dlkm_image_path)
343            self._mock_avd_spec.local_tool_dirs.append("/otatools")
344            mock_gf_utils.ConvertAvdSpecToArgs.return_value = ["-gpu", "auto"]
345            mock_gf_utils.MixWithBootImage.return_value = (
346                "/path/to/kernel", "/path/to/ramdisk")
347            self._mock_create_credentials.side_effect = AssertionError(
348                "CreateCredentials should not be called.")
349
350            factory = gf_factory.RemoteHostGoldfishDeviceFactory(
351                self._mock_avd_spec)
352            factory.CreateInstance()
353
354            mock_gf_utils.FindSystemDlkmImage.assert_called_once()
355            mock_gf_utils.MixWithBootImage.assert_called_once()
356            mock_gf_utils.MixDiskImage.assert_called_once()
357            mock_ota_tools.FindOtaToolsDir.assert_called_once()
358            self.assertEqual("/otatools",
359                             mock_ota_tools.FindOtaToolsDir.call_args[0][0][0])
360
361            mock_gf_utils.FormatRemoteHostInstanceName.assert_called()
362            self.assertEqual({}, factory.GetBuildInfoDict())
363            self.assertEqual([5555], factory.GetAdbPorts())
364            self.assertEqual([None], factory.GetVncPorts())
365            self.assertEqual({}, factory.GetFailures())
366
367    def testCreateInstanceInitError(self):
368        """Test RemoteHostGoldfishDeviceFactory with SSH error."""
369        self._mock_ssh.Run.side_effect = errors.DeviceConnectionError
370
371        factory = gf_factory.RemoteHostGoldfishDeviceFactory(
372            self._mock_avd_spec)
373        factory.CreateInstance()
374
375        failures = factory.GetFailures()
376        self.assertIsInstance(failures.get(self._X86_64_INSTANCE_NAME),
377                              errors.DeviceConnectionError)
378        self.assertEqual({}, factory.GetLogs())
379        self._mock_remote_host_client.RecordTime.assert_called_once_with(
380            constants.TIME_GCE, mock.ANY)
381        self._mock_remote_host_client.SetStage.assert_called_once_with(
382            constants.STAGE_SSH_CONNECT)
383
384    def testCreateInstanceDownloadError(self):
385        """Test RemoteHostGoldfishDeviceFactory with download error."""
386        self._mock_android_build_client.DownloadArtifact.side_effect = (
387            errors.DriverError)
388
389        factory = gf_factory.RemoteHostGoldfishDeviceFactory(
390            self._mock_avd_spec)
391        factory.CreateInstance()
392
393        failures = factory.GetFailures()
394        self.assertIsInstance(failures.get(self._X86_64_INSTANCE_NAME),
395                              errors.DriverError)
396        self.assertEqual({}, factory.GetLogs())
397        self._mock_remote_host_client.RecordTime.assert_has_calls([
398            mock.call(constants.TIME_GCE, mock.ANY),
399            mock.call(constants.TIME_ARTIFACT, mock.ANY)])
400        self.assertEqual(2,
401                         self._mock_remote_host_client.RecordTime.call_count)
402        self._mock_remote_host_client.SetStage.assert_has_calls([
403            mock.call(constants.STAGE_SSH_CONNECT),
404            mock.call(constants.STAGE_ARTIFACT)])
405        self.assertEqual(2, self._mock_remote_host_client.SetStage.call_count)
406
407    def testCreateInstanceBootError(self):
408        """Test RemoteHostGoldfishDeviceFactory with boot error."""
409        self._mock_console.Reconnect.side_effect = (
410            errors.DeviceConnectionError)
411
412        factory = gf_factory.RemoteHostGoldfishDeviceFactory(
413            self._mock_avd_spec)
414        factory.CreateInstance()
415
416        failures = factory.GetFailures()
417        self.assertIsInstance(failures.get(self._X86_64_INSTANCE_NAME),
418                              errors.DeviceBootError)
419        self.assertEqual({self._X86_64_INSTANCE_NAME: self._LOGS},
420                         factory.GetLogs())
421        self.assertEqual(3,
422                         self._mock_remote_host_client.RecordTime.call_count)
423        self.assertEqual(3, self._mock_remote_host_client.SetStage.call_count)
424
425    def testCreateInstanceTimeout(self):
426        """Test RemoteHostGoldfishDeviceFactory with timeout."""
427        self._mock_avd_spec.boot_timeout_secs = 1
428        self._mock_poll_and_wait.side_effect = errors.DeviceBootTimeoutError()
429
430        factory = gf_factory.RemoteHostGoldfishDeviceFactory(
431            self._mock_avd_spec)
432        factory.CreateInstance()
433
434        self._mock_poll_and_wait.assert_called_once_with(
435            func=mock.ANY,
436            expected_return=True,
437            timeout_exception=errors.DeviceBootTimeoutError,
438            timeout_secs=1,
439            sleep_interval_secs=mock.ANY)
440        failures = factory.GetFailures()
441        self.assertIsInstance(failures.get(self._X86_64_INSTANCE_NAME),
442                              errors.DeviceBootTimeoutError)
443        self.assertEqual({self._X86_64_INSTANCE_NAME: self._LOGS},
444                         factory.GetLogs())
445        self.assertEqual(3,
446                         self._mock_remote_host_client.RecordTime.call_count)
447        self.assertEqual(3, self._mock_remote_host_client.SetStage.call_count)
448
449
450if __name__ == "__main__":
451    unittest.main()
452