task-150: 清理前引用检查契约(chunk 共享/scope 引用保留、自身行可清、异常保守保留、只读无副作用、批量 IN 反查)+ 8 条测试
This commit is contained in:
+152
@@ -0,0 +1,152 @@
|
||||
package com.nanri.aiimage.modules.task.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.config.InstanceMetadata;
|
||||
import com.nanri.aiimage.config.StorageProperties;
|
||||
import com.nanri.aiimage.config.TransientStorageProperties;
|
||||
import com.nanri.aiimage.modules.file.service.object.RustfsObjectStorageService;
|
||||
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
|
||||
import com.nanri.aiimage.modules.task.mapper.TaskChunkMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.TaskScopeStateMapper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
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-150:清理前引用检查契约(plan 09)。
|
||||
* payload 被 biz_task_chunk / biz_task_scope_state 引用则不清理;
|
||||
* chunk 仅剩自身一行(count=1)不算共享引用;查询异常保守保留;
|
||||
* 检查只读无副作用;候选值批量反查。
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TaskPayloadReferenceCheckTest {
|
||||
|
||||
private static final String RUSTFS_VALUE = "\"rustfs:payload-key\"";
|
||||
private static final String RUSTFS_POINTER = "rustfs:payload-key";
|
||||
|
||||
@Mock private RustfsObjectStorageService rustfsObjectStorageService;
|
||||
@Mock private TaskChunkMapper taskChunkMapper;
|
||||
@Mock private TaskScopeStateMapper taskScopeStateMapper;
|
||||
@Mock private OssStorageService ossStorageService;
|
||||
|
||||
private TransientPayloadStorageService service;
|
||||
|
||||
@BeforeAll
|
||||
static void initializeMybatisMetadata() {
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
|
||||
TableInfoHelper.initTableInfo(assistant, com.nanri.aiimage.modules.task.model.entity.TaskChunkEntity.class);
|
||||
TableInfoHelper.initTableInfo(assistant, com.nanri.aiimage.modules.task.model.entity.TaskScopeStateEntity.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
TransientStorageProperties transientProperties = new TransientStorageProperties();
|
||||
transientProperties.setEnabled(true);
|
||||
StorageProperties storageProperties = new StorageProperties();
|
||||
storageProperties.setLocalTempDir("target/tmp-refcheck");
|
||||
service = new TransientPayloadStorageService(
|
||||
transientProperties,
|
||||
storageProperties,
|
||||
rustfsObjectStorageService,
|
||||
ossStorageService,
|
||||
new ObjectMapper(),
|
||||
new InstanceMetadata("test-instance"),
|
||||
taskChunkMapper,
|
||||
taskScopeStateMapper);
|
||||
org.mockito.Mockito.lenient().when(taskChunkMapper.selectCount(any())).thenReturn(0L);
|
||||
org.mockito.Mockito.lenient().when(taskScopeStateMapper.selectCount(any())).thenReturn(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void referencedByChunkSharedIsKept() {
|
||||
when(taskChunkMapper.selectCount(any())).thenReturn(2L);
|
||||
|
||||
service.deletePayloadIfPresent(RUSTFS_VALUE);
|
||||
|
||||
verify(rustfsObjectStorageService, never()).deleteObject(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unreferencedCandidateIsDeleted() {
|
||||
service.deletePayloadIfPresent(RUSTFS_VALUE);
|
||||
|
||||
verify(rustfsObjectStorageService).deleteObject("payload-key");
|
||||
}
|
||||
|
||||
@Test
|
||||
void referencedByScopeStateIsKept() {
|
||||
when(taskScopeStateMapper.selectCount(any())).thenReturn(1L);
|
||||
|
||||
service.deletePayloadIfPresent(RUSTFS_VALUE);
|
||||
|
||||
verify(rustfsObjectStorageService, never()).deleteObject(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleChunkRowIsOwnRowNotSharedReference() {
|
||||
when(taskChunkMapper.selectCount(any())).thenReturn(1L);
|
||||
|
||||
service.deletePayloadIfPresent(RUSTFS_VALUE);
|
||||
|
||||
verify(rustfsObjectStorageService).deleteObject("payload-key");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkFailureKeepsPayloadConservatively() {
|
||||
when(taskChunkMapper.selectCount(any())).thenThrow(new RuntimeException("db down"));
|
||||
|
||||
service.deletePayloadIfPresent(RUSTFS_VALUE);
|
||||
|
||||
verify(rustfsObjectStorageService, never()).deleteObject(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkIsReadOnlyNoSideEffects() {
|
||||
service.deletePayloadIfPresent(RUSTFS_VALUE);
|
||||
|
||||
verify(taskChunkMapper).selectCount(any());
|
||||
verify(taskScopeStateMapper).selectCount(any());
|
||||
verify(rustfsObjectStorageService).deleteObject("payload-key");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkFailureOnScopeAlsoKeeps() {
|
||||
when(taskScopeStateMapper.selectCount(any())).thenThrow(new RuntimeException("db down"));
|
||||
|
||||
service.deletePayloadIfPresent(RUSTFS_VALUE);
|
||||
|
||||
verify(rustfsObjectStorageService, never()).deleteObject(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void batchCandidatesQueriedInOneInClause() {
|
||||
service.deletePayloadIfPresent(RUSTFS_VALUE);
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
ArgumentCaptor<LambdaQueryWrapper> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
verify(taskChunkMapper).selectCount(captor.capture());
|
||||
String segment = captor.getValue().getSqlSegment();
|
||||
assertTrue(segment.contains("IN"), "候选值必须批量 IN 查询: " + segment);
|
||||
// 候选集 = value + pointer(json(pointer) 与 value 同值去重),IN 参数非空
|
||||
assertTrue(!captor.getValue().getParamNameValuePairs().isEmpty(),
|
||||
"IN 查询必须携带候选参数");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user