From 1d2e97d886115e21216c9c060c09179cd2be0938 Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Mon, 10 Sep 2018 10:06:02 +0200 Subject: [PATCH] SONAR-11238 tasks can persist messages --- server/sonar-ce-task/build.gradle | 1 + .../org/sonar/ce/task/log/CeTaskMessages.java | 92 +++++++++++ .../sonar/ce/task/log/CeTaskMessagesImpl.java | 83 ++++++++++ .../ce/task/log/CeTaskMessagesImplTest.java | 155 ++++++++++++++++++ .../task/log/CeTaskMessagesMessageTest.java | 94 +++++++++++ 5 files changed, 425 insertions(+) create mode 100644 server/sonar-ce-task/src/main/java/org/sonar/ce/task/log/CeTaskMessages.java create mode 100644 server/sonar-ce-task/src/main/java/org/sonar/ce/task/log/CeTaskMessagesImpl.java create mode 100644 server/sonar-ce-task/src/test/java/org/sonar/ce/task/log/CeTaskMessagesImplTest.java create mode 100644 server/sonar-ce-task/src/test/java/org/sonar/ce/task/log/CeTaskMessagesMessageTest.java diff --git a/server/sonar-ce-task/build.gradle b/server/sonar-ce-task/build.gradle index 0fc3a96a321..109a4ca3307 100644 --- a/server/sonar-ce-task/build.gradle +++ b/server/sonar-ce-task/build.gradle @@ -51,6 +51,7 @@ dependencies { testCompile 'org.assertj:assertj-guava' testCompile 'org.mockito:mockito-core' testCompile 'org.reflections:reflections' + testCompile project(':server:sonar-db-testing') } task testJar(type: Jar) { diff --git a/server/sonar-ce-task/src/main/java/org/sonar/ce/task/log/CeTaskMessages.java b/server/sonar-ce-task/src/main/java/org/sonar/ce/task/log/CeTaskMessages.java new file mode 100644 index 00000000000..f3cdf40f500 --- /dev/null +++ b/server/sonar-ce-task/src/main/java/org/sonar/ce/task/log/CeTaskMessages.java @@ -0,0 +1,92 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.ce.task.log; + +import java.util.Collection; +import java.util.Objects; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.ce.ComputeEngineSide; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Provides the ability to record message attached to the current task. + */ +@ComputeEngineSide +public interface CeTaskMessages { + + /** + * Add a single message + */ + void add(Message message); + + /** + * Add multiple messages. Use this method over {@link #add(Message)} if you have more than one message to add, you'll + * allow implementation to batch persistence if possible. + */ + void addAll(Collection messages); + + @Immutable + class Message { + private final String text; + private final long timestamp; + + public Message(String text, long timestamp) { + checkArgument(text != null && !text.isEmpty(), "Text can't be null nor empty"); + checkArgument(timestamp >= 0, "Text can't be less than 0"); + this.text = text; + this.timestamp = timestamp; + } + + public String getText() { + return text; + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Message message1 = (Message) o; + return timestamp == message1.timestamp && + Objects.equals(text, message1.text); + } + + @Override + public int hashCode() { + return Objects.hash(text, timestamp); + } + + @Override + public String toString() { + return "Message{" + + "text='" + text + '\'' + + ", timestamp=" + timestamp + + '}'; + } + } +} diff --git a/server/sonar-ce-task/src/main/java/org/sonar/ce/task/log/CeTaskMessagesImpl.java b/server/sonar-ce-task/src/main/java/org/sonar/ce/task/log/CeTaskMessagesImpl.java new file mode 100644 index 00000000000..8d70febb7e1 --- /dev/null +++ b/server/sonar-ce-task/src/main/java/org/sonar/ce/task/log/CeTaskMessagesImpl.java @@ -0,0 +1,83 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.ce.task.log; + +import java.util.Collection; +import org.sonar.ce.task.CeTask; +import org.sonar.core.util.UuidFactory; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.ce.CeTaskMessageDto; + +import static java.util.Objects.requireNonNull; + +/** + * Implementation of {@link CeTaskMessages} to made available into a task's container. + *

+ * Messages are persisted as the are recorded. + */ +public class CeTaskMessagesImpl implements CeTaskMessages { + private final DbClient dbClient; + private final UuidFactory uuidFactory; + private final CeTask ceTask; + + public CeTaskMessagesImpl(DbClient dbClient, UuidFactory uuidFactory, CeTask ceTask) { + this.dbClient = dbClient; + this.uuidFactory = uuidFactory; + this.ceTask = ceTask; + } + + @Override + public void add(Message message) { + checkMessage(message); + + try (DbSession dbSession = dbClient.openSession(false)) { + insert(dbSession, message); + dbSession.commit(); + } + } + + @Override + public void addAll(Collection messages) { + if (messages.isEmpty()) { + return; + } + + messages.forEach(CeTaskMessagesImpl::checkMessage); + + // TODO: commit every X messages? + try (DbSession dbSession = dbClient.openSession(true)) { + messages.forEach(message -> insert(dbSession, message)); + dbSession.commit(); + } + } + + public void insert(DbSession dbSession, Message message) { + dbClient.ceTaskMessageDao().insert(dbSession, new CeTaskMessageDto() + .setUuid(uuidFactory.create()) + .setTaskUuid(ceTask.getUuid()) + .setMessage(message.getText()) + .setCreatedAt(message.getTimestamp())); + } + + private static void checkMessage(Message message) { + requireNonNull(message, "message can't be null"); + } +} diff --git a/server/sonar-ce-task/src/test/java/org/sonar/ce/task/log/CeTaskMessagesImplTest.java b/server/sonar-ce-task/src/test/java/org/sonar/ce/task/log/CeTaskMessagesImplTest.java new file mode 100644 index 00000000000..c16e035bfe7 --- /dev/null +++ b/server/sonar-ce-task/src/test/java/org/sonar/ce/task/log/CeTaskMessagesImplTest.java @@ -0,0 +1,155 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.ce.task.log; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.sonar.api.utils.System2; +import org.sonar.ce.task.CeTask; +import org.sonar.core.util.UuidFactory; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; + +import static java.util.stream.Collectors.toList; +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +public class CeTaskMessagesImplTest { + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private DbClient dbClient = dbTester.getDbClient(); + private UuidFactory uuidFactory = mock(UuidFactory.class); + private String taskUuid = randomAlphabetic(12); + + private CeTask ceTask = new CeTask.Builder() + .setUuid(taskUuid) + .setOrganizationUuid(randomAlphabetic(10)) + .setType(randomAlphabetic(5)) + .build(); + + private CeTaskMessagesImpl underTest = new CeTaskMessagesImpl(dbClient, uuidFactory, ceTask); + + @Test + public void add_fails_with_NPE_if_arg_is_null() { + expectMessageCantBeNullNPE(); + + underTest.add(null); + } + + @Test + public void add_persist_message_to_DB() { + CeTaskMessages.Message message = new CeTaskMessages.Message(randomAlphabetic(20), 2_999L); + String uuid = randomAlphanumeric(40); + when(uuidFactory.create()).thenReturn(uuid); + + underTest.add(message); + + assertThat(dbTester.select("select uuid as \"UUID\", task_uuid as \"TASK_UUID\", message as \"MESSAGE\", created_at as \"CREATED_AT\" from ce_task_message")) + .extracting(t -> t.get("UUID"), t -> t.get("TASK_UUID"), t -> t.get("MESSAGE"), t -> t.get("CREATED_AT")) + .containsOnly(tuple(uuid, taskUuid, message.getText(), message.getTimestamp())); + } + + @Test + public void addAll_fails_with_NPE_if_arg_is_null() { + expectedException.expect(NullPointerException.class); + + underTest.addAll(null); + } + + @Test + public void addAll_fails_with_NPE_if_any_message_in_list_is_null() { + Random random = new Random(); + List messages = Stream.of( + // some (or none) non null Message before null one + IntStream.range(0, random.nextInt(5)).mapToObj(i -> new CeTaskMessages.Message(randomAlphabetic(3) + "_i", 1_999L + i)), + Stream.of((CeTaskMessages.Message) null), + // some (or none) non null Message after null one + IntStream.range(0, random.nextInt(5)).mapToObj(i -> new CeTaskMessages.Message(randomAlphabetic(3) + "_i", 1_999L + i))) + .flatMap(t -> t) + .collect(toList()); + + expectMessageCantBeNullNPE(); + + underTest.addAll(messages); + } + + @Test + public void addAll_has_no_effect_if_arg_is_empty() { + DbClient dbClientMock = mock(DbClient.class); + UuidFactory uuidFactoryMock = mock(UuidFactory.class); + CeTask ceTaskMock = mock(CeTask.class); + CeTaskMessagesImpl underTest = new CeTaskMessagesImpl(dbClientMock, uuidFactoryMock, ceTaskMock); + + underTest.addAll(Collections.emptyList()); + + verifyZeroInteractions(dbClientMock, uuidFactoryMock, ceTaskMock); + } + + @Test + public void addAll_persists_all_messages_to_DB() { + int messageCount = 5; + String[] uuids = IntStream.range(0, messageCount).mapToObj(i -> "UUId_" + i).toArray(String[]::new); + CeTaskMessages.Message[] messages = IntStream.range(0, messageCount) + .mapToObj(i -> new CeTaskMessages.Message("message_" + i, 2_999L + i)) + .toArray(CeTaskMessages.Message[]::new); + when(uuidFactory.create()).thenAnswer(new Answer() { + int i = 0; + + @Override + public String answer(InvocationOnMock invocation) { + return uuids[i++]; + } + }); + + underTest.addAll(Arrays.stream(messages).collect(Collectors.toList())); + + assertThat(dbTester.select("select uuid as \"UUID\", task_uuid as \"TASK_UUID\", message as \"MESSAGE\", created_at as \"CREATED_AT\" from ce_task_message")) + .extracting(t -> t.get("UUID"), t -> t.get("TASK_UUID"), t -> t.get("MESSAGE"), t -> t.get("CREATED_AT")) + .containsOnly( + tuple(uuids[0], taskUuid, messages[0].getText(), messages[0].getTimestamp()), + tuple(uuids[1], taskUuid, messages[1].getText(), messages[1].getTimestamp()), + tuple(uuids[2], taskUuid, messages[2].getText(), messages[2].getTimestamp()), + tuple(uuids[3], taskUuid, messages[3].getText(), messages[3].getTimestamp()), + tuple(uuids[4], taskUuid, messages[4].getText(), messages[4].getTimestamp())); + } + + private void expectMessageCantBeNullNPE() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("message can't be null"); + } +} diff --git a/server/sonar-ce-task/src/test/java/org/sonar/ce/task/log/CeTaskMessagesMessageTest.java b/server/sonar-ce-task/src/test/java/org/sonar/ce/task/log/CeTaskMessagesMessageTest.java new file mode 100644 index 00000000000..545c27959f8 --- /dev/null +++ b/server/sonar-ce-task/src/test/java/org/sonar/ce/task/log/CeTaskMessagesMessageTest.java @@ -0,0 +1,94 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.ce.task.log; + +import java.util.Random; +import java.util.stream.LongStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.ce.task.log.CeTaskMessages.Message; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.assertj.core.api.Assertions.assertThat; + +public class CeTaskMessagesMessageTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void constructor_throws_IAE_if_text_is_null() { + expectTextCantBeNullNorEmptyIAE(); + + new Message(null, 12L); + } + + @Test + public void constructor_throws_IAE_if_text_is_empty() { + expectTextCantBeNullNorEmptyIAE(); + + new Message("", 12L); + } + + private void expectTextCantBeNullNorEmptyIAE() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Text can't be null nor empty"); + } + + @Test + public void constructor_throws_IAE_if_timestamp_is_less_than_0() { + LongStream.of(0, 1 + new Random().nextInt(12)) + .forEach(timestamp -> assertThat(new Message("foo", timestamp).getTimestamp()).isEqualTo(timestamp)); + + long lessThanZero = -1 - new Random().nextInt(33); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Text can't be less than 0"); + + new Message("bar", lessThanZero); + } + + @Test + public void equals_is_based_on_text_and_timestamp() { + long timestamp = new Random().nextInt(10_999); + String text = randomAlphabetic(23); + Message underTest = new Message(text, timestamp); + + assertThat(underTest).isEqualTo(underTest); + assertThat(underTest).isEqualTo(new Message(text, timestamp)); + assertThat(underTest).isNotEqualTo(new Message(text + "ç", timestamp)); + assertThat(underTest).isNotEqualTo(new Message(text, timestamp + 10_999L)); + assertThat(underTest).isNotEqualTo(null); + assertThat(underTest).isNotEqualTo(new Object()); + } + + @Test + public void hashsode_is_based_on_text_and_timestamp() { + long timestamp = new Random().nextInt(10_999); + String text = randomAlphabetic(23); + Message underTest = new Message(text, timestamp); + + assertThat(underTest.hashCode()).isEqualTo(underTest.hashCode()); + assertThat(underTest.hashCode()).isEqualTo(new Message(text, timestamp).hashCode()); + assertThat(underTest.hashCode()).isNotEqualTo(new Message(text + "ç", timestamp).hashCode()); + assertThat(underTest.hashCode()).isNotEqualTo(new Message(text, timestamp + 10_999L).hashCode()); + assertThat(underTest.hashCode()).isNotEqualTo(new Object().hashCode()); + } +} -- 2.39.5