Skip to content

Commit b7b2c52

Browse files
authored
[fix] Fix flaky inconsistency detection (#125)
1 parent 8cc831b commit b7b2c52

File tree

11 files changed

+154
-59
lines changed

11 files changed

+154
-59
lines changed

src/main/java/se/kth/spork/base3dm/TdmMerge.java

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package se.kth.spork.base3dm;
22

3+
import se.kth.spork.exception.ConflictException;
34
import se.kth.spork.util.LazyLogger;
45

56
import java.util.*;
@@ -39,10 +40,8 @@ public static <T extends ListNode,V> void resolveRawMerge(ChangeSet<T,V> base, C
3940
mergeContent(pcs.getSuccessor(), base, delta);
4041

4142
List<Pcs<T>> others = delta.getOtherRoots(pcs);
42-
if (others.isEmpty())
43-
others = delta.getOtherPredecessors(pcs);
44-
if (others.isEmpty())
45-
others = delta.getOtherSuccessors(pcs);
43+
others.addAll(delta.getOtherPredecessors(pcs));
44+
others.addAll(delta.getOtherSuccessors(pcs));
4645

4746
for (Pcs<T> otherPcs : others) {
4847
if (base.contains(otherPcs)) {
@@ -102,7 +101,7 @@ private static <T extends ListNode,V> Set<Content<T,V>> handleContentConflict(Se
102101
} else if (newContent.size() > 2) {
103102
// This should never happen, as there are at most 3 pieces of content to begin with and base has been
104103
// removed.
105-
throw new IllegalStateException("Unexpected amount of conflicting content: " + newContent);
104+
throw new ConflictException("Unexpected amount of conflicting content: " + newContent);
106105
}
107106

108107
if (newContent.size() != 1) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package se.kth.spork.exception;
2+
3+
/**
4+
* Thrown when Spork encounters a conflict that cannot be handled properly.
5+
*
6+
* @author Simon Larsén
7+
*/
8+
public class ConflictException extends RuntimeException {
9+
public ConflictException(String s) {
10+
super(s);
11+
}
12+
public ConflictException(String s, Throwable throwable) {
13+
super(s, throwable);
14+
}
15+
}

src/main/java/se/kth/spork/spoon/pcsinterpreter/ContentMerger.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ private static _ContentTriple getContentRevisions(Set<Content<SpoonNode, RoledVa
291291
}
292292

293293
if (left == null || right == null)
294-
throw new IllegalStateException("Expected at least left and right revisions, got: " + contents);
294+
throw new IllegalArgumentException("Expected at least left and right revisions, got: " + contents);
295295

296296
return new _ContentTriple(Optional.ofNullable(base), left, right);
297297
}

src/main/java/se/kth/spork/spoon/pcsinterpreter/PcsInterpreter.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public static Pair<CtElement, Integer> fromMergedPcs(
2727
SpoonMapping baseLeft,
2828
SpoonMapping baseRight) {
2929
SporkTreeBuilder sporkTreeBuilder = new SporkTreeBuilder(delta);
30-
SporkTree sporkTreeRoot = sporkTreeBuilder.build(NodeFactory.ROOT);
30+
SporkTree sporkTreeRoot = sporkTreeBuilder.buildTree();
3131

3232
// this is a bit of a hack, get any used environment such that the SpoonTreeBuilder can copy environment
3333
// details

src/main/java/se/kth/spork/spoon/pcsinterpreter/SporkTreeBuilder.java

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package se.kth.spork.spoon.pcsinterpreter;
22

33
import se.kth.spork.base3dm.*;
4+
import se.kth.spork.exception.ConflictException;
45
import se.kth.spork.spoon.wrappers.NodeFactory;
56
import se.kth.spork.spoon.wrappers.RoledValues;
67
import se.kth.spork.spoon.wrappers.SpoonNode;
@@ -27,6 +28,10 @@ class SporkTreeBuilder {
2728
// if any node is added twice, there's an unresolved move conflict
2829
private Set<SpoonNode> usedNodes;
2930

31+
// keeps track of all structural inconsistencies that are used
32+
// if any have not been used when the tree has been built, there's something wrong
33+
private Set<Pcs<SpoonNode>> remainingInconsistencies;
34+
3035
/**
3136
* Create a builder.
3237
*
@@ -38,6 +43,8 @@ public SporkTreeBuilder(ChangeSet<SpoonNode, RoledValues> delta) {
3843
contents = delta.getContents();
3944
numStructuralConflicts = 0;
4045
usedNodes = new HashSet<>();
46+
remainingInconsistencies = new HashSet<>();
47+
structuralConflicts.values().forEach(remainingInconsistencies::addAll);
4148
}
4249

4350
private static <T extends ListNode> Map<T, Map<T, Pcs<T>>> buildRootToChildren(Set<Pcs<T>> pcses) {
@@ -75,6 +82,14 @@ public int numStructuralConflicts() {
7582
return numStructuralConflicts;
7683
}
7784

85+
public SporkTree buildTree() {
86+
SporkTree tree = build(NodeFactory.ROOT);
87+
if (!remainingInconsistencies.isEmpty()) {
88+
throw new ConflictException("Unhandled inconsistencies remain: " + remainingInconsistencies);
89+
}
90+
return tree;
91+
}
92+
7893
/**
7994
* Build a subtree of the {@link ChangeSet} contained in this builder. The {@link SporkTree} that's returned
8095
* contains all information required to build the final Spoon tree, including structural conflict information.
@@ -124,6 +139,9 @@ private SpoonNode traverseConflict(
124139
Pcs<SpoonNode> conflicting,
125140
Map<SpoonNode, Pcs<SpoonNode>> children,
126141
SporkTree tree) {
142+
remainingInconsistencies.remove(nextPcs);
143+
remainingInconsistencies.remove(conflicting);
144+
127145
SpoonNode next = nextPcs.getSuccessor();
128146
Arrays.asList(Revision.LEFT, Revision.RIGHT).forEach(tree::addRevision);
129147
Pcs<SpoonNode> leftPcs = nextPcs.getRevision() == Revision.LEFT ? nextPcs : conflicting;
@@ -152,7 +170,7 @@ private SpoonNode traverseConflict(
152170
private void addChild(SporkTree tree, SporkTree child) {
153171
if (usedNodes.contains(child.getNode())) {
154172
// if this happens, then there is a duplicate node in the tree, indicating a move conflict
155-
throw new IllegalStateException("Move conflict detected");
173+
throw new ConflictException("Move conflict detected");
156174
}
157175
tree.addChild(child);
158176
usedNodes.add(child.getNode());
@@ -174,14 +192,15 @@ private List<SpoonNode> extractConflictList(Pcs<SpoonNode> pcs, Map<SpoonNode, P
174192
.filter(confPcs -> StructuralConflict.isPredecessorConflict(finalPcs, confPcs)).findFirst();
175193

176194
if (predConflict.isPresent()) {
195+
remainingInconsistencies.remove(predConflict.get());
177196
return nodes;
178197
}
179198
}
180199

181200
SpoonNode nextNode = pcs.getSuccessor();
182201

183202
if (nextNode.isEndOfList())
184-
throw new IllegalStateException(
203+
throw new ConflictException(
185204
"Reached the end of the child list without finding a predecessor conflict");
186205

187206
nodes.add(nextNode);

src/main/java/se/kth/spork/spoon/printer/PrinterPreprocessor.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package se.kth.spork.spoon.printer;
22

3+
import se.kth.spork.exception.ConflictException;
34
import se.kth.spork.spoon.ContentConflict;
45
import se.kth.spork.spoon.pcsinterpreter.ContentMerger;
56
import se.kth.spork.spoon.wrappers.RoledValue;
@@ -182,7 +183,7 @@ private void processConflict(ContentConflict conflict, CtElement element) {
182183
localPrinterMap.put(leftStr, Pair.of(leftStr, rightStr));
183184
break;
184185
default:
185-
throw new IllegalStateException("Unhandled conflict: " + leftVal + ", " + rightVal);
186+
throw new ConflictException("Unhandled conflict: " + leftVal + ", " + rightVal);
186187
}
187188

188189
if (!localPrinterMap.isEmpty()) {

src/test/java/se/kth/spork/Util.java

+73-49
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.nio.file.Path;
1616
import java.nio.file.Paths;
1717
import java.util.*;
18+
import java.util.function.Function;
1819
import java.util.regex.Matcher;
1920
import java.util.regex.Pattern;
2021
import java.util.stream.Stream;
@@ -29,61 +30,18 @@ public class Util {
2930
public static final Path BOTH_MODIFIED_DIRPATH = CLEAN_MERGE_DIRPATH.resolve("both_modified");
3031
public static final Path LEFT_MODIFIED_DIRPATH = CLEAN_MERGE_DIRPATH.resolve("left_modified");
3132
public static final Path CONFLICT_DIRPATH = Paths.get("src/test/resources/conflict");
33+
public static final Path UNHANDLED_INCONSISTENCY_PATH = Paths.get("src/test/resources/unhandled_inconsistency");
3234

33-
/**
34-
* Provides test sources for scenarios where both left and right revisions are modified.
35-
*/
36-
public static class BothModifiedSourceProvider implements ArgumentsProvider {
37-
@Override
38-
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
39-
return getArgumentSourcesStream(BOTH_MODIFIED_DIRPATH.toFile());
40-
}
41-
}
42-
43-
/**
44-
* Provides test sources for scenarios where left is modified.
45-
*/
46-
public static class LeftModifiedSourceProvider implements ArgumentsProvider {
47-
@Override
48-
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
49-
return getArgumentSourcesStream(LEFT_MODIFIED_DIRPATH.toFile());
50-
}
51-
}
52-
53-
/**
54-
* Provides test sources for scenarios where right is modified.
55-
*/
56-
public static class RightModifiedSourceProvider implements ArgumentsProvider {
57-
@Override
58-
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
59-
return getArgumentSourcesStream(LEFT_MODIFIED_DIRPATH.toFile()).map(
60-
arg -> {
61-
TestSources sources = (TestSources) arg.get()[0];
62-
// swap left and right around to make this a "right modified" test case
63-
Path left = sources.left;
64-
sources.left = sources.right;
65-
sources.right = left;
66-
return Arguments.of(sources);
67-
}
68-
);
69-
}
70-
}
7135

72-
/**
73-
* Provides test sources for scenarios where there are conflicts.
74-
*/
75-
public static class ConflictSourceProvider implements ArgumentsProvider {
76-
@Override
77-
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
78-
return getArgumentSourcesStream(CONFLICT_DIRPATH.toFile());
79-
}
36+
private static Stream<? extends Arguments> getArgumentSourcesStream(File testDir) {
37+
return getArgumentSourcesStream(testDir, TestSources::fromTestDirectory);
8038
}
8139

82-
private static Stream<? extends Arguments> getArgumentSourcesStream(File testDir) {
40+
private static Stream<? extends Arguments> getArgumentSourcesStream(File testDir, Function<File, TestSources> sourceGetter) {
8341
return Arrays.stream(testDir.listFiles())
8442
.filter(File::isDirectory)
8543
.filter(f -> !f.getName().startsWith("IGNORE"))
86-
.map(TestSources::fromTestDirectory)
44+
.map(sourceGetter)
8745
.map(Arguments::of);
8846
}
8947

@@ -142,6 +100,62 @@ public static String keepLeftConflict(String string) {
142100
return leftMarkerMatcher.replaceAll("");
143101
}
144102

103+
/**
104+
* Provides test sources for scenarios where both left and right revisions are modified.
105+
*/
106+
public static class BothModifiedSourceProvider implements ArgumentsProvider {
107+
@Override
108+
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
109+
return getArgumentSourcesStream(BOTH_MODIFIED_DIRPATH.toFile());
110+
}
111+
}
112+
113+
/**
114+
* Provides test sources for scenarios where left is modified.
115+
*/
116+
public static class LeftModifiedSourceProvider implements ArgumentsProvider {
117+
@Override
118+
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
119+
return getArgumentSourcesStream(LEFT_MODIFIED_DIRPATH.toFile());
120+
}
121+
}
122+
123+
/**
124+
* Provides test sources for scenarios where right is modified.
125+
*/
126+
public static class RightModifiedSourceProvider implements ArgumentsProvider {
127+
@Override
128+
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
129+
return getArgumentSourcesStream(LEFT_MODIFIED_DIRPATH.toFile()).map(
130+
arg -> {
131+
TestSources sources = (TestSources) arg.get()[0];
132+
// swap left and right around to make this a "right modified" test case
133+
Path left = sources.left;
134+
sources.left = sources.right;
135+
sources.right = left;
136+
return Arguments.of(sources);
137+
}
138+
);
139+
}
140+
}
141+
142+
public static class UnhandledInconsistencyProvider implements ArgumentsProvider {
143+
@Override
144+
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {
145+
return getArgumentSourcesStream(UNHANDLED_INCONSISTENCY_PATH.toFile(), TestSources::fromTestDirectoryWithoutExpected);
146+
}
147+
}
148+
149+
/**
150+
* Provides test sources for scenarios where there are conflicts.
151+
*/
152+
public static class ConflictSourceProvider implements ArgumentsProvider {
153+
@Override
154+
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
155+
return getArgumentSourcesStream(CONFLICT_DIRPATH.toFile());
156+
}
157+
}
158+
145159
public static class Conflict {
146160
String left;
147161
String right;
@@ -169,7 +183,6 @@ public int hashCode() {
169183
}
170184
}
171185

