pybuilder-integration 102__tar.gz → 105__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 (21) hide show
  1. {pybuilder_integration-102 → pybuilder_integration-105}/PKG-INFO +8 -2
  2. {pybuilder_integration-102 → pybuilder_integration-105}/pybuilder_integration/__init__.py +23 -1
  3. {pybuilder_integration-102 → pybuilder_integration-105}/pybuilder_integration/artifact_manager.py +10 -2
  4. pybuilder_integration-105/pybuilder_integration/directory_utility.py +168 -0
  5. {pybuilder_integration-102 → pybuilder_integration-105}/pybuilder_integration/properties.py +4 -0
  6. pybuilder_integration-105/pybuilder_integration/tasks.py +548 -0
  7. pybuilder_integration-105/pybuilder_integration/tool_utility.py +53 -0
  8. {pybuilder_integration-102 → pybuilder_integration-105}/pybuilder_integration.egg-info/PKG-INFO +8 -2
  9. {pybuilder_integration-102 → pybuilder_integration-105}/setup.py +2 -1
  10. pybuilder_integration-102/pybuilder_integration/directory_utility.py +0 -70
  11. pybuilder_integration-102/pybuilder_integration/tasks.py +0 -249
  12. pybuilder_integration-102/pybuilder_integration/tool_utility.py +0 -24
  13. {pybuilder_integration-102 → pybuilder_integration-105}/pybuilder_integration/cloudwatchlogs_utility.py +0 -0
  14. {pybuilder_integration-102 → pybuilder_integration-105}/pybuilder_integration/exec_utility.py +0 -0
  15. {pybuilder_integration-102 → pybuilder_integration-105}/pybuilder_integration.egg-info/SOURCES.txt +0 -0
  16. {pybuilder_integration-102 → pybuilder_integration-105}/pybuilder_integration.egg-info/dependency_links.txt +0 -0
  17. {pybuilder_integration-102 → pybuilder_integration-105}/pybuilder_integration.egg-info/namespace_packages.txt +0 -0
  18. {pybuilder_integration-102 → pybuilder_integration-105}/pybuilder_integration.egg-info/requires.txt +0 -0
  19. {pybuilder_integration-102 → pybuilder_integration-105}/pybuilder_integration.egg-info/top_level.txt +0 -0
  20. {pybuilder_integration-102 → pybuilder_integration-105}/pybuilder_integration.egg-info/zip-safe +0 -0
  21. {pybuilder_integration-102 → pybuilder_integration-105}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pybuilder-integration
3
- Version: 102
3
+ Version: 105
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:
@@ -13,5 +13,11 @@ Classifier: Programming Language :: Python
13
13
  Requires-Dist: pytest
14
14
  Requires-Dist: tavern==1.25.2
15
15
  Requires-Dist: boto3
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: home-page
19
+ Dynamic: license
20
+ Dynamic: requires-dist
21
+ Dynamic: summary
16
22
 
17
23
  A pybuilder plugin that runs integration tests against a target. This is intended to be a broader scope than unit-tests encompassing dependant functionality.
@@ -1,9 +1,10 @@
1
+ import json
1
2
  import os
2
3
 
3
4
  from pybuilder.core import task, Project, Logger, depends, after, init
4
5
  from pybuilder.reactor import Reactor
5
6
 
6
- import pybuilder_integration.tasks
7
+ import pybuilder_integration.tasks as tasks
7
8
  from pybuilder_integration.properties import *
8
9
 
9
10
 
@@ -18,6 +19,17 @@ def init_plugin(project):
18
19
  project.plugin_depends_on("pytest")
19
20
  project.build_depends_on('pytest-xdist')
20
21
  project.plugin_depends_on("tavern")
22
+ _apply_integration_properties_file(project)
23
+
24
+
25
+ def _apply_integration_properties_file(project):
26
+ properties_file = os.environ.get(SUBPROCESS_PROPERTIES_FILE_ENV)
27
+ if not properties_file or not os.path.exists(properties_file):
28
+ return
29
+ with open(properties_file) as fp:
30
+ properties = json.load(fp)
31
+ for key, value in properties.items():
32
+ project.set_property(key, value)
21
33
 
22
34
 
