pybuilder-integration 101__tar.gz → 104__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pybuilder-integration-101 → pybuilder_integration-104}/PKG-INFO +8 -2
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration/__init__.py +24 -1
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration/artifact_manager.py +12 -3
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration/properties.py +4 -0
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration/tasks.py +288 -12
- pybuilder_integration-104/pybuilder_integration/tool_utility.py +53 -0
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration.egg-info/PKG-INFO +8 -2
- {pybuilder-integration-101 → pybuilder_integration-104}/setup.py +2 -1
- pybuilder-integration-101/pybuilder_integration/tool_utility.py +0 -24
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration/cloudwatchlogs_utility.py +0 -0
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration/directory_utility.py +0 -0
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration/exec_utility.py +0 -0
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration.egg-info/SOURCES.txt +0 -0
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration.egg-info/dependency_links.txt +0 -0
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration.egg-info/namespace_packages.txt +0 -0
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration.egg-info/requires.txt +0 -0
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration.egg-info/top_level.txt +0 -0
- {pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration.egg-info/zip-safe +0 -0
- {pybuilder-integration-101 → pybuilder_integration-104}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pybuilder-integration
|
|
3
|
-
Version:
|
|
3
|
+
Version: 104
|
|
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."
|
|
@@ -27,12 +39,23 @@ def init_plugin(project):
|
|
|
27
39
|
f"\t${INTEGRATION_TARGET_URL} - (required) Full URL target for tests\n"
|
|
28
40
|
f"\t${INTEGRATION_PUBLIC_TARGET_URL} - (required) Full public URL target for tests\n"
|
|
29
41
|
f"\t${ENVIRONMENT} - (required) Environment that is being tested (ci/prod)\n"
|
|
42
|
+
f"\t${TESTING_SCOPE} - (optional) Will limit scope of tests, default is to the same application - * can be used.\n"
|
|
30
43
|
f"\t${PROMOTE_ARTIFACT} - Promote integration tests to LATEST-${ENVIRONMENT} (default TRUE)\n"
|
|
31
44
|
)
|
|
32
45
|
def verify_environment(project: Project, logger: Logger, reactor: Reactor):
|
|
33
46
|
tasks.verify_environment(project, logger, reactor)
|
|
34
47
|
|
|
35
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
|
+
|
|
36
59
|
@task(description="Run integration tests using a cypress spec. Requires NPM installed.\n"
|
|
37
60
|
f"\t{INTEGRATION_TARGET_URL} - (required) Full URL target for cypress tests\n"
|
|
38
61
|
f"\t{INTEGRATION_PUBLIC_TARGET_URL} - (required) Full public URL target for cypress tests\n"
|
{pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration/artifact_manager.py
RENAMED
|
@@ -174,13 +174,22 @@ def extract_application_role(logger, project):
|
|
|
174
174
|
return app_group, app_name, role
|
|
175
175
|
|
|
176
176
|
|
|
177
|
+
def in_scope(scope, filename):
|
|
178
|
+
if scope == "*": return True
|
|
179
|
+
return scope in filename
|
|
180
|
+
|
|
181
|
+
|
|
177
182
|
def _unzip_downloaded_artifacts(dir_with_zips: str, destination: str, logger: Logger, project:Project) -> str:
|
|
183
|
+
scope = project.get_property(TESTING_SCOPE,project.get_property(APPLICATION,'*'))
|
|
178
184
|
for file in os.listdir(dir_with_zips):
|
|
179
185
|
# expect {tool}-{self.project.name}.zip
|
|
180
|
-
|
|
186
|
+
filename = os.path.basename(file)
|
|
187
|
+
if not in_scope(scope, filename):
|
|
188
|
+
continue
|
|
189
|
+
if filename.find("tavern") >= 0:
|
|
181
190
|
shutil.unpack_archive(filename=os.path.join(dir_with_zips, file), extract_dir=f"{destination}/tavern",
|
|
182
191
|
format="zip")
|
|
183
|
-
elif
|
|
192
|
+
elif filename.find("cypress") >= 0:
|
|
184
193
|
shutil.unpack_archive(filename=os.path.join(dir_with_zips, file), extract_dir=f"{destination}/cypress",
|
|
185
194
|
format="zip")
|
|
186
195
|
else:
|
|
@@ -197,7 +206,7 @@ def _unzip_downloaded_artifacts(dir_with_zips: str, destination: str, logger: Lo
|
|
|
197
206
|
shutil.copytree(f"{destination}/tavern/{dirn}", consolidated_folder, dirs_exist_ok=True)
|
|
198
207
|
shutil.rmtree(f"{destination}/tavern/{dirn}")
|
|
199
208
|
with open(f"{consolidated_folder}/roles", "a") as fp:
|
|
200
|
-
fp.
|
|
209
|
+
fp.write(f"{dirn}\n")
|
|
201
210
|
return destination
|
|
202
211
|
|
|
203
212
|
|
|
@@ -17,3 +17,7 @@ APPLICATION = "application"
|
|
|
17
17
|
APPLICATION_GROUP = "application_group"
|
|
18
18
|
RUN_PARALLEL = "pytest_parallel"
|
|
19
19
|
CONSOLIDATE_TESTS = "consolidate_tavern"
|
|
20
|
+
TESTING_SCOPE = "testing_scope"
|
|
21
|
+
PARALLEL_VERIFY_ENVIRONMENT = "parallel_verify_environment"
|
|
22
|
+
SUBPROCESS_PROPERTIES_FILE_ENV = "PYBUILDER_INTEGRATION_PROPERTIES_FILE"
|
|
23
|
+
CYPRESS_CACHE_FOLDER = "cypress_cache_folder"
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
3
|
+
import re
|
|
2
4
|
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import xml.etree.ElementTree as ET
|
|
3
9
|
|
|
4
10
|
import pytest
|
|
5
11
|
from pybuilder.core import Project, Logger, RequirementsFile
|
|
@@ -17,6 +23,29 @@ from pybuilder_integration.properties import *
|
|
|
17
23
|
from pybuilder_integration.tool_utility import install_cypress
|
|
18
24
|
|
|
19
25
|
|
|
26
|
+
_VERIFY_ENVIRONMENT_SUBPROCESS_PROPERTIES = [
|
|
27
|
+
INTEGRATION_ARTIFACT_BUCKET,
|
|
28
|
+
ENVIRONMENT,
|
|
29
|
+
ARTIFACT_MANAGER,
|
|
30
|
+
CYPRESS_TEST_DIR,
|
|
31
|
+
CYPRESS_CONFIG_FILE,
|
|
32
|
+
INTEGRATION_TARGET_URL,
|
|
33
|
+
INTEGRATION_PUBLIC_TARGET_URL,
|
|
34
|
+
TAVERN_ADDITIONAL_ARGS,
|
|
35
|
+
TAVERN_TEST_DIR,
|
|
36
|
+
PROMOTE_ARTIFACT,
|
|
37
|
+
ROLE,
|
|
38
|
+
SHOULD_SKIP_LATEST,
|
|
39
|
+
APPLICATION,
|
|
40
|
+
APPLICATION_GROUP,
|
|
41
|
+
RUN_PARALLEL,
|
|
42
|
+
CONSOLIDATE_TESTS,
|
|
43
|
+
TESTING_SCOPE,
|
|
44
|
+
"abort_upload",
|
|
45
|
+
"record_cypress",
|
|
46
|
+
"verbose",
|
|
47
|
+
]
|
|
48
|
+
|
|
20
49
|
def integration_artifact_push(project: Project, logger: Logger, reactor: Reactor):
|
|
21
50
|
logger.info("Starting upload of integration artifacts")
|
|
22
51
|
manager = get_artifact_manager(project)
|
|
@@ -30,16 +59,153 @@ def integration_artifact_push(project: Project, logger: Logger, reactor: Reactor
|
|
|
30
59
|
|
|
31
60
|
|
|
32
61
|
def verify_environment(project: Project, logger: Logger, reactor: Reactor):
|
|
33
|
-
|
|
62
|
+
if _get_bool_property(project, PARALLEL_VERIFY_ENVIRONMENT, False):
|
|
63
|
+
_verify_environment_parallel(project, logger, reactor)
|
|
64
|
+
return
|
|
65
|
+
_verify_environment_current(project, logger, reactor)
|
|
66
|
+
latest_directory = _download_latest_environment_artifacts(project, logger, reactor)
|
|
67
|
+
_verify_environment_latest(project, logger, reactor, latest_directory)
|
|
68
|
+
if _get_bool_property(project, PROMOTE_ARTIFACT, True):
|
|
69
|
+
integration_artifact_push(project=project, logger=logger, reactor=reactor)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _verify_environment_current(project: Project, logger: Logger, reactor: Reactor, dist_directory=None):
|
|
73
|
+
if dist_directory is None:
|
|
74
|
+
dist_directory = project.get_property(WORKING_TEST_DIR, get_working_distribution_directory(project))
|
|
34
75
|
logger.info(f"Preparing to run tests found in: {dist_directory}")
|
|
35
76
|
_run_tests_in_directory(dist_directory, logger, project, reactor)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _download_latest_environment_artifacts(project: Project, logger: Logger, reactor: Reactor):
|
|
36
80
|
artifact_manager = get_artifact_manager(project=project)
|
|
37
|
-
|
|
81
|
+
return artifact_manager.download_artifacts(project=project, logger=logger, reactor=reactor)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _verify_environment_latest(project: Project, logger: Logger, reactor: Reactor, latest_directory=None):
|
|
85
|
+
if latest_directory is None:
|
|
86
|
+
latest_directory = project.get_property(WORKING_TEST_DIR, None)
|
|
87
|
+
if latest_directory is None:
|
|
88
|
+
latest_directory = get_artifact_manager(project=project).download_artifacts(project=project,
|
|
89
|
+
logger=logger,
|
|
90
|
+
reactor=reactor)
|
|
38
91
|
_run_tests_in_directory(latest_directory, logger, project, reactor, latest=True)
|
|
39
|
-
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _verify_environment_parallel(project: Project, logger: Logger, reactor: Reactor):
|
|
95
|
+
dist_directory = project.get_property(WORKING_TEST_DIR, get_working_distribution_directory(project))
|
|
96
|
+
logger.info(f"Preparing to run tests found in: {dist_directory}")
|
|
97
|
+
latest_directory = _download_latest_environment_artifacts(project, logger, reactor)
|
|
98
|
+
passes = [
|
|
99
|
+
{
|
|
100
|
+
"name": "current",
|
|
101
|
+
"task": "verify_environment_current",
|
|
102
|
+
"test_dir": dist_directory,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "latest",
|
|
106
|
+
"task": "verify_environment_latest",
|
|
107
|
+
"test_dir": latest_directory,
|
|
108
|
+
},
|
|
109
|
+
]
|
|
110
|
+
_run_verify_environment_passes_in_subprocesses(project, logger, passes)
|
|
111
|
+
if _get_bool_property(project, PROMOTE_ARTIFACT, True):
|
|
40
112
|
integration_artifact_push(project=project, logger=logger, reactor=reactor)
|
|
41
113
|
|
|
42
114
|
|
|
115
|
+
def _run_verify_environment_passes_in_subprocesses(project: Project, logger: Logger, passes):
|
|
116
|
+
properties_file = _write_subprocess_properties_file(project)
|
|
117
|
+
processes = []
|
|
118
|
+
try:
|
|
119
|
+
for pass_config in passes:
|
|
120
|
+
process, log_file = _start_verify_environment_subprocess(project, logger, pass_config, properties_file)
|
|
121
|
+
processes.append((pass_config, process, log_file))
|
|
122
|
+
|
|
123
|
+
failures = []
|
|
124
|
+
for pass_config, process, log_file in processes:
|
|
125
|
+
exit_code = process.wait()
|
|
126
|
+
log_handle = getattr(process, "_pybuilder_integration_log_handle", None)
|
|
127
|
+
if log_handle:
|
|
128
|
+
log_handle.close()
|
|
129
|
+
if exit_code != 0:
|
|
130
|
+
failures.append(f"{pass_config['name']} exit_code={exit_code} log={log_file}")
|
|
131
|
+
|
|
132
|
+
if failures:
|
|
133
|
+
raise BuildFailedException("Parallel verify_environment failed: " + "; ".join(failures))
|
|
134
|
+
finally:
|
|
135
|
+
if os.path.exists(properties_file):
|
|
136
|
+
os.remove(properties_file)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _start_verify_environment_subprocess(project: Project, logger: Logger, pass_config, properties_file):
|
|
140
|
+
log_file = os.path.join(prepare_logs_directory(project), f"verify_environment_{pass_config['name']}.log")
|
|
141
|
+
command = _build_verify_environment_subprocess_command(project, pass_config)
|
|
142
|
+
env = os.environ.copy()
|
|
143
|
+
env[SUBPROCESS_PROPERTIES_FILE_ENV] = properties_file
|
|
144
|
+
logger.info(f"Starting {pass_config['name']} verify_environment subprocess: {' '.join(command)}")
|
|
145
|
+
log_handle = os.fdopen(
|
|
146
|
+
os.open(log_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600),
|
|
147
|
+
"w",
|
|
148
|
+
encoding="utf-8",
|
|
149
|
+
)
|
|
150
|
+
try:
|
|
151
|
+
process = subprocess.Popen(command,
|
|
152
|
+
stdout=log_handle,
|
|
153
|
+
stderr=subprocess.STDOUT,
|
|
154
|
+
cwd=project.basedir,
|
|
155
|
+
env=env,
|
|
156
|
+
shell=False)
|
|
157
|
+
except Exception:
|
|
158
|
+
log_handle.close()
|
|
159
|
+
raise
|
|
160
|
+
process._pybuilder_integration_log_handle = log_handle
|
|
161
|
+
return process, log_file
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _build_verify_environment_subprocess_command(project: Project, pass_config):
|
|
165
|
+
stage_target = os.path.join(project.expand_path("$dir_target"), "verify_environment", pass_config["name"])
|
|
166
|
+
return [
|
|
167
|
+
sys.executable,
|
|
168
|
+
"-c",
|
|
169
|
+
"from pybuilder.cli import main; main()",
|
|
170
|
+
pass_config["task"],
|
|
171
|
+
"-P", f"{WORKING_TEST_DIR}={pass_config['test_dir']}",
|
|
172
|
+
"-P", f"{PARALLEL_VERIFY_ENVIRONMENT}=False",
|
|
173
|
+
"-P", f"{PROMOTE_ARTIFACT}=False",
|
|
174
|
+
"-P", f"dir_target={stage_target}",
|
|
175
|
+
"-P", f"dir_logs={os.path.join(stage_target, 'logs')}",
|
|
176
|
+
"-P", f"dir_reports={os.path.join(stage_target, 'reports')}",
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _write_subprocess_properties_file(project: Project):
|
|
181
|
+
properties = _collect_json_safe_project_properties(project)
|
|
182
|
+
fd, path = tempfile.mkstemp(prefix="verify_environment_", suffix=".json", dir=prepare_logs_directory(project))
|
|
183
|
+
with os.fdopen(fd, "w") as fp:
|
|
184
|
+
json.dump(properties, fp)
|
|
185
|
+
return path
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _collect_json_safe_project_properties(project: Project):
|
|
189
|
+
safe_properties = {}
|
|
190
|
+
for name in _VERIFY_ENVIRONMENT_SUBPROCESS_PROPERTIES:
|
|
191
|
+
value = project.get_property(name, None)
|
|
192
|
+
if value is None:
|
|
193
|
+
continue
|
|
194
|
+
try:
|
|
195
|
+
json.dumps(value)
|
|
196
|
+
except (TypeError, ValueError):
|
|
197
|
+
continue
|
|
198
|
+
safe_properties[name] = value
|
|
199
|
+
return safe_properties
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _get_bool_property(project: Project, property_name, default=False):
|
|
203
|
+
value = project.get_property(property_name, default)
|
|
204
|
+
if isinstance(value, str):
|
|
205
|
+
return value.lower() in ["1", "true", "yes", "on"]
|
|
206
|
+
return bool(value)
|
|
207
|
+
|
|
208
|
+
|
|
43
209
|
def _should_run_latest(test_dir, project):
|
|
44
210
|
if project.get_property(SHOULD_SKIP_LATEST, False):
|
|
45
211
|
if project.get_property(ROLE) == test_dir:
|
|
@@ -140,23 +306,108 @@ def _run_cypress_tests_in_directory(work_dir, logger, project, reactor: Reactor)
|
|
|
140
306
|
f"screenshotsFolder={test_report_folder}/screenshots",
|
|
141
307
|
"--reporter-options",
|
|
142
308
|
f"mochaFile={results_file}"]
|
|
143
|
-
if project
|
|
309
|
+
if _get_bool_property(project, "record_cypress", True):
|
|
144
310
|
args.append('--record')
|
|
145
311
|
_add_config_file(logger, project, args, environment, work_dir)
|
|
146
312
|
environment_variables = project.get_property(ENVIRONMENT_VARIABLES, {})
|
|
147
313
|
logger.info(f"Running cypress on host: {target_url}")
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
314
|
+
try:
|
|
315
|
+
exec_utility.exec_command(command_name=executable, args=args,
|
|
316
|
+
failure_message="Failed to execute cypress tests", log_file_name='cypress_run.log',
|
|
317
|
+
project=project, reactor=reactor, logger=logger, working_dir=work_dir, report=False,
|
|
318
|
+
env_vars=environment_variables)
|
|
319
|
+
# workaround but cypress output are relative to location of cypress.json, so we need to collapse
|
|
320
|
+
if os.path.exists(f"{work_dir}/target"):
|
|
321
|
+
shutil.copytree(f"{work_dir}/target", project.expand_path("$dir_target"), dirs_exist_ok=True)
|
|
322
|
+
finally:
|
|
323
|
+
_write_cypress_summary(project, logger)
|
|
155
324
|
total_time.stop()
|
|
156
325
|
logger.info(f"Ran Cypress Tests: {total_time.get_millis()}")
|
|
157
326
|
return True
|
|
158
327
|
|
|
159
328
|
|
|
329
|
+
def _write_cypress_summary(project, logger):
|
|
330
|
+
"""Parse cypress_run.log and write a portable Markdown summary artifact."""
|
|
331
|
+
log_path = os.path.join(prepare_logs_directory(project), "cypress_run.log")
|
|
332
|
+
summary_path = os.path.join(prepare_reports_directory(project), "cypress-summary.md")
|
|
333
|
+
try:
|
|
334
|
+
output = _build_cypress_summary(log_path)
|
|
335
|
+
with os.fdopen(
|
|
336
|
+
os.open(summary_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600),
|
|
337
|
+
"w",
|
|
338
|
+
encoding="utf-8",
|
|
339
|
+
) as summary_file:
|
|
340
|
+
summary_file.write(output)
|
|
341
|
+
github_summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
|
342
|
+
if github_summary_path:
|
|
343
|
+
with os.fdopen(
|
|
344
|
+
os.open(github_summary_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600),
|
|
345
|
+
"a",
|
|
346
|
+
encoding="utf-8",
|
|
347
|
+
) as f:
|
|
348
|
+
f.write(output)
|
|
349
|
+
print(output)
|
|
350
|
+
return summary_path
|
|
351
|
+
except Exception as e:
|
|
352
|
+
logger.warn(f"[cypress_summary] failed to generate summary: {e}")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _build_cypress_summary(log_path):
|
|
356
|
+
if not os.path.exists(log_path):
|
|
357
|
+
return "## Cypress Test Results\n\n> warning `cypress_run.log` not found\n"
|
|
358
|
+
|
|
359
|
+
raw = open(log_path, errors="replace").read()
|
|
360
|
+
clean = re.sub(r"\x1b\[[0-9;]*m", "", raw)
|
|
361
|
+
|
|
362
|
+
total = failed = skipped = 0
|
|
363
|
+
failures = []
|
|
364
|
+
for xml_block in re.findall(r"<testsuites[\s\S]*?</testsuites>", clean):
|
|
365
|
+
try:
|
|
366
|
+
root = ET.fromstring(xml_block)
|
|
367
|
+
total += int(root.get("tests", 0))
|
|
368
|
+
failed += int(root.get("failures", 0))
|
|
369
|
+
skipped += int(root.get("skipped", 0) or 0)
|
|
370
|
+
for tc in root.iter("testcase"):
|
|
371
|
+
failure = tc.find("failure")
|
|
372
|
+
if failure is not None:
|
|
373
|
+
suite = tc.get("classname", "").split(".")[-1]
|
|
374
|
+
name = tc.get("name", "")
|
|
375
|
+
msg = (failure.get("message") or failure.text or "").split("\n")[0][:120]
|
|
376
|
+
failures.append((f"{suite} > {name}", msg))
|
|
377
|
+
except ET.ParseError:
|
|
378
|
+
pass
|
|
379
|
+
|
|
380
|
+
passed = total - failed - skipped
|
|
381
|
+
if total == 0:
|
|
382
|
+
icon = "WARN"
|
|
383
|
+
label = "No tests ran - check if Cypress found any spec files"
|
|
384
|
+
elif failed == 0:
|
|
385
|
+
icon = "PASS"
|
|
386
|
+
label = "All tests passed"
|
|
387
|
+
else:
|
|
388
|
+
icon = "FAIL"
|
|
389
|
+
label = f"{failed} test(s) failed"
|
|
390
|
+
|
|
391
|
+
lines = [
|
|
392
|
+
"## Cypress Test Results\n",
|
|
393
|
+
f"### {icon} {label}\n",
|
|
394
|
+
"| Total | Passed | Failed | Skipped |",
|
|
395
|
+
"|-------|--------|--------|---------|",
|
|
396
|
+
f"| {total} | {passed} | {failed} | {skipped} |",
|
|
397
|
+
]
|
|
398
|
+
|
|
399
|
+
if failures:
|
|
400
|
+
lines.append("\n### Failed Tests\n")
|
|
401
|
+
for title, msg in failures[:20]:
|
|
402
|
+
lines.append(f"- **{title}**")
|
|
403
|
+
if msg:
|
|
404
|
+
lines.append(f" - _{msg}_")
|
|
405
|
+
if len(failures) > 20:
|
|
406
|
+
lines.append(f"\n_...and {len(failures) - 20} more_")
|
|
407
|
+
|
|
408
|
+
return "\n".join(lines) + "\n"
|
|
409
|
+
|
|
410
|
+
|
|
160
411
|
def _add_config_file(logger, project, args, environment, work_dir):
|
|
161
412
|
|
|
162
413
|
# Environment variable override
|
|
@@ -211,7 +462,7 @@ def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, re
|
|
|
211
462
|
if project.get_property("verbose"):
|
|
212
463
|
args.append("-s")
|
|
213
464
|
args.append("-v")
|
|
214
|
-
if project
|
|
465
|
+
if _get_bool_property(project, RUN_PARALLEL, False):
|
|
215
466
|
args.extend(['-n', 'auto'])
|
|
216
467
|
os.environ['TARGET'] = project.get_property(INTEGRATION_TARGET_URL)
|
|
217
468
|
os.environ['PUBLIC_TARGET'] = project.get_property(INTEGRATION_PUBLIC_TARGET_URL)
|
|
@@ -223,6 +474,14 @@ def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, re
|
|
|
223
474
|
os.chdir(test_dir)
|
|
224
475
|
logger.debug(f"Running args: {args} ")
|
|
225
476
|
ret = pytest.main(args)
|
|
477
|
+
if ret == pytest.ExitCode.NO_TESTS_COLLECTED:
|
|
478
|
+
fallback_args = _without_tavern_marker_filter(args)
|
|
479
|
+
if fallback_args != args:
|
|
480
|
+
logger.warn(
|
|
481
|
+
"No Tavern tests matched the marker filter; rerunning the full suite"
|
|
482
|
+
)
|
|
483
|
+
logger.debug(f"Running fallback args: {fallback_args} ")
|
|
484
|
+
ret = pytest.main(fallback_args)
|
|
226
485
|
finally:
|
|
227
486
|
os.chdir(cache_wd)
|
|
228
487
|
|
|
@@ -243,6 +502,23 @@ def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, re
|
|
|
243
502
|
return True
|
|
244
503
|
|
|
245
504
|
|
|
505
|
+
def _without_tavern_marker_filter(args):
|
|
506
|
+
marker_options = {"-m", "--markexpr"}
|
|
507
|
+
fallback_args = []
|
|
508
|
+
skip_next = False
|
|
509
|
+
for arg in args:
|
|
510
|
+
if skip_next:
|
|
511
|
+
skip_next = False
|
|
512
|
+
continue
|
|
513
|
+
if arg in marker_options:
|
|
514
|
+
skip_next = True
|
|
515
|
+
continue
|
|
516
|
+
if arg.startswith("--markexpr="):
|
|
517
|
+
continue
|
|
518
|
+
fallback_args.append(arg)
|
|
519
|
+
return fallback_args
|
|
520
|
+
|
|
521
|
+
|
|
246
522
|
def get_test_report_file(project, test_dir, tool="tavern"):
|
|
247
523
|
run_name = os.path.basename(test_dir)
|
|
248
524
|
output_file = os.path.join(prepare_reports_directory(project), f"{tool}-{run_name}.out.xml")
|
|
@@ -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
|
+
|
{pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pybuilder-integration
|
|
3
|
-
Version:
|
|
3
|
+
Version: 104
|
|
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 = '
|
|
24
|
+
version = '104',
|
|
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,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
|
-
|
|
File without changes
|
{pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration/directory_utility.py
RENAMED
|
File without changes
|
{pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration/exec_utility.py
RENAMED
|
File without changes
|
{pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration.egg-info/requires.txt
RENAMED
|
File without changes
|
{pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration.egg-info/top_level.txt
RENAMED
|
File without changes
|
{pybuilder-integration-101 → pybuilder_integration-104}/pybuilder_integration.egg-info/zip-safe
RENAMED
|
File without changes
|
|
File without changes
|