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/// Builder for computing detection confidence from weighted signals.
140///
141/// Instead of hardcoded confidence values, each adapter accumulates
142/// signals (config files found, test dirs present, runner available, etc.)
143/// that dynamically determine how confident we are in the detection.
144///
145/// # Example
146/// ```ignore
147/// let confidence = ConfidenceScore::base(0.50)
148///     .signal(0.20, project_dir.join("tests").is_dir())
149///     .signal(0.10, project_dir.join("Cargo.lock").exists())
150///     .signal(0.10, which::which("cargo").is_ok())
151///     .finish();
152/// ```
153pub struct ConfidenceScore {
154    score: f32,
155}
156
157impl ConfidenceScore {
158    /// Start with base confidence from the primary project marker being found.
159    pub fn base(score: f32) -> Self {
160        Self { score }
161    }
162
163    /// Add weight when a confirmatory signal is present.
164    pub fn signal(mut self, weight: f32, present: bool) -> Self {
165        if present {
166            self.score += weight;
167        }
168        self
169    }
170
171    /// Return final confidence clamped to `[0.0, 0.99]`.
172    pub fn finish(self) -> f32 {
173        self.score.clamp(0.0, 0.99)
174    }
175}
176
177/// Serialize a Duration as milliseconds (f64) for clean JSON output.
178fn serialize_duration_ms<S>(d: &Duration, s: S) -> Result<S::Ok, S::Error>
179where
180    S: serde::Serializer,
181{
182    s.serialize_f64(d.as_secs_f64() * 1000.0)
183}
184
185/// Trait that each language adapter must implement
186pub trait TestAdapter {
187    /// Check if this adapter can handle the project at the given path
188    fn detect(&self, project_dir: &Path) -> Option<DetectionResult>;
189
190    /// Build the command to run tests
191    fn build_command(&self, project_dir: &Path, extra_args: &[String]) -> Result<Command>;
192
193    /// Parse stdout/stderr from the test runner into structured results
194    fn parse_output(&self, stdout: &str, stderr: &str, exit_code: i32) -> TestRunResult;
195
196    /// Name of this adapter for display
197    fn name(&self) -> &str;
198
199    /// Check if the required test runner binary is available on PATH
200    fn check_runner(&self) -> Option<String> {
201        None // Default: no check
202    }
203
204    /// Return framework-specific CLI arguments to filter tests by pattern.
205    ///
206    /// Different test frameworks accept filters in different ways:
207    /// - Rust: positional arg (regex)
208    /// - Go: `-run <pattern>`
209    /// - Python: `-k <pattern>`
210    /// - JS: `-t <pattern>`
211    ///
212    /// Returns arguments to append to `extra_args`.
213    fn filter_args(&self, pattern: &str) -> Vec<String> {
214        // Default: pass pattern as a positional argument
215        vec![pattern.to_string()]
216    }
217}