pybuilder-integration 107__tar.gz → 109__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-107 → pybuilder_integration-109}/PKG-INFO +1 -1
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration/directory_utility.py +1 -2
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration/properties.py +2 -0
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration/tasks.py +750 -81
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration.egg-info/PKG-INFO +1 -1
- {pybuilder_integration-107 → pybuilder_integration-109}/setup.py +1 -1
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration/__init__.py +0 -0
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration/artifact_manager.py +0 -0
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration/cloudwatchlogs_utility.py +0 -0
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration/exec_utility.py +0 -0
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration/tool_utility.py +0 -0
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration.egg-info/SOURCES.txt +0 -0
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration.egg-info/dependency_links.txt +0 -0
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration.egg-info/namespace_packages.txt +0 -0
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration.egg-info/requires.txt +0 -0
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration.egg-info/top_level.txt +0 -0
- {pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration.egg-info/zip-safe +0 -0
- {pybuilder_integration-107 → pybuilder_integration-109}/setup.cfg +0 -0
|
@@ -20,5 +20,7 @@ CONSOLIDATE_TESTS = "consolidate_tavern"
|
|
|
20
20
|
SKIP_NON_MATCHING_TAVERN_ARTIFACTS = "skip_non_matching_tavern_artifacts"
|
|
21
21
|
TESTING_SCOPE = "testing_scope"
|
|
22
22
|
PARALLEL_VERIFY_ENVIRONMENT = "parallel_verify_environment"
|
|
23
|
+
PARALLEL_TAVERN_SUITES = "parallel_tavern_suites"
|
|
24
|
+
PARALLEL_TAVERN_SUITES_WORKERS = "parallel_tavern_suites_workers"
|
|
23
25
|
SUBPROCESS_PROPERTIES_FILE_ENV = "PYBUILDER_INTEGRATION_PROPERTIES_FILE"
|
|
24
26
|
CYPRESS_CACHE_FOLDER = "cypress_cache_folder"
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import errno
|
|
1
3
|
import json
|
|
2
4
|
import os
|
|
3
5
|
import re
|
|
4
6
|
import shutil
|
|
5
7
|
import subprocess
|
|
6
8
|
import tempfile
|
|
9
|
+
import threading
|
|
10
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
11
|
|
|
8
12
|
from defusedxml import ElementTree as ET
|
|
9
13
|
from defusedxml.common import DefusedXmlException
|
|
@@ -45,11 +49,16 @@ _VERIFY_ENVIRONMENT_SUBPROCESS_PROPERTIES = [
|
|
|
45
49
|
CONSOLIDATE_TESTS,
|
|
46
50
|
SKIP_NON_MATCHING_TAVERN_ARTIFACTS,
|
|
47
51
|
TESTING_SCOPE,
|
|
52
|
+
PARALLEL_TAVERN_SUITES,
|
|
53
|
+
PARALLEL_TAVERN_SUITES_WORKERS,
|
|
48
54
|
"abort_upload",
|
|
49
55
|
"record_cypress",
|
|
50
56
|
"verbose",
|
|
51
57
|
]
|
|
52
58
|
|
|
59
|
+
_TAVERN_INSTALL_LOCK = threading.Lock()
|
|
60
|
+
_DEFAULT_PARALLEL_TAVERN_SUITE_WORKERS = 8
|
|
61
|
+
|
|
53
62
|
def integration_artifact_push(project: Project, logger: Logger, reactor: Reactor):
|
|
54
63
|
logger.info("Starting upload of integration artifacts")
|
|
55
64
|
manager = get_artifact_manager(project)
|
|
@@ -124,15 +133,21 @@ def _run_verify_environment_passes_in_subprocesses(project: Project, logger: Log
|
|
|
124
133
|
process, log_file = _start_verify_environment_subprocess(project, logger, pass_config, properties_file)
|
|
125
134
|
processes.append((pass_config, process, log_file))
|
|
126
135
|
|
|
127
|
-
|
|
136
|
+
results = []
|
|
128
137
|
for pass_config, process, log_file in processes:
|
|
129
138
|
exit_code = process.wait()
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
_close_subprocess_log(process)
|
|
140
|
+
results.append({
|
|
141
|
+
"name": pass_config["name"],
|
|
142
|
+
"exit_code": exit_code,
|
|
143
|
+
"log_file": log_file,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
_log_parallel_verify_environment_summary(logger, results)
|
|
147
|
+
failures = [
|
|
148
|
+
f"{result['name']} exit_code={result['exit_code']} log={result['log_file']}"
|
|
149
|
+
for result in results if result["exit_code"] != 0
|
|
150
|
+
]
|
|
136
151
|
if failures:
|
|
137
152
|
raise BuildFailedException("Parallel verify_environment failed: " + "; ".join(failures))
|
|
138
153
|
finally:
|
|
@@ -141,24 +156,422 @@ def _run_verify_environment_passes_in_subprocesses(project: Project, logger: Log
|
|
|
141
156
|
if callable(poll) and poll() is None:
|
|
142
157
|
process.terminate()
|
|
143
158
|
process.wait()
|
|
144
|
-
|
|
145
|
-
if log_handle and not log_handle.closed:
|
|
146
|
-
log_handle.close()
|
|
159
|
+
_close_subprocess_log(process)
|
|
147
160
|
_merge_parallel_verify_environment_outputs(project, passes)
|
|
161
|
+
_write_cypress_summary(project, logger)
|
|
148
162
|
if os.path.exists(properties_file):
|
|
149
163
|
os.remove(properties_file)
|
|
150
164
|
|
|
151
165
|
|
|
166
|
+
_ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*m")
|
|
167
|
+
_SUITE_RUNNING_LINE = re.compile(
|
|
168
|
+
r"^\[INFO\]\s+Running (?!tavern tests\b|against target\b|cypress on host\b|args:).+"
|
|
169
|
+
)
|
|
170
|
+
_SECRET_QUOTE = r"(?:['\"]|"|')?"
|
|
171
|
+
_SECRET_VALUE = rf"{_SECRET_QUOTE}[^'\"\s<&>,}}]+"
|
|
172
|
+
_SECRET_LITERAL_VALUE = rf"(?!(?:true|false|yes|no|on|off|null|none)\b){_SECRET_VALUE}"
|
|
173
|
+
_SECRET_KEY_NAME = (
|
|
174
|
+
r"(?:[A-Za-z][A-Za-z0-9]*[_-])*"
|
|
175
|
+
r"(?:"
|
|
176
|
+
r"x-api-key|x-amz-security-token|"
|
|
177
|
+
r"api[_-]?key|secret[_-]?access[_-]?key|access[_-]?key(?:[_-]?id)?|"
|
|
178
|
+
r"client[_-]?secret|password|secret|"
|
|
179
|
+
r"(?:id|access|refresh|session)?[_-]?token"
|
|
180
|
+
r")"
|
|
181
|
+
)
|
|
182
|
+
_SECRET_HINT_LINE = re.compile(
|
|
183
|
+
r"("
|
|
184
|
+
rf"\bauthorization\b{_SECRET_QUOTE}\s*:\s*{_SECRET_QUOTE}(?:(?:bearer|basic)\s+)?{_SECRET_VALUE}"
|
|
185
|
+
rf"|\b(?:set-cookie|cookie)\b{_SECRET_QUOTE}\s*:\s*{_SECRET_QUOTE}{_SECRET_VALUE}(?:\s*;\s*{_SECRET_VALUE})*"
|
|
186
|
+
rf"|{_SECRET_KEY_NAME}{_SECRET_QUOTE}\s*[:=]\s*{_SECRET_LITERAL_VALUE}"
|
|
187
|
+
r"|\bbearer\s+[A-Za-z0-9._\-]+"
|
|
188
|
+
r"|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?:\.[A-Za-z0-9_-]{10,})?"
|
|
189
|
+
r"|https?://[^/\s]+:[^/\s]+@"
|
|
190
|
+
r"|\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})"
|
|
191
|
+
r")",
|
|
192
|
+
re.IGNORECASE,
|
|
193
|
+
)
|
|
194
|
+
_REDACTED_SECRET = "[redacted]"
|
|
195
|
+
_REDACTED_COPY_EXTENSIONS = {".html", ".json", ".log", ".md", ".out", ".txt", ".xml"}
|
|
196
|
+
_REDACT_STAGING_DIRNAME = ".pybuilder-integration-redact"
|
|
197
|
+
_REDACT_TEMP_FILES = set()
|
|
198
|
+
_SUBPROCESS_LOG_JOIN_SECONDS = 30
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _log_parallel_verify_environment_summary(logger: Logger, results):
|
|
202
|
+
summary = _build_parallel_verify_environment_summary(results)
|
|
203
|
+
print(summary)
|
|
204
|
+
github_summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
|
205
|
+
if not github_summary_path:
|
|
206
|
+
return
|
|
207
|
+
try:
|
|
208
|
+
with os.fdopen(
|
|
209
|
+
os.open(github_summary_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600),
|
|
210
|
+
"a",
|
|
211
|
+
encoding="utf-8",
|
|
212
|
+
) as summary_file:
|
|
213
|
+
summary_file.write(summary)
|
|
214
|
+
if not summary.endswith("\n"):
|
|
215
|
+
summary_file.write("\n")
|
|
216
|
+
except OSError:
|
|
217
|
+
logger.warn("Could not append parallel verify_environment summary to GITHUB_STEP_SUMMARY")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _build_parallel_verify_environment_summary(results):
|
|
221
|
+
lines = ["Parallel verify_environment summary"]
|
|
222
|
+
for result in results:
|
|
223
|
+
status = "PASSED" if result["exit_code"] == 0 else "FAILED"
|
|
224
|
+
lines.append(f" {result['name']}: {status} (exit_code={result['exit_code']})")
|
|
225
|
+
for result in results:
|
|
226
|
+
if result["exit_code"] == 0:
|
|
227
|
+
continue
|
|
228
|
+
lines.append(f" {result['name']} log: {result['log_file']}")
|
|
229
|
+
for hint in _extract_parallel_verify_failure_hints(result["log_file"]):
|
|
230
|
+
lines.append(f" {hint}")
|
|
231
|
+
return "\n".join(lines) + "\n"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _extract_parallel_verify_failure_hints(log_file, max_hints=12):
|
|
235
|
+
if not log_file or not os.path.exists(log_file):
|
|
236
|
+
return ["child log was not found"]
|
|
237
|
+
try:
|
|
238
|
+
with open(log_file, encoding="utf-8", errors="replace") as log_handle:
|
|
239
|
+
raw = log_handle.read()
|
|
240
|
+
except OSError:
|
|
241
|
+
return ["child log could not be read"]
|
|
242
|
+
text = _ANSI_ESCAPE.sub("", raw)
|
|
243
|
+
lines = [line.rstrip() for line in text.splitlines()]
|
|
244
|
+
last_running = None
|
|
245
|
+
last_build_failed = None
|
|
246
|
+
failed_tests = []
|
|
247
|
+
session_summaries = []
|
|
248
|
+
extra_failures = []
|
|
249
|
+
for line in lines:
|
|
250
|
+
stripped = line.strip()
|
|
251
|
+
if not stripped:
|
|
252
|
+
continue
|
|
253
|
+
if _SUITE_RUNNING_LINE.match(stripped):
|
|
254
|
+
last_running = stripped
|
|
255
|
+
elif stripped.startswith("BUILD FAILED"):
|
|
256
|
+
last_build_failed = stripped
|
|
257
|
+
elif stripped.startswith("FAILED ") and "::" in stripped:
|
|
258
|
+
failed_tests.append(stripped)
|
|
259
|
+
elif stripped.startswith("===") and "failed" in stripped.lower():
|
|
260
|
+
session_summaries.append(stripped)
|
|
261
|
+
elif stripped.startswith("### FAIL") or "Failed to execute cypress" in stripped:
|
|
262
|
+
extra_failures.append(stripped)
|
|
263
|
+
hints = []
|
|
264
|
+
if last_running:
|
|
265
|
+
hints.append(last_running)
|
|
266
|
+
if last_build_failed:
|
|
267
|
+
hints.append(last_build_failed)
|
|
268
|
+
hints.extend(extra_failures[-3:])
|
|
269
|
+
hints.extend(failed_tests[-8:])
|
|
270
|
+
hints.extend(session_summaries[-3:])
|
|
271
|
+
if hints:
|
|
272
|
+
return _without_secret_hint_lines(hints)[:max_hints]
|
|
273
|
+
tail = [line.strip() for line in lines[-20:] if line.strip()]
|
|
274
|
+
if not tail:
|
|
275
|
+
return ["child log was empty"]
|
|
276
|
+
return _without_secret_hint_lines(tail)[-max_hints:]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _start_subprocess_output_tee(process, log_handle, logger: Logger, pass_name):
|
|
280
|
+
stdout = getattr(process, "stdout", None)
|
|
281
|
+
if stdout is None or isinstance(stdout, (int, str)):
|
|
282
|
+
process._pybuilder_integration_log_thread = None
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
def tee():
|
|
286
|
+
prefix = f"[{pass_name}] "
|
|
287
|
+
try:
|
|
288
|
+
for line in stdout:
|
|
289
|
+
redacted = _redact_secret_log_line(_decode_subprocess_stdout_line(line))
|
|
290
|
+
try:
|
|
291
|
+
log_handle.write(redacted + "\n")
|
|
292
|
+
log_handle.flush()
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
try:
|
|
296
|
+
logger.info(prefix + redacted)
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
finally:
|
|
300
|
+
try:
|
|
301
|
+
stdout.close()
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
thread = threading.Thread(target=tee, name=f"verify-environment-{pass_name}-log", daemon=True)
|
|
306
|
+
thread.start()
|
|
307
|
+
process._pybuilder_integration_log_thread = thread
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _close_subprocess_stdout(process):
|
|
311
|
+
stdout = getattr(process, "stdout", None)
|
|
312
|
+
if stdout is None or isinstance(stdout, (int, str)):
|
|
313
|
+
return
|
|
314
|
+
try:
|
|
315
|
+
stdout.close()
|
|
316
|
+
except Exception:
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _close_subprocess_log(process):
|
|
321
|
+
thread = getattr(process, "_pybuilder_integration_log_thread", None)
|
|
322
|
+
if thread is not None:
|
|
323
|
+
thread.join(timeout=_SUBPROCESS_LOG_JOIN_SECONDS)
|
|
324
|
+
if thread.is_alive():
|
|
325
|
+
_close_subprocess_stdout(process)
|
|
326
|
+
thread.join(timeout=_SUBPROCESS_LOG_JOIN_SECONDS)
|
|
327
|
+
if thread.is_alive():
|
|
328
|
+
return
|
|
329
|
+
process._pybuilder_integration_log_thread = None
|
|
330
|
+
log_handle = getattr(process, "_pybuilder_integration_log_handle", None)
|
|
331
|
+
if log_handle and not getattr(log_handle, "closed", True):
|
|
332
|
+
log_handle.close()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _decode_subprocess_stdout_line(line):
|
|
336
|
+
if isinstance(line, bytes):
|
|
337
|
+
line = line.decode("utf-8", "replace")
|
|
338
|
+
return line.rstrip("\n\r")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _redact_secret_log_line(line):
|
|
342
|
+
return _SECRET_HINT_LINE.sub(_REDACTED_SECRET, line)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _redact_secret_log_text(text):
|
|
346
|
+
if not text:
|
|
347
|
+
return text
|
|
348
|
+
return "\n".join(_redact_secret_log_line(line) for line in text.splitlines()) + ("\n" if text.endswith("\n") else "")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _without_secret_hint_lines(lines):
|
|
352
|
+
return [_redact_secret_log_line(line) for line in lines]
|
|
353
|
+
|
|
354
|
+
|
|
152
355
|
_CHILD_OUTPUT_SOURCES = (
|
|
153
356
|
("logs", "logs"),
|
|
154
357
|
("reports", "reports"),
|
|
155
358
|
("reports", "test-reports"),
|
|
156
359
|
)
|
|
157
360
|
_SUBPROCESS_WORKDIR_SKIP = {".pybuilder", "target", ".git", ".nudge-ci-venv"}
|
|
361
|
+
_CHILD_TARGET_LINK_SKIP = {"verify_environment", "logs", "reports", "test-reports"}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _child_output_source_roots(stage_target):
|
|
365
|
+
return (
|
|
366
|
+
stage_target,
|
|
367
|
+
os.path.join(stage_target, "project", "target"),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _first_child_cypress_log(stage_target):
|
|
372
|
+
for source_root in _child_output_source_roots(stage_target):
|
|
373
|
+
candidate = os.path.join(source_root, "logs", "integration", "cypress_run.log")
|
|
374
|
+
if os.path.isfile(candidate):
|
|
375
|
+
return candidate
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _copy_maybe_redacted_file(src, dst, *, follow_symlinks=True):
|
|
380
|
+
if os.path.splitext(src)[1].lower() in _REDACTED_COPY_EXTENSIONS:
|
|
381
|
+
_copy_redacted_text_file(src, dst)
|
|
382
|
+
return dst
|
|
383
|
+
return shutil.copy2(src, dst, follow_symlinks=follow_symlinks)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _redact_text_file_inplace(path):
|
|
387
|
+
if os.path.islink(path) or not os.path.isfile(path):
|
|
388
|
+
return
|
|
389
|
+
try:
|
|
390
|
+
with open(path, encoding="utf-8", errors="replace") as source_file:
|
|
391
|
+
text = source_file.read()
|
|
392
|
+
except OSError:
|
|
393
|
+
return
|
|
394
|
+
redacted = _redact_secret_log_text(text)
|
|
395
|
+
if redacted == text:
|
|
396
|
+
return
|
|
397
|
+
fd, temporary_copy = tempfile.mkstemp(
|
|
398
|
+
prefix=f".{os.path.basename(path)}.",
|
|
399
|
+
suffix=".redact",
|
|
400
|
+
dir=tempfile.gettempdir(),
|
|
401
|
+
)
|
|
402
|
+
_register_redact_temp(temporary_copy)
|
|
403
|
+
try:
|
|
404
|
+
with os.fdopen(fd, "w", encoding="utf-8") as dest_file:
|
|
405
|
+
dest_file.write(redacted)
|
|
406
|
+
dest_file.flush()
|
|
407
|
+
os.fchmod(dest_file.fileno(), 0o600)
|
|
408
|
+
_replace_redacted_tempfile(temporary_copy, path)
|
|
409
|
+
except OSError:
|
|
410
|
+
_unlink_redact_temp(temporary_copy)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _register_redact_temp(path):
|
|
414
|
+
_REDACT_TEMP_FILES.add(path)
|
|
415
|
+
if not getattr(_register_redact_temp, "_atexit_registered", False):
|
|
416
|
+
atexit.register(_cleanup_redact_temps)
|
|
417
|
+
_register_redact_temp._atexit_registered = True
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _unlink_redact_temp(path):
|
|
421
|
+
try:
|
|
422
|
+
os.unlink(path)
|
|
423
|
+
except OSError:
|
|
424
|
+
pass
|
|
425
|
+
_REDACT_TEMP_FILES.discard(path)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _cleanup_redact_temps():
|
|
429
|
+
for path in list(_REDACT_TEMP_FILES):
|
|
430
|
+
_unlink_redact_temp(path)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _same_device_staging_base(path):
|
|
434
|
+
dest = os.path.abspath(path)
|
|
435
|
+
dest_dev = os.stat(dest).st_dev
|
|
436
|
+
current = dest if os.path.isdir(dest) else os.path.dirname(dest)
|
|
437
|
+
last_same = current
|
|
438
|
+
outside_target = None
|
|
439
|
+
while True:
|
|
440
|
+
try:
|
|
441
|
+
if os.stat(current).st_dev != dest_dev:
|
|
442
|
+
break
|
|
443
|
+
except OSError:
|
|
444
|
+
break
|
|
445
|
+
last_same = current
|
|
446
|
+
if "target" not in os.path.abspath(current).split(os.sep):
|
|
447
|
+
outside_target = current
|
|
448
|
+
break
|
|
449
|
+
parent = os.path.dirname(current)
|
|
450
|
+
if parent == current:
|
|
451
|
+
break
|
|
452
|
+
current = parent
|
|
453
|
+
return outside_target or last_same
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _same_device_staging_dir(path):
|
|
457
|
+
staging = os.path.join(_same_device_staging_base(path), _REDACT_STAGING_DIRNAME)
|
|
458
|
+
os.makedirs(staging, mode=0o700, exist_ok=True)
|
|
459
|
+
try:
|
|
460
|
+
os.chmod(staging, 0o700)
|
|
461
|
+
except OSError:
|
|
462
|
+
pass
|
|
463
|
+
return staging
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _remove_empty_redact_staging_dir(staging):
|
|
467
|
+
try:
|
|
468
|
+
os.rmdir(staging)
|
|
469
|
+
except OSError:
|
|
470
|
+
pass
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _purge_redact_staging_dir(staging):
|
|
474
|
+
if os.path.basename(os.path.abspath(staging)) != _REDACT_STAGING_DIRNAME:
|
|
475
|
+
return
|
|
476
|
+
if os.path.islink(staging) or not os.path.isdir(staging):
|
|
477
|
+
return
|
|
478
|
+
for name in os.listdir(staging):
|
|
479
|
+
path = os.path.join(staging, name)
|
|
480
|
+
try:
|
|
481
|
+
if os.path.isdir(path) and not os.path.islink(path):
|
|
482
|
+
shutil.rmtree(path)
|
|
483
|
+
else:
|
|
484
|
+
os.unlink(path)
|
|
485
|
+
except OSError:
|
|
486
|
+
pass
|
|
487
|
+
_REDACT_TEMP_FILES.discard(path)
|
|
488
|
+
_remove_empty_redact_staging_dir(staging)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _redact_staging_candidates(path):
|
|
492
|
+
candidates = []
|
|
493
|
+
try:
|
|
494
|
+
candidates.append(os.path.join(_same_device_staging_base(path), _REDACT_STAGING_DIRNAME))
|
|
495
|
+
except OSError:
|
|
496
|
+
pass
|
|
497
|
+
abs_path = os.path.abspath(path)
|
|
498
|
+
if os.path.isdir(abs_path):
|
|
499
|
+
candidates.append(os.path.join(abs_path, _REDACT_STAGING_DIRNAME))
|
|
500
|
+
return candidates
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _purge_known_redact_staging_dirs(*paths):
|
|
504
|
+
seen = set()
|
|
505
|
+
for path in paths:
|
|
506
|
+
if not path or not os.path.exists(path):
|
|
507
|
+
continue
|
|
508
|
+
for staging in _redact_staging_candidates(path):
|
|
509
|
+
if staging in seen:
|
|
510
|
+
continue
|
|
511
|
+
seen.add(staging)
|
|
512
|
+
_purge_redact_staging_dir(staging)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _replace_redacted_tempfile(temporary_copy, path):
|
|
516
|
+
try:
|
|
517
|
+
os.replace(temporary_copy, path)
|
|
518
|
+
_REDACT_TEMP_FILES.discard(temporary_copy)
|
|
519
|
+
return
|
|
520
|
+
except OSError as error:
|
|
521
|
+
if getattr(error, "errno", None) != errno.EXDEV:
|
|
522
|
+
raise
|
|
523
|
+
staging = _same_device_staging_dir(path)
|
|
524
|
+
fd, local_copy = tempfile.mkstemp(
|
|
525
|
+
prefix=f".{os.path.basename(path)}.",
|
|
526
|
+
suffix=".redact",
|
|
527
|
+
dir=staging,
|
|
528
|
+
)
|
|
529
|
+
_register_redact_temp(local_copy)
|
|
530
|
+
try:
|
|
531
|
+
with os.fdopen(fd, "wb") as destination, open(temporary_copy, "rb") as source:
|
|
532
|
+
shutil.copyfileobj(source, destination)
|
|
533
|
+
destination.flush()
|
|
534
|
+
os.fchmod(destination.fileno(), 0o600)
|
|
535
|
+
os.replace(local_copy, path)
|
|
536
|
+
_REDACT_TEMP_FILES.discard(local_copy)
|
|
537
|
+
except OSError:
|
|
538
|
+
_unlink_redact_temp(local_copy)
|
|
539
|
+
raise
|
|
540
|
+
finally:
|
|
541
|
+
_unlink_redact_temp(temporary_copy)
|
|
542
|
+
_remove_empty_redact_staging_dir(staging)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _redact_directory_text_files(root):
|
|
546
|
+
if not os.path.isdir(root):
|
|
547
|
+
return
|
|
548
|
+
_purge_known_redact_staging_dirs(root)
|
|
549
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
550
|
+
if _REDACT_STAGING_DIRNAME in dirnames:
|
|
551
|
+
dirnames.remove(_REDACT_STAGING_DIRNAME)
|
|
552
|
+
for name in filenames:
|
|
553
|
+
if os.path.splitext(name)[1].lower() not in _REDACTED_COPY_EXTENSIONS:
|
|
554
|
+
continue
|
|
555
|
+
_redact_text_file_inplace(os.path.join(dirpath, name))
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _copy_redacted_text_file(src, dst):
|
|
559
|
+
parent = os.path.dirname(dst)
|
|
560
|
+
if parent:
|
|
561
|
+
os.makedirs(parent, exist_ok=True)
|
|
562
|
+
try:
|
|
563
|
+
with open(src, encoding="utf-8", errors="replace") as source_file:
|
|
564
|
+
text = _redact_secret_log_text(source_file.read())
|
|
565
|
+
except OSError:
|
|
566
|
+
shutil.copy2(src, dst)
|
|
567
|
+
return
|
|
568
|
+
with os.fdopen(os.open(dst, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600), "w", encoding="utf-8") as dest_file:
|
|
569
|
+
dest_file.write(text)
|
|
158
570
|
|
|
159
571
|
|
|
160
572
|
def _merge_parallel_verify_environment_outputs(project: Project, passes):
|
|
161
573
|
parent_target = project.expand_path("$dir_target")
|
|
574
|
+
_purge_known_redact_staging_dirs(parent_target, project.basedir)
|
|
162
575
|
parent_logs_directory = prepare_logs_directory(project)
|
|
163
576
|
parent_destinations = {
|
|
164
577
|
"logs": parent_logs_directory,
|
|
@@ -168,29 +581,36 @@ def _merge_parallel_verify_environment_outputs(project: Project, passes):
|
|
|
168
581
|
has_parent_cypress_log = False
|
|
169
582
|
for pass_config in passes:
|
|
170
583
|
stage_target = os.path.join(parent_target, "verify_environment", pass_config["name"])
|
|
171
|
-
for
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
584
|
+
for source_root in _child_output_source_roots(stage_target):
|
|
585
|
+
for destination_kind, source_dirname in _CHILD_OUTPUT_SOURCES:
|
|
586
|
+
source = os.path.join(source_root, source_dirname, "integration")
|
|
587
|
+
if not os.path.exists(source):
|
|
588
|
+
continue
|
|
589
|
+
_redact_directory_text_files(source)
|
|
590
|
+
destination = os.path.join(
|
|
591
|
+
parent_destinations[destination_kind],
|
|
592
|
+
"parallel",
|
|
593
|
+
pass_config["name"],
|
|
594
|
+
)
|
|
595
|
+
shutil.copytree(source, destination, dirs_exist_ok=True, copy_function=_copy_maybe_redacted_file)
|
|
596
|
+
cypress_log = _first_child_cypress_log(stage_target)
|
|
597
|
+
if cypress_log:
|
|
598
|
+
_copy_cypress_log(cypress_log, parent_cypress_log, has_parent_cypress_log)
|
|
599
|
+
has_parent_cypress_log = True
|
|
186
600
|
|
|
187
601
|
|
|
188
602
|
def _copy_cypress_log(source, destination, append):
|
|
603
|
+
try:
|
|
604
|
+
with open(source, encoding="utf-8", errors="replace") as source_file:
|
|
605
|
+
text = _redact_secret_log_text(source_file.read())
|
|
606
|
+
except OSError:
|
|
607
|
+
return
|
|
608
|
+
if text and not text.endswith("\n"):
|
|
609
|
+
text += "\n"
|
|
189
610
|
flags = os.O_WRONLY | os.O_CREAT | (os.O_APPEND if append else os.O_TRUNC)
|
|
190
|
-
mode = "
|
|
191
|
-
with open(
|
|
192
|
-
|
|
193
|
-
shutil.copyfileobj(source_file, destination_file)
|
|
611
|
+
mode = "a" if append else "w"
|
|
612
|
+
with os.fdopen(os.open(destination, flags, 0o600), mode, encoding="utf-8") as destination_file:
|
|
613
|
+
destination_file.write(text)
|
|
194
614
|
|
|
195
615
|
|
|
196
616
|
def _verify_environment_subprocess_stage_dir(project: Project, pass_config):
|
|
@@ -231,7 +651,7 @@ def _link_parent_target_tree(project: Project, workdir):
|
|
|
231
651
|
if not os.path.isdir(parent_target):
|
|
232
652
|
return
|
|
233
653
|
for name in os.listdir(parent_target):
|
|
234
|
-
if name
|
|
654
|
+
if name in _CHILD_TARGET_LINK_SKIP:
|
|
235
655
|
continue
|
|
236
656
|
os.symlink(os.path.join(parent_target, name), os.path.join(child_target, name))
|
|
237
657
|
|
|
@@ -317,21 +737,27 @@ _PYBUILDER_CLI_MAIN = "import sys; from pybuilder.cli import main; raise SystemE
|
|
|
317
737
|
def _isolated_plugin_python_command(plugin_python, args):
|
|
318
738
|
pyb_script = os.path.join(os.path.dirname(plugin_python), "pyb")
|
|
319
739
|
if os.path.isfile(pyb_script):
|
|
320
|
-
return [plugin_python, pyb_script] + args
|
|
321
|
-
return [plugin_python, "-c", _PYBUILDER_CLI_MAIN] + args
|
|
740
|
+
return [plugin_python, "-u", pyb_script] + args
|
|
741
|
+
return [plugin_python, "-u", "-c", _PYBUILDER_CLI_MAIN] + args
|
|
322
742
|
|
|
323
743
|
|
|
324
744
|
def _uses_isolated_plugin_python(command):
|
|
325
745
|
if len(command) < 2:
|
|
326
746
|
return False
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
747
|
+
args = command[1:]
|
|
748
|
+
if args and args[0] == "-u":
|
|
749
|
+
args = args[1:]
|
|
750
|
+
if not args:
|
|
751
|
+
return False
|
|
752
|
+
if args[0] == "-c":
|
|
753
|
+
return len(args) >= 2 and args[1] == _PYBUILDER_CLI_MAIN
|
|
754
|
+
return os.path.basename(args[0]) == "pyb"
|
|
330
755
|
|
|
331
756
|
|
|
332
757
|
def _verify_environment_subprocess_env(workdir, command, properties_file):
|
|
333
758
|
env = os.environ.copy()
|
|
334
759
|
env[SUBPROCESS_PROPERTIES_FILE_ENV] = properties_file
|
|
760
|
+
env["PYTHONUNBUFFERED"] = "1"
|
|
335
761
|
env.pop("GITHUB_STEP_SUMMARY", None)
|
|
336
762
|
env.pop("PYTHONHOME", None)
|
|
337
763
|
env.pop("VIRTUAL_ENV", None)
|
|
@@ -354,15 +780,16 @@ def _start_verify_environment_subprocess(project: Project, logger: Logger, pass_
|
|
|
354
780
|
)
|
|
355
781
|
try:
|
|
356
782
|
process = subprocess.Popen(command,
|
|
357
|
-
stdout=
|
|
783
|
+
stdout=subprocess.PIPE,
|
|
358
784
|
stderr=subprocess.STDOUT,
|
|
359
785
|
cwd=workdir,
|
|
360
786
|
env=env,
|
|
361
787
|
shell=False)
|
|
788
|
+
process._pybuilder_integration_log_handle = log_handle
|
|
789
|
+
_start_subprocess_output_tee(process, log_handle, logger, pass_config["name"])
|
|
362
790
|
except Exception:
|
|
363
791
|
log_handle.close()
|
|
364
792
|
raise
|
|
365
|
-
process._pybuilder_integration_log_handle = log_handle
|
|
366
793
|
return process, log_file
|
|
367
794
|
|
|
368
795
|
|
|
@@ -465,15 +892,17 @@ def _run_tavern_tests_in_dist_dir(dist_directory, latest, logger, project, react
|
|
|
465
892
|
logger.debug(f"Run tavern tests in directory: {tavern_test_path} ")
|
|
466
893
|
logger.debug(f"Run tavern tests in directory files: {os.listdir(tavern_test_path)} ")
|
|
467
894
|
if latest:
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
895
|
+
suite_dirs = list(_iter_latest_tavern_suite_dirs(tavern_test_path, project))
|
|
896
|
+
if _get_bool_property(project, PARALLEL_TAVERN_SUITES, False) and len(suite_dirs) > 1:
|
|
897
|
+
_run_tavern_suites_parallel(suite_dirs, logger, project, reactor)
|
|
898
|
+
else:
|
|
899
|
+
for tavern_test_directory in suite_dirs:
|
|
900
|
+
logger.info(f"Running {tavern_test_directory}")
|
|
901
|
+
_run_tavern_tests_in_dir(test_dir=tavern_test_directory,
|
|
473
902
|
logger=logger,
|
|
474
903
|
project=project,
|
|
475
904
|
reactor=reactor,
|
|
476
|
-
role=os.path.basename(
|
|
905
|
+
role=os.path.basename(tavern_test_directory))
|
|
477
906
|
else:
|
|
478
907
|
_run_tavern_tests_in_dir(test_dir=f"{tavern_test_path}",
|
|
479
908
|
logger=logger,
|
|
@@ -483,6 +912,92 @@ def _run_tavern_tests_in_dist_dir(dist_directory, latest, logger, project, react
|
|
|
483
912
|
logger.info(f"Ran Tavern tests: {total_time.get_millis()}")
|
|
484
913
|
|
|
485
914
|
|
|
915
|
+
def _iter_latest_tavern_suite_dirs(tavern_test_path, project):
|
|
916
|
+
for tavern_dir in sorted(os.listdir(tavern_test_path)):
|
|
917
|
+
tavern_test_directory = os.path.join(tavern_test_path, tavern_dir)
|
|
918
|
+
if os.path.isdir(tavern_test_directory) and _should_run_latest(tavern_dir, project):
|
|
919
|
+
yield tavern_test_directory
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def _parallel_tavern_suite_workers(project, suite_count):
|
|
923
|
+
configured = project.get_property(PARALLEL_TAVERN_SUITES_WORKERS, None)
|
|
924
|
+
if configured is None or configured == "":
|
|
925
|
+
return max(1, min(_DEFAULT_PARALLEL_TAVERN_SUITE_WORKERS, suite_count))
|
|
926
|
+
try:
|
|
927
|
+
workers = int(configured)
|
|
928
|
+
except (TypeError, ValueError):
|
|
929
|
+
workers = _DEFAULT_PARALLEL_TAVERN_SUITE_WORKERS
|
|
930
|
+
return max(1, min(workers, suite_count))
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def _run_tavern_suites_parallel(suite_dirs, logger, project, reactor):
|
|
934
|
+
prepare_reports_directory(project)
|
|
935
|
+
runnable = []
|
|
936
|
+
for tavern_test_directory in suite_dirs:
|
|
937
|
+
_, requested_marker, declared_markers, skip_non_matching = _tavern_marker_context(
|
|
938
|
+
tavern_test_directory, project
|
|
939
|
+
)
|
|
940
|
+
if _should_skip_nonmatching_tavern(requested_marker, declared_markers, skip_non_matching):
|
|
941
|
+
logger.info(
|
|
942
|
+
f"Skipping Tavern tests: artifact markers {sorted(declared_markers)} do not match {requested_marker}"
|
|
943
|
+
)
|
|
944
|
+
continue
|
|
945
|
+
runnable.append(tavern_test_directory)
|
|
946
|
+
if not runnable:
|
|
947
|
+
return
|
|
948
|
+
if len(runnable) == 1:
|
|
949
|
+
tavern_test_directory = runnable[0]
|
|
950
|
+
logger.info(f"Running {tavern_test_directory}")
|
|
951
|
+
_run_tavern_tests_in_dir(
|
|
952
|
+
test_dir=tavern_test_directory,
|
|
953
|
+
logger=logger,
|
|
954
|
+
project=project,
|
|
955
|
+
reactor=reactor,
|
|
956
|
+
role=os.path.basename(tavern_test_directory),
|
|
957
|
+
)
|
|
958
|
+
return
|
|
959
|
+
|
|
960
|
+
suite_sites = {}
|
|
961
|
+
for tavern_test_directory in runnable:
|
|
962
|
+
suite_sites[tavern_test_directory] = _install_tavern_suite_site(
|
|
963
|
+
tavern_test_directory, logger, project, reactor
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
workers = _parallel_tavern_suite_workers(project, len(runnable))
|
|
967
|
+
logger.info(f"Running {len(runnable)} tavern suites in parallel (workers={workers})")
|
|
968
|
+
failures = []
|
|
969
|
+
|
|
970
|
+
def run_suite(tavern_test_directory):
|
|
971
|
+
role = os.path.basename(tavern_test_directory)
|
|
972
|
+
logger.info(f"Running {tavern_test_directory}")
|
|
973
|
+
_run_tavern_tests_in_dir(
|
|
974
|
+
test_dir=tavern_test_directory,
|
|
975
|
+
logger=logger,
|
|
976
|
+
project=project,
|
|
977
|
+
reactor=reactor,
|
|
978
|
+
role=role,
|
|
979
|
+
isolated=True,
|
|
980
|
+
skip_install=True,
|
|
981
|
+
extra_pythonpath=suite_sites[tavern_test_directory],
|
|
982
|
+
)
|
|
983
|
+
return role
|
|
984
|
+
|
|
985
|
+
with ThreadPoolExecutor(max_workers=workers) as executor:
|
|
986
|
+
futures = {executor.submit(run_suite, suite_dir): suite_dir for suite_dir in runnable}
|
|
987
|
+
for future in as_completed(futures):
|
|
988
|
+
suite_dir = futures[future]
|
|
989
|
+
role = os.path.basename(suite_dir)
|
|
990
|
+
try:
|
|
991
|
+
future.result()
|
|
992
|
+
except Exception as error:
|
|
993
|
+
failures.append(f"{role}: {error}")
|
|
994
|
+
|
|
995
|
+
if failures:
|
|
996
|
+
raise BuildFailedException(
|
|
997
|
+
"Parallel tavern suites failed: " + "; ".join(sorted(failures))
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
|
|
486
1001
|
def verify_cypress(project: Project, logger: Logger, reactor: Reactor):
|
|
487
1002
|
# Get directories with test and cypress executable
|
|
488
1003
|
work_dir = project.expand_path(f"${CYPRESS_TEST_DIR}")
|
|
@@ -540,11 +1055,23 @@ def _run_cypress_tests_in_directory(work_dir, logger, project, reactor: Reactor)
|
|
|
540
1055
|
|
|
541
1056
|
|
|
542
1057
|
def _write_cypress_summary(project, logger):
|
|
543
|
-
"""Parse cypress_run.log and write a portable Markdown summary artifact.
|
|
1058
|
+
"""Parse cypress_run.log and write a portable Markdown summary artifact.
|
|
1059
|
+
|
|
1060
|
+
No-op when Cypress did not run (missing or empty cypress_run.log), so
|
|
1061
|
+
verify_environment jobs without Cypress do not emit a false warning.
|
|
1062
|
+
Parallel child subprocesses also skip it; the parent writes one summary
|
|
1063
|
+
after merging child Cypress logs.
|
|
1064
|
+
"""
|
|
544
1065
|
log_path = os.path.join(prepare_logs_directory(project), "cypress_run.log")
|
|
1066
|
+
if os.environ.get(SUBPROCESS_PROPERTIES_FILE_ENV):
|
|
1067
|
+
logger.debug("Skipping Cypress summary in verify_environment subprocess")
|
|
1068
|
+
return None
|
|
1069
|
+
if not os.path.isfile(log_path) or os.path.getsize(log_path) == 0:
|
|
1070
|
+
logger.debug("Skipping Cypress summary: cypress_run.log not found")
|
|
1071
|
+
return None
|
|
545
1072
|
summary_path = os.path.join(prepare_reports_directory(project), "cypress-summary.md")
|
|
546
1073
|
try:
|
|
547
|
-
output = _build_cypress_summary(log_path)
|
|
1074
|
+
output = _redact_secret_log_text(_build_cypress_summary(log_path))
|
|
548
1075
|
with os.fdopen(
|
|
549
1076
|
os.open(summary_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600),
|
|
550
1077
|
"w",
|
|
@@ -654,7 +1181,101 @@ def verify_tavern(project: Project, logger: Logger, reactor: Reactor):
|
|
|
654
1181
|
package_artifacts(project, test_dir, "tavern", project.get_property(ROLE))
|
|
655
1182
|
|
|
656
1183
|
|
|
657
|
-
def
|
|
1184
|
+
def _tavern_marker_context(test_dir, project):
|
|
1185
|
+
extra_args = [project.expand(prop) for prop in project.get_property(TAVERN_ADDITIONAL_ARGS, [])]
|
|
1186
|
+
requested_marker = _extract_tavern_marker_filter(extra_args)
|
|
1187
|
+
declared_markers = read_tavern_service_markers(test_dir)
|
|
1188
|
+
skip_non_matching = _get_bool_property(project, SKIP_NON_MATCHING_TAVERN_ARTIFACTS, False)
|
|
1189
|
+
return extra_args, requested_marker, declared_markers, skip_non_matching
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def _should_skip_nonmatching_tavern(requested_marker, declared_markers, skip_non_matching):
|
|
1193
|
+
return bool(skip_non_matching and requested_marker and declared_markers and requested_marker not in declared_markers)
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
def _install_tavern_requirements(test_dir, logger, project, reactor):
|
|
1197
|
+
requirements_file = os.path.join(test_dir, "requirements.txt")
|
|
1198
|
+
if not os.path.exists(requirements_file):
|
|
1199
|
+
return
|
|
1200
|
+
dependency = RequirementsFile(requirements_file)
|
|
1201
|
+
with _TAVERN_INSTALL_LOCK:
|
|
1202
|
+
install_dependencies(logger, project, dependency, reactor.pybuilder_venv,
|
|
1203
|
+
f"{prepare_logs_directory(project)}/install_tavern_pip_dependencies.log")
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
def _plugin_venv_executable_path(executable):
|
|
1207
|
+
if executable is None:
|
|
1208
|
+
return None
|
|
1209
|
+
if isinstance(executable, (list, tuple)):
|
|
1210
|
+
if not executable:
|
|
1211
|
+
return None
|
|
1212
|
+
return _plugin_venv_executable_path(executable[0])
|
|
1213
|
+
try:
|
|
1214
|
+
path = os.fspath(executable)
|
|
1215
|
+
except TypeError:
|
|
1216
|
+
return None
|
|
1217
|
+
if isinstance(path, bytes):
|
|
1218
|
+
path = os.fsdecode(path)
|
|
1219
|
+
return path
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
def _plugin_venv_python(reactor):
|
|
1223
|
+
venv = getattr(reactor, "pybuilder_venv", None)
|
|
1224
|
+
path = _plugin_venv_executable_path(getattr(venv, "executable", None))
|
|
1225
|
+
if path and os.path.isfile(path) and os.access(path, os.X_OK):
|
|
1226
|
+
return path
|
|
1227
|
+
raise BuildFailedException("plugin venv python is missing or not executable")
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def _tavern_suite_site_directory(project, role):
|
|
1231
|
+
root = project.expand_path("$dir_target/integration/tavern-sites")
|
|
1232
|
+
directory_utility._ensure_directory_exists(root)
|
|
1233
|
+
site = os.path.join(root, role)
|
|
1234
|
+
if os.path.islink(site) or os.path.isfile(site):
|
|
1235
|
+
os.unlink(site)
|
|
1236
|
+
elif os.path.isdir(site):
|
|
1237
|
+
shutil.rmtree(site)
|
|
1238
|
+
os.makedirs(site, mode=0o700)
|
|
1239
|
+
return site
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
def _isolated_suite_subprocess_env(extra_pythonpath=None):
|
|
1243
|
+
env = os.environ.copy()
|
|
1244
|
+
env.pop("GITHUB_STEP_SUMMARY", None)
|
|
1245
|
+
env.pop("PYTHONHOME", None)
|
|
1246
|
+
env.pop("VIRTUAL_ENV", None)
|
|
1247
|
+
env.pop("PYTHONPATH", None)
|
|
1248
|
+
if extra_pythonpath:
|
|
1249
|
+
env["PYTHONPATH"] = extra_pythonpath
|
|
1250
|
+
return env
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
def _install_tavern_suite_site(test_dir, logger, project, reactor):
|
|
1254
|
+
requirements_file = os.path.join(test_dir, "requirements.txt")
|
|
1255
|
+
if not os.path.exists(requirements_file):
|
|
1256
|
+
return None
|
|
1257
|
+
role = os.path.basename(test_dir)
|
|
1258
|
+
site = _tavern_suite_site_directory(project, role)
|
|
1259
|
+
command = [_plugin_venv_python(reactor), "-m", "pip", "install", "-r", requirements_file, "-t", site]
|
|
1260
|
+
logger.info(f"Installing tavern requirements for {role} into {site}")
|
|
1261
|
+
process = subprocess.Popen(
|
|
1262
|
+
command,
|
|
1263
|
+
stdout=subprocess.PIPE,
|
|
1264
|
+
stderr=subprocess.STDOUT,
|
|
1265
|
+
cwd=test_dir,
|
|
1266
|
+
env=_isolated_suite_subprocess_env(),
|
|
1267
|
+
shell=False,
|
|
1268
|
+
)
|
|
1269
|
+
returncode = _stream_subprocess_output(process, logger, role)
|
|
1270
|
+
if returncode != 0:
|
|
1271
|
+
raise BuildFailedException(
|
|
1272
|
+
f"Failed to install tavern requirements for {role} into isolated site {site}"
|
|
1273
|
+
)
|
|
1274
|
+
return site
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, reactor: Reactor, role=None,
|
|
1278
|
+
isolated=False, skip_install=False, extra_pythonpath=None):
|
|
658
1279
|
logger.info("Running tavern tests: {}".format(test_dir))
|
|
659
1280
|
if not os.path.exists(test_dir):
|
|
660
1281
|
logger.info("Skipping tavern run: no tests")
|
|
@@ -662,51 +1283,51 @@ def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, re
|
|
|
662
1283
|
logger.info(f"Found {len(os.listdir(test_dir))} files in tavern test directory")
|
|
663
1284
|
# todo is this unique enough for each run?
|
|
664
1285
|
output_file, run_name = get_test_report_file(project, test_dir)
|
|
665
|
-
extra_args
|
|
666
|
-
requested_marker
|
|
667
|
-
declared_markers = read_tavern_service_markers(test_dir)
|
|
668
|
-
skip_non_matching_artifacts = _get_bool_property(project, SKIP_NON_MATCHING_TAVERN_ARTIFACTS, False)
|
|
669
|
-
if skip_non_matching_artifacts and requested_marker and declared_markers and requested_marker not in declared_markers:
|
|
1286
|
+
extra_args, requested_marker, declared_markers, skip_non_matching = _tavern_marker_context(test_dir, project)
|
|
1287
|
+
if _should_skip_nonmatching_tavern(requested_marker, declared_markers, skip_non_matching):
|
|
670
1288
|
logger.info(
|
|
671
1289
|
f"Skipping Tavern tests: artifact markers {sorted(declared_markers)} do not match {requested_marker}"
|
|
672
1290
|
)
|
|
673
1291
|
return True
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
dependency = RequirementsFile(requirements_file)
|
|
680
|
-
install_dependencies(logger, project, dependency, reactor.pybuilder_venv,
|
|
681
|
-
f"{prepare_logs_directory(project)}/install_tavern_pip_dependencies.log")
|
|
1292
|
+
if not isolated:
|
|
1293
|
+
from sys import path as syspath
|
|
1294
|
+
syspath.insert(0, test_dir)
|
|
1295
|
+
if not skip_install:
|
|
1296
|
+
_install_tavern_requirements(test_dir, logger, project, reactor)
|
|
682
1297
|
args = ["--junit-xml", f"{output_file}", test_dir] + extra_args
|
|
683
1298
|
if project.get_property("verbose"):
|
|
684
1299
|
args.append("-s")
|
|
685
1300
|
args.append("-v")
|
|
686
|
-
if _get_bool_property(project, RUN_PARALLEL, False):
|
|
1301
|
+
if not isolated and _get_bool_property(project, RUN_PARALLEL, False):
|
|
687
1302
|
args.extend(['-n', 'auto'])
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1303
|
+
target_url = project.get_property(INTEGRATION_TARGET_URL)
|
|
1304
|
+
public_target_url = project.get_property(INTEGRATION_PUBLIC_TARGET_URL)
|
|
1305
|
+
environment = project.get_property(ENVIRONMENT)
|
|
1306
|
+
pytest_env = _isolated_suite_subprocess_env(extra_pythonpath) if isolated else os.environ.copy()
|
|
1307
|
+
pytest_env['TARGET'] = target_url
|
|
1308
|
+
pytest_env['PUBLIC_TARGET'] = public_target_url
|
|
1309
|
+
pytest_env[ENVIRONMENT] = environment
|
|
1310
|
+
if not isolated:
|
|
1311
|
+
os.environ['TARGET'] = target_url
|
|
1312
|
+
os.environ['PUBLIC_TARGET'] = public_target_url
|
|
1313
|
+
os.environ[ENVIRONMENT] = environment
|
|
1314
|
+
logger.info(f"Running against target: {target_url} and "
|
|
1315
|
+
f"public target: {public_target_url}")
|
|
1316
|
+
logger.debug(f"Running args: {args} ")
|
|
1317
|
+
label = role or os.path.basename(test_dir)
|
|
1318
|
+
ret = _invoke_pytest(args, test_dir, pytest_env, logger, label, isolated, reactor)
|
|
1319
|
+
if _is_pytest_no_tests_collected(ret):
|
|
1320
|
+
fallback_args = _without_tavern_marker_filter(args)
|
|
1321
|
+
if fallback_args != args and (not skip_non_matching or not declared_markers):
|
|
1322
|
+
logger.warn(
|
|
1323
|
+
"No Tavern tests matched the marker filter; rerunning the full suite"
|
|
1324
|
+
)
|
|
1325
|
+
logger.debug(f"Running fallback args: {fallback_args} ")
|
|
1326
|
+
ret = _invoke_pytest(
|
|
1327
|
+
fallback_args, test_dir, pytest_env, logger, label, isolated, reactor
|
|
1328
|
+
)
|
|
708
1329
|
|
|
709
|
-
if ret
|
|
1330
|
+
if _is_pytest_failure(ret):
|
|
710
1331
|
if role:
|
|
711
1332
|
roles = []
|
|
712
1333
|
if project.get_property(CONSOLIDATE_TESTS, False):
|
|
@@ -723,6 +1344,54 @@ def _run_tavern_tests_in_dir(test_dir: str, logger: Logger, project: Project, re
|
|
|
723
1344
|
return True
|
|
724
1345
|
|
|
725
1346
|
|
|
1347
|
+
def _is_pytest_no_tests_collected(ret):
|
|
1348
|
+
if ret is None:
|
|
1349
|
+
return False
|
|
1350
|
+
return int(ret) == int(pytest.ExitCode.NO_TESTS_COLLECTED)
|
|
1351
|
+
|
|
1352
|
+
|
|
1353
|
+
def _is_pytest_failure(ret):
|
|
1354
|
+
if ret is None:
|
|
1355
|
+
return True
|
|
1356
|
+
return int(ret) != 0
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
def _invoke_pytest(args, test_dir, env, logger, label, isolated, reactor=None):
|
|
1360
|
+
if not isolated:
|
|
1361
|
+
cache_wd = os.getcwd()
|
|
1362
|
+
try:
|
|
1363
|
+
os.chdir(test_dir)
|
|
1364
|
+
return pytest.main(args)
|
|
1365
|
+
finally:
|
|
1366
|
+
os.chdir(cache_wd)
|
|
1367
|
+
command = [_plugin_venv_python(reactor), "-u", "-m", "pytest"] + args
|
|
1368
|
+
process = subprocess.Popen(
|
|
1369
|
+
command,
|
|
1370
|
+
stdout=subprocess.PIPE,
|
|
1371
|
+
stderr=subprocess.STDOUT,
|
|
1372
|
+
cwd=test_dir,
|
|
1373
|
+
env=env,
|
|
1374
|
+
shell=False,
|
|
1375
|
+
)
|
|
1376
|
+
return _stream_subprocess_output(process, logger, label)
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def _stream_subprocess_output(process, logger, label):
|
|
1380
|
+
stdout = process.stdout
|
|
1381
|
+
if stdout is None:
|
|
1382
|
+
return process.wait()
|
|
1383
|
+
try:
|
|
1384
|
+
while True:
|
|
1385
|
+
chunk = stdout.readline()
|
|
1386
|
+
if not chunk:
|
|
1387
|
+
break
|
|
1388
|
+
redacted = _redact_secret_log_line(_decode_subprocess_stdout_line(chunk))
|
|
1389
|
+
logger.info(f"[{label}] {redacted}")
|
|
1390
|
+
finally:
|
|
1391
|
+
stdout.close()
|
|
1392
|
+
return process.wait()
|
|
1393
|
+
|
|
1394
|
+
|
|
726
1395
|
def _without_tavern_marker_filter(args):
|
|
727
1396
|
marker_options = {"-m", "--markexpr"}
|
|
728
1397
|
fallback_args = []
|
|
@@ -21,7 +21,7 @@ class install(_install):
|
|
|
21
21
|
if __name__ == '__main__':
|
|
22
22
|
setup(
|
|
23
23
|
name = 'pybuilder-integration',
|
|
24
|
-
version = '
|
|
24
|
+
version = '109',
|
|
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,
|
|
File without changes
|
{pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration/artifact_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration/exec_utility.py
RENAMED
|
File without changes
|
{pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration/tool_utility.py
RENAMED
|
File without changes
|
{pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration.egg-info/requires.txt
RENAMED
|
File without changes
|
{pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration.egg-info/top_level.txt
RENAMED
|
File without changes
|
{pybuilder_integration-107 → pybuilder_integration-109}/pybuilder_integration.egg-info/zip-safe
RENAMED
|
File without changes
|
|
File without changes
|