Package: FileComparisonUtils$Tuple
FileComparisonUtils$Tuple
name | instruction | branch | complexity | line | method | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
FileComparisonUtils.Tuple(String, String) |
|
|
|
|
|
||||||||||||||||||||
canEqual(Object) |
|
|
|
|
|
||||||||||||||||||||
equals(Object) |
|
|
|
|
|
||||||||||||||||||||
getA() |
|
|
|
|
|
||||||||||||||||||||
getB() |
|
|
|
|
|
||||||||||||||||||||
hashCode() |
|
|
|
|
|
||||||||||||||||||||
of(String, String) |
|
|
|
|
|
||||||||||||||||||||
toString() |
|
|
|
|
|
Coverage
1: /*
2: * *********************************************************************************************************************
3: *
4: * TheseFoolishThings: Miscellaneous utilities
5: * http://tidalwave.it/projects/thesefoolishthings
6: *
7: * Copyright (C) 2009 - 2023 by Tidalwave s.a.s. (http://tidalwave.it)
8: *
9: * *********************************************************************************************************************
10: *
11: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
12: * the License. You may obtain a copy of the License at
13: *
14: * http://www.apache.org/licenses/LICENSE-2.0
15: *
16: * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
17: * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
18: * specific language governing permissions and limitations under the License.
19: *
20: * *********************************************************************************************************************
21: *
22: * git clone https://bitbucket.org/tidalwave/thesefoolishthings-src
23: * git clone https://github.com/tidalwave-it/thesefoolishthings-src
24: *
25: * *********************************************************************************************************************
26: */
27: package it.tidalwave.util.test;
28:
29: import javax.annotation.Nonnegative;
30: import javax.annotation.Nonnull;
31: import javax.annotation.Nullable;
32: import java.util.ArrayList;
33: import java.util.List;
34: import java.io.InputStream;
35: import java.io.BufferedReader;
36: import java.io.ByteArrayInputStream;
37: import java.io.File;
38: import java.io.FileInputStream;
39: import java.io.IOException;
40: import java.io.InputStreamReader;
41: import java.nio.file.Path;
42: import com.github.difflib.DiffUtils;
43: import com.github.difflib.patch.AbstractDelta;
44: import com.github.difflib.text.DiffRowGenerator;
45: import lombok.Data;
46: import lombok.RequiredArgsConstructor;
47: import lombok.extern.slf4j.Slf4j;
48: import lombok.experimental.UtilityClass;
49: import static java.util.stream.Collectors.toList;
50: import static java.nio.charset.StandardCharsets.*;
51: import static it.tidalwave.util.Pair.indexedPairStream;
52: import static org.junit.Assert.*;
53:
54: /***********************************************************************************************************************
55: *
56: * A utility class to compare two text files and assert that they have the same contents.
57: *
58: * @author Fabrizio Giudici
59: *
60: **********************************************************************************************************************/
61: @UtilityClass @Slf4j
62: public class FileComparisonUtils
63: {
64:• @Data @RequiredArgsConstructor(staticName = "of")
65: static class Tuple { public final String a, b; }
66:
67: private static final String P_BASE_NAME = FileComparisonUtils.class.getName();
68:
69: public static final String P_TABULAR_OUTPUT = P_BASE_NAME + ".tabularOutput";
70: public static final String P_TABULAR_LIMIT = P_BASE_NAME + ".tabularLimit";
71:
72: private static final boolean TABULAR_OUTPUT = Boolean.getBoolean(P_TABULAR_OUTPUT);
73: private static final int TABULAR_LIMIT = Integer.getInteger(P_TABULAR_LIMIT, 500);
74: private static final String TF = "TEST FAILED";
75:
76: /*******************************************************************************************************************
77: *
78: * Asserts that two files have the same contents.
79: *
80: * @param expectedFile the file with the expected contents
81: * @param actualFile the file with the contents to probe
82: * @throws IOException in case of error
83: *
84: ******************************************************************************************************************/
85: public static void assertSameContents (@Nonnull final Path expectedFile, @Nonnull final Path actualFile)
86: throws IOException
87: {
88: assertSameContents(expectedFile.toFile(), actualFile.toFile());
89: }
90:
91: /*******************************************************************************************************************
92: *
93: * Asserts that two files have the same contents.
94: *
95: * @param expectedFile the file with the expected contents
96: * @param actualFile the file with the contents to probe
97: * @throws IOException in case of error
98: *
99: ******************************************************************************************************************/
100: public static void assertSameContents (@Nonnull final File expectedFile, @Nonnull final File actualFile)
101: throws IOException
102: {
103: final String expectedPath = expectedFile.getAbsolutePath();
104: final String actualPath = actualFile.getAbsolutePath();
105: log.info("******** Comparing files:");
106: logPaths(expectedPath, actualPath, "");
107: assertSameContents(fileToStrings(expectedFile), fileToStrings(actualFile), expectedPath, actualPath);
108: }
109:
110: /*******************************************************************************************************************
111: *
112: * Asserts that two collections of strings have the same contents.
113: *
114: * @param expected the expected values
115: * @param actual the actual values
116: *
117: ******************************************************************************************************************/
118: public static void assertSameContents (@Nonnull final List<String> expected, @Nonnull final List<String> actual)
119: {
120: assertSameContents(expected, actual, null, null);
121: }
122:
123: /*******************************************************************************************************************
124: *
125: * Converts a string which contains newlines into a list of strings.
126: *
127: * @param string the source
128: * @return the strings
129: * @throws IOException in case of error
130: *
131: ******************************************************************************************************************/
132: @Nonnull
133: public static List<String> stringToStrings (@Nonnull final String string)
134: throws IOException
135: {
136: //return Arrays.asList(string.split("\n"));
137: return fileToStrings(new ByteArrayInputStream(string.getBytes(UTF_8)));
138: }
139:
140: /*******************************************************************************************************************
141: *
142: * Reads a file into a list of strings.
143: *
144: * @param file the file
145: * @return the strings
146: * @throws IOException in case of error
147: *
148: ******************************************************************************************************************/
149: @Nonnull
150: public static List<String> fileToStrings (@Nonnull final File file)
151: throws IOException
152: {
153: return fileToStrings(new FileInputStream(file));
154: }
155:
156: /*******************************************************************************************************************
157: *
158: * Reads a classpath resource into a list of strings.
159: *
160: * @param path the path of the classpath resource
161: * @return the strings
162: * @throws IOException in case of error
163: *
164: ******************************************************************************************************************/
165: @Nonnull
166: public static List<String> fileToStrings (@Nonnull final String path)
167: throws IOException
168: {
169: final InputStream is = FileComparisonUtils.class.getClassLoader().getResourceAsStream(path);
170:
171: if (is == null)
172: {
173: throw new RuntimeException("Resource not found: " + path);
174: }
175:
176: return fileToStrings(is);
177: }
178:
179: /*******************************************************************************************************************
180: *
181: * Reads an input stream into a list of strings.
182: *
183: * @param is the input stream
184: * @return the strings
185: * @throws IOException in case of error
186: *
187: ******************************************************************************************************************/
188: @Nonnull
189: public static List<String> fileToStrings (@Nonnull final InputStream is)
190: throws IOException
191: {
192: try (final BufferedReader br = new BufferedReader(new InputStreamReader(is, UTF_8)))
193: {
194: final List<String> result = new ArrayList<>();
195:
196: for (;;)
197: {
198: final String s = br.readLine();
199:
200: if (s == null)
201: {
202: break;
203: }
204:
205: result.add(s);
206: }
207:
208: return result;
209: }
210: }
211:
212: /*******************************************************************************************************************
213: *
214: * Given a string that represents a path whose segments are separated by the standard separator of the platform,
215: * returns the common prefix - which means the common directory parents.
216: *
217: * @param s1 the former string
218: * @param s2 the latter string
219: * @return the common prefix
220: *
221: ******************************************************************************************************************/
222: @Nonnull
223: public static String commonPrefix (@Nonnull final String s1, @Nonnull final String s2)
224: {
225: final int min = Math.min(s1.length(), s2.length());
226: int latestSeenSlash = 0;
227:
228: for (int i = 0; i < min; i++)
229: {
230: if (s1.charAt(i) != s2.charAt(i))
231: {
232: return (i == 0) ? "" : s1.substring(0, Math.min(latestSeenSlash + 1, min));
233: }
234: else
235: {
236: if (s1.charAt(i) == File.separatorChar)
237: {
238: latestSeenSlash = i;
239: }
240: }
241: }
242:
243: return s1.substring(0, min);
244: }
245:
246: /*******************************************************************************************************************
247: *
248: * Asserts that two collections of strings have the same contents.
249: *
250: * @param expected the expected values
251: * @param actual the actual values
252: * @param expectedPath an optional path for expected values
253: * @param actualPath an optional path for actual values
254: *
255: ******************************************************************************************************************/
256: private static void assertSameContents (@Nonnull final List<String> expected,
257: @Nonnull final List<String> actual,
258: @Nullable final String expectedPath,
259: @Nullable final String actualPath)
260: {
261: final List<AbstractDelta<String>> deltas = DiffUtils.diff(expected, actual).getDeltas();
262:
263: if (!deltas.isEmpty())
264: {
265: if ((expectedPath != null) && (actualPath != null))
266: {
267: logPaths(expectedPath, actualPath, "TEST FAILED ");
268: }
269:
270: final List<String> strings = toStrings(deltas);
271: strings.forEach(log::error);
272:
273: if (!TABULAR_OUTPUT)
274: {
275: log.error("{} You can set -D{}=true for tabular output; -D{}=<num> to set max table size",
276: TF, P_TABULAR_OUTPUT, P_TABULAR_LIMIT);
277: }
278: else
279: {
280: final DiffRowGenerator generator = DiffRowGenerator.create()
281: .showInlineDiffs(false)
282: .inlineDiffByWord(true)
283: .lineNormalizer(l -> l)
284: .build();
285: final List<Tuple> tuples = generator.generateDiffRows(expected, actual)
286: .stream()
287: .filter(row -> !row.getNewLine().equals(row.getOldLine()))
288: .map(row -> Tuple.of(row.getOldLine().trim(), row.getNewLine().trim()))
289: .limit(TABULAR_LIMIT)
290: .collect(toList());
291:
292: final int padA = tuples.stream().mapToInt(p -> p.a.length()).max().getAsInt();
293: final int padB = tuples.stream().mapToInt(p -> p.b.length()).max().getAsInt();
294: log.error("{} Tabular text is trimmed; row limit set to -D{}={}",
295: TF, P_TABULAR_LIMIT, TABULAR_LIMIT);
296: log.error("{} |-{}-+-{}-|", TF, pad("--------", padA, '-'), pad("--------", padB, '-'));
297: log.error("{} | {} | {} |", TF, pad("expected", padA, ' '), pad("actual ", padB, ' '));
298: log.error("{} |-{}-+-{}-|", TF, pad("--------", padA, '-'), pad("--------", padB, '-'));
299: tuples.forEach(p -> log.error("{} | {} | {} |", TF, pad(p.a, padA, ' '), pad(p.b, padB,' ')));
300: log.error("{} |-{}-+-{}-|", TF, pad("--------", padA, '-'), pad("--------", padB, '-'));
301: }
302:
303: strings.add(0, "Unexpected contents: see log above (you can grep '" + TF + "')");
304: fail(String.join(System.lineSeparator(), strings));
305: }
306: }
307:
308: /*******************************************************************************************************************
309: *
310: * Converts deltas to output as a list of strings.
311: *
312: * @param deltas the deltas
313: * @return the strings
314: *
315: ******************************************************************************************************************/
316: @Nonnull
317: private static List<String> toStrings (@Nonnull final Iterable<AbstractDelta<String>> deltas)
318: {
319: final List<String> strings = new ArrayList<>();
320:
321: deltas.forEach(delta ->
322: {
323: final List<String> sourceLines = delta.getSource().getLines();
324: final List<String> targetLines = delta.getTarget().getLines();
325: final int sourcePosition = delta.getSource().getPosition() + 1;
326: final int targetPosition = delta.getTarget().getPosition() + 1;
327:
328: switch (delta.getType())
329: {
330: case CHANGE:
331: indexedPairStream(sourceLines).forEach(p -> strings.add(
332: String.format("%s exp[%d] *%s*", TF, sourcePosition + p.a, p.b)));
333: indexedPairStream(targetLines).forEach(p -> strings.add(
334: String.format("%s act[%d] *%s*", TF, targetPosition + p.a, p.b)));
335: break;
336:
337: case DELETE:
338: indexedPairStream(sourceLines).forEach(p -> strings.add(
339: String.format("%s -act[%d] *%s*", TF, sourcePosition + p.a, p.b)));
340: break;
341:
342: case INSERT:
343: indexedPairStream(targetLines).forEach(p -> strings.add(
344: String.format("%s +act[%d] *%s*", TF, targetPosition + p.a, p.b)));
345: break;
346:
347: default:
348: }
349: });
350:
351: return strings;
352: }
353:
354: /*******************************************************************************************************************
355: *
356: * Logs info about file comparison paths.
357: *
358: * @param expectedPath the expected path
359: * @param actualPath the actual path
360: * @param prefix a log prefix
361: *
362: ******************************************************************************************************************/
363: private static void logPaths (@Nonnull final String expectedPath,
364: @Nonnull final String actualPath,
365: @Nonnull final String prefix)
366: {
367: final String commonPath = commonPrefix(expectedPath, actualPath);
368: log.info("{}>>>> path is: {}", prefix, commonPath);
369: log.info("{}>>>> exp is: {}", prefix, expectedPath.substring(commonPath.length()));
370: log.info("{}>>>> act is: {}", prefix, actualPath.substring(commonPath.length()));
371: }
372:
373: /*******************************************************************************************************************
374: *
375: * Pads a string to left to fit the given width.
376: *
377: * @param string the string
378: * @param width the width
379: * @return the padded string
380: *
381: ******************************************************************************************************************/
382: @Nonnull
383: private static String pad (@Nonnull final String string, @Nonnegative final int width, final char padding)
384: {
385: return String.format("%-" + width + "s", string).replace(' ', padding);
386: }
387: }