]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11515 Add RegisterQualityProfileStatusStep
authorBenoît Gianinetti <benoit.gianinetti@sonarsource.com>
Fri, 30 Nov 2018 15:56:43 +0000 (16:56 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 3 Dec 2018 19:20:59 +0000 (20:20 +0100)
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/MutableQProfileStatusRepository.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QProfileStatusRepository.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QProfileStatusRepositoryImpl.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/RegisterQualityProfileStatusStep.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualityprofile/QProfileStatusRepositoryImplTest.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/RegisterQualityProfileStatusStepTest.java [new file with mode: 0644]

index a6bc5c9d28a5b7ead0c2272b02ddadf863adabeb..04ec32ad55f2ab889f5ea9dd538a98042a5193f0 100644 (file)
@@ -110,6 +110,7 @@ import org.sonar.ce.task.projectanalysis.qualitymodel.NewReliabilityAndSecurityR
 import org.sonar.ce.task.projectanalysis.qualitymodel.RatingSettings;
 import org.sonar.ce.task.projectanalysis.qualitymodel.ReliabilityAndSecurityRatingMeasuresVisitor;
 import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolderImpl;
+import org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepositoryImpl;
 import org.sonar.ce.task.projectanalysis.scm.ScmInfoDbLoader;
 import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepositoryImpl;
 import org.sonar.ce.task.projectanalysis.source.DbLineHashVersion;
@@ -218,6 +219,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
       NewLinesRepository.class,
       FileSourceDataComputer.class,
       SourceLineReadersFactory.class,
+      QProfileStatusRepositoryImpl.class,
 
       // issues
       RuleRepositoryImpl.class,
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/MutableQProfileStatusRepository.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/MutableQProfileStatusRepository.java
new file mode 100644 (file)
index 0000000..61d3f3e
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.ce.task.projectanalysis.qualityprofile;
+
+public interface MutableQProfileStatusRepository extends QProfileStatusRepository {
+  /**
+   * @throws IllegalStateException if the given quality profile is already registered
+   */
+  void register(String qpKey, Status status);
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QProfileStatusRepository.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QProfileStatusRepository.java
new file mode 100644 (file)
index 0000000..2fdf601
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.ce.task.projectanalysis.qualityprofile;
+
+import java.util.Optional;
+
+public interface QProfileStatusRepository {
+
+  Optional<Status> get(String qpKey);
+
+  enum Status {
+    /**
+     * the QP was used in the last analysis but not anymore in the current one.
+     */
+    REMOVED,
+    /**
+     * the QP was not used in the last analysis
+     */
+    ADDED,
+    /**
+     * the QP was used in the last and current analysis and a rule has changed
+     */
+    UPDATED,
+    /**
+     * neither the QP or a rule has changed since last analysis
+     */
+    UNCHANGED
+  }
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QProfileStatusRepositoryImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/QProfileStatusRepositoryImpl.java
new file mode 100644 (file)
index 0000000..3715519
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.ce.task.projectanalysis.qualityprofile;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.Nullable;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+public class QProfileStatusRepositoryImpl implements MutableQProfileStatusRepository {
+
+  private final Map<String, Status> statuses = new HashMap<>();
+
+  @Override
+  public Optional<Status> get(@Nullable String qpKey) {
+    return Optional.ofNullable(statuses.get(qpKey));
+  }
+
+  @Override
+  public void register(String qpKey, Status status) {
+    checkNotNull(qpKey, "qpKey can't be null");
+    checkNotNull(status, "status can't be null");
+    checkState(statuses.put(qpKey, status) == null, "Quality Profile '%s' is already registered", qpKey);
+  }
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/RegisterQualityProfileStatusStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/RegisterQualityProfileStatusStep.java
new file mode 100644 (file)
index 0000000..90240eb
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.ce.task.projectanalysis.qualityprofile;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit;
+import org.sonar.ce.task.projectanalysis.component.DepthTraversalTypeAwareCrawler;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
+import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter;
+import org.sonar.ce.task.projectanalysis.measure.Measure;
+import org.sonar.ce.task.projectanalysis.measure.MeasureRepository;
+import org.sonar.ce.task.projectanalysis.metric.MetricRepository;
+import org.sonar.ce.task.step.ComputationStep;
+import org.sonar.server.qualityprofile.QPMeasureData;
+import org.sonar.server.qualityprofile.QualityProfile;
+
+import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER;
+import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.ADDED;
+import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.REMOVED;
+import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.UNCHANGED;
+import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.UPDATED;
+
+public class RegisterQualityProfileStatusStep implements ComputationStep {
+
+  private TreeRootHolder treeRootHolder;
+  private MeasureRepository measureRepository;
+  private MetricRepository metricRepository;
+  private MutableQProfileStatusRepository qProfileStatusRepository;
+  private AnalysisMetadataHolder analysisMetadataHolder;
+
+  public RegisterQualityProfileStatusStep(TreeRootHolder treeRootHolder, MeasureRepository measureRepository, MetricRepository metricRepository,
+    MutableQProfileStatusRepository qProfileStatusRepository, AnalysisMetadataHolder analysisMetadataHolder) {
+    this.treeRootHolder = treeRootHolder;
+    this.measureRepository = measureRepository;
+    this.metricRepository = metricRepository;
+    this.qProfileStatusRepository = qProfileStatusRepository;
+    this.analysisMetadataHolder = analysisMetadataHolder;
+  }
+
+  @Override
+  public void execute(Context context) {
+    new DepthTraversalTypeAwareCrawler(
+      new TypeAwareVisitorAdapter(CrawlerDepthLimit.PROJECT, POST_ORDER) {
+        @Override
+        public void visitProject(Component tree) {
+          executeForProject(tree);
+        }
+      }).visit(treeRootHolder.getRoot());
+  }
+
+  private void executeForProject(Component project) {
+    measureRepository.getBaseMeasure(project, metricRepository.getByKey(CoreMetrics.QUALITY_PROFILES_KEY)).ifPresent(baseProfilesMeasure -> {
+      Map<String, QualityProfile> baseProfiles = parseJsonData(baseProfilesMeasure);
+      Map<String, QualityProfile> rawProfiles = analysisMetadataHolder
+        .getQProfilesByLanguage().values().stream()
+        .collect(Collectors.toMap(QualityProfile::getQpKey, q -> q));
+
+      registerNoMoreUsedProfiles(baseProfiles, rawProfiles);
+      registerNewOrUpdatedProfiles(baseProfiles, rawProfiles);
+    });
+  }
+
+  private void registerNoMoreUsedProfiles(Map<String, QualityProfile> baseProfiles, Map<String, QualityProfile> rawProfiles) {
+    for (QualityProfile baseProfile : baseProfiles.values()) {
+      if (!rawProfiles.containsKey(baseProfile.getQpKey())) {
+        register(baseProfile, REMOVED);
+      }
+    }
+  }
+
+  private void registerNewOrUpdatedProfiles(Map<String, QualityProfile> baseProfiles, Map<String, QualityProfile> rawProfiles) {
+    for (QualityProfile profile : rawProfiles.values()) {
+      QualityProfile baseProfile = baseProfiles.get(profile.getQpKey());
+      if (baseProfile == null) {
+        register(profile, ADDED);
+      } else if  (profile.getRulesUpdatedAt().after(baseProfile.getRulesUpdatedAt())) {
+        register(baseProfile, UPDATED);
+      } else {
+        register(baseProfile, UNCHANGED);
+      }
+    }
+  }
+
+  private void register(QualityProfile profile, QProfileStatusRepository.Status status) {
+    qProfileStatusRepository.register(profile.getQpKey(), status);
+  }
+
+  private static Map<String, QualityProfile> parseJsonData(Measure measure) {
+    String data = measure.getStringValue();
+    if (data == null) {
+      return Collections.emptyMap();
+    }
+    return QPMeasureData.fromJson(data).getProfilesByKey();
+  }
+
+  @Override
+  public String getDescription() {
+    return "Compute Quality Profile status";
+  }
+
+}
index dff4aabc1334bcbc5e53af12a041cd28545308ae..8ffb2b5312407bb8827caeab24bd37868211512e 100644 (file)
@@ -25,6 +25,7 @@ import org.sonar.ce.task.container.TaskContainer;
 import org.sonar.ce.task.projectanalysis.filemove.FileMoveDetectionStep;
 import org.sonar.ce.task.projectanalysis.measure.PostMeasuresComputationChecksStep;
 import org.sonar.ce.task.projectanalysis.purge.PurgeDatastoresStep;
+import org.sonar.ce.task.projectanalysis.qualityprofile.RegisterQualityProfileStatusStep;
 import org.sonar.ce.task.projectanalysis.source.PersistFileSourcesStep;
 import org.sonar.ce.task.step.ComputationStep;
 import org.sonar.ce.task.step.ExecuteStatelessInitExtensionsStep;
@@ -73,6 +74,7 @@ public class ReportComputationSteps extends AbstractComputationSteps {
     ComplexityMeasuresStep.class,
 
     LoadMeasureComputersStep.class,
+    RegisterQualityProfileStatusStep.class,
     ExecuteVisitorsStep.class,
 
     PostMeasuresComputationChecksStep.class,
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualityprofile/QProfileStatusRepositoryImplTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/qualityprofile/QProfileStatusRepositoryImplTest.java
new file mode 100644 (file)
index 0000000..687f10c
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.ce.task.projectanalysis.qualityprofile;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Optional;
+import java.util.stream.Stream;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RunWith(DataProviderRunner.class)
+public class QProfileStatusRepositoryImplTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  private QProfileStatusRepositoryImpl underTest;
+
+  @Before
+  public void setUp() {
+    underTest = new QProfileStatusRepositoryImpl();
+  }
+
+  @Test
+  @UseDataProvider("qualityProfileStatuses")
+  public void get_return_optional_of_status(QProfileStatusRepository.Status status) {
+    underTest.register("key", status);
+
+    assertThat(underTest.get("key")).isEqualTo(Optional.of(status));
+  }
+
+  @Test
+  @UseDataProvider("qualityProfileStatuses")
+  public void get_return_empty_for_qp_not_registered(QProfileStatusRepository.Status status) {
+    underTest.register("key", status);
+
+    assertThat(underTest.get("other_key")).isEqualTo(Optional.empty());
+  }
+
+  @Test
+  public void get_return_empty_for_null_qp_key() {
+    assertThat(underTest.get(null)).isEqualTo(Optional.empty());
+  }
+
+  @Test
+  @UseDataProvider("qualityProfileStatuses")
+  public void register_fails_with_NPE_if_qpKey_is_null(QProfileStatusRepository.Status status) {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("qpKey can't be null");
+
+    underTest.register(null, status);
+  }
+
+  @Test
+  public void register_fails_with_NPE_if_status_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("status can't be null");
+
+    underTest.register("key", null);
+  }
+
+  @Test
+  @UseDataProvider("qualityProfileStatuses")
+  public void register_fails_with_ISE_if_qp_is_already_registered(QProfileStatusRepository.Status status) {
+    underTest.register("key", status);
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Quality Profile 'key' is already registered");
+
+    underTest.register("key", status);
+  }
+
+  @DataProvider
+  public static Object[][] qualityProfileStatuses() {
+    return Stream.of(QProfileStatusRepository.Status.values())
+      .map(s -> new Object[] {s})
+      .toArray(Object[][]::new);
+  }
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/RegisterQualityProfileStatusStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/RegisterQualityProfileStatusStepTest.java
new file mode 100644 (file)
index 0000000..184c447
--- /dev/null
@@ -0,0 +1,179 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.ce.task.projectanalysis.step;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.ReportComponent;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
+import org.sonar.ce.task.projectanalysis.measure.Measure;
+import org.sonar.ce.task.projectanalysis.measure.MeasureRepository;
+import org.sonar.ce.task.projectanalysis.metric.Metric;
+import org.sonar.ce.task.projectanalysis.metric.MetricRepository;
+import org.sonar.ce.task.projectanalysis.qualityprofile.MutableQProfileStatusRepository;
+import org.sonar.ce.task.projectanalysis.qualityprofile.RegisterQualityProfileStatusStep;
+import org.sonar.ce.task.step.TestComputationStepContext;
+import org.sonar.server.qualityprofile.QPMeasureData;
+import org.sonar.server.qualityprofile.QualityProfile;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.ADDED;
+import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.REMOVED;
+import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.UNCHANGED;
+import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.UPDATED;
+
+
+public class RegisterQualityProfileStatusStepTest {
+
+  private static final String QP_NAME_1 = "qp_1";
+  private static final String QP_NAME_2 = "qp_2";
+  private static final String LANGUAGE_KEY_1 = "language_key1";
+  private static final String LANGUAGE_KEY_2 = "language_key_2";
+
+  @Rule
+  public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
+
+  private MetricRepository metricRepository = mock(MetricRepository.class);
+  private MeasureRepository measureRepository = mock(MeasureRepository.class);
+  private MutableQProfileStatusRepository qProfileStatusRepository = mock(MutableQProfileStatusRepository.class);
+  private AnalysisMetadataHolder analysisMetadataHolder = mock(AnalysisMetadataHolder.class);
+  private Metric qualityProfileMetric = mock(Metric.class);
+
+  private RegisterQualityProfileStatusStep underTest = new RegisterQualityProfileStatusStep(treeRootHolder, measureRepository, metricRepository, qProfileStatusRepository, analysisMetadataHolder);
+
+  @Before
+  public void setUp() {
+    when(metricRepository.getByKey(CoreMetrics.QUALITY_PROFILES_KEY)).thenReturn(qualityProfileMetric);
+    treeRootHolder.setRoot(ReportComponent.builder(Component.Type.PROJECT, 1).setUuid("uuid").setKey("key").build());
+  }
+
+  @Test
+  public void register_nothing_if_no_base_measure() {
+    when(measureRepository.getBaseMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.empty());
+
+    underTest.execute(new TestComputationStepContext());
+
+    verifyNoMoreInteractions(qProfileStatusRepository);
+  }
+
+  @Test
+  public void register_nothing_if_no_base_and_quality_profile_measure_is_empty() {
+    mockBaseQPMeasures(treeRootHolder.getRoot(), null);
+
+    underTest.execute(new TestComputationStepContext());
+
+    verifyNoMoreInteractions(qProfileStatusRepository);
+  }
+
+  @Test
+  public void register_removed_profile() {
+    QualityProfile qp = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date());
+
+    mockBaseQPMeasures(treeRootHolder.getRoot(), new QualityProfile[] {qp});
+
+    underTest.execute(new TestComputationStepContext());
+
+    verify(qProfileStatusRepository).register(eq(qp.getQpKey()), eq(REMOVED));
+    verifyNoMoreInteractions(qProfileStatusRepository);
+  }
+
+  @Test
+  public void register_added_profile() {
+    QualityProfile qp1 = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date(1000L));
+    QualityProfile qp2 = qp(QP_NAME_2, LANGUAGE_KEY_2, new Date(1000L));
+
+    mockBaseQPMeasures(treeRootHolder.getRoot(), arrayOf(qp1));
+    mockRawQProfiles(ImmutableList.of(qp1, qp2));
+    underTest.execute(new TestComputationStepContext());
+
+    verify(qProfileStatusRepository).register(eq(qp1.getQpKey()), eq(UNCHANGED));
+    verify(qProfileStatusRepository).register(eq(qp2.getQpKey()), eq(ADDED));
+    verifyNoMoreInteractions(qProfileStatusRepository);
+  }
+
+  @Test
+  public void register_updated_profile() {
+    QualityProfile qp1 = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date(1000L));
+    QualityProfile qp2 = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date(1200L));
+
+    mockBaseQPMeasures(treeRootHolder.getRoot(), arrayOf(qp1));
+    mockRawQProfiles(ImmutableList.of(qp2));
+    underTest.execute(new TestComputationStepContext());
+
+    verify(qProfileStatusRepository).register(eq(qp2.getQpKey()), eq(UPDATED));
+    verifyNoMoreInteractions(qProfileStatusRepository);
+  }
+
+  @Test
+  public void register_unchanged_profile() {
+    QualityProfile qp1 = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date(1000L));
+
+    mockBaseQPMeasures(treeRootHolder.getRoot(), arrayOf(qp1));
+    mockRawQProfiles(ImmutableList.of(qp1));
+    underTest.execute(new TestComputationStepContext());
+
+    verify(qProfileStatusRepository).register(eq(qp1.getQpKey()), eq(UNCHANGED));
+    verifyNoMoreInteractions(qProfileStatusRepository);
+  }
+
+  private void mockBaseQPMeasures(Component component, @Nullable QualityProfile[] previous) {
+    when(measureRepository.getBaseMeasure(component, qualityProfileMetric)).thenReturn(Optional.of(newMeasure(previous)));
+  }
+
+  private void mockRawQProfiles(@Nullable List<QualityProfile> previous) {
+    Map<String, QualityProfile> qpByLanguages = previous.stream().collect(Collectors.toMap(QualityProfile::getLanguageKey, q -> q));
+    when(analysisMetadataHolder.getQProfilesByLanguage()).thenReturn(qpByLanguages);
+  }
+
+  private static QualityProfile qp(String qpName, String languageKey, Date date) {
+    return new QualityProfile(qpName + "-" + languageKey, qpName, languageKey, date);
+  }
+
+  private static QualityProfile[] arrayOf(QualityProfile... qps) {
+    return qps;
+  }
+
+  private static Measure newMeasure(@Nullable QualityProfile... qps) {
+    return Measure.newMeasureBuilder().create(toJson(qps));
+  }
+
+  private static String toJson(@Nullable QualityProfile... qps) {
+    List<QualityProfile> qualityProfiles = qps == null ? Collections.emptyList() : Arrays.asList(qps);
+    return QPMeasureData.toJson(new QPMeasureData(qualityProfiles));
+  }
+
+}