robotframework-live-trace 0.1.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/LICENSE +21 -0
- robotframework_live_trace-0.1.0/PKG-INFO +98 -0
- robotframework_live_trace-0.1.0/README.md +67 -0
- robotframework_live_trace-0.1.0/pyproject.toml +54 -0
- robotframework_live_trace-0.1.0/src/RobotLiveTrace/__init__.py +6 -0
- robotframework_live_trace-0.1.0/src/RobotLiveTrace/fonts/NotoSans-Regular.ttf +0 -0
- robotframework_live_trace-0.1.0/src/RobotLiveTrace/fonts/OFL.txt +93 -0
- robotframework_live_trace-0.1.0/src/RobotLiveTrace/gui.py +470 -0
- robotframework_live_trace-0.1.0/src/RobotLiveTrace/listener.py +112 -0
- robotframework_live_trace-0.1.0/src/RobotLiveTrace/message.py +7 -0
- robotframework_live_trace-0.1.0/src/RobotLiveTrace/py.typed +0 -0
- robotframework_live_trace-0.1.0/src/RobotLiveTrace/version.py +8 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Manuel Ketterer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robotframework-live-trace
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A live trace visualization for Robot Framework test runs.
|
|
5
|
+
Keywords: robotframework,automation,testautomation,testing,debugging,visualization,trace,live
|
|
6
|
+
Author: Manuel Ketterer
|
|
7
|
+
Author-email: Manuel Ketterer <ps3udonym4@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Framework :: Robot Framework
|
|
12
|
+
Classifier: Framework :: Robot Framework :: Tool
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Software Development :: Testing
|
|
23
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
24
|
+
Requires-Dist: dearpygui>=2.2
|
|
25
|
+
Requires-Dist: robotframework>=7.0
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Project-URL: Homepage, https://github.com/iDentitY4/robotframework-live-trace
|
|
28
|
+
Project-URL: Repository, https://github.com/iDentitY4/robotframework-live-trace.git
|
|
29
|
+
Project-URL: Issues, https://github.com/iDentitY4/robotframework-live-trace/issues
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# Robotframework Live Trace
|
|
33
|
+
|
|
34
|
+
A real-time, cross-platform execution visualization dashboard for Robot Framework.
|
|
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.
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
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.
|
|
45
|
+
* **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
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
* Python 3.10+
|
|
50
|
+
* Robot Framework >= 7.0 *(Requires the V3 Listener API)*
|
|
51
|
+
* Dear PyGui >= 2.2
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
Install the package directly from PyPI:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv add robotframework-live-trace
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
or using pip:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install robotframework-live-trace
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
You do not need to modify your Robot Framework test files. Simply pass the listener to the `robot` command:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
robot --listener RobotLiveTrace my_tests/
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
## UI Controls
|
|
80
|
+
|
|
81
|
+
The top control bar provides several ways to manage large test executions:
|
|
82
|
+
|
|
83
|
+
* **Pause**: Freezes the Robot Framework execution thread. Uncheck to resume.
|
|
84
|
+
* **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.
|
|
86
|
+
* **Expand All / Collapse All**: Instantly open or close all currently rendered tree nodes.
|
|
87
|
+
* **Filter (Name/Args/Logs)**: Type to instantly hide non-matching keywords. The tree will automatically expand parents to reveal deeply nested matches.
|
|
88
|
+
|
|
89
|
+
## Contributing
|
|
90
|
+
|
|
91
|
+
Contributions, issues, and feature requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
Distributed under the MIT License. See [LICENSE](LICENSE) for more information.
|
|
96
|
+
|
|
97
|
+
### Third-Party Assets
|
|
98
|
+
* **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,67 @@
|
|
|
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).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "robotframework-live-trace"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A live trace visualization for Robot Framework test runs."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Manuel Ketterer", email = "ps3udonym4@gmail.com" }]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = ["dearpygui>=2.2", "robotframework>=7.0"]
|
|
11
|
+
keywords = [
|
|
12
|
+
"robotframework",
|
|
13
|
+
"automation",
|
|
14
|
+
"testautomation",
|
|
15
|
+
"testing",
|
|
16
|
+
"debugging",
|
|
17
|
+
"visualization",
|
|
18
|
+
"trace",
|
|
19
|
+
"live",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Framework :: Robot Framework",
|
|
24
|
+
"Framework :: Robot Framework :: Tool",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Programming Language :: Python :: 3.14",
|
|
34
|
+
"Topic :: Software Development :: Testing",
|
|
35
|
+
"Topic :: Software Development :: Debuggers",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/iDentitY4/robotframework-live-trace"
|
|
40
|
+
Repository = "https://github.com/iDentitY4/robotframework-live-trace.git"
|
|
41
|
+
Issues = "https://github.com/iDentitY4/robotframework-live-trace/issues"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["uv_build>=0.10.4,<0.11.0"]
|
|
45
|
+
build-backend = "uv_build"
|
|
46
|
+
|
|
47
|
+
[tool.uv.build-backend]
|
|
48
|
+
module-name = "RobotLiveTrace"
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
dev = ["pytest>=9.0.2", "ruff>=0.15.2", "ty>=0.0.18"]
|
|
52
|
+
|
|
53
|
+
[tool.ty.rules]
|
|
54
|
+
unused-type-ignore-comment = "ignore"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
https://openfontlicense.org
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import queue
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
from multiprocessing.synchronize import Event
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import dearpygui.dearpygui as dpg # type: ignore
|
|
8
|
+
|
|
9
|
+
from RobotLiveTrace.message import Message
|
|
10
|
+
from RobotLiveTrace.version import AUTHOR, VERSION
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FontSize(enum.IntEnum):
|
|
14
|
+
TINY = 10
|
|
15
|
+
SMALL = 14
|
|
16
|
+
NORMAL = 18
|
|
17
|
+
LARGE = 24
|
|
18
|
+
|
|
19
|
+
def __str__(self) -> str:
|
|
20
|
+
return f"{self.name.title()} ({self.value}px)"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GUI:
|
|
24
|
+
def __init__(
|
|
25
|
+
self, message_queue: queue.Queue[Message], pause_event: Event | None = None
|
|
26
|
+
) -> None:
|
|
27
|
+
self.message_queue = message_queue
|
|
28
|
+
self.pause_event = pause_event
|
|
29
|
+
|
|
30
|
+
self.all_nodes: list[int | str] = []
|
|
31
|
+
self.node_parents: dict[int | str, Any] = {}
|
|
32
|
+
self.node_search_text: dict[int | str, str] = {}
|
|
33
|
+
self.node_themes: dict[int | str, int] = {}
|
|
34
|
+
self.node_data: dict[int | str, dict[str, Any]] = {}
|
|
35
|
+
self.active_stack: list[int | str] = ["tree_container"]
|
|
36
|
+
|
|
37
|
+
self.font_sizes: dict[FontSize, int | str] = {}
|
|
38
|
+
self.default_font_size = FontSize.NORMAL
|
|
39
|
+
self.themes: dict[str, int] = {}
|
|
40
|
+
|
|
41
|
+
self.needs_filter_update = False
|
|
42
|
+
self.items_added_this_frame = False
|
|
43
|
+
|
|
44
|
+
self.prefixes = {
|
|
45
|
+
"start_suite": "[SUITE]",
|
|
46
|
+
"start_test": "[TEST]",
|
|
47
|
+
"start_user_keyword": "[USER-KW]",
|
|
48
|
+
"start_library_keyword": "[LIB-KW]",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def setup_icons(self) -> None:
|
|
52
|
+
pass
|
|
53
|
+
# if sys.platform == "win32":
|
|
54
|
+
# icon_file = files("RobotLiveTrace").joinpath("icons/app_icon.ico")
|
|
55
|
+
# dpg.set_viewport_small_icon(str(icon_file))
|
|
56
|
+
|
|
57
|
+
def setup_fonts(self) -> None:
|
|
58
|
+
font_file = files("RobotLiveTrace").joinpath("fonts/NotoSans-Regular.ttf")
|
|
59
|
+
|
|
60
|
+
# Setup fonts
|
|
61
|
+
with dpg.font_registry():
|
|
62
|
+
font_path = str(font_file)
|
|
63
|
+
|
|
64
|
+
for size in FontSize:
|
|
65
|
+
self.font_sizes[size] = dpg.add_font(font_path, size.value)
|
|
66
|
+
|
|
67
|
+
def setup_themes(self) -> None:
|
|
68
|
+
with dpg.theme() as self.themes["running"]:
|
|
69
|
+
with dpg.theme_component(dpg.mvTreeNode):
|
|
70
|
+
# Light Blue text for running keywords
|
|
71
|
+
dpg.add_theme_color(dpg.mvThemeCol_Text, (100, 200, 255, 255))
|
|
72
|
+
|
|
73
|
+
with dpg.theme() as self.themes["pass"]:
|
|
74
|
+
with dpg.theme_component(dpg.mvTreeNode):
|
|
75
|
+
# Green text for passed keywords
|
|
76
|
+
dpg.add_theme_color(dpg.mvThemeCol_Text, (50, 255, 50, 255))
|
|
77
|
+
|
|
78
|
+
with dpg.theme() as self.themes["fail"]:
|
|
79
|
+
with dpg.theme_component(dpg.mvTreeNode):
|
|
80
|
+
# Red text for failed keywords
|
|
81
|
+
dpg.add_theme_color(dpg.mvThemeCol_Text, (255, 50, 50, 255))
|
|
82
|
+
|
|
83
|
+
with dpg.theme() as self.themes["log_warn"]:
|
|
84
|
+
with dpg.theme_component(dpg.mvTreeNode):
|
|
85
|
+
dpg.add_theme_color(dpg.mvThemeCol_Text, (255, 200, 50, 255))
|
|
86
|
+
|
|
87
|
+
with dpg.theme() as self.themes["log_info"]:
|
|
88
|
+
with dpg.theme_component(dpg.mvTreeNode):
|
|
89
|
+
dpg.add_theme_color(dpg.mvThemeCol_Text, (180, 180, 180, 255))
|
|
90
|
+
|
|
91
|
+
with dpg.theme() as self.themes["highlight"]:
|
|
92
|
+
with dpg.theme_component(dpg.mvAll):
|
|
93
|
+
# Bright Yellow text so it pops out instantly
|
|
94
|
+
dpg.add_theme_color(dpg.mvThemeCol_Text, (255, 255, 0, 255))
|
|
95
|
+
|
|
96
|
+
def setup_layout(self) -> None:
|
|
97
|
+
with dpg.window(
|
|
98
|
+
label="Call Stack",
|
|
99
|
+
tag="main_window",
|
|
100
|
+
width=1400,
|
|
101
|
+
height=800,
|
|
102
|
+
no_close=True,
|
|
103
|
+
no_collapse=True,
|
|
104
|
+
no_resize=True,
|
|
105
|
+
no_move=True,
|
|
106
|
+
no_title_bar=True,
|
|
107
|
+
):
|
|
108
|
+
with dpg.menu_bar():
|
|
109
|
+
with dpg.menu(label="Menu"):
|
|
110
|
+
with dpg.menu(
|
|
111
|
+
label="Settings",
|
|
112
|
+
):
|
|
113
|
+
dpg.add_combo(
|
|
114
|
+
[str(size) for size in self.font_sizes],
|
|
115
|
+
label="Font Size",
|
|
116
|
+
default_value=str(self.default_font_size),
|
|
117
|
+
width=150,
|
|
118
|
+
callback=self.change_font_size,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
dpg.add_menu_item(
|
|
122
|
+
label="About RobotLiveTrace", callback=self.show_about
|
|
123
|
+
)
|
|
124
|
+
dpg.add_menu_item(
|
|
125
|
+
label="About Dearpygui",
|
|
126
|
+
callback=lambda: dpg.show_tool(dpg.mvTool_About),
|
|
127
|
+
)
|
|
128
|
+
dpg.add_menu_item(
|
|
129
|
+
label="Exit", callback=lambda: dpg.stop_dearpygui()
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
with dpg.group(horizontal=True):
|
|
133
|
+
dpg.add_checkbox(label="Pause Execution", callback=self.toggle_pause)
|
|
134
|
+
dpg.add_text(" | ")
|
|
135
|
+
dpg.add_checkbox(
|
|
136
|
+
label="Auto-Scroll", tag="auto_scroll_toggle", default_value=True
|
|
137
|
+
)
|
|
138
|
+
dpg.add_text(" | ")
|
|
139
|
+
dpg.add_checkbox(
|
|
140
|
+
label="Auto-Collapse Passed",
|
|
141
|
+
tag="auto_collapse_toggle",
|
|
142
|
+
default_value=True,
|
|
143
|
+
)
|
|
144
|
+
dpg.add_text(" | ")
|
|
145
|
+
dpg.add_button(label="Expand All", callback=self.expand_all)
|
|
146
|
+
dpg.add_button(label="Collapse All", callback=self.collapse_all)
|
|
147
|
+
|
|
148
|
+
with dpg.group(horizontal=True):
|
|
149
|
+
dpg.add_input_text(
|
|
150
|
+
label="Filter (Name/Args)",
|
|
151
|
+
tag="filter_input",
|
|
152
|
+
callback=self.apply_filter,
|
|
153
|
+
width=300,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
dpg.add_separator()
|
|
157
|
+
|
|
158
|
+
with dpg.group(horizontal=True):
|
|
159
|
+
with dpg.child_window(
|
|
160
|
+
tag="tree_container", width=900, resizable_x=True
|
|
161
|
+
):
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
with dpg.child_window(tag="error_feed_container"):
|
|
165
|
+
dpg.add_text("Error & Warning Feed", color=(255, 100, 100))
|
|
166
|
+
dpg.add_separator()
|
|
167
|
+
|
|
168
|
+
def setup_extras(self) -> None:
|
|
169
|
+
with dpg.window(
|
|
170
|
+
label="About RobotLiveTrace",
|
|
171
|
+
tag="about_window",
|
|
172
|
+
width=400,
|
|
173
|
+
pos=(200, 200),
|
|
174
|
+
show=False,
|
|
175
|
+
no_resize=True,
|
|
176
|
+
):
|
|
177
|
+
dpg.add_text("RobotLiveTrace", color=(100, 200, 255), wrap=380)
|
|
178
|
+
dpg.add_text(
|
|
179
|
+
"A real-time execution trace viewer for Robot Framework, built with DearPyGui.",
|
|
180
|
+
wrap=380,
|
|
181
|
+
)
|
|
182
|
+
dpg.add_separator()
|
|
183
|
+
dpg.add_text(f"Version: {VERSION}")
|
|
184
|
+
dpg.add_separator()
|
|
185
|
+
dpg.add_text("Authors:", color=(100, 200, 255))
|
|
186
|
+
dpg.add_text(f"{AUTHOR}")
|
|
187
|
+
|
|
188
|
+
#
|
|
189
|
+
# Callbacks
|
|
190
|
+
#
|
|
191
|
+
def toggle_pause(self, sender: int, app_data: Any):
|
|
192
|
+
if self.pause_event is None:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
if app_data:
|
|
196
|
+
self.pause_event.clear() # Block the listener process
|
|
197
|
+
else:
|
|
198
|
+
self.pause_event.set() # Resume the listener process
|
|
199
|
+
|
|
200
|
+
def expand_all(self, sender: int, app_data: Any):
|
|
201
|
+
for node_id in self.all_nodes:
|
|
202
|
+
dpg.set_value(node_id, True)
|
|
203
|
+
|
|
204
|
+
def collapse_all(self, sender: int, app_data: Any):
|
|
205
|
+
for node_id in self.all_nodes:
|
|
206
|
+
dpg.set_value(node_id, False)
|
|
207
|
+
|
|
208
|
+
def apply_filter(self, sender: int | None = None, app_data: Any | None = None):
|
|
209
|
+
search_string = dpg.get_value("filter_input").lower()
|
|
210
|
+
|
|
211
|
+
if not search_string:
|
|
212
|
+
for node_id in self.all_nodes:
|
|
213
|
+
dpg.show_item(node_id)
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
matches = set()
|
|
217
|
+
for node_id, txt in self.node_search_text.items():
|
|
218
|
+
if search_string in txt:
|
|
219
|
+
matches.add(node_id)
|
|
220
|
+
|
|
221
|
+
visible_set = set(matches)
|
|
222
|
+
for node_id in matches:
|
|
223
|
+
curr = self.node_parents.get(node_id)
|
|
224
|
+
while curr and curr not in ("tree_container", "main_window"):
|
|
225
|
+
visible_set.add(curr)
|
|
226
|
+
dpg.set_value(
|
|
227
|
+
curr, True
|
|
228
|
+
) # Auto-expand the parent so we can see the match
|
|
229
|
+
curr = self.node_parents.get(curr)
|
|
230
|
+
|
|
231
|
+
for node_id in self.all_nodes:
|
|
232
|
+
if node_id in visible_set:
|
|
233
|
+
dpg.show_item(node_id)
|
|
234
|
+
else:
|
|
235
|
+
dpg.hide_item(node_id)
|
|
236
|
+
|
|
237
|
+
def jump_to_node(self, sender: int, app_data: Any, user_data: Any):
|
|
238
|
+
target_node = user_data
|
|
239
|
+
|
|
240
|
+
if target_node is None or not dpg.does_item_exist(target_node):
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
if app_data:
|
|
244
|
+
dpg.bind_item_theme(target_node, self.themes["highlight"])
|
|
245
|
+
|
|
246
|
+
curr = self.node_parents.get(target_node)
|
|
247
|
+
while curr and curr not in ("tree_container", "main_window"):
|
|
248
|
+
dpg.set_value(curr, True)
|
|
249
|
+
curr = self.node_parents.get(curr)
|
|
250
|
+
|
|
251
|
+
dpg.focus_item(target_node)
|
|
252
|
+
else:
|
|
253
|
+
original_theme = self.node_themes.get(target_node, self.themes["running"])
|
|
254
|
+
dpg.bind_item_theme(target_node, original_theme)
|
|
255
|
+
|
|
256
|
+
def change_font_size(self, sender, app_data):
|
|
257
|
+
if app_data is None:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
for size, font_id in self.font_sizes.items():
|
|
261
|
+
if str(size) == app_data:
|
|
262
|
+
dpg.bind_font(font_id)
|
|
263
|
+
dpg.bind_item_font("main_window", font_id)
|
|
264
|
+
dpg.bind_item_font("about_window", font_id)
|
|
265
|
+
break
|
|
266
|
+
|
|
267
|
+
def show_about(self, sender: int, app_data: Any):
|
|
268
|
+
dpg.show_item("about_window")
|
|
269
|
+
|
|
270
|
+
#
|
|
271
|
+
# Message handling
|
|
272
|
+
#
|
|
273
|
+
|
|
274
|
+
def handle_start(
|
|
275
|
+
self,
|
|
276
|
+
action: str,
|
|
277
|
+
data: dict[str, Any],
|
|
278
|
+
) -> None:
|
|
279
|
+
name = data.get("name", "Unknown")
|
|
280
|
+
args = data.get("args", [])
|
|
281
|
+
|
|
282
|
+
prefix = self.prefixes.get(action, "[*]")
|
|
283
|
+
parent_id = self.active_stack[-1]
|
|
284
|
+
base_label = f"{prefix} {name}"
|
|
285
|
+
|
|
286
|
+
node_id = dpg.add_tree_node(
|
|
287
|
+
label=base_label, parent=parent_id, default_open=True
|
|
288
|
+
)
|
|
289
|
+
dpg.bind_item_theme(node_id, self.themes["running"])
|
|
290
|
+
self.node_themes[node_id] = self.themes["running"]
|
|
291
|
+
|
|
292
|
+
self.all_nodes.append(node_id)
|
|
293
|
+
self.node_parents[node_id] = parent_id
|
|
294
|
+
self.node_search_text[node_id] = f"{name} {' '.join(args)}".lower()
|
|
295
|
+
self.needs_filter_update = True
|
|
296
|
+
self.items_added_this_frame = True
|
|
297
|
+
|
|
298
|
+
with dpg.tooltip(node_id):
|
|
299
|
+
dpg.add_text(
|
|
300
|
+
f"Type: {action.replace('start_', '').replace('_', ' ').title()}"
|
|
301
|
+
)
|
|
302
|
+
dpg.add_text(f"Name: {name}")
|
|
303
|
+
|
|
304
|
+
if args:
|
|
305
|
+
dpg.add_text(f"Args: {', '.join(args)}")
|
|
306
|
+
|
|
307
|
+
status_id = dpg.add_text("Status: Running...")
|
|
308
|
+
time_id = dpg.add_text("Elapsed Time: Running...")
|
|
309
|
+
dpg.add_separator()
|
|
310
|
+
|
|
311
|
+
self.active_stack.append(node_id)
|
|
312
|
+
|
|
313
|
+
self.node_data[node_id] = {
|
|
314
|
+
"label": base_label,
|
|
315
|
+
"status_id": status_id,
|
|
316
|
+
"time_id": time_id,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
def handle_log(self, result: dict[str, Any]) -> None:
|
|
320
|
+
if len(self.active_stack) <= 1:
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
parent_id = self.active_stack[-1]
|
|
324
|
+
|
|
325
|
+
level = result.get("level", "<UNKNOWN>")
|
|
326
|
+
full_txt = result.get("message", "<MISSING LOG MESSAGE>")
|
|
327
|
+
timestamp = result.get("timestamp", "<NO TIMESTAMP>")
|
|
328
|
+
|
|
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
|
+
clean_txt = full_txt.replace("\n", " ")
|
|
332
|
+
display_txt = clean_txt if len(clean_txt) < 80 else clean_txt[:77] + "..."
|
|
333
|
+
|
|
334
|
+
node_id = dpg.add_tree_node(
|
|
335
|
+
label=f"[{level}] {display_txt}",
|
|
336
|
+
parent=parent_id,
|
|
337
|
+
leaf=True,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if level in ["WARN", "WARNING"]:
|
|
341
|
+
dpg.bind_item_theme(node_id, self.themes["log_warn"])
|
|
342
|
+
self.node_themes[node_id] = self.themes["log_warn"]
|
|
343
|
+
elif level in ["FAIL", "ERROR"]:
|
|
344
|
+
dpg.bind_item_theme(node_id, self.themes["fail"])
|
|
345
|
+
self.node_themes[node_id] = self.themes["fail"]
|
|
346
|
+
else:
|
|
347
|
+
dpg.bind_item_theme(node_id, self.themes["log_info"])
|
|
348
|
+
self.node_themes[node_id] = self.themes["log_info"]
|
|
349
|
+
|
|
350
|
+
self.all_nodes.append(node_id)
|
|
351
|
+
self.node_parents[node_id] = parent_id
|
|
352
|
+
self.node_search_text[node_id] = f"{level} {full_txt}".lower()
|
|
353
|
+
self.needs_filter_update = True
|
|
354
|
+
self.items_added_this_frame = True
|
|
355
|
+
|
|
356
|
+
with dpg.tooltip(node_id):
|
|
357
|
+
dpg.add_text(f"Level: {level}")
|
|
358
|
+
dpg.add_text(f"Time: {timestamp}")
|
|
359
|
+
dpg.add_text(full_txt, wrap=600)
|
|
360
|
+
dpg.add_separator()
|
|
361
|
+
|
|
362
|
+
# Error feed
|
|
363
|
+
if level in ["FAIL", "ERROR", "WARN", "WARNING"]:
|
|
364
|
+
parent_data = self.node_data.get(parent_id, {"label": "Unknown context"})
|
|
365
|
+
context_name = parent_data["label"]
|
|
366
|
+
|
|
367
|
+
error_item = dpg.add_selectable(
|
|
368
|
+
label=f"[{level}] {context_name}",
|
|
369
|
+
callback=self.jump_to_node,
|
|
370
|
+
user_data=node_id,
|
|
371
|
+
parent="error_feed_container",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
if level in ["FAIL", "ERROR"]:
|
|
375
|
+
dpg.bind_item_theme(error_item, self.themes["fail"])
|
|
376
|
+
else:
|
|
377
|
+
dpg.bind_item_theme(error_item, self.themes["log_warn"])
|
|
378
|
+
|
|
379
|
+
dpg.add_text(clean_txt, wrap=440, parent="error_feed_container")
|
|
380
|
+
dpg.add_separator(parent="error_feed_container")
|
|
381
|
+
|
|
382
|
+
def handle_end(self, action: str, result: dict[str, Any]) -> None:
|
|
383
|
+
if len(self.active_stack) <= 1:
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
node_id = self.active_stack.pop()
|
|
387
|
+
node = self.node_data.get(node_id)
|
|
388
|
+
|
|
389
|
+
if node is None or not dpg.does_item_exist(node_id):
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
status = result.get("status", "UNKNOWN")
|
|
393
|
+
elapsed = result.get("elapsed_time", 0.0)
|
|
394
|
+
|
|
395
|
+
if status == "PASS":
|
|
396
|
+
dpg.bind_item_theme(node_id, self.themes["pass"])
|
|
397
|
+
self.node_themes[node_id] = self.themes["pass"]
|
|
398
|
+
|
|
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"]
|
|
404
|
+
|
|
405
|
+
dpg.set_value(node["status_id"], f"Status: {status}")
|
|
406
|
+
dpg.set_value(node["time_id"], f"Elapsed Time: {elapsed} s")
|
|
407
|
+
|
|
408
|
+
def run(self) -> None:
|
|
409
|
+
dpg.create_context()
|
|
410
|
+
dpg.create_viewport(title="RobotLiveTrace", width=1400, height=800)
|
|
411
|
+
dpg.setup_dearpygui()
|
|
412
|
+
|
|
413
|
+
self.setup_icons()
|
|
414
|
+
self.setup_fonts()
|
|
415
|
+
self.setup_themes()
|
|
416
|
+
self.setup_layout()
|
|
417
|
+
self.setup_extras()
|
|
418
|
+
|
|
419
|
+
dpg.set_primary_window("main_window", True)
|
|
420
|
+
default_font = self.font_sizes[self.default_font_size]
|
|
421
|
+
dpg.bind_item_font("main_window", default_font)
|
|
422
|
+
dpg.bind_item_font("about_window", default_font)
|
|
423
|
+
|
|
424
|
+
dpg.show_viewport()
|
|
425
|
+
|
|
426
|
+
while dpg.is_dearpygui_running():
|
|
427
|
+
self.needs_filter_update = False
|
|
428
|
+
self.items_added_this_frame = False
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
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)
|
|
446
|
+
except queue.Empty:
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
if self.needs_filter_update and dpg.get_value("filter_input"):
|
|
450
|
+
self.apply_filter()
|
|
451
|
+
|
|
452
|
+
if self.items_added_this_frame and dpg.get_value("auto_scroll_toggle"):
|
|
453
|
+
dpg.set_y_scroll("tree_container", 999999)
|
|
454
|
+
dpg.set_y_scroll("error_feed_container", 999999)
|
|
455
|
+
|
|
456
|
+
dpg.render_dearpygui_frame()
|
|
457
|
+
|
|
458
|
+
if self.pause_event is not None:
|
|
459
|
+
self.pause_event.set() # Ensure listener isn't stuck waiting if GUI is closed
|
|
460
|
+
|
|
461
|
+
dpg.destroy_context()
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def run_gui(q: queue.Queue[Message], pause_event: Event | None = None):
|
|
465
|
+
gui = GUI(q, pause_event)
|
|
466
|
+
gui.run()
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
if __name__ == "__main__":
|
|
470
|
+
run_gui(queue.Queue())
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|