use fresh::model::buffer::TextBuffer;
use fresh::model::filesystem::{FileSystem, WriteOp};
use fresh::services::remote::{
spawn_local_agent, spawn_local_agent_with_capacity, RemoteFileSystem, TEST_RECV_DELAY_US,
};
use std::sync::atomic::Ordering;
use std::sync::Arc;
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Self(seed)
}
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn usize(&mut self, max: usize) -> usize {
(self.next() % max as u64) as usize
}
}
fn create_test_filesystem() -> Option<(RemoteFileSystem, tempfile::TempDir, tokio::runtime::Runtime)>
{
let temp_dir = tempfile::tempdir().ok()?;
let rt = tokio::runtime::Runtime::new().ok()?;
let channel = rt.block_on(spawn_local_agent()).ok()?;
let fs = RemoteFileSystem::new(channel, "test@localhost".to_string());
Some((fs, temp_dir, rt))
}
#[test]
fn test_read_file_content() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let test_path = temp_dir.path().join("test.txt");
let test_content = b"Hello, this is test content!\nLine 2\nLine 3";
std::fs::write(&test_path, test_content).unwrap();
let read_content = fs.read_file(&test_path).unwrap();
assert_eq!(
read_content, test_content,
"File content should match what was written"
);
}
#[test]
fn test_write_and_read_roundtrip() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let test_path = temp_dir.path().join("write_test.txt");
let test_content = b"Content written via RemoteFileSystem";
fs.write_file(&test_path, test_content).unwrap();
let read_content = fs.read_file(&test_path).unwrap();
assert_eq!(
read_content, test_content,
"Read content should match written content"
);
let direct_read = std::fs::read(&test_path).unwrap();
assert_eq!(
direct_read, test_content,
"Direct file read should match written content"
);
}
#[test]
fn test_read_large_file() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let test_path = temp_dir.path().join("large.bin");
let test_content: Vec<u8> = (0..100_000).map(|i| (i % 256) as u8).collect();
std::fs::write(&test_path, &test_content).unwrap();
let read_content = fs.read_file(&test_path).unwrap();
assert_eq!(
read_content.len(),
test_content.len(),
"File sizes should match"
);
assert_eq!(
read_content, test_content,
"Large file content should match"
);
}
#[test]
fn test_is_dir() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let dir_path = temp_dir.path().join("subdir");
let file_path = temp_dir.path().join("file.txt");
std::fs::create_dir(&dir_path).unwrap();
std::fs::write(&file_path, b"content").unwrap();
assert!(fs.is_dir(&dir_path).unwrap(), "Should detect directory");
assert!(!fs.is_dir(&file_path).unwrap(), "File should not be a dir");
}
#[test]
fn test_read_dir() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
std::fs::write(temp_dir.path().join("file1.txt"), b"1").unwrap();
std::fs::write(temp_dir.path().join("file2.txt"), b"2").unwrap();
std::fs::create_dir(temp_dir.path().join("subdir")).unwrap();
let entries = fs.read_dir(temp_dir.path()).unwrap();
let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"file1.txt"), "Should contain file1.txt");
assert!(names.contains(&"file2.txt"), "Should contain file2.txt");
assert!(names.contains(&"subdir"), "Should contain subdir");
}
#[test]
fn test_remote_connection_info() {
let Some((fs, _temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
assert_eq!(
fs.remote_connection_info(),
Some("test@localhost"),
"Should return connection string"
);
}
#[test]
fn test_metadata() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let test_path = temp_dir.path().join("meta_test.txt");
let content = b"test content for metadata";
std::fs::write(&test_path, content).unwrap();
let meta = fs.metadata(&test_path).unwrap();
assert_eq!(
meta.size,
content.len() as u64,
"Size should match content length"
);
}
#[test]
fn test_read_file_larger_than_threshold() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let test_path = temp_dir.path().join("very_large.bin");
let size = 1_500_000;
let test_content: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
std::fs::write(&test_path, &test_content).unwrap();
let read_content = fs.read_file(&test_path).unwrap();
assert_eq!(
read_content.len(),
test_content.len(),
"File sizes should match for 1.5MB file"
);
assert_eq!(
read_content, test_content,
"Very large file content should match"
);
}
#[test]
fn test_write_and_read_file_larger_than_threshold() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let test_path = temp_dir.path().join("write_large.bin");
let size = 2_000_000;
let test_content: Vec<u8> = (0..size).map(|i| ((i * 7) % 256) as u8).collect();
fs.write_file(&test_path, &test_content).unwrap();
let read_content = fs.read_file(&test_path).unwrap();
assert_eq!(
read_content.len(),
test_content.len(),
"2MB file sizes should match after roundtrip"
);
assert_eq!(
read_content, test_content,
"2MB file content should match after roundtrip"
);
}
#[test]
fn test_read_range_on_large_file() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let test_path = temp_dir.path().join("range_large.bin");
let size = 1_500_000;
let test_content: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
std::fs::write(&test_path, &test_content).unwrap();
let offset = 1_000_000; let len = 100_000; let read_content = fs.read_range(&test_path, offset, len).unwrap();
assert_eq!(read_content.len(), len, "Read range length should match");
assert_eq!(
read_content,
&test_content[offset as usize..(offset as usize + len)],
"Read range content should match"
);
}
#[test]
fn test_append_to_file() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let test_path = temp_dir.path().join("append_test.txt");
fs.write_file(&test_path, b"Hello").unwrap();
{
use std::io::Write;
let mut writer = fs.open_file_for_append(&test_path).unwrap();
writer.write_all(b" World").unwrap();
writer.sync_all().unwrap();
}
let content = fs.read_file(&test_path).unwrap();
assert_eq!(
content, b"Hello World",
"Append should add to existing content"
);
}
#[test]
fn test_append_creates_file_if_missing() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let test_path = temp_dir.path().join("append_new.txt");
assert!(!test_path.exists());
{
use std::io::Write;
let mut writer = fs.open_file_for_append(&test_path).unwrap();
writer.write_all(b"New content").unwrap();
writer.sync_all().unwrap();
}
let content = fs.read_file(&test_path).unwrap();
assert_eq!(content, b"New content");
}
#[test]
fn test_truncate_file() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let test_path = temp_dir.path().join("truncate_test.txt");
fs.write_file(&test_path, b"Hello World!").unwrap();
fs.set_file_length(&test_path, 5).unwrap();
let content = fs.read_file(&test_path).unwrap();
assert_eq!(content, b"Hello", "File should be truncated to 5 bytes");
}
#[test]
fn test_truncate_extend_file() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let test_path = temp_dir.path().join("extend_test.txt");
fs.write_file(&test_path, b"Hi").unwrap();
fs.set_file_length(&test_path, 10).unwrap();
let content = fs.read_file(&test_path).unwrap();
assert_eq!(content.len(), 10, "File should be extended to 10 bytes");
assert_eq!(&content[0..2], b"Hi", "Original content preserved");
assert!(
content[2..].iter().all(|&b| b == 0),
"Extended portion should be zeros"
);
}
#[test]
fn test_write_patched_copy_and_insert() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let src_path = temp_dir.path().join("patch_src.txt");
let dst_path = temp_dir.path().join("patch_dst.txt");
fs.write_file(&src_path, b"AAABBBCCC").unwrap();
let ops = vec![
WriteOp::Copy { offset: 0, len: 3 }, WriteOp::Insert { data: b"XXX" }, WriteOp::Copy { offset: 6, len: 3 }, ];
fs.write_patched(&src_path, &dst_path, &ops).unwrap();
let content = fs.read_file(&dst_path).unwrap();
assert_eq!(
content, b"AAAXXXCCC",
"Patched content should match expected"
);
}
#[test]
fn test_write_patched_in_place() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let path = temp_dir.path().join("patch_inplace.txt");
fs.write_file(&path, b"Hello World").unwrap();
let ops = vec![
WriteOp::Copy { offset: 0, len: 6 }, WriteOp::Insert { data: b"Rust!" }, ];
fs.write_patched(&path, &path, &ops).unwrap();
let content = fs.read_file(&path).unwrap();
assert_eq!(content, b"Hello Rust!", "In-place patch should work");
}
#[test]
fn test_write_patched_large_file_small_edit() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let path = temp_dir.path().join("large_patch.bin");
let size = 1_000_000;
let original: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
fs.write_file(&path, &original).unwrap();
let insert_data = b"THIS IS THE NEW CONTENT INSERTED IN THE MIDDLE OF A LARGE FILE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!";
let ops = vec![
WriteOp::Copy {
offset: 0,
len: 500_000,
},
WriteOp::Insert { data: insert_data },
WriteOp::Copy {
offset: 500_000,
len: 500_000,
},
];
fs.write_patched(&path, &path, &ops).unwrap();
let content = fs.read_file(&path).unwrap();
assert_eq!(
content.len(),
size + insert_data.len(),
"File size should be original + inserted"
);
assert_eq!(
&content[0..500_000],
&original[0..500_000],
"First half should match"
);
assert_eq!(
&content[500_000..500_000 + insert_data.len()],
insert_data,
"Inserted content should match"
);
assert_eq!(
&content[500_000 + insert_data.len()..],
&original[500_000..],
"Second half should match"
);
}
#[test]
fn test_write_patched_preserves_permissions() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let path = temp_dir.path().join("perms_test.txt");
fs.write_file(&path, b"original").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let ops = vec![WriteOp::Insert { data: b"patched" }];
fs.write_patched(&path, &path, &ops).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::metadata(&path).unwrap().permissions();
assert_eq!(
perms.mode() & 0o777,
0o755,
"Permissions should be preserved after patch"
);
}
}
fn create_test_filesystem_arc() -> Option<(
Arc<RemoteFileSystem>,
tempfile::TempDir,
tokio::runtime::Runtime,
)> {
let temp_dir = tempfile::tempdir().ok()?;
let rt = tokio::runtime::Runtime::new().ok()?;
let channel = rt.block_on(spawn_local_agent()).ok()?;
let fs = Arc::new(RemoteFileSystem::new(channel, "test@localhost".to_string()));
Some((fs, temp_dir, rt))
}
#[test]
fn test_buffer_save_new_file_through_remote() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem_arc() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let file_path = temp_dir.path().join("new_file.txt");
let mut buffer = TextBuffer::from_bytes(b"Hello, World!\nLine 2\n".to_vec(), fs);
buffer.save_to_file(&file_path).unwrap();
let content = std::fs::read(&file_path).unwrap();
assert_eq!(content, b"Hello, World!\nLine 2\n");
}
#[test]
fn test_buffer_save_edited_file_through_remote() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem_arc() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let file_path = temp_dir.path().join("edit_test.txt");
std::fs::write(&file_path, b"AAABBBCCC").unwrap();
let mut buffer = TextBuffer::load_from_file(&file_path, 1024 * 1024, fs).unwrap();
buffer.delete_bytes(3, 3); buffer.insert_bytes(3, b"XXX".to_vec());
buffer.save_to_file(&file_path).unwrap();
let content = std::fs::read(&file_path).unwrap();
assert_eq!(content, b"AAAXXXCCC");
}
#[test]
fn test_buffer_save_with_copy_ops_through_remote() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem_arc() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let file_path = temp_dir.path().join("copy_ops_test.txt");
let original: Vec<u8> = (0..10000).map(|i| b'A' + (i % 26) as u8).collect();
std::fs::write(&file_path, &original).unwrap();
let mut buffer = TextBuffer::load_from_file(&file_path, 1024 * 1024, fs).unwrap();
let edit_pos = 5000;
buffer.delete_bytes(edit_pos, 10);
buffer.insert_bytes(edit_pos, b"EDITED".to_vec());
buffer.save_to_file(&file_path).unwrap();
let content = std::fs::read(&file_path).unwrap();
assert_eq!(content.len(), original.len() - 10 + 6);
assert_eq!(&content[edit_pos..edit_pos + 6], b"EDITED");
assert_eq!(&content[0..100], &original[0..100]);
assert_eq!(
&content[content.len() - 100..],
&original[original.len() - 100..]
);
}
#[test]
fn test_buffer_save_as_different_path_through_remote() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem_arc() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let original_path = temp_dir.path().join("original.txt");
let new_path = temp_dir.path().join("copy.txt");
std::fs::write(&original_path, b"Original content").unwrap();
let mut buffer = TextBuffer::load_from_file(&original_path, 1024 * 1024, fs).unwrap();
buffer.insert_bytes(0, b"Modified: ".to_vec());
buffer.save_to_file(&new_path).unwrap();
let new_content = std::fs::read(&new_path).unwrap();
assert_eq!(new_content, b"Modified: Original content");
let original_content = std::fs::read(&original_path).unwrap();
assert_eq!(original_content, b"Original content");
}
#[test]
fn test_buffer_save_with_line_ending_conversion_through_remote() {
use fresh::model::buffer::LineEnding;
let Some((fs, temp_dir, _rt)) = create_test_filesystem_arc() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let file_path = temp_dir.path().join("line_endings.txt");
std::fs::write(&file_path, b"Line 1\r\nLine 2\r\nLine 3\r\n").unwrap();
let mut buffer = TextBuffer::load_from_file(&file_path, 1024 * 1024, fs).unwrap();
assert_eq!(buffer.line_ending(), LineEnding::CRLF);
buffer.set_line_ending(LineEnding::LF);
buffer.save_to_file(&file_path).unwrap();
let content = std::fs::read(&file_path).unwrap();
assert_eq!(content, b"Line 1\nLine 2\nLine 3\n");
assert!(!content.contains(&b'\r'));
}
#[test]
fn test_buffer_save_empty_file_through_remote() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem_arc() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let file_path = temp_dir.path().join("empty.txt");
let mut buffer = TextBuffer::from_bytes(Vec::new(), fs);
buffer.save_to_file(&file_path).unwrap();
let content = std::fs::read(&file_path).unwrap();
assert!(content.is_empty());
}
#[test]
fn test_buffer_multiple_edits_then_save_through_remote() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem_arc() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let file_path = temp_dir.path().join("multi_edit.txt");
std::fs::write(&file_path, b"The quick brown fox jumps over the lazy dog.").unwrap();
let mut buffer = TextBuffer::load_from_file(&file_path, 1024 * 1024, fs).unwrap();
buffer.delete_bytes(35, 4); buffer.insert_bytes(35, b"energetic".to_vec());
buffer.delete_bytes(10, 5); buffer.insert_bytes(10, b"red".to_vec());
buffer.delete_bytes(4, 5); buffer.insert_bytes(4, b"slow".to_vec());
buffer.save_to_file(&file_path).unwrap();
let content = std::fs::read(&file_path).unwrap();
assert_eq!(content, b"The slow red fox jumps over the energetic dog.");
}
#[test]
fn test_buffer_save_large_file_with_small_edit_through_remote() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem_arc() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let file_path = temp_dir.path().join("large_edit.bin");
let size = 1_000_000;
let original: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
std::fs::write(&file_path, &original).unwrap();
let mut buffer = TextBuffer::load_from_file(&file_path, 1024 * 1024, fs).unwrap();
let edit_pos = size - 10;
buffer.delete_bytes(edit_pos, 5);
buffer.insert_bytes(edit_pos, b"END".to_vec());
buffer.save_to_file(&file_path).unwrap();
let content = std::fs::read(&file_path).unwrap();
assert_eq!(content.len(), size - 5 + 3);
assert_eq!(&content[0..1000], &original[0..1000]);
assert_eq!(&content[edit_pos..edit_pos + 3], b"END");
}
#[test]
fn test_buffer_large_file_edits_at_beginning_middle_and_end_through_remote() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem_arc() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let file_path = temp_dir.path().join("large_multi_edit.txt");
const LINE_LEN: usize = 22;
const NUM_LINES: usize = 1_000_000;
let mut original = Vec::with_capacity(LINE_LEN * NUM_LINES);
let mut original_lines: Vec<Vec<u8>> = vec![];
for i in 0..NUM_LINES {
let line = format!("Line {:08} content\n", i);
assert_eq!(line.len(), LINE_LEN);
original.extend_from_slice(line.as_bytes());
original_lines.push(line.into_bytes());
}
let size = original.len();
assert_eq!(size, LINE_LEN * NUM_LINES);
std::fs::write(&file_path, &original).unwrap();
let mut buffer = TextBuffer::load_from_file(&file_path, 0, fs).unwrap();
assert_eq!(
buffer.total_bytes(),
size,
"Loaded buffer size ({}) != original file size ({}). \
{} bytes lost during streaming read!",
buffer.total_bytes(),
size,
size.saturating_sub(buffer.total_bytes()),
);
let orig_line_count = original_lines.len();
let mut expected_lines = Vec::from(original_lines);
let steps = 4;
let mut offset = 0;
for i in 0..steps {
let pos = i * (NUM_LINES / steps);
if pos >= orig_line_count {
break;
}
let line = format!("new {:08}\n", pos);
let bytes = line.into_bytes();
let target_offset = pos * LINE_LEN + offset;
offset += bytes.len();
println!("Inserting: at line: {}, offset: {}", pos, target_offset);
buffer.insert_bytes(target_offset, bytes.clone());
expected_lines.insert(pos + i, bytes);
}
buffer.save_to_file(&file_path).unwrap();
let content = std::fs::read(&file_path).unwrap();
let content_str = String::from_utf8(content.clone()).expect("Content should be valid UTF-8");
let content_lines: Vec<&str> = content_str.lines().collect();
for (line_num, (got, want)) in content_lines.iter().zip(expected_lines.iter()).enumerate() {
let want_bytes = &want[..(want.len() - 1)]; assert_eq!(
got.as_bytes(),
want_bytes,
"Line {} mismatch:\n got: {:?}\n expected: {:?}",
line_num,
got,
String::from_utf8(want.clone()).unwrap()
);
}
assert_eq!(
content_lines.len(),
expected_lines.len(),
"Line count should match: got {}, expected {}",
content_lines.len(),
expected_lines.len()
);
}
#[test]
fn test_buffer_large_file_multiple_scattered_edits_through_remote() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem_arc() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let file_path = temp_dir.path().join("scattered_edits.txt");
const LINE_LEN: usize = 28;
const NUM_LINES: usize = 400_000; let make_line = |n: usize| -> String { format!("L:{:08}:{:08}:content\n", n, n) };
let mut original = Vec::with_capacity(LINE_LEN * NUM_LINES);
for i in 0..NUM_LINES {
let line = make_line(i);
assert_eq!(line.len(), LINE_LEN);
original.extend_from_slice(line.as_bytes());
}
let size = original.len();
assert_eq!(size, LINE_LEN * NUM_LINES);
std::fs::write(&file_path, &original).unwrap();
let mut buffer = TextBuffer::load_from_file(&file_path, 0, fs).unwrap();
let edits: Vec<(usize, &[u8], usize)> = vec![
(380_000, b"[NEAR_END_MARKER]\n", 1), (300_000, b"[300K_LINE_MARKER]\n", 0), (200_123, b"[HALFWAY_MARKER]\n", 0), (100_000, b"[100K_LINE_MARKER]\n", 0), (50_515, b"[50K_LINE_MARKER]\n", 0), (10_000, b"[10K_LINE_MARKER]\n", 0), (0, b"[FILE_HEADER]\n", 0), ];
for (line_num, insert_data, lines_to_delete) in &edits {
let byte_pos = line_num * LINE_LEN;
let delete_bytes = lines_to_delete * LINE_LEN;
if delete_bytes > 0 {
buffer.delete_bytes(byte_pos, delete_bytes);
}
buffer.insert_bytes(byte_pos, insert_data.to_vec());
}
buffer.save_to_file(&file_path).unwrap();
let mut expected = Vec::with_capacity(size + 200);
let deleted_lines: std::collections::HashSet<usize> = edits
.iter()
.filter(|(_, _, del)| *del > 0)
.map(|(line, _, _)| *line)
.collect();
let mut markers: Vec<(usize, &[u8])> =
edits.iter().map(|(line, data, _)| (*line, *data)).collect();
markers.sort_by_key(|(line, _)| *line);
let mut current_line = 0;
for (marker_line, marker_data) in &markers {
while current_line < *marker_line {
if !deleted_lines.contains(¤t_line) {
expected.extend_from_slice(make_line(current_line).as_bytes());
}
current_line += 1;
}
expected.extend_from_slice(marker_data);
if deleted_lines.contains(marker_line) {
current_line = marker_line + 1;
}
}
while current_line < NUM_LINES {
if !deleted_lines.contains(¤t_line) {
expected.extend_from_slice(make_line(current_line).as_bytes());
}
current_line += 1;
}
let content = std::fs::read(&file_path).unwrap();
let content_str = String::from_utf8(content.clone()).expect("Content should be valid UTF-8");
let expected_str = String::from_utf8(expected.clone()).expect("Expected should be valid UTF-8");
let content_lines: Vec<&str> = content_str.lines().collect();
let expected_lines: Vec<&str> = expected_str.lines().collect();
assert_eq!(
content_lines.len(),
expected_lines.len(),
"Line count should match: got {}, expected {}",
content_lines.len(),
expected_lines.len()
);
for (line_num, (got, want)) in content_lines.iter().zip(expected_lines.iter()).enumerate() {
assert_eq!(
got, want,
"Line {} mismatch:\n got: {:?}\n expected: {:?}",
line_num, got, want
);
}
assert_eq!(
content.len(),
expected.len(),
"Byte length should match: got {}, expected {}",
content.len(),
expected.len()
);
assert_eq!(content, expected, "Full byte content should match expected");
}
#[test]
fn test_buffer_huge_file_multi_save_cycle_through_remote() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem_arc() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let file_path = temp_dir.path().join("multi_save_huge.txt");
const NUM_LINES: usize = 1_000_000;
let mut original = Vec::new();
let mut expected_lines = Vec::with_capacity(NUM_LINES);
let mut line_starts = Vec::with_capacity(NUM_LINES);
let mut current_offset = 0;
for i in 0..NUM_LINES {
line_starts.push(current_offset);
let line = format!("Line {:05}: original content\n", i);
current_offset += line.len();
original.extend_from_slice(line.as_bytes());
expected_lines.push(line);
}
std::fs::write(&file_path, &original).unwrap();
let threshold = 1024 * 1024;
let mut buffer = TextBuffer::load_from_file(&file_path, threshold, fs).unwrap();
for target_line in vec![5000, 3] {
let edit_text = format!("ITER_{}_", target_line);
let byte_pos = line_starts[target_line];
buffer.insert_bytes(byte_pos, edit_text.as_bytes().to_vec());
expected_lines[target_line] = format!("{}{}", edit_text, expected_lines[target_line]);
buffer.save_to_file(&file_path).unwrap();
let content = std::fs::read(&file_path).unwrap();
let content_str = String::from_utf8(content).unwrap();
let content_lines: Vec<&str> = content_str.lines().collect();
assert_eq!(
content_lines.len(),
expected_lines.len(),
"Line count mismatch at iter {}",
target_line
);
for (i, (got, want)) in content_lines.iter().zip(expected_lines.iter()).enumerate() {
let want_trimmed = want.trim_end_matches('\n');
if *got != want_trimmed {
panic!(
"Line {} mismatch at iter {}:\n got: {:?}\n expected: {:?}",
i, target_line, got, want_trimmed
);
}
}
}
}
#[test]
fn test_buffer_shadow_random_ops_through_remote() {
let Some((fs, temp_dir, _rt)) = create_test_filesystem_arc() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let file_path = temp_dir.path().join("shadow_random.txt");
const LINE_LEN: usize = 22;
const NUM_LINES: usize = 100_000; let mut shadow: Vec<u8> = Vec::with_capacity(LINE_LEN * NUM_LINES);
for i in 0..NUM_LINES {
let line = format!("Line {:08} content\n", i);
shadow.extend_from_slice(line.as_bytes());
}
std::fs::write(&file_path, &shadow).unwrap();
let mut buffer = TextBuffer::load_from_file(&file_path, 0, fs.clone()).unwrap();
assert_eq!(
buffer.total_bytes(),
shadow.len(),
"Initial load lost data: buffer={} shadow={}",
buffer.total_bytes(),
shadow.len(),
);
let mut rng = Rng::new(0xDEAD_BEEF_CAFE_1059);
const NUM_OPS: usize = 2000;
const SAVE_EVERY: usize = 50;
for op_idx in 0..NUM_OPS {
let buf_len = buffer.total_bytes();
assert_eq!(
buf_len,
shadow.len(),
"Size mismatch before op {}: buffer={} shadow={}",
op_idx,
buf_len,
shadow.len(),
);
let op_kind = rng.usize(10);
if buf_len == 0 || op_kind < 5 {
let pos = if buf_len == 0 {
0
} else {
rng.usize(buf_len + 1)
};
let payload_len = 1 + rng.usize(200);
let payload: Vec<u8> = (0..payload_len)
.map(|j| b'A' + ((op_idx + j) % 26) as u8)
.collect();
buffer.insert_bytes(pos, payload.clone());
shadow.splice(pos..pos, payload.iter().copied());
} else if op_kind < 8 {
let pos = rng.usize(buf_len);
let max_del = (buf_len - pos).min(500);
if max_del == 0 {
continue;
}
let del_len = 1 + rng.usize(max_del);
buffer.delete_bytes(pos, del_len);
shadow.drain(pos..pos + del_len);
} else {
let pos = rng.usize(buf_len);
let max_del = (buf_len - pos).min(300);
if max_del == 0 {
continue;
}
let del_len = 1 + rng.usize(max_del);
buffer.delete_bytes(pos, del_len);
shadow.drain(pos..pos + del_len);
let payload_len = 1 + rng.usize(200);
let payload: Vec<u8> = (0..payload_len)
.map(|j| b'a' + ((op_idx + j) % 26) as u8)
.collect();
let insert_pos = pos.min(buffer.total_bytes());
buffer.insert_bytes(insert_pos, payload.clone());
shadow.splice(insert_pos..insert_pos, payload.iter().copied());
}
if (op_idx + 1) % SAVE_EVERY == 0 {
buffer.save_to_file(&file_path).unwrap();
let on_disk = std::fs::read(&file_path).unwrap();
assert_eq!(
on_disk.len(),
shadow.len(),
"Size mismatch after save at op {}: disk={} shadow={}",
op_idx,
on_disk.len(),
shadow.len(),
);
if on_disk != shadow {
let first_diff = on_disk
.iter()
.zip(shadow.iter())
.position(|(a, b)| a != b)
.unwrap_or(on_disk.len().min(shadow.len()));
let context_start = first_diff.saturating_sub(20);
let context_end = (first_diff + 20).min(on_disk.len()).min(shadow.len());
panic!(
"Content mismatch after op {}! First diff at byte {}.\n\
disk[{}..{}]: {:?}\n\
shadow[{}..{}]: {:?}",
op_idx,
first_diff,
context_start,
context_end,
&on_disk[context_start..context_end],
context_start,
context_end,
&shadow[context_start..context_end],
);
}
println!(" verified after op {}: {} bytes OK", op_idx, shadow.len());
buffer = TextBuffer::load_from_file(&file_path, 0, fs.clone()).unwrap();
assert_eq!(
buffer.total_bytes(),
shadow.len(),
"Reload lost data after op {}: buffer={} shadow={}",
op_idx,
buffer.total_bytes(),
shadow.len(),
);
}
}
buffer.save_to_file(&file_path).unwrap();
let final_content = std::fs::read(&file_path).unwrap();
assert_eq!(
final_content.len(),
shadow.len(),
"Final size mismatch: disk={} shadow={}",
final_content.len(),
shadow.len(),
);
assert_eq!(
final_content,
shadow,
"Final content mismatch (sizes matched at {} bytes)",
shadow.len(),
);
println!(
"Shadow test passed: {} ops, {} save/reload cycles, final size {} bytes",
NUM_OPS,
NUM_OPS / SAVE_EVERY,
shadow.len(),
);
}
#[test]
fn test_regression_1059_streaming_read_backpressure() {
let temp_dir = tempfile::tempdir().unwrap();
let rt = tokio::runtime::Runtime::new().unwrap();
let Some(channel) = rt.block_on(spawn_local_agent_with_capacity(2)).ok() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let fs: Arc<dyn FileSystem + Send + Sync> =
Arc::new(RemoteFileSystem::new(channel, "test@localhost".to_string()));
TEST_RECV_DELAY_US.store(1000, Ordering::SeqCst);
let file_path = temp_dir.path().join("backpressure_test.txt");
const LINE_LEN: usize = 22;
const NUM_LINES: usize = 100_000;
let mut original = Vec::with_capacity(LINE_LEN * NUM_LINES);
for i in 0..NUM_LINES {
let line = format!("Line {:08} content\n", i);
original.extend_from_slice(line.as_bytes());
}
std::fs::write(&file_path, &original).unwrap();
let mut buffer = TextBuffer::load_from_file(&file_path, 0, fs.clone()).unwrap();
assert_eq!(
buffer.total_bytes(),
original.len(),
"Streaming read lost data: loaded {} bytes, expected {} ({} bytes lost)",
buffer.total_bytes(),
original.len(),
original.len().saturating_sub(buffer.total_bytes()),
);
let insert_pos = 50_000 * LINE_LEN;
let insert_data = b"INSERTED LINE\n".to_vec();
buffer.insert_bytes(insert_pos, insert_data.clone());
buffer.save_to_file(&file_path).unwrap();
let saved = std::fs::read(&file_path).unwrap();
let mut expected = original.clone();
expected.splice(insert_pos..insert_pos, insert_data.iter().copied());
assert_eq!(
saved.len(),
expected.len(),
"Save round-trip size mismatch: got {} expected {}",
saved.len(),
expected.len(),
);
assert_eq!(saved, expected, "Save round-trip content mismatch");
TEST_RECV_DELAY_US.store(0, Ordering::SeqCst);
println!("Regression test #1059 passed: streaming read + save correct under backpressure");
}
#[test]
fn test_concurrent_count_lf_requests() {
let temp_dir = tempfile::tempdir().unwrap();
let rt = tokio::runtime::Runtime::new().unwrap();
let Some(channel) = rt.block_on(spawn_local_agent()).ok() else {
eprintln!("Skipping test: could not spawn agent");
return;
};
let fs = Arc::new(RemoteFileSystem::new(channel, "test@localhost".to_string()));
let line_len = 100usize;
let num_lines = 10_000; let mut content = Vec::with_capacity(line_len * num_lines);
for _ in 0..num_lines {
content.extend(std::iter::repeat(b'A').take(line_len - 1));
content.push(b'\n');
}
let file_path = temp_dir.path().join("concurrent_lf.bin");
std::fs::write(&file_path, &content).unwrap();
let num_requests = 64usize;
let chunk_size = content.len() / num_requests;
let results: Vec<std::io::Result<(usize, usize)>> = rt.block_on(async {
let mut handles = Vec::with_capacity(num_requests);
for i in 0..num_requests {
let fs = fs.clone();
let path = file_path.clone();
let offset = (i * chunk_size) as u64;
let len = if i == num_requests - 1 {
content.len() - i * chunk_size } else {
chunk_size
};
handles.push(tokio::task::spawn_blocking(move || {
let count = fs.count_line_feeds_in_range(&path, offset, len)?;
Ok((i, count))
}));
}
let mut results = Vec::with_capacity(handles.len());
for handle in handles {
results.push(handle.await.unwrap());
}
results
});
let mut total_lf = 0usize;
for result in &results {
let (idx, count) = result.as_ref().expect("count_lf request should succeed");
let offset = idx * chunk_size;
let len = if *idx == num_requests - 1 {
content.len() - idx * chunk_size
} else {
chunk_size
};
let expected = content[offset..offset + len]
.iter()
.filter(|&&b| b == b'\n')
.count();
assert_eq!(
*count, expected,
"chunk {}: got {} newlines, expected {} (offset={}, len={})",
idx, count, expected, offset, len
);
total_lf += count;
}
assert_eq!(
total_lf, num_lines,
"total newlines across all chunks should equal num_lines"
);
println!(
"Concurrent count_lf test passed: {} requests, {} total newlines",
num_requests, total_lf
);
}
#[test]
fn test_concurrent_mixed_requests() {
let temp_dir = tempfile::tempdir().unwrap();
let rt = tokio::runtime::Runtime::new().unwrap();
let Some(channel) = rt.block_on(spawn_local_agent()).ok() else {
eprintln!("Skipping test: could not spawn agent");
return;
};
let fs = Arc::new(RemoteFileSystem::new(channel, "test@localhost".to_string()));
let line_len = 80usize;
let num_lines = 5_000;
let mut content = Vec::with_capacity(line_len * num_lines);
for i in 0..num_lines {
let line = format!("{:>079}\n", i); content.extend_from_slice(line.as_bytes());
}
let file_path = temp_dir.path().join("mixed_concurrent.bin");
std::fs::write(&file_path, &content).unwrap();
let num_each = 32usize;
let chunk_size = content.len() / num_each;
enum Expected {
CountLf { idx: usize, expected: usize },
ReadRange { idx: usize, expected: Vec<u8> },
}
let (expectations, results): (Vec<Expected>, Vec<std::io::Result<()>>) = rt.block_on(async {
let mut handles: Vec<tokio::task::JoinHandle<std::io::Result<(usize, Vec<u8>, usize)>>> =
Vec::new();
let mut expectations = Vec::new();
for i in 0..num_each {
let offset = (i * chunk_size) as u64;
let len = chunk_size;
{
let fs = fs.clone();
let path = file_path.clone();
let expected_lf = content[i * chunk_size..(i + 1) * chunk_size]
.iter()
.filter(|&&b| b == b'\n')
.count();
expectations.push(Expected::CountLf {
idx: i,
expected: expected_lf,
});
handles.push(tokio::task::spawn_blocking(move || {
let count = fs.count_line_feeds_in_range(&path, offset, len)?;
Ok((i, Vec::new(), count))
}));
}
{
let fs = fs.clone();
let path = file_path.clone();
let expected_data = content[i * chunk_size..(i + 1) * chunk_size].to_vec();
expectations.push(Expected::ReadRange {
idx: i,
expected: expected_data,
});
handles.push(tokio::task::spawn_blocking(move || {
let data = fs.read_range(&path, offset, len)?;
Ok((i, data, 0))
}));
}
}
let mut results = Vec::new();
let mut handle_results = Vec::new();
for handle in handles {
handle_results.push(handle.await.unwrap());
}
for (exp, result) in expectations.iter().zip(handle_results.iter()) {
match (exp, result) {
(Expected::CountLf { idx, expected }, Ok((_i, _data, count))) => {
if count != expected {
results.push(Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"count_lf chunk {}: got {}, expected {}",
idx, count, expected
),
)));
} else {
results.push(Ok(()));
}
}
(Expected::ReadRange { idx, expected }, Ok((_i, data, _count))) => {
if data != expected {
results.push(Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"read_range chunk {}: got {} bytes, expected {} bytes",
idx,
data.len(),
expected.len()
),
)));
} else {
results.push(Ok(()));
}
}
(_, Err(e)) => {
results.push(Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("request failed: {}", e),
)));
}
}
}
(expectations, results)
});
for (i, result) in results.iter().enumerate() {
result
.as_ref()
.unwrap_or_else(|e| panic!("request {} failed: {}", i, e));
}
println!(
"Mixed concurrent test passed: {} total requests ({} count_lf + {} read_range)",
expectations.len(),
num_each,
num_each
);
}
#[test]
fn test_unique_temp_path_uses_system_temp_dir() {
let Some((fs, _temp_dir, _rt)) = create_test_filesystem() else {
eprintln!("Skipping test: could not create test filesystem");
return;
};
let dest = std::path::Path::new("/some/dir/myfile.txt");
let temp_path = fs.unique_temp_path(dest);
let expected_temp_dir = std::env::temp_dir();
assert!(
temp_path.starts_with(&expected_temp_dir),
"unique_temp_path should use system temp dir ({:?}), got {:?}",
expected_temp_dir,
temp_path,
);
let name = temp_path.file_name().unwrap().to_string_lossy();
assert!(
name.contains("myfile.txt"),
"temp file name should contain the original file name, got {:?}",
name,
);
assert!(
name.ends_with(".tmp"),
"temp file name should end with .tmp, got {:?}",
name,
);
}