• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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