美文网首页
JUnit 耗时中断Rule原理

JUnit 耗时中断Rule原理

作者: 普通的程序员 | 来源:发表于2020-09-29 18:28 被阅读0次

JUnit框架本身是支持 TestRule注解的
学习本文前可参考
http://www.testclass.net/junit/rule

废话

本文着重在学习TimeoutRule原理
在阅读源码前,先多比比一句,最近学习到的观点,比较认可,分享给大家。


image.png

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


image.png

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


image.png

然后看这个FutureTask的执行返回


image.png

这里就是借助FutureTask的超时机制。

超时之后,是把new出来的thread中断掉


image.png

也就是说,
新开一个thread 执行原始单测,超时后,中断该thread,原用例执行失败。

以上就是TimeoutRule的全部实现,很简单。

深入

我们再往下思考,
当时遇到的问题是,我们如果执行了某些单测,单测内部有Android的 Handler mock
那么该单测会报 Looper need init的异常。

其实上方源码也很清楚了,因为我们是新开的thread去执行一个单测,那么单测中有handler,必然就存在
没有looper的情况。

这里的解法我当时尝试了是 copy源码魔改,在thread启动前,Looper.prepare()

能解决looper的问题。
但是会带来更多关于looper 问题。

这里暂时没有好的思路。

问题

就是关于 Rule注解的执行时机的问题,这里不属于本文原理的源码范围,我先鸽吧。

相关文章

网友评论

      本文标题:JUnit 耗时中断Rule原理

      本文链接:https://www.haomeiwen.com/subject/zzadektx.html