Skip to content

feat(checks): add progress bar to suite runs#2514

Merged
kevinmessiaen merged 8 commits into
mainfrom
feat/suite-progress-bar
Jun 8, 2026
Merged

feat(checks): add progress bar to suite runs#2514
kevinmessiaen merged 8 commits into
mainfrom
feat/suite-progress-bar

Conversation

@henchaves

@henchaves henchaves commented Jun 4, 2026

Copy link
Copy Markdown
Member

Description

This PR adds a progress bar to Suite run, making it easier to track which Scenario is being executed (or more than one if parallel=True).

CleanShot 2026-06-04 at 19 41 27@2x

Related Issue

#2473

Type of Change

  • 📚 Examples / docs / tutorials / dependencies update
  • 🔧 Bug fix (non-breaking change which fixes an issue)
  • 🥂 Improvement (non-breaking change which improves an existing feature)
  • 🚀 New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to change)
  • 🔐 Security fix

@henchaves henchaves self-assigned this Jun 4, 2026
@henchaves
henchaves requested a review from kevinmessiaen June 4, 2026 17:41

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces an interactive progress bar using the rich library to display the status of running scenarios in a suite, supporting both serial and parallel execution modes. The feedback highlights three key areas for improvement: propagating the internal _progress reference to wrapped columns in the custom _OverallOnly class to avoid potential AttributeErrors, resetting the progress bar description to a clean state upon completion, and guarding task creation calls when progress tracking is disabled to avoid unnecessary overhead.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +34 to +44
class _OverallOnly(ProgressColumn):
"""Render the wrapped column only for the overall task, not per-scenario rows."""

def __init__(self, column: ProgressColumn) -> None:
super().__init__()
self._column: ProgressColumn = column

def render(self, task: "Task") -> RenderableType:
if not task.fields.get("overall"):
return Text("")
return self._column.render(task)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

When wrapping a ProgressColumn inside _OverallOnly, the inner column (self._column) does not automatically receive the _progress reference from the parent Progress instance. If the wrapped column (or any custom column) attempts to access self.progress, it will raise an AttributeError because _progress is unset.

To prevent this, we can override __setattr__ to propagate _progress to the wrapped column. Additionally, we should invoke the wrapped column using self._column(task) (the standard __call__ interface) instead of calling render directly to respect any potential custom wrapper behaviors in rich.

class _OverallOnly(ProgressColumn):
    """Render the wrapped column only for the overall task, not per-scenario rows."""

    def __init__(self, column: ProgressColumn) -> None:
        super().__init__()
        self._column: ProgressColumn = column

    def __setattr__(self, name: str, value: Any) -> None:
        super().__setattr__(name, value)
        if name == "_progress":
            self._column._progress = value

    def render(self, task: "Task") -> RenderableType:
        if not task.fields.get("overall"):
            return Text("")
        return self._column(task)

Comment on lines +210 to +226
@contextmanager
def _progress_bar(self, enabled: bool) -> Iterator[tuple[Progress, TaskID]]:
"""Yield ``(progress, task_id)``; disabled when ``enabled`` is False so calls are no-ops."""
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
_OverallOnly(BarColumn()),
_OverallOnly(MofNCompleteColumn()),
TimeElapsedColumn(),
disable=not enabled,
) as progress:
task_id = progress.add_task(
"Running scenarios", total=len(self.scenarios), overall=True
)
yield progress, task_id
progress.update(task_id, completed=len(self.scenarios))
progress.refresh()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

During serial runs, the overall task description is updated to "Running: {scenario.name}". When the suite run finishes, the progress bar remains on the screen (since transient is False by default) showing "Running: <last_scenario_name>" even though execution has completed.

Resetting the description back to "Running scenarios" (or a completed message) when exiting the context manager ensures the final terminal output is clean and accurate.

    @contextmanager
    def _progress_bar(self, enabled: bool) -> Iterator[tuple[Progress, TaskID]]:
        """Yield ``(progress, task_id)``; disabled when ``enabled`` is False so calls are no-ops."""
        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            _OverallOnly(BarColumn()),
            _OverallOnly(MofNCompleteColumn()),
            TimeElapsedColumn(),
            disable=not enabled,
        ) as progress:
            task_id = progress.add_task(
                "Running scenarios", total=len(self.scenarios), overall=True
            )
            yield progress, task_id
            progress.update(
                task_id,
                description="Running scenarios",
                completed=len(self.scenarios),
            )
            progress.refresh()

Comment on lines 256 to +269
async def run_scenario(
scenario: Scenario[InputType, OutputType, Trace[Any, Any]],
) -> ScenarioResult[Trace[Any, Any]]:
async with semaphore:
return await scenario.run(
target=target, return_exception=return_exception
)
# Show a row per scenario while it runs, so all in-flight ones appear.
sub_id = progress.add_task(f" ↳ {scenario.name}", total=None)
try:
result = await scenario.run(
target=target, return_exception=return_exception
)
finally:
progress.remove_task(sub_id)
progress.advance(overall_id)
return result

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

When progress is disabled (e.g., in non-interactive environments or CI/CD), calling progress.add_task and progress.remove_task still executes and allocates/deallocates Task objects internally in rich.

Guarding these calls with if not progress.disable avoids unnecessary overhead and memory allocations during parallel execution of large suites.

        async def run_scenario(
            scenario: Scenario[InputType, OutputType, Trace[Any, Any]],
        ) -> ScenarioResult[Trace[Any, Any]]:
            async with semaphore:
                # Show a row per scenario while it runs, so all in-flight ones appear.
                sub_id = (
                    progress.add_task(f"  ↳ {scenario.name}", total=None)
                    if not progress.disable
                    else None
                )
                try:
                    result = await scenario.run(
                        target=target, return_exception=return_exception
                    )
                finally:
                    if sub_id is not None:
                        progress.remove_task(sub_id)
                progress.advance(overall_id)
            return result

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

Development

Successfully merging this pull request may close these issues.

feat(checks): live progress output when running a suite

2 participants