001package org.conqat.lib.commons.test; 002 003import static org.assertj.core.api.Assertions.assertThat; 004 005import java.util.ArrayList; 006import java.util.Arrays; 007import java.util.Comparator; 008import java.util.List; 009 010import org.conqat.lib.commons.collections.CollectionUtils; 011import org.conqat.lib.commons.filesystem.FileSystemUtils; 012import org.conqat.lib.commons.resources.Resource; 013import org.conqat.lib.commons.string.StringUtils; 014import org.conqat.lib.commons.tree.Trie; 015 016/** 017 * Represents a generic collection of test resource files that logically belong 018 * together. A container holds the resources that belong to a single test. It 019 * consists of a single {@link #expectedResource} that holds the expected output 020 * of the test case and test resources that are considered to belong to the 021 * container by some naming heuristics. 022 * 023 * 024 * A container is formed around an expected file (default suffix is ".expected" 025 * if not stated differently in 026 * {@link ExpectedResourceSource#expectedSuffix()}). All files that have a 027 * longer common path prefix with expected file X than with any other expected 028 * file, belong to the test container of X. 029 * 030 * The "primary test resource" is the file exactly matching the expected file 031 * name without extension. 032 * 033 * Example Setups: 034 * 035 * <pre> 036 * folder/A.java <- container "folder/A.java", primary resource 037 * folder/A.java.expected <- container "folder/A.java", expected resource 038 * folder/B.cpp <- container "folder/B.cpp", primary resource 039 * folder/BHeader.h <- container "folder/B.cpp" 040 * folder/B.cpp.expected <- container "folder/B.cpp", expected resource 041 * folder/subfolder/B.metadata <- container "folder/subfolder/B.java" 042 * folder/subfolder/B.java <- container "folder/subfolder/B.java", primary resource 043 * folder/subfolder/B.java.expected <- container "folder/subfolder/B.java", expected resource 044 * </pre> 045 * 046 * @see ExpectedResourceSource 047 * @see #of(List, String, String) 048 */ 049public class ExpectedDataContainer { 050 051 /** 052 * The name shown as test name when passing the {@link ExpectedDataContainer} 053 * into a parameterized test. Matches the {@link #expectedResource}'s name 054 * without the path prefix and expectedSuffix as those are shared by all 055 * containers and therefore don't add any additional information and make the 056 * tests harder to read. 057 */ 058 private final String representativeName; 059 060 /** 061 * Resource that hold the expected test result and also acts as an anchor point 062 * for test resources that are considered to belong together. 063 */ 064 private final Resource expectedResource; 065 066 /** List of resources that belong to the {@link #expectedResource}. */ 067 private final List<Resource> testResources = new ArrayList<>(); 068 069 private ExpectedDataContainer(Resource expectedResource, String representativeName) { 070 this.expectedResource = expectedResource; 071 this.representativeName = representativeName; 072 } 073 074 /** Returns the expected resource of the container. */ 075 public Resource getExpectedResource() { 076 return expectedResource; 077 } 078 079 /** 080 * Returns all test resource that belong to the container (not including the 081 * expected resource). 082 */ 083 public List<Resource> getTestResources() { 084 return testResources; 085 } 086 087 /** 088 * Returns the primary test resource, which is the only resource whose name 089 * matches the expected resource without the suffix. e.g. for 090 * "Test.java.expected" "Test.java" would be the primary test resource. 091 */ 092 public Resource getPrimaryTestResource() { 093 String baseName = StringUtils.getLastPart(representativeName, FileSystemUtils.UNIX_SEPARATOR); 094 List<Resource> primaryResource = CollectionUtils.filter(testResources, 095 resource -> baseName.equals(resource.getName())); 096 assertThat(primaryResource) 097 .as("No or multiple primary resource(s) found for " + expectedResource + ". Got " + testResources) 098 .hasSize(1); 099 return CollectionUtils.getAny(primaryResource); 100 } 101 102 /** 103 * Returns a single test resource. If suffixes are given only a resource that 104 * matches one of them is returned. The method asserts that the selection is 105 * unambiguous. 106 */ 107 public Resource getSingleTestResource(String... suffixes) { 108 List<Resource> matches; 109 if (suffixes.length == 0) { 110 matches = testResources; 111 } else { 112 matches = CollectionUtils.filter(testResources, 113 resource -> Arrays.stream(suffixes).anyMatch(suffix -> resource.getName().endsWith(suffix))); 114 } 115 assertThat(matches).as("Expected single test file for " + expectedResource + " with suffix " 116 + Arrays.toString(suffixes) + ". Found " + matches + "!").hasSize(1); 117 return matches.stream().findFirst().orElse(null); 118 } 119 120 /** Used as test case name. */ 121 @Override 122 public String toString() { 123 return representativeName; 124 } 125 126 /** 127 * Builds a list of {@link ExpectedDataContainer}s out of the given resources. 128 * Resources are grouped by the given expectedSuffix. Files that either start 129 * with the same resource name, the same basename (excluding any file extensions 130 * and suffix numbers) or the same folder (including subfolders) as the 131 * #expectedFile are considered. 132 * 133 * @param path 134 * Is the subpath that was used to search for resources. It is 135 * stripped from the representativeName to make it more readable. 136 */ 137 public static List<ExpectedDataContainer> of(List<Resource> resources, String path, String expectedSuffix) { 138 List<Resource> expectedFiles = CollectionUtils.filter(resources, 139 resource -> resource.getName().endsWith(expectedSuffix)); 140 expectedFiles.sort(Comparator.comparing(Resource::getPath)); 141 resources.removeAll(expectedFiles); 142 List<ExpectedDataContainer> expectedDataContainers = CollectionUtils.map(expectedFiles, expectedFile -> { 143 String representativeName = getRepresentativeName(path, expectedSuffix, expectedFile); 144 return new ExpectedDataContainer(expectedFile, representativeName); 145 }); 146 147 Trie<ExpectedDataContainer> prefixToContainerMapping = buildPrefixToContainerTrie(expectedSuffix, 148 expectedDataContainers); 149 150 addResourcesToContainers(resources, prefixToContainerMapping); 151 return expectedDataContainers; 152 } 153 154 /** 155 * Returns the representative name of the container. This is shown as test name 156 * during execution. 157 */ 158 private static String getRepresentativeName(String path, String expectedSuffix, Resource expectedFile) { 159 String representativeName = StringUtils.stripPrefix(expectedFile.getPath(), path); 160 representativeName = StringUtils.stripPrefix(representativeName, 161 String.valueOf(FileSystemUtils.UNIX_SEPARATOR)); 162 representativeName = StringUtils.stripSuffix(representativeName, expectedSuffix); 163 representativeName = StringUtils.stripSuffix(representativeName, 164 String.valueOf(FileSystemUtils.UNIX_SEPARATOR)); 165 return representativeName; 166 } 167 168 private static void addResourcesToContainers(List<Resource> allResources, 169 Trie<ExpectedDataContainer> prefixToContainerMapping) { 170 for (Resource testDataResource : allResources) { 171 ExpectedDataContainer container = prefixToContainerMapping.getLongestPrefix(testDataResource.getPath()); 172 if (container == null) { 173 if (!testDataResource.hasExtension("class")) { 174 System.err.println("No expected container for resource " + testDataResource.getPath()); 175 } 176 } else { 177 container.testResources.add(testDataResource); 178 } 179 } 180 } 181 182 /** 183 * Builds a Trie holding three prefix variants for each expected file: 184 * <li>The path and name of the expected file without the expectedSuffix 185 * <li>The path and name of the expected file without the expectedSuffix, 186 * preceding file extensions and numeric suffixes 187 * <li>The parent path only 188 */ 189 private static Trie<ExpectedDataContainer> buildPrefixToContainerTrie(String expectedSuffix, 190 List<ExpectedDataContainer> expectedDataContainers) { 191 Trie<ExpectedDataContainer> prefixToContainerMapping = new Trie<>(); 192 for (ExpectedDataContainer expectedDataContainer : expectedDataContainers) { 193 Resource expectedResource = expectedDataContainer.expectedResource; 194 String expectedBasePath = StringUtils.stripSuffix(expectedResource.getPath(), expectedSuffix); 195 prefixToContainerMapping.put(expectedBasePath, expectedDataContainer); 196 prefixToContainerMapping.put(getBaseNameWithoutNumericSuffix(expectedBasePath), expectedDataContainer); 197 prefixToContainerMapping.put(expectedResource.getParentPath(), expectedDataContainer); 198 } 199 return prefixToContainerMapping; 200 } 201 202 private static String getBaseNameWithoutNumericSuffix(String filePathName) { 203 return filePathName.replaceAll("\\d*(\\.\\w+)?$", StringUtils.EMPTY_STRING); 204 } 205}