172-
173186
public static class TestSources {
174187
public Path base;
175188
public Path left;
@@ -194,9 +207,20 @@ public static TestSources fromTestDirectory(File testDir) {
194207
);
195208
}
196209

210+
public static TestSources fromTestDirectoryWithoutExpected(File testDir) {
211+
Path path = testDir.toPath();
212+
return new TestSources(
213+
path.resolve("Base.java"),
214+
path.resolve("Left.java"),
215+
path.resolve("Right.java"),
216+
null
217+
);
218+
}
219+
197220
@Override
198221
public String toString() {
199222
return base.getParent().getFileName().toString();
200223
}
201224
}
225+
202226
}

src/test/java/se/kth/spork/spoon/Spoon3dmMergeTest.java

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.junit.jupiter.params.provider.ArgumentsSource;
55
import se.kth.spork.Util;
66
import se.kth.spork.cli.Cli;
7+
import se.kth.spork.exception.ConflictException;
78
import se.kth.spork.util.Pair;
89
import spoon.reflect.declaration.*;
910

@@ -31,6 +32,15 @@ void mergeToTree_shouldReturnExpectedTree_whenBothVersionsAreModified(Util.TestS
3132
runTestMerge(sources);
3233
}
3334

35+
@ParameterizedTest
36+
@ArgumentsSource(Util.UnhandledInconsistencyProvider.class)
37+
void merge_shouldThrow_onUnhandledInconsistencies(Util.TestSources sources) {
38+
assertThrows(
39+
ConflictException.class,
40+
() -> Spoon3dmMerge.merge(sources.base, sources.left, sources.right)
41+
);
42+
}
43+
3444
private static void runTestMerge(Util.TestSources sources) {
3545
CtModule expected = Parser.parse(sources.expected);
3646
Object expectedImports = expected.getMetadata(Parser.IMPORT_STATEMENTS);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class Main {
2+
public static void main(String[] args) {
3+
if (true) {
4+
}
5+
int a = 2;
6+
if (true) {
7+
}
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class Main {
2+
public static void main(String[] args) {
3+
int a = 2;
4+
if (true) {
5+
}
6+
if (true) {
7+
}
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class Main {
2+
public static void main(String[] args) {
3+
if (true) {
4+
}
5+
if (true) {
6+
}
7+
int a = 2;
8+
}
9+
}

0 commit comments

Comments
 (0)