From e0323323a9659be7bcdd5b9ac096ed802c208111 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 10:02:16 -0400 Subject: [PATCH 01/34] chore: set up autoresearch session (less tests, same coverage) --- .auto/.gitignore | 7 ++++ .auto/checks.sh | 58 ++++++++++++++++++++++++++ .auto/ideas.md | 10 +++++ .auto/log_run.py | 96 +++++++++++++++++++++++++++++++++++++++++++ .auto/measure.sh | 69 +++++++++++++++++++++++++++++++ .auto/prompt.md | 105 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 345 insertions(+) create mode 100644 .auto/.gitignore create mode 100755 .auto/checks.sh create mode 100644 .auto/ideas.md create mode 100644 .auto/log_run.py create mode 100755 .auto/measure.sh create mode 100644 .auto/prompt.md diff --git a/.auto/.gitignore b/.auto/.gitignore new file mode 100644 index 0000000000..58f7050cfe --- /dev/null +++ b/.auto/.gitignore @@ -0,0 +1,7 @@ +# Run artifacts — not committed (regenerated each iteration) +coverage.json +junit.xml +last_run.log +log.jsonl +baseline_coverage.json +__pycache__/ diff --git a/.auto/checks.sh b/.auto/checks.sh new file mode 100755 index 0000000000..8f7b2fa482 --- /dev/null +++ b/.auto/checks.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# Autoresearch backpressure checks: coverage guard + ruff. +# Runs after a PASSING benchmark. Output kept minimal (errors only). +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +# --- Coverage guard: totals must not decrease vs baseline ------------------- +if [ -f .auto/baseline_coverage.json ] && [ -f .auto/coverage.json ]; then + python3 - <<'EOF' +import json, sys + +with open(".auto/baseline_coverage.json") as f: + base = json.load(f)["totals"] +with open(".auto/coverage.json") as f: + cur = json.load(f)["totals"] + +problems = [] +for key in ("covered_lines", "covered_branches"): + if cur[key] < base[key]: + problems.append(f"{key}: {cur[key]} < baseline {base[key]}") + +if problems: + print("COVERAGE GUARD FAILED:") + for p in problems: + print(" " + p) + # Show which files lost coverage to help the agent + with open(".auto/baseline_coverage.json") as f: + bfiles = json.load(f)["files"] + with open(".auto/coverage.json") as f: + cfiles = json.load(f)["files"] + dips = [] + for fn, b in bfiles.items(): + c = cfiles.get(fn) + if c is None: + dips.append((fn, b["summary"]["covered_lines"], 0)) + continue + bl = b["summary"]["covered_lines"] + b["summary"].get("covered_branches", 0) + cl = c["summary"]["covered_lines"] + c["summary"].get("covered_branches", 0) + if cl < bl: + dips.append((fn, bl, cl)) + dips.sort(key=lambda d: d[1] - d[2], reverse=True) + for fn, bl, cl in dips[:15]: + print(f" {fn}: {bl} -> {cl}") + sys.exit(1) + +print("coverage guard OK " + f"(lines {cur['covered_lines']}>={base['covered_lines']}, " + f"branches {cur['covered_branches']}>={base['covered_branches']})") +EOF +else + echo "no baseline coverage yet — guard skipped" +fi + +# --- Ruff on tests ---------------------------------------------------------- +uv run ruff check tests/ 2>&1 | tail -20 +echo "checks OK" diff --git a/.auto/ideas.md b/.auto/ideas.md new file mode 100644 index 0000000000..1d30c68e0c --- /dev/null +++ b/.auto/ideas.md @@ -0,0 +1,10 @@ +# Ideas backlog + +(Promising but deferred optimization ideas go here. Prune when tried/stale.) + +- Legacy Hub/scope API tests (test_basics.py, test_scope.py) vs + tests/new_scopes_compat/ — the latter replays scope behaviors through + new-style APIs; some assertions may be exact duplicates. +- Look for hand-rolled loops over inputs that could be one parametrized test. +- tests/test_basics.py has many small "processors"/"breadcrumbs" tests that + may overlap heavily in setup + covered lines. diff --git a/.auto/log_run.py b/.auto/log_run.py new file mode 100644 index 0000000000..9c5ac56fd6 --- /dev/null +++ b/.auto/log_run.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Append a run entry to .auto/log.jsonl in the pi-autoresearch extension format. + +Usage: + python3 .auto/log_run.py --status keep --metric 1234 \ + --metrics '{"runtime_s": 42.1, "covered_lines": 9000}' \ + --description "merged redundant scope tests" \ + --asi '{"file": "tests/test_scope.py", "delta": -12}' +""" +import argparse +import json +import subprocess +import time +from pathlib import Path + +LOG = Path(__file__).parent / "log.jsonl" + + +def next_run_number() -> int: + n = 0 + if LOG.exists(): + for line in LOG.read_text().splitlines(): + if not line.strip(): + continue + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + if isinstance(entry.get("run"), int): + n = max(n, entry["run"]) + return n + 1 + + +def confidence(metric: float, status: str): + """Best improvement as a multiple of the session noise floor + (stdev of kept-run primary metrics).""" + kept = [] + best = None + if LOG.exists(): + for line in LOG.read_text().splitlines(): + if not line.strip(): + continue + try: + e = json.loads(line) + except json.JSONDecodeError: + continue + if "run" not in e or not isinstance(e.get("metric"), (int, float)): + continue + if e.get("status") == "keep": + kept.append(e["metric"]) + best = e["metric"] if best is None else min(best, e["metric"]) + if best is None or len(kept) < 3: + return None + mean = sum(kept) / len(kept) + var = sum((m - mean) ** 2 for m in kept) / (len(kept) - 1) + noise = var**0.5 + if noise < 1e-9: + return None + improvement = best - metric if status == "keep" else 0.0 + return round(improvement / noise, 2) + + +def main() -> None: + p = argparse.ArgumentParser() + p.add_argument( + "--status", required=True, choices=["keep", "discard", "crash", "checks_failed"] + ) + p.add_argument("--metric", required=True, type=float) + p.add_argument("--metrics", default="{}") + p.add_argument("--description", required=True) + p.add_argument("--asi", default="{}") + args = p.parse_args() + + commit = subprocess.run( + ["git", "rev-parse", "HEAD"], capture_output=True, text=True, check=True + ).stdout.strip() + + entry = { + "run": next_run_number(), + "commit": commit, + "metric": args.metric, + "metrics": json.loads(args.metrics), + "status": args.status, + "description": args.description, + "timestamp": int(time.time() * 1000), + "segment": 0, + "confidence": confidence(args.metric, args.status), + "asi": json.loads(args.asi), + } + with LOG.open("a") as f: + f.write(json.dumps(entry) + "\n") + print(f"logged run {entry['run']} status={entry['status']} metric={entry['metric']}") + + +if __name__ == "__main__": + main() diff --git a/.auto/measure.sh b/.auto/measure.sh new file mode 100755 index 0000000000..e0a3733cf2 --- /dev/null +++ b/.auto/measure.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# Autoresearch benchmark: run the common test suite, emit METRIC lines. +# Primary metric: test_count (lower is better). +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +TOX_ENV_DIR=".tox/py3.14-common" +PY="$TOX_ENV_DIR/bin/python" + +# --- Ensure the tox env exists --------------------------------------------- +if [ ! -x "$PY" ]; then + echo "Provisioning tox env py3.14-common (one-time)..." >&2 + uv run tox -e py3.14-common --notest >&2 +fi + +# --- Fast pre-check: syntax of all test files (<1s after first run) --------- +python3 -m compileall -q tests/ >/dev/null + +# --- Run the suite ---------------------------------------------------------- +# Mirrors CI: tox py3.14-common runs `python -m pytest tests` with +# PYTEST_ADDOPTS="--ignore=tests/test_shadowed_module.py" and +# -W error::pytest.PytestUnraisableExceptionWarning. +START=$(python3 -c 'import time; print(time.time())') +set +e +"$PY" -m pytest tests \ + -W error::pytest.PytestUnraisableExceptionWarning \ + --ignore=tests/test_shadowed_module.py \ + --cov-report=json:.auto/coverage.json \ + --junitxml=.auto/junit.xml -o junit_suite_name=common \ + > .auto/last_run.log 2>&1 +PYTEST_EXIT=$? +set -e +END=$(python3 -c 'import time; print(time.time())') + +echo "=== pytest tail (exit=$PYTEST_EXIT) ===" +tail -n 12 .auto/last_run.log + +# --- Metrics ---------------------------------------------------------------- +TEST_COUNT=$(grep -c ' experiment crashed (caller treats as crash/discard) +exit "$PYTEST_EXIT" diff --git a/.auto/prompt.md b/.auto/prompt.md new file mode 100644 index 0000000000..8a0116d8e1 --- /dev/null +++ b/.auto/prompt.md @@ -0,0 +1,105 @@ +# Autoresearch: fewer common-suite tests, same coverage + +## Objective + +Reduce the number of collected tests in the sentry-python **common test suite** +(`tests/`, excluding `tests/integrations/`) **without reducing code coverage** +of `sentry_sdk/` and without losing meaningful assertions. + +The value is CI time and maintenance burden. The guardrails are: +1. All remaining tests pass. +2. Coverage totals do not decrease: `covered_lines` AND `covered_branches` + (branch coverage, of `sentry_sdk/`) must stay >= baseline. +3. `ruff check tests/` is clean. + +Reductions must come from **true redundancy**, e.g.: +- Tests that are exact/near duplicates of another test (same code paths, same + assertions, no new branch coverage). +- Tests superseded by a broader test that covers the same paths plus more. +- N near-identical tests merged into one `@pytest.mark.parametrize` case + (ALL original assertions preserved). +- Tests of trivial behavior already exercised as a side effect of broader tests + (only if deleting them does not drop any covered line/branch). + +Do NOT: +- Weaken or delete assertions just to make merging easier. +- Delete tests whose value is not visible in coverage (e.g. asserting the + ABSENCE of events/spans, exact payload values, ordering, warning text) + unless an equivalent assertion exists elsewhere. +- Merge tests that test conceptually different behaviors into an unreadable + mega-test. Clarity counts. + +## Metrics + +- **Primary**: `test_count` (count, lower is better) — collected+executed test + cases (parametrized cases count individually), from JUnit XML. +- **Secondary**: `runtime_s` (suite wall time), `covered_lines`, + `covered_branches`, `coverage_pct`, `failed`, `skipped`. + +## How to Run + +- Benchmark: `./.auto/measure.sh` — runs the common suite with coverage, + prints `METRIC name=value` lines, saves full output to `.auto/last_run.log`, + JUnit to `.auto/junit.xml`, coverage JSON to `.auto/coverage.json`. + Exits nonzero if pytest fails. +- Checks: `./.auto/checks.sh` — coverage guard vs `.auto/baseline_coverage.json` + + `ruff check tests/`. Exits nonzero on failure. + +### Emulated tool loop (extension tools not loaded in this session) + +The pi-autoresearch extension is not active, so the loop is driven manually: + +1. Make a focused change to test files (one idea per iteration). +2. `./.auto/measure.sh` — if it exits nonzero → status `crash`. +3. Otherwise `./.auto/checks.sh` — if it exits nonzero → status `checks_failed`. +4. Compare `test_count` to best kept value: + - lower → `keep`: `git add tests .auto/prompt.md .auto/ideas.md && git commit` + - equal/higher → `discard`: `git restore --source=HEAD --worktree --staged tests && git clean -fd tests` +5. Log EVERY run: `python3 .auto/log_run.py --status --metric --metrics '{"runtime_s":..,"covered_lines":..,"covered_branches":..}' --description "..." --asi '{"key":"value"}'` +6. Update "What's Been Tried" in this file after notable outcomes. + +Baseline runs: run measure.sh twice before accepting the baseline to gauge +flakiness of runtime and coverage totals. + +## Files in Scope + +- `tests/*.py` (top-level test modules; biggest: test_ai_monitoring.py 2083 LOC, + test_client.py 1873, test_basics.py 1243, test_scope.py 1112, + test_utils.py 1095, test_transport.py 1041) +- `tests/tracing/`, `tests/utils/`, `tests/profiler/`, `tests/new_scopes_compat/` + (note: new_scopes_compat tests the SAME scope behaviors through new APIs — + some overlap with legacy-API tests may be intentional API-compat coverage; + only merge/delete if truly redundant) +- `tests/conftest.py` — CAUTION: shared with the `gevent` tox env (also runs + `tests/`). Fixture changes must not break gevent. Prefer not touching it. + +## Off Limits + +- `sentry_sdk/**` — the SDK source. Never modify. +- `tests/integrations/**` — out of scope for now (separate tox envs). +- `tests/test_shadowed_module.py` — excluded from common; run by its own env. +- `tests/test_ai_integration_deactivation.py` — run by its own env too + (integration_deactivation). Leave alone unless it affects common counts + (it is collected by common as well — verify from baseline JUnit). +- `pyproject.toml` (pytest addopts, coverage config), `tox.ini`, `scripts/`, + `.github/`, `tests/test.key`, `tests/test.pem`. + +## Constraints + +- Remaining tests must pass: `pytest` exit code 0. +- Coverage guard: `covered_lines` and `covered_branches` in + `.auto/coverage.json` must both be >= the baseline values. +- `ruff check tests/` must pass. +- No new dependencies. No changes to pytest/coverage configuration. +- Deleting a test is only justified if its covered lines+branches are covered + by other tests AND its assertions are either redundant or preserved elsewhere. + +## Flakiness notes + +- If a run fails checks due to a small coverage dip in an UNRELATED file, + re-run measure.sh once before discarding — some tests are timing-sensitive. +- `runtime_s` is noisy; it is informational only, never a keep/discard reason. + +## What's Been Tried + +(nothing yet — baseline pending) From f3df3b18012b971ad109bdf3a05d80fc3ae2e1eb Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 10:06:52 -0400 Subject: [PATCH 02/34] fix: count junit testcases by occurrences, not lines --- .auto/measure.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.auto/measure.sh b/.auto/measure.sh index e0a3733cf2..f911d5d74a 100755 --- a/.auto/measure.sh +++ b/.auto/measure.sh @@ -38,7 +38,7 @@ echo "=== pytest tail (exit=$PYTEST_EXIT) ===" tail -n 12 .auto/last_run.log # --- Metrics ---------------------------------------------------------------- -TEST_COUNT=$(grep -c ' Date: Thu, 30 Jul 2026 10:15:34 -0400 Subject: [PATCH 03/34] test: remove 6 permanently-skipped dead tests (Hub deprecation leftovers) --- tests/test_basics.py | 98 -------------------------------------------- tests/test_client.py | 21 ---------- 2 files changed, 119 deletions(-) diff --git a/tests/test_basics.py b/tests/test_basics.py index 3f9331df40..db1a028c45 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -23,7 +23,6 @@ push_scope, start_transaction, ) -from sentry_sdk.client import Client from sentry_sdk.integrations import ( _AUTO_ENABLING_INTEGRATIONS, _DEFAULT_INTEGRATIONS, @@ -334,38 +333,6 @@ def test_push_scope_null_client( assert len(events) == 0 -@pytest.mark.skip( - reason="This test is not valid anymore, because push_scope just returns the isolation scope. This test should be removed once the Hub is removed" -) -@pytest.mark.parametrize("null_client", (True, False)) -def test_push_scope_callback(sentry_init, null_client, capture_events): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - sentry_init() - - if null_client: - Hub.current.bind_client(None) - - outer_scope = Hub.current.scope - - calls = [] - - @push_scope - def _(scope): - assert scope is Hub.current.scope - assert scope is not outer_scope - calls.append(1) - - # push_scope always needs to execute the callback regardless of - # client state, because that actually runs usercode in it, not - # just scope config code - assert calls == [1] - - # Assert scope gets popped correctly - assert Hub.current.scope is outer_scope - - def test_breadcrumbs(sentry_init, capture_events): sentry_init(max_breadcrumbs=10) events = capture_events() @@ -636,71 +603,6 @@ def test_integrations( } == expected_integrations -@pytest.mark.skip( - reason="This test is not valid anymore, because with the new Scopes calling bind_client on the Hub sets the client on the global scope. This test should be removed once the Hub is removed" -) -def test_client_initialized_within_scope(sentry_init, caplog): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - caplog.set_level(logging.WARNING) - - sentry_init() - - with push_scope(): - Hub.current.bind_client(Client()) - - (record,) = (x for x in caplog.records if x.levelname == "WARNING") - - assert record.msg.startswith("init() called inside of pushed scope.") - - -@pytest.mark.skip( - reason="This test is not valid anymore, because with the new Scopes the push_scope just returns the isolation scope. This test should be removed once the Hub is removed" -) -def test_scope_leaks_cleaned_up(sentry_init, caplog): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - caplog.set_level(logging.WARNING) - - sentry_init() - - old_stack = list(Hub.current._stack) - - with push_scope(): - push_scope() - - assert Hub.current._stack == old_stack - - (record,) = (x for x in caplog.records if x.levelname == "WARNING") - - assert record.message.startswith("Leaked 1 scopes:") - - -@pytest.mark.skip( - reason="This test is not valid anymore, because with the new Scopes there is not pushing and popping of scopes. This test should be removed once the Hub is removed" -) -def test_scope_popped_too_soon(sentry_init, caplog): - """ - This test can be removed when we remove push_scope and the Hub from the SDK. - """ - caplog.set_level(logging.ERROR) - - sentry_init() - - old_stack = list(Hub.current._stack) - - with push_scope(): - Hub.current.pop_scope_unsafe() - - assert Hub.current._stack == old_stack - - (record,) = (x for x in caplog.records if x.levelname == "ERROR") - - assert record.message == ("Scope popped too soon. Popped 1 scopes too many.") - - def test_scope_event_processor_order(sentry_init, capture_events): def before_send(event, hint): event["message"] += "baz" diff --git a/tests/test_client.py b/tests/test_client.py index 78868e434a..3869bd9520 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -678,27 +678,6 @@ def test_client_debug_option_disabled(with_client, sentry_init, caplog): assert "OK" not in caplog.text -@pytest.mark.skip( - reason="New behavior in SDK 2.0: You have a scope before init and add data to it." -) -def test_scope_initialized_before_client(sentry_init, capture_events): - """ - This is a consequence of how configure_scope() works. We must - make `configure_scope()` a noop if no client is configured. Even - if the user later configures a client: We don't know that. - """ - with configure_scope() as scope: - scope.set_tag("foo", 42) - - sentry_init() - - events = capture_events() - capture_message("hi") - (event,) = events - - assert "tags" not in event - - def test_weird_chars(sentry_init, capture_events): sentry_init() events = capture_events() From ab3919f5260a441f5f1fd0e24f20aad2d07cabed Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 10:46:12 -0400 Subject: [PATCH 04/34] test(transport): replace 192-case cross-product with 24 curated cases The full debug x flush x pickle x level x algo x http2 matrix ran identical assertions 192 times. The compression-relevant dimensions (level x algo x http2) stay fully crossed; debug/flush/pickle rotate through cases so every value is still exercised. --- .auto/analyze.py | 148 ++++++++++++++++++++++++++++++++++++++++ .auto/prompt.md | 19 +++++- tests/test_transport.py | 40 +++++++++-- 3 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 .auto/analyze.py diff --git a/.auto/analyze.py b/.auto/analyze.py new file mode 100644 index 0000000000..b09026e553 --- /dev/null +++ b/.auto/analyze.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Analyze .coverage DB with per-test contexts: find coverage-redundant tests. + +A test is "coverage-redundant" iff every line and every branch arc it covers +is also covered by at least one other test. Deleting it cannot reduce coverage +totals (advisory only — checks.sh is the real guard). + +Outputs: + .auto/redundant_tests.txt — ranked candidates (redundant first) + .auto/attribution.json — per-test covered/unique entity counts +""" +import json +import sqlite3 +from collections import defaultdict +from pathlib import Path + +from coverage.numbits import numbits_to_nums + +DB = ".coverage" +OUT_DIR = Path(".auto") + +con = sqlite3.connect(DB) +files = dict(con.execute("select id, path from file").fetchall()) +contexts = dict(con.execute("select id, context from context").fetchall()) + + +def nodeid(ctx: str) -> str: + # "tests/x.py::test_y[1]|setup" -> "tests/x.py::test_y[1]" + return ctx.rsplit("|", 1)[0] if "|" in ctx else ctx + + +# test -> set((file, lineno)), test -> set((file, from, to)) +test_lines = defaultdict(set) +test_arcs = defaultdict(set) + +# With branch=true, coverage stores arcs only; line_bits is empty. +# Derive per-test lines from arcs: arc fromno->tono covers fromno (and tono if >0). +for file_id, ctx_id, numbits in con.execute( + "select file_id, context_id, numbits from line_bits" +): + t = nodeid(contexts[ctx_id]) + path = files[file_id] + for ln in numbits_to_nums(numbits): + test_lines[t].add((path, ln)) + +for file_id, ctx_id, fromno, tono in con.execute( + "select file_id, context_id, fromno, tono from arc" +): + t = nodeid(contexts[ctx_id]) + path = files[file_id] + test_arcs[t].add((path, fromno, tono)) + test_lines[t].add((path, fromno)) + if tono > 0: + test_lines[t].add((path, tono)) + +# coverage counts per entity +line_count = defaultdict(int) +for lines in test_lines.values(): + for e in lines: + line_count[e] += 1 +arc_count = defaultdict(int) +for arcs in test_arcs.values(): + for e in arcs: + arc_count[e] += 1 + +rows = [] +all_tests = sorted(set(test_lines) | set(test_arcs)) +for t in all_tests: + lines = test_lines.get(t, set()) + arcs = test_arcs.get(t, set()) + uniq_lines = sum(1 for e in lines if line_count[e] == 1) + uniq_arcs = sum(1 for e in arcs if arc_count[e] == 1) + rows.append( + { + "test": t, + "lines": len(lines), + "arcs": len(arcs), + "unique_lines": uniq_lines, + "unique_arcs": uniq_arcs, + "redundant": uniq_lines == 0 and uniq_arcs == 0, + } + ) + +redundant = [r for r in rows if r["redundant"]] +redundant.sort(key=lambda r: -(r["lines"] + r["arcs"])) +keepers = [r for r in rows if not r["redundant"]] +keepers.sort(key=lambda r: (r["unique_lines"] + r["unique_arcs"])) + +with open(OUT_DIR / "attribution.json", "w") as f: + json.dump(rows, f, indent=1) + +with open(OUT_DIR / "redundant_tests.txt", "w") as f: + f.write(f"# {len(redundant)} fully coverage-redundant tests " + f"(of {len(rows)} tests with attribution)\n") + f.write("# NOTE: map excludes tests/profiler/test_continuous_profiler.py and\n") + f.write("# 16 tests that fail under --cov-context=test (conservative).\n\n") + for r in redundant: + f.write(f"{r['test']} (lines={r['lines']}, arcs={r['arcs']})\n") + +print(f"tests with attribution: {len(rows)}") +print(f"fully redundant: {len(redundant)}") +print(f"lines covered: {len(line_count)}, arcs covered: {len(arc_count)}") + +# Greedy maximal deletable set. Deletion only decreases entity counts, so a +# test can never become deletable later; a single heap-ordered pass suffices: +# pop the most-specialized deletable candidate, recheck against live counts, +# delete (decrement) or skip permanently. +import heapq + +heap = [ + (len(test_lines.get(t, ())) + len(test_arcs.get(t, ())), t) for t in all_tests +] +heapq.heapify(heap) +cur_line_count = dict(line_count) +cur_arc_count = dict(arc_count) +removed = set() +deletable = [] + + +def is_deletable(t): + return all(cur_line_count.get(e, 0) >= 2 for e in test_lines.get(t, ())) and all( + cur_arc_count.get(e, 0) >= 2 for e in test_arcs.get(t, ()) + ) + + +while heap: + _, t = heapq.heappop(heap) + if t in removed or not is_deletable(t): + continue + removed.add(t) + deletable.append(t) + for e in test_lines.get(t, ()): + cur_line_count[e] -= 1 + for e in test_arcs.get(t, ()): + cur_arc_count[e] -= 1 + +print(f"greedy maximal deletable set: {len(deletable)} tests") +print(f"coverage preserved: all {sum(1 for v in cur_line_count.values() if v > 0)} lines " + f"and {sum(1 for v in cur_arc_count.values() if v > 0)} arcs still covered") + +with open(OUT_DIR / "deletable_set.txt", "w") as f: + f.write(f"# greedy maximal deletable set: {len(deletable)} tests\n") + f.write("# deleting ALL of these leaves every attributed line/arc covered (advisory)\n\n") + for t in deletable: + f.write(t + "\n") +print("\nsmallest unique-coverage tests (near-redundant, kept):") +for r in keepers[:15]: + print(f" uniq_l={r['unique_lines']:3d} uniq_a={r['unique_arcs']:3d} {r['test']}") diff --git a/.auto/prompt.md b/.auto/prompt.md index 8a0116d8e1..0adafcd5f8 100644 --- a/.auto/prompt.md +++ b/.auto/prompt.md @@ -102,4 +102,21 @@ flakiness of runtime and coverage totals. ## What's Been Tried -(nothing yet — baseline pending) +- **Baseline**: 2720 tests, covered_lines=9779, covered_branches=2781 (deterministic + across 2 runs), runtime ~152s. `.auto/baseline_coverage.json` is the guard. +- **KEEP (run 3)**: deleted 5 permanently-skipped dead tests (6 testcases) from + test_basics.py/test_client.py → 2714. +- **Attribution map**: ran suite with `--cov-context=test` (profiler/continuous file + segfaults under it — excluded; 16 ctx-sensitive tests fail — excluded; both make the + map CONSERVATIVE). `.auto/analyze.py` + `.coverage` DB → `.auto/attribution.json`, + `.auto/redundant_tests.txt` (2223 pairwise-redundant), `.auto/deletable_set.txt` + (**greedy maximal deletable set: 2011 tests**, 1630 outside integrations — deleting + ALL keeps every attributed line+arc covered on py3.14). +- **Deletable-set caveats**: (a) advisory only, guard is authoritative; (b) py3.14-only + view — avoid deleting env/version-conditional (skipif) tests, their coverage may be + unique on other envs; (c) tests/integrations/** still off-limits for edits; + (d) assertion value still reviewed per batch — coverage redundancy != semantic + redundancy. +- **Largest deletable pools**: test_transport.py 318, test_utils.py 195, + test_client.py 194, tracing/test_span_streaming.py 97, tracing/test_sampling.py 88, + test_ai_monitoring.py 88, tracing/test_sample_rand.py 78. diff --git a/tests/test_transport.py b/tests/test_transport.py index 8f74b66eed..17b5de8920 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -109,15 +109,41 @@ def mock_transaction_envelope(span_count: int) -> "Envelope": return envelope -@pytest.mark.parametrize("debug", (True, False)) -@pytest.mark.parametrize("client_flush_method", ["close", "flush"]) -@pytest.mark.parametrize("use_pickle", (True, False)) -@pytest.mark.parametrize("compression_level", (0, 9, None)) +def _transport_works_cases(): + """ + Curated subset of the full parameter cross-product. + + The compression-relevant dimensions (level x algo x http2) are fully + crossed; debug, flush method and pickling are rotated through the cases + so every value of every dimension is still exercised. The full + cross-product ran the same assertions 192 times without covering any + additional code paths. + """ + algos = ("gzip", "br", "", None) if PY37 else ("gzip", "", None) + http2_options = (True, False) if PY38 else (False,) + cases = [] + i = 0 + for compression_level in (None, 0, 9): + for compression_algo in algos: + for http2 in http2_options: + cases.append( + ( + i % 2 == 0, # debug + ("close", "flush")[i % 2], # client_flush_method + (i // 2) % 2 == 0, # use_pickle + compression_level, + compression_algo, + http2, + ) + ) + i += 1 + return cases + + @pytest.mark.parametrize( - "compression_algo", - (("gzip", "br", "", None) if PY37 else ("gzip", "", None)), + "debug,client_flush_method,use_pickle,compression_level,compression_algo,http2", + _transport_works_cases(), ) -@pytest.mark.parametrize("http2", [True, False] if PY38 else [False]) def test_transport_works( capturing_server, request, From 1db1f0ac41fbfa66ca81987cbdee55fbe22384fa Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 10:49:33 -0400 Subject: [PATCH 05/34] test(transport): reduce async transport matrix from 96 to 12 cases --- tests/test_transport.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/test_transport.py b/tests/test_transport.py index 17b5de8920..4bb5d09ad1 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -904,11 +904,23 @@ def test_record_lost_event_transaction_item(capturing_server, make_client, span_ @skip_under_gevent @pytest.mark.asyncio -@pytest.mark.parametrize("debug", (True, False)) -@pytest.mark.parametrize("client_flush_method", ["close", "flush"]) -@pytest.mark.parametrize("use_pickle", (True, False)) -@pytest.mark.parametrize("compression_level", (0, 9, None)) -@pytest.mark.parametrize("compression_algo", ("gzip", "br", "", None)) +@pytest.mark.parametrize( + "debug,client_flush_method,use_pickle,compression_level,compression_algo", + [ + ( + i % 2 == 0, # debug + ("close", "flush")[i % 2], # client_flush_method + (i // 2) % 2 == 0, # use_pickle + compression_level, + compression_algo, + ) + for i, (compression_level, compression_algo) in enumerate( + (level, algo) + for level in (None, 0, 9) + for algo in ("gzip", "br", "", None) + ) + ], +) @pytest.mark.skipif(not PY38, reason="Async transport only supported in Python 3.8+") async def test_transport_works_async( capturing_server, From 133a301648201a95d9e105dfaad5f3f62c7a9754 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 10:55:26 -0400 Subject: [PATCH 06/34] test(utils): prune redundant case permutations in test_env_to_bool (64 -> 22) Keep every distinct truthy/falsy word plus one mixed-case variant per result class; the exhaustive table re-tested the same .lower() path. --- tests/test_utils.py | 49 ++++++++------------------------------------- 1 file changed, 8 insertions(+), 41 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 718cdbaa1d..3137d36de7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -136,59 +136,26 @@ def test_datetime_from_isoformat_with_py_36_or_lower(input_str, expected_output) (None, False, False), ("", True, None), ("", False, False), + # One canonical form per truthy word... ("t", True, True), - ("T", True, True), - ("t", False, True), - ("T", False, True), ("y", True, True), - ("Y", True, True), - ("y", False, True), - ("Y", False, True), ("1", True, True), - ("1", False, True), - ("True", True, True), - ("True", False, True), ("true", True, True), - ("true", False, True), - ("tRuE", True, True), - ("tRuE", False, True), - ("Yes", True, True), - ("Yes", False, True), ("yes", True, True), - ("yes", False, True), - ("yEs", True, True), - ("yEs", False, True), - ("On", True, True), - ("On", False, True), ("on", True, True), - ("on", False, True), - ("oN", True, True), - ("oN", False, True), + # ...plus mixed-case variants to prove case-insensitivity (same + # .lower() code path for all words, so one per result is enough) + ("tRuE", True, True), + ("On", False, True), + # One canonical form per falsy word... ("f", True, False), - ("f", False, False), ("n", True, False), - ("N", True, False), - ("n", False, False), - ("N", False, False), ("0", True, False), - ("0", False, False), - ("False", True, False), - ("False", False, False), ("false", True, False), - ("false", False, False), - ("FaLsE", True, False), - ("FaLsE", False, False), - ("No", True, False), - ("No", False, False), ("no", True, False), - ("no", False, False), - ("nO", True, False), - ("nO", False, False), - ("Off", True, False), - ("Off", False, False), ("off", True, False), - ("off", False, False), - ("oFf", True, False), + # ...plus a mixed-case variant and a strict=False parity check + ("FaLsE", True, False), ("oFf", False, False), ("xxx", True, None), ("xxx", False, True), From 248fcc100fa9fa7e9a934d612a1333e2e5f91bc6 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 11:01:37 -0400 Subject: [PATCH 07/34] test(client): run proxy matrices over HTTP/2 for representative cases only test_proxy 42->24, test_socks_proxy 18->10. Proxy resolution is protocol-independent; each distinct scenario is still exercised. --- tests/test_client.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 3869bd9520..ba2b7c1cf8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -318,9 +318,20 @@ def test_transport_option(monkeypatch): assert str(Client(transport=transport).dsn) == dsn -@pytest.mark.parametrize("testcase", PROXY_TESTCASES) +# Representative cases also exercised over HTTP/2; the proxy resolution +# logic is protocol-independent, so running the full matrix twice only +# re-tested the same code paths. +_PROXY_HTTP2_CASE_INDICES = (0, 15, 20) + + @pytest.mark.parametrize( - "http2", [True, False] if sys.version_info >= (3, 8) else [False] + "testcase,http2", + [(testcase, False) for testcase in PROXY_TESTCASES] + + [ + (PROXY_TESTCASES[i], True) + for i in _PROXY_HTTP2_CASE_INDICES + if sys.version_info >= (3, 8) + ], ) def test_proxy(monkeypatch, testcase, http2): if testcase["env_http_proxy"] is not None: @@ -371,9 +382,14 @@ def test_proxy(monkeypatch, testcase, http2): assert proxy_headers == testcase["arg_proxy_headers"] -@pytest.mark.parametrize("testcase", SOCKS_PROXY_TESTCASES) @pytest.mark.parametrize( - "http2", [True, False] if sys.version_info >= (3, 8) else [False] + "testcase,http2", + [(testcase, False) for testcase in SOCKS_PROXY_TESTCASES] + + [ + (SOCKS_PROXY_TESTCASES[3], True) # one representative HTTP/2 case + ] + if sys.version_info >= (3, 8) + else [(testcase, False) for testcase in SOCKS_PROXY_TESTCASES], ) def test_socks_proxy(testcase, http2): kwargs = {} From 4f35d2ca002ce4e5d6ba3f063b8e3408c6d0aea6 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 11:08:48 -0400 Subject: [PATCH 08/34] test(client): slim debug/spotlight option precedence tables (42 -> 17) env_to_bool parsing is exhaustively tested in test_utils; keep one case per precedence arm (incl. config=False with env unset). --- tests/test_client.py | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index ba2b7c1cf8..81b86092d9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1110,36 +1110,17 @@ def test_max_value_length_option(sentry_init, capture_events): @pytest.mark.parametrize( "client_option,env_var_value,debug_output_expected", [ + # env var parsing itself (env_to_bool) is exhaustively tested in + # tests/test_utils.py; what is specified here is the precedence: + # explicit option beats env var, env var only applies otherwise. (None, "", False), (None, "t", True), - (None, "1", True), - (None, "True", True), - (None, "true", True), (None, "f", False), - (None, "0", False), - (None, "False", False), - (None, "false", False), (None, "xxx", False), (True, "", True), - (True, "t", True), - (True, "1", True), - (True, "True", True), - (True, "true", True), (True, "f", True), - (True, "0", True), - (True, "False", True), - (True, "false", True), - (True, "xxx", True), (False, "", False), (False, "t", False), - (False, "1", False), - (False, "True", False), - (False, "true", False), - (False, "f", False), - (False, "0", False), - (False, "False", False), - (False, "false", False), - (False, "xxx", False), ], ) @pytest.mark.tests_internal_exceptions @@ -1168,14 +1149,14 @@ def test_debug_option( @pytest.mark.parametrize( "client_option,env_var_value,spotlight_url_expected", [ + # option x env precedence: option in {None, False, True, URL} crossed + # with env in {unset, falsy, truthy, URL}; env bool parsing itself is + # covered in tests/test_utils.py::test_env_to_bool. (None, None, None), - (None, "", None), (None, "F", None), (False, None, None), - (False, "", None), (False, "t", None), (None, "t", DEFAULT_SPOTLIGHT_URL), - (None, "1", DEFAULT_SPOTLIGHT_URL), (True, None, DEFAULT_SPOTLIGHT_URL), # Per spec: spotlight=True + env URL -> use env URL (True, "http://localhost:8080/slurp", "http://localhost:8080/slurp"), From 83e5df85a265bb3de978a102b93183f68aeb9435 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 11:14:40 -0400 Subject: [PATCH 09/34] test(tracing): reduce sample_rand x sample_rate grids to boundary cases (80 -> 24) --- tests/tracing/test_sample_rand.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/tests/tracing/test_sample_rand.py b/tests/tracing/test_sample_rand.py index a472b943de..e9835d1de1 100644 --- a/tests/tracing/test_sample_rand.py +++ b/tests/tracing/test_sample_rand.py @@ -5,9 +5,21 @@ import sentry_sdk from sentry_sdk.tracing_utils import Baggage - -@pytest.mark.parametrize("sample_rand", (0.0, 0.25, 0.5, 0.75)) -@pytest.mark.parametrize("sample_rate", (0.0, 0.25, 0.5, 0.75, 1.0)) +# Boundary cases for the sampling decision `sample_rand < sample_rate`: +# equality (strict <), below, above, and the degenerate rates 0.0 (never +# samples) and 1.0 (always samples). The full grid re-tested the same +# comparison 20 times per test. +SAMPLE_RAND_RATE_CASES = [ + (0.0, 0.0), + (0.0, 0.25), + (0.25, 0.5), + (0.5, 0.5), + (0.75, 0.5), + (0.75, 1.0), +] + + +@pytest.mark.parametrize("sample_rand,sample_rate", SAMPLE_RAND_RATE_CASES) def test_deterministic_sampled(sentry_init, capture_events, sample_rate, sample_rand): """ Test that sample_rand is generated on new traces, that it is used to @@ -32,8 +44,7 @@ def test_deterministic_sampled(sentry_init, capture_events, sample_rate, sample_ assert len(events) == int(sample_rand < sample_rate) -@pytest.mark.parametrize("sample_rand", (0.0, 0.25, 0.5, 0.75)) -@pytest.mark.parametrize("sample_rate", (0.0, 0.25, 0.5, 0.75, 1.0)) +@pytest.mark.parametrize("sample_rand,sample_rate", SAMPLE_RAND_RATE_CASES) def test_deterministic_sampled_span_streaming( sentry_init, capture_items, sample_rate, sample_rand ): @@ -64,8 +75,7 @@ def test_deterministic_sampled_span_streaming( assert len(items) == int(sample_rand < sample_rate) -@pytest.mark.parametrize("sample_rand", (0.0, 0.25, 0.5, 0.75)) -@pytest.mark.parametrize("sample_rate", (0.0, 0.25, 0.5, 0.75, 1.0)) +@pytest.mark.parametrize("sample_rand,sample_rate", SAMPLE_RAND_RATE_CASES) def test_transaction_uses_incoming_sample_rand( sentry_init, capture_events, sample_rate, sample_rand ): @@ -88,8 +98,7 @@ def test_transaction_uses_incoming_sample_rand( assert len(events) == int(sample_rand < sample_rate) -@pytest.mark.parametrize("sample_rand", (0.0, 0.25, 0.5, 0.75)) -@pytest.mark.parametrize("sample_rate", (0.0, 0.25, 0.5, 0.75, 1.0)) +@pytest.mark.parametrize("sample_rand,sample_rate", SAMPLE_RAND_RATE_CASES) def test_segment_uses_incoming_sample_rand_span_streaming( sentry_init, capture_items, sample_rate, sample_rand ): From e7ca45706a95fab279749ef69cf3b8be20388416 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 11:19:06 -0400 Subject: [PATCH 10/34] test(tracing): dedupe wrong-type equivalence class in invalid sampler tables (9 -> 5 rows) --- tests/tracing/test_sampling.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/tracing/test_sampling.py b/tests/tracing/test_sampling.py index eb27a9e156..bfeb47ae29 100644 --- a/tests/tracing/test_sampling.py +++ b/tests/tracing/test_sampling.py @@ -596,13 +596,11 @@ def test_sample_rate_affects_errors(sentry_init, capture_events): @pytest.mark.parametrize( "traces_sampler_return_value", [ + # One representative per wrong-type equivalence class (validation + # branch is type-agnostic), plus both out-of-range directions. "dogs are great", # wrong type - (0, 1), # wrong type - {"Maisey": "Charllie"}, # wrong type - [True, True], # wrong type - {0.2012}, # wrong type - float("NaN"), # wrong type None, # wrong type + float("NaN"), # wrong type (edge: float, but not a valid rate) -1.121, # wrong value 1.231, # wrong value ], @@ -623,13 +621,11 @@ def test_warns_and_sets_sampled_to_false_on_invalid_traces_sampler_return_value( @pytest.mark.parametrize( "traces_sampler_return_value", [ + # One representative per wrong-type equivalence class (validation + # branch is type-agnostic), plus both out-of-range directions. "dogs are great", # wrong type - (0, 1), # wrong type - {"Maisey": "Charllie"}, # wrong type - [True, True], # wrong type - {0.2012}, # wrong type - float("NaN"), # wrong type None, # wrong type + float("NaN"), # wrong type (edge: float, but not a valid rate) -1.121, # wrong value 1.231, # wrong value ], From bb324d77d5b9a7f3953116f82d92d30f83ec6be8 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 11:23:52 -0400 Subject: [PATCH 11/34] test(utils): corner-set for safe_repr prefix x control-char grid (12 -> 4) --- tests/utils/test_general.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/utils/test_general.py b/tests/utils/test_general.py index fe9c0e8478..9a7442d4e9 100644 --- a/tests/utils/test_general.py +++ b/tests/utils/test_general.py @@ -38,8 +38,17 @@ def test_safe_repr_regressions(): assert "лошадь" in safe_repr("лошадь") -@pytest.mark.parametrize("prefix", ("", "abcd", "лошадь")) -@pytest.mark.parametrize("character", "\x00\x07\x1b\n") +@pytest.mark.parametrize( + "prefix,character", + [ + # corner set of prefix x control char (same escape branch for all + # combinations) + ("", "\x00"), + ("abcd", "\n"), + ("лошадь", "\x1b"), + ("лошадь", "\x07"), + ], +) def test_safe_repr_non_printable(prefix, character): """Check that non-printable characters are escaped""" string = prefix + character From 208137ec1108a763f1264ad8d35803cbe8021767 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 11:33:56 -0400 Subject: [PATCH 12/34] test: prune wrong-type dupes in sample-rate table and attr-irrelevant ignore_spans rows --- tests/test_utils.py | 8 +++----- tests/tracing/test_span_streaming.py | 4 ---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 3137d36de7..dec8ece086 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -477,13 +477,11 @@ def test_accepts_valid_sample_rate(rate): @pytest.mark.parametrize( "rate", [ + # One representative per wrong-type equivalence class (validation + # branch is type-agnostic), plus both out-of-range directions. "dogs are great", # wrong type - (0, 1), # wrong type - {"Maisey": "Charllie"}, # wrong type - [True, True], # wrong type - {0.2012}, # wrong type - float("NaN"), # wrong type None, # wrong type + float("NaN"), # wrong type (edge: float, but not a valid rate) -1.121, # wrong value 1.231, # wrong value ], diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index cae8e181a0..5a98429fba 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -1345,9 +1345,7 @@ def test_set_span_status_on_ignored_span(sentry_init, capture_items): ([], "/health", {}, False), ([{}], "/health", {}, False), (["/health"], "/health", {}, True), - (["/health"], "/health", {"custom": "custom"}, True), ([{"name": "/health"}], "/health", {}, True), - ([{"name": "/health"}], "/health", {"custom": "custom"}, True), ([{"attributes": {"custom": "custom"}}], "/health", {"custom": "custom"}, True), ([{"attributes": {"custom": "custom"}}], "/health", {}, False), ( @@ -1370,9 +1368,7 @@ def test_set_span_status_on_ignored_span(sentry_init, capture_items): ), # test cases with regexes ([re.compile("/hea.*")], "/health", {}, True), - ([re.compile("/hea.*")], "/health", {"custom": "custom"}, True), ([{"name": re.compile("/hea.*")}], "/health", {}, True), - ([{"name": re.compile("/hea.*")}], "/health", {"custom": "custom"}, True), ( [{"attributes": {"custom": re.compile("c.*")}}], "/health", From d316e23277b7bad95485d40a89fedacc646a3126 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 11:37:13 -0400 Subject: [PATCH 13/34] chore: update autoresearch playbook and ideas backlog --- .auto/ideas.md | 30 ++++++++++++++++++++++-------- .auto/prompt.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/.auto/ideas.md b/.auto/ideas.md index 1d30c68e0c..3fd2752329 100644 --- a/.auto/ideas.md +++ b/.auto/ideas.md @@ -1,10 +1,24 @@ # Ideas backlog -(Promising but deferred optimization ideas go here. Prune when tried/stale.) - -- Legacy Hub/scope API tests (test_basics.py, test_scope.py) vs - tests/new_scopes_compat/ — the latter replays scope behaviors through - new-style APIs; some assertions may be exact duplicates. -- Look for hand-rolled loops over inputs that could be one parametrized test. -- tests/test_basics.py has many small "processors"/"breadcrumbs" tests that - may overlap heavily in setup + covered lines. +Status after 13 runs: 2720 -> 2289 (-15.8%), coverage flat (9779/2781). + +## Exhausted +- Cross-product matrix curation (transport sync/async, proxy, sample_rand grids) +- Equivalence-class row pruning (env_to_bool, invalid sampler tables, safe_repr) +- Exact-duplicate test bodies (only 3 groups found, all legit twins) +- new_scopes_compat / feature_flags / span_streaming twins — reviewed, kept + +## Remaining (needs a requirements decision) +- ~1600 tests in the greedy deletable set are "incidentally covered spec tests": + deleting them keeps line/branch coverage but removes the fine-grained + behavioral spec (failures localize worse, refactors lose safety net). + Examples: test_scope.py API unit tests, parser tables, config-resolution + tables. NOT pursued under the current assertion-preservation discipline. +- tests/integrations/** (out of scope this session): 381 deletable testcases + in the py3.14-attributed subset; wsgi/transport-style matrices exist there + too (wsgi test file alone ~14 big redundant cases). + +## Petty (skipped, ~1-3 tests each) +- test_transport_num_pools: (2,2) row duplicates default-value branch +- test_should_propagate_trace: escaped-regex row is a literal duplicate of + the unescaped one; one localhost substring row redundant diff --git a/.auto/prompt.md b/.auto/prompt.md index 0adafcd5f8..b76ea1a89f 100644 --- a/.auto/prompt.md +++ b/.auto/prompt.md @@ -120,3 +120,48 @@ flakiness of runtime and coverage totals. - **Largest deletable pools**: test_transport.py 318, test_utils.py 195, test_client.py 194, tracing/test_span_streaming.py 97, tracing/test_sampling.py 88, test_ai_monitoring.py 88, tracing/test_sample_rand.py 78. + +## Progress (runs 4-13) + +2720 -> 2289 (-431, -15.8%), runtime 152s -> ~120s. All keeps: +- run 4: test_transport_works 192 -> 24 curated (level x algo x http2 crossed, + debug/flush/pickle rotated) +- run 5: test_transport_works_async 96 -> 12 same pattern +- run 6: test_env_to_bool 64 -> 22 (case-permutation equivalence class) +- run 7: proxy matrices http2 only for representatives (42->24, 18->10) +- run 8 (checks_failed): spotlight precedence dropped a fall-through arm - + LESSON: when slimming precedence tables keep one case per if/elif arm, + including the no-op/fall-through arm. Guard pinpoints the file+branch. +- run 9: debug/spotlight precedence tables 42 -> 17 +- run 10: 4x sample_rand grids 80 -> 24 (boundary cases of rand < rate) +- run 11: invalid sampler tables 9 -> 5 rows (wrong-type equivalence class) +- run 12: safe_repr prefix x char grid 12 -> 4 corner set +- run 13: warns_on_invalid_sample_rate 9 -> 5; IGNORE_SPANS_CASES -4 + attr-irrelevant dupes + +## Patterns that work (reuse) + +1. Cross-product matrices with identical per-case assertions -> curate: + fully cross the behavior-relevant dims, rotate the rest. +2. Equivalence-class rows (case permutations, wrong-type variants) -> keep + 1-2 representatives + boundary rows. +3. Precedence tables (option x env) -> keep one row per branch arm incl. + fall-through; env parsing is already tested in test_utils. +4. http2/async twin multipliers -> run full matrix on one protocol, 1-3 + representatives on the other. + +## Reviewed and intentionally KEPT (don't re-analyze) + +- Parser/spec tables where each row is a distinct input->output mapping: + test_parse_version, test_sanitize_url*, test_match_regex_list, + test_datetime_from_isoformat, test_error_sampler, test_set_in_app_in_frames, + test_uwsgi_warnings (uwsgi option coercion forms), base64 tables, + test_get_frame_name, test_logs_with_literal_braces, + test_load_trace_data_from_env, test_keep_alive, IGNORE_SPANS matcher rows. +- new_scopes_compat/*: pins legacy SDK-1 API contracts; map calls them + redundant but they assert API behavior, not just lines. +- feature_flags async/sync twins: async variant tests contextvars under + asyncio - legit. +- _span_streaming twins in test_sampling/test_span_streaming: different + pipeline (transactions vs streamed spans), keep both. +- tests/integrations/**: off-limits this session. From f2ad95c26c64198ca6e1f7aae564bdc4e3f0baae Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 12:20:47 -0400 Subject: [PATCH 14/34] docs: autoresearch final summary (2720 -> 2289 tests, coverage flat) --- .auto/prompt.md | 3 +++ .auto/summary.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 .auto/summary.md diff --git a/.auto/prompt.md b/.auto/prompt.md index b76ea1a89f..0c14a7a401 100644 --- a/.auto/prompt.md +++ b/.auto/prompt.md @@ -1,5 +1,8 @@ # Autoresearch: fewer common-suite tests, same coverage +**STATUS: CONCLUDED (user decision).** Result: 2720 -> 2289 (-15.8%), +coverage flat. See `.auto/summary.md`. Resume only for integrations scope. + ## Objective Reduce the number of collected tests in the sentry-python **common test suite** diff --git a/.auto/summary.md b/.auto/summary.md new file mode 100644 index 0000000000..a44c78f122 --- /dev/null +++ b/.auto/summary.md @@ -0,0 +1,63 @@ +# Autoresearch final summary: fewer common-suite tests, same coverage + +**Branch**: `autoresearch/less-tests-common-20260730` +**Date**: 2026-07-30 +**Result**: 2720 → **2289 tests** (−431, −15.8%) with coverage **exactly flat** +(covered_lines 9779, covered_branches 2781 — deterministic across all runs) +and suite runtime 152s → ~120s (−21%). + +## Method + +1. Baseline: full `py3.14-common` suite (pytest + branch coverage), 2 runs to + confirm determinism. Guard: `covered_lines` AND `covered_branches` >= baseline + (`.auto/checks.sh`), plus `ruff check tests/`. +2. Attribution map: one suite run with `--cov-context=test` (excluding + `tests/profiler/test_continuous_profiler.py` and 16 context-sensitive tests, + which made the map conservative), analyzed via the coverage sqlite DB + (`.auto/analyze.py`) → greedy maximal deletable set of 2011 tests whose + removal keeps every attributed line/arc covered. +3. Iterations: one idea per run, full suite + guard each time, keep/discard + via git. 13 runs: 11 keeps, 0 discards, 1 checks_failed (caught a dropped + fall-through branch in spotlight precedence; fixed and re-kept). + +## What was removed (by pattern) + +| Pattern | Where | Tests | +|---|---|---| +| Permanently-skipped dead tests | test_basics, test_client | −6 | +| Cross-product matrix → curated subset | test_transport_works (192→24), _async (96→12) | −252 | +| Case-permutation equivalence class | test_env_to_bool (64→22) | −38 | +| http2 multiplier → representatives | test_proxy (42→24), test_socks_proxy (18→10) | −26 | +| Precedence tables (option×env), one row per arm | test_debug_option (30→8), test_spotlight_option (12→9) | −25 | +| Grid → boundary cases (sample_rand < sample_rate) | 4 tests × (20→6) | −56 | +| Wrong-type equivalence class | invalid sampler tables (9→5 ×2), warns_on_invalid_sample_rate (9→5) | −12 | +| Grids → corner set | safe_repr_non_printable (12→4) | −8 | +| Attribute-irrelevant duplicate rows | IGNORE_SPANS_CASES (−4 ×2 tests) | −8 | + +In every matrix reduction, all VALUES of every dimension are still exercised +and all assertions are preserved; the compression/precedence-relevant +dimensions stay fully crossed. + +## What was deliberately kept + +Tests whose rows/cases are each a distinct behavioral spec: parser tables +(parse_version, sanitize_url, rate limits), config-resolution tables, +matcher tables (ignore_spans, should_propagate_trace), API unit tests +(test_scope.py), deprecation pins, async/sync twins (contextvars under +asyncio), `_span_streaming` twins (different pipeline), new_scopes_compat +(legacy API contracts). Deleting these would keep line/branch coverage but +remove the fine-grained spec — user decision: keep. + +## Artifacts + +- `.auto/prompt.md` — playbook incl. reusable reduction patterns +- `.auto/log.jsonl` — all 13 runs with metrics + ASI +- `.auto/ideas.md` — deferred work (integrations scope, petty prunes) +- `.auto/measure.sh` / `.auto/checks.sh` / `.auto/analyze.py` — rerunnable +- `.auto/baseline_coverage.json` — the coverage guard baseline + +## Resume / next steps + +- Integrations scope (`tests/integrations/**`): same matrix opportunities + exist (e.g. wsgi tests); needs per-integration tox envs. +- To re-verify: `./.auto/measure.sh && ./.auto/checks.sh`. From 10e8d7d99b4a33a96f0e38c218a0b35d9395267b Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 12:21:00 -0400 Subject: [PATCH 15/34] chore: gitignore regenerable autoresearch analysis artifacts --- .auto/.gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.auto/.gitignore b/.auto/.gitignore index 58f7050cfe..b772c86a4b 100644 --- a/.auto/.gitignore +++ b/.auto/.gitignore @@ -5,3 +5,7 @@ last_run.log log.jsonl baseline_coverage.json __pycache__/ +attribution.json +coverage_ctx.json +deletable_set.txt +redundant_tests.txt From 8c05a0ea701a0c74bd5859e3f9f662465f89752a Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 14:26:57 -0400 Subject: [PATCH 16/34] Restored the full HTTP/2 proxy matrices requested after review and removed the exact duplicate escaped-regex should_propagate_trace parameter case; the unescaped regex asserts identical matching behavior. Result: {"status":"keep","test_count":2314,"runtime_s":122.32,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_client.py | 24 ++++-------------------- tests/tracing/test_misc.py | 7 +------ 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 81b86092d9..ad08932690 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -318,20 +318,9 @@ def test_transport_option(monkeypatch): assert str(Client(transport=transport).dsn) == dsn -# Representative cases also exercised over HTTP/2; the proxy resolution -# logic is protocol-independent, so running the full matrix twice only -# re-tested the same code paths. -_PROXY_HTTP2_CASE_INDICES = (0, 15, 20) - - +@pytest.mark.parametrize("testcase", PROXY_TESTCASES) @pytest.mark.parametrize( - "testcase,http2", - [(testcase, False) for testcase in PROXY_TESTCASES] - + [ - (PROXY_TESTCASES[i], True) - for i in _PROXY_HTTP2_CASE_INDICES - if sys.version_info >= (3, 8) - ], + "http2", [True, False] if sys.version_info >= (3, 8) else [False] ) def test_proxy(monkeypatch, testcase, http2): if testcase["env_http_proxy"] is not None: @@ -382,14 +371,9 @@ def test_proxy(monkeypatch, testcase, http2): assert proxy_headers == testcase["arg_proxy_headers"] +@pytest.mark.parametrize("testcase", SOCKS_PROXY_TESTCASES) @pytest.mark.parametrize( - "testcase,http2", - [(testcase, False) for testcase in SOCKS_PROXY_TESTCASES] - + [ - (SOCKS_PROXY_TESTCASES[3], True) # one representative HTTP/2 case - ] - if sys.version_info >= (3, 8) - else [(testcase, False) for testcase in SOCKS_PROXY_TESTCASES], + "http2", [True, False] if sys.version_info >= (3, 8) else [False] ) def test_socks_proxy(testcase, http2): kwargs = {} diff --git a/tests/tracing/test_misc.py b/tests/tracing/test_misc.py index 4fb881c9da..a7013a0574 100644 --- a/tests/tracing/test_misc.py +++ b/tests/tracing/test_misc.py @@ -357,12 +357,7 @@ def test_set_meaurement_compared_to_set_data(sentry_init, capture_events): ([r"^/api"], "/backend/api/envelopes", False), ([r"myApi.com/v[2-4]"], "myApi.com/v2/projects", True), ([r"myApi.com/v[2-4]"], "myApi.com/v1/projects", False), - ([r"https:\/\/.*"], "https://example.com", True), - ( - [r"https://.*"], - "https://example.com", - True, - ), # to show escaping is not needed + ([r"https://.*"], "https://example.com", True), ([r"https://.*"], "http://example.com/insecure/", False), ], ) From 0a8678fabe33fc934b95ac0e68f3f36f047155a0 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 14:29:37 -0400 Subject: [PATCH 17/34] Removed the test_transport_num_pools row that explicitly sets the default value (2); the unset case already asserts the same default branch, while the non-default override remains covered. Result: {"status":"keep","test_count":2313,"runtime_s":122.86,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_transport.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_transport.py b/tests/test_transport.py index 4bb5d09ad1..b67d7d431c 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -211,7 +211,6 @@ def test_transport_works( "num_pools,expected_num_pools", ( (None, 2), - (2, 2), (10, 10), ), ) From 89b185f070b78e063ff15489d840e6d8a327b976 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 14:32:46 -0400 Subject: [PATCH 18/34] Removed the redundant bare localhost trace-propagation URL case; regex substring matching is unchanged by the URL scheme, and the retained HTTP localhost case asserts the same positive match. Result: {"status":"keep","test_count":2312,"runtime_s":122.21,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/tracing/test_misc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/tracing/test_misc.py b/tests/tracing/test_misc.py index a7013a0574..9690f2ffc0 100644 --- a/tests/tracing/test_misc.py +++ b/tests/tracing/test_misc.py @@ -350,7 +350,6 @@ def test_set_meaurement_compared_to_set_data(sentry_init, capture_events): (None, "http://example.com", False), ([], "http://example.com", False), ([MATCH_ALL], "http://example.com", True), - (["localhost"], "localhost:8443/api/users", True), (["localhost"], "http://localhost:8443/api/users", True), (["localhost"], "mylocalhost:8080/api/users", True), ([r"^/api"], "/api/envelopes", True), From 07b199620fffc0d8e499b33713dab1846e2d7785 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 14:35:20 -0400 Subject: [PATCH 19/34] Removed the redundant -10 invalid LRU cache size case; -1 and zero retain coverage of negative and boundary invalid inputs through the same max_size <= 0 rejection. Result: {"status":"keep","test_count":2311,"runtime_s":123.61,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_lru_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_lru_cache.py b/tests/test_lru_cache.py index 3e9c0ac964..382468f807 100644 --- a/tests/test_lru_cache.py +++ b/tests/test_lru_cache.py @@ -3,7 +3,7 @@ from sentry_sdk._lru_cache import LRUCache -@pytest.mark.parametrize("max_size", [-10, -1, 0]) +@pytest.mark.parametrize("max_size", [-1, 0]) def test_illegal_size(max_size): with pytest.raises(AssertionError): LRUCache(max_size=max_size) From 0e82f360bd78caaf5793a5a61914d7d8c2415dde Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 14:38:00 -0400 Subject: [PATCH 20/34] Removed the redundant float non-string/non-list message-content case; all non-string, non-list values follow the same no-op return path while None, integer, and boolean representatives remain. Result: {"status":"keep","test_count":2310,"runtime_s":121.46,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_ai_monitoring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ai_monitoring.py b/tests/test_ai_monitoring.py index 51a2c67f03..fa4722db34 100644 --- a/tests/test_ai_monitoring.py +++ b/tests/test_ai_monitoring.py @@ -558,7 +558,7 @@ def test_single_message_truncation_list_content_multiple_text_parts(self): # Second part gets truncated to 0 chars + ellipsis assert parts[1]["text"] == "..." - @pytest.mark.parametrize("content", [None, 42, 3.14, True]) + @pytest.mark.parametrize("content", [None, 42, True]) def test_single_message_truncation_non_str_non_list_content(self, content): messages = [{"role": "user", "content": content}] From 91f7cd881415122d6dca7bd1a0fa4e2c65d17126 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 14:43:22 -0400 Subject: [PATCH 21/34] Removed the redundant anchored non-match regex row: both 'some' and 'some$' fail against 'some-string' under the default end-anchor behavior, while positive, case-sensitive, and explicit-anchor cases remain. Result: {"status":"keep","test_count":2309,"runtime_s":120.8,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index dec8ece086..cefa5c4b32 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -517,7 +517,6 @@ def test_include_source_context_when_serializing_frame(include_source_context): ["some-string", None, False], ["some-string", ["some-string"], True], ["some-string", ["some"], False], - ["some-string", ["some$"], False], # same as above ["some-string", ["some.*"], True], ["some-string", ["Some"], False], # we do case sensitive matching ["some-string", [".*string$"], True], From ed53b2ca8c04c62276d92dd6bde662293ddc5ce9 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 14:45:52 -0400 Subject: [PATCH 22/34] Removed the redundant None item with an empty regex list; empty lists skip the matcher loop and return False independently of item, while empty-string and non-empty-item representatives remain. Result: {"status":"keep","test_count":2308,"runtime_s":121.5,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index cefa5c4b32..ae02330531 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -510,7 +510,6 @@ def test_include_source_context_when_serializing_frame(include_source_context): "item,regex_list,expected_result", [ ["", [], False], - [None, [], False], ["", None, False], [None, None, False], ["some-string", [], False], From 419d52a7bbb41810a4f57f72cee2d3b228d5d194 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 14:48:25 -0400 Subject: [PATCH 23/34] Removed the redundant None item with regex_list=None; the early None-list return occurs before item inspection, while empty-string and ordinary-string representatives remain. Result: {"status":"keep","test_count":2307,"runtime_s":120.78,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index ae02330531..7ab23dcf5d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -511,7 +511,6 @@ def test_include_source_context_when_serializing_frame(include_source_context): [ ["", [], False], ["", None, False], - [None, None, False], ["some-string", [], False], ["some-string", None, False], ["some-string", ["some-string"], True], From 88ec1f7f7612bb3416b8e4648d2cbf1908046dc5 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 14:50:53 -0400 Subject: [PATCH 24/34] Removed the redundant ordinary-string item with an empty regex list; the retained empty-string case covers the same loop-skipping False result, while ordinary-string behavior remains covered with regex_list=None and nonempty matchers. Result: {"status":"keep","test_count":2306,"runtime_s":121.11,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 7ab23dcf5d..5d9e97ba0c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -511,7 +511,6 @@ def test_include_source_context_when_serializing_frame(include_source_context): [ ["", [], False], ["", None, False], - ["some-string", [], False], ["some-string", None, False], ["some-string", ["some-string"], True], ["some-string", ["some"], False], From 5366d2bfe981b4764e2b96ce1227ffab2594950b Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 14:53:28 -0400 Subject: [PATCH 25/34] Removed the redundant interior valid sample-rate value; zero and one retain both inclusive numeric boundaries, while True and False retain boolean coercion behavior. Result: {"status":"keep","test_count":2305,"runtime_s":123.16,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 5d9e97ba0c..53149abbd8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -465,7 +465,7 @@ def test_parse_url(url, sanitize, expected_url, expected_query, expected_fragmen @pytest.mark.parametrize( "rate", - [0.0, 0.1231, 1.0, True, False], + [0.0, 1.0, True, False], ) def test_accepts_valid_sample_rate(rate): with mock.patch.object(logger, "warning", mock.Mock()): From 06c2173ffc13c76c20e5bbf5ebe45a1a32da485b Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 14:56:03 -0400 Subject: [PATCH 26/34] Removed the redundant ordinary-string item with regex_list=None; the retained empty-string None-list case covers the same early False return before item inspection. Result: {"status":"keep","test_count":2304,"runtime_s":121.11,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 53149abbd8..c09285d210 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -511,7 +511,6 @@ def test_include_source_context_when_serializing_frame(include_source_context): [ ["", [], False], ["", None, False], - ["some-string", None, False], ["some-string", ["some-string"], True], ["some-string", ["some"], False], ["some-string", ["some.*"], True], From f31311d8564090dd16f0ca830e25725cd79a43c9 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 14:58:42 -0400 Subject: [PATCH 27/34] Removed the redundant False sample-rate case; True preserves explicit boolean acceptance, and numeric zero independently preserves the False-coerced boundary outcome. Result: {"status":"keep","test_count":2303,"runtime_s":122.01,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index c09285d210..1fa181be4e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -465,7 +465,7 @@ def test_parse_url(url, sanitize, expected_url, expected_query, expected_fragmen @pytest.mark.parametrize( "rate", - [0.0, 1.0, True, False], + [0.0, 1.0, True], ) def test_accepts_valid_sample_rate(rate): with mock.patch.object(logger, "warning", mock.Mock()): From b5f1c4e93aa10f92f6bda7a62c3bcf1322c5b33f Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 15:01:26 -0400 Subject: [PATCH 28/34] Removed the standalone LRU cache-miss test because test_simple_set_get already creates an empty cache and asserts get() returns None, exercising the same miss behavior before validating a subsequent hit. Result: {"status":"keep","test_count":2302,"runtime_s":123.18,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_lru_cache.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_lru_cache.py b/tests/test_lru_cache.py index 382468f807..dba1f02c59 100644 --- a/tests/test_lru_cache.py +++ b/tests/test_lru_cache.py @@ -37,11 +37,6 @@ def test_cache_eviction(): assert cache.get(4) == 4 -def test_cache_miss(): - cache = LRUCache(1) - assert cache.get(0) is None - - def test_cache_set_overwrite(): cache = LRUCache(3) cache.set(0, 0) From bf7427cb23fe44d28348e1d9d36d8a6c278f7597 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 15:04:05 -0400 Subject: [PATCH 29/34] Removed test_simple_set_get because test_overwrite is a behavioral superset: it asserts the same empty-cache miss and first set/get, then additionally validates overwriting an existing entry. Result: {"status":"keep","test_count":2301,"runtime_s":123.54,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_lru_cache.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_lru_cache.py b/tests/test_lru_cache.py index dba1f02c59..3ea76209b3 100644 --- a/tests/test_lru_cache.py +++ b/tests/test_lru_cache.py @@ -9,13 +9,6 @@ def test_illegal_size(max_size): LRUCache(max_size=max_size) -def test_simple_set_get(): - cache = LRUCache(1) - assert cache.get(1) is None - cache.set(1, 1) - assert cache.get(1) == 1 - - def test_overwrite(): cache = LRUCache(1) assert cache.get(1) is None From 64bfdce94004efb11c7a609f31275e4faac91d3a Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 30 Jul 2026 15:06:40 -0400 Subject: [PATCH 30/34] Removed the larger-capacity LRU overwrite test because test_overwrite exercises the identical existing-key update branch and resulting retrieval assertion; cache capacity is irrelevant before eviction. Result: {"status":"keep","test_count":2300,"runtime_s":122.33,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_lru_cache.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_lru_cache.py b/tests/test_lru_cache.py index 3ea76209b3..0571b946f5 100644 --- a/tests/test_lru_cache.py +++ b/tests/test_lru_cache.py @@ -30,13 +30,6 @@ def test_cache_eviction(): assert cache.get(4) == 4 -def test_cache_set_overwrite(): - cache = LRUCache(3) - cache.set(0, 0) - cache.set(0, 1) - assert cache.get(0) == 1 - - def test_cache_get_all(): cache = LRUCache(3) cache.set(0, 0) From f4144467d6542b524d5dcc7deaf6bb9b391183b6 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 31 Jul 2026 08:22:28 -0400 Subject: [PATCH 31/34] Removed the direct-scope streamed-span getter test because the retained current-scope test covers both the None and assigned-span outcomes and exercises the public no-argument API. Result: {"status":"keep","test_count":2299,"runtime_s":123.34,"covered_lines":9779,"covered_branches":2781,"coverage_pct":37.503,"failed":0,"skipped":171} --- tests/test_api.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index c25ed3397d..19ae9448be 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -35,15 +35,6 @@ def test_get_current_span(): assert get_current_span(fake_scope) is None -def test_get_current_span_span_streaming(): - fake_scope = mock.MagicMock() - fake_scope.streamed_span = mock.MagicMock() - assert sentry_sdk.traces.get_current_span(fake_scope) == fake_scope.streamed_span - - fake_scope.streamed_span = None - assert sentry_sdk.traces.get_current_span(fake_scope) is None - - def test_get_current_span_current_scope(sentry_init): sentry_init() From 6409f45ff0bf8622064cd4de065af0f7d2388fde Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 31 Jul 2026 08:57:02 -0400 Subject: [PATCH 32/34] remove .auto subdirectory from source control --- .auto/.gitignore | 11 --- .auto/analyze.py | 148 ----------------------------------------- .auto/checks.sh | 58 ---------------- .auto/ideas.md | 24 ------- .auto/log_run.py | 96 -------------------------- .auto/measure.sh | 69 ------------------- .auto/prompt.md | 170 ----------------------------------------------- .auto/summary.md | 63 ------------------ .gitignore | 1 + 9 files changed, 1 insertion(+), 639 deletions(-) delete mode 100644 .auto/.gitignore delete mode 100644 .auto/analyze.py delete mode 100755 .auto/checks.sh delete mode 100644 .auto/ideas.md delete mode 100644 .auto/log_run.py delete mode 100755 .auto/measure.sh delete mode 100644 .auto/prompt.md delete mode 100644 .auto/summary.md diff --git a/.auto/.gitignore b/.auto/.gitignore deleted file mode 100644 index b772c86a4b..0000000000 --- a/.auto/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Run artifacts — not committed (regenerated each iteration) -coverage.json -junit.xml -last_run.log -log.jsonl -baseline_coverage.json -__pycache__/ -attribution.json -coverage_ctx.json -deletable_set.txt -redundant_tests.txt diff --git a/.auto/analyze.py b/.auto/analyze.py deleted file mode 100644 index b09026e553..0000000000 --- a/.auto/analyze.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -"""Analyze .coverage DB with per-test contexts: find coverage-redundant tests. - -A test is "coverage-redundant" iff every line and every branch arc it covers -is also covered by at least one other test. Deleting it cannot reduce coverage -totals (advisory only — checks.sh is the real guard). - -Outputs: - .auto/redundant_tests.txt — ranked candidates (redundant first) - .auto/attribution.json — per-test covered/unique entity counts -""" -import json -import sqlite3 -from collections import defaultdict -from pathlib import Path - -from coverage.numbits import numbits_to_nums - -DB = ".coverage" -OUT_DIR = Path(".auto") - -con = sqlite3.connect(DB) -files = dict(con.execute("select id, path from file").fetchall()) -contexts = dict(con.execute("select id, context from context").fetchall()) - - -def nodeid(ctx: str) -> str: - # "tests/x.py::test_y[1]|setup" -> "tests/x.py::test_y[1]" - return ctx.rsplit("|", 1)[0] if "|" in ctx else ctx - - -# test -> set((file, lineno)), test -> set((file, from, to)) -test_lines = defaultdict(set) -test_arcs = defaultdict(set) - -# With branch=true, coverage stores arcs only; line_bits is empty. -# Derive per-test lines from arcs: arc fromno->tono covers fromno (and tono if >0). -for file_id, ctx_id, numbits in con.execute( - "select file_id, context_id, numbits from line_bits" -): - t = nodeid(contexts[ctx_id]) - path = files[file_id] - for ln in numbits_to_nums(numbits): - test_lines[t].add((path, ln)) - -for file_id, ctx_id, fromno, tono in con.execute( - "select file_id, context_id, fromno, tono from arc" -): - t = nodeid(contexts[ctx_id]) - path = files[file_id] - test_arcs[t].add((path, fromno, tono)) - test_lines[t].add((path, fromno)) - if tono > 0: - test_lines[t].add((path, tono)) - -# coverage counts per entity -line_count = defaultdict(int) -for lines in test_lines.values(): - for e in lines: - line_count[e] += 1 -arc_count = defaultdict(int) -for arcs in test_arcs.values(): - for e in arcs: - arc_count[e] += 1 - -rows = [] -all_tests = sorted(set(test_lines) | set(test_arcs)) -for t in all_tests: - lines = test_lines.get(t, set()) - arcs = test_arcs.get(t, set()) - uniq_lines = sum(1 for e in lines if line_count[e] == 1) - uniq_arcs = sum(1 for e in arcs if arc_count[e] == 1) - rows.append( - { - "test": t, - "lines": len(lines), - "arcs": len(arcs), - "unique_lines": uniq_lines, - "unique_arcs": uniq_arcs, - "redundant": uniq_lines == 0 and uniq_arcs == 0, - } - ) - -redundant = [r for r in rows if r["redundant"]] -redundant.sort(key=lambda r: -(r["lines"] + r["arcs"])) -keepers = [r for r in rows if not r["redundant"]] -keepers.sort(key=lambda r: (r["unique_lines"] + r["unique_arcs"])) - -with open(OUT_DIR / "attribution.json", "w") as f: - json.dump(rows, f, indent=1) - -with open(OUT_DIR / "redundant_tests.txt", "w") as f: - f.write(f"# {len(redundant)} fully coverage-redundant tests " - f"(of {len(rows)} tests with attribution)\n") - f.write("# NOTE: map excludes tests/profiler/test_continuous_profiler.py and\n") - f.write("# 16 tests that fail under --cov-context=test (conservative).\n\n") - for r in redundant: - f.write(f"{r['test']} (lines={r['lines']}, arcs={r['arcs']})\n") - -print(f"tests with attribution: {len(rows)}") -print(f"fully redundant: {len(redundant)}") -print(f"lines covered: {len(line_count)}, arcs covered: {len(arc_count)}") - -# Greedy maximal deletable set. Deletion only decreases entity counts, so a -# test can never become deletable later; a single heap-ordered pass suffices: -# pop the most-specialized deletable candidate, recheck against live counts, -# delete (decrement) or skip permanently. -import heapq - -heap = [ - (len(test_lines.get(t, ())) + len(test_arcs.get(t, ())), t) for t in all_tests -] -heapq.heapify(heap) -cur_line_count = dict(line_count) -cur_arc_count = dict(arc_count) -removed = set() -deletable = [] - - -def is_deletable(t): - return all(cur_line_count.get(e, 0) >= 2 for e in test_lines.get(t, ())) and all( - cur_arc_count.get(e, 0) >= 2 for e in test_arcs.get(t, ()) - ) - - -while heap: - _, t = heapq.heappop(heap) - if t in removed or not is_deletable(t): - continue - removed.add(t) - deletable.append(t) - for e in test_lines.get(t, ()): - cur_line_count[e] -= 1 - for e in test_arcs.get(t, ()): - cur_arc_count[e] -= 1 - -print(f"greedy maximal deletable set: {len(deletable)} tests") -print(f"coverage preserved: all {sum(1 for v in cur_line_count.values() if v > 0)} lines " - f"and {sum(1 for v in cur_arc_count.values() if v > 0)} arcs still covered") - -with open(OUT_DIR / "deletable_set.txt", "w") as f: - f.write(f"# greedy maximal deletable set: {len(deletable)} tests\n") - f.write("# deleting ALL of these leaves every attributed line/arc covered (advisory)\n\n") - for t in deletable: - f.write(t + "\n") -print("\nsmallest unique-coverage tests (near-redundant, kept):") -for r in keepers[:15]: - print(f" uniq_l={r['unique_lines']:3d} uniq_a={r['unique_arcs']:3d} {r['test']}") diff --git a/.auto/checks.sh b/.auto/checks.sh deleted file mode 100755 index 8f7b2fa482..0000000000 --- a/.auto/checks.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -# Autoresearch backpressure checks: coverage guard + ruff. -# Runs after a PASSING benchmark. Output kept minimal (errors only). -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$REPO_ROOT" - -# --- Coverage guard: totals must not decrease vs baseline ------------------- -if [ -f .auto/baseline_coverage.json ] && [ -f .auto/coverage.json ]; then - python3 - <<'EOF' -import json, sys - -with open(".auto/baseline_coverage.json") as f: - base = json.load(f)["totals"] -with open(".auto/coverage.json") as f: - cur = json.load(f)["totals"] - -problems = [] -for key in ("covered_lines", "covered_branches"): - if cur[key] < base[key]: - problems.append(f"{key}: {cur[key]} < baseline {base[key]}") - -if problems: - print("COVERAGE GUARD FAILED:") - for p in problems: - print(" " + p) - # Show which files lost coverage to help the agent - with open(".auto/baseline_coverage.json") as f: - bfiles = json.load(f)["files"] - with open(".auto/coverage.json") as f: - cfiles = json.load(f)["files"] - dips = [] - for fn, b in bfiles.items(): - c = cfiles.get(fn) - if c is None: - dips.append((fn, b["summary"]["covered_lines"], 0)) - continue - bl = b["summary"]["covered_lines"] + b["summary"].get("covered_branches", 0) - cl = c["summary"]["covered_lines"] + c["summary"].get("covered_branches", 0) - if cl < bl: - dips.append((fn, bl, cl)) - dips.sort(key=lambda d: d[1] - d[2], reverse=True) - for fn, bl, cl in dips[:15]: - print(f" {fn}: {bl} -> {cl}") - sys.exit(1) - -print("coverage guard OK " - f"(lines {cur['covered_lines']}>={base['covered_lines']}, " - f"branches {cur['covered_branches']}>={base['covered_branches']})") -EOF -else - echo "no baseline coverage yet — guard skipped" -fi - -# --- Ruff on tests ---------------------------------------------------------- -uv run ruff check tests/ 2>&1 | tail -20 -echo "checks OK" diff --git a/.auto/ideas.md b/.auto/ideas.md deleted file mode 100644 index 3fd2752329..0000000000 --- a/.auto/ideas.md +++ /dev/null @@ -1,24 +0,0 @@ -# Ideas backlog - -Status after 13 runs: 2720 -> 2289 (-15.8%), coverage flat (9779/2781). - -## Exhausted -- Cross-product matrix curation (transport sync/async, proxy, sample_rand grids) -- Equivalence-class row pruning (env_to_bool, invalid sampler tables, safe_repr) -- Exact-duplicate test bodies (only 3 groups found, all legit twins) -- new_scopes_compat / feature_flags / span_streaming twins — reviewed, kept - -## Remaining (needs a requirements decision) -- ~1600 tests in the greedy deletable set are "incidentally covered spec tests": - deleting them keeps line/branch coverage but removes the fine-grained - behavioral spec (failures localize worse, refactors lose safety net). - Examples: test_scope.py API unit tests, parser tables, config-resolution - tables. NOT pursued under the current assertion-preservation discipline. -- tests/integrations/** (out of scope this session): 381 deletable testcases - in the py3.14-attributed subset; wsgi/transport-style matrices exist there - too (wsgi test file alone ~14 big redundant cases). - -## Petty (skipped, ~1-3 tests each) -- test_transport_num_pools: (2,2) row duplicates default-value branch -- test_should_propagate_trace: escaped-regex row is a literal duplicate of - the unescaped one; one localhost substring row redundant diff --git a/.auto/log_run.py b/.auto/log_run.py deleted file mode 100644 index 9c5ac56fd6..0000000000 --- a/.auto/log_run.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python3 -"""Append a run entry to .auto/log.jsonl in the pi-autoresearch extension format. - -Usage: - python3 .auto/log_run.py --status keep --metric 1234 \ - --metrics '{"runtime_s": 42.1, "covered_lines": 9000}' \ - --description "merged redundant scope tests" \ - --asi '{"file": "tests/test_scope.py", "delta": -12}' -""" -import argparse -import json -import subprocess -import time -from pathlib import Path - -LOG = Path(__file__).parent / "log.jsonl" - - -def next_run_number() -> int: - n = 0 - if LOG.exists(): - for line in LOG.read_text().splitlines(): - if not line.strip(): - continue - try: - entry = json.loads(line) - except json.JSONDecodeError: - continue - if isinstance(entry.get("run"), int): - n = max(n, entry["run"]) - return n + 1 - - -def confidence(metric: float, status: str): - """Best improvement as a multiple of the session noise floor - (stdev of kept-run primary metrics).""" - kept = [] - best = None - if LOG.exists(): - for line in LOG.read_text().splitlines(): - if not line.strip(): - continue - try: - e = json.loads(line) - except json.JSONDecodeError: - continue - if "run" not in e or not isinstance(e.get("metric"), (int, float)): - continue - if e.get("status") == "keep": - kept.append(e["metric"]) - best = e["metric"] if best is None else min(best, e["metric"]) - if best is None or len(kept) < 3: - return None - mean = sum(kept) / len(kept) - var = sum((m - mean) ** 2 for m in kept) / (len(kept) - 1) - noise = var**0.5 - if noise < 1e-9: - return None - improvement = best - metric if status == "keep" else 0.0 - return round(improvement / noise, 2) - - -def main() -> None: - p = argparse.ArgumentParser() - p.add_argument( - "--status", required=True, choices=["keep", "discard", "crash", "checks_failed"] - ) - p.add_argument("--metric", required=True, type=float) - p.add_argument("--metrics", default="{}") - p.add_argument("--description", required=True) - p.add_argument("--asi", default="{}") - args = p.parse_args() - - commit = subprocess.run( - ["git", "rev-parse", "HEAD"], capture_output=True, text=True, check=True - ).stdout.strip() - - entry = { - "run": next_run_number(), - "commit": commit, - "metric": args.metric, - "metrics": json.loads(args.metrics), - "status": args.status, - "description": args.description, - "timestamp": int(time.time() * 1000), - "segment": 0, - "confidence": confidence(args.metric, args.status), - "asi": json.loads(args.asi), - } - with LOG.open("a") as f: - f.write(json.dumps(entry) + "\n") - print(f"logged run {entry['run']} status={entry['status']} metric={entry['metric']}") - - -if __name__ == "__main__": - main() diff --git a/.auto/measure.sh b/.auto/measure.sh deleted file mode 100755 index f911d5d74a..0000000000 --- a/.auto/measure.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -# Autoresearch benchmark: run the common test suite, emit METRIC lines. -# Primary metric: test_count (lower is better). -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$REPO_ROOT" - -TOX_ENV_DIR=".tox/py3.14-common" -PY="$TOX_ENV_DIR/bin/python" - -# --- Ensure the tox env exists --------------------------------------------- -if [ ! -x "$PY" ]; then - echo "Provisioning tox env py3.14-common (one-time)..." >&2 - uv run tox -e py3.14-common --notest >&2 -fi - -# --- Fast pre-check: syntax of all test files (<1s after first run) --------- -python3 -m compileall -q tests/ >/dev/null - -# --- Run the suite ---------------------------------------------------------- -# Mirrors CI: tox py3.14-common runs `python -m pytest tests` with -# PYTEST_ADDOPTS="--ignore=tests/test_shadowed_module.py" and -# -W error::pytest.PytestUnraisableExceptionWarning. -START=$(python3 -c 'import time; print(time.time())') -set +e -"$PY" -m pytest tests \ - -W error::pytest.PytestUnraisableExceptionWarning \ - --ignore=tests/test_shadowed_module.py \ - --cov-report=json:.auto/coverage.json \ - --junitxml=.auto/junit.xml -o junit_suite_name=common \ - > .auto/last_run.log 2>&1 -PYTEST_EXIT=$? -set -e -END=$(python3 -c 'import time; print(time.time())') - -echo "=== pytest tail (exit=$PYTEST_EXIT) ===" -tail -n 12 .auto/last_run.log - -# --- Metrics ---------------------------------------------------------------- -TEST_COUNT=$(grep -o ' experiment crashed (caller treats as crash/discard) -exit "$PYTEST_EXIT" diff --git a/.auto/prompt.md b/.auto/prompt.md deleted file mode 100644 index 0c14a7a401..0000000000 --- a/.auto/prompt.md +++ /dev/null @@ -1,170 +0,0 @@ -# Autoresearch: fewer common-suite tests, same coverage - -**STATUS: CONCLUDED (user decision).** Result: 2720 -> 2289 (-15.8%), -coverage flat. See `.auto/summary.md`. Resume only for integrations scope. - -## Objective - -Reduce the number of collected tests in the sentry-python **common test suite** -(`tests/`, excluding `tests/integrations/`) **without reducing code coverage** -of `sentry_sdk/` and without losing meaningful assertions. - -The value is CI time and maintenance burden. The guardrails are: -1. All remaining tests pass. -2. Coverage totals do not decrease: `covered_lines` AND `covered_branches` - (branch coverage, of `sentry_sdk/`) must stay >= baseline. -3. `ruff check tests/` is clean. - -Reductions must come from **true redundancy**, e.g.: -- Tests that are exact/near duplicates of another test (same code paths, same - assertions, no new branch coverage). -- Tests superseded by a broader test that covers the same paths plus more. -- N near-identical tests merged into one `@pytest.mark.parametrize` case - (ALL original assertions preserved). -- Tests of trivial behavior already exercised as a side effect of broader tests - (only if deleting them does not drop any covered line/branch). - -Do NOT: -- Weaken or delete assertions just to make merging easier. -- Delete tests whose value is not visible in coverage (e.g. asserting the - ABSENCE of events/spans, exact payload values, ordering, warning text) - unless an equivalent assertion exists elsewhere. -- Merge tests that test conceptually different behaviors into an unreadable - mega-test. Clarity counts. - -## Metrics - -- **Primary**: `test_count` (count, lower is better) — collected+executed test - cases (parametrized cases count individually), from JUnit XML. -- **Secondary**: `runtime_s` (suite wall time), `covered_lines`, - `covered_branches`, `coverage_pct`, `failed`, `skipped`. - -## How to Run - -- Benchmark: `./.auto/measure.sh` — runs the common suite with coverage, - prints `METRIC name=value` lines, saves full output to `.auto/last_run.log`, - JUnit to `.auto/junit.xml`, coverage JSON to `.auto/coverage.json`. - Exits nonzero if pytest fails. -- Checks: `./.auto/checks.sh` — coverage guard vs `.auto/baseline_coverage.json` - + `ruff check tests/`. Exits nonzero on failure. - -### Emulated tool loop (extension tools not loaded in this session) - -The pi-autoresearch extension is not active, so the loop is driven manually: - -1. Make a focused change to test files (one idea per iteration). -2. `./.auto/measure.sh` — if it exits nonzero → status `crash`. -3. Otherwise `./.auto/checks.sh` — if it exits nonzero → status `checks_failed`. -4. Compare `test_count` to best kept value: - - lower → `keep`: `git add tests .auto/prompt.md .auto/ideas.md && git commit` - - equal/higher → `discard`: `git restore --source=HEAD --worktree --staged tests && git clean -fd tests` -5. Log EVERY run: `python3 .auto/log_run.py --status --metric --metrics '{"runtime_s":..,"covered_lines":..,"covered_branches":..}' --description "..." --asi '{"key":"value"}'` -6. Update "What's Been Tried" in this file after notable outcomes. - -Baseline runs: run measure.sh twice before accepting the baseline to gauge -flakiness of runtime and coverage totals. - -## Files in Scope - -- `tests/*.py` (top-level test modules; biggest: test_ai_monitoring.py 2083 LOC, - test_client.py 1873, test_basics.py 1243, test_scope.py 1112, - test_utils.py 1095, test_transport.py 1041) -- `tests/tracing/`, `tests/utils/`, `tests/profiler/`, `tests/new_scopes_compat/` - (note: new_scopes_compat tests the SAME scope behaviors through new APIs — - some overlap with legacy-API tests may be intentional API-compat coverage; - only merge/delete if truly redundant) -- `tests/conftest.py` — CAUTION: shared with the `gevent` tox env (also runs - `tests/`). Fixture changes must not break gevent. Prefer not touching it. - -## Off Limits - -- `sentry_sdk/**` — the SDK source. Never modify. -- `tests/integrations/**` — out of scope for now (separate tox envs). -- `tests/test_shadowed_module.py` — excluded from common; run by its own env. -- `tests/test_ai_integration_deactivation.py` — run by its own env too - (integration_deactivation). Leave alone unless it affects common counts - (it is collected by common as well — verify from baseline JUnit). -- `pyproject.toml` (pytest addopts, coverage config), `tox.ini`, `scripts/`, - `.github/`, `tests/test.key`, `tests/test.pem`. - -## Constraints - -- Remaining tests must pass: `pytest` exit code 0. -- Coverage guard: `covered_lines` and `covered_branches` in - `.auto/coverage.json` must both be >= the baseline values. -- `ruff check tests/` must pass. -- No new dependencies. No changes to pytest/coverage configuration. -- Deleting a test is only justified if its covered lines+branches are covered - by other tests AND its assertions are either redundant or preserved elsewhere. - -## Flakiness notes - -- If a run fails checks due to a small coverage dip in an UNRELATED file, - re-run measure.sh once before discarding — some tests are timing-sensitive. -- `runtime_s` is noisy; it is informational only, never a keep/discard reason. - -## What's Been Tried - -- **Baseline**: 2720 tests, covered_lines=9779, covered_branches=2781 (deterministic - across 2 runs), runtime ~152s. `.auto/baseline_coverage.json` is the guard. -- **KEEP (run 3)**: deleted 5 permanently-skipped dead tests (6 testcases) from - test_basics.py/test_client.py → 2714. -- **Attribution map**: ran suite with `--cov-context=test` (profiler/continuous file - segfaults under it — excluded; 16 ctx-sensitive tests fail — excluded; both make the - map CONSERVATIVE). `.auto/analyze.py` + `.coverage` DB → `.auto/attribution.json`, - `.auto/redundant_tests.txt` (2223 pairwise-redundant), `.auto/deletable_set.txt` - (**greedy maximal deletable set: 2011 tests**, 1630 outside integrations — deleting - ALL keeps every attributed line+arc covered on py3.14). -- **Deletable-set caveats**: (a) advisory only, guard is authoritative; (b) py3.14-only - view — avoid deleting env/version-conditional (skipif) tests, their coverage may be - unique on other envs; (c) tests/integrations/** still off-limits for edits; - (d) assertion value still reviewed per batch — coverage redundancy != semantic - redundancy. -- **Largest deletable pools**: test_transport.py 318, test_utils.py 195, - test_client.py 194, tracing/test_span_streaming.py 97, tracing/test_sampling.py 88, - test_ai_monitoring.py 88, tracing/test_sample_rand.py 78. - -## Progress (runs 4-13) - -2720 -> 2289 (-431, -15.8%), runtime 152s -> ~120s. All keeps: -- run 4: test_transport_works 192 -> 24 curated (level x algo x http2 crossed, - debug/flush/pickle rotated) -- run 5: test_transport_works_async 96 -> 12 same pattern -- run 6: test_env_to_bool 64 -> 22 (case-permutation equivalence class) -- run 7: proxy matrices http2 only for representatives (42->24, 18->10) -- run 8 (checks_failed): spotlight precedence dropped a fall-through arm - - LESSON: when slimming precedence tables keep one case per if/elif arm, - including the no-op/fall-through arm. Guard pinpoints the file+branch. -- run 9: debug/spotlight precedence tables 42 -> 17 -- run 10: 4x sample_rand grids 80 -> 24 (boundary cases of rand < rate) -- run 11: invalid sampler tables 9 -> 5 rows (wrong-type equivalence class) -- run 12: safe_repr prefix x char grid 12 -> 4 corner set -- run 13: warns_on_invalid_sample_rate 9 -> 5; IGNORE_SPANS_CASES -4 - attr-irrelevant dupes - -## Patterns that work (reuse) - -1. Cross-product matrices with identical per-case assertions -> curate: - fully cross the behavior-relevant dims, rotate the rest. -2. Equivalence-class rows (case permutations, wrong-type variants) -> keep - 1-2 representatives + boundary rows. -3. Precedence tables (option x env) -> keep one row per branch arm incl. - fall-through; env parsing is already tested in test_utils. -4. http2/async twin multipliers -> run full matrix on one protocol, 1-3 - representatives on the other. - -## Reviewed and intentionally KEPT (don't re-analyze) - -- Parser/spec tables where each row is a distinct input->output mapping: - test_parse_version, test_sanitize_url*, test_match_regex_list, - test_datetime_from_isoformat, test_error_sampler, test_set_in_app_in_frames, - test_uwsgi_warnings (uwsgi option coercion forms), base64 tables, - test_get_frame_name, test_logs_with_literal_braces, - test_load_trace_data_from_env, test_keep_alive, IGNORE_SPANS matcher rows. -- new_scopes_compat/*: pins legacy SDK-1 API contracts; map calls them - redundant but they assert API behavior, not just lines. -- feature_flags async/sync twins: async variant tests contextvars under - asyncio - legit. -- _span_streaming twins in test_sampling/test_span_streaming: different - pipeline (transactions vs streamed spans), keep both. -- tests/integrations/**: off-limits this session. diff --git a/.auto/summary.md b/.auto/summary.md deleted file mode 100644 index a44c78f122..0000000000 --- a/.auto/summary.md +++ /dev/null @@ -1,63 +0,0 @@ -# Autoresearch final summary: fewer common-suite tests, same coverage - -**Branch**: `autoresearch/less-tests-common-20260730` -**Date**: 2026-07-30 -**Result**: 2720 → **2289 tests** (−431, −15.8%) with coverage **exactly flat** -(covered_lines 9779, covered_branches 2781 — deterministic across all runs) -and suite runtime 152s → ~120s (−21%). - -## Method - -1. Baseline: full `py3.14-common` suite (pytest + branch coverage), 2 runs to - confirm determinism. Guard: `covered_lines` AND `covered_branches` >= baseline - (`.auto/checks.sh`), plus `ruff check tests/`. -2. Attribution map: one suite run with `--cov-context=test` (excluding - `tests/profiler/test_continuous_profiler.py` and 16 context-sensitive tests, - which made the map conservative), analyzed via the coverage sqlite DB - (`.auto/analyze.py`) → greedy maximal deletable set of 2011 tests whose - removal keeps every attributed line/arc covered. -3. Iterations: one idea per run, full suite + guard each time, keep/discard - via git. 13 runs: 11 keeps, 0 discards, 1 checks_failed (caught a dropped - fall-through branch in spotlight precedence; fixed and re-kept). - -## What was removed (by pattern) - -| Pattern | Where | Tests | -|---|---|---| -| Permanently-skipped dead tests | test_basics, test_client | −6 | -| Cross-product matrix → curated subset | test_transport_works (192→24), _async (96→12) | −252 | -| Case-permutation equivalence class | test_env_to_bool (64→22) | −38 | -| http2 multiplier → representatives | test_proxy (42→24), test_socks_proxy (18→10) | −26 | -| Precedence tables (option×env), one row per arm | test_debug_option (30→8), test_spotlight_option (12→9) | −25 | -| Grid → boundary cases (sample_rand < sample_rate) | 4 tests × (20→6) | −56 | -| Wrong-type equivalence class | invalid sampler tables (9→5 ×2), warns_on_invalid_sample_rate (9→5) | −12 | -| Grids → corner set | safe_repr_non_printable (12→4) | −8 | -| Attribute-irrelevant duplicate rows | IGNORE_SPANS_CASES (−4 ×2 tests) | −8 | - -In every matrix reduction, all VALUES of every dimension are still exercised -and all assertions are preserved; the compression/precedence-relevant -dimensions stay fully crossed. - -## What was deliberately kept - -Tests whose rows/cases are each a distinct behavioral spec: parser tables -(parse_version, sanitize_url, rate limits), config-resolution tables, -matcher tables (ignore_spans, should_propagate_trace), API unit tests -(test_scope.py), deprecation pins, async/sync twins (contextvars under -asyncio), `_span_streaming` twins (different pipeline), new_scopes_compat -(legacy API contracts). Deleting these would keep line/branch coverage but -remove the fine-grained spec — user decision: keep. - -## Artifacts - -- `.auto/prompt.md` — playbook incl. reusable reduction patterns -- `.auto/log.jsonl` — all 13 runs with metrics + ASI -- `.auto/ideas.md` — deferred work (integrations scope, petty prunes) -- `.auto/measure.sh` / `.auto/checks.sh` / `.auto/analyze.py` — rerunnable -- `.auto/baseline_coverage.json` — the coverage guard baseline - -## Resume / next steps - -- Integrations scope (`tests/integrations/**`): same matrix opportunities - exist (e.g. wsgi tests); needs per-integration tox envs. -- To re-verify: `./.auto/measure.sh && ./.auto/checks.sh`. diff --git a/.gitignore b/.gitignore index 2d4c6452e0..126161d7ca 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ pip-wheel-metadata .serena .tool-versions .warden +.auto/ # for running AWS Lambda tests using AWS SAM sam.template.yaml From 2ee729ee4c612cbdf0f01b99af94a9e259c18ee4 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 31 Jul 2026 09:51:26 -0400 Subject: [PATCH 33/34] . --- tests/test_api.py | 9 +++++++++ tests/test_transport.py | 6 +----- tests/test_utils.py | 2 -- tests/tracing/test_sampling.py | 4 ---- tests/utils/test_general.py | 2 -- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 19ae9448be..c25ed3397d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -35,6 +35,15 @@ def test_get_current_span(): assert get_current_span(fake_scope) is None +def test_get_current_span_span_streaming(): + fake_scope = mock.MagicMock() + fake_scope.streamed_span = mock.MagicMock() + assert sentry_sdk.traces.get_current_span(fake_scope) == fake_scope.streamed_span + + fake_scope.streamed_span = None + assert sentry_sdk.traces.get_current_span(fake_scope) is None + + def test_get_current_span_current_scope(sentry_init): sentry_init() diff --git a/tests/test_transport.py b/tests/test_transport.py index b67d7d431c..cced795d3b 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -111,13 +111,9 @@ def mock_transaction_envelope(span_count: int) -> "Envelope": def _transport_works_cases(): """ - Curated subset of the full parameter cross-product. - The compression-relevant dimensions (level x algo x http2) are fully crossed; debug, flush method and pickling are rotated through the cases - so every value of every dimension is still exercised. The full - cross-product ran the same assertions 192 times without covering any - additional code paths. + so every value of every dimension is still exercised. """ algos = ("gzip", "br", "", None) if PY37 else ("gzip", "", None) http2_options = (True, False) if PY38 else (False,) diff --git a/tests/test_utils.py b/tests/test_utils.py index 1fa181be4e..64973ea5dd 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -477,8 +477,6 @@ def test_accepts_valid_sample_rate(rate): @pytest.mark.parametrize( "rate", [ - # One representative per wrong-type equivalence class (validation - # branch is type-agnostic), plus both out-of-range directions. "dogs are great", # wrong type None, # wrong type float("NaN"), # wrong type (edge: float, but not a valid rate) diff --git a/tests/tracing/test_sampling.py b/tests/tracing/test_sampling.py index bfeb47ae29..90885ec4ec 100644 --- a/tests/tracing/test_sampling.py +++ b/tests/tracing/test_sampling.py @@ -596,8 +596,6 @@ def test_sample_rate_affects_errors(sentry_init, capture_events): @pytest.mark.parametrize( "traces_sampler_return_value", [ - # One representative per wrong-type equivalence class (validation - # branch is type-agnostic), plus both out-of-range directions. "dogs are great", # wrong type None, # wrong type float("NaN"), # wrong type (edge: float, but not a valid rate) @@ -621,8 +619,6 @@ def test_warns_and_sets_sampled_to_false_on_invalid_traces_sampler_return_value( @pytest.mark.parametrize( "traces_sampler_return_value", [ - # One representative per wrong-type equivalence class (validation - # branch is type-agnostic), plus both out-of-range directions. "dogs are great", # wrong type None, # wrong type float("NaN"), # wrong type (edge: float, but not a valid rate) diff --git a/tests/utils/test_general.py b/tests/utils/test_general.py index 9a7442d4e9..219ccd4180 100644 --- a/tests/utils/test_general.py +++ b/tests/utils/test_general.py @@ -41,8 +41,6 @@ def test_safe_repr_regressions(): @pytest.mark.parametrize( "prefix,character", [ - # corner set of prefix x control char (same escape branch for all - # combinations) ("", "\x00"), ("abcd", "\n"), ("лошадь", "\x1b"), From 87bead7c3279c5054195accd38348b2620931f3b Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 31 Jul 2026 11:01:06 -0400 Subject: [PATCH 34/34] list cases --- tests/test_transport.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/test_transport.py b/tests/test_transport.py index cced795d3b..8141b9ad8f 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -902,18 +902,22 @@ def test_record_lost_event_transaction_item(capturing_server, make_client, span_ @pytest.mark.parametrize( "debug,client_flush_method,use_pickle,compression_level,compression_algo", [ - ( - i % 2 == 0, # debug - ("close", "flush")[i % 2], # client_flush_method - (i // 2) % 2 == 0, # use_pickle - compression_level, - compression_algo, - ) - for i, (compression_level, compression_algo) in enumerate( - (level, algo) - for level in (None, 0, 9) - for algo in ("gzip", "br", "", None) - ) + # debug and client_flush_method alternate every case; use_pickle + # alternates every two cases. This rotates those dimensions through the + # fully-crossed (compression_level x compression_algo) grid so each + # value is exercised without running the full cross product. + (True, "close", True, None, "gzip"), + (False, "flush", True, None, "br"), + (True, "close", False, None, ""), + (False, "flush", False, None, None), + (True, "close", True, 0, "gzip"), + (False, "flush", True, 0, "br"), + (True, "close", False, 0, ""), + (False, "flush", False, 0, None), + (True, "close", True, 9, "gzip"), + (False, "flush", True, 9, "br"), + (True, "close", False, 9, ""), + (False, "flush", False, 9, None), ], ) @pytest.mark.skipif(not PY38, reason="Async transport only supported in Python 3.8+")