robotframework-pabot 5.2.0b1__py3-none-any.whl → 5.2.0rc2__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 +19 -1
- pabot/pabot.py +495 -223
- pabot/result_merger.py +13 -9
- pabot/writer.py +210 -52
- {robotframework_pabot-5.2.0b1.dist-info → robotframework_pabot-5.2.0rc2.dist-info}/METADATA +30 -7
- robotframework_pabot-5.2.0rc2.dist-info/RECORD +23 -0
- {robotframework_pabot-5.2.0b1.dist-info → robotframework_pabot-5.2.0rc2.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.0rc2.dist-info}/entry_points.txt +0 -0
- {robotframework_pabot-5.2.0b1.dist-info → robotframework_pabot-5.2.0rc2.dist-info}/top_level.txt +0 -0
pabot/pabot.py
CHANGED
|
@@ -84,7 +84,7 @@ from .execution_items import (
|
|
|
84
84
|
create_dependency_tree,
|
|
85
85
|
)
|
|
86
86
|
from .result_merger import merge
|
|
87
|
-
from .writer import get_writer
|
|
87
|
+
from .writer import get_writer, get_stdout_writer, get_stderr_writer, ThreadSafeWriter, MessageWriter
|
|
88
88
|
|
|
89
89
|
try:
|
|
90
90
|
import queue # type: ignore
|
|
@@ -105,18 +105,26 @@ except ImportError:
|
|
|
105
105
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
106
106
|
|
|
107
107
|
CTRL_C_PRESSED = False
|
|
108
|
-
#MESSAGE_QUEUE = queue.Queue()
|
|
109
|
-
EXECUTION_POOL_IDS = [] # type: List[int]
|
|
110
|
-
EXECUTION_POOL_ID_LOCK = threading.Lock()
|
|
111
|
-
POPEN_LOCK = threading.Lock()
|
|
112
108
|
_PABOTLIBURI = "127.0.0.1:8270"
|
|
113
109
|
_PABOTLIBPROCESS = None # type: Optional[subprocess.Popen]
|
|
110
|
+
_PABOTWRITER = None # type: Optional[MessageWriter]
|
|
111
|
+
_PABOTLIBTHREAD = None # type: Optional[threading.Thread]
|
|
114
112
|
_NUMBER_OF_ITEMS_TO_BE_EXECUTED = 0
|
|
115
113
|
_ABNORMAL_EXIT_HAPPENED = False
|
|
114
|
+
_PABOTCONSOLE = "verbose" # type: str
|
|
115
|
+
_USE_USER_COMMAND = False
|
|
116
116
|
|
|
117
117
|
_COMPLETED_LOCK = threading.Lock()
|
|
118
118
|
_NOT_COMPLETED_INDEXES = [] # type: List[int]
|
|
119
119
|
|
|
120
|
+
# Thread-local storage for tracking executor number assigned to each thread
|
|
121
|
+
_EXECUTOR_THREAD_LOCAL = threading.local()
|
|
122
|
+
# Next executor number to assign (incremented each time a task is submitted)
|
|
123
|
+
_EXECUTOR_COUNTER = 0
|
|
124
|
+
_EXECUTOR_COUNTER_LOCK = threading.Lock()
|
|
125
|
+
# Maximum number of executors (workers in the thread pool)
|
|
126
|
+
_MAX_EXECUTORS = 1
|
|
127
|
+
|
|
120
128
|
_ROBOT_EXTENSIONS = [
|
|
121
129
|
".html",
|
|
122
130
|
".htm",
|
|
@@ -205,15 +213,18 @@ def extract_section(lines, start_marker="<!-- START DOCSTRING -->", end_marker="
|
|
|
205
213
|
if end_marker in line:
|
|
206
214
|
break
|
|
207
215
|
if inside_section:
|
|
208
|
-
# Remove Markdown hyperlinks but keep text
|
|
209
|
-
line = re.sub(r'\[([^\]]+)\]\(https?://[^\)]+\)', r'\1', line)
|
|
210
|
-
# Remove Markdown section links but keep text
|
|
211
|
-
line = re.sub(r'\[([^\]]+)\]\(#[^\)]+\)', r'\1', line)
|
|
212
|
-
# Remove ** and backticks `
|
|
213
|
-
line = re.sub(r'(\*\*|`)', '', line)
|
|
214
216
|
extracted_lines.append(line)
|
|
215
217
|
|
|
216
|
-
|
|
218
|
+
result = "".join(extracted_lines)
|
|
219
|
+
|
|
220
|
+
# Remove Markdown hyperlinks but keep text
|
|
221
|
+
result = re.sub(r'\[([^\]]+)\]\(https?://[^\)]+\)', r'\1', result)
|
|
222
|
+
# Remove Markdown section links but keep text
|
|
223
|
+
result = re.sub(r'\[([^\]]+)\]\(#[^\)]+\)', r'\1', result)
|
|
224
|
+
# Remove ** and backticks `
|
|
225
|
+
result = re.sub(r'(\*\*|`)', '', result)
|
|
226
|
+
|
|
227
|
+
return result.strip()
|
|
217
228
|
|
|
218
229
|
|
|
219
230
|
class Color:
|
|
@@ -225,6 +236,32 @@ class Color:
|
|
|
225
236
|
YELLOW = "\033[93m"
|
|
226
237
|
|
|
227
238
|
|
|
239
|
+
def _get_next_executor_num():
|
|
240
|
+
"""Get the next executor number in round-robin fashion."""
|
|
241
|
+
global _EXECUTOR_COUNTER, _MAX_EXECUTORS
|
|
242
|
+
with _EXECUTOR_COUNTER_LOCK:
|
|
243
|
+
executor_num = _EXECUTOR_COUNTER % _MAX_EXECUTORS
|
|
244
|
+
_EXECUTOR_COUNTER += 1
|
|
245
|
+
return executor_num
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _set_executor_num(executor_num):
|
|
249
|
+
"""Set the executor number for the current thread."""
|
|
250
|
+
_EXECUTOR_THREAD_LOCAL.executor_num = executor_num
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _get_executor_num():
|
|
254
|
+
"""Get the executor number for the current thread."""
|
|
255
|
+
return getattr(_EXECUTOR_THREAD_LOCAL, 'executor_num', 0)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _execute_item_with_executor_tracking(item):
|
|
259
|
+
"""Wrapper to track executor number and call execute_and_wait_with."""
|
|
260
|
+
executor_num = _get_next_executor_num()
|
|
261
|
+
_set_executor_num(executor_num)
|
|
262
|
+
return execute_and_wait_with(item)
|
|
263
|
+
|
|
264
|
+
|
|
228
265
|
def execute_and_wait_with(item):
|
|
229
266
|
# type: ('QueueItem') -> int
|
|
230
267
|
global CTRL_C_PRESSED, _NUMBER_OF_ITEMS_TO_BE_EXECUTED
|
|
@@ -253,7 +290,7 @@ def execute_and_wait_with(item):
|
|
|
253
290
|
outs_dir,
|
|
254
291
|
name,
|
|
255
292
|
item.verbose,
|
|
256
|
-
|
|
293
|
+
_get_executor_num(),
|
|
257
294
|
caller_id,
|
|
258
295
|
item.index,
|
|
259
296
|
)
|
|
@@ -264,7 +301,7 @@ def execute_and_wait_with(item):
|
|
|
264
301
|
outs_dir,
|
|
265
302
|
name,
|
|
266
303
|
item.verbose,
|
|
267
|
-
|
|
304
|
+
_get_executor_num(),
|
|
268
305
|
caller_id,
|
|
269
306
|
item.index,
|
|
270
307
|
item.execution_item.type != "test",
|
|
@@ -272,10 +309,10 @@ def execute_and_wait_with(item):
|
|
|
272
309
|
sleep_before_start=item.sleep_before_start
|
|
273
310
|
)
|
|
274
311
|
outputxml_preprocessing(
|
|
275
|
-
item.options, outs_dir, name, item.verbose,
|
|
312
|
+
item.options, outs_dir, name, item.verbose, _get_executor_num(), caller_id, item.index
|
|
276
313
|
)
|
|
277
314
|
except:
|
|
278
|
-
_write(traceback.format_exc())
|
|
315
|
+
_write(traceback.format_exc(), level="error")
|
|
279
316
|
return rc
|
|
280
317
|
|
|
281
318
|
|
|
@@ -313,7 +350,7 @@ def _hived_execute(
|
|
|
313
350
|
try:
|
|
314
351
|
make_order(hive, " ".join(cmd), outs_dir)
|
|
315
352
|
except:
|
|
316
|
-
_write(traceback.format_exc())
|
|
353
|
+
_write(traceback.format_exc(), level="error")
|
|
317
354
|
if plib:
|
|
318
355
|
_increase_completed(plib, my_index)
|
|
319
356
|
|
|
@@ -336,45 +373,63 @@ def _try_execute_and_wait(
|
|
|
336
373
|
is_ignored = False
|
|
337
374
|
if _pabotlib_in_use():
|
|
338
375
|
plib = Remote(_PABOTLIBURI)
|
|
376
|
+
|
|
377
|
+
command_name = _get_command_name(run_cmd[0])
|
|
378
|
+
stdout_path = os.path.join(outs_dir, f"{command_name}_stdout.out")
|
|
379
|
+
stderr_path = os.path.join(outs_dir, f"{command_name}_stderr.out")
|
|
380
|
+
|
|
339
381
|
try:
|
|
340
|
-
with open(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
382
|
+
with open(stdout_path, "w", encoding="utf-8", buffering=1) as stdout, \
|
|
383
|
+
open(stderr_path, "w", encoding="utf-8", buffering=1) as stderr:
|
|
384
|
+
|
|
385
|
+
process, (rc, elapsed) = _run(
|
|
386
|
+
run_cmd,
|
|
387
|
+
run_options,
|
|
388
|
+
stderr,
|
|
389
|
+
stdout,
|
|
390
|
+
item_name,
|
|
391
|
+
verbose,
|
|
392
|
+
pool_id,
|
|
393
|
+
my_index,
|
|
394
|
+
outs_dir,
|
|
395
|
+
process_timeout,
|
|
396
|
+
sleep_before_start
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Ensure writing
|
|
400
|
+
stdout.flush()
|
|
401
|
+
stderr.flush()
|
|
402
|
+
os.fsync(stdout.fileno())
|
|
403
|
+
os.fsync(stderr.fileno())
|
|
404
|
+
|
|
405
|
+
if plib:
|
|
406
|
+
_increase_completed(plib, my_index)
|
|
407
|
+
is_ignored = _is_ignored(plib, caller_id)
|
|
408
|
+
|
|
409
|
+
# Thread-safe list append
|
|
410
|
+
_ALL_ELAPSED.append(elapsed)
|
|
411
|
+
|
|
412
|
+
_result_to_stdout(
|
|
413
|
+
elapsed=elapsed,
|
|
414
|
+
is_ignored=is_ignored,
|
|
415
|
+
item_name=item_name,
|
|
416
|
+
my_index=my_index,
|
|
417
|
+
pool_id=pool_id,
|
|
418
|
+
process=process,
|
|
419
|
+
rc=rc,
|
|
420
|
+
stderr=stderr_path,
|
|
421
|
+
stdout=stdout_path,
|
|
422
|
+
verbose=verbose,
|
|
423
|
+
show_stdout_on_failure=show_stdout_on_failure,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if is_ignored and os.path.isdir(outs_dir):
|
|
427
|
+
_rmtree_with_path(outs_dir)
|
|
428
|
+
return rc
|
|
429
|
+
|
|
355
430
|
except:
|
|
356
|
-
_write(traceback.format_exc())
|
|
357
|
-
|
|
358
|
-
_increase_completed(plib, my_index)
|
|
359
|
-
is_ignored = _is_ignored(plib, caller_id)
|
|
360
|
-
# Thread-safe list append
|
|
361
|
-
_ALL_ELAPSED.append(elapsed)
|
|
362
|
-
_result_to_stdout(
|
|
363
|
-
elapsed,
|
|
364
|
-
is_ignored,
|
|
365
|
-
item_name,
|
|
366
|
-
my_index,
|
|
367
|
-
pool_id,
|
|
368
|
-
process,
|
|
369
|
-
rc,
|
|
370
|
-
stderr,
|
|
371
|
-
stdout,
|
|
372
|
-
verbose,
|
|
373
|
-
show_stdout_on_failure,
|
|
374
|
-
)
|
|
375
|
-
if is_ignored and os.path.isdir(outs_dir):
|
|
376
|
-
_rmtree_with_path(outs_dir)
|
|
377
|
-
return rc
|
|
431
|
+
_write(traceback.format_exc(), level="error")
|
|
432
|
+
return 252
|
|
378
433
|
|
|
379
434
|
|
|
380
435
|
def _result_to_stdout(
|
|
@@ -396,6 +451,7 @@ def _result_to_stdout(
|
|
|
396
451
|
pool_id,
|
|
397
452
|
my_index,
|
|
398
453
|
_execution_ignored_message(item_name, stdout, stderr, elapsed, verbose),
|
|
454
|
+
level="info_ignored",
|
|
399
455
|
)
|
|
400
456
|
elif rc != 0:
|
|
401
457
|
_write_with_id(
|
|
@@ -406,6 +462,7 @@ def _result_to_stdout(
|
|
|
406
462
|
item_name, stdout, stderr, rc, verbose or show_stdout_on_failure
|
|
407
463
|
),
|
|
408
464
|
Color.RED,
|
|
465
|
+
level="info_failed",
|
|
409
466
|
)
|
|
410
467
|
else:
|
|
411
468
|
_write_with_id(
|
|
@@ -414,6 +471,7 @@ def _result_to_stdout(
|
|
|
414
471
|
my_index,
|
|
415
472
|
_execution_passed_message(item_name, stdout, stderr, elapsed, verbose),
|
|
416
473
|
Color.GREEN,
|
|
474
|
+
level="info_passed",
|
|
417
475
|
)
|
|
418
476
|
|
|
419
477
|
|
|
@@ -489,25 +547,16 @@ def outputxml_preprocessing(options, outs_dir, item_name, verbose, pool_id, call
|
|
|
489
547
|
print(sys.exc_info())
|
|
490
548
|
|
|
491
549
|
|
|
492
|
-
def _write_with_id(process, pool_id, item_index, message, color=None, timestamp=None):
|
|
550
|
+
def _write_with_id(process, pool_id, item_index, message, color=None, timestamp=None, level="debug"):
|
|
493
551
|
timestamp = timestamp or datetime.datetime.now()
|
|
494
552
|
_write(
|
|
495
553
|
"%s [PID:%s] [%s] [ID:%s] %s"
|
|
496
554
|
% (timestamp, process.pid, pool_id, item_index, message),
|
|
497
555
|
color,
|
|
556
|
+
level=level,
|
|
498
557
|
)
|
|
499
558
|
|
|
500
559
|
|
|
501
|
-
def _make_id(): # type: () -> int
|
|
502
|
-
global EXECUTION_POOL_IDS, EXECUTION_POOL_ID_LOCK
|
|
503
|
-
thread_id = threading.current_thread().ident
|
|
504
|
-
assert thread_id is not None
|
|
505
|
-
with EXECUTION_POOL_ID_LOCK:
|
|
506
|
-
if thread_id not in EXECUTION_POOL_IDS:
|
|
507
|
-
EXECUTION_POOL_IDS += [thread_id]
|
|
508
|
-
return EXECUTION_POOL_IDS.index(thread_id)
|
|
509
|
-
|
|
510
|
-
|
|
511
560
|
def _increase_completed(plib, my_index):
|
|
512
561
|
# type: (Remote, int) -> None
|
|
513
562
|
global _COMPLETED_LOCK, _NOT_COMPLETED_INDEXES
|
|
@@ -577,7 +626,7 @@ def _run(
|
|
|
577
626
|
_write(f"{timestamp} [{pool_id}] [ID:{item_index}] SLEEPING {sleep_before_start} SECONDS BEFORE STARTING {item_name}")
|
|
578
627
|
time.sleep(sleep_before_start)
|
|
579
628
|
|
|
580
|
-
command_name = run_command[
|
|
629
|
+
command_name = _get_command_name(run_command[0])
|
|
581
630
|
argfile_path = os.path.join(outs_dir, f"{command_name}_argfile.txt")
|
|
582
631
|
_write_internal_argument_file(run_options, filename=argfile_path)
|
|
583
632
|
|
|
@@ -608,11 +657,11 @@ def _run(
|
|
|
608
657
|
|
|
609
658
|
def _read_file(file_handle):
|
|
610
659
|
try:
|
|
611
|
-
with open(file_handle
|
|
660
|
+
with open(file_handle, "r") as content_file:
|
|
612
661
|
content = content_file.read()
|
|
613
662
|
return content
|
|
614
|
-
except:
|
|
615
|
-
return "Unable to read file %s" % file_handle
|
|
663
|
+
except Exception as e:
|
|
664
|
+
return "Unable to read file %s, error: %s" % (os.path.abspath(file_handle), e)
|
|
616
665
|
|
|
617
666
|
|
|
618
667
|
def _execution_failed_message(suite_name, stdout, stderr, rc, verbose):
|
|
@@ -679,7 +728,7 @@ def _options_for_executor(
|
|
|
679
728
|
# Prevent multiple appending of PABOTLIBURI variable setting
|
|
680
729
|
if pabotLibURIVar not in options["variable"]:
|
|
681
730
|
options["variable"].append(pabotLibURIVar)
|
|
682
|
-
pabotExecutionPoolId = "PABOTEXECUTIONPOOLID:%d" %
|
|
731
|
+
pabotExecutionPoolId = "PABOTEXECUTIONPOOLID:%d" % _get_executor_num()
|
|
683
732
|
if pabotExecutionPoolId not in options["variable"]:
|
|
684
733
|
options["variable"].append(pabotExecutionPoolId)
|
|
685
734
|
pabotIsLast = "PABOTISLASTEXECUTIONINPOOL:%s" % ("1" if is_last else "0")
|
|
@@ -702,7 +751,7 @@ def _options_for_executor(
|
|
|
702
751
|
del options["include"]
|
|
703
752
|
if skip:
|
|
704
753
|
this_dir = os.path.dirname(os.path.abspath(__file__))
|
|
705
|
-
listener_path = os.path.join(this_dir, "skip_listener.py")
|
|
754
|
+
listener_path = os.path.join(this_dir, "listener", "skip_listener.py")
|
|
706
755
|
options["dryrun"] = True
|
|
707
756
|
options["listener"].append(listener_path)
|
|
708
757
|
return _set_terminal_coloring_options(options)
|
|
@@ -1206,7 +1255,7 @@ def store_suite_names(hashes, suite_names):
|
|
|
1206
1255
|
_write(
|
|
1207
1256
|
"[ "
|
|
1208
1257
|
+ _wrap_with(Color.YELLOW, "WARNING")
|
|
1209
|
-
+ " ]: storing .pabotsuitenames failed"
|
|
1258
|
+
+ " ]: storing .pabotsuitenames failed", level="warning",
|
|
1210
1259
|
)
|
|
1211
1260
|
|
|
1212
1261
|
|
|
@@ -1236,6 +1285,7 @@ def generate_suite_names_with_builder(outs_dir, datasources, options):
|
|
|
1236
1285
|
if ROBOT_VERSION >= "6.1":
|
|
1237
1286
|
builder = TestSuiteBuilder(
|
|
1238
1287
|
included_extensions=settings.extension,
|
|
1288
|
+
included_files=settings.parse_include,
|
|
1239
1289
|
rpa=settings.rpa,
|
|
1240
1290
|
lang=opts.get("language"),
|
|
1241
1291
|
)
|
|
@@ -1271,13 +1321,13 @@ def generate_suite_names_with_builder(outs_dir, datasources, options):
|
|
|
1271
1321
|
if stdout_value:
|
|
1272
1322
|
_write(
|
|
1273
1323
|
"[STDOUT] from suite search:\n" + stdout_value + "[STDOUT] end",
|
|
1274
|
-
Color.YELLOW,
|
|
1324
|
+
Color.YELLOW, level="warning",
|
|
1275
1325
|
)
|
|
1276
1326
|
stderr_value = opts["stderr"].getvalue()
|
|
1277
1327
|
if stderr_value:
|
|
1278
1328
|
_write(
|
|
1279
1329
|
"[STDERR] from suite search:\n" + stderr_value + "[STDERR] end",
|
|
1280
|
-
Color.RED,
|
|
1330
|
+
Color.RED, level="error",
|
|
1281
1331
|
)
|
|
1282
1332
|
return list(sorted(set(suite_names)))
|
|
1283
1333
|
|
|
@@ -1333,7 +1383,7 @@ def _options_for_dryrun(options, outs_dir):
|
|
|
1333
1383
|
return _set_terminal_coloring_options(options)
|
|
1334
1384
|
|
|
1335
1385
|
|
|
1336
|
-
def _options_for_rebot(options, start_time_string, end_time_string):
|
|
1386
|
+
def _options_for_rebot(options, start_time_string, end_time_string, num_of_executions=0):
|
|
1337
1387
|
rebot_options = options.copy()
|
|
1338
1388
|
rebot_options["starttime"] = start_time_string
|
|
1339
1389
|
rebot_options["endtime"] = end_time_string
|
|
@@ -1342,6 +1392,12 @@ def _options_for_rebot(options, start_time_string, end_time_string):
|
|
|
1342
1392
|
rebot_options["test"] = []
|
|
1343
1393
|
rebot_options["exclude"] = []
|
|
1344
1394
|
rebot_options["include"] = []
|
|
1395
|
+
rebot_options["metadata"].append(
|
|
1396
|
+
f"Pabot Info:[https://pabot.org/?ref=log|Pabot] result from {num_of_executions} executions."
|
|
1397
|
+
)
|
|
1398
|
+
rebot_options["metadata"].append(
|
|
1399
|
+
f"Pabot Version:{PABOT_VERSION}"
|
|
1400
|
+
)
|
|
1345
1401
|
if rebot_options.get("runemptysuite"):
|
|
1346
1402
|
rebot_options["processemptysuite"] = True
|
|
1347
1403
|
if ROBOT_VERSION >= "2.8":
|
|
@@ -1387,9 +1443,11 @@ def _now():
|
|
|
1387
1443
|
def _print_elapsed(start, end):
|
|
1388
1444
|
_write(
|
|
1389
1445
|
"Total testing: "
|
|
1390
|
-
+ _time_string(sum(_ALL_ELAPSED))
|
|
1391
|
-
|
|
1392
|
-
|
|
1446
|
+
+ _time_string(sum(_ALL_ELAPSED)), level="info"
|
|
1447
|
+
)
|
|
1448
|
+
_write(
|
|
1449
|
+
"Elapsed time: "
|
|
1450
|
+
+ _time_string(end - start), level="info"
|
|
1393
1451
|
)
|
|
1394
1452
|
|
|
1395
1453
|
|
|
@@ -1416,6 +1474,13 @@ def _time_string(elapsed):
|
|
|
1416
1474
|
def keyboard_interrupt(*args):
|
|
1417
1475
|
global CTRL_C_PRESSED
|
|
1418
1476
|
CTRL_C_PRESSED = True
|
|
1477
|
+
# Notify ProcessManager to interrupt running processes
|
|
1478
|
+
if _PROCESS_MANAGER:
|
|
1479
|
+
_PROCESS_MANAGER.set_interrupted()
|
|
1480
|
+
if _PABOTWRITER:
|
|
1481
|
+
_write("[ INTERRUPT ] Ctrl+C pressed - initiating graceful shutdown...", Color.YELLOW, level="warning")
|
|
1482
|
+
else:
|
|
1483
|
+
print("[ INTERRUPT ] Ctrl+C pressed - initiating graceful shutdown...")
|
|
1419
1484
|
|
|
1420
1485
|
|
|
1421
1486
|
def _get_depends(item):
|
|
@@ -1423,32 +1488,90 @@ def _get_depends(item):
|
|
|
1423
1488
|
|
|
1424
1489
|
|
|
1425
1490
|
def _dependencies_satisfied(item, completed):
|
|
1426
|
-
|
|
1491
|
+
"""
|
|
1492
|
+
Check if all dependencies for an item are satisfied (completed).
|
|
1493
|
+
Uses unique names that include argfile_index when applicable.
|
|
1494
|
+
"""
|
|
1495
|
+
for dep in _get_depends(item):
|
|
1496
|
+
# Build unique name for dependency with same argfile_index as the item
|
|
1497
|
+
if hasattr(item, 'argfile_index') and item.argfile_index:
|
|
1498
|
+
# Item has an argfile index, so check for dependency with same argfile index
|
|
1499
|
+
dep_unique_name = f"{item.argfile_index}:{dep}"
|
|
1500
|
+
if dep_unique_name not in completed:
|
|
1501
|
+
return False
|
|
1502
|
+
else:
|
|
1503
|
+
# No argfile index (single argumentfile case)
|
|
1504
|
+
if dep not in completed:
|
|
1505
|
+
return False
|
|
1506
|
+
|
|
1507
|
+
return True
|
|
1427
1508
|
|
|
1428
1509
|
|
|
1429
1510
|
def _collect_transitive_dependents(failed_name, pending_items):
|
|
1430
1511
|
"""
|
|
1431
1512
|
Returns all pending items that (directly or indirectly) depend on failed_name.
|
|
1513
|
+
Handles both regular names and unique names (with argfile_index).
|
|
1514
|
+
|
|
1515
|
+
When failed_name is "1:Suite", it means Suite failed in argumentfile 1.
|
|
1516
|
+
We should only skip items in argumentfile 1 that depend on Suite,
|
|
1517
|
+
not items in other argumentfiles.
|
|
1432
1518
|
"""
|
|
1433
1519
|
to_skip = set()
|
|
1434
1520
|
queue = [failed_name]
|
|
1435
1521
|
|
|
1436
|
-
#
|
|
1522
|
+
# Extract argfile_index from failed_name if it has one
|
|
1523
|
+
if ":" in failed_name:
|
|
1524
|
+
argfile_index, base_name = failed_name.split(":", 1)
|
|
1525
|
+
else:
|
|
1526
|
+
argfile_index = ""
|
|
1527
|
+
base_name = failed_name
|
|
1528
|
+
|
|
1529
|
+
# Build dependency map: item unique name -> set of dependency base names
|
|
1437
1530
|
depends_map = {
|
|
1438
|
-
item
|
|
1531
|
+
_get_unique_execution_name(item): set(_get_depends(item))
|
|
1439
1532
|
for item in pending_items
|
|
1440
1533
|
}
|
|
1441
1534
|
|
|
1442
1535
|
while queue:
|
|
1443
1536
|
current = queue.pop(0)
|
|
1537
|
+
|
|
1538
|
+
# Extract base name from current (e.g., "1:Suite" -> "Suite")
|
|
1539
|
+
if ":" in current:
|
|
1540
|
+
current_argfile, current_base = current.split(":", 1)
|
|
1541
|
+
else:
|
|
1542
|
+
current_argfile = ""
|
|
1543
|
+
current_base = current
|
|
1544
|
+
|
|
1444
1545
|
for item_name, deps in depends_map.items():
|
|
1445
|
-
|
|
1546
|
+
# Only skip items from the same argumentfile
|
|
1547
|
+
# Check if item_name corresponds to the same argumentfile
|
|
1548
|
+
if ":" in item_name:
|
|
1549
|
+
item_argfile, _ = item_name.split(":", 1)
|
|
1550
|
+
else:
|
|
1551
|
+
item_argfile = ""
|
|
1552
|
+
|
|
1553
|
+
# Only process if same argumentfile
|
|
1554
|
+
if item_argfile != argfile_index:
|
|
1555
|
+
continue
|
|
1556
|
+
|
|
1557
|
+
# Check if this item depends on the current failed item
|
|
1558
|
+
if current_base in deps and item_name not in to_skip:
|
|
1446
1559
|
to_skip.add(item_name)
|
|
1447
1560
|
queue.append(item_name)
|
|
1448
1561
|
|
|
1449
1562
|
return to_skip
|
|
1450
1563
|
|
|
1451
1564
|
|
|
1565
|
+
def _get_unique_execution_name(item):
|
|
1566
|
+
"""
|
|
1567
|
+
Create a unique identifier for an execution item that includes argfile index.
|
|
1568
|
+
This ensures that the same test run with different argumentfiles are treated as distinct items.
|
|
1569
|
+
"""
|
|
1570
|
+
if item.argfile_index:
|
|
1571
|
+
return f"{item.argfile_index}:{item.execution_item.name}"
|
|
1572
|
+
return item.execution_item.name
|
|
1573
|
+
|
|
1574
|
+
|
|
1452
1575
|
def _parallel_execute_dynamic(
|
|
1453
1576
|
items,
|
|
1454
1577
|
processes,
|
|
@@ -1457,9 +1580,13 @@ def _parallel_execute_dynamic(
|
|
|
1457
1580
|
opts_for_run,
|
|
1458
1581
|
pabot_args,
|
|
1459
1582
|
):
|
|
1460
|
-
|
|
1583
|
+
# Signal handler is already set in main_program, no need to set it again
|
|
1584
|
+
# Just use the thread pool without managing signals
|
|
1585
|
+
global _MAX_EXECUTORS, _EXECUTOR_COUNTER
|
|
1461
1586
|
|
|
1462
1587
|
max_processes = processes or len(items)
|
|
1588
|
+
_MAX_EXECUTORS = max_processes
|
|
1589
|
+
_EXECUTOR_COUNTER = 0 # Reset executor counter for each parallel execution batch
|
|
1463
1590
|
pool = ThreadPool(max_processes)
|
|
1464
1591
|
|
|
1465
1592
|
pending = set(items)
|
|
@@ -1475,24 +1602,28 @@ def _parallel_execute_dynamic(
|
|
|
1475
1602
|
|
|
1476
1603
|
with lock:
|
|
1477
1604
|
running.pop(it, None)
|
|
1478
|
-
|
|
1605
|
+
unique_name = _get_unique_execution_name(it)
|
|
1606
|
+
completed.add(unique_name)
|
|
1479
1607
|
|
|
1480
1608
|
if rc != 0:
|
|
1481
|
-
failed.add(
|
|
1609
|
+
failed.add(unique_name)
|
|
1482
1610
|
|
|
1483
1611
|
if failure_policy == "skip":
|
|
1484
1612
|
to_skip_names = _collect_transitive_dependents(
|
|
1485
|
-
|
|
1613
|
+
unique_name,
|
|
1486
1614
|
pending,
|
|
1487
1615
|
)
|
|
1488
1616
|
|
|
1489
1617
|
for other in list(pending):
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1618
|
+
other_unique_name = _get_unique_execution_name(other)
|
|
1619
|
+
if other_unique_name in to_skip_names:
|
|
1620
|
+
# Only log skip once when first marking it as skipped
|
|
1621
|
+
if not other.skip:
|
|
1622
|
+
_write(
|
|
1623
|
+
f"Skipping '{other_unique_name}' because dependency "
|
|
1624
|
+
f"'{unique_name}' failed (transitive).",
|
|
1625
|
+
Color.YELLOW, level="debug"
|
|
1626
|
+
)
|
|
1496
1627
|
other.skip = True
|
|
1497
1628
|
|
|
1498
1629
|
try:
|
|
@@ -1508,7 +1639,7 @@ def _parallel_execute_dynamic(
|
|
|
1508
1639
|
pending.remove(item)
|
|
1509
1640
|
|
|
1510
1641
|
result = pool.apply_async(
|
|
1511
|
-
|
|
1642
|
+
_execute_item_with_executor_tracking,
|
|
1512
1643
|
(item,),
|
|
1513
1644
|
callback=lambda rc, it=item: on_complete(it, rc),
|
|
1514
1645
|
)
|
|
@@ -1526,15 +1657,19 @@ def _parallel_execute_dynamic(
|
|
|
1526
1657
|
|
|
1527
1658
|
finally:
|
|
1528
1659
|
pool.close()
|
|
1529
|
-
|
|
1660
|
+
# Signal handler was set in main_program and will be restored there
|
|
1530
1661
|
|
|
1531
1662
|
|
|
1532
1663
|
def _parallel_execute(
|
|
1533
1664
|
items, processes, datasources, outs_dir, opts_for_run, pabot_args
|
|
1534
1665
|
):
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1666
|
+
# Signal handler is already set in main_program, no need to set it again
|
|
1667
|
+
global _MAX_EXECUTORS, _EXECUTOR_COUNTER
|
|
1668
|
+
max_workers = len(items) if processes is None else processes
|
|
1669
|
+
_MAX_EXECUTORS = max_workers
|
|
1670
|
+
_EXECUTOR_COUNTER = 0 # Reset executor counter for each parallel execution batch
|
|
1671
|
+
pool = ThreadPool(max_workers)
|
|
1672
|
+
results = [pool.map_async(_execute_item_with_executor_tracking, items, 1)]
|
|
1538
1673
|
delayed_result_append = 0
|
|
1539
1674
|
new_items = []
|
|
1540
1675
|
while not all(result.ready() for result in results) or delayed_result_append > 0:
|
|
@@ -1554,10 +1689,10 @@ def _parallel_execute(
|
|
|
1554
1689
|
delayed_result_append = max(0, delayed_result_append - 1)
|
|
1555
1690
|
if new_items and delayed_result_append == 0:
|
|
1556
1691
|
_construct_last_levels([new_items])
|
|
1557
|
-
results.append(pool.map_async(
|
|
1692
|
+
results.append(pool.map_async(_execute_item_with_executor_tracking, new_items, 1))
|
|
1558
1693
|
new_items = []
|
|
1559
1694
|
pool.close()
|
|
1560
|
-
|
|
1695
|
+
# Signal handler will be restored in main_program's finally block
|
|
1561
1696
|
|
|
1562
1697
|
|
|
1563
1698
|
def _output_dir(options, cleanup=True):
|
|
@@ -1622,7 +1757,7 @@ def _copy_output_artifacts(options, timestamp_id=None, file_extensions=None, inc
|
|
|
1622
1757
|
return copied_artifacts
|
|
1623
1758
|
|
|
1624
1759
|
|
|
1625
|
-
def _check_pabot_results_for_missing_xml(base_dir, command_name, output_xml_name):
|
|
1760
|
+
def _check_pabot_results_for_missing_xml(base_dir, command_name, output_xml_name='output.xml'):
|
|
1626
1761
|
"""
|
|
1627
1762
|
Check for missing Robot Framework output XML files in pabot result directories,
|
|
1628
1763
|
taking into account the optional timestamp added by the -T option.
|
|
@@ -1648,14 +1783,18 @@ def _check_pabot_results_for_missing_xml(base_dir, command_name, output_xml_name
|
|
|
1648
1783
|
# Check if any file matches the expected XML name or timestamped variant
|
|
1649
1784
|
has_xml = any(pattern.match(fname) for fname in os.listdir(subdir_path))
|
|
1650
1785
|
if not has_xml:
|
|
1651
|
-
sanitized_cmd = command_name
|
|
1786
|
+
sanitized_cmd = _get_command_name(command_name)
|
|
1652
1787
|
missing.append(os.path.join(subdir_path, f"{sanitized_cmd}_stderr.out"))
|
|
1653
1788
|
break # only check immediate subdirectories
|
|
1654
1789
|
return missing
|
|
1655
1790
|
|
|
1656
1791
|
|
|
1792
|
+
def _get_command_name(command_name):
|
|
1793
|
+
global _USE_USER_COMMAND
|
|
1794
|
+
return "user_command" if _USE_USER_COMMAND else command_name
|
|
1795
|
+
|
|
1796
|
+
|
|
1657
1797
|
def _report_results(outs_dir, pabot_args, options, start_time_string, tests_root_name):
|
|
1658
|
-
output_xml_name = options.get("output") or "output.xml"
|
|
1659
1798
|
if "pythonpath" in options:
|
|
1660
1799
|
del options["pythonpath"]
|
|
1661
1800
|
if ROBOT_VERSION < "4.0":
|
|
@@ -1673,41 +1812,44 @@ def _report_results(outs_dir, pabot_args, options, start_time_string, tests_root
|
|
|
1673
1812
|
missing_outputs = []
|
|
1674
1813
|
if pabot_args["argumentfiles"]:
|
|
1675
1814
|
outputs = [] # type: List[str]
|
|
1815
|
+
total_num_of_executions = 0
|
|
1676
1816
|
for index, _ in pabot_args["argumentfiles"]:
|
|
1677
1817
|
copied_artifacts = _copy_output_artifacts(
|
|
1678
1818
|
options, _get_timestamp_id(start_time_string, pabot_args["artifactstimestamps"]), pabot_args["artifacts"], pabot_args["artifactsinsubfolders"], index
|
|
1679
1819
|
)
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
missing_outputs.extend(_check_pabot_results_for_missing_xml(os.path.join(outs_dir, index), pabot_args.get('command')
|
|
1820
|
+
output, num_of_executions = _merge_one_run(
|
|
1821
|
+
os.path.join(outs_dir, index),
|
|
1822
|
+
options,
|
|
1823
|
+
tests_root_name,
|
|
1824
|
+
stats,
|
|
1825
|
+
copied_artifacts,
|
|
1826
|
+
timestamp_id=_get_timestamp_id(start_time_string, pabot_args["artifactstimestamps"]),
|
|
1827
|
+
outputfile=os.path.join("pabot_results", "output%s.xml" % index),
|
|
1828
|
+
)
|
|
1829
|
+
outputs += [output]
|
|
1830
|
+
total_num_of_executions += num_of_executions
|
|
1831
|
+
missing_outputs.extend(_check_pabot_results_for_missing_xml(os.path.join(outs_dir, index), pabot_args.get('command')))
|
|
1692
1832
|
if "output" not in options:
|
|
1693
1833
|
options["output"] = "output.xml"
|
|
1694
1834
|
_write_stats(stats)
|
|
1695
|
-
|
|
1835
|
+
stdout_writer = get_stdout_writer()
|
|
1836
|
+
stderr_writer = get_stderr_writer(original_stderr_name='Internal Rebot')
|
|
1837
|
+
exit_code = rebot(*outputs, **_options_for_rebot(options, start_time_string, _now(), total_num_of_executions), stdout=stdout_writer, stderr=stderr_writer)
|
|
1696
1838
|
else:
|
|
1697
1839
|
exit_code = _report_results_for_one_run(
|
|
1698
1840
|
outs_dir, pabot_args, options, start_time_string, tests_root_name, stats
|
|
1699
1841
|
)
|
|
1700
|
-
missing_outputs.extend(_check_pabot_results_for_missing_xml(outs_dir, pabot_args.get('command')
|
|
1842
|
+
missing_outputs.extend(_check_pabot_results_for_missing_xml(outs_dir, pabot_args.get('command')))
|
|
1701
1843
|
if missing_outputs:
|
|
1702
1844
|
_write(("[ " + _wrap_with(Color.YELLOW, 'WARNING') + " ] "
|
|
1703
1845
|
"One or more subprocesses encountered an error and the "
|
|
1704
1846
|
"internal .xml files could not be generated. Please check the "
|
|
1705
|
-
"following stderr files to identify the cause:"))
|
|
1847
|
+
"following stderr files to identify the cause:"), level="warning")
|
|
1706
1848
|
for missing in missing_outputs:
|
|
1707
|
-
_write(repr(missing))
|
|
1849
|
+
_write(repr(missing), level="warning")
|
|
1708
1850
|
_write((f"[ " + _wrap_with(Color.RED, 'ERROR') + " ] "
|
|
1709
1851
|
"The output, log and report files produced by Pabot are "
|
|
1710
|
-
"incomplete and do not contain all test cases."))
|
|
1852
|
+
"incomplete and do not contain all test cases."), level="error")
|
|
1711
1853
|
return exit_code if not missing_outputs else 252
|
|
1712
1854
|
|
|
1713
1855
|
|
|
@@ -1717,18 +1859,18 @@ def _write_stats(stats):
|
|
|
1717
1859
|
al = stats["all"]
|
|
1718
1860
|
_write(
|
|
1719
1861
|
"%d critical tests, %d passed, %d failed"
|
|
1720
|
-
% (crit["total"], crit["passed"], crit["failed"])
|
|
1862
|
+
% (crit["total"], crit["passed"], crit["failed"]), level="info"
|
|
1721
1863
|
)
|
|
1722
1864
|
_write(
|
|
1723
1865
|
"%d tests total, %d passed, %d failed"
|
|
1724
|
-
% (al["total"], al["passed"], al["failed"])
|
|
1866
|
+
% (al["total"], al["passed"], al["failed"]), level="info"
|
|
1725
1867
|
)
|
|
1726
1868
|
else:
|
|
1727
1869
|
_write(
|
|
1728
1870
|
"%d tests, %d passed, %d failed, %d skipped."
|
|
1729
|
-
% (stats["total"], stats["passed"], stats["failed"], stats["skipped"])
|
|
1871
|
+
% (stats["total"], stats["passed"], stats["failed"], stats["skipped"]), level="info"
|
|
1730
1872
|
)
|
|
1731
|
-
_write("===================================================")
|
|
1873
|
+
_write("===================================================", level="info")
|
|
1732
1874
|
|
|
1733
1875
|
|
|
1734
1876
|
def add_timestamp_to_filename(file_path: str, timestamp: str) -> str:
|
|
@@ -1753,7 +1895,7 @@ def _report_results_for_one_run(
|
|
|
1753
1895
|
copied_artifacts = _copy_output_artifacts(
|
|
1754
1896
|
options, _get_timestamp_id(start_time_string, pabot_args["artifactstimestamps"]), pabot_args["artifacts"], pabot_args["artifactsinsubfolders"]
|
|
1755
1897
|
)
|
|
1756
|
-
output_path = _merge_one_run(
|
|
1898
|
+
output_path, num_of_executions = _merge_one_run(
|
|
1757
1899
|
outs_dir, options, tests_root_name, stats, copied_artifacts, _get_timestamp_id(start_time_string, pabot_args["artifactstimestamps"])
|
|
1758
1900
|
)
|
|
1759
1901
|
_write_stats(stats)
|
|
@@ -1770,9 +1912,12 @@ def _report_results_for_one_run(
|
|
|
1770
1912
|
"output"
|
|
1771
1913
|
] = output_path # REBOT will return error 252 if nothing is written
|
|
1772
1914
|
else:
|
|
1773
|
-
_write("Output: %s" % output_path)
|
|
1915
|
+
_write("Output: %s" % output_path, level="info")
|
|
1774
1916
|
options["output"] = None # Do not write output again with rebot
|
|
1775
|
-
|
|
1917
|
+
stdout_writer = get_stdout_writer()
|
|
1918
|
+
stderr_writer = get_stderr_writer(original_stderr_name="Internal Rebot")
|
|
1919
|
+
exit_code = rebot(output_path, **_options_for_rebot(options, start_time_string, ts, num_of_executions), stdout=stdout_writer, stderr=stderr_writer)
|
|
1920
|
+
return exit_code
|
|
1776
1921
|
|
|
1777
1922
|
|
|
1778
1923
|
def _merge_one_run(
|
|
@@ -1782,7 +1927,7 @@ def _merge_one_run(
|
|
|
1782
1927
|
output_path = os.path.abspath(
|
|
1783
1928
|
os.path.join(options.get("outputdir", "."), outputfile)
|
|
1784
1929
|
)
|
|
1785
|
-
filename =
|
|
1930
|
+
filename = "output.xml"
|
|
1786
1931
|
base_name, ext = os.path.splitext(filename)
|
|
1787
1932
|
# Glob all candidates
|
|
1788
1933
|
candidate_files = glob(os.path.join(outs_dir, "**", f"*{base_name}*{ext}"), recursive=True)
|
|
@@ -1794,8 +1939,8 @@ def _merge_one_run(
|
|
|
1794
1939
|
files = natsorted(files)
|
|
1795
1940
|
|
|
1796
1941
|
if not files:
|
|
1797
|
-
_write('
|
|
1798
|
-
return ""
|
|
1942
|
+
_write('[ WARNING ]: No output files in "%s"' % outs_dir, Color.YELLOW, level="warning")
|
|
1943
|
+
return "", 0
|
|
1799
1944
|
|
|
1800
1945
|
def invalid_xml_callback():
|
|
1801
1946
|
global _ABNORMAL_EXIT_HAPPENED
|
|
@@ -1811,7 +1956,7 @@ def _merge_one_run(
|
|
|
1811
1956
|
resu.save(output_path, legacy_output=True)
|
|
1812
1957
|
else:
|
|
1813
1958
|
resu.save(output_path)
|
|
1814
|
-
return output_path
|
|
1959
|
+
return output_path, len(files)
|
|
1815
1960
|
|
|
1816
1961
|
|
|
1817
1962
|
def _update_stats(result, stats):
|
|
@@ -1843,9 +1988,9 @@ def _glob_escape(pathname):
|
|
|
1843
1988
|
return drive + pathname
|
|
1844
1989
|
|
|
1845
1990
|
|
|
1846
|
-
def _write(message, color=None):
|
|
1991
|
+
def _write(message, color=None, level="debug"):
|
|
1847
1992
|
writer = get_writer()
|
|
1848
|
-
writer.write(message, color=color)
|
|
1993
|
+
writer.write(message, color=color, level=level)
|
|
1849
1994
|
|
|
1850
1995
|
|
|
1851
1996
|
def _wrap_with(color, message):
|
|
@@ -1877,11 +2022,11 @@ def _get_free_port():
|
|
|
1877
2022
|
return s.getsockname()[1]
|
|
1878
2023
|
|
|
1879
2024
|
|
|
1880
|
-
def _start_remote_library(pabot_args): # type: (dict) -> Optional[subprocess.Popen]
|
|
2025
|
+
def _start_remote_library(pabot_args): # type: (dict) -> Optional[Tuple[subprocess.Popen, threading.Thread]]
|
|
1881
2026
|
global _PABOTLIBURI
|
|
1882
2027
|
# If pabotlib is not enabled, do nothing
|
|
1883
2028
|
if not pabot_args.get("pabotlib"):
|
|
1884
|
-
return None
|
|
2029
|
+
return None, None
|
|
1885
2030
|
|
|
1886
2031
|
host = pabot_args.get("pabotlibhost", "127.0.0.1")
|
|
1887
2032
|
port = pabot_args.get("pabotlibport", 8270)
|
|
@@ -1891,7 +2036,7 @@ def _start_remote_library(pabot_args): # type: (dict) -> Optional[subprocess.Po
|
|
|
1891
2036
|
_write(
|
|
1892
2037
|
f"Warning: specified pabotlibport {port} is already in use. "
|
|
1893
2038
|
"A free port will be assigned automatically.",
|
|
1894
|
-
Color.YELLOW,
|
|
2039
|
+
Color.YELLOW, level="warning"
|
|
1895
2040
|
)
|
|
1896
2041
|
port = _get_free_port()
|
|
1897
2042
|
|
|
@@ -1905,7 +2050,7 @@ def _start_remote_library(pabot_args): # type: (dict) -> Optional[subprocess.Po
|
|
|
1905
2050
|
_write(
|
|
1906
2051
|
"Warning: specified resource file doesn't exist."
|
|
1907
2052
|
" Some tests may fail or continue forever.",
|
|
1908
|
-
Color.YELLOW,
|
|
2053
|
+
Color.YELLOW, level="warning"
|
|
1909
2054
|
)
|
|
1910
2055
|
resourcefile = ""
|
|
1911
2056
|
cmd = [
|
|
@@ -1915,28 +2060,82 @@ def _start_remote_library(pabot_args): # type: (dict) -> Optional[subprocess.Po
|
|
|
1915
2060
|
pabot_args["pabotlibhost"],
|
|
1916
2061
|
str(port),
|
|
1917
2062
|
]
|
|
1918
|
-
|
|
2063
|
+
# Start PabotLib in isolation so it doesn't receive CTRL+C when the main process is interrupted.
|
|
2064
|
+
# This allows graceful shutdown in finally block.
|
|
2065
|
+
kwargs = {
|
|
2066
|
+
"stdout": subprocess.PIPE,
|
|
2067
|
+
"stderr": subprocess.STDOUT,
|
|
2068
|
+
"text": True,
|
|
2069
|
+
"bufsize": 1,
|
|
2070
|
+
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
|
2071
|
+
}
|
|
2072
|
+
if sys.platform.startswith('win'):
|
|
2073
|
+
# Windows: use CREATE_NEW_PROCESS_GROUP
|
|
2074
|
+
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
2075
|
+
else:
|
|
2076
|
+
# Unix/Linux/macOS: use preexec_fn to create new session
|
|
2077
|
+
import os as os_module
|
|
2078
|
+
kwargs["preexec_fn"] = os_module.setsid
|
|
2079
|
+
|
|
2080
|
+
process = subprocess.Popen(cmd, **kwargs)
|
|
2081
|
+
|
|
2082
|
+
def _read_output(proc, writer):
|
|
2083
|
+
try:
|
|
2084
|
+
for line in proc.stdout:
|
|
2085
|
+
if line.strip(): # Skip empty lines
|
|
2086
|
+
try:
|
|
2087
|
+
writer.write(line.rstrip('\n') + '\n', level="info")
|
|
2088
|
+
writer.flush()
|
|
2089
|
+
except (RuntimeError, ValueError):
|
|
2090
|
+
# Writer/stdout already closed during shutdown
|
|
2091
|
+
break
|
|
2092
|
+
finally:
|
|
2093
|
+
try:
|
|
2094
|
+
proc.stdout.close()
|
|
2095
|
+
except Exception:
|
|
2096
|
+
pass
|
|
2097
|
+
|
|
2098
|
+
pabotlib_writer = ThreadSafeWriter(get_writer())
|
|
2099
|
+
thread = threading.Thread(
|
|
2100
|
+
target=_read_output,
|
|
2101
|
+
args=(process, pabotlib_writer),
|
|
2102
|
+
daemon=False, # Non-daemon so output is captured before exit
|
|
2103
|
+
)
|
|
2104
|
+
thread.start()
|
|
2105
|
+
|
|
2106
|
+
return process, thread
|
|
1919
2107
|
|
|
1920
2108
|
|
|
1921
2109
|
def _stop_remote_library(process): # type: (subprocess.Popen) -> None
|
|
1922
|
-
_write("Stopping PabotLib process")
|
|
2110
|
+
_write("Stopping PabotLib process", level="debug")
|
|
1923
2111
|
try:
|
|
1924
2112
|
remoteLib = Remote(_PABOTLIBURI)
|
|
1925
2113
|
remoteLib.run_keyword("stop_remote_libraries", [], {})
|
|
1926
2114
|
remoteLib.run_keyword("stop_remote_server", [], {})
|
|
1927
2115
|
except RuntimeError:
|
|
1928
|
-
_write("Could not connect to PabotLib - assuming stopped already")
|
|
1929
|
-
|
|
2116
|
+
_write("Could not connect to PabotLib - assuming stopped already", level="info")
|
|
2117
|
+
|
|
2118
|
+
# Always wait for graceful shutdown, regardless of remote connection status
|
|
1930
2119
|
i = 50
|
|
1931
2120
|
while i > 0 and process.poll() is None:
|
|
1932
2121
|
time.sleep(0.1)
|
|
1933
2122
|
i -= 1
|
|
1934
|
-
|
|
2123
|
+
|
|
2124
|
+
# If still running after remote stop attempt, terminate it
|
|
2125
|
+
if process.poll() is None:
|
|
1935
2126
|
_write(
|
|
1936
2127
|
"Could not stop PabotLib Process in 5 seconds " "- calling terminate",
|
|
1937
|
-
Color.YELLOW,
|
|
2128
|
+
Color.YELLOW, level="warning"
|
|
1938
2129
|
)
|
|
1939
2130
|
process.terminate()
|
|
2131
|
+
# Give it a moment to respond to SIGTERM
|
|
2132
|
+
time.sleep(0.5)
|
|
2133
|
+
if process.poll() is None:
|
|
2134
|
+
_write(
|
|
2135
|
+
"PabotLib Process did not respond to terminate - calling kill",
|
|
2136
|
+
Color.RED, level="error"
|
|
2137
|
+
)
|
|
2138
|
+
process.kill()
|
|
1940
2139
|
else:
|
|
1941
2140
|
_write("PabotLib process stopped")
|
|
1942
2141
|
|
|
@@ -1971,6 +2170,7 @@ class QueueItem(object):
|
|
|
1971
2170
|
outs_dir.encode("utf-8") if PY2 and is_unicode(outs_dir) else outs_dir
|
|
1972
2171
|
)
|
|
1973
2172
|
self.options = options
|
|
2173
|
+
self.options["output"] = "output.xml" # This is hardcoded output.xml inside pabot_results, not the final output
|
|
1974
2174
|
self.execution_item = (
|
|
1975
2175
|
execution_item if not hive else HivedItem(execution_item, hive)
|
|
1976
2176
|
)
|
|
@@ -2051,7 +2251,9 @@ def _create_execution_items_for_run(
|
|
|
2051
2251
|
return all_items
|
|
2052
2252
|
|
|
2053
2253
|
|
|
2054
|
-
def _create_items(datasources, opts_for_run, outs_dir, pabot_args, suite_group):
|
|
2254
|
+
def _create_items(datasources, opts_for_run, outs_dir, pabot_args, suite_group, argfile=None):
|
|
2255
|
+
# If argfile is provided, use only that one. Otherwise, loop through all argumentfiles.
|
|
2256
|
+
argumentfiles = [argfile] if argfile is not None else (pabot_args["argumentfiles"] or [("", None)])
|
|
2055
2257
|
return [
|
|
2056
2258
|
QueueItem(
|
|
2057
2259
|
datasources,
|
|
@@ -2060,13 +2262,13 @@ def _create_items(datasources, opts_for_run, outs_dir, pabot_args, suite_group):
|
|
|
2060
2262
|
suite,
|
|
2061
2263
|
pabot_args["command"],
|
|
2062
2264
|
pabot_args["verbose"],
|
|
2063
|
-
|
|
2265
|
+
af,
|
|
2064
2266
|
pabot_args.get("hive"),
|
|
2065
2267
|
pabot_args["processes"],
|
|
2066
2268
|
pabot_args["processtimeout"],
|
|
2067
2269
|
)
|
|
2068
2270
|
for suite in suite_group
|
|
2069
|
-
for
|
|
2271
|
+
for af in argumentfiles
|
|
2070
2272
|
]
|
|
2071
2273
|
|
|
2072
2274
|
|
|
@@ -2093,31 +2295,20 @@ def _create_execution_items_for_dry_run(
|
|
|
2093
2295
|
def _chunk_items(items, chunk_size):
|
|
2094
2296
|
for i in range(0, len(items), chunk_size):
|
|
2095
2297
|
chunked_items = items[i : i + chunk_size]
|
|
2096
|
-
|
|
2097
|
-
if not base_item:
|
|
2298
|
+
if not chunked_items:
|
|
2098
2299
|
continue
|
|
2300
|
+
# For TestItem execution items, yield each item separately
|
|
2301
|
+
# For Suite items, combine them into one item
|
|
2302
|
+
base_item = chunked_items[0]
|
|
2099
2303
|
if isinstance(base_item.execution_item, TestItem):
|
|
2100
2304
|
for item in chunked_items:
|
|
2101
|
-
|
|
2102
|
-
yield chunked_item
|
|
2305
|
+
yield item
|
|
2103
2306
|
else:
|
|
2307
|
+
# For suites, create a combined execution item with all suite execution items
|
|
2104
2308
|
execution_items = SuiteItems([item.execution_item for item in chunked_items])
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
def _queue_item(base_item, execution_items):
|
|
2110
|
-
return QueueItem(
|
|
2111
|
-
base_item.datasources,
|
|
2112
|
-
base_item.outs_dir,
|
|
2113
|
-
base_item.options,
|
|
2114
|
-
execution_items,
|
|
2115
|
-
base_item.command,
|
|
2116
|
-
base_item.verbose,
|
|
2117
|
-
(base_item.argfile_index, base_item.argfile),
|
|
2118
|
-
processes=base_item.processes,
|
|
2119
|
-
timeout=base_item.timeout,
|
|
2120
|
-
)
|
|
2309
|
+
# Reuse the base item but update its execution_item to the combined one
|
|
2310
|
+
base_item.execution_item = execution_items
|
|
2311
|
+
yield base_item
|
|
2121
2312
|
|
|
2122
2313
|
|
|
2123
2314
|
def _find_ending_level(name, group):
|
|
@@ -2184,10 +2375,10 @@ def _get_dynamically_created_execution_items(
|
|
|
2184
2375
|
new_suites = plib.run_keyword("get_added_suites", [], {})
|
|
2185
2376
|
except RuntimeError as err:
|
|
2186
2377
|
_write(
|
|
2187
|
-
"[
|
|
2378
|
+
"[ WARNING ] PabotLib unreachable during post-run phase, "
|
|
2188
2379
|
"assuming no dynamically added suites. "
|
|
2189
2380
|
"Original error: %s",
|
|
2190
|
-
err,
|
|
2381
|
+
err, level="warning"
|
|
2191
2382
|
)
|
|
2192
2383
|
new_suites = []
|
|
2193
2384
|
if len(new_suites) == 0:
|
|
@@ -2220,7 +2411,7 @@ def main(args=None):
|
|
|
2220
2411
|
|
|
2221
2412
|
|
|
2222
2413
|
def main_program(args):
|
|
2223
|
-
global _PABOTLIBPROCESS
|
|
2414
|
+
global _PABOTLIBPROCESS, _PABOTCONSOLE, _PABOTWRITER, _PABOTLIBTHREAD, _USE_USER_COMMAND
|
|
2224
2415
|
outs_dir = None
|
|
2225
2416
|
args = args or sys.argv[1:]
|
|
2226
2417
|
if len(args) == 0:
|
|
@@ -2234,14 +2425,17 @@ def main_program(args):
|
|
|
2234
2425
|
start_time = time.time()
|
|
2235
2426
|
start_time_string = _now()
|
|
2236
2427
|
# NOTE: timeout option
|
|
2428
|
+
original_signal_handler = signal.default_int_handler # Save default handler in case of early exit
|
|
2237
2429
|
try:
|
|
2238
2430
|
options, datasources, pabot_args, opts_for_run = parse_args(args)
|
|
2431
|
+
_USE_USER_COMMAND = pabot_args.get("use_user_command", False)
|
|
2432
|
+
_PABOTCONSOLE = pabot_args.get("pabotconsole", "verbose")
|
|
2239
2433
|
if pabot_args["help"]:
|
|
2240
2434
|
help_print = __doc__.replace(
|
|
2241
2435
|
"PLACEHOLDER_README.MD",
|
|
2242
2436
|
read_args_from_readme()
|
|
2243
2437
|
)
|
|
2244
|
-
print(help_print.replace("[PABOT_VERSION]", PABOT_VERSION))
|
|
2438
|
+
print(help_print.replace("[PABOT_VERSION]", PABOT_VERSION, 1))
|
|
2245
2439
|
return 251
|
|
2246
2440
|
if len(datasources) == 0:
|
|
2247
2441
|
print("[ " + _wrap_with(Color.RED, "ERROR") + " ]: No datasources given.")
|
|
@@ -2250,11 +2444,14 @@ def main_program(args):
|
|
|
2250
2444
|
outs_dir = _output_dir(options)
|
|
2251
2445
|
|
|
2252
2446
|
# These ensure MessageWriter and ProcessManager are ready before any parallel execution.
|
|
2253
|
-
|
|
2447
|
+
_PABOTWRITER = get_writer(log_dir=outs_dir, console_type=_PABOTCONSOLE)
|
|
2254
2448
|
_ensure_process_manager()
|
|
2255
|
-
_write(f"Initialized logging in {outs_dir}")
|
|
2449
|
+
_write(f"Initialized logging in {outs_dir}", level="info")
|
|
2256
2450
|
|
|
2257
|
-
_PABOTLIBPROCESS = _start_remote_library(pabot_args)
|
|
2451
|
+
_PABOTLIBPROCESS, _PABOTLIBTHREAD = _start_remote_library(pabot_args)
|
|
2452
|
+
# Set up signal handler to keep PabotLib alive during CTRL+C
|
|
2453
|
+
# This ensures graceful shutdown in the finally block
|
|
2454
|
+
original_signal_handler = signal.signal(signal.SIGINT, keyboard_interrupt)
|
|
2258
2455
|
if _pabotlib_in_use():
|
|
2259
2456
|
_initialize_queue_index()
|
|
2260
2457
|
|
|
@@ -2262,19 +2459,23 @@ def main_program(args):
|
|
|
2262
2459
|
if pabot_args["verbose"]:
|
|
2263
2460
|
_write("Suite names resolved in %s seconds" % str(time.time() - start_time))
|
|
2264
2461
|
if not suite_groups or suite_groups == [[]]:
|
|
2265
|
-
_write("No tests to execute")
|
|
2462
|
+
_write("No tests to execute", level="info")
|
|
2266
2463
|
if not options.get("runemptysuite", False):
|
|
2267
2464
|
return 252
|
|
2268
|
-
|
|
2465
|
+
|
|
2466
|
+
# Create execution items for all argumentfiles at once
|
|
2467
|
+
all_execution_items = _create_execution_items(
|
|
2269
2468
|
suite_groups, datasources, outs_dir, options, opts_for_run, pabot_args
|
|
2270
2469
|
)
|
|
2470
|
+
|
|
2471
|
+
# Now execute all items from all argumentfiles in parallel
|
|
2271
2472
|
if pabot_args.get("ordering", {}).get("mode") == "dynamic":
|
|
2272
2473
|
# flatten stages
|
|
2273
|
-
|
|
2274
|
-
for stage in
|
|
2275
|
-
|
|
2474
|
+
flattened_items = []
|
|
2475
|
+
for stage in all_execution_items:
|
|
2476
|
+
flattened_items.extend(stage)
|
|
2276
2477
|
_parallel_execute_dynamic(
|
|
2277
|
-
|
|
2478
|
+
flattened_items,
|
|
2278
2479
|
pabot_args["processes"],
|
|
2279
2480
|
datasources,
|
|
2280
2481
|
outs_dir,
|
|
@@ -2282,8 +2483,8 @@ def main_program(args):
|
|
|
2282
2483
|
pabot_args,
|
|
2283
2484
|
)
|
|
2284
2485
|
else:
|
|
2285
|
-
while
|
|
2286
|
-
items =
|
|
2486
|
+
while all_execution_items:
|
|
2487
|
+
items = all_execution_items.pop(0)
|
|
2287
2488
|
_parallel_execute(
|
|
2288
2489
|
items,
|
|
2289
2490
|
pabot_args["processes"],
|
|
@@ -2297,8 +2498,8 @@ def main_program(args):
|
|
|
2297
2498
|
"All tests were executed, but the --no-rebot argument was given, "
|
|
2298
2499
|
"so the results were not compiled, and no summary was generated. "
|
|
2299
2500
|
f"All results have been saved in the {outs_dir} folder."
|
|
2300
|
-
))
|
|
2301
|
-
_write("===================================================")
|
|
2501
|
+
), level="info")
|
|
2502
|
+
_write("===================================================", level="info")
|
|
2302
2503
|
return 253
|
|
2303
2504
|
result_code = _report_results(
|
|
2304
2505
|
outs_dir,
|
|
@@ -2307,67 +2508,138 @@ def main_program(args):
|
|
|
2307
2508
|
start_time_string,
|
|
2308
2509
|
_get_suite_root_name(suite_groups),
|
|
2309
2510
|
)
|
|
2511
|
+
# If CTRL+C was pressed during execution, raise KeyboardInterrupt now.
|
|
2512
|
+
# This can happen without previous errors if test are for example almost ready.
|
|
2513
|
+
if CTRL_C_PRESSED:
|
|
2514
|
+
raise KeyboardInterrupt()
|
|
2310
2515
|
return result_code if not _ABNORMAL_EXIT_HAPPENED else 252
|
|
2311
2516
|
except Information as i:
|
|
2312
2517
|
version_print = __doc__.replace("\nPLACEHOLDER_README.MD\n", "")
|
|
2313
2518
|
print(version_print.replace("[PABOT_VERSION]", PABOT_VERSION))
|
|
2314
|
-
|
|
2519
|
+
if _PABOTWRITER:
|
|
2520
|
+
_write(i.message, level="info")
|
|
2521
|
+
else:
|
|
2522
|
+
print(i.message)
|
|
2315
2523
|
return 251
|
|
2316
2524
|
except DataError as err:
|
|
2317
|
-
|
|
2525
|
+
if _PABOTWRITER:
|
|
2526
|
+
_write(err.message, Color.RED, level="error")
|
|
2527
|
+
else:
|
|
2528
|
+
print(err.message)
|
|
2318
2529
|
return 252
|
|
2319
|
-
except Exception:
|
|
2530
|
+
except (Exception, KeyboardInterrupt):
|
|
2320
2531
|
if not CTRL_C_PRESSED:
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2532
|
+
if _PABOTWRITER:
|
|
2533
|
+
_write("[ ERROR ] EXCEPTION RAISED DURING PABOT EXECUTION", Color.RED, level="error")
|
|
2534
|
+
_write(
|
|
2535
|
+
"[ ERROR ] PLEASE CONSIDER REPORTING THIS ISSUE TO https://github.com/mkorpela/pabot/issues",
|
|
2536
|
+
Color.RED, level="error"
|
|
2537
|
+
)
|
|
2538
|
+
_write("Pabot: %s" % PABOT_VERSION, level="info")
|
|
2539
|
+
_write("Python: %s" % sys.version, level="info")
|
|
2540
|
+
_write("Robot Framework: %s" % ROBOT_VERSION, level="info")
|
|
2541
|
+
else:
|
|
2542
|
+
print("[ ERROR ] EXCEPTION RAISED DURING PABOT EXECUTION")
|
|
2543
|
+
print("[ ERROR ] PLEASE CONSIDER REPORTING THIS ISSUE TO https://github.com/mkorpela/pabot/issues")
|
|
2544
|
+
print("Pabot: %s" % PABOT_VERSION)
|
|
2545
|
+
print("Python: %s" % sys.version)
|
|
2546
|
+
print("Robot Framework: %s" % ROBOT_VERSION)
|
|
2329
2547
|
import traceback
|
|
2330
2548
|
traceback.print_exc()
|
|
2331
|
-
|
|
2549
|
+
return 255
|
|
2332
2550
|
else:
|
|
2333
|
-
|
|
2334
|
-
|
|
2551
|
+
if _PABOTWRITER:
|
|
2552
|
+
_write("[ ERROR ] Execution stopped by user (Ctrl+C)", Color.RED, level="error")
|
|
2553
|
+
else:
|
|
2554
|
+
print("[ ERROR ] Execution stopped by user (Ctrl+C)")
|
|
2555
|
+
return 253
|
|
2335
2556
|
finally:
|
|
2336
|
-
|
|
2337
|
-
|
|
2557
|
+
if _PABOTWRITER:
|
|
2558
|
+
_write("Finalizing Pabot execution...", level="debug")
|
|
2559
|
+
else:
|
|
2560
|
+
print("Finalizing Pabot execution...")
|
|
2561
|
+
|
|
2562
|
+
# Restore original signal handler
|
|
2338
2563
|
try:
|
|
2339
|
-
|
|
2340
|
-
writer = get_writer(log_dir=outs_dir)
|
|
2564
|
+
signal.signal(signal.SIGINT, original_signal_handler)
|
|
2341
2565
|
except Exception as e:
|
|
2342
|
-
|
|
2343
|
-
|
|
2566
|
+
if _PABOTWRITER:
|
|
2567
|
+
_write(f"[ WARNING ] Could not restore signal handler: {e}", Color.YELLOW, level="warning")
|
|
2568
|
+
else:
|
|
2569
|
+
print(f"[ WARNING ] Could not restore signal handler: {e}")
|
|
2570
|
+
|
|
2571
|
+
# First: Terminate all test subprocesses gracefully
|
|
2572
|
+
# This must happen BEFORE stopping PabotLib so test processes
|
|
2573
|
+
# can cleanly disconnect from the remote library
|
|
2574
|
+
try:
|
|
2575
|
+
if _PROCESS_MANAGER:
|
|
2576
|
+
_PROCESS_MANAGER.terminate_all()
|
|
2577
|
+
except Exception as e:
|
|
2578
|
+
if _PABOTWRITER:
|
|
2579
|
+
_write(f"[ WARNING ] Could not terminate test subprocesses: {e}", Color.YELLOW, level="warning")
|
|
2580
|
+
else:
|
|
2581
|
+
print(f"[ WARNING ] Could not terminate test subprocesses: {e}")
|
|
2582
|
+
|
|
2583
|
+
# Then: Stop PabotLib after all test processes are gone
|
|
2584
|
+
# This ensures clean shutdown with no orphaned remote connections
|
|
2344
2585
|
try:
|
|
2345
2586
|
if _PABOTLIBPROCESS:
|
|
2346
2587
|
_stop_remote_library(_PABOTLIBPROCESS)
|
|
2347
2588
|
except Exception as e:
|
|
2348
|
-
if
|
|
2349
|
-
|
|
2589
|
+
if _PABOTWRITER:
|
|
2590
|
+
_write(f"[ WARNING ] Failed to stop remote library cleanly: {e}", Color.YELLOW, level="warning")
|
|
2350
2591
|
else:
|
|
2351
|
-
print(f"[
|
|
2352
|
-
|
|
2592
|
+
print(f"[ WARNING ] Failed to stop remote library cleanly: {e}")
|
|
2593
|
+
|
|
2594
|
+
# Print elapsed time
|
|
2353
2595
|
try:
|
|
2354
2596
|
_print_elapsed(start_time, time.time())
|
|
2355
2597
|
except Exception as e:
|
|
2356
|
-
if
|
|
2357
|
-
|
|
2598
|
+
if _PABOTWRITER:
|
|
2599
|
+
_write(f"[ WARNING ] Failed to print elapsed time: {e}", Color.YELLOW, level="warning")
|
|
2600
|
+
else:
|
|
2601
|
+
print(f"[ WARNING ] Failed to print elapsed time: {e}")
|
|
2602
|
+
|
|
2603
|
+
# Ensure pabotlib output reader thread has finished
|
|
2604
|
+
try:
|
|
2605
|
+
if _PABOTLIBTHREAD:
|
|
2606
|
+
_PABOTLIBTHREAD.join(timeout=5)
|
|
2607
|
+
if _PABOTLIBTHREAD.is_alive():
|
|
2608
|
+
if _PABOTWRITER:
|
|
2609
|
+
_write(
|
|
2610
|
+
"[ WARNING ] PabotLib output thread did not finish before timeout",
|
|
2611
|
+
Color.YELLOW,
|
|
2612
|
+
level="warning"
|
|
2613
|
+
)
|
|
2614
|
+
else:
|
|
2615
|
+
print("[ WARNING ] PabotLib output thread did not finish before timeout")
|
|
2616
|
+
except Exception as e:
|
|
2617
|
+
if _PABOTWRITER:
|
|
2618
|
+
_write(f"[ WARNING ] Could not join pabotlib output thread: {e}", Color.YELLOW, level="warning")
|
|
2358
2619
|
else:
|
|
2359
|
-
print(f"[
|
|
2620
|
+
print(f"[ WARNING ] Could not join pabotlib output thread: {e}")
|
|
2621
|
+
|
|
2360
2622
|
# Flush and stop writer
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2623
|
+
try:
|
|
2624
|
+
if _PABOTWRITER:
|
|
2625
|
+
_PABOTWRITER.write("Logs flushed successfully.", level="debug")
|
|
2626
|
+
_PABOTWRITER.flush()
|
|
2627
|
+
else:
|
|
2628
|
+
writer = get_writer()
|
|
2629
|
+
if writer:
|
|
2630
|
+
writer.flush()
|
|
2631
|
+
except Exception as e:
|
|
2632
|
+
print(f"[ WARNING ] Could not flush writer: {e}")
|
|
2633
|
+
|
|
2634
|
+
try:
|
|
2635
|
+
if _PABOTWRITER:
|
|
2636
|
+
_PABOTWRITER.stop()
|
|
2637
|
+
else:
|
|
2638
|
+
writer = get_writer()
|
|
2639
|
+
if writer:
|
|
2640
|
+
writer.stop()
|
|
2641
|
+
except Exception as e:
|
|
2642
|
+
print(f"[ WARNING ] Could not stop writer: {e}")
|
|
2371
2643
|
|
|
2372
2644
|
|
|
2373
2645
|
def _parse_ordering(filename): # type: (str) -> List[ExecutionItem]
|
|
@@ -2403,13 +2675,13 @@ def _check_ordering(ordering_file, suite_names): # type: (List[ExecutionItem],
|
|
|
2403
2675
|
duplicates.append(f"{item.type.title()} item: '{item.name}'")
|
|
2404
2676
|
suite_and_test_names.append(item.name)
|
|
2405
2677
|
if skipped_runnable_items:
|
|
2406
|
-
_write("Note: The ordering file contains test or suite items that are not included in the current test run. The following items will be ignored/skipped:")
|
|
2678
|
+
_write("Note: The ordering file contains test or suite items that are not included in the current test run. The following items will be ignored/skipped:", level="info")
|
|
2407
2679
|
for item in skipped_runnable_items:
|
|
2408
|
-
_write(f" - {item}")
|
|
2680
|
+
_write(f" - {item}", level="info")
|
|
2409
2681
|
if duplicates:
|
|
2410
|
-
_write("Note: The ordering file contains duplicate suite or test items. Only the first occurrence is taken into account. These are duplicates:")
|
|
2682
|
+
_write("Note: The ordering file contains duplicate suite or test items. Only the first occurrence is taken into account. These are duplicates:", level="info")
|
|
2411
2683
|
for item in duplicates:
|
|
2412
|
-
_write(f" - {item}")
|
|
2684
|
+
_write(f" - {item}", level="info")
|
|
2413
2685
|
|
|
2414
2686
|
|
|
2415
2687
|
def _group_suites(outs_dir, datasources, options, pabot_args):
|