robotframework-live-trace 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotframework-live-trace
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A live trace visualization for Robot Framework test runs.
5
5
  Keywords: robotframework,automation,testautomation,testing,debugging,visualization,trace,live
6
6
  Author: Manuel Ketterer
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Programming Language :: Python :: 3.14
22
22
  Classifier: Topic :: Software Development :: Testing
23
23
  Classifier: Topic :: Software Development :: Debuggers
24
- Requires-Dist: dearpygui>=2.2
24
+ Requires-Dist: dearpygui>=2.0
25
25
  Requires-Dist: robotframework>=7.0
26
26
  Requires-Python: >=3.10
27
27
  Project-URL: Homepage, https://github.com/iDentitY4/robotframework-live-trace
@@ -29,26 +29,28 @@ Project-URL: Repository, https://github.com/iDentitY4/robotframework-live-trace.
29
29
  Project-URL: Issues, https://github.com/iDentitY4/robotframework-live-trace/issues
30
30
  Description-Content-Type: text/markdown
31
31
 
32
- # Robotframework Live Trace
32
+ # Robot Framework Live Trace
33
33
 
34
- A real-time, cross-platform execution visualization dashboard for Robot Framework.
34
+ A real-time, cross-platform test execution visualization for Robot Framework powered by [DearPyGui](https://github.com/hoffstadt/DearPyGui).
35
35
 
36
- Built on top of [Dear PyGui](https://github.com/hoffstadt/DearPyGui) and the Robot Framework Listener API (v3), this tool runs a high-performance, hardware-accelerated GUI in a completely isolated process. It allows you to monitor deeply nested keyword call stacks, inspect live arguments, and trace failures exactly as they happen without blocking your test execution.
36
+ > Nothing has such power to broaden the mind as the ability to investigate systematically and truly all that comes under thy observation. - Marcus Aurelius
37
+
38
+ ![Demo Screenshot of RobotLiveTrace GUI](/images/demo-1.png)
37
39
 
38
40
  ## Features
39
41
 
40
- * **Live Execution Tree**: Watch your Suites, Tests, and Keywords expand and collapse in real-time.
41
- * **Dedicated Error Feed**: A split-pane chronological feed of all `WARN`, `ERROR`, and `FAIL` logs. Click any error to instantly expand the tree and highlight the exact keyword that caused it.
42
- * **Interactive Tooltips**: Hover over any keyword to see its execution time, status, and input arguments.
43
- * **Execution Control**: Pause and resume the active Robot Framework test run directly from the GUI.
44
- * **Smart Filtering**: Instantly search massive execution trees by keyword name, argument, or log message.
42
+ * **Live Execution Tree**: Watch your Suites, Tests, Keywords, and control structures such as `FOR` and `IF` expand and collapse in real-time. Keywords that can't be found are shown too, and anything that didn't run is grayed out.
43
+ * **Error Feed**: A split-pane chronological feed of all `WARN`, `ERROR`, and `FAIL` logs. Click any error to instantly expand the tree and highlight the exact keyword that caused it.
44
+ * **Tooltips**: Hover over any keyword to see its execution time, status, and input arguments.
45
+ * **Execution Control**: Pause and resume the active Robot Framework test run directly from the GUI, or pause automatically when a failure happens.
46
+ * **Search and Filtering**: Instantly search large execution trees by keyword name, argument, or log message.
45
47
  * **Process Isolated**: Runs in a separate `multiprocessing` process, dodging the Python GIL and ensuring your test timings are not affected by GUI rendering overhead.
46
48
 
47
49
  ## Requirements
48
50
 
49
51
  * Python 3.10+
50
52
  * Robot Framework >= 7.0 *(Requires the V3 Listener API)*
51
- * Dear PyGui >= 2.2
53
+ * Dear PyGui >= 2.0
52
54
 
53
55
  ## Installation
54
56
 
@@ -65,6 +67,14 @@ pip install robotframework-live-trace
65
67
 
66
68
  ```
67
69
 
70
+ ### RHEL, Rocky and AlmaLinux 9
71
+
72
+ Dear PyGui 2.1 and newer need a newer `libstdc++` than these systems ship, so importing them fails with an error like ``version `GLIBCXX_3.4.30' not found``. Install Dear PyGui 2.0.0 instead (available for Python 3.13 and older):
73
+
74
+ ```bash
75
+ pip install robotframework-live-trace "dearpygui==2.0.0"
76
+ ```
77
+
68
78
  ## Usage
69
79
 
70
80
  You do not need to modify your Robot Framework test files. Simply pass the listener to the `robot` command:
@@ -81,8 +91,9 @@ The Dear PyGui dashboard will open immediately, and the test execution will begi
81
91
  The top control bar provides several ways to manage large test executions:
82
92
 
83
93
  * **Pause**: Freezes the Robot Framework execution thread. Uncheck to resume.
94
+ * **Pause on Failure**: Pauses where a keyword or control structure fails, and checks **Pause** so you can inspect it. Uncheck **Pause** to resume. This includes failures that are handled later, such as by `TRY/EXCEPT` or `Run Keyword And Ignore Error`, because they can't be told apart when they happen.
84
95
  * **Auto-Scroll**: Automatically scrolls the tree and error feed to the bottom as new keywords and logs arrive.
85
- * **Auto-Collapse Passed**: When a test or suite finishes with a `PASS` status, it will automatically collapse its tree node to keep the UI clean. Failed nodes will remain expanded.
96
+ * **Auto-Collapse Passed**: When a keyword, test or suite finishes with a `PASS` or `NOT RUN` status, it will automatically collapse its tree node to keep the UI clean. Failed nodes will remain expanded.
86
97
  * **Expand All / Collapse All**: Instantly open or close all currently rendered tree nodes.
87
98
  * **Filter (Name/Args/Logs)**: Type to instantly hide non-matching keywords. The tree will automatically expand parents to reveal deeply nested matches.
88
99
 
@@ -0,0 +1,78 @@
1
+ # Robot Framework Live Trace
2
+
3
+ A real-time, cross-platform test execution visualization for Robot Framework powered by [DearPyGui](https://github.com/hoffstadt/DearPyGui).
4
+
5
+ > Nothing has such power to broaden the mind as the ability to investigate systematically and truly all that comes under thy observation. - Marcus Aurelius
6
+
7
+ ![Demo Screenshot of RobotLiveTrace GUI](/images/demo-1.png)
8
+
9
+ ## Features
10
+
11
+ * **Live Execution Tree**: Watch your Suites, Tests, Keywords, and control structures such as `FOR` and `IF` expand and collapse in real-time. Keywords that can't be found are shown too, and anything that didn't run is grayed out.
12
+ * **Error Feed**: A split-pane chronological feed of all `WARN`, `ERROR`, and `FAIL` logs. Click any error to instantly expand the tree and highlight the exact keyword that caused it.
13
+ * **Tooltips**: Hover over any keyword to see its execution time, status, and input arguments.
14
+ * **Execution Control**: Pause and resume the active Robot Framework test run directly from the GUI, or pause automatically when a failure happens.
15
+ * **Search and Filtering**: Instantly search large execution trees by keyword name, argument, or log message.
16
+ * **Process Isolated**: Runs in a separate `multiprocessing` process, dodging the Python GIL and ensuring your test timings are not affected by GUI rendering overhead.
17
+
18
+ ## Requirements
19
+
20
+ * Python 3.10+
21
+ * Robot Framework >= 7.0 *(Requires the V3 Listener API)*
22
+ * Dear PyGui >= 2.0
23
+
24
+ ## Installation
25
+
26
+ Install the package directly from PyPI:
27
+
28
+ ```bash
29
+ uv add robotframework-live-trace
30
+ ```
31
+
32
+ or using pip:
33
+
34
+ ```bash
35
+ pip install robotframework-live-trace
36
+
37
+ ```
38
+
39
+ ### RHEL, Rocky and AlmaLinux 9
40
+
41
+ Dear PyGui 2.1 and newer need a newer `libstdc++` than these systems ship, so importing them fails with an error like ``version `GLIBCXX_3.4.30' not found``. Install Dear PyGui 2.0.0 instead (available for Python 3.13 and older):
42
+
43
+ ```bash
44
+ pip install robotframework-live-trace "dearpygui==2.0.0"
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ You do not need to modify your Robot Framework test files. Simply pass the listener to the `robot` command:
50
+
51
+ ```bash
52
+ robot --listener RobotLiveTrace my_tests/
53
+
54
+ ```
55
+
56
+ The Dear PyGui dashboard will open immediately, and the test execution will begin. When the test suite finishes, the window will remain open so you can inspect the final call stack. Close the window to exit robotframework.
57
+
58
+ ## UI Controls
59
+
60
+ The top control bar provides several ways to manage large test executions:
61
+
62
+ * **Pause**: Freezes the Robot Framework execution thread. Uncheck to resume.
63
+ * **Pause on Failure**: Pauses where a keyword or control structure fails, and checks **Pause** so you can inspect it. Uncheck **Pause** to resume. This includes failures that are handled later, such as by `TRY/EXCEPT` or `Run Keyword And Ignore Error`, because they can't be told apart when they happen.
64
+ * **Auto-Scroll**: Automatically scrolls the tree and error feed to the bottom as new keywords and logs arrive.
65
+ * **Auto-Collapse Passed**: When a keyword, test or suite finishes with a `PASS` or `NOT RUN` status, it will automatically collapse its tree node to keep the UI clean. Failed nodes will remain expanded.
66
+ * **Expand All / Collapse All**: Instantly open or close all currently rendered tree nodes.
67
+ * **Filter (Name/Args/Logs)**: Type to instantly hide non-matching keywords. The tree will automatically expand parents to reveal deeply nested matches.
68
+
69
+ ## Contributing
70
+
71
+ Contributions, issues, and feature requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md)
72
+
73
+ ## License
74
+
75
+ Distributed under the MIT License. See [LICENSE](LICENSE) for more information.
76
+
77
+ ### Third-Party Assets
78
+ * **Noto Sans Font**: This package bundles the "Noto Sans" font. The font is licensed under the SIL Open Font License, Version 1.1. The full license text can be found in [src/RobotLiveTrace/fonts/OFL.txt](src/RobotLiveTrace/fonts/OFL.txt).
@@ -0,0 +1,64 @@
1
+ [project]
2
+ name = "robotframework-live-trace"
3
+ version = "0.2.0"
4
+ description = "A live trace visualization for Robot Framework test runs."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.10"
9
+ dependencies = [
10
+ "dearpygui>=2.0",
11
+ "robotframework>=7.0",
12
+ ]
13
+ keywords = [
14
+ "robotframework",
15
+ "automation",
16
+ "testautomation",
17
+ "testing",
18
+ "debugging",
19
+ "visualization",
20
+ "trace",
21
+ "live",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Framework :: Robot Framework",
26
+ "Framework :: Robot Framework :: Tool",
27
+ "Intended Audience :: Developers",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Programming Language :: Python :: 3.14",
36
+ "Topic :: Software Development :: Testing",
37
+ "Topic :: Software Development :: Debuggers",
38
+ ]
39
+
40
+ [[project.authors]]
41
+ name = "Manuel Ketterer"
42
+ email = "ps3udonym4@gmail.com"
43
+
44
+ [project.urls]
45
+ Homepage = "https://github.com/iDentitY4/robotframework-live-trace"
46
+ Repository = "https://github.com/iDentitY4/robotframework-live-trace.git"
47
+ Issues = "https://github.com/iDentitY4/robotframework-live-trace/issues"
48
+
49
+ [build-system]
50
+ requires = ["uv_build>=0.13.7,<0.14.0"]
51
+ build-backend = "uv_build"
52
+
53
+ [tool.uv.build-backend]
54
+ module-name = "RobotLiveTrace"
55
+
56
+ [tool.ty.rules]
57
+ unused-type-ignore-comment = "ignore"
58
+
59
+ [dependency-groups]
60
+ dev = [
61
+ "pytest>=9.0.2",
62
+ "ruff>=0.15.2",
63
+ "ty>=0.0.18",
64
+ ]
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "robotframework-live-trace"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "A live trace visualization for Robot Framework test runs."
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
7
  license-files = ["LICENSE"]
8
8
  authors = [{ name = "Manuel Ketterer", email = "ps3udonym4@gmail.com" }]
9
9
  requires-python = ">=3.10"
10
- dependencies = ["dearpygui>=2.2", "robotframework>=7.0"]
10
+ dependencies = ["dearpygui>=2.0", "robotframework>=7.0"]
11
11
  keywords = [
12
12
  "robotframework",
13
13
  "automation",
@@ -41,7 +41,7 @@ Repository = "https://github.com/iDentitY4/robotframework-live-trace.git"
41
41
  Issues = "https://github.com/iDentitY4/robotframework-live-trace/issues"
42
42
 
43
43
  [build-system]
44
- requires = ["uv_build>=0.10.4,<0.11.0"]
44
+ requires = ["uv_build>=0.13.7,<0.14.0"]
45
45
  build-backend = "uv_build"
46
46
 
47
47
  [tool.uv.build-backend]
@@ -2,5 +2,5 @@ from RobotLiveTrace.listener import RobotLiveTrace
2
2
  from RobotLiveTrace.version import AUTHOR, VERSION
3
3
 
4
4
  __all__ = ["RobotLiveTrace"]
5
- __version = VERSION
5
+ __version__ = VERSION
6
6
  __author__ = AUTHOR
@@ -20,12 +20,25 @@ class FontSize(enum.IntEnum):
20
20
  return f"{self.name.title()} ({self.value}px)"
21
21
 
22
22
 
23
+ STATUS_THEMES = {"PASS": "pass", "NOT RUN": "not_run", "SKIP": "log_warn"}
24
+
25
+
26
+ def preview(text: str) -> str:
27
+ """Shorten text to one line so it doesn't break the UI layout."""
28
+ single_line = text.replace("\n", " ")
29
+ return single_line if len(single_line) < 80 else single_line[:77] + "..."
30
+
31
+
23
32
  class GUI:
24
33
  def __init__(
25
- self, message_queue: queue.Queue[Message], pause_event: Event | None = None
34
+ self,
35
+ message_queue: queue.Queue[Message],
36
+ pause_event: Event | None = None,
37
+ pause_on_failure_event: Event | None = None,
26
38
  ) -> None:
27
39
  self.message_queue = message_queue
28
40
  self.pause_event = pause_event
41
+ self.pause_on_failure_event = pause_on_failure_event
29
42
 
30
43
  self.all_nodes: list[int | str] = []
31
44
  self.node_parents: dict[int | str, Any] = {}
@@ -46,6 +59,7 @@ class GUI:
46
59
  "start_test": "[TEST]",
47
60
  "start_user_keyword": "[USER-KW]",
48
61
  "start_library_keyword": "[LIB-KW]",
62
+ "start_invalid_keyword": "[INVALID-KW]",
49
63
  }
50
64
 
51
65
  def setup_icons(self) -> None:
@@ -88,6 +102,11 @@ class GUI:
88
102
  with dpg.theme_component(dpg.mvTreeNode):
89
103
  dpg.add_theme_color(dpg.mvThemeCol_Text, (180, 180, 180, 255))
90
104
 
105
+ with dpg.theme() as self.themes["not_run"]:
106
+ with dpg.theme_component(dpg.mvTreeNode):
107
+ # Dim gray text for keywords and branches that didn't run
108
+ dpg.add_theme_color(dpg.mvThemeCol_Text, (120, 120, 120, 255))
109
+
91
110
  with dpg.theme() as self.themes["highlight"]:
92
111
  with dpg.theme_component(dpg.mvAll):
93
112
  # Bright Yellow text so it pops out instantly
@@ -130,7 +149,16 @@ class GUI:
130
149
  )
131
150
 
132
151
  with dpg.group(horizontal=True):
133
- dpg.add_checkbox(label="Pause Execution", callback=self.toggle_pause)
152
+ dpg.add_checkbox(
153
+ label="Pause Execution",
154
+ tag="pause_toggle",
155
+ callback=self.toggle_pause,
156
+ )
157
+ dpg.add_text(" | ")
158
+ dpg.add_checkbox(
159
+ label="Pause on Failure",
160
+ callback=self.toggle_pause_on_failure,
161
+ )
134
162
  dpg.add_text(" | ")
135
163
  dpg.add_checkbox(
136
164
  label="Auto-Scroll", tag="auto_scroll_toggle", default_value=True
@@ -197,6 +225,20 @@ class GUI:
197
225
  else:
198
226
  self.pause_event.set() # Resume the listener process
199
227
 
228
+ def toggle_pause_on_failure(self, sender: int, app_data: Any):
229
+ if self.pause_on_failure_event is None:
230
+ return
231
+
232
+ if app_data:
233
+ self.pause_on_failure_event.set()
234
+ else:
235
+ self.pause_on_failure_event.clear()
236
+
237
+ def sync_pause_toggle(self) -> None:
238
+ # The listener can pause on its own when a failure happens
239
+ if self.pause_event is not None:
240
+ dpg.set_value("pause_toggle", not self.pause_event.is_set())
241
+
200
242
  def expand_all(self, sender: int, app_data: Any):
201
243
  for node_id in self.all_nodes:
202
244
  dpg.set_value(node_id, True)
@@ -279,9 +321,9 @@ class GUI:
279
321
  name = data.get("name", "Unknown")
280
322
  args = data.get("args", [])
281
323
 
282
- prefix = self.prefixes.get(action, "[*]")
324
+ prefix = self.prefixes.get(action, "")
283
325
  parent_id = self.active_stack[-1]
284
- base_label = f"{prefix} {name}"
326
+ base_label = preview(f"{prefix} {name}".strip())
285
327
 
286
328
  node_id = dpg.add_tree_node(
287
329
  label=base_label, parent=parent_id, default_open=True
@@ -299,7 +341,7 @@ class GUI:
299
341
  dpg.add_text(
300
342
  f"Type: {action.replace('start_', '').replace('_', ' ').title()}"
301
343
  )
302
- dpg.add_text(f"Name: {name}")
344
+ dpg.add_text(f"Name: {name}", wrap=600)
303
345
 
304
346
  if args:
305
347
  dpg.add_text(f"Args: {', '.join(args)}")
@@ -326,13 +368,10 @@ class GUI:
326
368
  full_txt = result.get("message", "<MISSING LOG MESSAGE>")
327
369
  timestamp = result.get("timestamp", "<NO TIMESTAMP>")
328
370
 
329
- # Truncate text for the tree label so it doesn't break the UI layout
330
- # Replace newlines with spaces for a cleaner single-line preview
331
371
  clean_txt = full_txt.replace("\n", " ")
332
- display_txt = clean_txt if len(clean_txt) < 80 else clean_txt[:77] + "..."
333
372
 
334
373
  node_id = dpg.add_tree_node(
335
- label=f"[{level}] {display_txt}",
374
+ label=f"[{level}] {preview(full_txt)}",
336
375
  parent=parent_id,
337
376
  leaf=True,
338
377
  )
@@ -392,19 +431,28 @@ class GUI:
392
431
  status = result.get("status", "UNKNOWN")
393
432
  elapsed = result.get("elapsed_time", 0.0)
394
433
 
395
- if status == "PASS":
396
- dpg.bind_item_theme(node_id, self.themes["pass"])
397
- self.node_themes[node_id] = self.themes["pass"]
434
+ theme = self.themes[STATUS_THEMES.get(status, "fail")]
435
+ dpg.bind_item_theme(node_id, theme)
436
+ self.node_themes[node_id] = theme
398
437
 
399
- if dpg.get_value("auto_collapse_toggle"):
400
- dpg.set_value(node_id, False)
401
- else:
402
- dpg.bind_item_theme(node_id, self.themes["fail"])
403
- self.node_themes[node_id] = self.themes["fail"]
438
+ if status in ("PASS", "NOT RUN") and dpg.get_value("auto_collapse_toggle"):
439
+ dpg.set_value(node_id, False)
404
440
 
405
441
  dpg.set_value(node["status_id"], f"Status: {status}")
406
442
  dpg.set_value(node["time_id"], f"Elapsed Time: {elapsed} s")
407
443
 
444
+ def handle_message(self, msg: Message) -> None:
445
+ action = msg.get("action", "unknown")
446
+ data = msg.get("data", None)
447
+ result = msg.get("result", None)
448
+
449
+ if action.startswith("start_") and data is not None:
450
+ self.handle_start(action, data=data)
451
+ elif action == "log_message" and result is not None:
452
+ self.handle_log(result)
453
+ elif action.startswith("end_") and result is not None:
454
+ self.handle_end(action, result)
455
+
408
456
  def run(self) -> None:
409
457
  dpg.create_context()
410
458
  dpg.create_viewport(title="RobotLiveTrace", width=1400, height=800)
@@ -429,23 +477,12 @@ class GUI:
429
477
 
430
478
  try:
431
479
  while True:
432
- msg: Message = self.message_queue.get_nowait()
433
- action = msg.get("action", "unknown")
434
- data = msg.get("data", None)
435
- result = msg.get("result", None)
436
-
437
- if action.startswith("start_") and data is not None:
438
- self.handle_start(
439
- action,
440
- data=data,
441
- )
442
- elif action == "log_message" and result is not None:
443
- self.handle_log(result)
444
- elif action.startswith("end_") and result is not None:
445
- self.handle_end(action, result)
480
+ self.handle_message(self.message_queue.get_nowait())
446
481
  except queue.Empty:
447
482
  pass
448
483
 
484
+ self.sync_pause_toggle()
485
+
449
486
  if self.needs_filter_update and dpg.get_value("filter_input"):
450
487
  self.apply_filter()
451
488
 
@@ -461,8 +498,12 @@ class GUI:
461
498
  dpg.destroy_context()
462
499
 
463
500
 
464
- def run_gui(q: queue.Queue[Message], pause_event: Event | None = None):
465
- gui = GUI(q, pause_event)
501
+ def run_gui(
502
+ q: queue.Queue[Message],
503
+ pause_event: Event | None = None,
504
+ pause_on_failure_event: Event | None = None,
505
+ ):
506
+ gui = GUI(q, pause_event, pause_on_failure_event)
466
507
  gui.run()
467
508
 
468
509
 
@@ -0,0 +1,225 @@
1
+ import multiprocessing
2
+ import queue
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from robot.model import ModelObject # type: ignore
7
+ from robot.utils import safe_str # type: ignore
8
+
9
+ from RobotLiveTrace.message import Message
10
+
11
+
12
+ # These only hold IF and TRY branches, so showing them would just add a tree level.
13
+ _BRANCH_CONTAINERS = ("IF/ELSE ROOT", "TRY/EXCEPT ROOT")
14
+
15
+
16
+ def _body_item_name(data: Any) -> str:
17
+ if data.type != "ITERATION":
18
+ return str(data)
19
+
20
+ # FOR iterations have assigned values, WHILE iterations have none
21
+ values = getattr(data, "assign", {})
22
+ assigned = ", ".join(f"{name} = {safe_str(v)}" for name, v in values.items())
23
+ return f"ITERATION {assigned}".rstrip()
24
+
25
+
26
+ def _load_run_gui() -> Callable[..., None]:
27
+ """Import the GUI on demand so a broken Dear PyGui install gets a clear error."""
28
+ try:
29
+ from RobotLiveTrace.gui import run_gui
30
+ except ImportError as error:
31
+ raise ImportError(
32
+ f"RobotLiveTrace could not load Dear PyGui: {error}\n"
33
+ "If this mentions GLIBC or GLIBCXX (for example on RHEL, Rocky or "
34
+ "AlmaLinux 9), install a compatible version with: "
35
+ 'pip install "dearpygui==2.0.0"'
36
+ ) from error
37
+
38
+ return run_gui
39
+
40
+
41
+ class RobotLiveTrace:
42
+ """A Robot Framework listener that provides a real-time execution trace viewer.
43
+
44
+ Usage:
45
+ To use this listener, run your Robot Framework tests with the --listener
46
+ option:
47
+
48
+ `robot --listener RobotLiveTrace path/to/tests.robot`
49
+ """
50
+
51
+ ROBOT_LISTENER_API_VERSION = 3
52
+
53
+ def __init__(self):
54
+ run_gui = _load_run_gui()
55
+
56
+ self.message_queue = multiprocessing.Queue()
57
+ self.pause_event = multiprocessing.Event()
58
+ self.pause_event.set()
59
+ self.pause_on_failure_event = multiprocessing.Event()
60
+ self.last_failure_id = ""
61
+
62
+ self.gui_process = multiprocessing.Process(
63
+ target=run_gui,
64
+ args=(self.message_queue, self.pause_event, self.pause_on_failure_event),
65
+ )
66
+ self.gui_process.start()
67
+
68
+ def _wait_and_send(
69
+ self,
70
+ action: str,
71
+ data: dict[str, Any] | None = None,
72
+ result: dict[str, Any] | None = None,
73
+ ):
74
+ """
75
+ Helper method to enforce pausing and send a message to the GUI.
76
+ """
77
+
78
+ if not self.gui_process.is_alive():
79
+ return
80
+
81
+ while not self.pause_event.is_set():
82
+ if not self.gui_process.is_alive():
83
+ return
84
+ self.pause_event.wait(timeout=0.2)
85
+
86
+ message: Message = {
87
+ "action": action,
88
+ "data": data,
89
+ "result": result,
90
+ }
91
+
92
+ try:
93
+ self.message_queue.put(message)
94
+ except (queue.Full, ValueError):
95
+ pass
96
+
97
+ def _send_start(self, action: str, data: Any):
98
+ # Only send what the GUI displays. Arguments are converted to strings
99
+ # because libraries can pass any object, including ones that can't be
100
+ # pickled, when calling keywords programmatically.
101
+ payload: dict[str, Any] = {"name": data.name}
102
+ args = getattr(data, "args", ())
103
+ if args:
104
+ payload["args"] = [safe_str(arg) for arg in args]
105
+
106
+ self._wait_and_send(action, data=payload)
107
+
108
+ def _send_end(self, action: str, result: Any):
109
+ self._wait_and_send(
110
+ action,
111
+ result={
112
+ "status": result.status,
113
+ "elapsed_time": result.elapsed_time.total_seconds(),
114
+ },
115
+ )
116
+
117
+ def _pause_if_failure_started_here(self, result: Any):
118
+ """Request a pause, which takes effect when the next event is sent.
119
+
120
+ A failure also ends every parent as failed. IDs are hierarchical, so an
121
+ item whose ID prefixes the previous failure's ID is only passing it on.
122
+ """
123
+ if result.status != "FAIL":
124
+ return
125
+
126
+ passed_on = self.last_failure_id.startswith(f"{result.id}-")
127
+ self.last_failure_id = result.id
128
+
129
+ if not passed_on and self.pause_on_failure_event.is_set():
130
+ self.pause_event.clear()
131
+
132
+ def _send_keyword_end(self, action: str, result: Any):
133
+ self._send_end(action, result)
134
+ self._pause_if_failure_started_here(result)
135
+
136
+ def start_suite(self, data: ModelObject, result: ModelObject):
137
+ self._send_start("start_suite", data)
138
+
139
+ def end_suite(self, data: ModelObject, result: ModelObject):
140
+ self._send_end("end_suite", result)
141
+
142
+ def start_test(self, data: ModelObject, result: ModelObject):
143
+ self._send_start("start_test", data)
144
+
145
+ def end_test(self, data: ModelObject, result: ModelObject):
146
+ self._send_end("end_test", result)
147
+
148
+ def start_user_keyword(
149
+ self,
150
+ data: ModelObject,
151
+ implementation: ModelObject,
152
+ result: ModelObject,
153
+ ):
154
+ self._send_start("start_user_keyword", data)
155
+
156
+ def end_user_keyword(
157
+ self,
158
+ data: ModelObject,
159
+ implementation: ModelObject,
160
+ result: ModelObject,
161
+ ):
162
+ self._send_keyword_end("end_user_keyword", result)
163
+
164
+ def start_library_keyword(
165
+ self,
166
+ data: ModelObject,
167
+ implementation: ModelObject,
168
+ result: ModelObject,
169
+ ):
170
+ self._send_start("start_library_keyword", data)
171
+
172
+ def end_library_keyword(
173
+ self,
174
+ data: ModelObject,
175
+ implementation: ModelObject,
176
+ result: ModelObject,
177
+ ):
178
+ self._send_keyword_end("end_library_keyword", result)
179
+
180
+ def start_invalid_keyword(
181
+ self,
182
+ data: ModelObject,
183
+ implementation: ModelObject,
184
+ result: ModelObject,
185
+ ):
186
+ self._send_start("start_invalid_keyword", data)
187
+
188
+ def end_invalid_keyword(
189
+ self,
190
+ data: ModelObject,
191
+ implementation: ModelObject,
192
+ result: ModelObject,
193
+ ):
194
+ self._send_keyword_end("end_invalid_keyword", result)
195
+
196
+ def start_body_item(self, data: Any, result: ModelObject):
197
+ """Called for everything without its own method, such as FOR and IF."""
198
+ if data.type not in _BRANCH_CONTAINERS:
199
+ self._wait_and_send(
200
+ f"start_{data.type.lower()}", data={"name": _body_item_name(data)}
201
+ )
202
+
203
+ def end_body_item(self, data: Any, result: Any):
204
+ if data.type not in _BRANCH_CONTAINERS:
205
+ self._send_keyword_end(f"end_{data.type.lower()}", result)
206
+
207
+ def log_message(self, message: Any):
208
+ result = {"level": message.level, "message": message.message}
209
+ if message.timestamp:
210
+ result["timestamp"] = message.timestamp.isoformat()
211
+
212
+ self._wait_and_send("log_message", result=result)
213
+
214
+ def close(self):
215
+ # We join the process here. This intentionally blocks Robot Framework
216
+ # from completely exiting until you manually close the GUI window,
217
+ # allowing you to inspect the final call stack tree.
218
+
219
+ self.message_queue.cancel_join_thread()
220
+
221
+ if self.gui_process.is_alive():
222
+ print(
223
+ "RobotLiveTrace is open. Close the GUI window to exit Robot Framework."
224
+ )
225
+ self.gui_process.join()
@@ -1,67 +0,0 @@
1
- # Robotframework Live Trace
2
-
3
- A real-time, cross-platform execution visualization dashboard for Robot Framework.
4
-
5
- Built on top of [Dear PyGui](https://github.com/hoffstadt/DearPyGui) and the Robot Framework Listener API (v3), this tool runs a high-performance, hardware-accelerated GUI in a completely isolated process. It allows you to monitor deeply nested keyword call stacks, inspect live arguments, and trace failures exactly as they happen without blocking your test execution.
6
-
7
- ## Features
8
-
9
- * **Live Execution Tree**: Watch your Suites, Tests, and Keywords expand and collapse in real-time.
10
- * **Dedicated Error Feed**: A split-pane chronological feed of all `WARN`, `ERROR`, and `FAIL` logs. Click any error to instantly expand the tree and highlight the exact keyword that caused it.
11
- * **Interactive Tooltips**: Hover over any keyword to see its execution time, status, and input arguments.
12
- * **Execution Control**: Pause and resume the active Robot Framework test run directly from the GUI.
13
- * **Smart Filtering**: Instantly search massive execution trees by keyword name, argument, or log message.
14
- * **Process Isolated**: Runs in a separate `multiprocessing` process, dodging the Python GIL and ensuring your test timings are not affected by GUI rendering overhead.
15
-
16
- ## Requirements
17
-
18
- * Python 3.10+
19
- * Robot Framework >= 7.0 *(Requires the V3 Listener API)*
20
- * Dear PyGui >= 2.2
21
-
22
- ## Installation
23
-
24
- Install the package directly from PyPI:
25
-
26
- ```bash
27
- uv add robotframework-live-trace
28
- ```
29
-
30
- or using pip:
31
-
32
- ```bash
33
- pip install robotframework-live-trace
34
-
35
- ```
36
-
37
- ## Usage
38
-
39
- You do not need to modify your Robot Framework test files. Simply pass the listener to the `robot` command:
40
-
41
- ```bash
42
- robot --listener RobotLiveTrace my_tests/
43
-
44
- ```
45
-
46
- The Dear PyGui dashboard will open immediately, and the test execution will begin. When the test suite finishes, the window will remain open so you can inspect the final call stack. Close the window to exit robotframework.
47
-
48
- ## UI Controls
49
-
50
- The top control bar provides several ways to manage large test executions:
51
-
52
- * **Pause**: Freezes the Robot Framework execution thread. Uncheck to resume.
53
- * **Auto-Scroll**: Automatically scrolls the tree and error feed to the bottom as new keywords and logs arrive.
54
- * **Auto-Collapse Passed**: When a test or suite finishes with a `PASS` status, it will automatically collapse its tree node to keep the UI clean. Failed nodes will remain expanded.
55
- * **Expand All / Collapse All**: Instantly open or close all currently rendered tree nodes.
56
- * **Filter (Name/Args/Logs)**: Type to instantly hide non-matching keywords. The tree will automatically expand parents to reveal deeply nested matches.
57
-
58
- ## Contributing
59
-
60
- Contributions, issues, and feature requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md)
61
-
62
- ## License
63
-
64
- Distributed under the MIT License. See [LICENSE](LICENSE) for more information.
65
-
66
- ### Third-Party Assets
67
- * **Noto Sans Font**: This package bundles the "Noto Sans" font. The font is licensed under the SIL Open Font License, Version 1.1. The full license text can be found in [src/RobotLiveTrace/fonts/OFL.txt](src/RobotLiveTrace/fonts/OFL.txt).
@@ -1,112 +0,0 @@
1
- import multiprocessing
2
- import queue
3
-
4
- from robot.model import ModelObject # type: ignore
5
-
6
- from RobotLiveTrace.gui import run_gui
7
- from RobotLiveTrace.message import Message
8
-
9
-
10
- class RobotLiveTrace:
11
- """ """
12
-
13
- ROBOT_LISTENER_API_VERSION = 3
14
-
15
- def __init__(self):
16
- self.message_queue = multiprocessing.Queue()
17
- self.pause_event = multiprocessing.Event()
18
- self.pause_event.set()
19
-
20
- self.gui_process = multiprocessing.Process(
21
- target=run_gui, args=(self.message_queue, self.pause_event)
22
- )
23
- self.gui_process.start()
24
-
25
- def _wait_and_send(
26
- self,
27
- action: str,
28
- data: ModelObject | None = None,
29
- result: ModelObject | None = None,
30
- ):
31
- """
32
- Helper method to enforce pausing and serialize data.
33
- """
34
-
35
- if not self.gui_process.is_alive():
36
- return
37
-
38
- while not self.pause_event.is_set():
39
- if not self.gui_process.is_alive():
40
- return
41
- self.pause_event.wait(timeout=0.2)
42
-
43
- message: Message = {
44
- "action": action,
45
- "data": data.to_dict() if data else None,
46
- "result": result.to_dict() if result else None,
47
- }
48
-
49
- try:
50
- self.message_queue.put(message)
51
- except (queue.Full, ValueError):
52
- pass
53
-
54
- def start_suite(self, data: ModelObject, result: ModelObject):
55
- self._wait_and_send("start_suite", data=data, result=result)
56
-
57
- def end_suite(self, data: ModelObject, result: ModelObject):
58
- self._wait_and_send("end_suite", data=data, result=result)
59
-
60
- def start_test(self, data: ModelObject, result: ModelObject):
61
- self._wait_and_send("start_test", data=data, result=result)
62
-
63
- def end_test(self, data: ModelObject, result: ModelObject):
64
- self._wait_and_send("end_test", data=data, result=result)
65
-
66
- def start_user_keyword(
67
- self,
68
- data: ModelObject,
69
- implementation: ModelObject,
70
- result: ModelObject,
71
- ):
72
- self._wait_and_send("start_user_keyword", data=data, result=result)
73
-
74
- def end_user_keyword(
75
- self,
76
- data: ModelObject,
77
- implementation: ModelObject,
78
- result: ModelObject,
79
- ):
80
- self._wait_and_send("end_user_keyword", data=data, result=result)
81
-
82
- def start_library_keyword(
83
- self,
84
- data: ModelObject,
85
- implementation: ModelObject,
86
- result: ModelObject,
87
- ):
88
- self._wait_and_send("start_library_keyword", data=data, result=result)
89
-
90
- def end_library_keyword(
91
- self,
92
- data: ModelObject,
93
- implementation: ModelObject,
94
- result: ModelObject,
95
- ):
96
- self._wait_and_send("end_library_keyword", data=data, result=result)
97
-
98
- def log_message(self, message: ModelObject):
99
- self._wait_and_send("log_message", result=message)
100
-
101
- def close(self):
102
- # We join the process here. This intentionally blocks Robot Framework
103
- # from completely exiting until you manually close the GUI window,
104
- # allowing you to inspect the final call stack tree.
105
-
106
- self.message_queue.cancel_join_thread()
107
-
108
- if self.gui_process.is_alive():
109
- print(
110
- "RobotLiveTrace is open. Close the GUI window to exit Robot Framework."
111
- )
112
- self.gui_process.join()