use crate::MetricsOptions;
use crate::spaces::metrics_inner;
use crate::{CppParser, ParserTrait, SpaceKind, check_func_space};
#[test]
fn space_kind_non_exhaustive_serde_roundtrip_unchanged() {
for kind in [
SpaceKind::Unknown,
SpaceKind::Function,
SpaceKind::Class,
SpaceKind::Struct,
SpaceKind::Trait,
SpaceKind::Impl,
SpaceKind::Unit,
SpaceKind::Namespace,
SpaceKind::Interface,
] {
let json = serde_json::to_string(&kind).unwrap();
assert_eq!(json, format!("\"{kind}\""));
let back: SpaceKind = serde_json::from_str(&json).unwrap();
assert_eq!(back, kind);
assert_eq!(SpaceKind::from_serialized(&kind.to_string()), kind);
}
assert_eq!(SpaceKind::from_serialized("not_a_kind"), SpaceKind::Unknown);
}
#[test]
fn cpp_function_definition_is_classified_as_function() {
use crate::Cpp;
use crate::checker::Checker;
use crate::getter::Getter;
use crate::langs::CppCode;
use crate::traits::Search;
let source = "int the_func(int x) { return x; }\n";
let path = std::path::PathBuf::from("fd.cc");
let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
let root = parser.root();
let fn_node = root
.first_occurrence(|id| {
Cpp::FunctionDefinition == id
|| Cpp::FunctionDefinition2 == id
|| Cpp::FunctionDefinition3 == id
|| Cpp::FunctionDefinition4 == id
})
.expect("parse must produce a function_definition node");
assert!(
CppCode::is_func(&fn_node),
"is_func must return true for a function_definition"
);
assert!(
CppCode::is_func_space(&fn_node),
"is_func_space must return true for a function_definition"
);
assert_eq!(
CppCode::get_space_kind(&fn_node),
SpaceKind::Function,
"get_space_kind must classify function_definition as Function"
);
assert_eq!(
CppCode::get_func_space_name(&fn_node, source.as_bytes()),
Some("the_func"),
"get_func_space_name must extract the declarator identifier"
);
}
#[test]
fn cpp_scope_resolution_operator() {
check_func_space::<CppParser, _>(
"void Foo::bar(){
return;
}",
"foo.cpp",
|func_space| {
insta::assert_json_snapshot!(
func_space.spaces[0].name,
@r###""Foo::bar""###
);
},
);
}
#[test]
fn cpp_error_root_yields_unit_top_level_space() {
let source = "#ifndef A\n\
namespace a { namespace b { namespace c {\n\
template <class S, class V> class C : publi\n";
let path = std::path::PathBuf::from("error_root.cc");
let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
assert!(
parser.root().as_tree_sitter().is_error(),
"test premise broken: grammar must yield ERROR root for this snippet"
);
let space = metrics_inner(
&parser,
path.to_str().map(str::to_owned),
MetricsOptions::default(),
)
.unwrap();
assert_eq!(
space.kind,
SpaceKind::Unit,
"top-level FuncSpace must be Unit, not {:?}",
space.kind
);
let loc = &space.metrics.loc;
let sloc = loc.sloc();
let ploc = loc.ploc();
let blank = loc.blank();
let line_count = source.lines().count();
assert!(
sloc >= ploc,
"sloc ({sloc}) must be >= ploc ({ploc}) for the file-level space"
);
assert!(blank <= sloc, "blank ({blank}) must be <= sloc ({sloc})");
assert_eq!(
sloc as usize, line_count,
"sloc ({sloc}) should match the file's line count ({line_count})"
);
}
fn assert_top_level_space_is_unit_contract<P: ParserTrait>(source: &str, filename: &str) {
let path = std::path::PathBuf::from(filename);
let parser = P::new(source.as_bytes().to_vec(), &path, None);
let space = metrics_inner(
&parser,
path.to_str().map(str::to_owned),
MetricsOptions::default(),
)
.expect("metrics must yield a top-level space");
assert_eq!(
space.kind,
SpaceKind::Unit,
"top-level FuncSpace for {filename:?} must be Unit, not {:?}",
space.kind
);
let loc = &space.metrics.loc;
let sloc = loc.sloc();
let ploc = loc.ploc();
let blank = loc.blank();
assert!(
sloc >= ploc,
"sloc ({sloc}) must be >= ploc ({ploc}) for the file-level space of {filename:?}",
);
assert!(
blank <= sloc,
"blank ({blank}) must be <= sloc ({sloc}) for the file-level space of {filename:?}",
);
}
fn assert_partial_input_yields_synthetic_unit_wrapper<P: ParserTrait>(
source: &str,
filename: &str,
) {
let path = std::path::PathBuf::from(filename);
let parser = P::new(source.as_bytes().to_vec(), &path, None);
assert!(
parser.root().as_tree_sitter().is_error(),
"test premise broken: grammar must yield ERROR root for {filename:?}",
);
assert_top_level_space_is_unit_contract::<P>(source, filename);
}
#[test]
fn python_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::PythonParser>(
"def foo(x):\n return x +\n",
"partial.py",
);
}
#[test]
fn javascript_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::JavascriptParser>(
"function foo(x) {\n return x +\n",
"partial.js",
);
}
#[test]
fn mozjs_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::MozjsParser>(
"function foo(x) {\n return x +\n",
"partial.js",
);
}
#[test]
fn typescript_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::TypescriptParser>(
"function foo(x: number): number {\n return x +\n",
"partial.ts",
);
}
#[test]
fn tsx_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::TsxParser>(
"function Foo(x: number): JSX.Element {\n return <div>{x +\n",
"partial.tsx",
);
}
#[test]
fn java_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::JavaParser>(
"class Foo {\n void bar(int x) {\n return x +\n",
"Partial.java",
);
}
#[test]
fn kotlin_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::KotlinParser>(
"class Foo {\n fun bar(x: Int): Int {\n return x +\n",
"Partial.kt",
);
}
#[test]
fn go_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::GoParser>(
"package main\nfunc foo(x int) int {\n return x +\n",
"partial.go",
);
}
#[test]
fn rust_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::RustParser>(
"fn foo(x: i32) -> i32 {\n return x +\n",
"partial.rs",
);
}
#[test]
fn csharp_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::CsharpParser>(
"class Foo {\n void Bar(int x) {\n return x +\n",
"Partial.cs",
);
}
#[test]
fn csharp_enum_space_kind_is_class() {
let src = "enum Color { Red, Green, Blue }\n";
let path = std::path::PathBuf::from("Color.cs");
let parser = crate::CsharpParser::new(src.as_bytes().to_vec(), &path, None);
let space = metrics_inner(
&parser,
path.to_str().map(str::to_owned),
MetricsOptions::default(),
)
.expect("metrics must yield a top-level space");
let enum_space = space.spaces.first().expect("enum must open a child space");
assert_eq!(enum_space.kind, SpaceKind::Class);
assert_eq!(enum_space.name.as_deref(), Some("Color"));
}
#[test]
fn bash_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::BashParser>(
"function foo() {\n echo \"x +\n",
"partial.sh",
);
}
#[test]
fn lua_partial_input_yields_synthetic_unit_wrapper() {
assert_partial_input_yields_synthetic_unit_wrapper::<crate::LuaParser>(
"function foo(x)\n return x +\n",
"partial.lua",
);
}
#[test]
fn tcl_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::TclParser>(
"proc foo {x} {\n return [expr {$x +\n",
"partial.tcl",
);
}
#[test]
fn irules_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::IrulesParser>(
"when HTTP_REQUEST { if { $a } {\n",
"partial.irule",
);
}
#[test]
fn perl_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::PerlParser>(
"sub foo {\n my $x = shift;\n return $x +\n",
"partial.pl",
);
}
#[test]
fn php_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::PhpParser>(
"<?php\nfunction foo($x) {\n return $x +\n",
"partial.php",
);
}
#[test]
fn elixir_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::ElixirParser>(
"defmodule Foo do\n def bar(x) do\n x +\n",
"partial.ex",
);
}
#[test]
fn elixir_func_space_names_resolve_through_arguments_wrapper() {
let src = "defmodule Foo.Bar do\n def hello(x), do: x\n defp helper, do: :ok\n defmodule Inner do\n def i, do: 1\n end\nend\n";
let path = std::path::PathBuf::from("foo.ex");
let parser = crate::ElixirParser::new(src.as_bytes().to_vec(), &path, None);
let space = metrics_inner(
&parser,
path.to_str().map(str::to_owned),
MetricsOptions::default(),
)
.expect("metrics must yield a top-level space");
assert_eq!(space.name.as_deref(), Some("foo.ex"));
let outer = space.spaces.first().expect("outer class space");
assert_eq!(outer.kind, SpaceKind::Class);
assert_eq!(outer.name.as_deref(), Some("Foo.Bar"));
let names: Vec<&str> = outer
.spaces
.iter()
.map(|s| s.name.as_deref().unwrap_or("?"))
.collect();
assert_eq!(names, vec!["hello", "helper", "Inner"]);
let inner = outer
.spaces
.iter()
.find(|s| s.kind == SpaceKind::Class)
.expect("nested class");
let inner_names: Vec<&str> = inner
.spaces
.iter()
.map(|s| s.name.as_deref().unwrap_or("?"))
.collect();
assert_eq!(inner_names, vec!["i"]);
}
#[test]
fn preproc_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::PreprocParser>(
"#ifdef FOO\n#define BAR(x) (x +\n",
"partial.h",
);
}
#[test]
fn ccomment_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::CcommentParser>(
"/* unterminated comment\n spanning several\n",
"partial.c",
);
}
#[test]
fn ruby_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::RubyParser>(
"class Foo\n def bar(\n x\n ",
"partial.rb",
);
}
#[test]
fn analyze_in_memory_snippet_carries_caller_supplied_name() {
use crate::{Source, analyze};
let source =
Source::new(crate::LANG::Cpp, b"int a = 42;").with_name(Some("in-memory.cpp".to_owned()));
let space =
analyze(source, MetricsOptions::default()).expect("analyze must yield a top-level space");
assert_eq!(
space.name.as_deref(),
Some("in-memory.cpp"),
"top-level name must be the caller-supplied string, byte-for-byte"
);
}
#[test]
fn analyze_without_name_leaves_top_level_name_none() {
use crate::{Source, analyze};
let space = analyze(
Source::new(crate::LANG::Cpp, b"int a = 42;"),
MetricsOptions::default(),
)
.expect("analyze must yield a top-level space");
assert!(
space.name.is_none(),
"top-level name must be None when Source::name is None, got {:?}",
space.name
);
}
fn make_state<'a>(kind: SpaceKind) -> super::State<'a> {
super::State {
space: super::FuncSpace {
name: None,
start_line: 0,
end_line: 0,
kind,
spaces: Vec::new(),
metrics: super::CodeMetrics::default(),
suppressed: super::SuppressionScope::default(),
},
halstead_maps: crate::metrics::halstead::HalsteadMaps::new(),
}
}
fn file_suppression_all() -> crate::suppression::Suppression {
crate::suppression::Suppression {
kind: crate::suppression::SuppressionKind::File,
scope: crate::suppression::SuppressionScope::All,
source: crate::suppression::SuppressionSource::Native,
}
}
#[test]
fn file_suppression_attaches_to_unit_frame() {
let mut stack = vec![make_state(SpaceKind::Unit), make_state(SpaceKind::Function)];
super::apply_suppression(&mut stack, &file_suppression_all());
assert!(
stack[0].space.suppressed.is_all(),
"file marker (scope=All) must attach to the Unit root frame"
);
assert!(
stack[1].space.suppressed.is_empty(),
"file marker must not attach to a non-Unit frame"
);
}
#[test]
fn file_suppression_skips_non_unit_root_frame() {
let mut stack = vec![
make_state(SpaceKind::Function),
make_state(SpaceKind::Class),
];
super::apply_suppression(&mut stack, &file_suppression_all());
assert!(
stack.iter().all(|s| s.space.suppressed.is_empty()),
"file marker must be silently dropped when no Unit frame exists"
);
}
#[test]
fn file_suppression_finds_unit_deeper_in_stack() {
let mut stack = vec![make_state(SpaceKind::Function), make_state(SpaceKind::Unit)];
super::apply_suppression(&mut stack, &file_suppression_all());
assert!(
stack[0].space.suppressed.is_empty(),
"non-Unit frame above the Unit must not absorb the file marker"
);
assert!(
stack[1].space.suppressed.is_all(),
"file marker must land on the Unit frame even when not at index 0"
);
}
#[test]
fn file_suppression_empty_stack_is_silent_noop() {
let mut stack: Vec<super::State<'_>> = Vec::new();
super::apply_suppression(&mut stack, &file_suppression_all());
}
mod exclude_tests_rust {
use crate::spaces::metrics_inner;
use crate::{MetricsOptions, ParserTrait, RustParser};
use std::path::PathBuf;
fn analyse(source: &str, exclude_tests: bool) -> crate::FuncSpace {
let path = PathBuf::from("lib.rs");
let parser = RustParser::new(source.as_bytes().to_vec(), &path, None);
metrics_inner(
&parser,
path.to_str().map(str::to_owned),
MetricsOptions::default().with_exclude_tests(exclude_tests),
)
.expect("metrics must yield a top-level space")
}
#[test]
fn outer_test_attribute_elides_function() {
let source = "\
fn prod() -> i32 { 1 + 2 }
#[test]
fn t() { assert_eq!(1 + 1, 2); }
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 2);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
assert!(
pruned.metrics.cyclomatic.cyclomatic_sum()
<= baseline.metrics.cyclomatic.cyclomatic_sum()
);
}
#[test]
fn cfg_test_mod_elides_entire_module() {
let source = "\
fn prod() -> i32 { 1 }
#[cfg(test)]
mod tests {
fn helper() -> i32 { 2 }
fn another_helper() -> i32 { 3 }
#[test] fn t() { assert_eq!(1, 1); }
}
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 4);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
}
#[test]
fn tokio_test_attribute_is_elided() {
let source = "\
fn prod() -> i32 { 1 }
#[tokio::test]
async fn async_t() { let _x = 1; }
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 2);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
}
#[test]
fn cfg_all_test_with_extras_is_elided() {
let source = "\
fn prod() -> i32 { 1 }
#[cfg(all(test, target_arch = \"x86_64\"))]
fn arch_specific_test() { let _x = 1; }
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 2);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
}
#[test]
fn pure_production_unaffected_by_flag() {
let source = "\
fn prod() -> i32 { 1 + 2 }
fn helper(x: i32) -> i32 { x * 2 }
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 2);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 2);
assert_eq!(
baseline.metrics.cyclomatic.cyclomatic_sum(),
pruned.metrics.cyclomatic.cyclomatic_sum(),
);
}
#[test]
fn default_flag_off_preserves_baseline() {
let source = "\
fn prod() -> i32 { 1 }
#[test]
fn t() { assert_eq!(1, 1); }
";
let baseline_default = analyse(source, false);
assert_eq!(baseline_default.metrics.nom.functions_sum() as usize, 2);
}
#[test]
fn stacked_attributes_walk_all_siblings() {
let source = "\
fn prod() -> i32 { 1 }
#[cfg(target_arch = \"x86_64\")]
#[cfg(test)]
fn t() { let _x = 1; }
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 2);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
}
#[test]
fn cfg_with_test_not_first_is_elided() {
let source = "\
fn prod() -> i32 { 1 }
#[cfg(all(unix, test))]
fn unix_only_test() { let _x = 1; }
#[cfg(any(feature = \"slow\", test))]
fn slow_or_test() { let _x = 2; }
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 3);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
}
#[test]
fn lookalike_attributes_are_not_pruned() {
let source = "\
#[cfg(not(test))]
fn only_outside_tests() -> i32 { 1 }
#[cfg(feature = \"test\")]
fn behind_test_feature() -> i32 { 2 }
#[my_crate::test_helper]
fn decorated_helper() -> i32 { 3 }
#[cfg(all(unix, not(test)))]
fn unix_prod_only() -> i32 { 4 }
";
let pruned = analyse(source, true);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 4);
}
#[test]
fn inner_cfg_test_attribute_elides_module() {
let source = "\
fn prod() -> i32 { 1 }
mod tests {
#![cfg(test)]
fn helper() -> i32 { 2 }
#[test] fn t() { assert_eq!(1, 1); }
}
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 3);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
}
#[test]
fn sloc_drops_with_pruned_cfg_test_mod() {
let source = "\
fn prod() {
let x = 1;
println!(\"{x}\");
}
#[cfg(test)]
mod tests {
#[test]
fn a() {
assert_eq!(1, 1);
}
}
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.loc.sloc(), 12);
assert_eq!(baseline.metrics.loc.ploc(), 11);
assert_eq!(pruned.metrics.loc.sloc(), 6);
assert_eq!(pruned.metrics.loc.ploc(), 5);
assert_eq!(pruned.metrics.loc.blank(), 1);
}
#[test]
fn sloc_drops_for_adjacent_test_modules() {
let source = "\
fn prod() {}
#[cfg(test)]
mod a {
#[test]
fn x() {}
}
#[cfg(test)]
mod b {
#[test]
fn y() {}
}
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.loc.sloc(), 11);
assert_eq!(pruned.metrics.loc.sloc(), 3);
assert_eq!(pruned.metrics.loc.ploc(), 3);
assert_eq!(pruned.metrics.loc.blank(), 0);
}
#[test]
fn sloc_drops_for_nested_test_modules() {
let source = "\
fn prod() {}
#[cfg(test)]
mod outer {
mod inner {
#[test]
fn t() {}
}
}
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.loc.sloc(), 8);
assert_eq!(pruned.metrics.loc.sloc(), 2);
assert_eq!(pruned.metrics.loc.ploc(), 2);
assert_eq!(pruned.metrics.loc.blank(), 0);
}
#[test]
fn sloc_drops_for_test_fn_nested_in_impl() {
let source = "\
impl Foo {
fn prod(&self) {}
#[test]
fn t() {}
}
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.spaces.len(), 1);
assert_eq!(pruned.spaces.len(), 1);
let baseline_impl = &baseline.spaces[0];
let pruned_impl = &pruned.spaces[0];
assert_eq!(baseline_impl.metrics.loc.sloc(), 5);
assert_eq!(pruned_impl.metrics.loc.sloc(), 4);
assert_eq!(baseline.metrics.loc.sloc(), 5);
assert_eq!(pruned.metrics.loc.sloc(), 4);
}
#[test]
fn sloc_drops_for_test_fn_in_production_impl() {
let source = "\
impl Calc {
fn add(&self, x: i32) -> i32 {
x + 1
}
#[test]
fn t() {
assert_eq!(1 + 1, 2);
}
}
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.loc.sloc(), 9);
assert_eq!(pruned.metrics.loc.sloc(), 6);
assert_eq!(pruned.metrics.loc.ploc(), pruned.metrics.loc.sloc());
}
#[test]
fn sloc_drops_for_test_fn_nested_in_closure() {
let source = "\
fn make() {
let f = || {
#[test]
fn t() {}
};
}
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.loc.sloc(), 6);
assert_eq!(pruned.metrics.loc.sloc(), 5);
}
#[test]
fn non_test_impl_sloc_unaffected_by_pruning() {
let source = "\
impl Calc {
fn add(&self, x: i32) -> i32 {
x + 1
}
}
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.loc.sloc(), pruned.metrics.loc.sloc());
assert_eq!(pruned.spaces.len(), 1);
assert_eq!(
baseline.spaces[0].metrics.loc.sloc(),
pruned.spaces[0].metrics.loc.sloc()
);
}
}
mod exclude_tests_non_rust {
use crate::spaces::metrics_inner;
use crate::{CppParser, MetricsOptions, ParserTrait};
use std::path::PathBuf;
#[test]
fn cpp_ignores_exclude_tests_flag() {
let source = "\
int prod() { return 1; }
int helper() { return 2; }
";
let path = PathBuf::from("foo.cpp");
let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
let baseline = metrics_inner(
&parser,
path.to_str().map(str::to_owned),
MetricsOptions::default().with_exclude_tests(false),
)
.expect("baseline must yield a top-level space");
let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
let pruned = metrics_inner(
&parser,
path.to_str().map(str::to_owned),
MetricsOptions::default().with_exclude_tests(true),
)
.expect("pruned must yield a top-level space");
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 2);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 2);
}
}
mod with_only {
use crate::{LANG, Metric, MetricSet, MetricsOptions, Source, analyze};
const SOURCE: &str = "\
fn prod(x: i32) -> i32 {
if x > 0 { x + 1 } else { x - 1 }
}
";
fn analyse(metrics: &[Metric]) -> crate::FuncSpace {
let opts = MetricsOptions::default().with_only(metrics);
analyze(
Source::new(LANG::Rust, SOURCE.as_bytes()).with_name(Some("lib.rs".to_owned())),
opts,
)
.expect("analyze must yield a top-level space")
}
#[test]
fn loc_only_skips_other_metrics() {
let full = analyze(
Source::new(LANG::Rust, SOURCE.as_bytes()).with_name(Some("lib.rs".to_owned())),
MetricsOptions::default(),
)
.expect("full analyze must yield a top-level space");
let pruned = analyse(&[Metric::Loc]);
assert_eq!(
pruned.metrics.selected(),
MetricSet::empty().with(Metric::Loc),
"with_only(&[Loc]) must record exactly the Loc bit"
);
assert!(pruned.metrics.loc.ploc() >= 1);
assert!(full.metrics.cognitive.cognitive_sum() > 0);
assert_eq!(pruned.metrics.cognitive.cognitive_sum(), 0);
assert!(full.metrics.cyclomatic.cyclomatic_sum() > 0);
assert_eq!(pruned.metrics.cyclomatic.cyclomatic_sum(), 0);
assert_eq!(pruned.metrics.halstead.unique_operators(), 0);
}
#[test]
fn mi_auto_pulls_dependencies() {
let pruned = analyse(&[Metric::Mi]);
let sel = pruned.metrics.selected();
assert!(sel.contains(Metric::Mi));
assert!(sel.contains(Metric::Loc), "Mi depends on Loc");
assert!(sel.contains(Metric::Cyclomatic), "Mi depends on Cyclomatic");
assert!(sel.contains(Metric::Halstead), "Mi depends on Halstead");
assert!(!sel.contains(Metric::Abc));
assert!(!sel.contains(Metric::Tokens));
assert!(
pruned.metrics.loc.ploc() > 0,
"Loc must have run (Mi dependency); got ploc=0"
);
assert!(
pruned.metrics.cyclomatic.cyclomatic_sum() > 0,
"Cyclomatic must have run (Mi dependency); got sum=0"
);
let mi_value = pruned.metrics.mi.original();
assert!(
mi_value.is_finite() && mi_value != 0.0,
"MI must be finite and non-default when its dependencies were computed; got {mi_value}"
);
}
#[test]
fn wmc_auto_pulls_dependencies() {
let pruned = analyse(&[Metric::Wmc]);
let sel = pruned.metrics.selected();
assert!(sel.contains(Metric::Wmc));
assert!(
sel.contains(Metric::Cyclomatic),
"Wmc depends on Cyclomatic"
);
assert!(sel.contains(Metric::Nom), "Wmc depends on Nom");
assert!(!sel.contains(Metric::Halstead));
assert!(
pruned.metrics.cyclomatic.cyclomatic_sum() > 0,
"Cyclomatic must have run (Wmc dependency); got sum=0"
);
assert!(
pruned.metrics.nom.functions_sum() > 0,
"Nom must have run (Wmc dependency); got functions_sum=0"
);
}
#[test]
fn cognitive_only_pulls_nom_and_average_is_finite() {
let pruned = analyse(&[Metric::Cognitive]);
let sel = pruned.metrics.selected();
assert!(sel.contains(Metric::Cognitive));
assert!(sel.contains(Metric::Nom), "Cognitive depends on Nom (#428)");
assert!(
pruned.metrics.nom.total() > 0,
"Nom must have run (Cognitive dependency); got total=0"
);
let avg = pruned.metrics.cognitive.cognitive_average();
assert!(
avg.is_finite(),
"cognitive_average must be finite when Nom is pulled in; got {avg}"
);
assert_eq!(pruned.metrics.cognitive.cognitive_sum(), 2);
assert_eq!(avg, 2.0);
}
#[test]
fn exit_only_pulls_nom_and_average_is_finite() {
let pruned = analyse(&[Metric::Nexits]);
let sel = pruned.metrics.selected();
assert!(sel.contains(Metric::Nexits));
assert!(sel.contains(Metric::Nom), "Exit depends on Nom (#428)");
assert!(
pruned.metrics.nom.total() > 0,
"Nom must have run (Exit dependency); got total=0"
);
let avg = pruned.metrics.nexits.nexits_average();
assert!(
avg.is_finite(),
"nexits_average must be finite when Nom is pulled in; got {avg}"
);
assert_eq!(pruned.metrics.nexits.nexits_sum(), 0);
assert_eq!(avg, 0.0);
}
#[test]
fn nargs_only_pulls_nom_and_average_is_finite() {
let pruned = analyse(&[Metric::Nargs]);
let sel = pruned.metrics.selected();
assert!(sel.contains(Metric::Nargs));
assert!(sel.contains(Metric::Nom), "NArgs depends on Nom (#428)");
assert!(
pruned.metrics.nom.total() > 0,
"Nom must have run (NArgs dependency); got total=0"
);
let avg = pruned.metrics.nargs.average();
assert!(
avg.is_finite(),
"average must be finite when Nom is pulled in; got {avg}"
);
assert_eq!(avg, 1.0);
}
#[test]
fn default_options_select_every_metric() {
let full = analyze(
Source::new(LANG::Rust, SOURCE.as_bytes()).with_name(Some("lib.rs".to_owned())),
MetricsOptions::default(),
)
.expect("analyze must yield a top-level space");
assert_eq!(full.metrics.selected(), MetricSet::all());
}
#[test]
fn unselected_metrics_are_skipped_in_json() {
let pruned = analyse(&[Metric::Loc]);
let json =
serde_json::to_value(&pruned.metrics).expect("CodeMetrics must serialize cleanly");
let metrics = json.as_object().expect("CodeMetrics serializes as object");
assert!(
metrics.contains_key("loc"),
"loc must be serialized when selected"
);
for skipped in [
"cognitive",
"cyclomatic",
"halstead",
"nom",
"tokens",
"nargs",
"nexits",
"abc",
"mi",
"wmc",
"npm",
"npa",
] {
assert!(
!metrics.contains_key(skipped),
"{skipped} must be elided when not selected"
);
}
}
#[test]
fn elixir_loc_deselected_preserves_kinds_and_metrics() {
use crate::SpaceKind;
const ELIXIR_SOURCE: &str = "\
defmodule Greeter do
def hello(name) do
if name == \"\" do
:anon
else
name
end
end
def bye() do
:ok
end
end
";
fn collect_kinds(space: &crate::FuncSpace, out: &mut Vec<SpaceKind>) {
out.push(space.kind);
for sub in &space.spaces {
collect_kinds(sub, out);
}
}
fn analyse_elixir(metrics: Option<&[Metric]>) -> crate::FuncSpace {
let opts = match metrics {
Some(m) => MetricsOptions::default().with_only(m),
None => MetricsOptions::default(),
};
analyze(
Source::new(LANG::Elixir, ELIXIR_SOURCE.as_bytes())
.with_name(Some("greeter.ex".to_owned())),
opts,
)
.expect("analyze must yield a top-level space")
}
let full = analyse_elixir(None);
let pruned = analyse_elixir(Some(&[Metric::Cognitive]));
assert!(
!pruned.metrics.selected().contains(Metric::Loc),
"test premise: Loc must be deselected on the pruned run"
);
let mut full_kinds = Vec::new();
let mut pruned_kinds = Vec::new();
collect_kinds(&full, &mut full_kinds);
collect_kinds(&pruned, &mut pruned_kinds);
assert_eq!(
full_kinds, pruned_kinds,
"lazy `kind` computation must not change the space-tree SpaceKinds"
);
assert!(
full_kinds.contains(&SpaceKind::Class),
"test premise: defmodule must promote to a Class space"
);
assert!(
full_kinds.contains(&SpaceKind::Function),
"test premise: def must promote to a Function space"
);
assert!(
full.metrics.cognitive.cognitive_sum() > 0,
"test premise: the source has cognitive complexity (the `if`/`else`)"
);
assert_eq!(
full.metrics.cognitive.cognitive_sum(),
pruned.metrics.cognitive.cognitive_sum(),
"deselecting Loc must not change cognitive complexity"
);
}
#[test]
fn empty_slice_selects_nothing() {
let pruned = analyse(&[]);
assert_eq!(pruned.metrics.selected(), MetricSet::empty());
let json =
serde_json::to_value(&pruned.metrics).expect("CodeMetrics must serialize cleanly");
let metrics = json.as_object().expect("CodeMetrics serializes as object");
assert!(
metrics.is_empty(),
"with_only(&[]) must elide every metric, got keys {:?}",
metrics.keys().collect::<Vec<_>>()
);
}
#[test]
fn with_metric_set_resolves_dependency_closure() {
let unresolved = MetricSet::empty().with(Metric::Mi);
assert!(
!unresolved.contains(Metric::Loc),
"test premise: the verbatim set must omit Mi's deps"
);
let opts = MetricsOptions::default().with_metric_set(unresolved);
let space = analyze(
Source::new(LANG::Rust, SOURCE.as_bytes()).with_name(Some("lib.rs".to_owned())),
opts,
)
.expect("analyze must yield a top-level space");
let sel = space.metrics.selected();
assert!(sel.contains(Metric::Mi));
assert!(sel.contains(Metric::Loc), "Mi depends on Loc");
assert!(sel.contains(Metric::Cyclomatic), "Mi depends on Cyclomatic");
assert!(sel.contains(Metric::Halstead), "Mi depends on Halstead");
assert!(
space.metrics.loc.ploc() > 0,
"Loc must have run (Mi dependency); got ploc=0"
);
assert!(
space.metrics.cyclomatic.cyclomatic_sum() > 0,
"Cyclomatic must have run (Mi dependency); got sum=0"
);
let via_only = analyze(
Source::new(LANG::Rust, SOURCE.as_bytes()).with_name(Some("lib.rs".to_owned())),
MetricsOptions::default().with_only(&[Metric::Mi]),
)
.expect("analyze must yield a top-level space");
assert_eq!(sel, via_only.metrics.selected());
let mi_value = space.metrics.mi.original();
assert!(
mi_value.is_finite() && mi_value != 0.0,
"MI must be finite and non-default once its deps are computed; got {mi_value}"
);
assert_eq!(mi_value, via_only.metrics.mi.original());
}
#[test]
fn with_metric_set_passes_resolved_set_unchanged() {
let resolved = MetricSet::from_slice_with_deps(&[Metric::Mi]);
let opts = MetricsOptions::default().with_metric_set(resolved);
let space = analyze(
Source::new(LANG::Rust, SOURCE.as_bytes()).with_name(Some("lib.rs".to_owned())),
opts,
)
.expect("analyze must yield a top-level space");
assert_eq!(space.metrics.selected(), resolved);
}
}
#[cfg(feature = "rust")]
mod from_path_tests {
use crate::{Ast, FromPathError, LANG, MetricsOptions, SpaceKind};
use std::io::Write;
fn write_file(dir: &std::path::Path, name: &str, bytes: &[u8]) -> std::path::PathBuf {
let path = dir.join(name);
let mut f = std::fs::File::create(&path).expect("create temp file");
f.write_all(bytes).expect("write temp file");
path
}
#[test]
fn from_path_parses_and_detects_language() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_file(dir.path(), "foo.rs", b"fn main() {\n let x = 1;\n}\n");
let ast = Ast::from_path(&path).expect("from_path parses a .rs file");
assert_eq!(ast.language(), LANG::Rust);
assert_eq!(ast.source(), b"fn main() {\n let x = 1;\n}\n");
let space = ast
.metrics(MetricsOptions::default())
.expect("metrics from parsed file");
assert_eq!(space.kind, SpaceKind::Unit);
assert_eq!(space.name.as_deref(), path.to_str());
}
#[test]
fn from_path_normalizes_crlf_for_analyze_parity() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_file(
dir.path(),
"crlf.rs",
b"fn a() {\r\n let x = 1;\r\n}\r\n",
);
let ast = Ast::from_path(&path).expect("from_path parses CRLF source");
assert!(
!ast.source().contains(&b'\r'),
"carriage returns must be normalized away; got {:?}",
ast.source()
);
}
#[test]
fn from_path_unknown_extension_is_unknown_language() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_file(dir.path(), "mystery.zzz", b"some unrecognized content\n");
assert!(matches!(
Ast::from_path(&path),
Err(FromPathError::UnknownLanguage)
));
}
#[test]
fn from_path_missing_file_is_io_error() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("does-not-exist.rs");
assert!(matches!(Ast::from_path(&path), Err(FromPathError::Io(_))));
}
#[test]
fn from_path_tiny_or_binary_file_is_unreadable() {
let dir = tempfile::tempdir().expect("tempdir");
let tiny = write_file(dir.path(), "tiny.rs", b"ab");
assert!(matches!(
Ast::from_path(&tiny),
Err(FromPathError::Unreadable)
));
}
#[cfg(unix)]
#[test]
fn from_path_non_utf8_path_is_rejected() {
use std::os::unix::ffi::OsStrExt;
let bad = std::path::Path::new(std::ffi::OsStr::from_bytes(b"/tmp/\xff\xfe.rs"));
assert!(matches!(
Ast::from_path(bad),
Err(FromPathError::NonUtf8Path)
));
}
}
#[test]
fn code_metrics_display_concatenates_reported_submetrics_in_order() {
use crate::{Source, analyze};
let space = analyze(
Source::new(crate::LANG::Cpp, b"int add(int a, int b) { return a + b; }"),
MetricsOptions::default(),
)
.expect("analyze must yield a top-level space");
let m = &space.metrics;
let shown = format!("{m}");
let expected = format!(
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
m.nargs, m.nexits, m.cognitive, m.cyclomatic, m.halstead, m.loc, m.nom, m.tokens, m.mi
);
assert_eq!(
shown, expected,
"CodeMetrics Display must be the nine sub-metric Displays in order, newline-joined"
);
assert!(
!shown.ends_with('\n'),
"mi is the final block, so Display must not end with a trailing newline: {shown:?}"
);
}
#[cfg(feature = "cpp")]
#[test]
fn ast_debug_reports_language_and_name_non_exhaustively() {
use crate::{Ast, Source};
let ast = Ast::parse(
Source::new(crate::LANG::Cpp, b"int a = 42;").with_name(Some("dbg.cpp".to_owned())),
)
.expect("parse of a trivial C++ snippet must succeed");
let shown = format!("{ast:?}");
assert!(
shown.starts_with("Ast {"),
"Debug must render the struct by name: {shown:?}"
);
assert!(
shown.contains("language: Cpp"),
"Debug must report the resolved language value, not merely the label: {shown:?}"
);
assert!(
shown.contains("name: Some(\"dbg.cpp\")"),
"Debug must report the caller-supplied name value: {shown:?}"
);
assert!(
shown.trim_end().ends_with(".. }"),
"finish_non_exhaustive must elide the tree/source as a trailing `..`: {shown:?}"
);
}
#[test]
fn objc_func_space_tree_carries_names_and_kinds() {
use crate::ObjcParser;
let src = "\
@interface Greeter : NSObject
- (int)compute:(int)x;
@end
@implementation Greeter
- (int)compute:(int)x {
return x + 1;
}
@end
int helper(int y) {
return y * 2;
}
";
check_func_space::<ObjcParser, _>(src, "greeter.m", |root| {
assert_eq!(root.kind, SpaceKind::Unit, "root is the translation unit");
let top: Vec<(SpaceKind, Option<&str>)> = root
.spaces
.iter()
.map(|s| (s.kind, s.name.as_deref()))
.collect();
assert_eq!(
top,
vec![
(SpaceKind::Interface, Some("Greeter")),
(SpaceKind::Class, Some("Greeter")),
(SpaceKind::Function, Some("helper")),
],
"ObjC top-level spaces (kind, name) in source order; got {top:?}"
);
let impl_methods: Vec<Option<&str>> = root.spaces[1]
.spaces
.iter()
.map(|s| s.name.as_deref())
.collect();
assert_eq!(
impl_methods,
vec![Some("compute")],
"the @implementation's method is a named nested Function space"
);
});
}