pybuilder-integration 108__tar.gz → 109__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-109}/PKG-INFO +1 -1
  2. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration/directory_utility.py +1 -2
  3. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration/properties.py +2 -0
  4. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration/tasks.py +278 -42
  5. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration.egg-info/PKG-INFO +1 -1
  6. {pybuilder_integration-108 → pybuilder_integration-109}/setup.py +1 -1
  7. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration/__init__.py +0 -0
  8. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration/artifact_manager.py +0 -0
  9. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration/cloudwatchlogs_utility.py +0 -0
  10. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration/exec_utility.py +0 -0
  11. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration/tool_utility.py +0 -0
  12. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration.egg-info/SOURCES.txt +0 -0
  13. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration.egg-info/dependency_links.txt +0 -0
  14. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration.egg-info/namespace_packages.txt +0 -0
  15. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration.egg-info/requires.txt +0 -0
  16. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration.egg-info/top_level.txt +0 -0
  17. {pybuilder_integration-108 → pybuilder_integration-109}/pybuilder_integration.egg-info/zip-safe +0 -0
  18. {pybuilder_integration-108 → pybuilder_integration-109}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybuilder-integration
3
- Version: 108
3
+ Version: 109
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"
@@ -7,6 +7,7 @@ import shutil
7
7
  import subprocess
8
8
  import tempfile
9
9
  import threading
10
+ from concurrent.futures import ThreadPoolExecutor, as_completed
10
11
 
11
12
  from defusedxml import ElementTree as ET
12
13
  from defusedxml.common import DefusedXmlException
@@ -48,11 +49,16 @@ _VERIFY_ENVIRONMENT_SUBPROCESS_PROPERTIES = [
48
49
  CONSOLIDATE_TESTS,
49
50
  SKIP_NON_MATCHING_TAVERN_ARTIFACTS,
50
51
  TESTING_SCOPE,
52
+ PARALLEL_TAVERN_SUITES,
53
+ PARALLEL_TAVERN_SUITES_WORKERS,
51
54
  "abort_upload",
52
55
  "record_cypress",
53
56
  "verbose",
54
57
  ]
55
58
 
59
+ _TAVERN_INSTALL_LOCK = threading.Lock()
60
+ _DEFAULT_PARALLEL_TAVERN_SUITE_WORKERS = 8
61
+
56
62
  def integration_artifact_push(project: Project, logger: Logger, reactor: Reactor):
57
63
  logger.info("Starting upload of integration artifacts")
58
64
  manager = get_artifact_manager(project)
@@ -886,15 +892,17 @@ def _run_tavern_tests_in_dist_dir(dist_directory, latest, logger, project, react
886
892
  logger.debug(f"Run tavern tests in directory: {tavern_test_path} ")
887
893
  logger.debug(f"Run tavern tests in directory files: {os.listdir(tavern_test_path)} ")
888
894
  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,
895
+ suite_dirs = list(_iter_latest_tavern_suite_dirs(tavern_test_path, project))
896
+ if _get_bool_property(project, PARALLEL_TAVERN_SUITES, False) and len(suite_dirs) > 1:
897
+ _run_tavern_suites_parallel(suite_dirs, logger, project, reactor)
898
+ else:
899
+ for tavern_test_directory in suite_dirs:
900
+ logger.info(f"Running {tavern_test_directory}")
901
+ _run_tavern_tests_in_dir(test_dir=tavern_test_directory,
894
902
  logger=logger,
895
903
  project=project,
896
904
  reactor=reactor,
897
- role=os.path.basename(tavern_dir))
905
+ role=os.path.basename(tavern_test_directory))
898
906
  else:
