Skip to main content

zlayer_builder/
wasm_builder.rs

1//! WebAssembly component builder with multi-language support
2//!
3//! This module provides functionality to build WebAssembly components from
4//! various source languages, with support for both WASI Preview 1 and Preview 2
5//! (component model) targets.
6//!
7//! # Supported Languages
8//!
9//! - **Rust**: `cargo build --target wasm32-wasip1` or `cargo component build`
10//! - **Go**: TinyGo compiler with WASI target
11//! - **Python**: componentize-py for component model
12//! - **TypeScript/JavaScript**: jco/componentize-js
13//! - **AssemblyScript**: asc compiler
14//! - **C**: Clang with WASI SDK
15//! - **Zig**: Native WASI support
16//!
17//! # Usage
18//!
19//! ```no_run
20//! use std::path::Path;
21//! use zlayer_builder::wasm_builder::{build_wasm, WasmBuildConfig, WasiTarget};
22//!
23//! # async fn example() -> Result<(), zlayer_builder::wasm_builder::WasmBuildError> {
24//! let config = WasmBuildConfig {
25//!     language: None,  // Auto-detect
26//!     target: WasiTarget::Preview2,
27//!     optimize: true,
28//!     wit_path: None,
29//!     output_path: None,
30//! };
31//!
32//! let result = build_wasm(Path::new("./my-plugin"), config).await?;
33//! println!("Built {} WASM at {}", result.language, result.wasm_path.display());
34//! # Ok(())
35//! # }
36//! ```
37
38use std::fmt;
39use std::path::{Path, PathBuf};
40
41use thiserror::Error;
42use tokio::process::Command;
43use tracing::{debug, info, instrument, trace, warn};
44
45/// Error types for WASM build operations
46#[derive(Debug, Error)]
47pub enum WasmBuildError {
48    /// Failed to detect the source language
49    #[error("Could not detect source language in '{path}'")]
50    LanguageNotDetected {
51        /// The path that was inspected
52        path: PathBuf,
53    },
54
55    /// Build tool not found
56    #[error("Build tool '{tool}' not found: {message}")]
57    ToolNotFound {
58        /// Name of the missing tool
59        tool: String,
60        /// Additional context
61        message: String,
62    },
63
64    /// Build command failed
65    #[error("Build failed with exit code {exit_code}: {stderr}")]
66    BuildFailed {
67        /// Exit code from the build command
68        exit_code: i32,
69        /// Standard error output
70        stderr: String,
71        /// Standard output (may contain useful info)
72        stdout: String,
73    },
74
75    /// WASM output not found after build
76    #[error("WASM output not found at expected path: {path}")]
77    OutputNotFound {
78        /// The expected output path
79        path: PathBuf,
80    },
81
82    /// Configuration error
83    #[error("Configuration error: {message}")]
84    ConfigError {
85        /// Description of the configuration problem
86        message: String,
87    },
88
89    /// IO error during build operations
90    #[error("IO error: {0}")]
91    Io(#[from] std::io::Error),
92
93    /// Failed to read project configuration
94    #[error("Failed to read project configuration: {message}")]
95    ProjectConfigError {
96        /// Description of what went wrong
97        message: String,
98    },
99}
100
101/// Result type for WASM build operations
102pub type Result<T, E = WasmBuildError> = std::result::Result<T, E>;
103
104/// Supported source languages for WASM compilation
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106pub enum WasmLanguage {
107    /// Standard Rust with wasm32-wasip1 or wasm32-wasip2 target
108    Rust,
109    /// Rust with cargo-component for WASI Preview 2 components
110    RustComponent,
111    /// Go using TinyGo compiler
112    Go,
113    /// Python using componentize-py
114    Python,
115    /// TypeScript using jco/componentize-js
116    TypeScript,
117    /// AssemblyScript using asc compiler
118    AssemblyScript,
119    /// C using WASI SDK (clang)
120    C,
121    /// Zig with native WASI support
122    Zig,
123}
124
125impl WasmLanguage {
126    /// Get all supported languages
127    pub fn all() -> &'static [WasmLanguage] {
128        &[
129            WasmLanguage::Rust,
130            WasmLanguage::RustComponent,
131            WasmLanguage::Go,
132            WasmLanguage::Python,
133            WasmLanguage::TypeScript,
134            WasmLanguage::AssemblyScript,
135            WasmLanguage::C,
136            WasmLanguage::Zig,
137        ]
138    }
139
140    /// Get the display name for this language
141    pub fn name(&self) -> &'static str {
142        match self {
143            WasmLanguage::Rust => "Rust",
144            WasmLanguage::RustComponent => "Rust (cargo-component)",
145            WasmLanguage::Go => "Go (TinyGo)",
146            WasmLanguage::Python => "Python",
147            WasmLanguage::TypeScript => "TypeScript",
148            WasmLanguage::AssemblyScript => "AssemblyScript",
149            WasmLanguage::C => "C",
150            WasmLanguage::Zig => "Zig",
151        }
152    }
153
154    /// Check if this language produces component model output by default
155    pub fn is_component_native(&self) -> bool {
156        matches!(
157            self,
158            WasmLanguage::RustComponent | WasmLanguage::Python | WasmLanguage::TypeScript
159        )
160    }
161}
162
163impl fmt::Display for WasmLanguage {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        write!(f, "{}", self.name())
166    }
167}
168
169/// WASI target version
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
171pub enum WasiTarget {
172    /// WASI Preview 1 (wasm32-wasip1)
173    Preview1,
174    /// WASI Preview 2 (component model)
175    #[default]
176    Preview2,
177}
178
179impl WasiTarget {
180    /// Get the Rust target triple for this WASI version
181    pub fn rust_target(&self) -> &'static str {
182        match self {
183            WasiTarget::Preview1 => "wasm32-wasip1",
184            WasiTarget::Preview2 => "wasm32-wasip2",
185        }
186    }
187
188    /// Get the display name
189    pub fn name(&self) -> &'static str {
190        match self {
191            WasiTarget::Preview1 => "WASI Preview 1",
192            WasiTarget::Preview2 => "WASI Preview 2",
193        }
194    }
195}
196
197impl fmt::Display for WasiTarget {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        write!(f, "{}", self.name())
200    }
201}
202
203/// Configuration for building a WASM component
204#[derive(Debug, Clone, Default)]
205pub struct WasmBuildConfig {
206    /// Source language (None = auto-detect)
207    pub language: Option<WasmLanguage>,
208
209    /// Target WASI version
210    pub target: WasiTarget,
211
212    /// Whether to optimize the output (release mode)
213    pub optimize: bool,
214
215    /// Path to WIT files for component model
216    pub wit_path: Option<PathBuf>,
217
218    /// Override output path
219    pub output_path: Option<PathBuf>,
220}
221
222impl WasmBuildConfig {
223    /// Create a new default configuration
224    pub fn new() -> Self {
225        Self::default()
226    }
227
228    /// Set the source language
229    pub fn language(mut self, lang: WasmLanguage) -> Self {
230        self.language = Some(lang);
231        self
232    }
233
234    /// Set the WASI target
235    pub fn target(mut self, target: WasiTarget) -> Self {
236        self.target = target;
237        self
238    }
239
240    /// Enable optimization
241    pub fn optimize(mut self, optimize: bool) -> Self {
242        self.optimize = optimize;
243        self
244    }
245
246    /// Set the WIT path
247    pub fn wit_path(mut self, path: impl Into<PathBuf>) -> Self {
248        self.wit_path = Some(path.into());
249        self
250    }
251
252    /// Set the output path
253    pub fn output_path(mut self, path: impl Into<PathBuf>) -> Self {
254        self.output_path = Some(path.into());
255        self
256    }
257}
258
259/// Result of a successful WASM build
260#[derive(Debug, Clone)]
261pub struct WasmBuildResult {
262    /// Path to the built WASM file
263    pub wasm_path: PathBuf,
264
265    /// The language that was used
266    pub language: WasmLanguage,
267
268    /// The WASI target that was used
269    pub target: WasiTarget,
270
271    /// Size of the output file in bytes
272    pub size: u64,
273}
274
275/// Detect the source language from files in the given directory
276///
277/// This function examines project files to determine which language
278/// should be used for WASM compilation.
279///
280/// # Detection Order
281///
282/// 1. Cargo.toml with cargo-component -> RustComponent
283/// 2. Cargo.toml -> Rust
284/// 3. go.mod -> Go (TinyGo)
285/// 4. pyproject.toml or requirements.txt -> Python
286/// 5. package.json with assemblyscript -> AssemblyScript
287/// 6. package.json -> TypeScript
288/// 7. build.zig -> Zig
289/// 8. Makefile with *.c files -> C
290#[instrument(level = "debug", skip_all, fields(path = %context.as_ref().display()))]
291pub fn detect_language(context: impl AsRef<Path>) -> Result<WasmLanguage> {
292    let path = context.as_ref();
293    debug!("Detecting WASM source language");
294
295    // Check for Rust projects
296    let cargo_toml = path.join("Cargo.toml");
297    if cargo_toml.exists() {
298        trace!("Found Cargo.toml");
299
300        // Check if this is a cargo-component project
301        if is_cargo_component_project(&cargo_toml)? {
302            debug!("Detected Rust (cargo-component) project");
303            return Ok(WasmLanguage::RustComponent);
304        }
305
306        debug!("Detected Rust project");
307        return Ok(WasmLanguage::Rust);
308    }
309
310    // Check for Go projects
311    if path.join("go.mod").exists() {
312        debug!("Detected Go (TinyGo) project");
313        return Ok(WasmLanguage::Go);
314    }
315
316    // Check for Python projects
317    if path.join("pyproject.toml").exists()
318        || path.join("requirements.txt").exists()
319        || path.join("setup.py").exists()
320    {
321        debug!("Detected Python project");
322        return Ok(WasmLanguage::Python);
323    }
324
325    // Check for Node.js/TypeScript projects
326    let package_json = path.join("package.json");
327    if package_json.exists() {
328        trace!("Found package.json");
329
330        // Check for AssemblyScript
331        if is_assemblyscript_project(&package_json)? {
332            debug!("Detected AssemblyScript project");
333            return Ok(WasmLanguage::AssemblyScript);
334        }
335
336        debug!("Detected TypeScript project");
337        return Ok(WasmLanguage::TypeScript);
338    }
339
340    // Check for Zig projects
341    if path.join("build.zig").exists() {
342        debug!("Detected Zig project");
343        return Ok(WasmLanguage::Zig);
344    }
345
346    // Check for C projects (Makefile + *.c files)
347    if (path.join("Makefile").exists() || path.join("CMakeLists.txt").exists())
348        && has_c_source_files(path)
349    {
350        debug!("Detected C project");
351        return Ok(WasmLanguage::C);
352    }
353
354    // Also check for C without makefile (just source files)
355    if has_c_source_files(path) {
356        debug!("Detected C project (source files only)");
357        return Ok(WasmLanguage::C);
358    }
359
360    Err(WasmBuildError::LanguageNotDetected {
361        path: path.to_path_buf(),
362    })
363}
364
365/// Check if a Cargo.toml indicates a cargo-component project
366fn is_cargo_component_project(cargo_toml: &Path) -> Result<bool> {
367    let content =
368        std::fs::read_to_string(cargo_toml).map_err(|e| WasmBuildError::ProjectConfigError {
369            message: format!("Failed to read Cargo.toml: {}", e),
370        })?;
371
372    // Check for cargo-component specific configuration
373    // This includes [package.metadata.component] or [lib] with crate-type = ["cdylib"]
374    // and presence of wit files
375    if content.contains("[package.metadata.component]") {
376        return Ok(true);
377    }
378
379    // Check for component-related dependencies
380    if content.contains("wit-bindgen") || content.contains("cargo-component-bindings") {
381        return Ok(true);
382    }
383
384    // Check for cargo-component.toml in the same directory
385    let component_toml = cargo_toml.parent().map(|p| p.join("cargo-component.toml"));
386    if let Some(ref component_toml) = component_toml {
387        if component_toml.exists() {
388            return Ok(true);
389        }
390    }
391
392    Ok(false)
393}
394
395/// Check if a package.json indicates an AssemblyScript project
396fn is_assemblyscript_project(package_json: &Path) -> Result<bool> {
397    let content =
398        std::fs::read_to_string(package_json).map_err(|e| WasmBuildError::ProjectConfigError {
399            message: format!("Failed to read package.json: {}", e),
400        })?;
401
402    let json: serde_json::Value =
403        serde_json::from_str(&content).map_err(|e| WasmBuildError::ProjectConfigError {
404            message: format!("Invalid package.json: {}", e),
405        })?;
406
407    // Check dependencies and devDependencies for assemblyscript
408    let has_assemblyscript = |deps: Option<&serde_json::Value>| -> bool {
409        deps.and_then(|d| d.as_object())
410            .map(|d| d.contains_key("assemblyscript"))
411            .unwrap_or(false)
412    };
413
414    if has_assemblyscript(json.get("dependencies"))
415        || has_assemblyscript(json.get("devDependencies"))
416    {
417        return Ok(true);
418    }
419
420    // Check for asc in scripts
421    if let Some(scripts) = json.get("scripts").and_then(|s| s.as_object()) {
422        for script in scripts.values() {
423            if let Some(cmd) = script.as_str() {
424                if cmd.contains("asc ") || cmd.starts_with("asc") {
425                    return Ok(true);
426                }
427            }
428        }
429    }
430
431    Ok(false)
432}
433
434/// Check if the directory contains C source files
435fn has_c_source_files(path: &Path) -> bool {
436    if let Ok(entries) = std::fs::read_dir(path) {
437        for entry in entries.flatten() {
438            let file_path = entry.path();
439            if let Some(ext) = file_path.extension() {
440                if ext == "c" || ext == "h" {
441                    return true;
442                }
443            }
444        }
445    }
446
447    // Also check src/ subdirectory
448    let src_dir = path.join("src");
449    if src_dir.is_dir() {
450        if let Ok(entries) = std::fs::read_dir(&src_dir) {
451            for entry in entries.flatten() {
452                let file_path = entry.path();
453                if let Some(ext) = file_path.extension() {
454                    if ext == "c" || ext == "h" {
455                        return true;
456                    }
457                }
458            }
459        }
460    }
461
462    false
463}
464
465/// Get the build command for a specific language and target
466pub fn get_build_command(language: WasmLanguage, target: WasiTarget, release: bool) -> Vec<String> {
467    match language {
468        WasmLanguage::Rust => {
469            let mut cmd = vec![
470                "cargo".to_string(),
471                "build".to_string(),
472                "--target".to_string(),
473                target.rust_target().to_string(),
474            ];
475            if release {
476                cmd.push("--release".to_string());
477            }
478            cmd
479        }
480
481        WasmLanguage::RustComponent => {
482            let mut cmd = vec![
483                "cargo".to_string(),
484                "component".to_string(),
485                "build".to_string(),
486            ];
487            if release {
488                cmd.push("--release".to_string());
489            }
490            cmd
491        }
492
493        WasmLanguage::Go => {
494            // TinyGo command for WASI
495            let wasi_target = match target {
496                WasiTarget::Preview1 => "wasip1",
497                WasiTarget::Preview2 => "wasip2",
498            };
499            let mut cmd = vec![
500                "tinygo".to_string(),
501                "build".to_string(),
502                "-target".to_string(),
503                wasi_target.to_string(),
504                "-o".to_string(),
505                "main.wasm".to_string(),
506            ];
507            if release {
508                cmd.push("-opt".to_string());
509                cmd.push("2".to_string());
510            }
511            cmd.push(".".to_string());
512            cmd
513        }
514
515        WasmLanguage::Python => {
516            // componentize-py for WASI Preview 2
517            // Note: componentize-py doesn't have explicit optimization flags,
518            // the output is already optimized regardless of the release flag
519            let _ = release; // Silence unused warning
520            vec![
521                "componentize-py".to_string(),
522                "-d".to_string(),
523                "wit".to_string(),
524                "-w".to_string(),
525                "world".to_string(),
526                "componentize".to_string(),
527                "app".to_string(),
528                "-o".to_string(),
529                "app.wasm".to_string(),
530            ]
531        }
532
533        WasmLanguage::TypeScript => {
534            // jco componentize for TypeScript
535            vec![
536                "npx".to_string(),
537                "jco".to_string(),
538                "componentize".to_string(),
539                "src/index.js".to_string(),
540                "--wit".to_string(),
541                "wit".to_string(),
542                "-o".to_string(),
543                "dist/component.wasm".to_string(),
544            ]
545        }
546
547        WasmLanguage::AssemblyScript => {
548            let mut cmd = vec![
549                "npx".to_string(),
550                "asc".to_string(),
551                "assembly/index.ts".to_string(),
552                "--target".to_string(),
553                "release".to_string(),
554                "-o".to_string(),
555                "build/release.wasm".to_string(),
556            ];
557            if release {
558                cmd.push("--optimize".to_string());
559            }
560            cmd
561        }
562
563        WasmLanguage::C => {
564            // Using WASI SDK's clang
565            let mut cmd = vec![
566                "clang".to_string(),
567                "--target=wasm32-wasi".to_string(),
568                "-o".to_string(),
569                "main.wasm".to_string(),
570            ];
571            if release {
572                cmd.push("-O2".to_string());
573            }
574            cmd.push("src/main.c".to_string());
575            cmd
576        }
577
578        WasmLanguage::Zig => {
579            // Zig with WASI target
580            let mut cmd = vec![
581                "zig".to_string(),
582                "build".to_string(),
583                "-Dtarget=wasm32-wasi".to_string(),
584            ];
585            if release {
586                cmd.push("-Doptimize=ReleaseFast".to_string());
587            }
588            cmd
589        }
590    }
591}
592
593/// Build a WASM component from source
594///
595/// This is the main entry point for building WASM components.
596/// It will detect the source language if not specified, run the
597/// appropriate build command, and return information about the
598/// built artifact.
599#[instrument(level = "info", skip_all, fields(
600    context = %context.as_ref().display(),
601    language = ?config.language,
602    target = ?config.target
603))]
604pub async fn build_wasm(
605    context: impl AsRef<Path>,
606    config: WasmBuildConfig,
607) -> Result<WasmBuildResult> {
608    let context = context.as_ref();
609    info!("Building WASM component");
610
611    // Detect language if not specified
612    let language = match config.language {
613        Some(lang) => {
614            debug!("Using specified language: {}", lang);
615            lang
616        }
617        None => {
618            let detected = detect_language(context)?;
619            info!("Auto-detected language: {}", detected);
620            detected
621        }
622    };
623
624    // Verify build tool is available
625    verify_build_tool(language).await?;
626
627    // Get build command
628    let cmd = get_build_command(language, config.target, config.optimize);
629    debug!("Build command: {:?}", cmd);
630
631    // Execute build
632    let output = execute_build_command(context, &cmd).await?;
633
634    // Check for success
635    if !output.status.success() {
636        let exit_code = output.status.code().unwrap_or(-1);
637        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
638        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
639
640        warn!("Build failed with exit code {}", exit_code);
641        trace!("stdout: {}", stdout);
642        trace!("stderr: {}", stderr);
643
644        return Err(WasmBuildError::BuildFailed {
645            exit_code,
646            stderr,
647            stdout,
648        });
649    }
650
651    // Find output file
652    let wasm_path = find_wasm_output(context, language, config.target, config.optimize)?;
653
654    // Apply output path override if specified
655    let final_path = if let Some(ref output_path) = config.output_path {
656        std::fs::copy(&wasm_path, output_path)?;
657        output_path.clone()
658    } else {
659        wasm_path
660    };
661
662    // Get file size
663    let metadata = std::fs::metadata(&final_path)?;
664    let size = metadata.len();
665
666    info!("Successfully built {} WASM ({} bytes)", language, size);
667
668    Ok(WasmBuildResult {
669        wasm_path: final_path,
670        language,
671        target: config.target,
672        size,
673    })
674}
675
676/// Verify that the required build tool is available
677async fn verify_build_tool(language: WasmLanguage) -> Result<()> {
678    let (tool, check_cmd) = match language {
679        WasmLanguage::Rust | WasmLanguage::RustComponent => ("cargo", vec!["cargo", "--version"]),
680        WasmLanguage::Go => ("tinygo", vec!["tinygo", "version"]),
681        WasmLanguage::Python => ("componentize-py", vec!["componentize-py", "--version"]),
682        WasmLanguage::TypeScript | WasmLanguage::AssemblyScript => {
683            ("npx", vec!["npx", "--version"])
684        }
685        WasmLanguage::C => ("clang", vec!["clang", "--version"]),
686        WasmLanguage::Zig => ("zig", vec!["zig", "version"]),
687    };
688
689    debug!("Checking for tool: {}", tool);
690
691    let result = Command::new(check_cmd[0])
692        .args(&check_cmd[1..])
693        .output()
694        .await;
695
696    match result {
697        Ok(output) if output.status.success() => {
698            trace!("{} is available", tool);
699            Ok(())
700        }
701        Ok(output) => {
702            let stderr = String::from_utf8_lossy(&output.stderr);
703            Err(WasmBuildError::ToolNotFound {
704                tool: tool.to_string(),
705                message: format!("Command failed: {}", stderr),
706            })
707        }
708        Err(e) => Err(WasmBuildError::ToolNotFound {
709            tool: tool.to_string(),
710            message: format!("Not found in PATH: {}", e),
711        }),
712    }
713}
714
715/// Execute the build command
716async fn execute_build_command(context: &Path, cmd: &[String]) -> Result<std::process::Output> {
717    let mut command = Command::new(&cmd[0]);
718    command
719        .args(&cmd[1..])
720        .current_dir(context)
721        .stdout(std::process::Stdio::piped())
722        .stderr(std::process::Stdio::piped());
723
724    debug!("Executing: {} in {}", cmd.join(" "), context.display());
725
726    command.output().await.map_err(WasmBuildError::Io)
727}
728
729/// Find the WASM output file after a successful build
730fn find_wasm_output(
731    context: &Path,
732    language: WasmLanguage,
733    target: WasiTarget,
734    release: bool,
735) -> Result<PathBuf> {
736    // Language-specific output locations
737    let candidates: Vec<PathBuf> = match language {
738        WasmLanguage::Rust => {
739            let profile = if release { "release" } else { "debug" };
740            let target_name = target.rust_target();
741
742            // Try to find the package name from Cargo.toml
743            let package_name =
744                get_rust_package_name(context).unwrap_or_else(|_| "output".to_string());
745
746            vec![
747                context
748                    .join("target")
749                    .join(target_name)
750                    .join(profile)
751                    .join(format!("{}.wasm", package_name)),
752                context
753                    .join("target")
754                    .join(target_name)
755                    .join(profile)
756                    .join(format!("{}.wasm", package_name.replace('-', "_"))),
757            ]
758        }
759
760        WasmLanguage::RustComponent => {
761            let profile = if release { "release" } else { "debug" };
762            let package_name =
763                get_rust_package_name(context).unwrap_or_else(|_| "output".to_string());
764
765            vec![
766                // cargo-component outputs to wasm32-wasip1 or wasm32-wasip2
767                context
768                    .join("target")
769                    .join("wasm32-wasip1")
770                    .join(profile)
771                    .join(format!("{}.wasm", package_name)),
772                context
773                    .join("target")
774                    .join("wasm32-wasip2")
775                    .join(profile)
776                    .join(format!("{}.wasm", package_name)),
777                context
778                    .join("target")
779                    .join("wasm32-wasi")
780                    .join(profile)
781                    .join(format!("{}.wasm", package_name)),
782            ]
783        }
784
785        WasmLanguage::Go => {
786            vec![context.join("main.wasm")]
787        }
788
789        WasmLanguage::Python => {
790            vec![context.join("app.wasm")]
791        }
792
793        WasmLanguage::TypeScript => {
794            vec![
795                context.join("dist").join("component.wasm"),
796                context.join("component.wasm"),
797            ]
798        }
799
800        WasmLanguage::AssemblyScript => {
801            vec![
802                context.join("build").join("release.wasm"),
803                context.join("build").join("debug.wasm"),
804            ]
805        }
806
807        WasmLanguage::C => {
808            vec![context.join("main.wasm")]
809        }
810
811        WasmLanguage::Zig => {
812            vec![
813                context.join("zig-out").join("bin").join("main.wasm"),
814                context.join("zig-out").join("lib").join("main.wasm"),
815            ]
816        }
817    };
818
819    // Find the first existing file
820    for candidate in &candidates {
821        if candidate.exists() {
822            debug!("Found WASM output at: {}", candidate.display());
823            return Ok(candidate.clone());
824        }
825    }
826
827    // If no specific file found, search for any .wasm file
828    if let Some(wasm_path) = find_any_wasm_file(context) {
829        debug!("Found WASM file via search: {}", wasm_path.display());
830        return Ok(wasm_path);
831    }
832
833    Err(WasmBuildError::OutputNotFound {
834        path: candidates
835            .first()
836            .cloned()
837            .unwrap_or_else(|| context.join("output.wasm")),
838    })
839}
840
841/// Get the package name from Cargo.toml
842fn get_rust_package_name(context: &Path) -> Result<String> {
843    let cargo_toml = context.join("Cargo.toml");
844    let content =
845        std::fs::read_to_string(&cargo_toml).map_err(|e| WasmBuildError::ProjectConfigError {
846            message: format!("Failed to read Cargo.toml: {}", e),
847        })?;
848
849    // Simple TOML parsing for package name
850    for line in content.lines() {
851        let line = line.trim();
852        if line.starts_with("name") {
853            if let Some(name) = line
854                .split('=')
855                .nth(1)
856                .map(|s| s.trim().trim_matches('"').trim_matches('\''))
857            {
858                return Ok(name.to_string());
859            }
860        }
861    }
862
863    Err(WasmBuildError::ProjectConfigError {
864        message: "Could not find package name in Cargo.toml".to_string(),
865    })
866}
867
868/// Search for any .wasm file in the project
869fn find_any_wasm_file(context: &Path) -> Option<PathBuf> {
870    // Common output directories to search
871    let search_dirs = [
872        context.to_path_buf(),
873        context.join("target"),
874        context.join("build"),
875        context.join("dist"),
876        context.join("out"),
877        context.join("zig-out"),
878    ];
879
880    for dir in &search_dirs {
881        if let Some(path) = search_wasm_recursive(dir, 3) {
882            return Some(path);
883        }
884    }
885
886    None
887}
888
889/// Recursively search for .wasm files up to a certain depth
890fn search_wasm_recursive(dir: &Path, max_depth: usize) -> Option<PathBuf> {
891    if max_depth == 0 || !dir.is_dir() {
892        return None;
893    }
894
895    if let Ok(entries) = std::fs::read_dir(dir) {
896        for entry in entries.flatten() {
897            let path = entry.path();
898
899            if path.is_file() {
900                if let Some(ext) = path.extension() {
901                    if ext == "wasm" {
902                        return Some(path);
903                    }
904                }
905            } else if path.is_dir() {
906                if let Some(found) = search_wasm_recursive(&path, max_depth - 1) {
907                    return Some(found);
908                }
909            }
910        }
911    }
912
913    None
914}
915
916#[cfg(test)]
917mod tests {
918    use super::*;
919    use std::fs;
920    use tempfile::TempDir;
921
922    fn create_temp_dir() -> TempDir {
923        TempDir::new().expect("Failed to create temp directory")
924    }
925
926    // =========================================================================
927    // WasmLanguage tests
928    // =========================================================================
929
930    mod wasm_language_tests {
931        use super::*;
932
933        #[test]
934        fn test_display_all_variants() {
935            assert_eq!(WasmLanguage::Rust.to_string(), "Rust");
936            assert_eq!(
937                WasmLanguage::RustComponent.to_string(),
938                "Rust (cargo-component)"
939            );
940            assert_eq!(WasmLanguage::Go.to_string(), "Go (TinyGo)");
941            assert_eq!(WasmLanguage::Python.to_string(), "Python");
942            assert_eq!(WasmLanguage::TypeScript.to_string(), "TypeScript");
943            assert_eq!(WasmLanguage::AssemblyScript.to_string(), "AssemblyScript");
944            assert_eq!(WasmLanguage::C.to_string(), "C");
945            assert_eq!(WasmLanguage::Zig.to_string(), "Zig");
946        }
947
948        #[test]
949        fn test_debug_formatting() {
950            // Verify Debug trait is implemented and produces expected output
951            let debug_str = format!("{:?}", WasmLanguage::Rust);
952            assert_eq!(debug_str, "Rust");
953
954            let debug_str = format!("{:?}", WasmLanguage::RustComponent);
955            assert_eq!(debug_str, "RustComponent");
956
957            let debug_str = format!("{:?}", WasmLanguage::Go);
958            assert_eq!(debug_str, "Go");
959
960            let debug_str = format!("{:?}", WasmLanguage::Python);
961            assert_eq!(debug_str, "Python");
962
963            let debug_str = format!("{:?}", WasmLanguage::TypeScript);
964            assert_eq!(debug_str, "TypeScript");
965
966            let debug_str = format!("{:?}", WasmLanguage::AssemblyScript);
967            assert_eq!(debug_str, "AssemblyScript");
968
969            let debug_str = format!("{:?}", WasmLanguage::C);
970            assert_eq!(debug_str, "C");
971
972            let debug_str = format!("{:?}", WasmLanguage::Zig);
973            assert_eq!(debug_str, "Zig");
974        }
975
976        #[test]
977        fn test_clone() {
978            let lang = WasmLanguage::Rust;
979            let cloned = lang;
980            assert_eq!(lang, cloned);
981
982            let lang = WasmLanguage::Python;
983            let cloned = lang;
984            assert_eq!(lang, cloned);
985        }
986
987        #[test]
988        fn test_copy() {
989            let lang = WasmLanguage::Go;
990            let copied = lang; // Copy semantics
991            assert_eq!(lang, copied);
992            // Verify original is still usable (proving Copy, not Move)
993            assert_eq!(lang, WasmLanguage::Go);
994        }
995
996        #[test]
997        fn test_partial_eq() {
998            assert_eq!(WasmLanguage::Rust, WasmLanguage::Rust);
999            assert_ne!(WasmLanguage::Rust, WasmLanguage::Go);
1000            assert_ne!(WasmLanguage::Rust, WasmLanguage::RustComponent);
1001            assert_eq!(WasmLanguage::TypeScript, WasmLanguage::TypeScript);
1002            assert_ne!(WasmLanguage::TypeScript, WasmLanguage::AssemblyScript);
1003        }
1004
1005        #[test]
1006        fn test_name_method() {
1007            assert_eq!(WasmLanguage::Rust.name(), "Rust");
1008            assert_eq!(WasmLanguage::RustComponent.name(), "Rust (cargo-component)");
1009            assert_eq!(WasmLanguage::Go.name(), "Go (TinyGo)");
1010            assert_eq!(WasmLanguage::Python.name(), "Python");
1011            assert_eq!(WasmLanguage::TypeScript.name(), "TypeScript");
1012            assert_eq!(WasmLanguage::AssemblyScript.name(), "AssemblyScript");
1013            assert_eq!(WasmLanguage::C.name(), "C");
1014            assert_eq!(WasmLanguage::Zig.name(), "Zig");
1015        }
1016
1017        #[test]
1018        fn test_all_returns_all_variants() {
1019            let all = WasmLanguage::all();
1020            assert_eq!(all.len(), 8);
1021            assert!(all.contains(&WasmLanguage::Rust));
1022            assert!(all.contains(&WasmLanguage::RustComponent));
1023            assert!(all.contains(&WasmLanguage::Go));
1024            assert!(all.contains(&WasmLanguage::Python));
1025            assert!(all.contains(&WasmLanguage::TypeScript));
1026            assert!(all.contains(&WasmLanguage::AssemblyScript));
1027            assert!(all.contains(&WasmLanguage::C));
1028            assert!(all.contains(&WasmLanguage::Zig));
1029        }
1030
1031        #[test]
1032        fn test_is_component_native() {
1033            // Component-native languages (produce component model output by default)
1034            assert!(WasmLanguage::RustComponent.is_component_native());
1035            assert!(WasmLanguage::Python.is_component_native());
1036            assert!(WasmLanguage::TypeScript.is_component_native());
1037
1038            // Non-component-native languages
1039            assert!(!WasmLanguage::Rust.is_component_native());
1040            assert!(!WasmLanguage::Go.is_component_native());
1041            assert!(!WasmLanguage::AssemblyScript.is_component_native());
1042            assert!(!WasmLanguage::C.is_component_native());
1043            assert!(!WasmLanguage::Zig.is_component_native());
1044        }
1045
1046        #[test]
1047        fn test_hash() {
1048            use std::collections::HashSet;
1049
1050            let mut set = HashSet::new();
1051            set.insert(WasmLanguage::Rust);
1052            set.insert(WasmLanguage::Go);
1053            set.insert(WasmLanguage::Rust); // Duplicate
1054
1055            assert_eq!(set.len(), 2);
1056            assert!(set.contains(&WasmLanguage::Rust));
1057            assert!(set.contains(&WasmLanguage::Go));
1058        }
1059    }
1060
1061    // =========================================================================
1062    // WasiTarget tests
1063    // =========================================================================
1064
1065    mod wasi_target_tests {
1066        use super::*;
1067
1068        #[test]
1069        fn test_default_returns_preview2() {
1070            let target = WasiTarget::default();
1071            assert_eq!(target, WasiTarget::Preview2);
1072        }
1073
1074        #[test]
1075        fn test_display_preview1() {
1076            assert_eq!(WasiTarget::Preview1.to_string(), "WASI Preview 1");
1077        }
1078
1079        #[test]
1080        fn test_display_preview2() {
1081            assert_eq!(WasiTarget::Preview2.to_string(), "WASI Preview 2");
1082        }
1083
1084        #[test]
1085        fn test_debug_formatting() {
1086            let debug_str = format!("{:?}", WasiTarget::Preview1);
1087            assert_eq!(debug_str, "Preview1");
1088
1089            let debug_str = format!("{:?}", WasiTarget::Preview2);
1090            assert_eq!(debug_str, "Preview2");
1091        }
1092
1093        #[test]
1094        fn test_clone() {
1095            let target = WasiTarget::Preview1;
1096            let cloned = target;
1097            assert_eq!(target, cloned);
1098
1099            let target = WasiTarget::Preview2;
1100            let cloned = target;
1101            assert_eq!(target, cloned);
1102        }
1103
1104        #[test]
1105        fn test_copy() {
1106            let target = WasiTarget::Preview1;
1107            let copied = target; // Copy semantics
1108            assert_eq!(target, copied);
1109            // Verify original is still usable (proving Copy, not Move)
1110            assert_eq!(target, WasiTarget::Preview1);
1111        }
1112
1113        #[test]
1114        fn test_partial_eq() {
1115            assert_eq!(WasiTarget::Preview1, WasiTarget::Preview1);
1116            assert_eq!(WasiTarget::Preview2, WasiTarget::Preview2);
1117            assert_ne!(WasiTarget::Preview1, WasiTarget::Preview2);
1118        }
1119
1120        #[test]
1121        fn test_rust_target_preview1() {
1122            assert_eq!(WasiTarget::Preview1.rust_target(), "wasm32-wasip1");
1123        }
1124
1125        #[test]
1126        fn test_rust_target_preview2() {
1127            assert_eq!(WasiTarget::Preview2.rust_target(), "wasm32-wasip2");
1128        }
1129
1130        #[test]
1131        fn test_name_method() {
1132            assert_eq!(WasiTarget::Preview1.name(), "WASI Preview 1");
1133            assert_eq!(WasiTarget::Preview2.name(), "WASI Preview 2");
1134        }
1135
1136        #[test]
1137        fn test_hash() {
1138            use std::collections::HashSet;
1139
1140            let mut set = HashSet::new();
1141            set.insert(WasiTarget::Preview1);
1142            set.insert(WasiTarget::Preview2);
1143            set.insert(WasiTarget::Preview1); // Duplicate
1144
1145            assert_eq!(set.len(), 2);
1146            assert!(set.contains(&WasiTarget::Preview1));
1147            assert!(set.contains(&WasiTarget::Preview2));
1148        }
1149    }
1150
1151    // =========================================================================
1152    // WasmBuildConfig tests
1153    // =========================================================================
1154
1155    mod wasm_build_config_tests {
1156        use super::*;
1157
1158        #[test]
1159        fn test_default_trait() {
1160            let config = WasmBuildConfig::default();
1161
1162            assert_eq!(config.language, None);
1163            assert_eq!(config.target, WasiTarget::Preview2); // Default WasiTarget
1164            assert!(!config.optimize);
1165            assert_eq!(config.wit_path, None);
1166            assert_eq!(config.output_path, None);
1167        }
1168
1169        #[test]
1170        fn test_new_equals_default() {
1171            let new_config = WasmBuildConfig::new();
1172            let default_config = WasmBuildConfig::default();
1173
1174            assert_eq!(new_config.language, default_config.language);
1175            assert_eq!(new_config.target, default_config.target);
1176            assert_eq!(new_config.optimize, default_config.optimize);
1177            assert_eq!(new_config.wit_path, default_config.wit_path);
1178            assert_eq!(new_config.output_path, default_config.output_path);
1179        }
1180
1181        #[test]
1182        fn test_with_language() {
1183            let config = WasmBuildConfig::new().language(WasmLanguage::Rust);
1184            assert_eq!(config.language, Some(WasmLanguage::Rust));
1185
1186            let config = WasmBuildConfig::new().language(WasmLanguage::Python);
1187            assert_eq!(config.language, Some(WasmLanguage::Python));
1188        }
1189
1190        #[test]
1191        fn test_with_target() {
1192            let config = WasmBuildConfig::new().target(WasiTarget::Preview1);
1193            assert_eq!(config.target, WasiTarget::Preview1);
1194
1195            let config = WasmBuildConfig::new().target(WasiTarget::Preview2);
1196            assert_eq!(config.target, WasiTarget::Preview2);
1197        }
1198
1199        #[test]
1200        fn test_with_optimize_true() {
1201            let config = WasmBuildConfig::new().optimize(true);
1202            assert!(config.optimize);
1203        }
1204
1205        #[test]
1206        fn test_with_optimize_false() {
1207            let config = WasmBuildConfig::new().optimize(false);
1208            assert!(!config.optimize);
1209        }
1210
1211        #[test]
1212        fn test_with_wit_path_string() {
1213            let config = WasmBuildConfig::new().wit_path("/path/to/wit");
1214            assert_eq!(config.wit_path, Some(PathBuf::from("/path/to/wit")));
1215        }
1216
1217        #[test]
1218        fn test_with_wit_path_pathbuf() {
1219            let path = PathBuf::from("/another/wit/path");
1220            let config = WasmBuildConfig::new().wit_path(path.clone());
1221            assert_eq!(config.wit_path, Some(path));
1222        }
1223
1224        #[test]
1225        fn test_with_output_path_string() {
1226            let config = WasmBuildConfig::new().output_path("/output/file.wasm");
1227            assert_eq!(config.output_path, Some(PathBuf::from("/output/file.wasm")));
1228        }
1229
1230        #[test]
1231        fn test_with_output_path_pathbuf() {
1232            let path = PathBuf::from("/custom/output.wasm");
1233            let config = WasmBuildConfig::new().output_path(path.clone());
1234            assert_eq!(config.output_path, Some(path));
1235        }
1236
1237        #[test]
1238        fn test_builder_pattern_chaining() {
1239            let config = WasmBuildConfig::new()
1240                .language(WasmLanguage::Go)
1241                .target(WasiTarget::Preview1)
1242                .optimize(true)
1243                .wit_path("/wit")
1244                .output_path("/out.wasm");
1245
1246            assert_eq!(config.language, Some(WasmLanguage::Go));
1247            assert_eq!(config.target, WasiTarget::Preview1);
1248            assert!(config.optimize);
1249            assert_eq!(config.wit_path, Some(PathBuf::from("/wit")));
1250            assert_eq!(config.output_path, Some(PathBuf::from("/out.wasm")));
1251        }
1252
1253        #[test]
1254        fn test_debug_formatting() {
1255            let config = WasmBuildConfig::new().language(WasmLanguage::Rust);
1256            let debug_str = format!("{:?}", config);
1257
1258            assert!(debug_str.contains("WasmBuildConfig"));
1259            assert!(debug_str.contains("Rust"));
1260        }
1261
1262        #[test]
1263        fn test_clone() {
1264            let config = WasmBuildConfig::new()
1265                .language(WasmLanguage::Python)
1266                .optimize(true);
1267
1268            let cloned = config.clone();
1269
1270            assert_eq!(cloned.language, Some(WasmLanguage::Python));
1271            assert!(cloned.optimize);
1272        }
1273    }
1274
1275    // =========================================================================
1276    // WasmBuildResult tests
1277    // =========================================================================
1278
1279    mod wasm_build_result_tests {
1280        use super::*;
1281
1282        #[test]
1283        fn test_struct_creation() {
1284            let result = WasmBuildResult {
1285                wasm_path: PathBuf::from("/path/to/output.wasm"),
1286                language: WasmLanguage::Rust,
1287                target: WasiTarget::Preview2,
1288                size: 1024,
1289            };
1290
1291            assert_eq!(result.wasm_path, PathBuf::from("/path/to/output.wasm"));
1292            assert_eq!(result.language, WasmLanguage::Rust);
1293            assert_eq!(result.target, WasiTarget::Preview2);
1294            assert_eq!(result.size, 1024);
1295        }
1296
1297        #[test]
1298        fn test_struct_creation_all_languages() {
1299            for lang in WasmLanguage::all() {
1300                let result = WasmBuildResult {
1301                    wasm_path: PathBuf::from("/test.wasm"),
1302                    language: *lang,
1303                    target: WasiTarget::Preview1,
1304                    size: 512,
1305                };
1306                assert_eq!(result.language, *lang);
1307            }
1308        }
1309
1310        #[test]
1311        fn test_debug_formatting() {
1312            let result = WasmBuildResult {
1313                wasm_path: PathBuf::from("/test.wasm"),
1314                language: WasmLanguage::Go,
1315                target: WasiTarget::Preview1,
1316                size: 2048,
1317            };
1318
1319            let debug_str = format!("{:?}", result);
1320            assert!(debug_str.contains("WasmBuildResult"));
1321            assert!(debug_str.contains("test.wasm"));
1322            assert!(debug_str.contains("Go"));
1323            assert!(debug_str.contains("2048"));
1324        }
1325
1326        #[test]
1327        fn test_clone() {
1328            let result = WasmBuildResult {
1329                wasm_path: PathBuf::from("/original.wasm"),
1330                language: WasmLanguage::Zig,
1331                target: WasiTarget::Preview2,
1332                size: 4096,
1333            };
1334
1335            let cloned = result.clone();
1336
1337            assert_eq!(cloned.wasm_path, result.wasm_path);
1338            assert_eq!(cloned.language, result.language);
1339            assert_eq!(cloned.target, result.target);
1340            assert_eq!(cloned.size, result.size);
1341        }
1342
1343        #[test]
1344        fn test_zero_size() {
1345            let result = WasmBuildResult {
1346                wasm_path: PathBuf::from("/empty.wasm"),
1347                language: WasmLanguage::C,
1348                target: WasiTarget::Preview1,
1349                size: 0,
1350            };
1351            assert_eq!(result.size, 0);
1352        }
1353
1354        #[test]
1355        fn test_large_size() {
1356            let result = WasmBuildResult {
1357                wasm_path: PathBuf::from("/large.wasm"),
1358                language: WasmLanguage::AssemblyScript,
1359                target: WasiTarget::Preview2,
1360                size: u64::MAX,
1361            };
1362            assert_eq!(result.size, u64::MAX);
1363        }
1364    }
1365
1366    // =========================================================================
1367    // WasmBuildError tests
1368    // =========================================================================
1369
1370    mod wasm_build_error_tests {
1371        use super::*;
1372
1373        #[test]
1374        fn test_display_language_not_detected() {
1375            let err = WasmBuildError::LanguageNotDetected {
1376                path: PathBuf::from("/test/path"),
1377            };
1378            let display = err.to_string();
1379            assert!(display.contains("Could not detect source language"));
1380            assert!(display.contains("/test/path"));
1381        }
1382
1383        #[test]
1384        fn test_display_tool_not_found() {
1385            let err = WasmBuildError::ToolNotFound {
1386                tool: "cargo".to_string(),
1387                message: "Not in PATH".to_string(),
1388            };
1389            let display = err.to_string();
1390            assert!(display.contains("Build tool 'cargo' not found"));
1391            assert!(display.contains("Not in PATH"));
1392        }
1393
1394        #[test]
1395        fn test_display_build_failed() {
1396            let err = WasmBuildError::BuildFailed {
1397                exit_code: 1,
1398                stderr: "compilation error".to_string(),
1399                stdout: "some output".to_string(),
1400            };
1401            let display = err.to_string();
1402            assert!(display.contains("Build failed with exit code 1"));
1403            assert!(display.contains("compilation error"));
1404        }
1405
1406        #[test]
1407        fn test_display_output_not_found() {
1408            let err = WasmBuildError::OutputNotFound {
1409                path: PathBuf::from("/expected/output.wasm"),
1410            };
1411            let display = err.to_string();
1412            assert!(display.contains("WASM output not found"));
1413            assert!(display.contains("/expected/output.wasm"));
1414        }
1415
1416        #[test]
1417        fn test_display_config_error() {
1418            let err = WasmBuildError::ConfigError {
1419                message: "Invalid configuration".to_string(),
1420            };
1421            let display = err.to_string();
1422            assert!(display.contains("Configuration error"));
1423            assert!(display.contains("Invalid configuration"));
1424        }
1425
1426        #[test]
1427        fn test_display_project_config_error() {
1428            let err = WasmBuildError::ProjectConfigError {
1429                message: "Failed to parse Cargo.toml".to_string(),
1430            };
1431            let display = err.to_string();
1432            assert!(display.contains("Failed to read project configuration"));
1433            assert!(display.contains("Failed to parse Cargo.toml"));
1434        }
1435
1436        #[test]
1437        fn test_display_io_error() {
1438            let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
1439            let err = WasmBuildError::Io(io_err);
1440            let display = err.to_string();
1441            assert!(display.contains("IO error"));
1442            assert!(display.contains("file not found"));
1443        }
1444
1445        #[test]
1446        fn test_debug_formatting_all_variants() {
1447            let errors = vec![
1448                WasmBuildError::LanguageNotDetected {
1449                    path: PathBuf::from("/test"),
1450                },
1451                WasmBuildError::ToolNotFound {
1452                    tool: "test".to_string(),
1453                    message: "msg".to_string(),
1454                },
1455                WasmBuildError::BuildFailed {
1456                    exit_code: 0,
1457                    stderr: String::new(),
1458                    stdout: String::new(),
1459                },
1460                WasmBuildError::OutputNotFound {
1461                    path: PathBuf::from("/test"),
1462                },
1463                WasmBuildError::ConfigError {
1464                    message: "test".to_string(),
1465                },
1466                WasmBuildError::ProjectConfigError {
1467                    message: "test".to_string(),
1468                },
1469            ];
1470
1471            for err in errors {
1472                let debug_str = format!("{:?}", err);
1473                assert!(!debug_str.is_empty());
1474            }
1475        }
1476
1477        #[test]
1478        fn test_from_io_error() {
1479            let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
1480            let wasm_err: WasmBuildError = io_err.into();
1481
1482            match wasm_err {
1483                WasmBuildError::Io(e) => {
1484                    assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied);
1485                }
1486                _ => panic!("Expected Io variant"),
1487            }
1488        }
1489
1490        #[test]
1491        fn test_from_io_error_various_kinds() {
1492            let kinds = vec![
1493                std::io::ErrorKind::NotFound,
1494                std::io::ErrorKind::PermissionDenied,
1495                std::io::ErrorKind::AlreadyExists,
1496                std::io::ErrorKind::InvalidData,
1497            ];
1498
1499            for kind in kinds {
1500                let io_err = std::io::Error::new(kind, "test error");
1501                let wasm_err: WasmBuildError = io_err.into();
1502                assert!(matches!(wasm_err, WasmBuildError::Io(_)));
1503            }
1504        }
1505
1506        #[test]
1507        fn test_error_implements_std_error() {
1508            let err = WasmBuildError::ConfigError {
1509                message: "test".to_string(),
1510            };
1511            // Verify it implements std::error::Error by using the trait
1512            let _: &dyn std::error::Error = &err;
1513        }
1514    }
1515
1516    // =========================================================================
1517    // detect_language tests
1518    // =========================================================================
1519
1520    mod detect_language_tests {
1521        use super::*;
1522
1523        #[test]
1524        fn test_detect_cargo_toml_rust() {
1525            let dir = create_temp_dir();
1526            fs::write(
1527                dir.path().join("Cargo.toml"),
1528                r#"[package]
1529name = "test"
1530version = "0.1.0"
1531"#,
1532            )
1533            .unwrap();
1534
1535            let lang = detect_language(dir.path()).unwrap();
1536            assert_eq!(lang, WasmLanguage::Rust);
1537        }
1538
1539        #[test]
1540        fn test_detect_cargo_toml_with_cargo_component_metadata() {
1541            let dir = create_temp_dir();
1542            fs::write(
1543                dir.path().join("Cargo.toml"),
1544                r#"[package]
1545name = "test"
1546version = "0.1.0"
1547
1548[package.metadata.component]
1549package = "test:component"
1550"#,
1551            )
1552            .unwrap();
1553
1554            let lang = detect_language(dir.path()).unwrap();
1555            assert_eq!(lang, WasmLanguage::RustComponent);
1556        }
1557
1558        #[test]
1559        fn test_detect_cargo_toml_with_wit_bindgen_dep() {
1560            let dir = create_temp_dir();
1561            fs::write(
1562                dir.path().join("Cargo.toml"),
1563                r#"[package]
1564name = "test"
1565version = "0.1.0"
1566
1567[dependencies]
1568wit-bindgen = "0.20"
1569"#,
1570            )
1571            .unwrap();
1572
1573            let lang = detect_language(dir.path()).unwrap();
1574            assert_eq!(lang, WasmLanguage::RustComponent);
1575        }
1576
1577        #[test]
1578        fn test_detect_cargo_toml_with_cargo_component_bindings() {
1579            let dir = create_temp_dir();
1580            fs::write(
1581                dir.path().join("Cargo.toml"),
1582                r#"[package]
1583name = "test"
1584version = "0.1.0"
1585
1586[dependencies]
1587cargo-component-bindings = "0.1"
1588"#,
1589            )
1590            .unwrap();
1591
1592            let lang = detect_language(dir.path()).unwrap();
1593            assert_eq!(lang, WasmLanguage::RustComponent);
1594        }
1595
1596        #[test]
1597        fn test_detect_cargo_component_toml_file() {
1598            let dir = create_temp_dir();
1599            fs::write(
1600                dir.path().join("Cargo.toml"),
1601                r#"[package]
1602name = "test"
1603version = "0.1.0"
1604"#,
1605            )
1606            .unwrap();
1607            fs::write(
1608                dir.path().join("cargo-component.toml"),
1609                "# cargo-component config",
1610            )
1611            .unwrap();
1612
1613            let lang = detect_language(dir.path()).unwrap();
1614            assert_eq!(lang, WasmLanguage::RustComponent);
1615        }
1616
1617        #[test]
1618        fn test_detect_go_mod() {
1619            let dir = create_temp_dir();
1620            fs::write(
1621                dir.path().join("go.mod"),
1622                "module example.com/test\n\ngo 1.21\n",
1623            )
1624            .unwrap();
1625
1626            let lang = detect_language(dir.path()).unwrap();
1627            assert_eq!(lang, WasmLanguage::Go);
1628        }
1629
1630        #[test]
1631        fn test_detect_pyproject_toml() {
1632            let dir = create_temp_dir();
1633            fs::write(
1634                dir.path().join("pyproject.toml"),
1635                r#"[project]
1636name = "my-package"
1637version = "0.1.0"
1638"#,
1639            )
1640            .unwrap();
1641
1642            let lang = detect_language(dir.path()).unwrap();
1643            assert_eq!(lang, WasmLanguage::Python);
1644        }
1645
1646        #[test]
1647        fn test_detect_requirements_txt() {
1648            let dir = create_temp_dir();
1649            fs::write(
1650                dir.path().join("requirements.txt"),
1651                "flask==2.0.0\nrequests>=2.25.0",
1652            )
1653            .unwrap();
1654
1655            let lang = detect_language(dir.path()).unwrap();
1656            assert_eq!(lang, WasmLanguage::Python);
1657        }
1658
1659        #[test]
1660        fn test_detect_setup_py() {
1661            let dir = create_temp_dir();
1662            fs::write(
1663                dir.path().join("setup.py"),
1664                r#"from setuptools import setup
1665setup(name="mypackage")
1666"#,
1667            )
1668            .unwrap();
1669
1670            let lang = detect_language(dir.path()).unwrap();
1671            assert_eq!(lang, WasmLanguage::Python);
1672        }
1673
1674        #[test]
1675        fn test_detect_package_json_assemblyscript_in_dependencies() {
1676            let dir = create_temp_dir();
1677            fs::write(
1678                dir.path().join("package.json"),
1679                r#"{"name": "test", "dependencies": {"assemblyscript": "^0.27.0"}}"#,
1680            )
1681            .unwrap();
1682
1683            let lang = detect_language(dir.path()).unwrap();
1684            assert_eq!(lang, WasmLanguage::AssemblyScript);
1685        }
1686
1687        #[test]
1688        fn test_detect_package_json_assemblyscript_in_dev_dependencies() {
1689            let dir = create_temp_dir();
1690            fs::write(
1691                dir.path().join("package.json"),
1692                r#"{"name": "test", "devDependencies": {"assemblyscript": "^0.27.0"}}"#,
1693            )
1694            .unwrap();
1695
1696            let lang = detect_language(dir.path()).unwrap();
1697            assert_eq!(lang, WasmLanguage::AssemblyScript);
1698        }
1699
1700        #[test]
1701        fn test_detect_package_json_assemblyscript_in_scripts() {
1702            let dir = create_temp_dir();
1703            fs::write(
1704                dir.path().join("package.json"),
1705                r#"{"name": "test", "scripts": {"build": "asc assembly/index.ts"}}"#,
1706            )
1707            .unwrap();
1708
1709            let lang = detect_language(dir.path()).unwrap();
1710            assert_eq!(lang, WasmLanguage::AssemblyScript);
1711        }
1712
1713        #[test]
1714        fn test_detect_package_json_assemblyscript_asc_command() {
1715            let dir = create_temp_dir();
1716            fs::write(
1717                dir.path().join("package.json"),
1718                r#"{"name": "test", "scripts": {"compile": "asc"}}"#,
1719            )
1720            .unwrap();
1721
1722            let lang = detect_language(dir.path()).unwrap();
1723            assert_eq!(lang, WasmLanguage::AssemblyScript);
1724        }
1725
1726        #[test]
1727        fn test_detect_package_json_typescript() {
1728            let dir = create_temp_dir();
1729            fs::write(
1730                dir.path().join("package.json"),
1731                r#"{"name": "test", "version": "1.0.0", "devDependencies": {"typescript": "^5.0.0"}}"#,
1732            )
1733            .unwrap();
1734
1735            let lang = detect_language(dir.path()).unwrap();
1736            assert_eq!(lang, WasmLanguage::TypeScript);
1737        }
1738
1739        #[test]
1740        fn test_detect_package_json_plain_no_assemblyscript() {
1741            let dir = create_temp_dir();
1742            fs::write(
1743                dir.path().join("package.json"),
1744                r#"{"name": "test", "version": "1.0.0"}"#,
1745            )
1746            .unwrap();
1747
1748            let lang = detect_language(dir.path()).unwrap();
1749            assert_eq!(lang, WasmLanguage::TypeScript);
1750        }
1751
1752        #[test]
1753        fn test_detect_build_zig() {
1754            let dir = create_temp_dir();
1755            fs::write(
1756                dir.path().join("build.zig"),
1757                r#"const std = @import("std");
1758pub fn build(b: *std.build.Builder) void {}
1759"#,
1760            )
1761            .unwrap();
1762
1763            let lang = detect_language(dir.path()).unwrap();
1764            assert_eq!(lang, WasmLanguage::Zig);
1765        }
1766
1767        #[test]
1768        fn test_detect_makefile_with_c_files() {
1769            let dir = create_temp_dir();
1770            fs::write(dir.path().join("Makefile"), "all:\n\t$(CC) main.c -o main").unwrap();
1771            fs::write(dir.path().join("main.c"), "int main() { return 0; }").unwrap();
1772
1773            let lang = detect_language(dir.path()).unwrap();
1774            assert_eq!(lang, WasmLanguage::C);
1775        }
1776
1777        #[test]
1778        fn test_detect_cmakelists_with_c_files() {
1779            let dir = create_temp_dir();
1780            fs::write(
1781                dir.path().join("CMakeLists.txt"),
1782                "cmake_minimum_required(VERSION 3.10)\nproject(test)",
1783            )
1784            .unwrap();
1785            fs::write(dir.path().join("main.c"), "int main() { return 0; }").unwrap();
1786
1787            let lang = detect_language(dir.path()).unwrap();
1788            assert_eq!(lang, WasmLanguage::C);
1789        }
1790
1791        #[test]
1792        fn test_detect_c_header_file_only() {
1793            let dir = create_temp_dir();
1794            fs::write(
1795                dir.path().join("header.h"),
1796                "#ifndef HEADER_H\n#define HEADER_H\n#endif",
1797            )
1798            .unwrap();
1799
1800            let lang = detect_language(dir.path()).unwrap();
1801            assert_eq!(lang, WasmLanguage::C);
1802        }
1803
1804        #[test]
1805        fn test_detect_c_in_src_directory() {
1806            let dir = create_temp_dir();
1807            let src_dir = dir.path().join("src");
1808            fs::create_dir(&src_dir).unwrap();
1809            fs::write(src_dir.join("main.c"), "int main() { return 0; }").unwrap();
1810
1811            let lang = detect_language(dir.path()).unwrap();
1812            assert_eq!(lang, WasmLanguage::C);
1813        }
1814
1815        #[test]
1816        fn test_detect_empty_directory_error() {
1817            let dir = create_temp_dir();
1818            // Empty directory
1819
1820            let result = detect_language(dir.path());
1821            assert!(matches!(
1822                result,
1823                Err(WasmBuildError::LanguageNotDetected { .. })
1824            ));
1825        }
1826
1827        #[test]
1828        fn test_detect_unknown_files_error() {
1829            let dir = create_temp_dir();
1830            fs::write(dir.path().join("random.txt"), "some text").unwrap();
1831            fs::write(dir.path().join("data.json"), "{}").unwrap();
1832
1833            let result = detect_language(dir.path());
1834            assert!(matches!(
1835                result,
1836                Err(WasmBuildError::LanguageNotDetected { .. })
1837            ));
1838        }
1839
1840        #[test]
1841        fn test_detect_makefile_without_c_files_error() {
1842            let dir = create_temp_dir();
1843            fs::write(dir.path().join("Makefile"), "all:\n\techo hello").unwrap();
1844
1845            let result = detect_language(dir.path());
1846            assert!(matches!(
1847                result,
1848                Err(WasmBuildError::LanguageNotDetected { .. })
1849            ));
1850        }
1851
1852        #[test]
1853        fn test_detect_priority_rust_over_package_json() {
1854            // When both Cargo.toml and package.json exist, Rust takes priority
1855            let dir = create_temp_dir();
1856            fs::write(
1857                dir.path().join("Cargo.toml"),
1858                r#"[package]
1859name = "test"
1860version = "0.1.0"
1861"#,
1862            )
1863            .unwrap();
1864            fs::write(dir.path().join("package.json"), r#"{"name": "test"}"#).unwrap();
1865
1866            let lang = detect_language(dir.path()).unwrap();
1867            assert_eq!(lang, WasmLanguage::Rust);
1868        }
1869
1870        #[test]
1871        fn test_detect_priority_go_over_python() {
1872            // When both go.mod and pyproject.toml exist, Go takes priority
1873            let dir = create_temp_dir();
1874            fs::write(dir.path().join("go.mod"), "module test").unwrap();
1875            fs::write(dir.path().join("pyproject.toml"), "[project]").unwrap();
1876
1877            let lang = detect_language(dir.path()).unwrap();
1878            assert_eq!(lang, WasmLanguage::Go);
1879        }
1880    }
1881
1882    // =========================================================================
1883    // get_build_command tests
1884    // =========================================================================
1885
1886    mod get_build_command_tests {
1887        use super::*;
1888
1889        #[test]
1890        fn test_rust_preview1_release() {
1891            let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview1, true);
1892            assert_eq!(cmd[0], "cargo");
1893            assert_eq!(cmd[1], "build");
1894            assert!(cmd.contains(&"--target".to_string()));
1895            assert!(cmd.contains(&"wasm32-wasip1".to_string()));
1896            assert!(cmd.contains(&"--release".to_string()));
1897        }
1898
1899        #[test]
1900        fn test_rust_preview2_release() {
1901            let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview2, true);
1902            assert_eq!(cmd[0], "cargo");
1903            assert_eq!(cmd[1], "build");
1904            assert!(cmd.contains(&"--target".to_string()));
1905            assert!(cmd.contains(&"wasm32-wasip2".to_string()));
1906            assert!(cmd.contains(&"--release".to_string()));
1907        }
1908
1909        #[test]
1910        fn test_rust_preview1_debug() {
1911            let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview1, false);
1912            assert_eq!(cmd[0], "cargo");
1913            assert!(cmd.contains(&"wasm32-wasip1".to_string()));
1914            assert!(!cmd.contains(&"--release".to_string()));
1915        }
1916
1917        #[test]
1918        fn test_rust_preview2_debug() {
1919            let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview2, false);
1920            assert_eq!(cmd[0], "cargo");
1921            assert!(cmd.contains(&"wasm32-wasip2".to_string()));
1922            assert!(!cmd.contains(&"--release".to_string()));
1923        }
1924
1925        #[test]
1926        fn test_rust_component_release() {
1927            let cmd = get_build_command(WasmLanguage::RustComponent, WasiTarget::Preview2, true);
1928            assert_eq!(cmd[0], "cargo");
1929            assert_eq!(cmd[1], "component");
1930            assert_eq!(cmd[2], "build");
1931            assert!(cmd.contains(&"--release".to_string()));
1932        }
1933
1934        #[test]
1935        fn test_rust_component_debug() {
1936            let cmd = get_build_command(WasmLanguage::RustComponent, WasiTarget::Preview2, false);
1937            assert_eq!(cmd[0], "cargo");
1938            assert_eq!(cmd[1], "component");
1939            assert_eq!(cmd[2], "build");
1940            assert!(!cmd.contains(&"--release".to_string()));
1941        }
1942
1943        #[test]
1944        fn test_go_preview1() {
1945            let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview1, false);
1946            assert_eq!(cmd[0], "tinygo");
1947            assert_eq!(cmd[1], "build");
1948            assert!(cmd.contains(&"-target".to_string()));
1949            assert!(cmd.contains(&"wasip1".to_string()));
1950        }
1951
1952        #[test]
1953        fn test_go_preview2() {
1954            let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview2, false);
1955            assert_eq!(cmd[0], "tinygo");
1956            assert!(cmd.contains(&"-target".to_string()));
1957            assert!(cmd.contains(&"wasip2".to_string()));
1958        }
1959
1960        #[test]
1961        fn test_go_release_optimization() {
1962            let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview2, true);
1963            assert_eq!(cmd[0], "tinygo");
1964            assert!(cmd.contains(&"-opt".to_string()));
1965            assert!(cmd.contains(&"2".to_string()));
1966        }
1967
1968        #[test]
1969        fn test_go_debug_no_optimization() {
1970            let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview2, false);
1971            assert_eq!(cmd[0], "tinygo");
1972            assert!(!cmd.contains(&"-opt".to_string()));
1973        }
1974
1975        #[test]
1976        fn test_tinygo_correct_target_flag() {
1977            // TinyGo uses -target (single dash) not --target
1978            let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview1, false);
1979            assert!(cmd.contains(&"-target".to_string()));
1980            assert!(!cmd.contains(&"--target".to_string()));
1981        }
1982
1983        #[test]
1984        fn test_python() {
1985            let cmd = get_build_command(WasmLanguage::Python, WasiTarget::Preview2, true);
1986            assert_eq!(cmd[0], "componentize-py");
1987            assert!(cmd.contains(&"-d".to_string()));
1988            assert!(cmd.contains(&"wit".to_string()));
1989            assert!(cmd.contains(&"-w".to_string()));
1990            assert!(cmd.contains(&"world".to_string()));
1991            assert!(cmd.contains(&"componentize".to_string()));
1992            assert!(cmd.contains(&"app".to_string()));
1993            assert!(cmd.contains(&"-o".to_string()));
1994            assert!(cmd.contains(&"app.wasm".to_string()));
1995        }
1996
1997        #[test]
1998        fn test_python_release_same_as_debug() {
1999            // componentize-py doesn't have explicit optimization flags
2000            let release_cmd = get_build_command(WasmLanguage::Python, WasiTarget::Preview2, true);
2001            let debug_cmd = get_build_command(WasmLanguage::Python, WasiTarget::Preview2, false);
2002            assert_eq!(release_cmd, debug_cmd);
2003        }
2004
2005        #[test]
2006        fn test_componentize_py_arguments() {
2007            let cmd = get_build_command(WasmLanguage::Python, WasiTarget::Preview2, false);
2008            // Verify the order and presence of key arguments
2009            let cmd_str = cmd.join(" ");
2010            assert!(
2011                cmd_str.contains("componentize-py -d wit -w world componentize app -o app.wasm")
2012            );
2013        }
2014
2015        #[test]
2016        fn test_typescript() {
2017            let cmd = get_build_command(WasmLanguage::TypeScript, WasiTarget::Preview2, true);
2018            assert_eq!(cmd[0], "npx");
2019            assert!(cmd.contains(&"jco".to_string()));
2020            assert!(cmd.contains(&"componentize".to_string()));
2021            assert!(cmd.contains(&"--wit".to_string()));
2022        }
2023
2024        #[test]
2025        fn test_assemblyscript_release() {
2026            let cmd = get_build_command(WasmLanguage::AssemblyScript, WasiTarget::Preview2, true);
2027            assert_eq!(cmd[0], "npx");
2028            assert!(cmd.contains(&"asc".to_string()));
2029            assert!(cmd.contains(&"--optimize".to_string()));
2030        }
2031
2032        #[test]
2033        fn test_assemblyscript_debug() {
2034            let cmd = get_build_command(WasmLanguage::AssemblyScript, WasiTarget::Preview2, false);
2035            assert_eq!(cmd[0], "npx");
2036            assert!(cmd.contains(&"asc".to_string()));
2037            assert!(!cmd.contains(&"--optimize".to_string()));
2038        }
2039
2040        #[test]
2041        fn test_c_release() {
2042            let cmd = get_build_command(WasmLanguage::C, WasiTarget::Preview1, true);
2043            assert_eq!(cmd[0], "clang");
2044            assert!(cmd.contains(&"--target=wasm32-wasi".to_string()));
2045            assert!(cmd.contains(&"-O2".to_string()));
2046        }
2047
2048        #[test]
2049        fn test_c_debug() {
2050            let cmd = get_build_command(WasmLanguage::C, WasiTarget::Preview1, false);
2051            assert_eq!(cmd[0], "clang");
2052            assert!(cmd.contains(&"--target=wasm32-wasi".to_string()));
2053            assert!(!cmd.contains(&"-O2".to_string()));
2054        }
2055
2056        #[test]
2057        fn test_zig_release() {
2058            let cmd = get_build_command(WasmLanguage::Zig, WasiTarget::Preview1, true);
2059            assert_eq!(cmd[0], "zig");
2060            assert_eq!(cmd[1], "build");
2061            assert!(cmd.contains(&"-Dtarget=wasm32-wasi".to_string()));
2062            assert!(cmd.contains(&"-Doptimize=ReleaseFast".to_string()));
2063        }
2064
2065        #[test]
2066        fn test_zig_debug() {
2067            let cmd = get_build_command(WasmLanguage::Zig, WasiTarget::Preview1, false);
2068            assert_eq!(cmd[0], "zig");
2069            assert_eq!(cmd[1], "build");
2070            assert!(cmd.contains(&"-Dtarget=wasm32-wasi".to_string()));
2071            assert!(!cmd.contains(&"-Doptimize=ReleaseFast".to_string()));
2072        }
2073
2074        #[test]
2075        fn test_cargo_uses_double_dash_target() {
2076            // cargo uses --target (double dash)
2077            let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview1, false);
2078            assert!(cmd.contains(&"--target".to_string()));
2079        }
2080
2081        #[test]
2082        fn test_go_output_file() {
2083            let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview1, false);
2084            assert!(cmd.contains(&"-o".to_string()));
2085            assert!(cmd.contains(&"main.wasm".to_string()));
2086        }
2087
2088        #[test]
2089        fn test_all_commands_non_empty() {
2090            for lang in WasmLanguage::all() {
2091                for target in [WasiTarget::Preview1, WasiTarget::Preview2] {
2092                    for release in [true, false] {
2093                        let cmd = get_build_command(*lang, target, release);
2094                        assert!(
2095                            !cmd.is_empty(),
2096                            "Command for {:?}/{:?}/{} should not be empty",
2097                            lang,
2098                            target,
2099                            release
2100                        );
2101                        assert!(
2102                            !cmd[0].is_empty(),
2103                            "First command element should not be empty"
2104                        );
2105                    }
2106                }
2107            }
2108        }
2109    }
2110
2111    // =========================================================================
2112    // Additional helper function tests
2113    // =========================================================================
2114
2115    mod helper_function_tests {
2116        use super::*;
2117
2118        #[test]
2119        fn test_get_rust_package_name_success() {
2120            let dir = create_temp_dir();
2121            fs::write(
2122                dir.path().join("Cargo.toml"),
2123                r#"[package]
2124name = "my-cool-package"
2125version = "0.1.0"
2126"#,
2127            )
2128            .unwrap();
2129
2130            let name = get_rust_package_name(dir.path()).unwrap();
2131            assert_eq!(name, "my-cool-package");
2132        }
2133
2134        #[test]
2135        fn test_get_rust_package_name_with_single_quotes() {
2136            let dir = create_temp_dir();
2137            fs::write(
2138                dir.path().join("Cargo.toml"),
2139                "[package]\nname = 'single-quoted'\nversion = '0.1.0'\n",
2140            )
2141            .unwrap();
2142
2143            let name = get_rust_package_name(dir.path()).unwrap();
2144            assert_eq!(name, "single-quoted");
2145        }
2146
2147        #[test]
2148        fn test_get_rust_package_name_missing_file() {
2149            let dir = create_temp_dir();
2150            // No Cargo.toml
2151
2152            let result = get_rust_package_name(dir.path());
2153            assert!(matches!(
2154                result,
2155                Err(WasmBuildError::ProjectConfigError { .. })
2156            ));
2157        }
2158
2159        #[test]
2160        fn test_get_rust_package_name_no_name_field() {
2161            let dir = create_temp_dir();
2162            fs::write(
2163                dir.path().join("Cargo.toml"),
2164                "[package]\nversion = \"0.1.0\"\n",
2165            )
2166            .unwrap();
2167
2168            let result = get_rust_package_name(dir.path());
2169            assert!(matches!(
2170                result,
2171                Err(WasmBuildError::ProjectConfigError { .. })
2172            ));
2173        }
2174
2175        #[test]
2176        fn test_find_any_wasm_file_in_root() {
2177            let dir = create_temp_dir();
2178            fs::write(dir.path().join("test.wasm"), "wasm content").unwrap();
2179
2180            let found = find_any_wasm_file(dir.path());
2181            assert!(found.is_some());
2182            assert!(found.unwrap().ends_with("test.wasm"));
2183        }
2184
2185        #[test]
2186        fn test_find_any_wasm_file_in_target() {
2187            let dir = create_temp_dir();
2188            let target_dir = dir.path().join("target");
2189            fs::create_dir(&target_dir).unwrap();
2190            fs::write(target_dir.join("output.wasm"), "wasm content").unwrap();
2191
2192            let found = find_any_wasm_file(dir.path());
2193            assert!(found.is_some());
2194        }
2195
2196        #[test]
2197        fn test_find_any_wasm_file_in_build() {
2198            let dir = create_temp_dir();
2199            let build_dir = dir.path().join("build");
2200            fs::create_dir(&build_dir).unwrap();
2201            fs::write(build_dir.join("module.wasm"), "wasm content").unwrap();
2202
2203            let found = find_any_wasm_file(dir.path());
2204            assert!(found.is_some());
2205        }
2206
2207        #[test]
2208        fn test_find_any_wasm_file_in_dist() {
2209            let dir = create_temp_dir();
2210            let dist_dir = dir.path().join("dist");
2211            fs::create_dir(&dist_dir).unwrap();
2212            fs::write(dist_dir.join("bundle.wasm"), "wasm content").unwrap();
2213
2214            let found = find_any_wasm_file(dir.path());
2215            assert!(found.is_some());
2216        }
2217
2218        #[test]
2219        fn test_find_any_wasm_file_nested() {
2220            let dir = create_temp_dir();
2221            let nested = dir
2222                .path()
2223                .join("target")
2224                .join("wasm32-wasip2")
2225                .join("release");
2226            fs::create_dir_all(&nested).unwrap();
2227            fs::write(nested.join("deep.wasm"), "wasm content").unwrap();
2228
2229            let found = find_any_wasm_file(dir.path());
2230            assert!(found.is_some());
2231        }
2232
2233        #[test]
2234        fn test_find_any_wasm_file_none() {
2235            let dir = create_temp_dir();
2236            fs::write(dir.path().join("not_wasm.txt"), "text").unwrap();
2237
2238            let found = find_any_wasm_file(dir.path());
2239            assert!(found.is_none());
2240        }
2241
2242        #[test]
2243        fn test_find_any_wasm_file_respects_depth_limit() {
2244            let dir = create_temp_dir();
2245            // Create a deeply nested wasm file (beyond max_depth of 3)
2246            let deep = dir.path().join("a").join("b").join("c").join("d").join("e");
2247            fs::create_dir_all(&deep).unwrap();
2248            fs::write(deep.join("too_deep.wasm"), "wasm").unwrap();
2249
2250            // The recursive search has max_depth of 3, so this might not be found
2251            // depending on the search order
2252            let _ = find_any_wasm_file(dir.path());
2253            // We don't assert the result as it depends on implementation details
2254        }
2255
2256        #[test]
2257        fn test_has_c_source_files_true_c() {
2258            let dir = create_temp_dir();
2259            fs::write(dir.path().join("main.c"), "int main() {}").unwrap();
2260
2261            assert!(has_c_source_files(dir.path()));
2262        }
2263
2264        #[test]
2265        fn test_has_c_source_files_true_h() {
2266            let dir = create_temp_dir();
2267            fs::write(dir.path().join("header.h"), "#pragma once").unwrap();
2268
2269            assert!(has_c_source_files(dir.path()));
2270        }
2271
2272        #[test]
2273        fn test_has_c_source_files_in_src() {
2274            let dir = create_temp_dir();
2275            let src = dir.path().join("src");
2276            fs::create_dir(&src).unwrap();
2277            fs::write(src.join("lib.c"), "void foo() {}").unwrap();
2278
2279            assert!(has_c_source_files(dir.path()));
2280        }
2281
2282        #[test]
2283        fn test_has_c_source_files_false() {
2284            let dir = create_temp_dir();
2285            fs::write(dir.path().join("main.rs"), "fn main() {}").unwrap();
2286
2287            assert!(!has_c_source_files(dir.path()));
2288        }
2289
2290        #[test]
2291        fn test_is_cargo_component_project_with_metadata() {
2292            let dir = create_temp_dir();
2293            let cargo_toml = dir.path().join("Cargo.toml");
2294            fs::write(
2295                &cargo_toml,
2296                r#"[package]
2297name = "test"
2298[package.metadata.component]
2299package = "test:component"
2300"#,
2301            )
2302            .unwrap();
2303
2304            assert!(is_cargo_component_project(&cargo_toml).unwrap());
2305        }
2306
2307        #[test]
2308        fn test_is_cargo_component_project_with_wit_bindgen() {
2309            let dir = create_temp_dir();
2310            let cargo_toml = dir.path().join("Cargo.toml");
2311            fs::write(
2312                &cargo_toml,
2313                r#"[package]
2314name = "test"
2315[dependencies]
2316wit-bindgen = "0.20"
2317"#,
2318            )
2319            .unwrap();
2320
2321            assert!(is_cargo_component_project(&cargo_toml).unwrap());
2322        }
2323
2324        #[test]
2325        fn test_is_cargo_component_project_plain_rust() {
2326            let dir = create_temp_dir();
2327            let cargo_toml = dir.path().join("Cargo.toml");
2328            fs::write(
2329                &cargo_toml,
2330                r#"[package]
2331name = "test"
2332version = "0.1.0"
2333"#,
2334            )
2335            .unwrap();
2336
2337            assert!(!is_cargo_component_project(&cargo_toml).unwrap());
2338        }
2339
2340        #[test]
2341        fn test_is_assemblyscript_project_dependencies() {
2342            let dir = create_temp_dir();
2343            let package_json = dir.path().join("package.json");
2344            fs::write(
2345                &package_json,
2346                r#"{"dependencies": {"assemblyscript": "^0.27"}}"#,
2347            )
2348            .unwrap();
2349
2350            assert!(is_assemblyscript_project(&package_json).unwrap());
2351        }
2352
2353        #[test]
2354        fn test_is_assemblyscript_project_dev_dependencies() {
2355            let dir = create_temp_dir();
2356            let package_json = dir.path().join("package.json");
2357            fs::write(
2358                &package_json,
2359                r#"{"devDependencies": {"assemblyscript": "^0.27"}}"#,
2360            )
2361            .unwrap();
2362
2363            assert!(is_assemblyscript_project(&package_json).unwrap());
2364        }
2365
2366        #[test]
2367        fn test_is_assemblyscript_project_script_with_asc() {
2368            let dir = create_temp_dir();
2369            let package_json = dir.path().join("package.json");
2370            fs::write(
2371                &package_json,
2372                r#"{"scripts": {"build": "asc assembly/index.ts"}}"#,
2373            )
2374            .unwrap();
2375
2376            assert!(is_assemblyscript_project(&package_json).unwrap());
2377        }
2378
2379        #[test]
2380        fn test_is_assemblyscript_project_false() {
2381            let dir = create_temp_dir();
2382            let package_json = dir.path().join("package.json");
2383            fs::write(&package_json, r#"{"dependencies": {"react": "^18.0.0"}}"#).unwrap();
2384
2385            assert!(!is_assemblyscript_project(&package_json).unwrap());
2386        }
2387
2388        #[test]
2389        fn test_is_assemblyscript_project_invalid_json() {
2390            let dir = create_temp_dir();
2391            let package_json = dir.path().join("package.json");
2392            fs::write(&package_json, "not valid json").unwrap();
2393
2394            let result = is_assemblyscript_project(&package_json);
2395            assert!(matches!(
2396                result,
2397                Err(WasmBuildError::ProjectConfigError { .. })
2398            ));
2399        }
2400    }
2401
2402    // =========================================================================
2403    // find_wasm_output tests
2404    // =========================================================================
2405
2406    mod find_wasm_output_tests {
2407        use super::*;
2408
2409        #[test]
2410        fn test_find_rust_release_output() {
2411            let dir = create_temp_dir();
2412            fs::write(
2413                dir.path().join("Cargo.toml"),
2414                "[package]\nname = \"myapp\"\nversion = \"0.1.0\"\n",
2415            )
2416            .unwrap();
2417
2418            let output_dir = dir
2419                .path()
2420                .join("target")
2421                .join("wasm32-wasip2")
2422                .join("release");
2423            fs::create_dir_all(&output_dir).unwrap();
2424            fs::write(output_dir.join("myapp.wasm"), "wasm").unwrap();
2425
2426            let result =
2427                find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview2, true);
2428            assert!(result.is_ok());
2429            assert!(result.unwrap().ends_with("myapp.wasm"));
2430        }
2431
2432        #[test]
2433        fn test_find_rust_debug_output() {
2434            let dir = create_temp_dir();
2435            fs::write(
2436                dir.path().join("Cargo.toml"),
2437                "[package]\nname = \"myapp\"\nversion = \"0.1.0\"\n",
2438            )
2439            .unwrap();
2440
2441            let output_dir = dir
2442                .path()
2443                .join("target")
2444                .join("wasm32-wasip1")
2445                .join("debug");
2446            fs::create_dir_all(&output_dir).unwrap();
2447            fs::write(output_dir.join("myapp.wasm"), "wasm").unwrap();
2448
2449            let result =
2450                find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview1, false);
2451            assert!(result.is_ok());
2452        }
2453
2454        #[test]
2455        fn test_find_rust_underscore_name() {
2456            let dir = create_temp_dir();
2457            fs::write(
2458                dir.path().join("Cargo.toml"),
2459                "[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
2460            )
2461            .unwrap();
2462
2463            let output_dir = dir
2464                .path()
2465                .join("target")
2466                .join("wasm32-wasip2")
2467                .join("release");
2468            fs::create_dir_all(&output_dir).unwrap();
2469            // Rust converts hyphens to underscores in the output filename
2470            fs::write(output_dir.join("my_app.wasm"), "wasm").unwrap();
2471
2472            let result =
2473                find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview2, true);
2474            assert!(result.is_ok());
2475        }
2476
2477        #[test]
2478        fn test_find_go_output() {
2479            let dir = create_temp_dir();
2480            fs::write(dir.path().join("main.wasm"), "wasm").unwrap();
2481
2482            let result =
2483                find_wasm_output(dir.path(), WasmLanguage::Go, WasiTarget::Preview1, false);
2484            assert!(result.is_ok());
2485            assert!(result.unwrap().ends_with("main.wasm"));
2486        }
2487
2488        #[test]
2489        fn test_find_python_output() {
2490            let dir = create_temp_dir();
2491            fs::write(dir.path().join("app.wasm"), "wasm").unwrap();
2492
2493            let result =
2494                find_wasm_output(dir.path(), WasmLanguage::Python, WasiTarget::Preview2, true);
2495            assert!(result.is_ok());
2496            assert!(result.unwrap().ends_with("app.wasm"));
2497        }
2498
2499        #[test]
2500        fn test_find_typescript_output() {
2501            let dir = create_temp_dir();
2502            let dist_dir = dir.path().join("dist");
2503            fs::create_dir(&dist_dir).unwrap();
2504            fs::write(dist_dir.join("component.wasm"), "wasm").unwrap();
2505
2506            let result = find_wasm_output(
2507                dir.path(),
2508                WasmLanguage::TypeScript,
2509                WasiTarget::Preview2,
2510                true,
2511            );
2512            assert!(result.is_ok());
2513        }
2514
2515        #[test]
2516        fn test_find_assemblyscript_release_output() {
2517            let dir = create_temp_dir();
2518            let build_dir = dir.path().join("build");
2519            fs::create_dir(&build_dir).unwrap();
2520            fs::write(build_dir.join("release.wasm"), "wasm").unwrap();
2521
2522            let result = find_wasm_output(
2523                dir.path(),
2524                WasmLanguage::AssemblyScript,
2525                WasiTarget::Preview2,
2526                true,
2527            );
2528            assert!(result.is_ok());
2529        }
2530
2531        #[test]
2532        fn test_find_c_output() {
2533            let dir = create_temp_dir();
2534            fs::write(dir.path().join("main.wasm"), "wasm").unwrap();
2535
2536            let result = find_wasm_output(dir.path(), WasmLanguage::C, WasiTarget::Preview1, true);
2537            assert!(result.is_ok());
2538        }
2539
2540        #[test]
2541        fn test_find_zig_output() {
2542            let dir = create_temp_dir();
2543            let zig_out = dir.path().join("zig-out").join("bin");
2544            fs::create_dir_all(&zig_out).unwrap();
2545            fs::write(zig_out.join("main.wasm"), "wasm").unwrap();
2546
2547            let result =
2548                find_wasm_output(dir.path(), WasmLanguage::Zig, WasiTarget::Preview1, true);
2549            assert!(result.is_ok());
2550        }
2551
2552        #[test]
2553        fn test_find_output_not_found() {
2554            let dir = create_temp_dir();
2555            // No wasm files
2556
2557            let result =
2558                find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview2, true);
2559            assert!(matches!(result, Err(WasmBuildError::OutputNotFound { .. })));
2560        }
2561
2562        #[test]
2563        fn test_find_output_fallback_search() {
2564            let dir = create_temp_dir();
2565            fs::write(
2566                dir.path().join("Cargo.toml"),
2567                "[package]\nname = \"test\"\n",
2568            )
2569            .unwrap();
2570
2571            // Put wasm in unexpected location
2572            let other_dir = dir.path().join("target").join("other");
2573            fs::create_dir_all(&other_dir).unwrap();
2574            fs::write(other_dir.join("unexpected.wasm"), "wasm").unwrap();
2575
2576            // Should find via fallback search
2577            let result =
2578                find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview2, true);
2579            assert!(result.is_ok());
2580        }
2581    }
2582}