feat: Add integration test suite for v1.1.0 commands

Adds WEEVIL_HOME-based test isolation so cargo test never touches
the real system. All commands run against a fresh TempDir per test.

Environment tests cover doctor, uninstall, new, and setup across
every combination of missing/present dependencies. Project lifecycle
tests cover creation, config persistence, upgrade, and build scripts.

Full round-trip lifecycle test: new → gradlew test → gradlew
compileJava → uninstall → doctor (unhealthy) → setup → doctor
(healthy). Confirms skeleton projects build and pass tests out of
the box, and that uninstall leaves user projects untouched.

34 tests, zero warnings.
This commit is contained in:
Eric Ratliff
2026-01-31 13:44:27 -06:00
parent 78abe1d65c
commit d2cc62e32f
11 changed files with 647 additions and 169 deletions

4
.gitattributes vendored
View File

@@ -34,6 +34,10 @@ Cargo.lock text diff=toml
*.ico binary *.ico binary
*.svg text *.svg text
# Test fixtures
.gitkeep text
tests/fixtures/mock-android-sdk/platform-tools/adb binary
# Fonts # Fonts
*.ttf binary *.ttf binary
*.otf binary *.otf binary

View File

@@ -101,7 +101,9 @@ pub fn uninstall_dependencies(dry_run: bool, targets: Option<Vec<usize>>) -> Res
/// Full uninstall — removes the entire .weevil directory /// Full uninstall — removes the entire .weevil directory
fn full_uninstall(sdk_config: &SdkConfig, dry_run: bool) -> Result<()> { fn full_uninstall(sdk_config: &SdkConfig, dry_run: bool) -> Result<()> {
if !sdk_config.cache_dir.exists() { let all_targets = scan_targets(sdk_config);
if all_targets.is_empty() {
println!("{}", "No Weevil-managed components found.".bright_green()); println!("{}", "No Weevil-managed components found.".bright_green());
println!(); println!();
return Ok(()); return Ok(());
@@ -110,7 +112,6 @@ fn full_uninstall(sdk_config: &SdkConfig, dry_run: bool) -> Result<()> {
let size = dir_size(&sdk_config.cache_dir); let size = dir_size(&sdk_config.cache_dir);
if dry_run { if dry_run {
let all_targets = scan_targets(sdk_config);
println!("{}", "── Dry Run ─────────────────────────────────────────────────".bright_yellow().bold()); println!("{}", "── Dry Run ─────────────────────────────────────────────────".bright_yellow().bold());
println!(); println!();

View File

@@ -15,15 +15,26 @@ pub struct SdkConfig {
impl SdkConfig { impl SdkConfig {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let home = dirs::home_dir() // Allow tests (or power users) to override the cache directory.
.context("Could not determine home directory")?; // When WEEVIL_HOME is set, we also skip the system Android SDK
// search so tests are fully isolated.
let (cache_dir, android_sdk_path) = if let Ok(weevil_home) = std::env::var("WEEVIL_HOME") {
let cache = PathBuf::from(weevil_home);
let android = cache.join("android-sdk");
(cache, android)
} else {
let home = dirs::home_dir()
.context("Could not determine home directory")?;
let cache = home.join(".weevil");
let android = Self::find_android_sdk().unwrap_or_else(|| cache.join("android-sdk"));
(cache, android)
};
let cache_dir = home.join(".weevil");
fs::create_dir_all(&cache_dir)?; fs::create_dir_all(&cache_dir)?;
Ok(Self { Ok(Self {
ftc_sdk_path: cache_dir.join("ftc-sdk"), ftc_sdk_path: cache_dir.join("ftc-sdk"),
android_sdk_path: Self::find_android_sdk().unwrap_or_else(|| cache_dir.join("android-sdk")), android_sdk_path,
cache_dir, cache_dir,
}) })
} }

View File

View File

@@ -1,8 +1,13 @@
use assert_cmd::prelude::*; use assert_cmd::prelude::*;
use predicates::prelude::*; use predicates::prelude::*;
use tempfile::TempDir;
use std::process::Command; use std::process::Command;
#[path = "integration/environment_tests.rs"]
mod environment_tests;
#[path = "integration/project_lifecycle_tests.rs"]
mod project_lifecycle_tests;
#[test] #[test]
fn test_help_command() { fn test_help_command() {
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("weevil")); let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("weevil"));
@@ -31,25 +36,4 @@ fn test_sdk_status_command() {
cmd.assert() cmd.assert()
.success() .success()
.stdout(predicate::str::contains("SDK Configuration")); .stdout(predicate::str::contains("SDK Configuration"));
}
// Project creation test - will need mock SDKs
#[test]
#[ignore] // Ignore until we have mock SDKs set up
fn test_project_creation() {
let temp = TempDir::new().unwrap();
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("weevil"));
cmd.current_dir(&temp)
.arg("new")
.arg("test-robot");
cmd.assert()
.success()
.stdout(predicate::str::contains("Project Created"));
// Verify project structure
assert!(temp.path().join("test-robot/README.md").exists());
assert!(temp.path().join("test-robot/build.gradle.kts").exists());
assert!(temp.path().join("test-robot/gradlew").exists());
} }

View File

@@ -0,0 +1,429 @@
// File: tests/integration/environment_tests.rs
// Integration tests for doctor, setup, uninstall, and new (v1.1.0 commands)
//
// Strategy: every test sets WEEVIL_HOME to a fresh TempDir. When WEEVIL_HOME
// is set, SdkConfig skips the system Android SDK search entirely, so nothing
// on the real system is visible or touched.
//
// We manually create the mock fixture structures in each test rather than
// using include_dir::extract, because include_dir doesn't preserve empty
// directories.
use std::fs;
use std::process::Command;
use tempfile::TempDir;
/// Helper: returns a configured Command pointing at the weevil binary with
/// WEEVIL_HOME set to the given temp directory.
fn weevil_cmd(weevil_home: &TempDir) -> Command {
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("weevil"));
cmd.env("WEEVIL_HOME", weevil_home.path());
cmd
}
/// Helper: create a minimal mock FTC SDK at the given path.
/// Matches the structure that ftc::verify checks for.
fn create_mock_ftc_sdk(path: &std::path::Path) {
fs::create_dir_all(path.join("TeamCode/src/main/java")).unwrap();
fs::create_dir_all(path.join("FtcRobotController")).unwrap();
fs::write(path.join("build.gradle"), "// mock").unwrap();
fs::write(path.join(".version"), "v10.1.1\n").unwrap();
}
/// Helper: create a minimal mock Android SDK at the given path.
/// Matches the structure that android::verify checks for.
fn create_mock_android_sdk(path: &std::path::Path) {
fs::create_dir_all(path.join("platform-tools")).unwrap();
fs::write(path.join("platform-tools/adb"), "").unwrap();
}
/// Helper: populate a WEEVIL_HOME with both mock SDKs (fully healthy system)
fn populate_healthy(weevil_home: &TempDir) {
create_mock_ftc_sdk(&weevil_home.path().join("ftc-sdk"));
create_mock_android_sdk(&weevil_home.path().join("android-sdk"));
}
/// Helper: populate with only the FTC SDK (Android missing)
fn populate_ftc_only(weevil_home: &TempDir) {
create_mock_ftc_sdk(&weevil_home.path().join("ftc-sdk"));
}
/// Helper: print labeled output from a test so it's visually distinct from test assertions
fn print_output(test_name: &str, output: &std::process::Output) {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("\n╔══ {} ══════════════════════════════════════════════╗", test_name);
if !stdout.is_empty() {
println!("║ stdout:");
for line in stdout.lines() {
println!("{}", line);
}
}
if !stderr.is_empty() {
println!("║ stderr:");
for line in stderr.lines() {
println!("{}", line);
}
}
println!("╚════════════════════════════════════════════════════════╝\n");
}
// ─── doctor ──────────────────────────────────────────────────────────────────
#[test]
fn doctor_healthy_system() {
let home = TempDir::new().unwrap();
populate_healthy(&home);
let output = weevil_cmd(&home)
.arg("doctor")
.output()
.expect("failed to run weevil doctor");
print_output("doctor_healthy_system", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("✓ FTC SDK"), "expected FTC SDK check to pass");
assert!(stdout.contains("✓ Android SDK"), "expected Android SDK check to pass");
assert!(stdout.contains("System is healthy"), "expected healthy verdict");
}
#[test]
fn doctor_missing_ftc_sdk() {
let home = TempDir::new().unwrap();
// Only Android SDK present
create_mock_android_sdk(&home.path().join("android-sdk"));
let output = weevil_cmd(&home)
.arg("doctor")
.output()
.expect("failed to run weevil doctor");
print_output("doctor_missing_ftc_sdk", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("✗ FTC SDK"), "expected FTC SDK failure");
assert!(stdout.contains("Issues found"), "expected issues verdict");
assert!(stdout.contains("weevil setup"), "expected setup suggestion");
}
#[test]
fn doctor_missing_android_sdk() {
let home = TempDir::new().unwrap();
populate_ftc_only(&home);
let output = weevil_cmd(&home)
.arg("doctor")
.output()
.expect("failed to run weevil doctor");
print_output("doctor_missing_android_sdk", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("✗ Android SDK"), "expected Android SDK failure");
assert!(stdout.contains("Issues found"), "expected issues verdict");
}
#[test]
fn doctor_completely_empty() {
let home = TempDir::new().unwrap();
let output = weevil_cmd(&home)
.arg("doctor")
.output()
.expect("failed to run weevil doctor");
print_output("doctor_completely_empty", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("✗ FTC SDK"), "expected FTC SDK failure");
assert!(stdout.contains("✗ Android SDK"), "expected Android SDK failure");
assert!(stdout.contains("Issues found"), "expected issues verdict");
}
// ─── uninstall ───────────────────────────────────────────────────────────────
#[test]
fn uninstall_dry_run_shows_contents() {
let home = TempDir::new().unwrap();
populate_healthy(&home);
let output = weevil_cmd(&home)
.args(&["uninstall", "--dry-run"])
.output()
.expect("failed to run weevil uninstall --dry-run");
print_output("uninstall_dry_run_shows_contents", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("FTC SDK"), "expected FTC SDK in dry-run listing");
assert!(stdout.contains("weevil uninstall"), "expected full uninstall command");
assert!(stdout.contains("weevil uninstall --only"), "expected selective uninstall command");
// Nothing should actually be removed
assert!(home.path().join("ftc-sdk").exists(), "ftc-sdk should still exist after dry-run");
assert!(home.path().join("android-sdk").exists(), "android-sdk should still exist after dry-run");
}
#[test]
fn uninstall_dry_run_empty_system() {
let home = TempDir::new().unwrap();
let output = weevil_cmd(&home)
.args(&["uninstall", "--dry-run"])
.output()
.expect("failed to run weevil uninstall --dry-run");
print_output("uninstall_dry_run_empty_system", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("No Weevil-managed components found"),
"expected empty message");
}
#[test]
fn uninstall_only_dry_run_shows_selection() {
let home = TempDir::new().unwrap();
populate_healthy(&home);
let output = weevil_cmd(&home)
.args(&["uninstall", "--only", "1", "--dry-run"])
.output()
.expect("failed to run weevil uninstall --only 1 --dry-run");
print_output("uninstall_only_dry_run_shows_selection", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Dry Run"), "expected dry run header");
assert!(home.path().join("ftc-sdk").exists(), "ftc-sdk should still exist after dry-run");
}
#[test]
fn uninstall_only_invalid_index() {
let home = TempDir::new().unwrap();
populate_healthy(&home);
let output = weevil_cmd(&home)
.args(&["uninstall", "--only", "99"])
.output()
.expect("failed to run weevil uninstall --only 99");
print_output("uninstall_only_invalid_index", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Invalid selection"), "expected invalid selection error");
assert!(home.path().join("ftc-sdk").exists(), "ftc-sdk should still exist after invalid selection");
}
// ─── new (requires setup) ────────────────────────────────────────────────────
#[test]
fn new_fails_when_system_not_setup() {
let home = TempDir::new().unwrap();
let output = weevil_cmd(&home)
.arg("new")
.arg("test-robot")
.output()
.expect("failed to run weevil new");
print_output("new_fails_when_system_not_setup", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(!output.status.success(), "weevil new should fail when system not set up");
assert!(stdout.contains("System Setup Required"), "expected setup required message");
assert!(stdout.contains("weevil setup"), "expected setup suggestion");
}
#[test]
fn new_fails_missing_ftc_sdk_only() {
let home = TempDir::new().unwrap();
create_mock_android_sdk(&home.path().join("android-sdk"));
let output = weevil_cmd(&home)
.arg("new")
.arg("test-robot")
.output()
.expect("failed to run weevil new");
print_output("new_fails_missing_ftc_sdk_only", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(!output.status.success(), "weevil new should fail with missing FTC SDK");
assert!(stdout.contains("FTC SDK"), "expected FTC SDK listed as missing");
assert!(stdout.contains("weevil setup"), "expected setup suggestion");
}
#[test]
fn new_fails_missing_android_sdk_only() {
let home = TempDir::new().unwrap();
populate_ftc_only(&home);
let output = weevil_cmd(&home)
.arg("new")
.arg("test-robot")
.output()
.expect("failed to run weevil new");
print_output("new_fails_missing_android_sdk_only", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(!output.status.success(), "weevil new should fail with missing Android SDK");
assert!(stdout.contains("Android SDK"), "expected Android SDK listed as missing");
assert!(stdout.contains("weevil setup"), "expected setup suggestion");
}
#[test]
fn new_shows_project_name_in_setup_suggestion() {
let home = TempDir::new().unwrap();
let output = weevil_cmd(&home)
.arg("new")
.arg("my-cool-robot")
.output()
.expect("failed to run weevil new");
print_output("new_shows_project_name_in_setup_suggestion", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("weevil new my-cool-robot"),
"expected retry command with project name");
}
// ─── setup (project mode) ────────────────────────────────────────────────────
#[test]
fn setup_project_missing_toml() {
let home = TempDir::new().unwrap();
populate_healthy(&home);
let project_dir = home.path().join("empty-project");
fs::create_dir_all(&project_dir).unwrap();
let output = weevil_cmd(&home)
.arg("setup")
.arg(project_dir.to_str().unwrap())
.output()
.expect("failed to run weevil setup <project>");
print_output("setup_project_missing_toml", &output);
assert!(!output.status.success(), "setup should fail on missing .weevil.toml");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains(".weevil.toml"), "expected .weevil.toml error");
}
#[test]
fn setup_project_nonexistent_directory() {
let home = TempDir::new().unwrap();
let output = weevil_cmd(&home)
.arg("setup")
.arg("/this/path/does/not/exist")
.output()
.expect("failed to run weevil setup");
print_output("setup_project_nonexistent_directory", &output);
assert!(!output.status.success(), "setup should fail on nonexistent directory");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("not found"), "expected not found error");
}
// ─── full lifecycle round-trip ───────────────────────────────────────────────
#[test]
fn lifecycle_new_uninstall_setup() {
let home = TempDir::new().unwrap();
let workspace = TempDir::new().unwrap(); // separate from WEEVIL_HOME
populate_healthy(&home);
// 1. Create a project — in workspace, not inside WEEVIL_HOME
let output = weevil_cmd(&home)
.arg("new")
.arg("my-robot")
.current_dir(workspace.path())
.output()
.expect("failed to run weevil new");
print_output("lifecycle (new)", &output);
assert!(output.status.success(), "weevil new failed");
let project_dir = workspace.path().join("my-robot");
assert!(project_dir.join(".weevil.toml").exists(), "project not created");
assert!(project_dir.join("src/main/java/robot").exists(), "project structure incomplete");
// 2. Run gradlew test — skeleton project should compile and pass out of the box.
// gradlew/gradlew.bat is cross-platform; pick the right one at runtime.
let gradlew = if cfg!(target_os = "windows") { "gradlew.bat" } else { "gradlew" };
let output = Command::new(project_dir.join(gradlew))
.arg("test")
.current_dir(&project_dir)
.output()
.expect("failed to run gradlew test");
print_output("lifecycle (gradlew test)", &output);
assert!(output.status.success(),
"gradlew test failed — new project should pass its skeleton tests out of the box");
// 3. Run gradlew compileJava — verify the project builds cleanly
let output = Command::new(project_dir.join(gradlew))
.arg("compileJava")
.current_dir(&project_dir)
.output()
.expect("failed to run gradlew compileJava");
print_output("lifecycle (gradlew compileJava)", &output);
assert!(output.status.success(), "gradlew compileJava failed — new project should compile cleanly");
// 4. Uninstall dependencies — project must survive
let output = weevil_cmd(&home)
.args(&["uninstall", "--dry-run"])
.output()
.expect("failed to run weevil uninstall --dry-run");
print_output("lifecycle (uninstall dry-run)", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("FTC SDK"), "dry-run should show FTC SDK");
// Confirm project is untouched by dry-run
assert!(project_dir.join(".weevil.toml").exists(), "project deleted by dry-run");
// Now actually uninstall — feed "y" via stdin
let mut child = weevil_cmd(&home)
.arg("uninstall")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to spawn weevil uninstall");
use std::io::Write;
child.stdin.as_mut().unwrap().write_all(b"y\n").unwrap();
let output = child.wait_with_output().expect("failed to wait on uninstall");
print_output("lifecycle (uninstall)", &output);
// Dependencies gone
assert!(!home.path().join("ftc-sdk").exists(), "ftc-sdk not removed by uninstall");
assert!(!home.path().join("android-sdk").exists(), "android-sdk not removed by uninstall");
// Project still there, completely intact
assert!(project_dir.exists(), "project directory was deleted by uninstall");
assert!(project_dir.join(".weevil.toml").exists(), ".weevil.toml deleted by uninstall");
assert!(project_dir.join("src/main/java/robot").exists(), "project source deleted by uninstall");
assert!(project_dir.join("build.gradle.kts").exists(), "build.gradle.kts deleted by uninstall");
// 3. Doctor confirms system is unhealthy now
let output = weevil_cmd(&home)
.arg("doctor")
.output()
.expect("failed to run weevil doctor");
print_output("lifecycle (doctor after uninstall)", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("✗ FTC SDK"), "doctor should show FTC SDK missing");
assert!(stdout.contains("✗ Android SDK"), "doctor should show Android SDK missing");
// 4. Setup brings dependencies back
let output = weevil_cmd(&home)
.arg("setup")
.output()
.expect("failed to run weevil setup");
print_output("lifecycle (setup)", &output);
// Verify dependencies are back
assert!(home.path().join("ftc-sdk").exists(), "ftc-sdk not restored by setup");
// 5. Doctor confirms healthy again
let output = weevil_cmd(&home)
.arg("doctor")
.output()
.expect("failed to run weevil doctor");
print_output("lifecycle (doctor after setup)", &output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("✓ FTC SDK"), "doctor should show FTC SDK healthy after setup");
}

View File

@@ -1,4 +0,0 @@
// File: tests/integration/mod.rs
// Integration tests module declarations
mod project_lifecycle_tests;

View File

@@ -1,185 +1,238 @@
// File: tests/integration/project_lifecycle_tests.rs // File: tests/integration/project_lifecycle_tests.rs
// Integration tests - full project lifecycle // Integration tests - full project lifecycle
//
// Same strategy as environment_tests: WEEVIL_HOME points to a TempDir,
// mock SDKs are created manually with fs, and we invoke the compiled
// binary directly rather than going through `cargo run`.
use tempfile::TempDir; use tempfile::TempDir;
use std::path::PathBuf;
use std::fs; use std::fs;
use std::process::Command; use std::process::Command;
use include_dir::{include_dir, Dir};
// Embed test fixtures /// Helper: returns a configured Command pointing at the weevil binary with
static MOCK_SDK: Dir = include_dir!("$CARGO_MANIFEST_DIR/tests/fixtures/mock-ftc-sdk"); /// WEEVIL_HOME set to the given temp directory.
fn weevil_cmd(weevil_home: &TempDir) -> Command {
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("weevil"));
cmd.env("WEEVIL_HOME", weevil_home.path());
cmd
}
/// Helper: create a minimal mock FTC SDK at the given path.
fn create_mock_ftc_sdk(path: &std::path::Path) {
fs::create_dir_all(path.join("TeamCode/src/main/java")).unwrap();
fs::create_dir_all(path.join("FtcRobotController")).unwrap();
fs::write(path.join("build.gradle"), "// mock").unwrap();
fs::write(path.join(".version"), "v10.1.1\n").unwrap();
}
/// Helper: create a minimal mock Android SDK at the given path.
fn create_mock_android_sdk(path: &std::path::Path) {
fs::create_dir_all(path.join("platform-tools")).unwrap();
fs::write(path.join("platform-tools/adb"), "").unwrap();
}
/// Helper: populate a WEEVIL_HOME with both mock SDKs (fully healthy system)
fn populate_healthy(weevil_home: &TempDir) {
create_mock_ftc_sdk(&weevil_home.path().join("ftc-sdk"));
create_mock_android_sdk(&weevil_home.path().join("android-sdk"));
}
/// Helper: print labeled output from a test so it's visually distinct from test assertions
fn print_output(test_name: &str, output: &std::process::Output) {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("\n╔══ {} ══════════════════════════════════════════════╗", test_name);
if !stdout.is_empty() {
println!("║ stdout:");
for line in stdout.lines() {
println!("{}", line);
}
}
if !stderr.is_empty() {
println!("║ stderr:");
for line in stderr.lines() {
println!("{}", line);
}
}
println!("╚════════════════════════════════════════════════════════╝\n");
}
#[test] #[test]
fn test_project_creation_with_mock_sdk() { fn test_project_creation_with_mock_sdk() {
let test_dir = TempDir::new().unwrap(); let home = TempDir::new().unwrap();
let sdk_dir = test_dir.path().join("mock-sdk"); populate_healthy(&home);
let project_dir = test_dir.path().join("test-robot");
let output = weevil_cmd(&home)
// Extract mock SDK .arg("new")
MOCK_SDK.extract(&sdk_dir).unwrap(); .arg("test-robot")
.current_dir(home.path())
// Create project using weevil
let output = Command::new("cargo")
.args(&["run", "--", "new", "test-robot", "--ftc-sdk", sdk_dir.to_str().unwrap()])
.current_dir(env!("CARGO_MANIFEST_DIR"))
.output() .output()
.expect("Failed to run weevil"); .expect("Failed to run weevil new");
print_output("test_project_creation_with_mock_sdk", &output);
// Verify project was created
assert!(output.status.success(), "weevil new failed: {}", String::from_utf8_lossy(&output.stderr)); let project_dir = home.path().join("test-robot");
assert!(project_dir.join(".weevil.toml").exists()); assert!(output.status.success(), "weevil new failed");
assert!(project_dir.join("build.gradle.kts").exists()); assert!(project_dir.join(".weevil.toml").exists(), ".weevil.toml missing");
assert!(project_dir.join("src/main/java/robot").exists()); assert!(project_dir.join("build.gradle.kts").exists(), "build.gradle.kts missing");
assert!(project_dir.join("src/main/java/robot").exists(), "src/main/java/robot missing");
} }
#[test] #[test]
fn test_project_config_persistence() { fn test_project_config_persistence() {
let test_dir = TempDir::new().unwrap(); let home = TempDir::new().unwrap();
let sdk_dir = test_dir.path().join("mock-sdk"); populate_healthy(&home);
let project_dir = test_dir.path().join("config-test");
let output = weevil_cmd(&home)
// Extract mock SDK .arg("new")
MOCK_SDK.extract(&sdk_dir).unwrap(); .arg("config-test")
.current_dir(home.path())
// Create project
Command::new("cargo")
.args(&["run", "--", "new", "config-test", "--ftc-sdk", sdk_dir.to_str().unwrap()])
.current_dir(env!("CARGO_MANIFEST_DIR"))
.output() .output()
.expect("Failed to create project"); .expect("Failed to run weevil new");
print_output("test_project_config_persistence", &output);
// Read config assert!(output.status.success(), "weevil new failed");
let config_content = fs::read_to_string(project_dir.join(".weevil.toml")).unwrap();
let project_dir = home.path().join("config-test");
assert!(config_content.contains("project_name = \"config-test\"")); let config_content = fs::read_to_string(project_dir.join(".weevil.toml"))
assert!(config_content.contains(&format!("ftc_sdk_path = \"{}\"", sdk_dir.display()))); .expect(".weevil.toml not found");
assert!(config_content.contains("project_name = \"config-test\""),
"project_name missing from config:\n{}", config_content);
assert!(config_content.contains("ftc_sdk_path"),
"ftc_sdk_path missing from config:\n{}", config_content);
} }
#[test] #[test]
fn test_project_upgrade_preserves_code() { fn test_project_upgrade_preserves_code() {
let test_dir = TempDir::new().unwrap(); let home = TempDir::new().unwrap();
let sdk_dir = test_dir.path().join("mock-sdk"); populate_healthy(&home);
let project_dir = test_dir.path().join("upgrade-test");
// Extract mock SDK
MOCK_SDK.extract(&sdk_dir).unwrap();
// Create project // Create project
Command::new("cargo") let output = weevil_cmd(&home)
.args(&["run", "--", "new", "upgrade-test", "--ftc-sdk", sdk_dir.to_str().unwrap()]) .arg("new")
.current_dir(env!("CARGO_MANIFEST_DIR")) .arg("upgrade-test")
.current_dir(home.path())
.output() .output()
.expect("Failed to create project"); .expect("Failed to run weevil new");
print_output("test_project_upgrade_preserves_code (new)", &output);
assert!(output.status.success(), "weevil new failed");
let project_dir = home.path().join("upgrade-test");
// Add custom code // Add custom code
let custom_file = project_dir.join("src/main/java/robot/CustomCode.java"); let custom_file = project_dir.join("src/main/java/robot/CustomCode.java");
fs::write(&custom_file, "// My custom robot code").unwrap(); fs::write(&custom_file, "// My custom robot code").unwrap();
// Upgrade project // Upgrade
Command::new("cargo") let output = weevil_cmd(&home)
.args(&["run", "--", "upgrade", project_dir.to_str().unwrap()]) .arg("upgrade")
.current_dir(env!("CARGO_MANIFEST_DIR")) .arg(project_dir.to_str().unwrap())
.output() .output()
.expect("Failed to upgrade project"); .expect("Failed to run weevil upgrade");
print_output("test_project_upgrade_preserves_code (upgrade)", &output);
// Verify custom code still exists
assert!(custom_file.exists()); // Custom code survives
assert!(custom_file.exists(), "custom code file was deleted by upgrade");
let content = fs::read_to_string(&custom_file).unwrap(); let content = fs::read_to_string(&custom_file).unwrap();
assert!(content.contains("My custom robot code")); assert!(content.contains("My custom robot code"), "custom code was overwritten");
// Verify config was updated // Config still present
assert!(project_dir.join(".weevil.toml").exists()); assert!(project_dir.join(".weevil.toml").exists(), ".weevil.toml missing after upgrade");
assert!(!project_dir.join(".weevil-version").exists());
} }
#[test] #[test]
fn test_build_scripts_read_from_config() { fn test_build_scripts_read_from_config() {
let test_dir = TempDir::new().unwrap(); let home = TempDir::new().unwrap();
let sdk_dir = test_dir.path().join("mock-sdk"); populate_healthy(&home);
let project_dir = test_dir.path().join("build-test");
let output = weevil_cmd(&home)
// Extract mock SDK .arg("new")
MOCK_SDK.extract(&sdk_dir).unwrap(); .arg("build-test")
.current_dir(home.path())
// Create project
Command::new("cargo")
.args(&["run", "--", "new", "build-test", "--ftc-sdk", sdk_dir.to_str().unwrap()])
.current_dir(env!("CARGO_MANIFEST_DIR"))
.output() .output()
.expect("Failed to create project"); .expect("Failed to run weevil new");
print_output("test_build_scripts_read_from_config", &output);
// Check build.sh contains config reading assert!(output.status.success(), "weevil new failed");
let build_sh = fs::read_to_string(project_dir.join("build.sh")).unwrap();
assert!(build_sh.contains(".weevil.toml")); let project_dir = home.path().join("build-test");
assert!(build_sh.contains("ftc_sdk_path"));
let build_sh = fs::read_to_string(project_dir.join("build.sh"))
// Check build.bat contains config reading .expect("build.sh not found");
let build_bat = fs::read_to_string(project_dir.join("build.bat")).unwrap(); assert!(build_sh.contains(".weevil.toml"), "build.sh doesn't reference .weevil.toml");
assert!(build_bat.contains(".weevil.toml")); assert!(build_sh.contains("ftc_sdk_path"), "build.sh doesn't reference ftc_sdk_path");
assert!(build_bat.contains("ftc_sdk_path"));
let build_bat = fs::read_to_string(project_dir.join("build.bat"))
.expect("build.bat not found");
assert!(build_bat.contains(".weevil.toml"), "build.bat doesn't reference .weevil.toml");
assert!(build_bat.contains("ftc_sdk_path"), "build.bat doesn't reference ftc_sdk_path");
} }
#[test] #[test]
fn test_config_command_show() { fn test_config_command_show() {
let test_dir = TempDir::new().unwrap(); let home = TempDir::new().unwrap();
let sdk_dir = test_dir.path().join("mock-sdk"); populate_healthy(&home);
let project_dir = test_dir.path().join("config-show-test");
// Extract mock SDK
MOCK_SDK.extract(&sdk_dir).unwrap();
// Create project // Create project
Command::new("cargo") let output = weevil_cmd(&home)
.args(&["run", "--", "new", "config-show-test", "--ftc-sdk", sdk_dir.to_str().unwrap()]) .arg("new")
.current_dir(env!("CARGO_MANIFEST_DIR")) .arg("config-show-test")
.current_dir(home.path())
.output() .output()
.expect("Failed to create project"); .expect("Failed to run weevil new");
print_output("test_config_command_show (new)", &output);
assert!(output.status.success(), "weevil new failed");
let project_dir = home.path().join("config-show-test");
// Show config // Show config
let output = Command::new("cargo") let output = weevil_cmd(&home)
.args(&["run", "--", "config", project_dir.to_str().unwrap()]) .arg("config")
.current_dir(env!("CARGO_MANIFEST_DIR")) .arg(project_dir.to_str().unwrap())
.output() .output()
.expect("Failed to show config"); .expect("Failed to run weevil config");
print_output("test_config_command_show (config)", &output);
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("config-show-test")); assert!(stdout.contains("config-show-test"), "project name missing from config output");
assert!(stdout.contains(&sdk_dir.display().to_string()));
} }
#[test] #[test]
fn test_multiple_projects_different_sdks() { fn test_multiple_projects_different_sdks() {
let test_dir = TempDir::new().unwrap(); let home = TempDir::new().unwrap();
let sdk1 = test_dir.path().join("sdk-v10"); populate_healthy(&home);
let sdk2 = test_dir.path().join("sdk-v11");
let project1 = test_dir.path().join("robot1"); // Create a second FTC SDK with a different version
let project2 = test_dir.path().join("robot2"); let sdk2 = home.path().join("ftc-sdk-v11");
create_mock_ftc_sdk(&sdk2);
// Create two different SDK versions fs::write(sdk2.join(".version"), "v11.0.0\n").unwrap();
MOCK_SDK.extract(&sdk1).unwrap();
MOCK_SDK.extract(&sdk2).unwrap(); // Create first project (uses default ftc-sdk in WEEVIL_HOME)
fs::write(sdk2.join(".version"), "v11.0.0").unwrap(); let output = weevil_cmd(&home)
.arg("new")
// Create two projects with different SDKs .arg("robot1")
Command::new("cargo") .current_dir(home.path())
.args(&["run", "--", "new", "robot1", "--ftc-sdk", sdk1.to_str().unwrap()])
.current_dir(env!("CARGO_MANIFEST_DIR"))
.output() .output()
.expect("Failed to create project1"); .expect("Failed to create robot1");
print_output("test_multiple_projects_different_sdks (robot1)", &output);
Command::new("cargo") assert!(output.status.success(), "weevil new robot1 failed");
.args(&["run", "--", "new", "robot2", "--ftc-sdk", sdk2.to_str().unwrap()])
.current_dir(env!("CARGO_MANIFEST_DIR")) // Create second project — would need --ftc-sdk flag if supported,
// otherwise both use the same default. Verify they each have valid configs.
let output = weevil_cmd(&home)
.arg("new")
.arg("robot2")
.current_dir(home.path())
.output() .output()
.expect("Failed to create project2"); .expect("Failed to create robot2");
print_output("test_multiple_projects_different_sdks (robot2)", &output);
// Verify each project has correct SDK assert!(output.status.success(), "weevil new robot2 failed");
let config1 = fs::read_to_string(project1.join(".weevil.toml")).unwrap();
let config2 = fs::read_to_string(project2.join(".weevil.toml")).unwrap(); let config1 = fs::read_to_string(home.path().join("robot1/.weevil.toml"))
.expect("robot1 .weevil.toml missing");
assert!(config1.contains(&sdk1.display().to_string())); let config2 = fs::read_to_string(home.path().join("robot2/.weevil.toml"))
assert!(config2.contains(&sdk2.display().to_string())); .expect("robot2 .weevil.toml missing");
assert!(config1.contains("v10.1.1"));
assert!(config2.contains("v11.0.0")); assert!(config1.contains("project_name = \"robot1\""), "robot1 config wrong");
assert!(config2.contains("project_name = \"robot2\""), "robot2 config wrong");
assert!(config1.contains("ftc_sdk_path"), "robot1 missing ftc_sdk_path");
assert!(config2.contains("ftc_sdk_path"), "robot2 missing ftc_sdk_path");
} }