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:
429
tests/integration/environment_tests.rs
Normal file
429
tests/integration/environment_tests.rs
Normal 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");
|
||||
}
|
||||
Reference in New Issue
Block a user