Included tests to run test --clean from include_dir projects
Some checks failed
CI / Test (Linux) (push) Has been cancelled
CI / Test (Windows MSVC) (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled

This commit is contained in:
Eric Ratliff
2026-02-22 18:23:13 -06:00
parent 05f248577f
commit d5220bea03
2 changed files with 270 additions and 1 deletions

265
tests/test_e2e.rs Normal file
View File

@@ -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 <n> [--template T] --board uno
// cd <n>
// ./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\""));
}