use anvil::project::config::{ ProjectConfig, BoardProfile, CONFIG_FILENAME, set_default_in_file, }; use anvil::templates::{TemplateManager, TemplateContext}; use std::fs; use tempfile::TempDir; // ============================================================================ // Config: default board and [boards.X] structure // ============================================================================ #[test] fn test_config_default_points_to_boards_section() { let config = ProjectConfig::new("test"); assert_eq!(config.build.default, "uno"); assert!( config.boards.contains_key("uno"), "boards map should contain the default board" ); assert_eq!(config.boards["uno"].fqbn, "arduino:avr:uno"); } #[test] fn test_config_default_fqbn_resolves() { let config = ProjectConfig::new("test"); let fqbn = config.default_fqbn().unwrap(); assert_eq!(fqbn, "arduino:avr:uno"); } #[test] fn test_config_default_board_returns_fqbn_and_baud() { let config = ProjectConfig::new("test"); let (fqbn, baud) = config.default_board().unwrap(); assert_eq!(fqbn, "arduino:avr:uno"); assert_eq!(baud, 115200); } #[test] fn test_config_resolve_board_named() { let mut config = ProjectConfig::new("test"); config.boards.insert("mega".to_string(), BoardProfile { fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(), baud: Some(57600), }); let (fqbn, baud) = config.resolve_board("mega").unwrap(); assert_eq!(fqbn, "arduino:avr:mega:cpu=atmega2560"); assert_eq!(baud, 57600); } #[test] fn test_config_resolve_board_inherits_baud() { let mut config = ProjectConfig::new("test"); config.boards.insert("nano".to_string(), BoardProfile { fqbn: "arduino:avr:nano".to_string(), baud: None, }); let (_, baud) = config.resolve_board("nano").unwrap(); assert_eq!( baud, config.monitor.baud, "Should inherit monitor baud when board has no override" ); } #[test] fn test_config_resolve_board_unknown_fails() { let config = ProjectConfig::new("test"); assert!(config.resolve_board("esp32").is_err()); } #[test] fn test_config_new_with_board_preset() { let config = ProjectConfig::new_with_board( "test", "mega", "arduino:avr:mega:cpu=atmega2560", 115200, ); assert_eq!(config.build.default, "mega"); assert!(config.boards.contains_key("mega")); assert_eq!( config.boards["mega"].fqbn, "arduino:avr:mega:cpu=atmega2560" ); assert!( !config.boards.contains_key("uno"), "Should only contain the specified board" ); } #[test] fn test_config_board_roundtrip() { let tmp = TempDir::new().unwrap(); let mut config = ProjectConfig::new("multi"); config.boards.insert("mega".to_string(), BoardProfile { fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(), baud: Some(57600), }); config.save(tmp.path()).unwrap(); let loaded = ProjectConfig::load(tmp.path()).unwrap(); assert!(loaded.boards.contains_key("mega")); let mega = &loaded.boards["mega"]; assert_eq!(mega.fqbn, "arduino:avr:mega:cpu=atmega2560"); assert_eq!(mega.baud, Some(57600)); } #[test] fn test_toml_template_has_default_field() { let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "default_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 content = fs::read_to_string(tmp.path().join(".anvil.toml")).unwrap(); assert!( content.contains("default = \"uno\""), ".anvil.toml should have default = \"uno\" in [build]" ); assert!( content.contains("[boards.uno]"), ".anvil.toml should have a [boards.uno] section" ); } #[test] fn test_toml_template_no_fqbn_in_build_section() { // New configs should NOT have fqbn directly in [build] let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "no_build_fqbn".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(); let mut in_build = false; for line in content.lines() { let trimmed = line.trim(); if trimmed == "[build]" { in_build = true; continue; } if trimmed.starts_with('[') { in_build = false; } if in_build && !trimmed.starts_with('#') { assert!( !trimmed.starts_with("fqbn"), "[build] section should not contain fqbn; it belongs in [boards.X]" ); } } } // ============================================================================ // Config migration: old format -> new format // ============================================================================ #[test] fn test_migrate_old_config_adds_default_and_boards() { let tmp = TempDir::new().unwrap(); let old_config = "\ [project]\n\ name = \"hand\"\n\ anvil_version = \"1.0.0\"\n\ \n\ [build]\n\ fqbn = \"arduino:avr:uno\"\n\ warnings = \"more\"\n\ include_dirs = [\"lib/hal\", \"lib/app\"]\n\ extra_flags = [\"-Werror\"]\n\ \n\ [monitor]\n\ baud = 115200\n\ \n\ [boards.micro]\n\ fqbn = \"arduino:avr:micro\"\n"; fs::write(tmp.path().join(CONFIG_FILENAME), old_config).unwrap(); let config = ProjectConfig::load(tmp.path()).unwrap(); // In-memory state assert_eq!(config.build.default, "uno"); assert!(config.boards.contains_key("uno")); assert_eq!(config.boards["uno"].fqbn, "arduino:avr:uno"); assert!(config.boards.contains_key("micro")); // On-disk file rewritten let content = fs::read_to_string(tmp.path().join(CONFIG_FILENAME)).unwrap(); assert!( content.contains("default = \"uno\""), "Migrated file should contain default = \"uno\"" ); assert!( content.contains("[boards.uno]"), "Migrated file should add [boards.uno] section" ); // Old fqbn kept for backward compat with pre-refresh scripts assert!( content.contains("fqbn = \"arduino:avr:uno\""), "Migration should keep old fqbn for backward compat" ); } #[test] fn test_migrate_preserves_comments() { let tmp = TempDir::new().unwrap(); let old_config = "\ [project]\n\ name = \"test\"\n\ anvil_version = \"1.0.0\"\n\ \n\ [build]\n\ fqbn = \"arduino:avr:uno\"\n\ warnings = \"more\"\n\ include_dirs = [\"lib/hal\", \"lib/app\"]\n\ extra_flags = [\"-Werror\"]\n\ \n\ [monitor]\n\ baud = 115200\n\ # port = \"/dev/ttyUSB0\" # Uncomment to skip auto-detect\n"; fs::write(tmp.path().join(CONFIG_FILENAME), old_config).unwrap(); ProjectConfig::load(tmp.path()).unwrap(); let content = fs::read_to_string(tmp.path().join(CONFIG_FILENAME)).unwrap(); assert!( content.contains("# port = \"/dev/ttyUSB0\""), "Migration should preserve comments" ); } #[test] fn test_migrate_matches_preset_name() { // A mega FQBN should get board name "mega", not "default" let tmp = TempDir::new().unwrap(); let old_config = "\ [project]\n\ name = \"test\"\n\ anvil_version = \"1.0.0\"\n\ \n\ [build]\n\ fqbn = \"arduino:avr:mega:cpu=atmega2560\"\n\ warnings = \"more\"\n\ include_dirs = [\"lib/hal\", \"lib/app\"]\n\ extra_flags = [\"-Werror\"]\n\ \n\ [monitor]\n\ baud = 115200\n"; fs::write(tmp.path().join(CONFIG_FILENAME), old_config).unwrap(); let config = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(config.build.default, "mega"); assert!(config.boards.contains_key("mega")); } #[test] fn test_migrate_unknown_fqbn_uses_default_name() { let tmp = TempDir::new().unwrap(); let old_config = "\ [project]\n\ name = \"test\"\n\ anvil_version = \"1.0.0\"\n\ \n\ [build]\n\ fqbn = \"some:custom:board\"\n\ warnings = \"more\"\n\ include_dirs = [\"lib/hal\", \"lib/app\"]\n\ extra_flags = [\"-Werror\"]\n\ \n\ [monitor]\n\ baud = 115200\n"; fs::write(tmp.path().join(CONFIG_FILENAME), old_config).unwrap(); let config = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(config.build.default, "default"); assert!(config.boards.contains_key("default")); assert_eq!(config.boards["default"].fqbn, "some:custom:board"); } #[test] fn test_migrate_skips_if_already_has_default() { let tmp = TempDir::new().unwrap(); let new_config = "\ [project]\n\ name = \"test\"\n\ anvil_version = \"1.0.0\"\n\ \n\ [build]\n\ default = \"uno\"\n\ warnings = \"more\"\n\ include_dirs = [\"lib/hal\", \"lib/app\"]\n\ extra_flags = [\"-Werror\"]\n\ \n\ [monitor]\n\ baud = 115200\n\ \n\ [boards.uno]\n\ fqbn = \"arduino:avr:uno\"\n"; fs::write(tmp.path().join(CONFIG_FILENAME), new_config).unwrap(); let config = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(config.build.default, "uno"); // File should be unchanged let content = fs::read_to_string(tmp.path().join(CONFIG_FILENAME)).unwrap(); assert_eq!( content, new_config, "New-format config should not be modified" ); } #[test] fn test_migrate_does_not_duplicate_board_section() { // If [boards.uno] already exists, migration should not add a second one let tmp = TempDir::new().unwrap(); let old_config = "\ [project]\n\ name = \"test\"\n\ anvil_version = \"1.0.0\"\n\ \n\ [build]\n\ fqbn = \"arduino:avr:uno\"\n\ warnings = \"more\"\n\ include_dirs = [\"lib/hal\", \"lib/app\"]\n\ extra_flags = [\"-Werror\"]\n\ \n\ [monitor]\n\ baud = 115200\n\ \n\ [boards.uno]\n\ fqbn = \"arduino:avr:uno\"\n"; fs::write(tmp.path().join(CONFIG_FILENAME), old_config).unwrap(); ProjectConfig::load(tmp.path()).unwrap(); let content = fs::read_to_string(tmp.path().join(CONFIG_FILENAME)).unwrap(); let count = content.matches("[boards.uno]").count(); assert_eq!(count, 1, "Should not duplicate [boards.uno] section"); } #[test] fn test_migrate_default_appears_before_fqbn() { // After migration, default = "..." should come before fqbn = "..." // in [build] for natural reading order. let tmp = TempDir::new().unwrap(); let old_config = "\ [project]\n\ name = \"test\"\n\ anvil_version = \"1.0.0\"\n\ \n\ [build]\n\ fqbn = \"arduino:avr:uno\"\n\ warnings = \"more\"\n\ include_dirs = [\"lib/hal\", \"lib/app\"]\n\ extra_flags = [\"-Werror\"]\n\ \n\ [monitor]\n\ baud = 115200\n"; fs::write(tmp.path().join(CONFIG_FILENAME), old_config).unwrap(); ProjectConfig::load(tmp.path()).unwrap(); let content = fs::read_to_string(tmp.path().join(CONFIG_FILENAME)).unwrap(); let default_pos = content.find("default = ").expect("should have default"); let fqbn_pos = content.find("fqbn = ").expect("should have fqbn"); assert!( default_pos < fqbn_pos, "default should appear before fqbn in [build] section" ); } // ============================================================================ // set_default_in_file: text-based config editing // ============================================================================ #[test] fn test_set_default_replaces_existing() { let tmp = TempDir::new().unwrap(); let config = ProjectConfig::new("test"); config.save(tmp.path()).unwrap(); let config_path = tmp.path().join(CONFIG_FILENAME); let old = set_default_in_file(&config_path, "mega").unwrap(); assert_eq!(old, "uno"); let content = fs::read_to_string(&config_path).unwrap(); assert!(content.contains("default = \"mega\"")); assert!(!content.contains("default = \"uno\"")); } #[test] fn test_set_default_adds_when_missing() { let tmp = TempDir::new().unwrap(); let content = "\ [project]\n\ name = \"test\"\n\ anvil_version = \"1.0.0\"\n\ \n\ [build]\n\ warnings = \"more\"\n\ include_dirs = [\"lib/hal\", \"lib/app\"]\n\ extra_flags = [\"-Werror\"]\n\ \n\ [monitor]\n\ baud = 115200\n\ \n\ [boards.uno]\n\ fqbn = \"arduino:avr:uno\"\n"; let config_path = tmp.path().join(CONFIG_FILENAME); fs::write(&config_path, content).unwrap(); let old = set_default_in_file(&config_path, "uno").unwrap(); assert_eq!(old, "", "Should return empty string when no previous default"); let result = fs::read_to_string(&config_path).unwrap(); assert!(result.contains("default = \"uno\"")); } #[test] fn test_set_default_is_idempotent() { let tmp = TempDir::new().unwrap(); let config = ProjectConfig::new("test"); config.save(tmp.path()).unwrap(); let config_path = tmp.path().join(CONFIG_FILENAME); set_default_in_file(&config_path, "mega").unwrap(); let content_after_first = fs::read_to_string(&config_path).unwrap(); set_default_in_file(&config_path, "mega").unwrap(); let content_after_second = fs::read_to_string(&config_path).unwrap(); assert_eq!( content_after_first, content_after_second, "Second set should not change file" ); } #[test] fn test_set_default_preserves_other_fields() { let tmp = TempDir::new().unwrap(); let config = ProjectConfig::new("test"); config.save(tmp.path()).unwrap(); let config_path = tmp.path().join(CONFIG_FILENAME); set_default_in_file(&config_path, "mega").unwrap(); // Reload and check nothing else broke let loaded = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(loaded.build.default, "mega"); assert_eq!(loaded.build.warnings, "more"); assert_eq!(loaded.project.name, "test"); assert_eq!(loaded.monitor.baud, 115200); assert!(loaded.build.include_dirs.contains(&"lib/hal".to_string())); } // ========================================================================== // Board preset tests // ========================================================================== #[test] fn test_board_preset_fqbn_in_config() { // Creating a project with --board mega should set the FQBN in .anvil.toml let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "mega_test".to_string(), anvil_version: "1.0.0".to_string(), board_name: "mega".to_string(), fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(), baud: 115200, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let config = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!( config.build.default, "mega", ".anvil.toml should have default = mega" ); assert!( config.boards.contains_key("mega"), ".anvil.toml should have [boards.mega]" ); assert_eq!( config.boards["mega"].fqbn, "arduino:avr:mega:cpu=atmega2560", "[boards.mega] should have the correct FQBN" ); } #[test] fn test_board_preset_custom_fqbn_in_config() { // Even arbitrary FQBNs should work through the template let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "custom_board".to_string(), anvil_version: "1.0.0".to_string(), board_name: "esp".to_string(), fqbn: "esp32:esp32:esp32".to_string(), baud: 9600, }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let config = ProjectConfig::load(tmp.path()).unwrap(); assert_eq!(config.build.default, "esp"); assert_eq!(config.monitor.baud, 9600); } // ========================================================================== // Multi-board profile tests // ========================================================================== #[test] fn test_scripts_accept_board_flag() { // All build/upload/monitor scripts should accept --board let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "multi_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 scripts = vec![ "build.sh", "build.bat", "upload.sh", "upload.bat", "monitor.sh", "monitor.bat", ]; for script in &scripts { let content = fs::read_to_string(tmp.path().join(script)).unwrap(); assert!( content.contains("--board"), "{} should accept --board flag", script ); } } #[test] fn test_sh_scripts_have_toml_section_get() { // Shell scripts need section-aware TOML parsing for board profiles let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "section_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 &["build.sh", "upload.sh", "monitor.sh"] { let content = fs::read_to_string(tmp.path().join(script)).unwrap(); assert!( content.contains("toml_section_get"), "{} should have toml_section_get function for board profiles", script ); } } #[test] fn test_bat_scripts_have_section_parser() { // Batch scripts need section-aware TOML parsing for board profiles let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "bat_section".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 &["build.bat", "upload.bat", "monitor.bat"] { let content = fs::read_to_string(tmp.path().join(bat)).unwrap(); assert!( content.contains("BOARD_SECTION") || content.contains("IN_SECTION"), "{} should have section parser for board profiles", bat ); } } #[test] fn test_toml_template_has_board_profile_comments() { // The generated .anvil.toml should include commented examples // showing how to add board profiles let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "comment_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 content = fs::read_to_string(tmp.path().join(".anvil.toml")).unwrap(); assert!( content.contains("[boards.mega]") || content.contains("boards.mega"), ".anvil.toml should show board profile examples in comments" ); }