--- /dev/null
+/*
+ * 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<String> getLogs() {
+ return TestLogbackAppender.events.stream()
+ .map(LoggingEvent::getFormattedMessage)
+ .collect(Collectors.toList());
+ }
+
+ public List<String> 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));
+ }
+}
--- /dev/null
+/*
+ * 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<LoggingEvent> {
+ static List<LoggingEvent> events = new ArrayList<>();
+
+ @Override
+ protected void append(LoggingEvent e) {
+ events.add(e);
+ }
+
+}
--- /dev/null
+/*
+ * 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");
+ }
+}
--- /dev/null
+/*
+ * 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<Runnable> 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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<String, TimestampedNodeHealth> 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<String, TimestampedNodeHealth> 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<String, TimestampedNodeHealth> allNodeHealths = new HashMap<>();
+ Map<String, NodeHealth> 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<String, TimestampedNodeHealth> 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<String, TimestampedNodeHealth> 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<String, TimestampedNodeHealth> 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<String, TimestampedNodeHealth> 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<String, TimestampedNodeHealth> 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<String, TimestampedNodeHealth> 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<String, TimestampedNodeHealth> 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();
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration debug="true">
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} %-5level - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <appender name="TESTING" class="org.sonar.process.TestLogbackAppender">
+ <encoder>
+ <pattern>%-5level %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <root level="INFO">
+ <appender-ref ref="STDOUD" />
+ <appender-ref ref="TESTING" />
+ </root>
+</configuration>