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)]
26#[non_exhaustive]
27pub enum SystemError {
28 None,
30 Timeout(Duration),
32 CompileFailed,
34 CargoFailed,
37 ProcessSpawnFailed(String),
39}
40
41#[derive(Debug, Clone)]
57pub struct DnyResult {
58 pub stdout: String,
60 pub stderr: String,
62 pub exit_code: i32,
64 pub build_duration: Duration,
66 pub run_duration: Duration,
68 pub ok: bool,
70 pub system_err: SystemError,
72}
73
74impl Default for DnyResult {
75 fn default() -> Self {
76 Self {
77 stdout: String::new(),
78 stderr: String::new(),
79 exit_code: -1,
80 build_duration: Duration::ZERO,
81 run_duration: Duration::ZERO,
82 ok: false,
83 system_err: SystemError::None,
84 }
85 }
86}
87
88fn truncate_output(text: &str, max_lines: usize) -> String {
89 const MAX_LINE_CHARS: usize = 300;
90
91 let text = text.trim();
92 if text.is_empty() {
93 return String::new();
94 }
95
96 let truncate_line = |line: &str| -> String {
97 if line.len() <= MAX_LINE_CHARS {
98 return line.to_string();
99 }
100
101 let mut chars = line.chars();
102 let head: String = chars.by_ref().take(MAX_LINE_CHARS / 2).collect();
103
104 if chars.next().is_none() {
105 return head;
106 }
107
108 let tail: String = line
109 .chars()
110 .rev()
111 .take(MAX_LINE_CHARS / 2)
112 .collect::<Vec<char>>()
113 .into_iter()
114 .rev()
115 .collect();
116
117 format!("{} ... [单行超长截断] ... {}", head, tail)
118 };
119
120 let lines: Vec<&str> = text.lines().collect();
121
122 if lines.len() <= max_lines {
123 return lines
124 .into_iter()
125 .map(truncate_line)
126 .collect::<Vec<_>>()
127 .join("\n");
128 }
129
130 let half = max_lines / 2;
131 let head = lines[..half]
132 .iter()
133 .copied()
134 .map(truncate_line)
135 .collect::<Vec<_>>()
136 .join("\n");
137 let tail = lines[lines.len() - half..]
138 .iter()
139 .copied()
140 .map(truncate_line)
141 .collect::<Vec<_>>()
142 .join("\n");
143
144 format!(
145 "{}\n\n... [已截断 {} 行] ...\n\n{}",
146 head,
147 lines.len() - max_lines,
148 tail
149 )
150}
151
152impl fmt::Display for DnyResult {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 let green = "\x1b[32m";
155 let red = "\x1b[31m";
156 let yellow = "\x1b[33m";
157 let cyan = "\x1b[36m";
158 let reset = "\x1b[0m";
159
160 let (status_color, status_text) = match &self.system_err {
161 SystemError::None if self.ok => (green, "SUCCESS"),
162 SystemError::None => (red, "FAILED (Runtime Error)"),
163 SystemError::Timeout(_) => (yellow, "TIMEOUT (TLE)"),
164 SystemError::CompileFailed => (red, "COMPILE ERROR (CE)"),
165 SystemError::CargoFailed => (red, "CARGO ERROR (CE)"),
166 SystemError::ProcessSpawnFailed(_) => (red, "SYSTEM ERROR"),
167 };
168
169 writeln!(f, "{status_color}=== [Result: {}] ==={reset}", status_text)?;
170 writeln!(
171 f,
172 "> Build: {cyan}{:?}{reset} | Run: {cyan}{:?}{reset}",
173 self.build_duration, self.run_duration
174 )?;
175 writeln!(f, "> Exit Code: {status_color}{}{reset}", self.exit_code)?;
176
177 let stdout_trimmed = self.stdout.trim();
178 if !stdout_trimmed.is_empty() {
179 writeln!(
180 f,
181 "{}--- Stdout (size: {} bytes) ---{}",
182 yellow,
183 self.stdout.len(),
184 reset
185 )?;
186 writeln!(f, "{}", truncate_output(stdout_trimmed, 20))?;
187 }
188
189 let clean_stderr = self
190 .stderr
191 .lines()
192 .filter(|l| !l.trim().starts_with("Compiling "))
193 .filter(|l| !l.trim().starts_with("Finished "))
194 .collect::<Vec<_>>()
195 .join("\n");
196
197 let stderr_trimmed = clean_stderr.trim();
198 if !stderr_trimmed.is_empty() {
199 writeln!(
200 f,
201 "{}--- Stderr (size: {} bytes) ---{}",
202 red,
203 clean_stderr.len(),
204 reset
205 )?;
206 writeln!(f, "{}", truncate_output(stderr_trimmed, 40))?;
207 }
208
209 writeln!(f, "{status_color}=========================={reset}")?;
210 Ok(())
211 }
212}
213
214#[derive(Clone)]
232pub struct DnyTask {
233 pub tag: String,
235 pub main_code: String,
237 pub deps: String,
239 pub envs: HashMap<String, String>,
241 pub cargo_config: Option<String>,
243 pub is_release: bool,
245}
246
247impl DnyTask {
248 pub fn new(tag: impl Into<String>, code: impl Into<String>) -> Self {
263 Self {
264 tag: tag.into(),
265 main_code: code.into(),
266 deps: String::new(),
267 envs: HashMap::new(),
268 cargo_config: None,
269 is_release: false,
270 }
271 }
272 pub fn with_deps(mut self, deps: impl Into<String>) -> Self {
278 self.deps = deps.into();
279 self
280 }
281 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
287 self.envs.insert(key.into(), value.into());
288 self
289 }
290 pub fn with_envs<I, K, V>(mut self, envs: I) -> Self
296 where
297 I: IntoIterator<Item = (K, V)>,
298 K: Into<String>,
299 V: Into<String>,
300 {
301 for (k, v) in envs {
302 self.envs.insert(k.into(), v.into());
303 }
304 self
305 }
306 pub fn with_cargo_config(mut self, config: impl Into<String>) -> Self {
312 self.cargo_config = Some(config.into());
313 self
314 }
315 pub fn with_release(mut self, release: bool) -> Self {
321 self.is_release = release;
322 self
323 }
324}
325
326impl From<(String, String, Option<String>)> for DnyTask {
327 fn from(tuple: (String, String, Option<String>)) -> Self {
328 Self {
329 main_code: tuple.0,
330 deps: tuple.1,
331 tag: tuple.2.unwrap_or_else(|| "unnamed_task".to_string()),
332 envs: HashMap::new(),
333 cargo_config: None,
334 is_release: false, }
336 }
337}
338
339pub struct BatchRunner {
355 timeout: Option<Duration>,
356 only_build: bool,
357 use_cache: bool,
358 on_result: Option<Box<dyn FnMut(String, DnyResult)>>,
359}
360
361impl Default for BatchRunner {
362 fn default() -> Self {
363 Self::new()
364 }
365}
366
367impl BatchRunner {
368 pub fn new() -> Self {
374 Self {
375 timeout: None,
376 only_build: false,
377 use_cache: false,
378 on_result: None,
379 }
380 }
381
382 pub fn timeout(mut self, d: Duration) -> Self {
388 self.timeout = Some(d);
389 self
390 }
391 pub fn only_build(mut self, b: bool) -> Self {
397 self.only_build = b;
398 self
399 }
400 pub fn use_cache(mut self, c: bool) -> Self {
406 self.use_cache = c;
407 self
408 }
409 pub fn on_result<F>(mut self, f: F) -> Self
415 where
416 F: FnMut(String, DnyResult) + 'static,
417 {
418 self.on_result = Some(Box::new(f));
419 self
420 }
421
422 pub(crate) fn with_boxed_callback(
423 mut self,
424 cb: Option<Box<dyn FnMut(String, DnyResult)>>,
425 ) -> Self {
426 self.on_result = cb;
427 self
428 }
429
430 pub fn run<I, T>(mut self, tasks: I) -> Vec<(String, DnyResult)>
449 where
450 I: IntoIterator<Item = T>,
451 T: Into<DnyTask>,
452 {
453 let tasks: Vec<DnyTask> = tasks.into_iter().map(|t| t.into()).collect();
454
455 if tasks.is_empty() {
456 return vec![];
457 }
458
459 if self.use_cache {
460 self.run_with_cache(tasks)
461 } else {
462 self.run_standard(tasks)
463 }
464 }
465
466 fn run_standard(&mut self, tasks: Vec<DnyTask>) -> Vec<(String, DnyResult)> {
467 let mut results = Vec::with_capacity(tasks.len());
468 for (index, task) in tasks.into_iter().enumerate() {
469 let tag = if task.tag == "unnamed_task" {
470 format!("task_{}", index)
471 } else {
472 task.tag
473 };
474
475 let mut runner = DnyRun::new(&task.main_code, &task.deps);
476 runner.envs(task.envs.clone());
477 runner.release(task.is_release);
478
479 if let Some(config) = task.cargo_config {
480 runner.cargo_config(config);
481 }
482
483 let res = if self.only_build {
484 runner.build(self.timeout)
485 } else {
486 runner.run(self.timeout)
487 };
488
489 if let Some(ref mut callback) = self.on_result {
490 callback(tag.clone(), res.clone());
491 }
492 results.push((tag, res));
493 }
494 results
495 }
496
497 fn run_with_cache(&mut self, tasks: Vec<DnyTask>) -> Vec<(String, DnyResult)> {
498 let mut results = Vec::with_capacity(tasks.len());
499 let first_deps = &tasks[0].deps;
500
501 let mut runner = DnyRun::new("fn main() {}", first_deps);
502 runner.release(tasks[0].is_release);
503
504 if let Some(config) = &tasks[0].cargo_config {
505 runner.cargo_config(config.clone());
506 }
507
508 let init_res = runner.build(self.timeout);
509 if let Some(ref mut callback) = self.on_result {
510 callback("init_cache".to_string(), init_res.clone());
511 }
512 results.push(("init_cache".to_string(), init_res));
513
514 for (index, task) in tasks.into_iter().enumerate() {
515 let tag = if task.tag == "unnamed_task" {
516 format!("task_{}", index)
517 } else {
518 task.tag
519 };
520
521 runner.reset_deps(&task.deps);
522 runner.reset_main_code(&task.main_code);
523
524 runner.clear_envs();
525 runner.envs(task.envs.clone());
526 runner.release(task.is_release); if let Some(config) = task.cargo_config {
529 runner.cargo_config(config);
530 } else {
531 runner.clear_cargo_config();
532 }
533
534 let res = if self.only_build {
535 runner.build(self.timeout)
536 } else {
537 runner.run(self.timeout)
538 };
539
540 if let Some(ref mut callback) = self.on_result {
541 callback(tag.clone(), res.clone());
542 }
543 results.push((tag, res));
544 }
545 results
546 }
547}
548
549pub fn dny_run(
568 main_code: &str,
569 deps: &str,
570 timeout: Option<Duration>,
571 only_build: bool,
572) -> DnyResult {
573 let dny_runner = DnyRun::new(main_code, deps);
574 if only_build {
575 dny_runner.build(timeout)
576 } else {
577 dny_runner.run(timeout)
578 }
579}
580
581pub fn dny_run_batch<I, T>(
600 task: I,
601 task_timeout: Option<Duration>,
602 only_build: Option<bool>,
603 on_result: Option<Box<dyn FnMut(String, DnyResult)>>,
604) -> Vec<(String, DnyResult)>
605where
606 I: IntoIterator<Item = T>,
607 T: Into<DnyTask>,
608{
609 let mut runner = BatchRunner::new()
610 .only_build(only_build.unwrap_or(false))
611 .use_cache(false)
612 .with_boxed_callback(on_result);
613
614 if let Some(t) = task_timeout {
615 runner = runner.timeout(t);
616 }
617 runner.run(task)
618}
619
620pub fn dny_run_batch_use_cache<I, T>(
639 task: I,
640 task_timeout: Option<Duration>,
641 only_build: Option<bool>,
642 on_result: Option<Box<dyn FnMut(String, DnyResult)>>,
643) -> Vec<(String, DnyResult)>
644where
645 I: IntoIterator<Item = T>,
646 T: Into<DnyTask>,
647{
648 let mut runner = BatchRunner::new()
649 .only_build(only_build.unwrap_or(false))
650 .use_cache(true)
651 .with_boxed_callback(on_result);
652
653 if let Some(t) = task_timeout {
654 runner = runner.timeout(t);
655 }
656 runner.run(task)
657}
658
659pub struct DnyRun {
665 pub main_code: String,
667 pub deps: String,
669 dir: PathBuf,
670 project_name: String,
671 lock: DynTestLock,
672 envs: HashMap<String, String>,
673 cargo_config: Option<String>,
674 pub is_release: bool,
676}
677
678impl DnyRun {
679 pub fn new(main_code: &str, deps: &str) -> Self {
699 let id = format!(
700 "{}_{}",
701 SystemTime::now()
702 .duration_since(UNIX_EPOCH)
703 .unwrap()
704 .as_nanos(),
705 rand::random::<u32>()
706 );
707 let project_name = format!("dyn_test_{}", id);
708
709 let mut dir = std::env::current_dir().unwrap();
710 dir.push("target");
711 dir.push("dyn_tests");
712 dir.push(format!("dny_{}", id));
713
714 let src_dir = dir.join("src");
715 fs::create_dir_all(&src_dir).expect("无法创建共享测试目录");
716
717 let instance = Self {
718 main_code: main_code.to_string(),
719 deps: deps.to_string(),
720 dir,
721 project_name: project_name.clone(),
722 lock: DynTestLock::new(&project_name),
723 envs: HashMap::new(),
724 cargo_config: None,
725 is_release: false,
726 };
727 instance.sync_files();
728 instance
729 }
730
731 pub fn release(&mut self, release: bool) -> &mut Self {
737 self.is_release = release;
738 self
739 }
740
741 pub fn env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
747 self.envs.insert(key.into(), value.into());
748 self
749 }
750
751 pub fn envs<I, K, V>(&mut self, envs: I) -> &mut Self
757 where
758 I: IntoIterator<Item = (K, V)>,
759 K: Into<String>,
760 V: Into<String>,
761 {
762 for (k, v) in envs {
763 self.envs.insert(k.into(), v.into());
764 }
765 self
766 }
767
768 pub fn clear_envs(&mut self) -> &mut Self {
774 self.envs.clear();
775 self
776 }
777
778 pub fn cargo_config(&mut self, config: impl Into<String>) -> &mut Self {
788 self.cargo_config = Some(config.into());
789 self.sync_cargo_config();
790 self
791 }
792
793 pub fn clear_cargo_config(&mut self) -> &mut Self {
799 self.cargo_config = None;
800 let config_path = self.dir.join(".cargo").join("config.toml");
801 if config_path.exists() {
802 let _ = fs::remove_file(config_path);
803 }
804 self
805 }
806
807 fn sync_cargo_config(&self) {
808 if let Some(config_content) = &self.cargo_config {
809 let cargo_dir = self.dir.join(".cargo");
810 if !cargo_dir.exists() {
811 fs::create_dir_all(&cargo_dir).expect("无法创建 .cargo 目录");
812 }
813 fs::write(cargo_dir.join("config.toml"), config_content)
814 .expect("写入 .cargo/config.toml 失败");
815 }
816 }
817
818 pub fn sync_files(&self) {
828 let cargo_toml = format!(
829 r#"[package]
830name = "{}"
831version = "0.1.0"
832edition = "2021"
833
834[dependencies]
835{}
836"#,
837 self.project_name, self.deps
838 );
839 fs::write(self.dir.join("Cargo.toml"), cargo_toml).expect("写入 Cargo.toml 失败");
840 fs::write(self.dir.join("src").join("main.rs"), &self.main_code)
841 .expect("写入 main.rs 失败");
842
843 self.sync_cargo_config();
844 }
845
846 fn execute_cmd(
847 cmd: &mut Command,
848 timeout: Option<Duration>,
849 ) -> Result<(Output, Duration), SystemError> {
850 let start_time = Instant::now();
851
852 let mut child = cmd
853 .stdout(Stdio::piped())
854 .stderr(Stdio::piped())
855 .spawn()
856 .map_err(|e| SystemError::ProcessSpawnFailed(e.to_string()))?;
857
858 let mut stdout_pipe = child.stdout.take().expect("无法获取 stdout");
859 let mut stderr_pipe = child.stderr.take().expect("无法获取 stderr");
860
861 let stdout_handle = thread::spawn(move || {
862 let mut buf = Vec::new();
863 let _ = stdout_pipe.read_to_end(&mut buf);
864 buf
865 });
866 let stderr_handle = thread::spawn(move || {
867 let mut buf = Vec::new();
868 let _ = stderr_pipe.read_to_end(&mut buf);
869 buf
870 });
871
872 match timeout {
873 None => {
874 let status = child
875 .wait()
876 .map_err(|e| SystemError::ProcessSpawnFailed(e.to_string()))?;
877 let stdout = stdout_handle.join().unwrap_or_default();
878 let stderr = stderr_handle.join().unwrap_or_default();
879 Ok((
880 Output {
881 status,
882 stdout,
883 stderr,
884 },
885 start_time.elapsed(),
886 ))
887 }
888 Some(limit) => {
889 let check_interval = Duration::from_millis(10);
890 loop {
891 if let Ok(Some(status)) = child.try_wait() {
892 let stdout = stdout_handle.join().unwrap_or_default();
893 let stderr = stderr_handle.join().unwrap_or_default();
894 return Ok((
895 Output {
896 status,
897 stdout,
898 stderr,
899 },
900 start_time.elapsed(),
901 ));
902 }
903
904 if start_time.elapsed() >= limit {
905 let _ = child.kill();
906 let _ = child.wait();
907 return Err(SystemError::Timeout(limit));
908 }
909 thread::sleep(check_interval);
910 }
911 }
912 }
913 }
914
915 pub fn build(&self, timeout: Option<Duration>) -> DnyResult {
930 let mut result = DnyResult::default();
931 let mut cmd = Command::new("cargo");
932 cmd.arg("build").arg("-q").current_dir(&self.dir);
933
934 if self.is_release {
935 cmd.arg("--release");
936 }
937
938 cmd.envs(&self.envs);
939
940 match Self::execute_cmd(&mut cmd, timeout) {
941 Ok((output, duration)) => {
942 result.build_duration = duration;
943 result.stdout = String::from_utf8_lossy(&output.stdout).to_string();
944 result.stderr = String::from_utf8_lossy(&output.stderr).to_string();
945 result.exit_code = output.status.code().unwrap_or(-1);
946 result.ok = output.status.success();
947 if !result.ok {
948 result.system_err = SystemError::CompileFailed;
949 }
950 }
951 Err(sys_err) => {
952 result.system_err = sys_err;
953 result.exit_code = -2;
954 }
955 }
956 result
957 }
958
959 pub fn run(&self, timeout: Option<Duration>) -> DnyResult {
974 let build_result = self.build(timeout);
975 if !build_result.ok {
976 return build_result;
977 }
978 self.run_no_build(timeout, Some(build_result))
979 }
980
981 pub fn bin_path(&self) -> PathBuf {
1007 let mut target_dir = self.dir.join("target");
1008
1009 if let Some(cfg_str) = &self.cargo_config {
1010 let mut in_build_section = false;
1011
1012 for line in cfg_str.lines() {
1013 let line = line.trim();
1014
1015 if line.is_empty() || line.starts_with('#') {
1016 continue;
1017 }
1018
1019 if line.starts_with('[') {
1020 in_build_section = line == "[build]";
1021 continue;
1022 }
1023
1024 if in_build_section
1025 && line.starts_with("target")
1026 && let Some((_, val)) = line.split_once('=')
1027 {
1028 let val_no_comment = val.split('#').next().unwrap_or(val);
1029 let target_val = val_no_comment
1030 .trim()
1031 .trim_matches(|c| c == '"' || c == '\'');
1032 target_dir = target_dir.join(target_val);
1033 break;
1034 }
1035 }
1036 }
1037
1038 let exe_name = if cfg!(windows) {
1039 format!("{}.exe", self.project_name)
1040 } else {
1041 self.project_name.clone()
1042 };
1043
1044 let profile_dir = if self.is_release { "release" } else { "debug" };
1045 target_dir.join(profile_dir).join(exe_name)
1046 }
1047
1048 pub fn run_no_build(
1068 &self,
1069 timeout: Option<Duration>,
1070 build_result: Option<DnyResult>,
1071 ) -> DnyResult {
1072 let bin_path = self.bin_path();
1073
1074 let build_result = build_result.unwrap_or_default();
1075 let mut result = DnyResult {
1076 build_duration: build_result.build_duration,
1077 ..Default::default()
1078 };
1079
1080 if !bin_path.exists() {
1081 result.system_err = SystemError::ProcessSpawnFailed(format!(
1082 "测试可执行文件未找到。\n预期路径: {}\n请检查编译是否真的成功,或者 target/profile 逻辑是否正确。",
1083 bin_path.display()
1084 ));
1085 result.exit_code = -2;
1086 return result;
1087 }
1088
1089 let mut cmd = Command::new(&bin_path);
1090 cmd.envs(&self.envs);
1091
1092 match Self::execute_cmd(&mut cmd, timeout) {
1093 Ok((output, duration)) => {
1094 result.run_duration = duration;
1095 result.stdout = String::from_utf8_lossy(&output.stdout).into_owned();
1096 result.stderr = String::from_utf8_lossy(&output.stderr).into_owned();
1097 result.exit_code = output.status.code().unwrap_or(-1);
1098 result.ok = output.status.success();
1099 }
1100 Err(sys_err) => {
1101 result.system_err = sys_err;
1102 result.exit_code = -2;
1103 }
1104 }
1105
1106 result
1107 }
1108
1109 pub fn cargo(&self, args: &[&str], timeout: Option<Duration>) -> DnyResult {
1147 let mut result = DnyResult::default();
1148 let mut cmd = Command::new("cargo");
1149 cmd.args(args).current_dir(&self.dir);
1150 cmd.envs(&self.envs);
1151
1152 match Self::execute_cmd(&mut cmd, timeout) {
1153 Ok((output, duration)) => {
1154 result.run_duration = duration;
1155 result.stdout = String::from_utf8_lossy(&output.stdout).to_string();
1156 result.stderr = String::from_utf8_lossy(&output.stderr).to_string();
1157 result.exit_code = output.status.code().unwrap_or(-1);
1158 result.ok = output.status.success();
1159 if !result.ok {
1160 result.system_err = SystemError::CargoFailed;
1161 }
1162 }
1163 Err(sys_err) => {
1164 result.system_err = sys_err;
1165 result.exit_code = -2;
1166 }
1167 }
1168 result
1169 }
1170
1171 pub fn reset_main_code(&mut self, main_code: &str) {
1181 self.main_code = main_code.to_string();
1182 self.sync_files();
1183 }
1184 pub fn reset_deps(&mut self, deps: &str) {
1194 self.deps = deps.to_string();
1195 self.sync_files();
1196 }
1197}
1198
1199pub struct DynTestLock {
1205 lock_path: PathBuf,
1206 stop_signal: Arc<AtomicBool>,
1207 thread_handle: Option<thread::JoinHandle<()>>,
1208}
1209
1210impl DynTestLock {
1211 pub fn new(id: &str) -> Self {
1221 let mut lock_path = std::env::current_dir().unwrap();
1222 lock_path.push("target");
1223 lock_path.push("dyn_tests");
1224 lock_path.push("lock");
1225
1226 fs::create_dir_all(&lock_path).expect("无法创建锁目录");
1227 let lock_file = lock_path.join(format!("lock_{}", id));
1228 fs::File::create(&lock_file).expect("无法创建锁文件");
1229
1230 let stop_signal = Arc::new(AtomicBool::new(false));
1231 let thread_stop_signal = stop_signal.clone();
1232 let thread_lock_file = lock_file.clone();
1233
1234 let thread_handle = thread::spawn(move || {
1235 let mut counter = 0u64;
1236 let heartbeat_interval = Duration::from_millis(500);
1237 while !thread_stop_signal.load(Ordering::Relaxed) {
1238 if let Ok(mut file) = fs::File::create(&thread_lock_file) {
1239 let _ = write!(file, "heartbeat: {}", counter);
1240 }
1241 counter += 1;
1242 for _ in 0..10 {
1243 if thread_stop_signal.load(Ordering::Relaxed) {
1244 break;
1245 }
1246 thread::sleep(heartbeat_interval / 10);
1247 }
1248 }
1249 });
1250
1251 Self {
1252 lock_path: lock_file,
1253 stop_signal,
1254 thread_handle: Some(thread_handle),
1255 }
1256 }
1257}
1258
1259impl Drop for DynTestLock {
1260 fn drop(&mut self) {
1261 self.stop_signal.store(true, Ordering::Relaxed);
1262 if let Some(handle) = self.thread_handle.take() {
1263 let _ = handle.join();
1264 }
1265 if self.lock_path.exists() {
1266 let _ = fs::remove_file(&self.lock_path);
1267 }
1268 }
1269}
1270
1271pub fn clear_dny_project(timeout: Option<Duration>) {
1289 thread::sleep(Duration::from_secs_f32(0.3));
1290 let base_dir = std::env::current_dir()
1291 .unwrap()
1292 .join("target")
1293 .join("dyn_tests");
1294 let lock_dir = base_dir.join("lock");
1295 let start_time = Instant::now();
1296 let zombie_threshold = Duration::from_millis(1500);
1297
1298 if lock_dir.exists() {
1299 loop {
1300 let mut active_locks = 0;
1301 if let Ok(entries) = fs::read_dir(&lock_dir) {
1302 for entry in entries.flatten() {
1303 let path = entry.path();
1304 if !path.is_file() {
1305 continue;
1306 }
1307
1308 let is_zombie = match fs::metadata(&path).and_then(|m| m.modified()) {
1309 Ok(modified_time) => {
1310 modified_time.elapsed().unwrap_or(Duration::ZERO) > zombie_threshold
1311 }
1312 Err(_) => true,
1313 };
1314
1315 if is_zombie {
1316 let _ = fs::remove_file(&path);
1317 } else {
1318 active_locks += 1;
1319 }
1320 }
1321 }
1322
1323 if active_locks == 0 {
1324 break;
1325 }
1326 if let Some(limit) = timeout
1327 && start_time.elapsed() >= limit
1328 {
1329 eprintln!("警告: 清除动态测试环境超时!");
1330 break;
1331 }
1332 thread::sleep(Duration::from_millis(100));
1333 }
1334 }
1335
1336 if base_dir.exists() {
1337 let _ = fs::remove_dir_all(base_dir);
1338 }
1339}