Add board presets, devices --clear, and test/UX fixes

Board presets:
- anvil new --board mega (uno, mega, nano, nano-old, leonardo, micro)
- anvil new --list-boards shows presets with compatible clones
- FQBN and baud rate flow into .anvil.toml via template variables
- Defaults to uno when --board is omitted

Devices --clear:
- anvil devices --clear deletes .anvil.local, reverts to auto-detect
This commit is contained in:
Eric Ratliff
2026-02-19 07:41:12 -06:00
parent d7c6d432f9
commit 2739d83b99
9 changed files with 384 additions and 12 deletions

View File

@@ -4,6 +4,8 @@ use serde::Deserialize;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
pub mod presets;
/// Information about a detected serial port. /// Information about a detected serial port.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PortInfo { pub struct PortInfo {

134
src/board/presets.rs Normal file
View File

@@ -0,0 +1,134 @@
/// Board presets for common Arduino boards.
///
/// Each preset provides a known-good FQBN and default baud rate.
/// Users can always override the FQBN in .anvil.toml after creation.
#[derive(Debug, Clone)]
pub struct BoardPreset {
/// Short name used on the command line (e.g. "uno", "mega")
pub name: &'static str,
/// Fully Qualified Board Name for arduino-cli
pub fqbn: &'static str,
/// Default serial baud rate
pub baud: u32,
/// Human-readable description
pub description: &'static str,
/// Common clones and compatible boards
pub also_known_as: &'static str,
/// Required core (for display in help text)
pub core: &'static str,
}
/// Built-in board presets.
///
/// Only includes boards whose core is installed by `anvil setup`
/// (arduino:avr). Users targeting other platforms can set the FQBN
/// manually in .anvil.toml.
pub const PRESETS: &[BoardPreset] = &[
BoardPreset {
name: "uno",
fqbn: "arduino:avr:uno",
baud: 115200,
description: "Arduino Uno (ATmega328P)",
also_known_as: "SparkFun RedBoard, Elegoo Uno R3, DFRobot DFRduino, SunFounder Uno",
core: "arduino:avr",
},
BoardPreset {
name: "mega",
fqbn: "arduino:avr:mega:cpu=atmega2560",
baud: 115200,
description: "Arduino Mega 2560",
also_known_as: "Elegoo Mega R3, Robotdyn Mega, SunFounder Mega",
core: "arduino:avr",
},
BoardPreset {
name: "nano",
fqbn: "arduino:avr:nano:cpu=atmega328",
baud: 115200,
description: "Arduino Nano (new bootloader)",
also_known_as: "Elegoo Nano, Makerfire Nano V3",
core: "arduino:avr",
},
BoardPreset {
name: "nano-old",
fqbn: "arduino:avr:nano:cpu=atmega328old",
baud: 115200,
description: "Arduino Nano (old bootloader)",
also_known_as: "pre-2018 Nano clones, most cheap Nano boards",
core: "arduino:avr",
},
BoardPreset {
name: "leonardo",
fqbn: "arduino:avr:leonardo",
baud: 115200,
description: "Arduino Leonardo (ATmega32U4)",
also_known_as: "SparkFun Pro Micro (Leonardo mode), Freetronics LeoStick",
core: "arduino:avr",
},
BoardPreset {
name: "micro",
fqbn: "arduino:avr:micro",
baud: 115200,
description: "Arduino Micro (ATmega32U4)",
also_known_as: "SparkFun Pro Micro, Adafruit ItsyBitsy 32u4",
core: "arduino:avr",
},
];
/// Default board used when --board is not specified.
pub const DEFAULT_PRESET: &str = "uno";
/// Look up a board preset by short name (case-insensitive).
pub fn find_preset(name: &str) -> Option<&'static BoardPreset> {
let lower = name.to_lowercase();
PRESETS.iter().find(|p| p.name == lower)
}
/// List all preset names for display.
pub fn preset_names() -> Vec<&'static str> {
PRESETS.iter().map(|p| p.name).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_preset_uno() {
let p = find_preset("uno").unwrap();
assert_eq!(p.fqbn, "arduino:avr:uno");
assert_eq!(p.baud, 115200);
}
#[test]
fn test_find_preset_case_insensitive() {
assert!(find_preset("Mega").is_some());
assert!(find_preset("NANO").is_some());
}
#[test]
fn test_find_preset_unknown() {
assert!(find_preset("esp32").is_none());
assert!(find_preset("stm32").is_none());
}
#[test]
fn test_default_preset_exists() {
assert!(find_preset(DEFAULT_PRESET).is_some());
}
#[test]
fn test_all_presets_have_avr_core() {
for p in PRESETS {
assert_eq!(p.core, "arduino:avr", "{} should use arduino:avr core", p.name);
}
}
#[test]
fn test_preset_names() {
let names = preset_names();
assert!(names.contains(&"uno"));
assert!(names.contains(&"mega"));
assert!(names.contains(&"nano"));
}
}

