Templates are now composed declaratively via template.toml -- no Rust code changes needed to add new templates. The weather station is the first composed template, demonstrating the full pattern. Template engine: - Composed templates declare base, required libraries, and per-board pins - Overlay mechanism replaces base files (app, sketch, tests) cleanly - Generic orchestration: extract base, apply overlay, install libs, assign pins - Template name tracked in .anvil.toml for refresh awareness Weather template (--template weather): - WeatherApp with 2-second polling, C/F conversion, serial output - TMP36 driver: TempSensor interface, Tmp36 impl, Tmp36Mock, Tmp36Sim - Managed example tests in test_weather.cpp (unit + system) - Minimal student starters in test_unit.cpp and test_system.cpp - Per-board pin defaults (A0 for uno, A0 for mega, A0 for nano) .anvilignore system: - Glob pattern matching (*, ?) with comments and backslash normalization - Default patterns protect student tests, app code, sketch, config - anvil refresh --force respects .anvilignore - anvil refresh --force --file <path> overrides ignore for one file - anvil refresh --ignore/--unignore manages patterns from CLI - Missing managed files always recreated even without --force - .anvilignore itself is in NEVER_REFRESH (cannot be overwritten) Refresh rewrite: - Discovers all template-produced files dynamically (no hardcoded list) - Extracts fresh template + libraries into temp dir for byte comparison - Config template field drives which files are managed - Separated missing-file creation from changed-file updates 428 tests passing on Windows MSVC, 0 warnings.
2121 lines
66 KiB
Rust
2121 lines
66 KiB
Rust
/// Integration tests for `anvil refresh` and `.anvilignore`.
|
|
///
|
|
/// Tests the full lifecycle: create project -> modify files -> refresh ->
|
|
/// verify ignore/update behavior across exact names, glob patterns,
|
|
/// --force, --file overrides, --ignore/--unignore management.
|
|
|
|
use anvil::commands;
|
|
use anvil::ignore::{self, AnvilIgnore};
|
|
use anvil::project::config::ProjectConfig;
|
|
use anvil::templates::{TemplateManager, TemplateContext};
|
|
use std::fs;
|
|
use tempfile::TempDir;
|
|
|
|
// =========================================================================
|
|
// Helpers
|
|
// =========================================================================
|
|
|
|
/// Create a fully-wired weather project (extract + config + libs + pins + .anvilignore).
|
|
fn make_weather(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("weather", tmp.path(), &ctx).unwrap();
|
|
|
|
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
|
config.project.template = "weather".to_string();
|
|
config.save(tmp.path()).unwrap();
|
|
|
|
ignore::generate_default(tmp.path(), "weather").unwrap();
|
|
|
|
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
|
|
}
|
|
|
|
/// Create a basic project with .anvilignore.
|
|
fn make_basic(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("basic", tmp.path(), &ctx).unwrap();
|
|
ignore::generate_default(tmp.path(), "basic").unwrap();
|
|
tmp
|
|
}
|
|
|
|
/// Shorthand for run_refresh with path string conversion.
|
|
fn refresh(dir: &std::path::Path, force: bool, file: Option<&str>) {
|
|
commands::refresh::run_refresh(
|
|
Some(dir.to_str().unwrap()),
|
|
force,
|
|
file,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
/// Shorthand for add_ignore.
|
|
fn add_ignore(dir: &std::path::Path, pattern: &str) {
|
|
commands::refresh::add_ignore(
|
|
Some(dir.to_str().unwrap()),
|
|
pattern,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
/// Shorthand for remove_ignore.
|
|
fn remove_ignore(dir: &std::path::Path, pattern: &str) {
|
|
commands::refresh::remove_ignore(
|
|
Some(dir.to_str().unwrap()),
|
|
pattern,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
/// Tamper with a file by overwriting its contents.
|
|
fn tamper(dir: &std::path::Path, rel: &str, content: &str) {
|
|
let path = dir.join(rel);
|
|
fs::write(&path, content).unwrap();
|
|
}
|
|
|
|
/// Read a project file.
|
|
fn read(dir: &std::path::Path, rel: &str) -> String {
|
|
fs::read_to_string(dir.join(rel)).unwrap()
|
|
}
|
|
|
|
/// Check if file contains a substring.
|
|
fn contains(dir: &std::path::Path, rel: &str, needle: &str) -> bool {
|
|
read(dir, rel).contains(needle)
|
|
}
|
|
|
|
// =========================================================================
|
|
// Refresh basics -- managed files updated, user files untouched
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_fresh_project_nothing_to_refresh() {
|
|
// A freshly created project should have all managed files identical
|
|
// to the template. Refresh should find nothing to do.
|
|
let tmp = make_weather("wx");
|
|
// No panic, no error
|
|
refresh(tmp.path(), false, None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tampered_managed_script_detected_without_force() {
|
|
let tmp = make_weather("wx");
|
|
tamper(tmp.path(), "build.sh", "#!/bin/bash\n# tampered\n");
|
|
|
|
// Without --force, run_refresh reports but does not overwrite
|
|
refresh(tmp.path(), false, None);
|
|
assert!(
|
|
contains(tmp.path(), "build.sh", "# tampered"),
|
|
"Without --force, tampered file should remain"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_force_restores_tampered_managed_script() {
|
|
let tmp = make_weather("wx");
|
|
let original = read(tmp.path(), "build.sh");
|
|
tamper(tmp.path(), "build.sh", "#!/bin/bash\n# tampered\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
let after = read(tmp.path(), "build.sh");
|
|
assert_eq!(after, original, "Force should restore build.sh");
|
|
}
|
|
|
|
#[test]
|
|
fn test_force_restores_tampered_mock_header() {
|
|
let tmp = make_weather("wx");
|
|
tamper(tmp.path(), "test/mocks/mock_hal.h", "// tampered\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "test/mocks/mock_hal.h", "MockHal"),
|
|
"Force should restore mock_hal.h"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_force_restores_tampered_cmake() {
|
|
let tmp = make_weather("wx");
|
|
tamper(tmp.path(), "test/CMakeLists.txt", "# tampered\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "test/CMakeLists.txt", "cmake_minimum_required"),
|
|
"Force should restore CMakeLists.txt"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_force_restores_tampered_hal() {
|
|
let tmp = make_weather("wx");
|
|
tamper(tmp.path(), "lib/hal/hal.h", "// tampered\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "lib/hal/hal.h", "class Hal"),
|
|
"Force should restore hal.h"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Ignored file survives --force
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_ignored_test_unit_survives_force() {
|
|
let tmp = make_weather("wx");
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// my custom tests\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_unit.cpp"),
|
|
"// my custom tests\n",
|
|
"Ignored test_unit.cpp must survive --force"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ignored_test_system_survives_force() {
|
|
let tmp = make_weather("wx");
|
|
tamper(tmp.path(), "test/test_system.cpp", "// my system tests\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_system.cpp"),
|
|
"// my system tests\n",
|
|
"Ignored test_system.cpp must survive --force"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ignored_app_code_survives_force() {
|
|
let tmp = make_weather("wx");
|
|
tamper(tmp.path(), "lib/app/wx_app.h", "// my custom app\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "lib/app/wx_app.h"),
|
|
"// my custom app\n",
|
|
"Ignored app code must survive --force"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ignored_config_survives_force() {
|
|
let tmp = make_weather("wx");
|
|
let config_before = read(tmp.path(), ".anvil.toml");
|
|
// Append a comment to config
|
|
let modified = format!("{}# user addition\n", config_before);
|
|
tamper(tmp.path(), ".anvil.toml", &modified);
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), ".anvil.toml", "# user addition"),
|
|
".anvil.toml is ignored, user changes must survive"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Managed template example test (test_weather.cpp) IS refreshed
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_managed_template_test_refreshed_by_force() {
|
|
let tmp = make_weather("wx");
|
|
tamper(
|
|
tmp.path(),
|
|
"test/test_weather.cpp",
|
|
"// tampered\n",
|
|
);
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "test/test_weather.cpp", "WeatherUnitTest"),
|
|
"Managed test_weather.cpp should be restored by --force"
|
|
);
|
|
assert!(
|
|
!contains(tmp.path(), "test/test_weather.cpp", "// tampered"),
|
|
"Tampered content should be gone"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Library driver headers are managed and refreshed
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_library_header_refreshed_by_force() {
|
|
let tmp = make_weather("wx");
|
|
tamper(
|
|
tmp.path(),
|
|
"lib/drivers/tmp36/tmp36.h",
|
|
"// tampered\n",
|
|
);
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "lib/drivers/tmp36/tmp36.h", "TempSensor"),
|
|
"Driver header should be restored"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_library_sim_header_refreshed_by_force() {
|
|
let tmp = make_weather("wx");
|
|
tamper(
|
|
tmp.path(),
|
|
"lib/drivers/tmp36/tmp36_sim.h",
|
|
"// tampered\n",
|
|
);
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "lib/drivers/tmp36/tmp36_sim.h", "Tmp36Sim"),
|
|
"Sim header should be restored"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_library_mock_header_refreshed_by_force() {
|
|
let tmp = make_weather("wx");
|
|
tamper(
|
|
tmp.path(),
|
|
"lib/drivers/tmp36/tmp36_mock.h",
|
|
"// tampered\n",
|
|
);
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "lib/drivers/tmp36/tmp36_mock.h", "Tmp36Mock"),
|
|
"Mock header should be restored"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_library_test_file_refreshed_by_force() {
|
|
let tmp = make_weather("wx");
|
|
tamper(tmp.path(), "test/test_tmp36.cpp", "// tampered\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "test/test_tmp36.cpp", "Tmp36"),
|
|
"Library test file should be restored"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// --file override: force-update one specific ignored file
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_file_override_restores_ignored_file() {
|
|
let tmp = make_weather("wx");
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// my custom code\n");
|
|
|
|
// Without --file, it stays
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(read(tmp.path(), "test/test_unit.cpp"), "// my custom code\n");
|
|
|
|
// With --file, it gets restored
|
|
refresh(tmp.path(), true, Some("test/test_unit.cpp"));
|
|
assert!(
|
|
contains(tmp.path(), "test/test_unit.cpp", "Your unit tests go here"),
|
|
"File override should restore test_unit.cpp to template"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_override_only_affects_specified_file() {
|
|
let tmp = make_weather("wx");
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// custom unit\n");
|
|
tamper(tmp.path(), "test/test_system.cpp", "// custom system\n");
|
|
|
|
// Override only test_unit.cpp
|
|
refresh(tmp.path(), true, Some("test/test_unit.cpp"));
|
|
|
|
// test_unit.cpp restored
|
|
assert!(
|
|
contains(tmp.path(), "test/test_unit.cpp", "Your unit tests go here"),
|
|
);
|
|
// test_system.cpp still has custom content (still ignored)
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_system.cpp"),
|
|
"// custom system\n",
|
|
"Other ignored files should remain untouched"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_override_with_backslash_path() {
|
|
let tmp = make_weather("wx");
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// custom\n");
|
|
|
|
// Windows-style path should work
|
|
refresh(tmp.path(), true, Some("test\\test_unit.cpp"));
|
|
assert!(
|
|
contains(tmp.path(), "test/test_unit.cpp", "Your unit tests go here"),
|
|
"Backslash path should be normalized"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_override_for_app_code() {
|
|
let tmp = make_weather("wx");
|
|
tamper(tmp.path(), "lib/app/wx_app.h", "// my custom app\n");
|
|
|
|
// Normally ignored via lib/app/*
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(read(tmp.path(), "lib/app/wx_app.h"), "// my custom app\n");
|
|
|
|
// Force override for this specific file
|
|
refresh(tmp.path(), true, Some("lib/app/wx_app.h"));
|
|
assert!(
|
|
contains(tmp.path(), "lib/app/wx_app.h", "WeatherApp"),
|
|
"File override should restore app code"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Full lifecycle: ignore -> modify -> force -> still protected ->
|
|
// unignore -> force -> restored
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_lifecycle_ignore_modify_force_unignore_force() {
|
|
let tmp = make_weather("wx");
|
|
let original = read(tmp.path(), "test/test_unit.cpp");
|
|
|
|
// Step 1: test_unit.cpp is already ignored by default
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.is_ignored("test/test_unit.cpp"));
|
|
|
|
// Step 2: Modify it
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// student work\n");
|
|
|
|
// Step 3: Force refresh -- file is protected
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_unit.cpp"),
|
|
"// student work\n",
|
|
"Protected file must survive force"
|
|
);
|
|
|
|
// Step 4: Unignore it
|
|
remove_ignore(tmp.path(), "test/test_unit.cpp");
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(!ignore.is_ignored("test/test_unit.cpp"));
|
|
|
|
// Step 5: Force refresh -- now it gets restored
|
|
refresh(tmp.path(), true, None);
|
|
let after = read(tmp.path(), "test/test_unit.cpp");
|
|
assert_eq!(
|
|
after, original,
|
|
"After unignore + force, file should be restored to template"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_lifecycle_add_custom_ignore_then_remove() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// build.sh is normally managed (not ignored)
|
|
let original = read(tmp.path(), "build.sh");
|
|
tamper(tmp.path(), "build.sh", "#!/bin/bash\n# my custom build\n");
|
|
|
|
// Force refresh restores it
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(read(tmp.path(), "build.sh"), original);
|
|
|
|
// Now add build.sh to .anvilignore
|
|
add_ignore(tmp.path(), "build.sh");
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.is_ignored("build.sh"));
|
|
|
|
// Tamper again
|
|
tamper(tmp.path(), "build.sh", "#!/bin/bash\n# my custom build\n");
|
|
|
|
// Force refresh -- now protected
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "build.sh", "# my custom build"),
|
|
"After adding to ignore, build.sh should be protected"
|
|
);
|
|
|
|
// Remove from ignore
|
|
remove_ignore(tmp.path(), "build.sh");
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(!ignore.is_ignored("build.sh"));
|
|
|
|
// Force refresh -- restored
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "build.sh"),
|
|
original,
|
|
"After removing from ignore, build.sh should be restored"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Glob pattern protection
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_glob_pattern_protects_multiple_files() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Add a glob pattern to protect all mock files
|
|
add_ignore(tmp.path(), "test/mocks/mock_*.h");
|
|
|
|
// Tamper with mock files
|
|
tamper(tmp.path(), "test/mocks/mock_hal.h", "// custom mock\n");
|
|
tamper(tmp.path(), "test/mocks/mock_arduino.h", "// custom arduino\n");
|
|
|
|
// Force refresh -- both protected by glob
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/mocks/mock_hal.h"),
|
|
"// custom mock\n",
|
|
"mock_hal.h should be protected by test/mocks/mock_*.h"
|
|
);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/mocks/mock_arduino.h"),
|
|
"// custom arduino\n",
|
|
"mock_arduino.h should be protected by test/mocks/mock_*.h"
|
|
);
|
|
|
|
// sim_hal.h is NOT matched by mock_*.h, so it should be restored
|
|
tamper(tmp.path(), "test/mocks/sim_hal.h", "// tampered sim\n");
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "test/mocks/sim_hal.h", "SimHal"),
|
|
"sim_hal.h does not match mock_*.h pattern, should be restored"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_glob_pattern_file_override_punches_through() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Add glob pattern
|
|
add_ignore(tmp.path(), "test/mocks/mock_*.h");
|
|
tamper(tmp.path(), "test/mocks/mock_hal.h", "// custom\n");
|
|
|
|
// Force with glob in place -- protected
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(read(tmp.path(), "test/mocks/mock_hal.h"), "// custom\n");
|
|
|
|
// File override punches through the glob
|
|
refresh(tmp.path(), true, Some("test/mocks/mock_hal.h"));
|
|
assert!(
|
|
contains(tmp.path(), "test/mocks/mock_hal.h", "MockHal"),
|
|
"--file should override glob pattern protection"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_glob_star_cpp_protects_all_test_files() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Nuclear option: protect ALL .cpp files in test/
|
|
add_ignore(tmp.path(), "test/*.cpp");
|
|
|
|
// Tamper with everything
|
|
tamper(tmp.path(), "test/test_weather.cpp", "// custom weather\n");
|
|
tamper(tmp.path(), "test/test_tmp36.cpp", "// custom tmp36\n");
|
|
|
|
// Force refresh -- all protected
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_weather.cpp"),
|
|
"// custom weather\n",
|
|
);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_tmp36.cpp"),
|
|
"// custom tmp36\n",
|
|
);
|
|
|
|
// But scripts are NOT .cpp, so they get refreshed
|
|
tamper(tmp.path(), "build.sh", "#!/bin/bash\n# tampered\n");
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
!contains(tmp.path(), "build.sh", "# tampered"),
|
|
"build.sh is not .cpp, should be refreshed"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_question_mark_wildcard_in_pattern() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Protect files matching test_???36.cpp (test_tmp36.cpp matches)
|
|
add_ignore(tmp.path(), "test/test_???36.cpp");
|
|
|
|
tamper(tmp.path(), "test/test_tmp36.cpp", "// custom\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_tmp36.cpp"),
|
|
"// custom\n",
|
|
"? wildcard should match single char"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Pattern management via add_ignore / remove_ignore
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_add_ignore_creates_file_if_missing() {
|
|
let tmp = make_basic("bp");
|
|
fs::remove_file(tmp.path().join(".anvilignore")).ok();
|
|
assert!(!tmp.path().join(".anvilignore").exists());
|
|
|
|
add_ignore(tmp.path(), "custom.txt");
|
|
assert!(tmp.path().join(".anvilignore").exists());
|
|
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.has_pattern("custom.txt"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_ignore_idempotent() {
|
|
let tmp = make_weather("wx");
|
|
add_ignore(tmp.path(), "test/test_unit.cpp");
|
|
add_ignore(tmp.path(), "test/test_unit.cpp");
|
|
|
|
let content = read(tmp.path(), ".anvilignore");
|
|
let count = content
|
|
.lines()
|
|
.filter(|l| l.trim() == "test/test_unit.cpp")
|
|
.count();
|
|
assert_eq!(count, 1, "Should not duplicate pattern");
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_ignore_normalizes_backslash() {
|
|
let tmp = make_weather("wx");
|
|
add_ignore(tmp.path(), "test\\custom.cpp");
|
|
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.has_pattern("test/custom.cpp"));
|
|
assert!(ignore.is_ignored("test/custom.cpp"));
|
|
assert!(ignore.is_ignored("test\\custom.cpp"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_remove_ignore_that_exists() {
|
|
let tmp = make_weather("wx");
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.has_pattern("test/test_unit.cpp"));
|
|
|
|
remove_ignore(tmp.path(), "test/test_unit.cpp");
|
|
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(!ignore.has_pattern("test/test_unit.cpp"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_remove_ignore_preserves_other_patterns() {
|
|
let tmp = make_weather("wx");
|
|
remove_ignore(tmp.path(), "test/test_unit.cpp");
|
|
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(!ignore.has_pattern("test/test_unit.cpp"));
|
|
// Other defaults should remain
|
|
assert!(ignore.has_pattern("test/test_system.cpp"));
|
|
assert!(ignore.has_pattern(".anvil.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_remove_nonexistent_pattern_does_not_error() {
|
|
let tmp = make_weather("wx");
|
|
// Should not panic or error
|
|
remove_ignore(tmp.path(), "does/not/exist.txt");
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_then_remove_glob_pattern() {
|
|
let tmp = make_weather("wx");
|
|
|
|
add_ignore(tmp.path(), "test/my_*.cpp");
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.is_ignored("test/my_custom.cpp"));
|
|
|
|
remove_ignore(tmp.path(), "test/my_*.cpp");
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(!ignore.is_ignored("test/my_custom.cpp"));
|
|
}
|
|
|
|
// =========================================================================
|
|
// No .anvilignore -- everything managed
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_no_anvilignore_all_files_managed() {
|
|
let tmp = make_weather("wx");
|
|
// Delete .anvilignore
|
|
fs::remove_file(tmp.path().join(".anvilignore")).unwrap();
|
|
|
|
// Tamper with a normally-ignored file
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// custom\n");
|
|
|
|
// Force refresh -- with no .anvilignore, nothing is protected
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "test/test_unit.cpp", "Your unit tests go here"),
|
|
"Without .anvilignore, all managed files should be refreshed"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// .anvilignore edge cases
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_anvilignore_comments_and_blanks_ignored() {
|
|
let tmp = make_basic("bp");
|
|
fs::write(
|
|
tmp.path().join(".anvilignore"),
|
|
"# This is a comment\n\
|
|
\n\
|
|
# Another comment\n\
|
|
\n\
|
|
build.sh\n\
|
|
\n\
|
|
# trailing comment\n",
|
|
)
|
|
.unwrap();
|
|
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert_eq!(ignore.patterns().len(), 1);
|
|
assert!(ignore.is_ignored("build.sh"));
|
|
assert!(!ignore.is_ignored("# This is a comment"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_anvilignore_whitespace_trimmed() {
|
|
let tmp = make_basic("bp");
|
|
fs::write(
|
|
tmp.path().join(".anvilignore"),
|
|
" build.sh \n test/test_unit.cpp\t\n",
|
|
)
|
|
.unwrap();
|
|
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.is_ignored("build.sh"));
|
|
assert!(ignore.is_ignored("test/test_unit.cpp"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_anvilignore_backslash_in_pattern_normalized() {
|
|
let tmp = make_basic("bp");
|
|
fs::write(
|
|
tmp.path().join(".anvilignore"),
|
|
"test\\test_unit.cpp\n",
|
|
)
|
|
.unwrap();
|
|
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.is_ignored("test/test_unit.cpp"));
|
|
assert!(ignore.is_ignored("test\\test_unit.cpp"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_anvilignore_empty_file_protects_nothing() {
|
|
let tmp = make_weather("wx");
|
|
fs::write(tmp.path().join(".anvilignore"), "").unwrap();
|
|
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// custom\n");
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "test/test_unit.cpp", "Your unit tests go here"),
|
|
"Empty .anvilignore protects nothing"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// matching_pattern -- which rule matched (UX reporting)
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_matching_pattern_exact_name() {
|
|
let tmp = make_weather("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_glob() {
|
|
let tmp = make_weather("wx");
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert_eq!(
|
|
ignore.matching_pattern("lib/app/wx_app.h"),
|
|
Some("lib/app/*"),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_matching_pattern_none_for_managed() {
|
|
let tmp = make_weather("wx");
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert_eq!(ignore.matching_pattern("build.sh"), None);
|
|
assert_eq!(ignore.matching_pattern("test/mocks/mock_hal.h"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_matching_pattern_first_pattern_wins() {
|
|
let tmp = make_basic("bp");
|
|
fs::write(
|
|
tmp.path().join(".anvilignore"),
|
|
"test/test_unit.cpp\ntest/test_*.cpp\n",
|
|
)
|
|
.unwrap();
|
|
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
// Exact match comes first in the file, so it wins
|
|
assert_eq!(
|
|
ignore.matching_pattern("test/test_unit.cpp"),
|
|
Some("test/test_unit.cpp"),
|
|
);
|
|
// Only matches the glob
|
|
assert_eq!(
|
|
ignore.matching_pattern("test/test_system.cpp"),
|
|
Some("test/test_*.cpp"),
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Multiple patterns stacking
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_multiple_exact_patterns() {
|
|
let tmp = make_basic("bp");
|
|
fs::write(
|
|
tmp.path().join(".anvilignore"),
|
|
"build.sh\nupload.sh\nmonitor.sh\n",
|
|
)
|
|
.unwrap();
|
|
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.is_ignored("build.sh"));
|
|
assert!(ignore.is_ignored("upload.sh"));
|
|
assert!(ignore.is_ignored("monitor.sh"));
|
|
assert!(!ignore.is_ignored("build.bat"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_overlapping_exact_and_glob() {
|
|
let tmp = make_basic("bp");
|
|
fs::write(
|
|
tmp.path().join(".anvilignore"),
|
|
"build.sh\n*.sh\n",
|
|
)
|
|
.unwrap();
|
|
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.is_ignored("build.sh")); // matched by both
|
|
assert!(ignore.is_ignored("upload.sh")); // matched by glob
|
|
assert!(!ignore.is_ignored("build.bat")); // not .sh
|
|
}
|
|
|
|
// =========================================================================
|
|
// Config template field
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_basic_project_template_defaults_to_basic() {
|
|
let tmp = make_basic("bp");
|
|
let config = ProjectConfig::load(tmp.path()).unwrap();
|
|
assert_eq!(config.project.template, "basic");
|
|
}
|
|
|
|
#[test]
|
|
fn test_weather_project_template_is_weather() {
|
|
let tmp = make_weather("wx");
|
|
let config = ProjectConfig::load(tmp.path()).unwrap();
|
|
assert_eq!(config.project.template, "weather");
|
|
}
|
|
|
|
#[test]
|
|
fn test_template_field_survives_save_reload() {
|
|
let tmp = make_weather("wx");
|
|
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
|
config.project.template = "custom".to_string();
|
|
config.save(tmp.path()).unwrap();
|
|
|
|
let reloaded = ProjectConfig::load(tmp.path()).unwrap();
|
|
assert_eq!(reloaded.project.template, "custom");
|
|
}
|
|
|
|
// =========================================================================
|
|
// Multi-step scenario: full student workflow
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_student_workflow_scenario() {
|
|
// Simulate a student's journey:
|
|
// 1. Create project
|
|
// 2. Write custom tests
|
|
// 3. Anvil upgrades, student runs refresh
|
|
// 4. Student's tests survive
|
|
// 5. Student wants to reset one file
|
|
// 6. Student adds protection for a custom helper
|
|
|
|
// Step 1: Create project
|
|
let tmp = make_weather("wx");
|
|
|
|
// Step 2: Student writes custom test code
|
|
tamper(
|
|
tmp.path(),
|
|
"test/test_unit.cpp",
|
|
"#include <gtest/gtest.h>\n\
|
|
TEST(MyTemp, ReadsCorrectly) {\n\
|
|
\x20 // student's real work\n\
|
|
\x20 EXPECT_TRUE(true);\n\
|
|
}\n",
|
|
);
|
|
tamper(
|
|
tmp.path(),
|
|
"test/test_system.cpp",
|
|
"#include <gtest/gtest.h>\n\
|
|
TEST(MySys, RunsForAnHour) {\n\
|
|
\x20 // student's system test\n\
|
|
\x20 EXPECT_TRUE(true);\n\
|
|
}\n",
|
|
);
|
|
|
|
// Step 3: Anvil upgrades. Simulate by tampering a managed file.
|
|
tamper(tmp.path(), "build.sh", "#!/bin/bash\n# old version\n");
|
|
tamper(tmp.path(), "test/test_weather.cpp", "// old example\n");
|
|
|
|
// Student runs refresh
|
|
refresh(tmp.path(), true, None);
|
|
|
|
// Step 4: Student's tests survive
|
|
assert!(
|
|
contains(tmp.path(), "test/test_unit.cpp", "ReadsCorrectly"),
|
|
"Student's unit test must survive"
|
|
);
|
|
assert!(
|
|
contains(tmp.path(), "test/test_system.cpp", "RunsForAnHour"),
|
|
"Student's system test must survive"
|
|
);
|
|
|
|
// Managed files are restored
|
|
assert!(
|
|
!contains(tmp.path(), "build.sh", "# old version"),
|
|
"build.sh should be refreshed"
|
|
);
|
|
assert!(
|
|
contains(tmp.path(), "test/test_weather.cpp", "WeatherUnitTest"),
|
|
"test_weather.cpp should be refreshed"
|
|
);
|
|
|
|
// Step 5: Student wants to reset test_system.cpp to the template starter
|
|
refresh(tmp.path(), true, Some("test/test_system.cpp"));
|
|
assert!(
|
|
contains(tmp.path(), "test/test_system.cpp", "Your system tests go here"),
|
|
"File override should reset test_system.cpp"
|
|
);
|
|
// test_unit.cpp still has student's work
|
|
assert!(
|
|
contains(tmp.path(), "test/test_unit.cpp", "ReadsCorrectly"),
|
|
"test_unit.cpp should still have student's work"
|
|
);
|
|
|
|
// Step 6: Student adds protection for a custom file they created
|
|
fs::write(
|
|
tmp.path().join("test").join("test_helpers.cpp"),
|
|
"// shared helpers\n",
|
|
)
|
|
.unwrap();
|
|
add_ignore(tmp.path(), "test/test_helpers.cpp");
|
|
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.is_ignored("test/test_helpers.cpp"));
|
|
}
|
|
|
|
// =========================================================================
|
|
// Refresh with basic template (no libraries, no template test)
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_basic_project_refresh_works() {
|
|
let tmp = make_basic("bp");
|
|
tamper(tmp.path(), "build.sh", "#!/bin/bash\n# tampered\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
!contains(tmp.path(), "build.sh", "# tampered"),
|
|
"build.sh should be restored for basic project"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_project_ignores_student_tests() {
|
|
let tmp = make_basic("bp");
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// my custom\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_unit.cpp"),
|
|
"// my custom\n",
|
|
"Basic project test_unit.cpp should be protected"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// .anvilignore itself is never refreshed or overwritten
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_anvilignore_itself_not_refreshed() {
|
|
let tmp = make_weather("wx");
|
|
let _original = read(tmp.path(), ".anvilignore");
|
|
|
|
// Add a custom pattern
|
|
add_ignore(tmp.path(), "my_custom_pattern");
|
|
|
|
// Force refresh
|
|
refresh(tmp.path(), true, None);
|
|
|
|
// .anvilignore should still have the custom pattern
|
|
assert!(
|
|
contains(tmp.path(), ".anvilignore", "my_custom_pattern"),
|
|
".anvilignore should not be overwritten by refresh"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Edge: delete a managed file, refresh recreates it
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_refresh_recreates_deleted_managed_file() {
|
|
let tmp = make_weather("wx");
|
|
let build_sh = tmp.path().join("build.sh");
|
|
assert!(build_sh.exists());
|
|
|
|
fs::remove_file(&build_sh).unwrap();
|
|
assert!(!build_sh.exists());
|
|
|
|
// Even without --force, missing files should be created
|
|
refresh(tmp.path(), false, None);
|
|
assert!(build_sh.exists(), "Deleted managed file should be recreated");
|
|
}
|
|
|
|
#[test]
|
|
fn test_refresh_recreates_deleted_mock_header() {
|
|
let tmp = make_weather("wx");
|
|
let mock = tmp.path().join("test").join("mocks").join("mock_hal.h");
|
|
fs::remove_file(&mock).unwrap();
|
|
|
|
refresh(tmp.path(), false, None);
|
|
assert!(mock.exists(), "Deleted mock header should be recreated");
|
|
}
|
|
|
|
// =========================================================================
|
|
// ASCII check
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_all_managed_files_ascii_after_refresh() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Tamper and force refresh to exercise the full write path
|
|
tamper(tmp.path(), "build.sh", "#!/bin/bash\n");
|
|
tamper(tmp.path(), "test/test_weather.cpp", "// x\n");
|
|
refresh(tmp.path(), true, None);
|
|
|
|
// Check every file under test/ and lib/ for non-ASCII
|
|
for entry in walkdir(tmp.path()) {
|
|
let content = fs::read(&entry).unwrap();
|
|
for (i, &byte) in content.iter().enumerate() {
|
|
assert!(
|
|
byte < 128,
|
|
"Non-ASCII byte {} at offset {} in {}",
|
|
byte,
|
|
i,
|
|
entry.display()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn walkdir(dir: &std::path::Path) -> Vec<std::path::PathBuf> {
|
|
let mut files = vec![];
|
|
if let Ok(entries) = fs::read_dir(dir) {
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
files.extend(walkdir(&path));
|
|
} else {
|
|
files.push(path);
|
|
}
|
|
}
|
|
}
|
|
files
|
|
}
|
|
|
|
// =========================================================================
|
|
// Multi-file simultaneous modification
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_simultaneous_modifications_across_categories() {
|
|
// Tamper with files in every category at once and verify each
|
|
// category behaves correctly after --force.
|
|
let tmp = make_weather("wx");
|
|
|
|
// Category 1: managed scripts (should be restored)
|
|
tamper(tmp.path(), "build.sh", "#!/bin/bash\n# tampered\n");
|
|
tamper(tmp.path(), "upload.sh", "#!/bin/bash\n# tampered\n");
|
|
tamper(tmp.path(), "test.sh", "#!/bin/bash\n# tampered\n");
|
|
|
|
// Category 2: managed mocks (should be restored)
|
|
tamper(tmp.path(), "test/mocks/mock_hal.h", "// tampered mock_hal\n");
|
|
tamper(tmp.path(), "test/mocks/sim_hal.h", "// tampered sim_hal\n");
|
|
|
|
// Category 3: managed template test (should be restored)
|
|
tamper(tmp.path(), "test/test_weather.cpp", "// tampered weather\n");
|
|
|
|
// Category 4: managed library headers (should be restored)
|
|
tamper(
|
|
tmp.path(),
|
|
"lib/drivers/tmp36/tmp36.h",
|
|
"// tampered driver\n",
|
|
);
|
|
|
|
// Category 5: managed HAL (should be restored)
|
|
tamper(tmp.path(), "lib/hal/hal.h", "// tampered hal\n");
|
|
|
|
// Category 6: ignored student files (should survive)
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// student unit work\n");
|
|
tamper(tmp.path(), "test/test_system.cpp", "// student system work\n");
|
|
|
|
// Category 7: ignored app code (should survive)
|
|
tamper(tmp.path(), "lib/app/wx_app.h", "// student app code\n");
|
|
|
|
// One big force refresh
|
|
refresh(tmp.path(), true, None);
|
|
|
|
// Verify category 1: restored
|
|
assert!(
|
|
!contains(tmp.path(), "build.sh", "# tampered"),
|
|
"build.sh should be restored"
|
|
);
|
|
assert!(
|
|
!contains(tmp.path(), "upload.sh", "# tampered"),
|
|
"upload.sh should be restored"
|
|
);
|
|
assert!(
|
|
!contains(tmp.path(), "test.sh", "# tampered"),
|
|
"test.sh should be restored"
|
|
);
|
|
|
|
// Verify category 2: restored
|
|
assert!(
|
|
contains(tmp.path(), "test/mocks/mock_hal.h", "MockHal"),
|
|
"mock_hal.h should be restored"
|
|
);
|
|
assert!(
|
|
contains(tmp.path(), "test/mocks/sim_hal.h", "SimHal"),
|
|
"sim_hal.h should be restored"
|
|
);
|
|
|
|
// Verify category 3: restored
|
|
assert!(
|
|
contains(tmp.path(), "test/test_weather.cpp", "WeatherUnitTest"),
|
|
"test_weather.cpp should be restored"
|
|
);
|
|
|
|
// Verify category 4: restored
|
|
assert!(
|
|
contains(tmp.path(), "lib/drivers/tmp36/tmp36.h", "TempSensor"),
|
|
"tmp36.h should be restored"
|
|
);
|
|
|
|
// Verify category 5: restored
|
|
assert!(
|
|
contains(tmp.path(), "lib/hal/hal.h", "class Hal"),
|
|
"hal.h should be restored"
|
|
);
|
|
|
|
// Verify category 6: survived
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_unit.cpp"),
|
|
"// student unit work\n",
|
|
);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_system.cpp"),
|
|
"// student system work\n",
|
|
);
|
|
|
|
// Verify category 7: survived
|
|
assert_eq!(
|
|
read(tmp.path(), "lib/app/wx_app.h"),
|
|
"// student app code\n",
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Glob lifecycle: add glob -> modify -> force -> protected ->
|
|
// remove glob -> force -> restored
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_glob_lifecycle_protect_then_unprotect() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Save originals for comparison
|
|
let orig_weather = read(tmp.path(), "test/test_weather.cpp");
|
|
let orig_tmp36 = read(tmp.path(), "test/test_tmp36.cpp");
|
|
|
|
// Add glob that covers managed test files
|
|
add_ignore(tmp.path(), "test/test_*.cpp");
|
|
|
|
// Tamper managed test files (now protected by glob)
|
|
tamper(tmp.path(), "test/test_weather.cpp", "// student weather\n");
|
|
tamper(tmp.path(), "test/test_tmp36.cpp", "// student tmp36\n");
|
|
|
|
// Force refresh -- both protected by glob
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_weather.cpp"),
|
|
"// student weather\n",
|
|
"Glob should protect test_weather.cpp"
|
|
);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_tmp36.cpp"),
|
|
"// student tmp36\n",
|
|
"Glob should protect test_tmp36.cpp"
|
|
);
|
|
|
|
// Remove the glob pattern
|
|
remove_ignore(tmp.path(), "test/test_*.cpp");
|
|
|
|
// Note: test_unit.cpp and test_system.cpp have their own exact patterns
|
|
// so they remain protected even after glob removal.
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(
|
|
ignore.is_ignored("test/test_unit.cpp"),
|
|
"Exact pattern for test_unit should survive glob removal"
|
|
);
|
|
assert!(
|
|
!ignore.is_ignored("test/test_weather.cpp"),
|
|
"test_weather should no longer be protected"
|
|
);
|
|
|
|
// Force refresh -- glob removed, managed files restored
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_weather.cpp"),
|
|
orig_weather,
|
|
"After glob removal, test_weather.cpp should be restored"
|
|
);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_tmp36.cpp"),
|
|
orig_tmp36,
|
|
"After glob removal, test_tmp36.cpp should be restored"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Pattern layering: exact + glob, remove exact -> still protected by glob
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_pattern_layering_exact_plus_glob() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Default has exact "test/test_unit.cpp".
|
|
// Add a glob that also covers it.
|
|
add_ignore(tmp.path(), "test/test_*.cpp");
|
|
|
|
// Verify: matched by exact pattern first
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert_eq!(
|
|
ignore.matching_pattern("test/test_unit.cpp"),
|
|
Some("test/test_unit.cpp"),
|
|
"Exact pattern should match first"
|
|
);
|
|
|
|
// Remove the exact pattern
|
|
remove_ignore(tmp.path(), "test/test_unit.cpp");
|
|
|
|
// Still protected by the glob
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(
|
|
ignore.is_ignored("test/test_unit.cpp"),
|
|
"Glob should still protect test_unit.cpp after exact removal"
|
|
);
|
|
assert_eq!(
|
|
ignore.matching_pattern("test/test_unit.cpp"),
|
|
Some("test/test_*.cpp"),
|
|
"Now matched by glob pattern"
|
|
);
|
|
|
|
// Tamper and force -- still protected
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// still protected\n");
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_unit.cpp"),
|
|
"// still protected\n",
|
|
);
|
|
|
|
// Remove the glob too -- now unprotected
|
|
remove_ignore(tmp.path(), "test/test_*.cpp");
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(!ignore.is_ignored("test/test_unit.cpp"));
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "test/test_unit.cpp", "Your unit tests go here"),
|
|
"With both patterns gone, file should be restored"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Re-ignore after unignore: round-trip protection
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_reignore_after_unignore() {
|
|
let tmp = make_weather("wx");
|
|
let original = read(tmp.path(), "test/test_unit.cpp");
|
|
|
|
// Unignore
|
|
remove_ignore(tmp.path(), "test/test_unit.cpp");
|
|
|
|
// Tamper and force -- gets restored since unprotected
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// custom\n");
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_unit.cpp"),
|
|
original,
|
|
"Unprotected file should be restored"
|
|
);
|
|
|
|
// Re-add the ignore
|
|
add_ignore(tmp.path(), "test/test_unit.cpp");
|
|
|
|
// Now tamper and force -- protected again
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// custom again\n");
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_unit.cpp"),
|
|
"// custom again\n",
|
|
"Re-ignored file should be protected"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Multiple sequential force refreshes
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_repeated_force_refreshes_idempotent() {
|
|
let tmp = make_weather("wx");
|
|
let orig_build = read(tmp.path(), "build.sh");
|
|
|
|
// Tamper, force, tamper, force, tamper, force
|
|
for i in 0..3 {
|
|
tamper(
|
|
tmp.path(),
|
|
"build.sh",
|
|
&format!("#!/bin/bash\n# round {}\n", i),
|
|
);
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "build.sh"),
|
|
orig_build,
|
|
"build.sh should be restored on round {}",
|
|
i
|
|
);
|
|
}
|
|
|
|
// Ignored file should survive all rounds
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// stable\n");
|
|
for _ in 0..3 {
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_unit.cpp"),
|
|
"// stable\n",
|
|
"Ignored file should survive every round"
|
|
);
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// --file with glob protection: only the named file is overridden
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_file_override_with_glob_surgical_precision() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Glob-protect all mock headers
|
|
add_ignore(tmp.path(), "test/mocks/*.h");
|
|
|
|
// Tamper three mock headers
|
|
tamper(tmp.path(), "test/mocks/mock_hal.h", "// custom mock_hal\n");
|
|
tamper(tmp.path(), "test/mocks/sim_hal.h", "// custom sim_hal\n");
|
|
tamper(tmp.path(), "test/mocks/mock_arduino.h", "// custom arduino\n");
|
|
|
|
// Force with --file for just mock_hal.h
|
|
refresh(tmp.path(), true, Some("test/mocks/mock_hal.h"));
|
|
|
|
// mock_hal.h restored, others still custom
|
|
assert!(
|
|
contains(tmp.path(), "test/mocks/mock_hal.h", "MockHal"),
|
|
"Named file should be restored"
|
|
);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/mocks/sim_hal.h"),
|
|
"// custom sim_hal\n",
|
|
"Non-named glob-protected file should survive"
|
|
);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/mocks/mock_arduino.h"),
|
|
"// custom arduino\n",
|
|
"Non-named glob-protected file should survive"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Whole-directory glob protection
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_directory_glob_protects_all_children() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Protect the entire drivers tree
|
|
add_ignore(tmp.path(), "lib/drivers/*");
|
|
|
|
// Tamper all driver headers
|
|
tamper(tmp.path(), "lib/drivers/tmp36/tmp36.h", "// custom\n");
|
|
tamper(tmp.path(), "lib/drivers/tmp36/tmp36_mock.h", "// custom\n");
|
|
tamper(tmp.path(), "lib/drivers/tmp36/tmp36_sim.h", "// custom\n");
|
|
|
|
// Force -- all protected
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "lib/drivers/tmp36/tmp36.h"),
|
|
"// custom\n",
|
|
);
|
|
assert_eq!(
|
|
read(tmp.path(), "lib/drivers/tmp36/tmp36_mock.h"),
|
|
"// custom\n",
|
|
);
|
|
assert_eq!(
|
|
read(tmp.path(), "lib/drivers/tmp36/tmp36_sim.h"),
|
|
"// custom\n",
|
|
);
|
|
|
|
// --file override for just one
|
|
refresh(tmp.path(), true, Some("lib/drivers/tmp36/tmp36.h"));
|
|
assert!(
|
|
contains(tmp.path(), "lib/drivers/tmp36/tmp36.h", "TempSensor"),
|
|
"Named file should be restored through directory glob"
|
|
);
|
|
assert_eq!(
|
|
read(tmp.path(), "lib/drivers/tmp36/tmp36_mock.h"),
|
|
"// custom\n",
|
|
"Other driver files still protected"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Sketch protection via default .anvilignore
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_sketch_protected_by_default() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// The default .anvilignore has a */*.ino pattern
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(
|
|
ignore.is_ignored("wx/wx.ino"),
|
|
"Sketch .ino should be protected by default"
|
|
);
|
|
|
|
// Tamper sketch and force -- should survive
|
|
tamper(tmp.path(), "wx/wx.ino", "// student sketch\n");
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "wx/wx.ino"),
|
|
"// student sketch\n",
|
|
"Sketch should survive force refresh"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// .anvilignore itself is in NEVER_REFRESH, cannot be overwritten
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_anvilignore_survives_even_if_not_in_patterns() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Write a completely custom .anvilignore
|
|
fs::write(
|
|
tmp.path().join(".anvilignore"),
|
|
"# my very custom ignore\nonly_this_one.txt\n",
|
|
)
|
|
.unwrap();
|
|
|
|
// Force refresh
|
|
refresh(tmp.path(), true, None);
|
|
|
|
// .anvilignore should still have our custom content
|
|
let content = read(tmp.path(), ".anvilignore");
|
|
assert!(
|
|
content.contains("# my very custom ignore"),
|
|
".anvilignore must never be overwritten by refresh"
|
|
);
|
|
assert!(
|
|
content.contains("only_this_one.txt"),
|
|
"Custom patterns must survive"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_anvilignore_not_overwritten_even_with_file_flag() {
|
|
// Even --file .anvilignore should not work because NEVER_REFRESH
|
|
let tmp = make_weather("wx");
|
|
fs::write(
|
|
tmp.path().join(".anvilignore"),
|
|
"# custom\n",
|
|
)
|
|
.unwrap();
|
|
|
|
refresh(tmp.path(), true, Some(".anvilignore"));
|
|
let content = read(tmp.path(), ".anvilignore");
|
|
assert_eq!(
|
|
content, "# custom\n",
|
|
".anvilignore must survive even --file override"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Delete multiple managed files, recreated without --force
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_multiple_deleted_files_recreated_without_force() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Delete several managed files
|
|
fs::remove_file(tmp.path().join("build.sh")).unwrap();
|
|
fs::remove_file(tmp.path().join("upload.sh")).unwrap();
|
|
fs::remove_file(tmp.path().join("test").join("run_tests.sh")).unwrap();
|
|
fs::remove_file(
|
|
tmp.path().join("test").join("mocks").join("mock_hal.h"),
|
|
)
|
|
.unwrap();
|
|
|
|
// Refresh without --force should recreate missing files
|
|
refresh(tmp.path(), false, None);
|
|
|
|
assert!(
|
|
tmp.path().join("build.sh").exists(),
|
|
"build.sh should be recreated"
|
|
);
|
|
assert!(
|
|
tmp.path().join("upload.sh").exists(),
|
|
"upload.sh should be recreated"
|
|
);
|
|
assert!(
|
|
tmp.path().join("test").join("run_tests.sh").exists(),
|
|
"run_tests.sh should be recreated"
|
|
);
|
|
assert!(
|
|
tmp.path()
|
|
.join("test")
|
|
.join("mocks")
|
|
.join("mock_hal.h")
|
|
.exists(),
|
|
"mock_hal.h should be recreated"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Delete an ignored file -- refresh does NOT recreate it (still ignored)
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_deleted_ignored_file_not_recreated() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// test_unit.cpp is ignored
|
|
let test_unit = tmp.path().join("test").join("test_unit.cpp");
|
|
assert!(test_unit.exists());
|
|
|
|
fs::remove_file(&test_unit).unwrap();
|
|
assert!(!test_unit.exists());
|
|
|
|
// Force refresh -- file is ignored so should NOT be recreated
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
!test_unit.exists(),
|
|
"Deleted ignored file should NOT be recreated by refresh"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deleted_ignored_file_recreated_with_file_override() {
|
|
let tmp = make_weather("wx");
|
|
let test_unit = tmp.path().join("test").join("test_unit.cpp");
|
|
fs::remove_file(&test_unit).unwrap();
|
|
|
|
// --file override should recreate even though ignored
|
|
refresh(tmp.path(), true, Some("test/test_unit.cpp"));
|
|
assert!(
|
|
test_unit.exists(),
|
|
"--file override should recreate deleted ignored file"
|
|
);
|
|
assert!(
|
|
contains(tmp.path(), "test/test_unit.cpp", "Your unit tests go here"),
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Without --force, tampered files are left alone (only missing created)
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_no_force_tampered_file_left_alone_missing_created() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Tamper one file and delete another
|
|
tamper(tmp.path(), "build.sh", "#!/bin/bash\n# old\n");
|
|
fs::remove_file(tmp.path().join("upload.sh")).unwrap();
|
|
|
|
refresh(tmp.path(), false, None);
|
|
|
|
// Tampered file untouched (no --force)
|
|
assert!(
|
|
contains(tmp.path(), "build.sh", "# old"),
|
|
"Without --force, tampered file should remain"
|
|
);
|
|
// Missing file recreated
|
|
assert!(
|
|
tmp.path().join("upload.sh").exists(),
|
|
"Missing file should be recreated even without --force"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Complex lifecycle: student works through the entire semester
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_full_semester_lifecycle() {
|
|
// A realistic multi-step scenario simulating a student over time:
|
|
//
|
|
// Week 1: Create project, start writing tests
|
|
// Week 3: Anvil v2 released, student refreshes
|
|
// Week 5: Student protects a custom helper file
|
|
// Week 7: Student accidentally deletes a script, refreshes to recover
|
|
// Week 9: Student decides to reset their system test to starter
|
|
// Week 11: Student protects all their custom test files with a glob
|
|
// Week 13: Course ends, student unprotects everything for grading
|
|
|
|
// == Week 1: Create and start writing ==
|
|
let tmp = make_weather("station");
|
|
|
|
tamper(
|
|
tmp.path(),
|
|
"test/test_unit.cpp",
|
|
"#include <gtest/gtest.h>\n\
|
|
#include \"tmp36_mock.h\"\n\
|
|
#include \"station_app.h\"\n\
|
|
TEST(StationUnit, ReadsTemp) {\n\
|
|
\x20 Tmp36Mock sensor;\n\
|
|
\x20 sensor.setTemperature(22.5f);\n\
|
|
\x20 EXPECT_NEAR(sensor.readCelsius(), 22.5f, 0.1f);\n\
|
|
}\n",
|
|
);
|
|
tamper(
|
|
tmp.path(),
|
|
"test/test_system.cpp",
|
|
"#include <gtest/gtest.h>\n\
|
|
TEST(StationSys, RunsOneMinute) {\n\
|
|
\x20 // TODO: implement\n\
|
|
\x20 EXPECT_TRUE(true);\n\
|
|
}\n",
|
|
);
|
|
|
|
// == Week 3: Anvil v2 -- scripts change, student refreshes ==
|
|
tamper(tmp.path(), "build.sh", "#!/bin/bash\n# v1 build\n");
|
|
tamper(tmp.path(), "test/test_weather.cpp", "// v1 examples\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
|
|
// Student's tests survived
|
|
assert!(
|
|
contains(tmp.path(), "test/test_unit.cpp", "StationUnit"),
|
|
"Week 3: student unit tests must survive"
|
|
);
|
|
assert!(
|
|
contains(tmp.path(), "test/test_system.cpp", "StationSys"),
|
|
"Week 3: student system tests must survive"
|
|
);
|
|
// Managed files updated
|
|
assert!(
|
|
!contains(tmp.path(), "build.sh", "# v1 build"),
|
|
"Week 3: build.sh should be refreshed"
|
|
);
|
|
assert!(
|
|
contains(tmp.path(), "test/test_weather.cpp", "WeatherUnitTest"),
|
|
"Week 3: test_weather.cpp should be refreshed"
|
|
);
|
|
|
|
// == Week 5: Student creates a helper and protects it ==
|
|
fs::create_dir_all(tmp.path().join("test").join("helpers")).unwrap();
|
|
fs::write(
|
|
tmp.path().join("test").join("helpers").join("test_utils.h"),
|
|
"// shared test utilities\n",
|
|
)
|
|
.unwrap();
|
|
add_ignore(tmp.path(), "test/helpers/*");
|
|
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.is_ignored("test/helpers/test_utils.h"));
|
|
|
|
// == Week 7: Student accidentally deletes build.sh, recovers ==
|
|
fs::remove_file(tmp.path().join("build.sh")).unwrap();
|
|
refresh(tmp.path(), false, None);
|
|
assert!(
|
|
tmp.path().join("build.sh").exists(),
|
|
"Week 7: deleted script should be recreated"
|
|
);
|
|
|
|
// == Week 9: Student wants to restart system tests from template ==
|
|
assert!(
|
|
contains(tmp.path(), "test/test_system.cpp", "StationSys"),
|
|
"Before reset, student's work is present"
|
|
);
|
|
refresh(tmp.path(), true, Some("test/test_system.cpp"));
|
|
assert!(
|
|
contains(
|
|
tmp.path(),
|
|
"test/test_system.cpp",
|
|
"Your system tests go here"
|
|
),
|
|
"Week 9: system test should be reset to starter"
|
|
);
|
|
// Unit test still has student's work
|
|
assert!(
|
|
contains(tmp.path(), "test/test_unit.cpp", "StationUnit"),
|
|
"Week 9: unit test should still have student's work"
|
|
);
|
|
|
|
// == Week 11: Student adds glob for all their custom test files ==
|
|
fs::write(
|
|
tmp.path().join("test").join("test_integration.cpp"),
|
|
"// student integration tests\n",
|
|
)
|
|
.unwrap();
|
|
add_ignore(tmp.path(), "test/test_integration.cpp");
|
|
|
|
// Verify protection stacking
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.is_ignored("test/test_unit.cpp"));
|
|
assert!(ignore.is_ignored("test/test_system.cpp"));
|
|
assert!(ignore.is_ignored("test/test_integration.cpp"));
|
|
assert!(ignore.is_ignored("test/helpers/test_utils.h"));
|
|
assert!(!ignore.is_ignored("test/test_weather.cpp"));
|
|
|
|
// == Week 13: For grading, remove all custom protection ==
|
|
remove_ignore(tmp.path(), "test/test_unit.cpp");
|
|
remove_ignore(tmp.path(), "test/test_system.cpp");
|
|
remove_ignore(tmp.path(), "test/test_integration.cpp");
|
|
remove_ignore(tmp.path(), "test/helpers/*");
|
|
|
|
// Config and app code still protected by remaining patterns
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.is_ignored(".anvil.toml"));
|
|
assert!(ignore.is_ignored("lib/app/station_app.h"));
|
|
assert!(!ignore.is_ignored("test/test_unit.cpp"));
|
|
}
|
|
|
|
// =========================================================================
|
|
// Refresh correctly uses the config template field
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_refresh_basic_does_not_have_template_test() {
|
|
let tmp = make_basic("bp");
|
|
tamper(tmp.path(), "build.sh", "# tampered\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
|
|
// build.sh refreshed
|
|
assert!(
|
|
!contains(tmp.path(), "build.sh", "# tampered"),
|
|
);
|
|
|
|
// No test_basic.cpp should exist (basic template has no example test)
|
|
assert!(
|
|
!tmp.path().join("test").join("test_basic.cpp").exists(),
|
|
"Basic template should NOT produce a test_basic.cpp"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Refresh after config template change
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_refresh_after_manual_template_change() {
|
|
// If someone changes config.project.template, refresh adapts.
|
|
// This is an edge case -- switching templates mid-project isn't
|
|
// officially supported, but refresh shouldn't crash.
|
|
let tmp = make_weather("wx");
|
|
|
|
// Change template to "basic" in config
|
|
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
|
config.project.template = "basic".to_string();
|
|
config.save(tmp.path()).unwrap();
|
|
|
|
// Refresh should still work (extracts basic template, not weather)
|
|
refresh(tmp.path(), true, None);
|
|
|
|
// Should not crash -- that's the main assertion
|
|
assert!(tmp.path().join("build.sh").exists());
|
|
}
|
|
|
|
// =========================================================================
|
|
// Edge: --force with no changes should be a no-op
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_force_on_clean_project_is_noop() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Save content of a managed file
|
|
let before = read(tmp.path(), "build.sh");
|
|
|
|
// Force refresh on a clean project
|
|
refresh(tmp.path(), true, None);
|
|
|
|
// Content should be byte-identical
|
|
let after = read(tmp.path(), "build.sh");
|
|
assert_eq!(before, after, "Force on clean project should be a no-op");
|
|
}
|
|
|
|
// =========================================================================
|
|
// Glob with nested directories
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_glob_star_matches_nested_paths() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// lib/drivers/tmp36/* should match files inside
|
|
add_ignore(tmp.path(), "lib/drivers/tmp36/*");
|
|
|
|
tamper(tmp.path(), "lib/drivers/tmp36/tmp36.h", "// protected\n");
|
|
tamper(tmp.path(), "lib/drivers/tmp36/tmp36_sim.h", "// protected\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
|
|
assert_eq!(
|
|
read(tmp.path(), "lib/drivers/tmp36/tmp36.h"),
|
|
"// protected\n",
|
|
);
|
|
assert_eq!(
|
|
read(tmp.path(), "lib/drivers/tmp36/tmp36_sim.h"),
|
|
"// protected\n",
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Edge: adding multiple glob patterns that overlap
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_multiple_overlapping_globs() {
|
|
let tmp = make_weather("wx");
|
|
|
|
add_ignore(tmp.path(), "test/*.cpp");
|
|
add_ignore(tmp.path(), "test/test_*.cpp");
|
|
|
|
tamper(tmp.path(), "test/test_weather.cpp", "// protected\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_weather.cpp"),
|
|
"// protected\n",
|
|
"File matched by multiple globs should be protected"
|
|
);
|
|
|
|
// Remove one glob -- still protected by the other
|
|
remove_ignore(tmp.path(), "test/test_*.cpp");
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_weather.cpp"),
|
|
"// protected\n",
|
|
"Still protected by remaining glob"
|
|
);
|
|
|
|
// Remove the other glob -- now unprotected
|
|
remove_ignore(tmp.path(), "test/*.cpp");
|
|
|
|
// Note: exact pattern for test_unit and test_system still exists
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(
|
|
!ignore.is_ignored("test/test_weather.cpp"),
|
|
"test_weather.cpp should now be unprotected"
|
|
);
|
|
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
contains(tmp.path(), "test/test_weather.cpp", "WeatherUnitTest"),
|
|
"Unprotected file should be restored"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Edge: empty .anvilignore vs missing .anvilignore
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_empty_vs_missing_anvilignore_equivalent() {
|
|
// Both should result in nothing being protected.
|
|
let tmp1 = make_weather("wx1");
|
|
let tmp2 = make_weather("wx2");
|
|
|
|
// tmp1: empty .anvilignore
|
|
fs::write(tmp1.path().join(".anvilignore"), "").unwrap();
|
|
// tmp2: delete .anvilignore
|
|
fs::remove_file(tmp2.path().join(".anvilignore")).unwrap();
|
|
|
|
// Tamper same file in both
|
|
tamper(tmp1.path(), "test/test_unit.cpp", "// custom\n");
|
|
tamper(tmp2.path(), "test/test_unit.cpp", "// custom\n");
|
|
|
|
// Force refresh both
|
|
refresh(tmp1.path(), true, None);
|
|
refresh(tmp2.path(), true, None);
|
|
|
|
// Both should have file restored (nothing protected)
|
|
assert!(
|
|
contains(tmp1.path(), "test/test_unit.cpp", "Your unit tests go here"),
|
|
"Empty .anvilignore: file should be restored"
|
|
);
|
|
assert!(
|
|
contains(tmp2.path(), "test/test_unit.cpp", "Your unit tests go here"),
|
|
"Missing .anvilignore: file should be restored"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Edge: only comments in .anvilignore
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_comments_only_anvilignore_protects_nothing() {
|
|
let tmp = make_weather("wx");
|
|
fs::write(
|
|
tmp.path().join(".anvilignore"),
|
|
"# just comments\n# nothing real\n# more comments\n",
|
|
)
|
|
.unwrap();
|
|
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// custom\n");
|
|
refresh(tmp.path(), true, None);
|
|
|
|
assert!(
|
|
contains(tmp.path(), "test/test_unit.cpp", "Your unit tests go here"),
|
|
"Comments-only .anvilignore should protect nothing"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Verify ignore patterns persist across multiple refreshes
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_added_patterns_persist_across_refreshes() {
|
|
let tmp = make_weather("wx");
|
|
|
|
add_ignore(tmp.path(), "test/test_weather.cpp");
|
|
tamper(tmp.path(), "test/test_weather.cpp", "// custom\n");
|
|
|
|
// Refresh 1
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_weather.cpp"),
|
|
"// custom\n",
|
|
);
|
|
|
|
// Refresh 2 -- pattern should still be in .anvilignore
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(
|
|
read(tmp.path(), "test/test_weather.cpp"),
|
|
"// custom\n",
|
|
"Added patterns must persist across refreshes"
|
|
);
|
|
|
|
// Verify the pattern is still in the file
|
|
let ignore = AnvilIgnore::load(tmp.path()).unwrap();
|
|
assert!(ignore.has_pattern("test/test_weather.cpp"));
|
|
}
|
|
|
|
// =========================================================================
|
|
// Edge: refresh with all scripts deleted AND modified files
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_refresh_with_mixed_deleted_and_modified() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// Delete some managed files
|
|
fs::remove_file(tmp.path().join("build.sh")).unwrap();
|
|
fs::remove_file(tmp.path().join("upload.sh")).unwrap();
|
|
|
|
// Tamper others
|
|
tamper(tmp.path(), "monitor.sh", "#!/bin/bash\n# old monitor\n");
|
|
tamper(tmp.path(), "test.sh", "#!/bin/bash\n# old test\n");
|
|
|
|
// Refresh without --force: missing files created, tampered left alone
|
|
refresh(tmp.path(), false, None);
|
|
|
|
assert!(tmp.path().join("build.sh").exists(), "Deleted: recreated");
|
|
assert!(tmp.path().join("upload.sh").exists(), "Deleted: recreated");
|
|
assert!(
|
|
contains(tmp.path(), "monitor.sh", "# old monitor"),
|
|
"Tampered without --force: left alone"
|
|
);
|
|
|
|
// Now force
|
|
refresh(tmp.path(), true, None);
|
|
assert!(
|
|
!contains(tmp.path(), "monitor.sh", "# old monitor"),
|
|
"Tampered with --force: restored"
|
|
);
|
|
assert!(
|
|
!contains(tmp.path(), "test.sh", "# old test"),
|
|
"Tampered with --force: restored"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Edge: tamper a .bat file (Windows script), verify restoration
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_bat_scripts_managed_and_refreshed() {
|
|
let tmp = make_weather("wx");
|
|
let orig = read(tmp.path(), "build.bat");
|
|
|
|
tamper(tmp.path(), "build.bat", "@echo off\nREM tampered\n");
|
|
tamper(tmp.path(), "upload.bat", "@echo off\nREM tampered\n");
|
|
|
|
refresh(tmp.path(), true, None);
|
|
|
|
assert_eq!(
|
|
read(tmp.path(), "build.bat"),
|
|
orig,
|
|
"build.bat should be restored"
|
|
);
|
|
assert!(
|
|
!contains(tmp.path(), "upload.bat", "REM tampered"),
|
|
"upload.bat should be restored"
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Edge: add a pattern, verify it's there, add it again, still only once
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_add_ignore_truly_idempotent_with_refresh() {
|
|
let tmp = make_weather("wx");
|
|
|
|
// The default already has test/test_unit.cpp
|
|
add_ignore(tmp.path(), "test/test_unit.cpp");
|
|
add_ignore(tmp.path(), "test/test_unit.cpp");
|
|
add_ignore(tmp.path(), "test/test_unit.cpp");
|
|
|
|
let content = read(tmp.path(), ".anvilignore");
|
|
let count = content
|
|
.lines()
|
|
.filter(|l| l.trim() == "test/test_unit.cpp")
|
|
.count();
|
|
assert_eq!(count, 1, "Pattern must appear exactly once");
|
|
|
|
// Refresh still works fine
|
|
tamper(tmp.path(), "test/test_unit.cpp", "// custom\n");
|
|
refresh(tmp.path(), true, None);
|
|
assert_eq!(read(tmp.path(), "test/test_unit.cpp"), "// custom\n");
|
|
}
|
|
|
|
// =========================================================================
|
|
// Verify all scripts (.sh and .bat) are in the managed set
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn test_all_scripts_are_managed() {
|
|
let tmp = make_weather("wx");
|
|
|
|
let scripts = [
|
|
"build.sh",
|
|
"build.bat",
|
|
"upload.sh",
|
|
"upload.bat",
|
|
"monitor.sh",
|
|
"monitor.bat",
|
|
"test.sh",
|
|
"test.bat",
|
|
"test/run_tests.sh",
|
|
"test/run_tests.bat",
|
|
];
|
|
|
|
for script in &scripts {
|
|
let orig = read(tmp.path(), script);
|
|
tamper(tmp.path(), script, "TAMPERED");
|
|
refresh(tmp.path(), true, None);
|
|
|
|
let after = read(tmp.path(), script);
|
|
assert_eq!(
|
|
after, orig,
|
|
"{} should be managed and restored by refresh",
|
|
script
|
|
);
|
|
}
|
|
} |