import os
import sys
import re
import argparse
from pathlib import Path
def find_test_files(crate_path):
test_files = []
src_path = Path(crate_path) / 'src'
if not src_path.exists():
print(f"Error: {src_path} does not exist", file=sys.stderr)
return None
for test_file in src_path.rglob('tests/**/*.rs'):
if test_file.name != 'mod.rs':
test_files.append(test_file)
for rs_file in src_path.rglob('*.rs'):
if '/tests/' not in str(rs_file):
with open(rs_file, 'r') as f:
if '#[cfg(test)]' in f.read():
test_files.append(rs_file)
return test_files
def extract_function_names(file_path):
with open(file_path, 'r') as f:
content = f.read()
functions = {
'test_functions': [],
'helper_functions': [],
'all_functions': []
}
pattern = r'(?:pub\s+)?(?:async\s+)?fn\s+(\w+)\s*(?:<[^>]+>)?\s*\('
for match in re.finditer(pattern, content):
func_name = match.group(1)
line_num = content[:match.start()].count('\n') + 1
lines_before = content[:match.start()].split('\n')
is_test = False
is_nested = False
for i in range(min(10, len(lines_before))):
line_idx = len(lines_before) - 1 - i
if line_idx < 0:
break
line = lines_before[line_idx].strip()
if line.startswith('#[test]') or line.startswith('#[tokio::test]'):
is_test = True
break
if line.startswith('fn ') or line.startswith('pub fn ') or line.startswith('async fn ') or line.startswith('pub async fn '):
is_nested = True
break
if is_nested:
is_test = False
if is_test:
functions['test_functions'].append((func_name, line_num))
else:
functions['helper_functions'].append((func_name, line_num))
functions['all_functions'].append((func_name, line_num, is_test))
return functions
def check_test_function_naming(test_functions):
issues = []
for func_name, line_num in test_functions:
if not func_name.startswith('test_'):
issues.append({
'line': line_num,
'function': func_name,
'issue': 'Test function should start with test_',
'suggestion': f'Rename to test_{func_name}'
})
return issues
def is_valid_helper_name(func_name):
valid_prefixes = [
'helper_', 'create_concrete_', 'create_mock_', 'setup_', 'teardown_', 'assert_', 'verify_', 'build_', 'make_', ]
return any(func_name.startswith(prefix) for prefix in valid_prefixes)
def check_helper_function_naming(helper_functions):
issues = []
suggestions = []
for func_name, line_num in helper_functions:
if is_valid_helper_name(func_name):
if func_name.startswith('create_'):
if not func_name.startswith('create_concrete_') and not func_name.startswith('create_mock_'):
suggestions.append({
'line': line_num,
'function': func_name,
'issue': 'create_ helpers should be create_concrete_<unit>() or create_mock_<dependency>()',
'severity': 'warning'
})
else:
test_keywords = ['should', 'when', 'verifies', 'validates', 'checks', 'example', 'caller']
if any(keyword in func_name.lower() for keyword in test_keywords):
suggestions.append({
'line': line_num,
'function': func_name,
'issue': 'Helper function should use a recognized prefix (helper_, create_mock_, create_concrete_, setup_, etc.)',
'severity': 'warning'
})
return issues, suggestions
def check_module_structure(file_path):
with open(file_path, 'r') as f:
content = f.read()
issues = []
has_cfg_test = '#[cfg(test)]' in content
has_mod_tests = re.search(r'mod\s+tests\s*\{', content) is not None
if '#[test]' in content or '#[tokio::test]' in content:
if '/tests/' not in str(file_path):
if not has_cfg_test:
issues.append({
'issue': 'File has tests but missing #[cfg(test)]',
'severity': 'violation'
})
if not has_mod_tests:
issues.append({
'issue': 'File has tests but missing mod tests block',
'severity': 'violation'
})
return issues
def check_trait_compliance_tests(file_path):
with open(file_path, 'r') as f:
content = f.read()
suggestions = []
trait_impl_pattern = r'impl\s+(\w+)\s+for\s+(\w+)'
trait_impls = {}
for match in re.finditer(trait_impl_pattern, content):
trait_name = match.group(1)
impl_name = match.group(2)
if trait_name not in trait_impls:
trait_impls[trait_name] = []
trait_impls[trait_name].append(impl_name)
for trait_name, impl_names in trait_impls.items():
if len(impl_names) > 1:
if 'trait_compliance_tests' not in content:
suggestions.append({
'trait': trait_name,
'implementations': impl_names,
'issue': f'Multiple implementations of {trait_name} found but no trait_compliance_tests module',
'severity': 'info'
})
return suggestions
def main():
parser = argparse.ArgumentParser(
description='Verify unit test naming follows multi-llm conventions'
)
parser.add_argument(
'crate_path',
help='Path to the crate to check (e.g., multi-llm)'
)
args = parser.parse_args()
test_files = find_test_files(args.crate_path)
if test_files is None:
return 2
if not test_files:
print("⚠️ No test files found")
return 1
all_violations = []
all_warnings = []
all_suggestions = []
print(f"Checking naming conventions in {len(test_files)} test file(s)...\n")
for test_file in test_files:
rel_path = test_file.relative_to(Path(args.crate_path) / 'src')
functions = extract_function_names(test_file)
test_naming_issues = check_test_function_naming(functions['test_functions'])
for issue in test_naming_issues:
all_violations.append({
'file': str(rel_path),
**issue
})
helper_issues, helper_suggestions = check_helper_function_naming(functions['helper_functions'])
for issue in helper_issues:
all_violations.append({
'file': str(rel_path),
**issue
})
for suggestion in helper_suggestions:
all_warnings.append({
'file': str(rel_path),
**suggestion
})
module_issues = check_module_structure(test_file)
for issue in module_issues:
if issue['severity'] == 'violation':
all_violations.append({
'file': str(rel_path),
**issue
})
else:
all_warnings.append({
'file': str(rel_path),
**issue
})
trait_suggestions = check_trait_compliance_tests(test_file)
for suggestion in trait_suggestions:
all_suggestions.append({
'file': str(rel_path),
**suggestion
})
exit_code = 0
if all_violations:
print(f"❌ VIOLATIONS: {len(all_violations)} naming convention violation(s):")
print(" These MUST be fixed.\n")
for violation in all_violations[:20]:
file_info = f"{violation['file']}:{violation.get('line', '?')}"
func_info = f"{violation.get('function', 'N/A')}"
print(f" {file_info} - {func_info}")
print(f" Issue: {violation['issue']}")
if 'suggestion' in violation:
print(f" Suggestion: {violation['suggestion']}")
if len(all_violations) > 20:
print(f"\n ... and {len(all_violations) - 20} more")
exit_code = 2
print()
if all_warnings:
print(f"{'⚠️ ' if not all_violations else ''}WARNINGS: {len(all_warnings)} naming convention warning(s):")
print(" These should be fixed but don't block progress.\n")
for warning in all_warnings[:15]:
file_info = f"{warning['file']}:{warning.get('line', '?')}"
func_info = f"{warning.get('function', 'N/A')}"
print(f" {file_info} - {func_info}")
print(f" {warning['issue']}")
if len(all_warnings) > 15:
print(f"\n ... and {len(all_warnings) - 15} more")
if exit_code == 0:
exit_code = 1
print()
if all_suggestions:
print(f"{'ℹ️ ' if exit_code == 0 else ''}INFO: {len(all_suggestions)} suggestion(s):")
print(" Consider these improvements.\n")
for suggestion in all_suggestions[:10]:
print(f" {suggestion['file']}")
print(f" {suggestion['issue']}")
if len(all_suggestions) > 10:
print(f"\n ... and {len(all_suggestions) - 10} more")
print()
if exit_code == 0:
print("✓ All tests follow naming conventions")
return exit_code
if __name__ == '__main__':
sys.exit(main())