Included tests to run test --clean from include_dir projects
This commit is contained in:
265
tests/test_e2e.rs
Normal file
265
tests/test_e2e.rs
Normal 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\""));
|
||||
}
|
||||
Reference in New Issue
Block a user