Trying to fix some build errors with regression tests, but this is failing
This commit is contained in:
@@ -440,7 +440,7 @@ Upload these to a Gitea release. The script requires `build-essential`,
|
|||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
642 unit/integration tests plus 7 end-to-end tests, zero warnings. The e2e
|
650 tests (137 unit + 506 integration + 7 end-to-end), zero warnings. The e2e
|
||||||
tests generate real projects and compile their C++ test suites, catching
|
tests generate real projects and compile their C++ test suites, catching
|
||||||
build-system issues like missing linker flags and include paths. They require
|
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
|
cmake and a C++ compiler; if those tools are not installed, the compile tests
|
||||||
|
|||||||
@@ -155,6 +155,12 @@ for %%d in (lib\hal lib\app) do (
|
|||||||
set "BUILD_FLAGS=!BUILD_FLAGS! -I%SCRIPT_DIR%\%%d"
|
set "BUILD_FLAGS=!BUILD_FLAGS! -I%SCRIPT_DIR%\%%d"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
:: Auto-discover driver libraries (added by: anvil add <driver>)
|
||||||
|
if exist "%SCRIPT_DIR%\lib\drivers" (
|
||||||
|
for /d %%d in ("%SCRIPT_DIR%\lib\drivers\*") do (
|
||||||
|
set "BUILD_FLAGS=!BUILD_FLAGS! -I%%d"
|
||||||
|
)
|
||||||
|
)
|
||||||
set "BUILD_FLAGS=!BUILD_FLAGS! -Werror"
|
set "BUILD_FLAGS=!BUILD_FLAGS! -Werror"
|
||||||
|
|
||||||
:: -- Compile --------------------------------------------------------------
|
:: -- Compile --------------------------------------------------------------
|
||||||
|
|||||||
@@ -149,6 +149,14 @@ for dir in $INCLUDE_DIRS; do
|
|||||||
warn "Include directory not found: $dir"
|
warn "Include directory not found: $dir"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
# Auto-discover driver libraries (added by: anvil add <driver>)
|
||||||
|
if [[ -d "$SCRIPT_DIR/lib/drivers" ]]; then
|
||||||
|
for driver_dir in "$SCRIPT_DIR"/lib/drivers/*/; do
|
||||||
|
if [[ -d "$driver_dir" ]]; then
|
||||||
|
BUILD_FLAGS="$BUILD_FLAGS -I$driver_dir"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
for flag in $EXTRA_FLAGS; do
|
for flag in $EXTRA_FLAGS; do
|
||||||
BUILD_FLAGS="$BUILD_FLAGS $flag"
|
BUILD_FLAGS="$BUILD_FLAGS $flag"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -204,6 +204,12 @@ for %%d in (lib\hal lib\app) do (
|
|||||||
set "BUILD_FLAGS=!BUILD_FLAGS! -I%SCRIPT_DIR%\%%d"
|
set "BUILD_FLAGS=!BUILD_FLAGS! -I%SCRIPT_DIR%\%%d"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
:: Auto-discover driver libraries (added by: anvil add <driver>)
|
||||||
|
if exist "%SCRIPT_DIR%\lib\drivers" (
|
||||||
|
for /d %%d in ("%SCRIPT_DIR%\lib\drivers\*") do (
|
||||||
|
set "BUILD_FLAGS=!BUILD_FLAGS! -I%%d"
|
||||||
|
)
|
||||||
|
)
|
||||||
set "BUILD_FLAGS=!BUILD_FLAGS! -Werror"
|
set "BUILD_FLAGS=!BUILD_FLAGS! -Werror"
|
||||||
|
|
||||||
:: -- Compile --------------------------------------------------------------
|
:: -- Compile --------------------------------------------------------------
|
||||||
|
|||||||
@@ -227,6 +227,14 @@ for dir in $INCLUDE_DIRS; do
|
|||||||
BUILD_FLAGS="$BUILD_FLAGS -I$abs"
|
BUILD_FLAGS="$BUILD_FLAGS -I$abs"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
# Auto-discover driver libraries (added by: anvil add <driver>)
|
||||||
|
if [[ -d "$SCRIPT_DIR/lib/drivers" ]]; then
|
||||||
|
for driver_dir in "$SCRIPT_DIR"/lib/drivers/*/; do
|
||||||
|
if [[ -d "$driver_dir" ]]; then
|
||||||
|
BUILD_FLAGS="$BUILD_FLAGS -I$driver_dir"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
for flag in $EXTRA_FLAGS; do
|
for flag in $EXTRA_FLAGS; do
|
||||||
BUILD_FLAGS="$BUILD_FLAGS $flag"
|
BUILD_FLAGS="$BUILD_FLAGS $flag"
|
||||||
done
|
done
|
||||||
|
|||||||
661
tests/script_execution_test.rs
Normal file
661
tests/script_execution_test.rs
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// Script Execution Tests
|
||||||
|
// ==========================================================================
|
||||||
|
//
|
||||||
|
// These tests extract a real Anvil project to a temp directory and then
|
||||||
|
// ACTUALLY EXECUTE the generated scripts. They are not fast and they
|
||||||
|
// require real build-machine dependencies:
|
||||||
|
//
|
||||||
|
// test/run_tests.sh --> cmake, g++ (or clang++), git
|
||||||
|
// build.sh --> arduino-cli (with arduino:avr core installed)
|
||||||
|
//
|
||||||
|
// If any dependency is missing the test FAILS -- that is intentional.
|
||||||
|
// A build machine that ships Anvil binaries MUST have these tools.
|
||||||
|
//
|
||||||
|
// On Windows the .bat variants are tested instead.
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use anvil::templates::{TemplateContext, TemplateManager};
|
||||||
|
use anvil::version::ANVIL_VERSION;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Extract a fresh project into a temp directory and return the TempDir.
|
||||||
|
fn extract_project(name: &str) -> TempDir {
|
||||||
|
let tmp = TempDir::new().expect("Failed to create temp directory");
|
||||||
|
let ctx = TemplateContext {
|
||||||
|
project_name: name.to_string(),
|
||||||
|
anvil_version: ANVIL_VERSION.to_string(),
|
||||||
|
};
|
||||||
|
let count = TemplateManager::extract("basic", tmp.path(), &ctx)
|
||||||
|
.expect("Failed to extract template");
|
||||||
|
assert!(count > 0, "Template extraction produced zero files");
|
||||||
|
tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make all .sh files under `root` executable (Unix only).
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn chmod_scripts(root: &Path) {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
for entry in walkdir::WalkDir::new(root)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
{
|
||||||
|
let p = entry.path();
|
||||||
|
if p.extension().map(|e| e == "sh").unwrap_or(false) {
|
||||||
|
let mut perms = fs::metadata(p)
|
||||||
|
.expect("Failed to read metadata")
|
||||||
|
.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(p, perms).expect("Failed to chmod");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a shell script (bash on Unix, cmd /C on Windows).
|
||||||
|
/// Returns (success, stdout, stderr).
|
||||||
|
fn run_script(dir: &Path, script: &str) -> (bool, String, String) {
|
||||||
|
let output = if cfg!(windows) {
|
||||||
|
Command::new("cmd")
|
||||||
|
.args(["/C", script])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
} else {
|
||||||
|
Command::new("bash")
|
||||||
|
.arg(script)
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = output.unwrap_or_else(|e| panic!("Failed to execute {}: {}", script, e));
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a script with extra arguments.
|
||||||
|
fn run_script_with_args(dir: &Path, script: &str, args: &[&str]) -> (bool, String, String) {
|
||||||
|
let output = if cfg!(windows) {
|
||||||
|
let mut all_args = vec!["/C", script];
|
||||||
|
all_args.extend_from_slice(args);
|
||||||
|
Command::new("cmd")
|
||||||
|
.args(&all_args)
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
} else {
|
||||||
|
Command::new("bash")
|
||||||
|
.arg(script)
|
||||||
|
.args(args)
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = output.unwrap_or_else(|e| panic!("Failed to execute {}: {}", script, e));
|
||||||
|
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 a command-line tool is available in PATH.
|
||||||
|
/// Panics with a clear message if not found.
|
||||||
|
fn require_tool(name: &str) {
|
||||||
|
let check = if cfg!(windows) {
|
||||||
|
Command::new("where").arg(name).output()
|
||||||
|
} else {
|
||||||
|
Command::new("which").arg(name).output()
|
||||||
|
};
|
||||||
|
|
||||||
|
match check {
|
||||||
|
Ok(output) if output.status.success() => {}
|
||||||
|
_ => panic!(
|
||||||
|
"\n\n\
|
||||||
|
===================================================================\n\
|
||||||
|
MISSING BUILD DEPENDENCY: {name}\n\
|
||||||
|
===================================================================\n\
|
||||||
|
\n\
|
||||||
|
Anvil's cargo tests REQUIRE build-machine dependencies.\n\
|
||||||
|
Install '{name}' and re-run. See 'anvil doctor' for guidance.\n\
|
||||||
|
\n\
|
||||||
|
===================================================================\n"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check that at least one C++ compiler is present.
|
||||||
|
fn require_cpp_compiler() {
|
||||||
|
let has_gpp = Command::new(if cfg!(windows) { "where" } else { "which" })
|
||||||
|
.arg("g++")
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let has_clangpp = Command::new(if cfg!(windows) { "where" } else { "which" })
|
||||||
|
.arg("clang++")
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !has_gpp && !has_clangpp {
|
||||||
|
panic!(
|
||||||
|
"\n\n\
|
||||||
|
===================================================================\n\
|
||||||
|
MISSING BUILD DEPENDENCY: C++ compiler (g++ or clang++)\n\
|
||||||
|
===================================================================\n\
|
||||||
|
\n\
|
||||||
|
Install g++ or clang++ and re-run.\n\
|
||||||
|
\n\
|
||||||
|
===================================================================\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Require cmake + C++ compiler + git (the test script prereqs).
|
||||||
|
fn require_test_script_deps() {
|
||||||
|
require_tool("cmake");
|
||||||
|
require_tool("git");
|
||||||
|
require_cpp_compiler();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Require arduino-cli (the build script prereqs).
|
||||||
|
fn require_build_script_deps() {
|
||||||
|
require_tool("arduino-cli");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Choose the platform-appropriate script path.
|
||||||
|
fn test_script() -> &'static str {
|
||||||
|
if cfg!(windows) { "test\\run_tests.bat" } else { "test/run_tests.sh" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_script() -> &'static str {
|
||||||
|
if cfg!(windows) { "build.bat" } else { "build.sh" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List directory contents recursively (for diagnostic output on failure).
|
||||||
|
fn list_dir_recursive(dir: &Path) -> String {
|
||||||
|
if !dir.exists() {
|
||||||
|
return format!(" (directory does not exist: {})", dir.display());
|
||||||
|
}
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
for entry in walkdir::WalkDir::new(dir)
|
||||||
|
.max_depth(4)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
{
|
||||||
|
let depth = entry.depth();
|
||||||
|
let indent = " ".repeat(depth);
|
||||||
|
let name = entry.file_name().to_string_lossy();
|
||||||
|
lines.push(format!("{}{}", indent, name));
|
||||||
|
}
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// TEST SCRIPT EXECUTION (cmake + Google Test)
|
||||||
|
//
|
||||||
|
// Extracts a project, then runs test/run_tests.sh which:
|
||||||
|
// 1. cmake configures the test/ directory
|
||||||
|
// 2. FetchContent downloads Google Test
|
||||||
|
// 3. Compiles C++ unit tests against HAL mocks
|
||||||
|
// 4. Runs tests via ctest
|
||||||
|
//
|
||||||
|
// Required on build machine: cmake, g++ or clang++, git
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_tests_script_executes_successfully() {
|
||||||
|
require_test_script_deps();
|
||||||
|
|
||||||
|
let tmp = extract_project("test_exec");
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
chmod_scripts(tmp.path());
|
||||||
|
|
||||||
|
let (success, stdout, stderr) = run_script(tmp.path(), test_script());
|
||||||
|
|
||||||
|
// Always print output so CI logs are useful
|
||||||
|
println!("--- run_tests stdout ---\n{}", stdout);
|
||||||
|
if !stderr.is_empty() {
|
||||||
|
eprintln!("--- run_tests stderr ---\n{}", stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
success,
|
||||||
|
"test/run_tests script failed!\n\nstdout:\n{}\n\nstderr:\n{}",
|
||||||
|
stdout, stderr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_tests_script_google_tests_actually_ran() {
|
||||||
|
require_test_script_deps();
|
||||||
|
|
||||||
|
let tmp = extract_project("gtest_verify");
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
chmod_scripts(tmp.path());
|
||||||
|
|
||||||
|
let (success, stdout, stderr) = run_script(tmp.path(), test_script());
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
success,
|
||||||
|
"Test script failed.\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
stdout, stderr
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that Google Test actually executed tests -- not a silent no-op
|
||||||
|
let combined = format!("{}{}", stdout, stderr);
|
||||||
|
let tests_ran = combined.contains("passed")
|
||||||
|
|| combined.contains("PASSED")
|
||||||
|
|| combined.contains("tests passed")
|
||||||
|
|| combined.contains("100%");
|
||||||
|
assert!(
|
||||||
|
tests_ran,
|
||||||
|
"Output does not indicate any Google Tests actually executed.\n\
|
||||||
|
This could mean cmake built but ctest found no registered tests.\n\n\
|
||||||
|
stdout:\n{}\n\nstderr:\n{}",
|
||||||
|
stdout, stderr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_tests_script_clean_flag_rebuilds() {
|
||||||
|
require_test_script_deps();
|
||||||
|
|
||||||
|
let tmp = extract_project("clean_rebuild");
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
chmod_scripts(tmp.path());
|
||||||
|
|
||||||
|
// First run -- populates build dir and fetches gtest
|
||||||
|
let (success, _, _) = run_script(tmp.path(), test_script());
|
||||||
|
assert!(success, "First test run failed");
|
||||||
|
|
||||||
|
// Verify build artifacts exist
|
||||||
|
let build_dir = tmp.path().join("test").join("build");
|
||||||
|
assert!(
|
||||||
|
build_dir.exists(),
|
||||||
|
"test/build/ directory should exist after first run"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Second run with --clean -- should nuke build dir and rebuild
|
||||||
|
let (success, stdout, stderr) =
|
||||||
|
run_script_with_args(tmp.path(), test_script(), &["--clean"]);
|
||||||
|
|
||||||
|
println!("--- clean rebuild stdout ---\n{}", stdout);
|
||||||
|
if !stderr.is_empty() {
|
||||||
|
eprintln!("--- clean rebuild stderr ---\n{}", stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
success,
|
||||||
|
"Clean rebuild failed.\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
stdout, stderr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_tests_script_produces_test_binary() {
|
||||||
|
require_test_script_deps();
|
||||||
|
|
||||||
|
let tmp = extract_project("bin_check");
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
chmod_scripts(tmp.path());
|
||||||
|
|
||||||
|
let (success, _, _) = run_script(tmp.path(), test_script());
|
||||||
|
assert!(success, "Test script failed");
|
||||||
|
|
||||||
|
// The cmake build should produce a test_unit binary somewhere
|
||||||
|
// under test/build/
|
||||||
|
let build_dir = tmp.path().join("test").join("build");
|
||||||
|
let has_binary = walkdir::WalkDir::new(&build_dir)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.any(|entry| {
|
||||||
|
let name = entry.file_name().to_string_lossy();
|
||||||
|
name.starts_with("test_unit")
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
has_binary,
|
||||||
|
"Expected test_unit binary under test/build/ after running tests.\n\
|
||||||
|
Contents of test/build/:\n{}",
|
||||||
|
list_dir_recursive(&build_dir)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_tests_idempotent_second_run() {
|
||||||
|
// Running the test script twice should succeed both times.
|
||||||
|
// Second run reuses the cached build and should be fast.
|
||||||
|
require_test_script_deps();
|
||||||
|
|
||||||
|
let tmp = extract_project("idempotent");
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
chmod_scripts(tmp.path());
|
||||||
|
|
||||||
|
let (success1, _, _) = run_script(tmp.path(), test_script());
|
||||||
|
assert!(success1, "First run failed");
|
||||||
|
|
||||||
|
let (success2, stdout2, stderr2) = run_script(tmp.path(), test_script());
|
||||||
|
assert!(
|
||||||
|
success2,
|
||||||
|
"Second run failed (should be idempotent).\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
stdout2, stderr2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// BUILD SCRIPT EXECUTION (arduino-cli compile)
|
||||||
|
//
|
||||||
|
// Extracts a project, then runs build.sh which:
|
||||||
|
// 1. Reads .anvil.toml for FQBN, include_dirs, extra_flags
|
||||||
|
// 2. Invokes arduino-cli compile with those settings
|
||||||
|
//
|
||||||
|
// Required on build machine: arduino-cli with arduino:avr core installed
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_script_compiles_sketch() {
|
||||||
|
require_build_script_deps();
|
||||||
|
|
||||||
|
let tmp = extract_project("build_test");
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
chmod_scripts(tmp.path());
|
||||||
|
|
||||||
|
let (success, stdout, stderr) = run_script(tmp.path(), build_script());
|
||||||
|
|
||||||
|
println!("--- build.sh stdout ---\n{}", stdout);
|
||||||
|
if !stderr.is_empty() {
|
||||||
|
eprintln!("--- build.sh stderr ---\n{}", stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
success,
|
||||||
|
"build script failed!\n\nstdout:\n{}\n\nstderr:\n{}",
|
||||||
|
stdout, stderr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_script_produces_compilation_output() {
|
||||||
|
require_build_script_deps();
|
||||||
|
|
||||||
|
let tmp = extract_project("compile_output");
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
chmod_scripts(tmp.path());
|
||||||
|
|
||||||
|
let (success, stdout, stderr) = run_script(tmp.path(), build_script());
|
||||||
|
assert!(
|
||||||
|
success,
|
||||||
|
"Build script failed.\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
stdout, stderr
|
||||||
|
);
|
||||||
|
|
||||||
|
// arduino-cli compile produces output indicating sketch size.
|
||||||
|
// Look for evidence of successful compilation.
|
||||||
|
let combined = format!("{}{}", stdout, stderr);
|
||||||
|
let compiled = combined.contains("Sketch uses")
|
||||||
|
|| combined.contains("bytes")
|
||||||
|
|| combined.contains("Compiling")
|
||||||
|
|| combined.contains("Used")
|
||||||
|
|| combined.contains("compiled");
|
||||||
|
assert!(
|
||||||
|
compiled,
|
||||||
|
"Build output does not indicate a successful arduino-cli compilation.\n\n\
|
||||||
|
stdout:\n{}\n\nstderr:\n{}",
|
||||||
|
stdout, stderr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_script_idempotent_second_run() {
|
||||||
|
require_build_script_deps();
|
||||||
|
|
||||||
|
let tmp = extract_project("build_idem");
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
chmod_scripts(tmp.path());
|
||||||
|
|
||||||
|
let (success1, _, _) = run_script(tmp.path(), build_script());
|
||||||
|
assert!(success1, "First build failed");
|
||||||
|
|
||||||
|
let (success2, stdout2, stderr2) = run_script(tmp.path(), build_script());
|
||||||
|
assert!(
|
||||||
|
success2,
|
||||||
|
"Second build failed (should be idempotent).\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
stdout2, stderr2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// COMBINED: build + test scripts both succeed on the same project
|
||||||
|
//
|
||||||
|
// Full end-to-end: one extracted project, both scripts pass.
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_full_project_build_and_test_scripts_both_pass() {
|
||||||
|
require_test_script_deps();
|
||||||
|
require_build_script_deps();
|
||||||
|
|
||||||
|
let tmp = extract_project("full_e2e");
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
chmod_scripts(tmp.path());
|
||||||
|
|
||||||
|
// Build the sketch
|
||||||
|
let (build_ok, build_out, build_err) = run_script(tmp.path(), build_script());
|
||||||
|
println!("--- build stdout ---\n{}", build_out);
|
||||||
|
if !build_err.is_empty() {
|
||||||
|
eprintln!("--- build stderr ---\n{}", build_err);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
build_ok,
|
||||||
|
"build script failed in full E2E.\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
build_out, build_err
|
||||||
|
);
|
||||||
|
|
||||||
|
// Run the host-side unit tests
|
||||||
|
let (test_ok, test_out, test_err) = run_script(tmp.path(), test_script());
|
||||||
|
println!("--- test stdout ---\n{}", test_out);
|
||||||
|
if !test_err.is_empty() {
|
||||||
|
eprintln!("--- test stderr ---\n{}", test_err);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
test_ok,
|
||||||
|
"test script failed in full E2E.\nstdout:\n{}\nstderr:\n{}",
|
||||||
|
test_out, test_err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// SCRIPT CONTENT SANITY CHECKS
|
||||||
|
//
|
||||||
|
// Verify the scripts are well-formed before even executing them.
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_all_sh_scripts_have_strict_error_handling() {
|
||||||
|
let tmp = extract_project("strict_mode");
|
||||||
|
|
||||||
|
let sh_scripts = vec![
|
||||||
|
"build.sh",
|
||||||
|
"upload.sh",
|
||||||
|
"monitor.sh",
|
||||||
|
"test/run_tests.sh",
|
||||||
|
];
|
||||||
|
|
||||||
|
for script in &sh_scripts {
|
||||||
|
let path = tmp.path().join(script);
|
||||||
|
if !path.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let content = fs::read_to_string(&path)
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to read {}", script));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
content.contains("set -e") || content.contains("set -euo pipefail"),
|
||||||
|
"{} must use 'set -e' or 'set -euo pipefail' for strict error handling",
|
||||||
|
script
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_all_sh_scripts_have_shebangs() {
|
||||||
|
let tmp = extract_project("shebang_check");
|
||||||
|
|
||||||
|
let sh_scripts = vec![
|
||||||
|
"build.sh",
|
||||||
|
"upload.sh",
|
||||||
|
"monitor.sh",
|
||||||
|
"test/run_tests.sh",
|
||||||
|
];
|
||||||
|
|
||||||
|
for script in &sh_scripts {
|
||||||
|
let path = tmp.path().join(script);
|
||||||
|
if !path.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let content = fs::read_to_string(&path)
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to read {}", script));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
content.starts_with("#!/"),
|
||||||
|
"{} must start with a shebang line (#!/...)",
|
||||||
|
script
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bat_scripts_exist_for_windows_parity() {
|
||||||
|
let tmp = extract_project("win_parity");
|
||||||
|
|
||||||
|
// Every .sh should have a matching .bat
|
||||||
|
let pairs = vec![
|
||||||
|
("build.sh", "build.bat"),
|
||||||
|
("upload.sh", "upload.bat"),
|
||||||
|
("monitor.sh", "monitor.bat"),
|
||||||
|
("test/run_tests.sh", "test/run_tests.bat"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (sh, bat) in &pairs {
|
||||||
|
let sh_exists = tmp.path().join(sh).exists();
|
||||||
|
let bat_exists = tmp.path().join(bat).exists();
|
||||||
|
if sh_exists {
|
||||||
|
assert!(
|
||||||
|
bat_exists,
|
||||||
|
"{} exists but {} is missing -- Windows parity broken",
|
||||||
|
sh, bat
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cmake_lists_fetches_google_test() {
|
||||||
|
let tmp = extract_project("cmake_gtest");
|
||||||
|
|
||||||
|
let cmake_path = tmp.path().join("test").join("CMakeLists.txt");
|
||||||
|
assert!(cmake_path.exists(), "test/CMakeLists.txt must exist");
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&cmake_path).unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
content.contains("FetchContent") || content.contains("fetchcontent"),
|
||||||
|
"CMakeLists.txt should use FetchContent to download Google Test"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
content.contains("googletest") || content.contains("GTest"),
|
||||||
|
"CMakeLists.txt should reference Google Test"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_scripts_all_reference_anvil_toml() {
|
||||||
|
let tmp = extract_project("toml_refs");
|
||||||
|
|
||||||
|
// build and upload scripts must read .anvil.toml for configuration
|
||||||
|
let config_scripts = vec![
|
||||||
|
"build.sh",
|
||||||
|
"build.bat",
|
||||||
|
"upload.sh",
|
||||||
|
"upload.bat",
|
||||||
|
];
|
||||||
|
|
||||||
|
for script in &config_scripts {
|
||||||
|
let path = tmp.path().join(script);
|
||||||
|
if !path.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let content = fs::read_to_string(&path)
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to read {}", script));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
content.contains(".anvil.toml"),
|
||||||
|
"{} should reference .anvil.toml for project configuration",
|
||||||
|
script
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_scripts_invoke_arduino_cli_not_anvil() {
|
||||||
|
let tmp = extract_project("no_anvil_dep");
|
||||||
|
|
||||||
|
// All scripts must invoke arduino-cli directly, never the anvil binary
|
||||||
|
let scripts = vec![
|
||||||
|
"build.sh", "build.bat",
|
||||||
|
"upload.sh", "upload.bat",
|
||||||
|
"monitor.sh", "monitor.bat",
|
||||||
|
];
|
||||||
|
|
||||||
|
for script in &scripts {
|
||||||
|
let path = tmp.path().join(script);
|
||||||
|
if !path.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let content = fs::read_to_string(&path)
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to read {}", script));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
content.contains("arduino-cli"),
|
||||||
|
"{} should invoke arduino-cli directly",
|
||||||
|
script
|
||||||
|
);
|
||||||
|
|
||||||
|
// No line should shell out to the anvil binary
|
||||||
|
let has_anvil_cmd = content.lines().any(|line| {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
// Skip comments and echo/print lines
|
||||||
|
if trimmed.starts_with('#')
|
||||||
|
|| trimmed.starts_with("::")
|
||||||
|
|| trimmed.starts_with("echo")
|
||||||
|
|| trimmed.starts_with("REM")
|
||||||
|
|| trimmed.starts_with("rem")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check for "anvil " as a command invocation
|
||||||
|
trimmed.contains("anvil ")
|
||||||
|
&& !trimmed.contains("anvil.toml")
|
||||||
|
&& !trimmed.contains("Anvil")
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
!has_anvil_cmd,
|
||||||
|
"{} should not invoke the anvil binary -- project must be self-contained",
|
||||||
|
script
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -914,4 +914,31 @@ fn test_monitor_sh_timestamps_work_in_watch_mode() {
|
|||||||
"monitor_filter should be defined and used in both watch and normal mode (found {} refs)",
|
"monitor_filter should be defined and used in both watch and normal mode (found {} refs)",
|
||||||
filter_count
|
filter_count
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Driver auto-discovery in build/upload scripts
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_scripts_autodiscover_driver_includes() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let ctx = TemplateContext {
|
||||||
|
project_name: "drv_test".to_string(),
|
||||||
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
|
baud: 115200,
|
||||||
|
};
|
||||||
|
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||||
|
|
||||||
|
// All four compile scripts must auto-discover lib/drivers/*
|
||||||
|
for script in &["build.sh", "upload.sh", "build.bat", "upload.bat"] {
|
||||||
|
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
|
||||||
|
assert!(
|
||||||
|
content.contains("lib/drivers") || content.contains("lib\\drivers"),
|
||||||
|
"{} must auto-discover lib/drivers/* for library include paths",
|
||||||
|
script
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user