mirror of
https://github.com/google/nomulus.git
synced 2025-06-21 20:00:46 +02:00
Use TextDiffSubject to compare multi-line text (#406)
* Use TextDiffSubject to compare multi-line text It illustrates differences better. Moved TextDiffSubject.java to the common project for sharing.
This commit is contained in:
parent
3690a2b7ce
commit
b6daafd341
23 changed files with 104 additions and 20 deletions
|
@ -15,7 +15,7 @@
|
|||
package google.registry.sql.flyway;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.TextDiffSubject.assertThat;
|
||||
import static google.registry.testing.truth.TextDiffSubject.assertThat;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.io.Resources;
|
||||
|
|
|
@ -1,200 +0,0 @@
|
|||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.truth.Truth.assertAbout;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.github.difflib.DiffUtils;
|
||||
import com.github.difflib.UnifiedDiffUtils;
|
||||
import com.github.difflib.algorithm.DiffException;
|
||||
import com.github.difflib.patch.Patch;
|
||||
import com.github.difflib.text.DiffRow;
|
||||
import com.github.difflib.text.DiffRowGenerator;
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.common.truth.Fact;
|
||||
import com.google.common.truth.FailureMetadata;
|
||||
import com.google.common.truth.Subject;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Compares two multi-line text blocks, and displays their diffs in readable formats.
|
||||
*
|
||||
* <p>User may choose one of the following diff formats:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link DiffFormat#UNIFIED_DIFF} displays the differences in the unified-diff format
|
||||
* <li>{@link DiffFormat#SIDE_BY_SIDE_MARKDOWN} displays the two text blocks side by side, with
|
||||
* markdown annotations to highlight the differences.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note that if one text block has one trailing newline at the end while another has none, this
|
||||
* difference will not be shown in the generated diffs. This is the case where two texts may be
|
||||
* reported as unequal but the diffs appear equal. Fixing this requires special treatment of the
|
||||
* last line of text. The fix would not be useful in our environment, where all important files are
|
||||
* covered by a style checker that ensures the presence of a trailing newline.
|
||||
*/
|
||||
// TODO(weiminyu): move this class and test to a standalone 'testing' project. Note that the util
|
||||
// project is not good since it depends back to core.
|
||||
public class TextDiffSubject extends Subject {
|
||||
|
||||
private final ImmutableList<String> actual;
|
||||
private DiffFormat diffFormat = DiffFormat.SIDE_BY_SIDE_MARKDOWN;
|
||||
|
||||
protected TextDiffSubject(FailureMetadata metadata, List<String> actual) {
|
||||
super(metadata, actual);
|
||||
this.actual = ImmutableList.copyOf(actual);
|
||||
}
|
||||
|
||||
public TextDiffSubject withDiffFormat(DiffFormat format) {
|
||||
this.diffFormat = format;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void hasSameContentAs(List<String> expectedContent) {
|
||||
checkNotNull(expectedContent, "expectedContent");
|
||||
ImmutableList<String> expected = ImmutableList.copyOf(expectedContent);
|
||||
if (expected.equals(actual)) {
|
||||
return;
|
||||
}
|
||||
String diffString = diffFormat.generateDiff(expected, actual);
|
||||
failWithoutActual(
|
||||
Fact.simpleFact(
|
||||
Joiner.on('\n')
|
||||
.join(
|
||||
"Files differ in content. Displaying " + Ascii.toLowerCase(diffFormat.name()),
|
||||
diffString)));
|
||||
}
|
||||
|
||||
public void hasSameContentAs(URL resourceUrl) throws IOException {
|
||||
hasSameContentAs(Resources.asCharSource(resourceUrl, UTF_8).readLines());
|
||||
}
|
||||
|
||||
public static TextDiffSubject assertThat(List<String> actual) {
|
||||
return assertAbout(textFactory()).that(ImmutableList.copyOf(checkNotNull(actual, "actual")));
|
||||
}
|
||||
|
||||
public static TextDiffSubject assertThat(URL resourceUrl) throws IOException {
|
||||
return assertThat(Resources.asCharSource(resourceUrl, UTF_8).readLines());
|
||||
}
|
||||
|
||||
private static final Subject.Factory<TextDiffSubject, ImmutableList<String>>
|
||||
TEXT_DIFF_SUBJECT_TEXT_FACTORY = TextDiffSubject::new;
|
||||
|
||||
public static Subject.Factory<TextDiffSubject, ImmutableList<String>> textFactory() {
|
||||
return TEXT_DIFF_SUBJECT_TEXT_FACTORY;
|
||||
}
|
||||
|
||||
static String generateUnifiedDiff(
|
||||
ImmutableList<String> expectedContent, ImmutableList<String> actualContent) {
|
||||
Patch<String> diff;
|
||||
try {
|
||||
diff = DiffUtils.diff(expectedContent, actualContent);
|
||||
} catch (DiffException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
List<String> unifiedDiff =
|
||||
UnifiedDiffUtils.generateUnifiedDiff("expected", "actual", expectedContent, diff, 0);
|
||||
|
||||
return Joiner.on('\n').join(unifiedDiff);
|
||||
}
|
||||
|
||||
static String generateSideBySideDiff(
|
||||
ImmutableList<String> expectedContent, ImmutableList<String> actualContent) {
|
||||
DiffRowGenerator generator =
|
||||
DiffRowGenerator.create()
|
||||
.showInlineDiffs(true)
|
||||
.inlineDiffByWord(true)
|
||||
.oldTag(f -> "~")
|
||||
.newTag(f -> "**")
|
||||
.build();
|
||||
List<DiffRow> rows;
|
||||
try {
|
||||
rows = generator.generateDiffRows(expectedContent, actualContent);
|
||||
} catch (DiffException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
int maxExpectedLineLength =
|
||||
findMaxLineLength(rows.stream().map(DiffRow::getOldLine).collect(Collectors.toList()));
|
||||
int maxActualLineLength =
|
||||
findMaxLineLength(rows.stream().map(DiffRow::getNewLine).collect(Collectors.toList()));
|
||||
|
||||
SideBySideRowFormatter sideBySideRowFormatter =
|
||||
new SideBySideRowFormatter(maxExpectedLineLength, maxActualLineLength);
|
||||
|
||||
return Joiner.on('\n')
|
||||
.join(
|
||||
sideBySideRowFormatter.formatRow("Expected", "Actual", ' '),
|
||||
sideBySideRowFormatter.formatRow("", "", '-'),
|
||||
rows.stream()
|
||||
.map(
|
||||
row ->
|
||||
sideBySideRowFormatter.formatRow(row.getOldLine(), row.getNewLine(), ' '))
|
||||
.toArray());
|
||||
}
|
||||
|
||||
private static int findMaxLineLength(Collection<String> lines) {
|
||||
return lines.stream()
|
||||
.max(Comparator.comparingInt(String::length))
|
||||
.map(String::length)
|
||||
.orElse(0);
|
||||
}
|
||||
|
||||
private static class SideBySideRowFormatter {
|
||||
private final int maxExpectedLineLength;
|
||||
private final int maxActualLineLength;
|
||||
|
||||
private SideBySideRowFormatter(int maxExpectedLineLength, int maxActualLineLength) {
|
||||
this.maxExpectedLineLength = maxExpectedLineLength;
|
||||
this.maxActualLineLength = maxActualLineLength;
|
||||
}
|
||||
|
||||
public String formatRow(String expected, String actual, char padChar) {
|
||||
return String.format(
|
||||
"|%s|%s|",
|
||||
Strings.padEnd(expected, maxExpectedLineLength, padChar),
|
||||
Strings.padEnd(actual, maxActualLineLength, padChar));
|
||||
}
|
||||
}
|
||||
|
||||
/** The format used to display diffs when two text blocks are different. */
|
||||
public enum DiffFormat {
|
||||
UNIFIED_DIFF {
|
||||
@Override
|
||||
String generateDiff(ImmutableList<String> expected, ImmutableList<String> actual) {
|
||||
return generateUnifiedDiff(expected, actual);
|
||||
}
|
||||
},
|
||||
SIDE_BY_SIDE_MARKDOWN {
|
||||
@Override
|
||||
String generateDiff(ImmutableList<String> expected, ImmutableList<String> actual) {
|
||||
return generateSideBySideDiff(expected, actual);
|
||||
}
|
||||
};
|
||||
|
||||
abstract String generateDiff(ImmutableList<String> expected, ImmutableList<String> actual);
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.io.Resources.getResource;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.testing.TextDiffSubject.assertThat;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.Resources;
|
||||
import google.registry.testing.TextDiffSubject.DiffFormat;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link TextDiffSubject}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class TextDiffSubjectTest {
|
||||
|
||||
// Resources for input data.
|
||||
private static final String ACTUAL_RESOURCE = "google/registry/testing/text-diff-actual.txt";
|
||||
private static final String EXPECTED_RESOURCE = "google/registry/testing/text-diff-expected.txt";
|
||||
|
||||
// Resources for expected diff texts.
|
||||
private static final String UNIFIED_DIFF_RESOURCE =
|
||||
"google/registry/testing/text-unified-diff.txt";
|
||||
private static final String SIDE_BY_SIDE_DIFF_RESOURCE =
|
||||
"google/registry/testing/text-sidebyside-diff.txt";
|
||||
|
||||
@Test
|
||||
public void unifiedDiff_equal() throws IOException {
|
||||
assertThat(getResource(ACTUAL_RESOURCE))
|
||||
.withDiffFormat(DiffFormat.UNIFIED_DIFF)
|
||||
.hasSameContentAs(getResource(ACTUAL_RESOURCE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sideBySideDiff_equal() throws IOException {
|
||||
assertThat(getResource(ACTUAL_RESOURCE))
|
||||
.withDiffFormat(DiffFormat.SIDE_BY_SIDE_MARKDOWN)
|
||||
.hasSameContentAs(getResource(ACTUAL_RESOURCE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unifedDiff_notEqual() throws IOException {
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertThat(getResource(ACTUAL_RESOURCE))
|
||||
.withDiffFormat(DiffFormat.UNIFIED_DIFF)
|
||||
.hasSameContentAs(getResource(EXPECTED_RESOURCE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sideBySideDiff_notEqual() throws IOException {
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertThat(getResource(ACTUAL_RESOURCE))
|
||||
.withDiffFormat(DiffFormat.SIDE_BY_SIDE_MARKDOWN)
|
||||
.hasSameContentAs(getResource(EXPECTED_RESOURCE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void displayed_unifiedDiff_noDiff() throws IOException {
|
||||
ImmutableList<String> actual = readAllLinesFromResource(ACTUAL_RESOURCE);
|
||||
assertThat(TextDiffSubject.generateUnifiedDiff(actual, actual)).isEqualTo("");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void displayed_unifiedDiff_hasDiff() throws IOException {
|
||||
ImmutableList<String> actual = readAllLinesFromResource(ACTUAL_RESOURCE);
|
||||
ImmutableList<String> expected = readAllLinesFromResource(EXPECTED_RESOURCE);
|
||||
String diff = Joiner.on('\n').join(readAllLinesFromResource(UNIFIED_DIFF_RESOURCE));
|
||||
assertThat(TextDiffSubject.generateUnifiedDiff(expected, actual)).isEqualTo(diff);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void displayed_sideBySideDiff_hasDiff() throws IOException {
|
||||
ImmutableList<String> actual = readAllLinesFromResource(ACTUAL_RESOURCE);
|
||||
ImmutableList<String> expected = readAllLinesFromResource(EXPECTED_RESOURCE);
|
||||
String diff = Joiner.on('\n').join(readAllLinesFromResource(SIDE_BY_SIDE_DIFF_RESOURCE));
|
||||
assertThat(TextDiffSubject.generateSideBySideDiff(expected, actual)).isEqualTo(diff);
|
||||
}
|
||||
|
||||
private static ImmutableList<String> readAllLinesFromResource(String resourceName)
|
||||
throws IOException {
|
||||
return ImmutableList.copyOf(
|
||||
Resources.readLines(getResource(resourceName), StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
This is a random file,
|
||||
with two lines and terminates with a newline.
|
|
@ -1,3 +0,0 @@
|
|||
This is a random file,
|
||||
|
||||
with three lines and terminates without a newline.
|
|
@ -1,5 +0,0 @@
|
|||
|Expected |Actual |
|
||||
|------------------------------------------------------|-----------------------------------------------------|
|
||||
|This is a random file, |This is a random file, |
|
||||
| |with **two** lines and terminates **with** a newline.|
|
||||
|with ~three~ lines and terminates ~without~ a newline.| |
|
|
@ -1,6 +0,0 @@
|
|||
--- expected
|
||||
+++ actual
|
||||
@@ -2,2 +2,1 @@
|
||||
-
|
||||
-with three lines and terminates without a newline.
|
||||
+with two lines and terminates with a newline.
|
Loading…
Add table
Add a link
Reference in a new issue