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}