use super::*;
use bonsai_lang_api::{AdapterContext, DeclIndex, ImportIndex, LanguageAdapter, LanguageCapabilities};
struct TestPythonAdapter;
struct TestCAdapter;
impl LanguageAdapter for TestPythonAdapter {
fn language_id(&self) -> LanguageId {
LanguageId::new("python")
}
fn display_name(&self) -> &'static str {
"test python"
}
fn file_extensions(&self) -> &'static [&'static str] {
&["py"]
}
fn tree_sitter_language(&self) -> Result<tree_sitter::Language, AdapterError> {
bonsai_lang_api::kit::language_from_pack("python")
}
fn capabilities(&self) -> LanguageCapabilities {
LanguageCapabilities::unsupported()
}
fn extract_declarations(&self, file: FileId, _ctx: &AdapterContext<'_>) -> DeclIndex {
DeclIndex {
file,
..DeclIndex::default()
}
}
fn extract_imports(&self, file: FileId, _ctx: &AdapterContext<'_>) -> ImportIndex {
ImportIndex {
file,
imports: Vec::new(),
}
}
}
fn test_python_adapter() -> AdapterArc {
Arc::new(TestPythonAdapter)
}
impl LanguageAdapter for TestCAdapter {
fn language_id(&self) -> LanguageId {
LanguageId::new("c")
}
fn display_name(&self) -> &'static str {
"test C"
}
fn file_extensions(&self) -> &'static [&'static str] {
&["c"]
}
fn tree_sitter_language(&self) -> Result<tree_sitter::Language, AdapterError> {
bonsai_lang_api::kit::language_from_pack("c")
}
fn parse_recovery_edits(
&self,
snapshot: &bonsai_lang_api::FileSnapshot,
vfs: &Vfs,
tree: &Tree,
) -> Vec<bonsai_lang_api::ParseRecoveryEdit> {
bonsai_lang_api::c_family_declaration_macro_recovery_edits(
snapshot,
vfs,
tree,
&["va_arg", "__builtin_va_arg"],
)
}
fn capabilities(&self) -> LanguageCapabilities {
LanguageCapabilities::unsupported()
}
fn extract_declarations(&self, file: FileId, _ctx: &AdapterContext<'_>) -> DeclIndex {
DeclIndex {
file,
..DeclIndex::default()
}
}
fn extract_imports(&self, file: FileId, _ctx: &AdapterContext<'_>) -> ImportIndex {
ImportIndex {
file,
imports: Vec::new(),
}
}
}
fn test_c_adapter() -> AdapterArc {
Arc::new(TestCAdapter)
}
#[test]
fn byte_offsets_saturate_instead_of_wrapping() {
assert_eq!(saturating_byte_offset(u64::MAX as usize), u64::MAX);
}
#[test]
fn zero_parse_timeout_disables_timeout() {
assert_eq!(parse_timeout_millis(0), None);
assert_eq!(parse_timeout_millis(5), Some(Duration::from_millis(5)));
}
#[test]
fn parser_default_is_uncapped_when_environment_is_unset() {
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _lock = ENV_LOCK.lock().expect("parse-timeout env lock");
let previous = std::env::var_os("BONSAI_PARSE_TIMEOUT_MS");
std::env::remove_var("BONSAI_PARSE_TIMEOUT_MS");
assert_eq!(ParserOptions::default().parse_timeout, None);
if let Some(previous) = previous {
std::env::set_var("BONSAI_PARSE_TIMEOUT_MS", previous);
} else {
std::env::remove_var("BONSAI_PARSE_TIMEOUT_MS");
}
}
#[test]
fn parse_timeout_diagnostic_is_file_level_warning() {
let diagnostic = parse_timeout_diagnostic(FileId::new(1), 42, Duration::from_millis(7));
assert_eq!(diagnostic.severity, Severity::Warning);
assert_eq!(diagnostic.code.as_deref(), Some("parse-timeout"));
assert_eq!(diagnostic.span, bonsai_common::Span::new(FileId::new(1), 0, 42));
assert_eq!(diagnostic.message, "file skipped: parse timeout after 7 ms");
}
#[test]
fn c_variadic_pointer_type_recovers_without_changing_source_coordinates() {
let source = "void exec_all(int count, ...) {\n\
va_list args;\n\
char *cmd = va_arg(args, char *);\n\
sink(cmd);\n\
}\n";
let cache = ParserCache::with_options(ParserOptions::with_parse_timeout(None));
let vfs = Vfs::new();
let file = vfs.write("variadic.c", source);
let parsed = cache
.parse(file, &test_c_adapter(), &vfs)
.expect("parse recovered C variadic fixture");
assert!(
parsed.used_recovery,
"pointer type operand should use grammar recovery"
);
assert!(!parsed.tree.root_node().has_error());
assert!(parsed.diagnostics.is_empty());
assert_eq!(parsed.source_text(), source);
}
#[test]
fn repeated_exact_snapshot_reuses_parsed_file_and_tree_arcs() {
let cache = ParserCache::with_options(ParserOptions::with_parse_timeout(None));
let vfs = Vfs::new();
let file = vfs.write("fixture.py", "def cached():\n return 1\n");
let adapter = test_python_adapter();
let first = cache.parse(file, &adapter, &vfs).expect("first parse");
let second = cache.parse(file, &adapter, &vfs).expect("cached parse");
assert!(Arc::ptr_eq(&first, &second));
assert!(Arc::ptr_eq(&first.tree, &second.tree));
}
#[test]
fn exact_release_evicts_only_the_lowered_workspace_file_and_language() {
let cache = ParserCache::with_options(ParserOptions::with_parse_timeout(None));
let vfs = Vfs::new();
let released_file = vfs.write("released.py", "def released():\n return 1\n");
let retained_file = vfs.write("retained.py", "def retained():\n return 2\n");
let adapter = test_python_adapter();
let released_before = cache
.parse(released_file, &adapter, &vfs)
.expect("parse released fixture");
let retained_before = cache
.parse(retained_file, &adapter, &vfs)
.expect("parse retained fixture");
cache.release(released_file, &adapter, &vfs);
let released_after = cache
.parse(released_file, &adapter, &vfs)
.expect("reparse released fixture");
let retained_after = cache
.parse(retained_file, &adapter, &vfs)
.expect("reuse retained fixture");
assert!(!Arc::ptr_eq(&released_before, &released_after));
assert!(!Arc::ptr_eq(&released_before.tree, &released_after.tree));
assert!(Arc::ptr_eq(&retained_before, &retained_after));
assert!(Arc::ptr_eq(&retained_before.tree, &retained_after.tree));
}
#[test]
fn reparsing_an_edit_keeps_tree_and_source_on_the_same_version() {
let cache = ParserCache::with_options(ParserOptions::with_parse_timeout(None));
let vfs = Vfs::new();
let file = vfs.write("fixture.py", "def before():\n return 1\n");
let adapter = test_python_adapter();
let before = cache.parse(file, &adapter, &vfs).expect("initial parse");
vfs.write("fixture.py", "def after():\n return 2\n");
let after = cache.parse(file, &adapter, &vfs).expect("edited parse");
assert_eq!(after.version, before.version + 1);
assert_eq!(after.source_text(), "def after():\n return 2\n");
assert_eq!(
first_node_text(&after.tree, after.source_text(), "identifier").as_deref(),
Some("after")
);
assert!(!Arc::ptr_eq(&before.tree, &after.tree));
}
#[test]
fn cache_identity_includes_the_vfs_instance() {
let cache = ParserCache::with_options(ParserOptions::with_parse_timeout(None));
let first_vfs = Vfs::new();
let second_vfs = Vfs::new();
let first_file = first_vfs.write("fixture.py", "def first():\n pass\n");
let second_file = second_vfs.write("fixture.py", "def second():\n pass\n");
assert_eq!(first_file, second_file, "fixture must exercise colliding FileIds");
let adapter = test_python_adapter();
let first = cache
.parse(first_file, &adapter, &first_vfs)
.expect("first workspace parse");
let second = cache
.parse(second_file, &adapter, &second_vfs)
.expect("second workspace parse");
assert_eq!(first.source_text(), "def first():\n pass\n");
assert_eq!(second.source_text(), "def second():\n pass\n");
assert!(!Arc::ptr_eq(&first.tree, &second.tree));
}
#[test]
fn same_language_worker_checkouts_are_not_globally_serialized() {
use std::sync::{mpsc, Condvar, Mutex as StdMutex};
let cache = ParserCache::with_options(ParserOptions::with_parse_timeout(None));
let release = Arc::new((StdMutex::new(false), Condvar::new()));
let (entered_tx, entered_rx) = mpsc::channel();
let mut workers = Vec::new();
for _ in 0..2 {
let cache = cache.clone();
let release = Arc::clone(&release);
let entered_tx = entered_tx.clone();
workers.push(std::thread::spawn(move || {
let _parser = cache.checkout_parser("python");
entered_tx.send(()).expect("announce parser checkout");
let (lock, wake) = &*release;
let mut released = lock.lock().expect("release lock");
while !*released {
released = wake.wait(released).expect("release wait");
}
}));
}
drop(entered_tx);
let first_entered = entered_rx.recv_timeout(Duration::from_secs(2));
let second_entered = entered_rx.recv_timeout(Duration::from_secs(2));
{
let (lock, wake) = &*release;
*lock.lock().expect("release lock") = true;
wake.notify_all();
}
for worker in workers {
worker.join().expect("parser worker");
}
assert!(first_entered.is_ok(), "first worker never checked out a parser");
assert!(
second_entered.is_ok(),
"same-language worker was serialized behind the first parser checkout"
);
}
#[test]
fn replacement_edit_uses_utf8_boundaries_and_exact_points() {
let old = "let café = 1;\nnext();\n";
let new = "let cañon = 22;\nnext();\n";
let edit = single_replacement_edit(old, new);
assert!(old.is_char_boundary(edit.start_byte));
assert!(old.is_char_boundary(edit.old_end_byte));
assert!(new.is_char_boundary(edit.new_end_byte));
assert_eq!(edit.start_position, point_at_byte(old, edit.start_byte));
assert_eq!(edit.old_end_position, point_at_byte(old, edit.old_end_byte));
assert_eq!(edit.new_end_position, point_at_byte(new, edit.new_end_byte));
}
fn first_node_text(tree: &Tree, source: &str, kind: &str) -> Option<String> {
let mut stack = vec![tree.root_node()];
while let Some(node) = stack.pop() {
if node.kind() == kind {
return source.get(node.byte_range()).map(ToOwned::to_owned);
}
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor));
}
None
}