mod common;
use assert_cmd::Command;
use common::sqry_bin;
use serde_json::Value;
use sqry_core::test_support::verbosity;
use std::collections::BTreeSet;
use std::fs;
use std::io;
use std::path::Path;
use std::sync::Once;
use tempfile::TempDir;
static INIT: Once = Once::new();
fn init_logging() {
INIT.call_once(|| {
verbosity::init(env!("CARGO_PKG_NAME"));
});
}
fn create_test_project(files: &[(&str, &str)]) -> TempDir {
let dir = TempDir::new().unwrap();
for (path, content) in files {
let file_path = dir.path().join(path);
fs::create_dir_all(file_path.parent().unwrap()).unwrap();
fs::write(&file_path, content).unwrap();
}
dir
}
fn sqry_cmd() -> Command {
Command::new(sqry_bin())
}
fn create_indexed_project(files: &[(&str, &str)]) -> TempDir {
let project = create_test_project(files);
sqry_cmd()
.arg("index")
.current_dir(project.path())
.assert()
.success();
project
}
fn copy_fixture_dir(relative: &str) -> TempDir {
let source = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("workspace root")
.join(relative);
let temp = TempDir::new().expect("create temp dir");
copy_dir_all(&source, temp.path()).expect("copy fixture into temp dir");
temp
}
fn copy_dir_all(src: &Path, dst: &Path) -> io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let dest_path = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&entry.path(), &dest_path)?;
} else if ty.is_file() {
fs::copy(entry.path(), dest_path)?;
}
}
Ok(())
}
fn extract_symbol_names(json: &Value) -> Vec<String> {
json["results"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|item| item.get("name").and_then(|n| n.as_str()))
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
})
.unwrap_or_default()
}
#[test]
fn test_boolean_and_basic() {
init_logging();
log::info!("REGRESSION TEST: Boolean AND operator (kind:function AND name:public)");
let project = create_indexed_project(&[(
"test.rs",
r"
pub fn public_func() {}
fn private_func() {}
pub struct PublicStruct {}
",
)]);
let output = sqry_cmd()
.arg("--semantic")
.arg("query")
.arg("kind:function AND name~=/public/")
.arg("--limit")
.arg("10")
.current_dir(project.path())
.output()
.unwrap();
assert!(
output.status.success(),
"Boolean AND query failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.contains("public_func"),
"Expected public_func, got: {stdout}"
);
assert!(
!stdout.contains("private_func"),
"Should not contain private_func, got: {stdout}"
);
log::info!("✓ PASS: AND operator correctly filters by both conditions");
}
#[test]
fn test_boolean_and_with_regex() {
init_logging();
log::info!("REGRESSION TEST: AND with regex patterns (kind:function AND name~=/^public_/)");
let project = create_indexed_project(&[(
"test.rs",
r"
pub fn public_async() {}
pub fn public_sync() {}
fn private_func() {}
",
)]);
let output = sqry_cmd()
.arg("--semantic")
.arg("query")
.arg("kind:function AND name~=/^public_/")
.arg("--limit")
.arg("10")
.current_dir(project.path())
.output()
.unwrap();
assert!(
output.status.success(),
"Boolean AND with regex failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.contains("public_async") || stdout.contains("public_sync"),
"Expected public_ functions, got: {stdout}"
);
assert!(
!stdout.contains("private_func"),
"Should not contain private_func, got: {stdout}"
);
log::info!("✓ PASS: AND with regex patterns works correctly");
}
#[test]
fn test_boolean_or_basic() {
init_logging();
log::info!("REGRESSION TEST: Boolean OR operator (kind:function OR kind:struct)");
let project = create_indexed_project(&[(
"test.rs",
r"
fn test_function() {}
struct TestStruct {}
mod test_module {}
",
)]);
let output = sqry_cmd()
.arg("--semantic")
.arg("query")
.arg("kind:function OR kind:struct")
.arg("--limit")
.arg("10")
.current_dir(project.path())
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.contains("test_function") || stdout.contains("TestStruct"),
"Expected function or struct, got: {stdout}"
);
assert!(
!stdout.contains("test_module"),
"Should not contain module, got: {stdout}"
);
log::info!("✓ PASS: OR operator matches either condition");
}
#[test]
fn test_boolean_not_basic() {
init_logging();
log::info!("REGRESSION TEST: Boolean NOT operator (kind:function AND NOT name~=/test/)");
let project = create_indexed_project(&[(
"test.rs",
r"
fn test_one() {}
fn test_two() {}
fn production_code() {}
",
)]);
let output = sqry_cmd()
.arg("--semantic")
.arg("query")
.arg("kind:function AND NOT name~=/test/")
.arg("--limit")
.arg("10")
.current_dir(project.path())
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.contains("production"),
"Expected production_code, got: {stdout}"
);
assert!(
!stdout.contains("test_one") && !stdout.contains("test_two"),
"Should not contain test functions, got: {stdout}"
);
log::info!("✓ PASS: NOT operator correctly excludes matches");
}
#[test]
fn test_boolean_parentheses_grouping() {
init_logging();
log::info!("REGRESSION TEST: Parentheses grouping ((A OR B) AND C)");
let project = create_indexed_project(&[(
"test.rs",
r"
pub fn public_function() {}
fn private_function() {}
pub struct PublicStruct {}
",
)]);
let output = sqry_cmd()
.arg("--semantic")
.arg("query")
.arg("(kind:function OR kind:struct) AND name~=/public/i")
.arg("--limit")
.arg("10")
.current_dir(project.path())
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.contains("public") || stdout.contains("Public"),
"Expected public symbols, got: {stdout}"
);
log::info!("✓ PASS: Parentheses correctly group OR before AND");
}
#[test]
fn test_boolean_regex_pattern() {
init_logging();
log::info!("REGRESSION TEST: Regex with AND (kind:function AND name~=/^get_/)");
let project = create_indexed_project(&[(
"test.rs",
r"
fn get_user() {}
fn get_data() {}
fn set_user() {}
fn user_getter() {}
",
)]);
let output = sqry_cmd()
.arg("--semantic")
.arg("query")
.arg("kind:function AND name~=/^get_/")
.arg("--limit")
.arg("10")
.current_dir(project.path())
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.contains("get_user") || stdout.contains("get_data"),
"Expected get_* functions, got: {stdout}"
);
assert!(
!stdout.contains("set_user") && !stdout.contains("user_getter"),
"Should not contain set_ or *getter, got: {stdout}"
);
log::info!("✓ PASS: Regex patterns work correctly with boolean operators");
}
#[test]
fn test_boolean_complex_expression() {
init_logging();
log::info!("REGRESSION TEST: Complex expression (A AND (B OR C) AND NOT D)");
let project = create_indexed_project(&[(
"test.rs",
r"
pub fn public_prod() {}
pub fn public_test() {}
fn private_prod() {}
fn private_test() {}
",
)]);
let output = sqry_cmd()
.arg("--semantic")
.arg("query")
.arg("kind:function AND NOT name~=/test/")
.arg("--limit")
.arg("10")
.current_dir(project.path())
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.contains("prod"),
"Expected prod functions, got: {stdout}"
);
assert!(
!stdout.contains("public_test") && !stdout.contains("private_test"),
"Should not contain test functions, got: {stdout}"
);
log::info!("✓ PASS: Complex boolean expressions evaluate correctly");
}
#[test]
fn test_hybrid_mode_preserves_boolean_syntax() {
init_logging();
log::info!("REGRESSION TEST: Hybrid mode preserves boolean syntax (no Query::parse fallback)");
let project = create_indexed_project(&[(
"test.rs",
r"
fn helper() {}
fn test_helper() {}
// TODO: implement features
",
)]);
let output = sqry_cmd()
.arg("query")
.arg("kind:function AND name:helper")
.arg("--limit")
.arg("10")
.current_dir(project.path())
.output()
.unwrap();
assert!(
output.status.success(),
"Hybrid mode boolean query failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(
stderr.contains("[Semantic") || stderr.contains("[Hybrid") || stderr.is_empty(),
"Expected semantic attempt, got: {stderr}"
);
assert!(
stdout.contains("helper"),
"Expected to find helper function, got: {stdout}"
);
log::info!("✓ PASS: Hybrid mode correctly handles boolean query syntax with correct results");
}
#[test]
fn test_semantic_flag_boolean_query() {
init_logging();
log::info!("REGRESSION TEST: --semantic flag with boolean query");
let project = create_indexed_project(&[(
"test.rs",
r"
pub fn public_func() {}
fn private_func() {}
",
)]);
let output = sqry_cmd()
.arg("--semantic")
.arg("query")
.arg("kind:function AND name~=/public/")
.arg("--limit")
.arg("10")
.current_dir(project.path())
.output()
.unwrap();
assert!(
output.status.success(),
"Semantic mode boolean query failed: {}",
String::from_utf8_lossy(&output.stderr)
);
log::info!("✓ PASS: --semantic flag works with boolean syntax");
}
#[test]
fn test_relation_callers_supports_fqn_and_simple_names() {
init_logging();
log::info!("REGRESSION TEST: Relation callers predicate supports FQN + simple names");
let project = copy_fixture_dir("tests/fixtures/java/relation_tracking/calls");
let project_path = project.path();
sqry_cmd()
.arg("index")
.arg("--force")
.arg(project_path)
.assert()
.success();
let fqn_output = sqry_cmd()
.arg("--json")
.arg("query")
.arg("callers:com.example.service.UserService.findById")
.arg(project_path)
.output()
.expect("run callers query with FQN");
assert!(
fqn_output.status.success(),
"FQN callers query failed: {}",
String::from_utf8_lossy(&fqn_output.stderr)
);
let fqn_json: Value = serde_json::from_slice(&fqn_output.stdout).expect("valid JSON output");
let fqn_names = extract_symbol_names(&fqn_json);
let fqn_set: BTreeSet<_> = fqn_names.iter().cloned().collect();
assert!(
!fqn_set.is_empty(),
"FQN callers query should return at least one caller"
);
let simple_output = sqry_cmd()
.arg("--json")
.arg("query")
.arg("callers:findById")
.arg(project_path)
.output()
.expect("run callers query with simple name");
assert!(
simple_output.status.success(),
"Simple callers query failed: {}",
String::from_utf8_lossy(&simple_output.stderr)
);
let simple_json: Value =
serde_json::from_slice(&simple_output.stdout).expect("valid JSON output");
let simple_names = extract_symbol_names(&simple_json);
let simple_set: BTreeSet<_> = simple_names.iter().cloned().collect();
assert!(
fqn_set.is_subset(&simple_set),
"FQN results should be subset of simple name results. FQN: {fqn_set:?}, Simple: {simple_set:?}"
);
assert!(
fqn_set.iter().any(|name| name.contains("getUser")),
"Expected caller list to include controller method, got: {fqn_set:?}"
);
assert!(
simple_set.iter().any(|name| name.contains("getUser")),
"Expected simple name caller list to include controller method, got: {simple_set:?}"
);
}
#[test]
fn test_relation_returns_supports_fqn_and_simple_names() {
init_logging();
log::info!("REGRESSION TEST: Relation returns predicate supports FQN + simple names");
let project = copy_fixture_dir("tests/fixtures/java/relation_tracking/return_types");
let project_path = project.path();
sqry_cmd().arg("index").arg(project_path).assert().success();
let fqn_output = sqry_cmd()
.arg("--json")
.arg("query")
.arg("returns:Optional<User>")
.arg(project_path)
.output()
.expect("run returns query with FQN");
assert!(
fqn_output.status.success(),
"FQN returns query failed: {}",
String::from_utf8_lossy(&fqn_output.stderr)
);
let fqn_json: Value = serde_json::from_slice(&fqn_output.stdout).expect("valid JSON output");
let fqn_results = fqn_json["results"]
.as_array()
.expect("results should be array");
let fqn_names = extract_symbol_names(&fqn_json);
let fqn_set: BTreeSet<_> = fqn_names.iter().cloned().collect();
assert!(
!fqn_set.is_empty(),
"FQN returns query should return at least one method"
);
let simple_output = sqry_cmd()
.arg("--json")
.arg("query")
.arg("returns:Optional")
.arg(project_path)
.output()
.expect("run returns query with simple name");
assert!(
simple_output.status.success(),
"Simple returns query failed: {}",
String::from_utf8_lossy(&simple_output.stderr)
);
let simple_json: Value =
serde_json::from_slice(&simple_output.stdout).expect("valid JSON output");
let simple_results = simple_json["results"]
.as_array()
.expect("results should be array");
let simple_names = extract_symbol_names(&simple_json);
let simple_set: BTreeSet<_> = simple_names.iter().cloned().collect();
assert_eq!(
fqn_set.len(),
4,
"FQN query should match exactly 4 methods returning Optional<User>, got: {}",
fqn_set.len()
);
assert_eq!(
fqn_names.len(),
fqn_set.len(),
"FQN query should not emit duplicate semantic results, got: {fqn_names:?}"
);
assert_eq!(
simple_set.len(),
9,
"Simple query should match exactly 9 methods returning Optional<*>, got: {}",
simple_set.len()
);
assert_eq!(
simple_names.len(),
simple_set.len(),
"Simple query should not emit duplicate semantic results, got: {simple_names:?}"
);
assert!(
fqn_set.is_subset(&simple_set),
"FQN results must be a subset of simple name results. Missing: {:?}",
fqn_set.difference(&simple_set).collect::<Vec<_>>()
);
assert!(
fqn_set.iter().any(|name| name.ends_with("findUser")),
"Expected findUser in returns:Optional<User> results, got: {fqn_set:?}"
);
assert!(
fqn_set.iter().any(|name| name.ends_with("maybeFind")),
"Expected maybeFind in returns:Optional<User> results, got: {fqn_set:?}"
);
assert!(
fqn_set
.iter()
.any(|name| name.ends_with("annotatedOptional")),
"Expected annotatedOptional in returns:Optional<User> results, got: {fqn_set:?}"
);
assert!(
simple_set.iter().any(|name| name.ends_with("findUser"))
&& simple_set.iter().any(|name| name.ends_with("maybeFind")),
"Expected findUser and maybeFind in simple query results, got: {simple_set:?}"
);
assert!(
simple_set
.iter()
.any(|name| name.ends_with("nestedOptionals"))
|| simple_set
.iter()
.any(|name| name.ends_with("wildcardOptional")),
"Expected broader Optional<*> matches in simple query, got: {simple_set:?}"
);
let find_user_result = fqn_results
.iter()
.find(|result| result["name"].as_str() == Some("findUser"))
.expect("findUser should be present");
let qualified_name = find_user_result["qualified_name"]
.as_str()
.expect("qualified_name should be string");
assert_eq!(
qualified_name, "com.example.relations.ReturnTypes.findUser",
"Expected Java-native qualified_name display"
);
assert!(
!qualified_name.contains("::"),
"JSON qualified_name should not leak canonical separators: {qualified_name}"
);
assert_eq!(
simple_results.len(),
simple_set.len(),
"Simple query should not contain duplicate rows, got: {simple_results:?}"
);
let name_output = sqry_cmd()
.arg("--json")
.arg("query")
.arg("name:com.example.relations.ReturnTypes.Nested.nestedOptional")
.arg(project_path)
.output()
.expect("run native-qualified name query");
assert!(
name_output.status.success(),
"Native-qualified name query failed: {}",
String::from_utf8_lossy(&name_output.stderr)
);
let name_json: Value = serde_json::from_slice(&name_output.stdout).expect("valid JSON output");
let name_results = name_json["results"]
.as_array()
.expect("results should be array");
assert_eq!(
name_results.len(),
1,
"Expected one nestedOptional match for native-qualified name query, got: {name_results:?}"
);
}
#[test]
fn test_csharp_phase2_callers_and_returns() {
init_logging();
log::info!("REGRESSION TEST: C# relation queries (Phase 2)");
let project = copy_fixture_dir("sqry-lang-csharp/tests/fixtures/relation_tracking");
let project_path = project.path();
sqry_cmd().arg("index").arg(project_path).assert().success();
let callers_output = sqry_cmd()
.arg("--json")
.arg("query")
.arg("callers:LoadAsync")
.arg(project_path)
.output()
.expect("run callers query");
assert!(
callers_output.status.success(),
"callers query failed: {}",
String::from_utf8_lossy(&callers_output.stderr)
);
let callers_json: Value = serde_json::from_slice(&callers_output.stdout).expect("valid JSON");
let caller_names = extract_symbol_names(&callers_json);
assert!(
caller_names.iter().any(|name| name.contains("FetchAsync")),
"expected Service.FetchAsync caller, got: {caller_names:?}"
);
let returns_output = sqry_cmd()
.arg("--json")
.arg("query")
.arg("returns:Task<User>")
.arg(project_path)
.output()
.expect("run returns query");
assert!(
returns_output.status.success(),
"returns query failed: {}",
String::from_utf8_lossy(&returns_output.stderr)
);
let returns_json: Value = serde_json::from_slice(&returns_output.stdout).expect("valid JSON");
let return_names = extract_symbol_names(&returns_json);
assert!(
return_names.iter().any(|name| name.contains("FetchAsync")),
"expected FetchAsync in returns results, got: {return_names:?}"
);
}
#[test]
fn test_cpp_phase2_callers_and_returns() {
init_logging();
log::info!("REGRESSION TEST: C++ relation queries (Phase 2)");
let project = copy_fixture_dir("sqry-lang-cpp/tests/fixtures/relation_tracking");
let project_path = project.path();
sqry_cmd()
.arg("index")
.arg("--force")
.arg(project_path)
.assert()
.success();
let callers_output = sqry_cmd()
.arg("--json")
.arg("query")
.arg("callers:helper")
.arg(project_path)
.output()
.expect("run callers query");
assert!(callers_output.status.success());
let callers_json: Value = serde_json::from_slice(&callers_output.stdout).expect("valid JSON");
let caller_names = extract_symbol_names(&callers_json);
assert!(
caller_names.iter().any(|name| name.ends_with("process")),
"expected Service::process caller, got: {caller_names:?}"
);
let returns_output = sqry_cmd()
.arg("--json")
.arg("query")
.arg("returns:int")
.arg(project_path)
.output()
.expect("run returns query");
assert!(returns_output.status.success());
let returns_json: Value = serde_json::from_slice(&returns_output.stdout).expect("valid JSON");
let return_names = extract_symbol_names(&returns_json);
assert!(
return_names
.iter()
.any(|name| name.contains("save") || name.contains("process")),
"expected int-returning functions in results, got: {return_names:?}"
);
}
#[test]
fn test_kotlin_phase2_callers_and_returns() {
init_logging();
log::info!("REGRESSION TEST: Kotlin relation queries (Phase 2)");
let project = copy_fixture_dir("sqry-lang-kotlin/tests/fixtures/relation_tracking");
let project_path = project.path();
sqry_cmd().arg("index").arg(project_path).assert().success();
let callers_output = sqry_cmd()
.arg("--json")
.arg("query")
.arg("callers:fetch")
.arg(project_path)
.output()
.expect("run callers query");
assert!(callers_output.status.success());
let callers_json: Value = serde_json::from_slice(&callers_output.stdout).expect("valid JSON");
let caller_names = extract_symbol_names(&callers_json);
assert!(
caller_names.iter().any(|name| name.contains("process")),
"expected Service.process caller, got: {caller_names:?}"
);
let returns_output = sqry_cmd()
.arg("--json")
.arg("query")
.arg("returns:Deferred<Int>")
.arg(project_path)
.output()
.expect("run returns query");
assert!(returns_output.status.success());
let returns_json: Value = serde_json::from_slice(&returns_output.stdout).expect("valid JSON");
let return_names = extract_symbol_names(&returns_json);
assert!(
return_names.iter().any(|name| name.contains("process")),
"expected Service.process in returns results, got: {return_names:?}"
);
}
#[test]
fn test_boolean_syntax_error_reporting() {
init_logging();
log::info!("REGRESSION TEST: Boolean syntax errors are reported clearly");
let project = create_indexed_project(&[("test.rs", "fn main() {}")]);
let output = sqry_cmd()
.arg("--semantic")
.arg("query")
.arg("kind:function AND")
.arg("--limit")
.arg("10")
.current_dir(project.path())
.output()
.unwrap();
assert!(
!output.status.success(),
"Invalid boolean query should fail"
);
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(
stderr.contains("Error") || stderr.contains("parse") || stderr.contains("syntax"),
"Expected error message, got: {stderr}"
);
log::info!("✓ PASS: Syntax errors reported clearly");
}
#[test]
fn test_boolean_regex_security_large_quantifier() {
init_logging();
log::info!("SECURITY TEST: Large regex quantifiers handled safely");
let project = create_indexed_project(&[("test.rs", "fn test() {}")]);
let output = sqry_cmd()
.arg("--semantic")
.arg("query")
.arg("kind:function AND name~=/[a-z]{10000}/")
.arg("--limit")
.arg("10")
.current_dir(project.path())
.output()
.unwrap();
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(
output.status.success() || stderr.contains("InvalidRegex") || stderr.contains("regex"),
"Large quantifier should succeed or fail gracefully, got: {stderr}"
);
log::info!("✓ PASS: Large regex quantifiers handled safely (no DoS risk)");
}