Down to one test failing
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user