pybuilder-integration 106__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 (18) hide show
  1. {pybuilder_integration-106 → pybuilder_integration-107}/PKG-INFO +1 -1
  2. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration/tasks.py +196 -28
  3. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration.egg-info/PKG-INFO +1 -1
  4. {pybuilder_integration-106 → pybuilder_integration-107}/setup.py +1 -1
  5. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration/__init__.py +0 -0
  6. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration/artifact_manager.py +0 -0
  7. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration/cloudwatchlogs_utility.py +0 -0
  8. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration/directory_utility.py +0 -0
  9. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration/exec_utility.py +0 -0
  10. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration/properties.py +0 -0
  11. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration/tool_utility.py +0 -0
  12. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration.egg-info/SOURCES.txt +0 -0
  13. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration.egg-info/dependency_links.txt +0 -0
  14. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration.egg-info/namespace_packages.txt +0 -0
  15. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration.egg-info/requires.txt +0 -0
  16. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration.egg-info/top_level.txt +0 -0
  17. {pybuilder_integration-106 → pybuilder_integration-107}/pybuilder_integration.egg-info/zip-safe +0 -0
  18. {pybuilder_integration-106 → pybuilder_integration-107}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybuilder-integration
3
- Version: 106
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:
@@ -149,31 +149,40 @@ def _run_verify_environment_passes_in_subprocesses(project: Project, logger: Log
149
149
  os.remove(properties_file)
150
150
 
151
151
 
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
+
152
160
  def _merge_parallel_verify_environment_outputs(project: Project, passes):
153
161
  parent_target = project.expand_path("$dir_target")
154
162
  parent_logs_directory = prepare_logs_directory(project)
155
- parent_output_directories = [
156
- ("logs", parent_logs_directory),
157
- ("reports", prepare_reports_directory(project)),
158
- ]
163
+ parent_destinations = {
164
+ "logs": parent_logs_directory,
165
+ "reports": prepare_reports_directory(project),
166
+ }
159
167
  parent_cypress_log = os.path.join(parent_logs_directory, "cypress_run.log")
160
168
  has_parent_cypress_log = False
161
169
  for pass_config in passes:
162
170
  stage_target = os.path.join(parent_target, "verify_environment", pass_config["name"])
163
- for output_directory, destination_directory in parent_output_directories:
164
- source = os.path.join(stage_target, output_directory, "integration")
165
- if os.path.exists(source):
166
- destination = os.path.join(
167
- destination_directory,
168
- "parallel",
169
- pass_config["name"],
170
- )
171
- shutil.copytree(source, destination, dirs_exist_ok=True)
172
- if output_directory == "logs":
173
- cypress_log = os.path.join(source, "cypress_run.log")
174
- if os.path.exists(cypress_log):
175
- _copy_cypress_log(cypress_log, parent_cypress_log, has_parent_cypress_log)
176
- has_parent_cypress_log = True
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
177
186
 
178
187
 
179
188
  def _copy_cypress_log(source, destination, append):
@@ -184,12 +193,160 @@ def _copy_cypress_log(source, destination, append):
184
193
  shutil.copyfileobj(source_file, destination_file)
185
194
 
186
195
 
187
- def _start_verify_environment_subprocess(project: Project, logger: Logger, pass_config, properties_file):
188
- log_file = os.path.join(prepare_logs_directory(project), f"verify_environment_{pass_config['name']}.log")
189
- command = _build_verify_environment_subprocess_command(project, pass_config)
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):
190
333
  env = os.environ.copy()
191
334
  env[SUBPROCESS_PROPERTIES_FILE_ENV] = properties_file
192
- 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
+ )
193
350
  log_handle = os.fdopen(
194
351
  os.open(log_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600),
195
352
  "w",
@@ -199,7 +356,7 @@ def _start_verify_environment_subprocess(project: Project, logger: Logger, pass_
199
356
  process = subprocess.Popen(command,
200
357
  stdout=log_handle,
201
358
  stderr=subprocess.STDOUT,
202
- cwd=project.basedir,
359
+ cwd=workdir,
203
360
  env=env,
204
361
  shell=False)
205
362
  except Exception:
@@ -209,18 +366,29 @@ def _start_verify_environment_subprocess(project: Project, logger: Logger, pass_
209
366
  return process, log_file
210
367
 
211
368
 
212
- def _build_verify_environment_subprocess_command(project: Project, pass_config):
213
- stage_target = os.path.join(project.expand_path("$dir_target"), "verify_environment", pass_config["name"])
214
- return [
215
- "pyb",
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",
216
373
  pass_config["task"],
217
374
  "-P", f"{WORKING_TEST_DIR}={pass_config['test_dir']}",
218
375
  "-P", f"{PARALLEL_VERIFY_ENVIRONMENT}=False",
219
376
  "-P", f"{PROMOTE_ARTIFACT}=False",
220
377
  "-P", f"dir_target={stage_target}",
221
378
  "-P", f"dir_logs={os.path.join(stage_target, 'logs')}",
222
- "-P", f"dir_reports={os.path.join(stage_target, 'reports')}",
379
+ "-P", f"dir_reports={os.path.join(stage_target, 'test-reports')}",
223
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)
224
392
 
225
393
 
226
394
  def _write_subprocess_properties_file(project: Project):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybuilder-integration
3
- Version: 106
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:
@@ -21,7 +21,7 @@ class install(_install):
21
21
  if __name__ == '__main__':
22
22
  setup(
23
23
  name = 'pybuilder-integration',
24
- version = '106',
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,