use std::io::Write;
use std::path::{Path, PathBuf};
use sqry_classpath::detect::{BuildSystem, detect_build_system};
use sqry_classpath::pipeline::{ClasspathConfig, ClasspathDepth, run_classpath_pipeline};
use sqry_classpath::stub::index::ClasspathIndex;
use sqry_classpath::stub::model::{
AccessFlags, AnnotationStub, BaseType, ClassKind, ClassStub, FieldStub, MethodStub,
TypeSignature,
};
use tempfile::TempDir;
use zip::write::SimpleFileOptions;
fn fixtures_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../test-fixtures/jvm-classpath")
}
fn build_minimal_class(class_name: &str) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&0xCAFE_BABEu32.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&52u16.to_be_bytes());
let class_bytes = class_name.as_bytes();
let object_bytes = b"java/lang/Object";
let cp_count: u16 = 5;
bytes.extend_from_slice(&cp_count.to_be_bytes());
bytes.push(1);
bytes.extend_from_slice(&(class_bytes.len() as u16).to_be_bytes());
bytes.extend_from_slice(class_bytes);
bytes.push(7);
bytes.extend_from_slice(&1u16.to_be_bytes());
bytes.push(1);
bytes.extend_from_slice(&(object_bytes.len() as u16).to_be_bytes());
bytes.extend_from_slice(object_bytes);
bytes.push(7);
bytes.extend_from_slice(&3u16.to_be_bytes());
bytes.extend_from_slice(&0x0021u16.to_be_bytes());
bytes.extend_from_slice(&2u16.to_be_bytes());
bytes.extend_from_slice(&4u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes
}
fn build_test_jar(entries: &[(&str, &[u8])]) -> Vec<u8> {
let mut buf = Vec::new();
{
let mut writer = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
let options =
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
for (name, data) in entries {
writer.start_file(*name, options).unwrap();
writer.write_all(data).unwrap();
}
writer.finish().unwrap();
}
buf
}
fn write_test_jar(dir: &Path, name: &str, classes: &[(&str, &[u8])]) -> PathBuf {
let jar_bytes = build_test_jar(classes);
let jar_path = dir.join(name);
std::fs::write(&jar_path, &jar_bytes).unwrap();
jar_path
}
fn make_stub(fqn: &str) -> ClassStub {
ClassStub {
fqn: fqn.to_owned(),
name: fqn.rsplit('.').next().unwrap_or(fqn).to_owned(),
kind: ClassKind::Class,
access: AccessFlags::new(0x0021), superclass: Some("java.lang.Object".to_owned()),
interfaces: vec![],
methods: vec![],
fields: vec![],
annotations: vec![],
generic_signature: None,
inner_classes: vec![],
lambda_targets: vec![],
module: None,
record_components: vec![],
enum_constants: vec![],
source_file: None,
source_jar: None,
kotlin_metadata: None,
scala_signature: None,
}
}
fn make_stub_with_methods(fqn: &str, method_names: &[&str]) -> ClassStub {
let mut stub = make_stub(fqn);
stub.methods = method_names
.iter()
.map(|name| MethodStub {
name: (*name).to_owned(),
descriptor: "()V".to_owned(),
access: AccessFlags::new(0x0001), annotations: vec![],
parameter_annotations: vec![],
generic_signature: None,
parameter_names: vec![],
return_type: TypeSignature::Base(BaseType::Void),
parameter_types: vec![],
})
.collect();
stub
}
fn make_annotated_stub(fqn: &str, annotation_fqns: &[&str]) -> ClassStub {
let mut stub = make_stub(fqn);
stub.annotations = annotation_fqns
.iter()
.map(|a| AnnotationStub {
type_fqn: (*a).to_owned(),
elements: vec![],
is_runtime_visible: true,
})
.collect();
stub
}
fn make_stub_with_fields(fqn: &str, field_names: &[(&str, &str)]) -> ClassStub {
let mut stub = make_stub(fqn);
stub.fields = field_names
.iter()
.map(|(name, descriptor)| FieldStub {
name: (*name).to_owned(),
descriptor: (*descriptor).to_owned(),
access: AccessFlags::new(0x0001), annotations: vec![],
generic_signature: None,
constant_value: None,
})
.collect();
stub
}
fn write_classpath_file(dir: &Path, jar_paths: &[&Path]) -> PathBuf {
let cp_file = dir.join("classpath.txt");
let contents: String = jar_paths
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join("\n");
std::fs::write(&cp_file, format!("{contents}\n")).unwrap();
cp_file
}
#[test]
fn detect_gradle_single_module() {
let fixture = fixtures_dir().join("gradle-single-module");
assert!(fixture.exists(), "fixture not found: {}", fixture.display());
let result = detect_build_system(&fixture, None);
assert_eq!(result.build_system, Some(BuildSystem::Gradle));
assert!(
result.markers_found.contains(&"build.gradle".to_string()),
"expected build.gradle marker, found: {:?}",
result.markers_found
);
}
#[test]
fn detect_gradle_multi_module() {
let fixture = fixtures_dir().join("gradle-multi-module");
assert!(fixture.exists(), "fixture not found: {}", fixture.display());
let result = detect_build_system(&fixture, None);
assert_eq!(result.build_system, Some(BuildSystem::Gradle));
assert!(
result
.markers_found
.contains(&"settings.gradle".to_string()),
"expected settings.gradle marker, found: {:?}",
result.markers_found
);
assert!(
result.markers_found.contains(&"build.gradle".to_string()),
"expected build.gradle marker in multi-module root"
);
}
#[test]
fn detect_maven_single_module() {
let fixture = fixtures_dir().join("maven-single-module");
assert!(fixture.exists(), "fixture not found: {}", fixture.display());
let result = detect_build_system(&fixture, None);
assert_eq!(result.build_system, Some(BuildSystem::Maven));
assert!(
result.markers_found.contains(&"pom.xml".to_string()),
"expected pom.xml marker"
);
}
#[test]
fn detect_maven_multi_module() {
let fixture = fixtures_dir().join("maven-multi-module");
assert!(fixture.exists(), "fixture not found: {}", fixture.display());
let result = detect_build_system(&fixture, None);
assert_eq!(result.build_system, Some(BuildSystem::Maven));
assert!(
result.markers_found.contains(&"pom.xml".to_string()),
"expected pom.xml marker"
);
}
#[test]
fn detect_bazel_java() {
let fixture = fixtures_dir().join("bazel-java");
assert!(fixture.exists(), "fixture not found: {}", fixture.display());
let result = detect_build_system(&fixture, None);
assert_eq!(result.build_system, Some(BuildSystem::Bazel));
assert!(
result.markers_found.contains(&"WORKSPACE".to_string()),
"expected WORKSPACE marker, found: {:?}",
result.markers_found
);
assert!(
result.markers_found.contains(&"BUILD".to_string()),
"expected BUILD marker, found: {:?}",
result.markers_found
);
}
#[test]
fn detect_kotlin_gradle_project() {
let fixture = fixtures_dir().join("kotlin-project");
assert!(fixture.exists(), "fixture not found: {}", fixture.display());
let result = detect_build_system(&fixture, None);
assert_eq!(result.build_system, Some(BuildSystem::Gradle));
assert!(
result
.markers_found
.contains(&"build.gradle.kts".to_string()),
"expected build.gradle.kts marker, found: {:?}",
result.markers_found
);
}
#[test]
fn detect_scala_sbt_project() {
let fixture = fixtures_dir().join("scala-project");
assert!(fixture.exists(), "fixture not found: {}", fixture.display());
let result = detect_build_system(&fixture, None);
assert_eq!(result.build_system, Some(BuildSystem::Sbt));
assert!(
result.markers_found.contains(&"build.sbt".to_string()),
"expected build.sbt marker, found: {:?}",
result.markers_found
);
}
#[test]
fn detect_no_build_system_in_empty_dir() {
let tmp = TempDir::new().unwrap();
let result = detect_build_system(tmp.path(), None);
assert_eq!(
result.build_system, None,
"empty directory should detect no build system"
);
assert!(
result.markers_found.is_empty(),
"no markers should be found in empty directory"
);
}
#[test]
fn detect_override_takes_precedence() {
let fixture = fixtures_dir().join("maven-single-module");
let result = detect_build_system(&fixture, Some("gradle"));
assert_eq!(result.build_system, Some(BuildSystem::Gradle));
assert_eq!(result.override_source, Some("gradle".to_string()));
assert!(
result.markers_found.is_empty(),
"override should skip marker scanning"
);
}
#[test]
fn pipeline_manual_classpath_single_jar() {
let tmp = TempDir::new().unwrap();
let class_a = build_minimal_class("com/example/Alpha");
let class_b = build_minimal_class("com/example/Beta");
let jar_path = write_test_jar(
tmp.path(),
"deps.jar",
&[
("com/example/Alpha.class", &class_a),
("com/example/Beta.class", &class_b),
],
);
let cp_file = write_classpath_file(tmp.path(), &[jar_path.as_path()]);
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
build_system_override: None,
classpath_file: Some(cp_file),
force: false,
timeout_secs: 30,
};
let result = run_classpath_pipeline(tmp.path(), &config).unwrap();
assert_eq!(result.jars_scanned, 1);
assert_eq!(result.classes_parsed, 2);
assert!(result.index.lookup_fqn("com.example.Alpha").is_some());
assert!(result.index.lookup_fqn("com.example.Beta").is_some());
assert!(
result.index.lookup_fqn("com.example.Gamma").is_none(),
"non-existent class should not be found"
);
}
#[test]
fn pipeline_manual_classpath_multiple_jars() {
let tmp = TempDir::new().unwrap();
let class_a = build_minimal_class("com/example/Alpha");
let class_b = build_minimal_class("org/other/Beta");
let class_c = build_minimal_class("org/other/Gamma");
let jar_a = write_test_jar(
tmp.path(),
"a.jar",
&[("com/example/Alpha.class", &class_a)],
);
let jar_b = write_test_jar(
tmp.path(),
"b.jar",
&[
("org/other/Beta.class", &class_b),
("org/other/Gamma.class", &class_c),
],
);
let cp_file = write_classpath_file(tmp.path(), &[jar_a.as_path(), jar_b.as_path()]);
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
build_system_override: None,
classpath_file: Some(cp_file),
force: false,
timeout_secs: 30,
};
let result = run_classpath_pipeline(tmp.path(), &config).unwrap();
assert_eq!(result.jars_scanned, 2);
assert_eq!(result.classes_parsed, 3);
assert!(result.index.lookup_fqn("com.example.Alpha").is_some());
assert!(result.index.lookup_fqn("org.other.Beta").is_some());
assert!(result.index.lookup_fqn("org.other.Gamma").is_some());
assert_eq!(result.provenance.len(), 2);
}
#[test]
fn pipeline_manual_classpath_with_comments_and_blanks() {
let tmp = TempDir::new().unwrap();
let class_a = build_minimal_class("com/example/Only");
let jar_path = write_test_jar(
tmp.path(),
"only.jar",
&[("com/example/Only.class", &class_a)],
);
let cp_file = tmp.path().join("classpath.txt");
std::fs::write(
&cp_file,
format!(
"# Comment line\n\n{}\n\n# Another comment\n",
jar_path.display()
),
)
.unwrap();
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
build_system_override: None,
classpath_file: Some(cp_file),
force: false,
timeout_secs: 30,
};
let result = run_classpath_pipeline(tmp.path(), &config).unwrap();
assert_eq!(result.jars_scanned, 1);
assert_eq!(result.classes_parsed, 1);
assert!(result.index.lookup_fqn("com.example.Only").is_some());
}
#[test]
fn pipeline_no_build_system_returns_detection_error() {
let tmp = TempDir::new().unwrap();
let config = ClasspathConfig {
enabled: true,
..ClasspathConfig::default()
};
let result = run_classpath_pipeline(tmp.path(), &config);
assert!(result.is_err(), "empty project should fail detection");
let err = result.unwrap_err().to_string();
assert!(
err.contains("No JVM build system detected"),
"expected detection error, got: {err}"
);
}
#[test]
fn pipeline_empty_classpath_file() {
let tmp = TempDir::new().unwrap();
let cp_file = tmp.path().join("classpath.txt");
std::fs::write(&cp_file, "# Only comments\n\n").unwrap();
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
build_system_override: None,
classpath_file: Some(cp_file),
force: false,
timeout_secs: 30,
};
let result = run_classpath_pipeline(tmp.path(), &config).unwrap();
assert_eq!(result.jars_scanned, 0);
assert_eq!(result.classes_parsed, 0);
assert_eq!(result.index.classes.len(), 0);
}
#[test]
fn pipeline_nonexistent_classpath_file_returns_error() {
let tmp = TempDir::new().unwrap();
let config = ClasspathConfig {
enabled: true,
classpath_file: Some(tmp.path().join("nonexistent.txt")),
..ClasspathConfig::default()
};
let result = run_classpath_pipeline(tmp.path(), &config);
assert!(result.is_err(), "nonexistent classpath file should fail");
let err = result.unwrap_err().to_string();
assert!(
err.contains("Cannot open classpath file"),
"expected file open error, got: {err}"
);
}
#[test]
fn index_persistence_roundtrip_empty() {
let tmp = TempDir::new().unwrap();
let index_path = tmp.path().join("index.sqry");
let index = ClasspathIndex::build(vec![]);
index.save(&index_path).unwrap();
let loaded = ClasspathIndex::load(&index_path).unwrap();
assert_eq!(loaded.classes.len(), 0);
assert!(loaded.package_index.is_empty());
assert!(loaded.annotation_index.is_empty());
}
#[test]
fn index_persistence_roundtrip_with_classes() {
let tmp = TempDir::new().unwrap();
let index_path = tmp.path().join("classpath/index.sqry");
let stubs = vec![
make_stub("com.example.Alpha"),
make_stub("com.example.Beta"),
make_stub("java.util.HashMap"),
make_stub("java.util.ArrayList"),
make_stub("org.slf4j.Logger"),
];
let index = ClasspathIndex::build(stubs);
index.save(&index_path).unwrap();
let loaded = ClasspathIndex::load(&index_path).unwrap();
assert_eq!(loaded.classes.len(), 5);
for window in loaded.classes.windows(2) {
assert!(
window[0].fqn <= window[1].fqn,
"sort order violated after roundtrip: {} > {}",
window[0].fqn,
window[1].fqn
);
}
assert!(loaded.lookup_fqn("com.example.Alpha").is_some());
assert!(loaded.lookup_fqn("java.util.HashMap").is_some());
assert!(loaded.lookup_fqn("org.slf4j.Logger").is_some());
assert!(loaded.lookup_fqn("does.not.Exist").is_none());
}
#[test]
fn index_persistence_roundtrip_with_annotations() {
let tmp = TempDir::new().unwrap();
let index_path = tmp.path().join("index.sqry");
let stubs = vec![
make_annotated_stub(
"com.example.MyController",
&["org.springframework.stereotype.Controller"],
),
make_annotated_stub(
"com.example.MyService",
&["org.springframework.stereotype.Service"],
),
make_annotated_stub(
"com.example.AnotherController",
&[
"org.springframework.stereotype.Controller",
"org.springframework.web.bind.annotation.RestController",
],
),
];
let index = ClasspathIndex::build(stubs);
index.save(&index_path).unwrap();
let loaded = ClasspathIndex::load(&index_path).unwrap();
let controllers = loaded.lookup_annotated("org.springframework.stereotype.Controller");
assert_eq!(
controllers.len(),
2,
"should find 2 controller-annotated classes after roundtrip"
);
let rest_controllers =
loaded.lookup_annotated("org.springframework.web.bind.annotation.RestController");
assert_eq!(
rest_controllers.len(),
1,
"should find 1 RestController-annotated class after roundtrip"
);
}
#[test]
fn index_persistence_roundtrip_via_pipeline() {
let tmp = TempDir::new().unwrap();
let class_a = build_minimal_class("com/example/Roundtrip");
let jar_path = write_test_jar(
tmp.path(),
"rt.jar",
&[("com/example/Roundtrip.class", &class_a)],
);
let cp_file = write_classpath_file(tmp.path(), &[jar_path.as_path()]);
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
classpath_file: Some(cp_file),
force: false,
timeout_secs: 30,
..ClasspathConfig::default()
};
let result = run_classpath_pipeline(tmp.path(), &config).unwrap();
assert_eq!(result.classes_parsed, 1);
let index_path = tmp.path().join(".sqry/classpath/index.sqry");
assert!(
index_path.exists(),
"pipeline should persist index at .sqry/classpath/index.sqry"
);
let loaded = ClasspathIndex::load(&index_path).unwrap();
assert_eq!(loaded.classes.len(), 1);
assert_eq!(loaded.classes[0].fqn, "com.example.Roundtrip");
let prov_path = tmp.path().join(".sqry/classpath/provenance.json");
assert!(
prov_path.exists(),
"pipeline should persist provenance at .sqry/classpath/provenance.json"
);
let prov_json = std::fs::read_to_string(&prov_path).unwrap();
let prov: serde_json::Value = serde_json::from_str(&prov_json).unwrap();
assert!(prov.is_array());
assert_eq!(prov.as_array().unwrap().len(), 1);
}
#[test]
fn workspace_fqn_shadows_classpath_in_index() {
let classpath_stub = make_stub_with_methods("com.example.AppService", &["handleRequest"]);
let workspace_stub =
make_stub_with_methods("com.example.AppService", &["handleRequest", "initialize"]);
let index = ClasspathIndex::build(vec![classpath_stub, workspace_stub]);
let all_in_package = index.lookup_package("com.example");
assert_eq!(
all_in_package.len(),
2,
"both stubs with same FQN should be in the index"
);
let found = index.lookup_fqn("com.example.AppService");
assert!(found.is_some(), "should find AppService by FQN");
}
#[test]
fn workspace_shadow_different_packages() {
let ws_stub = make_stub("com.example.Foo");
let cp_stub = make_stub("org.library.Foo");
let index = ClasspathIndex::build(vec![ws_stub, cp_stub]);
assert!(index.lookup_fqn("com.example.Foo").is_some());
assert!(index.lookup_fqn("org.library.Foo").is_some());
let com_example = index.lookup_package("com.example");
assert_eq!(com_example.len(), 1);
assert_eq!(com_example[0].fqn, "com.example.Foo");
let org_library = index.lookup_package("org.library");
assert_eq!(org_library.len(), 1);
assert_eq!(org_library[0].fqn, "org.library.Foo");
}
#[test]
fn package_lookup_groups_correctly() {
let stubs = vec![
make_stub("com.example.Alpha"),
make_stub("com.example.Beta"),
make_stub("com.example.Gamma"),
make_stub("java.util.HashMap"),
make_stub("java.util.ArrayList"),
make_stub("java.util.LinkedList"),
make_stub("org.slf4j.Logger"),
];
let index = ClasspathIndex::build(stubs);
let com_example = index.lookup_package("com.example");
assert_eq!(com_example.len(), 3);
let fqns: Vec<&str> = com_example.iter().map(|s| s.fqn.as_str()).collect();
assert!(fqns.contains(&"com.example.Alpha"));
assert!(fqns.contains(&"com.example.Beta"));
assert!(fqns.contains(&"com.example.Gamma"));
let java_util = index.lookup_package("java.util");
assert_eq!(java_util.len(), 3);
let org_slf4j = index.lookup_package("org.slf4j");
assert_eq!(org_slf4j.len(), 1);
assert_eq!(org_slf4j[0].fqn, "org.slf4j.Logger");
let nonexistent = index.lookup_package("does.not.exist");
assert!(nonexistent.is_empty());
}
#[test]
fn package_lookup_default_package() {
let stubs = vec![make_stub("NoPackageClass"), make_stub("AnotherDefault")];
let index = ClasspathIndex::build(stubs);
let default_pkg = index.lookup_package("");
assert_eq!(default_pkg.len(), 2);
}
#[test]
fn package_lookup_deeply_nested() {
let stubs = vec![
make_stub("com.example.deep.nested.pkg.ClassA"),
make_stub("com.example.deep.nested.pkg.ClassB"),
make_stub("com.example.deep.other.ClassC"),
];
let index = ClasspathIndex::build(stubs);
let nested = index.lookup_package("com.example.deep.nested.pkg");
assert_eq!(nested.len(), 2);
let other = index.lookup_package("com.example.deep.other");
assert_eq!(other.len(), 1);
let partial = index.lookup_package("com.example.deep");
assert!(
partial.is_empty(),
"partial package path should not match any classes"
);
}
#[test]
fn annotation_lookup_single_annotation() {
let stubs = vec![
make_annotated_stub("com.example.UserService", &["javax.inject.Singleton"]),
make_annotated_stub("com.example.OrderService", &["javax.inject.Singleton"]),
make_stub("com.example.PlainClass"), ];
let index = ClasspathIndex::build(stubs);
let singletons = index.lookup_annotated("javax.inject.Singleton");
assert_eq!(singletons.len(), 2);
let fqns: Vec<&str> = singletons.iter().map(|s| s.fqn.as_str()).collect();
assert!(fqns.contains(&"com.example.UserService"));
assert!(fqns.contains(&"com.example.OrderService"));
let plain = index.lookup_annotated("non.existent.Annotation");
assert!(plain.is_empty());
}
#[test]
fn annotation_lookup_multiple_annotations_per_class() {
let stubs = vec![make_annotated_stub(
"com.example.RestEndpoint",
&[
"javax.ws.rs.Path",
"javax.inject.Singleton",
"javax.enterprise.context.RequestScoped",
],
)];
let index = ClasspathIndex::build(stubs);
assert_eq!(index.lookup_annotated("javax.ws.rs.Path").len(), 1);
assert_eq!(index.lookup_annotated("javax.inject.Singleton").len(), 1);
assert_eq!(
index
.lookup_annotated("javax.enterprise.context.RequestScoped")
.len(),
1
);
let found = index.lookup_annotated("javax.ws.rs.Path");
assert_eq!(found[0].fqn, "com.example.RestEndpoint");
}
#[test]
fn annotation_lookup_spring_framework_pattern() {
let stubs = vec![
make_annotated_stub(
"com.example.web.UserController",
&["org.springframework.web.bind.annotation.RestController"],
),
make_annotated_stub(
"com.example.web.OrderController",
&["org.springframework.web.bind.annotation.RestController"],
),
make_annotated_stub(
"com.example.service.UserService",
&["org.springframework.stereotype.Service"],
),
make_annotated_stub(
"com.example.repo.UserRepository",
&["org.springframework.stereotype.Repository"],
),
make_annotated_stub(
"com.example.config.AppConfig",
&["org.springframework.context.annotation.Configuration"],
),
];
let index = ClasspathIndex::build(stubs);
let controllers =
index.lookup_annotated("org.springframework.web.bind.annotation.RestController");
assert_eq!(controllers.len(), 2);
let services = index.lookup_annotated("org.springframework.stereotype.Service");
assert_eq!(services.len(), 1);
assert_eq!(services[0].fqn, "com.example.service.UserService");
let repos = index.lookup_annotated("org.springframework.stereotype.Repository");
assert_eq!(repos.len(), 1);
let configs = index.lookup_annotated("org.springframework.context.annotation.Configuration");
assert_eq!(configs.len(), 1);
}
#[test]
fn scan_jar_via_pipeline_produces_correct_stubs() {
let tmp = TempDir::new().unwrap();
let class_root = build_minimal_class("RootClass");
let class_single = build_minimal_class("com/Example");
let class_deep = build_minimal_class("com/example/deep/nested/DeepClass");
let jar_path = write_test_jar(
tmp.path(),
"mixed.jar",
&[
("RootClass.class", &class_root),
("com/Example.class", &class_single),
("com/example/deep/nested/DeepClass.class", &class_deep),
],
);
let cp_file = write_classpath_file(tmp.path(), &[jar_path.as_path()]);
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
classpath_file: Some(cp_file),
force: false,
timeout_secs: 30,
..ClasspathConfig::default()
};
let result = run_classpath_pipeline(tmp.path(), &config).unwrap();
assert_eq!(result.classes_parsed, 3);
assert!(result.index.lookup_fqn("RootClass").is_some());
assert!(result.index.lookup_fqn("com.Example").is_some());
assert!(
result
.index
.lookup_fqn("com.example.deep.nested.DeepClass")
.is_some()
);
let deep_pkg = result.index.lookup_package("com.example.deep.nested");
assert_eq!(deep_pkg.len(), 1);
assert_eq!(deep_pkg[0].fqn, "com.example.deep.nested.DeepClass");
}
#[test]
fn scan_jar_ignores_non_class_entries() {
let tmp = TempDir::new().unwrap();
let class_a = build_minimal_class("com/example/Valid");
let jar_path = write_test_jar(
tmp.path(),
"mixed-content.jar",
&[
("com/example/Valid.class", &class_a),
("META-INF/MANIFEST.MF", b"Manifest-Version: 1.0\n"),
("README.txt", b"This is not a class file.\n"),
("resources/config.properties", b"key=value\n"),
],
);
let cp_file = write_classpath_file(tmp.path(), &[jar_path.as_path()]);
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
classpath_file: Some(cp_file),
force: false,
timeout_secs: 30,
..ClasspathConfig::default()
};
let result = run_classpath_pipeline(tmp.path(), &config).unwrap();
assert_eq!(
result.classes_parsed, 1,
"only .class entries should be parsed"
);
assert!(result.index.lookup_fqn("com.example.Valid").is_some());
}
#[test]
fn index_preserves_method_stubs() {
let tmp = TempDir::new().unwrap();
let index_path = tmp.path().join("index.sqry");
let stubs = vec![make_stub_with_methods(
"com.example.Service",
&["doWork", "initialize", "shutdown"],
)];
let index = ClasspathIndex::build(stubs);
index.save(&index_path).unwrap();
let loaded = ClasspathIndex::load(&index_path).unwrap();
let service = loaded.lookup_fqn("com.example.Service").unwrap();
assert_eq!(service.methods.len(), 3);
let method_names: Vec<&str> = service.methods.iter().map(|m| m.name.as_str()).collect();
assert!(method_names.contains(&"doWork"));
assert!(method_names.contains(&"initialize"));
assert!(method_names.contains(&"shutdown"));
}
#[test]
fn index_preserves_field_stubs() {
let tmp = TempDir::new().unwrap();
let index_path = tmp.path().join("index.sqry");
let stubs = vec![make_stub_with_fields(
"com.example.Config",
&[
("port", "I"),
("host", "Ljava/lang/String;"),
("enabled", "Z"),
],
)];
let index = ClasspathIndex::build(stubs);
index.save(&index_path).unwrap();
let loaded = ClasspathIndex::load(&index_path).unwrap();
let config = loaded.lookup_fqn("com.example.Config").unwrap();
assert_eq!(config.fields.len(), 3);
let field_names: Vec<&str> = config.fields.iter().map(|f| f.name.as_str()).collect();
assert!(field_names.contains(&"port"));
assert!(field_names.contains(&"host"));
assert!(field_names.contains(&"enabled"));
}
#[test]
fn index_handles_various_class_kinds() {
let mut interface_stub = make_stub("com.example.Service");
interface_stub.kind = ClassKind::Interface;
let mut enum_stub = make_stub("com.example.Color");
enum_stub.kind = ClassKind::Enum;
let mut annotation_stub = make_stub("com.example.MyAnnotation");
annotation_stub.kind = ClassKind::Annotation;
let class_stub = make_stub("com.example.Impl");
let index = ClasspathIndex::build(vec![interface_stub, enum_stub, annotation_stub, class_stub]);
assert_eq!(index.classes.len(), 4);
let service = index.lookup_fqn("com.example.Service").unwrap();
assert_eq!(service.kind, ClassKind::Interface);
let color = index.lookup_fqn("com.example.Color").unwrap();
assert_eq!(color.kind, ClassKind::Enum);
let ann = index.lookup_fqn("com.example.MyAnnotation").unwrap();
assert_eq!(ann.kind, ClassKind::Annotation);
let imp = index.lookup_fqn("com.example.Impl").unwrap();
assert_eq!(imp.kind, ClassKind::Class);
}
#[test]
fn pipeline_second_run_uses_cache() {
let tmp = TempDir::new().unwrap();
let class_a = build_minimal_class("com/cache/CacheTest");
let jar_path = write_test_jar(
tmp.path(),
"cache-test.jar",
&[("com/cache/CacheTest.class", &class_a)],
);
let cp_file = write_classpath_file(tmp.path(), &[jar_path.as_path()]);
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
classpath_file: Some(cp_file.clone()),
force: false,
timeout_secs: 30,
..ClasspathConfig::default()
};
let result1 = run_classpath_pipeline(tmp.path(), &config).unwrap();
assert!(!result1.from_cache, "first run should not be from cache");
assert_eq!(result1.classes_parsed, 1);
let config2 = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
classpath_file: Some(cp_file),
force: false,
timeout_secs: 30,
..ClasspathConfig::default()
};
let result2 = run_classpath_pipeline(tmp.path(), &config2).unwrap();
assert!(result2.from_cache, "second run should use cached stubs");
assert_eq!(result2.classes_parsed, 1);
assert!(
result2.index.lookup_fqn("com.cache.CacheTest").is_some(),
"cached result should still find classes"
);
}
#[test]
fn pipeline_force_bypasses_cache() {
let tmp = TempDir::new().unwrap();
let class_a = build_minimal_class("com/force/ForceTest");
let jar_path = write_test_jar(
tmp.path(),
"force-test.jar",
&[("com/force/ForceTest.class", &class_a)],
);
let cp_file = write_classpath_file(tmp.path(), &[jar_path.as_path()]);
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
classpath_file: Some(cp_file.clone()),
force: false,
timeout_secs: 30,
..ClasspathConfig::default()
};
let _ = run_classpath_pipeline(tmp.path(), &config).unwrap();
let config_force = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
classpath_file: Some(cp_file),
force: true,
timeout_secs: 30,
..ClasspathConfig::default()
};
let result = run_classpath_pipeline(tmp.path(), &config_force).unwrap();
assert!(!result.from_cache, "force=true should bypass cache");
assert_eq!(result.classes_parsed, 1);
}
#[test]
fn large_index_build_and_lookup() {
let stubs: Vec<ClassStub> = (0..2000)
.map(|i| {
let pkg = match i % 4 {
0 => "com.example.alpha",
1 => "com.example.beta",
2 => "org.library.core",
_ => "io.framework.util",
};
make_stub(&format!("{pkg}.Class{i:04}"))
})
.collect();
let index = ClasspathIndex::build(stubs);
assert_eq!(index.classes.len(), 2000);
for window in index.classes.windows(2) {
assert!(
window[0].fqn <= window[1].fqn,
"sort violation: {} > {}",
window[0].fqn,
window[1].fqn
);
}
assert!(index.lookup_fqn("com.example.alpha.Class0000").is_some());
assert!(index.lookup_fqn("org.library.core.Class0002").is_some());
assert!(index.lookup_fqn("io.framework.util.Class1999").is_some());
assert!(index.lookup_fqn("com.example.alpha.Class9999").is_none());
let alpha = index.lookup_package("com.example.alpha");
assert_eq!(alpha.len(), 500);
let beta = index.lookup_package("com.example.beta");
assert_eq!(beta.len(), 500);
}
#[test]
fn detect_override_all_build_systems() {
let tmp = TempDir::new().unwrap();
for (name, expected) in [
("gradle", BuildSystem::Gradle),
("maven", BuildSystem::Maven),
("bazel", BuildSystem::Bazel),
("sbt", BuildSystem::Sbt),
] {
let result = detect_build_system(tmp.path(), Some(name));
assert_eq!(
result.build_system,
Some(expected),
"override '{name}' should detect {expected:?}"
);
}
}
#[test]
fn detect_override_case_insensitive() {
let tmp = TempDir::new().unwrap();
for name in [
"GRADLE", "Gradle", "gradle", "MAVEN", "Maven", "BAZEL", "SBT",
] {
let result = detect_build_system(tmp.path(), Some(name));
assert!(
result.build_system.is_some(),
"case-insensitive override '{name}' should succeed"
);
}
}
#[test]
fn detect_override_invalid_returns_none() {
let tmp = TempDir::new().unwrap();
let result = detect_build_system(tmp.path(), Some("cmake"));
assert_eq!(
result.build_system, None,
"invalid override should return None"
);
}
#[test]
#[ignore = "requires JVM toolchain: Gradle must be installed and on PATH"]
fn pipeline_gradle_single_module_real() {
let fixture = fixtures_dir().join("gradle-single-module");
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
build_system_override: None,
classpath_file: None,
force: true,
timeout_secs: 120,
};
let result = run_classpath_pipeline(&fixture, &config).unwrap();
assert!(
result.jars_scanned > 0,
"should scan at least one JAR from Gradle"
);
assert!(
result.classes_parsed > 0,
"should parse at least one class from Gradle dependencies"
);
}
#[test]
#[ignore = "requires JVM toolchain: Maven must be installed and on PATH"]
fn pipeline_maven_single_module_real() {
let fixture = fixtures_dir().join("maven-single-module");
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
build_system_override: None,
classpath_file: None,
force: true,
timeout_secs: 120,
};
let result = run_classpath_pipeline(&fixture, &config).unwrap();
assert!(
result.jars_scanned > 0,
"should scan at least one JAR from Maven"
);
}
#[test]
#[ignore = "requires JVM toolchain: Bazel must be installed and on PATH"]
fn pipeline_bazel_java_real() {
let fixture = fixtures_dir().join("bazel-java");
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
build_system_override: None,
classpath_file: None,
force: true,
timeout_secs: 120,
};
let result = run_classpath_pipeline(&fixture, &config).unwrap();
assert!(
result.jars_scanned > 0,
"should scan at least one JAR from Bazel"
);
}
#[test]
#[ignore = "requires JVM toolchain: Gradle (KTS) must be installed and on PATH"]
fn pipeline_kotlin_project_real() {
let fixture = fixtures_dir().join("kotlin-project");
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
build_system_override: None,
classpath_file: None,
force: true,
timeout_secs: 120,
};
let result = run_classpath_pipeline(&fixture, &config).unwrap();
assert!(
result.jars_scanned > 0,
"should scan at least one JAR from Kotlin/Gradle"
);
}
#[test]
#[ignore = "requires JVM toolchain: sbt must be installed and on PATH"]
fn pipeline_scala_project_real() {
let fixture = fixtures_dir().join("scala-project");
let config = ClasspathConfig {
enabled: true,
depth: ClasspathDepth::Full,
build_system_override: None,
classpath_file: None,
force: true,
timeout_secs: 120,
};
let result = run_classpath_pipeline(&fixture, &config).unwrap();
assert!(
result.jars_scanned > 0,
"should scan at least one JAR from sbt"
);
}