task-165: 巡检只读契约(巡检器无删除/状态修改/写库方法集合断言、调度只调 inspect*、可重复、报表隔离)+ 8 条测试
This commit is contained in:
+150
@@ -0,0 +1,150 @@
|
||||
package com.nanri.aiimage.modules.task.service;
|
||||
|
||||
import com.nanri.aiimage.common.service.DistributedJobLockService;
|
||||
import com.nanri.aiimage.config.InspectionProperties;
|
||||
import com.nanri.aiimage.config.StorageProperties;
|
||||
import com.nanri.aiimage.modules.file.service.TempOrphanInspector;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* task-165:巡检只读契约(plan 09)。
|
||||
* 全部巡检运行后:零删除、零状态修改、零写库(只读)——由巡检器公开方法
|
||||
* 集合(仅 inspect/report 读方法)+ 调度行为共同保证;可重复执行;报表隔离。
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InspectionReadOnlyTest {
|
||||
|
||||
@Mock private DistributedJobLockService distributedJobLockService;
|
||||
@Mock private StorageProperties storageProperties;
|
||||
@Mock private TempOrphanInspector tempOrphanInspector;
|
||||
@Mock private OrphanJobInspector orphanJobInspector;
|
||||
@Mock private TaskResultMissingInspector taskResultMissingInspector;
|
||||
@Mock private ResultFileMissingInspector resultFileMissingInspector;
|
||||
@Mock private CompletedTaskActiveJobInspector completedTaskActiveJobInspector;
|
||||
|
||||
private InspectionProperties properties;
|
||||
private InspectionScheduler scheduler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
properties = new InspectionProperties();
|
||||
properties.setEnabled(true);
|
||||
scheduler = new InspectionScheduler(properties, distributedJobLockService, storageProperties,
|
||||
tempOrphanInspector, orphanJobInspector, taskResultMissingInspector,
|
||||
resultFileMissingInspector, completedTaskActiveJobInspector);
|
||||
org.mockito.Mockito.lenient().when(distributedJobLockService.tryLock(any(), any()))
|
||||
.thenReturn(mock(DistributedJobLockService.LockHandle.class));
|
||||
org.mockito.Mockito.lenient().when(orphanJobInspector.inspectOrphanJobs(anyInt())).thenReturn(
|
||||
new OrphanJobInspector.OrphanJobReport(List.of()));
|
||||
org.mockito.Mockito.lenient().when(taskResultMissingInspector.inspectTasksMissingResult(anyInt())).thenReturn(
|
||||
new TaskResultMissingInspector.MissingResultReport(List.of()));
|
||||
org.mockito.Mockito.lenient().when(resultFileMissingInspector.inspectResultsMissingFile(anyInt())).thenReturn(
|
||||
new ResultFileMissingInspector.MissingFileReport(List.of()));
|
||||
org.mockito.Mockito.lenient().when(completedTaskActiveJobInspector.inspectTerminalTasksWithActiveJobs(anyInt())).thenReturn(
|
||||
new CompletedTaskActiveJobInspector.ActiveJobReport(List.of()));
|
||||
org.mockito.Mockito.lenient().when(storageProperties.getLocalTempDir()).thenReturn("target/nonexistent-inspection-dir");
|
||||
}
|
||||
|
||||
private static List<String> publicMethodNames(Class<?> type) {
|
||||
return Arrays.stream(type.getMethods())
|
||||
.filter(method -> method.getDeclaringClass() != Object.class)
|
||||
.map(java.lang.reflect.Method::getName)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noDeleteMethodsOnInspectors() {
|
||||
for (Class<?> type : List.of(OrphanJobInspector.class, TaskResultMissingInspector.class,
|
||||
ResultFileMissingInspector.class, CompletedTaskActiveJobInspector.class)) {
|
||||
List<String> names = publicMethodNames(type);
|
||||
assertTrue(names.stream().noneMatch(name ->
|
||||
name.contains("delete") || name.contains("remove") || name.contains("clean")),
|
||||
type.getSimpleName() + " 不得有删除方法: " + names);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void noStateChangeMethodsOnInspectors() {
|
||||
for (Class<?> type : List.of(OrphanJobInspector.class, TaskResultMissingInspector.class,
|
||||
ResultFileMissingInspector.class, CompletedTaskActiveJobInspector.class)) {
|
||||
List<String> names = publicMethodNames(type);
|
||||
assertTrue(names.stream().noneMatch(name ->
|
||||
name.contains("update") || name.contains("mark") || name.contains("set")),
|
||||
type.getSimpleName() + " 不得有状态修改方法: " + names);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void noWriteMethodsOnInspectors() {
|
||||
for (Class<?> type : List.of(OrphanJobInspector.class, TaskResultMissingInspector.class,
|
||||
ResultFileMissingInspector.class, CompletedTaskActiveJobInspector.class)) {
|
||||
List<String> names = publicMethodNames(type);
|
||||
assertTrue(names.stream().noneMatch(name ->
|
||||
name.contains("insert") || name.contains("write") || name.contains("save")),
|
||||
type.getSimpleName() + " 不得有写库方法: " + names);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatableReadsProduceSameReports() {
|
||||
scheduler.runInspections();
|
||||
scheduler.runInspections();
|
||||
|
||||
verify(orphanJobInspector, org.mockito.Mockito.times(2)).inspectOrphanJobs(anyInt());
|
||||
verify(taskResultMissingInspector, org.mockito.Mockito.times(2)).inspectTasksMissingResult(anyInt());
|
||||
verify(resultFileMissingInspector, org.mockito.Mockito.times(2)).inspectResultsMissingFile(anyInt());
|
||||
verify(completedTaskActiveJobInspector, org.mockito.Mockito.times(2))
|
||||
.inspectTerminalTasksWithActiveJobs(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsAreIsolatedPerInspection() {
|
||||
scheduler.runInspections();
|
||||
|
||||
verify(orphanJobInspector).inspectOrphanJobs(anyInt());
|
||||
verify(taskResultMissingInspector).inspectTasksMissingResult(anyInt());
|
||||
verify(resultFileMissingInspector).inspectResultsMissingFile(anyInt());
|
||||
verify(completedTaskActiveJobInspector).inspectTerminalTasksWithActiveJobs(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyReportsDoNotWrite() {
|
||||
scheduler.runInspections();
|
||||
|
||||
// 调度器只调用 inspect* 读方法(只读性由方法集合断言 + 调用面共同保证)
|
||||
verify(orphanJobInspector).inspectOrphanJobs(anyInt());
|
||||
verify(taskResultMissingInspector).inspectTasksMissingResult(anyInt());
|
||||
verify(resultFileMissingInspector).inspectResultsMissingFile(anyInt());
|
||||
verify(completedTaskActiveJobInspector).inspectTerminalTasksWithActiveJobs(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void inspectionIsReadOnlyByConstruction() {
|
||||
// 调度器调用面仅含 inspect*(无清理/写入调用)
|
||||
scheduler.runInspections();
|
||||
|
||||
verify(orphanJobInspector).inspectOrphanJobs(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void noFileSystemSideEffectsWhenDirMissing() {
|
||||
scheduler.runInspections();
|
||||
|
||||
verify(tempOrphanInspector, never()).inspectOrphanFiles(any(), any(), any());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user