diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisCancellationService.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisCancellationService.java new file mode 100644 index 0000000..73fbf81 --- /dev/null +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisCancellationService.java @@ -0,0 +1,68 @@ +/* + * SonarQube Ide VisualStudio Roslyn Plugin + * Copyright (C) 2025-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.visualstudio.roslyn; + +import org.sonar.api.Startable; +import org.sonarsource.api.sonarlint.SonarLintSide; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +@SonarLintSide(lifespan = SonarLintSide.INSTANCE) +public class AnalysisCancellationService implements Startable { + private final ScheduledExecutorService scheduledExecutorService; + + public AnalysisCancellationService() { + scheduledExecutorService = Executors.newScheduledThreadPool(5); + } + + public void registerAnalysis(AnalysisTracker analysisTracker) { + AnalysisPollingRunnable task = new AnalysisPollingRunnable(analysisTracker); + task.selfFuture = scheduledExecutorService.scheduleAtFixedRate(task, 100, 100, TimeUnit.MILLISECONDS); + } + + @Override + public void start() { + // do nothing, executor created in constructor + } + + @Override + public void stop() { + scheduledExecutorService.shutdownNow(); + } + + private static class AnalysisPollingRunnable implements Runnable{ + private final AnalysisTracker analysisCompletion; + ScheduledFuture selfFuture; + + public AnalysisPollingRunnable(AnalysisTracker analysisCompletion) { + this.analysisCompletion = analysisCompletion; + } + + @Override + public void run() { + if (analysisCompletion.cancelIfNeeded()) { + selfFuture.cancel(false); + } + } + } +} diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisTracker.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisTracker.java new file mode 100644 index 0000000..6478d88 --- /dev/null +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisTracker.java @@ -0,0 +1,29 @@ +/* + * SonarQube Ide VisualStudio Roslyn Plugin + * Copyright (C) 2025-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.visualstudio.roslyn; + +import java.io.Closeable; +import java.util.UUID; + +public interface AnalysisTracker extends Closeable { + UUID getAnalysisId(); + + boolean cancelIfNeeded(); +} diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisTrackerImpl.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisTrackerImpl.java new file mode 100644 index 0000000..c307fac --- /dev/null +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisTrackerImpl.java @@ -0,0 +1,69 @@ +/* + * SonarQube Ide VisualStudio Roslyn Plugin + * Copyright (C) 2025-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.visualstudio.roslyn; + +import org.sonar.api.batch.sensor.SensorContext; +import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler; + +import java.util.UUID; + +public class AnalysisTrackerImpl implements AnalysisTracker { + private final UUID analysisId; + private boolean isCompleted; + private SensorContext sensorContext; + private final HttpAnalysisRequestHandler handler; + + public AnalysisTrackerImpl(SensorContext sensorContext, HttpAnalysisRequestHandler handler, AnalysisCancellationService analysisCancellationService) { + this.sensorContext = sensorContext; + this.handler = handler; + this.analysisId = UUID.randomUUID(); + analysisCancellationService.registerAnalysis(this); + } + + @Override + public UUID getAnalysisId() { + return analysisId; + } + + @Override + public synchronized boolean cancelIfNeeded() { + if (isCompleted) { + return true; + } + + if (sensorContext.isCancelled()) { + handler.cancelAnalysis(analysisId); + setCompletedState(); + return true; + } + + return false; + } + + @Override + public synchronized void close() { + setCompletedState(); + } + + private synchronized void setCompletedState() { + isCompleted = true; + sensorContext = null; + } +} diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/RemoteAnalysisService.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/RemoteAnalysisService.java new file mode 100644 index 0000000..5b7deb8 --- /dev/null +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/RemoteAnalysisService.java @@ -0,0 +1,57 @@ +/* + * SonarQube Ide VisualStudio Roslyn Plugin + * Copyright (C) 2025-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.visualstudio.roslyn; + +import org.sonar.api.batch.rule.ActiveRule; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonarsource.api.sonarlint.SonarLintSide; +import org.sonarsource.sonarlint.visualstudio.roslyn.http.AnalyzerInfoDto; +import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler; +import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssue; + +import java.util.Collection; +import java.util.Map; + +@SonarLintSide +public class RemoteAnalysisService { + + private final AnalysisCancellationService analysisCancellationService; + private final HttpAnalysisRequestHandler httpAnalysisRequestHandler; + private final SensorContext sensorContext; + + public RemoteAnalysisService( + AnalysisCancellationService analysisCancellationService, + HttpAnalysisRequestHandler httpAnalysisRequestHandler, + SensorContext sensorContext) { + this.analysisCancellationService = analysisCancellationService; + this.httpAnalysisRequestHandler = httpAnalysisRequestHandler; + this.sensorContext = sensorContext; + } + + public Collection analyze( + Collection inputFiles, + Collection activeRules, + Map analysisProperties, + AnalyzerInfoDto analyzerInfo) { + try (var tracker = new AnalysisTrackerImpl(sensorContext, httpAnalysisRequestHandler, analysisCancellationService)) { + return httpAnalysisRequestHandler.analyze(inputFiles, activeRules, analysisProperties, analyzerInfo, tracker.getAnalysisId()); + } + } +} diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynPlugin.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynPlugin.java index 19161ee..54c65ef 100644 --- a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynPlugin.java +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynPlugin.java @@ -23,6 +23,7 @@ import org.sonar.api.SonarProduct; import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler; import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpClientHandler; +import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpClientProvider; import org.sonarsource.sonarlint.visualstudio.roslyn.http.JsonRequestBuilder; public class SqvsRoslynPlugin implements Plugin { @@ -33,9 +34,12 @@ public void define(Context context) { context.addExtensions( SqvsRoslynSensor.class, JsonRequestBuilder.class, + HttpClientProvider.class, HttpClientHandler.class, + AnalysisCancellationService.class, InstanceConfigurationProvider.class, AnalysisPropertiesProvider.class, + RemoteAnalysisService.class, HttpAnalysisRequestHandler.class); } diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynSensor.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynSensor.java index 5651016..1ad1155 100644 --- a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynSensor.java +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynSensor.java @@ -36,7 +36,6 @@ import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonarsource.sonarlint.visualstudio.roslyn.http.AnalyzerInfoDto; -import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler; import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssue; import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssueLocation; import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssueQuickFix; @@ -44,15 +43,17 @@ public class SqvsRoslynSensor implements Sensor { private static final Logger LOG = Loggers.get(SqvsRoslynSensor.class); - private final HttpAnalysisRequestHandler httpRequestHandler; private final InstanceConfigurationProvider instanceConfigurationProvider; private final AnalysisPropertiesProvider analysisPropertiesProvider; + private final RemoteAnalysisService remoteAnalysisService; - public SqvsRoslynSensor(HttpAnalysisRequestHandler httpRequestHandler, InstanceConfigurationProvider instanceConfigurationProvider, - AnalysisPropertiesProvider analysisPropertiesProvider) { - this.httpRequestHandler = httpRequestHandler; + public SqvsRoslynSensor( + InstanceConfigurationProvider instanceConfigurationProvider, + AnalysisPropertiesProvider analysisPropertiesProvider, + RemoteAnalysisService remoteAnalysisService) { this.instanceConfigurationProvider = instanceConfigurationProvider; this.analysisPropertiesProvider = analysisPropertiesProvider; + this.remoteAnalysisService = remoteAnalysisService; } private static void handle(SensorContext context, RoslynIssue roslynIssue) { @@ -140,7 +141,7 @@ private void analyze(SensorContext context, FilePredicate predicate) { var activeRules = getActiveRules(context); var analysisProperties = analysisPropertiesProvider.getAnalysisProperties(); var analyzerInfo = getAnalyzerInfo(); - var roslynIssues = httpRequestHandler.analyze(inputFiles, activeRules, analysisProperties, analyzerInfo); + var roslynIssues = remoteAnalysisService.analyze(inputFiles, activeRules, analysisProperties, analyzerInfo); for (var roslynIssue : roslynIssues) { try { handle(context, roslynIssue); @@ -148,6 +149,7 @@ private void analyze(SensorContext context, FilePredicate predicate) { LOG.error(String.format("Issue %s can not be saved due to ", roslynIssue.getRuleId()), exception.fillInStackTrace()); } } + } private Collection getFilePaths(SensorContext context, FilePredicate predicate) { diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/AnalysisRequestDto.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/AnalysisRequestDto.java index 0723324..a0aeff7 100644 --- a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/AnalysisRequestDto.java +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/AnalysisRequestDto.java @@ -23,7 +23,11 @@ import java.util.Collection; import java.util.Map; -public record AnalysisRequestDto(@SerializedName("FileNames") Collection fileNames, @SerializedName("ActiveRules") Collection activeRules, - @SerializedName("AnalysisProperties") Map analysisProperties, @SerializedName("AnalyzerInfo") AnalyzerInfoDto analyzerInfo) { - +public record AnalysisRequestDto( + @SerializedName("FileNames") Collection fileNames, + @SerializedName("ActiveRules") Collection activeRules, + @SerializedName("AnalysisProperties") Map analysisProperties, + @SerializedName("AnalyzerInfo") AnalyzerInfoDto analyzerInfo, + @SerializedName("AnalysisId") java.util.UUID analysisId) { } + diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/CancellationRequestDto.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/CancellationRequestDto.java new file mode 100644 index 0000000..d5af53d --- /dev/null +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/CancellationRequestDto.java @@ -0,0 +1,25 @@ +/* + * SonarQube Ide VisualStudio Roslyn Plugin + * Copyright (C) 2025-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.visualstudio.roslyn.http; + +import com.google.gson.annotations.SerializedName; + +public record CancellationRequestDto(@SerializedName("AnalysisId") java.util.UUID analysisId) { +} diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpAnalysisRequestHandler.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpAnalysisRequestHandler.java index 57e20b2..4ad8e49 100644 --- a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpAnalysisRequestHandler.java +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpAnalysisRequestHandler.java @@ -24,27 +24,32 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Map; +import java.util.UUID; + import org.sonar.api.batch.rule.ActiveRule; -import org.sonar.api.scanner.ScannerSide; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonarsource.api.sonarlint.SonarLintSide; import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssue; -@ScannerSide @SonarLintSide public class HttpAnalysisRequestHandler { private static final Logger LOG = Loggers.get(HttpAnalysisRequestHandler.class); - private final HttpClientHandler httpClientFactory; + private final HttpClientHandler httpClientHandler; - public HttpAnalysisRequestHandler(HttpClientHandler httpClientFactory) { - this.httpClientFactory = httpClientFactory; + public HttpAnalysisRequestHandler(HttpClientHandler httpClientHandler) { + this.httpClientHandler = httpClientHandler; } - public Collection analyze(Collection fileNames, Collection activeRules, Map analysisProperties, AnalyzerInfoDto analyzerInfo) { + public Collection analyze( + Collection fileNames, + Collection activeRules, + Map analysisProperties, + AnalyzerInfoDto analyzerInfo, + UUID analysisId) { Collection roslynIssues = new ArrayList<>(); try { - var response = httpClientFactory.sendRequest(fileNames, activeRules, analysisProperties, analyzerInfo); + var response = httpClientHandler.sendAnalyzeRequest(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId); if (response.statusCode() != HttpURLConnection.HTTP_OK) { LOG.error("Response from server is {}.", response.statusCode()); return roslynIssues; @@ -66,4 +71,18 @@ public Collection analyze(Collection fileNames, Collection< return roslynIssues; } + + public void cancelAnalysis(UUID analysisId) { + var requestFuture = httpClientHandler.sendCancelRequest(analysisId); + + requestFuture.exceptionally(e -> { + LOG.error("Failed to cancel analysis due to: {}", e.getMessage(), e); + return null; + }).thenApply(response -> { + if (response != null && response.statusCode() != HttpURLConnection.HTTP_OK) { + LOG.error("Response from cancel request is {}.", response.statusCode()); + } + return null; + }); + } } diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientHandler.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientHandler.java index af28761..cefe213 100644 --- a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientHandler.java +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientHandler.java @@ -21,42 +21,53 @@ import java.io.IOException; import java.net.URI; -import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.Collection; import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + import org.sonar.api.batch.rule.ActiveRule; import org.sonar.api.batch.sensor.SensorContext; -import org.sonar.api.scanner.ScannerSide; import org.sonarsource.api.sonarlint.SonarLintSide; import org.sonarsource.sonarlint.visualstudio.roslyn.SqvsRoslynPluginPropertyDefinitions; -@ScannerSide @SonarLintSide public class HttpClientHandler { private final SensorContext context; private final JsonRequestBuilder jsonRequestBuilder; - private final HttpClient client; + private final java.net.http.HttpClient httpClient; - public HttpClientHandler(SensorContext context, JsonRequestBuilder jsonRequestBuilder) { + public HttpClientHandler(SensorContext context, JsonRequestBuilder jsonRequestBuilder, HttpClientProvider httpClientProvider) { this.context = context; this.jsonRequestBuilder = jsonRequestBuilder; - client = HttpClient.newHttpClient(); + this.httpClient = httpClientProvider.getHttpClient(); + } + + public CompletableFuture> sendCancelRequest(UUID analysisId){ + var payload = jsonRequestBuilder.buildCancelBody(analysisId); + var request = createRequest(payload, "cancel"); + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()); } - public HttpResponse sendRequest(Collection fileNames, Collection activeRules, Map analysisProperties, AnalyzerInfoDto analyzerInfo) + public HttpResponse sendAnalyzeRequest( + Collection fileNames, + Collection activeRules, + Map analysisProperties, + AnalyzerInfoDto analyzerInfo, + UUID analysisId) throws IOException, InterruptedException { - var jsonPayload = jsonRequestBuilder.buildBody(fileNames, activeRules, analysisProperties, analyzerInfo); - var request = createRequest(jsonPayload); - return client.send(request, HttpResponse.BodyHandlers.ofString()); + var jsonPayload = jsonRequestBuilder.buildAnalyzeBody(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId); + var request = createRequest(jsonPayload, "analyze"); + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); } - public HttpRequest createRequest(String jsonPayload) { + public HttpRequest createRequest(String jsonPayload, String path) { var settings = context.settings(); var port = settings.getString(SqvsRoslynPluginPropertyDefinitions.getServerPort()); var token = settings.getString(SqvsRoslynPluginPropertyDefinitions.getServerToken()); - var uri = String.format("http://localhost:%s/analyze", port); + var uri = String.format("http://localhost:%s/%s", port, path); return HttpRequest.newBuilder() .uri(URI.create(uri)) .header("Content-Type", "application/json") diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientProvider.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientProvider.java new file mode 100644 index 0000000..8dd55c8 --- /dev/null +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientProvider.java @@ -0,0 +1,38 @@ +/* + * SonarQube Ide VisualStudio Roslyn Plugin + * Copyright (C) 2025-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.visualstudio.roslyn.http; + +import org.sonarsource.api.sonarlint.SonarLintSide; + +import java.net.http.HttpClient; + +@SonarLintSide(lifespan = SonarLintSide.INSTANCE) +public class HttpClientProvider { + + private final HttpClient httpClient; + + public HttpClientProvider() { + httpClient = HttpClient.newHttpClient(); + } + + public HttpClient getHttpClient() { + return httpClient; + } +} diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/JsonRequestBuilder.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/JsonRequestBuilder.java index 148ed10..efa9f3e 100644 --- a/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/JsonRequestBuilder.java +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/JsonRequestBuilder.java @@ -22,24 +22,35 @@ import com.google.gson.Gson; import java.util.Collection; import java.util.Map; +import java.util.UUID; + import org.sonar.api.batch.rule.ActiveRule; -import org.sonar.api.scanner.ScannerSide; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonarsource.api.sonarlint.SonarLintSide; -@ScannerSide @SonarLintSide(lifespan = "INSTANCE") public class JsonRequestBuilder { private static final Logger LOG = Loggers.get(JsonRequestBuilder.class); - public String buildBody(Collection fileNames, Collection activeRules, Map analysisProperties, AnalyzerInfoDto analyzerInfo) { + public String buildAnalyzeBody( + Collection fileNames, + Collection activeRules, + Map analysisProperties, + AnalyzerInfoDto analyzerInfo, + UUID analysisId) { var activeRuleDtos = activeRules.stream() .map(rule -> new ActiveRuleDto( rule.ruleKey().toString(), rule.params())) .toList(); - var analysisRequest = new AnalysisRequestDto(fileNames, activeRuleDtos, analysisProperties, analyzerInfo); + var analysisRequest = new AnalysisRequestDto(fileNames, activeRuleDtos, analysisProperties, analyzerInfo, analysisId); + + return new Gson().toJson(analysisRequest); + } + + public String buildCancelBody(UUID analysisId) { + var analysisRequest = new CancellationRequestDto(analysisId); return new Gson().toJson(analysisRequest); } diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisCancellationServiceTest.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisCancellationServiceTest.java new file mode 100644 index 0000000..dc0d25c --- /dev/null +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisCancellationServiceTest.java @@ -0,0 +1,91 @@ +/* + * SonarQube Ide VisualStudio Roslyn Plugin + * Copyright (C) 2025-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.visualstudio.roslyn; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class AnalysisCancellationServiceTest { + private AnalysisTracker createMockTrackerWithLatch(CountDownLatch latch) { + var mockTracker = Mockito.mock(AnalysisTrackerImpl.class); + when(mockTracker.cancelIfNeeded()).thenAnswer(invocation -> { + latch.countDown(); + return latch.getCount() == 0; + }); + return mockTracker; + } + + @Test + void testAnalysisPollingWhenCancellationNeeded() throws InterruptedException { + var service = new AnalysisCancellationService(); + final var latch = new CountDownLatch(3); + var mockTracker = createMockTrackerWithLatch(latch); + + try { + service.registerAnalysis(mockTracker); + assertTrue(latch.await(400, TimeUnit.MILLISECONDS)); + verify(mockTracker, times(3)).cancelIfNeeded(); + } finally { + service.stop(); + } + } + + @Test + void testMultipleAnalysisRegistration() throws InterruptedException { + var service = new AnalysisCancellationService(); + + final var latch1 = new CountDownLatch(1); + final var latch2 = new CountDownLatch(1); + + var mockTracker1 = createMockTrackerWithLatch(latch1); + var mockTracker2 = createMockTrackerWithLatch(latch2); + + try { + service.registerAnalysis(mockTracker1); + service.registerAnalysis(mockTracker2); + + assertTrue(latch1.await(200, TimeUnit.MILLISECONDS)); + assertTrue(latch2.await(200, TimeUnit.MILLISECONDS)); + + verify(mockTracker1, atLeastOnce()).cancelIfNeeded(); + verify(mockTracker2, atLeastOnce()).cancelIfNeeded(); + } finally { + service.stop(); + } + } + + @Test + void testRegisterAnalysisAfterStop() throws InterruptedException { + var service = new AnalysisCancellationService(); + service.stop(); + final var latch = new CountDownLatch(Integer.MAX_VALUE); + var mockTracker = createMockTrackerWithLatch(latch); + + assertThrows(RejectedExecutionException.class, () -> service.registerAnalysis(mockTracker)); + assertFalse(latch.await(300, TimeUnit.MILLISECONDS)); + } +} diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisTrackerTest.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisTrackerTest.java new file mode 100644 index 0000000..58f557f --- /dev/null +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/AnalysisTrackerTest.java @@ -0,0 +1,99 @@ +/* + * SonarQube Ide VisualStudio Roslyn Plugin + * Copyright (C) 2025-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.visualstudio.roslyn; + +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +class AnalysisTrackerTest { + private SensorContext sensorContext; + private HttpAnalysisRequestHandler handler; + private AnalysisCancellationService analysisCancellationService; + private AnalysisTrackerImpl underTest; + + @BeforeEach + void setUp() { + sensorContext = mock(SensorContext.class); + handler = mock(HttpAnalysisRequestHandler.class); + analysisCancellationService = mock(AnalysisCancellationService.class); + underTest = new AnalysisTrackerImpl(sensorContext, handler, analysisCancellationService); + } + + @Test + void shouldRegisterWithCancellationService() { + verify(analysisCancellationService).registerAnalysis(underTest); + } + + @Test + void shouldGenerateRandomAnalysisId() { + UUID analysisId = underTest.getAnalysisId(); + assertThat(analysisId).isNotNull(); + } + + @Test + void cancelIfNeeded_shouldReturnTrueWhenAlreadyCompleted() { + underTest.close(); + + boolean result = underTest.cancelIfNeeded(); + + assertThat(result).isTrue(); + verifyNoInteractions(handler); + verifyNoInteractions(sensorContext); + } + + @Test + void cancelIfNeeded_shouldReturnTrueWhenSensorContextIsCancelled() { + when(sensorContext.isCancelled()).thenReturn(true); + + boolean result = underTest.cancelIfNeeded(); + + assertThat(result).isTrue(); + verify(handler).cancelAnalysis(underTest.getAnalysisId()); + } + + @Test + void cancelIfNeeded_shouldReturnFalseWhenAnalysisIsNotCancelled() { + when(sensorContext.isCancelled()).thenReturn(false); + + boolean result = underTest.cancelIfNeeded(); + + assertThat(result).isFalse(); + verifyNoInteractions(handler); + } + + @Test + void cancelIfNeeded_shouldOnlyCancelOnce() { + when(sensorContext.isCancelled()).thenReturn(true); + + boolean firstResult = underTest.cancelIfNeeded(); + assertThat(firstResult).isTrue(); + verify(handler).cancelAnalysis(underTest.getAnalysisId()); + + boolean secondResult = underTest.cancelIfNeeded(); + assertThat(secondResult).isTrue(); + verify(handler, times(1)).cancelAnalysis(any()); + } +} diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/RemoteAnalysisServiceTest.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/RemoteAnalysisServiceTest.java new file mode 100644 index 0000000..6d747f2 --- /dev/null +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/RemoteAnalysisServiceTest.java @@ -0,0 +1,93 @@ +/* + * SonarQube Ide VisualStudio Roslyn Plugin + * Copyright (C) 2025-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.visualstudio.roslyn; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.sonar.api.batch.rule.ActiveRule; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonarsource.sonarlint.visualstudio.roslyn.http.AnalyzerInfoDto; +import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler; +import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssue; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class RemoteAnalysisServiceTest { + + private AnalysisCancellationService analysisCancellationService; + private HttpAnalysisRequestHandler httpAnalysisRequestHandler; + private RemoteAnalysisService underTest; + + private final Collection fileNames = List.of("File1.cs", "File2.cs"); + private final Collection activeRules = List.of(mock(ActiveRule.class)); + private final Map analysisProperties = Map.of("sonar.cs.disableRazor", "true"); + private final AnalyzerInfoDto analyzerInfo = new AnalyzerInfoDto(false, false); + + @BeforeEach + void setUp() { + analysisCancellationService = mock(AnalysisCancellationService.class); + httpAnalysisRequestHandler = mock(HttpAnalysisRequestHandler.class); + var sensorContext = mock(SensorContext.class); + + underTest = new RemoteAnalysisService( + analysisCancellationService, + httpAnalysisRequestHandler, + sensorContext); + } + + @Test + void analyze_ShouldReturnRoslynIssues() { + var mockIssues = mockIssues(); + + var result = underTest.analyze( + fileNames, + activeRules, + analysisProperties, + analyzerInfo); + + assertSame(mockIssues, result); + verify(httpAnalysisRequestHandler).analyze( + eq(fileNames), + eq(activeRules), + eq(analysisProperties), + eq(analyzerInfo), + any(UUID.class)); + verify(analysisCancellationService).registerAnalysis(any(AnalysisTrackerImpl.class)); + } + + private List mockIssues() { + var mockIssue = mock(RoslynIssue.class); + var expectedIssues = List.of(mockIssue); + when(httpAnalysisRequestHandler.analyze( + any(), any(), any(), any(), any(UUID.class))) + .thenReturn(expectedIssues); + return expectedIssues; + } +} diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynPluginTests.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynPluginTests.java index 3032b95..c221d57 100644 --- a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynPluginTests.java +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynPluginTests.java @@ -31,7 +31,7 @@ class SqvsRoslynPluginTests { private static final int PROPERTY_DEFINITIONS_COUNT = 6; - private static final int REGISTERED_CLASSES_COUNT = 8; + private static final int REGISTERED_CLASSES_COUNT = 11; @Test void getExtensions() { diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynSensorTests.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynSensorTests.java index bda36cb..987d425 100644 --- a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynSensorTests.java +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynSensorTests.java @@ -62,7 +62,7 @@ class SqvsRoslynSensorTests { private final NewActiveRule vbActiveRule = new NewActiveRule.Builder().setRuleKey(RuleKey.of(VbNetLanguage.REPOSITORY_KEY, "S456")).build(); @RegisterExtension LogTesterJUnit5 logTester = new LogTesterJUnit5(); - private HttpAnalysisRequestHandler analysisRequestHandler; + private RemoteAnalysisService remoteAnalysisService; private InstanceConfigurationProvider instanceConfigurationProvider; private AnalysisPropertiesProvider analysisPropertiesProvider; private SensorContextTester sensorContext; @@ -96,12 +96,12 @@ static Stream notAllowedFilesForAnalysis() { @BeforeEach void prepare(@TempDir Path tmp) throws Exception { - analysisRequestHandler = mock(HttpAnalysisRequestHandler.class); + remoteAnalysisService = mock(RemoteAnalysisService.class); analysisPropertiesProvider = mock(AnalysisPropertiesProvider.class); instanceConfigurationProvider = mock(InstanceConfigurationProvider.class); baseDir = tmp.toRealPath(); sensorContext = SensorContextTester.create(baseDir); - underTest = new SqvsRoslynSensor(analysisRequestHandler, instanceConfigurationProvider, analysisPropertiesProvider); + underTest = new SqvsRoslynSensor(instanceConfigurationProvider, analysisPropertiesProvider, remoteAnalysisService); csFile = createInputFile("foo.cs", "var a=1;", CSharpLanguage.LANGUAGE_KEY); csFile2 = createInputFile("foo2.cs", "var b=2;", CSharpLanguage.LANGUAGE_KEY); vbFile = createInputFile("boo.vb", "Dim a As Integer = 1", VbNetLanguage.LANGUAGE_KEY); @@ -125,7 +125,7 @@ void describe() { void noopIfNoFiles() { underTest.execute(sensorContext); - verifyNoInteractions(analysisRequestHandler); + verifyNoInteractions(remoteAnalysisService); } @ParameterizedTest @@ -136,7 +136,7 @@ void analyzeCsharpAndVbNetAndRazor_executesAnalysis(String fileName, String lang underTest.execute(sensorContext); - verify(analysisRequestHandler).analyze(argThat(fileNames -> fileNames.stream().anyMatch(f -> f.contains(fileName))), + verify(remoteAnalysisService).analyze(argThat(fileNames -> fileNames.stream().anyMatch(f -> f.contains(fileName))), anyList(), any(), any()); } @@ -148,7 +148,7 @@ void analyzeOtherLanguages_doesNotExecuteAnalysis(String fileName, String langua underTest.execute(sensorContext); - verifyNoInteractions(analysisRequestHandler); + verifyNoInteractions(remoteAnalysisService); } @ParameterizedTest @@ -165,7 +165,7 @@ void analyzeCsFile_callsHttpRequestWithCorrectParameters(boolean expectedShouldU underTest.execute(sensorContext); - verify(analysisRequestHandler).analyze(argThat(fileNames -> fileNames.stream().anyMatch(file -> file.contains(fileName))), + verify(remoteAnalysisService).analyze(argThat(fileNames -> fileNames.stream().anyMatch(file -> file.contains(fileName))), argThat(activeRules -> activeRules.size() == 1 && activeRules.stream().findFirst().get().ruleKey().rule().equals("S123")), argThat(x -> x.entrySet().contains(Map.entry("sonar.cs.disableRazor", "true"))), argThat(x -> x.shouldUseCsharpEnterprise() == expectedShouldUseCsharpEnterprise && !x.shouldUseVbEnterprise())); @@ -185,7 +185,7 @@ void analyzeVbNetFile_callsHttpRequestWithCorrectParameters(boolean expectedShou underTest.execute(sensorContext); - verify(analysisRequestHandler).analyze(argThat(fileNames -> fileNames.stream().anyMatch(file -> file.contains(fileName))), + verify(remoteAnalysisService).analyze(argThat(fileNames -> fileNames.stream().anyMatch(file -> file.contains(fileName))), argThat(activeRules -> activeRules.size() == 1 && activeRules.stream().findFirst().get().ruleKey().rule().equals("S456")), argThat(x -> x.entrySet().contains(Map.entry("sonar.vbnet.disableRazor", "false"))), argThat(x -> x.shouldUseVbEnterprise() == expectedShouldUseVbEnterprise && !x.shouldUseCsharpEnterprise())); @@ -204,7 +204,7 @@ void analyzeCsAndVbNetFiles_callsHttpRequestWithCorrectParameters() throws Excep underTest.execute(sensorContext); - verify(analysisRequestHandler).analyze(argThat(fileNames -> fileNames.size() == 2 && + verify(remoteAnalysisService).analyze(argThat(fileNames -> fileNames.size() == 2 && fileNames.stream().anyMatch(file -> file.contains("foo.cs") || file.contains("boo.vb"))), argThat(activeRules -> activeRules.size() == 2 && activeRules.stream().anyMatch(rule -> rule.ruleKey().rule().equals("S123") || rule.ruleKey().rule().contains("S456"))), @@ -216,7 +216,7 @@ void analyzeCsAndVbNetFiles_callsHttpRequestWithCorrectParameters() throws Excep void analyzeCs_reportIssueForActiveRules() { sensorContext.fileSystem().add(csFile); sensorContext.setActiveRules(new ActiveRulesBuilder().addRule(csActiveRule).build()); - when(analysisRequestHandler.analyze( + when(remoteAnalysisService.analyze( argThat(x -> x.stream().anyMatch(file -> file.contains(csFile.filename()))), argThat(x -> x.stream().anyMatch(cs -> cs.ruleKey().rule().contains(csActiveRule.ruleKey().rule()))), any(), @@ -234,7 +234,7 @@ void analyzeVb_reportIssueForActiveRules() { sensorContext.setActiveRules(new ActiveRulesBuilder() .addRule(vbActiveRule) .build()); - when(analysisRequestHandler.analyze( + when(remoteAnalysisService.analyze( argThat(x -> x.stream().anyMatch(file -> file.contains(vbFile.filename()))), argThat(x -> x.stream().anyMatch(cs -> cs.ruleKey().rule().contains(vbActiveRule.ruleKey().rule()))), any(), @@ -252,7 +252,7 @@ void analyzeCs_handleSecondaryLocations() { sensorContext.fileSystem().add(csFile2); sensorContext.setActiveRules(new ActiveRulesBuilder().addRule(csActiveRule).build()); var csIssueWithSecondaryLocations = mockRoslynIssueWithSecondaryLocations(csActiveRule.ruleKey().rule(), CSharpLanguage.REPOSITORY_KEY, csFile.filename(), csFile2.filename()); - when(analysisRequestHandler.analyze( + when(remoteAnalysisService.analyze( argThat(x -> x.stream().anyMatch(file -> file.contains(csFile.filename()))), argThat(x -> x.stream().anyMatch(cs -> cs.ruleKey().rule().contains(csActiveRule.ruleKey().rule()))), any(), @@ -275,7 +275,7 @@ void analyzeVb_handleSecondaryLocations() { sensorContext.fileSystem().add(vbFile2); sensorContext.setActiveRules(new ActiveRulesBuilder().addRule(vbActiveRule).build()); var vbIssueWithSecondaryLocations = mockRoslynIssueWithSecondaryLocations(vbActiveRule.ruleKey().rule(), VbNetLanguage.REPOSITORY_KEY, vbFile.filename(), vbFile2.filename()); - when(analysisRequestHandler.analyze( + when(remoteAnalysisService.analyze( argThat(x -> x.stream().anyMatch(file -> file.contains(vbFile.filename()))), argThat(x -> x.stream().anyMatch(cs -> cs.ruleKey().rule().contains(vbActiveRule.ruleKey().rule()))), any(), @@ -299,7 +299,7 @@ void analyze_handleOneIssueThrowsException_logsAndShowsOtherIssues() { .addRule(vbActiveRule) .build()); var vbWrongIssue = mockRoslynIssueWithWrongLocation(vbActiveRule.ruleKey().rule(), VbNetLanguage.REPOSITORY_KEY, vbFile.filename()); - when(analysisRequestHandler.analyze(any(), any(), any(), any())) + when(remoteAnalysisService.analyze(any(), any(), any(), any())) .thenReturn(List.of(vbIssue, vbWrongIssue)); underTest.execute(sensorContext); @@ -316,12 +316,12 @@ private void testQuickFixes(InputFile testFile, NewActiveRule activeRule, String sensorContext.fileSystem().add(testFile); sensorContext.setActiveRules(new ActiveRulesBuilder().addRule(activeRule).build()); var csIssueWithQuickFix = mockRoslynIssueWithQuickFixes(activeRule.ruleKey().rule(), languageRepositoryKey, testFileName, "Custom QuickFix value provided by RoslynIssue"); - when(analysisRequestHandler.analyze( + when(remoteAnalysisService.analyze( argThat(x -> x.stream().anyMatch(file -> file.contains(testFileName))), argThat(x -> x.stream().anyMatch(cs -> cs.ruleKey().rule().contains(activeRule.ruleKey().rule()))), any(), argThat(x -> !x.shouldUseCsharpEnterprise() && !x.shouldUseVbEnterprise()))) - .thenReturn(List.of(csIssueWithQuickFix)); + .thenReturn(List.of(csIssueWithQuickFix)); underTest.execute(sensorContext); diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpAnalysisRequestHandlerTests.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpAnalysisRequestHandlerTests.java index b09015e..4113e37 100644 --- a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpAnalysisRequestHandlerTests.java +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpAnalysisRequestHandlerTests.java @@ -24,6 +24,9 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -43,6 +46,7 @@ class HttpAnalysisRequestHandlerTests { private final Collection activeRules = List.of(mock(ActiveRule.class)); private final Map analysisProperties = Map.of("sonar.cs.disableRazor", "true"); private final AnalyzerInfoDto analyzerInfo = new AnalyzerInfoDto(false, false); + private final UUID analysisId = UUID.randomUUID(); @RegisterExtension private final LogTesterJUnit5 logTester = new LogTesterJUnit5(); private HttpClientHandler httpClientHandler; @@ -58,17 +62,17 @@ void init() { void analyze_requestSucceeds_ReturnsIssues() throws IOException, InterruptedException { mockResponseWithOneIssue(200); - var result = analysisRequestHandler.analyze(fileNames, activeRules, analysisProperties, analyzerInfo); + var result = analysisRequestHandler.analyze(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId); assertThat(result).hasSize(1); - verify(httpClientHandler).sendRequest(fileNames, activeRules, analysisProperties, analyzerInfo); + verify(httpClientHandler).sendAnalyzeRequest(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId); } @Test void analyze_requestSucceedsWithEmptyBody_logsAndReturnsEmptyIssues() throws IOException, InterruptedException { mockResponse(200, ""); - var result = analysisRequestHandler.analyze(fileNames, activeRules, analysisProperties, analyzerInfo); + var result = analysisRequestHandler.analyze(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId); assertThat(result).isEmpty(); assertThat(logTester.logs(LoggerLevel.WARN)).contains("No body received from the server."); @@ -78,23 +82,61 @@ void analyze_requestSucceedsWithEmptyBody_logsAndReturnsEmptyIssues() throws IOE void analyze_requestFails_returnsEmptyIssues() throws IOException, InterruptedException { mockResponseWithOneIssue(404); - var result = analysisRequestHandler.analyze(fileNames, activeRules, analysisProperties, analyzerInfo); + var result = analysisRequestHandler.analyze(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId); assertThat(result).isEmpty(); - verify(httpClientHandler).sendRequest(fileNames, activeRules, analysisProperties, analyzerInfo); + verify(httpClientHandler).sendAnalyzeRequest(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId); assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Response from server is 404."); } @Test void analyze_throws_logsAndReturnsEmptyIssues() throws IOException, InterruptedException { var exceptionMessage = "message"; - when(httpClientHandler.sendRequest(fileNames, activeRules, analysisProperties, analyzerInfo)).thenThrow(new RuntimeException(exceptionMessage)); + when(httpClientHandler.sendAnalyzeRequest(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId)).thenThrow(new RuntimeException(exceptionMessage)); - var thrown = assertThrows(IllegalStateException.class, () -> analysisRequestHandler.analyze(fileNames, activeRules, analysisProperties, analyzerInfo)); + var thrown = assertThrows(IllegalStateException.class, () -> analysisRequestHandler.analyze(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId)); assertThat(thrown).hasMessageContaining("Response crashed due to: " + exceptionMessage); } + @Test + void cancelAnalysis_shouldSendCancelRequest() { + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + CompletableFuture> future = CompletableFuture.completedFuture(mockResponse); + when(httpClientHandler.sendCancelRequest(analysisId)).thenReturn(future); + + analysisRequestHandler.cancelAnalysis(analysisId); + + verify(httpClientHandler).sendCancelRequest(analysisId); + } + + @Test + void cancelAnalysis_shouldHandleExceptions() { + var exceptionMessage = "Connection error"; + CompletableFuture> future = new CompletableFuture<>(); + future.completeExceptionally(new RuntimeException(exceptionMessage)); + when(httpClientHandler.sendCancelRequest(analysisId)).thenReturn(future); + + analysisRequestHandler.cancelAnalysis(analysisId); + + verify(httpClientHandler).sendCancelRequest(analysisId); + assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Failed to cancel analysis due to: " + exceptionMessage); + } + + @Test + void cancelAnalysis_shouldHandleNonOkStatusCode() { + HttpResponse mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(404); + CompletableFuture> future = CompletableFuture.completedFuture(mockResponse); + when(httpClientHandler.sendCancelRequest(analysisId)).thenReturn(future); + + analysisRequestHandler.cancelAnalysis(analysisId); + + verify(httpClientHandler).sendCancelRequest(analysisId); + assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Response from cancel request is 404."); + } + private void mockResponseWithOneIssue(int statusCode) throws IOException, InterruptedException { mockResponse(statusCode, "{\"RoslynIssues\":[{\"RuleId\":\"S100\"}]}"); } @@ -103,6 +145,6 @@ private void mockResponse(int statusCode, String body) throws IOException, Inter var mockResponse = mock(HttpResponse.class); when(mockResponse.statusCode()).thenReturn(statusCode); when(mockResponse.body()).thenReturn(body); - when(httpClientHandler.sendRequest(fileNames, activeRules, analysisProperties, analyzerInfo)).thenReturn(mockResponse); + when(httpClientHandler.sendAnalyzeRequest(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId)).thenReturn(mockResponse); } } diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientHandlerTests.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientHandlerTests.java index 9a07200..f9d12c5 100644 --- a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientHandlerTests.java +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientHandlerTests.java @@ -20,10 +20,13 @@ package org.sonarsource.sonarlint.visualstudio.roslyn.http; import java.io.IOException; +import java.net.http.HttpClient; import java.net.http.HttpHeaders; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.UUID; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.sonar.api.batch.rule.ActiveRule; @@ -33,26 +36,36 @@ import org.sonarsource.sonarlint.visualstudio.roslyn.SqvsRoslynPluginPropertyDefinitions; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class HttpClientHandlerTests { private SensorContext sensorContext; + private JsonRequestBuilder jsonRequestBuilder; + private HttpClient httpClient; + private HttpClientHandler underTest; @BeforeEach void init() { sensorContext = mock(SensorContext.class); + mockSettings("60000", "myToken"); + jsonRequestBuilder = mock(JsonRequestBuilder.class); + when(jsonRequestBuilder.buildAnalyzeBody(any(), any(), any(), any(), any())).thenReturn(""); + when(jsonRequestBuilder.buildCancelBody(any())).thenReturn(""); + HttpClientProvider httpClientProvider = mock(HttpClientProvider.class); + httpClient = mock(HttpClient.class); + when(httpClientProvider.getHttpClient()).thenReturn(httpClient); + underTest = new HttpClientHandler(sensorContext, jsonRequestBuilder, httpClientProvider); } @Test void createRequest_setsUriAsExpected() { - mockSettings("60000", "myToken"); - var httpClientHandler = new HttpClientHandler(sensorContext, mock(JsonRequestBuilder.class)); + var result = underTest.createRequest("", "myuri"); - var result = httpClientHandler.createRequest(""); - - assertThat(result.uri().toString()).hasToString("http://localhost:60000/analyze"); + assertThat(result.uri().toString()).hasToString("http://localhost:60000/myuri"); assertThat(result.method()).isEqualTo("POST"); HttpHeaders headers = result.headers(); assertThat(headers.firstValue("Content-Type").get()).hasToString("application/json"); @@ -60,21 +73,28 @@ void createRequest_setsUriAsExpected() { } @Test - void sendRequest_callsParserWithExpectedParameters() throws IOException, InterruptedException { - JsonRequestBuilder myMock = mock(JsonRequestBuilder.class); + void sendAnalyzeRequest_callsSerializerWithExpectedParameters() throws IOException, InterruptedException { + Collection fileNames = List.of("File1.cs", "File2.cs"); Map analysisProperties = Map.of(); var analyzerInfo = new AnalyzerInfoDto(true, true); Collection activeRules = List.of(createMockActiveRule("S100")); - var httpClientHandler = new HttpClientHandler(sensorContext, myMock); + var analysisId = UUID.randomUUID(); + + underTest.sendAnalyzeRequest(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId); + + verify(jsonRequestBuilder).buildAnalyzeBody(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId); + verify(httpClient).send(argThat(httpRequest -> httpRequest.uri().toString().endsWith("/analyze")), any()); + } + + @Test + void sendCancelRequest_callsSerializerWithExpectedParameters(){ + var analysisId = UUID.randomUUID(); - try { - httpClientHandler.sendRequest(fileNames, activeRules, analysisProperties, analyzerInfo); - } catch (Exception ex) { - // expecting request to fail - } + underTest.sendCancelRequest(analysisId); - verify(myMock).buildBody(fileNames, activeRules, analysisProperties, analyzerInfo); + verify(jsonRequestBuilder).buildCancelBody(analysisId); + verify(httpClient).sendAsync(argThat(httpRequest -> httpRequest.uri().toString().endsWith("/cancel")), any()); } private ActiveRule createMockActiveRule(String ruleId) { diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientProviderTest.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientProviderTest.java new file mode 100644 index 0000000..046aff7 --- /dev/null +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpClientProviderTest.java @@ -0,0 +1,49 @@ +/* + * SonarQube Ide VisualStudio Roslyn Plugin + * Copyright (C) 2025-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.visualstudio.roslyn.http; + +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.http.HttpClient; + +class HttpClientProviderTest { + + @Test + void getHttpClient_returnsTheSameInstanceOfClient() { + var underTest = new HttpClientProvider(); + + HttpClient httpClient1 = underTest.getHttpClient(); + HttpClient httpClient2 = underTest.getHttpClient(); + + assertThat(httpClient1).isNotNull().isSameAs(httpClient2); + } + + @Test + void getHttpClient_httpClientInstanceIsNotStatic() { + var underTest1 = new HttpClientProvider(); + var underTest2 = new HttpClientProvider(); + + HttpClient httpClient1 = underTest1.getHttpClient(); + HttpClient httpClient2 = underTest2.getHttpClient(); + + assertThat(httpClient1).isNotNull().isNotSameAs(httpClient2); + } +} diff --git a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/JsonRequestBuilderTests.java b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/JsonRequestBuilderTests.java index d5953dc..190975d 100644 --- a/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/JsonRequestBuilderTests.java +++ b/sonarqube-ide-visualstudio-roslyn-plugin/src/test/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/JsonRequestBuilderTests.java @@ -41,6 +41,7 @@ class JsonRequestBuilderTests { private JsonRequestBuilder jsonParser; + private static final java.util.UUID analysisId = java.util.UUID.fromString("ed89f185-c2d6-4d03-aef1-334747e7fbdb"); @BeforeEach void init() { @@ -53,9 +54,9 @@ void buildBody_withEmptyCollections_shouldReturnValidJson() { var activeRules = new ArrayList(); var analysisProperties = Map.of(); var analyzerInfo = new AnalyzerInfoDto(false, false); - var expected = "{\"FileNames\":[],\"ActiveRules\":[],\"AnalysisProperties\":{},\"AnalyzerInfo\":{\"ShouldUseCsharpEnterprise\":false,\"ShouldUseVbEnterprise\":false}}"; + var expected = "{\"FileNames\":[],\"ActiveRules\":[],\"AnalysisProperties\":{},\"AnalyzerInfo\":{\"ShouldUseCsharpEnterprise\":false,\"ShouldUseVbEnterprise\":false},\"AnalysisId\":\"ed89f185-c2d6-4d03-aef1-334747e7fbdb\"}"; - var result = jsonParser.buildBody(fileNames, activeRules, analysisProperties, analyzerInfo); + var result = jsonParser.buildAnalyzeBody(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId); assertThat(result).isEqualTo(expected); } @@ -67,9 +68,9 @@ void buildBody_withAllParametersFilled_shouldReturnValidJson() { createMockActiveRule("S101", VbNetLanguage.REPOSITORY_KEY, new HashMap<>())); var analysisProperties = Map.of("sonar.vb.disableRazor", "false"); var analyzerInfo = new AnalyzerInfoDto(true, false); - var expected = "{\"FileNames\":[\"File1.cs\",\"File2.vb\"],\"ActiveRules\":[{\"RuleId\":\"csharpsquid:S100\",\"Parameters\":{}},{\"RuleId\":\"vbnet:S101\",\"Parameters\":{}}],\"AnalysisProperties\":{\"sonar.vb.disableRazor\":\"false\"},\"AnalyzerInfo\":{\"ShouldUseCsharpEnterprise\":true,\"ShouldUseVbEnterprise\":false}}"; + var expected = "{\"FileNames\":[\"File1.cs\",\"File2.vb\"],\"ActiveRules\":[{\"RuleId\":\"csharpsquid:S100\",\"Parameters\":{}},{\"RuleId\":\"vbnet:S101\",\"Parameters\":{}}],\"AnalysisProperties\":{\"sonar.vb.disableRazor\":\"false\"},\"AnalyzerInfo\":{\"ShouldUseCsharpEnterprise\":true,\"ShouldUseVbEnterprise\":false},\"AnalysisId\":\"ed89f185-c2d6-4d03-aef1-334747e7fbdb\"}"; - var result = jsonParser.buildBody(fileNames, activeRules, analysisProperties, analyzerInfo); + var result = jsonParser.buildAnalyzeBody(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId); assertThat(result).isEqualTo(expected); } @@ -80,9 +81,9 @@ void buildBody_withActiveRules_shouldReturnRuleId() { var activeRule = createMockActiveRule("S100", CSharpLanguage.REPOSITORY_KEY, new HashMap<>()); var analysisProperties = Map.of(); var analyzerInfo = new AnalyzerInfoDto(false, true); - var expected = "{\"FileNames\":[],\"ActiveRules\":[{\"RuleId\":\"csharpsquid:S100\",\"Parameters\":{}}],\"AnalysisProperties\":{},\"AnalyzerInfo\":{\"ShouldUseCsharpEnterprise\":false,\"ShouldUseVbEnterprise\":true}}"; + var expected = "{\"FileNames\":[],\"ActiveRules\":[{\"RuleId\":\"csharpsquid:S100\",\"Parameters\":{}}],\"AnalysisProperties\":{},\"AnalyzerInfo\":{\"ShouldUseCsharpEnterprise\":false,\"ShouldUseVbEnterprise\":true},\"AnalysisId\":\"ed89f185-c2d6-4d03-aef1-334747e7fbdb\"}"; - var result = jsonParser.buildBody(fileNames, List.of(activeRule), analysisProperties, analyzerInfo); + var result = jsonParser.buildAnalyzeBody(fileNames, List.of(activeRule), analysisProperties, analyzerInfo, analysisId); assertThat(result).isEqualTo(expected); } @@ -98,9 +99,9 @@ void buildBody_withActiveRulesWithParams_shouldIncludeParams() { var vbnetRuleWithParams = createMockActiveRule("S1066", VbNetLanguage.REPOSITORY_KEY, params); var activeRules = List.of(csharpRuleWithParams, vbnetRuleWithParams); var analyzerInfo = new AnalyzerInfoDto(true, true); - var expected = "{\"FileNames\":[],\"ActiveRules\":[{\"RuleId\":\"csharpsquid:S1003\",\"Parameters\":{\"maximum\":\"10\",\"isRegularExpression\":\"true\"}},{\"RuleId\":\"vbnet:S1066\",\"Parameters\":{\"maximum\":\"10\",\"isRegularExpression\":\"true\"}}],\"AnalysisProperties\":{},\"AnalyzerInfo\":{\"ShouldUseCsharpEnterprise\":true,\"ShouldUseVbEnterprise\":true}}"; + var expected = "{\"FileNames\":[],\"ActiveRules\":[{\"RuleId\":\"csharpsquid:S1003\",\"Parameters\":{\"maximum\":\"10\",\"isRegularExpression\":\"true\"}},{\"RuleId\":\"vbnet:S1066\",\"Parameters\":{\"maximum\":\"10\",\"isRegularExpression\":\"true\"}}],\"AnalysisProperties\":{},\"AnalyzerInfo\":{\"ShouldUseCsharpEnterprise\":true,\"ShouldUseVbEnterprise\":true},\"AnalysisId\":\"ed89f185-c2d6-4d03-aef1-334747e7fbdb\"}"; - var result = jsonParser.buildBody(fileNames, activeRules, analysisProperties, analyzerInfo); + var result = jsonParser.buildAnalyzeBody(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId); assertThat(result).isEqualTo(expected); } @@ -112,7 +113,7 @@ void buildBody_withNullCollections_throws() { Map analysisProperties = null; AnalyzerInfoDto analyzerInfo = null; - assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> jsonParser.buildBody(fileNames, activeRules, analysisProperties, analyzerInfo)); + assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> jsonParser.buildAnalyzeBody(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId)); } @Test @@ -124,9 +125,9 @@ void buildBody_withSpecialCharactersInFileNames_shouldEscapeProperly() { var activeRules = new ArrayList(); var analysisProperties = Map.of(); var analyzerInfo = new AnalyzerInfoDto(false, false); - var expected = "{\"FileNames\":[\"file with spaces.cs\",\"file\\\"with\\\"quotes.cs\",\"file\\\\with\\\\backslashes.cs\"],\"ActiveRules\":[],\"AnalysisProperties\":{},\"AnalyzerInfo\":{\"ShouldUseCsharpEnterprise\":false,\"ShouldUseVbEnterprise\":false}}"; + var expected = "{\"FileNames\":[\"file with spaces.cs\",\"file\\\"with\\\"quotes.cs\",\"file\\\\with\\\\backslashes.cs\"],\"ActiveRules\":[],\"AnalysisProperties\":{},\"AnalyzerInfo\":{\"ShouldUseCsharpEnterprise\":false,\"ShouldUseVbEnterprise\":false},\"AnalysisId\":\"ed89f185-c2d6-4d03-aef1-334747e7fbdb\"}"; - var result = jsonParser.buildBody(fileNames, activeRules, analysisProperties, analyzerInfo); + var result = jsonParser.buildAnalyzeBody(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId); assertThat(result).isEqualTo(expected); var fileNamesArray = JsonParser.parseString(result).getAsJsonObject().get("FileNames").getAsJsonArray(); @@ -136,6 +137,15 @@ void buildBody_withSpecialCharactersInFileNames_shouldEscapeProperly() { assertThat(fileNamesArray.get(2).getAsString()).hasToString("file\\with\\backslashes.cs"); } + @Test + void buildCancelBody_withValidAnalysisId_shouldReturnValidJson() { + var expected = "{\"AnalysisId\":\"ed89f185-c2d6-4d03-aef1-334747e7fbdb\"}"; + + var result = jsonParser.buildCancelBody(analysisId); + + assertThat(result).isEqualTo(expected); + } + private ActiveRule createMockActiveRule(String ruleKey, String repositoryKey, Map params) { ActiveRule activeRule = mock(ActiveRule.class); RuleKey rule = mock(RuleKey.class);