[CI] Enable retry logic for flaky CI tests (#14983)

This commit is contained in:
Alison Shao
2025-12-22 22:23:42 -08:00
committed by GitHub
parent 883747ced1
commit ac42797cf7
5 changed files with 392 additions and 68 deletions
+62 -14
View File
@@ -38,6 +38,7 @@ jobs:
multimodal_gen: ${{ steps.filter.outputs.multimodal_gen || steps.scheduled.outputs.multimodal_gen }} multimodal_gen: ${{ steps.filter.outputs.multimodal_gen || steps.scheduled.outputs.multimodal_gen }}
max_parallel: ${{ steps.set-parallel.outputs.max_parallel }} max_parallel: ${{ steps.set-parallel.outputs.max_parallel }}
b200_runner: ${{ steps.set-runner.outputs.b200_runner }} b200_runner: ${{ steps.set-runner.outputs.b200_runner }}
enable_retry: ${{ steps.set-retry.outputs.enable_retry }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -91,6 +92,12 @@ jobs:
echo "b200_runner=4-gpu-b200" >> $GITHUB_OUTPUT echo "b200_runner=4-gpu-b200" >> $GITHUB_OUTPUT
fi fi
- name: Enable retry for CI
id: set-retry
run: |
echo "enable_retry=true" >> $GITHUB_OUTPUT
echo "Retry logic enabled for CI"
- name: Show filter results in summary (table) - name: Show filter results in summary (table)
run: | run: |
{ {
@@ -103,6 +110,7 @@ jobs:
echo "| multimodal_gen | ${{ steps.filter.outputs.multimodal_gen || steps.scheduled.outputs.multimodal_gen }} |" echo "| multimodal_gen | ${{ steps.filter.outputs.multimodal_gen || steps.scheduled.outputs.multimodal_gen }} |"
echo "| max_parallel | ${{ steps.set-parallel.outputs.max_parallel }} |" echo "| max_parallel | ${{ steps.set-parallel.outputs.max_parallel }} |"
echo "| b200_runner | ${{ steps.set-runner.outputs.b200_runner }} |" echo "| b200_runner | ${{ steps.set-runner.outputs.b200_runner }} |"
echo "| enable_retry | ${{ steps.set-retry.outputs.enable_retry }} |"
} >> $GITHUB_STEP_SUMMARY } >> $GITHUB_STEP_SUMMARY
# =============================================== PR Gate ==================================================== # =============================================== PR Gate ====================================================
@@ -676,7 +684,11 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
run: | run: |
cd test/srt cd test/srt
python3 run_suite.py --suite quantization_test RETRY_FLAG=""
if [[ "${{ needs.check-changes.outputs.enable_retry }}" == "true" ]]; then
RETRY_FLAG="--enable-retry"
fi
python3 run_suite.py --suite quantization_test $RETRY_FLAG
unit-test-backend-1-gpu: unit-test-backend-1-gpu:
needs: [check-changes, call-gate, stage-a-test-1] needs: [check-changes, call-gate, stage-a-test-1]
@@ -718,7 +730,11 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
run: | run: |
cd test/srt cd test/srt
python3 run_suite.py --suite per-commit-1-gpu --auto-partition-id ${{ matrix.part }} --auto-partition-size 15 RETRY_FLAG=""
if [[ "${{ needs.check-changes.outputs.enable_retry }}" == "true" ]]; then
RETRY_FLAG="--enable-retry"
fi
python3 run_suite.py --suite per-commit-1-gpu --auto-partition-id ${{ matrix.part }} --auto-partition-size 15 $RETRY_FLAG
stage-b-test-4-gpu-b200: stage-b-test-4-gpu-b200:
@@ -801,7 +817,11 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
run: | run: |
cd test/srt cd test/srt
python3 run_suite.py --suite per-commit-2-gpu --auto-partition-id ${{ matrix.part }} --auto-partition-size 2 RETRY_FLAG=""
if [[ "${{ needs.check-changes.outputs.enable_retry }}" == "true" ]]; then
RETRY_FLAG="--enable-retry"
fi
python3 run_suite.py --suite per-commit-2-gpu --auto-partition-id ${{ matrix.part }} --auto-partition-size 2 $RETRY_FLAG
unit-test-backend-4-gpu: unit-test-backend-4-gpu:
needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200] needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200]
@@ -842,7 +862,11 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
run: | run: |
cd test/srt cd test/srt
python3 run_suite.py --suite per-commit-4-gpu --auto-partition-id ${{ matrix.part }} --auto-partition-size 3 RETRY_FLAG=""
if [[ "${{ needs.check-changes.outputs.enable_retry }}" == "true" ]]; then
RETRY_FLAG="--enable-retry"
fi
python3 run_suite.py --suite per-commit-4-gpu --auto-partition-id ${{ matrix.part }} --auto-partition-size 3 $RETRY_FLAG
unit-test-backend-8-gpu-h200: unit-test-backend-8-gpu-h200:
needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200] needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200]
@@ -889,7 +913,11 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
run: | run: |
cd test/srt cd test/srt
python3 run_suite.py --suite per-commit-8-gpu-h200 --auto-partition-id ${{ matrix.part }} --auto-partition-size 3 RETRY_FLAG=""
if [[ "${{ needs.check-changes.outputs.enable_retry }}" == "true" ]]; then
RETRY_FLAG="--enable-retry"
fi
python3 run_suite.py --suite per-commit-8-gpu-h200 --auto-partition-id ${{ matrix.part }} --auto-partition-size 3 $RETRY_FLAG
unit-test-backend-8-gpu-h20: unit-test-backend-8-gpu-h20:
needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200] needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200]
@@ -931,7 +959,11 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
run: | run: |
cd test/srt cd test/srt
python3 run_suite.py --suite per-commit-8-gpu-h20 --auto-partition-id ${{ matrix.part }} --auto-partition-size 2 RETRY_FLAG=""
if [[ "${{ needs.check-changes.outputs.enable_retry }}" == "true" ]]; then
RETRY_FLAG="--enable-retry"
fi
python3 run_suite.py --suite per-commit-8-gpu-h20 --auto-partition-id ${{ matrix.part }} --auto-partition-size 2 $RETRY_FLAG
performance-test-1-gpu-part-1: performance-test-1-gpu-part-1:
needs: [check-changes, call-gate, stage-a-test-1] needs: [check-changes, call-gate, stage-a-test-1]
@@ -1220,10 +1252,10 @@ jobs:
pip install -e . pip install -e .
- name: Evaluate accuracy - name: Evaluate accuracy
timeout-minutes: 20 timeout-minutes: 25
run: | run: |
cd test/srt cd test/srt
python3 test_eval_accuracy_large.py python3 -m sglang.test.ci.run_with_retry test_eval_accuracy_large.py
accuracy-test-2-gpu: accuracy-test-2-gpu:
needs: [check-changes, call-gate, accuracy-test-1-gpu] needs: [check-changes, call-gate, accuracy-test-1-gpu]
@@ -1260,10 +1292,10 @@ jobs:
pip install -e . pip install -e .
- name: Evaluate accuracy (TP=2) - name: Evaluate accuracy (TP=2)
timeout-minutes: 20 timeout-minutes: 25
run: | run: |
cd test/srt cd test/srt
python3 test_moe_eval_accuracy_large.py python3 -m sglang.test.ci.run_with_retry test_moe_eval_accuracy_large.py
unit-test-deepep-4-gpu: unit-test-deepep-4-gpu:
needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200] needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200]
@@ -1300,7 +1332,11 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
run: | run: |
cd test/srt cd test/srt
python3 run_suite.py --suite per-commit-4-gpu-deepep RETRY_FLAG=""
if [[ "${{ needs.check-changes.outputs.enable_retry }}" == "true" ]]; then
RETRY_FLAG="--enable-retry"
fi
python3 run_suite.py --suite per-commit-4-gpu-deepep $RETRY_FLAG
unit-test-deepep-8-gpu: unit-test-deepep-8-gpu:
needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200] needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200]
@@ -1337,7 +1373,11 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
run: | run: |
cd test/srt cd test/srt
python3 run_suite.py --suite per-commit-8-gpu-h200-deepep RETRY_FLAG=""
if [[ "${{ needs.check-changes.outputs.enable_retry }}" == "true" ]]; then
RETRY_FLAG="--enable-retry"
fi
python3 run_suite.py --suite per-commit-8-gpu-h200-deepep $RETRY_FLAG
unit-test-backend-4-gpu-b200: unit-test-backend-4-gpu-b200:
needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200] needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200]
@@ -1379,7 +1419,11 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
run: | run: |
cd test/srt cd test/srt
IS_BLACKWELL=1 python3 run_suite.py --suite per-commit-4-gpu-b200 --auto-partition-id ${{ matrix.part }} --auto-partition-size 3 --timeout-per-file 1800 RETRY_FLAG=""
if [[ "${{ needs.check-changes.outputs.enable_retry }}" == "true" ]]; then
RETRY_FLAG="--enable-retry"
fi
IS_BLACKWELL=1 python3 run_suite.py --suite per-commit-4-gpu-b200 --auto-partition-id ${{ matrix.part }} --auto-partition-size 3 --timeout-per-file 1800 $RETRY_FLAG
unit-test-backend-4-gpu-gb200: unit-test-backend-4-gpu-gb200:
needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200, sgl-kernel-build-wheels-arm] needs: [check-changes, call-gate, unit-test-backend-2-gpu, stage-b-test-4-gpu-b200, sgl-kernel-build-wheels-arm]
@@ -1418,7 +1462,11 @@ jobs:
timeout-minutes: 45 timeout-minutes: 45
run: | run: |
cd test/srt cd test/srt
python3 run_suite.py --suite per-commit-4-gpu-gb200 --auto-partition-id 0 --auto-partition-size 1 --timeout-per-file 3600 RETRY_FLAG=""
if [[ "${{ needs.check-changes.outputs.enable_retry }}" == "true" ]]; then
RETRY_FLAG="--enable-retry"
fi
python3 run_suite.py --suite per-commit-4-gpu-gb200 --auto-partition-id 0 --auto-partition-size 1 --timeout-per-file 3600 $RETRY_FLAG
pr-test-finish: pr-test-finish:
needs: needs:
+207 -52
View File
@@ -1,4 +1,6 @@
import logging
import os import os
import re
import subprocess import subprocess
import threading import threading
import time import time
@@ -8,6 +10,10 @@ from typing import Callable, List, Optional, Union
from sglang.srt.utils.common import kill_process_tree from sglang.srt.utils.common import kill_process_tree
from sglang.test.ci.ci_register import CIRegistry from sglang.test.ci.ci_register import CIRegistry
# Configure logger to output to stdout
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
@dataclass @dataclass
class TestFile: class TestFile:
@@ -15,6 +21,62 @@ class TestFile:
estimated_time: float = 60 estimated_time: float = 60
# Patterns that indicate retriable accuracy/performance failures
RETRIABLE_PATTERNS = [
r"AssertionError:.*not greater than",
r"AssertionError:.*not less than",
r"AssertionError:.*not equal to",
r"AssertionError:.*!=.*expected",
r"accuracy",
r"score",
r"latency",
r"throughput",
r"timeout",
]
# Patterns that indicate non-retriable failures (real code errors)
NON_RETRIABLE_PATTERNS = [
r"SyntaxError",
r"ImportError",
r"ModuleNotFoundError",
r"NameError",
r"TypeError",
r"AttributeError",
r"RuntimeError",
r"CUDA out of memory",
r"OOM",
r"Segmentation fault",
r"core dumped",
r"ConnectionRefusedError",
r"FileNotFoundError",
]
def is_retriable_failure(output: str) -> tuple[bool, str]:
"""
Determine if a test failure is retriable based on output patterns.
Returns:
tuple: (is_retriable, reason)
"""
# Check for non-retriable patterns first
for pattern in NON_RETRIABLE_PATTERNS:
if re.search(pattern, output, re.IGNORECASE):
return False, f"non-retriable error: {pattern}"
# Check for retriable patterns
for pattern in RETRIABLE_PATTERNS:
if re.search(pattern, output, re.IGNORECASE):
return True, f"retriable pattern: {pattern}"
# If we have an AssertionError but didn't match non-retriable, assume retriable
if re.search(r"AssertionError", output):
return True, "AssertionError (assuming retriable)"
# Default: not retriable
return False, "unknown failure type"
def run_with_timeout( def run_with_timeout(
func: Callable, func: Callable,
args: tuple = (), args: tuple = (),
@@ -39,10 +101,21 @@ def run_with_timeout(
return ret_value[0] return ret_value[0]
def write_github_step_summary(content: str):
"""Write content to GitHub Step Summary if available."""
summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
if summary_file:
with open(summary_file, "a") as f:
f.write(content)
def run_unittest_files( def run_unittest_files(
files: Union[List[TestFile], List[CIRegistry]], files: Union[List[TestFile], List[CIRegistry]],
timeout_per_file: float, timeout_per_file: float,
continue_on_error: bool = False, continue_on_error: bool = False,
enable_retry: bool = False,
max_attempts: int = 2,
retry_wait_seconds: int = 60,
): ):
""" """
Run a list of test files. Run a list of test files.
@@ -52,11 +125,16 @@ def run_unittest_files(
timeout_per_file: Timeout in seconds for each test file timeout_per_file: Timeout in seconds for each test file
continue_on_error: If True, continue running remaining tests even if one fails. continue_on_error: If True, continue running remaining tests even if one fails.
If False, stop at first failure (default behavior for PR tests). If False, stop at first failure (default behavior for PR tests).
enable_retry: If True, retry failed tests that appear to be accuracy/performance
assertion failures (not code errors).
max_attempts: Maximum number of attempts per file including initial run (default: 2).
retry_wait_seconds: Seconds to wait between retries (default: 60).
""" """
tic = time.perf_counter() tic = time.perf_counter()
success = True success = True
passed_tests = [] passed_tests = []
failed_tests = [] failed_tests = []
retried_tests = [] # Track which tests were retried
for i, file in enumerate(files): for i, file in enumerate(files):
if isinstance(file, CIRegistry): if isinstance(file, CIRegistry):
@@ -66,77 +144,154 @@ def run_unittest_files(
filename, estimated_time = file.name, file.estimated_time filename, estimated_time = file.name, file.estimated_time
process = None process = None
output_lines = []
def run_one_file(filename): def run_one_file(filename, capture_output=False):
nonlocal process nonlocal process, output_lines
filename = os.path.join(os.getcwd(), filename) full_path = os.path.join(os.getcwd(), filename)
print( logger.info(
f".\n.\nBegin ({i}/{len(files) - 1}):\npython3 {filename}\n.\n.\n", f".\n.\nBegin ({i}/{len(files) - 1}):\npython3 {full_path}\n.\n.\n"
flush=True,
) )
tic = time.perf_counter() file_tic = time.perf_counter()
process = subprocess.Popen( if capture_output:
["python3", filename], stdout=None, stderr=None, env=os.environ # Capture output for retry decision
) process = subprocess.Popen(
process.wait() ["python3", full_path],
elapsed = time.perf_counter() - tic stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=os.environ,
text=True,
errors="ignore", # Ignore non-UTF-8 bytes to prevent UnicodeDecodeError
)
output_lines = []
for line in process.stdout:
logger.info(line.rstrip())
output_lines.append(line)
process.wait()
else:
process = subprocess.Popen(
["python3", full_path], stdout=None, stderr=None, env=os.environ
)
process.wait()
print( elapsed = time.perf_counter() - file_tic
f".\n.\nEnd ({i}/{len(files) - 1}):\n{filename=}, {elapsed=:.0f}, {estimated_time=}\n.\n.\n",
flush=True, logger.info(
f".\n.\nEnd ({i}/{len(files) - 1}):\n{filename=}, {elapsed=:.0f}, {estimated_time=}\n.\n.\n"
) )
return process.returncode return process.returncode
try: # Retry loop for each file
ret_code = run_with_timeout( attempt = 1
run_one_file, args=(filename,), timeout=timeout_per_file file_passed = False
) was_retried = False
if ret_code != 0:
print( while attempt <= (max_attempts if enable_retry else 1):
f"\n✗ FAILED: {filename} returned exit code {ret_code}\n", if attempt > 1:
flush=True, logger.info(
f"\n[CI Retry] Attempt {attempt}/{max_attempts} for {filename}\n"
) )
success = False was_retried = True
failed_tests.append((filename, f"exit code {ret_code}"))
if not continue_on_error: try:
# Stop at first failure for PR tests ret_code = run_with_timeout(
run_one_file,
args=(filename,),
kwargs={"capture_output": enable_retry},
timeout=timeout_per_file,
)
if ret_code == 0:
file_passed = True
if was_retried:
logger.info(
f"\n✓ PASSED on retry (attempt {attempt}): {filename}\n"
)
retried_tests.append((filename, attempt, "passed"))
passed_tests.append(filename)
break break
# Otherwise continue to next test for nightly tests else:
else: # Check if we should retry
passed_tests.append(filename) if enable_retry and attempt < max_attempts:
except TimeoutError: output = "".join(output_lines)
kill_process_tree(process.pid) is_retriable, reason = is_retriable_failure(output)
time.sleep(5)
print( if is_retriable:
f"\n✗ TIMEOUT: {filename} after {timeout_per_file} seconds\n", logger.info(f"\n[CI Retry] {filename} failed with {reason}")
flush=True, logger.info(
) f"[CI Retry] Waiting {retry_wait_seconds}s before retry...\n"
success = False )
failed_tests.append((filename, f"timeout after {timeout_per_file}s")) time.sleep(retry_wait_seconds)
if not continue_on_error: attempt += 1
# Stop at first timeout for PR tests continue
else:
logger.info(
f"\n[CI Retry] {filename} failed with {reason} - not retrying\n"
)
# No retry or not retriable
logger.info(
f"\n✗ FAILED: {filename} returned exit code {ret_code}\n"
)
if was_retried:
retried_tests.append((filename, attempt, "failed"))
failed_tests.append((filename, f"exit code {ret_code}"))
break
except TimeoutError:
kill_process_tree(process.pid)
time.sleep(5)
logger.info(
f"\n✗ TIMEOUT: {filename} after {timeout_per_file} seconds\n"
)
if was_retried:
retried_tests.append((filename, attempt, "timeout"))
failed_tests.append((filename, f"timeout after {timeout_per_file}s"))
break break
# Otherwise continue to next test for nightly tests
if not file_passed:
success = False
if not continue_on_error:
break
elapsed_total = time.perf_counter() - tic
if success: if success:
print(f"Success. Time elapsed: {time.perf_counter() - tic:.2f}s", flush=True) logger.info(f"Success. Time elapsed: {elapsed_total:.2f}s")
else: else:
print(f"Fail. Time elapsed: {time.perf_counter() - tic:.2f}s", flush=True) logger.info(f"Fail. Time elapsed: {elapsed_total:.2f}s")
# Print summary # Print summary
print(f"\n{'='*60}", flush=True) logger.info(f"\n{'='*60}")
print(f"Test Summary: {len(passed_tests)}/{len(files)} passed", flush=True) logger.info(f"Test Summary: {len(passed_tests)}/{len(files)} passed")
print(f"{'='*60}", flush=True) if enable_retry and retried_tests:
logger.info(f"Retries: {len(retried_tests)} test(s) were retried")
logger.info(f"{'='*60}")
if passed_tests: if passed_tests:
print("✓ PASSED:", flush=True) logger.info("✓ PASSED:")
for test in passed_tests: for test in passed_tests:
print(f" {test}", flush=True) logger.info(f" {test}")
if failed_tests: if failed_tests:
print("\n✗ FAILED:", flush=True) logger.info("\n✗ FAILED:")
for test, reason in failed_tests: for test, reason in failed_tests:
print(f" {test} ({reason})", flush=True) logger.info(f" {test} ({reason})")
print(f"{'='*60}\n", flush=True) if retried_tests:
logger.info("\n↻ RETRIED:")
for test, attempts, result in retried_tests:
logger.info(f" {test} ({attempts} attempts, {result})")
logger.info(f"{'='*60}\n")
# Write GitHub Step Summary only if retries occurred
if retried_tests:
passed_on_retry = [t for t, _, r in retried_tests if r == "passed"]
failed_after_retry = [t for t, _, r in retried_tests if r != "passed"]
summary = f"**↻ Retried {len(retried_tests)} test(s):**\n"
if passed_on_retry:
summary += f"- ✓ Passed on retry: {', '.join(passed_on_retry)}\n"
if failed_after_retry:
summary += f"- ✗ Still failed: {', '.join(failed_after_retry)}\n"
write_github_step_summary(summary)
return 0 if success else -1 return 0 if success else -1
+53
View File
@@ -0,0 +1,53 @@
"""
Simple wrapper to run a test file with retry logic.
Usage:
python3 -m sglang.test.ci.run_with_retry test_file.py [--max-attempts 2] [--retry-wait 60]
"""
import argparse
import sys
from sglang.test.ci.ci_utils import TestFile, run_unittest_files
def main():
parser = argparse.ArgumentParser(description="Run a test file with retry logic")
parser.add_argument("test_file", help="The test file to run")
parser.add_argument(
"--max-attempts",
type=int,
default=2,
help="Maximum number of attempts (default: 2)",
)
parser.add_argument(
"--retry-wait",
type=int,
default=60,
help="Seconds to wait between retries (default: 60)",
)
parser.add_argument(
"--timeout",
type=int,
default=1200,
help="Timeout per attempt in seconds (default: 1200)",
)
args = parser.parse_args()
# Create a TestFile with a reasonable estimated time
test_file = TestFile(name=args.test_file, estimated_time=args.timeout)
exit_code = run_unittest_files(
files=[test_file],
timeout_per_file=args.timeout,
continue_on_error=False,
enable_retry=True,
max_attempts=args.max_attempts,
retry_wait_seconds=args.retry_wait,
)
sys.exit(exit_code)
if __name__ == "__main__":
main()
+33 -1
View File
@@ -168,10 +168,18 @@ def run_a_suite(args):
pretty_print_tests(args, ci_tests, skipped_tests) pretty_print_tests(args, ci_tests, skipped_tests)
# Add extra timeout when retry is enabled
timeout = args.timeout_per_file
if args.enable_retry:
timeout += args.retry_timeout_increase
return run_unittest_files( return run_unittest_files(
ci_tests, ci_tests,
timeout_per_file=args.timeout_per_file, timeout_per_file=timeout,
continue_on_error=args.continue_on_error, continue_on_error=args.continue_on_error,
enable_retry=args.enable_retry,
max_attempts=args.max_attempts,
retry_wait_seconds=args.retry_wait_seconds,
) )
@@ -214,6 +222,30 @@ def main():
type=int, type=int,
help="Use auto load balancing. The number of parts.", help="Use auto load balancing. The number of parts.",
) )
parser.add_argument(
"--enable-retry",
action="store_true",
default=False,
help="Enable smart retry for accuracy/performance assertion failures (not code errors)",
)
parser.add_argument(
"--max-attempts",
type=int,
default=2,
help="Maximum number of attempts per file including initial run (default: 2)",
)
parser.add_argument(
"--retry-wait-seconds",
type=int,
default=60,
help="Seconds to wait between retries (default: 60)",
)
parser.add_argument(
"--retry-timeout-increase",
type=int,
default=600,
help="Additional timeout in seconds when retry is enabled (default: 600)",
)
args = parser.parse_args() args = parser.parse_args()
# Validate auto-partition arguments # Validate auto-partition arguments
+37 -1
View File
@@ -491,6 +491,30 @@ def main():
default=False, default=False,
help="Continue running remaining tests even if one fails (useful for nightly tests)", help="Continue running remaining tests even if one fails (useful for nightly tests)",
) )
arg_parser.add_argument(
"--enable-retry",
action="store_true",
default=False,
help="Enable smart retry for accuracy/performance assertion failures (not code errors)",
)
arg_parser.add_argument(
"--max-attempts",
type=int,
default=2,
help="Maximum number of attempts per file including initial run (default: 2)",
)
arg_parser.add_argument(
"--retry-wait-seconds",
type=int,
default=60,
help="Seconds to wait between retries (default: 60)",
)
arg_parser.add_argument(
"--retry-timeout-increase",
type=int,
default=600,
help="Additional timeout in seconds when retry is enabled (default: 600)",
)
args = arg_parser.parse_args() args = arg_parser.parse_args()
print(f"{args=}") print(f"{args=}")
@@ -506,7 +530,19 @@ def main():
print("The running tests are ", [f.name for f in files]) print("The running tests are ", [f.name for f in files])
exit_code = run_unittest_files(files, args.timeout_per_file, args.continue_on_error) # Add extra timeout when retry is enabled
timeout = args.timeout_per_file
if args.enable_retry:
timeout += args.retry_timeout_increase
exit_code = run_unittest_files(
files,
timeout,
args.continue_on_error,
args.enable_retry,
args.max_attempts,
args.retry_wait_seconds,
)
exit(exit_code) exit(exit_code)