Refactor CLI, add refresh command, fix port detection, add device tracking
- Remove build/upload/monitor subcommands (projects are self-contained) - Remove ctrlc dependency (only used by removed monitor watch mode) - Update next-steps messaging to reference project scripts directly - Add 'anvil refresh [DIR] [--force]' to update project scripts to latest templates without touching user code - Fix Windows port detection: replace fragile findstr/batch TOML parsing with proper comment-skipping logic; add _detect_port.ps1 helper for reliable JSON-based port detection via PowerShell - Add .anvil.local for machine-specific config (gitignored) - 'anvil devices --set [PORT] [-d DIR]' saves port + VID:PID - 'anvil devices --get [-d DIR]' shows saved port status - VID:PID tracks USB devices across COM port reassignment - Port resolution: -p flag > VID:PID > saved port > auto-detect - Uppercase normalization for Windows COM port names - Update all .bat/.sh templates to read from .anvil.local - Remove port entries from .anvil.toml (no machine-specific config in git) - Add .anvil.local to .gitignore template - Expand 'anvil devices' output with VID:PID, serial number, and usage instructions
This commit is contained in:
@@ -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,260 @@ 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"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scripts_tolerate_missing_toml_keys() {
|
||||
// Regression: toml_get must not kill the script when a key is absent.
|
||||
// With set -euo pipefail, bare grep returns exit 1 on no match,
|
||||
// pipefail propagates it, and set -e terminates silently.
|
||||
// Every grep in toml_get/toml_array must have "|| true".
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "grep_safe".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"] {
|
||||
let content = fs::read_to_string(tmp.path().join(script)).unwrap();
|
||||
|
||||
// If the script uses set -e (or -euo pipefail), then every
|
||||
// toml_get/toml_array function must guard grep with || true
|
||||
if content.contains("set -e") || content.contains("set -euo") {
|
||||
// Find the toml_get function body and check for || true
|
||||
let has_safe_grep = content.contains("|| true");
|
||||
assert!(
|
||||
has_safe_grep,
|
||||
"{} uses set -e but toml_get/toml_array lacks '|| true' guard. \
|
||||
Missing TOML keys will silently kill the script.",
|
||||
script
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user