Skip to content

Commit cf67abf

Browse files
committed
feat(runner): add results command to inspect Robot Framework runs
After running tests, `robotcode results` lets you explore the output without opening `report.html` or re-running the suite — handy for CI logs, agent-driven workflows, and quick failure inspection from the terminal. Three subcommands: - `robotcode results summary` — overall status, pass/fail/skip counts, run duration, and the total number of error and warning messages. `--failures` adds the list of failed tests above the counts. - `robotcode results show` — one line per test with its status, source link, and failure message. Filter by status, tags, suite or test name pattern. - `robotcode results log` — the full execution tree: every keyword call, control structure, and log message, just like Robot's `report.html` but in your terminal. `--failures-only`, `--level WARN`, `--extract DIR` (pulls out screenshots and embedded artefacts), and more. The output file is auto-discovered from your active `robot.toml` profile; override with `-o/--output PATH`. Test names carry a `(path:line)` suffix that VS Code's integrated terminal turns into a clickable goto-source link. Pass `--format json` (or `toml`) for a structured, agent-friendly payload. See `docs/03_reference/cli.md` and `robotcode results --help` for the full reference.
1 parent 571413b commit cf67abf

12 files changed

Lines changed: 2951 additions & 87 deletions

File tree

docs/03_reference/cli.md

Lines changed: 351 additions & 61 deletions
Large diffs are not rendered by default.

packages/plugin/src/robotcode/plugin/__init__.py

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -240,21 +240,19 @@ def print_data(
240240
if format == OutputFormat.JSON_INDENT:
241241
format = OutputFormat.JSON
242242
console = Console(soft_wrap=True)
243-
if self.config.pager:
243+
syntax = Syntax(text, format, background_color="default")
244+
if self._should_page(lambda: text.count("\n")):
244245
with console.pager(styles=True, links=True):
245-
console.print(Syntax(text, format, background_color="default"))
246+
console.print(syntax)
246247
else:
247-
console.print(Syntax(text, format, background_color="default"))
248+
console.print(syntax)
248249

249250
return
250251
except ImportError:
251252
if self.config.colored_output == ColoredOutput.YES:
252253
self.warning('Package "rich" is required to use colored output.')
253254

254-
if self.config.pager:
255-
self.echo_via_pager(text)
256-
else:
257-
self.echo(text)
255+
self.echo_via_pager(text)
258256

259257
return
260258

@@ -273,6 +271,30 @@ def echo(
273271
err=err,
274272
)
275273

274+
def _should_page(self, measure_lines: Callable[[], int]) -> bool:
275+
"""Tri-state pager decision.
276+
277+
- `config.pager` is True → always page.
278+
- `config.pager` is False → never page.
279+
- `config.pager` is None → auto: page only when stdout is a TTY AND
280+
the rendered output exceeds the terminal
281+
height (measured via the callback).
282+
"""
283+
pref = self.config.pager
284+
if pref is True:
285+
return True
286+
if pref is False:
287+
return False
288+
if not sys.stdout.isatty():
289+
return False
290+
try:
291+
from shutil import get_terminal_size
292+
293+
term_lines = get_terminal_size(fallback=(80, 24)).lines
294+
return measure_lines() > term_lines - 2
295+
except Exception:
296+
return False
297+
276298
def echo_as_markdown(self, text: str) -> None:
277299
if self.colored:
278300
try:
@@ -307,7 +329,14 @@ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderR
307329
markdown = Markdown(text, justify="left", code_theme="default")
308330

309331
console = Console()
310-
if self.config.pager:
332+
333+
def _measure() -> int:
334+
measure = Console(width=console.size.width, record=False, soft_wrap=True)
335+
with measure.capture() as cap:
336+
measure.print(markdown)
337+
return cap.get().count("\n")
338+
339+
if self._should_page(_measure):
311340
with console.pager(styles=True, links=True):
312341
console.print(markdown)
313342
else:
@@ -325,18 +354,20 @@ def echo_via_pager(
325354
color: Optional[bool] = None,
326355
) -> None:
327356
try:
328-
if not self.config.pager:
329-
text = (
330-
text_or_generator
331-
if isinstance(text_or_generator, str)
332-
else "".join(text_or_generator() if callable(text_or_generator) else text_or_generator)
333-
)
334-
click.echo(text, color=color if color is not None else self.colored)
357+
text = (
358+
text_or_generator
359+
if isinstance(text_or_generator, str)
360+
else "".join(text_or_generator() if callable(text_or_generator) else text_or_generator)
361+
)
362+
use_color = color if color is not None else self.colored
363+
if self._should_page(lambda: text.count("\n")):
364+
if use_color:
365+
# click only sets `LESS=-R` when color=None — set it here so
366+
# less renders ANSI styles instead of showing raw ESC codes.
367+
os.environ.setdefault("LESS", "-R")
368+
click.echo_via_pager(text, color=use_color)
335369
else:
336-
click.echo_via_pager(
337-
text_or_generator,
338-
color=color if color is not None else self.colored,
339-
)
370+
click.echo(text, color=use_color)
340371
except OSError:
341372
pass
342373

packages/runner/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ dependencies = [
3535
"robotcode",
3636
]
3737

38+
[project.optional-dependencies]
39+
html = ["html-to-markdown>=2.0"]
40+
3841
[project.entry-points.robotcode]
3942
runner = "robotcode.runner.hooks"
4043

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from .discover import discover
22
from .libdoc import libdoc
33
from .rebot import rebot
4+
from .results import results
45
from .robot import robot
56
from .testdoc import testdoc
67

7-
__all__ = ["discover", "libdoc", "rebot", "robot", "testdoc"]
8+
__all__ = ["discover", "libdoc", "rebot", "results", "robot", "testdoc"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .results import results
2+
3+
__all__ = ["results"]

0 commit comments

Comments
 (0)