Compare commits
8 Commits
d5220bea03
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d86c79b9cb | ||
|
|
a517fba88a | ||
|
|
3570777a0d | ||
|
|
34d6a765b0 | ||
|
|
8374cb6079 | ||
|
|
7e8d7ecce5 | ||
|
|
1ede07df81 | ||
|
|
6a0b3af330 |
148
Cargo.lock
generated
148
Cargo.lock
generated
@@ -76,6 +76,7 @@ dependencies = [
|
|||||||
"predicates",
|
"predicates",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serial_test",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
@@ -261,6 +262,41 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-executor"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -380,6 +416,21 @@ version = "0.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
@@ -419,6 +470,35 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "predicates"
|
name = "predicates"
|
||||||
version = "3.1.4"
|
version = "3.1.4"
|
||||||
@@ -473,6 +553,15 @@ version = "5.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -539,6 +628,27 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scc"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc"
|
||||||
|
dependencies = [
|
||||||
|
"sdd",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sdd"
|
||||||
|
version = "3.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -591,6 +701,44 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serial_test"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f"
|
||||||
|
dependencies = [
|
||||||
|
"futures-executor",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
|
"scc",
|
||||||
|
"serial_test_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serial_test_derive"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ tempfile = "3.13"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2.0"
|
assert_cmd = "2.0"
|
||||||
predicates = "3.1"
|
predicates = "3.1"
|
||||||
|
serial_test = "3"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -259,6 +259,7 @@ your-project/
|
|||||||
test_tmp36.cpp Library driver tests
|
test_tmp36.cpp Library driver tests
|
||||||
CMakeLists.txt Fetches Google Test, compiles tests
|
CMakeLists.txt Fetches Google Test, compiles tests
|
||||||
build.sh / build.bat Compile sketch
|
build.sh / build.bat Compile sketch
|
||||||
|
build.sh / build.bat / build.ps1 Compile sketch (bat wraps ps1)
|
||||||
upload.sh / upload.bat Compile + upload to board
|
upload.sh / upload.bat Compile + upload to board
|
||||||
monitor.sh / monitor.bat Serial monitor
|
monitor.sh / monitor.bat Serial monitor
|
||||||
test.sh / test.bat Run host-side tests
|
test.sh / test.bat Run host-side tests
|
||||||
@@ -435,17 +436,38 @@ Upload these to a Gitea release. The script requires `build-essential`,
|
|||||||
`mingw-w64`, and `zip` as described above.
|
`mingw-w64`, and `zip` as described above.
|
||||||
|
|
||||||
### Running the test suite
|
### Running the test suite
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
642 unit/integration tests plus 7 end-to-end tests, zero warnings. The e2e
|
The test suite covers unit, integration, and end-to-end scenarios. 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
|
||||||
skip gracefully and everything else still passes.
|
skip gracefully and everything else still passes.
|
||||||
|
|
||||||
|
#### Full test suite on Linux / WSL
|
||||||
|
|
||||||
|
The e2e tests need `cmake`, `g++`, and `arduino-cli` with the AVR core:
|
||||||
|
```bash
|
||||||
|
sudo apt install cmake g++
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
|
||||||
|
sudo mv bin/arduino-cli /usr/local/bin/
|
||||||
|
arduino-cli core install arduino:avr
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Full test suite on Windows
|
||||||
|
|
||||||
|
Install [arduino-cli](https://arduino.github.io/arduino-cli/installation/)
|
||||||
|
and add it to your PATH, then install the AVR core:
|
||||||
|
```
|
||||||
|
arduino-cli core install arduino:avr
|
||||||
|
```
|
||||||
|
|
||||||
|
CMake and a C++ compiler are needed for the host-side test compilation.
|
||||||
|
Install [CMake](https://cmake.org/download/) and either MinGW-w64 or open
|
||||||
|
a Visual Studio Developer Command Prompt (which provides `cl.exe`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
95
build.rs
Normal file
95
build.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
// build.rs -- Compile-time detection of optional build tools.
|
||||||
|
//
|
||||||
|
// Sets cfg flags that integration tests use to gracefully skip when
|
||||||
|
// tools are missing instead of panicking with scary error messages.
|
||||||
|
//
|
||||||
|
// has_cmake cmake is in PATH
|
||||||
|
// has_cpp_compiler g++, clang++, or cl is in PATH
|
||||||
|
// has_git git is in PATH
|
||||||
|
// has_arduino_cli arduino-cli is in PATH
|
||||||
|
//
|
||||||
|
// Usage in tests:
|
||||||
|
// #[cfg_attr(not(has_cmake), ignore = "cmake not found")]
|
||||||
|
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
/// Check if a tool is available by running it with --version.
|
||||||
|
/// Works for most tools (cmake, git, g++, clang++, arduino-cli).
|
||||||
|
fn has_tool(name: &str) -> bool {
|
||||||
|
Command::new(name)
|
||||||
|
.arg("--version")
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()
|
||||||
|
.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a C++ compiler is available.
|
||||||
|
///
|
||||||
|
/// g++ and clang++ respond to --version normally. MSVC's cl.exe does
|
||||||
|
/// not -- it rejects --version and returns an error. On Windows we
|
||||||
|
/// fall back to checking PATH via `where cl`. On Unix, cmake cannot
|
||||||
|
/// discover MSVC so we only check g++ and clang++.
|
||||||
|
fn has_cpp_compiler() -> bool {
|
||||||
|
if has_tool("g++") || has_tool("clang++") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cl.exe doesn't support --version; check PATH directly on Windows.
|
||||||
|
// On a regular command prompt cl may not be in PATH, but cmake can
|
||||||
|
// still find MSVC via the Visual Studio registry. We check `where`
|
||||||
|
// as a best-effort signal.
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let found = Command::new("where")
|
||||||
|
.arg("cl")
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()
|
||||||
|
.map(|s| s.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if found {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: cmake can discover MSVC even when cl is not in
|
||||||
|
// PATH. Check if any Visual Studio installation exists by
|
||||||
|
// looking for vswhere, which ships with VS 2017+.
|
||||||
|
let vswhere = Command::new("cmd")
|
||||||
|
.args(["/C", "where", "vswhere"])
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()
|
||||||
|
.map(|s| s.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if vswhere {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let _ = (); // silence unused warning
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Declare custom cfg names so rustc doesn't warn about them.
|
||||||
|
println!("cargo::rustc-check-cfg=cfg(has_cmake)");
|
||||||
|
println!("cargo::rustc-check-cfg=cfg(has_cpp_compiler)");
|
||||||
|
println!("cargo::rustc-check-cfg=cfg(has_git)");
|
||||||
|
println!("cargo::rustc-check-cfg=cfg(has_arduino_cli)");
|
||||||
|
|
||||||
|
if has_tool("cmake") {
|
||||||
|
println!("cargo:rustc-cfg=has_cmake");
|
||||||
|
}
|
||||||
|
if has_cpp_compiler() {
|
||||||
|
println!("cargo:rustc-cfg=has_cpp_compiler");
|
||||||
|
}
|
||||||
|
if has_tool("git") {
|
||||||
|
println!("cargo:rustc-cfg=has_git");
|
||||||
|
}
|
||||||
|
if has_tool("arduino-cli") {
|
||||||
|
println!("cargo:rustc-cfg=has_arduino_cli");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
@echo off
|
@echo off
|
||||||
setlocal enabledelayedexpansion
|
:: build.bat -- Thin wrapper that invokes build.ps1
|
||||||
|
|
||||||
:: build.bat -- Compile the sketch using arduino-cli
|
|
||||||
::
|
::
|
||||||
:: Reads all settings from .anvil.toml. No Anvil binary required.
|
:: Students can type "build" at a command prompt or double-click this file.
|
||||||
|
:: All logic lives in build.ps1. Requires PowerShell 5.1+ (ships with
|
||||||
|
:: Windows 10/11).
|
||||||
|
::
|
||||||
|
:: Settings are read from .anvil.toml. No Anvil binary required.
|
||||||
::
|
::
|
||||||
:: Usage:
|
:: Usage:
|
||||||
:: build.bat Compile (verify only)
|
:: build.bat Compile (verify only)
|
||||||
@@ -11,167 +13,5 @@ setlocal enabledelayedexpansion
|
|||||||
:: build.bat --clean Delete build cache first
|
:: build.bat --clean Delete build cache first
|
||||||
:: build.bat --verbose Show full compiler output
|
:: build.bat --verbose Show full compiler output
|
||||||
|
|
||||||
set "SCRIPT_DIR=%~dp0"
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0build.ps1" %*
|
||||||
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
|
exit /b %ERRORLEVEL%
|
||||||
set "CONFIG=%SCRIPT_DIR%\.anvil.toml"
|
|
||||||
|
|
||||||
if not exist "%CONFIG%" (
|
|
||||||
echo FAIL: No .anvil.toml found in %SCRIPT_DIR%
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
:: -- Parse .anvil.toml (flat keys) ----------------------------------------
|
|
||||||
for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do (
|
|
||||||
set "_K=%%a"
|
|
||||||
if not "!_K:~0,1!"=="#" if not "!_K:~0,1!"=="[" (
|
|
||||||
set "_K=!_K: =!"
|
|
||||||
set "_V=%%b"
|
|
||||||
if defined _V (
|
|
||||||
set "_V=!_V: =!"
|
|
||||||
set "_V=!_V:"=!"
|
|
||||||
)
|
|
||||||
if "!_K!"=="name" set "SKETCH_NAME=!_V!"
|
|
||||||
if "!_K!"=="default" set "DEFAULT_BOARD=!_V!"
|
|
||||||
if "!_K!"=="warnings" set "WARNINGS=!_V!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%SKETCH_NAME%"=="" (
|
|
||||||
echo FAIL: Could not read project name from .anvil.toml
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
set "SKETCH_DIR=%SCRIPT_DIR%\%SKETCH_NAME%"
|
|
||||||
set "BUILD_DIR=%SCRIPT_DIR%\.build"
|
|
||||||
|
|
||||||
:: -- Parse arguments ------------------------------------------------------
|
|
||||||
set "DO_CLEAN=0"
|
|
||||||
set "VERBOSE="
|
|
||||||
set "BOARD_NAME="
|
|
||||||
|
|
||||||
:parse_args
|
|
||||||
if "%~1"=="" goto done_args
|
|
||||||
if "%~1"=="--board" set "BOARD_NAME=%~2" & shift & shift & goto parse_args
|
|
||||||
if "%~1"=="--clean" set "DO_CLEAN=1" & shift & goto parse_args
|
|
||||||
if "%~1"=="--verbose" set "VERBOSE=--verbose" & shift & goto parse_args
|
|
||||||
if "%~1"=="--help" goto show_help
|
|
||||||
if "%~1"=="-h" goto show_help
|
|
||||||
echo FAIL: Unknown option: %~1
|
|
||||||
exit /b 1
|
|
||||||
|
|
||||||
:show_help
|
|
||||||
echo Usage: build.bat [--board NAME] [--clean] [--verbose]
|
|
||||||
echo Compiles the sketch. Settings from .anvil.toml.
|
|
||||||
echo --board NAME selects a board from [boards.NAME].
|
|
||||||
exit /b 0
|
|
||||||
|
|
||||||
:done_args
|
|
||||||
|
|
||||||
:: -- Resolve board --------------------------------------------------------
|
|
||||||
if "%BOARD_NAME%"=="" set "BOARD_NAME=%DEFAULT_BOARD%"
|
|
||||||
|
|
||||||
if "%BOARD_NAME%"=="" (
|
|
||||||
echo FAIL: No default board set in .anvil.toml.
|
|
||||||
echo.
|
|
||||||
echo Add a default to the [build] section of .anvil.toml:
|
|
||||||
echo default = "uno"
|
|
||||||
echo.
|
|
||||||
echo And make sure a matching [boards.uno] section exists:
|
|
||||||
echo [boards.uno]
|
|
||||||
echo fqbn = "arduino:avr:uno"
|
|
||||||
echo.
|
|
||||||
echo Or with Anvil: anvil board --default uno
|
|
||||||
echo List boards: anvil board --listall
|
|
||||||
echo arduino-cli board listall
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
set "BOARD_SECTION=[boards.%BOARD_NAME%]"
|
|
||||||
set "IN_SECTION=0"
|
|
||||||
set "FQBN="
|
|
||||||
for /f "usebackq tokens=*" %%L in ("%CONFIG%") do (
|
|
||||||
set "_LINE=%%L"
|
|
||||||
if "!_LINE!"=="!BOARD_SECTION!" (
|
|
||||||
set "IN_SECTION=1"
|
|
||||||
) else if "!IN_SECTION!"=="1" (
|
|
||||||
if "!_LINE:~0,1!"=="[" (
|
|
||||||
set "IN_SECTION=0"
|
|
||||||
) else if not "!_LINE:~0,1!"=="#" (
|
|
||||||
for /f "tokens=1,* delims==" %%a in ("!_LINE!") do (
|
|
||||||
set "_BK=%%a"
|
|
||||||
set "_BK=!_BK: =!"
|
|
||||||
set "_BV=%%b"
|
|
||||||
if defined _BV (
|
|
||||||
set "_BV=!_BV: =!"
|
|
||||||
set "_BV=!_BV:"=!"
|
|
||||||
)
|
|
||||||
if "!_BK!"=="fqbn" set "FQBN=!_BV!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if "!FQBN!"=="" (
|
|
||||||
echo FAIL: No [boards.%BOARD_NAME%] section in .anvil.toml.
|
|
||||||
echo.
|
|
||||||
echo Add it to .anvil.toml:
|
|
||||||
echo [boards.%BOARD_NAME%]
|
|
||||||
echo fqbn = "arduino:avr:uno" ^(replace with your board^)
|
|
||||||
echo.
|
|
||||||
echo Or with Anvil: anvil board --add %BOARD_NAME%
|
|
||||||
echo List boards: anvil board --listall
|
|
||||||
echo arduino-cli board listall
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if not "%BOARD_NAME%"=="%DEFAULT_BOARD%" (
|
|
||||||
echo ok Using board: %BOARD_NAME% -- %FQBN%
|
|
||||||
)
|
|
||||||
|
|
||||||
:: -- Preflight ------------------------------------------------------------
|
|
||||||
where arduino-cli >nul 2>nul
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo FAIL: arduino-cli not found in PATH.
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if not exist "%SKETCH_DIR%" (
|
|
||||||
echo FAIL: Sketch directory not found: %SKETCH_DIR%
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
:: -- Clean ----------------------------------------------------------------
|
|
||||||
if "%DO_CLEAN%"=="1" (
|
|
||||||
if exist "%BUILD_DIR%" (
|
|
||||||
echo Cleaning build cache...
|
|
||||||
rmdir /s /q "%BUILD_DIR%"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
:: -- Build include flags --------------------------------------------------
|
|
||||||
set "BUILD_FLAGS="
|
|
||||||
for %%d in (lib\hal lib\app) do (
|
|
||||||
if exist "%SCRIPT_DIR%\%%d" (
|
|
||||||
set "BUILD_FLAGS=!BUILD_FLAGS! -I%SCRIPT_DIR%\%%d"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
set "BUILD_FLAGS=!BUILD_FLAGS! -Werror"
|
|
||||||
|
|
||||||
:: -- Compile --------------------------------------------------------------
|
|
||||||
echo Compiling %SKETCH_NAME%...
|
|
||||||
echo Board: %FQBN%
|
|
||||||
echo Sketch: %SKETCH_DIR%
|
|
||||||
echo.
|
|
||||||
|
|
||||||
if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%"
|
|
||||||
|
|
||||||
arduino-cli compile --fqbn %FQBN% --build-path "%BUILD_DIR%" --warnings %WARNINGS% --build-property "compiler.cpp.extra_flags=%BUILD_FLAGS%" --build-property "compiler.c.extra_flags=%BUILD_FLAGS%" %VERBOSE% "%SKETCH_DIR%"
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo.
|
|
||||||
echo FAIL: Compilation failed.
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ok Compile succeeded.
|
|
||||||
echo.
|
|
||||||
212
templates/basic/build.ps1
Normal file
212
templates/basic/build.ps1
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# build.ps1 -- Compile the sketch using arduino-cli
|
||||||
|
#
|
||||||
|
# Reads all settings from .anvil.toml. No Anvil binary required.
|
||||||
|
# Called by build.bat (thin wrapper) or directly:
|
||||||
|
# powershell -File build.ps1 [--board NAME] [--clean] [--verbose]
|
||||||
|
#
|
||||||
|
# Exit codes: 0 = success, 1 = error
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$board = "",
|
||||||
|
[switch]$clean,
|
||||||
|
[switch]$verbose,
|
||||||
|
[switch]$help
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# -- Helpers ---------------------------------------------------------------
|
||||||
|
|
||||||
|
function Fail($msg) {
|
||||||
|
Write-Host "FAIL: $msg" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ok($msg) {
|
||||||
|
Write-Host "ok $msg" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Locate config ---------------------------------------------------------
|
||||||
|
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$Config = Join-Path $ScriptDir ".anvil.toml"
|
||||||
|
|
||||||
|
if (-not (Test-Path $Config)) {
|
||||||
|
Fail "No .anvil.toml found in $ScriptDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Parse .anvil.toml -----------------------------------------------------
|
||||||
|
# Simple line-by-line parser. Tracks current section header to support
|
||||||
|
# [project], [build], [boards.NAME], etc.
|
||||||
|
|
||||||
|
$tomlData = @{}
|
||||||
|
$currentSection = ""
|
||||||
|
|
||||||
|
foreach ($rawLine in Get-Content $Config) {
|
||||||
|
$line = $rawLine.Trim()
|
||||||
|
|
||||||
|
# Skip blank lines and comments
|
||||||
|
if ($line -eq "" -or $line.StartsWith("#")) { continue }
|
||||||
|
|
||||||
|
# Section header: [project], [boards.uno], etc.
|
||||||
|
if ($line -match '^\[(.+)\]$') {
|
||||||
|
$currentSection = $Matches[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Key = value (only process lines with =)
|
||||||
|
if ($line -match '^([^=]+?)\s*=\s*(.+)$') {
|
||||||
|
$key = $Matches[1].Trim()
|
||||||
|
$val = $Matches[2].Trim()
|
||||||
|
|
||||||
|
# Strip surrounding quotes
|
||||||
|
if ($val.StartsWith('"') -and $val.EndsWith('"')) {
|
||||||
|
$val = $val.Substring(1, $val.Length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Skip array values (multi-line or inline [...])
|
||||||
|
if ($val.StartsWith("[")) { continue }
|
||||||
|
|
||||||
|
$fullKey = if ($currentSection) { "$currentSection.$key" } else { $key }
|
||||||
|
$tomlData[$fullKey] = $val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Extract settings ------------------------------------------------------
|
||||||
|
|
||||||
|
$SketchName = $tomlData["project.name"]
|
||||||
|
$DefaultBoard = $tomlData["build.default"]
|
||||||
|
$Warnings = $tomlData["build.warnings"]
|
||||||
|
|
||||||
|
if (-not $SketchName) {
|
||||||
|
Fail "Could not read project name from .anvil.toml"
|
||||||
|
}
|
||||||
|
|
||||||
|
$SketchDir = Join-Path $ScriptDir $SketchName
|
||||||
|
$BuildDir = Join-Path $ScriptDir ".build"
|
||||||
|
|
||||||
|
# -- Help ------------------------------------------------------------------
|
||||||
|
|
||||||
|
if ($help) {
|
||||||
|
Write-Host "Usage: build.bat [--board NAME] [--clean] [--verbose]"
|
||||||
|
Write-Host " Compiles the sketch. Settings from .anvil.toml."
|
||||||
|
Write-Host " --board NAME selects a board from [boards.NAME]."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Resolve board ---------------------------------------------------------
|
||||||
|
|
||||||
|
$BoardName = if ($board) { $board } else { $DefaultBoard }
|
||||||
|
|
||||||
|
if (-not $BoardName) {
|
||||||
|
Fail @"
|
||||||
|
No default board set in .anvil.toml.
|
||||||
|
|
||||||
|
Add a default to the [build] section of .anvil.toml:
|
||||||
|
default = "uno"
|
||||||
|
|
||||||
|
And make sure a matching [boards.uno] section exists:
|
||||||
|
[boards.uno]
|
||||||
|
fqbn = "arduino:avr:uno"
|
||||||
|
|
||||||
|
Or with Anvil: Anvil board --default uno
|
||||||
|
List boards: Anvil board --listall
|
||||||
|
arduino-cli board listall
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
$Fqbn = $tomlData["boards.$BoardName.fqbn"]
|
||||||
|
|
||||||
|
if (-not $Fqbn) {
|
||||||
|
Fail @"
|
||||||
|
No [boards.$BoardName] section in .anvil.toml.
|
||||||
|
|
||||||
|
Add it to .anvil.toml:
|
||||||
|
[boards.$BoardName]
|
||||||
|
fqbn = "arduino:avr:uno" (replace with your board)
|
||||||
|
|
||||||
|
Or with Anvil: Anvil board --add $BoardName
|
||||||
|
List boards: Anvil board --listall
|
||||||
|
arduino-cli board listall
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($BoardName -ne $DefaultBoard) {
|
||||||
|
Ok "Using board: $BoardName -- $Fqbn"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Preflight -------------------------------------------------------------
|
||||||
|
|
||||||
|
$arduinoCli = Get-Command "arduino-cli" -ErrorAction SilentlyContinue
|
||||||
|
if (-not $arduinoCli) {
|
||||||
|
Fail "arduino-cli not found in PATH."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $SketchDir)) {
|
||||||
|
Fail "Sketch directory not found: $SketchDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Clean -----------------------------------------------------------------
|
||||||
|
|
||||||
|
if ($clean -and (Test-Path $BuildDir)) {
|
||||||
|
Write-Host "Cleaning build cache..."
|
||||||
|
Remove-Item -Recurse -Force $BuildDir
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Build include flags ---------------------------------------------------
|
||||||
|
|
||||||
|
$buildFlags = @()
|
||||||
|
foreach ($sub in @("lib\hal", "lib\app")) {
|
||||||
|
$dir = Join-Path $ScriptDir $sub
|
||||||
|
if (Test-Path $dir) {
|
||||||
|
$buildFlags += "-I$dir"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auto-discover driver libraries (added by: anvil add <driver>)
|
||||||
|
$driversDir = Join-Path $ScriptDir "lib\drivers"
|
||||||
|
if (Test-Path $driversDir) {
|
||||||
|
foreach ($d in Get-ChildItem -Path $driversDir -Directory) {
|
||||||
|
$buildFlags += "-I$($d.FullName)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$buildFlags += "-Werror"
|
||||||
|
$flagsStr = $buildFlags -join " "
|
||||||
|
|
||||||
|
# -- Compile ---------------------------------------------------------------
|
||||||
|
|
||||||
|
Write-Host "Compiling $SketchName..."
|
||||||
|
Write-Host " Board: $Fqbn"
|
||||||
|
Write-Host " Sketch: $SketchDir"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
if (-not (Test-Path $BuildDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $BuildDir | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$compileArgs = @(
|
||||||
|
"compile"
|
||||||
|
"--fqbn", $Fqbn
|
||||||
|
"--build-path", $BuildDir
|
||||||
|
"--warnings", $Warnings
|
||||||
|
"--build-property", "compiler.cpp.extra_flags=$flagsStr"
|
||||||
|
"--build-property", "compiler.c.extra_flags=$flagsStr"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($verbose) {
|
||||||
|
$compileArgs += "--verbose"
|
||||||
|
}
|
||||||
|
|
||||||
|
$compileArgs += $SketchDir
|
||||||
|
|
||||||
|
& arduino-cli @compileArgs
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Fail "Compilation failed."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Ok "Compile succeeded."
|
||||||
|
Write-Host ""
|
||||||
|
exit 0
|
||||||
@@ -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
|
||||||
|
|||||||
819
tests/script_execution_test.rs
Normal file
819
tests/script_execution_test.rs
Normal file
@@ -0,0 +1,819 @@
|
|||||||
|
// ==========================================================================
|
||||||
|
// 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 a dependency is missing, the test is SKIPPED (shown as "ignored"
|
||||||
|
// in cargo test output). Detection happens at compile time via build.rs
|
||||||
|
// which sets cfg flags: has_cmake, has_cpp_compiler, has_git,
|
||||||
|
// has_arduino_cli.
|
||||||
|
//
|
||||||
|
// 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: "uno".to_string(), // <-- was "Arduino Uno (ATmega328P)"
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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]
|
||||||
|
#[cfg_attr(not(has_cmake), ignore = "cmake not found")]
|
||||||
|
#[cfg_attr(not(has_cpp_compiler), ignore = "C++ compiler not found")]
|
||||||
|
#[cfg_attr(not(has_git), ignore = "git not found")]
|
||||||
|
fn test_root_test_script_executes_successfully() {
|
||||||
|
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]
|
||||||
|
#[cfg_attr(not(has_cmake), ignore = "cmake not found")]
|
||||||
|
#[cfg_attr(not(has_cpp_compiler), ignore = "C++ compiler not found")]
|
||||||
|
#[cfg_attr(not(has_git), ignore = "git not found")]
|
||||||
|
fn test_root_test_script_tests_actually_ran() {
|
||||||
|
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]
|
||||||
|
#[cfg_attr(not(has_cmake), ignore = "cmake not found")]
|
||||||
|
#[cfg_attr(not(has_cpp_compiler), ignore = "C++ compiler not found")]
|
||||||
|
#[cfg_attr(not(has_git), ignore = "git not found")]
|
||||||
|
fn test_root_test_script_idempotent() {
|
||||||
|
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]
|
||||||
|
#[cfg_attr(not(has_cmake), ignore = "cmake not found")]
|
||||||
|
#[cfg_attr(not(has_cpp_compiler), ignore = "C++ compiler not found")]
|
||||||
|
#[cfg_attr(not(has_git), ignore = "git not found")]
|
||||||
|
fn test_inner_run_tests_script_executes_successfully() {
|
||||||
|
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]
|
||||||
|
#[cfg_attr(not(has_cmake), ignore = "cmake not found")]
|
||||||
|
#[cfg_attr(not(has_cpp_compiler), ignore = "C++ compiler not found")]
|
||||||
|
#[cfg_attr(not(has_git), ignore = "git not found")]
|
||||||
|
fn test_inner_run_tests_google_tests_actually_ran() {
|
||||||
|
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]
|
||||||
|
#[cfg_attr(not(has_cmake), ignore = "cmake not found")]
|
||||||
|
#[cfg_attr(not(has_cpp_compiler), ignore = "C++ compiler not found")]
|
||||||
|
#[cfg_attr(not(has_git), ignore = "git not found")]
|
||||||
|
fn test_inner_run_tests_clean_flag_rebuilds() {
|
||||||
|
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]
|
||||||
|
#[cfg_attr(not(has_cmake), ignore = "cmake not found")]
|
||||||
|
#[cfg_attr(not(has_cpp_compiler), ignore = "C++ compiler not found")]
|
||||||
|
#[cfg_attr(not(has_git), ignore = "git not found")]
|
||||||
|
fn test_inner_run_tests_produces_test_binary() {
|
||||||
|
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]
|
||||||
|
#[cfg_attr(not(has_cmake), ignore = "cmake not found")]
|
||||||
|
#[cfg_attr(not(has_cpp_compiler), ignore = "C++ compiler not found")]
|
||||||
|
#[cfg_attr(not(has_git), ignore = "git not found")]
|
||||||
|
fn test_inner_run_tests_idempotent() {
|
||||||
|
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]
|
||||||
|
#[cfg_attr(not(has_arduino_cli), ignore = "arduino-cli not found")]
|
||||||
|
fn test_build_script_compiles_sketch() {
|
||||||
|
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]
|
||||||
|
#[serial]
|
||||||
|
#[cfg_attr(not(has_arduino_cli), ignore = "arduino-cli not found")]
|
||||||
|
fn test_build_script_produces_compilation_output() {
|
||||||
|
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]
|
||||||
|
#[serial]
|
||||||
|
#[cfg_attr(not(has_arduino_cli), ignore = "arduino-cli not found")]
|
||||||
|
fn test_build_script_idempotent() {
|
||||||
|
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]
|
||||||
|
#[serial]
|
||||||
|
#[cfg_attr(not(has_cmake), ignore = "cmake not found")]
|
||||||
|
#[cfg_attr(not(has_cpp_compiler), ignore = "C++ compiler not found")]
|
||||||
|
#[cfg_attr(not(has_git), ignore = "git not found")]
|
||||||
|
#[cfg_attr(not(has_arduino_cli), ignore = "arduino-cli not found")]
|
||||||
|
fn test_full_project_all_scripts_pass() {
|
||||||
|
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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -574,7 +574,9 @@ fn test_sh_scripts_have_toml_section_get() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bat_scripts_have_section_parser() {
|
fn test_bat_scripts_have_section_parser() {
|
||||||
// Batch scripts need section-aware TOML parsing for board profiles
|
// Windows scripts need section-aware TOML parsing for board profiles.
|
||||||
|
// build.bat delegates to build.ps1; upload.bat and monitor.bat may
|
||||||
|
// still use batch-native parsing or their own .ps1 backends.
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
let ctx = TemplateContext {
|
let ctx = TemplateContext {
|
||||||
project_name: "bat_section".to_string(),
|
project_name: "bat_section".to_string(),
|
||||||
@@ -585,12 +587,33 @@ fn test_bat_scripts_have_section_parser() {
|
|||||||
};
|
};
|
||||||
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||||
|
|
||||||
for bat in &["build.bat", "upload.bat", "monitor.bat"] {
|
// Check each Windows script OR its PowerShell backend for section parsing
|
||||||
let content = fs::read_to_string(tmp.path().join(bat)).unwrap();
|
let pairs: &[(&str, &str)] = &[
|
||||||
|
("build.bat", "build.ps1"),
|
||||||
|
("upload.bat", "upload.ps1"),
|
||||||
|
("monitor.bat", "monitor.ps1"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (bat, ps1) in pairs {
|
||||||
|
let bat_path = tmp.path().join(bat);
|
||||||
|
let ps1_path = tmp.path().join(ps1);
|
||||||
|
|
||||||
|
let has_parser = if ps1_path.exists() {
|
||||||
|
// PowerShell backend handles TOML parsing
|
||||||
|
let content = fs::read_to_string(&ps1_path).unwrap();
|
||||||
|
content.contains("boards.") || content.contains("currentSection")
|
||||||
|
} else if bat_path.exists() {
|
||||||
|
// Batch does its own section parsing
|
||||||
|
let content = fs::read_to_string(&bat_path).unwrap();
|
||||||
|
content.contains("BOARD_SECTION") || content.contains("IN_SECTION")
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
content.contains("BOARD_SECTION") || content.contains("IN_SECTION"),
|
has_parser,
|
||||||
"{} should have section parser for board profiles",
|
"{} (or {}) should have section parser for board profiles",
|
||||||
bat
|
bat, ps1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -489,7 +489,7 @@ fn test_refresh_freshly_extracted_is_up_to_date() {
|
|||||||
TemplateManager::extract("basic", reference.path(), &ctx).unwrap();
|
TemplateManager::extract("basic", reference.path(), &ctx).unwrap();
|
||||||
|
|
||||||
let refreshable = vec![
|
let refreshable = vec![
|
||||||
"build.sh", "build.bat",
|
"build.sh", "build.bat", "build.ps1",
|
||||||
"upload.sh", "upload.bat",
|
"upload.sh", "upload.bat",
|
||||||
"monitor.sh", "monitor.bat",
|
"monitor.sh", "monitor.bat",
|
||||||
"test.sh", "test.bat",
|
"test.sh", "test.bat",
|
||||||
@@ -561,7 +561,7 @@ fn test_refresh_does_not_list_user_files() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let refreshable = vec![
|
let refreshable = vec![
|
||||||
"build.sh", "build.bat",
|
"build.sh", "build.bat", "build.ps1",
|
||||||
"upload.sh", "upload.bat",
|
"upload.sh", "upload.bat",
|
||||||
"monitor.sh", "monitor.bat",
|
"monitor.sh", "monitor.bat",
|
||||||
"test.sh", "test.bat",
|
"test.sh", "test.bat",
|
||||||
@@ -644,12 +644,14 @@ fn test_scripts_read_default_board() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for bat in &["build.bat", "upload.bat", "monitor.bat"] {
|
// build.bat is now a thin wrapper; build.ps1 has the real logic.
|
||||||
let content = fs::read_to_string(tmp.path().join(bat)).unwrap();
|
// upload.bat and monitor.bat still have batch-native parsing.
|
||||||
|
for script in &["build.ps1", "upload.bat", "monitor.bat"] {
|
||||||
|
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
content.contains("DEFAULT_BOARD"),
|
content.contains("DEFAULT_BOARD") || content.contains("DefaultBoard") || content.contains("default"),
|
||||||
"{} should read default field into DEFAULT_BOARD",
|
"{} should read default field for board selection",
|
||||||
bat
|
script
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -674,8 +676,9 @@ fn test_scripts_use_compiler_extra_flags_not_build() {
|
|||||||
};
|
};
|
||||||
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||||
|
|
||||||
|
// build.bat is a thin wrapper; check build.ps1 for content
|
||||||
let compile_scripts = vec![
|
let compile_scripts = vec![
|
||||||
"build.sh", "build.bat",
|
"build.sh", "build.ps1",
|
||||||
"upload.sh", "upload.bat",
|
"upload.sh", "upload.bat",
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -743,8 +746,9 @@ fn test_script_errors_show_manual_fix() {
|
|||||||
};
|
};
|
||||||
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||||
|
|
||||||
|
// build.bat is a thin wrapper; check build.ps1 for content
|
||||||
let all_scripts = vec![
|
let all_scripts = vec![
|
||||||
"build.sh", "build.bat",
|
"build.sh", "build.ps1",
|
||||||
"upload.sh", "upload.bat",
|
"upload.sh", "upload.bat",
|
||||||
"monitor.sh", "monitor.bat",
|
"monitor.sh", "monitor.bat",
|
||||||
];
|
];
|
||||||
@@ -752,7 +756,7 @@ fn test_script_errors_show_manual_fix() {
|
|||||||
for script in &all_scripts {
|
for script in &all_scripts {
|
||||||
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
|
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
content.contains("default = "),
|
content.contains("default = ") || content.contains("default ="),
|
||||||
"{} error messages should show the manual fix (default = \"...\")",
|
"{} error messages should show the manual fix (default = \"...\")",
|
||||||
script
|
script
|
||||||
);
|
);
|
||||||
@@ -773,8 +777,9 @@ fn test_script_errors_mention_arduino_cli() {
|
|||||||
};
|
};
|
||||||
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||||
|
|
||||||
|
// build.bat is a thin wrapper; check build.ps1 for content
|
||||||
let all_scripts = vec![
|
let all_scripts = vec![
|
||||||
"build.sh", "build.bat",
|
"build.sh", "build.ps1",
|
||||||
"upload.sh", "upload.bat",
|
"upload.sh", "upload.bat",
|
||||||
"monitor.sh", "monitor.bat",
|
"monitor.sh", "monitor.bat",
|
||||||
];
|
];
|
||||||
@@ -803,8 +808,8 @@ fn test_script_errors_mention_toml_section_syntax() {
|
|||||||
};
|
};
|
||||||
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||||
|
|
||||||
// build and upload scripts have both no-default and board-not-found errors
|
// build.bat is a thin wrapper; check build.ps1 for content
|
||||||
for script in &["build.sh", "build.bat", "upload.sh", "upload.bat"] {
|
for script in &["build.sh", "build.ps1", "upload.sh", "upload.bat"] {
|
||||||
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
|
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
content.contains("[boards."),
|
content.contains("[boards."),
|
||||||
@@ -914,4 +919,32 @@ 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();
|
||||||
|
|
||||||
|
// build.bat is a thin wrapper; check build.ps1 for content.
|
||||||
|
// All four compile scripts must auto-discover lib/drivers/*
|
||||||
|
for script in &["build.sh", "upload.sh", "build.ps1", "upload.bat"] {
|
||||||
|
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
|
||||||
|
assert!(
|
||||||
|
content.contains("lib/drivers") || 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