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#[derive(Debug, Clone, PartialEq, serde::Serialize)]
22#[serde(rename_all = "lowercase")]
23pub enum TestStatus {
24 Passed,
25 Failed,
26 Skipped,
27}
28
29#[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 pub error: Option<TestError>,
38}
39
40#[derive(Debug, Clone, PartialEq, serde::Serialize)]
42pub struct TestError {
43 pub message: String,
44 pub location: Option<String>,
45}
46
47#[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 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#[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 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#[derive(Debug, Clone)]
133pub struct DetectionResult {
134 pub language: String,
135 pub framework: String,
136 pub confidence: f32,
137}
138
139pub struct ConfidenceScore {
154 score: f32,
155}
156
157impl ConfidenceScore {
158 pub fn base(score: f32) -> Self {
160 Self { score }
161 }
162
163 pub fn signal(mut self, weight: f32, present: bool) -> Self {
165 if present {
166 self.score += weight;
167 }
168 self
169 }
170
171 pub fn finish(self) -> f32 {
173 self.score.clamp(0.0, 0.99)
174 }
175}
176
177fn 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
185pub trait TestAdapter {
187 fn detect(&self, project_dir: &Path) -> Option<DetectionResult>;
189
190 fn build_command(&self, project_dir: &Path, extra_args: &[String]) -> Result<Command>;
192
193 fn parse_output(&self, stdout: &str, stderr: &str, exit_code: i32) -> TestRunResult;
195
196 fn name(&self) -> &str;
198
199 fn check_runner(&self) -> Option<String> {
201 None }
203
204 fn filter_args(&self, pattern: &str) -> Vec<String> {
214 vec![pattern.to_string()]
216 }
217}