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 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
- # Install SIGINT only in main thread
33
- try:
34
- if threading.current_thread() is threading.main_thread():
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
- color=Color.YELLOW
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 tests as failed due to timeout: {' '.join(dryrun_cmd)}"
376
+ f"Starting dry run to mark test as failed due to timeout: {' '.join(dryrun_cmd)}",
377
+ level='debug'
350
378
  )
351
- subprocess.run(dryrun_cmd, env=dry_run_env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
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
@@ -7,4 +7,4 @@ try:
7
7
  except ImportError:
8
8
  pass
9
9
 
10
- __version__ = "5.2.0b1"
10
+ __version__ = "5.2.0rc1"
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":