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.
429 lines
17 KiB
Rust
429 lines
17 KiB
Rust
// 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");
|
|
} |