task-163: 引用中文件不清契约(job/result 引用跳过、引用解除后可清、判定失败保守跳过、批量混合、幂等)+ 8 条测试
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
|||||||
|
package com.nanri.aiimage.modules.file.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 引用感知清理决策(task-163)。
|
||||||
|
*
|
||||||
|
* 清理流程对每个文件先做引用判定:被引用(job/result/chunk/scope 引用)→ 跳过;
|
||||||
|
* 仅未引用文件进入清理。本类固化决策逻辑(清理执行本身由各清理服务负责)。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ReferenceAwareCleaner {
|
||||||
|
|
||||||
|
public enum CleanDecision {
|
||||||
|
/** 被引用,跳过清理。 */
|
||||||
|
SKIP_REFERENCED,
|
||||||
|
/** 未引用,可清理。 */
|
||||||
|
CLEAN,
|
||||||
|
}
|
||||||
|
|
||||||
|
public record FileDecision(File file, CleanDecision decision) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单文件决策:引用判定抛异常时保守跳过(宁可保留不可误删)。
|
||||||
|
*/
|
||||||
|
public CleanDecision decide(File file, Predicate<String> isReferenced) {
|
||||||
|
if (file == null || isReferenced == null) {
|
||||||
|
return CleanDecision.CLEAN;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return isReferenced.test(file.getName())
|
||||||
|
? CleanDecision.SKIP_REFERENCED
|
||||||
|
: CleanDecision.CLEAN;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return CleanDecision.SKIP_REFERENCED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量决策:混合批次中仅未引用文件 CLEAN;引用判定失败的文件保守跳过。
|
||||||
|
*/
|
||||||
|
public List<FileDecision> decideBatch(List<File> files, Predicate<String> isReferenced) {
|
||||||
|
List<FileDecision> decisions = new ArrayList<>();
|
||||||
|
if (files == null) {
|
||||||
|
return decisions;
|
||||||
|
}
|
||||||
|
for (File file : files) {
|
||||||
|
decisions.add(new FileDecision(file, decide(file, isReferenced)));
|
||||||
|
}
|
||||||
|
return decisions;
|
||||||
|
}
|
||||||
|
}
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
package com.nanri.aiimage.modules.file.service;
|
||||||
|
|
||||||
|
import com.nanri.aiimage.modules.file.service.ReferenceAwareCleaner.CleanDecision;
|
||||||
|
import com.nanri.aiimage.modules.file.service.ReferenceAwareCleaner.FileDecision;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* task-163:引用中文件不清契约(plan 09)。
|
||||||
|
* 被 job/result 等引用的文件 → 跳过清理;仅未引用文件清理;引用判定失败保守
|
||||||
|
* 跳过;批量混合;幂等。
|
||||||
|
*/
|
||||||
|
class ReferenceAwareCleanerTest {
|
||||||
|
|
||||||
|
private final ReferenceAwareCleaner cleaner = new ReferenceAwareCleaner();
|
||||||
|
|
||||||
|
private final File jobReferenced = new File("target/tmp/job-ref.tmp");
|
||||||
|
private final File resultReferenced = new File("target/tmp/result-ref.tmp");
|
||||||
|
private final File unreferenced = new File("target/tmp/free.tmp");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jobReferencedFileIsSkipped() {
|
||||||
|
CleanDecision decision = cleaner.decide(jobReferenced, name -> name.equals("job-ref.tmp"));
|
||||||
|
|
||||||
|
assertEquals(CleanDecision.SKIP_REFERENCED, decision, "被 job 引用 → 跳过清理");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resultReferencedFileIsSkipped() {
|
||||||
|
CleanDecision decision = cleaner.decide(resultReferenced, name -> name.equals("result-ref.tmp"));
|
||||||
|
|
||||||
|
assertEquals(CleanDecision.SKIP_REFERENCED, decision, "被 result 引用 → 跳过清理");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unreferencedFilePasses() {
|
||||||
|
CleanDecision decision = cleaner.decide(unreferenced, name -> false);
|
||||||
|
|
||||||
|
assertEquals(CleanDecision.CLEAN, decision, "未引用文件可清理");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multiReferenceAnyHitSkips() {
|
||||||
|
// 多引用来源:chunk/scope/job/result 任一命中即跳过
|
||||||
|
CleanDecision decision = cleaner.decide(jobReferenced,
|
||||||
|
name -> name.equals("other") || name.equals("job-ref.tmp") || name.equals("x"));
|
||||||
|
|
||||||
|
assertEquals(CleanDecision.SKIP_REFERENCED, decision);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void referenceReleasedAfterHistoryDeleteAllowsClean() {
|
||||||
|
// 删除历史后引用解除:谓词从 true 变 false → 可清理
|
||||||
|
CleanDecision before = cleaner.decide(jobReferenced, name -> true);
|
||||||
|
CleanDecision after = cleaner.decide(jobReferenced, name -> false);
|
||||||
|
|
||||||
|
assertEquals(CleanDecision.SKIP_REFERENCED, before);
|
||||||
|
assertEquals(CleanDecision.CLEAN, after, "引用解除后可清理");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void batchMixedOnlyUnreferencedCleans() {
|
||||||
|
List<FileDecision> decisions = cleaner.decideBatch(
|
||||||
|
List.of(jobReferenced, unreferenced, resultReferenced),
|
||||||
|
name -> name.equals("job-ref.tmp") || name.equals("result-ref.tmp"));
|
||||||
|
|
||||||
|
assertEquals(3, decisions.size());
|
||||||
|
assertEquals(CleanDecision.SKIP_REFERENCED, decisions.get(0).decision());
|
||||||
|
assertEquals(CleanDecision.CLEAN, decisions.get(1).decision(), "仅未引用文件清理");
|
||||||
|
assertEquals(CleanDecision.SKIP_REFERENCED, decisions.get(2).decision());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nullInputsAreSafe() {
|
||||||
|
assertEquals(CleanDecision.CLEAN, cleaner.decide(null, name -> true));
|
||||||
|
assertEquals(CleanDecision.CLEAN, cleaner.decide(unreferenced, null));
|
||||||
|
assertTrue(cleaner.decideBatch(null, name -> false).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkIsIdempotent() {
|
||||||
|
CleanDecision first = cleaner.decide(jobReferenced, name -> name.equals("job-ref.tmp"));
|
||||||
|
CleanDecision second = cleaner.decide(jobReferenced, name -> name.equals("job-ref.tmp"));
|
||||||
|
|
||||||
|
assertEquals(first, second, "同输入同决策");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user