use assert_cmd::Command as AssertCommand;
use predicates::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn tldr_cmd() -> Command {
Command::new(assert_cmd::cargo::cargo_bin!("tldr"))
}
fn tldr_assert_cmd() -> AssertCommand {
AssertCommand::new(assert_cmd::cargo::cargo_bin!("tldr"))
}
fn create_test_file(dir: &TempDir, name: &str, content: &str) -> PathBuf {
let path = dir.path().join(name);
fs::write(&path, content).unwrap();
path
}
mod patterns_types {
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DocstringStyle {
Google,
Numpy,
Sphinx,
Plain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EffectType {
Io,
GlobalWrite,
AttributeWrite,
CollectionModify,
Call,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConditionSource {
Guard,
Docstring,
TypeHint,
Assertion,
Icontract,
Deal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CohesionVerdict {
Cohesive,
SplitCandidate,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ComponentInfo {
pub methods: Vec<String>,
pub fields: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClassCohesion {
pub class_name: String,
pub file_path: String,
pub line: u32,
pub lcom4: u32,
pub method_count: u32,
pub field_count: u32,
pub verdict: CohesionVerdict,
pub split_suggestion: Option<String>,
pub components: Vec<ComponentInfo>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CohesionSummary {
pub total_classes: u32,
pub cohesive: u32,
pub split_candidates: u32,
pub avg_lcom4: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CohesionReport {
pub classes: Vec<ClassCohesion>,
pub summary: CohesionSummary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CouplingVerdict {
Low,
Moderate,
High,
VeryHigh,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CrossCall {
pub caller: String,
pub callee: String,
pub line: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CrossCalls {
pub calls: Vec<CrossCall>,
pub count: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CouplingReport {
pub path_a: String,
pub path_b: String,
pub a_to_b: CrossCalls,
pub b_to_a: CrossCalls,
pub total_calls: u32,
pub coupling_score: f64,
pub verdict: CouplingVerdict,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionInfo {
pub name: String,
pub signature: String,
pub docstring: Option<String>,
pub lineno: u32,
pub is_async: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MethodInfo {
pub name: String,
pub signature: String,
pub is_async: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClassInfo {
pub name: String,
pub lineno: u32,
pub bases: Vec<String>,
pub methods: Vec<MethodInfo>,
pub private_method_count: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InterfaceInfo {
pub file: String,
pub all_exports: Option<Vec<String>>,
pub functions: Vec<FunctionInfo>,
pub classes: Vec<ClassInfo>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TemporalExample {
pub file: String,
pub line: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TemporalConstraint {
pub before: String,
pub after: String,
pub support: u32,
pub confidence: f64,
pub examples: Vec<TemporalExample>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Trigram {
pub sequence: [String; 3],
pub support: u32,
pub confidence: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TemporalMetadata {
pub files_analyzed: u32,
pub sequences_extracted: u32,
pub min_support: u32,
pub min_confidence: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TemporalReport {
pub constraints: Vec<TemporalConstraint>,
pub trigrams: Vec<Trigram>,
pub metadata: TemporalMetadata,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Precondition {
pub param: String,
pub expression: Option<String>,
pub description: Option<String>,
pub type_hint: Option<String>,
pub source: ConditionSource,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Postcondition {
pub expression: Option<String>,
pub description: Option<String>,
pub type_hint: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExceptionInfo {
pub exception_type: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SideEffect {
pub effect_type: EffectType,
pub target: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FunctionBehavior {
pub function_name: String,
pub file_path: String,
pub line: u32,
pub purity_classification: String,
pub is_generator: bool,
pub is_async: bool,
pub preconditions: Vec<Precondition>,
pub postconditions: Vec<Postcondition>,
pub exceptions: Vec<ExceptionInfo>,
pub side_effects: Vec<SideEffect>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BehavioralReport {
pub file_path: String,
pub docstring_style: DocstringStyle,
pub has_icontract: bool,
pub has_deal: bool,
pub functions: Vec<FunctionBehavior>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResourceInfo {
pub name: String,
pub resource_type: String,
pub line: u32,
pub closed: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LeakInfo {
pub resource: String,
pub line: u32,
pub paths: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DoubleCloseInfo {
pub resource: String,
pub first_close: u32,
pub second_close: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UseAfterCloseInfo {
pub resource: String,
pub close_line: u32,
pub use_line: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextSuggestion {
pub resource: String,
pub suggestion: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResourceConstraint {
pub rule: String,
pub context: String,
pub confidence: f64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResourceSummary {
pub resources_detected: u32,
pub leaks_found: u32,
pub double_closes_found: u32,
pub use_after_closes_found: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResourceReport {
pub file: String,
pub language: String,
pub function: Option<String>,
pub resources: Vec<ResourceInfo>,
pub leaks: Vec<LeakInfo>,
pub double_closes: Vec<DoubleCloseInfo>,
pub use_after_closes: Vec<UseAfterCloseInfo>,
pub suggestions: Vec<ContextSuggestion>,
pub constraints: Vec<ResourceConstraint>,
pub summary: ResourceSummary,
pub analysis_time_ms: u64,
}
}
use patterns_types::*;
const PYTHON_CLASS_COHESIVE: &str = r#"
class Calculator:
"""A cohesive calculator class where all methods use the same state."""
def __init__(self, value: int = 0):
self.value = value
self.history = []
def add(self, x: int) -> int:
self.value += x
self.history.append(('add', x))
return self.value
def subtract(self, x: int) -> int:
self.value -= x
self.history.append(('sub', x))
return self.value
def get_value(self) -> int:
return self.value
def get_history(self) -> list:
return self.history
"#;
const PYTHON_CLASS_SPLIT_CANDIDATE: &str = r#"
class UserManager:
"""A class doing too many things - split candidate."""
def __init__(self):
# Auth-related
self.password_hash = None
self.session = None
# Profile-related
self.name = ""
self.email = ""
# Auth methods - use password_hash and session
def login(self, password: str) -> bool:
if self.verify_password(password):
self.session = self.create_session()
return True
return False
def logout(self):
self.session = None
def verify_password(self, password: str) -> bool:
return hash(password) == self.password_hash
def create_session(self):
return "session_token"
# Profile methods - use name and email only
def get_name(self) -> str:
return self.name
def set_name(self, name: str):
self.name = name
def get_email(self) -> str:
return self.email
def set_email(self, email: str):
self.email = email
"#;
const PYTHON_MODULE_A: &str = r#"
from module_b import helper_func, DataProcessor
def process_data(data):
"""Process data using module B's helper."""
processed = helper_func(data)
return processed
def analyze(items):
"""Analyze items using DataProcessor from module B."""
processor = DataProcessor()
result = processor.run(items)
return result
def standalone():
"""A function that doesn't call module B."""
return "standalone"
"#;
const PYTHON_MODULE_B: &str = r#"
from module_a import process_data
def helper_func(data):
"""Helper function called by module A."""
return [x * 2 for x in data]
class DataProcessor:
def run(self, items):
return len(items)
def validate(data):
"""Validates by calling process_data from module A."""
processed = process_data(data)
return len(processed) > 0
def pure_helper():
"""A function that doesn't call module A."""
return 42
"#;
const PYTHON_PURE_FUNCTIONS: &str = r#"
def add(a: int, b: int) -> int:
"""Pure function - no side effects."""
return a + b
def multiply(x: int, y: int) -> int:
"""Pure function - no side effects."""
return x * y
def transform(data: list) -> list:
"""Pure function - creates new list."""
return [x * 2 for x in data]
def calculate(a, b, c):
"""Pure function - arithmetic only."""
result = a + b
result = result * c
return result
"#;
const PYTHON_IMPURE_FUNCTIONS: &str = r#"
import os
counter = 0
def log_message(msg: str):
"""Impure - I/O operation."""
print(msg)
def read_file(path: str) -> str:
"""Impure - I/O operation."""
with open(path) as f:
return f.read()
def increment_counter():
"""Impure - global write."""
global counter
counter += 1
return counter
class Counter:
def __init__(self):
self.count = 0
def increment(self):
"""Impure - attribute write."""
self.count += 1
return self.count
def modify_list(items: list):
"""Impure - collection modification."""
items.append("new_item")
return items
"#;
const PYTHON_TEMPORAL_SEQUENCES: &str = r#"
def read_config(path):
f = open(path)
content = f.read()
f.close()
return content
def process_file(filename):
handle = open(filename, 'r')
data = handle.read()
result = parse(data)
handle.close()
return result
def copy_file(src, dst):
src_file = open(src, 'r')
dst_file = open(dst, 'w')
content = src_file.read()
dst_file.write(content)
src_file.close()
dst_file.close()
def safe_read(path):
with open(path) as f:
return f.read()
def acquire_and_release():
lock = acquire()
do_work()
release()
"#;
const PYTHON_PUBLIC_API: &str = r#"
"""Public API module."""
__all__ = ['PublicClass', 'public_function', 'CONSTANT']
CONSTANT = 42
_PRIVATE_CONSTANT = "private"
def public_function(x: int, y: str = "default") -> bool:
"""A public function."""
return True
def _private_helper():
"""Private helper function."""
pass
async def async_public(data: list) -> dict:
"""Async public function."""
return {}
class PublicClass:
"""A public class."""
def __init__(self, name: str):
self.name = name
def public_method(self) -> str:
"""Public method."""
return self.name
async def async_method(self) -> int:
"""Async public method."""
return 42
def _private_method(self):
"""Private method."""
pass
class _PrivateClass:
"""Private class."""
pass
"#;
const PYTHON_RESOURCE_LEAK: &str = r#"
def leaky_function(path):
"""Function with potential resource leak."""
f = open(path)
if some_condition():
return None # Leak: f not closed on this path
content = f.read()
f.close()
return content
def double_close(path):
"""Function that closes resource twice."""
f = open(path)
content = f.read()
f.close()
# ... more code ...
f.close() # Double close
return content
def use_after_close(path):
"""Function that uses resource after closing."""
f = open(path)
f.close()
content = f.read() # Use after close
return content
def safe_with_context(path):
"""Safe function using context manager."""
with open(path) as f:
return f.read()
"#;
const PYTHON_BEHAVIORAL: &str = r#"
def process_positive(x: int) -> int:
"""Process a positive number.
Args:
x: Must be positive
Returns:
The doubled value
Raises:
ValueError: If x is not positive
"""
if x <= 0:
raise ValueError("x must be positive")
result = x * 2
assert result > 0, "Result should be positive"
return result
def validate_string(s: str) -> str:
"""Validate and return the string.
Args:
s: Input string, cannot be empty
Returns:
The stripped string
"""
if not isinstance(s, str):
raise TypeError("Expected string")
if len(s) == 0:
raise ValueError("String cannot be empty")
return s.strip()
def divide(a: float, b: float) -> float:
"""Divide a by b.
Raises:
ZeroDivisionError: If b is zero
"""
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
"#;
mod cohesion_command {
use super::*;
#[test]
#[ignore = "cohesion command not yet implemented"]
fn test_cohesion_help() {
tldr_assert_cmd()
.args(["cohesion", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("path"))
.stdout(predicate::str::contains("--min-methods"))
.stdout(predicate::str::contains("--format"));
}
#[test]
#[ignore = "cohesion command not yet implemented"]
fn test_cohesion_cohesive_class() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "cohesive.py", PYTHON_CLASS_COHESIVE);
let output = tldr_cmd()
.args(["cohesion", file_path.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let report: CohesionReport =
serde_json::from_str(&stdout).expect("Should return valid JSON CohesionReport");
assert!(!report.classes.is_empty(), "Should find at least one class");
let calc_class = report.classes.iter().find(|c| c.class_name == "Calculator");
assert!(calc_class.is_some(), "Should find Calculator class");
let calc = calc_class.unwrap();
assert_eq!(calc.lcom4, 1, "Cohesive class should have LCOM4=1");
assert_eq!(calc.verdict, CohesionVerdict::Cohesive);
}
#[test]
#[ignore = "cohesion command not yet implemented"]
fn test_cohesion_split_candidate() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "split.py", PYTHON_CLASS_SPLIT_CANDIDATE);
let output = tldr_cmd()
.args(["cohesion", file_path.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let report: CohesionReport = serde_json::from_str(&stdout).unwrap();
let user_class = report
.classes
.iter()
.find(|c| c.class_name == "UserManager");
assert!(user_class.is_some(), "Should find UserManager class");
let user = user_class.unwrap();
assert!(user.lcom4 > 1, "Split candidate should have LCOM4 > 1");
assert_eq!(user.verdict, CohesionVerdict::SplitCandidate);
assert!(
user.split_suggestion.is_some(),
"Should provide split suggestion"
);
assert!(
!user.components.is_empty(),
"Should identify connected components"
);
}
#[test]
#[ignore = "cohesion command not yet implemented"]
fn test_cohesion_min_methods_filter() {
let temp = TempDir::new().unwrap();
let code = r#"
class TinyClass:
def single_method(self):
return 42
"#;
let file_path = create_test_file(&temp, "tiny.py", code);
let output = tldr_cmd()
.args([
"cohesion",
file_path.to_str().unwrap(),
"--min-methods",
"2",
])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let report: CohesionReport = serde_json::from_str(&stdout).unwrap();
assert!(
report.classes.is_empty()
|| !report.classes.iter().any(|c| c.class_name == "TinyClass"),
"Single-method class should be filtered with --min-methods 2"
);
}
#[test]
#[ignore = "cohesion command not yet implemented"]
fn test_cohesion_include_dunder() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "dunder.py", PYTHON_CLASS_COHESIVE);
let output_without = tldr_cmd()
.args(["cohesion", file_path.to_str().unwrap()])
.output()
.unwrap();
let output_with = tldr_cmd()
.args(["cohesion", file_path.to_str().unwrap(), "--include-dunder"])
.output()
.unwrap();
assert!(output_without.status.success());
assert!(output_with.status.success());
let report_without: CohesionReport =
serde_json::from_str(&String::from_utf8_lossy(&output_without.stdout)).unwrap();
let report_with: CohesionReport =
serde_json::from_str(&String::from_utf8_lossy(&output_with.stdout)).unwrap();
if let (Some(class_without), Some(class_with)) =
(report_without.classes.first(), report_with.classes.first())
{
assert!(
class_with.method_count >= class_without.method_count,
"Including dunder should include more methods"
);
}
}
#[test]
#[ignore = "cohesion command not yet implemented"]
fn test_cohesion_text_output() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "cohesive.py", PYTHON_CLASS_COHESIVE);
tldr_assert_cmd()
.args(["cohesion", file_path.to_str().unwrap(), "--format", "text"])
.assert()
.success()
.stdout(predicate::str::contains("Class:"))
.stdout(predicate::str::contains("LCOM4:"))
.stdout(predicate::str::contains("Verdict:"));
}
#[test]
#[ignore = "cohesion command not yet implemented"]
fn test_cohesion_file_not_found() {
tldr_assert_cmd()
.args(["cohesion", "/nonexistent/file.py"])
.assert()
.failure()
.stderr(predicate::str::contains("file not found"));
}
#[test]
#[ignore = "cohesion command not yet implemented"]
fn test_cohesion_directory_mode() {
let temp = TempDir::new().unwrap();
create_test_file(&temp, "a.py", PYTHON_CLASS_COHESIVE);
create_test_file(&temp, "b.py", PYTHON_CLASS_SPLIT_CANDIDATE);
let output = tldr_cmd()
.args(["cohesion", temp.path().to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let report: CohesionReport = serde_json::from_str(&stdout).unwrap();
assert!(
report.classes.len() >= 2,
"Should analyze classes from multiple files"
);
}
#[test]
#[ignore = "cohesion command not yet implemented"]
fn test_cohesion_empty_class() {
let temp = TempDir::new().unwrap();
let code = "class Empty: pass";
let file_path = create_test_file(&temp, "empty.py", code);
let output = tldr_cmd()
.args(["cohesion", file_path.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
}
#[test]
#[ignore = "cohesion command not yet implemented"]
fn test_cohesion_staticmethods_excluded() {
let temp = TempDir::new().unwrap();
let code = r#"
class WithStatic:
def __init__(self):
self.value = 0
@staticmethod
def static_method():
return 42
def instance_method(self):
return self.value
"#;
let file_path = create_test_file(&temp, "static.py", code);
let output = tldr_cmd()
.args(["cohesion", file_path.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
}
#[test]
#[ignore = "cohesion command not yet implemented"]
fn test_cohesion_nested_classes() {
let temp = TempDir::new().unwrap();
let code = r#"
class Outer:
class Inner:
def inner_method(self):
return 1
def outer_method(self):
return 2
"#;
let file_path = create_test_file(&temp, "nested.py", code);
let output = tldr_cmd()
.args(["cohesion", file_path.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
}
#[test]
#[ignore = "cohesion command not yet implemented"]
fn test_cohesion_inheritance() {
let temp = TempDir::new().unwrap();
let code = r#"
class Base:
def base_method(self):
return self.value
class Derived(Base):
def derived_method(self):
return self.value * 2
"#;
let file_path = create_test_file(&temp, "inherit.py", code);
let output = tldr_cmd()
.args(["cohesion", file_path.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
}
#[test]
#[ignore = "cohesion command not yet implemented"]
fn test_cohesion_summary_stats() {
let temp = TempDir::new().unwrap();
create_test_file(&temp, "a.py", PYTHON_CLASS_COHESIVE);
create_test_file(&temp, "b.py", PYTHON_CLASS_SPLIT_CANDIDATE);
let output = tldr_cmd()
.args(["cohesion", temp.path().to_str().unwrap()])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: CohesionReport = serde_json::from_str(&stdout).unwrap();
assert!(report.summary.total_classes >= 2);
assert!(report.summary.avg_lcom4 > 0.0);
}
}
mod coupling_command {
use super::*;
#[test]
#[ignore = "coupling command not yet implemented"]
fn test_coupling_help() {
tldr_assert_cmd()
.args(["coupling", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("path_a"))
.stdout(predicate::str::contains("path_b"))
.stdout(predicate::str::contains("--format"));
}
#[test]
#[ignore = "coupling command not yet implemented"]
fn test_coupling_two_modules() {
let temp = TempDir::new().unwrap();
let file_a = create_test_file(&temp, "module_a.py", PYTHON_MODULE_A);
let file_b = create_test_file(&temp, "module_b.py", PYTHON_MODULE_B);
let output = tldr_cmd()
.args([
"coupling",
file_a.to_str().unwrap(),
file_b.to_str().unwrap(),
])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let report: CouplingReport =
serde_json::from_str(&stdout).expect("Should return valid JSON CouplingReport");
assert!(report.a_to_b.count > 0, "Module A should call module B");
assert!(report.b_to_a.count > 0, "Module B should call module A");
assert!(report.total_calls > 0);
assert!(report.coupling_score >= 0.0 && report.coupling_score <= 1.0);
}
#[test]
#[ignore = "coupling command not yet implemented"]
fn test_coupling_bidirectional() {
let temp = TempDir::new().unwrap();
let file_a = create_test_file(&temp, "module_a.py", PYTHON_MODULE_A);
let file_b = create_test_file(&temp, "module_b.py", PYTHON_MODULE_B);
let output = tldr_cmd()
.args([
"coupling",
file_a.to_str().unwrap(),
file_b.to_str().unwrap(),
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: CouplingReport = serde_json::from_str(&stdout).unwrap();
assert!(!report.a_to_b.calls.is_empty());
assert!(!report.b_to_a.calls.is_empty());
}
#[test]
#[ignore = "coupling command not yet implemented"]
fn test_coupling_no_coupling() {
let temp = TempDir::new().unwrap();
let code_a = "def func_a(): return 1";
let code_b = "def func_b(): return 2";
let file_a = create_test_file(&temp, "a.py", code_a);
let file_b = create_test_file(&temp, "b.py", code_b);
let output = tldr_cmd()
.args([
"coupling",
file_a.to_str().unwrap(),
file_b.to_str().unwrap(),
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: CouplingReport = serde_json::from_str(&stdout).unwrap();
assert_eq!(report.total_calls, 0, "Should have no coupling");
assert_eq!(report.coupling_score, 0.0);
assert_eq!(report.verdict, CouplingVerdict::Low);
}
#[test]
#[ignore = "coupling command not yet implemented"]
fn test_coupling_verdict_levels() {
let temp = TempDir::new().unwrap();
let file_a = create_test_file(&temp, "module_a.py", PYTHON_MODULE_A);
let file_b = create_test_file(&temp, "module_b.py", PYTHON_MODULE_B);
let output = tldr_cmd()
.args([
"coupling",
file_a.to_str().unwrap(),
file_b.to_str().unwrap(),
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: CouplingReport = serde_json::from_str(&stdout).unwrap();
match report.verdict {
CouplingVerdict::Low => assert!(report.coupling_score < 0.2),
CouplingVerdict::Moderate => {
assert!(report.coupling_score >= 0.2 && report.coupling_score < 0.4)
}
CouplingVerdict::High => {
assert!(report.coupling_score >= 0.4 && report.coupling_score < 0.6)
}
CouplingVerdict::VeryHigh => assert!(report.coupling_score >= 0.6),
}
}
#[test]
#[ignore = "coupling command not yet implemented"]
fn test_coupling_text_output() {
let temp = TempDir::new().unwrap();
let file_a = create_test_file(&temp, "module_a.py", PYTHON_MODULE_A);
let file_b = create_test_file(&temp, "module_b.py", PYTHON_MODULE_B);
tldr_assert_cmd()
.args([
"coupling",
file_a.to_str().unwrap(),
file_b.to_str().unwrap(),
"--format",
"text",
])
.assert()
.success()
.stdout(predicate::str::contains("Coupling:"))
.stdout(predicate::str::contains("Score:"))
.stdout(predicate::str::contains("Verdict:"));
}
#[test]
#[ignore = "coupling command not yet implemented"]
fn test_coupling_file_not_found() {
let temp = TempDir::new().unwrap();
let file_a = create_test_file(&temp, "a.py", "def a(): pass");
tldr_assert_cmd()
.args(["coupling", file_a.to_str().unwrap(), "/nonexistent/file.py"])
.assert()
.failure()
.stderr(predicate::str::contains("file not found"));
}
#[test]
#[ignore = "coupling command not yet implemented"]
fn test_coupling_same_file_error() {
let temp = TempDir::new().unwrap();
let file_a = create_test_file(&temp, "a.py", "def a(): pass");
tldr_assert_cmd()
.args([
"coupling",
file_a.to_str().unwrap(),
file_a.to_str().unwrap(),
])
.assert()
.failure()
.stderr(predicate::str::contains("same file"));
}
#[test]
#[ignore = "coupling command not yet implemented"]
fn test_coupling_import_tracking() {
let temp = TempDir::new().unwrap();
let file_a = create_test_file(&temp, "module_a.py", PYTHON_MODULE_A);
let file_b = create_test_file(&temp, "module_b.py", PYTHON_MODULE_B);
let output = tldr_cmd()
.args([
"coupling",
file_a.to_str().unwrap(),
file_b.to_str().unwrap(),
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: CouplingReport = serde_json::from_str(&stdout).unwrap();
for call in &report.a_to_b.calls {
assert!(!call.caller.is_empty());
assert!(!call.callee.is_empty());
assert!(call.line > 0);
}
}
#[test]
#[ignore = "coupling command not yet implemented"]
fn test_coupling_transitive() {
let temp = TempDir::new().unwrap();
let file_a = create_test_file(&temp, "module_a.py", PYTHON_MODULE_A);
let file_b = create_test_file(&temp, "module_b.py", PYTHON_MODULE_B);
let output = tldr_cmd()
.args([
"coupling",
file_a.to_str().unwrap(),
file_b.to_str().unwrap(),
])
.output()
.unwrap();
assert!(output.status.success());
}
#[test]
#[ignore = "coupling command not yet implemented"]
fn test_coupling_score_calculation() {
let temp = TempDir::new().unwrap();
let file_a = create_test_file(&temp, "module_a.py", PYTHON_MODULE_A);
let file_b = create_test_file(&temp, "module_b.py", PYTHON_MODULE_B);
let output = tldr_cmd()
.args([
"coupling",
file_a.to_str().unwrap(),
file_b.to_str().unwrap(),
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: CouplingReport = serde_json::from_str(&stdout).unwrap();
assert!(report.coupling_score >= 0.0);
assert!(report.coupling_score <= 1.0);
}
#[test]
#[ignore = "coupling command not yet implemented"]
fn test_coupling_call_lines() {
let temp = TempDir::new().unwrap();
let file_a = create_test_file(&temp, "module_a.py", PYTHON_MODULE_A);
let file_b = create_test_file(&temp, "module_b.py", PYTHON_MODULE_B);
let output = tldr_cmd()
.args([
"coupling",
file_a.to_str().unwrap(),
file_b.to_str().unwrap(),
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: CouplingReport = serde_json::from_str(&stdout).unwrap();
for call in &report.a_to_b.calls {
assert!(call.line > 0, "Call should have valid line number");
}
}
#[test]
#[ignore = "coupling command not yet implemented"]
fn test_coupling_circular() {
let temp = TempDir::new().unwrap();
let file_a = create_test_file(&temp, "module_a.py", PYTHON_MODULE_A);
let file_b = create_test_file(&temp, "module_b.py", PYTHON_MODULE_B);
let output = tldr_cmd()
.args([
"coupling",
file_a.to_str().unwrap(),
file_b.to_str().unwrap(),
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: CouplingReport = serde_json::from_str(&stdout).unwrap();
if report.a_to_b.count > 0 && report.b_to_a.count > 0 {
assert!(report.total_calls > 0);
}
}
}
mod interface_command {
use super::*;
#[test]
#[ignore = "interface command not yet implemented"]
fn test_interface_help() {
tldr_assert_cmd()
.args(["interface", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("path"))
.stdout(predicate::str::contains("--lang"))
.stdout(predicate::str::contains("--format"));
}
#[test]
#[ignore = "interface command not yet implemented"]
fn test_interface_functions() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "api.py", PYTHON_PUBLIC_API);
let output = tldr_cmd()
.args(["interface", file_path.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let info: InterfaceInfo =
serde_json::from_str(&stdout).expect("Should return valid JSON InterfaceInfo");
let public_fn = info.functions.iter().find(|f| f.name == "public_function");
assert!(public_fn.is_some(), "Should find public_function");
let func = public_fn.unwrap();
assert!(func.signature.contains("x: int"));
assert!(func.signature.contains("-> bool"));
assert!(!func.is_async);
}
#[test]
#[ignore = "interface command not yet implemented"]
fn test_interface_classes() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "api.py", PYTHON_PUBLIC_API);
let output = tldr_cmd()
.args(["interface", file_path.to_str().unwrap()])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let info: InterfaceInfo = serde_json::from_str(&stdout).unwrap();
let public_class = info.classes.iter().find(|c| c.name == "PublicClass");
assert!(public_class.is_some(), "Should find PublicClass");
let class = public_class.unwrap();
assert!(class.lineno > 0);
assert!(!class.methods.is_empty(), "Should have public methods");
assert!(
class.methods.iter().any(|m| m.name == "public_method"),
"Should include public_method"
);
}
#[test]
#[ignore = "interface command not yet implemented"]
fn test_interface_all_exports() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "api.py", PYTHON_PUBLIC_API);
let output = tldr_cmd()
.args(["interface", file_path.to_str().unwrap()])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let info: InterfaceInfo = serde_json::from_str(&stdout).unwrap();
assert!(info.all_exports.is_some(), "Should detect __all__");
let exports = info.all_exports.unwrap();
assert!(exports.contains(&"PublicClass".to_string()));
assert!(exports.contains(&"public_function".to_string()));
assert!(exports.contains(&"CONSTANT".to_string()));
}
#[test]
#[ignore = "interface command not yet implemented"]
fn test_interface_private_excluded() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "api.py", PYTHON_PUBLIC_API);
let output = tldr_cmd()
.args(["interface", file_path.to_str().unwrap()])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let info: InterfaceInfo = serde_json::from_str(&stdout).unwrap();
assert!(
!info.functions.iter().any(|f| f.name == "_private_helper"),
"Private functions should be excluded"
);
assert!(
!info.classes.iter().any(|c| c.name == "_PrivateClass"),
"Private classes should be excluded"
);
}
#[test]
#[ignore = "interface command not yet implemented"]
fn test_interface_directory_mode() {
let temp = TempDir::new().unwrap();
create_test_file(&temp, "api.py", PYTHON_PUBLIC_API);
create_test_file(&temp, "utils.py", PYTHON_PURE_FUNCTIONS);
let output = tldr_cmd()
.args(["interface", temp.path().to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let json: Value = serde_json::from_str(&stdout).unwrap();
assert!(
json.is_array() || json.get("files").is_some(),
"Directory mode should return multiple file info"
);
}
#[test]
#[ignore = "interface command not yet implemented"]
fn test_interface_text_output() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "api.py", PYTHON_PUBLIC_API);
tldr_assert_cmd()
.args(["interface", file_path.to_str().unwrap(), "--format", "text"])
.assert()
.success()
.stdout(predicate::str::contains("File:"))
.stdout(predicate::str::contains("Functions:"))
.stdout(predicate::str::contains("Classes:"));
}
#[test]
#[ignore = "interface command not yet implemented"]
fn test_interface_file_not_found() {
tldr_assert_cmd()
.args(["interface", "/nonexistent/file.py"])
.assert()
.failure()
.stderr(predicate::str::contains("file not found"));
}
#[test]
#[ignore = "interface command not yet implemented"]
fn test_interface_async_functions() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "api.py", PYTHON_PUBLIC_API);
let output = tldr_cmd()
.args(["interface", file_path.to_str().unwrap()])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let info: InterfaceInfo = serde_json::from_str(&stdout).unwrap();
let async_fn = info.functions.iter().find(|f| f.name == "async_public");
assert!(async_fn.is_some());
assert!(async_fn.unwrap().is_async, "Should mark async functions");
}
#[test]
#[ignore = "interface command not yet implemented"]
fn test_interface_signatures() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "api.py", PYTHON_PUBLIC_API);
let output = tldr_cmd()
.args(["interface", file_path.to_str().unwrap()])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let info: InterfaceInfo = serde_json::from_str(&stdout).unwrap();
let public_fn = info.functions.iter().find(|f| f.name == "public_function");
assert!(public_fn.is_some());
assert!(public_fn.unwrap().signature.contains("int"));
}
#[test]
#[ignore = "interface command not yet implemented"]
fn test_interface_docstrings() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "api.py", PYTHON_PUBLIC_API);
let output = tldr_cmd()
.args(["interface", file_path.to_str().unwrap()])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let info: InterfaceInfo = serde_json::from_str(&stdout).unwrap();
let public_fn = info.functions.iter().find(|f| f.name == "public_function");
assert!(public_fn.is_some());
assert!(public_fn.unwrap().docstring.is_some());
}
}
mod temporal_command {
use super::*;
#[test]
fn test_temporal_help() {
tldr_assert_cmd()
.args(["temporal", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("path"))
.stdout(predicate::str::contains("--min-support"))
.stdout(predicate::str::contains("--min-confidence"));
}
#[test]
fn test_temporal_basic_sequence() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "sequences.py", PYTHON_TEMPORAL_SEQUENCES);
let output = tldr_cmd()
.args(["temporal", file_path.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let report: TemporalReport =
serde_json::from_str(&stdout).expect("Should return valid JSON TemporalReport");
let read_close = report
.constraints
.iter()
.find(|c| c.before == "read" && c.after == "close");
assert!(read_close.is_some(), "Should find read->close pattern");
let constraint = read_close.unwrap();
assert!(constraint.support >= 2, "Should have support >= 2");
assert!(constraint.confidence > 0.0);
}
#[test]
fn test_temporal_min_support_filter() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "sequences.py", PYTHON_TEMPORAL_SEQUENCES);
let output = tldr_cmd()
.args([
"temporal",
file_path.to_str().unwrap(),
"--min-support",
"10",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: TemporalReport = serde_json::from_str(&stdout).unwrap();
for constraint in &report.constraints {
assert!(
constraint.support >= 10,
"All constraints should meet min-support threshold"
);
}
}
#[test]
fn test_temporal_min_confidence_filter() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "sequences.py", PYTHON_TEMPORAL_SEQUENCES);
let output = tldr_cmd()
.args([
"temporal",
file_path.to_str().unwrap(),
"--min-confidence",
"0.9",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: TemporalReport = serde_json::from_str(&stdout).unwrap();
for constraint in &report.constraints {
assert!(
constraint.confidence >= 0.9,
"All constraints should meet min-confidence threshold"
);
}
}
#[test]
fn test_temporal_query_filter() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "sequences.py", PYTHON_TEMPORAL_SEQUENCES);
let output = tldr_cmd()
.args(["temporal", file_path.to_str().unwrap(), "--query", "open"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: TemporalReport = serde_json::from_str(&stdout).unwrap();
for constraint in &report.constraints {
assert!(
constraint.before == "open" || constraint.after == "open",
"Query filter should only return patterns involving 'open'"
);
}
}
#[test]
fn test_temporal_trigrams() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "sequences.py", PYTHON_TEMPORAL_SEQUENCES);
let output = tldr_cmd()
.args([
"temporal",
file_path.to_str().unwrap(),
"--include-trigrams",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: TemporalReport = serde_json::from_str(&stdout).unwrap();
assert!(report.trigrams.is_empty() || !report.trigrams.is_empty());
}
#[test]
fn test_temporal_examples() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "sequences.py", PYTHON_TEMPORAL_SEQUENCES);
let output = tldr_cmd()
.args([
"temporal",
file_path.to_str().unwrap(),
"--include-examples",
"3",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: TemporalReport = serde_json::from_str(&stdout).unwrap();
for constraint in &report.constraints {
assert!(
constraint.examples.len() <= 3,
"Should limit examples to requested count"
);
for example in &constraint.examples {
assert!(!example.file.is_empty());
assert!(example.line > 0);
}
}
}
#[test]
#[ignore = "--format text not yet implemented for temporal command"]
fn test_temporal_text_output() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "sequences.py", PYTHON_TEMPORAL_SEQUENCES);
tldr_assert_cmd()
.args(["temporal", file_path.to_str().unwrap(), "--format", "text"])
.assert()
.success()
.stdout(predicate::str::contains("->"))
.stdout(predicate::str::contains("support"))
.stdout(predicate::str::contains("confidence"));
}
#[test]
fn test_temporal_no_sequences_exit_2() {
let temp = TempDir::new().unwrap();
let code = "x = 1\ny = 2";
let file_path = create_test_file(&temp, "no_calls.py", code);
let output = tldr_cmd()
.args([
"temporal",
file_path.to_str().unwrap(),
"--min-support",
"100",
])
.output()
.unwrap();
assert_eq!(
output.status.code(),
Some(2),
"Should exit with code 2 when no constraints found"
);
}
#[test]
fn test_temporal_directory_not_found() {
tldr_assert_cmd()
.args(["temporal", "/nonexistent/dir"])
.assert()
.failure()
.stderr(predicate::str::contains("not found"));
}
#[test]
#[ignore = "--max-files flag may not be working correctly"]
fn test_temporal_max_files_limit() {
let temp = TempDir::new().unwrap();
for i in 0..10 {
create_test_file(&temp, &format!("file_{}.py", i), PYTHON_TEMPORAL_SEQUENCES);
}
let output = tldr_cmd()
.args([
"temporal",
temp.path().to_str().unwrap(),
"--max-files",
"5",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: TemporalReport = serde_json::from_str(&stdout).unwrap();
assert!(
report.metadata.files_analyzed <= 5,
"Should respect max-files limit"
);
}
#[test]
fn test_temporal_multi_language() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "sequences.py", PYTHON_TEMPORAL_SEQUENCES);
let output = tldr_cmd()
.args(["temporal", file_path.to_str().unwrap(), "--lang", "python"])
.output()
.unwrap();
assert!(output.status.success());
}
#[test]
fn test_temporal_nested_sequences() {
let temp = TempDir::new().unwrap();
let code = r#"
def nested():
outer = open("outer")
inner = open("inner")
inner.read()
inner.close()
outer.read()
outer.close()
"#;
let file_path = create_test_file(&temp, "nested.py", code);
let output = tldr_cmd()
.args(["temporal", file_path.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
}
}
mod behavioral_command {
use super::*;
#[test]
fn test_behavioral_help() {
tldr_assert_cmd()
.args(["behavioral", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("FILE"))
.stdout(predicate::str::contains("[FUNCTION]"))
.stdout(predicate::str::contains("--constraints"));
}
#[test]
fn test_behavioral_preconditions() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "behavioral.py", PYTHON_BEHAVIORAL);
let output = tldr_cmd()
.args([
"behavioral",
file_path.to_str().unwrap(),
"process_positive",
])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let report: BehavioralReport =
serde_json::from_str(&stdout).expect("Should return valid JSON BehavioralReport");
let func = report
.functions
.iter()
.find(|f| f.function_name == "process_positive");
assert!(func.is_some(), "Should find process_positive");
let func = func.unwrap();
assert!(
!func.preconditions.is_empty(),
"Should detect preconditions"
);
let has_x_precond = func
.preconditions
.iter()
.any(|p| p.param == "x" && p.expression.as_ref().is_some_and(|e| e.contains(">")));
assert!(has_x_precond, "Should find x > 0 precondition");
}
#[test]
fn test_behavioral_postconditions() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "behavioral.py", PYTHON_BEHAVIORAL);
let output = tldr_cmd()
.args([
"behavioral",
file_path.to_str().unwrap(),
"process_positive",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: BehavioralReport = serde_json::from_str(&stdout).unwrap();
let func = report
.functions
.iter()
.find(|f| f.function_name == "process_positive");
assert!(func.is_some());
let func = func.unwrap();
assert!(
!func.postconditions.is_empty(),
"Should detect postconditions"
);
}
#[test]
fn test_behavioral_exceptions() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "behavioral.py", PYTHON_BEHAVIORAL);
let output = tldr_cmd()
.args(["behavioral", file_path.to_str().unwrap(), "divide"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: BehavioralReport = serde_json::from_str(&stdout).unwrap();
let func = report
.functions
.iter()
.find(|f| f.function_name == "divide");
assert!(func.is_some());
let func = func.unwrap();
assert!(!func.exceptions.is_empty(), "Should detect exceptions");
let has_zero_div = func
.exceptions
.iter()
.any(|e| e.exception_type == "ZeroDivisionError");
assert!(has_zero_div, "Should detect ZeroDivisionError");
}
#[test]
fn test_behavioral_side_effects() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "impure.py", PYTHON_IMPURE_FUNCTIONS);
let output = tldr_cmd()
.args(["behavioral", file_path.to_str().unwrap(), "log_message"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: BehavioralReport = serde_json::from_str(&stdout).unwrap();
let func = report
.functions
.iter()
.find(|f| f.function_name == "log_message");
assert!(func.is_some());
let func = func.unwrap();
assert_eq!(
func.purity_classification, "impure",
"log_message should be impure"
);
assert!(!func.side_effects.is_empty(), "Should detect side effects");
}
#[test]
fn test_behavioral_function_filter() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "behavioral.py", PYTHON_BEHAVIORAL);
let output = tldr_cmd()
.args(["behavioral", file_path.to_str().unwrap(), "divide"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: BehavioralReport = serde_json::from_str(&stdout).unwrap();
assert_eq!(
report.functions.len(),
1,
"Should only analyze specified function"
);
assert_eq!(report.functions[0].function_name, "divide");
}
#[test]
fn test_behavioral_constraints_flag() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "behavioral.py", PYTHON_BEHAVIORAL);
let output = tldr_cmd()
.args(["behavioral", file_path.to_str().unwrap(), "--constraints"])
.output()
.unwrap();
assert!(output.status.success());
}
#[test]
fn test_behavioral_text_output() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "behavioral.py", PYTHON_BEHAVIORAL);
tldr_assert_cmd()
.args([
"behavioral",
file_path.to_str().unwrap(),
"--output",
"text",
])
.assert()
.success()
.stdout(predicate::str::contains("Function:"))
.stdout(predicate::str::contains("Preconditions:"));
}
#[test]
fn test_behavioral_file_not_found() {
tldr_assert_cmd()
.args(["behavioral", "/nonexistent/file.py"])
.assert()
.failure()
.stderr(predicate::str::contains("file not found"));
}
#[test]
fn test_behavioral_file_too_large() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "large.py", "def f(): pass");
let output = tldr_cmd()
.args(["behavioral", file_path.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
}
#[test]
fn test_behavioral_not_python_file() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "file.txt", "not python code");
tldr_assert_cmd()
.args(["behavioral", file_path.to_str().unwrap()])
.assert()
.failure();
}
#[test]
fn test_behavioral_docstring_styles() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "behavioral.py", PYTHON_BEHAVIORAL);
let output = tldr_cmd()
.args(["behavioral", file_path.to_str().unwrap()])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: BehavioralReport = serde_json::from_str(&stdout).unwrap();
assert!(
report.docstring_style == DocstringStyle::Google
|| report.docstring_style == DocstringStyle::Numpy
|| report.docstring_style == DocstringStyle::Sphinx
|| report.docstring_style == DocstringStyle::Plain
);
}
#[test]
fn test_behavioral_class_methods() {
let temp = TempDir::new().unwrap();
let code = r#"
class MyClass:
def method(self, x: int) -> int:
"""Method with precondition.
Args:
x: Must be positive
"""
if x <= 0:
raise ValueError("x must be positive")
return x * 2
"#;
let file_path = create_test_file(&temp, "cls.py", code);
let output = tldr_cmd()
.args(["behavioral", file_path.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
}
}
mod resources_command {
use super::*;
#[test]
fn test_resources_help() {
tldr_assert_cmd()
.args(["resources", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("file"))
.stdout(predicate::str::contains("[FUNCTION]"))
.stdout(predicate::str::contains("--check-all"));
}
#[test]
fn test_resources_detect_leak() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "leak.py", PYTHON_RESOURCE_LEAK);
let output = tldr_cmd()
.args(["resources", file_path.to_str().unwrap(), "leaky_function"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: ResourceReport =
serde_json::from_str(&stdout).expect("Should return valid JSON ResourceReport");
assert!(!report.leaks.is_empty(), "Should detect potential leak");
}
#[test]
fn test_resources_no_leak_context_manager() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "leak.py", PYTHON_RESOURCE_LEAK);
let output = tldr_cmd()
.args([
"resources",
file_path.to_str().unwrap(),
"safe_with_context",
])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let report: ResourceReport = serde_json::from_str(&stdout).unwrap();
assert!(
report.leaks.is_empty(),
"Context manager should prevent leak"
);
}
#[test]
fn test_resources_double_close() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "leak.py", PYTHON_RESOURCE_LEAK);
let output = tldr_cmd()
.args([
"resources",
file_path.to_str().unwrap(),
"double_close",
"--check-double-close",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: ResourceReport = serde_json::from_str(&stdout).unwrap();
assert!(
!report.double_closes.is_empty(),
"Should detect double close"
);
}
#[test]
fn test_resources_use_after_close() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "leak.py", PYTHON_RESOURCE_LEAK);
let output = tldr_cmd()
.args([
"resources",
file_path.to_str().unwrap(),
"use_after_close",
"--check-use-after-close",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: ResourceReport = serde_json::from_str(&stdout).unwrap();
assert!(
!report.use_after_closes.is_empty(),
"Should detect use after close"
);
}
#[test]
fn test_resources_check_all_flag() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "leak.py", PYTHON_RESOURCE_LEAK);
let output = tldr_cmd()
.args(["resources", file_path.to_str().unwrap(), "--check-all"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: ResourceReport = serde_json::from_str(&stdout).unwrap();
assert!(report.summary.resources_detected > 0);
}
#[test]
fn test_resources_suggest_context() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "leak.py", PYTHON_RESOURCE_LEAK);
let output = tldr_cmd()
.args([
"resources",
file_path.to_str().unwrap(),
"leaky_function",
"--suggest-context",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: ResourceReport = serde_json::from_str(&stdout).unwrap();
assert!(
!report.suggestions.is_empty(),
"Should suggest context manager"
);
}
#[test]
fn test_resources_show_paths() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "leak.py", PYTHON_RESOURCE_LEAK);
let output = tldr_cmd()
.args([
"resources",
file_path.to_str().unwrap(),
"leaky_function",
"--show-paths",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: ResourceReport = serde_json::from_str(&stdout).unwrap();
for leak in &report.leaks {
assert!(
leak.paths.is_some(),
"--show-paths should include leak paths"
);
}
}
#[test]
fn test_resources_constraints_flag() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "leak.py", PYTHON_RESOURCE_LEAK);
let output = tldr_cmd()
.args(["resources", file_path.to_str().unwrap(), "--constraints"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: ResourceReport = serde_json::from_str(&stdout).unwrap();
assert!(!report.constraints.is_empty());
}
#[test]
fn test_resources_summary_flag() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "leak.py", PYTHON_RESOURCE_LEAK);
let output = tldr_cmd()
.args(["resources", file_path.to_str().unwrap(), "--summary"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let json: Value = serde_json::from_str(&stdout).unwrap();
assert!(json.get("summary").is_some());
}
#[test]
#[ignore = "--format text not yet implemented for resources command"]
fn test_resources_text_output() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "leak.py", PYTHON_RESOURCE_LEAK);
tldr_assert_cmd()
.args(["resources", file_path.to_str().unwrap(), "--format", "text"])
.assert()
.success()
.stdout(predicate::str::contains("Resource"));
}
#[test]
fn test_resources_exit_code_3_on_issues() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "leak.py", PYTHON_RESOURCE_LEAK);
let output = tldr_cmd()
.args(["resources", file_path.to_str().unwrap(), "leaky_function"])
.output()
.unwrap();
assert!(
output.status.code() == Some(0) || output.status.code() == Some(3),
"Should exit with 0 (no issues) or 3 (issues found)"
);
}
#[test]
fn test_resources_file_not_found() {
tldr_assert_cmd()
.args(["resources", "/nonexistent/file.py"])
.assert()
.failure()
.stderr(predicate::str::contains("file not found"));
}
#[test]
fn test_resources_function_not_found() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "leak.py", "def existing(): pass");
tldr_assert_cmd()
.args(["resources", file_path.to_str().unwrap(), "nonexistent"])
.assert()
.failure()
.stderr(
predicate::str::contains("function").and(predicate::str::contains("not found")),
);
}
#[test]
fn test_resources_complex_cfgs() {
let temp = TempDir::new().unwrap();
let code = r#"
def complex_cfg(path, flag):
f = open(path)
try:
if flag:
data = f.read()
if data.startswith("error"):
raise ValueError("Error data")
return data
else:
return "default"
except IOError:
return None
finally:
f.close()
"#;
let file_path = create_test_file(&temp, "complex.py", code);
let output = tldr_cmd()
.args(["resources", file_path.to_str().unwrap(), "complex_cfg"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let report: ResourceReport = serde_json::from_str(&stdout).unwrap();
assert!(!report.resources.is_empty());
assert_eq!(report.resources[0].name, "f");
}
#[test]
fn test_resources_nested_contexts() {
let temp = TempDir::new().unwrap();
let code = r#"
def nested_contexts(path1, path2):
with open(path1) as f1:
with open(path2) as f2:
return f1.read() + f2.read()
"#;
let file_path = create_test_file(&temp, "nested.py", code);
let output = tldr_cmd()
.args(["resources", file_path.to_str().unwrap(), "nested_contexts"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let report: ResourceReport = serde_json::from_str(&stdout).unwrap();
assert!(report.leaks.is_empty());
}
}
mod integration {
use super::*;
#[test]
#[ignore = "integration test - requires all commands implemented"]
fn test_temporal_and_resources_consistency() {
let temp = TempDir::new().unwrap();
let file_path = create_test_file(&temp, "test.py", PYTHON_TEMPORAL_SEQUENCES);
let temporal_output = tldr_cmd()
.args(["temporal", file_path.to_str().unwrap()])
.output()
.unwrap();
let resources_output = tldr_cmd()
.args(["resources", file_path.to_str().unwrap(), "--check-all"])
.output()
.unwrap();
assert!(temporal_output.status.success() || temporal_output.status.code() == Some(2));
assert!(resources_output.status.success() || resources_output.status.code() == Some(3));
}
}