Skip to content

Commit

Permalink
Added tests to demonstrate the potential bug with requirements discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
jan-molak committed Dec 6, 2023
1 parent 0eeba61 commit 8cf49b6
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
import net.serenitybdd.model.collect.NewList;
import net.serenitybdd.model.exceptions.SerenityManagedException;
import net.thucydides.model.ThucydidesSystemProperty;
import net.thucydides.model.environment.SystemEnvironmentVariables;
import net.thucydides.model.files.TheDirectoryStructure;
import net.thucydides.model.domain.PathElements;
import net.thucydides.model.domain.RequirementCache;
import net.thucydides.model.domain.TestOutcome;
import net.thucydides.model.domain.TestTag;
import net.thucydides.model.environment.SystemEnvironmentVariables;
import net.thucydides.model.files.TheDirectoryStructure;
import net.thucydides.model.requirements.model.*;
import net.thucydides.model.requirements.model.cucumber.CucumberParser;
import net.thucydides.model.requirements.model.cucumber.InvalidFeatureFileException;
Expand Down Expand Up @@ -163,7 +163,7 @@ public List<Requirement> getRequirements() {
if (requirements == null) { // double-checked locking
List<Requirement> loadedRequirements = getRootDirectoryPaths()
.stream()
.flatMap(this::capabilitiesAndStoriesIn)
.flatMap(this::discoverRequirementsInRootDirectories)
.sorted()
.collect(Collectors.toList());
if (addParents) {
Expand All @@ -187,15 +187,15 @@ private Map<PathElements, Requirement> indexByPath(List<Requirement> requirement
return index;
}

private Stream<Requirement> capabilitiesAndStoriesIn(String path) {
private Stream<Requirement> discoverRequirementsInRootDirectories(String path) {
File rootDirectory = new File(path);

if (! (rootDirectory.exists() && rootDirectory.isDirectory())) {
return NO_REQUIREMENTS.stream();
}

return Stream.concat(
loadCapabilitiesFrom(rootDirectory.listFiles(thatAreFeatureOrSpecDirectories())),
loadRequirementsFrom(rootDirectory.listFiles(thatAreFeatureOrSpecDirectories())),
loadStoriesFrom(rootDirectory.listFiles(thatAreStories()))
);
}
Expand Down Expand Up @@ -552,7 +552,7 @@ private java.util.Optional<Requirement> findMatchingRequirementIn(String storyPa
return Optional.empty();
}

private Stream<Requirement> loadCapabilitiesFrom(File[] requirementDirectories) {
private Stream<Requirement> loadRequirementsFrom(File[] requirementDirectories) {
return Arrays.stream(requirementDirectories).map(this::readRequirementFrom);
}

Expand Down Expand Up @@ -596,9 +596,9 @@ public Requirement readRequirementFrom(File requirementDirectory) {
return requirementWithNarrative(requirementDirectory,
humanReadableVersionOf(requirementDirectory.getName()),
requirementNarrative.get());
} else {
return requirementFromDirectoryName(requirementDirectory);
}

return requirementFromDirectoryName(requirementDirectory);
}

private final Set<File> invalidFeatureFiles = new HashSet<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,39 +111,39 @@ private List<Requirement> loadRequirements() {
requirementsDirectory = outputDirectory;
}
// If no output directory exists (yet), the test run is still in progress, so don't bother reading the requirements yet.
if (requirementsDirectory.exists()) {
List<TestOutcome> outcomes = loader.loadFrom(requirementsDirectory);
if (! requirementsDirectory.exists()) {
return new ArrayList<>();
}

int maxRequirementsDepth = getMaxRequirementsDepthFrom(outcomes);
List<TestOutcome> outcomes = loader.loadFrom(requirementsDirectory);

// Bottom-level requirements
Map<PathElements, Requirement> leafLevelRequirements = getLeafLevelRequirementsFrom(outcomes);
int maxRequirementsDepth = getMaxRequirementsDepthFrom(outcomes);

Set<PathElements> leafPathElements = leafLevelRequirements.keySet();
// Bottom-level requirements
Map<PathElements, Requirement> leafLevelRequirements = getLeafLevelRequirementsFrom(outcomes);

Map<PathElements, Requirement> requirementsByPath = new HashMap<>();
Set<PathElements> leafPathElements = leafLevelRequirements.keySet();

// Non-leaf requirements indexed by path
findPathElementsIn(outcomes).forEach(pathElements -> processPathElements(pathElements, maxRequirementsDepth, leafPathElements, leafLevelRequirements, requirementsByPath));
Map<PathElements, Requirement> requirementsByPath = new HashMap<>();

Collection<Requirement> allRequirements = requirementsByPath.values();
// Non-leaf requirements indexed by path
findPathElementsIn(outcomes).forEach(pathElements -> processPathElements(pathElements, maxRequirementsDepth, leafPathElements, leafLevelRequirements, requirementsByPath));

// Use the map to update the leaf requirements
updateParentFieldsIn(requirementsByPath, allRequirements);
Collection<Requirement> allRequirements = requirementsByPath.values();

// Make an alias for any leaf requirements that also appear in the non-leaf requirements.
// Use the map to update the leaf requirements
updateParentFieldsIn(requirementsByPath, allRequirements);

populateChildren(requirementsByPath, allRequirements);
// Make an alias for any leaf requirements that also appear in the non-leaf requirements.

RequirementCache.getInstance().indexRequirements(requirementsByPath);
populateChildren(requirementsByPath, allRequirements);

// Return a list of the top-level or leaf requirements with no parent elements
return allRequirements.stream()
.filter(requirement -> StringUtils.isEmpty(requirement.getParent()))
.collect(Collectors.toList());
} else {
return new ArrayList<>();
}
RequirementCache.getInstance().indexRequirements(requirementsByPath);

// Return a list of the top-level or leaf requirements with no parent elements
return allRequirements.stream()
.filter(requirement -> StringUtils.isEmpty(requirement.getParent()))
.collect(Collectors.toList());
}

private void processPathElements(PathElements pathElements, int maxRequirementsDepth, Set<PathElements> leafPathElements,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.thucydides.model.requirements;

import net.serenitybdd.model.di.ModelInfrastructure;
import net.thucydides.model.domain.RequirementCache;
import net.thucydides.model.requirements.model.Requirement;
import org.junit.jupiter.api.*;
Expand Down Expand Up @@ -31,6 +32,8 @@ void should_treat_files_in_a_flat_directory_structure_as_representing_features()

List<Requirement> requirements = requirementsFrom(pathTo("serenity-js/spec-flat"));

System.out.println(requirements);

assertThat(requirements).hasSize(1);

Requirement feature = requirements.get(0);
Expand All @@ -45,6 +48,8 @@ void should_treat_files_in_a_single_level_directory_structure_as_representing_ca

List<Requirement> requirements = requirementsFrom(pathTo("serenity-js/spec-1-level"));

System.out.println(requirements);

assertThat(requirements).hasSize(2);

Requirement feature = requirements.get(0);
Expand All @@ -64,6 +69,8 @@ void should_treat_files_in_a_two_level_directory_structure_as_representing_theme

List<Requirement> requirements = requirementsFrom(pathTo("serenity-js/spec-2-levels"));

System.out.println(requirements);

assertThat(requirements).hasSize(3);

Requirement feature = requirements.get(0);
Expand All @@ -85,11 +92,15 @@ void should_treat_files_in_a_two_level_directory_structure_as_representing_theme
}

private List<Requirement> requirementsFrom(Path exampleRootDirectory) {

Path requirementsDirectory = exampleRootDirectory.resolve("spec");
Path jsonOutcomesDirectory = exampleRootDirectory.resolve("outcomes");

final AggregateRequirements aggregateRequirements = new AggregateRequirements(jsonOutcomesDirectory, requirementsDirectory.toString());
final RequirementsService service = aggregateRequirements.getRequirementsService();
final RequirementsService service = new AggregateRequirementsService(
ModelInfrastructure.getEnvironmentVariables(),
new FileSystemRequirementsTagProvider(requirementsDirectory.toString()),
new TestOutcomeRequirementsTagProvider().fromSourceDirectory(jsonOutcomesDirectory)
);

return service.getRequirements();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,110 @@
import static org.junit.jupiter.api.Assertions.*;
class FileSystemRequirmentsOutcomeFactoryTest {

package net.thucydides.model.requirements.reports;

import net.serenitybdd.model.di.ModelInfrastructure;
import net.serenitybdd.model.environment.ConfiguredEnvironment;
import net.thucydides.model.domain.ReportType;
import net.thucydides.model.domain.RequirementCache;
import net.thucydides.model.reports.OutcomeFormat;
import net.thucydides.model.reports.TestOutcomeLoader;
import net.thucydides.model.reports.TestOutcomes;
import net.thucydides.model.reports.html.ReportNameProvider;
import net.thucydides.model.requirements.model.Requirement;
import org.junit.jupiter.api.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;

import static net.thucydides.model.reports.html.ReportNameProvider.NO_CONTEXT;
import static org.assertj.core.api.Assertions.assertThat;

class FileSystemRequirementsOutcomeFactoryTest {

@BeforeEach
void setUp() {
RequirementCache.getInstance().clear();
}

@AfterAll
static void afterAll() {
RequirementCache.getInstance().clear();
}

@Nested
@DisplayName("when interpreting Serenity/JS test outcomes")
class SerenityJSTestOutcomes {

@Test
void should_not_discover_any_additional_requirements_if_the_directory_structure_is_flat() throws IOException {

List<Requirement> requirements = requirementsFrom(pathTo("serenity-js/spec-flat"));

assertThat(requirements).hasSize(0);
}

@Test
void should_treat_directories_in_a_single_level_directory_structure_as_representing_capabilities() throws IOException {

List<Requirement> requirements = requirementsFrom(pathTo("serenity-js/spec-1-level"));

System.out.println(requirements);

assertThat(requirements).hasSize(1);

Requirement capability = requirements.get(0);

assertThat(capability.getType()).isEqualTo("capability");
assertThat(capability.getName()).isEqualTo("payments");
assertThat(capability.getDisplayName()).isEqualTo("Payments");
}

@Test
void should_treat_directories_in_a_two_level_directory_structure_as_representing_themes_and_capabilities() throws IOException {

List<Requirement> requirements = requirementsFrom(pathTo("serenity-js/spec-2-levels"));

System.out.println(requirements);

assertThat(requirements).hasSize(2);

Requirement capability = requirements.get(0);
Requirement theme = requirements.get(1);

assertThat(capability.getType()).isEqualTo("capability");
assertThat(capability.getName()).isEqualTo("payments");
assertThat(capability.getDisplayName()).isEqualTo("Payments");

assertThat(theme.getType()).isEqualTo("theme");
assertThat(theme.getName()).isEqualTo("ecommerce");
assertThat(theme.getDisplayName()).isEqualTo("Ecommerce");
}
}

private List<Requirement> requirementsFrom(Path exampleRootDirectory) throws IOException {

Path requirementsDirectory = exampleRootDirectory.resolve("spec");
Path jsonOutcomesDirectory = exampleRootDirectory.resolve("outcomes");

final FileSystemRequirementsOutcomeFactory requirmentsOutcomeFactory = new FileSystemRequirementsOutcomeFactory(
ConfiguredEnvironment.getEnvironmentVariables(),
ModelInfrastructure.getIssueTracking(),
new ReportNameProvider(
NO_CONTEXT,
ReportType.HTML,
null // requirementsService; not needed in this context
),
requirementsDirectory.toString());

TestOutcomes outcomes = TestOutcomeLoader.loadTestOutcomes()
.inFormat(OutcomeFormat.JSON)
.from(jsonOutcomesDirectory.toFile())
.withRequirementsTags();

return requirmentsOutcomeFactory.buildRequirementsOutcomesFrom(outcomes).getRequirements();
}

private static Path pathTo(String resource) {
return new File(ClassLoader.getSystemClassLoader().getResource(resource).getFile()).toPath();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"id": "card-payment",
"storyName": "Card payment",
"displayName": "Card payment",
"path": "ecommerce/payments/card_payment",
"path": "ecommerce/payments",
"type": "feature",
"narrative": "",
"pathElements": [
Expand All @@ -30,10 +30,6 @@
{
"name": "payments",
"description": ""
},
{
"name": "card_payment",
"description": ""
}
]
},
Expand Down

0 comments on commit 8cf49b6

Please sign in to comment.