From: Duarte Meneses Date: Mon, 15 Jun 2015 12:59:17 +0000 (+0200) Subject: SONARUNNER-136 and SONARUNNER-137 - Provide a new API to run several analysis with... X-Git-Tag: 2.5-rc1~81 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=e4ba9869d57dc4dd92648b5c8406d26fcd550d46;p=sonar-scanner-cli.git SONARUNNER-136 and SONARUNNER-137 - Provide a new API to run several analysis with same process and an interactive mode --- diff --git a/pom.xml b/pom.xml index 527cbc7..5460ed3 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,7 @@ sonar-runner-api sonar-runner-batch + sonar-runner-batch-interface sonar-runner-dist sonar-runner-impl diff --git a/sonar-runner-api/pom.xml b/sonar-runner-api/pom.xml index 0f491c7..b138660 100644 --- a/sonar-runner-api/pom.xml +++ b/sonar-runner-api/pom.xml @@ -15,12 +15,7 @@ ${project.groupId} sonar-runner-impl ${project.version} - - - * - * - - + com.google.code.findbugs diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/CommandExecutor.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/CommandExecutor.java index 999150d..d9903bf 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/CommandExecutor.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/CommandExecutor.java @@ -80,7 +80,9 @@ class CommandExecutor { return exitCode; } catch (TimeoutException te) { - process.destroy(); + if (process != null) { + process.destroy(); + } throw new CommandException("Timeout exceeded: " + timeoutMilliseconds + " ms", command, te); } catch (CommandException e) { @@ -120,6 +122,7 @@ class CommandExecutor { private Future executeProcess(ExecutorService executorService, Process process) { final Process finalProcess = process; return executorService.submit(new Callable() { + @Override public Integer call() throws InterruptedException { return finalProcess.waitFor(); } diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/Dirs.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/Dirs.java index 1ed4a04..2ab4d6c 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/Dirs.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/Dirs.java @@ -22,28 +22,32 @@ package org.sonar.runner.api; import org.sonar.runner.impl.Logs; import java.io.File; +import java.util.Properties; class Dirs { + private Dirs() { - void init(Runner runner) { - boolean onProject = Utils.taskRequiresProject(runner.properties()); + } + + static void init(Properties p) { + boolean onProject = Utils.taskRequiresProject(p); if (onProject) { - initProjectDirs(runner); + initProjectDirs(p); } else { - initTaskDirs(runner); + initTaskDirs(p); } } - private void initProjectDirs(Runner runner) { - String path = runner.property(ScanProperties.PROJECT_BASEDIR, "."); + private static void initProjectDirs(Properties p) { + String path = p.getProperty(ScanProperties.PROJECT_BASEDIR, "."); File projectDir = new File(path); if (!projectDir.isDirectory()) { throw new IllegalStateException("Project home must be an existing directory: " + path); } - runner.setProperty(ScanProperties.PROJECT_BASEDIR, projectDir.getAbsolutePath()); + p.setProperty(ScanProperties.PROJECT_BASEDIR, projectDir.getAbsolutePath()); File workDir; - path = runner.property(RunnerProperties.WORK_DIR, ""); + path = p.getProperty(RunnerProperties.WORK_DIR, ""); if ("".equals(path.trim())) { workDir = new File(projectDir, ".sonar"); @@ -54,16 +58,16 @@ class Dirs { } } Utils.deleteQuietly(workDir); - runner.setProperty(RunnerProperties.WORK_DIR, workDir.getAbsolutePath()); + p.setProperty(RunnerProperties.WORK_DIR, workDir.getAbsolutePath()); Logs.info("Work directory: " + workDir.getAbsolutePath()); } /** * Non-scan task */ - private void initTaskDirs(Runner runner) { - String path = runner.property(RunnerProperties.WORK_DIR, "."); + private static void initTaskDirs(Properties p) { + String path = p.getProperty(RunnerProperties.WORK_DIR, "."); File workDir = new File(path); - runner.setProperty(RunnerProperties.WORK_DIR, workDir.getAbsolutePath()); + p.setProperty(RunnerProperties.WORK_DIR, workDir.getAbsolutePath()); } } diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/EmbeddedRunner.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/EmbeddedRunner.java index b0366c3..9dfad19 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/EmbeddedRunner.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/EmbeddedRunner.java @@ -19,11 +19,14 @@ */ package org.sonar.runner.api; -import org.sonar.runner.impl.BatchLauncher; +import org.sonar.runner.batch.IsolatedLauncher; +import org.sonar.runner.impl.IsolatedLauncherFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Properties; /** * Implementation of {@link Runner} that is executed in the same JVM. The application can inject @@ -34,19 +37,20 @@ import java.util.List; */ public class EmbeddedRunner extends Runner { - private final BatchLauncher batchLauncher; + private final IsolatedLauncherFactory launcherFactory; + private IsolatedLauncher launcher; private final List extensions = new ArrayList(); private static final String MASK_RULES_PROP = "sonarRunner.maskRules"; - EmbeddedRunner(BatchLauncher bl) { - this.batchLauncher = bl; + EmbeddedRunner(IsolatedLauncherFactory bl) { + this.launcherFactory = bl; } /** * Create a new instance. */ public static EmbeddedRunner create() { - return new EmbeddedRunner(new BatchLauncher()); + return new EmbeddedRunner(new IsolatedLauncherFactory()); } /** @@ -69,12 +73,12 @@ public class EmbeddedRunner extends Runner { } private EmbeddedRunner addMaskRule(String type, String fqcnPrefix) { - String existingRules = property(MASK_RULES_PROP, ""); + String existingRules = globalProperty(MASK_RULES_PROP, ""); if (!"".equals(existingRules)) { existingRules += ","; } existingRules += type + "|" + fqcnPrefix; - return setProperty(MASK_RULES_PROP, existingRules); + return setGlobalProperty(MASK_RULES_PROP, existingRules); } /** @@ -98,7 +102,18 @@ public class EmbeddedRunner extends Runner { } @Override - protected void doExecute() { - batchLauncher.execute(properties(), extensions); + protected void doStart() { + launcher = launcherFactory.createLauncher(globalProperties()); + launcher.start(globalProperties(), extensions); + } + + @Override + protected void doStop() { + launcher.stop(); + } + + @Override + protected void doExecute(Properties analysisProperties) { + launcher.execute(analysisProperties); } } diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/ForkedRunner.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/ForkedRunner.java index fd7dc0b..02fdae7 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/ForkedRunner.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/ForkedRunner.java @@ -32,6 +32,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Properties; /** * Runner executed in a new JVM. @@ -136,17 +137,33 @@ public class ForkedRunner extends Runner { } @Override - protected void doExecute() { - ForkCommand forkCommand = createCommand(); + protected void doExecute(Properties props) { + //merge both global and analysis-specific properties because it will be used both to start and to execute. + Properties p = new Properties(); + + p.putAll(globalProperties()); + p.putAll(props); + + ForkCommand forkCommand = createCommand(p); try { fork(forkCommand); } finally { deleteTempFiles(forkCommand); } } + + @Override + protected void doStop() { + //nothing to do + } + + @Override + protected void doStart() { + //nothing to do + } - ForkCommand createCommand() { - File propertiesFile = writeProperties(); + ForkCommand createCommand(Properties p) { + File propertiesFile = writeProperties(p); File jarFile = jarExtractor.extractToTemp("sonar-runner-impl"); if (javaExecutable == null) { javaExecutable = new Os().thisJavaExe().getAbsolutePath(); @@ -160,11 +177,11 @@ public class ForkedRunner extends Runner { return new ForkCommand(command, jarFile, propertiesFile); } - private File writeProperties() { + File writeProperties(Properties p) { try { File file = File.createTempFile("sonar-project", ".properties"); try (OutputStream output = new FileOutputStream(file)) { - properties().store(output, "Generated by sonar-runner"); + p.store(output, "Generated by sonar-runner"); return file; } } catch (Exception e) { @@ -172,7 +189,7 @@ public class ForkedRunner extends Runner { } } - private void deleteTempFiles(ForkCommand forkCommand) { + private static void deleteTempFiles(ForkCommand forkCommand) { Utils.deleteQuietly(forkCommand.jarFile); Utils.deleteQuietly(forkCommand.propertiesFile); } diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/PrintStreamConsumer.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/PrintStreamConsumer.java index 5be7d04..bab55a7 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/PrintStreamConsumer.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/PrintStreamConsumer.java @@ -34,6 +34,7 @@ public class PrintStreamConsumer implements StreamConsumer { this.output = output; } + @Override public void consumeLine(String line) { output.println(line); } diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/Runner.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/Runner.java index b4e06bc..081b7f7 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/Runner.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/Runner.java @@ -24,8 +24,6 @@ import org.sonar.runner.impl.InternalProperties; import javax.annotation.Nullable; import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStream; import java.util.Properties; /** @@ -33,14 +31,14 @@ import java.util.Properties; */ public abstract class Runner { - private final Properties properties = new Properties(); + private final Properties globalProperties = new Properties(); protected Runner() { } - public Properties properties() { + public Properties globalProperties() { Properties clone = new Properties(); - clone.putAll(properties); + clone.putAll(globalProperties); return clone; } @@ -49,8 +47,8 @@ public abstract class Runner { * * @see #setProperty(String, String) */ - public T addProperties(Properties p) { - properties.putAll(p); + public T addGlobalProperties(Properties p) { + globalProperties.putAll(p); return (T) this; } @@ -60,65 +58,86 @@ public abstract class Runner { * @see RunnerProperties * @see ScanProperties */ - public T setProperty(String key, String value) { - properties.setProperty(key, value); + public T setGlobalProperty(String key, String value) { + globalProperties.setProperty(key, value); return (T) this; } - public String property(String key, @Nullable String defaultValue) { - return properties.getProperty(key, defaultValue); + public String globalProperty(String key, @Nullable String defaultValue) { + return globalProperties.getProperty(key, defaultValue); } /** * User-agent used in the HTTP requests to the Sonar server */ public T setApp(String app, String version) { - setProperty(InternalProperties.RUNNER_APP, app); - setProperty(InternalProperties.RUNNER_APP_VERSION, version); + setGlobalProperty(InternalProperties.RUNNER_APP, app); + setGlobalProperty(InternalProperties.RUNNER_APP_VERSION, version); return (T) this; } public String app() { - return property(InternalProperties.RUNNER_APP, null); + return globalProperty(InternalProperties.RUNNER_APP, null); } public String appVersion() { - return property(InternalProperties.RUNNER_APP_VERSION, null); + return globalProperty(InternalProperties.RUNNER_APP_VERSION, null); } - public void execute() { - initDefaultValues(); - new SourceEncoding().init(this); - new Dirs().init(this); - String dumpToFile = properties.getProperty(InternalProperties.RUNNER_DUMP_TO_FILE); + public void runAnalysis(Properties analysisProperties) { + Properties copy = new Properties(); + copy.putAll(analysisProperties); + initAnalysisProperties(copy); + + String dumpToFile = copy.getProperty(InternalProperties.RUNNER_DUMP_TO_FILE); if (dumpToFile != null) { File dumpFile = new File(dumpToFile); - writeProperties(dumpFile); + Utils.writeProperties(dumpFile, copy); System.out.println("Simulation mode. Configuration written to " + dumpFile.getAbsolutePath()); } else { - doExecute(); + doExecute(copy); } } - private void writeProperties(File outputFile) { - try (OutputStream output = new FileOutputStream(outputFile)) { - properties().store(output, "Generated by sonar-runner"); - } catch (Exception e) { - throw new IllegalStateException("Fail to export sonar-runner properties", e); - } + public void start() { + initGlobalDefaultValues(); + doStart(); + } + + public void stop() { + doStop(); + } + + /** + * @deprecated since 2.5 use {@link #start()}, {@link #runAnalysis(Properties)} and then {@link #stop()} + */ + @Deprecated + public final void execute() { + start(); + runAnalysis(new Properties()); + stop(); } - protected abstract void doExecute(); + protected abstract void doStart(); + + protected abstract void doStop(); + + protected abstract void doExecute(Properties analysisProperties); + + private void initGlobalDefaultValues() { + setGlobalDefaultValue(RunnerProperties.HOST_URL, "http://localhost:9000"); + setGlobalDefaultValue(InternalProperties.RUNNER_APP, "SonarQubeRunner"); + setGlobalDefaultValue(InternalProperties.RUNNER_APP_VERSION, RunnerVersion.version()); + } - private void initDefaultValues() { - setDefaultValue(RunnerProperties.HOST_URL, "http://localhost:9000"); - setDefaultValue(InternalProperties.RUNNER_APP, "SonarQubeRunner"); - setDefaultValue(InternalProperties.RUNNER_APP_VERSION, RunnerVersion.version()); + private static void initAnalysisProperties(Properties p) { + SourceEncoding.init(p); + Dirs.init(p); } - private void setDefaultValue(String key, String value) { - if (!properties.containsKey(key)) { - setProperty(key, value); + private void setGlobalDefaultValue(String key, String value) { + if (!globalProperties.containsKey(key)) { + setGlobalProperty(key, value); } } diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/SourceEncoding.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/SourceEncoding.java index 44d243b..0029308 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/SourceEncoding.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/SourceEncoding.java @@ -23,21 +23,24 @@ import org.sonar.runner.impl.Logs; import java.nio.charset.Charset; import java.util.Locale; +import java.util.Properties; class SourceEncoding { - - void init(Runner runner) { - boolean onProject = Utils.taskRequiresProject(runner.properties()); + private SourceEncoding() { + + } + static void init(Properties p) { + boolean onProject = Utils.taskRequiresProject(p); if (onProject) { - String sourceEncoding = runner.property(ScanProperties.PROJECT_SOURCE_ENCODING, ""); + String sourceEncoding = p.getProperty(ScanProperties.PROJECT_SOURCE_ENCODING, ""); boolean platformDependent = false; if ("".equals(sourceEncoding)) { sourceEncoding = Charset.defaultCharset().name(); platformDependent = true; - runner.setProperty(ScanProperties.PROJECT_SOURCE_ENCODING, sourceEncoding); + p.setProperty(ScanProperties.PROJECT_SOURCE_ENCODING, sourceEncoding); } Logs.info("Default locale: \"" + Locale.getDefault() + "\", source code encoding: \"" + sourceEncoding + "\"" - + (platformDependent ? " (analysis is platform dependent)" : "")); + + (platformDependent ? " (analysis is platform dependent)" : "")); } } } diff --git a/sonar-runner-api/src/main/java/org/sonar/runner/api/Utils.java b/sonar-runner-api/src/main/java/org/sonar/runner/api/Utils.java index a4286c1..4d0524a 100644 --- a/sonar-runner-api/src/main/java/org/sonar/runner/api/Utils.java +++ b/sonar-runner-api/src/main/java/org/sonar/runner/api/Utils.java @@ -23,7 +23,9 @@ import javax.annotation.Nullable; import java.io.Closeable; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -59,7 +61,15 @@ class Utils { return task == null || ScanProperties.SCAN_TASK.equals(task); } - public static void deleteQuietly(File f) { + static void writeProperties(File outputFile, Properties p) { + try (OutputStream output = new FileOutputStream(outputFile)) { + p.store(output, "Generated by sonar-runner"); + } catch (Exception e) { + throw new IllegalStateException("Fail to export sonar-runner properties", e); + } + } + + static void deleteQuietly(File f) { try { Files.walkFileTree(f.toPath(), new DeleteFileVisitor()); } catch (IOException e) { @@ -92,7 +102,7 @@ class Utils { } } - public static void closeQuietly(@Nullable Closeable c) { + static void closeQuietly(@Nullable Closeable c) { if (c == null) { return; } diff --git a/sonar-runner-api/src/test/java/org/sonar/runner/api/DirsTest.java b/sonar-runner-api/src/test/java/org/sonar/runner/api/DirsTest.java index b66d075..8cd7630 100644 --- a/sonar-runner-api/src/test/java/org/sonar/runner/api/DirsTest.java +++ b/sonar-runner-api/src/test/java/org/sonar/runner/api/DirsTest.java @@ -24,47 +24,46 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import java.io.File; +import java.util.Properties; import static org.fest.assertions.Assertions.assertThat; public class DirsTest { - Runner runner = new SimpleRunner(); - Dirs dirs = new Dirs(); + Properties p = new Properties(); @Rule public TemporaryFolder temp = new TemporaryFolder(); @Test public void should_init_default_task_work_dir() throws Exception { - runner.setProperty("sonar.task", "views"); - dirs.init(runner); + p.setProperty("sonar.task", "views"); + Dirs.init(p); - File workDir = new File(runner.property("sonar.working.directory", null)); + File workDir = new File(p.getProperty(RunnerProperties.WORK_DIR, null)); assertThat(workDir).isNotNull().isDirectory(); assertThat(workDir.getCanonicalPath()).isEqualTo(new File(".").getCanonicalPath()); } @Test public void should_use_parameterized_task_work_dir() throws Exception { - runner.setProperty("sonar.task", "views"); - runner.setProperty("sonar.working.directory", "generated/reports"); - dirs.init(runner); + p.setProperty("sonar.task", "views"); + p.setProperty(RunnerProperties.WORK_DIR, "generated/reports"); + Dirs.init(p); - File workDir = new File(runner.property("sonar.working.directory", null)); + File workDir = new File(p.getProperty(RunnerProperties.WORK_DIR, null)); assertThat(workDir).isNotNull(); - //separators from windows to unix + // separators from windows to unix assertThat(workDir.getCanonicalPath().replace("\\", "/")).contains("generated/reports"); } @Test public void should_init_default_project_dirs() throws Exception { - runner.setProperty("sonar.task", "scan"); - dirs.init(runner); + p.setProperty("sonar.task", "scan"); + Dirs.init(p); - - File projectDir = new File(runner.property("sonar.projectBaseDir", null)); - File workDir = new File(runner.property("sonar.working.directory", null)); + File projectDir = new File(p.getProperty(ScanProperties.PROJECT_BASEDIR, null)); + File workDir = new File(p.getProperty(RunnerProperties.WORK_DIR, null)); assertThat(projectDir).isNotNull().isDirectory(); assertThat(workDir).isNotNull(); @@ -77,14 +76,13 @@ public class DirsTest { @Test public void should_set_relative_path_to_project_work_dir() throws Exception { File initialProjectDir = temp.newFolder(); - runner.setProperty("sonar.task", "scan"); - runner.setProperty("sonar.working.directory", "relative/path"); - runner.setProperty("sonar.projectBaseDir", initialProjectDir.getAbsolutePath()); - dirs.init(runner); - + p.setProperty("sonar.task", "scan"); + p.setProperty(RunnerProperties.WORK_DIR, "relative/path"); + p.setProperty(ScanProperties.PROJECT_BASEDIR, initialProjectDir.getAbsolutePath()); + Dirs.init(p); - File projectDir = new File(runner.property("sonar.projectBaseDir", null)); - File workDir = new File(runner.property("sonar.working.directory", null)); + File projectDir = new File(p.getProperty(ScanProperties.PROJECT_BASEDIR, null)); + File workDir = new File(p.getProperty(RunnerProperties.WORK_DIR, null)); assertThat(projectDir).isNotNull().isDirectory(); assertThat(projectDir.getCanonicalPath()).isEqualTo(initialProjectDir.getCanonicalPath()); diff --git a/sonar-runner-api/src/test/java/org/sonar/runner/api/EmbeddedRunnerTest.java b/sonar-runner-api/src/test/java/org/sonar/runner/api/EmbeddedRunnerTest.java index 2a4302b..2082b29 100644 --- a/sonar-runner-api/src/test/java/org/sonar/runner/api/EmbeddedRunnerTest.java +++ b/sonar-runner-api/src/test/java/org/sonar/runner/api/EmbeddedRunnerTest.java @@ -19,19 +19,22 @@ */ package org.sonar.runner.api; +import org.junit.Before; +import org.sonar.runner.batch.IsolatedLauncher; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.mockito.ArgumentMatcher; -import org.sonar.runner.impl.BatchLauncher; +import org.sonar.runner.impl.IsolatedLauncherFactory; import org.sonar.runner.impl.InternalProperties; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.util.List; import java.util.Properties; +import static org.mockito.Mockito.when; +import static org.mockito.Matchers.any; import static org.fest.assertions.Assertions.assertThat; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.mock; @@ -47,6 +50,18 @@ public class EmbeddedRunnerTest { assertThat(EmbeddedRunner.create()).isNotNull().isInstanceOf(EmbeddedRunner.class); } + private IsolatedLauncherFactory batchLauncher; + private IsolatedLauncher launcher; + private EmbeddedRunner runner; + + @Before + public void setUp() { + batchLauncher = mock(IsolatedLauncherFactory.class); + launcher = mock(IsolatedLauncher.class); + when(batchLauncher.createLauncher(any(Properties.class))).thenReturn(launcher); + runner = new EmbeddedRunner(batchLauncher); + } + @Test public void test_app() { EmbeddedRunner runner = EmbeddedRunner.create().setApp("Eclipse", "3.1"); @@ -57,23 +72,23 @@ public class EmbeddedRunnerTest { @Test public void should_set_unmasked_packages() { EmbeddedRunner runner = EmbeddedRunner.create(); - assertThat(runner.property(InternalProperties.RUNNER_MASK_RULES, null)).isNull(); + assertThat(runner.globalProperty(InternalProperties.RUNNER_MASK_RULES, null)).isNull(); runner = EmbeddedRunner.create().setUnmaskedPackages("org.apache.ant", "org.ant"); - assertThat(runner.property(InternalProperties.RUNNER_MASK_RULES, null)).isEqualTo("UNMASK|org.apache.ant.,UNMASK|org.ant."); + assertThat(runner.globalProperty(InternalProperties.RUNNER_MASK_RULES, null)).isEqualTo("UNMASK|org.apache.ant.,UNMASK|org.ant."); } @Test public void should_set_mask_rules() { EmbeddedRunner runner = EmbeddedRunner.create(); - assertThat(runner.property(InternalProperties.RUNNER_MASK_RULES, null)).isNull(); + assertThat(runner.globalProperty(InternalProperties.RUNNER_MASK_RULES, null)).isNull(); runner = EmbeddedRunner.create() .unmask("org.slf4j.Logger") .mask("org.slf4j.") .mask("ch.qos.logback.") .unmask(""); - assertThat(runner.property(InternalProperties.RUNNER_MASK_RULES, null)).isEqualTo("UNMASK|org.slf4j.Logger,MASK|org.slf4j.,MASK|ch.qos.logback.,UNMASK|"); + assertThat(runner.globalProperty(InternalProperties.RUNNER_MASK_RULES, null)).isEqualTo("UNMASK|org.slf4j.Logger,MASK|org.slf4j.,MASK|ch.qos.logback.,UNMASK|"); } @Test @@ -89,38 +104,76 @@ public class EmbeddedRunnerTest { @Test public void should_set_properties() { EmbeddedRunner runner = EmbeddedRunner.create(); - runner.setProperty("sonar.projectKey", "foo"); - runner.addProperties(new Properties() { + runner.setGlobalProperty("sonar.projectKey", "foo"); + runner.addGlobalProperties(new Properties() { { setProperty("sonar.login", "admin"); setProperty("sonar.password", "gniark"); } }); - assertThat(runner.property("sonar.projectKey", null)).isEqualTo("foo"); - assertThat(runner.property("sonar.login", null)).isEqualTo("admin"); - assertThat(runner.property("sonar.password", null)).isEqualTo("gniark"); - assertThat(runner.property("not.set", "this_is_default")).isEqualTo("this_is_default"); + assertThat(runner.globalProperty("sonar.projectKey", null)).isEqualTo("foo"); + assertThat(runner.globalProperty("sonar.login", null)).isEqualTo("admin"); + assertThat(runner.globalProperty("sonar.password", null)).isEqualTo("gniark"); + assertThat(runner.globalProperty("not.set", "this_is_default")).isEqualTo("this_is_default"); } @Test public void should_launch_batch() { - BatchLauncher batchLauncher = mock(BatchLauncher.class); - EmbeddedRunner runner = new EmbeddedRunner(batchLauncher); final FakeExtension fakeExtension = new FakeExtension(); runner.addExtensions(fakeExtension); - runner.setProperty("sonar.projectKey", "foo"); - runner.execute(); + runner.setGlobalProperty("sonar.projectKey", "foo"); + runner.start(); + runner.runAnalysis(new Properties()); + runner.stop(); - verify(batchLauncher).execute(argThat(new ArgumentMatcher() { + verify(batchLauncher).createLauncher(argThat(new ArgumentMatcher() { @Override public boolean matches(Object o) { return "foo".equals(((Properties) o).getProperty("sonar.projectKey")); } - }), argThat(new ArgumentMatcher>() { + })); + + // it should have added a few properties to analysisProperties + final String[] mustHaveKeys = {"sonar.working.directory", "sonar.sourceEncoding", "sonar.projectBaseDir"}; + + verify(launcher).execute(argThat(new ArgumentMatcher() { + @Override + public boolean matches(Object o) { + Properties m = (Properties) o; + for (String s : mustHaveKeys) { + if (!m.containsKey(s)) { + return false; + } + } + return true; + } + })); + } + + @Test + public void should_launch_batch_analysisProperties() { + final FakeExtension fakeExtension = new FakeExtension(); + runner.addExtensions(fakeExtension); + runner.setGlobalProperty("sonar.projectKey", "foo"); + runner.start(); + + Properties analysisProperties = new Properties(); + analysisProperties.put("sonar.projectKey", "value1"); + runner.runAnalysis(analysisProperties); + runner.stop(); + + verify(batchLauncher).createLauncher(argThat(new ArgumentMatcher() { + @Override + public boolean matches(Object o) { + return "foo".equals(((Properties) o).getProperty("sonar.projectKey")); + } + })); + + verify(launcher).execute(argThat(new ArgumentMatcher() { @Override public boolean matches(Object o) { - return ((List) o).contains(fakeExtension); + return "value1".equals(((Properties) o).getProperty("sonar.projectKey")); } })); } @@ -128,12 +181,13 @@ public class EmbeddedRunnerTest { @Test public void should_launch_in_simulation_mode() throws IOException { File dump = temp.newFile(); + Properties p = new Properties(); - BatchLauncher batchLauncher = mock(BatchLauncher.class); - EmbeddedRunner runner = new EmbeddedRunner(batchLauncher); - runner.setProperty("sonar.projectKey", "foo"); - runner.setProperty("sonarRunner.dumpToFile", dump.getAbsolutePath()); - runner.execute(); + p.setProperty("sonar.projectKey", "foo"); + p.setProperty("sonarRunner.dumpToFile", dump.getAbsolutePath()); + runner.start(); + runner.runAnalysis(p); + runner.stop(); Properties props = new Properties(); props.load(new FileInputStream(dump)); diff --git a/sonar-runner-api/src/test/java/org/sonar/runner/api/ForkedRunnerTest.java b/sonar-runner-api/src/test/java/org/sonar/runner/api/ForkedRunnerTest.java index d6f7a1f..0c27273 100644 --- a/sonar-runner-api/src/test/java/org/sonar/runner/api/ForkedRunnerTest.java +++ b/sonar-runner-api/src/test/java/org/sonar/runner/api/ForkedRunnerTest.java @@ -19,6 +19,9 @@ */ package org.sonar.runner.api; +import org.mockito.Mockito; + +import org.mockito.ArgumentCaptor; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -99,13 +102,13 @@ public class ForkedRunnerTest { JarExtractor jarExtractor = createMockExtractor(); ForkedRunner runner = new ForkedRunner(jarExtractor, mock(CommandExecutor.class), any(ProcessMonitor.class)); - runner.setProperty("sonar.dynamicAnalysis", "false"); - runner.setProperty("sonar.login", "admin"); + runner.setGlobalProperty("sonar.dynamicAnalysis", "false"); + runner.setGlobalProperty("sonar.login", "admin"); runner.addJvmArguments("-Xmx512m"); runner.addJvmEnvVariables(System.getenv()); runner.setJvmEnvVariable("SONAR_HOME", "/path/to/sonar"); - ForkedRunner.ForkCommand forkCommand = runner.createCommand(); + ForkedRunner.ForkCommand forkCommand = runner.createCommand(runner.globalProperties()); Properties properties = new Properties(); properties.load(new FileInputStream(forkCommand.propertiesFile)); @@ -115,7 +118,21 @@ public class ForkedRunnerTest { assertThat(properties.getProperty("-Xmx512m")).isNull(); assertThat(properties.getProperty("SONAR_HOME")).isNull(); } - + + @Test + public void should_merge_properties() throws IOException { + JarExtractor jarExtractor = createMockExtractor(); + ForkedRunner runner = new ForkedRunner(jarExtractor, mock(CommandExecutor.class), null); + + ForkedRunner spy = Mockito.spy(runner); + spy.setGlobalProperty("sonar.login", "admin"); + spy.execute(); + // generated analysis properties should have been added + ArgumentCaptor properties = ArgumentCaptor.forClass(Properties.class); + verify(spy).writeProperties(properties.capture()); + assertThat(properties.getValue().keySet()).contains("sonar.working.directory", "sonar.host.url", "sonar.sourceEncoding", "sonar.login"); + } + @Test public void test_java_command() throws IOException { JarExtractor jarExtractor = mock(JarExtractor.class); @@ -126,8 +143,8 @@ public class ForkedRunnerTest { ForkedRunner runner = new ForkedRunner(jarExtractor, commandExecutor); runner.setJavaExecutable("java"); - runner.setProperty("sonar.dynamicAnalysis", "false"); - runner.setProperty("sonar.login", "admin"); + runner.setGlobalProperty("sonar.dynamicAnalysis", "false"); + runner.setGlobalProperty("sonar.login", "admin"); runner.addJvmArguments("-Xmx512m"); runner.addJvmEnvVariables(System.getenv()); runner.setJvmEnvVariable("SONAR_HOME", "/path/to/sonar"); diff --git a/sonar-runner-api/src/test/java/org/sonar/runner/api/SimpleRunner.java b/sonar-runner-api/src/test/java/org/sonar/runner/api/SimpleRunner.java index 25b8c6b..5d3f0c2 100644 --- a/sonar-runner-api/src/test/java/org/sonar/runner/api/SimpleRunner.java +++ b/sonar-runner-api/src/test/java/org/sonar/runner/api/SimpleRunner.java @@ -19,8 +19,20 @@ */ package org.sonar.runner.api; -class SimpleRunner extends Runner { +import java.util.Properties; + +class SimpleRunner extends Runner { + @Override + protected void doExecute(Properties props) { + } + + @Override + protected void doStart() { + + } + @Override - protected void doExecute() { + protected void doStop() { + } } diff --git a/sonar-runner-api/src/test/java/org/sonar/runner/api/SourceEncodingTest.java b/sonar-runner-api/src/test/java/org/sonar/runner/api/SourceEncodingTest.java index 78b0ec4..027a87f 100644 --- a/sonar-runner-api/src/test/java/org/sonar/runner/api/SourceEncodingTest.java +++ b/sonar-runner-api/src/test/java/org/sonar/runner/api/SourceEncodingTest.java @@ -22,33 +22,33 @@ package org.sonar.runner.api; import org.junit.Test; import java.nio.charset.Charset; +import java.util.Properties; import static org.fest.assertions.Assertions.assertThat; public class SourceEncodingTest { - SourceEncoding encoding = new SourceEncoding(); - Runner runner = new SimpleRunner(); + Properties p = new Properties(); @Test public void should_set_default_platform_encoding() throws Exception { - runner.setProperty("sonar.task", "scan"); - encoding.init(runner); - assertThat(runner.property("sonar.sourceEncoding", null)).isEqualTo(Charset.defaultCharset().name()); + p.setProperty("sonar.task", "scan"); + SourceEncoding.init(p); + assertThat(p.getProperty("sonar.sourceEncoding", null)).isEqualTo(Charset.defaultCharset().name()); } @Test public void should_use_parameterized_encoding() throws Exception { - runner.setProperty("sonar.task", "scan"); - runner.setProperty("sonar.sourceEncoding", "THE_ISO_1234"); - encoding.init(runner); - assertThat(runner.property("sonar.sourceEncoding", null)).isEqualTo("THE_ISO_1234"); + p.setProperty("sonar.task", "scan"); + p.setProperty("sonar.sourceEncoding", "THE_ISO_1234"); + SourceEncoding.init(p); + assertThat(p.getProperty("sonar.sourceEncoding", null)).isEqualTo("THE_ISO_1234"); } @Test public void should_not_init_encoding_if_not_project_task() throws Exception { - runner.setProperty("sonar.task", "views"); - encoding.init(runner); - assertThat(runner.property("sonar.sourceEncoding", null)).isNull(); + p.setProperty("sonar.task", "views"); + SourceEncoding.init(p); + assertThat(p.getProperty("sonar.sourceEncoding", null)).isNull(); } } diff --git a/sonar-runner-batch-interface/.gitignore b/sonar-runner-batch-interface/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/sonar-runner-batch-interface/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/sonar-runner-batch-interface/pom.xml b/sonar-runner-batch-interface/pom.xml new file mode 100644 index 0000000..d70c04a --- /dev/null +++ b/sonar-runner-batch-interface/pom.xml @@ -0,0 +1,11 @@ + + 4.0.0 + + org.codehaus.sonar.runner + sonar-runner + 2.5-SNAPSHOT + + + sonar-runner-batch-interface + SonarQube Runner - Batch API + diff --git a/sonar-runner-batch-interface/src/main/java/org/sonar/runner/batch/IsolatedLauncher.java b/sonar-runner-batch-interface/src/main/java/org/sonar/runner/batch/IsolatedLauncher.java new file mode 100644 index 0000000..71d97c3 --- /dev/null +++ b/sonar-runner-batch-interface/src/main/java/org/sonar/runner/batch/IsolatedLauncher.java @@ -0,0 +1,29 @@ +/* + * SonarQube Runner - Batch API + * Copyright (C) 2011 SonarSource + * dev@sonar.codehaus.org + * + * 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 02 + */ +package org.sonar.runner.batch; + +import java.util.List; +import java.util.Properties; + +public interface IsolatedLauncher { + void start(Properties properties, List extensions); + void stop(); + void execute(Properties properties); +} diff --git a/sonar-runner-batch-interface/src/test/java_sample/sonar-project.properties b/sonar-runner-batch-interface/src/test/java_sample/sonar-project.properties new file mode 100644 index 0000000..0c35484 --- /dev/null +++ b/sonar-runner-batch-interface/src/test/java_sample/sonar-project.properties @@ -0,0 +1,5 @@ +sonar.projectKey=sample +sonar.projectName=Sample +sonar.projectVersion=1.0-SNAPSHOT +sonar.projectDescription=Sample description +sonar.sources=src diff --git a/sonar-runner-batch-interface/src/test/java_sample/src/Sample.java b/sonar-runner-batch-interface/src/test/java_sample/src/Sample.java new file mode 100644 index 0000000..e69de29 diff --git a/sonar-runner-batch/pom.xml b/sonar-runner-batch/pom.xml index a05893a..e7ad0e3 100644 --- a/sonar-runner-batch/pom.xml +++ b/sonar-runner-batch/pom.xml @@ -10,10 +10,16 @@ SonarQube Runner - Batch - 3.7 + 4.4 + + ${project.groupId} + sonar-runner-batch-interface + ${project.version} + provided + ch.qos.logback logback-classic diff --git a/sonar-runner-batch/src/main/java/org/sonar/runner/batch/BatchIsolatedLauncher.java b/sonar-runner-batch/src/main/java/org/sonar/runner/batch/BatchIsolatedLauncher.java new file mode 100644 index 0000000..069f16a --- /dev/null +++ b/sonar-runner-batch/src/main/java/org/sonar/runner/batch/BatchIsolatedLauncher.java @@ -0,0 +1,109 @@ +/* + * SonarQube Runner - Batch + * Copyright (C) 2011 SonarSource + * dev@sonar.codehaus.org + * + * 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 02 + */ +package org.sonar.runner.batch; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.core.joran.spi.JoranException; +import com.google.common.annotations.VisibleForTesting; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.slf4j.LoggerFactory; +import org.sonar.api.utils.SonarException; +import org.sonar.batch.bootstrapper.Batch; +import org.sonar.batch.bootstrapper.EnvironmentInformation; + +/** + * This class is executed within the classloader provided by the server. It contains the installed plugins and + * the same version of sonar-batch as the server. + */ +public class BatchIsolatedLauncher implements IsolatedLauncher { + + private static final String WARN = "WARN"; + private static final String DEBUG = "DEBUG"; + private static final String FALSE = "false"; + + private Batch batch = null; + + @Override + public void start(Properties globalProperties, List extensions) { + batch = createBatch(globalProperties, extensions); + batch.start(); + } + + @Override + public void stop() { + batch.stop(); + } + + @Override + public void execute(Properties properties) { + batch.executeTask((Map) properties); + } + + Batch createBatch(Properties properties, List extensions) { + initLogging(properties); + EnvironmentInformation env = new EnvironmentInformation(properties.getProperty("sonarRunner.app"), properties.getProperty("sonarRunner.appVersion")); + return Batch.builder() + .setEnvironment(env) + .addComponents(extensions) + .setBootstrapProperties((Map) properties) + .build(); + } + + private void initLogging(Properties props) { + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + JoranConfigurator jc = new JoranConfigurator(); + jc.setContext(context); + context.reset(); + try (InputStream input = Batch.class.getResourceAsStream("/org/sonar/batch/logback.xml")) { + System.setProperty("ROOT_LOGGER_LEVEL", isDebug(props) ? DEBUG : "INFO"); + context.putProperty("SQL_LOGGER_LEVEL", getSqlLevel(props)); + context.putProperty("SQL_RESULTS_LOGGER_LEVEL", getSqlResultsLevel(props)); + jc.doConfigure(input); + } catch (JoranException e) { + throw new SonarException("can not initialize logging", e); + } catch (IOException e1) { + throw new SonarException("couldn't close resource", e1); + } + } + + @VisibleForTesting + protected boolean isDebug(Properties props) { + return Boolean.parseBoolean(props.getProperty("sonar.verbose", FALSE)); + } + + @VisibleForTesting + protected static String getSqlLevel(Properties props) { + boolean showSql = "true".equals(props.getProperty("sonar.showSql", FALSE)); + return showSql ? DEBUG : WARN; + } + + @VisibleForTesting + protected static String getSqlResultsLevel(Properties props) { + boolean showSql = "true".equals(props.getProperty("sonar.showSqlResults", FALSE)); + return showSql ? DEBUG : WARN; + } +} diff --git a/sonar-runner-batch/src/main/java/org/sonar/runner/batch/IsolatedLauncher.java b/sonar-runner-batch/src/main/java/org/sonar/runner/batch/IsolatedLauncher.java deleted file mode 100644 index 8298d63..0000000 --- a/sonar-runner-batch/src/main/java/org/sonar/runner/batch/IsolatedLauncher.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SonarQube Runner - Batch - * Copyright (C) 2011 SonarSource - * dev@sonar.codehaus.org - * - * 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 02 - */ -package org.sonar.runner.batch; - -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.joran.JoranConfigurator; -import ch.qos.logback.core.joran.spi.JoranException; -import com.google.common.annotations.VisibleForTesting; - -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import org.slf4j.LoggerFactory; -import org.sonar.api.utils.SonarException; -import org.sonar.batch.bootstrapper.Batch; -import org.sonar.batch.bootstrapper.EnvironmentInformation; - -/** - * This class is executed within the classloader provided by the server. It contains the installed plugins and - * the same version of sonar-batch as the server. - */ -public class IsolatedLauncher { - - private static final String WARN = "WARN"; - private static final String DEBUG = "DEBUG"; - private static final String FALSE = "false"; - - public void execute(Properties properties, List extensions) { - createBatch(properties, extensions).execute(); - } - - Batch createBatch(Properties properties, List extensions) { - initLogging(properties); - EnvironmentInformation env = new EnvironmentInformation(properties.getProperty("sonarRunner.app"), properties.getProperty("sonarRunner.appVersion")); - return Batch.builder() - .setEnvironment(env) - .addComponents(extensions) - .setBootstrapProperties((Map) properties) - .build(); - } - - private void initLogging(Properties props) { - LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); - JoranConfigurator jc = new JoranConfigurator(); - jc.setContext(context); - context.reset(); - try (InputStream input = Batch.class.getResourceAsStream("/org/sonar/batch/logback.xml")) { - System.setProperty("ROOT_LOGGER_LEVEL", isDebug(props) ? DEBUG : "INFO"); - context.putProperty("SQL_LOGGER_LEVEL", getSqlLevel(props)); - context.putProperty("SQL_RESULTS_LOGGER_LEVEL", getSqlResultsLevel(props)); - jc.doConfigure(input); - } catch (JoranException e) { - throw new SonarException("can not initialize logging", e); - } catch (IOException e1) { - throw new SonarException("couldn't close resource", e1); - } - } - - @VisibleForTesting - protected boolean isDebug(Properties props) { - return Boolean.parseBoolean(props.getProperty("sonar.verbose", FALSE)); - } - - @VisibleForTesting - protected static String getSqlLevel(Properties props) { - boolean showSql = "true".equals(props.getProperty("sonar.showSql", FALSE)); - return showSql ? DEBUG : WARN; - } - - @VisibleForTesting - protected static String getSqlResultsLevel(Properties props) { - boolean showSql = "true".equals(props.getProperty("sonar.showSqlResults", FALSE)); - return showSql ? DEBUG : WARN; - } -} diff --git a/sonar-runner-batch/src/test/java/org/sonar/runner/batch/IsolatedLauncherTest.java b/sonar-runner-batch/src/test/java/org/sonar/runner/batch/IsolatedLauncherTest.java index cc43305..f8fd10c 100644 --- a/sonar-runner-batch/src/test/java/org/sonar/runner/batch/IsolatedLauncherTest.java +++ b/sonar-runner-batch/src/test/java/org/sonar/runner/batch/IsolatedLauncherTest.java @@ -29,7 +29,7 @@ import static org.fest.assertions.Assertions.assertThat; public class IsolatedLauncherTest { Properties props = new Properties(); - IsolatedLauncher launcher = new IsolatedLauncher(); + BatchIsolatedLauncher launcher = new BatchIsolatedLauncher(); @Test public void should_create_batch() { @@ -45,24 +45,24 @@ public class IsolatedLauncherTest { @Test public void testGetSqlLevel() throws Exception { - assertThat(IsolatedLauncher.getSqlLevel(props)).isEqualTo("WARN"); + assertThat(BatchIsolatedLauncher.getSqlLevel(props)).isEqualTo("WARN"); props.setProperty("sonar.showSql", "true"); - assertThat(IsolatedLauncher.getSqlLevel(props)).isEqualTo("DEBUG"); + assertThat(BatchIsolatedLauncher.getSqlLevel(props)).isEqualTo("DEBUG"); props.setProperty("sonar.showSql", "false"); - assertThat(IsolatedLauncher.getSqlLevel(props)).isEqualTo("WARN"); + assertThat(BatchIsolatedLauncher.getSqlLevel(props)).isEqualTo("WARN"); } @Test public void testGetSqlResultsLevel() throws Exception { - assertThat(IsolatedLauncher.getSqlResultsLevel(props)).isEqualTo("WARN"); + assertThat(BatchIsolatedLauncher.getSqlResultsLevel(props)).isEqualTo("WARN"); props.setProperty("sonar.showSqlResults", "true"); - assertThat(IsolatedLauncher.getSqlResultsLevel(props)).isEqualTo("DEBUG"); + assertThat(BatchIsolatedLauncher.getSqlResultsLevel(props)).isEqualTo("DEBUG"); props.setProperty("sonar.showSqlResults", "false"); - assertThat(IsolatedLauncher.getSqlResultsLevel(props)).isEqualTo("WARN"); + assertThat(BatchIsolatedLauncher.getSqlResultsLevel(props)).isEqualTo("WARN"); } @Test diff --git a/sonar-runner-dist/src/main/java/org/sonar/runner/Cli.java b/sonar-runner-dist/src/main/java/org/sonar/runner/Cli.java index 8235a64..3a796e8 100644 --- a/sonar-runner-dist/src/main/java/org/sonar/runner/Cli.java +++ b/sonar-runner-dist/src/main/java/org/sonar/runner/Cli.java @@ -29,7 +29,13 @@ class Cli { private boolean debugMode = false; private boolean displayVersionOnly = false; private boolean displayStackTrace = false; + private boolean interactive = false; private Properties props = new Properties(); + private Exit exit; + + public Cli(Exit exit) { + this.exit = exit; + } boolean isDebugMode() { return debugMode; @@ -43,6 +49,10 @@ class Cli { return displayStackTrace; } + boolean isInteractive() { + return interactive; + } + Properties properties() { return props; } @@ -82,13 +92,23 @@ class Cli { arg = arg.substring(2); appendPropertyTo(arg, props); + } else if ("-i".equals(arg) || "--interactive".equals(arg)) { + interactive = true; + } else { printError("Unrecognized option: " + arg); } } + return this; } + public void verify() { + if ("fork".equals(props.getProperty("sonarRunner.mode")) && isInteractive()) { + printError("Cannot run interactively in fork mode."); + } + } + private void reset() { props.clear(); debugMode = false; @@ -96,7 +116,7 @@ class Cli { displayVersionOnly = false; } - private void appendPropertyTo(String arg, Properties props) { + private static void appendPropertyTo(String arg, Properties props) { final String key, value; int j = arg.indexOf('='); if (j == -1) { @@ -124,6 +144,7 @@ class Cli { Logs.info(" -h,--help Display help information"); Logs.info(" -v,--version Display version information"); Logs.info(" -X,--debug Produce execution debug output"); - System.exit(Exit.SUCCESS); + Logs.info(" -i,--interactive Run interactively - not valid in fork mode"); + exit.exit(Exit.SUCCESS); } } diff --git a/sonar-runner-dist/src/main/java/org/sonar/runner/Conf.java b/sonar-runner-dist/src/main/java/org/sonar/runner/Conf.java index 4066f83..4885a04 100644 --- a/sonar-runner-dist/src/main/java/org/sonar/runner/Conf.java +++ b/sonar-runner-dist/src/main/java/org/sonar/runner/Conf.java @@ -84,7 +84,7 @@ class Conf { return new Properties(); } - private void initRootProjectBaseDir(Properties cliProps, Properties rootProps) { + private static void initRootProjectBaseDir(Properties cliProps, Properties rootProps) { if (!cliProps.containsKey(PROPERTY_PROJECT_BASEDIR)) { String baseDir = cliProps.getProperty(PROJECT_HOME); rootProps.put(PROPERTY_PROJECT_BASEDIR, baseDir); @@ -109,7 +109,7 @@ class Conf { } - private void merge(Properties projectProps, String prefix, String module, Properties moduleProps) { + private static void merge(Properties projectProps, String prefix, String module, Properties moduleProps) { for (Map.Entry entry : moduleProps.entrySet()) { projectProps.put(prefix + module + "." + entry.getKey(), entry.getValue()); } @@ -160,7 +160,7 @@ class Conf { return moduleProps; } - private File locatePropertiesFile(Properties props, String homeKey, String relativePathFromHome, String settingsKey) { + private static File locatePropertiesFile(Properties props, String homeKey, String relativePathFromHome, String settingsKey) { File settingsFile = null; String runnerHome = props.getProperty(homeKey, ""); if (!"".equals(runnerHome)) { @@ -225,7 +225,7 @@ class Conf { } } - private void tryToFindAndLoadPropsFile(File baseDir, Properties moduleProps, String moduleId) { + private static void tryToFindAndLoadPropsFile(File baseDir, Properties moduleProps, String moduleId) { File propertyFile = new File(baseDir, SONAR_PROJECT_PROPERTIES_FILENAME); if (propertyFile.isFile()) { Properties propsFromFile = toProperties(propertyFile); diff --git a/sonar-runner-dist/src/main/java/org/sonar/runner/Main.java b/sonar-runner-dist/src/main/java/org/sonar/runner/Main.java index aae7915..d991dc2 100644 --- a/sonar-runner-dist/src/main/java/org/sonar/runner/Main.java +++ b/sonar-runner-dist/src/main/java/org/sonar/runner/Main.java @@ -19,6 +19,14 @@ */ package org.sonar.runner; +import org.sonar.runner.api.Runner; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Properties; + import org.sonar.runner.impl.Logs; /** @@ -39,6 +47,8 @@ public class Main { private final Cli cli; private final Conf conf; private final RunnerFactory runnerFactory; + private Runner runner; + private BufferedReader inputReader; Main(Exit exit, Cli cli, Conf conf, RunnerFactory runnerFactory) { this.exit = exit; @@ -48,37 +58,67 @@ public class Main { } public static void main(String[] args) { - Cli cli = new Cli().parse(args); - Main main = new Main(new Exit(), cli, new Conf(cli), new RunnerFactory()); + Exit exit = new Exit(); + Cli cli = new Cli(exit).parse(args); + cli.verify(); + Main main = new Main(exit, cli, new Conf(cli), new RunnerFactory()); main.execute(); } void execute() { - SystemInfo.print(); - if (!cli.isDisplayVersionOnly()) { - int status = executeTask(); - exit.exit(status); - } - } - - private int executeTask() { Stats stats = new Stats().start(); + try { - if (cli.isDisplayStackTrace()) { - Logs.info("Error stacktraces are turned on."); - } - runnerFactory.create(conf.properties()).execute(); + Properties p = conf.properties(); + init(p); + runner.start(); + + runAnalysis(stats, p); + if(cli.isInteractive()) { + while (waitForUser()) { + stats = new Stats().start(); + runAnalysis(stats, p); + } + } } catch (Exception e) { displayExecutionResult(stats, "FAILURE"); showError("Error during Sonar runner execution", e, cli.isDisplayStackTrace()); - return Exit.ERROR; + exit.exit(Exit.ERROR); + } + + runner.stop(); + exit.exit(Exit.SUCCESS); + } + + private void init(Properties p) throws IOException { + SystemInfo.print(); + if (cli.isDisplayVersionOnly()) { + exit.exit(Exit.SUCCESS); } + + if (cli.isDisplayStackTrace()) { + Logs.info("Error stacktraces are turned on."); + } + + runner = runnerFactory.create(p); + } + + private void runAnalysis(Stats stats, Properties p) { + runner.runAnalysis(p); displayExecutionResult(stats, "SUCCESS"); - return Exit.SUCCESS; } - private void displayExecutionResult(Stats stats, String resultMsg) { + private boolean waitForUser() throws IOException { + if (inputReader == null) { + inputReader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)); + } + Logs.info(""); + + return inputReader.readLine() != null; + } + + private static void displayExecutionResult(Stats stats, String resultMsg) { Logs.info("------------------------------------------------------------------------"); Logs.info("EXECUTION " + resultMsg); Logs.info("------------------------------------------------------------------------"); @@ -113,7 +153,7 @@ public class Main { } } - private void suggestDebugMode() { + private static void suggestDebugMode() { Logs.error("Re-run SonarQube Runner using the -X switch to enable full debug logging."); } diff --git a/sonar-runner-dist/src/main/java/org/sonar/runner/RunnerFactory.java b/sonar-runner-dist/src/main/java/org/sonar/runner/RunnerFactory.java index 0396428..8b68140 100644 --- a/sonar-runner-dist/src/main/java/org/sonar/runner/RunnerFactory.java +++ b/sonar-runner-dist/src/main/java/org/sonar/runner/RunnerFactory.java @@ -27,19 +27,19 @@ import java.util.Properties; class RunnerFactory { - Runner create(Properties props) { + Runner create(Properties props) { Runner runner; if ("fork".equals(props.getProperty("sonarRunner.mode"))) { runner = ForkedRunner.create(); String jvmArgs = props.getProperty("sonarRunner.fork.jvmArgs", ""); if (!"".equals(jvmArgs)) { - ((ForkedRunner)runner).addJvmArguments(jvmArgs.split(" ")); + ((ForkedRunner) runner).addJvmArguments(jvmArgs.split(" ")); } } else { runner = EmbeddedRunner.create(); } - runner.addProperties(props); + runner.addGlobalProperties(props); return runner; } } diff --git a/sonar-runner-dist/src/test/java/org/sonar/runner/CliTest.java b/sonar-runner-dist/src/test/java/org/sonar/runner/CliTest.java index dd63f36..81091ea 100644 --- a/sonar-runner-dist/src/test/java/org/sonar/runner/CliTest.java +++ b/sonar-runner-dist/src/test/java/org/sonar/runner/CliTest.java @@ -19,13 +19,16 @@ */ package org.sonar.runner; -import org.junit.Test; +import static org.mockito.Mockito.mock; + +import static org.mockito.Mockito.verify; +import org.junit.Test; import static org.fest.assertions.Assertions.assertThat; public class CliTest { - - Cli cli = new Cli(); + Exit exit = mock(Exit.class); + Cli cli = new Cli(exit); @Test public void should_parse_empty_arguments() { @@ -43,6 +46,13 @@ public class CliTest { assertThat(cli.properties().get("hello")).isEqualTo("world"); assertThat(cli.properties().get("boolean")).isEqualTo("true"); } + + @Test + public void dont_allow_interactive_fork() { + cli.parse(new String[]{"-i", "-DsonarRunner.mode=fork"}); + cli.verify(); + verify(exit).exit(Exit.SUCCESS); + } @Test public void should_parse_optional_task() { diff --git a/sonar-runner-dist/src/test/java/org/sonar/runner/MainTest.java b/sonar-runner-dist/src/test/java/org/sonar/runner/MainTest.java index 01a97c5..773632d 100644 --- a/sonar-runner-dist/src/test/java/org/sonar/runner/MainTest.java +++ b/sonar-runner-dist/src/test/java/org/sonar/runner/MainTest.java @@ -19,51 +19,109 @@ */ package org.sonar.runner; +import org.mockito.MockitoAnnotations; +import org.mockito.Mock; +import org.junit.Before; +import org.mockito.Mockito; +import org.mockito.InOrder; import org.junit.Test; import org.sonar.runner.api.Runner; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.Properties; +import static org.mockito.Mockito.times; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.RETURNS_MOCKS; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; public class MainTest { - Exit exit = mock(Exit.class); - Cli cli = mock(Cli.class); - Conf conf = mock(Conf.class); - RunnerFactory runnerFactory = mock(RunnerFactory.class, RETURNS_MOCKS); + @Mock + private Exit exit; + @Mock + private Cli cli; + @Mock + private Conf conf; + @Mock + private Properties properties; + @Mock + private RunnerFactory runnerFactory; + @Mock + private Runner runner; + + @Before + public void setUp() throws IOException { + MockitoAnnotations.initMocks(this); + when(runnerFactory.create(any(Properties.class))).thenReturn(runner); + when(conf.properties()).thenReturn(properties); + + } @Test public void should_execute_runner() { Main main = new Main(exit, cli, conf, runnerFactory); main.execute(); - verify(exit).exit(0); + verify(exit).exit(Exit.SUCCESS); + verify(runnerFactory).create(properties); + + verify(runner, times(1)).start(); + verify(runner, times(1)).runAnalysis(properties); + verify(runner, times(1)).stop(); } @Test public void should_fail_on_error() { - Runner runner = mock(Runner.class); - doThrow(new IllegalStateException("Error")).when(runner).execute(); + Runner runner = mock(Runner.class); + doThrow(new IllegalStateException("Error")).when(runner).runAnalysis(any(Properties.class)); when(runnerFactory.create(any(Properties.class))).thenReturn(runner); Main main = new Main(exit, cli, conf, runnerFactory); main.execute(); - verify(exit).exit(1); + verify(exit).exit(Exit.ERROR); } @Test - public void should_only_display_version() { + public void should_only_display_version() throws IOException { + + Properties p = new Properties(); when(cli.isDisplayVersionOnly()).thenReturn(true); + when(conf.properties()).thenReturn(p); + + Main main = new Main(exit, cli, conf, runnerFactory); + main.execute(); + + InOrder inOrder = Mockito.inOrder(exit, runnerFactory); + + inOrder.verify(exit, times(1)).exit(Exit.SUCCESS); + inOrder.verify(runnerFactory, times(1)).create(p); + inOrder.verify(exit, times(1)).exit(Exit.SUCCESS); + } + + @Test(timeout=30000) + public void test_interactive_mode() throws IOException { + String inputStr = "qwe" + System.lineSeparator() + "qwe" + System.lineSeparator(); + InputStream input = new ByteArrayInputStream(inputStr.getBytes(StandardCharsets.UTF_8)); + System.setIn(input); + input.close(); + + when(cli.isInteractive()).thenReturn(true); + when(cli.isDebugMode()).thenReturn(true); + when(cli.isDisplayStackTrace()).thenReturn(true); + Main main = new Main(exit, cli, conf, runnerFactory); main.execute(); - verifyZeroInteractions(runnerFactory); + + verify(runner, times(1)).start(); + verify(runner, times(3)).runAnalysis(any(Properties.class)); + verify(runner, times(1)).stop(); } } diff --git a/sonar-runner-dist/src/test/java/org/sonar/runner/RunnerFactoryTest.java b/sonar-runner-dist/src/test/java/org/sonar/runner/RunnerFactoryTest.java index 56124d8..4ecd89f 100644 --- a/sonar-runner-dist/src/test/java/org/sonar/runner/RunnerFactoryTest.java +++ b/sonar-runner-dist/src/test/java/org/sonar/runner/RunnerFactoryTest.java @@ -38,7 +38,7 @@ public class RunnerFactoryTest { Runner runner = new RunnerFactory().create(props); assertThat(runner).isInstanceOf(EmbeddedRunner.class); - assertThat(runner.properties().get("foo")).isEqualTo("bar"); + assertThat(runner.globalProperties().get("foo")).isEqualTo("bar"); } @Test @@ -49,8 +49,8 @@ public class RunnerFactoryTest { Runner runner = new RunnerFactory().create(props); assertThat(runner).isInstanceOf(ForkedRunner.class); - assertThat(runner.properties().get("foo")).isEqualTo("bar"); - assertThat(((ForkedRunner)runner).jvmArguments()).contains("-Xms128m", "-Xmx512m"); + assertThat(runner.globalProperties().get("foo")).isEqualTo("bar"); + assertThat(((ForkedRunner) runner).jvmArguments()).contains("-Xms128m", "-Xmx512m"); } @Test @@ -60,7 +60,10 @@ public class RunnerFactoryTest { Runner runner = new RunnerFactory().create(props); assertThat(runner).isInstanceOf(ForkedRunner.class); - assertThat(runner.properties().get("foo")).isEqualTo("bar"); - assertThat(((ForkedRunner)runner).jvmArguments()).isEmpty(); - } + assertThat(runner.globalProperties().get("foo")).isEqualTo("bar"); + + assertThat(runner).isInstanceOf(ForkedRunner.class); + assertThat(runner.globalProperties().get("foo")).isEqualTo("bar"); + assertThat(((ForkedRunner) runner).jvmArguments()).isEmpty(); + } } diff --git a/sonar-runner-dist/src/test/java/org/sonar/runner/StatsTest.java b/sonar-runner-dist/src/test/java/org/sonar/runner/StatsTest.java index 4f39d99..b7062ad 100644 --- a/sonar-runner-dist/src/test/java/org/sonar/runner/StatsTest.java +++ b/sonar-runner-dist/src/test/java/org/sonar/runner/StatsTest.java @@ -19,17 +19,29 @@ */ package org.sonar.runner; -import org.junit.Test; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import org.junit.Test; import static org.fest.assertions.Assertions.assertThat; public class StatsTest { @Test - public void shouldPrintStats() { + public void shouldPrintStats() throws UnsupportedEncodingException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + System.setOut(new PrintStream(output)); new Stats().start().stop(); - //TODO mock Logs + + String out = output.toString("UTF-8"); + String[] lines = out.split("\n"); + + assertThat(lines).hasSize(2); + + assertThat(lines[0]).startsWith("Total time: "); + assertThat(lines[1]).startsWith("Final Memory: "); } @Test diff --git a/sonar-runner-impl/pom.xml b/sonar-runner-impl/pom.xml index 6d48d0c..95fc302 100644 --- a/sonar-runner-impl/pom.xml +++ b/sonar-runner-impl/pom.xml @@ -33,6 +33,11 @@ ${project.version} provided + + ${project.groupId} + sonar-runner-batch-interface + ${project.version} + @@ -50,6 +55,12 @@ mockito-all test + + org.assertj + assertj-core + 1.7.1 + test + org.mortbay.jetty jetty diff --git a/sonar-runner-impl/src/main/java/org/sonar/runner/impl/BatchLauncher.java b/sonar-runner-impl/src/main/java/org/sonar/runner/impl/BatchLauncher.java deleted file mode 100644 index edc057a..0000000 --- a/sonar-runner-impl/src/main/java/org/sonar/runner/impl/BatchLauncher.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * SonarQube Runner - Implementation - * Copyright (C) 2011 SonarSource - * dev@sonar.codehaus.org - * - * 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 02 - */ -package org.sonar.runner.impl; - -import org.sonar.home.cache.PersistentCacheBuilder; - -import org.sonar.home.cache.PersistentCache; - -import java.io.File; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.List; -import java.util.Properties; - -public class BatchLauncher { - final String isolatedLauncherClass; - private final TempCleaning tempCleaning; - - /** - * For unit tests - */ - BatchLauncher(String isolatedLauncherClass, TempCleaning tempCleaning) { - this.isolatedLauncherClass = isolatedLauncherClass; - this.tempCleaning = tempCleaning; - } - - public BatchLauncher() { - this("org.sonar.runner.batch.IsolatedLauncher", new TempCleaning()); - } - - public void execute(Properties props, List extensions) { - ServerConnection serverConnection = ServerConnection.create(props, getCache(props)); - JarDownloader jarDownloader = new JarDownloader(serverConnection); - doExecute(jarDownloader, props, extensions); - } - - private static PersistentCache getCache(Properties props) { - PersistentCacheBuilder builder = new PersistentCacheBuilder(); - - if (!"true".equals(props.getProperty("sonar.enableHttpCache"))) { - builder.forceUpdate(true); - } - - return builder.build(); - } - - private static String[][] getMaskRules(final Properties props) { - String maskRulesProp = props.getProperty(InternalProperties.RUNNER_MASK_RULES, null); - String[] maskRulesConcat = maskRulesProp != null ? maskRulesProp.split(",") : new String[0]; - String[][] maskRules = new String[maskRulesConcat.length][2]; - for (int i = 0; i < maskRulesConcat.length; i++) { - String[] splitted = maskRulesConcat[i].split("\\|"); - maskRules[i][0] = splitted[0]; - maskRules[i][1] = splitted.length > 1 ? splitted[1] : ""; - } - return maskRules; - } - - /** - * @return the {@link org.sonar.runner.batch.IsolatedLauncher} instance for unit tests - */ - Object doExecute(final JarDownloader jarDownloader, final Properties props, final List extensions) { - return AccessController.doPrivileged(new PrivilegedAction() { - public Object run() { - List jarFiles = jarDownloader.download(); - Logs.debug("Create isolated classloader..."); - String[][] maskRules = getMaskRules(props); - IsolatedClassloader classloader = new IsolatedClassloader(getClass().getClassLoader(), maskRules); - classloader.addFiles(jarFiles); - Object launcher = delegateExecution(classloader, props, extensions); - tempCleaning.clean(); - return launcher; - } - - private Object delegateExecution(IsolatedClassloader classloader, Properties properties, List extensions) { - ClassLoader initialContextClassLoader = Thread.currentThread().getContextClassLoader(); - try { - Thread.currentThread().setContextClassLoader(classloader); - Class launcherClass = classloader.loadClass(isolatedLauncherClass); - Method executeMethod = launcherClass.getMethod("execute", Properties.class, List.class); - Object launcher = launcherClass.newInstance(); - Logs.debug("Start IsolatedLauncher"); - executeMethod.invoke(launcher, properties, extensions); - return launcher; - } catch (InvocationTargetException e) { - // Unwrap original exception - throw new RunnerException("Unable to execute Sonar", e.getTargetException()); - } catch (Exception e) { - // Catch all other exceptions, which relates to reflection - throw new RunnerException("Unable to execute SonarQube", e); - } finally { - Thread.currentThread().setContextClassLoader(initialContextClassLoader); - } - } - }); - } - -} diff --git a/sonar-runner-impl/src/main/java/org/sonar/runner/impl/BatchLauncherMain.java b/sonar-runner-impl/src/main/java/org/sonar/runner/impl/BatchLauncherMain.java index ab70776..1e4ab2c 100644 --- a/sonar-runner-impl/src/main/java/org/sonar/runner/impl/BatchLauncherMain.java +++ b/sonar-runner-impl/src/main/java/org/sonar/runner/impl/BatchLauncherMain.java @@ -19,16 +19,18 @@ */ package org.sonar.runner.impl; +import org.sonar.runner.batch.IsolatedLauncher; + import java.io.FileInputStream; import java.io.IOException; import java.util.Collections; import java.util.Properties; public class BatchLauncherMain { - private final BatchLauncher launcher; + private final IsolatedLauncherFactory launcherFactory; - BatchLauncherMain(BatchLauncher l) { - this.launcher = l; + BatchLauncherMain(IsolatedLauncherFactory factory) { + this.launcherFactory = factory; } void execute(String[] args) throws IOException { @@ -36,10 +38,13 @@ public class BatchLauncherMain { throw new IllegalArgumentException("Missing path to properties file"); } Properties props = loadProperties(args[0]); - launcher.execute(props, Collections.emptyList()); + IsolatedLauncher launcher = launcherFactory.createLauncher(props); + launcher.start(props, Collections.emptyList()); + launcher.execute(props); + launcher.stop(); } - private Properties loadProperties(String arg) throws IOException { + private static Properties loadProperties(String arg) throws IOException { Properties props = new Properties(); try (FileInputStream input = new FileInputStream(arg)) { props.load(input); @@ -51,6 +56,6 @@ public class BatchLauncherMain { } public static void main(String[] args) throws IOException { - new BatchLauncherMain(new BatchLauncher()).execute(args); + new BatchLauncherMain(new IsolatedLauncherFactory()).execute(args); } } diff --git a/sonar-runner-impl/src/main/java/org/sonar/runner/impl/IsolatedLauncherFactory.java b/sonar-runner-impl/src/main/java/org/sonar/runner/impl/IsolatedLauncherFactory.java new file mode 100644 index 0000000..3a9b594 --- /dev/null +++ b/sonar-runner-impl/src/main/java/org/sonar/runner/impl/IsolatedLauncherFactory.java @@ -0,0 +1,119 @@ +/* + * SonarQube Runner - Implementation + * Copyright (C) 2011 SonarSource + * dev@sonar.codehaus.org + * + * 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 02 + */ +package org.sonar.runner.impl; + +import org.sonar.runner.batch.IsolatedLauncher; +import org.sonar.home.cache.PersistentCacheBuilder; +import org.sonar.home.cache.PersistentCache; + +import java.io.File; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.List; +import java.util.Properties; + +public class IsolatedLauncherFactory { + static final String ISOLATED_LAUNCHER_IMPL = "org.sonar.runner.batch.BatchIsolatedLauncher"; + private final TempCleaning tempCleaning; + private final String launcherImplClassName; + + /** + * For unit tests + */ + IsolatedLauncherFactory(String isolatedLauncherClassName, TempCleaning tempCleaning) { + this.tempCleaning = tempCleaning; + this.launcherImplClassName = isolatedLauncherClassName; + } + + public IsolatedLauncherFactory() { + this(ISOLATED_LAUNCHER_IMPL, new TempCleaning()); + } + + private static PersistentCache getCache(Properties props) { + PersistentCacheBuilder builder = new PersistentCacheBuilder(); + + if (!"true".equals(props.getProperty("sonar.enableHttpCache"))) { + builder.forceUpdate(true); + } + + return builder.build(); + } + + static String[][] getMaskRules(final Properties props) { + String maskRulesProp = props.getProperty(InternalProperties.RUNNER_MASK_RULES, null); + String[] maskRulesConcat = maskRulesProp != null ? maskRulesProp.split(",") : (new String[0]); + String[][] maskRules = new String[maskRulesConcat.length][2]; + for (int i = 0; i < maskRulesConcat.length; i++) { + String[] splitted = maskRulesConcat[i].split("\\|"); + maskRules[i][0] = splitted[0]; + maskRules[i][1] = splitted.length > 1 ? splitted[1] : ""; + } + return maskRules; + } + + private static void addIsolatedLauncherMaskRule(Properties props) { + String unmask = "UNMASK|org.sonar.runner.batch.IsolatedLauncher"; + String currentRules = (String) props.get(InternalProperties.RUNNER_MASK_RULES); + + if (currentRules == null) { + props.put(InternalProperties.RUNNER_MASK_RULES, unmask); + } else { + props.put(InternalProperties.RUNNER_MASK_RULES, currentRules + "," + unmask); + } + } + + private ClassLoader createClassLoader(List jarFiles, final Properties props) { + Properties copy = new Properties(); + copy.putAll(props); + addIsolatedLauncherMaskRule(copy); + String[][] maskRules = getMaskRules(copy); + IsolatedClassloader classloader = new IsolatedClassloader(getClass().getClassLoader(), maskRules); + classloader.addFiles(jarFiles); + + return classloader; + } + + public IsolatedLauncher createLauncher(Properties props) { + ServerConnection serverConnection = ServerConnection.create(props, getCache(props)); + JarDownloader jarDownloader = new JarDownloader(serverConnection); + + return createLauncher(jarDownloader, props); + } + + IsolatedLauncher createLauncher(final JarDownloader jarDownloader, final Properties props) { + return AccessController.doPrivileged(new PrivilegedAction() { + @Override + public IsolatedLauncher run() { + try { + List jarFiles = jarDownloader.download(); + Logs.debug("Create isolated classloader..."); + ClassLoader cl = createClassLoader(jarFiles, props); + IsolatedLauncher objProxy = IsolatedLauncherProxy.create(cl, IsolatedLauncher.class, launcherImplClassName); + tempCleaning.clean(); + + return objProxy; + } catch (Exception e) { + // Catch all other exceptions, which relates to reflection + throw new RunnerException("Unable to execute SonarQube", e); + } + } + }); + } +} diff --git a/sonar-runner-impl/src/main/java/org/sonar/runner/impl/IsolatedLauncherProxy.java b/sonar-runner-impl/src/main/java/org/sonar/runner/impl/IsolatedLauncherProxy.java new file mode 100644 index 0000000..7d2a96b --- /dev/null +++ b/sonar-runner-impl/src/main/java/org/sonar/runner/impl/IsolatedLauncherProxy.java @@ -0,0 +1,82 @@ +/* + * SonarQube Runner - Implementation + * Copyright (C) 2011 SonarSource + * dev@sonar.codehaus.org + * + * 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 02 + */ +package org.sonar.runner.impl; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.lang.reflect.UndeclaredThrowableException; + +public class IsolatedLauncherProxy implements InvocationHandler { + private Object proxied = null; + private ClassLoader cl = null; + + private IsolatedLauncherProxy(ClassLoader cl, Object proxied) { + this.cl = cl; + this.proxied = proxied; + } + + public static T create(ClassLoader cl, Class interfaceClass, String proxiedClassName) throws ReflectiveOperationException { + Object proxied = createProxiedObject(cl, proxiedClassName); + // interfaceClass needs to be loaded with a parent ClassLoader (common to both ClassLoaders) + // In addition, Proxy.newProxyInstance checks if the target ClassLoader sees the same class as the one given + Class loadedInterfaceClass = cl.loadClass(interfaceClass.getName()); + return (T) create(cl, proxied, loadedInterfaceClass); + } + + public static T create(ClassLoader cl, Object proxied, Class interfaceClass) { + Class[] c = {interfaceClass}; + return (T) Proxy.newProxyInstance(cl, c, new IsolatedLauncherProxy(cl, proxied)); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + ClassLoader initialContextClassLoader = Thread.currentThread().getContextClassLoader(); + + try { + Thread.currentThread().setContextClassLoader(cl); + Logs.debug("Execution " + method.getName()); + return method.invoke(proxied, args); + } catch (UndeclaredThrowableException | InvocationTargetException e) { + throw unwrapException(e); + } finally { + Thread.currentThread().setContextClassLoader(initialContextClassLoader); + } + } + + private static Throwable unwrapException(Throwable e) { + Throwable cause = e; + + while (cause.getCause() != null) { + if (cause instanceof UndeclaredThrowableException || cause instanceof InvocationTargetException) { + cause = cause.getCause(); + } else { + break; + } + } + return cause; + } + + private static Object createProxiedObject(ClassLoader cl, String proxiedClassName) throws ClassNotFoundException, InstantiationException, IllegalAccessException { + Class proxiedClass = cl.loadClass(proxiedClassName); + return proxiedClass.newInstance(); + } +} diff --git a/sonar-runner-impl/src/main/java/org/sonar/runner/impl/Jars.java b/sonar-runner-impl/src/main/java/org/sonar/runner/impl/Jars.java index 5e4342e..102ccdc 100644 --- a/sonar-runner-impl/src/main/java/org/sonar/runner/impl/Jars.java +++ b/sonar-runner-impl/src/main/java/org/sonar/runner/impl/Jars.java @@ -89,6 +89,7 @@ class Jars { this.connection = conn; } + @Override public void download(String filename, File toFile) throws IOException { connection.download(BATCH_PATH + filename, toFile); } diff --git a/sonar-runner-impl/src/main/java/org/sonar/runner/impl/ServerConnection.java b/sonar-runner-impl/src/main/java/org/sonar/runner/impl/ServerConnection.java index c5b67e3..915fba8 100644 --- a/sonar-runner-impl/src/main/java/org/sonar/runner/impl/ServerConnection.java +++ b/sonar-runner-impl/src/main/java/org/sonar/runner/impl/ServerConnection.java @@ -55,7 +55,7 @@ class ServerConnection { this.isModePreview = preview; } - private String removeEndSlash(String url) { + private static String removeEndSlash(String url) { if (url == null) { return null; } diff --git a/sonar-runner-impl/src/main/java/org/sonar/runner/impl/TempCleaning.java b/sonar-runner-impl/src/main/java/org/sonar/runner/impl/TempCleaning.java index 698a174..4067b96 100644 --- a/sonar-runner-impl/src/main/java/org/sonar/runner/impl/TempCleaning.java +++ b/sonar-runner-impl/src/main/java/org/sonar/runner/impl/TempCleaning.java @@ -28,7 +28,7 @@ import org.apache.commons.io.filefilter.PrefixFileFilter; /** * The file sonar-runner-batch.jar is locked by the classloader on Windows and can't be dropped at the end of the execution. - * See {@link BatchLauncher} + * See {@link IsolatedLauncherFactory} */ class TempCleaning { static final int ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000; diff --git a/sonar-runner-impl/src/test/java/org/sonar/runner/impl/BatchLauncherMainTest.java b/sonar-runner-impl/src/test/java/org/sonar/runner/impl/BatchLauncherMainTest.java index 996fc42..0f4a2fa 100644 --- a/sonar-runner-impl/src/test/java/org/sonar/runner/impl/BatchLauncherMainTest.java +++ b/sonar-runner-impl/src/test/java/org/sonar/runner/impl/BatchLauncherMainTest.java @@ -19,14 +19,17 @@ */ package org.sonar.runner.impl; +import org.sonar.runner.batch.IsolatedLauncher; + import org.junit.Test; import org.mockito.ArgumentMatcher; import java.io.File; import java.net.URL; -import java.util.List; import java.util.Properties; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; import static org.fest.assertions.Fail.fail; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.mock; @@ -34,31 +37,29 @@ import static org.mockito.Mockito.verify; public class BatchLauncherMainTest { - BatchLauncher launcher = mock(BatchLauncher.class); + IsolatedLauncherFactory launcherFactory = mock(IsolatedLauncherFactory.class); @Test public void should_load_properties_and_execute() throws Exception { URL url = getClass().getResource("/org/sonar/runner/impl/BatchLauncherMainTest/props.properties"); - BatchLauncherMain main = new BatchLauncherMain(launcher); - main.execute(new String[]{new File(url.toURI()).getAbsolutePath()}); + when(launcherFactory.createLauncher(any(Properties.class))).thenReturn(mock(IsolatedLauncher.class)); + + BatchLauncherMain main = new BatchLauncherMain(launcherFactory); + new File(url.toURI()).getAbsolutePath(); + main.execute(new String[] {new File(url.toURI()).getAbsolutePath()}); - verify(launcher).execute(argThat(new ArgumentMatcher() { + verify(launcherFactory).createLauncher(argThat(new ArgumentMatcher() { @Override public boolean matches(Object o) { return ((Properties) o).get("sonar.login").equals("foo"); } - }), argThat(new ArgumentMatcher>() { - @Override - public boolean matches(Object o) { - return ((List) o).isEmpty(); - } })); } @Test public void should_fail_if_missing_path_to_properties_file() { try { - BatchLauncherMain main = new BatchLauncherMain(launcher); + BatchLauncherMain main = new BatchLauncherMain(launcherFactory); main.execute(new String[0]); fail(); } catch (Exception e) { @@ -69,8 +70,8 @@ public class BatchLauncherMainTest { @Test public void should_fail_if_properties_file_does_not_exist() { try { - BatchLauncherMain main = new BatchLauncherMain(launcher); - main.execute(new String[]{"unknown/file.properties"}); + BatchLauncherMain main = new BatchLauncherMain(launcherFactory); + main.execute(new String[] {"unknown/file.properties"}); fail(); } catch (Exception e) { // success diff --git a/sonar-runner-impl/src/test/java/org/sonar/runner/impl/BatchLauncherTest.java b/sonar-runner-impl/src/test/java/org/sonar/runner/impl/BatchLauncherTest.java deleted file mode 100644 index 763d7c5..0000000 --- a/sonar-runner-impl/src/test/java/org/sonar/runner/impl/BatchLauncherTest.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * SonarQube Runner - Implementation - * Copyright (C) 2011 SonarSource - * dev@sonar.codehaus.org - * - * 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 02 - */ -package org.sonar.runner.impl; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Properties; -import org.junit.Test; -import org.sonar.runner.batch.IsolatedLauncher; - -import static org.fest.assertions.Assertions.assertThat; -import static org.fest.assertions.Fail.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -public class BatchLauncherTest { - - JarDownloader jarDownloader = mock(JarDownloader.class); - - @Test - public void should_download_jars_and_execute_batch() { - TempCleaning tempCleaning = mock(TempCleaning.class); - BatchLauncher launcher = new BatchLauncher(FakeIsolatedLauncher.class.getName(), tempCleaning); - Properties props = new Properties(); - props.put("foo", "bar"); - - // Unmask the current classloader in order to access FakeIsolatedLauncher - props.put(InternalProperties.RUNNER_MASK_RULES, "UNMASK|org.sonar.runner.impl."); - List extensions = new ArrayList(); - - FakeIsolatedLauncher isolatedLauncher = (FakeIsolatedLauncher) launcher.doExecute(jarDownloader, props, extensions); - assertThat(isolatedLauncher.props.get("foo")).isEqualTo("bar"); - assertThat(isolatedLauncher.extensions).isSameAs(extensions); - verify(jarDownloader).download(); - verify(tempCleaning).clean(); - } - - @Test - public void should_use_isolated_classloader() { - BatchLauncher launcher = new BatchLauncher(FakeIsolatedLauncher.class.getName(), mock(TempCleaning.class)); - Properties props = new Properties(); - - // The current classloader in not available -> fail to load FakeIsolatedLauncher - props.put(InternalProperties.RUNNER_MASK_RULES, ""); - try { - launcher.doExecute(jarDownloader, props, Collections.emptyList()); - fail(); - } catch (RunnerException e) { - // success - } - } - - @Test - public void verify_isolated_classloader_name() { - // the class IsolatedLauncher should not be loaded in the classloader of BatchLauncher, - // that's why it's referenced by its name - assertThat(new BatchLauncher().isolatedLauncherClass).isEqualTo(IsolatedLauncher.class.getName()); - } - - @Test - public void test_real_execution() { - // verify the creation of dependent components - Properties props = new Properties(); - List extensions = Collections.emptyList(); - BatchLauncher launcher = spy(new BatchLauncher()); - doReturn(new Object()).when(launcher).doExecute(any(JarDownloader.class), eq(props), eq(extensions)); - - launcher.execute(props, extensions); - - verify(launcher).execute(props, extensions); - } - - public static class FakeIsolatedLauncher { - public Properties props = null; - public List extensions = null; - - public void execute(Properties props, List extensions) { - this.props = props; - this.extensions = extensions; - } - } -} diff --git a/sonar-runner-impl/src/test/java/org/sonar/runner/impl/IsolatedLauncherFactoryTest.java b/sonar-runner-impl/src/test/java/org/sonar/runner/impl/IsolatedLauncherFactoryTest.java new file mode 100644 index 0000000..bfdd54a --- /dev/null +++ b/sonar-runner-impl/src/test/java/org/sonar/runner/impl/IsolatedLauncherFactoryTest.java @@ -0,0 +1,106 @@ +/* + * SonarQube Runner - Implementation + * Copyright (C) 2011 SonarSource + * dev@sonar.codehaus.org + * + * 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 02 + */ +package org.sonar.runner.impl; + +import org.junit.Before; +import org.sonar.runner.batch.IsolatedLauncher; + +import java.util.List; +import java.util.Properties; + +import org.junit.Test; +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class IsolatedLauncherFactoryTest { + IsolatedLauncherFactory factory; + Properties props; + TempCleaning tempCleaning; + JarDownloader jarDownloader; + + @Before + public void setUp() { + tempCleaning = mock(TempCleaning.class); + factory = new IsolatedLauncherFactory(FakeIsolatedLauncher.class.getName(), tempCleaning); + props = new Properties(); + jarDownloader = mock(JarDownloader.class); + } + + @Test + public void should_process_mask_rules() { + Properties p = new Properties(); + p.put(InternalProperties.RUNNER_MASK_RULES, "UNMASK|org.sonar.runner.impl."); + p.put("a", "b"); + + String[][] maskRules = IsolatedLauncherFactory.getMaskRules(p); + + assertThat(maskRules).hasSize(1); + assertThat(maskRules[0]).hasSize(2); + assertThat(maskRules[0][0]).isEqualTo("UNMASK"); + assertThat(maskRules[0][1]).isEqualTo("org.sonar.runner.impl."); + } + + @Test + public void should_download_jars_and_execute_batch() { + props.put("foo", "bar"); + + // Unmask the current classloader in order to access FakeIsolatedLauncher + props.put(InternalProperties.RUNNER_MASK_RULES, "UNMASK|org.sonar.runner.impl."); + + IsolatedLauncher isolatedLauncher = factory.createLauncher(jarDownloader, props); + isolatedLauncher.execute(props); + + verify(jarDownloader).download(); + verify(tempCleaning).clean(); + assertThat(FakeIsolatedLauncher.props.get("foo")).isEqualTo("bar"); + assertThat(isolatedLauncher.getClass().getClassLoader().getClass().getSimpleName()).isEqualTo("IsolatedClassloader"); + } + + @Test + public void should_use_isolated_classloader() { + // The current classloader in not available -> fail to load FakeIsolatedLauncher + props.put(InternalProperties.RUNNER_MASK_RULES, ""); + try { + factory.createLauncher(jarDownloader, props); + fail(); + } catch (RunnerException e) { + // success + } + } + + public static class FakeIsolatedLauncher implements IsolatedLauncher { + public static Properties props = null; + + @Override + public void start(Properties properties, List extensions) { + } + + @Override + public void stop() { + } + + @Override + public void execute(Properties properties) { + FakeIsolatedLauncher.props = properties; + } + } +} diff --git a/sonar-runner-impl/src/test/java/org/sonar/runner/impl/IsolatedLauncherProxyTest.java b/sonar-runner-impl/src/test/java/org/sonar/runner/impl/IsolatedLauncherProxyTest.java new file mode 100644 index 0000000..8a2bb01 --- /dev/null +++ b/sonar-runner-impl/src/test/java/org/sonar/runner/impl/IsolatedLauncherProxyTest.java @@ -0,0 +1,74 @@ +/* + * SonarQube Runner - Implementation + * Copyright (C) 2011 SonarSource + * dev@sonar.codehaus.org + * + * 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 02 + */ +package org.sonar.runner.impl; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.concurrent.Callable; + +import static org.assertj.core.api.Assertions.assertThat; +import org.sonar.runner.batch.BatchIsolatedLauncher; +import org.junit.Test; +import org.junit.Before; + +public class IsolatedLauncherProxyTest { + BatchIsolatedLauncher laucherProxy = null; + ClassLoader cl = null; + + @Before + public void setUp() { + cl = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader()); + } + + @Test + public void delegate_proxied() throws ClassNotFoundException, InstantiationException, IllegalAccessException { + String str = "test"; + CharSequence s = IsolatedLauncherProxy.create(cl, str, CharSequence.class); + assertThat(s).isEqualTo(str); + } + + @Test(expected = IllegalStateException.class) + public void exceptions_unwrapped() throws ReflectiveOperationException { + Runnable r = IsolatedLauncherProxy.create(cl, Runnable.class, ExceptionThrower.class.getName()); + r.run(); + } + + @Test + public void create_proxied() throws Exception { + Callable c = IsolatedLauncherProxy.create(cl, Callable.class, SimpleClass.class.getName()); + assertThat(c.getClass().getClassLoader()).isEqualTo(cl); + assertThat(c.getClass().getClassLoader()).isNotEqualTo(Thread.currentThread().getContextClassLoader()); + assertThat(c.call()).isEqualTo(URLClassLoader.class.getSimpleName()); + } + + public static class ExceptionThrower implements Runnable { + @Override + public void run() { + throw new IllegalStateException("message"); + } + } + + public static class SimpleClass implements Callable { + @Override + public String call() throws Exception { + return Thread.currentThread().getContextClassLoader().getClass().getSimpleName(); + } + } +}