3 * Copyright (C) 2009-2024 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.ce.task.projectanalysis.qualitymodel;
22 import java.util.stream.Stream;
23 import org.junit.jupiter.api.BeforeEach;
24 import org.junit.jupiter.api.Test;
25 import org.junit.jupiter.api.extension.RegisterExtension;
26 import org.junit.jupiter.params.ParameterizedTest;
27 import org.junit.jupiter.params.provider.Arguments;
28 import org.junit.jupiter.params.provider.MethodSource;
29 import org.sonar.ce.task.projectanalysis.component.Component;
30 import org.sonar.ce.task.projectanalysis.component.FileAttributes;
31 import org.sonar.ce.task.projectanalysis.component.ReportComponent;
32 import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
33 import org.sonar.ce.task.projectanalysis.component.VisitorsCrawler;
34 import org.sonar.ce.task.projectanalysis.measure.Measure;
35 import org.sonar.ce.task.projectanalysis.measure.MeasureRepositoryRule;
36 import org.sonar.ce.task.projectanalysis.metric.MetricRepositoryRule;
37 import org.sonar.server.measure.DebtRatingGrid;
38 import org.sonar.server.measure.Rating;
40 import static java.util.Collections.singletonList;
41 import static org.assertj.core.api.Assertions.assertThat;
42 import static org.junit.jupiter.params.provider.Arguments.arguments;
43 import static org.mockito.Mockito.mock;
44 import static org.mockito.Mockito.when;
45 import static org.sonar.api.measures.CoreMetrics.DEVELOPMENT_COST;
46 import static org.sonar.api.measures.CoreMetrics.DEVELOPMENT_COST_KEY;
47 import static org.sonar.api.measures.CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A;
48 import static org.sonar.api.measures.CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A_KEY;
49 import static org.sonar.api.measures.CoreMetrics.NCLOC;
50 import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY;
51 import static org.sonar.api.measures.CoreMetrics.SQALE_DEBT_RATIO;
52 import static org.sonar.api.measures.CoreMetrics.SQALE_DEBT_RATIO_KEY;
53 import static org.sonar.api.measures.CoreMetrics.SQALE_RATING;
54 import static org.sonar.api.measures.CoreMetrics.SQALE_RATING_KEY;
55 import static org.sonar.api.measures.CoreMetrics.TECHNICAL_DEBT;
56 import static org.sonar.api.measures.CoreMetrics.TECHNICAL_DEBT_KEY;
57 import static org.sonar.ce.task.projectanalysis.component.Component.Type.DIRECTORY;
58 import static org.sonar.ce.task.projectanalysis.component.Component.Type.FILE;
59 import static org.sonar.ce.task.projectanalysis.component.Component.Type.PROJECT;
60 import static org.sonar.ce.task.projectanalysis.component.ReportComponent.builder;
61 import static org.sonar.ce.task.projectanalysis.measure.Measure.newMeasureBuilder;
62 import static org.sonar.ce.task.projectanalysis.measure.MeasureRepoEntry.entryOf;
63 import static org.sonar.ce.task.projectanalysis.measure.MeasureRepoEntry.toEntries;
64 import static org.sonar.server.measure.Rating.A;
65 import static org.sonar.server.measure.Rating.C;
66 import static org.sonar.server.measure.Rating.D;
67 import static org.sonar.server.measure.Rating.E;
68 import static org.sonar.server.metric.SoftwareQualitiesMetrics.EFFORT_TO_REACH_SOFTWARE_QUALITY_MAINTAINABILITY_RATING_A;
69 import static org.sonar.server.metric.SoftwareQualitiesMetrics.EFFORT_TO_REACH_SOFTWARE_QUALITY_MAINTAINABILITY_RATING_A_KEY;
70 import static org.sonar.server.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_DEBT_RATIO;
71 import static org.sonar.server.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_DEBT_RATIO_KEY;
72 import static org.sonar.server.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_RATING;
73 import static org.sonar.server.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_RATING_KEY;
74 import static org.sonar.server.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT;
75 import static org.sonar.server.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT_KEY;
77 class MaintainabilityMeasuresVisitorTest {
79 static final String LANGUAGE_KEY_1 = "lKey1";
80 static final String LANGUAGE_KEY_2 = "lKey2";
82 static final double[] RATING_GRID = new double[] {0.1, 0.2, 0.5, 1};
84 static final long DEV_COST = 30;
86 static final int PROJECT_REF = 1;
87 static final int DIRECTORY_REF = 123;
88 static final int FILE_1_REF = 1231;
89 static final int FILE_2_REF = 1232;
91 static final Component ROOT_PROJECT = builder(Component.Type.PROJECT, PROJECT_REF).setKey("project")
93 builder(DIRECTORY, DIRECTORY_REF).setKey("directory")
95 builder(FILE, FILE_1_REF).setFileAttributes(new FileAttributes(false, LANGUAGE_KEY_1, 1)).setKey("file1").build(),
96 builder(FILE, FILE_2_REF).setFileAttributes(new FileAttributes(false, LANGUAGE_KEY_1, 1)).setKey("file2").build())
101 public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
104 public MetricRepositoryRule metricRepository = new MetricRepositoryRule()
106 .add(DEVELOPMENT_COST)
108 .add(SQALE_DEBT_RATIO)
110 .add(EFFORT_TO_REACH_MAINTAINABILITY_RATING_A)
111 .add(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT)
112 .add(SOFTWARE_QUALITY_MAINTAINABILITY_DEBT_RATIO)
113 .add(SOFTWARE_QUALITY_MAINTAINABILITY_RATING)
114 .add(EFFORT_TO_REACH_SOFTWARE_QUALITY_MAINTAINABILITY_RATING_A);
117 public MeasureRepositoryRule measureRepository = MeasureRepositoryRule.create(treeRootHolder, metricRepository);
119 private final RatingSettings ratingSettings = mock(RatingSettings.class);
121 private VisitorsCrawler underTest;
124 public void setUp() {
125 // assumes rating configuration is consistent
126 when(ratingSettings.getDebtRatingGrid()).thenReturn(new DebtRatingGrid(RATING_GRID));
127 when(ratingSettings.getDevCost()).thenReturn(DEV_COST);
129 underTest = new VisitorsCrawler(singletonList(new MaintainabilityMeasuresVisitor(metricRepository, measureRepository, ratingSettings)));
133 void measures_created_for_project_are_all_zero_when_they_have_no_FILE_child() {
134 ReportComponent root = builder(PROJECT, 1).build();
135 treeRootHolder.setRoot(root);
137 underTest.visit(root);
139 assertThat(measureRepository.getRawMeasures(root).entrySet().stream().map(e -> entryOf(e.getKey(), e.getValue())))
141 entryOf(DEVELOPMENT_COST_KEY, newMeasureBuilder().create("0")),
142 entryOf(SQALE_DEBT_RATIO_KEY, newMeasureBuilder().create(0d, 1)),
143 entryOf(SQALE_RATING_KEY, createMaintainabilityRatingMeasure(A)),
144 entryOf(EFFORT_TO_REACH_MAINTAINABILITY_RATING_A_KEY, newMeasureBuilder().create(0L)),
145 entryOf(SOFTWARE_QUALITY_MAINTAINABILITY_DEBT_RATIO_KEY, newMeasureBuilder().create(0d, 1)),
146 entryOf(SOFTWARE_QUALITY_MAINTAINABILITY_RATING_KEY, createMaintainabilityRatingMeasure(A)),
147 entryOf(EFFORT_TO_REACH_SOFTWARE_QUALITY_MAINTAINABILITY_RATING_A_KEY, newMeasureBuilder().create(0L)));
151 void compute_development_cost() {
152 ReportComponent root = builder(PROJECT, 1).addChildren(
153 builder(DIRECTORY, 111).addChildren(
154 createFileComponent(LANGUAGE_KEY_1, 1111),
155 createFileComponent(LANGUAGE_KEY_2, 1112),
156 // Unit test should not be ignored
157 builder(FILE, 1113).setFileAttributes(new FileAttributes(true, LANGUAGE_KEY_1, 1)).build())
159 builder(DIRECTORY, 112).addChildren(
160 createFileComponent(LANGUAGE_KEY_2, 1121))
162 builder(DIRECTORY, 121).addChildren(
163 createFileComponent(LANGUAGE_KEY_1, 1211))
165 builder(DIRECTORY, 122).build())
168 treeRootHolder.setRoot(root);
171 addRawMeasure(NCLOC_KEY, 1112, ncloc1112);
174 addRawMeasure(NCLOC_KEY, 1113, ncloc1113);
176 int nclocValue1121 = 30;
177 addRawMeasure(NCLOC_KEY, 1121, nclocValue1121);
180 addRawMeasure(NCLOC_KEY, 1211, ncloc1211);
182 underTest.visit(root);
184 // verify measures on files
185 verifyAddedRawMeasure(1112, DEVELOPMENT_COST_KEY, Long.toString(ncloc1112 * DEV_COST));
186 verifyAddedRawMeasure(1113, DEVELOPMENT_COST_KEY, Long.toString(ncloc1113 * DEV_COST));
187 verifyAddedRawMeasure(1121, DEVELOPMENT_COST_KEY, Long.toString(nclocValue1121 * DEV_COST));
188 verifyAddedRawMeasure(1211, DEVELOPMENT_COST_KEY, Long.toString(ncloc1211 * DEV_COST));
190 // directory has no children => no file => 0 everywhere and A rating
191 verifyAddedRawMeasure(122, DEVELOPMENT_COST_KEY, "0");
193 // directory has children => dev cost is aggregated
194 verifyAddedRawMeasure(111, DEVELOPMENT_COST_KEY, Long.toString(
195 ncloc1112 * DEV_COST +
196 ncloc1113 * DEV_COST));
197 verifyAddedRawMeasure(112, DEVELOPMENT_COST_KEY, Long.toString(nclocValue1121 * DEV_COST));
198 verifyAddedRawMeasure(121, DEVELOPMENT_COST_KEY, Long.toString(ncloc1211 * DEV_COST));
200 verifyAddedRawMeasure(1, DEVELOPMENT_COST_KEY, Long.toString(
201 ncloc1112 * DEV_COST +
202 ncloc1113 * DEV_COST +
203 nclocValue1121 * DEV_COST +
204 ncloc1211 * DEV_COST));
207 private static Stream<Arguments> metrics() {
209 arguments(TECHNICAL_DEBT_KEY, SQALE_DEBT_RATIO_KEY, SQALE_RATING_KEY, EFFORT_TO_REACH_MAINTAINABILITY_RATING_A_KEY),
210 arguments(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT_KEY, SOFTWARE_QUALITY_MAINTAINABILITY_DEBT_RATIO_KEY, SOFTWARE_QUALITY_MAINTAINABILITY_RATING_KEY,
211 EFFORT_TO_REACH_SOFTWARE_QUALITY_MAINTAINABILITY_RATING_A_KEY));
215 @MethodSource("metrics")
216 void compute_maintainability_debt_ratio_measure(String remediationEffortKey, String debtRatioKey) {
217 treeRootHolder.setRoot(ROOT_PROJECT);
220 addRawMeasure(NCLOC_KEY, FILE_1_REF, file1Ncloc);
221 long file1MaintainabilityCost = 100L;
222 addRawMeasure(remediationEffortKey, FILE_1_REF, file1MaintainabilityCost);
225 addRawMeasure(NCLOC_KEY, FILE_2_REF, file2Ncloc);
226 long file2MaintainabilityCost = 1L;
227 addRawMeasure(remediationEffortKey, FILE_2_REF, file2MaintainabilityCost);
229 long directoryMaintainabilityCost = 100L;
230 addRawMeasure(remediationEffortKey, DIRECTORY_REF, directoryMaintainabilityCost);
232 long projectMaintainabilityCost = 1000L;
233 addRawMeasure(remediationEffortKey, PROJECT_REF, projectMaintainabilityCost);
235 underTest.visit(ROOT_PROJECT);
237 verifyAddedRawMeasure(FILE_1_REF, debtRatioKey, file1MaintainabilityCost * 1d / (file1Ncloc * DEV_COST) * 100);
238 verifyAddedRawMeasure(FILE_2_REF, debtRatioKey, file2MaintainabilityCost * 1d / (file2Ncloc * DEV_COST) * 100);
239 verifyAddedRawMeasure(DIRECTORY_REF, debtRatioKey, directoryMaintainabilityCost * 1d / ((file1Ncloc + file2Ncloc) * DEV_COST) * 100);
240 verifyAddedRawMeasure(PROJECT_REF, debtRatioKey, projectMaintainabilityCost * 1d / ((file1Ncloc + file2Ncloc) * DEV_COST) * 100);
244 void compute_maintainability_rating_measure() {
245 treeRootHolder.setRoot(ROOT_PROJECT);
247 addRawMeasure(NCLOC_KEY, FILE_1_REF, 10);
248 addRawMeasure(TECHNICAL_DEBT_KEY, FILE_1_REF, 100L);
250 addRawMeasure(NCLOC_KEY, FILE_2_REF, 5);
251 addRawMeasure(TECHNICAL_DEBT_KEY, FILE_2_REF, 1L);
253 addRawMeasure(TECHNICAL_DEBT_KEY, DIRECTORY_REF, 100L);
254 addRawMeasure(TECHNICAL_DEBT_KEY, PROJECT_REF, 1000L);
256 underTest.visit(ROOT_PROJECT);
258 verifyAddedRawMeasure(FILE_1_REF, SQALE_RATING_KEY, C);
259 verifyAddedRawMeasure(FILE_2_REF, SQALE_RATING_KEY, A);
260 verifyAddedRawMeasure(DIRECTORY_REF, SQALE_RATING_KEY, C);
261 verifyAddedRawMeasure(PROJECT_REF, SQALE_RATING_KEY, E);
265 void compute_software_quality_maintainability_rating_measure() {
266 treeRootHolder.setRoot(ROOT_PROJECT);
268 addRawMeasure(NCLOC_KEY, FILE_1_REF, 10);
269 addRawMeasure(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT_KEY, FILE_1_REF, 100L);
271 addRawMeasure(NCLOC_KEY, FILE_2_REF, 5);
272 addRawMeasure(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT_KEY, FILE_2_REF, 1L);
274 addRawMeasure(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT_KEY, DIRECTORY_REF, 100L);
275 addRawMeasure(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT_KEY, PROJECT_REF, 1000L);
277 underTest.visit(ROOT_PROJECT);
279 verifyAddedRawMeasure(FILE_1_REF, SOFTWARE_QUALITY_MAINTAINABILITY_RATING_KEY, C);
280 verifyAddedRawMeasure(FILE_2_REF, SOFTWARE_QUALITY_MAINTAINABILITY_RATING_KEY, A);
281 verifyAddedRawMeasure(DIRECTORY_REF, SOFTWARE_QUALITY_MAINTAINABILITY_RATING_KEY, C);
282 verifyAddedRawMeasure(PROJECT_REF, SOFTWARE_QUALITY_MAINTAINABILITY_RATING_KEY, D);
286 @MethodSource("metrics")
287 void compute_effort_to_maintainability_rating_A_measure(String remediationEffortKey, String debtRatioKey,
288 String ratingKey, String effortReachRatingAKey) {
289 treeRootHolder.setRoot(ROOT_PROJECT);
292 long file1Effort = 100L;
293 addRawMeasure(NCLOC_KEY, FILE_1_REF, file1Ncloc);
294 addRawMeasure(remediationEffortKey, FILE_1_REF, file1Effort);
297 long file2Effort = 20L;
298 addRawMeasure(NCLOC_KEY, FILE_2_REF, file2Ncloc);
299 addRawMeasure(remediationEffortKey, FILE_2_REF, file2Effort);
301 long dirEffort = 120L;
302 addRawMeasure(remediationEffortKey, DIRECTORY_REF, dirEffort);
304 long projectEffort = 150L;
305 addRawMeasure(remediationEffortKey, PROJECT_REF, projectEffort);
307 underTest.visit(ROOT_PROJECT);
309 verifyAddedRawMeasure(FILE_1_REF, effortReachRatingAKey,
310 (long) (file1Effort - RATING_GRID[0] * file1Ncloc * DEV_COST));
311 verifyAddedRawMeasure(FILE_2_REF, effortReachRatingAKey,
312 (long) (file2Effort - RATING_GRID[0] * file2Ncloc * DEV_COST));
313 verifyAddedRawMeasure(DIRECTORY_REF, effortReachRatingAKey,
314 (long) (dirEffort - RATING_GRID[0] * (file1Ncloc + file2Ncloc) * DEV_COST));
315 verifyAddedRawMeasure(PROJECT_REF, effortReachRatingAKey,
316 (long) (projectEffort - RATING_GRID[0] * (file1Ncloc + file2Ncloc) * DEV_COST));
320 @MethodSource("metrics")
321 void compute_0_effort_to_maintainability_rating_A_when_effort_is_lower_than_dev_cost(String remediationEffortKey, String debtRatioKey,
322 String ratingKey, String effortReachRatingAKey) {
323 treeRootHolder.setRoot(ROOT_PROJECT);
325 addRawMeasure(NCLOC_KEY, FILE_1_REF, 10);
326 addRawMeasure(remediationEffortKey, FILE_1_REF, 2L);
328 underTest.visit(ROOT_PROJECT);
330 verifyAddedRawMeasure(FILE_1_REF, effortReachRatingAKey, 0L);
334 @MethodSource("metrics")
335 void effort_to_maintainability_rating_A_is_same_as_effort_when_no_dev_cost(String remediationEffortKey, String debtRatioKey,
336 String ratingKey, String effortReachRatingAKey) {
337 treeRootHolder.setRoot(ROOT_PROJECT);
339 addRawMeasure(remediationEffortKey, FILE_1_REF, 100L);
341 underTest.visit(ROOT_PROJECT);
343 verifyAddedRawMeasure(FILE_1_REF, effortReachRatingAKey, 100);
346 private void addRawMeasure(String metricKey, int componentRef, long value) {
347 measureRepository.addRawMeasure(componentRef, metricKey, newMeasureBuilder().create(value));
350 private void addRawMeasure(String metricKey, int componentRef, int value) {
351 measureRepository.addRawMeasure(componentRef, metricKey, newMeasureBuilder().create(value));
354 private void verifyAddedRawMeasure(int componentRef, String metricKey, long value) {
355 assertThat(toEntries(measureRepository.getAddedRawMeasures(componentRef))).contains(entryOf(metricKey, newMeasureBuilder().create(value)));
358 private void verifyAddedRawMeasure(int componentRef, String metricKey, double value) {
359 assertThat(toEntries(measureRepository.getAddedRawMeasures(componentRef))).contains(entryOf(metricKey, newMeasureBuilder().create(value, 1)));
362 private void verifyAddedRawMeasure(int componentRef, String metricKey, Rating rating) {
363 assertThat(toEntries(measureRepository.getAddedRawMeasures(componentRef))).contains(entryOf(metricKey, newMeasureBuilder().create(rating.getIndex(), rating.name())));
366 private void verifyAddedRawMeasure(int componentRef, String metricKey, String value) {
367 assertThat(toEntries(measureRepository.getAddedRawMeasures(componentRef))).contains(entryOf(metricKey, newMeasureBuilder().create(value)));
370 private static ReportComponent createFileComponent(String languageKey1, int fileRef) {
371 return builder(FILE, fileRef).setFileAttributes(new FileAttributes(false, languageKey1, 1)).build();
374 private static Measure createMaintainabilityRatingMeasure(Rating rating) {
375 return newMeasureBuilder().create(rating.getIndex(), rating.name());