test(detection): 添加检测锁管理器的单元测试

- 为 DetectionLockManager 添加完整的单元测试覆盖
- 测试空状态下的获取操作返回成功结果
- 测试被其他用户持有时的获取操作返回忙状态
- 测试同用户重入时刷新页面和过期时间功能
- 测试过期后任意用户可获取锁的功能
- 测试匹配用户时释放锁的操作
- 测试非匹配用户时释放锁的无操作行为
- 测试匹配页面时释放锁的功能
- 测试强制释放锁的操作
- 添加并发获取锁的竞态条件测试确保线程安全
This commit is contained in:
2026-06-12 19:09:00 +08:00
parent b35bbf11f7
commit 366d1fadfa
2 changed files with 158 additions and 7 deletions

View File

@@ -0,0 +1,151 @@
package com.njcn.gather.detection.lock;
import org.junit.Before;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.*;
public class DetectionLockManagerTest {
private DetectionLockManager manager;
@Before
public void setUp() throws Exception {
manager = DetectionLockManager.getInstance();
// 通过反射把 current 清零,避免不同测试方法间状态污染
Field f = DetectionLockManager.class.getDeclaredField("current");
f.setAccessible(true);
((AtomicReference<?>) f.get(manager)).set(null);
}
@Test
public void tryAcquire_whenEmpty_returnsOk() {
DetectionLockManager.AcquireResult r = manager.tryAcquire("u1", "alice", "page-1");
assertTrue(r.isOk());
assertNull(r.getHolder());
assertEquals("u1", manager.getCurrent().getUserId());
}
@Test
public void tryAcquire_whenHeldByOther_returnsBusyWithHolder() {
manager.tryAcquire("u1", "alice", "page-1");
DetectionLockManager.AcquireResult r = manager.tryAcquire("u2", "bob", "page-2");
assertFalse(r.isOk());
assertNotNull(r.getHolder());
assertEquals("u1", r.getHolder().getHolderUserId());
assertEquals("alice", r.getHolder().getHolderUserName());
}
@Test
public void tryAcquire_reentrantSameUser_refreshesPageAndExpireAt() throws Exception {
manager.tryAcquire("u1", "alice", "page-1");
long oldAcquire = manager.getCurrent().getAcquireTime();
long oldExpire = manager.getCurrent().getExpireAt();
// sleep 50ms远超 Windows 系统时钟 ~15ms 精度),保证时间戳推进
Thread.sleep(50);
DetectionLockManager.AcquireResult r = manager.tryAcquire("u1", "alice", "page-2");
assertTrue(r.isOk());
assertEquals("page-2", manager.getCurrent().getUserPageId());
assertTrue("acquireTime 应推进", manager.getCurrent().getAcquireTime() > oldAcquire);
assertTrue("expireAt 应推进", manager.getCurrent().getExpireAt() > oldExpire);
}
@Test
public void tryAcquire_whenExpired_anyUserCanAcquire() throws Exception {
// 直接构造一个 expireAt 在过去的 lock 写进去
Field f = DetectionLockManager.class.getDeclaredField("current");
f.setAccessible(true);
@SuppressWarnings("unchecked")
AtomicReference<DetectionLock> ref = (AtomicReference<DetectionLock>) f.get(manager);
long past = System.currentTimeMillis() - 1000;
ref.set(new DetectionLock("u1", "alice", "page-1", past - 1000, past));
DetectionLockManager.AcquireResult r = manager.tryAcquire("u2", "bob", "page-2");
assertTrue(r.isOk());
assertEquals("u2", manager.getCurrent().getUserId());
}
@Test
public void releaseIfHeldBy_matchingUser_clears() {
manager.tryAcquire("u1", "alice", "page-1");
manager.releaseIfHeldBy("u1", "TEST");
assertNull(manager.getCurrent());
}
@Test
public void releaseIfHeldBy_nonMatchingUser_isNoOp() {
manager.tryAcquire("u1", "alice", "page-1");
manager.releaseIfHeldBy("u2", "TEST");
assertNotNull(manager.getCurrent());
assertEquals("u1", manager.getCurrent().getUserId());
}
@Test
public void releaseIfHeldBy_whenEmpty_isNoOp() {
manager.releaseIfHeldBy("u1", "TEST");
assertNull(manager.getCurrent());
}
@Test
public void releaseIfMatchPage_matchingPage_clears() {
manager.tryAcquire("u1", "alice", "page-1");
manager.releaseIfMatchPage("page-1", "TEST");
assertNull(manager.getCurrent());
}
@Test
public void releaseIfMatchPage_nonMatchingPage_isNoOp() {
manager.tryAcquire("u1", "alice", "page-1");
manager.releaseIfMatchPage("page-2", "TEST");
assertNotNull(manager.getCurrent());
}
@Test
public void forceRelease_alwaysClears() {
manager.tryAcquire("u1", "alice", "page-1");
manager.forceRelease("admin", "TEST");
assertNull(manager.getCurrent());
}
@Test
public void concurrentTryAcquire_onlyOneWins() throws Exception {
int n = 100;
ExecutorService pool = Executors.newFixedThreadPool(16);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(n);
AtomicInteger okCount = new AtomicInteger(0);
// 记下胜出线程的 userId便于断言 holder 身份与胜出者一致
AtomicReference<String> winnerUserId = new AtomicReference<>(null);
for (int i = 0; i < n; i++) {
final String uid = "u" + i;
pool.submit(() -> {
try {
start.await();
DetectionLockManager.AcquireResult r = manager.tryAcquire(uid, uid, "page-" + uid);
if (r.isOk()) {
okCount.incrementAndGet();
winnerUserId.set(uid);
}
} catch (InterruptedException ignored) {
} finally {
done.countDown();
}
});
}
start.countDown();
done.await(5, TimeUnit.SECONDS);
pool.shutdownNow();
assertEquals("100 个不同账号并发只能有 1 个抢到锁", 1, okCount.get());
assertNotNull(manager.getCurrent());
// 持锁者必须就是宣称 ok 的那个线程,不能是别人
assertEquals("holder 身份必须等于胜出线程的 userId", winnerUserId.get(), manager.getCurrent().getUserId());
}
}

View File

@@ -80,12 +80,12 @@ public class BookmarkUtil {
P p = (P) element; P p = (P) element;
String textFromP = Docx4jUtil.getTextFromP(p); String textFromP = Docx4jUtil.getTextFromP(p);
if (textFromP.contains("测量回路")) { if (textFromP.contains("测量回路")) {
if (!textFromP.contains("1")) { // if (!textFromP.contains("1")) {
// 另起一页 // // 另起一页
P pagePara = Docx4jUtil.getPageBreak(); // P pagePara = Docx4jUtil.getPageBreak();
idx = idx + 1; // idx = idx + 1;
parentContent.add(idx, pagePara); // parentContent.add(idx, pagePara);
} // }
idx = idx + 1; idx = idx + 1;
parentContent.add(idx, p); parentContent.add(idx, p);
} }