Skip to main content

testx/adapters/
mod.rs

1use std::path::Path;
2use std::process::Command;
3use std::time::Duration;
4
5use anyhow::Result;
6
7pub mod cpp;
8pub mod dotnet;
9pub mod elixir;
10pub mod go;
11pub mod java;
12pub mod javascript;
13pub mod php;
14pub mod python;
15pub mod ruby;
16pub mod rust;
17pub mod util;
18pub mod zig;
19
20/// Status of a single test case
21#[derive(Debug, Clone, PartialEq, serde::Serialize)]
22#[serde(rename_all = "lowercase")]
23pub enum TestStatus {
24    Passed,
25    Failed,
26    Skipped,
27}
28
29/// A single test case result
30#[derive(Debug, Clone, serde::Serialize)]
31pub struct TestCase {
32    pub name: String,
33    pub status: TestStatus,
34    #[serde(serialize_with = "serialize_duration_ms")]
35    pub duration: Duration,
36    /// Error message + location if failed
37    pub error: Option<TestError>,
38}
39
40/// Error details for a failed test
41#[derive(Debug, Clone, PartialEq, serde::Serialize)]
42pub struct TestError {
43    pub message: String,
44    pub location: Option<String>,
45}
46
47/// A group of test cases (typically a file or class)
48#[derive(Debug, Clone, serde::Serialize)]
49pub struct TestSuite {
50    pub name: String,
51    pub tests: Vec<TestCase>,
52}
53
54impl TestSuite {
55    pub fn passed(&self) -> usize {
56        self.tests
57            .iter()
58            .filter(|t| t.status == TestStatus::Passed)
59            .count()
60    }
61
62    pub fn failed(&self) -> usize {
63        self.tests
64            .iter()
65            .filter(|t| t.status == TestStatus::Failed)
66            .count()
67    }
68
69    pub fn skipped(&self) -> usize {
70        self.tests
71            .iter()
72            .filter(|t| t.status == TestStatus::Skipped)
73            .count()
74    }
75
76    /// Returns all failed test cases with their error details
77    pub fn failures(&self) -> Vec<&TestCase> {
78        self.tests
79            .iter()
80            .filter(|t| t.status == TestStatus::Failed)
81            .collect()
82    }
83
84    pub fn is_passed(&self) -> bool {
85        self.failed() == 0
86    }
87}
88
89/// Complete result of a test run
90#[derive(Debug, Clone, serde::Serialize)]
91pub struct TestRunResult {
92    pub suites: Vec<TestSuite>,
93    #[serde(serialize_with = "serialize_duration_ms")]
94    pub duration: Duration,
95    pub raw_exit_code: i32,
96}
97
98impl TestRunResult {
99    pub fn total_passed(&self) -> usize {
100        self.suites.iter().map(|s| s.passed()).sum()
101    }
102
103    pub fn total_failed(&self) -> usize {
104        self.suites.iter().map(|s| s.failed()).sum()
105    }
106
107    pub fn total_skipped(&self) -> usize {
108        self.suites.iter().map(|s| s.skipped()).sum()
109    }
110
111    pub fn total_tests(&self) -> usize {
112        self.suites.iter().map(|s| s.tests.len()).sum()
113    }
114
115    pub fn is_success(&self) -> bool {
116        self.total_failed() == 0
117    }
118
119    /// Get all tests sorted by duration (slowest first)
120    pub fn slowest_tests(&self, n: usize) -> Vec<(&TestSuite, &TestCase)> {
121        let mut all: Vec<_> = self
122            .suites
123            .iter()
124            .flat_map(|s| s.tests.iter().map(move |t| (s, t)))
125            .collect();
126        all.sort_by(|a, b| b.1.duration.cmp(&a.1.duration));
127        all.into_iter().take(n).collect()
128    }
129}
130
131/// What was detected about a project
132#[derive(Debug, Clone)]
133pub struct DetectionResult {
134    pub language: String,
135    pub framework: String,
136    pub confidence: f32,
137}
138
139/// Serialize a Duration as milliseconds (f64) for clean JSON output.
140fn serialize_duration_ms<S>(d: &Duration, s: S) -> Result<S::Ok, S::Error>
141where
142    S: serde::Serializer,
143{
144    s.serialize_f64(d.as_secs_f64() * 1000.0)
145}
146
147/// Trait that each language adapter must implement
148pub trait TestAdapter {
149    /// Check if this adapter can handle the project at the given path
150    fn detect(&self, project_dir: &Path) -> Option<DetectionResult>;
151
152    /// Build the command to run tests
153    fn build_command(&self, project_dir: &Path, extra_args: &[String]) -> Result<Command>;
154
155    /// Parse stdout/stderr from the test runner into structured results
156    fn parse_output(&self, stdout: &str, stderr: &str, exit_code: i32) -> TestRunResult;
157
158    /// Name of this adapter for display
159    fn name(&self) -> &str;
160
161    /// Check if the required test runner binary is available on PATH
162    fn check_runner(&self) -> Option<String> {
163        None // Default: no check
164    }
165}