use crate::config::Config;
use crate::parser::SqllogParser;
use std::path::Path;
#[must_use]
pub(crate) fn check(cfg: &Config) -> PreflightResult {
let mut result = PreflightResult::default();
for input in &cfg.sqllog.inputs {
check_log_path(input, &mut result);
}
check_output_writable(cfg, &mut result);
result
}
fn check_log_path(path_str: &str, result: &mut PreflightResult) {
let has_glob = path_str.contains('*') || path_str.contains('?') || path_str.contains('[');
if !has_glob {
let path = Path::new(path_str);
if !path.exists() {
result.errors.push(format!(
"日志路径不存在: {path_str} (检查 [sqllog].inputs 或 --input 标志)"
));
return;
}
}
match SqllogParser::new(vec![path_str.to_string()]).log_files() {
Ok(files) if files.is_empty() => {
result
.warnings
.push(format!("路径 {path_str} 中未找到 .log 文件"));
}
Ok(_) => {}
Err(e) => {
result.errors.push(format!("扫描日志路径失败: {e}"));
}
}
}
fn check_output_writable(cfg: &Config, result: &mut PreflightResult) {
if let Some(csv) = &cfg.exporter.csv {
check_path_writable(&csv.file, result);
return;
}
if let Some(sqlite) = &cfg.exporter.sqlite {
check_path_writable(&sqlite.database_url, result);
}
}
fn check_path_writable(file_path: &str, result: &mut PreflightResult) {
let path = Path::new(file_path);
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
if !parent.exists() {
if std::fs::create_dir_all(parent).is_err() {
result
.errors
.push(format!("无法创建输出目录: {}", parent.display()));
}
return;
}
}
if std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(path)
.is_err()
{
result.errors.push(format!("输出文件不可写: {file_path}"));
}
}
#[derive(Debug, Default)]
pub(crate) struct PreflightResult {
pub(crate) errors: Vec<String>,
pub(crate) warnings: Vec<String>,
}
impl PreflightResult {
#[must_use]
pub(crate) fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
#[must_use]
pub(crate) fn print_and_check(&self) -> bool {
for warn in &self.warnings {
eprintln!("Warning: {warn}");
}
for err in &self.errors {
eprintln!("Error: {err}");
}
self.has_errors()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Config, CsvExporterConfig, ExporterConfig, SqllogConfig};
fn config_with_log_dir(dir: &str) -> Config {
Config {
sqllog: SqllogConfig {
inputs: vec![dir.to_string()],
path_deprecated: None,
},
..Default::default()
}
}
#[test]
fn test_preflight_result_no_errors() {
let result = PreflightResult::default();
assert!(!result.has_errors());
assert!(!result.print_and_check());
}
#[test]
fn test_preflight_result_with_errors() {
let mut result = PreflightResult::default();
result.errors.push("some error".to_string());
assert!(result.has_errors());
assert!(result.print_and_check());
}
#[test]
fn test_preflight_result_warnings_no_error() {
let mut result = PreflightResult::default();
result.warnings.push("some warning".to_string());
assert!(!result.has_errors());
assert!(!result.print_and_check());
}
#[test]
fn test_check_nonexistent_log_dir_produces_error() {
let cfg = config_with_log_dir("/this/path/definitely/does/not/exist");
let result = check(&cfg);
assert!(result.has_errors());
assert!(result.errors[0].contains("不存在"));
}
#[test]
fn test_check_single_log_file_is_valid() {
let dir = tempfile::TempDir::new().unwrap();
let file_path = dir.path().join("test.log");
std::fs::write(&file_path, "").unwrap();
let cfg = config_with_log_dir(file_path.to_str().unwrap());
let result = check(&cfg);
assert!(!result.has_errors());
}
#[test]
fn test_check_log_dir_empty_produces_warning() {
let dir = tempfile::TempDir::new().unwrap();
let cfg = config_with_log_dir(dir.path().to_str().unwrap());
let result = check(&cfg);
assert!(!result.has_errors());
assert!(!result.warnings.is_empty());
}
#[test]
fn test_check_log_dir_with_log_files_no_warning() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join("test.log"), "").unwrap();
let cfg = config_with_log_dir(dir.path().to_str().unwrap());
let result = check(&cfg);
assert!(!result.has_errors());
assert!(result.warnings.is_empty());
}
#[test]
fn test_check_glob_pattern_with_matches() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join("a.log"), "").unwrap();
let pattern = format!("{}/*.log", dir.path().display());
let cfg = config_with_log_dir(&pattern);
let result = check(&cfg);
assert!(!result.has_errors());
assert!(result.warnings.is_empty());
}
#[test]
fn test_check_glob_pattern_no_matches_produces_warning() {
let dir = tempfile::TempDir::new().unwrap();
let pattern = format!("{}/nomatch*.log", dir.path().display());
let cfg = config_with_log_dir(&pattern);
let result = check(&cfg);
assert!(!result.has_errors());
assert!(!result.warnings.is_empty());
}
#[test]
fn test_check_csv_output_in_existing_dir() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join("test.log"), "").unwrap();
let out_file = dir.path().join("out.csv");
let mut cfg = config_with_log_dir(dir.path().to_str().unwrap());
cfg.exporter = ExporterConfig {
csv: Some(CsvExporterConfig {
file: out_file.to_str().unwrap().to_string(),
overwrite: false,
append: false,
..CsvExporterConfig::default()
}),
..Default::default()
};
let result = check(&cfg);
assert!(!result.has_errors());
}
#[test]
fn test_check_csv_existing_writable_file() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join("test.log"), "").unwrap();
let out_file = dir.path().join("out.csv");
std::fs::write(&out_file, "").unwrap(); let mut cfg = config_with_log_dir(dir.path().to_str().unwrap());
cfg.exporter = ExporterConfig {
csv: Some(CsvExporterConfig {
file: out_file.to_str().unwrap().to_string(),
overwrite: false,
append: false,
..CsvExporterConfig::default()
}),
..Default::default()
};
let result = check(&cfg);
assert!(!result.has_errors());
}
}