use tempfile::TempDir; use std::fs; use std::path::Path; use anvil::templates::{TemplateManager, TemplateContext}; use anvil::project::config::ProjectConfig; // ============================================================================ // Template extraction tests // ============================================================================ #[test] fn test_basic_template_extracts_all_expected_files() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "test_proj".to_string(), anvil_version: "1.0.0".to_string(), }; let count = TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); assert!(count >= 16, "Expected at least 16 files, got {}", count); } #[test] fn test_template_creates_sketch_directory() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let sketch_dir = tmp.path().join("blink"); assert!(sketch_dir.is_dir(), "Sketch directory should exist"); let ino_file = sketch_dir.join("blink.ino"); assert!(ino_file.exists(), "Sketch .ino file should exist"); // Verify the .ino content has correct includes let content = fs::read_to_string(&ino_file).unwrap(); assert!( content.contains("blink_app.h"), ".ino should include project-specific app header" ); assert!( content.contains("hal_arduino.h"), ".ino should include hal_arduino.h" ); } #[test] fn test_template_creates_hal_files() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "sensor".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); assert!( tmp.path().join("lib/hal/hal.h").exists(), "hal.h should exist" ); assert!( tmp.path().join("lib/hal/hal_arduino.h").exists(), "hal_arduino.h should exist" ); // Verify hal.h defines the abstract Hal class let hal_content = fs::read_to_string(tmp.path().join("lib/hal/hal.h")).unwrap(); assert!( hal_content.contains("class Hal"), "hal.h should define class Hal" ); assert!( hal_content.contains("virtual void pinMode"), "hal.h should declare pinMode" ); } #[test] fn test_template_creates_app_header() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "my_sensor".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let app_path = tmp.path().join("lib/app/my_sensor_app.h"); assert!(app_path.exists(), "App header should exist with project name"); let content = fs::read_to_string(&app_path).unwrap(); assert!( content.contains("#include "), "App header should include hal.h" ); assert!( content.contains("class BlinkApp"), "App header should define BlinkApp class" ); } #[test] fn test_template_creates_test_infrastructure() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); assert!( tmp.path().join("test/CMakeLists.txt").exists(), "CMakeLists.txt should exist" ); assert!( tmp.path().join("test/test_unit.cpp").exists(), "test_unit.cpp should exist" ); assert!( tmp.path().join("test/mocks/mock_hal.h").exists(), "mock_hal.h should exist" ); assert!( tmp.path().join("test/mocks/sim_hal.h").exists(), "sim_hal.h should exist" ); assert!( tmp.path().join("test/run_tests.sh").exists(), "run_tests.sh should exist" ); assert!( tmp.path().join("test/run_tests.bat").exists(), "run_tests.bat should exist" ); } #[test] fn test_template_test_file_references_correct_app() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "motor_ctrl".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let test_content = fs::read_to_string( tmp.path().join("test/test_unit.cpp") ).unwrap(); assert!( test_content.contains("motor_ctrl_app.h"), "Test file should include project-specific app header" ); } #[test] fn test_template_cmake_references_correct_project() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "my_bot".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let cmake = fs::read_to_string( tmp.path().join("test/CMakeLists.txt") ).unwrap(); assert!( cmake.contains("my_bot"), "CMakeLists.txt should contain project name" ); } #[test] fn test_template_creates_dot_files() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); assert!( tmp.path().join(".gitignore").exists(), ".gitignore should be created from _dot_ prefix" ); assert!( tmp.path().join(".editorconfig").exists(), ".editorconfig should be created" ); assert!( tmp.path().join(".clang-format").exists(), ".clang-format should be created" ); assert!( tmp.path().join(".vscode/settings.json").exists(), ".vscode/settings.json should be created" ); } #[test] fn test_template_creates_readme() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let readme = tmp.path().join("README.md"); assert!(readme.exists(), "README.md should exist"); let content = fs::read_to_string(&readme).unwrap(); assert!(content.contains("blink"), "README should contain project name"); } // ============================================================================ // .anvil.toml config tests // ============================================================================ #[test] fn test_template_creates_valid_config() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "blink".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); // Should be loadable by ProjectConfig let config = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(config.project.name, "blink"); assert_eq!(config.build.fqbn, "arduino:avr:uno"); assert_eq!(config.monitor.baud, 115200); assert!(config.build.extra_flags.contains(&"-Werror".to_string())); } #[test] fn test_config_roundtrip() { let tmp = TempDir::new().unwrap(); let config = ProjectConfig::new("roundtrip_test"); config.save(tmp.path()).unwrap(); let loaded = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(loaded.project.name, "roundtrip_test"); assert_eq!(loaded.build.fqbn, config.build.fqbn); assert_eq!(loaded.monitor.baud, config.monitor.baud); assert_eq!(loaded.build.include_dirs, config.build.include_dirs); } #[test] fn test_config_find_project_root_walks_up() { let tmp = TempDir::new().unwrap(); let config = ProjectConfig::new("walk_test"); config.save(tmp.path()).unwrap(); // Create nested subdirectory let deep = tmp.path().join("sketch").join("src").join("deep"); fs::create_dir_all(&deep).unwrap(); let found = ProjectConfig::find_project_root(&deep).unwrap(); let expected = tmp.path().canonicalize().unwrap(); // On Windows, canonicalize() returns \\?\ extended path prefix. // Strip it for comparison since find_project_root may not include it. let found_str = found.to_string_lossy().to_string(); let expected_str = expected.to_string_lossy().to_string(); let norm = |s: &str| s.strip_prefix(r"\\?\").unwrap_or(s).to_string(); assert_eq!(norm(&found_str), norm(&expected_str)); } #[test] fn test_config_resolve_include_flags() { let tmp = TempDir::new().unwrap(); fs::create_dir_all(tmp.path().join("lib/hal")).unwrap(); fs::create_dir_all(tmp.path().join("lib/app")).unwrap(); let config = ProjectConfig::new("flags_test"); let flags = config.resolve_include_flags(tmp.path()); assert_eq!(flags.len(), 2); assert!(flags[0].starts_with("-I")); assert!(flags[0].ends_with("lib/hal") || flags[0].ends_with("lib\\hal")); } #[test] fn test_config_skips_nonexistent_include_dirs() { let tmp = TempDir::new().unwrap(); // Don't create the directories let config = ProjectConfig::new("missing_dirs"); let flags = config.resolve_include_flags(tmp.path()); assert_eq!(flags.len(), 0, "Should skip non-existent directories"); } // ============================================================================ // Full project creation test (end-to-end in temp dir) // ============================================================================ #[test] fn test_full_project_structure() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "full_test".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); // Verify the complete expected file tree let expected_files = vec![ ".anvil.toml", ".gitignore", ".editorconfig", ".clang-format", ".vscode/settings.json", "README.md", "full_test/full_test.ino", "lib/hal/hal.h", "lib/hal/hal_arduino.h", "lib/app/full_test_app.h", "build.sh", "build.bat", "upload.sh", "upload.bat", "monitor.sh", "monitor.bat", "_detect_port.ps1", "test/CMakeLists.txt", "test/test_unit.cpp", "test/run_tests.sh", "test/run_tests.bat", "test/mocks/mock_hal.h", "test/mocks/sim_hal.h", ]; for f in &expected_files { let p = tmp.path().join(f); assert!( p.exists(), "Expected file missing: {} (checked {})", f, p.display() ); } } #[test] fn test_no_unicode_in_template_output() { // Eric's rule: only ASCII characters let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "ascii_test".to_string(), anvil_version: "1.0.0".to_string(), }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); // Check all generated text files for non-ASCII check_ascii_recursive(tmp.path()); } fn check_ascii_recursive(dir: &Path) { for entry in fs::read_dir(dir).unwrap() { let entry = entry.unwrap(); let path = entry.path(); if path.is_dir() { check_ascii_recursive(&path); } else { // Only check text files let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); if matches!(ext, "h" | "cpp" | "ino" | "txt" | "sh" | "bat" | "json" | "toml" | "md") { let content = fs::read_to_string(&path).unwrap(); for (line_num, line) in content.lines().enumerate() { for (col, ch) in line.chars().enumerate() { assert!( ch.is_ascii(), "Non-ASCII character '{}' (U+{:04X}) at {}:{}:{} ", ch, ch as u32, path.display(), line_num + 1, col + 1 ); } } } } } } // ============================================================================ // Error case tests // ============================================================================ #[test] fn test_unknown_template_fails() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "test".to_string(), anvil_version: "1.0.0".to_string(), }; let result = TemplateManager::extract("nonexistent", tmp.path(), &ctx); assert!(result.is_err()); } #[test] fn test_load_config_from_nonproject_fails() { let tmp = TempDir::new().unwrap(); let result = ProjectConfig::load(tmp.path()); assert!(result.is_err()); } // ============================================================================ // 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(), }; 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(), }; 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(), }; 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(), }; 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(), }; 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(), }; 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(), }; 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(), }; 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(), }; 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(), }; 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(), }; 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(), }; 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(), }; 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(), }; 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(), }; 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", "_detect_port.ps1", "test/run_tests.sh", "test/run_tests.bat", ]; 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(), }; 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 config must never be refreshable. let never_refreshable = vec![ ".anvil.toml", ".anvil.local", ".gitignore", ".editorconfig", ".clang-format", "README.md", "test/CMakeLists.txt", "test/test_unit.cpp", "test/mocks/mock_hal.h", "test/mocks/sim_hal.h", ]; let refreshable = vec![ "build.sh", "build.bat", "upload.sh", "upload.bat", "monitor.sh", "monitor.bat", "_detect_port.ps1", "test/run_tests.sh", "test/run_tests.bat", ]; 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(), }; 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 ); } }