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/ProcessManager.py +67 -28
- pabot/__init__.py +1 -1
- pabot/arguments.py +13 -0
- pabot/pabot.py +358 -151
- pabot/result_merger.py +13 -3
- pabot/writer.py +135 -10
- {robotframework_pabot-5.2.0b1.dist-info → robotframework_pabot-5.2.0rc1.dist-info}/METADATA +29 -6
- robotframework_pabot-5.2.0rc1.dist-info/RECORD +23 -0
- {robotframework_pabot-5.2.0b1.dist-info → robotframework_pabot-5.2.0rc1.dist-info}/WHEEL +1 -1
- pabot/skip_listener.py +0 -7
- pabot/timeout_listener.py +0 -5
- robotframework_pabot-5.2.0b1.dist-info/RECORD +0 -25
- {robotframework_pabot-5.2.0b1.dist-info → robotframework_pabot-5.2.0rc1.dist-info}/entry_points.txt +0 -0
- {robotframework_pabot-5.2.0b1.dist-info → robotframework_pabot-5.2.0rc1.dist-info}/top_level.txt +0 -0
pabot/ProcessManager.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
3
|
import time
|
|
4
|
-
import signal
|
|
5
4
|
import threading
|
|
6
5
|
import subprocess
|
|
7
6
|
import datetime
|
|
8
7
|
import queue
|
|
9
8
|
import locale
|
|
9
|
+
import signal
|
|
10
10
|
|
|
11
11
|
try:
|
|
12
12
|
import psutil
|
|
@@ -28,26 +28,14 @@ class ProcessManager:
|
|
|
28
28
|
self.processes = []
|
|
29
29
|
self.lock = threading.Lock()
|
|
30
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()
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
signal.signal(signal.SIGINT, self._handle_sigint)
|
|
36
|
-
else:
|
|
37
|
-
self.writer.write(
|
|
38
|
-
"[ProcessManager] (test mode) signal handlers disabled (not in main thread)"
|
|
39
|
-
)
|
|
40
|
-
except Exception as e:
|
|
41
|
-
self.writer.write(f"[WARN] Could not register signal handler: {e}")
|
|
42
|
-
|
|
43
|
-
# -------------------------------
|
|
44
|
-
# SIGNAL HANDLING
|
|
45
|
-
# -------------------------------
|
|
46
|
-
|
|
47
|
-
def _handle_sigint(self, signum, frame):
|
|
48
|
-
self.writer.write("[ProcessManager] Ctrl+C detected — terminating all subprocesses", color=Color.RED)
|
|
49
|
-
self.terminate_all()
|
|
50
|
-
sys.exit(130)
|
|
36
|
+
def set_interrupted(self):
|
|
37
|
+
"""Called by pabot.py when CTRL+C is received."""
|
|
38
|
+
self.interrupted = True
|
|
51
39
|
|
|
52
40
|
# -------------------------------
|
|
53
41
|
# OUTPUT STREAM READERS
|
|
@@ -213,7 +201,7 @@ class ProcessManager:
|
|
|
213
201
|
|
|
214
202
|
self.writer.write(
|
|
215
203
|
f"[ProcessManager] Terminating process tree PID={process.pid}",
|
|
216
|
-
|
|
204
|
+
level='debug'
|
|
217
205
|
)
|
|
218
206
|
|
|
219
207
|
# PRIMARY: psutil (best reliability)
|
|
@@ -304,11 +292,13 @@ class ProcessManager:
|
|
|
304
292
|
if verbose:
|
|
305
293
|
self.writer.write(
|
|
306
294
|
f"{ts} [PID:{process.pid}] [{pool_id}] [ID:{item_index}] "
|
|
307
|
-
f"EXECUTING PARALLEL {item_name}:\n{' '.join(cmd)}"
|
|
295
|
+
f"EXECUTING PARALLEL {item_name}:\n{' '.join(cmd)}",
|
|
296
|
+
level='debug'
|
|
308
297
|
)
|
|
309
298
|
else:
|
|
310
299
|
self.writer.write(
|
|
311
|
-
f"{ts} [PID:{process.pid}] [{pool_id}] [ID:{item_index}] EXECUTING {item_name}"
|
|
300
|
+
f"{ts} [PID:{process.pid}] [{pool_id}] [ID:{item_index}] EXECUTING {item_name}",
|
|
301
|
+
level='debug'
|
|
312
302
|
)
|
|
313
303
|
|
|
314
304
|
# Start logging thread
|
|
@@ -327,28 +317,76 @@ class ProcessManager:
|
|
|
327
317
|
while rc is None:
|
|
328
318
|
rc = process.poll()
|
|
329
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
|
+
|
|
330
356
|
# TIMEOUT CHECK
|
|
331
357
|
if timeout and (time.time() - start > timeout):
|
|
332
358
|
ts = datetime.datetime.now()
|
|
333
359
|
self.writer.write(
|
|
334
360
|
f"{ts} [PID:{process.pid}] [{pool_id}] [ID:{item_index}] "
|
|
335
|
-
f"Process {item_name} killed due to exceeding the maximum timeout of {timeout} seconds"
|
|
361
|
+
f"Process {item_name} killed due to exceeding the maximum timeout of {timeout} seconds",
|
|
362
|
+
color=Color.YELLOW, level='warning'
|
|
336
363
|
)
|
|
337
364
|
self._terminate_tree(process)
|
|
338
365
|
rc = -1
|
|
339
366
|
|
|
340
367
|
# Dryrun process to mark all tests as failed due to timeout
|
|
341
368
|
this_dir = os.path.dirname(os.path.abspath(__file__))
|
|
342
|
-
listener_path = os.path.join(this_dir, "timeout_listener.py")
|
|
369
|
+
listener_path = os.path.join(this_dir, "listener", "timeout_listener.py")
|
|
343
370
|
dry_run_env = env.copy() if env else os.environ.copy()
|
|
344
371
|
before, after = split_on_first(cmd, "-A")
|
|
345
372
|
dryrun_cmd = before + ["--dryrun", '--listener', listener_path, '-A'] + after
|
|
346
373
|
|
|
347
374
|
self.writer.write(
|
|
348
375
|
f"{ts} [PID:{process.pid}] [{pool_id}] [ID:{item_index}] "
|
|
349
|
-
f"Starting dry run to mark
|
|
376
|
+
f"Starting dry run to mark test as failed due to timeout: {' '.join(dryrun_cmd)}",
|
|
377
|
+
level='debug'
|
|
350
378
|
)
|
|
351
|
-
|
|
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')
|
|
352
390
|
|
|
353
391
|
break
|
|
354
392
|
|
|
@@ -357,7 +395,8 @@ class ProcessManager:
|
|
|
357
395
|
ts = datetime.datetime.now()
|
|
358
396
|
self.writer.write(
|
|
359
397
|
f"{ts} [PID:{process.pid}] [{pool_id}] [ID:{item_index}] still running "
|
|
360
|
-
f"{item_name} after {(counter * 0.1):.1f}s"
|
|
398
|
+
f"{item_name} after {(counter * 0.1):.1f}s",
|
|
399
|
+
level='debug'
|
|
361
400
|
)
|
|
362
401
|
ping_interval += 50
|
|
363
402
|
next_ping += ping_interval
|
pabot/__init__.py
CHANGED
pabot/arguments.py
CHANGED
|
@@ -174,6 +174,7 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
|
|
|
174
174
|
"shardcount": 1,
|
|
175
175
|
"chunk": False,
|
|
176
176
|
"no-rebot": False,
|
|
177
|
+
"pabotconsole": "verbose",
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
# Arguments that are flags (boolean)
|
|
@@ -200,6 +201,7 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
|
|
|
200
201
|
"suitesfrom": str,
|
|
201
202
|
"artifacts": _parse_artifacts,
|
|
202
203
|
"shard": _parse_shard,
|
|
204
|
+
"pabotconsole": str,
|
|
203
205
|
}
|
|
204
206
|
|
|
205
207
|
argumentfiles = []
|
|
@@ -284,6 +286,17 @@ def _parse_pabot_args(args): # type: (List[str]) -> Tuple[List[str], Dict[str,
|
|
|
284
286
|
# move index past ordering args only
|
|
285
287
|
i += 2 + i_mode_offset + i_failure_offset
|
|
286
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
|
|
287
300
|
else:
|
|
288
301
|
value = value_args[arg_name](args[i + 1])
|
|
289
302
|
if arg_name == "shard":
|