View File

@@ -274,6 +274,31 @@ pub fn set_port(port: Option<&str>, project_dir: Option<&str>) -> Result<()> {
// -- Helpers -------------------------------------------------------------- // -- Helpers --------------------------------------------------------------
/// Delete .anvil.local from the given project directory.
pub fn clear_port(project_dir: Option<&str>) -> Result<()> {
let project_path = resolve_project_dir(project_dir)?;
require_anvil_project(&project_path)?;
let local_file = project_path.join(".anvil.local");
if !local_file.exists() {
println!(
"{} No .anvil.local file found -- nothing to clear.",
"--".bright_black()
);
return Ok(());
}
fs::remove_file(&local_file)
.context("Failed to delete .anvil.local")?;
println!(
"{} Removed .anvil.local -- port will be auto-detected.",
"ok".green()
);
Ok(())
}
fn resolve_project_dir(project_dir: Option<&str>) -> Result<PathBuf> { fn resolve_project_dir(project_dir: Option<&str>) -> Result<PathBuf> {
match project_dir { match project_dir {
Some(dir) => Ok(PathBuf::from(dir)), Some(dir) => Ok(PathBuf::from(dir)),

View File

@@ -2,6 +2,7 @@ use anyhow::{Result, bail};
use colored::*; use colored::*;
use std::path::PathBuf; use std::path::PathBuf;
use crate::board::presets::{self, BoardPreset};
use crate::templates::{TemplateManager, TemplateContext}; use crate::templates::{TemplateManager, TemplateContext};
use crate::version::ANVIL_VERSION; use crate::version::ANVIL_VERSION;
@@ -28,7 +29,52 @@ pub fn list_templates() -> Result<()> {
Ok(()) Ok(())
} }
pub fn create_project(name: &str, template: Option<&str>) -> Result<()> { pub fn list_boards() -> Result<()> {
println!("{}", "Available board presets:".bright_cyan().bold());
println!();
for preset in presets::PRESETS {
let marker = if preset.name == presets::DEFAULT_PRESET {
" (default)"
} else {
""
};
println!(
" {}{}",
preset.name.bright_white().bold(),
marker.bright_cyan()
);
println!(" {}", preset.description);
if !preset.also_known_as.is_empty() {
println!(" Also: {}", preset.also_known_as.bright_black());
}
println!(" FQBN: {}", preset.fqbn.bright_black());
println!();
}
println!(
" {}",
"Usage: anvil new <project-name> --board mega".bright_yellow()
);
println!();
println!(
" {}",
"For boards not listed here, create a project and edit the".bright_black()
);
println!(
" {}",
"fqbn value in .anvil.toml to any valid arduino-cli FQBN.".bright_black()
);
println!();
Ok(())
}
pub fn create_project(
name: &str,
template: Option<&str>,
board: Option<&str>,
) -> Result<()> {
// Validate project name // Validate project name
validate_project_name(name)?; validate_project_name(name)?;
@@ -52,6 +98,25 @@ pub fn create_project(name: &str, template: Option<&str>) -> Result<()> {
bail!("Invalid template"); bail!("Invalid template");
} }
// Resolve board preset
let preset: &BoardPreset = match board {
Some(b) => {
match presets::find_preset(b) {
Some(p) => p,
None => {
println!(
"{}",
format!("Unknown board preset: '{}'", b).red().bold()
);
println!();
list_boards()?;
bail!("Invalid board preset");
}
}
}
None => presets::find_preset(presets::DEFAULT_PRESET).unwrap(),
};
println!( println!(
"{}", "{}",
format!("Creating Arduino project: {}", name) format!("Creating Arduino project: {}", name)
@@ -59,6 +124,14 @@ pub fn create_project(name: &str, template: Option<&str>) -> Result<()> {
.bold() .bold()
); );
println!("{}", format!("Template: {}", template_name).bright_cyan()); println!("{}", format!("Template: {}", template_name).bright_cyan());
println!(
"{}",
format!("Board: {} ({})", preset.name, preset.description).bright_cyan()
);
println!(
"{}",
format!("FQBN: {}", preset.fqbn).bright_black()
);
println!(); println!();
// Create project directory // Create project directory
@@ -69,6 +142,8 @@ pub fn create_project(name: &str, template: Option<&str>) -> Result<()> {
let context = TemplateContext { let context = TemplateContext {
project_name: name.to_string(), project_name: name.to_string(),
anvil_version: ANVIL_VERSION.to_string(), anvil_version: ANVIL_VERSION.to_string(),
fqbn: preset.fqbn.to_string(),
baud: preset.baud,
}; };
let file_count = TemplateManager::extract(template_name, &project_path, &context)?; let file_count = TemplateManager::extract(template_name, &project_path, &context)?;

View File

@@ -47,6 +47,8 @@ pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
let context = TemplateContext { let context = TemplateContext {
project_name: config.project.name.clone(), project_name: config.project.name.clone(),
anvil_version: ANVIL_VERSION.to_string(), anvil_version: ANVIL_VERSION.to_string(),
fqbn: config.build.fqbn.clone(),
baud: config.monitor.baud,
}; };
// Extract template into a temp directory so we can compare // Extract template into a temp directory so we can compare

View File

@@ -31,9 +31,17 @@ enum Commands {
#[arg(long, short = 't', value_name = "TEMPLATE")] #[arg(long, short = 't', value_name = "TEMPLATE")]
template: Option<String>, template: Option<String>,
/// Board preset (uno, mega, nano, leonardo, micro)
#[arg(long, short = 'b', value_name = "BOARD")]
board: Option<String>,
/// List available templates /// List available templates
#[arg(long, conflicts_with = "name")] #[arg(long, conflicts_with = "name")]
list_templates: bool, list_templates: bool,
/// List available board presets
#[arg(long, conflicts_with = "name")]
list_boards: bool,
}, },
/// Check system health and diagnose issues /// Check system health and diagnose issues
@@ -45,13 +53,17 @@ enum Commands {
/// List connected boards and serial ports /// List connected boards and serial ports
Devices { Devices {
/// Save a port to .anvil.local for this project /// Save a port to .anvil.local for this project
#[arg(long, conflicts_with = "get")] #[arg(long, conflicts_with_all = ["get", "clear"])]
set: bool, set: bool,
/// Show the saved port for this project /// Show the saved port for this project
#[arg(long, conflicts_with = "set")] #[arg(long, conflicts_with_all = ["set", "clear"])]
get: bool, get: bool,
/// Remove .anvil.local (revert to auto-detect)
#[arg(long, conflicts_with_all = ["set", "get"])]
clear: bool,
/// Port name (e.g. COM3, /dev/ttyUSB0). Auto-detects if omitted with --set. /// Port name (e.g. COM3, /dev/ttyUSB0). Auto-detects if omitted with --set.
port_or_dir: Option<String>, port_or_dir: Option<String>,
@@ -80,18 +92,23 @@ fn main() -> Result<()> {
print_banner(); print_banner();
match cli.command { match cli.command {
Commands::New { name, template, list_templates } => { Commands::New { name, template, board, list_templates, list_boards } => {
if list_templates { if list_boards {
commands::new::list_boards()
} else if list_templates {
commands::new::list_templates() commands::new::list_templates()
} else if let Some(project_name) = name { } else if let Some(project_name) = name {
commands::new::create_project( commands::new::create_project(
&project_name, &project_name,
template.as_deref(), template.as_deref(),
board.as_deref(),
) )
} else { } else {
anyhow::bail!( anyhow::bail!(
"Project name required.\n\ "Project name required.\n\
Usage: anvil new <n>\n\ Usage: anvil new <name>\n\
Usage: anvil new <name> --board mega\n\
List boards: anvil new --list-boards\n\
List templates: anvil new --list-templates" List templates: anvil new --list-templates"
); );
} }
@@ -102,7 +119,7 @@ fn main() -> Result<()> {
Commands::Setup => { Commands::Setup => {
commands::setup::run_setup() commands::setup::run_setup()
} }
Commands::Devices { set, get, port_or_dir, dir } => { Commands::Devices { set, get, clear, port_or_dir, dir } => {
if set { if set {
commands::devices::set_port( commands::devices::set_port(
port_or_dir.as_deref(), port_or_dir.as_deref(),
@@ -112,6 +129,10 @@ fn main() -> Result<()> {
commands::devices::get_port( commands::devices::get_port(
dir.as_deref().or(port_or_dir.as_deref()), dir.as_deref().or(port_or_dir.as_deref()),
) )
} else if clear {
commands::devices::clear_port(
dir.as_deref().or(port_or_dir.as_deref()),
)
} else { } else {
commands::devices::scan_devices() commands::devices::scan_devices()
} }

View File

@@ -10,6 +10,8 @@ static BASIC_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/basic")
pub struct TemplateContext { pub struct TemplateContext {
pub project_name: String, pub project_name: String,
pub anvil_version: String, pub anvil_version: String,
pub fqbn: String,
pub baud: u32,
} }
pub struct TemplateManager; pub struct TemplateManager;
@@ -140,6 +142,8 @@ fn substitute_variables(text: &str, context: &TemplateContext) -> String {
text.replace("{{PROJECT_NAME}}", &context.project_name) text.replace("{{PROJECT_NAME}}", &context.project_name)
.replace("{{ANVIL_VERSION}}", &context.anvil_version) .replace("{{ANVIL_VERSION}}", &context.anvil_version)
.replace("{{ANVIL_VERSION_CURRENT}}", ANVIL_VERSION) .replace("{{ANVIL_VERSION_CURRENT}}", ANVIL_VERSION)
.replace("{{FQBN}}", &context.fqbn)
.replace("{{BAUD}}", &context.baud.to_string())
} }
#[cfg(test)] #[cfg(test)]
@@ -172,10 +176,15 @@ mod tests {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "my_project".to_string(), project_name: "my_project".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:mega:cpu=atmega2560".to_string(),
baud: 9600,
}; };
let input = "Name: {{PROJECT_NAME}}, Version: {{ANVIL_VERSION}}"; let input = "Name: {{PROJECT_NAME}}, Version: {{ANVIL_VERSION}}, FQBN: {{FQBN}}, Baud: {{BAUD}}";
let output = substitute_variables(input, &ctx); let output = substitute_variables(input, &ctx);
assert_eq!(output, "Name: my_project, Version: 1.0.0"); assert_eq!(
output,
"Name: my_project, Version: 1.0.0, FQBN: arduino:avr:mega:cpu=atmega2560, Baud: 9600"
);
} }
#[test] #[test]
@@ -190,6 +199,8 @@ mod tests {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "test_proj".to_string(), project_name: "test_proj".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
let count = TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let count = TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -220,6 +231,8 @@ mod tests {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "my_sensor".to_string(), project_name: "my_sensor".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -231,4 +244,4 @@ mod tests {
".anvil.toml should contain project name" ".anvil.toml should contain project name"
); );
} }
} }

View File

@@ -3,10 +3,10 @@ name = "{{PROJECT_NAME}}"
anvil_version = "{{ANVIL_VERSION}}" anvil_version = "{{ANVIL_VERSION}}"
[build] [build]
fqbn = "arduino:avr:uno" fqbn = "{{FQBN}}"
warnings = "more" warnings = "more"
include_dirs = ["lib/hal", "lib/app"] include_dirs = ["lib/hal", "lib/app"]
extra_flags = ["-Werror"] extra_flags = ["-Werror"]
[monitor] [monitor]
baud = 115200 baud = {{BAUD}}

View File

@@ -15,6 +15,8 @@ fn test_basic_template_extracts_all_expected_files() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "test_proj".to_string(), project_name: "test_proj".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
let count = TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); let count = TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -27,6 +29,8 @@ fn test_template_creates_sketch_directory() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "blink".to_string(), project_name: "blink".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -55,6 +59,8 @@ fn test_template_creates_hal_files() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "sensor".to_string(), project_name: "sensor".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -86,6 +92,8 @@ fn test_template_creates_app_header() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "my_sensor".to_string(), project_name: "my_sensor".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -110,6 +118,8 @@ fn test_template_creates_test_infrastructure() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "blink".to_string(), project_name: "blink".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -146,6 +156,8 @@ fn test_template_test_file_references_correct_app() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "motor_ctrl".to_string(), project_name: "motor_ctrl".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -165,6 +177,8 @@ fn test_template_cmake_references_correct_project() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "my_bot".to_string(), project_name: "my_bot".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -184,6 +198,8 @@ fn test_template_creates_dot_files() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "blink".to_string(), project_name: "blink".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -212,6 +228,8 @@ fn test_template_creates_readme() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "blink".to_string(), project_name: "blink".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -233,6 +251,8 @@ fn test_template_creates_valid_config() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "blink".to_string(), project_name: "blink".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -312,6 +332,8 @@ fn test_full_project_structure() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "full_test".to_string(), project_name: "full_test".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -361,6 +383,8 @@ fn test_no_unicode_in_template_output() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "ascii_test".to_string(), project_name: "ascii_test".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -408,6 +432,8 @@ fn test_unknown_template_fails() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "test".to_string(), project_name: "test".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
let result = TemplateManager::extract("nonexistent", tmp.path(), &ctx); let result = TemplateManager::extract("nonexistent", tmp.path(), &ctx);
@@ -431,6 +457,8 @@ fn test_template_creates_self_contained_scripts() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "standalone".to_string(), project_name: "standalone".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -453,6 +481,8 @@ fn test_build_sh_reads_anvil_toml() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "toml_reader".to_string(), project_name: "toml_reader".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -478,6 +508,8 @@ fn test_upload_sh_reads_anvil_toml() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "uploader".to_string(), project_name: "uploader".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -511,6 +543,8 @@ fn test_monitor_sh_reads_anvil_toml() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "serial_mon".to_string(), project_name: "serial_mon".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -536,6 +570,8 @@ fn test_scripts_have_shebangs() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "shebangs".to_string(), project_name: "shebangs".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -558,6 +594,8 @@ fn test_scripts_no_anvil_binary_dependency() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "no_anvil_dep".to_string(), project_name: "no_anvil_dep".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -605,6 +643,8 @@ fn test_gitignore_excludes_build_cache() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "gitcheck".to_string(), project_name: "gitcheck".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -630,6 +670,8 @@ fn test_readme_documents_self_contained_workflow() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "docs_check".to_string(), project_name: "docs_check".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -663,6 +705,8 @@ fn test_scripts_tolerate_missing_toml_keys() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "grep_safe".to_string(), project_name: "grep_safe".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -698,6 +742,8 @@ fn test_bat_scripts_no_unescaped_parens_in_echo() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "parens_test".to_string(), project_name: "parens_test".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -757,6 +803,8 @@ fn test_scripts_read_anvil_local_for_port() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "local_test".to_string(), project_name: "local_test".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -777,6 +825,8 @@ fn test_anvil_toml_template_has_no_port() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "no_port".to_string(), project_name: "no_port".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -806,6 +856,8 @@ fn test_bat_scripts_call_detect_port_ps1() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "ps1_test".to_string(), project_name: "ps1_test".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -826,6 +878,8 @@ fn test_detect_port_ps1_is_valid() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "ps1_valid".to_string(), project_name: "ps1_valid".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -856,6 +910,8 @@ fn test_refresh_freshly_extracted_is_up_to_date() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "fresh_proj".to_string(), project_name: "fresh_proj".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -884,6 +940,8 @@ fn test_refresh_detects_modified_script() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "mod_proj".to_string(), project_name: "mod_proj".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -951,6 +1009,8 @@ fn test_scripts_read_vid_pid_from_anvil_local() {
let ctx = TemplateContext { let ctx = TemplateContext {
project_name: "vidpid_test".to_string(), project_name: "vidpid_test".to_string(),
anvil_version: "1.0.0".to_string(), anvil_version: "1.0.0".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
}; };
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
@@ -962,4 +1022,44 @@ fn test_scripts_read_vid_pid_from_anvil_local() {
script script
); );
} }
}
// ==========================================================================
// 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(),
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.fqbn, "arduino:avr:mega:cpu=atmega2560",
".anvil.toml should contain mega 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(),
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.fqbn, "esp32:esp32:esp32");
assert_eq!(config.monitor.baud, 9600);
} }