diff options
author | Simon Brandhof <simon.brandhof@sonarsource.com> | 2017-03-10 15:01:01 +0100 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@sonarsource.com> | 2017-03-13 13:54:03 +0100 |
commit | 6fa3d925c688fa8e67480c7a69ded9f86aba5326 (patch) | |
tree | 11f4baac39111ae6822f945faf344e88b99dec7c /server/sonar-process-monitor/src/test | |
parent | 857d12fa9909a5b5fde7a42f231b0b8d42e50303 (diff) | |
download | sonarqube-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')
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 Binary files differdeleted file mode 100644 index 6dfd458329a..00000000000 --- a/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar +++ /dev/null |