Browse Source

Ensure that parameter resolution in SpringExtension is thread-safe

Prior to this commit, parallel execution of @BeforeEach and @AfterEach
methods that accepted @Autowired arguments would fail intermittently
due to a race condition in the internal implementation of the JDK's
java.lang.reflect.Executable.getParameters() method.

This commit addresses this issue by creating instances of
SynthesizingMethodParameter via
SynthesizingMethodParameter.forExecutable(Executable, int) instead of
SynthesizingMethodParameter.forParameter(Parameter), since the latter
looks up the parameter index by iterating over the array returned by
Executable.getParameters() (which is not thread-safe).

Issue: SPR-17533
pull/2025/head
Sam Brannen 7 years ago
parent
commit
aa7f69a5d1
  1. 3
      spring-test/src/main/java/org/springframework/test/context/junit/jupiter/ParameterAutowireUtils.java
  2. 2
      spring-test/src/test/java/org/springframework/test/context/junit/SpringJUnitJupiterTestSuite.java
  3. 89
      spring-test/src/test/java/org/springframework/test/context/junit/jupiter/parallel/ParallelExecutionSpringExtensionTests.java

3
spring-test/src/main/java/org/springframework/test/context/junit/jupiter/ParameterAutowireUtils.java

@ -118,7 +118,8 @@ abstract class ParameterAutowireUtils { @@ -118,7 +118,8 @@ abstract class ParameterAutowireUtils {
Autowired autowired = AnnotatedElementUtils.findMergedAnnotation(annotatedParameter, Autowired.class);
boolean required = (autowired == null || autowired.required());
MethodParameter methodParameter = SynthesizingMethodParameter.forParameter(parameter);
MethodParameter methodParameter = SynthesizingMethodParameter.forExecutable(
parameter.getDeclaringExecutable(), parameterIndex);
DependencyDescriptor descriptor = new DependencyDescriptor(methodParameter, required);
descriptor.setContainingClass(containingClass);
return applicationContext.getAutowireCapableBeanFactory().resolveDependency(descriptor, null);

2
spring-test/src/test/java/org/springframework/test/context/junit/SpringJUnitJupiterTestSuite.java

@ -18,6 +18,7 @@ package org.springframework.test.context.junit; @@ -18,6 +18,7 @@ package org.springframework.test.context.junit;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.ExcludeTags;
import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.UseTechnicalNames;
@ -48,6 +49,7 @@ import org.junit.runner.RunWith; @@ -48,6 +49,7 @@ import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@IncludeEngines("junit-jupiter")
@SelectPackages("org.springframework.test.context.junit.jupiter")
@IncludeClassNamePatterns(".*Tests$")
@ExcludeTags("failing-test-case")
@UseTechnicalNames
public class SpringJUnitJupiterTestSuite {

89
spring-test/src/test/java/org/springframework/test/context/junit/jupiter/parallel/ParallelExecutionSpringExtensionTests.java

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.junit.jupiter.parallel;
import java.lang.reflect.Parameter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.engine.Constants.*;
import static org.junit.platform.engine.discovery.DiscoverySelectors.*;
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.*;
/**
* Integration tests which verify that {@code @BeforeEach} and {@code @AfterEach} methods
* that accept {@code @Autowired} arguments can be executed in parallel without issues
* regarding concurrent access to the {@linkplain Parameter parameters} of such methods.
*
* @author Sam Brannen
* @since 5.1.3
*/
class ParallelExecutionSpringExtensionTests {
private static final int NUM_TESTS = 1000;
@RepeatedTest(10)
void runTestsInParallel() {
Launcher launcher = LauncherFactory.create();
SummaryGeneratingListener listener = new SummaryGeneratingListener();
launcher.registerTestExecutionListeners(listener);
LauncherDiscoveryRequest request = request()//
.configurationParameter(PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, "true")//
.configurationParameter(PARALLEL_CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME, "10")//
.selectors(selectClass(TestCase.class))//
.build();
launcher.execute(request);
assertEquals(NUM_TESTS, listener.getSummary().getTestsSucceededCount(),
"number of tests executed successfully");
}
@SpringJUnitConfig
static class TestCase {
@BeforeEach
void beforeEach(@Autowired ApplicationContext context) {
}
@RepeatedTest(NUM_TESTS)
void repeatedTest(@Autowired ApplicationContext context) {
}
@AfterEach
void afterEach(@Autowired ApplicationContext context) {
}
@Configuration
static class Config {
}
}
}
Loading…
Cancel
Save