Skip to content

feat(Repeat): Expose `#[Repeat] attribute#113

Merged
roxblnfk merged 4 commits into1.xfrom
feat/repeat-attribute
Apr 6, 2026
Merged

feat(Repeat): Expose `#[Repeat] attribute#113
roxblnfk merged 4 commits into1.xfrom
feat/repeat-attribute

Conversation

@petrdobr
Copy link
Copy Markdown
Contributor

@petrdobr petrdobr commented Apr 6, 2026

What was changed

Added a new #[Repeat] attribute and its RepeatPolicyRunInterceptor.

The attribute can be applied to test methods, functions, and classes. It reruns the same test a fixed number of times using the times argument, following the same attribute/interceptor pattern already used by #[Retry].

Self-tests were also added to cover:

  • successful repetition with the default times = 2
  • stopping on the first failure during repeated execution

See commit history for details.

Why?

Issue #111 asks for a #[Repeat] attribute.

The implementation follows the maintainer’s guidance to use #[Retry] as the template and keep the design consistent with the current attribute pipeline in Testo.

One important design decision in this PR is that #[Repeat] stops on the first failed or errored execution and returns that TestResult immediately, instead of continuing all remaining repetitions. This keeps the behavior simple and matches the current single-result execution model.

Checklist

@petrdobr petrdobr requested a review from a team as a code owner April 6, 2026 16:40
@roxblnfk roxblnfk requested a review from Copilot April 6, 2026 18:19
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new #[Repeat] attribute to the Testo attribute/interceptor pipeline to rerun tests a fixed number of times (defaulting to times = 2) and stop on the first failed/errored execution, mirroring the existing #[Retry] design.

Changes:

  • Introduced Testo\Repeat attribute with times option and validation.
  • Added RepeatPolicyRunInterceptor to repeat test execution and short-circuit on failures.
  • Added sandbox/self-test cases covering successful repetition and early stop on failure.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
tests/Testo/Self/AssertTest.php Adds sandbox/self-test cases exercising #[Repeat] success and failure-stop behavior.
src/Repeat.php Defines the new #[Repeat] attribute and wires it to its fallback interceptor.
src/Repeat/Interceptor/RepeatPolicyRunInterceptor.php Implements the repeat execution loop and early-return on failure.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@roxblnfk
Copy link
Copy Markdown
Member

roxblnfk commented Apr 6, 2026

Try

    #[Test]
    #[Repeat(times: 3)]
    #[Retry(maxAttempts: 3)]
    public function repeatFail(): void
    {
        static $counter = 0;
        ++$counter;
        try {
            Assert::int($counter)->lessThanOrEqual(2);
        } catch (\Throwable $t) {
            $counter = 0;
            throw $t;
        }
    }

and then place #[Retry] before #[Repeat].

1 Repeat -> Retry

Assertion History:
  ✓ Assert that `1` is int;  less than or equal to `2`.
  ✓ Assert that `2` is int;  less than or equal to `2`.
  ✗ Assert that `3` is int;  less than or equal to `2`, but the value is not less than or equal to 2.
    ✗ Failed assertion that `3` less than or equal to `2`: the value is not less than or equal to 2.
  ✓ Assert that `1` is int;  less than or equal to `2`.
PASSED

2 Retry -> Repeat

Assertion History:
  ✓ Assert that `1` is int;  less than or equal to `2`.
  ✓ Assert that `2` is int;  less than or equal to `2`.
  ✗ Assert that `3` is int;  less than or equal to `2`, but the value is not less than or equal to 2.
    ✗ Failed assertion that `3` less than or equal to `2`: the value is not less than or equal to 2.
  ✓ Assert that `1` is int;  less than or equal to `2`.
  ✓ Assert that `2` is int;  less than or equal to `2`.
  ✗ Assert that `3` is int;  less than or equal to `2`, but the value is not less than or equal to 2.
    ✗ Failed assertion that `3` less than or equal to `2`: the value is not less than or equal to 2.
  ✓ Assert that `1` is int;  less than or equal to `2`.
  ✓ Assert that `2` is int;  less than or equal to `2`.
  ✗ Assert that `3` is int;  less than or equal to `2`, but the value is not less than or equal to 2.
    ✗ Failed assertion that `3` less than or equal to `2`: the value is not less than or equal to 2.
FAILED

Should we force the attributes priority or let them depend of their order?

@roxblnfk roxblnfk requested a review from Copilot April 6, 2026 20:04
@roxblnfk roxblnfk changed the title Add #[Repeat] attribute with repeat run interceptor feat(Repeat): Expose `#[Repeat] attribute Apr 6, 2026
@roxblnfk roxblnfk merged commit 38e8d29 into 1.x Apr 6, 2026
22 checks passed
@roxblnfk roxblnfk deleted the feat/repeat-attribute branch April 6, 2026 20:12
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +25 to +26
// First call passes, second call fails
Assert::same($counter, 1);
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

failsOnSecondIteration() uses a static counter but never resets it on failure. Because the feature tests re-run the whole stub suite multiple times via TestRunner::runTest(), this static state can leak between runs and change the scenario (later runs may fail on the first iteration instead of the second). Reset the counter in a catch/finally before rethrowing to keep the stub deterministic across runs.

Suggested change
// First call passes, second call fails
Assert::same($counter, 1);
try {
// First call passes, second call fails
Assert::same($counter, 1);
} catch (\Throwable $exception) {
$counter = 0;
throw $exception;
}

Copilot uses AI. Check for mistakes.
use Testo\Core\Definition\CaseDefinition;
use Testo\Core\Definition\TestDefinition;
use Testo\Core\Value\Status;
use Testo\Data\DataProvider;
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import Testo\Data\DataProvider in this test file. Removing it avoids dead code and potential CI/lint failures in environments that check for unused use statements.

Suggested change
use Testo\Data\DataProvider;

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +23
/**
* Test suites for Repeat component.
*/
return [
new SuiteConfig(
name: 'Repeat: Unit',
location: new FinderConfig(
include: [__DIR__ . '/Unit'],
),
),
new SuiteConfig(
name: 'Repeat: Feature',
location: new FinderConfig(
include: [__DIR__ . '/Feature'],
),
),
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new Repeat test suite config is not referenced by the main testo.php application config (which currently requires suites for Assert/Common/etc.). As a result, these Repeat unit/feature tests (and the self-tests added under tests/Testo, which are skipped in CI) likely won’t run in CI. Add require 'tests/Repeat/suites.php' to testo.php (or the appropriate suite aggregator) so the new component is covered by the normal test run.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

omg

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add #[Repeat] attribute

3 participants