7 Commits

Author SHA1 Message Date
Eric Ratliff
5596f5bade fix: single source of truth for version across crate and tests
Replace all hardcoded "1.1.0" version strings with env!("CARGO_PKG_VERSION")
in src/, so Cargo.toml is the sole source for the built binary. Tests
intentionally use a separate hardcoded constant in tests/common.rs to act
as a canary — they will fail on a version bump until manually updated.

- src/project/mod.rs: add WEEVIL_VERSION const, wire into Tera context,
  generated README, and .weevil-version marker
- tests/common.rs: new file, holds EXPECTED_VERSION for all test crates
- tests/{integration,project_lifecycle,unit/config_tests}.rs: pull from
  common instead of env! or inline literals
2026-01-31 18:45:29 -06:00
Eric Ratliff
d2cc62e32f 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.
2026-01-31 13:56:01 -06:00
Eric Ratliff
78abe1d65c feat: Add weevil uninstall command
Adds `weevil uninstall` with three modes of operation:
- Full uninstall removes the entire .weevil directory
- --dry-run enumerates managed components and their sizes
- --only N removes specific components by index

Acknowledges system-installed dependencies (Android SDK, Gradle)
in dry-run output so users know what will and won't be touched.
2026-01-31 13:05:20 -06:00
Eric Ratliff
d8e3c54f3d refactor: Remove SDK installation from weevil new
Project creation now requires environment setup first.
Checks system health and directs users to `weevil setup` if needed.
Separates concerns: setup installs, new creates projects.
2026-01-31 10:56:34 -06:00
Eric Ratliff
df7ca091ec feat: Add system diagnostics command
Adds `weevil doctor` to check development environment health.
Reports status of Java, FTC SDK, Android SDK, ADB, and Gradle.
Provides clear next steps based on system state.
2026-01-31 10:56:22 -06:00
Eric Ratliff
4e9575cc4f feat: Add environment setup command
Adds `weevil setup` with two modes:
- System setup: installs default SDKs and dependencies
- Project setup: reads .weevil.toml and installs project requirements

Provides platform-specific installation instructions when auto-install fails.
Never leaves users wondering what to do next.
2026-01-31 10:55:53 -06:00
Eric Ratliff
6b6ba058b7 chore: Release v1.0.0 - First stable release
Update version references from 1.0.0-rc2 to 1.0.0 across documentation.

This marks the first production-ready stable release of Weevil with complete
cross-platform support, robust Windows deployment, and comprehensive project
management features.

Changes:
- Update README.md current version to 1.0.0
- Remove "Next Release" section from ROADMAP.md (now tracking post-1.0 features)

Weevil is now stable and ready for FTC teams to use in production. 🎉
2026-01-26 19:39:11 -06:00
27 changed files with 1978 additions and 241 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

17
CHANGELOG.md Normal file
View File

@@ -0,0 +1,17 @@
# Changelog
## [1.0.0] - 2026-01-27
First stable release! 🎉
### Added
- Complete Windows deployment support
- Android SDK path in project configuration
- Robust cross-platform build and deployment scripts
- Project upgrade command with config migration
- Comprehensive test suite
### Fixed
- Windows APK discovery and deployment
- Batch file path parsing (quote handling)
- ADB integration and error reporting

View File

@@ -1,8 +1,8 @@
[package] [package]
name = "weevil" name = "weevil"
version = "1.0.0" version = "1.1.0-beta.1"
edition = "2021" edition = "2021"
authors = ["Eric Ratliff <eric@intrepidfusion.com>"] authors = ["Eric Ratliff <eric@nxlearn.net>"]
description = "FTC robotics project generator - bores into complexity, emerges with clean code" description = "FTC robotics project generator - bores into complexity, emerges with clean code"
license = "MIT" license = "MIT"

View File

@@ -511,7 +511,7 @@ Built with frustration at unnecessarily complex robotics frameworks, and hope th
## Project Status ## Project Status
**Current Version:** 1.0.0-rc2 **Current Version:** 1.0.0
**What Works:** **What Works:**
- ✅ Project generation - ✅ Project generation

View File

@@ -2,9 +2,6 @@
This document outlines the planned feature development for Weevil across multiple versions. Features are subject to change based on user feedback, technical constraints, and market needs. This document outlines the planned feature development for Weevil across multiple versions. Features are subject to change based on user feedback, technical constraints, and market needs.
**Current Version:** 1.0.0-rc2
**Next Release:** 1.1.0 (Target: TBD)
--- ---
## Version 1.1.0 - Core Stability & Team Adoption ## Version 1.1.0 - Core Stability & Team Adoption

267
src/commands/doctor.rs Normal file
View File

