// 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 "); 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"); }