| #!/usr/bin/env vpython3 |
| # Copyright 2022 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Tests scenarios for ermine_ctl""" |
| import logging |
| import subprocess |
| import time |
| import unittest |
| import unittest.mock as mock |
| |
| from base_ermine_ctl import BaseErmineCtl |
| |
| |
| class BaseBaseErmineCtlTest(unittest.TestCase): |
| """Unit tests for BaseBaseErmineCtl interface.""" |
| |
| def __init__(self, *args, **kwargs): |
| super().__init__(*args, **kwargs) |
| self.ermine_ctl = BaseErmineCtl() |
| |
| def _set_mock_proc(self, return_value: int): |
| """Set |execute_command_async|'s return value to a mocked subprocess.""" |
| self.ermine_ctl.execute_command_async = mock.MagicMock() |
| mock_proc = mock.create_autospec(subprocess.Popen, instance=True) |
| mock_proc.communicate.return_value = 'foo', 'stderr' |
| mock_proc.returncode = return_value |
| self.ermine_ctl.execute_command_async.return_value = mock_proc |
| |
| return mock_proc |
| |
| def test_check_exists(self): |
| """Test |exists| returns True if tool command succeeds (returns 0).""" |
| self._set_mock_proc(return_value=0) |
| |
| self.assertTrue(self.ermine_ctl.exists) |
| |
| # Modifying this will not result in a change in state due to caching. |
| self._set_mock_proc(return_value=42) |
| self.assertTrue(self.ermine_ctl.exists) |
| |
| def test_does_not_exist(self): |
| """Test |exists| returns False if tool command fails (returns != 0).""" |
| self._set_mock_proc(return_value=42) |
| |
| self.assertFalse(self.ermine_ctl.exists) |
| |
| def test_ready_raises_assertion_error_if_not_exist(self): |
| """Test |ready| raises AssertionError if tool does not exist.""" |
| self._set_mock_proc(return_value=42) |
| self.assertRaises(AssertionError, getattr, self.ermine_ctl, 'ready') |
| |
| def test_ready_returns_false_if_bad_status(self): |
| """Test |ready| return False if tool has a bad status.""" |
| with mock.patch.object( |
| BaseErmineCtl, 'status', |
| new_callable=mock.PropertyMock) as mock_status, \ |
| mock.patch.object(BaseErmineCtl, 'exists', |
| new_callable=mock.PropertyMock) as mock_exists: |
| mock_exists.return_value = True |
| mock_status.return_value = (1, 'FakeStatus') |
| self.assertFalse(self.ermine_ctl.ready) |
| |
| def test_ready_returns_true(self): |
| """Test |ready| return True if tool returns good status (rc = 0).""" |
| with mock.patch.object( |
| BaseErmineCtl, 'status', |
| new_callable=mock.PropertyMock) as mock_status, \ |
| mock.patch.object(BaseErmineCtl, 'exists', |
| new_callable=mock.PropertyMock) as mock_exists: |
| mock_exists.return_value = True |
| mock_status.return_value = (0, 'FakeStatus') |
| self.assertTrue(self.ermine_ctl.ready) |
| |
| def test_status_raises_assertion_error_if_dne(self): |
| """Test |status| returns |InvalidState| if tool does not exist.""" |
| with mock.patch.object(BaseErmineCtl, |
| 'exists', |
| new_callable=mock.PropertyMock) as mock_exists: |
| mock_exists.return_value = False |
| |
| self.assertRaises(AssertionError, getattr, self.ermine_ctl, |
| 'status') |
| |
| def test_status_returns_rc_and_stdout(self): |
| """Test |status| returns subprocess stdout and rc if tool exists.""" |
| with mock.patch.object(BaseErmineCtl, |
| 'exists', |
| new_callable=mock.PropertyMock) as _: |
| self._set_mock_proc(return_value=10) |
| |
| self.assertEqual(self.ermine_ctl.status, (10, 'foo')) |
| |
| def test_status_returns_timeout_state(self): |
| """Test |status| returns |Timeout| if exception is raised.""" |
| with mock.patch.object( |
| BaseErmineCtl, 'exists', new_callable=mock.PropertyMock) as _, \ |
| mock.patch.object(logging, 'warning') as _: |
| mock_proc = self._set_mock_proc(return_value=0) |
| mock_proc.wait.side_effect = subprocess.TimeoutExpired( |
| 'cmd', 'some timeout') |
| |
| self.assertEqual(self.ermine_ctl.status, (-1, 'Timeout')) |
| |
| def test_wait_until_ready_raises_assertion_error_if_tool_dne(self): |
| """Test |wait_until_ready| is returns false if tool does not exist.""" |
| with mock.patch.object(BaseErmineCtl, |
| 'exists', |
| new_callable=mock.PropertyMock) as mock_exists: |
| mock_exists.return_value = False |
| |
| self.assertRaises(AssertionError, self.ermine_ctl.wait_until_ready) |
| |
| def test_wait_until_ready_loops_until_ready(self): |
| """Test |wait_until_ready| loops until |ready| returns True.""" |
| with mock.patch.object(BaseErmineCtl, 'exists', |
| new_callable=mock.PropertyMock) as mock_exists, \ |
| mock.patch.object(time, 'sleep') as mock_sleep, \ |
| mock.patch.object(BaseErmineCtl, 'ready', |
| new_callable=mock.PropertyMock) as mock_ready: |
| mock_exists.return_value = True |
| mock_ready.side_effect = [False, False, False, True] |
| |
| self.ermine_ctl.wait_until_ready() |
| |
| self.assertEqual(mock_ready.call_count, 4) |
| self.assertEqual(mock_sleep.call_count, 3) |
| |
| def test_wait_until_ready_raises_assertion_error_if_attempts_exceeded( |
| self): |
| """Test |wait_until_ready| loops if |ready| is not True n attempts.""" |
| with mock.patch.object(BaseErmineCtl, 'exists', |
| new_callable=mock.PropertyMock) as mock_exists, \ |
| mock.patch.object(time, 'sleep') as mock_sleep, \ |
| mock.patch.object(BaseErmineCtl, 'ready', |
| new_callable=mock.PropertyMock) as mock_ready: |
| mock_exists.return_value = True |
| mock_ready.side_effect = [False] * 15 + [True] |
| |
| self.assertRaises(TimeoutError, self.ermine_ctl.wait_until_ready) |
| |
| self.assertEqual(mock_ready.call_count, 10) |
| self.assertEqual(mock_sleep.call_count, 10) |
| |
| def test_take_to_shell_raises_assertion_error_if_tool_dne(self): |
| """Test |take_to_shell| throws AssertionError if not ready is False.""" |
| with mock.patch.object(BaseErmineCtl, |
| 'exists', |
| new_callable=mock.PropertyMock) as mock_exists: |
| mock_exists.return_value = False |
| self.assertRaises(AssertionError, self.ermine_ctl.take_to_shell) |
| |
| def test_take_to_shell_exits_on_complete_state(self): |
| """Test |take_to_shell| exits with no calls if in completed state.""" |
| with mock.patch.object(BaseErmineCtl, |
| 'wait_until_ready') as mock_wait_ready, \ |
| mock.patch.object( |
| BaseErmineCtl, 'status', |
| new_callable=mock.PropertyMock) as mock_status: |
| mock_proc = self._set_mock_proc(return_value=52) |
| mock_wait_ready.return_value = True |
| mock_status.return_value = (0, 'Shell') |
| |
| self.ermine_ctl.take_to_shell() |
| |
| self.assertEqual(mock_proc.call_count, 0) |
| |
| def test_take_to_shell_invalid_state_raises_not_implemented_error(self): |
| """Test |take_to_shell| raises exception if invalid state is returned. |
| """ |
| with mock.patch.object(BaseErmineCtl, |
| 'wait_until_ready') as mock_wait_ready, \ |
| mock.patch.object( |
| BaseErmineCtl, 'status', |
| new_callable=mock.PropertyMock) as mock_status: |
| mock_wait_ready.return_value = True |
| mock_status.return_value = (0, 'SomeUnknownState') |
| |
| self.assertRaises(NotImplementedError, |
| self.ermine_ctl.take_to_shell) |
| |
| def test_take_to_shell_with_max_transitions_raises_runtime_error(self): |
| """Test |take_to_shell| raises exception on too many transitions. |
| |
| |take_to_shell| attempts to transition from one state to another. |
| After 5 attempts, if this does not end in the completed state, an |
| Exception is thrown. |
| """ |
| with mock.patch.object(BaseErmineCtl, |
| 'wait_until_ready') as mock_wait_ready, \ |
| mock.patch.object( |
| BaseErmineCtl, 'status', |
| new_callable=mock.PropertyMock) as mock_status: |
| mock_wait_ready.return_value = True |
| # Returns too many state transitions before CompleteState. |
| mock_status.side_effect = [(0, 'Unknown'), |
| (0, 'KnownWithPassword'), |
| (0, 'Unknown')] * 3 + [ |
| (0, 'CompleteState') |
| ] |
| self.assertRaises(RuntimeError, self.ermine_ctl.take_to_shell) |
| |
| def test_take_to_shell_executes_known_commands(self): |
| """Test |take_to_shell| executes commands if necessary. |
| |
| Some states can only be transitioned between with specific commands. |
| These are executed by |take_to_shell| until the final test |Shell| is |
| reached. |
| """ |
| with mock.patch.object(BaseErmineCtl, |
| 'wait_until_ready') as mock_wait_ready, \ |
| mock.patch.object( |
| BaseErmineCtl, 'status', |
| new_callable=mock.PropertyMock) as mock_status: |
| self._set_mock_proc(return_value=0) |
| mock_wait_ready.return_value = True |
| mock_status.side_effect = [(0, 'Unknown'), (0, 'SetPassword'), |
| (0, 'Shell')] |
| |
| self.ermine_ctl.take_to_shell() |
| |
| self.assertEqual(self.ermine_ctl.execute_command_async.call_count, |
| 2) |
| self.ermine_ctl.execute_command_async.assert_has_calls([ |
| mock.call(['erminectl', 'oobe', 'skip']), |
| mock.call().communicate(), |
| mock.call([ |
| 'erminectl', 'oobe', 'set_password', |
| 'workstation_test_password' |
| ]), |
| mock.call().communicate() |
| ]) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |