Skip to main content

depyler_core/
cargo_first.rs

1//! Cargo-First Compilation Strategy (DEPYLER-CARGO-FIRST)
2//!
3//! Implements the "Jidoka" approach to verification: instead of running bare `rustc`,
4//! we create ephemeral Cargo workspaces with proper dependency resolution.
5//!
6//! This eliminates false-positive compilation failures from missing external crates
7//! (E0432 errors), allowing Hunt Mode to focus on true semantic defects.
8//!
9//! # Toyota Way Principles
10//! - **Jidoka**: Automatically provide necessary resources (dependencies)
11//! - **Poka-Yoke**: Fail-safe compilation that can't miss dependencies
12//! - **Heijunka**: Standardized build environment for all outputs
13
14use anyhow::{Context, Result};
15use std::path::{Path, PathBuf};
16use std::process::{Command, Output};
17use tempfile::TempDir;
18
19/// Result of a Cargo check operation
20#[derive(Debug, Clone)]
21pub struct CheckResult {
22    /// Whether compilation succeeded
23    pub success: bool,
24    /// Compiler errors (if any)
25    pub errors: Vec<CompilerError>,
26    /// Compiler warnings
27    pub warnings: Vec<CompilerWarning>,
28    /// Raw stderr output
29    pub stderr: String,
30}
31
32/// A structured compiler error from cargo check --message-format=json
33#[derive(Debug, Clone)]
34pub struct CompilerError {
35    /// Error code (e.g., "E0308")
36    pub code: Option<String>,
37    /// Error message
38    pub message: String,
39    /// Primary span location
40    pub span: Option<ErrorSpan>,
41    /// Is this a true semantic error vs dependency error?
42    pub is_semantic: bool,
43}
44
45/// A compiler warning
46#[derive(Debug, Clone)]
47pub struct CompilerWarning {
48    pub code: Option<String>,
49    pub message: String,
50}
51
52/// Source location of an error
53#[derive(Debug, Clone)]
54pub struct ErrorSpan {
55    pub file: String,
56    pub line_start: u32,
57    pub line_end: u32,
58    pub column_start: u32,
59    pub column_end: u32,
60}
61
62/// Ephemeral Cargo workspace for compilation verification
63///
64/// Creates a temporary directory with:
65/// - `Cargo.toml` with detected dependencies
66/// - `src/lib.rs` with the Rust code to verify
67///
68/// # Example
69/// ```ignore
70/// let workspace = EphemeralWorkspace::new("my_module", rust_code, &deps)?;
71/// let result = workspace.check()?;
72/// if result.success {
73///     println!("Code compiles!");
74/// }
75/// ```
76pub struct EphemeralWorkspace {
77    /// Temporary directory (auto-cleaned on drop)
78    dir: TempDir,
79    /// Module name
80    #[allow(dead_code)]
81    name: String,
82    /// Path to the generated lib.rs
83    lib_path: PathBuf,
84}
85
86impl EphemeralWorkspace {
87    /// Create a new ephemeral workspace with the given Rust code
88    ///
89    /// # Arguments
90    /// * `name` - Module name (used in Cargo.toml)
91    /// * `rust_code` - The Rust source code to verify
92    /// * `cargo_toml` - Pre-generated Cargo.toml content
93    ///
94    /// # Jidoka Principle
95    /// Automatically sets up the build environment with all necessary dependencies,
96    /// eliminating manual configuration errors.
97    pub fn new(name: &str, rust_code: &str, cargo_toml: &str) -> Result<Self> {
98        let dir = TempDir::new().context("Failed to create temp directory")?;
99        let dir_path = dir.path();
100
101        // Create src directory
102        let src_dir = dir_path.join("src");
103        std::fs::create_dir_all(&src_dir).context("Failed to create src directory")?;
104
105        // Write Cargo.toml
106        let cargo_path = dir_path.join("Cargo.toml");
107        std::fs::write(&cargo_path, cargo_toml).context("Failed to write Cargo.toml")?;
108
109        // Write src/lib.rs
110        let lib_path = src_dir.join("lib.rs");
111        std::fs::write(&lib_path, rust_code).context("Failed to write lib.rs")?;
112
113        Ok(Self {
114            dir,
115            name: name.to_string(),
116            lib_path,
117        })
118    }
119
120    /// Get the path to the workspace directory
121    pub fn path(&self) -> &Path {
122        self.dir.path()
123    }
124
125    /// Get the path to lib.rs
126    pub fn lib_path(&self) -> &Path {
127        &self.lib_path
128    }
129
130    /// Run `cargo check` and parse the results
131    ///
132    /// # Poka-Yoke Principle
133    /// Uses `--message-format=json` for structured error parsing,
134    /// making it impossible to miss or misinterpret compiler messages.
135    ///
136    /// # Coverage Mode Compatibility
137    /// Clears LLVM coverage environment variables to prevent interference
138    /// when running under cargo-llvm-cov. These vars cause compilation
139    /// issues in spawned cargo subprocesses.
140    pub fn check(&self) -> Result<CheckResult> {
141        let output = Command::new("cargo")
142            .arg("check")
143            .arg("--message-format=json")
144            .current_dir(self.dir.path())
145            // Clear LLVM coverage environment to prevent interference with sub-cargo
146            .env_remove("CARGO_LLVM_COV")
147            .env_remove("CARGO_LLVM_COV_SHOW_ENV")
148            .env_remove("CARGO_LLVM_COV_TARGET_DIR")
149            .env_remove("LLVM_PROFILE_FILE")
150            .env_remove("RUSTFLAGS")
151            .env_remove("CARGO_INCREMENTAL")
152            .env_remove("CARGO_BUILD_JOBS")
153            .env_remove("CARGO_TARGET_DIR")
154            .output()
155            .context("Failed to run cargo check")?;
156
157        self.parse_cargo_output(output)
158    }
159
160    /// Parse cargo check JSON output into structured results
161    fn parse_cargo_output(&self, output: Output) -> Result<CheckResult> {
162        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
163        let stdout = String::from_utf8_lossy(&output.stdout);
164
165        let mut errors = Vec::new();
166        let mut warnings = Vec::new();
167
168        // Parse JSON messages from stdout
169        for line in stdout.lines() {
170            if let Ok(msg) = serde_json::from_str::<serde_json::Value>(line) {
171                if let Some(reason) = msg.get("reason").and_then(|r| r.as_str()) {
172                    if reason == "compiler-message" {
173                        if let Some(message) = msg.get("message") {
174                            self.parse_compiler_message(message, &mut errors, &mut warnings);
175                        }
176                    }
177                }
178            }
179        }
180
181        Ok(CheckResult {
182            success: output.status.success(),
183            errors,
184            warnings,
185            stderr,
186        })
187    }
188
189    /// Parse a single compiler message
190    fn parse_compiler_message(
191        &self,
192        message: &serde_json::Value,
193        errors: &mut Vec<CompilerError>,
194        warnings: &mut Vec<CompilerWarning>,
195    ) {
196        let level = message.get("level").and_then(|l| l.as_str()).unwrap_or("");
197        let msg_text = message
198            .get("message")
199            .and_then(|m| m.as_str())
200            .unwrap_or("")
201            .to_string();
202        let code = message
203            .get("code")
204            .and_then(|c| c.get("code"))
205            .and_then(|c| c.as_str())
206            .map(|s| s.to_string());
207
208        // Parse span if available
209        let span = message
210            .get("spans")
211            .and_then(|s| s.as_array())
212            .and_then(|arr| arr.first())
213            .map(|s| ErrorSpan {
214                file: s
215                    .get("file_name")
216                    .and_then(|f| f.as_str())
217                    .unwrap_or("")
218                    .to_string(),
219                line_start: s.get("line_start").and_then(|l| l.as_u64()).unwrap_or(0) as u32,
220                line_end: s.get("line_end").and_then(|l| l.as_u64()).unwrap_or(0) as u32,
221                column_start: s.get("column_start").and_then(|c| c.as_u64()).unwrap_or(0) as u32,
222                column_end: s.get("column_end").and_then(|c| c.as_u64()).unwrap_or(0) as u32,
223            });
224
225        match level {
226            "error" => {
227                // Determine if this is a semantic error vs dependency error
228                let is_semantic = !Self::is_dependency_error(&code, &msg_text);
229                errors.push(CompilerError {
230                    code,
231                    message: msg_text,
232                    span,
233                    is_semantic,
234                });
235            }
236            "warning" => {
237                warnings.push(CompilerWarning {
238                    code,
239                    message: msg_text,
240                });
241            }
242            _ => {}
243        }
244    }
245
246    /// Check if an error is a dependency-related error (not a true semantic issue)
247    ///
248    /// These errors are automatically resolved by Cargo-First approach:
249    /// - E0432: unresolved import (missing crate)
250    /// - E0433: failed to resolve (missing crate path)
251    fn is_dependency_error(code: &Option<String>, message: &str) -> bool {
252        match code.as_deref() {
253            Some("E0432") => true, // unresolved import
254            Some("E0433") => true, // failed to resolve
255            Some("E0463") => true, // can't find crate
256            _ => {
257                // Also check message content for dependency hints
258                message.contains("can't find crate")
259                    || message.contains("unresolved import")
260                    || message.contains("could not find")
261            }
262        }
263    }
264}
265
266/// Compile Rust code using Cargo-First approach
267///
268/// This is the main entry point for verifying transpiled Rust code.
269/// It automatically:
270/// 1. Detects dependencies from `use` statements
271/// 2. Generates an appropriate Cargo.toml
272/// 3. Creates an ephemeral workspace
273/// 4. Runs `cargo check` and returns structured results
274///
275/// # Arguments
276/// * `name` - Module name for the workspace
277/// * `rust_code` - The Rust source code to verify
278/// * `cargo_toml` - Optional pre-generated Cargo.toml (uses minimal if None)
279///
280/// # Returns
281/// Result with CheckResult containing success status and any errors
282pub fn compile_with_cargo(
283    name: &str,
284    rust_code: &str,
285    cargo_toml: Option<&str>,
286) -> Result<CheckResult> {
287    // Generate comprehensive Cargo.toml with common dependencies
288    // that are used by the Python->Rust module mappings
289    let default_toml = format!(
290        r#"[package]
291name = "{}"
292version = "0.1.0"
293edition = "2021"
294
295[dependencies]
296# Serialization (json, pickle modules)
297serde = {{ version = "1.0", features = ["derive"] }}
298serde_json = "1.0"
299
300# Regex (re module)
301regex = "1.10"
302
303# Random (random module)
304rand = "0.8"
305
306# Collections (itertools module)
307itertools = "0.12"
308
309# Date/time (datetime module)
310chrono = "0.4"
311
312# Async runtime (asyncio module)
313tokio = {{ version = "1.0", features = ["full"] }}
314
315# Lazy static (constants)
316once_cell = "1.19"
317
318# Testing
319quickcheck = "1.0"
320
321# Hashing (hashlib module)
322sha2 = "0.10"
323md-5 = "0.10"
324hex = "0.4"
325
326# Base64 encoding
327base64 = "0.22"
328
329# URL parsing (urllib module)
330url = "2.5"
331
332# Temporary file operations
333tempfile = "3.10"
334
335# Argument parsing (argparse module)
336clap = {{ version = "4.5", features = ["derive"] }}
337"#,
338        name.replace('-', "_")
339    );
340
341    let toml = cargo_toml.unwrap_or(&default_toml);
342    let workspace = EphemeralWorkspace::new(name, rust_code, toml)?;
343    workspace.check()
344}
345
346/// Quick check if Rust code compiles (returns Ok/Err)
347///
348/// Convenience wrapper that returns a simple Result<(), String>
349/// for integration with existing verification flows.
350pub fn quick_check(name: &str, rust_code: &str, cargo_toml: Option<&str>) -> Result<(), String> {
351    match compile_with_cargo(name, rust_code, cargo_toml) {
352        Ok(result) if result.success => Ok(()),
353        Ok(result) => {
354            let error_msg = result
355                .errors
356                .iter()
357                .filter(|e| e.is_semantic) // Only report semantic errors
358                .map(|e| format!("{}: {}", e.code.as_deref().unwrap_or("E????"), e.message))
359                .collect::<Vec<_>>()
360                .join("\n");
361            if error_msg.is_empty() {
362                // All errors were dependency-related (shouldn't happen with proper Cargo.toml)
363                Err(result.stderr)
364            } else {
365                Err(error_msg)
366            }
367        }
368        Err(e) => Err(e.to_string()),
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    // === CheckResult tests ===
377
378    #[test]
379    fn test_check_result_success_fields() {
380        let result = CheckResult {
381            success: true,
382            errors: vec![],
383            warnings: vec![],
384            stderr: String::new(),
385        };
386        assert!(result.success);
387        assert!(result.errors.is_empty());
388        assert!(result.warnings.is_empty());
389        assert!(result.stderr.is_empty());
390    }
391
392    #[test]
393    fn test_check_result_with_errors() {
394        let result = CheckResult {
395            success: false,
396            errors: vec![CompilerError {
397                code: Some("E0308".to_string()),
398                message: "mismatched types".to_string(),
399                span: None,
400                is_semantic: true,
401            }],
402            warnings: vec![],
403            stderr: "error: mismatched types".to_string(),
404        };
405        assert!(!result.success);
406        assert_eq!(result.errors.len(), 1);
407        assert_eq!(result.errors[0].code.as_deref(), Some("E0308"));
408    }
409
410    #[test]
411    fn test_check_result_with_warnings() {
412        let result = CheckResult {
413            success: true,
414            errors: vec![],
415            warnings: vec![CompilerWarning {
416                code: Some("unused_variables".to_string()),
417                message: "unused variable `x`".to_string(),
418            }],
419            stderr: String::new(),
420        };
421        assert!(result.success);
422        assert_eq!(result.warnings.len(), 1);
423        assert!(result.warnings[0].message.contains("unused"));
424    }
425
426    #[test]
427    fn test_check_result_clone() {
428        let result = CheckResult {
429            success: true,
430            errors: vec![],
431            warnings: vec![],
432            stderr: "test".to_string(),
433        };
434        let cloned = result.clone();
435        assert_eq!(cloned.success, result.success);
436        assert_eq!(cloned.stderr, result.stderr);
437    }
438
439    #[test]
440    fn test_check_result_debug() {
441        let result = CheckResult {
442            success: true,
443            errors: vec![],
444            warnings: vec![],
445            stderr: String::new(),
446        };
447        let debug = format!("{:?}", result);
448        assert!(debug.contains("CheckResult"));
449        assert!(debug.contains("success"));
450    }
451
452    // === CompilerError tests ===
453
454    #[test]
455    fn test_compiler_error_all_fields() {
456        let error = CompilerError {
457            code: Some("E0308".to_string()),
458            message: "mismatched types".to_string(),
459            span: Some(ErrorSpan {
460                file: "lib.rs".to_string(),
461                line_start: 10,
462                line_end: 10,
463                column_start: 5,
464                column_end: 15,
465            }),
466            is_semantic: true,
467        };
468        assert_eq!(error.code.as_deref(), Some("E0308"));
469        assert!(error.message.contains("mismatched"));
470        assert!(error.span.is_some());
471        assert!(error.is_semantic);
472    }
473
474    #[test]
475    fn test_compiler_error_no_code() {
476        let error = CompilerError {
477            code: None,
478            message: "some error".to_string(),
479            span: None,
480            is_semantic: false,
481        };
482        assert!(error.code.is_none());
483        assert!(!error.is_semantic);
484    }
485
486    #[test]
487    fn test_compiler_error_clone() {
488        let error = CompilerError {
489            code: Some("E0001".to_string()),
490            message: "test".to_string(),
491            span: None,
492            is_semantic: true,
493        };
494        let cloned = error.clone();
495        assert_eq!(cloned.code, error.code);
496        assert_eq!(cloned.message, error.message);
497    }
498
499    #[test]
500    fn test_compiler_error_debug() {
501        let error = CompilerError {
502            code: Some("E0001".to_string()),
503            message: "test".to_string(),
504            span: None,
505            is_semantic: true,
506        };
507        let debug = format!("{:?}", error);
508        assert!(debug.contains("CompilerError"));
509        assert!(debug.contains("E0001"));
510    }
511
512    // === CompilerWarning tests ===
513
514    #[test]
515    fn test_compiler_warning_fields() {
516        let warning = CompilerWarning {
517            code: Some("unused_variables".to_string()),
518            message: "unused variable: `x`".to_string(),
519        };
520        assert_eq!(warning.code.as_deref(), Some("unused_variables"));
521        assert!(warning.message.contains("unused"));
522    }
523
524    #[test]
525    fn test_compiler_warning_no_code() {
526        let warning = CompilerWarning {
527            code: None,
528            message: "warning without code".to_string(),
529        };
530        assert!(warning.code.is_none());
531    }
532
533    #[test]
534    fn test_compiler_warning_clone() {
535        let warning = CompilerWarning {
536            code: Some("dead_code".to_string()),
537            message: "unused".to_string(),
538        };
539        let cloned = warning.clone();
540        assert_eq!(cloned.code, warning.code);
541        assert_eq!(cloned.message, warning.message);
542    }
543
544    #[test]
545    fn test_compiler_warning_debug() {
546        let warning = CompilerWarning {
547            code: Some("test".to_string()),
548            message: "msg".to_string(),
549        };
550        let debug = format!("{:?}", warning);
551        assert!(debug.contains("CompilerWarning"));
552    }
553
554    // === ErrorSpan tests ===
555
556    #[test]
557    fn test_error_span_fields() {
558        let span = ErrorSpan {
559            file: "src/main.rs".to_string(),
560            line_start: 10,
561            line_end: 15,
562            column_start: 1,
563            column_end: 20,
564        };
565        assert_eq!(span.file, "src/main.rs");
566        assert_eq!(span.line_start, 10);
567        assert_eq!(span.line_end, 15);
568        assert_eq!(span.column_start, 1);
569        assert_eq!(span.column_end, 20);
570    }
571
572    #[test]
573    fn test_error_span_single_line() {
574        let span = ErrorSpan {
575            file: "lib.rs".to_string(),
576            line_start: 5,
577            line_end: 5,
578            column_start: 10,
579            column_end: 25,
580        };
581        assert_eq!(span.line_start, span.line_end);
582    }
583
584    #[test]
585    fn test_error_span_clone() {
586        let span = ErrorSpan {
587            file: "test.rs".to_string(),
588            line_start: 1,
589            line_end: 2,
590            column_start: 3,
591            column_end: 4,
592        };
593        let cloned = span.clone();
594        assert_eq!(cloned.file, span.file);
595        assert_eq!(cloned.line_start, span.line_start);
596    }
597
598    #[test]
599    fn test_error_span_debug() {
600        let span = ErrorSpan {
601            file: "test.rs".to_string(),
602            line_start: 1,
603            line_end: 1,
604            column_start: 1,
605            column_end: 10,
606        };
607        let debug = format!("{:?}", span);
608        assert!(debug.contains("ErrorSpan"));
609        assert!(debug.contains("test.rs"));
610    }
611
612    // === EphemeralWorkspace tests ===
613
614    #[test]
615    fn test_ephemeral_workspace_creates_files() {
616        let rust_code = r#"
617            pub fn hello() -> &'static str {
618                "Hello, world!"
619            }
620        "#;
621        let cargo_toml = r#"
622            [package]
623            name = "test_module"
624            version = "0.1.0"
625            edition = "2021"
626        "#;
627
628        let workspace = EphemeralWorkspace::new("test_module", rust_code, cargo_toml).unwrap();
629
630        // Verify files were created
631        assert!(workspace.path().join("Cargo.toml").exists());
632        assert!(workspace.path().join("src/lib.rs").exists());
633
634        // Verify content
635        let lib_content = std::fs::read_to_string(workspace.lib_path()).unwrap();
636        assert!(lib_content.contains("hello"));
637    }
638
639    #[test]
640    fn test_ephemeral_workspace_path_accessors() {
641        let rust_code = "pub fn test() {}";
642        let cargo_toml = r#"
643[package]
644name = "test_paths"
645version = "0.1.0"
646edition = "2021"
647"#;
648        let workspace = EphemeralWorkspace::new("test_paths", rust_code, cargo_toml).unwrap();
649
650        // path() returns temp dir
651        assert!(workspace.path().exists());
652        assert!(workspace.path().is_dir());
653
654        // lib_path() returns lib.rs path
655        assert!(workspace.lib_path().exists());
656        assert!(workspace.lib_path().ends_with("lib.rs"));
657    }
658
659    #[test]
660    fn test_ephemeral_workspace_with_special_name() {
661        let rust_code = "pub fn test() {}";
662        let cargo_toml = r#"
663[package]
664name = "test_special_name"
665version = "0.1.0"
666edition = "2021"
667"#;
668        let workspace =
669            EphemeralWorkspace::new("test-special-name", rust_code, cargo_toml).unwrap();
670        assert!(workspace.path().exists());
671    }
672
673    #[test]
674    fn test_valid_code_compiles() {
675        let rust_code = r#"
676            pub fn add(a: i32, b: i32) -> i32 {
677                a + b
678            }
679        "#;
680        let cargo_toml = r#"
681[package]
682name = "test_valid"
683version = "0.1.0"
684edition = "2021"
685"#;
686
687        let workspace = EphemeralWorkspace::new("test_valid", rust_code, cargo_toml).unwrap();
688        let result = workspace.check().unwrap();
689
690        assert!(
691            result.success,
692            "Valid code should compile: {:?}",
693            result.errors
694        );
695        assert!(result.errors.is_empty(), "Should have no errors");
696    }
697
698    #[test]
699    fn test_invalid_code_fails() {
700        let rust_code = r#"
701            pub fn broken() -> i32 {
702                "not an integer"  // Type mismatch
703            }
704        "#;
705        let cargo_toml = r#"
706[package]
707name = "test_invalid"
708version = "0.1.0"
709edition = "2021"
710"#;
711
712        let workspace = EphemeralWorkspace::new("test_invalid", rust_code, cargo_toml).unwrap();
713        let result = workspace.check().unwrap();
714
715        assert!(!result.success, "Invalid code should fail");
716        assert!(!result.errors.is_empty(), "Should have errors");
717        // This is a semantic error, not dependency error
718        assert!(
719            result.errors.iter().any(|e| e.is_semantic),
720            "Should be semantic error"
721        );
722    }
723
724    #[test]
725    fn test_with_serde_dependency() {
726        let rust_code = r#"
727            use serde::{Serialize, Deserialize};
728
729            #[derive(Serialize, Deserialize)]
730            pub struct Point {
731                pub x: f64,
732                pub y: f64,
733            }
734        "#;
735        let cargo_toml = r#"
736[package]
737name = "test_serde"
738version = "0.1.0"
739edition = "2021"
740
741[dependencies]
742serde = { version = "1.0", features = ["derive"] }
743"#;
744
745        let workspace = EphemeralWorkspace::new("test_serde", rust_code, cargo_toml).unwrap();
746        let result = workspace.check().unwrap();
747
748        assert!(
749            result.success,
750            "Code with serde should compile: {:?}",
751            result.errors
752        );
753    }
754
755    // === is_dependency_error tests ===
756
757    #[test]
758    fn test_is_dependency_error() {
759        // E0432 is dependency error
760        assert!(EphemeralWorkspace::is_dependency_error(
761            &Some("E0432".to_string()),
762            "unresolved import"
763        ));
764
765        // E0463 is dependency error
766        assert!(EphemeralWorkspace::is_dependency_error(
767            &Some("E0463".to_string()),
768            "can't find crate"
769        ));
770
771        // E0308 (type mismatch) is NOT dependency error
772        assert!(!EphemeralWorkspace::is_dependency_error(
773            &Some("E0308".to_string()),
774            "mismatched types"
775        ));
776
777        // E0599 (method not found) is NOT dependency error
778        assert!(!EphemeralWorkspace::is_dependency_error(
779            &Some("E0599".to_string()),
780            "method not found"
781        ));
782    }
783
784    #[test]
785    fn test_is_dependency_error_e0433() {
786        // E0433 - failed to resolve
787        assert!(EphemeralWorkspace::is_dependency_error(
788            &Some("E0433".to_string()),
789            "failed to resolve"
790        ));
791    }
792
793    #[test]
794    fn test_is_dependency_error_message_based() {
795        // Check message-based detection
796        assert!(EphemeralWorkspace::is_dependency_error(
797            &None,
798            "can't find crate `foo`"
799        ));
800
801        assert!(EphemeralWorkspace::is_dependency_error(
802            &None,
803            "unresolved import `bar::baz`"
804        ));
805
806        assert!(EphemeralWorkspace::is_dependency_error(
807            &None,
808            "could not find `qux` in `xyz`"
809        ));
810    }
811
812    #[test]
813    fn test_is_dependency_error_not_dependency() {
814        // Various non-dependency errors
815        assert!(!EphemeralWorkspace::is_dependency_error(
816            &Some("E0277".to_string()),
817            "trait bound not satisfied"
818        ));
819
820        assert!(!EphemeralWorkspace::is_dependency_error(
821            &Some("E0382".to_string()),
822            "borrow of moved value"
823        ));
824
825        assert!(!EphemeralWorkspace::is_dependency_error(
826            &None,
827            "some other error message"
828        ));
829    }
830
831    #[test]
832    fn test_is_dependency_error_none_code() {
833        // With None code, relies on message
834        assert!(!EphemeralWorkspace::is_dependency_error(
835            &None,
836            "type mismatch"
837        ));
838    }
839
840    // === compile_with_cargo integration tests ===
841    // NOTE: These tests spawn actual `cargo check` processes and are marked #[ignore]
842    // because they are flaky under coverage instrumentation (cargo-llvm-cov) and
843    // consume significant memory/time. Run explicitly with: cargo test -- --ignored
844
845    #[test]
846    #[ignore = "spawns cargo process - flaky under coverage"]
847    fn test_compile_with_cargo_default_toml() {
848        let rust_code = r#"
849            pub fn simple() -> i32 {
850                42
851            }
852        "#;
853        let result = compile_with_cargo("test_default", rust_code, None).unwrap();
854        assert!(
855            result.success,
856            "Simple code should compile with default toml"
857        );
858    }
859
860    #[test]
861    #[ignore = "spawns cargo process - flaky under coverage"]
862    fn test_compile_with_cargo_custom_toml() {
863        let rust_code = "pub fn test() {}";
864        let custom_toml = r#"
865[package]
866name = "custom_pkg"
867version = "0.2.0"
868edition = "2021"
869"#;
870        let result = compile_with_cargo("custom_pkg", rust_code, Some(custom_toml)).unwrap();
871        assert!(result.success);
872    }
873
874    #[test]
875    #[ignore = "spawns cargo process - flaky under coverage"]
876    fn test_compile_with_cargo_name_with_dash() {
877        // Test that names with dashes are converted to underscores
878        let rust_code = "pub fn test() {}";
879        let result = compile_with_cargo("my-test-module", rust_code, None).unwrap();
880        assert!(result.success);
881    }
882
883    // === quick_check integration tests ===
884
885    #[test]
886    #[ignore = "spawns cargo process - flaky under coverage"]
887    fn test_quick_check_success() {
888        let rust_code = "pub fn hello() {}";
889        let result = quick_check("test_quick_ok", rust_code, None);
890        assert!(result.is_ok());
891    }
892
893    #[test]
894    #[ignore = "spawns cargo process - flaky under coverage"]
895    fn test_quick_check_semantic_error() {
896        let rust_code = r#"
897            pub fn broken() -> i32 {
898                "string"  // type error
899            }
900        "#;
901        let result = quick_check("test_quick_fail", rust_code, None);
902        assert!(result.is_err());
903        let err = result.unwrap_err();
904        // Should contain error code for semantic error
905        assert!(err.contains("E0") || !err.is_empty());
906    }
907
908    #[test]
909    #[ignore = "spawns cargo process - flaky under coverage"]
910    fn test_quick_check_with_custom_toml() {
911        let rust_code = "pub fn test() {}";
912        let toml = r#"
913[package]
914name = "quick_custom"
915version = "0.1.0"
916edition = "2021"
917"#;
918        let result = quick_check("quick_custom", rust_code, Some(toml));
919        assert!(result.is_ok());
920    }
921
922    // === Edge case integration tests ===
923
924    #[test]
925    #[ignore = "spawns cargo process - flaky under coverage"]
926    fn test_empty_rust_code() {
927        let rust_code = "";
928        let result = compile_with_cargo("empty_code", rust_code, None).unwrap();
929        // Empty code should compile (creates empty lib)
930        assert!(result.success);
931    }
932
933    #[test]
934    #[ignore = "spawns cargo process - flaky under coverage"]
935    fn test_code_with_warnings() {
936        let rust_code = r#"
937            pub fn test() {
938                let unused_var = 42;
939            }
940        "#;
941        let result = compile_with_cargo("with_warnings", rust_code, None).unwrap();
942        // Should still succeed (warnings don't fail check by default)
943        assert!(result.success);
944        // May have warnings
945        // (Note: warning detection depends on cargo check behavior)
946    }
947
948    #[test]
949    #[ignore = "spawns cargo process - flaky under coverage"]
950    fn test_multiple_functions() {
951        let rust_code = r#"
952            pub fn add(a: i32, b: i32) -> i32 { a + b }
953            pub fn sub(a: i32, b: i32) -> i32 { a - b }
954            pub fn mul(a: i32, b: i32) -> i32 { a * b }
955        "#;
956        let result = compile_with_cargo("multi_fn", rust_code, None).unwrap();
957        assert!(result.success);
958    }
959
960    #[test]
961    #[ignore = "spawns cargo process - flaky under coverage"]
962    fn test_with_struct() {
963        let rust_code = r#"
964            pub struct Point {
965                pub x: f64,
966                pub y: f64,
967            }
968
969            impl Point {
970                pub fn new(x: f64, y: f64) -> Self {
971                    Self { x, y }
972                }
973            }
974        "#;
975        let result = compile_with_cargo("with_struct", rust_code, None).unwrap();
976        assert!(result.success);
977    }
978
979    #[test]
980    #[ignore = "spawns cargo process - flaky under coverage"]
981    fn test_with_enum() {
982        let rust_code = r#"
983            pub enum Status {
984                Active,
985                Inactive,
986                Pending,
987            }
988        "#;
989        let result = compile_with_cargo("with_enum", rust_code, None).unwrap();
990        assert!(result.success);
991    }
992
993    #[test]
994    #[ignore = "spawns cargo process - flaky under coverage"]
995    fn test_with_generics() {
996        let rust_code = r#"
997            pub fn identity<T>(x: T) -> T {
998                x
999            }
1000
1001            pub struct Container<T> {
1002                pub value: T,
1003            }
1004        "#;
1005        let result = compile_with_cargo("with_generics", rust_code, None).unwrap();
1006        assert!(result.success);
1007    }
1008
1009    #[test]
1010    #[ignore = "spawns cargo process - flaky under coverage"]
1011    fn test_syntax_error() {
1012        let rust_code = "pub fn broken( { }"; // syntax error
1013        let result = compile_with_cargo("syntax_err", rust_code, None).unwrap();
1014        assert!(!result.success);
1015        assert!(!result.errors.is_empty());
1016    }
1017}