Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# robotframework-retryfailed – Copilot Guide

## Architecture
- `src/RetryFailed/retry_failed.py` hosts the entire listener/library; `RetryFailed` exposes ROBOT_LISTENER_API_VERSION 3 and is imported via `RetryFailed` package (see `src/RetryFailed/__init__.py`).
- `KeywordMetaData` snapshots keyword name/source/line so we can reinsert the same `RunningKeyword` when retrying without duplicating registrations.
- `start_test` deep-copies the original `RunningTestCase` and sets `${RETRYFAILED_RETRY_INDEX}`; `end_test` requeues failures by inserting the saved test back into `test.parent.tests` and flagging original results as SKIP.
- `end_suite` and `RetryMerger` (a `robot.api.ResultVisitor`) rewrite the execution tree so persisted output either collapses duplicates or keeps them when `keep_retried_tests=True`.

## Retry semantics
- Tests/tasks opt in via tags like `[Tags] test:retry(2)` or `task:retry(5)`; missing tags fall back to the constructor’s `global_test_retries` default supplied on the `--listener RetryFailed:<args>` CLI.
- Keyword retries are entirely tag-driven: `[Tags] keyword:retry(<n>)` on either user keywords or test cases, with `start_keyword` storing metadata and `end_keyword` ensuring only one registration per (name, source, lineno).
- The listener exposes two warning toggles (`warn_on_test_retry`, `warn_on_kw_retry`) that gate whether BuiltIn logs use WARN or INFO when retries happen; preserve these semantics whenever adding new log messages.
- When `log_level` is passed, retries temporarily raise Robot’s log level; always reset through `_original_log_level` before exiting the listener hook to avoid leaking state between tests/keywords.

## Result shaping & messaging
- `message()` intercepts duplicate-test WARNs emitted by Robot and rewrites them into retry status lines so final logs stay meaningful; any new warning flow should piggyback on this formatting pattern (`Retry {x}/{y} of test ...`).
- `output_file()` always rewrites `output.xml` in place via `ExecutionResult.visit(RetryMerger(...))`; if you touch result data structures remember that post-processing happens after Robot finishes.
- HTML links inserted by `_get_keyword_link()` and `_get_test_link()` rely on Robot-generated element ids; keep IDs intact when adjusting result objects.

## Tests & verification
- Acceptance tests live under `atest/`; run both suites locally with `robot -d results --listener RetryFailed:10:True:TRACE atest/01_SimpleTestSuite.robot` and `atest/02_KeywordRetryListener.robot` (scripted in `atest/run_atest.sh`).
- The suites rely on stateful suite variables (`VAR … scope=SUITE/GLOBAL`), so run them serially and reset `${counter_*}` variables when writing new cases.
- There are no unit tests; regression coverage hinges on these Robot suites plus manual log inspection of `output.xml`/`log.html`.

## Development workflow
- Use Python ≥3.8; `pip install -r requirements-dev.txt` installs the project in editable mode plus the `dev` extra (flit, mypy, ruff, robocop, check-manifest, twine).
- Packaging now runs through Flit: bump `__version__` in `src/RetryFailed/__init__.py`, then execute `./createPip_whl_tar.sh` (wraps `flit build`, `twine check`, and `flit publish`).
- `.pre-commit-config.yaml` is authoritative (ruff lint/format, mypy, robocop); the same settings live in `pyproject.toml`, so add new tooling there.

## Contribution conventions
- Prefer pure Python stdlib + Robot APIs; new deps must be justified because the listener is imported inside Robot runs.
- Keep listener hooks fast and side-effect free—no network/file IO inside `start_*`/`end_*` without caching since they run per test/keyword.
- Document any new listener args in both the README table and `atest` tags so behavior stays discoverable.
- When adding retry metadata, key lookups currently use `(kw_name, kw_source, kw_lineno)`; preserve or extend that tuple instead of relying solely on names to avoid clashes between identically named keywords in different files.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,8 @@ dmypy.json
.pyre/
atest/results
results
log.html
output.xml
report.html
.vscode/launch.json
temp_listener.py
41 changes: 41 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
repos:
# mypy linter for Python files
- repo: local
hooks:
- id: mypy
name: mypy
description: 'Run mypy for static type checking'
entry: mypy
language: python
types_or: [python, pyi]
require_serial: true

# Ruff linter for Python files
- repo: local
hooks:
- id: ruff-format
name: ruff-format
description: "Run 'ruff format' for extremely fast Python formatting"
entry: ruff format --force-exclude
language: python
types_or: [python, pyi, jupyter]
require_serial: true

- id: ruff
name: ruff
description: "Run 'ruff' for extremely fast Python linting"
entry: ruff check --force-exclude
language: python
types_or: [python, pyi, jupyter]
require_serial: true

