pybuilder-integration 109__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-109 → pybuilder_integration-110}/PKG-INFO +1 -1
  2. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration/tasks.py +111 -15
  3. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration.egg-info/PKG-INFO +1 -1
  4. {pybuilder_integration-109 → pybuilder_integration-110}/setup.py +1 -1
  5. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration/__init__.py +0 -0
  6. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration/artifact_manager.py +0 -0
  7. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration/cloudwatchlogs_utility.py +0 -0
  8. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration/directory_utility.py +0 -0
  9. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration/exec_utility.py +0 -0
  10. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration/properties.py +0 -0
  11. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration/tool_utility.py +0 -0
  12. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration.egg-info/SOURCES.txt +0 -0
  13. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration.egg-info/dependency_links.txt +0 -0
  14. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration.egg-info/namespace_packages.txt +0 -0
  15. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration.egg-info/requires.txt +0 -0
  16. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration.egg-info/top_level.txt +0 -0
  17. {pybuilder_integration-109 → pybuilder_integration-110}/pybuilder_integration.egg-info/zip-safe +0 -0
  18. {pybuilder_integration-109 → pybuilder_integration-110}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybuilder-integration
3
- Version: 109
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:
@@ -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
@@ -957,19 +958,33 @@ def _run_tavern_suites_parallel(suite_dirs, logger, project, reactor):
957
958
  )
958
959
  return
959
960
 
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
961
  workers = _parallel_tavern_suite_workers(project, len(runnable))
967
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)
968
976
  failures = []
969
977
 
970
978
  def run_suite(tavern_test_directory):
971
979
  role = os.path.basename(tavern_test_directory)
972
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
+ )
973
988
  _run_tavern_tests_in_dir(
974
989
  test_dir=tavern_test_directory,
975
990
  logger=logger,
@@ -978,7 +993,7 @@ def _run_tavern_suites_parallel(suite_dirs, logger, project, reactor):
978
993
  role=role,
979
994
  isolated=True,
980
995
  skip_install=True,
981
- extra_pythonpath=suite_sites[tavern_test_directory],
996
+ extra_pythonpath=site,
982
997
  )
983
998
  return role
984
999
 
@@ -998,6 +1013,71 @@ def _run_tavern_suites_parallel(suite_dirs, logger, project, reactor):
998
1013
  )
999
1014
 
1000
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
+
1001
1081
  def verify_cypress(project: Project, logger: Logger, reactor: Reactor):
1002
1082
  # Get directories with test and cypress executable
1003
1083
  work_dir = project.expand_path(f"${CYPRESS_TEST_DIR}")
@@ -1227,10 +1307,18 @@ def _plugin_venv_python(reactor):
1227
1307
  raise BuildFailedException("plugin venv python is missing or not executable")
1228
1308
 
1229
1309
 
1230
- def _tavern_suite_site_directory(project, role):
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):
1231
1319
  root = project.expand_path("$dir_target/integration/tavern-sites")
1232
1320
  directory_utility._ensure_directory_exists(root)
1233
- site = os.path.join(root, role)
1321
+ site = os.path.join(root, fingerprint)
1234
1322
  if os.path.islink(site) or os.path.isfile(site):
1235
1323
  os.unlink(site)
1236
1324
  elif os.path.isdir(site):
@@ -1239,37 +1327,45 @@ def _tavern_suite_site_directory(project, role):
1239
1327
  return site
1240
1328
 
1241
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
+
1242
1335
  def _isolated_suite_subprocess_env(extra_pythonpath=None):
1243
1336
  env = os.environ.copy()
1244
1337
  env.pop("GITHUB_STEP_SUMMARY", None)
1245
1338
  env.pop("PYTHONHOME", None)
1246
1339
  env.pop("VIRTUAL_ENV", None)
1247
1340
  env.pop("PYTHONPATH", None)
1341
+ env["PYTHONDONTWRITEBYTECODE"] = "1"
1248
1342
  if extra_pythonpath:
1249
1343
  env["PYTHONPATH"] = extra_pythonpath
1250
1344
  return env
1251
1345
 
1252
1346
 
1253
- def _install_tavern_suite_site(test_dir, logger, project, reactor):
1347
+ def _install_tavern_suite_site(test_dir, logger, project, reactor, fingerprint):
1254
1348
  requirements_file = os.path.join(test_dir, "requirements.txt")
1255
- if not os.path.exists(requirements_file):
1349
+ if not os.path.isfile(requirements_file):
1256
1350
  return None
1257
1351
  role = os.path.basename(test_dir)
1258
- site = _tavern_suite_site_directory(project, role)
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)
1259
1355
  command = [_plugin_venv_python(reactor), "-m", "pip", "install", "-r", requirements_file, "-t", site]
1260
- logger.info(f"Installing tavern requirements for {role} into {site}")
1356
+ logger.info(f"Installing tavern requirements from {role} into isolated site {site}")
1261
1357
  process = subprocess.Popen(
1262
1358
  command,
1263
1359
  stdout=subprocess.PIPE,
1264
1360
  stderr=subprocess.STDOUT,
1265
1361
  cwd=test_dir,
1266
- env=_isolated_suite_subprocess_env(),
1362
+ env=env,
1267
1363
  shell=False,
1268
1364
  )
1269
1365
  returncode = _stream_subprocess_output(process, logger, role)
1270
1366
  if returncode != 0:
1271
1367
  raise BuildFailedException(
1272
- f"Failed to install tavern requirements for {role} into isolated site {site}"
1368
+ f"Failed to install tavern requirements from {role} into isolated site {site}"
1273
1369
  )
1274
1370
  return site
1275
1371
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybuilder-integration
3
- Version: 109
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 = '109',
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,