pybuilder-integration 105__tar.gz → 107__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 (19) hide show
  1. {pybuilder_integration-105 → pybuilder_integration-107}/PKG-INFO +4 -3
  2. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration/tasks.py +233 -22
  3. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration.egg-info/PKG-INFO +4 -3
  4. pybuilder_integration-107/pybuilder_integration.egg-info/requires.txt +4 -0
  5. {pybuilder_integration-105 → pybuilder_integration-107}/setup.py +4 -3
  6. pybuilder_integration-105/pybuilder_integration.egg-info/requires.txt +0 -3
  7. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration/__init__.py +0 -0
  8. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration/artifact_manager.py +0 -0
  9. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration/cloudwatchlogs_utility.py +0 -0
  10. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration/directory_utility.py +0 -0
  11. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration/exec_utility.py +0 -0
  12. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration/properties.py +0 -0
  13. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration/tool_utility.py +0 -0
  14. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration.egg-info/SOURCES.txt +0 -0
  15. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration.egg-info/dependency_links.txt +0 -0
  16. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration.egg-info/namespace_packages.txt +0 -0
  17. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration.egg-info/top_level.txt +0 -0
  18. {pybuilder_integration-105 → pybuilder_integration-107}/pybuilder_integration.egg-info/zip-safe +0 -0
  19. {pybuilder_integration-105 → pybuilder_integration-107}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybuilder-integration
3
- Version: 105
3
+ Version: 107
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:
@@ -10,9 +10,10 @@ Maintainer-email:
10
10
  License: Apache 2.0
11
11
  Classifier: Development Status :: 3 - Alpha
12
12
  Classifier: Programming Language :: Python
13
- Requires-Dist: pytest
13
+ Requires-Dist: pytest==7.4.4
14
14
  Requires-Dist: tavern==1.25.2
15
- Requires-Dist: boto3
15
+ Requires-Dist: boto3==1.43.72
16
+ Requires-Dist: defusedxml==0.7.1
16
17
  Dynamic: classifier
17
18
  Dynamic: description
18
19
  Dynamic: home-page
@@ -3,9 +3,10 @@ import os
3
3
  import re
4
4
  import shutil
5
5
  import subprocess
6
- import sys
7
6
  import tempfile
8
- import xml.etree.ElementTree as ET
7
+
8
+ from defusedxml import ElementTree as ET
9
+ from defusedxml.common import DefusedXmlException
9
10
 
10
11
  import pytest
11
12
  from pybuilder.core import Project, Logger, RequirementsFile
