3 * Copyright (C) 2009-2023 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.telemetry;
22 import com.tngtech.java.junit.dataprovider.DataProvider;
23 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
24 import com.tngtech.java.junit.dataprovider.UseDataProvider;
25 import java.io.StringWriter;
26 import java.util.Arrays;
27 import java.util.Collections;
28 import java.util.List;
29 import java.util.Locale;
31 import java.util.Random;
33 import java.util.stream.Collectors;
34 import java.util.stream.IntStream;
35 import org.apache.commons.codec.digest.DigestUtils;
36 import org.jetbrains.annotations.NotNull;
37 import org.junit.Test;
38 import org.junit.runner.RunWith;
39 import org.sonar.api.utils.System2;
40 import org.sonar.api.utils.text.JsonWriter;
41 import org.sonar.core.platform.EditionProvider;
42 import org.sonar.core.telemetry.TelemetryExtension;
43 import org.sonar.core.util.stream.MoreCollectors;
44 import org.sonar.db.user.UserTelemetryDto;
46 import static java.lang.String.format;
47 import static java.util.stream.Collectors.joining;
48 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
49 import static org.assertj.core.api.Assertions.assertThat;
50 import static org.mockito.Mockito.mock;
51 import static org.mockito.Mockito.when;
52 import static org.sonar.server.telemetry.TelemetryDataJsonWriter.SCIM_PROPERTY;
53 import static org.sonar.test.JsonAssert.assertJson;
55 @RunWith(DataProviderRunner.class)
56 public class TelemetryDataJsonWriterTest {
58 private final Random random = new Random();
60 private final TelemetryExtension extension = mock(TelemetryExtension.class);
62 private final System2 system2 = mock(System2.class);
64 private final TelemetryDataJsonWriter underTest = new TelemetryDataJsonWriter(List.of(extension), system2);
67 public void write_server_id_version_and_sequence() {
68 TelemetryData data = telemetryBuilder().build();
70 String json = writeTelemetryData(data);
71 assertJson(json).isSimilarTo("""
75 "messageSequenceNumber": %s
77 """.formatted(data.getServerId(), data.getVersion(), data.getMessageSequenceNumber()));
81 public void does_not_write_edition_if_null() {
82 TelemetryData data = telemetryBuilder().build();
84 String json = writeTelemetryData(data);
86 assertThat(json).doesNotContain("edition");
90 @UseDataProvider("allEditions")
91 public void writes_edition_if_non_null(EditionProvider.Edition edition) {
92 TelemetryData data = telemetryBuilder()
96 String json = writeTelemetryData(data);
97 assertJson(json).isSimilarTo("""
101 """.formatted(edition.name().toLowerCase(Locale.ENGLISH)));
105 public void writes_default_qg() {
106 TelemetryData data = telemetryBuilder()
107 .setDefaultQualityGate("default-qg")
110 String json = writeTelemetryData(data);
111 assertJson(json).isSimilarTo("""
113 "defaultQualityGate": "%s"
115 """.formatted(data.getDefaultQualityGate()));
119 public void writes_database() {
120 String name = randomAlphabetic(12);
121 String version = randomAlphabetic(10);
122 TelemetryData data = telemetryBuilder()
123 .setDatabase(new TelemetryData.Database(name, version))
126 String json = writeTelemetryData(data);
127 assertJson(json).isSimilarTo("""
134 """.formatted(name, version));
138 public void writes_no_plugins() {
139 TelemetryData data = telemetryBuilder()
140 .setPlugins(Collections.emptyMap())
143 String json = writeTelemetryData(data);
145 assertJson(json).isSimilarTo("""
153 public void writes_all_plugins() {
154 Map<String, String> plugins = IntStream.range(0, 1 + random.nextInt(10))
156 .collect(MoreCollectors.uniqueIndex(i -> "P" + i, i -> "V" + i));
157 TelemetryData data = telemetryBuilder()
161 String json = writeTelemetryData(data);
162 assertJson(json).isSimilarTo("""
166 """.formatted(plugins.entrySet().stream().map(e -> "{\"name\":\"" + e.getKey() + "\",\"version\":\"" + e.getValue() + "\"}").collect(joining(","))));
170 public void does_not_write_installation_date_if_null() {
171 TelemetryData data = telemetryBuilder()
172 .setInstallationDate(null)
175 String json = writeTelemetryData(data);
177 assertThat(json).doesNotContain("installationDate");
181 public void write_installation_date_in_utc_format() {
182 TelemetryData data = telemetryBuilder()
183 .setInstallationDate(1_000L)
186 String json = writeTelemetryData(data);
188 assertJson(json).isSimilarTo("""
190 "installationDate":"1970-01-01T00:00:01+0000"
196 public void does_not_write_installation_version_if_null() {
197 TelemetryData data = telemetryBuilder()
198 .setInstallationVersion(null)
201 String json = writeTelemetryData(data);
203 assertThat(json).doesNotContain("installationVersion");
207 public void write_installation_version() {
208 String installationVersion = randomAlphabetic(5);
209 TelemetryData data = telemetryBuilder()
210 .setInstallationVersion(installationVersion)
213 String json = writeTelemetryData(data);
214 assertJson(json).isSimilarTo("""
216 "installationVersion": "%s"
218 """.formatted(installationVersion));
222 @UseDataProvider("getFeatureFlagEnabledStates")
223 public void write_docker_flag(boolean isInDocker) {
224 TelemetryData data = telemetryBuilder()
225 .setInDocker(isInDocker)
228 String json = writeTelemetryData(data);
229 assertJson(json).isSimilarTo("""
233 """.formatted(isInDocker));
237 @UseDataProvider("getFeatureFlagEnabledStates")
238 public void write_scim_feature_flag(boolean isScimEnabled) {
239 TelemetryData data = telemetryBuilder()
240 .setIsScimEnabled(isScimEnabled)
243 String json = writeTelemetryData(data);
245 assertJson(json).isSimilarTo("{" + format(" \"%s\":", SCIM_PROPERTY) + isScimEnabled + "}");
249 public void writes_has_unanalyzed_languages() {
250 TelemetryData data = telemetryBuilder()
251 .setHasUnanalyzedC(true)
252 .setHasUnanalyzedCpp(false)
255 String json = writeTelemetryData(data);
257 assertJson(json).isSimilarTo("""
259 "hasUnanalyzedC": true,
260 "hasUnanalyzedCpp": false,
266 public void writes_security_custom_config() {
267 TelemetryData data = telemetryBuilder()
268 .setCustomSecurityConfigs(Set.of("php", "java"))
271 String json = writeTelemetryData(data);
273 assertJson(json).isSimilarTo("""
275 "customSecurityConfig": ["php", "java"]
281 public void writes_local_timestamp() {
282 when(system2.now()).thenReturn(1000L);
284 TelemetryData data = telemetryBuilder().build();
285 String json = writeTelemetryData(data);
287 assertJson(json).isSimilarTo("""
289 "localTimestamp": "1970-01-01T00:00:01+0000"
295 public void writes_all_users_with_anonymous_md5_uuids() {
296 TelemetryData data = telemetryBuilder()
297 .setUsers(attachUsers())
300 String json = writeTelemetryData(data);
302 assertJson(json).isSimilarTo("""
308 "identityProvider": "gitlab",
309 "lastActivity": "1970-01-01T00:00:00+0000",
310 "lastSonarlintActivity": "1970-01-01T00:00:00+0000"
314 "status": "inactive",
315 "identityProvider": "gitlab",
316 "lastActivity": "1970-01-01T00:00:00+0000",
317 "lastSonarlintActivity": "1970-01-01T00:00:00+0000"
322 "identityProvider": "gitlab",
323 "lastActivity": "1970-01-01T00:00:00+0000",
324 "lastSonarlintActivity": "1970-01-01T00:00:00+0000"
329 .formatted(DigestUtils.sha3_224Hex("uuid-0"), DigestUtils.sha3_224Hex("uuid-1"), DigestUtils.sha3_224Hex("uuid-2")));
333 public void writes_all_projects() {
334 TelemetryData data = telemetryBuilder()
335 .setProjects(attachProjects())
338 String json = writeTelemetryData(data);
340 assertJson(json).isSimilarTo("""
344 "projectUuid": "uuid-0",
345 "lastAnalysis": "1970-01-01T00:00:00+0000",
346 "language": "lang-0",
350 "projectUuid": "uuid-1",
351 "lastAnalysis": "1970-01-01T00:00:00+0000",
352 "language": "lang-1",
356 "projectUuid": "uuid-2",
357 "lastAnalysis": "1970-01-01T00:00:00+0000",
358 "language": "lang-2",
367 public void writeTelemetryData_whenAnalyzedLanguages_shouldwriteAllProjectsStats() {
368 TelemetryData data = telemetryBuilder()
369 .setProjectStatistics(attachProjectStatsWithMetrics())
372 String json = writeTelemetryData(data);
374 assertJson(json).isSimilarTo("""
376 "projects-general-stats": [
378 "projectUuid": "uuid-0",
380 "pullRequestCount": 2,
381 "qualityGate": "qg-0",
384 "devopsPlatform": "devops-0",
386 "vulnerabilities": 3,
387 "securityHotspots": 4,
389 "developmentCost": 30
392 "projectUuid": "uuid-1",
394 "pullRequestCount": 4,
395 "qualityGate": "qg-1",
398 "devopsPlatform": "devops-1",
400 "vulnerabilities": 6,
401 "securityHotspots": 8,
402 "technicalDebt": 120,
403 "developmentCost": 60
406 "projectUuid": "uuid-2",
408 "pullRequestCount": 6,
409 "qualityGate": "qg-2",
412 "devopsPlatform": "devops-2",
414 "vulnerabilities": 9,
415 "securityHotspots": 12,
416 "technicalDebt": 180,
417 "developmentCost": 90
426 public void writes_all_projects_stats_with_unanalyzed_languages() {
427 TelemetryData data = telemetryBuilder()
428 .setProjectStatistics(attachProjectStats())
431 String json = writeTelemetryData(data);
432 assertThat(json).doesNotContain("hasUnanalyzedC", "hasUnanalyzedCpp");
436 public void writes_all_projects_stats_without_missing_metrics() {
437 TelemetryData data = telemetryBuilder()
438 .setProjectStatistics(attachProjectStats())
440 String json = writeTelemetryData(data);
441 assertThat(json).doesNotContain("bugs", "vulnerabilities", "securityHotspots", "technicalDebt", "developmentCost");
445 public void writes_all_quality_gates() {
446 TelemetryData data = telemetryBuilder()
447 .setQualityGates(attachQualityGates())
450 String json = writeTelemetryData(data);
451 assertJson(json).isSimilarTo("""
456 "caycStatus": "non-compliant"
460 "caycStatus": "compliant"
464 "caycStatus": "over-compliant"
472 private static TelemetryData.Builder telemetryBuilder() {
473 return TelemetryData.builder()
476 .setMessageSequenceNumber(1L)
477 .setPlugins(Collections.emptyMap())
478 .setDatabase(new TelemetryData.Database("H2", "11"));
482 private static List<UserTelemetryDto> attachUsers() {
483 return IntStream.range(0, 3)
485 i -> new UserTelemetryDto().setUuid("uuid-" + i).setActive(i % 2 == 0).setLastConnectionDate(1L).setLastSonarlintConnectionDate(2L).setExternalIdentityProvider("gitlab"))
486 .collect(Collectors.toList());
489 private static List<TelemetryData.Project> attachProjects() {
490 return IntStream.range(0, 3).mapToObj(i -> new TelemetryData.Project("uuid-" + i, 1L, "lang-" + i, (i + 1L) * 2L)).collect(Collectors.toList());
493 private static List<TelemetryData.ProjectStatistics> attachProjectStatsWithMetrics() {
494 return IntStream.range(0, 3).mapToObj(i -> getProjectStatisticsWithMetricBuilder(i).build()).toList();
497 private static List<TelemetryData.ProjectStatistics> attachProjectStats() {
498 return IntStream.range(0, 3).mapToObj(i -> getProjectStatisticsBuilder(i).build()).toList();
501 private static TelemetryData.ProjectStatistics.Builder getProjectStatisticsBuilder(int i) {
502 return new TelemetryData.ProjectStatistics.Builder()
503 .setProjectUuid("uuid-" + i)
504 .setBranchCount((i + 1L) * 2L)
505 .setPRCount((i + 1L) * 2L)
506 .setQG("qg-" + i).setCi("ci-" + i)
508 .setDevops("devops-" + i);
511 private static TelemetryData.ProjectStatistics.Builder getProjectStatisticsWithMetricBuilder(int i) {
512 return getProjectStatisticsBuilder(i)
513 .setBugs((i + 1L) * 2)
514 .setVulnerabilities((i + 1L) * 3)
515 .setSecurityHotspots((i + 1L) * 4)
516 .setDevelopmentCost((i + 1L) * 30d)
517 .setTechnicalDebt((i + 1L) * 60d);
520 private List<TelemetryData.QualityGate> attachQualityGates() {
521 return List.of(new TelemetryData.QualityGate("uuid-0", "non-compliant"),
522 new TelemetryData.QualityGate("uuid-1", "compliant"),
523 new TelemetryData.QualityGate("uuid-2", "over-compliant"));
527 public static Object[][] allEditions() {
528 return Arrays.stream(EditionProvider.Edition.values())
529 .map(t -> new Object[]{t})
530 .toArray(Object[][]::new);
533 private String writeTelemetryData(TelemetryData data) {
534 StringWriter jsonString = new StringWriter();
535 try (JsonWriter json = JsonWriter.of(jsonString)) {
536 underTest.writeTelemetryData(json, data);
538 return jsonString.toString();
542 public static Set<Boolean> getFeatureFlagEnabledStates() {
543 return Set.of(true, false);