Skip to content

Commit 1bfe138

Browse files
authored
Tech Debt: Retry failed integration tests (#1269)
* Add support for retrying failed integration tests and uploading results
1 parent 8d09d73 commit 1bfe138

File tree

2 files changed

+96
-3
lines changed

2 files changed

+96
-3
lines changed

.github/workflows/build.yml

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,20 @@ jobs:
125125
echo "synapse_pat_available=true" >> $GITHUB_OUTPUT;
126126
fi
127127
128+
- name: Download Failed Tests from Previous Attempt
129+
if: ${{ github.run_attempt > 1 && (contains(fromJSON('["3.9"]'), matrix.python) || contains(fromJSON('["3.13"]'), matrix.python)) && steps.secret-check.outputs.secrets_available == 'true'}}
130+
uses: actions/download-artifact@v4
131+
with:
132+
name: failed-tests-${{ matrix.os }}-${{ matrix.python }}
133+
continue-on-error: true
134+
128135
# run integration tests iff the decryption keys for the test configuration are available.
129136
# they will not be available in pull requests from forks.
130137
# run integration tests on the oldest and newest supported versions of python.
131138
# we don't run on the entire matrix to avoid a 3xN set of concurrent tests against
132139
# the target server where N is the number of supported python versions.
133140
- name: run-integration-tests
141+
id: integration_tests
134142
shell: bash
135143

136144
# keep versions consistent with the first and last from the strategy matrix
@@ -204,11 +212,96 @@ jobs:
204212
echo "Running integration tests for Max Python version (3.13) - ignoring synchronous tests"
205213
fi
206214
207-
# use loadscope to avoid issues running tests concurrently that share scoped fixtures
208-
pytest -sv --reruns 3 --cov-append --cov=. --cov-report xml tests/integration -n 8 $IGNORE_FLAGS --dist loadscope
215+
# Check if we should run only failed tests from previous attempt
216+
if [[ -f failed_tests.txt && -s failed_tests.txt ]]; then
217+
echo "::notice::Retry attempt ${{ github.run_attempt }} detected - running only previously failed tests"
218+
cat failed_tests.txt
219+
220+
# Run only the failed tests
221+
pytest -sv --reruns 3 --cov-append --cov=. --cov-report xml \
222+
--junit-xml=test-results.xml \
223+
-n 8 --dist loadscope \
224+
$(cat failed_tests.txt | tr '\n' ' ')
225+
else
226+
echo "::notice::First attempt or no previous failures - running full integration test suite"
227+
228+
# use loadscope to avoid issues running tests concurrently that share scoped fixtures
229+
pytest -sv --reruns 3 --cov-append --cov=. --cov-report xml \
230+
--junit-xml=test-results.xml \
231+
tests/integration -n 8 $IGNORE_FLAGS --dist loadscope
232+
fi
209233
210234
# Execute the CLI tests in a non-dist way because they were causing some test instability when being run concurrently
211235
pytest -sv --reruns 3 --cov-append --cov=. --cov-report xml tests/integration/synapseclient/test_command_line_client.py
236+
237+
- name: Extract Failed Tests
238+
if: always() && steps.integration_tests.outcome == 'failure'
239+
shell: bash
240+
run: |
241+
python -c "
242+
import xml.etree.ElementTree as ET
243+
import os
244+
tree = ET.parse('test-results.xml')
245+
root = tree.getroot()
246+
failed = []
247+
for testcase in root.iter('testcase'):
248+
if testcase.find('failure') is not None or testcase.find('error') is not None:
249+
classname = testcase.get('classname')
250+
name = testcase.get('name')
251+
file_attr = testcase.get('file')
252+
253+
# Use the file attribute if available, otherwise convert classname
254+
if file_attr:
255+
# file attribute is already in the correct format
256+
test_path = f'{file_attr}::{classname.split(\".\")[-1]}::{name}'
257+
else:
258+
# Convert classname from dot notation to file path
259+
# e.g., 'tests.integration.foo.test_bar.TestClass' -> 'tests/integration/foo/test_bar.py::TestClass'
260+
parts = classname.split('.')
261+
# Find the test file (usually starts with 'test_')
262+
module_parts = []
263+
class_parts = []
264+
found_test_file = False
265+
for part in parts:
266+
if not found_test_file:
267+
module_parts.append(part)
268+
if part.startswith('test_'):
269+
found_test_file = True
270+
else:
271+
class_parts.append(part)
272+
273+
file_path = '/'.join(module_parts) + '.py'
274+
if class_parts:
275+
test_path = f'{file_path}::{\"::\".join(class_parts)}::{name}'
276+
else:
277+
test_path = f'{file_path}::{name}'
278+
279+
failed.append(test_path)
280+
281+
with open('failed_tests.txt', 'w') as f:
282+
f.write('\n'.join(failed))
283+
print(f'Found {len(failed)} failed tests')
284+
for test in failed:
285+
print(f' - {test}')
286+
print(f'Current attempt: ${{ github.run_attempt }}')
287+
"
288+
289+
- name: Upload Failed Tests for Next Attempt
290+
if: always() && steps.integration_tests.outcome == 'failure' && github.run_attempt < 3
291+
uses: actions/upload-artifact@v4
292+
with:
293+
name: failed-tests-${{ matrix.os }}-${{ matrix.python }}
294+
path: failed_tests.txt
295+
retention-days: 2
296+
overwrite: true
297+
298+
- name: Fail job if integration tests failed after all retries
299+
if: always() && steps.integration_tests.outcome == 'failure'
300+
shell: bash
301+
run: |
302+
echo "::error::Integration tests failed after ${{ github.run_attempt }} attempt(s)"
303+
exit 1
304+
212305
- name: Upload coverage report
213306
id: upload_coverage_report
214307
uses: actions/upload-artifact@v4

tests/integration/synapseclient/models/synchronous/test_schema_organization.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,9 @@ async def test_store_and_get(self, json_schema: JSONSchema) -> None:
210210
assert json_schema.organization_name
211211
assert json_schema.uri
212212
assert json_schema.organization_id
213-
assert json_schema.id
214213
assert json_schema.created_by
215214
assert json_schema.created_on
215+
assert not json_schema.id
216216
# AND it should be getable by future instances with the same name
217217
js2 = JSONSchema(json_schema.name, json_schema.organization_name)
218218
js2.get(synapse_client=self.syn)

0 commit comments

Comments
 (0)