Browse Source

SONAR-11722 add ability for Post tasks to add log statistics

tags/8.0
Sébastien Lesaint 4 years ago
parent
commit
c310acf604

+ 45
- 5
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java View File

@@ -48,6 +48,7 @@ import org.sonar.ce.task.step.ComputationStepExecutor;
import org.sonar.core.util.logs.Profiler;
import org.sonar.core.util.stream.MoreCollectors;

import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.empty;
@@ -111,16 +112,55 @@ public class PostProjectAnalysisTasksExecutor implements ComputationStepExecutor

private static void executeTask(ProjectAnalysisImpl projectAnalysis, PostProjectAnalysisTask postProjectAnalysisTask) {
String status = "FAILED";
Profiler stepProfiler = Profiler.create(LOG).logTimeLast(true);
Profiler task = Profiler.create(LOG).logTimeLast(true);
try {
stepProfiler.start();
postProjectAnalysisTask.finished(projectAnalysis);
task.start();
postProjectAnalysisTask.finished(new ContextImpl(projectAnalysis, task));
status = "SUCCESS";
} catch (Exception e) {
LOG.error("Execution of task " + postProjectAnalysisTask.getClass() + " failed", e);
} finally {
stepProfiler.addContext("status", status);
stepProfiler.stopInfo("{}", postProjectAnalysisTask.getDescription());
task.addContext("status", status);
task.stopInfo("{}", postProjectAnalysisTask.getDescription());
}
}

private static class ContextImpl implements PostProjectAnalysisTask.Context {
private final ProjectAnalysisImpl projectAnalysis;
private final Profiler task;

private ContextImpl(ProjectAnalysisImpl projectAnalysis, Profiler task) {
this.projectAnalysis = projectAnalysis;
this.task = task;
}

@Override
public PostProjectAnalysisTask.ProjectAnalysis getProjectAnalysis() {
return projectAnalysis;
}

@Override
public PostProjectAnalysisTask.LogStatistics getLogStatistics() {
return new LogStatisticsImpl(task);
}
}

private static class LogStatisticsImpl implements PostProjectAnalysisTask.LogStatistics {
private final Profiler profiler;

private LogStatisticsImpl(Profiler profiler) {
this.profiler = profiler;
}

@Override
public PostProjectAnalysisTask.LogStatistics add(String key, Object value) {
requireNonNull(key, "Statistic has null key");
requireNonNull(value, () -> format("Statistic with key [%s] has null value", key));
checkArgument(!key.equalsIgnoreCase("time") && !key.equalsIgnoreCase("status"),
"Statistic with key [%s] is not accepted", key);
checkArgument(!profiler.hasContext(key), "Statistic with key [%s] is already present", key);
profiler.addContext(key, value);
return this;
}
}


+ 2
- 1
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/webhook/WebhookPostTask.java View File

@@ -46,7 +46,8 @@ public class WebhookPostTask implements PostProjectAnalysisTask {
}