# Robot framework acceptance tests
- repo: local
hooks:
- id: acceptance-tests
name: acceptance-tests
description: "Run Robot Framework acceptance tests"
entry: robot -d results --listener RetryFailed:0:True:TRACE --loglevel INFO atest
language: system
pass_filenames: false
always_run: true
58 changes: 58 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Contributing to robotframework-retryfailed

Thanks for helping improve the RetryFailed listener! This project is intentionally small, so the
workflow is simple and scripted. The steps below explain how to set up an environment, run
tooling, and publish a release.

## 1. Environment setup
1. Use Python 3.8+.
2. Create a virtual environment and install the dev extras:
```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
```
The `dev` extra installs flit, mypy, ruff, robocop, check-manifest, twine, and Robot Framework
add-ons used in the acceptance suites.
3. Install the pre-commit hooks (optional but recommended):
```bash
pre-commit install
```

## 2. Linting & formatting
- **Python formatting**: `ruff format .`
- **Python linting**: `ruff check .`
- **Static typing**: `mypy src`
- **Robot Framework linting**: `robocop check --include *.robot --include *.resource`
- **Robot Framework formatting**: `robocop format --include *.robot --include *.resource`
- **Pre-commit (full suite)**: `pre-commit run --all-files`

The same settings live in `pyproject.toml`, so CI and local runs stay aligned. Prefer running the
standalone commands when iterating quickly, and fall back to the pre-commit aggregate before
delivering work.

## 3. Tests
All regression coverage is provided by the acceptance suites under `atest/`. Run them from the
repository root with the listener installed in editable mode:
```bash
robot -d results --listener RetryFailed:10:True:TRACE atest/01_SimpleTestSuite.robot
robot -d results --listener RetryFailed:10:True:TRACE atest/02_KeywordRetryListener.robot
```
Inspect `results/output.xml` and `results/log.html` to confirm retries are recorded correctly.

## 4. Release workflow
1. Bump `__version__` inside `src/RetryFailed/__init__.py`.
2. Verify the manifest and build artifacts:
```bash
check-manifest --update
flit build
twine check dist/*
```
3. Upload to PyPI (after any manual smoke tests):
```bash
flit publish
```
The helper script `./createPip_whl_tar.sh` automates the same sequence with a keypress between
verification and upload.

Please open an issue before large or breaking changes so we can align on direction. Happy hacking!
94 changes: 75 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,109 @@
# robotframework-retryfailed

A listener to automatically retry tests or tasks based on tags.
A listener to automatically retry tests, tasks or keywords based on tags.

## Installation

Install with pip:
```
pip install robotframework-retryfailed
```

pip install robotframework-retryfailed
## CLI Arguments

You can configure the following CLI arguments when registering the listener in your robotframework cli call:

| Argument | Description | Mandatory | Default Value |
|----------|-------------|-----------|---------------|
| ``global_test_retries`` | Define a global number of retries which is valid for ALL your tests by default! | No | `**0** |
| ``keep_retried_tests`` | Define if the retried tests should be kept in the logs or not. If ``True``, they will be marked with status ``Skip`` | No | **False** |
| ``log_level`` | If set, the loglevel will be changed to the given value IF a test / keyword is getting retried. | No | **None** |
| ``warn_on_test_retry`` | If ``True``, the retried tests will be logged as warning to the ``log.html`` | No | **True** |
| ``warn_on_kw_retry`` | If ``True``, the retried keywords will be logged as warning to the ``log.html`` | No | **False** |

## Usage

Add the listener to your robot execution, via command line arguments.
When your tests do fail and you have tagged them with `test:retry(2)`, it will retry the test 2 times.
Retry can be also set globally as a parameter to the listener.

### Attaching Listener
### Attaching Listener - Retry Tests

Example:
```
# Attaching listener without any default retry configuratio
robot --listener RetryFailed <your robot suite>

robot --listener RetryFailed <your robot suite>
# Attach listener & retrying every failed tests once if failed
robot --listener RetryFailed:1 <robot suite>

robot --listener RetryFailed:1 <robot suite>
# Attaching listener, retrying every tests once, keep failed tests in logfile & increase loglevel to TRACE for retry tests.
robot --listener RetryFailed:1:True:TRACE <robot suite>
```

Second one will by default retry once every failing test.
### Retry Test Cases - Tagging Tests

### Tagging Tests

Example:
If attaching the listener without any default retry configuration, you must set the count of max. retry as ``Test Tag``.

*** Test Cases ***
Test Case
[Tags] test:retry(2)
Log This test will be retried 2 times if it fails
See Example:
```
*** Test Cases ***
Test Case
[Tags] test:retry(2)
Log This test will be retried 2 times if it fails
```

Tagging tasks by `task:retry(3)` should also work.

### Retry Keywords - Tagging Keywords

The ``KeywordRetryListener`` is basically always ``activated`` & needs to be defined by yourself within your test.

It makes no sense to configure a default retry count for every keyword, because usually too many keywords are part a test / parent keyword to retry them **ALL** !
This means, you need to define the keywords which should be retried by yourself!

Therefore, you must configure the following ``Keyword Tags``:
```
*** Keywords ***
KeywordABC
[Documentation] Keyword takes max. 1 retry!
[Tags] keyword:retry(1)

