From: Sébastien Lesaint Date: Fri, 22 May 2015 12:33:11 +0000 (+0200) Subject: SONAR-6568 add step to generate QProfile events X-Git-Tag: 5.2-RC1~1856 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=refs%2Fpull%2F324%2Fhead;p=sonarqube.git SONAR-6568 add step to generate QProfile events --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/qualityprofile/QPMeasureData.java b/server/sonar-server/src/main/java/org/sonar/server/computation/qualityprofile/QPMeasureData.java new file mode 100644 index 00000000000..791e6f67272 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/qualityprofile/QPMeasureData.java @@ -0,0 +1,116 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.computation.qualityprofile; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.StringWriter; +import java.util.Comparator; +import java.util.Map; +import java.util.SortedSet; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.utils.text.JsonWriter; +import org.sonar.core.UtcDateUtils; + +/** + * Represents the array of JSON objects stored in the value of the + * {@link org.sonar.api.measures.CoreMetrics#QUALITY_PROFILES} measures. + */ +@Immutable +public class QPMeasureData { + + private final SortedSet profiles; + + public QPMeasureData(Iterable qualityProfiles) { + this.profiles = ImmutableSortedSet.copyOf(QualityProfileComparator.INSTANCE, qualityProfiles); + } + + public static QPMeasureData fromJson(String json) { + return new QPMeasureData(Iterables.transform(new JsonParser().parse(json).getAsJsonArray(), JsonElementToQualityProfile.INSTANCE)); + } + + public static String toJson(QPMeasureData data) { + StringWriter json = new StringWriter(); + JsonWriter writer = JsonWriter.of(json); + writer.beginArray(); + for (QualityProfile profile : data.getProfiles()) { + writer + .beginObject() + .prop("key", profile.getQpKey()) + .prop("language", profile.getLanguageKey()) + .prop("name", profile.getQpName()) + .prop("rulesUpdatedAt", UtcDateUtils.formatDateTime(profile.getRulesUpdatedAt())) + .endObject(); + } + writer.endArray(); + writer.close(); + return json.toString(); + } + + public SortedSet getProfiles() { + return profiles; + } + + public Map getProfilesByKey() { + return Maps.uniqueIndex(this.profiles, QualityProfileToKey.INSTANCE); + } + + private enum QualityProfileComparator implements Comparator { + INSTANCE; + + @Override + public int compare(QualityProfile o1, QualityProfile o2) { + int c = o1.getLanguageKey().compareTo(o2.getLanguageKey()); + if (c == 0) { + c = o1.getQpName().compareTo(o2.getQpName()); + } + return c; + } + } + + private enum JsonElementToQualityProfile implements Function { + INSTANCE; + + @Override + public QualityProfile apply(@Nonnull JsonElement jsonElt) { + JsonObject jsonProfile = jsonElt.getAsJsonObject(); + return new QualityProfile( + jsonProfile.get("key").getAsString(), + jsonProfile.get("name").getAsString(), + jsonProfile.get("language").getAsString(), + UtcDateUtils.parseDateTime(jsonProfile.get("rulesUpdatedAt").getAsString())); + } + } + + private enum QualityProfileToKey implements Function { + INSTANCE; + + @Override + public String apply(@Nonnull QualityProfile input) { + return input.getQpKey(); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/qualityprofile/QualityProfile.java b/server/sonar-server/src/main/java/org/sonar/server/computation/qualityprofile/QualityProfile.java new file mode 100644 index 00000000000..accfd11c1a0 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/qualityprofile/QualityProfile.java @@ -0,0 +1,89 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.computation.qualityprofile; + +import com.google.common.base.Objects; +import java.util.Date; +import javax.annotation.concurrent.Immutable; + +import static java.util.Objects.requireNonNull; + +/** + * Represents the JSON object an array of which is stored in the value of the + * {@link org.sonar.api.measures.CoreMetrics#QUALITY_PROFILES} measures. + */ +@Immutable +public class QualityProfile { + private final String qpKey; + private final String qpName; + private final String languageKey; + private final Date rulesUpdatedAt; + + public QualityProfile(String qpKey, String qpName, String languageKey, Date rulesUpdatedAt) { + this.qpKey = requireNonNull(qpKey); + this.qpName = requireNonNull(qpName); + this.languageKey = requireNonNull(languageKey); + this.rulesUpdatedAt = requireNonNull(rulesUpdatedAt); + } + + public String getQpKey() { + return qpKey; + } + + public String getQpName() { + return qpName; + } + + public String getLanguageKey() { + return languageKey; + } + + public Date getRulesUpdatedAt() { + return new Date(rulesUpdatedAt.getTime()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + QualityProfile qProfile = (QualityProfile) o; + return qpKey.equals(qProfile.qpKey); + } + + @Override + public int hashCode() { + return qpKey.hashCode(); + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("key", qpKey) + .add("name", qpName) + .add("language", languageKey) + .add("rulesUpdatedAt", rulesUpdatedAt) + .toString(); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/qualityprofile/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/computation/qualityprofile/package-info.java new file mode 100644 index 00000000000..5fae16422e9 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/qualityprofile/package-info.java @@ -0,0 +1,24 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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. + */ + +@ParametersAreNonnullByDefault +package org.sonar.server.computation.qualityprofile; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java b/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java index 134418ae278..0ff44fd8b93 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java @@ -20,12 +20,13 @@ package org.sonar.server.computation.step; -import com.google.common.collect.Lists; -import org.sonar.server.computation.ComputationContainer; - import java.util.Arrays; import java.util.List; +import org.sonar.server.computation.ComputationContainer; + +import com.google.common.collect.Lists; + /** * Ordered list of steps to be executed */ @@ -42,6 +43,9 @@ public class ComputationSteps { // Read report ParseReportStep.class, + // data computation + QualityProfileEventsStep.class, + // Persist data PersistComponentsStep.class, PersistNumberOfDaysSinceLastCommitStep.class, diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/step/QualityProfileEventsStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/step/QualityProfileEventsStep.java new file mode 100644 index 00000000000..4bb1e5f019e --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/step/QualityProfileEventsStep.java @@ -0,0 +1,134 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.computation.step; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableSortedMap; +import java.util.Date; +import java.util.Map; +import javax.annotation.Nullable; +import org.apache.commons.lang.time.DateUtils; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.resources.Language; +import org.sonar.api.utils.KeyValueFormat; +import org.sonar.batch.protocol.output.BatchReport; +import org.sonar.core.UtcDateUtils; +import org.sonar.core.measure.db.MeasureDto; +import org.sonar.server.computation.ComputationContext; +import org.sonar.server.computation.component.ChildFirstTypeAwareVisitor; +import org.sonar.server.computation.component.Component; +import org.sonar.server.computation.event.Event; +import org.sonar.server.computation.qualityprofile.QualityProfile; +import org.sonar.server.computation.qualityprofile.QPMeasureData; + +public class QualityProfileEventsStep implements ComputationStep { + + @Override + public void execute(ComputationContext context) { + new ChildFirstTypeAwareVisitor(Component.Type.PROJECT) { + @Override + public void visitProject(Component tree) { + executeForProject(tree); + } + }.visit(context.getRoot()); + } + + private void executeForProject(Component projectComponent) { + Optional previousMeasure = projectComponent.getMeasureRepository().findPrevious(CoreMetrics.QUALITY_PROFILES); + if (!previousMeasure.isPresent()) { + // first analysis -> do not generate events + return; + } + + // Load current profiles + Map previousProfiles = QPMeasureData.fromJson(previousMeasure.get().getData()).getProfilesByKey(); + Optional currentMeasure = projectComponent.getMeasureRepository().findCurrent(CoreMetrics.QUALITY_PROFILES); + if (!currentMeasure.isPresent()) { + throw new IllegalStateException("Missing measure " + CoreMetrics.QUALITY_PROFILES + " for component " + projectComponent.getRef()); + } + Map currentProfiles = QPMeasureData.fromJson(currentMeasure.get().getStringValue()).getProfilesByKey(); + + detectNewOrUpdatedProfiles(projectComponent, previousProfiles, currentProfiles); + detectNoMoreUsedProfiles(projectComponent, previousProfiles, currentProfiles); + } + + private void detectNoMoreUsedProfiles(Component context, Map previousProfiles, Map currentProfiles) { + for (QualityProfile previousProfile : previousProfiles.values()) { + if (!currentProfiles.containsKey(previousProfile.getQpKey())) { + markAsRemoved(context, previousProfile); + } + } + } + + private void detectNewOrUpdatedProfiles(Component component, Map previousProfiles, Map currentProfiles) { + for (QualityProfile profile : currentProfiles.values()) { + QualityProfile previousProfile = previousProfiles.get(profile.getQpKey()); + if (previousProfile == null) { + markAsAdded(component, profile); + } else if (profile.getRulesUpdatedAt().after(previousProfile.getRulesUpdatedAt())) { + markAsChanged(component, previousProfile, profile); + } + } + } + + private void markAsChanged(Component component, QualityProfile previousProfile, QualityProfile profile) { + Date from = previousProfile.getRulesUpdatedAt(); + + String data = KeyValueFormat.format(ImmutableSortedMap.of( + "key", profile.getQpKey(), + "from", UtcDateUtils.formatDateTime(fixDate(from)), + "to", UtcDateUtils.formatDateTime(fixDate(profile.getRulesUpdatedAt())))); + component.getEventRepository().add(createQProfileEvent(component, profile, "Changes in %s", data)); + } + + private void markAsRemoved(Component component, QualityProfile profile) { + component.getEventRepository().add(createQProfileEvent(component, profile, "Stop using %s")); + } + + private void markAsAdded(Component component, QualityProfile profile) { + component.getEventRepository().add(createQProfileEvent(component, profile, "Use %s")); + } + + private static Event createQProfileEvent(Component component, QualityProfile profile, String namePattern) { + return createQProfileEvent(component, profile, namePattern, null); + } + + private static Event createQProfileEvent(Component component, QualityProfile profile, String namePattern, @Nullable String data) { + return Event.createProfile(String.format(namePattern, profileLabel(component, profile)), data, null); + } + + private static String profileLabel(Component component, QualityProfile profile) { + Optional language = component.getContext().getLanguageRepository().find(profile.getLanguageKey()); + String languageName = language.isPresent() ? language.get().getName() : profile.getLanguageKey(); + return String.format("'%s' (%s)", profile.getQpName(), languageName); + } + + /** + * This hack must be done because date precision is millisecond in db/es and date format is select only + */ + private Date fixDate(Date date) { + return DateUtils.addSeconds(date, 1); + } + + @Override + public String getDescription() { + return "Compute Quality Profile events"; + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/step/ComputationStepsTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/step/ComputationStepsTest.java index 4e217db6925..e82c3794e97 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/step/ComputationStepsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/step/ComputationStepsTest.java @@ -50,12 +50,14 @@ public class ComputationStepsTest { mock(PersistTestsStep.class), mock(IndexTestsStep.class), mock(FeedComponentsCacheStep.class), - mock(PersistComponentsStep.class) - ); + mock(PersistComponentsStep.class), + mock(IndexTestsStep.class), + mock(QualityProfileEventsStep.class) + ); - assertThat(registry.orderedSteps()).hasSize(19); + assertThat(registry.orderedSteps()).hasSize(20); assertThat(registry.orderedSteps().get(0)).isInstanceOf(FeedComponentsCacheStep.class); - assertThat(registry.orderedSteps().get(18)).isInstanceOf(SendIssueNotificationsStep.class); + assertThat(registry.orderedSteps().get(19)).isInstanceOf(SendIssueNotificationsStep.class); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/step/QualityProfileEventsStepTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/step/QualityProfileEventsStepTest.java new file mode 100644 index 00000000000..bb2e9854cc2 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/step/QualityProfileEventsStepTest.java @@ -0,0 +1,315 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.computation.step; + +import com.google.common.base.Optional; +import com.google.common.collect.Lists; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; +import org.junit.Test; +import org.sonar.api.config.Settings; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.resources.AbstractLanguage; +import org.sonar.api.resources.Language; +import org.sonar.batch.protocol.output.BatchReport; +import org.sonar.batch.protocol.output.BatchReportReader; +import org.sonar.core.UtcDateUtils; +import org.sonar.core.measure.db.MeasureDto; +import org.sonar.server.computation.ComputationContext; +import org.sonar.server.computation.component.Component; +import org.sonar.server.computation.component.ComponentTreeBuilder; +import org.sonar.server.computation.component.DumbComponent; +import org.sonar.server.computation.event.Event; +import org.sonar.server.computation.event.EventRepository; +import org.sonar.server.computation.language.LanguageRepository; +import org.sonar.server.computation.measure.MeasureRepository; +import org.sonar.server.computation.qualityprofile.QPMeasureData; +import org.sonar.server.computation.qualityprofile.QualityProfile; +import org.sonar.server.db.DbClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.api.utils.DateUtils.parseDateTime; + +public class QualityProfileEventsStepTest { + + private static final String QP_NAME_1 = "qp_1"; + private static final String QP_NAME_2 = "qp_2"; + private static final String LANGUAGE_KEY_1 = "language_key1"; + private static final String LANGUAGE_KEY_2 = "language_key_2"; + private static final String LANGUAGE_KEY_3 = "languageKey3"; + + private MeasureRepository measureRepository = mock(MeasureRepository.class); + private LanguageRepository languageRepository = mock(LanguageRepository.class); + + private QualityProfileEventsStep underTest = new QualityProfileEventsStep(); + + @Test + public void no_effect_if_no_previous_measure() { + when(measureRepository.findPrevious(CoreMetrics.QUALITY_PROFILES)).thenReturn(Optional.absent()); + + ComputationContext context = newNoChildRootContext(); + underTest.execute(context); + + assertThat(context.getRoot().getEventRepository().getEvents()).isEmpty(); + } + + @Test(expected = IllegalStateException.class) + public void ISE_if_no_current_measure() { + when(measureRepository.findPrevious(CoreMetrics.QUALITY_PROFILES)).thenReturn(Optional.of(newMeasureDto())); + when(measureRepository.findCurrent(CoreMetrics.QUALITY_PROFILES)).thenReturn(Optional.absent()); + + underTest.execute(newNoChildRootContext()); + } + + @Test + public void no_event_if_no_qp_now_nor_before() { + mockMeasures(null, null); + + ComputationContext context = newNoChildRootContext(); + underTest.execute(context); + + assertThat(context.getRoot().getEventRepository().getEvents()).isEmpty(); + } + + @Test + public void added_event_if_one_new_qp() { + QualityProfile qp = qp(QP_NAME_1, LANGUAGE_KEY_1); + mockMeasures(null, arrayOf(qp)); + Language language = mockLanguageInRepository(LANGUAGE_KEY_1); + + ComputationContext context = newNoChildRootContext(); + underTest.execute(context); + + List events = Lists.newArrayList(context.getRoot().getEventRepository().getEvents()); + assertThat(events).hasSize(1); + verifyEvent(events.get(0), "Use '" + qp.getQpName() + "' (" + language.getName() + ")", null); + } + + @Test + public void added_event_uses_language_key_in_message_if_language_not_found() { + QualityProfile qp = qp(QP_NAME_1, LANGUAGE_KEY_1); + mockMeasures(null, arrayOf(qp)); + mockLanguageNotInRepository(LANGUAGE_KEY_1); + + ComputationContext context = newNoChildRootContext(); + underTest.execute(context); + + List events = Lists.newArrayList(context.getRoot().getEventRepository().getEvents()); + assertThat(events).hasSize(1); + verifyEvent(events.get(0), "Use '" + qp.getQpName() + "' (" + qp.getLanguageKey() + ")", null); + } + + @Test + public void no_more_used_event_if_qp_no_more_listed() { + QualityProfile qp = qp(QP_NAME_1, LANGUAGE_KEY_1); + mockMeasures(arrayOf(qp), null); + Language language = mockLanguageInRepository(LANGUAGE_KEY_1); + + ComputationContext context = newNoChildRootContext(); + underTest.execute(context); + + List events = Lists.newArrayList(context.getRoot().getEventRepository().getEvents()); + assertThat(events).hasSize(1); + verifyEvent(events.get(0), "Stop using '" + qp.getQpName() + "' (" + language.getName() + ")", null); + } + + @Test + public void no_more_used_event_uses_language_key_in_message_if_language_not_found() { + QualityProfile qp = qp(QP_NAME_1, LANGUAGE_KEY_1); + mockMeasures(arrayOf(qp), null); + mockLanguageNotInRepository(LANGUAGE_KEY_1); + + ComputationContext context = newNoChildRootContext(); + underTest.execute(context); + + List events = Lists.newArrayList(context.getRoot().getEventRepository().getEvents()); + assertThat(events).hasSize(1); + verifyEvent(events.get(0), "Stop using '" + qp.getQpName() + "' (" + qp.getLanguageKey() + ")", null); + } + + @Test + public void no_event_if_same_qp_with_same_date() { + QualityProfile qp = qp(QP_NAME_1, LANGUAGE_KEY_1); + mockMeasures(arrayOf(qp), arrayOf(qp)); + + ComputationContext context = newNoChildRootContext(); + underTest.execute(context); + + assertThat(context.getRoot().getEventRepository().getEvents()).isEmpty(); + } + + @Test + public void changed_event_if_same_qp_but_no_same_date() { + QualityProfile qp1 = qp(QP_NAME_1, LANGUAGE_KEY_1, parseDateTime("2011-04-25T01:05:13+0100")); + QualityProfile qp2 = qp(QP_NAME_1, LANGUAGE_KEY_1, parseDateTime("2011-04-25T01:05:17+0100")); + mockMeasures(arrayOf(qp1), arrayOf(qp2)); + Language language = mockLanguageInRepository(LANGUAGE_KEY_1); + + ComputationContext context = newNoChildRootContext(); + underTest.execute(context); + + List events = Lists.newArrayList(context.getRoot().getEventRepository().getEvents()); + assertThat(events).hasSize(1); + verifyEvent( + events.get(0), + "Changes in '" + qp2.getQpName() + "' (" + language.getName() + ")", + "from=" + UtcDateUtils.formatDateTime(parseDateTime("2011-04-25T01:05:14+0100")) + ";key=" + qp1.getQpKey() + ";to=" + + UtcDateUtils.formatDateTime(parseDateTime("2011-04-25T01:05:18+0100"))); + } + + @Test + public void verify_detection_with_complex_mix_of_qps() { + mockMeasures( + arrayOf( + qp(QP_NAME_2, LANGUAGE_KEY_1), + qp(QP_NAME_2, LANGUAGE_KEY_2), + qp(QP_NAME_1, LANGUAGE_KEY_1, parseDateTime("2011-04-25T01:05:13+0100")) + ), + arrayOf( + qp(QP_NAME_1, LANGUAGE_KEY_1, parseDateTime("2011-04-25T01:05:17+0100")), + qp(QP_NAME_2, LANGUAGE_KEY_2), + qp(QP_NAME_2, LANGUAGE_KEY_3) + )); + mockNoLanguageInRepository(); + + ComputationContext context = newNoChildRootContext(); + underTest.execute(context); + + assertThat(context.getRoot().getEventRepository().getEvents()).extracting("name").containsOnly( + "Stop using '" + QP_NAME_2 + "' (" + LANGUAGE_KEY_1 + ")", + "Use '" + QP_NAME_2 + "' (" + LANGUAGE_KEY_3 + ")", + "Changes in '" + QP_NAME_1 + "' (" + LANGUAGE_KEY_1 + ")" + ); + + } + + private Language mockLanguageInRepository(String languageKey) { + Language language = new AbstractLanguage(languageKey, languageKey + "_name") { + @Override + public String[] getFileSuffixes() { + return new String[0]; + } + }; + when(languageRepository.find(languageKey)).thenReturn(Optional.of(language)); + return language; + } + + private void mockLanguageNotInRepository(String languageKey) { + when(languageRepository.find(languageKey)).thenReturn(Optional.absent()); + } + + private void mockNoLanguageInRepository() { + when(languageRepository.find(anyString())).thenReturn(Optional.absent()); + } + + private void mockMeasures(@Nullable QualityProfile[] previous, @Nullable QualityProfile[] current) { + when(measureRepository.findPrevious(CoreMetrics.QUALITY_PROFILES)).thenReturn(Optional.of(newMeasureDto(previous))); + when(measureRepository.findCurrent(CoreMetrics.QUALITY_PROFILES)).thenReturn(Optional.of(newQPBatchMeasure(current))); + } + + private static void verifyEvent(Event event, String expectedName, @Nullable String expectedData) { + assertThat(event.getName()).isEqualTo(expectedName); + assertThat(event.getData()).isEqualTo(expectedData); + assertThat(event.getCategory()).isEqualTo(Event.Category.PROFILE); + assertThat(event.getDescription()).isNull(); + } + + private static QualityProfile qp(String qpName, String languageKey) { + return qp(qpName, languageKey, new Date()); + } + + private static QualityProfile qp(String qpName, String languageKey, Date date) { + return new QualityProfile(qpName + "-" + languageKey, qpName, languageKey, date); + } + + /** + * Just a trick to use variable args which is shorter than writing new QualityProfile[] { } + */ + private static QualityProfile[] arrayOf(QualityProfile... qps) { + return qps; + } + + private static MeasureDto newMeasureDto(@Nullable QualityProfile... qps) { + return new MeasureDto().setData(toJson(qps)); + } + + private static BatchReport.Measure newQPBatchMeasure(@Nullable QualityProfile... qps) { + return BatchReport.Measure.newBuilder().setStringValue(toJson(qps)).build(); + } + + private static String toJson(@Nullable QualityProfile... qps) { + List qualityProfiles = qps == null ? Collections.emptyList() : Arrays.asList(qps); + return QPMeasureData.toJson(new QPMeasureData(qualityProfiles)); + } + + private ComputationContext newNoChildRootContext() { + return newContext(new ComponentTreeBuilder() { + @Override + public Component build(ComputationContext context) { + return new EventAndMeasureRepoComponent(context, Component.Type.PROJECT, 1); + } + }); + } + + private ComputationContext newContext(ComponentTreeBuilder builder) { + return new ComputationContext(mock(BatchReportReader.class), "COMPONENT_KEY", new Settings(), mock(DbClient.class), + builder, languageRepository); + } + + private class EventAndMeasureRepoComponent extends DumbComponent { + private final EventRepository eventRepository = new EventRepository() { + private final Set events = new HashSet<>(); + + @Override + public void add(Event event) { + events.add(event); + } + + @Override + public Iterable getEvents() { + return events; + } + }; + + public EventAndMeasureRepoComponent(@Nullable org.sonar.server.computation.context.ComputationContext context, + Type type, int ref, @Nullable Component... children) { + super(context, type, ref, children); + } + + @Override + public MeasureRepository getMeasureRepository() { + return measureRepository; + } + + @Override + public EventRepository getEventRepository() { + return eventRepository; + } + } + +}