Skip to content

Commit

Permalink
Introduce TestSocketUtils as a replacement for SocketUtils
Browse files Browse the repository at this point in the history
SocketUtils was officially deprecated in 5.3.16 (spring-projectsgh-28052) and removed
in 6.0 M3 (spring-projectsgh-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 spring-projectsgh-29132
  • Loading branch information
onobc authored and sbrannen committed Nov 14, 2022
1 parent 743a96b commit ee51dab
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 0 deletions.
@@ -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.
*
* <p>This is a limited form of {@code SocketUtils} which is deprecated in Spring
* Framework 5.3 and removed in Spring Framework 6.0.
*
* <p>{@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).
*
* <p>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;
}
}

}
@@ -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<ServerSocketFactory> 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");

}
}

}
@@ -0,0 +1 @@
mock-maker-inline

0 comments on commit ee51dab

Please sign in to comment.