import org.sonar.server.issue.TransitionService;
import org.sonar.server.issue.workflow.FunctionExecutor;
import org.sonar.server.issue.workflow.IssueWorkflow;
+import org.sonar.server.qualitygate.LiveQualityGateFactoryImpl;
import org.sonar.server.qualitygate.changeevent.IssueChangeTriggerImpl;
import org.sonar.server.qualitygate.changeevent.QGChangeEventListenersImpl;
import org.sonar.server.settings.ProjectConfigurationLoaderImpl;
ChangelogAction.class,
BulkChangeAction.class,
ProjectConfigurationLoaderImpl.class,
+ LiveQualityGateFactoryImpl.class,
IssueChangeTriggerImpl.class,
WebhookQGChangeEventListener.class,
QGChangeEventListenersImpl.class);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.qualitygate;
+
+import org.sonar.db.component.ComponentDto;
+
+public interface LiveQualityGateFactory {
+ EvaluatedQualityGate buildForShortLivedBranch(ComponentDto componentDto);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.qualitygate;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import org.elasticsearch.action.search.SearchResponse;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.System2;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.qualitygate.QualityGateConditionDto;
+import org.sonar.server.es.Facets;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.issue.IssueQuery;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.rule.index.RuleIndex;
+
+import static java.lang.String.format;
+import static java.lang.String.valueOf;
+import static java.util.Collections.singletonList;
+import static org.sonar.core.util.stream.MoreCollectors.toSet;
+
+public class LiveQualityGateFactoryImpl implements LiveQualityGateFactory {
+
+ private final IssueIndex issueIndex;
+ private final System2 system2;
+
+ public LiveQualityGateFactoryImpl(IssueIndex issueIndex, System2 system2) {
+ this.issueIndex = issueIndex;
+ this.system2 = system2;
+ }
+
+ @Override
+ public EvaluatedQualityGate buildForShortLivedBranch(ComponentDto componentDto) {
+ return createQualityGate(componentDto, issueIndex);
+ }
+
+ private EvaluatedQualityGate createQualityGate(ComponentDto project, IssueIndex issueIndex) {
+ SearchResponse searchResponse = issueIndex.search(IssueQuery.builder()
+ .projectUuids(singletonList(project.getMainBranchProjectUuid()))
+ .branchUuid(project.uuid())
+ .mainBranch(false)
+ .resolved(false)
+ .checkAuthorization(false)
+ .build(),
+ new SearchOptions().addFacets(RuleIndex.FACET_TYPES));
+ LinkedHashMap<String, Long> typeFacet = new Facets(searchResponse, system2.getDefaultTimeZone())
+ .get(RuleIndex.FACET_TYPES);
+
+ EvaluatedQualityGate.Builder builder = EvaluatedQualityGate.newBuilder();
+ Set<Condition> conditions = ShortLivingBranchQualityGate.CONDITIONS.stream()
+ .map(c -> {
+ long measure = getMeasure(typeFacet, c);
+ EvaluatedCondition.EvaluationStatus status = measure > 0 ? EvaluatedCondition.EvaluationStatus.ERROR : EvaluatedCondition.EvaluationStatus.OK;
+ Condition condition = new Condition(c.getMetricKey(), toOperator(c), c.getErrorThreshold(), c.getWarnThreshold(), c.isOnLeak());
+ builder.addCondition(condition, status, valueOf(measure));
+ return condition;
+ })
+ .collect(toSet(ShortLivingBranchQualityGate.CONDITIONS.size()));
+ builder
+ .setQualityGate(
+ new org.sonar.server.qualitygate.QualityGate(
+ valueOf(ShortLivingBranchQualityGate.ID),
+ ShortLivingBranchQualityGate.NAME,
+ conditions))
+ .setStatus(qgStatusFrom(builder.getEvaluatedConditions()));
+
+ return builder.build();
+ }
+
+ private static Condition.Operator toOperator(ShortLivingBranchQualityGate.Condition c) {
+ String operator = c.getOperator();
+ switch (operator) {
+ case QualityGateConditionDto.OPERATOR_GREATER_THAN:
+ return Condition.Operator.GREATER_THAN;
+ case QualityGateConditionDto.OPERATOR_LESS_THAN:
+ return Condition.Operator.LESS_THAN;
+ case QualityGateConditionDto.OPERATOR_EQUALS:
+ return Condition.Operator.EQUALS;
+ case QualityGateConditionDto.OPERATOR_NOT_EQUALS:
+ return Condition.Operator.NOT_EQUALS;
+ default:
+ throw new IllegalArgumentException(format("Unsupported Condition operator '%s'", operator));
+ }
+ }
+
+ private static EvaluatedQualityGate.Status qgStatusFrom(Set<EvaluatedCondition> conditions) {
+ if (conditions.stream().anyMatch(c -> c.getStatus() == EvaluatedCondition.EvaluationStatus.ERROR)) {
+ return EvaluatedQualityGate.Status.ERROR;
+ }
+ return EvaluatedQualityGate.Status.OK;
+ }
+
+ private static long getMeasure(LinkedHashMap<String, Long> typeFacet, ShortLivingBranchQualityGate.Condition c) {
+ String metricKey = c.getMetricKey();
+ switch (metricKey) {
+ case CoreMetrics.BUGS_KEY:
+ return getValueForRuleType(typeFacet, RuleType.BUG);
+ case CoreMetrics.VULNERABILITIES_KEY:
+ return getValueForRuleType(typeFacet, RuleType.VULNERABILITY);
+ case CoreMetrics.CODE_SMELLS_KEY:
+ return getValueForRuleType(typeFacet, RuleType.CODE_SMELL);
+ default:
+ throw new IllegalArgumentException(format("Unsupported metric key '%s' in hardcoded quality gate", metricKey));
+ }
+ }
+
+ private static long getValueForRuleType(Map<String, Long> facet, RuleType ruleType) {
+ Long res = facet.get(ruleType.name());
+ if (res == null) {
+ return 0L;
+ }
+ return res;
+ }
+}
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
+import org.sonar.server.qualitygate.LiveQualityGateFactory;
import org.sonar.server.settings.ProjectConfigurationLoader;
import static org.sonar.core.util.stream.MoreCollectors.toSet;
private final DbClient dbClient;
private final ProjectConfigurationLoader projectConfigurationLoader;
private final QGChangeEventListeners qgEventListeners;
+ private final LiveQualityGateFactory liveQualityGateFactory;
- public IssueChangeTriggerImpl(DbClient dbClient, ProjectConfigurationLoader projectConfigurationLoader, QGChangeEventListeners qgEventListeners) {
+ public IssueChangeTriggerImpl(DbClient dbClient, ProjectConfigurationLoader projectConfigurationLoader,
+ QGChangeEventListeners qgEventListeners, LiveQualityGateFactory liveQualityGateFactory) {
this.dbClient = dbClient;
this.projectConfigurationLoader = projectConfigurationLoader;
this.qgEventListeners = qgEventListeners;
+ this.liveQualityGateFactory = liveQualityGateFactory;
}
@Override
return;
}
- callWebHook(issueChangeData);
+ broadcastToListeners(issueChangeData);
}
private static boolean isRelevant(IssueChange issueChange) {
return MEANINGFUL_TRANSITIONS.contains(transitionKey);
}
- private void callWebHook(IssueChangeData issueChangeData) {
+ private void broadcastToListeners(IssueChangeData issueChangeData) {
try (DbSession dbSession = dbClient.openSession(false)) {
Map<String, ComponentDto> branchesByUuid = getBranchComponents(dbSession, issueChangeData);
if (branchesByUuid.isEmpty()) {
if (branch != null && analysis != null) {
Configuration configuration = configurationByUuid.get(shortBranch.getUuid());
- return new QGChangeEvent(branch, shortBranch, analysis, configuration);
+ return new QGChangeEvent(branch, shortBranch, analysis, configuration,
+ () -> Optional.of(liveQualityGateFactory.buildForShortLivedBranch(branch)));
}
return null;
})
*/
package org.sonar.server.qualitygate.changeevent;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
+import java.util.Optional;
+import java.util.function.Supplier;
+import javax.annotation.concurrent.Immutable;
import org.sonar.api.config.Configuration;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
+import static java.util.Objects.requireNonNull;
+
+@Immutable
public class QGChangeEvent {
private final ComponentDto project;
private final BranchDto branch;
private final SnapshotDto analysis;
private final Configuration projectConfiguration;
+ private final Supplier<Optional<EvaluatedQualityGate>> qualityGateSupplier;
- public QGChangeEvent(ComponentDto project, BranchDto branch, SnapshotDto analysis, Configuration projectConfiguration) {
- this.branch = branch;
- this.project = project;
- this.analysis = analysis;
- this.projectConfiguration = projectConfiguration;
+ public QGChangeEvent(ComponentDto project, BranchDto branch, SnapshotDto analysis, Configuration projectConfiguration,
+ Supplier<Optional<EvaluatedQualityGate>> qualityGateSupplier) {
+ this.project = requireNonNull(project, "project can't be null");
+ this.branch = requireNonNull(branch, "branch can't be null");
+ this.analysis = requireNonNull(analysis, "analysis can't be null");
+ this.projectConfiguration = requireNonNull(projectConfiguration, "projectConfiguration can't be null");
+ this.qualityGateSupplier = requireNonNull(qualityGateSupplier, "qualityGateSupplier can't be null");
}
public BranchDto getBranch() {
return projectConfiguration;
}
+ public Supplier<Optional<EvaluatedQualityGate>> getQualityGateSupplier() {
+ return qualityGateSupplier;
+ }
+
@Override
public String toString() {
return "QGChangeEvent{" +
- "branch=" + toString(branch) +
- ", project=" + toString(project) +
+ "project=" + toString(project) +
+ ", branch=" + toString(branch) +
", analysis=" + toString(analysis) +
", projectConfiguration=" + projectConfiguration +
+ ", qualityGateSupplier=" + qualityGateSupplier +
'}';
}
- @CheckForNull
- private static String toString(@Nullable BranchDto shortBranch) {
- if (shortBranch == null) {
- return null;
- }
- return shortBranch.getBranchType() + ":" + shortBranch.getUuid() + ":" + shortBranch.getProjectUuid() + ":" + shortBranch.getMergeBranchUuid();
+ private static String toString(ComponentDto project) {
+ return project.uuid() + ":" + project.getKey();
}
- @CheckForNull
- private static String toString(@Nullable ComponentDto shortBranchComponent) {
- if (shortBranchComponent == null) {
- return null;
- }
- return shortBranchComponent.uuid() + ":" + shortBranchComponent.getKey();
+ private static String toString(BranchDto branch) {
+ return branch.getBranchType() + ":" + branch.getUuid() + ":" + branch.getProjectUuid() + ":" + branch.getMergeBranchUuid();
}
- @CheckForNull
- private static String toString(@Nullable SnapshotDto latestAnalysis) {
- if (latestAnalysis == null) {
- return null;
- }
- return latestAnalysis.getUuid() + ":" + latestAnalysis.getCreatedAt();
+ private static String toString(SnapshotDto analysis) {
+ return analysis.getUuid() + ":" + analysis.getCreatedAt();
}
}
*/
package org.sonar.server.qualitygate.changeevent;
+import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
@Override
public void broadcast(Trigger trigger, Collection<QGChangeEvent> changeEvents) {
+ if (changeEvents.isEmpty()) {
+ return;
+ }
+
try {
- Arrays.stream(listeners).forEach(listener -> broadcastTo(trigger, changeEvents, listener));
+ List<QGChangeEvent> immutableChangeEvents = ImmutableList.copyOf(changeEvents);
+ Arrays.stream(listeners).forEach(listener -> broadcastTo(trigger, immutableChangeEvents, listener));
} catch (Error e) {
LOG.warn(format("Broadcasting to listeners failed for %s events", changeEvents.size()), e);
}
}
- private void broadcastTo(Trigger trigger, Collection<QGChangeEvent> changeEvents, QGChangeEventListener listener) {
+ private static void broadcastTo(Trigger trigger, Collection<QGChangeEvent> changeEvents, QGChangeEventListener listener) {
try {
- LOG.debug("calling onChange() on listener {} for events {}...", listener.getClass().getName(), changeEvents);
+ LOG.trace("calling onChange() on listener {} for events {}...", listener.getClass().getName(), changeEvents);
listener.onChanges(trigger, changeEvents);
} catch (Exception e) {
LOG.warn(format("onChange() call failed on listener %s for events %s", listener.getClass().getName(), changeEvents), e);
package org.sonar.server.webhook;
import java.util.Collection;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.stream.Collectors;
-import org.elasticsearch.action.search.SearchResponse;
-import org.sonar.api.measures.CoreMetrics;
-import org.sonar.api.rules.RuleType;
-import org.sonar.api.utils.System2;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
-import org.sonar.db.qualitygate.QualityGateConditionDto;
-import org.sonar.server.es.Facets;
-import org.sonar.server.es.SearchOptions;
-import org.sonar.server.issue.IssueQuery;
-import org.sonar.server.issue.index.IssueIndex;
-import org.sonar.server.qualitygate.Condition;
-import org.sonar.server.qualitygate.EvaluatedCondition;
-import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus;
-import org.sonar.server.qualitygate.EvaluatedQualityGate;
-import org.sonar.server.qualitygate.ShortLivingBranchQualityGate;
import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
import org.sonar.server.qualitygate.changeevent.QGChangeEventListener;
import org.sonar.server.qualitygate.changeevent.Trigger;
-import org.sonar.server.rule.index.RuleIndex;
-import org.sonar.server.settings.ProjectConfigurationLoader;
-import org.sonar.server.webhook.Analysis;
-import org.sonar.server.webhook.Branch;
-import org.sonar.server.webhook.Project;
-import org.sonar.server.webhook.ProjectAnalysis;
-import org.sonar.server.webhook.WebHooks;
-import org.sonar.server.webhook.WebhookPayload;
-import org.sonar.server.webhook.WebhookPayloadFactory;
-
-import static java.lang.String.format;
-import static java.lang.String.valueOf;
-import static java.util.Collections.singletonList;
-import static org.sonar.core.util.stream.MoreCollectors.toSet;
public class WebhookQGChangeEventListener implements QGChangeEventListener {
private final WebHooks webhooks;
private final WebhookPayloadFactory webhookPayloadFactory;
- private final IssueIndex issueIndex;
private final DbClient dbClient;
- private final System2 system2;
- public WebhookQGChangeEventListener(WebHooks webhooks, WebhookPayloadFactory webhookPayloadFactory, IssueIndex issueIndex, DbClient dbClient, System2 system2) {
+ public WebhookQGChangeEventListener(WebHooks webhooks, WebhookPayloadFactory webhookPayloadFactory, DbClient dbClient) {
this.webhooks = webhooks;
this.webhookPayloadFactory = webhookPayloadFactory;
- this.issueIndex = issueIndex;
this.dbClient = dbClient;
- this.system2 = system2;
}
@Override
webhooks.sendProjectAnalysisUpdate(
event.getProjectConfiguration(),
new WebHooks.Analysis(event.getBranch().getUuid(), event.getAnalysis().getUuid(), null),
- () -> buildWebHookPayload(dbSession, event.getProject(), event.getBranch(), event.getAnalysis()));
+ () -> buildWebHookPayload(dbSession, event));
}
- private WebhookPayload buildWebHookPayload(DbSession dbSession, ComponentDto branch, BranchDto shortBranch, SnapshotDto analysis) {
+ private WebhookPayload buildWebHookPayload(DbSession dbSession, QGChangeEvent event) {
+ ComponentDto branch = event.getProject();
+ BranchDto shortBranch = event.getBranch();
+ SnapshotDto analysis = event.getAnalysis();
Map<String, String> analysisProperties = dbClient.analysisPropertiesDao().selectBySnapshotUuid(dbSession, analysis.getUuid())
.stream()
.collect(Collectors.toMap(AnalysisPropertyDto::getKey, AnalysisPropertyDto::getValue));
null,
new Analysis(analysis.getUuid(), analysis.getCreatedAt()),
new Branch(false, shortBranch.getKey(), Branch.Type.SHORT),
- createQualityGate(branch, issueIndex),
+ event.getQualityGateSupplier().get().orElse(null),
null,
analysisProperties);
return webhookPayloadFactory.create(projectAnalysis);
}
- private EvaluatedQualityGate createQualityGate(ComponentDto branch, IssueIndex issueIndex) {
- SearchResponse searchResponse = issueIndex.search(IssueQuery.builder()
- .projectUuids(singletonList(branch.getMainBranchProjectUuid()))
- .branchUuid(branch.uuid())
- .mainBranch(false)
- .resolved(false)
- .checkAuthorization(false)
- .build(),
- new SearchOptions().addFacets(RuleIndex.FACET_TYPES));
- LinkedHashMap<String, Long> typeFacet = new Facets(searchResponse, system2.getDefaultTimeZone())
- .get(RuleIndex.FACET_TYPES);
-
- EvaluatedQualityGate.Builder builder = EvaluatedQualityGate.newBuilder();
- Set<Condition> conditions = ShortLivingBranchQualityGate.CONDITIONS.stream()
- .map(c -> {
- long measure = getMeasure(typeFacet, c);
- EvaluationStatus status = measure > 0 ? EvaluationStatus.ERROR : EvaluationStatus.OK;
- Condition condition = new Condition(c.getMetricKey(), toOperator(c), c.getErrorThreshold(), c.getWarnThreshold(), c.isOnLeak());
- builder.addCondition(condition, status, valueOf(measure));
- return condition;
- })
- .collect(toSet(ShortLivingBranchQualityGate.CONDITIONS.size()));
- builder
- .setQualityGate(
- new org.sonar.server.qualitygate.QualityGate(
- valueOf(ShortLivingBranchQualityGate.ID),
- ShortLivingBranchQualityGate.NAME,
- conditions))
- .setStatus(qgStatusFrom(builder.getEvaluatedConditions()));
-
- return builder.build();
- }
-
- private static Condition.Operator toOperator(ShortLivingBranchQualityGate.Condition c) {
- String operator = c.getOperator();
- switch (operator) {
- case QualityGateConditionDto.OPERATOR_GREATER_THAN:
- return Condition.Operator.GREATER_THAN;
- case QualityGateConditionDto.OPERATOR_LESS_THAN:
- return Condition.Operator.LESS_THAN;
- case QualityGateConditionDto.OPERATOR_EQUALS:
- return Condition.Operator.EQUALS;
- case QualityGateConditionDto.OPERATOR_NOT_EQUALS:
- return Condition.Operator.NOT_EQUALS;
- default:
- throw new IllegalArgumentException(format("Unsupported Condition operator '%s'", operator));
- }
- }
-
- private static EvaluatedQualityGate.Status qgStatusFrom(Set<EvaluatedCondition> conditions) {
- if (conditions.stream().anyMatch(c -> c.getStatus() == EvaluationStatus.ERROR)) {
- return EvaluatedQualityGate.Status.ERROR;
- }
- return EvaluatedQualityGate.Status.OK;
- }
-
- private static long getMeasure(LinkedHashMap<String, Long> typeFacet, ShortLivingBranchQualityGate.Condition c) {
- String metricKey = c.getMetricKey();
- switch (metricKey) {
- case CoreMetrics.BUGS_KEY:
- return getValueForRuleType(typeFacet, RuleType.BUG);
- case CoreMetrics.VULNERABILITIES_KEY:
- return getValueForRuleType(typeFacet, RuleType.VULNERABILITY);
- case CoreMetrics.CODE_SMELLS_KEY:
- return getValueForRuleType(typeFacet, RuleType.CODE_SMELL);
- default:
- throw new IllegalArgumentException(format("Unsupported metric key '%s' in hardcoded quality gate", metricKey));
- }
- }
-
- private static long getValueForRuleType(Map<String, Long> facet, RuleType ruleType) {
- Long res = facet.get(ruleType.name());
- if (res == null) {
- return 0L;
- }
- return res;
- }
}
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new IssueWsModule().configure(container);
- assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 32);
+ assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 33);
}
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.qualitygate;
+
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Random;
+import java.util.stream.IntStream;
+import javax.annotation.Nullable;
+import org.assertj.core.groups.Tuple;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleTesting;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.issue.index.IssueIndexDefinition;
+import org.sonar.server.issue.index.IssueIndexer;
+import org.sonar.server.issue.index.IssueIteratorFactory;
+import org.sonar.server.permission.index.AuthorizationTypeSupport;
+import org.sonar.server.tester.UserSessionRule;
+
+import static java.lang.String.valueOf;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.sonar.api.measures.CoreMetrics.BUGS_KEY;
+import static org.sonar.api.measures.CoreMetrics.CODE_SMELLS_KEY;
+import static org.sonar.api.measures.CoreMetrics.VULNERABILITIES_KEY;
+import static org.sonar.db.component.ComponentTesting.newBranchDto;
+
+public class LiveQualityGateFactoryImplTest {
+ private static final List<String> OPEN_STATUSES = ImmutableList.of(Issue.STATUS_OPEN, Issue.STATUS_CONFIRMED);
+ private static final List<String> NON_OPEN_STATUSES = Issue.STATUSES.stream().filter(OPEN_STATUSES::contains).collect(MoreCollectors.toList());
+
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+ @Rule
+ public EsTester esTester = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig()));
+ @Rule
+ public UserSessionRule userSessionRule = UserSessionRule.standalone();
+
+ private Random random = new Random();
+ private String randomOpenStatus = OPEN_STATUSES.get(random.nextInt(OPEN_STATUSES.size()));
+ private String randomNonOpenStatus = NON_OPEN_STATUSES.get(random.nextInt(NON_OPEN_STATUSES.size()));
+ private String randomResolution = Issue.RESOLUTIONS.get(random.nextInt(Issue.RESOLUTIONS.size()));
+ private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbTester.getDbClient(), new IssueIteratorFactory(dbTester.getDbClient()));
+ private IssueIndex issueIndex = new IssueIndex(esTester.client(), System2.INSTANCE, userSessionRule, new AuthorizationTypeSupport(userSessionRule));
+
+ private LiveQualityGateFactoryImpl underTest = new LiveQualityGateFactoryImpl(issueIndex, System2.INSTANCE);
+
+ @Test
+ public void compute_QG_ok_if_there_is_no_issue_in_index_ignoring_permissions() {
+ OrganizationDto organization = dbTester.organizations().insert();
+ ComponentDto project = insertPrivateBranch(organization, BranchType.SHORT);
+
+ EvaluatedQualityGate qualityGate = underTest.buildForShortLivedBranch(project);
+
+ assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.OK);
+ assertThat(qualityGate.getEvaluatedConditions())
+ .extracting(EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
+ .containsOnly(tuple(EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")));
+ }
+
+ @Test
+ public void computes_QG_error_if_there_is_one_unresolved_bug_issue_in_index_ignoring_permissions() {
+ int unresolvedIssues = 1 + random.nextInt(10);
+
+ computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
+ unresolvedIssues,
+ RuleType.BUG,
+ tuple(BUGS_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))),
+ tuple(VULNERABILITIES_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")),
+ tuple(CODE_SMELLS_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")));
+ }
+
+ @Test
+ public void computes_QG_error_if_there_is_one_unresolved_vulnerability_issue_in_index_ignoring_permissions() {
+ int unresolvedIssues = 1 + random.nextInt(10);
+
+ computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
+ unresolvedIssues,
+ RuleType.VULNERABILITY,
+ tuple(BUGS_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")),
+ tuple(VULNERABILITIES_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))),
+ tuple(CODE_SMELLS_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")));
+ }
+
+ @Test
+ public void computes_QG_error_if_there_is_one_unresolved_codeSmell_issue_in_index_ignoring_permissions() {
+ int unresolvedIssues = 1 + random.nextInt(10);
+
+ computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
+ unresolvedIssues,
+ RuleType.CODE_SMELL,
+ tuple(BUGS_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")),
+ tuple(VULNERABILITIES_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")),
+ tuple(CODE_SMELLS_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))));
+ }
+
+ private void computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
+ int unresolvedIssues, RuleType ruleType, Tuple... expectedQGConditions) {
+ OrganizationDto organization = dbTester.organizations().insert();
+ ComponentDto project = insertPrivateBranch(organization, BranchType.SHORT);
+ IntStream.range(0, unresolvedIssues).forEach(i -> insertIssue(project, ruleType, randomOpenStatus, null));
+ IntStream.range(0, random.nextInt(10)).forEach(i -> insertIssue(project, ruleType, randomNonOpenStatus, randomResolution));
+ indexIssues(project);
+
+ EvaluatedQualityGate qualityGate = underTest.buildForShortLivedBranch(project);
+
+ assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR);
+ assertThat(qualityGate.getEvaluatedConditions())
+ .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
+ .containsOnly(expectedQGConditions);
+ }
+
+ @Test
+ public void computes_QG_error_with_all_failing_conditions() {
+ OrganizationDto organization = dbTester.organizations().insert();
+ ComponentDto project = insertPrivateBranch(organization, BranchType.SHORT);
+ int unresolvedBugs = 1 + random.nextInt(10);
+ int unresolvedVulnerabilities = 1 + random.nextInt(10);
+ int unresolvedCodeSmells = 1 + random.nextInt(10);
+ IntStream.range(0, unresolvedBugs).forEach(i -> insertIssue(project, RuleType.BUG, randomOpenStatus, null));
+ IntStream.range(0, unresolvedVulnerabilities).forEach(i -> insertIssue(project, RuleType.VULNERABILITY, randomOpenStatus, null));
+ IntStream.range(0, unresolvedCodeSmells).forEach(i -> insertIssue(project, RuleType.CODE_SMELL, randomOpenStatus, null));
+ indexIssues(project);
+
+ EvaluatedQualityGate qualityGate = underTest.buildForShortLivedBranch(project);
+
+ assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR);
+ assertThat(qualityGate.getEvaluatedConditions())
+ .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
+ .containsOnly(
+ Tuple.tuple(BUGS_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedBugs))),
+ Tuple.tuple(VULNERABILITIES_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedVulnerabilities))),
+ Tuple.tuple(CODE_SMELLS_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedCodeSmells))));
+ }
+
+ private void indexIssues(ComponentDto project) {
+ issueIndexer.indexOnAnalysis(project.uuid());
+ }
+
+ private void insertIssue(ComponentDto component, RuleType ruleType, String status, @Nullable String resolution) {
+ RuleDefinitionDto rule = RuleTesting.newRule();
+ dbTester.rules().insert(rule);
+ dbTester.commit();
+ dbTester.issues().insert(rule, component, component, i -> i.setType(ruleType).setStatus(status).setResolution(resolution));
+ dbTester.commit();
+ }
+
+ private ComponentDto insertPrivateBranch(OrganizationDto organization, BranchType branchType) {
+ ComponentDto project = dbTester.components().insertPrivateProject(organization);
+ BranchDto branchDto = newBranchDto(project.projectUuid(), branchType)
+ .setKey("foo");
+ return dbTester.components().insertProjectBranch(project, branchDto);
+ }
+
+}
import org.sonar.db.component.SnapshotDto;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.organization.OrganizationDto;
+import org.sonar.server.qualitygate.LiveQualityGateFactory;
import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger.IssueChange;
import org.sonar.server.settings.ProjectConfigurationLoader;
import org.sonar.server.tester.UserSessionRule;
private DbClient spiedOnDbClient = Mockito.spy(dbClient);
private ProjectConfigurationLoader projectConfigurationLoader = mock(ProjectConfigurationLoader.class);
private QGChangeEventListeners qgChangeEventListeners = mock(QGChangeEventListeners.class);
- private IssueChangeTriggerImpl underTest = new IssueChangeTriggerImpl(spiedOnDbClient, projectConfigurationLoader, qgChangeEventListeners);
+ private LiveQualityGateFactory liveQualityGateFactory = mock(LiveQualityGateFactory.class);
+ private IssueChangeTriggerImpl underTest = new IssueChangeTriggerImpl(spiedOnDbClient, projectConfigurationLoader, qgChangeEventListeners, liveQualityGateFactory);
private DbClient mockedDbClient = mock(DbClient.class);
- private IssueChangeTriggerImpl mockedUnderTest = new IssueChangeTriggerImpl(mockedDbClient, projectConfigurationLoader, qgChangeEventListeners);
+ private IssueChangeTriggerImpl mockedUnderTest = new IssueChangeTriggerImpl(mockedDbClient, projectConfigurationLoader, qgChangeEventListeners, liveQualityGateFactory);
@Test
public void on_type_change_has_no_effect_if_SearchResponseData_has_no_issue() {
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.qualitygate.changeevent;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+public class QGChangeEventListenersImplTest {
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ private QGChangeEventListener listener1 = mock(QGChangeEventListener.class);
+ private QGChangeEventListener listener2 = mock(QGChangeEventListener.class);
+ private QGChangeEventListener listener3 = mock(QGChangeEventListener.class);
+ private InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
+ private List<QGChangeEvent> threeChangeEvents = Arrays.asList(mock(QGChangeEvent.class), mock(QGChangeEvent.class));
+
+ private QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl(new QGChangeEventListener[] {listener1, listener2, listener3});
+
+ @Test
+ public void isEmpty_returns_true_for_constructor_without_argument() {
+ QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl();
+
+ assertThat(underTest.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void isEmpty_returns_false_for_constructor_with_one_argument() {
+ QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl(new QGChangeEventListener[] {listener2});
+
+ assertThat(underTest.isEmpty()).isFalse();
+ }
+
+ @Test
+ public void isEmpty_returns_false_for_constructor_with_multiple_arguments() {
+ QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl(new QGChangeEventListener[] {listener2, listener3});
+
+ assertThat(underTest.isEmpty()).isFalse();
+ }
+
+ @Test
+ public void no_effect_when_no_changeEvent() {
+ underTest.broadcast(Trigger.ISSUE_CHANGE, Collections.emptySet());
+
+ verifyZeroInteractions(listener1, listener2, listener3);
+ }
+
+ @Test
+ public void broadcast_passes_Trigger_and_collection_to_all_listeners_in_order_of_addition_to_constructor() {
+ underTest.broadcast(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+ inOrder.verify(listener1).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+ inOrder.verify(listener2).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+ inOrder.verify(listener3).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void broadcast_calls_all_listeners_even_if_one_throws_an_exception() {
+ QGChangeEventListener failingListener = new QGChangeEventListener[] {listener1, listener2, listener3}[new Random().nextInt(3)];
+ doThrow(new RuntimeException("Faking an exception thrown by onChanges"))
+ .when(failingListener)
+ .onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+ underTest.broadcast(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+ inOrder.verify(listener1).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+ inOrder.verify(listener2).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+ inOrder.verify(listener3).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+ inOrder.verifyNoMoreInteractions();
+ assertThat(logTester.logs()).hasSize(4);
+ assertThat(logTester.logs(LoggerLevel.WARN)).hasSize(1);
+ }
+
+ @Test
+ public void broadcast_stops_calling_listeners_when_one_throws_an_ERROR() {
+ doThrow(new Error("Faking an error thrown by a listener"))
+ .when(listener2)
+ .onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+ underTest.broadcast(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+ inOrder.verify(listener1).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+ inOrder.verify(listener2).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+ inOrder.verifyNoMoreInteractions();
+ assertThat(logTester.logs()).hasSize(3);
+ assertThat(logTester.logs(LoggerLevel.WARN)).hasSize(1);
+ }
+
+ @Test
+ public void broadcast_logs_each_listener_call_at_TRACE_level() {
+ underTest.broadcast(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+ assertThat(logTester.logs()).hasSize(3);
+ List<String> traceLogs = logTester.logs(LoggerLevel.TRACE);
+ assertThat(traceLogs).hasSize(3)
+ .containsOnly(
+ "calling onChange() on listener " + listener1.getClass().getName() + " for events " + threeChangeEvents.toString() + "...",
+ "calling onChange() on listener " + listener2.getClass().getName() + " for events " + threeChangeEvents.toString() + "...",
+ "calling onChange() on listener " + listener3.getClass().getName() + " for events " + threeChangeEvents.toString() + "...");
+ }
+
+ @Test
+ public void broadcast_passes_immutable_list_of_events() {
+ QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl(new QGChangeEventListener[] {listener1});
+
+ underTest.broadcast(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+ ArgumentCaptor<Collection> collectionCaptor = ArgumentCaptor.forClass(Collection.class);
+ verify(listener1).onChanges(eq(Trigger.ISSUE_CHANGE), collectionCaptor.capture());
+ assertThat(collectionCaptor.getValue()).isInstanceOf(ImmutableList.class);
+ }
+
+ @Test
+ public void no_effect_when_no_listener() {
+ QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl();
+
+ underTest.broadcast(Trigger.ISSUE_CHANGE, Collections.emptySet());
+
+ verifyZeroInteractions(listener1, listener2, listener3);
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.qualitygate.changeevent;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mockito;
+import org.sonar.api.config.Configuration;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class QGChangeEventTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private ComponentDto project = new ComponentDto()
+ .setDbKey("foo")
+ .setUuid("bar");
+ private BranchDto branch = new BranchDto()
+ .setBranchType(BranchType.SHORT)
+ .setUuid("bar")
+ .setProjectUuid("doh")
+ .setMergeBranchUuid("zop");
+ private SnapshotDto analysis = new SnapshotDto()
+ .setUuid("pto")
+ .setCreatedAt(8_999_999_765L);
+ private Configuration configuration = Mockito.mock(Configuration.class);
+ private Supplier<Optional<EvaluatedQualityGate>> supplier = Optional::empty;
+
+ @Test
+ public void constructor_fails_with_NPE_if_project_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("project can't be null");
+
+ new QGChangeEvent(null, branch, analysis, configuration, supplier);
+ }
+
+ @Test
+ public void constructor_fails_with_NPE_if_branch_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("branch can't be null");
+
+ new QGChangeEvent(project, null, analysis, configuration, supplier);
+ }
+
+ @Test
+ public void constructor_fails_with_NPE_if_analysis_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("analysis can't be null");
+
+ new QGChangeEvent(project, branch, null, configuration, supplier);
+ }
+
+ @Test
+ public void constructor_fails_with_NPE_if_configuration_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("projectConfiguration can't be null");
+
+ new QGChangeEvent(project, branch, analysis, null, supplier);
+ }
+
+ @Test
+ public void constructor_fails_with_NPE_if_supplier_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("qualityGateSupplier can't be null");
+
+ new QGChangeEvent(project, branch, analysis, configuration, null);
+ }
+
+ @Test
+ public void verify_getters() {
+ QGChangeEvent underTest = new QGChangeEvent(project, branch, analysis, configuration, supplier);
+
+ assertThat(underTest.getProject()).isSameAs(project);
+ assertThat(underTest.getBranch()).isSameAs(branch);
+ assertThat(underTest.getAnalysis()).isSameAs(analysis);
+ assertThat(underTest.getProjectConfiguration()).isSameAs(configuration);
+ assertThat(underTest.getQualityGateSupplier()).isSameAs(supplier);
+ }
+
+ @Test
+ public void overrides_toString() {
+ QGChangeEvent underTest = new QGChangeEvent(project, branch, analysis, configuration, supplier);
+
+ assertThat(underTest.toString())
+ .isEqualTo("QGChangeEvent{project=bar:foo, branch=SHORT:bar:doh:zop, analysis=pto:8999999765, projectConfiguration=" + configuration.toString() +
+ ", qualityGateSupplier=" + supplier + "}");
+
+ }
+}
package org.sonar.server.webhook;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.Random;
import java.util.function.Supplier;
-import java.util.stream.IntStream;
import javax.annotation.Nullable;
-import org.assertj.core.groups.Tuple;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.sonar.api.config.Configuration;
-import org.sonar.api.config.internal.MapSettings;
-import org.sonar.api.issue.Issue;
-import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.System2;
import org.sonar.core.util.UuidFactoryFast;
-import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbTester;
import org.sonar.db.component.AnalysisPropertyDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
import org.sonar.db.organization.OrganizationDto;
-import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonar.db.rule.RuleTesting;
-import org.sonar.server.es.EsTester;
-import org.sonar.server.issue.index.IssueIndex;
-import org.sonar.server.issue.index.IssueIndexDefinition;
-import org.sonar.server.issue.index.IssueIndexer;
-import org.sonar.server.issue.index.IssueIteratorFactory;
-import org.sonar.server.permission.index.AuthorizationTypeSupport;
-import org.sonar.server.qualitygate.Condition;
-import org.sonar.server.qualitygate.EvaluatedCondition;
-import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus;
import org.sonar.server.qualitygate.EvaluatedQualityGate;
import org.sonar.server.qualitygate.QualityGate;
import org.sonar.server.qualitygate.ShortLivingBranchQualityGate;
import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
import org.sonar.server.qualitygate.changeevent.Trigger;
-import org.sonar.server.tester.UserSessionRule;
import static java.lang.String.valueOf;
+import static java.util.Collections.emptySet;
import static java.util.Collections.singletonList;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.groups.Tuple.tuple;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.sonar.api.measures.CoreMetrics.BUGS_KEY;
-import static org.sonar.api.measures.CoreMetrics.CODE_SMELLS_KEY;
-import static org.sonar.api.measures.CoreMetrics.VULNERABILITIES_KEY;
+import static org.mockito.Mockito.when;
import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
import static org.sonar.db.component.BranchType.LONG;
import static org.sonar.db.component.ComponentTesting.newBranchDto;
import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
-import static org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN;
public class WebhookQGChangeEventListenerTest {
- private static final List<String> OPEN_STATUSES = ImmutableList.of(Issue.STATUS_OPEN, Issue.STATUS_CONFIRMED);
- private static final List<String> NON_OPEN_STATUSES = Issue.STATUSES.stream().filter(OPEN_STATUSES::contains).collect(MoreCollectors.toList());
+
+ private static final EvaluatedQualityGate EVALUATED_QUALITY_GATE_1 = EvaluatedQualityGate.newBuilder()
+ .setQualityGate(new QualityGate(valueOf(ShortLivingBranchQualityGate.ID), ShortLivingBranchQualityGate.NAME, emptySet()))
+ .setStatus(EvaluatedQualityGate.Status.OK)
+ .build();
@Rule
public DbTester dbTester = DbTester.create(System2.INSTANCE);
- @Rule
- public EsTester esTester = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig()));
- @Rule
- public UserSessionRule userSessionRule = UserSessionRule.standalone();
private DbClient dbClient = dbTester.getDbClient();
- private Random random = new Random();
- private String randomResolution = Issue.RESOLUTIONS.get(random.nextInt(Issue.RESOLUTIONS.size()));
- private String randomOpenStatus = OPEN_STATUSES.get(random.nextInt(OPEN_STATUSES.size()));
- private String randomNonOpenStatus = NON_OPEN_STATUSES.get(random.nextInt(NON_OPEN_STATUSES.size()));
-
- private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbTester.getDbClient(), new IssueIteratorFactory(dbTester.getDbClient()));
private WebHooks webHooks = mock(WebHooks.class);
private WebhookPayloadFactory webhookPayloadFactory = mock(WebhookPayloadFactory.class);
- private IssueIndex issueIndex = new IssueIndex(esTester.client(), System2.INSTANCE, userSessionRule, new AuthorizationTypeSupport(userSessionRule));
private DbClient spiedOnDbClient = Mockito.spy(dbClient);
- private WebhookQGChangeEventListener underTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, issueIndex, spiedOnDbClient, System2.INSTANCE);
+ private WebhookQGChangeEventListener underTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, spiedOnDbClient);
private DbClient mockedDbClient = mock(DbClient.class);
- private IssueIndex spiedOnIssueIndex = Mockito.spy(issueIndex);
- private WebhookQGChangeEventListener mockedUnderTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, spiedOnIssueIndex, mockedDbClient, System2.INSTANCE);
+ private WebhookQGChangeEventListener mockedUnderTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, mockedDbClient);
@Test
public void onChanges_has_no_effect_if_changeEvents_is_empty() {
mockedUnderTest.onChanges(Trigger.ISSUE_CHANGE, Collections.emptyList());
- verifyZeroInteractions(webHooks, webhookPayloadFactory, spiedOnIssueIndex, mockedDbClient);
+ verifyZeroInteractions(webHooks, webhookPayloadFactory, mockedDbClient);
}
@Test
mockWebhookDisabled(configuration1, configuration2);
mockedUnderTest.onChanges(Trigger.ISSUE_CHANGE, ImmutableList.of(
- new QGChangeEvent(new ComponentDto(), new BranchDto(), new SnapshotDto(), configuration1),
- new QGChangeEvent(new ComponentDto(), new BranchDto(), new SnapshotDto(), configuration2)));
+ new QGChangeEvent(new ComponentDto(), new BranchDto(), new SnapshotDto(), configuration1, Optional::empty),
+ new QGChangeEvent(new ComponentDto(), new BranchDto(), new SnapshotDto(), configuration2, Optional::empty)));
verify(webHooks).isEnabled(configuration1);
verify(webHooks).isEnabled(configuration2);
- verifyZeroInteractions(webhookPayloadFactory, spiedOnIssueIndex, mockedDbClient);
+ verifyZeroInteractions(webhookPayloadFactory, mockedDbClient);
}
@Test
properties.put("sonar.analysis.test2", randomAlphanumeric(5000));
insertPropertiesFor(analysis.getUuid(), properties);
- underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration)));
+ underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration, EVALUATED_QUALITY_GATE_1)));
ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis);
- Condition condition1 = new Condition(BUGS_KEY, GREATER_THAN, "0", null, false);
- Condition condition2 = new Condition(VULNERABILITIES_KEY, GREATER_THAN, "0", null, false);
- Condition condition3 = new Condition(CODE_SMELLS_KEY, GREATER_THAN, "0", null, false);
assertThat(projectAnalysis).isEqualTo(
new ProjectAnalysis(
new Project(project.uuid(), project.getKey(), project.name()),
null,
new Analysis(analysis.getUuid(), analysis.getCreatedAt()),
new Branch(false, "foo", Branch.Type.SHORT),
- EvaluatedQualityGate.newBuilder()
- .setQualityGate(
- new QualityGate(
- valueOf(ShortLivingBranchQualityGate.ID),
- ShortLivingBranchQualityGate.NAME,
- ImmutableSet.of(condition1, condition2, condition3)))
- .setStatus(EvaluatedQualityGate.Status.OK)
- .addCondition(condition1, EvaluationStatus.OK, "0")
- .addCondition(condition2, EvaluationStatus.OK, "0")
- .addCondition(condition3, EvaluationStatus.OK, "0")
- .build(),
+ EVALUATED_QUALITY_GATE_1,
null,
properties));
}
underTest.onChanges(
Trigger.ISSUE_CHANGE,
ImmutableList.of(
- newQGChangeEvent(branch1, analysis1, configuration1),
- newQGChangeEvent(branch2, analysis2, configuration2)));
+ newQGChangeEvent(branch1, analysis1, configuration1, null),
+ newQGChangeEvent(branch2, analysis2, configuration2, EVALUATED_QUALITY_GATE_1)));
verifyWebhookNotCalled(branch1, analysis1, configuration1);
verifyWebhookCalled(branch2, analysis2, configuration2);
mockWebhookEnabled(configuration1, configuration2);
underTest.onChanges(Trigger.ISSUE_CHANGE, ImmutableList.of(
- newQGChangeEvent(mainBranch, analysis1, configuration1),
- newQGChangeEvent(longBranch, analysis2, configuration2)));
+ newQGChangeEvent(mainBranch, analysis1, configuration1, EVALUATED_QUALITY_GATE_1),
+ newQGChangeEvent(longBranch, analysis2, configuration2, null)));
verifyWebhookCalled(mainBranch, analysis1, configuration1);
verifyWebhookCalled(longBranch, analysis2, configuration2);
mockPayloadSupplierConsumedByWebhooks();
underTest.onChanges(Trigger.ISSUE_CHANGE, ImmutableList.of(
- newQGChangeEvent(branch1, analysis1, configuration1),
- newQGChangeEvent(branch1, analysis1, configuration1),
- newQGChangeEvent(branch1, analysis1, configuration1)));
+ newQGChangeEvent(branch1, analysis1, configuration1, null),
+ newQGChangeEvent(branch1, analysis1, configuration1, EVALUATED_QUALITY_GATE_1),
+ newQGChangeEvent(branch1, analysis1, configuration1, null)));
verify(webHooks, times(3)).isEnabled(configuration1);
verify(webHooks, times(3)).sendProjectAnalysisUpdate(
extractPayloadFactoryArguments(3);
}
- @Test
- public void compute_QG_ok_if_there_is_no_issue_in_index_ignoring_permissions() {
- OrganizationDto organization = dbTester.organizations().insert();
- ComponentAndBranch branch = insertPrivateBranch(organization, BranchType.SHORT);
- SnapshotDto analysis = insertAnalysisTask(branch);
- Configuration configuration = mock(Configuration.class);
- mockWebhookEnabled(configuration);
- mockPayloadSupplierConsumedByWebhooks();
-
- underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration)));
-
- ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis);
- EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get();
- assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.OK);
- assertThat(qualityGate.getEvaluatedConditions())
- .extracting(EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
- .containsOnly(tuple(EvaluationStatus.OK, Optional.of("0")));
- }
-
- @Test
- public void computes_QG_error_if_there_is_one_unresolved_bug_issue_in_index_ignoring_permissions() {
- int unresolvedIssues = 1 + random.nextInt(10);
-
- computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
- unresolvedIssues,
- RuleType.BUG,
- tuple(BUGS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))),
- tuple(VULNERABILITIES_KEY, EvaluationStatus.OK, Optional.of("0")),
- tuple(CODE_SMELLS_KEY, EvaluationStatus.OK, Optional.of("0")));
- }
-
- @Test
- public void computes_QG_error_if_there_is_one_unresolved_vulnerability_issue_in_index_ignoring_permissions() {
- int unresolvedIssues = 1 + random.nextInt(10);
-
- computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
- unresolvedIssues,
- RuleType.VULNERABILITY,
- tuple(BUGS_KEY, EvaluationStatus.OK, Optional.of("0")),
- tuple(VULNERABILITIES_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))),
- tuple(CODE_SMELLS_KEY, EvaluationStatus.OK, Optional.of("0")));
- }
-
- @Test
- public void computes_QG_error_if_there_is_one_unresolved_codeSmell_issue_in_index_ignoring_permissions() {
- int unresolvedIssues = 1 + random.nextInt(10);
-
- computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
- unresolvedIssues,
- RuleType.CODE_SMELL,
- tuple(BUGS_KEY, EvaluationStatus.OK, Optional.of("0")),
- tuple(VULNERABILITIES_KEY, EvaluationStatus.OK, Optional.of("0")),
- tuple(CODE_SMELLS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))));
- }
-
- private void computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
- int unresolvedIssues, RuleType ruleType, Tuple... expectedQGConditions) {
- OrganizationDto organization = dbTester.organizations().insert();
- ComponentAndBranch branch = insertPrivateBranch(organization, BranchType.SHORT);
- SnapshotDto analysis = insertAnalysisTask(branch);
- IntStream.range(0, unresolvedIssues).forEach(i -> insertIssue(branch, ruleType, randomOpenStatus, null));
- IntStream.range(0, random.nextInt(10)).forEach(i -> insertIssue(branch, ruleType, randomNonOpenStatus, randomResolution));
- indexIssues(branch);
- Configuration configuration = mock(Configuration.class);
- mockWebhookEnabled(configuration);
- mockPayloadSupplierConsumedByWebhooks();
-
- underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration)));
-
- ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis);
- EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get();
- assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR);
- assertThat(qualityGate.getEvaluatedConditions())
- .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
- .containsOnly(expectedQGConditions);
- }
-
- @Test
- public void computes_QG_error_with_all_failing_conditions() {
- OrganizationDto organization = dbTester.organizations().insert();
- ComponentAndBranch branch = insertPrivateBranch(organization, BranchType.SHORT);
- SnapshotDto analysis = insertAnalysisTask(branch);
- int unresolvedBugs = 1 + random.nextInt(10);
- int unresolvedVulnerabilities = 1 + random.nextInt(10);
- int unresolvedCodeSmells = 1 + random.nextInt(10);
- IntStream.range(0, unresolvedBugs).forEach(i -> insertIssue(branch, RuleType.BUG, randomOpenStatus, null));
- IntStream.range(0, unresolvedVulnerabilities).forEach(i -> insertIssue(branch, RuleType.VULNERABILITY, randomOpenStatus, null));
- IntStream.range(0, unresolvedCodeSmells).forEach(i -> insertIssue(branch, RuleType.CODE_SMELL, randomOpenStatus, null));
- indexIssues(branch);
- Configuration configuration = mock(Configuration.class);
- mockWebhookEnabled(configuration);
- mockPayloadSupplierConsumedByWebhooks();
-
- underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration)));
-
- ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis);
- EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get();
- assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR);
- assertThat(qualityGate.getEvaluatedConditions())
- .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
- .containsOnly(
- Tuple.tuple(BUGS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedBugs))),
- Tuple.tuple(VULNERABILITIES_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedVulnerabilities))),
- Tuple.tuple(CODE_SMELLS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedCodeSmells))));
- }
-
private void mockWebhookEnabled(Configuration... configurations) {
for (Configuration configuration : configurations) {
- Mockito.when(webHooks.isEnabled(configuration)).thenReturn(true);
+ when(webHooks.isEnabled(configuration)).thenReturn(true);
}
}
private void mockWebhookDisabled(Configuration... configurations) {
for (Configuration configuration : configurations) {
- Mockito.when(webHooks.isEnabled(configuration)).thenReturn(false);
+ when(webHooks.isEnabled(configuration)).thenReturn(false);
}
}
.sendProjectAnalysisUpdate(Matchers.any(Configuration.class), Matchers.any(), Matchers.any());
}
- private void insertIssue(ComponentAndBranch componentAndBranch, RuleType ruleType, String status, @Nullable String resolution) {
- ComponentDto component = componentAndBranch.component;
- RuleDefinitionDto rule = RuleTesting.newRule();
- dbTester.rules().insert(rule);
- dbTester.commit();
- dbTester.issues().insert(rule, component, component, i -> i.setType(ruleType).setStatus(status).setResolution(resolution));
- dbTester.commit();
- }
-
private void insertPropertiesFor(String snapshotUuid, Map<String, String> properties) {
List<AnalysisPropertyDto> analysisProperties = properties.entrySet().stream()
.map(entry -> new AnalysisPropertyDto()
return projectAnalysisCaptor.getAllValues();
}
- private void indexIssues(ComponentAndBranch componentAndBranch) {
- issueIndexer.indexOnAnalysis(componentAndBranch.uuid());
- }
-
private ComponentAndBranch insertPrivateBranch(OrganizationDto organization, BranchType branchType) {
ComponentDto project = dbTester.components().insertPrivateProject(organization);
BranchDto branchDto = newBranchDto(project.projectUuid(), branchType)
}
- private static QGChangeEvent newQGChangeEvent(ComponentAndBranch branch, SnapshotDto analysis, Configuration configuration) {
- return new QGChangeEvent(branch.component, branch.branch, analysis, configuration);
+ private static QGChangeEvent newQGChangeEvent(ComponentAndBranch branch, SnapshotDto analysis, Configuration configuration, @Nullable EvaluatedQualityGate evaluatedQualityGate) {
+ return new QGChangeEvent(branch.component, branch.branch, analysis, configuration, () -> Optional.ofNullable(evaluatedQualityGate));
}
}