3 * Copyright (C) 2009-2021 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.measure.live;
22 import com.tngtech.java.junit.dataprovider.DataProvider;
23 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
24 import com.tngtech.java.junit.dataprovider.UseDataProvider;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.List;
28 import java.util.Optional;
29 import java.util.concurrent.atomic.AtomicInteger;
30 import java.util.function.Supplier;
31 import javax.annotation.Nullable;
32 import org.junit.Before;
33 import org.junit.Rule;
34 import org.junit.Test;
35 import org.junit.rules.ExpectedException;
36 import org.junit.runner.RunWith;
37 import org.sonar.api.config.PropertyDefinitions;
38 import org.sonar.api.config.internal.MapSettings;
39 import org.sonar.api.measures.CoreMetrics;
40 import org.sonar.api.measures.Metric;
41 import org.sonar.api.resources.Qualifiers;
42 import org.sonar.api.utils.System2;
43 import org.sonar.core.config.CorePropertyDefinitions;
44 import org.sonar.db.DbSession;
45 import org.sonar.db.DbTester;
46 import org.sonar.db.component.BranchDto;
47 import org.sonar.db.component.BranchType;
48 import org.sonar.db.component.ComponentDto;
49 import org.sonar.db.component.ComponentTesting;
50 import org.sonar.db.measure.LiveMeasureDto;
51 import org.sonar.db.metric.MetricDto;
52 import org.sonar.db.project.ProjectDto;
53 import org.sonar.server.es.ProjectIndexer;
54 import org.sonar.server.es.TestProjectIndexers;
55 import org.sonar.server.measure.Rating;
56 import org.sonar.server.qualitygate.EvaluatedQualityGate;
57 import org.sonar.server.qualitygate.QualityGate;
58 import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
59 import org.sonar.server.setting.ProjectConfigurationLoader;
60 import org.sonar.server.setting.TestProjectConfigurationLoader;
62 import static java.util.Arrays.asList;
63 import static java.util.Collections.emptyList;
64 import static java.util.Collections.singleton;
65 import static org.assertj.core.api.Assertions.assertThat;
66 import static org.mockito.ArgumentMatchers.any;
67 import static org.mockito.ArgumentMatchers.argThat;
68 import static org.mockito.ArgumentMatchers.eq;
69 import static org.mockito.ArgumentMatchers.same;
70 import static org.mockito.Mockito.mock;
71 import static org.mockito.Mockito.verify;
72 import static org.mockito.Mockito.when;
73 import static org.sonar.api.resources.Qualifiers.ORDERED_BOTTOM_UP;
75 @RunWith(DataProviderRunner.class)
76 public class LiveMeasureComputerImplTest {
79 public DbTester db = DbTester.create();
81 public ExpectedException expectedException = ExpectedException.none();
83 private final TestProjectIndexers projectIndexer = new TestProjectIndexers();
84 private MetricDto intMetric;
85 private MetricDto ratingMetric;
86 private MetricDto alertStatusMetric;
87 private ComponentDto project;
88 private ProjectDto projectDto;
89 private ComponentDto dir;
90 private ComponentDto file1;
91 private ComponentDto file2;
92 private ComponentDto branch;
93 private ComponentDto branchFile;
94 private final LiveQualityGateComputer qGateComputer = mock(LiveQualityGateComputer.class);
95 private final QualityGate qualityGate = mock(QualityGate.class);
96 private final EvaluatedQualityGate newQualityGate = mock(EvaluatedQualityGate.class);
100 intMetric = db.measures().insertMetric(m -> m.setValueType(Metric.ValueType.INT.name()));
101 ratingMetric = db.measures().insertMetric(m -> m.setValueType(Metric.ValueType.RATING.name()));
102 alertStatusMetric = db.measures().insertMetric(m -> m.setKey(CoreMetrics.ALERT_STATUS_KEY));
103 project = db.components().insertPublicProject();
104 projectDto = db.components().getProjectDto(project);
105 dir = db.components().insertComponent(ComponentTesting.newDirectory(project, "src/main/java"));
106 file1 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir));
107 file2 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir));
108 branch = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.PULL_REQUEST));
109 branchFile = db.components().insertComponent(ComponentTesting.newFileDto(branch));
113 public void compute_and_insert_measures_if_they_do_not_exist_yet() {
114 markProjectAsAnalyzed(project);
116 List<QGChangeEvent> result = run(asList(file1, file2), newQualifierBasedIntFormula(), newRatingConstantFormula(Rating.C));
118 // 2 measures per component have been created
119 // Numeric value depends on qualifier (see newQualifierBasedIntFormula())
120 assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(8);
121 assertThatIntMeasureHasValue(file1, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
122 assertThatRatingMeasureHasValue(file1, Rating.C);
123 assertThatIntMeasureHasValue(file2, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
124 assertThatRatingMeasureHasValue(file2, Rating.C);
125 assertThatIntMeasureHasValue(dir, ORDERED_BOTTOM_UP.indexOf(Qualifiers.DIRECTORY));
126 assertThatRatingMeasureHasValue(dir, Rating.C);
127 assertThatIntMeasureHasValue(project, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT));
128 assertThatRatingMeasureHasValue(project, Rating.C);
129 assertThatProjectChanged(result, project);
133 public void compute_and_update_measures_if_they_already_exist() {
134 markProjectAsAnalyzed(project);
135 db.measures().insertLiveMeasure(project, intMetric, m -> m.setValue(42.0));
136 db.measures().insertLiveMeasure(dir, intMetric, m -> m.setValue(42.0));
137 db.measures().insertLiveMeasure(file1, intMetric, m -> m.setValue(42.0));
138 db.measures().insertLiveMeasure(file2, intMetric, m -> m.setValue(42.0));
140 // generates values 1, 2, 3
141 List<QGChangeEvent> result = run(file1, newQualifierBasedIntFormula());
143 assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(4);
144 assertThatProjectChanged(result, project);
146 // Numeric value depends on qualifier (see newQualifierBasedIntFormula())
147 assertThatIntMeasureHasValue(file1, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
148 assertThatIntMeasureHasValue(dir, ORDERED_BOTTOM_UP.indexOf(Qualifiers.DIRECTORY));
149 assertThatIntMeasureHasValue(project, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT));
151 assertThatIntMeasureHasValue(file2, 42.0);
155 public void variation_is_refreshed_when_int_value_is_changed() {
156 markProjectAsAnalyzed(project);
158 // 42 on last analysis
159 // 42-12=30 on beginning of leak period
160 db.measures().insertLiveMeasure(project, intMetric, m -> m.setValue(42.0).setVariation(12.0));
162 // new value is 44, so variation on leak period is 44-30=14
163 List<QGChangeEvent> result = run(file1, newIntConstantFormula(44.0));
165 LiveMeasureDto measure = assertThatIntMeasureHasValue(project, 44.0);
166 assertThat(measure.getVariation()).isEqualTo(14.0);
167 assertThatProjectChanged(result, project);
171 public void variation_is_refreshed_when_rating_value_is_changed() {
172 markProjectAsAnalyzed(project);
174 // B on last analysis
175 // D on beginning of leak period --> variation is -2
176 db.measures().insertLiveMeasure(project, ratingMetric, m -> m.setValue((double) Rating.B.getIndex()).setData("B").setVariation(-2.0));
178 // new value is C, so variation on leak period is D to C = -1
179 List<QGChangeEvent> result = run(file1, newRatingConstantFormula(Rating.C));
181 LiveMeasureDto measure = assertThatRatingMeasureHasValue(project, Rating.C);
182 assertThat(measure.getVariation()).isEqualTo(-1.0);
183 assertThatProjectChanged(result, project);
187 public void variation_does_not_change_if_rating_value_does_not_change() {
188 markProjectAsAnalyzed(project);
190 // B on last analysis
191 // D on beginning of leak period --> variation is -2
192 db.measures().insertLiveMeasure(project, ratingMetric, m -> m.setValue((double) Rating.B.getIndex()).setData("B").setVariation(-2.0));
194 // new value is still B, so variation on leak period is still -2
195 List<QGChangeEvent> result = run(file1, newRatingConstantFormula(Rating.B));
197 LiveMeasureDto measure = assertThatRatingMeasureHasValue(project, Rating.B);
198 assertThat(measure.getVariation()).isEqualTo(-2.0);
199 assertThatProjectChanged(result, project);
203 public void refresh_leak_measures() {
204 markProjectAsAnalyzed(project);
205 db.measures().insertLiveMeasure(project, intMetric, m -> m.setVariation(42.0).setValue(null));
206 db.measures().insertLiveMeasure(project, ratingMetric, m -> m.setVariation((double) Rating.E.getIndex()));
207 db.measures().insertLiveMeasure(dir, intMetric, m -> m.setVariation(42.0).setValue(null));
208 db.measures().insertLiveMeasure(dir, ratingMetric, m -> m.setVariation((double) Rating.D.getIndex()));
209 db.measures().insertLiveMeasure(file1, intMetric, m -> m.setVariation(42.0).setValue(null));
210 db.measures().insertLiveMeasure(file1, ratingMetric, m -> m.setVariation((double) Rating.C.getIndex()));
212 // generates values 1, 2, 3 on leak measures
213 List<QGChangeEvent> result = run(file1, newQualifierBasedIntLeakFormula(), newRatingLeakFormula(Rating.B));
215 assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(6);
217 // Numeric value depends on qualifier (see newQualifierBasedIntLeakFormula())
218 assertThatIntMeasureHasLeakValue(file1, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
219 assertThatRatingMeasureHasLeakValue(file1, Rating.B);
220 assertThatIntMeasureHasLeakValue(dir, ORDERED_BOTTOM_UP.indexOf(Qualifiers.DIRECTORY));
221 assertThatRatingMeasureHasLeakValue(dir, Rating.B);
222 assertThatIntMeasureHasLeakValue(project, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT));
223 assertThatRatingMeasureHasLeakValue(project, Rating.B);
224 assertThatProjectChanged(result, project);
228 public void calculate_new_metrics_if_it_is_pr_or_branch() {
229 markProjectAsAnalyzed(branch, null);
230 db.measures().insertLiveMeasure(branch, intMetric, m -> m.setVariation(42.0).setValue(null));
231 db.measures().insertLiveMeasure(branchFile, intMetric, m -> m.setVariation(42.0).setValue(null));
233 // generates values 1, 2, 3 on leak measures
234 List<QGChangeEvent> result = run(branchFile, newQualifierBasedIntLeakFormula(), newRatingLeakFormula(Rating.B));
236 assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(4);
238 // Numeric value depends on qualifier (see newQualifierBasedIntLeakFormula())
239 assertThatIntMeasureHasLeakValue(branchFile, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
240 assertThatRatingMeasureHasLeakValue(branchFile, Rating.B);
241 assertThatIntMeasureHasLeakValue(branch, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT));
242 assertThatRatingMeasureHasLeakValue(branch, Rating.B);
243 assertThatProjectChanged(result, branch);
247 public void do_nothing_if_project_has_not_been_analyzed() {
248 // project has no snapshots
249 List<QGChangeEvent> result = run(file1, newIncrementalFormula());
250 assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isZero();
251 assertThatProjectNotChanged(result, project);
255 public void do_nothing_if_input_components_are_empty() {
256 List<QGChangeEvent> result = run(emptyList(), newIncrementalFormula());
258 assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isZero();
259 assertThatProjectNotChanged(result, project);
263 public void refresh_multiple_projects_at_the_same_time() {
264 markProjectAsAnalyzed(project);
265 ComponentDto project2 = db.components().insertPublicProject();
266 ComponentDto fileInProject2 = db.components().insertComponent(ComponentTesting.newFileDto(project2));
267 markProjectAsAnalyzed(project2);
269 List<QGChangeEvent> result = run(asList(file1, fileInProject2), newQualifierBasedIntFormula());
271 // generated values depend on position of qualifier in Qualifiers.ORDERED_BOTTOM_UP (see formula)
272 assertThatIntMeasureHasValue(file1, 0);
273 assertThatIntMeasureHasValue(dir, 2);
274 assertThatIntMeasureHasValue(project, 4);
275 assertThatIntMeasureHasValue(fileInProject2, 0);
276 assertThatIntMeasureHasValue(project2, 4);
278 // no other measures generated
279 assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(5);
280 assertThatProjectChanged(result, project, project2);
284 public void refresh_multiple_branches_at_the_same_time() {
289 public void event_contains_no_previousStatus_if_measure_does_not_exist() {
290 markProjectAsAnalyzed(project);
292 List<QGChangeEvent> result = run(file1);
295 .extracting(QGChangeEvent::getPreviousStatus)
296 .containsExactly(Optional.empty());
300 public void event_contains_no_previousStatus_if_measure_exists_and_has_no_value() {
301 markProjectAsAnalyzed(project);
302 db.measures().insertLiveMeasure(project, alertStatusMetric, m -> m.setData((String) null));
304 List<QGChangeEvent> result = run(file1);
307 .extracting(QGChangeEvent::getPreviousStatus)
308 .containsExactly(Optional.empty());
312 public void event_contains_no_previousStatus_if_measure_exists_and_is_empty() {
313 markProjectAsAnalyzed(project);
314 db.measures().insertLiveMeasure(project, alertStatusMetric, m -> m.setData(""));
316 List<QGChangeEvent> result = run(file1);
319 .extracting(QGChangeEvent::getPreviousStatus)
320 .containsExactly(Optional.empty());
324 public void event_contains_no_previousStatus_if_measure_exists_and_is_not_a_level() {
325 markProjectAsAnalyzed(project);
326 db.measures().insertLiveMeasure(project, alertStatusMetric, m -> m.setData("fooBar"));
328 List<QGChangeEvent> result = run(file1);
331 .extracting(QGChangeEvent::getPreviousStatus)
332 .containsExactly(Optional.empty());
336 @UseDataProvider("metricLevels")
337 public void event_contains_previousStatus_if_measure_exists(Metric.Level level) {
338 markProjectAsAnalyzed(project);
339 db.measures().insertLiveMeasure(project, alertStatusMetric, m -> m.setData(level.name()));
340 db.measures().insertLiveMeasure(project, intMetric, m -> m.setVariation(42.0).setValue(null));
342 List<QGChangeEvent> result = run(file1, newQualifierBasedIntLeakFormula());
345 .extracting(QGChangeEvent::getPreviousStatus)
346 .containsExactly(Optional.of(level));
350 public static Object[][] metricLevels() {
351 return Arrays.stream(Metric.Level.values())
352 .map(l -> new Object[] {l})
353 .toArray(Object[][]::new);
357 public void event_contains_newQualityGate_computed_by_LiveQualityGateComputer() {
358 markProjectAsAnalyzed(project);
359 db.measures().insertLiveMeasure(project, alertStatusMetric, m -> m.setData(Metric.Level.ERROR.name()));
360 db.measures().insertLiveMeasure(project, intMetric, m -> m.setVariation(42.0).setValue(null));
361 BranchDto branch = db.getDbClient().branchDao().selectByBranchKey(db.getSession(), project.projectUuid(), "master")
362 .orElseThrow(() -> new IllegalStateException("Can't find master branch"));
364 List<QGChangeEvent> result = run(file1, newQualifierBasedIntLeakFormula());
367 .extracting(QGChangeEvent::getQualityGateSupplier)
368 .extracting(Supplier::get)
369 .containsExactly(Optional.of(newQualityGate));
370 verify(qGateComputer).loadQualityGate(any(DbSession.class), argThat(p -> p.getUuid().equals(projectDto.getUuid())), eq(branch));
371 verify(qGateComputer).getMetricsRelatedTo(qualityGate);
372 verify(qGateComputer).refreshGateStatus(eq(project), same(qualityGate), any(MeasureMatrix.class));
376 public void exception_describes_context_when_a_formula_fails() {
377 markProjectAsAnalyzed(project);
378 Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create();
380 expectedException.expect(IllegalStateException.class);
381 expectedException.expectMessage("Fail to compute " + metric.getKey() + " on " + project.getDbKey());
383 run(project, new IssueMetricFormula(metric, false, (context, issueCounter) -> {
384 throw new NullPointerException("BOOM");
388 private List<QGChangeEvent> run(ComponentDto component, IssueMetricFormula... formulas) {
389 return run(singleton(component), formulas);
392 private List<QGChangeEvent> run(Collection<ComponentDto> components, IssueMetricFormula... formulas) {
393 IssueMetricFormulaFactory formulaFactory = new TestIssueMetricFormulaFactory(asList(formulas));
395 when(qGateComputer.loadQualityGate(any(DbSession.class), any(ProjectDto.class), any(BranchDto.class)))
396 .thenReturn(qualityGate);
397 when(qGateComputer.getMetricsRelatedTo(qualityGate)).thenReturn(singleton(CoreMetrics.ALERT_STATUS_KEY));
398 when(qGateComputer.refreshGateStatus(eq(project), same(qualityGate), any(MeasureMatrix.class)))
399 .thenReturn(newQualityGate);
400 MapSettings settings = new MapSettings(new PropertyDefinitions(System2.INSTANCE, CorePropertyDefinitions.all()));
401 ProjectConfigurationLoader configurationLoader = new TestProjectConfigurationLoader(settings.asConfig());
403 LiveMeasureComputerImpl underTest = new LiveMeasureComputerImpl(db.getDbClient(), formulaFactory, qGateComputer, configurationLoader, projectIndexer);
405 return underTest.refresh(db.getSession(), components);
408 private void markProjectAsAnalyzed(ComponentDto p) {
409 markProjectAsAnalyzed(p, 1_490_000_000L);
412 private void markProjectAsAnalyzed(ComponentDto p, @Nullable Long periodDate) {
413 assertThat(p.qualifier()).isEqualTo(Qualifiers.PROJECT);
414 db.components().insertSnapshot(p, s -> s.setPeriodDate(periodDate));
417 private LiveMeasureDto assertThatIntMeasureHasValue(ComponentDto component, double expectedValue) {
418 LiveMeasureDto measure = db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), component.uuid(), intMetric.getKey()).get();
419 assertThat(measure.getComponentUuid()).isEqualTo(component.uuid());
420 assertThat(measure.getProjectUuid()).isEqualTo(component.projectUuid());
421 assertThat(measure.getMetricUuid()).isEqualTo(intMetric.getUuid());
422 assertThat(measure.getValue()).isEqualTo(expectedValue);
426 private LiveMeasureDto assertThatRatingMeasureHasValue(ComponentDto component, Rating expectedRating) {
427 LiveMeasureDto measure = db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), component.uuid(), ratingMetric.getKey()).get();
428 assertThat(measure.getComponentUuid()).isEqualTo(component.uuid());
429 assertThat(measure.getProjectUuid()).isEqualTo(component.projectUuid());
430 assertThat(measure.getMetricUuid()).isEqualTo(ratingMetric.getUuid());
431 assertThat(measure.getValue()).isEqualTo(expectedRating.getIndex());
432 assertThat(measure.getDataAsString()).isEqualTo(expectedRating.name());
436 private void assertThatIntMeasureHasLeakValue(ComponentDto component, double expectedValue) {
437 LiveMeasureDto measure = db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), component.uuid(), intMetric.getKey()).get();
438 assertThat(measure.getComponentUuid()).isEqualTo(component.uuid());
439 assertThat(measure.getProjectUuid()).isEqualTo(component.projectUuid());
440 assertThat(measure.getMetricUuid()).isEqualTo(intMetric.getUuid());
441 assertThat(measure.getValue()).isNull();
442 assertThat(measure.getVariation()).isEqualTo(expectedValue);
445 private void assertThatRatingMeasureHasLeakValue(ComponentDto component, Rating expectedValue) {
446 LiveMeasureDto measure = db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), component.uuid(), ratingMetric.getKey()).get();
447 assertThat(measure.getComponentUuid()).isEqualTo(component.uuid());
448 assertThat(measure.getProjectUuid()).isEqualTo(component.projectUuid());
449 assertThat(measure.getMetricUuid()).isEqualTo(ratingMetric.getUuid());
450 assertThat(measure.getVariation()).isEqualTo(expectedValue.getIndex());
453 private IssueMetricFormula newIncrementalFormula() {
454 Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create();
455 AtomicInteger counter = new AtomicInteger();
456 return new IssueMetricFormula(metric, false, (ctx, issues) -> {
457 ctx.setValue(counter.incrementAndGet());
461 private IssueMetricFormula newIntConstantFormula(double constant) {
462 Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create();
463 return new IssueMetricFormula(metric, false, (ctx, issues) -> {
464 ctx.setValue(constant);
468 private IssueMetricFormula newRatingConstantFormula(Rating constant) {
469 Metric metric = new Metric.Builder(ratingMetric.getKey(), ratingMetric.getShortName(), Metric.ValueType.valueOf(ratingMetric.getValueType())).create();
470 return new IssueMetricFormula(metric, false, (ctx, issues) -> {
471 ctx.setValue(constant);
475 private IssueMetricFormula newRatingLeakFormula(Rating rating) {
476 Metric metric = new Metric.Builder(ratingMetric.getKey(), ratingMetric.getShortName(), Metric.ValueType.valueOf(ratingMetric.getValueType())).create();
477 return new IssueMetricFormula(metric, true, (ctx, issues) -> {
478 ctx.setLeakValue(rating);
482 private IssueMetricFormula newQualifierBasedIntFormula() {
483 Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create();
484 return new IssueMetricFormula(metric, false, (ctx, issues) -> {
485 ctx.setValue(ORDERED_BOTTOM_UP.indexOf(ctx.getComponent().qualifier()));
489 private IssueMetricFormula newQualifierBasedIntLeakFormula() {
490 Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create();
491 return new IssueMetricFormula(metric, true, (ctx, issues) -> {
492 ctx.setLeakValue(ORDERED_BOTTOM_UP.indexOf(ctx.getComponent().qualifier()));
496 private void assertThatProjectChanged(List<QGChangeEvent> events, ComponentDto... projects) {
497 for (ComponentDto p : projects) {
498 assertThat(projectIndexer.hasBeenCalled(p.uuid(), ProjectIndexer.Cause.MEASURE_CHANGE)).isTrue();
501 assertThat(events).extracting(e -> e.getBranch().getUuid())
502 .containsExactlyInAnyOrder(Arrays.stream(projects).map(ComponentDto::uuid).toArray(String[]::new));
505 private void assertThatProjectNotChanged(List<QGChangeEvent> events, ComponentDto project) {
506 assertThat(projectIndexer.hasBeenCalled(project.uuid(), ProjectIndexer.Cause.MEASURE_CHANGE)).isFalse();
507 assertThat(events).isEmpty();