1#!/usr/bin/env python3 2# 3# Copyright 2016 - 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 17import logging 18import subprocess 19import time 20import unittest 21 22import mock 23 24from acts import utils 25from acts import signals 26from acts.controllers.adb_lib.error import AdbError 27from acts.controllers.android_device import AndroidDevice 28from acts.controllers.fuchsia_device import FuchsiaDevice 29from acts.controllers.fuchsia_lib.sl4f import SL4F 30from acts.controllers.fuchsia_lib.ssh import SSHConfig, SSHProvider, SSHResult 31from acts.controllers.utils_lib.ssh.connection import SshConnection 32from acts.libs.proc import job 33 34PROVISIONED_STATE_GOOD = 1 35 36MOCK_ENO1_IP_ADDRESSES = """100.127.110.79 372401:fa00:480:7a00:8d4f:85ff:cc5c:787e 382401:fa00:480:7a00:459:b993:fcbf:1419 39fe80::c66d:3c75:2cec:1d72""" 40 41MOCK_WLAN1_IP_ADDRESSES = "" 42 43FUCHSIA_INTERFACES = { 44 'id': 45 '1', 46 'result': [ 47 { 48 'id': 1, 49 'name': 'lo', 50 'ipv4_addresses': [ 51 [127, 0, 0, 1], 52 ], 53 'ipv6_addresses': [ 54 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 55 ], 56 'online': True, 57 'mac': [0, 0, 0, 0, 0, 0], 58 }, 59 { 60 'id': 61 2, 62 'name': 63 'eno1', 64 'ipv4_addresses': [ 65 [100, 127, 110, 79], 66 ], 67 'ipv6_addresses': [ 68 [ 69 254, 128, 0, 0, 0, 0, 0, 0, 198, 109, 60, 117, 44, 236, 29, 70 114 71 ], 72 [ 73 36, 1, 250, 0, 4, 128, 122, 0, 141, 79, 133, 255, 204, 92, 74 120, 126 75 ], 76 [ 77 36, 1, 250, 0, 4, 128, 122, 0, 4, 89, 185, 147, 252, 191, 78 20, 25 79 ], 80 ], 81 'online': 82 True, 83 'mac': [0, 224, 76, 5, 76, 229], 84 }, 85 { 86 'id': 87 3, 88 'name': 89 'wlanxc0', 90 'ipv4_addresses': [], 91 'ipv6_addresses': [ 92 [ 93 254, 128, 0, 0, 0, 0, 0, 0, 96, 255, 93, 96, 52, 253, 253, 94 243 95 ], 96 [ 97 254, 128, 0, 0, 0, 0, 0, 0, 70, 7, 11, 255, 254, 118, 126, 98 192 99 ], 100 ], 101 'online': 102 False, 103 'mac': [68, 7, 11, 118, 126, 192], 104 }, 105 ], 106 'error': 107 None, 108} 109 110CORRECT_FULL_IP_LIST = { 111 'ipv4_private': [], 112 'ipv4_public': ['100.127.110.79'], 113 'ipv6_link_local': ['fe80::c66d:3c75:2cec:1d72'], 114 'ipv6_private_local': [], 115 'ipv6_public': [ 116 '2401:fa00:480:7a00:8d4f:85ff:cc5c:787e', 117 '2401:fa00:480:7a00:459:b993:fcbf:1419' 118 ] 119} 120 121CORRECT_EMPTY_IP_LIST = { 122 'ipv4_private': [], 123 'ipv4_public': [], 124 'ipv6_link_local': [], 125 'ipv6_private_local': [], 126 'ipv6_public': [] 127} 128 129 130class ByPassSetupWizardTests(unittest.TestCase): 131 """This test class for unit testing acts.utils.bypass_setup_wizard.""" 132 133 def test_start_standing_subproc(self): 134 with self.assertRaisesRegex(utils.ActsUtilsError, 135 'Process .* has terminated'): 136 utils.start_standing_subprocess('sleep 0', check_health_delay=0.1) 137 138 def test_stop_standing_subproc(self): 139 p = utils.start_standing_subprocess('sleep 0') 140 time.sleep(0.1) 141 with self.assertRaisesRegex(utils.ActsUtilsError, 142 'Process .* has terminated'): 143 utils.stop_standing_subprocess(p) 144 145 @mock.patch('time.sleep') 146 def test_bypass_setup_wizard_no_complications(self, _): 147 ad = mock.Mock() 148 ad.adb.shell.side_effect = [ 149 # Return value for SetupWizardExitActivity 150 BypassSetupWizardReturn.NO_COMPLICATIONS, 151 # Return value for device_provisioned 152 PROVISIONED_STATE_GOOD, 153 ] 154 ad.adb.return_state = BypassSetupWizardReturn.NO_COMPLICATIONS 155 self.assertTrue(utils.bypass_setup_wizard(ad)) 156 self.assertFalse( 157 ad.adb.root_adb.called, 158 'The root command should not be called if there are no ' 159 'complications.') 160 161 @mock.patch('time.sleep') 162 def test_bypass_setup_wizard_unrecognized_error(self, _): 163 ad = mock.Mock() 164 ad.adb.shell.side_effect = [ 165 # Return value for SetupWizardExitActivity 166 BypassSetupWizardReturn.UNRECOGNIZED_ERR, 167 # Return value for device_provisioned 168 PROVISIONED_STATE_GOOD, 169 ] 170 with self.assertRaises(AdbError): 171 utils.bypass_setup_wizard(ad) 172 self.assertFalse( 173 ad.adb.root_adb.called, 174 'The root command should not be called if we do not have a ' 175 'codepath for recovering from the failure.') 176 177 @mock.patch('time.sleep') 178 def test_bypass_setup_wizard_need_root_access(self, _): 179 ad = mock.Mock() 180 ad.adb.shell.side_effect = [ 181 # Return value for SetupWizardExitActivity 182 BypassSetupWizardReturn.ROOT_ADB_NO_COMP, 183 # Return value for rooting the device 184 BypassSetupWizardReturn.NO_COMPLICATIONS, 185 # Return value for device_provisioned 186 PROVISIONED_STATE_GOOD 187 ] 188 189 utils.bypass_setup_wizard(ad) 190 191 self.assertTrue( 192 ad.adb.root_adb_called, 193 'The command required root access, but the device was never ' 194 'rooted.') 195 196 @mock.patch('time.sleep') 197 def test_bypass_setup_wizard_need_root_already_skipped(self, _): 198 ad = mock.Mock() 199 ad.adb.shell.side_effect = [ 200 # Return value for SetupWizardExitActivity 201 BypassSetupWizardReturn.ROOT_ADB_SKIPPED, 202 # Return value for SetupWizardExitActivity after root 203 BypassSetupWizardReturn.ALREADY_BYPASSED, 204 # Return value for device_provisioned 205 PROVISIONED_STATE_GOOD 206 ] 207 self.assertTrue(utils.bypass_setup_wizard(ad)) 208 self.assertTrue(ad.adb.root_adb_called) 209 210 @mock.patch('time.sleep') 211 def test_bypass_setup_wizard_root_access_still_fails(self, _): 212 ad = mock.Mock() 213 ad.adb.shell.side_effect = [ 214 # Return value for SetupWizardExitActivity 215 BypassSetupWizardReturn.ROOT_ADB_FAILS, 216 # Return value for SetupWizardExitActivity after root 217 BypassSetupWizardReturn.UNRECOGNIZED_ERR, 218 # Return value for device_provisioned 219 PROVISIONED_STATE_GOOD 220 ] 221 222 with self.assertRaises(AdbError): 223 utils.bypass_setup_wizard(ad) 224 self.assertTrue(ad.adb.root_adb_called) 225 226 227class BypassSetupWizardReturn: 228 # No complications. Bypass works the first time without issues. 229 NO_COMPLICATIONS = ( 230 'Starting: Intent { cmp=com.google.android.setupwizard/' 231 '.SetupWizardExitActivity }') 232 233 # Fail with doesn't need to be skipped/was skipped already. 234 ALREADY_BYPASSED = AdbError('', 'ADB_CMD_OUTPUT:0', 'Error type 3\n' 235 'Error: Activity class', 1) 236 # Fail with different error. 237 UNRECOGNIZED_ERR = AdbError('', 'ADB_CMD_OUTPUT:0', 'Error type 4\n' 238 'Error: Activity class', 0) 239 # Fail, get root access, then no complications arise. 240 ROOT_ADB_NO_COMP = AdbError( 241 '', 'ADB_CMD_OUTPUT:255', 'Security exception: Permission Denial: ' 242 'starting Intent { flg=0x10000000 ' 243 'cmp=com.google.android.setupwizard/' 244 '.SetupWizardExitActivity } from null ' 245 '(pid=5045, uid=2000) not exported from uid ' 246 '10000', 0) 247 # Even with root access, the bypass setup wizard doesn't need to be skipped. 248 ROOT_ADB_SKIPPED = AdbError( 249 '', 'ADB_CMD_OUTPUT:255', 'Security exception: Permission Denial: ' 250 'starting Intent { flg=0x10000000 ' 251 'cmp=com.google.android.setupwizard/' 252 '.SetupWizardExitActivity } from null ' 253 '(pid=5045, uid=2000) not exported from ' 254 'uid 10000', 0) 255 # Even with root access, the bypass setup wizard fails 256 ROOT_ADB_FAILS = AdbError( 257 '', 'ADB_CMD_OUTPUT:255', 258 'Security exception: Permission Denial: starting Intent { ' 259 'flg=0x10000000 cmp=com.google.android.setupwizard/' 260 '.SetupWizardExitActivity } from null (pid=5045, uid=2000) not ' 261 'exported from uid 10000', 0) 262 263 264class ConcurrentActionsTest(unittest.TestCase): 265 """Tests acts.utils.run_concurrent_actions and related functions.""" 266 267 @staticmethod 268 def function_returns_passed_in_arg(arg): 269 return arg 270 271 @staticmethod 272 def function_raises_passed_in_exception_type(exception_type): 273 raise exception_type 274 275 def test_run_concurrent_actions_no_raise_returns_proper_return_values( 276 self): 277 """Tests run_concurrent_actions_no_raise returns in the correct order. 278 279 Each function passed into run_concurrent_actions_no_raise returns the 280 values returned from each individual callable in the order passed in. 281 """ 282 ret_values = utils.run_concurrent_actions_no_raise( 283 lambda: self.function_returns_passed_in_arg( 284 'ARG1'), lambda: self.function_returns_passed_in_arg('ARG2'), 285 lambda: self.function_returns_passed_in_arg('ARG3')) 286 287 self.assertEqual(len(ret_values), 3) 288 self.assertEqual(ret_values[0], 'ARG1') 289 self.assertEqual(ret_values[1], 'ARG2') 290 self.assertEqual(ret_values[2], 'ARG3') 291 292 def test_run_concurrent_actions_no_raise_returns_raised_exceptions(self): 293 """Tests run_concurrent_actions_no_raise returns raised exceptions. 294 295 Instead of allowing raised exceptions to be raised in the main thread, 296 this function should capture the exception and return them in the slot 297 the return value should have been returned in. 298 """ 299 ret_values = utils.run_concurrent_actions_no_raise( 300 lambda: self.function_raises_passed_in_exception_type(IndexError), 301 lambda: self.function_raises_passed_in_exception_type(KeyError)) 302 303 self.assertEqual(len(ret_values), 2) 304 self.assertEqual(ret_values[0].__class__, IndexError) 305 self.assertEqual(ret_values[1].__class__, KeyError) 306 307 def test_run_concurrent_actions_returns_proper_return_values(self): 308 """Tests run_concurrent_actions returns in the correct order. 309 310 Each function passed into run_concurrent_actions returns the values 311 returned from each individual callable in the order passed in. 312 """ 313 314 ret_values = utils.run_concurrent_actions( 315 lambda: self.function_returns_passed_in_arg( 316 'ARG1'), lambda: self.function_returns_passed_in_arg('ARG2'), 317 lambda: self.function_returns_passed_in_arg('ARG3')) 318 319 self.assertEqual(len(ret_values), 3) 320 self.assertEqual(ret_values[0], 'ARG1') 321 self.assertEqual(ret_values[1], 'ARG2') 322 self.assertEqual(ret_values[2], 'ARG3') 323 324 def test_run_concurrent_actions_raises_exceptions(self): 325 """Tests run_concurrent_actions raises exceptions from given actions.""" 326 with self.assertRaises(KeyError): 327 utils.run_concurrent_actions( 328 lambda: self.function_returns_passed_in_arg('ARG1'), lambda: 329 self.function_raises_passed_in_exception_type(KeyError)) 330 331 def test_test_concurrent_actions_raises_non_test_failure(self): 332 """Tests test_concurrent_actions raises the given exception.""" 333 with self.assertRaises(KeyError): 334 utils.test_concurrent_actions( 335 lambda: self.function_raises_passed_in_exception_type(KeyError 336 ), 337 failure_exceptions=signals.TestFailure) 338 339 def test_test_concurrent_actions_raises_test_failure(self): 340 """Tests test_concurrent_actions raises the given exception.""" 341 with self.assertRaises(signals.TestFailure): 342 utils.test_concurrent_actions( 343 lambda: self.function_raises_passed_in_exception_type(KeyError 344 ), 345 failure_exceptions=KeyError) 346 347 348class SuppressLogOutputTest(unittest.TestCase): 349 """Tests SuppressLogOutput""" 350 351 def test_suppress_log_output(self): 352 """Tests that the SuppressLogOutput context manager removes handlers 353 of the specified levels upon entry and re-adds handlers upon exit. 354 """ 355 handlers = [ 356 logging.NullHandler(level=lvl) 357 for lvl in (logging.DEBUG, logging.INFO, logging.ERROR) 358 ] 359 log = logging.getLogger('test_log') 360 for handler in handlers: 361 log.addHandler(handler) 362 with utils.SuppressLogOutput(log, [logging.INFO, logging.ERROR]): 363 self.assertTrue( 364 any(handler.level == logging.DEBUG 365 for handler in log.handlers)) 366 self.assertFalse( 367 any(handler.level in (logging.INFO, logging.ERROR) 368 for handler in log.handlers)) 369 self.assertCountEqual(handlers, log.handlers) 370 371 372class IpAddressUtilTest(unittest.TestCase): 373 def test_positive_ipv4_normal_address(self): 374 ip_address = "192.168.1.123" 375 self.assertTrue(utils.is_valid_ipv4_address(ip_address)) 376 377 def test_positive_ipv4_any_address(self): 378 ip_address = "0.0.0.0" 379 self.assertTrue(utils.is_valid_ipv4_address(ip_address)) 380 381 def test_positive_ipv4_broadcast(self): 382 ip_address = "255.255.255.0" 383 self.assertTrue(utils.is_valid_ipv4_address(ip_address)) 384 385 def test_negative_ipv4_with_ipv6_address(self): 386 ip_address = "fe80::f693:9fff:fef4:1ac" 387 self.assertFalse(utils.is_valid_ipv4_address(ip_address)) 388 389 def test_negative_ipv4_with_invalid_string(self): 390 ip_address = "fdsafdsafdsafdsf" 391 self.assertFalse(utils.is_valid_ipv4_address(ip_address)) 392 393 def test_negative_ipv4_with_invalid_number(self): 394 ip_address = "192.168.500.123" 395 self.assertFalse(utils.is_valid_ipv4_address(ip_address)) 396 397 def test_positive_ipv6(self): 398 ip_address = 'fe80::f693:9fff:fef4:1ac' 399 self.assertTrue(utils.is_valid_ipv6_address(ip_address)) 400 401 def test_positive_ipv6_link_local(self): 402 ip_address = 'fe80::' 403 self.assertTrue(utils.is_valid_ipv6_address(ip_address)) 404 405 def test_negative_ipv6_with_ipv4_address(self): 406 ip_address = '192.168.1.123' 407 self.assertFalse(utils.is_valid_ipv6_address(ip_address)) 408 409 def test_negative_ipv6_invalid_characters(self): 410 ip_address = 'fe80:jkyr:f693:9fff:fef4:1ac' 411 self.assertFalse(utils.is_valid_ipv6_address(ip_address)) 412 413 def test_negative_ipv6_invalid_string(self): 414 ip_address = 'fdsafdsafdsafdsf' 415 self.assertFalse(utils.is_valid_ipv6_address(ip_address)) 416 417 @mock.patch('acts.libs.proc.job.run') 418 def test_local_get_interface_ip_addresses_full(self, job_mock): 419 job_mock.side_effect = [ 420 job.Result(stdout=bytes(MOCK_ENO1_IP_ADDRESSES, 'utf-8'), 421 encoding='utf-8'), 422 ] 423 self.assertEqual(utils.get_interface_ip_addresses(job, 'eno1'), 424 CORRECT_FULL_IP_LIST) 425 426 @mock.patch('acts.libs.proc.job.run') 427 def test_local_get_interface_ip_addresses_empty(self, job_mock): 428 job_mock.side_effect = [ 429 job.Result(stdout=bytes(MOCK_WLAN1_IP_ADDRESSES, 'utf-8'), 430 encoding='utf-8'), 431 ] 432 self.assertEqual(utils.get_interface_ip_addresses(job, 'wlan1'), 433 CORRECT_EMPTY_IP_LIST) 434 435 @mock.patch('acts.controllers.utils_lib.ssh.connection.SshConnection.run') 436 def test_ssh_get_interface_ip_addresses_full(self, ssh_mock): 437 ssh_mock.side_effect = [ 438 job.Result(stdout=bytes(MOCK_ENO1_IP_ADDRESSES, 'utf-8'), 439 encoding='utf-8'), 440 ] 441 self.assertEqual( 442 utils.get_interface_ip_addresses(SshConnection('mock_settings'), 443 'eno1'), CORRECT_FULL_IP_LIST) 444 445 @mock.patch('acts.controllers.utils_lib.ssh.connection.SshConnection.run') 446 def test_ssh_get_interface_ip_addresses_empty(self, ssh_mock): 447 ssh_mock.side_effect = [ 448 job.Result(stdout=bytes(MOCK_WLAN1_IP_ADDRESSES, 'utf-8'), 449 encoding='utf-8'), 450 ] 451 self.assertEqual( 452 utils.get_interface_ip_addresses(SshConnection('mock_settings'), 453 'wlan1'), CORRECT_EMPTY_IP_LIST) 454 455 @mock.patch('acts.controllers.adb.AdbProxy') 456 @mock.patch.object(AndroidDevice, 'is_bootloader', return_value=True) 457 def test_android_get_interface_ip_addresses_full(self, is_bootloader, 458 adb_mock): 459 adb_mock().shell.side_effect = [ 460 MOCK_ENO1_IP_ADDRESSES, 461 ] 462 self.assertEqual( 463 utils.get_interface_ip_addresses(AndroidDevice(), 'eno1'), 464 CORRECT_FULL_IP_LIST) 465 466 @mock.patch('acts.controllers.adb.AdbProxy') 467 @mock.patch.object(AndroidDevice, 'is_bootloader', return_value=True) 468 def test_android_get_interface_ip_addresses_empty(self, is_bootloader, 469 adb_mock): 470 adb_mock().shell.side_effect = [ 471 MOCK_WLAN1_IP_ADDRESSES, 472 ] 473 self.assertEqual( 474 utils.get_interface_ip_addresses(AndroidDevice(), 'wlan1'), 475 CORRECT_EMPTY_IP_LIST) 476 477 @mock.patch('acts.controllers.fuchsia_device.FuchsiaDevice.sl4f', 478 new_callable=mock.PropertyMock) 479 @mock.patch('acts.controllers.fuchsia_device.FuchsiaDevice.ffx', 480 new_callable=mock.PropertyMock) 481 @mock.patch('acts.controllers.fuchsia_lib.utils_lib.wait_for_port') 482 @mock.patch('acts.controllers.fuchsia_lib.ssh.SSHProvider.run') 483 @mock.patch( 484 'acts.controllers.fuchsia_lib.sl4f.SL4F._verify_sl4f_connection') 485 @mock.patch('acts.controllers.fuchsia_device.' 486 'FuchsiaDevice._set_control_path_config') 487 @mock.patch('acts.controllers.' 488 'fuchsia_lib.netstack.netstack_lib.' 489 'FuchsiaNetstackLib.netstackListInterfaces') 490 def test_fuchsia_get_interface_ip_addresses_full( 491 self, list_interfaces_mock, control_path_mock, 492 verify_sl4f_conn_mock, ssh_run_mock, wait_for_port_mock, 493 ffx_mock, sl4f_mock): 494 # Configure the log path which is required by ACTS logger. 495 logging.log_path = '/tmp/unit_test_garbage' 496 497 ssh = SSHProvider(SSHConfig('192.168.1.1', 22, '/dev/null')) 498 ssh_run_mock.return_value = SSHResult( 499 subprocess.CompletedProcess([], 0, stdout=b'', stderr=b'')) 500 501 # Don't try to wait for the SL4F server to start; it's not being used. 502 wait_for_port_mock.return_value = None 503 504 sl4f_mock.return_value = SL4F(ssh, 'http://192.168.1.1:80') 505 verify_sl4f_conn_mock.return_value = None 506 507 list_interfaces_mock.return_value = FUCHSIA_INTERFACES 508 self.assertEqual( 509 utils.get_interface_ip_addresses( 510 FuchsiaDevice({'ip': '192.168.1.1'}), 'eno1'), 511 CORRECT_FULL_IP_LIST) 512 513 @mock.patch('acts.controllers.fuchsia_device.FuchsiaDevice.sl4f', 514 new_callable=mock.PropertyMock) 515 @mock.patch('acts.controllers.fuchsia_device.FuchsiaDevice.ffx', 516 new_callable=mock.PropertyMock) 517 @mock.patch('acts.controllers.fuchsia_lib.utils_lib.wait_for_port') 518 @mock.patch('acts.controllers.fuchsia_lib.ssh.SSHProvider.run') 519 @mock.patch( 520 'acts.controllers.fuchsia_lib.sl4f.SL4F._verify_sl4f_connection') 521 @mock.patch('acts.controllers.fuchsia_device.' 522 'FuchsiaDevice._set_control_path_config') 523 @mock.patch('acts.controllers.' 524 'fuchsia_lib.netstack.netstack_lib.' 525 'FuchsiaNetstackLib.netstackListInterfaces') 526 def test_fuchsia_get_interface_ip_addresses_empty( 527 self, list_interfaces_mock, control_path_mock, 528 verify_sl4f_conn_mock, ssh_run_mock, wait_for_port_mock, 529 ffx_mock, sl4f_mock): 530 # Configure the log path which is required by ACTS logger. 531 logging.log_path = '/tmp/unit_test_garbage' 532 533 ssh = SSHProvider(SSHConfig('192.168.1.1', 22, '/dev/null')) 534 ssh_run_mock.return_value = SSHResult( 535 subprocess.CompletedProcess([], 0, stdout=b'', stderr=b'')) 536 537 # Don't try to wait for the SL4F server to start; it's not being used. 538 wait_for_port_mock.return_value = None 539 540 sl4f_mock.return_value = SL4F(ssh, 'http://192.168.1.1:80') 541 verify_sl4f_conn_mock.return_value = None 542 543 list_interfaces_mock.return_value = FUCHSIA_INTERFACES 544 self.assertEqual( 545 utils.get_interface_ip_addresses( 546 FuchsiaDevice({'ip': '192.168.1.1'}), 'wlan1'), 547 CORRECT_EMPTY_IP_LIST) 548 549 550class GetDeviceTest(unittest.TestCase): 551 class TestDevice: 552 def __init__(self, id, device_type=None) -> None: 553 self.id = id 554 if device_type: 555 self.device_type = device_type 556 557 def test_get_device_none(self): 558 devices = [] 559 self.assertRaises(ValueError, utils.get_device, devices, 'DUT') 560 561 def test_get_device_default_one(self): 562 devices = [self.TestDevice(0)] 563 self.assertEqual(utils.get_device(devices, 'DUT').id, 0) 564 565 def test_get_device_default_many(self): 566 devices = [self.TestDevice(0), self.TestDevice(1)] 567 self.assertEqual(utils.get_device(devices, 'DUT').id, 0) 568 569 def test_get_device_specified_one(self): 570 devices = [self.TestDevice(0), self.TestDevice(1, 'DUT')] 571 self.assertEqual(utils.get_device(devices, 'DUT').id, 1) 572 573 def test_get_device_specified_many(self): 574 devices = [self.TestDevice(0, 'DUT'), self.TestDevice(1, 'DUT')] 575 self.assertRaises(ValueError, utils.get_device, devices, 'DUT') 576 577 578if __name__ == '__main__': 579 unittest.main() 580