diff --git a/medium-tests/pom.xml b/medium-tests/pom.xml index c6f21e03b1..d35168df60 100644 --- a/medium-tests/pom.xml +++ b/medium-tests/pom.xml @@ -90,6 +90,14 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + 1C + true + + org.apache.maven.plugins maven-dependency-plugin diff --git a/medium-tests/src/test/java/mediumtest/EffectiveRulesMediumTests.java b/medium-tests/src/test/java/mediumtest/EffectiveRulesMediumTests.java index e0b26f4022..bbe5855cef 100644 --- a/medium-tests/src/test/java/mediumtest/EffectiveRulesMediumTests.java +++ b/medium-tests/src/test/java/mediumtest/EffectiveRulesMediumTests.java @@ -125,7 +125,7 @@ void it_should_fail_when_rule_key_unknown_and_project_is_not_bound(SonarLintTest var futureResponse = backend.getRulesService().getEffectiveRuleDetails(new GetEffectiveRuleDetailsParams("scopeId", "python:SXXXX", null)); - assertThat(futureResponse).failsWithin(1, TimeUnit.SECONDS) + assertThat(futureResponse).failsWithin(2, TimeUnit.SECONDS) .withThrowableOfType(ExecutionException.class) .withCauseInstanceOf(ResponseErrorException.class) .withMessageContaining("Could not find rule 'python:SXXXX' in embedded rules"); diff --git a/medium-tests/src/test/java/mediumtest/promotion/ExtraEnabledLanguagesInConnectedModePromotionMediumTests.java b/medium-tests/src/test/java/mediumtest/promotion/ExtraEnabledLanguagesInConnectedModePromotionMediumTests.java index 3395e7d926..be4ee898e5 100644 --- a/medium-tests/src/test/java/mediumtest/promotion/ExtraEnabledLanguagesInConnectedModePromotionMediumTests.java +++ b/medium-tests/src/test/java/mediumtest/promotion/ExtraEnabledLanguagesInConnectedModePromotionMediumTests.java @@ -42,7 +42,6 @@ import static org.mockito.Mockito.after; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability.EMBEDDED_SERVER; class ExtraEnabledLanguagesInConnectedModePromotionMediumTests { @RegisterExtension @@ -58,7 +57,6 @@ void it_should_notify_clients_for_a_detected_language_that_is_enabled_only_in_co var backend = harness.newBackend() .withExtraEnabledLanguagesInConnectedMode(Language.ABAP) .withUnboundConfigScope("configScopeId") - .withBackendCapability(EMBEDDED_SERVER) .withTelemetryEnabled() .start(fakeClient); @@ -83,7 +81,6 @@ void it_should_not_notify_clients_when_already_in_connected_mode(SonarLintTestHa .withExtraEnabledLanguagesInConnectedMode(Language.ABAP) .withSonarQubeConnection("connectionId", server, storage -> storage.withProject("projectKey", project -> project.withRuleSet("abap", ruleSet -> ruleSet.withActiveRule("abap:S100", "MAJOR")).withMainBranch("main"))) .withBoundConfigScope("configScopeId", "connectionId", "projectKey") - .withBackendCapability(EMBEDDED_SERVER) .withTelemetryEnabled() .start(fakeClient); @@ -104,7 +101,6 @@ void it_should_not_notify_clients_when_detected_language_is_not_an_extra_languag var backend = harness.newBackend() .withEnabledLanguageInStandaloneMode(Language.ABAP) .withUnboundConfigScope("configScopeId") - .withBackendCapability(EMBEDDED_SERVER) .withTelemetryEnabled() .start(fakeClient); @@ -125,7 +121,6 @@ void it_should_not_notify_clients_when_no_language_was_detected_during_analysis( .build(); var backend = harness.newBackend() .withUnboundConfigScope("configScopeId") - .withBackendCapability(EMBEDDED_SERVER) .withTelemetryEnabled() .start(fakeClient); diff --git a/medium-tests/src/test/java/mediumtest/smartnotifications/SmartNotificationsMediumTests.java b/medium-tests/src/test/java/mediumtest/smartnotifications/SmartNotificationsMediumTests.java index b5026a615e..2176770a01 100644 --- a/medium-tests/src/test/java/mediumtest/smartnotifications/SmartNotificationsMediumTests.java +++ b/medium-tests/src/test/java/mediumtest/smartnotifications/SmartNotificationsMediumTests.java @@ -287,7 +287,7 @@ void it_should_skip_polling_notifications_when_sonarcloud_websocket_opened(Sonar harness.newBackend() .withSonarQubeCloudEuRegionUri(mockWebServerExtension.endpointParams().getBaseUrl()) - .withSonarQubeCloudEuRegionWebSocketUri(webSocketServer.getUrl()) + .withSonarQubeCloudEuRegionWebSocketUri(webSocketServer.getUri()) .withSonarCloudConnectionAndNotifications(CONNECTION_ID, "myOrg", storage -> storage.withProject(PROJECT_KEY, project -> project.withLastSmartNotificationPoll(STORED_DATE))) .withBoundConfigScope("scopeId", CONNECTION_ID, PROJECT_KEY) .withBackendCapability(SMART_NOTIFICATIONS, SERVER_SENT_EVENTS) diff --git a/medium-tests/src/test/java/mediumtest/websockets/WebSocketMediumTests.java b/medium-tests/src/test/java/mediumtest/websockets/WebSocketMediumTests.java index c16bd47c28..5ad8fcdfc9 100644 --- a/medium-tests/src/test/java/mediumtest/websockets/WebSocketMediumTests.java +++ b/medium-tests/src/test/java/mediumtest/websockets/WebSocketMediumTests.java @@ -74,7 +74,7 @@ class WebSocketMediumTests { void prepare() { webSocketServerEU = new WebSocketServer(); webSocketServerEU.start(); - webSocketServerUS = new WebSocketServer(WebSocketServer.DEFAULT_PORT + 1); + webSocketServerUS = new WebSocketServer(); webSocketServerUS.start(); } @@ -512,9 +512,9 @@ void should_log_failure_and_reconnect_later_if_server_unavailable(SonarLintTestH backend.getConnectionService() .didUpdateConnections(new DidUpdateConnectionsParams(emptyList(), List.of(new SonarCloudConnectionConfigurationDto("connectionId", "orgKey", SonarCloudRegion.EU, false)))); - await().untilAsserted(() -> assertThat(client.getLogMessages()).contains("Error while trying to create websocket connection for ws://localhost:54321/endpoint")); + await().untilAsserted(() -> assertThat(client.getLogMessages()).contains("Error while trying to create websocket connection for " + webSocketServerEU.getUri())); - webSocketServerEU.start(); + webSocketServerEU.restart(); // Emulate a change on the connection to force websocket service to reconnect backend.getConnectionService().didChangeCredentials(new DidChangeCredentialsParams("connectionId")); @@ -1493,8 +1493,8 @@ void should_send_one_subscribe_message_per_project_key_when_reopening_connection public SonarLintBackendFixture.SonarLintBackendBuilder newBackendWithWebSockets(SonarLintTestHarness harness) { return harness.newBackend() .withBackendCapability(SERVER_SENT_EVENTS) - .withSonarQubeCloudEuRegionWebSocketUri(webSocketServerEU.getUrl()) - .withSonarQubeCloudUsRegionWebSocketUri(webSocketServerUS.getUrl()); + .withSonarQubeCloudEuRegionWebSocketUri(webSocketServerEU.getUri()) + .withSonarQubeCloudUsRegionWebSocketUri(webSocketServerUS.getUri()); } public static class WebSocketPayloadBuilder { diff --git a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/SonarLintBackendFixture.java b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/SonarLintBackendFixture.java index d875e9a98c..1668734bad 100644 --- a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/SonarLintBackendFixture.java +++ b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/SonarLintBackendFixture.java @@ -162,14 +162,14 @@ public static class SonarLintBackendBuilder { @Nullable private String euRegionApiUri; @Nullable - private String euRegionWebSocketUri; + private URI euRegionWebSocketUri; @Nullable private String usRegionUri; @Nullable private String usRegionApiUri; @Nullable - private String usRegionWebSocketUri; + private URI usRegionWebSocketUri; private Duration responseTimeout; private Path keyStorePath; @@ -253,7 +253,7 @@ public SonarLintBackendBuilder withSonarQubeCloudEuRegionApiUri(String euRegionA return this; } - public SonarLintBackendBuilder withSonarQubeCloudEuRegionWebSocketUri(String euRegionWebSocketUri) { + public SonarLintBackendBuilder withSonarQubeCloudEuRegionWebSocketUri(URI euRegionWebSocketUri) { this.euRegionWebSocketUri = euRegionWebSocketUri; return this; } @@ -268,7 +268,7 @@ public SonarLintBackendBuilder withSonarQubeCloudUsRegionApiUri(String usRegionA return this; } - public SonarLintBackendBuilder withSonarQubeCloudUsRegionWebSocketUri(String usRegionWebSocketUri) { + public SonarLintBackendBuilder withSonarQubeCloudUsRegionWebSocketUri(URI usRegionWebSocketUri) { this.usRegionWebSocketUri = usRegionWebSocketUri; return this; } @@ -496,9 +496,9 @@ public SonarLintTestRpcServer start(SonarLintRpcClientDelegate client) { // If more regions are added in the future, extend this by adding a new entry set and add the fields / methods above! var sonarCloudAlternativeEnvironment = new SonarCloudAlternativeEnvironmentDto(Map.of( SonarCloudRegion.EU, - new SonarQubeCloudRegionDto(createUriFromString(euRegionUri), createUriFromString(euRegionApiUri), createUriFromString(euRegionWebSocketUri)), + new SonarQubeCloudRegionDto(createUriFromString(euRegionUri), createUriFromString(euRegionApiUri), euRegionWebSocketUri), SonarCloudRegion.US, - new SonarQubeCloudRegionDto(createUriFromString(usRegionUri), createUriFromString(usRegionApiUri), createUriFromString(usRegionWebSocketUri)))); + new SonarQubeCloudRegionDto(createUriFromString(usRegionUri), createUriFromString(usRegionApiUri), usRegionWebSocketUri))); var sslConfiguration = new SslConfigurationDto(null, null, null, keyStorePath, keyStorePassword, keyStoreType); var httpConfiguration = new HttpConfigurationDto(sslConfiguration, null, null, null, responseTimeout); diff --git a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/NetworkUtils.java b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/NetworkUtils.java new file mode 100644 index 0000000000..9074cd06b4 --- /dev/null +++ b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/NetworkUtils.java @@ -0,0 +1,81 @@ +/* + * SonarLint Core - Test Utils + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarsource.sonarlint.core.test.utils.server; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.UnknownHostException; +import java.util.HashSet; +import java.util.Set; + +public final class NetworkUtils { + + private static final Set ALREADY_ALLOCATED = new HashSet<>(); + private static final int MAX_TRIES = 50; + + private static final Set HTTP_BLOCKED_PORTS = Set.of(2_049, 4_045, 6_000); + + private NetworkUtils() { + // prevent instantiation + } + + public static int getNextAvailablePort() { + return getNextAvailablePort(getLocalhost()); + } + + static int getNextAvailablePort(InetAddress inetAddress) { + return getNextAvailablePort(inetAddress, new PortAllocator()); + } + + private static InetAddress getLocalhost() { + try { + return InetAddress.getLocalHost(); + } catch (UnknownHostException e) { + throw new IllegalStateException("Fail to get localhost IP", e); + } + } + + static int getNextAvailablePort(InetAddress address, PortAllocator portAllocator) { + for (var i = 0; i < MAX_TRIES; i++) { + int port = portAllocator.getAvailable(address); + if (isValidPort(port)) { + ALREADY_ALLOCATED.add(port); + return port; + } + } + throw new IllegalStateException("Fail to find an available port on " + address); + } + + private static boolean isValidPort(int port) { + return port > 1023 && !HTTP_BLOCKED_PORTS.contains(port) && !ALREADY_ALLOCATED.contains(port); + } + + static class PortAllocator { + + int getAvailable(InetAddress address) { + try (var socket = new ServerSocket(0, 50, address)) { + return socket.getLocalPort(); + } catch (IOException e) { + throw new IllegalStateException("Fail to find an available port on " + address, e); + } + } + } +} diff --git a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/ServerFixture.java b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/ServerFixture.java index 1430d16c37..d990102762 100644 --- a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/ServerFixture.java +++ b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/ServerFixture.java @@ -27,6 +27,7 @@ import com.github.tomakehurst.wiremock.matching.AnythingPattern; import com.google.protobuf.Message; import java.io.IOException; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; @@ -775,6 +776,7 @@ public static class Server { private final boolean serverSentEventsEnabled; private final Set features; private SSEServer sseServer; + private URI sseServerUri; public Server(ServerKind serverKind, ServerStatus serverStatus, @Nullable String version, Map organizationsByKey, Map projectsByProjectKey, @@ -800,7 +802,7 @@ public void start() { mockServer.start(); if (serverSentEventsEnabled) { sseServer = new SSEServer(); - sseServer.start(); + sseServerUri = sseServer.start(); } registerWebApiResponses(); } @@ -1508,7 +1510,7 @@ private void registerPushApiResponses() { .withQueryParam("languages", new AnythingPattern()) .willReturn(aResponse() .withStatus(302) - .withHeader("Location", sseServer.getUrl()))); + .withHeader("Location", sseServerUri.toString()))); } private void registerFeaturesApiResponses() { diff --git a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/sse/SSEServer.java b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/sse/SSEServer.java index d4212e3410..a81b3a7029 100644 --- a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/sse/SSEServer.java +++ b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/sse/SSEServer.java @@ -20,27 +20,30 @@ package org.sonarsource.sonarlint.core.test.utils.server.sse; import java.io.File; +import java.net.URI; import org.apache.catalina.LifecycleException; import org.apache.catalina.startup.Tomcat; +import org.sonarsource.sonarlint.core.test.utils.server.NetworkUtils; public class SSEServer { - public static final int DEFAULT_PORT = 54321; private Tomcat tomcat; private SSEServlet sseServlet; - public void start() { + public URI start() { try { var baseDir = new File("").getAbsoluteFile().getParentFile().getPath(); tomcat = new Tomcat(); tomcat.setBaseDir(baseDir); - tomcat.setPort(DEFAULT_PORT); + var port = NetworkUtils.getNextAvailablePort(); + tomcat.setPort(port); var context = tomcat.addContext("", baseDir); sseServlet = new SSEServlet(); Tomcat.addServlet(context, "sse", sseServlet).addMapping("/"); // needed to start the endpoint tomcat.getConnector(); tomcat.start(); + return URI.create("http://localhost:" + port); } catch (LifecycleException e) { throw new IllegalStateException(e); } @@ -55,10 +58,6 @@ public void stop() { } } - public String getUrl() { - return "http://localhost:" + DEFAULT_PORT; - } - public void sendEventToAllClients(String eventPayload) { sseServlet.sendEventToAllClients(eventPayload); } diff --git a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/websockets/WebSocketServer.java b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/websockets/WebSocketServer.java index a04f54411b..9cc42f5e79 100644 --- a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/websockets/WebSocketServer.java +++ b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/server/websockets/WebSocketServer.java @@ -20,33 +20,35 @@ package org.sonarsource.sonarlint.core.test.utils.server.websockets; import java.io.File; +import java.net.URI; import java.util.List; import org.apache.catalina.LifecycleException; import org.apache.catalina.servlets.DefaultServlet; import org.apache.catalina.startup.Tomcat; +import org.sonarsource.sonarlint.core.test.utils.server.NetworkUtils; public class WebSocketServer { - public static final int DEFAULT_PORT = 54321; public static final String CONNECTION_REPOSITORY_ATTRIBUTE_KEY = "connectionRepository"; private Tomcat tomcat; private WebSocketConnectionRepository connectionRepository; - private final int port; + private int port = -1; - public WebSocketServer(int port) { - this.port = port; + public void start() { + start(NetworkUtils.getNextAvailablePort()); } - public WebSocketServer() { - this(DEFAULT_PORT); + public void restart() { + start(port); } - public void start() { + private void start(int port) { try { var baseDir = new File("").getAbsoluteFile().getParentFile().getPath(); tomcat = new Tomcat(); tomcat.setBaseDir(baseDir); tomcat.setPort(port); + this.port = port; var context = tomcat.addContext("", baseDir); connectionRepository = new WebSocketConnectionRepository(); context.getServletContext().setAttribute(CONNECTION_REPOSITORY_ATTRIBUTE_KEY, connectionRepository); @@ -69,8 +71,8 @@ public void stop() { } } - public String getUrl() { - return "ws://localhost:" + port + "/endpoint"; + public URI getUri() { + return URI.create("ws://localhost:" + port + "/endpoint"); } public List getConnections() { diff --git a/test-utils/src/test/java/org/sonarsource/sonarlint/core/test/utils/server/NetworkUtilsTest.java b/test-utils/src/test/java/org/sonarsource/sonarlint/core/test/utils/server/NetworkUtilsTest.java new file mode 100644 index 0000000000..b7e1e8df99 --- /dev/null +++ b/test-utils/src/test/java/org/sonarsource/sonarlint/core/test/utils/server/NetworkUtilsTest.java @@ -0,0 +1,78 @@ +/* + * SonarLint Core - Test Utils + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarsource.sonarlint.core.test.utils.server; + +import java.net.InetAddress; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +import static java.net.InetAddress.getLoopbackAddress; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonarsource.sonarlint.core.test.utils.server.NetworkUtils.getNextAvailablePort; + +public class NetworkUtilsTest { + + @Test + public void getNextAvailablePort_never_returns_the_same_port_in_current_jvm() { + Set ports = new HashSet<>(); + for (int i = 0; i < 100; i++) { + int port = getNextAvailablePort(getLoopbackAddress()); + assertThat(port).isGreaterThan(1023); + ports.add(port); + } + assertThat(ports).hasSize(100); + } + + @Test + public void getNextAvailablePort_retries_to_get_available_port_when_port_has_already_been_allocated() { + NetworkUtils.PortAllocator portAllocator = mock(NetworkUtils.PortAllocator.class); + when(portAllocator.getAvailable(any(InetAddress.class))).thenReturn(9_000, 9_000, 9_000, 9_100); + + InetAddress address = getLoopbackAddress(); + assertThat(getNextAvailablePort(address, portAllocator)).isEqualTo(9_000); + assertThat(getNextAvailablePort(address, portAllocator)).isEqualTo(9_100); + } + + @Test + public void getNextAvailablePort_does_not_return_special_ports() { + NetworkUtils.PortAllocator portAllocator = mock(NetworkUtils.PortAllocator.class); + when(portAllocator.getAvailable(any(InetAddress.class))).thenReturn(900, 2_049, 4_045, 6_000, 1_059); + + // the four first ports are banned, so 1_059 is returned + assertThat(getNextAvailablePort(getLoopbackAddress(), portAllocator)).isEqualTo(1_059); + } + + @Test + public void getNextAvailablePort_throws_ISE_if_too_many_attempts() { + NetworkUtils.PortAllocator portAllocator = mock(NetworkUtils.PortAllocator.class); + when(portAllocator.getAvailable(any(InetAddress.class))).thenReturn(900); + + var throwable = catchThrowable(() -> getNextAvailablePort(getLoopbackAddress(), portAllocator)); + + assertThat(throwable) + .isInstanceOf(IllegalStateException.class) + .hasMessageStartingWith("Fail to find an available port on "); + } +}