All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.wl4g.infra.common.tests.integration.manager.AbstractITContainerManager Maven / Gradle / Ivy

There is a newer version: 3.1.72
Show newest version
/**
 * Copyright (C) 2023 ~ 2035 the original authors WL4G (James Wong).
 * 

* 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 com.wl4g.infra.common.tests.integration.manager; import ch.qos.logback.classic.Level; import com.google.common.annotations.VisibleForTesting; import com.wl4g.infra.common.cli.ProcessUtils; import com.wl4g.infra.common.net.InetUtils; import com.wl4g.infra.common.reflect.ReflectionUtils2; import com.wl4g.infra.common.tests.integration.mock.IDataMocker; import lombok.Getter; import org.apache.commons.lang3.ClassUtils; import org.junit.jupiter.api.Test; import org.junit.platform.commons.util.ReflectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.dockerclient.EnvironmentAndSystemPropertyClientProviderStrategy; import org.testcontainers.lifecycle.Startable; import org.testcontainers.utility.ResourceReaper; import org.testcontainers.utility.TestcontainersConfiguration; import javax.annotation.Nullable; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.wl4g.infra.common.collection.CollectionUtils2.safeArrayToList; import static com.wl4g.infra.common.collection.CollectionUtils2.safeList; import static com.wl4g.infra.common.lang.EnvironmentUtil.getIntProperty; import static com.wl4g.infra.common.lang.EnvironmentUtil.getStringProperty; import static com.wl4g.infra.common.reflect.ReflectionUtils2.findFieldNullable; import static com.wl4g.infra.common.reflect.ReflectionUtils2.getField; import static java.lang.String.format; import static java.lang.System.*; import static java.util.Collections.unmodifiableMap; import static java.util.Objects.nonNull; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; import static org.apache.commons.lang3.StringUtils.*; import static org.apache.commons.lang3.SystemUtils.IS_OS_MAC; import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS; import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage; /** * The {@link AbstractITContainerManager} * * @author James Wong * @since v3.1 **/ //@Testcontainers @SuppressWarnings({"rawtypes", "unchecked", "deprecation", "unused"}) public abstract class AbstractITContainerManager implements Closeable { // The assertion operation timeout(seconds) public static final int IT_START_CONTAINERS_TIMEOUT = getIntProperty("IT_START_CONTAINERS_TIMEOUT", 600); public static final int IT_DATA_MOCKERS_TIMEOUT = getIntProperty("IT_DATA_MOCKERS_TIMEOUT", 300); // IT docker daemon properties definitions. private static final int DOCKER_DAEMON_PORT = getIntProperty("IT_DOCKER_DAEMON_PORT", 2375); private static @Getter String dockerDaemonVmIp; private static @Getter String localHostIp; // IT runtime properties definitions. // see:https://java.testcontainers.org/modules/kafka/#example private final Map mwContainers = new ConcurrentHashMap<>(); private CountDownLatch mwContainersStartedLatch; private final Map dataMockers = new ConcurrentHashMap<>(); private CountDownLatch dataMockersFinishedLatch; // // Notice: That using @RunWith/@SpringBootTest to start the application cannot control the startup // sequence (e.g after the kafka container is started), so it can only be controlled by manual startup. // //protected static final Map> extraEnvSupplier = synchronizedMap(new HashMap<>()); // ----- Integration Test Global Initialization Methods. ----- static { ((ch.qos.logback.classic.Logger) LoggerFactory .getLogger(Logger.ROOT_LOGGER_NAME)) .setLevel(Level.INFO); ((ch.qos.logback.classic.Logger) LoggerFactory .getLogger("org.testcontainers")) .setLevel(Level.INFO); ((ch.qos.logback.classic.Logger) LoggerFactory .getLogger("com.github.dockerjava")) .setLevel(Level.INFO); ((ch.qos.logback.classic.Logger) LoggerFactory .getLogger("com.github.dockerjava.api.command.PullImageResultCallback")) .setLevel(Level.DEBUG); setupITLocalHostIp(); setupITDockerHost(); setupRyukContainerIfNeed(); } @VisibleForTesting static void setupITLocalHostIp() { try (InetUtils helper = new InetUtils(new InetUtils.InetUtilsProperties())) { localHostIp = helper.findFirstNonLoopbackHostInfo().getIpAddress(); } } /** * Set Up to IT docker host. (for compatibility with local multipass VM in docker) */ @VisibleForTesting static void setupITDockerHost() { String itDockerHost = getenv("IT_DOCKER_HOST"); if (isBlank(itDockerHost)) { // Detect for docker daemon VM IP in multipass(MacOS). if (IS_OS_MAC) { if (new File("/var/run/docker.sock").exists()) { itDockerHost = "unix:///var/run/docker.sock"; } else { try { final String dockerDaemonVmIP = ProcessUtils .execSimpleString("[ $(command -v multipass) ] && multipass info docker | grep -i IPv4 | awk '{print $2}' || echo ''"); out.printf(">>> [MacOS] Found local multipass(macos) VM for docker IP: %s%n", dockerDaemonVmIP); if (!isBlank(dockerDaemonVmIP)) { itDockerHost = dockerDaemonVmIP; } } catch (Throwable ex) { err.printf(">>> [MacOS] Unable to detect local multipass VM for docker. reason: %s%n", ex.getMessage()); } try { // 1. OrbStack 会创建如 bridge101: 2层接口, 且为容器自动映射了 DNS // 即如: docker run --name=minio1 --net=host minio/minio 的容器可直接 curl -I localhost:9900 访问 // 或: curl -I minio1.orb.local:9900 访问 // see:https://docs.orbstack.dev/docker/network#host-networking // 2. 启用 OrbStack 内嵌 docker 的 2375 tcp 端口. (~/.orbstack/config/docker.json) // see:https://docs.orbstack.dev/docker/#engine-config final String dockerDaemonVmIP = ProcessUtils .execSimpleString("[ $(command -v orbctl) ] && echo '127.0.0.1' || echo ''"); out.printf(">>> [MacOS] Found local OrbStack(macos) VM for docker IP: %s%n", dockerDaemonVmIP); if (!isBlank(dockerDaemonVmIP)) { itDockerHost = dockerDaemonVmIP; } } catch (Throwable ex) { err.printf(">>> [MacOS] Unable to detect local OrbStack VM for docker. reason: %s%n", ex.getMessage()); } } } // Detect for docker daemon VM IP in multipass(Windows). else if (IS_OS_WINDOWS) { if (new File("\\\\.\\pipe\\docker_engine").exists()) { itDockerHost = "npipe:////./pipe/docker_engine"; } else { try { // call to windows multipass command find str info docker final String dockerDaemonVmIP = ProcessUtils .execSimpleString("cmd /c \"(if exist %SystemRoot%\\System32\\multipass.exe (multipass info docker | findstr /i IPv4 | awk \"{print $2}\") else (echo.))\""); out.printf(">>> [Windows] Found local multipass(windows) VM for docker IP: %s%n", dockerDaemonVmIP); if (!isBlank(dockerDaemonVmIP)) { itDockerHost = dockerDaemonVmIP; } } catch (Throwable ex) { err.printf(">>> [Windows] Unable to detect local multipass VM for docker. reason: %s%n", ex.getMessage()); } } } } if (isNotBlank(itDockerHost)) { // CleanUp the line feed. Pattern regex = Pattern.compile("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$"); Matcher matcher = regex.matcher(itDockerHost); if (matcher.find()) { itDockerHost = matcher.group(); } dockerDaemonVmIp = itDockerHost; // see:org.testcontainers.utility.TestcontainersConfiguration#getDockerClientStrategyClassName() TestcontainersConfiguration.getInstance().updateGlobalConfig("docker.client.strategy", EnvironmentAndSystemPropertyClientProviderStrategy.class.getName()); TestcontainersConfiguration.getInstance() .updateGlobalConfig("docker.host", format("tcp://%s:%s", itDockerHost, DOCKER_DAEMON_PORT)); } } /** * {@link org.testcontainers.DockerClientFactory#client()} * {@link org.testcontainers.utility.ResourceReaper#instance()} * {@link org.testcontainers.utility.ResourceReaper#init()} * {@link org.testcontainers.utility.RyukResourceReaper#ryukContainer} */ @VisibleForTesting @SuppressWarnings("all") static void setupRyukContainerIfNeed() { try { final ResourceReaper reaper = ResourceReaper.instance(); final Class reaperResourceCls = ClassUtils.getClass("org.testcontainers.utility.RyukResourceReaper"); final Class ryukContainerCls = ClassUtils.getClass("org.testcontainers.utility.RyukContainer"); if (reaperResourceCls.isAssignableFrom(reaper.getClass())) { final Field ryukContainerField = findFieldNullable(reaperResourceCls, "ryukContainer", ryukContainerCls); if (nonNull(ryukContainerField)) { final GenericContainer ryukContainer = getField(ryukContainerField, reaper, true); final String ryukImageName = getStringProperty("IT_RYUK_CONTAINER_IMAGE", "registry.cn-shenzhen.aliyuncs.com/wl4g-k8s/testcontainers_ryuk:0.5.1"); ryukContainer.setDockerImageName(ryukImageName); } } } catch (Exception e) { err.printf("Unable to setup ryuk container, cause by: %s%n", getRootCauseMessage(e)); } } // // Notice: That using @RunWith/@SpringBootTest to start the application cannot control the startup // sequence (e.g after the kafka container is started), so it can only be controlled by manual startup. // //@DynamicPropertySource //static void registerExtraEnvironment(DynamicPropertyRegistry registry) { // extraEnvSupplier.forEach(registry::add); //} // ----- Integration Test Assertion Basic Methods. ----- protected final Logger log = LoggerFactory.getLogger(getClass()); private final AtomicBoolean started = new AtomicBoolean(false); private final AtomicInteger closePending; public AbstractITContainerManager(@NotNull Class testClass) { requireNonNull(testClass, "testClass must not be null"); // Find all methods total in target test class. this.closePending = new AtomicInteger(ReflectionUtils .findMethods(testClass, m -> m.isAnnotationPresent(Test.class) || m.isAnnotationPresent(org.junit.Test.class)).size()); // Check for use is must a static field in test class, so it will be shared by all test cases. safeArrayToList(ReflectionUtils2.getDeclaredFields(testClass)) .stream() .filter(f -> AbstractITContainerManager.class.isAssignableFrom(f.getType())) .forEach(f -> { if (!Modifier.isStatic(f.getModifiers())) { throw new IllegalStateException(String.format("Field %s must be static, so it will be shared by all test cases.", f)); } }); } @Override public void close() throws IOException { close(false); } @SuppressWarnings("all") private void close(boolean force) { // If there are still test methods pending, skip closing. if (!force && closePending.decrementAndGet() > 0) { return; } if (started.compareAndSet(true, false)) { log.info("Shutting down to IT middleware containers ..."); mwContainers.values().forEach(ITGenericContainer::close); log.info("Shutting down to IT data mockers ..."); dataMockers.values().forEach(dataMocker -> { try { dataMocker.close(); } catch (Throwable ex) { throw new IllegalStateException(String.format("Could not shutting down data mocker %s", dataMocker), ex); } }); log.info(">>>>>>>>>> Shutdown for IT containers manager. <<<<<<<<<<"); } } public void start() throws Exception { if (started.compareAndSet(false, true)) { try { log.info(">>>>>>>>>> Initializing for IT containers manager ... <<<<<<<<<<"); startForMwContainers(); startForDataMocks(); } catch (Exception ex) { log.error("Could not start IT containers manager", ex); throw ex; } } } /** * Startup middleware Containers(e.g: zookeeper/kafka/mongodb) */ private void startForMwContainers() throws Exception { log.info("Initializing for IT middleware containers ..."); final Supplier mwContainersStartedLatchSupplier = () -> this.mwContainersStartedLatch; initMwContainers(mwContainersStartedLatchSupplier, mwContainers); this.mwContainersStartedLatch = new CountDownLatch(mwContainers.size()); // Run for middleware containers. log.info("Starting IT middleware containers ..."); mwContainers.forEach((name, container) -> new Thread(() -> { log.info("Starting for IT middleware container: {}", name); container.getContainer().start(); }).start()); if (!mwContainersStartedLatch.await(IT_START_CONTAINERS_TIMEOUT, TimeUnit.SECONDS)) { throw new TimeoutException("Failed to start IT middleware containers. timeout: " + IT_START_CONTAINERS_TIMEOUT + "s"); } log.info("Started for IT middleware containers: " + mwContainers.keySet()); } /** * Startup Data Mockers. */ private void startForDataMocks() throws Exception { log.info("Initializing for IT data mockers ..."); final Supplier dataMockersFinishedLatchSupplier = () -> dataMockersFinishedLatch; initDataMockers(dataMockersFinishedLatchSupplier, dataMockers); this.dataMockersFinishedLatch = new CountDownLatch(dataMockers.size()); log.info("Starting for IT data mockers ..."); this.dataMockers.forEach((name, mocker) -> new Thread(() -> { log.info("Starting IT data mocker: {}", name); mocker.run(); mocker.printStatistics(); }).start()); if (!dataMockersFinishedLatch.await(IT_DATA_MOCKERS_TIMEOUT, TimeUnit.SECONDS)) { throw new TimeoutException("Failed to start IT data mockers. timeout: " + IT_DATA_MOCKERS_TIMEOUT + "s"); } } protected abstract void initMwContainers(@NotNull Supplier startedLatchSupplier, @NotNull Map mwContainers); protected abstract void initDataMockers(@NotNull Supplier finishedLatchSupplier, @NotNull Map dataMockers); // ------ Getting Running Containers Configuration ------ public Map getMwContainers() { return unmodifiableMap(mwContainers); } @SuppressWarnings("unchecked") public T getRequiredContainer(String name) { return (T) requireNonNull(mwContainers.get(name), String.format("Could not get first container for %s", name)); } @SuppressWarnings("unchecked") public T getRequiredDataMocker(String name) { return (T) requireNonNull(dataMockers.get(name), String.format("Could not get data mocker for %s", name)); } @SuppressWarnings("all") public String getServersConnectString(String protocol, String clusterName, int portBindingsIndex) { final ITGenericContainer container = getRequiredContainer(clusterName); final String portBinding = container.getPortBindings().get(portBindingsIndex); return getServersConnectString(protocol, Integer.parseInt(split(portBinding, ":")[0])); } public String getServersConnectString(String protocol, int mappedPort) { final String availableHost = isBlank(dockerDaemonVmIp) ? localHostIp : dockerDaemonVmIp; return String.format("%s%s:%s", protocol, availableHost, mappedPort); } @Getter public static class ITGenericContainer implements Closeable { private final List portBindings; private final GenericContainer container; @SuppressWarnings("all") public ITGenericContainer(@NotEmpty List portBindings, @NotNull GenericContainer container) { portBindings = safeList(portBindings).stream().filter(Objects::nonNull).collect(toList()); this.portBindings = portBindings; this.container = requireNonNull(container, "container must not be null"); } @Override public void close() { container.close(); } public void start() { container.start(); } @SuppressWarnings("all") public ITGenericContainer withDependsOn(@Nullable ITGenericContainer... dependsOn) { this.container.dependsOn(safeArrayToList(dependsOn) .stream() .filter(Objects::nonNull) .map(ITGenericContainer::getContainer) .toArray(Startable[]::new)); return this; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy