You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

TelemetryDataJsonWriterTest.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  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.
  10. *
  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.
  15. *
  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.
  19. */
  20. package org.sonar.server.telemetry;
  21. import com.tngtech.java.junit.dataprovider.DataProvider;
  22. import com.tngtech.java.junit.dataprovider.DataProviderRunner;
  23. import com.tngtech.java.junit.dataprovider.UseDataProvider;
  24. import java.io.StringWriter;
  25. import java.util.Arrays;
  26. import java.util.Collections;
  27. import java.util.List;
  28. import java.util.Locale;
  29. import java.util.Map;
  30. import java.util.Random;
  31. import java.util.Set;
  32. import java.util.stream.Collectors;
  33. import java.util.stream.IntStream;
  34. import org.apache.commons.codec.digest.DigestUtils;
  35. import org.jetbrains.annotations.NotNull;
  36. import org.junit.Test;
  37. import org.junit.runner.RunWith;
  38. import org.sonar.api.utils.System2;
  39. import org.sonar.api.utils.text.JsonWriter;
  40. import org.sonar.core.platform.EditionProvider;
  41. import org.sonar.core.telemetry.TelemetryExtension;
  42. import org.sonar.core.util.stream.MoreCollectors;
  43. import org.sonar.db.user.UserTelemetryDto;
  44. import static java.lang.String.format;
  45. import static java.util.stream.Collectors.joining;
  46. import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
  47. import static org.assertj.core.api.Assertions.assertThat;
  48. import static org.mockito.Mockito.mock;
  49. import static org.mockito.Mockito.when;
  50. import static org.sonar.server.telemetry.TelemetryDataJsonWriter.SCIM_PROPERTY;
  51. import static org.sonar.test.JsonAssert.assertJson;
  52. @RunWith(DataProviderRunner.class)
  53. public class TelemetryDataJsonWriterTest {
  54. private final Random random = new Random();
  55. private final TelemetryExtension extension = mock(TelemetryExtension.class);
  56. private final System2 system2 = mock(System2.class);
  57. private final TelemetryDataJsonWriter underTest = new TelemetryDataJsonWriter(List.of(extension), system2);
  58. @Test
  59. public void write_server_id_version_and_sequence() {
  60. TelemetryData data = telemetryBuilder().build();
  61. String json = writeTelemetryData(data);
  62. assertJson(json).isSimilarTo("""
  63. {
  64. "id": "%s",
  65. "version": "%s",
  66. "messageSequenceNumber": %s
  67. }
  68. """.formatted(data.getServerId(), data.getVersion(), data.getMessageSequenceNumber()));
  69. }
  70. @Test
  71. public void does_not_write_edition_if_null() {
  72. TelemetryData data = telemetryBuilder().build();
  73. String json = writeTelemetryData(data);
  74. assertThat(json).doesNotContain("edition");
  75. }
  76. @Test
  77. @UseDataProvider("allEditions")
  78. public void writes_edition_if_non_null(EditionProvider.Edition edition) {
  79. TelemetryData data = telemetryBuilder()
  80. .setEdition(edition)
  81. .build();
  82. String json = writeTelemetryData(data);
  83. assertJson(json).isSimilarTo("""
  84. {
  85. "edition": "%s"
  86. }
  87. """.formatted(edition.name().toLowerCase(Locale.ENGLISH)));
  88. }
  89. @Test
  90. public void writes_default_qg() {
  91. TelemetryData data = telemetryBuilder()
  92. .setDefaultQualityGate("default-qg")
  93. .build();
  94. String json = writeTelemetryData(data);
  95. assertJson(json).isSimilarTo("""
  96. {
  97. "defaultQualityGate": "%s"
  98. }
  99. """.formatted(data.getDefaultQualityGate()));
  100. }
  101. @Test
  102. public void writes_database() {
  103. String name = randomAlphabetic(12);
  104. String version = randomAlphabetic(10);
  105. TelemetryData data = telemetryBuilder()
  106. .setDatabase(new TelemetryData.Database(name, version))
  107. .build();
  108. String json = writeTelemetryData(data);
  109. assertJson(json).isSimilarTo("""
  110. {
  111. "database": {
  112. "name": "%s",
  113. "version": "%s"
  114. }
  115. }
  116. """.formatted(name, version));
  117. }
  118. @Test
  119. public void writes_no_plugins() {
  120. TelemetryData data = telemetryBuilder()
  121. .setPlugins(Collections.emptyMap())
  122. .build();
  123. String json = writeTelemetryData(data);
  124. assertJson(json).isSimilarTo("""
  125. {
  126. "plugins": []
  127. }
  128. """);
  129. }
  130. @Test
  131. public void writes_all_plugins() {
  132. Map<String, String> plugins = IntStream.range(0, 1 + random.nextInt(10))
  133. .boxed()
  134. .collect(MoreCollectors.uniqueIndex(i -> "P" + i, i -> "V" + i));
  135. TelemetryData data = telemetryBuilder()
  136. .setPlugins(plugins)
  137. .build();
  138. String json = writeTelemetryData(data);
  139. assertJson(json).isSimilarTo("""
  140. {
  141. "plugins": [%s]
  142. }
  143. """.formatted(plugins.entrySet().stream().map(e -> "{\"name\":\"" + e.getKey() + "\",\"version\":\"" + e.getValue() + "\"}").collect(joining(","))));
  144. }
  145. @Test
  146. public void does_not_write_installation_date_if_null() {
  147. TelemetryData data = telemetryBuilder()
  148. .setInstallationDate(null)
  149. .build();
  150. String json = writeTelemetryData(data);
  151. assertThat(json).doesNotContain("installationDate");
  152. }
  153. @Test
  154. public void write_installation_date_in_utc_format() {
  155. TelemetryData data = telemetryBuilder()
  156. .setInstallationDate(1_000L)
  157. .build();
  158. String json = writeTelemetryData(data);
  159. assertJson(json).isSimilarTo("""
  160. {
  161. "installationDate":"1970-01-01T00:00:01+0000"
  162. }
  163. """);
  164. }
  165. @Test
  166. public void does_not_write_installation_version_if_null() {
  167. TelemetryData data = telemetryBuilder()
  168. .setInstallationVersion(null)
  169. .build();
  170. String json = writeTelemetryData(data);
  171. assertThat(json).doesNotContain("installationVersion");
  172. }
  173. @Test
  174. public void write_installation_version() {
  175. String installationVersion = randomAlphabetic(5);
  176. TelemetryData data = telemetryBuilder()
  177. .setInstallationVersion(installationVersion)
  178. .build();
  179. String json = writeTelemetryData(data);
  180. assertJson(json).isSimilarTo("""
  181. {
  182. "installationVersion": "%s"
  183. }
  184. """.formatted(installationVersion));
  185. }
  186. @Test
  187. @UseDataProvider("getFeatureFlagEnabledStates")
  188. public void write_docker_flag(boolean isInDocker) {
  189. TelemetryData data = telemetryBuilder()
  190. .setInDocker(isInDocker)
  191. .build();
  192. String json = writeTelemetryData(data);
  193. assertJson(json).isSimilarTo("""
  194. {
  195. "docker": %s
  196. }
  197. """.formatted(isInDocker));
  198. }
  199. @Test
  200. @UseDataProvider("getFeatureFlagEnabledStates")
  201. public void write_scim_feature_flag(boolean isScimEnabled) {
  202. TelemetryData data = telemetryBuilder()
  203. .setIsScimEnabled(isScimEnabled)
  204. .build();
  205. String json = writeTelemetryData(data);
  206. assertJson(json).isSimilarTo("{" + format(" \"%s\":", SCIM_PROPERTY) + isScimEnabled + "}");
  207. }
  208. @Test
  209. public void writes_has_unanalyzed_languages() {
  210. TelemetryData data = telemetryBuilder()
  211. .setHasUnanalyzedC(true)
  212. .setHasUnanalyzedCpp(false)
  213. .build();
  214. String json = writeTelemetryData(data);
  215. assertJson(json).isSimilarTo("""
  216. {
  217. "hasUnanalyzedC": true,
  218. "hasUnanalyzedCpp": false,
  219. }
  220. """);
  221. }
  222. @Test
  223. public void writes_security_custom_config() {
  224. TelemetryData data = telemetryBuilder()
  225. .setCustomSecurityConfigs(Set.of("php", "java"))
  226. .build();
  227. String json = writeTelemetryData(data);
  228. assertJson(json).isSimilarTo("""
  229. {
  230. "customSecurityConfig": ["php", "java"]
  231. }
  232. """);
  233. }
  234. @Test
  235. public void writes_local_timestamp() {
  236. when(system2.now()).thenReturn(1000L);
  237. TelemetryData data = telemetryBuilder().build();
  238. String json = writeTelemetryData(data);
  239. assertJson(json).isSimilarTo("""
  240. {
  241. "localTimestamp": "1970-01-01T00:00:01+0000"
  242. }
  243. """);
  244. }
  245. @Test
  246. public void writes_all_users_with_anonymous_md5_uuids() {
  247. TelemetryData data = telemetryBuilder()
  248. .setUsers(attachUsers())
  249. .build();
  250. String json = writeTelemetryData(data);
  251. assertJson(json).isSimilarTo("""
  252. {
  253. "users": [
  254. {
  255. "userUuid": "%s",
  256. "status": "active",
  257. "identityProvider": "gitlab",
  258. "lastActivity": "1970-01-01T00:00:00+0000",
  259. "lastSonarlintActivity": "1970-01-01T00:00:00+0000"
  260. },
  261. {
  262. "userUuid": "%s",
  263. "status": "inactive",
  264. "identityProvider": "gitlab",
  265. "lastActivity": "1970-01-01T00:00:00+0000",
  266. "lastSonarlintActivity": "1970-01-01T00:00:00+0000"
  267. },
  268. {
  269. "userUuid": "%s",
  270. "status": "active",
  271. "identityProvider": "gitlab",
  272. "lastActivity": "1970-01-01T00:00:00+0000",
  273. "lastSonarlintActivity": "1970-01-01T00:00:00+0000"
  274. }
  275. ]
  276. }
  277. """
  278. .formatted(DigestUtils.sha3_224Hex("uuid-0"), DigestUtils.sha3_224Hex("uuid-1"), DigestUtils.sha3_224Hex("uuid-2")));
  279. }
  280. @Test
  281. public void writes_all_projects() {
  282. TelemetryData data = telemetryBuilder()
  283. .setProjects(attachProjects())
  284. .build();
  285. String json = writeTelemetryData(data);
  286. assertJson(json).isSimilarTo("""
  287. {
  288. "projects": [
  289. {
  290. "projectUuid": "uuid-0",
  291. "lastAnalysis": "1970-01-01T00:00:00+0000",
  292. "language": "lang-0",
  293. "loc": 2
  294. },
  295. {
  296. "projectUuid": "uuid-1",
  297. "lastAnalysis": "1970-01-01T00:00:00+0000",
  298. "language": "lang-1",
  299. "loc": 4
  300. },
  301. {
  302. "projectUuid": "uuid-2",
  303. "lastAnalysis": "1970-01-01T00:00:00+0000",
  304. "language": "lang-2",
  305. "loc": 6
  306. }
  307. ]
  308. }
  309. """);
  310. }
  311. @Test
  312. public void writeTelemetryData_whenAnalyzedLanguages_shouldwriteAllProjectsStats() {
  313. TelemetryData data = telemetryBuilder()
  314. .setProjectStatistics(attachProjectStatsWithMetrics())
  315. .build();
  316. String json = writeTelemetryData(data);
  317. assertJson(json).isSimilarTo("""
  318. {
  319. "projects-general-stats": [
  320. {
  321. "projectUuid": "uuid-0",
  322. "branchCount": 2,
  323. "pullRequestCount": 2,
  324. "qualityGate": "qg-0",
  325. "scm": "scm-0",
  326. "ci": "ci-0",
  327. "devopsPlatform": "devops-0",
  328. "bugs": 2,
  329. "vulnerabilities": 3,
  330. "securityHotspots": 4,
  331. "technicalDebt": 60,
  332. "developmentCost": 30
  333. },
  334. {
  335. "projectUuid": "uuid-1",
  336. "branchCount": 4,
  337. "pullRequestCount": 4,
  338. "qualityGate": "qg-1",
  339. "scm": "scm-1",
  340. "ci": "ci-1",
  341. "devopsPlatform": "devops-1",
  342. "bugs": 4,
  343. "vulnerabilities": 6,
  344. "securityHotspots": 8,
  345. "technicalDebt": 120,
  346. "developmentCost": 60
  347. },
  348. {
  349. "projectUuid": "uuid-2",
  350. "branchCount": 6,
  351. "pullRequestCount": 6,
  352. "qualityGate": "qg-2",
  353. "scm": "scm-2",
  354. "ci": "ci-2",
  355. "devopsPlatform": "devops-2",
  356. "bugs": 6,
  357. "vulnerabilities": 9,
  358. "securityHotspots": 12,
  359. "technicalDebt": 180,
  360. "developmentCost": 90
  361. }
  362. ]
  363. }
  364. """
  365. );
  366. }
  367. @Test
  368. public void writes_all_projects_stats_with_unanalyzed_languages() {
  369. TelemetryData data = telemetryBuilder()
  370. .setProjectStatistics(attachProjectStats())
  371. .build();
  372. String json = writeTelemetryData(data);
  373. assertThat(json).doesNotContain("hasUnanalyzedC", "hasUnanalyzedCpp");
  374. }
  375. @Test
  376. public void writes_all_projects_stats_without_missing_metrics() {
  377. TelemetryData data = telemetryBuilder()
  378. .setProjectStatistics(attachProjectStats())
  379. .build();
  380. String json = writeTelemetryData(data);
  381. assertThat(json).doesNotContain("bugs", "vulnerabilities", "securityHotspots", "technicalDebt", "developmentCost");
  382. }
  383. @Test
  384. public void writes_all_quality_gates() {
  385. TelemetryData data = telemetryBuilder()
  386. .setQualityGates(attachQualityGates())
  387. .build();
  388. String json = writeTelemetryData(data);
  389. assertJson(json).isSimilarTo("""
  390. {
  391. "quality-gates": [
  392. {
  393. "uuid": "uuid-0",
  394. "caycStatus": "non-compliant"
  395. },
  396. {
  397. "uuid": "uuid-1",
  398. "caycStatus": "compliant"
  399. },
  400. {
  401. "uuid": "uuid-2",
  402. "caycStatus": "over-compliant"
  403. }
  404. ]
  405. }
  406. """
  407. );
  408. }
  409. private static TelemetryData.Builder telemetryBuilder() {
  410. return TelemetryData.builder()
  411. .setServerId("foo")
  412. .setVersion("bar")
  413. .setMessageSequenceNumber(1L)
  414. .setPlugins(Collections.emptyMap())
  415. .setDatabase(new TelemetryData.Database("H2", "11"));
  416. }
  417. @NotNull
  418. private static List<UserTelemetryDto> attachUsers() {
  419. return IntStream.range(0, 3)
  420. .mapToObj(
  421. i -> new UserTelemetryDto().setUuid("uuid-" + i).setActive(i % 2 == 0).setLastConnectionDate(1L).setLastSonarlintConnectionDate(2L).setExternalIdentityProvider("gitlab"))
  422. .collect(Collectors.toList());
  423. }
  424. private static List<TelemetryData.Project> attachProjects() {
  425. return IntStream.range(0, 3).mapToObj(i -> new TelemetryData.Project("uuid-" + i, 1L, "lang-" + i, (i + 1L) * 2L)).collect(Collectors.toList());
  426. }
  427. private static List<TelemetryData.ProjectStatistics> attachProjectStatsWithMetrics() {
  428. return IntStream.range(0, 3).mapToObj(i -> getProjectStatisticsWithMetricBuilder(i).build()).toList();
  429. }
  430. private static List<TelemetryData.ProjectStatistics> attachProjectStats() {
  431. return IntStream.range(0, 3).mapToObj(i -> getProjectStatisticsBuilder(i).build()).toList();
  432. }
  433. private static TelemetryData.ProjectStatistics.Builder getProjectStatisticsBuilder(int i) {
  434. return new TelemetryData.ProjectStatistics.Builder()
  435. .setProjectUuid("uuid-" + i)
  436. .setBranchCount((i + 1L) * 2L)
  437. .setPRCount((i + 1L) * 2L)
  438. .setQG("qg-" + i).setCi("ci-" + i)
  439. .setScm("scm-" + i)
  440. .setDevops("devops-" + i);
  441. }
  442. private static TelemetryData.ProjectStatistics.Builder getProjectStatisticsWithMetricBuilder(int i) {
  443. return getProjectStatisticsBuilder(i)
  444. .setBugs((i + 1L) * 2)
  445. .setVulnerabilities((i + 1L) * 3)
  446. .setSecurityHotspots((i + 1L) * 4)
  447. .setDevelopmentCost((i + 1L) * 30d)
  448. .setTechnicalDebt((i + 1L) * 60d);
  449. }
  450. private List<TelemetryData.QualityGate> attachQualityGates() {
  451. return List.of(new TelemetryData.QualityGate("uuid-0", "non-compliant"),
  452. new TelemetryData.QualityGate("uuid-1", "compliant"),
  453. new TelemetryData.QualityGate("uuid-2", "over-compliant"));
  454. }
  455. @DataProvider
  456. public static Object[][] allEditions() {
  457. return Arrays.stream(EditionProvider.Edition.values())
  458. .map(t -> new Object[]{t})
  459. .toArray(Object[][]::new);
  460. }
  461. private String writeTelemetryData(TelemetryData data) {
  462. StringWriter jsonString = new StringWriter();
  463. try (JsonWriter json = JsonWriter.of(jsonString)) {
  464. underTest.writeTelemetryData(json, data);
  465. }
  466. return jsonString.toString();
  467. }
  468. @DataProvider
  469. public static Set<Boolean> getFeatureFlagEnabledStates() {
  470. return Set.of(true, false);
  471. }
  472. }