899
907
  _run_tavern_tests_in_dir(test_dir=f"{tavern_test_path}",
900
908
  logger=logger,
@@ -904,6 +912,92 @@ def _run_tavern_tests_in_dist_dir(dist_directory, latest, logger, project, react
904
912
  logger.info(f"Ran Tavern tests: {total_time.get_millis()}")
905
913
 
906
914
 
915
+ def _iter_latest_tavern_suite_dirs(tavern_test_path, project):
916
+ for tavern_dir in sorted(os.listdir(tavern_test_path)):
917
+ tavern_test_directory = os.path.join(tavern_test_path, tavern_dir)
918
+ if os.path.isdir(tavern_test_directory) and _should_run_latest(tavern_dir, project):
919
+ yield tavern_test_directory
920
+
921
+
922
+ def _parallel_tavern_suite_workers(project, suite_count):
923
+ configured = project.get_property(PARALLEL_TAVERN_SUITES_WORKERS, None)
924
+ if configured is None or configured == "":
925
+ return max(1, min(_DEFAULT_PARALLEL_TAVERN_SUITE_WORKERS, suite_count))
926
+ try:
927
+ workers = int(configured)
928
+ except (TypeError, ValueError):
929
+ workers = _DEFAULT_PARALLEL_TAVERN_SUITE_WORKERS
930
+ return max(1, min(workers, suite_count))
931
+
932
+
933
+ def _run_tavern_suites_parallel(suite_dirs, logger, project, reactor):
934
+ prepare_reports_directory(project)
935
+ runnable = []
936
+ for tavern_test_directory in suite_dirs:
937
+ _, requested_marker, declared_markers, skip_non_matching = _tavern_marker_context(
938
+ tavern_test_directory, project
939
+ )
940
+ if _should_skip_nonmatching_tavern(requested_marker, declared_markers, skip_non_matching):
941
+ logger.info(
942
+ f"Skipping Tavern tests: artifact markers {sorted(declared_markers)} do not match {requested_marker}"
943
+ )
944
+ continue
945
+ runnable.append(tavern_test_directory)
946
+ if not runnable:
947
+ return
948
+ if len(runnable) == 1:
949
+ tavern_test_directory = runnable[0]
950
+ logger.info(f"Running {tavern_test_directory}")
951
+ _run_tavern_tests_in_dir(
952
+ test_dir=tavern_test_directory,
953
+ logger=logger,
954
+ project=project,
955
+ reactor=reactor,
956
+ role=os.path.basename(tavern_test_directory),
957
+ )
958
+ return
959
+
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
+ workers = _parallel_tavern_suite_workers(project, len(runnable))
967
+ logger.info(f"Running {len(runnable)} tavern suites in parallel (workers={workers})")
968
+ failures = []
969
+
970
+ def run_suite(tavern_test_directory):
971
+ role = os.path.basename(tavern_test_directory)
972
+ logger.info(f"Running {tavern_test_directory}")
973
+ _run_tavern_tests_in_dir(
974
+ test_dir=tavern_test_directory,
975
+ logger=logger,
976
+ project=project,
977
+ reactor=reactor,
978
+ role=role,
979
+ isolated=True,
980
+ skip_install=True,
981
+ extra_pythonpath=suite_sites[tavern_test_directory],
982
+ )
983
+ return role
984
+
985
+ with ThreadPoolExecutor(max_workers=workers) as executor:
986
+ futures = {executor.submit(run_suite, suite_dir): suite_dir for suite_dir in runnable}
987
+ for future in as_completed(futures):
988
+ suite_dir = futures[future]
989
+ role = os.path.basename(suite_dir)
990
+ try:
991
+ future.result()
992
+ except Exception as error:
993
+ failures.append(f"{role}: {error}")
994
+
995
+ if failures:
996
+ raise BuildFailedException(
997
+ "Parallel tavern suites failed: " + "; ".join(sorted(failures))
998
+ )
999
+
1000
+
907
1001
  def verify_cypress(project: Project, logger: Logger, reactor: Reactor):
908
1002
  # Get directories with test and cypress executable
909
1003
  work_dir = project.expand_path(f"${CYPRESS_TEST_DIR}")
@@ -1087,7 +1181,101 @@ def verify_tavern(project: Project, logger: Logger, reactor: Reactor):
1087
1181
  package_artifacts(project, test_dir, "tavern", project.get_property(ROLE))
1088
1182
 
1089
1183
 
1090
- def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, reactor: Reactor, role=None):
1184
+ def _tavern_marker_context(test_dir, project):
1185
+ extra_args = [project.expand(prop) for prop in project.get_property(TAVERN_ADDITIONAL_ARGS, [])]
1186
+ requested_marker = _extract_tavern_marker_filter(extra_args)
1187
+ declared_markers = read_tavern_service_markers(test_dir)
1188
+ skip_non_matching = _get_bool_property(project, SKIP_NON_MATCHING_TAVERN_ARTIFACTS, False)
1189
+ return extra_args, requested_marker, declared_markers, skip_non_matching
1190
+
1191
+
1192
+ def _should_skip_nonmatching_tavern(requested_marker, declared_markers, skip_non_matching):
1193
+ return bool(skip_non_matching and requested_marker and declared_markers and requested_marker not in declared_markers)
1194
+
1195
+
1196
+ def _install_tavern_requirements(test_dir, logger, project, reactor):
1197
+ requirements_file = os.path.join(test_dir, "requirements.txt")
1198
+ if not os.path.exists(requirements_file):
1199
+ return
1200
+ dependency = RequirementsFile(requirements_file)
1201
+ with _TAVERN_INSTALL_LOCK:
1202
+ install_dependencies(logger, project, dependency, reactor.pybuilder_venv,
1203
+ f"{prepare_logs_directory(project)}/install_tavern_pip_dependencies.log")
1204
+
1205
+
1206
+ def _plugin_venv_executable_path(executable):
1207
+ if executable is None:
1208
+ return None
1209
+ if isinstance(executable, (list, tuple)):
1210
+ if not executable:
1211
+ return None
1212
+ return _plugin_venv_executable_path(executable[0])
1213
+ try:
1214
+ path = os.fspath(executable)
1215
+ except TypeError:
1216
+ return None
1217
+ if isinstance(path, bytes):
1218
+ path = os.fsdecode(path)
1219
+ return path
1220
+
1221
+
1222
+ def _plugin_venv_python(reactor):
1223
+ venv = getattr(reactor, "pybuilder_venv", None)
1224
+ path = _plugin_venv_executable_path(getattr(venv, "executable", None))
1225
+ if path and os.path.isfile(path) and os.access(path, os.X_OK):
1226
+ return path
1227
+ raise BuildFailedException("plugin venv python is missing or not executable")
1228
+
1229
+
1230
+ def _tavern_suite_site_directory(project, role):
1231
+ root = project.expand_path("$dir_target/integration/tavern-sites")
1232
+ directory_utility._ensure_directory_exists(root)
1233
+ site = os.path.join(root, role)
1234
+ if os.path.islink(site) or os.path.isfile(site):
1235
+ os.unlink(site)
1236
+ elif os.path.isdir(site):
1237
+ shutil.rmtree(site)
1238
+ os.makedirs(site, mode=0o700)
1239
+ return site
1240
+
1241
+
1242
+ def _isolated_suite_subprocess_env(extra_pythonpath=None):
1243
+ env = os.environ.copy()
1244
+ env.pop("GITHUB_STEP_SUMMARY", None)
1245
+ env.pop("PYTHONHOME", None)
1246
+ env.pop("VIRTUAL_ENV", None)
1247
+ env.pop("PYTHONPATH", None)
1248
+ if extra_pythonpath:
1249
+ env["PYTHONPATH"] = extra_pythonpath
1250
+ return env
1251
+
1252
+
1253
+ def _install_tavern_suite_site(test_dir, logger, project, reactor):
1254
+ requirements_file = os.path.join(test_dir, "requirements.txt")
1255
+ if not os.path.exists(requirements_file):
1256
+ return None
1257
+ role = os.path.basename(test_dir)
1258
+ site = _tavern_suite_site_directory(project, role)
1259
+ command = [_plugin_venv_python(reactor), "-m", "pip", "install", "-r", requirements_file, "-t", site]
1260
+ logger.info(f"Installing tavern requirements for {role} into {site}")
1261
+ process = subprocess.Popen(
1262
+ command,
1263
+ stdout=subprocess.PIPE,
1264
+ stderr=subprocess.STDOUT,
1265
+ cwd=test_dir,
1266
+ env=_isolated_suite_subprocess_env(),
1267
+ shell=False,
1268
+ )
1269
+ returncode = _stream_subprocess_output(process, logger, role)
1270
+ if returncode != 0:
1271
+ raise BuildFailedException(
1272
+ f"Failed to install tavern requirements for {role} into isolated site {site}"
1273
+ )
1274
+ return site
1275
+
1276
+
1277
+ def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, reactor: Reactor, role=None,
1278
+ isolated=False, skip_install=False, extra_pythonpath=None):
1091
1279
  logger.info("Running tavern tests: {}".format(test_dir))
