ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

JUnit 4.13.2 发布要点深度解析:FailOnTimeout 线程组修复与 AssumptionViolatedException 序列化修复

JUnit 4.13.2 发布要点深度解析:FailOnTimeout 线程组修复与 AssumptionViolatedException 序列化修复 测试开发工具【免费下载链接】junit4A programmer-oriented testing framework for Java — :warning: maintenance mode项目地址https://gitcode.com/gh_mirrors/ju/junit4点击查看免费下载导读本文基于 JUnit 4 官方发布说明 doc/ReleaseNotes4.13.2.md 展开深入剖析该版本在三方面的核心变更FailOnTimeout含Timeout规则线程组行为的回归修复、AssumptionViolatedException携带 Hamcrest Matcher 时的序列化修复并对照 4.13.1 的安全与 API 变更背景。读完本文你将理解 JUnit 4 超时机制中卡死线程检测lookForStuckThread的底层线程模型、ThreadGroup泄漏问题的来龙去脉掌握如何用Timeout.Builder精确控制超时行为以及assumeThat()抛出的异常为何在分布式测试/序列化场景下曾引发NotSerializableException及其修复原理。版本背景JUnit 4 维护模式的最后一次系列修复JUnit 4 系列已进入维护模式项目描述明确标注 maintenance mode。4.13.2 是继 4.13、4.13.1 之后的又一个补丁版本其发布说明篇幅虽短却聚焦于两个影响面较大的历史问题线程组泄漏回归与异常序列化缺陷。它们都属于修复本身引入新问题regression的典型案例值得开发者深究其来龙去脉。Rules 之变FailOnTimeout 的 ThreadGroup 处理方式问题起点4.13 中显式销毁 ThreadGroup 的尝试与反噬4.13.2 发布说明首先回顾了 JUnit 4.13 中的一次尝试为了修复超时测试运行时创建的ThreadGroup实例泄漏问题对应 pull request #1517当时的方案是在测试执行完毕后**显式销毁destroy**为限时测试创建的ThreadGroup。然而大量用户反馈显式销毁ThreadGroup本身会带来新的问题例如破坏ThreadGroupContext等关联状态、在特定 JVM 版本上抛出不期望的异常。新方案setDaemon(true) 替代 destroy()4.13.2 将实现改为调用ThreadGroup.setDaemon(true)来替代显式销毁。在 src/main/java/org/junit/internal/runners/statements/FailOnTimeout.java 的threadGroupForNewThread()方法中可以看到完整实现private ThreadGroup threadGroupForNewThread() { if (!lookForStuckThread) { // Use the default ThreadGroup (usually the one from the current // thread). return null; } // Create the thread in a new ThreadGroup, so if the time-limited thread // becomes stuck, getStuckThread() can find the thread likely to be the // culprit. ThreadGroup threadGroup new ThreadGroup(FailOnTimeoutGroup); if (!threadGroup.isDaemon()) { // Mark the new ThreadGroup as a daemon thread group, so it will be // destroyed after the time-limited thread completes. By ensuring the // ThreadGroup is destroyed, any data associated with the ThreadGroup // (ex: via java.beans.ThreadGroupContext) is destroyed. try { threadGroup.setDaemon(true); } catch (SecurityException e) { // Swallow the exception to keep the same behavior as in JUnit 4.12. } } return threadGroup; }关键点解读当lookForStuckThread为true时仍会创建名为FailOnTimeoutGroup的新ThreadGroup这是定位卡死线程所必需的见下文卡死线程检测原理创建后立即调用setDaemon(true)将其标记为守护线程组。JVM 规范保证守护线程组在其最后一个线程终止后会被自动销毁因此无需手动 destroy既解决泄漏又避免显式销毁带来的副作用代码对SecurityException做了吞掉处理注释明确说明这是为了保持与 4.12 一致的行为。这一行为在测试 src/test/java/org/junit/internal/runners/statements/FailOnTimeoutTest.java 中得到了双重验证lookingForStuckThread_threadGroupNotLeaked断言内部线程使用与外部不同的线程组、该线程组是 daemon 线程组并在测试结束后被自动销毁isDestroyed()为 truenotLookingForStuckThread_usesSameThreadGroup断言未开启卡死线程检测时内部线程复用外部线程组。行为回归仅当 lookForStuckThreadtrue 时才创建新 ThreadGroup第二个变更对应 pull request #1691进一步收窄了创建ThreadGroup的触发条件。历史背景JUnit 4.12 引入Timeout规则的卡死线程栈回溯能力对应 pull request #742该能力默认关闭需通过Timeout.Builder.lookForStuckThread(boolean)传入true来按需开启但实现上从 4.12 起所有限时测试都被放到了一个新的ThreadGroup中运行即使没有调用lookForStuckThread()也是如此。这一隐蔽的行为变化导致部分测试出现可见差异——发布说明特别点名了依赖java.beans.ThreadGroupContext的代码。4.13.2 修正为只有调用者显式传入Timeout.Builder.lookForStuckThread(true)时才创建新ThreadGroup。未开启该特性的超时测试其线程行为与 JUnit 4.11 一致也更接近无超时测试。源码中threadGroupForNewThread()的第一段分支正是这一逻辑的直接体现if (!lookForStuckThread) { // Use the default ThreadGroup (usually the one from the current thread). return null; }需要特别提醒的是发布说明中的兼容性警告不幸的是这可能导致自 4.12 发布以来编写或更新的测试出现可见行为变化。如果此变更对你的测试产生不利影响可以通过 builder 创建Timeout规则并调用Timeout.Builder.lookForStuckThread(true)。卡死线程检测原理从源码看 FailOnTimeout 的线程模型为深入理解上述两个修复有必要厘清FailOnTimeout的完整执行流程源码位于 src/main/java/org/junit/internal/runners/statements/FailOnTimeout.javaevaluate()将测试语句包装为CallableStatement放入FutureTask在新建的线程线程名为Time-limited test中执行测试线程本身设为 daemonThread thread new Thread(threadGroup, task, Time-limited test); thread.setDaemon(true); thread.start();通过FutureTask.get(timeout, timeUnit)等待结果超时则抛出TimeoutExceptioncreateTimeoutException()在超时时取出测试线程的栈回溯设置到TestTimedOutException并调用thread.interrupt()尝试中断若开启lookForStuckThread还会调用getStuckThread()枚举FailOnTimeoutGroup线程组内的所有线程在 RUNNABLE 状态的线程中挑出 CPU 时间最长者通过 src/main/java/org/junit/internal/management/ThreadMXBean.java 提供的getThreadCpuTime将其栈回溯以MultipleFailureException形式一并抛出帮助定位看起来卡住的线程。因此ThreadGroup在这里承担着隔离与搜索范围的双重角色只有把测试线程放进独立线程组getStuckThread()才能精准枚举本次测试派生出的线程而不会误伤其他并发测试的线程。这也是为什么lookForStuckThread开启与否直接决定了是否创建新线程组。实战用法通过 Timeout.Builder 精确控制org.junit.rules.Timeout源码见 src/main/java/org/junit/rules/Timeout.java自 4.12 起提供 Builder 模式。常用的两种写法// 方式一传统写法不开启卡死线程检测 Rule public Timeout globalTimeout Timeout.millis(20); // 方式二Builder 写法开启卡死线程检测这也是 4.13.2 唯一会创建独立 ThreadGroup 的方式 Rule public Timeout globalTimeout Timeout.builder() .withTimeout(20, TimeUnit.MILLISECONDS) .withLookingForStuckThread(true) .build();Builder 的关键方法与默认值见 src/main/java/org/junit/rules/Timeout.java方法作用默认值withTimeout(long, TimeUnit)指定超时时长与时间单位0秒0 表示不超时但测试仍在线程中运行可用于按环境动态关闭超时withLookingForStuckThread(boolean)是否在超时时查找卡死线程并 dump 其栈回溯该特性标记为 experimentalfalsebuild()生成Timeout实例—底层FailOnTimeout.Buildersrc/main/java/org/junit/internal/runners/statements/FailOnTimeout.java还有两个值得注意的校验withTimeout传入负数会抛IllegalArgumentException(timeout must be non-negative)传入null的TimeUnit会抛NullPointerException。Timeout.apply()将规则包裹到语句上时若参数非法会生成一个在evaluate()时抛出RuntimeException(Invalid parameters for Timeout, e)的兜底语句保证规则失败不会静默吞掉配置错误。Exceptions 之变AssumptionViolatedException 的序列化修复问题现象NotSerializableExceptionissue #1192第二个修复对应 pull request #1654 的实现public static T void assumeThat(T actual, MatcherT matcher) { if (!matcher.matches(actual)) { throw new AssumptionViolatedException(actual, matcher); } } public static T void assumeThat(String message, T actual, MatcherT matcher) { if (!matcher.matches(actual)) { throw new AssumptionViolatedException(message, actual, matcher); } }根因AssumptionViolatedException作为Throwable必须是Serializable的但其内部字段保存了Matcher和实际值Object——绝大多数 Hamcrest Matcher 实现以及任意用户对象值并不实现Serializable。一旦该异常被序列化例如在分布式测试环境中跨进程传递、或通过 RMI/消息队列上报测试结果就会抛出NotSerializableException。修复方案自定义 writeObject 可序列化包装描述核心修复位于 src/main/java/org/junit/internal/AssumptionViolatedException.java 的writeObject方法private void writeObject(ObjectOutputStream objectOutputStream) throws IOException { ObjectOutputStream.PutField putField objectOutputStream.putFields(); putField.put(fAssumption, fAssumption); putField.put(fValueMatcher, fValueMatcher); // We have to wrap the matcher into a serializable form. putField.put(fMatcher, SerializableMatcherDescription.asSerializableMatcher(fMatcher)); // We have to wrap the value inside a non-String class ... Wrapping it makes sure // that the description of a serialized and non-serialized instance produce // the exact same description putField.put(fValue, SerializableValueDescription.asSerializableValue(fValue)); objectOutputStream.writeFields(); }该实现刻意不实现readObject注释说明这是为了保证向后兼容和遵循 Java 序列化的标准方式只覆盖writeObject通过PutField将两个原本可能不可序列化的字段替换为可序列化的描述包装Matcher 的包装src/main/java/org/junit/internal/SerializableMatcherDescription.java ——asSerializableMatcher()检查 matcher 是否为空或已实现Serializable是则原样返回否则用StringDescription.asString(matcher)预先把其文本描述捕获到一个BaseMatcherString子类中。由于异常在运行时只消费 matcher 的描述见describeTo序列化后描述不会丢失值的包装src/main/java/org/junit/internal/SerializableValueDescription.java ——asSerializableValue()同样检查值是否可序列化否则用String.valueOf(value)捕获其字符串形式。包装为非 String 类而非直接写 String是为了保证序列化前后describeTo产生的描述完全一致JUnit 的Description对 String 值加引号...、对其他对象加尖括号...。此外还保留了fValueMatcher表示构造时是否传入 valuematcher与fAssumption提示消息字段配合serialVersionUID 2L维持与历史序列化数据的兼容字段保留f前缀正是为了序列化兼容参见 src/main/java/org/junit/internal/AssumptionViolatedException.java 的注释。测试验证序列化往返与反向兼容src/test/java/org/junit/AssumptionViolatedExceptionTest.java 提供了充分的验证用例unserializableValueAndMatcherCanBeSerialized用不可序列化的值 不可序列化的 matcher 构造异常经ObjectOutputStream/ObjectInputStream往返后反序列化实例的消息与描述StringDescription.asString与原实例完全一致serializableValueAndMatcherCanBeSerialized、nullValueAndMatcherCanBeSerialized覆盖可序列化值与空值场景assumptionViolatedExceptionWithoutValueAndMatcherCanBeReserialized_v4_13/...WithValueAndMatcherCanBeReserialized_v4_13从测试资源中读取4.13 时代序列化的二进制文件反序列化验证旧数据在新版本仍可读即向后兼容性backwards compatibility得到保证。对比断言可知校验点正是message 一致 description 一致并刻意跳过 stackTrace受启动方式影响与 cause由父类负责。关联背景4.13.1 的两项先行变更4.13.2 的修复建立在此前 4.13.1 的基础上见 doc/ReleaseNotes4.13.1.md理解它们有助于把握版本演进脉络安全修复TemporaryFolder在 Java 7 上限制临时目录访问。这是一项本地信息泄露local information disclosure漏洞修复对应安全公告 GHSA-269g-pwp5-87pp。TemporaryFolder的实现位于 src/main/java/org/junit/rules/TemporaryFolder.java创建临时目录时优先使用 NIO 的Files.createTempDirectoryJava 7 路径见createTemporaryFolderWithNioApiJava 5/6 则回退到File.createTempFile加mkdir()的旧方案createTemporaryFolderWithFileApi最多重试 10000 次。该规则支持TemporaryFolder.builder().assureDeletion().build()的保证删除模式——删除失败会以AssertionError使测试失败API 变更FrameworkField构造函数从包私有提升为 public。此前自定义 Runner 只能构造FrameworkMethod实例而不能构造FrameworkField实例此变更后两者皆可见 src/main/java/org/junit/runners/model/FrameworkField.java 中Access relaxed to public since version 4.13.1的注释。FrameworkField目前主要用于BlockJUnit4ClassRunner处理Rule字段但自定义 Runner 可以做其他用途。升级与兼容性建议综合 4.13.2 发布说明与源码事实给 JUnit 4 使用者如下建议超时测试依赖线程组隔离的如果你的被测代码依赖测试线程运行在特定线程组、或依赖java.beans.ThreadGroupContext之类的状态请显式通过Timeout.builder().withLookingForStuckThread(true).build()创建规则以复现 4.12/4.13 的行为在分布式/序列化环境下使用assumeThat的升级到 4.13.2 后AssumptionViolatedException可携带任意不可序列化的matcher 与值进行序列化传输无需再自行兜底转换从 4.11 及更早版本升级的超时测试的默认线程行为将更接近 4.11——即不创建独立ThreadGroup若你的测试在 4.12 之后出现过与线程组相关的诡异行为4.13.2 很可能已经将其消除。以上分析全部基于当前仓库实际代码与发布说明涉及的验证路径包括 FailOnTimeout.java、Timeout.java、AssumptionViolatedException.java 及对应的测试文件读者可自行进入仓库对应路径核对细节。赞分享测试开发工具【免费下载链接】junit4A programmer-oriented testing framework for Java — :warning: maintenance mode项目地址https://gitcode.com/gh_mirrors/ju/junit4点击查看免费下载相关推荐JUnit 4.8 深度解析Categories 测试分类机制与 Result 线程安全修复JUnit 4.8 深度解析Categories 测试分类机制与 Result 线程安全修复 本文以 JUnit 4.8 官方发布说明 doc/Releas测试开发工具RuboCop v0.74.0 发布要点深度解析ERB 新参数自动修正与 8 项核心缺陷修复RuboCop v0.74.0 发布要点深度解析ERB 新参数自动修正与 8 项核心缺陷修复 RuboCop v0.74.0 是该项目在迈向 1.0 稳定版之代码质量Lint格式化静态分析开发工具Eclipse Mosquitto 1.0.2 版本发布深度解读持久化重启修复、线程修复与五处变更全解析Eclipse Mosquitto 1.0.2 版本发布深度解读持久化重启修复、线程修复与五处变更全解析 本文为 Mosquitto 1.0.2 版本发布公告物联网消息队列后端网络/通信上一篇OpenHands三步打造你的自托管AI开发控制中心让编码助手24小时在线工作下一篇Cline 仓库中的 OpenTUI Solid 参考解析用 opentui/solid 构建细粒度响应式终端 TUI创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表