From 914a384ad19302f71c356dedbf3207eaed00ca67 Mon Sep 17 00:00:00 2001 From: Duarte Meneses Date: Mon, 20 Jul 2015 13:30:07 +0200 Subject: [PATCH] SONARUNNER-132 Add support for offline mode --- .../com/sonar/runner/it/MultimoduleTest.java | 1 - .../sonar/runner/it/SonarRunnerTestSuite.java | 2 +- .../runner/impl/IsolatedLauncherFactory.java | 5 - .../sonar/runner/impl/ServerConnection.java | 101 +++++++++++------- .../runner/impl/ServerConnectionTest.java | 36 ++++++- 5 files changed, 100 insertions(+), 45 deletions(-) diff --git a/it/src/test/java/com/sonar/runner/it/MultimoduleTest.java b/it/src/test/java/com/sonar/runner/it/MultimoduleTest.java index 35dc8d9..6f2a25c 100644 --- a/it/src/test/java/com/sonar/runner/it/MultimoduleTest.java +++ b/it/src/test/java/com/sonar/runner/it/MultimoduleTest.java @@ -28,7 +28,6 @@ import org.sonar.wsclient.services.ResourceQuery; import java.io.File; import static org.fest.assertions.Assertions.assertThat; -import static org.junit.Assume.assumeTrue; public class MultimoduleTest extends RunnerTestCase { diff --git a/it/src/test/java/com/sonar/runner/it/SonarRunnerTestSuite.java b/it/src/test/java/com/sonar/runner/it/SonarRunnerTestSuite.java index e33768d..b219366 100644 --- a/it/src/test/java/com/sonar/runner/it/SonarRunnerTestSuite.java +++ b/it/src/test/java/com/sonar/runner/it/SonarRunnerTestSuite.java @@ -24,7 +24,7 @@ import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) -@SuiteClasses({JavaTest.class, MultimoduleTest.class}) +@SuiteClasses({JavaTest.class, MultimoduleTest.class, CacheTest.class}) public class SonarRunnerTestSuite { } diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/impl/IsolatedLauncherFactory.java b/sonar-runner-api/src/main/java/org/sonar/runner/impl/IsolatedLauncherFactory.java index 5c2a590..c56b72d 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/impl/IsolatedLauncherFactory.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/impl/IsolatedLauncherFactory.java @@ -50,11 +50,6 @@ public class IsolatedLauncherFactory { private PersistentCache getCache(Properties props) { PersistentCacheBuilder builder = new PersistentCacheBuilder(logger); - - if (!"true".equals(props.getProperty("sonar.enableHttpCache"))) { - builder.forceUpdate(true); - } - return builder.build(); } diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/impl/ServerConnection.java b/sonar-runner-api/src/main/java/org/sonar/runner/impl/ServerConnection.java index 7abd093..da9a86c 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/impl/ServerConnection.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/impl/ServerConnection.java @@ -19,7 +19,10 @@ */ package org.sonar.runner.impl; +import com.github.kevinsawicki.http.HttpRequest.HttpRequestException; + import com.github.kevinsawicki.http.HttpRequest; + import java.io.File; import java.io.IOException; import java.net.ConnectException; @@ -27,9 +30,9 @@ import java.net.URL; import java.net.UnknownHostException; import java.text.MessageFormat; import java.util.Properties; -import java.util.concurrent.Callable; import java.util.regex.Matcher; import java.util.regex.Pattern; + import org.apache.commons.io.FileUtils; import org.sonar.home.cache.Logger; import org.sonar.home.cache.PersistentCache; @@ -38,7 +41,7 @@ class ServerConnection { private static final String SONAR_SERVER_CAN_NOT_BE_REACHED = "Sonar server ''{0}'' can not be reached"; private static final String STATUS_RETURNED_BY_URL_IS_INVALID = "Status returned by url : ''{0}'' is invalid : {1}"; - static final int CONNECT_TIMEOUT_MILLISECONDS = 30000; + static final int CONNECT_TIMEOUT_MILLISECONDS = 5000; static final int READ_TIMEOUT_MILLISECONDS = 60000; private static final Pattern CHARSET_PATTERN = Pattern.compile("(?i)\\bcharset=\\s*\"?([^\\s;\"]*)"); @@ -46,15 +49,15 @@ class ServerConnection { private final String userAgent; private final PersistentCache wsCache; - private final boolean isModePreview; + private final boolean isCacheEnable; private final Logger logger; - private ServerConnection(String serverUrl, String app, String appVersion, boolean preview, PersistentCache cache, Logger logger) { + private ServerConnection(String serverUrl, String app, String appVersion, boolean isCacheEnable, PersistentCache cache, Logger logger) { this.logger = logger; this.serverUrl = removeEndSlash(serverUrl); this.userAgent = app + "/" + appVersion; this.wsCache = cache; - this.isModePreview = preview; + this.isCacheEnable = isCacheEnable; } private static String removeEndSlash(String url) { @@ -69,36 +72,42 @@ class ServerConnection { String app = properties.getProperty(InternalProperties.RUNNER_APP); String appVersion = properties.getProperty(InternalProperties.RUNNER_APP_VERSION); String analysisMode = properties.getProperty("sonar.analysis.mode"); - boolean preview = "preview".equalsIgnoreCase(analysisMode); + String enableOffline = properties.getProperty("sonar.enableOffline"); + boolean enableCache = "preview".equalsIgnoreCase(analysisMode) && "true".equals(enableOffline); - return new ServerConnection(serverUrl, app, appVersion, preview, cache, logger); + return new ServerConnection(serverUrl, app, appVersion, enableCache, cache, logger); } - private class StringDownloader implements Callable { - private String url; - - StringDownloader(String url) { - this.url = url; - } + /** + * + * @throws HttpRequestException If there is an underlying IOException related to the connection + * @throws IOException If the HTTP response code is != 200 + */ + private String downloadString(String url, boolean saveCache) throws HttpRequestException, IOException { + HttpRequest httpRequest = null; + try { + httpRequest = newHttpRequest(new URL(url)); + String charset = getCharsetFromContentType(httpRequest.contentType()); + if (charset == null || "".equals(charset)) { + charset = "UTF-8"; + } + if (!httpRequest.ok()) { + throw new IOException(MessageFormat.format(STATUS_RETURNED_BY_URL_IS_INVALID, url, httpRequest.code())); + } - @Override - public String call() throws Exception { - HttpRequest httpRequest = null; - try { - httpRequest = newHttpRequest(new URL(url)); - String charset = getCharsetFromContentType(httpRequest.contentType()); - if (charset == null || "".equals(charset)) { - charset = "UTF-8"; - } - if (!httpRequest.ok()) { - throw new IOException(MessageFormat.format(STATUS_RETURNED_BY_URL_IS_INVALID, url, httpRequest.code())); - } - return httpRequest.body(charset); - } finally { - if (httpRequest != null) { - httpRequest.disconnect(); + byte[] body = httpRequest.bytes(); + if (saveCache) { + try { + wsCache.put(url, body); + } catch (IOException e) { + logger.warn("Failed to cache WS call: " + e.getMessage()); } } + return new String(body, charset); + } finally { + if (httpRequest != null) { + httpRequest.disconnect(); + } } } @@ -121,22 +130,42 @@ class ServerConnection { } } - String downloadStringCache(String path) throws Exception { + /** + * Tries to fetch from server and falls back to cache. If both attempts fail, it throws the exception + * linked to the server connection failure. + */ + String downloadStringCache(String path) throws IOException { String fullUrl = serverUrl + path; try { - if (isModePreview) { - return wsCache.getString(serverUrl, new StringDownloader(fullUrl)); - } else { - return new StringDownloader(fullUrl).call(); - } + return downloadString(fullUrl, isCacheEnable); } catch (HttpRequest.HttpRequestException e) { if (e.getCause() instanceof ConnectException || e.getCause() instanceof UnknownHostException) { - logger.error(MessageFormat.format(SONAR_SERVER_CAN_NOT_BE_REACHED, serverUrl)); + if (isCacheEnable) { + return fallbackToCache(fullUrl, e); + } } + + logger.error(MessageFormat.format(SONAR_SERVER_CAN_NOT_BE_REACHED, serverUrl)); throw e; } } + private String fallbackToCache(String fullUrl, HttpRequest.HttpRequestException originalException) { + logger.info(MessageFormat.format(SONAR_SERVER_CAN_NOT_BE_REACHED + ", trying cache", serverUrl)); + + try { + String cached = wsCache.getString(fullUrl, null); + if (cached != null) { + return cached; + } + logger.error(MessageFormat.format(SONAR_SERVER_CAN_NOT_BE_REACHED + " and had a cache miss", serverUrl)); + throw originalException; + } catch (IOException e) { + throw new IllegalStateException("Failed to access cache", e); + } + + } + private HttpRequest newHttpRequest(URL url) { HttpRequest request = HttpRequest.get(url); request.trustAllCerts().trustAllHosts(); diff --git a/sonar-runner-api/src/test/java/org/sonar/runner/impl/ServerConnectionTest.java b/sonar-runner-api/src/test/java/org/sonar/runner/impl/ServerConnectionTest.java index 20016d8..7e9dec4 100644 --- a/sonar-runner-api/src/test/java/org/sonar/runner/impl/ServerConnectionTest.java +++ b/sonar-runner-api/src/test/java/org/sonar/runner/impl/ServerConnectionTest.java @@ -19,8 +19,13 @@ */ package org.sonar.runner.impl; +import com.github.kevinsawicki.http.HttpRequest; + import java.io.File; +import java.io.IOException; +import java.net.ConnectException; import java.util.Properties; + import org.apache.commons.io.FileUtils; import org.junit.Before; import org.junit.Rule; @@ -29,7 +34,6 @@ import org.junit.rules.TemporaryFolder; import org.sonar.home.cache.Logger; import org.sonar.home.cache.PersistentCache; import org.sonar.home.cache.PersistentCacheBuilder; - import static org.fest.assertions.Assertions.assertThat; import static org.fest.assertions.Fail.fail; import static org.mockito.Mockito.mock; @@ -81,6 +85,7 @@ public class ServerConnectionTest { Properties props = new Properties(); props.setProperty("sonar.host.url", httpServer.url() + "/"); props.setProperty("sonar.analysis.mode", "preview"); + props.setProperty("sonar.enableOffline", "true"); assertThat(cacheDir.list().length).isEqualTo(0); ServerConnection connection = ServerConnection.create(props, cache, mock(Logger.class)); @@ -89,10 +94,37 @@ public class ServerConnectionTest { assertThat(str).isEqualTo("abcde"); assertThat(cacheDir.list().length).isEqualTo(2); - httpServer.setMockResponseData("never requested"); + httpServer.after(); str = connection.downloadStringCache("/batch/index.txt"); assertThat(str).isEqualTo("abcde"); } + + @Test + public void should_throw_connection_exception_() throws IOException { + File cacheDir = new File(temp.getRoot(), "ws_cache"); + httpServer.setMockResponseData("abcde"); + Properties props = new Properties(); + props.setProperty("sonar.host.url", httpServer.url() + "/"); + + assertThat(cacheDir.list().length).isEqualTo(0); + ServerConnection connection = ServerConnection.create(props, cache, mock(Logger.class)); + String str = connection.downloadStringCache("/batch/index.txt"); + assertThat(str).isEqualTo("abcde"); + + httpServer.after(); + + try { + connection.downloadStringCache("/batch/index.txt"); + fail("exception expected"); + } catch(HttpRequest.HttpRequestException e) { + //expected + assertThat(e.getCause()).isInstanceOf(ConnectException.class); + + //cache never used + assertThat(cacheDir.list().length).isEqualTo(0); + } + + } @Test public void should_not_cache_not_preview() throws Exception { -- 2.39.5