task-164: 清理失败不阻断契约(单文件失败其余继续、失败路径可见、不上抛、下轮重试、引用跳过不计失败)+ 8 条测试
This commit is contained in:
+61
@@ -0,0 +1,61 @@
|
||||
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.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* 批量清理执行器(task-164)。
|
||||
*
|
||||
* 单个文件清理失败(删除函数返回 false/抛异常/引用判定失败)不影响其他文件;
|
||||
* 失败可见(failedPaths 与失败计数);不上抛;下次调用可重试失败项。
|
||||
*/
|
||||
@Service
|
||||
public class BatchFileCleaner {
|
||||
|
||||
public record BatchCleanResult(int cleanedCount, int failedCount, List<String> failedPaths) {
|
||||
|
||||
public boolean hasFailures() {
|
||||
return failedCount > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param files 候选文件
|
||||
* @param deleteFn 删除函数(返回 true 表示删除成功)
|
||||
* @param isReferenced 引用判定(true → 跳过,不算失败)
|
||||
*/
|
||||
public BatchCleanResult cleanBatch(List<File> files, Function<File, Boolean> deleteFn,
|
||||
Predicate<String> isReferenced) {
|
||||
int cleaned = 0;
|
||||
int failed = 0;
|
||||
List<String> failedPaths = new ArrayList<>();
|
||||
if (files == null || files.isEmpty()) {
|
||||
return new BatchCleanResult(0, 0, failedPaths);
|
||||
}
|
||||
for (File file : files) {
|
||||
if (file == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (isReferenced != null && isReferenced.test(file.getName())) {
|
||||
continue;
|
||||
}
|
||||
if (deleteFn != null && Boolean.TRUE.equals(deleteFn.apply(file))) {
|
||||
cleaned++;
|
||||
} else {
|
||||
failed++;
|
||||
failedPaths.add(file.getAbsolutePath());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
failed++;
|
||||
failedPaths.add(file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
return new BatchCleanResult(cleaned, failed, List.copyOf(failedPaths));
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package com.nanri.aiimage.modules.file.service;
|
||||
|
||||
import com.nanri.aiimage.modules.file.service.BatchFileCleaner.BatchCleanResult;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* task-164:清理失败不阻断契约(plan 09)。
|
||||
* 单文件失败不影响其他;错误可见(failedPaths);不上抛;下轮可重试;
|
||||
* 引用判定失败保守跳过不计数为失败。
|
||||
*/
|
||||
class BatchFileCleanerTest {
|
||||
|
||||
private final BatchFileCleaner cleaner = new BatchFileCleaner();
|
||||
|
||||
private final File a = new File("target/tmp/a.tmp");
|
||||
private final File b = new File("target/tmp/b.tmp");
|
||||
private final File c = new File("target/tmp/c.tmp");
|
||||
|
||||
@Test
|
||||
void oneFailureOthersSucceed() {
|
||||
BatchCleanResult result = cleaner.cleanBatch(
|
||||
List.of(a, b, c),
|
||||
file -> !file.getName().equals("b.tmp"),
|
||||
name -> false);
|
||||
|
||||
assertEquals(2, result.cleanedCount(), "b 失败其余成功");
|
||||
assertEquals(1, result.failedCount());
|
||||
assertTrue(result.hasFailures());
|
||||
}
|
||||
|
||||
@Test
|
||||
void failureIsVisible() {
|
||||
BatchCleanResult result = cleaner.cleanBatch(
|
||||
List.of(a, b),
|
||||
file -> !file.getName().equals("b.tmp"),
|
||||
name -> false);
|
||||
|
||||
assertEquals(1, result.failedPaths().size());
|
||||
assertTrue(result.failedPaths().getFirst().contains("b.tmp"), "失败路径可见");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noThrowUpOnFailure() {
|
||||
BatchCleanResult result = cleaner.cleanBatch(
|
||||
List.of(a, b),
|
||||
file -> {
|
||||
if (file.getName().equals("b.tmp")) {
|
||||
throw new RuntimeException("delete blew up");
|
||||
}
|
||||
return true;
|
||||
},
|
||||
name -> false);
|
||||
|
||||
assertEquals(1, result.cleanedCount());
|
||||
assertEquals(1, result.failedCount(), "抛异常按失败计数不上抛");
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryNextRoundSucceeds() {
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
BatchCleanResult first = cleaner.cleanBatch(
|
||||
List.of(b),
|
||||
file -> attempts.incrementAndGet() == 1 ? false : true,
|
||||
name -> false);
|
||||
BatchCleanResult second = cleaner.cleanBatch(
|
||||
List.of(b),
|
||||
file -> attempts.incrementAndGet() > 1,
|
||||
name -> false);
|
||||
|
||||
assertEquals(0, first.cleanedCount());
|
||||
assertEquals(1, second.cleanedCount(), "下轮重试成功");
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialResultReported() {
|
||||
BatchCleanResult result = cleaner.cleanBatch(
|
||||
List.of(a, c),
|
||||
file -> true,
|
||||
name -> false);
|
||||
|
||||
assertEquals(2, result.cleanedCount());
|
||||
assertEquals(0, result.failedCount());
|
||||
assertFalse(result.hasFailures());
|
||||
}
|
||||
|
||||
@Test
|
||||
void failureDoesNotBlockRemaining() {
|
||||
BatchCleanResult result = cleaner.cleanBatch(
|
||||
List.of(a, b, c),
|
||||
file -> !file.getName().equals("b.tmp"),
|
||||
name -> false);
|
||||
|
||||
assertEquals(2, result.cleanedCount(), "b 失败后 c 仍被清理(无级联)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void referencedFilesSkippedNotCountedAsFailure() {
|
||||
BatchCleanResult result = cleaner.cleanBatch(
|
||||
List.of(a, b),
|
||||
file -> true,
|
||||
name -> name.equals("a.tmp"));
|
||||
|
||||
assertEquals(1, result.cleanedCount(), "被引用文件跳过,不算失败");
|
||||
assertEquals(0, result.failedCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullInputsAreSafe() {
|
||||
BatchCleanResult empty = cleaner.cleanBatch(null, file -> true, name -> false);
|
||||
assertEquals(0, empty.cleanedCount());
|
||||
assertEquals(0, empty.failedCount());
|
||||
assertTrue(empty.failedPaths().isEmpty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user