pybuilder-integration 111__tar.gz → 113__tar.gz
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.
- {pybuilder_integration-111 → pybuilder_integration-113}/PKG-INFO +1 -1
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration/properties.py +2 -0
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration/tasks.py +355 -55
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration/tool_utility.py +6 -6
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration.egg-info/PKG-INFO +1 -1
- {pybuilder_integration-111 → pybuilder_integration-113}/setup.py +1 -1
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration/__init__.py +0 -0
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration/artifact_manager.py +0 -0
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration/cloudwatchlogs_utility.py +0 -0
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration/directory_utility.py +0 -0
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration/exec_utility.py +0 -0
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration.egg-info/SOURCES.txt +0 -0
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration.egg-info/dependency_links.txt +0 -0
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration.egg-info/namespace_packages.txt +0 -0
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration.egg-info/requires.txt +0 -0
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration.egg-info/top_level.txt +0 -0
- {pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration.egg-info/zip-safe +0 -0
- {pybuilder_integration-111 → pybuilder_integration-113}/setup.cfg +0 -0
|
@@ -22,5 +22,7 @@ TESTING_SCOPE = "testing_scope"
|
|
|
22
22
|
PARALLEL_VERIFY_ENVIRONMENT = "parallel_verify_environment"
|
|
23
23
|
PARALLEL_TAVERN_SUITES = "parallel_tavern_suites"
|
|
24
24
|
PARALLEL_TAVERN_SUITES_WORKERS = "parallel_tavern_suites_workers"
|
|
25
|
+
PARALLEL_CYPRESS_SUITES = "parallel_cypress_suites"
|
|
26
|
+
PARALLEL_CYPRESS_SUITES_WORKERS = "parallel_cypress_suites_workers"
|
|
25
27
|
SUBPROCESS_PROPERTIES_FILE_ENV = "PYBUILDER_INTEGRATION_PROPERTIES_FILE"
|
|
26
28
|
CYPRESS_CACHE_FOLDER = "cypress_cache_folder"
|
|
@@ -52,13 +52,23 @@ _VERIFY_ENVIRONMENT_SUBPROCESS_PROPERTIES = [
|
|
|
52
52
|
TESTING_SCOPE,
|
|
53
53
|
PARALLEL_TAVERN_SUITES,
|
|
54
54
|
PARALLEL_TAVERN_SUITES_WORKERS,
|
|
55
|
+
PARALLEL_CYPRESS_SUITES,
|
|
56
|
+
PARALLEL_CYPRESS_SUITES_WORKERS,
|
|
55
57
|
"abort_upload",
|
|
56
58
|
"record_cypress",
|
|
57
59
|
"verbose",
|
|
58
60
|
]
|
|
59
61
|
|
|
60
62
|
_TAVERN_INSTALL_LOCK = threading.Lock()
|
|
63
|
+
_CYPRESS_TARGET_COPY_LOCK = threading.Lock()
|
|
61
64
|
_DEFAULT_PARALLEL_TAVERN_SUITE_WORKERS = 8
|
|
65
|
+
_DEFAULT_PARALLEL_CYPRESS_SUITE_WORKERS = 4
|
|
66
|
+
_CYPRESS_XVFB_DISPLAY_START = 100
|
|
67
|
+
_CYPRESS_SPEC_SUFFIXES = (
|
|
68
|
+
".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".coffee", ".feature",
|
|
69
|
+
)
|
|
70
|
+
_CYPRESS_SPEC_SKIP_DIRS = {"node_modules", "dist", "target", "videos", "screenshots", ".git"}
|
|
71
|
+
_CYPRESS_PARALLEL_FAILURES_FILE = "cypress_parallel_failures.json"
|
|
62
72
|
|
|
63
73
|
def integration_artifact_push(project: Project, logger: Logger, reactor: Reactor):
|
|
64
74
|
logger.info("Starting upload of integration artifacts")
|
|
@@ -158,8 +168,8 @@ def _run_verify_environment_passes_in_subprocesses(project: Project, logger: Log
|
|
|
158
168
|
process.terminate()
|
|
159
169
|
process.wait()
|
|
160
170
|
_close_subprocess_log(process)
|
|
161
|
-
_merge_parallel_verify_environment_outputs(project, passes)
|
|
162
|
-
_write_cypress_summary(project, logger)
|
|
171
|
+
cypress_failures = _merge_parallel_verify_environment_outputs(project, passes) or []
|
|
172
|
+
_write_cypress_summary(project, logger, extra_failures=cypress_failures)
|
|
163
173
|
if os.path.exists(properties_file):
|
|
164
174
|
os.remove(properties_file)
|
|
165
175
|
|
|
@@ -377,6 +387,27 @@ def _first_child_cypress_log(stage_target):
|
|
|
377
387
|
return None
|
|
378
388
|
|
|
379
389
|
|
|
390
|
+
def _child_parallel_cypress_failures(stage_target, pass_name):
|
|
391
|
+
for source_root in _child_output_source_roots(stage_target):
|
|
392
|
+
candidate = os.path.join(
|
|
393
|
+
source_root, "logs", "integration", _CYPRESS_PARALLEL_FAILURES_FILE
|
|
394
|
+
)
|
|
395
|
+
if not os.path.isfile(candidate):
|
|
396
|
+
continue
|
|
397
|
+
try:
|
|
398
|
+
with open(candidate, encoding="utf-8") as failures_file:
|
|
399
|
+
failures = json.load(failures_file)
|
|
400
|
+
if not isinstance(failures, list) or not all(isinstance(failure, str) for failure in failures):
|
|
401
|
+
raise ValueError("expected a list of strings")
|
|
402
|
+
return [
|
|
403
|
+
f"{pass_name}: {_redact_secret_log_text(failure).strip()}"
|
|
404
|
+
for failure in failures
|
|
405
|
+
]
|
|
406
|
+
except (OSError, ValueError, TypeError, json.JSONDecodeError) as error:
|
|
407
|
+
return [f"{pass_name}: could not read Cypress parallel failure details: {error}"]
|
|
408
|
+
return []
|
|
409
|
+
|
|
410
|
+
|
|
380
411
|
def _copy_maybe_redacted_file(src, dst, *, follow_symlinks=True):
|
|
381
412
|
if os.path.splitext(src)[1].lower() in _REDACTED_COPY_EXTENSIONS:
|
|
382
413
|
_copy_redacted_text_file(src, dst)
|
|
@@ -580,8 +611,12 @@ def _merge_parallel_verify_environment_outputs(project: Project, passes):
|
|
|
580
611
|
}
|
|
581
612
|
parent_cypress_log = os.path.join(parent_logs_directory, "cypress_run.log")
|
|
582
613
|
has_parent_cypress_log = False
|
|
614
|
+
cypress_failures = []
|
|
583
615
|
for pass_config in passes:
|
|
584
616
|
stage_target = os.path.join(parent_target, "verify_environment", pass_config["name"])
|
|
617
|
+
cypress_failures.extend(
|
|
618
|
+
_child_parallel_cypress_failures(stage_target, pass_config["name"])
|
|
619
|
+
)
|
|
585
620
|
for source_root in _child_output_source_roots(stage_target):
|
|
586
621
|
for destination_kind, source_dirname in _CHILD_OUTPUT_SOURCES:
|
|
587
622
|
source = os.path.join(source_root, source_dirname, "integration")
|
|
@@ -598,6 +633,7 @@ def _merge_parallel_verify_environment_outputs(project: Project, passes):
|
|
|
598
633
|
if cypress_log:
|
|
599
634
|
_copy_cypress_log(cypress_log, parent_cypress_log, has_parent_cypress_log)
|
|
600
635
|
has_parent_cypress_log = True
|
|
636
|
+
return cypress_failures
|
|
601
637
|
|
|
602
638
|
|
|
603
639
|
def _copy_cypress_log(source, destination, append):
|
|
@@ -868,9 +904,11 @@ def _run_cypress_tests_in_dist_dir(dist_directory, latest, logger, project, reac
|
|
|
868
904
|
logger.debug(f"Run cypress tests in directory files: {os.listdir(cypress_test_path)} ")
|
|
869
905
|
logger.info(f"Found cypress tests - starting run latest: {latest}")
|
|
870
906
|
if latest:
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
907
|
+
suite_dirs = list(_iter_latest_cypress_suite_dirs(cypress_test_path, project))
|
|
908
|
+
if _get_bool_property(project, PARALLEL_CYPRESS_SUITES, False) and suite_dirs:
|
|
909
|
+
_run_cypress_suites_parallel(suite_dirs, logger, project, reactor)
|
|
910
|
+
else:
|
|
911
|
+
for directory_to_test in suite_dirs:
|
|
874
912
|
logger.info(f"Running {directory_to_test}")
|
|
875
913
|
_run_cypress_tests_in_directory(work_dir=directory_to_test,
|
|
876
914
|
logger=logger,
|
|
@@ -920,17 +958,42 @@ def _iter_latest_tavern_suite_dirs(tavern_test_path, project):
|
|
|
920
958
|
yield tavern_test_directory
|
|
921
959
|
|
|
922
960
|
|
|
923
|
-
def
|
|
924
|
-
|
|
961
|
+
def _iter_latest_cypress_suite_dirs(cypress_test_path, project):
|
|
962
|
+
for test_dir in sorted(os.listdir(cypress_test_path)):
|
|
963
|
+
directory_to_test = os.path.join(cypress_test_path, test_dir)
|
|
964
|
+
if os.path.isdir(directory_to_test) and _should_run_latest(test_dir, project):
|
|
965
|
+
yield directory_to_test
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _parallel_suite_workers(project, suite_count, property_name, default_workers):
|
|
969
|
+
configured = project.get_property(property_name, None)
|
|
925
970
|
if configured is None or configured == "":
|
|
926
|
-
return max(1, min(
|
|
971
|
+
return max(1, min(default_workers, suite_count))
|
|
927
972
|
try:
|
|
928
973
|
workers = int(configured)
|
|
929
974
|
except (TypeError, ValueError):
|
|
930
|
-
workers =
|
|
975
|
+
workers = default_workers
|
|
931
976
|
return max(1, min(workers, suite_count))
|
|
932
977
|
|
|
933
978
|
|
|
979
|
+
def _parallel_tavern_suite_workers(project, suite_count):
|
|
980
|
+
return _parallel_suite_workers(
|
|
981
|
+
project,
|
|
982
|
+
suite_count,
|
|
983
|
+
PARALLEL_TAVERN_SUITES_WORKERS,
|
|
984
|
+
_DEFAULT_PARALLEL_TAVERN_SUITE_WORKERS,
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def _parallel_cypress_suite_workers(project, suite_count):
|
|
989
|
+
return _parallel_suite_workers(
|
|
990
|
+
project,
|
|
991
|
+
suite_count,
|
|
992
|
+
PARALLEL_CYPRESS_SUITES_WORKERS,
|
|
993
|
+
_DEFAULT_PARALLEL_CYPRESS_SUITE_WORKERS,
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
|
|
934
997
|
def _run_tavern_suites_parallel(suite_dirs, logger, project, reactor):
|
|
935
998
|
prepare_reports_directory(project)
|
|
936
999
|
runnable = []
|
|
@@ -1078,6 +1141,198 @@ def _plugin_venv_requirements_fingerprint(fingerprint_by_dir):
|
|
|
1078
1141
|
return max(order, key=lambda fingerprint: (counts[fingerprint], -order.index(fingerprint)))
|
|
1079
1142
|
|
|
1080
1143
|
|
|
1144
|
+
def _run_cypress_suites_parallel(suite_dirs, logger, project, reactor):
|
|
1145
|
+
if not suite_dirs:
|
|
1146
|
+
return
|
|
1147
|
+
_write_parallel_cypress_failures(project, [])
|
|
1148
|
+
failures = []
|
|
1149
|
+
prepared = []
|
|
1150
|
+
for suite_dir in suite_dirs:
|
|
1151
|
+
role = os.path.basename(suite_dir)
|
|
1152
|
+
try:
|
|
1153
|
+
logger.info(f"Installing cypress dependencies for {role} before parallel run")
|
|
1154
|
+
_install_cypress_suite_dependencies(
|
|
1155
|
+
suite_dir, logger, project, reactor, suite_name=role
|
|
1156
|
+
)
|
|
1157
|
+
prepared.append(suite_dir)
|
|
1158
|
+
except Exception as error:
|
|
1159
|
+
failures.append(f"{role}: {error}")
|
|
1160
|
+
|
|
1161
|
+
work_items = _cypress_parallel_work_items(prepared, project)
|
|
1162
|
+
if not work_items:
|
|
1163
|
+
_merge_parallel_cypress_suite_logs(project, logger, [], extra_failures=failures)
|
|
1164
|
+
if failures:
|
|
1165
|
+
raise BuildFailedException(
|
|
1166
|
+
"Parallel cypress suites failed: " + "; ".join(sorted(failures))
|
|
1167
|
+
)
|
|
1168
|
+
return
|
|
1169
|
+
|
|
1170
|
+
if len(work_items) == 1 and not failures:
|
|
1171
|
+
suite_dir, name, spec_files = work_items[0]
|
|
1172
|
+
logger.info(f"Running {suite_dir}")
|
|
1173
|
+
try:
|
|
1174
|
+
_run_cypress_tests_in_directory(
|
|
1175
|
+
work_dir=suite_dir,
|
|
1176
|
+
logger=logger,
|
|
1177
|
+
project=project,
|
|
1178
|
+
reactor=reactor,
|
|
1179
|
+
write_summary=False,
|
|
1180
|
+
skip_install=True,
|
|
1181
|
+
)
|
|
1182
|
+
except Exception as error:
|
|
1183
|
+
failures.append(f"{name}: {error}")
|
|
1184
|
+
_write_parallel_cypress_failures(project, failures)
|
|
1185
|
+
_write_cypress_summary(project, logger, extra_failures=failures)
|
|
1186
|
+
raise
|
|
1187
|
+
_write_cypress_summary(project, logger)
|
|
1188
|
+
return
|
|
1189
|
+
|
|
1190
|
+
workers = _parallel_cypress_suite_workers(project, len(work_items))
|
|
1191
|
+
logger.info(
|
|
1192
|
+
f"Running {len(work_items)} cypress shards in parallel (workers={workers})"
|
|
1193
|
+
)
|
|
1194
|
+
xvfb_display_numbers = {
|
|
1195
|
+
name: _CYPRESS_XVFB_DISPLAY_START + index
|
|
1196
|
+
for index, (_suite_dir, name, _spec_files) in enumerate(work_items)
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
def run_item(work_item):
|
|
1200
|
+
suite_dir, name, spec_files = work_item
|
|
1201
|
+
logger.info(f"Running {suite_dir} [{name}]")
|
|
1202
|
+
try:
|
|
1203
|
+
_run_cypress_tests_in_directory(
|
|
1204
|
+
work_dir=suite_dir,
|
|
1205
|
+
logger=logger,
|
|
1206
|
+
project=project,
|
|
1207
|
+
reactor=reactor,
|
|
1208
|
+
suite_name=name,
|
|
1209
|
+
write_summary=False,
|
|
1210
|
+
spec_files=spec_files,
|
|
1211
|
+
skip_install=True,
|
|
1212
|
+
record=False,
|
|
1213
|
+
xvfb_display_num=xvfb_display_numbers[name],
|
|
1214
|
+
)
|
|
1215
|
+
return "ran"
|
|
1216
|
+
except BuildFailedException:
|
|
1217
|
+
if spec_files and _cypress_shard_had_no_matching_specs(project, name):
|
|
1218
|
+
logger.info(
|
|
1219
|
+
f"Cypress shard {name} had no specs after applying configured filters"
|
|
1220
|
+
)
|
|
1221
|
+
return "empty"
|
|
1222
|
+
raise
|
|
1223
|
+
|
|
1224
|
+
ran_items = 0
|
|
1225
|
+
empty_items = 0
|
|
1226
|
+
try:
|
|
1227
|
+
with ThreadPoolExecutor(max_workers=workers) as executor:
|
|
1228
|
+
futures = {executor.submit(run_item, item): item for item in work_items}
|
|
1229
|
+
for future in as_completed(futures):
|
|
1230
|
+
suite_dir, name, _spec_files = futures[future]
|
|
1231
|
+
try:
|
|
1232
|
+
result = future.result()
|
|
1233
|
+
if result == "empty":
|
|
1234
|
+
empty_items += 1
|
|
1235
|
+
else:
|
|
1236
|
+
ran_items += 1
|
|
1237
|
+
except Exception as error:
|
|
1238
|
+
failures.append(f"{name}: {error}")
|
|
1239
|
+
finally:
|
|
1240
|
+
if not failures and empty_items and not ran_items:
|
|
1241
|
+
failures.append(
|
|
1242
|
+
"No Cypress specs matched the configured specPattern and test filters"
|
|
1243
|
+
)
|
|
1244
|
+
shard_names = [name for _suite_dir, name, _spec_files in work_items]
|
|
1245
|
+
_merge_parallel_cypress_suite_logs(
|
|
1246
|
+
project, logger, shard_names, extra_failures=failures
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
if failures:
|
|
1250
|
+
raise BuildFailedException(
|
|
1251
|
+
"Parallel cypress suites failed: " + "; ".join(sorted(failures))
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def _cypress_parallel_work_items(suite_dirs, project):
|
|
1256
|
+
items = []
|
|
1257
|
+
for suite_dir in suite_dirs:
|
|
1258
|
+
role = os.path.basename(suite_dir)
|
|
1259
|
+
specs = _iter_cypress_spec_files(suite_dir)
|
|
1260
|
+
if len(specs) > 1:
|
|
1261
|
+
workers = _parallel_cypress_suite_workers(project, len(specs))
|
|
1262
|
+
chunks = _partition_cypress_specs(specs, workers)
|
|
1263
|
+
for index, chunk in enumerate(chunks):
|
|
1264
|
+
name = role if len(chunks) == 1 else f"{role}-{index}"
|
|
1265
|
+
items.append((suite_dir, name, chunk))
|
|
1266
|
+
else:
|
|
1267
|
+
items.append((suite_dir, role, None))
|
|
1268
|
+
return items
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
def _iter_cypress_spec_files(work_dir):
|
|
1272
|
+
specs = []
|
|
1273
|
+
for root, dirs, files in os.walk(work_dir):
|
|
1274
|
+
dirs[:] = [directory for directory in dirs if directory not in _CYPRESS_SPEC_SKIP_DIRS]
|
|
1275
|
+
for filename in files:
|
|
1276
|
+
lower = filename.lower()
|
|
1277
|
+
if any(lower.endswith(suffix) for suffix in _CYPRESS_SPEC_SUFFIXES):
|
|
1278
|
+
relative = os.path.relpath(os.path.join(root, filename), work_dir)
|
|
1279
|
+
specs.append(relative.replace(os.sep, "/"))
|
|
1280
|
+
return sorted(specs)
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def _cypress_shard_had_no_matching_specs(project, shard_name):
|
|
1284
|
+
log_path = os.path.join(
|
|
1285
|
+
prepare_logs_directory(project), f"cypress_run_{shard_name}.log"
|
|
1286
|
+
)
|
|
1287
|
+
for candidate in (log_path, f"{log_path}.err"):
|
|
1288
|
+
try:
|
|
1289
|
+
with open(candidate, encoding="utf-8", errors="replace") as log_file:
|
|
1290
|
+
output = log_file.read()
|
|
1291
|
+
except OSError:
|
|
1292
|
+
continue
|
|
1293
|
+
plain_output = _ANSI_ESCAPE.sub("", output)
|
|
1294
|
+
if re.search(r"no spec files were found|Specs:\s*0 found", plain_output, re.IGNORECASE):
|
|
1295
|
+
return True
|
|
1296
|
+
return False
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def _partition_cypress_specs(specs, worker_count):
|
|
1300
|
+
workers = max(1, min(worker_count, len(specs)))
|
|
1301
|
+
chunks = [[] for _ in range(workers)]
|
|
1302
|
+
for index, spec in enumerate(specs):
|
|
1303
|
+
chunks[index % workers].append(spec)
|
|
1304
|
+
return [chunk for chunk in chunks if chunk]
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def _merge_parallel_cypress_suite_logs(project, logger, shard_names, extra_failures=None):
|
|
1308
|
+
parent_cypress_log = os.path.join(prepare_logs_directory(project), "cypress_run.log")
|
|
1309
|
+
appended = False
|
|
1310
|
+
for name in sorted(shard_names):
|
|
1311
|
+
source = os.path.join(prepare_logs_directory(project), f"cypress_run_{name}.log")
|
|
1312
|
+
if os.path.isfile(source) and os.path.getsize(source) > 0:
|
|
1313
|
+
_copy_cypress_log(source, parent_cypress_log, appended)
|
|
1314
|
+
appended = True
|
|
1315
|
+
_write_parallel_cypress_failures(project, extra_failures or [])
|
|
1316
|
+
_write_cypress_summary(project, logger, extra_failures=extra_failures)
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
def _write_parallel_cypress_failures(project, failures):
|
|
1320
|
+
path = os.path.join(prepare_logs_directory(project), _CYPRESS_PARALLEL_FAILURES_FILE)
|
|
1321
|
+
if not failures:
|
|
1322
|
+
if os.path.isfile(path):
|
|
1323
|
+
os.remove(path)
|
|
1324
|
+
return
|
|
1325
|
+
redacted_failures = [
|
|
1326
|
+
_redact_secret_log_text(str(failure)).strip() for failure in failures
|
|
1327
|
+
]
|
|
1328
|
+
with os.fdopen(
|
|
1329
|
+
os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600),
|
|
1330
|
+
"w",
|
|
1331
|
+
encoding="utf-8",
|
|
1332
|
+
) as failures_file:
|
|
1333
|
+
json.dump(redacted_failures, failures_file)
|
|
1334
|
+
|
|
1335
|
+
|
|
1081
1336
|
def verify_cypress(project: Project, logger: Logger, reactor: Reactor):
|
|
1082
1337
|
# Get directories with test and cypress executable
|
|
1083
1338
|
work_dir = project.expand_path(f"${CYPRESS_TEST_DIR}")
|
|
@@ -1085,7 +1340,23 @@ def verify_cypress(project: Project, logger: Logger, reactor: Reactor):
|
|
|
1085
1340
|
package_artifacts(project, work_dir, "cypress", project.get_property(ROLE))
|
|
1086
1341
|
|
|
1087
1342
|
|
|
1088
|
-
def
|
|
1343
|
+
def _install_cypress_suite_dependencies(work_dir, logger, project, reactor, suite_name=None):
|
|
1344
|
+
npm_log_file = f"package_json_npm_install_{suite_name}.log" if suite_name else "package_json_npm_install.log"
|
|
1345
|
+
cypress_npm_log_file = f"cypress_npm_install_{suite_name}.log" if suite_name else "cypress_npm_install.log"
|
|
1346
|
+
package_json = os.path.join(work_dir, "package.json")
|
|
1347
|
+
if os.path.exists(package_json):
|
|
1348
|
+
logger.info("Found package.json installing dependencies")
|
|
1349
|
+
tool_utility.install_npm_dependencies(
|
|
1350
|
+
work_dir, project=project, logger=logger, reactor=reactor, log_file_name=npm_log_file
|
|
1351
|
+
)
|
|
1352
|
+
else:
|
|
1353
|
+
install_cypress(
|
|
1354
|
+
logger=logger, project=project, reactor=reactor, work_dir=work_dir, log_file_name=cypress_npm_log_file
|
|
1355
|
+
)
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
def _run_cypress_tests_in_directory(work_dir, logger, project, reactor: Reactor, suite_name=None, write_summary=True,
|
|
1359
|
+
spec_files=None, skip_install=False, record=None, xvfb_display_num=None):
|
|
1089
1360
|
total_time = Timer.start()
|
|
1090
1361
|
target_url = project.get_mandatory_property(INTEGRATION_TARGET_URL)
|
|
1091
1362
|
environment = project.get_mandatory_property(ENVIRONMENT)
|
|
@@ -1094,64 +1365,79 @@ def _run_cypress_tests_in_directory(work_dir, logger, project, reactor: Reactor)
|
|
|
1094
1365
|
return False
|
|
1095
1366
|
logger.info(f"Found {len(os.listdir(work_dir))} files in cypress test directory")
|
|
1096
1367
|
logger.debug(f"Files: {os.listdir(work_dir)} ")
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
if os.path.exists(package_json):
|
|
1100
|
-
logger.info("Found package.json installing dependencies")
|
|
1101
|
-
tool_utility.install_npm_dependencies(work_dir, project=project, logger=logger, reactor=reactor)
|
|
1102
|
-
else:
|
|
1103
|
-
install_cypress(logger=logger, project=project, reactor=reactor, work_dir=work_dir)
|
|
1368
|
+
if not skip_install:
|
|
1369
|
+
_install_cypress_suite_dependencies(work_dir, logger, project, reactor, suite_name=suite_name)
|
|
1104
1370
|
total_time.stop()
|
|
1105
1371
|
logger.info(f"Configured Cypress Environment: {total_time.get_millis()}")
|
|
1106
1372
|
total_time = Timer.start()
|
|
1107
1373
|
executable = os.path.join(work_dir, "node_modules/cypress/bin/cypress")
|
|
1108
|
-
|
|
1374
|
+
report_name = suite_name or os.path.basename(work_dir)
|
|
1375
|
+
results_file = os.path.join(prepare_reports_directory(project), f"cypress-{report_name}.out.xml")
|
|
1109
1376
|
# Run the actual tests against the baseURL provided by ${integration_target}
|
|
1110
1377
|
test_report_folder = directory_utility.prepare_reports_directory(project)
|
|
1378
|
+
videos_folder = f"{test_report_folder}/videos"
|
|
1379
|
+
screenshots_folder = f"{test_report_folder}/screenshots"
|
|
1380
|
+
if suite_name:
|
|
1381
|
+
videos_folder = f"{videos_folder}/{suite_name}"
|
|
1382
|
+
screenshots_folder = f"{screenshots_folder}/{suite_name}"
|
|
1111
1383
|
args = ["run", "--config",
|
|
1112
1384
|
f"baseUrl={target_url},"
|
|
1113
|
-
f"videosFolder={
|
|
1114
|
-
f"screenshotsFolder={
|
|
1385
|
+
f"videosFolder={videos_folder},"
|
|
1386
|
+
f"screenshotsFolder={screenshots_folder}",
|
|
1115
1387
|
"--reporter-options",
|
|
1116
1388
|
f"mochaFile={results_file}"]
|
|
1117
|
-
|
|
1389
|
+
should_record = _get_bool_property(project, "record_cypress", True) if record is None else bool(record)
|
|
1390
|
+
if should_record:
|
|
1118
1391
|
args.append('--record')
|
|
1392
|
+
if spec_files:
|
|
1393
|
+
args.extend(["--spec", ",".join(spec_files)])
|
|
1119
1394
|
_add_config_file(logger, project, args, environment, work_dir)
|
|
1120
|
-
environment_variables = project.get_property(ENVIRONMENT_VARIABLES, {})
|
|
1395
|
+
environment_variables = dict(project.get_property(ENVIRONMENT_VARIABLES, {}) or {})
|
|
1396
|
+
if xvfb_display_num is not None:
|
|
1397
|
+
environment_variables["XVFB_DISPLAY_NUM"] = str(xvfb_display_num)
|
|
1121
1398
|
logger.info(f"Running cypress on host: {target_url}")
|
|
1399
|
+
log_file_name = f"cypress_run_{suite_name}.log" if suite_name else "cypress_run.log"
|
|
1122
1400
|
try:
|
|
1123
1401
|
exec_utility.exec_command(command_name=executable, args=args,
|
|
1124
|
-
failure_message="Failed to execute cypress tests", log_file_name=
|
|
1402
|
+
failure_message="Failed to execute cypress tests", log_file_name=log_file_name,
|
|
1125
1403
|
project=project, reactor=reactor, logger=logger, working_dir=work_dir, report=False,
|
|
1126
1404
|
env_vars=environment_variables)
|
|
1127
1405
|
# workaround but cypress output are relative to location of cypress.json, so we need to collapse
|
|
1128
1406
|
if os.path.exists(f"{work_dir}/target"):
|
|
1129
|
-
|
|
1407
|
+
with _CYPRESS_TARGET_COPY_LOCK:
|
|
1408
|
+
shutil.copytree(f"{work_dir}/target", project.expand_path("$dir_target"), dirs_exist_ok=True)
|
|
1130
1409
|
finally:
|
|
1131
|
-
|
|
1410
|
+
if write_summary:
|
|
1411
|
+
_write_cypress_summary(project, logger)
|
|
1132
1412
|
total_time.stop()
|
|
1133
1413
|
logger.info(f"Ran Cypress Tests: {total_time.get_millis()}")
|
|
1134
1414
|
return True
|
|
1135
1415
|
|
|
1136
1416
|
|
|
1137
|
-
def _write_cypress_summary(project, logger):
|
|
1417
|
+
def _write_cypress_summary(project, logger, extra_failures=None):
|
|
1138
1418
|
"""Parse cypress_run.log and write a portable Markdown summary artifact.
|
|
1139
1419
|
|
|
1140
|
-
No-op when Cypress did not run (missing or empty cypress_run.log)
|
|
1141
|
-
|
|
1142
|
-
Parallel child subprocesses also skip it;
|
|
1143
|
-
after merging child Cypress logs.
|
|
1420
|
+
No-op when Cypress did not run (missing or empty cypress_run.log) and
|
|
1421
|
+
there are no extra_failures, so verify_environment jobs without Cypress
|
|
1422
|
+
do not emit a false warning. Parallel child subprocesses also skip it;
|
|
1423
|
+
the parent writes one summary after merging child Cypress logs.
|
|
1424
|
+
extra_failures keeps the summary from reporting PASS when a parallel
|
|
1425
|
+
shard failed before producing JUnit.
|
|
1144
1426
|
"""
|
|
1427
|
+
extra_failures = extra_failures or []
|
|
1145
1428
|
log_path = os.path.join(prepare_logs_directory(project), "cypress_run.log")
|
|
1146
1429
|
if os.environ.get(SUBPROCESS_PROPERTIES_FILE_ENV):
|
|
1147
1430
|
logger.debug("Skipping Cypress summary in verify_environment subprocess")
|
|
1148
1431
|
return None
|
|
1149
|
-
|
|
1432
|
+
has_log = os.path.isfile(log_path) and os.path.getsize(log_path) > 0
|
|
1433
|
+
if not has_log and not extra_failures:
|
|
1150
1434
|
logger.debug("Skipping Cypress summary: cypress_run.log not found")
|
|
1151
1435
|
return None
|
|
1152
1436
|
summary_path = os.path.join(prepare_reports_directory(project), "cypress-summary.md")
|
|
1153
1437
|
try:
|
|
1154
|
-
output = _redact_secret_log_text(
|
|
1438
|
+
output = _redact_secret_log_text(
|
|
1439
|
+
_build_cypress_summary(log_path if has_log else None, extra_failures)
|
|
1440
|
+
)
|
|
1155
1441
|
with os.fdopen(
|
|
1156
1442
|
os.open(summary_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600),
|
|
1157
1443
|
"w",
|
|
@@ -1172,33 +1458,40 @@ def _write_cypress_summary(project, logger):
|
|
|
1172
1458
|
logger.warn(f"[cypress_summary] failed to generate summary: {e}")
|
|
1173
1459
|
|
|
1174
1460
|
|
|
1175
|
-
def _build_cypress_summary(log_path):
|
|
1176
|
-
|
|
1177
|
-
return "## Cypress Test Results\n\n> warning `cypress_run.log` not found\n"
|
|
1178
|
-
|
|
1179
|
-
raw = open(log_path, errors="replace").read()
|
|
1180
|
-
clean = re.sub(r"\x1b\[[0-9;]*m", "", raw)
|
|
1181
|
-
|
|
1461
|
+
def _build_cypress_summary(log_path, extra_failures=None):
|
|
1462
|
+
extra_failures = extra_failures or []
|
|
1182
1463
|
total = failed = skipped = 0
|
|
1183
1464
|
failures = []
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1465
|
+
if log_path and os.path.exists(log_path):
|
|
1466
|
+
raw = open(log_path, errors="replace").read()
|
|
1467
|
+
clean = re.sub(r"\x1b\[[0-9;]*m", "", raw)
|
|
1468
|
+
|
|
1469
|
+
for xml_block in re.findall(r"<testsuites[\s\S]*?</testsuites>", clean):
|
|
1470
|
+
try:
|
|
1471
|
+
root = ET.fromstring(xml_block)
|
|
1472
|
+
total += int(root.get("tests", 0))
|
|
1473
|
+
failed += int(root.get("failures", 0))
|
|
1474
|
+
skipped += int(root.get("skipped", 0) or 0)
|
|
1475
|
+
for tc in root.iter("testcase"):
|
|
1476
|
+
failure = tc.find("failure")
|
|
1477
|
+
if failure is not None:
|
|
1478
|
+
suite = tc.get("classname", "").split(".")[-1]
|
|
1479
|
+
name = tc.get("name", "")
|
|
1480
|
+
msg = (failure.get("message") or failure.text or "").split("\n")[0][:120]
|
|
1481
|
+
failures.append((f"{suite} > {name}", msg))
|
|
1482
|
+
except (ET.ParseError, DefusedXmlException):
|
|
1483
|
+
pass
|
|
1484
|
+
elif not extra_failures:
|
|
1485
|
+
return "## Cypress Test Results\n\n> warning `cypress_run.log` not found\n"
|
|
1199
1486
|
|
|
1200
1487
|
passed = total - failed - skipped
|
|
1201
|
-
if
|
|
1488
|
+
if extra_failures:
|
|
1489
|
+
icon = "FAIL"
|
|
1490
|
+
if failed:
|
|
1491
|
+
label = f"{failed} test(s) failed; {len(extra_failures)} suite(s) failed"
|
|
1492
|
+
else:
|
|
1493
|
+
label = f"{len(extra_failures)} parallel suite(s) failed"
|
|
1494
|
+
elif total == 0:
|
|
1202
1495
|
icon = "WARN"
|
|
1203
1496
|
label = "No tests ran - check if Cypress found any spec files"
|
|
1204
1497
|
elif failed == 0:
|
|
@@ -1225,6 +1518,13 @@ def _build_cypress_summary(log_path):
|
|
|
1225
1518
|
if len(failures) > 20:
|
|
1226
1519
|
lines.append(f"\n_...and {len(failures) - 20} more_")
|
|
1227
1520
|
|
|
1521
|
+
if extra_failures:
|
|
1522
|
+
lines.append("\n### Parallel Suite Failures\n")
|
|
1523
|
+
for extra in extra_failures[:20]:
|
|
1524
|
+
lines.append(f"- **{extra}**")
|
|
1525
|
+
if len(extra_failures) > 20:
|
|
1526
|
+
lines.append(f"\n_...and {len(extra_failures) - 20} more_")
|
|
1527
|
+
|
|
1228
1528
|
return "\n".join(lines) + "\n"
|
|
1229
1529
|
|
|
1230
1530
|
|
{pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration/tool_utility.py
RENAMED
|
@@ -5,12 +5,12 @@ from pybuilder_integration.exec_utility import exec_command
|
|
|
5
5
|
from pybuilder_integration.properties import CYPRESS_CACHE_FOLDER
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def install_cypress(logger: Logger, project: Project, reactor: Reactor, work_dir):
|
|
8
|
+
def install_cypress(logger: Logger, project: Project, reactor: Reactor, work_dir, log_file_name=None):
|
|
9
9
|
_verify_npm(reactor)
|
|
10
10
|
logger.info(f"Ensuring cypress is installed")
|
|
11
11
|
exec_command('npm', ['install', "cypress"], f'Failed to install cypress - required for integration tests',
|
|
12
|
-
|
|
13
|
-
env_vars=_get_npm_env(project))
|
|
12
|
+
log_file_name or 'cypress_npm_install.log', project, reactor, logger, report=False,
|
|
13
|
+
working_dir=work_dir, env_vars=_get_npm_env(project))
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def _verify_npm(reactor):
|
|
@@ -18,12 +18,12 @@ def _verify_npm(reactor):
|
|
|
18
18
|
command_and_arguments=["npm", "--version"], prerequisite="npm", caller="integration_tests")
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def install_npm_dependencies(work_dir, project, logger, reactor):
|
|
21
|
+
def install_npm_dependencies(work_dir, project, logger, reactor, log_file_name=None):
|
|
22
22
|
_verify_npm(reactor)
|
|
23
23
|
install_command = 'ci' if _has_package_lock(work_dir) else 'install'
|
|
24
24
|
exec_command('npm', [install_command], f'Failed to install package.json - required for integration tests',
|
|
25
|
-
|
|
26
|
-
env_vars=_get_npm_env(project))
|
|
25
|
+
log_file_name or 'package_json_npm_install.log', project, reactor, logger, report=False,
|
|
26
|
+
working_dir=work_dir, env_vars=_get_npm_env(project))
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def _has_package_lock(work_dir):
|
|
@@ -21,7 +21,7 @@ class install(_install):
|
|
|
21
21
|
if __name__ == '__main__':
|
|
22
22
|
setup(
|
|
23
23
|
name = 'pybuilder-integration',
|
|
24
|
-
version = '
|
|
24
|
+
version = '113',
|
|
25
25
|
description = 'A pybuilder plugin that runs integration tests (Tavern & Cypress) against a target.',
|
|
26
26
|
long_description = 'A pybuilder plugin that runs integration tests against a target. This is intended to be a broader scope than unit-tests encompassing dependant functionality.',
|
|
27
27
|
long_description_content_type = None,
|
|
File without changes
|
{pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration/artifact_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration/directory_utility.py
RENAMED
|
File without changes
|
{pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration/exec_utility.py
RENAMED
|
File without changes
|
{pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration.egg-info/requires.txt
RENAMED
|
File without changes
|
{pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration.egg-info/top_level.txt
RENAMED
|
File without changes
|
{pybuilder_integration-111 → pybuilder_integration-113}/pybuilder_integration.egg-info/zip-safe
RENAMED
|
File without changes
|
|
File without changes
|