use leankg::indexer::{
extract_calls_with_resolution, AndroidHiltExtractor, AndroidManifestExtractor,
AndroidResourceLinker, AndroidResourceRefExtractor, AndroidRoomExtractor,
GradleModuleExtractor, KotlinAnnotationExtractor,
};
use std::fs;
use tree_sitter::Parser;
const TV_APP_DIR: &str = "tests/fixtures/complex_scenarios/tv_app";
#[test]
fn test_tv_app_full_extraction() {
let manifest_path = format!("{}/AndroidManifest.xml", TV_APP_DIR);
let manifest_source = fs::read_to_string(&manifest_path).expect("TV app manifest not found");
let manifest_extractor =
AndroidManifestExtractor::new(manifest_source.as_bytes(), &manifest_path);
let (manifest_elements, manifest_rels) = manifest_extractor.extract();
let activities: Vec<_> = manifest_elements
.iter()
.filter(|e| e.element_type == "android_activity")
.collect();
assert!(!activities.is_empty(), "TV app should have activities");
let services: Vec<_> = manifest_elements
.iter()
.filter(|e| e.element_type == "android_service")
.collect();
println!("Found {} services", services.len());
let app_rels: Vec<_> = manifest_rels
.iter()
.filter(|r| r.rel_type == "has_application_class")
.collect();
println!("Found {} app class relationships", app_rels.len());
}
#[test]
fn test_room_entities_with_relationships() {
let entity_path = format!(
"{}/src/main/java/com/tv/app/data/local/entity/ChannelEntity.kt",
TV_APP_DIR
);
let entity_source = fs::read_to_string(&entity_path).expect("Entity file not found");
let room_extractor = AndroidRoomExtractor::new(entity_source.as_bytes(), &entity_path);
let (elements, relationships) = room_extractor.extract();
let entities: Vec<_> = elements
.iter()
.filter(|e| e.element_type == "room_entity")
.collect();
assert!(!entities.is_empty(), "Should extract Room entities");
let fk_rels: Vec<_> = relationships
.iter()
.filter(|r| r.rel_type == "room_entity_has_foreign_key")
.collect();
assert!(
!fk_rels.is_empty() || entities.len() >= 2,
"Should have entities with FK relationships or multiple entities"
);
}
#[test]
fn test_dao_with_queries() {
let dao_path = format!(
"{}/src/main/java/com/tv/app/data/local/dao/ChannelDao.kt",
TV_APP_DIR
);
let dao_source = fs::read_to_string(&dao_path).expect("DAO file not found");
let room_extractor = AndroidRoomExtractor::new(dao_source.as_bytes(), &dao_path);
let (elements, relationships) = room_extractor.extract();
let daos: Vec<_> = elements
.iter()
.filter(|e| e.element_type == "room_dao")
.collect();
assert!(!daos.is_empty(), "Should extract DAOs");
let query_rels: Vec<_> = relationships
.iter()
.filter(|r| r.rel_type == "room_dao_queries_entity")
.collect();
assert!(
!query_rels.is_empty() || daos.len() > 0,
"Should have DAO query relationships or DAOs present"
);
}
#[test]
fn test_hilt_module_providers() {
let module_path = format!("{}/src/main/java/com/tv/app/di/AppModule.kt", TV_APP_DIR);
let module_source = fs::read_to_string(&module_path).expect("Hilt module file not found");
let hilt_extractor = AndroidHiltExtractor::new(module_source.as_bytes(), &module_path);
let (elements, relationships) = hilt_extractor.extract();
let modules: Vec<_> = elements
.iter()
.filter(|e| e.element_type == "hilt_module")
.collect();
println!(
"Found {} Hilt modules: {:?}",
modules.len(),
modules.iter().map(|e| &e.name).collect::<Vec<_>>()
);
let providers: Vec<_> = elements
.iter()
.filter(|e| e.element_type == "hilt_provider")
.collect();
println!("Found {} Hilt providers", providers.len());
assert!(
modules.len() > 0 || providers.len() > 0 || relationships.len() > 0,
"Should extract something from Hilt module"
);
}
#[test]
fn test_repository_with_inject() {
let repo_path = format!(
"{}/src/main/java/com/tv/app/data/repository/ChannelRepository.kt",
TV_APP_DIR
);
let repo_source = fs::read_to_string(&repo_path).expect("Repository file not found");
let hilt_extractor = AndroidHiltExtractor::new(repo_source.as_bytes(), &repo_path);
let (_, relationships) = hilt_extractor.extract();
let inject_rels: Vec<_> = relationships
.iter()
.filter(|r| r.rel_type.contains("injected"))
.collect();
assert!(
repo_source.contains("ChannelRepository"),
"Repository should contain class name"
);
assert!(
repo_source.contains("@Inject") || repo_source.contains("constructor("),
"Repository should have injection pattern"
);
}
#[test]
fn test_tv_app_structure_completeness() {
let expected_files = vec![
format!("{}/AndroidManifest.xml", TV_APP_DIR),
format!("{}/src/main/java/com/tv/app/TvApplication.kt", TV_APP_DIR),
format!("{}/src/main/java/com/tv/app/di/AppModule.kt", TV_APP_DIR),
format!(
"{}/src/main/java/com/tv/app/data/local/TvDatabase.kt",
TV_APP_DIR
),
format!(
"{}/src/main/java/com/tv/app/data/local/entity/ChannelEntity.kt",
TV_APP_DIR
),
format!(
"{}/src/main/java/com/tv/app/data/local/dao/ChannelDao.kt",
TV_APP_DIR
),
format!(
"{}/src/main/java/com/tv/app/data/remote/PlaylistApi.kt",
TV_APP_DIR
),
format!(
"{}/src/main/java/com/tv/app/data/repository/ChannelRepository.kt",
TV_APP_DIR
),
format!(
"{}/src/main/java/com/tv/app/ui/browse/BrowseFragment.kt",
TV_APP_DIR
),
format!(
"{}/src/main/java/com/tv/app/ui/player/PlayerActivity.kt",
TV_APP_DIR
),
];
for path in &expected_files {
assert!(
fs::metadata(path).is_ok(),
"Expected TV app file should exist: {}",
path
);
}
}
#[test]
fn test_cross_file_relationships() {
let db_path = format!(
"{}/src/main/java/com/tv/app/data/local/TvDatabase.kt",
TV_APP_DIR
);
let db_source = fs::read_to_string(&db_path).expect("Database file not found");
let room_extractor = AndroidRoomExtractor::new(db_source.as_bytes(), &db_path);
let (elements, relationships) = room_extractor.extract();
let databases: Vec<_> = elements
.iter()
.filter(|e| e.element_type == "room_database")
.collect();
assert!(!databases.is_empty(), "Should extract Room database");
let db_entity_rels: Vec<_> = relationships
.iter()
.filter(|r| r.rel_type == "room_database_contains_entity")
.collect();
assert_eq!(databases.len(), 1, "Should have one database");
}
#[test]
fn test_resource_references_in_ui() {
let fragment_path = format!(
"{}/src/main/java/com/tv/app/ui/browse/BrowseFragment.kt",
TV_APP_DIR
);
let fragment_source = fs::read_to_string(&fragment_path).expect("Fragment file not found");
assert!(
fragment_source.contains("@AndroidEntryPoint")
|| fragment_source.contains("class BrowseFragment"),
"Should be Hilt-enabled fragment or have BrowseFragment class"
);
let res_extractor =
AndroidResourceRefExtractor::new(fragment_source.as_bytes(), &fragment_path);
let (_, relationships) = res_extractor.extract();
for rel in &relationships {
assert!(!rel.rel_type.is_empty(), "Relationship should have type");
}
}
#[test]
fn test_kotlin_annotation_extraction() {
let fragment_path = format!(
"{}/src/main/java/com/tv/app/ui/browse/BrowseFragment.kt",
TV_APP_DIR
);
let source = fs::read_to_string(&fragment_path).expect("Fragment not found");
let mut parser = Parser::new();
let lang: tree_sitter::Language = tree_sitter_kotlin_ng::LANGUAGE.into();
parser.set_language(&lang).ok();
let tree = parser.parse(&source, None).expect("Parse failed");
let extractor = KotlinAnnotationExtractor::new(source.as_bytes(), &fragment_path);
let (elements, relationships) = extractor.extract(&tree);
for elem in &elements {
assert!(!elem.qualified_name.is_empty());
assert!(!elem.element_type.is_empty());
}
for rel in &relationships {
assert!(!rel.rel_type.is_empty());
}
}
#[test]
fn test_resource_linker_extraction() {
let fragment_path = format!(
"{}/src/main/java/com/tv/app/ui/browse/BrowseFragment.kt",
TV_APP_DIR
);
let source = fs::read_to_string(&fragment_path).expect("Fragment not found");
let linker = AndroidResourceLinker::new(source.as_bytes(), &fragment_path);
let (_, relationships) = linker.extract();
for rel in &relationships {
assert!(!rel.rel_type.is_empty());
assert!(!rel.target_qualified.is_empty());
}
}
#[test]
fn test_call_graph_resolution() {
let fragment_path = format!(
"{}/src/main/java/com/tv/app/ui/browse/BrowseFragment.kt",
TV_APP_DIR
);
let source = fs::read_to_string(&fragment_path).expect("Fragment not found");
let mut parser = Parser::new();
let lang: tree_sitter::Language = tree_sitter_kotlin_ng::LANGUAGE.into();
parser.set_language(&lang).ok();
let tree = parser.parse(&source, None).expect("Parse failed");
let calls = extract_calls_with_resolution(&tree, source.as_bytes(), &fragment_path, "kotlin");
for call in &calls {
assert!(!call.source_qualified.is_empty());
assert!(!call.target_qualified.is_empty());
assert!(!call.rel_type.is_empty());
}
}
#[test]
fn test_gradle_module_extractor() {
let build_gradle = format!("{}/build.gradle.kts", TV_APP_DIR);
let source = match fs::read_to_string(&build_gradle) {
Ok(s) => s,
Err(_) => {
return;
}
};
let extractor = GradleModuleExtractor::new(source.as_bytes(), &build_gradle);
let (_, relationships) = extractor.extract();
for rel in &relationships {
assert!(!rel.rel_type.is_empty());
}
}
#[test]
fn test_android_resource_refs_dedup() {
let source = r#"
val title = getString(R.string.app_name)
val desc = resources.getString(R.string.app_name)
"#;
let extractor = AndroidResourceRefExtractor::new(source.as_bytes(), "./Test.kt");
let (_, relationships) = extractor.extract();
let string_refs: Vec<_> = relationships
.iter()
.filter(|r| r.rel_type == "uses_string_resource")
.collect();
assert_eq!(string_refs.len(), 1);
}