diff --git a/pom.xml b/pom.xml
index c2a3112bd..32c9dd2b6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,17 @@
                 </configuration>
             </plugin>
 
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>3.0.0-M7</version>
+                <configuration>
+                    <includes>
+                        <include>**/*.java</include>
+                    </includes>
+                </configuration>
+            </plugin>
+
             <!-- To build jar with dependencies-->
             <plugin>
                 <artifactId>maven-assembly-plugin</artifactId>
@@ -122,10 +133,16 @@
         </dependency>
 
         <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <version>4.13.2</version>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>5.9.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <version>5.9.1</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
-</project>
\ No newline at end of file
+</project>
diff --git a/src/main/java/org/variantsync/diffdetective/analysis/AnalysisResult.java b/src/main/java/org/variantsync/diffdetective/analysis/AnalysisResult.java
index 548d5faf7..acb3e1dc5 100644
--- a/src/main/java/org/variantsync/diffdetective/analysis/AnalysisResult.java
+++ b/src/main/java/org/variantsync/diffdetective/analysis/AnalysisResult.java
@@ -239,9 +239,13 @@ public static AnalysisResult importFrom(final Path p, final Map<String, BiConsum
                         } else if (key.startsWith(ExplainedFilterSummary.FILTERED_MESSAGE_BEGIN)) {
                             filterHitsLines.add(line);
                         } else if (key.startsWith(ERROR_BEGIN)) {
-                            DiffError e = new DiffError(key.substring(ERROR_BEGIN.length(), key.length() - ERROR_END.length()));
+                            var errorId = key.substring(ERROR_BEGIN.length(), key.length() - ERROR_END.length());
+                            var e = DiffError.fromMessage(errorId);
+                            if (e.isEmpty()) {
+                                throw new RuntimeException("Invalid error id " + errorId + " while importing " + p);
+                            }
                             // add DiffError
-                            result.diffErrors.put(e, Integer.parseInt(value));
+                            result.diffErrors.put(e.get(), Integer.parseInt(value));
                         } else {
                             final BiConsumer<AnalysisResult, String> customParser = customParsers.get(key);
                             if (customParser == null) {
diff --git a/src/main/java/org/variantsync/diffdetective/datasets/ParseOptions.java b/src/main/java/org/variantsync/diffdetective/datasets/ParseOptions.java
index b4fc12569..4bb31f557 100644
--- a/src/main/java/org/variantsync/diffdetective/datasets/ParseOptions.java
+++ b/src/main/java/org/variantsync/diffdetective/datasets/ParseOptions.java
@@ -1,6 +1,6 @@
 package org.variantsync.diffdetective.datasets;
 
-import org.variantsync.diffdetective.diff.difftree.parse.DiffNodeParser;
+import org.variantsync.diffdetective.feature.CPPAnnotationParser;
 
 /**
  * Parse options that should be used when parsing commits and patches within a commit history.
@@ -8,7 +8,7 @@
  * @param annotationParser A parser for parsing c preprocessor annotations.
  * @author Paul Bittner
  */
-public record ParseOptions(DiffStoragePolicy diffStoragePolicy, DiffNodeParser annotationParser) {
+public record ParseOptions(DiffStoragePolicy diffStoragePolicy, CPPAnnotationParser annotationParser) {
     public enum DiffStoragePolicy {
         REMEMBER_DIFF,
         REMEMBER_STRIPPED_DIFF,
@@ -21,13 +21,13 @@ public enum DiffStoragePolicy {
      * @see ParseOptions#Default
      * @param annotationParser A parser for parsing c preprocessor annotations.
      */
-    public ParseOptions(DiffNodeParser annotationParser) {
+    public ParseOptions(CPPAnnotationParser annotationParser) {
         this(Default.diffStoragePolicy, annotationParser);
     }
 
     /**
      * Creates ParseOptions with the given policy for storing diffs.
-     * @see ParseOptions#ParseOptions(DiffStoragePolicy, DiffNodeParser)
+     * @see ParseOptions#ParseOptions(DiffStoragePolicy, CPPAnnotationParser)
      * @param diffStoragePolicy Decides if and how unix diffs should be remembered when parsing commits.
      */
     public ParseOptions withDiffStoragePolicy(DiffStoragePolicy diffStoragePolicy) {
@@ -36,10 +36,10 @@ public ParseOptions withDiffStoragePolicy(DiffStoragePolicy diffStoragePolicy) {
 
     /**
      * Default value for ParseOptions that does not remember parsed unix diffs
-     * and uses the default value for the parsing annotations ({@link DiffNodeParser#Default}).
+     * and uses the default value for the parsing annotations ({@link CPPAnnotationParser#Default}).
      */
     public static final ParseOptions Default = new ParseOptions(
             DiffStoragePolicy.DO_NOT_REMEMBER,
-            DiffNodeParser.Default
+            CPPAnnotationParser.Default
     );
 }
diff --git a/src/main/java/org/variantsync/diffdetective/datasets/predefined/Marlin.java b/src/main/java/org/variantsync/diffdetective/datasets/predefined/Marlin.java
index 9d6e1fdac..a69f38b8d 100644
--- a/src/main/java/org/variantsync/diffdetective/datasets/predefined/Marlin.java
+++ b/src/main/java/org/variantsync/diffdetective/datasets/predefined/Marlin.java
@@ -2,7 +2,6 @@
 
 import org.variantsync.diffdetective.datasets.ParseOptions;
 import org.variantsync.diffdetective.datasets.Repository;
-import org.variantsync.diffdetective.diff.difftree.parse.DiffNodeParser;
 import org.variantsync.diffdetective.feature.CPPAnnotationParser;
 import org.variantsync.diffdetective.feature.PropositionalFormulaParser;
 
@@ -14,9 +13,11 @@
  * @author Kevin Jedelhauser, Paul Maximilian Bittner
  */
 public class Marlin {
-    public static final DiffNodeParser ANNOTATION_PARSER = new DiffNodeParser(
-            new CPPAnnotationParser(PropositionalFormulaParser.Default, new MarlinCPPDiffLineFormulaExtractor())
-    );
+    public static final CPPAnnotationParser ANNOTATION_PARSER =
+        new CPPAnnotationParser(
+            PropositionalFormulaParser.Default,
+            new MarlinCPPDiffLineFormulaExtractor()
+        );
 
     /**
      * Clones Marlin from Github.
diff --git a/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java b/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java
index 5bb8bcafe..bd103243f 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java
@@ -12,12 +12,11 @@
  * - the corresponding line number after the edit
  * @author Paul Bittner
  */
-public class DiffLineNumber {
+public record DiffLineNumber(int inDiff, int beforeEdit, int afterEdit) {
     /**
      * Index for invalid line numbers.
      */
     public static final int InvalidLineNumber = -1;
-    public int inDiff, beforeEdit, afterEdit;
 
     /**
      * Creates a line number of a diff.
@@ -41,34 +40,26 @@ public static DiffLineNumber Invalid() {
     }
 
     /**
-     * Creates a copy of the given DiffLineNumber.
-     */
-    public static DiffLineNumber Copy(final DiffLineNumber other) {
-        return new DiffLineNumber(other.inDiff, other.beforeEdit, other.afterEdit);
-    }
-
-    /**
-     * Make this line number become a copy of the given line number.
-     * @param other Number to copy. Remains unchanged.
-     * @return this
+     * Shifts this line number by adding the given offset.
+     * @param offset value to add to this line number.
+     * @return a new {@code DiffLineNumber} shifted by {@code offset}
      */
-    public DiffLineNumber set(final DiffLineNumber other) {
-        this.inDiff = other.inDiff;
-        this.beforeEdit = other.beforeEdit;
-        this.afterEdit = other.afterEdit;
-        return this;
+    public DiffLineNumber add(int offset) {
+        return add(offset, DiffType.NON);
     }
 
     /**
      * Shifts this line number by adding the given offset.
-     * @param offset Value to add to this line number.
-     * @return this
+     * @param offset value to add to this line number.
+     * @param diffType specifies which components are shifted
+     * @return a new {@code DiffLineNumber} shifted by {@code offset}
      */
-    public DiffLineNumber add(int offset) {
-        this.inDiff += offset;
-        this.beforeEdit += offset;
-        this.afterEdit += offset;
-        return this;
+    public DiffLineNumber add(int offset, DiffType diffType) {
+        return new DiffLineNumber(
+            inDiff + offset,
+            beforeEdit + (diffType == DiffType.ADD ? 0 : offset),
+            afterEdit + (diffType == DiffType.REM ? 0 : offset)
+        );
     }
 
     /**
@@ -78,12 +69,12 @@ public DiffLineNumber add(int offset) {
      * Non-existing values will be set to {@link DiffLineNumber#InvalidLineNumber}.
      * @param diffType The diff type according to which this line number should be filtered.
      */
-    public void as(final DiffType diffType) {
-        if (diffType == DiffType.ADD) {
-            beforeEdit = InvalidLineNumber;
-        } else if (diffType == DiffType.REM) {
-            afterEdit = InvalidLineNumber;
-        }
+    public DiffLineNumber as(final DiffType diffType) {
+        return new DiffLineNumber(
+            inDiff,
+            diffType == DiffType.ADD ? InvalidLineNumber : beforeEdit,
+            diffType == DiffType.REM ? InvalidLineNumber : afterEdit
+        );
     }
 
     @Override
diff --git a/src/main/java/org/variantsync/diffdetective/diff/GitDiffer.java b/src/main/java/org/variantsync/diffdetective/diff/GitDiffer.java
index 7a9b4a16d..3c9bdeaf7 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/GitDiffer.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/GitDiffer.java
@@ -22,10 +22,9 @@
 import org.variantsync.diffdetective.diff.difftree.parse.DiffTreeParser;
 import org.variantsync.diffdetective.diff.result.CommitDiffResult;
 import org.variantsync.diffdetective.diff.result.DiffError;
-import org.variantsync.diffdetective.diff.result.DiffResult;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
 import org.variantsync.diffdetective.preliminary.GitDiff;
 import org.variantsync.diffdetective.util.StringUtils;
-import org.variantsync.functjonal.Result;
 import org.variantsync.functjonal.iteration.MappedIterator;
 import org.variantsync.functjonal.iteration.SideEffectIterator;
 import org.variantsync.functjonal.iteration.Yield;
@@ -308,18 +307,26 @@ private static CommitDiffResult getPatchDiffs(
 
                 diffFormatter.format(diffEntry);
                 final String gitDiff = outputStream.toString(StandardCharsets.UTF_8);
-                final Result<PatchDiff, DiffError> patchDiff =
-                        getBeforeFullFile(git, parentCommit, diffEntry.getOldPath()).unwrap()
-                        .bind(file -> createPatchDiff(
-                                commitDiff,
-                                diffEntry,
-                                gitDiff,
-                                file,
-                                parseOptions).unwrap()
+                final String filename = diffEntry.getOldPath();
+                try {
+                    final BufferedReader file = getBeforeFullFile(git, parentCommit, filename);
+                    final PatchDiff patchDiff =
+                        createPatchDiff(
+                            commitDiff,
+                            diffEntry,
+                            gitDiff,
+                            file,
+                            parseOptions
                         );
 
-                patchDiff.ifSuccess(commitDiff::addPatchDiff);
-                patchDiff.ifFailure(errors::add);
+                    commitDiff.addPatchDiff(patchDiff);
+                } catch (IOException e) {
+                    Logger.debug(e, "Could not obtain full diff of file " + filename + " before commit " + parentCommit + "!");
+                    errors.add(DiffError.COULD_NOT_OBTAIN_FULLDIFF);
+                } catch (DiffParseException e) {
+                    errors.add(e.getError());
+                }
+
                 outputStream.reset();
             }
         } catch (IOException e) {
@@ -337,13 +344,15 @@ private static CommitDiffResult getPatchDiffs(
      * @param gitDiff        The git diff of the file that was changed
      * @param beforeFullFile The full file before the change
      * @return The PatchDiff of the given DiffEntry
+     * @throws DiffParseException if {@code gitDiff} couldn't be parsed
      */
-    private static DiffResult<PatchDiff> createPatchDiff(
+    private static PatchDiff createPatchDiff(
             CommitDiff commitDiff,
             DiffEntry diffEntry,
             String gitDiff,
             BufferedReader beforeFullFile,
-            final ParseOptions parseOptions) {
+            final ParseOptions parseOptions
+    ) throws DiffParseException {
         final Matcher matcher = DIFF_HEADER_PATTERN.matcher(gitDiff);
         final String strippedDiff;
         if (matcher.find()) {
@@ -353,14 +362,9 @@ private static DiffResult<PatchDiff> createPatchDiff(
         }
 
         final String fullDiff = getFullDiff(beforeFullFile, new BufferedReader(new StringReader(strippedDiff)));
-        final DiffResult<DiffTree> diffTree = DiffTreeParser.createDiffTree(fullDiff, true, true, parseOptions.annotationParser());
-
-//        if (diffTree.isFailure()) {
-//            Logger.debug("Something went wrong parsing patch for file {} at commit {}!",
-//                    diffEntry.getOldPath(), commitDiff.getAbbreviatedCommitHash());
-//        }
+        try {
+            DiffTree diffTree = DiffTreeParser.createDiffTree(fullDiff, true, true, parseOptions.annotationParser());
 
-        return diffTree.map(t -> {
             // not storing the full diff reduces memory usage by around 40-50%
             final String diffToRemember = switch (parseOptions.diffStoragePolicy()) {
                 case DO_NOT_REMEMBER -> "";
@@ -369,8 +373,14 @@ private static DiffResult<PatchDiff> createPatchDiff(
                 case REMEMBER_STRIPPED_DIFF -> strippedDiff;
             };
 
-            return new PatchDiff(commitDiff, diffEntry, diffToRemember, t);
-        });
+            return new PatchDiff(commitDiff, diffEntry, diffToRemember, diffTree);
+        } catch (DiffParseException e) {
+            // if (diffTree.isFailure()) {
+            //     Logger.debug(e, "Something went wrong parsing patch for file {} at commit {}!",
+            //             diffEntry.getOldPath(), commitDiff.getAbbreviatedCommitHash());
+            // }
+            throw e;
+        }
     }
 
     /**
@@ -435,7 +445,7 @@ public static String getFullDiff(BufferedReader beforeFile, BufferedReader gitDi
      * @param filename The name of the file
      * @return The full content of the file before the commit
      */
-    public static DiffResult<BufferedReader> getBeforeFullFile(Git git, RevCommit commit, String filename) {
+    public static BufferedReader getBeforeFullFile(Git git, RevCommit commit, String filename) throws IOException {
         RevTree tree = commit.getTree();
 
         try (TreeWalk treeWalk = new TreeWalk(git.getRepository())) {
@@ -445,14 +455,12 @@ public static DiffResult<BufferedReader> getBeforeFullFile(Git git, RevCommit co
 
             // Look for the first file that matches filename.
             if (!treeWalk.next()) {
-                return DiffResult.Failure(DiffError.COULD_NOT_OBTAIN_FULLDIFF, "Could not obtain full diff of file " + filename + " before commit " + commit + "!");
+                throw new FileNotFoundException("Couldn't find " + filename + " in the commit " + commit);
             }
 
             ObjectId objectId = treeWalk.getObjectId(0);
             ObjectLoader loader = git.getRepository().open(objectId);
-            return DiffResult.Success(new BufferedReader(new InputStreamReader(loader.openStream())));
-        } catch (IOException e) {
-            return DiffResult.Failure(DiffError.COULD_NOT_OBTAIN_FULLDIFF, "Could not obtain full diff of file " + filename + " before commit " + commit + "!");
+            return new BufferedReader(new InputStreamReader(loader.openStream()));
         }
     }
 
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
index e244a16c2..16c7416be 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
@@ -38,10 +38,8 @@ public class DiffNode {
      */
     public final NodeType nodeType;
 
-    private boolean isMultilineMacro = false;
-
-    private final DiffLineNumber from = DiffLineNumber.Invalid();
-    private final DiffLineNumber to = DiffLineNumber.Invalid();
+    private DiffLineNumber from = DiffLineNumber.Invalid();
+    private DiffLineNumber to = DiffLineNumber.Invalid();
 
     private Node featureMapping;
     private List<String> lines;
@@ -100,8 +98,8 @@ public DiffNode(DiffType diffType, NodeType nodeType,
 
         this.diffType = diffType;
         this.nodeType = nodeType;
-        this.from.set(fromLines);
-        this.to.set(toLines);
+        this.from = fromLines;
+        this.to = toLines;
         this.featureMapping = featureMapping;
         this.lines = lines;
     }
@@ -620,6 +618,10 @@ public DiffLineNumber getFromLine() {
         return from;
     }
 
+    public void setFromLine(DiffLineNumber from) {
+        this.from = from.as(diffType);
+    }
+
     /**
      * Returns the end line number of this node's corresponding text block.
      * The line number is exclusive (i.e., it points 1 behind the last included line).
@@ -628,6 +630,10 @@ public DiffLineNumber getToLine() {
         return to;
     }
 
+    public void setToLine(DiffLineNumber to) {
+        this.to = to.as(diffType);
+    }
+
     /**
      * Returns the range of line numbers of this node's corresponding source code in the text-based diff.
      * @see DiffLineNumber#rangeInDiff
@@ -677,21 +683,6 @@ public List<DiffNode> getAllChildren() {
         return getChildOrder();
     }
 
-    /**
-     * Determines if this node represents a multi-line macro.
-     * @param isMultilineMacro True iff this node represents a multi-line macro.
-     */
-    public void setIsMultilineMacro(boolean isMultilineMacro) {
-        this.isMultilineMacro = isMultilineMacro;
-    }
-
-    /**
-     * Returns true if this node represents a multi-line macro.
-     */
-    public boolean isMultilineMacro() {
-        return isMultilineMacro;
-    }
-
     /**
      * Returns the full feature mapping formula of this node.
      * The feature mapping of an {@link NodeType#IF} node is its {@link DiffNode#getDirectFeatureMapping direct feature mapping}.
@@ -986,7 +977,7 @@ public boolean isAnnotation() {
      */
     public int getID() {
         // Add one to ensure invalid (negative) line numbers don't cause issues.
-        int lineNumber = 1 + from.inDiff;
+        int lineNumber = 1 + from.inDiff();
         Assert.assertTrue((lineNumber << 2*ID_OFFSET) >> 2*ID_OFFSET == lineNumber);
 
         int id;
@@ -1123,12 +1114,12 @@ public String toTextDiff() {
     public String toString() {
         String s;
         if (isArtifact()) {
-            s = String.format("%s_%s from %d to %d", diffType, nodeType, from.inDiff, to.inDiff);
+            s = String.format("%s_%s from %d to %d", diffType, nodeType, from.inDiff(), to.inDiff());
         } else if (isRoot()) {
             s = "ROOT";
         } else {
             s = String.format("%s_%s from %d to %d with \"%s\"", diffType, nodeType,
-                    from.inDiff, to.inDiff, featureMapping);
+                    from.inDiff(), to.inDiff(), featureMapping);
         }
         return s;
     }
@@ -1138,7 +1129,7 @@ public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         DiffNode diffNode = (DiffNode) o;
-        return isMultilineMacro == diffNode.isMultilineMacro && diffType == diffNode.diffType && nodeType == diffNode.nodeType && from.equals(diffNode.from) && to.equals(diffNode.to) && Objects.equals(featureMapping, diffNode.featureMapping) && lines.equals(diffNode.lines);
+        return diffType == diffNode.diffType && nodeType == diffNode.nodeType && from.equals(diffNode.from) && to.equals(diffNode.to) && Objects.equals(featureMapping, diffNode.featureMapping) && lines.equals(diffNode.lines);
     }
 
     /**
@@ -1151,6 +1142,6 @@ public boolean equals(Object o) {
      */
     @Override
     public int hashCode() {
-        return Objects.hash(diffType, nodeType, isMultilineMacro, from, to, featureMapping, lines);
+        return Objects.hash(diffType, nodeType, from, to, featureMapping, lines);
     }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java
index c73b07bef..ac6778fed 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java
@@ -1,12 +1,12 @@
 package org.variantsync.diffdetective.diff.difftree;
 
-import org.variantsync.diffdetective.diff.difftree.parse.DiffNodeParser;
 import org.variantsync.diffdetective.diff.difftree.parse.DiffTreeParser;
 import org.variantsync.diffdetective.diff.difftree.source.PatchFile;
 import org.variantsync.diffdetective.diff.difftree.source.PatchString;
 import org.variantsync.diffdetective.diff.difftree.traverse.DiffTreeTraversal;
 import org.variantsync.diffdetective.diff.difftree.traverse.DiffTreeVisitor;
-import org.variantsync.diffdetective.diff.result.DiffResult;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
+import org.variantsync.diffdetective.feature.CPPAnnotationParser;
 import org.variantsync.diffdetective.util.Assert;
 
 import java.io.BufferedReader;
@@ -55,19 +55,21 @@ public DiffTree(DiffNode root, DiffTreeSource source) {
     }
 
     /**
-     * Same as {@link DiffTree#fromFile(Path, boolean, boolean, DiffNodeParser)} but with
-     * the {@link DiffNodeParser#Default default parser} for the lines in the diff.
+     * Same as {@link DiffTree#fromFile(Path, boolean, boolean, CPPAnnotationParser)} but with
+     * the {@link CPPAnnotationParser#Default default parser} for the lines in the diff.
      */
-    public static DiffResult<DiffTree> fromFile(final Path p, boolean collapseMultipleCodeLines, boolean ignoreEmptyLines) throws IOException {
-        return fromFile(p, collapseMultipleCodeLines, ignoreEmptyLines, DiffNodeParser.Default);
+    public static DiffTree fromFile(final Path p, boolean collapseMultipleCodeLines, boolean ignoreEmptyLines) throws IOException, DiffParseException {
+        return fromFile(p, collapseMultipleCodeLines, ignoreEmptyLines, CPPAnnotationParser.Default);
     }
 
     /**
-     * Same as {@link DiffTree#fromDiff(String, boolean, boolean, DiffNodeParser)} but with
-     * the {@link DiffNodeParser#Default default parser} for the lines in the diff.
+     * Same as {@link DiffTree#fromDiff(String, boolean, boolean, CPPAnnotationParser)} but with
+     * the {@link CPPAnnotationParser#Default default parser} for the lines in the diff.
+     *
+     * @throws DiffParseException if {@code diff} couldn't be parsed
      */
-    public static DiffResult<DiffTree> fromDiff(final String diff, boolean collapseMultipleCodeLines, boolean ignoreEmptyLines) {
-        return fromDiff(diff, collapseMultipleCodeLines, ignoreEmptyLines, DiffNodeParser.Default);
+    public static DiffTree fromDiff(final String diff, boolean collapseMultipleCodeLines, boolean ignoreEmptyLines) throws DiffParseException {
+        return fromDiff(diff, collapseMultipleCodeLines, ignoreEmptyLines, CPPAnnotationParser.Default);
     }
 
     /**
@@ -84,10 +86,10 @@ public static DiffResult<DiffTree> fromDiff(final String diff, boolean collapseM
      * @return A result either containing the parsed DiffTree or an error message in case of failure.
      * @throws IOException when the given file could not be read for some reason.
      */
-    public static DiffResult<DiffTree> fromFile(final Path p, boolean collapseMultipleCodeLines, boolean ignoreEmptyLines, final DiffNodeParser annotationParser) throws IOException {
+    public static DiffTree fromFile(final Path p, boolean collapseMultipleCodeLines, boolean ignoreEmptyLines, final CPPAnnotationParser annotationParser) throws IOException, DiffParseException {
         try (BufferedReader file = Files.newBufferedReader(p)) {
-            final DiffResult<DiffTree> tree = DiffTreeParser.createDiffTree(file, collapseMultipleCodeLines, ignoreEmptyLines, annotationParser);
-            tree.unwrap().ifSuccess(t -> t.setSource(new PatchFile(p)));
+            final DiffTree tree = DiffTreeParser.createDiffTree(file, collapseMultipleCodeLines, ignoreEmptyLines, annotationParser);
+            tree.setSource(new PatchFile(p));
             return tree;
         }
     }
@@ -104,10 +106,11 @@ public static DiffResult<DiffTree> fromFile(final Path p, boolean collapseMultip
      * @param ignoreEmptyLines Set to true if empty lines should not be included in the DiffTree.
      * @param annotationParser The parser that is used to parse lines in the diff to {@link DiffNode}s.
      * @return A result either containing the parsed DiffTree or an error message in case of failure.
+     * @throws DiffParseException if {@code diff} couldn't be parsed
      */
-    public static DiffResult<DiffTree> fromDiff(final String diff, boolean collapseMultipleCodeLines, boolean ignoreEmptyLines, final DiffNodeParser annotationParser) {
-        final DiffResult<DiffTree> tree = DiffTreeParser.createDiffTree(diff, collapseMultipleCodeLines, ignoreEmptyLines, annotationParser);
-        tree.unwrap().ifSuccess(t -> t.setSource(new PatchString(diff)));
+    public static DiffTree fromDiff(final String diff, boolean collapseMultipleCodeLines, boolean ignoreEmptyLines, final CPPAnnotationParser annotationParser) throws DiffParseException {
+        final DiffTree tree = DiffTreeParser.createDiffTree(diff, collapseMultipleCodeLines, ignoreEmptyLines, annotationParser);
+        tree.setSource(new PatchString(diff));
         return tree;
     }
 
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffType.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffType.java
index a2f917889..54f8dba7c 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffType.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffType.java
@@ -1,6 +1,6 @@
 package org.variantsync.diffdetective.diff.difftree;
 
-import java.util.function.Consumer;
+import org.apache.commons.lang3.function.FailableConsumer;
 
 /**
  * Type of change made to an artifact (e.g., a line of text in a text-based diff).
@@ -53,12 +53,18 @@ public void matchBeforeAfter(final Runnable ifExistsBefore, final Runnable ifExi
      * Runs task on ifExistsBefore if the value existed before the edit (DiffType != ADD).
      * Runs task on ifExistsAfter if the value exists after the edit (DiffType != ADD).
      * Note: Runs task on both arguments sequentially if the artifact was not edited (DiffType == NON).
+     * Some tasks may not be run if a task throws an exception.
      *
      * @param ifExistsBefore Argument that is valid if the diff did not add.
      * @param ifExistsAfter Argument that is valid if the edit did not remove.
      * @param task Task to run with all given arguments that are valid w.r.t. to this DiffType's lifetime.
+     * @throws E iff {@code task} throws {@code E}
      */
-    public <T> void matchBeforeAfter(final T ifExistsBefore, final T ifExistsAfter, final Consumer<T> task) {
+    public <T, E extends Throwable> void matchBeforeAfter(
+        final T ifExistsBefore,
+        final T ifExistsAfter,
+        final FailableConsumer<T, E> task
+    ) throws E {
         if (this != DiffType.ADD) {
             task.accept(ifExistsBefore);
         }
@@ -85,15 +91,17 @@ public DiffType inverse() {
     /**
      * Parses the diff type from a line taken from a text-based diff.
      * @param line A line in a patch.
-     * @return The type of edit of <code>line</code>.
+     * @return The type of edit of <code>line</code> or null if its an invalid diff line.
      */
     public static DiffType ofDiffLine(String line) {
         if (line.startsWith(ADD.symbol)) {
             return ADD;
         } else if (line.startsWith(REM.symbol)) {
             return REM;
-        } else {
+        } else if (line.startsWith(NON.symbol)) {
             return NON;
+        } else {
+            return null;
         }
     }
 
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/NodeType.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/NodeType.java
index aaf3a5660..66a6feb85 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/NodeType.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/NodeType.java
@@ -1,7 +1,5 @@
 package org.variantsync.diffdetective.diff.difftree;
 
-import org.variantsync.diffdetective.diff.difftree.parse.MultiLineMacroParser;
-
 /**
  * The type of nodes in a {@link DiffTree}.
  * Corresponds to the tau function from our paper.
@@ -34,26 +32,6 @@ public boolean isAnnotation() {
         return this != ARTIFACT;
     }
 
-    /**
-     * Parses the node type from a line taken from a text-based diff.
-     * @param line A line in a patch.
-     * @return The type of edit of <code>line</code>.
-     */
-    public static NodeType ofDiffLine(String line) {
-        String macro = MultiLineMacroParser.conditionalMacroName(line);
-        if (macro != null) {
-            if (macro.equals(IF.name)) {
-                return IF;
-            } else if (macro.equals(ELSE.name)) {
-                return ELSE;
-            } else if (macro.equals(ELIF.name)) {
-                return ELIF;
-            }
-        }
-
-        return ARTIFACT;
-    }
-
     /**
      * Creates a NodeType from its value names.
      * @see Enum#name()
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/DiffNodeParser.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/DiffNodeParser.java
deleted file mode 100644
index 179755c49..000000000
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/DiffNodeParser.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.variantsync.diffdetective.diff.difftree.parse;
-
-import org.prop4j.Node;
-import org.variantsync.diffdetective.diff.DiffLineNumber;
-import org.variantsync.diffdetective.diff.difftree.NodeType;
-import org.variantsync.diffdetective.diff.difftree.DiffNode;
-import org.variantsync.diffdetective.diff.difftree.DiffType;
-import org.variantsync.diffdetective.feature.CPPAnnotationParser;
-
-import java.util.ArrayList;
-
-/**
- * A parser that parses a {@link DiffNode}s from a line in a text-based diff.
- * @param annotationParser The parser to use for parsing feature annotations.
- */
-public record DiffNodeParser(CPPAnnotationParser annotationParser) {
-    /**
-     * The default node parser that uses {@link CPPAnnotationParser#Default}.
-     */
-    public static final DiffNodeParser Default = new DiffNodeParser(CPPAnnotationParser.Default);
-
-    /**
-     * Parses the given line from a text-based diff to a DiffNode.
-     *
-     * @param diffLine The line which the new node represents.
-     * @return A DiffNode with a node type, diff type, label, and feature mapping.
-     */
-    public DiffNode fromDiffLine(String diffLine) throws IllFormedAnnotationException {
-        DiffType diffType = DiffType.ofDiffLine(diffLine);
-        NodeType nodeType = NodeType.ofDiffLine(diffLine);
-        String label = diffLine.isEmpty() ? diffLine : diffLine.substring(1);
-        Node featureMapping;
-
-        if (nodeType == NodeType.ARTIFACT || nodeType == NodeType.ELSE) {
-            featureMapping = null;
-        } else {
-            featureMapping = annotationParser.parseDiffLine(diffLine);
-        }
-
-        ArrayList<String> lines = new ArrayList<>();
-        lines.add(label);
-        return new DiffNode(
-                diffType, nodeType,
-                DiffLineNumber.Invalid(), DiffLineNumber.Invalid(),
-                featureMapping,
-                lines);
-    }
-}
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/DiffTreeParser.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/DiffTreeParser.java
index dcf78b002..cf2eff98e 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/DiffTreeParser.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/DiffTreeParser.java
@@ -1,9 +1,11 @@
 package org.variantsync.diffdetective.diff.difftree.parse;
 
+import org.apache.commons.lang3.function.FailableSupplier;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.tinylog.Logger;
 import org.variantsync.diffdetective.datasets.Repository;
 import org.variantsync.diffdetective.diff.CommitDiff;
 import org.variantsync.diffdetective.diff.DiffLineNumber;
@@ -12,279 +14,467 @@
 import org.variantsync.diffdetective.diff.difftree.DiffNode;
 import org.variantsync.diffdetective.diff.difftree.DiffTree;
 import org.variantsync.diffdetective.diff.difftree.DiffType;
+import org.variantsync.diffdetective.diff.difftree.NodeType;
 import org.variantsync.diffdetective.diff.result.DiffError;
-import org.variantsync.diffdetective.diff.result.DiffResult;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
+import org.variantsync.diffdetective.feature.CPPAnnotationParser;
 import org.variantsync.diffdetective.util.Assert;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Stack;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.BiConsumer;
+import java.util.regex.Pattern;
 
 /**
  * Parser that parses {@link DiffTree}s from text-based diffs.
+ *
+ * Note: Weird line continuations and comments can cause misidentification of conditional macros.
+ * The following examples are all correct according to the C11 standard: (comment end is marked by
+ * {@code *\/}):
+ * <code>
+ * /*
+ * #ifdef A
+ * *\/
+ *
+ * #ifdef /*
+ * *\/ A
+ * #endif
+ *
+ * # /**\/ ifdef
+ * #endif
+ *
+ * # \
+ *   ifdef
+ * #endif
+ * </code>
  */
 public class DiffTreeParser {
     /**
-     * The same as {@link DiffTreeParser#createDiffTree(BufferedReader, boolean, boolean, DiffNodeParser)}
-     * but with the diff given as a single string with linebreaks instead of a BufferedReader.
-     * Rethrows IOExceptions as Assertion errors (that do not have to be catched).
+     * One line of a diff.
+     * In contrast to {@link LogicalLine}, this represents a physical line corresponding to a diff
+     * instead of some source code file.
+     *
+     * @param diffType the diff type of this line, may be {@code null} if this line has no valid
+     * diff type
+     * @param content the actual line content without a line delimiter
+     */
+    public record DiffLine(DiffType diffType, String content) {}
+
+    /**
+     * Matches the beginning of conditional macros.
+     * It doesn't match the whole macro name, for example for {@code #ifdef} only {@code "#if"} is
+     * matched and only {@code "if"} is captured.
+     *
+     * Note that this pattern doesn't handle comments between {@code #} and the macro name.
+     */
+    private final static Pattern macroPattern =
+        Pattern.compile("^[+-]?\\s*#\\s*(if|elif|else|endif)");
+
+
+    /* Settings */
+
+    /**
+     * Whether to collapse multiple adjacent lines with the same {@code DiffType} to one artifact
+     * node.
+     */
+    final boolean collapseMultipleCodeLines;
+    /**
+     * Whether to add {@code DiffNode}s for empty lines (regardless of their {@code DiffType}).
+     * If {@link collapseMultipleCodeLines} is {@code true} empty lines are also not added to
+     * existing {@code DiffNode}s.
      */
-    public static DiffResult<DiffTree> createDiffTree(
+    final boolean ignoreEmptyLines;
+    /**
+     * Customization point for how conditional macros are parsed.
+     */
+    final CPPAnnotationParser annotationParser;
+
+
+    /* State */
+
+    /**
+     * A stack containing the current path before the edit from the root of the currently parsed
+     * {@link DiffTree} to the currently parsed {@link DiffNode}.
+     *
+     * The granularity of the {@link DiffTree}s parsed by this class is always lines because line
+     * diffs can't represent different granularities. This implies that there are no nested artifact
+     * nodes in the resulting {@link DiffTree} and therefore {@code beforeStack} will never contain
+     * an artifact node.
+     */
+    private final Stack<DiffNode> beforeStack = new Stack<>();
+    /**
+     * A stack containing the current path after the edit from the root of the currently parsed
+     * {@link DiffTree} to the currently parsed {@link DiffNode}.
+     *
+     * See {@link beforeStack} for more explanations.
+     */
+    private final Stack<DiffNode> afterStack = new Stack<>();
+
+    /**
+     * The last artifact node which was parsed by {@link parseLine}.
+     * If the last parsed {@code DiffNode} was not an artifact, {@code lastArtifact} is {@code null}.
+     *
+     * This state is used to implement {@link collapseMultipleCodeLines}.
+     */
+    private DiffNode lastArtifact = null;
+
+
+    /**
+     * The same as {@link DiffTreeParser#createDiffTree(BufferedReader, boolean, boolean, CPPAnnotationParser)}
+     * but with the diff given as a single string with line breaks instead of a {@link BufferedReader}.
+     *
+     * @throws DiffParseException if {@code fullDiff} couldn't be parsed
+     */
+    public static DiffTree createDiffTree(
             String fullDiff,
             boolean collapseMultipleCodeLines,
             boolean ignoreEmptyLines,
-            DiffNodeParser nodeParser)
-    {
+            CPPAnnotationParser annotationParser
+    ) throws DiffParseException {
         try {
-            return createDiffTree(new BufferedReader(new StringReader(fullDiff)), collapseMultipleCodeLines, ignoreEmptyLines, nodeParser);
+            return createDiffTree(new BufferedReader(new StringReader(fullDiff)), collapseMultipleCodeLines, ignoreEmptyLines, annotationParser);
         } catch (IOException e) {
-            throw new AssertionError("No actual IO should be performed, because only a StringReader is used");
+            throw new AssertionError("No actual IO should be performed because only a StringReader is used");
         }
     }
 
     /**
-     * Default parsing method for DiffTrees from diffs.
+     * Default parsing method for {@link DiffTree}s from diffs.
      * This implementation has options to collapse multiple code lines into one node and to
      * discard empty lines.
      * This parsing algorithm is described in detail in Sören Viegener's bachelor's thesis.
      *
-     * @param fullDiff                  The full diff of a patch obtained from a buffered reader.
-     * @param collapseMultipleCodeLines Whether multiple consecutive code lines with the same diff type
-     *                                  should be collapsed into a single artifact node.
-     * @param ignoreEmptyLines          Whether empty lines (no matter if they are added removed
-     *                                  or remained unchanged) should be ignored.
-     * @param nodeParser                The parser to parse individual lines in the diff to DiffNodes.
-     * @return A parsed DiffTree upon success or an error indicating why parsing failed.
-     * @throws IOException when reading the given BufferedReader fails.
+     * @param fullDiff The full diff of a patch obtained from a buffered reader.
+     * @param collapseMultipleCodeLines Whether multiple consecutive code lines with the same diff
+     * type should be collapsed into a single artifact node.
+     * @param ignoreEmptyLines Whether empty lines (no matter if they are added removed or remained
+     * unchanged) should be ignored.
+     * @param annotationParser The parser to parse conditional macros lines in the diff to
+     * {@link DiffNode}s.
+     * @return A parsed {@link DiffTree} upon success or an error indicating why parsing failed.
+     * @throws IOException when reading from {@code fullDiff} fails.
+     * @throws DiffParseException if an error in the diff or macro syntax is detected
      */
-    public static DiffResult<DiffTree> createDiffTree(
+    public static DiffTree createDiffTree(
             BufferedReader fullDiff,
             boolean collapseMultipleCodeLines,
             boolean ignoreEmptyLines,
-            DiffNodeParser nodeParser) throws IOException
-    {
-        final List<DiffNode> nodes = new ArrayList<>();
-        final Stack<DiffNode> beforeStack = new Stack<>();
-        final Stack<DiffNode> afterStack = new Stack<>();
-        final DiffLineNumber lineNo = new DiffLineNumber(0, 0, 0);
-        final DiffLineNumber lastLineNo = DiffLineNumber.Copy(lineNo);
-
-        DiffNode lastArtifact = null;
-        final AtomicReference<DiffResult<DiffTree>> error = new AtomicReference<>();
-        final BiConsumer<DiffError, String> errorPropagation = (errType, message) -> {
-            if (error.get() == null) {
-                error.set(DiffResult.Failure(errType, message));
+            CPPAnnotationParser annotationParser
+    ) throws IOException, DiffParseException {
+        return new DiffTreeParser(
+            collapseMultipleCodeLines,
+            ignoreEmptyLines,
+            annotationParser
+        ).parse(() -> {
+            String line = fullDiff.readLine();
+            if (line == null) {
+                return null;
+            } else if (line.length() == 0) {
+                return new DiffLine(null, "");
+            } else {
+                return new DiffLine(DiffType.ofDiffLine(line), line.substring(1));
             }
-        };
+        });
+    }
 
-        final MultiLineMacroParser mlMacroParser = new MultiLineMacroParser(nodeParser);
+    /**
+     * Parses a variation tree from a source file.
+     * This method is similar to {@link createDiffTree(BufferedReader, boolean, boolean, CPPAnnotationParser)}
+     * but acts as if all lines where unmodified.
+     *
+     * @param file The source code file (not a diff) to be parsed.
+     * @param collapseMultipleCodeLines Whether multiple consecutive artifact lines should be
+     * collapsed into a single artifact node.
+     * @param ignoreEmptyLines Whether empty lines (no matter if they are added removed or remained
+     * unchanged) should be ignored.
+     * @param annotationParser The parser to parse conditional macros.
+     * @return A parsed {@link DiffTree}.
+     * @throws IOException iff {@code file} throws an {@code IOException}
+     * @throws DiffParseException if an error in the diff or macro syntax is detected
+     */
+    public static DiffTree createVariationTree(
+            BufferedReader file,
+            boolean collapseMultipleCodeLines,
+            boolean ignoreEmptyLines,
+            CPPAnnotationParser annotationParser
+    ) throws IOException, DiffParseException {
+        return new DiffTreeParser(
+            collapseMultipleCodeLines,
+            ignoreEmptyLines,
+            annotationParser
+        ).parse(() -> {
+            String line = file.readLine();
+            if (line == null) {
+                return null;
+            } else {
+                if (line.startsWith("+") || line.startsWith("-")) {
+                    Logger.warn(
+                        "The source file given to createVariationTree contains a plus or " +
+                        "minus sign at the start of a line. Please ensure that you are " +
+                        "actually parsing a source file and not a diff."
+                    );
+                }
 
-        final DiffNode root = DiffNode.createRoot();
+                return new DiffLine(DiffType.NON, line);
+            }
+        });
+    }
+
+    /**
+     * Initializes the parse state.
+     *
+     * @see createDiffTree(BufferedReader, boolean, boolean, CPPAnnotationParser)
+     */
+    private DiffTreeParser(
+            boolean collapseMultipleCodeLines,
+            boolean ignoreEmptyLines,
+            CPPAnnotationParser annotationParser
+    ) {
+        this.collapseMultipleCodeLines = collapseMultipleCodeLines;
+        this.ignoreEmptyLines = ignoreEmptyLines;
+        this.annotationParser = annotationParser;
+    }
+
+    /**
+     * Parses the line diff {@code fullDiff}.
+     *
+     * @param lines should supply successive lines of the diff to be parsed, or {@code null} if
+     * there are no more lines to be parsed.
+     * @return the parsed {@code DiffTree}
+     * @throws IOException iff {@code lines.get()} throws {@code IOException}
+     * @throws DiffParseException if an error in the line diff or the underlying preprocessor syntax
+     * is detected
+     */
+    private DiffTree parse(
+        FailableSupplier<DiffLine, IOException> lines
+    ) throws IOException, DiffParseException {
+        DiffNode root = DiffNode.createRoot();
         beforeStack.push(root);
         afterStack.push(root);
 
-        String currentLine;
-        for (int i = 0; (currentLine = fullDiff.readLine()) != null; i++) {
-            final DiffType diffType = DiffType.ofDiffLine(currentLine);
+        final LogicalLine beforeLine = new LogicalLine();
+        final LogicalLine afterLine = new LogicalLine();
+        boolean isNon = false;
 
-            // count line numbers
-            lastLineNo.set(lineNo);
-            lineNo.inDiff = i + 1;
-            diffType.matchBeforeAfter(() -> ++lineNo.beforeEdit, () -> ++lineNo.afterEdit);
+        DiffLineNumber lineNumber = new DiffLineNumber(0, 0, 0);
+        DiffLine currentDiffLine;
+        while ((currentDiffLine = lines.get()) != null) {
+            final String currentLine = currentDiffLine.content();
 
             // Ignore line if it is empty.
-            if (ignoreEmptyLines && (currentLine.isEmpty()
-                    // substring(1) here because of diff symbol ('+', '-', ' ') at the beginning of a line.
-                    || currentLine.substring(1).isBlank())) {
+            if (ignoreEmptyLines && currentLine.isBlank()) {
                 // discard empty lines
                 continue;
             }
 
-            // check if this is a multiline macro
-            final ParseResult isMLMacro;
-            try {
-                isMLMacro = mlMacroParser.consume(lineNo, currentLine, beforeStack, afterStack, nodes);
-            } catch (IllFormedAnnotationException e) {
-                return DiffResult.Failure(e);
+            final DiffType diffType = currentDiffLine.diffType();
+            if (diffType == null) {
+                throw new DiffParseException(DiffError.INVALID_DIFF, lineNumber.add(1));
             }
 
-            switch (isMLMacro.type()) {
-                case Success: {
-                    if (lastArtifact != null) {
-                        lastArtifact = endCodeBlock(lastArtifact, lastLineNo);
-                    }
-                    // This line belongs to a multiline macro and was handled, so go to the next line.
-                    continue;
-                }
-                case Error: {
-                    isMLMacro.onError(errorPropagation);
-                    return error.get();
-                }
-                // line is not a mult-line macro so keep going (break the switch statement).
-                case NotMyDuty: break;
-            }
-
-            if ("endif".equals(MultiLineMacroParser.conditionalMacroName(currentLine))) {
-                if (lastArtifact != null) {
-                    lastArtifact = endCodeBlock(lastArtifact, lastLineNo);
-                }
-
-                final String currentLineFinal = currentLine;
-                diffType.matchBeforeAfter(beforeStack, afterStack,
-                        stack -> {
-                            // Set corresponding line of now closed annotation.
-                            // The last block is the first one on the stack.
-                            endMacroBlock(stack.peek(), lastLineNo, diffType);
-
-                            // Pop the relevant stacks until an IF node is popped. If there were ELSEs or ELIFs between
-                            // an IF and an ENDIF, they were placed on the stack and have to be popped now.
-                            popIf(stack);
-
-                            if (stack.isEmpty()) {
-                                errorPropagation.accept(DiffError.ENDIF_WITHOUT_IF, "ENDIF without IF at line \"" + currentLineFinal + "\"!");
-                            }
-                        });
-                if (error.get() != null) { return error.get(); }
+            lineNumber = lineNumber.add(1, diffType);
+
+            // Do beforeLine and afterLine represent the same unchanged diff line?
+            isNon = diffType == DiffType.NON &&
+                (isNon || (!beforeLine.hasStarted() && !afterLine.hasStarted()));
+
+            // Add the physical line to the logical line.
+            final DiffLineNumber lineNumberFinal = lineNumber;
+            diffType.matchBeforeAfter(beforeLine, afterLine,
+                node -> node.consume(currentLine, lineNumberFinal)
+            );
+
+            // Parse the completed logical line
+            if (isNon && beforeLine.isComplete() && afterLine.isComplete()) {
+                // Only parse it once if beforeLine and afterLine represent the same unchanged
+                // diff line.
+                parseLine(beforeLine, DiffType.NON, lineNumber);
+                beforeLine.reset();
+                afterLine.reset();
             } else {
-                // This gets the node type and diff type of the current line and creates a node
-                // Note that the node is not yet added to the diff tree.
-                final DiffNode newNode;
-                try {
-                    newNode = nodeParser.fromDiffLine(currentLine);
-                } catch (IllFormedAnnotationException e) {
-                    return DiffResult.Failure(e);
+                if (beforeLine.isComplete()) {
+                    parseLine(beforeLine, DiffType.REM, lineNumber);
+                    beforeLine.reset();
                 }
-
-                // collapse multiple code lines
-                if (lastArtifact != null) {
-                    if (collapseMultipleCodeLines && newNode.isArtifact() && lastArtifact.diffType.equals(newNode.diffType)) {
-                        lastArtifact.addLines(newNode.getLines());
-                        continue;
-                    } else {
-                        lastArtifact = endCodeBlock(lastArtifact, lastLineNo);
-                    }
-                }
-
-                newNode.getFromLine().set(lineNo);
-                newNode.addBelow(beforeStack.peek(), afterStack.peek());
-                nodes.add(newNode);
-
-                if (newNode.isArtifact()) {
-                    lastArtifact = newNode;
-                } else {
-                    // newNode is if, elif or else
-                    // push the node to the relevant stacks
-                    diffType.matchBeforeAfter(beforeStack, afterStack, stack ->
-                            pushNodeToStack(newNode, stack, lastLineNo).onError(errorPropagation)
-                    );
-                    if (error.get() != null) { return error.get(); }
+                if (afterLine.isComplete()) {
+                    parseLine(afterLine, DiffType.ADD, lineNumber);
+                    afterLine.reset();
                 }
             }
         }
 
-        if (beforeStack.size() > 1 || afterStack.size() > 1) {
-            return DiffResult.Failure(DiffError.NOT_ALL_ANNOTATIONS_CLOSED);
+        if (beforeLine.hasStarted() || afterLine.hasStarted()) {
+            throw new DiffParseException(
+                DiffError.INVALID_LINE_CONTINUATION,
+                lineNumber
+            );
         }
 
-        if (lastArtifact != null) {
-            lastArtifact = endCodeBlock(lastArtifact, lineNo);
+        if (beforeStack.size() > 1) {
+            throw new DiffParseException(
+                DiffError.NOT_ALL_ANNOTATIONS_CLOSED,
+                beforeStack.peek().getFromLine()
+            );
         }
-
-        // Invalidate line numbers according to edits.
-        // E.g. if a node was added, it had no line number before the edit.
-        for (final DiffNode node : nodes) {
-            node.getFromLine().as(node.diffType);
-            node.getToLine().as(node.diffType);
+        if (afterStack.size() > 1) {
+            throw new DiffParseException(
+                DiffError.NOT_ALL_ANNOTATIONS_CLOSED,
+                afterStack.peek().getFromLine()
+            );
         }
 
-        return DiffResult.Success(new DiffTree(root));
+        // Cleanup state
+        beforeStack.clear();
+        afterStack.clear();
+        lastArtifact = null;
+
+        return new DiffTree(root);
     }
 
     /**
-     * Push the given node to the given stack including some sanity checks.
-     * This method also ensures that annotation blocks are closed correctly,
-     * by setting ending line numbers.
-     * @param newNode The node to push to the given parsing stack.
-     * @param stack Describes current the nesting depth of feature annotations.
-     * @param lastLineNo The current line number from which's line the given node was parsed.
-     * @return Either success or an error in case a sanity check failed.
+     * Parses one logical line and most notably, handles conditional macros.
+     *
+     * @param line a logical line with {@code line.isComplete() == true}
+     * @param diffType whether {@code line} was added, inserted or unchanged
+     * @param lastLineNumber the last physical line of {@code line}
+     * @throws DiffParseException if erroneous preprocessor macros are detected
      */
-    static ParseResult pushNodeToStack(
-            final DiffNode newNode,
-            final Stack<DiffNode> stack,
-            final DiffLineNumber lastLineNo) {
-        if (newNode.isElif() || newNode.isElse()) {
-            if (stack.size() == 1) {
-                return ParseResult.ERROR(DiffError.ELSE_OR_ELIF_WITHOUT_IF);
-            }
+    private void parseLine(
+            final LogicalLine line,
+            final DiffType diffType,
+            final DiffLineNumber lastLineNumber
+    ) throws DiffParseException {
+        final DiffLineNumber fromLine = line.getStartLineNumber().as(diffType);
+        final DiffLineNumber toLine = lastLineNumber.add(1).as(diffType);
+
+        // Is this line a conditional macro?
+        // Note: The following line doesn't handle comments and line continuations correctly.
+        var matcher = macroPattern.matcher(line.getLines().get(0));
+        var conditionalMacroName = matcher.find()
+            ? matcher.group(1)
+            : null;
+
+        if ("endif".equals(conditionalMacroName)) {
+            lastArtifact = null;
+
+            // Do not create a node for ENDIF, but update the line numbers of the closed if-chain
+            // and remove that if-chain from the relevant stacks.
+            diffType.matchBeforeAfter(beforeStack, afterStack, stack ->
+                popIfChain(stack, fromLine)
+            );
+        } else if (collapseMultipleCodeLines
+                && conditionalMacroName == null
+                && lastArtifact != null
+                && lastArtifact.diffType.equals(diffType)
+                && lastArtifact.getToLine().inDiff() == fromLine.inDiff()) {
+            // Collapse consecutive lines if possible.
+            lastArtifact.addLines(line.getLines());
+            lastArtifact.setToLine(toLine);
+        } else {
+            try {
+                NodeType nodeType = NodeType.ARTIFACT;
+                if (conditionalMacroName != null) {
+                    try {
+                        nodeType = NodeType.fromName(conditionalMacroName);
+                    } catch (IllegalArgumentException e) {
+                        throw new DiffParseException(DiffError.INVALID_MACRO_NAME, fromLine);
+                    }
+                }
 
-            if (stack.peek().isElse()) {
-                return ParseResult.ERROR(DiffError.ELSE_AFTER_ELSE);
+                DiffNode newNode = new DiffNode(
+                    diffType,
+                    nodeType,
+                    fromLine,
+                    toLine,
+                    nodeType == NodeType.ARTIFACT || nodeType == NodeType.ELSE
+                        ? null
+                        : annotationParser.parseDiffLines(line.getLines()),
+                    line.getLines()
+                );
+
+                addNode(newNode);
+                lastArtifact = newNode.isArtifact() ? newNode : null;
+            } catch (IllFormedAnnotationException e) {
+                throw new DiffParseException(e.getType(), fromLine);
             }
-
-            // set corresponding line of now closed annotation
-            endMacroBlock(stack.peek(), lastLineNo, newNode.diffType);
         }
-
-        stack.push(newNode);
-        return ParseResult.SUCCESS;
     }
 
     /**
-     * Ends a given code block by setting its ending line number correctly.
-     * @param block The code block to end.
-     * @param lastLineNo The line number of the last parsed line.
-     *                   This should be the line number of the last line in the diff
-     *                   that is part of the given block.
-     * @return null
+     * Pop {@code stack} until an IF node is popped.
+     * If there were ELSEs or ELIFs between an IF and an ENDIF, they were placed on the stack and
+     * have to be popped now. The {@link DiffNode#getToLine() end line numbers} are adjusted
+     *
+     * @param stack the stack which should be popped
+     * @param elseLineNumber the first line of the else which causes this IF to be popped
+     * @throws DiffParseException if {@code stack} doesn't contain an IF node
      */
-    private static DiffNode endCodeBlock(final DiffNode block, final DiffLineNumber lastLineNo) {
-        block.getToLine().set(lastLineNo).add(1);
-        return null;
+    private void popIfChain(
+        Stack<DiffNode> stack,
+        DiffLineNumber elseLineNumber
+    ) throws DiffParseException {
+        DiffLineNumber previousLineNumber = elseLineNumber;
+        do {
+            DiffNode annotation = stack.peek();
+
+            // Set the line number of now closed annotations to the beginning of the
+            // following annotation.
+            annotation.setToLine(new DiffLineNumber(
+                Math.max(previousLineNumber.inDiff(), annotation.getToLine().inDiff()),
+                stack == beforeStack
+                    ? previousLineNumber.beforeEdit()
+                    : annotation.getToLine().beforeEdit(),
+                stack == afterStack
+                    ? previousLineNumber.afterEdit()
+                    : annotation.getToLine().afterEdit()
+            ));
+
+            previousLineNumber = annotation.getFromLine();
+        } while (!stack.pop().isIf());
+
+        if (stack.isEmpty()) {
+            throw new DiffParseException(DiffError.ENDIF_WITHOUT_IF, elseLineNumber);
+        }
     }
 
     /**
-     * Ends a given macro block by setting its ending line number correctly.
-     * @param block The macro block to end.
-     * @param lastLineNo The last line number in the diff that is part of the given block.
-     * @param diffTypeOfNewBlock The diff type of the upcoming block. This type determines
-     *                           at which times the given block ends.
+     * Adds a fully parsed node into the {@code DiffTree}.
+     * Annotations also pushed to the relevant stacks.
+     *
+     * @param newNode the fully parsed node to be added
+     * @throws DiffParseException if {@code line} erroneous preprocessor macros are detected
      */
-    private static void endMacroBlock(final DiffNode block, final DiffLineNumber lastLineNo, final DiffType diffTypeOfNewBlock) {
-        // Add 1 because the end line is exclusive, so we have to point one behind the last line we found.
-        final DiffLineNumber to = block.getToLine();
-        diffTypeOfNewBlock.matchBeforeAfter(
-                () -> to.beforeEdit = lastLineNo.beforeEdit + 1,
-                () -> to.afterEdit = lastLineNo.afterEdit + 1);
-        // Take the highest value ever set as we want to include all lines that are somehow affected by this block.
-        to.inDiff = Math.max(to.inDiff, lastLineNo.inDiff + 1);
-    }
+    private void addNode(DiffNode newNode) throws DiffParseException {
+        newNode.addBelow(beforeStack.peek(), afterStack.peek());
+
+        if (newNode.isAnnotation()) {
+            // newNode is IF, ELIF or ELSE, so push it to the relevant stacks.
+            newNode.diffType.matchBeforeAfter(beforeStack, afterStack, stack -> {
+                if (newNode.isElif() || newNode.isElse()) {
+                    if (stack.size() == 1) {
+                        throw new DiffParseException(
+                            DiffError.ELSE_OR_ELIF_WITHOUT_IF,
+                            newNode.getFromLine()
+                        );
+                    }
 
-    /**
-     * Pop nodes from the given annotation nesting stack until an If or the root was popped.
-     * Used to exist if-elif-else chains.
-     * @param stack The stack representing the current nesting of annotations.
-     * @return The last popped node. This is either an IF node or the root.
-     */
-    public static DiffNode popIf(final Stack<DiffNode> stack) {
-        DiffNode popped;
-        do {
-            // Don't update line numbers of popped nodes here as this already happened.
-            popped = stack.pop();
-        } while (!popped.isIf());
-        return popped;
+                    if (stack.peek().isElse()) {
+                        throw new DiffParseException(DiffError.ELSE_AFTER_ELSE, newNode.getFromLine());
+                    }
+                }
+
+                stack.push(newNode);
+            });
+        }
     }
 
     /**
      * Parses the given commit of the given repository.
      * @param repo The repository from which a commit should be parsed.
      * @param commitHash Hash of the commit to parse.
-     * @return A CommitDiff describing edits to variability introduced by the given commit relative to its first parent commit.
+     * @return A CommitDiff describing edits to variability introduced by the given commit relative
+     * to its first parent commit.
      * @throws IOException when an error occurred.
      */
     public static CommitDiff parseCommit(Repository repo, String commitHash) throws IOException {
@@ -312,7 +502,8 @@ public static CommitDiff parseCommit(Repository repo, String commitHash) throws
      * @param repo The repository from which a patch should be parsed.
      * @param file The file that was edited by the patch.
      * @param commitHash The hash of the commit in which the patch was made.
-     * @return A PatchDiff describing edits to variability introduced by the given patch relative to the corresponding commit's first parent commit.
+     * @return A PatchDiff describing edits to variability introduced by the given patch relative to
+     * the corresponding commit's first parent commit.
      * @throws IOException when an error occurred.
      * @throws AssertionError when no such patch exists.
      */
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/LogicalLine.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/LogicalLine.java
new file mode 100644
index 000000000..709ddc612
--- /dev/null
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/LogicalLine.java
@@ -0,0 +1,87 @@
+package org.variantsync.diffdetective.diff.difftree.parse;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.variantsync.diffdetective.diff.DiffLineNumber;
+import org.variantsync.diffdetective.util.Assert;
+
+/**
+ * A logical line consisting of multiple physical lines of a text file joined by line continuations.
+ *
+ * The concept of a logical line is used by the C11 standard to refer to one source code line of a
+ * source file after all line continuations have been eliminated. Essentially a logical line is a
+ * string matched by the regex {@code ([^\n]|\\\n)*} where {@code \n} represents the platform
+ * dependent line delimiter. In contrast, a physical line is matched by the regex {@code [^\n]*}.
+ *
+ * @author Benjamin Moosherr
+ */
+class LogicalLine {
+    private List<String> lines;
+    private boolean isContinued;
+    private DiffLineNumber startLineNumber;
+
+    /**
+     * Constructs an empty logical line.
+     */
+    public LogicalLine() {
+        reset();
+    }
+
+    /**
+     * Starts a new, empty logical line.
+     * This especially ensures {@code hasStarted() == false} and {@code isComplete() == false}.
+     */
+    public void reset() {
+        lines = new ArrayList<>();
+        isContinued = false;
+        startLineNumber = DiffLineNumber.Invalid();
+    }
+
+    /**
+     * Adds the physical line {@code line} with the line number {@code lineNumber} to this logical
+     * line.
+     * This must not be called while {@code isComplete()} returns {@code true}. There must be no
+     * new line inside of {@code line}.
+     */
+    public void consume(String line, DiffLineNumber lineNumber) {
+        Assert.assertTrue(!isComplete());
+
+        if (!hasStarted()) {
+            startLineNumber = lineNumber;
+        }
+        lines.add(line);
+        isContinued = line.endsWith("\\");
+    }
+
+    /**
+     * Returns {@code true} iff at least one physical line was {@link consume}d.
+     */
+    public boolean hasStarted() {
+        return !lines.isEmpty();
+    }
+
+    /**
+     * Returns {@code true} iff the current logical is complete and the next physical line belongs
+     * to a new logical line.
+     */
+    public boolean isComplete() {
+        return hasStarted() && !isContinued;
+    }
+
+    /**
+     * Returns the line number of the first {@link consume}d physical line of the current physical
+     * line.
+     */
+    public DiffLineNumber getStartLineNumber() {
+        return startLineNumber;
+    }
+
+    /**
+     * Returns all physical lines {@link consume}d for the current logical line.
+     * The backslashes of line continuations are still part of the strings.
+     */
+    public List<String> getLines() {
+        return lines;
+    }
+}
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/MultiLineMacroParser.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/MultiLineMacroParser.java
deleted file mode 100644
index c8dbe1522..000000000
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/MultiLineMacroParser.java
+++ /dev/null
@@ -1,226 +0,0 @@
-package org.variantsync.diffdetective.diff.difftree.parse;
-
-import org.variantsync.diffdetective.diff.DiffLineNumber;
-import org.variantsync.diffdetective.diff.difftree.DiffNode;
-import org.variantsync.diffdetective.diff.difftree.DiffType;
-
-import java.io.BufferedReader;
-import java.util.List;
-import java.util.Stack;
-import java.util.regex.Pattern;
-
-import static org.variantsync.diffdetective.diff.result.DiffError.MLMACRO_WITHIN_MLMACRO;
-
-/**
- * A parser for definitions of multiline macros in text-based diffs.
- * @author Paul Bittner
- */
-public class MultiLineMacroParser {
-    /** Matches conditional macros. Note that it doesn't match the whole line or even the whole
-     * macro name. For example {@code #ifdef} is also matched, but only {@code "if"} is captured.
-     */
-    private final static Pattern macroPattern = Pattern.compile("^[+-]?\\s*#\\s*(if|elif|else|endif)");
-
-    private final DiffNodeParser nodeParser;
-
-    private MultilineMacro beforeMLMacro = null;
-    private MultilineMacro afterMLMacro = null;
-
-    /**
-     * Create a new parser that uses the given DiffNodeParser to construct DiffNodes.
-     * @param nodeParser Parser to build DiffNodes.
-     */
-    public MultiLineMacroParser(DiffNodeParser nodeParser) {
-        this.nodeParser = nodeParser;
-    }
-
-    /**
-     * Converts a multiline macro to a DiffNode.
-     * @param lineNo The end line number of the macro.
-     * @param line The last line of the macro.
-     * @param macro The macro to finalize.
-     * @param diffType The diff type of the produced node.
-     * @param nodes The list to add the node to.
-     * @return The finalized macro converted to a DiffNode.
-     */
-    private DiffNode finalizeMLMacro(
-            final DiffLineNumber lineNo,
-            final String line,
-            final MultilineMacro macro,
-            final DiffType diffType,
-            final List<DiffNode> nodes) throws IllFormedAnnotationException {
-        macro.addLine(line);
-        macro.diffType = diffType;
-
-        final DiffNode node = macro.toDiffNode(nodeParser);
-        node.getToLine().set(lineNo);
-        nodes.add(node);
-        return node;
-    }
-
-    /**
-     * Consumes the next line a text-based diff and determines if that line
-     * is part of a multi-line macro definition or not.
-     * @param lineNo The line number of the currently parsed line.
-     * @param line The line to parse.
-     * @param beforeStack The current before stack as defined by Sören's algorithm.
-     * @param afterStack The current after stack as defined by Sören's algorithm.
-     * @param nodes The list of all DiffNodes that where already parsed.
-     * @return {@link ParseResult#SUCCESS} if the line was consumed and is part of a multiline macro definition.
-     *         {@link ParseResult#NOT_MY_DUTY} if the line is not part of a multiline macro definition and was not parsed.
-     *         The line remains unparsed and should be parsed in another way.
-     *         {@link ParseResult#ERROR} if an error occurred (e.g., because of a syntax error).
-     * @throws IllFormedAnnotationException when {@link MultiLineMacroParser#finalizeMLMacro} fails.
-     * @see DiffTreeParser#createDiffTree(BufferedReader, boolean, boolean, DiffNodeParser)
-     */
-    ParseResult consume(
-            final DiffLineNumber lineNo,
-            final String line,
-            final Stack<DiffNode> beforeStack,
-            final Stack<DiffNode> afterStack,
-            final List<DiffNode> nodes
-    ) throws IllFormedAnnotationException {
-        final DiffType diffType = DiffType.ofDiffLine(line);
-        final boolean isAdd = diffType == DiffType.ADD;
-        final boolean isRem = diffType == DiffType.REM;
-
-        if (continuesMultilineDefinition(line)) {
-            // If this multiline macro line is a header...
-            if (conditionalMacroName(line) != null) {
-                // ... create a new multi line macro to complete.
-                if (!isAdd) {
-                    if (beforeMLMacro != null) {
-                        return ParseResult.ERROR(MLMACRO_WITHIN_MLMACRO, "Found definition of multiline macro within multiline macro at line " + line + "!");
-                    }
-                    beforeMLMacro = new MultilineMacro(line, diffType, lineNo, beforeStack.peek(), afterStack.peek());
-                }
-                if (!isRem) {
-                    if (afterMLMacro != null) {
-                        return ParseResult.ERROR(MLMACRO_WITHIN_MLMACRO, "Found definition of multiline macro within multiline macro at line " + line + "!");
-                    }
-                    afterMLMacro = new MultilineMacro(line, diffType, lineNo, beforeStack.peek(), afterStack.peek());
-                }
-            } else { // body
-                // ... otherwise, it is a line within a body of a multiline macro. Thus append it.
-                if (!isAdd) {
-                    if (beforeMLMacro == null) {
-                        /* If this happens (at least) one of this happened
-                         * 1. Found line of a multiline macro without header at line " + line + "!
-                         * 2. Backslash in a comment.
-                         * 3. It is the head of a multiline #define macro that we classify as artifact.
-                         *
-                         * As 2 and 3 are most likely we just assume those.
-                         */
-//                        return ParseResult.ERROR("Found line of a multiline macro without header at line " + line + "!");
-                        return ParseResult.NOT_MY_DUTY;
-                    }
-                    beforeMLMacro.addLine(line);
-                }
-                if (!isRem) {
-                    if (afterMLMacro == null) {
-                        // see above
-//                        return ParseResult.ERROR("Found line of a multiline macro without header at line " + line + "!");
-                        return ParseResult.NOT_MY_DUTY;
-                    }
-                    afterMLMacro.addLine(line);
-                }
-            }
-
-            return ParseResult.SUCCESS;
-        } else {
-            // We either found the ending line of a multiline macro or just a plain line outside of a macro.
-            final boolean inBeforeMLMacro = beforeMLMacro != null;
-            final boolean inAfterMLMacro = afterMLMacro != null;
-
-            // check if last line of a multi macro
-            if (inBeforeMLMacro || inAfterMLMacro) {
-                if (
-                        inBeforeMLMacro
-                        && inAfterMLMacro
-                        && diffType == DiffType.NON
-                        && beforeMLMacro.equals(afterMLMacro)) {
-                    // We have one single end line for two equal multi line macros -> Merge the nodes.
-                    final DiffNode mlNode = finalizeMLMacro(
-                            lineNo,
-                            line,
-                            beforeMLMacro /* == afterMLMacro */,
-                            DiffType.NON, nodes);
-
-                    ParseResult pushResult = DiffTreeParser.pushNodeToStack(mlNode, beforeStack, beforeMLMacro.getLineFrom());
-                    if (pushResult.isError()) {
-                        return pushResult;
-                    }
-
-                    pushResult = DiffTreeParser.pushNodeToStack(mlNode, afterStack, afterMLMacro.getLineFrom());
-                    if (pushResult.isError()) {
-                        return pushResult;
-                    }
-
-                    beforeMLMacro = null;
-                    afterMLMacro = null;
-                } else {
-                    if (inBeforeMLMacro && !isAdd) {
-                        final DiffNode beforeMLNode = finalizeMLMacro(
-                                lineNo,
-                                line,
-                                beforeMLMacro,
-                                DiffType.REM, nodes);
-
-                        final ParseResult pushResult = DiffTreeParser.pushNodeToStack(beforeMLNode, beforeStack, beforeMLMacro.getLineFrom());
-                        if (pushResult.isError()) {
-                            return pushResult;
-                        }
-
-                        beforeMLMacro = null;
-                    }
-
-                    if (inAfterMLMacro && !isRem) {
-                        final DiffNode afterMLNode = finalizeMLMacro(
-                                lineNo,
-                                line,
-                                afterMLMacro, DiffType.ADD, nodes);
-
-                        final ParseResult pushResult = DiffTreeParser.pushNodeToStack(afterMLNode, afterStack, afterMLMacro.getLineFrom());
-                        if (pushResult.isError()) {
-                            return pushResult;
-                        }
-
-                        afterMLMacro = null;
-                    }
-                }
-
-                return ParseResult.SUCCESS;
-            }
-        }
-
-        return ParseResult.NOT_MY_DUTY;
-    }
-
-    /**
-     * Checks whether the given line continues the definition of a multiline macro (possibly within a text-based diff).
-     * @param line The line to check for continuing a multiline macro definition.
-     * @return True iff the line ends with a backslash.
-     */
-    public static boolean continuesMultilineDefinition(String line) {
-        return line.trim().endsWith("\\");
-    }
-
-    /**
-     * Returns the shortened name of a conditional macro.
-     *
-     * Shortened means it's one of {@code if}, {@code elif}, {@code else} or {@code endif},
-     * although the actual macro name may be longer (for example {@code ifdef}).
-     *
-     * @param line the first line of the macro
-     * @return the shortened name of a conditional macro in {@code line} or {@code null} if there
-     * is no conditional macro on {@code line}
-     */
-    public static String conditionalMacroName(String line) {
-        var matcher = macroPattern.matcher(line);
-        if (matcher.find()) {
-            return matcher.group(1);
-        } else {
-            return null;
-        }
-    }
-}
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/MultilineMacro.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/MultilineMacro.java
deleted file mode 100644
index 4ca5129ed..000000000
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/MultilineMacro.java
+++ /dev/null
@@ -1,108 +0,0 @@
-package org.variantsync.diffdetective.diff.difftree.parse;
-
-import org.variantsync.diffdetective.diff.DiffLineNumber;
-import org.variantsync.diffdetective.diff.difftree.DiffNode;
-import org.variantsync.diffdetective.diff.difftree.DiffType;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * Represents a multiline macro definition in a diff during parsing of a text-based diff.
- * @author Paul Bittner
- */
-public class MultilineMacro {
-    private final List<String> lines;
-    private final DiffNode beforeParent;
-    private final DiffNode afterParent;
-    private final DiffLineNumber startLine = DiffLineNumber.Invalid();
-    DiffType diffType;
-
-    /**
-     * Create the definition of a multiline macro from the given values.
-     * @param line The corresponding line in the diff.
-     * @param diffType The diff type of this multiline macro.
-     * @param startLine The line number at which the definition of this macro began.
-     * @param beforeParent The parent of this macro before the edit.
-     * @param afterParent The parent of this macro after the edit.
-     */
-    public MultilineMacro(
-            final String line,
-            final DiffType diffType,
-            final DiffLineNumber startLine,
-            final DiffNode beforeParent,
-            final DiffNode afterParent) {
-        this.lines = new ArrayList<>();
-        this.lines.add(line);
-        this.diffType = diffType;
-        this.startLine.set(startLine);
-        this.beforeParent = beforeParent;
-        this.afterParent = afterParent;
-    }
-
-    /**
-     * Returns the line number at which the definition of this macro began.
-     */
-    public DiffLineNumber getLineFrom() {
-        return startLine;
-    }
-
-    /**
-     * Adds the given line to this multiline macro.
-     * The given line is considered to be part of this multiline macro's definition.
-     */
-    public void addLine(final String line) {
-        lines.add(line);
-    }
-
-    /**
-     * Parses this multiline macro definition in a DiffNode.
-     * @param nodeParser This parse is used to create the DiffNode after all lines of this
-     *                   multiline macro are joined to a single line.
-     * @return A DiffNode representing this multiline macro.
-     * @throws IllFormedAnnotationException when {@link DiffNodeParser#fromDiffLine(String)} fails.
-     */
-    public DiffNode toDiffNode(DiffNodeParser nodeParser) throws IllFormedAnnotationException {
-        final StringBuilder asSingleLine = new StringBuilder(diffType.symbol);
-
-        for (int l = 0; l < lines.size(); ++l) {
-            final String line = lines.get(l);
-            if (l < lines.size() - 1) {
-                asSingleLine.append(line.substring(1, line.lastIndexOf('\\')).trim()).append(" ");
-            } else {
-                asSingleLine.append(line.substring(1).trim());
-            }
-        }
-
-        final DiffNode result = nodeParser.fromDiffLine(asSingleLine.toString());
-        result.getFromLine().set(startLine);
-        result.addBelow(beforeParent, afterParent);
-        result.setIsMultilineMacro(true);
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        MultilineMacro that = (MultilineMacro) o;
-        return startLine.equals(that.startLine) && lines.equals(that.lines) && diffType == that.diffType;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(lines, diffType, startLine);
-    }
-
-    @Override
-    public String toString() {
-        return "MultilineMacro{" +
-                "lines=" + lines +
-                ", beforeParent=" + beforeParent +
-                ", afterParent=" + afterParent +
-                ", startLine=" + startLine +
-                ", diffType=" + diffType +
-                '}';
-    }
-}
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/ParseResult.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/ParseResult.java
deleted file mode 100644
index 21bd87101..000000000
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/ParseResult.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package org.variantsync.diffdetective.diff.difftree.parse;
-
-import org.variantsync.diffdetective.diff.result.DiffError;
-
-import java.util.function.BiConsumer;
-
-/**
- * Result type for parsing.
- * Mostly used for parsing multiline macros.
- * @param type Decides if an element was parsed successfully or not .
- * @param errorType An error description in case an error occurred.
- * @param message A custom message.
- * @see MultiLineMacroParser
- * @author Paul Bittner
- */
-public record ParseResult(ParseResultType type, DiffError errorType, String message) {
-    /**
-     * Default value for returning that parsing was successful.
-     */
-    public static final ParseResult SUCCESS = new ParseResult(ParseResultType.Success);
-
-    /**
-     * Default value for returning that a token was not parsed because
-     * it was not relevant for a certain parser.
-     * This is mostly used by the {@link MultiLineMacroParser} when it finds a
-     * source code line that is not part of a multiline macro definition.
-     */
-    public static final ParseResult NOT_MY_DUTY = new ParseResult(ParseResultType.NotMyDuty);
-
-    /**
-     * Creates an error result.
-     * @param errorType The type of error that occurred.
-     * @param message A custom error message.
-     * @return An error result.
-     */
-    public static ParseResult ERROR(final DiffError errorType, final String message) {
-        return new ParseResult(ParseResultType.Error, errorType, message);
-    }
-
-    /**
-     * The same as {@link ParseResult#ERROR(DiffError, String)} but uses the message stored in the given
-     * error as the custom message (i.e., {@link DiffError#id()}).
-     */
-    public static ParseResult ERROR(final DiffError errorType) {
-        return ERROR(errorType, errorType.id());
-    }
-
-    private ParseResult(ParseResultType type) {
-        this(type, null, "");
-    }
-
-    /**
-     * Returns true if this result represents an error.
-     */
-    public boolean isError() {
-        return type == ParseResultType.Error;
-    }
-
-    /**
-     * Invokes the given function with the respective error if this
-     * result is an error.
-     * @param handler Callback to invoke in case an error occured.
-     * @return True iff this result is an error.
-     * @see ParseResult#isError()
-     */
-    public boolean onError(BiConsumer<DiffError, String> handler) {
-        if (isError()) {
-            handler.accept(errorType, message);
-            return true;
-        }
-
-        return false;
-    }
-}
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/ParseResultType.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/ParseResultType.java
deleted file mode 100644
index 0599a199c..000000000
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/parse/ParseResultType.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.variantsync.diffdetective.diff.difftree.parse;
-
-/**
- * Result values for reporting parsing results.
- * Used by {@link ParseResult}.
- * @author Paul Bittner
- */
-public enum ParseResultType {
-    /**
-     * A token (or LOC) could be parsed successfully and was consumed.
-     */
-    Success,
-    /**
-     * The parser was not responsible for parsing a token (or LOC) and thus did neither parse
-     * nor consume the given token. Another parser should be used.
-     */
-    NotMyDuty,
-    /**
-     * A token (or LOC) should have been parsed but parsing failed (e.g., because of an ill-formed string).
-     */
-    Error
-}
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/GraphvizExporter.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/GraphvizExporter.java
index 03ecd733e..de3bf6580 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/GraphvizExporter.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/GraphvizExporter.java
@@ -7,6 +7,7 @@
 import java.io.OutputStream;
 import java.io.PrintStream;
 import java.util.List;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -144,8 +145,9 @@ public InputStream computeGraphvizLayout(
     static private String escape(List<String> label) {
         return label
             .stream()
-            .map((line) -> quotePattern.matcher(line).replaceAll((match) -> "\\\\" + match.group()))
+            .map((line) -> quotePattern.matcher(line).replaceAll((match) ->
+                Matcher.quoteReplacement("\\" + match.group())
+            ))
             .collect(Collectors.joining("\\n"));
-
     }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/TikzExporter.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/TikzExporter.java
index fa03a22b2..a46dd13d6 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/TikzExporter.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/TikzExporter.java
@@ -1,13 +1,13 @@
 package org.variantsync.diffdetective.diff.difftree.serialize;
 
 import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Locale;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -131,10 +131,10 @@ public void exportDiffTree(
      * by exporting directly into a file.
      *
      * @param diffTree to be exported
-     * @param filename of the destination file
+     * @param destination path of the generated file
      */
-    public void exportFullLatexExample(DiffTree diffTree, String filename) throws IOException {
-        try (var file = new BufferedOutputStream(new FileOutputStream(filename))) {
+    public void exportFullLatexExample(DiffTree diffTree, Path destination) throws IOException {
+        try (var file = Files.newOutputStream(destination)) {
             try (var header = new BufferedInputStream(getClass().getResourceAsStream("/tikz_header.tex"))) {
                 header.transferTo(file);
             }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/edgeformat/ChildOrderEdgeFormat.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/edgeformat/ChildOrderEdgeFormat.java
new file mode 100644
index 000000000..dc12e04e2
--- /dev/null
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/edgeformat/ChildOrderEdgeFormat.java
@@ -0,0 +1,24 @@
+package org.variantsync.diffdetective.diff.difftree.serialize.edgeformat;
+
+import org.variantsync.diffdetective.diff.difftree.serialize.StyledEdge;
+import org.variantsync.diffdetective.diff.difftree.DiffTree; // For JavaDoc
+
+/**
+ * An edge format encoding the child index of this edge.
+ * The child index is the index of the child to the parent of the edge. If there is no child parent
+ * relationship between the children in a given edge, the child index is -1.
+ *
+ * This index is encoded into decimal and delimited by a semicolon from the previous value.
+ *
+ * This format is mainly useful to equivalence of two {@link DiffTree}s, for example in tests.
+ *
+ * @author Benjamin Moosherr
+ */
+public class ChildOrderEdgeFormat extends EdgeLabelFormat {
+    @Override
+    public String labelOf(StyledEdge edge) {
+        int i = edge.from().indexOfChild(edge.to());
+        int j = edge.to().indexOfChild(edge.from());
+        return String.format(";%d", i < 0 ? j : i);
+    }
+}
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/FormulasAndLineNumbersNodeFormat.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/FormulasAndLineNumbersNodeFormat.java
index 21d47929e..5f31be78d 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/FormulasAndLineNumbersNodeFormat.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/FormulasAndLineNumbersNodeFormat.java
@@ -13,7 +13,7 @@
 public class FormulasAndLineNumbersNodeFormat implements DiffNodeLabelFormat {
     @Override
     public String toLabel(DiffNode node) {
-        final String lineNumbers = node.getFromLine().inDiff + "-" + node.getToLine().inDiff + ": " + node.nodeType;
+        final String lineNumbers = node.getFromLine().inDiff() + "-" + node.getToLine().inDiff() + ": " + node.nodeType;
         if (node.isAnnotation()) {
             return lineNumbers + " " + node.getDirectFeatureMapping();
         }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/FullNodeFormat.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/FullNodeFormat.java
new file mode 100644
index 000000000..c8e4ee82e
--- /dev/null
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/FullNodeFormat.java
@@ -0,0 +1,45 @@
+package org.variantsync.diffdetective.diff.difftree.serialize.nodeformat;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.variantsync.diffdetective.diff.difftree.DiffNode;
+
+/**
+ * Labels containing all information encoded into a {@link DiffNode}.
+ *
+ * The format consists of the following lines:
+ * <code>
+ * diffType
+ * nodeType
+ * fromLine
+ * toLine
+ * directFeatureMapping
+ * label
+ * </code>.
+ *
+ * @author Benjamin Moosherr
+ */
+public class FullNodeFormat implements DiffNodeLabelFormat {
+    @Override
+    public List<String> toMultilineLabel(final DiffNode node) {
+        List<String> lines = new ArrayList<>();
+
+        lines.add(node.diffType.toString());
+        lines.add(node.nodeType.toString());
+        lines.add(node.getFromLine().toString());
+        lines.add(node.getToLine().toString());
+        lines.add(node.getDirectFeatureMapping() == null ? "" : node.getDirectFeatureMapping().toString());
+        lines.addAll(node.getLines());
+
+        return lines;
+    }
+
+    @Override
+    public String toLabel(final DiffNode node) {
+        return toMultilineLabel(node)
+            .stream()
+            .collect(Collectors.joining(";"));
+    }
+}
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/LineNumberFormat.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/LineNumberFormat.java
index f69618e7a..7c865a1c5 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/LineNumberFormat.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/LineNumberFormat.java
@@ -8,6 +8,6 @@
 public class LineNumberFormat implements DiffNodeLabelFormat {
 	@Override
 	public String toLabel(final DiffNode node) {
-		return String.valueOf(node.getFromLine().inDiff);
+		return String.valueOf(node.getFromLine().inDiff());
 	}
 }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/NaiveMovedArtifactDetection.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/NaiveMovedArtifactDetection.java
index 9882b19ee..492f17be8 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/NaiveMovedArtifactDetection.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/NaiveMovedArtifactDetection.java
@@ -94,8 +94,8 @@ private static DiffNode merge(final DiffNode added, final DiffNode removed) {
         final DiffLineNumber addTo = added.getToLine();
         final DiffLineNumber remTo = removed.getToLine();
 
-        final DiffLineNumber from = new DiffLineNumber(Math.min(addFrom.inDiff, remFrom.inDiff), remFrom.beforeEdit, addFrom.afterEdit);
-        final DiffLineNumber to = new DiffLineNumber(Math.max(addTo.inDiff, remTo.inDiff), remTo.beforeEdit, addTo.afterEdit);
+        final DiffLineNumber from = new DiffLineNumber(Math.min(addFrom.inDiff(), remFrom.inDiff()), remFrom.beforeEdit(), addFrom.afterEdit());
+        final DiffLineNumber to = new DiffLineNumber(Math.max(addTo.inDiff(), remTo.inDiff()), remTo.beforeEdit(), addTo.afterEdit());
 
         return DiffNode.createArtifact(DiffType.NON, from, to, added.getLabel() /* equals removed.getText() */);
     }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/result/CommitDiffResult.java b/src/main/java/org/variantsync/diffdetective/diff/result/CommitDiffResult.java
index e8098bd56..ea5df3a07 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/result/CommitDiffResult.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/result/CommitDiffResult.java
@@ -1,5 +1,6 @@
 package org.variantsync.diffdetective.diff.result;
 
+import org.tinylog.Logger;
 import org.variantsync.diffdetective.diff.CommitDiff;
 
 import java.util.List;
@@ -19,9 +20,10 @@ public record CommitDiffResult(Optional<CommitDiff> diff, List<DiffError> errors
      * @return A failure result that was caused by the given error.
      */
     public static CommitDiffResult Failure(DiffError error, String message) {
+        Logger.debug("{}", message);
         return new CommitDiffResult(
                 Optional.empty(),
-                List.of(DiffResult.Failure(error, message).unwrap().getFailure())
+                List.of(error)
         );
     }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/result/DiffError.java b/src/main/java/org/variantsync/diffdetective/diff/result/DiffError.java
index f057f10a1..02302d64e 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/result/DiffError.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/result/DiffError.java
@@ -1,61 +1,98 @@
 package org.variantsync.diffdetective.diff.result;
 
+import java.util.Arrays;
+import java.util.Optional;
+
 /**
  * Describes an error that occurred when processing a text-based diff.
- * @param id error message
  */
-public record DiffError(String id) {
+public enum DiffError {
     /**
      * A commit has no parents and thus no real diff.
      */
-    public final static DiffError COMMIT_HAS_NO_PARENTS = new DiffError("commit has no parents");
+    COMMIT_HAS_NO_PARENTS("commit has no parents"),
 
     /**
      * Some internal error occurred when operating JGit.
      * The error is not further specified or unknown.
      */
-    public final static DiffError JGIT_ERROR = new DiffError("error when operating jgit");
+    JGIT_ERROR("error when operating jgit"),
 
     /**
      * An error which occurred when obtaining the full diff from a local diff.
      */
-    public final static DiffError COULD_NOT_OBTAIN_FULLDIFF = new DiffError("could not obtain full diff");
+    COULD_NOT_OBTAIN_FULLDIFF("could not obtain full diff"),
 
     /**
      * A preprocessor block was opened but not closed.
      * Typically, this occurs when an #endif is missing.
      */
-    public final static DiffError NOT_ALL_ANNOTATIONS_CLOSED = new DiffError("not all annotations closed");
+    NOT_ALL_ANNOTATIONS_CLOSED("not all annotations closed"),
 
     /**
      * A file or patch contained an expression that closes an annotation block (typically an #endif)
      * but there is no block to close.
      */
-    public final static DiffError ENDIF_WITHOUT_IF = new DiffError("#endif without #if");
+    ENDIF_WITHOUT_IF("#endif without #if"),
 
     /**
      * A multiline macro was defined within a multiline macro.
      */
-    public final static DiffError MLMACRO_WITHIN_MLMACRO = new DiffError("definition of multiline macro within multiline macro");
+    MLMACRO_WITHIN_MLMACRO("definition of multiline macro within multiline macro"),
 
     /**
      * An #else or #elif expression has no corresponding #if (or #ifdef, ...) expression.
      */
-    public final static DiffError ELSE_OR_ELIF_WITHOUT_IF = new DiffError("#else or #elif without #if");
+    ELSE_OR_ELIF_WITHOUT_IF("#else or #elif without #if"),
 
     /**
      * An #else expression is followed by another #else expression which has no semantics.
      */
-    public final static DiffError ELSE_AFTER_ELSE = new DiffError("#else after #else");
+    ELSE_AFTER_ELSE("#else after #else"),
 
     /**
      * A condition annotation is missing an expression.
      * This typically occurs when an #if macro has no arguments.
      */
-    public final static DiffError IF_WITHOUT_CONDITION = new DiffError("conditional macro without expression");
+    IF_WITHOUT_CONDITION("conditional macro without expression"),
+
+    /**
+     * Unknown macro name which was identified as a conditional macro.
+     * Example: {@code #iflol}
+     */
+    INVALID_MACRO_NAME("invalid preprocessor macro name"),
+
+    /**
+     * Empty line in a diff.
+     * All lines in a diff need at least one symbol (the
+     * {@link org.variantsync.diffdetective.diff.difftree.DiffType}) in it.
+     */
+    INVALID_DIFF("missing diff symbol"),
+
+    /**
+     * A line continuation without a following line.
+     */
+    INVALID_LINE_CONTINUATION("a line continuation was detected but there are no more lines");
+
+    private final String message;
+
+    private DiffError(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public static Optional<DiffError> fromMessage(String message) {
+        return Arrays
+            .stream(values())
+            .filter(m -> m.getMessage().equals(message))
+            .findFirst();
+    }
 
     @Override
     public String toString() {
-        return id;
+        return message;
     }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/result/DiffParseException.java b/src/main/java/org/variantsync/diffdetective/diff/result/DiffParseException.java
new file mode 100644
index 000000000..c55ff9a61
--- /dev/null
+++ b/src/main/java/org/variantsync/diffdetective/diff/result/DiffParseException.java
@@ -0,0 +1,34 @@
+package org.variantsync.diffdetective.diff.result;
+
+import org.variantsync.diffdetective.diff.DiffLineNumber;
+
+/**
+ * Describes an error that occurred when processing a text-based diff.
+ * @author Benjamin Moosherr
+ */
+public final class DiffParseException extends Exception {
+    private final DiffError error;
+    private final DiffLineNumber lineNumber;
+
+    /**
+     * @param error the error type to be reported
+     * @param lineNumber the source line of the error
+     */
+    public DiffParseException(DiffError error, DiffLineNumber lineNumber) {
+        this.error = error;
+        this.lineNumber = lineNumber;
+    }
+
+    public DiffError getError() {
+        return error;
+    }
+
+    public DiffLineNumber getLineNumber() {
+        return lineNumber;
+    }
+
+    @Override
+    public String toString() {
+        return "DiffParseException on line " + lineNumber + ": " + error;
+    }
+}
diff --git a/src/main/java/org/variantsync/diffdetective/diff/result/DiffResult.java b/src/main/java/org/variantsync/diffdetective/diff/result/DiffResult.java
deleted file mode 100644
index ce3dfbeb6..000000000
--- a/src/main/java/org/variantsync/diffdetective/diff/result/DiffResult.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package org.variantsync.diffdetective.diff.result;
-
-import org.tinylog.Logger;
-import org.variantsync.diffdetective.diff.difftree.parse.IllFormedAnnotationException;
-import org.variantsync.functjonal.Result;
-
-import java.util.function.Function;
-
-/**
- * The result of processing a diff.
- * The result either contains the desired result or an error message in case an error occurred.
- * @param <T> The type of the result upon success.
- */
-public class DiffResult<T> {
-    private final Result<T, DiffError> result;
-
-    private DiffResult(Result<T, DiffError> result) {
-        this.result = result;
-    }
-
-    /**
-     * Get the internal representation of this result.
-     * @see org.variantsync.functjonal.Result
-     */
-    public Result<T, DiffError> unwrap() {
-        return result;
-    }
-
-    /**
-     * DiffResult is a functor.
-     * Applies the given function to this results value if it has one.
-     * @param f Function to apply to this result's value.
-     * @return A new result with the transformed value.
-     * @param <U> Type to which the value of this result is transformed to by the given function.
-     */
-    public <U> DiffResult<U> map(final Function<T, U> f) {
-        return new DiffResult<>(result.map(f));
-    }
-
-    /// Constructors
-
-    /**
-     * Create a result that indicates success.
-     * The result will hold the given value and no error message.
-     * @param val The result value.
-     * @return A success result.
-     * @param <T> The type of the return value.
-     */
-    public static <T> DiffResult<T> Success(T val) {
-        return new DiffResult<>(Result.Success(val));
-    }
-
-    /**
-     * Create a result that indicates failure.
-     * The result will hold an error message and no value.
-     * @param error The error that occurred.
-     * @return A failure result.
-     * @param <T> The type the result value should have had if no error had been occurred.
-     */
-    public static <T> DiffResult<T> Failure(DiffError error) {
-        return Failure(error, error.id());
-    }
-
-    /**
-     * Convenience constructor to create a result from an {@link IllFormedAnnotationException}.
-     * The produced result will indicate failure with the given error message.
-     * @param e The exception that was thrown when processing a diff.
-     * @return A failure result.
-     * @param <T> The type the result value should have had if no error had been occurred.
-     */
-    public static <T> DiffResult<T> Failure(IllFormedAnnotationException e) {
-        return Failure(e.getType(), e.getMessage());
-    }
-
-
-    /**
-     * Create a result that indicates failure.
-     * The result will hold the given DiffError and no value.
-     * The message will be logged and then discarded.
-     * @param error The error that occurred.
-     * @param message An additional error message that should be logged.
-     * @return A failure result.
-     * @param <T> The type the result value should have had if no error had been occurred.
-     */
-    public static <T> DiffResult<T> Failure(DiffError error, String message) {
-        Logger.debug("[DiffResult::Failure] {}", message);
-        return new DiffResult<>(Result.Failure(error));
-    }
-}
diff --git a/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java b/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java
index eba5170ac..b36706db7 100644
--- a/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java
+++ b/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java
@@ -1,5 +1,7 @@
 package org.variantsync.diffdetective.feature;
 
+import java.util.List;
+
 import org.prop4j.Literal;
 import org.prop4j.Node;
 import org.variantsync.diffdetective.diff.difftree.parse.IllFormedAnnotationException;
@@ -52,4 +54,15 @@ public Node parseDiffLine(String line) throws IllFormedAnnotationException {
 
         return formula;
     }
+
+    public Node parseDiffLines(List<String> lines) throws IllFormedAnnotationException {
+        var logicalLine = new StringBuilder();
+        for (var it = lines.iterator(); it.hasNext(); ) {
+            String physicalLine = it.next();
+            // Remove the backslash of the line continuation
+            logicalLine.append(physicalLine, 0, physicalLine.length() - (it.hasNext() ? 1 : 0));
+        }
+
+        return parseDiffLine(logicalLine.toString());
+    }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/internal/SimpleRenderer.java b/src/main/java/org/variantsync/diffdetective/internal/SimpleRenderer.java
index 766752706..8e4319721 100644
--- a/src/main/java/org/variantsync/diffdetective/internal/SimpleRenderer.java
+++ b/src/main/java/org/variantsync/diffdetective/internal/SimpleRenderer.java
@@ -5,12 +5,13 @@
 import org.variantsync.diffdetective.datasets.Repository;
 import org.variantsync.diffdetective.diff.PatchDiff;
 import org.variantsync.diffdetective.diff.difftree.DiffTree;
-import org.variantsync.diffdetective.diff.difftree.parse.DiffNodeParser;
 import org.variantsync.diffdetective.diff.difftree.parse.DiffTreeParser;
 import org.variantsync.diffdetective.diff.difftree.render.DiffTreeRenderer;
 import org.variantsync.diffdetective.diff.difftree.render.RenderOptions;
 import org.variantsync.diffdetective.diff.difftree.serialize.nodeformat.MappingsDiffNodeFormat;
 import org.variantsync.diffdetective.diff.difftree.transform.DiffTreeTransformer;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
+import org.variantsync.diffdetective.feature.CPPAnnotationParser;
 import org.variantsync.diffdetective.mining.DiffTreeMiner;
 import org.variantsync.diffdetective.mining.RWCompositePatternNodeFormat;
 import org.variantsync.diffdetective.mining.RWCompositePatternTreeFormat;
@@ -97,8 +98,8 @@ private static void render(final Path fileToRender) {
             Logger.info("Rendering {}", fileToRender);
             final DiffTree t;
             try {
-                t = DiffTree.fromFile(fileToRender, collapseMultipleCodeLines, ignoreEmptyLines, DiffNodeParser.Default).unwrap().getSuccess();
-            } catch (IOException e) {
+                t = DiffTree.fromFile(fileToRender, collapseMultipleCodeLines, ignoreEmptyLines, CPPAnnotationParser.Default);
+            } catch (IOException | DiffParseException e) {
                 Logger.error(e, "Could not read given file '{}'", fileToRender);
                 return;
             }
diff --git a/src/main/java/org/variantsync/diffdetective/mining/DiffTreeMiner.java b/src/main/java/org/variantsync/diffdetective/mining/DiffTreeMiner.java
index 7b87a7935..856533c23 100644
--- a/src/main/java/org/variantsync/diffdetective/mining/DiffTreeMiner.java
+++ b/src/main/java/org/variantsync/diffdetective/mining/DiffTreeMiner.java
@@ -11,7 +11,6 @@
 import org.variantsync.diffdetective.datasets.predefined.StanciulescuMarlin;
 import org.variantsync.diffdetective.diff.difftree.filter.DiffTreeFilter;
 import org.variantsync.diffdetective.diff.difftree.filter.ExplainedFilter;
-import org.variantsync.diffdetective.diff.difftree.parse.DiffNodeParser;
 import org.variantsync.diffdetective.diff.difftree.serialize.LineGraphExportOptions;
 import org.variantsync.diffdetective.diff.difftree.serialize.GraphFormat;
 import org.variantsync.diffdetective.diff.difftree.serialize.edgeformat.EdgeLabelFormat;
@@ -20,6 +19,7 @@
 import org.variantsync.diffdetective.diff.difftree.transform.CutNonEditedSubtrees;
 import org.variantsync.diffdetective.diff.difftree.transform.DiffTreeTransformer;
 import org.variantsync.diffdetective.diff.difftree.transform.Starfold;
+import org.variantsync.diffdetective.feature.CPPAnnotationParser;
 import org.variantsync.diffdetective.metadata.ExplainedFilterSummary;
 import org.variantsync.diffdetective.mining.formats.DirectedEdgeLabelFormat;
 import org.variantsync.diffdetective.mining.formats.MiningNodeFormat;
@@ -44,7 +44,7 @@ public static List<DiffTreeTransformer> Postprocessing(final Repository reposito
         final List<DiffTreeTransformer> processing = new ArrayList<>();
         processing.add(new CutNonEditedSubtrees());
         if (SEARCH_FOR_GOOD_RUNNING_EXAMPLES) {
-            processing.add(new RunningExampleFinder(repository == null ? DiffNodeParser.Default : repository.getParseOptions().annotationParser()).
+            processing.add(new RunningExampleFinder(repository == null ? CPPAnnotationParser.Default : repository.getParseOptions().annotationParser()).
                     The_Diff_Itself_Is_A_Valid_DiffTree_And(
                             RunningExampleFinder.DefaultExampleConditions,
                             RunningExampleFinder.DefaultExamplesDirectory.resolve(repository == null ? "unknown" : repository.getRepositoryName())
diff --git a/src/main/java/org/variantsync/diffdetective/mining/RunningExampleFinder.java b/src/main/java/org/variantsync/diffdetective/mining/RunningExampleFinder.java
index 3027ba793..15367bc4d 100644
--- a/src/main/java/org/variantsync/diffdetective/mining/RunningExampleFinder.java
+++ b/src/main/java/org/variantsync/diffdetective/mining/RunningExampleFinder.java
@@ -10,10 +10,10 @@
 import org.variantsync.diffdetective.diff.difftree.filter.DiffTreeFilter;
 import org.variantsync.diffdetective.diff.difftree.filter.ExplainedFilter;
 import org.variantsync.diffdetective.diff.difftree.filter.TaggedPredicate;
-import org.variantsync.diffdetective.diff.difftree.parse.DiffNodeParser;
 import org.variantsync.diffdetective.diff.difftree.render.DiffTreeRenderer;
 import org.variantsync.diffdetective.diff.difftree.transform.ExampleFinder;
-import org.variantsync.diffdetective.diff.result.DiffResult;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
+import org.variantsync.diffdetective.feature.CPPAnnotationParser;
 import org.variantsync.diffdetective.util.Assert;
 
 import java.nio.file.Path;
@@ -33,10 +33,10 @@ public class RunningExampleFinder {
             new TaggedPredicate<>("has a complex formula", RunningExampleFinder::hasAtLeastOneComplexFormulaBeforeTheEdit)
     );
 
-    private final DiffNodeParser nodeParser;
+    private final CPPAnnotationParser annotationParser;
 
-    public RunningExampleFinder(final DiffNodeParser nodeParser) {
-        this.nodeParser = nodeParser;
+    public RunningExampleFinder(final CPPAnnotationParser annotationParser) {
+        this.annotationParser = annotationParser;
     }
 
     public ExampleFinder The_Diff_Itself_Is_A_Valid_DiffTree_And(
@@ -45,22 +45,21 @@ public ExampleFinder The_Diff_Itself_Is_A_Valid_DiffTree_And(
     {
         return new ExampleFinder(
                 diffTree -> {
-                    final String localDiff = getDiff(diffTree);
-                    final DiffResult<DiffTree> parseResult = DiffTree.fromDiff(localDiff, true, true, nodeParser);
-                    // Not every local diff can be parsed to a difftree because diffs are unaware of the underlying language (i.e., CPP).
-                    // We want only running examples whose diffs describe entire diff trees for easier understanding.
-                    return parseResult.unwrap().match(
-                            localTree -> {
-                                if (treeConditions.test(localTree)) {
-                                    Assert.assertTrue(diffTree.getSource() instanceof GitPatch);
-                                    final GitPatch diffTreeSource = (GitPatch) diffTree.getSource();
-                                    localTree.setSource(diffTreeSource.shallowClone());
-                                    return Optional.of(localTree);
-                                }
-                                return Optional.empty();
-                            },
-                            error -> Optional.empty()
-                    );
+                    try {
+                        final String localDiff = getDiff(diffTree);
+                        final DiffTree localTree = DiffTree.fromDiff(localDiff, true, true, annotationParser);
+                        // Not every local diff can be parsed to a difftree because diffs are unaware of the underlying language (i.e., CPP).
+                        // We want only running examples whose diffs describe entire diff trees for easier understanding.
+                        if (treeConditions.test(localTree)) {
+                            Assert.assertTrue(diffTree.getSource() instanceof GitPatch);
+                            final GitPatch diffTreeSource = (GitPatch) diffTree.getSource();
+                            localTree.setSource(diffTreeSource.shallowClone());
+                            return Optional.of(localTree);
+                        }
+                        return Optional.empty();
+                    } catch (DiffParseException e) {
+                        return Optional.empty();
+                    }
                 },
                 exportDirectory,
                 DiffTreeRenderer.WithinDiffDetective()
diff --git a/src/main/resources/tikz_header.tex b/src/main/resources/tikz_header.tex
index 3d021847a..4aba5cbd4 100644
--- a/src/main/resources/tikz_header.tex
+++ b/src/main/resources/tikz_header.tex
@@ -52,7 +52,7 @@
 	node distance = {9.5mm},
 	object/.style = {draw, thick},
 	large/.style = {node distance = 13mm, minimum size=9.5mm},
-	textbox/.style = {draw, rectangle, rounded corners=0.4mm, fill=white, inner sep=2pt, font=\tiny},
+	textbox/.style = {draw, rectangle, rounded corners=0.4mm, fill=white, inner sep=2pt, font=\tiny, align=center},
 	largertextbox/.style = {font=\small},
 	vtdcloud/.style = {cloud, draw, minimum width=22mm, minimum height=15mm}
 }
diff --git a/src/test/java/DiffTreeParserTest.java b/src/test/java/DiffTreeParserTest.java
new file mode 100644
index 000000000..01a9d6803
--- /dev/null
+++ b/src/test/java/DiffTreeParserTest.java
@@ -0,0 +1,97 @@
+import org.variantsync.diffdetective.diff.difftree.DiffTree;
+import org.variantsync.diffdetective.diff.difftree.parse.DiffTreeParser;
+import org.variantsync.diffdetective.diff.difftree.serialize.LineGraphExporter;
+import org.variantsync.diffdetective.diff.difftree.serialize.Format;
+import org.variantsync.diffdetective.diff.difftree.serialize.TikzExporter;
+import org.variantsync.diffdetective.diff.difftree.serialize.edgeformat.ChildOrderEdgeFormat;
+import org.variantsync.diffdetective.diff.difftree.serialize.edgeformat.DefaultEdgeLabelFormat;
+import org.variantsync.diffdetective.diff.difftree.serialize.nodeformat.FullNodeFormat;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
+import org.variantsync.diffdetective.feature.CPPAnnotationParser;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+public class DiffTreeParserTest {
+    private final static Path testDir = Constants.RESOURCE_DIR.resolve("diffs").resolve("parser");
+    private final static String testCaseSuffix = ".diff";
+
+    private static Stream<Path> findTestCases(Path dir) throws IOException {
+        return Files
+            .list(dir)
+            .filter(filename -> filename.getFileName().toString().endsWith(testCaseSuffix));
+    }
+
+    public static Stream<Path> tests() throws IOException {
+        return findTestCases(testDir);
+    }
+
+    public static Stream<Path> wontfixTests() throws IOException {
+        return findTestCases(testDir.resolve("wontfix"));
+    }
+
+    @ParameterizedTest
+    @MethodSource("tests")
+    public void test(Path basename) throws IOException, DiffParseException {
+        testCase(basename);
+    }
+
+    @Disabled("WONTFIX")
+    @ParameterizedTest
+    @MethodSource("wontfixTests")
+    public void wontfixTest(Path testCase) throws IOException, DiffParseException {
+        testCase(testCase);
+    }
+
+    public void testCase(Path testCasePath) throws IOException, DiffParseException {
+        String filename = testCasePath.getFileName().toString();
+        String basename = filename.substring(0, filename.length() - testCaseSuffix.length());
+        var actualPath = testDir.resolve(basename + "_actual.lg");
+        var expectedPath = testDir.resolve(basename + "_expected.lg");
+
+        DiffTree diffTree;
+        try (var inputFile = Files.newBufferedReader(testCasePath)) {
+            diffTree = DiffTreeParser.createDiffTree(
+                inputFile,
+                false,
+                false,
+                CPPAnnotationParser.Default
+            );
+        }
+
+        try (
+                var unbufferedOutput = Files.newOutputStream(actualPath);
+                var output = new BufferedOutputStream(unbufferedOutput)
+        ) {
+            new LineGraphExporter(new Format(new FullNodeFormat(), new ChildOrderEdgeFormat()))
+                .exportDiffTree(diffTree, output);
+        }
+
+        try (
+                var expectedFile = Files.newBufferedReader(expectedPath);
+                var actualFile = Files.newBufferedReader(actualPath);
+        ) {
+            if (!IOUtils.contentEqualsIgnoreEOL(expectedFile, actualFile)) {
+                var visualizationPath = testDir.resolve(basename + ".tex");
+                new TikzExporter(new Format(new FullNodeFormat(), new DefaultEdgeLabelFormat()))
+                    .exportFullLatexExample(diffTree, visualizationPath);
+                fail("The DiffTree in file " + testCasePath + " didn't parse correctly. "
+                    + "Expected the content of " + expectedPath + " but got the content of " + actualPath + ". "
+                    + "Note: A visualisation is available at " + visualizationPath);
+                // Keep output files if the test failed
+            } else {
+                // Delete output files if the test succeeded
+                Files.delete(actualPath);
+            }
+        }
+    }
+}
diff --git a/src/test/java/EditClassesTest.java b/src/test/java/EditClassesTest.java
index 4ec3adc13..992a22468 100644
--- a/src/test/java/EditClassesTest.java
+++ b/src/test/java/EditClassesTest.java
@@ -1,8 +1,10 @@
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 import org.variantsync.diffdetective.diff.difftree.DiffTree;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
 import org.variantsync.diffdetective.editclass.proposed.ProposedEditClasses;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
 import java.io.IOException;
 import java.nio.file.Path;
 
@@ -10,12 +12,12 @@ public class EditClassesTest {
     private final static Path testDir = Constants.RESOURCE_DIR.resolve("patterns");
 
     @Test
-    public void testAtomics() throws IOException {
+    public void testAtomics() throws IOException, DiffParseException {
         final Path path = testDir.resolve("elementary.diff");
-        final DiffTree t = DiffTree.fromFile(path, false, true).unwrap().getSuccess();
+        final DiffTree t = DiffTree.fromFile(path, false, true);
         t.forAll(node -> {
             if (node.isArtifact()) {
-                Assert.assertEquals(
+                assertEquals(
                         node.getLabel(),
                         ProposedEditClasses.Instance.match(node).getName()
                 );
diff --git a/src/test/java/ExportTest.java b/src/test/java/ExportTest.java
index 75f4f916b..15cfef7c8 100644
--- a/src/test/java/ExportTest.java
+++ b/src/test/java/ExportTest.java
@@ -1,5 +1,6 @@
-import org.junit.Test;
-import org.tinylog.Logger;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
 import org.variantsync.diffdetective.diff.difftree.DiffTree;
 import org.variantsync.diffdetective.diff.difftree.serialize.*;
 import org.variantsync.diffdetective.diff.difftree.serialize.edgeformat.DefaultEdgeLabelFormat;
@@ -8,13 +9,17 @@
 import org.variantsync.diffdetective.diff.difftree.serialize.treeformat.CommitDiffDiffTreeLabelFormat;
 import org.variantsync.diffdetective.diff.difftree.serialize.Format;
 
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 
 public class ExportTest {
+    private final static Path RESOURCE_DIR = Path.of("src/test/resources/serialize");
+
     /**
      * Format used for the test export.
      */
@@ -35,26 +40,47 @@ public class ExportTest {
             new DefaultEdgeLabelFormat()
         );
 
+    public static boolean isGraphvizInstalled() throws InterruptedException {
+        try {
+            Process dotProcess = new ProcessBuilder("dot", "-V").start();
+            dotProcess.waitFor();
+            return true;
+        } catch (IOException e) {
+            return false;
+        }
+    }
+
     @Test
+    @EnabledIf("isGraphvizInstalled")
     public void export() throws IOException {
+        var testCasePath = RESOURCE_DIR.resolve("testcase.lg");
+        var actualPath = RESOURCE_DIR.resolve("actual.tex");
+        var expectedPath = RESOURCE_DIR.resolve("expected.tex");
+
         // Deserialize the test case.
-        var testFile = Path.of("src/test/resources/serialize/testcase.lg");
         DiffTree diffTree;
-        try (BufferedReader lineGraph = Files.newBufferedReader(testFile)) {
-            diffTree = LineGraphImport.fromLineGraph(lineGraph, testFile, importOptions).get(0);
+        try (BufferedReader lineGraph = Files.newBufferedReader(testCasePath)) {
+            diffTree = LineGraphImport.fromLineGraph(lineGraph, testCasePath, importOptions).get(0);
         }
 
         // Export the test case
-        var tikzOutput = new ByteArrayOutputStream();
+        try (
+                var unbufferedOutput = Files.newOutputStream(actualPath);
+                var output = new BufferedOutputStream(unbufferedOutput)
+        ) {
+            new TikzExporter(format).exportDiffTree(diffTree, output);
+        }
 
-        try {
-            new TikzExporter(format).exportDiffTree(diffTree, tikzOutput);
-            TestUtils.assertEqualToFile(Path.of("src/test/resources/serialize/expected.tex"), tikzOutput.toString());
-        } catch (IOException e) {
-            if (e.getMessage().contains("Cannot run program")) {
-                Logger.warn("Missing programs! Did you install graphviz? Reason: " + e.getMessage());
+        try (
+                var expectedFile = Files.newBufferedReader(expectedPath);
+                var actualFile = Files.newBufferedReader(actualPath);
+        ) {
+            if (!IOUtils.contentEqualsIgnoreEOL(expectedFile, actualFile)) {
+                fail("The DiffTree in file " + testCasePath + " didn't parse correctly. "
+                    + "Expected the content of " + expectedPath + " but got the content of " + actualPath + ". ");
             } else {
-                throw e;
+                // Keep output file for debugging on errors
+                Files.delete(actualPath);
             }
         }
     }
diff --git a/src/test/java/FeatureIDETest.java b/src/test/java/FeatureIDETest.java
index 719b7b348..1db9dffbf 100644
--- a/src/test/java/FeatureIDETest.java
+++ b/src/test/java/FeatureIDETest.java
@@ -1,9 +1,12 @@
 import de.ovgu.featureide.fm.core.editing.NodeCreator;
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 import org.prop4j.*;
 import org.variantsync.diffdetective.analysis.logic.SAT;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 import java.util.HashMap;
 import java.util.Map;
 
@@ -20,12 +23,12 @@ private static Node createFalse() {
 
     @Test
     public void trueIsTaut() {
-        Assert.assertTrue(SAT.isTautology(createTrue()));
+        assertTrue(SAT.isTautology(createTrue()));
     }
 
     @Test
     public void falseIsContradiction() {
-        Assert.assertFalse(SAT.isSatisfiable(createFalse()));
+        assertFalse(SAT.isSatisfiable(createFalse()));
     }
 
     /**
@@ -36,7 +39,7 @@ public void trueAndA_Equals_A() {
         final Node tru = createTrue();
         final Node a = new Literal("A");
         final Node trueAndA = new And(tru, a);
-        Assert.assertTrue(SAT.equivalent(trueAndA, a));
+        assertTrue(SAT.equivalent(trueAndA, a));
     }
 
     /**
@@ -45,7 +48,7 @@ public void trueAndA_Equals_A() {
     @Test
     public void A_Equals_A() {
         final Node a = new Literal("A");
-        Assert.assertTrue(SAT.equivalent(a, a));
+        assertTrue(SAT.equivalent(a, a));
     }
 
     @Test
@@ -53,7 +56,7 @@ public void falseOrA_Equals_A() {
         final Node no = createFalse();
         final Node a = new Literal("A");
         final Node noOrA = new Or(no, a);
-        Assert.assertTrue(SAT.equivalent(noOrA, a));
+        assertTrue(SAT.equivalent(noOrA, a));
     }
 
     // The following three tests failed and where reported in Issue 1111 (https://github.com/FeatureIDE/FeatureIDE/issues/1111).
@@ -69,8 +72,8 @@ public void atomString() {
 
     @Test
     public void atomValuesEqual() {
-        Assert.assertEquals(createTrue(), new Literal(NodeCreator.varTrue));
-        Assert.assertEquals(createFalse(), new Literal(NodeCreator.varFalse));
+        assertEquals(createTrue(), new Literal(NodeCreator.varTrue));
+        assertEquals(createFalse(), new Literal(NodeCreator.varFalse));
     }
 
     @Test
diff --git a/src/test/java/FixTrueFalseTest.java b/src/test/java/FixTrueFalseTest.java
index 37ccc771d..a5eb8e451 100644
--- a/src/test/java/FixTrueFalseTest.java
+++ b/src/test/java/FixTrueFalseTest.java
@@ -1,11 +1,11 @@
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.prop4j.*;
 import org.variantsync.diffdetective.util.fide.FixTrueFalse;
 
 import java.util.List;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.variantsync.diffdetective.util.fide.FixTrueFalse.False;
 import static org.variantsync.diffdetective.util.fide.FixTrueFalse.True;
 import static org.variantsync.diffdetective.util.fide.FormulaUtils.negate;
@@ -13,16 +13,13 @@
 public class FixTrueFalseTest {
     private record TestCase(Node formula, Node expectedResult) {}
 
-    private List<TestCase> testCases;
-
     private final static Literal A = new Literal("A");
     private final static Literal B = new Literal("B");
     private final static Literal C = new Literal("C");
     private final static Node SomeIrreducible = new And(A, new Implies(A, B));
 
-    @Before
-    public void initTestCases() {
-        testCases = List.of(
+    public static List<TestCase> testCases() {
+        return List.of(
                 new TestCase(new And(True, A), A),
                 new TestCase(new Or(False, A), A),
                 new TestCase(new And(False, A), False),
@@ -37,7 +34,7 @@ public void initTestCases() {
                 new TestCase(new Equals(True, A), A),
                 new TestCase(new Equals(A, False), negate(A)),
                 new TestCase(new Equals(False, A), negate(A)),
-                
+
                 new TestCase(
                         new Equals(
                                 new Or(
@@ -53,10 +50,9 @@ public void initTestCases() {
         );
     }
 
-    @Test
-    public void testAll() {
-        for (TestCase testCase : testCases) {
-            Assert.assertEquals(FixTrueFalse.EliminateTrueAndFalse(testCase.formula), testCase.expectedResult);
-        }
+    @ParameterizedTest
+    @MethodSource("testCases")
+    public void test(TestCase testCase) {
+        assertEquals(FixTrueFalse.EliminateTrueAndFalse(testCase.formula), testCase.expectedResult);
     }
 }
diff --git a/src/test/java/LineGraphTest.java b/src/test/java/LineGraphTest.java
index 6ee2d7cbb..30d681b58 100644
--- a/src/test/java/LineGraphTest.java
+++ b/src/test/java/LineGraphTest.java
@@ -1,8 +1,6 @@
 import org.apache.commons.io.IOUtils;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.tinylog.Logger;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.variantsync.diffdetective.diff.difftree.DiffTree;
 import org.variantsync.diffdetective.diff.difftree.serialize.*;
 import org.variantsync.diffdetective.diff.difftree.serialize.edgeformat.DefaultEdgeLabelFormat;
@@ -10,12 +8,15 @@
 import org.variantsync.diffdetective.diff.difftree.serialize.treeformat.CommitDiffDiffTreeLabelFormat;
 import org.variantsync.diffdetective.util.IO;
 
+import static org.junit.jupiter.api.Assertions.fail;
+
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
+import java.util.stream.Stream;
 
 /**
  * For testing the import of a line graph.
@@ -31,45 +32,40 @@ public class LineGraphTest {
             IMPORT_OPTIONS
     );
 
-    private static List<Path> TEST_FILES;
-
-    @BeforeClass
-    public static void init() throws IOException {
-        TEST_FILES = Files.list(Paths.get("src/test/resources/line_graph")).toList();
+    public static Stream<Path> testCases() throws IOException {
+        return Files.list(Paths.get("src/test/resources/line_graph"));
     }
 
-	/**
-	 * Test the import of a line graph.
-	 */
-	@Test
-	public void idempotentReadWrite() throws IOException {
-        for (final Path testFile : TEST_FILES) {
-            Logger.info("Testing {}", testFile);
-            List<DiffTree> diffTrees;
-            try (BufferedReader lineGraph = Files.newBufferedReader(testFile)) {
-                diffTrees = LineGraphImport.fromLineGraph(lineGraph, testFile, IMPORT_OPTIONS);
-            }
-            assertConsistencyForAll(diffTrees);
+    /**
+     * Test the import of a line graph.
+     */
+    @ParameterizedTest
+    @MethodSource("testCases")
+    public void idempotentReadWrite(Path testFile) throws IOException {
+        List<DiffTree> diffTrees;
+        try (BufferedReader lineGraph = Files.newBufferedReader(testFile)) {
+            diffTrees = LineGraphImport.fromLineGraph(lineGraph, testFile, IMPORT_OPTIONS);
+        }
+        assertConsistencyForAll(diffTrees);
 
-            Path actualPath = testFile.getParent().resolve(testFile.getFileName().toString() + ".actual");
-            try (var output = IO.newBufferedOutputStream(actualPath)) {
-                LineGraphExport.toLineGraphFormat(diffTrees, EXPORT_OPTIONS, output);
-            }
+        Path actualPath = testFile.getParent().resolve(testFile.getFileName().toString() + ".actual");
+        try (var output = IO.newBufferedOutputStream(actualPath)) {
+            LineGraphExport.toLineGraphFormat(diffTrees, EXPORT_OPTIONS, output);
+        }
 
-            try (
-                    var expectedFile = Files.newBufferedReader(testFile);
-                    var actualFile = Files.newBufferedReader(actualPath);
-            ) {
-                if (!IOUtils.contentEqualsIgnoreEOL(expectedFile, actualFile)) {
-                    Assert.fail("The file " + testFile + " couldn't be exported or imported without modifications");
-                } else {
-                    // Only keep output file on errors
-                    Files.delete(actualPath);
-                }
+        try (
+                var expectedFile = Files.newBufferedReader(testFile);
+                var actualFile = Files.newBufferedReader(actualPath);
+        ) {
+            if (!IOUtils.contentEqualsIgnoreEOL(expectedFile, actualFile)) {
+                fail("The file " + testFile + " couldn't be exported or imported without modifications");
+            } else {
+                // Only keep output file on errors
+                Files.delete(actualPath);
             }
         }
-	}
-	
+    }
+
 	/**
 	 * Check consistency of {@link DiffTree DiffTrees}.
 	 * 
diff --git a/src/test/java/LinuxParsingTest.java b/src/test/java/LinuxParsingTest.java
index 9bad73fe7..90303f651 100644
--- a/src/test/java/LinuxParsingTest.java
+++ b/src/test/java/LinuxParsingTest.java
@@ -1,8 +1,7 @@
 import org.variantsync.diffdetective.diff.difftree.DiffTree;
 import org.variantsync.diffdetective.diff.difftree.render.DiffTreeRenderer;
-import org.variantsync.diffdetective.diff.result.DiffError;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
 import org.variantsync.diffdetective.util.Assert;
-import org.variantsync.functjonal.Result;
 
 import java.io.IOException;
 import java.nio.file.Path;
@@ -12,17 +11,11 @@ public class LinuxParsingTest {
     private final static Path testDir = Constants.RESOURCE_DIR.resolve("linux");
 
 //    @Test
-    public void test1() throws IOException {
+    public void test1() throws IOException, DiffParseException {
         final String testFilename = "test1.diff";
         final Path path = testDir.resolve(testFilename);
-        final Result<DiffTree, DiffError> parseResult =
-                DiffTree.fromFile(path, false, true).unwrap();
+        final DiffTree t = DiffTree.fromFile(path, false, true);
 
-        if (parseResult.isFailure()) {
-            throw new AssertionError("Could not parse " + path + " because " + parseResult.getFailure());
-        }
-
-        final DiffTree t = parseResult.getSuccess();
 //        new FeatureExpressionFilter(LinuxKernel::isFeature).transform(t);
 
         t.forAll(n -> {
diff --git a/src/test/java/MarlinDebug.java b/src/test/java/MarlinDebug.java
index 4936e4619..9357e7f78 100644
--- a/src/test/java/MarlinDebug.java
+++ b/src/test/java/MarlinDebug.java
@@ -2,9 +2,9 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
 import org.prop4j.Node;
 import org.tinylog.Logger;
 import org.variantsync.diffdetective.datasets.DatasetDescription;
@@ -24,12 +24,15 @@
 import org.variantsync.diffdetective.util.Clock;
 import org.variantsync.diffdetective.validation.Validation;
 
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
 
 @Deprecated
+@Disabled
 public class MarlinDebug {
     private record RepoInspection(
             List<String> suspiciousCommits,
@@ -42,7 +45,7 @@ private record RepoInspection(
 
     private static RepoInspection MARLIN, PHP;
 
-    @Before
+    @BeforeEach
     public void init() {
         {
             DatasetDescription marlin = new DatasetDescription(
@@ -126,7 +129,7 @@ public static void testCommit(final RepoInspection repoInspection, final String
 
     public static void asMiningTask(final RepoInspection repoInspection, final String commitHash) throws Exception {
         final Git git = repoInspection.repo.getGitRepo().run();
-        Assert.assertNotNull(git);
+        assertNotNull(git);
         final RevWalk revWalk = new RevWalk(git.getRepository());
         final RevCommit childCommit = revWalk.parseCommit(ObjectId.fromString(commitHash));
 
@@ -140,7 +143,7 @@ public static void asMiningTask(final RepoInspection repoInspection, final Strin
 
     public static void asValidationTask(final RepoInspection repoInspection, final String commitHash) throws Exception {
         final Git git = repoInspection.repo.getGitRepo().run();
-        Assert.assertNotNull(git);
+        assertNotNull(git);
         final RevWalk revWalk = new RevWalk(git.getRepository());
         final RevCommit childCommit = revWalk.parseCommit(ObjectId.fromString(commitHash));
 
diff --git a/src/test/java/MoveDetectionTest.java b/src/test/java/MoveDetectionTest.java
index 2ca98a82d..cd89c95ef 100644
--- a/src/test/java/MoveDetectionTest.java
+++ b/src/test/java/MoveDetectionTest.java
@@ -1,6 +1,7 @@
 import org.variantsync.diffdetective.diff.difftree.DiffTree;
 import org.variantsync.diffdetective.diff.difftree.render.DiffTreeRenderer;
 import org.variantsync.diffdetective.diff.difftree.transform.NaiveMovedArtifactDetection;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
 
 import java.io.IOException;
 import java.nio.file.Path;
@@ -10,8 +11,8 @@ public class MoveDetectionTest {
     private static final Path genDir = resDir.resolve("gen");
 
 //    @Test
-    public void simpleTest() throws IOException {
-        final DiffTree t = DiffTree.fromFile(resDir.resolve("simple.txt"), true, true).unwrap().getSuccess();
+    public void simpleTest() throws IOException, DiffParseException {
+        final DiffTree t = DiffTree.fromFile(resDir.resolve("simple.txt"), true, true);
         final DiffTreeRenderer renderer = DiffTreeRenderer.WithinDiffDetective();
         renderer.render(t, "MoveDetectionTestSimpleTest_Before", genDir);
         new NaiveMovedArtifactDetection().transform(t);
diff --git a/src/test/java/PCTest.java b/src/test/java/PCTest.java
index f19f33231..cb5c3d0b7 100644
--- a/src/test/java/PCTest.java
+++ b/src/test/java/PCTest.java
@@ -1,16 +1,19 @@
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.prop4j.And;
 import org.prop4j.Literal;
 import org.prop4j.Node;
 import org.tinylog.Logger;
 import org.variantsync.diffdetective.analysis.logic.SAT;
 import org.variantsync.diffdetective.diff.difftree.DiffTree;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
 
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.List;
 import java.util.Map;
 
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.variantsync.diffdetective.util.fide.FormulaUtils.negate;
 
 public class PCTest {
@@ -20,7 +23,12 @@ public class PCTest {
     private static final Literal D = new Literal("D");
     private static final Literal E = new Literal("E");
     record ExpectedPC(Node before, Node after) {}
-    record TestCase(Path file, Map<String, ExpectedPC> expectedResult) {}
+    record TestCase(Path file, Map<String, ExpectedPC> expectedResult) {
+        @Override
+        public String toString() {
+            return file.toString();
+        }
+    }
 
     private final static Path testDir = Constants.RESOURCE_DIR.resolve("pctest");
     private final static TestCase a = new TestCase(
@@ -53,41 +61,32 @@ private static String errorAt(final String node, String time, Node is, Node shou
         return time + " PC of node \"" + node + "\" is \"" + is + "\" but expected \"" + should + "\"!";
     }
 
-    private void test(final TestCase testCase) throws IOException {
+    public static List<TestCase> testCases() {
+        return List.of(a, elif, elze);
+    }
+
+    @ParameterizedTest
+    @MethodSource("testCases")
+    public void test(final TestCase testCase) throws IOException, DiffParseException {
         final Path path = testDir.resolve(testCase.file);
-        final DiffTree t = DiffTree.fromFile(path, false, true).unwrap().getSuccess();
+        final DiffTree t = DiffTree.fromFile(path, false, true);
         t.forAll(node -> {
            if (node.isArtifact()) {
                final String text = node.getLabel().trim();
                final ExpectedPC expectedPC = testCase.expectedResult.getOrDefault(text, null);
                if (expectedPC != null) {
                    Node pc = node.getBeforePresenceCondition();
-                   Assert.assertTrue(
-                           errorAt(text, "before", pc, expectedPC.before),
-                           SAT.equivalent(pc, expectedPC.before));
+                   assertTrue(
+                           SAT.equivalent(pc, expectedPC.before),
+                           errorAt(text, "before", pc, expectedPC.before));
                    pc = node.getAfterPresenceCondition();
-                   Assert.assertTrue(
-                           errorAt(text, "after", pc, expectedPC.after),
-                           SAT.equivalent(pc, expectedPC.after));
+                   assertTrue(
+                           SAT.equivalent(pc, expectedPC.after),
+                           errorAt(text, "after", pc, expectedPC.after));
                } else {
                    Logger.warn("No expected PC specified for node '{}'!", text);
                }
            }
         });
     }
-
-    @Test
-    public void testA() throws IOException {
-        test(a);
-    }
-
-    @Test
-    public void testElif() throws IOException {
-        test(elif);
-    }
-
-    @Test
-    public void testElse() throws IOException {
-        test(elze);
-    }
 }
diff --git a/src/test/java/PrintWorkingTreeDiff.java b/src/test/java/PrintWorkingTreeDiff.java
index 24d5b2334..5c1565b84 100644
--- a/src/test/java/PrintWorkingTreeDiff.java
+++ b/src/test/java/PrintWorkingTreeDiff.java
@@ -1,15 +1,14 @@
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.NoHeadException;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
 import org.variantsync.diffdetective.datasets.ParseOptions.DiffStoragePolicy;
 import org.variantsync.diffdetective.datasets.Repository;
 import org.variantsync.diffdetective.diff.CommitDiff;
 import org.variantsync.diffdetective.diff.GitDiffer;
 import org.variantsync.diffdetective.diff.PatchDiff;
 import org.variantsync.diffdetective.diff.result.CommitDiffResult;
-import org.variantsync.diffdetective.util.IO;
 
 import java.io.IOException;
 import java.nio.file.Path;
@@ -18,6 +17,7 @@
 /**
  * Perform "git diff" on a git repository.
  */
+@Disabled
 public class PrintWorkingTreeDiff {
 	
 	@Test
diff --git a/src/test/java/StarfoldTest.java b/src/test/java/StarfoldTest.java
index 855b43583..c4b8f1831 100644
--- a/src/test/java/StarfoldTest.java
+++ b/src/test/java/StarfoldTest.java
@@ -1,51 +1,49 @@
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.variantsync.diffdetective.diff.difftree.DiffTree;
 import org.variantsync.diffdetective.diff.difftree.transform.Starfold;
-import org.variantsync.diffdetective.util.FileUtils;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
 
 import java.io.IOException;
-import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.List;
-import java.util.function.Function;
 import java.util.stream.Stream;
 
 public class StarfoldTest {
-    private static final Path RESOURCE_DIR = Constants.RESOURCE_DIR.resolve("starfold");
+    private static final Path INPUT_DIR = Constants.RESOURCE_DIR.resolve("starfold");
+    private static final Path EXPECTED_DIR = INPUT_DIR.resolve("expected");
 
-    private record TestCase(Path inputDiff, Path expectedDiffRespectingNodeOrder, Path expectedDiffIgnoringNodeOrder) {
-        TestCase(final String filename) {
-            this(
-                    RESOURCE_DIR.resolve(filename),
-                    RESOURCE_DIR.resolve("expected").resolve("respectNodeOrder").resolve(filename),
-                    RESOURCE_DIR.resolve("expected").resolve("ignoreNodeOrder").resolve(filename)
-            );
-        }
+    public static Stream<String> testCases() {
+        return Stream
+            .of("2x2", "nesting1")
+            .map(s -> s + ".diff");
     }
 
-    private static final List<TestCase> TEST_CASES = Stream.of(
-            "2x2", "nesting1"
-    ).map(s -> s + ".diff").map(TestCase::new).toList();
-
-    private void test(final Starfold starfold, Function<TestCase, Path> getExpectedResultFile) throws IOException {
-        for (TestCase testCase : TEST_CASES) {
-            final DiffTree t = DiffTree.fromFile(testCase.inputDiff, true, true).unwrap().getSuccess();
-            starfold.transform(t);
-            TestUtils.assertEqualToFile(
-                    getExpectedResultFile.apply(testCase),
-                    t.toTextDiff().trim()
-            );
-        }
+    private void test(Path inputDiff, final Starfold starfold, Path expected) throws IOException, DiffParseException {
+        final DiffTree t = DiffTree.fromFile(inputDiff, true, true);
+        starfold.transform(t);
+        TestUtils.assertEqualToFile(
+                expected,
+                t.toTextDiff().trim()
+        );
     }
 
-    @Test
-    public void testRespectNodeOrder() throws IOException {
-        test(Starfold.RespectNodeOrder(), TestCase::expectedDiffRespectingNodeOrder);
+    @ParameterizedTest
+    @MethodSource("testCases")
+    public void testRespectNodeOrder(String filename) throws IOException, DiffParseException {
+        test(
+            INPUT_DIR.resolve(filename),
+            Starfold.RespectNodeOrder(),
+            EXPECTED_DIR.resolve("respectNodeOrder").resolve(filename)
+        );
     }
 
-    @Test
-    public void testIgnoreNodeOrder() throws IOException {
-        test(Starfold.IgnoreNodeOrder(), TestCase::expectedDiffIgnoringNodeOrder);
+    @ParameterizedTest
+    @MethodSource("testCases")
+    public void testIgnoreNodeOrder(String filename) throws IOException, DiffParseException {
+        test(
+            INPUT_DIR.resolve(filename),
+            Starfold.IgnoreNodeOrder(),
+            EXPECTED_DIR.resolve("ignoreNodeOrder").resolve(filename)
+        );
     }
 }
diff --git a/src/test/java/TestLineNumbers.java b/src/test/java/TestLineNumbers.java
index e692cd86e..7bcb95552 100644
--- a/src/test/java/TestLineNumbers.java
+++ b/src/test/java/TestLineNumbers.java
@@ -1,10 +1,12 @@
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.variantsync.diffdetective.diff.DiffLineNumber;
 import org.variantsync.diffdetective.diff.difftree.DiffTree;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
 import org.variantsync.functjonal.Pair;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.HashMap;
@@ -15,10 +17,8 @@
 public class TestLineNumbers {
     private static final Path resDir = Constants.RESOURCE_DIR.resolve("diffs/linenumbers");
     private record TestCase(String filename, Map<Integer, Pair<DiffLineNumber, DiffLineNumber>> expectedLineNumbers) { }
-    private List<TestCase> testCases;
 
-    @Before
-    public void initTestCases() {
+    public static List<TestCase> testCases() {
         // Testcases rely on stability of IDs
 
         final var elifchain_map = new HashMap<Integer, Pair<DiffLineNumber, DiffLineNumber>>();
@@ -56,11 +56,11 @@ public void initTestCases() {
         deleteMLM_map.put(267, new Pair<>(new DiffLineNumber(3, 3, -1), new DiffLineNumber(4, 4, -1)));
         TestCase deleteMLM = new TestCase("deleteMLM.txt", deleteMLM_map);
 
-        testCases = List.of(elifchain, lineno1, deleteMLM);
+        return List.of(elifchain, lineno1, deleteMLM);
     }
 
-    private static DiffTree loadFullDiff(final Path p) throws IOException {
-        return DiffTree.fromFile(p, false, false).unwrap().getSuccess();
+    private static DiffTree loadFullDiff(final Path p) throws IOException, DiffParseException {
+        return DiffTree.fromFile(p, false, false);
     }
 
     private static void printLineNumbers(final DiffTree diffTree) {
@@ -76,10 +76,10 @@ private static void printLineNumbers(final DiffTree diffTree) {
         System.out.println();
     }
 
-    private static String generateTestCaseCode(final Path p) throws IOException {
+    private static String generateTestCaseCode(final Path p) throws IOException, DiffParseException {
         final DiffTree diffTree = loadFullDiff(p);
         final Function<DiffLineNumber, String> toConstructorCall = l ->
-                "new DiffLineNumber(" + l.inDiff + ", " + l.beforeEdit + ", " + l.afterEdit + ")";
+                "new DiffLineNumber(" + l.inDiff() + ", " + l.beforeEdit() + ", " + l.afterEdit() + ")";
         String testName = p.getFileName().toString();
         testName = testName.substring(0, testName.lastIndexOf("."));
         String mapName = testName + "_map";
@@ -99,10 +99,10 @@ private static String generateTestCaseCode(final Path p) throws IOException {
     }
 
 //    @Test
-    public void generateTestCode() throws IOException {
+    public void generateTestCode() throws IOException, DiffParseException {
         final StringBuilder listof = new StringBuilder("List.of(");
         boolean first = true;
-        for (final TestCase s : testCases) {
+        for (final TestCase s : testCases()) {
             if (first) {
                 first = false;
             } else {
@@ -114,25 +114,23 @@ public void generateTestCode() throws IOException {
         System.out.println("testCases = " + listof);
     }
 
-//    @Test
-    public void printLineNumbers() throws IOException {
-        for (final TestCase s : testCases) {
-            System.out.println("Diff of " + s.filename());
-            printLineNumbers(loadFullDiff(resDir.resolve(s.filename())));
-        }
+//    @ParameterizedTest
+    @MethodSource("testCases")
+    public void printLineNumbers(TestCase testCase) throws IOException, DiffParseException {
+        System.out.println("Diff of " + testCase.filename());
+        printLineNumbers(loadFullDiff(resDir.resolve(testCase.filename())));
     }
 
-    @Test
-    public void testLineNumbers() throws IOException {
-        for (final TestCase s : testCases) {
-            final DiffTree t = loadFullDiff(resDir.resolve(s.filename()));
-            t.forAll(node -> {
-                var fromTo = s.expectedLineNumbers.get(node.getID());
-                final DiffLineNumber from = fromTo.first();
-                final DiffLineNumber to = fromTo.second();
-                Assert.assertEquals(from, node.getFromLine());
-                Assert.assertEquals(to, node.getToLine());
-            });
-        }
+    @ParameterizedTest
+    @MethodSource("testCases")
+    public void testLineNumbers(TestCase testCase) throws IOException, DiffParseException {
+        final DiffTree t = loadFullDiff(resDir.resolve(testCase.filename()));
+        t.forAll(node -> {
+            var fromTo = testCase.expectedLineNumbers.get(node.getID());
+            final DiffLineNumber from = fromTo.first();
+            final DiffLineNumber to = fromTo.second();
+            assertEquals(from, node.getFromLine());
+            assertEquals(to, node.getToLine());
+        });
     }
 }
diff --git a/src/test/java/TestMultiLineMacros.java b/src/test/java/TestMultiLineMacros.java
index b8941829a..70b90000d 100644
--- a/src/test/java/TestMultiLineMacros.java
+++ b/src/test/java/TestMultiLineMacros.java
@@ -1,8 +1,8 @@
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.tinylog.Logger;
 import org.variantsync.diffdetective.diff.difftree.DiffTree;
-import org.variantsync.diffdetective.diff.difftree.parse.DiffNodeParser;
+import org.variantsync.diffdetective.feature.CPPAnnotationParser;
 import org.variantsync.diffdetective.diff.difftree.parse.DiffTreeParser;
 import org.variantsync.diffdetective.diff.difftree.serialize.LineGraphExportOptions;
 import org.variantsync.diffdetective.diff.difftree.serialize.DiffTreeSerializeDebugData;
@@ -11,9 +11,12 @@
 import org.variantsync.diffdetective.diff.difftree.serialize.edgeformat.DefaultEdgeLabelFormat;
 import org.variantsync.diffdetective.diff.difftree.serialize.nodeformat.DebugDiffNodeFormat;
 import org.variantsync.diffdetective.diff.difftree.serialize.treeformat.CommitDiffDiffTreeLabelFormat;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
 import org.variantsync.diffdetective.util.IO;
 import org.variantsync.diffdetective.util.StringUtils;
 
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -22,22 +25,21 @@
 public class TestMultiLineMacros {
     private static final Path resDir = Constants.RESOURCE_DIR.resolve("multilinemacros");
 
-    public void diffToDiffTree(LineGraphExportOptions exportOptions, Path p) throws IOException {
+    public void diffToDiffTree(LineGraphExportOptions exportOptions, Path p) throws IOException, DiffParseException {
         DiffTree tree;
         try (BufferedReader fullDiff = Files.newBufferedReader(p)) {
             tree = DiffTreeParser.createDiffTree(
                     fullDiff,
                     true,
-                    true,
-                    DiffNodeParser.Default).unwrap().getSuccess();
+                    false,
+                    CPPAnnotationParser.Default);
         }
 
         try (var destination = IO.newBufferedOutputStream(resDir.resolve("gen").resolve(p.getFileName() + ".lg"))) {
             destination.write(("t # 1" + StringUtils.LINEBREAK).getBytes());
 
-
             final DiffTreeSerializeDebugData debugData = LineGraphExport.toLineGraphFormat(tree, exportOptions, destination);
-            Assert.assertNotNull(debugData);
+            assertNotNull(debugData);
             Logger.info("Parsed {} nodes of diff type NON.", debugData.numExportedNonNodes);
             Logger.info("Parsed {} nodes of diff type ADD.", debugData.numExportedAddNodes);
             Logger.info("Parsed {} nodes of diff type REM.", debugData.numExportedRemNodes);
@@ -45,8 +47,9 @@ public void diffToDiffTree(LineGraphExportOptions exportOptions, Path p) throws
         }
     }
 
-    @Test
-    public void test() throws IOException {
+    @ParameterizedTest
+    @ValueSource(strings = { "mldiff1.txt", "diffWithComments.txt" })
+    public void test(String filename) throws IOException, DiffParseException {
         final LineGraphExportOptions exportOptions = new LineGraphExportOptions(
                 GraphFormat.DIFFTREE,
                 new CommitDiffDiffTreeLabelFormat(),
@@ -54,7 +57,6 @@ public void test() throws IOException {
                 new DefaultEdgeLabelFormat()
         );
 
-        diffToDiffTree(exportOptions, resDir.resolve("mldiff1.txt"));
-        diffToDiffTree(exportOptions, resDir.resolve("diffWithComments.txt"));
+        diffToDiffTree(exportOptions, resDir.resolve(filename));
     }
 }
diff --git a/src/test/java/TestUtils.java b/src/test/java/TestUtils.java
index f4565f8bd..6f6593ed6 100644
--- a/src/test/java/TestUtils.java
+++ b/src/test/java/TestUtils.java
@@ -1,12 +1,13 @@
 import org.apache.commons.io.IOUtils;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
 import java.nio.file.Files;
 import java.nio.file.Path;
 
-import static org.junit.Assert.assertTrue;
-
 public final class TestUtils {
     /**
      * Compare two line graphs.
@@ -16,8 +17,9 @@ public final class TestUtils {
      */
     public static void assertEqualToFile(final Path filePath, final String actual) throws IOException {
         try (BufferedReader expected = Files.newBufferedReader(filePath)) {
-            assertTrue("expected content of " + filePath + " but was:<" + actual + ">",
-                    IOUtils.contentEqualsIgnoreEOL(expected, new StringReader(actual)));
+            assertTrue(
+                    IOUtils.contentEqualsIgnoreEOL(expected, new StringReader(actual)),
+                    "expected content of " + filePath + " but was:<" + actual + ">");
         }
     }
 }
diff --git a/src/test/java/TreeTransformersTest.java b/src/test/java/TreeTransformersTest.java
index da1c7cb8f..6004e3abe 100644
--- a/src/test/java/TreeTransformersTest.java
+++ b/src/test/java/TreeTransformersTest.java
@@ -1,6 +1,6 @@
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Ignore;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
 import org.variantsync.diffdetective.datasets.Repository;
 import org.variantsync.diffdetective.datasets.predefined.StanciulescuMarlin;
 import org.variantsync.diffdetective.diff.PatchDiff;
@@ -14,13 +14,17 @@
 import org.variantsync.diffdetective.diff.difftree.serialize.nodeformat.TypeDiffNodeFormat;
 import org.variantsync.diffdetective.diff.difftree.serialize.treeformat.CommitDiffDiffTreeLabelFormat;
 import org.variantsync.diffdetective.diff.difftree.transform.DiffTreeTransformer;
+import org.variantsync.diffdetective.diff.result.DiffParseException;
 import org.variantsync.diffdetective.mining.DiffTreeMiner;
 
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.List;
 import java.util.function.Consumer;
 
+@Disabled
 public class TreeTransformersTest {
     private static final boolean RENDER = true;
     private static final Path resDir = Constants.RESOURCE_DIR.resolve("diffs/collapse");
@@ -42,8 +46,8 @@ public class TreeTransformersTest {
 
     private static final Consumer<String> INFO = System.out::println;
 
-    private void transformAndRender(String diffFileName) throws IOException {
-        final DiffTree t = DiffTree.fromFile(resDir.resolve(diffFileName), true, true).unwrap().getSuccess();
+    private void transformAndRender(String diffFileName) throws IOException, DiffParseException {
+        final DiffTree t = DiffTree.fromFile(resDir.resolve(diffFileName), true, true);
         transformAndRender(t, diffFileName, "0", null);
     }
 
@@ -78,63 +82,55 @@ private void transformAndRender(DiffTree diffTree, String name, String commit, R
         }
     }
 
-    @Before
+    @BeforeEach
     public void init() {
 //        Main.setupLogger(Level.INFO);
 //        DiffTreeTransformer.checkDependencies(transformers);
     }
 
-//    @Test
-    @Ignore
-    public void simpleTest() throws IOException {
+    @Test
+    public void simpleTest() throws IOException, DiffParseException {
         transformAndRender("simple.txt");
     }
 
-//    @Test
-    @Ignore
-    public void elifTest() throws IOException {
+    @Test
+    public void elifTest() throws IOException, DiffParseException {
         transformAndRender("elif.txt");
     }
 
     private void testCommit(String file, String commitHash) throws IOException {
         final Repository marlin = StanciulescuMarlin.fromZipInDiffDetectiveAt(Path.of("."));
         final PatchDiff patch = DiffTreeParser.parsePatch(marlin, file, commitHash);
-        Assert.assertNotNull(patch);
+        assertNotNull(patch);
         transformAndRender(patch.getDiffTree(), file, commitHash, marlin);
     }
 
-//    @Test
-    @Ignore
+    @Test
     public void testWurmcoil() throws IOException {
         testCommit("Marlin/pins.h", "d6d6fb8930be8d0b3bd34592c915732937c6f4d9");
     }
 
-//    @Test
-    @Ignore
+    @Test
     public void testConfiguration_adv() throws IOException {
         testCommit("Marlin/example_configurations/RigidBot/Configuration_adv.h", "d3fe3a0962fdbdcd9548abaf765e0cff72d9cf8d");
     }
 
-//    @Test
-    @Ignore
+    @Test
     public void test_pins_SANGUINOLOLU_11() throws IOException {
         testCommit("Marlin/pins_SANGUINOLOLU_11.h", "d3fe3a0962fdbdcd9548abaf765e0cff72d9cf8d");
     }
 
-//    @Test
-    @Ignore
+    @Test
     public void test_pins_RAMPS_13() throws IOException {
         testCommit("Marlin/pins_RAMPS_13.h", "d882e1aee7fb4e4afb43445899b477caf1fffce3");
     }
 
-//    @Test
-    @Ignore
+    @Test
     public void test_SanityCheck() throws IOException {
         testCommit("Marlin/SanityCheck.h", "cbd582865e2a76b7be3b03533a0e06e8daf76f15");
     }
 
-//    @Test
-    @Ignore
+    @Test
     public void test_pins_MINIRAMBO() throws IOException {
         testCommit("Marlin/pins_MINIRAMBO.h", "50f1a8fd92b351bf1fa29e5cd31f24fc884999c0");
     }
diff --git a/src/test/java/TseytinTest.java b/src/test/java/TseytinTest.java
index 26cf9e011..71775ca29 100644
--- a/src/test/java/TseytinTest.java
+++ b/src/test/java/TseytinTest.java
@@ -1,6 +1,5 @@
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.prop4j.*;
 import org.variantsync.diffdetective.analysis.logic.SAT;
 import org.variantsync.diffdetective.analysis.logic.Tseytin;
@@ -9,34 +8,33 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.variantsync.diffdetective.util.fide.FormulaUtils.negate;
 
 public class TseytinTest {
     private static final Literal A = new Literal("A");
     private static final Literal B = new Literal("B");
-    private static final Literal C = new Literal("C");
-    private static final Literal D = new Literal("D");
 
-    private static List<Node> tautologyTestCases;
-    private static List<Node> contradictoryTestCases;
-    private static List<Node> satisfiableTestCases;
-
-    @BeforeClass
-    public static void setupTestCases() {
-        tautologyTestCases = List.of(
+    public static List<Node> tautologyTestCases() {
+        return List.of(
                 FixTrueFalse.True,
                 negate(FixTrueFalse.False),
                 new Or(A, negate(A)),
                 new Implies(new And(A, new Implies(A, B)), B) // modus ponens
         );
+    }
 
-        contradictoryTestCases = List.of(
+    public static List<Node> contradictoryTestCases() {
+        return List.of(
                 FixTrueFalse.False,
                 negate(FixTrueFalse.True),
                 new And(A, negate(A))
         );
+    }
 
-        satisfiableTestCases = new ArrayList<>(List.of(
+    public static List<Node> satisfiableTestCases() {
+       var satisfiableTestCases = new ArrayList<>(List.of(
                 A,
                 negate(A),
                 negate(new And(A, B)),
@@ -45,37 +43,35 @@ public static void setupTestCases() {
                 new Implies(A, B),
                 new Equals(A, B)
         ));
-        satisfiableTestCases.addAll(tautologyTestCases);
+        satisfiableTestCases.addAll(tautologyTestCases());
+        return satisfiableTestCases;
     }
 
-    @Test
-    public void testSAT() {
-        for (final Node formula : satisfiableTestCases) {
-            Assert.assertTrue(formula.toString(), SAT.isSatisfiableNoTseytin(formula));
-            Assert.assertTrue(formula.toString(), SAT.isSatisfiableAlwaysTseytin(formula));
-        }
+    @ParameterizedTest
+    @MethodSource("satisfiableTestCases")
+    public void testSAT(Node formula) {
+        assertTrue(SAT.isSatisfiableNoTseytin(formula), formula.toString());
+        assertTrue(SAT.isSatisfiableAlwaysTseytin(formula), formula.toString());
     }
 
-    @Test
-    public void testTAUT() {
-        for (final Node formula : tautologyTestCases) {
-            final Node no = negate(formula);
-            Assert.assertFalse(
-                    no.toString(),
-                    SAT.isSatisfiableNoTseytin(no)
-            );
-            Assert.assertFalse(
-                    "Expected SAT(tseytin(" + no + ")) = SAT(" + Tseytin.toEquisatisfiableCNF(no) + ") = false but got true.",
-                    SAT.isSatisfiableAlwaysTseytin(no)
-            );
-        }
+    @ParameterizedTest
+    @MethodSource("tautologyTestCases")
+    public void testTAUT(Node formula) {
+        final Node no = negate(formula);
+        assertFalse(
+                SAT.isSatisfiableNoTseytin(no),
+                no.toString()
+        );
+        assertFalse(
+                SAT.isSatisfiableAlwaysTseytin(no),
+                "Expected SAT(tseytin(" + no + ")) = SAT(" + Tseytin.toEquisatisfiableCNF(no) + ") = false but got true."
+        );
     }
 
-    @Test
-    public void testContradictions() {
-        for (final Node formula : contradictoryTestCases) {
-            Assert.assertFalse(formula.toString(), SAT.isSatisfiableNoTseytin(formula));
-            Assert.assertFalse(formula.toString(), SAT.isSatisfiableAlwaysTseytin(formula));
-        }
+    @ParameterizedTest
+    @MethodSource("contradictoryTestCases")
+    public void testContradictions(Node formula) {
+        assertFalse(SAT.isSatisfiableNoTseytin(formula), formula.toString());
+        assertFalse(SAT.isSatisfiableAlwaysTseytin(formula), formula.toString());
     }
 }
diff --git a/src/test/resources/diffs/parser/.gitignore b/src/test/resources/diffs/parser/.gitignore
new file mode 100644
index 000000000..e79a2a109
--- /dev/null
+++ b/src/test/resources/diffs/parser/.gitignore
@@ -0,0 +1,10 @@
+# People might run LaTeX to visualize failed tests and there are many LaTeX
+# files which will be generated, so just ignore all files, except explicitly
+# allowed.
+*
+
+!.gitignore
+!*.diff
+!*_expected.lg
+!/wontfix/
+!README.md
diff --git a/src/test/resources/diffs/parser/01.diff b/src/test/resources/diffs/parser/01.diff
new file mode 100644
index 000000000..2030b9966
--- /dev/null
+++ b/src/test/resources/diffs/parser/01.diff
@@ -0,0 +1,3 @@
+-#ifdef \
+   A
+-#endif
diff --git a/src/test/resources/diffs/parser/01_expected.lg b/src/test/resources/diffs/parser/01_expected.lg
new file mode 100644
index 000000000..d1a507823
--- /dev/null
+++ b/src/test/resources/diffs/parser/01_expected.lg
@@ -0,0 +1,5 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 136 REM;IF;(old: 1, diff: 1, new:-1);(old: 3, diff: 3, new:-1);A;#ifdef \;  A
+v 195 ADD;ARTIFACT;(old: -1, diff: 2, new:1);(old: -1, diff: 3, new:2);;  A
+e 136 16 b;0
+e 195 16 a;1
diff --git a/src/test/resources/diffs/parser/02.diff b/src/test/resources/diffs/parser/02.diff
new file mode 100644
index 000000000..bc41b0595
--- /dev/null
+++ b/src/test/resources/diffs/parser/02.diff
@@ -0,0 +1,4 @@
+-#if A
+   Code
+-#endif \
+   // Hehe
diff --git a/src/test/resources/diffs/parser/02_expected.lg b/src/test/resources/diffs/parser/02_expected.lg
new file mode 100644
index 000000000..9293d49db
--- /dev/null
+++ b/src/test/resources/diffs/parser/02_expected.lg
@@ -0,0 +1,8 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 136 REM;IF;(old: 1, diff: 1, new:-1);(old: 3, diff: 3, new:-1);A;#if A
+v 211 NON;ARTIFACT;(old: 2, diff: 2, new:1);(old: 3, diff: 3, new:2);;  Code
+v 323 ADD;ARTIFACT;(old: -1, diff: 4, new:2);(old: -1, diff: 5, new:3);;  // Hehe
+e 136 16 b;0
+e 211 136 b;0
+e 211 16 a;1
+e 323 16 a;2
diff --git a/src/test/resources/diffs/parser/03.diff b/src/test/resources/diffs/parser/03.diff
new file mode 100644
index 000000000..06d38aa66
--- /dev/null
+++ b/src/test/resources/diffs/parser/03.diff
@@ -0,0 +1,11 @@
+ #if A \
+-  && B \
+-  && C \
++  || B \
+-  && D
++  || C \
+   // Foo
+ Code
+-#endif \
++#endif
+   // Bar
diff --git a/src/test/resources/diffs/parser/03_expected.lg b/src/test/resources/diffs/parser/03_expected.lg
new file mode 100644
index 000000000..362f8320d
--- /dev/null
+++ b/src/test/resources/diffs/parser/03_expected.lg
@@ -0,0 +1,12 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 136 REM;IF;(old: 1, diff: 1, new:-1);(old: 7, diff: 9, new:-1);A & B & C & D;#if A \;  && B \;  && C \;  && D
+v 523 REM;ARTIFACT;(old: 5, diff: 7, new:-1);(old: 6, diff: 8, new:-1);;  // Foo
+v 595 NON;ARTIFACT;(old: 6, diff: 8, new:5);(old: 7, diff: 9, new:6);;Code
+v 128 ADD;IF;(old: -1, diff: 1, new:1);(old: -1, diff: 10, new:6);A | B | C;#if A \;  || B \;  || C \;  // Foo
+v 771 ADD;ARTIFACT;(old: -1, diff: 11, new:7);(old: -1, diff: 12, new:8);;  // Bar
+e 136 16 b;0
+e 523 136 b;0
+e 595 136 b;1
+e 595 128 a;0
+e 128 16 a;1
+e 771 16 a;2
diff --git a/src/test/resources/diffs/parser/04.diff b/src/test/resources/diffs/parser/04.diff
new file mode 100644
index 000000000..29e4d6b1c
--- /dev/null
+++ b/src/test/resources/diffs/parser/04.diff
@@ -0,0 +1,4 @@
+ #ifdef A
+   a \
+   b
+ #endif
diff --git a/src/test/resources/diffs/parser/04_expected.lg b/src/test/resources/diffs/parser/04_expected.lg
new file mode 100644
index 000000000..f5c884bcc
--- /dev/null
+++ b/src/test/resources/diffs/parser/04_expected.lg
@@ -0,0 +1,5 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 144 NON;IF;(old: 1, diff: 1, new:1);(old: 4, diff: 4, new:4);A;#ifdef A
+v 211 NON;ARTIFACT;(old: 2, diff: 2, new:2);(old: 4, diff: 4, new:4);;  a \;  b
+e 144 16 ba;0;0
+e 211 144 ba;0;0
diff --git a/src/test/resources/diffs/parser/05.diff b/src/test/resources/diffs/parser/05.diff
new file mode 100644
index 000000000..29e4d6b1c
--- /dev/null
+++ b/src/test/resources/diffs/parser/05.diff
@@ -0,0 +1,4 @@
+ #ifdef A
+   a \
+   b
+ #endif
diff --git a/src/test/resources/diffs/parser/05_expected.lg b/src/test/resources/diffs/parser/05_expected.lg
new file mode 100644
index 000000000..f5c884bcc
--- /dev/null
+++ b/src/test/resources/diffs/parser/05_expected.lg
@@ -0,0 +1,5 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 144 NON;IF;(old: 1, diff: 1, new:1);(old: 4, diff: 4, new:4);A;#ifdef A
+v 211 NON;ARTIFACT;(old: 2, diff: 2, new:2);(old: 4, diff: 4, new:4);;  a \;  b
+e 144 16 ba;0;0
+e 211 144 ba;0;0
diff --git a/src/test/resources/diffs/parser/06.diff b/src/test/resources/diffs/parser/06.diff
new file mode 100644
index 000000000..68cfda5df
--- /dev/null
+++ b/src/test/resources/diffs/parser/06.diff
@@ -0,0 +1,4 @@
+ // hehe \
+ #ifdef A
+ Code \
+ #endif
diff --git a/src/test/resources/diffs/parser/06_expected.lg b/src/test/resources/diffs/parser/06_expected.lg
new file mode 100644
index 000000000..3db6fa39c
--- /dev/null
+++ b/src/test/resources/diffs/parser/06_expected.lg
@@ -0,0 +1,5 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 147 NON;ARTIFACT;(old: 1, diff: 1, new:1);(old: 3, diff: 3, new:3);;// hehe \;#ifdef A
+v 275 NON;ARTIFACT;(old: 3, diff: 3, new:3);(old: 5, diff: 5, new:5);;Code \;#endif
+e 147 16 ba;0;0
+e 275 16 ba;1;1
diff --git a/src/test/resources/diffs/parser/07.diff b/src/test/resources/diffs/parser/07.diff
new file mode 100644
index 000000000..4377a1cb6
--- /dev/null
+++ b/src/test/resources/diffs/parser/07.diff
@@ -0,0 +1,2 @@
+-a \
+ b
diff --git a/src/test/resources/diffs/parser/07_expected.lg b/src/test/resources/diffs/parser/07_expected.lg
new file mode 100644
index 000000000..1923b2dc1
--- /dev/null
+++ b/src/test/resources/diffs/parser/07_expected.lg
@@ -0,0 +1,5 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 139 REM;ARTIFACT;(old: 1, diff: 1, new:-1);(old: 3, diff: 3, new:-1);;a \;b
+v 195 ADD;ARTIFACT;(old: -1, diff: 2, new:1);(old: -1, diff: 3, new:2);;b
+e 139 16 b;0
+e 195 16 a;1
diff --git a/src/test/resources/diffs/parser/08.diff b/src/test/resources/diffs/parser/08.diff
new file mode 100644
index 000000000..77b982282
--- /dev/null
+++ b/src/test/resources/diffs/parser/08.diff
@@ -0,0 +1,4 @@
+-#ifdef A
++#ifdef B
+ Code
+ #endif
diff --git a/src/test/resources/diffs/parser/08_expected.lg b/src/test/resources/diffs/parser/08_expected.lg
new file mode 100644
index 000000000..f03adcf72
--- /dev/null
+++ b/src/test/resources/diffs/parser/08_expected.lg
@@ -0,0 +1,8 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 136 REM;IF;(old: 1, diff: 1, new:-1);(old: 3, diff: 4, new:-1);A;#ifdef A
+v 275 NON;ARTIFACT;(old: 2, diff: 3, new:2);(old: 3, diff: 4, new:3);;Code
+v 192 ADD;IF;(old: -1, diff: 2, new:1);(old: -1, diff: 4, new:3);B;#ifdef B
+e 136 16 b;0
+e 275 136 b;0
+e 275 192 a;0
+e 192 16 a;1
diff --git a/src/test/resources/diffs/parser/09.diff b/src/test/resources/diffs/parser/09.diff
new file mode 100644
index 000000000..5d2f2bd7a
--- /dev/null
+++ b/src/test/resources/diffs/parser/09.diff
@@ -0,0 +1,5 @@
+ #ifdef A
+ Code 1
+-#endif
+ Code 2
++#endif
diff --git a/src/test/resources/diffs/parser/09_expected.lg b/src/test/resources/diffs/parser/09_expected.lg
new file mode 100644
index 000000000..eec645c42
--- /dev/null
+++ b/src/test/resources/diffs/parser/09_expected.lg
@@ -0,0 +1,8 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 144 NON;IF;(old: 1, diff: 1, new:1);(old: 3, diff: 5, new:4);A;#ifdef A
+v 211 NON;ARTIFACT;(old: 2, diff: 2, new:2);(old: 3, diff: 3, new:3);;Code 1
+v 339 NON;ARTIFACT;(old: 4, diff: 4, new:3);(old: 5, diff: 5, new:4);;Code 2
+e 144 16 ba;0;0
+e 211 144 ba;0;0
+e 339 16 b;1
+e 339 144 a;1
diff --git a/src/test/resources/diffs/parser/10.diff b/src/test/resources/diffs/parser/10.diff
new file mode 100644
index 000000000..d278e57a4
--- /dev/null
+++ b/src/test/resources/diffs/parser/10.diff
@@ -0,0 +1,5 @@
++#ifdef B
+-#ifdef A
+ Code
+-#endif
++#endif // B
diff --git a/src/test/resources/diffs/parser/10_expected.lg b/src/test/resources/diffs/parser/10_expected.lg
new file mode 100644
index 000000000..522c47316
--- /dev/null
+++ b/src/test/resources/diffs/parser/10_expected.lg
@@ -0,0 +1,8 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 128 ADD;IF;(old: -1, diff: 1, new:1);(old: -1, diff: 5, new:3);B;#ifdef B
+v 275 NON;ARTIFACT;(old: 2, diff: 3, new:2);(old: 3, diff: 4, new:3);;Code
+v 200 REM;IF;(old: 1, diff: 2, new:-1);(old: 3, diff: 4, new:-1);A;#ifdef A
+e 128 16 a;0
+e 275 200 b;0
+e 275 128 a;0
+e 200 16 b;1
diff --git a/src/test/resources/diffs/parser/11.diff b/src/test/resources/diffs/parser/11.diff
new file mode 100644
index 000000000..b4f556535
--- /dev/null
+++ b/src/test/resources/diffs/parser/11.diff
@@ -0,0 +1,5 @@
+-#ifdef A
++#ifdef B
+ Code
+-#endif
++#endif // B
diff --git a/src/test/resources/diffs/parser/11_expected.lg b/src/test/resources/diffs/parser/11_expected.lg
new file mode 100644
index 000000000..2aa304f8d
--- /dev/null
+++ b/src/test/resources/diffs/parser/11_expected.lg
@@ -0,0 +1,8 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 136 REM;IF;(old: 1, diff: 1, new:-1);(old: 3, diff: 4, new:-1);A;#ifdef A
+v 275 NON;ARTIFACT;(old: 2, diff: 3, new:2);(old: 3, diff: 4, new:3);;Code
+v 192 ADD;IF;(old: -1, diff: 2, new:1);(old: -1, diff: 5, new:3);B;#ifdef B
+e 136 16 b;0
+e 275 136 b;0
+e 275 192 a;0
+e 192 16 a;1
diff --git a/src/test/resources/diffs/parser/12.diff b/src/test/resources/diffs/parser/12.diff
new file mode 100644
index 000000000..0e1892404
--- /dev/null
+++ b/src/test/resources/diffs/parser/12.diff
@@ -0,0 +1,3 @@
+ #if defined(A)
+ Code
+ #endif
diff --git a/src/test/resources/diffs/parser/12_expected.lg b/src/test/resources/diffs/parser/12_expected.lg
new file mode 100644
index 000000000..d1e8054a8
--- /dev/null
+++ b/src/test/resources/diffs/parser/12_expected.lg
@@ -0,0 +1,5 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 144 NON;IF;(old: 1, diff: 1, new:1);(old: 3, diff: 3, new:3);A;#if defined(A)
+v 211 NON;ARTIFACT;(old: 2, diff: 2, new:2);(old: 3, diff: 3, new:3);;Code
+e 144 16 ba;0;0
+e 211 144 ba;0;0
diff --git a/src/test/resources/diffs/parser/13.diff b/src/test/resources/diffs/parser/13.diff
new file mode 100644
index 000000000..4c143fa45
--- /dev/null
+++ b/src/test/resources/diffs/parser/13.diff
@@ -0,0 +1,3 @@
+ #ifdef A
+ #else // B
+ #endif
diff --git a/src/test/resources/diffs/parser/13_expected.lg b/src/test/resources/diffs/parser/13_expected.lg
new file mode 100644
index 000000000..c6ab93d6e
--- /dev/null
+++ b/src/test/resources/diffs/parser/13_expected.lg
@@ -0,0 +1,5 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 144 NON;IF;(old: 1, diff: 1, new:1);(old: 2, diff: 2, new:2);A;#ifdef A
+v 209 NON;ELSE;(old: 2, diff: 2, new:2);(old: 3, diff: 3, new:3);;#else // B
+e 144 16 ba;0;0
+e 209 144 ba;0;0
diff --git a/src/test/resources/diffs/parser/14.diff b/src/test/resources/diffs/parser/14.diff
new file mode 100644
index 000000000..c47039691
--- /dev/null
+++ b/src/test/resources/diffs/parser/14.diff
@@ -0,0 +1,3 @@
+ #ifdef A
+-#else // B
+ #endif
diff --git a/src/test/resources/diffs/parser/14_expected.lg b/src/test/resources/diffs/parser/14_expected.lg
new file mode 100644
index 000000000..068ae3d90
--- /dev/null
+++ b/src/test/resources/diffs/parser/14_expected.lg
@@ -0,0 +1,5 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 144 NON;IF;(old: 1, diff: 1, new:1);(old: 2, diff: 3, new:2);A;#ifdef A
+v 201 REM;ELSE;(old: 2, diff: 2, new:-1);(old: 3, diff: 3, new:-1);;#else // B
+e 144 16 ba;0;0
+e 201 144 b;0
diff --git a/src/test/resources/diffs/parser/15.diff b/src/test/resources/diffs/parser/15.diff
new file mode 100644
index 000000000..fb44d3df0
--- /dev/null
+++ b/src/test/resources/diffs/parser/15.diff
@@ -0,0 +1,4 @@
+ #ifdef A
+-#elif \
+   B
+ #endif
diff --git a/src/test/resources/diffs/parser/15_expected.lg b/src/test/resources/diffs/parser/15_expected.lg
new file mode 100644
index 000000000..b3c35d57e
--- /dev/null
+++ b/src/test/resources/diffs/parser/15_expected.lg
@@ -0,0 +1,7 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 144 NON;IF;(old: 1, diff: 1, new:1);(old: 2, diff: 4, new:3);A;#ifdef A
+v 202 REM;ELIF;(old: 2, diff: 2, new:-1);(old: 4, diff: 4, new:-1);B;#elif \;  B
+v 259 ADD;ARTIFACT;(old: -1, diff: 3, new:2);(old: -1, diff: 4, new:3);;  B
+e 144 16 ba;0;0
+e 202 144 b;0
+e 259 144 a;1
diff --git a/src/test/resources/diffs/parser/16.diff b/src/test/resources/diffs/parser/16.diff
new file mode 100644
index 000000000..e924f3444
--- /dev/null
+++ b/src/test/resources/diffs/parser/16.diff
@@ -0,0 +1,10 @@
+-#ifdef A
+ a
++#ifdef B
+ b
++#elif C
+ c
+ #else D
+ d
+-#endif
++#endif // D
diff --git a/src/test/resources/diffs/parser/16_expected.lg b/src/test/resources/diffs/parser/16_expected.lg
new file mode 100644
index 000000000..61be20264
--- /dev/null
+++ b/src/test/resources/diffs/parser/16_expected.lg
@@ -0,0 +1,21 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 136 REM;IF;(old: 1, diff: 1, new:-1);(old: 5, diff: 7, new:-1);A;#ifdef A
+v 211 NON;ARTIFACT;(old: 2, diff: 2, new:1);(old: 3, diff: 3, new:2);;a
+v 339 NON;ARTIFACT;(old: 3, diff: 4, new:3);(old: 4, diff: 5, new:4);;b
+v 467 NON;ARTIFACT;(old: 4, diff: 6, new:5);(old: 5, diff: 7, new:6);;c
+v 529 NON;ELSE;(old: 5, diff: 7, new:6);(old: 7, diff: 10, new:8);;#else D
+v 595 NON;ARTIFACT;(old: 6, diff: 8, new:7);(old: 7, diff: 9, new:8);;d
+v 256 ADD;IF;(old: -1, diff: 3, new:2);(old: -1, diff: 5, new:4);B;#ifdef B
+v 386 ADD;ELIF;(old: -1, diff: 5, new:4);(old: -1, diff: 7, new:6);C;#elif C
+e 136 16 b;0
+e 211 136 b;0
+e 211 16 a;1
+e 339 136 b;1
+e 339 256 a;0
+e 467 136 b;2
+e 467 386 a;0
+e 529 136 b;3
+e 529 386 a;1
+e 595 529 ba;0;0
+e 256 16 a;2
+e 386 256 a;1
diff --git a/src/test/resources/diffs/parser/17.diff b/src/test/resources/diffs/parser/17.diff
new file mode 100644
index 000000000..792600fa9
--- /dev/null
+++ b/src/test/resources/diffs/parser/17.diff
@@ -0,0 +1,5 @@
+-#ifdef A
++#ifdef B #\
+ #else
++#endif
+-#endif
diff --git a/src/test/resources/diffs/parser/17_expected.lg b/src/test/resources/diffs/parser/17_expected.lg
new file mode 100644
index 000000000..c6bb75516
--- /dev/null
+++ b/src/test/resources/diffs/parser/17_expected.lg
@@ -0,0 +1,7 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 136 REM;IF;(old: 1, diff: 1, new:-1);(old: 2, diff: 3, new:-1);A;#ifdef A
+v 265 REM;ELSE;(old: 2, diff: 3, new:-1);(old: 3, diff: 5, new:-1);;#else
+v 192 ADD;IF;(old: -1, diff: 2, new:1);(old: -1, diff: 4, new:3);B##else;#ifdef B #\;#else
+e 136 16 b;0
+e 265 136 b;0
+e 192 16 a;1
diff --git a/src/test/resources/diffs/parser/18.diff b/src/test/resources/diffs/parser/18.diff
new file mode 100644
index 000000000..0163672b8
--- /dev/null
+++ b/src/test/resources/diffs/parser/18.diff
@@ -0,0 +1,2 @@
+ // Comment \
+ #ifdef A
diff --git a/src/test/resources/diffs/parser/18_expected.lg b/src/test/resources/diffs/parser/18_expected.lg
new file mode 100644
index 000000000..547cadeaa
--- /dev/null
+++ b/src/test/resources/diffs/parser/18_expected.lg
@@ -0,0 +1,3 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 147 NON;ARTIFACT;(old: 1, diff: 1, new:1);(old: 3, diff: 3, new:3);;// Comment \;#ifdef A
+e 147 16 ba;0;0
diff --git a/src/test/resources/diffs/parser/wontfix/01.diff b/src/test/resources/diffs/parser/wontfix/01.diff
new file mode 100644
index 000000000..e08b3d2d5
--- /dev/null
+++ b/src/test/resources/diffs/parser/wontfix/01.diff
@@ -0,0 +1,3 @@
+ # \
+   ifdef A
+ #endif
diff --git a/src/test/resources/diffs/parser/wontfix/01_expected.lg b/src/test/resources/diffs/parser/wontfix/01_expected.lg
new file mode 100644
index 000000000..b37a4fe9e
--- /dev/null
+++ b/src/test/resources/diffs/parser/wontfix/01_expected.lg
@@ -0,0 +1,3 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 144 NON;IF;(old: 1, diff: 1, new:1);(old: 3, diff: 3, new:3);A;# \; ifdef A
+e 144 16 ba;0;0
diff --git a/src/test/resources/diffs/parser/wontfix/02.diff b/src/test/resources/diffs/parser/wontfix/02.diff
new file mode 100644
index 000000000..3f0c241eb
--- /dev/null
+++ b/src/test/resources/diffs/parser/wontfix/02.diff
@@ -0,0 +1,2 @@
+ # /* Comment */ ifdef A
+ #endif
diff --git a/src/test/resources/diffs/parser/wontfix/02_expected.lg b/src/test/resources/diffs/parser/wontfix/02_expected.lg
new file mode 100644
index 000000000..e898d4807
--- /dev/null
+++ b/src/test/resources/diffs/parser/wontfix/02_expected.lg
@@ -0,0 +1,3 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 144 NON;IF;(old: 1, diff: 1, new:1);(old: 2, diff: 2, new:2);A;# /* Comment */ ifdef A
+e 144 16 ba;0;0
diff --git a/src/test/resources/diffs/parser/wontfix/03.diff b/src/test/resources/diffs/parser/wontfix/03.diff
new file mode 100644
index 000000000..352827574
--- /dev/null
+++ b/src/test/resources/diffs/parser/wontfix/03.diff
@@ -0,0 +1,3 @@
+ /* This is a comment, so it's removed before the preprocessor is run
+  #ifdef A
+  */
diff --git a/src/test/resources/diffs/parser/wontfix/03_expected.lg b/src/test/resources/diffs/parser/wontfix/03_expected.lg
new file mode 100644
index 000000000..65da8a078
--- /dev/null
+++ b/src/test/resources/diffs/parser/wontfix/03_expected.lg
@@ -0,0 +1,7 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 147 NON;ARTIFACT;(old: 1, diff: 1, new:1);(old: 2, diff: 2, new:2);;/* This is a comment, so it's removed before the preprocessor is run
+v 211 NON;ARTIFACT;(old: 2, diff: 2, new:2);(old: 3, diff: 3, new:3);; #ifdef A
+v 275 NON;ARTIFACT;(old: 3, diff: 3, new:3);(old: 4, diff: 4, new:4);; */
+e 147 16 ba;0;0
+e 211 16 ba;1;1
+e 275 16 ba;2;2
diff --git a/src/test/resources/diffs/parser/wontfix/04.diff b/src/test/resources/diffs/parser/wontfix/04.diff
new file mode 100644
index 000000000..55acbcfc2
--- /dev/null
+++ b/src/test/resources/diffs/parser/wontfix/04.diff
@@ -0,0 +1,4 @@
+ #ifdef /*
+  * Comment
+  */ A
+ #endif
diff --git a/src/test/resources/diffs/parser/wontfix/04_expected.lg b/src/test/resources/diffs/parser/wontfix/04_expected.lg
new file mode 100644
index 000000000..33e0a085e
--- /dev/null
+++ b/src/test/resources/diffs/parser/wontfix/04_expected.lg
@@ -0,0 +1,3 @@
+v 16 NON;IF;(old: -1, diff: -1, new:-1);(old: -1, diff: -1, new:-1);True
+v 144 NON;IF;(old: 1, diff: 1, new:1);(old: 4, diff: 4, new:4);A;#ifdef /*; * Comment; */ A
+e 144 16 ba;0;0
diff --git a/src/test/resources/diffs/parser/wontfix/README.md b/src/test/resources/diffs/parser/wontfix/README.md
new file mode 100644
index 000000000..5c016ea6e
--- /dev/null
+++ b/src/test/resources/diffs/parser/wontfix/README.md
@@ -0,0 +1,9 @@
+# Test cases of WONTFIX behaviour
+
+## Sophisticated comment parsing
+The test cases 01 and 02 would require the removal of comments before checking
+if a line is a macro. The test cases 03 and 04 would required the detection of
+multi-line macros which span multiple lines.
+
+All of these cases require more engineering than they are worth because they
+should be pretty rare in practice.
diff --git a/src/test/resources/multilinemacros/diffWithComments.txt b/src/test/resources/multilinemacros/diffWithComments.txt
index 468926863..196f062c1 100644
--- a/src/test/resources/multilinemacros/diffWithComments.txt
+++ b/src/test/resources/multilinemacros/diffWithComments.txt
@@ -1,10 +1,11 @@
 +#define multi \
 +kulti
-
+ 
  /** hihi **\
-
+ 
  //\
-
+ a
+ 
 +#if A & B
  foo();
-+#endif
\ No newline at end of file
++#endif
diff --git a/src/test/resources/patterns/elementary.diff b/src/test/resources/patterns/elementary.diff
index 5ea837d20..ee545f137 100644
--- a/src/test/resources/patterns/elementary.diff
+++ b/src/test/resources/patterns/elementary.diff
@@ -1,39 +1,39 @@
  #if root
-
+ 
 +#if a
 +AddWithMapping
 +#endif
-
+ 
  #if a
 +AddToPC
  #endif
-
+ 
 -#if a
 -RemWithMapping
 -#endif
-
+ 
  #if a
 -RemFromPC
  #endif
-
+ 
 +#if a
  Specialization
 +#endif
-
+ 
 -#if a
  Generalization
 -#endif
-
+ 
 +#if a
 -#if b
  Reconfiguration
  #endif
-
+ 
 +#if a
 -#if (a && 1) || false
  Refactoring
  #endif
-
+ 
  Untouched
-
- #endif
\ No newline at end of file
+ 
+ #endif
diff --git a/src/test/resources/serialize/.gitignore b/src/test/resources/serialize/.gitignore
new file mode 100644
index 000000000..87dba571b
--- /dev/null
+++ b/src/test/resources/serialize/.gitignore
@@ -0,0 +1 @@
+actual.tex
