test_job.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. # Copyright 2021-2024 Avaiga Private Limited
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  4. # the License. You may obtain a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  9. # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  10. # specific language governing permissions and limitations under the License.
  11. from datetime import timedelta
  12. from time import sleep
  13. from typing import Union, cast
  14. from unittest import mock
  15. from unittest.mock import MagicMock
  16. import pytest
  17. from taipy.config.common.scope import Scope
  18. from taipy.config.config import Config
  19. from taipy.core import JobId, TaskId
  20. from taipy.core._orchestrator._abstract_orchestrator import _AbstractOrchestrator
  21. from taipy.core._orchestrator._dispatcher._development_job_dispatcher import _DevelopmentJobDispatcher
  22. from taipy.core._orchestrator._dispatcher._standalone_job_dispatcher import _StandaloneJobDispatcher
  23. from taipy.core._orchestrator._orchestrator_factory import _OrchestratorFactory
  24. from taipy.core.config.job_config import JobConfig
  25. from taipy.core.data.in_memory import InMemoryDataNode
  26. from taipy.core.job._job_manager import _JobManager
  27. from taipy.core.job.job import Job
  28. from taipy.core.job.status import Status
  29. from taipy.core.scenario.scenario import Scenario
  30. from taipy.core.submission._submission_manager_factory import _SubmissionManagerFactory
  31. from taipy.core.task._task_manager import _TaskManager
  32. from taipy.core.task.task import Task
  33. @pytest.fixture
  34. def task_id():
  35. return TaskId("task_id1")
  36. @pytest.fixture
  37. def task(task_id):
  38. return Task(config_id="name", properties={}, function=print, input=[], output=[], id=task_id)
  39. @pytest.fixture
  40. def job_id():
  41. return JobId("id1")
  42. @pytest.fixture(scope="class")
  43. def scenario():
  44. return Scenario(
  45. "scenario_config",
  46. [],
  47. {},
  48. [],
  49. "SCENARIO_scenario_config",
  50. version="random_version_number",
  51. )
  52. @pytest.fixture
  53. def job(task, job_id):
  54. return Job(job_id, task, "submit_id", "SCENARIO_scenario_config")
  55. @pytest.fixture
  56. def replace_in_memory_write_fct():
  57. default_write = InMemoryDataNode.write
  58. InMemoryDataNode.write = _error
  59. yield
  60. InMemoryDataNode.write = default_write
  61. def _foo():
  62. return 42
  63. def _error():
  64. raise RuntimeError("Something bad has happened")
  65. def test_create_job(scenario, task, job):
  66. from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
  67. _ScenarioManagerFactory._build_manager()._set(scenario)
  68. assert job.id == "id1"
  69. assert task in job
  70. assert job.is_submitted()
  71. assert job.submit_id is not None
  72. assert job.submit_entity_id == "SCENARIO_scenario_config"
  73. assert job.submit_entity == scenario
  74. with mock.patch("taipy.core.get") as get_mck:
  75. get_mck.return_value = task
  76. assert job.get_label() == "name > " + job.id
  77. assert job.get_simple_label() == job.id
  78. def test_comparison(task):
  79. job_id_1 = JobId("id1")
  80. job_id_2 = JobId("id2")
  81. job_1 = Job(job_id_1, task, "submit_id", "scenario_entity_id")
  82. sleep(0.01) # Comparison is based on time, precision on Windows is not enough important
  83. job_2 = Job(job_id_2, task, "submit_id", "scenario_entity_id")
  84. assert job_1 < job_2
  85. assert job_2 > job_1
  86. assert job_1 <= job_2
  87. assert job_1 <= job_1
  88. assert job_2 >= job_1
  89. assert job_1 >= job_1
  90. assert job_1 == job_1
  91. assert job_1 != job_2
  92. def test_status_job(task):
  93. submission = _SubmissionManagerFactory._build_manager()._create(task.id, task._ID_PREFIX, task.config_id)
  94. job = Job("job_id", task, submission.id, "SCENARIO_scenario_config")
  95. submission.jobs = [job]
  96. assert job.is_submitted()
  97. assert job.is_skipped() is False
  98. assert job.is_pending() is False
  99. assert job.is_blocked() is False
  100. assert job.is_canceled() is False
  101. assert job.is_failed() is False
  102. assert job.is_completed() is False
  103. assert job.is_running() is False
  104. job.canceled()
  105. assert job.is_canceled()
  106. job.failed()
  107. assert job.is_failed()
  108. job.running()
  109. assert job.is_running()
  110. job.completed()
  111. assert job.is_completed()
  112. job.pending()
  113. assert job.is_pending()
  114. job.blocked()
  115. assert job.is_blocked()
  116. job.skipped()
  117. assert job.is_skipped()
  118. def test_stacktrace_job(task):
  119. submission = _SubmissionManagerFactory._build_manager()._create(task.id, task._ID_PREFIX, task.config_id)
  120. job = Job("job_id", task, submission.id, "SCENARIO_scenario_config")
  121. fake_stacktraces = [
  122. """Traceback (most recent call last):
  123. File "<stdin>", line 1, in <module>
  124. ZeroDivisionError: division by zero""",
  125. "Another error",
  126. "yet\nAnother\nError",
  127. ]
  128. job.stacktrace = fake_stacktraces
  129. assert job.stacktrace == fake_stacktraces
  130. def test_notification_job(task):
  131. subscribe = MagicMock()
  132. submission = _SubmissionManagerFactory._build_manager()._create(task.id, task._ID_PREFIX, task.config_id)
  133. job = Job("job_id", task, submission.id, "SCENARIO_scenario_config")
  134. submission.jobs = [job]
  135. job._on_status_change(subscribe)
  136. job.running()
  137. subscribe.assert_called_once_with(job)
  138. subscribe.reset_mock()
  139. job.completed()
  140. subscribe.assert_called_once_with(job)
  141. subscribe.reset_mock()
  142. job.skipped()
  143. subscribe.assert_called_once_with(job)
  144. def test_handle_exception_in_user_function(task_id, job_id):
  145. task = Task(config_id="name", properties={}, input=[], function=_error, output=[], id=task_id)
  146. submission = _SubmissionManagerFactory._build_manager()._create(task.id, task._ID_PREFIX, task.config_id)
  147. job = Job(job_id, task, submission.id, "scenario_entity_id")
  148. submission.jobs = [job]
  149. _dispatch(task, job)
  150. job = _JobManager._get(job_id)
  151. assert job.is_failed()
  152. assert 'raise RuntimeError("Something bad has happened")' in str(job.stacktrace[0])
  153. def test_handle_exception_in_input_data_node(task_id, job_id):
  154. data_node = InMemoryDataNode("data_node", scope=Scope.SCENARIO)
  155. task = Task(config_id="name", properties={}, input=[data_node], function=print, output=[], id=task_id)
  156. submission = _SubmissionManagerFactory._build_manager()._create(task.id, task._ID_PREFIX, task.config_id)
  157. job = Job(job_id, task, submission.id, "scenario_entity_id")
  158. submission.jobs = [job]
  159. _dispatch(task, job)
  160. job = _JobManager._get(job_id)
  161. assert job.is_failed()
  162. assert "taipy.core.exceptions.exceptions.NoData" in str(job.stacktrace[0])
  163. def test_handle_exception_in_ouptut_data_node(replace_in_memory_write_fct, task_id, job_id):
  164. data_node = InMemoryDataNode("data_node", scope=Scope.SCENARIO)
  165. task = Task(config_id="name", properties={}, input=[], function=_foo, output=[data_node], id=task_id)
  166. submission = _SubmissionManagerFactory._build_manager()._create(task.id, task._ID_PREFIX, task.config_id)
  167. job = Job(job_id, task, submission.id, "scenario_entity_id")
  168. submission.jobs = [job]
  169. _dispatch(task, job)
  170. job = _JobManager._get(job_id)
  171. assert job.is_failed()
  172. assert "taipy.core.exceptions.exceptions.DataNodeWritingError" in str(job.stacktrace[0])
  173. def test_auto_set_and_reload(current_datetime, job_id):
  174. task_1 = Task(config_id="name_1", properties={}, function=_foo, id=TaskId("task_1"))
  175. task_2 = Task(config_id="name_2", properties={}, function=_foo, id=TaskId("task_2"))
  176. submission = _SubmissionManagerFactory._build_manager()._create(task_1.id, task_1._ID_PREFIX, task_1.config_id)
  177. job_1 = Job(job_id, task_1, submission.id, "scenario_entity_id")
  178. submission.jobs = [job_1]
  179. _TaskManager._set(task_1)
  180. _TaskManager._set(task_2)
  181. _JobManager._set(job_1)
  182. job_2 = _JobManager._get(job_1, "submit_id_2")
  183. # auto set & reload on task attribute
  184. assert job_1.task.id == task_1.id
  185. assert job_2.task.id == task_1.id
  186. job_1.task = task_2
  187. assert job_1.task.id == task_2.id
  188. assert job_2.task.id == task_2.id
  189. job_2.task = task_1
  190. assert job_1.task.id == task_1.id
  191. assert job_2.task.id == task_1.id
  192. # auto set & reload on force attribute
  193. assert not job_1.force
  194. assert not job_2.force
  195. job_1.force = True
  196. assert job_1.force
  197. assert job_2.force
  198. job_2.force = False
  199. assert not job_1.force
  200. assert not job_2.force
  201. # auto set & reload on status attribute
  202. assert job_1.status == Status.SUBMITTED
  203. assert job_2.status == Status.SUBMITTED
  204. job_1.status = Status.CANCELED
  205. assert job_1.status == Status.CANCELED
  206. assert job_2.status == Status.CANCELED
  207. job_2.status = Status.BLOCKED
  208. assert job_1.status == Status.BLOCKED
  209. assert job_2.status == Status.BLOCKED
  210. # auto set & reload on creation_date attribute
  211. new_datetime = current_datetime + timedelta(1)
  212. new_datetime_1 = current_datetime + timedelta(1)
  213. job_1.creation_date = new_datetime_1
  214. assert job_1.creation_date == new_datetime_1
  215. assert job_2.creation_date == new_datetime_1
  216. job_2.creation_date = new_datetime
  217. assert job_1.creation_date == new_datetime
  218. assert job_2.creation_date == new_datetime
  219. with job_1 as job:
  220. assert job.task.id == task_1.id
  221. assert not job.force
  222. assert job.status == Status.BLOCKED
  223. assert job.creation_date == new_datetime
  224. assert job._is_in_context
  225. new_datetime_2 = new_datetime + timedelta(1)
  226. job.task = task_2
  227. job.force = True
  228. job.status = Status.COMPLETED
  229. job.creation_date = new_datetime_2
  230. assert job.task.id == task_1.id
  231. assert not job.force
  232. assert job.status == Status.BLOCKED
  233. assert job.creation_date == new_datetime
  234. assert job._is_in_context
  235. assert job_1.task.id == task_2.id
  236. assert job_1.force
  237. assert job_1.status == Status.COMPLETED
  238. assert job_1.creation_date == new_datetime_2
  239. assert not job_1._is_in_context
  240. def _dispatch(task: Task, job: Job, mode=JobConfig._DEVELOPMENT_MODE):
  241. Config.configure_job_executions(mode=mode)
  242. _OrchestratorFactory._build_dispatcher()
  243. _TaskManager._set(task)
  244. _JobManager._set(job)
  245. dispatcher: Union[_StandaloneJobDispatcher, _DevelopmentJobDispatcher] = _StandaloneJobDispatcher(
  246. cast(_AbstractOrchestrator, _OrchestratorFactory._orchestrator)
  247. )
  248. if mode == JobConfig._DEVELOPMENT_MODE:
  249. dispatcher = _DevelopmentJobDispatcher(cast(_AbstractOrchestrator, _OrchestratorFactory._orchestrator))
  250. dispatcher._dispatch(job)
  251. def test_is_deletable():
  252. with mock.patch("taipy.core.job._job_manager._JobManager._is_deletable") as mock_submit:
  253. task = Task(config_id="name_1", properties={}, function=_foo, id=TaskId("task_1"))
  254. job = Job(job_id, task, "submit_id_1", "scenario_entity_id")
  255. job.is_deletable()
  256. mock_submit.assert_called_once_with(job)