@Override
public void finished(ProjectAnalysis analysis) {
public void finished(Context context) {
ProjectAnalysis analysis = context.getProjectAnalysis();
webHooks.sendProjectAnalysisUpdate(
new WebHooks.Analysis(
analysis.getProject().getUuid(),

+ 134
- 39
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutorTest.java View File

@@ -24,7 +24,9 @@ import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import javax.annotation.Nullable;
import org.apache.commons.lang.RandomStringUtils;
@@ -58,9 +60,12 @@ import org.sonar.scanner.protocol.output.ScannerReport;

import static com.google.common.collect.ImmutableList.of;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.ThrowableAssert.catchThrowable;
import static org.assertj.core.data.MapEntry.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
@@ -89,7 +94,7 @@ public class PostProjectAnalysisTasksExecutorTest {
private String organizationKey = organizationUuid + "_key";
private String organizationName = organizationUuid + "_name";
private System2 system2 = mock(System2.class);
private ArgumentCaptor<PostProjectAnalysisTask.ProjectAnalysis> projectAnalysisArgumentCaptor = ArgumentCaptor.forClass(PostProjectAnalysisTask.ProjectAnalysis.class);
private ArgumentCaptor<PostProjectAnalysisTask.Context> taskContextCaptor = ArgumentCaptor.forClass(PostProjectAnalysisTask.Context.class);
private CeTask.Component component = new CeTask.Component("component uuid", "component key", "component name");
private CeTask ceTask = new CeTask.Builder()
.setOrganizationUuid(organizationUuid)
@@ -138,15 +143,16 @@ public class PostProjectAnalysisTasksExecutorTest {
new PostProjectAnalysisTasksExecutor(
ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder, reportReader,
system2, new PostProjectAnalysisTask[] {postProjectAnalysisTask1, postProjectAnalysisTask2})
.finished(allStepsExecuted);
.finished(allStepsExecuted);

inOrder.verify(postProjectAnalysisTask1).finished(projectAnalysisArgumentCaptor.capture());
inOrder.verify(postProjectAnalysisTask1).finished(taskContextCaptor.capture());
inOrder.verify(postProjectAnalysisTask1).getDescription();
inOrder.verify(postProjectAnalysisTask2).finished(projectAnalysisArgumentCaptor.capture());
inOrder.verify(postProjectAnalysisTask2).finished(taskContextCaptor.capture());
inOrder.verify(postProjectAnalysisTask2).getDescription();
inOrder.verifyNoMoreInteractions();

List<PostProjectAnalysisTask.ProjectAnalysis> allValues = projectAnalysisArgumentCaptor.getAllValues();
ArgumentCaptor<PostProjectAnalysisTask.Context> taskContextCaptor = this.taskContextCaptor;
List<PostProjectAnalysisTask.ProjectAnalysis> allValues = getAllProjectAnalyses(taskContextCaptor);
assertThat(allValues).hasSize(2);
assertThat(allValues.get(0)).isSameAs(allValues.get(1));

@@ -166,9 +172,9 @@ public class PostProjectAnalysisTasksExecutorTest {
new OrganizationDto().setKey(organizationKey).setName(organizationName).setUuid(organizationUuid).setDefaultQualityGateUuid("foo")));
underTest.finished(allStepsExecuted);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

assertThat(projectAnalysisArgumentCaptor.getValue().getOrganization()).isEmpty();
assertThat(taskContextCaptor.getValue().getProjectAnalysis().getOrganization()).isEmpty();
}

@Test
@@ -180,9 +186,9 @@ public class PostProjectAnalysisTasksExecutorTest {
new OrganizationDto().setKey(organizationKey).setName(organizationName).setUuid(organizationUuid).setDefaultQualityGateUuid("foo")));
underTest.finished(allStepsExecuted);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

org.sonar.api.ce.posttask.Organization organization = projectAnalysisArgumentCaptor.getValue().getOrganization().get();
org.sonar.api.ce.posttask.Organization organization = taskContextCaptor.getValue().getProjectAnalysis().getOrganization().get();
assertThat(organization.getKey()).isEqualTo(organizationKey);
assertThat(organization.getName()).isEqualTo(organizationName);
}
@@ -192,9 +198,9 @@ public class PostProjectAnalysisTasksExecutorTest {
public void CeTask_status_depends_on_finished_method_argument_is_true_or_false(boolean allStepsExecuted) {
underTest.finished(allStepsExecuted);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

assertThat(projectAnalysisArgumentCaptor.getValue().getCeTask().getStatus())
assertThat(taskContextCaptor.getValue().getProjectAnalysis().getCeTask().getStatus())
.isEqualTo(
allStepsExecuted ? org.sonar.api.ce.posttask.CeTask.Status.SUCCESS : org.sonar.api.ce.posttask.CeTask.Status.FAILED);
}
@@ -203,9 +209,9 @@ public class PostProjectAnalysisTasksExecutorTest {
public void ceTask_uuid_is_UUID_of_CeTask() {
underTest.finished(true);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

assertThat(projectAnalysisArgumentCaptor.getValue().getCeTask().getId())
assertThat(taskContextCaptor.getValue().getProjectAnalysis().getCeTask().getId())
.isEqualTo(ceTask.getUuid());
}

@@ -213,9 +219,9 @@ public class PostProjectAnalysisTasksExecutorTest {
public void project_uuid_key_and_name_come_from_CeTask() {
underTest.finished(true);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

Project project = projectAnalysisArgumentCaptor.getValue().getProject();
Project project = taskContextCaptor.getValue().getProjectAnalysis().getProject();
assertThat(project.getUuid()).isEqualTo(ceTask.getComponent().get().getUuid());
assertThat(project.getKey()).isEqualTo(ceTask.getComponent().get().getKey().get());
assertThat(project.getName()).isEqualTo(ceTask.getComponent().get().getName().get());
@@ -228,9 +234,9 @@ public class PostProjectAnalysisTasksExecutorTest {

underTest.finished(true);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

assertThat(projectAnalysisArgumentCaptor.getValue().getDate())
assertThat(taskContextCaptor.getValue().getProjectAnalysis().getDate())
.isEqualTo(new Date(analysisMetadataHolder.getAnalysisDate()));
}

@@ -241,9 +247,9 @@ public class PostProjectAnalysisTasksExecutorTest {

underTest.finished(false);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

assertThat(projectAnalysisArgumentCaptor.getValue().getDate()).isEqualTo(new Date(now));
assertThat(taskContextCaptor.getValue().getProjectAnalysis().getDate()).isEqualTo(new Date(now));
}

@Test
@@ -253,11 +259,11 @@ public class PostProjectAnalysisTasksExecutorTest {

underTest.finished(true);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

assertThat(projectAnalysisArgumentCaptor.getValue().getAnalysis().get().getDate())
assertThat(taskContextCaptor.getValue().getProjectAnalysis().getAnalysis().get().getDate())
.isEqualTo(new Date(analysisMetadataHolder.getAnalysisDate()));
assertThat(projectAnalysisArgumentCaptor.getValue().getAnalysis().get().getAnalysisUuid())
assertThat(taskContextCaptor.getValue().getProjectAnalysis().getAnalysis().get().getAnalysisUuid())
.isEqualTo(analysisMetadataHolder.getUuid());
}

@@ -265,9 +271,9 @@ public class PostProjectAnalysisTasksExecutorTest {
public void analysis_is_empty_when_not_set_in_AnalysisMetadataHolder() {
underTest.finished(false);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

assertThat(projectAnalysisArgumentCaptor.getValue().getAnalysis()).isEmpty();
assertThat(taskContextCaptor.getValue().getProjectAnalysis().getAnalysis()).isEmpty();
}

@Test
@@ -276,9 +282,9 @@ public class PostProjectAnalysisTasksExecutorTest {

underTest.finished(true);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

assertThat(projectAnalysisArgumentCaptor.getValue().getBranch()).isEmpty();
assertThat(taskContextCaptor.getValue().getProjectAnalysis().getBranch()).isEmpty();
}

@Test
@@ -332,9 +338,9 @@ public class PostProjectAnalysisTasksExecutorTest {

underTest.finished(true);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

org.sonar.api.ce.posttask.Branch branch = projectAnalysisArgumentCaptor.getValue().getBranch().get();
org.sonar.api.ce.posttask.Branch branch = taskContextCaptor.getValue().getProjectAnalysis().getBranch().get();
assertThat(branch.isMain()).isFalse();
assertThat(branch.getName()).hasValue("feature/foo");
assertThat(branch.getType()).isEqualTo(BranchImpl.Type.SHORT);
@@ -344,18 +350,18 @@ public class PostProjectAnalysisTasksExecutorTest {
public void qualityGate_is_null_when_finished_method_argument_is_false() {
underTest.finished(false);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

assertThat(projectAnalysisArgumentCaptor.getValue().getQualityGate()).isNull();
assertThat(taskContextCaptor.getValue().getProjectAnalysis().getQualityGate()).isNull();
}

@Test
public void qualityGate_is_populated_when_finished_method_argument_is_true() {
underTest.finished(true);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

org.sonar.api.ce.posttask.QualityGate qualityGate = projectAnalysisArgumentCaptor.getValue().getQualityGate();
org.sonar.api.ce.posttask.QualityGate qualityGate = taskContextCaptor.getValue().getProjectAnalysis().getQualityGate();
assertThat(qualityGate.getStatus()).isEqualTo(org.sonar.api.ce.posttask.QualityGate.Status.OK);
assertThat(qualityGate.getId()).isEqualTo(String.valueOf(QUALITY_GATE_ID));
assertThat(qualityGate.getName()).isEqualTo(QUALITY_GATE_NAME);
@@ -367,12 +373,93 @@ public class PostProjectAnalysisTasksExecutorTest {
reportReader.putContextProperties(asList(ScannerReport.ContextProperty.newBuilder().setKey("foo").setValue("bar").build()));
underTest.finished(true);

verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture());
verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());

org.sonar.api.ce.posttask.ScannerContext scannerContext = projectAnalysisArgumentCaptor.getValue().getScannerContext();
org.sonar.api.ce.posttask.ScannerContext scannerContext = taskContextCaptor.getValue().getProjectAnalysis().getScannerContext();
assertThat(scannerContext.getProperties()).containsExactly(entry("foo", "bar"));
}

@Test
@UseDataProvider("booleanValues")
public void logStatistics_add_fails_when_NPE_if_key_or_value_is_null(boolean allStepsExecuted) {
underTest.finished(allStepsExecuted);

verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
PostProjectAnalysisTask.LogStatistics logStatistics = taskContextCaptor.getValue().getLogStatistics();

assertThat(catchThrowable(() -> logStatistics.add(null, "foo")))
.isInstanceOf(NullPointerException.class)
.hasMessage("Statistic has null key");
assertThat(catchThrowable(() -> logStatistics.add(null, null)))
.isInstanceOf(NullPointerException.class)
.hasMessage("Statistic has null key");
assertThat(catchThrowable(() -> logStatistics.add("bar", null)))
.isInstanceOf(NullPointerException.class)
.hasMessage("Statistic with key [bar] has null value");
}

@Test
@UseDataProvider("booleanValues")
public void logStatistics_add_fails_with_IAE_if_key_is_time_or_status_ignoring_case(boolean allStepsExecuted) {
underTest.finished(allStepsExecuted);

verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
PostProjectAnalysisTask.LogStatistics logStatistics = taskContextCaptor.getValue().getLogStatistics();

for (String reservedKey : asList("time", "TIME", "TImE", "status", "STATUS", "STaTuS")) {
assertThat(catchThrowable(() -> logStatistics.add(reservedKey, "foo")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Statistic with key [" + reservedKey + "] is not accepted");
}
}

@Test
@UseDataProvider("booleanValues")
public void logStatistics_add_fails_with_IAE_if_same_key_with_exact_case_added_twice(boolean allStepsExecuted) {
underTest.finished(allStepsExecuted);

verify(postProjectAnalysisTask).finished(taskContextCaptor.capture());
PostProjectAnalysisTask.LogStatistics logStatistics = taskContextCaptor.getValue().getLogStatistics();

String key = RandomStringUtils.randomAlphabetic(10);
logStatistics.add(key, new Object());
assertThat(catchThrowable(() -> logStatistics.add(key, "bar")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Statistic with key [" + key + "] is already present");
}

@Test
@UseDataProvider("booleanValues")
public void logStatistics_adds_statistics_to_end_of_task_log(boolean allStepsExecuted) {
Map<String, Object> stats = new HashMap<>();
for (int i = 0; i <= new Random().nextInt(10); i++) {
stats.put("statKey_" + i, "statVal_" + i);
}
PostProjectAnalysisTask logStatisticsTask = mock(PostProjectAnalysisTask.class);
when(logStatisticsTask.getDescription()).thenReturn("PT1");
doAnswer(i -> {
PostProjectAnalysisTask.Context context = i.getArgument(0);
stats.forEach((k, v) -> context.getLogStatistics().add(k, v));
return null;
}).when(logStatisticsTask)
.finished(any(PostProjectAnalysisTask.Context.class));

new PostProjectAnalysisTasksExecutor(
ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder, reportReader,
system2, new PostProjectAnalysisTask[] {logStatisticsTask})
.finished(allStepsExecuted);

verify(logStatisticsTask).finished(taskContextCaptor.capture());

assertThat(logTester.logs()).hasSize(1);
List<String> logs = logTester.logs(LoggerLevel.INFO);
assertThat(logs).hasSize(1);
StringBuilder expectedLog = new StringBuilder("^PT1 ");
stats.forEach((k,v) -> expectedLog.append("\\| " + k + "=" + v +" "));
expectedLog.append("\\| status=SUCCESS \\| time=\\d+ms$");
assertThat(logs.get(0)).matches(expectedLog.toString());
}

@Test
@UseDataProvider("booleanValues")
public void finished_does_not_fail_if_listener_throws_exception_and_execute_subsequent_listeners(boolean allStepsExecuted) {
@@ -383,18 +470,18 @@ public class PostProjectAnalysisTasksExecutorTest {

doThrow(new RuntimeException("Faking a listener throws an exception"))
.when(postProjectAnalysisTask2)
.finished(any(PostProjectAnalysisTask.ProjectAnalysis.class));
.finished(any(PostProjectAnalysisTask.Context.class));

new PostProjectAnalysisTasksExecutor(
ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder, reportReader,
system2, new PostProjectAnalysisTask[] {postProjectAnalysisTask1, postProjectAnalysisTask2, postProjectAnalysisTask3})
.finished(allStepsExecuted);
.finished(allStepsExecuted);

inOrder.verify(postProjectAnalysisTask1).finished(projectAnalysisArgumentCaptor.capture());
inOrder.verify(postProjectAnalysisTask1).finished(taskContextCaptor.capture());
inOrder.verify(postProjectAnalysisTask1).getDescription();
inOrder.verify(postProjectAnalysisTask2).finished(projectAnalysisArgumentCaptor.capture());
inOrder.verify(postProjectAnalysisTask2).finished(taskContextCaptor.capture());
inOrder.verify(postProjectAnalysisTask2).getDescription();
inOrder.verify(postProjectAnalysisTask3).finished(projectAnalysisArgumentCaptor.capture());
inOrder.verify(postProjectAnalysisTask3).finished(taskContextCaptor.capture());
inOrder.verify(postProjectAnalysisTask3).getDescription();
inOrder.verifyNoMoreInteractions();

@@ -423,7 +510,15 @@ public class PostProjectAnalysisTasksExecutorTest {
private static PostProjectAnalysisTask newPostProjectAnalysisTask(String description) {
PostProjectAnalysisTask res = mock(PostProjectAnalysisTask.class);
when(res.getDescription()).thenReturn(description);
doAnswer(i -> null).when(res).finished(any(PostProjectAnalysisTask.Context.class));
return res;
}

private static List<PostProjectAnalysisTask.ProjectAnalysis> getAllProjectAnalyses(ArgumentCaptor<PostProjectAnalysisTask.Context> taskContextCaptor) {
return taskContextCaptor.getAllValues()
.stream()
.map(PostProjectAnalysisTask.Context::getProjectAnalysis)
.collect(toList());
}

}

+ 46
- 0
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/api/posttask/TestPostTaskLogStatistics.java View File

@@ -0,0 +1,46 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.projectanalysis.api.posttask;

import java.util.HashMap;
import java.util.Map;
import org.sonar.api.ce.posttask.PostProjectAnalysisTask;

import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

class TestPostTaskLogStatistics implements PostProjectAnalysisTask.LogStatistics {
private final Map<String, Object> stats = new HashMap<>();

@Override
public PostProjectAnalysisTask.LogStatistics add(String key, Object value) {
requireNonNull(key, "Statistic has null key");
requireNonNull(value, () -> format("Statistic with key [%s] has null value", key));
checkArgument(!key.equalsIgnoreCase("time"), "Statistic with key [time] is not accepted");
checkArgument(!stats.containsKey(key), "Statistic with key [%s] is already present", key);
stats.put(key, value);
return this;
}

public Map<String, Object> getStats() {
return stats;
}
}

+ 30
- 1
sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/PostProjectAnalysisTask.java View File

@@ -51,8 +51,37 @@ public interface PostProjectAnalysisTask {

/**
* This method is called whenever the processing of a Project analysis has finished, whether successfully or not.
*
* @deprecated implement {@link #finished(Context)} instead
*/
void finished(ProjectAnalysis analysis);
@Deprecated
default void finished(ProjectAnalysis analysis) {
throw new IllegalStateException("Provide an implementation of method finished(Context)");
}

default void finished(Context context) {
finished(context.getProjectAnalysis());
}

interface Context {
ProjectAnalysis getProjectAnalysis();

LogStatistics getLogStatistics();
}

/**
* Each key-value paar will be added to the log describing the end of the
*/
interface LogStatistics {
/**
* @return this
* @throws NullPointerException if key or value is null
* @throws IllegalArgumentException if key has already been set
* @throws IllegalArgumentException if key is "status", to avoid conflict with field with same name added by the executor
* @throws IllegalArgumentException if key is "time", to avoid conflict with the profiler field with same name
*/
LogStatistics add(String key, Object value);
}

/**
* @since 5.5

+ 32
- 2
sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/PostProjectAnalysisTaskTester.java View File

@@ -30,8 +30,10 @@ import java.util.Optional;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;

import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static org.sonar.api.utils.Preconditions.checkArgument;
import static org.sonar.api.utils.Preconditions.checkState;

/**
* This class can be used to test {@link PostProjectAnalysisTask} implementations, see example below:
@@ -110,6 +112,8 @@ public class PostProjectAnalysisTaskTester {
private Branch branch;
private ScannerContext scannerContext;
private String analysisUuid;
@CheckForNull
private Map<String, Object> stats;

private PostProjectAnalysisTaskTester(PostProjectAnalysisTask underTest) {
this.underTest = requireNonNull(underTest, "PostProjectAnalysisTask instance cannot be null");
@@ -223,12 +227,38 @@ public class PostProjectAnalysisTaskTester {
.setDate(date)
.build();

this.underTest.
finished(projectAnalysis);
stats = new HashMap<>();
PostProjectAnalysisTask.LogStatistics logStatistics = new PostProjectAnalysisTask.LogStatistics() {
@Override
public PostProjectAnalysisTask.LogStatistics add(String key, Object value) {
requireNonNull(key, "Statistic has null key");
requireNonNull(value, () -> format("Statistic with key [%s] has null value", key));
checkArgument(!key.equalsIgnoreCase("time"), "Statistic with key [time] is not accepted");
checkArgument(!stats.containsKey(key), "Statistic with key [%s] is already present", key);
stats.put(key, value);
return this;
}
};

this.underTest.finished(new PostProjectAnalysisTask.Context() {
@Override
public PostProjectAnalysisTask.ProjectAnalysis getProjectAnalysis() {
return projectAnalysis;
}
@Override
public PostProjectAnalysisTask.LogStatistics getLogStatistics() {
return logStatistics;
}
});

return projectAnalysis;
}

public Map<String, Object> getLogStatistics() {
checkState(stats != null, "execute must be called first");
return stats;
}

public static final class OrganizationBuilder {
@CheckForNull
private String name;

+ 79
- 0
sonar-plugin-api/src/test/java/org/sonar/api/ce/posttask/PostProjectAnalysisTaskTest.java View File

@@ -0,0 +1,79 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.api.ce.posttask;

import org.junit.Test;
import org.mockito.Mockito;
import org.sonar.api.ce.posttask.PostProjectAnalysisTask.Context;
import org.sonar.api.ce.posttask.PostProjectAnalysisTask.ProjectAnalysis;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class PostProjectAnalysisTaskTest {
private final Context context = mock(Context.class);
private final ProjectAnalysis projectAnalysis = Mockito.mock(ProjectAnalysis.class);

@Test
public void default_implementation_of_finished_ProjectAnalysis_throws_ISE() {
PostProjectAnalysisTask underTest = new PostProjectAnalysisTask() {
@Override
public String getDescription() {
throw new UnsupportedOperationException("getDescription not implemented");
}
};

try {
underTest.finished(projectAnalysis);
fail("should have thrown an ISE");
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("Provide an implementation of method finished(Context)");
Mockito.verifyZeroInteractions(projectAnalysis);
}
}

@Test
public void default_implementation_of_finished_Context_calls_finished_ProjectAnalysis() {
when(context.getProjectAnalysis()).thenReturn(projectAnalysis);
boolean[] called = {false};
PostProjectAnalysisTask underTest = new PostProjectAnalysisTask() {

// override default implementation which throws an exception
@Override
public void finished(ProjectAnalysis analysis) {
called[0] = true;
assertThat(analysis).isSameAs(projectAnalysis);
}

@Override
public String getDescription() {
throw new UnsupportedOperationException("getDescription not implemented");
}
};

underTest.finished(context);

assertThat(called[0]).isTrue();
verify(context).getProjectAnalysis();
}
}

+ 45
- 2
sonar-plugin-api/src/test/java/org/sonar/api/ce/posttask/PostProjectAnalysisTaskTesterTest.java View File

@@ -20,12 +20,17 @@
package org.sonar.api.ce.posttask;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import org.apache.commons.lang.RandomStringUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@@ -141,6 +146,44 @@ public class PostProjectAnalysisTaskTesterTest {
underTest.execute();
}

@Test
public void getLogStatistics_throws_ISE_if_called_before_execute() {
expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("execute must be called first");

underTest.getLogStatistics();
}

@Test
public void getLogStatistics_returns_empty_if_no_log_statistic_added_by_tested_Task() {
underTest.withOrganization(organization).withCeTask(ceTask).withProject(project).withQualityGate(qualityGate).withAnalysisUuid(analysisUuid).at(someDate);

underTest.execute();

assertThat(underTest.getLogStatistics()).isEmpty();
}

@Test
public void getLogStatistics_returns_log_statistics_added_by_tested_Task() {
Random random = new Random();
Map<String, Object> expected = new HashMap<>();
for (int i = 0; i < 1 + random.nextInt(10); i++) {
expected.put(String.valueOf(i), random.nextInt(100));
}
PostProjectAnalysisTask projectAnalysisTask = mock(PostProjectAnalysisTask.class);
doAnswer(i -> {
PostProjectAnalysisTask.Context context = i.getArgument(0);
expected.forEach((k,v) -> context.getLogStatistics().add(k, v));
return null;
}).when(projectAnalysisTask).finished(any(PostProjectAnalysisTask.Context.class));
PostProjectAnalysisTaskTester underTest = PostProjectAnalysisTaskTester.of(projectAnalysisTask);
underTest.withOrganization(organization).withCeTask(ceTask).withProject(project).withQualityGate(qualityGate).withAnalysisUuid(analysisUuid).at(someDate);

underTest.execute();

assertThat(underTest.getLogStatistics()).isEqualTo(expected);
}

private static class CaptorPostProjectAnalysisTask implements PostProjectAnalysisTask {
private ProjectAnalysis projectAnalysis;

@@ -150,8 +193,8 @@ public class PostProjectAnalysisTaskTesterTest {
}

@Override
public void finished(ProjectAnalysis analysis) {
this.projectAnalysis = analysis;
public void finished(Context context) {
this.projectAnalysis = context.getProjectAnalysis();
}
}
}

Loading…
Cancel
Save