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}