New button template
This commit is contained in:
400
tests/test_template_button.rs
Normal file
400
tests/test_template_button.rs
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user