1092
1280
  if not os.path.exists(test_dir):
1093
1281
  logger.info("Skipping tavern run: no tests")
@@ -1095,51 +1283,51 @@ def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, re
1095
1283
  logger.info(f"Found {len(os.listdir(test_dir))} files in tavern test directory")
1096
1284
  # todo is this unique enough for each run?
1097
1285
  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:
1286
+ extra_args, requested_marker, declared_markers, skip_non_matching = _tavern_marker_context(test_dir, project)
1287
+ if _should_skip_nonmatching_tavern(requested_marker, declared_markers, skip_non_matching):
1103
1288
  logger.info(
1104
1289
  f"Skipping Tavern tests: artifact markers {sorted(declared_markers)} do not match {requested_marker}"
1105
1290
  )
1106
1291
  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")
1292
+ if not isolated:
1293
+ from sys import path as syspath
1294
+ syspath.insert(0, test_dir)
1295
+ if not skip_install:
1296
+ _install_tavern_requirements(test_dir, logger, project, reactor)
1115
1297
  args = ["--junit-xml", f"{output_file}", test_dir] + extra_args
1116
1298
  if project.get_property("verbose"):
1117
1299
  args.append("-s")
1118
1300
  args.append("-v")
1119
- if _get_bool_property(project, RUN_PARALLEL, False):
1301
+ if not isolated and _get_bool_property(project, RUN_PARALLEL, False):
1120
1302
  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)
1303
+ target_url = project.get_property(INTEGRATION_TARGET_URL)
1304
+ public_target_url = project.get_property(INTEGRATION_PUBLIC_TARGET_URL)
1305
+ environment = project.get_property(ENVIRONMENT)
1306
+ pytest_env = _isolated_suite_subprocess_env(extra_pythonpath) if isolated else os.environ.copy()
1307
+ pytest_env['TARGET'] = target_url
1308
+ pytest_env['PUBLIC_TARGET'] = public_target_url
1309
+ pytest_env[ENVIRONMENT] = environment
1310
+ if not isolated:
1311
+ os.environ['TARGET'] = target_url
1312
+ os.environ['PUBLIC_TARGET'] = public_target_url
1313
+ os.environ[ENVIRONMENT] = environment
1314
+ logger.info(f"Running against target: {target_url} and "
1315
+ f"public target: {public_target_url}")
1316
+ logger.debug(f"Running args: {args} ")
1317
+ label = role or os.path.basename(test_dir)
1318
+ ret = _invoke_pytest(args, test_dir, pytest_env, logger, label, isolated, reactor)
1319
+ if _is_pytest_no_tests_collected(ret):
1320
+ fallback_args = _without_tavern_marker_filter(args)
1321
+ if fallback_args != args and (not skip_non_matching or not declared_markers):
1322
+ logger.warn(
1323
+ "No Tavern tests matched the marker filter; rerunning the full suite"
1324
+ )
1325
+ logger.debug(f"Running fallback args: {fallback_args} ")
1326
+ ret = _invoke_pytest(
1327
+ fallback_args, test_dir, pytest_env, logger, label, isolated, reactor
1328
+ )
1141
1329
 
1142
- if ret != 0:
1330
+ if _is_pytest_failure(ret):
1143
1331
  if role:
1144
1332
  roles = []
1145
1333
  if project.get_property(CONSOLIDATE_TESTS, False):
@@ -1156,6 +1344,54 @@ def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, re
1156
1344
  return True
1157
1345
 
1158
1346
 
1347
+ def _is_pytest_no_tests_collected(ret):
1348
+ if ret is None:
1349
+ return False
1350
+ return int(ret) == int(pytest.ExitCode.NO_TESTS_COLLECTED)
1351
+
1352
+
1353
+ def _is_pytest_failure(ret):
1354
+ if ret is None:
1355
+ return True
1356
+ return int(ret) != 0
1357
+
1358
+
1359
+ def _invoke_pytest(args, test_dir, env, logger, label, isolated, reactor=None):
1360
+ if not isolated:
1361
+ cache_wd = os.getcwd()
1362
+ try:
1363
+ os.chdir(test_dir)
1364
+ return pytest.main(args)
1365
+ finally:
1366
+ os.chdir(cache_wd)
1367
+ command = [_plugin_venv_python(reactor), "-u", "-m", "pytest"] + args
1368
+ process = subprocess.Popen(
1369
+ command,
1370
+ stdout=subprocess.PIPE,
1371
+ stderr=subprocess.STDOUT,
1372
+ cwd=test_dir,
1373
+ env=env,
1374
+ shell=False,
1375
+ )
1376
+ return _stream_subprocess_output(process, logger, label)
1377
+
1378
+
1379
+ def _stream_subprocess_output(process, logger, label):
1380
+ stdout = process.stdout
1381
+ if stdout is None:
1382
+ return process.wait()
1383
+ try:
1384
+ while True:
1385
+ chunk = stdout.readline()
1386
+ if not chunk:
1387
+ break
1388
+ redacted = _redact_secret_log_line(_decode_subprocess_stdout_line(chunk))
1389
+ logger.info(f"[{label}] {redacted}")
1390
+ finally:
1391
+ stdout.close()
1392
+ return process.wait()
1393
+
1394
+
1159
1395
  def _without_tavern_marker_filter(args):
1160
1396
  marker_options = {"-m", "--markexpr"}
1161
1397
  fallback_args = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybuilder-integration
3
- Version: 108
3
+ Version: 109
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 = '109',
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,