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.
- {robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/PKG-INFO +23 -12
- robotframework_live_trace-0.2.0/README.md +78 -0
- robotframework_live_trace-0.2.0/pyproject.toml +64 -0
- robotframework_live_trace-0.1.0/pyproject.toml → robotframework_live_trace-0.2.0/pyproject.toml.orig +3 -3
- {robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/src/RobotLiveTrace/__init__.py +1 -1
- {robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/src/RobotLiveTrace/gui.py +74 -33
- robotframework_live_trace-0.2.0/src/RobotLiveTrace/listener.py +225 -0
- robotframework_live_trace-0.1.0/README.md +0 -67
- robotframework_live_trace-0.1.0/src/RobotLiveTrace/listener.py +0 -112
- {robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/LICENSE +0 -0
- {robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/src/RobotLiveTrace/fonts/NotoSans-Regular.ttf +0 -0
- {robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/src/RobotLiveTrace/fonts/OFL.txt +0 -0
- {robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/src/RobotLiveTrace/message.py +0 -0
- {robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/src/RobotLiveTrace/py.typed +0 -0
- {robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/src/RobotLiveTrace/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: robotframework-live-trace
|
|
3
|
-
Version: 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.
|
|
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
|
-
#
|
|
32
|
+
# Robot Framework Live Trace
|
|
33
33
|
|
|
34
|
-
A real-time, cross-platform execution visualization
|
|
34
|
+
A real-time, cross-platform test execution visualization for Robot Framework powered by [DearPyGui](https://github.com/hoffstadt/DearPyGui).
|
|
35
35
|
|
|
36
|
-
|
|
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
|
+

|
|
37
39
|
|
|
38
40
|
## Features
|
|
39
41
|
|
|
40
|
-
* **Live Execution Tree**: Watch your Suites, Tests, and
|
|
41
|
-
* **
|
|
42
|
-
* **
|
|
43
|
-
* **Execution Control**: Pause and resume the active Robot Framework test run directly from the GUI.
|
|
44
|
-
* **
|
|
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.
|
|
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
|
+

|
|
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
|
+
]
|
robotframework_live_trace-0.1.0/pyproject.toml → robotframework_live_trace-0.2.0/pyproject.toml.orig
RENAMED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "robotframework-live-trace"
|
|
3
|
-
version = "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.
|
|
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.
|
|
44
|
+
requires = ["uv_build>=0.13.7,<0.14.0"]
|
|
45
45
|
build-backend = "uv_build"
|
|
46
46
|
|
|
47
47
|
[tool.uv.build-backend]
|
{robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/src/RobotLiveTrace/gui.py
RENAMED
|
@@ -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,
|
|
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(
|
|
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}] {
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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(
|
|
465
|
-
|
|
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()
|
|
File without changes
|
|
File without changes
|
{robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/src/RobotLiveTrace/fonts/OFL.txt
RENAMED
|
File without changes
|
{robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/src/RobotLiveTrace/message.py
RENAMED
|
File without changes
|
{robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/src/RobotLiveTrace/py.typed
RENAMED
|
File without changes
|
{robotframework_live_trace-0.1.0 → robotframework_live_trace-0.2.0}/src/RobotLiveTrace/version.py
RENAMED
|
File without changes
|