Browse Source

SONAR-19483 Collect daily counts of analysis and green Quality Gate in Telemetry for branches

Co-authored-by: Zipeng WU <zipeng.wu@sonarsource.com>
Co-authored-by: Nolwenn Cadic <nolwenn.cadic@sonarsource.com>
tags/10.1.0.73491
Zipeng WU 1 year ago
parent
commit
6bf45ac19f

+ 32
- 5
server/sonar-db-dao/src/it/java/org/sonar/db/component/BranchDaoIT.java View File

import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.Assertions.entry;
import static org.assertj.core.api.Assertions.tuple; import static org.assertj.core.api.Assertions.tuple;
import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
import static org.sonar.db.component.BranchType.BRANCH; import static org.sonar.db.component.BranchType.BRANCH;
import static org.sonar.db.component.BranchType.PULL_REQUEST; import static org.sonar.db.component.BranchType.PULL_REQUEST;


assertThat(loaded.isMain()).isTrue(); assertThat(loaded.isMain()).isTrue();
} }


@Test
public void selectBranchMeasuresForTelemetry() {
BranchDto dto = new BranchDto();
dto.setProjectUuid("U1");
dto.setUuid("U1");
dto.setBranchType(BranchType.BRANCH);
dto.setKey("feature");
dto.setIsMain(true);
dto.setExcludeFromPurge(false);
underTest.insert(dbSession, dto);

MetricDto qg = db.measures().insertMetric(m -> m.setKey(ALERT_STATUS_KEY));
SnapshotDto analysis = db.components().insertSnapshot(dto);
db.measures().insertMeasure(dto, analysis, qg, pm -> pm.setData("OK"));

var branchMeasures = underTest.selectBranchMeasuresWithCaycMetric(dbSession);

assertThat(branchMeasures)
.hasSize(1)
.extracting(BranchMeasuresDto::getBranchUuid, BranchMeasuresDto::getBranchKey, BranchMeasuresDto::getProjectUuid,
BranchMeasuresDto::getAnalysisCount, BranchMeasuresDto::getGreenQualityGateCount, BranchMeasuresDto::getExcludeFromPurge)
.containsExactly(tuple("U1", "feature", "U1", 1, 1, false));
}

