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.
Files changed (18) hide show
  1. {pybuilder_integration-108 → pybuilder_integration-110}/PKG-INFO +1 -1
  2. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/directory_utility.py +1 -2
  3. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/properties.py +2 -0
  4. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/tasks.py +374 -42
  5. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/PKG-INFO +1 -1
  6. {pybuilder_integration-108 → pybuilder_integration-110}/setup.py +1 -1
  7. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/__init__.py +0 -0
  8. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/artifact_manager.py +0 -0
  9. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/cloudwatchlogs_utility.py +0 -0
  10. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/exec_utility.py +0 -0
  11. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration/tool_utility.py +0 -0
  12. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/SOURCES.txt +0 -0
  13. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/dependency_links.txt +0 -0
  14. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/namespace_packages.txt +0 -0
  15. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/requires.txt +0 -0
  16. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/top_level.txt +0 -0
  17. {pybuilder_integration-108 → pybuilder_integration-110}/pybuilder_integration.egg-info/zip-safe +0 -0
  18. {pybuilder_integration-108 → pybuilder_integration-110}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybuilder-integration
3
- Version: 108
3
+ Version: 110
4
4
  Summary: A pybuilder plugin that runs integration tests (Tavern & Cypress) against a target.
5
5
  Home-page: https://github.com/rspitler/pybuilder-integration
6
6
  Author:
@@ -29,8 +29,7 @@ def prepare_directory(dir_variable, project):
29
29
 
30
30
 
31
31
  def _ensure_directory_exists(path):
32
- if not os.path.exists(path):
33
- os.makedirs(path)
32
+ os.makedirs(path, exist_ok=True)
34
33
  return path
35
34
 
36
35
 
@@ -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
- for tavern_dir in os.listdir(tavern_test_path):
890
- tavern_test_direcotry = f"{tavern_test_path}/{tavern_dir}"
891
- if os.path.isdir(tavern_test_direcotry) and _should_run_latest(tavern_dir, project):
892
- logger.info(f"Running {tavern_test_direcotry}")
893
- _run_tavern_tests_in_dir(test_dir=tavern_test_direcotry,
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(tavern_dir))
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 _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, reactor: Reactor, role=None):
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 = [project.expand(prop) for prop in project.get_property(TAVERN_ADDITIONAL_ARGS, [])]
1099
- requested_marker = _extract_tavern_marker_filter(extra_args)
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
- from sys import path as syspath
1108
- syspath.insert(0, test_dir)
1109
- # install any requirements that my exist
1110
- requirements_file = os.path.join(test_dir, "requirements.txt")
1111
- if os.path.exists(requirements_file):
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
- os.environ['TARGET'] = project.get_property(INTEGRATION_TARGET_URL)
1122
- os.environ['PUBLIC_TARGET'] = project.get_property(INTEGRATION_PUBLIC_TARGET_URL)
1123
- os.environ[ENVIRONMENT] = project.get_property(ENVIRONMENT)
1124
- logger.info(f"Running against target: {project.get_property(INTEGRATION_TARGET_URL)} and "
1125
- f"public target: {project.get_property(INTEGRATION_PUBLIC_TARGET_URL)}")
1126
- cache_wd = os.getcwd()
1127
- try:
1128
- os.chdir(test_dir)
1129
- logger.debug(f"Running args: {args} ")
1130
- ret = pytest.main(args)
1131
- if ret == pytest.ExitCode.NO_TESTS_COLLECTED:
1132
- fallback_args = _without_tavern_marker_filter(args)
1133
- if fallback_args != args and (not skip_non_matching_artifacts or not declared_markers):
1134
- logger.warn(
1135
- "No Tavern tests matched the marker filter; rerunning the full suite"
1136
- )
1137
- logger.debug(f"Running fallback args: {fallback_args} ")
1138
- ret = pytest.main(fallback_args)
1139
- finally:
1140
- os.chdir(cache_wd)
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 != 0:
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 = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybuilder-integration
3
- Version: 108
3
+ Version: 110
4
4
  Summary: A pybuilder plugin that runs integration tests (Tavern & Cypress) against a target.
5
5
  Home-page: https://github.com/rspitler/pybuilder-integration
6
6
  Author:
@@ -21,7 +21,7 @@ class install(_install):
21
21
  if __name__ == '__main__':
22
22
  setup(
23
23
  name = 'pybuilder-integration',
24
- version = '108',
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,