Skip to content

Commit 698f899

Browse files
committed
Revise TestSocketUtils and tests
Closes gh-29132
1 parent ee51dab commit 698f899

File tree

3 files changed

+83
-48
lines changed

3 files changed

+83
-48
lines changed

spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java

+61-31
Original file line numberDiff line numberDiff line change
@@ -22,69 +22,98 @@
2222

2323
import javax.net.ServerSocketFactory;
2424

25+
import org.springframework.util.Assert;
26+
2527
/**
26-
* Simple utility methods for finding available ports on {@code localhost} for
27-
* use in integration testing scenarios.
28-
*
29-
* <p>This is a limited form of {@code SocketUtils} which is deprecated in Spring
30-
* Framework 5.3 and removed in Spring Framework 6.0.
28+
* Simple utility for finding available TCP ports on {@code localhost} for use in
29+
* integration testing scenarios.
3130
*
32-
* <p>{@code SocketUtils} was introduced in Spring Framework 4.0, primarily to
33-
* assist in writing integration tests which start an external server on an
34-
* available random port. However, these utilities make no guarantee about the
35-
* subsequent availability of a given port and are therefore unreliable (the reason
36-
* for deprecation and removal).
31+
* <p>This is a limited form of {@link org.springframework.util.SocketUtils} which
32+
* has been deprecated since Spring Framework 5.3.16 and removed in Spring
33+
* Framework 6.0.
3734
*
38-
* <p>Instead of using {@code TestSocketUtils} to find an available local port for a server,
39-
* it is recommended that you rely on a server's ability to start on a random port
40-
* that it selects or is assigned by the operating system. To interact with that
41-
* server, you should query the server for the port it is currently using.
35+
* <p>{@code TestSocketUtils} can be used in integration tests which start an
36+
* external server on an available random port. However, these utilities make no
37+
* guarantee about the subsequent availability of a given port and are therefore
38+
* unreliable. Instead of using {@code TestSocketUtils} to find an available local
39+
* port for a server, it is recommended that you rely on a server's ability to
40+
* start on a random <em>ephemeral</em> port that it selects or is assigned by the
41+
* operating system. To interact with that server, you should query the server
42+
* for the port it is currently using.
4243
*
4344
* @author Sam Brannen
4445
* @author Ben Hale
4546
* @author Arjen Poutsma
4647
* @author Gunnar Hillert
4748
* @author Gary Russell
4849
* @author Chris Bono
49-
* @since 5.3
50+
* @since 5.3.24
5051
*/
51-
public final class TestSocketUtils {
52+
public class TestSocketUtils {
5253

5354
/**
5455
* The minimum value for port ranges used when finding an available TCP port.
5556
*/
56-
private static final int PORT_RANGE_MIN = 1024;
57+
static final int PORT_RANGE_MIN = 1024;
5758

5859
/**
5960
* The maximum value for port ranges used when finding an available TCP port.
6061
*/
61-
private static final int PORT_RANGE_MAX = 65535;
62+
static final int PORT_RANGE_MAX = 65535;
6263

63-
private static final int PORT_RANGE = PORT_RANGE_MAX - PORT_RANGE_MIN;
64+
private static final int PORT_RANGE_PLUS_ONE = PORT_RANGE_MAX - PORT_RANGE_MIN + 1;
6465

6566
private static final int MAX_ATTEMPTS = 1_000;
6667

6768
private static final Random random = new Random(System.nanoTime());
6869

69-
private TestSocketUtils() {
70+
private static final TestSocketUtils INSTANCE = new TestSocketUtils();
71+
72+
73+
/**
74+
* Although {@code TestSocketUtils} consists solely of static utility methods,
75+
* this constructor is intentionally {@code public}.
76+
* <h4>Rationale</h4>
77+
* <p>Static methods from this class may be invoked from within XML
78+
* configuration files using the Spring Expression Language (SpEL) and the
79+
* following syntax.
80+
* <pre><code>
81+
* &lt;bean id="myBean" ... p:port="#{T(org.springframework.test.util.TestSocketUtils).findAvailableTcpPort()}" /&gt;</code>
82+
* </pre>
83+
* <p>If this constructor were {@code private}, you would be required to supply
84+
* the fully qualified class name to SpEL's {@code T()} function for each usage.
85+
* Thus, the fact that this constructor is {@code public} allows you to reduce
86+
* boilerplate configuration with SpEL as can be seen in the following example.
87+
* <pre><code>
88+
* &lt;bean id="socketUtils" class="org.springframework.test.util.TestSocketUtils" /&gt;
89+
* &lt;bean id="myBean" ... p:port="#{socketUtils.findAvailableTcpPort()}" /&gt;</code>
90+
* </pre>
91+
*/
92+
public TestSocketUtils() {
7093
}
7194

7295
/**
7396
* Find an available TCP port randomly selected from the range [1024, 65535].
7497
* @return an available TCP port number
75-
* @throws IllegalStateException if no available port could be found within max attempts
98+
* @throws IllegalStateException if no available port could be found
7699
*/
77100
public static int findAvailableTcpPort() {
101+
return INSTANCE.findAvailableTcpPortInternal();
102+
}
103+
104+
105+
/**
106+
* Internal implementation of {@link #findAvailableTcpPort()}.
107+
* <p>Package-private solely for testing purposes.
108+
*/
109+
int findAvailableTcpPortInternal() {
78110
int candidatePort;
79111
int searchCounter = 0;
80112
do {
81-
if (searchCounter > MAX_ATTEMPTS) {
82-
throw new IllegalStateException(String.format(
83-
"Could not find an available TCP port in the range [%d, %d] after %d attempts",
84-
PORT_RANGE_MIN, PORT_RANGE_MAX, MAX_ATTEMPTS));
85-
}
86-
candidatePort = PORT_RANGE_MIN + random.nextInt(PORT_RANGE + 1);
87-
searchCounter++;
113+
Assert.state(++searchCounter <= MAX_ATTEMPTS, () -> String.format(
114+
"Could not find an available TCP port in the range [%d, %d] after %d attempts",
115+
PORT_RANGE_MIN, PORT_RANGE_MAX, MAX_ATTEMPTS));
116+
candidatePort = PORT_RANGE_MIN + random.nextInt(PORT_RANGE_PLUS_ONE);
88117
}
89118
while (!isPortAvailable(candidatePort));
90119

@@ -93,11 +122,12 @@ public static int findAvailableTcpPort() {
93122

94123
/**
95124
* Determine if the specified TCP port is currently available on {@code localhost}.
125+
* <p>Package-private solely for testing purposes.
96126
*/
97-
private static boolean isPortAvailable(int port) {
127+
boolean isPortAvailable(int port) {
98128
try {
99-
ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket(
100-
port, 1, InetAddress.getByName("localhost"));
129+
ServerSocket serverSocket = ServerSocketFactory.getDefault()
130+
.createServerSocket(port, 1, InetAddress.getByName("localhost"));
101131
serverSocket.close();
102132
return true;
103133
}

spring-test/src/test/java/org/springframework/test/util/TestSocketUtilsTests.java

+22-16
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,8 @@
1616

1717
package org.springframework.test.util;
1818

19-
import javax.net.ServerSocketFactory;
20-
19+
import org.junit.jupiter.api.RepeatedTest;
2120
import org.junit.jupiter.api.Test;
22-
import org.mockito.MockedStatic;
23-
import org.mockito.Mockito;
2421

2522
import static org.assertj.core.api.Assertions.assertThat;
2623
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
@@ -30,26 +27,35 @@
3027
*
3128
* @author Sam Brannen
3229
* @author Gary Russell
33-
* @author Chris Bono
30+
* @since 5.3.24
3431
*/
3532
class TestSocketUtilsTests {
3633

3734
@Test
35+
void canBeInstantiated() {
36+
// Just making sure somebody doesn't try to make SocketUtils abstract,
37+
// since that would be a breaking change due to the intentional public
38+
// constructor.
39+
new TestSocketUtils();
40+
}
41+
42+
@RepeatedTest(10)
3843
void findAvailableTcpPort() {
39-
int port = TestSocketUtils.findAvailableTcpPort();
40-
assertThat(port >= 1024).as("port [" + port + "] >= " + 1024).isTrue();
41-
assertThat(port <= 65535).as("port [" + port + "] <= " + 65535).isTrue();
44+
assertThat(TestSocketUtils.findAvailableTcpPort())
45+
.isBetween(TestSocketUtils.PORT_RANGE_MIN, TestSocketUtils.PORT_RANGE_MAX);
4246
}
4347

4448
@Test
45-
void findAvailableTcpPortWhenNoAvailablePortFoundInMaxAttempts() {
46-
try (MockedStatic<ServerSocketFactory> mockedServerSocketFactory = Mockito.mockStatic(ServerSocketFactory.class)) {
47-
mockedServerSocketFactory.when(ServerSocketFactory::getDefault).thenThrow(new RuntimeException("Boom"));
48-
assertThatIllegalStateException().isThrownBy(TestSocketUtils::findAvailableTcpPort)
49-
.withMessageStartingWith("Could not find an available TCP port")
50-
.withMessageEndingWith("after 1000 attempts");
51-
52-
}
49+
void findAvailableTcpPortWhenNoAvailablePortFoundInMaxAttempts() {
50+
TestSocketUtils socketUtils = new TestSocketUtils() {
51+
@Override
52+
boolean isPortAvailable(int port) {
53+
return false;
54+
}
55+
};
56+
assertThatIllegalStateException()
57+
.isThrownBy(socketUtils::findAvailableTcpPortInternal)
58+
.withMessage("Could not find an available TCP port in the range [1024, 65535] after 1000 attempts");
5359
}
5460

5561
}

spring-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker

-1
This file was deleted.

0 commit comments

Comments
 (0)