use std::path::{Path, PathBuf};
use crate::error::ParseError;
use crate::filter::builder::Filter;
use crate::parser::builder::LogParserBuilder;
use crate::parser::encoding::FileEncodingHint;
use crate::record::Sqllog;
pub struct AsyncLogParser {
path: PathBuf,
encoding_hint: FileEncodingHint,
filter: Option<Filter>,
}
impl AsyncLogParser {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
path: path.as_ref().to_path_buf(),
encoding_hint: FileEncodingHint::Auto,
filter: None,
}
}
pub fn encoding_hint(mut self, hint: FileEncodingHint) -> Self {
self.encoding_hint = hint;
self
}
pub fn with_filter(mut self, filter: Filter) -> Self {
self.filter = Some(filter);
self
}
pub async fn parse(self) -> Result<Vec<Sqllog>, AsyncError> {
let path = self.path;
let encoding_hint = self.encoding_hint;
let filter = self.filter;
tokio::task::spawn_blocking(move || -> Result<Vec<Sqllog>, ParseError> {
let parser = LogParserBuilder::new(&path)
.encoding_hint(encoding_hint)
.build()?;
let iter = parser.iter()?;
let records = match filter {
Some(f) => iter.apply_filter(f).filter_map(Result::ok).collect(),
None => iter.filter_map(Result::ok).collect(),
};
Ok(records)
})
.await
.map_err(|e| AsyncError::Panic(e.to_string()))?
.map_err(AsyncError::Parse)
}
}
#[derive(Debug, thiserror::Error)]
pub enum AsyncError {
#[error("parse error: {0}")]
Parse(#[from] ParseError),
#[error("blocking task panicked: {0}")]
Panic(String),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filter::builder::FilterBuilder;
fn make_record_line(exectime: f32) -> String {
format!(
"2025-11-17 16:09:41.123 (EP[0] sess:1 thrd:2 user:SYSDBA trxid:3 stmt:4 appname:app) \
SELECT 1\nEXECTIME:{exectime}(ms) ROWCOUNT:1(rows) EXEC_ID:1."
)
}
#[cfg(not(miri))]
#[tokio::test]
async fn test_parse_returns_records() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut tmp = NamedTempFile::new().expect("创建临时文件失败");
write!(
tmp,
"2025-11-17 16:09:41.123 (EP[0] sess:1 thrd:2 user:SYSDBA trxid:3 stmt:4 appname:app) SELECT 1\nEXECTIME:0.100(ms) ROWCOUNT:1(rows) EXEC_ID:1."
)
.unwrap();
tmp.as_file().sync_all().unwrap();
let records = AsyncLogParser::new(tmp.path())
.parse()
.await
.expect("parse 不应失败");
assert_eq!(records.len(), 1);
}
#[cfg(not(miri))]
#[tokio::test]
async fn test_parse_file_not_found_returns_error() {
let result = AsyncLogParser::new("/nonexistent/no_such_file.log")
.parse()
.await;
assert!(result.is_err());
assert!(matches!(result, Err(AsyncError::Parse(_))));
}
#[cfg(not(miri))]
#[tokio::test]
async fn test_parse_with_filter() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut tmp = NamedTempFile::new().expect("创建临时文件失败");
write!(
tmp,
"{}\n{}",
make_record_line(50.0),
make_record_line(200.0),
)
.unwrap();
tmp.as_file().sync_all().unwrap();
let filter = FilterBuilder::new().exec_time_gt(100.0).build();
let records = AsyncLogParser::new(tmp.path())
.with_filter(filter)
.parse()
.await
.expect("parse 不应失败");
assert_eq!(records.len(), 1);
assert!(records[0].exectime > 100.0);
}
#[cfg(not(miri))]
#[tokio::test]
async fn test_encoding_hint_propagated() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut tmp = NamedTempFile::new().expect("创建临时文件失败");
write!(
tmp,
"2025-11-17 16:09:41.123 (EP[0] sess:1 thrd:2 user:SYSDBA trxid:3 stmt:4 appname:app) SELECT 1"
)
.unwrap();
tmp.as_file().sync_all().unwrap();
let result = AsyncLogParser::new(tmp.path())
.encoding_hint(FileEncodingHint::Utf8)
.parse()
.await;
assert!(result.is_ok(), "encoding_hint(Utf8) 解析应成功");
let records = result.unwrap();
assert!(!records.is_empty());
}
#[test]
fn test_async_error_is_error() {
let err = AsyncError::Panic("test panic".to_string());
let display = format!("{err}");
assert!(display.contains("test panic"), "Display 应包含 panic 消息");
}
#[test]
fn test_async_error_from_parse_error() {
let parse_err = ParseError::FileNotFound {
path: "test.log".to_string(),
};
let async_err: AsyncError = parse_err.into();
assert!(
matches!(async_err, AsyncError::Parse(_)),
"ParseError 应转换为 AsyncError::Parse"
);
}
#[cfg(not(miri))]
#[tokio::test]
async fn test_parse_panic_becomes_async_error() {
let result: Result<Vec<Sqllog>, AsyncError> = {
let blocking =
tokio::task::spawn_blocking(|| -> Result<Vec<Sqllog>, crate::error::ParseError> {
panic!("intentional test panic");
})
.await;
blocking
.map_err(|e| AsyncError::Panic(e.to_string()))
.and_then(|r| r.map_err(AsyncError::Parse))
};
assert!(matches!(result, Err(AsyncError::Panic(_))));
if let Err(AsyncError::Panic(msg)) = result {
assert!(!msg.is_empty());
}
}
}