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
This commit is contained in:
Sam Brannen 2018-11-23 16:30:24 +01:00
parent 91de8d265e
commit aa7f69a5d1
3 changed files with 93 additions and 1 deletions

View File

@ -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);

View File

@ -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;
@RunWith(JUnitPlatform.class)
@IncludeEngines("junit-jupiter")
@SelectPackages("org.springframework.test.context.junit.jupiter")
@IncludeClassNamePatterns(".*Tests$")
@ExcludeTags("failing-test-case")
@UseTechnicalNames
public class SpringJUnitJupiterTestSuite {

View File

@ -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 {
}
}
}