use crate::error::Result;
use std::path::Path;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
use tokio::sync::mpsc;
pub(crate) async fn read_file_content(
file_path: &Path,
last_position: &mut u64,
separator: &str,
tx: &mpsc::UnboundedSender<Result<Vec<String>>>,
) -> Result<()> {
if !file_path.exists() {
return Ok(());
}
let mut file = File::open(file_path).await?;
let metadata = file.metadata().await?;
let current_size = metadata.len();
if detect_file_truncation(current_size, *last_position) {
*last_position = 0;
}
let bytes_to_read = match calculate_bytes_to_read(current_size, *last_position) {
Some(bytes) => bytes,
None => return Ok(()), };
file.seek(std::io::SeekFrom::Start(*last_position)).await?;
let mut new_content = String::new();
file.take(bytes_to_read)
.read_to_string(&mut new_content)
.await?;
*last_position = current_size;
let parts = split_and_filter_content(&new_content, separator);
if !parts.is_empty() {
if tx.send(Ok(parts)).is_err() {
return Ok(());
}
}
Ok(())
}
fn split_and_filter_content(content: &str, separator: &str) -> Vec<String> {
content
.split(separator)
.filter_map(|part| {
let trimmed = part.trim();
if trimmed.is_empty() {
None
} else {
Some(part.to_string())
}
})
.collect()
}
fn detect_file_truncation(current_size: u64, last_position: u64) -> bool {
current_size < last_position
}
fn calculate_bytes_to_read(current_size: u64, last_position: u64) -> Option<u64> {
if current_size <= last_position {
None } else {
Some(current_size - last_position)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use tokio::fs;
use tokio::sync::mpsc;
async fn collect_messages(mut rx: mpsc::UnboundedReceiver<Result<Vec<String>>>) -> Vec<Vec<String>> {
let mut messages = Vec::new();
while let Ok(result) = rx.try_recv() {
match result {
Ok(content) => messages.push(content),
Err(e) => panic!("Unexpected error: {}", e),
}
}
messages
}
#[test]
fn test_split_and_filter_content_newline() {
let content = "line1\nline2\nline3\n";
let result = split_and_filter_content(content, "\n");
assert_eq!(result, vec!["line1", "line2", "line3"]);
}
#[test]
fn test_split_and_filter_content_with_empty_lines() {
let content = "line1\n\n\nline2\n \n\nline3\n";
let result = split_and_filter_content(content, "\n");
assert_eq!(result, vec!["line1", "line2", "line3"]);
}
#[test]
fn test_split_and_filter_content_custom_separator() {
let content = "data1|data2|data3|";
let result = split_and_filter_content(content, "|");
assert_eq!(result, vec!["data1", "data2", "data3"]);
}
#[test]
fn test_split_and_filter_content_multi_char_separator() {
let content = "part1<<>>part2<<>>part3<<>>";
let result = split_and_filter_content(content, "<<>>");
assert_eq!(result, vec!["part1", "part2", "part3"]);
}
#[test]
fn test_split_and_filter_content_no_separator() {
let content = "single_line_content";
let result = split_and_filter_content(content, "\n");
assert_eq!(result, vec!["single_line_content"]);
}
#[test]
fn test_split_and_filter_content_empty_string() {
let content = "";
let result = split_and_filter_content(content, "\n");
assert_eq!(result, Vec::<String>::new());
}
#[test]
fn test_split_and_filter_content_only_separators() {
let content = "\n\n\n";
let result = split_and_filter_content(content, "\n");
assert_eq!(result, Vec::<String>::new());
}
#[test]
fn test_split_and_filter_content_whitespace_preservation() {
let content = " line1 \n line2 \n";
let result = split_and_filter_content(content, "\n");
assert_eq!(result, vec![" line1 ", " line2 "]);
}
#[test]
fn test_detect_file_truncation() {
assert!(detect_file_truncation(100, 200)); assert!(!detect_file_truncation(200, 100)); assert!(!detect_file_truncation(100, 100)); }
#[test]
fn test_calculate_bytes_to_read() {
assert_eq!(calculate_bytes_to_read(200, 100), Some(100)); assert_eq!(calculate_bytes_to_read(100, 100), None); assert_eq!(calculate_bytes_to_read(50, 100), None); assert_eq!(calculate_bytes_to_read(0, 0), None); }
#[tokio::test]
async fn test_read_simple_file_with_newline_separator() {
let file_path = PathBuf::from("fixtures/simple_append.log");
let (tx, rx) = mpsc::unbounded_channel();
let mut position = 0u64;
read_file_content(&file_path, &mut position, "\n", &tx)
.await
.expect("Should read file successfully");
let messages = collect_messages(rx).await;
assert_eq!(messages.len(), 1);
let lines = &messages[0];
let expected = vec![
"2023-01-01 10:00:00 INFO Starting application".to_string(),
"2023-01-01 10:00:01 INFO Loading configuration".to_string(),
"2023-01-01 10:00:02 INFO Database connection established".to_string(),
"2023-01-01 10:00:03 DEBUG User session created for user_id=123".to_string(),
"2023-01-01 10:00:04 INFO Application ready to serve requests".to_string(),
"2023-01-01 10:00:05 WARN High memory usage detected: 85%".to_string(),
"2023-01-01 10:00:06 ERROR Failed to process request: timeout".to_string(),
"2023-01-01 10:00:07 INFO Request processed successfully".to_string(),
"2023-01-01 10:00:08 DEBUG Cache hit for key=user_data_123".to_string(),
"2023-01-01 10:00:09 INFO User authenticated successfully ".to_string(),
];
assert_eq!(lines, &expected);
let metadata = fs::metadata(&file_path).await.unwrap();
assert_eq!(position, metadata.len());
}
#[tokio::test]
async fn test_read_file_with_different_separator() {
let file_path = PathBuf::from("fixtures/different_separators.log");
let (tx, rx) = mpsc::unbounded_channel();
let mut position = 0u64;
read_file_content(&file_path, &mut position, "|", &tx)
.await
.expect("Should read file successfully");
let messages = collect_messages(rx).await;
assert_eq!(messages.len(), 1);
let lines = &messages[0];
assert!(lines.len() > 1);
assert!(lines[0].contains("Starting application"));
assert!(lines[1].contains("Loading configuration"));
let metadata = fs::metadata(&file_path).await.unwrap();
assert_eq!(position, metadata.len());
}
#[tokio::test]
async fn test_incremental_reading() {
let file_path = PathBuf::from("fixtures/simple_append.log");
let (tx, rx) = mpsc::unbounded_channel();
let file = File::open(&file_path).await.unwrap();
let first_chunk_size = 50;
let mut content = String::new();
file.take(first_chunk_size)
.read_to_string(&mut content)
.await
.unwrap();
let mut position = first_chunk_size;
read_file_content(&file_path, &mut position, "\n", &tx)
.await
.expect("Should read remaining content");
let messages = collect_messages(rx).await;
assert_eq!(messages.len(), 1);
let lines = &messages[0];
let expected = vec![
"-01-01 10:00:01 INFO Loading configuration".to_string(),
"2023-01-01 10:00:02 INFO Database connection established".to_string(),
"2023-01-01 10:00:03 DEBUG User session created for user_id=123".to_string(),
"2023-01-01 10:00:04 INFO Application ready to serve requests".to_string(),
"2023-01-01 10:00:05 WARN High memory usage detected: 85%".to_string(),
"2023-01-01 10:00:06 ERROR Failed to process request: timeout".to_string(),
"2023-01-01 10:00:07 INFO Request processed successfully".to_string(),
"2023-01-01 10:00:08 DEBUG Cache hit for key=user_data_123".to_string(),
"2023-01-01 10:00:09 INFO User authenticated successfully ".to_string(),
];
assert_eq!(lines, &expected);
let metadata = fs::metadata(&file_path).await.unwrap();
assert_eq!(position, metadata.len());
}
#[tokio::test]
async fn test_file_truncation_handling() {
let file_path = PathBuf::from("fixtures/simple_append.log");
let (tx, rx) = mpsc::unbounded_channel();
let mut position = 1000u64;
read_file_content(&file_path, &mut position, "\n", &tx)
.await
.expect("Should handle truncation");
let messages = collect_messages(rx).await;
assert!(messages.len() > 0);
let metadata = fs::metadata(&file_path).await.unwrap();
assert_eq!(position, metadata.len());
}
#[tokio::test]
async fn test_nonexistent_file() {
let file_path = PathBuf::from("fixtures/nonexistent.log");
let (tx, _rx) = mpsc::unbounded_channel();
let mut position = 0u64;
let result = read_file_content(&file_path, &mut position, "\n", &tx).await;
assert!(result.is_ok());
assert_eq!(position, 0);
}
#[tokio::test]
async fn test_empty_file() {
let file_path = PathBuf::from("fixtures/empty.log");
let (tx, rx) = mpsc::unbounded_channel();
let mut position = 0u64;
read_file_content(&file_path, &mut position, "\n", &tx)
.await
.expect("Should handle empty file");
let messages = collect_messages(rx).await;
assert_eq!(messages.len(), 0);
let metadata = fs::metadata(&file_path).await.unwrap();
assert_eq!(position, metadata.len());
}
#[tokio::test]
async fn test_no_new_content_when_position_at_end() {
let file_path = PathBuf::from("fixtures/simple_append.log");
let (tx, rx) = mpsc::unbounded_channel();
let metadata = fs::metadata(&file_path).await.unwrap();
let mut position = metadata.len();
read_file_content(&file_path, &mut position, "\n", &tx)
.await
.expect("Should handle no new content");
let messages = collect_messages(rx).await;
assert_eq!(messages.len(), 0);
assert_eq!(position, metadata.len());
}
#[tokio::test]
async fn test_receiver_dropped() {
let file_path = PathBuf::from("fixtures/simple_append.log");
let (tx, rx) = mpsc::unbounded_channel();
let mut position = 0u64;
drop(rx);
let result = read_file_content(&file_path, &mut position, "\n", &tx).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_filters_empty_lines() {
let temp_file = "test_empty_lines.tmp";
fs::write(temp_file, "line1\n\n\nline2\n \n\nline3\n")
.await
.unwrap();
let file_path = PathBuf::from(temp_file);
let (tx, rx) = mpsc::unbounded_channel();
let mut position = 0u64;
read_file_content(&file_path, &mut position, "\n", &tx)
.await
.expect("Should read file successfully");
let messages = collect_messages(rx).await;
assert_eq!(messages.len(), 1);
let lines = &messages[0];
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "line1");
assert_eq!(lines[1], "line2");
assert_eq!(lines[2], "line3");
fs::remove_file(temp_file).await.unwrap();
}
#[tokio::test]
async fn test_utf8_handling_valid_content() {
let temp_file = "test_utf8_valid.tmp";
let utf8_content = "Hello 世界\nUnicode: 🦀\n日本語テスト\n";
fs::write(temp_file, utf8_content).await.unwrap();
let file_path = PathBuf::from(temp_file);
let (tx, rx) = mpsc::unbounded_channel();
let mut position = 0u64;
read_file_content(&file_path, &mut position, "\n", &tx)
.await
.expect("Should read UTF-8 content successfully");
let messages = collect_messages(rx).await;
assert_eq!(messages.len(), 1);
let lines = &messages[0];
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "Hello 世界");
assert_eq!(lines[1], "Unicode: 🦀");
assert_eq!(lines[2], "日本語テスト");
fs::remove_file(temp_file).await.unwrap();
}
#[tokio::test]
async fn test_large_file_reading() {
let temp_file = "test_large_file.tmp";
let mut large_content = String::new();
for i in 0..1000 {
large_content.push_str(&format!("Line number {}\n", i));
}
fs::write(temp_file, &large_content).await.unwrap();
let file_path = PathBuf::from(temp_file);
let (tx, rx) = mpsc::unbounded_channel();
let mut position = 0u64;
read_file_content(&file_path, &mut position, "\n", &tx)
.await
.expect("Should read large file successfully");
let messages = collect_messages(rx).await;
assert_eq!(messages.len(), 1);
let lines = &messages[0];
assert_eq!(lines.len(), 1000);
assert_eq!(lines[0], "Line number 0");
assert_eq!(lines[999], "Line number 999");
fs::remove_file(temp_file).await.unwrap();
}
#[tokio::test]
async fn test_file_with_very_long_lines() {
let temp_file = "test_long_lines.tmp";
let long_line = "A".repeat(10000);
let content = format!("{}\n{}\n", long_line, "short line");
fs::write(temp_file, &content).await.unwrap();
let file_path = PathBuf::from(temp_file);
let (tx, rx) = mpsc::unbounded_channel();
let mut position = 0u64;
read_file_content(&file_path, &mut position, "\n", &tx)
.await
.expect("Should read file with long lines successfully");
let messages = collect_messages(rx).await;
assert_eq!(messages.len(), 1);
let lines = &messages[0];
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].len(), 10000);
assert!(lines[0].chars().all(|c| c == 'A'));
assert_eq!(lines[1], "short line");
fs::remove_file(temp_file).await.unwrap();
}
#[tokio::test]
async fn test_binary_like_content_handling() {
let temp_file = "test_binary_content.tmp";
let content = "line1\nline with \0 null byte\nline3\n";
fs::write(temp_file, content).await.unwrap();
let file_path = PathBuf::from(temp_file);
let (tx, rx) = mpsc::unbounded_channel();
let mut position = 0u64;
read_file_content(&file_path, &mut position, "\n", &tx)
.await
.expect("Should read binary-like content successfully");
let messages = collect_messages(rx).await;
assert_eq!(messages.len(), 1);
let lines = &messages[0];
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "line1");
assert!(lines[1].contains("null byte"));
assert_eq!(lines[2], "line3");
fs::remove_file(temp_file).await.unwrap();
}
#[tokio::test]
async fn test_concurrent_reading_attempts() {
let file_path = PathBuf::from("fixtures/simple_append.log");
let mut handles = Vec::new();
for i in 0..5 {
let path = file_path.clone();
let handle = tokio::spawn(async move {
let (tx, rx) = mpsc::unbounded_channel();
let mut position = 0u64;
read_file_content(&path, &mut position, "\n", &tx)
.await
.expect("Should read file successfully");
let messages = collect_messages(rx).await;
(i, messages.len())
});
handles.push(handle);
}
let results: Vec<_> = futures::future::join_all(handles).await;
for result in results {
let (task_id, message_count) = result.unwrap();
assert!(
message_count > 0,
"Task {} should have read some messages",
task_id
);
}
}
#[test]
fn test_split_and_filter_content_edge_cases() {
let content = "\nline1\nline2";
let result = split_and_filter_content(content, "\n");
assert_eq!(result, vec!["line1", "line2"]);
let content = "line1\nline2\n";
let result = split_and_filter_content(content, "\n");
assert_eq!(result, vec!["line1", "line2"]);
let content = "line1\n\n\n\nline2";
let result = split_and_filter_content(content, "\n");
assert_eq!(result, vec!["line1", "line2"]);
let content = "line1\n \n\t\n \nline2";
let result = split_and_filter_content(content, "\n");
assert_eq!(result, vec!["line1", "line2"]);
}
#[test]
fn test_position_calculation_edge_cases() {
assert_eq!(calculate_bytes_to_read(0, 0), None);
assert_eq!(calculate_bytes_to_read(u64::MAX, u64::MAX - 1), Some(1));
assert_eq!(calculate_bytes_to_read(u64::MAX - 1, u64::MAX), None);
assert_eq!(calculate_bytes_to_read(1, 0), Some(1));
assert_eq!(calculate_bytes_to_read(0, 1), None);
}
#[test]
fn test_file_truncation_edge_cases() {
assert!(!detect_file_truncation(100, 100));
assert!(!detect_file_truncation(0, 0));
assert!(detect_file_truncation(0, 1));
assert!(detect_file_truncation(u64::MAX - 1, u64::MAX));
assert!(!detect_file_truncation(u64::MAX, u64::MAX - 1));
}
}