#!/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()