KeywordDEF
[Documentation] Keyword takes max. 5 retries!
[Tags] keyword:retry(5)
```


### Configuration

On top of specifying the number of retries, you can also define whether your want to **keep the logs** of the retried tests and change the **log level** when retrying, by providing respectfully second and third parameter to the listener: `RetryFailed:<global_retries>:<keep_retried_tests>:<log_level>`

By default the logs of the retried tests are not kept, and the log level remains the same as the regular run.

Example:
```
# keep the logs of the retried tests
robot --listener RetryFailed:1:True

# does not keep the logs of the retried tests and change log level to DEBUG when retrying
robot --listener RetryFailed:2:False:DEBUG

# keep the logs of the retried tests and change the log level to TRACE when retrying
robot --listener RetryFailed:1:True:TRACE

# Same like previous one, but keep in mind: all retried tests are getting logged as warning. But all retried keywords are not getting logged as warning!
robot --listener RetryFailed:1:True:TRACE

# Both retried tests & retried keywords are getting logged as warning into the log.html
robot --listener RetryFailed:1:True:TRACE:True:True

# Only retried keywords are getting logged as warning into the log.html
robot --listener RetryFailed:1:True:TRACE:False:True
```

# keep the logs of the retried tests
robot --listener RetryFailed:1:True
### Log Warnings at Retry

# does not keep the logs of the retried tests and change log level to DEBUG when retrying
robot --listener RetryFailed:2:False:DEBUG
If you've set both parameters, ``warn_on_test_retry`` & ``warn_on_kw_retry``, to ``False``, a simple ``Info`` message gets logged during the keyword execution in the log.html.

# keep the logs of the retried tests and change the log level to TRACE when retrying
robot --listener RetryFailed:1:True:TRACE
You won't see at the top of the log file, but you can find it directly within the logged keyword execution.
87 changes: 48 additions & 39 deletions atest/01_SimpleTestSuite.robot
Original file line number Diff line number Diff line change
@@ -1,39 +1,48 @@
*** Settings ***
# Library RetryFailed log_level=TRACE


*** Variables ***
${retry_1} ${0}
${retry_2} ${0}


*** Test Cases ***
My Simple Test
Log Hello World
Should Be Equal Hello Hello

Sometime Fail
[Tags] test:retry(1)
Should Be True ${{random.randint(0, 1)}} == 1

Sometime Fail1
[Tags] test:retry(3)
Should Be True ${{random.randint(0, 1)}} == 1

Sometime Fail2
[Tags] test:retry(1)
Should Be True ${{random.randint(0, 1)}} == 1

Passes after 3 Fails
[Tags] test:retry(3)
Should Be Equal ${retry_1} ${3}
[Teardown] Set Suite Variable ${retry_1} ${retry_1 + 1}

Fails on 4th Exec
[Tags] task:retry(5)
Should Be Equal ${retry_2} ${4}
[Teardown] Set Suite Variable ${retry_2} ${retry_2 + 1}

My Simple Test1
Log Hello World
Should Be Equal Hello Hello
*** Variables ***
${tc_01} ${0}
${tc_02} ${0}
${retry_1} ${0}
${retry_2} ${0}


*** Test Cases ***
My Simple Test
Log Hello World
Log This TRACE message should not be logged! level=TRACE
Should Be Equal Hello Hello

TC01 - Retry Once
[Tags] test:retry(2)
Log This TRACE message can be logged sometimes level=TRACE
VAR ${tc_01} = ${${tc_01}+1} scope=SUITE
Should Be Equal As Integers ${tc_01} ${2}

TC01 - Retry Twice
[Tags] test:retry(2)
Log This TRACE message can be logged sometimes level=TRACE
VAR ${tc_02} = ${${tc_02}+1} scope=SUITE
Should Be Equal As Integers ${tc_02} ${3}

My Simple Test2
Log Hello World
Log This TRACE message should not be logged! level=TRACE
Should Be Equal Hello Hello

Log Trace Message
Log This TRACE message should not be logged! level=TRACE

Passes after 3 Fails
[Tags] test:retry(3)
Log This TRACE message should be logged on failures only! level=TRACE
Should Be Equal ${retry_1} ${3}
[Teardown] Set Suite Variable ${retry_1} ${retry_1 + 1}

Passes on 4th Exec
[Tags] task:retry(5)
Log This TRACE message should be logged on failures only! level=TRACE
Should Be Equal ${retry_2} ${4}
[Teardown] Set Suite Variable ${retry_2} ${retry_2 + 1}

My Simple Test1
Log Hello World
Should Be Equal Hello Hello
Loading