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