use crate::frontend::ast::{Expr, ExprKind};
use crate::frontend::parser::Parser;
use crate::utils::common_patterns::ResultContextExt;
use anyhow::{bail, Context, Result};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[derive(Debug)]
pub struct ModuleLoader {
cache: HashMap<String, ParsedModule>,
pub(crate) search_paths: Vec<PathBuf>,
loading_stack: Vec<String>,
files_loaded: usize,
cache_hits: usize,
}
impl ModuleLoader {
#[must_use]
pub fn new() -> Self {
Self {
cache: HashMap::new(),
search_paths: vec![
PathBuf::from("."), PathBuf::from("./src"), PathBuf::from("./modules"), ],
loading_stack: Vec::new(),
files_loaded: 0,
cache_hits: 0,
}
}
pub fn add_search_path<P: AsRef<Path>>(&mut self, path: P) {
self.search_paths.push(path.as_ref().to_path_buf());
}
pub fn load_module(&mut self, module_name: &str) -> Result<ParsedModule> {
if self.loading_stack.contains(&module_name.to_string()) {
let stack = self.loading_stack.join(" -> ");
let cycle_path = format!("{stack} -> {module_name}");
bail!("Circular dependency detected: {cycle_path}");
}
if let Some(cached) = self.cache.get(module_name) {
if self.is_cache_valid(cached)? {
self.cache_hits += 1;
return Ok(cached.clone());
}
}
let file_path = self
.resolve_module_path(module_name)
.module_context("find", module_name)?;
let content = fs::read_to_string(&file_path).file_context("read", &file_path)?;
self.loading_stack.push(module_name.to_string());
let mut parser = Parser::new(&content);
let ast = parser.parse().module_context("parse", module_name)?;
let dependencies = self.extract_dependencies(&ast)?;
let parsed_module = ParsedModule {
ast,
file_path: file_path.clone(),
dependencies: dependencies.clone(),
last_modified: fs::metadata(&file_path)?.modified()?,
};
for dep in &dependencies {
if self.loading_stack.contains(&dep.clone()) {
let stack = self.loading_stack.join(" -> ");
let cycle_path = format!("{stack} -> {module_name} -> {dep}");
bail!("Circular dependency detected: {cycle_path}");
}
self.load_module(dep).with_context(|| {
format!("Failed to load dependency '{dep}' for module '{module_name}'")
})?;
}
self.loading_stack.pop();
self.cache.remove(module_name);
self.cache
.insert(module_name.to_string(), parsed_module.clone());
self.files_loaded += 1;
Ok(parsed_module)
}
fn resolve_module_path(&self, module_name: &str) -> Result<PathBuf> {
let possible_names = [
format!("{module_name}.ruchy"),
format!("{module_name}/mod.ruchy"),
format!("{module_name}.rchy"),
];
for search_path in &self.search_paths {
for name in &possible_names {
let candidate = search_path.join(name);
if candidate.exists() && candidate.is_file() {
return Ok(candidate);
}
}
}
bail!(
"Module '{}' not found. Searched in: {}\nLooked for: {}",
module_name,
self.search_paths
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", "),
possible_names.join(", ")
);
}
fn is_cache_valid(&self, module: &ParsedModule) -> Result<bool> {
let current_modified = fs::metadata(&module.file_path)?.modified()?;
Ok(current_modified <= module.last_modified)
}
fn extract_dependencies(&self, ast: &Expr) -> Result<Vec<String>> {
let mut dependencies = Vec::new();
Self::collect_dependencies(ast, &mut dependencies);
Ok(dependencies)
}
fn collect_dependencies(expr: &Expr, dependencies: &mut Vec<String>) {
match &expr.kind {
ExprKind::Import { module, .. }
| ExprKind::ImportAll { module, .. }
| ExprKind::ImportDefault { module, .. } => {
if !module.contains("::")
&& !module.starts_with("std::")
&& !module.starts_with("http")
{
dependencies.push(module.clone());
}
}
ExprKind::ReExport { module, .. } => {
if !module.contains("::")
&& !module.starts_with("std::")
&& !module.starts_with("http")
{
dependencies.push(module.clone());
}
}
ExprKind::Block(exprs) => {
for expr in exprs {
Self::collect_dependencies(expr, dependencies);
}
}
ExprKind::Module { body, .. } => {
Self::collect_dependencies(body, dependencies);
}
ExprKind::Function { body, .. } => {
Self::collect_dependencies(body, dependencies);
}
_ => {
}
}
}
#[must_use]
pub fn stats(&self) -> ModuleLoaderStats {
ModuleLoaderStats {
cached_modules: self.cache.len(),
files_loaded: self.files_loaded,
cache_hits: self.cache_hits,
search_paths: self.search_paths.len(),
}
}
pub fn clear_cache(&mut self) {
self.cache.clear();
self.files_loaded = 0;
self.cache_hits = 0;
}
#[must_use]
pub fn is_loading(&self, module_name: &str) -> bool {
self.loading_stack.contains(&module_name.to_string())
}
}
impl Default for ModuleLoader {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ParsedModule {
pub ast: Expr,
pub file_path: PathBuf,
pub dependencies: Vec<String>,
pub last_modified: SystemTime,
}
impl ParsedModule {
#[must_use]
pub fn name(&self) -> Option<String> {
self.file_path
.file_stem()
.and_then(|stem| stem.to_str())
.map(std::string::ToString::to_string)
}
#[must_use]
pub fn has_dependencies(&self) -> bool {
!self.dependencies.is_empty()
}
}
#[derive(Debug, Clone, Copy)]
pub struct ModuleLoaderStats {
pub cached_modules: usize,
pub files_loaded: usize,
pub cache_hits: usize,
pub search_paths: usize,
}
impl ModuleLoaderStats {
#[must_use]
pub fn cache_hit_ratio(&self) -> f64 {
if self.files_loaded + self.cache_hits == 0 {
0.0
} else {
f64::from(self.cache_hits as u32)
/ f64::from((self.files_loaded + self.cache_hits) as u32)
* 100.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn create_test_module(temp_dir: &TempDir, name: &str, content: &str) -> Result<()> {
let file_path = temp_dir.path().join(format!("{name}.ruchy"));
fs::write(file_path, content)?;
Ok(())
}
#[test]
fn test_module_loader_creation() {
let loader = ModuleLoader::new();
assert_eq!(loader.cache.len(), 0);
assert_eq!(loader.search_paths.len(), 3);
assert!(loader.search_paths.contains(&PathBuf::from(".")));
assert!(loader.search_paths.contains(&PathBuf::from("./src")));
assert!(loader.search_paths.contains(&PathBuf::from("./modules")));
}
#[test]
fn test_add_search_path() {
let mut loader = ModuleLoader::new();
loader.add_search_path("/custom/path");
assert_eq!(loader.search_paths.len(), 4);
assert!(loader.search_paths.contains(&PathBuf::from("/custom/path")));
}
#[test]
fn test_resolve_module_path_success() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "math", "42")?;
let resolved = loader.resolve_module_path("math")?;
assert_eq!(resolved, temp_dir.path().join("math.ruchy"));
assert!(resolved.exists());
Ok(())
}
#[test]
fn test_resolve_module_path_not_found() {
let loader = ModuleLoader::new();
let result = loader.resolve_module_path("nonexistent");
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Module 'nonexistent' not found"));
}
#[test]
fn test_circular_dependency_detection() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "a", "import \"b\"")?;
create_test_module(&temp_dir, "b", "import \"a\"")?;
let result = loader.load_module("a");
assert!(result.is_err());
let error = result.unwrap_err();
let error_msg = format!("{error:?}"); let found_circular_dep = error_msg.contains("Circular dependency detected")
|| error_msg.contains("circular dependency");
assert!(
found_circular_dep,
"Expected circular dependency error, got: {error_msg}"
);
Ok(())
}
#[test]
fn test_stats_tracking() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear(); loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "test", "42")?;
let initial_stats = loader.stats();
assert_eq!(initial_stats.files_loaded, 0);
assert_eq!(initial_stats.cache_hits, 0);
assert_eq!(initial_stats.cached_modules, 0);
loader.load_module("test")?;
let after_load = loader.stats();
assert_eq!(after_load.files_loaded, 1);
assert_eq!(after_load.cached_modules, 1);
loader.load_module("test")?;
let after_cache = loader.stats();
assert_eq!(after_cache.files_loaded, 1); assert_eq!(after_cache.cache_hits, 1); Ok(())
}
#[test]
fn test_cache_hit_ratio_calculation() {
let stats = ModuleLoaderStats {
cached_modules: 5,
files_loaded: 10,
cache_hits: 20,
search_paths: 3,
};
let ratio = stats.cache_hit_ratio();
assert!((ratio - 66.67).abs() < 0.01); }
#[test]
fn test_clear_cache() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear(); loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "test", "42")?;
loader.load_module("test")?;
assert_eq!(loader.cache.len(), 1);
loader.clear_cache();
assert_eq!(loader.cache.len(), 0);
assert_eq!(loader.files_loaded, 0);
assert_eq!(loader.cache_hits, 0);
Ok(())
}
#[test]
fn test_parsed_module_name() -> Result<()> {
let temp_dir = TempDir::new()?;
let path = temp_dir.path().join("math.ruchy");
let module = ParsedModule {
ast: Expr::new(
crate::frontend::ast::ExprKind::Literal(crate::frontend::ast::Literal::Unit),
crate::frontend::ast::Span { start: 0, end: 0 },
),
file_path: path,
dependencies: Vec::new(),
last_modified: SystemTime::now(),
};
assert_eq!(module.name(), Some("math".to_string()));
assert!(!module.has_dependencies());
Ok(())
}
#[test]
fn test_multiple_search_paths() -> Result<()> {
let temp_dir1 = TempDir::new()?;
let temp_dir2 = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir1.path());
loader.add_search_path(temp_dir2.path());
create_test_module(&temp_dir2, "found", "42")?;
let resolved = loader.resolve_module_path("found")?;
assert!(resolved.exists());
assert_eq!(resolved.parent(), Some(temp_dir2.path()));
Ok(())
}
#[test]
fn test_load_module_complex_content() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "complex", "let x = 42;\nlet y = x + 1;")?;
let module = loader.load_module("complex")?;
assert!(module.file_path.exists());
assert!(module.dependencies.is_empty());
Ok(())
}
#[test]
fn test_extract_dependencies() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "math", "42")?;
create_test_module(&temp_dir, "utils", "84")?;
create_test_module(&temp_dir, "with_deps", "use math;\nuse utils;\nlet x = 42;")?;
let _ = loader.load_module("with_deps");
Ok(())
}
#[test]
fn test_cache_invalidation_on_file_change() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "changing", "42")?;
let _module1 = loader.load_module("changing")?;
std::thread::sleep(std::time::Duration::from_millis(10));
create_test_module(&temp_dir, "changing", "84")?;
let _module2 = loader.load_module("changing")?;
let stats = loader.stats();
assert_eq!(stats.files_loaded, 2);
Ok(())
}
#[test]
fn test_default_search_paths() {
let loader = ModuleLoader::new();
let paths: Vec<String> = loader
.search_paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
assert!(!paths.is_empty());
assert!(paths
.iter()
.any(|p| p == "." || p.ends_with("/.") || p.is_empty()));
}
#[test]
fn test_module_not_utf8() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir.path());
let path = temp_dir.path().join("invalid.ruchy");
fs::write(&path, [0xFF, 0xFE, 0x00])?;
let result = loader.load_module("invalid");
assert!(result.is_err());
Ok(())
}
#[test]
fn test_empty_module_name() {
let loader = ModuleLoader::new();
let result = loader.resolve_module_path("");
assert!(result.is_err());
}
#[test]
fn test_module_with_special_characters() {
let loader = ModuleLoader::new();
let invalid_names = vec![
"module-name",
"module.name",
"module/name",
"module\\name",
"../module",
];
for name in invalid_names {
let result = loader.resolve_module_path(name);
let _ = result;
}
}
#[test]
fn test_parsed_module_with_dependencies() -> Result<()> {
let temp_dir = TempDir::new()?;
let path = temp_dir.path().join("with_deps.ruchy");
let module = ParsedModule {
ast: Expr::new(
crate::frontend::ast::ExprKind::Literal(crate::frontend::ast::Literal::Unit),
crate::frontend::ast::Span { start: 0, end: 0 },
),
file_path: path,
dependencies: vec!["math".to_string(), "utils".to_string()],
last_modified: SystemTime::now(),
};
assert!(module.has_dependencies());
assert_eq!(module.dependencies.len(), 2);
Ok(())
}
#[test]
fn test_deep_circular_dependency() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "a", "use b;")?;
create_test_module(&temp_dir, "b", "use c;")?;
create_test_module(&temp_dir, "c", "use a;")?;
let result = loader.load_module("a");
assert!(result.is_err());
Ok(())
}
#[test]
fn test_is_loading_initially_false() {
let loader = ModuleLoader::new();
assert!(!loader.is_loading("any_module"));
assert!(!loader.is_loading(""));
assert!(!loader.is_loading("test"));
}
#[test]
fn test_default_trait_impl() {
let loader = ModuleLoader::default();
assert_eq!(loader.stats().files_loaded, 0);
assert_eq!(loader.stats().cached_modules, 0);
}
#[test]
fn test_stats_search_paths_count() {
let loader = ModuleLoader::new();
let stats = loader.stats();
assert_eq!(stats.search_paths, 3); }
#[test]
fn test_cache_hit_ratio_zero_when_empty() {
let stats = ModuleLoaderStats {
cached_modules: 0,
files_loaded: 0,
cache_hits: 0,
search_paths: 1,
};
assert_eq!(stats.cache_hit_ratio(), 0.0);
}
#[test]
fn test_cache_hit_ratio_100_percent() {
let stats = ModuleLoaderStats {
cached_modules: 10,
files_loaded: 0,
cache_hits: 10,
search_paths: 1,
};
assert_eq!(stats.cache_hit_ratio(), 100.0);
}
#[test]
fn test_add_multiple_search_paths() {
let mut loader = ModuleLoader::new();
loader.add_search_path("/path1");
loader.add_search_path("/path2");
loader.add_search_path("/path3");
assert_eq!(loader.search_paths.len(), 6); }
#[test]
fn test_add_duplicate_search_path() {
let mut loader = ModuleLoader::new();
loader.add_search_path("/custom");
loader.add_search_path("/custom");
assert!(
loader
.search_paths
.iter()
.filter(|p| p.to_string_lossy() == "/custom")
.count()
>= 2
);
}
#[test]
fn test_resolve_rchy_extension() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir.path());
let path = temp_dir.path().join("short.rchy");
fs::write(&path, "42")?;
let resolved = loader.resolve_module_path("short")?;
assert!(resolved.exists());
assert!(resolved.to_string_lossy().ends_with(".rchy"));
Ok(())
}
#[test]
fn test_resolve_mod_ruchy_in_directory() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir.path());
let mod_dir = temp_dir.path().join("mymodule");
fs::create_dir(&mod_dir)?;
let mod_file = mod_dir.join("mod.ruchy");
fs::write(&mod_file, "42")?;
let resolved = loader.resolve_module_path("mymodule")?;
assert!(resolved.exists());
assert!(resolved.to_string_lossy().contains("mod.ruchy"));
Ok(())
}
#[test]
fn test_parsed_module_name_with_nested_path() -> Result<()> {
let module = ParsedModule {
ast: Expr::new(
crate::frontend::ast::ExprKind::Literal(crate::frontend::ast::Literal::Unit),
crate::frontend::ast::Span { start: 0, end: 0 },
),
file_path: PathBuf::from("/some/path/deep/nested/module.ruchy"),
dependencies: Vec::new(),
last_modified: SystemTime::now(),
};
assert_eq!(module.name(), Some("module".to_string()));
Ok(())
}
#[test]
fn test_parsed_module_no_extension() {
let module = ParsedModule {
ast: Expr::new(
crate::frontend::ast::ExprKind::Literal(crate::frontend::ast::Literal::Unit),
crate::frontend::ast::Span { start: 0, end: 0 },
),
file_path: PathBuf::from("module_no_ext"),
dependencies: Vec::new(),
last_modified: SystemTime::now(),
};
assert_eq!(module.name(), Some("module_no_ext".to_string()));
}
#[test]
fn test_clear_cache_resets_all_counters() {
let mut loader = ModuleLoader::new();
loader.files_loaded = 10;
loader.cache_hits = 5;
loader.clear_cache();
assert_eq!(loader.files_loaded, 0);
assert_eq!(loader.cache_hits, 0);
assert_eq!(loader.cache.len(), 0);
}
#[test]
fn test_load_simple_literal_module() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "literal", "123")?;
let module = loader.load_module("literal")?;
assert_eq!(module.name(), Some("literal".to_string()));
assert!(!module.has_dependencies());
Ok(())
}
#[test]
fn test_load_string_literal_module() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "string_mod", "\"hello world\"")?;
let module = loader.load_module("string_mod")?;
assert!(module.file_path.exists());
Ok(())
}
#[test]
fn test_load_bool_module() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "bool_mod", "true")?;
let module = loader.load_module("bool_mod")?;
assert!(module.file_path.exists());
Ok(())
}
#[test]
fn test_load_function_module() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "func_mod", "fun add(a, b) { a + b }")?;
let module = loader.load_module("func_mod")?;
assert!(module.file_path.exists());
Ok(())
}
#[test]
fn test_cache_hit_on_second_load() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "cached", "42")?;
loader.load_module("cached")?;
assert_eq!(loader.cache_hits, 0);
loader.load_module("cached")?;
assert_eq!(loader.cache_hits, 1);
Ok(())
}
#[test]
fn test_stats_after_multiple_loads() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "mod1", "1")?;
create_test_module(&temp_dir, "mod2", "2")?;
create_test_module(&temp_dir, "mod3", "3")?;
loader.load_module("mod1")?;
loader.load_module("mod2")?;
loader.load_module("mod3")?;
let stats = loader.stats();
assert_eq!(stats.files_loaded, 3);
assert_eq!(stats.cached_modules, 3);
Ok(())
}
#[test]
fn test_module_priority_ruchy_over_rchy() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir.path());
create_test_module(&temp_dir, "priority", "42")?;
let rchy_path = temp_dir.path().join("priority.rchy");
fs::write(&rchy_path, "84")?;
let resolved = loader.resolve_module_path("priority")?;
assert!(resolved.to_string_lossy().ends_with(".ruchy"));
Ok(())
}
#[test]
fn test_search_path_order() -> Result<()> {
let temp_dir1 = TempDir::new()?;
let temp_dir2 = TempDir::new()?;
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
loader.add_search_path(temp_dir1.path());
loader.add_search_path(temp_dir2.path());
create_test_module(&temp_dir1, "order_test", "1")?;
create_test_module(&temp_dir2, "order_test", "2")?;
let resolved = loader.resolve_module_path("order_test")?;
assert_eq!(resolved.parent(), Some(temp_dir1.path()));
Ok(())
}
#[test]
fn test_empty_search_paths() {
let mut loader = ModuleLoader::new();
loader.search_paths.clear();
let result = loader.resolve_module_path("any");
assert!(result.is_err());
}
#[test]
fn test_module_loader_stats_debug_impl() {
let stats = ModuleLoaderStats {
cached_modules: 1,
files_loaded: 2,
cache_hits: 3,
search_paths: 4,
};
let debug_str = format!("{:?}", stats);
assert!(debug_str.contains("cached_modules"));
assert!(debug_str.contains("files_loaded"));
}
#[test]
fn test_parsed_module_debug_impl() {
let module = ParsedModule {
ast: Expr::new(
crate::frontend::ast::ExprKind::Literal(crate::frontend::ast::Literal::Unit),
crate::frontend::ast::Span { start: 0, end: 0 },
),
file_path: PathBuf::from("test.ruchy"),
dependencies: vec!["dep".to_string()],
last_modified: SystemTime::now(),
};
let debug_str = format!("{:?}", module);
assert!(debug_str.contains("file_path"));
assert!(debug_str.contains("dependencies"));
}
#[test]
fn test_module_loader_debug_impl() {
let loader = ModuleLoader::new();
let debug_str = format!("{:?}", loader);
assert!(debug_str.contains("search_paths"));
assert!(debug_str.contains("cache"));
}
#[test]
fn test_parsed_module_clone() {
let module = ParsedModule {
ast: Expr::new(
crate::frontend::ast::ExprKind::Literal(crate::frontend::ast::Literal::Unit),
crate::frontend::ast::Span { start: 0, end: 0 },
),
file_path: PathBuf::from("test.ruchy"),
dependencies: vec!["dep".to_string()],
last_modified: SystemTime::now(),
};
let cloned = module.clone();
assert_eq!(module.file_path, cloned.file_path);
assert_eq!(module.dependencies, cloned.dependencies);
}
#[test]
fn test_module_loader_stats_copy() {
let stats = ModuleLoaderStats {
cached_modules: 1,
files_loaded: 2,
cache_hits: 3,
search_paths: 4,
};
let copied = stats;
assert_eq!(stats.cached_modules, copied.cached_modules);
assert_eq!(stats.files_loaded, copied.files_loaded);
}
}
#[cfg(test)]
mod property_tests_module_loader {
use super::*;
use proptest::proptest;
proptest! {
#[test]
fn test_module_loader_new_never_panics(_input: String) {
let result = std::panic::catch_unwind(|| {
let loader = ModuleLoader::new();
let stats = loader.stats();
assert_eq!(stats.cached_modules, 0);
assert_eq!(stats.cache_hits, 0);
assert_eq!(stats.files_loaded, 0);
});
assert!(result.is_ok(), "ModuleLoader::new() panicked unexpectedly");
}
}
}