ryo-source 0.1.0

High-speed Rust AST manipulation engine
Documentation
//! Test utilities for AST codegen validation.
//!
//! This module provides helpers for verifying that generated Rust code is:
//! 1. Valid syntax (parseable by syn)
//! 2. Compilable by rustc (via cargo check)
//!
//! # Usage
//!
//! Enable the `test-utils` feature in your Cargo.toml:
//! ```toml
//! [dev-dependencies]
//! ryo-source = { version = "0.1", features = ["test-utils"] }
//! ```
//!
//! Then use in tests:
//! ```rust,ignore
//! use ryo_source::test_utils::{assert_valid_rust, assert_compiles};
//!
//! #[test]
//! fn my_test() {
//!     let source = "struct Foo { x: i32 }";
//!     assert_valid_rust(source);  // Fast: syn parse only
//!     assert_compiles(source);    // Slow: cargo check
//! }
//! ```

use crate::pure::PureFile;

/// Converts source through Pure roundtrip and validates the output is valid Rust.
///
/// This function:
/// 1. Parses source into PureFile
/// 2. Converts back to source via to_source()
/// 3. Validates the output can be parsed by syn (syntax check)
///
/// Returns the generated output for further assertions.
///
/// # Panics
/// Panics if the input cannot be parsed or the output is invalid Rust syntax.
pub fn assert_valid_rust(source: &str) -> String {
    let pure = PureFile::from_source(source).expect("Input should parse into PureFile");
    let output = pure
        .to_source()
        .expect("to_source should succeed for valid PureFile");

    // Validate generated code is valid Rust syntax
    syn::parse_str::<syn::File>(&output).unwrap_or_else(|_| {
        panic!(
            "Generated code must be valid Rust syntax.\nInput:\n{}\nOutput:\n{}",
            source, output
        )
    });

    output
}

/// Validates generated code compiles with rustc via cargo check.
///
/// This is a heavier validation that catches:
/// - Type errors
/// - Name resolution errors
/// - Semantic issues that syn doesn't catch
///
/// Use sparingly on representative cases due to execution time (~1-2s per test).
///
/// # Panics
/// Panics if the code fails to compile.
pub fn assert_compiles(source: &str) -> String {
    use std::io::Write;
    use std::process::Command;

    let output = assert_valid_rust(source);

    // Create temp directory with Cargo project structure
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let src_dir = temp_dir.path().join("src");
    std::fs::create_dir_all(&src_dir).expect("Failed to create src dir");

    // Write lib.rs
    let lib_path = src_dir.join("lib.rs");
    let mut file = std::fs::File::create(&lib_path).expect("Failed to create lib.rs");
    // Add common preludes to avoid "not found" errors for std types
    writeln!(file, "#![allow(unused, dead_code)]").unwrap();
    write!(file, "{}", output).expect("Failed to write source");

    // Write Cargo.toml
    let cargo_toml = temp_dir.path().join("Cargo.toml");
    std::fs::write(
        &cargo_toml,
        r#"[package]
name = "roundtrip_test"
version = "0.1.0"
edition = "2021"
"#,
    )
    .expect("Failed to write Cargo.toml");

    // Run cargo check
    let result = Command::new("cargo")
        .args(["check", "--message-format=short"])
        .current_dir(temp_dir.path())
        .output()
        .expect("Failed to run cargo check");

    if !result.status.success() {
        let stderr = String::from_utf8_lossy(&result.stderr);
        panic!(
            "Generated code failed to compile.\n\
             === Input ===\n{}\n\
             === Generated ===\n{}\n\
             === Errors ===\n{}",
            source, output, stderr
        );
    }

    output
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_assert_valid_rust_simple() {
        let output = assert_valid_rust("fn main() {}");
        assert!(output.contains("fn main"));
    }

    #[test]
    fn test_assert_valid_rust_struct() {
        let output = assert_valid_rust("struct Foo { x: i32 }");
        assert!(output.contains("struct Foo"));
    }

    #[test]
    #[should_panic(expected = "Input should parse")]
    fn test_assert_valid_rust_invalid_input() {
        assert_valid_rust("this is not valid rust {{{{");
    }
}