265 lines
9.3 KiB
Rust
265 lines
9.3 KiB
Rust
// 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\""));
|
|
} |