pybuilder-integration 108__tar.gz → 110__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-108 → pybuilder_integration-110}/PKG-INFO +1 -1
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/directory_utility.py +1 -2
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/properties.py +2 -0
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/tasks.py +374 -42
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/PKG-INFO +1 -1
- {pybuilder_integration-108 → pybuilder_integration-110}/setup.py +1 -1
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/__init__.py +0 -0
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/artifact_manager.py +0 -0
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/cloudwatchlogs_utility.py +0 -0
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/exec_utility.py +0 -0
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/tool_utility.py +0 -0
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/SOURCES.txt +0 -0
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/dependency_links.txt +0 -0
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/namespace_packages.txt +0 -0
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/requires.txt +0 -0
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/top_level.txt +0 -0
- {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/zip-safe +0 -0
- {pybuilder_integration-108 → pybuilder_integration-110}/setup.cfg +0 -0
|
@@ -20,5 +20,7 @@ CONSOLIDATE_TESTS = "consolidate_tavern"
|
|
|
20
20
|
SKIP_NON_MATCHING_TAVERN_ARTIFACTS = "skip_non_matching_tavern_artifacts"
|
|
21
21
|
TESTING_SCOPE = "testing_scope"
|
|
22
22
|
PARALLEL_VERIFY_ENVIRONMENT = "parallel_verify_environment"
|
|
23
|
+
PARALLEL_TAVERN_SUITES = "parallel_tavern_suites"
|
|
24
|
+
PARALLEL_TAVERN_SUITES_WORKERS = "parallel_tavern_suites_workers"
|
|
23
25
|
SUBPROCESS_PROPERTIES_FILE_ENV = "PYBUILDER_INTEGRATION_PROPERTIES_FILE"
|
|
24
26
|
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
|
|
@@ -7,6 +8,7 @@ import shutil
|
|
|
7
8
|
import subprocess
|
|
8
9
|
import tempfile
|
|
9
10
|
import threading
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
10
12
|
|
|
11
13
|
from defusedxml import ElementTree as ET
|
|
12
14
|
from defusedxml.common import DefusedXmlException
|
|
@@ -48,11 +50,16 @@ _VERIFY_ENVIRONMENT_SUBPROCESS_PROPERTIES = [
|
|
|
48
50
|
CONSOLIDATE_TESTS,
|
|
49
51
|
SKIP_NON_MATCHING_TAVERN_ARTIFACTS,
|
|
50
52
|
TESTING_SCOPE,
|
|
53
|
+
PARALLEL_TAVERN_SUITES,
|
|
54
|
+
PARALLEL_TAVERN_SUITES_WORKERS,
|
|
51
55
|
"abort_upload",
|
|
52
56
|
"record_cypress",
|
|
53
57
|
"verbose",
|
|
54
58
|
]
|
|
55
59
|
|
|
60
|
+
_TAVERN_INSTALL_LOCK = threading.Lock()
|
|
61
|
+
_DEFAULT_PARALLEL_TAVERN_SUITE_WORKERS = 8
|
|
62
|
+
|
|
56
63
|
def integration_artifact_push(project: Project, logger: Logger, reactor: Reactor):
|
|
57
64
|
logger.info("Starting upload of integration artifacts")
|
|
58
65
|
manager = get_artifact_manager(project)
|
|
@@ -886,15 +893,17 @@ def _run_tavern_tests_in_dist_dir(dist_directory, latest, logger, project, react
|
|
|
886
893
|
logger.debug(f"Run tavern tests in directory: {tavern_test_path} ")
|
|
887
894
|
logger.debug(f"Run tavern tests in directory files: {os.listdir(tavern_test_path)} ")
|
|
888
895
|
if latest:
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
896
|
+
suite_dirs = list(_iter_latest_tavern_suite_dirs(tavern_test_path, project))
|
|
897
|
+
if _get_bool_property(project, PARALLEL_TAVERN_SUITES, False) and len(suite_dirs) > 1:
|
|
898
|
+
_run_tavern_suites_parallel(suite_dirs, logger, project, reactor)
|
|
899
|
+
else:
|
|
900
|
+
for tavern_test_directory in suite_dirs:
|
|
901
|
+
logger.info(f"Running {tavern_test_directory}")
|
|
902
|
+
_run_tavern_tests_in_dir(test_dir=tavern_test_directory,
|
|
894
903
|
logger=logger,
|
|
895
904
|
project=project,
|
|
896
905
|
reactor=reactor,
|
|
897
|
-
role=os.path.basename(
|
|
906
|
+
role=os.path.basename(tavern_test_directory))
|
|
898
907
|
else:
|
|
899
908
|
_run_tavern_tests_in_dir(test_dir=f"{tavern_test_path}",
|
|
900
909
|
logger=logger,
|
|
@@ -904,6 +913,171 @@ def _run_tavern_tests_in_dist_dir(dist_directory, latest, logger, project, react
|
|
|
904
913
|
logger.info(f"Ran Tavern tests: {total_time.get_millis()}")
|
|
905
914
|
|
|
906
915
|
|
|
916
|
+
def _iter_latest_tavern_suite_dirs(tavern_test_path, project):
|
|
917
|
+
for tavern_dir in sorted(os.listdir(tavern_test_path)):
|
|
918
|
+
tavern_test_directory = os.path.join(tavern_test_path, tavern_dir)
|
|
919
|
+
if os.path.isdir(tavern_test_directory) and _should_run_latest(tavern_dir, project):
|
|
920
|
+
yield tavern_test_directory
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def _parallel_tavern_suite_workers(project, suite_count):
|
|
924
|
+
configured = project.get_property(PARALLEL_TAVERN_SUITES_WORKERS, None)
|
|
925
|
+
if configured is None or configured == "":
|
|
926
|
+
return max(1, min(_DEFAULT_PARALLEL_TAVERN_SUITE_WORKERS, suite_count))
|
|
927
|
+
try:
|
|
928
|
+
workers = int(configured)
|
|
929
|
+
except (TypeError, ValueError):
|
|
930
|
+
workers = _DEFAULT_PARALLEL_TAVERN_SUITE_WORKERS
|
|
931
|
+
return max(1, min(workers, suite_count))
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _run_tavern_suites_parallel(suite_dirs, logger, project, reactor):
|
|
935
|
+
prepare_reports_directory(project)
|
|
936
|
+
runnable = []
|
|
937
|
+
for tavern_test_directory in suite_dirs:
|
|
938
|
+
_, requested_marker, declared_markers, skip_non_matching = _tavern_marker_context(
|
|
939
|
+
tavern_test_directory, project
|
|
940
|
+
)
|
|
941
|
+
if _should_skip_nonmatching_tavern(requested_marker, declared_markers, skip_non_matching):
|
|
942
|
+
logger.info(
|
|
943
|
+
f"Skipping Tavern tests: artifact markers {sorted(declared_markers)} do not match {requested_marker}"
|
|
944
|
+
)
|
|
945
|
+
continue
|
|
946
|
+
runnable.append(tavern_test_directory)
|
|
947
|
+
if not runnable:
|
|
948
|
+
return
|
|
949
|
+
if len(runnable) == 1:
|
|
950
|
+
tavern_test_directory = runnable[0]
|
|
951
|
+
logger.info(f"Running {tavern_test_directory}")
|
|
952
|
+
_run_tavern_tests_in_dir(
|
|
953
|
+
test_dir=tavern_test_directory,
|
|
954
|
+
logger=logger,
|
|
955
|
+
project=project,
|
|
956
|
+
reactor=reactor,
|
|
957
|
+
role=os.path.basename(tavern_test_directory),
|
|
958
|
+
)
|
|
959
|
+
return
|
|
960
|
+
|
|
961
|
+
workers = _parallel_tavern_suite_workers(project, len(runnable))
|
|
962
|
+
logger.info(f"Running {len(runnable)} tavern suites in parallel (workers={workers})")
|
|
963
|
+
fingerprint_by_dir = {suite_dir: _tavern_requirements_fingerprint(suite_dir) for suite_dir in runnable}
|
|
964
|
+
plugin_fingerprint = _plugin_venv_requirements_fingerprint(fingerprint_by_dir)
|
|
965
|
+
installer = _ParallelTavernRequirementInstaller(plugin_fingerprint)
|
|
966
|
+
if plugin_fingerprint is not None:
|
|
967
|
+
sample_dir = next(
|
|
968
|
+
suite_dir for suite_dir, fingerprint in fingerprint_by_dir.items()
|
|
969
|
+
if fingerprint == plugin_fingerprint
|
|
970
|
+
)
|
|
971
|
+
logger.info(
|
|
972
|
+
f"Installing tavern requirements from {os.path.basename(sample_dir)} "
|
|
973
|
+
f"into the plugin venv before parallel pytest"
|
|
974
|
+
)
|
|
975
|
+
installer.install(sample_dir, plugin_fingerprint, logger, project, reactor)
|
|
976
|
+
failures = []
|
|
977
|
+
|
|
978
|
+
def run_suite(tavern_test_directory):
|
|
979
|
+
role = os.path.basename(tavern_test_directory)
|
|
980
|
+
logger.info(f"Running {tavern_test_directory}")
|
|
981
|
+
site = installer.install(
|
|
982
|
+
tavern_test_directory,
|
|
983
|
+
fingerprint_by_dir[tavern_test_directory],
|
|
984
|
+
logger,
|
|
985
|
+
project,
|
|
986
|
+
reactor,
|
|
987
|
+
)
|
|
988
|
+
_run_tavern_tests_in_dir(
|
|
989
|
+
test_dir=tavern_test_directory,
|
|
990
|
+
logger=logger,
|
|
991
|
+
project=project,
|
|
992
|
+
reactor=reactor,
|
|
993
|
+
role=role,
|
|
994
|
+
isolated=True,
|
|
995
|
+
skip_install=True,
|
|
996
|
+
extra_pythonpath=site,
|
|
997
|
+
)
|
|
998
|
+
return role
|
|
999
|
+
|
|
1000
|
+
with ThreadPoolExecutor(max_workers=workers) as executor:
|
|
1001
|
+
futures = {executor.submit(run_suite, suite_dir): suite_dir for suite_dir in runnable}
|
|
1002
|
+
for future in as_completed(futures):
|
|
1003
|
+
suite_dir = futures[future]
|
|
1004
|
+
role = os.path.basename(suite_dir)
|
|
1005
|
+
try:
|
|
1006
|
+
future.result()
|
|
1007
|
+
except Exception as error:
|
|
1008
|
+
failures.append(f"{role}: {error}")
|
|
1009
|
+
|
|
1010
|
+
if failures:
|
|
1011
|
+
raise BuildFailedException(
|
|
1012
|
+
"Parallel tavern suites failed: " + "; ".join(sorted(failures))
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
class _ParallelTavernRequirementInstaller:
|
|
1017
|
+
def __init__(self, plugin_venv_fingerprint):
|
|
1018
|
+
self._plugin_venv_fingerprint = plugin_venv_fingerprint
|
|
1019
|
+
self._lock = threading.Lock()
|
|
1020
|
+
self._events = {}
|
|
1021
|
+
self._results = {}
|
|
1022
|
+
self._errors = {}
|
|
1023
|
+
|
|
1024
|
+
def install(self, test_dir, fingerprint, logger, project, reactor):
|
|
1025
|
+
if fingerprint is None:
|
|
1026
|
+
return None
|
|
1027
|
+
with self._lock:
|
|
1028
|
+
if fingerprint in self._results:
|
|
1029
|
+
return self._results[fingerprint]
|
|
1030
|
+
if fingerprint in self._errors:
|
|
1031
|
+
raise self._errors[fingerprint]
|
|
1032
|
+
event = self._events.get(fingerprint)
|
|
1033
|
+
if event is None:
|
|
1034
|
+
event = threading.Event()
|
|
1035
|
+
self._events[fingerprint] = event
|
|
1036
|
+
leader = True
|
|
1037
|
+
else:
|
|
1038
|
+
leader = False
|
|
1039
|
+
if not leader:
|
|
1040
|
+
event.wait()
|
|
1041
|
+
with self._lock:
|
|
1042
|
+
if fingerprint in self._errors:
|
|
1043
|
+
raise self._errors[fingerprint]
|
|
1044
|
+
if fingerprint in self._results:
|
|
1045
|
+
return self._results[fingerprint]
|
|
1046
|
+
raise BuildFailedException(
|
|
1047
|
+
f"Tavern requirements install for {os.path.basename(test_dir)} finished without a result"
|
|
1048
|
+
)
|
|
1049
|
+
try:
|
|
1050
|
+
if fingerprint == self._plugin_venv_fingerprint:
|
|
1051
|
+
_install_tavern_requirements(test_dir, logger, project, reactor)
|
|
1052
|
+
site = None
|
|
1053
|
+
else:
|
|
1054
|
+
site = _install_tavern_suite_site(test_dir, logger, project, reactor, fingerprint)
|
|
1055
|
+
with self._lock:
|
|
1056
|
+
self._results[fingerprint] = site
|
|
1057
|
+
return site
|
|
1058
|
+
except Exception as error:
|
|
1059
|
+
with self._lock:
|
|
1060
|
+
self._errors[fingerprint] = error
|
|
1061
|
+
raise
|
|
1062
|
+
finally:
|
|
1063
|
+
event.set()
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def _plugin_venv_requirements_fingerprint(fingerprint_by_dir):
|
|
1067
|
+
counts = {}
|
|
1068
|
+
order = []
|
|
1069
|
+
for fingerprint in fingerprint_by_dir.values():
|
|
1070
|
+
if fingerprint is None:
|
|
1071
|
+
continue
|
|
1072
|
+
if fingerprint not in counts:
|
|
1073
|
+
order.append(fingerprint)
|
|
1074
|
+
counts[fingerprint] = 0
|
|
1075
|
+
counts[fingerprint] += 1
|
|
1076
|
+
if not order:
|
|
1077
|
+
return None
|
|
1078
|
+
return max(order, key=lambda fingerprint: (counts[fingerprint], -order.index(fingerprint)))
|
|
1079
|
+
|
|
1080
|
+
|
|
907
1081
|
def verify_cypress(project: Project, logger: Logger, reactor: Reactor):
|
|
908
1082
|
# Get directories with test and cypress executable
|
|
909
1083
|
work_dir = project.expand_path(f"${CYPRESS_TEST_DIR}")
|
|
@@ -1087,7 +1261,117 @@ def verify_tavern(project: Project, logger: Logger, reactor: Reactor):
|
|
|
1087
1261
|
package_artifacts(project, test_dir, "tavern", project.get_property(ROLE))
|
|
1088
1262
|
|
|
1089
1263
|
|
|
1090
|
-
def
|
|
1264
|
+
def _tavern_marker_context(test_dir, project):
|
|
1265
|
+
extra_args = [project.expand(prop) for prop in project.get_property(TAVERN_ADDITIONAL_ARGS, [])]
|
|
1266
|
+
requested_marker = _extract_tavern_marker_filter(extra_args)
|
|
1267
|
+
declared_markers = read_tavern_service_markers(test_dir)
|
|
1268
|
+
skip_non_matching = _get_bool_property(project, SKIP_NON_MATCHING_TAVERN_ARTIFACTS, False)
|
|
1269
|
+
return extra_args, requested_marker, declared_markers, skip_non_matching
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
def _should_skip_nonmatching_tavern(requested_marker, declared_markers, skip_non_matching):
|
|
1273
|
+
return bool(skip_non_matching and requested_marker and declared_markers and requested_marker not in declared_markers)
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def _install_tavern_requirements(test_dir, logger, project, reactor):
|
|
1277
|
+
requirements_file = os.path.join(test_dir, "requirements.txt")
|
|
1278
|
+
if not os.path.exists(requirements_file):
|
|
1279
|
+
return
|
|
1280
|
+
dependency = RequirementsFile(requirements_file)
|
|
1281
|
+
with _TAVERN_INSTALL_LOCK:
|
|
1282
|
+
install_dependencies(logger, project, dependency, reactor.pybuilder_venv,
|
|
1283
|
+
f"{prepare_logs_directory(project)}/install_tavern_pip_dependencies.log")
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def _plugin_venv_executable_path(executable):
|
|
1287
|
+
if executable is None:
|
|
1288
|
+
return None
|
|
1289
|
+
if isinstance(executable, (list, tuple)):
|
|
1290
|
+
if not executable:
|
|
1291
|
+
return None
|
|
1292
|
+
return _plugin_venv_executable_path(executable[0])
|
|
1293
|
+
try:
|
|
1294
|
+
path = os.fspath(executable)
|
|
1295
|
+
except TypeError:
|
|
1296
|
+
return None
|
|
1297
|
+
if isinstance(path, bytes):
|
|
1298
|
+
path = os.fsdecode(path)
|
|
1299
|
+
return path
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
def _plugin_venv_python(reactor):
|
|
1303
|
+
venv = getattr(reactor, "pybuilder_venv", None)
|
|
1304
|
+
path = _plugin_venv_executable_path(getattr(venv, "executable", None))
|
|
1305
|
+
if path and os.path.isfile(path) and os.access(path, os.X_OK):
|
|
1306
|
+
return path
|
|
1307
|
+
raise BuildFailedException("plugin venv python is missing or not executable")
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
def _tavern_requirements_fingerprint(test_dir):
|
|
1311
|
+
requirements_file = os.path.join(test_dir, "requirements.txt")
|
|
1312
|
+
if not os.path.isfile(requirements_file):
|
|
1313
|
+
return None
|
|
1314
|
+
with open(requirements_file, "rb") as requirements:
|
|
1315
|
+
return hashlib.sha256(requirements.read()).hexdigest()
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def _tavern_suite_site_directory(project, fingerprint):
|
|
1319
|
+
root = project.expand_path("$dir_target/integration/tavern-sites")
|
|
1320
|
+
directory_utility._ensure_directory_exists(root)
|
|
1321
|
+
site = os.path.join(root, fingerprint)
|
|
1322
|
+
if os.path.islink(site) or os.path.isfile(site):
|
|
1323
|
+
os.unlink(site)
|
|
1324
|
+
elif os.path.isdir(site):
|
|
1325
|
+
shutil.rmtree(site)
|
|
1326
|
+
os.makedirs(site, mode=0o700)
|
|
1327
|
+
return site
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def _tavern_pip_cache_directory(project, fingerprint):
|
|
1331
|
+
cache = os.path.join(project.expand_path("$dir_target/integration/tavern-pip-cache"), fingerprint)
|
|
1332
|
+
return directory_utility._ensure_directory_exists(cache)
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
def _isolated_suite_subprocess_env(extra_pythonpath=None):
|
|
1336
|
+
env = os.environ.copy()
|
|
1337
|
+
env.pop("GITHUB_STEP_SUMMARY", None)
|
|
1338
|
+
env.pop("PYTHONHOME", None)
|
|
1339
|
+
env.pop("VIRTUAL_ENV", None)
|
|
1340
|
+
env.pop("PYTHONPATH", None)
|
|
1341
|
+
env["PYTHONDONTWRITEBYTECODE"] = "1"
|
|
1342
|
+
if extra_pythonpath:
|
|
1343
|
+
env["PYTHONPATH"] = extra_pythonpath
|
|
1344
|
+
return env
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
def _install_tavern_suite_site(test_dir, logger, project, reactor, fingerprint):
|
|
1348
|
+
requirements_file = os.path.join(test_dir, "requirements.txt")
|
|
1349
|
+
if not os.path.isfile(requirements_file):
|
|
1350
|
+
return None
|
|
1351
|
+
role = os.path.basename(test_dir)
|
|
1352
|
+
site = _tavern_suite_site_directory(project, fingerprint)
|
|
1353
|
+
env = _isolated_suite_subprocess_env()
|
|
1354
|
+
env["PIP_CACHE_DIR"] = _tavern_pip_cache_directory(project, fingerprint)
|
|
1355
|
+
command = [_plugin_venv_python(reactor), "-m", "pip", "install", "-r", requirements_file, "-t", site]
|
|
1356
|
+
logger.info(f"Installing tavern requirements from {role} into isolated site {site}")
|
|
1357
|
+
process = subprocess.Popen(
|
|
1358
|
+
command,
|
|
1359
|
+
stdout=subprocess.PIPE,
|
|
1360
|
+
stderr=subprocess.STDOUT,
|
|
1361
|
+
cwd=test_dir,
|
|
1362
|
+
env=env,
|
|
1363
|
+
shell=False,
|
|
1364
|
+
)
|
|
1365
|
+
returncode = _stream_subprocess_output(process, logger, role)
|
|
1366
|
+
if returncode != 0:
|
|
1367
|
+
raise BuildFailedException(
|
|
1368
|
+
f"Failed to install tavern requirements from {role} into isolated site {site}"
|
|
1369
|
+
)
|
|
1370
|
+
return site
|
|
1371
|
+
|
|
1372
|
+
|
|
1373
|
+
def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, reactor: Reactor, role=None,
|
|
1374
|
+
isolated=False, skip_install=False, extra_pythonpath=None):
|
|
1091
1375
|
logger.info("Running tavern tests: {}".format(test_dir))
|
|
1092
1376
|
if not os.path.exists(test_dir):
|
|
1093
1377
|
logger.info("Skipping tavern run: no tests")
|
|
@@ -1095,51 +1379,51 @@ def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, re
|
|
|
1095
1379
|
logger.info(f"Found {len(os.listdir(test_dir))} files in tavern test directory")
|
|
1096
1380
|
# todo is this unique enough for each run?
|
|
1097
1381
|
output_file, run_name = get_test_report_file(project, test_dir)
|
|
1098
|
-
extra_args
|
|
1099
|
-
requested_marker
|
|
1100
|
-
declared_markers = read_tavern_service_markers(test_dir)
|
|
1101
|
-
skip_non_matching_artifacts = _get_bool_property(project, SKIP_NON_MATCHING_TAVERN_ARTIFACTS, False)
|
|
1102
|
-
if skip_non_matching_artifacts and requested_marker and declared_markers and requested_marker not in declared_markers:
|
|
1382
|
+
extra_args, requested_marker, declared_markers, skip_non_matching = _tavern_marker_context(test_dir, project)
|
|
1383
|
+
if _should_skip_nonmatching_tavern(requested_marker, declared_markers, skip_non_matching):
|
|
1103
1384
|
logger.info(
|
|
1104
1385
|
f"Skipping Tavern tests: artifact markers {sorted(declared_markers)} do not match {requested_marker}"
|
|
1105
1386
|
)
|
|
1106
1387
|
return True
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
dependency = RequirementsFile(requirements_file)
|
|
1113
|
-
install_dependencies(logger, project, dependency, reactor.pybuilder_venv,
|
|
1114
|
-
f"{prepare_logs_directory(project)}/install_tavern_pip_dependencies.log")
|
|
1388
|
+
if not isolated:
|
|
1389
|
+
from sys import path as syspath
|
|
1390
|
+
syspath.insert(0, test_dir)
|
|
1391
|
+
if not skip_install:
|
|
1392
|
+
_install_tavern_requirements(test_dir, logger, project, reactor)
|
|
1115
1393
|
args = ["--junit-xml", f"{output_file}", test_dir] + extra_args
|
|
1116
1394
|
if project.get_property("verbose"):
|
|
1117
1395
|
args.append("-s")
|
|
1118
1396
|
args.append("-v")
|
|
1119
|
-
if _get_bool_property(project, RUN_PARALLEL, False):
|
|
1397
|
+
if not isolated and _get_bool_property(project, RUN_PARALLEL, False):
|
|
1120
1398
|
args.extend(['-n', 'auto'])
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1399
|
+
target_url = project.get_property(INTEGRATION_TARGET_URL)
|
|
1400
|
+
public_target_url = project.get_property(INTEGRATION_PUBLIC_TARGET_URL)
|
|
1401
|
+
environment = project.get_property(ENVIRONMENT)
|
|
1402
|
+
pytest_env = _isolated_suite_subprocess_env(extra_pythonpath) if isolated else os.environ.copy()
|
|
1403
|
+
pytest_env['TARGET'] = target_url
|
|
1404
|
+
pytest_env['PUBLIC_TARGET'] = public_target_url
|
|
1405
|
+
pytest_env[ENVIRONMENT] = environment
|
|
1406
|
+
if not isolated:
|
|
1407
|
+
os.environ['TARGET'] = target_url
|
|
1408
|
+
os.environ['PUBLIC_TARGET'] = public_target_url
|
|
1409
|
+
os.environ[ENVIRONMENT] = environment
|
|
1410
|
+
logger.info(f"Running against target: {target_url} and "
|
|
1411
|
+
f"public target: {public_target_url}")
|
|
1412
|
+
logger.debug(f"Running args: {args} ")
|
|
1413
|
+
label = role or os.path.basename(test_dir)
|
|
1414
|
+
ret = _invoke_pytest(args, test_dir, pytest_env, logger, label, isolated, reactor)
|
|
1415
|
+
if _is_pytest_no_tests_collected(ret):
|
|
1416
|
+
fallback_args = _without_tavern_marker_filter(args)
|
|
1417
|
+
if fallback_args != args and (not skip_non_matching or not declared_markers):
|
|
1418
|
+
logger.warn(
|
|
1419
|
+
"No Tavern tests matched the marker filter; rerunning the full suite"
|
|
1420
|
+
)
|
|
1421
|
+
logger.debug(f"Running fallback args: {fallback_args} ")
|
|
1422
|
+
ret = _invoke_pytest(
|
|
1423
|
+
fallback_args, test_dir, pytest_env, logger, label, isolated, reactor
|
|
1424
|
+
)
|
|
1141
1425
|
|
|
1142
|
-
if ret
|
|
1426
|
+
if _is_pytest_failure(ret):
|
|
1143
1427
|
if role:
|
|
1144
1428
|
roles = []
|
|
1145
1429
|
if project.get_property(CONSOLIDATE_TESTS, False):
|
|
@@ -1156,6 +1440,54 @@ def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, re
|
|
|
1156
1440
|
return True
|
|
1157
1441
|
|
|
1158
1442
|
|
|
1443
|
+
def _is_pytest_no_tests_collected(ret):
|
|
1444
|
+
if ret is None:
|
|
1445
|
+
return False
|
|
1446
|
+
return int(ret) == int(pytest.ExitCode.NO_TESTS_COLLECTED)
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
def _is_pytest_failure(ret):
|
|
1450
|
+
if ret is None:
|
|
1451
|
+
return True
|
|
1452
|
+
return int(ret) != 0
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
def _invoke_pytest(args, test_dir, env, logger, label, isolated, reactor=None):
|
|
1456
|
+
if not isolated:
|
|
1457
|
+
cache_wd = os.getcwd()
|
|
1458
|
+
try:
|
|
1459
|
+
os.chdir(test_dir)
|
|
1460
|
+
return pytest.main(args)
|
|
1461
|
+
finally:
|
|
1462
|
+
os.chdir(cache_wd)
|
|
1463
|
+
command = [_plugin_venv_python(reactor), "-u", "-m", "pytest"] + args
|
|
1464
|
+
process = subprocess.Popen(
|
|
1465
|
+
command,
|
|
1466
|
+
stdout=subprocess.PIPE,
|
|
1467
|
+
stderr=subprocess.STDOUT,
|
|
1468
|
+
cwd=test_dir,
|
|
1469
|
+
env=env,
|
|
1470
|
+
shell=False,
|
|
1471
|
+
)
|
|
1472
|
+
return _stream_subprocess_output(process, logger, label)
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def _stream_subprocess_output(process, logger, label):
|
|
1476
|
+
stdout = process.stdout
|
|
1477
|
+
if stdout is None:
|
|
1478
|
+
return process.wait()
|
|
1479
|
+
try:
|
|
1480
|
+
while True:
|
|
1481
|
+
chunk = stdout.readline()
|
|
1482
|
+
if not chunk:
|
|
1483
|
+
break
|
|
1484
|
+
redacted = _redact_secret_log_line(_decode_subprocess_stdout_line(chunk))
|
|
1485
|
+
logger.info(f"[{label}] {redacted}")
|
|
1486
|
+
finally:
|
|
1487
|
+
stdout.close()
|
|
1488
|
+
return process.wait()
|
|
1489
|
+
|
|
1490
|
+
|
|
1159
1491
|
def _without_tavern_marker_filter(args):
|
|
1160
1492
|
marker_options = {"-m", "--markexpr"}
|
|
1161
1493
|
fallback_args = []
|
|
@@ -21,7 +21,7 @@ class install(_install):
|
|
|
21
21
|
if __name__ == '__main__':
|
|
22
22
|
setup(
|
|
23
23
|
name = 'pybuilder-integration',
|
|
24
|
-
version = '
|
|
24
|
+
version = '110',
|
|
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-108 → pybuilder_integration-110}/pybuilder_integration/artifact_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/exec_utility.py
RENAMED
|
File without changes
|
{pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/tool_utility.py
RENAMED
|
File without changes
|
{pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/requires.txt
RENAMED
|
File without changes
|
{pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/top_level.txt
RENAMED
|
File without changes
|
{pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/zip-safe
RENAMED
|
File without changes
|
|
File without changes
|