Files
rustfs/scripts/find_assertless_tests.py
T
overtrue 695eb89da7 test: assert four leaf-crate smoke tests, and two more census fixes
Two more census heuristics, both verified by bisecting the candidate count so a "fix" that widened the queue could not slip through:

- any `assert*!` macro counts as verification, not just the three built-ins — `assert_fields_bound!` in `protos` was being missed. The pattern deliberately stays a substring match: an earlier attempt anchored it with `\b`, which silently stopped matching prefixed macros like `const_assert!` and pushed the queue from 32 to 55 before the count caught it.
- `let _ = Type::<T>::method;` is a signature guard, the same as the already-recognised nested-fn form. That is `iam`'s deprecated-API test.

Tree-wide candidates go 32 to 30 from the script alone, then to 26 with the four tests below.

`detect_storage_media`'s two tests only checked that the call did not panic, over a `match` whose arms were all empty. What the machine reports depends on the machine, but two rules do not: an override wins over probing, including when probing is disabled, and disabled probing reports `Unknown` rather than guessing. A third test keeps the platform call and asserts it returns a known variant *and* the same one twice — a probe that flapped would make the scheduler's profile depend on when it asked.

`runtime_facade_stops_empty_replay_workers` called the stop path and asserted nothing. It now checks the worker list is empty afterwards and that a second call stays harmless, which is what shutdown paths actually do.

`test_mask_never_recurses_for_any_variant` discarded every mask. Termination is still the property under test — a regression overflows the stack rather than failing an assertion — but the masks are now collected and checked, so the loop cannot fold away and a variant that starts returning an empty mask is caught too.

Two known false positives are left in the queue rather than chased: `utils/src/string.rs:942` does assert, but its input string `"{1...2}}"` unbalances the scanner's brace counter and truncates the body before the assertion. Making the counter literal-aware needs a lexer that understands raw strings — a first attempt desynchronised on `r#"{"invalid": json}"#` and pushed the queue to 120, so it was backed out. `s3select-api:379` declares a nested exhaustive-match fn and never calls it; recognising that shape without hiding genuinely empty bodies needs more care than it is worth today.

Refs backlog#1836
2026-08-19 10:32:19 +08:00

