Files
anvil/tests/test_scripts.rs

944 lines
31 KiB
Rust

use anvil::templates::{TemplateManager, TemplateContext};
use std::fs;
use tempfile::TempDir;
// ============================================================================
// Self-contained script tests
// ============================================================================
#[test]
fn test_template_creates_self_contained_scripts() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "standalone".to_string(),
anvil_version: "1.0.0".to_string(),
board_name: "uno".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
// All six scripts must exist
let scripts = vec![
"build.sh", "build.bat",
"upload.sh", "upload.bat",
"monitor.sh", "monitor.bat",
];
for script in &scripts {
let p = tmp.path().join(script);
assert!(p.exists(), "Script missing: {}", script);
}
}
#[test]
fn test_build_sh_reads_anvil_toml() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "toml_reader".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();
let content = fs::read_to_string(tmp.path().join("build.sh")).unwrap();
assert!(
content.contains(".anvil.toml"),
"build.sh should reference .anvil.toml"
);
assert!(
content.contains("arduino-cli"),
"build.sh should invoke arduino-cli"
);
assert!(
!content.contains("anvil build"),
"build.sh must NOT depend on the anvil binary"
);
}
#[test]
fn test_upload_sh_reads_anvil_toml() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "uploader".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();
let content = fs::read_to_string(tmp.path().join("upload.sh")).unwrap();
assert!(
content.contains(".anvil.toml"),
"upload.sh should reference .anvil.toml"
);
assert!(
content.contains("arduino-cli"),
"upload.sh should invoke arduino-cli"
);
assert!(
content.contains("upload"),
"upload.sh should contain upload command"
);
assert!(
content.contains("--monitor"),
"upload.sh should support --monitor flag"
);
assert!(
!content.contains("anvil upload"),
"upload.sh must NOT depend on the anvil binary"
);
}
#[test]
fn test_monitor_sh_reads_anvil_toml() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "serial_mon".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();
let content = fs::read_to_string(tmp.path().join("monitor.sh")).unwrap();
assert!(
content.contains(".anvil.toml"),
"monitor.sh should reference .anvil.toml"
);
assert!(
content.contains("--watch"),
"monitor.sh should support --watch flag"
);
assert!(
!content.contains("anvil monitor"),
"monitor.sh must NOT depend on the anvil binary"
);
}
#[test]
fn test_scripts_have_shebangs() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "shebangs".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();
for script in &["build.sh", "upload.sh", "monitor.sh", "test/run_tests.sh"] {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
assert!(
content.starts_with("#!/"),
"{} should start with a shebang line",
script
);
}
}
#[test]
fn test_scripts_no_anvil_binary_dependency() {
// Critical: generated projects must NOT require the anvil binary
// for build, upload, or monitor operations.
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "no_anvil_dep".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();
let scripts = vec![
"build.sh", "build.bat",
"upload.sh", "upload.bat",
"monitor.sh", "monitor.bat",
"test/run_tests.sh", "test/run_tests.bat",
];
for script in &scripts {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
// None of these scripts should shell out to anvil
let has_anvil_cmd = content.lines().any(|line| {
let trimmed = line.trim();
// Skip comments, echo/print lines, and shell output functions
if trimmed.starts_with('#')
|| trimmed.starts_with("::")
|| trimmed.starts_with("echo")
|| trimmed.starts_with("REM")
|| trimmed.starts_with("rem")
|| trimmed.starts_with("warn")
|| trimmed.starts_with("die")
{
return false;
}
// Check for "anvil " as a command invocation
trimmed.contains("anvil ")
&& !trimmed.contains("anvil.toml")
&& !trimmed.contains("anvil.local")
&& !trimmed.contains("Anvil")
});
assert!(
!has_anvil_cmd,
"{} should not invoke the anvil binary (project must be self-contained)",
script
);
}
}
#[test]
fn test_gitignore_excludes_build_cache() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "gitcheck".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();
let content = fs::read_to_string(tmp.path().join(".gitignore")).unwrap();
assert!(
content.contains(".build/"),
".gitignore should exclude .build/ (arduino-cli build cache)"
);
assert!(
content.contains("test/build/"),
".gitignore should exclude test/build/ (cmake build cache)"
);
assert!(
content.contains(".anvil.local"),
".gitignore should exclude .anvil.local (machine-specific config)"
);
}
#[test]
fn test_readme_documents_self_contained_workflow() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "docs_check".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();
let readme = fs::read_to_string(tmp.path().join("README.md")).unwrap();
assert!(
readme.contains("./build.sh"),
"README should document build.sh"
);
assert!(
readme.contains("./upload.sh"),
"README should document upload.sh"
);
assert!(
readme.contains("./monitor.sh"),
"README should document monitor.sh"
);
assert!(
readme.contains("self-contained"),
"README should mention self-contained"
);
}
#[test]
fn test_scripts_tolerate_missing_toml_keys() {
// Regression: toml_get must not kill the script when a key is absent.
// With set -euo pipefail, bare grep returns exit 1 on no match,
// pipefail propagates it, and set -e terminates silently.
// Every grep in toml_get/toml_array must have "|| true".
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "grep_safe".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();
for script in &["build.sh", "upload.sh", "monitor.sh"] {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
// If the script uses set -e (or -euo pipefail), then every
// toml_get/toml_array function must guard grep with || true
if content.contains("set -e") || content.contains("set -euo") {
// Find the toml_get function body and check for || true
let has_safe_grep = content.contains("|| true");
assert!(
has_safe_grep,
"{} uses set -e but toml_get/toml_array lacks '|| true' guard. \
Missing TOML keys will silently kill the script.",
script
);
}
}
}
// ==========================================================================
// Batch script safety
// ==========================================================================
#[test]
fn test_bat_scripts_no_unescaped_parens_in_echo() {
// Regression: unescaped ( or ) in echo lines inside if blocks
// cause cmd.exe to misparse block boundaries.
// e.g. "echo Configuring (first run)..." closes the if block early.
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "parens_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();
let bat_files = vec![
"build.bat",
"upload.bat",
"monitor.bat",
"test/run_tests.bat",
];
for bat in &bat_files {
let content = fs::read_to_string(tmp.path().join(bat)).unwrap();
let mut in_if_block = 0i32;
for (line_num, line) in content.lines().enumerate() {
let trimmed = line.trim();
// Track if-block nesting (rough heuristic)
if trimmed.starts_with("if ") && trimmed.ends_with('(') {
in_if_block += 1;
}
if trimmed == ")" {
in_if_block -= 1;
}
// Inside if blocks, echo lines must not have bare ( or )
if in_if_block > 0
&& (trimmed.starts_with("echo ") || trimmed.starts_with("echo."))
{
let msg_part = &trimmed[4..]; // after "echo"
// Allow ^( and ^) which are escaped
let unescaped_open = msg_part.matches('(').count()
- msg_part.matches("^(").count();
let unescaped_close = msg_part.matches(')').count()
- msg_part.matches("^)").count();
assert!(
unescaped_open == 0 && unescaped_close == 0,
"{} line {}: unescaped parentheses in echo inside if block: {}",
bat,
line_num + 1,
trimmed
);
}
}
}
}
// ==========================================================================
// .anvil.local references in scripts
// ==========================================================================
#[test]
fn test_scripts_read_anvil_local_for_port() {
// upload and monitor scripts should read port from .anvil.local,
// NOT from .anvil.toml.
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "local_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();
for script in &["upload.sh", "upload.bat", "monitor.sh", "monitor.bat"] {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
assert!(
content.contains(".anvil.local"),
"{} should reference .anvil.local for port config",
script
);
}
}
#[test]
fn test_anvil_toml_template_has_no_port() {
// Port config belongs in .anvil.local, not .anvil.toml
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "no_port".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();
let content = fs::read_to_string(tmp.path().join(".anvil.toml")).unwrap();
for line in content.lines() {
let trimmed = line.trim().trim_start_matches('#').trim();
assert!(
!trimmed.starts_with("port ")
&& !trimmed.starts_with("port=")
&& !trimmed.starts_with("port_windows")
&& !trimmed.starts_with("port_linux"),
".anvil.toml should not contain port entries, found: {}",
line
);
}
}
// ==========================================================================
// _detect_port.ps1 integration
// ==========================================================================
#[test]
fn test_bat_scripts_call_detect_port_ps1() {
// upload.bat and monitor.bat should delegate port detection to
// _detect_port.ps1, not do inline powershell with { } braces
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "ps1_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();
for bat in &["upload.bat", "monitor.bat"] {
let content = fs::read_to_string(tmp.path().join(bat)).unwrap();
assert!(
content.contains("_detect_port.ps1"),
"{} should call _detect_port.ps1 for port detection",
bat
);
}
}
#[test]
fn test_detect_port_ps1_is_valid() {
// Basic structural checks on the PowerShell helper
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "ps1_valid".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();
let content = fs::read_to_string(tmp.path().join("_detect_port.ps1")).unwrap();
assert!(
content.contains("arduino-cli board list --format json"),
"_detect_port.ps1 should use arduino-cli JSON output"
);
assert!(
content.contains("protocol_label"),
"_detect_port.ps1 should check protocol_label for USB detection"
);
assert!(
content.contains("VidPid"),
"_detect_port.ps1 should support VID:PID resolution"
);
}
// ==========================================================================
// Refresh command
// ==========================================================================
#[test]
fn test_refresh_freshly_extracted_is_up_to_date() {
// A freshly extracted project should have all refreshable files
// byte-identical to the template.
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "fresh_proj".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();
let reference = TempDir::new().unwrap();
TemplateManager::extract("basic", reference.path(), &ctx).unwrap();
let refreshable = vec![
"build.sh", "build.bat",
"upload.sh", "upload.bat",
"monitor.sh", "monitor.bat",
"test.sh", "test.bat",
"_detect_port.ps1",
"_monitor_filter.ps1",
"test/run_tests.sh", "test/run_tests.bat",
"test/CMakeLists.txt",
"test/mocks/mock_arduino.h",
"test/mocks/mock_arduino.cpp",
"test/mocks/Arduino.h",
"test/mocks/Wire.h",
"test/mocks/SPI.h",
"test/mocks/mock_hal.h",
"test/mocks/sim_hal.h",
];
for f in &refreshable {
let a = fs::read(tmp.path().join(f)).unwrap();
let b = fs::read(reference.path().join(f)).unwrap();
assert_eq!(a, b, "Freshly extracted {} should match template", f);
}
}
#[test]
fn test_refresh_detects_modified_script() {
// Tampering with a script should cause a byte mismatch
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "mod_proj".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();
// Tamper with build.sh
let build_sh = tmp.path().join("build.sh");
let mut content = fs::read_to_string(&build_sh).unwrap();
content.push_str("\n# user modification\n");
fs::write(&build_sh, content).unwrap();
// Compare with fresh template
let reference = TempDir::new().unwrap();
TemplateManager::extract("basic", reference.path(), &ctx).unwrap();
let a = fs::read(tmp.path().join("build.sh")).unwrap();
let b = fs::read(reference.path().join("build.sh")).unwrap();
assert_ne!(a, b, "Modified build.sh should differ from template");
// Non-modified file should still match
let a = fs::read(tmp.path().join("upload.sh")).unwrap();
let b = fs::read(reference.path().join("upload.sh")).unwrap();
assert_eq!(a, b, "Untouched upload.sh should match template");
}
#[test]
fn test_refresh_does_not_list_user_files() {
// .anvil.toml, source files, and user test code must never be refreshable.
let never_refreshable = vec![
".anvil.toml",
".anvil.local",
".gitignore",
".editorconfig",
".clang-format",
"README.md",
"test/test_unit.cpp",
"test/test_system.cpp",
];
let refreshable = vec![
"build.sh", "build.bat",
"upload.sh", "upload.bat",
"monitor.sh", "monitor.bat",
"test.sh", "test.bat",
"_detect_port.ps1",
"_monitor_filter.ps1",
"test/run_tests.sh", "test/run_tests.bat",
"test/CMakeLists.txt",
"test/mocks/mock_arduino.h",
"test/mocks/mock_arduino.cpp",
"test/mocks/Arduino.h",
"test/mocks/Wire.h",
"test/mocks/SPI.h",
"test/mocks/mock_hal.h",
"test/mocks/sim_hal.h",
];
for uf in &never_refreshable {
assert!(
!refreshable.contains(uf),
"{} must never be in the refreshable files list",
uf
);
}
}
// ==========================================================================
// .anvil.local VID:PID in scripts
// ==========================================================================
#[test]
fn test_scripts_read_vid_pid_from_anvil_local() {
// upload and monitor scripts should parse vid_pid from .anvil.local
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "vidpid_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();
for script in &["upload.sh", "upload.bat", "monitor.sh", "monitor.bat"] {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
assert!(
content.contains("vid_pid") || content.contains("VidPid") || content.contains("VID_PID"),
"{} should parse vid_pid from .anvil.local",
script
);
}
}
// ==========================================================================
// Scripts read default board from [build] section
// ==========================================================================
#[test]
fn test_scripts_read_default_board() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "default_read".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();
for script in &["build.sh", "upload.sh", "monitor.sh"] {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
assert!(
content.contains("DEFAULT_BOARD") && content.contains("'default'"),
"{} should read 'default' field from .anvil.toml into DEFAULT_BOARD",
script
);
assert!(
content.contains("ACTIVE_BOARD"),
"{} should resolve ACTIVE_BOARD from DEFAULT_BOARD or --board flag",
script
);
}
for bat in &["build.bat", "upload.bat", "monitor.bat"] {
let content = fs::read_to_string(tmp.path().join(bat)).unwrap();
assert!(
content.contains("DEFAULT_BOARD"),
"{} should read default field into DEFAULT_BOARD",
bat
);
}
}
// ==========================================================================
// USB_VID/USB_PID fix: compiler.cpp.extra_flags (not build.extra_flags)
// ==========================================================================
#[test]
fn test_scripts_use_compiler_extra_flags_not_build() {
// Regression: build.extra_flags clobbers board defaults (USB_VID, USB_PID)
// on ATmega32U4 boards (Micro, Leonardo). Scripts must use
// compiler.cpp.extra_flags and compiler.c.extra_flags instead,
// which are additive.
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "usb_fix".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();
let compile_scripts = vec![
"build.sh", "build.bat",
"upload.sh", "upload.bat",
];
for script in &compile_scripts {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
assert!(
!content.contains("build.extra_flags"),
"{} must NOT use build.extra_flags (clobbers USB_VID/USB_PID on ATmega32U4)",
script
);
assert!(
content.contains("compiler.cpp.extra_flags"),
"{} should use compiler.cpp.extra_flags (additive, USB-safe)",
script
);
assert!(
content.contains("compiler.c.extra_flags"),
"{} should use compiler.c.extra_flags (additive, USB-safe)",
script
);
}
}
#[test]
fn test_monitor_scripts_have_no_compile_flags() {
// monitor.sh and monitor.bat should NOT contain any compile flags
// since they don't compile anything.
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "mon_flags".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();
for script in &["monitor.sh", "monitor.bat"] {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
assert!(
!content.contains("compiler.cpp.extra_flags")
&& !content.contains("compiler.c.extra_flags")
&& !content.contains("build.extra_flags"),
"{} should not contain any compile flags",
script
);
}
}
// ==========================================================================
// Script error messages: helpful guidance without requiring anvil
// ==========================================================================
#[test]
fn test_script_errors_show_manual_fix() {
// Error messages should explain how to fix .anvil.toml by hand,
// since the project is self-contained and anvil may not be installed.
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "err_msg".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();
let all_scripts = vec![
"build.sh", "build.bat",
"upload.sh", "upload.bat",
"monitor.sh", "monitor.bat",
];
for script in &all_scripts {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
assert!(
content.contains("default = "),
"{} error messages should show the manual fix (default = \"...\")",
script
);
}
}
#[test]
fn test_script_errors_mention_arduino_cli() {
// Error messages should mention arduino-cli board listall as a
// discovery option, since the project doesn't require anvil.
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "cli_err".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();
let all_scripts = vec![
"build.sh", "build.bat",
"upload.sh", "upload.bat",
"monitor.sh", "monitor.bat",
];
for script in &all_scripts {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
assert!(
content.contains("arduino-cli board listall"),
"{} error messages should mention 'arduino-cli board listall' for board discovery",
script
);
}
}
#[test]
fn test_script_errors_mention_toml_section_syntax() {
// The "board not found" error in build/upload scripts should show
// the [boards.X] section syntax so users know exactly what to add.
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "section_err".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 and upload scripts have both no-default and board-not-found errors
for script in &["build.sh", "build.bat", "upload.sh", "upload.bat"] {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
assert!(
content.contains("[boards."),
"{} board-not-found error should show [boards.X] section syntax",
script
);
}
}
// ==========================================================================
// Monitor: --timestamps and --log flags
// ==========================================================================
#[test]
fn test_monitor_scripts_accept_timestamps_flag() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "ts_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();
for script in &["monitor.sh", "monitor.bat"] {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
assert!(
content.contains("--timestamps"),
"{} should accept --timestamps flag",
script
);
}
}
#[test]
fn test_monitor_scripts_accept_log_flag() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "log_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();
for script in &["monitor.sh", "monitor.bat"] {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
assert!(
content.contains("--log"),
"{} should accept --log flag for file output",
script
);
}
}
#[test]
fn test_monitor_sh_has_timestamp_format() {
// The timestamp format should include hours, minutes, seconds, and millis
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "ts_fmt".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();
let content = fs::read_to_string(tmp.path().join("monitor.sh")).unwrap();
assert!(
content.contains("%H:%M:%S"),
"monitor.sh should use HH:MM:SS timestamp format"
);
assert!(
content.contains("%3N"),
"monitor.sh should include milliseconds in timestamps"
);
}
#[test]
fn test_monitor_sh_timestamps_work_in_watch_mode() {
// The timestamp filter should also apply in --watch mode
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "watch_ts".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();
let content = fs::read_to_string(tmp.path().join("monitor.sh")).unwrap();
// The filter function should be called in the watch loop
assert!(
content.contains("monitor_filter"),
"monitor.sh should use a filter function for timestamps"
);
// Count usages of monitor_filter - should appear in both watch and non-watch
let filter_count = content.matches("monitor_filter").count();
assert!(
filter_count >= 3,
"monitor_filter should be defined and used in both watch and normal mode (found {} refs)",
filter_count
);
}
// ============================================================================
// Driver auto-discovery in build/upload scripts
// ============================================================================
#[test]
fn test_build_scripts_autodiscover_driver_includes() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "drv_test".to_string(),
anvil_version: "1.0.0".to_string(),
board_name: "uno".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
// All four compile scripts must auto-discover lib/drivers/*
for script in &["build.sh", "upload.sh", "build.bat", "upload.bat"] {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
assert!(
content.contains("lib/drivers") || content.contains("lib\\drivers"),
"{} must auto-discover lib/drivers/* for library include paths",
script
);
}
}