1use serde::{Deserialize, Serialize};
2
3mod generic;
4mod language;
5mod route;
6mod util;
7
8use route::{DetectedKind, route};
9use util::{CompressionCandidate, guard_expansion};
10
11#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
12#[serde(rename_all = "lowercase")]
13#[value(rename_all = "lowercase")]
14pub enum CompressionMode {
15 Off,
16 #[default]
17 Route,
18 Errors,
19 Tests,
20 Logs,
21 Git,
22 Json,
23 Summary,
24}
25
26impl CompressionMode {
27 pub fn as_str(self) -> &'static str {
28 match self {
29 Self::Off => "off",
30 Self::Route => "route",
31 Self::Errors => "errors",
32 Self::Tests => "tests",
33 Self::Logs => "logs",
34 Self::Git => "git",
35 Self::Json => "json",
36 Self::Summary => "summary",
37 }
38 }
39}
40
41#[derive(Debug)]
42pub struct CompressionInput<'a> {
43 pub command: &'a [String],
44 pub stdout: &'a str,
45 pub stderr: &'a str,
46 pub stdout_original_bytes: u64,
47 pub stderr_original_bytes: u64,
48 pub mode: CompressionMode,
49}
50
51pub fn resolve_cli_mode(
52 compress: Option<CompressionMode>,
53 rtk: Option<CompressionMode>,
54) -> Result<Option<CompressionMode>, String> {
55 match (compress, rtk) {
56 (Some(a), Some(b)) if a != b => Err(format!(
57 "--compress {} conflicts with --rtk {}",
58 a.as_str(),
59 b.as_str()
60 )),
61 (Some(a), _) | (_, Some(a)) => Ok(Some(a)),
62 (None, None) => Ok(None),
63 }
64}
65
66pub fn compress(input: CompressionInput<'_>) -> Option<crate::schema::CompressionData> {
67 if input.mode == CompressionMode::Off {
68 return None;
69 }
70
71 let kind = if input.mode == CompressionMode::Route {
72 route(input.command, input.stdout, input.stderr).kind
73 } else {
74 mode_kind(input.mode)
75 };
76
77 let candidate = language::compress_kind(kind, input.stdout, input.stderr)
78 .unwrap_or_else(|| generic::compress_kind(kind, input.stdout, input.stderr));
79 Some(guard_expansion(
80 into_data(candidate, &input, kind),
81 input.stdout,
82 input.stderr,
83 ))
84}
85
86fn into_data(
87 candidate: CompressionCandidate,
88 input: &CompressionInput<'_>,
89 kind: DetectedKind,
90) -> crate::schema::CompressionData {
91 crate::schema::CompressionData {
92 mode: input.mode.as_str().to_string(),
93 applied: true,
94 detected_kind: kind.as_str().to_string(),
95 stdout_compressed_bytes: candidate.stdout.len() as u64,
96 stderr_compressed_bytes: candidate.stderr.len() as u64,
97 stdout_original_bytes: input.stdout_original_bytes,
98 stderr_original_bytes: input.stderr_original_bytes,
99 omitted: candidate.omitted,
100 strategy: candidate.strategy,
101 stdout: candidate.stdout,
102 stderr: candidate.stderr,
103 }
104}
105
106fn mode_kind(mode: CompressionMode) -> DetectedKind {
107 match mode {
108 CompressionMode::Off | CompressionMode::Route => DetectedKind::Summary,
109 CompressionMode::Errors => DetectedKind::Errors,
110 CompressionMode::Tests => DetectedKind::Tests,
111 CompressionMode::Logs => DetectedKind::Logs,
112 CompressionMode::Git => DetectedKind::Git,
113 CompressionMode::Json => DetectedKind::Json,
114 CompressionMode::Summary => DetectedKind::Summary,
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn conflicting_cli_modes_are_rejected() {
124 let err = resolve_cli_mode(Some(CompressionMode::Errors), Some(CompressionMode::Logs))
125 .unwrap_err();
126 assert!(err.contains("conflicts"));
127 }
128
129 #[test]
130 fn logs_mode_deduplicates_lines() {
131 let raw = format!("{}other\n", "same\n".repeat(20));
132 let data = compress(CompressionInput {
133 command: &[],
134 stdout: &raw,
135 stderr: "",
136 stdout_original_bytes: raw.len() as u64,
137 stderr_original_bytes: 0,
138 mode: CompressionMode::Logs,
139 })
140 .unwrap();
141 assert!(data.applied);
142 assert!(data.stdout.contains("repeated 20x"));
143 assert!(data.stdout.len() < raw.len());
144 }
145
146 #[test]
147 fn expansion_guard_suppresses_larger_candidate() {
148 let raw = "same\nsame\nother\n";
149 let data = compress(CompressionInput {
150 command: &[],
151 stdout: raw,
152 stderr: "",
153 stdout_original_bytes: raw.len() as u64,
154 stderr_original_bytes: 0,
155 mode: CompressionMode::Logs,
156 })
157 .unwrap();
158 assert!(!data.applied);
159 assert_eq!(data.stdout, "");
160 assert_eq!(data.stderr, "");
161 assert_eq!(data.stdout_compressed_bytes, 0);
162 assert_eq!(data.stderr_compressed_bytes, 0);
163 assert_eq!(data.strategy, vec!["expansion-guard"]);
164 }
165}