Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle incomplete lines #22

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ repositories {
}

// run gradle with "-Dsnapshot=true" to automatically append "-SNAPSHOT" to the version
version = '1.5' + (Boolean.valueOf(System.getProperty("snapshot")) ? "-SNAPSHOT" : "")
version = '2.0' + (Boolean.valueOf(System.getProperty("snapshot")) ? "-SNAPSHOT" : "")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I bumped the major version because the change to how incomplete line markers are parsed is a breaking change: consumers might've handled them in some way already.

sourceCompatibility = 1.8

ext{
Expand All @@ -24,7 +24,7 @@ ext{
dependencies {
compile('org.slf4j:slf4j-api:1.7.25')
testCompile('org.slf4j:slf4j-log4j12:1.7.25')
testCompile('org.testng:testng:6.8.7')
testCompile('org.testng:testng:6.14.3')
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestNG used to pull in JUnit, and test classes were using a mix of assertion methods from both libraries.

This recent version fixed that, hence there're many import changes in the test classes.

}

task sourcesJar(type: Jar, dependsOn: classes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,34 @@ public List<Diff> parse(InputStream in) {
}

private void parseNeutralLine(Diff currentDiff, String currentLine) {
Line line = new Line(Line.LineType.NEUTRAL, currentLine);
// Neutral line should have a space as its first character,
// however not all tools seem to obey this rule for empty lines (see Tortoise diff),
// so let's strip the first character if present.
String content = currentLine.substring(Math.min(1, currentLine.length()));
Line line = new Line(Line.LineType.NEUTRAL, content);
currentDiff.getLatestHunk().getLines().add(line);
}

private void parseToLine(Diff currentDiff, String currentLine) {
Line toLine = new Line(Line.LineType.TO, currentLine.substring(1));
currentDiff.getLatestHunk().getLines().add(toLine);
if (isIncompleteMarker(currentLine)) {
currentDiff.setToFileIncomplete(true);
} else {
Line toLine = new Line(Line.LineType.TO, currentLine.substring(1));
currentDiff.getLatestHunk().getLines().add(toLine);
}
}

private void parseFromLine(Diff currentDiff, String currentLine) {
Line fromLine = new Line(Line.LineType.FROM, currentLine.substring(1));
currentDiff.getLatestHunk().getLines().add(fromLine);
if (isIncompleteMarker(currentLine)) {
currentDiff.setFromFileIncomplete(true);
} else {
Line fromLine = new Line(Line.LineType.FROM, currentLine.substring(1));
currentDiff.getLatestHunk().getLines().add(fromLine);
}
}

private boolean isIncompleteMarker(String line) {
return line.charAt(0) == '\\';
}

private void parseHunkStart(Diff currentDiff, String currentLine) {
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/io/reflectoring/diffparser/api/model/Diff.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public class Diff {

private String toFileName;

private boolean fromFileIncomplete;

private boolean toFileIncomplete;

private List<String> headerLines = new ArrayList<>();

private List<Hunk> hunks = new ArrayList<>();
Expand Down Expand Up @@ -76,6 +80,34 @@ public List<Hunk> getHunks() {
return hunks;
}

/**
* Reflects whether the first file has an incomplete trailing line reported by the diff, i.e. whether
* the first file doesn't end with a newline character.
* <p>
* Note that the diff tools don't stick to a common rule regarding when to report the lack of trailing
* newline character, e.g. some tools report about it only when it differs between the compared files
* while others report always.
*
* @return {@code true} if the diff reported no newline at end of the first file
*/
public boolean isFromFileIncomplete() {
return fromFileIncomplete;
}

/**
* Reflects whether the second file has an incomplete trailing line reported by the diff, i.e. whether
* the second file doesn't end with a newline character.
* <p>
* Note that the diff tools don't stick to a common rule regarding when to report the lack of trailing
* newline character, e.g. some tools report about it only when it differs between the compared files
* while others report always.
*
* @return {@code true} if the diff reported no newline at end of the second file
*/
public boolean isToFileIncomplete() {
return toFileIncomplete;
}

public void setFromFileName(String fromFileName) {
this.fromFileName = fromFileName;
}
Expand All @@ -88,6 +120,14 @@ public void setHunks(List<Hunk> hunks) {
this.hunks = hunks;
}

public void setFromFileIncomplete(boolean fromFileIncomplete) {
this.fromFileIncomplete = fromFileIncomplete;
}

public void setToFileIncomplete(boolean toFileIncomplete) {
this.toFileIncomplete = toFileIncomplete;
}

/**
* Gets the last {@link Hunk} of changes that is part of this Diff.
*
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/io/reflectoring/diffparser/unified/ParserState.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ public ParserState nextState(ParseWindow window) {
@Override
public ParserState nextState(ParseWindow window) {
String line = window.getFocusLine();
if (matchesFromLinePattern(line)) {
if (matchesFromLinePattern(line) || matchesIncompleteLinePattern(line)) {
// Incomplete line always follows the line from the same side of the diff,
// so there's no actual state transition
logTransition(line, FROM_LINE, FROM_LINE);
return FROM_LINE;
} else if (matchesToLinePattern(line)) {
Expand Down Expand Up @@ -166,7 +168,9 @@ public ParserState nextState(ParseWindow window) {
if (matchesFromLinePattern(line)) {
logTransition(line, TO_LINE, FROM_LINE);
return FROM_LINE;
} else if (matchesToLinePattern(line)) {
} else if (matchesToLinePattern(line) || matchesIncompleteLinePattern(line)) {
// Incomplete line always follows the line from the same side of the diff,
// so there's no actual state transition
logTransition(line, TO_LINE, TO_LINE);
return TO_LINE;
} else if (matchesEndPattern(line, window)) {
Expand Down Expand Up @@ -253,6 +257,10 @@ protected boolean matchesToLinePattern(String line) {
return line.startsWith("+");
}

protected boolean matchesIncompleteLinePattern(String line) {
return line.startsWith("\\");
}

protected boolean matchesHunkStartPattern(String line) {
return LINE_RANGE_PATTERN.matcher(line).matches();
}
Expand Down
80 changes: 76 additions & 4 deletions src/test/java/io/reflectoring/diffparser/unified/GitDiffTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@
import io.reflectoring.diffparser.api.model.Hunk;
import io.reflectoring.diffparser.api.model.Line;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;

import org.testng.annotations.Test;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static io.reflectoring.diffparser.unified.TestUtil.assertLine;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import static org.testng.AssertJUnit.assertEquals;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signature of .assertEquals() in TestNG and JUnit differs: they accept "expected" and "actual" values in different order. I didn't want to change flip all the usages in this PR too, so for now used this adapting method.


/**
* Tests the {@link UnifiedDiffParser} with a diff created by the {@code git diff} command.
*/
public class GitDiffTest {

@Test
Expand Down Expand Up @@ -47,7 +54,72 @@ public void testParse() {

List<Line> lines = hunk1.getLines();
assertEquals(8, lines.size());
assertEquals(Line.LineType.FROM, lines.get(3).getLineType());
assertEquals(Line.LineType.TO, lines.get(4).getLineType());
assertLine(lines.get(2), Line.LineType.NEUTRAL, " <artifactId>diffparser</artifactId>");
assertLine(lines.get(3), Line.LineType.FROM, " <version>1.1-SNAPSHOT</version>");
assertLine(lines.get(4), Line.LineType.TO, " <version>1.0</version>");
}

@Test
public void testParse_IncompleteLinesInTheFirstFile() {
// given
DiffParser parser = new UnifiedDiffParser();
String in = "" +
"diff --git a/test.txt b/test.txt\n" +
"index 2e65efe..7898192 100644\n" +
"--- a/test.txt\n" +
"+++ b/test.txt\n" +
"@@ -1 +1 @@\n" +
"-a\n" +
"\\ No newline at end of file\n" +
"+a";

// when
List<Diff> diffs = parser.parse(in.getBytes(StandardCharsets.UTF_8));

// then
assertNotNull(diffs);
assertEquals(1, diffs.size());

Diff diff = diffs.get(0);
assertEquals(1, diff.getHunks().size());

List<Line> lines = diff.getHunks().get(0).getLines();
assertEquals(2, lines.size());
assertLine(lines.get(0), Line.LineType.FROM, "a");
assertLine(lines.get(1), Line.LineType.TO, "a");
assertTrue(diff.isFromFileIncomplete());
assertFalse(diff.isToFileIncomplete());
}

@Test
public void testParse_IncompleteLinesInTheSecondFile() {
// given
DiffParser parser = new UnifiedDiffParser();
String in = "" +
"diff --git a/test.txt b/test.txt\n" +
"index 7898192..2e65efe 100644\n" +
"--- a/test.txt\n" +
"+++ b/test.txt\n" +
"@@ -1 +1 @@\n" +
"-a\n" +
"+a\n" +
"\\ No newline at end of file";

// when
List<Diff> diffs = parser.parse(in.getBytes(StandardCharsets.UTF_8));

// then
assertNotNull(diffs);
assertEquals(1, diffs.size());

Diff diff = diffs.get(0);
assertEquals(1, diff.getHunks().size());

List<Line> lines = diff.getHunks().get(0).getLines();
assertEquals(2, lines.size());
assertLine(lines.get(0), Line.LineType.FROM, "a");
assertLine(lines.get(1), Line.LineType.TO, "a");
assertFalse(diff.isFromFileIncomplete());
assertTrue(diff.isToFileIncomplete());
}
}
64 changes: 34 additions & 30 deletions src/test/java/io/reflectoring/diffparser/unified/SvnDiffTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@

import io.reflectoring.diffparser.api.DiffParser;
import io.reflectoring.diffparser.api.UnifiedDiffParser;
import junit.framework.Assert;
import org.testng.annotations.Test;
import io.reflectoring.diffparser.api.model.Diff;
import io.reflectoring.diffparser.api.model.Hunk;
import io.reflectoring.diffparser.api.model.Line;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;

import static io.reflectoring.diffparser.unified.TestUtil.assertLine;
import static org.testng.Assert.assertNotNull;
import static org.testng.AssertJUnit.assertEquals;

/**
* Tests the DiffParser with a diff created by the "svn diff" command.
* Tests the {@link UnifiedDiffParser} with a diff created by the {@code svn diff} command.
*/
public class SvnDiffTest {

@Test
public void testParse() throws Exception {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed unused declarations along the way, since I touched most of the test classes content anyway.

public void testParse() {
// given
DiffParser parser = new UnifiedDiffParser();
InputStream in = getClass().getResourceAsStream("svn.diff");
Expand All @@ -26,33 +30,33 @@ public void testParse() throws Exception {
List<Diff> diffs = parser.parse(in);

// then
Assert.assertNotNull(diffs);
Assert.assertEquals(2, diffs.size());
assertNotNull(diffs);
assertEquals(2, diffs.size());

Diff diff1 = diffs.get(0);
Assert.assertEquals("UnifiedDiffParser.java", diff1.getFromFileName());
Assert.assertEquals("UnifiedDiffParser.java", diff1.getToFileName());
Assert.assertEquals(1, diff1.getHunks().size());
assertEquals("UnifiedDiffParser.java", diff1.getFromFileName());
assertEquals("UnifiedDiffParser.java", diff1.getToFileName());
assertEquals(1, diff1.getHunks().size());

List<String> headerLines = diff1.getHeaderLines();
Assert.assertEquals(2, headerLines.size());
assertEquals(2, headerLines.size());

Hunk hunk1 = diff1.getHunks().get(0);
Assert.assertEquals(73, hunk1.getFromFileRange().getLineStart());
Assert.assertEquals(13, hunk1.getFromFileRange().getLineCount());
Assert.assertEquals(73, hunk1.getToFileRange().getLineStart());
Assert.assertEquals(13, hunk1.getToFileRange().getLineCount());
assertEquals(73, hunk1.getFromFileRange().getLineStart());
assertEquals(13, hunk1.getFromFileRange().getLineCount());
assertEquals(73, hunk1.getToFileRange().getLineStart());
assertEquals(13, hunk1.getToFileRange().getLineCount());

List<Line> lines = hunk1.getLines();
Assert.assertEquals(16, lines.size());
Assert.assertEquals(Line.LineType.TO, lines.get(3).getLineType());
Assert.assertEquals(Line.LineType.FROM, lines.get(7).getLineType());
Assert.assertEquals(Line.LineType.TO, lines.get(8).getLineType());

assertEquals(16, lines.size());
assertLine(lines.get(2), Line.LineType.NEUTRAL, " case TO_FILE:");
assertLine(lines.get(3), Line.LineType.TO, "\t\t\t\t\tnew line");
assertLine(lines.get(7), Line.LineType.FROM, " parseHunkStart(currentDiff, currentLine);");
assertLine(lines.get(8), Line.LineType.TO, " changedLine(currentDiff, currentLine);");
}

@Test
public void testParse_WhenHunkRangeLineCountNotSpecified_ShouldSetHunkRangeLineCountToOne() throws Exception {
public void testParse_WhenHunkRangeLineCountNotSpecified_ShouldSetHunkRangeLineCountToOne() {
// given
DiffParser parser = new UnifiedDiffParser();
String in = ""
Expand All @@ -64,22 +68,22 @@ public void testParse_WhenHunkRangeLineCountNotSpecified_ShouldSetHunkRangeLineC
+ "\n";

// when
List<Diff> diffs = parser.parse(in.getBytes());
List<Diff> diffs = parser.parse(in.getBytes(StandardCharsets.UTF_8));
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is generally unsafe to call .getBytes() without specifying a charset, so I declared UTF-8 everywhere.


// then
Assert.assertNotNull(diffs);
Assert.assertEquals(1, diffs.size());
assertNotNull(diffs);
assertEquals(1, diffs.size());

Diff diff1 = diffs.get(0);
Assert.assertEquals(1, diff1.getHunks().size());
assertEquals(1, diff1.getHunks().size());

Hunk hunk1 = diff1.getHunks().get(0);
Assert.assertEquals(1, hunk1.getFromFileRange().getLineCount());
Assert.assertEquals(1, hunk1.getToFileRange().getLineCount());
assertEquals(1, hunk1.getFromFileRange().getLineCount());
assertEquals(1, hunk1.getToFileRange().getLineCount());
}

@Test
public void testParse_WhenInputDoesNotEndWithEmptyLine_ShouldTransitionToEndState() throws Exception {
public void testParse_WhenInputDoesNotEndWithEmptyLine_ShouldTransitionToEndState() {
// given
DiffParser parser = new UnifiedDiffParser();
String in = ""
Expand All @@ -90,13 +94,13 @@ public void testParse_WhenInputDoesNotEndWithEmptyLine_ShouldTransitionToEndStat
+ "+to\n";

// when
List<Diff> diffs = parser.parse(in.getBytes());
List<Diff> diffs = parser.parse(in.getBytes(StandardCharsets.UTF_8));

// then
Assert.assertNotNull(diffs);
Assert.assertEquals(1, diffs.size());
assertNotNull(diffs);
assertEquals(1, diffs.size());

Diff diff1 = diffs.get(0);
Assert.assertEquals(1, diff1.getHunks().size());
assertEquals(1, diff1.getHunks().size());
}
}
18 changes: 18 additions & 0 deletions src/test/java/io/reflectoring/diffparser/unified/TestUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.reflectoring.diffparser.unified;

import io.reflectoring.diffparser.api.model.Line;

import static org.testng.Assert.assertEquals;

class TestUtil {

private TestUtil() {
throw new UnsupportedOperationException(getClass().getSimpleName() + " only contains static utility methods " +
"and should not be instantiated.");
}

static void assertLine(Line actualLine, Line.LineType expectedType, String expectedContent) {
assertEquals(expectedType, actualLine.getLineType());
assertEquals(expectedContent, actualLine.getContent());
}
}
Loading