1use super::config::{OptLevel, RunnerConfig};
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use std::time::Duration;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum BuildStatus {
13 Pending,
15 Building,
17 Success,
19 Failed,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub enum BuildEvent {
26 Started {
28 timestamp: std::time::SystemTime,
30 },
31 Compiling {
33 crate_name: String,
35 version: String,
37 },
38 Finished {
40 success: bool,
42 duration: Duration,
44 },
45 Error {
47 message: String,
49 },
50 Optimizing {
52 step: String,
54 },
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct BuildResult {
60 pub status: BuildStatus,
62 pub size: Option<u64>,
64 pub gzip_size: Option<u64>,
66 pub duration: Option<Duration>,
68 pub errors: Vec<String>,
70 pub warnings: Vec<String>,
72 pub output_path: Option<PathBuf>,
74}
75
76impl BuildResult {
77 #[must_use]
79 pub fn success(size: u64, duration: Duration) -> Self {
80 Self {
81 status: BuildStatus::Success,
82 size: Some(size),
83 gzip_size: None,
84 duration: Some(duration),
85 errors: Vec::new(),
86 warnings: Vec::new(),
87 output_path: None,
88 }
89 }
90
91 #[must_use]
93 pub fn failure(errors: Vec<String>) -> Self {
94 Self {
95 status: BuildStatus::Failed,
96 size: None,
97 gzip_size: None,
98 duration: None,
99 errors,
100 warnings: Vec::new(),
101 output_path: None,
102 }
103 }
104
105 #[must_use]
107 pub fn is_success(&self) -> bool {
108 self.status == BuildStatus::Success
109 }
110
111 #[must_use]
113 pub fn size_bytes(&self) -> Option<u64> {
114 self.size
115 }
116
117 #[must_use]
119 pub fn size_kb(&self) -> Option<f64> {
120 self.size.map(|s| s as f64 / 1024.0)
121 }
122
123 #[must_use]
125 pub fn errors(&self) -> Option<&[String]> {
126 if self.errors.is_empty() {
127 None
128 } else {
129 Some(&self.errors)
130 }
131 }
132
133 pub fn add_warning(&mut self, warning: impl Into<String>) {
135 self.warnings.push(warning.into());
136 }
137
138 pub fn set_output_path(&mut self, path: PathBuf) {
140 self.output_path = Some(path);
141 }
142
143 pub fn set_gzip_size(&mut self, size: u64) {
145 self.gzip_size = Some(size);
146 }
147}
148
149#[derive(Debug)]
151pub struct BuildCoordinator {
152 config: RunnerConfig,
153 opt_level: OptLevel,
154 status: BuildStatus,
155 last_result: Option<BuildResult>,
156}
157
158impl BuildCoordinator {
159 #[must_use]
161 pub fn new(config: RunnerConfig, opt_level: OptLevel) -> Self {
162 Self {
163 config,
164 opt_level,
165 status: BuildStatus::Pending,
166 last_result: None,
167 }
168 }
169
170 #[must_use]
172 pub fn status(&self) -> BuildStatus {
173 self.status
174 }
175
176 #[must_use]
178 pub fn last_result(&self) -> Option<&BuildResult> {
179 self.last_result.as_ref()
180 }
181
182 #[must_use]
184 pub fn build_args(&self) -> Vec<String> {
185 let mut args = vec![
186 "build".to_string(),
187 "--target".to_string(),
188 self.config.target.clone(),
189 ];
190
191 if self.opt_level.is_release() {
192 args.push("--release".to_string());
193 }
194
195 if let Some(ref package) = self.config.package {
196 args.push("--package".to_string());
197 args.push(package.clone());
198 }
199
200 for feature in &self.config.features {
201 args.push("--features".to_string());
202 args.push(feature.clone());
203 }
204
205 args
206 }
207
208 pub fn simulate_build(&mut self) -> BuildResult {
210 self.status = BuildStatus::Building;
211
212 let result = BuildResult::success(150_000, Duration::from_millis(500));
214 self.last_result = Some(result.clone());
215 self.status = BuildStatus::Success;
216
217 result
218 }
219
220 pub fn mark_started(&mut self) {
222 self.status = BuildStatus::Building;
223 }
224
225 pub fn mark_completed(&mut self, result: BuildResult) {
227 self.status = result.status;
228 self.last_result = Some(result);
229 }
230
231 #[must_use]
233 pub fn config(&self) -> &RunnerConfig {
234 &self.config
235 }
236
237 #[must_use]
239 pub fn opt_level(&self) -> OptLevel {
240 self.opt_level
241 }
242}
243
244#[allow(dead_code)]
246#[must_use]
247pub fn format_size(bytes: u64) -> String {
248 if bytes < 1024 {
249 format!("{} B", bytes)
250 } else if bytes < 1024 * 1024 {
251 format!("{:.1} KB", bytes as f64 / 1024.0)
252 } else {
253 format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
254 }
255}
256
257#[cfg(test)]
258#[allow(
259 clippy::unwrap_used,
260 clippy::expect_used,
261 clippy::field_reassign_with_default
262)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn test_build_result_success() {
268 let result = BuildResult::success(1024, Duration::from_secs(1));
269 assert!(result.is_success());
270 assert_eq!(result.size_bytes(), Some(1024));
271 assert!((result.size_kb().unwrap() - 1.0).abs() < 0.01);
272 }
273
274 #[test]
275 fn test_build_result_failure() {
276 let result = BuildResult::failure(vec!["error".to_string()]);
277 assert!(!result.is_success());
278 assert!(result.errors().is_some());
279 assert_eq!(result.errors().unwrap().len(), 1);
280 }
281
282 #[test]
283 fn test_build_result_add_warning() {
284 let mut result = BuildResult::success(1024, Duration::from_secs(1));
285 result.add_warning("test warning");
286 assert_eq!(result.warnings.len(), 1);
287 }
288
289 #[test]
290 fn test_coordinator_new() {
291 let config = RunnerConfig::default();
292 let coordinator = BuildCoordinator::new(config, OptLevel::Debug);
293 assert_eq!(coordinator.status(), BuildStatus::Pending);
294 }
295
296 #[test]
297 fn test_coordinator_build_args() {
298 let mut config = RunnerConfig::default();
299 config.package = Some("myapp".to_string());
300 config.features = vec!["web".to_string()];
301
302 let coordinator = BuildCoordinator::new(config, OptLevel::Release);
303 let args = coordinator.build_args();
304
305 assert!(args.contains(&"build".to_string()));
306 assert!(args.contains(&"--target".to_string()));
307 assert!(args.contains(&"--release".to_string()));
308 assert!(args.contains(&"--package".to_string()));
309 assert!(args.contains(&"myapp".to_string()));
310 assert!(args.contains(&"--features".to_string()));
311 assert!(args.contains(&"web".to_string()));
312 }
313
314 #[test]
315 fn test_coordinator_simulate_build() {
316 let config = RunnerConfig::default();
317 let mut coordinator = BuildCoordinator::new(config, OptLevel::Debug);
318
319 let result = coordinator.simulate_build();
320 assert!(result.is_success());
321 assert_eq!(coordinator.status(), BuildStatus::Success);
322 }
323
324 #[test]
325 fn test_format_size() {
326 assert_eq!(format_size(500), "500 B");
327 assert_eq!(format_size(1024), "1.0 KB");
328 assert_eq!(format_size(1536), "1.5 KB");
329 assert_eq!(format_size(1024 * 1024), "1.00 MB");
330 assert_eq!(format_size(1024 * 1024 + 512 * 1024), "1.50 MB");
331 }
332
333 #[test]
334 fn test_build_event_variants() {
335 let event = BuildEvent::Started {
336 timestamp: std::time::SystemTime::now(),
337 };
338 assert!(matches!(event, BuildEvent::Started { .. }));
339
340 let event = BuildEvent::Compiling {
341 crate_name: "test".to_string(),
342 version: "1.0.0".to_string(),
343 };
344 assert!(matches!(event, BuildEvent::Compiling { .. }));
345 }
346
347 #[test]
348 fn test_build_event_finished_variant() {
349 let event = BuildEvent::Finished {
350 success: true,
351 duration: Duration::from_secs(5),
352 };
353 assert!(matches!(
354 event,
355 BuildEvent::Finished {
356 success: true,
357 duration: _
358 }
359 ));
360
361 let event_fail = BuildEvent::Finished {
362 success: false,
363 duration: Duration::from_millis(100),
364 };
365 assert!(matches!(
366 event_fail,
367 BuildEvent::Finished { success: false, .. }
368 ));
369 }
370
371 #[test]
372 fn test_build_event_error_variant() {
373 let event = BuildEvent::Error {
374 message: "compilation failed".to_string(),
375 };
376 if let BuildEvent::Error { message } = event {
377 assert_eq!(message, "compilation failed");
378 } else {
379 panic!("Expected BuildEvent::Error");
380 }
381 }
382
383 #[test]
384 fn test_build_event_optimizing_variant() {
385 let event = BuildEvent::Optimizing {
386 step: "wasm-opt".to_string(),
387 };
388 if let BuildEvent::Optimizing { step } = event {
389 assert_eq!(step, "wasm-opt");
390 } else {
391 panic!("Expected BuildEvent::Optimizing");
392 }
393 }
394
395 #[test]
396 fn test_build_status_variants() {
397 assert_eq!(BuildStatus::Pending, BuildStatus::Pending);
398 assert_eq!(BuildStatus::Building, BuildStatus::Building);
399 assert_eq!(BuildStatus::Success, BuildStatus::Success);
400 assert_eq!(BuildStatus::Failed, BuildStatus::Failed);
401
402 assert_ne!(BuildStatus::Pending, BuildStatus::Building);
404 assert_ne!(BuildStatus::Building, BuildStatus::Success);
405 assert_ne!(BuildStatus::Success, BuildStatus::Failed);
406 }
407
408 #[test]
409 fn test_build_result_errors_empty() {
410 let result = BuildResult::success(1024, Duration::from_secs(1));
411 assert!(result.errors().is_none());
412 }
413
414 #[test]
415 fn test_build_result_set_output_path() {
416 let mut result = BuildResult::success(1024, Duration::from_secs(1));
417 assert!(result.output_path.is_none());
418
419 result.set_output_path(PathBuf::from("/tmp/output.wasm"));
420 assert_eq!(result.output_path, Some(PathBuf::from("/tmp/output.wasm")));
421 }
422
423 #[test]
424 fn test_build_result_set_gzip_size() {
425 let mut result = BuildResult::success(10240, Duration::from_secs(1));
426 assert!(result.gzip_size.is_none());
427
428 result.set_gzip_size(3200);
429 assert_eq!(result.gzip_size, Some(3200));
430 }
431
432 #[test]
433 fn test_build_result_failure_no_size() {
434 let result = BuildResult::failure(vec!["error1".to_string(), "error2".to_string()]);
435 assert!(result.size_bytes().is_none());
436 assert!(result.size_kb().is_none());
437 assert!(result.duration.is_none());
438 assert!(result.gzip_size.is_none());
439 assert!(result.output_path.is_none());
440 assert_eq!(result.warnings.len(), 0);
441 }
442
443 #[test]
444 fn test_coordinator_mark_started() {
445 let config = RunnerConfig::default();
446 let mut coordinator = BuildCoordinator::new(config, OptLevel::Debug);
447 assert_eq!(coordinator.status(), BuildStatus::Pending);
448
449 coordinator.mark_started();
450 assert_eq!(coordinator.status(), BuildStatus::Building);
451 }
452
453 #[test]
454 fn test_coordinator_mark_completed_success() {
455 let config = RunnerConfig::default();
456 let mut coordinator = BuildCoordinator::new(config, OptLevel::Release);
457
458 coordinator.mark_started();
459 let result = BuildResult::success(50000, Duration::from_secs(2));
460 coordinator.mark_completed(result);
461
462 assert_eq!(coordinator.status(), BuildStatus::Success);
463 assert!(coordinator.last_result().is_some());
464 assert!(coordinator.last_result().unwrap().is_success());
465 }
466
467 #[test]
468 fn test_coordinator_mark_completed_failure() {
469 let config = RunnerConfig::default();
470 let mut coordinator = BuildCoordinator::new(config, OptLevel::Debug);
471
472 coordinator.mark_started();
473 let result = BuildResult::failure(vec!["error: cannot find crate".to_string()]);
474 coordinator.mark_completed(result);
475
476 assert_eq!(coordinator.status(), BuildStatus::Failed);
477 assert!(coordinator.last_result().is_some());
478 assert!(!coordinator.last_result().unwrap().is_success());
479 }
480
481 #[test]
482 fn test_coordinator_config_accessor() {
483 let mut config = RunnerConfig::default();
484 config.package = Some("my-wasm-app".to_string());
485 config.features = vec!["feature1".to_string(), "feature2".to_string()];
486
487 let coordinator = BuildCoordinator::new(config, OptLevel::Size);
488
489 assert_eq!(
490 coordinator.config().package,
491 Some("my-wasm-app".to_string())
492 );
493 assert_eq!(coordinator.config().features.len(), 2);
494 assert_eq!(coordinator.config().target, "wasm32-unknown-unknown");
495 }
496
497 #[test]
498 fn test_coordinator_opt_level_accessor() {
499 let config = RunnerConfig::default();
500
501 let coord_debug = BuildCoordinator::new(config.clone(), OptLevel::Debug);
502 assert_eq!(coord_debug.opt_level(), OptLevel::Debug);
503
504 let coord_release = BuildCoordinator::new(config.clone(), OptLevel::Release);
505 assert_eq!(coord_release.opt_level(), OptLevel::Release);
506
507 let coord_size = BuildCoordinator::new(config.clone(), OptLevel::Size);
508 assert_eq!(coord_size.opt_level(), OptLevel::Size);
509
510 let coord_minsize = BuildCoordinator::new(config, OptLevel::MinSize);
511 assert_eq!(coord_minsize.opt_level(), OptLevel::MinSize);
512 }
513
514 #[test]
515 fn test_coordinator_last_result_none() {
516 let config = RunnerConfig::default();
517 let coordinator = BuildCoordinator::new(config, OptLevel::Debug);
518 assert!(coordinator.last_result().is_none());
519 }
520
521 #[test]
522 fn test_coordinator_build_args_debug_no_package() {
523 let config = RunnerConfig::default();
524 let coordinator = BuildCoordinator::new(config, OptLevel::Debug);
525 let args = coordinator.build_args();
526
527 assert!(args.contains(&"build".to_string()));
528 assert!(args.contains(&"--target".to_string()));
529 assert!(args.contains(&"wasm32-unknown-unknown".to_string()));
530 assert!(!args.contains(&"--release".to_string()));
531 assert!(!args.contains(&"--package".to_string()));
532 assert!(!args.contains(&"--features".to_string()));
533 }
534
535 #[test]
536 fn test_coordinator_build_args_multiple_features() {
537 let mut config = RunnerConfig::default();
538 config.features = vec![
539 "feature_a".to_string(),
540 "feature_b".to_string(),
541 "feature_c".to_string(),
542 ];
543
544 let coordinator = BuildCoordinator::new(config, OptLevel::Release);
545 let args = coordinator.build_args();
546
547 let feature_count = args.iter().filter(|a| *a == "--features").count();
549 assert_eq!(feature_count, 3);
550
551 assert!(args.contains(&"feature_a".to_string()));
552 assert!(args.contains(&"feature_b".to_string()));
553 assert!(args.contains(&"feature_c".to_string()));
554 }
555
556 #[test]
557 fn test_format_size_edge_cases() {
558 assert_eq!(format_size(1023), "1023 B");
560 assert_eq!(format_size(1024), "1.0 KB");
561 assert_eq!(format_size(1025), "1.0 KB");
562
563 assert_eq!(format_size(1024 * 1024 - 1), "1024.0 KB");
565 assert_eq!(format_size(1024 * 1024), "1.00 MB");
566 assert_eq!(format_size(1024 * 1024 + 1), "1.00 MB");
567
568 assert_eq!(format_size(0), "0 B");
570
571 assert_eq!(format_size(10 * 1024 * 1024), "10.00 MB");
573 }
574
575 #[test]
576 fn test_build_result_size_kb_precision() {
577 let result = BuildResult::success(2560, Duration::from_secs(1));
578 let kb = result.size_kb().unwrap();
579 assert!((kb - 2.5).abs() < 0.001);
580
581 let result2 = BuildResult::success(512, Duration::from_secs(1));
582 let kb2 = result2.size_kb().unwrap();
583 assert!((kb2 - 0.5).abs() < 0.001);
584 }
585
586 #[test]
587 fn test_build_result_multiple_warnings() {
588 let mut result = BuildResult::success(1024, Duration::from_secs(1));
589 result.add_warning("warning 1");
590 result.add_warning("warning 2");
591 result.add_warning(String::from("warning 3"));
592
593 assert_eq!(result.warnings.len(), 3);
594 assert_eq!(result.warnings[0], "warning 1");
595 assert_eq!(result.warnings[1], "warning 2");
596 assert_eq!(result.warnings[2], "warning 3");
597 }
598
599 #[test]
600 fn test_build_status_serialization() {
601 let status = BuildStatus::Building;
602 let json = serde_json::to_string(&status).unwrap();
603 let deserialized: BuildStatus = serde_json::from_str(&json).unwrap();
604 assert_eq!(status, deserialized);
605 }
606
607 #[test]
608 fn test_build_event_serialization() {
609 let event = BuildEvent::Compiling {
610 crate_name: "my_crate".to_string(),
611 version: "0.1.0".to_string(),
612 };
613 let json = serde_json::to_string(&event).unwrap();
614 assert!(json.contains("my_crate"));
615 assert!(json.contains("0.1.0"));
616 }
617
618 #[test]
619 fn test_build_result_serialization() {
620 let mut result = BuildResult::success(4096, Duration::from_millis(250));
621 result.set_output_path(PathBuf::from("/output/app.wasm"));
622 result.set_gzip_size(1500);
623 result.add_warning("unused variable");
624
625 let json = serde_json::to_string(&result).unwrap();
626 let deserialized: BuildResult = serde_json::from_str(&json).unwrap();
627
628 assert_eq!(deserialized.status, BuildStatus::Success);
629 assert_eq!(deserialized.size, Some(4096));
630 assert_eq!(deserialized.gzip_size, Some(1500));
631 assert_eq!(deserialized.warnings.len(), 1);
632 }
633
634 #[test]
635 fn test_coordinator_simulate_build_state_transitions() {
636 let config = RunnerConfig::default();
637 let mut coordinator = BuildCoordinator::new(config, OptLevel::Debug);
638
639 assert_eq!(coordinator.status(), BuildStatus::Pending);
641 assert!(coordinator.last_result().is_none());
642
643 let result = coordinator.simulate_build();
645 assert_eq!(coordinator.status(), BuildStatus::Success);
646 assert!(coordinator.last_result().is_some());
647 assert_eq!(result.size, Some(150_000));
648 assert_eq!(result.duration, Some(Duration::from_millis(500)));
649 }
650
651 #[test]
652 fn test_coordinator_with_custom_target() {
653 let mut config = RunnerConfig::default();
654 config.target = "wasm32-wasi".to_string();
655
656 let coordinator = BuildCoordinator::new(config, OptLevel::Release);
657 let args = coordinator.build_args();
658
659 assert!(args.contains(&"wasm32-wasi".to_string()));
660 }
661}