feat: reorganize AI test plugin into four tabs
This commit is contained in:
@@ -16,6 +16,9 @@ import com.example.workutils.biz.aitest.project.AiTestProjectCatalog;
|
||||
import com.example.workutils.biz.aitest.project.MavenModule;
|
||||
import com.example.workutils.biz.aitest.project.MavenProjectAnalyzer;
|
||||
import com.example.workutils.biz.aitest.project.MavenProjectModel;
|
||||
import com.example.workutils.biz.aitest.project.ModuleLoadPresentation;
|
||||
import com.example.workutils.biz.aitest.ui.DirectoryPathSelector;
|
||||
import com.example.workutils.biz.aitest.ui.TaskProgressState;
|
||||
import com.example.workutils.system.support.AppExecutors;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -24,8 +27,8 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
@@ -46,8 +49,10 @@ public class AiTestPane extends BorderPane {
|
||||
private final TextField baselineField = new TextField("master");
|
||||
private final TextField targetRefField = new TextField("HEAD");
|
||||
private final CheckBox includeWorkingTree = new CheckBox("包含未提交改动");
|
||||
private final TextField jdk8Field = new TextField();
|
||||
private final TextField mavenField = new TextField();
|
||||
private final DirectoryPathSelector jdkSelector =
|
||||
new DirectoryPathSelector("JDK 8 Home", this::ownerWindow);
|
||||
private final DirectoryPathSelector mavenSelector =
|
||||
new DirectoryPathSelector("Maven Home", this::ownerWindow);
|
||||
private final TextField codeFreeField = new TextField("codefree");
|
||||
private final Spinner<Integer> coverageTarget = new Spinner<>(0, 100, 85);
|
||||
private final Spinner<Integer> repairRounds = new Spinner<>(1, 10, 3);
|
||||
@@ -59,6 +64,7 @@ public class AiTestPane extends BorderPane {
|
||||
private final Button integrateButton = new Button("合并到本地分支");
|
||||
private final ProgressBar progressBar = new ProgressBar(0);
|
||||
private final Label statusLabel = new Label("请选择项目并检查工具链");
|
||||
private final Label moduleStatusLabel = new Label("请选择项目");
|
||||
private final Label toolchainStatus = new Label("工具链未检查");
|
||||
private final TabPane resultTabs = new TabPane();
|
||||
private final ListView<String> generatedTests = new ListView<>();
|
||||
@@ -69,6 +75,7 @@ public class AiTestPane extends BorderPane {
|
||||
private final TextField targetBranch = new TextField();
|
||||
private final TextField commitMessage = new TextField("test: add AI generated incremental tests");
|
||||
private final LatestRequestGate projectLoadGate = new LatestRequestGate();
|
||||
private final TaskProgressState taskProgressState = new TaskProgressState();
|
||||
|
||||
private volatile AiTestRun currentRun;
|
||||
private volatile boolean busy;
|
||||
@@ -102,6 +109,8 @@ public class AiTestPane extends BorderPane {
|
||||
cancelButton.setDisable(true);
|
||||
projectCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
moduleCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
baselineField.setId("baseline-ref");
|
||||
targetRefField.setId("target-ref");
|
||||
projectCombo.setCellFactory(ignored -> projectCell());
|
||||
projectCombo.setButtonCell(projectCell());
|
||||
}
|
||||
@@ -120,14 +129,17 @@ public class AiTestPane extends BorderPane {
|
||||
addRow(selection, 0, "项目", projectCombo, "生成模块", moduleCombo);
|
||||
addRow(selection, 1, "基线分支/Commit", baselineField, "目标分支/Commit", targetRefField);
|
||||
selection.add(includeWorkingTree, 1, 2);
|
||||
selection.add(moduleStatusLabel, 3, 2, 2, 1);
|
||||
VBox projectPage = page(selection);
|
||||
|
||||
GridPane toolchain = grid();
|
||||
Button validateToolchain = new Button("检查并保存工具链");
|
||||
addRow(toolchain, 0, "JDK 8 Home", jdk8Field, "Maven Home", mavenField);
|
||||
addRow(toolchain, 0, "JDK 8 Home", jdkSelector, "Maven Home", mavenSelector);
|
||||
addRow(toolchain, 1, "CodeFree 命令", codeFreeField, "状态", toolchainStatus);
|
||||
addRow(toolchain, 2, "覆盖率目标", coverageTarget, "最大修复轮次", repairRounds);
|
||||
toolchain.add(validateToolchain, 4, 2);
|
||||
validateToolchain.setOnAction(event -> validateAndSaveToolchain());
|
||||
VBox toolchainPage = page(toolchain);
|
||||
|
||||
defaultSpecification.setPrefRowCount(7);
|
||||
projectSpecification.setPrefRowCount(7);
|
||||
@@ -136,6 +148,8 @@ public class AiTestPane extends BorderPane {
|
||||
specifications.setDividerPositions(0.5);
|
||||
Button saveSpecifications = new Button("保存质量规范");
|
||||
saveSpecifications.setOnAction(event -> saveSpecifications());
|
||||
VBox specificationPage = page(specifications, saveSpecifications);
|
||||
VBox.setVgrow(specifications, Priority.ALWAYS);
|
||||
|
||||
resultTabs.getTabs().addAll(tab("生成文件", generatedTests), tab("覆盖率", coverageArea),
|
||||
tab("运行日志", logArea), tab("历史摘要", historyArea));
|
||||
@@ -148,18 +162,19 @@ public class AiTestPane extends BorderPane {
|
||||
addRow(integration, 0, "目标本地分支", targetBranch, "提交信息", commitMessage);
|
||||
integration.add(reviewApproved, 0, 1, 2, 1);
|
||||
integration.add(integrateButton, 4, 1);
|
||||
|
||||
Accordion setup = new Accordion(new TitledPane("1. 项目与增量", selection),
|
||||
new TitledPane("2. JDK 8 / Maven / CodeFree", toolchain),
|
||||
new TitledPane("3. 单测质量规范", new VBox(8, specifications, saveSpecifications)));
|
||||
setup.setExpandedPane(setup.getPanes().get(0));
|
||||
VBox content = new VBox(12, setup, actions, resultTabs, titled("审核并合并", integration));
|
||||
VBox resultPage = page(actions, resultTabs, titled("审核并合并", integration));
|
||||
VBox.setVgrow(resultTabs, Priority.ALWAYS);
|
||||
return content;
|
||||
|
||||
TabPane mainTabs = new TabPane(tab("项目与增量", projectPage), tab("工具链", toolchainPage),
|
||||
tab("质量规范", specificationPage), tab("结果与合并", resultPage));
|
||||
mainTabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
|
||||
return mainTabs;
|
||||
}
|
||||
|
||||
private Node buildStatus() {
|
||||
progressBar.setMaxWidth(Double.MAX_VALUE);
|
||||
taskProgressState.finish();
|
||||
applyProgressState();
|
||||
HBox.setHgrow(progressBar, Priority.ALWAYS);
|
||||
HBox status = new HBox(12, progressBar, statusLabel);
|
||||
status.setAlignment(Pos.CENTER_LEFT);
|
||||
@@ -172,8 +187,10 @@ public class AiTestPane extends BorderPane {
|
||||
moduleCombo.valueProperty().addListener((ignored, oldValue, module) -> {
|
||||
AiTestProject project = projectCombo.getValue();
|
||||
if (project != null && module != null) {
|
||||
runAsync(() -> { modulePreferences.save(project.path(), module.artifactId()); return null; },
|
||||
ignoredResult -> { });
|
||||
runSilent(() -> {
|
||||
modulePreferences.save(project.path(), module.artifactId());
|
||||
return null;
|
||||
}, ignoredResult -> { }, failure -> appendLog("保存模块偏好失败: " + message(failure)));
|
||||
}
|
||||
});
|
||||
analyzeButton.setOnAction(event -> analyze());
|
||||
@@ -184,7 +201,7 @@ public class AiTestPane extends BorderPane {
|
||||
}
|
||||
|
||||
private void loadInitialData() {
|
||||
runAsync(() -> new InitialData(settingsService.load(), specificationService.loadDefault(),
|
||||
runSilent(() -> new InitialData(settingsService.load(), specificationService.loadDefault(),
|
||||
projectCatalog.load(), historyService.load()), data -> {
|
||||
applySettings(data.settings());
|
||||
defaultSpecification.setText(data.defaultSpec());
|
||||
@@ -192,6 +209,9 @@ public class AiTestPane extends BorderPane {
|
||||
historyArea.setText(data.history().stream().map(this::formatHistory)
|
||||
.reduce("", (left, right) -> left + right + "\n"));
|
||||
statusLabel.setText(data.projects().isEmpty() ? "project_paths.txt 中没有可用项目" : "请选择项目");
|
||||
}, failure -> {
|
||||
statusLabel.setText("初始化失败: " + message(failure));
|
||||
appendLog(statusLabel.getText());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -202,7 +222,7 @@ public class AiTestPane extends BorderPane {
|
||||
if (project == null || !project.available()) {
|
||||
return;
|
||||
}
|
||||
statusLabel.setText("正在加载项目模块...");
|
||||
moduleStatusLabel.setText("正在加载项目模块...");
|
||||
AppExecutors.io().submit(() -> {
|
||||
try {
|
||||
ProjectData data = new ProjectData(projectAnalyzer.analyze(project.path()),
|
||||
@@ -219,9 +239,8 @@ public class AiTestPane extends BorderPane {
|
||||
if (!projectLoadGate.isLatest(projectRequest)) {
|
||||
return;
|
||||
}
|
||||
statusLabel.setText(failure.getMessage() == null
|
||||
? failure.getClass().getSimpleName() : failure.getMessage());
|
||||
appendLog("项目加载失败: " + statusLabel.getText());
|
||||
moduleStatusLabel.setText("项目加载失败: " + message(failure));
|
||||
appendLog(moduleStatusLabel.getText());
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -230,14 +249,10 @@ public class AiTestPane extends BorderPane {
|
||||
private void applyProjectData(ProjectData data) {
|
||||
moduleCombo.setItems(FXCollections.observableArrayList(data.model().modules()));
|
||||
projectSpecification.setText(data.specification());
|
||||
MavenModule selected = data.model().moduleByArtifactId(data.preferredModule()).orElse(null);
|
||||
if (selected == null) {
|
||||
List<MavenModule> starts = data.model().modules().stream()
|
||||
.filter(module -> module.artifactId().endsWith("-start")).toList();
|
||||
selected = starts.size() == 1 ? starts.get(0) : null;
|
||||
}
|
||||
moduleCombo.setValue(selected);
|
||||
statusLabel.setText(selected == null ? "请选择生成模块" : "已选择模块 " + selected.artifactId());
|
||||
ModuleLoadPresentation presentation = ModuleLoadPresentation.from(
|
||||
data.model(), data.preferredModule());
|
||||
moduleCombo.setValue(presentation.selectedModule().orElse(null));
|
||||
moduleStatusLabel.setText(presentation.message());
|
||||
}
|
||||
|
||||
private void validateAndSaveToolchain() {
|
||||
@@ -255,13 +270,14 @@ public class AiTestPane extends BorderPane {
|
||||
|
||||
private void saveSpecifications() {
|
||||
AiTestProject project = projectCombo.getValue();
|
||||
runAsync(() -> {
|
||||
runSilent(() -> {
|
||||
specificationService.saveDefault(defaultSpecification.getText());
|
||||
if (project != null) {
|
||||
specificationService.saveProject(project.path(), projectSpecification.getText());
|
||||
}
|
||||
return null;
|
||||
}, ignored -> statusLabel.setText("质量规范已保存"));
|
||||
}, ignored -> statusLabel.setText("质量规范已保存"),
|
||||
failure -> statusLabel.setText("保存质量规范失败: " + message(failure)));
|
||||
}
|
||||
|
||||
private void analyze() {
|
||||
@@ -323,8 +339,11 @@ public class AiTestPane extends BorderPane {
|
||||
private void onProgress(AiTestProgress progress) {
|
||||
Platform.runLater(() -> {
|
||||
statusLabel.setText(progress.message());
|
||||
progressBar.setProgress(progress.round() <= 0 ? ProgressIndicator.INDETERMINATE_PROGRESS
|
||||
: Math.min(0.95, progress.round() / (double) Math.max(1, repairRounds.getValue())));
|
||||
if (progress.round() > 0) {
|
||||
taskProgressState.update(Math.min(0.95,
|
||||
progress.round() / (double) Math.max(1, repairRounds.getValue())));
|
||||
applyProgressState();
|
||||
}
|
||||
appendLog(progress.state() + " [轮次 " + progress.round() + "] " + progress.message());
|
||||
});
|
||||
}
|
||||
@@ -349,13 +368,34 @@ public class AiTestPane extends BorderPane {
|
||||
});
|
||||
}
|
||||
|
||||
private <T> void runSilent(Callable<T> operation, Consumer<T> success,
|
||||
Consumer<Exception> failureHandler) {
|
||||
AppExecutors.io().submit(() -> {
|
||||
try {
|
||||
T result = operation.call();
|
||||
Platform.runLater(() -> success.accept(result));
|
||||
} catch (Exception failure) {
|
||||
Platform.runLater(() -> failureHandler.accept(failure));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setBusy(boolean value) {
|
||||
busy = value;
|
||||
cancelButton.setDisable(!value);
|
||||
updateButtons();
|
||||
if (value) {
|
||||
progressBar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
|
||||
taskProgressState.start();
|
||||
} else {
|
||||
taskProgressState.finish();
|
||||
}
|
||||
applyProgressState();
|
||||
}
|
||||
|
||||
private void applyProgressState() {
|
||||
progressBar.setVisible(taskProgressState.visible());
|
||||
progressBar.setManaged(taskProgressState.visible());
|
||||
progressBar.setProgress(taskProgressState.progress());
|
||||
}
|
||||
|
||||
private void updateButtons() {
|
||||
@@ -367,15 +407,15 @@ public class AiTestPane extends BorderPane {
|
||||
}
|
||||
|
||||
private AiTestSettings settingsFromForm() {
|
||||
return new AiTestSettings(path(jdk8Field.getText()), path(mavenField.getText()),
|
||||
return new AiTestSettings(jdkSelector.getPath(), mavenSelector.getPath(),
|
||||
codeFreeField.getText().isBlank() ? "codefree" : codeFreeField.getText().trim(),
|
||||
coverageTarget.getValue(), repairRounds.getValue(), Duration.ofMinutes(5),
|
||||
Duration.ofMinutes(15));
|
||||
}
|
||||
|
||||
private void applySettings(AiTestSettings settings) {
|
||||
jdk8Field.setText(settings.jdk8Home().toString());
|
||||
mavenField.setText(settings.mavenHome().toString());
|
||||
jdkSelector.setPath(settings.jdk8Home());
|
||||
mavenSelector.setPath(settings.mavenHome());
|
||||
codeFreeField.setText(settings.codeFreeCommand());
|
||||
coverageTarget.getValueFactory().setValue(settings.coverageTarget());
|
||||
repairRounds.getValueFactory().setValue(settings.maxRepairRounds());
|
||||
@@ -402,6 +442,12 @@ public class AiTestPane extends BorderPane {
|
||||
return grid;
|
||||
}
|
||||
|
||||
private static VBox page(Node... nodes) {
|
||||
VBox page = new VBox(16, nodes);
|
||||
page.setPadding(new Insets(20));
|
||||
return page;
|
||||
}
|
||||
|
||||
private static void addRow(GridPane grid, int row, String leftLabel, Node left,
|
||||
String rightLabel, Node right) {
|
||||
grid.addRow(row, new Label(leftLabel), left, new Label(rightLabel), right);
|
||||
@@ -437,8 +483,12 @@ public class AiTestPane extends BorderPane {
|
||||
};
|
||||
}
|
||||
|
||||
private static Path path(String value) {
|
||||
return value == null || value.isBlank() ? Path.of("") : Path.of(value.trim());
|
||||
private Window ownerWindow() {
|
||||
return getScene() == null ? null : getScene().getWindow();
|
||||
}
|
||||
|
||||
private static String message(Exception failure) {
|
||||
return failure.getMessage() == null ? failure.getClass().getSimpleName() : failure.getMessage();
|
||||
}
|
||||
|
||||
private String formatHistory(AiTestRunSummary summary) {
|
||||
|
||||
@@ -1,35 +1,102 @@
|
||||
package com.example.workutils.biz.aitest;
|
||||
|
||||
import com.example.workutils.biz.aitest.application.AiTestHistoryService;
|
||||
import com.example.workutils.biz.aitest.application.AiTestWorkflow;
|
||||
import com.example.workutils.biz.aitest.config.AiTestModulePreferenceService;
|
||||
import com.example.workutils.biz.aitest.config.AiTestSettings;
|
||||
import com.example.workutils.biz.aitest.config.AiTestSettingsService;
|
||||
import com.example.workutils.biz.aitest.config.TestSpecificationService;
|
||||
import com.example.workutils.biz.aitest.project.AiTestProjectCatalog;
|
||||
import com.example.workutils.biz.aitest.project.MavenProjectAnalyzer;
|
||||
import com.example.workutils.biz.aitest.ui.DirectoryPathSelector;
|
||||
import com.example.workutils.system.ui.FxTestSupport;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.Accordion;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.control.TextField;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class AiTestPaneArchitectureTest {
|
||||
|
||||
@Test
|
||||
void exposesConfigurationWorkflowReviewAndIntegrationControls() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/com/example/workutils/biz/aitest/AiTestPane.java"));
|
||||
void presentsFourMainTabsWithoutAccordionAndStartsIdle() throws Exception {
|
||||
AiTestPane pane = createPane();
|
||||
|
||||
assertTrue(source.contains("projectCombo"));
|
||||
assertTrue(source.contains("moduleCombo"));
|
||||
assertTrue(source.contains("baselineField"));
|
||||
assertTrue(source.contains("targetRefField"));
|
||||
assertTrue(source.contains("new TextField(\"master\")"));
|
||||
assertTrue(source.contains("LatestRequestGate"));
|
||||
assertTrue(source.contains("isLatest(projectRequest)"));
|
||||
assertTrue(source.contains("includeWorkingTree"));
|
||||
assertTrue(source.contains("defaultSpecification"));
|
||||
assertTrue(source.contains("projectSpecification"));
|
||||
assertTrue(source.contains("analyzeButton"));
|
||||
assertTrue(source.contains("generateButton"));
|
||||
assertTrue(source.contains("cancelButton"));
|
||||
assertTrue(source.contains("integrateButton"));
|
||||
assertTrue(source.contains("progressBar"));
|
||||
assertTrue(source.contains("resultTabs"));
|
||||
assertTrue(source.contains("runAsync"));
|
||||
TabPane mainTabs = assertInstanceOf(TabPane.class, FxTestSupport.fx(pane::getCenter));
|
||||
assertEquals(List.of("项目与增量", "工具链", "质量规范", "结果与合并"),
|
||||
FxTestSupport.fx(() -> mainTabs.getTabs().stream().map(tab -> tab.getText()).toList()));
|
||||
assertEquals(0, FxTestSupport.fx(() -> tabDescendants(mainTabs, Accordion.class).size()));
|
||||
assertEquals(2, FxTestSupport.fx(
|
||||
() -> tabDescendants(mainTabs, DirectoryPathSelector.class).size()));
|
||||
|
||||
List<DirectoryPathSelector> selectors = FxTestSupport.fx(
|
||||
() -> tabDescendants(mainTabs, DirectoryPathSelector.class));
|
||||
assertFalse(FxTestSupport.fx(() -> selectors.get(0).pathField().isEditable()));
|
||||
assertFalse(FxTestSupport.fx(() -> selectors.get(1).pathField().isEditable()));
|
||||
|
||||
ProgressBar progress = FxTestSupport.fx(() -> descendants(pane, ProgressBar.class).get(0));
|
||||
assertFalse(FxTestSupport.fx(progress::isVisible));
|
||||
assertFalse(FxTestSupport.fx(progress::isManaged));
|
||||
|
||||
TextField baseline = FxTestSupport.fx(() -> textField(mainTabs, "baseline-ref"));
|
||||
TextField target = FxTestSupport.fx(() -> textField(mainTabs, "target-ref"));
|
||||
assertEquals("master", FxTestSupport.fx((Callable<String>) baseline::getText));
|
||||
assertEquals("HEAD", FxTestSupport.fx((Callable<String>) target::getText));
|
||||
}
|
||||
|
||||
private static AiTestPane createPane() throws Exception {
|
||||
AiTestSettingsService settingsService = mock(AiTestSettingsService.class);
|
||||
when(settingsService.load()).thenReturn(new AiTestSettings(Path.of("jdk8"), Path.of("maven"),
|
||||
"codefree", 85, 3, Duration.ofMinutes(5), Duration.ofMinutes(15)));
|
||||
TestSpecificationService specifications = mock(TestSpecificationService.class);
|
||||
when(specifications.loadDefault()).thenReturn("");
|
||||
AiTestProjectCatalog catalog = mock(AiTestProjectCatalog.class);
|
||||
when(catalog.load()).thenReturn(List.of());
|
||||
AiTestHistoryService history = mock(AiTestHistoryService.class);
|
||||
when(history.load()).thenReturn(List.of());
|
||||
return FxTestSupport.fx(() -> new AiTestPane(mock(AiTestWorkflow.class), catalog,
|
||||
mock(MavenProjectAnalyzer.class), settingsService, specifications,
|
||||
mock(AiTestModulePreferenceService.class), history, mock(ToolchainValidator.class)));
|
||||
}
|
||||
|
||||
private static TextField textField(TabPane tabs, String id) {
|
||||
return tabDescendants(tabs, TextField.class).stream()
|
||||
.filter(field -> id.equals(field.getId()))
|
||||
.findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private static <T extends Node> List<T> tabDescendants(TabPane tabs, Class<T> type) {
|
||||
List<T> result = new ArrayList<>();
|
||||
tabs.getTabs().forEach(tab -> collect(tab.getContent(), type, result));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static <T extends Node> List<T> descendants(Parent root, Class<T> type) {
|
||||
List<T> result = new ArrayList<>();
|
||||
collect(root, type, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static <T extends Node> void collect(Node node, Class<T> type, List<T> result) {
|
||||
if (type.isInstance(node)) {
|
||||
result.add(type.cast(node));
|
||||
}
|
||||
if (node instanceof Parent parent) {
|
||||
parent.getChildrenUnmodifiable().forEach(child -> collect(child, type, result));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user