aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-process-monitor/src/test
diff options
context:
space:
mode:
authorSimon Brandhof <simon.brandhof@sonarsource.com>2017-03-10 15:01:01 +0100
committerSimon Brandhof <simon.brandhof@sonarsource.com>2017-03-13 13:54:03 +0100
commit6fa3d925c688fa8e67480c7a69ded9f86aba5326 (patch)
tree11f4baac39111ae6822f945faf344e88b99dec7c /server/sonar-process-monitor/src/test
parent857d12fa9909a5b5fde7a42f231b0b8d42e50303 (diff)
downloadsonarqube-6fa3d925c688fa8e67480c7a69ded9f86aba5326.tar.gz
sonarqube-6fa3d925c688fa8e67480c7a69ded9f86aba5326.zip
SONAR-8816 automatic election of web leader in cluster mode
Diffstat (limited to 'server/sonar-process-monitor/src/test')
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/AppFileSystemTest.java197
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/AppLoggingTest.java323
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/AppReloaderImplTest.java110
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/AppStateFactoryTest.java48
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/AppStateImplTest.java84
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/SchedulerImplTest.java443
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/TestAppState.java77
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/AppStateClusterImplTest.java125
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/ClusterPropertiesTest.java136
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/HazelcastHelper.java91
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/config/AppSettingsImplTest.java (renamed from server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java)26
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/config/AppSettingsLoaderImplTest.java104
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/config/ClusterSettingsTest.java134
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/config/CommandLineParserTest.java61
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/config/FileSystemSettingsTest.java75
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/config/JdbcSettingsTest.java220
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/config/TestAppSettings.java60
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/process/JavaCommandTest.java (renamed from server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java)11
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/process/JavaProcessLauncherImplTest.java166
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/process/LifecycleTest.java111
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/process/ProcessMonitorImplTest.java111
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/process/SQProcessTest.java327
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/process/StopRequestWatcherImplTest.java114
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/application/process/StreamGobblerTest.java (renamed from server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/StreamGobblerTest.java)6
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java49
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java630
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java39
-rw-r--r--server/sonar-process-monitor/src/test/resources/logback-test.xml18
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt1
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt1
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt3
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt1
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties3
-rw-r--r--server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jarbin854048 -> 0 bytes
34 files changed, 3159 insertions, 746 deletions
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/AppFileSystemTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/AppFileSystemTest.java
new file mode 100644
index 00000000000..16c11d89946
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/AppFileSystemTest.java
@@ -0,0 +1,197 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.BasicFileAttributes;
+import javax.annotation.CheckForNull;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.AllProcessesCommands;
+import org.sonar.process.ProcessProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.process.ProcessCommands.MAX_PROCESSES;
+
+public class AppFileSystemTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private File homeDir;
+ private File dataDir;
+ private File tempDir;
+ private File logsDir;
+ private File webDir;
+ private TestAppSettings settings = new TestAppSettings();
+ private AppFileSystem underTest = new AppFileSystem(settings);
+
+ @Before
+ public void before() throws IOException {
+ homeDir = temp.newFolder();
+ dataDir = new File(homeDir, "data");
+ tempDir = new File(homeDir, "temp");
+ logsDir = new File(homeDir, "logs");
+ webDir = new File(homeDir, "web");
+
+ settings.getProps().set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
+ settings.getProps().set(ProcessProperties.PATH_DATA, dataDir.getAbsolutePath());
+ settings.getProps().set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath());
+ settings.getProps().set(ProcessProperties.PATH_LOGS, logsDir.getAbsolutePath());
+ settings.getProps().set(ProcessProperties.PATH_WEB, webDir.getAbsolutePath());
+ }
+
+ @Test
+ public void reset_creates_dirs_if_they_don_t_exist() throws Exception {
+ assertThat(dataDir).doesNotExist();
+
+ underTest.reset();
+
+ assertThat(dataDir).exists().isDirectory();
+ assertThat(logsDir).exists().isDirectory();
+ assertThat(tempDir).exists().isDirectory();
+ assertThat(webDir).exists().isDirectory();
+
+ underTest.reset();
+
+ assertThat(dataDir).exists().isDirectory();
+ assertThat(logsDir).exists().isDirectory();
+ assertThat(tempDir).exists().isDirectory();
+ assertThat(webDir).exists().isDirectory();
+ }
+
+ @Test
+ public void reset_deletes_content_of_temp_dir_but_not_temp_dir_itself_if_it_already_exists() throws Exception {
+ assertThat(tempDir.mkdir()).isTrue();
+ Object tempDirKey = getFileKey(tempDir);
+ File fileInTempDir = new File(tempDir, "someFile.txt");
+ assertThat(fileInTempDir.createNewFile()).isTrue();
+ File subDirInTempDir = new File(tempDir, "subDir");
+ assertThat(subDirInTempDir.mkdir()).isTrue();
+
+ underTest.reset();
+
+ assertThat(tempDir).exists();
+ assertThat(fileInTempDir).doesNotExist();
+ assertThat(subDirInTempDir).doesNotExist();
+ assertThat(getFileKey(tempDir)).isEqualTo(tempDirKey);
+ }
+
+ @Test
+ public void reset_deletes_content_of_temp_dir_but_not_sharedmemory_file() throws Exception {
+ assertThat(tempDir.mkdir()).isTrue();
+ File sharedmemory = new File(tempDir, "sharedmemory");
+ assertThat(sharedmemory.createNewFile()).isTrue();
+ FileUtils.write(sharedmemory, "toto");
+ Object fileKey = getFileKey(sharedmemory);
+
+ Object tempDirKey = getFileKey(tempDir);
+ File fileInTempDir = new File(tempDir, "someFile.txt");
+ assertThat(fileInTempDir.createNewFile()).isTrue();
+
+ underTest.reset();
+
+ assertThat(tempDir).exists();
+ assertThat(fileInTempDir).doesNotExist();
+ assertThat(getFileKey(tempDir)).isEqualTo(tempDirKey);
+ assertThat(getFileKey(sharedmemory)).isEqualTo(fileKey);
+ // content of sharedMemory file is reset
+ assertThat(FileUtils.readFileToString(sharedmemory)).isNotEqualTo("toto");
+ }
+
+ @Test
+ public void reset_cleans_the_sharedmemory_file() throws IOException {
+ assertThat(tempDir.mkdir()).isTrue();
+ try (AllProcessesCommands commands = new AllProcessesCommands(tempDir)) {
+ for (int i = 0; i < MAX_PROCESSES; i++) {
+ commands.create(i).setUp();
+ }
+
+ underTest.reset();
+
+ for (int i = 0; i < MAX_PROCESSES; i++) {
+ assertThat(commands.create(i).isUp()).isFalse();
+ }
+ }
+ }
+
+ @CheckForNull
+ private static Object getFileKey(File fileInTempDir) throws IOException {
+ Path path = Paths.get(fileInTempDir.toURI());
+ BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
+ return attrs.fileKey();
+ }
+
+ @Test
+ public void reset_throws_ISE_if_data_dir_is_a_file() throws Exception {
+ resetThrowsISEIfDirIsAFile(ProcessProperties.PATH_DATA);
+ }
+
+ @Test
+ public void reset_throws_ISE_if_web_dir_is_a_file() throws Exception {
+ resetThrowsISEIfDirIsAFile(ProcessProperties.PATH_WEB);
+ }
+
+ @Test
+ public void reset_throws_ISE_if_logs_dir_is_a_file() throws Exception {
+ resetThrowsISEIfDirIsAFile(ProcessProperties.PATH_LOGS);
+ }
+
+ @Test
+ public void reset_throws_ISE_if_temp_dir_is_a_file() throws Exception {
+ resetThrowsISEIfDirIsAFile(ProcessProperties.PATH_TEMP);
+ }
+
+ private void resetThrowsISEIfDirIsAFile(String property) throws IOException {
+ File file = new File(homeDir, "zoom.store");
+ assertThat(file.createNewFile()).isTrue();
+ settings.getProps().set(property, file.getAbsolutePath());
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Property '" + property + "' is not valid, not a directory: " + file.getAbsolutePath());
+
+ underTest.reset();
+ }
+
+ @Test
+ public void fail_if_required_directory_is_a_file() throws Exception {
+ // <home>/data is missing
+ FileUtils.forceMkdir(webDir);
+ FileUtils.forceMkdir(logsDir);
+ FileUtils.touch(dataDir);
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Property 'sonar.path.data' is not valid, not a directory: " + dataDir.getAbsolutePath());
+
+ underTest.reset();
+ }
+
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/AppLoggingTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/AppLoggingTest.java
new file mode 100644
index 00000000000..61b5feb26db
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/AppLoggingTest.java
@@ -0,0 +1,323 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.ConsoleAppender;
+import ch.qos.logback.core.FileAppender;
+import ch.qos.logback.core.encoder.Encoder;
+import ch.qos.logback.core.joran.spi.ConsoleTarget;
+import ch.qos.logback.core.rolling.RollingFileAppender;
+import java.io.File;
+import java.io.IOException;
+import java.util.Iterator;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.slf4j.LoggerFactory;
+import org.sonar.application.config.AppSettings;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.logging.LogbackHelper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.slf4j.Logger.ROOT_LOGGER_NAME;
+import static org.sonar.application.process.StreamGobbler.LOGGER_GOBBLER;
+
+public class AppLoggingTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private File logDir;
+
+ private AppSettings settings = new TestAppSettings();
+ private AppLogging underTest = new AppLogging(settings);
+
+ @Before
+ public void setUp() throws Exception {
+ logDir = temp.newFolder();
+ settings.getProps().set(ProcessProperties.PATH_LOGS, logDir.getAbsolutePath());
+ }
+
+ @AfterClass
+ public static void resetLogback() throws Exception {
+ new LogbackHelper().resetFromXml("/logback-test.xml");
+ }
+
+ @Test
+ public void no_writing_to_sonar_log_file_when_running_from_sonar_script() {
+ emulateRunFromSonarScript();
+
+ LoggerContext ctx = underTest.configure();
+
+ ctx.getLoggerList().forEach(AppLoggingTest::verifyNoFileAppender);
+ }
+
+ @Test
+ public void root_logger_only_writes_to_console_with_formatting_when_running_from_sonar_script() {
+ emulateRunFromSonarScript();
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+ ConsoleAppender<ILoggingEvent> consoleAppender = (ConsoleAppender<ILoggingEvent>) rootLogger.getAppender("APP_CONSOLE");
+ verifyAppFormattedLogEncoder(consoleAppender.getEncoder());
+ assertThat(rootLogger.iteratorForAppenders()).hasSize(1);
+ }
+
+ @Test
+ public void gobbler_logger_writes_to_console_without_formatting_when_running_from_sonar_script() {
+ emulateRunFromSonarScript();
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger gobblerLogger = ctx.getLogger(LOGGER_GOBBLER);
+ verifyGobblerConsoleAppender(gobblerLogger);
+ assertThat(gobblerLogger.iteratorForAppenders()).hasSize(1);
+ }
+
+ @Test
+ public void root_logger_writes_to_console_with_formatting_and_to_sonar_log_file_when_running_from_command_line() {
+ emulateRunFromCommandLine(false);
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+ verifyAppConsoleAppender(rootLogger.getAppender("APP_CONSOLE"));
+ verifySonarLogFileAppender(rootLogger.getAppender("file_sonar"));
+ assertThat(rootLogger.iteratorForAppenders()).hasSize(2);
+
+ // verify no other logger writes to sonar.log
+ ctx.getLoggerList()
+ .stream()
+ .filter(logger -> !ROOT_LOGGER_NAME.equals(logger.getName()))
+ .forEach(AppLoggingTest::verifyNoFileAppender);
+ }
+
+ @Test
+ public void gobbler_logger_writes_to_console_without_formatting_when_running_from_command_line() {
+ emulateRunFromCommandLine(false);
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger gobblerLogger = ctx.getLogger(LOGGER_GOBBLER);
+ verifyGobblerConsoleAppender(gobblerLogger);
+ assertThat(gobblerLogger.iteratorForAppenders()).hasSize(1);
+ }
+
+ @Test
+ public void root_logger_writes_to_console_with_formatting_and_to_sonar_log_file_when_running_from_ITs() {
+ emulateRunFromCommandLine(true);
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+ verifyAppConsoleAppender(rootLogger.getAppender("APP_CONSOLE"));
+ verifySonarLogFileAppender(rootLogger.getAppender("file_sonar"));
+ assertThat(rootLogger.iteratorForAppenders()).hasSize(2);
+
+ ctx.getLoggerList()
+ .stream()
+ .filter(logger -> !ROOT_LOGGER_NAME.equals(logger.getName()))
+ .forEach(AppLoggingTest::verifyNoFileAppender);
+ }
+
+ @Test
+ public void gobbler_logger_writes_to_console_without_formatting_when_running_from_ITs() {
+ emulateRunFromCommandLine(true);
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger gobblerLogger = ctx.getLogger(LOGGER_GOBBLER);
+ verifyGobblerConsoleAppender(gobblerLogger);
+ assertThat(gobblerLogger.iteratorForAppenders()).hasSize(1);
+ }
+
+ @Test
+ public void configure_no_rotation_on_sonar_file() {
+ settings.getProps().set("sonar.log.rollingPolicy", "none");
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+ Appender<ILoggingEvent> appender = rootLogger.getAppender("file_sonar");
+ assertThat(appender)
+ .isNotInstanceOf(RollingFileAppender.class)
+ .isInstanceOf(FileAppender.class);
+ }
+
+ @Test
+ public void default_level_for_root_logger_is_INFO() {
+ LoggerContext ctx = underTest.configure();
+
+ verifyRootLogLevel(ctx, Level.INFO);
+ }
+
+ @Test
+ public void root_logger_level_changes_with_global_property() {
+ settings.getProps().set("sonar.log.level", "TRACE");
+
+ LoggerContext ctx = underTest.configure();
+
+ verifyRootLogLevel(ctx, Level.TRACE);
+ }
+
+ @Test
+ public void root_logger_level_changes_with_app_property() {
+ settings.getProps().set("sonar.log.level.app", "TRACE");
+
+ LoggerContext ctx = underTest.configure();
+
+ verifyRootLogLevel(ctx, Level.TRACE);
+ }
+
+ @Test
+ public void root_logger_level_is_configured_from_app_property_over_global_property() {
+ settings.getProps().set("sonar.log.level", "TRACE");
+ settings.getProps().set("sonar.log.level.app", "DEBUG");
+
+ LoggerContext ctx = underTest.configure();
+
+ verifyRootLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void root_logger_level_changes_with_app_property_and_is_case_insensitive() {
+ settings.getProps().set("sonar.log.level.app", "debug");
+
+ LoggerContext ctx = underTest.configure();
+
+ verifyRootLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void default_to_INFO_if_app_property_has_invalid_value() {
+ settings.getProps().set("sonar.log.level.app", "DodoDouh!");
+
+ LoggerContext ctx = underTest.configure();
+ verifyRootLogLevel(ctx, Level.INFO);
+ }
+
+ @Test
+ public void fail_with_IAE_if_global_property_unsupported_level() {
+ settings.getProps().set("sonar.log.level", "ERROR");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("log level ERROR in property sonar.log.level is not a supported value (allowed levels are [TRACE, DEBUG, INFO])");
+
+ underTest.configure();
+ }
+
+ @Test
+ public void fail_with_IAE_if_app_property_unsupported_level() {
+ settings.getProps().set("sonar.log.level.app", "ERROR");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("log level ERROR in property sonar.log.level.app is not a supported value (allowed levels are [TRACE, DEBUG, INFO])");
+
+ underTest.configure();
+ }
+
+ @Test
+ public void no_info_log_from_hazelcast() throws IOException {
+ settings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
+ underTest.configure();
+
+ assertThat(
+ LoggerFactory.getLogger("com.hazelcast").isInfoEnabled()).isEqualTo(false);
+ }
+
+ @Test
+ public void configure_logging_for_hazelcast() throws IOException {
+ settings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.getProps().set(ProcessProperties.HAZELCAST_LOG_LEVEL, "INFO");
+ underTest.configure();
+
+ assertThat(
+ LoggerFactory.getLogger("com.hazelcast").isInfoEnabled()).isEqualTo(true);
+ assertThat(
+ LoggerFactory.getLogger("com.hazelcast").isDebugEnabled()).isEqualTo(false);
+ }
+
+ private void emulateRunFromSonarScript() {
+ settings.getProps().set("sonar.wrapped", "true");
+ }
+
+ private void emulateRunFromCommandLine(boolean withAllLogsPrintedToConsole) {
+ if (withAllLogsPrintedToConsole) {
+ settings.getProps().set("sonar.log.console", "true");
+ }
+ }
+
+ private static void verifyNoFileAppender(Logger logger) {
+ Iterator<Appender<ILoggingEvent>> iterator = logger.iteratorForAppenders();
+ while (iterator.hasNext()) {
+ assertThat(iterator.next()).isNotInstanceOf(FileAppender.class);
+ }
+ }
+
+ private void verifySonarLogFileAppender(Appender<ILoggingEvent> appender) {
+ assertThat(appender).isInstanceOf(FileAppender.class);
+ FileAppender fileAppender = (FileAppender) appender;
+ assertThat(fileAppender.getFile()).isEqualTo(new File(logDir, "sonar.log").getAbsolutePath());
+ verifyAppFormattedLogEncoder(fileAppender.getEncoder());
+ }
+
+ private void verifyAppConsoleAppender(Appender<ILoggingEvent> appender) {
+ assertThat(appender).isInstanceOf(ConsoleAppender.class);
+ ConsoleAppender<ILoggingEvent> consoleAppender = (ConsoleAppender<ILoggingEvent>) appender;
+ assertThat(consoleAppender.getTarget()).isEqualTo(ConsoleTarget.SystemOut.getName());
+ verifyAppFormattedLogEncoder(consoleAppender.getEncoder());
+ }
+
+ private void verifyAppFormattedLogEncoder(Encoder<ILoggingEvent> encoder) {
+ verifyFormattedLogEncoder(encoder, "%d{yyyy.MM.dd HH:mm:ss} %-5level app[][%logger{20}] %msg%n");
+ }
+
+ private void verifyGobblerConsoleAppender(Logger logger) {
+ Appender<ILoggingEvent> appender = logger.getAppender("GOBBLER_CONSOLE");
+ assertThat(appender).isInstanceOf(ConsoleAppender.class);
+ ConsoleAppender<ILoggingEvent> consoleAppender = (ConsoleAppender<ILoggingEvent>) appender;
+ assertThat(consoleAppender.getTarget()).isEqualTo(ConsoleTarget.SystemOut.getName());
+ verifyFormattedLogEncoder(consoleAppender.getEncoder(), "%msg%n");
+ }
+
+ private void verifyFormattedLogEncoder(Encoder<ILoggingEvent> encoder, String logPattern) {
+ assertThat(encoder).isInstanceOf(PatternLayoutEncoder.class);
+ PatternLayoutEncoder patternEncoder = (PatternLayoutEncoder) encoder;
+ assertThat(patternEncoder.getPattern()).isEqualTo(logPattern);
+ }
+
+ private void verifyRootLogLevel(LoggerContext ctx, Level expected) {
+ Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+ assertThat(rootLogger.getLevel()).isEqualTo(expected);
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/AppReloaderImplTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/AppReloaderImplTest.java
new file mode 100644
index 00000000000..972720a6c7e
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/AppReloaderImplTest.java
@@ -0,0 +1,110 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.io.IOException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.application.config.AppSettings;
+import org.sonar.application.config.AppSettingsLoader;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.MessageException;
+import org.sonar.process.ProcessProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.data.MapEntry.entry;
+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 AppReloaderImplTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private AppSettingsLoader settingsLoader = mock(AppSettingsLoader.class);
+ private FileSystem fs = mock(FileSystem.class);
+ private AppState state = mock(AppState.class);
+ private AppLogging logging = mock(AppLogging.class);
+ private AppReloaderImpl underTest = new AppReloaderImpl(settingsLoader, fs, state, logging);
+
+ @Test
+ public void reload_configuration_then_reset_all() throws IOException {
+ AppSettings settings = new TestAppSettings().set("foo", "bar");
+ AppSettings newSettings = new TestAppSettings()
+ .set("foo", "newBar")
+ .set("newProp", "newVal");
+ when(settingsLoader.load()).thenReturn(newSettings);
+
+ underTest.reload(settings);
+
+ assertThat(settings.getProps().rawProperties())
+ .contains(entry("foo", "newBar"))
+ .contains(entry("newProp", "newVal"));
+ verify(logging).configure();
+ verify(state).reset();
+ verify(fs).reset();
+ }
+
+ @Test
+ public void throw_ISE_if_cluster_is_enabled() throws IOException {
+ AppSettings settings = new TestAppSettings().set(ProcessProperties.CLUSTER_ENABLED, "true");
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Restart is not possible with cluster mode");
+
+ underTest.reload(settings);
+
+ verifyZeroInteractions(logging);
+ verifyZeroInteractions(state);
+ verifyZeroInteractions(fs);
+ }
+
+ @Test
+ public void throw_MessageException_if_path_properties_are_changed() throws IOException {
+ verifyFailureIfPropertyValueChanged(ProcessProperties.PATH_DATA);
+ verifyFailureIfPropertyValueChanged(ProcessProperties.PATH_LOGS);
+ verifyFailureIfPropertyValueChanged(ProcessProperties.PATH_TEMP);
+ verifyFailureIfPropertyValueChanged(ProcessProperties.PATH_WEB);
+ }
+
+ @Test
+ public void throw_MessageException_if_cluster_mode_changed() throws IOException {
+ verifyFailureIfPropertyValueChanged(ProcessProperties.CLUSTER_ENABLED);
+ }
+
+ private void verifyFailureIfPropertyValueChanged(String propertyKey) throws IOException {
+ AppSettings settings = new TestAppSettings().set(propertyKey, "val1");
+ AppSettings newSettings = new TestAppSettings()
+ .set(propertyKey, "val2");
+ when(settingsLoader.load()).thenReturn(newSettings);
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Property [" + propertyKey + "] cannot be changed on restart: [val1] => [val2]");
+
+ underTest.reload(settings);
+
+ verifyZeroInteractions(logging);
+ verifyZeroInteractions(state);
+ verifyZeroInteractions(fs);
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/AppStateFactoryTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/AppStateFactoryTest.java
new file mode 100644
index 00000000000..ffffcc00855
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/AppStateFactoryTest.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import org.junit.Test;
+import org.sonar.application.cluster.AppStateClusterImpl;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.ProcessProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AppStateFactoryTest {
+
+ private TestAppSettings settings = new TestAppSettings();
+ private AppStateFactory underTest = new AppStateFactory(settings);
+
+ @Test
+ public void create_cluster_implementation_if_cluster_is_enabled() {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_NAME, "foo");
+
+ AppState appState = underTest.create();
+ assertThat(appState).isInstanceOf(AppStateClusterImpl.class);
+ ((AppStateClusterImpl) appState).close();
+ }
+
+ @Test
+ public void cluster_implementation_is_disabled_by_default() {
+ assertThat(underTest.create()).isInstanceOf(AppStateImpl.class);
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/AppStateImplTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/AppStateImplTest.java
new file mode 100644
index 00000000000..e49c5f54758
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/AppStateImplTest.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import org.junit.Test;
+import org.sonar.process.ProcessId;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+public class AppStateImplTest {
+
+ private AppStateListener listener = mock(AppStateListener.class);
+ private AppStateImpl underTest = new AppStateImpl();
+
+ @Test
+ public void get_and_set_operational_flag() {
+ assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isFalse();
+ assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isFalse();
+ assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isFalse();
+
+ underTest.setOperational(ProcessId.ELASTICSEARCH);
+
+ assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isFalse();
+ assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isTrue();
+ assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isFalse();
+
+ // only local mode is supported. App state = local state
+ assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, false)).isFalse();
+ assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, false)).isTrue();
+ assertThat(underTest.isOperational(ProcessId.WEB_SERVER, false)).isFalse();
+ }
+
+ @Test
+ public void notify_listeners_when_a_process_becomes_operational() {
+ underTest.addListener(listener);
+
+ underTest.setOperational(ProcessId.ELASTICSEARCH);
+
+ verify(listener).onAppStateOperational(ProcessId.ELASTICSEARCH);
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void tryToLockWebLeader_returns_true_if_first_call() {
+ assertThat(underTest.tryToLockWebLeader()).isTrue();
+
+ // next calls return false
+ assertThat(underTest.tryToLockWebLeader()).isFalse();
+ assertThat(underTest.tryToLockWebLeader()).isFalse();
+ }
+
+ @Test
+ public void reset_initializes_all_flags() {
+ underTest.setOperational(ProcessId.ELASTICSEARCH);
+ assertThat(underTest.tryToLockWebLeader()).isTrue();
+
+ underTest.reset();
+
+ assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isFalse();
+ assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isFalse();
+ assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isFalse();
+ assertThat(underTest.tryToLockWebLeader()).isTrue();
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/SchedulerImplTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/SchedulerImplTest.java
new file mode 100644
index 00000000000..ddc2c587281
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/SchedulerImplTest.java
@@ -0,0 +1,443 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.mockito.Mockito;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.application.process.JavaCommand;
+import org.sonar.application.process.JavaCommandFactory;
+import org.sonar.application.process.JavaProcessLauncher;
+import org.sonar.application.process.ProcessMonitor;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.sonar.process.ProcessId.COMPUTE_ENGINE;
+import static org.sonar.process.ProcessId.ELASTICSEARCH;
+import static org.sonar.process.ProcessId.WEB_SERVER;
+
+public class SchedulerImplTest {
+
+ private static final JavaCommand ES_COMMAND = new JavaCommand(ELASTICSEARCH);
+ private static final JavaCommand WEB_LEADER_COMMAND = new JavaCommand(WEB_SERVER);
+ private static final JavaCommand WEB_FOLLOWER_COMMAND = new JavaCommand(WEB_SERVER);
+ private static final JavaCommand CE_COMMAND = new JavaCommand(COMPUTE_ENGINE);
+
+ @Rule
+ public TestRule safeGuard = new DisableOnDebug(Timeout.seconds(10));
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private AppReloader appReloader = mock(AppReloader.class);
+ private TestAppSettings settings = new TestAppSettings();
+ private TestJavaCommandFactory javaCommandFactory = new TestJavaCommandFactory();
+ private TestJavaProcessLauncher processLauncher = new TestJavaProcessLauncher();
+ private TestAppState appState = new TestAppState();
+ private List<ProcessId> orderedStops = new ArrayList<>();
+
+ @After
+ public void tearDown() throws Exception {
+ processLauncher.close();
+ }
+
+ @Test
+ public void start_and_stop_sequence_of_ES_WEB_CE_in_order() throws Exception {
+ enableAllProcesses();
+ SchedulerImpl underTest = newScheduler();
+ underTest.schedule();
+
+ // elasticsearch does not have preconditions to start
+ TestProcess es = processLauncher.waitForProcess(ELASTICSEARCH);
+ assertThat(es.isAlive()).isTrue();
+ assertThat(processLauncher.processes).hasSize(1);
+
+ // elasticsearch becomes operational -> web leader is starting
+ es.operational = true;
+ waitForAppStateOperational(ELASTICSEARCH);
+ TestProcess web = processLauncher.waitForProcess(WEB_SERVER);
+ assertThat(web.isAlive()).isTrue();
+ assertThat(processLauncher.processes).hasSize(2);
+ assertThat(processLauncher.commands).containsExactly(ES_COMMAND, WEB_LEADER_COMMAND);
+
+ // web becomes operational -> CE is starting
+ web.operational = true;
+ waitForAppStateOperational(WEB_SERVER);
+ TestProcess ce = processLauncher.waitForProcess(COMPUTE_ENGINE);
+ assertThat(ce.isAlive()).isTrue();
+ assertThat(processLauncher.processes).hasSize(3);
+ assertThat(processLauncher.commands).containsExactly(ES_COMMAND, WEB_LEADER_COMMAND, CE_COMMAND);
+
+ // all processes are up
+ processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isTrue());
+
+ // processes are stopped in reverse order of startup
+ underTest.terminate();
+ assertThat(orderedStops).containsExactly(COMPUTE_ENGINE, WEB_SERVER, ELASTICSEARCH);
+ processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());
+
+ // does nothing because scheduler is already terminated
+ underTest.awaitTermination();
+ }
+
+ private void enableAllProcesses() {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ }
+
+ @Test
+ public void all_processes_are_stopped_if_one_process_goes_down() throws Exception {
+ Scheduler underTest = startAll();
+
+ processLauncher.waitForProcess(WEB_SERVER).destroyForcibly();
+
+ underTest.awaitTermination();
+ assertThat(orderedStops).containsExactly(WEB_SERVER, COMPUTE_ENGINE, ELASTICSEARCH);
+ processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());
+
+ // following does nothing
+ underTest.terminate();
+ underTest.awaitTermination();
+ }
+
+ @Test
+ public void all_processes_are_stopped_if_one_process_fails_to_start() throws Exception {
+ enableAllProcesses();
+ SchedulerImpl underTest = newScheduler();
+ processLauncher.makeStartupFail = COMPUTE_ENGINE;
+
+ underTest.schedule();
+
+ processLauncher.waitForProcess(ELASTICSEARCH).operational = true;
+ processLauncher.waitForProcess(WEB_SERVER).operational = true;
+
+ underTest.awaitTermination();
+ assertThat(orderedStops).containsExactly(WEB_SERVER, ELASTICSEARCH);
+ processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());
+ }
+
+ @Test
+ public void terminate_can_be_called_multiple_times() throws Exception {
+ Scheduler underTest = startAll();
+
+ underTest.terminate();
+ processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());
+
+ // does nothing
+ underTest.terminate();
+ }
+
+ @Test
+ public void awaitTermination_blocks_until_all_processes_are_stopped() throws Exception {
+ Scheduler underTest = startAll();
+
+ Thread awaitingTermination = new Thread(() -> underTest.awaitTermination());
+ awaitingTermination.start();
+ assertThat(awaitingTermination.isAlive()).isTrue();
+
+ underTest.terminate();
+ // the thread is being stopped
+ awaitingTermination.join();
+ assertThat(awaitingTermination.isAlive()).isFalse();
+ }
+
+ @Test
+ public void restart_reloads_java_commands_and_restarts_all_processes() throws Exception {
+ Scheduler underTest = startAll();
+
+ processLauncher.waitForProcess(WEB_SERVER).askedForRestart = true;
+
+ // waiting for all processes to be stopped
+ boolean stopped = false;
+ while (!stopped) {
+ stopped = orderedStops.size() == 3;
+ Thread.sleep(1L);
+ }
+
+ // restarting
+ verify(appReloader, timeout(10_000)).reload(settings);
+ processLauncher.waitForProcessAlive(ELASTICSEARCH);
+ processLauncher.waitForProcessAlive(COMPUTE_ENGINE);
+ processLauncher.waitForProcessAlive(WEB_SERVER);
+
+ underTest.terminate();
+ // 3+3 processes have been stopped
+ assertThat(orderedStops).hasSize(6);
+ assertThat(processLauncher.waitForProcess(ELASTICSEARCH).isAlive()).isFalse();
+ assertThat(processLauncher.waitForProcess(COMPUTE_ENGINE).isAlive()).isFalse();
+ assertThat(processLauncher.waitForProcess(WEB_SERVER).isAlive()).isFalse();
+
+ // verify that awaitTermination() does not block
+ underTest.awaitTermination();
+ }
+
+ @Test
+ public void restart_stops_all_if_new_settings_are_not_allowed() throws Exception {
+ Scheduler underTest = startAll();
+ doThrow(new IllegalStateException("reload error")).when(appReloader).reload(settings);
+
+ processLauncher.waitForProcess(WEB_SERVER).askedForRestart = true;
+
+ // waiting for all processes to be stopped
+ processLauncher.waitForProcessDown(ELASTICSEARCH);
+ processLauncher.waitForProcessDown(COMPUTE_ENGINE);
+ processLauncher.waitForProcessDown(WEB_SERVER);
+
+ // verify that awaitTermination() does not block
+ underTest.awaitTermination();
+ }
+
+ @Test
+ public void web_follower_starts_only_when_web_leader_is_operational() throws Exception {
+ // leader takes the lock, so underTest won't get it
+ assertThat(appState.tryToLockWebLeader()).isTrue();
+
+ appState.setOperational(ProcessId.ELASTICSEARCH);
+ enableAllProcesses();
+ SchedulerImpl underTest = newScheduler();
+ underTest.schedule();
+
+ processLauncher.waitForProcessAlive(ProcessId.ELASTICSEARCH);
+ assertThat(processLauncher.processes).hasSize(1);
+
+ // leader becomes operational -> follower can start
+ appState.setOperational(ProcessId.WEB_SERVER);
+ processLauncher.waitForProcessAlive(WEB_SERVER);
+
+ underTest.terminate();
+ }
+
+ @Test
+ public void web_server_waits_for_remote_elasticsearch_to_be_started_if_local_es_is_disabled() throws Exception {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
+ SchedulerImpl underTest = newScheduler();
+ underTest.schedule();
+
+ // WEB and CE wait for ES to be up
+ assertThat(processLauncher.processes).isEmpty();
+
+ // ES becomes operational on another node -> web leader can start
+ appState.setRemoteOperational(ProcessId.ELASTICSEARCH);
+ processLauncher.waitForProcessAlive(WEB_SERVER);
+ assertThat(processLauncher.processes).hasSize(1);
+
+ underTest.terminate();
+ }
+
+ @Test
+ public void compute_engine_waits_for_remote_elasticsearch_and_web_leader_to_be_started_if_local_es_is_disabled() throws Exception {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_WEB_DISABLED, "true");
+ SchedulerImpl underTest = newScheduler();
+ underTest.schedule();
+
+ // CE waits for ES and WEB leader to be up
+ assertThat(processLauncher.processes).isEmpty();
+
+ // ES and WEB leader become operational on another nodes -> CE can start
+ appState.setRemoteOperational(ProcessId.ELASTICSEARCH);
+ appState.setRemoteOperational(ProcessId.WEB_SERVER);
+
+ processLauncher.waitForProcessAlive(COMPUTE_ENGINE);
+ assertThat(processLauncher.processes).hasSize(1);
+
+ underTest.terminate();
+ }
+
+ private SchedulerImpl newScheduler() {
+ return new SchedulerImpl(settings, appReloader, javaCommandFactory, processLauncher, appState)
+ .setProcessWatcherDelayMs(1L);
+ }
+
+ private Scheduler startAll() throws InterruptedException {
+ enableAllProcesses();
+ SchedulerImpl scheduler = newScheduler();
+ scheduler.schedule();
+ processLauncher.waitForProcess(ELASTICSEARCH).operational = true;
+ processLauncher.waitForProcess(WEB_SERVER).operational = true;
+ processLauncher.waitForProcess(COMPUTE_ENGINE).operational = true;
+ return scheduler;
+ }
+
+ private void waitForAppStateOperational(ProcessId id) throws InterruptedException {
+ while (true) {
+ if (appState.isOperational(id, true)) {
+ return;
+ }
+ Thread.sleep(1L);
+ }
+ }
+
+ private static class TestJavaCommandFactory implements JavaCommandFactory {
+ @Override
+ public JavaCommand createEsCommand() {
+ return ES_COMMAND;
+ }
+
+ @Override
+ public JavaCommand createWebCommand(boolean leader) {
+ return leader ? WEB_LEADER_COMMAND : WEB_FOLLOWER_COMMAND;
+ }
+
+ @Override
+ public JavaCommand createCeCommand() {
+ return CE_COMMAND;
+ }
+ }
+
+ private class TestJavaProcessLauncher implements JavaProcessLauncher {
+ private final EnumMap<ProcessId, TestProcess> processes = new EnumMap<>(ProcessId.class);
+ private final List<JavaCommand> commands = new ArrayList<>();
+ private ProcessId makeStartupFail = null;
+
+ @Override
+ public ProcessMonitor launch(JavaCommand javaCommand) {
+ commands.add(javaCommand);
+ if (makeStartupFail == javaCommand.getProcessId()) {
+ throw new IllegalStateException("cannot start " + javaCommand.getProcessId());
+ }
+ TestProcess process = new TestProcess(javaCommand.getProcessId());
+ processes.put(javaCommand.getProcessId(), process);
+ return process;
+ }
+
+ private TestProcess waitForProcess(ProcessId id) throws InterruptedException {
+ while (true) {
+ TestProcess p = processes.get(id);
+ if (p != null) {
+ return p;
+ }
+ Thread.sleep(1L);
+ }
+ }
+
+ private TestProcess waitForProcessAlive(ProcessId id) throws InterruptedException {
+ while (true) {
+ TestProcess p = processes.get(id);
+ if (p != null && p.isAlive()) {
+ return p;
+ }
+ Thread.sleep(1L);
+ }
+ }
+
+ private TestProcess waitForProcessDown(ProcessId id) throws InterruptedException {
+ while (true) {
+ TestProcess p = processes.get(id);
+ if (p != null && !p.isAlive()) {
+ return p;
+ }
+ Thread.sleep(1L);
+ }
+ }
+
+ @Override
+ public void close() {
+ for (TestProcess process : processes.values()) {
+ process.destroyForcibly();
+ }
+ }
+ }
+
+ private class TestProcess implements ProcessMonitor, AutoCloseable {
+ private final ProcessId processId;
+ private final CountDownLatch alive = new CountDownLatch(1);
+ private boolean operational = false;
+ private boolean askedForRestart = false;
+
+ private TestProcess(ProcessId processId) {
+ this.processId = processId;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return mock(InputStream.class, Mockito.RETURNS_MOCKS);
+ }
+
+ @Override
+ public void closeStreams() {
+ }
+
+ @Override
+ public boolean isAlive() {
+ return alive.getCount() == 1;
+ }
+
+ @Override
+ public void askForStop() {
+ destroyForcibly();
+ }
+
+ @Override
+ public void destroyForcibly() {
+ if (isAlive()) {
+ orderedStops.add(processId);
+ }
+ alive.countDown();
+ }
+
+ @Override
+ public void waitFor() throws InterruptedException {
+ alive.await();
+ }
+
+ @Override
+ public void waitFor(long timeout, TimeUnit timeoutUnit) throws InterruptedException {
+ alive.await(timeout, timeoutUnit);
+ }
+
+ @Override
+ public boolean isOperational() {
+ return operational;
+ }
+
+ @Override
+ public boolean askedForRestart() {
+ return askedForRestart;
+ }
+
+ @Override
+ public void acknowledgeAskForRestart() {
+ this.askedForRestart = false;
+ }
+
+ @Override
+ public void close() {
+ alive.countDown();
+ }
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/TestAppState.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/TestAppState.java
new file mode 100644
index 00000000000..e69624382eb
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/TestAppState.java
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.Nonnull;
+import org.sonar.process.ProcessId;
+
+public class TestAppState implements AppState {
+
+ private final Map<ProcessId, Boolean> localProcesses = new EnumMap<>(ProcessId.class);
+ private final Map<ProcessId, Boolean> remoteProcesses = new EnumMap<>(ProcessId.class);
+ private final List<AppStateListener> listeners = new ArrayList<>();
+ private final AtomicBoolean webLeaderLocked = new AtomicBoolean(false);
+
+ @Override
+ public void addListener(@Nonnull AppStateListener listener) {
+ this.listeners.add(listener);
+ }
+
+ @Override
+ public boolean isOperational(ProcessId processId, boolean local) {
+ if (local) {
+ return localProcesses.computeIfAbsent(processId, p -> false);
+ }
+ return remoteProcesses.computeIfAbsent(processId, p -> false);
+ }
+
+ @Override
+ public void setOperational(ProcessId processId) {
+ localProcesses.put(processId, true);
+ remoteProcesses.put(processId, true);
+ listeners.forEach(l -> l.onAppStateOperational(processId));
+ }
+
+ public void setRemoteOperational(ProcessId processId) {
+ remoteProcesses.put(processId, true);
+ listeners.forEach(l -> l.onAppStateOperational(processId));
+ }
+
+ @Override
+ public boolean tryToLockWebLeader() {
+ return webLeaderLocked.compareAndSet(false, true);
+ }
+
+ @Override
+ public void reset() {
+ webLeaderLocked.set(false);
+ localProcesses.clear();
+ }
+
+ @Override
+ public void close() {
+ // nothing to do
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/AppStateClusterImplTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/AppStateClusterImplTest.java
new file mode 100644
index 00000000000..a1be5f33895
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/AppStateClusterImplTest.java
@@ -0,0 +1,125 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.cluster;
+
+import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.core.ReplicatedMap;
+import java.net.InetAddress;
+import java.util.UUID;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.sonar.application.AppStateListener;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+
+import static org.assertj.core.api.Java6Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.sonar.application.cluster.AppStateClusterImpl.OPERATIONAL_PROCESSES;
+
+public class AppStateClusterImplTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Rule
+ public TestRule safeGuard = new DisableOnDebug(Timeout.seconds(10));
+
+ @Test
+ public void instantiation_throws_ISE_if_cluster_mode_is_disabled() throws Exception {
+ TestAppSettings settings = new TestAppSettings();
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "false");
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Cluster is not enabled on this instance");
+
+ new AppStateClusterImpl(settings);
+ }
+
+ @Test
+ public void tryToLockWebLeader_returns_true_only_for_the_first_call() throws Exception {
+ TestAppSettings settings = newClusterSettings();
+
+ try (AppStateClusterImpl underTest = new AppStateClusterImpl(settings)) {
+ assertThat(underTest.tryToLockWebLeader()).isEqualTo(true);
+ assertThat(underTest.tryToLockWebLeader()).isEqualTo(false);
+ }
+ }
+
+ @Test
+ public void test_listeners() throws InterruptedException {
+ AppStateListener listener = mock(AppStateListener.class);
+ try (AppStateClusterImpl underTest = new AppStateClusterImpl(newClusterSettings())) {
+ underTest.addListener(listener);
+
+ underTest.setOperational(ProcessId.ELASTICSEARCH);
+ verify(listener, timeout(20_000)).onAppStateOperational(ProcessId.ELASTICSEARCH);
+
+ assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isEqualTo(true);
+ assertThat(underTest.isOperational(ProcessId.APP, true)).isEqualTo(false);
+ assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isEqualTo(false);
+ assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isEqualTo(false);
+ }
+ }
+
+ @Test
+ public void simulate_network_cluster() throws InterruptedException {
+ TestAppSettings settings = newClusterSettings();
+ settings.set(ProcessProperties.CLUSTER_INTERFACES, InetAddress.getLoopbackAddress().getHostAddress());
+ AppStateListener listener = mock(AppStateListener.class);
+
+ try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
+ appStateCluster.addListener(listener);
+
+ HazelcastInstance hzInstance = HazelcastHelper.createHazelcastClient(appStateCluster);
+ String uuid = UUID.randomUUID().toString();
+ ReplicatedMap<ClusterProcess, Boolean> replicatedMap = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
+ // process is not up yet --> no events are sent to listeners
+ replicatedMap.put(
+ new ClusterProcess(uuid, ProcessId.ELASTICSEARCH),
+ Boolean.FALSE);
+
+ // process is up yet --> notify listeners
+ replicatedMap.replace(
+ new ClusterProcess(uuid, ProcessId.ELASTICSEARCH),
+ Boolean.TRUE);
+
+ // should be called only once
+ verify(listener, timeout(20_000)).onAppStateOperational(ProcessId.ELASTICSEARCH);
+ verifyNoMoreInteractions(listener);
+
+ hzInstance.shutdown();
+ }
+ }
+
+ private static TestAppSettings newClusterSettings() {
+ TestAppSettings settings = new TestAppSettings();
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_NAME, "sonarqube");
+ return settings;
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/ClusterPropertiesTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/ClusterPropertiesTest.java
new file mode 100644
index 00000000000..c974219839d
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/ClusterPropertiesTest.java
@@ -0,0 +1,136 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.cluster;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.application.config.AppSettings;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.ProcessProperties;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ClusterPropertiesTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private AppSettings appSettings = new TestAppSettings();
+
+ @Test
+ public void test_default_values() throws Exception {
+
+ ClusterProperties props = new ClusterProperties(appSettings);
+
+ assertThat(props.getInterfaces())
+ .isEqualTo(Collections.emptyList());
+ assertThat(props.getPort())
+ .isEqualTo(9003);
+ assertThat(props.isEnabled())
+ .isEqualTo(false);
+ assertThat(props.getMembers())
+ .isEqualTo(Collections.emptyList());
+ assertThat(props.getName())
+ .isEqualTo("");
+ }
+
+ @Test
+ public void test_port_parameter() {
+ appSettings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
+ appSettings.getProps().set(ProcessProperties.CLUSTER_NAME, "sonarqube");
+
+ Stream.of("-50", "0", "65536", "128563").forEach(
+ port -> {
+ appSettings.getProps().set(ProcessProperties.CLUSTER_PORT, port);
+
+ ClusterProperties clusterProperties = new ClusterProperties(appSettings);
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage(
+ String.format("Cluster port have been set to %s which is outside the range [1-65535].", port));
+ clusterProperties.validate();
+
+ });
+ }
+
+ @Test
+ public void test_interfaces_parameter() {
+ appSettings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
+ appSettings.getProps().set(ProcessProperties.CLUSTER_NAME, "sonarqube");
+ appSettings.getProps().set(ProcessProperties.CLUSTER_INTERFACES, "8.8.8.8"); // This IP belongs to Google
+
+ ClusterProperties clusterProperties = new ClusterProperties(appSettings);
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage(
+ String.format("Interface %s is not available on this machine.", "8.8.8.8"));
+ clusterProperties.validate();
+ }
+
+ @Test
+ public void test_missing_name() {
+ appSettings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
+ appSettings.getProps().set(ProcessProperties.CLUSTER_NAME, "");
+
+ ClusterProperties clusterProperties = new ClusterProperties(appSettings);
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage(
+ String.format("Cluster have been enabled but a %s has not been defined.",
+ ProcessProperties.CLUSTER_NAME));
+ clusterProperties.validate();
+ }
+
+ @Test
+ public void validate_does_not_fail_if_cluster_enabled_and_name_specified() {
+ appSettings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
+ appSettings.getProps().set(ProcessProperties.CLUSTER_NAME, "sonarqube");
+
+ ClusterProperties clusterProperties = new ClusterProperties(appSettings);
+ clusterProperties.validate();
+ }
+
+ @Test
+ public void test_members() {
+ appSettings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
+ appSettings.getProps().set(ProcessProperties.CLUSTER_NAME, "sonarqube");
+
+ assertThat(
+ new ClusterProperties(appSettings).getMembers()).isEqualTo(
+ Collections.emptyList());
+
+ appSettings.getProps().set(ProcessProperties.CLUSTER_MEMBERS, "192.168.1.1");
+ assertThat(
+ new ClusterProperties(appSettings).getMembers()).isEqualTo(
+ Arrays.asList("192.168.1.1:9003"));
+
+ appSettings.getProps().set(ProcessProperties.CLUSTER_MEMBERS, "192.168.1.2:5501");
+ assertThat(
+ new ClusterProperties(appSettings).getMembers()).containsExactlyInAnyOrder(
+ "192.168.1.2:5501");
+
+ appSettings.getProps().set(ProcessProperties.CLUSTER_MEMBERS, "192.168.1.2:5501,192.168.1.1");
+ assertThat(
+ new ClusterProperties(appSettings).getMembers()).containsExactlyInAnyOrder(
+ "192.168.1.2:5501", "192.168.1.1:9003");
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/HazelcastHelper.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/HazelcastHelper.java
new file mode 100644
index 00000000000..9cfe2559e67
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/HazelcastHelper.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.cluster;
+
+import com.hazelcast.client.HazelcastClient;
+import com.hazelcast.client.config.ClientConfig;
+import com.hazelcast.config.Config;
+import com.hazelcast.config.JoinConfig;
+import com.hazelcast.config.NetworkConfig;
+import com.hazelcast.core.Hazelcast;
+import com.hazelcast.core.HazelcastInstance;
+import java.net.InetSocketAddress;
+import java.util.Collection;
+
+public class HazelcastHelper {
+ static HazelcastInstance createHazelcastNode(AppStateClusterImpl appStateCluster) {
+ Config hzConfig = new Config()
+ .setInstanceName(appStateCluster.hzInstance.getName() + "_1");
+
+ // Configure the network instance
+ NetworkConfig netConfig = hzConfig.getNetworkConfig();
+ netConfig.setPort(9003).setPortAutoIncrement(true);
+ Collection<String> interfaces = appStateCluster.hzInstance.getConfig().getNetworkConfig().getInterfaces().getInterfaces();
+ if (!interfaces.isEmpty()) {
+ netConfig.getInterfaces().addInterface(
+ interfaces.iterator().next()
+ );
+ }
+
+ // Only allowing TCP/IP configuration
+ JoinConfig joinConfig = netConfig.getJoin();
+ joinConfig.getAwsConfig().setEnabled(false);
+ joinConfig.getMulticastConfig().setEnabled(false);
+ joinConfig.getTcpIpConfig().setEnabled(true);
+
+ InetSocketAddress socketAddress = (InetSocketAddress) appStateCluster.hzInstance.getLocalEndpoint().getSocketAddress();
+ joinConfig.getTcpIpConfig().addMember(
+ String.format("%s:%d",
+ socketAddress.getHostString(),
+ socketAddress.getPort()
+ )
+ );
+
+ // Tweak HazelCast configuration
+ hzConfig
+ // Increase the number of tries
+ .setProperty("hazelcast.tcp.join.port.try.count", "10")
+ // Don't bind on all interfaces
+ .setProperty("hazelcast.socket.bind.any", "false")
+ // Don't phone home
+ .setProperty("hazelcast.phone.home.enabled", "false")
+ // Use slf4j for logging
+ .setProperty("hazelcast.logging.type", "slf4j");
+
+ // We are not using the partition group of Hazelcast, so disabling it
+ hzConfig.getPartitionGroupConfig().setEnabled(false);
+
+ return Hazelcast.newHazelcastInstance(hzConfig);
+ }
+
+ static HazelcastInstance createHazelcastClient(AppStateClusterImpl appStateCluster) {
+ ClientConfig clientConfig = new ClientConfig();
+ InetSocketAddress socketAddress = (InetSocketAddress) appStateCluster.hzInstance.getLocalEndpoint().getSocketAddress();
+
+ clientConfig.getNetworkConfig().getAddresses().add(
+ String.format("%s:%d",
+ socketAddress.getHostString(),
+ socketAddress.getPort()
+ ));
+ clientConfig.getGroupConfig().setName(appStateCluster.hzInstance.getConfig().getGroupConfig().getName());
+ return HazelcastClient.newHazelcastClient(clientConfig);
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/AppSettingsImplTest.java
index e1161fbaf26..2ea6215052e 100644
--- a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/AppSettingsImplTest.java
@@ -17,23 +17,29 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-package org.sonar.process.monitor;
+package org.sonar.application.config;
+import java.util.Properties;
import org.junit.Test;
+import org.sonar.process.Props;
import static org.assertj.core.api.Assertions.assertThat;
-public class TimeoutsTest {
+public class AppSettingsImplTest {
@Test
- public void test_default_values() throws Exception {
- Timeouts timeouts = new Timeouts();
- assertThat(timeouts.getTerminationTimeout()).isGreaterThan(1000L);
- }
+ public void reload_updates_properties() {
+ Props initialProps = new Props(new Properties());
+ initialProps.set("foo", "bar");
+ Props newProps = new Props(new Properties());
+ newProps.set("foo", "baz");
+ newProps.set("newProp", "newVal");
- @Test
- public void test_values() throws Exception {
- Timeouts timeouts = new Timeouts(3L);
- assertThat(timeouts.getTerminationTimeout()).isEqualTo(3L);
+ AppSettingsImpl underTest = new AppSettingsImpl(initialProps);
+ underTest.reload(newProps);
+
+ assertThat(underTest.getValue("foo").get()).isEqualTo("baz");
+ assertThat(underTest.getValue("newProp").get()).isEqualTo("newVal");
+ assertThat(underTest.getProps().rawProperties()).hasSize(2);
}
}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/config/AppSettingsLoaderImplTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/AppSettingsLoaderImplTest.java
new file mode 100644
index 00000000000..d1337a8c6bc
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/AppSettingsLoaderImplTest.java
@@ -0,0 +1,104 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.io.File;
+import org.apache.commons.io.FileUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.data.MapEntry.entry;
+
+public class AppSettingsLoaderImplTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void load_properties_from_file() throws Exception {
+ File homeDir = temp.newFolder();
+ File propsFile = new File(homeDir, "conf/sonar.properties");
+ FileUtils.write(propsFile, "foo=bar");
+
+ AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[0], homeDir);
+ AppSettings settings = underTest.load();
+
+ assertThat(settings.getProps().rawProperties()).contains(entry("foo", "bar"));
+ }
+
+ @Test
+ public void throws_ISE_if_file_fails_to_be_loaded() throws Exception {
+ File homeDir = temp.newFolder();
+ File propsFileAsDir = new File(homeDir, "conf/sonar.properties");
+ FileUtils.forceMkdir(propsFileAsDir);
+ AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[0], homeDir);
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Cannot open file " + propsFileAsDir.getAbsolutePath());
+
+ underTest.load();
+ }
+
+ @Test
+ public void file_is_not_loaded_if_it_does_not_exist() throws Exception {
+ File homeDir = temp.newFolder();
+
+ AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[0], homeDir);
+ AppSettings settings = underTest.load();
+
+ // no failure, file is ignored
+ assertThat(settings.getProps()).isNotNull();
+ }
+
+ @Test
+ public void command_line_arguments_are_included_to_settings() throws Exception {
+ File homeDir = temp.newFolder();
+
+ AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[] {"-Dsonar.foo=bar", "-Dhello=world"}, homeDir);
+ AppSettings settings = underTest.load();
+
+ assertThat(settings.getProps().rawProperties())
+ .contains(entry("sonar.foo", "bar"))
+ .contains(entry("hello", "world"));
+ }
+
+ @Test
+ public void command_line_arguments_make_precedence_over_properties_files() throws Exception {
+ File homeDir = temp.newFolder();
+ File propsFile = new File(homeDir, "conf/sonar.properties");
+ FileUtils.write(propsFile, "sonar.foo=file");
+
+ AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[]{"-Dsonar.foo=cli"}, homeDir);
+ AppSettings settings = underTest.load();
+
+ assertThat(settings.getProps().rawProperties()).contains(entry("sonar.foo", "cli"));
+ }
+
+ @Test
+ public void detectHomeDir_returns_existing_dir() throws Exception {
+ assertThat(new AppSettingsLoaderImpl(new String[0]).getHomeDir()).exists().isDirectory();
+
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/config/ClusterSettingsTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/ClusterSettingsTest.java
new file mode 100644
index 00000000000..079cbf49b6a
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/ClusterSettingsTest.java
@@ -0,0 +1,134 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.process.MessageException;
+import org.sonar.process.ProcessProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.process.ProcessId.COMPUTE_ENGINE;
+import static org.sonar.process.ProcessId.ELASTICSEARCH;
+import static org.sonar.process.ProcessId.WEB_SERVER;
+
+public class ClusterSettingsTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private TestAppSettings settings = new TestAppSettings();
+
+ @Test
+ public void test_isClusterEnabled() {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ assertThat(ClusterSettings.isClusterEnabled(settings)).isTrue();
+
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "false");
+ assertThat(ClusterSettings.isClusterEnabled(settings)).isFalse();
+ }
+
+ @Test
+ public void isClusterEnabled_returns_false_by_default() {
+ assertThat(ClusterSettings.isClusterEnabled(settings)).isFalse();
+ }
+
+ @Test
+ public void getEnabledProcesses_returns_all_processes_by_default() {
+ assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, ELASTICSEARCH, WEB_SERVER);
+ }
+
+ @Test
+ public void getEnabledProcesses_returns_all_processes_by_default_in_cluster_mode() {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+
+ assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, ELASTICSEARCH, WEB_SERVER);
+ }
+
+ @Test
+ public void getEnabledProcesses_returns_configured_processes_in_cluster_mode() {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
+
+ assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, WEB_SERVER);
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_internal_property_for_web_leader_is_configured() {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set("sonar.cluster.web.startupLeader", "true");
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Property [sonar.cluster.web.startupLeader] is forbidden");
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ @Test
+ public void accept_does_nothing_if_cluster_is_disabled() {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "false");
+ // this property is supposed to fail if cluster is enabled
+ settings.set("sonar.cluster.web.startupLeader", "true");
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_h2() {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set("sonar.jdbc.url", "jdbc:h2:mem");
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Embedded database is not supported in cluster mode");
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_default_jdbc_url() {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Embedded database is not supported in cluster mode");
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ @Test
+ public void isLocalElasticsearchEnabled_returns_true_by_default() {
+ assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isTrue();
+ }
+
+ @Test
+ public void isLocalElasticsearchEnabled_returns_true_by_default_in_cluster_mode() {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+
+ assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isTrue();
+ }
+
+ @Test
+ public void isLocalElasticsearchEnabled_returns_false_if_local_es_node_is_disabled_in_cluster_mode() {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
+
+ assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isFalse();
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/config/CommandLineParserTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/CommandLineParserTest.java
new file mode 100644
index 00000000000..d94851e0cfc
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/CommandLineParserTest.java
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.util.Properties;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CommandLineParserTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void parseArguments() {
+ System.setProperty("CommandLineParserTest.unused", "unused");
+ System.setProperty("sonar.CommandLineParserTest.used", "used");
+
+ Properties p = CommandLineParser.parseArguments(new String[] {"-Dsonar.foo=bar"});
+
+ // test environment can already declare some system properties prefixed by "sonar."
+ // so we can't test the exact number "2"
+ assertThat(p.size()).isGreaterThanOrEqualTo(2);
+ assertThat(p.getProperty("sonar.foo")).isEqualTo("bar");
+ assertThat(p.getProperty("sonar.CommandLineParserTest.used")).isEqualTo("used");
+
+ }
+
+ @Test
+ public void argumentsToProperties_throws_IAE_if_argument_does_not_start_with_minusD() {
+ Properties p = CommandLineParser.argumentsToProperties(new String[] {"-Dsonar.foo=bar", "-Dsonar.whitespace=foo bar"});
+ assertThat(p).hasSize(2);
+ assertThat(p.getProperty("sonar.foo")).isEqualTo("bar");
+ assertThat(p.getProperty("sonar.whitespace")).isEqualTo("foo bar");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Command-line argument must start with -D, for example -Dsonar.jdbc.username=sonar. Got: sonar.bad=true");
+
+ CommandLineParser.argumentsToProperties(new String[] {"-Dsonar.foo=bar", "sonar.bad=true"});
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/config/FileSystemSettingsTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/FileSystemSettingsTest.java
new file mode 100644
index 00000000000..dd902f0d5b4
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/FileSystemSettingsTest.java
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.io.File;
+import java.util.Properties;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.process.Props;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.process.ProcessProperties.PATH_DATA;
+import static org.sonar.process.ProcessProperties.PATH_HOME;
+import static org.sonar.process.ProcessProperties.PATH_LOGS;
+import static org.sonar.process.ProcessProperties.PATH_TEMP;
+import static org.sonar.process.ProcessProperties.PATH_WEB;
+
+
+public class FileSystemSettingsTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private FileSystemSettings underTest = new FileSystemSettings();
+ private File homeDir;
+
+ @Before
+ public void setUp() throws Exception {
+ homeDir = temp.newFolder();
+ }
+
+ @Test
+ public void relative_paths_are_converted_to_absolute_paths() throws Exception {
+ Props props = new Props(new Properties());
+ props.set(PATH_HOME, homeDir.getAbsolutePath());
+
+ // relative paths
+ props.set(PATH_DATA, "data");
+ props.set(PATH_LOGS, "logs");
+ props.set(PATH_TEMP, "temp");
+
+ // already absolute paths
+ props.set(PATH_WEB, new File(homeDir, "web").getAbsolutePath());
+
+ underTest.accept(props);
+
+ assertThat(props.nonNullValue(PATH_DATA)).isEqualTo(new File(homeDir, "data").getAbsolutePath());
+ assertThat(props.nonNullValue(PATH_LOGS)).isEqualTo(new File(homeDir, "logs").getAbsolutePath());
+ assertThat(props.nonNullValue(PATH_TEMP)).isEqualTo(new File(homeDir, "temp").getAbsolutePath());
+ assertThat(props.nonNullValue(PATH_WEB)).isEqualTo(new File(homeDir, "web").getAbsolutePath());
+ }
+
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/config/JdbcSettingsTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/JdbcSettingsTest.java
new file mode 100644
index 00000000000..45510230779
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/JdbcSettingsTest.java
@@ -0,0 +1,220 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.io.File;
+import java.util.Properties;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.process.MessageException;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.application.config.JdbcSettings.Provider;
+import static org.sonar.process.ProcessProperties.JDBC_URL;
+
+public class JdbcSettingsTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private JdbcSettings underTest = new JdbcSettings();
+ private File homeDir;
+
+ @Before
+ public void setUp() throws Exception {
+ homeDir = temp.newFolder();
+ }
+
+ @Test
+ public void resolve_H2_provider_when_props_is_empty_and_set_URL_to_default_H2() {
+ Props props = newProps();
+
+ assertThat(underTest.resolveProviderAndEnforceNonnullJdbcUrl(props))
+ .isEqualTo(Provider.H2);
+ assertThat(props.nonNullValue(JDBC_URL)).isEqualTo("jdbc:h2:tcp://localhost:9092/sonar");
+ }
+
+ @Test
+ public void resolve_Oracle_when_jdbc_url_contains_oracle_in_any_case() {
+ checkProviderForUrlAndUnchangedUrl("jdbc:oracle:foo", Provider.ORACLE);
+ checkProviderForUrlAndUnchangedUrl("jdbc:OrAcLe:foo", Provider.ORACLE);
+ }
+
+ @Test
+ public void resolve_MySql_when_jdbc_url_contains_mysql_in_any_case() {
+ checkProviderForUrlAndUnchangedUrl("jdbc:mysql:foo", Provider.MYSQL);
+
+ checkProviderForUrlAndUnchangedUrl("jdbc:mYsQL:foo", Provider.MYSQL);
+ }
+
+ @Test
+ public void resolve_SqlServer_when_jdbc_url_contains_sqlserver_in_any_case() {
+ checkProviderForUrlAndUnchangedUrl("jdbc:sqlserver:foo", Provider.SQLSERVER);
+
+ checkProviderForUrlAndUnchangedUrl("jdbc:SQLSeRVeR:foo", Provider.SQLSERVER);
+ }
+
+ @Test
+ public void resolve_POSTGRESQL_when_jdbc_url_contains_POSTGRESQL_in_any_case() {
+ checkProviderForUrlAndUnchangedUrl("jdbc:postgresql:foo", Provider.POSTGRESQL);
+
+ checkProviderForUrlAndUnchangedUrl("jdbc:POSTGRESQL:foo", Provider.POSTGRESQL);
+ }
+
+ private void checkProviderForUrlAndUnchangedUrl(String url, Provider expected) {
+ Props props = newProps(JDBC_URL, url);
+
+ assertThat(underTest.resolveProviderAndEnforceNonnullJdbcUrl(props)).isEqualTo(expected);
+ assertThat(props.nonNullValue(JDBC_URL)).isEqualTo(url);
+ }
+
+ @Test
+ public void fail_with_MessageException_when_provider_is_not_supported() {
+ Props props = newProps(JDBC_URL, "jdbc:microsoft:sqlserver://localhost");
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Unsupported JDBC driver provider: microsoft");
+
+ underTest.resolveProviderAndEnforceNonnullJdbcUrl(props);
+ }
+
+ @Test
+ public void fail_with_MessageException_when_url_does_not_have_jdbc_prefix() {
+ Props props = newProps(JDBC_URL, "oracle:thin:@localhost/XE");
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Bad format of JDBC URL: oracle:thin:@localhost/XE");
+
+ underTest.resolveProviderAndEnforceNonnullJdbcUrl(props);
+ }
+
+ @Test
+ public void check_mysql_parameters() {
+ // minimal -> ok
+ underTest.checkUrlParameters(Provider.MYSQL,
+ "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8");
+
+ // full -> ok
+ underTest.checkUrlParameters(Provider.MYSQL,
+ "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance");
+
+ // missing required -> ko
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("JDBC URL must have the property 'useUnicode=true'");
+
+ underTest.checkUrlParameters(Provider.MYSQL, "jdbc:mysql://localhost:3306/sonar?characterEncoding=utf8");
+ }
+
+ @Test
+ public void checkAndComplete_sets_driver_path_for_oracle() throws Exception {
+ File driverFile = new File(homeDir, "extensions/jdbc-driver/oracle/ojdbc6.jar");
+ FileUtils.touch(driverFile);
+
+ Props props = newProps(JDBC_URL, "jdbc:oracle:thin:@localhost/XE");
+ underTest.accept(props);
+ assertThat(props.nonNullValueAsFile(ProcessProperties.JDBC_DRIVER_PATH)).isEqualTo(driverFile);
+ }
+
+ @Test
+ public void sets_driver_path_for_h2() throws Exception {
+ File driverFile = new File(homeDir, "lib/jdbc/h2/h2.jar");
+ FileUtils.touch(driverFile);
+
+ Props props = newProps(JDBC_URL, "jdbc:h2:tcp://localhost:9092/sonar");
+ underTest.accept(props);
+ assertThat(props.nonNullValueAsFile(ProcessProperties.JDBC_DRIVER_PATH)).isEqualTo(driverFile);
+ }
+
+ @Test
+ public void checkAndComplete_sets_driver_path_for_postgresql() throws Exception {
+ File driverFile = new File(homeDir, "lib/jdbc/postgresql/pg.jar");
+ FileUtils.touch(driverFile);
+
+ Props props = newProps(JDBC_URL, "jdbc:postgresql://localhost/sonar");
+ underTest.accept(props);
+ assertThat(props.nonNullValueAsFile(ProcessProperties.JDBC_DRIVER_PATH)).isEqualTo(driverFile);
+ }
+
+ @Test
+ public void checkAndComplete_sets_driver_path_for_mssql() throws Exception {
+ File driverFile = new File(homeDir, "lib/jdbc/mssql/sqljdbc4.jar");
+ FileUtils.touch(driverFile);
+
+ Props props = newProps(JDBC_URL, "jdbc:sqlserver://localhost/sonar;SelectMethod=Cursor");
+ underTest.accept(props);
+ assertThat(props.nonNullValueAsFile(ProcessProperties.JDBC_DRIVER_PATH)).isEqualTo(driverFile);
+ }
+
+ @Test
+ public void driver_file() throws Exception {
+ File driverFile = new File(homeDir, "extensions/jdbc-driver/oracle/ojdbc6.jar");
+ FileUtils.touch(driverFile);
+
+ String path = underTest.driverPath(homeDir, Provider.ORACLE);
+ assertThat(path).isEqualTo(driverFile.getAbsolutePath());
+ }
+
+ @Test
+ public void driver_dir_does_not_exist() throws Exception {
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Directory does not exist: extensions/jdbc-driver/oracle");
+
+ underTest.driverPath(homeDir, Provider.ORACLE);
+ }
+
+ @Test
+ public void no_files_in_driver_dir() throws Exception {
+ FileUtils.forceMkdir(new File(homeDir, "extensions/jdbc-driver/oracle"));
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Directory does not contain JDBC driver: extensions/jdbc-driver/oracle");
+
+ underTest.driverPath(homeDir, Provider.ORACLE);
+ }
+
+ @Test
+ public void too_many_files_in_driver_dir() throws Exception {
+ FileUtils.touch(new File(homeDir, "extensions/jdbc-driver/oracle/ojdbc5.jar"));
+ FileUtils.touch(new File(homeDir, "extensions/jdbc-driver/oracle/ojdbc6.jar"));
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Directory must contain only one JAR file: extensions/jdbc-driver/oracle");
+
+ underTest.driverPath(homeDir, Provider.ORACLE);
+ }
+
+ private Props newProps(String... params) {
+ Properties properties = new Properties();
+ for (int i = 0; i < params.length; i++) {
+ properties.setProperty(params[i], params[i + 1]);
+ i++;
+ }
+ properties.setProperty(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
+ return new Props(properties);
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/config/TestAppSettings.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/TestAppSettings.java
new file mode 100644
index 00000000000..58971dd04b9
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/config/TestAppSettings.java
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.config;
+
+import java.util.Optional;
+import java.util.Properties;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+
+/**
+ * Simple implementation of {@link AppSettings} that loads
+ * the default values defined by {@link ProcessProperties}.
+ */
+public class TestAppSettings implements AppSettings {
+
+ private Props properties;
+
+ public TestAppSettings() {
+ this.properties = new Props(new Properties());
+ ProcessProperties.completeDefaults(this.properties);
+ }
+
+ public TestAppSettings set(String key, String value) {
+ this.properties.set(key, value);
+ return this;
+ }
+
+ @Override
+ public Props getProps() {
+ return properties;
+ }
+
+ @Override
+ public Optional<String> getValue(String key) {
+ return Optional.ofNullable(properties.value(key));
+ }
+
+ @Override
+ public void reload(Props copy) {
+ this.properties = copy;
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/JavaCommandTest.java
index b672afe2c15..4d8d1f85c99 100644
--- a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/JavaCommandTest.java
@@ -17,14 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-package org.sonar.process.monitor;
+package org.sonar.application.process;
+import java.io.File;
+import java.util.Properties;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
-
-import java.io.File;
-import java.util.Properties;
import org.sonar.process.ProcessId;
import static org.assertj.core.api.Assertions.assertThat;
@@ -35,7 +34,7 @@ public class JavaCommandTest {
public TemporaryFolder temp = new TemporaryFolder();
@Test
- public void test_parameters() throws Exception {
+ public void test_command_with_complete_information() throws Exception {
JavaCommand command = new JavaCommand(ProcessId.ELASTICSEARCH);
command.setArgument("first_arg", "val1");
@@ -63,7 +62,7 @@ public class JavaCommandTest {
}
@Test
- public void add_java_options() {
+ public void addJavaOptions_adds_jvm_options() {
JavaCommand command = new JavaCommand(ProcessId.ELASTICSEARCH);
assertThat(command.getJavaOptions()).isEmpty();
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/process/JavaProcessLauncherImplTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/JavaProcessLauncherImplTest.java
new file mode 100644
index 00000000000..3beb208ec25
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/JavaProcessLauncherImplTest.java
@@ -0,0 +1,166 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.application.process.JavaCommand;
+import org.sonar.application.process.JavaProcessLauncher;
+import org.sonar.application.process.JavaProcessLauncherImpl;
+import org.sonar.application.process.ProcessMonitor;
+import org.sonar.process.AllProcessesCommands;
+import org.sonar.process.ProcessId;
+
+import static org.assertj.core.api.Java6Assertions.assertThat;
+import static org.assertj.core.data.MapEntry.entry;
+import static org.mockito.Mockito.RETURNS_MOCKS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class JavaProcessLauncherImplTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private AllProcessesCommands commands = mock(AllProcessesCommands.class, RETURNS_MOCKS);
+
+ @Test
+ public void launch_forks_a_new_process() throws Exception {
+ File tempDir = temp.newFolder();
+ TestProcessBuilder processBuilder = new TestProcessBuilder();
+ JavaProcessLauncher underTest = new JavaProcessLauncherImpl(tempDir, commands, () -> processBuilder);
+ JavaCommand command = new JavaCommand(ProcessId.ELASTICSEARCH);
+ command.addClasspath("lib/*.class");
+ command.addClasspath("lib/*.jar");
+ command.setArgument("foo", "bar");
+ command.setClassName("org.sonarqube.Main");
+ command.setEnvVariable("VAR1", "valueOfVar1");
+ command.setWorkDir(temp.newFolder());
+
+ ProcessMonitor monitor = underTest.launch(command);
+
+ assertThat(monitor).isNotNull();
+ assertThat(processBuilder.started).isTrue();
+ assertThat(processBuilder.commands.get(0)).endsWith("java");
+ assertThat(processBuilder.commands).containsSequence(
+ "-Djava.io.tmpdir=" + tempDir.getAbsolutePath(),
+ "-cp",
+ "lib/*.class" + System.getProperty("path.separator") + "lib/*.jar",
+ "org.sonarqube.Main");
+ assertThat(processBuilder.dir).isEqualTo(command.getWorkDir());
+ assertThat(processBuilder.redirectErrorStream).isTrue();
+ assertThat(processBuilder.environment)
+ .contains(entry("VAR1", "valueOfVar1"))
+ .containsAllEntriesOf(command.getEnvVariables());
+ }
+
+ @Test
+ public void properties_are_passed_to_command_via_a_temporary_properties_file() throws Exception {
+ File tempDir = temp.newFolder();
+ TestProcessBuilder processBuilder = new TestProcessBuilder();
+ JavaProcessLauncher underTest = new JavaProcessLauncherImpl(tempDir, commands, () -> processBuilder);
+ JavaCommand command = new JavaCommand(ProcessId.ELASTICSEARCH);
+ command.setArgument("foo", "bar");
+ command.setArgument("baz", "woo");
+
+ underTest.launch(command);
+
+ String propsFilePath = processBuilder.commands.get(processBuilder.commands.size() - 1);
+ File file = new File(propsFilePath);
+ assertThat(file).exists().isFile();
+ try (FileReader reader = new FileReader(file)) {
+ Properties props = new Properties();
+ props.load(reader);
+ assertThat(props).containsOnly(
+ entry("foo", "bar"),
+ entry("baz", "woo"),
+ entry("process.terminationTimeout", "60000"),
+ entry("process.key", ProcessId.ELASTICSEARCH.getKey()),
+ entry("process.index", String.valueOf(ProcessId.ELASTICSEARCH.getIpcIndex())),
+ entry("process.sharedDir", tempDir.getAbsolutePath()));
+ }
+ }
+
+ @Test
+ public void throw_ISE_if_command_fails() throws IOException {
+ File tempDir = temp.newFolder();
+ JavaProcessLauncher.SystemProcessBuilder processBuilder = mock(JavaProcessLauncher.SystemProcessBuilder.class, RETURNS_MOCKS);
+ when(processBuilder.start()).thenThrow(new IOException("error"));
+ JavaProcessLauncher underTest = new JavaProcessLauncherImpl(tempDir, commands, () -> processBuilder);
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Fail to launch process [es]");
+
+ underTest.launch(new JavaCommand(ProcessId.ELASTICSEARCH));
+ }
+
+ private static class TestProcessBuilder extends JavaProcessLauncher.SystemProcessBuilder {
+ private List<String> commands = null;
+ private File dir = null;
+ private Boolean redirectErrorStream = null;
+ private final Map<String, String> environment = new HashMap<>();
+ private boolean started = false;
+
+ @Override
+ public List<String> command() {
+ return commands;
+ }
+
+ @Override
+ public TestProcessBuilder command(List<String> commands) {
+ this.commands = commands;
+ return this;
+ }
+
+ @Override
+ public TestProcessBuilder directory(File dir) {
+ this.dir = dir;
+ return this;
+ }
+
+ @Override
+ public Map<String, String> environment() {
+ return environment;
+ }
+
+ @Override
+ public TestProcessBuilder redirectErrorStream(boolean b) {
+ this.redirectErrorStream = b;
+ return this;
+ }
+
+ @Override
+ public Process start() throws IOException {
+ this.started = true;
+ return mock(Process.class);
+ }
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/process/LifecycleTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/LifecycleTest.java
new file mode 100644
index 00000000000..79c9e47b273
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/LifecycleTest.java
@@ -0,0 +1,111 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+import org.sonar.application.process.Lifecycle;
+import org.sonar.application.process.ProcessLifecycleListener;
+import org.sonar.process.ProcessId;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.application.process.Lifecycle.State.INIT;
+import static org.sonar.application.process.Lifecycle.State.STARTED;
+import static org.sonar.application.process.Lifecycle.State.STARTING;
+import static org.sonar.application.process.Lifecycle.State.STOPPING;
+
+public class LifecycleTest {
+
+ @Test
+ public void initial_state_is_INIT() {
+ Lifecycle lifecycle = new Lifecycle(ProcessId.ELASTICSEARCH, Collections.emptyList());
+ assertThat(lifecycle.getState()).isEqualTo(INIT);
+ }
+
+ @Test
+ public void try_to_move_does_not_support_jumping_states() {
+ TestLifeCycleListener listener = new TestLifeCycleListener();
+ Lifecycle lifecycle = new Lifecycle(ProcessId.ELASTICSEARCH, asList(listener));
+ assertThat(lifecycle.getState()).isEqualTo(INIT);
+ assertThat(listener.states).isEmpty();
+
+ assertThat(lifecycle.tryToMoveTo(STARTED)).isFalse();
+ assertThat(lifecycle.getState()).isEqualTo(INIT);
+ assertThat(listener.states).isEmpty();
+
+ assertThat(lifecycle.tryToMoveTo(STARTING)).isTrue();
+ assertThat(lifecycle.getState()).isEqualTo(STARTING);
+ assertThat(listener.states).containsOnly(STARTING);
+ }
+
+ @Test
+ public void no_state_can_not_move_to_itself() {
+ for (Lifecycle.State state : Lifecycle.State.values()) {
+ assertThat(newLifeCycle(state).tryToMoveTo(state)).isFalse();
+ }
+ }
+
+ @Test
+ public void can_move_to_STOPPING_from_STARTING_STARTED_only() {
+ for (Lifecycle.State state : Lifecycle.State.values()) {
+ TestLifeCycleListener listener = new TestLifeCycleListener();
+ boolean tryToMoveTo = newLifeCycle(state, listener).tryToMoveTo(STOPPING);
+ if (state == STARTING || state == STARTED) {
+ assertThat(tryToMoveTo).as("from state " + state).isTrue();
+ assertThat(listener.states).containsOnly(STOPPING);
+ } else {
+ assertThat(tryToMoveTo).as("from state " + state).isFalse();
+ assertThat(listener.states).isEmpty();
+ }
+ }
+ }
+
+ @Test
+ public void can_move_to_STARTED_from_STARTING_only() {
+ for (Lifecycle.State state : Lifecycle.State.values()) {
+ TestLifeCycleListener listener = new TestLifeCycleListener();
+ boolean tryToMoveTo = newLifeCycle(state, listener).tryToMoveTo(STARTED);
+ if (state == STARTING) {
+ assertThat(tryToMoveTo).as("from state " + state).isTrue();
+ assertThat(listener.states).containsOnly(STARTED);
+ } else {
+ assertThat(tryToMoveTo).as("from state " + state).isFalse();
+ assertThat(listener.states).isEmpty();
+ }
+ }
+ }
+
+ private static Lifecycle newLifeCycle(Lifecycle.State state, TestLifeCycleListener... listeners) {
+ return new Lifecycle(ProcessId.ELASTICSEARCH, Arrays.asList(listeners), state);
+ }
+
+ private static final class TestLifeCycleListener implements ProcessLifecycleListener {
+ private final List<Lifecycle.State> states = new ArrayList<>();
+
+ @Override
+ public void onProcessState(ProcessId processId, Lifecycle.State state) {
+ this.states.add(state);
+ }
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/process/ProcessMonitorImplTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/ProcessMonitorImplTest.java
new file mode 100644
index 00000000000..f1644b9c808
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/ProcessMonitorImplTest.java
@@ -0,0 +1,111 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.application.process.ProcessMonitorImpl;
+import org.sonar.process.ProcessCommands;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+
+public class ProcessMonitorImplTest {
+
+ @Test
+ public void ProcessMonitorImpl_is_a_proxy_of_Process() throws Exception {
+ Process process = mock(Process.class, RETURNS_DEEP_STUBS);
+ ProcessCommands commands = mock(ProcessCommands.class, RETURNS_DEEP_STUBS);
+
+ ProcessMonitorImpl underTest = new ProcessMonitorImpl(process, commands);
+
+ underTest.waitFor();
+ verify(process).waitFor();
+
+ underTest.closeStreams();
+ verify(process.getErrorStream()).close();
+ verify(process.getInputStream()).close();
+ verify(process.getOutputStream()).close();
+
+ underTest.destroyForcibly();
+ verify(process).destroyForcibly();
+
+ assertThat(underTest.getInputStream()).isNotNull();
+
+ underTest.isAlive();
+ verify(process).isAlive();
+
+ underTest.waitFor(123, TimeUnit.MILLISECONDS);
+ verify(process).waitFor(123, TimeUnit.MILLISECONDS);
+ }
+
+ @Test
+ public void ProcessMonitorImpl_is_a_proxy_of_Commands() throws Exception {
+ Process process = mock(Process.class, RETURNS_DEEP_STUBS);
+ ProcessCommands commands = mock(ProcessCommands.class, RETURNS_DEEP_STUBS);
+
+ ProcessMonitorImpl underTest = new ProcessMonitorImpl(process, commands);
+
+ underTest.askForStop();
+ verify(commands).askForStop();
+
+ underTest.acknowledgeAskForRestart();
+ verify(commands).acknowledgeAskForRestart();
+
+ underTest.askedForRestart();
+ verify(commands).askedForRestart();
+
+ underTest.isOperational();
+ verify(commands).isOperational();
+ }
+
+ @Test
+ public void closeStreams_ignores_null_stream() {
+ ProcessCommands commands = mock(ProcessCommands.class);
+ Process process = mock(Process.class);
+ when(process.getInputStream()).thenReturn(null);
+
+ ProcessMonitorImpl underTest = new ProcessMonitorImpl(process, commands);
+
+ // no failures
+ underTest.closeStreams();
+ }
+
+ @Test
+ public void closeStreams_ignores_failure_if_stream_fails_to_be_closed() throws Exception {
+ InputStream stream = mock(InputStream.class);
+ doThrow(new IOException("error")).when(stream).close();
+ Process process = mock(Process.class);
+ when(process.getInputStream()).thenReturn(stream);
+
+ ProcessMonitorImpl underTest = new ProcessMonitorImpl(process, mock(ProcessCommands.class, Mockito.RETURNS_MOCKS));
+
+ // no failures
+ underTest.closeStreams();
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/process/SQProcessTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/SQProcessTest.java
new file mode 100644
index 00000000000..84433436aa6
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/SQProcessTest.java
@@ -0,0 +1,327 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.InputStream;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.mockito.Mockito;
+import org.sonar.process.ProcessId;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+public class SQProcessTest {
+
+ private static final ProcessId A_PROCESS_ID = ProcessId.ELASTICSEARCH;
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public TestRule safeGuard = new DisableOnDebug(Timeout.seconds(10));
+
+ @Test
+ public void initial_state_is_INIT() {
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
+
+ assertThat(underTest.getProcessId()).isEqualTo(A_PROCESS_ID);
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.INIT);
+ }
+
+ @Test
+ public void start_and_stop_process() {
+ ProcessLifecycleListener listener = mock(ProcessLifecycleListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addProcessLifecycleListener(listener)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ assertThat(underTest.start(() -> testProcess)).isTrue();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STARTED);
+ assertThat(testProcess.isAlive()).isTrue();
+ assertThat(testProcess.streamsClosed).isFalse();
+ verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STARTED);
+
+ testProcess.close();
+ // do not wait next run of watcher threads
+ underTest.refreshState();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
+ assertThat(testProcess.isAlive()).isFalse();
+ assertThat(testProcess.streamsClosed).isTrue();
+ verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STOPPED);
+ }
+ }
+
+ @Test
+ public void start_does_not_nothing_if_already_started_once() {
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ assertThat(underTest.start(() -> testProcess)).isTrue();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STARTED);
+
+ assertThat(underTest.start(() -> {throw new IllegalStateException();})).isFalse();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STARTED);
+ }
+ }
+
+ @Test
+ public void start_throws_exception_and_move_to_state_STOPPED_if_execution_of_command_fails() {
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("error");
+
+ underTest.start(() -> {throw new IllegalStateException("error");});
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
+ }
+
+ @Test
+ public void send_event_when_process_is_operational() {
+ ProcessEventListener listener = mock(ProcessEventListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addEventListener(listener)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+
+ testProcess.operational = true;
+ underTest.refreshState();
+
+ verify(listener).onProcessEvent(A_PROCESS_ID, ProcessEventListener.Type.OPERATIONAL);
+ }
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void operational_event_is_sent_once() {
+ ProcessEventListener listener = mock(ProcessEventListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addEventListener(listener)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+ testProcess.operational = true;
+
+ underTest.refreshState();
+ verify(listener).onProcessEvent(A_PROCESS_ID, ProcessEventListener.Type.OPERATIONAL);
+
+ // second run
+ underTest.refreshState();
+ verifyNoMoreInteractions(listener);
+ }
+ }
+
+ @Test
+ public void send_event_when_process_requests_for_restart() {
+ ProcessEventListener listener = mock(ProcessEventListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addEventListener(listener)
+ .setWatcherDelayMs(1L)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+
+ testProcess.askedForRestart = true;
+ verify(listener, timeout(10_000)).onProcessEvent(A_PROCESS_ID, ProcessEventListener.Type.ASK_FOR_RESTART);
+
+ // flag is reset so that next run does not trigger again the event
+ underTest.refreshState();
+ verifyNoMoreInteractions(listener);
+ assertThat(testProcess.askedForRestart).isFalse();
+ }
+ }
+
+ @Test
+ public void stopForcibly_stops_the_process_without_graceful_request_for_stop() {
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+
+ underTest.stopForcibly();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
+ assertThat(testProcess.askedForStop).isFalse();
+ assertThat(testProcess.destroyedForcibly).isTrue();
+
+ // second execution of stopForcibly does nothing. It's still stopped.
+ underTest.stopForcibly();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
+ }
+ }
+
+ @Test
+ public void process_stops_after_graceful_request_for_stop() throws Exception {
+ ProcessLifecycleListener listener = mock(ProcessLifecycleListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addProcessLifecycleListener(listener)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+
+ Thread stopperThread = new Thread(() -> underTest.stop(1, TimeUnit.HOURS));
+ stopperThread.start();
+
+ // thread is blocked until process stopped
+ assertThat(stopperThread.isAlive()).isTrue();
+
+ // wait for the stopper thread to ask graceful stop
+ while (!testProcess.askedForStop) {
+ Thread.sleep(1L);
+ }
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPING);
+ verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STOPPING);
+
+ // process stopped
+ testProcess.close();
+
+ // waiting for stopper thread to detect and handle the stop
+ stopperThread.join();
+
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
+ verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STOPPED);
+ }
+ }
+
+ @Test
+ public void process_is_stopped_forcibly_if_graceful_stop_is_too_long() throws Exception {
+ ProcessLifecycleListener listener = mock(ProcessLifecycleListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addProcessLifecycleListener(listener)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+
+ underTest.stop(1L, TimeUnit.MILLISECONDS);
+
+ testProcess.waitFor();
+ assertThat(testProcess.askedForStop).isTrue();
+ assertThat(testProcess.destroyedForcibly).isTrue();
+ assertThat(testProcess.isAlive()).isFalse();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
+ verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STOPPED);
+ }
+ }
+
+ @Test
+ public void process_requests_are_listened_on_regular_basis() throws Exception {
+ ProcessEventListener listener = mock(ProcessEventListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addEventListener(listener)
+ .setWatcherDelayMs(1L)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+
+ testProcess.operational = true;
+
+ verify(listener, timeout(1_000L)).onProcessEvent(A_PROCESS_ID, ProcessEventListener.Type.OPERATIONAL);
+ }
+ }
+
+ @Test
+ public void test_toString() {
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
+ assertThat(underTest.toString()).isEqualTo("Process[" + A_PROCESS_ID.getKey() + "]");
+ }
+
+ private static class TestProcess implements ProcessMonitor, AutoCloseable {
+
+ private final CountDownLatch alive = new CountDownLatch(1);
+ private final InputStream inputStream = mock(InputStream.class, Mockito.RETURNS_MOCKS);
+ private boolean streamsClosed = false;
+ private boolean operational = false;
+ private boolean askedForRestart = false;
+ private boolean askedForStop = false;
+ private boolean destroyedForcibly = false;
+
+ @Override
+ public InputStream getInputStream() {
+ return inputStream;
+ }
+
+ @Override
+ public void closeStreams() {
+ streamsClosed = true;
+ }
+
+ @Override
+ public boolean isAlive() {
+ return alive.getCount() == 1;
+ }
+
+ @Override
+ public void askForStop() {
+ askedForStop = true;
+ // do not stop, just asking
+ }
+
+ @Override
+ public void destroyForcibly() {
+ destroyedForcibly = true;
+ alive.countDown();
+ }
+
+ @Override
+ public void waitFor() throws InterruptedException {
+ alive.await();
+ }
+
+ @Override
+ public void waitFor(long timeout, TimeUnit timeoutUnit) throws InterruptedException {
+ alive.await(timeout, timeoutUnit);
+ }
+
+ @Override
+ public boolean isOperational() {
+ return operational;
+ }
+
+ @Override
+ public boolean askedForRestart() {
+ return askedForRestart;
+ }
+
+ @Override
+ public void acknowledgeAskForRestart() {
+ this.askedForRestart = false;
+ }
+
+ @Override
+ public void close() {
+ alive.countDown();
+ }
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/process/StopRequestWatcherImplTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/StopRequestWatcherImplTest.java
new file mode 100644
index 00000000000..3fbe5f8a52c
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/StopRequestWatcherImplTest.java
@@ -0,0 +1,114 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.IOException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.sonar.application.FileSystem;
+import org.sonar.application.Scheduler;
+import org.sonar.application.config.AppSettings;
+import org.sonar.process.ProcessCommands;
+import org.sonar.process.ProcessProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class StopRequestWatcherImplTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public TestRule safeGuard = new DisableOnDebug(Timeout.seconds(10));
+
+ private AppSettings settings = mock(AppSettings.class, RETURNS_DEEP_STUBS);
+ private ProcessCommands commands = mock(ProcessCommands.class);
+ private Scheduler scheduler = mock(Scheduler.class);
+
+ @Test
+ public void do_not_watch_command_if_disabled() throws IOException {
+ enableSetting(false);
+ StopRequestWatcherImpl underTest = new StopRequestWatcherImpl(settings, scheduler, commands);
+
+ underTest.startWatching();
+ assertThat(underTest.isAlive()).isFalse();
+
+ underTest.stopWatching();
+ verifyZeroInteractions(commands, scheduler);
+ }
+
+ @Test
+ public void watch_stop_command_if_enabled() throws Exception {
+ enableSetting(true);
+ StopRequestWatcherImpl underTest = new StopRequestWatcherImpl(settings, scheduler, commands);
+ underTest.setDelayMs(1L);
+
+ underTest.startWatching();
+ assertThat(underTest.isAlive()).isTrue();
+ verify(scheduler, never()).terminate();
+
+ when(commands.askedForStop()).thenReturn(true);
+ verify(scheduler, timeout(1_000L)).terminate();
+
+ underTest.stopWatching();
+ while (underTest.isAlive()) {
+ Thread.sleep(1L);
+ }
+ }
+
+ @Test
+ public void create_instance_with_default_delay() throws IOException {
+ FileSystem fs = mock(FileSystem.class);
+ when(fs.getTempDir()).thenReturn(temp.newFolder());
+
+ StopRequestWatcherImpl underTest = StopRequestWatcherImpl.create(settings, scheduler, fs);
+
+ assertThat(underTest.getDelayMs()).isEqualTo(500L);
+ }
+
+ @Test
+ public void stop_watching_commands_if_thread_is_interrupted() throws Exception {
+ enableSetting(true);
+ StopRequestWatcherImpl underTest = new StopRequestWatcherImpl(settings, scheduler, commands);
+
+ underTest.startWatching();
+ underTest.interrupt();
+
+ while (underTest.isAlive()) {
+ Thread.sleep(1L);
+ }
+ assertThat(underTest.isAlive()).isFalse();
+ }
+
+ private void enableSetting(boolean b) {
+ when(settings.getProps().valueAsBoolean(ProcessProperties.ENABLE_STOP_COMMAND)).thenReturn(b);
+ }
+
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/StreamGobblerTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/StreamGobblerTest.java
index 643f0bdb8a7..2f1498d16dc 100644
--- a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/StreamGobblerTest.java
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/StreamGobblerTest.java
@@ -17,13 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-package org.sonar.process.monitor;
+package org.sonar.application.process;
+import java.io.InputStream;
import org.apache.commons.io.IOUtils;
import org.junit.Test;
import org.slf4j.Logger;
-
-import java.io.InputStream;
+import org.sonar.application.process.StreamGobbler;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java
deleted file mode 100644
index 53112e5935b..00000000000
--- a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.process.monitor;
-
-import java.io.File;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TemporaryFolder;
-import org.sonar.process.ProcessId;
-
-public class JavaProcessLauncherTest {
-
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- @Test
- public void fail_to_launch() throws Exception {
- File tempDir = temp.newFolder();
- JavaCommand command = new JavaCommand(ProcessId.ELASTICSEARCH);
- JavaProcessLauncher launcher = new JavaProcessLauncher(new Timeouts(), tempDir);
-
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("Fail to launch [es]");
-
- // command is not correct (missing options), java.lang.ProcessBuilder#start()
- // throws an exception
- launcher.launch(command);
- }
-}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java
deleted file mode 100644
index 175d99c41d1..00000000000
--- a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java
+++ /dev/null
@@ -1,630 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.process.monitor;
-
-import com.github.kevinsawicki.http.HttpRequest;
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.function.Supplier;
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang.StringUtils;
-import org.assertj.core.api.AbstractAssert;
-import org.assertj.core.internal.Longs;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.DisableOnDebug;
-import org.junit.rules.TemporaryFolder;
-import org.junit.rules.TestRule;
-import org.junit.rules.Timeout;
-import org.sonar.process.Lifecycle.State;
-import org.sonar.process.NetworkUtils;
-import org.sonar.process.ProcessId;
-import org.sonar.process.SystemExit;
-
-import static java.util.Collections.singletonList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.sonar.process.monitor.Monitor.newMonitorBuilder;
-import static org.sonar.process.monitor.MonitorTest.HttpProcessClientAssert.assertThat;
-
-public class MonitorTest {
-
- private static File testJar;
-
- private FileSystem fileSystem = mock(FileSystem.class);
- private SystemExit exit = mock(SystemExit.class);
-
- private Monitor underTest;
-
- /**
- * Find the JAR file containing the test apps. Classes can't be moved in sonar-process-monitor because
- * they require sonar-process dependencies when executed here (sonar-process, commons-*, ...).
- */
- @BeforeClass
- public static void initTestJar() {
- File targetDir = new File("server/sonar-process/target");
- if (!targetDir.exists() || !targetDir.isDirectory()) {
- targetDir = new File("../sonar-process/target");
- }
- if (!targetDir.exists() || !targetDir.isDirectory()) {
- throw new IllegalStateException("target dir of sonar-process module not found. Please build it.");
- }
- Collection<File> jars = FileUtils.listFiles(targetDir, new String[] {"jar"}, false);
- for (File jar : jars) {
- if (jar.getName().startsWith("sonar-process-") && jar.getName().endsWith("-test-jar-with-dependencies.jar")) {
- testJar = jar;
- return;
- }
- }
- throw new IllegalStateException("No sonar-process-*-test-jar-with-dependencies.jar in " + targetDir);
- }
-
- /**
- * Safeguard
- */
- @Rule
- public TestRule globalTimeout = new DisableOnDebug(Timeout.seconds(60));
-
- /**
- * Temporary directory is used to interact with monitored processes, which write in it.
- */
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
-
- private File tempDir;
-
- @Before
- public void setUp() throws Exception {
- tempDir = temp.newFolder();
-
- }
-
- /**
- * Safeguard
- */
- @After
- public void tearDown() {
- try {
- if (underTest != null) {
- underTest.stop();
- }
- } catch (Throwable ignored) {
- }
- }
-
- @Test
- public void fail_to_start_if_no_commands() throws Exception {
- underTest = newDefaultMonitor(tempDir);
- try {
- underTest.start(Collections::emptyList);
- fail();
- } catch (IllegalArgumentException e) {
- assertThat(e).hasMessage("At least one command is required");
- }
- }
-
- @Test
- public void fail_to_start_multiple_times() throws Exception {
- underTest = newDefaultMonitor(tempDir);
- underTest.start(() -> singletonList(newStandardProcessCommand()));
- boolean failed = false;
- try {
- underTest.start(() -> singletonList(newStandardProcessCommand()));
- } catch (IllegalStateException e) {
- failed = e.getMessage().equals("Can not start multiple times");
- }
- underTest.stop();
- assertThat(failed).isTrue();
- }
-
- @Test
- public void start_then_stop_gracefully() throws Exception {
- underTest = newDefaultMonitor(tempDir);
- HttpProcessClient client = new HttpProcessClient(tempDir, ProcessId.ELASTICSEARCH);
- // blocks until started
- underTest.start(() -> singletonList(client.newCommand()));
-
- assertThat(client).isUp()
- .wasStartedBefore(System.currentTimeMillis());
-
- // blocks until stopped
- underTest.stop();
- assertThat(client)
- .isNotUp()
- .wasGracefullyTerminated();
- assertThat(underTest.getState()).isEqualTo(State.STOPPED);
- verify(fileSystem).reset();
- }
-
- @Test
- public void start_then_stop_sequence_of_commands() throws Exception {
- underTest = newDefaultMonitor(tempDir);
- HttpProcessClient p1 = new HttpProcessClient(tempDir, ProcessId.ELASTICSEARCH);
- HttpProcessClient p2 = new HttpProcessClient(tempDir, ProcessId.WEB_SERVER);
- underTest.start(() -> Arrays.asList(p1.newCommand(), p2.newCommand()));
-
- // start p2 when p1 is fully started (ready)
- assertThat(p1)
- .isUp()
- .wasStartedBefore(p2);
- assertThat(p2)
- .isUp();
-
- underTest.stop();
-
- // stop in inverse order
- assertThat(p1)
- .isNotUp()
- .wasGracefullyTerminated();
- assertThat(p2)
- .isNotUp()
- .wasGracefullyTerminatedBefore(p1);
- verify(fileSystem).reset();
- }
-
- @Test
- public void stop_all_processes_if_monitor_shutdowns() throws Exception {
- underTest = newDefaultMonitor(tempDir);
- HttpProcessClient p1 = new HttpProcessClient(tempDir, ProcessId.ELASTICSEARCH);
- HttpProcessClient p2 = new HttpProcessClient(tempDir, ProcessId.WEB_SERVER);
- underTest.start(() -> Arrays.asList(p1.newCommand(), p2.newCommand()));
- assertThat(p1).isUp();
- assertThat(p2).isUp();
-
- // emulate CTRL-C
- underTest.getShutdownHook().run();
- underTest.getShutdownHook().join();
-
- assertThat(p1).wasGracefullyTerminated();
- assertThat(p2).wasGracefullyTerminated();
-
- verify(fileSystem).reset();
- }
-
- @Test
- public void restart_all_processes_if_one_asks_for_restart() throws Exception {
- underTest = newDefaultMonitor(tempDir);
- HttpProcessClient p1 = new HttpProcessClient(tempDir, ProcessId.ELASTICSEARCH);
- HttpProcessClient p2 = new HttpProcessClient(tempDir, ProcessId.WEB_SERVER);
- underTest.start(() -> Arrays.asList(p1.newCommand(), p2.newCommand()));
-
- assertThat(p1).isUp();
- assertThat(p2).isUp();
-
- p2.restart();
-
- assertThat(underTest.waitForOneRestart()).isTrue();
-
- assertThat(p1)
- .wasStarted(2)
- .wasGracefullyTerminated(1);
- assertThat(p2)
- .wasStarted(2)
- .wasGracefullyTerminated(1);
-
- underTest.stop();
-
- assertThat(p1)
- .wasStarted(2)
- .wasGracefullyTerminated(2);
- assertThat(p2)
- .wasStarted(2)
- .wasGracefullyTerminated(2);
-
- verify(fileSystem, times(2)).reset();
- }
-
- @Test
- public void restart_reloads_java_commands() throws Exception {
- underTest = newDefaultMonitor(tempDir);
- HttpProcessClient p1 = new HttpProcessClient(tempDir, ProcessId.ELASTICSEARCH);
- HttpProcessClient p2 = new HttpProcessClient(tempDir, ProcessId.WEB_SERVER);
-
- // a supplier that will return p1 the first time it's called and then p2 all the time
- Supplier<List<JavaCommand>> listSupplier = new Supplier<List<JavaCommand>>() {
- private int counter = 0;
- @Override
- public List<JavaCommand> get() {
- if (counter == 0) {
- counter++;
- return Collections.singletonList(p1.newCommand());
- } else {
- return Collections.singletonList(p2.newCommand());
- }
- }
- };
-
- underTest.start(listSupplier);
-
- assertThat(p1).isUp();
- assertThat(p2).isNotUp();
-
- p1.restart();
-
- assertThat(underTest.waitForOneRestart()).isTrue();
-
- assertThat(p1)
- .wasStarted(1)
- .wasGracefullyTerminated(1);
- assertThat(p2)
- .wasStarted(1)
- .isUp();
-
- underTest.stop();
- assertThat(p1)
- .wasStarted(1)
- .wasGracefullyTerminated(1);
- assertThat(p2)
- .wasStarted(1)
- .wasGracefullyTerminated(1);
- }
-
- @Test
- public void stop_all_processes_if_one_shutdowns() throws Exception {
- underTest = newDefaultMonitor(tempDir);
- HttpProcessClient p1 = new HttpProcessClient(tempDir, ProcessId.ELASTICSEARCH);
- HttpProcessClient p2 = new HttpProcessClient(tempDir, ProcessId.WEB_SERVER);
- underTest.start(() -> Arrays.asList(p1.newCommand(), p2.newCommand()));
- assertThat(p1.isUp()).isTrue();
- assertThat(p2.isUp()).isTrue();
-
- // kill p1 -> waiting for detection by monitor than termination of p2
- p1.kill();
- underTest.awaitTermination();
-
- assertThat(p1)
- .isNotUp()
- .wasNotGracefullyTerminated();
- assertThat(p2)
- .isNotUp()
- .wasGracefullyTerminated();
-
- verify(fileSystem).reset();
- }
-
- @Test
- public void stop_all_processes_if_one_fails_to_start() throws Exception {
- underTest = newDefaultMonitor(tempDir);
- HttpProcessClient p1 = new HttpProcessClient(tempDir, ProcessId.ELASTICSEARCH);
- HttpProcessClient p2 = new HttpProcessClient(tempDir, ProcessId.WEB_SERVER, -1);
- try {
- underTest.start(() -> Arrays.asList(p1.newCommand(), p2.newCommand()));
- fail();
- } catch (Exception expected) {
- assertThat(p1)
- .hasBeenReady()
- .wasGracefullyTerminated();
- assertThat(p2)
- .hasNotBeenReady()
- // self "gracefully terminated", even if startup went bad
- .wasGracefullyTerminated();
- }
- }
-
- @Test
- public void fail_to_start_if_bad_class_name() throws Exception {
- underTest = newDefaultMonitor(tempDir);
- JavaCommand command = new JavaCommand(ProcessId.ELASTICSEARCH)
- .addClasspath(testJar.getAbsolutePath())
- .setClassName("org.sonar.process.test.Unknown");
-
- try {
- underTest.start(() -> singletonList(command));
- fail();
- } catch (Exception e) {
- // expected
- // TODO improve, too many stacktraces logged
- }
- }
-
- @Test
- public void watchForHardStop_adds_a_hardStopWatcher_thread_and_starts_it() throws Exception {
- underTest = newDefaultMonitor(tempDir, true);
- assertThat(underTest.hardStopWatcher).isNull();
-
- HttpProcessClient p1 = new HttpProcessClient(tempDir, ProcessId.COMPUTE_ENGINE);
- underTest.start(() -> singletonList(p1.newCommand()));
-
- assertThat(underTest.hardStopWatcher).isNotNull();
- assertThat(underTest.hardStopWatcher.isAlive()).isTrue();
-
- p1.kill();
- underTest.awaitTermination();
-
- assertThat(underTest.hardStopWatcher.isAlive()).isFalse();
- }
-
- private Monitor newDefaultMonitor(File tempDir) throws IOException {
- return newDefaultMonitor(tempDir, false);
- }
-
- private Monitor newDefaultMonitor(File tempDir, boolean watchForHardStop) throws IOException {
- when(fileSystem.getTempDir()).thenReturn(tempDir);
- return newMonitorBuilder()
- .setProcessNumber(1)
- .setFileSystem(fileSystem)
- .setExit(exit)
- .setWatchForHardStop(watchForHardStop)
- .build();
- }
-
- /**
- * Interaction with {@link org.sonar.process.test.HttpProcess}
- */
- private class HttpProcessClient {
- private final int httpPort;
- private final ProcessId processId;
- private final File tempDir;
-
- private HttpProcessClient(File tempDir, ProcessId processId) throws IOException {
- this(tempDir, processId, NetworkUtils.freePort());
- }
-
- /**
- * Use httpPort=-1 to make server fail to start
- */
- private HttpProcessClient(File tempDir, ProcessId processId, int httpPort) throws IOException {
- this.tempDir = tempDir;
- this.processId = processId;
- this.httpPort = httpPort;
- }
-
- JavaCommand newCommand() {
- return new JavaCommand(processId)
- .addClasspath(testJar.getAbsolutePath())
- .setClassName("org.sonar.process.test.HttpProcess")
- .setArgument("httpPort", String.valueOf(httpPort));
- }
-
- /**
- * @see org.sonar.process.test.HttpProcess
- */
- boolean isUp() {
- try {
- HttpRequest httpRequest = HttpRequest.get("http://localhost:" + httpPort + "/" + "ping")
- .readTimeout(2000).connectTimeout(2000);
- return httpRequest.ok() && httpRequest.body().equals("ping");
- } catch (HttpRequest.HttpRequestException e) {
- return false;
- }
- }
-
- /**
- * @see org.sonar.process.test.HttpProcess
- */
- void kill() {
- try {
- HttpRequest.post("http://localhost:" + httpPort + "/" + "kill")
- .readTimeout(5000).connectTimeout(5000).ok();
- } catch (Exception e) {
- // HTTP request can't be fully processed, as web server hardly
- // calls "System.exit()"
- }
- }
-
- public void restart() {
- try {
- HttpRequest httpRequest = HttpRequest.post("http://localhost:" + httpPort + "/" + "restart")
- .readTimeout(5000).connectTimeout(5000);
- if (!httpRequest.ok() || !"ok".equals(httpRequest.body())) {
- throw new IllegalStateException("Wrong response calling restart");
- }
- } catch (Exception e) {
- throw new IllegalStateException("Failed to call restart", e);
- }
- }
-
- /**
- * @see org.sonar.process.test.HttpProcess
- */
- boolean wasGracefullyTerminated() {
- return fileExists("terminatedAt");
- }
-
- List<Long> wasStartingAt() {
- return readTimeFromFile("startingAt");
- }
-
- List<Long> wasGracefullyTerminatedAt() {
- return readTimeFromFile("terminatedAt");
- }
-
- boolean wasReady() {
- return fileExists("readyAt");
- }
-
- List<Long> wasReadyAt() {
- return readTimeFromFile("readyAt");
- }
-
- private List<Long> readTimeFromFile(String filename) {
- try {
- File file = new File(tempDir, httpPort + "-" + filename);
- if (file.isFile() && file.exists()) {
- String[] split = StringUtils.split(FileUtils.readFileToString(file), ',');
- List<Long> res = new ArrayList<>(split.length);
- for (String s : split) {
- res.add(Long.parseLong(s));
- }
- return res;
- }
- } catch (IOException e) {
- return Collections.emptyList();
- }
- throw new IllegalStateException("File does not exist");
- }
-
- private boolean fileExists(String filename) {
- File file = new File(tempDir, httpPort + "-" + filename);
- return file.isFile() && file.exists();
- }
- }
-
- public static class HttpProcessClientAssert extends AbstractAssert<HttpProcessClientAssert, HttpProcessClient> {
- Longs longs = Longs.instance();
-
- protected HttpProcessClientAssert(HttpProcessClient actual) {
- super(actual, HttpProcessClientAssert.class);
- }
-
- public static HttpProcessClientAssert assertThat(HttpProcessClient actual) {
- return new HttpProcessClientAssert(actual);
- }
-
- public HttpProcessClientAssert wasStarted(int times) {
- isNotNull();
-
- List<Long> startingAt = actual.wasStartingAt();
- longs.assertEqual(info, startingAt.size(), times);
-
- return this;
- }
-
- public HttpProcessClientAssert wasStartedBefore(long date) {
- isNotNull();
-
- List<Long> startingAt = actual.wasStartingAt();
- longs.assertEqual(info, startingAt.size(), 1);
- longs.assertLessThanOrEqualTo(info, startingAt.iterator().next(), date);
-
- return this;
- }
-
- public HttpProcessClientAssert wasStartedBefore(HttpProcessClient client) {
- isNotNull();
-
- List<Long> startingAt = actual.wasStartingAt();
- longs.assertEqual(info, startingAt.size(), 1);
- longs.assertLessThanOrEqualTo(info, startingAt.iterator().next(), client.wasStartingAt().iterator().next());
-
- return this;
- }
-
- public HttpProcessClientAssert wasTerminated(int times) {
- isNotNull();
-
- List<Long> terminatedAt = actual.wasGracefullyTerminatedAt();
- longs.assertEqual(info, terminatedAt.size(), 2);
-
- return this;
- }
-
- public HttpProcessClientAssert wasGracefullyTerminated() {
- isNotNull();
-
- if (!actual.wasGracefullyTerminated()) {
- failWithMessage("HttpClient %s should have been gracefully terminated", actual.processId.getKey());
- }
-
- return this;
- }
-
- public HttpProcessClientAssert wasNotGracefullyTerminated() {
- isNotNull();
-
- if (actual.wasGracefullyTerminated()) {
- failWithMessage("HttpClient %s should not have been gracefully terminated", actual.processId.getKey());
- }
-
- return this;
- }
-
- public HttpProcessClientAssert wasGracefullyTerminatedBefore(HttpProcessClient p1) {
- isNotNull();
-
- List<Long> wasGracefullyTerminatedAt = actual.wasGracefullyTerminatedAt();
- longs.assertEqual(info, wasGracefullyTerminatedAt.size(), 1);
- longs.assertLessThanOrEqualTo(info, wasGracefullyTerminatedAt.iterator().next(), p1.wasGracefullyTerminatedAt().iterator().next());
-
- return this;
- }
-
- public HttpProcessClientAssert wasGracefullyTerminated(int times) {
- isNotNull();
-
- List<Long> wasGracefullyTerminatedAt = actual.wasGracefullyTerminatedAt();
- longs.assertEqual(info, wasGracefullyTerminatedAt.size(), times);
-
- return this;
- }
-
- public HttpProcessClientAssert isUp() {
- isNotNull();
-
- // check condition
- if (!actual.isUp()) {
- failWithMessage("HttpClient %s should be up", actual.processId.getKey());
- }
-
- return this;
- }
-
- public HttpProcessClientAssert isNotUp() {
- isNotNull();
-
- if (actual.isUp()) {
- failWithMessage("HttpClient %s should not be up", actual.processId.getKey());
- }
-
- return this;
- }
-
- public HttpProcessClientAssert hasBeenReady() {
- isNotNull();
-
- // check condition
- if (!actual.wasReady()) {
- failWithMessage("HttpClient %s should been ready at least once", actual.processId.getKey());
- }
-
- return this;
- }
-
- public HttpProcessClientAssert hasNotBeenReady() {
- isNotNull();
-
- // check condition
- if (actual.wasReady()) {
- failWithMessage("HttpClient %s should never been ready", actual.processId.getKey());
- }
-
- return this;
- }
- }
-
- private JavaCommand newStandardProcessCommand() {
- return new JavaCommand(ProcessId.ELASTICSEARCH)
- .addClasspath(testJar.getAbsolutePath())
- .setClassName("org.sonar.process.test.StandardProcess");
- }
-
-}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java
deleted file mode 100644
index e91afaedb89..00000000000
--- a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.process.monitor;
-
-import org.junit.Test;
-import org.mockito.Mockito;
-
-import static org.mockito.Mockito.*;
-
-public class WatcherThreadTest {
-
- @Test(timeout = 10000L)
- public void continue_even_if_interrupted() throws Exception {
- Monitor monitor = mock(Monitor.class);
- ProcessRef ref = mock(ProcessRef.class, Mockito.RETURNS_DEEP_STUBS);
- when(ref.getProcess().waitFor()).thenThrow(new InterruptedException()).thenReturn(0);
- WatcherThread watcher = new WatcherThread(ref, monitor);
- watcher.start();
- watcher.join();
- verify(monitor).stopAsync();
- }
-}
diff --git a/server/sonar-process-monitor/src/test/resources/logback-test.xml b/server/sonar-process-monitor/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..4c62d576dee
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/resources/logback-test.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration debug="false">
+ <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
+
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>
+ %d{yyyy.MM.dd HH:mm:ss} %-5level %msg%n
+ </pattern>
+ </encoder>
+ </appender>
+
+ <root>
+ <level value="INFO"/>
+ <appender-ref ref="CONSOLE"/>
+ </root>
+
+</configuration>
diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt
deleted file mode 100644
index 65b98c522da..00000000000
--- a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt
+++ /dev/null
@@ -1 +0,0 @@
-0PZz+G+f8mjr3sPn4+AhHg== \ No newline at end of file
diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt
deleted file mode 100644
index b33e179e5c8..00000000000
--- a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt
+++ /dev/null
@@ -1 +0,0 @@
-badbadbad== \ No newline at end of file
diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt
deleted file mode 100644
index ab83e4adc03..00000000000
--- a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-
- 0PZz+G+f8mjr3sPn4+AhHg==
-
diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt
deleted file mode 100644
index 23f5ecf5104..00000000000
--- a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt
+++ /dev/null
@@ -1 +0,0 @@
-IBxEUxZ41c8XTxyaah1Qlg== \ No newline at end of file
diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties b/server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties
deleted file mode 100644
index 5c06e58a32e..00000000000
--- a/server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-hello: world
-foo=bar
-java.io.tmpdir=/should/be/overridden
diff --git a/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar b/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar
deleted file mode 100644
index 6dfd458329a..00000000000
--- a/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar
+++ /dev/null
Binary files differ