From a0c5aec5be9cb64b14461af4f2e4bc366b8e5ccc Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Thu, 28 Sep 2017 11:01:30 +0200 Subject: [PATCH] SONAR-9803 add the tests that have been dropped by mistake --- .../java/org/sonar/process/LoggingRule.java | 83 ++++++ .../sonar/process/TestLogbackAppender.java | 35 +++ .../sonar/process/cluster/NodeTypeTest.java | 47 ++++ .../health/HealthStateRefresherTest.java | 90 +++++++ .../cluster/health/NodeDetailsTest.java | 247 +++++++++++++++++ .../health/NodeDetailsTestSupport.java | 88 ++++++ .../cluster/health/NodeHealthTest.java | 195 ++++++++++++++ .../health/SharedHealthStateImplTest.java | 251 ++++++++++++++++++ .../org/sonar/process/logback-test.xml | 20 ++ 9 files changed, 1056 insertions(+) create mode 100644 server/sonar-process/src/test/java/org/sonar/process/LoggingRule.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/TestLogbackAppender.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/cluster/NodeTypeTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/cluster/health/HealthStateRefresherTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/cluster/health/NodeDetailsTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/cluster/health/NodeDetailsTestSupport.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/cluster/health/NodeHealthTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/cluster/health/SharedHealthStateImplTest.java create mode 100644 server/sonar-process/src/test/resources/org/sonar/process/logback-test.xml diff --git a/server/sonar-process/src/test/java/org/sonar/process/LoggingRule.java b/server/sonar-process/src/test/java/org/sonar/process/LoggingRule.java new file mode 100644 index 00000000000..00e8662f36a --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/LoggingRule.java @@ -0,0 +1,83 @@ +/* + * 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; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.LoggingEvent; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.rules.ExternalResource; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import org.sonar.process.logging.LogbackHelper; + +public class LoggingRule extends ExternalResource { + + private final Class loggerClass; + + public LoggingRule(Class loggerClass) { + this.loggerClass = loggerClass; + } + + @Override + protected void before() throws Throwable { + new LogbackHelper().resetFromXml("/org/sonar/process/logback-test.xml"); + TestLogbackAppender.events.clear(); + setLevel(Level.INFO); + } + + @Override + protected void after() { + TestLogbackAppender.events.clear(); + setLevel(Level.INFO); + } + + public LoggingRule setLevel(Level level) { + Logger logbackLogger = (Logger) LoggerFactory.getLogger(loggerClass); + ch.qos.logback.classic.Level l = ch.qos.logback.classic.Level.valueOf(level.name()); + logbackLogger.setLevel(l); + return this; + } + + public List getLogs() { + return TestLogbackAppender.events.stream() + .map(LoggingEvent::getFormattedMessage) + .collect(Collectors.toList()); + } + + public List getLogs(Level level) { + return TestLogbackAppender.events.stream() + .filter(e -> e.getLoggerName().equals(loggerClass.getName())) + .filter(e -> e.getLevel().levelStr.equals(level.name())) + .map(LoggingEvent::getFormattedMessage) + .collect(Collectors.toList()); + } + + public boolean hasLog(Level level, String message) { + return TestLogbackAppender.events.stream() + .filter(e -> e.getLevel().levelStr.equals(level.name())) + .anyMatch(e -> e.getFormattedMessage().equals(message)); + } + + public boolean hasLog(String message) { + return TestLogbackAppender.events.stream() + .anyMatch(e -> e.getFormattedMessage().equals(message)); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/TestLogbackAppender.java b/server/sonar-process/src/test/java/org/sonar/process/TestLogbackAppender.java new file mode 100644 index 00000000000..b38e23ca685 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/TestLogbackAppender.java @@ -0,0 +1,35 @@ +/* + * 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; + +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.AppenderBase; +import java.util.ArrayList; +import java.util.List; + +public class TestLogbackAppender extends AppenderBase { + static List events = new ArrayList<>(); + + @Override + protected void append(LoggingEvent e) { + events.add(e); + } + +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/NodeTypeTest.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/NodeTypeTest.java new file mode 100644 index 00000000000..c21a06f8c10 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/cluster/NodeTypeTest.java @@ -0,0 +1,47 @@ +/* + * 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.cluster; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NodeTypeTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void test_parse() { + assertThat(NodeType.parse("application")).isEqualTo(NodeType.APPLICATION); + assertThat(NodeType.parse("search")).isEqualTo(NodeType.SEARCH); + } + + @Test + public void parse_an_unknown_value_must_throw_IAE() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Invalid value: XYZ"); + + NodeType.parse("XYZ"); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/health/HealthStateRefresherTest.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/health/HealthStateRefresherTest.java new file mode 100644 index 00000000000..61baa7d9a2a --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/cluster/health/HealthStateRefresherTest.java @@ -0,0 +1,90 @@ +/* + * 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.cluster.health; + +import java.util.Random; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; + +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Matchers.eq; +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 HealthStateRefresherTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private Random random = new Random(); + private NodeDetailsTestSupport testSupport = new NodeDetailsTestSupport(random); + + private HealthStateRefresherExecutorService executorService = mock(HealthStateRefresherExecutorService.class); + private NodeHealthProvider nodeHealthProvider = mock(NodeHealthProvider.class); + private SharedHealthState sharedHealthState = mock(SharedHealthState.class); + private HealthStateRefresher underTest = new HealthStateRefresher(executorService, nodeHealthProvider, sharedHealthState); + + @Test + public void start_adds_runnable_with_10_second_delay_and_initial_delay_putting_NodeHealth_from_provider_into_SharedHealthState() { + ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); + NodeHealth[] nodeHealths = { + testSupport.randomNodeHealth(), + testSupport.randomNodeHealth(), + testSupport.randomNodeHealth() + }; + Error expected = new Error("Simulating exception raised by NodeHealthProvider"); + when(nodeHealthProvider.get()) + .thenReturn(nodeHealths[0]) + .thenReturn(nodeHealths[1]) + .thenReturn(nodeHealths[2]) + .thenThrow(expected); + + underTest.start(); + + verify(executorService).scheduleWithFixedDelay(runnableCaptor.capture(), eq(1L), eq(10L), eq(TimeUnit.SECONDS)); + + Runnable runnable = runnableCaptor.getValue(); + runnable.run(); + runnable.run(); + runnable.run(); + + verify(sharedHealthState).writeMine(nodeHealths[0]); + verify(sharedHealthState).writeMine(nodeHealths[1]); + verify(sharedHealthState).writeMine(nodeHealths[2]); + + try { + runnable.run(); + } catch (IllegalStateException e) { + fail("Runnable should catch any Throwable"); + } + } + + @Test + public void stop_has_no_effect() { + underTest.stop(); + + verify(sharedHealthState).clearMine(); + verifyZeroInteractions(executorService, nodeHealthProvider); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/health/NodeDetailsTest.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/health/NodeDetailsTest.java new file mode 100644 index 00000000000..9e15e2bc8d1 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/cluster/health/NodeDetailsTest.java @@ -0,0 +1,247 @@ +/* + * 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.cluster.health; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.Random; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.process.cluster.health.NodeDetails.newNodeDetailsBuilder; + +public class NodeDetailsTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private Random random = new Random(); + private NodeDetailsTestSupport testSupport = new NodeDetailsTestSupport(random); + private NodeDetails.Type randomType = testSupport.randomType(); + private NodeDetails.Builder builderUnderTest = newNodeDetailsBuilder(); + + @Test + public void setType_throws_NPE_if_arg_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("type can't be null"); + + builderUnderTest.setType(null); + } + + @Test + public void setName_throws_NPE_if_arg_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("name can't be null"); + + builderUnderTest.setName(null); + } + + @Test + public void setName_throws_IAE_if_arg_is_empty() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("name can't be empty"); + + builderUnderTest.setName(""); + } + + @Test + public void setName_throws_IAE_if_arg_is_empty_after_trim() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("name can't be empty"); + + builderUnderTest.setName(" "); + } + + @Test + public void setHost_throws_NPE_if_arg_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("host can't be null"); + + builderUnderTest.setHost(null); + } + + @Test + public void setHost_throws_IAE_if_arg_is_empty() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("host can't be empty"); + + builderUnderTest.setHost(""); + } + + @Test + public void setHost_throws_IAE_if_arg_is_empty_after_trim() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("host can't be empty"); + + builderUnderTest.setHost(" "); + } + + @Test + public void setPort_throws_IAE_if_arg_is_less_than_1() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("port must be > 0"); + + builderUnderTest.setPort(-random.nextInt(5)); + } + + @Test + public void setStarted_throws_IAE_if_arg_is_less_than_1() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("startedAt must be > 0"); + + builderUnderTest.setStartedAt(-random.nextInt(5)); + } + + @Test + public void build_throws_NPE_if_type_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("type can't be null"); + + builderUnderTest.build(); + } + + @Test + public void build_throws_NPE_if_name_is_null() { + builderUnderTest + .setType(randomType); + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("name can't be null"); + + builderUnderTest.build(); + } + + @Test + public void build_throws_NPE_if_host_is_null() { + builderUnderTest + .setType(randomType) + .setName(randomAlphanumeric(2)); + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("host can't be null"); + + builderUnderTest.build(); + } + + @Test + public void build_throws_IAE_if_setPort_not_called() { + builderUnderTest + .setType(randomType) + .setName(randomAlphanumeric(2)) + .setHost(randomAlphanumeric(3)); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("port must be > 0"); + + builderUnderTest.build(); + } + + @Test + public void build_throws_IAE_if_setStarted_not_called() { + builderUnderTest + .setType(randomType) + .setName(randomAlphanumeric(2)) + .setHost(randomAlphanumeric(3)) + .setPort(1 + random.nextInt(33)); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("startedAt must be > 0"); + + builderUnderTest.build(); + } + + @Test + public void equals_is_based_on_content() { + NodeDetails.Builder builder = testSupport.randomNodeDetailsBuilder(); + + NodeDetails underTest = builder.build(); + + assertThat(underTest).isEqualTo(underTest); + assertThat(builder.build()) + .isEqualTo(underTest) + .isNotSameAs(underTest); + assertThat(underTest).isNotEqualTo(null); + assertThat(underTest).isNotEqualTo(new Object()); + } + + @Test + public void hashcode_is_based_on_content() { + NodeDetails.Builder builder = testSupport.randomNodeDetailsBuilder(); + + NodeDetails underTest = builder.build(); + + assertThat(builder.build().hashCode()) + .isEqualTo(underTest.hashCode()); + } + + @Test + public void NodeDetails_is_Externalizable() throws IOException, ClassNotFoundException { + NodeDetails source = testSupport.randomNodeDetails(); + byte[] byteArray = testSupport.serialize(source); + + NodeDetails underTest = (NodeDetails) new ObjectInputStream(new ByteArrayInputStream(byteArray)).readObject(); + + assertThat(underTest).isEqualTo(source); + } + + @Test + public void verify_toString() { + String name = randomAlphanumeric(3); + String host = randomAlphanumeric(10); + int port = 1 + random.nextInt(10); + long startedAt = 1 + random.nextInt(666); + + NodeDetails underTest = builderUnderTest + .setType(randomType) + .setName(name) + .setHost(host) + .setPort(port) + .setStartedAt(startedAt) + .build(); + + assertThat(underTest.toString()) + .isEqualTo("NodeDetails{type=" + randomType + ", name='" + name + "', host='" + host + "', port=" + port + ", startedAt=" + startedAt + "}"); + } + + @Test + public void verify_getters() { + String name = randomAlphanumeric(3); + String host = randomAlphanumeric(10); + int port = 1 + random.nextInt(10); + long startedAt = 1 + random.nextInt(666); + + NodeDetails underTest = builderUnderTest + .setType(randomType) + .setName(name) + .setHost(host) + .setPort(port) + .setStartedAt(startedAt) + .build(); + + assertThat(underTest.getType()).isEqualTo(randomType); + assertThat(underTest.getName()).isEqualTo(name); + assertThat(underTest.getHost()).isEqualTo(host); + assertThat(underTest.getPort()).isEqualTo(port); + assertThat(underTest.getStartedAt()).isEqualTo(startedAt); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/health/NodeDetailsTestSupport.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/health/NodeDetailsTestSupport.java new file mode 100644 index 00000000000..b0448ef832b --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/cluster/health/NodeDetailsTestSupport.java @@ -0,0 +1,88 @@ +/* + * 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.cluster.health; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.util.Random; +import java.util.stream.IntStream; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.sonar.process.cluster.health.NodeDetails.newNodeDetailsBuilder; +import static org.sonar.process.cluster.health.NodeHealth.newNodeHealthBuilder; + +public class NodeDetailsTestSupport { + private final Random random; + + public NodeDetailsTestSupport() { + this(new Random()); + } + + NodeDetailsTestSupport(Random random) { + this.random = random; + } + + NodeHealth.Status randomStatus() { + return NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)]; + } + + NodeHealth randomNodeHealth() { + return randomBuilder().build(); + } + + NodeHealth.Builder randomBuilder() { + return randomBuilder(0); + } + + NodeHealth.Builder randomBuilder(int minCauseCount) { + NodeHealth.Builder builder = newNodeHealthBuilder() + .setStatus(randomStatus()) + .setDetails(randomNodeDetails()); + IntStream.range(0, minCauseCount + random.nextInt(2)).mapToObj(i -> randomAlphanumeric(4)).forEach(builder::addCause); + return builder; + } + + NodeDetails randomNodeDetails() { + return randomNodeDetailsBuilder() + .build(); + } + + NodeDetails.Builder randomNodeDetailsBuilder() { + return newNodeDetailsBuilder() + .setType(randomType()) + .setName(randomAlphanumeric(3)) + .setHost(randomAlphanumeric(10)) + .setPort(1 + random.nextInt(10)) + .setStartedAt(1 + random.nextInt(666)); + } + + NodeDetails.Type randomType() { + return NodeDetails.Type.values()[random.nextInt(NodeDetails.Type.values().length)]; + } + + static byte[] serialize(Object source) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(out)) { + objectOutputStream.writeObject(source); + } + return out.toByteArray(); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/health/NodeHealthTest.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/health/NodeHealthTest.java new file mode 100644 index 00000000000..2652d6d7519 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/cluster/health/NodeHealthTest.java @@ -0,0 +1,195 @@ +/* + * 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.cluster.health; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.Arrays; +import java.util.Random; +import java.util.stream.IntStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.process.cluster.health.NodeHealth.newNodeHealthBuilder; + +public class NodeHealthTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private Random random = new Random(); + private NodeDetailsTestSupport testSupport = new NodeDetailsTestSupport(random); + private NodeHealth.Status randomStatus = testSupport.randomStatus(); + private NodeHealth.Builder builderUnderTest = newNodeHealthBuilder(); + + @Test + public void setStatus_throws_NPE_if_arg_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("status can't be null"); + + builderUnderTest.setStatus(null); + } + + @Test + public void setDetails_throws_NPE_if_arg_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("details can't be null"); + + builderUnderTest.setDetails(null); + } + + @Test + public void build_throws_NPE_if_status_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("status can't be null"); + + builderUnderTest.build(); + } + + @Test + public void build_throws_NPE_if_details_is_null() { + builderUnderTest.setStatus(randomStatus); + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("details can't be null"); + + builderUnderTest.build(); + } + + @Test + public void clearClauses_clears_clauses_of_builder() { + NodeHealth.Builder underTest = testSupport.randomBuilder(); + NodeHealth original = underTest + .addCause(randomAlphanumeric(3)) + .build(); + + underTest.clearCauses(); + + NodeHealth second = underTest.build(); + assertThat(second.getStatus()).isEqualTo(original.getStatus()); + assertThat(second.getDetails()).isEqualTo(original.getDetails()); + assertThat(second.getCauses()).isEmpty(); + } + + @Test + public void builder_can_be_reused() { + NodeHealth.Builder builder = testSupport.randomBuilder(1); + NodeHealth original = builder.build(); + NodeHealth second = builder.build(); + + NodeHealth.Status newRandomStatus = NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)]; + NodeDetails newNodeDetails = testSupport.randomNodeDetails(); + builder + .clearCauses() + .setStatus(newRandomStatus) + .setDetails(newNodeDetails); + String[] newCauses = IntStream.range(0, 1 + random.nextInt(2)).mapToObj(i -> randomAlphanumeric(4)).toArray(String[]::new); + Arrays.stream(newCauses).forEach(builder::addCause); + + NodeHealth newNodeHealth = builder.build(); + + assertThat(second).isEqualTo(original); + assertThat(newNodeHealth.getStatus()).isEqualTo(newRandomStatus); + assertThat(newNodeHealth.getDetails()).isEqualTo(newNodeDetails); + assertThat(newNodeHealth.getCauses()).containsOnly(newCauses); + } + + @Test + public void equals_is_based_on_content() { + NodeHealth.Builder builder = testSupport.randomBuilder(); + + NodeHealth underTest = builder.build(); + + assertThat(underTest).isEqualTo(underTest); + assertThat(builder.build()) + .isEqualTo(underTest) + .isNotSameAs(underTest); + assertThat(underTest).isNotEqualTo(null); + assertThat(underTest).isNotEqualTo(new Object()); + } + + @Test + public void hashcode_is_based_on_content() { + NodeHealth.Builder builder = testSupport.randomBuilder(); + + NodeHealth underTest = builder.build(); + + assertThat(builder.build().hashCode()) + .isEqualTo(underTest.hashCode()); + } + + @Test + public void class_is_serializable_with_causes() throws IOException, ClassNotFoundException { + NodeHealth source = testSupport.randomBuilder(1).build(); + byte[] bytes = testSupport.serialize(source); + + NodeHealth underTest = (NodeHealth) new ObjectInputStream(new ByteArrayInputStream(bytes)).readObject(); + + assertThat(underTest).isEqualTo(source); + } + + @Test + public void class_is_serializable_without_causes() throws IOException, ClassNotFoundException { + NodeHealth.Builder builder = newNodeHealthBuilder() + .setStatus(randomStatus) + .setDetails(testSupport.randomNodeDetails()); + NodeHealth source = builder.build(); + byte[] bytes = testSupport.serialize(source); + + NodeHealth underTest = (NodeHealth) new ObjectInputStream(new ByteArrayInputStream(bytes)).readObject(); + + assertThat(underTest).isEqualTo(source); + } + + @Test + public void verify_toString() { + NodeDetails nodeDetails = testSupport.randomNodeDetails(); + String cause = randomAlphanumeric(4); + NodeHealth.Builder builder = builderUnderTest + .setStatus(randomStatus) + .setDetails(nodeDetails) + .addCause(cause); + + NodeHealth underTest = builder.build(); + + assertThat(underTest.toString()) + .isEqualTo("NodeHealth{status=" + randomStatus + ", causes=[" + cause + "], details=" + nodeDetails + "}"); + } + + @Test + public void verify_getters() { + NodeDetails nodeDetails = testSupport.randomNodeDetails(); + NodeHealth.Builder builder = builderUnderTest + .setStatus(randomStatus) + .setDetails(nodeDetails); + String[] causes = IntStream.range(0, random.nextInt(10)).mapToObj(i -> randomAlphanumeric(4)).toArray(String[]::new); + Arrays.stream(causes).forEach(builder::addCause); + + NodeHealth underTest = builder.build(); + + assertThat(underTest.getStatus()).isEqualTo(randomStatus); + assertThat(underTest.getDetails()).isEqualTo(nodeDetails); + assertThat(underTest.getCauses()).containsOnly(causes); + } + +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/health/SharedHealthStateImplTest.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/health/SharedHealthStateImplTest.java new file mode 100644 index 00000000000..0ce08ea9208 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/cluster/health/SharedHealthStateImplTest.java @@ -0,0 +1,251 @@ +/* + * 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.cluster.health; + +import com.google.common.collect.ImmutableSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.stream.IntStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.slf4j.event.Level; +import org.sonar.process.LoggingRule; +import org.sonar.process.cluster.hz.HazelcastMember; + +import static java.util.Collections.singleton; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.sonar.process.cluster.health.NodeDetails.newNodeDetailsBuilder; +import static org.sonar.process.cluster.health.NodeHealth.newNodeHealthBuilder; + +public class SharedHealthStateImplTest { + private static final String MAP_SQ_HEALTH_STATE = "sq_health_state"; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public LoggingRule logging = new LoggingRule(SharedHealthStateImpl.class); + + private final Random random = new Random(); + private long clusterTime = 99 + Math.abs(random.nextInt(9621)); + private HazelcastMember hazelcastMember = mock(HazelcastMember.class); + private SharedHealthStateImpl underTest = new SharedHealthStateImpl(hazelcastMember); + + @Test + public void write_fails_with_NPE_if_arg_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("nodeHealth can't be null"); + + underTest.writeMine(null); + } + + @Test + public void write_put_arg_into_map_sq_health_state_under_current_client_uuid() { + NodeHealth nodeHealth = randomNodeHealth(); + Map map = new HashMap<>(); + doReturn(map).when(hazelcastMember).getReplicatedMap(MAP_SQ_HEALTH_STATE); + long clusterTime = random.nextLong(); + String uuid = randomAlphanumeric(5); + when(hazelcastMember.getUuid()).thenReturn(uuid); + when(hazelcastMember.getClusterTime()).thenReturn(clusterTime); + + underTest.writeMine(nodeHealth); + + assertThat(map.size()).isEqualTo(1); + assertThat(map.get(uuid)).isEqualTo(new TimestampedNodeHealth(nodeHealth, clusterTime)); + assertThat(logging.getLogs()).isEmpty(); + } + + @Test + public void write_logs_map_sq_health_state_content_and_NodeHealth_to_be_added_if_TRACE() { + logging.setLevel(Level.TRACE); + NodeHealth newNodeHealth = randomNodeHealth(); + Map map = new HashMap<>(); + map.put(randomAlphanumeric(4), new TimestampedNodeHealth(randomNodeHealth(), random.nextLong())); + doReturn(new HashMap<>(map)).when(hazelcastMember).getReplicatedMap(MAP_SQ_HEALTH_STATE); + String uuid = randomAlphanumeric(5); + when(hazelcastMember.getUuid()).thenReturn(uuid); + + underTest.writeMine(newNodeHealth); + + assertThat(logging.getLogs()).hasSize(1); + assertThat(logging.hasLog(Level.TRACE, "Reading " + map + " and adding " + newNodeHealth)).isTrue(); + } + + @Test + public void readAll_returns_all_NodeHealth_in_map_sq_health_state_for_existing_client_uuids_aged_less_than_30_seconds() { + NodeHealth[] nodeHealths = IntStream.range(0, 1 + random.nextInt(6)).mapToObj(i -> randomNodeHealth()).toArray(NodeHealth[]::new); + Map allNodeHealths = new HashMap<>(); + Map expected = new HashMap<>(); + String randomUuidBase = randomAlphanumeric(5); + for (int i = 0; i < nodeHealths.length; i++) { + String memberUuid = randomUuidBase + i; + TimestampedNodeHealth timestampedNodeHealth = new TimestampedNodeHealth(nodeHealths[i], clusterTime - random.nextInt(30 * 1000)); + allNodeHealths.put(memberUuid, timestampedNodeHealth); + if (random.nextBoolean()) { + expected.put(memberUuid, nodeHealths[i]); + } + } + doReturn(allNodeHealths).when(hazelcastMember).getReplicatedMap(MAP_SQ_HEALTH_STATE); + when(hazelcastMember.getMemberUuids()).thenReturn(expected.keySet()); + when(hazelcastMember.getClusterTime()).thenReturn(clusterTime); + + assertThat(underTest.readAll()) + .containsOnly(expected.values().stream().toArray(NodeHealth[]::new)); + assertThat(logging.getLogs()).isEmpty(); + } + + @Test + public void readAll_ignores_NodeHealth_of_30_seconds_before_cluster_time() { + NodeHealth nodeHealth = randomNodeHealth(); + Map map = new HashMap<>(); + String memberUuid = randomAlphanumeric(5); + TimestampedNodeHealth timestampedNodeHealth = new TimestampedNodeHealth(nodeHealth, clusterTime - 30 * 1000); + map.put(memberUuid, timestampedNodeHealth); + doReturn(map).when(hazelcastMember).getReplicatedMap(MAP_SQ_HEALTH_STATE); + when(hazelcastMember.getMemberUuids()).thenReturn(map.keySet()); + when(hazelcastMember.getClusterTime()).thenReturn(clusterTime); + + assertThat(underTest.readAll()).isEmpty(); + } + + @Test + public void readAll_ignores_NodeHealth_of_more_than_30_seconds_before_cluster_time() { + NodeHealth nodeHealth = randomNodeHealth(); + Map map = new HashMap<>(); + String memberUuid = randomAlphanumeric(5); + TimestampedNodeHealth timestampedNodeHealth = new TimestampedNodeHealth(nodeHealth, clusterTime - 30 * 1000 - random.nextInt(99)); + map.put(memberUuid, timestampedNodeHealth); + doReturn(map).when(hazelcastMember).getReplicatedMap(MAP_SQ_HEALTH_STATE); + when(hazelcastMember.getMemberUuids()).thenReturn(map.keySet()); + when(hazelcastMember.getClusterTime()).thenReturn(clusterTime); + + assertThat(underTest.readAll()).isEmpty(); + } + + @Test + public void readAll_logs_map_sq_health_state_content_and_the_content_effectively_returned_if_TRACE() { + logging.setLevel(Level.TRACE); + Map map = new HashMap<>(); + String uuid = randomAlphanumeric(44); + NodeHealth nodeHealth = randomNodeHealth(); + map.put(uuid, new TimestampedNodeHealth(nodeHealth, clusterTime - 1)); + when(hazelcastMember.getClusterTime()).thenReturn(clusterTime); + when(hazelcastMember.getMemberUuids()).thenReturn(singleton(uuid)); + doReturn(map).when(hazelcastMember).getReplicatedMap(MAP_SQ_HEALTH_STATE); + + underTest.readAll(); + + assertThat(logging.getLogs()).hasSize(1); + assertThat(logging.hasLog(Level.TRACE, "Reading " + new HashMap<>(map) + " and keeping " + singleton(nodeHealth))).isTrue(); + } + + @Test + public void readAll_logs_message_for_each_non_existing_member_ignored_if_TRACE() { + logging.setLevel(Level.TRACE); + Map map = new HashMap<>(); + String memberUuid1 = randomAlphanumeric(44); + String memberUuid2 = randomAlphanumeric(44); + map.put(memberUuid1, new TimestampedNodeHealth(randomNodeHealth(), clusterTime - 1)); + map.put(memberUuid2, new TimestampedNodeHealth(randomNodeHealth(), clusterTime - 1)); + when(hazelcastMember.getClusterTime()).thenReturn(clusterTime); + doReturn(map).when(hazelcastMember).getReplicatedMap(MAP_SQ_HEALTH_STATE); + + underTest.readAll(); + + assertThat(logging.getLogs()).hasSize(3); + assertThat(logging.getLogs(Level.TRACE)) + .containsOnly( + "Reading " + new HashMap<>(map) + " and keeping []", + "Ignoring NodeHealth of member " + memberUuid1 + " because it is not part of the cluster at the moment", + "Ignoring NodeHealth of member " + memberUuid2 + " because it is not part of the cluster at the moment"); + } + + @Test + public void readAll_logs_message_for_each_timed_out_NodeHealth_ignored_if_TRACE() { + logging.setLevel(Level.TRACE); + Map map = new HashMap<>(); + String memberUuid1 = randomAlphanumeric(44); + String memberUuid2 = randomAlphanumeric(44); + map.put(memberUuid1, new TimestampedNodeHealth(randomNodeHealth(), clusterTime - 30 * 1000)); + map.put(memberUuid2, new TimestampedNodeHealth(randomNodeHealth(), clusterTime - 30 * 1000)); + doReturn(map).when(hazelcastMember).getReplicatedMap(MAP_SQ_HEALTH_STATE); + when(hazelcastMember.getMemberUuids()).thenReturn(ImmutableSet.of(memberUuid1, memberUuid2)); + when(hazelcastMember.getClusterTime()).thenReturn(clusterTime); + + underTest.readAll(); + + assertThat(logging.getLogs()).hasSize(3); + assertThat(logging.getLogs(Level.TRACE)) + .containsOnly( + "Reading " + new HashMap<>(map) + " and keeping []", + "Ignoring NodeHealth of member " + memberUuid1 + " because it is too old", + "Ignoring NodeHealth of member " + memberUuid2 + " because it is too old"); + } + + @Test + public void clearMine_clears_entry_into_map_sq_health_state_under_current_client_uuid() { + Map map = mock(Map.class); + doReturn(map).when(hazelcastMember).getReplicatedMap(MAP_SQ_HEALTH_STATE); + String uuid = randomAlphanumeric(5); + when(hazelcastMember.getUuid()).thenReturn(uuid); + + underTest.clearMine(); + + verify(map).remove(uuid); + verifyNoMoreInteractions(map); + assertThat(logging.getLogs()).isEmpty(); + } + + @Test + public void clearMine_logs_map_sq_health_state_and_current_client_uuid_if_TRACE() { + logging.setLevel(Level.TRACE); + Map map = new HashMap<>(); + map.put(randomAlphanumeric(4), new TimestampedNodeHealth(randomNodeHealth(), random.nextLong())); + doReturn(map).when(hazelcastMember).getReplicatedMap(MAP_SQ_HEALTH_STATE); + String uuid = randomAlphanumeric(5); + when(hazelcastMember.getUuid()).thenReturn(uuid); + + underTest.clearMine(); + + assertThat(logging.getLogs()).hasSize(1); + assertThat(logging.hasLog(Level.TRACE, "Reading " + map + " and clearing for " + uuid)).isTrue(); + } + + private NodeHealth randomNodeHealth() { + return newNodeHealthBuilder() + .setStatus(NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)]) + .setDetails(newNodeDetailsBuilder() + .setType(random.nextBoolean() ? NodeDetails.Type.SEARCH : NodeDetails.Type.APPLICATION) + .setName(randomAlphanumeric(30)) + .setHost(randomAlphanumeric(10)) + .setPort(1 + random.nextInt(666)) + .setStartedAt(1 + random.nextInt(852)) + .build()) + .build(); + } +} diff --git a/server/sonar-process/src/test/resources/org/sonar/process/logback-test.xml b/server/sonar-process/src/test/resources/org/sonar/process/logback-test.xml new file mode 100644 index 00000000000..72521000bab --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/logback-test.xml @@ -0,0 +1,20 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level - %msg%n + + + + + + %-5level %msg%n + + + + + + + + -- 2.39.5