diff --git a/testing/cucumber/features/environment.py b/testing/cucumber/features/environment.py index 5ad6eae7fe..4023fe4b06 100644 --- a/testing/cucumber/features/environment.py +++ b/testing/cucumber/features/environment.py @@ -276,10 +276,7 @@ def _cleanup_async_job_files(): ) return if response.status_code != 200: - print( - f"\n[CLEANUP] Async job cleanup returned {response.status_code}: " - f"{response.text[:200]}" - ) + print(f"\n[CLEANUP] Async job cleanup returned {response.status_code}: {response.text[:200]}") return try: summary = response.json() diff --git a/testing/cucumber/features/steps/job_step_definitions.py b/testing/cucumber/features/steps/job_step_definitions.py index 44c695b7a4..043dcf2c1e 100644 --- a/testing/cucumber/features/steps/job_step_definitions.py +++ b/testing/cucumber/features/steps/job_step_definitions.py @@ -1,4 +1,5 @@ """Steps for the async job API. DELETE is a cancel, so it 400s once the job finishes.""" + import time import requests @@ -21,27 +22,25 @@ def step_wait_for_job(context): while time.time() < deadline: response = requests.get( f"{BASE_URL}/api/v1/general/job/{context.job_id}", - headers=API_HEADERS, timeout=30, - ) - assert response.status_code == 200, ( - f"Job status returned {response.status_code}: {response.text}" + headers=API_HEADERS, + timeout=30, ) + assert response.status_code == 200, f"Job status returned {response.status_code}: {response.text}" context.response = response payload = response.json() if payload.get("complete"): context.job_status = payload return time.sleep(0.2) - raise AssertionError( - f"Job {context.job_id} did not complete within {POLL_TIMEOUT_SECONDS}s" - ) + raise AssertionError(f"Job {context.job_id} did not complete within {POLL_TIMEOUT_SECONDS}s") @when("I request the job result") def step_request_job_result(context): context.response = requests.get( f"{BASE_URL}/api/v1/general/job/{context.job_id}/result", - headers=API_HEADERS, timeout=60, + headers=API_HEADERS, + timeout=60, ) @@ -49,7 +48,8 @@ def step_request_job_result(context): def step_request_job_result_files(context): context.response = requests.get( f"{BASE_URL}/api/v1/general/job/{context.job_id}/result/files", - headers=API_HEADERS, timeout=60, + headers=API_HEADERS, + timeout=60, ) files = context.response.json().get("files") or [] context.job_files = files @@ -62,7 +62,8 @@ def step_download_job_file(context): assert getattr(context, "job_file_id", None), "No fileId captured from the result file list" context.response = requests.get( f"{BASE_URL}/api/v1/general/files/{context.job_file_id}", - headers=API_HEADERS, timeout=60, + headers=API_HEADERS, + timeout=60, ) @@ -71,7 +72,8 @@ def step_job_file_metadata(context): assert getattr(context, "job_file_id", None), "No fileId captured from the result file list" context.response = requests.get( f"{BASE_URL}/api/v1/general/files/{context.job_file_id}/metadata", - headers=API_HEADERS, timeout=60, + headers=API_HEADERS, + timeout=60, ) @@ -79,7 +81,8 @@ def step_job_file_metadata(context): def step_cancel_job(context): context.response = requests.delete( f"{BASE_URL}/api/v1/general/job/{context.job_id}", - headers=API_HEADERS, timeout=30, + headers=API_HEADERS, + timeout=30, ) @@ -127,8 +130,7 @@ def step_check_cleanup_idempotent(context): removed = context.cleanup_summary.get("jobsRemoved") deleted = context.cleanup_summary.get("filesDeleted") assert removed == 0 and deleted == 0, ( - "A repeat cleanup still found work to do, so the first pass did not fully clean up: " - f"{context.cleanup_summary}" + f"A repeat cleanup still found work to do, so the first pass did not fully clean up: {context.cleanup_summary}" ) @@ -136,11 +138,11 @@ def step_check_cleanup_idempotent(context): def step_check_job_gone(context): response = requests.get( f"{BASE_URL}/api/v1/general/job/{context.job_id}", - headers=API_HEADERS, timeout=30, + headers=API_HEADERS, + timeout=30, ) assert response.status_code == 404, ( - f"Job {context.job_id} still exists after cleanup: " - f"{response.status_code} {response.text[:200]}" + f"Job {context.job_id} still exists after cleanup: {response.status_code} {response.text[:200]}" ) @@ -149,11 +151,11 @@ def step_check_job_file_gone(context): assert getattr(context, "job_file_id", None), "No fileId captured from the result file list" response = requests.get( f"{BASE_URL}/api/v1/general/files/{context.job_file_id}", - headers=API_HEADERS, timeout=30, + headers=API_HEADERS, + timeout=30, ) assert response.status_code == 404, ( - f"File {context.job_file_id} still downloadable after cleanup: " - f"{response.status_code} {response.text[:200]}" + f"File {context.job_file_id} still downloadable after cleanup: {response.status_code} {response.text[:200]}" ) @@ -162,10 +164,10 @@ def step_check_job_file_still_there(context): assert getattr(context, "job_file_id", None), "No fileId captured from the result file list" response = requests.get( f"{BASE_URL}/api/v1/general/files/{context.job_file_id}", - headers=API_HEADERS, timeout=60, + headers=API_HEADERS, + timeout=60, ) assert response.status_code == 200, ( - f"File {context.job_file_id} was not retrievable a second time: " - f"{response.status_code} {response.text[:200]}" + f"File {context.job_file_id} was not retrievable a second time: {response.status_code} {response.text[:200]}" ) assert len(response.content) > 0, "Second download returned an empty body" diff --git a/testing/cucumber/features/steps/job_support.py b/testing/cucumber/features/steps/job_support.py index 027bdcb9eb..14b71123f4 100644 --- a/testing/cucumber/features/steps/job_support.py +++ b/testing/cucumber/features/steps/job_support.py @@ -4,6 +4,7 @@ Support module, not a step module: behave execs everything under features/steps step definitions, so anything environment.py needs to import has to live apart from the @when/@then decorators or they would register twice. """ + import requests BASE_URL = "http://localhost:8080" diff --git a/testing/cucumber/features/steps/parallel_support.py b/testing/cucumber/features/steps/parallel_support.py index be09c9cfaf..bebc7c3433 100644 --- a/testing/cucumber/features/steps/parallel_support.py +++ b/testing/cucumber/features/steps/parallel_support.py @@ -58,11 +58,7 @@ def _normalize_name(name): def _strip_volatile(value): """Drop keys whose values legitimately differ between two identical requests.""" if isinstance(value, dict): - return { - k: _strip_volatile(v) - for k, v in sorted(value.items()) - if not _VOLATILE_KEY_RE.match(k) - } + return {k: _strip_volatile(v) for k, v in sorted(value.items()) if not _VOLATILE_KEY_RE.match(k)} if isinstance(value, list): return [_strip_volatile(v) for v in value] if isinstance(value, str): @@ -130,7 +126,6 @@ def fingerprint(response): content_type = (response.headers.get("Content-Type") or "").split(";")[0].strip() parts = {"status": response.status_code, "content_type": content_type, "size": len(body)} - if "json" in content_type: try: parts["json"] = _strip_volatile(json_module.loads(body.decode("utf-8"))) @@ -151,11 +146,7 @@ def fingerprint(response): def differing_keys(baseline, other): - return { - key - for key in set(baseline) | set(other) - if key != "size" and baseline.get(key) != other.get(key) - } + return {key for key in set(baseline) | set(other) if key != "size" and baseline.get(key) != other.get(key)} def size_differs(baseline, other): @@ -167,9 +158,7 @@ def compare(baseline, other, ignore=frozenset(), ignore_size=False): """Return a list of human-readable differences between two fingerprints.""" diffs = [] for key in sorted(differing_keys(baseline, other) - set(ignore)): - diffs.append( - f"{key}: baseline={_short(baseline.get(key))} parallel={_short(other.get(key))}" - ) + diffs.append(f"{key}: baseline={_short(baseline.get(key))} parallel={_short(other.get(key))}") if not ignore_size and size_differs(baseline, other): diffs.append( f"size: baseline={baseline.get('size', 0)} parallel={other.get('size', 0)} " @@ -211,9 +200,7 @@ def build_decoy_spec(spec): width, height = float(box.width), float(box.height) overlay_buffer = io.BytesIO() overlay_canvas = canvas.Canvas(overlay_buffer, pagesize=(width, height)) - overlay_canvas.drawString( - 20, max(20.0, height - 20), f"DECOY-MARKER-{index}-do-not-mix" - ) + overlay_canvas.drawString(20, max(20.0, height - 20), f"DECOY-MARKER-{index}-do-not-mix") overlay_canvas.showPage() overlay_canvas.save() overlay_buffer.seek(0) @@ -260,16 +247,12 @@ def validate(context, url, spec, headers, baseline, label, timeout=300): baseline_fp = fingerprint(baseline) noise, noisy_size = frozenset(), False - failures = _collect_failures( - main_results, decoy_results, baseline_fp, repeat, noise, noisy_size - ) + failures = _collect_failures(main_results, decoy_results, baseline_fp, repeat, noise, noisy_size) if failures: # Some endpoints are inherently nondeterministic (embedded ids, timestamps, # deliberate randomness). Re-run sequentially to tell that apart from a real bug. noise, noisy_size = _probe_noise(url, spec, headers, baseline_fp, timeout) - failures = _collect_failures( - main_results, decoy_results, baseline_fp, repeat, noise, noisy_size - ) + failures = _collect_failures(main_results, decoy_results, baseline_fp, repeat, noise, noisy_size) VALIDATIONS.append( { @@ -302,12 +285,9 @@ def _collect_failures(main_results, decoy_results, baseline_fp, repeat, noise, n diffs = compare(baseline_fp, actual_fp, noise, noisy_size) if not diffs: continue - if decoy_fp is not None and not compare( - decoy_fp, actual_fp, noise, noisy_size - ): + if decoy_fp is not None and not compare(decoy_fp, actual_fp, noise, noisy_size): failures.append( - f"copy {index + 1}/{repeat} returned the CONCURRENT DECOY REQUEST'S response " - f"(cross-request bleed)" + f"copy {index + 1}/{repeat} returned the CONCURRENT DECOY REQUEST'S response (cross-request bleed)" ) else: failures.append(f"copy {index + 1}/{repeat} diverged: " + "; ".join(diffs)) @@ -373,7 +353,7 @@ def validate_get(context, url, params, headers, baseline, label, timeout=60): diffs = compare( baseline_fp, fingerprint(response), - noise, + noise, noisy_size, ) if diffs: @@ -386,11 +366,7 @@ def validate_get(context, url, params, headers, baseline, label, timeout=60): probes = [] for _ in range(NOISE_PROBE_SAMPLES): try: - probes.append( - fingerprint( - requests.get(url, params=params, headers=headers, timeout=timeout) - ) - ) + probes.append(fingerprint(requests.get(url, params=params, headers=headers, timeout=timeout))) except Exception: break noise, noisy_size = _noise_from_samples(baseline_fp, probes) @@ -407,8 +383,7 @@ def validate_get(context, url, params, headers, baseline, label, timeout=60): ) if failures: raise AssertionError( - f"Parallel consistency failed for GET {label} at concurrency {repeat}.\n - " - + "\n - ".join(failures) + f"Parallel consistency failed for GET {label} at concurrency {repeat}.\n - " + "\n - ".join(failures) )