Skip to content

Commit

Permalink
fix(functionality): display correct scenario aggregates
Browse files Browse the repository at this point in the history
  • Loading branch information
omar-chahbouni-decathlon committed Jul 12, 2022
1 parent 9fdfa11 commit bf0bf6d
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.decathlon.ara.scenario.cucumber.util.ScenarioExtractorUtil;
import com.decathlon.ara.service.exception.BadRequestException;
import com.decathlon.ara.service.exception.NotFoundException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.util.Pair;
Expand All @@ -18,6 +19,9 @@

import javax.persistence.EntityManager;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Component
@Transactional
Expand Down Expand Up @@ -69,16 +73,14 @@ public void processUploadedContent(long projectId, String sourceCode, Technology
// (first get all functionalities with their remaining scenarios eagerly-fetched)
Set<Functionality> functionalities = functionalityRepository.findAllByProjectIdAndType(projectId, FunctionalityType.FUNCTIONALITY);

deleteScenariosFromSameSource(source, functionalities);

functionalities = deleteScenariosFromSameSource(source, functionalities);

assignWrongFunctionalityIds(functionalities, newScenarios);
assignWrongSeverityCode(getSeverityCodes(projectId), newScenarios);
assignWrongCountryCodes(getCountryCodes(projectId), newScenarios);
// Save the new scenarios
newScenarios = scenarioRepository.saveAll(newScenarios);
LOG.info("SCENARIO|{} scenarios updated for source {}", newScenarios.size(), sourceCode);
entityManager.flush();

// Re-assign new scenarios to functionalities
assignCoverage(functionalities, newScenarios);
Expand All @@ -88,7 +90,7 @@ public void processUploadedContent(long projectId, String sourceCode, Technology
LOG.info("SCENARIO|Coverage complete!");
}

private void deleteScenariosFromSameSource(Source source, Set<Functionality> functionalities) {
private Set<Functionality> deleteScenariosFromSameSource(Source source, Set<Functionality> functionalities) {
var functionalitiesWithoutSourceScenarios = functionalities.stream()
.map(f -> Pair.of(f, f.getScenarios().stream().filter(s -> !s.getSource().equals(source)).toList()))
.map(p -> Pair.of(p.getFirst(), new TreeSet<>(p.getSecond())))
Expand All @@ -99,8 +101,9 @@ private void deleteScenariosFromSameSource(Source source, Set<Functionality> fun
return f;
})
.toList();
functionalityRepository.saveAll(functionalitiesWithoutSourceScenarios);
functionalitiesWithoutSourceScenarios = functionalityRepository.saveAll(functionalitiesWithoutSourceScenarios);
scenarioRepository.deleteAllBySource(source);
return new HashSet<>(functionalitiesWithoutSourceScenarios);
}

@FunctionalInterface
Expand Down Expand Up @@ -151,9 +154,10 @@ private List<String> getSeverityCodes(long projectId) {
public void assignWrongCountryCodes(List<String> countryCodes, List<Scenario> scenarios) {
for (Scenario scenario : scenarios) {
StringBuilder builder = new StringBuilder();
for (String countryCode : scenario.getCountryCodes().split(Scenario.COUNTRY_CODES_SEPARATOR)) {
if (!countryCodes.contains(countryCode)) {
builder.append(builder.length() == 0 ? "" : ",").append(countryCode);
var scenarioCountryCodes = StringUtils.isNotBlank(scenario.getCountryCodes()) ? scenario.getCountryCodes() : "";
for (String scenarioCountryCode : scenarioCountryCodes.split(Scenario.COUNTRY_CODES_SEPARATOR)) {
if (!countryCodes.contains(scenarioCountryCode)) {
builder.append(builder.length() == 0 ? "" : ",").append(scenarioCountryCode);
}
}
scenario.setWrongCountryCodes(builder.length() == 0 ? null : builder.toString());
Expand Down Expand Up @@ -187,96 +191,90 @@ private static void assignCoverage(Collection<Functionality> functionalities, Li
}

/**
* @param functionalities for each of them, if it is not a folder, update the coverage counts (normal & ignored) and coverage per source and ignore state
* @param functionalities for each of them, if it is not a folder, update the coverage counts (covered & ignored) and coverage per source and ignore state
*/
private static void computeAggregates(Collection<Functionality> functionalities) {
for (Functionality functionality : functionalities) {
final Set<Scenario> scenarios = functionality.getScenarios();

functionality.setCoveredScenarios(computeCount(scenarios, false));
functionality.setIgnoredScenarios(computeCount(scenarios, true));

functionality.setCoveredCountryScenarios(computeGlobalAggregate(scenarios, false));
functionality.setIgnoredCountryScenarios(computeGlobalAggregate(scenarios, true));
}
functionalities.forEach(ScenarioUploader::computeAggregates);
}

/**
* @param scenarios a list of scenarios to count for the ignore state
* @param ignored the ignore state for a scenario to be counted
* @return the count of scenarios with the given ignore state
* @param functionality update the coverage counts (covered & ignored) and coverage per source and ignore state
*/
private static Integer computeCount(Set<Scenario> scenarios, boolean ignored) {
return Integer.valueOf((int) scenarios.stream().filter(s -> s.isIgnored() == ignored).count());
private static void computeAggregates(Functionality functionality) {
var coverageAggregates = getCoverageAggregatesFromFunctionality(functionality);
var coverAggregate = coverageAggregates.get(false);
var ignoreAggregate = coverageAggregates.get(true);
var coverageNumbers = getCoverageNumbersFromFunctionality(functionality);
var coverNumber = coverageNumbers.containsKey(false) ? coverageNumbers.get(false).intValue() : 0;
var ignoreNumber = coverageNumbers.containsKey(true) ? coverageNumbers.get(true).intValue() : 0;

functionality.setCoveredScenarios(coverNumber);
functionality.setIgnoredScenarios(ignoreNumber);

functionality.setCoveredCountryScenarios(coverAggregate);
functionality.setIgnoredCountryScenarios(ignoreAggregate);
}

/**
* For a given ignore state, count matching scenarios per source and country.
*
* @param scenarios a list of scenarios to count per source and country for the ignore state
* @param ignored the ignore state for a scenario to match
* @return eg. "API:cn=3,nl=1|WEB:be=2" or null if no coverage was found for the ignore state
* Get aggregates displaying the scenarios distribution:
* - by state (ignored or covered), then
* - by source code, then
* - by country codes
* e.g. if map.get(true) returns "source_1:*=1,xx=1,yy=1|source_2:*=2,xx=1,yy=2", it means that:
* - it is an ignored state (map.get(true))
* - the functionality has 1 source_1 ignored scenario total (*=1) in which
* - 1 concerns the country xx (xx=1)
* - 1 concerns the country yy (yy=1)
* - it has 2 source_2 ignored scenarios total (*=2) in which
* - 1 concerns the country xx (xx=1)
* - 2 concerns the country yy (yy=2)
* If null, then there is no scenarios covered or ignored
* @param functionality the functionality to get the coverage aggregates from
* @return ignored and covered aggregates
*/
private static String computeGlobalAggregate(Set<Scenario> scenarios, boolean ignored) {
List<Source> sortedDistinctSources = scenarios.stream()
.map(Scenario::getSource)
.distinct()
static Map<Boolean, String> getCoverageAggregatesFromFunctionality(Functionality functionality) {
Function<List<Scenario>, String> partialAggregateFromScenarios = scenarios -> scenarios.stream()
.map(Scenario::getCountryCodes)
.filter(StringUtils::isNotBlank)
.map(s -> s.split(Scenario.COUNTRY_CODES_SEPARATOR))
.flatMap(Stream::of)
.collect(Collectors.groupingBy(s -> s, Collectors.counting()))
.entrySet()
.stream()
.map(e -> String.format("%s=%d", e.getKey(), e.getValue()))
.sorted()
.toList();

String coverage = null;
for (Source source : sortedDistinctSources) {
String aggregate = computeGlobalAggregate(scenarios, source, ignored);
if (aggregate != null) {
coverage = (coverage == null ? "" : coverage + "|") + aggregate;
}
}
return coverage;
}

/**
* For a given source and an ignore state, count matching scenarios per country.
*
* @param scenarios a list of scenarios to count per source for the given ignore state
* @param source the source for a scenario to match
* @param ignored the ignore state for a scenario to match
* @return eg. "API:cn=3,nl=1" or null if no coverage was found for the source and ignore state
*/
private static String computeGlobalAggregate(Set<Scenario> scenarios, Source source, boolean ignored) {
Map<String, Integer> countryCoverage = new TreeMap<>(); // TreeMap: ordered by key alphabetically

for (Scenario scenario : scenarios) {
if (scenario.getSource().equals(source) && scenario.isIgnored() == ignored) {
increment(countryCoverage, TOTAL);
// Still count scenarios without country codes
for (String countryCode : scenario.getCountryCodes().split(Scenario.COUNTRY_CODES_SEPARATOR)) {
increment(countryCoverage, countryCode);
}
}
}

if (countryCoverage.isEmpty()) {
return null;
}

StringBuilder aggregate = new StringBuilder(source.getCode());
boolean first = true;
for (Map.Entry<String, Integer> entry : countryCoverage.entrySet()) {
aggregate.append(first ? ':' : ',').append(entry.getKey()).append('=').append(entry.getValue().toString());
first = false;
}
return aggregate.toString();
.collect(Collectors.joining(","));
Function<Map.Entry<String, List<Scenario>>, String> aggregateForSource = entry -> {
var sourceCode = entry.getKey();
var scenarios = entry.getValue();
var partialScenarioAggregate = partialAggregateFromScenarios.apply(scenarios);
var partialAggregate = StringUtils.isNotBlank(partialScenarioAggregate) ? String.format(",%s", partialScenarioAggregate) : "";
return String.format("%s:%s=%d%s", sourceCode, TOTAL, scenarios.size(), partialAggregate);
};
return functionality.getScenarios()
.stream()
.collect(Collectors.groupingBy(Scenario::isIgnored))
.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, e1 -> e1.getValue()
.stream()
.collect(Collectors.groupingBy(sc -> sc.getSource().getCode()))
.entrySet()
.stream()
.map(aggregateForSource::apply)
.collect(Collectors.joining("|"))
));
}

/**
* Increment the <code>key</code> in the map of <code>counts</code> (the key may not exist beforehand).
*
* @param counts the map containing counts by a String key
* @param key the key to increment, created before increment if not existing yet
* Get coverage state distribution
* e.g. map.get(true) returns the total ignored scenarios number whereas map.get(false) is about covered scenarios number
* @param functionality the functionality
* @return the coverage state distribution
*/
private static void increment(Map<String, Integer> counts, String key) {
Integer oldValue = counts.get(key);
Integer newValue = Integer.valueOf(oldValue == null ? 1 : oldValue.intValue() + 1);
counts.put(key, newValue);
static Map<Boolean, Long> getCoverageNumbersFromFunctionality(Functionality functionality) {
return functionality.getScenarios()
.stream()
.collect(Collectors.groupingBy(Scenario::isIgnored, Collectors.counting()));
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
package com.decathlon.ara.scenario.common.upload;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Arrays;
import java.util.List;

import javax.persistence.EntityManager;

import com.decathlon.ara.domain.Functionality;
import com.decathlon.ara.domain.Scenario;
import com.decathlon.ara.domain.Source;
import com.decathlon.ara.repository.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.decathlon.ara.domain.Scenario;
import com.decathlon.ara.repository.CountryRepository;
import com.decathlon.ara.repository.FunctionalityRepository;
import com.decathlon.ara.repository.ScenarioRepository;
import com.decathlon.ara.repository.SeverityRepository;
import com.decathlon.ara.repository.SourceRepository;
import javax.persistence.EntityManager;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class ScenarioUploaderTest {
Expand Down Expand Up @@ -46,7 +45,6 @@ class ScenarioUploaderTest {

@Test
void assignWrongSeverityCode_ShouldComputeAWrongSeverityCode_WhenCalled() {

//GIVEN
final List<String> severityCodes = Arrays.asList("existing");
final List<Scenario> scenarios = Arrays.asList(scenario(null, "existing"), scenario(null, "nonexitent"));
Expand All @@ -61,8 +59,8 @@ void assignWrongSeverityCode_ShouldComputeAWrongSeverityCode_WhenCalled() {

}

@Test
void assignWrongCountryCodes_ShouldSetWrongCountryCodeToNull_WhenAllCountriesExist() {

//GIVEN
List<String> countryCodes = Arrays.asList("existing");
final List<Scenario> scenarios = Arrays.asList(scenario(null, "existing"));
Expand All @@ -75,8 +73,8 @@ void assignWrongCountryCodes_ShouldSetWrongCountryCodeToNull_WhenAllCountriesExi

}

@Test
void assignWrongCountryCodes_ShouldComputeWrongCountryCode_WhenCountriesDoNotExist() {

//GIVEN
List<String> countryCodes = Arrays.asList("be", "cn", "de", "hk", "nl");
final List<Scenario> scenarios = Arrays.asList(scenario("nl,XX,YY", null));
Expand All @@ -91,7 +89,6 @@ void assignWrongCountryCodes_ShouldComputeWrongCountryCode_WhenCountriesDoNotExi

@Test
void assignWrongCountryCodes_ShouldAssignWrongCountryCodesOnlyToWrongOnes_WhenCalledWithSeveralScenarios() {

//GIVEN
List<String> countryCodes = Arrays.asList("be", "cn", "de", "hk", "nl");
final List<Scenario> scenarios = Arrays.asList(scenario("nl", null), scenario("nonexitent", null), scenario("de,AA,ZZ", null));
Expand All @@ -114,4 +111,75 @@ private Scenario scenario(String countryCodes, String severity) {
scenario.setSeverity(severity);
return scenario;
}

@Test
void getCoverageAggregatesFromFunctionality_returnCoverageAggregates() {
// Given
Functionality functionality = mock(Functionality.class);
Scenario scenario1 = mock(Scenario.class);
Scenario scenario2 = mock(Scenario.class);
Scenario scenario3 = mock(Scenario.class);
Scenario scenario4 = mock(Scenario.class);
Scenario scenario5 = mock(Scenario.class);
var scenarios = new HashSet<>(List.of(scenario1, scenario2, scenario3, scenario4, scenario5));

Source source1 = mock(Source.class);
Source source2 = mock(Source.class);

// When
when(functionality.getScenarios()).thenReturn(scenarios);

when(scenario1.isIgnored()).thenReturn(false);
when(scenario1.getSource()).thenReturn(source1);
when(scenario1.getCountryCodes()).thenReturn("fr,us");
when(scenario2.isIgnored()).thenReturn(true);
when(scenario2.getSource()).thenReturn(source2);
when(scenario2.getCountryCodes()).thenReturn("fr,xx");
when(scenario3.isIgnored()).thenReturn(false);
when(scenario3.getSource()).thenReturn(source2);
when(scenario3.getCountryCodes()).thenReturn("fr,us");
when(scenario4.isIgnored()).thenReturn(false);
when(scenario4.getSource()).thenReturn(source2);
when(scenario4.getCountryCodes()).thenReturn("us,all");
when(scenario5.isIgnored()).thenReturn(true);
when(scenario5.getSource()).thenReturn(source1);
when(scenario5.getCountryCodes()).thenReturn(null);

when(source1.getCode()).thenReturn("source_1");
when(source2.getCode()).thenReturn("source_2");

// Then
var coverageAggregates = cut.getCoverageAggregatesFromFunctionality(functionality);
var coveredScenariosAggregate = coverageAggregates.get(false);
assertThat(coveredScenariosAggregate).isEqualTo("source_2:*=2,all=1,fr=1,us=2|source_1:*=1,fr=1,us=1");
var ignoredScenariosAggregate = coverageAggregates.get(true);
assertThat(ignoredScenariosAggregate).isEqualTo("source_2:*=1,fr=1,xx=1|source_1:*=1");
}

@Test
void getCoverageNumbersFromFunctionality_returnCoverageNumbers() {
// Given
Functionality functionality = mock(Functionality.class);
Scenario scenario1 = mock(Scenario.class);
Scenario scenario2 = mock(Scenario.class);
Scenario scenario3 = mock(Scenario.class);
Scenario scenario4 = mock(Scenario.class);
Scenario scenario5 = mock(Scenario.class);
var scenarios = new HashSet<>(List.of(scenario1, scenario2, scenario3, scenario4, scenario5));

// When
when(functionality.getScenarios()).thenReturn(scenarios);
when(scenario1.isIgnored()).thenReturn(false);
when(scenario2.isIgnored()).thenReturn(true);
when(scenario3.isIgnored()).thenReturn(false);
when(scenario4.isIgnored()).thenReturn(false);
when(scenario5.isIgnored()).thenReturn(true);

// Then
var coverageNumbers = cut.getCoverageNumbersFromFunctionality(functionality);
var coveredScenariosNumber = coverageNumbers.get(false);
assertThat(coveredScenariosNumber).isEqualTo(3L);
var ignoredScenariosNumber = coverageNumbers.get(true);
assertThat(ignoredScenariosNumber).isEqualTo(2L);
}
}
Loading

0 comments on commit bf0bf6d

Please sign in to comment.