Browse Source
Add support for a `spring.testcontainers.startup` property that can be set to "sequential" or "parallel" to change how containers are started. Closes gh-37073pull/37905/head
11 changed files with 293 additions and 16 deletions
@ -0,0 +1,94 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2012-2023 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 |
||||||
|
* |
||||||
|
* https://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.boot.testcontainers.lifecycle; |
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
|
||||||
|
import org.testcontainers.lifecycle.Startable; |
||||||
|
import org.testcontainers.lifecycle.Startables; |
||||||
|
|
||||||
|
import org.springframework.core.env.ConfigurableEnvironment; |
||||||
|
import org.springframework.core.env.Environment; |
||||||
|
|
||||||
|
/** |
||||||
|
* Testcontainers startup strategies. The strategy to use can be configured in the Spring |
||||||
|
* {@link Environment} with a {@value #PROPERTY} property. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
* @since 3.2.0 |
||||||
|
*/ |
||||||
|
public enum TestcontainersStartup { |
||||||
|
|
||||||
|
/** |
||||||
|
* Startup containers sequentially. |
||||||
|
*/ |
||||||
|
SEQUENTIAL { |
||||||
|
|
||||||
|
@Override |
||||||
|
void start(Collection<? extends Startable> startables) { |
||||||
|
startables.forEach(Startable::start); |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* Startup containers in parallel. |
||||||
|
*/ |
||||||
|
PARALLEL { |
||||||
|
|
||||||
|
@Override |
||||||
|
void start(Collection<? extends Startable> startables) { |
||||||
|
Startables.deepStart(startables).join(); |
||||||
|
} |
||||||
|
|
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* The {@link Environment} property used to change the {@link TestcontainersStartup} |
||||||
|
* strategy. |
||||||
|
*/ |
||||||
|
public static final String PROPERTY = "spring.testcontainers.startup"; |
||||||
|
|
||||||
|
abstract void start(Collection<? extends Startable> startables); |
||||||
|
|
||||||
|
static TestcontainersStartup get(ConfigurableEnvironment environment) { |
||||||
|
return get((environment != null) ? environment.getProperty(PROPERTY) : null); |
||||||
|
} |
||||||
|
|
||||||
|
private static TestcontainersStartup get(String value) { |
||||||
|
if (value == null) { |
||||||
|
return SEQUENTIAL; |
||||||
|
} |
||||||
|
String canonicalName = getCanonicalName(value); |
||||||
|
for (TestcontainersStartup candidate : values()) { |
||||||
|
if (candidate.name().equalsIgnoreCase(canonicalName)) { |
||||||
|
return candidate; |
||||||
|
} |
||||||
|
} |
||||||
|
throw new IllegalArgumentException("Unknown '%s' property value '%s'".formatted(PROPERTY, value)); |
||||||
|
} |
||||||
|
|
||||||
|
private static String getCanonicalName(String name) { |
||||||
|
StringBuilder canonicalName = new StringBuilder(name.length()); |
||||||
|
name.chars() |
||||||
|
.filter(Character::isLetterOrDigit) |
||||||
|
.map(Character::toLowerCase) |
||||||
|
.forEach((c) -> canonicalName.append((char) c)); |
||||||
|
return canonicalName.toString(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
{ |
||||||
|
"properties": [ |
||||||
|
{ |
||||||
|
"name": "spring.testcontainers.startup", |
||||||
|
"type": "org.springframework.boot.testcontainers.lifecycle.TestcontainersStartup", |
||||||
|
"description": "Testcontainers startup modes.", |
||||||
|
"defaultValue": "sequential" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
@ -0,0 +1,122 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2012-2023 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 |
||||||
|
* |
||||||
|
* https://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.boot.testcontainers.lifecycle; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.testcontainers.lifecycle.Startable; |
||||||
|
|
||||||
|
import org.springframework.mock.env.MockEnvironment; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link TestcontainersStartup}. |
||||||
|
* |
||||||
|
* @author Phillip Webb |
||||||
|
*/ |
||||||
|
class TestcontainersStartupTests { |
||||||
|
|
||||||
|
private static final String PROPERTY = TestcontainersStartup.PROPERTY; |
||||||
|
|
||||||
|
private final AtomicInteger counter = new AtomicInteger(); |
||||||
|
|
||||||
|
@Test |
||||||
|
void startWhenSquentialStartsSequentially() { |
||||||
|
List<TestStartable> startables = createTestStartables(100); |
||||||
|
TestcontainersStartup.SEQUENTIAL.start(startables); |
||||||
|
for (int i = 0; i < startables.size(); i++) { |
||||||
|
assertThat(startables.get(i).getIndex()).isEqualTo(i); |
||||||
|
assertThat(startables.get(i).getThreadName()).isEqualTo(Thread.currentThread().getName()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void startWhenParallelStartsInParallel() { |
||||||
|
List<TestStartable> startables = createTestStartables(100); |
||||||
|
TestcontainersStartup.PARALLEL.start(startables); |
||||||
|
assertThat(startables.stream().map(TestStartable::getThreadName)).hasSizeGreaterThan(1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void getWhenNoPropertyReturnsDefault() { |
||||||
|
MockEnvironment environment = new MockEnvironment(); |
||||||
|
assertThat(TestcontainersStartup.get(environment)).isEqualTo(TestcontainersStartup.SEQUENTIAL); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void getWhenPropertyReturnsBasedOnValue() { |
||||||
|
MockEnvironment environment = new MockEnvironment(); |
||||||
|
assertThat(TestcontainersStartup.get(environment.withProperty(PROPERTY, "SEQUENTIAL"))) |
||||||
|
.isEqualTo(TestcontainersStartup.SEQUENTIAL); |
||||||
|
assertThat(TestcontainersStartup.get(environment.withProperty(PROPERTY, "sequential"))) |
||||||
|
.isEqualTo(TestcontainersStartup.SEQUENTIAL); |
||||||
|
assertThat(TestcontainersStartup.get(environment.withProperty(PROPERTY, "SEQuenTIaL"))) |
||||||
|
.isEqualTo(TestcontainersStartup.SEQUENTIAL); |
||||||
|
assertThat(TestcontainersStartup.get(environment.withProperty(PROPERTY, "S-E-Q-U-E-N-T-I-A-L"))) |
||||||
|
.isEqualTo(TestcontainersStartup.SEQUENTIAL); |
||||||
|
assertThat(TestcontainersStartup.get(environment.withProperty(PROPERTY, "parallel"))) |
||||||
|
.isEqualTo(TestcontainersStartup.PARALLEL); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void getWhenUnknownPropertyThrowsException() { |
||||||
|
MockEnvironment environment = new MockEnvironment(); |
||||||
|
assertThatIllegalArgumentException() |
||||||
|
.isThrownBy(() -> TestcontainersStartup.get(environment.withProperty(PROPERTY, "bad"))) |
||||||
|
.withMessage("Unknown 'spring.testcontainers.startup' property value 'bad'"); |
||||||
|
} |
||||||
|
|
||||||
|
private List<TestStartable> createTestStartables(int size) { |
||||||
|
List<TestStartable> testStartables = new ArrayList<>(size); |
||||||
|
for (int i = 0; i < size; i++) { |
||||||
|
testStartables.add(new TestStartable()); |
||||||
|
} |
||||||
|
return testStartables; |
||||||
|
} |
||||||
|
|
||||||
|
private class TestStartable implements Startable { |
||||||
|
|
||||||
|
private int index; |
||||||
|
|
||||||
|
private String threadName; |
||||||
|
|
||||||
|
@Override |
||||||
|
public void start() { |
||||||
|
this.index = TestcontainersStartupTests.this.counter.getAndIncrement(); |
||||||
|
this.threadName = Thread.currentThread().getName(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void stop() { |
||||||
|
} |
||||||
|
|
||||||
|
int getIndex() { |
||||||
|
return this.index; |
||||||
|
} |
||||||
|
|
||||||
|
String getThreadName() { |
||||||
|
return this.threadName; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue