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 ); } }