robotframework-pabot 5.2.0b1__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/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
- print("Error while merging result %s" % merged.source)
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
- print(err.message)
222
- print("Skipping '%s' from final result" % src)
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/writer.py CHANGED
@@ -12,10 +12,79 @@ class Color:
12
12
  SUPPORTED_OSES = {"posix"} # Only Unix terminals support ANSI colors
13
13
 
14
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
+
15
82
  class MessageWriter:
16
- def __init__(self, log_file=None):
83
+ def __init__(self, log_file=None, console_type="verbose"):
17
84
  self.queue = queue.Queue()
18
85
  self.log_file = log_file
86
+ self.console_type = console_type
87
+ self.console = DottedConsole() if console_type == "dotted" else None
19
88
  if log_file:
20
89
  os.makedirs(os.path.dirname(log_file), exist_ok=True)
21
90
  self._stop_event = threading.Event()
@@ -31,24 +100,70 @@ class MessageWriter:
31
100
  return f"{color}{message}{Color.ENDC}"
32
101
  return message
33
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
+
34
130
  def _writer(self):
35
131
  while not self._stop_event.is_set():
36
132
  try:
37
- message, color = self.queue.get(timeout=0.1)
133
+ message, color, level = self.queue.get(timeout=0.1)
38
134
  except queue.Empty:
39
135
  continue
40
136
  if message is None:
41
137
  self.queue.task_done()
42
138
  break
43
- print(self._wrap_with(color, message))
44
- sys.stdout.flush()
139
+
140
+ message = message.rstrip("\n")
141
+ # Always write to log file
45
142
  if self.log_file:
46
143
  with open(self.log_file, "a", encoding="utf-8") as f:
47
- f.write(message + "\n")
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
+
48
163
  self.queue.task_done()
49
164
 
50
- def write(self, message, color=None):
51
- self.queue.put((f"{message}", color))
165
+ def write(self, message, color=None, level="info"):
166
+ self.queue.put((f"{message}", color, level))
52
167
 
53
168
  def flush(self, timeout=5):
54
169
  """
@@ -94,17 +209,27 @@ class MessageWriter:
94
209
  """
95
210
  self.flush()
96
211
  self._stop_event.set()
97
- self.queue.put((None, None)) # sentinel to break thread loop
212
+ self.queue.put((None, None, None)) # sentinel to break thread loop
98
213
  self.thread.join(timeout=1.0)
99
214
 
100
215
 
101
216
  _writer_instance = None
102
217
 
103
- def get_writer(log_dir=None):
218
+ def get_writer(log_dir=None, console_type="verbose"):
104
219
  global _writer_instance
105
220
  if _writer_instance is None:
106
221
  if log_dir:
107
222
  os.makedirs(log_dir, exist_ok=True)
108
223
  log_file = os.path.join(log_dir or ".", "pabot_manager.log")
109
- _writer_instance = MessageWriter(log_file=log_file)
224
+ _writer_instance = MessageWriter(log_file=log_file, console_type=console_type)
110
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.2.0b1
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
@@ -118,10 +118,11 @@ pabot [--verbose|--testlevelsplit|--command .. --end-command|
118
118
  --shard i/n|
119
119
  --artifacts extensions|--artifactsinsubfolders|
120
120
  --resourcefile file|--argumentfile[num] file|--suitesfrom file
121
- --ordering <FILENAME> [static|dynamic] [skip|run_all]|
121
+ --ordering file [static|dynamic] [skip|run_all]|
122
122
  --chunk|
123
123
  --pabotprerunmodifier modifier|
124
124
  --no-rebot|
125
+ --pabotconsole [verbose|dotted|quiet|none]|
125
126
  --help|--version]
126
127
  [robot options] [path ...]
127
128
  ```
@@ -206,10 +207,10 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
206
207
  Optionally read suites from output.xml file. Failed suites will run first and longer running ones will be executed
207
208
  before shorter ones.
208
209
 
209
- **--ordering [FILE PATH] [MODE] [FAILURE POLICY]**
210
+ **--ordering [FILEPATH] [MODE] [FAILURE POLICY]**
210
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)
211
- - MODE (optional): [static (default)|dynamic]
212
- - FAILURE POLICY (optional, only in dynamic mode): [skip|run_all (default)]
212
+ - MODE (optional): [ static (default) | dynamic ]
213
+ - FAILURE POLICY (optional, only in dynamic mode): [ skip | run_all (default) ]
213
214
 
214
215
  **--chunk**
215
216
  Optionally chunk tests to PROCESSES number of robot runs. This can save time because all the suites will share the same
@@ -226,6 +227,28 @@ Supports all [Robot Framework command line options](https://robotframework.org/r
226
227
  for scenarios where Rebot should be run later due to large log files, ensuring better memory and resource availability.
227
228
  Subprocess results are stored in the pabot_results folder.
228
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
+
229
252
  **--help**
230
253
  Print usage instructions.
231
254
 
@@ -491,7 +514,7 @@ pabot_results/
491
514
  │ ├── robot_stdout.out
492
515
  │ ├── robot_stderr.out
493
516
  │ └── artifacts...
494
- pabot_manager.log # Pabot's own main log. Basically same than prints in console
517
+ └── pabot_manager.log # Pabot's own main log.
495
518
  ```