@@ -0,0 +1,267 @@
use anyhow::Result;
use std::path::Path;
use std::process::Command;
use colored::*;
use crate::sdk::SdkConfig;
#[derive(Debug)]
pub struct SystemHealth {
pub java_ok: bool,
pub java_version: Option<String>,
pub ftc_sdk_ok: bool,
pub ftc_sdk_version: Option<String>,
pub android_sdk_ok: bool,
pub adb_ok: bool,
pub adb_version: Option<String>,
pub gradle_ok: bool,
pub gradle_version: Option<String>,
}
impl SystemHealth {
pub fn is_healthy(&self) -> bool {
// Required: Java, FTC SDK, Android SDK
// Optional: ADB in PATH (can be in Android SDK), Gradle (projects have wrapper)
self.java_ok && self.ftc_sdk_ok && self.android_sdk_ok
}
}
/// Run system diagnostics and report health status
pub fn run_diagnostics() -> Result<()> {
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!("{}", " 🩺 Weevil Doctor - System Diagnostics".bright_cyan().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
let health = check_system_health()?;
print_diagnostics(&health);
println!();
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
if health.is_healthy() {
println!("{}", " ✓ System is healthy and ready for FTC development".bright_green().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
println!("{}", "You can now:".bright_yellow().bold());
println!(" - Create a new project: {}", "weevil new <project-name>".bright_cyan());
println!(" - Setup a cloned project: {}", "weevil setup <project-path>".bright_cyan());
} else {
println!("{}", " ⚠ Issues found - setup required".bright_yellow().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
println!("{}", "To fix issues, run:".bright_yellow().bold());
println!(" {}", "weevil setup".bright_cyan());
}
println!();
Ok(())
}
/// Check system health and return a report
pub fn check_system_health() -> Result<SystemHealth> {
let sdk_config = SdkConfig::new()?;
// Check Java
let (java_ok, java_version) = match check_java() {
Ok(version) => (true, Some(version)),
Err(_) => (false, None),
};
// Check FTC SDK
let (ftc_sdk_ok, ftc_sdk_version) = if sdk_config.ftc_sdk_path.exists() {
match crate::sdk::ftc::verify(&sdk_config.ftc_sdk_path) {
Ok(_) => {
let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path)
.unwrap_or_else(|_| "unknown".to_string());
(true, Some(version))
}
Err(_) => (false, None),
}
} else {
(false, None)
};
// Check Android SDK
let android_sdk_ok = if sdk_config.android_sdk_path.exists() {
crate::sdk::android::verify(&sdk_config.android_sdk_path).is_ok()
} else {
false
};
// Check ADB
let (adb_ok, adb_version) = match check_adb(&sdk_config.android_sdk_path) {
Ok(version) => (true, Some(version)),
Err(_) => (false, None),
};
// Check Gradle (optional)
let (gradle_ok, gradle_version) = match check_gradle() {
Ok(version) => (true, Some(version)),
Err(_) => (false, None),
};
Ok(SystemHealth {
java_ok,
java_version,
ftc_sdk_ok,
ftc_sdk_version,
android_sdk_ok,
adb_ok,
adb_version,
gradle_ok,
gradle_version,
})
}
fn print_diagnostics(health: &SystemHealth) {
let sdk_config = SdkConfig::new().unwrap();
println!("{}", "Required Components:".bright_yellow().bold());
println!();
// Java
if health.java_ok {
println!(" {} Java JDK {}",
"".green(),
health.java_version.as_ref().unwrap()
);
} else {
println!(" {} Java JDK {}",
"".red(),
"not found".red()
);
}
// FTC SDK
if health.ftc_sdk_ok {
println!(" {} FTC SDK {} at {}",
"".green(),
health.ftc_sdk_version.as_ref().unwrap(),
sdk_config.ftc_sdk_path.display()
);
} else {
println!(" {} FTC SDK {} (expected at {})",
"".red(),
"not found".red(),
sdk_config.ftc_sdk_path.display()
);
}
// Android SDK
if health.android_sdk_ok {
println!(" {} Android SDK at {}",
"".green(),
sdk_config.android_sdk_path.display()
);
} else {
println!(" {} Android SDK {} (expected at {})",
"".red(),
"not found".red(),
sdk_config.android_sdk_path.display()
);
}
println!();
println!("{}", "Optional Components:".bright_yellow().bold());
println!();
// ADB
if health.adb_ok {
println!(" {} ADB {}",
"".green(),
health.adb_version.as_ref().unwrap()
);
} else {
println!(" {} ADB {}",
"".yellow(),
"not in PATH (included in Android SDK)".yellow()
);
}
// Gradle
if health.gradle_ok {
println!(" {} Gradle {}",
"".green(),
health.gradle_version.as_ref().unwrap()
);
} else {
println!(" {} Gradle {}",
"".yellow(),
"not in PATH (projects include wrapper)".yellow()
);
}
}
fn check_java() -> Result<String> {
let output = Command::new("java")
.arg("-version")
.output();
match output {
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
for line in stderr.lines() {
if line.contains("version") {
if let Some(version_str) = line.split('"').nth(1) {
return Ok(version_str.to_string());
}
}
}
Ok("installed (version unknown)".to_string())
}
Err(_) => anyhow::bail!("Java JDK not found in PATH"),
}
}
fn check_adb(android_sdk_path: &Path) -> Result<String> {
// First try system PATH
let output = Command::new("adb")
.arg("version")
.output();
if let Ok(out) = output {
if out.status.success() {
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.starts_with("Android Debug Bridge version") {
return Ok(line.replace("Android Debug Bridge version ", ""));
}
}
return Ok("installed (version unknown)".to_string());
}
}
// Try Android SDK location
let adb_path = if cfg!(target_os = "windows") {
android_sdk_path.join("platform-tools").join("adb.exe")
} else {
android_sdk_path.join("platform-tools").join("adb")
};
if adb_path.exists() {
anyhow::bail!("ADB found in Android SDK but not in PATH")
} else {
anyhow::bail!("ADB not found")
}
}
fn check_gradle() -> Result<String> {
let output = Command::new("gradle")
.arg("--version")
.output();
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.starts_with("Gradle") {
return Ok(line.replace("Gradle ", ""));
}
}
Ok("installed (version unknown)".to_string())
}
Err(_) => anyhow::bail!("Gradle not found in PATH"),
}
}

View File

@@ -3,3 +3,6 @@ pub mod upgrade;
pub mod deploy; pub mod deploy;
pub mod sdk; pub mod sdk;
pub mod config; pub mod config;
pub mod setup;
pub mod doctor;
pub mod uninstall;

View File

@@ -34,14 +34,47 @@ pub fn create_project(
println!("{}", format!("Creating FTC project: {}", name).bright_green().bold()); println!("{}", format!("Creating FTC project: {}", name).bright_green().bold());
println!(); println!();
// Check system health FIRST
println!("{}", "Checking system prerequisites...".bright_yellow());
let health = crate::commands::doctor::check_system_health()?;
if !health.is_healthy() {
println!();
println!("{}", "═══════════════════════════════════════════════════════════".bright_red());
println!("{}", " ✗ System Setup Required".bright_red().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_red());
println!();
println!("{}", "Missing required components:".bright_yellow().bold());
if !health.java_ok {
println!(" {} Java JDK", "".red());
}
if !health.ftc_sdk_ok {
println!(" {} FTC SDK", "".red());
}
if !health.android_sdk_ok {
println!(" {} Android SDK", "".red());
}
println!();
println!("{}", "Before creating a project, you need to set up your development environment.".bright_yellow());
println!();
println!("{}", "Run this command to install required components:".bright_yellow().bold());
println!(" {}", "weevil setup".bright_cyan());
println!();
println!("{}", "Then try creating your project again:".bright_yellow().bold());
println!(" {}", format!("weevil new {}", name).bright_cyan());
println!();
bail!("System setup required");
}
println!("{} All prerequisites met", "".green());
println!();
// Setup or verify SDK configuration // Setup or verify SDK configuration
let sdk_config = SdkConfig::with_paths(ftc_sdk, android_sdk)?; let sdk_config = SdkConfig::with_paths(ftc_sdk, android_sdk)?;
// Install SDKs if needed
println!("{}", "Checking SDKs...".bright_yellow());
ensure_sdks(&sdk_config)?;
println!();
println!("{}", "Creating project structure...".bright_yellow()); println!("{}", "Creating project structure...".bright_yellow());
// Build the project // Build the project
@@ -57,34 +90,12 @@ pub fn create_project(
println!("Version: {}", crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path).unwrap_or_else(|_| "unknown".to_string())); println!("Version: {}", crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path).unwrap_or_else(|_| "unknown".to_string()));
println!(); println!();
println!("{}", "Next steps:".bright_yellow().bold()); println!("{}", "Next steps:".bright_yellow().bold());
println!(" 1. cd {}", name); println!(" 1. {}", format!("cd {}", name).bright_cyan());
println!(" 2. Review README.md for project structure"); println!(" 2. Review README.md for project structure");
println!(" 3. Start coding in src/main/java/robot/"); println!(" 3. Start coding in src/main/java/robot/");
println!(" 4. Run: ./gradlew test"); println!(" 4. Run tests: {}", "./gradlew test".bright_cyan());
println!(" 5. Deploy: weevil deploy {}", name); println!(" 5. Deploy to robot: {}", format!("weevil deploy {}", name).bright_cyan());
println!(); println!();
Ok(()) Ok(())
} }
fn ensure_sdks(config: &SdkConfig) -> Result<()> {
// Check FTC SDK
if !config.ftc_sdk_path.exists() {
println!("FTC SDK not found. Installing...");
crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path)?;
} else {
println!("{} FTC SDK found at: {}", "".green(), config.ftc_sdk_path.display());
crate::sdk::ftc::verify(&config.ftc_sdk_path)?;
}
// Check Android SDK
if !config.android_sdk_path.exists() {
println!("Android SDK not found. Installing...");
crate::sdk::android::install(&config.android_sdk_path)?;
} else {
println!("{} Android SDK found at: {}", "".green(), config.android_sdk_path.display());
crate::sdk::android::verify(&config.android_sdk_path)?;
}
Ok(())
}

