Revamped tests into separate files
This commit is contained in:
503
tests/test_templates.rs
Normal file
503
tests/test_templates.rs
Normal file
@@ -0,0 +1,503 @@
|
||||
use anvil::project::config::{
|
||||
ProjectConfig,
|
||||
};
|
||||
use anvil::templates::{TemplateManager, TemplateContext};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
// ============================================================================
|
||||
// Template extraction tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_basic_template_extracts_all_expected_files() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "test_proj".to_string(),
|
||||
anvil_version: "1.0.0".to_string(),
|
||||
board_name: "uno".to_string(),
|
||||
fqbn: "arduino:avr:uno".to_string(),
|
||||
baud: 115200,
|
||||
};
|
||||
|
||||
let count = TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||
assert!(count >= 16, "Expected at least 16 files, got {}", count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_creates_sketch_directory() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "blink".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 sketch_dir = tmp.path().join("blink");
|
||||
assert!(sketch_dir.is_dir(), "Sketch directory should exist");
|
||||
|
||||
let ino_file = sketch_dir.join("blink.ino");
|
||||
assert!(ino_file.exists(), "Sketch .ino file should exist");
|
||||
|
||||
// Verify the .ino content has correct includes
|
||||
let content = fs::read_to_string(&ino_file).unwrap();
|
||||
assert!(
|
||||
content.contains("blink_app.h"),
|
||||
".ino should include project-specific app header"
|
||||
);
|
||||
assert!(
|
||||
content.contains("hal_arduino.h"),
|
||||
".ino should include hal_arduino.h"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_creates_hal_files() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "sensor".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();
|
||||
|
||||
assert!(
|
||||
tmp.path().join("lib/hal/hal.h").exists(),
|
||||
"hal.h should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("lib/hal/hal_arduino.h").exists(),
|
||||
"hal_arduino.h should exist"
|
||||
);
|
||||
|
||||
// Verify hal.h defines the abstract Hal class
|
||||
let hal_content = fs::read_to_string(tmp.path().join("lib/hal/hal.h")).unwrap();
|
||||
assert!(
|
||||
hal_content.contains("class Hal"),
|
||||
"hal.h should define class Hal"
|
||||
);
|
||||
assert!(
|
||||
hal_content.contains("virtual void pinMode"),
|
||||
"hal.h should declare pinMode"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_creates_app_header() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "my_sensor".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 app_path = tmp.path().join("lib/app/my_sensor_app.h");
|
||||
assert!(app_path.exists(), "App header should exist with project name");
|
||||
|
||||
let content = fs::read_to_string(&app_path).unwrap();
|
||||
assert!(
|
||||
content.contains("#include <hal.h>"),
|
||||
"App header should include hal.h"
|
||||
);
|
||||
assert!(
|
||||
content.contains("class BlinkApp"),
|
||||
"App header should define BlinkApp class"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_creates_test_infrastructure() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "blink".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();
|
||||
|
||||
assert!(
|
||||
tmp.path().join("test/CMakeLists.txt").exists(),
|
||||
"CMakeLists.txt should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("test/test_unit.cpp").exists(),
|
||||
"test_unit.cpp should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("test/mocks/mock_hal.h").exists(),
|
||||
"mock_hal.h should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("test/mocks/sim_hal.h").exists(),
|
||||
"sim_hal.h should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("test/mocks/mock_arduino.h").exists(),
|
||||
"mock_arduino.h should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("test/mocks/mock_arduino.cpp").exists(),
|
||||
"mock_arduino.cpp should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("test/mocks/Arduino.h").exists(),
|
||||
"Arduino.h shim should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("test/mocks/Wire.h").exists(),
|
||||
"Wire.h shim should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("test/mocks/SPI.h").exists(),
|
||||
"SPI.h shim should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("test/test_system.cpp").exists(),
|
||||
"test_system.cpp should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("test.sh").exists(),
|
||||
"test.sh root script should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("test.bat").exists(),
|
||||
"test.bat root script should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("test/run_tests.sh").exists(),
|
||||
"run_tests.sh should exist"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join("test/run_tests.bat").exists(),
|
||||
"run_tests.bat should exist"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_test_file_references_correct_app() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "motor_ctrl".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 test_content = fs::read_to_string(
|
||||
tmp.path().join("test/test_unit.cpp")
|
||||
).unwrap();
|
||||
assert!(
|
||||
test_content.contains("motor_ctrl_app.h"),
|
||||
"Test file should include project-specific app header"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_cmake_references_correct_project() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "my_bot".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 cmake = fs::read_to_string(
|
||||
tmp.path().join("test/CMakeLists.txt")
|
||||
).unwrap();
|
||||
assert!(
|
||||
cmake.contains("my_bot"),
|
||||
"CMakeLists.txt should contain project name"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_creates_dot_files() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "blink".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();
|
||||
|
||||
assert!(
|
||||
tmp.path().join(".gitignore").exists(),
|
||||
".gitignore should be created from _dot_ prefix"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join(".editorconfig").exists(),
|
||||
".editorconfig should be created"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join(".clang-format").exists(),
|
||||
".clang-format should be created"
|
||||
);
|
||||
assert!(
|
||||
tmp.path().join(".vscode/settings.json").exists(),
|
||||
".vscode/settings.json should be created"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_creates_readme() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "blink".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 readme = tmp.path().join("README.md");
|
||||
assert!(readme.exists(), "README.md should exist");
|
||||
|
||||
let content = fs::read_to_string(&readme).unwrap();
|
||||
assert!(content.contains("blink"), "README should contain project name");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// .anvil.toml config tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_template_creates_valid_config() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "blink".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();
|
||||
|
||||
// Should be loadable by ProjectConfig
|
||||
let config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.project.name, "blink");
|
||||
assert_eq!(config.build.default, "uno");
|
||||
assert_eq!(config.monitor.baud, 115200);
|
||||
assert!(config.build.extra_flags.contains(&"-Werror".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_roundtrip() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config = ProjectConfig::new("roundtrip_test");
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
let loaded = ProjectConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(loaded.project.name, "roundtrip_test");
|
||||
assert_eq!(loaded.build.default, config.build.default);
|
||||
assert_eq!(loaded.monitor.baud, config.monitor.baud);
|
||||
assert_eq!(loaded.build.include_dirs, config.build.include_dirs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_find_project_root_walks_up() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config = ProjectConfig::new("walk_test");
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
// Create nested subdirectory
|
||||
let deep = tmp.path().join("sketch").join("src").join("deep");
|
||||
fs::create_dir_all(&deep).unwrap();
|
||||
|
||||
let found = ProjectConfig::find_project_root(&deep).unwrap();
|
||||
let expected = tmp.path().canonicalize().unwrap();
|
||||
|
||||
// On Windows, canonicalize() returns \\?\ extended path prefix.
|
||||
// Strip it for comparison since find_project_root may not include it.
|
||||
let found_str = found.to_string_lossy().to_string();
|
||||
let expected_str = expected.to_string_lossy().to_string();
|
||||
let norm = |s: &str| s.strip_prefix(r"\\?\").unwrap_or(s).to_string();
|
||||
assert_eq!(norm(&found_str), norm(&expected_str));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_resolve_include_flags() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
fs::create_dir_all(tmp.path().join("lib/hal")).unwrap();
|
||||
fs::create_dir_all(tmp.path().join("lib/app")).unwrap();
|
||||
|
||||
let config = ProjectConfig::new("flags_test");
|
||||
let flags = config.resolve_include_flags(tmp.path());
|
||||
|
||||
assert_eq!(flags.len(), 2);
|
||||
assert!(flags[0].starts_with("-I"));
|
||||
assert!(flags[0].ends_with("lib/hal") || flags[0].ends_with("lib\\hal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_skips_nonexistent_include_dirs() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
// Don't create the directories
|
||||
let config = ProjectConfig::new("missing_dirs");
|
||||
let flags = config.resolve_include_flags(tmp.path());
|
||||
assert_eq!(flags.len(), 0, "Should skip non-existent directories");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Full project creation test (end-to-end in temp dir)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_full_project_structure() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "full_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();
|
||||
|
||||
// Verify the complete expected file tree
|
||||
let expected_files = vec![
|
||||
".anvil.toml",
|
||||
".gitignore",
|
||||
".editorconfig",
|
||||
".clang-format",
|
||||
".vscode/settings.json",
|
||||
"README.md",
|
||||
"full_test/full_test.ino",
|
||||
"lib/hal/hal.h",
|
||||
"lib/hal/hal_arduino.h",
|
||||
"lib/app/full_test_app.h",
|
||||
"build.sh",
|
||||
"build.bat",
|
||||
"upload.sh",
|
||||
"upload.bat",
|
||||
"monitor.sh",
|
||||
"monitor.bat",
|
||||
"_detect_port.ps1",
|
||||
"_monitor_filter.ps1",
|
||||
"test/CMakeLists.txt",
|
||||
"test/test_unit.cpp",
|
||||
"test/test_system.cpp",
|
||||
"test/run_tests.sh",
|
||||
"test/run_tests.bat",
|
||||
"test/mocks/mock_hal.h",
|
||||
"test/mocks/sim_hal.h",
|
||||
"test/mocks/mock_arduino.h",
|
||||
"test/mocks/mock_arduino.cpp",
|
||||
"test/mocks/Arduino.h",
|
||||
"test/mocks/Wire.h",
|
||||
"test/mocks/SPI.h",
|
||||
"test.sh",
|
||||
"test.bat",
|
||||
];
|
||||
|
||||
for f in &expected_files {
|
||||
let p = tmp.path().join(f);
|
||||
assert!(
|
||||
p.exists(),
|
||||
"Expected file missing: {} (checked {})",
|
||||
f,
|
||||
p.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_unicode_in_template_output() {
|
||||
// Eric's rule: only ASCII characters
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "ascii_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();
|
||||
|
||||
// Check all generated text files for non-ASCII
|
||||
check_ascii_recursive(tmp.path());
|
||||
}
|
||||
|
||||
fn check_ascii_recursive(dir: &Path) {
|
||||
for entry in fs::read_dir(dir).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
check_ascii_recursive(&path);
|
||||
} else {
|
||||
// Only check text files
|
||||
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
if matches!(ext, "h" | "cpp" | "ino" | "txt" | "sh" | "bat" | "json" | "toml" | "md") {
|
||||
let content = fs::read_to_string(&path).unwrap();
|
||||
for (line_num, line) in content.lines().enumerate() {
|
||||
for (col, ch) in line.chars().enumerate() {
|
||||
assert!(
|
||||
ch.is_ascii(),
|
||||
"Non-ASCII character '{}' (U+{:04X}) at {}:{}:{} ",
|
||||
ch,
|
||||
ch as u32,
|
||||
path.display(),
|
||||
line_num + 1,
|
||||
col + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Error case tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_unknown_template_fails() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "test".to_string(),
|
||||
anvil_version: "1.0.0".to_string(),
|
||||
board_name: "uno".to_string(),
|
||||
fqbn: "arduino:avr:uno".to_string(),
|
||||
baud: 115200,
|
||||
};
|
||||
|
||||
let result = TemplateManager::extract("nonexistent", tmp.path(), &ctx);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_config_from_nonproject_fails() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let result = ProjectConfig::load(tmp.path());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user