@Test @Test
public void updateExcludeFromPurge() { public void updateExcludeFromPurge() {
BranchDto dto = new BranchDto(); BranchDto dto = new BranchDto();


@DataProvider @DataProvider
public static Object[][] nullOrEmpty() { public static Object[][] nullOrEmpty() {
return new Object[][]{
return new Object[][] {
{null}, {null},
{""} {""}
}; };
public static Object[][] oldAndNewValuesCombinations() { public static Object[][] oldAndNewValuesCombinations() {
String value1 = randomAlphabetic(10); String value1 = randomAlphabetic(10);
String value2 = randomAlphabetic(20); String value2 = randomAlphabetic(20);
return new Object[][]{
return new Object[][] {
{null, value1}, {null, value1},
{"", value1}, {"", value1},
{value1, null}, {value1, null},


assertThat(branches).extracting(BranchDto::getUuid, BranchDto::getKey, BranchDto::isMain, BranchDto::getProjectUuid, BranchDto::getBranchType, BranchDto::getMergeBranchUuid) assertThat(branches).extracting(BranchDto::getUuid, BranchDto::getKey, BranchDto::isMain, BranchDto::getProjectUuid, BranchDto::getBranchType, BranchDto::getMergeBranchUuid)
.containsOnly(tuple(mainBranch.getUuid(), mainBranch.getKey(), mainBranch.isMain(), mainBranch.getProjectUuid(), mainBranch.getBranchType(), mainBranch.getMergeBranchUuid()), .containsOnly(tuple(mainBranch.getUuid(), mainBranch.getKey(), mainBranch.isMain(), mainBranch.getProjectUuid(), mainBranch.getBranchType(), mainBranch.getMergeBranchUuid()),
tuple(featureBranch.getUuid(), featureBranch.getKey(), featureBranch.isMain(), featureBranch.getProjectUuid(), featureBranch.getBranchType(), featureBranch.getMergeBranchUuid()));
tuple(featureBranch.getUuid(), featureBranch.getKey(), featureBranch.isMain(), featureBranch.getProjectUuid(), featureBranch.getBranchType(),
featureBranch.getMergeBranchUuid()));
} }


@Test @Test
db.measures().insertLiveMeasure(project3, unanalyzedC); db.measures().insertLiveMeasure(project3, unanalyzedC);


assertThat(underTest.countPrBranchAnalyzedLanguageByProjectUuid(db.getSession())) assertThat(underTest.countPrBranchAnalyzedLanguageByProjectUuid(db.getSession()))
.extracting(PrBranchAnalyzedLanguageCountByProjectDto::getProjectUuid, PrBranchAnalyzedLanguageCountByProjectDto::getBranch, PrBranchAnalyzedLanguageCountByProjectDto::getPullRequest)
.extracting(PrBranchAnalyzedLanguageCountByProjectDto::getProjectUuid, PrBranchAnalyzedLanguageCountByProjectDto::getBranch,
PrBranchAnalyzedLanguageCountByProjectDto::getPullRequest)
.containsExactlyInAnyOrder( .containsExactlyInAnyOrder(
tuple(project1.uuid(), 3L, 3L), tuple(project1.uuid(), 3L, 3L),
tuple(project2.uuid(), 1L, 1L), tuple(project2.uuid(), 1L, 1L),


@DataProvider @DataProvider
public static Object[][] booleanValues() { public static Object[][] booleanValues() {
return new Object[][]{
return new Object[][] {
{true}, {true},
{false} {false}
}; };

+ 5
- 2
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java View File

*/ */
package org.sonar.db.component; package org.sonar.db.component;


import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
.orElse(false); .orElse(false);
} }


public List<BranchDto> selectAllBranches(DbSession dbSession) {
return mapper(dbSession).selectAllBranches();
public List<BranchMeasuresDto> selectBranchMeasuresWithCaycMetric(DbSession dbSession) {
long yesterday = ZonedDateTime.now(ZoneId.systemDefault()).minusDays(1).toInstant().toEpochMilli();
return mapper(dbSession).selectBranchMeasuresWithCaycMetric(yesterday);
} }
} }

+ 1
- 2
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java View File

import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;


public interface BranchMapper { public interface BranchMapper {


List<BranchDto> selectMainBranchesByProjectUuids(@Param("projectUuids") Collection<String> projectUuids); List<BranchDto> selectMainBranchesByProjectUuids(@Param("projectUuids") Collection<String> projectUuids);


List<BranchDto> selectAllBranches();
List<BranchMeasuresDto> selectBranchMeasuresWithCaycMetric(long yesterday);
} }

+ 63
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMeasuresDto.java View File

/*
* SonarQube
* Copyright (C) 2009-2023 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.db.component;

public class BranchMeasuresDto {
private String branchUuid;
private String projectUuid;
private String branchKey;
private boolean excludeFromPurge;
private int greenQualityGateCount;
private int analysisCount;

public BranchMeasuresDto(String branchUuid, String projectUuid, String branchKey, boolean excludeFromPurge, int greenQualityGateCount, int analysisCount) {
this.branchUuid = branchUuid;
this.projectUuid = projectUuid;
this.branchKey = branchKey;
this.excludeFromPurge = excludeFromPurge;
this.greenQualityGateCount = greenQualityGateCount;
this.analysisCount = analysisCount;
}

public String getBranchUuid() {
return branchUuid;
}

public String getProjectUuid() {
return projectUuid;
}

public boolean getExcludeFromPurge() {
return excludeFromPurge;
}

public int getGreenQualityGateCount() {
return greenQualityGateCount;
}

public int getAnalysisCount() {
return analysisCount;
}

public String getBranchKey() {
return branchKey;
}

}

+ 21
- 3
server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml View File

pb.is_main as isMain pb.is_main as isMain
</sql> </sql>


<sql id="telemetryColumns">
pb.uuid as branchUuid,
pb.project_uuid as projectUuid,
pb.kee as branchKey,
pb.exclude_from_purge as excludeFromPurge,
coalesce(ag.greenQualityGateCount, 0) as greenQualityGateCount,
coalesce(ag.analysisCount, 0) as analysisCount
</sql>

<insert id="insert" parameterType="map" useGeneratedKeys="false"> <insert id="insert" parameterType="map" useGeneratedKeys="false">
insert into project_branches ( insert into project_branches (
uuid, uuid,
</foreach> </foreach>
</select> </select>


<select id="selectAllBranches" resultType="org.sonar.db.component.BranchDto">
<select id="selectBranchMeasuresWithCaycMetric" resultType="org.sonar.db.component.BranchMeasuresDto">
select select
<include refid="columns"/>
<include refid="telemetryColumns"/>
from project_branches pb from project_branches pb
where branch_type='BRANCH'
left join (
select pm.component_uuid as branchUuid, count(case when pm.text_value ='OK' then 1 end) as greenQualityGateCount, count(1) as analysisCount
from project_measures pm
inner join metrics m on m.uuid = pm.metric_uuid
inner join snapshots s on s.uuid = pm.analysis_uuid
where m.name = 'alert_status' and s.created_at >= #{yesterday, jdbcType=BIGINT}
group by pm.component_uuid
) ag
on ag.branchUuid = pb.uuid
where pb.branch_type='BRANCH'
</select> </select>


<select id="selectByProjectUuid" parameterType="string" resultType="org.sonar.db.component.BranchDto"> <select id="selectByProjectUuid" parameterType="string" resultType="org.sonar.db.component.BranchDto">

+ 1
- 1
server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java View File

} }
} }


record Branch(String projectUuid, String branchUuid, int ncdId) {
record Branch(String projectUuid, String branchUuid, int ncdId, int greenQualityGateCount, int analysisCount, boolean excludeFromPurge) {
} }


record Project(String projectUuid, Long lastAnalysis, String language, Long loc) { record Project(String projectUuid, Long lastAnalysis, String language, Long loc) {

+ 3
- 0
server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java View File

json.prop(PROJECT_ID, branch.projectUuid()); json.prop(PROJECT_ID, branch.projectUuid());
json.prop("branchUuid", branch.branchUuid()); json.prop("branchUuid", branch.branchUuid());
json.prop(NCD_ID, branch.ncdId()); json.prop(NCD_ID, branch.ncdId());
json.prop("greenQualityGateCount", branch.greenQualityGateCount());
json.prop("analysisCount", branch.analysisCount());
json.prop("excludeFromPurge", branch.excludeFromPurge());
json.endObject(); json.endObject();
}); });
json.endArray(); json.endArray();

+ 17
- 11
server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java View File

{ {
"branches": [ "branches": [
{ {
"projectUuid": "%s",
"branchUuid": "%s",
"ncdId": %s
"projectUuid": "projectUuid1",
"branchUuid": "branchUuid1",
"ncdId": 12345,
"greenQualityGateCount": 1,
"analysisCount": 2,
"excludeFromPurge": true
}, },
{ {
"projectUuid": "%s",
"branchUuid": "%s",
"ncdId": %s
},
"projectUuid": "projectUuid2",
"branchUuid": "branchUuid2",
"ncdId": 12345,
"greenQualityGateCount": 0,
"analysisCount": 2,
"excludeFromPurge": true
}
] ]

} }
""".formatted("projectUuid1", "branchUuid1", NCD_ID, "projectUuid2", "branchUuid2", NCD_ID));
""");
} }


@Test @Test
} }


private List<TelemetryData.Branch> attachBranches() { private List<TelemetryData.Branch> attachBranches() {
return List.of(new TelemetryData.Branch("projectUuid1", "branchUuid1", NCD_ID),
new TelemetryData.Branch("projectUuid2", "branchUuid2", NCD_ID));
return List.of(new TelemetryData.Branch("projectUuid1", "branchUuid1", NCD_ID, 1, 2, true),
new TelemetryData.Branch("projectUuid2", "branchUuid2", NCD_ID, 0, 2, true));
} }

private List<TelemetryData.NewCodeDefinition> attachNewCodeDefinitions() { private List<TelemetryData.NewCodeDefinition> attachNewCodeDefinitions() {
return List.of(NCD_INSTANCE, NCD_PROJECT); return List.of(NCD_INSTANCE, NCD_PROJECT);
} }

+ 13
- 10
server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java View File

import org.sonar.db.alm.setting.ALM; import org.sonar.db.alm.setting.ALM;
import org.sonar.db.alm.setting.ProjectAlmKeyAndProject; import org.sonar.db.alm.setting.ProjectAlmKeyAndProject;
import org.sonar.db.component.AnalysisPropertyValuePerProject; import org.sonar.db.component.AnalysisPropertyValuePerProject;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.BranchMeasuresDto;
import org.sonar.db.component.PrBranchAnalyzedLanguageCountByProjectDto; import org.sonar.db.component.PrBranchAnalyzedLanguageCountByProjectDto;
import org.sonar.db.component.SnapshotDto; import org.sonar.db.component.SnapshotDto;
import org.sonar.db.measure.LiveMeasureDto; import org.sonar.db.measure.LiveMeasureDto;
getVersion)); getVersion));
data.setPlugins(plugins); data.setPlugins(plugins);
try (DbSession dbSession = dbClient.openSession(false)) { try (DbSession dbSession = dbClient.openSession(false)) {
var branchDtos = dbClient.branchDao().selectAllBranches(dbSession);
loadNewCodeDefinitions(dbSession, branchDtos);
var branchMeasuresDtos = dbClient.branchDao().selectBranchMeasuresWithCaycMetric(dbSession);
loadNewCodeDefinitions(dbSession, branchMeasuresDtos);


data.setDatabase(loadDatabaseMetadata(dbSession)); data.setDatabase(loadDatabaseMetadata(dbSession));
data.setNcdId(instanceNcd.hashCode()); data.setNcdId(instanceNcd.hashCode());
resolveUnanalyzedLanguageCode(data, dbSession); resolveUnanalyzedLanguageCode(data, dbSession);
resolveProjectStatistics(data, dbSession, defaultQualityGateUuid); resolveProjectStatistics(data, dbSession, defaultQualityGateUuid);
resolveProjects(data, dbSession); resolveProjects(data, dbSession);
resolveBranches(data, branchDtos);
resolveBranches(data, branchMeasuresDtos);
resolveQualityGates(data, dbSession); resolveQualityGates(data, dbSession);
resolveUsers(data, dbSession); resolveUsers(data, dbSession);
} }
.build(); .build();
} }


private void resolveBranches(TelemetryData.Builder data, List<BranchDto> branchDtos) {
var branches = branchDtos.stream()
private void resolveBranches(TelemetryData.Builder data, List<BranchMeasuresDto> branchMeasuresDtos) {
var branches = branchMeasuresDtos.stream()
.map(dto -> { .map(dto -> {
var projectNcd = ncdByProject.getOrDefault(dto.getProjectUuid(), instanceNcd); var projectNcd = ncdByProject.getOrDefault(dto.getProjectUuid(), instanceNcd);
var ncdId = ncdByBranch.getOrDefault(dto.getUuid(), projectNcd).hashCode();
return new TelemetryData.Branch(dto.getProjectUuid(), dto.getUuid(), ncdId);
var ncdId = ncdByBranch.getOrDefault(dto.getBranchUuid(), projectNcd).hashCode();
return new TelemetryData.Branch(
dto.getProjectUuid(), dto.getBranchUuid(), ncdId,
dto.getGreenQualityGateCount(), dto.getAnalysisCount(), dto.getExcludeFromPurge());
}) })
.toList(); .toList();
data.setBranches(branches); data.setBranches(branches);
this.instanceNcd = NewCodeDefinition.getInstanceDefault(); this.instanceNcd = NewCodeDefinition.getInstanceDefault();
} }


private void loadNewCodeDefinitions(DbSession dbSession, List<BranchDto> branchDtos) {
var branchUuidByKey = branchDtos.stream().collect(Collectors.toMap(dto -> createBranchUniqueKey(dto.getProjectUuid(), dto.getBranchKey()), BranchDto::getUuid));
private void loadNewCodeDefinitions(DbSession dbSession, List<BranchMeasuresDto> branchMeasuresDtos) {
var branchUuidByKey = branchMeasuresDtos.stream()
.collect(Collectors.toMap(dto -> createBranchUniqueKey(dto.getProjectUuid(), dto.getBranchKey()), BranchMeasuresDto::getBranchUuid));
List<NewCodePeriodDto> newCodePeriodDtos = dbClient.newCodePeriodDao().selectAll(dbSession); List<NewCodePeriodDto> newCodePeriodDtos = dbClient.newCodePeriodDao().selectAll(dbSession);
NewCodeDefinition ncd; NewCodeDefinition ncd;
boolean hasInstance = false; boolean hasInstance = false;

+ 46
- 2
server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java View File

import com.tngtech.java.junit.dataprovider.UseDataProvider; import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.sql.DatabaseMetaData; import java.sql.DatabaseMetaData;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.sonar.server.property.MapInternalProperties; import org.sonar.server.property.MapInternalProperties;
import org.sonar.server.qualitygate.QualityGateCaycChecker; import org.sonar.server.qualitygate.QualityGateCaycChecker;
import org.sonar.server.qualitygate.QualityGateFinder; import org.sonar.server.qualitygate.QualityGateFinder;
import org.sonar.server.telemetry.TelemetryData.Branch;
import org.sonar.server.telemetry.TelemetryData.NewCodeDefinition; import org.sonar.server.telemetry.TelemetryData.NewCodeDefinition;
import org.sonar.server.telemetry.TelemetryData.ProjectStatistics; import org.sonar.server.telemetry.TelemetryData.ProjectStatistics;
import org.sonar.updatecenter.common.Version; import org.sonar.updatecenter.common.Version;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
import static org.sonar.api.measures.CoreMetrics.BUGS_KEY; import static org.sonar.api.measures.CoreMetrics.BUGS_KEY;
import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY; import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY;
import static org.sonar.api.measures.CoreMetrics.DEVELOPMENT_COST_KEY; import static org.sonar.api.measures.CoreMetrics.DEVELOPMENT_COST_KEY;
Optional.empty(), instanceNcdId)); Optional.empty(), instanceNcdId));


assertThat(data.getBranches()) assertThat(data.getBranches())
.extracting(TelemetryData.Branch::branchUuid, TelemetryData.Branch::ncdId)
.extracting(Branch::branchUuid, Branch::ncdId)
.containsExactlyInAnyOrder( .containsExactlyInAnyOrder(
tuple(branch1.uuid(), projectNcdId), tuple(branch1.uuid(), projectNcdId),
tuple(branch2.uuid(), branchNcdId), tuple(branch2.uuid(), branchNcdId),
); );
} }


@Test
public void send_branch_measures_data() {
Long analysisDate = ZonedDateTime.now(ZoneId.systemDefault()).toInstant().toEpochMilli();

MetricDto qg = db.measures().insertMetric(m -> m.setKey(ALERT_STATUS_KEY));

ComponentDto project1 = db.components().insertPrivateProject().getMainBranchComponent();

ComponentDto project2 = db.components().insertPrivateProject().getMainBranchComponent();

SnapshotDto project1Analysis1 = db.components().insertSnapshot(project1, t -> t.setLast(true).setBuildDate(analysisDate));
SnapshotDto project1Analysis2 = db.components().insertSnapshot(project1, t -> t.setLast(true).setBuildDate(analysisDate));
SnapshotDto project2Analysis = db.components().insertSnapshot(project2, t -> t.setLast(true).setBuildDate(analysisDate));
db.measures().insertMeasure(project1, project1Analysis1, qg, pm -> pm.setData("OK"));
db.measures().insertMeasure(project1, project1Analysis2, qg, pm -> pm.setData("ERROR"));
db.measures().insertMeasure(project2, project2Analysis, qg, pm -> pm.setData("ERROR"));

var branch1 = db.components().insertProjectBranch(project1, branchDto -> branchDto.setKey("reference"));
var branch2 = db.components().insertProjectBranch(project1, branchDto -> branchDto.setKey("custom"));

db.newCodePeriods().insert(project1.uuid(), NewCodePeriodType.NUMBER_OF_DAYS, "30");
db.newCodePeriods().insert(project1.uuid(), branch2.branchUuid(), NewCodePeriodType.REFERENCE_BRANCH, "reference");

var instanceNcdId = NewCodeDefinition.getInstanceDefault().hashCode();
var projectNcdId = new NewCodeDefinition(NewCodePeriodType.NUMBER_OF_DAYS.name(), "30", "project").hashCode();
var branchNcdId = new NewCodeDefinition(NewCodePeriodType.REFERENCE_BRANCH.name(), branch1.uuid(), "branch").hashCode();

TelemetryData data = communityUnderTest.load();

assertThat(data.getBranches())
.extracting(Branch::branchUuid, Branch::ncdId, Branch::greenQualityGateCount, Branch::analysisCount)
.containsExactlyInAnyOrder(
tuple(branch1.uuid(), projectNcdId, 0, 0),
tuple(branch2.uuid(), branchNcdId, 0, 0),
tuple(project1.uuid(), projectNcdId, 1, 2),
tuple(project2.uuid(), instanceNcdId, 0, 1));

}

private List<UserDto> composeActiveUsers(int count) { private List<UserDto> composeActiveUsers(int count) {
UserDbTester userDbTester = db.users(); UserDbTester userDbTester = db.users();
Function<Integer, Consumer<UserDto>> userConfigurator = index -> user -> user.setExternalIdentityProvider("provider" + index).setLastSonarlintConnectionDate(index * 2L); Function<Integer, Consumer<UserDto>> userConfigurator = index -> user -> user.setExternalIdentityProvider("provider" + index).setLastSonarlintConnectionDate(index * 2L);
.containsExactlyInAnyOrder(tuple(2L, projectNcdId)); .containsExactlyInAnyOrder(tuple(2L, projectNcdId));


assertThat(data.getBranches()) assertThat(data.getBranches())
.extracting(TelemetryData.Branch::branchUuid, TelemetryData.Branch::ncdId)
.extracting(Branch::branchUuid, Branch::ncdId)
.contains(tuple(branch.uuid(), projectNcdId)); .contains(tuple(branch.uuid(), projectNcdId));
} }



Loading…
Cancel
Save