// End-to-end tests: generate a project, compile its C++ tests, and run them. // // These tests exercise the full pipeline that a student would follow: // anvil new [--template T] --board uno // cd // ./test.sh --clean (Linux) // test --clean (Windows) // // The compile-and-run tests require cmake and a C++ compiler. If these // tools are not available, those tests are skipped (not failed). The // structure verification tests always run. // // On Windows, MSVC is detected via test.bat's own vcvarsall setup. // On Linux, g++ or clang++ must be on PATH. use std::process::Command; use std::path::PathBuf; use tempfile::TempDir; // ========================================================================= // Helpers // ========================================================================= /// Get the path to the anvil binary built by cargo. fn anvil_bin() -> PathBuf { PathBuf::from(env!("CARGO_BIN_EXE_anvil")) } /// Check if cmake is available. fn has_cmake() -> bool { Command::new("cmake").arg("--version").output() .map(|o| o.status.success()).unwrap_or(false) } /// Check if a C++ compiler is available. /// On Windows, we trust that test.bat will find MSVC via vcvarsall. /// On Linux, we check for g++ or clang++. fn has_cpp_compiler() -> bool { if cfg!(windows) { // test.bat handles MSVC detection internally true } else { let gpp = Command::new("g++").arg("--version").output() .map(|o| o.status.success()).unwrap_or(false); let clang = Command::new("clang++").arg("--version").output() .map(|o| o.status.success()).unwrap_or(false); gpp || clang } } /// Check prerequisites for compile-and-run tests. /// Returns true if we can proceed, false if we should skip. fn can_compile() -> bool { has_cmake() && has_cpp_compiler() } /// Run `anvil new` in a temp directory and return (tempdir, project_path). fn create_project(name: &str, template: Option<&str>) -> (TempDir, PathBuf) { let tmp = TempDir::new().expect("Failed to create temp dir"); let mut cmd = Command::new(anvil_bin()); cmd.current_dir(tmp.path()) .arg("new") .arg(name) .arg("--board") .arg("uno"); if let Some(t) = template { cmd.arg("--template").arg(t); } let output = cmd.output().expect("Failed to run anvil new"); assert!( output.status.success(), "anvil new failed:\nstdout: {}\nstderr: {}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr), ); let project_path = tmp.path().join(name); assert!(project_path.exists(), "Project directory was not created"); (tmp, project_path) } /// Run the project's test script (test.sh on Linux, test.bat on Windows). /// Returns (success, stdout, stderr). fn run_project_tests(project_path: &PathBuf) -> (bool, String, String) { let output = if cfg!(windows) { Command::new("cmd") .args(["/c", "test.bat", "--clean"]) .current_dir(project_path) .output() .expect("Failed to run test.bat") } else { // Make test.sh executable #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let test_sh = project_path.join("test.sh"); if let Ok(meta) = std::fs::metadata(&test_sh) { let mut perms = meta.permissions(); perms.set_mode(0o755); std::fs::set_permissions(&test_sh, perms).ok(); } } Command::new("bash") .args(["test.sh", "--clean"]) .current_dir(project_path) .output() .expect("Failed to run test.sh") }; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); (output.status.success(), stdout, stderr) } /// Assert that the test output indicates passing tests. fn assert_tests_passed(template: &str, success: bool, stdout: &str, stderr: &str) { assert!( success, "{} template tests failed:\nstdout:\n{}\nstderr:\n{}", template, stdout, stderr, ); assert!( stdout.contains("tests passed") || stdout.contains("PASSED") || stdout.contains("PASS"), "{} template: expected passing test output, got:\n{}", template, stdout, ); } // ========================================================================= // Compile-and-run tests (skipped if cmake / C++ compiler not available) // ========================================================================= #[test] fn e2e_basic_template_compiles_and_tests_pass() { if !can_compile() { eprintln!( "SKIP: e2e compile tests require cmake and a C++ compiler. \ Install with: sudo apt install cmake build-essential" ); return; } let (_tmp, project_path) = create_project("e2e_basic", None); let (success, stdout, stderr) = run_project_tests(&project_path); assert_tests_passed("basic", success, &stdout, &stderr); } #[test] fn e2e_weather_template_compiles_and_tests_pass() { if !can_compile() { eprintln!("SKIP: cmake or C++ compiler not found"); return; } let (_tmp, project_path) = create_project("e2e_weather", Some("weather")); let (success, stdout, stderr) = run_project_tests(&project_path); assert_tests_passed("weather", success, &stdout, &stderr); } #[test] fn e2e_button_template_compiles_and_tests_pass() { if !can_compile() { eprintln!("SKIP: cmake or C++ compiler not found"); return; } let (_tmp, project_path) = create_project("e2e_button", Some("button")); let (success, stdout, stderr) = run_project_tests(&project_path); assert_tests_passed("button", success, &stdout, &stderr); } // ========================================================================= // Structure verification (always runs -- no compiler needed) // ========================================================================= #[test] fn e2e_basic_project_structure() { let (_tmp, path) = create_project("e2e_struct_basic", None); // Core files assert!(path.join(".anvil.toml").exists()); assert!(path.join("test.sh").exists()); assert!(path.join("build.sh").exists()); assert!(path.join("upload.sh").exists()); assert!(path.join("test.bat").exists()); assert!(path.join("build.bat").exists()); // HAL assert!(path.join("lib/hal/hal.h").exists()); assert!(path.join("lib/hal/hal_arduino.h").exists()); // Mocks assert!(path.join("test/mocks/mock_hal.h").exists()); assert!(path.join("test/mocks/sim_hal.h").exists()); assert!(path.join("test/mocks/mock_arduino.h").exists()); assert!(path.join("test/mocks/mock_arduino.cpp").exists()); // Tests assert!(path.join("test/CMakeLists.txt").exists()); assert!(path.join("test/test_unit.cpp").exists()); assert!(path.join("test/test_system.cpp").exists()); // Sketch assert!(path.join("e2e_struct_basic/e2e_struct_basic.ino").exists()); } #[test] fn e2e_button_project_has_library_files() { let (_tmp, path) = create_project("e2e_struct_btn", Some("button")); // Button library should be installed assert!(path.join("lib/drivers/button/button.h").exists()); assert!(path.join("lib/drivers/button/button_digital.h").exists()); assert!(path.join("lib/drivers/button/button_mock.h").exists()); assert!(path.join("lib/drivers/button/button_sim.h").exists()); // Button-specific app and tests assert!(path.join("lib/app/e2e_struct_btn_app.h").exists()); assert!(path.join("test/test_button_app.cpp").exists()); assert!(path.join("test/test_button.cpp").exists()); // Sketch should reference ButtonApp, not BlinkApp let ino = std::fs::read_to_string( path.join("e2e_struct_btn/e2e_struct_btn.ino") ).unwrap(); assert!(ino.contains("ButtonApp"), "Sketch should use ButtonApp"); assert!(ino.contains("button_digital.h"), "Sketch should include button driver"); } #[test] fn e2e_weather_project_has_library_files() { let (_tmp, path) = create_project("e2e_struct_wx", Some("weather")); // TMP36 library should be installed assert!(path.join("lib/drivers/tmp36/tmp36.h").exists()); assert!(path.join("lib/drivers/tmp36/tmp36_analog.h").exists()); assert!(path.join("lib/drivers/tmp36/tmp36_mock.h").exists()); assert!(path.join("lib/drivers/tmp36/tmp36_sim.h").exists()); // Weather-specific app assert!(path.join("lib/app/e2e_struct_wx_app.h").exists()); let app = std::fs::read_to_string( path.join("lib/app/e2e_struct_wx_app.h") ).unwrap(); assert!(app.contains("WeatherApp"), "App should use WeatherApp"); } #[test] fn e2e_config_records_template() { let (_tmp, path) = create_project("e2e_cfg_basic", None); let config = std::fs::read_to_string(path.join(".anvil.toml")).unwrap(); assert!(config.contains("template = \"basic\"")); let (_tmp2, path2) = create_project("e2e_cfg_btn", Some("button")); let config2 = std::fs::read_to_string(path2.join(".anvil.toml")).unwrap(); assert!(config2.contains("template = \"button\"")); let (_tmp3, path3) = create_project("e2e_cfg_wx", Some("weather")); let config3 = std::fs::read_to_string(path3.join(".anvil.toml")).unwrap(); assert!(config3.contains("template = \"weather\"")); }