| | """ |
| | Java test runner for Modal sandbox execution. |
| | Handles Maven project structure, pom.xml generation, and JUnit 5 execution. |
| | """ |
| |
|
| | import subprocess |
| | import tempfile |
| | import time |
| | import logging |
| | import re |
| | from pathlib import Path |
| | from typing import Dict, List |
| |
|
| | logger = logging.getLogger(__name__) |
| |
|
| |
|
| | def _extract_class_name(code: str, module_name: str) -> str: |
| | """Extract Java class name from code.""" |
| | match = re.search(r'public\s+class\s+(\w+)', code) |
| | if match: |
| | return match.group(1) |
| | |
| | |
| | return ''.join(word.capitalize() for word in module_name.split('_')) |
| |
|
| |
|
| | def _create_maven_project(tmpdir: Path, module_name: str, code: str, tests: str) -> str: |
| | """ |
| | Create Maven project structure with proper directory layout. |
| | |
| | Returns: |
| | Class name extracted from code |
| | """ |
| | |
| | main_class = _extract_class_name(code, module_name) |
| | test_class = _extract_class_name(tests, f"{module_name}Test") |
| | |
| | |
| | src_main = tmpdir / "src" / "main" / "java" / "com" / "modernizer" |
| | src_test = tmpdir / "src" / "test" / "java" / "com" / "modernizer" |
| | src_main.mkdir(parents=True) |
| | src_test.mkdir(parents=True) |
| | |
| | |
| | if "package " not in code: |
| | code = "package com.modernizer;\n\n" + code |
| | if "package " not in tests: |
| | tests = "package com.modernizer;\n\n" + tests |
| | |
| | |
| | (src_main / f"{main_class}.java").write_text(code, encoding='utf-8') |
| | (src_test / f"{test_class}.java").write_text(tests, encoding='utf-8') |
| | |
| | |
| | pom_xml = f"""<?xml version="1.0" encoding="UTF-8"?> |
| | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 |
| | http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | <modelVersion>4.0.0</modelVersion> |
| | |
| | <groupId>com.modernizer</groupId> |
| | <artifactId>{module_name}</artifactId> |
| | <version>1.0-SNAPSHOT</version> |
| | <packaging>jar</packaging> |
| | |
| | <properties> |
| | <maven.compiler.source>17</maven.compiler.source> |
| | <maven.compiler.target>17</maven.compiler.target> |
| | <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
| | <junit.version>5.10.1</junit.version> |
| | </properties> |
| | |
| | <dependencies> |
| | <!-- JUnit 5 --> |
| | <dependency> |
| | <groupId>org.junit.jupiter</groupId> |
| | <artifactId>junit-jupiter</artifactId> |
| | <version>${{junit.version}}</version> |
| | <scope>test</scope> |
| | </dependency> |
| | |
| | <!-- Mockito for mocking --> |
| | <dependency> |
| | <groupId>org.mockito</groupId> |
| | <artifactId>mockito-core</artifactId> |
| | <version>5.7.0</version> |
| | <scope>test</scope> |
| | </dependency> |
| | |
| | <dependency> |
| | <groupId>org.assertj</groupId> |
| | <artifactId>assertj-core</artifactId> |
| | <version>3.24.2</version> |
| | <scope>test</scope> |
| | </dependency> |
| | |
| | <!-- Servlet API --> |
| | <dependency> |
| | <groupId>javax.servlet</groupId> |
| | <artifactId>javax.servlet-api</artifactId> |
| | <version>4.0.1</version> |
| | <scope>provided</scope> |
| | </dependency> |
| | </dependencies> |
| | |
| | <build> |
| | <plugins> |
| | <!-- Maven Compiler Plugin --> |
| | <plugin> |
| | <groupId>org.apache.maven.plugins</groupId> |
| | <artifactId>maven-compiler-plugin</artifactId> |
| | <version>3.11.0</version> |
| | <configuration> |
| | <source>17</source> |
| | <target>17</target> |
| | </configuration> |
| | </plugin> |
| | |
| | <!-- Maven Surefire Plugin for running tests --> |
| | <plugin> |
| | <groupId>org.apache.maven.plugins</groupId> |
| | <artifactId>maven-surefire-plugin</artifactId> |
| | <version>3.2.2</version> |
| | <configuration> |
| | <includes> |
| | <include>**/*Test.java</include> |
| | </includes> |
| | </configuration> |
| | </plugin> |
| | |
| | <!-- JaCoCo for code coverage --> |
| | <plugin> |
| | <groupId>org.jacoco</groupId> |
| | <artifactId>jacoco-maven-plugin</artifactId> |
| | <version>0.8.11</version> |
| | <executions> |
| | <execution> |
| | <goals> |
| | <goal>prepare-agent</goal> |
| | </goals> |
| | </execution> |
| | <execution> |
| | <id>report</id> |
| | <phase>test</phase> |
| | <goals> |
| | <goal>report</goal> |
| | </goals> |
| | </execution> |
| | </executions> |
| | </plugin> |
| | </plugins> |
| | </build> |
| | </project> |
| | """ |
| | (tmpdir / "pom.xml").write_text(pom_xml, encoding='utf-8') |
| | |
| | return main_class |
| |
|
| |
|
| | def _validate_java_tests(tests: str) -> tuple: |
| | """ |
| | Validate Java test code before execution. |
| | |
| | Returns: |
| | (is_valid, error_message) |
| | """ |
| | |
| | if "@Test" not in tests: |
| | return False, "No @Test annotations found (required for JUnit 5)" |
| | |
| | |
| | if "org.junit" not in tests: |
| | return False, "Missing JUnit imports (import org.junit.jupiter.api.Test)" |
| | |
| | |
| | if "class" not in tests: |
| | return False, "No test class found" |
| | |
| | return True, "" |
| |
|
| |
|
| | def run_java_tests(code: str, tests: str, requirements: List[str], module_name: str) -> Dict: |
| | """ |
| | Run Java tests using Maven and JUnit 5 in Modal container. |
| | |
| | Args: |
| | code: Java source code |
| | tests: JUnit test code |
| | requirements: List of Maven dependencies (not used currently) |
| | module_name: Name of the module |
| | |
| | Returns: |
| | Dictionary with test results |
| | """ |
| | |
| | is_valid, error_msg = _validate_java_tests(tests) |
| | if not is_valid: |
| | logger.error(f"Test validation failed: {error_msg}") |
| | return { |
| | "success": False, |
| | "error": f"Test validation failed: {error_msg}", |
| | "tests_run": 0, |
| | "tests_passed": 0, |
| | "tests_failed": 0, |
| | "execution_mode": "modal", |
| | "language": "java" |
| | } |
| | |
| | with tempfile.TemporaryDirectory() as tmpdir: |
| | tmpdir_path = Path(tmpdir) |
| | |
| | try: |
| | |
| | class_name = _create_maven_project(tmpdir_path, module_name, code, tests) |
| | logger.info(f"Created Maven project for class: {class_name}") |
| | except Exception as e: |
| | logger.error(f"Failed to create Maven project: {e}") |
| | return { |
| | "success": False, |
| | "error": f"Project setup failed: {str(e)}", |
| | "tests_run": 0, |
| | "tests_passed": 0, |
| | "tests_failed": 0, |
| | "execution_mode": "modal", |
| | "language": "java" |
| | } |
| | |
| | start_time = time.time() |
| | |
| | try: |
| | |
| | logger.info("Running Maven tests...") |
| | result = subprocess.run( |
| | ["mvn", "clean", "test", "-B", "-q"], |
| | cwd=tmpdir, |
| | capture_output=True, |
| | text=True, |
| | timeout=300 |
| | ) |
| | except subprocess.TimeoutExpired: |
| | return { |
| | "success": False, |
| | "error": "Maven test execution timeout (>5 minutes)", |
| | "tests_run": 0, |
| | "tests_passed": 0, |
| | "tests_failed": 0, |
| | "execution_time": 300.0, |
| | "execution_mode": "modal", |
| | "language": "java" |
| | } |
| | except FileNotFoundError: |
| | return { |
| | "success": False, |
| | "error": "Maven (mvn) not found in container", |
| | "tests_run": 0, |
| | "tests_passed": 0, |
| | "tests_failed": 0, |
| | "execution_mode": "modal", |
| | "language": "java" |
| | } |
| | |
| | execution_time = time.time() - start_time |
| | stdout = result.stdout[:10000] |
| | stderr = result.stderr[:10000] |
| | |
| | |
| | if "BUILD FAILURE" in stdout or "COMPILATION ERROR" in stdout or "BUILD FAILURE" in stderr: |
| | error_msg = "Maven build failed" |
| | |
| | if "COMPILATION ERROR" in stdout: |
| | error_msg = "Java compilation error" |
| | elif "[ERROR]" in stdout: |
| | |
| | for line in stdout.split('\n'): |
| | if '[ERROR]' in line and 'Failed to execute goal' not in line: |
| | error_msg = line.strip() |
| | break |
| | |
| | return { |
| | "success": False, |
| | "error": error_msg, |
| | "tests_run": 0, |
| | "tests_passed": 0, |
| | "tests_failed": 0, |
| | "execution_mode": "modal", |
| | "language": "java", |
| | "stdout": stdout, |
| | "stderr": stderr |
| | } |
| | |
| | |
| | |
| | tests_run = 0 |
| | tests_passed = 0 |
| | tests_failed = 0 |
| | tests_errors = 0 |
| | tests_skipped = 0 |
| | |
| | match = re.search(r'Tests run: (\d+),\s*Failures: (\d+),\s*Errors: (\d+),\s*Skipped: (\d+)', stdout) |
| | if match: |
| | tests_run = int(match.group(1)) |
| | failures = int(match.group(2)) |
| | tests_errors = int(match.group(3)) |
| | tests_skipped = int(match.group(4)) |
| | tests_failed = failures + tests_errors |
| | tests_passed = tests_run - tests_failed - tests_skipped |
| | elif tests_run == 0 and result.returncode == 0: |
| | |
| | logger.warning("Maven succeeded but no tests were detected") |
| | return { |
| | "success": False, |
| | "error": "No tests detected by Maven Surefire (missing @Test annotations?)", |
| | "tests_run": 0, |
| | "tests_passed": 0, |
| | "tests_failed": 0, |
| | "execution_mode": "modal", |
| | "language": "java", |
| | "stdout": stdout, |
| | "stderr": stderr |
| | } |
| | |
| | |
| | coverage_percent = 0.0 |
| | jacoco_report = tmpdir_path / "target" / "site" / "jacoco" / "index.html" |
| | if jacoco_report.exists(): |
| | try: |
| | report_content = jacoco_report.read_text() |
| | |
| | cov_match = re.search(r'Total.*?(\d+)%', report_content) |
| | if cov_match: |
| | coverage_percent = float(cov_match.group(1)) |
| | except Exception as e: |
| | logger.warning(f"Failed to parse JaCoCo coverage: {e}") |
| | |
| | return { |
| | "success": result.returncode == 0, |
| | "tests_run": tests_run, |
| | "tests_passed": tests_passed, |
| | "tests_failed": tests_failed, |
| | "tests_errors": tests_errors, |
| | "tests_skipped": tests_skipped, |
| | "execution_time": round(execution_time, 2), |
| | "coverage_percent": coverage_percent, |
| | "stdout": stdout, |
| | "stderr": stderr, |
| | "exit_code": result.returncode, |
| | "execution_mode": "modal", |
| | "language": "java" |
| | } |
| |
|