robotframework-pabot 5.1.0__py3-none-any.whl → 5.2.0rc1__py3-none-any.whl
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.
- pabot/ProcessManager.py +415 -0
- pabot/__init__.py +1 -1
- pabot/arguments.py +83 -26
- pabot/pabot.py +603 -218
- pabot/result_merger.py +13 -3
- pabot/robotremoteserver.py +27 -7
- pabot/writer.py +235 -0
- {robotframework_pabot-5.1.0.dist-info → robotframework_pabot-5.2.0rc1.dist-info}/METADATA +70 -21
- robotframework_pabot-5.2.0rc1.dist-info/RECORD +23 -0
- {robotframework_pabot-5.1.0.dist-info → robotframework_pabot-5.2.0rc1.dist-info}/WHEEL +1 -1
- robotframework_pabot-5.1.0.dist-info/RECORD +0 -22
- robotframework_pabot-5.1.0.dist-info/licenses/LICENSE.txt +0 -202
- {robotframework_pabot-5.1.0.dist-info → robotframework_pabot-5.2.0rc1.dist-info}/entry_points.txt +0 -0
- {robotframework_pabot-5.1.0.dist-info → robotframework_pabot-5.2.0rc1.dist-info}/top_level.txt +0 -0
pabot/result_merger.py
CHANGED
|
@@ -32,6 +32,7 @@ except ImportError:
|
|
|
32
32
|
from robot.result.testsuite import TestSuite
|
|
33
33
|
|
|
34
34
|
from robot.model import SuiteVisitor
|
|
35
|
+
from .writer import get_writer
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
class ResultMerger(SuiteVisitor):
|
|
@@ -45,6 +46,7 @@ class ResultMerger(SuiteVisitor):
|
|
|
45
46
|
self._out_dir = out_dir
|
|
46
47
|
self.legacy_output = legacy_output
|
|
47
48
|
self.timestamp_id = timestamp_id
|
|
49
|
+
self.writer = get_writer()
|
|
48
50
|
|
|
49
51
|
self._patterns = []
|
|
50
52
|
regexp_template = (
|
|
@@ -62,7 +64,10 @@ class ResultMerger(SuiteVisitor):
|
|
|
62
64
|
if self.errors != merged.errors:
|
|
63
65
|
self.errors.add(merged.errors)
|
|
64
66
|
except:
|
|
65
|
-
|
|
67
|
+
if self.writer:
|
|
68
|
+
self.writer.write("Error while merging result %s" % merged.source, level="error")
|
|
69
|
+
else:
|
|
70
|
+
print("Error while merging result %s" % merged.source)
|
|
66
71
|
raise
|
|
67
72
|
|
|
68
73
|
def _set_prefix(self, source):
|
|
@@ -214,12 +219,17 @@ def prefix(source, timestamp_id):
|
|
|
214
219
|
|
|
215
220
|
def group_by_root(results, critical_tags, non_critical_tags, invalid_xml_callback):
|
|
216
221
|
groups = {}
|
|
222
|
+
writer = get_writer()
|
|
217
223
|
for src in results:
|
|
218
224
|
try:
|
|
219
225
|
res = ExecutionResult(src)
|
|
220
226
|
except DataError as err:
|
|
221
|
-
|
|
222
|
-
|
|
227
|
+
if writer:
|
|
228
|
+
writer.write(err.message, level="error")
|
|
229
|
+
writer.write("Skipping '%s' from final result" % src, level="warning")
|
|
230
|
+
else:
|
|
231
|
+
print(err.message)
|
|
232
|
+
print("Skipping '%s' from final result" % src)
|
|
223
233
|
invalid_xml_callback()
|
|
224
234
|
continue
|
|
225
235
|
if ROBOT_VERSION < "4.0":
|
pabot/robotremoteserver.py
CHANGED
|
@@ -452,20 +452,28 @@ class StandardStreamInterceptor(object):
|
|
|
452
452
|
self.output = ""
|
|
453
453
|
self.origout = sys.stdout
|
|
454
454
|
self.origerr = sys.stderr
|
|
455
|
-
|
|
456
|
-
|
|
455
|
+
self.stdout_capture = StringIO()
|
|
456
|
+
self.stderr_capture = StringIO()
|
|
457
|
+
sys.stdout = self.stdout_capture
|
|
458
|
+
sys.stderr = self.stderr_capture
|
|
457
459
|
|
|
458
460
|
def __enter__(self):
|
|
459
461
|
return self
|
|
460
462
|
|
|
461
463
|
def __exit__(self, *exc_info):
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
464
|
+
# Safely get output from captured streams, handling case where streams were restored
|
|
465
|
+
stdout = self._safe_getvalue(sys.stdout, self.stdout_capture)
|
|
466
|
+
stderr = self._safe_getvalue(sys.stderr, self.stderr_capture)
|
|
467
|
+
|
|
468
|
+
# Close only our StringIO objects
|
|
469
|
+
for stream in [self.stdout_capture, self.stderr_capture]:
|
|
470
|
+
if hasattr(stream, 'close'):
|
|
471
|
+
stream.close()
|
|
472
|
+
|
|
473
|
+
# Restore original streams
|
|
465
474
|
sys.stdout = self.origout
|
|
466
475
|
sys.stderr = self.origerr
|
|
467
|
-
|
|
468
|
-
stream.close()
|
|
476
|
+
|
|
469
477
|
if stdout and stderr:
|
|
470
478
|
if not stderr.startswith(
|
|
471
479
|
("*TRACE*", "*DEBUG*", "*INFO*", "*HTML*", "*WARN*", "*ERROR*")
|
|
@@ -474,6 +482,18 @@ class StandardStreamInterceptor(object):
|
|
|
474
482
|
if not stdout.endswith("\n"):
|
|
475
483
|
stdout += "\n"
|
|
476
484
|
self.output = stdout + stderr
|
|
485
|
+
|
|
486
|
+
def _safe_getvalue(self, current_stream, original_capture):
|
|
487
|
+
"""Safely get value from stream, handling case where stream was restored."""
|
|
488
|
+
# If current stream is still our StringIO, get value from it
|
|
489
|
+
if hasattr(current_stream, 'getvalue') and current_stream is original_capture:
|
|
490
|
+
return current_stream.getvalue()
|
|
491
|
+
# If current stream was restored but our capture still has data
|
|
492
|
+
print("*WARN* Stream capture was interrupted", file=sys.stderr)
|
|
493
|
+
if hasattr(original_capture, 'getvalue'):
|
|
494
|
+
return original_capture.getvalue()
|
|
495
|
+
else:
|
|
496
|
+
return ""
|
|
477
497
|
|
|
478
498
|
|
|
479
499
|
class KeywordResult(object):
|
pabot/writer.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import queue
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
class Color:
|
|
8
|
+
RED = "\033[91m"
|
|
9
|
+
GREEN = "\033[92m"
|
|
10
|
+
YELLOW = "\033[93m"
|
|
11
|
+
ENDC = "\033[0m"
|
|
12
|
+
SUPPORTED_OSES = {"posix"} # Only Unix terminals support ANSI colors
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DottedConsole:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self._on_line = False
|
|
18
|
+
|
|
19
|
+
def dot(self, char):
|
|
20
|
+
print(char, end="", flush=True)
|
|
21
|
+
self._on_line = True
|
|
22
|
+
|
|
23
|
+
def newline(self):
|
|
24
|
+
if self._on_line:
|
|
25
|
+
print()
|
|
26
|
+
self._on_line = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BufferingWriter:
|
|
30
|
+
"""
|
|
31
|
+
Buffers partial writes until a newline is encountered.
|
|
32
|
+
Useful for handling output that comes in fragments (e.g., from stderr).
|
|
33
|
+
"""
|
|
34
|
+
def __init__(self, writer, level="info", original_stderr_name=None):
|
|
35
|
+
self._writer = writer
|
|
36
|
+
self._level = level
|
|
37
|
+
self.original_stderr_name = original_stderr_name
|
|
38
|
+
self._buffer = ""
|
|
39
|
+
self._lock = threading.Lock()
|
|
40
|
+
|
|
41
|
+
def write(self, msg):
|
|
42
|
+
with self._lock:
|
|
43
|
+
if not msg:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
self._buffer += msg
|
|
47
|
+
|
|
48
|
+
# Check if buffer contains newline(s)
|
|
49
|
+
while "\n" in self._buffer:
|
|
50
|
+
line, self._buffer = self._buffer.split("\n", 1)
|
|
51
|
+
if line: # Only write non-empty lines
|
|
52
|
+
if self.original_stderr_name:
|
|
53
|
+
line = f"From {self.original_stderr_name}: {line}"
|
|
54
|
+
self._writer.write(line, level=self._level)
|
|
55
|
+
|
|
56
|
+
# If buffer ends with partial content (no newline), keep it buffered
|
|
57
|
+
|
|
58
|
+
def flush(self):
|
|
59
|
+
with self._lock:
|
|
60
|
+
if self._buffer:
|
|
61
|
+
self._writer.write(self._buffer, level=self._level)
|
|
62
|
+
self._buffer = ""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ThreadSafeWriter:
|
|
66
|
+
def __init__(self, writer, level="info"):
|
|
67
|
+
self._writer = writer
|
|
68
|
+
self._lock = threading.Lock()
|
|
69
|
+
self._level = level # Default level for this writer instance
|
|
70
|
+
|
|
71
|
+
def write(self, msg, level=None):
|
|
72
|
+
# Use provided level or fall back to instance default
|
|
73
|
+
msg_level = level if level is not None else self._level
|
|
74
|
+
with self._lock:
|
|
75
|
+
self._writer.write(msg, level=msg_level)
|
|
76
|
+
|
|
77
|
+
def flush(self):
|
|
78
|
+
with self._lock:
|
|
79
|
+
self._writer.flush()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class MessageWriter:
|
|
83
|
+
def __init__(self, log_file=None, console_type="verbose"):
|
|
84
|
+
self.queue = queue.Queue()
|
|
85
|
+
self.log_file = log_file
|
|
86
|
+
self.console_type = console_type
|
|
87
|
+
self.console = DottedConsole() if console_type == "dotted" else None
|
|
88
|
+
if log_file:
|
|
89
|
+
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
|
90
|
+
self._stop_event = threading.Event()
|
|
91
|
+
self.thread = threading.Thread(target=self._writer)
|
|
92
|
+
self.thread.daemon = True
|
|
93
|
+
self.thread.start()
|
|
94
|
+
|
|
95
|
+
def _is_output_coloring_supported(self):
|
|
96
|
+
return sys.stdout.isatty() and os.name in Color.SUPPORTED_OSES
|
|
97
|
+
|
|
98
|
+
def _wrap_with(self, color, message):
|
|
99
|
+
if self._is_output_coloring_supported() and color:
|
|
100
|
+
return f"{color}{message}{Color.ENDC}"
|
|
101
|
+
return message
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _should_print_to_console(self, console_type=None, level="debug"):
|
|
105
|
+
"""
|
|
106
|
+
Determine if message should be printed to console based on console_type and level.
|
|
107
|
+
Always write to log file.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
console_type: The console type mode. If None, uses instance default.
|
|
111
|
+
level: Message level (debug, info, warning, error, and spesial results infos: info_passed, info_failed, info_skipped, info_ignored). Defaults to debug.
|
|
112
|
+
"""
|
|
113
|
+
ct = console_type if console_type is not None else self.console_type
|
|
114
|
+
|
|
115
|
+
# Map levels to importance: debug < info_passed/info_ignored/info_skipped < info_failed < info < warning < error
|
|
116
|
+
level_map = {"debug": 0, "info_passed": 1, "info_ignored": 1, "info_skipped": 1, "info_failed": 2, "info": 3, "warning": 4, "error": 5}
|
|
117
|
+
message_level = level_map.get(level, 0) # default to debug
|
|
118
|
+
|
|
119
|
+
if ct == "none":
|
|
120
|
+
return False
|
|
121
|
+
elif ct == "quiet":
|
|
122
|
+
# In quiet mode, show only warning and error level messages
|
|
123
|
+
return message_level >= 3
|
|
124
|
+
elif ct == "dotted":
|
|
125
|
+
# In dotted mode, show test result indicators (info_passed/failed/skipped/ignored) and warnings/errors
|
|
126
|
+
return message_level >= 1
|
|
127
|
+
# verbose mode - print everything
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
def _writer(self):
|
|
131
|
+
while not self._stop_event.is_set():
|
|
132
|
+
try:
|
|
133
|
+
message, color, level = self.queue.get(timeout=0.1)
|
|
134
|
+
except queue.Empty:
|
|
135
|
+
continue
|
|
136
|
+
if message is None:
|
|
137
|
+
self.queue.task_done()
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
message = message.rstrip("\n")
|
|
141
|
+
# Always write to log file
|
|
142
|
+
if self.log_file:
|
|
143
|
+
with open(self.log_file, "a", encoding="utf-8") as f:
|
|
144
|
+
lvl_msg = f"[{level.split('_')[0].upper()}]".ljust(9)
|
|
145
|
+
f.write(f"{lvl_msg} {message}\n")
|
|
146
|
+
|
|
147
|
+
# Print to console based on level
|
|
148
|
+
if self._should_print_to_console(level=level):
|
|
149
|
+
if self.console is not None:
|
|
150
|
+
# In dotted mode, only print single character messages directly
|
|
151
|
+
if level == "info_passed":
|
|
152
|
+
self.console.dot(self._wrap_with(color, "."))
|
|
153
|
+
elif level == "info_failed":
|
|
154
|
+
self.console.dot(self._wrap_with(color, "F"))
|
|
155
|
+
elif level in ("info_ignored", "info_skipped"):
|
|
156
|
+
self.console.dot(self._wrap_with(color, "s"))
|
|
157
|
+
else:
|
|
158
|
+
self.console.newline()
|
|
159
|
+
print(self._wrap_with(color, message), flush=True)
|
|
160
|
+
else:
|
|
161
|
+
print(self._wrap_with(color, message), flush=True)
|
|
162
|
+
|
|
163
|
+
self.queue.task_done()
|
|
164
|
+
|
|
165
|
+
def write(self, message, color=None, level="info"):
|
|
166
|
+
self.queue.put((f"{message}", color, level))
|
|
167
|
+
|
|
168
|
+
def flush(self, timeout=5):
|
|
169
|
+
"""
|
|
170
|
+
Wait until all queued messages have been written.
|
|
171
|
+
|
|
172
|
+
:param timeout: Optional timeout in seconds. If None, wait indefinitely.
|
|
173
|
+
:return: True if queue drained before timeout (or no timeout), False if timed out.
|
|
174
|
+
"""
|
|
175
|
+
start = time.time()
|
|
176
|
+
try:
|
|
177
|
+
# Loop until Queue reports no unfinished tasks
|
|
178
|
+
while True:
|
|
179
|
+
# If writer thread died, break to avoid infinite loop
|
|
180
|
+
if not self.thread.is_alive():
|
|
181
|
+
# Give one last moment for potential in-flight task_done()
|
|
182
|
+
time.sleep(0.01)
|
|
183
|
+
# If still unfinished, we can't do more
|
|
184
|
+
return getattr(self.queue, "unfinished_tasks", 0) == 0
|
|
185
|
+
|
|
186
|
+
unfinished = getattr(self.queue, "unfinished_tasks", None)
|
|
187
|
+
if unfinished is None:
|
|
188
|
+
# Fallback: call join once and return
|
|
189
|
+
try:
|
|
190
|
+
self.queue.join()
|
|
191
|
+
return True
|
|
192
|
+
except Exception:
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
if unfinished == 0:
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
if timeout is not None and (time.time() - start) > timeout:
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
time.sleep(0.05)
|
|
202
|
+
except KeyboardInterrupt:
|
|
203
|
+
# Allow tests/cli to interrupt flushing
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
def stop(self):
|
|
207
|
+
"""
|
|
208
|
+
Gracefully stop the writer thread and flush remaining messages.
|
|
209
|
+
"""
|
|
210
|
+
self.flush()
|
|
211
|
+
self._stop_event.set()
|
|
212
|
+
self.queue.put((None, None, None)) # sentinel to break thread loop
|
|
213
|
+
self.thread.join(timeout=1.0)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
_writer_instance = None
|
|
217
|
+
|
|
218
|
+
def get_writer(log_dir=None, console_type="verbose"):
|
|
219
|
+
global _writer_instance
|
|
220
|
+
if _writer_instance is None:
|
|
221
|
+
if log_dir:
|
|
222
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
223
|
+
log_file = os.path.join(log_dir or ".", "pabot_manager.log")
|
|
224
|
+
_writer_instance = MessageWriter(log_file=log_file, console_type=console_type)
|
|
225
|
+
return _writer_instance
|
|
226
|
+
|
|
227
|
+
def get_stdout_writer(log_dir=None, console_type="verbose"):
|
|
228
|
+
"""Get a writer configured for stdout with 'info' level"""
|
|
229
|
+
return ThreadSafeWriter(get_writer(log_dir, console_type), level="info")
|
|
230
|
+
|
|
231
|
+
def get_stderr_writer(log_dir=None, console_type="verbose", original_stderr_name: str = None):
|
|
232
|
+
"""Get a writer configured for stderr with 'error' level, buffered to handle partial writes"""
|
|
233
|
+
# Use BufferingWriter to combine fragments that come without newlines
|
|
234
|
+
buffering_writer = BufferingWriter(get_writer(log_dir, console_type), level="error", original_stderr_name=original_stderr_name)
|
|
235
|
+
return buffering_writer
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: robotframework-pabot
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.0rc1
|
|
4
4
|
Summary: Parallel test runner for Robot Framework
|
|
5
5
|
Home-page: https://pabot.org
|
|
6
6
|
Download-URL: https://pypi.python.org/pypi/robotframework-pabot
|
|
@@ -12,17 +12,14 @@ Classifier: Intended Audience :: Developers
|
|
|
12
12
|
Classifier: Natural Language :: English
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
14
|
Classifier: Topic :: Software Development :: Testing
|
|
15
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
15
|
Classifier: Development Status :: 5 - Production/Stable
|
|
17
16
|
Classifier: Framework :: Robot Framework
|
|
18
17
|
Requires-Python: >=3.6
|
|
19
18
|
Description-Content-Type: text/markdown
|
|
20
|
-
License-File: LICENSE.txt
|
|
21
19
|
Requires-Dist: robotframework>=3.2
|
|
22
20
|
Requires-Dist: robotframework-stacktrace>=0.4.1
|
|
23
21
|
Requires-Dist: natsort>=8.2.0
|
|
24
22
|
Dynamic: download-url
|
|
25
|
-
Dynamic: license-file
|
|
26
23
|
|
|
27
24
|
# Pabot
|
|
28
25
|
|
|
@@ -47,7 +44,7 @@ A parallel executor for [Robot Framework](http://www.robotframework.org) tests.
|
|
|
47
44
|
- [Contributing](#contributing-to-the-project)
|
|
48
45
|
- [Command-line options](#command-line-options)
|
|
49
46
|
- [PabotLib](#pabotlib)
|
|
50
|
-
- [Controlling execution order](#controlling-execution-order-and-level-of-parallelism)
|
|
47
|
+
- [Controlling execution order, mode and level of parallelism](#controlling-execution-order-mode-and-level-of-parallelism)
|
|
51
48
|
- [Programmatic use](#programmatic-use)
|
|
52
49
|
- [Global variables](#global-variables)
|
|
53
50
|
- [Output Files Generated by Pabot](#output-files-generated-by-pabot)
|
|
@@ -98,6 +95,12 @@ There are several ways you can help in improving this tool:
|
|
|
98
95
|
- Report an issue or an improvement idea to the [issue tracker](https://github.com/mkorpela/pabot/issues)
|
|
99
96
|
- Contribute by programming and making a pull request (easiest way is to work on an issue from the issue tracker)
|
|
100
97
|
|
|
98
|
+
Before contributing, please read our detailed contributing guidelines:
|
|
99
|
+
|
|
100
|
+
- [Contributing Guide](CONTRIBUTING.md)
|
|
101
|
+
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
|
102
|
+
- [Security Policy](SECURITY.md)
|
|
103
|
+
|
|
101
104
|
## Command-line options
|
|
102
105
|
<!-- NOTE:
|
|
103
106
|
The sections inside these docstring markers are also used in Pabot's --help output.
|
|
@@ -114,17 +117,19 @@ pabot [--verbose|--testlevelsplit|--command .. --end-command|
|
|
|
114
117
|
--processtimeout num|
|
|
115
118
|
--shard i/n|
|
|
116
119
|
--artifacts extensions|--artifactsinsubfolders|
|
|
117
|
-
--resourcefile file|--argumentfile[num] file|--suitesfrom file
|
|
120
|
+
--resourcefile file|--argumentfile[num] file|--suitesfrom file
|
|
121
|
+
--ordering file [static|dynamic] [skip|run_all]|
|
|
118
122
|
--chunk|
|
|
119
123
|
--pabotprerunmodifier modifier|
|
|
120
124
|
--no-rebot|
|
|
125
|
+
--pabotconsole [verbose|dotted|quiet|none]|
|
|
121
126
|
--help|--version]
|
|
122
127
|
[robot options] [path ...]
|
|
123
128
|
```
|
|
124
129
|
|
|
125
130
|
PabotLib remote server is started by default to enable locking and resource distribution between parallel test executions.
|
|
126
131
|
|
|
127
|
-
Supports all [Robot Framework command line options](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#
|
|
132
|
+
Supports all [Robot Framework command line options](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#command-line-options) and also following pabot options:
|
|
128
133
|
|
|
129
134
|
**--verbose**
|
|
130
135
|
More output from the parallel execution.
|
|
@@ -202,8 +207,10 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
|
|
|
202
207
|
Optionally read suites from output.xml file. Failed suites will run first and longer running ones will be executed
|
|
203
208
|
before shorter ones.
|
|
204
209
|
|
|
205
|
-
**--ordering [
|
|
206
|
-
Optionally give execution order from a file.
|
|
210
|
+
**--ordering [FILEPATH] [MODE] [FAILURE POLICY]**
|
|
211
|
+
Optionally give execution order from a file. See README.md section: [Controlling execution order, mode and level of parallelism](#controlling-execution-order-mode-and-level-of-parallelism)
|
|
212
|
+
- MODE (optional): [ static (default) | dynamic ]
|
|
213
|
+
- FAILURE POLICY (optional, only in dynamic mode): [ skip | run_all (default) ]
|
|
207
214
|
|
|
208
215
|
**--chunk**
|
|
209
216
|
Optionally chunk tests to PROCESSES number of robot runs. This can save time because all the suites will share the same
|
|
@@ -220,6 +227,28 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
|
|
|
220
227
|
for scenarios where Rebot should be run later due to large log files, ensuring better memory and resource availability.
|
|
221
228
|
Subprocess results are stored in the pabot_results folder.
|
|
222
229
|
|
|
230
|
+
**--pabotconsole [MODE]**
|
|
231
|
+
The --pabotconsole option controls how much output is printed to the console.
|
|
232
|
+
Note that all Pabot’s own messages are always logged to pabot_manager.log, regardless of the selected console mode.
|
|
233
|
+
|
|
234
|
+
The available options are:
|
|
235
|
+
- verbose (default):
|
|
236
|
+
Prints all messages to the console, corresponding closely to what is written to the log file.
|
|
237
|
+
- dotted:
|
|
238
|
+
Prints important messages, warnings, and errors to the console, along with execution progress using the following notation:
|
|
239
|
+
|
|
240
|
+
- PASS = .
|
|
241
|
+
- FAIL = F
|
|
242
|
+
- SKIP = s
|
|
243
|
+
|
|
244
|
+
Note that each Robot Framework process is represented by a single character.
|
|
245
|
+
Depending on the execution parameters, individual tests may not have their own status character;
|
|
246
|
+
instead, the status may represent an entire suite or a group of tests.
|
|
247
|
+
- quiet:
|
|
248
|
+
Similar to dotted, but suppresses execution progress output.
|
|
249
|
+
- none:
|
|
250
|
+
Produces no console output at all.
|
|
251
|
+
|
|
223
252
|
**--help**
|
|
224
253
|
Print usage instructions.
|
|
225
254
|
|
|
@@ -245,6 +274,9 @@ These can be helpful when you must ensure that only one of the processes uses so
|
|
|
245
274
|
|
|
246
275
|
PabotLib Docs are located at https://pabot.org/PabotLib.html.
|
|
247
276
|
|
|
277
|
+
Note that PabotLib uses the XML-RPC protocol, which does not support all possible object types.
|
|
278
|
+
These limitations are described in the Robot Framework documentation in chapter [Supported argument and return value types](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#supported-argument-and-return-value-types).
|
|
279
|
+
|
|
248
280
|
### PabotLib example:
|
|
249
281
|
|
|
250
282
|
test.robot
|
|
@@ -290,18 +322,28 @@ pabot call using resources from valueset.dat
|
|
|
290
322
|
|
|
291
323
|
pabot --pabotlib --resourcefile valueset.dat test.robot
|
|
292
324
|
|
|
293
|
-
### Controlling execution order and level of parallelism
|
|
325
|
+
### Controlling execution order, mode, and level of parallelism
|
|
294
326
|
|
|
295
327
|
.pabotsuitenames file contains the list of suites that will be executed.
|
|
296
|
-
|
|
297
|
-
The file
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
328
|
+
This file is created during pabot execution if it does not already exist. It acts as a cache to speed up processing when re-executing the same tests.
|
|
329
|
+
The file can be manually edited partially, but a simpler and more controlled approach is to use:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
--ordering <FILENAME> [static|dynamic] [skip|run_all]
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
- **FILENAME** – path to the ordering file.
|
|
336
|
+
- **mode** – optional execution mode, either `static` (default) or `dynamic`.
|
|
337
|
+
- `static` executes suites in predefined stages.
|
|
338
|
+
- `dynamic` executes tests as soon as all their dependencies are satisfied, allowing more optimal parallel execution.
|
|
339
|
+
- **failure_policy** – determines behavior when dependencies fail. Used only in dynamic mode. Optional:
|
|
340
|
+
- `skip` – dependent tests are skipped if a dependency fails.
|
|
341
|
+
- `run_all` – all tests run regardless of failures (default).
|
|
301
342
|
|
|
302
|
-
|
|
343
|
+
The ordering file syntax is similar to `.pabotsuitenames` but does not include the first 4 hash rows used by pabot. The ordering file defines the **execution order and dependencies** of suites and tests.
|
|
344
|
+
The actual selection of what to run must still be done using options like `--test`, `--suite`, `--include`, or `--exclude`.
|
|
303
345
|
|
|
304
|
-
|
|
346
|
+
#### Controlling execution order
|
|
305
347
|
|
|
306
348
|
There different possibilities to influence the execution:
|
|
307
349
|
|
|
@@ -313,7 +355,7 @@ There different possibilities to influence the execution:
|
|
|
313
355
|
--suite Top Suite
|
|
314
356
|
```
|
|
315
357
|
|
|
316
|
-
* If the base suite name is changing with robot option [```--name / -N```](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#setting-
|
|
358
|
+
* If the base suite name is changing with robot option [```--name / -N```](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#setting-suite-name) you can use either the new or old full test path. For example:
|
|
317
359
|
|
|
318
360
|
```
|
|
319
361
|
--test New Suite Name.Sub Suite.Test 1
|
|
@@ -323,8 +365,14 @@ OR
|
|
|
323
365
|
|
|
324
366
|
* You can add a line with text `#WAIT` to force executor to wait until all previous suites have been executed.
|
|
325
367
|
* You can group suites and tests together to same executor process by adding line `{` before the group and `}` after. Note that `#WAIT` cannot be used inside a group.
|
|
326
|
-
* You can introduce dependencies using the word `#DEPENDS` after a test declaration. This keyword can be used several times if it is necessary to refer to several different tests.
|
|
327
|
-
|
|
368
|
+
* You can introduce dependencies using the word `#DEPENDS` after a test declaration. This keyword can be used several times if it is necessary to refer to several different tests.
|
|
369
|
+
* The ordering algorithm is designed to preserve the exact user-defined order as closely as possible. However, if a test's execution dependencies are not yet satisfied, the test is postponed and moved to the earliest possible stage where all its dependencies are fulfilled.
|
|
370
|
+
* Please take care that in case of circular dependencies an exception will be thrown.
|
|
371
|
+
* Note that each `#WAIT` splits suites into separate execution blocks, and it's not possible to define dependencies for suites or tests that are inside another `#WAIT` block or inside another `{}` braces.
|
|
372
|
+
* Ordering mode effect to execution:
|
|
373
|
+
* **Dynamic mode** will schedule dependent tests as soon as all their dependencies are satisfied. Note that in dynamic mode `#WAIT` is ignored, but you can achieve same results with using only `#DEPENDS` keywords.
|
|
374
|
+
* **Static mode** preserves stage barriers and executes the next stage only after all tests in the previous stage finish.
|
|
375
|
+
* Note: Within a group `{}`, neither execution order nor the `#DEPENDS` keyword currently works. This is due to limitations in Robot Framework, which is invoked within Pabot subprocesses. These limitations may be addressed in a future release of Robot Framework. For now, tests or suites within a group will be executed in the order Robot Framework discovers them — typically in alphabetical order.
|
|
328
376
|
* An example could be:
|
|
329
377
|
|
|
330
378
|
```
|
|
@@ -378,7 +426,7 @@ where order.txt is:
|
|
|
378
426
|
#SLEEP 8
|
|
379
427
|
```
|
|
380
428
|
|
|
381
|
-
|
|
429
|
+
Possible output could be:
|
|
382
430
|
|
|
383
431
|
```
|
|
384
432
|
2025-02-15 19:15:00.408321 [0] [ID:1] SLEEPING 6 SECONDS BEFORE STARTING Data 1.suite C
|
|
@@ -466,6 +514,7 @@ pabot_results/
|
|
|
466
514
|
│ ├── robot_stdout.out
|
|
467
515
|
│ ├── robot_stderr.out
|
|
468
516
|
│ └── artifacts...
|
|
517
|
+
└── pabot_manager.log # Pabot's own main log.
|
|
469
518
|
```
|
|
470
519
|
|
|
471
520
|
Each `PABOTQUEUEINDEX` folder contains as default:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
pabot/ProcessManager.py,sha256=Y4SUOLJ-AmQCc1Y49IYjZS34uqRUnlDt-G2AGymAdHg,13627
|
|
2
|
+
pabot/SharedLibrary.py,sha256=mIipGs3ZhKYEakKprcbrMI4P_Un6qI8gE7086xpHaLY,2552
|
|
3
|
+
pabot/__init__.py,sha256=3MzL6YP6ocsJT8YWQWOMv3XsyC6HvKBZkA07ZtDHD2s,203
|
|
4
|
+
pabot/arguments.py,sha256=IBxkqa63hz5RvdnSZxhLjykkuMs91q8AF30QOzmRl_U,12034
|
|
5
|
+
pabot/clientwrapper.py,sha256=yz7battGs0exysnDeLDWJuzpb2Q-qSjitwxZMO2TlJw,231
|
|
6
|
+
pabot/coordinatorwrapper.py,sha256=nQQ7IowD6c246y8y9nsx0HZbt8vS2XODhPVDjm-lyi0,195
|
|
7
|
+
pabot/execution_items.py,sha256=zDVGW0AAeVbM-scC3Yui2TxvIPx1wYyFKHTPU2BkJkY,13329
|
|
8
|
+
pabot/pabot.py,sha256=rF20VvPfcsPO2_d9FyFGwmoRRqfpoS2EFuATMblOHsc,97383
|
|
9
|
+
pabot/pabotlib.py,sha256=vHbqV7L7mIvDzXBh9UcdULrwhBHNn70EDXF_31MNFO4,22320
|
|
10
|
+
pabot/result_merger.py,sha256=rRRSkQa6bdallwT4w9-jHJXvv7X866C1NwD0jdWdSaE,10177
|
|
11
|
+
pabot/robotremoteserver.py,sha256=BdeIni9Q4LJKVDBUlG2uJ9tiyAjrPXwU_YsPq1THWoo,23296
|
|
12
|
+
pabot/workerwrapper.py,sha256=BdELUVDs5BmEkdNBcYTlnP22Cj0tUpZEunYQMAKyKWU,185
|
|
13
|
+
pabot/writer.py,sha256=tRlPI1jH9NWIYy-VkDsb2odDxZwem7ZgccRBOXZvy4w,8861
|
|
14
|
+
pabot/py3/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
pabot/py3/client.py,sha256=Od9L4vZ0sozMHq_W_ITQHBBt8kAej40DG58wnxmbHGM,1434
|
|
16
|
+
pabot/py3/coordinator.py,sha256=kBshCzA_1QX_f0WNk42QBJyDYSwSlNM-UEBxOReOj6E,2313
|
|
17
|
+
pabot/py3/messages.py,sha256=7mFr4_0x1JHm5sW8TvKq28Xs_JoeIGku2bX7AyO0kng,2557
|
|
18
|
+
pabot/py3/worker.py,sha256=5rfp4ZiW6gf8GRz6eC0-KUkfx847A91lVtRYpLAv2sg,1612
|
|
19
|
+
robotframework_pabot-5.2.0rc1.dist-info/METADATA,sha256=qQ3V9OzAxIwAZGpUtL9Gi2cetHLXDsTzCPR2Wt1huVw,24792
|
|
20
|
+
robotframework_pabot-5.2.0rc1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
21
|
+
robotframework_pabot-5.2.0rc1.dist-info/entry_points.txt,sha256=JpAIFADTeFOQWdwmn56KpAil8V3-41ZC5ICXCYm3Ng0,43
|
|
22
|
+
robotframework_pabot-5.2.0rc1.dist-info/top_level.txt,sha256=t3OwfEAsSxyxrhjy_GCJYHKbV_X6AIsgeLhYeHvObG4,6
|
|
23
|
+
robotframework_pabot-5.2.0rc1.dist-info/RECORD,,
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
pabot/SharedLibrary.py,sha256=mIipGs3ZhKYEakKprcbrMI4P_Un6qI8gE7086xpHaLY,2552
|
|
2
|
-
pabot/__init__.py,sha256=0g7UY0dKCwXzo3sH_STKWVLBEVtZnj96gmuak8fdlf0,200
|
|
3
|
-
pabot/arguments.py,sha256=M1T2QAA0v2BO1bbryLC82RIA0VZZaEGfXnQiXfNcHOU,9577
|
|
4
|
-
pabot/clientwrapper.py,sha256=yz7battGs0exysnDeLDWJuzpb2Q-qSjitwxZMO2TlJw,231
|
|
5
|
-
pabot/coordinatorwrapper.py,sha256=nQQ7IowD6c246y8y9nsx0HZbt8vS2XODhPVDjm-lyi0,195
|
|
6
|
-
pabot/execution_items.py,sha256=zDVGW0AAeVbM-scC3Yui2TxvIPx1wYyFKHTPU2BkJkY,13329
|
|
7
|
-
pabot/pabot.py,sha256=wxkCGUzvibj7Jtdqhuyzo7F5k5xOPgVXICWZXbt7cn8,81246
|
|
8
|
-
pabot/pabotlib.py,sha256=vHbqV7L7mIvDzXBh9UcdULrwhBHNn70EDXF_31MNFO4,22320
|
|
9
|
-
pabot/result_merger.py,sha256=g4mm-BhhMK57Z6j6dpvfL5El1g5onOtfV4RByNrO8g0,9744
|
|
10
|
-
pabot/robotremoteserver.py,sha256=L3O2QRKSGSE4ux5M1ip5XJMaelqaxQWJxd9wLLdtpzM,22272
|
|
11
|
-
pabot/workerwrapper.py,sha256=BdELUVDs5BmEkdNBcYTlnP22Cj0tUpZEunYQMAKyKWU,185
|
|
12
|
-
pabot/py3/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
pabot/py3/client.py,sha256=Od9L4vZ0sozMHq_W_ITQHBBt8kAej40DG58wnxmbHGM,1434
|
|
14
|
-
pabot/py3/coordinator.py,sha256=kBshCzA_1QX_f0WNk42QBJyDYSwSlNM-UEBxOReOj6E,2313
|
|
15
|
-
pabot/py3/messages.py,sha256=7mFr4_0x1JHm5sW8TvKq28Xs_JoeIGku2bX7AyO0kng,2557
|
|
16
|
-
pabot/py3/worker.py,sha256=5rfp4ZiW6gf8GRz6eC0-KUkfx847A91lVtRYpLAv2sg,1612
|
|
17
|
-
robotframework_pabot-5.1.0.dist-info/licenses/LICENSE.txt,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
18
|
-
robotframework_pabot-5.1.0.dist-info/METADATA,sha256=-3nvXfoJrNCoqf6XVZccsiGhqiU8quQWI3cGk_RV0hY,22070
|
|
19
|
-
robotframework_pabot-5.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
-
robotframework_pabot-5.1.0.dist-info/entry_points.txt,sha256=JpAIFADTeFOQWdwmn56KpAil8V3-41ZC5ICXCYm3Ng0,43
|
|
21
|
-
robotframework_pabot-5.1.0.dist-info/top_level.txt,sha256=t3OwfEAsSxyxrhjy_GCJYHKbV_X6AIsgeLhYeHvObG4,6
|
|
22
|
-
robotframework_pabot-5.1.0.dist-info/RECORD,,
|