Skip to main content

jugar_probar/runner/
config.rs

1//! WASM Runner Configuration
2//!
3//! Configuration types for the WASM development server.
4
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7
8/// Optimization level for WASM builds
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
10pub enum OptLevel {
11    /// Debug build (no optimization)
12    #[default]
13    Debug,
14    /// Standard release optimization
15    Release,
16    /// Optimize for size
17    Size,
18    /// Minimize size (aggressive)
19    MinSize,
20}
21
22impl OptLevel {
23    /// Get the optimization flag for cargo
24    #[must_use]
25    pub fn as_str(&self) -> &'static str {
26        match self {
27            OptLevel::Debug => "0",
28            OptLevel::Release => "3",
29            OptLevel::Size => "s",
30            OptLevel::MinSize => "z",
31        }
32    }
33
34    /// Check if this is a release build
35    #[must_use]
36    pub fn is_release(&self) -> bool {
37        !matches!(self, OptLevel::Debug)
38    }
39}
40
41/// Configuration for the WASM runner
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct WasmRunnerConfig {
44    /// HTTP server port
45    pub http_port: u16,
46    /// WebSocket server port for hot reload
47    pub ws_port: u16,
48    /// Enable hot reload
49    pub hot_reload: bool,
50    /// Preserve application state during reload
51    pub preserve_state: bool,
52    /// Generate source maps
53    pub source_maps: bool,
54    /// Optimization level
55    pub opt_level: OptLevel,
56    /// Watch patterns (glob)
57    pub watch_patterns: Vec<String>,
58    /// Output directory for WASM
59    pub output_dir: PathBuf,
60    /// Static files directory
61    pub static_dir: Option<PathBuf>,
62    /// WASM file name
63    pub wasm_filename: String,
64}
65
66impl Default for WasmRunnerConfig {
67    fn default() -> Self {
68        Self {
69            http_port: super::DEFAULT_HTTP_PORT,
70            ws_port: super::DEFAULT_WS_PORT,
71            hot_reload: true,
72            preserve_state: true,
73            source_maps: true,
74            opt_level: OptLevel::Debug,
75            watch_patterns: vec!["src/**/*.rs".to_string(), "Cargo.toml".to_string()],
76            output_dir: PathBuf::from("target/wasm32-unknown-unknown/debug"),
77            static_dir: None,
78            wasm_filename: "app.wasm".to_string(),
79        }
80    }
81}
82
83impl WasmRunnerConfig {
84    /// Create a new builder
85    #[must_use]
86    pub fn builder() -> WasmRunnerConfigBuilder {
87        WasmRunnerConfigBuilder::default()
88    }
89
90    /// Get the full WASM file path
91    #[must_use]
92    pub fn wasm_path(&self) -> PathBuf {
93        self.output_dir.join(&self.wasm_filename)
94    }
95}
96
97/// Builder for `WasmRunnerConfig`
98#[derive(Debug, Clone, Default)]
99pub struct WasmRunnerConfigBuilder {
100    config: WasmRunnerConfig,
101}
102
103impl WasmRunnerConfigBuilder {
104    /// Set HTTP server port
105    #[must_use]
106    pub fn http_port(mut self, port: u16) -> Self {
107        self.config.http_port = port;
108        self
109    }
110
111    /// Set WebSocket server port
112    #[must_use]
113    pub fn ws_port(mut self, port: u16) -> Self {
114        self.config.ws_port = port;
115        self
116    }
117
118    /// Enable/disable hot reload
119    #[must_use]
120    pub fn hot_reload(mut self, enabled: bool) -> Self {
121        self.config.hot_reload = enabled;
122        self
123    }
124
125    /// Enable/disable state preservation
126    #[must_use]
127    pub fn preserve_state(mut self, enabled: bool) -> Self {
128        self.config.preserve_state = enabled;
129        self
130    }
131
132    /// Enable/disable source maps
133    #[must_use]
134    pub fn source_maps(mut self, enabled: bool) -> Self {
135        self.config.source_maps = enabled;
136        self
137    }
138
139    /// Set optimization level
140    #[must_use]
141    pub fn opt_level(mut self, level: OptLevel) -> Self {
142        self.config.opt_level = level;
143        self
144    }
145
146    /// Set watch patterns
147    #[must_use]
148    pub fn watch_patterns(mut self, patterns: Vec<String>) -> Self {
149        self.config.watch_patterns = patterns;
150        self
151    }
152
153    /// Set output directory
154    #[must_use]
155    pub fn output_dir(mut self, dir: PathBuf) -> Self {
156        self.config.output_dir = dir;
157        self
158    }
159
160    /// Set static files directory
161    #[must_use]
162    pub fn static_dir(mut self, dir: PathBuf) -> Self {
163        self.config.static_dir = Some(dir);
164        self
165    }
166
167    /// Set WASM filename
168    #[must_use]
169    pub fn wasm_filename(mut self, name: impl Into<String>) -> Self {
170        self.config.wasm_filename = name.into();
171        self
172    }
173
174    /// Build the configuration
175    #[must_use]
176    pub fn build(self) -> WasmRunnerConfig {
177        self.config
178    }
179}
180
181/// General runner configuration
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct RunnerConfig {
184    /// Project root directory
185    pub project_root: PathBuf,
186    /// Target triple
187    pub target: String,
188    /// Package name (optional)
189    pub package: Option<String>,
190    /// Features to enable
191    pub features: Vec<String>,
192    /// Environment variables
193    pub env: std::collections::HashMap<String, String>,
194}
195
196impl Default for RunnerConfig {
197    fn default() -> Self {
198        Self {
199            project_root: PathBuf::from("."),
200            target: "wasm32-unknown-unknown".to_string(),
201            package: None,
202            features: Vec::new(),
203            env: std::collections::HashMap::new(),
204        }
205    }
206}
207
208#[cfg(test)]
209#[allow(clippy::unwrap_used, clippy::expect_used)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_opt_level_as_str() {
215        assert_eq!(OptLevel::Debug.as_str(), "0");
216        assert_eq!(OptLevel::Release.as_str(), "3");
217        assert_eq!(OptLevel::Size.as_str(), "s");
218        assert_eq!(OptLevel::MinSize.as_str(), "z");
219    }
220
221    #[test]
222    fn test_opt_level_is_release() {
223        assert!(!OptLevel::Debug.is_release());
224        assert!(OptLevel::Release.is_release());
225        assert!(OptLevel::Size.is_release());
226        assert!(OptLevel::MinSize.is_release());
227    }
228
229    #[test]
230    fn test_wasm_config_default() {
231        let config = WasmRunnerConfig::default();
232        assert!(config.hot_reload);
233        assert!(config.preserve_state);
234        assert!(config.source_maps);
235        assert_eq!(config.opt_level, OptLevel::Debug);
236    }
237
238    #[test]
239    fn test_wasm_config_builder_chain() {
240        let config = WasmRunnerConfig::builder()
241            .http_port(9000)
242            .ws_port(9001)
243            .hot_reload(false)
244            .opt_level(OptLevel::Release)
245            .build();
246
247        assert_eq!(config.http_port, 9000);
248        assert_eq!(config.ws_port, 9001);
249        assert!(!config.hot_reload);
250        assert_eq!(config.opt_level, OptLevel::Release);
251    }
252
253    #[test]
254    fn test_wasm_path() {
255        let config = WasmRunnerConfig::builder()
256            .output_dir(PathBuf::from("target/wasm"))
257            .wasm_filename("myapp.wasm")
258            .build();
259
260        assert_eq!(config.wasm_path(), PathBuf::from("target/wasm/myapp.wasm"));
261    }
262
263    #[test]
264    fn test_runner_config_default() {
265        let config = RunnerConfig::default();
266        assert_eq!(config.target, "wasm32-unknown-unknown");
267        assert!(config.features.is_empty());
268    }
269
270    #[test]
271    fn test_opt_level_default() {
272        let opt = OptLevel::default();
273        assert_eq!(opt, OptLevel::Debug);
274        assert!(!opt.is_release());
275    }
276
277    #[test]
278    fn test_wasm_config_builder_preserve_state() {
279        let config = WasmRunnerConfig::builder().preserve_state(false).build();
280        assert!(!config.preserve_state);
281
282        let config2 = WasmRunnerConfig::builder().preserve_state(true).build();
283        assert!(config2.preserve_state);
284    }
285
286    #[test]
287    fn test_wasm_config_builder_source_maps() {
288        let config = WasmRunnerConfig::builder().source_maps(false).build();
289        assert!(!config.source_maps);
290
291        let config2 = WasmRunnerConfig::builder().source_maps(true).build();
292        assert!(config2.source_maps);
293    }
294
295    #[test]
296    fn test_wasm_config_builder_watch_patterns() {
297        let patterns = vec![
298            "src/**/*.rs".to_string(),
299            "tests/**/*.rs".to_string(),
300            "Cargo.toml".to_string(),
301        ];
302        let config = WasmRunnerConfig::builder()
303            .watch_patterns(patterns.clone())
304            .build();
305
306        assert_eq!(config.watch_patterns.len(), 3);
307        assert_eq!(config.watch_patterns, patterns);
308    }
309
310    #[test]
311    fn test_wasm_config_builder_static_dir() {
312        let config = WasmRunnerConfig::builder()
313            .static_dir(PathBuf::from("public"))
314            .build();
315
316        assert_eq!(config.static_dir, Some(PathBuf::from("public")));
317    }
318
319    #[test]
320    fn test_wasm_config_default_static_dir_none() {
321        let config = WasmRunnerConfig::default();
322        assert!(config.static_dir.is_none());
323    }
324
325    #[test]
326    fn test_runner_config_with_all_fields() {
327        let mut config = RunnerConfig::default();
328        config.project_root = PathBuf::from("/home/user/project");
329        config.target = "wasm32-wasi".to_string();
330        config.package = Some("my-wasm-lib".to_string());
331        config.features = vec!["feature1".to_string(), "feature2".to_string()];
332        config
333            .env
334            .insert("RUST_LOG".to_string(), "debug".to_string());
335        config
336            .env
337            .insert("CARGO_TERM_COLOR".to_string(), "always".to_string());
338
339        assert_eq!(config.project_root, PathBuf::from("/home/user/project"));
340        assert_eq!(config.target, "wasm32-wasi");
341        assert_eq!(config.package, Some("my-wasm-lib".to_string()));
342        assert_eq!(config.features.len(), 2);
343        assert_eq!(config.env.len(), 2);
344        assert_eq!(config.env.get("RUST_LOG"), Some(&"debug".to_string()));
345    }
346
347    #[test]
348    fn test_wasm_config_builder_all_options() {
349        let config = WasmRunnerConfig::builder()
350            .http_port(3000)
351            .ws_port(3001)
352            .hot_reload(true)
353            .preserve_state(false)
354            .source_maps(true)
355            .opt_level(OptLevel::MinSize)
356            .watch_patterns(vec!["*.rs".to_string()])
357            .output_dir(PathBuf::from("dist"))
358            .static_dir(PathBuf::from("assets"))
359            .wasm_filename("game.wasm")
360            .build();
361
362        assert_eq!(config.http_port, 3000);
363        assert_eq!(config.ws_port, 3001);
364        assert!(config.hot_reload);
365        assert!(!config.preserve_state);
366        assert!(config.source_maps);
367        assert_eq!(config.opt_level, OptLevel::MinSize);
368        assert_eq!(config.watch_patterns, vec!["*.rs".to_string()]);
369        assert_eq!(config.output_dir, PathBuf::from("dist"));
370        assert_eq!(config.static_dir, Some(PathBuf::from("assets")));
371        assert_eq!(config.wasm_filename, "game.wasm");
372    }
373
374    #[test]
375    fn test_wasm_path_with_nested_output_dir() {
376        let config = WasmRunnerConfig::builder()
377            .output_dir(PathBuf::from("target/wasm32-unknown-unknown/release"))
378            .wasm_filename("my_game.wasm")
379            .build();
380
381        assert_eq!(
382            config.wasm_path(),
383            PathBuf::from("target/wasm32-unknown-unknown/release/my_game.wasm")
384        );
385    }
386
387    #[test]
388    fn test_opt_level_serialization() {
389        for opt in [
390            OptLevel::Debug,
391            OptLevel::Release,
392            OptLevel::Size,
393            OptLevel::MinSize,
394        ] {
395            let json = serde_json::to_string(&opt).unwrap();
396            let deserialized: OptLevel = serde_json::from_str(&json).unwrap();
397            assert_eq!(opt, deserialized);
398        }
399    }
400
401    #[test]
402    fn test_wasm_runner_config_serialization() {
403        let config = WasmRunnerConfig::builder()
404            .http_port(8000)
405            .ws_port(8001)
406            .hot_reload(true)
407            .opt_level(OptLevel::Release)
408            .wasm_filename("app.wasm")
409            .build();
410
411        let json = serde_json::to_string(&config).unwrap();
412        let deserialized: WasmRunnerConfig = serde_json::from_str(&json).unwrap();
413
414        assert_eq!(deserialized.http_port, 8000);
415        assert_eq!(deserialized.ws_port, 8001);
416        assert!(deserialized.hot_reload);
417        assert_eq!(deserialized.opt_level, OptLevel::Release);
418        assert_eq!(deserialized.wasm_filename, "app.wasm");
419    }
420
421    #[test]
422    fn test_runner_config_serialization() {
423        let mut config = RunnerConfig::default();
424        config.package = Some("test-package".to_string());
425        config.features = vec!["web".to_string()];
426
427        let json = serde_json::to_string(&config).unwrap();
428        let deserialized: RunnerConfig = serde_json::from_str(&json).unwrap();
429
430        assert_eq!(deserialized.target, "wasm32-unknown-unknown");
431        assert_eq!(deserialized.package, Some("test-package".to_string()));
432        assert_eq!(deserialized.features, vec!["web".to_string()]);
433    }
434
435    #[test]
436    fn test_wasm_config_default_watch_patterns() {
437        let config = WasmRunnerConfig::default();
438        assert!(config.watch_patterns.contains(&"src/**/*.rs".to_string()));
439        assert!(config.watch_patterns.contains(&"Cargo.toml".to_string()));
440    }
441
442    #[test]
443    fn test_wasm_config_default_output_dir() {
444        let config = WasmRunnerConfig::default();
445        assert_eq!(
446            config.output_dir,
447            PathBuf::from("target/wasm32-unknown-unknown/debug")
448        );
449    }
450
451    #[test]
452    fn test_wasm_config_default_wasm_filename() {
453        let config = WasmRunnerConfig::default();
454        assert_eq!(config.wasm_filename, "app.wasm");
455    }
456
457    #[test]
458    fn test_wasm_config_builder_default_values() {
459        // Test that builder starts with defaults
460        let builder = WasmRunnerConfigBuilder::default();
461        let config = builder.build();
462
463        // Should match WasmRunnerConfig::default()
464        let default_config = WasmRunnerConfig::default();
465        assert_eq!(config.http_port, default_config.http_port);
466        assert_eq!(config.ws_port, default_config.ws_port);
467        assert_eq!(config.hot_reload, default_config.hot_reload);
468        assert_eq!(config.preserve_state, default_config.preserve_state);
469        assert_eq!(config.source_maps, default_config.source_maps);
470        assert_eq!(config.opt_level, default_config.opt_level);
471    }
472
473    #[test]
474    fn test_runner_config_empty_env() {
475        let config = RunnerConfig::default();
476        assert!(config.env.is_empty());
477    }
478
479    #[test]
480    fn test_runner_config_project_root_default() {
481        let config = RunnerConfig::default();
482        assert_eq!(config.project_root, PathBuf::from("."));
483    }
484
485    #[test]
486    fn test_wasm_config_builder_chaining() {
487        // Test that builder methods can be chained in any order
488        let config1 = WasmRunnerConfig::builder()
489            .http_port(9000)
490            .opt_level(OptLevel::Size)
491            .hot_reload(false)
492            .build();
493
494        let config2 = WasmRunnerConfig::builder()
495            .hot_reload(false)
496            .http_port(9000)
497            .opt_level(OptLevel::Size)
498            .build();
499
500        assert_eq!(config1.http_port, config2.http_port);
501        assert_eq!(config1.opt_level, config2.opt_level);
502        assert_eq!(config1.hot_reload, config2.hot_reload);
503    }
504
505    #[test]
506    fn test_opt_level_all_variants_as_str() {
507        // Comprehensive test for all variants
508        let variants = [
509            (OptLevel::Debug, "0"),
510            (OptLevel::Release, "3"),
511            (OptLevel::Size, "s"),
512            (OptLevel::MinSize, "z"),
513        ];
514
515        for (opt, expected) in variants {
516            assert_eq!(opt.as_str(), expected);
517        }
518    }
519
520    #[test]
521    fn test_opt_level_is_release_all_variants() {
522        // Debug is not release
523        assert!(!OptLevel::Debug.is_release());
524
525        // All others are release
526        assert!(OptLevel::Release.is_release());
527        assert!(OptLevel::Size.is_release());
528        assert!(OptLevel::MinSize.is_release());
529    }
530}