23
35
  @task(description="Runs integration tests against a CI/Prod environment."
@@ -34,6 +46,16 @@ def verify_environment(project: Project, logger: Logger, reactor: Reactor):
34
46
  tasks.verify_environment(project, logger, reactor)
35
47
 
36
48
 
49
+ @task(description="Run current build integration tests only. Intended for verify_environment subprocesses.")
50
+ def verify_environment_current(project: Project, logger: Logger, reactor: Reactor):
51
+ tasks._verify_environment_current(project, logger, reactor)
52
+
53
+
54
+ @task(description="Run downloaded LATEST integration tests only. Intended for verify_environment subprocesses.")
55
+ def verify_environment_latest(project: Project, logger: Logger, reactor: Reactor):
56
+ tasks._verify_environment_latest(project, logger, reactor)
57
+
58
+
37
59
  @task(description="Run integration tests using a cypress spec. Requires NPM installed.\n"
38
60
  f"\t{INTEGRATION_TARGET_URL} - (required) Full URL target for cypress tests\n"
39
61
  f"\t{INTEGRATION_PUBLIC_TARGET_URL} - (required) Full public URL target for cypress tests\n"
@@ -8,7 +8,7 @@ from pybuilder.reactor import Reactor
8
8
 
9
9
  from pybuilder_integration import exec_utility
10
10
  from pybuilder_integration.directory_utility import get_latest_distribution_directory, \
11
- get_latest_zipped_distribution_directory
11
+ get_latest_zipped_distribution_directory, read_tavern_service_markers, write_tavern_manifest
12
12
  from pybuilder_integration.properties import *
13
13
 
14
14
 
@@ -200,13 +200,21 @@ def _unzip_downloaded_artifacts(dir_with_zips: str, destination: str, logger: Lo
200
200
  os.makedirs(consolidated_folder, exist_ok=True)
201
201
  logger.debug(f"Creating role file for log retrieval {consolidated_folder}/roles")
202
202
  open(f"{consolidated_folder}/roles", 'w').close()
203
+ consolidated_markers = set()
204
+ has_tavern_manifest = False
203
205
  for dirn in os.listdir(f"{destination}/tavern"):
204
206
  if "consolidated" != dirn:
205
207
  logger.debug(f"Consolidating directory: {dirn}")
208
+ markers = read_tavern_service_markers(f"{destination}/tavern/{dirn}")
209
+ if markers is not None:
210
+ has_tavern_manifest = True
211
+ consolidated_markers.update(markers)
206
212
  shutil.copytree(f"{destination}/tavern/{dirn}", consolidated_folder, dirs_exist_ok=True)
207
213
  shutil.rmtree(f"{destination}/tavern/{dirn}")
208
214
  with open(f"{consolidated_folder}/roles", "a") as fp:
209
- fp.writelines(dirn)
215
+ fp.write(f"{dirn}\n")
216
+ if has_tavern_manifest:
217
+ write_tavern_manifest(consolidated_folder, consolidated_markers)
210
218
  return destination
211
219
 
212
220
 
@@ -0,0 +1,168 @@
1
+ import json
2
+ import os
3
+ import re
4
+ import shutil
5
+ import tempfile
6
+
7
+
8
+ TAVERN_MANIFEST_DIRECTORY = ".pybuilder-integration"
9
+ TAVERN_MANIFEST_FILENAME = "tavern-manifest.json"
10
+ _TAVERN_MARKER_EXCLUSIONS = {"parametrize", "skip", "skipif", "usefixtures", "xfail"}
11
+
12
+
13
+ def prepare_reports_directory(project):
14
+ return prepare_directory("$dir_reports", project)
15
+
16
+
17
+ def prepare_logs_directory(project):
18
+ return prepare_directory("$dir_logs", project)
19
+
20
+
21
+ def prepare_dist_directory(project):
22
+ return prepare_directory("$dir_dist", project)
23
+
24
+
25
+ def prepare_directory(dir_variable, project):
26
+ package__format = f"{dir_variable}/integration"
27
+ reports_dir = project.expand_path(package__format)
28
+ return _ensure_directory_exists(reports_dir)
29
+
30
+
31
+ def _ensure_directory_exists(path):
32
+ if not os.path.exists(path):
33
+ os.makedirs(path)
34
+ return path
35
+
36
+
37
+ def get_working_distribution_directory(project):
38
+ dist_directory = prepare_dist_directory(project)
39
+ return _ensure_directory_exists(f"{dist_directory}/working")
40
+
41
+
42
+ def get_latest_distribution_directory(project):
43
+ dist_directory = prepare_dist_directory(project)
44
+ from pybuilder_integration import ENVIRONMENT
45
+ environment = project.get_mandatory_property(ENVIRONMENT)
46
+ return _ensure_directory_exists(f"{dist_directory}/LATEST-{environment}")
47
+
48
+
49
+ def get_latest_zipped_distribution_directory(project):
50
+ dist_directory = prepare_dist_directory(project)
51
+ from pybuilder_integration import ENVIRONMENT
52
+ environment = project.get_mandatory_property(ENVIRONMENT)
53
+ return _ensure_directory_exists(f"{dist_directory}/LATEST-{environment}/zipped")
54
+
55
+
56
+ def get_local_zip_artifact_path(tool, project, include_ending=False):
57
+ artifact = f"{prepare_dist_directory(project)}/{tool}-{project.name}"
58
+ if include_ending:
59
+ return f"{artifact}.zip"
60
+ return artifact
61
+
62
+
63
+ def get_tavern_manifest_path(test_dir):
64
+ return os.path.join(test_dir, TAVERN_MANIFEST_DIRECTORY, TAVERN_MANIFEST_FILENAME)
65
+
66
+
67
+ def collect_tavern_service_markers(test_dir):
68
+ markers = set()
69
+ for root, _, files in os.walk(test_dir):
70
+ for filename in files:
71
+ if filename.endswith(".tavern.yaml"):
72
+ markers.update(_collect_markers_from_tavern_file(os.path.join(root, filename)))
73
+ return sorted(markers)
74
+
75
+
76
+ def write_tavern_manifest(test_dir, service_markers=None):
77
+ manifest_path = get_tavern_manifest_path(test_dir)
78
+ manifest_directory = os.path.dirname(manifest_path)
79
+ os.makedirs(manifest_directory, exist_ok=True)
80
+ if service_markers is None:
81
+ service_markers = collect_tavern_service_markers(test_dir)
82
+ manifest_content = json.dumps({"service_markers": sorted(service_markers)}).encode("utf-8")
83
+ descriptor, temporary_path = tempfile.mkstemp(prefix=".tavern-manifest-", dir=manifest_directory)
84
+ try:
85
+ bytes_written = 0
86
+ while bytes_written < len(manifest_content):
87
+ written = os.write(descriptor, manifest_content[bytes_written:])
88
+ if written == 0:
89
+ raise OSError("Could not write Tavern marker manifest")
90
+ bytes_written += written
91
+ os.close(descriptor)
92
+ descriptor = None
93
+ os.replace(temporary_path, manifest_path)
94
+ finally:
95
+ if descriptor is not None:
96
+ os.close(descriptor)
97
+ if os.path.exists(temporary_path):
98
+ os.unlink(temporary_path)
99
+
100
+
101
+ def read_tavern_service_markers(test_dir):
102
+ manifest_path = get_tavern_manifest_path(test_dir)
103
+ if not os.path.exists(manifest_path):
104
+ return None
105
+ try:
106
+ with open(manifest_path) as manifest_file:
107
+ manifest = json.load(manifest_file)
108
+ except (OSError, ValueError):
109
+ return None
110
+ markers = manifest.get("service_markers", [])
111
+ if not isinstance(markers, list) or not all(isinstance(marker, str) for marker in markers):
112
+ return None
113
+ return set(markers)
114
+
115
+
116
+ def _collect_markers_from_tavern_file(path):
117
+ markers = set()
118
+ in_marks = False
119
+ marks_indent = 0
120
+ with open(path) as tavern_file:
121
+ for line in tavern_file:
122
+ match = re.match(r"^(\s*)marks:\s*(.*)$", line)
123
+ if match:
124
+ in_marks = True
125
+ marks_indent = len(match.group(1))
126
+ inline_markers = match.group(2).strip()
127
+ if inline_markers.startswith("[") and inline_markers.endswith("]"):
128
+ for marker in inline_markers[1:-1].split(","):
129
+ _add_tavern_marker(markers, marker)
130
+ continue
131
+ if not in_marks:
132
+ continue
133
+ if not line.strip() or line.lstrip().startswith("#"):
134
+ continue
135
+ indent = len(line) - len(line.lstrip())
136
+ if indent <= marks_indent:
137
+ in_marks = False
138
+ continue
139
+ marker = re.match(r"^\s*-\s*(.+?)\s*(?:#.*)?$", line)
140
+ if marker:
141
+ _add_tavern_marker(markers, marker.group(1))
142
+ return markers
143
+
144
+
145
+ def _add_tavern_marker(markers, value):
146
+ marker = value.strip().strip("'\"")
147
+ if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", marker) and marker not in _TAVERN_MARKER_EXCLUSIONS:
148
+ markers.add(marker)
149
+
150
+
151
+ def package_artifacts(project, test_dir, tool, role):
152
+ # Make a copy for easy access in environment validation
153
+ working_dir = get_working_distribution_directory(project)
154
+ shutil.copytree(test_dir, f"{working_dir}/{tool}",dirs_exist_ok=True)
155
+ if tool == "tavern":
156
+ write_tavern_manifest(f"{working_dir}/{tool}")
157
+ with tempfile.TemporaryDirectory() as tempdir:
158
+ shutil.copytree(test_dir, f"{tempdir}/{role}",dirs_exist_ok=True)
159
+ if tool == "tavern":
160
+ write_tavern_manifest(f"{tempdir}/{role}")
161
+ # package a copy for distribution
162
+ # zip up the test and add them to the integration test dist directory
163
+ base_name = get_local_zip_artifact_path(tool=tool, project=project)
164
+ shutil.make_archive(base_name=base_name,
165
+ base_dir=role,
166
+ format="zip",
167
+ root_dir=tempdir)
168
+
@@ -17,4 +17,8 @@ APPLICATION = "application"
17
17
  APPLICATION_GROUP = "application_group"
18
18
  RUN_PARALLEL = "pytest_parallel"
19
19
  CONSOLIDATE_TESTS = "consolidate_tavern"
20
+ SKIP_NON_MATCHING_TAVERN_ARTIFACTS = "skip_non_matching_tavern_artifacts"
20
21
  TESTING_SCOPE = "testing_scope"
22
+ PARALLEL_VERIFY_ENVIRONMENT = "parallel_verify_environment"
23
+ SUBPROCESS_PROPERTIES_FILE_ENV = "PYBUILDER_INTEGRATION_PROPERTIES_FILE"
24
+ CYPRESS_CACHE_FOLDER = "cypress_cache_folder"
@@ -0,0 +1,548 @@
1
+ import json
2
+ import os
3
+ import re
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ import tempfile
8
+ import xml.etree.ElementTree as ET
9
+
10
+ import pytest
11
+ from pybuilder.core import Project, Logger, RequirementsFile
12
+ from pybuilder.errors import BuildFailedException
13
+ from pybuilder.install_utils import install_dependencies
14
+ from pybuilder.reactor import Reactor
15
+ from pybuilder.utils import Timer
16
+
17
+ from pybuilder_integration import exec_utility, tool_utility, directory_utility
18
+ from pybuilder_integration.artifact_manager import get_artifact_manager
19
+ from pybuilder_integration.cloudwatchlogs_utility import CloudwatchLogs
20
+ from pybuilder_integration.directory_utility import get_working_distribution_directory, \
21
+ package_artifacts, prepare_reports_directory, get_local_zip_artifact_path, prepare_logs_directory, \
22
+ read_tavern_service_markers
23
+ from pybuilder_integration.properties import *
24
+ from pybuilder_integration.tool_utility import install_cypress
25
+
26
+
27
+ _VERIFY_ENVIRONMENT_SUBPROCESS_PROPERTIES = [
28
+ INTEGRATION_ARTIFACT_BUCKET,
29
+ ENVIRONMENT,
30
+ ARTIFACT_MANAGER,
31
+ CYPRESS_TEST_DIR,
32
+ CYPRESS_CONFIG_FILE,
33
+ INTEGRATION_TARGET_URL,
34
+ INTEGRATION_PUBLIC_TARGET_URL,
35
+ TAVERN_ADDITIONAL_ARGS,
36
+ TAVERN_TEST_DIR,
37
+ PROMOTE_ARTIFACT,
38
+ ROLE,
39
+ SHOULD_SKIP_LATEST,
40
+ APPLICATION,
41
+ APPLICATION_GROUP,
42
+ RUN_PARALLEL,
43
+ CONSOLIDATE_TESTS,
44
+ SKIP_NON_MATCHING_TAVERN_ARTIFACTS,
45
+ TESTING_SCOPE,
46
+ "abort_upload",
47
+ "record_cypress",
48
+ "verbose",
49
+ ]
50
+
51
+ def integration_artifact_push(project: Project, logger: Logger, reactor: Reactor):
52
+ logger.info("Starting upload of integration artifacts")
53
+ manager = get_artifact_manager(project)
54
+ for tool in ["tavern", "cypress"]:
55
+ artifact_file = get_local_zip_artifact_path(tool=tool, project=project, include_ending=True)
56
+ if os.path.exists(artifact_file):
57
+ logger.info(
58
+ f"Starting upload of integration artifact: {os.path.basename(artifact_file)} to: {manager.friendly_name}"
59
+ )
60
+ manager.upload(file=artifact_file, project=project, logger=logger, reactor=reactor)
61
+
62
+
63
+ def verify_environment(project: Project, logger: Logger, reactor: Reactor):
64
+ if _get_bool_property(project, PARALLEL_VERIFY_ENVIRONMENT, False):
65
+ _verify_environment_parallel(project, logger, reactor)
66
+ return
67
+ _verify_environment_current(project, logger, reactor)
68
+ latest_directory = _download_latest_environment_artifacts(project, logger, reactor)
69
+ _verify_environment_latest(project, logger, reactor, latest_directory)
70
+ if _get_bool_property(project, PROMOTE_ARTIFACT, True):
71
+ integration_artifact_push(project=project, logger=logger, reactor=reactor)
72
+
73
+
74
+ def _verify_environment_current(project: Project, logger: Logger, reactor: Reactor, dist_directory=None):
75
+ if dist_directory is None:
76
+ dist_directory = project.get_property(WORKING_TEST_DIR, get_working_distribution_directory(project))
77
+ logger.info(f"Preparing to run tests found in: {dist_directory}")
78
+ _run_tests_in_directory(dist_directory, logger, project, reactor)
79
+
80
+
81
+ def _download_latest_environment_artifacts(project: Project, logger: Logger, reactor: Reactor):
82
+ artifact_manager = get_artifact_manager(project=project)
83
+ return artifact_manager.download_artifacts(project=project, logger=logger, reactor=reactor)
84
+
85
+
86
+ def _verify_environment_latest(project: Project, logger: Logger, reactor: Reactor, latest_directory=None):
87
+ if latest_directory is None:
88
+ latest_directory = project.get_property(WORKING_TEST_DIR, None)
89
+ if latest_directory is None:
90
+ latest_directory = get_artifact_manager(project=project).download_artifacts(project=project,
91
+ logger=logger,
92
+ reactor=reactor)
93
+ _run_tests_in_directory(latest_directory, logger, project, reactor, latest=True)
94
+
95
+
96
+ def _verify_environment_parallel(project: Project, logger: Logger, reactor: Reactor):
97
+ dist_directory = project.get_property(WORKING_TEST_DIR, get_working_distribution_directory(project))
98
+ logger.info(f"Preparing to run tests found in: {dist_directory}")
99
+ latest_directory = _download_latest_environment_artifacts(project, logger, reactor)
100
+ passes = [
101
+ {
102
+ "name": "current",
103
+ "task": "verify_environment_current",
104
+ "test_dir": dist_directory,
105
+ },
106
+ {
107
+ "name": "latest",
108
+ "task": "verify_environment_latest",
109
+ "test_dir": latest_directory,
110
+ },
111
+ ]
112
+ _run_verify_environment_passes_in_subprocesses(project, logger, passes)
113
+ if _get_bool_property(project, PROMOTE_ARTIFACT, True):
114
+ integration_artifact_push(project=project, logger=logger, reactor=reactor)
115
+
116
+
117
+ def _run_verify_environment_passes_in_subprocesses(project: Project, logger: Logger, passes):
118
+ properties_file = _write_subprocess_properties_file(project)
119
+ processes = []
120
+ try:
121
+ for pass_config in passes:
122
+ process, log_file = _start_verify_environment_subprocess(project, logger, pass_config, properties_file)
123
+ processes.append((pass_config, process, log_file))
124
+
125
+ failures = []
126
+ for pass_config, process, log_file in processes:
127
+ exit_code = process.wait()
128
+ log_handle = getattr(process, "_pybuilder_integration_log_handle", None)
129
+ if log_handle:
130
+ log_handle.close()
131
+ if exit_code != 0:
132
+ failures.append(f"{pass_config['name']} exit_code={exit_code} log={log_file}")
133
+
134
+ if failures:
135
+ raise BuildFailedException("Parallel verify_environment failed: " + "; ".join(failures))
136
+ finally:
137
+ if os.path.exists(properties_file):
138
+ os.remove(properties_file)
139
+
140
+
141
+ def _start_verify_environment_subprocess(project: Project, logger: Logger, pass_config, properties_file):
142
+ log_file = os.path.join(prepare_logs_directory(project), f"verify_environment_{pass_config['name']}.log")
143
+ command = _build_verify_environment_subprocess_command(project, pass_config)
144
+ env = os.environ.copy()
145
+ env[SUBPROCESS_PROPERTIES_FILE_ENV] = properties_file
146
+ logger.info(f"Starting {pass_config['name']} verify_environment subprocess: {' '.join(command)}")
147
+ log_handle = os.fdopen(
148
+ os.open(log_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600),
149
+ "w",
150
+ encoding="utf-8",
151
+ )
152
+ try:
153
+ process = subprocess.Popen(command,
154
+ stdout=log_handle,
155
+ stderr=subprocess.STDOUT,
156
+ cwd=project.basedir,
157
+ env=env,
158
+ shell=False)
159
+ except Exception:
160
+ log_handle.close()
161
+ raise
162
+ process._pybuilder_integration_log_handle = log_handle
163
+ return process, log_file
164
+
165
+
166
+ def _build_verify_environment_subprocess_command(project: Project, pass_config):
167
+ stage_target = os.path.join(project.expand_path("$dir_target"), "verify_environment", pass_config["name"])
168
+ return [
169
+ sys.executable,
170
+ "-c",
171
+ "from pybuilder.cli import main; main()",
172
+ pass_config["task"],
173
+ "-P", f"{WORKING_TEST_DIR}={pass_config['test_dir']}",
174
+ "-P", f"{PARALLEL_VERIFY_ENVIRONMENT}=False",
175
+ "-P", f"{PROMOTE_ARTIFACT}=False",
176
+ "-P", f"dir_target={stage_target}",
177
+ "-P", f"dir_logs={os.path.join(stage_target, 'logs')}",
178
+ "-P", f"dir_reports={os.path.join(stage_target, 'reports')}",
179
+ ]
180
+
181
+
182
+ def _write_subprocess_properties_file(project: Project):
183
+ properties = _collect_json_safe_project_properties(project)
184
+ fd, path = tempfile.mkstemp(prefix="verify_environment_", suffix=".json", dir=prepare_logs_directory(project))
185
+ with os.fdopen(fd, "w") as fp:
186
+ json.dump(properties, fp)
187
+ return path
188
+
189
+
190
+ def _collect_json_safe_project_properties(project: Project):
191
+ safe_properties = {}
192
+ for name in _VERIFY_ENVIRONMENT_SUBPROCESS_PROPERTIES:
193
+ value = project.get_property(name, None)
194
+ if value is None:
195
+ continue
196
+ try:
197
+ json.dumps(value)
198
+ except (TypeError, ValueError):
199
+ continue
200
+ safe_properties[name] = value
201
+ return safe_properties
202
+
203
+
204
+ def _get_bool_property(project: Project, property_name, default=False):
205
+ value = project.get_property(property_name, default)
206
+ if isinstance(value, str):
207
+ return value.lower() in ["1", "true", "yes", "on"]
208
+ return bool(value)
209
+
210
+
211
+ def _should_run_latest(test_dir, project):
212
+ if project.get_property(SHOULD_SKIP_LATEST, False):
213
+ if project.get_property(ROLE) == test_dir:
214
+ return False
215
+ return True
216
+
217
+
218
+ def _run_tests_in_directory(dist_directory, logger, project, reactor, latest=False):
219
+ logger.debug(f"Run tests in directory: {os.listdir(dist_directory)} ")
220
+ _run_tavern_tests_in_dist_dir(dist_directory, latest, logger, project, reactor)
221
+ _run_cypress_tests_in_dist_dir(dist_directory, latest, logger, project, reactor)
222
+
223
+
224
+ def _run_cypress_tests_in_dist_dir(dist_directory, latest, logger, project, reactor):
225
+ total_time = Timer.start()
226
+ cypress_test_path = f"{dist_directory}/cypress"
227
+ if os.path.exists(cypress_test_path):
228
+ logger.debug(f"Run cypress tests in directory: {cypress_test_path} ")
229
+ logger.debug(f"Run cypress tests in directory files: {os.listdir(cypress_test_path)} ")
230
+ logger.info(f"Found cypress tests - starting run latest: {latest}")
231
+ if latest:
232
+ for test_dir in os.listdir(cypress_test_path):
233
+ directory_to_test = f"{cypress_test_path}/{test_dir}"
234
+ if os.path.isdir(directory_to_test) and _should_run_latest(test_dir, project):
235
+ logger.info(f"Running {directory_to_test}")
236
+ _run_cypress_tests_in_directory(work_dir=directory_to_test,
237
+ logger=logger,
238
+ project=project,
239
+ reactor=reactor)
240
+ else:
241
+ _run_cypress_tests_in_directory(work_dir=cypress_test_path,
242
+ logger=logger,
243
+ project=project,
244
+ reactor=reactor)
245
+ total_time.stop()
246
+ logger.info(f"Ran Cypress tests: {total_time.get_millis()}")
247
+
248
+
249
+ def _run_tavern_tests_in_dist_dir(dist_directory, latest, logger, project, reactor):
250
+ total_time = Timer.start()
251
+ tavern_test_path = f"{dist_directory}/tavern"
252
+ if os.path.exists(tavern_test_path):
253
+ logger.info(f"Found tavern tests - starting run latest: {latest}")
254
+ logger.debug(f"Run tavern tests in directory: {tavern_test_path} ")
255
+ logger.debug(f"Run tavern tests in directory files: {os.listdir(tavern_test_path)} ")
256
+ if latest:
257
+ for tavern_dir in os.listdir(tavern_test_path):
258
+ tavern_test_direcotry = f"{tavern_test_path}/{tavern_dir}"
259
+ if os.path.isdir(tavern_test_direcotry) and _should_run_latest(tavern_dir, project):
260
+ logger.info(f"Running {tavern_test_direcotry}")
261
+ _run_tavern_tests_in_dir(test_dir=tavern_test_direcotry,
262
+ logger=logger,
263
+ project=project,
264
+ reactor=reactor,
265
+ role=os.path.basename(tavern_dir))
266
+ else:
267
+ _run_tavern_tests_in_dir(test_dir=f"{tavern_test_path}",
268
+ logger=logger,
269
+ project=project,
270
+ reactor=reactor)
271
+ total_time.stop()
272
+ logger.info(f"Ran Tavern tests: {total_time.get_millis()}")
273
+
274
+
275
+ def verify_cypress(project: Project, logger: Logger, reactor: Reactor):
276
+ # Get directories with test and cypress executable
277
+ work_dir = project.expand_path(f"${CYPRESS_TEST_DIR}")
278
+ if _run_cypress_tests_in_directory(work_dir=work_dir, logger=logger, project=project, reactor=reactor):
279
+ package_artifacts(project, work_dir, "cypress", project.get_property(ROLE))
280
+
281
+
282
+ def _run_cypress_tests_in_directory(work_dir, logger, project, reactor: Reactor):
283
+ total_time = Timer.start()
284
+ target_url = project.get_mandatory_property(INTEGRATION_TARGET_URL)
285
+ environment = project.get_mandatory_property(ENVIRONMENT)
286
+ if not os.path.exists(work_dir):
287
+ logger.info("Skipping cypress run: no tests")
288
+ return False
289
+ logger.info(f"Found {len(os.listdir(work_dir))} files in cypress test directory")
290
+ logger.debug(f"Files: {os.listdir(work_dir)} ")
291
+ # Validate NPM install and Install cypress
292
+ package_json = os.path.join(work_dir, "package.json")
293
+ if os.path.exists(package_json):
294
+ logger.info("Found package.json installing dependencies")
295
+ tool_utility.install_npm_dependencies(work_dir, project=project, logger=logger, reactor=reactor)
296
+ else:
297
+ install_cypress(logger=logger, project=project, reactor=reactor, work_dir=work_dir)
298
+ total_time.stop()
299
+ logger.info(f"Configured Cypress Environment: {total_time.get_millis()}")
300
+ total_time = Timer.start()
301
+ executable = os.path.join(work_dir, "node_modules/cypress/bin/cypress")
302
+ results_file, run_name = get_test_report_file(project=project, test_dir=work_dir, tool="cypress")
303
+ # Run the actual tests against the baseURL provided by ${integration_target}
304
+ test_report_folder = directory_utility.prepare_reports_directory(project)
305
+ args = ["run", "--config",
306
+ f"baseUrl={target_url},"
307
+ f"videosFolder={test_report_folder}/videos,"
308
+ f"screenshotsFolder={test_report_folder}/screenshots",
309
+ "--reporter-options",
310
+ f"mochaFile={results_file}"]
311
+ if _get_bool_property(project, "record_cypress", True):
312
+ args.append('--record')
313
+ _add_config_file(logger, project, args, environment, work_dir)
314
+ environment_variables = project.get_property(ENVIRONMENT_VARIABLES, {})
315
+ logger.info(f"Running cypress on host: {target_url}")
316
+ try:
317
+ exec_utility.exec_command(command_name=executable, args=args,
318
+ failure_message="Failed to execute cypress tests", log_file_name='cypress_run.log',
319
+ project=project, reactor=reactor, logger=logger, working_dir=work_dir, report=False,
320
+ env_vars=environment_variables)
321
+ # workaround but cypress output are relative to location of cypress.json, so we need to collapse
322
+ if os.path.exists(f"{work_dir}/target"):
323
+ shutil.copytree(f"{work_dir}/target", project.expand_path("$dir_target"), dirs_exist_ok=True)
324
+ finally:
325
+ _write_cypress_summary(project, logger)
326
+ total_time.stop()
327
+ logger.info(f"Ran Cypress Tests: {total_time.get_millis()}")
328
+ return True
329
+
330
+
331
+ def _write_cypress_summary(project, logger):
332
+ """Parse cypress_run.log and write a portable Markdown summary artifact."""
333
+ log_path = os.path.join(prepare_logs_directory(project), "cypress_run.log")
334
+ summary_path = os.path.join(prepare_reports_directory(project), "cypress-summary.md")
335
+ try:
336
+ output = _build_cypress_summary(log_path)
337
+ with os.fdopen(
338
+ os.open(summary_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600),
339
+ "w",
340
+ encoding="utf-8",
341
+ ) as summary_file:
342
+ summary_file.write(output)
343
+ github_summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
344
+ if github_summary_path:
345
+ with os.fdopen(
346
+ os.open(github_summary_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600),
347
+ "a",
348
+ encoding="utf-8",
349
+ ) as f:
350
+ f.write(output)
351
+ print(output)
352
+ return summary_path
353
+ except Exception as e:
354
+ logger.warn(f"[cypress_summary] failed to generate summary: {e}")
355
+
356
+
357
+ def _build_cypress_summary(log_path):
358
+ if not os.path.exists(log_path):
359
+ return "## Cypress Test Results\n\n> warning `cypress_run.log` not found\n"
360
+
361
+ raw = open(log_path, errors="replace").read()
362
+ clean = re.sub(r"\x1b\[[0-9;]*m", "", raw)
363
+
364
+ total = failed = skipped = 0
365
+ failures = []
366
+ for xml_block in re.findall(r"<testsuites[\s\S]*?</testsuites>", clean):
367
+ try:
368
+ root = ET.fromstring(xml_block)
369
+ total += int(root.get("tests", 0))
370
+ failed += int(root.get("failures", 0))
371
+ skipped += int(root.get("skipped", 0) or 0)
372
+ for tc in root.iter("testcase"):
373
+ failure = tc.find("failure")
374
+ if failure is not None:
375
+ suite = tc.get("classname", "").split(".")[-1]
376
+ name = tc.get("name", "")
377
+ msg = (failure.get("message") or failure.text or "").split("\n")[0][:120]
378
+ failures.append((f"{suite} > {name}", msg))
379
+ except ET.ParseError:
380
+ pass
381
+
382
+ passed = total - failed - skipped
383
+ if total == 0:
384
+ icon = "WARN"
385
+ label = "No tests ran - check if Cypress found any spec files"
386
+ elif failed == 0:
387
+ icon = "PASS"
388
+ label = "All tests passed"
389
+ else:
390
+ icon = "FAIL"
391
+ label = f"{failed} test(s) failed"
392
+
393
+ lines = [
394
+ "## Cypress Test Results\n",
395
+ f"### {icon} {label}\n",
396
+ "| Total | Passed | Failed | Skipped |",
397
+ "|-------|--------|--------|---------|",
398
+ f"| {total} | {passed} | {failed} | {skipped} |",
399
+ ]
400
+
401
+ if failures:
402
+ lines.append("\n### Failed Tests\n")
403
+ for title, msg in failures[:20]:
404
+ lines.append(f"- **{title}**")
405
+ if msg:
406
+ lines.append(f" - _{msg}_")
407
+ if len(failures) > 20:
408
+ lines.append(f"\n_...and {len(failures) - 20} more_")
409
+
410
+ return "\n".join(lines) + "\n"
411
+
412
+
413
+ def _add_config_file(logger, project, args, environment, work_dir):
414
+
415
+ # Environment variable override
416
+ cypress_config_file_override = os.environ.get('CYPRESS_CONFIG_FILE')
417
+ if cypress_config_file_override:
418
+ logger.info(f"Found CYPRESS_CONFIG_FILE environment variable: {cypress_config_file_override}")
419
+ project.set_property(CYPRESS_CONFIG_FILE, cypress_config_file_override)
420
+
421
+ # Check project for config file else test file system for environment named file
422
+ cypress_config_file = project.get_property(CYPRESS_CONFIG_FILE)
423
+ if cypress_config_file and os.path.exists(os.path.join(work_dir, cypress_config_file)):
424
+ logger.info(f"Setting --config-file parameter from project to {cypress_config_file}")
425
+ args.append("--config-file")
426
+ args.append(cypress_config_file)
427
+ else:
428
+ logger.info(f"Attempting to discover cypress config file from environment: {environment}")
429
+ for ending in ['ts', 'json']:
430
+ config_file_path = f'{environment}-config.{ending}'
431
+ if os.path.exists(os.path.join(work_dir, config_file_path)):
432
+ logger.info(f"Setting --config-file parameter from env to {config_file_path}")
433
+ args.append("--config-file")
434
+ args.append(config_file_path)
435
+ return
436
+
437
+
438
+ def verify_tavern(project: Project, logger: Logger, reactor: Reactor):
439
+ # Expand the directory to get full path
440
+ test_dir = project.expand_path(f"${TAVERN_TEST_DIR}")
441
+ # Run the tests in the directory
442
+ if _run_tavern_tests_in_dir(test_dir, logger, project, reactor):
443
+ package_artifacts(project, test_dir, "tavern", project.get_property(ROLE))
444
+
445
+
446
+ def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, reactor: Reactor, role=None):
447
+ logger.info("Running tavern tests: {}".format(test_dir))
448
+ if not os.path.exists(test_dir):
449
+ logger.info("Skipping tavern run: no tests")
450
+ return False
451
+ logger.info(f"Found {len(os.listdir(test_dir))} files in tavern test directory")
452
+ # todo is this unique enough for each run?
453
+ output_file, run_name = get_test_report_file(project, test_dir)
454
+ extra_args = [project.expand(prop) for prop in project.get_property(TAVERN_ADDITIONAL_ARGS, [])]
455
+ requested_marker = _extract_tavern_marker_filter(extra_args)
456
+ declared_markers = read_tavern_service_markers(test_dir)
457
+ skip_non_matching_artifacts = _get_bool_property(project, SKIP_NON_MATCHING_TAVERN_ARTIFACTS, False)
458
+ if skip_non_matching_artifacts and requested_marker and declared_markers and requested_marker not in declared_markers:
459
+ logger.info(
460
+ f"Skipping Tavern tests: artifact markers {sorted(declared_markers)} do not match {requested_marker}"
461
+ )
462
+ return True
463
+ from sys import path as syspath
464
+ syspath.insert(0, test_dir)
465
+ # install any requirements that my exist
466
+ requirements_file = os.path.join(test_dir, "requirements.txt")
467
+ if os.path.exists(requirements_file):
468
+ dependency = RequirementsFile(requirements_file)
469
+ install_dependencies(logger, project, dependency, reactor.pybuilder_venv,
470
+ f"{prepare_logs_directory(project)}/install_tavern_pip_dependencies.log")
471
+ args = ["--junit-xml", f"{output_file}", test_dir] + extra_args
472
+ if project.get_property("verbose"):
473
+ args.append("-s")
474
+ args.append("-v")
475
+ if _get_bool_property(project, RUN_PARALLEL, False):
476
+ args.extend(['-n', 'auto'])
477
+ os.environ['TARGET'] = project.get_property(INTEGRATION_TARGET_URL)
478
+ os.environ['PUBLIC_TARGET'] = project.get_property(INTEGRATION_PUBLIC_TARGET_URL)
479
+ os.environ[ENVIRONMENT] = project.get_property(ENVIRONMENT)
480
+ logger.info(f"Running against target: {project.get_property(INTEGRATION_TARGET_URL)} and "
481
+ f"public target: {project.get_property(INTEGRATION_PUBLIC_TARGET_URL)}")
482
+ cache_wd = os.getcwd()
483
+ try:
484
+ os.chdir(test_dir)
485
+ logger.debug(f"Running args: {args} ")
486
+ ret = pytest.main(args)
487
+ if ret == pytest.ExitCode.NO_TESTS_COLLECTED:
488
+ fallback_args = _without_tavern_marker_filter(args)
489
+ if fallback_args != args and (not skip_non_matching_artifacts or not declared_markers):
490
+ logger.warn(
491
+ "No Tavern tests matched the marker filter; rerunning the full suite"
492
+ )
493
+ logger.debug(f"Running fallback args: {fallback_args} ")
494
+ ret = pytest.main(fallback_args)
495
+ finally:
496
+ os.chdir(cache_wd)
497
+
498
+ if ret != 0:
499
+ if role:
500
+ roles = []
501
+ if project.get_property(CONSOLIDATE_TESTS, False):
502
+ with open(f"{test_dir}/roles") as fp:
503
+ for line in fp:
504
+ roles.append(line.strip())
505
+ else:
506
+ roles.append(role)
507
+
508
+ for service in roles:
509
+ CloudwatchLogs(project.get_property(ENVIRONMENT), project.get_property(APPLICATION), service,
510
+ logger).print_latest()
511
+ raise BuildFailedException(f"Tavern tests failed see complete output here - {output_file}")
512
+ return True
513
+
514
+
515
+ def _without_tavern_marker_filter(args):
516
+ marker_options = {"-m", "--markexpr"}
517
+ fallback_args = []
518
+ skip_next = False
519
+ for arg in args:
520
+ if skip_next:
521
+ skip_next = False
522
+ continue
523
+ if arg in marker_options:
524
+ skip_next = True
525
+ continue
526
+ if arg.startswith("--markexpr="):
527
+ continue
528
+ fallback_args.append(arg)
529
+ return fallback_args
530
+
531
+
532
+ def _extract_tavern_marker_filter(args):
533
+ for index, arg in enumerate(args):
534
+ if arg in {"-m", "--markexpr"} and index + 1 < len(args):
535
+ marker = args[index + 1]
536
+ elif arg.startswith("--markexpr="):
537
+ marker = arg.split("=", 1)[1]
538
+ else:
539
+ continue
540
+ if marker.replace("_", "").isalnum() and marker:
541
+ return marker
542
+ return None
543
+
544
+
545
+ def get_test_report_file(project, test_dir, tool="tavern"):
546
+ run_name = os.path.basename(test_dir)
547
+ output_file = os.path.join(prepare_reports_directory(project), f"{tool}-{run_name}.out.xml")
548
+ return output_file, run_name
@@ -0,0 +1,53 @@
1
+ from pybuilder.core import Logger, Project
2
+ from pybuilder.reactor import Reactor
3
+
4
+ from pybuilder_integration.exec_utility import exec_command
5
+ from pybuilder_integration.properties import CYPRESS_CACHE_FOLDER
6
+
7
+
8
+ def install_cypress(logger: Logger, project: Project, reactor: Reactor, work_dir):
9
+ _verify_npm(reactor)
10
+ logger.info(f"Ensuring cypress is installed")
11
+ exec_command('npm', ['install', "cypress"], f'Failed to install cypress - required for integration tests',
12
+ f'{"cypress"}_npm_install.log', project, reactor, logger, report=False, working_dir=work_dir,
13
+ env_vars=_get_npm_env(project))
14
+
15
+
16
+ def _verify_npm(reactor):
17
+ reactor.pybuilder_venv.verify_can_execute(
18
+ command_and_arguments=["npm", "--version"], prerequisite="npm", caller="integration_tests")
19
+
20
+
21
+ def install_npm_dependencies(work_dir, project, logger, reactor):
22
+ _verify_npm(reactor)
23
+ install_command = 'ci' if _has_package_lock(work_dir) else 'install'
24
+ exec_command('npm', [install_command], f'Failed to install package.json - required for integration tests',
25
+ f'package_json_npm_install.log', project, reactor, logger, report=False, working_dir=work_dir,
26
+ env_vars=_get_npm_env(project))
27
+
28
+
29
+ def _has_package_lock(work_dir):
30
+ return any(
31
+ [
32
+ _path_exists(work_dir, "package-lock.json"),
33
+ _path_exists(work_dir, "npm-shrinkwrap.json"),
34
+ ]
35
+ )
36
+
37
+
38
+ def _path_exists(work_dir, filename):
39
+ import os
40
+ return os.path.exists(os.path.join(work_dir, filename))
41
+
42
+
43
+ def _get_npm_env(project):
44
+ import os
45
+ cache_folder = project.get_property(CYPRESS_CACHE_FOLDER, None)
46
+ if cache_folder:
47
+ return {"CYPRESS_CACHE_FOLDER": project.expand_path(cache_folder)}
48
+ cache_folder = os.environ.get("CYPRESS_CACHE_FOLDER")
49
+ if not cache_folder:
50
+ return {}
51
+ return {"CYPRESS_CACHE_FOLDER": cache_folder}
52
+
53
+
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pybuilder-integration
3
- Version: 102
3
+ Version: 105
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:
@@ -13,5 +13,11 @@ Classifier: Programming Language :: Python
13
13
  Requires-Dist: pytest
14
14
  Requires-Dist: tavern==1.25.2
15
15
  Requires-Dist: boto3
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: home-page
19
+ Dynamic: license
20
+ Dynamic: requires-dist
21
+ Dynamic: summary
16
22
 
17
23
  A pybuilder plugin that runs integration tests against a target. This is intended to be a broader scope than unit-tests encompassing dependant functionality.
@@ -21,7 +21,7 @@ class install(_install):
21
21
  if __name__ == '__main__':
22
22
  setup(
23
23
  name = 'pybuilder-integration',
24
- version = '102',
24
+ version = '105',
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,
@@ -53,6 +53,7 @@ if __name__ == '__main__':
53
53
  'tavern==1.25.2',
54
54
  'boto3'
55
55
  ],
56
+ extras_require = {},
56
57
  dependency_links = [],
57
58
  zip_safe = True,
58
59
  cmdclass = {'install': install},
@@ -1,70 +0,0 @@
1
- import os
2
- import shutil
3
- import tempfile
4
-
5
-
6
-
7
- def prepare_reports_directory(project):
8
- return prepare_directory("$dir_reports", project)
9
-
10
-
11
- def prepare_logs_directory(project):
12
- return prepare_directory("$dir_logs", project)
13
-
14
-
15
- def prepare_dist_directory(project):
16
- return prepare_directory("$dir_dist", project)
17
-
18
-
19
- def prepare_directory(dir_variable, project):
20
- package__format = f"{dir_variable}/integration"
21
- reports_dir = project.expand_path(package__format)
22
- return _ensure_directory_exists(reports_dir)
23
-
24
-
25
- def _ensure_directory_exists(path):
26
- if not os.path.exists(path):
27
- os.makedirs(path)
28
- return path
29
-
30
-
31
- def get_working_distribution_directory(project):
32
- dist_directory = prepare_dist_directory(project)
33
- return _ensure_directory_exists(f"{dist_directory}/working")
34
-
35
-
36
- def get_latest_distribution_directory(project):
37
- dist_directory = prepare_dist_directory(project)
38
- from pybuilder_integration import ENVIRONMENT
39
- environment = project.get_mandatory_property(ENVIRONMENT)
40
- return _ensure_directory_exists(f"{dist_directory}/LATEST-{environment}")
41
-
42
-
43
- def get_latest_zipped_distribution_directory(project):
44
- dist_directory = prepare_dist_directory(project)
45
- from pybuilder_integration import ENVIRONMENT
46
- environment = project.get_mandatory_property(ENVIRONMENT)
47
- return _ensure_directory_exists(f"{dist_directory}/LATEST-{environment}/zipped")
48
-
49
-
50
- def get_local_zip_artifact_path(tool, project, include_ending=False):
51
- artifact = f"{prepare_dist_directory(project)}/{tool}-{project.name}"
52
- if include_ending:
53
- return f"{artifact}.zip"
54
- return artifact
55
-
56
-
57
- def package_artifacts(project, test_dir, tool, role):
58
- # Make a copy for easy access in environment validation
59
- working_dir = get_working_distribution_directory(project)
60
- shutil.copytree(test_dir, f"{working_dir}/{tool}",dirs_exist_ok=True)
61
- with tempfile.TemporaryDirectory() as tempdir:
62
- shutil.copytree(test_dir, f"{tempdir}/{role}",dirs_exist_ok=True)
63
- # package a copy for distribution
64
- # zip up the test and add them to the integration test dist directory
65
- base_name = get_local_zip_artifact_path(tool=tool, project=project)
66
- shutil.make_archive(base_name=base_name,
67
- base_dir=role,
68
- format="zip",
69
- root_dir=tempdir)
70
-
@@ -1,249 +0,0 @@
1
- import os
2
- import shutil
3
-
4
- import pytest
5
- from pybuilder.core import Project, Logger, RequirementsFile
6
- from pybuilder.errors import BuildFailedException
7
- from pybuilder.install_utils import install_dependencies
8
- from pybuilder.reactor import Reactor
9
- from pybuilder.utils import Timer
10
-
11
- from pybuilder_integration import exec_utility, tool_utility, directory_utility
12
- from pybuilder_integration.artifact_manager import get_artifact_manager
13
- from pybuilder_integration.cloudwatchlogs_utility import CloudwatchLogs
14
- from pybuilder_integration.directory_utility import get_working_distribution_directory, \
15
- package_artifacts, prepare_reports_directory, get_local_zip_artifact_path, prepare_logs_directory
16
- from pybuilder_integration.properties import *
17
- from pybuilder_integration.tool_utility import install_cypress
18
-
19
-
20
- def integration_artifact_push(project: Project, logger: Logger, reactor: Reactor):
21
- logger.info("Starting upload of integration artifacts")
22
- manager = get_artifact_manager(project)
23
- for tool in ["tavern", "cypress"]:
24
- artifact_file = get_local_zip_artifact_path(tool=tool, project=project, include_ending=True)
25
- if os.path.exists(artifact_file):
26
- logger.info(
27
- f"Starting upload of integration artifact: {os.path.basename(artifact_file)} to: {manager.friendly_name}"
28
- )
29
- manager.upload(file=artifact_file, project=project, logger=logger, reactor=reactor)
30
-
31
-
32
- def verify_environment(project: Project, logger: Logger, reactor: Reactor):
33
- dist_directory = project.get_property(WORKING_TEST_DIR, get_working_distribution_directory(project))
34
- logger.info(f"Preparing to run tests found in: {dist_directory}")
35
- _run_tests_in_directory(dist_directory, logger, project, reactor)
36
- artifact_manager = get_artifact_manager(project=project)
37
- latest_directory = artifact_manager.download_artifacts(project=project, logger=logger, reactor=reactor)
38
- _run_tests_in_directory(latest_directory, logger, project, reactor, latest=True)
39
- if project.get_property(PROMOTE_ARTIFACT, True):
40
- integration_artifact_push(project=project, logger=logger, reactor=reactor)
41
-
42
-
43
- def _should_run_latest(test_dir, project):
44
- if project.get_property(SHOULD_SKIP_LATEST, False):
45
- if project.get_property(ROLE) == test_dir:
46
- return False
47
- return True
48
-
49
-
50
- def _run_tests_in_directory(dist_directory, logger, project, reactor, latest=False):
51
- logger.debug(f"Run tests in directory: {os.listdir(dist_directory)} ")
52
- _run_tavern_tests_in_dist_dir(dist_directory, latest, logger, project, reactor)
53
- _run_cypress_tests_in_dist_dir(dist_directory, latest, logger, project, reactor)
54
-
55
-
56
- def _run_cypress_tests_in_dist_dir(dist_directory, latest, logger, project, reactor):
57
- total_time = Timer.start()
58
- cypress_test_path = f"{dist_directory}/cypress"
59
- if os.path.exists(cypress_test_path):
60
- logger.debug(f"Run cypress tests in directory: {cypress_test_path} ")
61
- logger.debug(f"Run cypress tests in directory files: {os.listdir(cypress_test_path)} ")
62
- logger.info(f"Found cypress tests - starting run latest: {latest}")
63
- if latest:
64
- for test_dir in os.listdir(cypress_test_path):
65
- directory_to_test = f"{cypress_test_path}/{test_dir}"
66
- if os.path.isdir(directory_to_test) and _should_run_latest(test_dir, project):
67
- logger.info(f"Running {directory_to_test}")
68
- _run_cypress_tests_in_directory(work_dir=directory_to_test,
69
- logger=logger,
70
- project=project,
71
- reactor=reactor)
72
- else:
73
- _run_cypress_tests_in_directory(work_dir=cypress_test_path,
74
- logger=logger,
75
- project=project,
76
- reactor=reactor)
77
- total_time.stop()
78
- logger.info(f"Ran Cypress tests: {total_time.get_millis()}")
79
-
80
-
81
- def _run_tavern_tests_in_dist_dir(dist_directory, latest, logger, project, reactor):
82
- total_time = Timer.start()
83
- tavern_test_path = f"{dist_directory}/tavern"
84
- if os.path.exists(tavern_test_path):
85
- logger.info(f"Found tavern tests - starting run latest: {latest}")
86
- logger.debug(f"Run tavern tests in directory: {tavern_test_path} ")
87
- logger.debug(f"Run tavern tests in directory files: {os.listdir(tavern_test_path)} ")
88
- if latest:
89
- for tavern_dir in os.listdir(tavern_test_path):
90
- tavern_test_direcotry = f"{tavern_test_path}/{tavern_dir}"
91
- if os.path.isdir(tavern_test_direcotry) and _should_run_latest(tavern_dir, project):
92
- logger.info(f"Running {tavern_test_direcotry}")
93
- _run_tavern_tests_in_dir(test_dir=tavern_test_direcotry,
94
- logger=logger,
95
- project=project,
96
- reactor=reactor,
97
- role=os.path.basename(tavern_dir))
98
- else:
99
- _run_tavern_tests_in_dir(test_dir=f"{tavern_test_path}",
100
- logger=logger,
101
- project=project,
102
- reactor=reactor)
103
- total_time.stop()
104
- logger.info(f"Ran Tavern tests: {total_time.get_millis()}")
105
-
106
-
107
- def verify_cypress(project: Project, logger: Logger, reactor: Reactor):
108
- # Get directories with test and cypress executable
109
- work_dir = project.expand_path(f"${CYPRESS_TEST_DIR}")
110
- if _run_cypress_tests_in_directory(work_dir=work_dir, logger=logger, project=project, reactor=reactor):
111
- package_artifacts(project, work_dir, "cypress", project.get_property(ROLE))
112
-
113
-
114
- def _run_cypress_tests_in_directory(work_dir, logger, project, reactor: Reactor):
115
- total_time = Timer.start()
116
- target_url = project.get_mandatory_property(INTEGRATION_TARGET_URL)
117
- environment = project.get_mandatory_property(ENVIRONMENT)
118
- if not os.path.exists(work_dir):
119
- logger.info("Skipping cypress run: no tests")
120
- return False
121
- logger.info(f"Found {len(os.listdir(work_dir))} files in cypress test directory")
122
- logger.debug(f"Files: {os.listdir(work_dir)} ")
123
- # Validate NPM install and Install cypress
124
- package_json = os.path.join(work_dir, "package.json")
125
- if os.path.exists(package_json):
126
- logger.info("Found package.json installing dependencies")
127
- tool_utility.install_npm_dependencies(work_dir, project=project, logger=logger, reactor=reactor)
128
- else:
129
- install_cypress(logger=logger, project=project, reactor=reactor, work_dir=work_dir)
130
- total_time.stop()
131
- logger.info(f"Configured Cypress Environment: {total_time.get_millis()}")
132
- total_time = Timer.start()
133
- executable = os.path.join(work_dir, "node_modules/cypress/bin/cypress")
134
- results_file, run_name = get_test_report_file(project=project, test_dir=work_dir, tool="cypress")
135
- # Run the actual tests against the baseURL provided by ${integration_target}
136
- test_report_folder = directory_utility.prepare_reports_directory(project)
137
- args = ["run", "--config",
138
- f"baseUrl={target_url},"
139
- f"videosFolder={test_report_folder}/videos,"
140
- f"screenshotsFolder={test_report_folder}/screenshots",
141
- "--reporter-options",
142
- f"mochaFile={results_file}"]
143
- if project.get_property("record_cypress", True):
144
- args.append('--record')
145
- _add_config_file(logger, project, args, environment, work_dir)
146
- environment_variables = project.get_property(ENVIRONMENT_VARIABLES, {})
147
- logger.info(f"Running cypress on host: {target_url}")
148
- exec_utility.exec_command(command_name=executable, args=args,
149
- failure_message="Failed to execute cypress tests", log_file_name='cypress_run.log',
150
- project=project, reactor=reactor, logger=logger, working_dir=work_dir, report=False,
151
- env_vars=environment_variables)
152
- # workaround but cypress output are relative to location of cypress.json, so we need to collapse
153
- if os.path.exists(f"{work_dir}/target"):
154
- shutil.copytree(f"{work_dir}/target", "./target", dirs_exist_ok=True)
155
- total_time.stop()
156
- logger.info(f"Ran Cypress Tests: {total_time.get_millis()}")
157
- return True
158
-
159
-
160
- def _add_config_file(logger, project, args, environment, work_dir):
161
-
162
- # Environment variable override
163
- cypress_config_file_override = os.environ.get('CYPRESS_CONFIG_FILE')
164
- if cypress_config_file_override:
165
- logger.info(f"Found CYPRESS_CONFIG_FILE environment variable: {cypress_config_file_override}")
166
- project.set_property(CYPRESS_CONFIG_FILE, cypress_config_file_override)
167
-
168
- # Check project for config file else test file system for environment named file
169
- cypress_config_file = project.get_property(CYPRESS_CONFIG_FILE)
170
- if cypress_config_file and os.path.exists(os.path.join(work_dir, cypress_config_file)):
171
- logger.info(f"Setting --config-file parameter from project to {cypress_config_file}")
172
- args.append("--config-file")
173
- args.append(cypress_config_file)
174
- else:
175
- logger.info(f"Attempting to discover cypress config file from environment: {environment}")
176
- for ending in ['ts', 'json']:
177
- config_file_path = f'{environment}-config.{ending}'
178
- if os.path.exists(os.path.join(work_dir, config_file_path)):
179
- logger.info(f"Setting --config-file parameter from env to {config_file_path}")
180
- args.append("--config-file")
181
- args.append(config_file_path)
182
- return
183
-
184
-
185
- def verify_tavern(project: Project, logger: Logger, reactor: Reactor):
186
- # Expand the directory to get full path
187
- test_dir = project.expand_path(f"${TAVERN_TEST_DIR}")
188
- # Run the tests in the directory
189
- if _run_tavern_tests_in_dir(test_dir, logger, project, reactor):
190
- package_artifacts(project, test_dir, "tavern", project.get_property(ROLE))
191
-
192
-
193
- def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, reactor: Reactor, role=None):
194
- logger.info("Running tavern tests: {}".format(test_dir))
195
- if not os.path.exists(test_dir):
196
- logger.info("Skipping tavern run: no tests")
197
- return False
198
- logger.info(f"Found {len(os.listdir(test_dir))} files in tavern test directory")
199
- # todo is this unique enough for each run?
200
- output_file, run_name = get_test_report_file(project, test_dir)
201
- from sys import path as syspath
202
- syspath.insert(0, test_dir)
203
- # install any requirements that my exist
204
- requirements_file = os.path.join(test_dir, "requirements.txt")
205
- if os.path.exists(requirements_file):
206
- dependency = RequirementsFile(requirements_file)
207
- install_dependencies(logger, project, dependency, reactor.pybuilder_venv,
208
- f"{prepare_logs_directory(project)}/install_tavern_pip_dependencies.log")
209
- extra_args = [project.expand(prop) for prop in project.get_property(TAVERN_ADDITIONAL_ARGS, [])]
210
- args = ["--junit-xml", f"{output_file}", test_dir] + extra_args
211
- if project.get_property("verbose"):
212
- args.append("-s")
213
- args.append("-v")
214
- if project.get_property(RUN_PARALLEL, False):
215
- args.extend(['-n', 'auto'])
216
- os.environ['TARGET'] = project.get_property(INTEGRATION_TARGET_URL)
217
- os.environ['PUBLIC_TARGET'] = project.get_property(INTEGRATION_PUBLIC_TARGET_URL)
218
- os.environ[ENVIRONMENT] = project.get_property(ENVIRONMENT)
219
- logger.info(f"Running against target: {project.get_property(INTEGRATION_TARGET_URL)} and "
220
- f"public target: {project.get_property(INTEGRATION_PUBLIC_TARGET_URL)}")
221
- cache_wd = os.getcwd()
222
- try:
223
- os.chdir(test_dir)
224
- logger.debug(f"Running args: {args} ")
225
- ret = pytest.main(args)
226
- finally:
227
- os.chdir(cache_wd)
228
-
229
- if ret != 0:
230
- if role:
231
- roles = []
232
- if project.get_property(CONSOLIDATE_TESTS, False):
233
- with open(f"{test_dir}/roles") as fp:
234
- for line in fp:
235
- roles.append(line.strip())
236
- else:
237
- roles.append(role)
238
-
239
- for service in roles:
240
- CloudwatchLogs(project.get_property(ENVIRONMENT), project.get_property(APPLICATION), service,
241
- logger).print_latest()
242
- raise BuildFailedException(f"Tavern tests failed see complete output here - {output_file}")
243
- return True
244
-
245
-
246
- def get_test_report_file(project, test_dir, tool="tavern"):
247
- run_name = os.path.basename(test_dir)
248
- output_file = os.path.join(prepare_reports_directory(project), f"{tool}-{run_name}.out.xml")
249
- return output_file, run_name
@@ -1,24 +0,0 @@
1
- from pybuilder.core import Logger, Project
2
- from pybuilder.reactor import Reactor
3
-
4
- from pybuilder_integration.exec_utility import exec_command
5
-
6
-
7
- def install_cypress(logger: Logger, project: Project, reactor: Reactor, work_dir):
8
- _verify_npm(reactor)
9
- logger.info(f"Ensuring cypress is installed")
10
- exec_command('npm', ['install', "cypress"], f'Failed to install cypress - required for integration tests',
11
- f'{"cypress"}_npm_install.log', project, reactor, logger, report=False, working_dir=work_dir)
12
-
13
-
14
- def _verify_npm(reactor):
15
- reactor.pybuilder_venv.verify_can_execute(
16
- command_and_arguments=["npm", "--version"], prerequisite="npm", caller="integration_tests")
17
-
18
-
19
- def install_npm_dependencies(work_dir, project, logger, reactor):
20
- _verify_npm(reactor)
21
- exec_command('npm', ['install'], f'Failed to install package.json - required for integration tests',
22
- f'package_json_npm_install.log', project, reactor, logger, report=False, working_dir=work_dir)
23
-
24
-