Down to one test failing

This commit is contained in:
Eric Ratliff
2026-02-21 21:33:27 -06:00
parent 8a72098443
commit a9f729c7de
11 changed files with 3590 additions and 336 deletions

View File

@@ -1,4 +1,5 @@
use anvil::commands;
use anvil::ignore::{self, AnvilIgnore};
use anvil::library;
use anvil::project::config::ProjectConfig;
use anvil::templates::{TemplateManager, TemplateContext};
@@ -174,25 +175,38 @@ fn test_weather_sketch_replaces_basic_sketch() {
}
#[test]
fn test_weather_has_unit_tests() {
fn test_weather_has_managed_example_tests() {
let tmp = extract_weather("wx");
let test_path = tmp.path().join("test").join("test_weather.cpp");
assert!(test_path.exists(), "Managed test_weather.cpp should exist");
let content = fs::read_to_string(&test_path).unwrap();
assert!(content.contains("Tmp36Mock"));
assert!(content.contains("WeatherUnitTest"));
assert!(content.contains("Tmp36Sim"));
assert!(content.contains("WeatherSystemTest"));
assert!(content.contains("wx_app.h"));
assert!(content.contains("MANAGED BY ANVIL"));
}
#[test]
fn test_weather_has_student_unit_starter() {
let tmp = extract_weather("wx");
let test_path = tmp.path().join("test").join("test_unit.cpp");
assert!(test_path.exists());
let content = fs::read_to_string(&test_path).unwrap();
assert!(content.contains("Tmp36Mock"));
assert!(content.contains("WeatherUnitTest"));
assert!(content.contains("wx_app.h"));
// Should be a minimal starter, not the full weather tests
assert!(content.contains("Your unit tests go here"));
assert!(!content.contains("WeatherUnitTest"));
}
#[test]
fn test_weather_has_system_tests() {
fn test_weather_has_student_system_starter() {
let tmp = extract_weather("wx");
let test_path = tmp.path().join("test").join("test_system.cpp");
assert!(test_path.exists());
let content = fs::read_to_string(&test_path).unwrap();
assert!(content.contains("Tmp36Sim"));
assert!(content.contains("WeatherSystemTest"));
assert!(content.contains("SimHal"));
assert!(content.contains("Your system tests go here"));
assert!(!content.contains("WeatherSystemTest"));
}
#[test]
@@ -425,11 +439,12 @@ fn test_weather_full_flow() {
assert!(!app_content.contains("BlinkApp"));
// Test files present (weather-specific)
let unit_content = fs::read_to_string(
tmp.path().join("test").join("test_unit.cpp"),
let weather_content = fs::read_to_string(
tmp.path().join("test").join("test_weather.cpp"),
)
.unwrap();
assert!(unit_content.contains("Tmp36Mock"));
assert!(weather_content.contains("Tmp36Mock"));
assert!(weather_content.contains("Tmp36Sim"));
// Sketch wires everything together
let ino_content = fs::read_to_string(
@@ -596,7 +611,7 @@ fn test_template_system_tests_use_valid_sim_api() {
.unwrap();
let test_source = fs::read_to_string(
tmp.path().join("test").join("test_system.cpp"),
tmp.path().join("test").join("test_weather.cpp"),
)
.unwrap();
@@ -624,7 +639,7 @@ fn test_template_system_tests_use_valid_sim_api() {
if before_dot.contains("sensor") || before_dot.contains("sim") {
assert!(
methods.contains(&method_name.to_string()),
"test_system.cpp calls '{}.{}()' but '{}' \
"test_weather.cpp calls '{}.{}()' but '{}' \
is not in tmp36_sim.h.\n \
Available methods: {:?}",
before_dot,
@@ -653,7 +668,7 @@ fn test_template_unit_tests_use_valid_mock_api() {
.unwrap();
let test_source = fs::read_to_string(
tmp.path().join("test").join("test_unit.cpp"),
tmp.path().join("test").join("test_weather.cpp"),
)
.unwrap();
@@ -676,7 +691,7 @@ fn test_template_unit_tests_use_valid_mock_api() {
if before_dot.contains("sensor") {
assert!(
methods.contains(&method_name.to_string()),
"test_unit.cpp calls '{}.{}()' but '{}' \
"test_weather.cpp calls '{}.{}()' but '{}' \
is not in tmp36_mock.h.\n \
Available methods: {:?}",
before_dot,
@@ -708,7 +723,7 @@ fn test_template_sim_constructor_arg_count() {
count_constructor_params(&sim_header, "Tmp36Sim");
let test_source = fs::read_to_string(
tmp.path().join("test").join("test_system.cpp"),
tmp.path().join("test").join("test_weather.cpp"),
)
.unwrap();
@@ -735,7 +750,7 @@ fn test_template_sim_constructor_arg_count() {
};
assert!(
arg_count >= min_args && arg_count <= total_params,
"test_system.cpp line {}: Tmp36Sim() called with \
"test_weather.cpp line {}: Tmp36Sim() called with \
{} args, but constructor accepts {}-{} args.\n \
Line: {}",
line_num + 1,
@@ -747,4 +762,370 @@ fn test_template_sim_constructor_arg_count() {
}
}
}
}
// =========================================================================
// .anvilignore -- generated defaults and behavior
// =========================================================================
/// Helper: full weather project setup (extract + set template + libs + pins + .anvilignore)
fn setup_weather_project(name: &str) -> TempDir {
let tmp = extract_weather(name);
// Set template field in config (as new.rs does)
let mut config = ProjectConfig::load(tmp.path()).unwrap();
config.project.template = "weather".to_string();
config.save(tmp.path()).unwrap();
// Generate .anvilignore
ignore::generate_default(tmp.path(), "weather").unwrap();
// Install library and assign pins
let meta = TemplateManager::composed_meta("weather").unwrap();
for lib_name in &meta.libraries {
commands::lib::install_library(lib_name, tmp.path()).unwrap();
}
for pin_def in meta.pins_for_board("uno") {
commands::pin::install_pin_assignment(
&pin_def.name,
&pin_def.pin,
&pin_def.mode,
"uno",
tmp.path(),
)
.unwrap();
}
tmp
}
#[test]
fn test_anvilignore_generated_on_project_creation() {
let tmp = setup_weather_project("wx");
assert!(
tmp.path().join(".anvilignore").exists(),
".anvilignore should be created"
);
}
#[test]
fn test_anvilignore_protects_student_test_files() {
let tmp = setup_weather_project("wx");
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
assert!(
ignore.is_ignored("test/test_unit.cpp"),
"test_unit.cpp should be protected"
);
assert!(
ignore.is_ignored("test/test_system.cpp"),
"test_system.cpp should be protected"
);
}
#[test]
fn test_anvilignore_protects_app_code() {
let tmp = setup_weather_project("wx");
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
assert!(
ignore.is_ignored("lib/app/wx_app.h"),
"App code should be protected by lib/app/* pattern"
);
}
#[test]
fn test_anvilignore_protects_config() {
let tmp = setup_weather_project("wx");
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
assert!(
ignore.is_ignored(".anvil.toml"),
".anvil.toml should be protected"
);
}
#[test]
fn test_anvilignore_does_not_protect_managed_scripts() {
let tmp = setup_weather_project("wx");
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
assert!(
!ignore.is_ignored("build.sh"),
"build.sh should NOT be protected"
);
assert!(
!ignore.is_ignored("test/run_tests.sh"),
"run_tests.sh should NOT be protected"
);
assert!(
!ignore.is_ignored("test/mocks/mock_hal.h"),
"mock_hal.h should NOT be protected"
);
}
#[test]
fn test_anvilignore_does_not_protect_managed_template_test() {
let tmp = setup_weather_project("wx");
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
assert!(
!ignore.is_ignored("test/test_weather.cpp"),
"test_weather.cpp is managed by Anvil, should NOT be protected"
);
}
#[test]
fn test_anvilignore_does_not_protect_library_headers() {
let tmp = setup_weather_project("wx");
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
assert!(
!ignore.is_ignored("lib/drivers/tmp36/tmp36.h"),
"Driver headers are managed, should NOT be protected"
);
}
// =========================================================================
// .anvilignore -- add/remove patterns
// =========================================================================
#[test]
fn test_add_ignore_pattern() {
let tmp = setup_weather_project("wx");
ignore::add_pattern(tmp.path(), "test/test_custom.cpp").unwrap();
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
assert!(ignore.is_ignored("test/test_custom.cpp"));
}
#[test]
fn test_add_ignore_pattern_no_duplicate() {
let tmp = setup_weather_project("wx");
ignore::add_pattern(tmp.path(), "test/test_unit.cpp").unwrap();
let content =
fs::read_to_string(tmp.path().join(".anvilignore")).unwrap();
// test_unit.cpp appears in the default, adding it again should not duplicate
let count = content
.lines()
.filter(|l| l.trim() == "test/test_unit.cpp")
.count();
assert_eq!(count, 1);
}
#[test]
fn test_add_ignore_wildcard_pattern() {
let tmp = setup_weather_project("wx");
ignore::add_pattern(tmp.path(), "test/my_*.cpp").unwrap();
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
assert!(ignore.is_ignored("test/my_custom_test.cpp"));
assert!(ignore.is_ignored("test/my_helper.cpp"));
assert!(!ignore.is_ignored("test/test_weather.cpp"));
}
#[test]
fn test_remove_ignore_pattern() {
let tmp = setup_weather_project("wx");
// test_unit.cpp is in defaults
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
assert!(ignore.is_ignored("test/test_unit.cpp"));
// Remove it
let removed =
ignore::remove_pattern(tmp.path(), "test/test_unit.cpp").unwrap();
assert!(removed);
// Verify it's gone
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
assert!(!ignore.is_ignored("test/test_unit.cpp"));
}
#[test]
fn test_remove_nonexistent_pattern_returns_false() {
let tmp = setup_weather_project("wx");
let removed =
ignore::remove_pattern(tmp.path(), "nonexistent.cpp").unwrap();
assert!(!removed);
}
// =========================================================================
// .anvilignore -- matching_pattern reports which rule matched
// =========================================================================
#[test]
fn test_matching_pattern_reports_exact() {
let tmp = setup_weather_project("wx");
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
assert_eq!(
ignore.matching_pattern("test/test_unit.cpp"),
Some("test/test_unit.cpp")
);
}
#[test]
fn test_matching_pattern_reports_glob() {
let tmp = setup_weather_project("wx");
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
// lib/app/* should match lib/app/wx_app.h
let pattern = ignore.matching_pattern("lib/app/wx_app.h");
assert_eq!(pattern, Some("lib/app/*"));
}
#[test]
fn test_matching_pattern_returns_none_for_unignored() {
let tmp = setup_weather_project("wx");
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
assert!(ignore.matching_pattern("build.sh").is_none());
}
// =========================================================================
// Config tracks template name
// =========================================================================
#[test]
fn test_config_records_template_name() {
let tmp = setup_weather_project("wx");
let config = ProjectConfig::load(tmp.path()).unwrap();
assert_eq!(config.project.template, "weather");
}
#[test]
fn test_config_default_template_is_basic() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "basic_proj".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 config = ProjectConfig::load(tmp.path()).unwrap();
assert_eq!(config.project.template, "basic");
}
// =========================================================================
// Refresh respects .anvilignore
// =========================================================================
#[test]
fn test_refresh_does_not_overwrite_ignored_files() {
let tmp = setup_weather_project("wx");
// Modify the student's test_unit.cpp (which is ignored)
let test_unit = tmp.path().join("test").join("test_unit.cpp");
fs::write(&test_unit, "// my custom test code\n").unwrap();
// Run refresh --force
commands::refresh::run_refresh(
Some(tmp.path().to_str().unwrap()),
true,
None,
)
.unwrap();
// Student's file should be untouched
let content = fs::read_to_string(&test_unit).unwrap();
assert_eq!(content, "// my custom test code\n");
}
#[test]
fn test_refresh_updates_managed_template_test() {
let tmp = setup_weather_project("wx");
// Tamper with managed test_weather.cpp
let test_weather = tmp.path().join("test").join("test_weather.cpp");
let original = fs::read_to_string(&test_weather).unwrap();
fs::write(&test_weather, "// tampered\n").unwrap();
// Run refresh --force
commands::refresh::run_refresh(
Some(tmp.path().to_str().unwrap()),
true,
None,
)
.unwrap();
// Managed file should be restored
let content = fs::read_to_string(&test_weather).unwrap();
assert_ne!(content, "// tampered\n");
assert!(content.contains("WeatherUnitTest"));
}
#[test]
fn test_refresh_force_file_overrides_ignore() {
let tmp = setup_weather_project("wx");
// Modify ignored test_unit.cpp
let test_unit = tmp.path().join("test").join("test_unit.cpp");
let original = fs::read_to_string(&test_unit).unwrap();
fs::write(&test_unit, "// i want this overwritten\n").unwrap();
// Run refresh --force --file test/test_unit.cpp
commands::refresh::run_refresh(
Some(tmp.path().to_str().unwrap()),
true,
Some("test/test_unit.cpp"),
)
.unwrap();
// File should be restored to template version
let content = fs::read_to_string(&test_unit).unwrap();
assert!(
content.contains("Your unit tests go here"),
"Should be restored to template starter"
);
}
#[test]
fn test_refresh_updates_library_driver_headers() {
let tmp = setup_weather_project("wx");
// Tamper with a driver header (managed)
let header = tmp
.path()
.join("lib")
.join("drivers")
.join("tmp36")
.join("tmp36.h");
fs::write(&header, "// tampered\n").unwrap();
// Run refresh --force
commands::refresh::run_refresh(
Some(tmp.path().to_str().unwrap()),
true,
None,
)
.unwrap();
// Header should be restored
let content = fs::read_to_string(&header).unwrap();
assert!(content.contains("TempSensor"));
}
#[test]
fn test_refresh_freshly_created_project_is_up_to_date() {
let tmp = setup_weather_project("wx");
// Refresh without --force should find nothing to do
// (just verifying it doesn't error)
commands::refresh::run_refresh(
Some(tmp.path().to_str().unwrap()),
false,
None,
)
.unwrap();
}
#[test]
fn test_anvilignore_all_files_ascii() {
let tmp = setup_weather_project("wx");
let content =
fs::read_to_string(tmp.path().join(".anvilignore")).unwrap();
for (i, byte) in content.bytes().enumerate() {
assert!(
byte < 128,
"Non-ASCII byte {} at offset {} in .anvilignore",
byte,
i
);
}
}