Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add timeout config parameter #2026

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,25 @@ public final class Constants {
@API(status = EXPERIMENTAL, since = "5.5")
public static final String DEFAULT_AFTER_ALL_METHOD_TIMEOUT_PROPERTY_NAME = JupiterConfiguration.DEFAULT_AFTER_ALL_METHOD_TIMEOUT_PROPERTY_NAME;

/**
* Property used to determine if timeouts are applied to tests: {@value}.
*
* <p>The value of this property will be used to toggle whether
* {@link org.junit.jupiter.api.Timeout @Timeout} is applied to tests.</p>
*
* <h3>Examples</h3>
*
* <ul>
* <li>{@code enabled}: enables timeouts.
* <li>{@code disabled}: disables timeouts.
* <li>{@code disabled_on_debug}: disables timeouts while debugging.
* </ul>
*
* @since 5.6
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Javadoc should document the default value.

@API(status = EXPERIMENTAL, since = "5.6")
public static final String TIMEOUT_MODE_PROPERTY_NAME = JupiterConfiguration.TIMEOUT_MODE_PROPERTY_NAME;

private Constants() {
/* no-op */
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public interface JupiterConfiguration {
String DEFAULT_BEFORE_EACH_METHOD_TIMEOUT_PROPERTY_NAME = "junit.jupiter.execution.timeout.beforeeach.method.default";
String DEFAULT_AFTER_EACH_METHOD_TIMEOUT_PROPERTY_NAME = "junit.jupiter.execution.timeout.aftereach.method.default";
String DEFAULT_AFTER_ALL_METHOD_TIMEOUT_PROPERTY_NAME = "junit.jupiter.execution.timeout.afterall.method.default";
String TIMEOUT_MODE_PROPERTY_NAME = "junit.jupiter.execution.timeout.mode";

Optional<String> getRawConfigurationParameter(String key);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

package org.junit.jupiter.engine.extension;

import static org.junit.jupiter.engine.config.JupiterConfiguration.TIMEOUT_MODE_PROPERTY_NAME;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Optional;
Expand All @@ -21,6 +23,7 @@
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
import org.junit.jupiter.api.extension.InvocationInterceptor;
Expand All @@ -29,6 +32,7 @@
import org.junit.platform.commons.support.AnnotationSupport;
import org.junit.platform.commons.util.ClassUtils;
import org.junit.platform.commons.util.ReflectionUtils;
import org.junit.platform.commons.util.RuntimeUtils;

/**
* @since 5.5
Expand All @@ -38,6 +42,9 @@ class TimeoutExtension implements BeforeAllCallback, BeforeEachCallback, Invocat
private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(Timeout.class);
private static final String TESTABLE_METHOD_TIMEOUT_KEY = "testable_method_timeout_from_annotation";
private static final String GLOBAL_TIMEOUT_CONFIG_KEY = "global_timeout_config";
public static final String ENABLED_MODE_VALUE = "enabled";
public static final String DISABLED_MODE_VALUE = "disabled";
public static final String DISABLED_ON_DEBUG_MODE_VALUE = "disabled_on_debug";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be private.


@Override
public void beforeAll(ExtensionContext context) {
Expand Down Expand Up @@ -145,7 +152,7 @@ private TimeoutConfiguration getGlobalTimeoutConfiguration(ExtensionContext exte

private <T> Invocation<T> decorate(Invocation<T> invocation, ReflectiveInvocationContext<Method> invocationContext,
ExtensionContext extensionContext, TimeoutDuration timeout) {
if (timeout == null) {
if (timeout == null || isModeIndicatingDisabled(extensionContext)) {
return invocation;
}
return new TimeoutInvocation<>(invocation, timeout, getExecutor(extensionContext),
Expand All @@ -165,6 +172,24 @@ private ScheduledExecutorService getExecutor(ExtensionContext extensionContext)
return extensionContext.getRoot().getStore(NAMESPACE).getOrComputeIfAbsent(ExecutorResource.class).get();
}

private boolean isModeIndicatingDisabled(ExtensionContext extensionContext) {
Optional<String> mode = extensionContext.getConfigurationParameter(TIMEOUT_MODE_PROPERTY_NAME);
return mode.filter(this::isDisabled).isPresent();
}

private boolean isDisabled(String mode) {
if (mode.equals(ENABLED_MODE_VALUE)) {
return false;
}
if (mode.equals(DISABLED_MODE_VALUE)) {
return true;
}
if (mode.equals(DISABLED_ON_DEBUG_MODE_VALUE)) {
return RuntimeUtils.isDebug();
}
throw new ExtensionConfigurationException("Unsupported timeout extension mode: " + mode);
}

@FunctionalInterface
private interface TimeoutProvider extends Function<TimeoutConfiguration, Optional<TimeoutDuration>> {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static org.junit.jupiter.engine.Constants.DEFAULT_TEST_FACTORY_METHOD_TIMEOUT_PROPERTY_NAME;
import static org.junit.jupiter.engine.Constants.DEFAULT_TEST_METHOD_TIMEOUT_PROPERTY_NAME;
import static org.junit.jupiter.engine.Constants.DEFAULT_TEST_TEMPLATE_METHOD_TIMEOUT_PROPERTY_NAME;
import static org.junit.jupiter.engine.Constants.TIMEOUT_MODE_PROPERTY_NAME;
import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement;
import static org.junit.platform.engine.TestExecutionResult.Status.FAILED;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
Expand All @@ -47,6 +48,7 @@
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.engine.AbstractJupiterTestEngineTests;
import org.junit.platform.commons.PreconditionViolationException;
import org.junit.platform.commons.util.RuntimeUtils;
import org.junit.platform.testkit.engine.EngineExecutionResults;
import org.junit.platform.testkit.engine.Events;
import org.junit.platform.testkit.engine.Execution;
Expand Down Expand Up @@ -74,6 +76,46 @@ void appliesTimeoutOnAnnotatedTestMethods() {
.hasMessage("testMethod() timed out after 10 milliseconds");
}

@Test
@DisplayName("is not applied on annotated @Test methods using timeout mode: disabled")
void doesNotApplyTimeoutOnAnnotatedTestMethodsUsingDisabledTimeoutMode() {
EngineExecutionResults results = executeTests(request() //
.selectors(selectMethod(TimeoutAnnotatedTestMethodTestCase.class, "testMethod")) //
.configurationParameter(DEFAULT_TEST_METHOD_TIMEOUT_PROPERTY_NAME, "42ns") //
.configurationParameter(TIMEOUT_MODE_PROPERTY_NAME, "disabled").build());

Execution execution = findExecution(results.testEvents(), "testMethod()");
assertThat(execution.getTerminationInfo().getExecutionResult().getThrowable()) //
.isEmpty();
}

@Test
@DisplayName("is not applied on annotated @Test methods using timeout mode: disabled")
void applyTimeoutOnAnnotatedTestMethodsUsingDisabledOnDebugTimeoutMode() {
EngineExecutionResults results = executeTests(request() //
.selectors(selectMethod(TimeoutAnnotatedTestMethodTestCase.class, "testMethod")) //
.configurationParameter(DEFAULT_TEST_METHOD_TIMEOUT_PROPERTY_NAME, "42ns") //
.configurationParameter(TIMEOUT_MODE_PROPERTY_NAME, "disabled_on_debug").build());

Execution execution = findExecution(results.testEvents(), "testMethod()");

assertThat(execution.getDuration()) //
.isGreaterThanOrEqualTo(Duration.ofMillis(10)) //
// The check to see if debugging is pushing the timer just above 1 second
.isLessThan(Duration.ofSeconds(2));

// Should we test if we're debugging? This test will fail if we are debugging.
if (RuntimeUtils.isDebug()) {
assertThat(execution.getTerminationInfo().getExecutionResult().getThrowable()) //
.isEmpty();
}
else {
assertThat(execution.getTerminationInfo().getExecutionResult().getThrowable().orElseThrow()) //
.isInstanceOf(TimeoutException.class) //
.hasMessage("testMethod() timed out after 10 milliseconds");
}
chrischild marked this conversation as resolved.
Show resolved Hide resolved
}

@Test
@DisplayName("is applied on annotated @TestTemplate methods")
void appliesTimeoutOnAnnotatedTestTemplateMethods() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2015-2019 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.commons.util;

import static org.apiguardian.api.API.Status.INTERNAL;

import java.util.List;
import java.util.Optional;

import org.apiguardian.api.API;

/**
* Collection of utilities for working with {@link Runtime},
* {@link java.lang.management.RuntimeMXBean}, etc.
*
* <h3>DISCLAIMER</h3>
*
* <p>These utilities are intended solely for usage within the JUnit framework
* itself. <strong>Any usage by external parties is not supported.</strong>
* Use at your own risk!
*
* @since 1.6
*/
@API(status = INTERNAL, since = "1.6")
public final class RuntimeUtils {

private RuntimeUtils() {
/* no-op */
}

/**
* Try to get the input arguments the VM was started with.
*/
public static Optional<List<String>> getInputArguments() {
Optional<Class<?>> managementFactoryClass = ReflectionUtils.tryToLoadClass(
"java.lang.management.ManagementFactory").toOptional();
if (!managementFactoryClass.isPresent()) {
return Optional.empty();
}
// Can't use "java.lang.management.ManagementFactory.getRuntimeMXBean().getInputArguments()"
// directly as module "java.management" might not be available and/or the current platform
// doesn't support the Java Management Extensions (JMX) API (like Android?).
// See https://github.com/junit-team/junit4/pull/1187
try {
Object bean = managementFactoryClass.get().getMethod("getRuntimeMXBean").invoke(null);
Class<?> mx = ReflectionUtils.tryToLoadClass("java.lang.management.RuntimeMXBean").get();
@SuppressWarnings("unchecked")
List<String> args = (List<String>) mx.getMethod("getInputArguments").invoke(bean);
return Optional.of(args);
}
catch (Exception e) {
return Optional.empty();
}
}

/**
* Try to determine whether the VM was started in debug mode or not.
*/
public static boolean isDebug() {
Optional<List<String>> optionalArguments = getInputArguments();
if (!optionalArguments.isPresent()) {
return false;
}
for (String argument : optionalArguments.get()) {
if (argument.startsWith("-agentlib:jdwp")) {
return true;
}
}
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
*/

module org.junit.platform.commons {
requires java.logging; // TODO Is "requires transitive java.logging" needed here?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the superseded TODO on-the-fly -- the module API of org.junit.platform.commons does not use types of java.logging.

requires java.logging;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left the static modifier out on purpose -- because we need the module when running on the module path.

Otherwise, users have to add it via --add-module java.management or via their module descriptor(s).

requires java.management; // needed by RuntimeUtils to determine input arguments
requires transitive org.apiguardian.api;

exports org.junit.platform.commons;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2015-2019 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.commons.util;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

/**
* Unit tests for {@link RuntimeUtils}.
*
* @since 1.6
*/
class RuntimeUtilsTests {

@Test
void jmxIsAvailableAndInputArgumentsAreReturned() {
var optionalArguments = RuntimeUtils.getInputArguments();
assertTrue(optionalArguments.isPresent(), "JMX not available or something else happened...");
var arguments = optionalArguments.get();
assertNotNull(arguments);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ exports org.junit.platform.commons.function
exports org.junit.platform.commons.support
requires java.base mandated
requires java.logging
requires java.management
requires org.apiguardian.api transitive
qualified exports org.junit.platform.commons.logging to org.junit.jupiter.api org.junit.jupiter.engine org.junit.jupiter.migrationsupport org.junit.jupiter.params org.junit.platform.console org.junit.platform.engine org.junit.platform.launcher org.junit.platform.reporting org.junit.platform.runner org.junit.platform.suite.api org.junit.platform.testkit org.junit.vintage.engine
qualified exports org.junit.platform.commons.util to org.junit.jupiter.api org.junit.jupiter.engine org.junit.jupiter.migrationsupport org.junit.jupiter.params org.junit.platform.console org.junit.platform.engine org.junit.platform.launcher org.junit.platform.reporting org.junit.platform.runner org.junit.platform.suite.api org.junit.platform.testkit org.junit.vintage.engine