162 lines
6.3 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Census of assertion-less tests (rustfs/backlog#1836 PR3).
Flags `#[test]` / `#[tokio::test]` functions whose bodies contain no
verification signal: no assert!/assert_eq!/assert_ne!/panic! macro, no
`.expect(`/`.unwrap(`, no `?` operator, no `#[should_panic]`, and no
`insta` snapshot / proptest / matches! usage. Such a test is green no
matter what the code under test does.
This is a heuristic REVIEW QUEUE, not a lint: a hit still needs human
reading before it is fixed or deleted, because assertions may live in a
called helper. Known false-positive classes are excluded up front:
- `#[test_case(...)]`-driven functions (the values are the assertion's
parameters; the assert lives in the shared body — still scanned, but a
body that asserts is not flagged anyway; the exclusion covers wrappers
that only delegate to a suite runner).
- Functions whose body calls a helper *named* like a shared check or suite
runner: an `assert_`/`verify_`/`check_`/`expect_`/`ensure_`/`run_` prefix,
or a `_case`/`_cases`/`_harness`/`_roundtrip` suffix. The name must carry
the token as its own leading or trailing segment — matching it anywhere
inside the identifier hid whole test bodies behind an unrelated domain
call such as `record_get_object_bitrot_verify_duration(..)`.
- Functions whose body only defines an unused inner `fn _name(..)`: that is
the compile-time shape check (exhaustive match, signature pin), where the
type system is the assertion.
Usage:
scripts/find_assertless_tests.py [path ...] # default: crates rustfs/src
Exit code is always 0; the output is the queue.
"""
import re
import sys
from pathlib import Path
VERIFY_SIGNALS = re.compile(
r"assert[a-z0-9_]*!|debug_assert|panic!\(|\.expect\(|\.unwrap\(|"
r"unreachable!|matches!\(|insta::|proptest!|\.await\?|\)\?|\?;|should_panic"
)
DELEGATION = re.compile(
r"\b(?:assert|verify|check|expect|ensure|run)_[a-z0-9_]*(?:::<[^>]*>)?\s*\(|"
r"\b[a-z0-9_]+_(?:case|cases|harness|roundtrip|round_trip)(?:::<[^>]*>)?\s*\("
)
# A body whose whole content is one call delegates by construction, whatever the
# callee is named: `run(DurabilityMode::Strict).await` and
# `aborting_encode_drops_blocked_producer(EncodePipeline::Vec).await` both hand
# every assertion to a shared harness.
SINGLE_CALL_BODY = re.compile(
r"\A\s*[a-zA-Z_][a-zA-Z0-9_:]*(?:::<[^>]*>)?\s*\([^;]*\)\s*(?:\.await\s*)?;?\s*\Z",
re.S,
)
# A nested `fn` that is only bound and discarded is a signature guard: the type
# system is the assertion, exactly like the `fn _name()` form below.
SIGNATURE_GUARD = re.compile(r"\bfn\s+[a-zA-Z0-9_]+\s*(?:<[^>]*>)?\s*\([^;]*\)[^;]*\{", re.S)
DISCARDED_BINDING = re.compile(r"\blet\s+_\s*=\s*[a-zA-Z_][a-zA-Z0-9_]*\s*;")
# `let _ = Type::<T>::method;` — a path item referenced but never called can only
# be a signature guard; the call form (`let _ = x.foo();`) is excluded by the
# absence of parens before the semicolon.
DISCARDED_PATH_ITEM = re.compile(r"\blet\s+_\s*=\s*[a-zA-Z_][a-zA-Z0-9_]*(?:::(?:<[^>]*>|[a-zA-Z_][a-zA-Z0-9_]*))+\s*;")
COMPILE_TIME_CHECK = re.compile(r"\bfn\s+_[a-zA-Z0-9_]*\s*(?:<[^>]*>)?\s*\(")
TEST_ATTR = re.compile(r"#\[(?:tokio::)?test[\](]")
TEST_CASE_ATTR = re.compile(r"#\[test_case")
FN_LINE = re.compile(r"^\s*(?:pub\s+)?(?:async\s+)?fn\s+([a-zA-Z0-9_]+)")
def extract_body(text: str) -> str:
"""Return what is between the outermost braces of a scanned function."""
start = text.find("{")
end = text.rfind("}")
if start == -1 or end <= start:
return text
return text[start + 1 : end]
def scan_file(path: Path):
try:
lines = path.read_text(encoding="utf-8").split("\n")
except (UnicodeDecodeError, OSError):
return
i = 0
while i < len(lines):
if not TEST_ATTR.search(lines[i]):
i += 1
continue
# collect the whole attribute block (may include #[serial], #[test_case], ...)
attrs = []
j = i
while j < len(lines) and (lines[j].strip().startswith("#[") or lines[j].strip().startswith("//")):
attrs.append(lines[j])
j += 1
if j >= len(lines):
break
m = FN_LINE.match(lines[j])
if not m:
i = j + 1
continue
name = m.group(1)
if any(TEST_CASE_ATTR.search(a) for a in attrs):
i = j + 1
continue
# brace-match the body
depth = 0
begun = False
body = []
k = j
while k < len(lines):
for ch in lines[k]:
if ch == "{":
depth += 1
begun = True
elif ch == "}":
depth -= 1
body.append(lines[k])
if begun and depth <= 0:
break
k += 1
text = "\n".join(body)
# The attribute block carries verification too: `#[should_panic(expected
# = "...")]` makes the panic message the assertion.
attr_text = "\n".join(attrs)
inner = extract_body(text)
delegates = (
DELEGATION.search(text)
or SINGLE_CALL_BODY.match(inner)
or (SIGNATURE_GUARD.search(inner) and DISCARDED_BINDING.search(inner))
or DISCARDED_PATH_ITEM.search(inner)
)
if not VERIFY_SIGNALS.search(text) and not VERIFY_SIGNALS.search(attr_text) and not delegates and not COMPILE_TIME_CHECK.search(text):
print(f"{path}:{j + 1}: {name}")
i = k + 1
def main():
roots = [Path(p) for p in (sys.argv[1:] or ["crates", "rustfs/src"])]
for root in roots:
for path in sorted(root.rglob("*.rs")):
if "target" in path.parts:
continue
scan_file(path)
if __name__ == "__main__":
main()