1use serde::{Deserialize, Serialize};
43use std::fmt;
44
45#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum FailureClass {
52 CorrectnessDefect,
54 GlobalStateFlake,
57 EnvironmentExhaustion,
60 RunnerPerformanceVariance,
63 PlatformCapabilityMismatch,
66 ToolchainDrift,
69 Unknown,
71}
72
73impl FailureClass {
74 #[must_use]
76 pub const fn as_str(&self) -> &'static str {
77 match self {
78 Self::CorrectnessDefect => "correctness_defect",
79 Self::GlobalStateFlake => "global_state_flake",
80 Self::EnvironmentExhaustion => "environment_exhaustion",
81 Self::RunnerPerformanceVariance => "runner_performance_variance",
82 Self::PlatformCapabilityMismatch => "platform_capability_mismatch",
83 Self::ToolchainDrift => "toolchain_drift",
84 Self::Unknown => "unknown",
85 }
86 }
87}
88
89impl fmt::Display for FailureClass {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 f.write_str(self.as_str())
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum Confidence {
99 High,
101 Medium,
103 Low,
105}
106
107impl Confidence {
108 #[must_use]
110 pub const fn as_str(&self) -> &'static str {
111 match self {
112 Self::High => "high",
113 Self::Medium => "medium",
114 Self::Low => "low",
115 }
116 }
117}
118
119impl fmt::Display for Confidence {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 f.write_str(self.as_str())
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum FixClass {
129 SerializeGlobalStateTests,
131 AdvisoryPerfOnNonStrictRunner,
133 CapabilitySkip,
135 DependencyUpdate,
137 CodeFix,
139 ResourceOptimization,
141}
142
143impl FixClass {
144 #[must_use]
146 pub const fn as_str(&self) -> &'static str {
147 match self {
148 Self::SerializeGlobalStateTests => "serialize_global_state_tests",
149 Self::AdvisoryPerfOnNonStrictRunner => "advisory_perf_on_non_strict_runner",
150 Self::CapabilitySkip => "capability_skip",
151 Self::DependencyUpdate => "dependency_update",
152 Self::CodeFix => "code_fix",
153 Self::ResourceOptimization => "resource_optimization",
154 }
155 }
156}
157
158impl fmt::Display for FixClass {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 f.write_str(self.as_str())
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct ClassifiedFailure {
170 pub class: FailureClass,
172 pub confidence: Confidence,
174 pub evidence: Vec<String>,
176 pub suggested_fix_class: Option<FixClass>,
178 pub platform: Option<String>,
180 pub test_name: Option<String>,
182 pub file_path: Option<String>,
184}
185
186impl ClassifiedFailure {
187 #[must_use]
213 pub fn summary(&self) -> String {
214 match self.evidence.first() {
215 Some(first) => {
216 format!("{} ({} confidence): {first}", self.class, self.confidence)
217 }
218 None => {
219 format!("{} ({} confidence)", self.class, self.confidence)
220 }
221 }
222 }
223}
224
225impl fmt::Display for ClassifiedFailure {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 f.write_str(&self.summary())
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
238 fn failure_class_display_matches_as_str() {
239 let variants = [
240 FailureClass::CorrectnessDefect,
241 FailureClass::GlobalStateFlake,
242 FailureClass::EnvironmentExhaustion,
243 FailureClass::RunnerPerformanceVariance,
244 FailureClass::PlatformCapabilityMismatch,
245 FailureClass::ToolchainDrift,
246 FailureClass::Unknown,
247 ];
248 for v in &variants {
249 assert_eq!(v.to_string(), v.as_str());
250 }
251 }
252
253 #[test]
254 fn failure_class_serde_roundtrip() {
255 let original = FailureClass::PlatformCapabilityMismatch;
256 let json = serde_json::to_string(&original).unwrap();
257 assert_eq!(json, r#""platform_capability_mismatch""#);
258 let restored: FailureClass = serde_json::from_str(&json).unwrap();
259 assert_eq!(original, restored);
260 }
261
262 #[test]
263 fn failure_class_all_variants_serde_roundtrip() {
264 let variants = [
265 (FailureClass::CorrectnessDefect, "\"correctness_defect\""),
266 (FailureClass::GlobalStateFlake, "\"global_state_flake\""),
267 (
268 FailureClass::EnvironmentExhaustion,
269 "\"environment_exhaustion\"",
270 ),
271 (
272 FailureClass::RunnerPerformanceVariance,
273 "\"runner_performance_variance\"",
274 ),
275 (
276 FailureClass::PlatformCapabilityMismatch,
277 "\"platform_capability_mismatch\"",
278 ),
279 (FailureClass::ToolchainDrift, "\"toolchain_drift\""),
280 (FailureClass::Unknown, "\"unknown\""),
281 ];
282 for (variant, expected_json) in &variants {
283 let json = serde_json::to_string(variant).unwrap();
284 assert_eq!(
285 &json, expected_json,
286 "serialization mismatch for {variant:?}"
287 );
288 let restored: FailureClass = serde_json::from_str(&json).unwrap();
289 assert_eq!(variant, &restored, "roundtrip mismatch for {variant:?}");
290 }
291 }
292
293 #[test]
296 fn confidence_display_matches_as_str() {
297 let variants = [Confidence::High, Confidence::Medium, Confidence::Low];
298 for v in &variants {
299 assert_eq!(v.to_string(), v.as_str());
300 }
301 }
302
303 #[test]
304 fn confidence_serde_roundtrip() {
305 let original = Confidence::Medium;
306 let json = serde_json::to_string(&original).unwrap();
307 assert_eq!(json, r#""medium""#);
308 let restored: Confidence = serde_json::from_str(&json).unwrap();
309 assert_eq!(original, restored);
310 }
311
312 #[test]
315 fn fix_class_display_matches_as_str() {
316 let variants = [
317 FixClass::SerializeGlobalStateTests,
318 FixClass::AdvisoryPerfOnNonStrictRunner,
319 FixClass::CapabilitySkip,
320 FixClass::DependencyUpdate,
321 FixClass::CodeFix,
322 FixClass::ResourceOptimization,
323 ];
324 for v in &variants {
325 assert_eq!(v.to_string(), v.as_str());
326 }
327 }
328
329 #[test]
330 fn fix_class_serde_roundtrip() {
331 let original = FixClass::SerializeGlobalStateTests;
332 let json = serde_json::to_string(&original).unwrap();
333 assert_eq!(json, r#""serialize_global_state_tests""#);
334 let restored: FixClass = serde_json::from_str(&json).unwrap();
335 assert_eq!(original, restored);
336 }
337
338 #[test]
341 fn summary_with_evidence() {
342 let f = ClassifiedFailure {
343 class: FailureClass::GlobalStateFlake,
344 confidence: Confidence::High,
345 evidence: vec!["test_a and test_b both write to /tmp/shared".into()],
346 suggested_fix_class: Some(FixClass::SerializeGlobalStateTests),
347 platform: Some("ubuntu-latest".into()),
348 test_name: Some("test_concurrent_write".into()),
349 file_path: Some("crates/xchecker-utils/tests/integration.rs".into()),
350 };
351 assert_eq!(
352 f.summary(),
353 "global_state_flake (high confidence): test_a and test_b both write to /tmp/shared"
354 );
355 }
356
357 #[test]
358 fn summary_without_evidence() {
359 let f = ClassifiedFailure {
360 class: FailureClass::Unknown,
361 confidence: Confidence::Low,
362 evidence: vec![],
363 suggested_fix_class: None,
364 platform: None,
365 test_name: None,
366 file_path: None,
367 };
368 assert_eq!(f.summary(), "unknown (low confidence)");
369 }
370
371 #[test]
372 fn display_delegates_to_summary() {
373 let f = ClassifiedFailure {
374 class: FailureClass::ToolchainDrift,
375 confidence: Confidence::Medium,
376 evidence: vec!["clippy 0.1.81 introduced new lint".into()],
377 suggested_fix_class: Some(FixClass::DependencyUpdate),
378 platform: None,
379 test_name: None,
380 file_path: None,
381 };
382 assert_eq!(f.to_string(), f.summary());
383 }
384
385 #[test]
386 fn classified_failure_serde_roundtrip() {
387 let original = ClassifiedFailure {
388 class: FailureClass::EnvironmentExhaustion,
389 confidence: Confidence::High,
390 evidence: vec!["disk usage at 98%".into(), "/tmp ran out of inodes".into()],
391 suggested_fix_class: Some(FixClass::ResourceOptimization),
392 platform: Some("windows-latest".into()),
393 test_name: Some("test_large_packet".into()),
394 file_path: Some("crates/xchecker-engine/tests/packet.rs".into()),
395 };
396
397 let json = serde_json::to_string_pretty(&original).unwrap();
398 let restored: ClassifiedFailure = serde_json::from_str(&json).unwrap();
399
400 assert_eq!(original.class, restored.class);
401 assert_eq!(original.confidence, restored.confidence);
402 assert_eq!(original.evidence, restored.evidence);
403 assert_eq!(original.suggested_fix_class, restored.suggested_fix_class);
404 assert_eq!(original.platform, restored.platform);
405 assert_eq!(original.test_name, restored.test_name);
406 assert_eq!(original.file_path, restored.file_path);
407 }
408
409 #[test]
410 fn classified_failure_optional_fields_absent() {
411 let json = r#"{
412 "class": "correctness_defect",
413 "confidence": "high",
414 "evidence": ["assertion failed in line 42"],
415 "suggested_fix_class": null,
416 "platform": null,
417 "test_name": null,
418 "file_path": null
419 }"#;
420 let f: ClassifiedFailure = serde_json::from_str(json).unwrap();
421 assert_eq!(f.class, FailureClass::CorrectnessDefect);
422 assert!(f.suggested_fix_class.is_none());
423 assert!(f.platform.is_none());
424 }
425
426 #[test]
427 fn summary_uses_first_evidence_only() {
428 let f = ClassifiedFailure {
429 class: FailureClass::RunnerPerformanceVariance,
430 confidence: Confidence::Medium,
431 evidence: vec![
432 "timeout after 5s on shared runner".into(),
433 "passes locally in 0.8s".into(),
434 ],
435 suggested_fix_class: Some(FixClass::AdvisoryPerfOnNonStrictRunner),
436 platform: Some("macos-latest".into()),
437 test_name: None,
438 file_path: None,
439 };
440 assert_eq!(
441 f.summary(),
442 "runner_performance_variance (medium confidence): timeout after 5s on shared runner"
443 );
444 }
445}