extendWith = AnnotatedElementUtils.findAllMergedAnnotations(testClass, ExtendWith.class);
97 | if (extendWith.isEmpty()) {
98 | return false;
99 | }
100 |
101 | return extendWith.stream()
102 | .map(ExtendWith::value)
103 | .flatMap(Arrays::stream)
104 | .anyMatch(SpringExtension.class::isAssignableFrom);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/JUnitPlatformSupport.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext;
2 |
3 | import org.springframework.util.ClassUtils;
4 |
5 | /**
6 | * @author Sergey Chernov
7 | */
8 | final class JUnitPlatformSupport {
9 |
10 | @Deprecated
11 | private static final boolean JUNIT_VINTAGE_ENGINE_PRESENT = isClassPresent(
12 | "org.junit.vintage.engine.descriptor.RunnerTestDescriptor");
13 |
14 | @Deprecated
15 | private static final boolean JUNIT4_PRESENT = isClassPresent(
16 | "org.junit.runner.RunWith");
17 |
18 | private static final boolean JUNIT5_JUPITER_API_PRESENT = isClassPresent(
19 | "org.junit.jupiter.api.extension.ExtendWith");
20 |
21 | private static final boolean JUNIT4_IDEA_TEST_RUNNER_PRESENT = isClassPresent(
22 | "com.intellij.junit4.JUnit4IdeaTestRunner");
23 |
24 | @Deprecated
25 | static boolean isJUnitVintageEnginePresent() {
26 | return JUNIT_VINTAGE_ENGINE_PRESENT;
27 | }
28 |
29 | @Deprecated
30 | static boolean isJunit4Present() {
31 | return JUNIT4_PRESENT;
32 | }
33 |
34 | static boolean isJunit5JupiterApiPresent() {
35 | return JUNIT5_JUPITER_API_PRESENT;
36 | }
37 |
38 | @Deprecated
39 | static boolean isJUnit4IdeaTestRunnerPresent() {
40 | return JUNIT4_IDEA_TEST_RUNNER_PRESENT;
41 | }
42 |
43 | private static boolean isClassPresent(String className) {
44 | return ClassUtils.isPresent(className, JUnitPlatformSupport.class.getClassLoader());
45 | }
46 |
47 | private JUnitPlatformSupport() {
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/SmartDirtiesContextTestExecutionListener.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext;
2 |
3 | import static com.github.seregamorph.testsmartcontext.SmartDirtiesTestsSupport.isInnerClass;
4 |
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.test.context.TestContext;
8 | import org.springframework.test.context.support.AbstractTestExecutionListener;
9 |
10 | /**
11 | * Listener that works in more tricky way than spring
12 | * {@link org.springframework.test.context.support.DirtiesContextTestExecutionListener}. Based on known list (ordered)
13 | * of tests to execute (reordered via {@link com.github.seregamorph.testsmartcontext.jupiter.SmartDirtiesClassOrderer}
14 | * for Jupiter classes, {@link com.github.seregamorph.testsmartcontext.testng.SmartDirtiesSuiteListener} for TestNG
15 | * classes or {@link SmartDirtiesPostDiscoveryFilter} for JUnit 4 classes), the last test in each group that shares the
16 | * same configuration (=share the same spring context) will automatically close the ApplicationContext on after-class,
17 | * which will release resources as well (like Docker containers defined as spring beans). See detailed explanation README.
19 | *
20 | * @author Sergey Chernov
21 | * @see com.github.seregamorph.testsmartcontext.jupiter.SmartDirtiesClassOrderer
22 | * @see com.github.seregamorph.testsmartcontext.testng.SmartDirtiesSuiteListener
23 | * @see SpringContextEventLoggerListener
24 | */
25 | public class SmartDirtiesContextTestExecutionListener extends AbstractTestExecutionListener {
26 |
27 | private static final Logger logger = LoggerFactory.getLogger(SmartDirtiesContextTestExecutionListener.class);
28 |
29 | @Override
30 | public int getOrder() {
31 | // DirtiesContextTestExecutionListener.getOrder() + 1
32 | //noinspection MagicNumber
33 | return 3001;
34 | }
35 |
36 | @Override
37 | public void beforeTestClass(TestContext testContext) {
38 | // stack Nested classes
39 | CurrentTestContext.pushCurrentTestClass(testContext.getTestClass());
40 | Class> testClass = testContext.getTestClass();
41 | if (isInnerClass(testClass)) {
42 | SmartDirtiesTestsSupport.verifyInnerClass(testClass);
43 | }
44 | }
45 |
46 | @Override
47 | public void afterTestClass(TestContext testContext) {
48 | try {
49 | Class> testClass = testContext.getTestClass();
50 | if (SmartDirtiesTestsSupport.isLastClassPerConfig(testClass)) {
51 | logger.info("markDirty (closing context) after {}", testClass.getName());
52 | testContext.markApplicationContextDirty(null);
53 | } else {
54 | logger.debug("Reusing context after {}", testClass.getName());
55 | }
56 | } finally {
57 | // pop Nested classes
58 | CurrentTestContext.popCurrentTestClass();
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/SmartDirtiesPostDiscoveryFilter.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext;
2 |
3 | import java.util.Arrays;
4 | import java.util.List;
5 | import java.util.Set;
6 | import java.util.stream.Collectors;
7 | import org.junit.platform.engine.FilterResult;
8 | import org.junit.platform.engine.TestDescriptor;
9 | import org.junit.platform.engine.TestSource;
10 | import org.junit.platform.engine.support.descriptor.ClassSource;
11 | import org.junit.platform.launcher.PostDiscoveryFilter;
12 | import org.springframework.lang.NonNull;
13 | import org.springframework.lang.Nullable;
14 |
15 | /**
16 | * Auto-discovered JUnit platform {@link PostDiscoveryFilter} which reorders and groups integration test classes
17 | * according to their configuration. Note: this class sorts only JUnit 4 and Kotest tests executed via
18 | * vintage-engine
19 | * or Kotest Engine.
20 | *
21 | * For TestNG test classes - see {@link com.github.seregamorph.testsmartcontext.testng.SmartDirtiesSuiteListener}, for
22 | * Jupiter test classes - see {@link com.github.seregamorph.testsmartcontext.jupiter.SmartDirtiesClassOrderer}.
23 | *
24 | * @author Sergey Chernov
25 | */
26 | public class SmartDirtiesPostDiscoveryFilter implements PostDiscoveryFilter {
27 |
28 | private static final List skippedEngines = Arrays.asList(
29 | SmartDirtiesTestsSupport.ENGINE_JUNIT_JUPITER,
30 | SmartDirtiesTestsSupport.ENGINE_TESTNG
31 | );
32 |
33 | @Override
34 | public FilterResult apply(TestDescriptor testDescriptor) {
35 | String engine = testDescriptor.getUniqueId().getEngineId().orElse("undefined");
36 | if (skippedEngines.contains(engine)) {
37 | // JUnit 5 Jupiter and TestNG have their own test ordering solutions, skip it
38 | return FilterResult.included("Skipping engine " + engine);
39 | }
40 |
41 | List childrenToReorder = testDescriptor.getChildren().stream()
42 | .filter(childTestDescriptor -> {
43 | // If it is a testng-engine running TestNG test, rely on SmartDirtiesSuiteListener, because
44 | // TestNG will alphabetically reorder it first anyway.
45 | // Jupiter engine has its own sorting via SmartDirtiesClassOrderer, so skip them as well.
46 | // Reorder only JUnit4 or Kotest here:
47 | return getTestClassOrNull(childTestDescriptor) != null;
48 | })
49 | .collect(Collectors.toList());
50 |
51 | if (childrenToReorder.isEmpty()) {
52 | return FilterResult.included("Empty list");
53 | }
54 |
55 | Set> uniqueClasses = childrenToReorder.stream()
56 | .map(SmartDirtiesPostDiscoveryFilter::getTestClass)
57 | .collect(Collectors.toSet());
58 | if (uniqueClasses.size() == 1) {
59 | // This filter is executed several times during discover and execute phases and
60 | // it's not possible to distinguish them here. Sometimes per single test is sent as argument,
61 | // sometimes - the whole suite. If it's a suite more than 1, we can save it and never update.
62 | // If it's 1 - we should also distinguish single test execution.
63 | if (SmartDirtiesTestsSupport.classOrderStateMapSize(engine) <= 1) {
64 | Class> testClass = getTestClass(childrenToReorder.get(0));
65 | SmartDirtiesTestsSupport.setTestClassesLists(engine, TestSortResult.singletonList(testClass));
66 | }
67 |
68 | // the logic here may differ for JUnit 4 via Maven vs IntelliJ:
69 | // Maven calls this filter several times (first per each test, then with all tests)
70 | return FilterResult.included("Skipping single element");
71 | }
72 |
73 | childrenToReorder.forEach(testDescriptor::removeChild);
74 |
75 | SmartDirtiesTestsSorter sorter = SmartDirtiesTestsSorter.getInstance();
76 | TestSortResult testSortResult;
77 | try {
78 | testSortResult = sorter.sort(childrenToReorder,
79 | TestClassExtractor.ofClass(SmartDirtiesPostDiscoveryFilter::getTestClass));
80 | } catch (Throwable e) {
81 | SmartDirtiesTestsSupport.setFailureCause(e);
82 | throw e;
83 | }
84 |
85 | childrenToReorder.forEach(testDescriptor::addChild);
86 |
87 | SmartDirtiesTestsSupport.setTestClassesLists(engine, testSortResult);
88 |
89 | return FilterResult.included("sorted");
90 | }
91 |
92 | @NonNull
93 | private static Class> getTestClass(TestDescriptor testDescriptor) {
94 | Class> testClass = getTestClassOrNull(testDescriptor);
95 | if (testClass == null) {
96 | throw new UnsupportedOperationException("Unsupported TestDescriptor type " + testDescriptor.getClass()
97 | + ", failed to obtain test class");
98 | }
99 | return testClass;
100 | }
101 |
102 | @Nullable
103 | private static Class> getTestClassOrNull(TestDescriptor testDescriptor) {
104 | TestSource testSource = testDescriptor.getSource().orElse(null);
105 | if (testSource instanceof ClassSource) {
106 | ClassSource classSource = (ClassSource) testSource;
107 | return classSource.getJavaClass();
108 | }
109 |
110 | return null;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/SpringContextEventLoggerListener.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext;
2 |
3 | import java.util.Locale;
4 | import java.util.concurrent.TimeUnit;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.context.ApplicationListener;
8 | import org.springframework.context.event.ApplicationContextEvent;
9 | import org.springframework.context.event.ContextClosedEvent;
10 | import org.springframework.context.event.ContextRefreshedEvent;
11 |
12 | /**
13 | * Helper bean that logs spring bootstrap and shutdown events.
14 | *
15 | * @author Sergey Chernov
16 | * @see SmartDirtiesContextTestExecutionListener
17 | */
18 | public class SpringContextEventLoggerListener implements ApplicationListener {
19 |
20 | private static final Logger logger = LoggerFactory.getLogger(SpringContextEventLoggerListener.class);
21 |
22 | private final long createdNanos = System.nanoTime();
23 |
24 | public SpringContextEventLoggerListener() {
25 | onCreated();
26 | }
27 |
28 | protected void onCreated() {
29 | String currentTestClassName = CurrentTestContext.getCurrentTestClassName();
30 | if (currentTestClassName == null) {
31 | logger.error("Could not resolve current class name, ensure that SmartDirtiesContextTestExecutionListener " +
32 | "is registered for test class. Hint:\n" +
33 | "Maybe you should set @TestExecutionListeners.mergeMode to MERGE_WITH_DEFAULTS for your test class.");
34 | } else {
35 | logger.info("Creating context for {}", currentTestClassName);
36 | }
37 | }
38 |
39 | @Override
40 | public void onApplicationEvent(ApplicationContextEvent event) {
41 | if (event instanceof ContextRefreshedEvent) {
42 | onContextRefreshedEvent((ContextRefreshedEvent) event);
43 | } else if (event instanceof ContextClosedEvent) {
44 | onContextClosedEvent((ContextClosedEvent) event);
45 | }
46 | }
47 |
48 | protected void onContextRefreshedEvent(ContextRefreshedEvent event) {
49 | long nowNanos = System.nanoTime();
50 | String contextInitFormatted = formatNanos(nowNanos - createdNanos);
51 | boolean isChild = event.getApplicationContext().getParent() != null;
52 | logger.info("Created {} in {} for {}",
53 | isChild ? "child context" : "context", contextInitFormatted, CurrentTestContext.getCurrentTestClassName());
54 | }
55 |
56 | protected void onContextClosedEvent(ContextClosedEvent event) {
57 | String testClassIdentifier = CurrentTestContext.getCurrentTestClassName();
58 | boolean isChild = event.getApplicationContext().getParent() != null;
59 | if (testClassIdentifier == null) {
60 | // system shutdown hook
61 | logger.info("Destroying {} (hook)", isChild ? "child context" : "context");
62 | } else {
63 | // triggered via SmartDirtiesContextTestExecutionListener or spring DirtiesContextTestExecutionListener
64 | logger.info("Destroying {} for {}", isChild ? "child context" : "context", testClassIdentifier);
65 | }
66 | }
67 |
68 | static String formatNanos(long timeNanos) {
69 | long millis = TimeUnit.NANOSECONDS.toMillis(timeNanos);
70 | return String.format(Locale.ROOT, "%.3f", millis / 1000.0d) + "s";
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/SpringContextEventLoggerListenerCustomizerFactory.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext;
2 |
3 | import java.util.Iterator;
4 | import java.util.List;
5 | import java.util.ServiceLoader;
6 | import org.springframework.context.ConfigurableApplicationContext;
7 | import org.springframework.lang.NonNull;
8 | import org.springframework.test.context.ContextConfigurationAttributes;
9 | import org.springframework.test.context.ContextCustomizer;
10 | import org.springframework.test.context.ContextCustomizerFactory;
11 | import org.springframework.test.context.MergedContextConfiguration;
12 |
13 | /**
14 | * @author Sergey Chernov
15 | */
16 | public class SpringContextEventLoggerListenerCustomizerFactory implements ContextCustomizerFactory {
17 |
18 | @Override
19 | public ContextCustomizer createContextCustomizer(
20 | @NonNull Class> testClass,
21 | @NonNull List configAttributes
22 | ) {
23 | return new ContextCustomizerImpl();
24 | }
25 |
26 | private static class ContextCustomizerImpl implements ContextCustomizer {
27 |
28 | @Override
29 | public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
30 | context.addApplicationListener(getSpringContextEventLoggerListener());
31 | }
32 |
33 | @Override
34 | public boolean equals(Object obj) {
35 | // we need either static singleton ContextCustomizerImpl or equals like this to produce
36 | // equal org.springframework.test.context.MergedContextConfiguration
37 | return getClass() == obj.getClass();
38 | }
39 |
40 | @Override
41 | public int hashCode() {
42 | return 0;
43 | }
44 | }
45 |
46 | private static SpringContextEventLoggerListener getSpringContextEventLoggerListener() {
47 | // overridden logic in demo-test-kit
48 | ServiceLoader loader = ServiceLoader.load(SpringContextEventLoggerListener.class,
49 | SpringContextEventLoggerListenerCustomizerFactory.class.getClassLoader());
50 |
51 | Iterator iterator = loader.iterator();
52 | if (iterator.hasNext()) {
53 | return iterator.next();
54 | } else {
55 | return new SpringContextEventLoggerListener();
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/TestClassExtractor.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext;
2 |
3 | import java.util.function.Function;
4 |
5 | /**
6 | * @author Sergey Chernov
7 | */
8 | public abstract class TestClassExtractor {
9 |
10 | private final ItemType itemType;
11 |
12 | protected TestClassExtractor(ItemType itemType) {
13 | this.itemType = itemType;
14 | }
15 |
16 | public ItemType getItemType() {
17 | return itemType;
18 | }
19 |
20 | public static TestClassExtractor ofClass(Function> testClassExtractor) {
21 | return new TestClassExtractor(ItemType.TEST_CLASS) {
22 | @Override
23 | public Class> getTestClass(T test) {
24 | return testClassExtractor.apply(test);
25 | }
26 | };
27 | }
28 |
29 | public static TestClassExtractor ofMethod(Function> testClassExtractor) {
30 | return new TestClassExtractor(ItemType.TEST_METHOD) {
31 | @Override
32 | public Class> getTestClass(T test) {
33 | return testClassExtractor.apply(test);
34 | }
35 | };
36 | }
37 |
38 | public abstract Class> getTestClass(T test);
39 |
40 | public enum ItemType {
41 | TEST_CLASS,
42 | TEST_METHOD
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/TestSortResult.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext;
2 |
3 | import static java.util.Collections.emptySet;
4 |
5 | import java.util.Collections;
6 | import java.util.List;
7 | import java.util.Set;
8 |
9 | /**
10 | * @author Sergey Chernov
11 | */
12 | public class TestSortResult {
13 |
14 | private final List>> sortedConfigToTests;
15 | private final Set> nonItClasses;
16 |
17 | TestSortResult(List>> sortedConfigToTests, Set> nonItClasses) {
18 | this.sortedConfigToTests = sortedConfigToTests;
19 | this.nonItClasses = nonItClasses;
20 | }
21 |
22 | public static TestSortResult singletonList(Class> testClass) {
23 | // This single test is either integration or not, it will be checked only in case if it's integration.
24 | // So we can skip IntegrationTestFilter
25 | return new TestSortResult(Collections.singletonList(Collections.singletonList(testClass)), emptySet());
26 | }
27 |
28 | public List>> getSortedConfigToTests() {
29 | return sortedConfigToTests;
30 | }
31 |
32 | public Set> getNonItClasses() {
33 | return nonItClasses;
34 | }
35 |
36 | @Override
37 | public String toString() {
38 | return "TestSortResult{" +
39 | "sortedConfigToTests=" + sortedConfigToTests +
40 | ", nonItClasses=" + nonItClasses +
41 | '}';
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/jdbc/CloseableDelegatingDataSource.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext.jdbc;
2 |
3 | import java.io.Closeable;
4 | import java.io.IOException;
5 | import javax.sql.DataSource;
6 | import org.springframework.jdbc.datasource.DelegatingDataSource;
7 |
8 | /**
9 | * Closeable DataSource which delegates close call to target
10 | *
11 | * @author Sergey Chernov
12 | */
13 | public class CloseableDelegatingDataSource extends DelegatingDataSource implements Closeable {
14 |
15 | public CloseableDelegatingDataSource() {
16 | // for lazy initialization
17 | }
18 |
19 | public CloseableDelegatingDataSource(DataSource targetDataSource) {
20 | // eagerly initialized DataSource should be closeable
21 | super(requireCloseable(targetDataSource));
22 | }
23 |
24 | private static DataSource requireCloseable(DataSource targetDataSource) {
25 | if (targetDataSource == null) {
26 | throw new IllegalArgumentException("targetDataSource is null");
27 | }
28 | if (!(targetDataSource instanceof Closeable)) {
29 | throw new IllegalArgumentException("targetDataSource is not closeable");
30 | }
31 | return targetDataSource;
32 | }
33 |
34 | @Override
35 | public void close() throws IOException {
36 | DataSource targetDataSource = getTargetDataSource();
37 | // condition may be false for lazily initialized DataSource
38 | if (targetDataSource instanceof Closeable) {
39 | ((Closeable) targetDataSource).close();
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/jdbc/GuavaSuppliers.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext.jdbc;
2 |
3 | import java.util.Objects;
4 | import java.util.function.Supplier;
5 | import org.springframework.lang.Nullable;
6 |
7 | /**
8 | * Original code:
9 | * Guava
10 | * Suppliers.java
11 | */
12 | final class GuavaSuppliers {
13 |
14 | /**
15 | * Returns a supplier which caches the instance retrieved during the first call to {@code get()}
16 | * and returns that value on subsequent calls to {@code get()}. See: memoization
18 | *
19 | * The returned supplier is thread-safe. The delegate's {@code get()} method will be invoked at
20 | * most once unless the underlying {@code get()} throws an exception.
21 | *
22 | *
When the underlying delegate throws an exception then this memoizing supplier will keep
23 | * delegating calls until it returns valid data.
24 | *
25 | *
If {@code delegate} is an instance created by an earlier call to {@code memoize}, it is
26 | * returned directly.
27 | */
28 | static Supplier memoize(Supplier delegate) {
29 | if (delegate instanceof NonSerializableMemoizingSupplier) {
30 | return delegate;
31 | }
32 | return new NonSerializableMemoizingSupplier<>(delegate);
33 | }
34 |
35 | static class NonSerializableMemoizingSupplier implements Supplier {
36 | volatile Supplier delegate;
37 | volatile boolean initialized;
38 | // "value" does not need to be volatile; visibility piggy-backs
39 | // on volatile read of "initialized".
40 | @Nullable
41 | T value;
42 |
43 | NonSerializableMemoizingSupplier(Supplier delegate) {
44 | this.delegate = Objects.requireNonNull(delegate);
45 | }
46 |
47 | @Override
48 | public T get() {
49 | // A 2-field variant of Double Checked Locking.
50 | if (!initialized) {
51 | synchronized (this) {
52 | if (!initialized) {
53 | T t = delegate.get();
54 | value = t;
55 | initialized = true;
56 | // Release the delegate to GC.
57 | delegate = null;
58 | return t;
59 | }
60 | }
61 | }
62 | return value;
63 | }
64 |
65 | @Override
66 | public String toString() {
67 | Supplier delegate = this.delegate;
68 | return "Suppliers.memoize("
69 | + (delegate == null ? "" : delegate)
70 | + ")";
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/jdbc/LateInitDataSource.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext.jdbc;
2 |
3 | import java.util.function.Supplier;
4 | import javax.sql.DataSource;
5 | import org.springframework.lang.Nullable;
6 |
7 | /**
8 | * DataSource decorator which resolves delegate DataSource on first demand.
9 | *
10 | * This can be an optimization for DataSources created beyond TestContainers-managed Docker containers: the container is
11 | * only started if needed.
12 | *
13 | * Usage example:
14 | *
{@code
15 | * @Bean
16 | * public DataSource dataSource(PostgreSQLContainer> container) {
17 | * // lazy late initialization - the JDBC url is not known yet, because container is not running
18 | * return new LateInitDataSource(() -> {
19 | * LOGGER.info("Late initialization data source docker container {}", container);
20 | * // start only on demand
21 | * container.start();
22 | * return createHikariDataSourceForContainer(container);
23 | * });
24 | * }
25 | * }
26 | * This DataSource delegates close call to the target.
27 | *
28 | * @author Sergey Chernov
29 | */
30 | public class LateInitDataSource extends CloseableDelegatingDataSource {
31 |
32 | @Nullable
33 | private final String name;
34 | private final Supplier dataSourceSupplier;
35 |
36 | public LateInitDataSource(Supplier dataSourceSupplier) {
37 | this(null, dataSourceSupplier);
38 | }
39 |
40 | public LateInitDataSource(String name, Supplier dataSourceSupplier) {
41 | this.name = name;
42 | this.dataSourceSupplier = GuavaSuppliers.memoize(() -> {
43 | DataSource dataSource = dataSourceSupplier.get();
44 | setTargetDataSource(dataSource);
45 | return dataSource;
46 | });
47 | }
48 |
49 | @Override
50 | public void afterPropertiesSet() {
51 | // no op to skip getTargetDataSource setup
52 | }
53 |
54 | @Override
55 | protected DataSource obtainTargetDataSource() {
56 | return dataSourceSupplier.get();
57 | }
58 |
59 | @Override
60 | public String toString() {
61 | return "LateInitDataSource{" +
62 | (name == null ? "" : "name='" + name + '\'') +
63 | ", delegate=" + getTargetDataSource() +
64 | '}';
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/junit4/AbstractJUnit4SpringIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext.junit4;
2 |
3 | import com.github.seregamorph.testsmartcontext.SmartDirtiesContextTestExecutionListener;
4 | import org.springframework.test.context.TestExecutionListeners;
5 | import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
6 |
7 | /**
8 | * Base class for JUnit 4 integration tests that create spring context. Supports
9 | * {@link SmartDirtiesContextTestExecutionListener} semantics to optimize IT suite execution.
10 | *
11 | * @author Sergey Chernov
12 | * @see SmartDirtiesContextTestExecutionListener
13 | * @deprecated support of JUnit 4 will be removed in 1.0 release
14 | */
15 | @Deprecated
16 | @TestExecutionListeners(listeners = {
17 | SmartDirtiesContextTestExecutionListener.class,
18 | }, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
19 | public abstract class AbstractJUnit4SpringIntegrationTest extends AbstractJUnit4SpringContextTests {
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/jupiter/AbstractJUnitSpringIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext.jupiter;
2 |
3 | import com.github.seregamorph.testsmartcontext.SmartDirtiesContextTestExecutionListener;
4 | import org.junit.jupiter.api.extension.ExtendWith;
5 | import org.springframework.test.context.TestExecutionListeners;
6 | import org.springframework.test.context.junit.jupiter.SpringExtension;
7 |
8 | /**
9 | * Base class for JUnit 5 Jupiter integration tests that create spring context. Supports
10 | * {@link SmartDirtiesContextTestExecutionListener} semantics to optimize IT suite execution.
11 | *
12 | * @author Sergey Chernov
13 | * @see SmartDirtiesContextTestExecutionListener
14 | */
15 | @TestExecutionListeners(listeners = {
16 | SmartDirtiesContextTestExecutionListener.class,
17 | }, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
18 | @ExtendWith(SpringExtension.class)
19 | public abstract class AbstractJUnitSpringIntegrationTest {
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/jupiter/SmartDirtiesClassOrderer.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext.jupiter;
2 |
3 | import com.github.seregamorph.testsmartcontext.SmartDirtiesTestsSorter;
4 | import com.github.seregamorph.testsmartcontext.SmartDirtiesTestsSupport;
5 | import com.github.seregamorph.testsmartcontext.TestClassExtractor;
6 | import com.github.seregamorph.testsmartcontext.TestSortResult;
7 | import java.util.LinkedHashSet;
8 | import java.util.List;
9 | import java.util.Set;
10 | import org.junit.jupiter.api.ClassDescriptor;
11 | import org.junit.jupiter.api.ClassOrderer;
12 | import org.junit.jupiter.api.ClassOrdererContext;
13 | import org.junit.jupiter.api.Nested;
14 | import org.springframework.core.annotation.AnnotationUtils;
15 |
16 | /**
17 | * Auto-discovered Jupiter {@link ClassOrderer} which reorders and groups the integration test classes per their
18 | * configuration. Also stores information about last integration class per configuration, which is used by
19 | * {@link com.github.seregamorph.testsmartcontext.SmartDirtiesContextTestExecutionListener}.
20 | *
21 | * For TestNG test classes - see {@link com.github.seregamorph.testsmartcontext.testng.SmartDirtiesSuiteListener}, for
22 | * JUnit 4 test classes - see {@link com.github.seregamorph.testsmartcontext.SmartDirtiesPostDiscoveryFilter}.
23 | *
24 | * @author Sergey Chernov
25 | */
26 | public class SmartDirtiesClassOrderer extends SmartDirtiesTestsSupport implements ClassOrderer {
27 |
28 | @Override
29 | public void orderClasses(ClassOrdererContext context) {
30 | List extends ClassDescriptor> classDescriptors = context.getClassDescriptors();
31 | if (classDescriptors.isEmpty()) {
32 | return;
33 | }
34 |
35 | // Special notes: Maven has different behavior in comparison with IDEA and Gradle, it calls orderClasses method
36 | // for each test class with a single element of classDescriptors list. That's why we need to handle single-size
37 | // list separately.
38 |
39 | // If Jupiter Test class has @Nested inner classes, for each of them (if there is only one inner class)
40 | // orderClasses will be called
41 |
42 | Set> uniqueClasses = new LinkedHashSet<>();
43 | for (ClassDescriptor classDescriptor : classDescriptors) {
44 | Class> testClass = classDescriptor.getTestClass();
45 | if (isInnerClass(testClass)) {
46 | if (!uniqueClasses.isEmpty()) {
47 | // this should not happen, they should be never mixed in one call
48 | throw new IllegalStateException("Unexpected mix of inner " + testClass + " and " + uniqueClasses);
49 | }
50 | Nested nested = AnnotationUtils.getAnnotation(testClass, Nested.class);
51 | if (nested == null) {
52 | // this should not happen
53 | throw new IllegalStateException("Missing @Nested annotation for inner " + testClass);
54 | }
55 | // implementation notice: if the exception is thrown from this block, it does not break the
56 | // test execution as it's suppressed in
57 | // org.junit.jupiter.engine.discovery.AbstractOrderingVisitor.doWithMatchingDescriptor
58 | // So this validation will be repeated at BeforeClass
59 | verifyInnerClass(testClass);
60 | } else {
61 | // regular test class
62 | uniqueClasses.add(testClass);
63 | }
64 | }
65 |
66 | if (uniqueClasses.isEmpty()) {
67 | // All are internal (@Nested), we do not reorder them.
68 | // The enclosing classes are already in the SmartDirtiesTestsHolder from previous call
69 | if (SmartDirtiesTestsSupport.classOrderStateMapSize(ENGINE_JUNIT_JUPITER) == 0) {
70 | throw new IllegalStateException("orderClasses is called with inner classes list " + classDescriptors
71 | + " before being called with enclosing class list");
72 | }
73 | return;
74 | }
75 |
76 | if (uniqueClasses.size() == 1) {
77 | // This filter is executed several times during discover and execute phases and
78 | // it's not possible to distinguish them here. Sometimes per single test is sent as argument,
79 | // sometimes - the whole suite. If it's a suite more than 1, we can save it and never update.
80 | // If it's 1 - we should also distinguish single test execution.
81 | if (SmartDirtiesTestsSupport.classOrderStateMapSize(ENGINE_JUNIT_JUPITER) <= 1) {
82 | Class> testClass = classDescriptors.get(0).getTestClass();
83 | SmartDirtiesTestsSupport.setTestClassesLists(ENGINE_JUNIT_JUPITER,
84 | TestSortResult.singletonList(testClass));
85 | }
86 | return;
87 | }
88 |
89 | SmartDirtiesTestsSorter sorter = SmartDirtiesTestsSorter.getInstance();
90 | TestSortResult testClassesLists;
91 | try {
92 | testClassesLists = sorter.sort(classDescriptors, TestClassExtractor.ofClass(ClassDescriptor::getTestClass));
93 | } catch (Throwable e) {
94 | SmartDirtiesTestsSupport.setFailureCause(e);
95 | throw e;
96 | }
97 | SmartDirtiesTestsSupport.setTestClassesLists(ENGINE_JUNIT_JUPITER, testClassesLists);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/testng/AbstractTestNGSpringIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext.testng;
2 |
3 | import com.github.seregamorph.testsmartcontext.SmartDirtiesContextTestExecutionListener;
4 | import org.springframework.test.context.TestExecutionListeners;
5 | import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
6 |
7 | /**
8 | * Base class for TestNG integration tests that create spring context. Supports
9 | * {@link SmartDirtiesContextTestExecutionListener} semantics to optimize IT suite execution.
10 | *
11 | * @author Sergey Chernov
12 | * @see SmartDirtiesContextTestExecutionListener
13 | */
14 | @TestExecutionListeners(listeners = {
15 | SmartDirtiesContextTestExecutionListener.class,
16 | }, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
17 | public abstract class AbstractTestNGSpringIntegrationTest extends AbstractTestNGSpringContextTests {
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/testng/SmartDirtiesSuiteListener.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext.testng;
2 |
3 | import com.github.seregamorph.testsmartcontext.SmartDirtiesContextTestExecutionListener;
4 | import com.github.seregamorph.testsmartcontext.SmartDirtiesTestsSorter;
5 | import com.github.seregamorph.testsmartcontext.SmartDirtiesTestsSupport;
6 | import com.github.seregamorph.testsmartcontext.TestClassExtractor;
7 | import com.github.seregamorph.testsmartcontext.TestSortResult;
8 | import java.util.LinkedHashSet;
9 | import java.util.List;
10 | import java.util.Set;
11 | import org.testng.IAlterSuiteListener;
12 | import org.testng.IMethodInstance;
13 | import org.testng.IMethodInterceptor;
14 | import org.testng.ITestContext;
15 | import org.testng.internal.RuntimeBehavior;
16 | import org.testng.xml.XmlSuite;
17 |
18 | /**
19 | * See description in {@link SmartDirtiesContextTestExecutionListener}.
20 | *
21 | * Reorders TestNG test classes in suite grouping ITs with the same context configuration to minimize number of parallel
22 | * existing contexts.
23 | *
24 | * For Jupiter test classes - see {@link com.github.seregamorph.testsmartcontext.jupiter.SmartDirtiesClassOrderer}, for
25 | * JUnit 4 test classes - see {@link com.github.seregamorph.testsmartcontext.SmartDirtiesPostDiscoveryFilter}.
26 | *
27 | * @author Sergey Chernov
28 | */
29 | @SuppressWarnings("CodeBlock2Expr")
30 | public class SmartDirtiesSuiteListener extends SmartDirtiesTestsSupport
31 | implements IAlterSuiteListener, IMethodInterceptor {
32 |
33 | @Override
34 | public void alter(List suites) {
35 | // dryRun is only true when called via junit5 testng-engine on discovery phase, there will be subsequent
36 | // call of this method with dryRun=false on execution phase
37 | if (RuntimeBehavior.isDryRun()) {
38 | // the list of test classes is wrong, listener is called per each class as single in suite
39 | return;
40 | }
41 | // TestNG needs it (otherwise reorders back to default alphabetical order)
42 | suites.forEach(suite -> {
43 | suite.getTests().forEach(xmlTest -> {
44 | xmlTest.setPreserveOrder(false);
45 | });
46 | });
47 | }
48 |
49 | @Override
50 | public List intercept(List methods, ITestContext context) {
51 | // dryRun is only true when called via junit5 testng-engine on discovery phase, there will be subsequent
52 | // call of this method with dryRun=false on execution phase
53 | if (RuntimeBehavior.isDryRun()) {
54 | // the list of test classes is wrong, listener is called per each class as single in suite
55 | return methods;
56 | }
57 |
58 | Set> uniqueClasses = new LinkedHashSet<>();
59 | for (IMethodInstance method : methods) {
60 | Class> testClass = getTestClass(method);
61 | uniqueClasses.add(testClass);
62 | }
63 |
64 | if (uniqueClasses.size() == 1) {
65 | Class> testClass = getTestClass(methods.get(0));
66 | SmartDirtiesTestsSupport.setTestClassesLists(ENGINE_TESTNG, TestSortResult.singletonList(testClass));
67 | return methods;
68 | }
69 |
70 | // this intercept method is executed by TestNG before running the suite (both IDEA and maven)
71 | SmartDirtiesTestsSorter sorter = SmartDirtiesTestsSorter.getInstance();
72 | // Do not store the failure as if it throws, TestNG will fail the suite
73 | // (both pure TestNG and JUnit 5 testng-engine)
74 | TestSortResult testClassesLists = sorter.sort(methods,
75 | TestClassExtractor.ofMethod(SmartDirtiesSuiteListener::getTestClass));
76 |
77 | SmartDirtiesTestsSupport.setTestClassesLists(ENGINE_TESTNG, testClassesLists);
78 |
79 | return methods;
80 | }
81 |
82 | private static Class> getTestClass(IMethodInstance methodInstance) {
83 | return methodInstance.getMethod().getTestClass().getRealClass();
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/java/org/springframework/test/context/BootstrapUtilsHelper.java:
--------------------------------------------------------------------------------
1 | package org.springframework.test.context;
2 |
3 | /**
4 | * Accessor of spring-boot package visible utility
5 | *
6 | * @author Sergey Chernov
7 | */
8 | public final class BootstrapUtilsHelper {
9 |
10 | public static TestContextBootstrapper resolveTestContextBootstrapper(Class> testClass) {
11 | // this utility becomes public since spring 6, but for spring 5 we call it from package-private accessor
12 | return BootstrapUtils.resolveTestContextBootstrapper(BootstrapUtils.createBootstrapContext(testClass));
13 | }
14 |
15 | private BootstrapUtilsHelper() {
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/resources/META-INF/services/org.junit.platform.launcher.PostDiscoveryFilter:
--------------------------------------------------------------------------------
1 | com.github.seregamorph.testsmartcontext.SmartDirtiesPostDiscoveryFilter
2 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/resources/META-INF/services/org.testng.ITestNGListener:
--------------------------------------------------------------------------------
1 | com.github.seregamorph.testsmartcontext.testng.SmartDirtiesSuiteListener
2 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/resources/META-INF/spring.factories:
--------------------------------------------------------------------------------
1 | org.springframework.test.context.TestExecutionListener = \
2 | com.github.seregamorph.testsmartcontext.SmartDirtiesContextTestExecutionListener
3 |
4 | org.springframework.test.context.ContextCustomizerFactory = \
5 | com.github.seregamorph.testsmartcontext.SpringContextEventLoggerListenerCustomizerFactory
6 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/main/resources/junit-platform.properties:
--------------------------------------------------------------------------------
1 | junit.jupiter.testclass.order.default = com.github.seregamorph.testsmartcontext.jupiter.SmartDirtiesClassOrderer
2 |
--------------------------------------------------------------------------------
/spring-test-smart-context/src/test/java/com/github/seregamorph/testsmartcontext/SpringContextEventLoggerListenerTest.java:
--------------------------------------------------------------------------------
1 | package com.github.seregamorph.testsmartcontext;
2 |
3 | import org.junit.jupiter.api.Assertions;
4 | import org.junit.jupiter.api.Test;
5 |
6 | class SpringContextEventLoggerListenerTest {
7 |
8 | @Test
9 | public void shouldFormatNanos() {
10 | Assertions.assertEquals("120.000s",
11 | SpringContextEventLoggerListener.formatNanos(120000000000L));
12 | }
13 | }
14 |
--------------------------------------------------------------------------------