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/ProcessManager.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import threading
|
|
5
|
+
import subprocess
|
|
6
|
+
import datetime
|
|
7
|
+
import queue
|
|
8
|
+
import locale
|
|
9
|
+
import signal
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import psutil
|
|
13
|
+
except ImportError:
|
|
14
|
+
psutil = None
|
|
15
|
+
|
|
16
|
+
from .writer import get_writer, Color
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def split_on_first(lst, value):
|
|
20
|
+
for i, x in enumerate(lst):
|
|
21
|
+
if x == value:
|
|
22
|
+
return lst[:i], lst[i+1:]
|
|
23
|
+
return lst, []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProcessManager:
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self.processes = []
|
|
29
|
+
self.lock = threading.Lock()
|
|
30
|
+
self.writer = get_writer()
|
|
31
|
+
self.interrupted = False
|
|
32
|
+
# Note: Signal handling is done in pabot.py's main_program() to ensure
|
|
33
|
+
# PabotLib is shut down gracefully before process termination
|
|
34
|
+
# This ProcessManager will check the interrupted flag set by pabot.py's keyboard_interrupt()
|
|
35
|
+
|
|
36
|
+
def set_interrupted(self):
|
|
37
|
+
"""Called by pabot.py when CTRL+C is received."""
|
|
38
|
+
self.interrupted = True
|
|
39
|
+
|
|
40
|
+
# -------------------------------
|
|
41
|
+
# OUTPUT STREAM READERS
|
|
42
|
+
# -------------------------------
|
|
43
|
+
|
|
44
|
+
def _enqueue_output(self, pipe, q):
|
|
45
|
+
"""
|
|
46
|
+
Reads lines from `pipe` and puts them into queue `q`.
|
|
47
|
+
When pipe is exhausted, pushes `None` sentinel.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
with pipe:
|
|
51
|
+
for line in iter(pipe.readline, b""):
|
|
52
|
+
q.put(line)
|
|
53
|
+
finally:
|
|
54
|
+
q.put(None) # sentinel → "this stream is finished"
|
|
55
|
+
|
|
56
|
+
def _safe_write_to_stream(self, stream, text):
|
|
57
|
+
"""
|
|
58
|
+
Writes text safely to an output stream.
|
|
59
|
+
If encoding errors occur, fall back to bytes/replace.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
stream.write(text)
|
|
63
|
+
try:
|
|
64
|
+
stream.flush()
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
return
|
|
68
|
+
except UnicodeEncodeError:
|
|
69
|
+
pass
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
enc = getattr(stream, "encoding", None) or locale.getpreferredencoding(False) or "utf-8"
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
b = text.encode(enc, errors="replace")
|
|
77
|
+
if hasattr(stream, "buffer"):
|
|
78
|
+
try:
|
|
79
|
+
stream.buffer.write(b)
|
|
80
|
+
stream.buffer.write(b"\n")
|
|
81
|
+
stream.buffer.flush()
|
|
82
|
+
return
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
safe = b.decode(enc, errors="replace")
|
|
87
|
+
stream.write(safe + "\n")
|
|
88
|
+
stream.flush()
|
|
89
|
+
except Exception:
|
|
90
|
+
try:
|
|
91
|
+
print(text)
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
# -------------------------------
|
|
96
|
+
# STREAM OUTPUT MERGER
|
|
97
|
+
# -------------------------------
|
|
98
|
+
|
|
99
|
+
def _stream_output(self, process, stdout=None, stderr=None,
|
|
100
|
+
item_name="process", log_file=None):
|
|
101
|
+
|
|
102
|
+
q_out = queue.Queue()
|
|
103
|
+
q_err = queue.Queue()
|
|
104
|
+
|
|
105
|
+
t_out = None
|
|
106
|
+
t_err = None
|
|
107
|
+
|
|
108
|
+
if process.stdout:
|
|
109
|
+
t_out = threading.Thread(target=self._enqueue_output, args=(process.stdout, q_out))
|
|
110
|
+
t_out.daemon = True
|
|
111
|
+
t_out.start()
|
|
112
|
+
|
|
113
|
+
if process.stderr:
|
|
114
|
+
t_err = threading.Thread(target=self._enqueue_output, args=(process.stderr, q_err))
|
|
115
|
+
t_err.daemon = True
|
|
116
|
+
t_err.start()
|
|
117
|
+
|
|
118
|
+
stdout_done = False
|
|
119
|
+
stderr_done = False
|
|
120
|
+
|
|
121
|
+
log_handle = None
|
|
122
|
+
if log_file:
|
|
123
|
+
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
|
124
|
+
log_handle = open(log_file, "a", encoding="utf-8")
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
while True:
|
|
128
|
+
now = datetime.datetime.now()
|
|
129
|
+
|
|
130
|
+
# STDOUT
|
|
131
|
+
if not stdout_done:
|
|
132
|
+
try:
|
|
133
|
+
line = q_out.get(timeout=0.05)
|
|
134
|
+
if line is None:
|
|
135
|
+
stdout_done = True
|
|
136
|
+
else:
|
|
137
|
+
msg = line.decode(errors="replace").rstrip()
|
|
138
|
+
self._safe_write_to_stream(stdout or sys.stdout, msg + "\n")
|
|
139
|
+
if log_handle:
|
|
140
|
+
log_handle.write(f"{now} {msg}\n")
|
|
141
|
+
except queue.Empty:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
# STDERR
|
|
145
|
+
if not stderr_done:
|
|
146
|
+
try:
|
|
147
|
+
line = q_err.get_nowait()
|
|
148
|
+
if line is None:
|
|
149
|
+
stderr_done = True
|
|
150
|
+
else:
|
|
151
|
+
msg = line.decode(errors="replace").rstrip()
|
|
152
|
+
self._safe_write_to_stream(stderr or sys.stderr, msg + "\n")
|
|
153
|
+
if log_handle:
|
|
154
|
+
log_handle.write(f"{now} {msg}\n")
|
|
155
|
+
except queue.Empty:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
# Terminate when both streams finished
|
|
159
|
+
if stdout_done and stderr_done:
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
finally:
|
|
163
|
+
if t_out:
|
|
164
|
+
t_out.join()
|
|
165
|
+
if t_err:
|
|
166
|
+
t_err.join()
|
|
167
|
+
if log_handle:
|
|
168
|
+
log_handle.close()
|
|
169
|
+
|
|
170
|
+
# -------------------------------
|
|
171
|
+
# PROCESS CREATION
|
|
172
|
+
# -------------------------------
|
|
173
|
+
|
|
174
|
+
def _start_process(self, cmd, env=None):
|
|
175
|
+
if sys.platform == "win32":
|
|
176
|
+
return subprocess.Popen(
|
|
177
|
+
cmd,
|
|
178
|
+
stdout=subprocess.PIPE,
|
|
179
|
+
stderr=subprocess.PIPE,
|
|
180
|
+
env=env,
|
|
181
|
+
shell=False,
|
|
182
|
+
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
return subprocess.Popen(
|
|
186
|
+
cmd,
|
|
187
|
+
stdout=subprocess.PIPE,
|
|
188
|
+
stderr=subprocess.PIPE,
|
|
189
|
+
env=env,
|
|
190
|
+
shell=False,
|
|
191
|
+
preexec_fn=os.setsid,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# -------------------------------
|
|
195
|
+
# PROCESS TREE TERMINATION
|
|
196
|
+
# -------------------------------
|
|
197
|
+
|
|
198
|
+
def _terminate_tree(self, process):
|
|
199
|
+
if process.poll() is not None:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
self.writer.write(
|
|
203
|
+
f"[ProcessManager] Terminating process tree PID={process.pid}",
|
|
204
|
+
level='debug'
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# PRIMARY: psutil (best reliability)
|
|
208
|
+
if psutil:
|
|
209
|
+
try:
|
|
210
|
+
parent = psutil.Process(process.pid)
|
|
211
|
+
children = parent.children(recursive=True)
|
|
212
|
+
for c in children:
|
|
213
|
+
try:
|
|
214
|
+
c.terminate()
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
psutil.wait_procs(children, timeout=5)
|
|
218
|
+
|
|
219
|
+
for c in children:
|
|
220
|
+
if c.is_running():
|
|
221
|
+
try:
|
|
222
|
+
c.kill()
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
parent.terminate()
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
parent.wait(timeout=5)
|
|
233
|
+
except psutil.TimeoutExpired:
|
|
234
|
+
try:
|
|
235
|
+
parent.kill()
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
return
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
# FALLBACK — Windows
|
|
244
|
+
if sys.platform == "win32":
|
|
245
|
+
subprocess.run(
|
|
246
|
+
["taskkill", "/PID", str(process.pid), "/T", "/F"],
|
|
247
|
+
stdout=subprocess.DEVNULL,
|
|
248
|
+
stderr=subprocess.DEVNULL,
|
|
249
|
+
)
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
# FALLBACK — Linux / macOS
|
|
253
|
+
try:
|
|
254
|
+
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
|
|
255
|
+
time.sleep(2)
|
|
256
|
+
if process.poll() is None:
|
|
257
|
+
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
|
|
258
|
+
except Exception:
|
|
259
|
+
if process.poll() is None:
|
|
260
|
+
try:
|
|
261
|
+
process.kill()
|
|
262
|
+
except Exception:
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
process.wait(timeout=5)
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
# -------------------------------
|
|
271
|
+
# PUBLIC API
|
|
272
|
+
# -------------------------------
|
|
273
|
+
|
|
274
|
+
def terminate_all(self):
|
|
275
|
+
with self.lock:
|
|
276
|
+
for p in list(self.processes):
|
|
277
|
+
self._terminate_tree(p)
|
|
278
|
+
self.processes.clear()
|
|
279
|
+
|
|
280
|
+
def run(self, cmd, *, env=None, stdout=None, stderr=None,
|
|
281
|
+
timeout=None, verbose=False, item_name="process",
|
|
282
|
+
log_file=None, pool_id=0, item_index=0):
|
|
283
|
+
|
|
284
|
+
start = time.time()
|
|
285
|
+
process = self._start_process(cmd, env)
|
|
286
|
+
|
|
287
|
+
with self.lock:
|
|
288
|
+
self.processes.append(process)
|
|
289
|
+
|
|
290
|
+
ts = datetime.datetime.now()
|
|
291
|
+
|
|
292
|
+
if verbose:
|
|
293
|
+
self.writer.write(
|
|
294
|
+
f"{ts} [PID:{process.pid}] [{pool_id}] [ID:{item_index}] "
|
|
295
|
+
f"EXECUTING PARALLEL {item_name}:\n{' '.join(cmd)}",
|
|
296
|
+
level='debug'
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
self.writer.write(
|
|
300
|
+
f"{ts} [PID:{process.pid}] [{pool_id}] [ID:{item_index}] EXECUTING {item_name}",
|
|
301
|
+
level='debug'
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Start logging thread
|
|
305
|
+
log_thread = threading.Thread(
|
|
306
|
+
target=self._stream_output,
|
|
307
|
+
args=(process, stdout, stderr, item_name, log_file),
|
|
308
|
+
)
|
|
309
|
+
log_thread.daemon = True
|
|
310
|
+
log_thread.start()
|
|
311
|
+
|
|
312
|
+
rc = None
|
|
313
|
+
ping_interval = 50 # 5s
|
|
314
|
+
next_ping = ping_interval
|
|
315
|
+
counter = 0
|
|
316
|
+
|
|
317
|
+
while rc is None:
|
|
318
|
+
rc = process.poll()
|
|
319
|
+
|
|
320
|
+
# INTERRUPT CHECK - terminate process gracefully when CTRL+C is pressed
|
|
321
|
+
if self.interrupted:
|
|
322
|
+
ts = datetime.datetime.now()
|
|
323
|
+
self.writer.write(
|
|
324
|
+
f"{ts} [PID:{process.pid}] [{pool_id}] [ID:{item_index}] "
|
|
325
|
+
f"Process {item_name} interrupted by user (Ctrl+C)",
|
|
326
|
+
color=Color.YELLOW, level='warning'
|
|
327
|
+
)
|
|
328
|
+
self._terminate_tree(process)
|
|
329
|
+
rc = -1
|
|
330
|
+
|
|
331
|
+
# Dryrun process to mark all tests as failed due to user interrupt
|
|
332
|
+
this_dir = os.path.dirname(os.path.abspath(__file__))
|
|
333
|
+
listener_path = os.path.join(this_dir, "listener", "interrupt_listener.py")
|
|
334
|
+
dry_run_env = env.copy() if env else os.environ.copy()
|
|
335
|
+
before, after = split_on_first(cmd, "-A")
|
|
336
|
+
dryrun_cmd = before + ["--dryrun", '--listener', listener_path, '-A'] + after
|
|
337
|
+
|
|
338
|
+
self.writer.write(
|
|
339
|
+
f"{ts} [PID:{process.pid}] [{pool_id}] [ID:{item_index}] "
|
|
340
|
+
f"Starting dry run to mark test as failed due to user interrupt: {' '.join(dryrun_cmd)}",
|
|
341
|
+
level='debug'
|
|
342
|
+
)
|
|
343
|
+
try:
|
|
344
|
+
subprocess.run(
|
|
345
|
+
dryrun_cmd,
|
|
346
|
+
env=dry_run_env,
|
|
347
|
+
stdout=subprocess.PIPE,
|
|
348
|
+
stderr=subprocess.PIPE,
|
|
349
|
+
timeout=3,
|
|
350
|
+
text=True,
|
|
351
|
+
)
|
|
352
|
+
except subprocess.TimeoutExpired as e:
|
|
353
|
+
self.writer.write(f"Dry-run timed out after 3s: {e}", level='debug')
|
|
354
|
+
break
|
|
355
|
+
|
|
356
|
+
# TIMEOUT CHECK
|
|
357
|
+
if timeout and (time.time() - start > timeout):
|
|
358
|
+
ts = datetime.datetime.now()
|
|
359
|
+
self.writer.write(
|
|
360
|
+
f"{ts} [PID:{process.pid}] [{pool_id}] [ID:{item_index}] "
|
|
361
|
+
f"Process {item_name} killed due to exceeding the maximum timeout of {timeout} seconds",
|
|
362
|
+
color=Color.YELLOW, level='warning'
|
|
363
|
+
)
|
|
364
|
+
self._terminate_tree(process)
|
|
365
|
+
rc = -1
|
|
366
|
+
|
|
367
|
+
# Dryrun process to mark all tests as failed due to timeout
|
|
368
|
+
this_dir = os.path.dirname(os.path.abspath(__file__))
|
|
369
|
+
listener_path = os.path.join(this_dir, "listener", "timeout_listener.py")
|
|
370
|
+
dry_run_env = env.copy() if env else os.environ.copy()
|
|
371
|
+
before, after = split_on_first(cmd, "-A")
|
|
372
|
+
dryrun_cmd = before + ["--dryrun", '--listener', listener_path, '-A'] + after
|
|
373
|
+
|
|
374
|
+
self.writer.write(
|
|
375
|
+
f"{ts} [PID:{process.pid}] [{pool_id}] [ID:{item_index}] "
|
|
376
|
+
f"Starting dry run to mark test as failed due to timeout: {' '.join(dryrun_cmd)}",
|
|
377
|
+
level='debug'
|
|
378
|
+
)
|
|
379
|
+
try:
|
|
380
|
+
subprocess.run(
|
|
381
|
+
dryrun_cmd,
|
|
382
|
+
env=dry_run_env,
|
|
383
|
+
stdout=subprocess.PIPE,
|
|
384
|
+
stderr=subprocess.PIPE,
|
|
385
|
+
timeout=3,
|
|
386
|
+
text=True,
|
|
387
|
+
)
|
|
388
|
+
except subprocess.TimeoutExpired as e:
|
|
389
|
+
self.writer.write(f"Dry-run timed out after 3s: {e}", level='debug')
|
|
390
|
+
|
|
391
|
+
break
|
|
392
|
+
|
|
393
|
+
# Progress ping
|
|
394
|
+
if counter == next_ping:
|
|
395
|
+
ts = datetime.datetime.now()
|
|
396
|
+
self.writer.write(
|
|
397
|
+
f"{ts} [PID:{process.pid}] [{pool_id}] [ID:{item_index}] still running "
|
|
398
|
+
f"{item_name} after {(counter * 0.1):.1f}s",
|
|
399
|
+
level='debug'
|
|
400
|
+
)
|
|
401
|
+
ping_interval += 50
|
|
402
|
+
next_ping += ping_interval
|
|
403
|
+
|
|
404
|
+
time.sleep(0.1)
|
|
405
|
+
counter += 1
|
|
406
|
+
|
|
407
|
+
log_thread.join()
|
|
408
|
+
|
|
409
|
+
elapsed = round(time.time() - start, 1)
|
|
410
|
+
|
|
411
|
+
with self.lock:
|
|
412
|
+
if process in self.processes:
|
|
413
|
+
self.processes.remove(process)
|
|
414
|
+
|
|
415
|
+
return process, (rc, elapsed)
|
pabot/__init__.py
CHANGED
pabot/arguments.py
CHANGED
|
@@ -151,6 +151,11 @@ def _parse_artifacts(arg):
|
|
|
151
151
|
|
|
152
152
|
|
|
153
153
|
def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str, object]]
|
|
154
|
+
"""
|
|
155
|
+
Parse pabot-specific command line arguments.
|
|
156
|
+
Supports new --ordering syntax:
|
|
157
|
+
--ordering <file> [static|dynamic] [skip|run_all]
|
|
158
|
+
"""
|
|
154
159
|
pabot_args = {
|
|
155
160
|
"command": ["pybot" if ROBOT_VERSION < "3.1" else "robot"],
|
|
156
161
|
"verbose": False,
|
|
@@ -169,8 +174,10 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
|
|
|
169
174
|
"shardcount": 1,
|
|
170
175
|
"chunk": False,
|
|
171
176
|
"no-rebot": False,
|
|
177
|
+
"pabotconsole": "verbose",
|
|
172
178
|
}
|
|
173
|
-
|
|
179
|
+
|
|
180
|
+
# Arguments that are flags (boolean)
|
|
174
181
|
flag_args = {
|
|
175
182
|
"verbose",
|
|
176
183
|
"help",
|
|
@@ -178,8 +185,10 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
|
|
|
178
185
|
"pabotlib",
|
|
179
186
|
"artifactsinsubfolders",
|
|
180
187
|
"chunk",
|
|
181
|
-
"no-rebot"
|
|
188
|
+
"no-rebot",
|
|
182
189
|
}
|
|
190
|
+
|
|
191
|
+
# Arguments that expect values
|
|
183
192
|
value_args = {
|
|
184
193
|
"hive": str,
|
|
185
194
|
"processes": lambda x: int(x) if x != "all" else None,
|
|
@@ -188,17 +197,18 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
|
|
|
188
197
|
"pabotlibport": int,
|
|
189
198
|
"pabotprerunmodifier": str,
|
|
190
199
|
"processtimeout": int,
|
|
191
|
-
"ordering": str,
|
|
200
|
+
"ordering": str, # special handling below
|
|
192
201
|
"suitesfrom": str,
|
|
193
202
|
"artifacts": _parse_artifacts,
|
|
194
203
|
"shard": _parse_shard,
|
|
204
|
+
"pabotconsole": str,
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
argumentfiles = []
|
|
198
208
|
remaining_args = []
|
|
199
209
|
i = 0
|
|
200
210
|
|
|
201
|
-
# Track conflicting options
|
|
211
|
+
# Track conflicting pabotlib options
|
|
202
212
|
saw_pabotlib_flag = False
|
|
203
213
|
saw_no_pabotlib = False
|
|
204
214
|
|
|
@@ -209,19 +219,20 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
|
|
|
209
219
|
i += 1
|
|
210
220
|
continue
|
|
211
221
|
|
|
212
|
-
arg_name = arg[2:] #
|
|
222
|
+
arg_name = arg[2:] # remove leading '--'
|
|
213
223
|
|
|
224
|
+
# Handle mutually exclusive pabotlib flags
|
|
214
225
|
if arg_name == "no-pabotlib":
|
|
215
226
|
saw_no_pabotlib = True
|
|
216
|
-
pabot_args["pabotlib"] = False
|
|
217
|
-
|
|
227
|
+
pabot_args["pabotlib"] = False
|
|
228
|
+
i += 1
|
|
218
229
|
continue
|
|
219
230
|
if arg_name == "pabotlib":
|
|
220
231
|
saw_pabotlib_flag = True
|
|
221
|
-
|
|
232
|
+
i += 1
|
|
222
233
|
continue
|
|
223
234
|
|
|
224
|
-
# Special
|
|
235
|
+
# Special handling for --command ... --end-command
|
|
225
236
|
if arg_name == "command":
|
|
226
237
|
try:
|
|
227
238
|
end_index = args.index("--end-command", i)
|
|
@@ -231,7 +242,7 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
|
|
|
231
242
|
except ValueError:
|
|
232
243
|
raise DataError("--command requires matching --end-command")
|
|
233
244
|
|
|
234
|
-
# Handle
|
|
245
|
+
# Handle boolean flags
|
|
235
246
|
if arg_name in flag_args:
|
|
236
247
|
pabot_args[arg_name] = True
|
|
237
248
|
i += 1
|
|
@@ -242,23 +253,68 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
|
|
|
242
253
|
if i + 1 >= len(args):
|
|
243
254
|
raise DataError(f"--{arg_name} requires a value")
|
|
244
255
|
try:
|
|
245
|
-
|
|
246
|
-
if arg_name == "
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
256
|
+
# Special parsing for --ordering <file> [mode] [failure_policy]
|
|
257
|
+
if arg_name == "ordering":
|
|
258
|
+
if i + 1 >= len(args):
|
|
259
|
+
raise DataError("--ordering requires at least a file path")
|
|
260
|
+
|
|
261
|
+
ordering_file = args[i + 1]
|
|
262
|
+
mode = "static" # default
|
|
263
|
+
failure_policy = "run_all" # default
|
|
264
|
+
|
|
265
|
+
# optional mode
|
|
266
|
+
if i + 2 < len(args) and args[i + 2] in ("static", "dynamic"):
|
|
267
|
+
mode = args[i + 2]
|
|
268
|
+
i_mode_offset = 1
|
|
269
|
+
else:
|
|
270
|
+
i_mode_offset = 0
|
|
271
|
+
|
|
272
|
+
# optional failure policy, only for dynamic mode
|
|
273
|
+
if mode == "dynamic" and i + 2 + i_mode_offset < len(args) and args[i + 2 + i_mode_offset] in ("skip", "run_all"):
|
|
274
|
+
failure_policy = args[i + 2 + i_mode_offset]
|
|
275
|
+
i_failure_offset = 1
|
|
276
|
+
else:
|
|
277
|
+
i_failure_offset = 0
|
|
278
|
+
|
|
279
|
+
# store
|
|
280
|
+
pabot_args["ordering"] = {
|
|
281
|
+
"file": ordering_file,
|
|
282
|
+
"mode": mode,
|
|
283
|
+
"failure_policy": failure_policy,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# move index past ordering args only
|
|
287
|
+
i += 2 + i_mode_offset + i_failure_offset
|
|
288
|
+
continue
|
|
289
|
+
elif arg_name == "pabotconsole":
|
|
290
|
+
console_type = args[i + 1]
|
|
291
|
+
valid_types = ("verbose", "dotted", "quiet", "none")
|
|
292
|
+
if console_type not in valid_types:
|
|
293
|
+
raise DataError(
|
|
294
|
+
f"Invalid value for --pabotconsole: {console_type}. "
|
|
295
|
+
f"Valid values are: {', '.join(valid_types)}"
|
|
296
|
+
)
|
|
297
|
+
pabot_args["pabotconsole"] = console_type
|
|
298
|
+
i += 2
|
|
299
|
+
continue
|
|
254
300
|
else:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
301
|
+
value = value_args[arg_name](args[i + 1])
|
|
302
|
+
if arg_name == "shard":
|
|
303
|
+
pabot_args["shardindex"], pabot_args["shardcount"] = value
|
|
304
|
+
elif arg_name == "pabotlibhost":
|
|
305
|
+
pabot_args["pabotlib"] = False
|
|
306
|
+
pabot_args[arg_name] = value
|
|
307
|
+
elif arg_name == "artifacts":
|
|
308
|
+
pabot_args["artifacts"] = value[0]
|
|
309
|
+
pabot_args["artifactstimestamps"] = value[1]
|
|
310
|
+
else:
|
|
311
|
+
pabot_args[arg_name] = value
|
|
312
|
+
i += 2
|
|
313
|
+
continue
|
|
314
|
+
except (ValueError, TypeError):
|
|
259
315
|
raise DataError(f"Invalid value for --{arg_name}: {args[i + 1]}")
|
|
260
|
-
|
|
261
|
-
# Handle
|
|
316
|
+
|
|
317
|
+
# Handle argumentfiles like --argumentfile1
|
|
262
318
|
match = ARGSMATCHER.match(arg)
|
|
263
319
|
if match:
|
|
264
320
|
if i + 1 >= len(args):
|
|
@@ -267,10 +323,11 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
|
|
|
267
323
|
i += 2
|
|
268
324
|
continue
|
|
269
325
|
|
|
270
|
-
#
|
|
326
|
+
# Any other non-pabot argument
|
|
271
327
|
remaining_args.append(arg)
|
|
272
328
|
i += 1
|
|
273
329
|
|
|
330
|
+
# Check for conflicting pabotlib flags
|
|
274
331
|
if saw_pabotlib_flag and saw_no_pabotlib:
|
|
275
332
|
raise DataError("Cannot use both --pabotlib and --no-pabotlib options together")
|
|
276
333
|
|