pybuilder-integration 109__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-109 → pybuilder_integration-112}/PKG-INFO +1 -1
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration/properties.py +2 -0
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration/tasks.py +456 -69
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration/tool_utility.py +6 -6
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration.egg-info/PKG-INFO +1 -1
- {pybuilder_integration-109 → pybuilder_integration-112}/setup.py +1 -1
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration/__init__.py +0 -0
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration/artifact_manager.py +0 -0
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration/cloudwatchlogs_utility.py +0 -0
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration/directory_utility.py +0 -0
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration/exec_utility.py +0 -0
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration.egg-info/SOURCES.txt +0 -0
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration.egg-info/dependency_links.txt +0 -0
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration.egg-info/namespace_packages.txt +0 -0
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration.egg-info/requires.txt +0 -0
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration.egg-info/top_level.txt +0 -0
- {pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration.egg-info/zip-safe +0 -0
- {pybuilder_integration-109 → 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"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import atexit
|
|
2
2
|
import errno
|
|
3
|
+
import hashlib
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
@@ -51,13 +52,22 @@ _VERIFY_ENVIRONMENT_SUBPROCESS_PROPERTIES = [
|
|
|
51
52
|
TESTING_SCOPE,
|
|
52
53
|
PARALLEL_TAVERN_SUITES,
|
|
53
54
|
PARALLEL_TAVERN_SUITES_WORKERS,
|
|
55
|
+
PARALLEL_CYPRESS_SUITES,
|
|
56
|
+
PARALLEL_CYPRESS_SUITES_WORKERS,
|
|
54
57
|
"abort_upload",
|
|
55
58
|
"record_cypress",
|
|
56
59
|
"verbose",
|
|
57
60
|
]
|
|
58
61
|
|
|
59
62
|
_TAVERN_INSTALL_LOCK = threading.Lock()
|
|
63
|
+
_CYPRESS_TARGET_COPY_LOCK = threading.Lock()
|
|
60
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"
|
|
61
71
|
|
|
62
72
|
def integration_artifact_push(project: Project, logger: Logger, reactor: Reactor):
|
|
63
73
|
logger.info("Starting upload of integration artifacts")
|
|
@@ -157,8 +167,8 @@ def _run_verify_environment_passes_in_subprocesses(project: Project, logger: Log
|
|
|
157
167
|
process.terminate()
|
|
158
168
|
process.wait()
|
|
159
169
|
_close_subprocess_log(process)
|
|
160
|
-
_merge_parallel_verify_environment_outputs(project, passes)
|
|
161
|
-
_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)
|
|
162
172
|
if os.path.exists(properties_file):
|
|
163
173
|
os.remove(properties_file)
|
|
164
174
|
|
|
@@ -376,6 +386,27 @@ def _first_child_cypress_log(stage_target):
|
|
|
376
386
|
return None
|
|
377
387
|
|
|
378
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
|
+
|
|
379
410
|
def _copy_maybe_redacted_file(src, dst, *, follow_symlinks=True):
|
|
380
411
|
if os.path.splitext(src)[1].lower() in _REDACTED_COPY_EXTENSIONS:
|
|
381
412
|
_copy_redacted_text_file(src, dst)
|
|
@@ -579,8 +610,12 @@ def _merge_parallel_verify_environment_outputs(project: Project, passes):
|
|
|
579
610
|
}
|
|
580
611
|
parent_cypress_log = os.path.join(parent_logs_directory, "cypress_run.log")
|
|
581
612
|
has_parent_cypress_log = False
|
|
613
|
+
cypress_failures = []
|
|
582
614
|
for pass_config in passes:
|
|
583
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
|
+
)
|
|
584
619
|
for source_root in _child_output_source_roots(stage_target):
|
|
585
620
|
for destination_kind, source_dirname in _CHILD_OUTPUT_SOURCES:
|
|
586
621
|
source = os.path.join(source_root, source_dirname, "integration")
|
|
@@ -597,6 +632,7 @@ def _merge_parallel_verify_environment_outputs(project: Project, passes):
|
|
|
597
632
|
if cypress_log:
|
|
598
633
|
_copy_cypress_log(cypress_log, parent_cypress_log, has_parent_cypress_log)
|
|
599
634
|
has_parent_cypress_log = True
|
|
635
|
+
return cypress_failures
|
|
600
636
|
|
|
601
637
|
|
|
602
638
|
def _copy_cypress_log(source, destination, append):
|
|
@@ -867,9 +903,11 @@ def _run_cypress_tests_in_dist_dir(dist_directory, latest, logger, project, reac
|
|
|
867
903
|
logger.debug(f"Run cypress tests in directory files: {os.listdir(cypress_test_path)} ")
|
|
868
904
|
logger.info(f"Found cypress tests - starting run latest: {latest}")
|
|
869
905
|
if latest:
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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:
|
|
873
911
|
logger.info(f"Running {directory_to_test}")
|
|
874
912
|
_run_cypress_tests_in_directory(work_dir=directory_to_test,
|
|
875
913
|
logger=logger,
|
|
@@ -919,17 +957,42 @@ def _iter_latest_tavern_suite_dirs(tavern_test_path, project):
|
|
|
919
957
|
yield tavern_test_directory
|
|
920
958
|
|
|
921
959
|
|
|
922
|
-
def
|
|
923
|
-
|
|
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)
|
|
924
969
|
if configured is None or configured == "":
|
|
925
|
-
return max(1, min(
|
|
970
|
+
return max(1, min(default_workers, suite_count))
|
|
926
971
|
try:
|
|
927
972
|
workers = int(configured)
|
|
928
973
|
except (TypeError, ValueError):
|
|
929
|
-
workers =
|
|
974
|
+
workers = default_workers
|
|
930
975
|
return max(1, min(workers, suite_count))
|
|
931
976
|
|
|
932
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
|
+
|
|
933
996
|
def _run_tavern_suites_parallel(suite_dirs, logger, project, reactor):
|
|
934
997
|
prepare_reports_directory(project)
|
|
935
998
|
runnable = []
|
|
@@ -957,19 +1020,33 @@ def _run_tavern_suites_parallel(suite_dirs, logger, project, reactor):
|
|
|
957
1020
|
)
|
|
958
1021
|
return
|
|
959
1022
|
|
|
960
|
-
suite_sites = {}
|
|
961
|
-
for tavern_test_directory in runnable:
|
|
962
|
-
suite_sites[tavern_test_directory] = _install_tavern_suite_site(
|
|
963
|
-
tavern_test_directory, logger, project, reactor
|
|
964
|
-
)
|
|
965
|
-
|
|
966
1023
|
workers = _parallel_tavern_suite_workers(project, len(runnable))
|
|
967
1024
|
logger.info(f"Running {len(runnable)} tavern suites in parallel (workers={workers})")
|
|
1025
|
+
fingerprint_by_dir = {suite_dir: _tavern_requirements_fingerprint(suite_dir) for suite_dir in runnable}
|
|
1026
|
+
plugin_fingerprint = _plugin_venv_requirements_fingerprint(fingerprint_by_dir)
|
|
1027
|
+
installer = _ParallelTavernRequirementInstaller(plugin_fingerprint)
|
|
1028
|
+
if plugin_fingerprint is not None:
|
|
1029
|
+
sample_dir = next(
|
|
1030
|
+
suite_dir for suite_dir, fingerprint in fingerprint_by_dir.items()
|
|
1031
|
+
if fingerprint == plugin_fingerprint
|
|
1032
|
+
)
|
|
1033
|
+
logger.info(
|
|
1034
|
+
f"Installing tavern requirements from {os.path.basename(sample_dir)} "
|
|
1035
|
+
f"into the plugin venv before parallel pytest"
|
|
1036
|
+
)
|
|
1037
|
+
installer.install(sample_dir, plugin_fingerprint, logger, project, reactor)
|
|
968
1038
|
failures = []
|
|
969
1039
|
|
|
970
1040
|
def run_suite(tavern_test_directory):
|
|
971
1041
|
role = os.path.basename(tavern_test_directory)
|
|
972
1042
|
logger.info(f"Running {tavern_test_directory}")
|
|
1043
|
+
site = installer.install(
|
|
1044
|
+
tavern_test_directory,
|
|
1045
|
+
fingerprint_by_dir[tavern_test_directory],
|
|
1046
|
+
logger,
|
|
1047
|
+
project,
|
|
1048
|
+
reactor,
|
|
1049
|
+
)
|
|
973
1050
|
_run_tavern_tests_in_dir(
|
|
974
1051
|
test_dir=tavern_test_directory,
|
|
975
1052
|
logger=logger,
|
|
@@ -978,7 +1055,7 @@ def _run_tavern_suites_parallel(suite_dirs, logger, project, reactor):
|
|
|
978
1055
|
role=role,
|
|
979
1056
|
isolated=True,
|
|
980
1057
|
skip_install=True,
|
|
981
|
-
extra_pythonpath=
|
|
1058
|
+
extra_pythonpath=site,
|
|
982
1059
|
)
|
|
983
1060
|
return role
|
|
984
1061
|
|
|
@@ -998,6 +1075,257 @@ def _run_tavern_suites_parallel(suite_dirs, logger, project, reactor):
|
|
|
998
1075
|
)
|
|
999
1076
|
|
|
1000
1077
|
|
|
1078
|
+
class _ParallelTavernRequirementInstaller:
|
|
1079
|
+
def __init__(self, plugin_venv_fingerprint):
|
|
1080
|
+
self._plugin_venv_fingerprint = plugin_venv_fingerprint
|
|
1081
|
+
self._lock = threading.Lock()
|
|
1082
|
+
self._events = {}
|
|
1083
|
+
self._results = {}
|
|
1084
|
+
self._errors = {}
|
|
1085
|
+
|
|
1086
|
+
def install(self, test_dir, fingerprint, logger, project, reactor):
|
|
1087
|
+
if fingerprint is None:
|
|
1088
|
+
return None
|
|
1089
|
+
with self._lock:
|
|
1090
|
+
if fingerprint in self._results:
|
|
1091
|
+
return self._results[fingerprint]
|
|
1092
|
+
if fingerprint in self._errors:
|
|
1093
|
+
raise self._errors[fingerprint]
|
|
1094
|
+
event = self._events.get(fingerprint)
|
|
1095
|
+
if event is None:
|
|
1096
|
+
event = threading.Event()
|
|
1097
|
+
self._events[fingerprint] = event
|
|
1098
|
+
leader = True
|
|
1099
|
+
else:
|
|
1100
|
+
leader = False
|
|
1101
|
+
if not leader:
|
|
1102
|
+
event.wait()
|
|
1103
|
+
with self._lock:
|
|
1104
|
+
if fingerprint in self._errors:
|
|
1105
|
+
raise self._errors[fingerprint]
|
|
1106
|
+
if fingerprint in self._results:
|
|
1107
|
+
return self._results[fingerprint]
|
|
1108
|
+
raise BuildFailedException(
|
|
1109
|
+
f"Tavern requirements install for {os.path.basename(test_dir)} finished without a result"
|
|
1110
|
+
)
|
|
1111
|
+
try:
|
|
1112
|
+
if fingerprint == self._plugin_venv_fingerprint:
|
|
1113
|
+
_install_tavern_requirements(test_dir, logger, project, reactor)
|
|
1114
|
+
site = None
|
|
1115
|
+
else:
|
|
1116
|
+
site = _install_tavern_suite_site(test_dir, logger, project, reactor, fingerprint)
|
|
1117
|
+
with self._lock:
|
|
1118
|
+
self._results[fingerprint] = site
|
|
1119
|
+
return site
|
|
1120
|
+
except Exception as error:
|
|
1121
|
+
with self._lock:
|
|
1122
|
+
self._errors[fingerprint] = error
|
|
1123
|
+
raise
|
|
1124
|
+
finally:
|
|
1125
|
+
event.set()
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def _plugin_venv_requirements_fingerprint(fingerprint_by_dir):
|
|
1129
|
+
counts = {}
|
|
1130
|
+
order = []
|
|
1131
|
+
for fingerprint in fingerprint_by_dir.values():
|
|
1132
|
+
if fingerprint is None:
|
|
1133
|
+
continue
|
|
1134
|
+
if fingerprint not in counts:
|
|
1135
|
+
order.append(fingerprint)
|
|
1136
|
+
counts[fingerprint] = 0
|
|
1137
|
+
counts[fingerprint] += 1
|
|
1138
|
+
if not order:
|
|
1139
|
+
return None
|
|
1140
|
+
return max(order, key=lambda fingerprint: (counts[fingerprint], -order.index(fingerprint)))
|
|
1141
|
+
|
|
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
|
+
|
|
1001
1329
|
def verify_cypress(project: Project, logger: Logger, reactor: Reactor):
|
|
1002
1330
|
# Get directories with test and cypress executable
|
|
1003
1331
|
work_dir = project.expand_path(f"${CYPRESS_TEST_DIR}")
|
|
@@ -1005,7 +1333,23 @@ def verify_cypress(project: Project, logger: Logger, reactor: Reactor):
|
|
|
1005
1333
|
package_artifacts(project, work_dir, "cypress", project.get_property(ROLE))
|
|
1006
1334
|
|
|
1007
1335
|
|
|
1008
|
-
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):
|
|
1009
1353
|
total_time = Timer.start()
|
|
1010
1354
|
target_url = project.get_mandatory_property(INTEGRATION_TARGET_URL)
|
|
1011
1355
|
environment = project.get_mandatory_property(ENVIRONMENT)
|
|
@@ -1014,64 +1358,77 @@ def _run_cypress_tests_in_directory(work_dir, logger, project, reactor: Reactor)
|
|
|
1014
1358
|
return False
|
|
1015
1359
|
logger.info(f"Found {len(os.listdir(work_dir))} files in cypress test directory")
|
|
1016
1360
|
logger.debug(f"Files: {os.listdir(work_dir)} ")
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
if os.path.exists(package_json):
|
|
1020
|
-
logger.info("Found package.json installing dependencies")
|
|
1021
|
-
tool_utility.install_npm_dependencies(work_dir, project=project, logger=logger, reactor=reactor)
|
|
1022
|
-
else:
|
|
1023
|
-
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)
|
|
1024
1363
|
total_time.stop()
|
|
1025
1364
|
logger.info(f"Configured Cypress Environment: {total_time.get_millis()}")
|
|
1026
1365
|
total_time = Timer.start()
|
|
1027
1366
|
executable = os.path.join(work_dir, "node_modules/cypress/bin/cypress")
|
|
1028
|
-
|
|
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")
|
|
1029
1369
|
# Run the actual tests against the baseURL provided by ${integration_target}
|
|
1030
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}"
|
|
1031
1376
|
args = ["run", "--config",
|
|
1032
1377
|
f"baseUrl={target_url},"
|
|
1033
|
-
f"videosFolder={
|
|
1034
|
-
f"screenshotsFolder={
|
|
1378
|
+
f"videosFolder={videos_folder},"
|
|
1379
|
+
f"screenshotsFolder={screenshots_folder}",
|
|
1035
1380
|
"--reporter-options",
|
|
1036
1381
|
f"mochaFile={results_file}"]
|
|
1037
|
-
|
|
1382
|
+
should_record = _get_bool_property(project, "record_cypress", True) if record is None else bool(record)
|
|
1383
|
+
if should_record:
|
|
1038
1384
|
args.append('--record')
|
|
1385
|
+
if spec_files:
|
|
1386
|
+
args.extend(["--spec", ",".join(spec_files)])
|
|
1039
1387
|
_add_config_file(logger, project, args, environment, work_dir)
|
|
1040
1388
|
environment_variables = project.get_property(ENVIRONMENT_VARIABLES, {})
|
|
1041
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"
|
|
1042
1391
|
try:
|
|
1043
1392
|
exec_utility.exec_command(command_name=executable, args=args,
|
|
1044
|
-
failure_message="Failed to execute cypress tests", log_file_name=
|
|
1393
|
+
failure_message="Failed to execute cypress tests", log_file_name=log_file_name,
|
|
1045
1394
|
project=project, reactor=reactor, logger=logger, working_dir=work_dir, report=False,
|
|
1046
1395
|
env_vars=environment_variables)
|
|
1047
1396
|
# workaround but cypress output are relative to location of cypress.json, so we need to collapse
|
|
1048
1397
|
if os.path.exists(f"{work_dir}/target"):
|
|
1049
|
-
|
|
1398
|
+
with _CYPRESS_TARGET_COPY_LOCK:
|
|
1399
|
+
shutil.copytree(f"{work_dir}/target", project.expand_path("$dir_target"), dirs_exist_ok=True)
|
|
1050
1400
|
finally:
|
|
1051
|
-
|
|
1401
|
+
if write_summary:
|
|
1402
|
+
_write_cypress_summary(project, logger)
|
|
1052
1403
|
total_time.stop()
|
|
1053
1404
|
logger.info(f"Ran Cypress Tests: {total_time.get_millis()}")
|
|
1054
1405
|
return True
|
|
1055
1406
|
|
|
1056
1407
|
|
|
1057
|
-
def _write_cypress_summary(project, logger):
|
|
1408
|
+
def _write_cypress_summary(project, logger, extra_failures=None):
|
|
1058
1409
|
"""Parse cypress_run.log and write a portable Markdown summary artifact.
|
|
1059
1410
|
|
|
1060
|
-
No-op when Cypress did not run (missing or empty cypress_run.log)
|
|
1061
|
-
|
|
1062
|
-
Parallel child subprocesses also skip it;
|
|
1063
|
-
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.
|
|
1064
1417
|
"""
|
|
1418
|
+
extra_failures = extra_failures or []
|
|
1065
1419
|
log_path = os.path.join(prepare_logs_directory(project), "cypress_run.log")
|
|
1066
1420
|
if os.environ.get(SUBPROCESS_PROPERTIES_FILE_ENV):
|
|
1067
1421
|
logger.debug("Skipping Cypress summary in verify_environment subprocess")
|
|
1068
1422
|
return None
|
|
1069
|
-
|
|
1423
|
+
has_log = os.path.isfile(log_path) and os.path.getsize(log_path) > 0
|
|
1424
|
+
if not has_log and not extra_failures:
|
|
1070
1425
|
logger.debug("Skipping Cypress summary: cypress_run.log not found")
|
|
1071
1426
|
return None
|
|
1072
1427
|
summary_path = os.path.join(prepare_reports_directory(project), "cypress-summary.md")
|
|
1073
1428
|
try:
|
|
1074
|
-
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
|
+
)
|
|
1075
1432
|
with os.fdopen(
|
|
1076
1433
|
os.open(summary_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600),
|
|
1077
1434
|
"w",
|
|
@@ -1092,33 +1449,40 @@ def _write_cypress_summary(project, logger):
|
|
|
1092
1449
|
logger.warn(f"[cypress_summary] failed to generate summary: {e}")
|
|
1093
1450
|
|
|
1094
1451
|
|
|
1095
|
-
def _build_cypress_summary(log_path):
|
|
1096
|
-
|
|
1097
|
-
return "## Cypress Test Results\n\n> warning `cypress_run.log` not found\n"
|
|
1098
|
-
|
|
1099
|
-
raw = open(log_path, errors="replace").read()
|
|
1100
|
-
clean = re.sub(r"\x1b\[[0-9;]*m", "", raw)
|
|
1101
|
-
|
|
1452
|
+
def _build_cypress_summary(log_path, extra_failures=None):
|
|
1453
|
+
extra_failures = extra_failures or []
|
|
1102
1454
|
total = failed = skipped = 0
|
|
1103
1455
|
failures = []
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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"
|
|
1119
1477
|
|
|
1120
1478
|
passed = total - failed - skipped
|
|
1121
|
-
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:
|
|
1122
1486
|
icon = "WARN"
|
|
1123
1487
|
label = "No tests ran - check if Cypress found any spec files"
|
|
1124
1488
|
elif failed == 0:
|
|
@@ -1145,6 +1509,13 @@ def _build_cypress_summary(log_path):
|
|
|
1145
1509
|
if len(failures) > 20:
|
|
1146
1510
|
lines.append(f"\n_...and {len(failures) - 20} more_")
|
|
1147
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
|
+
|
|
1148
1519
|
return "\n".join(lines) + "\n"
|
|
1149
1520
|
|
|
1150
1521
|
|
|
@@ -1227,10 +1598,18 @@ def _plugin_venv_python(reactor):
|
|
|
1227
1598
|
raise BuildFailedException("plugin venv python is missing or not executable")
|
|
1228
1599
|
|
|
1229
1600
|
|
|
1230
|
-
def
|
|
1601
|
+
def _tavern_requirements_fingerprint(test_dir):
|
|
1602
|
+
requirements_file = os.path.join(test_dir, "requirements.txt")
|
|
1603
|
+
if not os.path.isfile(requirements_file):
|
|
1604
|
+
return None
|
|
1605
|
+
with open(requirements_file, "rb") as requirements:
|
|
1606
|
+
return hashlib.sha256(requirements.read()).hexdigest()
|
|
1607
|
+
|
|
1608
|
+
|
|
1609
|
+
def _tavern_suite_site_directory(project, fingerprint):
|
|
1231
1610
|
root = project.expand_path("$dir_target/integration/tavern-sites")
|
|
1232
1611
|
directory_utility._ensure_directory_exists(root)
|
|
1233
|
-
site = os.path.join(root,
|
|
1612
|
+
site = os.path.join(root, fingerprint)
|
|
1234
1613
|
if os.path.islink(site) or os.path.isfile(site):
|
|
1235
1614
|
os.unlink(site)
|
|
1236
1615
|
elif os.path.isdir(site):
|
|
@@ -1239,37 +1618,45 @@ def _tavern_suite_site_directory(project, role):
|
|
|
1239
1618
|
return site
|
|
1240
1619
|
|
|
1241
1620
|
|
|
1621
|
+
def _tavern_pip_cache_directory(project, fingerprint):
|
|
1622
|
+
cache = os.path.join(project.expand_path("$dir_target/integration/tavern-pip-cache"), fingerprint)
|
|
1623
|
+
return directory_utility._ensure_directory_exists(cache)
|
|
1624
|
+
|
|
1625
|
+
|
|
1242
1626
|
def _isolated_suite_subprocess_env(extra_pythonpath=None):
|
|
1243
1627
|
env = os.environ.copy()
|
|
1244
1628
|
env.pop("GITHUB_STEP_SUMMARY", None)
|
|
1245
1629
|
env.pop("PYTHONHOME", None)
|
|
1246
1630
|
env.pop("VIRTUAL_ENV", None)
|
|
1247
1631
|
env.pop("PYTHONPATH", None)
|
|
1632
|
+
env["PYTHONDONTWRITEBYTECODE"] = "1"
|
|
1248
1633
|
if extra_pythonpath:
|
|
1249
1634
|
env["PYTHONPATH"] = extra_pythonpath
|
|
1250
1635
|
return env
|
|
1251
1636
|
|
|
1252
1637
|
|
|
1253
|
-
def _install_tavern_suite_site(test_dir, logger, project, reactor):
|
|
1638
|
+
def _install_tavern_suite_site(test_dir, logger, project, reactor, fingerprint):
|
|
1254
1639
|
requirements_file = os.path.join(test_dir, "requirements.txt")
|
|
1255
|
-
if not os.path.
|
|
1640
|
+
if not os.path.isfile(requirements_file):
|
|
1256
1641
|
return None
|
|
1257
1642
|
role = os.path.basename(test_dir)
|
|
1258
|
-
site = _tavern_suite_site_directory(project,
|
|
1643
|
+
site = _tavern_suite_site_directory(project, fingerprint)
|
|
1644
|
+
env = _isolated_suite_subprocess_env()
|
|
1645
|
+
env["PIP_CACHE_DIR"] = _tavern_pip_cache_directory(project, fingerprint)
|
|
1259
1646
|
command = [_plugin_venv_python(reactor), "-m", "pip", "install", "-r", requirements_file, "-t", site]
|
|
1260
|
-
logger.info(f"Installing tavern requirements
|
|
1647
|
+
logger.info(f"Installing tavern requirements from {role} into isolated site {site}")
|
|
1261
1648
|
process = subprocess.Popen(
|
|
1262
1649
|
command,
|
|
1263
1650
|
stdout=subprocess.PIPE,
|
|
1264
1651
|
stderr=subprocess.STDOUT,
|
|
1265
1652
|
cwd=test_dir,
|
|
1266
|
-
env=
|
|
1653
|
+
env=env,
|
|
1267
1654
|
shell=False,
|
|
1268
1655
|
)
|
|
1269
1656
|
returncode = _stream_subprocess_output(process, logger, role)
|
|
1270
1657
|
if returncode != 0:
|
|
1271
1658
|
raise BuildFailedException(
|
|
1272
|
-
f"Failed to install tavern requirements
|
|
1659
|
+
f"Failed to install tavern requirements from {role} into isolated site {site}"
|
|
1273
1660
|
)
|
|
1274
1661
|
return site
|
|
1275
1662
|
|
{pybuilder_integration-109 → 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-109 → pybuilder_integration-112}/pybuilder_integration/artifact_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration/directory_utility.py
RENAMED
|
File without changes
|
{pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration/exec_utility.py
RENAMED
|
File without changes
|
{pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration.egg-info/requires.txt
RENAMED
|
File without changes
|
{pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration.egg-info/top_level.txt
RENAMED
|
File without changes
|
{pybuilder_integration-109 → pybuilder_integration-112}/pybuilder_integration.egg-info/zip-safe
RENAMED
|
File without changes
|
|
File without changes
|