1#![allow(unused)]
5use std::collections::HashMap;
6use std::fmt;
7use std::fs;
8use std::io::{Read, Write};
9use std::path::PathBuf;
10use std::process::{Command, Output, Stdio};
11use std::sync::Arc;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::thread;
14use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
15
16use crate::rand;
17
18#[derive(Debug, Clone, PartialEq)]
24pub enum SystemError {
25 None,
27 Timeout(Duration),
29 CompileFailed,
31 ProcessSpawnFailed(String),
33}
34
35#[derive(Debug, Clone)]
51pub struct DnyResult {
52 pub stdout: String,
54 pub stderr: String,
56 pub exit_code: i32,
58 pub build_duration: Duration,
60 pub run_duration: Duration,
62 pub ok: bool,
64 pub system_err: SystemError,
66}
67
68impl Default for DnyResult {
69 fn default() -> Self {
70 Self {
71 stdout: String::new(),
72 stderr: String::new(),
73 exit_code: -1,
74 build_duration: Duration::ZERO,
75 run_duration: Duration::ZERO,
76 ok: false,
77 system_err: SystemError::None,
78 }
79 }
80}
81
82fn truncate_output(text: &str, max_lines: usize) -> String {
83 const MAX_LINE_CHARS: usize = 300;
84
85 let text = text.trim();
86 if text.is_empty() {
87 return String::new();
88 }
89
90 let truncate_line = |line: &str| -> String {
91 if line.len() <= MAX_LINE_CHARS {
92 return line.to_string();
93 }
94
95 let mut chars = line.chars();
96 let head: String = chars.by_ref().take(MAX_LINE_CHARS / 2).collect();
97
98 if chars.next().is_none() {
99 return head;
100 }
101
102 let tail: String = line
103 .chars()
104 .rev()
105 .take(MAX_LINE_CHARS / 2)
106 .collect::<Vec<char>>()
107 .into_iter()
108 .rev()
109 .collect();
110
111 format!("{} ... [单行超长截断] ... {}", head, tail)
112 };
113
114 let lines: Vec<&str> = text.lines().collect();
115
116 if lines.len() <= max_lines {
117 return lines
118 .into_iter()
119 .map(truncate_line)
120 .collect::<Vec<_>>()
121 .join("\n");
122 }
123
124 let half = max_lines / 2;
125 let head = lines[..half]
126 .iter()
127 .copied()
128 .map(truncate_line)
129 .collect::<Vec<_>>()
130 .join("\n");
131 let tail = lines[lines.len() - half..]
132 .iter()
133 .copied()
134 .map(truncate_line)
135 .collect::<Vec<_>>()
136 .join("\n");
137
138 format!(
139 "{}\n\n... [已截断 {} 行] ...\n\n{}",
140 head,
141 lines.len() - max_lines,
142 tail
143 )
144}
145
146impl fmt::Display for DnyResult {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 let green = "\x1b[32m";
149 let red = "\x1b[31m";
150 let yellow = "\x1b[33m";
151 let cyan = "\x1b[36m";
152 let reset = "\x1b[0m";
153
154 let (status_color, status_text) = match &self.system_err {
155 SystemError::None if self.ok => (green, "SUCCESS"),
156 SystemError::None => (red, "FAILED (Runtime Error)"),
157 SystemError::Timeout(_) => (yellow, "TIMEOUT (TLE)"),
158 SystemError::CompileFailed => (red, "COMPILE ERROR (CE)"),
159 SystemError::ProcessSpawnFailed(_) => (red, "SYSTEM ERROR"),
160 };
161
162 writeln!(f, "{status_color}=== [Result: {}] ==={reset}", status_text)?;
163 writeln!(
164 f,
165 "> Build: {cyan}{:?}{reset} | Run: {cyan}{:?}{reset}",
166 self.build_duration, self.run_duration
167 )?;
168 writeln!(f, "> Exit Code: {status_color}{}{reset}", self.exit_code)?;
169
170 let stdout_trimmed = self.stdout.trim();
171 if !stdout_trimmed.is_empty() {
172 writeln!(
173 f,
174 "{}--- Stdout (size: {} bytes) ---{}",
175 yellow,
176 self.stdout.len(),
177 reset
178 )?;
179 writeln!(f, "{}", truncate_output(stdout_trimmed, 20))?;
180 }
181
182 let clean_stderr = self
183 .stderr
184 .lines()
185 .filter(|l| !l.trim().starts_with("Compiling "))
186 .filter(|l| !l.trim().starts_with("Finished "))
187 .collect::<Vec<_>>()
188 .join("\n");
189
190 let stderr_trimmed = clean_stderr.trim();
191 if !stderr_trimmed.is_empty() {
192 writeln!(
193 f,
194 "{}--- Stderr (size: {} bytes) ---{}",
195 red,
196 clean_stderr.len(),
197 reset
198 )?;
199 writeln!(f, "{}", truncate_output(stderr_trimmed, 40))?;
200 }
201
202 writeln!(f, "{status_color}=========================={reset}")?;
203 Ok(())
204 }
205}
206
207#[derive(Clone)]
225pub struct DnyTask {
226 pub tag: String,
228 pub main_code: String,
230 pub deps: String,
232 pub envs: HashMap<String, String>,
234 pub cargo_config: Option<String>,
236 pub is_release: bool,
238}
239
240impl DnyTask {
241 pub fn new(tag: impl Into<String>, code: impl Into<String>) -> Self {
256 Self {
257 tag: tag.into(),
258 main_code: code.into(),
259 deps: String::new(),
260 envs: HashMap::new(),
261 cargo_config: None,
262 is_release: false,
263 }
264 }
265 pub fn with_deps(mut self, deps: impl Into<String>) -> Self {
271 self.deps = deps.into();
272 self
273 }
274 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
280 self.envs.insert(key.into(), value.into());
281 self
282 }
283 pub fn with_envs<I, K, V>(mut self, envs: I) -> Self
289 where
290 I: IntoIterator<Item = (K, V)>,
291 K: Into<String>,
292 V: Into<String>,
293 {
294 for (k, v) in envs {
295 self.envs.insert(k.into(), v.into());
296 }
297 self
298 }
299 pub fn with_cargo_config(mut self, config: impl Into<String>) -> Self {
305 self.cargo_config = Some(config.into());
306 self
307 }
308 pub fn with_release(mut self, release: bool) -> Self {
314 self.is_release = release;
315 self
316 }
317}
318
319impl From<(String, String, Option<String>)> for DnyTask {
320 fn from(tuple: (String, String, Option<String>)) -> Self {
321 Self {
322 main_code: tuple.0,
323 deps: tuple.1,
324 tag: tuple.2.unwrap_or_else(|| "unnamed_task".to_string()),
325 envs: HashMap::new(),
326 cargo_config: None,
327 is_release: false, }
329 }
330}
331
332pub struct BatchRunner {
348 timeout: Option<Duration>,
349 only_build: bool,
350 use_cache: bool,
351 on_result: Option<Box<dyn FnMut(String, DnyResult)>>,
352}
353
354impl Default for BatchRunner {
355 fn default() -> Self {
356 Self::new()
357 }
358}
359
360impl BatchRunner {
361 pub fn new() -> Self {
367 Self {
368 timeout: None,
369 only_build: false,
370 use_cache: false,
371 on_result: None,
372 }
373 }
374
375 pub fn timeout(mut self, d: Duration) -> Self {
381 self.timeout = Some(d);
382 self
383 }
384 pub fn only_build(mut self, b: bool) -> Self {
390 self.only_build = b;
391 self
392 }
393 pub fn use_cache(mut self, c: bool) -> Self {
399 self.use_cache = c;
400 self
401 }
402 pub fn on_result<F>(mut self, f: F) -> Self
408 where
409 F: FnMut(String, DnyResult) + 'static,
410 {
411 self.on_result = Some(Box::new(f));
412 self
413 }
414
415 pub(crate) fn with_boxed_callback(
416 mut self,
417 cb: Option<Box<dyn FnMut(String, DnyResult)>>,
418 ) -> Self {
419 self.on_result = cb;
420 self
421 }
422
423 pub fn run<I, T>(mut self, tasks: I) -> Vec<(String, DnyResult)>
442 where
443 I: IntoIterator<Item = T>,
444 T: Into<DnyTask>,
445 {
446 let tasks: Vec<DnyTask> = tasks.into_iter().map(|t| t.into()).collect();
447
448 if tasks.is_empty() {
449 return vec![];
450 }
451
452 if self.use_cache {
453 self.run_with_cache(tasks)
454 } else {
455 self.run_standard(tasks)
456 }
457 }
458
459 fn run_standard(&mut self, tasks: Vec<DnyTask>) -> Vec<(String, DnyResult)> {
460 let mut results = Vec::with_capacity(tasks.len());
461 for (index, task) in tasks.into_iter().enumerate() {
462 let tag = if task.tag == "unnamed_task" {
463 format!("task_{}", index)
464 } else {
465 task.tag
466 };
467
468 let mut runner = DnyRun::new(&task.main_code, &task.deps);
469 runner.envs(task.envs.clone());
470 runner.release(task.is_release);
471
472 if let Some(config) = task.cargo_config {
473 runner.cargo_config(config);
474 }
475
476 let res = if self.only_build {
477 runner.build(self.timeout)
478 } else {
479 runner.run(self.timeout)
480 };
481
482 if let Some(ref mut callback) = self.on_result {
483 callback(tag.clone(), res.clone());
484 }
485 results.push((tag, res));
486 }
487 results
488 }
489
490 fn run_with_cache(&mut self, tasks: Vec<DnyTask>) -> Vec<(String, DnyResult)> {
491 let mut results = Vec::with_capacity(tasks.len());
492 let first_deps = &tasks[0].deps;
493
494 let mut runner = DnyRun::new("fn main() {}", first_deps);
495 runner.release(tasks[0].is_release);
496
497 if let Some(config) = &tasks[0].cargo_config {
498 runner.cargo_config(config.clone());
499 }
500
501 let init_res = runner.build(self.timeout);
502 if let Some(ref mut callback) = self.on_result {
503 callback("init_cache".to_string(), init_res.clone());
504 }
505 results.push(("init_cache".to_string(), init_res));
506
507 for (index, task) in tasks.into_iter().enumerate() {
508 let tag = if task.tag == "unnamed_task" {
509 format!("task_{}", index)
510 } else {
511 task.tag
512 };
513
514 runner.reset_deps(&task.deps);
515 runner.reset_main_code(&task.main_code);
516
517 runner.clear_envs();
518 runner.envs(task.envs.clone());
519 runner.release(task.is_release); if let Some(config) = task.cargo_config {
522 runner.cargo_config(config);
523 } else {
524 runner.clear_cargo_config();
525 }
526
527 let res = if self.only_build {
528 runner.build(self.timeout)
529 } else {
530 runner.run(self.timeout)
531 };
532
533 if let Some(ref mut callback) = self.on_result {
534 callback(tag.clone(), res.clone());
535 }
536 results.push((tag, res));
537 }
538 results
539 }
540}
541
542pub fn dny_run(
561 main_code: &str,
562 deps: &str,
563 timeout: Option<Duration>,
564 only_build: bool,
565) -> DnyResult {
566 let dny_runner = DnyRun::new(main_code, deps);
567 if only_build {
568 dny_runner.build(timeout)
569 } else {
570 dny_runner.run(timeout)
571 }
572}
573
574pub fn dny_run_batch<I, T>(
593 task: I,
594 task_timeout: Option<Duration>,
595 only_build: Option<bool>,
596 on_result: Option<Box<dyn FnMut(String, DnyResult)>>,
597) -> Vec<(String, DnyResult)>
598where
599 I: IntoIterator<Item = T>,
600 T: Into<DnyTask>,
601{
602 let mut runner = BatchRunner::new()
603 .only_build(only_build.unwrap_or(false))
604 .use_cache(false)
605 .with_boxed_callback(on_result);
606
607 if let Some(t) = task_timeout {
608 runner = runner.timeout(t);
609 }
610 runner.run(task)
611}
612
613pub fn dny_run_batch_use_cache<I, T>(
632 task: I,
633 task_timeout: Option<Duration>,
634 only_build: Option<bool>,
635 on_result: Option<Box<dyn FnMut(String, DnyResult)>>,
636) -> Vec<(String, DnyResult)>
637where
638 I: IntoIterator<Item = T>,
639 T: Into<DnyTask>,
640{
641 let mut runner = BatchRunner::new()
642 .only_build(only_build.unwrap_or(false))
643 .use_cache(true)
644 .with_boxed_callback(on_result);
645
646 if let Some(t) = task_timeout {
647 runner = runner.timeout(t);
648 }
649 runner.run(task)
650}
651
652pub struct DnyRun {
658 pub main_code: String,
660 pub deps: String,
662 dir: PathBuf,
663 project_name: String,
664 lock: DynTestLock,
665 envs: HashMap<String, String>,
666 cargo_config: Option<String>,
667 pub is_release: bool,
669}
670
671impl DnyRun {
672 pub fn new(main_code: &str, deps: &str) -> Self {
692 let id = format!(
693 "{}_{}",
694 SystemTime::now()
695 .duration_since(UNIX_EPOCH)
696 .unwrap()
697 .as_nanos(),
698 rand::random::<u32>()
699 );
700 let project_name = format!("dyn_test_{}", id);
701
702 let mut dir = std::env::current_dir().unwrap();
703 dir.push("target");
704 dir.push("dyn_tests");
705 dir.push(format!("dny_{}", id));
706
707 let src_dir = dir.join("src");
708 fs::create_dir_all(&src_dir).expect("无法创建共享测试目录");
709
710 let instance = Self {
711 main_code: main_code.to_string(),
712 deps: deps.to_string(),
713 dir,
714 project_name: project_name.clone(),
715 lock: DynTestLock::new(&project_name),
716 envs: HashMap::new(),
717 cargo_config: None,
718 is_release: false,
719 };
720 instance.sync_files();
721 instance
722 }
723
724 pub fn release(&mut self, release: bool) -> &mut Self {
730 self.is_release = release;
731 self
732 }
733
734 pub fn env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
740 self.envs.insert(key.into(), value.into());
741 self
742 }
743
744 pub fn envs<I, K, V>(&mut self, envs: I) -> &mut Self
750 where
751 I: IntoIterator<Item = (K, V)>,
752 K: Into<String>,
753 V: Into<String>,
754 {
755 for (k, v) in envs {
756 self.envs.insert(k.into(), v.into());
757 }
758 self
759 }
760
761 pub fn clear_envs(&mut self) -> &mut Self {
767 self.envs.clear();
768 self
769 }
770
771 pub fn cargo_config(&mut self, config: impl Into<String>) -> &mut Self {
781 self.cargo_config = Some(config.into());
782 self.sync_cargo_config();
783 self
784 }
785
786 pub fn clear_cargo_config(&mut self) -> &mut Self {
792 self.cargo_config = None;
793 let config_path = self.dir.join(".cargo").join("config.toml");
794 if config_path.exists() {
795 let _ = fs::remove_file(config_path);
796 }
797 self
798 }
799
800 fn sync_cargo_config(&self) {
801 if let Some(config_content) = &self.cargo_config {
802 let cargo_dir = self.dir.join(".cargo");
803 if !cargo_dir.exists() {
804 fs::create_dir_all(&cargo_dir).expect("无法创建 .cargo 目录");
805 }
806 fs::write(cargo_dir.join("config.toml"), config_content)
807 .expect("写入 .cargo/config.toml 失败");
808 }
809 }
810
811 pub fn sync_files(&self) {
821 let cargo_toml = format!(
822 r#"[package]
823name = "{}"
824version = "0.1.0"
825edition = "2021"
826
827[dependencies]
828{}
829"#,
830 self.project_name, self.deps
831 );
832 fs::write(self.dir.join("Cargo.toml"), cargo_toml).expect("写入 Cargo.toml 失败");
833 fs::write(self.dir.join("src").join("main.rs"), &self.main_code)
834 .expect("写入 main.rs 失败");
835
836 self.sync_cargo_config();
837 }
838
839 fn execute_cmd(
840 cmd: &mut Command,
841 timeout: Option<Duration>,
842 ) -> Result<(Output, Duration), SystemError> {
843 let start_time = Instant::now();
844
845 let mut child = cmd
846 .stdout(Stdio::piped())
847 .stderr(Stdio::piped())
848 .spawn()
849 .map_err(|e| SystemError::ProcessSpawnFailed(e.to_string()))?;
850
851 let mut stdout_pipe = child.stdout.take().expect("无法获取 stdout");
852 let mut stderr_pipe = child.stderr.take().expect("无法获取 stderr");
853
854 let stdout_handle = thread::spawn(move || {
855 let mut buf = Vec::new();
856 let _ = stdout_pipe.read_to_end(&mut buf);
857 buf
858 });
859 let stderr_handle = thread::spawn(move || {
860 let mut buf = Vec::new();
861 let _ = stderr_pipe.read_to_end(&mut buf);
862 buf
863 });
864
865 match timeout {
866 None => {
867 let status = child
868 .wait()
869 .map_err(|e| SystemError::ProcessSpawnFailed(e.to_string()))?;
870 let stdout = stdout_handle.join().unwrap_or_default();
871 let stderr = stderr_handle.join().unwrap_or_default();
872 Ok((
873 Output {
874 status,
875 stdout,
876 stderr,
877 },
878 start_time.elapsed(),
879 ))
880 }
881 Some(limit) => {
882 let check_interval = Duration::from_millis(10);
883 loop {
884 if let Ok(Some(status)) = child.try_wait() {
885 let stdout = stdout_handle.join().unwrap_or_default();
886 let stderr = stderr_handle.join().unwrap_or_default();
887 return Ok((
888 Output {
889 status,
890 stdout,
891 stderr,
892 },
893 start_time.elapsed(),
894 ));
895 }
896
897 if start_time.elapsed() >= limit {
898 let _ = child.kill();
899 let _ = child.wait();
900 return Err(SystemError::Timeout(limit));
901 }
902 thread::sleep(check_interval);
903 }
904 }
905 }
906 }
907
908 pub fn build(&self, timeout: Option<Duration>) -> DnyResult {
923 let mut result = DnyResult::default();
924 let mut cmd = Command::new("cargo");
925 cmd.arg("build").arg("-q").current_dir(&self.dir);
926
927 if self.is_release {
928 cmd.arg("--release");
929 }
930
931 cmd.envs(&self.envs);
932
933 match Self::execute_cmd(&mut cmd, timeout) {
934 Ok((output, duration)) => {
935 result.build_duration = duration;
936 result.stdout = String::from_utf8_lossy(&output.stdout).to_string();
937 result.stderr = String::from_utf8_lossy(&output.stderr).to_string();
938 result.exit_code = output.status.code().unwrap_or(-1);
939 result.ok = output.status.success();
940 if !result.ok {
941 result.system_err = SystemError::CompileFailed;
942 }
943 }
944 Err(sys_err) => {
945 result.system_err = sys_err;
946 result.exit_code = -2;
947 }
948 }
949 result
950 }
951
952 pub fn run(&self, timeout: Option<Duration>) -> DnyResult {
967 let build_result = self.build(timeout);
968 if !build_result.ok {
969 return build_result;
970 }
971 self.run_no_build(timeout, Some(build_result))
972 }
973
974 pub fn run_no_build(
981 &self,
982 timeout: Option<Duration>,
983 build_result: Option<DnyResult>,
984 ) -> DnyResult {
985 let mut target_dir = self.dir.join("target");
986
987 if let Some(cfg_str) = &self.cargo_config {
988 let mut in_build_section = false;
989
990 for line in cfg_str.lines() {
991 let line = line.trim();
992
993 if line.is_empty() || line.starts_with('#') {
994 continue;
995 }
996
997 if line.starts_with('[') {
998 in_build_section = line == "[build]";
999 continue;
1000 }
1001
1002 if in_build_section
1003 && line.starts_with("target")
1004 && let Some((_, val)) = line.split_once('=')
1005 {
1006 let val_no_comment = val.split('#').next().unwrap_or(val);
1007 let target_val = val_no_comment
1008 .trim()
1009 .trim_matches(|c| c == '"' || c == '\'');
1010 target_dir = target_dir.join(target_val);
1011 break;
1012 }
1013 }
1014 }
1015
1016 let exe_name = if cfg!(windows) {
1017 format!("{}.exe", self.project_name)
1018 } else {
1019 self.project_name.clone()
1020 };
1021
1022 let profile_dir = if self.is_release { "release" } else { "debug" };
1023 let bin_path = target_dir.join(profile_dir).join(&exe_name);
1024
1025 let build_result = build_result.unwrap_or_default();
1026 let mut result = DnyResult {
1027 build_duration: build_result.build_duration,
1028 ..Default::default()
1029 };
1030
1031 if !bin_path.exists() {
1032 result.system_err = SystemError::ProcessSpawnFailed(format!(
1033 "测试可执行文件未找到。\n预期路径: {}\n请检查编译是否真的成功,或者 target/profile 逻辑是否正确。",
1034 bin_path.display()
1035 ));
1036 result.exit_code = -2;
1037 return result;
1038 }
1039
1040 let mut cmd = Command::new(&bin_path);
1041 cmd.envs(&self.envs);
1042
1043 match Self::execute_cmd(&mut cmd, timeout) {
1044 Ok((output, duration)) => {
1045 result.run_duration = duration;
1046 result.stdout = String::from_utf8_lossy(&output.stdout).into_owned();
1047 result.stderr = String::from_utf8_lossy(&output.stderr).into_owned();
1048 result.exit_code = output.status.code().unwrap_or(-1);
1049 result.ok = output.status.success();
1050 }
1051 Err(sys_err) => {
1052 result.system_err = sys_err;
1053 result.exit_code = -2;
1054 }
1055 }
1056
1057 result
1058 }
1059
1060 pub fn reset_main_code(&mut self, main_code: &str) {
1070 self.main_code = main_code.to_string();
1071 self.sync_files();
1072 }
1073 pub fn reset_deps(&mut self, deps: &str) {
1083 self.deps = deps.to_string();
1084 self.sync_files();
1085 }
1086}
1087
1088pub struct DynTestLock {
1094 lock_path: PathBuf,
1095 stop_signal: Arc<AtomicBool>,
1096 thread_handle: Option<thread::JoinHandle<()>>,
1097}
1098
1099impl DynTestLock {
1100 pub fn new(id: &str) -> Self {
1110 let mut lock_path = std::env::current_dir().unwrap();
1111 lock_path.push("target");
1112 lock_path.push("dyn_tests");
1113 lock_path.push("lock");
1114
1115 fs::create_dir_all(&lock_path).expect("无法创建锁目录");
1116 let lock_file = lock_path.join(format!("lock_{}", id));
1117 fs::File::create(&lock_file).expect("无法创建锁文件");
1118
1119 let stop_signal = Arc::new(AtomicBool::new(false));
1120 let thread_stop_signal = stop_signal.clone();
1121 let thread_lock_file = lock_file.clone();
1122
1123 let thread_handle = thread::spawn(move || {
1124 let mut counter = 0u64;
1125 let heartbeat_interval = Duration::from_millis(500);
1126 while !thread_stop_signal.load(Ordering::Relaxed) {
1127 if let Ok(mut file) = fs::File::create(&thread_lock_file) {
1128 let _ = write!(file, "heartbeat: {}", counter);
1129 }
1130 counter += 1;
1131 for _ in 0..10 {
1132 if thread_stop_signal.load(Ordering::Relaxed) {
1133 break;
1134 }
1135 thread::sleep(heartbeat_interval / 10);
1136 }
1137 }
1138 });
1139
1140 Self {
1141 lock_path: lock_file,
1142 stop_signal,
1143 thread_handle: Some(thread_handle),
1144 }
1145 }
1146}
1147
1148impl Drop for DynTestLock {
1149 fn drop(&mut self) {
1150 self.stop_signal.store(true, Ordering::Relaxed);
1151 if let Some(handle) = self.thread_handle.take() {
1152 let _ = handle.join();
1153 }
1154 if self.lock_path.exists() {
1155 let _ = fs::remove_file(&self.lock_path);
1156 }
1157 }
1158}
1159
1160pub fn clear_dny_project(timeout: Option<Duration>) {
1178 thread::sleep(Duration::from_secs_f32(0.3));
1179 let base_dir = std::env::current_dir()
1180 .unwrap()
1181 .join("target")
1182 .join("dyn_tests");
1183 let lock_dir = base_dir.join("lock");
1184 let start_time = Instant::now();
1185 let zombie_threshold = Duration::from_millis(1500);
1186
1187 if lock_dir.exists() {
1188 loop {
1189 let mut active_locks = 0;
1190 if let Ok(entries) = fs::read_dir(&lock_dir) {
1191 for entry in entries.flatten() {
1192 let path = entry.path();
1193 if !path.is_file() {
1194 continue;
1195 }
1196
1197 let is_zombie = match fs::metadata(&path).and_then(|m| m.modified()) {
1198 Ok(modified_time) => {
1199 modified_time.elapsed().unwrap_or(Duration::ZERO) > zombie_threshold
1200 }
1201 Err(_) => true,
1202 };
1203
1204 if is_zombie {
1205 let _ = fs::remove_file(&path);
1206 } else {
1207 active_locks += 1;
1208 }
1209 }
1210 }
1211
1212 if active_locks == 0 {
1213 break;
1214 }
1215 if let Some(limit) = timeout
1216 && start_time.elapsed() >= limit
1217 {
1218 eprintln!("警告: 清除动态测试环境超时!");
1219 break;
1220 }
1221 thread::sleep(Duration::from_millis(100));
1222 }
1223 }
1224
1225 if base_dir.exists() {
1226 let _ = fs::remove_dir_all(base_dir);
1227 }
1228}