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