Placing scripts in the generated project

This commit is contained in:
Eric Ratliff
2026-02-16 08:29:33 -06:00
parent 3298844399
commit fc1fb73d5a
12 changed files with 1093 additions and 38 deletions

View File

@@ -18,7 +18,7 @@ fn test_basic_template_extracts_all_expected_files() {
};
let count = TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
assert!(count >= 10, "Expected at least 10 files, got {}", count);
assert!(count >= 16, "Expected at least 16 files, got {}", count);
}
#[test]
@@ -321,6 +321,12 @@ fn test_full_project_structure() {
"lib/hal/hal.h",
"lib/hal/hal_arduino.h",
"lib/app/full_test_app.h",
"build.sh",
"build.bat",
"upload.sh",
"upload.bat",
"monitor.sh",
"monitor.bat",
"test/CMakeLists.txt",
"test/test_unit.cpp",
"test/run_tests.sh",
@@ -406,3 +412,228 @@ fn test_load_config_from_nonproject_fails() {
let result = ProjectConfig::load(tmp.path());
assert!(result.is_err());
}
// ============================================================================
// Self-contained script tests
// ============================================================================
#[test]
fn test_template_creates_self_contained_scripts() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "standalone".to_string(),
anvil_version: "1.0.0".to_string(),
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
// All six scripts must exist
let scripts = vec![
"build.sh", "build.bat",
"upload.sh", "upload.bat",
"monitor.sh", "monitor.bat",
];
for script in &scripts {
let p = tmp.path().join(script);
assert!(p.exists(), "Script missing: {}", script);
}
}
#[test]
fn test_build_sh_reads_anvil_toml() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "toml_reader".to_string(),
anvil_version: "1.0.0".to_string(),
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
let content = fs::read_to_string(tmp.path().join("build.sh")).unwrap();
assert!(
content.contains(".anvil.toml"),
"build.sh should reference .anvil.toml"
);
assert!(
content.contains("arduino-cli"),
"build.sh should invoke arduino-cli"
);
assert!(
!content.contains("anvil build"),
"build.sh must NOT depend on the anvil binary"
);
}
#[test]
fn test_upload_sh_reads_anvil_toml() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "uploader".to_string(),
anvil_version: "1.0.0".to_string(),
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
let content = fs::read_to_string(tmp.path().join("upload.sh")).unwrap();
assert!(
content.contains(".anvil.toml"),
"upload.sh should reference .anvil.toml"
);
assert!(
content.contains("arduino-cli"),
"upload.sh should invoke arduino-cli"
);
assert!(
content.contains("upload"),
"upload.sh should contain upload command"
);
assert!(
content.contains("--monitor"),
"upload.sh should support --monitor flag"
);
assert!(
!content.contains("anvil upload"),
"upload.sh must NOT depend on the anvil binary"
);
}
#[test]
fn test_monitor_sh_reads_anvil_toml() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "serial_mon".to_string(),
anvil_version: "1.0.0".to_string(),
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
let content = fs::read_to_string(tmp.path().join("monitor.sh")).unwrap();
assert!(
content.contains(".anvil.toml"),
"monitor.sh should reference .anvil.toml"
);
assert!(
content.contains("--watch"),
"monitor.sh should support --watch flag"
);
assert!(
!content.contains("anvil monitor"),
"monitor.sh must NOT depend on the anvil binary"
);
}
#[test]
fn test_scripts_have_shebangs() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "shebangs".to_string(),
anvil_version: "1.0.0".to_string(),
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
for script in &["build.sh", "upload.sh", "monitor.sh", "test/run_tests.sh"] {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
assert!(
content.starts_with("#!/"),
"{} should start with a shebang line",
script
);
}
}
#[test]
fn test_scripts_no_anvil_binary_dependency() {
// Critical: generated projects must NOT require the anvil binary
// for build, upload, or monitor operations.
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "no_anvil_dep".to_string(),
anvil_version: "1.0.0".to_string(),
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
let scripts = vec![
"build.sh", "build.bat",
"upload.sh", "upload.bat",
"monitor.sh", "monitor.bat",
"test/run_tests.sh", "test/run_tests.bat",
];
for script in &scripts {
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
// None of these scripts should shell out to anvil
let has_anvil_cmd = content.lines().any(|line| {
let trimmed = line.trim();
// Skip comments and echo/print lines
if trimmed.starts_with('#')
|| trimmed.starts_with("::")
|| trimmed.starts_with("echo")
|| trimmed.starts_with("REM")
|| trimmed.starts_with("rem")
{
return false;
}
// Check for "anvil " as a command invocation
trimmed.contains("anvil ")
&& !trimmed.contains("anvil.toml")
&& !trimmed.contains("Anvil")
});
assert!(
!has_anvil_cmd,
"{} should not invoke the anvil binary (project must be self-contained)",
script
);
}
}
#[test]
fn test_gitignore_excludes_build_cache() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "gitcheck".to_string(),
anvil_version: "1.0.0".to_string(),
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
let content = fs::read_to_string(tmp.path().join(".gitignore")).unwrap();
assert!(
content.contains(".build/"),
".gitignore should exclude .build/ (arduino-cli build cache)"
);
assert!(
content.contains("test/build/"),
".gitignore should exclude test/build/ (cmake build cache)"
);
}
#[test]
fn test_readme_documents_self_contained_workflow() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "docs_check".to_string(),
anvil_version: "1.0.0".to_string(),
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
let readme = fs::read_to_string(tmp.path().join("README.md")).unwrap();
assert!(
readme.contains("./build.sh"),
"README should document build.sh"
);
assert!(
readme.contains("./upload.sh"),
"README should document upload.sh"
);
assert!(
readme.contains("./monitor.sh"),
"README should document monitor.sh"
);
assert!(
readme.contains("self-contained"),
"README should mention self-contained"
);
}