Files
anvil/tests/script_execution_test.rs
Eric Ratliff 8374cb6079
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
There was still one occasional problem while running cargo test, it would fail sometimes. Added a serial preprocessor to ensure the underlying arduino command didn't run pipelined. That fixed the problem
2026-02-23 08:17:30 -06:00

893 lines
28 KiB
Rust

// ==========================================================================
// 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.sh / test/run_tests.sh --> cmake, g++ (or clang++), git
// build.sh --> arduino-cli (with arduino:avr core)
//
// 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 serial_test::serial;
use anvil::templates::{TemplateContext, TemplateManager};
use anvil::version::ANVIL_VERSION;
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
/// Build a TemplateContext with sensible defaults for testing.
fn test_context(name: &str) -> TemplateContext {
TemplateContext {
project_name: name.to_string(),
anvil_version: ANVIL_VERSION.to_string(),
board_name: "Arduino Uno (ATmega328P)".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}
}
/// 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 = test_context(name);
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) {
chmod_recursive(root);
}
#[cfg(unix)]
fn chmod_recursive(dir: &Path) {
use std::os::unix::fs::PermissionsExt;
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_dir() {
chmod_recursive(&path);
} else if path.extension().map(|e| e == "sh").unwrap_or(false) {
let mut perms = match fs::metadata(&path) {
Ok(m) => m.permissions(),
Err(_) => continue,
};
perms.set_mode(0o755);
let _ = fs::set_permissions(&path, perms);
}
}
}
/// 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.
///
/// On Windows, cmake discovers MSVC through the Visual Studio installation
/// even when cl.exe is not directly in PATH, so we check for cl.exe as
/// well as g++ and clang++. If none are found in PATH we still let cmake
/// try -- it will fail at configure time with a clear message.
fn require_cpp_compiler() {
let check_tool = |name: &str| -> bool {
Command::new(if cfg!(windows) { "where" } else { "which" })
.arg(name)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
};
let has_gpp = check_tool("g++");
let has_clangpp = check_tool("clang++");
let has_cl = if cfg!(windows) { check_tool("cl") } else { false };
// On Windows, cmake can discover MSVC even when cl.exe is not in
// the current PATH (via vswhere / VS installation registry). So
// we only hard-fail on Linux/macOS where the compiler really must
// be in PATH.
if !has_gpp && !has_clangpp && !has_cl {
if cfg!(windows) {
// Warn but don't panic -- cmake will try to find MSVC
eprintln!(
"\n\
WARNING: No C++ compiler (g++, clang++, cl) found in PATH.\n\
cmake may still find MSVC via Visual Studio installation.\n\
If tests fail, open a VS Developer Command Prompt or install\n\
Build Tools for Visual Studio.\n"
);
} else {
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");
}
/// Platform-appropriate script paths.
fn root_test_script() -> &'static str {
if cfg!(windows) { "test.bat" } else { "test.sh" }
}
fn inner_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" }
}
/// Recursively list directory contents using only std::fs (no external crates).
/// Used for diagnostic output when a test assertion fails.
fn list_dir_recursive(dir: &Path) -> String {
if !dir.exists() {
return format!(" (directory does not exist: {})", dir.display());
}
let mut lines = Vec::new();
collect_dir_entries(dir, 0, 4, &mut lines);
lines.join("\n")
}
fn collect_dir_entries(dir: &Path, depth: usize, max_depth: usize, lines: &mut Vec<String>) {
if depth > max_depth {
return;
}
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
let mut sorted: Vec<_> = entries.filter_map(|e| e.ok()).collect();
sorted.sort_by_key(|e| e.file_name());
for entry in sorted {
let indent = " ".repeat(depth + 1);
let name = entry.file_name();
let name_str = name.to_string_lossy();
lines.push(format!("{}{}", indent, name_str));
let path = entry.path();
if path.is_dir() {
collect_dir_entries(&path, depth + 1, max_depth, lines);
}
}
}
/// Search recursively for a file whose name starts with the given prefix.
/// Uses only std::fs (no external crates).
fn find_file_recursive(dir: &Path, prefix: &str) -> bool {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return false,
};
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
let name = entry.file_name();
if name.to_string_lossy().starts_with(prefix) {
return true;
}
if path.is_dir() && find_file_recursive(&path, prefix) {
return true;
}
}
false
}
// ==========================================================================
// ROOT test.sh / test.bat EXECUTION
//
// The root-level test script is the primary test entry point for the
// generated project. It must work out of the box.
//
// Required on build machine: cmake, g++ or clang++, git
// ==========================================================================
#[test]
fn test_root_test_script_executes_successfully() {
require_test_script_deps();
let tmp = extract_project("root_test");
#[cfg(unix)]
chmod_scripts(tmp.path());
let (success, stdout, stderr) = run_script(tmp.path(), root_test_script());
println!("--- {} stdout ---\n{}", root_test_script(), stdout);
if !stderr.is_empty() {
eprintln!("--- {} stderr ---\n{}", root_test_script(), stderr);
}
assert!(
success,
"{} failed!\n\nstdout:\n{}\n\nstderr:\n{}",
root_test_script(), stdout, stderr
);
}
#[test]
fn test_root_test_script_tests_actually_ran() {
require_test_script_deps();
let tmp = extract_project("root_verify");
#[cfg(unix)]
chmod_scripts(tmp.path());
let (success, stdout, stderr) = run_script(tmp.path(), root_test_script());
assert!(
success,
"{} failed.\nstdout:\n{}\nstderr:\n{}",
root_test_script(), stdout, stderr
);
// Verify that tests actually executed -- 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%")
|| combined.contains("PASS");
assert!(
tests_ran,
"{} output does not indicate any tests actually executed.\n\n\
stdout:\n{}\n\nstderr:\n{}",
root_test_script(), stdout, stderr
);
}
#[test]
fn test_root_test_script_idempotent() {
require_test_script_deps();
let tmp = extract_project("root_idem");
#[cfg(unix)]
chmod_scripts(tmp.path());
let (success1, _, _) = run_script(tmp.path(), root_test_script());
assert!(success1, "First run of {} failed", root_test_script());
let (success2, stdout2, stderr2) = run_script(tmp.path(), root_test_script());
assert!(
success2,
"Second run of {} failed (should be idempotent).\nstdout:\n{}\nstderr:\n{}",
root_test_script(), stdout2, stderr2
);
}
// ==========================================================================
// INNER test/run_tests.sh / test\run_tests.bat EXECUTION
//
// The test/ subdirectory script builds and runs the C++ unit tests
// directly. It must also work standalone.
//
// Required on build machine: cmake, g++ or clang++, git
// ==========================================================================
#[test]
fn test_inner_run_tests_script_executes_successfully() {
require_test_script_deps();
let tmp = extract_project("inner_test");
#[cfg(unix)]
chmod_scripts(tmp.path());
let (success, stdout, stderr) = run_script(tmp.path(), inner_test_script());
println!("--- {} stdout ---\n{}", inner_test_script(), stdout);
if !stderr.is_empty() {
eprintln!("--- {} stderr ---\n{}", inner_test_script(), stderr);
}
assert!(
success,
"{} failed!\n\nstdout:\n{}\n\nstderr:\n{}",
inner_test_script(), stdout, stderr
);
}
#[test]
fn test_inner_run_tests_google_tests_actually_ran() {
require_test_script_deps();
let tmp = extract_project("inner_gtest");
#[cfg(unix)]
chmod_scripts(tmp.path());
let (success, stdout, stderr) = run_script(tmp.path(), inner_test_script());
assert!(
success,
"{} failed.\nstdout:\n{}\nstderr:\n{}",
inner_test_script(), stdout, stderr
);
let combined = format!("{}{}", stdout, stderr);
let tests_ran = combined.contains("passed")
|| combined.contains("PASSED")
|| combined.contains("tests passed")
|| combined.contains("100%")
|| combined.contains("PASS");
assert!(
tests_ran,
"{} output does not indicate any Google Tests actually executed.\n\n\
stdout:\n{}\n\nstderr:\n{}",
inner_test_script(), stdout, stderr
);
}
#[test]
fn test_inner_run_tests_clean_flag_rebuilds() {
require_test_script_deps();
let tmp = extract_project("inner_clean");
#[cfg(unix)]
chmod_scripts(tmp.path());
// First run -- populates build dir and fetches gtest
let (success, _, _) = run_script(tmp.path(), inner_test_script());
assert!(success, "First run of {} failed", inner_test_script());
// 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(), inner_test_script(), &["--clean"]);
println!("--- clean rebuild stdout ---\n{}", stdout);
if !stderr.is_empty() {
eprintln!("--- clean rebuild stderr ---\n{}", stderr);
}
assert!(
success,
"Clean rebuild of {} failed.\nstdout:\n{}\nstderr:\n{}",
inner_test_script(), stdout, stderr
);
}
#[test]
fn test_inner_run_tests_produces_test_binary() {
require_test_script_deps();
let tmp = extract_project("inner_bin");
#[cfg(unix)]
chmod_scripts(tmp.path());
let (success, _, _) = run_script(tmp.path(), inner_test_script());
assert!(success, "{} failed", inner_test_script());
// 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 = find_file_recursive(&build_dir, "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_inner_run_tests_idempotent() {
require_test_script_deps();
let tmp = extract_project("inner_idem");
#[cfg(unix)]
chmod_scripts(tmp.path());
let (success1, _, _) = run_script(tmp.path(), inner_test_script());
assert!(success1, "First run failed");
let (success2, stdout2, stderr2) = run_script(tmp.path(), inner_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/build.bat 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]
#[serial]
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!("--- {} stdout ---\n{}", build_script(), stdout);
if !stderr.is_empty() {
eprintln!("--- {} stderr ---\n{}", build_script(), stderr);
}
assert!(
success,
"{} failed!\n\nstdout:\n{}\n\nstderr:\n{}",
build_script(), stdout, stderr
);
}
#[test]
fn test_build_script_produces_compilation_output() {
require_build_script_deps();
let tmp = extract_project("compile_out");
#[cfg(unix)]
chmod_scripts(tmp.path());
let (success, stdout, stderr) = run_script(tmp.path(), build_script());
assert!(
success,
"{} failed.\nstdout:\n{}\nstderr:\n{}",
build_script(), stdout, stderr
);
// arduino-cli compile produces output indicating sketch size
let combined = format!("{}{}", stdout, stderr);
let compiled = combined.contains("Sketch uses")
|| combined.contains("bytes")
|| combined.contains("Compiling")
|| combined.contains("Used")
|| combined.contains("compiled")
|| combined.contains("flash");
assert!(
compiled,
"{} output does not indicate a successful arduino-cli compilation.\n\n\
stdout:\n{}\n\nstderr:\n{}",
build_script(), stdout, stderr
);
}
#[test]
fn test_build_script_idempotent() {
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 all succeed on the same project
//
// Full end-to-end: one extracted project, all testable scripts pass.
// ==========================================================================
#[test]
fn test_full_project_all_scripts_pass() {
require_test_script_deps();
require_build_script_deps();
let tmp = extract_project("full_e2e");
#[cfg(unix)]
chmod_scripts(tmp.path());
// 1. Build the sketch with arduino-cli
let (build_ok, build_out, build_err) = run_script(tmp.path(), build_script());
println!("--- {} stdout ---\n{}", build_script(), build_out);
if !build_err.is_empty() {
eprintln!("--- {} stderr ---\n{}", build_script(), build_err);
}
assert!(
build_ok,
"{} failed in full E2E.\nstdout:\n{}\nstderr:\n{}",
build_script(), build_out, build_err
);
// 2. Run root-level test script
let (root_ok, root_out, root_err) = run_script(tmp.path(), root_test_script());
println!("--- {} stdout ---\n{}", root_test_script(), root_out);
if !root_err.is_empty() {
eprintln!("--- {} stderr ---\n{}", root_test_script(), root_err);
}
assert!(
root_ok,
"{} failed in full E2E.\nstdout:\n{}\nstderr:\n{}",
root_test_script(), root_out, root_err
);
// 3. Run inner test/run_tests script
let (inner_ok, inner_out, inner_err) = run_script(tmp.path(), inner_test_script());
println!("--- {} stdout ---\n{}", inner_test_script(), inner_out);
if !inner_err.is_empty() {
eprintln!("--- {} stderr ---\n{}", inner_test_script(), inner_err);
}
assert!(
inner_ok,
"{} failed in full E2E.\nstdout:\n{}\nstderr:\n{}",
inner_test_script(), inner_out, inner_err
);
}
// ==========================================================================
// SCRIPT CONTENT SANITY CHECKS
//
// Verify the scripts are well-formed before even executing them.
// These tests have NO external dependencies.
// ==========================================================================
#[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.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");
let sh_scripts = vec![
"build.sh",
"upload.sh",
"monitor.sh",
"test.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");
let pairs = vec![
("build.sh", "build.bat"),
("upload.sh", "upload.bat"),
("monitor.sh", "monitor.bat"),
("test.sh", "test.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.
// On Windows, build.bat is a thin wrapper that calls build.ps1,
// so we check the .ps1 file for content.
let config_scripts = vec![
"build.sh",
"build.ps1",
"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");
// Build/upload/monitor scripts must invoke arduino-cli directly.
// On Windows, build.bat is a thin wrapper calling build.ps1,
// so we check the .ps1 file for content.
let scripts = vec![
"build.sh", "build.ps1",
"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
if trimmed.starts_with('#')
|| trimmed.starts_with("::")
|| trimmed.starts_with("REM")
|| trimmed.starts_with("rem")
{
return false;
}
// Skip output/diagnostic lines -- these often contain
// suggestions like "Run: anvil doctor" which are messages
// to the user, not command invocations.
if trimmed.starts_with("echo")
|| trimmed.starts_with("Echo")
|| trimmed.starts_with("ECHO")
|| trimmed.starts_with("printf")
|| trimmed.starts_with("die ")
|| trimmed.starts_with("die(")
|| trimmed.starts_with("warn ")
|| trimmed.starts_with("warn(")
|| trimmed.starts_with("info ")
|| trimmed.starts_with("info(")
|| trimmed.starts_with("ok ")
|| trimmed.starts_with("ok(")
|| trimmed.starts_with(">&2")
|| trimmed.starts_with("1>&2")
|| trimmed.starts_with("Write-Host")
|| trimmed.starts_with("Write-Error")
|| trimmed.starts_with("Write-Warning")
|| trimmed.starts_with("Fail ")
|| trimmed.starts_with("Fail(")
|| trimmed.starts_with("Fail \"")
|| trimmed.starts_with("Fail @")
{
return false;
}
// Skip string assignments that contain suggestion text
// e.g. MSG="Run: anvil devices"
if trimmed.contains("=\"") && trimmed.contains("anvil ") {
return false;
}
// Check for "anvil " as a command invocation
trimmed.contains("anvil ")
&& !trimmed.contains("anvil.toml")
&& !trimmed.contains("Anvil")
&& !trimmed.contains("anvilignore")
&& !trimmed.contains("\"anvil ") // quoted suggestion text
&& !trimmed.contains("'anvil ") // single-quoted suggestion
});
assert!(
!has_anvil_cmd,
"{} should not invoke the anvil binary -- project must be self-contained",
script
);
}
}
#[test]
fn test_all_expected_scripts_exist() {
let tmp = extract_project("all_scripts");
let expected = vec![
"build.sh",
"build.bat",
"build.ps1",
"upload.sh",
"upload.bat",
"monitor.sh",
"monitor.bat",
"test.sh",
"test.bat",
"test/run_tests.sh",
"test/run_tests.bat",
];
for script in &expected {
let path = tmp.path().join(script);
assert!(
path.exists(),
"Expected script missing: {}\n\nProject contents:\n{}",
script,
list_dir_recursive(tmp.path())
);
}
}