From d5220bea03f39e726bc346ff31f0ed2e6df184ff Mon Sep 17 00:00:00 2001 From: Eric Ratliff Date: Sun, 22 Feb 2026 18:23:13 -0600 Subject: [PATCH] Included tests to run test --clean from include_dir projects --- README.md | 6 +- tests/test_e2e.rs | 265 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 tests/test_e2e.rs diff --git a/README.md b/README.md index 24fc629..cbcaa61 100644 --- a/README.md +++ b/README.md @@ -440,7 +440,11 @@ Upload these to a Gitea release. The script requires `build-essential`, cargo test ``` -642 tests (137 unit + 505 integration), ~4 seconds, zero warnings. +642 unit/integration tests plus 7 end-to-end tests, zero warnings. The e2e +tests generate real projects and compile their C++ test suites, catching +build-system issues like missing linker flags and include paths. They require +cmake and a C++ compiler; if those tools are not installed, the compile tests +skip gracefully and everything else still passes. --- diff --git a/tests/test_e2e.rs b/tests/test_e2e.rs new file mode 100644 index 0000000..2da3e10 --- /dev/null +++ b/tests/test_e2e.rs @@ -0,0 +1,265 @@ +// 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\"")); +} \ No newline at end of file