496
519
 
497
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
pabot/skip_listener.py DELETED
@@ -1,7 +0,0 @@
1
- ROBOT_LISTENER_API_VERSION = 3
2
-
3
- def end_test(data, result):
4
- # data: TestCase object
5
- # result: TestCaseResult object
6
- result.status = 'SKIP'
7
- result.message = "Pabot skip logic: this test was skipped due to dependencies and failure policy."
pabot/timeout_listener.py DELETED
@@ -1,5 +0,0 @@
1
- ROBOT_LISTENER_API_VERSION = 3
2
-
3
- def end_test(data, result):
4
- result.status = 'FAIL'
5
- result.message = "Pabot's --processtimeout option has been reached."
@@ -1,25 +0,0 @@
1
- pabot/ProcessManager.py,sha256=w3dgtEKGhn4rj3nQ1EEQFAPeiIv6OF-KU6R3WZwQxPs,11739
2
- pabot/SharedLibrary.py,sha256=mIipGs3ZhKYEakKprcbrMI4P_Un6qI8gE7086xpHaLY,2552
3
- pabot/__init__.py,sha256=KuCXsD2BDgEFiQAxVRLN3SXkkyCULXtW06tTTqCEuRo,202
4
- pabot/arguments.py,sha256=6MBXKnbDgWNdu7NnRqqd2_GNOs-u8QOxCz9wHhyfUVI,11404
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=hfMZdMYmeZEo3mNAVrvmuPU9vGhKpIBEVq5R8BI5ovE,87438
9
- pabot/pabotlib.py,sha256=vHbqV7L7mIvDzXBh9UcdULrwhBHNn70EDXF_31MNFO4,22320
10
- pabot/result_merger.py,sha256=g4mm-BhhMK57Z6j6dpvfL5El1g5onOtfV4RByNrO8g0,9744
11
- pabot/robotremoteserver.py,sha256=BdeIni9Q4LJKVDBUlG2uJ9tiyAjrPXwU_YsPq1THWoo,23296
12
- pabot/skip_listener.py,sha256=xv7wH-yB_nfc_AT1oZh3C0iu8hS67g5a28x8hwSrYsE,254
13
- pabot/timeout_listener.py,sha256=twZFiJEyn9tAqI1K6JDBGaZrxRqVLFDVWvyxa6d8Lb0,160
14
- pabot/workerwrapper.py,sha256=BdELUVDs5BmEkdNBcYTlnP22Cj0tUpZEunYQMAKyKWU,185
15
- pabot/writer.py,sha256=_sRN_EqIhaMwPYmTkN4NzO2Mj6tggcaxgYkqJKatb_c,3639
16
- pabot/py3/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- pabot/py3/client.py,sha256=Od9L4vZ0sozMHq_W_ITQHBBt8kAej40DG58wnxmbHGM,1434
18
- pabot/py3/coordinator.py,sha256=kBshCzA_1QX_f0WNk42QBJyDYSwSlNM-UEBxOReOj6E,2313
19
- pabot/py3/messages.py,sha256=7mFr4_0x1JHm5sW8TvKq28Xs_JoeIGku2bX7AyO0kng,2557
20
- pabot/py3/worker.py,sha256=5rfp4ZiW6gf8GRz6eC0-KUkfx847A91lVtRYpLAv2sg,1612
21
- robotframework_pabot-5.2.0b1.dist-info/METADATA,sha256=_7b2wWKxeuFlEbRCz_sHwmrWQCwp85v-rl9HX-mScyo,23811
22
- robotframework_pabot-5.2.0b1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- robotframework_pabot-5.2.0b1.dist-info/entry_points.txt,sha256=JpAIFADTeFOQWdwmn56KpAil8V3-41ZC5ICXCYm3Ng0,43
24
- robotframework_pabot-5.2.0b1.dist-info/top_level.txt,sha256=t3OwfEAsSxyxrhjy_GCJYHKbV_X6AIsgeLhYeHvObG4,6
25
- robotframework_pabot-5.2.0b1.dist-info/RECORD,,