feat: add AI unit test generation plugin UI
This commit is contained in:
@@ -0,0 +1,425 @@
|
|||||||
|
package com.example.workutils.biz.aitest;
|
||||||
|
|
||||||
|
import com.example.workutils.biz.aitest.application.AiTestProgress;
|
||||||
|
import com.example.workutils.biz.aitest.application.AiTestHistoryService;
|
||||||
|
import com.example.workutils.biz.aitest.application.AiTestRunSummary;
|
||||||
|
import com.example.workutils.biz.aitest.application.AiTestRequest;
|
||||||
|
import com.example.workutils.biz.aitest.application.AiTestRun;
|
||||||
|
import com.example.workutils.biz.aitest.application.AiTestRunState;
|
||||||
|
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.AiTestProject;
|
||||||
|
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.system.support.AppExecutors;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class AiTestPane extends BorderPane {
|
||||||
|
private final AiTestWorkflow workflow;
|
||||||
|
private final AiTestProjectCatalog projectCatalog;
|
||||||
|
private final MavenProjectAnalyzer projectAnalyzer;
|
||||||
|
private final AiTestSettingsService settingsService;
|
||||||
|
private final TestSpecificationService specificationService;
|
||||||
|
private final AiTestModulePreferenceService modulePreferences;
|
||||||
|
private final AiTestHistoryService historyService;
|
||||||
|
private final ToolchainValidator toolchainValidator;
|
||||||
|
|
||||||
|
private final ComboBox<AiTestProject> projectCombo = new ComboBox<>();
|
||||||
|
private final ComboBox<MavenModule> moduleCombo = new ComboBox<>();
|
||||||
|
private final TextField baselineField = new TextField("HEAD~1");
|
||||||
|
private final CheckBox includeWorkingTree = new CheckBox("包含未提交改动");
|
||||||
|
private final TextField jdk8Field = new TextField();
|
||||||
|
private final TextField mavenField = new TextField();
|
||||||
|
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);
|
||||||
|
private final TextArea defaultSpecification = new TextArea();
|
||||||
|
private final TextArea projectSpecification = new TextArea();
|
||||||
|
private final Button analyzeButton = new Button("分析增量");
|
||||||
|
private final Button generateButton = new Button("生成并验证");
|
||||||
|
private final Button cancelButton = new Button("取消");
|
||||||
|
private final Button integrateButton = new Button("合并到本地分支");
|
||||||
|
private final ProgressBar progressBar = new ProgressBar(0);
|
||||||
|
private final Label statusLabel = new Label("请选择项目并检查工具链");
|
||||||
|
private final Label toolchainStatus = new Label("工具链未检查");
|
||||||
|
private final TabPane resultTabs = new TabPane();
|
||||||
|
private final ListView<String> generatedTests = new ListView<>();
|
||||||
|
private final TextArea coverageArea = readOnlyArea();
|
||||||
|
private final TextArea logArea = readOnlyArea();
|
||||||
|
private final TextArea historyArea = readOnlyArea();
|
||||||
|
private final CheckBox reviewApproved = new CheckBox("我已审核生成的测试代码");
|
||||||
|
private final TextField targetBranch = new TextField();
|
||||||
|
private final TextField commitMessage = new TextField("test: add AI generated incremental tests");
|
||||||
|
|
||||||
|
private volatile AiTestRun currentRun;
|
||||||
|
private volatile boolean busy;
|
||||||
|
|
||||||
|
public AiTestPane(AiTestWorkflow workflow, AiTestProjectCatalog projectCatalog,
|
||||||
|
MavenProjectAnalyzer projectAnalyzer, AiTestSettingsService settingsService,
|
||||||
|
TestSpecificationService specificationService,
|
||||||
|
AiTestModulePreferenceService modulePreferences,
|
||||||
|
AiTestHistoryService historyService,
|
||||||
|
ToolchainValidator toolchainValidator) {
|
||||||
|
this.workflow = workflow;
|
||||||
|
this.projectCatalog = projectCatalog;
|
||||||
|
this.projectAnalyzer = projectAnalyzer;
|
||||||
|
this.settingsService = settingsService;
|
||||||
|
this.specificationService = specificationService;
|
||||||
|
this.modulePreferences = modulePreferences;
|
||||||
|
this.historyService = historyService;
|
||||||
|
this.toolchainValidator = toolchainValidator;
|
||||||
|
buildUi();
|
||||||
|
bindActions();
|
||||||
|
loadInitialData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildUi() {
|
||||||
|
setPadding(new Insets(20));
|
||||||
|
setTop(buildHeader());
|
||||||
|
setCenter(buildContent());
|
||||||
|
setBottom(buildStatus());
|
||||||
|
generateButton.setDisable(true);
|
||||||
|
integrateButton.setDisable(true);
|
||||||
|
cancelButton.setDisable(true);
|
||||||
|
projectCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
moduleCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
projectCombo.setCellFactory(ignored -> projectCell());
|
||||||
|
projectCombo.setButtonCell(projectCell());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node buildHeader() {
|
||||||
|
Label title = new Label("AI 单测生成");
|
||||||
|
title.setStyle("-fx-font-size: 22px; -fx-font-weight: bold;");
|
||||||
|
Label subtitle = new Label("CodeFree CLI · JDK 8 · Maven · JUnit 5 · 85% 增量行覆盖率");
|
||||||
|
VBox header = new VBox(5, title, subtitle);
|
||||||
|
header.setPadding(new Insets(0, 0, 16, 0));
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node buildContent() {
|
||||||
|
GridPane selection = grid();
|
||||||
|
addRow(selection, 0, "项目", projectCombo, "生成模块", moduleCombo);
|
||||||
|
addRow(selection, 1, "基线分支/Commit", baselineField, "工作区", includeWorkingTree);
|
||||||
|
|
||||||
|
GridPane toolchain = grid();
|
||||||
|
Button validateToolchain = new Button("检查并保存工具链");
|
||||||
|
addRow(toolchain, 0, "JDK 8 Home", jdk8Field, "Maven Home", mavenField);
|
||||||
|
addRow(toolchain, 1, "CodeFree 命令", codeFreeField, "状态", toolchainStatus);
|
||||||
|
addRow(toolchain, 2, "覆盖率目标", coverageTarget, "最大修复轮次", repairRounds);
|
||||||
|
toolchain.add(validateToolchain, 4, 2);
|
||||||
|
validateToolchain.setOnAction(event -> validateAndSaveToolchain());
|
||||||
|
|
||||||
|
defaultSpecification.setPrefRowCount(7);
|
||||||
|
projectSpecification.setPrefRowCount(7);
|
||||||
|
SplitPane specifications = new SplitPane(titled("默认质量规范(可编辑)", defaultSpecification),
|
||||||
|
titled("当前项目规范(可编辑)", projectSpecification));
|
||||||
|
specifications.setDividerPositions(0.5);
|
||||||
|
Button saveSpecifications = new Button("保存质量规范");
|
||||||
|
saveSpecifications.setOnAction(event -> saveSpecifications());
|
||||||
|
|
||||||
|
resultTabs.getTabs().addAll(tab("生成文件", generatedTests), tab("覆盖率", coverageArea),
|
||||||
|
tab("运行日志", logArea), tab("历史摘要", historyArea));
|
||||||
|
resultTabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
|
||||||
|
VBox.setVgrow(resultTabs, Priority.ALWAYS);
|
||||||
|
|
||||||
|
HBox actions = new HBox(10, analyzeButton, generateButton, cancelButton);
|
||||||
|
actions.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
GridPane integration = grid();
|
||||||
|
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.setVgrow(resultTabs, Priority.ALWAYS);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node buildStatus() {
|
||||||
|
progressBar.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
HBox.setHgrow(progressBar, Priority.ALWAYS);
|
||||||
|
HBox status = new HBox(12, progressBar, statusLabel);
|
||||||
|
status.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
status.setPadding(new Insets(12, 0, 0, 0));
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindActions() {
|
||||||
|
projectCombo.valueProperty().addListener((ignored, oldValue, project) -> loadProject(project));
|
||||||
|
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 -> { });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
analyzeButton.setOnAction(event -> analyze());
|
||||||
|
generateButton.setOnAction(event -> generate());
|
||||||
|
cancelButton.setOnAction(event -> workflow.cancel());
|
||||||
|
integrateButton.setOnAction(event -> integrate());
|
||||||
|
reviewApproved.selectedProperty().addListener((ignored, oldValue, approved) -> updateButtons());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadInitialData() {
|
||||||
|
runAsync(() -> new InitialData(settingsService.load(), specificationService.loadDefault(),
|
||||||
|
projectCatalog.load(), historyService.load()), data -> {
|
||||||
|
applySettings(data.settings());
|
||||||
|
defaultSpecification.setText(data.defaultSpec());
|
||||||
|
projectCombo.setItems(FXCollections.observableArrayList(data.projects()));
|
||||||
|
historyArea.setText(data.history().stream().map(this::formatHistory)
|
||||||
|
.reduce("", (left, right) -> left + right + "\n"));
|
||||||
|
statusLabel.setText(data.projects().isEmpty() ? "project_paths.txt 中没有可用项目" : "请选择项目");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadProject(AiTestProject project) {
|
||||||
|
if (project == null || !project.available()) {
|
||||||
|
moduleCombo.getItems().clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runAsync(() -> new ProjectData(projectAnalyzer.analyze(project.path()),
|
||||||
|
specificationService.loadProject(project.path()),
|
||||||
|
modulePreferences.load(project.path()).orElse("")), 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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateAndSaveToolchain() {
|
||||||
|
AiTestSettings settings = settingsFromForm();
|
||||||
|
runAsync(() -> {
|
||||||
|
settingsService.save(settings);
|
||||||
|
return toolchainValidator.validate(settings, this::appendLog);
|
||||||
|
}, validation -> {
|
||||||
|
toolchainStatus.setText(validation.ready() ? "可用" : "检查失败");
|
||||||
|
toolchainStatus.setStyle(validation.ready() ? "-fx-text-fill: #16803c" : "-fx-text-fill: #c53030");
|
||||||
|
validation.checks().forEach(check -> appendLog(check.name() + ": "
|
||||||
|
+ (check.successful() ? "通过" : "失败") + "\n" + check.detail()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveSpecifications() {
|
||||||
|
AiTestProject project = projectCombo.getValue();
|
||||||
|
runAsync(() -> {
|
||||||
|
specificationService.saveDefault(defaultSpecification.getText());
|
||||||
|
if (project != null) {
|
||||||
|
specificationService.saveProject(project.path(), projectSpecification.getText());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, ignored -> statusLabel.setText("质量规范已保存"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void analyze() {
|
||||||
|
AiTestProject project = projectCombo.getValue();
|
||||||
|
MavenModule module = moduleCombo.getValue();
|
||||||
|
if (project == null || module == null || baselineField.getText().isBlank()) {
|
||||||
|
statusLabel.setText("请选择项目、模块并填写基线");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AiTestRequest request = new AiTestRequest(project.path(), module.artifactId(),
|
||||||
|
baselineField.getText().trim(), includeWorkingTree.isSelected(), settingsFromForm());
|
||||||
|
runAsync(() -> workflow.analyze(request, this::onProgress), run -> {
|
||||||
|
currentRun = run;
|
||||||
|
renderRun(run);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generate() {
|
||||||
|
AiTestRun analyzed = currentRun;
|
||||||
|
if (analyzed == null || analyzed.state() != AiTestRunState.ANALYZED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runAsync(() -> workflow.generateAndVerify(analyzed, this::onProgress), run -> {
|
||||||
|
currentRun = run;
|
||||||
|
renderRun(run);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void integrate() {
|
||||||
|
AiTestRun reviewed = currentRun;
|
||||||
|
if (!reviewApproved.isSelected() || reviewed == null || targetBranch.getText().isBlank()) {
|
||||||
|
statusLabel.setText("请审核测试并填写目标本地分支");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runAsync(() -> workflow.integrate(reviewed, targetBranch.getText().trim(),
|
||||||
|
commitMessage.getText().trim(), this::onProgress), run -> {
|
||||||
|
currentRun = run;
|
||||||
|
renderRun(run);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderRun(AiTestRun run) {
|
||||||
|
statusLabel.setText(run.message());
|
||||||
|
generatedTests.setItems(FXCollections.observableArrayList(run.generatedFiles().stream()
|
||||||
|
.map(file -> file.path() + "\n\n" + file.content()).toList()));
|
||||||
|
coverageArea.setText(run.coverage() == null ? "尚无覆盖率结果" :
|
||||||
|
"增量行覆盖率: " + run.coverage().linePercent() + "%\n"
|
||||||
|
+ "增量分支覆盖率: " + run.coverage().branchPercent() + "%\n"
|
||||||
|
+ "目标: " + run.coverage().targetPercent() + "%\n"
|
||||||
|
+ "结果: " + (run.coverage().passed() ? "通过" : "未通过"));
|
||||||
|
historyArea.appendText(run.id() + " | " + run.state() + " | " + run.message() + "\n");
|
||||||
|
progressBar.setProgress(run.state() == AiTestRunState.READY_FOR_REVIEW
|
||||||
|
|| run.state() == AiTestRunState.SUCCEEDED ? 1 : 0);
|
||||||
|
updateButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
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())));
|
||||||
|
appendLog(progress.state() + " [轮次 " + progress.round() + "] " + progress.message());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> void runAsync(Callable<T> operation, Consumer<T> success) {
|
||||||
|
setBusy(true);
|
||||||
|
AppExecutors.io().submit(() -> {
|
||||||
|
try {
|
||||||
|
T result = operation.call();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
setBusy(false);
|
||||||
|
success.accept(result);
|
||||||
|
});
|
||||||
|
} catch (Exception failure) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
setBusy(false);
|
||||||
|
statusLabel.setText(failure.getMessage() == null
|
||||||
|
? failure.getClass().getSimpleName() : failure.getMessage());
|
||||||
|
appendLog("失败: " + statusLabel.getText());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setBusy(boolean value) {
|
||||||
|
busy = value;
|
||||||
|
cancelButton.setDisable(!value);
|
||||||
|
updateButtons();
|
||||||
|
if (value) {
|
||||||
|
progressBar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateButtons() {
|
||||||
|
analyzeButton.setDisable(busy);
|
||||||
|
generateButton.setDisable(busy || currentRun == null
|
||||||
|
|| currentRun.state() != AiTestRunState.ANALYZED);
|
||||||
|
integrateButton.setDisable(busy || !reviewApproved.isSelected() || currentRun == null
|
||||||
|
|| currentRun.state() != AiTestRunState.READY_FOR_REVIEW);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AiTestSettings settingsFromForm() {
|
||||||
|
return new AiTestSettings(path(jdk8Field.getText()), path(mavenField.getText()),
|
||||||
|
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());
|
||||||
|
codeFreeField.setText(settings.codeFreeCommand());
|
||||||
|
coverageTarget.getValueFactory().setValue(settings.coverageTarget());
|
||||||
|
repairRounds.getValueFactory().setValue(settings.maxRepairRounds());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendLog(String text) {
|
||||||
|
if (Platform.isFxApplicationThread()) {
|
||||||
|
logArea.appendText(text + "\n");
|
||||||
|
} else {
|
||||||
|
Platform.runLater(() -> logArea.appendText(text + "\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GridPane grid() {
|
||||||
|
GridPane grid = new GridPane();
|
||||||
|
grid.setHgap(10);
|
||||||
|
grid.setVgap(10);
|
||||||
|
grid.setPadding(new Insets(10));
|
||||||
|
ColumnConstraints label = new ColumnConstraints();
|
||||||
|
ColumnConstraints input = new ColumnConstraints();
|
||||||
|
input.setHgrow(Priority.ALWAYS);
|
||||||
|
input.setPercentWidth(35);
|
||||||
|
grid.getColumnConstraints().addAll(label, input, label, input, new ColumnConstraints());
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
GridPane.setHgrow(left, Priority.ALWAYS);
|
||||||
|
GridPane.setHgrow(right, Priority.ALWAYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TitledPane titled(String title, Node content) {
|
||||||
|
TitledPane pane = new TitledPane(title, content);
|
||||||
|
pane.setCollapsible(false);
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Tab tab(String title, Node content) {
|
||||||
|
return new Tab(title, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextArea readOnlyArea() {
|
||||||
|
TextArea area = new TextArea();
|
||||||
|
area.setEditable(false);
|
||||||
|
area.setWrapText(false);
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ListCell<AiTestProject> projectCell() {
|
||||||
|
return new ListCell<>() {
|
||||||
|
@Override
|
||||||
|
protected void updateItem(AiTestProject item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
setText(empty || item == null ? null : item.name() + " — " + item.path()
|
||||||
|
+ (item.available() ? "" : "(不可用)"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path path(String value) {
|
||||||
|
return value == null || value.isBlank() ? Path.of("") : Path.of(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatHistory(AiTestRunSummary summary) {
|
||||||
|
return summary.updatedAt() + " | " + summary.project() + " | " + summary.module()
|
||||||
|
+ " | " + summary.state() + " | "
|
||||||
|
+ (summary.lineCoverage() == null ? "-" : summary.lineCoverage() + "%")
|
||||||
|
+ " | " + summary.message();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record InitialData(AiTestSettings settings, String defaultSpec,
|
||||||
|
List<AiTestProject> projects, List<AiTestRunSummary> history) { }
|
||||||
|
private record ProjectData(MavenProjectModel model, String specification, String preferredModule) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.example.workutils.biz.aitest;
|
||||||
|
|
||||||
|
import com.example.workutils.biz.aitest.application.AiTestWorkflow;
|
||||||
|
import com.example.workutils.biz.aitest.application.AiTestHistoryService;
|
||||||
|
import com.example.workutils.biz.aitest.config.AiTestModulePreferenceService;
|
||||||
|
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.system.plugin.Plugin;
|
||||||
|
import com.example.workutils.system.plugin.PluginDescriptor;
|
||||||
|
import com.example.workutils.system.plugin.VisualPluginFactory;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AiTestPlugin implements Plugin {
|
||||||
|
private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder()
|
||||||
|
.id("ai-unit-test")
|
||||||
|
.name("AI 单测生成")
|
||||||
|
.version("1.0.0")
|
||||||
|
.description("按 Git 增量生成、验证并合并 JUnit 5 单测")
|
||||||
|
.order(45)
|
||||||
|
.icon("AI")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private final AiTestWorkflow workflow;
|
||||||
|
private final AiTestProjectCatalog projectCatalog;
|
||||||
|
private final MavenProjectAnalyzer projectAnalyzer;
|
||||||
|
private final AiTestSettingsService settingsService;
|
||||||
|
private final TestSpecificationService specificationService;
|
||||||
|
private final AiTestModulePreferenceService modulePreferences;
|
||||||
|
private final AiTestHistoryService historyService;
|
||||||
|
private final ToolchainValidator toolchainValidator;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginDescriptor getDescriptor() {
|
||||||
|
return DESCRIPTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VisualPluginFactory getVisualFactory() {
|
||||||
|
return new VisualPluginFactory() {
|
||||||
|
private AiTestPane pane;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Parent getEmbeddedPane() {
|
||||||
|
if (pane == null) {
|
||||||
|
pane = createPane();
|
||||||
|
}
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void open(Runnable onClosed) {
|
||||||
|
AiTestPane content = createPane();
|
||||||
|
Stage stage = new Stage();
|
||||||
|
stage.setTitle(DESCRIPTOR.getName());
|
||||||
|
stage.setScene(new Scene(content, 1180, 820));
|
||||||
|
stage.setOnHidden(event -> onClosed.run());
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AiTestPane createPane() {
|
||||||
|
return new AiTestPane(workflow, projectCatalog, projectAnalyzer, settingsService,
|
||||||
|
specificationService, modulePreferences, historyService, toolchainValidator);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.example.workutils.biz.aitest;
|
||||||
|
|
||||||
|
public record ToolchainCheck(String name, boolean successful, String detail) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.example.workutils.biz.aitest;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record ToolchainValidation(boolean ready, List<ToolchainCheck> checks) {
|
||||||
|
public ToolchainValidation {
|
||||||
|
checks = List.copyOf(checks);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.example.workutils.biz.aitest;
|
||||||
|
|
||||||
|
import com.example.workutils.biz.aitest.config.AiTestSettings;
|
||||||
|
import com.example.workutils.biz.aitest.process.CommandResult;
|
||||||
|
import com.example.workutils.biz.aitest.process.CommandSpec;
|
||||||
|
import com.example.workutils.biz.aitest.process.ProcessExecutor;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ToolchainValidator {
|
||||||
|
private final ProcessExecutor executor;
|
||||||
|
|
||||||
|
public ToolchainValidation validate(AiTestSettings settings, Consumer<String> output) {
|
||||||
|
List<ToolchainCheck> checks = new ArrayList<>();
|
||||||
|
checks.add(check("JDK 8", javaCommand(settings), javaEnvironment(settings), "1.8", output));
|
||||||
|
checks.add(check("Maven", mavenCommand(settings), javaEnvironment(settings), "Apache Maven", output));
|
||||||
|
checks.add(check("CodeFree CLI", shellCommand(settings.codeFreeCommand(), "--version"),
|
||||||
|
Map.of(), "", output));
|
||||||
|
return new ToolchainValidation(checks.stream().allMatch(ToolchainCheck::successful), checks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ToolchainCheck check(String name, List<String> command, Map<String, String> environment,
|
||||||
|
String requiredText, Consumer<String> output) {
|
||||||
|
CommandResult result = executor.execute(new CommandSpec(Path.of(".").toAbsolutePath().normalize(),
|
||||||
|
command, environment, Duration.ofSeconds(30)), output);
|
||||||
|
String detail = (result.stdout() + System.lineSeparator() + result.stderr()).trim();
|
||||||
|
boolean valid = result.successful() && (requiredText.isBlank() || detail.contains(requiredText));
|
||||||
|
return new ToolchainCheck(name, valid, detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> javaCommand(AiTestSettings settings) {
|
||||||
|
String executable = settings.jdk8Home().toString().isBlank() ? "java"
|
||||||
|
: settings.jdk8Home().resolve("bin").resolve(isWindows() ? "java.exe" : "java").toString();
|
||||||
|
return shellCommand(executable, "-version");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> mavenCommand(AiTestSettings settings) {
|
||||||
|
String executable = settings.mavenHome().toString().isBlank() ? (isWindows() ? "mvn.cmd" : "mvn")
|
||||||
|
: settings.mavenHome().resolve("bin").resolve(isWindows() ? "mvn.cmd" : "mvn").toString();
|
||||||
|
return shellCommand(executable, "-version");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> shellCommand(String executable, String argument) {
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
if (isWindows()) {
|
||||||
|
command.add(System.getenv().getOrDefault("COMSPEC", "cmd.exe"));
|
||||||
|
command.add("/d");
|
||||||
|
command.add("/c");
|
||||||
|
}
|
||||||
|
command.add(executable);
|
||||||
|
command.add(argument);
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> javaEnvironment(AiTestSettings settings) {
|
||||||
|
if (settings.jdk8Home().toString().isBlank()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
Map<String, String> environment = new HashMap<>();
|
||||||
|
environment.put("JAVA_HOME", settings.jdk8Home().toAbsolutePath().normalize().toString());
|
||||||
|
return environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isWindows() {
|
||||||
|
return System.getProperty("os.name", "").toLowerCase().contains("win");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,15 +99,18 @@ public class AiTestWorkflow {
|
|||||||
String feedback = "首次生成";
|
String feedback = "首次生成";
|
||||||
|
|
||||||
for (int round = 1; round <= analyzed.request().settings().maxRepairRounds(); round++) {
|
for (int round = 1; round <= analyzed.request().settings().maxRepairRounds(); round++) {
|
||||||
|
int currentRound = round;
|
||||||
if (cancelled.get()) {
|
if (cancelled.get()) {
|
||||||
worktree.close();
|
worktree.close();
|
||||||
return finish(run.cancelled(), progress);
|
return finish(run.cancelled(), progress);
|
||||||
}
|
}
|
||||||
emit(progress, AiTestRunState.GENERATING, round, "CodeFree 正在生成第 " + round + " 轮单测");
|
emit(progress, AiTestRunState.GENERATING, round, "CodeFree 正在生成第 " + round + " 轮单测");
|
||||||
String prompt = buildPrompt(run, module, specifications, feedback, round);
|
String prompt = buildPrompt(run, module, specifications,
|
||||||
|
loadExistingTestSamples(module), feedback, round);
|
||||||
TestGenerationResult generated = provider.generate(new TestGenerationRequest(
|
TestGenerationResult generated = provider.generate(new TestGenerationRequest(
|
||||||
worktree.path(), analyzed.request().settings().codeFreeCommand(), prompt,
|
worktree.path(), analyzed.request().settings().codeFreeCommand(), prompt,
|
||||||
analyzed.request().settings().codeFreeTimeout()), ignored -> { });
|
analyzed.request().settings().codeFreeTimeout()),
|
||||||
|
line -> emit(progress, AiTestRunState.GENERATING, currentRound, line));
|
||||||
if (!generated.successful()) {
|
if (!generated.successful()) {
|
||||||
return failAndClose(run, "CodeFree 生成失败: " + generated.diagnosis(), progress);
|
return failAndClose(run, "CodeFree 生成失败: " + generated.diagnosis(), progress);
|
||||||
}
|
}
|
||||||
@@ -117,7 +120,8 @@ public class AiTestWorkflow {
|
|||||||
|
|
||||||
emit(progress, AiTestRunState.VERIFYING, round, "正在编译并运行生成的单测");
|
emit(progress, AiTestRunState.VERIFYING, round, "正在编译并运行生成的单测");
|
||||||
MavenExecutionResult compilation = mavenRunner.run(worktree.path(), module,
|
MavenExecutionResult compilation = mavenRunner.run(worktree.path(), module,
|
||||||
MavenPhase.TEST_COMPILE, analyzed.request().settings(), ignored -> { });
|
MavenPhase.TEST_COMPILE, analyzed.request().settings(),
|
||||||
|
line -> emit(progress, AiTestRunState.VERIFYING, currentRound, line));
|
||||||
if (!compilation.successful()) {
|
if (!compilation.successful()) {
|
||||||
MavenFailureType type = errorClassifier.classify(compilation);
|
MavenFailureType type = errorClassifier.classify(compilation);
|
||||||
run = run.round(round, null, files, compilation.combinedLog());
|
run = run.round(round, null, files, compilation.combinedLog());
|
||||||
@@ -129,7 +133,8 @@ public class AiTestWorkflow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MavenExecutionResult verified = mavenRunner.run(worktree.path(), module,
|
MavenExecutionResult verified = mavenRunner.run(worktree.path(), module,
|
||||||
MavenPhase.JACOCO, analyzed.request().settings(), ignored -> { });
|
MavenPhase.JACOCO, analyzed.request().settings(),
|
||||||
|
line -> emit(progress, AiTestRunState.VERIFYING, currentRound, line));
|
||||||
if (!verified.successful()) {
|
if (!verified.successful()) {
|
||||||
MavenFailureType type = errorClassifier.classify(verified);
|
MavenFailureType type = errorClassifier.classify(verified);
|
||||||
run = run.round(round, null, files, verified.combinedLog());
|
run = run.round(round, null, files, verified.combinedLog());
|
||||||
@@ -215,7 +220,7 @@ public class AiTestWorkflow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String buildPrompt(AiTestRun run, MavenModule module, String specifications,
|
private static String buildPrompt(AiTestRun run, MavenModule module, String specifications,
|
||||||
String feedback, int round) {
|
String existingTests, String feedback, int round) {
|
||||||
StringBuilder sources = new StringBuilder();
|
StringBuilder sources = new StringBuilder();
|
||||||
run.analysis().files().forEach(changed -> {
|
run.analysis().files().forEach(changed -> {
|
||||||
Path source = run.worktree().path().resolve(changed.sourcePath()).normalize();
|
Path source = run.worktree().path().resolve(changed.sourcePath()).normalize();
|
||||||
@@ -243,10 +248,33 @@ public class AiTestWorkflow {
|
|||||||
|
|
||||||
增量源码:%s
|
增量源码:%s
|
||||||
|
|
||||||
|
所选模块已有单测样例(仅用于对齐风格):
|
||||||
|
%s
|
||||||
|
|
||||||
只返回纯 JSON,不要 Markdown:
|
只返回纯 JSON,不要 Markdown:
|
||||||
{"action":"WRITE_TESTS","tests":[{"path":"src/test/java/...Test.java","content":"...","targetClass":"...","coveredLines":[1],"scenarios":["..."]}],"assumptions":[],"warnings":[],"diagnosis":""}
|
{"action":"WRITE_TESTS","tests":[{"path":"src/test/java/...Test.java","content":"...","targetClass":"...","coveredLines":[1],"scenarios":["..."]}],"assumptions":[],"warnings":[],"diagnosis":""}
|
||||||
""".formatted(specifications, module.artifactId(), run.request().baseline(),
|
""".formatted(specifications, module.artifactId(), run.request().baseline(),
|
||||||
run.request().settings().coverageTarget(), round, feedback, sources);
|
run.request().settings().coverageTarget(), round, feedback, sources, existingTests);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String loadExistingTestSamples(MavenModule module) {
|
||||||
|
Path testRoot = module.directory().resolve("src/test/java");
|
||||||
|
if (Files.notExists(testRoot)) {
|
||||||
|
return "<暂无已有单测>";
|
||||||
|
}
|
||||||
|
try (Stream<Path> paths = Files.walk(testRoot)) {
|
||||||
|
StringBuilder samples = new StringBuilder();
|
||||||
|
for (Path file : paths.filter(Files::isRegularFile)
|
||||||
|
.filter(path -> path.getFileName().toString().endsWith("Test.java"))
|
||||||
|
.limit(5).toList()) {
|
||||||
|
String content = Files.readString(file, StandardCharsets.UTF_8);
|
||||||
|
samples.append("\n--- ").append(testRoot.relativize(file)).append(" ---\n")
|
||||||
|
.append(content, 0, Math.min(content.length(), 12000));
|
||||||
|
}
|
||||||
|
return samples.isEmpty() ? "<暂无已有单测>" : samples.toString();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return "<已有单测读取失败>";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String repairFeedback(MavenFailureType type, String log, IncrementalCoverage coverage) {
|
private static String repairFeedback(MavenFailureType type, String log, IncrementalCoverage coverage) {
|
||||||
|
|||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package com.example.workutils.biz.aitest.config;
|
||||||
|
|
||||||
|
import com.example.workutils.system.config.WorkUtilsProperties;
|
||||||
|
import com.example.workutils.system.support.ApplicationPaths;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AiTestModulePreferenceService {
|
||||||
|
private final Path file;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public AiTestModulePreferenceService(WorkUtilsProperties properties) {
|
||||||
|
this(properties.getWorkutilsConfigFilePath().getParent().resolve("ai-test")
|
||||||
|
.resolve("module-preferences.properties"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AiTestModulePreferenceService(ApplicationPaths paths) {
|
||||||
|
this(paths.resolve("data", "ai-test", "module-preferences.properties"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AiTestModulePreferenceService(Path file) {
|
||||||
|
this.file = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> load(Path project) throws IOException {
|
||||||
|
Properties values = read();
|
||||||
|
return Optional.ofNullable(values.getProperty(key(project))).filter(value -> !value.isBlank());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(Path project, String artifactId) throws IOException {
|
||||||
|
Properties values = read();
|
||||||
|
values.setProperty(key(project), artifactId == null ? "" : artifactId);
|
||||||
|
Files.createDirectories(file.getParent());
|
||||||
|
try (OutputStream output = Files.newOutputStream(file)) {
|
||||||
|
values.store(output, "WorkUtils AI test module preferences");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Properties read() throws IOException {
|
||||||
|
Properties values = new Properties();
|
||||||
|
if (Files.exists(file)) {
|
||||||
|
try (InputStream input = Files.newInputStream(file)) {
|
||||||
|
values.load(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String key(Path project) {
|
||||||
|
return Integer.toHexString(project.toAbsolutePath().normalize().toString().hashCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -316,7 +316,8 @@ public class MainWindow {
|
|||||||
return switch (pluginId) {
|
return switch (pluginId) {
|
||||||
case "scan" -> AppIcon.SCANNER;
|
case "scan" -> AppIcon.SCANNER;
|
||||||
case "diff" -> AppIcon.DIFF;
|
case "diff" -> AppIcon.DIFF;
|
||||||
case "api-doc" -> AppIcon.API_DOC;
|
case "api-doc" -> AppIcon.API_DOC;
|
||||||
|
case "ai-unit-test" -> AppIcon.AI_TEST;
|
||||||
case "git-sync" -> AppIcon.GIT_SYNC;
|
case "git-sync" -> AppIcon.GIT_SYNC;
|
||||||
case "git-review" -> AppIcon.GIT_REVIEW;
|
case "git-review" -> AppIcon.GIT_REVIEW;
|
||||||
case "mybatisexport" -> AppIcon.SQL_EXPORT;
|
case "mybatisexport" -> AppIcon.SQL_EXPORT;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public enum AppIcon {
|
|||||||
MYBATIS_GENERATOR(Feather.CODE),
|
MYBATIS_GENERATOR(Feather.CODE),
|
||||||
SCANNER(Feather.SEARCH),
|
SCANNER(Feather.SEARCH),
|
||||||
API_DOC(Feather.FILE_TEXT),
|
API_DOC(Feather.FILE_TEXT),
|
||||||
|
AI_TEST(Feather.CPU),
|
||||||
EXPORT(Feather.DOWNLOAD),
|
EXPORT(Feather.DOWNLOAD),
|
||||||
CANCEL(Feather.X),
|
CANCEL(Feather.X),
|
||||||
APP(Feather.TOOL);
|
APP(Feather.TOOL);
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.example.workutils.biz.aitest;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class AiTestPaneArchitectureTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exposesConfigurationWorkflowReviewAndIntegrationControls() throws Exception {
|
||||||
|
String source = Files.readString(Path.of(
|
||||||
|
"src/main/java/com/example/workutils/biz/aitest/AiTestPane.java"));
|
||||||
|
|
||||||
|
assertTrue(source.contains("projectCombo"));
|
||||||
|
assertTrue(source.contains("moduleCombo"));
|
||||||
|
assertTrue(source.contains("baselineField"));
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.example.workutils.biz.aitest;
|
||||||
|
|
||||||
|
import com.example.workutils.biz.aitest.application.AiTestWorkflow;
|
||||||
|
import com.example.workutils.biz.aitest.application.AiTestHistoryService;
|
||||||
|
import com.example.workutils.biz.aitest.config.AiTestSettingsService;
|
||||||
|
import com.example.workutils.biz.aitest.config.AiTestModulePreferenceService;
|
||||||
|
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 org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
class AiTestPluginTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exposesEmbeddedAiUnitTestPlugin() {
|
||||||
|
AiTestPlugin plugin = new AiTestPlugin(mock(AiTestWorkflow.class),
|
||||||
|
mock(AiTestProjectCatalog.class), mock(MavenProjectAnalyzer.class),
|
||||||
|
mock(AiTestSettingsService.class), mock(TestSpecificationService.class),
|
||||||
|
mock(AiTestModulePreferenceService.class), mock(AiTestHistoryService.class),
|
||||||
|
mock(ToolchainValidator.class));
|
||||||
|
|
||||||
|
assertEquals("ai-unit-test", plugin.getDescriptor().getId());
|
||||||
|
assertEquals("AI 单测生成", plugin.getDescriptor().getName());
|
||||||
|
assertNotNull(plugin.getVisualFactory());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.example.workutils.biz.aitest;
|
||||||
|
|
||||||
|
import com.example.workutils.biz.aitest.config.AiTestSettings;
|
||||||
|
import com.example.workutils.biz.aitest.process.CommandResult;
|
||||||
|
import com.example.workutils.biz.aitest.process.ProcessExecutor;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
class ToolchainValidatorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reportsAllRequiredJdk8MavenAndCodeFreeChecks() {
|
||||||
|
ProcessExecutor executor = mock(ProcessExecutor.class);
|
||||||
|
when(executor.execute(any(), any())).thenReturn(
|
||||||
|
ok("java version \"1.8.0_402\""), ok("Apache Maven 3.9.9"), ok("0.3.6"));
|
||||||
|
ToolchainValidator validator = new ToolchainValidator(executor);
|
||||||
|
AiTestSettings settings = new AiTestSettings(Path.of("jdk8"), Path.of("maven"),
|
||||||
|
"codefree", 85, 3, Duration.ofMinutes(5), Duration.ofMinutes(15));
|
||||||
|
|
||||||
|
ToolchainValidation validation = validator.validate(settings, ignored -> { });
|
||||||
|
|
||||||
|
assertTrue(validation.ready());
|
||||||
|
assertEquals(3, validation.checks().size());
|
||||||
|
assertTrue(validation.checks().stream().anyMatch(check -> check.name().equals("JDK 8")));
|
||||||
|
verify(executor, times(3)).execute(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CommandResult ok(String output) {
|
||||||
|
return new CommandResult(0, false, false, output, "", Duration.ofMillis(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,10 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.*;
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
class AiTestWorkflowTest {
|
class AiTestWorkflowTest {
|
||||||
|
|
||||||
@@ -29,6 +31,9 @@ class AiTestWorkflowTest {
|
|||||||
Path workspace = root.resolve("workspace");
|
Path workspace = root.resolve("workspace");
|
||||||
Files.createDirectories(project);
|
Files.createDirectories(project);
|
||||||
Files.createDirectories(workspace.resolve("order-start"));
|
Files.createDirectories(workspace.resolve("order-start"));
|
||||||
|
Files.createDirectories(workspace.resolve("order-start/src/test/java/a"));
|
||||||
|
Files.writeString(workspace.resolve("order-start/src/test/java/a/ExistingTest.java"),
|
||||||
|
"package a; class ExistingTest { void projectStyle() {} }");
|
||||||
MavenModule originalModule = module(project);
|
MavenModule originalModule = module(project);
|
||||||
MavenModule workspaceModule = module(workspace);
|
MavenModule workspaceModule = module(workspace);
|
||||||
MavenProjectModel originalModel = new MavenProjectModel(project, List.of(originalModule));
|
MavenProjectModel originalModel = new MavenProjectModel(project, List.of(originalModule));
|
||||||
@@ -78,6 +83,9 @@ class AiTestWorkflowTest {
|
|||||||
assertEquals(AiTestRunState.READY_FOR_REVIEW, result.state());
|
assertEquals(AiTestRunState.READY_FOR_REVIEW, result.state());
|
||||||
assertEquals(2, result.generationRounds());
|
assertEquals(2, result.generationRounds());
|
||||||
assertEquals(new BigDecimal("90.00"), result.coverage().linePercent());
|
assertEquals(new BigDecimal("90.00"), result.coverage().linePercent());
|
||||||
|
ArgumentCaptor<TestGenerationRequest> requestCaptor = ArgumentCaptor.forClass(TestGenerationRequest.class);
|
||||||
|
verify(provider, times(2)).generate(requestCaptor.capture(), any());
|
||||||
|
assertTrue(requestCaptor.getAllValues().get(0).prompt().contains("projectStyle"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MavenModule module(Path root) {
|
private static MavenModule module(Path root) {
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package com.example.workutils.biz.aitest.config;
|
||||||
|
|
||||||
|
import com.example.workutils.system.support.ApplicationPaths;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
class AiTestModulePreferenceServiceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void remembersSelectedModulePerProject(@TempDir Path home) throws Exception {
|
||||||
|
AiTestModulePreferenceService service = new AiTestModulePreferenceService(ApplicationPaths.of(home));
|
||||||
|
Path project = home.resolve("project");
|
||||||
|
|
||||||
|
service.save(project, "order-start");
|
||||||
|
|
||||||
|
assertEquals("order-start", service.load(project).orElseThrow());
|
||||||
|
assertEquals("", service.load(home.resolve("other")).orElse(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user