use std::collections::{BTreeSet, HashSet};
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use rustpython_ast::Visitor;
use rustpython_parser::lexer::lex;
use rustpython_parser::{ast, Mode, Parse, Tok};
use syn::visit::{self, Visit};
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum Language {
#[value(name = "python")]
Python,
#[value(name = "typescript")]
TypeScript,
#[value(name = "rust")]
Rust,
}
impl Language {
pub(crate) fn tracks(self, path: &Path) -> bool {
match self {
Language::Python => has_extension(path, &["py"]),
Language::TypeScript => {
has_extension(path, &["ts", "tsx", "mts", "cts"]) && !is_declaration(path)
}
Language::Rust => false,
}
}
pub(crate) fn is_test(self, path: &Path) -> bool {
match self {
Language::Python => stem_of(path).ends_with("_test"),
Language::TypeScript => {
let name = file_name_of(path);
name.ends_with(".test.ts")
|| name.ends_with(".test.tsx")
|| name.ends_with(".test.mts")
|| name.ends_with(".test.cts")
}
Language::Rust => false,
}
}
pub(crate) fn is_support(self, path: &Path) -> bool {
match self {
Language::Python => file_name_of(path) == "conftest.py",
Language::TypeScript | Language::Rust => false,
}
}
pub(crate) fn is_subject(self, source: &str, path: &Path) -> bool {
match self {
Language::Python => python_has_behavior(source),
Language::TypeScript => crate::ts::has_behavior(source, path),
Language::Rust => rust_has_behavior(source),
}
}
pub(crate) fn same_code(self, base: &str, head: &str, path: &Path) -> bool {
match self {
Language::Python => python_same_code(base, head),
Language::TypeScript => crate::ts::same_code(base, head, path),
Language::Rust => false,
}
}
pub(crate) fn expected_test_path(self, source: &Path) -> PathBuf {
match self {
Language::Python => source.with_file_name(format!("{}_test.py", stem_of(source))),
Language::TypeScript => {
source.with_file_name(format!("{}.test.{}", stem_of(source), extension_of(source)))
}
Language::Rust => source.to_path_buf(),
}
}
}
pub fn missing_unit_tests(
root: impl AsRef<Path>,
language: Language,
exempt: &BTreeSet<String>,
) -> Result<Vec<PathBuf>> {
let root = root.as_ref();
let mut files = Vec::new();
collect_files(root, language, &mut files)?;
let manifest = match language {
Language::Python => Some("pyproject.toml"),
Language::TypeScript => Some("package.json"),
Language::Rust => None,
};
if let Some(tests) = manifest.and_then(|m| crate::tiers::suite_tests_dir(root, m)) {
files.retain(|file| !file.starts_with(&tests));
}
let present: HashSet<&Path> = files.iter().map(PathBuf::as_path).collect();
let mut orphans: Vec<PathBuf> = Vec::new();
for source in &files {
if language.is_test(source) || language.is_support(source) {
continue;
}
if present.contains(language.expected_test_path(source).as_path()) {
continue;
}
let contents = std::fs::read_to_string(source)
.with_context(|| format!("reading source file `{}`", source.display()))?;
if !language.is_subject(&contents, source) {
continue;
}
let relative = source
.strip_prefix(root)
.unwrap_or(source)
.to_string_lossy()
.replace('\\', "/");
if exempt.contains(&relative) {
continue;
}
orphans.push(source.clone());
}
orphans.sort();
Ok(orphans)
}
pub(crate) fn collect_files(dir: &Path, language: Language, out: &mut Vec<PathBuf>) -> Result<()> {
let entries =
std::fs::read_dir(dir).with_context(|| format!("reading directory `{}`", dir.display()))?;
for entry in entries {
let path = crate::walk::dir_entry(entry, dir)?.path();
if path.is_dir() {
collect_files(&path, language, out)?;
} else if language.tracks(&path) {
out.push(path);
}
}
Ok(())
}
pub fn missing_inline_tests(
root: impl AsRef<Path>,
exempt: &BTreeSet<String>,
) -> Result<Vec<PathBuf>> {
let root = root.as_ref();
let mut files = Vec::new();
collect_rust_source_files(root, &mut files)?;
files.sort();
let mut orphans = Vec::new();
for file in &files {
let source = std::fs::read_to_string(file)
.with_context(|| format!("reading source file `{}`", file.display()))?;
let ast = syn::parse_file(&source)
.map_err(|err| anyhow!("parsing `{}`: {err}", file.display()))?;
let mut visitor = PresenceVisitor::default();
visitor.visit_file(&ast);
if !visitor.has_testable_fn || visitor.has_test_module {
continue;
}
let relative = file
.strip_prefix(root)
.unwrap_or(file)
.to_string_lossy()
.replace('\\', "/");
if exempt.contains(&relative) {
continue;
}
orphans.push(file.clone());
}
Ok(orphans)
}
pub(crate) fn collect_rust_source_files(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
let entries =
std::fs::read_dir(dir).with_context(|| format!("reading directory `{}`", dir.display()))?;
for entry in entries {
let path = crate::walk::dir_entry(entry, dir)?.path();
if path.is_dir() {
let skip = matches!(
path.file_name().and_then(|name| name.to_str()),
Some("tests" | "benches" | "examples" | "target")
);
if !skip {
collect_rust_source_files(&path, out)?;
}
} else if has_extension(&path, &["rs"]) && file_name_of(&path) != "build.rs" {
out.push(path);
}
}
Ok(())
}
#[derive(Default)]
struct PresenceVisitor {
test_depth: usize,
has_testable_fn: bool,
has_test_module: bool,
}
impl<'ast> Visit<'ast> for PresenceVisitor {
fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
let is_test = crate::isolation::has_cfg_test(&node.attrs);
if is_test {
self.has_test_module = true;
self.test_depth += 1;
}
visit::visit_item_mod(self, node);
if is_test {
self.test_depth -= 1;
}
}
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
if self.test_depth == 0
&& !crate::isolation::has_cfg_test(&node.attrs)
&& !is_logic_free_main(node)
{
self.has_testable_fn = true;
}
visit::visit_item_fn(self, node);
}
fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
if self.test_depth == 0 {
self.has_testable_fn = true;
}
visit::visit_impl_item_fn(self, node);
}
fn visit_trait_item_fn(&mut self, node: &'ast syn::TraitItemFn) {
if self.test_depth == 0 && node.default.is_some() {
self.has_testable_fn = true;
}
visit::visit_trait_item_fn(self, node);
}
}
fn is_logic_free_main(node: &syn::ItemFn) -> bool {
if node.sig.ident != "main" {
return false;
}
let [syn::Stmt::Expr(syn::Expr::Call(call), _)] = &node.block.stmts[..] else {
return false;
};
call.args.is_empty() && matches!(*call.func, syn::Expr::Path(_))
}
fn rust_has_behavior(source: &str) -> bool {
let Ok(file) = syn::parse_file(source) else {
return true;
};
let mut visitor = BehaviorPresenceVisitor { found: false };
visitor.visit_file(&file);
visitor.found
}
struct BehaviorPresenceVisitor {
found: bool,
}
impl<'ast> Visit<'ast> for BehaviorPresenceVisitor {
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
self.found = true;
visit::visit_item_fn(self, node);
}
fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
self.found = true;
visit::visit_impl_item_fn(self, node);
}
fn visit_trait_item_fn(&mut self, node: &'ast syn::TraitItemFn) {
if node.default.is_some() {
self.found = true;
}
visit::visit_trait_item_fn(self, node);
}
fn visit_expr_closure(&mut self, node: &'ast syn::ExprClosure) {
self.found = true;
visit::visit_expr_closure(self, node);
}
}
fn has_extension(path: &Path, extensions: &[&str]) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| extensions.contains(&ext))
}
fn is_declaration(path: &Path) -> bool {
let name = file_name_of(path);
name.ends_with(".d.ts") || name.ends_with(".d.mts") || name.ends_with(".d.cts")
}
fn python_same_code(base: &str, head: &str) -> bool {
match (python_tokens(base), python_tokens(head)) {
(Some(base), Some(head)) => base == head,
_ => false,
}
}
fn python_has_behavior(source: &str) -> bool {
let Ok(suite) = ast::Suite::parse(source, "<source>") else {
return true;
};
let mut visitor = BehaviorVisitor { found: false };
for stmt in suite {
visitor.visit_stmt(stmt);
}
visitor.found
}
struct BehaviorVisitor {
found: bool,
}
impl Visitor for BehaviorVisitor {
fn visit_stmt_function_def(&mut self, _: ast::StmtFunctionDef) {
self.found = true;
}
fn visit_stmt_async_function_def(&mut self, _: ast::StmtAsyncFunctionDef) {
self.found = true;
}
fn visit_expr_lambda(&mut self, _: ast::ExprLambda) {
self.found = true;
}
fn visit_stmt_if(&mut self, _: ast::StmtIf) {
self.found = true;
}
fn visit_stmt_for(&mut self, _: ast::StmtFor) {
self.found = true;
}
fn visit_stmt_async_for(&mut self, _: ast::StmtAsyncFor) {
self.found = true;
}
fn visit_stmt_while(&mut self, _: ast::StmtWhile) {
self.found = true;
}
fn visit_stmt_try(&mut self, _: ast::StmtTry) {
self.found = true;
}
fn visit_stmt_try_star(&mut self, _: ast::StmtTryStar) {
self.found = true;
}
fn visit_stmt_with(&mut self, _: ast::StmtWith) {
self.found = true;
}
fn visit_stmt_async_with(&mut self, _: ast::StmtAsyncWith) {
self.found = true;
}
fn visit_stmt_match(&mut self, _: ast::StmtMatch) {
self.found = true;
}
fn visit_expr_if_exp(&mut self, _: ast::ExprIfExp) {
self.found = true;
}
fn visit_comprehension(&mut self, node: ast::Comprehension) {
self.found |= !node.ifs.is_empty();
self.visit_expr(node.iter);
}
fn visit_keyword(&mut self, node: ast::Keyword) {
self.visit_expr(node.value);
}
}
fn python_tokens(source: &str) -> Option<Vec<Tok>> {
let tokens: Vec<Tok> = lex(source, Mode::Module)
.map(|token| token.ok().map(|(tok, _)| tok))
.collect::<Option<_>>()?;
ast::Suite::parse(source, "<source>").ok()?;
Some(tokens)
}
fn extension_of(path: &Path) -> String {
path.extension()
.map(|ext| ext.to_string_lossy().into_owned())
.unwrap_or_default()
}
fn file_name_of(path: &Path) -> String {
path.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default()
}
fn stem_of(path: &Path) -> String {
path.file_stem()
.map(|stem| stem.to_string_lossy().into_owned())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn python_tracks_py_files() {
assert!(Language::Python.tracks(Path::new("a.py")));
assert!(Language::Python.tracks(Path::new("pkg/widget.py")));
assert!(!Language::Python.tracks(Path::new("a.pyi")));
assert!(!Language::Python.tracks(Path::new("a.txt")));
assert!(!Language::Python.tracks(Path::new("README")));
}
#[test]
fn python_recognizes_test_files_by_stem_suffix() {
assert!(Language::Python.is_test(Path::new("widget_test.py")));
assert!(Language::Python.is_test(Path::new("pkg/helper_test.py")));
assert!(!Language::Python.is_test(Path::new("widget.py")));
}
#[test]
fn python_conftest_is_support_not_a_subject() {
assert!(Language::Python.is_support(Path::new("conftest.py")));
assert!(Language::Python.is_support(Path::new("pkg/conftest.py")));
assert!(!Language::Python.is_support(Path::new("widget.py")));
assert!(!Language::Python.is_support(Path::new("widget_test.py")));
assert!(!Language::TypeScript.is_support(Path::new("conftest.ts")));
}
#[test]
fn python_expected_test_path_is_the_colocated_twin() {
assert_eq!(
Language::Python.expected_test_path(Path::new("pkg/widget.py")),
PathBuf::from("pkg/widget_test.py")
);
assert_eq!(
Language::Python.expected_test_path(Path::new("widget.py")),
PathBuf::from("widget_test.py")
);
}
#[test]
fn typescript_tracks_ts_tsx_mts_cts_but_not_declarations() {
assert!(Language::TypeScript.tracks(Path::new("widget.ts")));
assert!(Language::TypeScript.tracks(Path::new("pkg/button.tsx")));
assert!(Language::TypeScript.tracks(Path::new("service.mts")));
assert!(Language::TypeScript.tracks(Path::new("legacy.cts")));
assert!(!Language::TypeScript.tracks(Path::new("types.d.ts")));
assert!(!Language::TypeScript.tracks(Path::new("ambient.d.mts")));
assert!(!Language::TypeScript.tracks(Path::new("globals.d.cts")));
assert!(!Language::TypeScript.tracks(Path::new("widget.py")));
assert!(!Language::TypeScript.tracks(Path::new("README")));
}
#[test]
fn typescript_recognizes_test_files_by_suffix() {
assert!(Language::TypeScript.is_test(Path::new("widget.test.ts")));
assert!(Language::TypeScript.is_test(Path::new("pkg/button.test.tsx")));
assert!(Language::TypeScript.is_test(Path::new("service.test.mts")));
assert!(Language::TypeScript.is_test(Path::new("legacy.test.cts")));
assert!(!Language::TypeScript.is_test(Path::new("widget.ts")));
assert!(!Language::TypeScript.is_test(Path::new("button.tsx")));
assert!(!Language::TypeScript.is_test(Path::new("service.mts")));
}
#[test]
fn typescript_expected_test_path_keeps_the_extension() {
assert_eq!(
Language::TypeScript.expected_test_path(Path::new("pkg/widget.ts")),
PathBuf::from("pkg/widget.test.ts")
);
assert_eq!(
Language::TypeScript.expected_test_path(Path::new("button.tsx")),
PathBuf::from("button.test.tsx")
);
assert_eq!(
Language::TypeScript.expected_test_path(Path::new("service.mts")),
PathBuf::from("service.test.mts")
);
assert_eq!(
Language::TypeScript.expected_test_path(Path::new("legacy.cts")),
PathBuf::from("legacy.test.cts")
);
}
#[test]
fn typescript_subject_skips_type_only_modules() {
let ts = Path::new("aliases.ts");
assert!(!Language::TypeScript.is_subject("export type Alias = string;\n", ts));
assert!(!Language::TypeScript.is_subject("export interface Shape { kind: string }\n", ts));
assert!(!Language::TypeScript.is_subject("import type { A } from './a';\n", ts));
}
#[test]
fn typescript_subject_needs_a_function_or_control_flow() {
let ts = Path::new("widget.ts");
assert!(Language::TypeScript.is_subject("export const x = () => 1;\n", ts));
assert!(Language::TypeScript.is_subject(
"export type Alias = string;\nexport const x = () => 1;\n",
ts
));
assert!(!Language::TypeScript.is_subject("export const x = 1;\n", ts));
assert!(!Language::TypeScript.is_subject("", ts));
assert!(!Language::TypeScript.is_subject(" \n\t\n", ts));
assert!(!Language::TypeScript.is_subject("// nothing here\n", ts));
assert!(!Language::TypeScript.is_subject("/* a\n block\n comment */\n", ts));
}
fn py_subject(source: &str) -> bool {
Language::Python.is_subject(source, Path::new("widget.py"))
}
#[test]
fn python_declaration_only_module_is_not_a_subject() {
assert!(!py_subject("x = 1\n"));
assert!(!py_subject("Alias = str\n"));
assert!(!py_subject(
"\"\"\"pkg.\"\"\"\nfrom ._version import __version__\n__all__ = [\"__version__\"]\n"
));
assert!(!py_subject("class Color(Enum):\n RED = 1\n"));
assert!(!py_subject(
"@dataclass\nclass Point:\n x: int\n y: int\n"
));
assert!(!py_subject("logger = getLogger(__name__)\n"));
assert!(!py_subject("TIMEOUT = 30 * 60\nNAME = f\"{TIMEOUT}s\"\n"));
assert!(!py_subject("\"\"\"Package docstring.\"\"\"\n"));
assert!(!py_subject(""));
assert!(!py_subject("\n \n"));
assert!(!py_subject("# just a comment\n # another\n"));
}
#[test]
fn python_short_circuit_and_a_plain_comprehension_are_not_control_flow() {
assert!(!py_subject("DEBUG = os.environ.get(\"DEBUG\") or \"0\"\n"));
assert!(!py_subject("READY = a and b\n"));
assert!(!py_subject("NAMES = [c.name for c in Color]\n"));
assert!(!py_subject("BY_NAME = {c.name: c for c in Color}\n"));
}
#[test]
fn python_every_function_form_is_a_subject() {
assert!(py_subject("def f():\n return 1\n"));
assert!(py_subject("async def f():\n return 1\n"));
assert!(py_subject("HANDLERS = {\"id\": lambda x: x}\n"));
assert!(py_subject(
"class Widget:\n def run(self):\n return 1\n"
));
assert!(py_subject(
"class Runner(Protocol):\n def run(self) -> int: ...\n"
));
}
#[test]
fn python_every_control_flow_form_is_a_subject() {
assert!(py_subject("if DEBUG:\n LEVEL = 10\n"));
assert!(py_subject("for i in range(3):\n pass\n"));
assert!(py_subject("async for i in aiter():\n pass\n"));
assert!(py_subject("while True:\n pass\n"));
assert!(py_subject(
"try:\n import x\nexcept ImportError:\n x = None\n"
));
assert!(py_subject(
"try:\n import x\nexcept* ImportError:\n x = None\n"
));
assert!(py_subject("with open(p) as f:\n DATA = f.read()\n"));
assert!(py_subject("async with lock:\n pass\n"));
assert!(py_subject("match cmd:\n case \"go\":\n pass\n"));
assert!(py_subject("LEVEL = 10 if DEBUG else 20\n"));
assert!(py_subject("EVENS = [n for n in range(9) if n % 2 == 0]\n"));
}
#[test]
fn python_control_flow_nested_in_an_expression_is_a_subject() {
assert!(py_subject("CONFIG = {\"level\": (10 if DEBUG else 20)}\n"));
assert!(py_subject("run(callback=lambda: None)\n"));
assert!(py_subject(
"ORDERED = [x for x in sorted(xs, key=lambda x: x.id)]\n"
));
}
#[test]
fn python_unparsable_content_stays_a_subject() {
assert!(py_subject("def f(:\n"));
}
const PY_WIDGET: &str = "def widget():\n return 1\n";
#[test]
fn python_same_code_ignores_comments_and_formatting() {
let py = Path::new("widget.py");
assert!(Language::Python.same_code(
"# widget helpers\ndef widget():\n return 1\n",
"# widget utilities\ndef widget():\n return 1\n",
py
));
assert!(Language::Python.same_code(
"# widget helpers\ndef widget():\n return 1\n",
PY_WIDGET,
py
));
assert!(Language::Python.same_code(PY_WIDGET, "def widget():\n\n return 1\n", py));
assert!(Language::Python.same_code("def widget(): \n return 1 \n", PY_WIDGET, py));
}
#[test]
fn python_same_code_sees_every_edit_the_interpreter_sees() {
let py = Path::new("widget.py");
assert!(!Language::Python.same_code(PY_WIDGET, "def widget():\n return 2\n", py));
assert!(!Language::Python.same_code(
"\"\"\"Widget helpers.\"\"\"\ndef widget():\n return 1\n",
"\"\"\"Widget utilities.\"\"\"\ndef widget():\n return 1\n",
py
));
assert!(!Language::Python.same_code(
"def widget():\n return \"one\"\n",
"def widget():\n return \"two\"\n",
py
));
assert!(!Language::Python.same_code(
"def widget(flag):\n if flag:\n count = 1\n return count\n",
"def widget(flag):\n if flag:\n count = 1\n return count\n",
py
));
}
#[test]
fn python_same_code_holds_unparseable_content_apart() {
let py = Path::new("widget.py");
assert!(!Language::Python.same_code(
"def widget(:\n return 1\n",
"# note\ndef widget(:\n return 1\n",
py
));
assert!(!Language::Python.same_code(
"def widget() return 1\n",
"# note\ndef widget() return 1\n",
py
));
assert!(!Language::Python.same_code(PY_WIDGET, "def widget() return 1\n", py));
assert!(!Language::Python.same_code("def widget() return 1\n", PY_WIDGET, py));
}
#[test]
fn typescript_same_code_reads_the_emitted_module() {
let ts = Path::new("widget.ts");
assert!(Language::TypeScript.same_code(
"// widget factory\nexport const widget = () => 1;\n",
"export const widget = () => 1;\n",
ts
));
assert!(!Language::TypeScript.same_code(
"export const widget = () => 1;\n",
"export const widget = () => 2;\n",
ts
));
}
#[test]
fn rust_same_code_never_answers_equal() {
assert!(!Language::Rust.same_code("fn f() {}\n", "fn f() {}\n", Path::new("lib.rs")));
}
#[test]
fn rust_has_no_file_based_colocated_convention() {
assert!(!Language::Rust.tracks(Path::new("lib.rs")));
assert!(!Language::Rust.is_test(Path::new("lib_test.rs")));
assert!(Language::Rust.is_subject("fn main() {}\n", Path::new("main.rs")));
assert_eq!(
Language::Rust.expected_test_path(Path::new("src/lib.rs")),
PathBuf::from("src/lib.rs")
);
}
#[test]
fn rust_const_only_file_is_not_a_subject() {
assert!(!Language::Rust.is_subject(
"pub const TIMEOUT: u64 = 30 * 60;\n",
Path::new("settings.rs")
));
}
#[test]
fn rust_unparseable_file_is_a_subject() {
assert!(Language::Rust.is_subject("fn (", Path::new("broken.rs")));
}
#[test]
fn rust_impl_method_is_a_subject() {
assert!(Language::Rust.is_subject(
"struct Widget;\nimpl Widget {\n fn run(&self) {}\n}\n",
Path::new("widget.rs")
));
}
#[test]
fn rust_default_trait_method_is_a_subject() {
assert!(Language::Rust.is_subject(
"trait Greeter {\n fn greet(&self) {\n println!(\"hi\");\n }\n}\n",
Path::new("greeter.rs")
));
}
#[test]
fn rust_trait_method_signature_without_a_default_is_not_a_subject() {
assert!(!Language::Rust.is_subject(
"trait Greeter {\n fn greet(&self);\n}\n",
Path::new("greeter.rs")
));
}
#[test]
fn rust_closure_outside_any_fn_is_a_subject() {
assert!(Language::Rust.is_subject(
"static ADDER: fn(i32) -> i32 = |x| x + 1;\n",
Path::new("adder.rs")
));
}
fn presence(src: &str) -> (bool, bool) {
let ast = syn::parse_file(src).expect("snippet parses");
let mut visitor = PresenceVisitor::default();
visitor.visit_file(&ast);
(visitor.has_testable_fn, visitor.has_test_module)
}
#[test]
fn rust_presence_free_fn_with_test_module_is_covered() {
assert_eq!(
presence(
"pub fn make(n: u8) -> u8 { n + 1 }\n\
#[cfg(test)]\nmod tests { #[test] fn t() {} }\n"
),
(true, true)
);
}
#[test]
fn rust_presence_free_fn_without_test_module_needs_one() {
assert_eq!(
presence("pub fn make(n: u8) -> u8 { n + 1 }\n"),
(true, false)
);
}
#[test]
fn rust_presence_type_only_file_is_not_a_subject() {
assert_eq!(presence("pub struct Point { pub x: u8 }\n"), (false, false));
}
#[test]
fn rust_presence_impl_method_is_testable() {
assert_eq!(
presence("pub struct W;\nimpl W { pub fn go(&self) -> u8 { 1 } }\n"),
(true, false)
);
}
#[test]
fn rust_presence_logic_free_main_is_a_declaration() {
assert_eq!(
presence("fn main() -> ExitCode { mycrate::entrypoint::main() }\n"),
(false, false)
);
assert_eq!(presence("fn main() { mycrate::run() }\n"), (false, false));
assert_eq!(presence("fn main() { mycrate::run(); }\n"), (false, false));
}
#[test]
fn rust_presence_a_main_that_could_hold_a_decision_is_a_subject() {
assert_eq!(
presence("fn main() { mycrate::run(std::env::args_os()) }\n"),
(true, false)
);
assert_eq!(
presence("fn main() { setup(); mycrate::run() }\n"),
(true, false)
);
assert_eq!(presence("fn main() { mycrate::run()? }\n"), (true, false));
assert_eq!(
presence("fn main() { mycrate::run().into() }\n"),
(true, false)
);
assert_eq!(
presence("fn main() { if x { a() } else { b() } }\n"),
(true, false)
);
assert_eq!(presence("fn main() {}\n"), (true, false));
}
#[test]
fn rust_presence_only_main_earns_the_declaration_reading() {
assert_eq!(presence("fn run() { mycrate::go() }\n"), (true, false));
}
#[test]
fn rust_presence_trait_default_is_testable_but_bare_signature_is_not() {
assert_eq!(
presence("pub trait T { fn d(&self) -> u8 { 1 } }\n"),
(true, false)
);
assert_eq!(
presence("pub trait T { fn s(&self) -> u8; }\n"),
(false, false)
);
}
#[test]
fn rust_presence_test_module_functions_are_not_subjects() {
assert_eq!(
presence("#[cfg(test)]\nmod tests { fn helper() {} #[test] fn t() {} }\n"),
(false, true)
);
}
#[test]
fn rust_presence_cfg_test_gated_free_fn_is_not_a_subject() {
assert_eq!(
presence("#[cfg(test)]\nfn only_in_tests() {}\n"),
(false, false)
);
}
}