New button template
Some checks failed
CI / Test (Linux) (push) Has been cancelled
CI / Test (Windows MSVC) (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled

This commit is contained in:
Eric Ratliff
2026-02-22 17:06:02 -06:00
parent 578b5f02c0
commit e12608370a
9 changed files with 898 additions and 5 deletions

View File

@@ -0,0 +1,400 @@
use anvil::commands;
use anvil::library;
use anvil::project::config::ProjectConfig;
use anvil::templates::{TemplateManager, TemplateContext};
use std::fs;
use tempfile::TempDir;
// ==========================================================================
// Template listing and metadata
// ==========================================================================
#[test]
fn test_list_templates_includes_button() {
let templates = TemplateManager::list_templates();
assert!(
templates.iter().any(|t| t.name == "button"),
"Should list button template"
);
}
#[test]
fn test_button_template_exists() {
assert!(TemplateManager::template_exists("button"));
}
#[test]
fn test_button_is_not_default() {
let templates = TemplateManager::list_templates();
let btn = templates.iter().find(|t| t.name == "button").unwrap();
assert!(!btn.is_default);
}
#[test]
fn test_button_lists_button_library() {
let templates = TemplateManager::list_templates();
let btn = templates.iter().find(|t| t.name == "button").unwrap();
assert!(btn.libraries.contains(&"button".to_string()));
}
#[test]
fn test_button_lists_digital_capability() {
let templates = TemplateManager::list_templates();
let btn = templates.iter().find(|t| t.name == "button").unwrap();
assert!(btn.board_capabilities.contains(&"digital".to_string()));
}
// ==========================================================================
// Composed metadata
// ==========================================================================
#[test]
fn test_button_composed_meta_exists() {
let meta = TemplateManager::composed_meta("button");
assert!(meta.is_some(), "Button should have composed metadata");
}
#[test]
fn test_button_composed_meta_base_is_basic() {
let meta = TemplateManager::composed_meta("button").unwrap();
assert_eq!(meta.base, "basic");
}
#[test]
fn test_button_composed_meta_requires_button_lib() {
let meta = TemplateManager::composed_meta("button").unwrap();
assert!(meta.libraries.contains(&"button".to_string()));
}
#[test]
fn test_button_composed_meta_has_pin_defaults() {
let meta = TemplateManager::composed_meta("button").unwrap();
assert!(
!meta.pins.is_empty(),
"Should have pin defaults"
);
}
#[test]
fn test_button_pins_for_uno() {
let meta = TemplateManager::composed_meta("button").unwrap();
let pins = meta.pins_for_board("uno");
assert_eq!(pins.len(), 1);
assert_eq!(pins[0].name, "button_signal");
assert_eq!(pins[0].pin, "2");
assert_eq!(pins[0].mode, "input");
}
#[test]
fn test_button_pins_fallback_to_default() {
let meta = TemplateManager::composed_meta("button").unwrap();
// "micro" is not explicitly listed, should fall back to "default"
let pins = meta.pins_for_board("micro");
assert!(!pins.is_empty());
assert!(pins.iter().any(|p| p.name == "button_signal"));
}
// ==========================================================================
// Template extraction
// ==========================================================================
fn extract_button(name: &str) -> TempDir {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: name.to_string(),
anvil_version: "1.0.0".to_string(),
board_name: "uno".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
};
TemplateManager::extract("button", tmp.path(), &ctx).unwrap();
// Record template name in config, same as create_project does
let mut config = ProjectConfig::load(tmp.path()).unwrap();
config.project.template = "button".to_string();
config.save(tmp.path()).unwrap();
tmp
}
#[test]
fn test_button_has_basic_scaffold() {
let tmp = extract_button("btn");
// Basic scaffold from base template
assert!(tmp.path().join("lib/hal/hal.h").exists());
assert!(tmp.path().join("lib/hal/hal_arduino.h").exists());
assert!(tmp.path().join("test/mocks/mock_hal.h").exists());
assert!(tmp.path().join("test/mocks/sim_hal.h").exists());
assert!(tmp.path().join("test/CMakeLists.txt").exists());
}
#[test]
fn test_button_has_button_app() {
let tmp = extract_button("btn");
let app = tmp.path().join("lib/app/btn_app.h");
assert!(app.exists(), "Should have button app header");
let content = fs::read_to_string(&app).unwrap();
assert!(content.contains("class ButtonApp"), "Should define ButtonApp");
assert!(content.contains("isPressed"), "Should use button interface");
assert!(content.contains("was_pressed_"), "Should track previous state");
assert!(content.contains("press_count_"), "Should count presses");
assert!(content.contains("serialPrintln"), "Should print to serial");
}
#[test]
fn test_button_app_replaces_basic_blink() {
let tmp = extract_button("btn");
let app = tmp.path().join("lib/app/btn_app.h");
let content = fs::read_to_string(&app).unwrap();
// Should NOT contain basic template's BlinkApp
assert!(!content.contains("BlinkApp"), "Should not have BlinkApp");
assert!(content.contains("ButtonApp"), "Should have ButtonApp");
}
#[test]
fn test_button_has_button_sketch() {
let tmp = extract_button("btn");
let sketch = tmp.path().join("btn/btn.ino");
assert!(sketch.exists(), "Sketch should exist");
let content = fs::read_to_string(&sketch).unwrap();
assert!(content.contains("button_digital.h"), "Should include button driver");
assert!(content.contains("ButtonDigital"), "Should create ButtonDigital");
assert!(content.contains("ButtonApp"), "Should create ButtonApp");
}
#[test]
fn test_button_sketch_replaces_basic_sketch() {
let tmp = extract_button("btn");
let sketch = tmp.path().join("btn/btn.ino");
let content = fs::read_to_string(&sketch).unwrap();
assert!(!content.contains("BlinkApp"), "Should not reference BlinkApp");
assert!(content.contains("ButtonApp"), "Should reference ButtonApp");
}
#[test]
fn test_button_has_managed_example_tests() {
let tmp = extract_button("btn");
let test_file = tmp.path().join("test/test_button_app.cpp");
assert!(test_file.exists(), "Should have managed test file");
let content = fs::read_to_string(&test_file).unwrap();
assert!(content.contains("MANAGED BY ANVIL"), "Should be marked as managed");
assert!(content.contains("ButtonUnitTest"), "Should have unit test fixture");
assert!(content.contains("ButtonSystemTest"), "Should have system test fixture");
}
#[test]
fn test_button_has_student_unit_starter() {
let tmp = extract_button("btn");
let test_file = tmp.path().join("test/test_unit.cpp");
assert!(test_file.exists());
let content = fs::read_to_string(&test_file).unwrap();
assert!(content.contains("YOURS"), "Should be marked as student-owned");
assert!(content.contains("button_mock.h"), "Should include button mock");
assert!(content.contains("btn_app.h"), "Should include project app");
}
#[test]
fn test_button_has_student_system_starter() {
let tmp = extract_button("btn");
let test_file = tmp.path().join("test/test_system.cpp");
assert!(test_file.exists());
let content = fs::read_to_string(&test_file).unwrap();
assert!(content.contains("YOURS"), "Should be marked as student-owned");
assert!(content.contains("button_sim.h"), "Should include button sim");
}
#[test]
fn test_button_no_template_toml_in_output() {
let tmp = extract_button("btn");
assert!(
!tmp.path().join("template.toml").exists(),
"template.toml should not be extracted to output"
);
}
#[test]
fn test_button_preserves_cmake() {
let tmp = extract_button("btn");
let cmake = tmp.path().join("test/CMakeLists.txt");
assert!(cmake.exists(), "CMakeLists.txt should survive overlay");
}
#[test]
fn test_button_variable_substitution() {
let tmp = extract_button("mybutton");
// Sketch should use project name
let sketch = tmp.path().join("mybutton/mybutton.ino");
let content = fs::read_to_string(&sketch).unwrap();
assert!(content.contains("mybutton_app.h"), "Should substitute project name");
assert!(!content.contains("{{PROJECT_NAME}}"), "No unresolved placeholders");
// App header should use project name
let app = tmp.path().join("lib/app/mybutton_app.h");
assert!(app.exists(), "App header should use project name");
// Test files should use project name
let test_file = tmp.path().join("test/test_button_app.cpp");
let test_content = fs::read_to_string(&test_file).unwrap();
assert!(test_content.contains("mybutton_app.h"), "Test should include project app");
assert!(!test_content.contains("{{PROJECT_NAME}}"), "No unresolved placeholders");
}
#[test]
fn test_button_all_files_ascii() {
let tmp = extract_button("btn");
for entry in walkdir(tmp.path()) {
let content = fs::read_to_string(&entry).unwrap_or_default();
for (line_num, line) in content.lines().enumerate() {
for (col, ch) in line.chars().enumerate() {
assert!(
ch.is_ascii(),
"Non-ASCII in {} at {}:{}: U+{:04X}",
entry.display(),
line_num + 1, col + 1, ch as u32
);
}
}
}
}
// ==========================================================================
// C++ API compatibility: test file references valid sensor methods
// ==========================================================================
#[test]
fn test_template_button_tests_use_valid_button_api() {
let tmp = extract_button("btn");
// Extract the button library so we can check method names
library::extract_library("button", tmp.path()).unwrap();
// Read mock and sim headers to build the set of valid methods
let mock_h = fs::read_to_string(
tmp.path().join("lib/drivers/button/button_mock.h")
).unwrap();
let sim_h = fs::read_to_string(
tmp.path().join("lib/drivers/button/button_sim.h")
).unwrap();
let test_file = fs::read_to_string(
tmp.path().join("test/test_button_app.cpp")
).unwrap();
// Check that test calls methods that exist in mock or sim
let expected_methods = ["isPressed", "setPressed", "press", "release", "setSeed", "setBounceReads"];
for method in &expected_methods {
let in_mock = mock_h.contains(method);
let in_sim = sim_h.contains(method);
let in_test = test_file.contains(method);
if in_test {
assert!(
in_mock || in_sim,
"Test uses {}() but it is not in mock or sim headers",
method
);
}
}
}
// ==========================================================================
// Full flow: new project with button template
// ==========================================================================
#[test]
fn test_button_full_flow() {
let tmp = extract_button("btn_flow");
// Install the button library (as anvil new --template button would)
let meta = library::find_library("button").unwrap();
library::extract_library("button", tmp.path()).unwrap();
let mut config = ProjectConfig::load(tmp.path()).unwrap();
config.libraries.insert("button".to_string(), meta.version.clone());
let driver_include = format!("lib/drivers/{}", meta.name);
if !config.build.include_dirs.contains(&driver_include) {
config.build.include_dirs.push(driver_include);
}
config.save(tmp.path()).unwrap();
// Assign pin
let dir_str = tmp.path().to_string_lossy().to_string();
commands::pin::assign_pin(
"button_signal", "2",
Some("input"),
None,
Some(&dir_str),
).unwrap();
// Verify everything is in place
let config_after = ProjectConfig::load(tmp.path()).unwrap();
assert!(config_after.libraries.contains_key("button"));
let board_pins = config_after.pins.get("uno").unwrap();
assert!(board_pins.assignments.contains_key("button_signal"));
// Files exist
assert!(tmp.path().join("lib/drivers/button/button.h").exists());
assert!(tmp.path().join("lib/drivers/button/button_digital.h").exists());
assert!(tmp.path().join("lib/drivers/button/button_mock.h").exists());
assert!(tmp.path().join("lib/drivers/button/button_sim.h").exists());
assert!(tmp.path().join("test/test_button.cpp").exists());
assert!(tmp.path().join("test/test_button_app.cpp").exists());
assert!(tmp.path().join("lib/app/btn_flow_app.h").exists());
}
#[test]
fn test_button_config_records_template_name() {
let tmp = extract_button("tmpl_name");
let config = ProjectConfig::load(tmp.path()).unwrap();
assert_eq!(config.project.template.as_str(), "button");
}
#[test]
fn test_button_audit_clean_after_full_setup() {
let tmp = extract_button("audit_btn");
let meta = library::find_library("button").unwrap();
library::extract_library("button", tmp.path()).unwrap();
let mut config = ProjectConfig::load(tmp.path()).unwrap();
config.libraries.insert("button".to_string(), meta.version.clone());
config.save(tmp.path()).unwrap();
let dir_str = tmp.path().to_string_lossy().to_string();
commands::pin::assign_pin(
"button_signal", "2",
Some("input"),
None,
Some(&dir_str),
).unwrap();
// Audit should pass cleanly
commands::pin::audit_pins(None, false, Some(&dir_str)).unwrap();
}
// ==========================================================================
// Helper: walk all files in a directory tree
// ==========================================================================
fn walkdir(root: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut files = Vec::new();
fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
walk(&path, out);
} else if path.is_file() {
out.push(path);
}
}
}
}
walk(root, &mut files);
files
}