Co-authored-by: UltralyticsAssistant <[email protected]>
285 lines
9.6 KiB
YAML
285 lines
9.6 KiB
YAML
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
|
|
|
name: Test Retry Action
|
|
permissions:
|
|
contents: read
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- ".github/workflows/test-retry.yml"
|
|
- "retry/action.yml"
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
test-bash:
|
|
runs-on: ${{ matrix.os }}
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Cleanup any leftover test files
|
|
run: rm -f /tmp/retry_attempt /tmp/retry_attempt_py /tmp/retry_child
|
|
|
|
- name: Test successful command
|
|
uses: ./retry
|
|
with:
|
|
run: |
|
|
echo "This should succeed first try"
|
|
true
|
|
|
|
- name: Test Python shell on Windows
|
|
if: matrix.os == 'windows-latest'
|
|
uses: ./retry
|
|
with:
|
|
shell: python
|
|
run: print("Python shell works")
|
|
|
|
- name: Test retry with eventual success
|
|
uses: ./retry
|
|
env:
|
|
TEST_VAR: "hello"
|
|
with:
|
|
retries: 2
|
|
retry_delay_seconds: 5
|
|
run: |
|
|
echo "Running attempt with TEST_VAR=$TEST_VAR"
|
|
# Validate env var is passed correctly
|
|
if [ "$TEST_VAR" != "hello" ]; then
|
|
echo "ERROR: TEST_VAR not passed correctly, got '$TEST_VAR'"
|
|
exit 1
|
|
fi
|
|
# Fail first two times, succeed on third attempt
|
|
if [ ! -f /tmp/retry_attempt ]; then
|
|
echo "1" > /tmp/retry_attempt
|
|
echo "First attempt - failing"
|
|
exit 1
|
|
fi
|
|
count=$(cat /tmp/retry_attempt)
|
|
if [ "$count" -lt 2 ]; then
|
|
echo "$((count + 1))" > /tmp/retry_attempt
|
|
echo "Attempt $((count + 1)) - failing"
|
|
exit 1
|
|
fi
|
|
rm -f /tmp/retry_attempt
|
|
echo "Success on attempt $((count + 1))!"
|
|
|
|
- name: Test hung command retry
|
|
uses: ./retry
|
|
with:
|
|
retries: 1
|
|
retry_delay_seconds: 0
|
|
timeout_minutes: 1
|
|
run: |
|
|
if [ ! -f /tmp/retry_attempt ]; then
|
|
touch /tmp/retry_attempt
|
|
if [ "$RUNNER_OS" = "Windows" ]; then
|
|
child_file=$(cygpath -w /tmp/retry_child)
|
|
powershell.exe -NoProfile -Command "[IO.File]::WriteAllText('$child_file', [string]\$PID); Start-Sleep 120" &
|
|
elif [ "$RUNNER_OS" = "Linux" ]; then
|
|
python3 -c 'import os, signal, time; os.fork() and os._exit(0); os.setsid(); os.fork() and os._exit(0); open("/tmp/retry_child", "w").write(str(os.getpid())); os.environ.clear(); signal.signal(signal.SIGTERM, signal.SIG_IGN); time.sleep(120)'
|
|
sleep 120 &
|
|
else
|
|
python3 -c 'import os, signal, time; os.fork() and os._exit(0); os.setsid(); os.fork() and os._exit(0); open("/tmp/retry_child", "w").write(str(os.getpid())); signal.signal(signal.SIGTERM, signal.SIG_IGN); time.sleep(120)'
|
|
sleep 120 &
|
|
fi
|
|
wait $!
|
|
fi
|
|
if [ "$RUNNER_OS" = "Windows" ]; then
|
|
powershell.exe -NoProfile -Command "if (Get-Process -Id $(cat /tmp/retry_child) -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }" && child_alive=true
|
|
elif kill -0 "$(cat /tmp/retry_child)" 2>/dev/null; then
|
|
child_alive=true
|
|
fi
|
|
if [ "$child_alive" = true ]; then
|
|
echo "::error::Timed-out child is still running"
|
|
exit 1
|
|
fi
|
|
rm -f /tmp/retry_attempt /tmp/retry_child
|
|
|
|
- name: Test multi-line failure
|
|
uses: ./retry
|
|
continue-on-error: true
|
|
env:
|
|
TEST_VAR: "hello"
|
|
with:
|
|
retries: 2
|
|
retry_delay_seconds: 5
|
|
run: |
|
|
echo "Running line 1 with TEST_VAR=$TEST_VAR"
|
|
echo "Running line 2 with TEST_VAR=$TEST_VAR"
|
|
false
|
|
|
|
- name: Record start (exponential)
|
|
id: start_exp
|
|
run: echo "t=$(date +%s)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Test exponential backoff timing
|
|
uses: ./retry
|
|
continue-on-error: true
|
|
with:
|
|
retries: 3
|
|
retry_delay_seconds: 2
|
|
backoff: exponential
|
|
jitter: false
|
|
run: |
|
|
# Waits should be 2s, 4s, 8s. Total ~14s between 4 attempts.
|
|
echo "Attempt at $(date +%s)"
|
|
false
|
|
|
|
- name: Assert exponential timing (~14s expected)
|
|
run: |
|
|
elapsed=$(( $(date +%s) - ${{ steps.start_exp.outputs.t }} ))
|
|
# Expected waits: 2 + 4 + 8 = 14s (no jitter). Allow generous upper bound for runner overhead.
|
|
if [ "$elapsed" -lt 13 ] || [ "$elapsed" -gt 40 ]; then
|
|
echo "::error::Exponential timing off: ${elapsed}s, expected ~14s (13-40s range)"
|
|
exit 1
|
|
fi
|
|
echo "OK: ${elapsed}s elapsed (expected ~14s)"
|
|
|
|
- name: Record start (fixed)
|
|
id: start_fixed
|
|
run: echo "t=$(date +%s)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Test fixed backoff
|
|
uses: ./retry
|
|
continue-on-error: true
|
|
with:
|
|
retries: 2
|
|
retry_delay_seconds: 3
|
|
backoff: fixed
|
|
jitter: false
|
|
run: |
|
|
echo "Attempt at $(date +%s)"
|
|
false
|
|
|
|
- name: Assert fixed timing (~6s expected)
|
|
run: |
|
|
elapsed=$(( $(date +%s) - ${{ steps.start_fixed.outputs.t }} ))
|
|
# Expected waits: 3 + 3 = 6s (no jitter). Allow runner overhead.
|
|
if [ "$elapsed" -lt 6 ] || [ "$elapsed" -gt 25 ]; then
|
|
echo "::error::Fixed timing off: ${elapsed}s, expected ~6s (6-25s range)"
|
|
exit 1
|
|
fi
|
|
echo "OK: ${elapsed}s elapsed (expected ~6s)"
|
|
|
|
- name: Record start (jitter floor)
|
|
id: start_jitter
|
|
run: echo "t=$(date +%s)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Test jitter never collapses small delay to 0
|
|
uses: ./retry
|
|
continue-on-error: true
|
|
with:
|
|
retries: 4
|
|
retry_delay_seconds: 1
|
|
backoff: fixed
|
|
jitter: true
|
|
run: |
|
|
echo "Attempt at $(date +%s)"
|
|
false
|
|
|
|
- name: Assert jitter floor (>=4s for 4 retries of >=1s each)
|
|
run: |
|
|
elapsed=$(( $(date +%s) - ${{ steps.start_jitter.outputs.t }} ))
|
|
# Without the floor, jitter would round 1s to 0s and the 4 retries would run instantly.
|
|
if [ "$elapsed" -lt 4 ]; then
|
|
echo "::error::Jitter collapsed small delays: ${elapsed}s elapsed, expected >=4s"
|
|
exit 1
|
|
fi
|
|
echo "OK: ${elapsed}s elapsed (>=4s confirmed)"
|
|
|
|
- name: Test invalid backoff value rejected
|
|
uses: ./retry
|
|
continue-on-error: true
|
|
id: invalid_backoff
|
|
with:
|
|
retries: 0
|
|
backoff: linear
|
|
run: echo "should not run"
|
|
|
|
- name: Assert invalid backoff failed
|
|
run: |
|
|
if [ "${{ steps.invalid_backoff.outcome }}" != "failure" ]; then
|
|
echo "::error::Expected invalid backoff to fail, got ${{ steps.invalid_backoff.outcome }}"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Test invalid numeric input rejected
|
|
uses: ./retry
|
|
continue-on-error: true
|
|
id: invalid_retries
|
|
with:
|
|
retries: "-1"
|
|
run: echo "should not run"
|
|
|
|
- name: Assert invalid numeric input failed
|
|
run: |
|
|
if [ "${{ steps.invalid_retries.outcome }}" != "failure" ]; then
|
|
echo "::error::Expected invalid retries to fail, got ${{ steps.invalid_retries.outcome }}"
|
|
exit 1
|
|
fi
|
|
|
|
test-python:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Cleanup any leftover test files
|
|
run: rm -f /tmp/retry_attempt /tmp/retry_attempt_py
|
|
|
|
- name: Test successful Python command
|
|
uses: ./retry
|
|
with:
|
|
shell: python
|
|
run: |
|
|
print("This should succeed first try")
|
|
|
|
- name: Test Python retry with eventual success
|
|
uses: ./retry
|
|
env:
|
|
TEST_VAR: "hello"
|
|
with:
|
|
shell: python
|
|
retries: 2
|
|
retry_delay_seconds: 5
|
|
run: |
|
|
import os
|
|
from pathlib import Path
|
|
print(f"Running attempt with TEST_VAR={os.environ.get('TEST_VAR')}")
|
|
# Validate env var is passed correctly
|
|
if os.environ.get('TEST_VAR') != 'hello':
|
|
raise Exception(f"TEST_VAR not passed correctly, got '{os.environ.get('TEST_VAR')}'")
|
|
# Fail first two times, succeed on third attempt
|
|
counter_file = Path("/tmp/retry_attempt_py")
|
|
if not counter_file.exists():
|
|
counter_file.write_text("1")
|
|
print("First attempt - failing")
|
|
raise Exception("Intentional failure")
|
|
count = int(counter_file.read_text())
|
|
if count < 2:
|
|
counter_file.write_text(str(count + 1))
|
|
print(f"Attempt {count + 1} - failing")
|
|
raise Exception("Intentional failure")
|
|
counter_file.unlink()
|
|
print(f"Success on attempt {count + 1}!")
|
|
|
|
- name: Test Python multi-line failure
|
|
uses: ./retry
|
|
continue-on-error: true
|
|
env:
|
|
TEST_VAR: "hello"
|
|
with:
|
|
shell: python
|
|
retries: 2
|
|
retry_delay_seconds: 5
|
|
run: |
|
|
import os
|
|
print(f"Running line 1 with TEST_VAR={os.environ.get('TEST_VAR')}")
|
|
print(f"Running line 2 with TEST_VAR={os.environ.get('TEST_VAR')}")
|
|
assert False, "Forced assertion error"
|