PythonThreadingTests.cpp 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "PythonTestingUtility.h"
  9. #include "PythonTraceMessageSink.h"
  10. #include <EditorPythonBindings/PythonCommon.h>
  11. #include <pybind11/embed.h>
  12. #include <pybind11/pybind11.h>
  13. #include <AzCore/RTTI/BehaviorContext.h>
  14. #include <AzFramework/StringFunc/StringFunc.h>
  15. #include <AzToolsFramework/API/EditorPythonRunnerRequestsBus.h>
  16. #include <AzToolsFramework/API/EditorPythonConsoleBus.h>
  17. namespace UnitTest
  18. {
  19. //////////////////////////////////////////////////////////////////////////
  20. // behavior
  21. struct PythonThreadNotifications
  22. : public AZ::EBusTraits
  23. {
  24. virtual AZ::s64 OnNotification(AZ::s64 value) = 0;
  25. };
  26. using PythonThreadNotificationBus = AZ::EBus<PythonThreadNotifications>;
  27. struct PythonThreadNotificationBusHandler final
  28. : public PythonThreadNotificationBus::Handler
  29. , public AZ::BehaviorEBusHandler
  30. {
  31. AZ_EBUS_BEHAVIOR_BINDER(PythonThreadNotificationBusHandler, "{CADEF35D-D88C-4DE0-B5FC-A88D383C124E}", AZ::SystemAllocator,
  32. OnNotification);
  33. virtual ~PythonThreadNotificationBusHandler() = default;
  34. AZ::s64 OnNotification(AZ::s64 value) override
  35. {
  36. AZ::s64 result = 0;
  37. CallResult(result, FN_OnNotification, value);
  38. return result;
  39. }
  40. void Reflect(AZ::ReflectContext* context)
  41. {
  42. if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
  43. {
  44. behaviorContext->EBus<PythonThreadNotificationBus>("PythonThreadNotificationBus")
  45. ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
  46. ->Attribute(AZ::Script::Attributes::Module, "test")
  47. ->Handler<PythonThreadNotificationBusHandler>()
  48. ->Event("OnNotification", &PythonThreadNotificationBus::Events::OnNotification)
  49. ;
  50. }
  51. }
  52. };
  53. //////////////////////////////////////////////////////////////////////////
  54. // fixtures
  55. struct PythonThreadingTest
  56. : public PythonTestingFixture
  57. {
  58. PythonTraceMessageSink m_testSink;
  59. void SetUp() override
  60. {
  61. PythonTestingFixture::SetUp();
  62. PythonTestingFixture::RegisterComponentDescriptors();
  63. }
  64. void TearDown() override
  65. {
  66. // clearing up memory
  67. m_testSink.CleanUp();
  68. PythonTestingFixture::TearDown();
  69. }
  70. };
  71. //////////////////////////////////////////////////////////////////////////
  72. // tests
  73. TEST_F(PythonThreadingTest, PythonInterface_ThreadLogic_Runs)
  74. {
  75. enum class LogTypes
  76. {
  77. Skip = 0,
  78. RanInThread
  79. };
  80. m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
  81. {
  82. if (AzFramework::StringFunc::Equal(window, "python"))
  83. {
  84. if (AzFramework::StringFunc::StartsWith(message, "RanInThread"))
  85. {
  86. return aznumeric_cast<int>(LogTypes::RanInThread);
  87. }
  88. }
  89. return aznumeric_cast<int>(LogTypes::Skip);
  90. };
  91. PythonThreadNotificationBusHandler pythonThreadNotificationBusHandler;
  92. pythonThreadNotificationBusHandler.Reflect(m_app.GetSerializeContext());
  93. pythonThreadNotificationBusHandler.Reflect(m_app.GetBehaviorContext());
  94. AZ::Entity e;
  95. Activate(e);
  96. SimulateEditorBecomingInitialized();
  97. try
  98. {
  99. // prepare handler on this thread
  100. const AZStd::string_view script =
  101. "import azlmbr.test\n"
  102. "\n"
  103. "def on_notification(args) :\n"
  104. " value = args[0] + 2\n"
  105. " print('RanInThread')\n"
  106. " return value\n"
  107. "\n"
  108. "handler = azlmbr.test.PythonThreadNotificationBusHandler()\n"
  109. "handler.connect()\n"
  110. "handler.add_callback('OnNotification', on_notification)\n";
  111. AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast(&AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByString, script, false /*printResult*/);
  112. // start thread; in thread issue notification
  113. auto threadCallback = []()
  114. {
  115. AZ::s64 result = 0;
  116. PythonThreadNotificationBus::BroadcastResult(result, &PythonThreadNotificationBus::Events::OnNotification, 40);
  117. EXPECT_EQ(42, result);
  118. };
  119. AZStd::thread theThread(threadCallback);
  120. theThread.join();
  121. }
  122. catch ([[maybe_unused]] const std::exception& e)
  123. {
  124. AZ_Error("UnitTest", false, "Failed during thread test with %s", e.what());
  125. }
  126. e.Deactivate();
  127. EXPECT_EQ(1, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::RanInThread)]);
  128. }
  129. TEST_F(PythonThreadingTest, PythonInterface_ThreadLogic_HandlesPythonException)
  130. {
  131. PythonThreadNotificationBusHandler pythonThreadNotificationBusHandler;
  132. pythonThreadNotificationBusHandler.Reflect(m_app.GetSerializeContext());
  133. pythonThreadNotificationBusHandler.Reflect(m_app.GetBehaviorContext());
  134. AZ::Entity e;
  135. Activate(e);
  136. SimulateEditorBecomingInitialized();
  137. try
  138. {
  139. AZ_TEST_START_TRACE_SUPPRESSION;
  140. // prepare handler on this thread, but will throw a Python exception
  141. pybind11::exec(R"(
  142. import azlmbr.test
  143. def on_notification(args):
  144. raise NotImplementedError("boom")
  145. handler = azlmbr.test.PythonThreadNotificationBusHandler()
  146. handler.connect()
  147. handler.add_callback('OnNotification', on_notification)
  148. )");
  149. // start thread; in thread issue notification
  150. auto threadCallback = []()
  151. {
  152. AZ::s64 result = 0;
  153. PythonThreadNotificationBus::BroadcastResult(result, &PythonThreadNotificationBus::Events::OnNotification, 40);
  154. EXPECT_EQ(0, result);
  155. };
  156. AZStd::thread theThread(threadCallback);
  157. theThread.join();
  158. // the Python script above raises an exception which causes two AZ_Error() message lines:
  159. // "Python callback threw an exception NotImplementedError : boom At : <string>(6) : on_notification"
  160. // "Python callback threw an exception TypeError : 'NoneType' object is not callable At : <string>(7) : on_notification"
  161. AZ_TEST_STOP_TRACE_SUPPRESSION(2);
  162. }
  163. catch ([[maybe_unused]] const std::exception& e)
  164. {
  165. AZ_Error("UnitTest", false, "Failed during thread test with %s", e.what());
  166. }
  167. e.Deactivate();
  168. }
  169. TEST_F(PythonThreadingTest, PythonInterface_DebugTrace_CallsOnTick)
  170. {
  171. enum class LogTypes
  172. {
  173. Skip = 0,
  174. OnPrewarning
  175. };
  176. m_testSink.m_evaluateMessage = [](const char* window, const char* message) -> int
  177. {
  178. if (AzFramework::StringFunc::Equal(window, "python"))
  179. {
  180. if (AzFramework::StringFunc::StartsWith(message, "OnPrewarning"))
  181. {
  182. return aznumeric_cast<int>(LogTypes::OnPrewarning);
  183. }
  184. }
  185. return aznumeric_cast<int>(LogTypes::Skip);
  186. };
  187. AZ::Entity e;
  188. Activate(e);
  189. SimulateEditorBecomingInitialized();
  190. try
  191. {
  192. // prepare handler on this thread
  193. pybind11::exec(R"(
  194. import azlmbr.debug
  195. def on_prewarning(args):
  196. print ('OnPrewarning: ' + args[0])
  197. handler = azlmbr.debug.TraceMessageBusHandler()
  198. handler.connect()
  199. handler.add_callback('OnPreWarning', on_prewarning)
  200. )");
  201. const size_t numWarnings = 64;
  202. auto doWarning = []()
  203. {
  204. AZ_Warning("PythonThreadingTest", false, "This is a warning message");
  205. };
  206. // start threads. In thread issue a warning.
  207. AZStd::vector<AZStd::thread> threads;
  208. threads.reserve(numWarnings);
  209. for (size_t i = 0; i < numWarnings; ++i)
  210. {
  211. threads.emplace_back(doWarning);
  212. }
  213. for (AZStd::thread& thread : threads)
  214. {
  215. thread.join();
  216. }
  217. // No prewarning calls should have happened because all of them were queued
  218. EXPECT_EQ(0, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::OnPrewarning)]);
  219. // Do one tick
  220. const float timeOneFrameSeconds = 0.016f; //approx 60 fps
  221. AZ::TickBus::Broadcast(&AZ::TickEvents::OnTick,
  222. timeOneFrameSeconds,
  223. AZ::ScriptTimePoint(AZStd::chrono::steady_clock::now()));
  224. // After one tick all the queued calls should have been processed
  225. EXPECT_EQ(numWarnings, m_testSink.m_evaluationMap[aznumeric_cast<int>(LogTypes::OnPrewarning)]);
  226. }
  227. catch ([[maybe_unused]] const std::exception& e)
  228. {
  229. AZ_Error("UnitTest", false, "Failed during thread test with %s", e.what());
  230. }
  231. e.Deactivate();
  232. }
  233. }