|
|
@@ -0,0 +1,594 @@
|
|
|
+use anyhow::Result;
|
|
|
+use chrono::{DateTime, Local};
|
|
|
+use std::{
|
|
|
+ collections::HashSet,
|
|
|
+ fs::{self, File},
|
|
|
+ io::{BufRead, BufReader, Write},
|
|
|
+ path::{Path, PathBuf},
|
|
|
+ process::Command,
|
|
|
+ sync::{
|
|
|
+ atomic::{AtomicUsize, Ordering},
|
|
|
+ Arc,
|
|
|
+ },
|
|
|
+ time::Instant,
|
|
|
+};
|
|
|
+
|
|
|
+/// 测试统计信息
|
|
|
+#[derive(Debug, Default)]
|
|
|
+pub struct TestStats {
|
|
|
+ total: AtomicUsize,
|
|
|
+ passed: AtomicUsize,
|
|
|
+ failed: AtomicUsize,
|
|
|
+ skipped: AtomicUsize,
|
|
|
+}
|
|
|
+
|
|
|
+impl TestStats {
|
|
|
+ pub fn increment_total(&self) {
|
|
|
+ self.total.fetch_add(1, Ordering::Relaxed);
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn increment_passed(&self) {
|
|
|
+ self.passed.fetch_add(1, Ordering::Relaxed);
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn increment_failed(&self) {
|
|
|
+ self.failed.fetch_add(1, Ordering::Relaxed);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[allow(dead_code)]
|
|
|
+ pub fn increment_skipped(&self) {
|
|
|
+ self.skipped.fetch_add(1, Ordering::Relaxed);
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn get_totals(&self) -> (usize, usize, usize, usize) {
|
|
|
+ (
|
|
|
+ self.total.load(Ordering::Relaxed),
|
|
|
+ self.passed.load(Ordering::Relaxed),
|
|
|
+ self.failed.load(Ordering::Relaxed),
|
|
|
+ self.skipped.load(Ordering::Relaxed),
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// 测试运行器配置
|
|
|
+#[derive(Debug, Clone)]
|
|
|
+pub struct Config {
|
|
|
+ pub verbose: bool,
|
|
|
+ pub timeout: u64,
|
|
|
+ pub parallel: usize,
|
|
|
+ pub use_blocklist: bool,
|
|
|
+ pub use_whitelist: bool,
|
|
|
+ pub whitelist_file: PathBuf,
|
|
|
+ pub tests_dir: PathBuf,
|
|
|
+ pub blocklists_dir: PathBuf,
|
|
|
+ pub results_dir: PathBuf,
|
|
|
+ pub temp_dir: PathBuf,
|
|
|
+ pub extra_blocklist_dirs: Vec<PathBuf>,
|
|
|
+ pub test_patterns: Vec<String>,
|
|
|
+}
|
|
|
+
|
|
|
+impl Default for Config {
|
|
|
+ fn default() -> Self {
|
|
|
+ let script_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
|
|
+ Self {
|
|
|
+ verbose: false,
|
|
|
+ timeout: 300,
|
|
|
+ parallel: 1,
|
|
|
+ use_blocklist: true,
|
|
|
+ use_whitelist: true,
|
|
|
+ whitelist_file: script_dir.join("whitelist.txt"),
|
|
|
+ tests_dir: script_dir.join("tests"),
|
|
|
+ blocklists_dir: script_dir.join("blocklists"),
|
|
|
+ results_dir: script_dir.join("results"),
|
|
|
+ temp_dir: PathBuf::from(
|
|
|
+ std::env::var("SYSCALL_TEST_WORKDIR")
|
|
|
+ .unwrap_or_else(|_| "/tmp/gvisor_tests".to_string()),
|
|
|
+ ),
|
|
|
+ extra_blocklist_dirs: Vec::new(),
|
|
|
+ test_patterns: Vec::new(),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// 颜色输出辅助函数(简化版)
|
|
|
+pub fn print_colored(color: &str, prefix: &str, msg: &str) {
|
|
|
+ match color {
|
|
|
+ "green" => println!("\x1b[32m[{}]\x1b[0m {}", prefix, msg),
|
|
|
+ "yellow" => println!("\x1b[33m[{}]\x1b[0m {}", prefix, msg),
|
|
|
+ "red" => eprintln!("\x1b[31m[{}]\x1b[0m {}", prefix, msg),
|
|
|
+ "blue" => println!("\x1b[34m[{}]\x1b[0m {}", prefix, msg),
|
|
|
+ _ => println!("[{}] {}", prefix, msg),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// gvisor系统调用测试运行器
|
|
|
+pub struct TestRunner {
|
|
|
+ pub config: Config,
|
|
|
+ pub stats: Arc<TestStats>,
|
|
|
+}
|
|
|
+
|
|
|
+impl TestRunner {
|
|
|
+ pub fn new(config: Config) -> Self {
|
|
|
+ Self {
|
|
|
+ config,
|
|
|
+ stats: Arc::new(TestStats::default()),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 打印信息日志
|
|
|
+ fn print_info(&self, msg: &str) {
|
|
|
+ print_colored("green", "INFO", msg);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 打印警告日志
|
|
|
+ fn print_warn(&self, msg: &str) {
|
|
|
+ print_colored("yellow", "WARN", msg);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 打印错误日志
|
|
|
+ fn print_error(&self, msg: &str) {
|
|
|
+ print_colored("red", "ERROR", msg);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 打印测试日志
|
|
|
+ fn print_test(&self, msg: &str) {
|
|
|
+ print_colored("blue", "TEST", msg);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 检查测试套件是否存在
|
|
|
+ pub fn check_test_suite(&self) -> Result<()> {
|
|
|
+ if !self.config.tests_dir.exists() {
|
|
|
+ anyhow::bail!(
|
|
|
+ "测试目录不存在: {:?}\n请先运行 ./download_tests.sh 下载测试套件",
|
|
|
+ self.config.tests_dir
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ let test_files: Vec<_> = fs::read_dir(&self.config.tests_dir)?
|
|
|
+ .filter_map(|entry| entry.ok())
|
|
|
+ .filter(|entry| {
|
|
|
+ entry.path().is_file() && entry.file_name().to_string_lossy().ends_with("_test")
|
|
|
+ })
|
|
|
+ .collect();
|
|
|
+
|
|
|
+ if test_files.is_empty() {
|
|
|
+ anyhow::bail!("测试套件未找到\n请先运行 ./download_tests.sh 下载测试套件");
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 创建必要的目录
|
|
|
+ pub fn setup_directories(&self) -> Result<()> {
|
|
|
+ fs::create_dir_all(&self.config.results_dir)?;
|
|
|
+ fs::create_dir_all(&self.config.temp_dir)?;
|
|
|
+ fs::create_dir_all(&self.config.blocklists_dir)?;
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 读取白名单中的测试程序
|
|
|
+ fn get_whitelist_tests(&self) -> Result<HashSet<String>> {
|
|
|
+ if !self.config.whitelist_file.exists() {
|
|
|
+ anyhow::bail!("白名单文件不存在: {:?}", self.config.whitelist_file);
|
|
|
+ }
|
|
|
+
|
|
|
+ let file = File::open(&self.config.whitelist_file)?;
|
|
|
+ let reader = BufReader::new(file);
|
|
|
+ let mut tests = HashSet::new();
|
|
|
+
|
|
|
+ for line in reader.lines() {
|
|
|
+ let line = line?;
|
|
|
+ let line = line.trim();
|
|
|
+ if !line.is_empty() && !line.starts_with('#') {
|
|
|
+ tests.insert(line.to_string());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(tests)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 检查测试是否在白名单中
|
|
|
+ fn is_test_whitelisted(&self, test_name: &str) -> bool {
|
|
|
+ if !self.config.use_whitelist {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ match self.get_whitelist_tests() {
|
|
|
+ Ok(whitelist) => whitelist.contains(test_name),
|
|
|
+ Err(_) => false,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 获取测试的blocklist
|
|
|
+ fn get_test_blocklist(&self, test_name: &str) -> Vec<String> {
|
|
|
+ if !self.config.use_blocklist {
|
|
|
+ return Vec::new();
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut blocked_subtests = Vec::new();
|
|
|
+
|
|
|
+ // 检查主blocklist目录
|
|
|
+ let blocklist_file = self.config.blocklists_dir.join(test_name);
|
|
|
+ if let Ok(content) = self.read_blocklist_file(&blocklist_file) {
|
|
|
+ blocked_subtests.extend(content);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查额外的blocklist目录
|
|
|
+ for extra_dir in &self.config.extra_blocklist_dirs {
|
|
|
+ let extra_blocklist = extra_dir.join(test_name);
|
|
|
+ if let Ok(content) = self.read_blocklist_file(&extra_blocklist) {
|
|
|
+ blocked_subtests.extend(content);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ blocked_subtests
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 读取blocklist文件
|
|
|
+ fn read_blocklist_file(&self, path: &Path) -> Result<Vec<String>> {
|
|
|
+ if !path.exists() {
|
|
|
+ return Ok(Vec::new());
|
|
|
+ }
|
|
|
+
|
|
|
+ let file = File::open(path)?;
|
|
|
+ let reader = BufReader::new(file);
|
|
|
+ let mut blocked = Vec::new();
|
|
|
+
|
|
|
+ for line in reader.lines() {
|
|
|
+ let line = line?;
|
|
|
+ let line = line.trim();
|
|
|
+ if !line.is_empty() && !line.starts_with('#') {
|
|
|
+ blocked.push(line.to_string());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(blocked)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 获取要运行的测试列表
|
|
|
+ pub fn get_test_list(&self) -> Result<Vec<String>> {
|
|
|
+ // 获取所有测试文件
|
|
|
+ let mut all_tests = Vec::new();
|
|
|
+ for entry in fs::read_dir(&self.config.tests_dir)? {
|
|
|
+ let entry = entry?;
|
|
|
+ if entry.path().is_file() {
|
|
|
+ let file_name = entry.file_name();
|
|
|
+ let file_name_str = file_name.to_string_lossy();
|
|
|
+ if file_name_str.ends_with("_test") {
|
|
|
+ all_tests.push(file_name_str.to_string());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 应用白名单过滤
|
|
|
+ let mut candidate_tests = Vec::new();
|
|
|
+ if self.config.use_whitelist {
|
|
|
+ for test in &all_tests {
|
|
|
+ if self.is_test_whitelisted(test) {
|
|
|
+ candidate_tests.push(test.clone());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if candidate_tests.is_empty() {
|
|
|
+ self.print_warn("没有测试通过白名单过滤");
|
|
|
+ return Ok(Vec::new());
|
|
|
+ }
|
|
|
+
|
|
|
+ if self.config.verbose {
|
|
|
+ self.print_info(&format!(
|
|
|
+ "白名单过滤后有 {} 个测试可用",
|
|
|
+ candidate_tests.len()
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ candidate_tests = all_tests;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有指定模式,返回候选测试
|
|
|
+ if self.config.test_patterns.is_empty() {
|
|
|
+ candidate_tests.sort();
|
|
|
+ return Ok(candidate_tests);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据模式过滤测试
|
|
|
+ let mut filtered_tests = HashSet::new();
|
|
|
+ for pattern in &self.config.test_patterns {
|
|
|
+ for test in &candidate_tests {
|
|
|
+ if test == pattern {
|
|
|
+ filtered_tests.insert(test.clone());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut result: Vec<_> = filtered_tests.into_iter().collect();
|
|
|
+ result.sort();
|
|
|
+ Ok(result)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 运行单个测试
|
|
|
+ pub fn run_single_test(&self, test_name: &str) -> Result<bool> {
|
|
|
+ println!("[DEBUG] 开始运行测试: {}", test_name);
|
|
|
+ let test_path = self.config.tests_dir.join(test_name);
|
|
|
+ println!("[DEBUG] 测试路径: {:?}", test_path);
|
|
|
+
|
|
|
+ if !test_path.exists() || !test_path.is_file() {
|
|
|
+ self.print_warn(&format!("测试不存在或不可执行: {}", test_name));
|
|
|
+ return Ok(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ self.print_test(&format!("运行测试用例: {}", test_name));
|
|
|
+
|
|
|
+ // 获取blocklist
|
|
|
+ let blocked_subtests = self.get_test_blocklist(test_name);
|
|
|
+
|
|
|
+ // 结果输出文件(使用绝对路径,避免工作目录切换影响)
|
|
|
+ let output_file = self
|
|
|
+ .config
|
|
|
+ .results_dir
|
|
|
+ .join(format!("{}.output", test_name));
|
|
|
+
|
|
|
+ println!("[DEBUG] 工作目录: {:?}", self.config.tests_dir);
|
|
|
+ println!("[DEBUG] TEST_TMPDIR: {:?}", self.config.temp_dir);
|
|
|
+ println!("[DEBUG] 直接执行: {:?}", test_path);
|
|
|
+ if !blocked_subtests.is_empty() {
|
|
|
+ println!("[DEBUG] gtest_filter: -{}", blocked_subtests.join(":"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保结果目录存在
|
|
|
+ if let Err(e) = fs::create_dir_all(&self.config.results_dir) {
|
|
|
+ self.print_error(&format!("创建结果目录失败: {}", e));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 打开输出文件,并将 stdout/stderr 重定向到该文件
|
|
|
+ let out = File::create(&output_file);
|
|
|
+ if let Err(e) = out.as_ref() {
|
|
|
+ self.print_error(&format!("创建输出文件失败: {:?}, 错误: {}", output_file, e));
|
|
|
+ }
|
|
|
+ let out = out?;
|
|
|
+ let err = out.try_clone()?;
|
|
|
+
|
|
|
+ // 构造并执行命令(不使用 shell,不捕获输出,不创建管道)
|
|
|
+ let start_time = Instant::now();
|
|
|
+ let mut cmd = Command::new(&test_path);
|
|
|
+ if !blocked_subtests.is_empty() {
|
|
|
+ cmd.arg("--gtest_filter")
|
|
|
+ .arg(format!("-{}", blocked_subtests.join(":")));
|
|
|
+ }
|
|
|
+ let status = cmd
|
|
|
+ .current_dir(&self.config.tests_dir)
|
|
|
+ .env("TEST_TMPDIR", &self.config.temp_dir)
|
|
|
+ .stdout(std::process::Stdio::from(out))
|
|
|
+ .stderr(std::process::Stdio::from(err))
|
|
|
+ .status();
|
|
|
+
|
|
|
+ // 清理临时目录
|
|
|
+ let _ = fs::remove_dir_all(&self.config.temp_dir);
|
|
|
+ let _ = fs::create_dir_all(&self.config.temp_dir);
|
|
|
+
|
|
|
+ let duration = start_time.elapsed();
|
|
|
+ match status {
|
|
|
+ Ok(s) if s.success() => {
|
|
|
+ self.print_info(&format!(
|
|
|
+ "✓ {} 通过 ({:.2}s)",
|
|
|
+ test_name,
|
|
|
+ duration.as_secs_f64()
|
|
|
+ ));
|
|
|
+ // 将输出文件尾部打印一点,便于快速查看
|
|
|
+ if let Ok(content) = fs::read_to_string(&output_file) {
|
|
|
+ let tail: String = content
|
|
|
+ .lines()
|
|
|
+ .rev()
|
|
|
+ .take(10)
|
|
|
+ .collect::<Vec<_>>()
|
|
|
+ .into_iter()
|
|
|
+ .rev()
|
|
|
+ .map(|s| format!("{}\n", s))
|
|
|
+ .collect();
|
|
|
+ if !tail.is_empty() {
|
|
|
+ println!("[DEBUG] 输出尾部: \n{}", tail);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Ok(true)
|
|
|
+ }
|
|
|
+ Ok(s) => {
|
|
|
+ self.print_error(&format!(
|
|
|
+ "✗ {} 失败 ({:.2}s), 退出码: {:?}",
|
|
|
+ test_name,
|
|
|
+ duration.as_secs_f64(),
|
|
|
+ s.code()
|
|
|
+ ));
|
|
|
+ // 打印错误输出尾部
|
|
|
+ if let Ok(content) = fs::read_to_string(&output_file) {
|
|
|
+ let tail: String = content
|
|
|
+ .lines()
|
|
|
+ .rev()
|
|
|
+ .take(20)
|
|
|
+ .collect::<Vec<_>>()
|
|
|
+ .into_iter()
|
|
|
+ .rev()
|
|
|
+ .map(|s| format!("{}\n", s))
|
|
|
+ .collect();
|
|
|
+ if !tail.is_empty() {
|
|
|
+ println!("[DEBUG] 错误输出尾部: \n{}", tail);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Ok(false)
|
|
|
+ }
|
|
|
+ Err(e) => {
|
|
|
+ self.print_error(&format!("✗ {} 执行错误: {}", test_name, e));
|
|
|
+ Ok(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 运行所有测试
|
|
|
+ pub fn run_all_tests(&self) -> Result<()> {
|
|
|
+ let test_list = self.get_test_list()?;
|
|
|
+
|
|
|
+ if test_list.is_empty() {
|
|
|
+ self.print_warn("没有找到匹配的测试用例");
|
|
|
+ return Ok(());
|
|
|
+ }
|
|
|
+
|
|
|
+ self.print_info(&format!("准备运行 {} 个测试用例", test_list.len()));
|
|
|
+
|
|
|
+ // 初始化结果文件
|
|
|
+ let failed_cases_file = self.config.results_dir.join("failed_cases.txt");
|
|
|
+ let mut failed_cases = File::create(&failed_cases_file)?;
|
|
|
+
|
|
|
+ // 运行测试
|
|
|
+ for test_name in test_list {
|
|
|
+ self.stats.increment_total();
|
|
|
+
|
|
|
+ match self.run_single_test(&test_name) {
|
|
|
+ Ok(true) => {
|
|
|
+ self.stats.increment_passed();
|
|
|
+ }
|
|
|
+ Ok(false) => {
|
|
|
+ self.stats.increment_failed();
|
|
|
+ writeln!(failed_cases, "{}", test_name)?;
|
|
|
+ }
|
|
|
+ Err(e) => {
|
|
|
+ self.stats.increment_failed();
|
|
|
+ writeln!(failed_cases, "{}", test_name)?;
|
|
|
+ self.print_error(&format!("测试 {} 出错: {}", test_name, e));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ println!("---");
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 生成测试报告
|
|
|
+ pub fn generate_report(&self) -> Result<()> {
|
|
|
+ let report_file = self.config.results_dir.join("test_report.txt");
|
|
|
+ let mut file = File::create(&report_file)?;
|
|
|
+ let (total, passed, failed, _skipped) = self.stats.get_totals();
|
|
|
+
|
|
|
+ let now: DateTime<Local> = Local::now();
|
|
|
+ let success_rate = if total > 0 {
|
|
|
+ passed as f64 * 100.0 / total as f64
|
|
|
+ } else {
|
|
|
+ 0.0
|
|
|
+ };
|
|
|
+
|
|
|
+ let report = format!(
|
|
|
+ "gvisor系统调用测试报告\n\
|
|
|
+ ==========================\n\
|
|
|
+ 测试时间: {}\n\
|
|
|
+ 测试目录: {:?}\n\
|
|
|
+ \n\
|
|
|
+ 测试统计:\n\
|
|
|
+ 总测试数: {}\n\
|
|
|
+ 通过: {}\n\
|
|
|
+ 失败: {}\n\
|
|
|
+ 成功率: {:.2}%\n\
|
|
|
+ \n",
|
|
|
+ now.format("%Y-%m-%d %H:%M:%S"),
|
|
|
+ self.config.tests_dir,
|
|
|
+ total,
|
|
|
+ passed,
|
|
|
+ failed,
|
|
|
+ success_rate
|
|
|
+ );
|
|
|
+
|
|
|
+ file.write_all(report.as_bytes())?;
|
|
|
+ println!("{}", report);
|
|
|
+
|
|
|
+ if failed > 0 {
|
|
|
+ let failed_cases_file = self.config.results_dir.join("failed_cases.txt");
|
|
|
+ if failed_cases_file.exists() {
|
|
|
+ let failed_content = fs::read_to_string(&failed_cases_file)?;
|
|
|
+ let failed_section = format!("失败的测试用例:\n{}", failed_content);
|
|
|
+ file.write_all(failed_section.as_bytes())?;
|
|
|
+ println!("{}", failed_section);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 显示测试结果
|
|
|
+ pub fn show_results(&self) {
|
|
|
+ let (total, passed, failed, _skipped) = self.stats.get_totals();
|
|
|
+
|
|
|
+ log::info!("");
|
|
|
+ log::info!("===============================================");
|
|
|
+ self.print_info("测试完成");
|
|
|
+ log::info!(
|
|
|
+ "\x1b[32m{}\x1b[0m / \x1b[32m{}\x1b[0m 测试用例通过",
|
|
|
+ passed,
|
|
|
+ total
|
|
|
+ );
|
|
|
+
|
|
|
+ if failed > 0 {
|
|
|
+ log::info!("\x1b[31m{}\x1b[0m 个测试用例失败:", failed);
|
|
|
+ let failed_cases_file = self.config.results_dir.join("failed_cases.txt");
|
|
|
+ if let Ok(content) = fs::read_to_string(&failed_cases_file) {
|
|
|
+ for line in content.lines() {
|
|
|
+ if !line.trim().is_empty() {
|
|
|
+ log::info!(" \x1b[31m[X]\x1b[0m {}", line);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ log::info!("");
|
|
|
+ log::info!(
|
|
|
+ "详细报告保存在: {:?}",
|
|
|
+ self.config.results_dir.join("test_report.txt")
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 列出所有测试用例
|
|
|
+ pub fn list_tests(&self) -> Result<()> {
|
|
|
+ if !self.config.tests_dir.exists() {
|
|
|
+ self.print_error(&format!("测试目录不存在: {:?}", self.config.tests_dir));
|
|
|
+ self.print_info("请先运行 ./download_tests.sh 下载测试套件");
|
|
|
+ return Ok(());
|
|
|
+ }
|
|
|
+
|
|
|
+ if self.config.use_whitelist {
|
|
|
+ self.print_info(&format!(
|
|
|
+ "白名单模式 - 可运行的测试用例 (来自: {:?}):",
|
|
|
+ self.config.whitelist_file
|
|
|
+ ));
|
|
|
+ let test_list = self.get_test_list()?;
|
|
|
+ for test_name in &test_list {
|
|
|
+ log::info!(" \x1b[32m✓\x1b[0m {}", test_name);
|
|
|
+ }
|
|
|
+
|
|
|
+ self.print_info("所有可用测试用例 (包括未在白名单中的):");
|
|
|
+ for entry in fs::read_dir(&self.config.tests_dir)? {
|
|
|
+ let entry = entry?;
|
|
|
+ if entry.path().is_file() {
|
|
|
+ let file_name = entry.file_name();
|
|
|
+ let file_name_str = file_name.to_string_lossy();
|
|
|
+ if file_name_str.ends_with("_test") {
|
|
|
+ if self.is_test_whitelisted(&file_name_str) {
|
|
|
+ log::info!(" \x1b[32m✓\x1b[0m {} (在白名单中)", file_name_str);
|
|
|
+ } else {
|
|
|
+ log::info!(" \x1b[33m○\x1b[0m {} (不在白名单中)", file_name_str);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ self.print_info("所有可用的测试用例:");
|
|
|
+ for entry in fs::read_dir(&self.config.tests_dir)? {
|
|
|
+ let entry = entry?;
|
|
|
+ if entry.path().is_file() {
|
|
|
+ let file_name = entry.file_name();
|
|
|
+ let file_name_str = file_name.to_string_lossy();
|
|
|
+ if file_name_str.ends_with("_test") {
|
|
|
+ log::info!(" {}", file_name_str);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+}
|