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
139fn 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
147pub trait TestAdapter {
149 fn detect(&self, project_dir: &Path) -> Option<DetectionResult>;
151
152 fn build_command(&self, project_dir: &Path, extra_args: &[String]) -> Result<Command>;
154
155 fn parse_output(&self, stdout: &str, stderr: &str, exit_code: i32) -> TestRunResult;
157
158 fn name(&self) -> &str;
160
161 fn check_runner(&self) -> Option<String> {
163 None }
165}