514
src/commands/setup.rs Normal file
View File

@@ -0,0 +1,514 @@
use anyhow::{Result, Context, bail};
use std::path::{Path, PathBuf};
use std::process::Command;
use colored::*;
use crate::sdk::SdkConfig;
use crate::project::ProjectConfig;
/// Setup development environment - either system-wide or for a specific project
pub fn setup_environment(project_path: Option<&str>) -> Result<()> {
match project_path {
Some(path) => setup_project(path),
None => setup_system(),
}
}
/// Setup system-wide development environment with default SDKs
fn setup_system() -> Result<()> {
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!("{}", " System Setup - Preparing FTC Development Environment".bright_cyan().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
let mut issues = Vec::new();
let mut installed = Vec::new();
// Check and install SDKs
let sdk_config = SdkConfig::new()?;
// 1. Check Java
println!("{}", "Checking Java JDK...".bright_yellow());
match check_java() {
Ok(version) => {
println!("{} Java JDK {} found", "".green(), version);
installed.push(format!("Java JDK {}", version));
}
Err(e) => {
println!("{} {}", "".red(), e);
issues.push(("Java JDK", get_java_install_instructions()));
}
}
println!();
// 2. Check/Install FTC SDK
println!("{}", "Checking FTC SDK...".bright_yellow());
if sdk_config.ftc_sdk_path.exists() {
match crate::sdk::ftc::verify(&sdk_config.ftc_sdk_path) {
Ok(_) => {
let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path)
.unwrap_or_else(|_| "unknown".to_string());
println!("{} FTC SDK {} found at: {}",
"".green(),
version,
sdk_config.ftc_sdk_path.display()
);
installed.push(format!("FTC SDK {}", version));
}
Err(_) => {
println!("{} FTC SDK found but incomplete, reinstalling...", "".yellow());
crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path)?;
let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path)
.unwrap_or_else(|_| "unknown".to_string());
installed.push(format!("FTC SDK {} (installed)", version));
}
}
} else {
println!("FTC SDK not found. Installing...");
crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path)?;
let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path)
.unwrap_or_else(|_| "unknown".to_string());
installed.push(format!("FTC SDK {} (installed)", version));
}
println!();
// 3. Check/Install Android SDK
println!("{}", "Checking Android SDK...".bright_yellow());
if sdk_config.android_sdk_path.exists() {
match crate::sdk::android::verify(&sdk_config.android_sdk_path) {
Ok(_) => {
println!("{} Android SDK found at: {}",
"".green(),
sdk_config.android_sdk_path.display()
);
installed.push("Android SDK".to_string());
}
Err(_) => {
println!("{} Android SDK found but incomplete, reinstalling...", "".yellow());
crate::sdk::android::install(&sdk_config.android_sdk_path)?;
installed.push("Android SDK (installed)".to_string());
}
}
} else {
println!("Android SDK not found. Installing...");
crate::sdk::android::install(&sdk_config.android_sdk_path)?;
installed.push("Android SDK (installed)".to_string());
}
println!();
// 4. Check ADB
println!("{}", "Checking ADB (Android Debug Bridge)...".bright_yellow());
match check_adb(&sdk_config.android_sdk_path) {
Ok(version) => {
println!("{} ADB {} found", "".green(), version);
installed.push(format!("ADB {}", version));
}
Err(e) => {
println!("{} {}", "".yellow(), e);
println!(" ADB is included in Android SDK platform-tools");
println!(" Add to PATH: {}", sdk_config.android_sdk_path.join("platform-tools").display());
}
}
println!();
// 5. Check Gradle
println!("{}", "Checking Gradle...".bright_yellow());
match check_gradle() {
Ok(version) => {
println!("{} Gradle {} found", "".green(), version);
installed.push(format!("Gradle {}", version));
}
Err(e) => {
println!("{} {}", "".yellow(), e);
println!(" Note: Weevil projects include Gradle wrapper, so this is optional");
}
}
println!();
// Print summary
print_system_summary(&installed, &issues, &sdk_config);
Ok(())
}
/// Setup dependencies for a specific project by reading its .weevil.toml
fn setup_project(project_path: &str) -> Result<()> {
let project_path = PathBuf::from(project_path);
if !project_path.exists() {
bail!("Project directory not found: {}", project_path.display());
}
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!("{}", " Project Setup - Installing Dependencies".bright_cyan().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
// Load project configuration
println!("{}", "Reading project configuration...".bright_yellow());
let config = ProjectConfig::load(&project_path)
.context("Failed to load .weevil.toml")?;
println!();
println!("{}", "Project Configuration:".bright_yellow().bold());
println!(" Project: {}", config.project_name.bright_white());
println!(" FTC SDK: {} ({})",
config.ftc_sdk_version.bright_white(),
config.ftc_sdk_path.display()
);
println!(" Android SDK: {}", config.android_sdk_path.display());
println!();
let mut installed = Vec::new();
let mut issues = Vec::new();
// 1. Check Java
println!("{}", "Checking Java JDK...".bright_yellow());
match check_java() {
Ok(version) => {
println!("{} Java JDK {} found", "".green(), version);
installed.push(format!("Java JDK {}", version));
}
Err(e) => {
println!("{} {}", "".red(), e);
issues.push(("Java JDK", get_java_install_instructions()));
}
}
println!();
// 2. Check/Install project-specific FTC SDK
println!("{}", format!("Checking FTC SDK {}...", config.ftc_sdk_version).bright_yellow());
if config.ftc_sdk_path.exists() {
match crate::sdk::ftc::verify(&config.ftc_sdk_path) {
Ok(_) => {
println!("{} FTC SDK {} found at: {}",
"".green(),
config.ftc_sdk_version,
config.ftc_sdk_path.display()
);
installed.push(format!("FTC SDK {}", config.ftc_sdk_version));
}
Err(_) => {
println!("{} FTC SDK path exists but is invalid", "".red());
println!(" Expected at: {}", config.ftc_sdk_path.display());
println!();
println!("{}", "Solution:".bright_yellow().bold());
println!(" The .weevil.toml specifies an FTC SDK location that doesn't exist or is incomplete.");
println!(" You have two options:");
println!();
println!(" 1. Update the project to use a different SDK:");
println!(" weevil config {} --set-sdk <path-to-sdk>", project_path.display());
println!();
println!(" 2. Install the SDK at the expected location:");
println!(" # Clone FTC SDK to the expected path");
println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git \\");
println!(" {}", config.ftc_sdk_path.display());
println!(" cd {}", config.ftc_sdk_path.display());
println!(" git checkout {}", config.ftc_sdk_version);
bail!("FTC SDK verification failed");
}
}
} else {
println!("{} FTC SDK not found at: {}", "".red(), config.ftc_sdk_path.display());
println!();
// Try to install it automatically
println!("{}", "Attempting automatic installation...".bright_yellow());
match crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path) {
Ok(_) => {
println!("{} FTC SDK {} installed successfully",
"".green(),
config.ftc_sdk_version
);
installed.push(format!("FTC SDK {} (installed)", config.ftc_sdk_version));
}
Err(e) => {
println!("{} Automatic installation failed: {}", "".red(), e);
println!();
println!("{}", "Manual Installation Required:".bright_yellow().bold());
println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git \\");
println!(" {}", config.ftc_sdk_path.display());
println!(" cd {}", config.ftc_sdk_path.display());
println!(" git checkout {}", config.ftc_sdk_version);
bail!("FTC SDK installation failed");
}
}
}
println!();
// 3. Check/Install Android SDK
println!("{}", "Checking Android SDK...".bright_yellow());
if config.android_sdk_path.exists() {
match crate::sdk::android::verify(&config.android_sdk_path) {
Ok(_) => {
println!("{} Android SDK found at: {}",
"".green(),
config.android_sdk_path.display()
);
installed.push("Android SDK".to_string());
}
Err(_) => {
println!("{} Android SDK found but incomplete, reinstalling...", "".yellow());
crate::sdk::android::install(&config.android_sdk_path)?;
installed.push("Android SDK (installed)".to_string());
}
}
} else {
println!("Android SDK not found. Installing...");
crate::sdk::android::install(&config.android_sdk_path)?;
installed.push("Android SDK (installed)".to_string());
}
println!();
// 4. Check ADB
println!("{}", "Checking ADB...".bright_yellow());
match check_adb(&config.android_sdk_path) {
Ok(version) => {
println!("{} ADB {} found", "".green(), version);
installed.push(format!("ADB {}", version));
}
Err(e) => {
println!("{} {}", "".yellow(), e);
println!(" Add to PATH: {}", config.android_sdk_path.join("platform-tools").display());
}
}
println!();
// 5. Check Gradle wrapper in project
println!("{}", "Checking Gradle wrapper...".bright_yellow());
let gradlew = if cfg!(target_os = "windows") {
project_path.join("gradlew.bat")
} else {
project_path.join("gradlew")
};
if gradlew.exists() {
println!("{} Gradle wrapper found in project", "".green());
installed.push("Gradle wrapper".to_string());
} else {
println!("{} Gradle wrapper not found in project", "".yellow());
println!(" Run 'weevil upgrade {}' to regenerate project files", project_path.display());
}
println!();
// Print summary
print_project_summary(&installed, &issues, &config, &project_path);
Ok(())
}
fn check_java() -> Result<String> {
let output = Command::new("java")
.arg("-version")
.output();
match output {
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
// Java version is typically in stderr, format: java version "11.0.x" or openjdk version "11.0.x"
for line in stderr.lines() {
if line.contains("version") {
if let Some(version_str) = line.split('"').nth(1) {
return Ok(version_str.to_string());
}
}
}
Ok("installed (version unknown)".to_string())
}
Err(_) => bail!("Java JDK not found in PATH"),
}
}
fn check_adb(android_sdk_path: &Path) -> Result<String> {
// First try system PATH
let output = Command::new("adb")
.arg("version")
.output();
if let Ok(out) = output {
if out.status.success() {
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.starts_with("Android Debug Bridge version") {
return Ok(line.replace("Android Debug Bridge version ", ""));
}
}
return Ok("installed (version unknown)".to_string());
}
}
// Try Android SDK location
let adb_path = if cfg!(target_os = "windows") {
android_sdk_path.join("platform-tools").join("adb.exe")
} else {
android_sdk_path.join("platform-tools").join("adb")
};
if adb_path.exists() {
bail!("ADB found in Android SDK but not in PATH")
} else {
bail!("ADB not found")
}
}
fn check_gradle() -> Result<String> {
let output = Command::new("gradle")
.arg("--version")
.output();
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.starts_with("Gradle") {
return Ok(line.replace("Gradle ", ""));
}
}
Ok("installed (version unknown)".to_string())
}
Err(_) => bail!("Gradle not found in PATH (optional)"),
}
}
fn get_java_install_instructions() -> String {
if cfg!(target_os = "windows") {
format!(
"Java JDK is required but not found.\n\
\n\
To install Java 11 on Windows:\n\
\n\
1. Download from: {}\n\
2. Run the installer\n\
3. Add Java to your PATH (installer usually does this)\n\
4. Run 'weevil setup' again to verify\n\
\n\
Verify installation: java -version",
"https://adoptium.net/temurin/releases/?version=11".bright_white()
)
} else if cfg!(target_os = "macos") {
format!(
"Java JDK is required but not found.\n\
\n\
To install Java 11 on macOS:\n\
\n\
Using Homebrew (recommended):\n\
{}\n\
\n\
Or download from: {}\n\
\n\
Verify installation: java -version",
" brew install openjdk@11".bright_white(),
"https://adoptium.net/temurin/releases/?version=11".bright_white()
)
} else {
format!(
"Java JDK is required but not found.\n\
\n\
To install Java 11 on Ubuntu/Debian:\n\
{}\n\
{}\n\
\n\
To install on Fedora/RHEL:\n\
{}\n\
\n\
Verify installation: java -version",
" sudo apt update".bright_white(),
" sudo apt install openjdk-11-jdk".bright_white(),
" sudo dnf install java-11-openjdk-devel".bright_white()
)
}
}
fn print_system_summary(installed: &[String], issues: &[(&str, String)], sdk_config: &SdkConfig) {
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
println!("{}", " System Setup Summary".bright_green().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
println!();
if !installed.is_empty() {
println!("{}", "Installed Components:".bright_green().bold());
for component in installed {
println!(" {} {}", "".green(), component);
}
println!();
}
if !issues.is_empty() {
println!("{}", "Manual Installation Required:".bright_yellow().bold());
println!();
for (name, instructions) in issues {
println!("{} {}", "".red(), name.red().bold());
println!();
for line in instructions.lines() {
println!(" {}", line);
}
println!();
}
}
println!("{}", "SDK Locations:".bright_cyan().bold());
println!(" FTC SDK: {}", sdk_config.ftc_sdk_path.display());
println!(" Android SDK: {}", sdk_config.android_sdk_path.display());
println!(" Cache: {}", sdk_config.cache_dir.display());
println!();
if issues.is_empty() {
println!("{}", "✓ System is ready for FTC development!".bright_green().bold());
println!();
println!("{}", "Next steps:".bright_yellow().bold());
println!(" Create a new project: {}", "weevil new my-robot".bright_white());
println!(" Clone existing project: {}", "git clone <repo> && cd <repo> && weevil setup .".bright_white());
} else {
println!("{}", "⚠ Please install the required components listed above".bright_yellow().bold());
println!(" Then run {} to verify", "weevil setup".bright_white());
}
println!();
}
fn print_project_summary(installed: &[String], issues: &[(&str, String)], config: &ProjectConfig, project_path: &Path) {
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
println!("{}", " Project Setup Summary".bright_green().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
println!();
println!("{}", "Project Details:".bright_cyan().bold());
println!(" Name: {}", config.project_name);
println!(" Location: {}", project_path.display());
println!(" FTC SDK: {} at {}", config.ftc_sdk_version, config.ftc_sdk_path.display());
println!();
if !installed.is_empty() {
println!("{}", "Installed Components:".bright_green().bold());
for component in installed {
println!(" {} {}", "".green(), component);
}
println!();
}
if !issues.is_empty() {
println!("{}", "Manual Installation Required:".bright_yellow().bold());
println!();
for (name, instructions) in issues {
println!("{} {}", "".red(), name.red().bold());
println!();
for line in instructions.lines() {
println!(" {}", line);
}
println!();
}
}
if issues.is_empty() {
println!("{}", "✓ Project is ready for development!".bright_green().bold());
println!();
println!("{}", "Next steps:".bright_yellow().bold());
println!(" 1. Review the code: {}", format!("cd {}", project_path.display()).bright_white());
println!(" 2. Run tests: {}", "./gradlew test".bright_white());
println!(" 3. Build: {}", "./build.sh (or build.bat on Windows)".bright_white());
println!(" 4. Deploy to robot: {}", format!("weevil deploy {}", project_path.display()).bright_white());
} else {
println!("{}", "⚠ Please install the required components listed above".bright_yellow().bold());
println!(" Then run {} to verify", format!("weevil setup {}", project_path.display()).bright_white());
}
println!();
}

394
src/commands/uninstall.rs Normal file
View File

@@ -0,0 +1,394 @@
use anyhow::Result;
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
use colored::*;
use crate::sdk::SdkConfig;
#[derive(Debug, Clone)]
enum RemoveTarget {
FtcSdk(PathBuf, String), // path, version label
AndroidSdk(PathBuf),
}
impl RemoveTarget {
fn label(&self) -> String {
match self {
RemoveTarget::FtcSdk(_, version) => format!("FTC SDK {}", version),
RemoveTarget::AndroidSdk(_) => "Android SDK".to_string(),
}
}
fn path(&self) -> &PathBuf {
match self {
RemoveTarget::FtcSdk(path, _) => path,
RemoveTarget::AndroidSdk(path) => path,
}
}
fn size(&self) -> u64 {
dir_size(self.path())
}
}
/// Uninstall Weevil-managed dependencies
///
/// - No args: removes ~/.weevil entirely
/// - --dry-run: shows what would be removed
/// - --only N [N ...]: selective removal of specific components
pub fn uninstall_dependencies(dry_run: bool, targets: Option<Vec<usize>>) -> Result<()> {
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!("{}", " 🗑️ Weevil Uninstall - Remove Dependencies".bright_cyan().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
let sdk_config = SdkConfig::new()?;
// No --only flag: full uninstall, just nuke .weevil
if targets.is_none() {
return full_uninstall(&sdk_config, dry_run);
}
// --only flag: selective removal
let all_targets = scan_targets(&sdk_config);
if all_targets.is_empty() {
println!("{}", "No Weevil-managed components found.".bright_green());
println!();
return Ok(());
}
// Show numbered list
println!("{}", "Found Weevil-managed components:".bright_yellow().bold());
println!();
for (i, target) in all_targets.iter().enumerate() {
println!(" {}. {}{}",
(i + 1).to_string().bright_cyan().bold(),
target.label(),
format!("{} at {}", format_size(target.size()), target.path().display()).dimmed()
);
}
println!();
// Resolve selected indices
let indices = targets.unwrap();
let mut selected = Vec::new();
for idx in indices {
if idx == 0 || idx > all_targets.len() {
println!("{} Invalid selection: {}. Valid range is 1{}",
"".red(), idx, all_targets.len());
return Ok(());
}
selected.push(all_targets[idx - 1].clone());
}
if dry_run {
print_dry_run(&selected);
return Ok(());
}
print_removal_list(&selected);
if !confirm()? {
return Ok(());
}
execute_removal(&selected);
Ok(())
}
/// Full uninstall — removes the entire .weevil directory
fn full_uninstall(sdk_config: &SdkConfig, dry_run: bool) -> Result<()> {
let all_targets = scan_targets(sdk_config);
if all_targets.is_empty() {
println!("{}", "No Weevil-managed components found.".bright_green());
println!();
return Ok(());
}
let size = dir_size(&sdk_config.cache_dir);
if dry_run {
println!("{}", "── Dry Run ─────────────────────────────────────────────────".bright_yellow().bold());
println!();
println!("{}", format!("Contents of {}:", sdk_config.cache_dir.display()).bright_yellow().bold());
println!();
for (i, target) in all_targets.iter().enumerate() {
println!(" {}. {}{}",
(i + 1).to_string().bright_cyan().bold(),
target.label(),
format!("{} at {}", format_size(target.size()), target.path().display()).dimmed()
);
}
// Note any system-installed dependencies that Weevil doesn't manage
let mut has_external = false;
if sdk_config.android_sdk_path.exists()
&& !sdk_config.android_sdk_path.to_string_lossy().contains(".weevil") {
if !has_external {
println!();
has_external = true;
}
println!(" {} Android SDK at {} — not managed by Weevil, will not be removed",
"".bright_cyan(),
sdk_config.android_sdk_path.display()
);
}
if let Ok(gradle_version) = check_gradle() {
if !has_external {
println!();
}
println!(" {} Gradle {} — not managed by Weevil, will not be removed",
"".bright_cyan(),
gradle_version
);
}
println!();
println!("{}", format!("Total: {} ({})", sdk_config.cache_dir.display(), format_size(size)).bright_yellow().bold());
println!();
println!("{}", "To remove everything:".bright_yellow().bold());
println!(" {}", "weevil uninstall".bright_cyan());
println!();
println!("{}", "To remove specific items:".bright_yellow().bold());
println!(" {}", "weevil uninstall --only 1 2".bright_cyan());
println!();
return Ok(());
}
println!("{}", "This will permanently remove:".bright_yellow().bold());
println!();
println!(" {} {} ({})", "".red(), sdk_config.cache_dir.display(), format_size(size));
println!();
println!("{}", "Everything Weevil installed will be gone.".bright_yellow());
println!();
if !confirm()? {
return Ok(());
}
println!();
print!(" Removing {} ... ", sdk_config.cache_dir.display());
match fs::remove_dir_all(&sdk_config.cache_dir) {
Ok(_) => {
println!("{}", "".green());
println!();
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!("{}", " ✓ Uninstall Complete".bright_green().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
println!("{}", "Weevil binary is still installed. To remove it, delete the weevil executable.".bright_yellow());
println!();
println!("{}", "To reinstall dependencies later:".bright_yellow().bold());
println!(" {}", "weevil setup".bright_cyan());
}
Err(e) => {
println!("{} ({})", "".red(), e);
println!();
println!("{}", "You may need to manually remove this directory.".bright_yellow());
}
}
println!();
Ok(())
}
fn print_dry_run(selected: &[RemoveTarget]) {
println!("{}", "── Dry Run ─────────────────────────────────────────────────".bright_yellow().bold());
println!();
println!("{}", "The following would be removed:".bright_yellow());
println!();
let mut total: u64 = 0;
for target in selected {
let size = target.size();
total += size;
println!(" {} {} ({})", "".red(), target.label(), format_size(size));
println!(" {}", target.path().display().to_string().dimmed());
}
println!();
println!("{}", format!("Total: {}", format_size(total)).bright_yellow().bold());
println!();
println!("{}", "Run without --dry-run to actually remove these components.".dimmed());
println!();
}
fn print_removal_list(selected: &[RemoveTarget]) {
println!("{}", "The following will be removed:".bright_yellow().bold());
println!();
let mut total: u64 = 0;
for target in selected {
let size = target.size();
total += size;
println!(" {} {} ({})", "".red(), target.label(), format_size(size));
println!(" {}", target.path().display().to_string().dimmed());
}
println!();
println!("{}", format!("Total: {}", format_size(total)).bright_yellow().bold());
println!();
}
fn confirm() -> Result<bool> {
print!("{}", "Are you sure you want to continue? (y/N): ".bright_yellow());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let answer = input.trim().to_lowercase();
if answer != "y" && answer != "yes" {
println!();
println!("{}", "Uninstall cancelled.".bright_green());
println!();
return Ok(false);
}
Ok(true)
}
fn execute_removal(selected: &[RemoveTarget]) {
println!();
println!("{}", "Removing components...".bright_yellow());
println!();
let mut removed = Vec::new();
let mut failed = Vec::new();
for target in selected {
print!(" Removing {}... ", target.label());
match fs::remove_dir_all(target.path()) {
Ok(_) => {
println!("{}", "".green());
removed.push(target.clone());
}
Err(e) => {
println!("{} ({})", "".red(), e);
failed.push((target.clone(), e.to_string()));
}
}
}
println!();
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
if failed.is_empty() {
println!("{}", " ✓ Uninstall Complete".bright_green().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
println!("{}", "Removed:".bright_green().bold());
for target in &removed {
println!(" {} {}", "".green(), target.label());
}
} else {
println!("{}", " ⚠ Uninstall Completed with Errors".bright_yellow().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
if !removed.is_empty() {
println!("{}", "Removed:".bright_green().bold());
for target in &removed {
println!(" {} {}", "".green(), target.label());
}
println!();
}
println!("{}", "Failed to remove:".bright_red().bold());
for (target, error) in &failed {
println!(" {} {}: {}", "".red(), target.label(), error);
}
println!();
println!("{}", "You may need to manually remove these directories.".bright_yellow());
}
println!();
println!("{}", "To reinstall dependencies later:".bright_yellow().bold());
println!(" {}", "weevil setup".bright_cyan());
println!();
}
/// Scan the cache directory for individual removable components (used by --only)
fn scan_targets(sdk_config: &SdkConfig) -> Vec<RemoveTarget> {
let mut targets = Vec::new();
if sdk_config.cache_dir.exists() {
if let Ok(entries) = fs::read_dir(&sdk_config.cache_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
let path = entry.path();
if !path.is_dir() {
continue;
}
if name.starts_with("ftc-sdk") {
let version = crate::sdk::ftc::get_version(&path)
.unwrap_or_else(|_| {
if name == "ftc-sdk" {
"default".to_string()
} else {
name.trim_start_matches("ftc-sdk-").to_string()
}
});
targets.push(RemoveTarget::FtcSdk(path, version));
}
}
}
}
// Android SDK — only if Weevil installed it (lives inside .weevil)
if sdk_config.android_sdk_path.exists()
&& sdk_config.android_sdk_path.to_string_lossy().contains(".weevil") {
targets.push(RemoveTarget::AndroidSdk(sdk_config.android_sdk_path.clone()));
}
targets
}
fn check_gradle() -> Result<String> {
let output = std::process::Command::new("gradle")
.arg("--version")
.output();
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.starts_with("Gradle") {
return Ok(line.replace("Gradle ", ""));
}
}
Ok("installed (version unknown)".to_string())
}
Err(_) => anyhow::bail!("Gradle not found"),
}
}
fn dir_size(path: &PathBuf) -> u64 {
let mut size: u64 = 0;
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
size += dir_size(&path);
} else if let Ok(metadata) = path.metadata() {
size += metadata.len();
}
}
}
size
}
fn format_size(bytes: u64) -> String {
if bytes >= 1_073_741_824 {
format!("{:.1} GB", bytes as f64 / 1_073_741_824.0)
} else if bytes >= 1_048_576 {
format!("{:.1} MB", bytes as f64 / 1_048_576.0)
} else if bytes >= 1_024 {
format!("{:.1} KB", bytes as f64 / 1_024.0)
} else {
format!("{} B", bytes)
}
}

