blob: 17c7c1c2521655bda2bef9bca75ea4bd79c81921 [file] [log] [blame]
# Lint as: python2, python3
# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import unittest
import os
import tempfile
import shutil
import stat
from unittest.mock import Mock, ANY, patch, call
from autotest_lib.client.common_lib import error
from autotest_lib.server.cros.tradefed import tradefed_test
from autotest_lib.server.utils import CmdResult
class TradefedTestTest(unittest.TestCase):
"""Tests for TradefedTest class."""
def setUp(self):
self._mockjob_tmpdirs = []
self._bindir = tempfile.mkdtemp()
self._outputdir = tempfile.mkdtemp()
self.mock_adb = Mock()
self.tradefed = tradefed_test.TradefedTest(self.create_mock_job(),
self._bindir,
self._outputdir,
adb=self.mock_adb)
def tearDown(self):
shutil.rmtree(self._bindir)
shutil.rmtree(self._outputdir)
for tmpdir in self._mockjob_tmpdirs:
shutil.rmtree(tmpdir)
def create_mock_job(self):
"""Creates a mock necessary for constructing tradefed_test instance."""
mock_job = Mock()
mock_job.pkgmgr = None
mock_job.autodir = None
mock_job.tmpdir = tempfile.mkdtemp()
self._mockjob_tmpdirs.append(mock_job.tmpdir)
return mock_job
def test_try_adb_connect(self):
"""Test when try_adb_connect succeeds."""
self.mock_adb.get_adb_target.return_value = '123.76.0.29:3467'
adb_run_retvals = [
# adb connect
CmdResult(exit_status=0),
# adb devices
CmdResult(exit_status=0, stdout=f'123.76.0.29:3467 device\n'),
# adb shell exit
CmdResult(exit_status=0),
]
self.mock_adb.run.side_effect = adb_run_retvals
self.assertTrue(self.tradefed._try_adb_connect(Mock()))
self.mock_adb.run.assert_has_calls([
call(ANY,
args=('connect', '123.76.0.29:3467'),
verbose=ANY,
env=ANY,
ignore_status=ANY,
timeout=ANY),
call(ANY, args=('devices', ), env=ANY, timeout=ANY),
call(ANY,
args=('shell', 'exit'),
env=ANY,
ignore_status=ANY,
timeout=ANY),
])
def test_try_adb_connect_run_adb_fail(self):
"""Verify that try_adb_connect fails when run_adb_cmd fails."""
mock_run_adb_cmd = self.mock_adb.run
# Exit status is set to non-0 to exit _try_adb_connect() early.
mock_run_adb_cmd.return_value.exit_status = 1
self.mock_adb.get_adb_target.return_value = '123.76.0.29:3467'
self.assertFalse(self.tradefed._try_adb_connect(Mock()))
mock_run_adb_cmd.assert_called_with(ANY,
args=('connect',
'123.76.0.29:3467'),
verbose=ANY,
env=ANY,
ignore_status=ANY,
timeout=ANY)
def test_try_adb_connect_offline_disconnect(self):
"""Verify if try_adb_connect disconnects if device is offline."""
self.mock_adb.get_adb_target.return_value = '123.76.0.29:3467'
adb_run_retvals = [
# adb connect
CmdResult(exit_status=0),
# adb devices
CmdResult(exit_status=0, stdout=f'123.76.0.29:3467 offline\n'),
# adb disconnect
CmdResult(exit_status=0),
]
self.mock_adb.run.side_effect = adb_run_retvals
self.assertFalse(self.tradefed._try_adb_connect(Mock()))
self.mock_adb.run.assert_has_calls([
call(ANY,
args=('connect', '123.76.0.29:3467'),
verbose=ANY,
env=ANY,
ignore_status=ANY,
timeout=ANY),
call(ANY, args=('devices', ), env=ANY, timeout=ANY),
call(ANY,
args=('disconnect', '123.76.0.29:3467'),
verbose=ANY,
env=ANY,
ignore_status=ANY,
timeout=ANY),
])
# Verify that _run_tradefed_with_timeout works.
@patch('autotest_lib.server.cros.tradefed.tradefed_test.TradefedTest._run')
@patch('autotest_lib.server.cros.tradefed.tradefed_test.TradefedTest._tradefed_cmd_path'
)
@patch('autotest_lib.server.cros.tradefed.tradefed_utils.adb_keepalive')
def test_run_tradefed_with_timeout(self, _, mock_tradefed_cmd_path,
mock_run):
self.tradefed._install_paths = '/any/install/path'
mock_host1 = Mock()
mock_host2 = Mock()
self.tradefed._hosts = [mock_host1, mock_host2]
self.mock_adb.get_adb_targets.return_value = ['host1:4321', 'host2:22']
mock_tradefed_cmd_path.return_value = '/any/path'
self.tradefed._run_tradefed_with_timeout(['command'], 1234)
self.mock_adb.get_adb_targets.assert_called_with(self.tradefed._hosts)
def test_kill_adb_server(self):
mock_run = self.mock_adb.run
self.tradefed._kill_adb_server()
mock_run.assert_called_with(None,
args=('kill-server', ),
timeout=ANY,
verbose=ANY)
def test_verify_arc_hosts_single_host(self):
mock_run = self.mock_adb.run
mock_host = Mock()
self.tradefed._hosts = [mock_host]
self.tradefed._verify_arc_hosts()
mock_run.assert_called_with(mock_host,
args=('shell', 'getprop',
'ro.build.fingerprint'))
# Verify that multiple hosts with differet fingerprints fail.
def test_verify_arc_hosts_different_fingerprints(self):
mock_run = self.mock_adb.run
mock_host1 = Mock()
mock_host2 = Mock()
self.tradefed._hosts = [mock_host1, mock_host2]
side_effects = [Mock(), Mock()]
side_effects[0].stdout = 'fingerprint1'
side_effects[1].stdout = 'fingerprint2'
mock_run.side_effect = side_effects
self.assertRaises(error.TestFail, self.tradefed._verify_arc_hosts)
mock_run.assert_any_call(mock_host1,
args=('shell', 'getprop',
'ro.build.fingerprint'))
mock_run.assert_any_call(mock_host2,
args=('shell', 'getprop',
'ro.build.fingerprint'))
# Verify that wait for arc boot uses polling with adb.
@patch('autotest_lib.server.utils.poll_for_condition')
def test_wait_for_arc_boot(self, mock_poll_for_condition):
mock_run = self.mock_adb.run
# stdout just has to be something that evaluates to True.
mock_run.return_value.stdout = 'anything'
mock_host = Mock()
self.tradefed._wait_for_arc_boot(mock_host)
self.assertEqual(mock_run.call_count, 0)
# Verify that the condition function uses the expected adb command.
self.assertEqual(mock_poll_for_condition.call_count, 1)
args = mock_poll_for_condition.call_args[0]
condition_func = args[0]
self.assertTrue(condition_func())
mock_run.assert_called_with(mock_host,
args=('shell', 'pgrep', '-f',
'org.chromium.arc.intent_helper'),
ignore_status=True)
def test_disable_adb_install_dialog_android_version_over_29(self):
mock_run = self.mock_adb.run
mock_run.return_value.stdout = 'disabled'
self.tradefed._android_version = 30
mock_host = Mock()
self.tradefed._disable_adb_install_dialog(mock_host)
mock_run.assert_has_calls([
call(mock_host,
args=('shell', 'settings', 'put', 'global',
'verifier_verify_adb_installs', '0'),
verbose=ANY)
])
def test_disable_adb_install_dialog_android_version_under_29(self):
mock_run = self.mock_adb.run
mock_run.return_value.stdout = 'disabled'
self.tradefed._android_version = 28
mock_host = Mock()
self.tradefed._disable_adb_install_dialog(mock_host)
mock_run.assert_has_calls([
call(mock_host,
args=('shell', 'settings', 'put', 'global',
'verifier_verify_adb_installs', '0'),
verbose=ANY)
])
mock_host.run.assert_called_with(
'android-sh -c \'setprop persist.sys.disable_rescue true\'')
def test_fetch_helpers_from_dut(self):
mock_run = self.mock_adb.run
self.tradefed._repository = '/repo/path'
mock_host = Mock()
self.tradefed._hosts = [mock_host]
# '::' is intentional and should be skipped.
mock_run.return_value.stdout = 'package1:package2::package3'
self.tradefed._fetch_helpers_from_dut()
mock_run.assert_called_with(
mock_host,
args=('shell', 'getprop',
'ro.vendor.cts_interaction_helper_packages'))
self.assertEqual(mock_host.get_file.call_count, 3)
mock_host.get_file.assert_any_call(
'/usr/local/opt/google/vms/android/package1.apk',
'/repo/path/testcases',
)
mock_host.get_file.assert_any_call(
'/usr/local/opt/google/vms/android/package2.apk',
'/repo/path/testcases',
)
mock_host.get_file.assert_any_call(
'/usr/local/opt/google/vms/android/package3.apk',
'/repo/path/testcases',
)
def test_get_abilist(self):
mock_run = self.mock_adb.run
mock_host = Mock()
self.tradefed._hosts = [mock_host]
mock_run.return_value.stdout = 'arm,x86,my_awesome_architecture'
self.assertEqual(['arm', 'x86', 'my_awesome_architecture'],
self.tradefed._get_abilist())
mock_run.assert_called_with(mock_host,
args=('shell', 'getprop',
'ro.product.cpu.abilist'))
def test_copy_extra_artifacts_dut(self):
mock_run = self.mock_adb.run
mock_host = Mock()
extra_artifacts = ['artifacts', '/path/to/some/file']
self.tradefed._copy_extra_artifacts_dut(extra_artifacts, mock_host,
self._outputdir)
self.assertEqual(mock_run.call_count, 2)
mock_run.assert_any_call(
mock_host,
args=('pull', 'artifacts', self._outputdir),
verbose=ANY,
timeout=ANY,
)
mock_run.assert_any_call(
mock_host,
args=('pull', '/path/to/some/file', self._outputdir),
verbose=ANY,
timeout=ANY,
)
# TODO(rkuroiwa): This test was added to test Adb.add_path.
# So most of these tradefed_test functions are mocked because
# they are not ncecessarily ready to be tested.
# Once the rest of the modules are tested, reevaluate unmocking them.
@patch('os.chmod')
@patch('autotest_lib.server.cros.tradefed.tradefed_test.TradefedTest._instance_copyfile'
)
@patch('autotest_lib.server.cros.tradefed.tradefed_test.TradefedTest._validate_download_cache'
)
@patch('autotest_lib.server.cros.tradefed.tradefed_test.TradefedTest._download_to_cache'
)
@patch('autotest_lib.server.cros.tradefed.tradefed_test.TradefedTest._invalidate_download_cache'
)
@patch('autotest_lib.server.cros.tradefed.tradefed_utils.lock')
def test_install_files(self, mock_lock, mock_invalidate_download_cache,
mock_download_to_cache,
mock_validate_download_cache, mock_instance_copy,
mock_chmod):
mock_add_path = self.mock_adb.add_path
self.tradefed._tradefed_cache_lock = '/lock/lock_file'
self.tradefed._install_paths = []
mock_download_to_cache.return_value = '/path/to/downloaded/file'
mock_instance_copy.return_value = '/path/to/local/downloaded_file'
self.tradefed._install_files('gs://mybucket/path/to/dir', ['anyfile'],
stat.S_IRWXU)
mock_lock.assert_called_with('/lock/lock_file')
mock_invalidate_download_cache.assert_called()
mock_validate_download_cache.assert_called()
mock_chmod.assert_called_with('/path/to/local/downloaded_file',
stat.S_IRWXU)
mock_add_path.assert_called_with('/path/to/local')
self.assertEqual(self.tradefed._install_paths, ['/path/to/local'])
@patch('autotest_lib.server.utils.run')
@patch('os.renames')
@patch('tempfile.mkdtemp')
@patch('os.path.isdir')
def test_unzip_no_password(self, mock_isdir, mock_mkdtemp, mock_renames,
mock_run):
mock_isdir.return_value = False
mock_mkdtemp.return_value = '/a/temp/dir'
self.tradefed._unzip('/path/to/archive.zip')
mock_run.assert_called_with('unzip',
args=('-d', '/a/temp/dir',
'/path/to/archive.zip'))
@patch('autotest_lib.server.utils.run')
@patch('os.renames')
@patch('tempfile.mkdtemp')
@patch('os.path.isdir')
def test_unzip_with_password(self, mock_isdir, mock_mkdtemp, mock_renames,
mock_run):
mock_isdir.return_value = False
mock_mkdtemp.return_value = '/a/temp/dir'
self.tradefed._unzip('/path/to/archive.zip', 'extraction_password!!')
mock_run.assert_called_with('unzip',
args=('-P', 'extraction_password!!', '-d',
'/a/temp/dir',
'/path/to/archive.zip'))
# Verify that parsing gsutil ls -L output correctly extracts the ETag and
# returns in hex.
@patch('autotest_lib.server.utils.run')
def test_parse_ETag(self, mock_run):
mock_run.return_value = Mock(
stdout="""gs://path/to/a.zip:
Creation time: Wed, 15 Jun 2022 16:53:14 GMT
Update time: Wed, 15 Jun 2022 16:53:14 GMT
Storage class: STANDARD
Content-Language: en
Content-Length: 201832881
Content-Type: application/zip
Hash (crc32c): TN1ctw==
Hash (md5): ZArA7Yt7EREmkFmTanLHdA==
ETag: COOOrtv1r/gCEAE=
TOTAL: 1 objects, 201832881 bytes (192.48 MiB) """)
etag_hex = tradefed_test._GetETagFromGsUri('gs://path/to/a.zip')
self.assertEqual(etag_hex, '434f4f4f72747631722f67434541453d')
# For the first time, it should call _download_to_dir to download the
# bundle.
@patch('autotest_lib.server.cros.tradefed.tradefed_test.TradefedTest._download_to_dir'
)
@patch('autotest_lib.server.utils.run')
@patch('os.path.exists')
def test_download_to_cache_initial_download(self, mock_exists, mock_run,
mock_download_to_dir):
mock_run.return_value = Mock(
stdout='ETag: COOOrtv1r/gCEAE=')
mock_exists.return_value = False
self.tradefed._tradefed_cache = '/any/test/dir'
self.tradefed._download_to_cache(
'gs://some-fake-bucket/path/to/bundle.zip')
mock_download_to_dir.assert_called_with(
'gs://some-fake-bucket/path/to/bundle.zip',
os.path.join(self.tradefed._tradefed_cache,
'434f4f4f72747631722f67434541453d'))
mock_run.assert_called_with(
'gsutil',
args=('ls', '-L',
'gs://some-fake-bucket/path/to/bundle.zip'),
verbose=ANY)
# Redownload a same-name-bundle that had been downloaded, but different
# Etag.
@patch('autotest_lib.server.cros.tradefed.tradefed_test.TradefedTest._download_to_dir'
)
@patch('autotest_lib.server.utils.run')
@patch('os.path.exists')
def test_download_to_cache_same_google_storage_path_different_bundle(
self, mock_exists, mock_run, mock_download_to_dir):
mock_run.return_value = Mock(
stdout='ETag: COOOrtv1r/gCEAE=')
# Let os.path.exist return false. Must also check what value was passed to the mock (below).
mock_exists.return_value = False
self.tradefed._tradefed_cache = '/any/test/dir'
self.tradefed._download_to_cache(
'gs://some-fake-bucket/path/to/bundle.zip')
mock_download_to_dir.assert_called_with(
'gs://some-fake-bucket/path/to/bundle.zip',
os.path.join(self.tradefed._tradefed_cache,
'434f4f4f72747631722f67434541453d'))
mock_exists.assert_called_with(
os.path.join(self.tradefed._tradefed_cache,
'434f4f4f72747631722f67434541453d'))
# Now let it return a different hash value, for the same path.
mock_run.return_value = Mock(
stdout='ETag: SomeOtherEtag')
self.tradefed._download_to_cache(
'gs://some-fake-bucket/path/to/bundle.zip')
# Verify that os.path.exists was called with different values than the
# first time.
mock_exists.assert_called_with(
os.path.join(self.tradefed._tradefed_cache,
'536f6d654f7468657245746167'))
@patch('autotest_lib.server.cros.tradefed.tradefed_test.TradefedTest._download_to_dir'
)
@patch('hashlib.md5')
@patch('os.path.exists')
def test_download_to_cache_non_google_storage(self, mock_exists, mock_md5,
mock_download_to_dir):
mock_exists.return_value = False
mock_md5.return_value.hexdigest.return_value = '6ae0e7fc911c1b310d85c0d9fa592c08'
self.tradefed._tradefed_cache = '/any/test/dir'
self.tradefed._download_to_cache(
'https://dl.google.com/dl/android/xts/bundle.zip')
mock_exists.assert_called_with(
os.path.join(self.tradefed._tradefed_cache,
'6ae0e7fc911c1b310d85c0d9fa592c08'))
mock_download_to_dir.assert_called_with(
'https://dl.google.com/dl/android/xts/bundle.zip',
os.path.join(self.tradefed._tradefed_cache,
'6ae0e7fc911c1b310d85c0d9fa592c08'))
# Verify that downloaded bundles is not redownloaded.
@patch('autotest_lib.server.cros.tradefed.tradefed_test.TradefedTest._download_to_dir'
)
@patch('hashlib.md5')
@patch('os.path.exists')
@patch('os.listdir')
def test_download_to_cache_same_hash(self, mock_listdir, mock_exists,
mock_md5, mock_download_to_dir):
# os.path.exists returning true means it has been downloaded.
mock_exists.return_value = True
mock_md5.return_value.hexdigest.return_value = '6ae0e7fc911c1b310d85c0d9fa592c08'
mock_listdir.return_value = ['non', 'empty', 'list']
self.tradefed._tradefed_cache = '/any/test/dir'
self.tradefed._download_to_cache(
'https://dl.google.com/dl/android/xts/bundle.zip')
mock_exists.assert_called_with(
os.path.join(self.tradefed._tradefed_cache,
'6ae0e7fc911c1b310d85c0d9fa592c08'))
mock_download_to_dir.assert_not_called()
# The path exists but the output dir is empty. It should redownload.
@patch('autotest_lib.server.cros.tradefed.tradefed_test.TradefedTest._download_to_dir'
)
@patch('hashlib.md5')
@patch('os.path.exists')
@patch('os.listdir')
def test_download_to_cache_same_hash_empty_listdir(self, mock_listdir,
mock_exists, mock_md5,
mock_download_to_dir):
mock_exists.return_value = True
mock_md5.return_value.hexdigest.return_value = '6ae0e7fc911c1b310d85c0d9fa592c08'
mock_listdir.return_value = []
self.tradefed._tradefed_cache = '/any/test/dir'
self.tradefed._download_to_cache(
'https://dl.google.com/dl/android/xts/bundle.zip')
mock_exists.assert_called_with(
os.path.join(self.tradefed._tradefed_cache,
'6ae0e7fc911c1b310d85c0d9fa592c08'))
mock_download_to_dir.assert_called()
# Verify that an exception is raised if there isn't an ETag in the gsutil
# output.
@patch('autotest_lib.server.utils.run')
def test_failed_to_find_ETag(self, mock_run):
mock_run.return_value = Mock(
stdout='This message does not contain an ETag.')
self.assertRaises(tradefed_test.ETagNotFoundException,
tradefed_test._GetETagFromGsUri,
'gs://anything/here')