From 52e478a2be6612b4a18a1e51755fc1ff5470269d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Wed, 5 Aug 2026 14:10:04 +0800 Subject: [PATCH] feat: discover default AI test toolchain --- .../aitest/config/AiTestSettingsService.java | 21 +++- .../config/ToolchainSettingsDetector.java | 112 ++++++++++++++++++ .../config/AiTestSettingsServiceTest.java | 12 +- .../config/ToolchainSettingsDetectorTest.java | 38 ++++++ 4 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/example/workutils/biz/aitest/config/ToolchainSettingsDetector.java create mode 100644 src/test/java/com/example/workutils/biz/aitest/config/ToolchainSettingsDetectorTest.java diff --git a/src/main/java/com/example/workutils/biz/aitest/config/AiTestSettingsService.java b/src/main/java/com/example/workutils/biz/aitest/config/AiTestSettingsService.java index 28d56fb..17de3b9 100644 --- a/src/main/java/com/example/workutils/biz/aitest/config/AiTestSettingsService.java +++ b/src/main/java/com/example/workutils/biz/aitest/config/AiTestSettingsService.java @@ -19,6 +19,7 @@ import java.util.Properties; public class AiTestSettingsService { private final Path settingsFile; + private final ToolchainSettingsDetector detector; @Autowired public AiTestSettingsService(WorkUtilsProperties properties) { @@ -29,20 +30,31 @@ public class AiTestSettingsService { this(paths.resolve("data", "ai-test")); } + AiTestSettingsService(ApplicationPaths paths, ToolchainSettingsDetector detector) { + this(paths.resolve("data", "ai-test"), detector); + } + private AiTestSettingsService(Path aiTestDataDirectory) { + this(aiTestDataDirectory, ToolchainSettingsDetector.system()); + } + + AiTestSettingsService(Path aiTestDataDirectory, ToolchainSettingsDetector detector) { this.settingsFile = aiTestDataDirectory.resolve("settings.properties"); + this.detector = detector; } public AiTestSettings load() throws IOException { if (Files.notExists(settingsFile)) { - return AiTestSettings.defaults(); + AiTestSettings detected = detector.complete(AiTestSettings.defaults()); + save(detected); + return detected; } Properties values = new Properties(); try (InputStream input = Files.newInputStream(settingsFile)) { values.load(input); } AiTestSettings defaults = AiTestSettings.defaults(); - return new AiTestSettings( + AiTestSettings loaded = new AiTestSettings( Path.of(values.getProperty("jdk8.home", defaults.jdk8Home().toString())), Path.of(values.getProperty("maven.home", defaults.mavenHome().toString())), values.getProperty("codefree.command", defaults.codeFreeCommand()), @@ -52,6 +64,11 @@ public class AiTestSettingsService { Math.toIntExact(defaults.codeFreeTimeout().toSeconds()))), Duration.ofSeconds(integer(values, "maven.timeout.seconds", Math.toIntExact(defaults.mavenTimeout().toSeconds())))); + AiTestSettings completed = detector.complete(loaded); + if (!completed.equals(loaded)) { + save(completed); + } + return completed; } public void save(AiTestSettings settings) throws IOException { diff --git a/src/main/java/com/example/workutils/biz/aitest/config/ToolchainSettingsDetector.java b/src/main/java/com/example/workutils/biz/aitest/config/ToolchainSettingsDetector.java new file mode 100644 index 0000000..32db816 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/aitest/config/ToolchainSettingsDetector.java @@ -0,0 +1,112 @@ +package com.example.workutils.biz.aitest.config; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public final class ToolchainSettingsDetector { + private static final Path WINDOWS_JDK8 = Path.of("C:/Program Files/Java/jdk-1.8"); + private static final Path WINDOWS_MAVEN = Path.of("D:/bestpay-maven/apache-maven-3.8.6"); + + private final Map environment; + private final Map properties; + private final Supplier mavenVersion; + private final Predicate directoryExists; + private final String osName; + + public ToolchainSettingsDetector(Map environment, Map properties, + Supplier mavenVersion, Predicate directoryExists, + String osName) { + this.environment = environment; + this.properties = properties; + this.mavenVersion = mavenVersion; + this.directoryExists = directoryExists; + this.osName = osName; + } + + public static ToolchainSettingsDetector system() { + return new ToolchainSettingsDetector(System.getenv(), + Map.of("java.home", System.getProperty("java.home", "")), + ToolchainSettingsDetector::readMavenVersion, Files::isDirectory, + System.getProperty("os.name", "")); + } + + public AiTestSettings complete(AiTestSettings saved) { + Path jdk = specified(saved.jdk8Home()) ? saved.jdk8Home() : detectJdk8(); + Path maven = specified(saved.mavenHome()) ? saved.mavenHome() : detectMaven(); + return new AiTestSettings(jdk, maven, saved.codeFreeCommand(), saved.coverageTarget(), + saved.maxRepairRounds(), saved.codeFreeTimeout(), saved.mavenTimeout()); + } + + private Path detectJdk8() { + Path environmentHome = path(environment.get("JAVA_HOME")); + if (usable(environmentHome)) return normalizeJdk(environmentHome); + Path runtimeHome = normalizeJdk(path(properties.get("java.home"))); + if (usable(runtimeHome)) return runtimeHome; + return windows() && usable(WINDOWS_JDK8) ? WINDOWS_JDK8 : Path.of(""); + } + + private Path detectMaven() { + for (String key : new String[]{"MAVEN_HOME", "M2_HOME"}) { + Path candidate = path(environment.get(key)); + if (usable(candidate)) return candidate; + } + Path parsed = parseMavenHome(mavenVersion.get()); + if (usable(parsed)) return parsed; + return windows() && usable(WINDOWS_MAVEN) ? WINDOWS_MAVEN : Path.of(""); + } + + static Path parseMavenHome(String output) { + if (output != null) { + for (String line : output.split("\\R")) { + if (line.regionMatches(true, 0, "Maven home:", 0, "Maven home:".length())) { + return path(line.substring("Maven home:".length()).trim()); + } + } + } + return Path.of(""); + } + + private Path normalizeJdk(Path value) { + if (value != null && value.getFileName() != null + && "jre".equalsIgnoreCase(value.getFileName().toString())) { + return value.getParent(); + } + return value; + } + + private boolean usable(Path path) { + return path != null && !path.toString().isBlank() && directoryExists.test(path); + } + + private static boolean specified(Path path) { + return path != null && !path.toString().isBlank(); + } + + private boolean windows() { + return osName.toLowerCase().contains("win"); + } + + private static Path path(String value) { + return value == null || value.isBlank() ? Path.of("") : Path.of(value.trim()); + } + + private static String readMavenVersion() { + try { + Process process = new ProcessBuilder("mvn", "-version").redirectErrorStream(true).start(); + if (!process.waitFor(5, TimeUnit.SECONDS)) { + process.destroyForcibly(); + return ""; + } + return new String(process.getInputStream().readAllBytes(), Charset.defaultCharset()); + } catch (IOException | InterruptedException ignored) { + if (ignored instanceof InterruptedException) Thread.currentThread().interrupt(); + return ""; + } + } +} diff --git a/src/test/java/com/example/workutils/biz/aitest/config/AiTestSettingsServiceTest.java b/src/test/java/com/example/workutils/biz/aitest/config/AiTestSettingsServiceTest.java index 5f3e74c..ea368ac 100644 --- a/src/test/java/com/example/workutils/biz/aitest/config/AiTestSettingsServiceTest.java +++ b/src/test/java/com/example/workutils/biz/aitest/config/AiTestSettingsServiceTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.io.TempDir; import java.nio.file.Path; import java.time.Duration; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -26,11 +27,18 @@ class AiTestSettingsServiceTest { @Test void returnsSafeDefaultsWhenNoSettingsExist(@TempDir Path home) throws Exception { - AiTestSettings settings = new AiTestSettingsService(ApplicationPaths.of(home)).load(); + ToolchainSettingsDetector detector = new ToolchainSettingsDetector( + Map.of("JAVA_HOME", "C:/jdk8", "MAVEN_HOME", "C:/maven"), Map.of(), + () -> "", ignored -> true, "Windows 11"); + AiTestSettingsService service = new AiTestSettingsService(ApplicationPaths.of(home), detector); + + AiTestSettings settings = service.load(); assertEquals(85, settings.coverageTarget()); assertEquals(3, settings.maxRepairRounds()); assertEquals("codefree", settings.codeFreeCommand()); - assertTrue(settings.jdk8Home().toString().isEmpty()); + assertEquals(Path.of("C:/jdk8"), settings.jdk8Home()); + assertEquals(Path.of("C:/maven"), settings.mavenHome()); + assertTrue(java.nio.file.Files.exists(service.settingsFile())); } } diff --git a/src/test/java/com/example/workutils/biz/aitest/config/ToolchainSettingsDetectorTest.java b/src/test/java/com/example/workutils/biz/aitest/config/ToolchainSettingsDetectorTest.java new file mode 100644 index 0000000..0842b3d --- /dev/null +++ b/src/test/java/com/example/workutils/biz/aitest/config/ToolchainSettingsDetectorTest.java @@ -0,0 +1,38 @@ +package com.example.workutils.biz.aitest.config; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ToolchainSettingsDetectorTest { + + @Test + void discoversJdkRootFromJavaHomeJreAndParsesMavenHome() { + ToolchainSettingsDetector detector = new ToolchainSettingsDetector(Map.of(), + Map.of("java.home", "C:/Program Files/Java/jdk-1.8/jre"), + () -> "Apache Maven 3.8.6\nMaven home: D:/bestpay-maven/apache-maven-3.8.6", + ignored -> true, "Windows 11"); + + AiTestSettings result = detector.complete(AiTestSettings.defaults()); + + assertEquals(Path.of("C:/Program Files/Java/jdk-1.8"), result.jdk8Home()); + assertEquals(Path.of("D:/bestpay-maven/apache-maven-3.8.6"), result.mavenHome()); + } + + @Test + void keepsUsableSavedPathsAheadOfDiscoveredValues() { + ToolchainSettingsDetector detector = new ToolchainSettingsDetector( + Map.of("JAVA_HOME", "C:/auto-jdk", "MAVEN_HOME", "C:/auto-maven"), + Map.of(), () -> "", ignored -> true, "Windows 11"); + AiTestSettings saved = new AiTestSettings(Path.of("C:/saved-jdk"), Path.of("C:/saved-maven"), + "codefree", 85, 3, java.time.Duration.ofMinutes(5), java.time.Duration.ofMinutes(15)); + + AiTestSettings result = detector.complete(saved); + + assertEquals(saved.jdk8Home(), result.jdk8Home()); + assertEquals(saved.mavenHome(), result.mavenHome()); + } +}