Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,37 @@ jobs:
- name: Render every Java template
run: uv run python scripts/verify_templates.py

java-compile:
name: Generated Java compile smoke
runs-on: ubuntu-latest
env:
PYTHONPATH: src
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Temurin Java 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "21"

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true

- name: Install runtime dependencies
run: |
uv venv --python 3.12
uv pip install -e "."

- name: Render and compile generated Java fixture
run: |
java --version
mvn --version
uv run python scripts/verify_java_compile.py

docker:
name: Docker image smoke test
runs-on: ubuntu-latest
Expand Down Expand Up @@ -241,7 +272,7 @@ jobs:
ci-success:
name: Required CI status
if: always()
needs: [metadata, quality, format, integration, templates, docker, package]
needs: [metadata, quality, format, integration, templates, java-compile, docker, package]
runs-on: ubuntu-latest
steps:
- name: Fail when any required job failed or was cancelled
Expand Down
3 changes: 3 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ PYTHONPATH=src python -m pytest tests/integration/ -q
# 运行模板和 MCP Apps 验证脚本
PYTHONPATH=src python scripts/verify_templates.py
PYTHONPATH=src python scripts/verify_mcp_apps.py

# 渲染并用 Java 21 / Maven 编译 sb35-java21 固定 fixture
PYTHONPATH=src python scripts/verify_java_compile.py
```

### 6. 验证安装
Expand Down
217 changes: 217 additions & 0 deletions scripts/verify_java_compile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
"""Compile a representative Java 21 project rendered from current templates."""

from __future__ import annotations

import argparse
import shutil
import subprocess
import tempfile
from pathlib import Path

import pystache

ROOT = Path(__file__).resolve().parents[1]
TEMPLATE_ROOT = ROOT / "src" / "dbjavagenix" / "templates" / "java" / "sb35-java21"
FIXTURE_PACKAGE = "com.dbjavagenix.compilefixture"

TEMPLATES = (
("entity.mustache", "entityPackage", "Invoice.java"),
("dao.mustache", "daoPackage", "InvoiceDao.java"),
("dto.mustache", "dtoPackage", "InvoiceDTO.java"),
("service.mustache", "servicePackage", "InvoiceService.java"),
("serviceImpl.mustache", "serviceImplPackage", "InvoiceServiceImpl.java"),
("controller.mustache", "controllerPackage", "InvoiceController.java"),
)

CONTEXT = {
"entityPackage": f"{FIXTURE_PACKAGE}.entity",
"dtoPackage": f"{FIXTURE_PACKAGE}.dto",
"daoPackage": f"{FIXTURE_PACKAGE}.dao",
"servicePackage": f"{FIXTURE_PACKAGE}.service",
"serviceImplPackage": f"{FIXTURE_PACKAGE}.service.impl",
"controllerPackage": f"{FIXTURE_PACKAGE}.controller",
"className": "Invoice",
"tableName": "invoice",
"entityNameLowerCase": "invoice",
"comment": "Invoice",
"author": "DBJavaGenix CI",
"date": "2026-09-08",
"primaryKeyName": "id",
"primaryKeyType": "Long",
"capitalizedPrimaryKeyName": "Id",
"serialVersionUID": "1",
"hasJakarta": True,
"hasSpringDoc": True,
"useLombok": True,
"useSwagger": True,
"generateDto": True,
"imports": ["java.math.BigDecimal", "java.time.OffsetDateTime"],
"columns": [
{
"name": "id",
"javaName": "id",
"capitalizedJavaName": "Id",
"javaType": "Long",
"comment": "Primary key",
"isPrimaryKey": True,
"nullable": False,
"isString": False,
"maxLength": None,
"last": False,
},
{
"name": "amount",
"javaName": "amount",
"capitalizedJavaName": "Amount",
"javaType": "BigDecimal",
"comment": "Amount",
"isPrimaryKey": False,
"nullable": False,
"isString": False,
"maxLength": None,
"last": False,
},
{
"name": "created_at",
"javaName": "createdAt",
"capitalizedJavaName": "CreatedAt",
"javaType": "OffsetDateTime",
"comment": "Creation time",
"isPrimaryKey": False,
"nullable": True,
"isString": False,
"maxLength": None,
"last": True,
},
],
}

POM = """<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.5</version>
<relativePath/>
</parent>
<groupId>com.dbjavagenix</groupId>
<artifactId>generated-compile-fixture</artifactId>
<version>1.0.0</version>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
<version>2.8.9</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
"""


def render_fixture(project_dir: Path) -> list[Path]:
"""Render the representative sb35-java21 source set into ``project_dir``."""
source_root = project_dir / "src" / "main" / "java"
renderer = pystache.Renderer()
rendered_files: list[Path] = []

for template_name, package_key, output_name in TEMPLATES:
template_path = TEMPLATE_ROOT / template_name
code = renderer.render(template_path.read_text(encoding="utf-8"), CONTEXT)
output_path = source_root / Path(CONTEXT[package_key].replace(".", "/")) / output_name
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(code, encoding="utf-8")
rendered_files.append(output_path)

(project_dir / "pom.xml").write_text(POM, encoding="utf-8")
return rendered_files


def build_maven_command(maven: str) -> list[str]:
"""Build the explicit, batch-mode compile command for a rendered fixture."""
return [maven, "-B", "-ntp", "-DskipTests", "clean", "compile"]


def cleanup_fixture(project_dir: Path, *, keep_fixture: bool, compile_succeeded: bool) -> None:
"""Remove a successful fixture unless diagnostics were explicitly requested."""
if keep_fixture or not compile_succeeded:
print(f"Fixture retained at {project_dir}")
return
shutil.rmtree(project_dir, ignore_errors=True)


def verify_java_compile(*, keep_fixture: bool = False) -> int:
"""Render and compile the fixture, retaining it only when requested or on failure."""
maven = shutil.which("mvn")
if maven is None:
print("Maven executable 'mvn' was not found on PATH")
return 1

project_dir = Path(tempfile.mkdtemp(prefix="dbjavagenix-java-compile-"))
compile_succeeded = False
try:
rendered_files = render_fixture(project_dir)
command = build_maven_command(maven)
print(f"Rendered {len(rendered_files)} sb35-java21 files to {project_dir}", flush=True)
print("$ " + " ".join(command), flush=True)
result = subprocess.run(command, cwd=project_dir, text=True, check=False)
if result.returncode != 0:
print("Java compile failed")
return result.returncode
compile_succeeded = True
print("Generated Java compile smoke passed")
return 0
finally:
cleanup_fixture(
project_dir,
keep_fixture=keep_fixture,
compile_succeeded=compile_succeeded,
)


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--keep-fixture",
action="store_true",
help="Keep the generated temporary Maven project after a successful compile.",
)
args = parser.parse_args()
return verify_java_compile(keep_fixture=args.keep_fixture)


if __name__ == "__main__":
raise SystemExit(main())
63 changes: 63 additions & 0 deletions tests/unit/test_java_compile_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Unit coverage for the generated Java compile fixture."""

import runpy
import xml.etree.ElementTree as element_tree
from pathlib import Path


_SCRIPT = runpy.run_path(str(Path(__file__).parents[2] / "scripts" / "verify_java_compile.py"))
CONTEXT = _SCRIPT["CONTEXT"]
POM = _SCRIPT["POM"]
TEMPLATES = _SCRIPT["TEMPLATES"]
build_maven_command = _SCRIPT["build_maven_command"]
cleanup_fixture = _SCRIPT["cleanup_fixture"]
render_fixture = _SCRIPT["render_fixture"]


def test_rendered_fixture_contains_every_java_layer(tmp_path):
files = render_fixture(tmp_path)

assert len(files) == len(TEMPLATES) == 6
assert {path.name for path in files} == {
"Invoice.java",
"InvoiceDao.java",
"InvoiceDTO.java",
"InvoiceService.java",
"InvoiceServiceImpl.java",
"InvoiceController.java",
}
for (_, package_key, _), path in zip(TEMPLATES, files, strict=True):
rendered = path.read_text(encoding="utf-8")
assert "{{" not in rendered
assert "}}" not in rendered
assert f"package {CONTEXT[package_key]};" in rendered


def test_fixture_pom_locks_java_and_spring_boot_versions():
namespace = {"m": "http://maven.apache.org/POM/4.0.0"}
root = element_tree.fromstring(POM)

assert root.findtext("m:parent/m:version", namespaces=namespace) == "3.5.5"
assert root.findtext("m:properties/m:java.version", namespaces=namespace) == "21"


def test_maven_command_is_non_interactive_compile_only():
command = build_maven_command("mvn")

assert command == ["mvn", "-B", "-ntp", "-DskipTests", "clean", "compile"]


def test_cleanup_removes_only_successful_default_fixture(tmp_path):
successful_fixture = tmp_path / "successful"
failed_fixture = tmp_path / "failed"
requested_fixture = tmp_path / "requested"
for fixture in (successful_fixture, failed_fixture, requested_fixture):
fixture.mkdir()

cleanup_fixture(successful_fixture, keep_fixture=False, compile_succeeded=True)
cleanup_fixture(failed_fixture, keep_fixture=False, compile_succeeded=False)
cleanup_fixture(requested_fixture, keep_fixture=True, compile_succeeded=True)

assert not successful_fixture.exists()
assert failed_fixture.exists()
assert requested_fixture.exists()
Loading