JUnit框架本身是支持 TestRule注解的
学习本文前可参考
http://www.testclass.net/junit/rule
废话
本文着重在学习TimeoutRule原理
在阅读源码前,先多比比一句,最近学习到的观点,比较认可,分享给大家。

1.要解决什么问题?
2.为什么能解决这个问题?
3.我们做该怎么办?
简化一下,是什么?为什么?怎么办?
那我以后写文章也遵从这几点
描述现象,解释原理,表达观点。
正文
具体需求就是,在实现单测的时候,我们需要对单测用例的执行时长进行一个监控,用例不可能无限制的执行,尤其在流水线自动化的情况下,用例的个数数量级一上来,整体耗时就非常大,需要对单测执行耗时进行监管。
举个例子。某单测用例执行耗时3s,1000个类似的可能需要3000s,或者出现某用例死锁,最终无法继续。
能不能对用例的执行时长进行限制,比如1s,超过1s就中断执行。
答案当然是可以的,不然今天也就不会读源码了。
其中一个方案就是 JUnit的原生注解 Timeout Rule
该注解如何使用我就不说了。
TimoutRule源码很简单,去掉builder,构造函数之后,就这两段有用。
其实就是个包装。真正生效是FailOnTimeout
protected Statement createFailOnTimeoutStatement(
Statement statement) throws Exception {
return FailOnTimeout.builder()
.withTimeout(timeout, timeUnit)
.withLookingForStuckThread(lookForStuckThread)
.build(statement);
}
public Statement apply(Statement base, Description description) {
try {
return createFailOnTimeoutStatement(base);
} catch (final Exception e) {
return new Statement() {
@Override public void evaluate() throws Throwable {
throw new RuntimeException("Invalid parameters for Timeout", e);
}
};
}
}
FailOnTimeout
同样去掉builder看关键代码
@Override
public void evaluate() throws Throwable {
CallableStatement callable = new CallableStatement();
FutureTask<Throwable> task = new FutureTask<Throwable>(callable);
ThreadGroup threadGroup = new ThreadGroup("FailOnTimeoutGroup");
Thread thread = new Thread(threadGroup, task, "Time-limited test");
try {
thread.setDaemon(true);
thread.start();
callable.awaitStarted();
Throwable throwable = getResult(task, thread);
if (throwable != null) {
throw throwable;
}
} finally {
try {
thread.join(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
try {
threadGroup.destroy();
} catch (IllegalThreadStateException e) {
// If a thread from the group is still alive, the ThreadGroup cannot be destroyed.
// Swallow the exception to keep the same behavior prior to this change.
}
}
}
...
private class CallableStatement implements Callable<Throwable> {
private final CountDownLatch startLatch = new CountDownLatch(1);
public Throwable call() throws Exception {
try {
startLatch.countDown();
originalStatement.evaluate();
} catch (Exception e) {
throw e;
} catch (Throwable e) {
return e;
}
return null;
}
public void awaitStarted() throws InterruptedException {
startLatch.await();
}
}
...
private Throwable getResult(FutureTask<Throwable> task, Thread thread) {
try {
if (timeout > 0) {
return task.get(timeout, timeUnit);
} else {
return task.get();
}
} catch (InterruptedException e) {
return e; // caller will re-throw; no need to call Thread.interrupt()
} catch (ExecutionException e) {
// test failed; have caller re-throw the exception thrown by the test
return e.getCause();
} catch (TimeoutException e) {
return createTimeoutException(thread);
}
}
private Exception createTimeoutException(Thread thread) {
StackTraceElement[] stackTrace = thread.getStackTrace();
final Thread stuckThread = lookForStuckThread ? getStuckThread(thread) : null;
Exception currThreadException = new TestTimedOutException(timeout, timeUnit);
if (stackTrace != null) {
currThreadException.setStackTrace(stackTrace);
thread.interrupt();
}
if (stuckThread != null) {
Exception stuckThreadException =
new Exception("Appears to be stuck in thread " +
stuckThread.getName());
stuckThreadException.setStackTrace(getStackTrace(stuckThread));
return new MultipleFailureException(
Arrays.<Throwable>asList(currThreadException, stuckThreadException));
} else {
return currThreadException;
}
}
看到这里应该就差不多知道了,他起了一个新的线程,注意,这里是直接new的
计时的是callable,然后调用originStatement执行
startLatch.countDown();
originalStatement.evaluate();
再仔细看这段
thread先启动,然后让倒计时线程wait

callable执行,倒计时线程开始,然后原真正的单测执行。

然后看这个FutureTask的执行返回

这里就是借助FutureTask的超时机制。
超时之后,是把new出来的thread中断掉

也就是说,
新开一个thread 执行原始单测,超时后,中断该thread,原用例执行失败。
以上就是TimeoutRule的全部实现,很简单。
深入
我们再往下思考,
当时遇到的问题是,我们如果执行了某些单测,单测内部有Android的 Handler mock
那么该单测会报 Looper need init的异常。
其实上方源码也很清楚了,因为我们是新开的thread去执行一个单测,那么单测中有handler,必然就存在
没有looper的情况。
这里的解法我当时尝试了是 copy源码魔改,在thread启动前,Looper.prepare()
能解决looper的问题。
但是会带来更多关于looper 问题。
这里暂时没有好的思路。
问题
就是关于 Rule注解的执行时机的问题,这里不属于本文原理的源码范围,我先鸽吧。
网友评论