diff --git a/framework/fit/java/fit-test/fit-test-framework/src/main/java/modelengine/fitframework/test/domain/listener/MockMvcListener.java b/framework/fit/java/fit-test/fit-test-framework/src/main/java/modelengine/fitframework/test/domain/listener/MockMvcListener.java index ea1c7d93b..5b1e8436d 100644 --- a/framework/fit/java/fit-test/fit-test-framework/src/main/java/modelengine/fitframework/test/domain/listener/MockMvcListener.java +++ b/framework/fit/java/fit-test/fit-test-framework/src/main/java/modelengine/fitframework/test/domain/listener/MockMvcListener.java @@ -38,6 +38,10 @@ public class MockMvcListener implements TestListener { private static final Set DEFAULT_SCAN_PACKAGES = new HashSet<>(Arrays.asList("modelengine.fit.server", "modelengine.fit.http")); + private static final String TIMEOUT_PROPERTY_KEY = "fit.test.mockmvc.startup.timeout"; + private static final long DEFAULT_STARTUP_TIMEOUT = 30_000L; + private static final long MIN_STARTUP_TIMEOUT = 1_000L; + private static final long MAX_STARTUP_TIMEOUT = 600_000L; private final int port; @@ -71,14 +75,60 @@ public void beforeTestClass(TestContext context) { } MockMvc mockMvc = new MockMvc(this.port); context.plugin().container().registry().register(mockMvc); + long timeout = this.getStartupTimeout(); + long startTime = System.currentTimeMillis(); boolean started = this.isStarted(mockMvc); while (!started) { + long elapsed = System.currentTimeMillis() - startTime; + if (elapsed > timeout) { + throw new IllegalStateException(this.buildTimeoutErrorMessage(elapsed, this.port)); + } ThreadUtils.sleep(100); started = this.isStarted(mockMvc); } } - private boolean isStarted(MockMvc mockMvc) { + private long getStartupTimeout() { + String timeoutStr = System.getProperty(TIMEOUT_PROPERTY_KEY); + if (StringUtils.isNotBlank(timeoutStr)) { + try { + long timeout = Long.parseLong(timeoutStr); + if (timeout < MIN_STARTUP_TIMEOUT) { + return DEFAULT_STARTUP_TIMEOUT; + } + if (timeout > MAX_STARTUP_TIMEOUT) { + return MAX_STARTUP_TIMEOUT; + } + return timeout; + } catch (NumberFormatException e) { + return DEFAULT_STARTUP_TIMEOUT; + } + } + return DEFAULT_STARTUP_TIMEOUT; + } + + private String buildTimeoutErrorMessage(long elapsed, int port) { + return StringUtils.format(""" + Mock MVC server failed to start within {0}ms. [port={1}] + + Possible causes: + 1. Port {1} is already in use by another process + 2. Network configuration issues + 3. Server startup is slower than expected in this environment + + Troubleshooting steps: + - Check if port {1} is in use: + * macOS/Linux: lsof -i :{1} + * Windows: netstat -ano | findstr :{1} + - Check server logs for detailed error messages + - If running in a slow environment, increase timeout: + mvn test -D{2}=60000""", + elapsed, + port, + TIMEOUT_PROPERTY_KEY); + } + + protected boolean isStarted(MockMvc mockMvc) { MockRequestBuilder builder = MockMvcRequestBuilders.get(MockController.PATH).responseType(String.class); try (HttpClassicClientResponse response = mockMvc.perform(builder)) { String content = response.textEntity() @@ -91,4 +141,4 @@ private boolean isStarted(MockMvc mockMvc) { return false; } } -} \ No newline at end of file +} diff --git a/framework/fit/java/fit-test/fit-test-framework/src/test/java/modelengine/fitframework/test/domain/listener/MockMvcListenerTest.java b/framework/fit/java/fit-test/fit-test-framework/src/test/java/modelengine/fitframework/test/domain/listener/MockMvcListenerTest.java new file mode 100644 index 000000000..c38f3d496 --- /dev/null +++ b/framework/fit/java/fit-test/fit-test-framework/src/test/java/modelengine/fitframework/test/domain/listener/MockMvcListenerTest.java @@ -0,0 +1,460 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2026 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fitframework.test.domain.listener; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import modelengine.fitframework.conf.Config; +import modelengine.fitframework.event.EventPublisher; +import modelengine.fitframework.globalization.StringResource; +import modelengine.fitframework.ioc.BeanContainer; +import modelengine.fitframework.ioc.BeanFactory; +import modelengine.fitframework.ioc.BeanMetadata; +import modelengine.fitframework.ioc.BeanRegisteredObserver; +import modelengine.fitframework.ioc.BeanRegistry; +import modelengine.fitframework.jvm.scan.PackageScanner; +import modelengine.fitframework.jvm.scan.PackageScanner.Callback; +import modelengine.fitframework.plugin.Plugin; +import modelengine.fitframework.plugin.PluginCollection; +import modelengine.fitframework.plugin.PluginMetadata; +import modelengine.fitframework.plugin.RootPlugin; +import modelengine.fitframework.resource.ResourceResolver; +import modelengine.fitframework.runtime.FitRuntime; +import modelengine.fitframework.test.annotation.EnableMockMvc; +import modelengine.fitframework.test.domain.TestContext; +import modelengine.fitframework.test.domain.mvc.MockMvc; +import modelengine.fitframework.util.DisposedCallback; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * MockMvcListener 测试。 + * + * @author 季聿阶 + * @since 2026-01-02 + */ +class MockMvcListenerTest { + private static final String TIMEOUT_KEY = "fit.test.mockmvc.startup.timeout"; + + @AfterEach + void tearDown() { + System.clearProperty(TIMEOUT_KEY); + } + + @Test + void shouldTimeoutWhenServerNotStarted() { + int port = 65530; + System.setProperty(TIMEOUT_KEY, "1000"); + StubBeanRegistry registry = new StubBeanRegistry(); + TestContext context = createContext(registry); + MockMvcListener listener = new MockMvcListenerStub(port, () -> false); + + assertThatThrownBy(() -> listener.beforeTestClass(context)).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Mock MVC server failed to start within") + .hasMessageContaining("port=" + port) + .hasMessageContaining(TIMEOUT_KEY); + } + + @Test + void shouldStartWhenServerAvailable() { + int port = 65531; + System.setProperty(TIMEOUT_KEY, "1000"); + StubBeanRegistry registry = new StubBeanRegistry(); + TestContext context = createContext(registry); + MockMvcListener listener = new MockMvcListenerStub(port, new DelayedStart(150)); + + listener.beforeTestClass(context); + + assertThat(registry.lastRegistered()).isInstanceOf(MockMvc.class); + } + + @Test + void shouldFallbackToDefaultTimeoutWhenInvalidConfig() throws Exception { + System.setProperty(TIMEOUT_KEY, "invalid"); + assertThat(readStartupTimeout()).isEqualTo(30_000L); + } + + @Test + void shouldFallbackToDefaultWhenNegativeTimeout() throws Exception { + System.setProperty(TIMEOUT_KEY, "-1"); + assertThat(readStartupTimeout()).isEqualTo(30_000L); + } + + @Test + void shouldFallbackToDefaultWhenZeroTimeout() throws Exception { + System.setProperty(TIMEOUT_KEY, "0"); + assertThat(readStartupTimeout()).isEqualTo(30_000L); + } + + @Test + void shouldClampWhenTooLargeTimeout() throws Exception { + System.setProperty(TIMEOUT_KEY, String.valueOf(Long.MAX_VALUE)); + assertThat(readStartupTimeout()).isEqualTo(600_000L); + } + + private TestContext createContext(StubBeanRegistry registry) { + StubRootPlugin plugin = new StubRootPlugin(registry); + return new TestContext(MockMvcEnabledTest.class, plugin, Collections.emptyList()); + } + + private long readStartupTimeout() throws Exception { + MockMvcListener listener = new MockMvcListener(8080); + Method method = MockMvcListener.class.getDeclaredMethod("getStartupTimeout"); + method.setAccessible(true); + return (long) method.invoke(listener); + } + + private static final class MockMvcListenerStub extends MockMvcListener { + private final java.util.function.BooleanSupplier startedSupplier; + + private MockMvcListenerStub(int port, java.util.function.BooleanSupplier startedSupplier) { + super(port); + this.startedSupplier = startedSupplier; + } + + @Override + protected boolean isStarted(MockMvc mockMvc) { + return this.startedSupplier.getAsBoolean(); + } + } + + private static final class DelayedStart implements java.util.function.BooleanSupplier { + private final long readyAt; + + private DelayedStart(long delayMillis) { + this.readyAt = System.currentTimeMillis() + delayMillis; + } + + @Override + public boolean getAsBoolean() { + return System.currentTimeMillis() >= this.readyAt; + } + } + + private static final class StubBeanRegistry implements BeanRegistry { + private Object lastRegistered; + + private Object lastRegistered() { + return this.lastRegistered; + } + + @Override + public List register(Class beanClass) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public List register(Object bean) { + this.lastRegistered = bean; + return Collections.emptyList(); + } + + @Override + public List register(Object bean, String name) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public List register(Object bean, Type type) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public List register(modelengine.fitframework.ioc.BeanDefinition definition) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public void subscribe(BeanRegisteredObserver observer) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public void unsubscribe(BeanRegisteredObserver observer) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + } + + private static final class StubBeanContainer implements BeanContainer { + private final StubRootPlugin plugin; + private final StubBeanRegistry registry; + + private StubBeanContainer(StubRootPlugin plugin, StubBeanRegistry registry) { + this.plugin = plugin; + this.registry = registry; + } + + @Override + public String name() { + return "mock-mvc-listener-test"; + } + + @Override + public Plugin plugin() { + return this.plugin; + } + + @Override + public BeanRegistry registry() { + return this.registry; + } + + @Override + public Optional factory(String name) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public Optional factory(Type type) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public List factories(Type type) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public List factories() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public Optional lookup(String name) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public Optional lookup(Type type) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public List all(Type type) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public List all() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public void start() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public void stop() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public Beans beans() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public void destroySingleton(String beanName) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public void removeBean(String beanName) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public void dispose() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public boolean disposed() { + return false; + } + + @Override + public void subscribe(DisposedCallback callback) {} + + @Override + public void unsubscribe(DisposedCallback callback) {} + } + + private static final class StubRootPlugin implements RootPlugin { + private final StubBeanContainer container; + + private StubRootPlugin(StubBeanRegistry registry) { + this.container = new StubBeanContainer(this, registry); + } + + @Override + public BeanContainer container() { + return this.container; + } + + @Override + public Plugin loadPlugin(URL plugin) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public Plugin unloadPlugin(URL plugin) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public PluginMetadata metadata() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public Config config() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public ClassLoader pluginClassLoader() { + return StubRootPlugin.class.getClassLoader(); + } + + @Override + public PackageScanner scanner(Callback callback) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public FitRuntime runtime() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public Plugin parent() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public PluginCollection children() { + return new StubPluginCollection(); + } + + @Override + public ResourceResolver resolverOfResources() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public EventPublisher publisherOfEvents() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public StringResource sr() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public boolean initialized() { + return false; + } + + @Override + public void initialize() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public boolean started() { + return false; + } + + @Override + public void start() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public boolean stopped() { + return false; + } + + @Override + public void stop() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public void dispose() { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public boolean disposed() { + return false; + } + + @Override + public void subscribe(DisposedCallback callback) {} + + @Override + public void unsubscribe(DisposedCallback callback) {} + } + + private static final class StubPluginCollection implements PluginCollection { + @Override + public int size() { + return 0; + } + + @Override + public Plugin add(URL location) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public Plugin remove(URL location) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public Plugin get(int index) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public Plugin get(URL location) { + throw new UnsupportedOperationException("Not needed for MockMvcListenerTest."); + } + + @Override + public boolean contains(URL location) { + return false; + } + + @Override + public java.util.stream.Stream stream() { + return java.util.stream.Stream.empty(); + } + + @Override + public java.util.Iterator iterator() { + return Collections.emptyIterator(); + } + } + + @EnableMockMvc + private static class MockMvcEnabledTest {} +}