Files
zeekay 9a90943b8f merge: ClickHouse 26.7.1.1315-stable into datastore — preserve debrand
13,838 upstream commits, forward from the 26.6.1.1 fork point. 129 conflicts:
37 resolved mechanically (take upstream's file, re-apply the brand sweep) and
92 by hand where our fork and upstream had both changed the same code.

The rule throughout: upstream owns the engine, we own the naming and a small set
of deliberate deviations. Never resolve by discarding an upstream change — a
dropped rename or a missed new parameter becomes a compile error a thousand
files away.

Deviations verified intact after the merge:
  ENABLE_XRAY still defaults OFF        (CMakeLists.txt:105)
  X-Datastore-User read by the server   (authenticateUserByHTTP.cpp)
  X-ClickHouse-* in src/                0
  hanzo/research                        present
  system.*_log retention bound          present
  ArrowFlight rip                       held against upstream re-adding it

Two things worth naming, because neither shows up as a conflict:

  src/CMakeLists.txt gained an UNGUARDED
  `add_object_library(datastore_storages_arrowflight Storages/ArrowFlight)` from
  upstream, for a directory our gRPC rip deleted. It sat outside every conflict
  hunk, so a resolver reading only the markers would sail straight past it into
  a CMake configure error. Removed; the Server/ArrowFlight sibling at line 474
  stays because it is inside `if (TARGET ch_contrib::arrow_flight)`.

  The brand sweep had rewritten github.com/ClickHouse/ClickHouse issue links to
  github.com/Datastore/Datastore — a dead host. Those point at upstream's own
  tracker and are restored, alongside the contributors list, sql.clickhouse.com
  and the ClickHouse Inc copyright: attribution is theirs, not ours to rename.

base/glibc-compatibility/memcpy/memcpy.h is deleted, not kept: upstream dropped
it for LLVM-libc's, and our only change to it was a comment rebrand.
2026-07-26 14:47:43 -07:00

2259 lines
115 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import argparse
import ast
import os
import re
import sys
from pathlib import Path
sys.path.append("./")
from ci.praktika.cidb import CIDB
from ci.praktika.info import Info
from ci.praktika.result import Result
from ci.praktika.settings import Settings
from ci.praktika.utils import Shell
# Query to fetch failed tests from CIDB for a given PR.
# Pre-filters out commit/check_name combinations with >= 20 failures — these indicate
# widespread failures (e.g. build broken, environment issue) where every test failed,
# not genuine per-test flakiness. Results are ordered by recency-weighted failure
# count (exponential decay, 7-day half-life). Capped at 100.
FAILED_TESTS_QUERY = """ \
select test_name
from checks
where pull_request_number = {PR_NUMBER}
and check_name LIKE '{JOB_TYPE}%'
and check_status = 'failure'
and match(test_name, '{TEST_NAME_PATTERN}')
and test_status = 'FAIL'
and check_start_time >= now() - interval 30 day
and (commit_sha, check_name) in (
select commit_sha, check_name
from checks
where pull_request_number = {PR_NUMBER}
and check_name LIKE '{JOB_TYPE}%'
and check_status = 'failure'
and test_status = 'FAIL'
and check_start_time >= now() - interval 30 day
group by commit_sha, check_name
having count(test_name) < 20
)
group by test_name
order by count() * exp(-dateDiff('day', max(check_start_time), now()) / 7.) desc
limit 100 \
"""
class Targeting:
INTEGRATION_JOB_TYPE = "Integration"
STATELESS_JOB_TYPE = "Stateless"
def __init__(self, info: Info):
self.info = info
self._cidb = None
if "stateless" in info.job_name.lower():
self.job_type = self.STATELESS_JOB_TYPE
elif "integration" in info.job_name.lower():
self.job_type = self.INTEGRATION_JOB_TYPE
else:
self.job_type = None
def _ci_db(self):
# Queries run as the privileged CI user. The public `play` user is
# rate/row/time-limited and must be used only for links handed to humans
# (see CIDB.get_link_to_test_case_statistics).
if self._cidb is None:
url, user, passwd = (
self.info.get_secret(Settings.SECRET_CI_DB_URL)
.join_with(self.info.get_secret(Settings.SECRET_CI_DB_USER))
.join_with(self.info.get_secret(Settings.SECRET_CI_DB_PASSWORD))
.get_value()
)
self._cidb = CIDB(url=url, user=user, passwd=passwd)
return self._cidb
# Keep in sync with TEST_FILE_EXTENSIONS in tests/datastore-test.
_TEST_FILE_EXTENSIONS = (".sql.j2", ".sql", ".sh", ".py", ".expect")
@classmethod
def _derive_test_name(cls, fpath: str):
"""Map a changed file under `tests/queries/0_stateless/` to a test name.
Returns the test base name (without extension) suitable for `datastore-test --test`,
or `None` if the file does not correspond to a real test (e.g. a data file like
`02995_settings_26_4_1.tsv`, which is consumed by `02995_new_settings_history.sh`
but has no test of its own).
"""
fname = os.path.basename(fpath)
# Direct hit: the changed file is itself a test source file.
for ext in cls._TEST_FILE_EXTENSIONS:
if fname.endswith(ext):
return fname[: -len(ext)]
# Supporting file (`.reference`, `.reference.j2`, `.tsv`, ...). Walk the
# extensions one at a time looking for a sibling test source file with
# the same base name. This catches reference updates like
# `00172_hits_joins.reference.j2` → `00172_hits_joins.sql.j2` while
# rejecting orphan data files like `02995_settings_26_4_1.tsv`.
test_dir = Path("tests/queries/0_stateless")
candidate = fname
while "." in candidate:
candidate = candidate.rsplit(".", 1)[0]
for ext in cls._TEST_FILE_EXTENSIONS:
if (test_dir / f"{candidate}{ext}").is_file():
return candidate
return None
@classmethod
def _tests_owning_data_file(cls, fpath: str):
"""Map an auxiliary stateless file back to the base names of the tests
that own it.
A data fixture may sit next to its test (`02995_settings_26_4_1.tsv`) or
in a subdirectory (`data_parquet/02716_data.parquet`); either way it has
no test of its own, so `_derive_test_name` skips it. But such a fixture
change still alters the surface of the test that consumes it, and a drift
guard that skips it would false-green the very drift it exists to catch.
Fixtures carry their owning test's five-digit prefix by convention, so
the prefix narrows the candidates to the `NNNNN_*` tests at the suite
root (a handful of files, never the whole suite). Among those, prefer the
ones whose body references the fixture's literal filename. When none
does, retry with the extensionless stem, since format schemas are
conventionally referenced without the extension
(`format_schema = 'NNNNN_foo:Message'` for `format_schemas/NNNNN_foo.proto`).
The stem is strictly a fallback: a short or cross-extension stem
(`03250.proto` → `03250`, or `03036_archive1.tar` next to a test that
reads `03036_archive1.zip`) would otherwise pull unrelated prefix
siblings into mappings the literal filename already resolves precisely.
Fall back to all prefix siblings when neither matches — the reference is
then constructed dynamically and cannot be found textually. Return an
empty list when the file has no numeric prefix or no test with that
prefix exists — there is then genuinely nothing to rerun, and emitting a
pattern that matches no test would make `datastore-test` exit 1 (the
failure mode `PR #104097` fixed).
"""
match = re.match(r"(\d{5})", os.path.basename(fpath))
if match is None:
return []
prefix = match.group(1)
test_dir = Path("tests/queries/0_stateless")
candidates = {}
for ext in cls._TEST_FILE_EXTENSIONS:
for test_file in test_dir.glob(f"{prefix}_*{ext}"):
candidates[test_file.name[: -len(ext)]] = test_file
if not candidates:
return []
fname = os.path.basename(fpath)
stem = os.path.splitext(fname)[0]
by_fname = []
by_stem = []
for base_name, test_file in candidates.items():
try:
with test_file.open("r", encoding="utf-8", errors="ignore") as f:
body = f.read()
except OSError:
continue
if fname in body:
by_fname.append(base_name)
elif stem in body:
by_stem.append(base_name)
referencing = by_fname or by_stem
return sorted(referencing) if referencing else sorted(candidates)
@staticmethod
def is_functional_test_file(fpath: str) -> bool:
"""A changed path that is itself a stateless functional test source file."""
fpath = fpath.removeprefix("./")
return fpath.startswith("tests/queries/0_stateless/") and Path(fpath).is_file()
@staticmethod
def is_integration_test_file(fpath: str) -> bool:
"""A changed path that is itself an integration test module."""
fpath = fpath.removeprefix("./")
return (
fpath.startswith("tests/integration/test_")
and not fpath.startswith("tests/integration/test_e2e_")
and Path(fpath).name.startswith("test")
and fpath.endswith(".py")
and Path(fpath).is_file()
)
@staticmethod
def is_ci_job_script(fpath: str) -> bool:
"""A changed path under the CI job scripts themselves.
Tolerated alongside test-file changes by the batch-skip check in
`functional_tests.py` / `integration_test_job.py`: such a change is
exercised identically by every batch, so a batch that does not
contain the changed test file is still a valid check of the
(possibly changed) job script.
"""
fpath = fpath.removeprefix("./")
return fpath.startswith("ci/jobs/") and Path(fpath).is_file()
@classmethod
def functional_test_hash_batch_file(cls, fpath: str):
"""Return the on-disk stateless test filename (with extension) that
`datastore-test --run-by-hash-*` uses to bucket the given changed path,
or `None` if it cannot be resolved to a concrete test source file.
`datastore-test`'s `is_test_from_dir`/`get_selected_tests` only look
at files directly inside the suite root (`os.listdir`, not
recursive), so a file nested in a subdirectory - e.g.
`tests/queries/0_stateless/helpers/httpclient.py` or
`tests/queries/0_stateless/data_avro/generate_avro.sh` - is never a
test case there, no matter its extension. Hashing such a nested
file's basename would fabricate a bucket assignment that does not
correspond to how `--run-by-hash-*` actually splits the suite, so
return `None` (the caller then conservatively runs the batch) unless
the file's parent directory is exactly the suite root.
"""
test_dir = Path("tests/queries/0_stateless")
path = Path(fpath.removeprefix("./"))
if path.parent != test_dir:
return None
fname = path.name
for ext in cls._TEST_FILE_EXTENSIONS:
if fname.endswith(ext):
return fname
base_name = cls._derive_test_name(fpath)
if base_name is None:
return None
for ext in cls._TEST_FILE_EXTENSIONS:
if (test_dir / f"{base_name}{ext}").is_file():
return f"{base_name}{ext}"
return None
@staticmethod
def is_sequential_functional_test(test_source_file: str) -> bool:
"""True if the on-disk stateless test file (e.g. `00001_x.sql`, as
returned by `functional_test_hash_batch_file`) is tagged `no-parallel`
or `sequential`.
Mirrors `datastore-test`'s own `is_sequential_test`/tag-parsing logic,
so the batch-skip check in `functional_tests.py` can tell whether a
changed test would even be selected by a job invoked with the
`parallel`/`sequential` runner option (`--no-sequential`/`--no-parallel`),
which splits the suite into two independently hash-batched job flavors.
"""
if test_source_file.endswith(".sql") or test_source_file.endswith(".sql.j2"):
comment_sign = "--"
elif test_source_file.endswith((".sh", ".py", ".expect")):
comment_sign = "#"
else:
return False
path = Path("tests/queries/0_stateless") / test_source_file
try:
with path.open("r", encoding="utf-8", errors="ignore") as f:
for line in f:
line = line.rstrip("\n")
if not line.startswith(comment_sign):
continue
rest = line[len(comment_sign):].lstrip()
if not rest.startswith("Tags:"):
continue
tags = {t.strip() for t in rest[len("Tags:"):].split(",")}
return "no-parallel" in tags or "sequential" in tags
except OSError:
return False
return False
def get_changed_tests(self):
# TODO: add support for integration tests
result = set()
if hasattr(self, '_diff_text') and self._diff_text:
# Reuse already-fetched diff text to extract changed file names — avoids
# a second diff fetch and works when the diff was pre-fetched via --diff-file.
changed_files = [
m.group(1)
for m in re.finditer(r'^\+\+\+ b/(.+)$', self._diff_text, re.MULTILINE)
]
elif self.info.is_local_run:
changed_files = Shell.get_output(
f"gh pr diff {self.info.pr_number} --repo Datastore/Datastore --name-only"
).splitlines()
else:
changed_files = self.info.get_changed_files()
if not changed_files:
return result
for fpath in changed_files:
if not fpath.startswith("tests/queries/0_stateless/"):
if fpath.startswith("tests/queries/"):
# Log any other changed file under tests/queries for future debugging
print(
f"File '{fpath}' changed, but doesn't match expected test pattern"
)
continue
if not Path(fpath).exists():
print(f"File '{fpath}' was removed — skipping")
continue
# A file directly at the suite root may itself be a test source, or a
# supporting file (`.reference`, a `.sql.j2` template, ...) whose sibling
# test shares its base name.
if Path(fpath).parent == Path("tests/queries/0_stateless"):
test_base_name = self._derive_test_name(fpath)
if test_base_name is not None:
print(f"Detected changed test: '{test_base_name}' (from '{fpath}')")
# Add '.' suffix to precisely match this test only
result.add(f"{test_base_name}.")
continue
# Either a data fixture nested in a subdirectory
# (`data_parquet/02716_data.parquet`) or a root-level orphan data file
# (`02995_settings_26_4_1.tsv`) with no sibling test. Map it back to the
# test(s) that own it so a fixture-only change still reruns the tests
# that consume it; otherwise the flaky check — and the merge-queue drift
# guard built on it — would silently skip the very test surface the
# change affects. Emit only real tests: `_tests_owning_data_file` returns
# an empty list when nothing maps, so we never fabricate a no-match
# pattern (which would make datastore-test exit 1).
owning_tests = self._tests_owning_data_file(fpath)
if owning_tests:
for base_name in owning_tests:
print(
f"Detected changed data file '{fpath}' owned by test '{base_name}'"
)
# Add '.' suffix to precisely match this test only
result.add(f"{base_name}.")
else:
print(
f"File '{fpath}' is not a test source and has no owning test — skipping"
)
return sorted(result)
def get_previously_failed_tests(self):
assert self.job_type, "Unsupported job type"
assert (
self.info.pr_number > 0
), "Find tests by previous failures applicable only for PRs"
tests = []
cidb = self._ci_db()
if self.job_type == self.INTEGRATION_JOB_TYPE:
test_name_pattern = "^test_"
elif self.job_type == self.STATELESS_JOB_TYPE:
test_name_pattern = "^[0-9]{5}_"
else:
assert False, f"Not supported job type [{self.job_type}]"
query = FAILED_TESTS_QUERY.format(
PR_NUMBER=self.info.pr_number,
JOB_TYPE=self.job_type,
TEST_NAME_PATTERN=test_name_pattern,
)
query_result = cidb.query(query, log_level="") or ""
# Parse test names from the query result
for line in query_result.strip().split("\n"):
if line.strip():
# Split by whitespace and get the first column (test_name)
parts = line.split()
if parts:
test_name = parts[0]
tests.append(test_name)
print(f"Parsed {len(tests)} test names: {tests}")
tests = list(set(tests))
return sorted(tests)
@staticmethod
def _escape_sql_string(s: str) -> str:
return s.replace("\\", "\\\\").replace("'", "\\'")
@staticmethod
def _stored_path(path: str) -> str:
"""Convert a repo-relative diff path to the stored coverage path format.
Coverage data is built with -ffile-prefix-map=/Datastore=. so all
source paths are stored as ./src/... in checks_coverage_lines.
Strip any leading ./ from the diff path then re-add the ./ prefix.
"""
p = path.replace("\\", "/").lstrip("./")
return "./" + p
# Regions wider than this are considered "broad" (low signal).
NARROW_REGION_MAX_LINES = 40
# Regions covered by more tests than this are skipped from Pass 1 (direct line
# coverage). Infrastructure files like SignalHandlers.cpp, Context.cpp, and
# Settings.cpp are touched by almost every test — a changed line in such a file
# has no diagnostic value for test selection. Skipping regions with too many
# owners prevents these files from flooding primary_tests (which would then
# cause sibling/indirect queries to fail with HTTP form-field-too-long errors
# and inflate unique_tests to nearly the full suite).
MAX_TESTS_PER_LINE = 150
# Synthetic width for tests found via indirect-call (virtual dispatch) co-occurrence.
# Lower than SIBLING_DIR_WIDTH (10000) because callee co-occurrence is a stronger
# signal than directory proximity: tests that call the same vtable slots as primary
# tests are exercising the same interface, not merely a related file.
INDIRECT_CALL_WIDTH = 2000
# Synthetic width assigned to tests found via same-directory sibling file
# expansion (secondary pass). Must be >> NARROW_REGION_MAX_LINES so they
# never get the narrow-tier bonus and always rank below direct hits.
SIBLING_DIR_WIDTH = 3000
# Per-pass score multipliers applied on top of the 1/(width×rc) signal.
# Pass 1 (direct line coverage) is the baseline. Passes 2 and 3 use
# secondary signals and are discounted so that even a weak direct hit
# outranks the strongest indirect or sibling hit.
PASS_WEIGHT_DIRECT = 1.0 # Pass 1: test directly covers changed lines
PASS_WEIGHT_HUNK_CONTEXT = 0.50 # Pass 1b: test covers context line within the diff hunk
PASS_WEIGHT_INDIRECT = 0.50 # Pass 3: test shares virtual-dispatch callees with primary tests
PASS_WEIGHT_BROAD2 = 0.40 # Pass 4: test covers changed files via very-broad regions (rc 2001-8000)
PASS_WEIGHT_SIBLING = 0.25 # Pass 2: test covers a sibling file in the same source directory
PASS_WEIGHT_KEYWORD = 0.20 # Fallback: test filename contains domain keywords from changed files
# Shared-registry files: purely declarative files whose changes are virtually always
# additive (`extern const Event …`, new setting entries, new error codes). Every
# test emits profile events / reads settings, so coverage for any changed line in
# these files returns thousands of tests and floods the candidate pool with noise
# (the real signal lives in the other files touched by the same PR). Skip them
# entirely from coverage, sibling, indirect, and keyword-fallback passes.
SHARED_REGISTRY_FILES = frozenset({
"src/Common/ProfileEvents.cpp",
"src/Common/ProfileEvents.h",
"src/Common/CurrentMetrics.cpp",
"src/Common/CurrentMetrics.h",
"src/Common/ErrorCodes.cpp",
"src/Common/ErrorCodes.h",
"src/Common/SettingsChanges.cpp",
"src/Core/Settings.cpp",
"src/Core/SettingsChangesHistory.cpp",
"src/Core/SettingsChangesHistory.h",
})
def get_tests_by_changed_lines(self, changed_lines: list,
hunk_ranges: dict | None = None) -> dict:
"""
Query `checks_coverage_lines` for tests that cover each (filename, line_no) pair.
`changed_lines` is a list of `(filename, line_no)` tuples.
`hunk_ranges` is an optional `{filename: [(start, end), ...]}` dict from
`_parse_diff_hunk_ranges`. When provided, the SQL pre-filter for `.cpp`
files uses the union of all hunk ranges instead of just `[min, max]` of the
changed lines. This ensures coverage points at unchanged context lines
adjacent to the actual change are also returned by the query (they are then
matched against truly-changed `line_no` values in the Python post-filter).
Returns a dict mapping each input tuple to a list of `(test_name, region_width)`
tuples, where `region_width = line_end - line_start + 1`. The region width is
used by the caller to weight test scores (narrow regions = high signal).
All matching regions are included; the scoring formula naturally penalises
regions covered by many tests via the `region_test_count` denominator.
"""
import time
t0 = time.monotonic()
assert self.job_type, "get_tests_by_changed_lines requires a known job type"
if not changed_lines:
return {fl: [] for fl in changed_lines}
# Filter to files that are actually tracked in checks_coverage_lines.
# Coverage is only collected for compiled C++ sources (src/, programs/,
# utils/ etc.). Querying test scripts, docs, CI configs, or contrib files
# always returns zero rows and wastes CIDB quota — skip them up front.
COVERAGE_TRACKED_PREFIXES = ("src/", "programs/", "utils/", "base/")
coverage_lines = [
(f, ln)
for f, ln in changed_lines
if any(f.startswith(p) for p in COVERAGE_TRACKED_PREFIXES)
and f not in self.SHARED_REGISTRY_FILES
]
skipped = len(changed_lines) - len(coverage_lines)
if skipped:
print(
f"[find_tests] skipping {skipped} lines in non-tracked or shared-registry "
f"files (test scripts, docs, CI, contrib, ProfileEvents/Settings registries)"
)
skipped_registry = sorted(
{f for f, _ in changed_lines if f in self.SHARED_REGISTRY_FILES}
)
if skipped_registry:
print(
f"[find_tests] skipping {len(skipped_registry)} shared-registry file(s): "
f"{skipped_registry}"
)
# Return the original (full) key-set in the result dict so that
# callers always get one entry per input line (with empty list for
# non-tracked files).
non_tracked_keys = {(f, ln) for f, ln in changed_lines if (f, ln) not in set(coverage_lines)}
base_result: dict = {k: [] for k in non_tracked_keys}
if not coverage_lines:
print("[find_tests] no coverage-tracked files changed — skipping CIDB query")
base_result.update({(f, ln): [] for f, ln in changed_lines})
return base_result
# Group changed lines by file so we query with `file IN (…)` instead of
# one condition per line. A PR touching 13 files but 1000+ lines would
# otherwise generate a URL too long for the CIDB HTTP endpoint.
files_to_lines: dict = {}
for f, ln in coverage_lines:
files_to_lines.setdefault(self._stored_path(f), set()).add(ln)
unique_files = sorted(files_to_lines)
print(
f"[find_tests] querying coverage for {len(coverage_lines)} changed lines "
f"across {len(unique_files)} files"
)
# Build one condition per file: file='X' AND <range condition>.
# This pre-filters to regions that *could* overlap any changed line in that file,
# avoiding a full table scan per file while keeping the query size O(files) not O(lines).
#
# For header files (.h), we expand the line range to the entire file because
# headers typically define a single class and a change to any method affects
# the class semantics. Tests covering OTHER methods in the same header are
# highly relevant. This mirrors the old symbol-level algo which matched all
# symbols in the same translation unit.
#
# For .cpp files, we use per-hunk range conditions when hunk_ranges is provided.
# This expands the SQL filter to cover context lines within each hunk so that
# coverage points at unchanged lines adjacent to the actual change are fetched.
# Example: a one-line change in @@ -331,6 @@ has context lines 332-336; CIDB may
# only record coverage at line 332 (the first statement in the branch). Without
# hunk expansion the query would use line_end >= 331 AND line_start <= 331 and
# miss this point; with hunk expansion it uses line_end >= 331 AND line_start <= 336
# and finds it. The Python post-filter still only assigns results to actually-changed
# lines, so no spurious tests are introduced — only the SQL range is widened.
per_file_conds_parts = []
for f, lines in sorted(files_to_lines.items()):
esc_f = self._escape_sql_string(f)
if hunk_ranges:
# Use the bounding box of all hunk ranges (min hunk start to max hunk end)
# as the SQL pre-filter for .cpp files. This covers:
# 1. Context lines at the start/end of each hunk (same as per-hunk OR).
# 2. Coverage points that fall BETWEEN two hunks in the same file —
# a common case when a PR changes two separate sections of a function.
# Example: S3RequestSettings.cpp with hunks (301,306) and (321,326)
# has coverage at line 312 (between the hunks, rc=107) that would be
# missed by per-hunk OR but is captured by the bounding box [301, 326].
# Derive the original (non-stored) filename from the stored path for lookup.
orig_f = f[2:] if f.startswith("./") else f # strip "./" prefix
file_hunks = hunk_ranges.get(orig_f, [])
if file_hunks:
# Merge hunks that are ≤ MERGE_GAP lines apart so coverage
# points between two nearby hunks (inter-hunk lines) are
# included. Still uses per-range conditions rather than a
# single bounding box to avoid vacuuming thousands of lines
# between distant hunks in the same file.
MERGE_GAP = 5
merged: list = [list(file_hunks[0])]
for h_start, h_end in file_hunks[1:]:
if h_start - merged[-1][1] <= MERGE_GAP:
merged[-1][1] = max(merged[-1][1], h_end)
else:
merged.append([h_start, h_end])
hunk_conds = " OR ".join(
f"(line_end >= {max(0, h_start - 1)} AND line_start <= {h_end + 1})"
for h_start, h_end in merged
)
per_file_conds_parts.append(f"(file = '{esc_f}' AND ({hunk_conds}))")
else:
# No hunk info for this file; fall back to min/max of changed lines.
per_file_conds_parts.append(
f"(file = '{esc_f}'"
f" AND line_end >= {min(lines)} AND line_start <= {max(lines)})"
)
else:
per_file_conds_parts.append(
f"(file = '{self._escape_sql_string(f)}'"
f" AND line_end >= {min(lines)} AND line_start <= {max(lines)})"
)
per_file_conds = " OR ".join(per_file_conds_parts)
# Two-tier query: primary (narrow, <= MAX_TESTS_PER_LINE tests per
# region) and broad (> MAX, <= BROAD_REGION_HARD_CAP). Broad regions
# are included so infrastructure files (Context.cpp, RemoteQueryExecutor,
# etc.) are not completely invisible — the scoring formula already
# penalises them via the 1/region_test_count denominator. Only truly
# ubiquitous regions (> HARD_CAP) are dropped to keep response size sane.
BROAD_REGION_HARD_CAP = 3000 # scoring handles broad regions; very ubiquitous dropped
query = f"""
SELECT
file,
line_start,
line_end,
groupArray(test_name) AS tests,
groupArray(min_depth) AS depths,
uniqExact(test_name) AS region_test_count
FROM checks_coverage_lines
WHERE check_start_time > now() - interval 3 days
AND check_name LIKE '{self._escape_sql_string(self.job_type)}%'
AND notEmpty(test_name)
AND ({per_file_conds})
GROUP BY file, line_start, line_end
HAVING region_test_count <= {BROAD_REGION_HARD_CAP}
"""
cidb = self._ci_db()
t_query = time.monotonic()
raw = cidb.query(query, log_level="") or ""
print(f"[find_tests] CIDB query: {time.monotonic()-t_query:.2f}s, response={len(raw)} bytes")
# Parse TSV: file \t line_start \t line_end \t [tests] \t [depths] \t region_test_count
# region_test_count: how many distinct tests cover this region (ownership denominator).
# Falls back gracefully when columns are absent (old CIDB schema).
coverage_ranges: list = []
for row in raw.strip().splitlines():
if not row:
continue
parts = row.split("\t", 5)
if len(parts) < 4:
continue
file_, line_start_s, line_end_s, tests_raw = parts[:4]
depths_raw = parts[4] if len(parts) >= 5 else None
count_raw = parts[5] if len(parts) >= 6 else None
try:
line_start = int(line_start_s)
line_end = int(line_end_s)
tests = ast.literal_eval(tests_raw.strip())
depths = ast.literal_eval(depths_raw.strip()) if depths_raw else None
region_test_count = int(count_raw.strip()) if count_raw else len(tests) if isinstance(tests, list) else 1
if not isinstance(tests, list):
continue
# Pair each test with its min_depth (255 = not available).
if isinstance(depths, list) and len(depths) == len(tests):
test_depths = [(t, int(d)) for t, d in zip(tests, depths)]
else:
test_depths = [(t, 255) for t in tests]
coverage_ranges.append((file_, line_start, line_end, test_depths, region_test_count))
except (ValueError, SyntaxError):
print(f"Failed to parse coverage row: {row[:100]}")
# --- Second tier: very broad regions (rc > BROAD_REGION_HARD_CAP) --------
# Infrastructure files (Context.cpp, ProcessList.cpp) have regions covered
# by 3000-8000+ tests. These are dropped by the primary query to keep its
# response size manageable (groupArray would return thousands of names per
# region). However, the tests covering these broad regions are still
# relevant — they just have low specificity.
#
# The second-tier query returns only DISTINCT test names (no per-region
# grouping) for regions above the primary cap. This keeps the response
# size linear in the number of unique tests rather than quadratic in
# (regions × tests_per_region). Tests found this way are assigned the
# BROAD_FALLBACK_WIDTH and a synthetic region_test_count equal to the
# average rc across all matching broad regions.
# Skip the broad-tier2 query if we've already consumed too much time.
elapsed_after_primary = time.monotonic() - t0
run_broad_tier2 = elapsed_after_primary < 8.0
if not run_broad_tier2:
print(f"[find_tests] skipping broad-tier2 query (elapsed={elapsed_after_primary:.1f}s)")
VERY_BROAD_REGION_CAP = 8000 # drop truly ubiquitous regions
# Query broad regions and count how many regions each test covers in the
# changed files. Tests covering more regions get proportionally higher
# scores, so tests that genuinely exercise the changed code path (even via
# broad Context.cpp/ProcessList.cpp regions) rank above tests that merely
# touch one broad region. ORDER BY cov_regions DESC + LIMIT ensures we
# prioritise the most-covering tests when truncating.
# Rank broad-tier2 tests by (cov_regions * files_covered) so that tests
# exercising MULTIPLE changed files via broad regions rank above tests that
# only cover a single changed file. This makes the guarantee selection more
# specific: a test covering 3 changed files at 74 broad regions each ranks
# above a test covering only 1 file at 74 regions.
broad_query = f"""
SELECT test_name, count() AS cov_regions, uniqExact(file) AS files_covered
FROM checks_coverage_lines
WHERE check_start_time > now() - interval 3 days
AND check_name LIKE '{self._escape_sql_string(self.job_type)}%'
AND notEmpty(test_name)
AND ({per_file_conds})
AND (file, line_start, line_end) IN (
SELECT file, line_start, line_end
FROM checks_coverage_lines
WHERE check_start_time > now() - interval 3 days
AND check_name LIKE '{self._escape_sql_string(self.job_type)}%'
AND ({per_file_conds})
GROUP BY file, line_start, line_end
HAVING uniqExact(test_name) > {BROAD_REGION_HARD_CAP}
AND uniqExact(test_name) <= {VERY_BROAD_REGION_CAP}
)
GROUP BY test_name
ORDER BY (cov_regions * uniqExact(file)) DESC
LIMIT 8000
"""
# Broad-tier2 tests have real coverage but from regions shared by
# thousands of tests. The effective score is:
# cov_regions × PASS_WEIGHT_BROAD_TIER2 / (BROAD_FALLBACK_WIDTH × BROAD_TIER2_RC)
# = cov_regions × 2e-7
# A test covering 100 broad regions scores 2e-5 (much higher than the
# flat 2e-7 the old single-injection approach gave every test).
BROAD_FALLBACK_WIDTH = 100 # synthetic width — broad but real coverage
# Synthetic region_test_count: set so that PASS_WEIGHT_BROAD2 / (BROAD_FALLBACK_WIDTH
# × BROAD_TIER2_RC) stays in the same absolute score range as before while correctly
# reflecting that broad-tier2 signal is weaker than indirect-call (0.15 < 0.30).
# With PASS_WEIGHT_BROAD2=0.15 and the same score target (1e-5 at cov_regions=50,
# effective_width=10): BROAD_TIER2_RC = 0.15 / (10 × 1e-5) = 1500.
BROAD_TIER2_RC = 600
PASS_WEIGHT_BROAD_TIER2 = self.PASS_WEIGHT_BROAD2
broad_tests: dict = {} # test_name -> cov_regions count
if run_broad_tier2:
t_broad = time.monotonic()
try:
broad_raw = cidb.query(broad_query, log_level="") or ""
broad_elapsed = time.monotonic() - t_broad
print(f"[find_tests] broad-tier2 query: {broad_elapsed:.2f}s, response={len(broad_raw)} bytes")
except Exception as e:
print(f"[find_tests] broad-tier2 query failed (non-fatal): {e}")
broad_raw = ""
# Parse broad-tier2 results: test_name \t cov_regions \t files_covered
# Store cov_regions * files_covered as the ranking key so tests covering
# multiple changed files via broad regions rank above single-file tests.
for row in broad_raw.strip().splitlines():
parts = row.strip().split("\t", 2)
if not parts[0]:
continue
cov_regions = int(parts[1]) if len(parts) >= 2 and parts[1].isdigit() else 1
files_covered = int(parts[2]) if len(parts) >= 3 and parts[2].isdigit() else 1
broad_tests[parts[0]] = cov_regions * files_covered
if broad_tests:
max_score = max(broad_tests.values()) if broad_tests else 0
print(
f"[find_tests] broad-tier2: {len(broad_tests)} additional tests from "
f"regions with {BROAD_REGION_HARD_CAP} < rc <= {VERY_BROAD_REGION_CAP} "
f"(top score={max_score})"
)
# Map each input (filename, line_no) to (test_name, region_width, min_depth,
# region_test_count) 4-tuples. The CIDB query returned all ranges for the
# touched files; now filter to ranges that actually overlap a changed line.
# Start from base_result which already has empty entries for non-tracked files.
# Pre-index coverage_ranges by file for faster lookup.
ranges_by_file: dict = {}
for entry in coverage_ranges:
ranges_by_file.setdefault(entry[0], []).append(entry)
# Build a per-file, per-changed-line hunk-range lookup: for each changed line,
# find the hunk (start, end) that contains it. A coverage region is relevant
# for a changed line if it overlaps either the line itself OR the hunk containing
# the line. This ensures regions at unchanged context lines (e.g. the `if`
# condition at the start of a hunk) are counted for the changed lines inside it.
# The region width used for scoring is the actual CIDB region width (not the hunk
# width) so the scoring formula remains correct.
line_to_hunk: dict = {} # (filename, line_no) -> (hunk_start, hunk_end) or None
if hunk_ranges:
for filename, line_no in coverage_lines:
orig_f = filename # coverage_lines use original paths
file_hunks = hunk_ranges.get(orig_f, [])
for h_start, h_end in file_hunks:
if h_start <= line_no <= h_end:
line_to_hunk[(filename, line_no)] = (h_start, h_end)
break
result: dict = dict(base_result)
for filename, line_no in coverage_lines:
stored = self._stored_path(filename)
matched: list = []
is_header = filename.endswith(".h")
hunk = line_to_hunk.get((filename, line_no)) # (hunk_start, hunk_end) or None
for file_, line_start, line_end, test_depths, region_test_count in ranges_by_file.get(stored, []):
# Direct overlap: region contains the changed line.
overlaps = line_start <= line_no <= line_end
# Hunk overlap: region overlaps the hunk containing the changed line.
# Used for regions at context lines adjacent to the actual change.
hunk_overlap = (
not overlaps and hunk is not None and
line_start <= hunk[1] and line_end >= hunk[0]
)
if overlaps:
width = line_end - line_start + 1
for t, depth in test_depths:
matched.append((t, width, depth, region_test_count,
self.PASS_WEIGHT_DIRECT))
elif hunk_overlap:
# Context-line hit: region is within the diff hunk but does not
# directly contain the changed line. Use PASS_WEIGHT_HUNK_CONTEXT
# (0.35 < PASS_WEIGHT_DIRECT but > PASS_WEIGHT_INDIRECT) so:
# - Scores rank below direct hits but above indirect/sibling.
# - The pass_weight is below PASS_WEIGHT_INDIRECT (0.3)... wait
# actually 0.35 > 0.3; sparse-file threshold uses pw >= PASS_WEIGHT_INDIRECT,
# so hunk-context hits WOULD still count. But we explicitly want them
# NOT to count — the file has nearby coverage but the changed lines
# themselves are in a code path with low rc (pure insertions).
# Using PASS_WEIGHT_HUNK_CONTEXT in the sparse-file check is handled
# by the condition: sparse-file uses pw >= PASS_WEIGHT_DIRECT (see below).
width = line_end - line_start + 1
for t, depth in test_depths:
matched.append((t, width, depth, region_test_count,
self.PASS_WEIGHT_HUNK_CONTEXT))
elif is_header:
# For header files: include non-overlapping regions from
# the same file with SIBLING_DIR_WIDTH penalty. These are
# other methods in the same class, still relevant but with
# weaker signal than a direct line overlap.
for t, depth in test_depths:
matched.append((t, self.SIBLING_DIR_WIDTH, 255, region_test_count,
self.PASS_WEIGHT_DIRECT))
# Deduplicate: keep best (min width, max pass_weight, min depth, min rc) per test.
by_test: dict = {} # test -> (min_width, min_depth, min_region_test_count, max_pw)
for t, w, d, rc, pw in matched:
if t not in by_test:
by_test[t] = (w, d, rc, pw)
else:
ow, od, orc, opw = by_test[t]
by_test[t] = (min(ow, w), min(od, d), min(orc, rc), max(opw, pw))
result[(filename, line_no)] = [
(t, w, d, rc, pw) for t, (w, d, rc, pw) in sorted(by_test.items())
]
# Inject broad-tier2 tests into coverage results.
# Tests are injected into the first coverage-tracked changed line only.
# The effective width is inversely proportional to cov_regions so that
# tests covering more of the changed files get lower width → higher score:
# score = PASS_WEIGHT_BROAD_TIER2 / (effective_width × BROAD_TIER2_RC)
# = 0.5 / (max(1, 500//cov_regions) × 5000)
# A test covering 100 regions: effective_width=5, score=0.5/(5×5000)=2e-5
# A test covering 1 region: effective_width=500, score=0.5/(500×5000)=2e-7
# The 100x difference correctly reflects relative specificity.
if broad_tests:
for filename, line_no in coverage_lines:
if not any(filename.startswith(p) for p in COVERAGE_TRACKED_PREFIXES):
continue
key = (filename, line_no)
existing = {e[0] for e in result.get(key, [])}
for tname, cov_regions in broad_tests.items():
if tname not in existing:
effective_width = max(1, BROAD_FALLBACK_WIDTH // max(1, cov_regions))
result.setdefault(key, []).append(
(tname, effective_width, 255, BROAD_TIER2_RC, PASS_WEIGHT_BROAD_TIER2)
)
break # inject into first tracked line only (score is cov_regions-weighted)
# --- Third tier: ultra-broad expansion -----------------------------------
# When both primary (rc ≤ BROAD_REGION_HARD_CAP) and broad-tier2
# (BROAD_REGION_HARD_CAP < rc ≤ VERY_BROAD_REGION_CAP) return no tests for
# all changed lines, expand further to rc ≤ ULTRA_BROAD_REGION_CAP.
# This handles infrastructure files where every changed line is covered by
# 8000+ tests (e.g. IMergeTreeDataPart.cpp, Context.cpp hot paths) so
# neither tier finds anything. Ultra-broad tests rank below all other
# evidence — they are a last resort before the keyword fallback.
all_found = {e[0] for pairs in result.values() for e in pairs}
if not all_found and run_broad_tier2 and (time.monotonic() - t0) < 12.0:
ULTRA_BROAD_REGION_CAP = 30000
ultra_query = f"""
SELECT test_name, count() AS cov_regions, uniqExact(file) AS files_covered
FROM checks_coverage_lines
WHERE check_start_time > now() - interval 3 days
AND check_name LIKE '{self._escape_sql_string(self.job_type)}%'
AND notEmpty(test_name)
AND ({per_file_conds})
AND (file, line_start, line_end) IN (
SELECT file, line_start, line_end
FROM checks_coverage_lines
WHERE check_start_time > now() - interval 3 days
AND check_name LIKE '{self._escape_sql_string(self.job_type)}%'
AND ({per_file_conds})
GROUP BY file, line_start, line_end
HAVING uniqExact(test_name) > {VERY_BROAD_REGION_CAP}
AND uniqExact(test_name) <= {ULTRA_BROAD_REGION_CAP}
)
GROUP BY test_name
ORDER BY (cov_regions * uniqExact(file)) DESC
LIMIT 4000
"""
t_ultra = time.monotonic()
try:
ultra_raw = cidb.query(ultra_query, log_level="") or ""
print(f"[find_tests] ultra-broad query: {time.monotonic()-t_ultra:.2f}s, "
f"response={len(ultra_raw)} bytes")
except Exception as e:
print(f"[find_tests] ultra-broad query failed (non-fatal): {e}")
ultra_raw = ""
ultra_tests: dict = {}
for row in ultra_raw.strip().splitlines():
parts = row.strip().split("\t", 2)
if not parts[0]:
continue
cov_regions = int(parts[1]) if len(parts) >= 2 and parts[1].isdigit() else 1
files_covered = int(parts[2]) if len(parts) >= 3 and parts[2].isdigit() else 1
ultra_tests[parts[0]] = cov_regions * files_covered
if ultra_tests:
print(f"[find_tests] ultra-broad: {len(ultra_tests)} tests from "
f"regions with rc > {VERY_BROAD_REGION_CAP} (top score={max(ultra_tests.values())})")
PASS_WEIGHT_ULTRA_BROAD = self.PASS_WEIGHT_BROAD2 / 2
for filename, line_no in coverage_lines:
if not any(filename.startswith(p) for p in COVERAGE_TRACKED_PREFIXES):
continue
key = (filename, line_no)
existing = {e[0] for e in result.get(key, [])}
for tname, score in ultra_tests.items():
if tname not in existing:
effective_width = max(1, BROAD_FALLBACK_WIDTH // max(1, score))
result.setdefault(key, []).append(
(tname, effective_width, 255, BROAD_TIER2_RC, PASS_WEIGHT_ULTRA_BROAD)
)
break # inject into first tracked line only
# --- Secondary pass: sibling files in the same source directory ----
# For each changed C++ file under src/, find tests that cover OTHER files
# in the same directory. These tests are added as very broad hits
# (SIBLING_DIR_WIDTH) so they rank below direct hits but are not silently
# dropped. This catches, e.g., Arrow-reader tests when the Arrow writer
# is changed — the reader and writer live side-by-side and a writer change
# may break reader round-trips that the direct coverage query misses because
# those tests never call the writer code path directly.
sibling_tests = self._query_sibling_dir_tests(files_to_lines, result)
if sibling_tests:
# Inject into every changed line so the scorer can accumulate width_score.
# Use the actual region_test_count from the sibling query so heavily-shared
# sibling tests are penalised the same way as direct broad hits.
for key in result:
fname, _ = key
stored = self._stored_path(fname)
dir_path = stored.rsplit("/", 1)[0] + "/"
if not stored.startswith("./src/"):
continue
for t, rc in sibling_tests.get(dir_path, {}).items():
# Skip tests already found by the primary query for this line.
existing = {e[0] for e in result[key]}
if t not in existing:
result[key].append((t, self.SIBLING_DIR_WIDTH, 255, max(1, rc), self.PASS_WEIGHT_SIBLING))
# --- Tertiary pass: file-level expansion for ultra-sparse changed lines ---
# When ALL coverage regions overlapping a .cpp file's changed lines have
# rc ≤ SPARSE_FILE_THRESHOLD, the changed code is exercised by at most a
# handful of tests in nightly runs (e.g. a JSON-column method only called
# by one specific test). The primary query finds those tests, but misses
# related tests that exercise the same class through *other* methods —
# exactly the gap the old DWARF "any symbol in the TU" approach filled.
# Fallback: fetch ALL regions in the sparse file with rc ≤ MAX_TESTS_PER_LINE
# so domain-specific tests (e.g. other JSON wide-part tests) get included.
SPARSE_FILE_THRESHOLD = 8 # trigger when max primary rc for a file ≤ this
PASS_WEIGHT_SPARSE_FILE = 0.30 # between direct (1.0) and sibling (0.1)
SPARSE_FILE_WIDTH = 600 # synthetic width: stronger than sibling (10000)
# Compute per-file max rc from the Python-filtered result dict (only regions
# that actually overlapped a changed line, not merely the SQL-fetched neighbourhood).
# coverage_ranges includes broader SQL hits (e.g. line 1236 rc=76 when the diff
# only touches 1173-1179), which would spuriously suppress the sparse-file pass.
file_max_primary_rc: dict = {}
for (fname, _), pairs in result.items():
stored = self._stored_path(fname) # fname from coverage_lines (orig path)
for _, _, _, region_rc, pw in pairs:
# Only count DIRECT hits (pw == PASS_WEIGHT_DIRECT) for sparse-file threshold.
# Hunk-context hits (PASS_WEIGHT_HUNK_CONTEXT) are excluded: a file where
# the actually-changed lines have rc=0 but nearby context has rc=30 is still
# "sparse from the changed code's perspective" and benefits from sparse-file
# expansion. Including hunk-context hits here would suppress that expansion.
if pw >= self.PASS_WEIGHT_DIRECT: # direct-pass entries only
if region_rc > file_max_primary_rc.get(stored, 0):
file_max_primary_rc[stored] = region_rc
# Files that appear in the primary SQL response (have coverage in CIDB at all).
# Used to distinguish "stale / no 3-day data" from "genuinely uncovered file".
files_in_coverage_ranges = {file_ for file_, *_ in coverage_ranges}
# files_to_lines keys are already stored-paths (./src/...) — no _stored_path needed.
sparse_cpp_files = [
f for f in files_to_lines
if not f.endswith(".h")
and f in files_in_coverage_ranges
and file_max_primary_rc.get(f, 0) <= SPARSE_FILE_THRESHOLD
]
if sparse_cpp_files:
sparse_conds = " OR ".join(
f"file = '{self._escape_sql_string(f)}'"
for f in sparse_cpp_files
)
sparse_query = f"""
SELECT file, line_start, line_end,
groupArray(test_name) AS tests,
groupArray(min_depth) AS depths,
uniqExact(test_name) AS region_test_count
FROM checks_coverage_lines
WHERE check_start_time > now() - interval 3 days
AND check_name LIKE '{self._escape_sql_string(self.job_type)}%'
AND notEmpty(test_name)
AND ({sparse_conds})
GROUP BY file, line_start, line_end
HAVING region_test_count <= {self.MAX_TESTS_PER_LINE}
"""
t_sparse = time.monotonic()
sparse_elapsed = 0.0
try:
sparse_raw = cidb.query(sparse_query, log_level="") or ""
sparse_elapsed = time.monotonic() - t_sparse
except Exception as e:
print(f"[find_tests] sparse-file query failed (non-fatal): {e}")
sparse_raw = ""
# Collect per-test best (min width, min depth, min rc) across all regions.
sparse_file_tests: dict = {} # test_name -> (width, depth, rc)
for row in sparse_raw.strip().splitlines():
if not row:
continue
parts = row.split("\t", 5)
if len(parts) < 4:
continue
file_, ls_s, le_s, tests_raw = parts[:4]
depths_raw = parts[4] if len(parts) >= 5 else None
count_raw = parts[5] if len(parts) >= 6 else None
try:
tests = ast.literal_eval(tests_raw.strip())
depths = ast.literal_eval(depths_raw.strip()) if depths_raw else None
rc = int(count_raw.strip()) if count_raw else (len(tests) if isinstance(tests, list) else 1)
width = max(1, int(le_s) - int(ls_s) + 1)
if not isinstance(tests, list):
continue
for i, t in enumerate(tests):
d = int(depths[i]) if isinstance(depths, list) and i < len(depths) else 255
if t not in sparse_file_tests:
sparse_file_tests[t] = (width, d, rc)
else:
ow, od, orc = sparse_file_tests[t]
sparse_file_tests[t] = (min(ow, width), min(od, d), min(orc, rc))
except (ValueError, SyntaxError):
pass
if sparse_file_tests:
print(
f"[find_tests] sparse-file expansion: {len(sparse_file_tests)} tests "
f"from {len(sparse_cpp_files)} ultra-sparse file(s) ({sparse_elapsed:.2f}s)"
)
# Inject into the first tracked changed line of each sparse file.
# sparse_cpp_files are already stored-paths; coverage_lines use orig paths
sparse_stored = set(sparse_cpp_files)
injected_sparse: set = set()
for fname, ln in coverage_lines:
if not any(fname.startswith(p) for p in COVERAGE_TRACKED_PREFIXES):
continue
if self._stored_path(fname) not in sparse_stored:
continue
key = (fname, ln)
if key in injected_sparse:
continue
existing = {e[0] for e in result.get(key, [])}
for t, (width, depth, rc) in sparse_file_tests.items():
if t not in existing:
result.setdefault(key, []).append(
(t, SPARSE_FILE_WIDTH, depth, rc, PASS_WEIGHT_SPARSE_FILE)
)
injected_sparse.add(key)
# --- Tertiary pass: indirect-call (virtual dispatch) co-occurrence ------
# Find tests calling the same vtable/function-pointer callees as primary
# tests. Ranked between direct and sibling hits (INDIRECT_CALL_WIDTH=3000).
indirect_tests = self._query_indirect_call_tests(result, sparse_cpp_files)
if indirect_tests:
for key in result:
fname, _ = key
if not any(fname.startswith(p) for p in COVERAGE_TRACKED_PREFIXES):
continue
existing = {e[0] for e in result[key]}
for t, shared_count in indirect_tests.items():
if t not in existing:
# Scale effective width inversely with shared_callees so that
# tests sharing more callees with the primary set rank higher.
# Floor at NARROW_REGION_MAX_LINES+1 to stay in the broad tier.
effective_width = max(
self.NARROW_REGION_MAX_LINES + 1,
self.INDIRECT_CALL_WIDTH // max(1, shared_count),
)
result[key].append((t, effective_width, 255, 1, self.PASS_WEIGHT_INDIRECT))
total_unique_tests = len({t for pairs in result.values() for t, *_ in pairs})
lines_with_tests = sum(1 for (f, _), pairs in result.items() if pairs and any(f.startswith(p) for p in COVERAGE_TRACKED_PREFIXES))
print(
f"[find_tests] done in {time.monotonic()-t0:.2f}s: "
f"{lines_with_tests}/{len(coverage_lines)} lines matched, "
f"{total_unique_tests} unique tests selected"
)
# Store top broad-tier2 tests (by cov_regions) on self so that
# get_most_relevant_tests() can guarantee they make it through
# the MAX_OUTPUT_TESTS cap even if their scores fall below the cutoff.
n_guarantee = min(100, max(20, len(coverage_lines) // 3))
self._broad_tier2_guarantee = [
t
for t, _ in sorted(broad_tests.items(), key=lambda x: -x[1])
][:n_guarantee] if broad_tests else []
return result
@staticmethod
def _extract_domain_keywords(filename: str) -> list:
"""
Extract domain-specific CamelCase words from a source filename.
These words are used to filter sibling files so we only expand to
files in the same functional domain (e.g. "Arrow" for Arrow I/O files)
rather than all files in the directory.
Returns a list of significant words (length > 4, not architectural).
Empty list means no filtering (expand to all sibling files, but this
is unusual and typically indicates a very generic filename).
"""
import re as _re
import os as _os
base = _os.path.splitext(_os.path.basename(filename))[0]
# Split CamelCase and all-caps acronyms:
# "CHColumnToArrowColumn" → ['CH', 'Column', 'To', 'Arrow', 'Column']
# "LDAPClient" → ['LDAP', 'Client']
# "PostgreSQLDictionary" → ['Postgre', 'SQL', 'Dictionary']
# Pattern: acronym-run-before-TitleCase | TitleCase-word | lowercase-word
# The trailing [A-Z] alternative captures single uppercase chars that
# the other patterns miss (e.g. the "K" in "TopK" or "N" in "MergeN").
words = _re.findall(r"[A-Z]+(?=[A-Z][a-z])|[A-Z][a-z0-9]+|[A-Z]{2,}|[a-z][a-z0-9]+|[A-Z]", base)
# Merge a lone trailing uppercase letter into the previous word so that
# compound names like "TopK" or "MergeN" are kept whole instead of losing
# the suffix.
merged: list = []
for w in words:
if len(w) == 1 and w.isupper() and merged:
merged[-1] = merged[-1] + w
else:
merged.append(w)
words = merged
# Architectural / ubiquitous words that appear in most files in a directory.
# Keeping this list generous avoids keywords that are too common to be useful.
COMMON = {
# Generic C++ / Datastore infrastructure words
"block", "input", "output", "format", "column", "stream",
"storage", "table", "query", "parser", "writer", "reader",
"buffer", "default", "base", "impl", "merge", "tree",
"row", "file", "data", "info", "type", "list", "map",
"with", "from", "into",
# MergeTree-specific architectural words (appear in almost every MergeTree file)
"condition", "granularity", "selector", "partition", "replica",
"transaction", "virtual", "local", "remote", "range", "level",
# Datastore architectural nouns that appear in many places but are not
# specific enough to pin to a test domain.
"handler", "manager", "source", "access", "control",
"service", "server", "client", "external", "internal",
"settings", "setting", "config", "context", "result",
"state", "status", "entry", "record", "update", "create",
}
# Allow 3-char all-uppercase acronyms (CSV, ORC, URL, JWT, KQL, etc.) in addition
# to words ≥ 4 chars. Generic acronyms like "API", "SQL", "DDL", "DML" are added
# to COMMON below so they don't generate false matches.
COMMON_ACRONYMS = {"api", "sql", "ddl", "dml", "ids", "uid", "abi", "cpu", "gpu", "ram",
"tcp", "udp", "tls", "ssl", "rpc", "ttl", "log", "tag", "row", "set"}
specific = [
w for w in words
if w.lower() not in COMMON
and (
(len(w) >= 4)
or (len(w) == 3 and w.isupper() and w.lower() not in COMMON_ACRONYMS)
)
]
return specific
def _query_indirect_call_tests(self, primary_result: dict, sparse_files: list | None = None) -> dict:
"""
Tertiary pass: find tests that call the same virtual / function-pointer
callees as the primary tests (those directly covering changed files).
Semantic: if primary test A covers changed file F and calls virtual
callee C (recorded via LLVM value profiling), then any other test B
that also calls callee C is exercising the same interface — even if B
never directly calls code in F. This catches tests that reach changed
implementations only via vtable dispatch or function pointers.
Uses only the existing `checks_coverage_indirect_calls` CIDB table;
no new tables are required. The join is:
primary tests → their callee_offsets → other tests with same callees.
Shared-library callees (operator new, malloc, etc.) are excluded at
collection time in coverage.cpp, so no extra filter is needed here.
Degrades gracefully when the table is empty (e.g. first nightly run).
"""
import time
# Use only high-confidence primary tests (from regions with
# <= MAX_TESTS_PER_LINE) for the indirect-call join. Including all
# tests from broad regions (e.g. 1000+) would make the IN-list too
# large for the CIDB HTTP endpoint ("Field value too long").
assert self.job_type, "_query_indirect_call_tests requires a known job type"
primary_tests = set()
seed_rcs: list = []
SHALLOW_DEPTH = 3 # min_depth ≤ this means the test called the changed function
# very sparingly — it specifically targets this code path.
# Use as seeds even when rc > MAX_TESTS_PER_LINE.
SHALLOW_RC_CAP = 2000 # don't use shallow seeds from ultra-broad regions
for pairs in primary_result.values():
for entry in pairs:
t = entry[0]
depth = entry[2] if len(entry) > 2 else 255
rc = entry[3] if len(entry) > 3 else 1
pw = entry[4] if len(entry) > 4 else 1.0
if pw < self.PASS_WEIGHT_INDIRECT:
continue
is_narrow = rc <= self.MAX_TESTS_PER_LINE
is_shallow = depth <= SHALLOW_DEPTH and rc <= SHALLOW_RC_CAP
if is_narrow or is_shallow:
primary_tests.add(t)
seed_rcs.append(rc)
if not primary_tests:
return {}
# C. Supplement seeds from narrow regions (rc ≤ FILE_SEED_RC) anywhere in
# the sparse files. When changed lines have rc=1 (only one test ever ran
# that path), the existing seeds are too few / too specific to find related
# tests via Jaccard. Seeds from other narrow regions in the same file have
# broader callee coverage and higher overlap with domain-related tests.
FILE_SEED_RC = 30 # narrower than MAX_TESTS_PER_LINE; avoids pulling in broad seeds
if sparse_files:
_cidb = self._ci_db()
# sparse_files are already stored-paths (./src/...)
sparse_conds = " OR ".join(
f"file = '{self._escape_sql_string(f)}'"
for f in sparse_files
)
seed_query = f"""
SELECT file, line_start, line_end,
groupArray(test_name) AS tests,
uniqExact(test_name) AS rc
FROM checks_coverage_lines
WHERE check_start_time > now() - interval 3 days
AND check_name LIKE '{self._escape_sql_string(self.job_type)}%'
AND notEmpty(test_name)
AND ({sparse_conds})
GROUP BY file, line_start, line_end
HAVING rc <= {FILE_SEED_RC}
"""
try:
seed_raw = _cidb.query(seed_query, log_level="") or ""
extra_seeds = 0
for row in seed_raw.strip().splitlines():
parts = row.split("\t", 4)
if len(parts) < 4:
continue
try:
tests = ast.literal_eval(parts[3].strip())
rc = int(parts[4].strip()) if len(parts) >= 5 else len(tests)
if isinstance(tests, list):
for t in tests:
if t not in primary_tests:
primary_tests.add(t)
seed_rcs.append(rc)
extra_seeds += 1
except (ValueError, SyntaxError):
pass
if extra_seeds:
print(f"[find_tests] indirect seeds: +{extra_seeds} from narrow file regions")
except Exception as e:
print(f"[find_tests] seed enrichment query failed (non-fatal): {e}")
# B. Adaptive Jaccard threshold.
# Only lower the threshold for truly sparse code (rc < 20) where the seed
# set is tiny and even 15% callee overlap is a meaningful signal.
# For rc ≥ 20 we already have enough seeds to enforce the full 70% threshold
# — loosening it further admits tests that share only generic callees
# (memory allocation, string ops) and pollutes results with non-domain tests.
# rc=1 → 15% (only 1 seed: anything sharing callees is relevant)
# rc=10 → 43%
# rc=20+ → 70% (enough seeds: enforce strict domain match)
min_seed_rc = min(seed_rcs) if seed_rcs else 200
JACCARD_MIN_PCT = max(15, min(70, 15 + int(55 * min(min_seed_rc, 20) / 20)))
# Adaptive LIMIT: fewer indirect tests when we already have many direct seeds.
# With many seeds the callee union is large and 70% Jaccard admits too many
# loosely-related tests. Cap inversely with seed count so that:
# n_seeds=1 → limit=200 (need many indirect to compensate)
# n_seeds=10 → limit=100
# n_seeds=50 → limit=20 (direct evidence is already rich)
n_seeds = len(primary_tests)
INDIRECT_LIMIT = max(20, min(200, int(200 / max(1, n_seeds / 5))))
# Cap at 500 tests to prevent HTTP field-length errors.
if len(primary_tests) > 500:
primary_tests = set(sorted(primary_tests)[:500])
escaped_primary = ", ".join(
f"'{self._escape_sql_string(t)}'" for t in sorted(primary_tests)
)
# Self-join on callee_offset ranked by Jaccard-like specificity.
#
# Problem with raw shared_callees ordering: infrastructure tests (filesystem
# cache, S3) rank highest because they share many I/O virtual-dispatch callees
# with any MergeTree-reading primary test, even if they're completely unrelated
# to the changed code domain.
#
# Fix: rank by shared / secondary_test_total_specific_callees (Jaccard
# fraction). A test whose callee set is mostly overlapping with the primary
# set is specifically exercising the same domain; a filesystem-cache test whose
# 200 specific callees all happen to be in the primary set gets lower rank than
# a DDL test whose 50 specific callees are 90% shared with the primary set.
#
# Additional guards:
# MIN_SHARED — require at least this many shared callees (eliminates
# accidental 1-2 callee overlaps).
# MIN_SECONDARY — secondary test must have at least this many specific
# callees; avoids 100%-Jaccard artifacts from tiny sets.
# MAX_CALLEE_COUNT — exclude globally-ubiquitous callees (logging, malloc).
MAX_CALLEE_TEST_COUNT = 300 # callees in >= this many tests are ubiquitous
# Thresholds scale with the primary set size so that small primary sets
# (e.g. 3 tests for a focused single-file PR) still find results while
# large primary sets use stricter filters to suppress infrastructure noise.
n_primary = len(primary_tests)
MIN_SHARED = max(1, min(10, n_primary // 5)) # 2 for tiny, 10 for large
MIN_SECONDARY = max(5, min(50, n_primary * 3)) # 15 for tiny, 50 for large
query = f"""
SELECT
ic2.test_name,
count(DISTINCT ic1.callee_offset) AS shared_callees,
ic2_tot.tot_callees,
count(DISTINCT ic1.callee_offset) * 100.0 / ic2_tot.tot_callees AS jaccard_pct
FROM checks_coverage_indirect_calls ic1
JOIN checks_coverage_indirect_calls ic2 ON ic1.callee_offset = ic2.callee_offset
JOIN (
-- Total number of specific (non-ubiquitous) callees each secondary test
-- uses. Used as the Jaccard denominator.
SELECT test_name, count(DISTINCT callee_offset) AS tot_callees
FROM checks_coverage_indirect_calls
WHERE check_start_time > now() - interval 3 days
AND check_name LIKE '{self._escape_sql_string(self.job_type)}%'
AND callee_offset IN (
SELECT callee_offset
FROM checks_coverage_indirect_calls
WHERE check_start_time > now() - interval 3 days
AND check_name LIKE '{self._escape_sql_string(self.job_type)}%'
GROUP BY callee_offset
HAVING uniqExact(test_name) < {MAX_CALLEE_TEST_COUNT}
)
GROUP BY test_name
HAVING tot_callees >= {MIN_SECONDARY}
) ic2_tot ON ic2.test_name = ic2_tot.test_name
WHERE ic1.check_start_time > now() - interval 3 days
AND ic2.check_start_time > now() - interval 3 days
AND ic1.check_name LIKE '{self._escape_sql_string(self.job_type)}%'
AND ic2.check_name LIKE '{self._escape_sql_string(self.job_type)}%'
AND ic1.test_name IN ({escaped_primary})
AND ic2.test_name NOT IN ({escaped_primary})
AND ic1.callee_offset IN (
SELECT callee_offset
FROM checks_coverage_indirect_calls
WHERE check_start_time > now() - interval 3 days
AND check_name LIKE '{self._escape_sql_string(self.job_type)}%'
GROUP BY callee_offset
HAVING uniqExact(test_name) < {MAX_CALLEE_TEST_COUNT}
)
GROUP BY ic2.test_name, ic2_tot.tot_callees
HAVING shared_callees >= {MIN_SHARED}
AND count(DISTINCT ic1.callee_offset) * 100.0 / ic2_tot.tot_callees >= {JACCARD_MIN_PCT:.1f}
ORDER BY jaccard_pct DESC, shared_callees DESC
LIMIT {INDIRECT_LIMIT}
"""
try:
cidb = self._ci_db()
t0 = time.monotonic()
raw = cidb.query(query, log_level="") or ""
elapsed = time.monotonic() - t0
except Exception as e:
print(f"[find_tests] indirect-call query failed (non-fatal): {e}")
return {}
# Parse TSV: test_name \t shared_callees \t tot_callees \t jaccard_pct
result_map: dict = {}
for row in raw.strip().splitlines():
parts = row.split("\t")
if len(parts) >= 2 and parts[0].strip():
try:
result_map[parts[0].strip()] = int(parts[1].strip())
except ValueError:
pass
if not result_map:
return {}
# Extract top Jaccard score from the raw output for logging
top_jaccard = 0.0
for row in raw.strip().splitlines():
parts = row.split("\t")
if len(parts) >= 4:
try:
top_jaccard = max(top_jaccard, float(parts[3].strip()))
except ValueError:
pass
print(
f"[find_tests] indirect-call query: {elapsed:.2f}s, "
f"{len(result_map)} additional test candidates via callee co-occurrence "
f"(top jaccard={top_jaccard:.0f}%, top shared={max(result_map.values())})"
)
return result_map
def _query_sibling_dir_tests(self, files_to_lines: dict, primary_result: dict) -> dict:
"""
Secondary pass: find tests that are likely related to the changed files
by co-coverage proximity.
For each changed C++ source file under src/ we:
1. Identify which sibling files (same directory, different name) the
PRIMARY tests also cover. These are files with a natural code
relationship to the changed file (e.g. Arrow reader ↔ writer).
2. Find tests that cover those same sibling files but were NOT already
selected by the primary query. These are tests that exercise the
same functional area from a different entry point.
Because we only expand through files that the primary tests already touch,
we avoid the false positives that come from querying the whole directory
(which would mix in unrelated format tests, HTTP tests, etc.).
Returns {dir_path -> {test_name -> SIBLING_RC}}.
"""
import time
assert self.job_type, "_query_sibling_dir_tests requires a known job type"
# Collect source directories of changed C++ files.
src_dirs: dict = {} # dir_path -> list of changed files in that dir
for f in files_to_lines:
if f.startswith("./src/") and (f.endswith(".cpp") or f.endswith(".h")):
dir_path = f.rsplit("/", 1)[0] + "/"
src_dirs.setdefault(dir_path, []).append(f)
if not src_dirs:
return {}
# Collect high-confidence primary tests (from narrow or moderately-broad
# regions) for the sibling-dir expansion. Using all primary tests
# (including those from very broad regions) would make the IN-list
# too large for the CIDB HTTP endpoint.
primary_tests = set()
for pairs in primary_result.values():
for entry in pairs:
t = entry[0]
rc = entry[3] if len(entry) > 3 else 1
pw = entry[4] if len(entry) > 4 else 1.0
if rc <= self.MAX_TESTS_PER_LINE and pw >= self.PASS_WEIGHT_INDIRECT:
primary_tests.add(t)
if not primary_tests:
return {}
# Cap at 500 tests to prevent HTTP field-length errors.
if len(primary_tests) > 500:
primary_tests = set(sorted(primary_tests)[:500])
changed_files = set(files_to_lines.keys())
escaped_primary = ", ".join(
f"'{self._escape_sql_string(t)}'" for t in sorted(primary_tests)
)
dir_conds = " OR ".join(
f"file LIKE '{self._escape_sql_string(d)}%'"
for d in sorted(src_dirs)
)
not_changed = " AND ".join(
f"file != '{self._escape_sql_string(f)}'"
for f in sorted(changed_files)
)
# Extract domain keywords from changed C++ SOURCE filenames only (not
# test files, which would add misleading keywords like "uuid" or "parquet").
# E.g. "Arrow" from CHColumnToArrowColumn.cpp ensures we only look at
# Arrow-related siblings, not every file in the directory.
all_keywords: list = []
for f in sorted(changed_files):
if f.startswith("./src/") and (f.endswith(".cpp") or f.endswith(".h")):
all_keywords.extend(self._extract_domain_keywords(f.split("/")[-1]))
# Deduplicate, keep unique keywords only (not repeated across changed files)
unique_kws = list(dict.fromkeys(all_keywords))
# Build keyword filter for sibling filenames.
# Use OR across keywords so that compound-named files (e.g. "DistributedSink")
# can find siblings matching EITHER component ("Distributed" OR "Sink").
# Previously AND was used but it was too restrictive: when a PR changes
# DistributedSink.cpp (keywords: Distributed, Sink), the AND condition
# "file LIKE '%Distributed%' AND file LIKE '%Sink%'" only matched
# DistributedSink.cpp itself (the changed file), yielding zero sibling results.
# With OR, related sibling files (DistributedAsyncInsert*.cpp etc.) are found.
#
# False positive risk is mitigated by:
# 1. MAX_SIBLING_FILE_TESTS cap (200): broad infrastructure files excluded.
# 2. Inner subquery: sibling file must be covered by >= min_sibling_coverage
# primary tests, so loosely-related files not exercised by the PR's primary
# tests are naturally excluded.
if unique_kws:
kws = unique_kws[:4] # cap to avoid overly long queries
# OR: sibling file must contain at least one keyword.
kw_cond = " OR ".join(
f"file LIKE '%{self._escape_sql_string(kw)}%'"
for kw in kws
)
sibling_file_filter = f"AND ({kw_cond})"
else:
# No specific keywords — fall back to all sibling files (rare)
sibling_file_filter = ""
# Step 1 + 2 combined: find tests covering sibling files in the same
# functional domain as the changed file. The inner SELECT identifies
# sibling files with the right domain keywords that the primary tests
# already touch; the outer SELECT finds new tests for those same files.
#
# Two filters prevent broad infrastructure files from flooding the candidates:
#
# 1. INNER subquery min coverage: the sibling file must be covered by at
# least MIN_SIBLING_COVERAGE primary tests. This excludes files that are
# only incidentally touched by primary tests (e.g. AggregatedDataVariants.cpp
# being exercised by Variant tests through GROUP BY operations).
#
# 2. Global test count cap (NOT IN subquery): exclude sibling files that are
# covered by more than MAX_SIBLING_TESTS distinct tests globally. Very
# broadly-covered files (like AggregatedDataVariants.cpp with 3400 tests,
# or RewriteCountVariantsVisitor.cpp with 4250 tests) are infrastructure —
# finding their tests adds noise rather than signal. The threshold matches
# MAX_TESTS_PER_LINE so we consistently exclude "too common" files.
MAX_SIBLING_FILE_TESTS = self.MAX_TESTS_PER_LINE # same cap as direct coverage
n_primary = len(primary_tests)
min_sibling_coverage = max(2, n_primary // 5)
query = f"""
SELECT DISTINCT test_name
FROM checks_coverage_lines
WHERE check_start_time > now() - interval 3 days
AND check_name LIKE '{self._escape_sql_string(self.job_type)}%'
AND notEmpty(test_name)
AND test_name NOT IN ({escaped_primary})
AND ({dir_conds})
AND ({not_changed})
{sibling_file_filter}
AND file NOT IN (
SELECT file
FROM checks_coverage_lines
WHERE check_start_time > now() - interval 3 days
AND check_name LIKE '{self._escape_sql_string(self.job_type)}%'
AND ({dir_conds})
{sibling_file_filter}
GROUP BY file
HAVING uniqExact(test_name) > {MAX_SIBLING_FILE_TESTS}
)
AND file IN (
SELECT file
FROM checks_coverage_lines
WHERE check_start_time > now() - interval 3 days
AND test_name IN ({escaped_primary})
AND ({dir_conds})
AND ({not_changed})
{sibling_file_filter}
GROUP BY file
HAVING uniqExact(test_name) >= {min_sibling_coverage}
)
LIMIT 200
"""
try:
cidb = self._ci_db()
t0 = time.monotonic()
raw = cidb.query(query, log_level="") or ""
print(
f"[find_tests] sibling-dir query: {time.monotonic()-t0:.2f}s, "
f"response={len(raw)} bytes"
)
except Exception as e:
print(f"[find_tests] sibling-dir query failed (non-fatal): {e}")
return {}
# Parse TSV: test_name (single column).
SIBLING_RC = 300
sibling_test_names: dict = {}
for row in raw.strip().splitlines():
test_name = row.strip()
if test_name:
sibling_test_names[test_name] = SIBLING_RC
total = len(sibling_test_names)
print(f"[find_tests] sibling-dir: {total} additional test candidates")
# Return the same set for every changed src dir.
return {d: dict(sibling_test_names) for d in src_dirs}
# Synthetic width for keyword-fallback tests (broader than sibling-dir tests
# since we have no coverage signal at all — just filename matching).
KEYWORD_FALLBACK_WIDTH = 30000
def _get_keyword_fallback_tests(self, changed_src_files: list) -> list:
"""
Last-resort fallback: when coverage gives zero results for one or more C++
source files, try to find stateless tests whose *filename* contains domain
keywords extracted from the changed source files.
This handles cases like `Parquet/Decoding.cpp` where the file may not appear
in the coverage database (e.g. the changed code path is rarely executed by
existing tests), but there clearly exist stateless tests named
`*parquet*.sql` / `*parquet*.sh` that should be run.
Returns a list of `(test_name, width, depth, region_test_count)` tuples
using KEYWORD_FALLBACK_WIDTH so they always rank below any direct or
sibling hit.
"""
if not changed_src_files:
return []
# Collect domain keywords from ALL changed source files.
# Include the *parent directory name* in keyword extraction so that
# generic filenames like `Decoding.cpp` inside `Parquet/` still yield
# "Parquet" as a keyword and match `*parquet*.sh` tests.
all_keywords: list = []
# Top-level src subdirectories whose names are too generic to use as
# test-name keywords (they appear in thousands of test files).
GENERIC_DIRS = frozenset({
"src", "programs", "utils", "base",
"Storages", "Interpreters", "Processors", "Functions",
"Common", "Server", "Parsers", "Analyzer", "Formats",
"Access", "IO", "Disks", "Columns", "DataTypes", "Core",
"Databases", "Backups", "Coordination", "Client",
"Daemon", "Compression", "AggregateFunctions",
"TableFunctions", "Dictionaries", "QueryPipeline",
# Sub-directories that are generic groupings (not domain names)
"Impl", "Sources", "Transforms", "Sinks",
"Utils", "Tests", "tests",
})
dir_keywords: list = [] # keywords from path components (directory names)
for f in changed_src_files:
# Keywords from basename (e.g. "CHColumnToArrowColumn.cpp" → "Arrow")
kws = self._extract_domain_keywords(f.split("/")[-1])
all_keywords.extend(kws)
# Keywords from ALL path components that are specific enough.
# e.g. src/Processors/Formats/Impl/Parquet/Decoding.cpp → "Parquet" from the
# 4th component (skipping "src", "Processors", "Formats", "Impl").
parts = f.replace("\\", "/").split("/")
for part in parts[:-1]: # all path components except the filename
if part in GENERIC_DIRS:
continue
part_kws = self._extract_domain_keywords(part + ".cpp")
for pk in part_kws:
if pk not in dir_keywords:
dir_keywords.append(pk)
# Directory keywords are prepended so they are considered first (they tend to
# be better test-name prefixes than generic filename decompositions).
for dk in dir_keywords:
if dk not in all_keywords:
all_keywords.insert(0, dk)
unique_kws = list(dict.fromkeys(all_keywords))
if not unique_kws:
return []
# Find the 0_stateless test directory relative to the repo root.
test_dir = Path("tests/queries/0_stateless")
if not test_dir.is_dir():
return []
# Pre-build list of (sql+sh) test filenames for quick iteration.
all_test_files = [
f.name for f in test_dir.iterdir()
if f.name.endswith(".sql") or f.name.endswith(".sh")
]
# For each candidate keyword, count how many tests it matches.
# Keywords matching too many tests (too generic) or zero tests
# (no signal) are discarded.
# We use two tiers:
# Specific tier (1–100 hits): high confidence, domain-specific keyword.
# At 100 we include domain words like "Arrow" (54), "Parquet" (99),
# "text_index" (86), "Variant" (64) while still excluding broad words.
# Broad tier (101–200 hits): lower confidence, include only if no
# specific keyword is available.
SPECIFIC_MAX = 100
BROAD_MAX = 200
def count_hits(kw_lower: str) -> int:
return sum(1 for f in all_test_files if kw_lower in f.lower())
specific_kws = [] # (hits, -len, kw) — few hits, long name preferred
broad_kws = []
for kw in unique_kws:
if len(kw) < 4:
continue
hits = count_hits(kw.lower())
if 1 <= hits <= SPECIFIC_MAX:
specific_kws.append((hits, -len(kw), kw))
elif SPECIFIC_MAX < hits <= BROAD_MAX:
broad_kws.append((hits, -len(kw), kw))
specific_kws.sort()
broad_kws.sort()
# Strategy: prefer directory-origin keywords (they directly name the domain,
# e.g. "Parquet" from src/.../Parquet/Decoding.cpp).
# Directory keywords come first in unique_kws (prepended above).
# We use at most one keyword to keep the test set focused.
dir_kw_set = set(dir_keywords)
# Split into directory-origin and filename-origin, each sorted by hit count.
all_ranked = specific_kws + [(h, l, kw) for h, l, kw in broad_kws]
all_ranked.sort() # fewest hits first, longer name preferred
dir_ranked = [(h, l, kw) for h, l, kw in all_ranked if kw in dir_kw_set]
file_ranked = [(h, l, kw) for h, l, kw in all_ranked if kw not in dir_kw_set]
# Pre-count actual test-file hits so we can detect zero-hit keywords and skip them.
def has_hits(kw: str) -> bool:
kw_lower = kw.lower()
return any(kw_lower in f.lower() for f in all_test_files)
candidate_kws: list = []
if dir_ranked:
# Use the best directory keyword. Skip any that match zero tests (e.g.
# "Coordination" for KeeperStorage.cpp — no test names contain "coordination").
# Fall back through the list until one has hits; if none do, continue to
# file_ranked below.
usable_dir = [(h, l, kw) for h, l, kw in dir_ranked if has_hits(kw)]
if usable_dir:
candidate_kws = [usable_dir[0][2]]
# Also add a specific file keyword to narrow results further.
if file_ranked and file_ranked[0][0] <= SPECIFIC_MAX:
candidate_kws.append(file_ranked[0][2])
else:
# All directory keywords have 0 test hits; fall through to file_ranked.
dir_ranked = []
if not dir_ranked:
if not file_ranked:
return []
# No useful directory keyword; use the most specific file keyword available.
# Prefer the specific tier (≤SPECIFIC_MAX), but fall back to the broad tier
# (≤BROAD_MAX) when nothing specific exists — e.g. "Keeper" (156 hits) is
# still a meaningful domain signal for KeeperStorage.cpp changes.
file_specific = [x for x in file_ranked if x[0] <= SPECIFIC_MAX]
if file_specific:
candidate_kws = [file_specific[0][2]]
elif broad_kws:
candidate_kws = [broad_kws[0][2]]
else:
return []
# Collect test names matching any of the selected keywords.
# Track the best (most specific) keyword that matched each test so we
# can weight by keyword specificity: rare keywords → narrow effective
# width → higher score. Among ties, prefer newer tests (higher number).
#
# Scoring: score = PASS_WEIGHT_KEYWORD / (KEYWORD_FALLBACK_WIDTH × kw_hits)
# kw_hits=1 → width=50000 score=1e-6 (very specific)
# kw_hits=10 → width=500000 score=1e-7
# kw_hits=100→ width=5000000 score=1e-8 ≈ MIN_SCORE (barely survives)
# Tests from broad keywords (hits>100) score below MIN_SCORE and are
# dropped at the ranking stage — no hard per-keyword filter needed.
MAX_KEYWORD_TESTS = 50 # hard cap: keyword signal is weak, keep it tight
matched_tests: dict = {} # normalised_name → (kw_hits, raw_name)
for kw in candidate_kws:
kw_lower = kw.lower()
kw_hits = count_hits(kw_lower)
for fname in all_test_files:
if kw_lower in fname.lower():
base = os.path.splitext(fname)[0]
tname = base + "."
if tname not in matched_tests or kw_hits < matched_tests[tname][0]:
matched_tests[tname] = (kw_hits, kw)
if not matched_tests:
return []
# Sort: most-specific keyword first (fewest hits), then newest test first
# (higher 5-digit prefix tends to be closer to the recently changed code).
def _kw_sort(item):
tname, (kw_hits, _kw) = item
m = re.match(r'(\d{5})', os.path.basename(tname).lstrip('./'))
test_num = int(m.group(1)) if m else 0
return (kw_hits, -test_num)
sorted_tests = sorted(matched_tests.items(), key=_kw_sort)[:MAX_KEYWORD_TESTS]
kws_used = sorted({kw for _, (_, kw) in sorted_tests})
print(
f"[find_tests] keyword-fallback: {len(sorted_tests)} tests "
f"matching keywords {kws_used} (from {len(matched_tests)} candidates)"
)
return [
(tname, self.KEYWORD_FALLBACK_WIDTH * kw_hits, 255, 1, self.PASS_WEIGHT_KEYWORD)
for tname, (kw_hits, _kw) in sorted_tests
]
def get_changed_or_new_tests_with_info(self):
tests = sorted(self.get_changed_tests())
info = f"Found {len(tests)} changed or new tests:\n"
for test in tests[:200]:
info += f" - {test}\n"
return tests, Result(
name="tests that were changed or added",
status=Result.Status.OK,
info=info,
)
def get_previously_failed_tests_with_info(self):
try:
tests = self.get_previously_failed_tests()
except Exception as e:
print(
f"WARNING: Failed to get previously failed tests (best effort): {e}",
file=sys.stderr,
)
tests = []
# TODO: add job name to the result.info
info = f"Found {len(tests)} previously failed tests:\n"
for test in tests[:200]:
info += f" - {test}\n"
return tests, Result(
name="tests that failed in previous runs",
status=Result.Status.OK,
info=info,
)
@staticmethod
def _parse_diff_lines(diff_text: str) -> list:
"""
Parse a unified diff and return `(filename, line_no)` tuples for every changed line.
Line numbers are old-file positions (from the `-a,b` hunk header), which match the
master-build coverage data stored in CIDB. For `-` lines the old-file line number
is used directly; for `+` lines (pure additions) the current old-file position
(insertion point) is used so that the surrounding function is still found in CIDB.
"""
changed: set = set()
current_file = None
old_line = 0
in_hunk = False
for line in diff_text.splitlines():
if line.startswith("--- "):
in_hunk = False
elif line.startswith("+++ b/"):
current_file = line[6:]
in_hunk = False
elif line.startswith("@@ ") and current_file:
m = re.search(r"-(\d+)", line)
old_line = int(m.group(1)) if m else 0
in_hunk = True
elif in_hunk:
if line.startswith("-"):
changed.add((current_file, old_line))
old_line += 1
elif line.startswith("+"):
changed.add((current_file, old_line)) # insertion point in old file
else:
old_line += 1 # context line
return list(changed)
@staticmethod
def _parse_diff_hunk_ranges(diff_text: str) -> dict:
"""
Parse a unified diff and return per-file hunk boundary ranges.
Returns `{filename: [(hunk_start, hunk_end), ...]}` where each tuple is the
old-file start and end line of a hunk. These ranges are used to expand the
CIDB range query beyond just the actually-changed lines so that coverage
regions at unchanged context lines adjacent to the change are also captured.
Example: a hunk `@@ -331,6 ... @@` contains 6 old-file lines (331-336).
If CIDB only records a coverage point at line 332 (the first statement
after a branch), a query using only the changed lines {331} would miss it,
but including the full hunk range [331, 336] finds it.
"""
hunks: dict = {} # filename -> list of (start, end)
current_file = None
for line in diff_text.splitlines():
if line.startswith("--- "):
pass
elif line.startswith("+++ b/"):
current_file = line[6:]
elif line.startswith("@@ ") and current_file:
# @@ -old_start,old_count +new_start,new_count @@
m = re.search(r"-(\d+)(?:,(\d+))?", line)
if m:
start = int(m.group(1))
count = int(m.group(2)) if m.group(2) is not None else 1
end = start + count - 1
hunks.setdefault(current_file, []).append((start, end))
return hunks
def get_diff_text(self) -> str:
"""Fetch the PR diff text (cached on self._diff_text after first call).
CI containers have no .git directory. Use the GitHub API via curl.
For public repos no auth is needed; for private repos GITHUB_TOKEN is used.
"""
if hasattr(self, '_diff_text') and self._diff_text is not None:
return self._diff_text
assert self.info.pr_number > 0, "Diff fetching applicable for PRs only"
repo = self.info.repo_name or "Datastore/Datastore"
if self.info.is_local_run:
self._diff_text = Shell.get_output(
f"gh pr diff {self.info.pr_number} --repo {repo}"
)
else:
# Use GitHub REST API to get the diff. Works for both public and private
# repos; private repos require GITHUB_TOKEN (available in Actions env).
token = os.environ.get("GITHUB_TOKEN", "")
auth = f'-H "Authorization: Bearer {token}"' if token else ""
self._diff_text = Shell.get_output(
f"curl -sSf {auth} "
f"-H 'Accept: application/vnd.github.v3.diff' "
f"'https://api.github.com/repos/{repo}/pulls/{self.info.pr_number}'"
)
return self._diff_text
def get_changed_lines_from_diff(self):
"""
Return changed lines from the PR diff.
In CI fetches the diff from GitHub API; for local runs uses `gh pr diff`.
"""
assert self.info.pr_number > 0, "Find tests by diff applicable for PRs only"
return self._parse_diff_lines(self.get_diff_text())
# min_depth stores the raw entry-counter call count (capped at 254; 255 = not tracked).
# A low call count means the function was called rarely during the test → more specific.
# Tests where a function was called ≤ this many times get the "direct" tier bonus.
DIRECT_CALL_MAX_DEPTH = 10
def get_most_relevant_tests(self):
"""
1. Gets changed lines from the PR diff.
2. Queries `checks_coverage_lines` for tests covering those lines.
3. Ranks tests by a composite score with three tiers:
- Tier A (best): narrow-region hit AND shallow call depth (≤ DIRECT_CALL_MAX_DEPTH)
- Tier B: narrow-region hit, deep call path
- Tier C (worst): only broad-region hits
Within each tier tests are ordered by width_score = sum(1/region_width),
which rewards tests that cover many of the changed lines through narrow regions.
The min_depth for each test is the shallowest call depth seen across all
coverage entries for that test (255 = depth not tracked; treated as deep).
4. Returns the ranked list and a `Result` with info about the findings.
"""
changed_lines = self.get_changed_lines_from_diff()
# Also parse hunk boundaries so the CIDB query covers context lines within
# each hunk (not just the actually-changed lines). See _parse_diff_hunk_ranges.
hunk_ranges = self._parse_diff_hunk_ranges(self.get_diff_text())
line_to_tests = self.get_tests_by_changed_lines(changed_lines,
hunk_ranges=hunk_ranges)
# Keyword-based fallback: if no coverage results were found for any
# C++ source file, try to find tests by matching the source filename
# against stateless test names. This catches files that are rarely
# (or never) reached by the current coverage suite — e.g.
# Parquet/Decoding.cpp when the change is a new edge-case code path.
# The fallback is only activated when the primary coverage query returned
# zero tests for a C++ file so it does not dilute high-confidence hits.
COVERAGE_TRACKED_PREFIXES = ("src/", "programs/", "utils/", "base/")
cpp_with_zero_coverage = [
f for f, ln in changed_lines
if any(f.startswith(p) for p in COVERAGE_TRACKED_PREFIXES)
and (f.endswith(".cpp") or f.endswith(".h"))
and f not in self.SHARED_REGISTRY_FILES
and not any(pairs for (ff, _), pairs in line_to_tests.items() if ff == f)
]
# Deduplicate file list.
cpp_with_zero_coverage = list(dict.fromkeys(cpp_with_zero_coverage))
if cpp_with_zero_coverage:
# Run per-file so each file's domain keywords are matched independently.
seen_fb: set = set()
fallback_quads: list = []
for f in cpp_with_zero_coverage:
for q in self._get_keyword_fallback_tests([f]):
if q[0] not in seen_fb:
seen_fb.add(q[0]); fallback_quads.append(q)
if fallback_quads:
# Inject fallback tests into the first line of each zero-coverage
# C++ file so the scorer sees them without duplication.
injected: set = set()
for f in cpp_with_zero_coverage:
for (ff, ln), pairs in line_to_tests.items():
if ff == f and (ff, ln) not in injected:
pairs.extend(fallback_quads)
injected.add((ff, ln))
break
# Supplementary keyword pass: even for C++ files WITH direct coverage,
# add tests whose *filename* contains domain keywords from the changed
# file. This catches broad regression tests (e.g. 01273_arrow.sh for
# CHColumnToArrowColumn.cpp) that exercise the same domain through
# higher-level call paths not captured in line coverage — the same tests
# the old symbol-based algo found via checks_coverage_inverted.
# Uses PASS_WEIGHT_KEYWORD (lowest weight) so they rank below all
# coverage-backed hits but are still present for the scorer.
cpp_with_coverage = list(dict.fromkeys(
f for f, ln in changed_lines
if any(f.startswith(p) for p in COVERAGE_TRACKED_PREFIXES)
and (f.endswith(".cpp") or f.endswith(".h"))
and f not in cpp_with_zero_coverage
and f not in self.SHARED_REGISTRY_FILES
and any(pairs for (ff, _), pairs in line_to_tests.items() if ff == f)
))
# Always run the supplementary keyword pass: keyword tests get PASS_WEIGHT_KEYWORD
# (the lowest weight), so they rank below all coverage-backed hits but remain
# present in the scoring pool. This ensures domain-specific tests (e.g.
# 03444_analyzer_resolve_alias_columns for QueryAnalyzer.cpp) are never
# silently dropped simply because coverage produced many generic candidates.
if cpp_with_coverage:
# Run keyword fallback per changed file so each file's domain keywords
# are matched independently. Calling with all files at once collapses
# to a single "best" keyword and silently drops domains from other files
# (e.g. for a PR touching ArrowColumnToCHColumn.cpp + MsgPackRowInputFormat.cpp
# the combined call picks "Pack" as more specific, missing all Arrow tests).
seen_supplement: set = set()
all_supplement_quads: list = []
for f in cpp_with_coverage:
per_file_quads = self._get_keyword_fallback_tests([f])
for q in per_file_quads:
if q[0] not in seen_supplement:
seen_supplement.add(q[0])
all_supplement_quads.append(q)
if all_supplement_quads:
injected_s: set = set()
for f in cpp_with_coverage:
for (ff, ln), pairs in line_to_tests.items():
if ff == f and (ff, ln) not in injected_s:
existing = {q[0] for q in pairs}
pairs.extend(
q for q in all_supplement_quads if q[0] not in existing
)
injected_s.add((ff, ln))
break
# Store keyword tests for the guarantee injection after ranking.
self._keyword_guarantee = [q[0] for q in all_supplement_quads]
# Accumulate per-test scores across all changed lines.
# line_to_tests values are lists of
# (test_name, region_width, min_depth, region_test_count, pass_weight).
#
# Scoring combines four signals:
# pass_weight — per-pass multiplier (1.0 direct, 0.3 indirect, 0.1 sibling, 0.05 keyword)
# width — narrow regions (few lines) are more precise → weight 1/width
# region_test_count — regions covered by few tests are more specific → weight 1/region_test_count
# min_depth — low call count means this test specifically exercised this path
#
# Final score = sum(pass_weight / (width × region_test_count)) across all matched changed lines.
width_score: dict = {} # test -> sum(pass_weight/(width×region_test_count))
has_narrow_hit: dict = {} # test -> bool: any covering region is narrow AND exclusive
min_depth_seen: dict = {} # test -> minimum call count across all hits
for quads in line_to_tests.values():
for t, width, depth, region_test_count, pass_weight in quads:
rc = max(1, region_test_count)
width_score[t] = width_score.get(t, 0.0) + pass_weight / (width * rc)
has_narrow_hit[t] = has_narrow_hit.get(t, False) or (
width <= self.NARROW_REGION_MAX_LINES
)
if depth < min_depth_seen.get(t, 256):
min_depth_seen[t] = depth
def sort_key(t):
narrow = has_narrow_hit[t]
depth = min_depth_seen.get(t, 255)
direct = narrow and depth <= self.DIRECT_CALL_MAX_DEPTH
# Tier A: direct narrow hit → 0; Tier B: indirect narrow → 1; Tier C: broad → 2
tier = 0 if direct else (1 if narrow else 2)
return (tier, -width_score[t])
# Sort: tier first (A < B < C), then by width score descending within tier.
# Use score-based filtering instead of a hard count cap. The old
# `[:1000]` cut threw away low-scoring but genuinely relevant tests
# from infrastructure files (Context.cpp, ProcessList.cpp) whose
# regions have high region_test_count and therefore low per-line
# scores. A minimum score threshold keeps the result set bounded
# without an arbitrary count limit.
MIN_SCORE = 1e-8 # floor: tests scoring below this have negligible signal
MAX_OUTPUT_TESTS = 250 # hard cap: targeted runs must stay focused
# Dynamic ratio floor: drop tests whose score is more than MAX_SCORE_RATIO times
# weaker than the best evidence. This supersedes per-pass suppression guards —
# when strong direct hits exist (e.g. rc=37 on a SAMPLE-specific line, score=0.027)
# weak signals like broad-tier2 (score=8e-7, ratio=34000x) or distant indirect-call
# results are naturally excluded without any explicit per-pass logic.
# A ratio of 1000 covers ~two jumps in signal quality (direct→indirect→keyword)
# while keeping genuinely related tests from weaker passes when direct is sparse.
MAX_SCORE_RATIO = 3000
# Cap the floor so that rc=1 changed lines (top_score=1.0) don't push
# effective_min to 1e-3 and cut indirect / sparse-file expansion results.
# Equivalent to treating any top_score above 1/1000 the same as 1/1000.
# Using 1e-6 recovers tests that cover changed .h files via non-overlapping
# regions (header expansion) at slightly higher rc values.
# score = 1.0/(SIBLING_DIR_WIDTH × rc) = 1.0/(10000 × rc)
# rc=10: score=1e-5, rc=100: score=1e-6 — both now pass the filter.
MAX_EFFECTIVE_MIN = 1e-6
# Compute top_score from direct-pass entries ONLY (pw >= PASS_WEIGHT_DIRECT = 1.0).
# Hunk-context hits (PASS_WEIGHT_HUNK_CONTEXT = 0.35) are excluded so that their
# score doesn't inflate top_score and suppress lower-scoring but genuinely
# relevant tests (e.g. sparse-file expansion results at high-rc regions).
# When there are no direct hits at all (pr changes code with zero CIDB coverage),
# top_score stays 0.0 and effective_min defaults to MIN_SCORE so ALL tests survive.
top_score = 0.0
for quads in line_to_tests.values():
for t, width, depth, region_test_count, pass_weight in quads:
if pass_weight >= self.PASS_WEIGHT_DIRECT:
rc = max(1, region_test_count)
s = pass_weight / (width * rc)
if s > top_score:
top_score = s
effective_min = min(MAX_EFFECTIVE_MIN, max(MIN_SCORE, top_score / MAX_SCORE_RATIO))
all_ranked = sorted(width_score, key=sort_key)
ranked = [t for t in all_ranked if width_score[t] >= effective_min][:MAX_OUTPUT_TESTS]
# Broad-tier2 guarantee: ensure the top-N high-cov_regions broad-tier2 tests
# are always in the output, even if the score-ranked list is already at the cap.
#
# Problem without this: when ALL evidence is from broad-tier2 regions (e.g.
# HTTPConnectionPool.cpp where the changed lines only have rc=5918 coverage),
# 5918+ tests all get nearly equal scores. The cap cuts them to MAX_OUTPUT_TESTS
# in an effectively random order, potentially omitting the most relevant tests
# (those with highest cov_regions, covering more of the changed hunks).
#
# Fix: after ranking, replace the last guarantee.size tests in the ranked list
# with the top-cov_regions tests from the guarantee that aren't already present.
# This ensures the most-covering broad-tier2 tests are always included.
# Broad-tier2 guarantee: if the cap cut off high-cov_regions broad-tier2 tests,
# append the top few (by cov_regions) that didn't make it — but only up to the cap.
broad_guarantee = getattr(self, '_broad_tier2_guarantee', [])
if broad_guarantee and len(ranked) < MAX_OUTPUT_TESTS:
ranked_set = set(ranked)
slots = MAX_OUTPUT_TESTS - len(ranked)
extra = [t for t in broad_guarantee if t not in ranked_set][:slots]
if extra:
ranked = ranked + extra
print(f"[find_tests] broad-tier2 guarantee: +{len(extra)} high-cov_regions tests added")
# Keyword guarantee: always include top keyword-matched tests even when their
# score is too low to survive the 300-cap against many direct hits.
# Replace the last N tail items with keyword tests not already present.
# This ensures domain-specific tests (e.g. 03444_analyzer_resolve_alias_columns
# for QueryAnalyzer.cpp changes) are never silently dropped.
keyword_guarantee = getattr(self, '_keyword_guarantee', [])
if keyword_guarantee:
ranked_set = set(ranked)
kw_extra = [t for t in keyword_guarantee if t not in ranked_set]
if kw_extra:
# Replace tail items to stay within MAX_OUTPUT_TESTS
n = min(len(kw_extra), MAX_OUTPUT_TESTS)
ranked = ranked[: MAX_OUTPUT_TESTS - n] + kw_extra[:n]
print(f"[find_tests] keyword guarantee: +{len(kw_extra[:n])} domain tests injected")
narrow_count = sum(1 for t in ranked if has_narrow_hit[t])
direct_count = sum(
1 for t in ranked
if has_narrow_hit[t] and min_depth_seen.get(t, 255) <= self.DIRECT_CALL_MAX_DEPTH
)
broad_count = len(ranked) - narrow_count
info = "Tests found for lines:\n"
if not line_to_tests:
info += " No changed lines found in diff\n"
else:
for (file_, line_), pairs in line_to_tests.items():
if pairs:
info += f" {file_}:{line_} -> {len(pairs)} tests\n"
scored_total = len(width_score)
filtered_out = scored_total - len(ranked)
info += (
f"Total unique tests: {len(ranked)} "
f"({direct_count} direct-narrow, {narrow_count - direct_count} indirect-narrow, "
f"{broad_count} broad"
)
if filtered_out > 0:
info += f"; {filtered_out} below score threshold"
info += ")\n"
if ranked:
top = ranked[0]
d = min_depth_seen.get(top, 255)
tier = ("direct-narrow" if has_narrow_hit[top] and d <= self.DIRECT_CALL_MAX_DEPTH
else "narrow" if has_narrow_hit[top] else "broad")
info += f"Top test: {top} (score={width_score[top]:.6f}, {tier}, depth={d})\n"
bottom = ranked[-1]
info += f"Bottom test: {bottom} (score={width_score[bottom]:.6f})\n"
return ranked, Result(
name="tests found by coverage", status=Result.Status.OK, info=info
)
def get_all_relevant_tests_with_info(self):
# Use a list to preserve insertion order and a seen set to deduplicate.
# Priority: changed/new tests first, then previously failed, then
# coverage-ranked tests (most changed lines covered first).
seen: set = set()
ranked: list = []
results = []
def add_tests(new_tests):
for t in new_tests:
if t not in seen:
seen.add(t)
ranked.append(t)
# Changed/new tests are already covered by the flaky check — skip them
# in the targeted check to avoid duplication.
# if self.job_type == self.STATELESS_JOB_TYPE:
# changed_tests, result = self.get_changed_or_new_tests_with_info()
# add_tests(changed_tests)
# results.append(result)
previously_failed_tests, result = self.get_previously_failed_tests_with_info()
add_tests(previously_failed_tests)
results.append(result)
# TODO: Add coverage support for Integration tests
if self.job_type == self.STATELESS_JOB_TYPE:
try:
covering_tests, result = self.get_most_relevant_tests()
add_tests(covering_tests)
results.append(result)
except Exception as e:
print(
f"WARNING: Failed to get coverage-based tests (best effort): {e}",
file=sys.stderr,
)
results.append(
Result(
name="tests found by coverage",
status=Result.Status.OK,
info=f"Skipped: {e}",
)
)
raise
return ranked, Result(
name="Fetch relevant tests",
status=Result.Status.OK,
info=f"Found {len(ranked)} relevant tests",
results=results,
)
if __name__ == "__main__":
# local run: use the same pipeline as CI (get_all_relevant_tests_with_info)
parser = argparse.ArgumentParser(
description="List tests covering changed lines for a PR by querying the coverage database."
)
parser.add_argument("pr", help="Pull request number")
parser.add_argument(
"--coverage-only",
action="store_true",
help="Run only the coverage-based pass (get_most_relevant_tests), skip changed-file "
"and previously-failed passes. Uses one fewer GitHub API call — useful for eval.",
)
parser.add_argument(
"--diff-file",
default=None,
help="Path to a pre-fetched unified diff file. When provided, "
"get_changed_lines_from_diff reads from this file instead of calling gh.",
)
args = parser.parse_args()
class InfoLocalTest:
pr_number = int(args.pr)
is_local_run = True
job_name = "Stateless"
info = InfoLocalTest()
targeting = Targeting(info)
# If a pre-fetched diff file is provided, monkey-patch get_diff_text so both
# get_changed_lines_from_diff and get_most_relevant_tests read from the file
# rather than fetching the diff.
if args.diff_file:
diff_text = Path(args.diff_file).read_text()
targeting._diff_text = diff_text
if args.coverage_only:
ranked, result = targeting.get_most_relevant_tests()
else:
ranked, result = targeting.get_all_relevant_tests_with_info()
print(f"\nAll selected tests ({len(ranked)}):")
for test in ranked:
print(f" {test}")
print(f"\nFound {len(ranked)} relevant tests")