Skip to main content

lib_unknown/
dyntest.rs

1//! 动态编译运行 harness(仅开发期使用):将代码片段生成临时 Cargo 工程并编译执行,支持超时、批量与缓存(`DnyRun` / `BatchRunner`)。
2//!
3//! 需要同时启用 `"dyntest"` 与 `"std"` 特性。传入代码将被编译执行,**仅可在隔离沙箱中使用**。
4#![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/// 动态测试运行的系统级错误(区别于被测程序自身的失败)。
19///
20/// 未来可能新增变体,请勿依赖穷尽匹配(匹配时请保留通配分支)。
21///
22/// # Feature Requirement
23///
24/// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
25#[derive(Debug, Clone, PartialEq)]
26#[non_exhaustive]
27pub enum SystemError {
28    /// 无系统级错误;`ok == false` 时表示被测程序自身返回非零退出码。
29    None,
30    /// 执行超时并已被终止,载荷为超时阈值。
31    Timeout(Duration),
32    /// `cargo build` 失败(退出码非零)。
33    CompileFailed,
34    /// [`DnyRun::cargo`](crate::dyntest::DnyRun::cargo) 执行的自定义 `cargo <args>`
35    /// 命令返回非零退出码(超时、启动失败除外,分别记为 `Timeout` / `ProcessSpawnFailed`)。
36    CargoFailed,
37    /// 子进程无法启动,载荷为系统错误描述。
38    ProcessSpawnFailed(String),
39}
40
41/// 单次动态测试的结果,`Display` 输出彩色摘要(超长输出自动截断)。
42///
43/// # Feature Requirement
44///
45/// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
46///
47/// # Examples
48///
49/// ```rust,no_run
50/// use lib_unknown::dyntest::{BatchRunner, DnyTask};
51///
52/// let task = DnyTask::new("hello", r#"fn main() { println!("hi"); }"#);
53/// let results = BatchRunner::new().run([task]);
54/// assert_eq!(results.len(), 1);
55/// ```
56#[derive(Debug, Clone)]
57pub struct DnyResult {
58    /// 被测程序标准输出全文。
59    pub stdout: String,
60    /// 被测程序标准错误全文(已过滤 `Compiling` / `Finished` 行)。
61    pub stderr: String,
62    /// 被测程序退出码;系统级失败时为 `-2`,默认构造时为 `-1`。
63    pub exit_code: i32,
64    /// 编译耗时。
65    pub build_duration: Duration,
66    /// 运行耗时。
67    pub run_duration: Duration,
68    /// 被测程序是否以退出码 0 正常结束。
69    pub ok: bool,
70    /// 系统级错误,无则为 [`SystemError::None`]。
71    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/// 单个动态测试任务:`main.rs` 内容、依赖与运行配置的 Builder。
215///
216/// # Feature Requirement
217///
218/// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
219///
220/// # Examples
221///
222/// ```rust
223/// use lib_unknown::dyntest::DnyTask;
224///
225/// let task = DnyTask::new("hello", r#"fn main() { println!("hi"); }"#)
226///     .with_deps(r#"serde = "1""#)
227///     .with_env("RUST_BACKTRACE", "1")
228///     .with_release(false);
229/// assert_eq!(task.tag, "hello");
230/// ```
231#[derive(Clone)]
232pub struct DnyTask {
233    /// 任务标识,用于结果回调用例区分;`From` 元组缺省时为 `"unnamed_task"`。
234    pub tag: String,
235    /// 待编译运行的 `main.rs` 源码。
236    pub main_code: String,
237    /// 追加到临时工程 `[dependencies]` 的依赖声明片段。
238    pub deps: String,
239    /// 注入子进程的环境变量。
240    pub envs: HashMap<String, String>,
241    /// 写入临时工程 `.cargo/config.toml` 的配置,无则为 `None`。
242    pub cargo_config: Option<String>,
243    /// 是否以 `--release` 编译运行。
244    pub is_release: bool,
245}
246
247impl DnyTask {
248    /// 以标识与 `main.rs` 源码创建任务,其余配置取默认值。
249    ///
250    /// # Feature Requirement
251    ///
252    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
253    ///
254    /// # Examples
255    ///
256    /// ```rust
257    /// use lib_unknown::dyntest::DnyTask;
258    ///
259    /// let task = DnyTask::new("t", "fn main() {}");
260    /// assert_eq!(task.tag, "t");
261    /// ```
262    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    /// 设置 `[dependencies]` 依赖声明片段。
273    ///
274    /// # Feature Requirement
275    ///
276    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
277    pub fn with_deps(mut self, deps: impl Into<String>) -> Self {
278        self.deps = deps.into();
279        self
280    }
281    /// 注入单个环境变量。
282    ///
283    /// # Feature Requirement
284    ///
285    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
286    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    /// 批量注入环境变量。
291    ///
292    /// # Feature Requirement
293    ///
294    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
295    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    /// 设置写入临时工程 `.cargo/config.toml` 的配置。
307    ///
308    /// # Feature Requirement
309    ///
310    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
311    pub fn with_cargo_config(mut self, config: impl Into<String>) -> Self {
312        self.cargo_config = Some(config.into());
313        self
314    }
315    /// 开启或关闭 release 模式。
316    ///
317    /// # Feature Requirement
318    ///
319    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
320    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, // [新增]
335        }
336    }
337}
338
339/// 批量运行器:串行执行多个 [`DnyTask`],可选超时、仅编译、依赖缓存与逐任务回调。
340///
341/// # Feature Requirement
342///
343/// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
344///
345/// # Examples
346///
347/// ```rust,no_run
348/// use lib_unknown::dyntest::{BatchRunner, DnyTask};
349///
350/// let tasks = [DnyTask::new("a", "fn main() {}"), DnyTask::new("b", "fn main() {}")];
351/// let results = BatchRunner::new().only_build(true).run(tasks);
352/// assert_eq!(results.len(), 2);
353/// ```
354pub 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    /// 以默认配置(无超时、编译并运行、不缓存、无回调)创建运行器。
369    ///
370    /// # Feature Requirement
371    ///
372    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
373    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    /// 设置单个任务编译 + 运行的总超时,超时后子进程被终止,结果记为 [`SystemError::Timeout`]。
383    ///
384    /// # Feature Requirement
385    ///
386    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
387    pub fn timeout(mut self, d: Duration) -> Self {
388        self.timeout = Some(d);
389        self
390    }
391    /// 仅编译不运行。
392    ///
393    /// # Feature Requirement
394    ///
395    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
396    pub fn only_build(mut self, b: bool) -> Self {
397        self.only_build = b;
398        self
399    }
400    /// 启用首任务依赖预热缓存(先空跑一次编译,后续任务复用编译缓存)。
401    ///
402    /// # Feature Requirement
403    ///
404    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
405    pub fn use_cache(mut self, c: bool) -> Self {
406        self.use_cache = c;
407        self
408    }
409    /// 设置每个任务完成后的回调(参数为任务标识与结果)。
410    ///
411    /// # Feature Requirement
412    ///
413    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
414    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    /// 串行执行全部任务,返回按输入顺序的 `(标识, 结果)` 列表;空输入直接返回空列表。
431    ///
432    /// # Feature Requirement
433    ///
434    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
435    ///
436    /// # Examples
437    ///
438    /// ```rust,no_run
439    /// use lib_unknown::dyntest::{BatchRunner, DnyTask};
440    ///
441    /// let results = BatchRunner::new().run([DnyTask::new("a", "fn main() {}")]);
442    /// assert!(results[0].1.ok);
443    /// ```
444    ///
445    /// # Panics
446    ///
447    /// - 任务工程目录创建/文件写入失败(磁盘不可写、权限不足)时 panic,源于内部 `DnyRun` 初始化。
448    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); // [新增]
527
528            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
549/// 运行单个动态测试任务的便捷入口(等价于单任务 `BatchRunner`)。
550///
551/// # Feature Requirement
552///
553/// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
554///
555/// # Examples
556///
557/// ```rust,no_run
558/// use lib_unknown::dyntest::dny_run;
559///
560/// let res = dny_run("fn main() {}", "", None, true);
561/// assert!(res.ok);
562/// ```
563///
564/// # Panics
565///
566/// - 任务工程目录创建/文件写入失败时 panic,源于内部 `DnyRun` 初始化。
567pub 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
581/// 批量运行的便捷入口(不使用依赖预热缓存)。
582///
583/// # Feature Requirement
584///
585/// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
586///
587/// # Examples
588///
589/// ```rust,no_run
590/// use lib_unknown::dyntest::{DnyTask, dny_run_batch};
591///
592/// let results = dny_run_batch([DnyTask::new("a", "fn main() {}")], None, None, None);
593/// assert_eq!(results.len(), 1);
594/// ```
595///
596/// # Panics
597///
598/// - 任务工程目录创建/文件写入失败时 panic,源于内部 `DnyRun` 初始化。
599pub 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
620/// 批量运行的便捷入口(启用首任务依赖预热缓存,首个结果为 `"init_cache"`)。
621///
622/// # Feature Requirement
623///
624/// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
625///
626/// # Examples
627///
628/// ```rust,no_run
629/// use lib_unknown::dyntest::{DnyTask, dny_run_batch_use_cache};
630///
631/// let results = dny_run_batch_use_cache([DnyTask::new("a", "fn main() {}")], None, None, None);
632/// assert_eq!(results[0].0, "init_cache");
633/// ```
634///
635/// # Panics
636///
637/// - 任务工程目录创建/文件写入失败时 panic,源于内部 `DnyRun` 初始化。
638pub 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
659/// 单个临时 Cargo 工程的句柄:在 `target/dyn_tests/dny_<id>/` 下生成工程并编译执行,`Drop` 时释放心跳锁。
660///
661/// # Feature Requirement
662///
663/// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
664pub struct DnyRun {
665    /// 待编译运行的 `main.rs` 源码。
666    pub main_code: String,
667    /// `[dependencies]` 依赖声明片段。
668    pub deps: String,
669    dir: PathBuf,
670    project_name: String,
671    lock: DynTestLock,
672    envs: HashMap<String, String>,
673    cargo_config: Option<String>,
674    /// 是否以 `--release` 编译运行。
675    pub is_release: bool,
676}
677
678impl DnyRun {
679    /// 在 `target/dyn_tests` 下创建以纳秒时间戳 + 随机数命名的临时工程并同步文件。
680    ///
681    /// # Feature Requirement
682    ///
683    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
684    ///
685    /// # Examples
686    ///
687    /// ```rust,no_run
688    /// use lib_unknown::dyntest::DnyRun;
689    ///
690    /// let runner = DnyRun::new("fn main() {}", "");
691    /// let res = runner.build(None);
692    /// assert!(res.ok);
693    /// ```
694    ///
695    /// # Panics
696    ///
697    /// - 系统时钟早于 UNIX 纪元、无法获取当前目录或工程目录/文件创建失败时 panic。
698    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    /// 设置是否以 `--release` 编译运行。
732    ///
733    /// # Feature Requirement
734    ///
735    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
736    pub fn release(&mut self, release: bool) -> &mut Self {
737        self.is_release = release;
738        self
739    }
740
741    /// 注入单个子进程环境变量。
742    ///
743    /// # Feature Requirement
744    ///
745    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
746    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    /// 批量注入子进程环境变量。
752    ///
753    /// # Feature Requirement
754    ///
755    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
756    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    /// 清空已注入的环境变量。
769    ///
770    /// # Feature Requirement
771    ///
772    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
773    pub fn clear_envs(&mut self) -> &mut Self {
774        self.envs.clear();
775        self
776    }
777
778    /// 设置 `.cargo/config.toml` 内容并立即同步到工程目录。
779    ///
780    /// # Feature Requirement
781    ///
782    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
783    ///
784    /// # Panics
785    ///
786    /// - `.cargo` 目录或配置文件写入失败时 panic。
787    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    /// 清除 cargo 配置(内存值置空并删除已写入的配置文件,忽略删除失败)。
794    ///
795    /// # Feature Requirement
796    ///
797    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
798    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    /// 将当前 `Cargo.toml` / `main.rs` / cargo 配置同步到工程目录。
819    ///
820    /// # Feature Requirement
821    ///
822    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
823    ///
824    /// # Panics
825    ///
826    /// - 文件写入失败(磁盘不可写、权限不足)时 panic。
827    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    /// 执行 `cargo build`(`is_release` 为真时加 `--release`),超时则终止并记为 [`SystemError::Timeout`];自身不 panic,失败均体现在返回的 [`DnyResult`] 中。
916    ///
917    /// # Feature Requirement
918    ///
919    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
920    ///
921    /// # Examples
922    ///
923    /// ```rust,no_run
924    /// use lib_unknown::dyntest::DnyRun;
925    ///
926    /// let res = DnyRun::new("fn main() {}", "").build(None);
927    /// assert!(res.ok);
928    /// ```
929    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    /// 先编译后直接运行产物;编译失败时直接返回编译结果,不启动进程。
960    ///
961    /// # Feature Requirement
962    ///
963    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
964    ///
965    /// # Examples
966    ///
967    /// ```rust,no_run
968    /// use lib_unknown::dyntest::DnyRun;
969    ///
970    /// let res = DnyRun::new(r#"fn main() { println!("hi"); }"#, "").run(None);
971    /// assert!(res.ok);
972    /// ```
973    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    /// 计算当前配置下产物可执行文件的预期路径(不保证文件已存在)。
982    ///
983    /// 路径由以下因素决定:
984    /// - 工程目录下的 `target` 子目录:若 `.cargo/config.toml` 中配置了
985    ///   `[build]` 段下以 `target` 开头的键(如 `target` 或 `target-dir`),
986    ///   则按配置值拼接实际输出目录;
987    /// - [`is_release`](Self::is_release) 决定使用 `release` 还是 `debug` profile 子目录;
988    /// - 当前运行平台决定可执行文件后缀(Windows 为 `.exe`,否则无后缀)。
989    ///
990    /// # Feature Requirement
991    ///
992    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
993    ///
994    /// # Examples
995    ///
996    /// ```rust,no_run
997    /// use lib_unknown::dyntest::DnyRun;
998    ///
999    /// let runner = DnyRun::new("fn main() {}", "");
1000    /// let build_res = runner.build(None);
1001    /// assert!(build_res.ok);
1002    ///
1003    /// let path = runner.bin_path();
1004    /// assert!(path.exists(), "产物路径应在编译成功后存在: {}", path.display());
1005    /// ```
1006    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    /// 不重新编译,直接运行已有产物(产物缺失时返回 [`SystemError::ProcessSpawnFailed`] 结果)。
1049    /// `build_result` 用于透传编译耗时,无则置零。
1050    ///
1051    /// # Feature Requirement
1052    ///
1053    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
1054    ///
1055    /// # Examples
1056    ///
1057    /// ```rust,no_run
1058    /// use lib_unknown::dyntest::DnyRun;
1059    ///
1060    /// let runner = DnyRun::new("fn main() {}", "");
1061    /// let build_res = runner.build(None);
1062    /// assert!(build_res.ok);
1063    ///
1064    /// let res = runner.run_no_build(None, None);
1065    /// assert!(res.ok);
1066    /// ```
1067    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    /// 在工程目录下执行自定义 `cargo <args>` 命令(如 `cargo test`、`cargo check`、
1110    /// `cargo clippy` 等),超时则终止并记为 [`SystemError::Timeout`];
1111    /// 自身不 panic,失败均体现在返回的 [`DnyResult`] 中。
1112    ///
1113    /// 耗时记为 [`DnyResult::run_duration`],[`DnyResult::build_duration`] 恒为 `Duration::ZERO`。
1114    ///
1115    /// # Note: 不透传 `is_release`
1116    ///
1117    /// 与 [`build`](Self::build) 不同,本方法**不会**根据 `is_release` 自动追加
1118    /// `--release`,`args` 将原样透传给 `cargo`。需要 release 行为时请调用者显式传入:
1119    /// `runner.cargo(&["test", "--release"], None)`。
1120    ///
1121    /// # 错误语义
1122    ///
1123    /// - `cargo` 进程启动失败 → [`SystemError::ProcessSpawnFailed`];
1124    /// - 超时被终止 → [`SystemError::Timeout`];
1125    /// - 正常退出但退出码非零(如 `check`/`test` 未通过)→ [`SystemError::CargoFailed`],
1126    ///   此时 [`DnyResult::ok`] 为 `false`,`stdout`/`stderr`/`exit_code` 保留子进程输出供诊断;
1127    /// - 退出码为 0 → `ok == true`,`system_err == SystemError::None`。
1128    ///
1129    /// # Feature Requirement
1130    ///
1131    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
1132    ///
1133    /// # Examples
1134    ///
1135    /// ```rust,no_run
1136    /// use lib_unknown::dyntest::DnyRun;
1137    ///
1138    /// let runner = DnyRun::new("fn main() {}", "");
1139    /// let res = runner.cargo(&["check"], None);
1140    /// assert!(res.ok);
1141    ///
1142    /// // release 行为不会自动追加,需显式传入:
1143    /// let res = runner.cargo(&["test", "--release"], None);
1144    /// assert!(res.ok);
1145    /// ```
1146    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    /// 替换 `main.rs` 内容并同步到工程目录(供缓存复用时复写任务代码)。
1172    ///
1173    /// # Feature Requirement
1174    ///
1175    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
1176    ///
1177    /// # Panics
1178    ///
1179    /// - 文件写入失败时 panic。
1180    pub fn reset_main_code(&mut self, main_code: &str) {
1181        self.main_code = main_code.to_string();
1182        self.sync_files();
1183    }
1184    /// 替换依赖声明并同步到工程目录。
1185    ///
1186    /// # Feature Requirement
1187    ///
1188    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
1189    ///
1190    /// # Panics
1191    ///
1192    /// - 文件写入失败时 panic。
1193    pub fn reset_deps(&mut self, deps: &str) {
1194        self.deps = deps.to_string();
1195        self.sync_files();
1196    }
1197}
1198
1199/// 工程心跳锁:在 `target/dyn_tests/lock/` 下创建锁文件并由后台线程每 500ms 续写,`Drop` 时停线程删文件,用于识别僵尸工程。
1200///
1201/// # Feature Requirement
1202///
1203/// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
1204pub struct DynTestLock {
1205    lock_path: PathBuf,
1206    stop_signal: Arc<AtomicBool>,
1207    thread_handle: Option<thread::JoinHandle<()>>,
1208}
1209
1210impl DynTestLock {
1211    /// 创建锁文件并启动心跳线程。
1212    ///
1213    /// # Feature Requirement
1214    ///
1215    /// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
1216    ///
1217    /// # Panics
1218    ///
1219    /// - 无法获取当前目录、锁目录/锁文件创建失败,或心跳线程无法启动时 panic。
1220    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
1271/// 清理全部动态测试工程:等待活跃锁释放(心跳停滞超 1500ms 的视为僵尸并清除其锁),再删除 `target/dyn_tests` 目录。
1272///
1273/// # Feature Requirement
1274///
1275/// 需要同时启用 `"dyntest"` 与 `"std"` 特性。
1276///
1277/// # Examples
1278///
1279/// ```rust,no_run
1280/// use lib_unknown::dyntest::clear_dny_project;
1281///
1282/// clear_dny_project(None);
1283/// ```
1284///
1285/// # Panics
1286///
1287/// - 无法获取当前目录时 panic;超时仅打印警告,不 panic。
1288pub 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}