Files
anvil/tests/test_config.rs
2026-02-21 13:27:40 -06:00

617 lines
18 KiB
Rust

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