Skip to main content

jugar_probar/runner/
builder.rs

1//! Build Coordinator for WASM Compilation
2//!
3//! Manages cargo build process for WASM targets with progress tracking.
4
5use super::config::{OptLevel, RunnerConfig};
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use std::time::Duration;
9
10/// Build status
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum BuildStatus {
13    /// Build not started
14    Pending,
15    /// Build in progress
16    Building,
17    /// Build succeeded
18    Success,
19    /// Build failed
20    Failed,
21}
22
23/// Build event for progress tracking
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub enum BuildEvent {
26    /// Build started
27    Started {
28        /// Timestamp
29        timestamp: std::time::SystemTime,
30    },
31    /// Compiling a crate
32    Compiling {
33        /// Crate name
34        crate_name: String,
35        /// Crate version
36        version: String,
37    },
38    /// Build finished
39    Finished {
40        /// Build result
41        success: bool,
42        /// Duration
43        duration: Duration,
44    },
45    /// Build error
46    Error {
47        /// Error message
48        message: String,
49    },
50    /// Optimization step
51    Optimizing {
52        /// Step name (e.g., "wasm-opt")
53        step: String,
54    },
55}
56
57/// Result of a build operation
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct BuildResult {
60    /// Build status
61    pub status: BuildStatus,
62    /// Size of output WASM in bytes
63    pub size: Option<u64>,
64    /// Gzipped size in bytes
65    pub gzip_size: Option<u64>,
66    /// Build duration
67    pub duration: Option<Duration>,
68    /// Error messages
69    pub errors: Vec<String>,
70    /// Warning messages
71    pub warnings: Vec<String>,
72    /// Output path
73    pub output_path: Option<PathBuf>,
74}
75
76impl BuildResult {
77    /// Create a success result
78    #[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    /// Create a failure result
92    #[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    /// Check if build succeeded
106    #[must_use]
107    pub fn is_success(&self) -> bool {
108        self.status == BuildStatus::Success
109    }
110
111    /// Get size in bytes
112    #[must_use]
113    pub fn size_bytes(&self) -> Option<u64> {
114        self.size
115    }
116
117    /// Get size in KB
118    #[must_use]
119    pub fn size_kb(&self) -> Option<f64> {
120        self.size.map(|s| s as f64 / 1024.0)
121    }
122
123    /// Get errors
124    #[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    /// Add a warning
134    pub fn add_warning(&mut self, warning: impl Into<String>) {
135        self.warnings.push(warning.into());
136    }
137
138    /// Set output path
139    pub fn set_output_path(&mut self, path: PathBuf) {
140        self.output_path = Some(path);
141    }
142
143    /// Set gzip size
144    pub fn set_gzip_size(&mut self, size: u64) {
145        self.gzip_size = Some(size);
146    }
147}
148
149/// Build coordinator for managing WASM compilation
150#[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    /// Create a new build coordinator
160    #[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    /// Get current status
171    #[must_use]
172    pub fn status(&self) -> BuildStatus {
173        self.status
174    }
175
176    /// Get last build result
177    #[must_use]
178    pub fn last_result(&self) -> Option<&BuildResult> {
179        self.last_result.as_ref()
180    }
181
182    /// Build the cargo command arguments
183    #[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    /// Simulate a build (for testing without actual compilation)
209    pub fn simulate_build(&mut self) -> BuildResult {
210        self.status = BuildStatus::Building;
211
212        // Simulate successful build
213        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    /// Mark build as started
221    pub fn mark_started(&mut self) {
222        self.status = BuildStatus::Building;
223    }
224
225    /// Mark build as completed
226    pub fn mark_completed(&mut self, result: BuildResult) {
227        self.status = result.status;
228        self.last_result = Some(result);
229    }
230
231    /// Get configuration
232    #[must_use]
233    pub fn config(&self) -> &RunnerConfig {
234        &self.config
235    }
236
237    /// Get optimization level
238    #[must_use]
239    pub fn opt_level(&self) -> OptLevel {
240        self.opt_level
241    }
242}
243
244/// Format file size for display
245#[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        // Test all variant distinctions
403        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        // Each feature gets its own --features flag
548        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        // Boundary at 1024 bytes
559        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        // Boundary at 1 MB
564        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        // Zero
569        assert_eq!(format_size(0), "0 B");
570
571        // Large file (10 MB)
572        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        // Initial state
640        assert_eq!(coordinator.status(), BuildStatus::Pending);
641        assert!(coordinator.last_result().is_none());
642
643        // After simulate_build
644        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}