From 743a96b75ebf79b2d9a6aba82629b5a85f7167bc Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 14 Nov 2022 19:56:40 +0100 Subject: [PATCH 1/3] Polish SocketUtilsTests --- .../util/SocketUtilsTests.java | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/util/SocketUtilsTests.java b/spring-core/src/test/java/org/springframework/util/SocketUtilsTests.java index c0f8748195..e624c9b12c 100644 --- a/spring-core/src/test/java/org/springframework/util/SocketUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/SocketUtilsTests.java @@ -50,14 +50,14 @@ class SocketUtilsTests { @Test void findAvailableTcpPortWithZeroMinPort() { - assertThatIllegalArgumentException().isThrownBy(() -> - org.springframework.util.SocketUtils.findAvailableTcpPort(0)); + assertThatIllegalArgumentException().isThrownBy( + () -> org.springframework.util.SocketUtils.findAvailableTcpPort(0)); } @Test void findAvailableTcpPortWithNegativeMinPort() { - assertThatIllegalArgumentException().isThrownBy(() -> - org.springframework.util.SocketUtils.findAvailableTcpPort(-500)); + assertThatIllegalArgumentException().isThrownBy( + () -> org.springframework.util.SocketUtils.findAvailableTcpPort(-500)); } @Test @@ -80,8 +80,8 @@ class SocketUtilsTests { try (ServerSocket socket = ServerSocketFactory.getDefault().createServerSocket(port, 1, InetAddress.getByName("localhost"))) { assertThat(socket).isNotNull(); // will only look for the exact port - assertThatIllegalStateException().isThrownBy(() -> - org.springframework.util.SocketUtils.findAvailableTcpPort(port, port)) + assertThatIllegalStateException().isThrownBy( + () -> org.springframework.util.SocketUtils.findAvailableTcpPort(port, port)) .withMessageStartingWith("Could not find an available TCP port") .withMessageEndingWith("after 1 attempts"); } @@ -123,8 +123,7 @@ class SocketUtilsTests { @Test void findAvailableTcpPortsWithRequestedNumberGreaterThanSizeOfRange() { - assertThatIllegalArgumentException().isThrownBy(() -> - findAvailableTcpPorts(50, 45000, 45010)); + assertThatIllegalArgumentException().isThrownBy(() -> findAvailableTcpPorts(50, 45000, 45010)); } @@ -132,14 +131,14 @@ class SocketUtilsTests { @Test void findAvailableUdpPortWithZeroMinPort() { - assertThatIllegalArgumentException().isThrownBy(() -> - org.springframework.util.SocketUtils.findAvailableUdpPort(0)); + assertThatIllegalArgumentException().isThrownBy( + () -> org.springframework.util.SocketUtils.findAvailableUdpPort(0)); } @Test void findAvailableUdpPortWithNegativeMinPort() { - assertThatIllegalArgumentException().isThrownBy(() -> - org.springframework.util.SocketUtils.findAvailableUdpPort(-500)); + assertThatIllegalArgumentException().isThrownBy( + () -> org.springframework.util.SocketUtils.findAvailableUdpPort(-500)); } @Test @@ -155,8 +154,8 @@ class SocketUtilsTests { try (DatagramSocket socket = new DatagramSocket(port, InetAddress.getByName("localhost"))) { assertThat(socket).isNotNull(); // will only look for the exact port - assertThatIllegalStateException().isThrownBy(() -> - org.springframework.util.SocketUtils.findAvailableUdpPort(port, port)) + assertThatIllegalStateException().isThrownBy( + () -> org.springframework.util.SocketUtils.findAvailableUdpPort(port, port)) .withMessageStartingWith("Could not find an available UDP port") .withMessageEndingWith("after 1 attempts"); } @@ -198,8 +197,7 @@ class SocketUtilsTests { @Test void findAvailableUdpPortsWithRequestedNumberGreaterThanSizeOfRange() { - assertThatIllegalArgumentException().isThrownBy(() -> - findAvailableUdpPorts(50, 45000, 45010)); + assertThatIllegalArgumentException().isThrownBy(() -> findAvailableUdpPorts(50, 45000, 45010)); } @@ -226,13 +224,13 @@ class SocketUtilsTests { SortedSet ports = org.springframework.util.SocketUtils.findAvailableUdpPorts(numRequested, minPort, maxPort); assertAvailablePorts(ports, numRequested, minPort, maxPort); } + private void assertPortInRange(int port, int minPort, int maxPort) { - assertThat(port >= minPort).as("port [" + port + "] >= " + minPort).isTrue(); - assertThat(port <= maxPort).as("port [" + port + "] <= " + maxPort).isTrue(); + assertThat(port).as("port").isBetween(minPort, maxPort); } private void assertAvailablePorts(SortedSet ports, int numRequested, int minPort, int maxPort) { - assertThat(ports.size()).as("number of ports requested").isEqualTo(numRequested); + assertThat(ports).as("number of ports requested").hasSize(numRequested); for (int port : ports) { assertPortInRange(port, minPort, maxPort); } From ee51dab1f348361f26c477ad64ab730ad7359d6e Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Sat, 10 Sep 2022 23:19:29 -0500 Subject: [PATCH 2/3] Introduce TestSocketUtils as a replacement for SocketUtils SocketUtils was officially deprecated in 5.3.16 (gh-28052) and removed in 6.0 M3 (gh-28054); however, there is still need for a subset of this functionality in integration tests for testing scenarios in which it is not possible for the system under test to select its own random port (or rely on the operating system to provide an ephemeral port). This commit therefore introduces a scaled down version in the spring-test module called TestSocketUtils which supports retrieval of a single TCP port. See gh-29132 --- .../test/util/TestSocketUtils.java | 109 ++++++++++++++++++ .../test/util/TestSocketUtilsTests.java | 55 +++++++++ .../org.mockito.plugins.MockMaker | 1 + 3 files changed, 165 insertions(+) create mode 100644 spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java create mode 100644 spring-test/src/test/java/org/springframework/test/util/TestSocketUtilsTests.java create mode 100644 spring-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java b/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java new file mode 100644 index 0000000000..5762eef807 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2022 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.test.util; + +import java.net.InetAddress; +import java.net.ServerSocket; +import java.util.Random; + +import javax.net.ServerSocketFactory; + +/** + * Simple utility methods for finding available ports on {@code localhost} for + * use in integration testing scenarios. + * + *

This is a limited form of {@code SocketUtils} which is deprecated in Spring + * Framework 5.3 and removed in Spring Framework 6.0. + * + *

{@code SocketUtils} was introduced in Spring Framework 4.0, primarily to + * assist in writing integration tests which start an external server on an + * available random port. However, these utilities make no guarantee about the + * subsequent availability of a given port and are therefore unreliable (the reason + * for deprecation and removal). + * + *

Instead of using {@code TestSocketUtils} to find an available local port for a server, + * it is recommended that you rely on a server's ability to start on a random port + * that it selects or is assigned by the operating system. To interact with that + * server, you should query the server for the port it is currently using. + * + * @author Sam Brannen + * @author Ben Hale + * @author Arjen Poutsma + * @author Gunnar Hillert + * @author Gary Russell + * @author Chris Bono + * @since 5.3 + */ +public final class TestSocketUtils { + + /** + * The minimum value for port ranges used when finding an available TCP port. + */ + private static final int PORT_RANGE_MIN = 1024; + + /** + * The maximum value for port ranges used when finding an available TCP port. + */ + private static final int PORT_RANGE_MAX = 65535; + + private static final int PORT_RANGE = PORT_RANGE_MAX - PORT_RANGE_MIN; + + private static final int MAX_ATTEMPTS = 1_000; + + private static final Random random = new Random(System.nanoTime()); + + private TestSocketUtils() { + } + + /** + * Find an available TCP port randomly selected from the range [1024, 65535]. + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found within max attempts + */ + public static int findAvailableTcpPort() { + int candidatePort; + int searchCounter = 0; + do { + if (searchCounter > MAX_ATTEMPTS) { + throw new IllegalStateException(String.format( + "Could not find an available TCP port in the range [%d, %d] after %d attempts", + PORT_RANGE_MIN, PORT_RANGE_MAX, MAX_ATTEMPTS)); + } + candidatePort = PORT_RANGE_MIN + random.nextInt(PORT_RANGE + 1); + searchCounter++; + } + while (!isPortAvailable(candidatePort)); + + return candidatePort; + } + + /** + * Determine if the specified TCP port is currently available on {@code localhost}. + */ + private static boolean isPortAvailable(int port) { + try { + ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket( + port, 1, InetAddress.getByName("localhost")); + serverSocket.close(); + return true; + } + catch (Exception ex) { + return false; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/util/TestSocketUtilsTests.java b/spring-test/src/test/java/org/springframework/test/util/TestSocketUtilsTests.java new file mode 100644 index 0000000000..b4879a18cf --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/util/TestSocketUtilsTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2022 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.test.util; + +import javax.net.ServerSocketFactory; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Unit tests for {@link TestSocketUtils}. + * + * @author Sam Brannen + * @author Gary Russell + * @author Chris Bono + */ +class TestSocketUtilsTests { + + @Test + void findAvailableTcpPort() { + int port = TestSocketUtils.findAvailableTcpPort(); + assertThat(port >= 1024).as("port [" + port + "] >= " + 1024).isTrue(); + assertThat(port <= 65535).as("port [" + port + "] <= " + 65535).isTrue(); + } + + @Test + void findAvailableTcpPortWhenNoAvailablePortFoundInMaxAttempts() { + try (MockedStatic mockedServerSocketFactory = Mockito.mockStatic(ServerSocketFactory.class)) { + mockedServerSocketFactory.when(ServerSocketFactory::getDefault).thenThrow(new RuntimeException("Boom")); + assertThatIllegalStateException().isThrownBy(TestSocketUtils::findAvailableTcpPort) + .withMessageStartingWith("Could not find an available TCP port") + .withMessageEndingWith("after 1000 attempts"); + + } + } + +} diff --git a/spring-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/spring-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/spring-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline From 698f8995f72239d11052e1319c6b2802a34ba3e8 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 14 Nov 2022 20:00:22 +0100 Subject: [PATCH 3/3] Revise TestSocketUtils and tests Closes gh-29132 --- .../test/util/TestSocketUtils.java | 92 ++++++++++++------- .../test/util/TestSocketUtilsTests.java | 38 ++++---- .../org.mockito.plugins.MockMaker | 1 - 3 files changed, 83 insertions(+), 48 deletions(-) delete mode 100644 spring-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java b/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java index 5762eef807..53c02be7cb 100644 --- a/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java @@ -22,23 +22,24 @@ import java.util.Random; import javax.net.ServerSocketFactory; +import org.springframework.util.Assert; + /** - * Simple utility methods for finding available ports on {@code localhost} for - * use in integration testing scenarios. + * Simple utility for finding available TCP ports on {@code localhost} for use in + * integration testing scenarios. * - *

This is a limited form of {@code SocketUtils} which is deprecated in Spring - * Framework 5.3 and removed in Spring Framework 6.0. + *

This is a limited form of {@link org.springframework.util.SocketUtils} which + * has been deprecated since Spring Framework 5.3.16 and removed in Spring + * Framework 6.0. * - *

{@code SocketUtils} was introduced in Spring Framework 4.0, primarily to - * assist in writing integration tests which start an external server on an - * available random port. However, these utilities make no guarantee about the - * subsequent availability of a given port and are therefore unreliable (the reason - * for deprecation and removal). - * - *

Instead of using {@code TestSocketUtils} to find an available local port for a server, - * it is recommended that you rely on a server's ability to start on a random port - * that it selects or is assigned by the operating system. To interact with that - * server, you should query the server for the port it is currently using. + *

{@code TestSocketUtils} can be used in integration tests which start an + * external server on an available random port. However, these utilities make no + * guarantee about the subsequent availability of a given port and are therefore + * unreliable. Instead of using {@code TestSocketUtils} to find an available local + * port for a server, it is recommended that you rely on a server's ability to + * start on a random ephemeral port that it selects or is assigned by the + * operating system. To interact with that server, you should query the server + * for the port it is currently using. * * @author Sam Brannen * @author Ben Hale @@ -46,45 +47,73 @@ import javax.net.ServerSocketFactory; * @author Gunnar Hillert * @author Gary Russell * @author Chris Bono - * @since 5.3 + * @since 5.3.24 */ -public final class TestSocketUtils { +public class TestSocketUtils { /** * The minimum value for port ranges used when finding an available TCP port. */ - private static final int PORT_RANGE_MIN = 1024; + static final int PORT_RANGE_MIN = 1024; /** * The maximum value for port ranges used when finding an available TCP port. */ - private static final int PORT_RANGE_MAX = 65535; + static final int PORT_RANGE_MAX = 65535; - private static final int PORT_RANGE = PORT_RANGE_MAX - PORT_RANGE_MIN; + private static final int PORT_RANGE_PLUS_ONE = PORT_RANGE_MAX - PORT_RANGE_MIN + 1; private static final int MAX_ATTEMPTS = 1_000; private static final Random random = new Random(System.nanoTime()); - private TestSocketUtils() { + private static final TestSocketUtils INSTANCE = new TestSocketUtils(); + + + /** + * Although {@code TestSocketUtils} consists solely of static utility methods, + * this constructor is intentionally {@code public}. + *

Rationale

+ *

Static methods from this class may be invoked from within XML + * configuration files using the Spring Expression Language (SpEL) and the + * following syntax. + *


+	 * <bean id="myBean" ... p:port="#{T(org.springframework.test.util.TestSocketUtils).findAvailableTcpPort()}" />
+	 * 
+ *

If this constructor were {@code private}, you would be required to supply + * the fully qualified class name to SpEL's {@code T()} function for each usage. + * Thus, the fact that this constructor is {@code public} allows you to reduce + * boilerplate configuration with SpEL as can be seen in the following example. + *


+	 * <bean id="socketUtils" class="org.springframework.test.util.TestSocketUtils" />
+	 * <bean id="myBean" ... p:port="#{socketUtils.findAvailableTcpPort()}" />
+	 * 
+ */ + public TestSocketUtils() { } /** * Find an available TCP port randomly selected from the range [1024, 65535]. * @return an available TCP port number - * @throws IllegalStateException if no available port could be found within max attempts + * @throws IllegalStateException if no available port could be found */ public static int findAvailableTcpPort() { + return INSTANCE.findAvailableTcpPortInternal(); + } + + + /** + * Internal implementation of {@link #findAvailableTcpPort()}. + *

Package-private solely for testing purposes. + */ + int findAvailableTcpPortInternal() { int candidatePort; int searchCounter = 0; do { - if (searchCounter > MAX_ATTEMPTS) { - throw new IllegalStateException(String.format( - "Could not find an available TCP port in the range [%d, %d] after %d attempts", - PORT_RANGE_MIN, PORT_RANGE_MAX, MAX_ATTEMPTS)); - } - candidatePort = PORT_RANGE_MIN + random.nextInt(PORT_RANGE + 1); - searchCounter++; + Assert.state(++searchCounter <= MAX_ATTEMPTS, () -> String.format( + "Could not find an available TCP port in the range [%d, %d] after %d attempts", + PORT_RANGE_MIN, PORT_RANGE_MAX, MAX_ATTEMPTS)); + candidatePort = PORT_RANGE_MIN + random.nextInt(PORT_RANGE_PLUS_ONE); } while (!isPortAvailable(candidatePort)); @@ -93,11 +122,12 @@ public final class TestSocketUtils { /** * Determine if the specified TCP port is currently available on {@code localhost}. + *

Package-private solely for testing purposes. */ - private static boolean isPortAvailable(int port) { + boolean isPortAvailable(int port) { try { - ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket( - port, 1, InetAddress.getByName("localhost")); + ServerSocket serverSocket = ServerSocketFactory.getDefault() + .createServerSocket(port, 1, InetAddress.getByName("localhost")); serverSocket.close(); return true; } diff --git a/spring-test/src/test/java/org/springframework/test/util/TestSocketUtilsTests.java b/spring-test/src/test/java/org/springframework/test/util/TestSocketUtilsTests.java index b4879a18cf..8ebc85f85f 100644 --- a/spring-test/src/test/java/org/springframework/test/util/TestSocketUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/util/TestSocketUtilsTests.java @@ -16,11 +16,8 @@ package org.springframework.test.util; -import javax.net.ServerSocketFactory; - +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -30,26 +27,35 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; * * @author Sam Brannen * @author Gary Russell - * @author Chris Bono + * @since 5.3.24 */ class TestSocketUtilsTests { @Test + void canBeInstantiated() { + // Just making sure somebody doesn't try to make SocketUtils abstract, + // since that would be a breaking change due to the intentional public + // constructor. + new TestSocketUtils(); + } + + @RepeatedTest(10) void findAvailableTcpPort() { - int port = TestSocketUtils.findAvailableTcpPort(); - assertThat(port >= 1024).as("port [" + port + "] >= " + 1024).isTrue(); - assertThat(port <= 65535).as("port [" + port + "] <= " + 65535).isTrue(); + assertThat(TestSocketUtils.findAvailableTcpPort()) + .isBetween(TestSocketUtils.PORT_RANGE_MIN, TestSocketUtils.PORT_RANGE_MAX); } @Test - void findAvailableTcpPortWhenNoAvailablePortFoundInMaxAttempts() { - try (MockedStatic mockedServerSocketFactory = Mockito.mockStatic(ServerSocketFactory.class)) { - mockedServerSocketFactory.when(ServerSocketFactory::getDefault).thenThrow(new RuntimeException("Boom")); - assertThatIllegalStateException().isThrownBy(TestSocketUtils::findAvailableTcpPort) - .withMessageStartingWith("Could not find an available TCP port") - .withMessageEndingWith("after 1000 attempts"); - - } + void findAvailableTcpPortWhenNoAvailablePortFoundInMaxAttempts() { + TestSocketUtils socketUtils = new TestSocketUtils() { + @Override + boolean isPortAvailable(int port) { + return false; + } + }; + assertThatIllegalStateException() + .isThrownBy(socketUtils::findAvailableTcpPortInternal) + .withMessage("Could not find an available TCP port in the range [1024, 65535] after 1000 attempts"); } } diff --git a/spring-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/spring-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index 1f0955d450..0000000000 --- a/spring-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline