feat: analyze AI tests against explicit target ref

This commit is contained in:
张家训
2026-08-04 18:10:12 +08:00
parent 9676c81455
commit 5f9bd609c3
15 changed files with 301 additions and 37 deletions
@@ -0,0 +1,84 @@
# AI 单测目标引用与异步竞态修复实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让 AI 单测插件稳定按 `master...DEV_1156339215001460262` 分析真实 `ft-loancapital-core` 增量,并阻止较早的项目加载结果覆盖用户后来选择的项目。
**Architecture:** 将 Git 比较终点从隐式 `HEAD` 提升为 `AiTestRequest.targetRef`,增量分析和临时 worktree 均解析同一目标引用;工作区改动仅允许叠加在目标引用等于当前 `HEAD` 的场景。项目元数据加载使用单调递增的请求令牌,只有最新令牌可以更新 JavaFX 控件。
**Tech Stack:** Java 17(WorkUtils 运行时)、JavaFX、Spring、Maven、JUnit 5、Mockito、Git CLI
## Global Constraints
- 真实业务项目使用 `D:/bestpay-srdcloud/ft-loancapital-core`;该目录不存在时集成回归测试使用 JUnit assumption 跳过。
- 默认基线为 `master`;目标引用可填写分支或 Commit,并对当前真实场景使用 `DEV_1156339215001460262`。
- 不修改真实业务仓库、不切换其分支、不提交其文件。
- 不触碰 WorkUtils 中现有未跟踪的审计文档、截图和 SQL 文件。
- 全程单线程实施,不使用子代理。
---
### Task 1: 显式 baseline 与 targetRef 的增量分析
**Files:**
- Modify: `src/main/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzer.java`
- Modify: `src/main/java/com/example/workutils/biz/aitest/application/AiTestRequest.java`
- Modify: `src/main/java/com/example/workutils/biz/aitest/application/AiTestWorkflow.java`
- Modify: `src/main/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManager.java`
- Test: `src/test/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzerTest.java`
- Test: `src/test/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManagerTest.java`
- Test: `src/test/java/com/example/workutils/biz/aitest/application/AiTestWorkflowTest.java`
**Interfaces:**
- Produces: `GitIncrementAnalyzer.analyze(Path, String baseline, String targetRef, boolean includeWorkingTree)`
- Produces: `TemporaryWorktreeManager.create(Path, String targetRef, boolean includeWorkingTree)`
- Produces: `AiTestRequest.targetRef()`
- [ ] **Step 1: 写失败测试**:构造 `master` 与未检出的 `DEV_*` 分支,断言分析结果来自 DEV 而不是当前 HEAD;断言临时 worktree 检出目标引用。
- [ ] **Step 2: 运行聚焦测试确认 RED**:`mvn -Dtest=GitIncrementAnalyzerTest,TemporaryWorktreeManagerTest,AiTestWorkflowTest test`,预期因新签名或仍使用 HEAD 而失败。
- [ ] **Step 3: 最小实现**:解析 baseline/target commit,以其 merge-base 到 target commit 生成 patch;workflow 将 request 的 targetRef 贯穿到分析和 worktree。
- [ ] **Step 4: 运行聚焦测试确认 GREEN**:同一 Maven 命令全部通过。
### Task 2: 真实 ft-loancapital-core 回归
**Files:**
- Create: `src/test/java/com/example/workutils/biz/aitest/project/FtLoanCapitalCoreRegressionTest.java`
**Interfaces:**
- Consumes: `MavenProjectAnalyzer.analyze(Path)`
- Consumes: `GitIncrementAnalyzer.analyze(Path, "master", "DEV_1156339215001460262", false)`
- [ ] **Step 1: 写真实只读回归测试**:断言解析到 `ft-loancapital-core-start`,并断言 master 到 DEV 的增量包含已知 service Java 文件及新增行。
- [ ] **Step 2: 运行测试确认结果**:`mvn -Dtest=FtLoanCapitalCoreRegressionTest test`;真实仓库存在时必须通过,不存在时仅跳过。
### Task 3: 项目加载的最新请求获胜
**Files:**
- Create: `src/main/java/com/example/workutils/biz/aitest/LatestRequestGate.java`
- Modify: `src/main/java/com/example/workutils/biz/aitest/AiTestPane.java`
- Create: `src/test/java/com/example/workutils/biz/aitest/LatestRequestGateTest.java`
- Modify: `src/test/java/com/example/workutils/biz/aitest/AiTestPaneArchitectureTest.java`
**Interfaces:**
- Produces: `LatestRequestGate.next()` 返回请求令牌。
- Produces: `LatestRequestGate.isLatest(long)` 仅对最后发出的令牌返回 true。
- [ ] **Step 1: 写失败测试**:先申请旧令牌再申请新令牌,断言旧令牌失效、新令牌有效;架构测试断言项目加载成功与失败回调都经过 gate。
- [ ] **Step 2: 运行聚焦测试确认 RED**:`mvn -Dtest=LatestRequestGateTest,AiTestPaneArchitectureTest test`,预期类不存在或 Pane 未使用 gate。
- [ ] **Step 3: 最小实现**:`loadProject` 发出令牌,清空旧模块;后台成功或失败回到 FX 线程时仅允许最新请求更新 UI。
- [ ] **Step 4: 运行聚焦测试确认 GREEN**:同一 Maven 命令全部通过。
### Task 4: UI 参数与完整验证
**Files:**
- Modify: `src/main/java/com/example/workutils/biz/aitest/AiTestPane.java`
- Modify: `src/test/java/com/example/workutils/biz/aitest/AiTestPaneArchitectureTest.java`
**Interfaces:**
- UI 提供 `baselineField`(默认 `master`)与 `targetRefField`(默认 `HEAD`,加载项目后显示当前分支)。
- [ ] **Step 1: 写失败测试**:断言 Pane 存在独立 targetRef 字段,且 baseline 默认 master。
- [ ] **Step 2: 最小实现**:把 targetRef 写入 `AiTestRequest`,校验 baseline、targetRef、项目和模块均已选择。
- [ ] **Step 3: 运行 AI 单测插件测试**:`mvn -Dtest='com.example.workutils.biz.aitest.**' test`。
- [ ] **Step 4: 运行全量验证**:`mvn test`、`mvn -DskipTests package`、`git diff --check`。
- [ ] **Step 5: 检查 `git status` 仅包含本次文件和用户原有未跟踪文件,然后提交到 WorkUtils `master`。**
@@ -43,7 +43,8 @@ public class AiTestPane extends BorderPane {
private final ComboBox<AiTestProject> projectCombo = new ComboBox<>();
private final ComboBox<MavenModule> moduleCombo = new ComboBox<>();
private final TextField baselineField = new TextField("HEAD~1");
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();
@@ -67,6 +68,7 @@ public class AiTestPane extends BorderPane {
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 final LatestRequestGate projectLoadGate = new LatestRequestGate();
private volatile AiTestRun currentRun;
private volatile boolean busy;
@@ -116,7 +118,8 @@ public class AiTestPane extends BorderPane {
private Node buildContent() {
GridPane selection = grid();
addRow(selection, 0, "项目", projectCombo, "生成模块", moduleCombo);
addRow(selection, 1, "基线分支/Commit", baselineField, "工作区", includeWorkingTree);
addRow(selection, 1, "基线分支/Commit", baselineField, "目标分支/Commit", targetRefField);
selection.add(includeWorkingTree, 1, 2);
GridPane toolchain = grid();
Button validateToolchain = new Button("检查并保存工具链");
@@ -193,26 +196,50 @@ public class AiTestPane extends BorderPane {
}
private void loadProject(AiTestProject project) {
long projectRequest = projectLoadGate.next();
moduleCombo.getItems().clear();
moduleCombo.setValue(null);
if (project == null || !project.available()) {
moduleCombo.getItems().clear();
return;
}
runAsync(() -> new ProjectData(projectAnalyzer.analyze(project.path()),
statusLabel.setText("正在加载项目模块...");
AppExecutors.io().submit(() -> {
try {
ProjectData data = 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;
modulePreferences.load(project.path()).orElse(""));
Platform.runLater(() -> {
if (!projectLoadGate.isLatest(projectRequest)) {
return;
}
applyProjectData(data);
});
} catch (Exception failure) {
Platform.runLater(() -> {
if (!projectLoadGate.isLatest(projectRequest)) {
return;
}
statusLabel.setText(failure.getMessage() == null
? failure.getClass().getSimpleName() : failure.getMessage());
appendLog("项目加载失败: " + statusLabel.getText());
});
}
moduleCombo.setValue(selected);
statusLabel.setText(selected == null ? "请选择生成模块" : "已选择模块 " + selected.artifactId());
});
}
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());
}
private void validateAndSaveToolchain() {
AiTestSettings settings = settingsFromForm();
runAsync(() -> {
@@ -240,12 +267,14 @@ public class AiTestPane extends BorderPane {
private void analyze() {
AiTestProject project = projectCombo.getValue();
MavenModule module = moduleCombo.getValue();
if (project == null || module == null || baselineField.getText().isBlank()) {
statusLabel.setText("请选择项目、模块并填写基线");
if (project == null || module == null || baselineField.getText().isBlank()
|| targetRefField.getText().isBlank()) {
statusLabel.setText("请选择项目、模块并填写基线和目标引用");
return;
}
AiTestRequest request = new AiTestRequest(project.path(), module.artifactId(),
baselineField.getText().trim(), includeWorkingTree.isSelected(), settingsFromForm());
baselineField.getText().trim(), targetRefField.getText().trim(),
includeWorkingTree.isSelected(), settingsFromForm());
runAsync(() -> workflow.analyze(request, this::onProgress), run -> {
currentRun = run;
renderRun(run);
@@ -0,0 +1,15 @@
package com.example.workutils.biz.aitest;
import java.util.concurrent.atomic.AtomicLong;
final class LatestRequestGate {
private final AtomicLong sequence = new AtomicLong();
long next() {
return sequence.incrementAndGet();
}
boolean isLatest(long request) {
return sequence.get() == request;
}
}
@@ -4,9 +4,17 @@ import com.example.workutils.biz.aitest.config.AiTestSettings;
import java.nio.file.Path;
public record AiTestRequest(Path project, String moduleArtifactId, String baseline,
public record AiTestRequest(Path project, String moduleArtifactId, String baseline, String targetRef,
boolean includeWorkingTree, AiTestSettings settings) {
public AiTestRequest {
project = project.toAbsolutePath().normalize();
if (baseline == null || baseline.isBlank()) {
throw new IllegalArgumentException("Baseline ref is required");
}
if (targetRef == null || targetRef.isBlank()) {
throw new IllegalArgumentException("Target ref is required");
}
baseline = baseline.trim();
targetRef = targetRef.trim();
}
}
@@ -63,7 +63,7 @@ public class AiTestWorkflow {
emit(progress, AiTestRunState.CREATED, 0, "正在分析 Maven 项目和 Git 增量");
MavenProjectModel model = projectAnalyzer.analyze(request.project());
MavenModule selected = requiredModule(model, request.moduleArtifactId());
var increment = incrementAnalyzer.analyze(request.project(), request.baseline(),
var increment = incrementAnalyzer.analyze(request.project(), request.baseline(), request.targetRef(),
request.includeWorkingTree());
for (var file : increment.files()) {
model.modules().stream()
@@ -91,7 +91,7 @@ public class AiTestWorkflow {
AiTestRun run = analyzed;
try {
TemporaryWorktree worktree = worktreeManager.create(analyzed.request().project(),
analyzed.request().includeWorkingTree());
analyzed.request().targetRef(), analyzed.request().includeWorkingTree());
run = run.workspace(worktree);
MavenProjectModel workspaceModel = projectAnalyzer.analyze(worktree.path());
MavenModule module = requiredModule(workspaceModel, analyzed.request().moduleArtifactId());
@@ -241,7 +241,7 @@ public class AiTestWorkflow {
%s
所选模块:%s
基线:%s
增量范围:%s...%s
目标增量行覆盖率:%d%%
当前轮次:%d
上轮反馈:%s
@@ -254,6 +254,7 @@ public class AiTestWorkflow {
只返回纯 JSON,不要 Markdown:
{"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(),
run.request().targetRef(),
run.request().settings().coverageTarget(), round, feedback, sources, existingTests);
}
@@ -26,16 +26,23 @@ public class GitIncrementAnalyzer {
private static final Pattern HUNK = Pattern.compile("^@@ -\\d+(?:,\\d+)? \\+(\\d+)(?:,(\\d+))? @@.*$");
private final ProcessExecutor processExecutor;
public IncrementAnalysis analyze(Path project, String baseline, boolean includeWorkingTree) {
public IncrementAnalysis analyze(Path project, String baseline, String targetRef,
boolean includeWorkingTree) {
Path root = project.toAbsolutePath().normalize();
String baselineCommit = git(root, "rev-parse", "--verify", baseline + "^{commit}").trim();
String mergeBase = git(root, "merge-base", baselineCommit, "HEAD").trim();
String targetCommit = git(root, "rev-parse", "--verify", targetRef + "^{commit}").trim();
String mergeBase = git(root, "merge-base", baselineCommit, targetCommit).trim();
Map<Path, Set<Integer>> changed = new LinkedHashMap<>();
merge(changed, parsePatch(git(root, "-c", "core.quotepath=false", "diff",
"--unified=0", "--no-color", mergeBase + "...HEAD")));
"--unified=0", "--no-color", mergeBase, targetCommit)));
if (includeWorkingTree) {
String headCommit = git(root, "rev-parse", "HEAD").trim();
if (!headCommit.equals(targetCommit)) {
throw new IllegalArgumentException(
"Working tree changes require target ref to resolve to current HEAD");
}
merge(changed, parsePatch(git(root, "-c", "core.quotepath=false", "diff",
"--unified=0", "--no-color", "HEAD")));
"--unified=0", "--no-color", targetCommit)));
addUntrackedJavaFiles(root, changed);
}
List<ChangedLines> files = changed.entrySet().stream()
@@ -17,21 +17,27 @@ public class TemporaryWorktreeManager {
this.git = new GitCommandRunner(executor);
}
public TemporaryWorktree create(Path repository, boolean includeWorkingTree) {
public TemporaryWorktree create(Path repository, String targetRef, boolean includeWorkingTree) {
Path repo = repository.toAbsolutePath().normalize();
if (Files.notExists(repo.resolve(".git"))) {
throw new IllegalArgumentException("Not a Git repository: " + repo);
}
try {
String head = git.required(repo, "rev-parse", "HEAD").trim();
String targetCommit = git.required(repo, "rev-parse", "--verify",
targetRef + "^{commit}").trim();
if (includeWorkingTree && !head.equals(targetCommit)) {
throw new IllegalArgumentException(
"Working tree changes require target ref to resolve to current HEAD");
}
Path parent = Files.createTempDirectory("workutils-ai-test-");
Path worktree = parent.resolve("generation");
git.required(repo, "worktree", "add", "--detach", worktree.toString(), head);
git.required(repo, "worktree", "add", "--detach", worktree.toString(), targetCommit);
try {
if (includeWorkingTree) {
copyWorkingTreeChanges(repo, worktree);
}
return new TemporaryWorktree(repo, worktree, parent, head, this);
return new TemporaryWorktree(repo, worktree, parent, targetCommit, this);
} catch (Exception failure) {
try { git.required(repo, "worktree", "remove", "--force", worktree.toString()); }
catch (RuntimeException ignored) { }
@@ -17,6 +17,10 @@ class AiTestPaneArchitectureTest {
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"));
@@ -0,0 +1,20 @@
package com.example.workutils.biz.aitest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class LatestRequestGateTest {
@Test
void onlyLatestIssuedRequestMayApplyItsResult() {
LatestRequestGate gate = new LatestRequestGate();
long older = gate.next();
long latest = gate.next();
assertFalse(gate.isLatest(older));
assertTrue(gate.isLatest(latest));
}
}
@@ -17,7 +17,7 @@ class AiTestHistoryServiceTest {
@Test
void persistsRunSummaryWithoutPromptOrProductionSource(@TempDir Path home) throws Exception {
AiTestHistoryService service = new AiTestHistoryService(ApplicationPaths.of(home));
AiTestRequest request = new AiTestRequest(home.resolve("project"), "order-start", "develop",
AiTestRequest request = new AiTestRequest(home.resolve("project"), "order-start", "develop", "feature/test",
false, new AiTestSettings(Path.of("jdk8"), Path.of("maven"), "codefree",
85, 3, Duration.ofMinutes(5), Duration.ofMinutes(15)));
AiTestRun run = AiTestRun.initial("run-1", request).fail("Maven仓库认证失败");
@@ -45,11 +45,11 @@ class AiTestWorkflowTest {
when(projectAnalyzer.analyze(project)).thenReturn(originalModel);
when(projectAnalyzer.analyze(workspace)).thenReturn(workspaceModel);
GitIncrementAnalyzer incrementAnalyzer = mock(GitIncrementAnalyzer.class);
when(incrementAnalyzer.analyze(project, "develop", false)).thenReturn(analysis);
when(incrementAnalyzer.analyze(project, "develop", "feature/test", false)).thenReturn(analysis);
TemporaryWorktreeManager worktrees = mock(TemporaryWorktreeManager.class);
TemporaryWorktree temporary = mock(TemporaryWorktree.class);
when(temporary.path()).thenReturn(workspace);
when(worktrees.create(project, false)).thenReturn(temporary);
when(worktrees.create(project, "feature/test", false)).thenReturn(temporary);
TestGenerationProvider provider = mock(TestGenerationProvider.class);
GeneratedTestFile generated = new GeneratedTestFile("src/test/java/a/ATest.java",
@@ -74,7 +74,7 @@ class AiTestWorkflowTest {
provider, fileValidator, maven, classifier, jacoco, coverage,
mock(TestSpecificationService.class), mock(AiTestHistoryService.class),
mock(TargetBranchIntegrator.class));
AiTestRequest request = new AiTestRequest(project, "order-start", "develop", false,
AiTestRequest request = new AiTestRequest(project, "order-start", "develop", "feature/test", false,
settings(root));
AiTestRun analyzed = workflow.analyze(request, ignored -> { });
@@ -86,6 +86,7 @@ class AiTestWorkflowTest {
ArgumentCaptor<TestGenerationRequest> requestCaptor = ArgumentCaptor.forClass(TestGenerationRequest.class);
verify(provider, times(2)).generate(requestCaptor.capture(), any());
assertTrue(requestCaptor.getAllValues().get(0).prompt().contains("projectStyle"));
assertTrue(requestCaptor.getAllValues().get(0).prompt().contains("develop...feature/test"));
}
private static MavenModule module(Path root) {
@@ -32,14 +32,38 @@ class GitIncrementAnalyzerTest {
write(source, "class A {\n int value() { return 2; }\n int next() { return 4; }\n}\n");
GitIncrementAnalyzer analyzer = new GitIncrementAnalyzer(new DefaultProcessExecutor());
IncrementAnalysis committed = analyzer.analyze(repository, baseline, false);
IncrementAnalysis withWorkingTree = analyzer.analyze(repository, baseline, true);
IncrementAnalysis committed = analyzer.analyze(repository, baseline, "HEAD", false);
IncrementAnalysis withWorkingTree = analyzer.analyze(repository, baseline, "HEAD", true);
assertEquals(Set.of(2, 3), committed.files().get(0).lines());
assertTrue(withWorkingTree.files().get(0).lines().containsAll(Set.of(2, 3)));
assertTrue(withWorkingTree.includesWorkingTree());
}
@Test
void analyzesExplicitTargetBranchWithoutCheckingItOut(@TempDir Path repository) throws Exception {
git(repository, "init");
git(repository, "config", "user.email", "workutils@example.com");
git(repository, "config", "user.name", "WorkUtils Test");
Path source = repository.resolve("core/src/main/java/a/A.java");
write(source, "class A {\n int value() { return 1; }\n}\n");
git(repository, "add", ".");
git(repository, "commit", "-m", "base");
git(repository, "branch", "-M", "master");
git(repository, "checkout", "-b", "DEV_1156339215001460262");
write(source, "class A {\n int value() { return 2; }\n int added() { return 3; }\n}\n");
git(repository, "add", ".");
git(repository, "commit", "-m", "dev change");
git(repository, "checkout", "master");
IncrementAnalysis result = new GitIncrementAnalyzer(new DefaultProcessExecutor())
.analyze(repository, "master", "DEV_1156339215001460262", false);
assertEquals(Set.of(2, 3), result.files().get(0).lines());
assertEquals("master", git(repository, "branch", "--show-current").trim());
}
private static void write(Path file, String content) throws Exception {
Files.createDirectories(file.getParent());
Files.writeString(file, content, StandardCharsets.UTF_8);
@@ -19,7 +19,7 @@ class TargetBranchIntegratorTest {
TargetBranchIntegrator integrator = new TargetBranchIntegrator(new DefaultProcessExecutor());
BranchIntegrationResult result;
try (TemporaryWorktree generation = manager.create(repository, false)) {
try (TemporaryWorktree generation = manager.create(repository, "HEAD", false)) {
Path test = generation.path().resolve("app-start/src/test/java/a/AServiceTest.java");
GitFixture.write(test, "package a; class AServiceTest {}");
result = integrator.integrate(new BranchIntegrationRequest(repository, generation.path(),
@@ -42,7 +42,7 @@ class TargetBranchIntegratorTest {
TargetBranchIntegrator integrator = new TargetBranchIntegrator(new DefaultProcessExecutor());
BranchIntegrationResult result;
try (TemporaryWorktree generation = manager.create(repository, false)) {
try (TemporaryWorktree generation = manager.create(repository, "HEAD", false)) {
GitFixture.write(generation.path().resolve("app-core/src/main/java/a/A.java"),
"class A { int value = 99; }");
result = integrator.integrate(new BranchIntegrationRequest(repository, generation.path(),
@@ -20,7 +20,7 @@ class TemporaryWorktreeManagerTest {
TemporaryWorktreeManager manager = new TemporaryWorktreeManager(new DefaultProcessExecutor());
Path worktreePath;
try (TemporaryWorktree worktree = manager.create(repository, true)) {
try (TemporaryWorktree worktree = manager.create(repository, "HEAD", true)) {
worktreePath = worktree.path();
assertEquals("class A { int value = 2; }",
Files.readString(worktree.path().resolve("app-core/src/main/java/a/A.java"), StandardCharsets.UTF_8));
@@ -30,4 +30,34 @@ class TemporaryWorktreeManagerTest {
assertFalse(Files.exists(worktreePath));
assertEquals("class A { int value = 2; }", Files.readString(source, StandardCharsets.UTF_8));
}
@Test
void createsWorktreeFromExplicitTargetBranch(@TempDir Path repository) throws Exception {
GitFixture.init(repository);
git(repository, "branch", "-M", "master");
Path source = repository.resolve("app-core/src/main/java/a/A.java");
git(repository, "checkout", "-b", "DEV_1156339215001460262");
GitFixture.write(source, "class A { int branchValue = 7; }");
git(repository, "add", ".");
git(repository, "commit", "-m", "dev change");
git(repository, "checkout", "master");
TemporaryWorktreeManager manager = new TemporaryWorktreeManager(new DefaultProcessExecutor());
try (TemporaryWorktree worktree = manager.create(repository, "DEV_1156339215001460262", false)) {
assertEquals("class A { int branchValue = 7; }",
Files.readString(worktree.path().resolve("app-core/src/main/java/a/A.java")));
}
assertEquals("master", git(repository, "branch", "--show-current").trim());
}
private static String git(Path directory, String... arguments) throws Exception {
java.util.List<String> command = new java.util.ArrayList<>();
command.add("git");
command.addAll(java.util.List.of(arguments));
Process process = new ProcessBuilder(command).directory(directory.toFile())
.redirectErrorStream(true).start();
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
assertEquals(0, process.waitFor(), output);
return output;
}
}
@@ -0,0 +1,35 @@
package com.example.workutils.biz.aitest.project;
import com.example.workutils.biz.aitest.git.GitIncrementAnalyzer;
import com.example.workutils.biz.aitest.git.IncrementAnalysis;
import com.example.workutils.biz.aitest.process.DefaultProcessExecutor;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
class FtLoanCapitalCoreRegressionTest {
private static final Path PROJECT = Path.of("D:/bestpay-srdcloud/ft-loancapital-core");
@Test
void detectsStartModuleAndMasterToDevIncrementInRealProject() {
assumeTrue(Files.isRegularFile(PROJECT.resolve("pom.xml")),
"real ft-loancapital-core checkout is not available");
MavenProjectModel model = new MavenProjectAnalyzer().analyze(PROJECT);
MavenModule start = model.moduleByArtifactId("ft-loancapital-core-start").orElseThrow();
IncrementAnalysis increment = new GitIncrementAnalyzer(new DefaultProcessExecutor())
.analyze(PROJECT, "master", "DEV_1156339215001460262", false);
assertTrue(Files.isRegularFile(start.directory().resolve("pom.xml")));
assertFalse(increment.files().isEmpty());
assertTrue(increment.files().stream().anyMatch(file -> file.sourcePath().toString()
.replace('\\', '/').equals("ft-loancapital-core-service/src/main/java/com/bestpay/ft/"
+ "loancapital/core/service/transfer/service/impl/OrgTransferManagementServiceImpl.java")));
}
}