test_watchdog.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. Unit Tests for watchdog.py
  6. """
  7. import unittest
  8. import unittest.mock as mock
  9. import pytest
  10. import ly_test_tools.environment.watchdog as watchdog
  11. pytestmark = pytest.mark.SUITE_smoke
  12. def mock_bool_fn():
  13. return
  14. class TestWatchdog(unittest.TestCase):
  15. @mock.patch('threading.Event')
  16. @mock.patch('threading.Thread')
  17. def test_Watchdog_Instantiated_CreatesEventAndThread(self, mock_thread, mock_event):
  18. mock_watchdog = watchdog.Watchdog(mock_bool_fn)
  19. mock_event.assert_called_once()
  20. mock_thread.assert_called_once_with(target=mock_watchdog._watchdog, name=mock_watchdog.name)
  21. @mock.patch('threading.Event', mock.MagicMock())
  22. @mock.patch('threading.Thread', mock.MagicMock())
  23. class TestWatchdogMethods(unittest.TestCase):
  24. def setUp(self):
  25. self.mock_watchdog = watchdog.Watchdog(mock_bool_fn)
  26. @mock.patch('threading.Thread.start')
  27. @mock.patch('threading.Event.clear')
  28. def test_Start_Called_ClearsEventAndStartsThread(self, mock_clear, mock_start):
  29. self.mock_watchdog.start()
  30. mock_clear.assert_called_once()
  31. mock_start.assert_called_once()
  32. @mock.patch('threading.Thread.join', mock.MagicMock())
  33. @mock.patch('threading.Event.set')
  34. def test_Stop_Called_CallsEventSet(self, under_test):
  35. self.mock_watchdog.stop()
  36. under_test.assert_called_once()
  37. @mock.patch('threading.Thread.join', mock.MagicMock())
  38. @mock.patch('threading.Event.set', mock.MagicMock())
  39. @mock.patch('ly_test_tools.environment.watchdog.logging.Logger.error')
  40. def test_Stop_NoCaughtFailures_NoRaiseOrError(self, mock_error_log):
  41. self.mock_watchdog.caught_failure = False
  42. try:
  43. self.mock_watchdog.stop()
  44. except watchdog.WatchdogError as e:
  45. self.fail(f"Unexpected WatchdogError called. Error: {e}")
  46. mock_error_log.assert_not_called()
  47. @mock.patch('threading.Thread.join', mock.MagicMock())
  48. @mock.patch('threading.Event.set', mock.MagicMock())
  49. @mock.patch('ly_test_tools.environment.watchdog.logging.Logger.error')
  50. def test_Stop_CaughtFailuresAndRaisesOnCondition_RaisesWatchdogError(self, mock_error_log):
  51. self.mock_watchdog.caught_failure = True
  52. self.mock_watchdog._raise_on_condition = True
  53. with pytest.raises(watchdog.WatchdogError):
  54. self.mock_watchdog.stop()
  55. mock_error_log.assert_not_called()
  56. @mock.patch('threading.Thread.join', mock.MagicMock())
  57. @mock.patch('threading.Event.set', mock.MagicMock())
  58. @mock.patch('ly_test_tools.environment.watchdog.logging.Logger.error')
  59. def test_Stop_CaughtFailuresAndNotRaisesOnCondition_LogsError(self, mock_error_log):
  60. self.mock_watchdog.caught_failure = True
  61. self.mock_watchdog._raise_on_condition = False
  62. try:
  63. self.mock_watchdog.stop()
  64. except watchdog.WatchdogError as e:
  65. self.fail(f"Unexpected WatchdogError called. Error: {e}")
  66. mock_error_log.assert_called_once()
  67. @mock.patch('threading.Thread.join')
  68. def test_Stop_Called_CallsJoin(self, under_test):
  69. self.mock_watchdog.caught_failure = False
  70. self.mock_watchdog.stop()
  71. under_test.assert_called_once()
  72. @mock.patch('threading.Thread.join', mock.MagicMock())
  73. @mock.patch('ly_test_tools.environment.watchdog.Watchdog.is_alive')
  74. @mock.patch('ly_test_tools.environment.watchdog.logging.Logger.error')
  75. def test_Stop_ThreadIsAlive_LogsError(self, under_test, mock_is_alive):
  76. mock_is_alive.return_value = True
  77. self.mock_watchdog.stop()
  78. under_test.assert_called_once()
  79. @mock.patch('threading.Thread.join', mock.MagicMock())
  80. @mock.patch('ly_test_tools.environment.watchdog.Watchdog.is_alive')
  81. @mock.patch('ly_test_tools.environment.watchdog.logging.Logger.error')
  82. def test_Stop_ThreadNotAlive_NoLogsError(self, under_test, mock_is_alive):
  83. mock_is_alive.return_value = False
  84. self.mock_watchdog.stop()
  85. under_test.assert_not_called()
  86. @mock.patch('threading.Thread.is_alive')
  87. def test_IsAlive_Called_CallsIsAlive(self, under_test):
  88. self.mock_watchdog.is_alive()
  89. under_test.assert_called_once()
  90. @mock.patch('threading.Event.wait')
  91. def test_WatchdogRunner_ShutdownEventNotSet_CallsBoolFn(self, mock_event_wait):
  92. mock_event_wait.side_effect = [False, True]
  93. mock_bool_fn = mock.MagicMock()
  94. mock_bool_fn.return_value = False
  95. self.mock_watchdog._bool_fn = mock_bool_fn
  96. self.mock_watchdog._watchdog()
  97. self.mock_watchdog._bool_fn.assert_called_once()
  98. assert self.mock_watchdog.caught_failure == False
  99. def test_WatchdogRunner_ShutdownEventSet_NoCallsBoolFn(self):
  100. self.mock_watchdog._shutdown.set()
  101. mock_bool_fn = mock.MagicMock()
  102. self.mock_watchdog._bool_fn = mock_bool_fn
  103. under_test = self.mock_watchdog._watchdog()
  104. assert under_test is None
  105. self.mock_watchdog._bool_fn.assert_not_called()
  106. @mock.patch('threading.Event.wait')
  107. def test_WatchdogRunner_BoolFnReturnsTrue_SetsCaughtFailureToTrue(self, mock_event_wait):
  108. mock_event_wait.side_effect = [False, True]
  109. mock_bool_fn = mock.MagicMock()
  110. mock_bool_fn.return_value = True
  111. self.mock_watchdog._bool_fn = mock_bool_fn
  112. self.mock_watchdog._watchdog()
  113. assert self.mock_watchdog.caught_failure == True
  114. class TestProcessUnresponsiveWatchdog(unittest.TestCase):
  115. mock_process_id = 11111
  116. mock_name = 'foo.exe'
  117. mock_process_not_resp_call = \
  118. '\r\n " \
  119. "Image Name PID Session Name Session# Mem Usage\r\n" \
  120. "========================= ======== ================ =========== ============\r\n" \
  121. "foo.exe %d Console 1 0 K\r\n"' % mock_process_id
  122. mock_process_resp_call = "INFO: No tasks are running which match the specified criteria.\r\n"
  123. @mock.patch('psutil.Process', mock.MagicMock())
  124. def setUp(self):
  125. self.mock_watchdog = watchdog.ProcessUnresponsiveWatchdog(self.mock_process_id)
  126. self.mock_watchdog._process_name = self.mock_name
  127. @mock.patch('ly_test_tools.environment.process_utils.check_output')
  128. def test_ProcessNotResponding_ProcessResponsive_ReturnsFalse(self, mock_check_output):
  129. mock_check_output.return_value = self.mock_process_resp_call
  130. self.mock_watchdog._pid = self.mock_process_id
  131. under_test = self.mock_watchdog._process_not_responding()
  132. assert not under_test
  133. assert self.mock_watchdog._calculated_timeout_point is None
  134. @mock.patch('time.time')
  135. @mock.patch('ly_test_tools.environment.process_utils.check_output')
  136. def test_ProcessNotResponding_ProcessUnresponsiveNoTimeout_ReturnsFalse(self, mock_check_output, mock_time):
  137. mock_time.return_value = 1
  138. mock_check_output.return_value = self.mock_process_not_resp_call
  139. self.mock_watchdog._pid = self.mock_process_id
  140. under_test = self.mock_watchdog._process_not_responding()
  141. timeout_under_test = mock_time.return_value + self.mock_watchdog._unresponsive_timeout
  142. assert not under_test
  143. assert self.mock_watchdog._calculated_timeout_point == timeout_under_test
  144. @mock.patch('time.time')
  145. @mock.patch('ly_test_tools.environment.process_utils.check_output')
  146. def test_ProcessNotResponding_ProcessUnresponsiveReachesTimeout_ReturnsTrue(self, mock_check_output, mock_time):
  147. mock_time.return_value = 3
  148. mock_check_output.return_value = self.mock_process_not_resp_call
  149. self.mock_watchdog._pid = self.mock_process_id
  150. self.mock_watchdog._calculated_timeout_point = 2
  151. under_test = self.mock_watchdog._process_not_responding()
  152. assert under_test
  153. def test_GetPid_Called_ReturnsAttribute(self):
  154. self.mock_watchdog._pid = self.mock_process_id
  155. assert self.mock_watchdog.get_pid() == self.mock_watchdog._pid
  156. class TestCrashLogWatchdog(unittest.TestCase):
  157. mock_log_path = 'C:/foo'
  158. @mock.patch('os.path.exists')
  159. def test_CrashExists_Called_CallsOsPathExists(self, under_test):
  160. under_test.side_effect = [False, mock.DEFAULT]
  161. mock_watchdog = watchdog.CrashLogWatchdog(self.mock_log_path)
  162. mock_watchdog._bool_fn()
  163. under_test.assert_called_with(self.mock_log_path)
  164. @mock.patch('os.path.exists')
  165. @mock.patch('os.remove')
  166. def test_CrashLogWatchdog_LogsExist_ClearsExistingLogs(self, under_test, mock_exists):
  167. mock_exists.return_value = True
  168. mock_watchdog = watchdog.CrashLogWatchdog(self.mock_log_path)
  169. under_test.assert_called_once_with(self.mock_log_path)
  170. @mock.patch('os.path.exists')
  171. @mock.patch('os.remove')
  172. def test_CrashLogWatchdog_LogsNotExist_NoClearsExistingLogs(self, under_test, mock_exists):
  173. mock_exists.return_value = False
  174. mock_watchdog = watchdog.CrashLogWatchdog(self.mock_log_path)
  175. under_test.assert_not_called()
  176. @mock.patch('threading.Thread.join', mock.MagicMock())
  177. @mock.patch('builtins.print')
  178. @mock.patch('builtins.open')
  179. @mock.patch('os.path.exists')
  180. def test_CrashLogWatchdogStop_LogsExists_OpensLogAndPrints(self, mock_exists, mock_open, mock_print):
  181. mock_exists.side_effect = [False, True]
  182. mock_watchdog = watchdog.CrashLogWatchdog(self.mock_log_path)
  183. mock_watchdog.caught_failure = True
  184. mock_watchdog._raise_on_condition = False
  185. mock_watchdog.stop()
  186. mock_open.assert_called_once_with(mock_watchdog._log_path, "r")
  187. assert mock_print.called
  188. @mock.patch('threading.Thread.join', mock.MagicMock())
  189. @mock.patch('builtins.print')
  190. @mock.patch('builtins.open')
  191. @mock.patch('os.path.exists')
  192. def test_CrashLogWatchdogStop_LogsNoExists_NoOpensAndNoPrintsLog(self, mock_exists, mock_open, mock_print):
  193. mock_exists.return_value = False
  194. mock_watchdog = watchdog.CrashLogWatchdog(self.mock_log_path)
  195. mock_watchdog.caught_failure = False
  196. mock_watchdog._raise_on_condition = False
  197. mock_watchdog.stop()
  198. assert not mock_open.called
  199. assert not mock_print.called