test_workflow.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import logging
  2. import pytest
  3. from unittest.mock import ANY, Mock, call, patch
  4. import cipy
  5. import cipy.workflow
  6. from cipy.workflow import Job
  7. from matchers import HasAttributes
  8. class NoOp(cipy.action.Action):
  9. def run(self, context: cipy.Context) -> cipy.Status:
  10. return cipy.Status.NOT_RUN
  11. class MockRunAction(cipy.action.Action, arbitrary_types_allowed=True):
  12. mock: Mock
  13. def run(self, context: cipy.Context) -> cipy.Status:
  14. return self.mock.run(name=self.name, context=context)
  15. def test_workflow_constructs_depends_map() -> None:
  16. workflow = cipy.workflow.Workflow(
  17. name="Test",
  18. jobs=[
  19. Job(id="A", action=NoOp(name="A")),
  20. Job(id="B", needs=["A"], action=NoOp(name="B")),
  21. Job(id="C", needs=["A"], action=NoOp(name="C")),
  22. Job(id="D", needs=["B", "C"], action=NoOp(name="D")),
  23. ],
  24. )
  25. assert workflow._depends == {
  26. "A": [],
  27. "B": ["A"],
  28. "C": ["A"],
  29. "D": ["B", "C"],
  30. }
  31. def test_workflow_calls_in_order() -> None:
  32. mock = Mock()
  33. mock.run.return_value = cipy.Status.SUCCESS
  34. workflow = cipy.workflow.Workflow(
  35. name="Test",
  36. jobs=[
  37. Job(id="A", action=MockRunAction(name="A", mock=mock)),
  38. Job(id="B", needs=["A"], action=MockRunAction(name="B", mock=mock)),
  39. Job(id="C", needs=["A"], action=MockRunAction(name="C", mock=mock)),
  40. Job(id="D", needs=["B", "C"], action=MockRunAction(name="D", mock=mock)),
  41. ],
  42. )
  43. workflow.run(cipy.Context())
  44. mock.run.assert_has_calls(
  45. [
  46. call(name="A", context=ANY),
  47. call(name="B", context=ANY),
  48. call(name="C", context=ANY),
  49. call(name="D", context=ANY),
  50. ],
  51. any_order=False,
  52. )
  53. def test_workflow_stops_after_failure() -> None:
  54. mock = Mock()
  55. mock.run.side_effect = [cipy.Status.SUCCESS, cipy.Status.FAILURE]
  56. workflow = cipy.workflow.Workflow(
  57. name="Test",
  58. jobs=[
  59. Job(id="A", action=MockRunAction(name="A", mock=mock)),
  60. Job(id="B", needs=["A"], action=MockRunAction(name="B", mock=mock)),
  61. Job(id="C", needs=["A"], action=MockRunAction(name="C", mock=mock)),
  62. Job(id="D", needs=["B", "C"], action=MockRunAction(name="D", mock=mock)),
  63. ],
  64. )
  65. workflow.run(cipy.Context())
  66. mock.run.assert_has_calls(
  67. [
  68. call(name="A", context=ANY),
  69. call(name="B", context=ANY),
  70. ],
  71. any_order=False,
  72. )
  73. assert len(mock.run.call_args_list) == 2
  74. def test_matrix_expands_combinatorally(ci_logger) -> None:
  75. mock = Mock()
  76. mock.run.return_value = cipy.Status.SUCCESS
  77. matrix = cipy.workflow.Matrix(
  78. on={
  79. "compiler": ["clang12", "clang18"],
  80. "os": ["ubuntu22", "ubuntu24"],
  81. "release": [True, False],
  82. },
  83. uses=MockRunAction(name="Test", mock=mock),
  84. )
  85. with patch("copy.deepcopy", side_effect=lambda x: x):
  86. matrix.run(cipy.Context())
  87. assert len(mock.run.call_args_list) == 8
  88. def call_with_args(**kwargs):
  89. return call(HasAttributes(levelno=logging.INFO, args=({**kwargs})))
  90. ci_logger.format.assert_has_calls(
  91. [
  92. call_with_args(compiler="clang12", os="ubuntu22", release=True),
  93. call_with_args(compiler="clang12", os="ubuntu22", release=False),
  94. call_with_args(compiler="clang12", os="ubuntu24", release=True),
  95. call_with_args(compiler="clang12", os="ubuntu24", release=False),
  96. call_with_args(compiler="clang18", os="ubuntu22", release=True),
  97. call_with_args(compiler="clang18", os="ubuntu22", release=False),
  98. call_with_args(compiler="clang18", os="ubuntu24", release=True),
  99. call_with_args(compiler="clang18", os="ubuntu24", release=False),
  100. ],
  101. any_order=True,
  102. )
  103. def test_matrix_deep_copies_used_action(ci_logger) -> None:
  104. mock = Mock()
  105. mock.run.return_value = cipy.Status.SUCCESS
  106. matrix = cipy.workflow.Matrix(
  107. on={
  108. "compiler": ["clang12", "clang18"],
  109. "os": ["ubuntu22", "ubuntu24"],
  110. "release": [True, False],
  111. },
  112. uses=MockRunAction(name="Test", mock=mock),
  113. )
  114. matrix.run(cipy.Context())
  115. assert len(mock.run.call_args_list) == 0
  116. def test_matrix_fails_fast(ci_logger) -> None:
  117. mock = Mock()
  118. mock.run.side_effect = [cipy.Status.SUCCESS, cipy.Status.FAILURE]
  119. mock.run.return_value = cipy.Status.SUCCESS
  120. matrix = cipy.workflow.Matrix(
  121. on={
  122. "compiler": ["clang12", "clang18"],
  123. "os": ["ubuntu22", "ubuntu24"],
  124. "release": [True, False],
  125. },
  126. uses=MockRunAction(name="Test", mock=mock),
  127. )
  128. with patch("copy.deepcopy", side_effect=lambda x: x):
  129. matrix.run(cipy.Context())
  130. assert len(mock.run.call_args_list) == 2
  131. def test_matrix_can_disable_failfast(ci_logger) -> None:
  132. mock = Mock()
  133. mock.run.side_effect = [
  134. cipy.Status.SUCCESS,
  135. cipy.Status.FAILURE,
  136. cipy.Status.SUCCESS,
  137. cipy.Status.SUCCESS,
  138. ]
  139. matrix = cipy.workflow.Matrix(
  140. on={
  141. "compiler": ["clang12", "clang18"],
  142. "os": ["ubuntu22", "ubuntu24"],
  143. },
  144. uses=MockRunAction(name="Test", mock=mock),
  145. fail_fast=False,
  146. )
  147. with patch("copy.deepcopy", side_effect=lambda x: x):
  148. matrix.run(cipy.Context())
  149. assert len(mock.run.call_args_list) == 4