From: Sébastien Lesaint Date: Fri, 17 Nov 2017 13:00:26 +0000 (+0100) Subject: SONAR-10085 include EvaluatedQualityGate in QGChangeEvent X-Git-Tag: 7.0-RC1~294 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=refs%2Fpull%2F2808%2Fhead;p=sonarqube.git SONAR-10085 include EvaluatedQualityGate in QGChangeEvent --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java index 2c750380e4d..5a0e17d044e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java @@ -28,6 +28,7 @@ import org.sonar.server.issue.ServerIssueStorage; 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; @@ -67,6 +68,7 @@ public class IssueWsModule extends Module { ChangelogAction.class, BulkChangeAction.class, ProjectConfigurationLoaderImpl.class, + LiveQualityGateFactoryImpl.class, IssueChangeTriggerImpl.class, WebhookQGChangeEventListener.class, QGChangeEventListenersImpl.class); diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactory.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactory.java new file mode 100644 index 00000000000..ec23ac97560 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactory.java @@ -0,0 +1,26 @@ +/* + * 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); +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImpl.java new file mode 100644 index 00000000000..b1186934d9c --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImpl.java @@ -0,0 +1,134 @@ +/* + * 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 typeFacet = new Facets(searchResponse, system2.getDefaultTimeZone()) + .get(RuleIndex.FACET_TYPES); + + EvaluatedQualityGate.Builder builder = EvaluatedQualityGate.newBuilder(); + Set 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 conditions) { + if (conditions.stream().anyMatch(c -> c.getStatus() == EvaluatedCondition.EvaluationStatus.ERROR)) { + return EvaluatedQualityGate.Status.ERROR; + } + return EvaluatedQualityGate.Status.OK; + } + + private static long getMeasure(LinkedHashMap 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 facet, RuleType ruleType) { + Long res = facet.get(ruleType.name()); + if (res == null) { + return 0L; + } + return res; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImpl.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImpl.java index 25af68798c9..bdca8f10ad7 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImpl.java @@ -24,6 +24,7 @@ import com.google.common.collect.Sets; 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; @@ -38,6 +39,7 @@ 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.LiveQualityGateFactory; import org.sonar.server.settings.ProjectConfigurationLoader; import static org.sonar.core.util.stream.MoreCollectors.toSet; @@ -49,11 +51,14 @@ public class IssueChangeTriggerImpl implements IssueChangeTrigger { 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 @@ -62,7 +67,7 @@ public class IssueChangeTriggerImpl implements IssueChangeTrigger { return; } - callWebHook(issueChangeData); + broadcastToListeners(issueChangeData); } private static boolean isRelevant(IssueChange issueChange) { @@ -81,7 +86,7 @@ public class IssueChangeTriggerImpl implements IssueChangeTrigger { return MEANINGFUL_TRANSITIONS.contains(transitionKey); } - private void callWebHook(IssueChangeData issueChangeData) { + private void broadcastToListeners(IssueChangeData issueChangeData) { try (DbSession dbSession = dbClient.openSession(false)) { Map branchesByUuid = getBranchComponents(dbSession, issueChangeData); if (branchesByUuid.isEmpty()) { @@ -116,7 +121,8 @@ public class IssueChangeTriggerImpl implements IssueChangeTrigger { 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; }) diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java index a3d82c69856..93ba771ffba 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java @@ -19,24 +19,32 @@ */ 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> 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> 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() { @@ -55,37 +63,30 @@ public class QGChangeEvent { return projectConfiguration; } + public Supplier> 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(); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java index 1c1d3d8750d..a697aad3f36 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java @@ -19,8 +19,10 @@ */ 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; @@ -56,16 +58,21 @@ public class QGChangeEventListenersImpl implements QGChangeEventListeners { @Override public void broadcast(Trigger trigger, Collection changeEvents) { + if (changeEvents.isEmpty()) { + return; + } + try { - Arrays.stream(listeners).forEach(listener -> broadcastTo(trigger, changeEvents, listener)); + List 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 changeEvents, QGChangeEventListener listener) { + private static void broadcastTo(Trigger trigger, Collection 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); diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java index 4f3c6baf8fd..59a2204cba4 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java @@ -20,15 +20,9 @@ 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; @@ -36,47 +30,19 @@ import org.sonar.db.component.AnalysisPropertyDto; 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 @@ -101,10 +67,13 @@ public class WebhookQGChangeEventListener implements QGChangeEventListener { 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 analysisProperties = dbClient.analysisPropertiesDao().selectBySnapshotUuid(dbSession, analysis.getUuid()) .stream() .collect(Collectors.toMap(AnalysisPropertyDto::getKey, AnalysisPropertyDto::getValue)); @@ -113,87 +82,10 @@ public class WebhookQGChangeEventListener implements QGChangeEventListener { 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 typeFacet = new Facets(searchResponse, system2.getDefaultTimeZone()) - .get(RuleIndex.FACET_TYPES); - - EvaluatedQualityGate.Builder builder = EvaluatedQualityGate.newBuilder(); - Set 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 conditions) { - if (conditions.stream().anyMatch(c -> c.getStatus() == EvaluationStatus.ERROR)) { - return EvaluatedQualityGate.Status.ERROR; - } - return EvaluatedQualityGate.Status.OK; - } - - private static long getMeasure(LinkedHashMap 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 facet, RuleType ruleType) { - Long res = facet.get(ruleType.name()); - if (res == null) { - return 0L; - } - return res; - } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java index e4a4bdea80b..903f908c965 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java @@ -30,7 +30,7 @@ public class IssueWsModuleTest { 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); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImplTest.java new file mode 100644 index 00000000000..9337e872380 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImplTest.java @@ -0,0 +1,186 @@ +/* + * 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 OPEN_STATUSES = ImmutableList.of(Issue.STATUS_OPEN, Issue.STATUS_CONFIRMED); + private static final List 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); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImplTest.java index d0d48fb01ec..9489f469510 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImplTest.java @@ -66,6 +66,7 @@ import org.sonar.db.component.SnapshotDao; 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; @@ -101,9 +102,10 @@ public class IssueChangeTriggerImplTest { 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() { diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java new file mode 100644 index 00000000000..9df7be5af3b --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java @@ -0,0 +1,158 @@ +/* + * 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 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 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 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); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventTest.java new file mode 100644 index 00000000000..f8f5d8fccb1 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventTest.java @@ -0,0 +1,115 @@ +/* + * 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> 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 + "}"); + + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java index acbeca51332..3e7c4651195 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java @@ -20,29 +20,21 @@ 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; @@ -51,76 +43,52 @@ import org.sonar.db.component.BranchType; 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 OPEN_STATUSES = ImmutableList.of(Issue.STATUS_OPEN, Issue.STATUS_CONFIRMED); - private static final List 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 @@ -130,12 +98,12 @@ public class WebhookQGChangeEventListenerTest { 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 @@ -152,29 +120,16 @@ public class WebhookQGChangeEventListenerTest { 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)); } @@ -196,8 +151,8 @@ public class WebhookQGChangeEventListenerTest { 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); @@ -215,8 +170,8 @@ public class WebhookQGChangeEventListenerTest { 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); @@ -232,9 +187,9 @@ public class WebhookQGChangeEventListenerTest { 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( @@ -244,121 +199,15 @@ public class WebhookQGChangeEventListenerTest { 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); } } @@ -371,15 +220,6 @@ public class WebhookQGChangeEventListenerTest { .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 properties) { List analysisProperties = properties.entrySet().stream() .map(entry -> new AnalysisPropertyDto() @@ -424,10 +264,6 @@ public class WebhookQGChangeEventListenerTest { 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) @@ -475,8 +311,8 @@ public class WebhookQGChangeEventListenerTest { } - 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)); } }