View File

@@ -1,6 +1,7 @@
// File: src/lib.rs // File: src/lib.rs
// Library interface for testing // Library interface for testing
pub mod version;
pub mod sdk; pub mod sdk;
pub mod project; pub mod project;
pub mod commands; pub mod commands;

View File

@@ -1,6 +1,7 @@
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use colored::*; use colored::*;
use anyhow::Result; use anyhow::Result;
use weevil::version::WEEVIL_VERSION;
mod commands; mod commands;
mod sdk; mod sdk;
@@ -9,9 +10,12 @@ mod templates;
#[derive(Parser)] #[derive(Parser)]
#[command(name = "weevil")] #[command(name = "weevil")]
#[command(author = "Eric Barch <eric@intrepidfusion.com>")] #[command(author = "Eric Ratliff <eric@nxlearn.net>")]
#[command(version = "1.0.0")] #[command(version = WEEVIL_VERSION)]
#[command(about = "FTC robotics project generator - bores into complexity, emerges with clean code", long_about = None)] #[command(
about = "FTC robotics project generator - bores into complexity, emerges with clean code",
long_about = None
)]
struct Cli { struct Cli {
#[command(subcommand)] #[command(subcommand)]
command: Commands, command: Commands,
@@ -33,6 +37,26 @@ enum Commands {
android_sdk: Option<String>, android_sdk: Option<String>,
}, },
/// Check system health and diagnose issues
Doctor,
/// Setup development environment (system or project)
Setup {
/// Path to project directory (optional - without it, sets up system)
path: Option<String>,
},
/// Remove Weevil-installed SDKs and dependencies
Uninstall {
/// Show what would be removed without actually removing anything
#[arg(long)]
dry_run: bool,
/// Remove only specific items by number (use --dry-run first to see the list)
#[arg(long, value_name = "NUM", num_args = 1..)]
only: Option<Vec<usize>>,
},
/// Upgrade an existing project to the latest generator version /// Upgrade an existing project to the latest generator version
Upgrade { Upgrade {
/// Path to the project directory /// Path to the project directory
@@ -99,19 +123,26 @@ fn main() -> Result<()> {
Commands::New { name, ftc_sdk, android_sdk } => { Commands::New { name, ftc_sdk, android_sdk } => {
commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref()) commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref())
} }
Commands::Doctor => {
commands::doctor::run_diagnostics()
}
Commands::Setup { path } => {
commands::setup::setup_environment(path.as_deref())
}
Commands::Uninstall { dry_run, only } => {
commands::uninstall::uninstall_dependencies(dry_run, only)
}
Commands::Upgrade { path } => { Commands::Upgrade { path } => {
commands::upgrade::upgrade_project(&path) commands::upgrade::upgrade_project(&path)
} }
Commands::Deploy { path, usb, wifi, ip } => { Commands::Deploy { path, usb, wifi, ip } => {
commands::deploy::deploy_project(&path, usb, wifi, ip.as_deref()) commands::deploy::deploy_project(&path, usb, wifi, ip.as_deref())
} }
Commands::Sdk { command } => { Commands::Sdk { command } => match command {
match command {
SdkCommands::Install => commands::sdk::install_sdks(), SdkCommands::Install => commands::sdk::install_sdks(),
SdkCommands::Status => commands::sdk::show_status(), SdkCommands::Status => commands::sdk::show_status(),
SdkCommands::Update => commands::sdk::update_sdks(), SdkCommands::Update => commands::sdk::update_sdks(),
} },
}
Commands::Config { path, set_sdk } => { Commands::Config { path, set_sdk } => {
if let Some(sdk_path) = set_sdk { if let Some(sdk_path) = set_sdk {
commands::config::set_sdk(&path, &sdk_path) commands::config::set_sdk(&path, &sdk_path)
@@ -124,7 +155,12 @@ fn main() -> Result<()> {
fn print_banner() { fn print_banner() {
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!("{}", " 🪲 Weevil - FTC Project Generator v1.0.0".bright_cyan().bold()); println!(
"{}",
format!(" 🪲 Weevil - FTC Project Generator v{}", WEEVIL_VERSION)
.bright_cyan()
.bold()
);
println!("{}", " Nexus Workshops LLC".bright_cyan()); println!("{}", " Nexus Workshops LLC".bright_cyan());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!(); println!();

View File

@@ -3,6 +3,8 @@ use std::path::{Path, PathBuf};
use std::fs; use std::fs;
use anyhow::{Result, Context, bail}; use anyhow::{Result, Context, bail};
const WEEVIL_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ProjectConfig { pub struct ProjectConfig {
pub project_name: String, pub project_name: String,
@@ -24,7 +26,7 @@ impl ProjectConfig {
Ok(Self { Ok(Self {
project_name: project_name.to_string(), project_name: project_name.to_string(),
weevil_version: "1.0.0".to_string(), weevil_version: WEEVIL_VERSION.to_string(),
ftc_sdk_path, ftc_sdk_path,
ftc_sdk_version, ftc_sdk_version,
android_sdk_path, android_sdk_path,

View File

@@ -7,6 +7,8 @@ use git2::Repository;
use crate::sdk::SdkConfig; use crate::sdk::SdkConfig;
const WEEVIL_VERSION: &str = env!("CARGO_PKG_VERSION");
pub mod deployer; pub mod deployer;
pub mod config; pub mod config;
@@ -68,7 +70,7 @@ impl ProjectBuilder {
let mut _context = TeraContext::new(); let mut _context = TeraContext::new();
_context.insert("project_name", &self.name); _context.insert("project_name", &self.name);
_context.insert("sdk_dir", &sdk_config.ftc_sdk_path.to_string_lossy()); _context.insert("sdk_dir", &sdk_config.ftc_sdk_path.to_string_lossy());
_context.insert("generator_version", "1.0.0"); _context.insert("generator_version", WEEVIL_VERSION);
self.create_project_files(project_path, sdk_config)?; self.create_project_files(project_path, sdk_config)?;
@@ -84,7 +86,7 @@ impl ProjectBuilder {
let readme = format!( let readme = format!(
r#"# {} r#"# {}
FTC Robot Project generated by Weevil v1.0.0 FTC Robot Project generated by Weevil v{}
## Quick Start ## Quick Start
```bash ```bash
@@ -111,7 +113,7 @@ deploy.bat
2. Test locally: `./gradlew test` 2. Test locally: `./gradlew test`
3. Deploy: `./deploy.sh` (or `deploy.bat` on Windows) 3. Deploy: `./deploy.sh` (or `deploy.bat` on Windows)
"#, "#,
self.name self.name, WEEVIL_VERSION
); );
fs::write(project_path.join("README.md"), readme)?; fs::write(project_path.join("README.md"), readme)?;
@@ -120,7 +122,7 @@ deploy.bat
fs::write(project_path.join(".gitignore"), gitignore)?; fs::write(project_path.join(".gitignore"), gitignore)?;
// Version marker // Version marker
fs::write(project_path.join(".weevil-version"), "1.0.0")?; fs::write(project_path.join(".weevil-version"), WEEVIL_VERSION)?;
// build.gradle.kts - Pure Java with deployToSDK task // build.gradle.kts - Pure Java with deployToSDK task
// Escape backslashes for Windows paths in Kotlin strings // Escape backslashes for Windows paths in Kotlin strings

View File

@@ -15,15 +15,26 @@ pub struct SdkConfig {
impl SdkConfig { impl SdkConfig {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
// Allow tests (or power users) to override the cache 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() let home = dirs::home_dir()
.context("Could not determine home directory")?; .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,
}) })
} }

1
src/version.rs Normal file
View File

@@ -0,0 +1 @@
pub const WEEVIL_VERSION: &str = env!("CARGO_PKG_VERSION");

3
tests/common.rs Normal file
View File

@@ -0,0 +1,3 @@
// Intentionally hardcoded. When you bump the version in Cargo.toml,
// tests will fail here until you update this to match.
pub const EXPECTED_VERSION: &str = "1.1.0-beta.1";

View File

View File

@@ -1,8 +1,17 @@
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 = "common.rs"]
mod common;
use common::EXPECTED_VERSION;
#[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"));
@@ -20,7 +29,7 @@ fn test_version_command() {
cmd.assert() cmd.assert()
.success() .success()
.stdout(predicate::str::contains("1.0.0")); .stdout(predicate::str::contains(EXPECTED_VERSION));
} }
#[test] #[test]
@@ -32,24 +41,3 @@ fn test_sdk_status_command() {
.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");
// Extract mock SDK let output = weevil_cmd(&home)
MOCK_SDK.extract(&sdk_dir).unwrap(); .arg("new")
.arg("test-robot")
// Create project using weevil .current_dir(home.path())
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 let project_dir = home.path().join("test-robot");
assert!(output.status.success(), "weevil new failed: {}", String::from_utf8_lossy(&output.stderr)); assert!(output.status.success(), "weevil new failed");
assert!(project_dir.join(".weevil.toml").exists()); assert!(project_dir.join(".weevil.toml").exists(), ".weevil.toml missing");
assert!(project_dir.join("build.gradle.kts").exists()); assert!(project_dir.join("build.gradle.kts").exists(), "build.gradle.kts missing");
assert!(project_dir.join("src/main/java/robot").exists()); 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");
// Extract mock SDK let output = weevil_cmd(&home)
MOCK_SDK.extract(&sdk_dir).unwrap(); .arg("new")
.arg("config-test")
// Create project .current_dir(home.path())
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);
assert!(output.status.success(), "weevil new failed");
// Read config let project_dir = home.path().join("config-test");
let config_content = fs::read_to_string(project_dir.join(".weevil.toml")).unwrap(); let config_content = fs::read_to_string(project_dir.join(".weevil.toml"))
.expect(".weevil.toml not found");
assert!(config_content.contains("project_name = \"config-test\"")); assert!(config_content.contains("project_name = \"config-test\""),
assert!(config_content.contains(&format!("ftc_sdk_path = \"{}\"", sdk_dir.display()))); "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 // Custom code survives
assert!(custom_file.exists()); 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");
// Extract mock SDK let output = weevil_cmd(&home)
MOCK_SDK.extract(&sdk_dir).unwrap(); .arg("new")
.arg("build-test")
// Create project .current_dir(home.path())
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);
assert!(output.status.success(), "weevil new failed");
// Check build.sh contains config reading let project_dir = home.path().join("build-test");
let build_sh = fs::read_to_string(project_dir.join("build.sh")).unwrap();
assert!(build_sh.contains(".weevil.toml"));
assert!(build_sh.contains("ftc_sdk_path"));
// Check build.bat contains config reading let build_sh = fs::read_to_string(project_dir.join("build.sh"))
let build_bat = fs::read_to_string(project_dir.join("build.bat")).unwrap(); .expect("build.sh not found");
assert!(build_bat.contains(".weevil.toml")); assert!(build_sh.contains(".weevil.toml"), "build.sh doesn't reference .weevil.toml");
assert!(build_bat.contains("ftc_sdk_path")); assert!(build_sh.contains("ftc_sdk_path"), "build.sh doesn't reference 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");
let project2 = test_dir.path().join("robot2");
// Create two different SDK versions // Create a second FTC SDK with a different version
MOCK_SDK.extract(&sdk1).unwrap(); let sdk2 = home.path().join("ftc-sdk-v11");
MOCK_SDK.extract(&sdk2).unwrap(); create_mock_ftc_sdk(&sdk2);
fs::write(sdk2.join(".version"), "v11.0.0").unwrap(); fs::write(sdk2.join(".version"), "v11.0.0\n").unwrap();
// Create two projects with different SDKs // Create first project (uses default ftc-sdk in WEEVIL_HOME)
Command::new("cargo") let output = weevil_cmd(&home)
.args(&["run", "--", "new", "robot1", "--ftc-sdk", sdk1.to_str().unwrap()]) .arg("new")
.current_dir(env!("CARGO_MANIFEST_DIR")) .arg("robot1")
.current_dir(home.path())
.output() .output()
.expect("Failed to create project1"); .expect("Failed to create robot1");
print_output("test_multiple_projects_different_sdks (robot1)", &output);
assert!(output.status.success(), "weevil new robot1 failed");
Command::new("cargo") // Create second project — would need --ftc-sdk flag if supported,
.args(&["run", "--", "new", "robot2", "--ftc-sdk", sdk2.to_str().unwrap()]) // otherwise both use the same default. Verify they each have valid configs.
.current_dir(env!("CARGO_MANIFEST_DIR")) 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);
assert!(output.status.success(), "weevil new robot2 failed");
// Verify each project has correct SDK let config1 = fs::read_to_string(home.path().join("robot1/.weevil.toml"))
let config1 = fs::read_to_string(project1.join(".weevil.toml")).unwrap(); .expect("robot1 .weevil.toml missing");
let config2 = fs::read_to_string(project2.join(".weevil.toml")).unwrap(); let config2 = fs::read_to_string(home.path().join("robot2/.weevil.toml"))
.expect("robot2 .weevil.toml missing");
assert!(config1.contains(&sdk1.display().to_string())); assert!(config1.contains("project_name = \"robot1\""), "robot1 config wrong");
assert!(config2.contains(&sdk2.display().to_string())); assert!(config2.contains("project_name = \"robot2\""), "robot2 config wrong");
assert!(config1.contains("v10.1.1")); assert!(config1.contains("ftc_sdk_path"), "robot1 missing ftc_sdk_path");
assert!(config2.contains("v11.0.0")); assert!(config2.contains("ftc_sdk_path"), "robot2 missing ftc_sdk_path");
} }

View File

@@ -6,6 +6,10 @@ use std::fs;
use weevil::project::{ProjectBuilder, ProjectConfig}; use weevil::project::{ProjectBuilder, ProjectConfig};
use weevil::sdk::SdkConfig; use weevil::sdk::SdkConfig;
#[path = "common.rs"]
mod common;
use common::EXPECTED_VERSION;
// Note: These tests use the actual FTC SDK if available, or skip if not // Note: These tests use the actual FTC SDK if available, or skip if not
// For true unit testing with mocks, we'd need to refactor to use dependency injection // For true unit testing with mocks, we'd need to refactor to use dependency injection
@@ -26,7 +30,7 @@ fn test_config_create_and_save() {
assert_eq!(config.project_name, "test-robot"); assert_eq!(config.project_name, "test-robot");
assert_eq!(config.ftc_sdk_path, sdk_path); assert_eq!(config.ftc_sdk_path, sdk_path);
assert_eq!(config.android_sdk_path, android_sdk_path); assert_eq!(config.android_sdk_path, android_sdk_path);
assert_eq!(config.weevil_version, "1.0.0"); assert_eq!(config.weevil_version, EXPECTED_VERSION);
// Save and reload // Save and reload
let project_path = temp_dir.path().join("project"); let project_path = temp_dir.path().join("project");
@@ -60,7 +64,7 @@ fn test_config_toml_format() {
let content = fs::read_to_string(project_path.join(".weevil.toml")).unwrap(); let content = fs::read_to_string(project_path.join(".weevil.toml")).unwrap();
assert!(content.contains("project_name = \"my-robot\"")); assert!(content.contains("project_name = \"my-robot\""));
assert!(content.contains("weevil_version = \"1.0.0\"")); assert!(content.contains(&format!("weevil_version = \"{}\"", EXPECTED_VERSION)));
assert!(content.contains("ftc_sdk_path")); assert!(content.contains("ftc_sdk_path"));
assert!(content.contains("ftc_sdk_version")); assert!(content.contains("ftc_sdk_version"));
assert!(content.contains("android_sdk_path")); assert!(content.contains("android_sdk_path"));

View File

@@ -6,6 +6,10 @@ use std::path::PathBuf;
use tempfile::TempDir; use tempfile::TempDir;
use std::fs; use std::fs;
#[path = "../common.rs"]
mod common;
use common::EXPECTED_VERSION;
#[test] #[test]
fn test_config_create_and_save() { fn test_config_create_and_save() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
@@ -15,7 +19,7 @@ fn test_config_create_and_save() {
assert_eq!(config.project_name, "test-robot"); assert_eq!(config.project_name, "test-robot");
assert_eq!(config.ftc_sdk_path, sdk_path); assert_eq!(config.ftc_sdk_path, sdk_path);
assert_eq!(config.weevil_version, "1.0.0"); assert_eq!(config.weevil_version, EXPECTED_VERSION);
// Save and reload // Save and reload
config.save(temp_dir.path()).unwrap(); config.save(temp_dir.path()).unwrap();
@@ -45,7 +49,7 @@ fn test_config_toml_format() {
let content = fs::read_to_string(temp_dir.path().join(".weevil.toml")).unwrap(); let content = fs::read_to_string(temp_dir.path().join(".weevil.toml")).unwrap();
assert!(content.contains("project_name = \"my-robot\"")); assert!(content.contains("project_name = \"my-robot\""));
assert!(content.contains("weevil_version = \"1.0.0\"")); assert!(content.contains(&format!("weevil_version = \"{}\"", EXPECTED_VERSION)));
assert!(content.contains("ftc_sdk_path")); assert!(content.contains("ftc_sdk_path"));
} }