task-200: 巡检报表任务落地(InspectionSqlReportTask 跑 6 张只读报表 SQL:默认 disabled、分布式锁、单条失败不阻断、只 query)+ 8 条测试

This commit is contained in:
2026-09-05 00:08:44 +08:00
parent d6b223ba25
commit 1b27a3f1ca
2 changed files with 289 additions and 0 deletions
@@ -0,0 +1,181 @@
package com.nanri.aiimage.modules.task.service;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.nanri.aiimage.config.InspectionProperties;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* task-200:巡检报表任务契约。
* 默认 disabled 不执行;启用后跑 6 张只读报表 SQL 并输出行数;单条失败不阻断;只 query 不改数据;
* 可重复执行;无锁/JDBC 时安全返回。
*/
class InspectionSqlReportTaskTest {
private static ObjectProvider<JdbcTemplate> provider(JdbcTemplate jdbc) {
return new ObjectProvider<JdbcTemplate>() {
@Override
public JdbcTemplate getObject() {
return jdbc;
}
@Override
public JdbcTemplate getObject(Object... args) {
return jdbc;
}
@Override
public JdbcTemplate getIfAvailable() {
return jdbc;
}
@Override
public JdbcTemplate getIfUnique() {
return jdbc;
}
};
}
private static ListAppender<ILoggingEvent> attach() {
Logger logger = (Logger) LoggerFactory.getLogger(InspectionSqlReportTask.class);
ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.start();
logger.addAppender(appender);
return appender;
}
@Test
void disabledByDefaultRunsNothing() {
InspectionProperties props = new InspectionProperties();
JdbcTemplate jdbc = mock(JdbcTemplate.class);
assertEquals(false, props.isEnabled(), "巡检默认 disabled");
InspectionSqlReportTask task = new InspectionSqlReportTask(props, provider(jdbc));
assertEquals(0, task.runAll(), "disabled 不执行");
Mockito.verifyNoInteractions(jdbc);
}
@Test
void enabledRunsAllSixReports() {
InspectionProperties props = new InspectionProperties();
props.setEnabled(true);
JdbcTemplate jdbc = mock(JdbcTemplate.class);
Mockito.when(jdbc.queryForList(anyString())).thenReturn(List.of());
InspectionSqlReportTask task = new InspectionSqlReportTask(props, provider(jdbc));
assertEquals(6, task.runAll());
verify(jdbc, times(6)).queryForList(anyString());
}
@Test
void reportOutputLogsRowCount() {
InspectionProperties props = new InspectionProperties();
props.setEnabled(true);
JdbcTemplate jdbc = mock(JdbcTemplate.class);
Mockito.when(jdbc.queryForList(anyString())).thenReturn(List.of(Map.of("a", 1), Map.of("a", 2), Map.of("a", 3)));
InspectionSqlReportTask task = new InspectionSqlReportTask(props, provider(jdbc));
ListAppender<ILoggingEvent> captured = attach();
try {
task.runAll();
String text = String.join("\n", captured.list.stream().map(ILoggingEvent::getFormattedMessage).toList());
assertTrue(text.contains("rows=3"), "报表日志应含行数: " + text);
assertTrue(text.contains("[inspection-sql] report"), "应含报表前缀");
} finally {
((Logger) LoggerFactory.getLogger(InspectionSqlReportTask.class)).detachAppender(captured);
}
}
@Test
void singleReportFailureDoesNotBlockOthers() {
InspectionProperties props = new InspectionProperties();
props.setEnabled(true);
JdbcTemplate jdbc = mock(JdbcTemplate.class);
AtomicInteger calls = new AtomicInteger();
Mockito.doAnswer(inv -> {
if (calls.incrementAndGet() == 3) {
throw new RuntimeException("模拟第三条 SQL 失败");
}
return List.of();
}).when(jdbc).queryForList(anyString());
InspectionSqlReportTask task = new InspectionSqlReportTask(props, provider(jdbc));
assertEquals(5, task.runAll(), "单条失败应被捕获,其余继续");
verify(jdbc, times(6)).queryForList(anyString());
}
@Test
void scheduleConfigDefaults() {
InspectionProperties props = new InspectionProperties();
assertEquals("0 0 3 * * *", props.getCron());
assertEquals(false, props.isEnabled());
}
@Test
void onlyReadQueriesNoWrites() {
InspectionProperties props = new InspectionProperties();
props.setEnabled(true);
JdbcTemplate jdbc = mock(JdbcTemplate.class);
Mockito.when(jdbc.queryForList(anyString())).thenReturn(List.of());
InspectionSqlReportTask task = new InspectionSqlReportTask(props, provider(jdbc));
task.runAll();
verify(jdbc, times(6)).queryForList(anyString());
verify(jdbc, never()).execute(anyString());
}
@Test
void noLockOrJdbcSafelyNoop() {
InspectionProperties props = new InspectionProperties();
props.setEnabled(true);
InspectionSqlReportTask noJdbc = new InspectionSqlReportTask(props, new ObjectProvider<JdbcTemplate>() {
@Override
public JdbcTemplate getObject() {
return null;
}
@Override
public JdbcTemplate getObject(Object... args) {
return null;
}
@Override
public JdbcTemplate getIfAvailable() {
return null;
}
@Override
public JdbcTemplate getIfUnique() {
return null;
}
});
assertEquals(0, noJdbc.runAll(), "启用但无 JdbcTemplate 应安全跳过");
// 无分布式锁实例时 runIfEnabled 不应抛错(测试构造传入 null lock)
InspectionSqlReportTask noLock = new InspectionSqlReportTask(props, null, provider(mock(JdbcTemplate.class)));
noLock.runIfEnabled();
}
@Test
void enabledStableAcrossRuns() {
InspectionProperties props = new InspectionProperties();
props.setEnabled(true);
JdbcTemplate jdbc = mock(JdbcTemplate.class);
Mockito.when(jdbc.queryForList(anyString())).thenReturn(List.of());
InspectionSqlReportTask task = new InspectionSqlReportTask(props, provider(jdbc));
assertEquals(6, task.runAll());
assertEquals(6, task.runAll());
verify(jdbc, times(12)).queryForList(anyString());
}
}