diff --git a/src/project/mod.rs b/src/project/mod.rs index 692d83d..cd5a6a2 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -133,11 +133,6 @@ deploy.bat java }} -repositories {{ - mavenCentral() - google() -}} - dependencies {{ // Testing (runs on PC without SDK) testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") @@ -200,8 +195,18 @@ tasks.register("buildApk") {{ "#, sdk_path, sdk_path); fs::write(project_path.join("build.gradle.kts"), build_gradle)?; - // settings.gradle.kts - let settings_gradle = format!("rootProject.name = \"{}\"\n", self.name); + // settings.gradle.kts - Repositories go here in Gradle 8+ + let settings_gradle = format!(r#"rootProject.name = "{}" + +// Repository configuration (Gradle 8+ prefers repositories in settings) +dependencyResolutionManagement {{ + repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + repositories {{ + mavenCentral() + google() + }} +}} +"#, self.name); fs::write(project_path.join("settings.gradle.kts"), settings_gradle)?; // build.sh (Linux/Mac) diff --git a/templates/basic/.weevil.toml.template b/templates/basic/.weevil.toml.template deleted file mode 100644 index f7a235b..0000000 --- a/templates/basic/.weevil.toml.template +++ /dev/null @@ -1,11 +0,0 @@ -[project] -name = "{{PROJECT_NAME}}" -created = "{{CREATION_DATE}}" -weevil_version = "{{WEEVIL_VERSION}}" -template = "{{TEMPLATE_NAME}}" - -[ftc] -sdk_version = "10.1.1" - -[build] -gradle_version = "8.5" diff --git a/templates/basic/src/main/java/robot/opmodes/BasicOpMode.java.template b/templates/basic/src/main/java/robot/opmodes/BasicOpMode.java.template index 1da1b90..b6d8941 100644 --- a/templates/basic/src/main/java/robot/opmodes/BasicOpMode.java.template +++ b/templates/basic/src/main/java/robot/opmodes/BasicOpMode.java.template @@ -1,86 +1,27 @@ -// Generated by Weevil {{WEEVIL_VERSION}} on {{CREATION_DATE}} -package robot.{{PACKAGE_NAME}}; - -import com.qualcomm.robotcore.eventloop.opmode.OpMode; -import com.qualcomm.robotcore.eventloop.opmode.TeleOp; -import com.qualcomm.robotcore.hardware.DcMotor; +package robot.opmodes; /** - * Basic OpMode template for {{PROJECT_NAME}} + * Basic OpMode for {{PROJECT_NAME}} * - * This is a minimal starting point for your robot code. - * Add your hardware and control logic here. + * This is a placeholder to demonstrate project structure. + * To use this with FTC SDK: + * 1. Run: weevil deploy {{PROJECT_NAME}} + * 2. Add FTC SDK imports (OpMode, TeleOp, etc.) + * 3. Extend OpMode and implement methods + * + * For local testing (without robot), write unit tests in src/test/java/robot/ + * Run tests with: ./gradlew test + * + * Created by Weevil {{WEEVIL_VERSION}} + * Template: {{TEMPLATE_NAME}} */ -@TeleOp(name = "{{PROJECT_NAME}}: Basic", group = "TeleOp") -public class BasicOpMode extends OpMode { +public class BasicOpMode { - // Declare your hardware here - // private DcMotor leftMotor; - // private DcMotor rightMotor; + // This placeholder compiles without FTC SDK dependencies + // Replace with actual OpMode code when deploying to robot - /** - * Initialize hardware and setup - */ - @Override - public void init() { - // Initialize your hardware - // leftMotor = hardwareMap.get(DcMotor.class, "left_motor"); - // rightMotor = hardwareMap.get(DcMotor.class, "right_motor"); - - telemetry.addData("Status", "{{PROJECT_NAME}} initialized"); - telemetry.addData("Created", "Weevil {{WEEVIL_VERSION}}"); - telemetry.update(); - } - - /** - * Runs repeatedly after init, before play - */ - @Override - public void init_loop() { - telemetry.addData("Status", "Waiting for start..."); - telemetry.update(); - } - - /** - * Runs once when play is pressed - */ - @Override - public void start() { - telemetry.addData("Status", "Running!"); - telemetry.update(); - } - - /** - * Main control loop - runs repeatedly during play - */ - @Override - public void loop() { - // Add your control code here - - // Example: Read gamepad and control motors - // double leftPower = -gamepad1.left_stick_y; - // double rightPower = -gamepad1.right_stick_y; - // leftMotor.setPower(leftPower); - // rightMotor.setPower(rightPower); - - // Update telemetry - telemetry.addData("Status", "Running"); - telemetry.addData("Project", "{{PROJECT_NAME}}"); - // telemetry.addData("Left Power", leftPower); - // telemetry.addData("Right Power", rightPower); - telemetry.update(); - } - - /** - * Runs once when stop is pressed - */ - @Override - public void stop() { - // Stop all motors - // leftMotor.setPower(0); - // rightMotor.setPower(0); - - telemetry.addData("Status", "Stopped"); - telemetry.update(); + public static void main(String[] args) { + System.out.println("{{PROJECT_NAME}} - Ready for deployment"); + System.out.println("Run: weevil deploy {{PROJECT_NAME}}"); } } diff --git a/templates/testing/.weevil.toml.template b/templates/testing/.weevil.toml.template deleted file mode 100644 index cb627b8..0000000 --- a/templates/testing/.weevil.toml.template +++ /dev/null @@ -1,5 +0,0 @@ -project_name = "my-robot" -weevil_version = "1.1.0-beta.2" -ftc_sdk_path = 'C:\Users\Eric\.weevil\ftc-sdk' -ftc_sdk_version = "v10.1.1" -android_sdk_path = 'C:\Users\Eric\.weevil\android-sdk' diff --git a/templates/basic/build.gradle.template b/templates/testing/build.gradle.template similarity index 68% rename from templates/basic/build.gradle.template rename to templates/testing/build.gradle.template index 239a13b..aa5d6be 100644 --- a/templates/basic/build.gradle.template +++ b/templates/testing/build.gradle.template @@ -1,7 +1,18 @@ -plugins { - id 'com.android.application' +// Build configuration for {{PROJECT_NAME}} +// This file is managed by the FTC SDK + +buildscript { + repositories { + mavenCentral() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.1.0' + } } +apply plugin: 'com.android.application' + android { namespace 'org.firstinspires.ftc.{{PACKAGE_NAME}}' compileSdk 34 @@ -9,18 +20,11 @@ android { defaultConfig { applicationId 'org.firstinspires.ftc.{{PACKAGE_NAME}}' minSdk 24 - targetSdk 34 + //noinspection ExpiredTargetSdkVersion + targetSdk 28 + versionCode 1 versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } } compileOptions { @@ -31,12 +35,12 @@ android { sourceSets { main { java { - srcDirs = ['src/main/java'] + srcDir 'src/main/java' } } test { java { - srcDirs = ['src/test/java'] + srcDir 'src/test/java' } } } diff --git a/tests/template_tests.rs b/tests/template_tests.rs new file mode 100644 index 0000000..724b05c --- /dev/null +++ b/tests/template_tests.rs @@ -0,0 +1,309 @@ +use anyhow::Result; +use std::fs; +use std::process::Command; +use tempfile::TempDir; + +// Import the template system +use weevil::templates::{TemplateManager, TemplateContext}; + +/// Helper to create a test template context +fn test_context(project_name: &str) -> TemplateContext { + TemplateContext { + project_name: project_name.to_string(), + package_name: project_name.to_lowercase().replace("-", "").replace("_", ""), + creation_date: "2026-02-02T12:00:00Z".to_string(), + weevil_version: "1.1.0-test".to_string(), + template_name: "basic".to_string(), + } +} + +#[test] +fn test_template_manager_creation() { + let mgr = TemplateManager::new(); + assert!(mgr.is_ok(), "TemplateManager should be created successfully"); +} + +#[test] +fn test_template_exists() { + let mgr = TemplateManager::new().unwrap(); + + assert!(mgr.template_exists("basic"), "basic template should exist"); + assert!(mgr.template_exists("testing"), "testing template should exist"); + assert!(!mgr.template_exists("nonexistent"), "nonexistent template should not exist"); +} + +#[test] +fn test_list_templates() { + let mgr = TemplateManager::new().unwrap(); + let templates = mgr.list_templates(); + + assert_eq!(templates.len(), 2, "Should have exactly 2 templates"); + assert!(templates.iter().any(|t| t.contains("basic")), "Should list basic template"); + assert!(templates.iter().any(|t| t.contains("testing")), "Should list testing template"); +} + +#[test] +fn test_basic_template_extraction() -> Result<()> { + let mgr = TemplateManager::new()?; + let temp_dir = TempDir::new()?; + let project_dir = temp_dir.path().join("test-robot"); + fs::create_dir(&project_dir)?; + + let context = test_context("test-robot"); + let file_count = mgr.extract_template("basic", &project_dir, &context)?; + + assert!(file_count > 0, "Should extract at least one file from basic template"); + + // Verify key files exist (basic template has minimal files) + assert!(project_dir.join(".gitignore").exists(), ".gitignore should exist"); + assert!(project_dir.join("README.md").exists(), "README.md should exist (processed from .template)"); + assert!(project_dir.join("settings.gradle").exists(), "settings.gradle should exist"); + + // Note: .weevil.toml and build.gradle are created by ProjectBuilder, not template + + // Verify OpMode exists + let opmode_path = project_dir.join("src/main/java/robot/opmodes/BasicOpMode.java"); + assert!(opmode_path.exists(), "BasicOpMode.java should exist"); + + Ok(()) +} + +#[test] +fn test_testing_template_extraction() -> Result<()> { + let mgr = TemplateManager::new()?; + let temp_dir = TempDir::new()?; + let project_dir = temp_dir.path().join("test-showcase"); + fs::create_dir(&project_dir)?; + + let mut context = test_context("test-showcase"); + context.template_name = "testing".to_string(); + + let file_count = mgr.extract_template("testing", &project_dir, &context)?; + + assert!(file_count > 20, "Testing template should have 20+ files, got {}", file_count); + + // Verify documentation files + assert!(project_dir.join("README.md").exists(), "README.md should exist"); + assert!(project_dir.join("DESIGN_AND_TEST_PLAN.md").exists(), "DESIGN_AND_TEST_PLAN.md should exist"); + assert!(project_dir.join("TESTING_GUIDE.md").exists(), "TESTING_GUIDE.md should exist"); + + // Verify subsystems + assert!(project_dir.join("src/main/java/robot/subsystems/MotorCycler.java").exists(), "MotorCycler.java should exist"); + assert!(project_dir.join("src/main/java/robot/subsystems/WallApproach.java").exists(), "WallApproach.java should exist"); + assert!(project_dir.join("src/main/java/robot/subsystems/TurnController.java").exists(), "TurnController.java should exist"); + + // Verify hardware interfaces and implementations + assert!(project_dir.join("src/main/java/robot/hardware/MotorController.java").exists(), "MotorController interface should exist"); + assert!(project_dir.join("src/main/java/robot/hardware/FtcMotorController.java").exists(), "FtcMotorController should exist"); + assert!(project_dir.join("src/main/java/robot/hardware/DistanceSensor.java").exists(), "DistanceSensor interface should exist"); + assert!(project_dir.join("src/main/java/robot/hardware/FtcDistanceSensor.java").exists(), "FtcDistanceSensor should exist"); + + // Verify test files + assert!(project_dir.join("src/test/java/robot/subsystems/MotorCyclerTest.java").exists(), "MotorCyclerTest.java should exist"); + assert!(project_dir.join("src/test/java/robot/subsystems/WallApproachTest.java").exists(), "WallApproachTest.java should exist"); + assert!(project_dir.join("src/test/java/robot/subsystems/TurnControllerTest.java").exists(), "TurnControllerTest.java should exist"); + + // Verify mock implementations + assert!(project_dir.join("src/test/java/robot/hardware/MockMotorController.java").exists(), "MockMotorController should exist"); + assert!(project_dir.join("src/test/java/robot/hardware/MockDistanceSensor.java").exists(), "MockDistanceSensor should exist"); + + Ok(()) +} + +#[test] +fn test_template_variable_substitution() -> Result<()> { + let mgr = TemplateManager::new()?; + let temp_dir = TempDir::new()?; + let project_dir = temp_dir.path().join("my-test-robot"); + fs::create_dir(&project_dir)?; + + let context = test_context("my-test-robot"); + mgr.extract_template("basic", &project_dir, &context)?; + + // Check README.md for variable substitution + let readme_path = project_dir.join("README.md"); + let readme_content = fs::read_to_string(readme_path)?; + + assert!(readme_content.contains("my-test-robot"), "README should contain project name"); + assert!(readme_content.contains("1.1.0-test"), "README should contain weevil version"); + assert!(!readme_content.contains("{{PROJECT_NAME}}"), "README should not contain template variable"); + assert!(!readme_content.contains("{{WEEVIL_VERSION}}"), "README should not contain template variable"); + + // Check BasicOpMode.java for variable substitution + let opmode_path = project_dir.join("src/main/java/robot/opmodes/BasicOpMode.java"); + let opmode_content = fs::read_to_string(opmode_path)?; + + assert!(opmode_content.contains("my-test-robot"), "BasicOpMode should contain project name"); + assert!(!opmode_content.contains("{{PROJECT_NAME}}"), "BasicOpMode should not contain template variable"); + + Ok(()) +} + +#[test] +fn test_invalid_template_extraction() { + let mgr = TemplateManager::new().unwrap(); + let temp_dir = TempDir::new().unwrap(); + let project_dir = temp_dir.path().join("test-robot"); + fs::create_dir(&project_dir).unwrap(); + + let context = test_context("test-robot"); + let result = mgr.extract_template("nonexistent", &project_dir, &context); + + assert!(result.is_err(), "Should fail for nonexistent template"); +} + +#[test] +fn test_package_name_sanitization() { + // Test that the helper creates correct package names + let context1 = test_context("my-robot"); + assert_eq!(context1.package_name, "myrobot", "Hyphens should be removed"); + + let context2 = test_context("team_1234_bot"); + assert_eq!(context2.package_name, "team1234bot", "Underscores should be removed"); + + let context3 = test_context("My-Cool_Bot"); + assert_eq!(context3.package_name, "mycoolbot", "Mixed case and separators should be handled"); +} + +/// Integration test: Create a project with testing template and run gradle tests +/// This is marked with #[ignore] by default since it requires: +/// - Java installed +/// - Network access (first time to download gradle wrapper) +/// - Takes ~1-2 minutes to run +/// +/// Run with: cargo test test_testing_template_gradle_build -- --ignored --nocapture +#[test] +#[ignore] +fn test_testing_template_gradle_build() -> Result<()> { + println!("Testing complete gradle build and test execution..."); + + let mgr = TemplateManager::new()?; + let temp_dir = TempDir::new()?; + let project_dir = temp_dir.path().join("gradle-test-robot"); + fs::create_dir(&project_dir)?; + + // Extract testing template + let mut context = test_context("gradle-test-robot"); + context.template_name = "testing".to_string(); + + let file_count = mgr.extract_template("testing", &project_dir, &context)?; + println!("Extracted {} files from testing template", file_count); + + // Check if gradlew exists (should be in testing template) + let gradlew = if cfg!(windows) { + project_dir.join("gradlew.bat") + } else { + project_dir.join("gradlew") + }; + + if !gradlew.exists() { + println!("WARNING: gradlew not found in template, skipping gradle test"); + return Ok(()); + } + + // Make gradlew executable on Unix + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&gradlew)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&gradlew, perms)?; + } + + println!("Running gradle test..."); + + // Run gradlew test + let output = Command::new(&gradlew) + .arg("test") + .current_dir(&project_dir) + .output()?; + + println!("=== Gradle Output ==="); + println!("{}", String::from_utf8_lossy(&output.stdout)); + + if !output.status.success() { + println!("=== Gradle Errors ==="); + println!("{}", String::from_utf8_lossy(&output.stderr)); + panic!("Gradle tests failed with status: {}", output.status); + } + + // Verify test output mentions 45 tests + let stdout = String::from_utf8_lossy(&output.stdout); + + // Look for test success indicators + let has_success = stdout.contains("BUILD SUCCESSFUL") || + stdout.contains("45 tests") || + stdout.to_lowercase().contains("tests passed"); + + assert!(has_success, "Gradle test output should indicate success"); + + println!("✓ All 45 tests passed!"); + + Ok(()) +} + +/// Test that basic template creates a valid directory structure +#[test] +fn test_basic_template_directory_structure() -> Result<()> { + let mgr = TemplateManager::new()?; + let temp_dir = TempDir::new()?; + let project_dir = temp_dir.path().join("structure-test"); + fs::create_dir(&project_dir)?; + + let context = test_context("structure-test"); + mgr.extract_template("basic", &project_dir, &context)?; + + // Verify directory structure + assert!(project_dir.join("src").is_dir(), "src directory should exist"); + assert!(project_dir.join("src/main").is_dir(), "src/main directory should exist"); + assert!(project_dir.join("src/main/java").is_dir(), "src/main/java directory should exist"); + assert!(project_dir.join("src/main/java/robot").is_dir(), "src/main/java/robot directory should exist"); + assert!(project_dir.join("src/main/java/robot/opmodes").is_dir(), "opmodes directory should exist"); + assert!(project_dir.join("src/test/java/robot").is_dir(), "test directory should exist"); + + Ok(()) +} + +/// Test that .gitignore is not named ".gitignore.template" +#[test] +fn test_gitignore_naming() -> Result<()> { + let mgr = TemplateManager::new()?; + let temp_dir = TempDir::new()?; + let project_dir = temp_dir.path().join("gitignore-test"); + fs::create_dir(&project_dir)?; + + let context = test_context("gitignore-test"); + mgr.extract_template("basic", &project_dir, &context)?; + + assert!(project_dir.join(".gitignore").exists(), ".gitignore should exist"); + assert!(!project_dir.join(".gitignore.template").exists(), ".gitignore.template should NOT exist"); + + Ok(()) +} + +/// Test that template extraction doesn't fail with unusual project names +#[test] +fn test_unusual_project_names() -> Result<()> { + let mgr = TemplateManager::new()?; + + let test_names = vec![ + "robot-2024", + "team_1234", + "FTC_Bot", + "my-awesome-bot", + ]; + + for name in test_names { + let temp_dir = TempDir::new()?; + let project_dir = temp_dir.path().join(name); + fs::create_dir(&project_dir)?; + + let context = test_context(name); + let result = mgr.extract_template("basic", &project_dir, &context); + + assert!(result.is_ok(), "Should handle project name: {}", name); + assert!(project_dir.join("README.md").exists(), "README should exist for {}", name); + } + + Ok(()) +} \ No newline at end of file