1use crate::stack::types::*;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum BumpType {
11 Patch,
13 Minor,
15 Major,
17}
18
19impl BumpType {
20 pub fn apply(&self, version: &semver::Version) -> semver::Version {
22 match self {
23 BumpType::Patch => {
24 semver::Version::new(version.major, version.minor, version.patch + 1)
25 }
26 BumpType::Minor => semver::Version::new(version.major, version.minor + 1, 0),
27 BumpType::Major => semver::Version::new(version.major + 1, 0, 0),
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
34pub struct ReleaseConfig {
35 pub bump_type: Option<BumpType>,
37
38 pub no_verify: bool,
40
41 pub dry_run: bool,
43
44 pub publish: bool,
46
47 pub min_coverage: f64,
49
50 pub lint_command: String,
52
53 pub coverage_command: String,
55
56 pub comply_command: String,
58
59 pub fail_on_comply_violations: bool,
61
62 pub quality_gate_command: String,
67
68 pub fail_on_quality_gate: bool,
70
71 pub tdg_command: String,
73
74 pub min_tdg_score: f64,
76
77 pub fail_on_tdg: bool,
79
80 pub dead_code_command: String,
82
83 pub fail_on_dead_code: bool,
85
86 pub complexity_command: String,
88
89 pub max_complexity: u32,
91
92 pub fail_on_complexity: bool,
94
95 pub satd_command: String,
97
98 pub max_satd_items: u32,
100
101 pub fail_on_satd: bool,
103
104 pub popper_command: String,
106
107 pub min_popper_score: f64,
109
110 pub fail_on_popper: bool,
112
113 pub book_command: String,
118
119 pub fail_on_book: bool,
121
122 pub examples_command: String,
125
126 pub fail_on_examples: bool,
128}
129
130impl Default for ReleaseConfig {
131 fn default() -> Self {
132 Self {
133 bump_type: None,
134 no_verify: false,
135 dry_run: false,
136 publish: false,
137 min_coverage: 90.0,
138 lint_command: "make lint".to_string(),
139 coverage_command: "make coverage".to_string(),
140 comply_command: "pmat comply".to_string(),
141 fail_on_comply_violations: true,
142 quality_gate_command: "pmat quality-gate".to_string(),
144 fail_on_quality_gate: true,
145 tdg_command: "pmat tdg --format json".to_string(),
146 min_tdg_score: 80.0,
147 fail_on_tdg: true,
148 dead_code_command: "pmat analyze dead-code --format json".to_string(),
149 fail_on_dead_code: false, complexity_command: "pmat analyze complexity --format json".to_string(),
151 max_complexity: 20,
152 fail_on_complexity: true,
153 satd_command: "pmat analyze satd --format json".to_string(),
154 max_satd_items: 10,
155 fail_on_satd: false, popper_command: "pmat popper-score --format json".to_string(),
157 min_popper_score: 60.0,
158 fail_on_popper: true,
159 book_command: "mdbook build book".to_string(),
161 fail_on_book: true,
162 examples_command: "cargo run --example".to_string(),
163 fail_on_examples: true,
164 }
165 }
166}
167
168#[derive(Debug, Clone)]
170pub struct ReleaseResult {
171 pub success: bool,
173
174 pub released_crates: Vec<ReleasedCrate>,
176
177 pub message: String,
179}
180
181#[derive(Debug, Clone)]
183pub struct ReleasedCrate {
184 pub name: String,
186
187 pub version: semver::Version,
189
190 pub published: bool,
192}
193
194pub fn format_plan_text(plan: &ReleasePlan) -> String {
196 let mut output = String::new();
197
198 if plan.dry_run {
199 output.push_str("📋 Release Plan (DRY RUN)\n");
200 } else {
201 output.push_str("📋 Release Plan\n");
202 }
203 output.push_str(&"═".repeat(50));
204 output.push_str("\n\n");
205
206 output.push_str("Release order (topological):\n");
207 for (i, release) in plan.releases.iter().enumerate() {
208 output.push_str(&format!(
209 " {}. {} {} → {}\n",
210 i + 1,
211 release.crate_name,
212 release.current_version,
213 release.new_version
214 ));
215 }
216
217 output.push_str("\nPre-flight status:\n");
218 for release in &plan.releases {
219 let status = plan
220 .preflight_results
221 .get(&release.crate_name)
222 .map(|r| if r.passed { "✓" } else { "✗" })
223 .unwrap_or("?");
224
225 output.push_str(&format!(" {} {}\n", status, release.crate_name));
226 }
227
228 output
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
240 fn test_bump_type_patch() {
241 let version = semver::Version::new(1, 2, 3);
242 let bumped = BumpType::Patch.apply(&version);
243 assert_eq!(bumped, semver::Version::new(1, 2, 4));
244 }
245
246 #[test]
247 fn test_bump_type_minor() {
248 let version = semver::Version::new(1, 2, 3);
249 let bumped = BumpType::Minor.apply(&version);
250 assert_eq!(bumped, semver::Version::new(1, 3, 0));
251 }
252
253 #[test]
254 fn test_bump_type_major() {
255 let version = semver::Version::new(1, 2, 3);
256 let bumped = BumpType::Major.apply(&version);
257 assert_eq!(bumped, semver::Version::new(2, 0, 0));
258 }
259
260 #[test]
261 fn test_bump_type_clone() {
262 let bump = BumpType::Minor;
263 let cloned = bump;
264 assert_eq!(bump, cloned);
265 }
266
267 #[test]
272 fn test_release_config_default() {
273 let config = ReleaseConfig::default();
274 assert!(config.bump_type.is_none());
275 assert!(!config.no_verify);
276 assert!(!config.dry_run);
277 assert!(!config.publish);
278 assert_eq!(config.min_coverage, 90.0);
279 }
280
281 #[test]
282 fn test_release_config_pmat_defaults() {
283 let config = ReleaseConfig::default();
284 assert_eq!(config.quality_gate_command, "pmat quality-gate");
285 assert!(config.fail_on_quality_gate);
286 assert_eq!(config.min_tdg_score, 80.0);
287 assert!(config.fail_on_tdg);
288 assert!(!config.fail_on_dead_code); assert_eq!(config.max_complexity, 20);
290 assert!(config.fail_on_complexity);
291 assert!(!config.fail_on_satd); assert_eq!(config.min_popper_score, 60.0);
293 assert!(config.fail_on_popper);
294 }
295
296 #[test]
297 fn test_release_config_book_defaults() {
298 let config = ReleaseConfig::default();
299 assert_eq!(config.book_command, "mdbook build book");
300 assert!(config.fail_on_book);
301 assert_eq!(config.examples_command, "cargo run --example");
302 assert!(config.fail_on_examples);
303 }
304
305 #[test]
306 fn test_release_config_custom_thresholds() {
307 let config = ReleaseConfig {
308 min_tdg_score: 90.0,
309 min_popper_score: 70.0,
310 max_complexity: 15,
311 max_satd_items: 5,
312 fail_on_dead_code: true,
313 fail_on_satd: true,
314 ..Default::default()
315 };
316
317 assert_eq!(config.min_tdg_score, 90.0);
318 assert_eq!(config.min_popper_score, 70.0);
319 assert_eq!(config.max_complexity, 15);
320 assert_eq!(config.max_satd_items, 5);
321 assert!(config.fail_on_dead_code);
322 assert!(config.fail_on_satd);
323 }
324
325 #[test]
326 fn test_release_config_disabled_checks() {
327 let config = ReleaseConfig {
328 quality_gate_command: String::new(),
329 tdg_command: String::new(),
330 dead_code_command: String::new(),
331 complexity_command: String::new(),
332 satd_command: String::new(),
333 popper_command: String::new(),
334 ..Default::default()
335 };
336
337 assert!(config.quality_gate_command.is_empty());
338 assert!(config.tdg_command.is_empty());
339 assert!(config.dead_code_command.is_empty());
340 assert!(config.complexity_command.is_empty());
341 assert!(config.satd_command.is_empty());
342 assert!(config.popper_command.is_empty());
343 }
344
345 #[test]
350 fn test_release_result_success() {
351 let result = ReleaseResult {
352 success: true,
353 released_crates: vec![ReleasedCrate {
354 name: "test".to_string(),
355 version: semver::Version::new(1, 0, 0),
356 published: true,
357 }],
358 message: "Success".to_string(),
359 };
360 assert!(result.success);
361 assert_eq!(result.released_crates.len(), 1);
362 }
363
364 #[test]
365 fn test_released_crate_clone() {
366 let crate_info = ReleasedCrate {
367 name: "test".to_string(),
368 version: semver::Version::new(1, 0, 0),
369 published: true,
370 };
371 let cloned = crate_info.clone();
372 assert_eq!(crate_info.name, cloned.name);
373 assert_eq!(crate_info.version, cloned.version);
374 assert_eq!(crate_info.published, cloned.published);
375 }
376}