24
24
)
25
25
26
26
_TEST_PASS_CONTENT = """
27
+ import unittest
28
+
27
29
def test_func_pass():
28
30
assert True
31
+
32
+ class SomeTestCase(unittest.TestCase):
33
+ def test_class_func_pass(self):
34
+ assert True
29
35
"""
30
36
31
37
_TEST_FAIL_CONTENT = """
32
38
import pytest
39
+ import unittest
33
40
34
41
def test_func_fail():
35
42
assert False
36
43
37
44
_test_func_retries_skip_count = 0
45
+
38
46
def test_func_retries_skip():
39
47
global _test_func_retries_skip_count
40
48
_test_func_retries_skip_count += 1
41
49
if _test_func_retries_skip_count > 1:
42
50
pytest.skip()
43
51
assert False
44
52
53
+ _test_class_func_retries_skip_count = 0
54
+
55
+ class SomeTestCase(unittest.TestCase):
56
+ def test_class_func_fail(self):
57
+ assert False
58
+
59
+ def test_class_func_retries_skip(self):
60
+ global _test_class_func_retries_skip_count
61
+ _test_class_func_retries_skip_count += 1
62
+ if _test_class_func_retries_skip_count > 1:
63
+ pytest.skip()
64
+ assert False
45
65
"""
46
66
67
+
47
68
_TEST_PASS_ON_RETRIES_CONTENT = """
69
+ import unittest
70
+
48
71
_test_func_passes_4th_retry_count = 0
49
72
def test_func_passes_4th_retry():
50
73
global _test_func_passes_4th_retry_count
@@ -56,10 +79,19 @@ def test_func_passes_1st_retry():
56
79
global _test_func_passes_1st_retry_count
57
80
_test_func_passes_1st_retry_count += 1
58
81
assert _test_func_passes_1st_retry_count == 2
82
+
83
+ class SomeTestCase(unittest.TestCase):
84
+ _test_func_passes_4th_retry_count = 0
85
+
86
+ def test_func_passes_4th_retry(self):
87
+ SomeTestCase._test_func_passes_4th_retry_count += 1
88
+ assert SomeTestCase._test_func_passes_4th_retry_count == 5
89
+
59
90
"""
60
91
61
92
_TEST_ERRORS_CONTENT = """
62
93
import pytest
94
+ import unittest
63
95
64
96
@pytest.fixture
65
97
def fixture_fails_setup():
@@ -79,13 +111,22 @@ def test_func_fails_teardown(fixture_fails_teardown):
79
111
80
112
_TEST_SKIP_CONTENT = """
81
113
import pytest
114
+ import unittest
82
115
83
116
@pytest.mark.skip
84
117
def test_func_skip_mark():
85
118
assert True
86
119
87
120
def test_func_skip_inside():
88
121
pytest.skip()
122
+
123
+ class SomeTestCase(unittest.TestCase):
124
+ @pytest.mark.skip
125
+ def test_class_func_skip_mark(self):
126
+ assert True
127
+
128
+ def test_class_func_skip_inside(self):
129
+ pytest.skip()
89
130
"""
90
131
91
132
@@ -109,7 +150,7 @@ def test_pytest_atr_no_ddtrace_does_not_retry(self):
109
150
self .testdir .makepyfile (test_pass_on_retries = _TEST_PASS_ON_RETRIES_CONTENT )
110
151
self .testdir .makepyfile (test_skip = _TEST_SKIP_CONTENT )
111
152
rec = self .inline_run ()
112
- rec .assertoutcome (passed = 2 , failed = 6 , skipped = 2 )
153
+ rec .assertoutcome (passed = 3 , failed = 9 , skipped = 4 )
113
154
assert len (self .pop_spans ()) == 0
114
155
115
156
def test_pytest_atr_env_var_disables_retrying (self ):
@@ -121,7 +162,7 @@ def test_pytest_atr_env_var_disables_retrying(self):
121
162
122
163
with mock .patch ("ddtrace.internal.ci_visibility.recorder.ddconfig" , _get_default_civisibility_ddconfig ()):
123
164
rec = self .inline_run ("--ddtrace" , "-s" , extra_env = {"DD_CIVISIBILITY_FLAKY_RETRY_ENABLED" : "0" })
124
- rec .assertoutcome (passed = 2 , failed = 6 , skipped = 2 )
165
+ rec .assertoutcome (passed = 3 , failed = 9 , skipped = 4 )
125
166
assert len (self .pop_spans ()) > 0
126
167
127
168
def test_pytest_atr_env_var_does_not_override_api (self ):
@@ -137,7 +178,7 @@ def test_pytest_atr_env_var_does_not_override_api(self):
137
178
return_value = TestVisibilityAPISettings (flaky_test_retries_enabled = False ),
138
179
):
139
180
rec = self .inline_run ("--ddtrace" , extra_env = {"DD_CIVISIBILITY_FLAKY_RETRY_ENABLED" : "1" })
140
- rec .assertoutcome (passed = 2 , failed = 6 , skipped = 2 )
181
+ rec .assertoutcome (passed = 3 , failed = 9 , skipped = 4 )
141
182
assert len (self .pop_spans ()) > 0
142
183
143
184
def test_pytest_atr_spans (self ):
@@ -178,6 +219,15 @@ def test_pytest_atr_spans(self):
178
219
func_fail_retries += 1
179
220
assert func_fail_retries == 5
180
221
222
+ class_func_fail_spans = _get_spans_from_list (spans , "test" , "SomeTestCase::test_class_func_fail" )
223
+ assert len (class_func_fail_spans ) == 6
224
+ class_func_fail_retries = 0
225
+ for class_func_fail_span in class_func_fail_spans :
226
+ assert class_func_fail_span .get_tag ("test.status" ) == "fail"
227
+ if class_func_fail_span .get_tag ("test.is_retry" ) == "true" :
228
+ class_func_fail_retries += 1
229
+ assert class_func_fail_retries == 5
230
+
181
231
func_fail_skip_spans = _get_spans_from_list (spans , "test" , "test_func_retries_skip" )
182
232
assert len (func_fail_skip_spans ) == 6
183
233
func_fail_skip_retries = 0
@@ -188,6 +238,18 @@ def test_pytest_atr_spans(self):
188
238
func_fail_skip_retries += 1
189
239
assert func_fail_skip_retries == 5
190
240
241
+ class_func_fail_skip_spans = _get_spans_from_list (spans , "test" , "SomeTestCase::test_class_func_retries_skip" )
242
+ assert len (class_func_fail_skip_spans ) == 6
243
+ class_func_fail_skip_retries = 0
244
+ for class_func_fail_skip_span in class_func_fail_skip_spans :
245
+ class_func_fail_skip_is_retry = class_func_fail_skip_span .get_tag ("test.is_retry" ) == "true"
246
+ assert class_func_fail_skip_span .get_tag ("test.status" ) == (
247
+ "skip" if class_func_fail_skip_is_retry else "fail"
248
+ )
249
+ if class_func_fail_skip_is_retry :
250
+ class_func_fail_skip_retries += 1
251
+ assert class_func_fail_skip_retries == 5
252
+
191
253
func_pass_spans = _get_spans_from_list (spans , "test" , "test_func_pass" )
192
254
assert len (func_pass_spans ) == 1
193
255
assert func_pass_spans [0 ].get_tag ("test.status" ) == "pass"
@@ -205,7 +267,17 @@ def test_pytest_atr_spans(self):
205
267
assert func_skip_inside_spans [0 ].get_tag ("test.status" ) == "skip"
206
268
assert func_skip_inside_spans [0 ].get_tag ("test.is_retry" ) is None
207
269
208
- assert len (spans ) == 31
270
+ class_func_skip_mark_spans = _get_spans_from_list (spans , "test" , "SomeTestCase::test_class_func_skip_mark" )
271
+ assert len (class_func_skip_mark_spans ) == 1
272
+ assert class_func_skip_mark_spans [0 ].get_tag ("test.status" ) == "skip"
273
+ assert class_func_skip_mark_spans [0 ].get_tag ("test.is_retry" ) is None
274
+
275
+ class_func_skip_inside_spans = _get_spans_from_list (spans , "test" , "SomeTestCase::test_class_func_skip_inside" )
276
+ assert len (class_func_skip_inside_spans ) == 1
277
+ assert class_func_skip_inside_spans [0 ].get_tag ("test.status" ) == "skip"
278
+ assert class_func_skip_inside_spans [0 ].get_tag ("test.is_retry" ) is None
279
+
280
+ assert len (spans ) == 51
209
281
210
282
def test_pytest_atr_fails_session_when_test_fails (self ):
211
283
self .testdir .makepyfile (test_pass = _TEST_PASS_CONTENT )
@@ -216,7 +288,7 @@ def test_pytest_atr_fails_session_when_test_fails(self):
216
288
rec = self .inline_run ("--ddtrace" )
217
289
spans = self .pop_spans ()
218
290
assert rec .ret == 1
219
- assert len (spans ) == 28
291
+ assert len (spans ) == 48
220
292
221
293
def test_pytest_atr_passes_session_when_test_pass (self ):
222
294
self .testdir .makepyfile (test_pass = _TEST_PASS_CONTENT )
@@ -226,9 +298,14 @@ def test_pytest_atr_passes_session_when_test_pass(self):
226
298
rec = self .inline_run ("--ddtrace" )
227
299
spans = self .pop_spans ()
228
300
assert rec .ret == 0
229
- assert len (spans ) == 15
301
+ assert len (spans ) == 23
230
302
231
303
def test_pytest_atr_does_not_retry_failed_setup_or_teardown (self ):
304
+ # NOTE: This feature only works for regular pytest tests. For tests inside unittest classes, setup and teardown
305
+ # happens at the 'call' phase, and we don't have a way to detect that the error happened during setup/teardown,
306
+ # so tests will be retried as if they were failing tests.
307
+ # See <https://docs.pytest.org/en/8.3.x/how-to/unittest.html#pdb-unittest-note>.
308
+
232
309
self .testdir .makepyfile (test_errors = _TEST_ERRORS_CONTENT )
233
310
rec = self .inline_run ("--ddtrace" )
234
311
spans = self .pop_spans ()
0 commit comments