@@ -27,6 +28,7 @@ from pybuilder_integration.tool_utility import install_cypress
27
28
  _VERIFY_ENVIRONMENT_SUBPROCESS_PROPERTIES = [
28
29
  INTEGRATION_ARTIFACT_BUCKET,
29
30
  ENVIRONMENT,
31
+ ENVIRONMENT_VARIABLES,
30
32
  ARTIFACT_MANAGER,
31
33
  CYPRESS_TEST_DIR,
32
34
  CYPRESS_CONFIG_FILE,
@@ -134,16 +136,217 @@ def _run_verify_environment_passes_in_subprocesses(project: Project, logger: Log
134
136
  if failures:
135
137
  raise BuildFailedException("Parallel verify_environment failed: " + "; ".join(failures))
136
138
  finally:
139
+ for _, process, _ in processes:
140
+ poll = getattr(process, "poll", None)
141
+ if callable(poll) and poll() is None:
142
+ process.terminate()
143
+ process.wait()
144
+ log_handle = getattr(process, "_pybuilder_integration_log_handle", None)
145
+ if log_handle and not log_handle.closed:
146
+ log_handle.close()
147
+ _merge_parallel_verify_environment_outputs(project, passes)
137
148
  if os.path.exists(properties_file):
138
149
  os.remove(properties_file)
139
150
 
140
151
 
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)
152
+ _CHILD_OUTPUT_SOURCES = (
153
+ ("logs", "logs"),
154
+ ("reports", "reports"),
155
+ ("reports", "test-reports"),
156
+ )
157
+ _SUBPROCESS_WORKDIR_SKIP = {".pybuilder", "target", ".git", ".nudge-ci-venv"}
158
+
159
+
160
+ def _merge_parallel_verify_environment_outputs(project: Project, passes):
161
+ parent_target = project.expand_path("$dir_target")
162
+ parent_logs_directory = prepare_logs_directory(project)
163
+ parent_destinations = {
164
+ "logs": parent_logs_directory,
165
+ "reports": prepare_reports_directory(project),
166
+ }
167
+ parent_cypress_log = os.path.join(parent_logs_directory, "cypress_run.log")
168
+ has_parent_cypress_log = False
169
+ for pass_config in passes:
170
+ stage_target = os.path.join(parent_target, "verify_environment", pass_config["name"])
171
+ for destination_kind, source_dirname in _CHILD_OUTPUT_SOURCES:
172
+ source = os.path.join(stage_target, source_dirname, "integration")
173
+ if not os.path.exists(source):
174
+ continue
175
+ destination = os.path.join(
176
+ parent_destinations[destination_kind],
177
+ "parallel",
178
+ pass_config["name"],
179
+ )
180
+ shutil.copytree(source, destination, dirs_exist_ok=True)
181
+ if destination_kind == "logs":
182
+ cypress_log = os.path.join(source, "cypress_run.log")
183
+ if os.path.exists(cypress_log):
184
+ _copy_cypress_log(cypress_log, parent_cypress_log, has_parent_cypress_log)
185
+ has_parent_cypress_log = True
186
+
187
+
188
+ def _copy_cypress_log(source, destination, append):
189
+ flags = os.O_WRONLY | os.O_CREAT | (os.O_APPEND if append else os.O_TRUNC)
190
+ mode = "ab" if append else "wb"
191
+ with open(source, "rb") as source_file:
192
+ with os.fdopen(os.open(destination, flags, 0o600), mode) as destination_file:
193
+ shutil.copyfileobj(source_file, destination_file)
194
+
195
+
196
+ def _verify_environment_subprocess_stage_dir(project: Project, pass_config):
197
+ return os.path.join(
198
+ project.expand_path("$dir_target"),
199
+ "verify_environment",
200
+ pass_config["name"],
201
+ )
202
+
203
+
204
+ def _verify_environment_subprocess_workdir_path(project: Project, pass_config):
205
+ return os.path.join(_verify_environment_subprocess_stage_dir(project, pass_config), "project")
206
+
207
+
208
+ def _prepare_verify_environment_subprocess_workdir(project: Project, pass_config):
209
+ stage_dir = _verify_environment_subprocess_stage_dir(project, pass_config)
210
+ workdir = _verify_environment_subprocess_workdir_path(project, pass_config)
211
+ if os.path.exists(stage_dir):
212
+ shutil.rmtree(stage_dir)
213
+ os.makedirs(workdir)
214
+ for name in os.listdir(project.basedir):
215
+ if name in _SUBPROCESS_WORKDIR_SKIP:
216
+ continue
217
+ os.symlink(os.path.join(project.basedir, name), os.path.join(workdir, name))
218
+ _link_parent_target_tree(project, workdir)
219
+ source_plugins = os.path.join(project.basedir, ".pybuilder")
220
+ if os.path.isdir(source_plugins):
221
+ destination_plugins = os.path.join(workdir, ".pybuilder")
222
+ shutil.copytree(source_plugins, destination_plugins, symlinks=True)
223
+ _materialize_external_venv_pythons(os.path.join(destination_plugins, "plugins"))
224
+ return workdir
225
+
226
+
227
+ def _link_parent_target_tree(project: Project, workdir):
228
+ parent_target = project.expand_path("$dir_target")
229
+ child_target = os.path.join(workdir, "target")
230
+ os.makedirs(child_target, exist_ok=True)
231
+ if not os.path.isdir(parent_target):
232
+ return
233
+ for name in os.listdir(parent_target):
234
+ if name == "verify_environment":
235
+ continue
236
+ os.symlink(os.path.join(parent_target, name), os.path.join(child_target, name))
237
+
238
+
239
+ def _is_venv_python_launcher(name):
240
+ if name in {"python", "python3"}:
241
+ return True
242
+ if name.startswith("python3."):
243
+ return name[len("python"):].replace(".", "").isdigit()
244
+ return False
245
+
246
+
247
+ def _is_real_directory(path):
248
+ return os.path.isdir(path) and not os.path.islink(path)
249
+
250
+
251
+ def _is_path_within(path, root):
252
+ root_real = os.path.realpath(root)
253
+ try:
254
+ return os.path.commonpath([root_real, os.path.realpath(path)]) == root_real
255
+ except ValueError:
256
+ return False
257
+
258
+
259
+ def _materialize_external_venv_pythons(plugins_root):
260
+ if not _is_real_directory(plugins_root):
261
+ return
262
+ plugins_root_real = os.path.realpath(plugins_root)
263
+ for version_dir in os.listdir(plugins_root):
264
+ version_root = os.path.join(plugins_root, version_dir)
265
+ bin_dir = os.path.join(version_root, "bin")
266
+ if not _is_real_directory(version_root) or not _is_real_directory(bin_dir):
267
+ continue
268
+ bin_dir_real = os.path.realpath(bin_dir)
269
+ if not _is_path_within(bin_dir_real, plugins_root_real):
270
+ continue
271
+ version_root_real = os.path.realpath(version_root)
272
+ for name in os.listdir(bin_dir_real):
273
+ if not _is_venv_python_launcher(name):
274
+ continue
275
+ path = os.path.join(bin_dir_real, name)
276
+ if not os.path.islink(path):
277
+ continue
278
+ target = os.path.realpath(path)
279
+ if not os.path.isfile(target):
280
+ continue
281
+ if _is_path_within(target, version_root_real):
282
+ continue
283
+ # mkstemp uses O_EXCL so a planted *.materialize symlink cannot
284
+ # redirect the write outside the staged workdir.
285
+ fd, temporary_copy = tempfile.mkstemp(
286
+ prefix=f".{name}.",
287
+ suffix=".materialize",
288
+ dir=bin_dir_real,
289
+ )
290
+ try:
291
+ with os.fdopen(fd, "wb") as destination, open(target, "rb") as source:
292
+ shutil.copyfileobj(source, destination)
293
+ destination.flush()
294
+ os.fchmod(destination.fileno(), 0o700)
295
+ os.replace(temporary_copy, path)
296
+ except Exception:
297
+ try:
298
+ os.unlink(temporary_copy)
299
+ except OSError:
300
+ pass
301
+ raise
302
+
303
+
304
+ def _plugin_python_executable(plugins_root):
305
+ if not os.path.isdir(plugins_root):
306
+ return None
307
+ for version_dir in sorted(os.listdir(plugins_root)):
308
+ candidate = os.path.join(plugins_root, version_dir, "bin", "python")
309
+ if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
310
+ return candidate
311
+ return None
312
+
313
+
314
+ _PYBUILDER_CLI_MAIN = "import sys; from pybuilder.cli import main; raise SystemExit(main(*sys.argv[1:]))"
315
+
316
+
317
+ def _isolated_plugin_python_command(plugin_python, args):
318
+ pyb_script = os.path.join(os.path.dirname(plugin_python), "pyb")
319
+ if os.path.isfile(pyb_script):
320
+ return [plugin_python, pyb_script] + args
321
+ return [plugin_python, "-c", _PYBUILDER_CLI_MAIN] + args
322
+
323
+
324
+ def _uses_isolated_plugin_python(command):
325
+ if len(command) < 2:
326
+ return False
327
+ if command[1] == "-c":
328
+ return len(command) >= 3 and command[2] == _PYBUILDER_CLI_MAIN
329
+ return os.path.basename(command[1]) == "pyb"
330
+
331
+
332
+ def _verify_environment_subprocess_env(workdir, command, properties_file):
144
333
  env = os.environ.copy()
145
334
  env[SUBPROCESS_PROPERTIES_FILE_ENV] = properties_file
146
- logger.info(f"Starting {pass_config['name']} verify_environment subprocess: {' '.join(command)}")
335
+ env.pop("GITHUB_STEP_SUMMARY", None)
336
+ env.pop("PYTHONHOME", None)
337
+ env.pop("VIRTUAL_ENV", None)
338
+ env.pop("PYTHONPATH", None)
339
+ return env
340
+
341
+
342
+ def _start_verify_environment_subprocess(project: Project, logger: Logger, pass_config, properties_file):
343
+ log_file = os.path.join(prepare_logs_directory(project), f"verify_environment_{pass_config['name']}.log")
344
+ workdir = _prepare_verify_environment_subprocess_workdir(project, pass_config)
345
+ command = _build_verify_environment_subprocess_command(project, pass_config, workdir)
346
+ env = _verify_environment_subprocess_env(workdir, command, properties_file)
347
+ logger.info(
348
+ f"Starting {pass_config['name']} verify_environment subprocess in {workdir}: {' '.join(command)}"
349
+ )
147
350
  log_handle = os.fdopen(
148
351
  os.open(log_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600),
149
352
  "w",
@@ -153,7 +356,7 @@ def _start_verify_environment_subprocess(project: Project, logger: Logger, pass_
153
356
  process = subprocess.Popen(command,
154
357
  stdout=log_handle,
155
358
  stderr=subprocess.STDOUT,
156
- cwd=project.basedir,
359
+ cwd=workdir,
157
360
  env=env,
158
361
  shell=False)
159
362
  except Exception:
@@ -163,20 +366,29 @@ def _start_verify_environment_subprocess(project: Project, logger: Logger, pass_
163
366
  return process, log_file
164
367
 
165
368
 
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()",
369
+ def _build_verify_environment_subprocess_command(project: Project, pass_config, workdir=None):
370
+ stage_target = _verify_environment_subprocess_stage_dir(project, pass_config)
371
+ args = [
372
+ "--no-venvs",
172
373
  pass_config["task"],
173
374
  "-P", f"{WORKING_TEST_DIR}={pass_config['test_dir']}",
174
375
  "-P", f"{PARALLEL_VERIFY_ENVIRONMENT}=False",
175
376
  "-P", f"{PROMOTE_ARTIFACT}=False",
176
377
  "-P", f"dir_target={stage_target}",
177
378
  "-P", f"dir_logs={os.path.join(stage_target, 'logs')}",
178
- "-P", f"dir_reports={os.path.join(stage_target, 'reports')}",
379
+ "-P", f"dir_reports={os.path.join(stage_target, 'test-reports')}",
179
380
  ]
381
+ if not workdir:
382
+ raise BuildFailedException(
383
+ f"Cannot start isolated verify_environment subprocess for {pass_config['name']}: workdir is required"
384
+ )
385
+ plugin_python = _plugin_python_executable(os.path.join(workdir, ".pybuilder", "plugins"))
386
+ if not plugin_python:
387
+ raise BuildFailedException(
388
+ f"Cannot start isolated verify_environment subprocess for {pass_config['name']}: "
389
+ "copied plugin python is missing or not executable"
390
+ )
391
+ return _isolated_plugin_python_command(plugin_python, args)
180
392
 
181
393
 
182
394
  def _write_subprocess_properties_file(project: Project):
@@ -191,13 +403,12 @@ def _collect_json_safe_project_properties(project: Project):
191
403
  safe_properties = {}
192
404
  for name in _VERIFY_ENVIRONMENT_SUBPROCESS_PROPERTIES:
193
405
  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
406
+ if value is not None:
407
+ try:
408
+ json.dumps(value)
409
+ except (TypeError, ValueError):
410
+ continue
411
+ safe_properties[name] = value
201
412
  return safe_properties
202
413
 
203
414
 
@@ -376,7 +587,7 @@ def _build_cypress_summary(log_path):
376
587
  name = tc.get("name", "")
377
588
  msg = (failure.get("message") or failure.text or "").split("\n")[0][:120]
378
589
  failures.append((f"{suite} > {name}", msg))
379
- except ET.ParseError:
590
+ except (ET.ParseError, DefusedXmlException):
380
591
  pass
381
592
 
382
593
  passed = total - failed - skipped
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybuilder-integration
3
- Version: 105
3
+ Version: 107
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:
@@ -10,9 +10,10 @@ Maintainer-email:
10
10
  License: Apache 2.0
11
11
  Classifier: Development Status :: 3 - Alpha
12
12
  Classifier: Programming Language :: Python
13
- Requires-Dist: pytest
13
+ Requires-Dist: pytest==7.4.4
14
14
  Requires-Dist: tavern==1.25.2
15
- Requires-Dist: boto3
15
+ Requires-Dist: boto3==1.43.72
16
+ Requires-Dist: defusedxml==0.7.1
16
17
  Dynamic: classifier
17
18
  Dynamic: description
18
19
  Dynamic: home-page
@@ -0,0 +1,4 @@
1
+ pytest==7.4.4
2
+ tavern==1.25.2
3
+ boto3==1.43.72
4
+ defusedxml==0.7.1
@@ -21,7 +21,7 @@ class install(_install):
21
21
  if __name__ == '__main__':
22
22
  setup(
23
23
  name = 'pybuilder-integration',
24
- version = '105',
24
+ version = '107',
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,
@@ -49,9 +49,10 @@ if __name__ == '__main__':
49
49
  data_files = [],
50
50
  package_data = {},
51
51
  install_requires = [
52
- 'pytest',
52
+ 'pytest==7.4.4',
53
53
  'tavern==1.25.2',
54
- 'boto3'
54
+ 'boto3==1.43.72',
55
+ 'defusedxml==0.7.1'
55
56
  ],
56
57
  extras_require = {},
57
58
  dependency_links = [],
@@ -1,3 +0,0 @@
1
- pytest
2
- tavern==1.25.2
3
- boto3