Skip to main content

cargo_mate/tools/
cross_test.rs

1use super::{Tool, Result, ToolError, common_options, parse_output_format, OutputFormat};
2use clap::{Arg, ArgMatches, Command};
3use colored::*;
4use std::path::Path;
5use std::process::Command as ProcessCommand;
6use std::collections::HashMap;
7use serde::{Deserialize, Serialize};
8#[derive(Debug, Clone)]
9pub struct CrossTestTool;
10#[derive(Debug, Clone, Deserialize, Serialize)]
11struct TestResult {
12    platform: String,
13    success: bool,
14    duration: Option<f64>,
15    output: String,
16    errors: Vec<String>,
17}
18#[derive(Debug, Deserialize, Serialize)]
19struct CrossTestReport {
20    test_name: String,
21    platforms: Vec<TestResult>,
22    summary: TestSummary,
23}
24#[derive(Debug, Clone, Deserialize, Serialize)]
25struct TestSummary {
26    total_platforms: usize,
27    successful: usize,
28    failed: usize,
29    total_duration: f64,
30    fastest_platform: Option<String>,
31    slowest_platform: Option<String>,
32}
33impl CrossTestTool {
34    pub fn new() -> Self {
35        Self
36    }
37    fn get_supported_platforms(&self) -> Vec<String> {
38        vec![
39            "x86_64-unknown-linux-gnu".to_string(), "x86_64-apple-darwin".to_string(),
40            "aarch64-apple-darwin".to_string(), "x86_64-pc-windows-msvc".to_string(),
41            "aarch64-unknown-linux-gnu".to_string(),
42        ]
43    }
44    fn detect_current_platform(&self) -> String {
45        let target = std::env::var("TARGET")
46            .unwrap_or_else(|_| {
47                format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS)
48            });
49        target
50    }
51    fn run_tests_for_platform(
52        &self,
53        platform: &str,
54        test_filter: Option<&str>,
55        verbose: bool,
56    ) -> Result<TestResult> {
57        println!("๐Ÿงช Testing on platform: {}", platform.cyan());
58        let start_time = std::time::Instant::now();
59        let mut cmd = ProcessCommand::new("cargo");
60        cmd.arg("test");
61        if platform != "current" && platform != self.detect_current_platform() {
62            cmd.arg("--target").arg(platform);
63        }
64        if let Some(filter) = test_filter {
65            cmd.arg(filter);
66        }
67        if verbose {
68            cmd.arg("--").arg("--nocapture");
69        } else {
70            cmd.arg("--quiet");
71        }
72        let output = cmd
73            .output()
74            .map_err(|e| ToolError::ExecutionFailed(
75                format!("Failed to run tests on {}: {}", platform, e),
76            ))?;
77        let duration = start_time.elapsed().as_secs_f64();
78        let success = output.status.success();
79        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
80        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
81        let mut errors = Vec::new();
82        if !success {
83            for line in stderr.lines() {
84                if line.contains("error") || line.contains("FAILED") {
85                    errors.push(line.to_string());
86                }
87            }
88        }
89        let combined_output = if verbose {
90            format!("{}\n{}", stdout, stderr)
91        } else {
92            if success {
93                "Tests passed successfully".to_string()
94            } else {
95                stderr.to_string()
96            }
97        };
98        Ok(TestResult {
99            platform: platform.to_string(),
100            success,
101            duration: Some(duration),
102            output: combined_output,
103            errors,
104        })
105    }
106    fn run_cross_platform_tests(
107        &self,
108        platforms: &[String],
109        test_filter: Option<&str>,
110        parallel: bool,
111        verbose: bool,
112    ) -> Result<Vec<TestResult>> {
113        let mut results = Vec::new();
114        if parallel {
115            println!("๐Ÿš€ Running tests in parallel across platforms");
116        }
117        for platform in platforms {
118            match self.run_tests_for_platform(platform, test_filter, verbose) {
119                Ok(result) => results.push(result),
120                Err(e) => {
121                    println!("โŒ Failed to test on {}: {}", platform.red(), e);
122                    results
123                        .push(TestResult {
124                            platform: platform.clone(),
125                            success: false,
126                            duration: None,
127                            output: format!("Test execution failed: {}", e),
128                            errors: vec![e.to_string()],
129                        });
130                }
131            }
132        }
133        Ok(results)
134    }
135    fn generate_summary(&self, results: &[TestResult]) -> TestSummary {
136        let total_platforms = results.len();
137        let successful = results.iter().filter(|r| r.success).count();
138        let failed = total_platforms - successful;
139        let total_duration: f64 = results.iter().filter_map(|r| r.duration).sum();
140        let mut fastest = None;
141        let mut slowest = None;
142        let mut min_duration = f64::INFINITY;
143        let mut max_duration = 0.0;
144        for result in results {
145            if let Some(duration) = result.duration {
146                if duration < min_duration {
147                    min_duration = duration;
148                    fastest = Some(result.platform.clone());
149                }
150                if duration > max_duration {
151                    max_duration = duration;
152                    slowest = Some(result.platform.clone());
153                }
154            }
155        }
156        TestSummary {
157            total_platforms,
158            successful,
159            failed,
160            total_duration,
161            fastest_platform: fastest,
162            slowest_platform: slowest,
163        }
164    }
165    fn display_results(
166        &self,
167        results: &[TestResult],
168        summary: &TestSummary,
169        format: OutputFormat,
170        verbose: bool,
171    ) -> Result<()> {
172        match format {
173            OutputFormat::Json => {
174                let report = CrossTestReport {
175                    test_name: "cross-platform-tests".to_string(),
176                    platforms: results.to_vec(),
177                    summary: (*summary).clone(),
178                };
179                println!("{}", serde_json::to_string_pretty(& report).unwrap());
180            }
181            OutputFormat::Table => {
182                println!(
183                    "{:<25} {:<10} {:<12} {:<15}", "Platform", "Status", "Duration",
184                    "Errors"
185                );
186                println!("{}", "โ”€".repeat(65));
187                for result in results {
188                    let status = if result.success {
189                        "โœ… PASS".green().to_string()
190                    } else {
191                        "โŒ FAIL".red().to_string()
192                    };
193                    let duration = result
194                        .duration
195                        .map(|d| format!("{:.2}s", d))
196                        .unwrap_or("N/A".to_string());
197                    let error_count = result.errors.len().to_string();
198                    println!(
199                        "{:<25} {:<10} {:<12} {:<15}", result.platform, status, duration,
200                        error_count
201                    );
202                }
203            }
204            OutputFormat::Human => {
205                println!("{}", "๐ŸŒ Cross-Platform Test Results".bold().blue());
206                println!("{}", "โ•".repeat(50).blue());
207                println!("๐Ÿ“Š Summary:");
208                println!("   Platforms tested: {}", summary.total_platforms);
209                println!("   โœ… Passed: {}", summary.successful.to_string().green());
210                println!("   โŒ Failed: {}", summary.failed.to_string().red());
211                println!("   โฑ๏ธ  Total time: {:.2}s", summary.total_duration);
212                if let Some(fastest) = &summary.fastest_platform {
213                    println!("   ๐Ÿƒ Fastest: {}", fastest.cyan());
214                }
215                if let Some(slowest) = &summary.slowest_platform {
216                    println!("   ๐ŸŒ Slowest: {}", slowest.yellow());
217                }
218                if verbose {
219                    println!("\n๐Ÿ“‹ Detailed Results:");
220                    for result in results {
221                        println!(
222                            "\n{}: {}", result.platform.bold(), if result.success {
223                            "PASSED".green() } else { "FAILED".red() }
224                        );
225                        if let Some(duration) = result.duration {
226                            println!("   Duration: {:.2}s", duration);
227                        }
228                        if !result.errors.is_empty() {
229                            println!("   Errors:");
230                            for error in &result.errors {
231                                println!("     {}", error.red());
232                            }
233                        }
234                        if verbose && !result.output.is_empty() {
235                            println!("   Output:");
236                            for line in result.output.lines() {
237                                println!("     {}", line);
238                            }
239                        }
240                    }
241                }
242            }
243        }
244        Ok(())
245    }
246    fn validate_platforms(&self, requested_platforms: &[String]) -> Result<Vec<String>> {
247        let supported = self.get_supported_platforms();
248        let mut valid_platforms = Vec::new();
249        for platform in requested_platforms {
250            if platform == "current" {
251                valid_platforms.push(self.detect_current_platform());
252            } else if supported.contains(platform) {
253                valid_platforms.push(platform.clone());
254            } else {
255                println!(
256                    "โš ๏ธ  Platform {} not fully supported, will attempt anyway",
257                    platform.yellow()
258                );
259                valid_platforms.push(platform.clone());
260            }
261        }
262        Ok(valid_platforms)
263    }
264}
265impl Tool for CrossTestTool {
266    fn name(&self) -> &'static str {
267        "cross-test"
268    }
269    fn description(&self) -> &'static str {
270        "Run tests across different platforms and architectures"
271    }
272    fn command(&self) -> Command {
273        Command::new(self.name())
274            .about(self.description())
275            .long_about(
276                "Test your Rust code across multiple platforms using Docker or native compilation. Helps catch platform-specific bugs early.",
277            )
278            .args(
279                &[
280                    Arg::new("platforms")
281                        .long("platforms")
282                        .short('p')
283                        .help("Comma-separated list of platforms to test")
284                        .default_value("current"),
285                    Arg::new("test-filter")
286                        .long("test-filter")
287                        .short('f')
288                        .help("Filter tests by name pattern"),
289                    Arg::new("docker")
290                        .long("docker")
291                        .help("Use Docker for cross-platform testing")
292                        .action(clap::ArgAction::SetTrue),
293                    Arg::new("parallel")
294                        .long("parallel")
295                        .help("Run tests in parallel (experimental)")
296                        .action(clap::ArgAction::SetTrue),
297                    Arg::new("list-platforms")
298                        .long("list-platforms")
299                        .help("List supported platforms")
300                        .action(clap::ArgAction::SetTrue),
301                    Arg::new("report")
302                        .long("report")
303                        .help("Generate detailed test report")
304                        .action(clap::ArgAction::SetTrue),
305                    Arg::new("failing-only")
306                        .long("failing-only")
307                        .help("Only show results for failed platforms")
308                        .action(clap::ArgAction::SetTrue),
309                ],
310            )
311            .args(&common_options())
312    }
313    fn execute(&self, matches: &ArgMatches) -> Result<()> {
314        let platforms_str = matches.get_one::<String>("platforms").unwrap();
315        let test_filter = matches.get_one::<String>("test-filter");
316        let docker = matches.get_flag("docker");
317        let parallel = matches.get_flag("parallel");
318        let list_platforms = matches.get_flag("list-platforms");
319        let report = matches.get_flag("report");
320        let failing_only = matches.get_flag("failing-only");
321        let output_format = parse_output_format(matches);
322        let verbose = matches.get_flag("verbose");
323        let dry_run = matches.get_flag("dry-run");
324        if list_platforms {
325            println!("{}", "Supported Platforms:".bold().blue());
326            for platform in &self.get_supported_platforms() {
327                println!("  โ€ข {}", platform.cyan());
328            }
329            println!("\n๐Ÿ’ก Use 'current' to test on your current platform");
330            return Ok(());
331        }
332        if docker {
333            println!("๐Ÿณ Docker support not yet implemented");
334            println!("   This would run tests in Docker containers for each platform");
335            return Ok(());
336        }
337        println!(
338            "๐ŸŒ {} - Running cross-platform tests", "CargoMate CrossTest".bold().blue()
339        );
340        let requested_platforms: Vec<String> = if platforms_str == "current" {
341            vec!["current".to_string()]
342        } else {
343            platforms_str.split(',').map(|s| s.trim().to_string()).collect()
344        };
345        let platforms = self.validate_platforms(&requested_platforms)?;
346        if dry_run {
347            println!("๐Ÿ” Dry run - would test on platforms: {:?}", platforms);
348            if let Some(filter) = test_filter {
349                println!("   Test filter: {}", filter);
350            }
351            return Ok(());
352        }
353        println!(
354            "๐Ÿงช Testing on {} platform(s): {}", platforms.len(), platforms.join(", ")
355            .cyan()
356        );
357        let results = self
358            .run_cross_platform_tests(
359                &platforms,
360                test_filter.map(|s| s.as_str()),
361                parallel,
362                verbose,
363            )?;
364        let summary = self.generate_summary(&results);
365        let display_results = if failing_only {
366            results.iter().filter(|r| !r.success).cloned().collect::<Vec<_>>()
367        } else {
368            results.clone()
369        };
370        self.display_results(&display_results, &summary, output_format, verbose)?;
371        if summary.failed > 0 {
372            println!(
373                "\nโš ๏ธ  {} platform(s) had test failures", summary.failed.to_string()
374                .yellow()
375            );
376            println!("   Use --verbose to see detailed error output");
377        } else {
378            println!("\n๐ŸŽ‰ All platforms passed tests successfully!");
379        }
380        Ok(())
381    }
382}
383impl Default for CrossTestTool {
384    fn default() -> Self {
385        Self::new()
386    }
387}