@@ -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<QualityProfile> profiles; | |||
public QPMeasureData(Iterable<QualityProfile> 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<QualityProfile> getProfiles() { | |||
return profiles; | |||
} | |||
public Map<String, QualityProfile> getProfilesByKey() { | |||
return Maps.uniqueIndex(this.profiles, QualityProfileToKey.INSTANCE); | |||
} | |||
private enum QualityProfileComparator implements Comparator<QualityProfile> { | |||
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<JsonElement, QualityProfile> { | |||
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<QualityProfile, String> { | |||
INSTANCE; | |||
@Override | |||
public String apply(@Nonnull QualityProfile input) { | |||
return input.getQpKey(); | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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; |
@@ -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, |
@@ -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<MeasureDto> previousMeasure = projectComponent.getMeasureRepository().findPrevious(CoreMetrics.QUALITY_PROFILES); | |||
if (!previousMeasure.isPresent()) { | |||
// first analysis -> do not generate events | |||
return; | |||
} | |||
// Load current profiles | |||
Map<String, QualityProfile> previousProfiles = QPMeasureData.fromJson(previousMeasure.get().getData()).getProfilesByKey(); | |||
Optional<BatchReport.Measure> currentMeasure = projectComponent.getMeasureRepository().findCurrent(CoreMetrics.QUALITY_PROFILES); | |||
if (!currentMeasure.isPresent()) { | |||
throw new IllegalStateException("Missing measure " + CoreMetrics.QUALITY_PROFILES + " for component " + projectComponent.getRef()); | |||
} | |||
Map<String, QualityProfile> currentProfiles = QPMeasureData.fromJson(currentMeasure.get().getStringValue()).getProfilesByKey(); | |||
detectNewOrUpdatedProfiles(projectComponent, previousProfiles, currentProfiles); | |||
detectNoMoreUsedProfiles(projectComponent, previousProfiles, currentProfiles); | |||
} | |||
private void detectNoMoreUsedProfiles(Component context, Map<String, QualityProfile> previousProfiles, Map<String, QualityProfile> currentProfiles) { | |||
for (QualityProfile previousProfile : previousProfiles.values()) { | |||
if (!currentProfiles.containsKey(previousProfile.getQpKey())) { | |||
markAsRemoved(context, previousProfile); | |||
} | |||
} | |||
} | |||
private void detectNewOrUpdatedProfiles(Component component, Map<String, QualityProfile> previousProfiles, Map<String, QualityProfile> 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> 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"; | |||
} | |||
} |
@@ -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 |
@@ -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.<MeasureDto>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.<BatchReport.Measure>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<Event> 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<Event> 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<Event> 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<Event> 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<Event> 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.<Language>absent()); | |||
} | |||
private void mockNoLanguageInRepository() { | |||
when(languageRepository.find(anyString())).thenReturn(Optional.<Language>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<QualityProfile> qualityProfiles = qps == null ? Collections.<QualityProfile>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<Event> events = new HashSet<>(); | |||
@Override | |||
public void add(Event event) { | |||
events.add(event); | |||
} | |||
@Override | |||
public Iterable<Event> 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; | |||
} | |||
} | |||
} |