From 0ff9b7313c17474a867f744aa63afd94795a134b Mon Sep 17 00:00:00 2001 From: Eric Giffon Date: Tue, 3 Sep 2024 12:04:04 +0200 Subject: [PATCH] SONAR-22872 Live update of JSON measures --- .../java/org/sonar/db/measure/MeasureDao.java | 51 +++++ .../java/org/sonar/db/measure/MeasureDto.java | 10 + .../java/org/sonar/db/metric/MetricDto.java | 7 + .../org/sonar/db/measure/MeasureDaoTest.java | 189 ++++++++++++++++++ .../org/sonar/db/measure/MeasureDtoTest.java | 23 +++ .../org/sonar/db/metric/MetricDtoTest.java | 38 ++++ .../org/sonar/db/measure/MeasureDbTester.java | 3 +- .../live/LiveMeasureComputerImplIT.java | 11 +- .../live/LiveMeasureTreeUpdaterImplIT.java | 56 +++--- .../live/LiveQualityGateComputerImplIT.java | 17 +- .../measure/live/LiveMeasureComputerImpl.java | 50 +++-- .../live/LiveMeasureTreeUpdaterImpl.java | 29 ++- .../live/LiveQualityGateComputerImpl.java | 22 +- .../server/measure/live/MeasureMatrix.java | 116 +++++++---- .../measure/live/MeasureMatrixTest.java | 56 +++--- 15 files changed, 532 insertions(+), 146 deletions(-) diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/measure/MeasureDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/measure/MeasureDao.java index eebceb810ad..1820903e7f9 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/measure/MeasureDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/measure/MeasureDao.java @@ -19,6 +19,8 @@ */ package org.sonar.db.measure; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; @@ -26,6 +28,8 @@ import org.sonar.api.utils.System2; import org.sonar.db.Dao; import org.sonar.db.DbSession; +import static org.sonar.db.DatabaseUtils.executeLargeInputs; + public class MeasureDao implements Dao { private final System2 system2; @@ -38,10 +42,32 @@ public class MeasureDao implements Dao { return mapper(dbSession).insert(dto, system2.now()); } + /** + * Update a measure. The measure json value will be overwritten. + */ public int update(DbSession dbSession, MeasureDto dto) { return mapper(dbSession).update(dto, system2.now()); } + /** + * Unlike {@link #update(DbSession, MeasureDto)}, this method will not overwrite the entire json value, + * but will update the measures inside the json. + */ + public int insertOrUpdate(DbSession dbSession, MeasureDto dto) { + long now = system2.now(); + Optional existingMeasureOpt = selectMeasure(dbSession, dto.getComponentUuid()); + if (existingMeasureOpt.isPresent()) { + MeasureDto existingDto = existingMeasureOpt.get(); + existingDto.getMetricValues().putAll(dto.getMetricValues()); + dto.getMetricValues().putAll(existingDto.getMetricValues()); + dto.computeJsonValueHash(); + return mapper(dbSession).update(dto, now); + } else { + dto.computeJsonValueHash(); + return mapper(dbSession).insert(dto, now); + } + } + public Optional selectMeasure(DbSession dbSession, String componentUuid) { List measures = mapper(dbSession).selectByComponentUuids(List.of(componentUuid)); if (!measures.isEmpty()) { @@ -51,6 +77,31 @@ public class MeasureDao implements Dao { return Optional.empty(); } + public Optional selectMeasure(DbSession dbSession, String componentUuid, String metricKey) { + List measures = selectByComponentUuidsAndMetricKeys(dbSession, List.of(componentUuid), List.of(metricKey)); + // component_uuid column is unique. List can't have more than 1 item. + if (measures.size() == 1) { + return Optional.of(measures.get(0)); + } + return Optional.empty(); + } + + public List selectByComponentUuidsAndMetricKeys(DbSession dbSession, Collection largeComponentUuids, Collection metricKeys) { + if (largeComponentUuids.isEmpty() || metricKeys.isEmpty()) { + return Collections.emptyList(); + } + + return executeLargeInputs( + largeComponentUuids, + componentUuids -> mapper(dbSession).selectByComponentUuids(componentUuids)).stream() + .map(measureDto -> { + measureDto.getMetricValues().entrySet().removeIf(entry -> !metricKeys.contains(entry.getKey())); + return measureDto; + }) + .filter(measureDto -> !measureDto.getMetricValues().isEmpty()) + .toList(); + } + public Set selectBranchMeasureHashes(DbSession dbSession, String branchUuid) { return mapper(dbSession).selectBranchMeasureHashes(branchUuid); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/measure/MeasureDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/measure/MeasureDto.java index 8e93bd9a9a5..e3abe752df3 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/measure/MeasureDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/measure/MeasureDto.java @@ -23,6 +23,7 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.util.Map; import java.util.TreeMap; +import javax.annotation.CheckForNull; import org.apache.commons.codec.digest.MurmurHash3; import static java.nio.charset.StandardCharsets.UTF_8; @@ -89,6 +90,15 @@ public class MeasureDto { return jsonValueHash; } + @CheckForNull + public String getString(String metricKey) { + Object value = metricValues.get(metricKey); + if (value == null) { + return null; + } + return String.valueOf(value); + } + @Override public String toString() { return "MeasureDto{" + diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/metric/MetricDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/metric/MetricDto.java index 439071c03a0..5ce78ff5953 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/metric/MetricDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/metric/MetricDto.java @@ -21,6 +21,7 @@ package org.sonar.db.metric; import javax.annotation.CheckForNull; import javax.annotation.Nullable; +import org.sonar.api.measures.Metric; import static org.sonar.db.metric.MetricValidator.checkMetricDescription; import static org.sonar.db.metric.MetricValidator.checkMetricDomain; @@ -202,4 +203,10 @@ public class MetricDto { return this; } + public boolean isNumeric() { + return switch (Metric.ValueType.valueOf(valueType)) { + case INT, MILLISEC, WORK_DUR, FLOAT, PERCENT, RATING, BOOL -> true; + default -> false; + }; + } } diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/measure/MeasureDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/measure/MeasureDaoTest.java index 1fdd6d7b9c1..a0786410bba 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/measure/MeasureDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/measure/MeasureDaoTest.java @@ -19,12 +19,24 @@ */ package org.sonar.db.measure; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang.math.RandomUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonar.api.utils.System2; +import org.sonar.db.DbSession; import org.sonar.db.DbTester; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.assertj.core.groups.Tuple.tuple; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; import static org.sonar.db.measure.MeasureTesting.newMeasure; class MeasureDaoTest { @@ -57,6 +69,59 @@ class MeasureDaoTest { verifyPersisted(dto); } + @Test + void insertOrUpdate_inserts_or_updates_measure() { + // insert + MeasureDto dto = newMeasure(); + int count = underTest.insertOrUpdate(db.getSession(), dto); + assertThat(count).isEqualTo(1); + verifyTableSize(1); + verifyPersisted(dto); + + // update + String key = dto.getMetricValues().keySet().stream().findFirst().orElseThrow(); + dto.addValue(key, getDoubleValue()); + count = underTest.insertOrUpdate(db.getSession(), dto); + assertThat(count).isEqualTo(1); + verifyTableSize(1); + verifyPersisted(dto); + } + + @Test + void insertOrUpdate_merges_measures() { + // insert + Double value2 = getDoubleValue(); + MeasureDto dto = newMeasure(); + dto.getMetricValues().clear(); + dto.addValue("key1", getDoubleValue()) + .addValue("key2", value2); + int count = underTest.insert(db.getSession(), dto); + verifyPersisted(dto); + verifyTableSize(1); + assertThat(count).isEqualTo(1); + + // update key1 value, remove key2 (must not disappear from DB) and add key3 + Double value1 = getDoubleValue(); + Double value3 = getDoubleValue(); + dto.addValue("key1", value1) + .addValue("key3", value3) + .getMetricValues().remove("key2"); + count = underTest.insertOrUpdate(db.getSession(), dto); + assertThat(count).isEqualTo(1); + verifyTableSize(1); + + assertThat(underTest.selectMeasure(db.getSession(), dto.getComponentUuid())) + .hasValueSatisfying(selected -> { + assertThat(selected.getComponentUuid()).isEqualTo(dto.getComponentUuid()); + assertThat(selected.getBranchUuid()).isEqualTo(dto.getBranchUuid()); + assertThat(selected.getMetricValues()).contains( + entry("key1", value1), + entry("key2", value2), + entry("key3", value3)); + assertThat(selected.getJsonValueHash()).isEqualTo(dto.computeJsonValueHash()); + }); + } + @Test void select_measure() { MeasureDto measure1 = newMeasure(); @@ -69,6 +134,126 @@ class MeasureDaoTest { assertThat(underTest.selectMeasure(db.getSession(), "unknown-component")).isEmpty(); } + @Test + void selectMeasure_with_single_metric() { + String metricKey = "metric1"; + String value = "foo"; + MeasureDto measure1 = newMeasure().addValue(metricKey, value); + MeasureDto measure2 = newMeasure(); + underTest.insert(db.getSession(), measure1); + underTest.insert(db.getSession(), measure2); + + assertThat(underTest.selectMeasure(db.getSession(), measure1.getComponentUuid(), metricKey)) + .hasValueSatisfying(selected -> assertThat(selected.getMetricValues()).containsOnly(entry(metricKey, value))); + + assertThat(underTest.selectMeasure(db.getSession(), "unknown-component", metricKey)).isEmpty(); + } + + @Test + void selectByComponentUuidsAndMetricKeys_does_not_use_db_when_no_components() { + String metricKey = randomAlphabetic(7); + newMeasure().addValue(metricKey, randomAlphabetic(11)); + + DbSession dbSession = mock(DbSession.class); + assertThat(underTest.selectByComponentUuidsAndMetricKeys(dbSession, emptyList(), singletonList(metricKey))) + .isEmpty(); + verifyNoInteractions(dbSession); + } + + @Test + void selectByComponentUuidsAndMetricKeys_does_not_use_db_when_no_metrics() { + DbSession dbSession = mock(DbSession.class); + assertThat(underTest.selectByComponentUuidsAndMetricKeys(dbSession, singletonList("nonexistent"), emptyList())) + .isEmpty(); + verifyNoInteractions(dbSession); + } + + @Test + void selectByComponentUuidsAndMetricKeys_with_single_component_and_single_metric() { + String metricKey = randomAlphabetic(7); + String value = randomAlphabetic(11); + MeasureDto measure = newMeasure().addValue(metricKey, value); + underTest.insert(db.getSession(), measure); + + List measureDtos = underTest.selectByComponentUuidsAndMetricKeys( + db.getSession(), singletonList(measure.getComponentUuid()), singletonList(metricKey)); + assertThat(measureDtos).hasSize(1); + assertThat(measureDtos.get(0).getMetricValues()).isEqualTo(Map.of(metricKey, value)); + } + + @Test + void selectByComponentUuidsAndMetricKeys_with_nonexistent_component_returns_empty() { + String metricKey = randomAlphabetic(7); + String value = randomAlphabetic(11); + MeasureDto measure = newMeasure().addValue(metricKey, value); + underTest.insert(db.getSession(), measure); + + assertThat(underTest.selectByComponentUuidsAndMetricKeys( + db.getSession(), singletonList("nonexistent"), singletonList(metricKey))).isEmpty(); + + assertThat(underTest.selectByComponentUuidsAndMetricKeys( + db.getSession(), singletonList(measure.getComponentUuid()), singletonList(metricKey))).isNotEmpty(); + } + + @Test + void selectByComponentUuidsAndMetricKeys_with_nonexistent_metric_returns_empty() { + String metricKey = randomAlphabetic(7); + String value = randomAlphabetic(11); + MeasureDto measure = newMeasure().addValue(metricKey, value); + underTest.insert(db.getSession(), measure); + + assertThat(underTest.selectByComponentUuidsAndMetricKeys( + db.getSession(), singletonList(measure.getComponentUuid()), singletonList("nonexistent"))).isEmpty(); + + assertThat(underTest.selectByComponentUuidsAndMetricKeys( + db.getSession(), singletonList(measure.getComponentUuid()), singletonList(metricKey))).isNotEmpty(); + + MeasureDto m = newMeasure().addValue("foo", "bar"); + underTest.insert(db.getSession(), m); + } + + @Test + void selectByComponentUuidsAndMetricKeys_with_many_components_and_many_metrics() { + String metric1 = "metric1"; + String metric2 = "metric2"; + String nonRequestedMetric = "nonRequestedMetric"; + + String component1 = "component1"; + String component1measure1 = "component1measure1"; + underTest.insert(db.getSession(), + newMeasure().setComponentUuid(component1) + .addValue(metric1, component1measure1) + .addValue(nonRequestedMetric, randomAlphabetic(7))); + + String component2 = "component2"; + String component2measure1 = "component2measure1"; + String component2measure2 = "component2measure2"; + underTest.insert(db.getSession(), + newMeasure().setComponentUuid(component2) + .addValue(metric1, component2measure1) + .addValue(metric2, component2measure2) + .addValue(nonRequestedMetric, randomAlphabetic(7))); + + String nonRequestedComponent = "nonRequestedComponent"; + underTest.insert(db.getSession(), + newMeasure().setComponentUuid(nonRequestedComponent).addValue(metric1, randomAlphabetic(7))); + + List measureDtos = underTest.selectByComponentUuidsAndMetricKeys( + db.getSession(), Arrays.asList(component1, component2), Arrays.asList(metric1, metric2)); + + assertThat(measureDtos.stream().map(MeasureDto::getComponentUuid)) + .containsExactlyInAnyOrder(component1, component2); + + assertThat(measureDtos).flatExtracting(m -> m.getMetricValues().entrySet().stream() + .map(entry -> tuple(m.getComponentUuid(), entry.getKey(), entry.getValue())) + .toList()) + .containsExactlyInAnyOrder( + tuple(component1, metric1, component1measure1), + tuple(component2, metric1, component2measure1), + tuple(component2, metric2, component2measure2) + ); + } + @Test void select_branch_measure_hashes() { MeasureDto measure1 = new MeasureDto() @@ -104,4 +289,8 @@ class MeasureDaoTest { assertThat(selected).usingRecursiveComparison().isEqualTo(dto); }); } + + private static double getDoubleValue() { + return RandomUtils.nextInt(100); + } } diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/measure/MeasureDtoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/measure/MeasureDtoTest.java index 9299402942d..7a582fc1320 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/measure/MeasureDtoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/measure/MeasureDtoTest.java @@ -19,14 +19,37 @@ */ package org.sonar.db.measure; +import java.util.List; import java.util.Map; import java.util.TreeMap; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; import static org.assertj.core.api.Assertions.assertThat; class MeasureDtoTest { + @ParameterizedTest + @MethodSource("valuesOfDifferentTypes") + void getString_returns_string_value(Object value) { + String metricKey = randomAlphabetic(7); + MeasureDto measureDto = new MeasureDto().addValue(metricKey, value); + assertThat(measureDto.getString(metricKey)).isEqualTo(String.valueOf(value)); + } + + private static List valuesOfDifferentTypes() { + return List.of(2, 3.14, "foo"); + } + + @Test + void getString_returns_null_for_nonexistent_metric() { + String metricKey = randomAlphabetic(7); + MeasureDto measureDto = new MeasureDto(); + assertThat(measureDto.getString(metricKey)).isNull(); + } + @Test void compute_json_value_hash() { MeasureDto measureDto = new MeasureDto(); diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/metric/MetricDtoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/metric/MetricDtoTest.java index f010d0fe2dd..4dc04b7e57d 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/metric/MetricDtoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/metric/MetricDtoTest.java @@ -19,11 +19,27 @@ */ package org.sonar.db.metric; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.sonar.api.measures.Metric; import static com.google.common.base.Strings.repeat; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.sonar.api.measures.Metric.ValueType.BOOL; +import static org.sonar.api.measures.Metric.ValueType.DATA; +import static org.sonar.api.measures.Metric.ValueType.DISTRIB; +import static org.sonar.api.measures.Metric.ValueType.FLOAT; +import static org.sonar.api.measures.Metric.ValueType.INT; +import static org.sonar.api.measures.Metric.ValueType.LEVEL; +import static org.sonar.api.measures.Metric.ValueType.MILLISEC; +import static org.sonar.api.measures.Metric.ValueType.PERCENT; +import static org.sonar.api.measures.Metric.ValueType.RATING; +import static org.sonar.api.measures.Metric.ValueType.STRING; +import static org.sonar.api.measures.Metric.ValueType.WORK_DUR; class MetricDtoTest { @@ -98,4 +114,26 @@ class MetricDtoTest { .isInstanceOf(IllegalArgumentException.class) .hasMessage("Metric domain length (65) is longer than the maximum authorized (64). '" + a65 + "' was provided."); } + + @ParameterizedTest + @MethodSource("metric_types") + void isNumeric_returns_true_for_numeric_types(Metric.ValueType type, boolean expected) { + assertThat(underTest.setValueType(type.name()).isNumeric()).isEqualTo(expected); + } + + private static Stream metric_types() { + return Stream.of( + Arguments.of(INT, true), + Arguments.of(FLOAT, true), + Arguments.of(PERCENT, true), + Arguments.of(BOOL, true), + Arguments.of(STRING, false), + Arguments.of(MILLISEC, true), + Arguments.of(DATA, false), + Arguments.of(LEVEL, false), + Arguments.of(DISTRIB, false), + Arguments.of(RATING, true), + Arguments.of(WORK_DUR, true) + ); + } } diff --git a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/measure/MeasureDbTester.java b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/measure/MeasureDbTester.java index 5c2f3e2eaf6..f834b2de6f7 100644 --- a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/measure/MeasureDbTester.java +++ b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/measure/MeasureDbTester.java @@ -113,8 +113,7 @@ public class MeasureDbTester { .setComponentUuid(component.uuid()) .setBranchUuid(component.branchUuid()); Arrays.stream(consumers).forEach(c -> c.accept(dto)); - dto.computeJsonValueHash(); - dbClient.measureDao().insert(db.getSession(), dto); + dbClient.measureDao().insertOrUpdate(db.getSession(), dto); db.getSession().commit(); return dto; } diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureComputerImplIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureComputerImplIT.java index 9cec87b46f7..acd661721b7 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureComputerImplIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureComputerImplIT.java @@ -86,7 +86,7 @@ public class LiveMeasureComputerImplIT { project = db.components().insertPublicProject().getMainBranchComponent(); branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), project.uuid()).get(); - db.measures().insertLiveMeasure(project, metric2, lm -> lm.setValue(1d)); + db.measures().insertMeasure(project, m -> m.addValue(metric2.getKey(), 1d)); when(componentIndexFactory.create(any(), any())).thenReturn(componentIndex); when(measureUpdateFormulaFactory.getFormulaMetrics()).thenReturn(Set.of(toMetric(metric1), toMetric(metric2))); @@ -104,11 +104,14 @@ public class LiveMeasureComputerImplIT { assertThat(treeUpdater.getMeasureMatrix()).isNotNull(); // measure matrix was loaded with formula's metrics and measures - assertThat(treeUpdater.getMeasureMatrix().getMetricByUuid(metric2.getUuid())).isNotNull(); + assertThat(treeUpdater.getMeasureMatrix().getMetric(metric2.getKey())).isNotNull(); assertThat(treeUpdater.getMeasureMatrix().getMeasure(project, metric2.getKey()).get().getValue()).isEqualTo(1d); // new measures were persisted - assertThat(db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), project.uuid(), metric1.getKey()).get().getValue()).isEqualTo(2d); + assertThat(db.getDbClient().measureDao().selectMeasure(db.getSession(), project.uuid())) + .isPresent() + .get() + .satisfies(measure -> assertThat(measure.getMetricValues()).containsEntry(metric1.getKey(),2D)); } @Test @@ -134,7 +137,7 @@ public class LiveMeasureComputerImplIT { when(componentIndex.getAllUuids()).thenReturn(Set.of(project.uuid())); MetricDto alertStatusMetric = db.measures().insertMetric(m -> m.setKey(ALERT_STATUS_KEY)); - db.measures().insertLiveMeasure(project, alertStatusMetric, lm -> lm.setData("OK")); + db.measures().insertMeasure(project, m -> m.getMetricValues().put(alertStatusMetric.getKey(), "OK")); when(qGateComputer.loadQualityGate(db.getSession(), db.components().getProjectDtoByMainBranch(project), branch)).thenReturn(qualityGate); when(qGateComputer.refreshGateStatus(eq(project), eq(qualityGate), any(MeasureMatrix.class), eq(configuration))).thenReturn(newQualityGate); diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImplIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImplIT.java index 23fbe967485..5ce72d50e46 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImplIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImplIT.java @@ -36,7 +36,7 @@ import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; import org.sonar.db.component.SnapshotDto; import org.sonar.db.issue.IssueDto; -import org.sonar.db.measure.LiveMeasureDto; +import org.sonar.db.measure.MeasureDto; import org.sonar.db.metric.MetricDto; import org.sonar.db.newcodeperiod.NewCodePeriodType; import org.sonar.db.rule.RuleDto; @@ -51,6 +51,7 @@ import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_KEY; import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_KEY; import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY; import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY; +import static org.sonar.api.measures.Metric.ValueType.*; public class LiveMeasureTreeUpdaterImplIT { @Rule @@ -79,8 +80,8 @@ public class LiveMeasureTreeUpdaterImplIT { file2 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir)); // other needed data - metricDto = db.measures().insertMetric(m -> m.setValueType(Metric.ValueType.INT.name())); - metric = new Metric.Builder(metricDto.getKey(), metricDto.getShortName(), Metric.ValueType.valueOf(metricDto.getValueType())).create(); + metricDto = db.measures().insertMetric(m -> m.setValueType(INT.name())); + metric = new Metric.Builder(metricDto.getKey(), metricDto.getShortName(), valueOf(metricDto.getValueType())).create(); matrix = new MeasureMatrix(List.of(project, dir, file1, file2), List.of(metricDto), List.of()); componentIndex = new ComponentIndexImpl(db.getDbClient()); } @@ -91,16 +92,16 @@ public class LiveMeasureTreeUpdaterImplIT { treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new AggregateValuesFormula()); componentIndex.load(db.getSession(), List.of(file1)); - List initialValues = List.of( - new LiveMeasureDto().setComponentUuid(file1.uuid()).setValue(1d).setMetricUuid(metricDto.getUuid()), - new LiveMeasureDto().setComponentUuid(file2.uuid()).setValue(1d).setMetricUuid(metricDto.getUuid()), - new LiveMeasureDto().setComponentUuid(dir.uuid()).setValue(1d).setMetricUuid(metricDto.getUuid()), - new LiveMeasureDto().setComponentUuid(project.uuid()).setValue(1d).setMetricUuid(metricDto.getUuid()) + List initialValues = List.of( + new MeasureDto().setComponentUuid(file1.uuid()).addValue(metricDto.getKey(), 1d), + new MeasureDto().setComponentUuid(file2.uuid()).addValue(metricDto.getKey(), 1d), + new MeasureDto().setComponentUuid(dir.uuid()).addValue(metricDto.getKey(), 1d), + new MeasureDto().setComponentUuid(project.uuid()).addValue(metricDto.getKey(), 1d) ); matrix = new MeasureMatrix(List.of(project, dir, file1, file2), List.of(metricDto), initialValues); treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix); - assertThat(matrix.getChanged()).extracting(LiveMeasureDto::getComponentUuid).containsOnly(project.uuid(), dir.uuid()); + assertThat(matrix.getChanged()).extracting(MeasureMatrix.Measure::getComponentUuid).containsOnly(project.uuid(), dir.uuid()); assertThat(matrix.getMeasure(project, metric.getKey()).get().getValue()).isEqualTo(4d); assertThat(matrix.getMeasure(dir, metric.getKey()).get().getValue()).isEqualTo(3d); assertThat(matrix.getMeasure(file1, metric.getKey()).get().getValue()).isEqualTo(1d); @@ -115,7 +116,7 @@ public class LiveMeasureTreeUpdaterImplIT { componentIndex.load(db.getSession(), List.of(file1)); treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix); - assertThat(matrix.getChanged()).extracting(LiveMeasureDto::getComponentUuid).containsOnly(project.uuid(), dir.uuid(), file1.uuid()); + assertThat(matrix.getChanged()).extracting(MeasureMatrix.Measure::getComponentUuid).containsOnly(project.uuid(), dir.uuid(), file1.uuid()); assertThat(matrix.getMeasure(project, metric.getKey()).get().getValue()).isEqualTo(1d); assertThat(matrix.getMeasure(dir, metric.getKey()).get().getValue()).isEqualTo(1d); assertThat(matrix.getMeasure(file1, metric.getKey()).get().getValue()).isEqualTo(1d); @@ -130,7 +131,7 @@ public class LiveMeasureTreeUpdaterImplIT { componentIndex.load(db.getSession(), List.of(file1)); treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix); - assertThat(matrix.getChanged()).extracting(LiveMeasureDto::getComponentUuid).isEmpty(); + assertThat(matrix.getChanged()).extracting(MeasureMatrix.Measure::getComponentUuid).isEmpty(); } @Test @@ -142,7 +143,7 @@ public class LiveMeasureTreeUpdaterImplIT { componentIndex.load(db.getSession(), List.of(file1)); treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix); - assertThat(matrix.getChanged()).extracting(LiveMeasureDto::getComponentUuid).containsOnly(project.uuid(), dir.uuid(), file1.uuid()); + assertThat(matrix.getChanged()).extracting(MeasureMatrix.Measure::getComponentUuid).containsOnly(project.uuid(), dir.uuid(), file1.uuid()); } @Test @@ -153,7 +154,7 @@ public class LiveMeasureTreeUpdaterImplIT { componentIndex.load(db.getSession(), List.of(file1)); treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix); - assertThat(matrix.getChanged()).extracting(LiveMeasureDto::getComponentUuid).containsOnly(project.uuid(), dir.uuid(), file1.uuid()); + assertThat(matrix.getChanged()).extracting(MeasureMatrix.Measure::getComponentUuid).containsOnly(project.uuid(), dir.uuid(), file1.uuid()); } @Test @@ -188,8 +189,11 @@ public class LiveMeasureTreeUpdaterImplIT { @Test public void context_calculates_hotspot_counts_from_percentage() { - List metrics = List.of(new MetricDto().setKey(SECURITY_HOTSPOTS_KEY), new MetricDto().setKey(SECURITY_HOTSPOTS_REVIEWED_KEY), - new MetricDto().setKey(SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY), new MetricDto().setKey(SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY)); + List metrics = List.of( + new MetricDto().setKey(SECURITY_HOTSPOTS_KEY).setValueType(PERCENT.name()), + new MetricDto().setKey(SECURITY_HOTSPOTS_REVIEWED_KEY).setValueType(PERCENT.name()), + new MetricDto().setKey(SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY).setValueType(INT.name()), + new MetricDto().setKey(SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY).setValueType(INT.name())); componentIndex.load(db.getSession(), List.of(file1)); matrix = new MeasureMatrix(List.of(project, dir, file1, file2), metrics, List.of()); @@ -207,8 +211,11 @@ public class LiveMeasureTreeUpdaterImplIT { @Test public void context_calculates_new_hotspot_counts_from_percentage() { - List metrics = List.of(new MetricDto().setKey(NEW_SECURITY_HOTSPOTS_KEY), new MetricDto().setKey(NEW_SECURITY_HOTSPOTS_REVIEWED_KEY), - new MetricDto().setKey(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY), new MetricDto().setKey(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY)); + List metrics = List.of( + new MetricDto().setKey(NEW_SECURITY_HOTSPOTS_KEY).setValueType(PERCENT.name()), + new MetricDto().setKey(NEW_SECURITY_HOTSPOTS_REVIEWED_KEY).setValueType(PERCENT.name()), + new MetricDto().setKey(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY).setValueType(INT.name()), + new MetricDto().setKey(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY).setValueType(INT.name())); componentIndex.load(db.getSession(), List.of(file1)); matrix = new MeasureMatrix(List.of(project, dir, file1, file2), metrics, List.of()); @@ -226,8 +233,11 @@ public class LiveMeasureTreeUpdaterImplIT { @Test public void context_returns_hotspots_counts_from_measures() { - List metrics = List.of(new MetricDto().setKey(SECURITY_HOTSPOTS_KEY), new MetricDto().setKey(SECURITY_HOTSPOTS_REVIEWED_KEY), - new MetricDto().setKey(SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY), new MetricDto().setKey(SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY)); + List metrics = List.of( + new MetricDto().setKey(SECURITY_HOTSPOTS_KEY).setValueType(PERCENT.name()), + new MetricDto().setKey(SECURITY_HOTSPOTS_REVIEWED_KEY).setValueType(PERCENT.name()), + new MetricDto().setKey(SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY).setValueType(INT.name()), + new MetricDto().setKey(SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY).setValueType(INT.name())); componentIndex.load(db.getSession(), List.of(file1)); matrix = new MeasureMatrix(List.of(project, dir, file1, file2), metrics, List.of()); @@ -253,8 +263,7 @@ public class LiveMeasureTreeUpdaterImplIT { treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new AggregateValuesFormula()); componentIndex.load(db.getSession(), List.of(file1)); - List initialValues = - List.of(new LiveMeasureDto().setComponentUuid(file1.uuid()).setValue(1d).setMetricUuid(metricDto.getUuid())); + List initialValues = List.of(new MeasureDto().setComponentUuid(file1.uuid()).addValue(metricDto.getKey(), 1d)); matrix = new MeasureMatrix(List.of(project, dir, file1, file2), List.of(metricDto), initialValues); treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix); @@ -268,12 +277,11 @@ public class LiveMeasureTreeUpdaterImplIT { treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new SetValuesFormula()); componentIndex.load(db.getSession(), List.of(file1)); - List initialValues = - List.of(new LiveMeasureDto().setComponentUuid(file1.uuid()).setValue(1d).setMetricUuid(metricDto.getUuid())); + List initialValues = List.of(new MeasureDto().setComponentUuid(file1.uuid()).addValue(metricDto.getKey(), 1d)); matrix = new MeasureMatrix(List.of(project, dir, file1, file2), List.of(metricDto), initialValues); treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix); - assertThat(matrix.getChanged()).extracting(LiveMeasureDto::getComponentUuid).containsOnly(project.uuid(), dir.uuid()); + assertThat(matrix.getChanged()).extracting(MeasureMatrix.Measure::getComponentUuid).containsOnly(project.uuid(), dir.uuid()); assertThat(matrix.getMeasure(project, metric.getKey()).get().getValue()).isEqualTo(1d); assertThat(matrix.getMeasure(dir, metric.getKey()).get().getValue()).isEqualTo(1d); assertThat(matrix.getMeasure(file1, metric.getKey()).get().getValue()).isEqualTo(1d); diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveQualityGateComputerImplIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveQualityGateComputerImplIT.java index c4694aba9f4..a9e4b9e5f48 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveQualityGateComputerImplIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/live/LiveQualityGateComputerImplIT.java @@ -35,7 +35,7 @@ import org.sonar.db.component.BranchDto; import org.sonar.db.component.BranchType; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; -import org.sonar.db.measure.LiveMeasureDto; +import org.sonar.db.measure.MeasureDto; import org.sonar.db.metric.MetricDto; import org.sonar.db.project.ProjectDto; import org.sonar.db.qualitygate.QualityGateConditionDto; @@ -53,6 +53,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; +import static org.sonar.db.measure.MeasureTesting.newMeasure; import static org.sonar.db.metric.MetricTesting.newMetricDto; public class LiveQualityGateComputerImplIT { @@ -118,8 +119,8 @@ public class LiveQualityGateComputerImplIT { public void refreshGateStatus_generates_gate_related_measures() { ComponentDto project = ComponentTesting.newPublicProjectDto(); MetricDto conditionMetric = newMetricDto(); - MetricDto statusMetric = newMetricDto().setKey(CoreMetrics.ALERT_STATUS_KEY); - MetricDto detailsMetric = newMetricDto().setKey(CoreMetrics.QUALITY_GATE_DETAILS_KEY); + MetricDto statusMetric = newMetricDto().setKey(CoreMetrics.ALERT_STATUS_KEY).setValueType(Metric.ValueType.STRING.name()); + MetricDto detailsMetric = newMetricDto().setKey(CoreMetrics.QUALITY_GATE_DETAILS_KEY).setValueType(Metric.ValueType.STRING.name()); Condition condition = new Condition(conditionMetric.getKey(), Condition.Operator.GREATER_THAN, "10"); QualityGate gate = new QualityGate("1", "foo", ImmutableSet.of(condition)); MeasureMatrix matrix = new MeasureMatrix(singleton(project), asList(conditionMetric, statusMetric, detailsMetric), emptyList()); @@ -133,8 +134,8 @@ public class LiveQualityGateComputerImplIT { assertThat(result.getEvaluatedConditions()) .extracting(EvaluatedCondition::getStatus) .containsExactly(EvaluatedCondition.EvaluationStatus.OK); - assertThat(matrix.getMeasure(project, CoreMetrics.ALERT_STATUS_KEY).get().getDataAsString()).isEqualTo(Metric.Level.OK.name()); - assertThat(matrix.getMeasure(project, CoreMetrics.QUALITY_GATE_DETAILS_KEY).get().getDataAsString()) + assertThat(matrix.getMeasure(project, CoreMetrics.ALERT_STATUS_KEY).get().stringValue()).isEqualTo(Metric.Level.OK.name()); + assertThat(matrix.getMeasure(project, CoreMetrics.QUALITY_GATE_DETAILS_KEY).get().stringValue()) .isNotEmpty() // json format .startsWith("{").endsWith("}"); @@ -149,9 +150,9 @@ public class LiveQualityGateComputerImplIT { MetricDto statusMetric = newMetricDto().setKey(CoreMetrics.ALERT_STATUS_KEY); MetricDto detailsMetric = newMetricDto().setKey(CoreMetrics.QUALITY_GATE_DETAILS_KEY); QualityGate gate = new QualityGate("1", "foo", Collections.emptySet()); - LiveMeasureDto numericMeasure = new LiveMeasureDto().setMetricUuid(numericMetric.getUuid()).setValue(1.23).setComponentUuid(project.uuid()); - LiveMeasureDto numericNewMeasure = new LiveMeasureDto().setMetricUuid(numericNewMetric.getUuid()).setValue(8.9).setComponentUuid(project.uuid()); - LiveMeasureDto stringMeasure = new LiveMeasureDto().setMetricUuid(stringMetric.getUuid()).setData("bar").setComponentUuid(project.uuid()); + MeasureDto numericMeasure = newMeasure(project, numericMetric, 1.23); + MeasureDto numericNewMeasure = newMeasure(project, numericNewMetric, 8.9); + MeasureDto stringMeasure = newMeasure(project, stringMetric, "bar"); MeasureMatrix matrix = new MeasureMatrix(singleton(project), asList(statusMetric, detailsMetric, numericMetric, numericNewMetric, stringMetric), asList(numericMeasure, numericNewMeasure, stringMeasure)); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java index 6c5bfc72a0f..8d6d2f693f9 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java @@ -21,6 +21,7 @@ package org.sonar.server.measure.live; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -37,8 +38,7 @@ 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.measure.LiveMeasureComparator; -import org.sonar.db.measure.LiveMeasureDto; +import org.sonar.db.measure.MeasureDto; import org.sonar.db.metric.MetricDto; import org.sonar.db.project.ProjectDto; import org.sonar.server.es.Indexers; @@ -113,30 +113,46 @@ public class LiveMeasureComputerImpl implements LiveMeasureComputer { private MeasureMatrix loadMeasureMatrix(DbSession dbSession, Set componentUuids, QualityGate qualityGate) { Collection metricKeys = getKeysOfAllInvolvedMetrics(qualityGate); - Map metricsPerUuid = dbClient.metricDao().selectByKeys(dbSession, metricKeys).stream().collect(Collectors.toMap(MetricDto::getUuid, Function.identity())); - List measures = dbClient.liveMeasureDao().selectByComponentUuidsAndMetricUuids(dbSession, componentUuids, metricsPerUuid.keySet()); - return new MeasureMatrix(componentUuids, metricsPerUuid.values(), measures); + Map metricPerKey = + dbClient.metricDao().selectByKeys(dbSession, metricKeys).stream().collect(Collectors.toMap(MetricDto::getKey, Function.identity())); + List measures = dbClient.measureDao() + .selectByComponentUuidsAndMetricKeys(dbSession, componentUuids, metricPerKey.keySet()); + return new MeasureMatrix(componentUuids, metricPerKey.values(), measures); } private void persistAndIndex(DbSession dbSession, MeasureMatrix matrix, BranchDto branch) { // persist the measures that have been created or updated - matrix.getChanged().sorted(LiveMeasureComparator.INSTANCE).forEach(m -> dbClient.liveMeasureDao().insertOrUpdate(dbSession, m)); + Map measureDtoPerComponent = new HashMap<>(); + matrix.getChanged().sorted(MeasureMatrix.Measure.COMPARATOR) + .filter(m -> m.getValue() != null) + .forEach(m -> measureDtoPerComponent.compute(m.getComponentUuid(), (componentUuid, measureDto) -> { + if (measureDto == null) { + measureDto = new MeasureDto() + .setComponentUuid(componentUuid) + .setBranchUuid(m.getBranchUuid()); + } + return measureDto.addValue( + m.getMetricKey(), + m.getValue() + ); + })); + measureDtoPerComponent.values().forEach(m -> dbClient.measureDao().insertOrUpdate(dbSession, m)); projectIndexer.commitAndIndexBranches(dbSession, singleton(branch), Indexers.BranchEvent.MEASURE_CHANGE); } @CheckForNull private Metric.Level loadPreviousStatus(DbSession dbSession, ComponentDto branchComponent) { - Optional measure = dbClient.liveMeasureDao().selectMeasure(dbSession, branchComponent.uuid(), ALERT_STATUS_KEY); - if (measure.isEmpty()) { - return null; - } - - try { - return Metric.Level.valueOf(measure.get().getTextValue()); - } catch (IllegalArgumentException e) { - LoggerFactory.getLogger(LiveMeasureComputerImpl.class).trace("Failed to parse value of metric '{}'", ALERT_STATUS_KEY, e); - return null; - } + return dbClient.measureDao().selectMeasure(dbSession, branchComponent.uuid()) + .map(m -> m.getString(ALERT_STATUS_KEY)) + .map(m -> { + try { + return Metric.Level.valueOf(m); + } catch (IllegalArgumentException e) { + LoggerFactory.getLogger(LiveMeasureComputerImpl.class).trace("Failed to parse value of metric '{}'", ALERT_STATUS_KEY, e); + return null; + } + }) + .orElse(null); } private Set getKeysOfAllInvolvedMetrics(QualityGate gate) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java index 7cff81bb3d9..f8b8c0aa9b5 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java @@ -30,7 +30,6 @@ 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.db.measure.LiveMeasureDto; import org.sonar.server.measure.DebtRatingGrid; import org.sonar.server.measure.Rating; @@ -153,7 +152,7 @@ public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater { List children = componentIndex.getChildren(currentComponent); return children.stream() .flatMap(c -> matrix.getMeasure(c, currentFormula.getMetric().getKey()).stream()) - .map(LiveMeasureDto::getValue) + .map(MeasureMatrix.Measure::doubleValue) .filter(Objects::nonNull) .toList(); } @@ -162,7 +161,7 @@ public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater { List children = componentIndex.getChildren(currentComponent); return children.stream() .flatMap(c -> matrix.getMeasure(c, currentFormula.getMetric().getKey()).stream()) - .map(LiveMeasureDto::getTextValue) + .map(MeasureMatrix.Measure::stringValue) .filter(Objects::nonNull) .toList(); } @@ -184,7 +183,7 @@ public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater { return componentIndex.getChildren(currentComponent) .stream() .map(c -> matrix.getMeasure(c, SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY).or(() -> matrix.getMeasure(c, SECURITY_HOTSPOTS_KEY))) - .mapToLong(lmOpt -> lmOpt.flatMap(lm -> Optional.ofNullable(lm.getValue())).orElse(0D).longValue()) + .mapToLong(lmOpt -> lmOpt.map(FormulaContextImpl::getDoubleOrZero).orElse(0D).longValue()) .sum(); } @@ -201,7 +200,7 @@ public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater { return componentIndex.getChildren(currentComponent) .stream() .map(c -> matrix.getMeasure(c, NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY).or(() -> matrix.getMeasure(c, NEW_SECURITY_HOTSPOTS_KEY))) - .mapToLong(lmOpt -> lmOpt.flatMap(lm -> Optional.ofNullable(lm.getValue())).orElse(0D).longValue()) + .mapToLong(lmOpt -> lmOpt.map(FormulaContextImpl::getDoubleOrZero).orElse(0D).longValue()) .sum(); } @@ -213,21 +212,19 @@ public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater { } private long getHotspotsReviewed(ComponentDto c, String metricKey, String percMetricKey, String hotspotsMetricKey) { - Optional measure = matrix.getMeasure(c, metricKey); - return measure.map(lm -> Optional.ofNullable(lm.getValue()).orElse(0D).longValue()) + Optional measure = matrix.getMeasure(c, metricKey); + return measure.map(lm -> getDoubleOrZero(lm).longValue()) .orElseGet(() -> matrix.getMeasure(c, percMetricKey) .flatMap(percentage -> matrix.getMeasure(c, hotspotsMetricKey) .map(hotspots -> { - double perc = Optional.ofNullable(percentage.getValue()).orElse(0D) / 100D; - double toReview = Optional.ofNullable(hotspots.getValue()).orElse(0D); + double perc = getDoubleOrZero(percentage) / 100D; + double toReview = getDoubleOrZero(hotspots); double reviewed = (toReview * perc) / (1D - perc); return Math.round(reviewed); })) .orElse(0L)); } - - @Override public ComponentDto getComponent() { return currentComponent; @@ -240,14 +237,12 @@ public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater { @Override public Optional getValue(Metric metric) { - Optional measure = matrix.getMeasure(currentComponent, metric.getKey()); - return measure.map(LiveMeasureDto::getValue); + return matrix.getMeasure(currentComponent, metric.getKey()).map(MeasureMatrix.Measure::doubleValue); } @Override public Optional getText(Metric metric) { - Optional measure = matrix.getMeasure(currentComponent, metric.getKey()); - return measure.map(LiveMeasureDto::getTextValue); + return matrix.getMeasure(currentComponent, metric.getKey()).map(MeasureMatrix.Measure::stringValue); } @Override @@ -267,5 +262,9 @@ public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater { String metricKey = currentFormula.getMetric().getKey(); matrix.setValue(currentComponent, metricKey, value); } + + private static Double getDoubleOrZero(MeasureMatrix.Measure lm) { + return Optional.ofNullable(lm.doubleValue()).orElse(0D); + } } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java index 4f16100b4ab..f51050973a0 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java @@ -36,7 +36,6 @@ import org.sonar.db.DbSession; import org.sonar.db.component.BranchDto; import org.sonar.db.component.BranchType; import org.sonar.db.component.ComponentDto; -import org.sonar.db.measure.LiveMeasureDto; import org.sonar.db.metric.MetricDto; import org.sonar.db.project.ProjectDto; import org.sonar.db.qualitygate.QualityGateConditionDto; @@ -83,12 +82,12 @@ public class LiveQualityGateComputerImpl implements LiveQualityGateComputer { @Override public EvaluatedQualityGate refreshGateStatus(ComponentDto project, QualityGate gate, MeasureMatrix measureMatrix, Configuration configuration) { QualityGateEvaluator.Measures measures = metricKey -> { - Optional liveMeasureDto = measureMatrix.getMeasure(project, metricKey); - if (!liveMeasureDto.isPresent()) { + Optional measure = measureMatrix.getMeasure(project, metricKey); + if (measure.isEmpty()) { return Optional.empty(); } - MetricDto metric = measureMatrix.getMetricByUuid(liveMeasureDto.get().getMetricUuid()); - return Optional.of(new LiveMeasure(liveMeasureDto.get(), metric)); + MetricDto metric = measureMatrix.getMetric(measure.get().getMetricKey()); + return Optional.of(new LiveMeasure(measure.get(), metric)); }; EvaluatedQualityGate evaluatedGate = evaluator.evaluate(gate, measures, configuration); @@ -109,11 +108,11 @@ public class LiveQualityGateComputerImpl implements LiveQualityGateComputer { } private static class LiveMeasure implements QualityGateEvaluator.Measure { - private final LiveMeasureDto dto; + private final MeasureMatrix.Measure measure; private final MetricDto metric; - LiveMeasure(LiveMeasureDto dto, MetricDto metric) { - this.dto = dto; + LiveMeasure(MeasureMatrix.Measure measure, MetricDto metric) { + this.measure = measure; this.metric = metric; } @@ -124,15 +123,16 @@ public class LiveQualityGateComputerImpl implements LiveQualityGateComputer { @Override public OptionalDouble getValue() { - if (dto.getValue() == null) { + Double doubleValue = measure.doubleValue(); + if (doubleValue == null) { return OptionalDouble.empty(); } - return OptionalDouble.of(dto.getValue()); + return OptionalDouble.of(doubleValue); } @Override public Optional getStringValue() { - return Optional.ofNullable(dto.getTextValue()); + return Optional.ofNullable(measure.stringValue()); } } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java index 4677cbc7d5e..de0f7849206 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java @@ -23,8 +23,8 @@ import com.google.common.collect.ArrayTable; import com.google.common.collect.Table; import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,9 +34,10 @@ import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.db.component.ComponentDto; -import org.sonar.db.measure.LiveMeasureDto; +import org.sonar.db.measure.MeasureDto; import org.sonar.db.metric.MetricDto; import org.sonar.server.measure.Rating; @@ -55,33 +56,31 @@ class MeasureMatrix { private final Table table; private final Map metricsByKeys = new HashMap<>(); - private final Map metricsByUuids = new HashMap<>(); - MeasureMatrix(Collection components, Collection metrics, List dbMeasures) { + MeasureMatrix(Collection components, Collection metrics, List dbMeasures) { this(components.stream().map(ComponentDto::uuid).collect(Collectors.toSet()), metrics, dbMeasures); } - MeasureMatrix(Set componentUuids, Collection metrics, List dbMeasures) { + MeasureMatrix(Set componentUuids, Collection metrics, List dbMeasures) { for (MetricDto metric : metrics) { this.metricsByKeys.put(metric.getKey(), metric); - this.metricsByUuids.put(metric.getUuid(), metric); } this.table = ArrayTable.create(componentUuids, metricsByKeys.keySet()); - - for (LiveMeasureDto dbMeasure : dbMeasures) { - table.put(dbMeasure.getComponentUuid(), metricsByUuids.get(dbMeasure.getMetricUuid()).getKey(), new MeasureCell(dbMeasure)); - } - } - - MetricDto getMetricByUuid(String uuid) { - return requireNonNull(metricsByUuids.get(uuid), () -> String.format("Metric with uuid %s not found", uuid)); + dbMeasures.forEach(dbMeasure -> { + String branchUuid = dbMeasure.getBranchUuid(); + String componentUuid = dbMeasure.getComponentUuid(); + dbMeasure.getMetricValues().forEach((metricKey, value) -> { + Measure measure = new Measure(componentUuid, branchUuid, metricsByKeys.get(metricKey), value); + table.put(componentUuid, metricKey, new MeasureCell(measure)); + }); + }); } - private MetricDto getMetric(String key) { + MetricDto getMetric(String key) { return requireNonNull(metricsByKeys.get(key), () -> String.format("Metric with key %s not found", key)); } - Optional getMeasure(ComponentDto component, String metricKey) { + Optional getMeasure(ComponentDto component, String metricKey) { checkArgument(table.containsColumn(metricKey), "Metric with key %s is not registered", metricKey); MeasureCell cell = table.get(component.uuid(), metricKey); return cell == null ? Optional.empty() : Optional.of(cell.measure); @@ -92,30 +91,24 @@ class MeasureMatrix { } void setValue(ComponentDto component, String metricKey, Rating value) { - changeCell(component, metricKey, m -> { - m.setData(value.name()); - m.setValue((double) value.getIndex()); - }); + changeCell(component, metricKey, m -> m.setValue((double) value.getIndex())); } void setValue(ComponentDto component, String metricKey, @Nullable String data) { - changeCell(component, metricKey, m -> m.setData(data)); + changeCell(component, metricKey, m -> m.setValue(data)); } - Stream getChanged() { + Stream getChanged() { return table.values().stream() .filter(Objects::nonNull) .filter(MeasureCell::isChanged) .map(MeasureCell::getMeasure); } - private void changeCell(ComponentDto component, String metricKey, Consumer changer) { + private void changeCell(ComponentDto component, String metricKey, Consumer changer) { MeasureCell cell = table.get(component.uuid(), metricKey); if (cell == null) { - LiveMeasureDto measure = new LiveMeasureDto() - .setComponentUuid(component.uuid()) - .setProjectUuid(component.branchUuid()) - .setMetricUuid(metricsByKeys.get(metricKey).getUuid()); + Measure measure = new Measure(component.uuid(), component.branchUuid(), metricsByKeys.get(metricKey), null); cell = new MeasureCell(measure); table.put(component.uuid(), metricKey, cell); } @@ -135,24 +128,75 @@ class MeasureMatrix { } private static class MeasureCell { - private final LiveMeasureDto measure; - private final Double initialValue; - private final byte[] initialData; - private final String initialTextValue; + private final Measure measure; + private final Object initialValue; - private MeasureCell(LiveMeasureDto measure) { + private MeasureCell(Measure measure) { this.measure = measure; this.initialValue = measure.getValue(); - this.initialData = measure.getData(); - this.initialTextValue = measure.getTextValue(); } - public LiveMeasureDto getMeasure() { + public Measure getMeasure() { return measure; } public boolean isChanged() { - return !Objects.equals(initialValue, measure.getValue()) || !Arrays.equals(initialData, measure.getData()) || !Objects.equals(initialTextValue, measure.getTextValue()); + return !Objects.equals(initialValue, measure.getValue()); + } + } + + static class Measure { + static final Comparator COMPARATOR = Comparator + .comparing((Measure m) -> m.componentUuid) + .thenComparing(m -> m.metricDto.getKey()); + + private final String componentUuid; + private final String branchUuid; + private final MetricDto metricDto; + private Object value; + + Measure(String componentUuid, String branchUuid, MetricDto metricDto, @Nullable Object value) { + this.componentUuid = componentUuid; + this.branchUuid = branchUuid; + this.metricDto = metricDto; + this.value = value; + } + + @CheckForNull + public Double doubleValue() { + if (value == null || !metricDto.isNumeric()) { + return null; + } + return Double.parseDouble(value.toString()); + } + + @CheckForNull + public String stringValue() { + if (value == null || metricDto.isNumeric()) { + return null; + } + return String.valueOf(value); + } + + public void setValue(@Nullable Object newValue) { + this.value = newValue; + } + + public String getBranchUuid() { + return branchUuid; + } + + public String getComponentUuid() { + return componentUuid; + } + + public String getMetricKey() { + return metricDto.getKey(); + } + + @Nullable + public Object getValue() { + return value; } } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java index b234c62c4bc..9d39b3ca074 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java @@ -21,58 +21,60 @@ package org.sonar.server.measure.live; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Optional; import javax.annotation.Nullable; import org.junit.Test; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; -import org.sonar.db.measure.LiveMeasureDto; +import org.sonar.db.measure.MeasureDto; import org.sonar.db.metric.MetricDto; -import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.sonar.db.measure.MeasureTesting.newMeasure; import static org.sonar.db.metric.MetricTesting.newMetricDto; public class MeasureMatrixTest { private static final ComponentDto PROJECT = ComponentTesting.newPublicProjectDto(); private static final ComponentDto FILE = ComponentTesting.newFileDto(PROJECT); - private static final MetricDto METRIC_1 = newMetricDto().setUuid("100"); + private static final MetricDto METRIC_1 = newMetricDto().setUuid("100").setValueType("STRING"); private static final MetricDto METRIC_2 = newMetricDto().setUuid("200"); @Test public void getMetric() { - Collection metrics = asList(METRIC_1, METRIC_2); + Collection metrics = List.of(METRIC_1, METRIC_2); - MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT, FILE), metrics, new ArrayList<>()); + MeasureMatrix underTest = new MeasureMatrix(List.of(PROJECT, FILE), metrics, new ArrayList<>()); - assertThat(underTest.getMetricByUuid(METRIC_2.getUuid())).isSameAs(METRIC_2); + assertThat(underTest.getMetric(METRIC_2.getKey())).isSameAs(METRIC_2); } @Test public void getMetric_fails_if_metric_is_not_registered() { - Collection metrics = asList(METRIC_1); - MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT, FILE), metrics, new ArrayList<>()); + Collection metrics = List.of(METRIC_1); + MeasureMatrix underTest = new MeasureMatrix(List.of(PROJECT, FILE), metrics, new ArrayList<>()); - assertThatThrownBy(() -> underTest.getMetricByUuid(METRIC_2.getUuid())) + String metricKey = METRIC_2.getKey(); + assertThatThrownBy(() -> underTest.getMetric(metricKey)) .isInstanceOf(NullPointerException.class) - .hasMessage("Metric with uuid " + METRIC_2.getUuid() + " not found"); + .hasMessage("Metric with key " + metricKey + " not found"); } @Test public void getValue_returns_empty_if_measure_is_absent() { MetricDto metric = newMetricDto(); - LiveMeasureDto measure = newMeasure(metric, PROJECT).setValue(null); - MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(metric), asList(measure)); + MeasureDto measure = newMeasure(PROJECT, metric, null); + MeasureMatrix underTest = new MeasureMatrix(List.of(PROJECT), List.of(metric), List.of(measure)); assertThat(underTest.getMeasure(FILE, metric.getKey())).isEmpty(); } @Test public void getMeasure_throws_IAE_if_metric_is_not_registered() { - MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(METRIC_1), emptyList()); + MeasureMatrix underTest = new MeasureMatrix(List.of(PROJECT), List.of(METRIC_1), emptyList()); assertThatThrownBy(() -> underTest.getMeasure(PROJECT, "_missing_")) .isInstanceOf(IllegalArgumentException.class) @@ -82,8 +84,8 @@ public class MeasureMatrixTest { @Test public void setValue_double_rounds_up_and_updates_value() { MetricDto metric = newMetricDto().setDecimalScale(2); - LiveMeasureDto measure = newMeasure(metric, PROJECT).setValue(1.23); - MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(metric), asList(measure)); + MeasureDto measure = newMeasure(PROJECT, metric, 1.23); + MeasureMatrix underTest = new MeasureMatrix(List.of(PROJECT), List.of(metric), List.of(measure)); underTest.setValue(PROJECT, metric.getKey(), 3.14159); @@ -95,7 +97,7 @@ public class MeasureMatrixTest { } private void verifyValue(MeasureMatrix underTest, ComponentDto component, MetricDto metric, @Nullable Double expectedValue) { - Optional measure = underTest.getMeasure(component, metric.getKey()); + Optional measure = underTest.getMeasure(component, metric.getKey()); assertThat(measure).isPresent(); assertThat(measure.get().getValue()).isEqualTo(expectedValue); } @@ -103,8 +105,8 @@ public class MeasureMatrixTest { @Test public void setValue_double_does_nothing_if_value_is_unchanged() { MetricDto metric = newMetricDto().setDecimalScale(2); - LiveMeasureDto measure = newMeasure(metric, PROJECT).setValue(3.14); - MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(metric), asList(measure)); + MeasureDto measure = newMeasure(PROJECT, metric, 3.14); + MeasureMatrix underTest = new MeasureMatrix(List.of(PROJECT), List.of(metric), List.of(measure)); underTest.setValue(PROJECT, metric.getKey(), 3.14159); @@ -114,27 +116,23 @@ public class MeasureMatrixTest { @Test public void setValue_String_does_nothing_if_value_is_not_changed() { - LiveMeasureDto measure = newMeasure(METRIC_1, PROJECT).setData("foo"); - MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT, FILE), asList(METRIC_1), asList(measure)); + MeasureDto measure = newMeasure(PROJECT, METRIC_1, "foo"); + MeasureMatrix underTest = new MeasureMatrix(List.of(PROJECT, FILE), List.of(METRIC_1), List.of(measure)); underTest.setValue(PROJECT, METRIC_1.getKey(), "foo"); - assertThat(underTest.getMeasure(PROJECT, METRIC_1.getKey()).get().getDataAsString()).isEqualTo("foo"); + assertThat(underTest.getMeasure(PROJECT, METRIC_1.getKey()).get().stringValue()).isEqualTo("foo"); assertThat(underTest.getChanged()).isEmpty(); } @Test public void setValue_String_updates_value() { - LiveMeasureDto measure = newMeasure(METRIC_1, PROJECT).setData("foo"); - MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT, FILE), asList(METRIC_1), asList(measure)); + MeasureDto measure = newMeasure(PROJECT, METRIC_1, "foo"); + MeasureMatrix underTest = new MeasureMatrix(List.of(PROJECT, FILE), List.of(METRIC_1), List.of(measure)); underTest.setValue(PROJECT, METRIC_1.getKey(), "bar"); - assertThat(underTest.getMeasure(PROJECT, METRIC_1.getKey()).get().getDataAsString()).isEqualTo("bar"); - assertThat(underTest.getChanged()).extracting(LiveMeasureDto::getDataAsString).containsExactly("bar"); - } - - private LiveMeasureDto newMeasure(MetricDto metric, ComponentDto component) { - return new LiveMeasureDto().setMetricUuid(metric.getUuid()).setData("foo").setComponentUuid(component.uuid()); + assertThat(underTest.getMeasure(PROJECT, METRIC_1.getKey()).get().stringValue()).isEqualTo("bar"); + assertThat(underTest.getChanged()).extracting(MeasureMatrix.Measure::stringValue).containsExactly("bar"); } } -- 2.39.5