]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3837 Improve violation tracking mechanism
authorEvgeny Mandrikov <mandrikov@gmail.com>
Thu, 14 Feb 2013 15:29:45 +0000 (16:29 +0100)
committerEvgeny Mandrikov <mandrikov@gmail.com>
Thu, 14 Feb 2013 18:40:06 +0000 (19:40 +0100)
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/ViolationPair.java [deleted file]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/ViolationTrackingBlocksRecognizer.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/ViolationTrackingDecorator.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/tracking/RollingHashSequence.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/tracking/RollingHashSequenceComparator.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/tracking/package-info.java
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/tracking/RollingHashSequenceTest.java [new file with mode: 0644]

diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/ViolationPair.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/ViolationPair.java
deleted file mode 100644 (file)
index 39c8de0..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Sonar, open source software quality management tool.
- * Copyright (C) 2008-2012 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * Sonar is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * Sonar is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with Sonar; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
- */
-package org.sonar.plugins.core.timemachine;
-
-import org.sonar.api.database.model.RuleFailureModel;
-import org.sonar.api.rules.Violation;
-
-import java.util.Comparator;
-
-public class ViolationPair {
-
-  private final RuleFailureModel pastViolation;
-  private final Violation newViolation;
-  private final int weight;
-
-  public ViolationPair(RuleFailureModel pastViolation, Violation newViolation, int weight) {
-    this.pastViolation = pastViolation;
-    this.newViolation = newViolation;
-    this.weight = weight;
-  }
-
-  public Violation getNewViolation() {
-    return newViolation;
-  }
-
-  public RuleFailureModel getPastViolation() {
-    return pastViolation;
-  }
-
-  public static final Comparator<ViolationPair> COMPARATOR = new Comparator<ViolationPair>() {
-    public int compare(ViolationPair o1, ViolationPair o2) {
-      return o2.weight - o1.weight;
-    }
-  };
-
-}
index c5a2666ae3aa705fb4b95bbea9ba80d4b6dfb24a..0d105f304865f34c49cf543373f193dba7eb466a 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.plugins.core.timemachine;
 
+import com.google.common.annotations.VisibleForTesting;
 import org.sonar.plugins.core.timemachine.tracking.HashedSequence;
 import org.sonar.plugins.core.timemachine.tracking.HashedSequenceComparator;
 import org.sonar.plugins.core.timemachine.tracking.StringText;
@@ -30,22 +31,25 @@ public class ViolationTrackingBlocksRecognizer {
   private final HashedSequence<StringText> b;
   private final HashedSequenceComparator<StringText> cmp;
 
+  @VisibleForTesting
   public ViolationTrackingBlocksRecognizer(String referenceSource, String source) {
-    this(new StringText(referenceSource), new StringText(source), StringTextComparator.IGNORE_WHITESPACE);
+    this.a = HashedSequence.wrap(new StringText(referenceSource), StringTextComparator.IGNORE_WHITESPACE);
+    this.b = HashedSequence.wrap(new StringText(source), StringTextComparator.IGNORE_WHITESPACE);
+    this.cmp = new HashedSequenceComparator<StringText>(StringTextComparator.IGNORE_WHITESPACE);
   }
 
-  private ViolationTrackingBlocksRecognizer(StringText a, StringText b, StringTextComparator cmp) {
-    this.a = HashedSequence.wrap(a, cmp);
-    this.b = HashedSequence.wrap(b, cmp);
-    this.cmp = new HashedSequenceComparator<StringText>(cmp);
+  public ViolationTrackingBlocksRecognizer(HashedSequence<StringText> a, HashedSequence<StringText> b, HashedSequenceComparator<StringText> cmp) {
+    this.a = a;
+    this.b = b;
+    this.cmp = cmp;
   }
 
-  public boolean isValidLineInReference(int line) {
-    return (0 <= line) && (line < a.length());
+  public boolean isValidLineInReference(Integer line) {
+    return (line != null) && (0 <= line - 1) && (line - 1 < a.length());
   }
 
-  public boolean isValidLineInSource(int line) {
-    return (0 <= line) && (line < b.length());
+  public boolean isValidLineInSource(Integer line) {
+    return (line != null) && (0 <= line - 1) && (line - 1 < b.length());
   }
 
   /**
index fee56b167875b65c1d43b100d3625b42329f0f6e..6da3b0153fffff50bac5736221ae21ea9f39e2ff 100644 (file)
 package org.sonar.plugins.core.timemachine;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.*;
+import com.google.common.base.Objects;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
 import org.apache.commons.lang.ObjectUtils;
 import org.apache.commons.lang.StringUtils;
-import org.sonar.api.batch.*;
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorBarriers;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.batch.SonarIndex;
 import org.sonar.api.database.model.RuleFailureModel;
 import org.sonar.api.resources.Project;
 import org.sonar.api.resources.Resource;
 import org.sonar.api.rules.Violation;
 import org.sonar.api.violations.ViolationQuery;
-
-import java.util.*;
+import org.sonar.plugins.core.timemachine.tracking.HashedSequence;
+import org.sonar.plugins.core.timemachine.tracking.HashedSequenceComparator;
+import org.sonar.plugins.core.timemachine.tracking.RollingHashSequence;
+import org.sonar.plugins.core.timemachine.tracking.RollingHashSequenceComparator;
+import org.sonar.plugins.core.timemachine.tracking.StringText;
+import org.sonar.plugins.core.timemachine.tracking.StringTextComparator;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 @DependsUpon({DecoratorBarriers.END_OF_VIOLATIONS_GENERATION, DecoratorBarriers.START_VIOLATION_TRACKING})
 @DependedUpon(DecoratorBarriers.END_OF_VIOLATION_TRACKING)
@@ -114,34 +135,71 @@ public class ViolationTrackingDecorator implements Decorator {
 
     // If each new violation matches an old one we can stop the matching mechanism
     if (referenceViolationsMap.size() != newViolations.size()) {
-
-      // SONAR-3072
-      ViolationTrackingBlocksRecognizer rec = null;
       if (source != null && resource != null) {
         String referenceSource = referenceAnalysis.getSource(resource);
         if (referenceSource != null) {
-          rec = new ViolationTrackingBlocksRecognizer(referenceSource, source);
-
-          List<ViolationPair> possiblePairs = Lists.newArrayList();
-          for (Violation newViolation : newViolations) {
-            if (newViolation.getLineId() != null && rec.isValidLineInSource(newViolation.getLineId() - 1)) {
-              for (RuleFailureModel pastViolation : pastViolationsByRule.get(newViolation.getRule().getId())) {
-                if (pastViolation.getLine() != null && rec.isValidLineInReference(pastViolation.getLine() - 1)) {
-                  int weight = rec.computeLengthOfMaximalBlock(pastViolation.getLine() - 1, newViolation.getLineId() - 1);
-                  possiblePairs.add(new ViolationPair(pastViolation, newViolation, weight));
-                }
-              }
+          HashedSequence<StringText> hashedReference = HashedSequence.wrap(new StringText(referenceSource), StringTextComparator.IGNORE_WHITESPACE);
+          HashedSequence<StringText> hashedSource = HashedSequence.wrap(new StringText(source), StringTextComparator.IGNORE_WHITESPACE);
+          HashedSequenceComparator<StringText> hashedComparator = new HashedSequenceComparator<StringText>(StringTextComparator.IGNORE_WHITESPACE);
+
+          ViolationTrackingBlocksRecognizer rec = new ViolationTrackingBlocksRecognizer(hashedReference, hashedSource, hashedComparator);
+
+          Multimap<Integer, Violation> newViolationsByLines = newViolationsByLines(newViolations, rec);
+          Multimap<Integer, RuleFailureModel> pastViolationsByLines = pastViolationsByLines(pastViolations, rec);
+
+          RollingHashSequence<HashedSequence<StringText>> a = RollingHashSequence.wrap(hashedReference, hashedComparator, 5);
+          RollingHashSequence<HashedSequence<StringText>> b = RollingHashSequence.wrap(hashedSource, hashedComparator, 5);
+          RollingHashSequenceComparator<HashedSequence<StringText>> cmp = new RollingHashSequenceComparator<HashedSequence<StringText>>(hashedComparator);
+
+          Map<Integer, HashOccurrence> map = Maps.newHashMap();
+
+          for (Integer line : pastViolationsByLines.keySet()) {
+            int hash = cmp.hash(a, line - 1);
+            HashOccurrence hashOccurrence = map.get(hash);
+            if (hashOccurrence == null) {
+              // first occurrence in A
+              hashOccurrence = new HashOccurrence();
+              hashOccurrence.lineA = line;
+              hashOccurrence.countA = 1;
+              map.put(hash, hashOccurrence);
+            } else {
+              hashOccurrence.countA++;
             }
           }
-          Collections.sort(possiblePairs, ViolationPair.COMPARATOR);
-
-          Set<RuleFailureModel> pp = Sets.newHashSet(pastViolations);
-          for (ViolationPair pair : possiblePairs) {
-            Violation newViolation = pair.getNewViolation();
-            RuleFailureModel pastViolation = pair.getPastViolation();
-            if (isNotAlreadyMapped(newViolation, referenceViolationsMap) && pp.contains(pastViolation)) {
-              pp.remove(pastViolation);
-              mapViolation(newViolation, pastViolation, pastViolationsByRule, referenceViolationsMap);
+
+          for (Integer line : newViolationsByLines.keySet()) {
+            int hash = cmp.hash(b, line - 1);
+            HashOccurrence hashOccurrence = map.get(hash);
+            if (hashOccurrence != null) {
+              hashOccurrence.lineB = line;
+              hashOccurrence.countB++;
+            }
+          }
+
+          Set<RuleFailureModel> unmappedPastViolations = Sets.newHashSet(pastViolations);
+
+          for (HashOccurrence hashOccurrence : map.values()) {
+            if (hashOccurrence.countA == 1 && hashOccurrence.countB == 1) {
+              // Guaranteed that lineA has been moved to lineB, so we can map all violations on lineA to all violations on lineB
+              map(newViolationsByLines.get(hashOccurrence.lineB), pastViolationsByLines.get(hashOccurrence.lineA), unmappedPastViolations, pastViolationsByRule);
+              pastViolationsByLines.removeAll(hashOccurrence.lineA);
+              newViolationsByLines.removeAll(hashOccurrence.lineB);
+            }
+          }
+
+          // Check if remaining number of lines exceeds threshold
+          if (pastViolationsByLines.keySet().size() * newViolationsByLines.keySet().size() < 250000) {
+            List<LinePair> possibleLinePairs = Lists.newArrayList();
+            for (Integer oldLine : pastViolationsByLines.keySet()) {
+              for (Integer newLine : newViolationsByLines.keySet()) {
+                int weight = rec.computeLengthOfMaximalBlock(oldLine - 1, newLine - 1);
+                possibleLinePairs.add(new LinePair(oldLine, newLine, weight));
+              }
+            }
+            Collections.sort(possibleLinePairs, LINE_PAIR_COMPARATOR);
+            for (LinePair linePair : possibleLinePairs) {
+              // High probability that lineA has been moved to lineB, so we can map all violations on lineA to all violations on lineB
+              map(newViolationsByLines.get(linePair.lineB), pastViolationsByLines.get(linePair.lineA), unmappedPastViolations, pastViolationsByRule);
             }
           }
         }
@@ -178,6 +236,68 @@ public class ViolationTrackingDecorator implements Decorator {
     return referenceViolationsMap;
   }
 
+  private void map(Collection<Violation> newViolations, Collection<RuleFailureModel> pastViolations, Set<RuleFailureModel> unmappedPastViolations,
+      Multimap<Integer, RuleFailureModel> pastViolationsByRule) {
+    for (Violation newViolation : newViolations) {
+      if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
+        for (RuleFailureModel pastViolation : pastViolations) {
+          if (unmappedPastViolations.contains(pastViolation) && Objects.equal(newViolation.getRule().getId(), pastViolation.getRuleId())) {
+            unmappedPastViolations.remove(pastViolation);
+            mapViolation(newViolation, pastViolation, pastViolationsByRule, referenceViolationsMap);
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  private Multimap<Integer, Violation> newViolationsByLines(List<Violation> newViolations, ViolationTrackingBlocksRecognizer rec) {
+    Multimap<Integer, Violation> newViolationsByLines = LinkedHashMultimap.create();
+    for (Violation newViolation : newViolations) {
+      if (isNotAlreadyMapped(newViolation, referenceViolationsMap)) {
+        if (rec.isValidLineInSource(newViolation.getLineId())) {
+          newViolationsByLines.put(newViolation.getLineId(), newViolation);
+        }
+      }
+    }
+    return newViolationsByLines;
+  }
+
+  private Multimap<Integer, RuleFailureModel> pastViolationsByLines(List<RuleFailureModel> pastViolations, ViolationTrackingBlocksRecognizer rec) {
+    Multimap<Integer, RuleFailureModel> pastViolationsByLines = LinkedHashMultimap.create();
+    for (RuleFailureModel pastViolation : pastViolations) {
+      if (rec.isValidLineInSource(pastViolation.getLine())) {
+        pastViolationsByLines.put(pastViolation.getLine(), pastViolation);
+      }
+    }
+    return pastViolationsByLines;
+  }
+
+  private static final Comparator<LinePair> LINE_PAIR_COMPARATOR = new Comparator<LinePair>() {
+    public int compare(LinePair o1, LinePair o2) {
+      return o2.weight - o1.weight;
+    }
+  };
+
+  static class LinePair {
+    int lineA;
+    int lineB;
+    int weight;
+
+    public LinePair(int lineA, int lineB, int weight) {
+      this.lineA = lineA;
+      this.lineB = lineB;
+      this.weight = weight;
+    }
+  }
+
+  static class HashOccurrence {
+    int lineA;
+    int lineB;
+    int countA;
+    int countB;
+  }
+
   private boolean isNotAlreadyMapped(Violation newViolation, Map<Violation, RuleFailureModel> violationMap) {
     return !violationMap.containsKey(newViolation);
   }
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/tracking/RollingHashSequence.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/tracking/RollingHashSequence.java
new file mode 100644 (file)
index 0000000..61912a1
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.plugins.core.timemachine.tracking;
+
+/**
+ * Wraps a {@link Sequence} to assign hash codes to elements.
+ */
+public class RollingHashSequence<S extends Sequence> implements Sequence {
+
+  final S base;
+  final int[] hashes;
+
+  public static <S extends Sequence> RollingHashSequence<S> wrap(S base, SequenceComparator<S> cmp, int lines) {
+    int size = base.length();
+    int[] hashes = new int[size];
+
+    RollingHashCalculator hashCalulator = new RollingHashCalculator(lines * 2 + 1);
+    for (int i = 0; i <= Math.min(size - 1, lines); i++) {
+      hashCalulator.add(cmp.hash(base, i));
+    }
+    for (int i = 0; i < size; i++) {
+      hashes[i] = hashCalulator.getHash();
+      if (i - lines >= 0) {
+        hashCalulator.remove(cmp.hash(base, i - lines));
+      }
+      if (i + lines + 1 < size) {
+        hashCalulator.add(cmp.hash(base, i + lines + 1));
+      } else {
+        hashCalulator.add(0);
+      }
+    }
+
+    return new RollingHashSequence<S>(base, hashes);
+  }
+
+  private RollingHashSequence(S base, int[] hashes) {
+    this.base = base;
+    this.hashes = hashes;
+  }
+
+  public int length() {
+    return base.length();
+  }
+
+  private static class RollingHashCalculator {
+
+    private static final int PRIME_BASE = 31;
+
+    private final int power;
+    private int hash;
+
+    public RollingHashCalculator(int size) {
+      int pow = 1;
+      for (int i = 0; i < size - 1; i++) {
+        pow = pow * PRIME_BASE;
+      }
+      this.power = pow;
+    }
+
+    public void add(int value) {
+      hash = hash * PRIME_BASE + value;
+    }
+
+    public void remove(int value) {
+      hash = hash - power * value;
+    }
+
+    public int getHash() {
+      return hash;
+    }
+
+  }
+
+}
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/tracking/RollingHashSequenceComparator.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/tracking/RollingHashSequenceComparator.java
new file mode 100644 (file)
index 0000000..a3ae2a7
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.plugins.core.timemachine.tracking;
+
+/**
+ * Wrap another {@link SequenceComparator} for use with {@link RollingHashSequence}.
+ */
+public class RollingHashSequenceComparator<S extends Sequence> implements SequenceComparator<RollingHashSequence<S>> {
+
+  private final SequenceComparator<? super S> cmp;
+
+  public RollingHashSequenceComparator(SequenceComparator<? super S> cmp) {
+    this.cmp = cmp;
+  }
+
+  public boolean equals(RollingHashSequence<S> a, int ai, RollingHashSequence<S> b, int bi) {
+    if (a.hashes[ai] == b.hashes[bi]) {
+      return cmp.equals(a.base, ai, b.base, bi);
+    }
+    return false;
+  }
+
+  public int hash(RollingHashSequence<S> seq, int i) {
+    return seq.hashes[i];
+  }
+
+}
index 344eb1dd68841100716e3ca67a367eff1d9b608f..3c1d85a0934e44a87f8fefcf73ec5d50bbb5e8e8 100644 (file)
@@ -18,8 +18,6 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
  */
 
-@ParametersAreNonnullByDefault
+@javax.annotation.ParametersAreNonnullByDefault
 package org.sonar.plugins.core.timemachine.tracking;
 
-import javax.annotation.ParametersAreNonnullByDefault;
-
diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/tracking/RollingHashSequenceTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/tracking/RollingHashSequenceTest.java
new file mode 100644 (file)
index 0000000..51e18ee
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.plugins.core.timemachine.tracking;
+
+import org.junit.Test;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class RollingHashSequenceTest {
+
+  @Test
+  public void test_hash() {
+    StringText seq = new StringText("line0 \n line1 \n line2");
+    StringTextComparator cmp = StringTextComparator.IGNORE_WHITESPACE;
+    RollingHashSequence<StringText> seq2 = RollingHashSequence.wrap(seq, cmp, 1);
+    RollingHashSequenceComparator<StringText> cmp2 = new RollingHashSequenceComparator<StringText>(cmp);
+
+    assertThat(seq2.length()).isEqualTo(3);
+    assertThat(cmp2.hash(seq2, 0)).isEqualTo(cmp.hash(seq, 0) * 31 + cmp.hash(seq, 1));
+    assertThat(cmp2.hash(seq2, 1)).isEqualTo((cmp.hash(seq, 0) * 31 + cmp.hash(seq, 1)) * 31 + cmp.hash(seq, 2));
+    assertThat(cmp2.hash(seq2, 2)).isEqualTo((cmp.hash(seq, 1) * 31 + cmp.hash(seq, 2)) * 31);
+  }
+
+  @Test
+  public void test_equals() {
+    StringTextComparator baseCmp = StringTextComparator.IGNORE_WHITESPACE;
+    RollingHashSequence<StringText> a = RollingHashSequence.wrap(new StringText("line0 \n line1 \n line2"), baseCmp, 1);
+    RollingHashSequence<StringText> b = RollingHashSequence.wrap(new StringText("line0 \n line1 \n line2 \n line3"), baseCmp, 1);
+    RollingHashSequenceComparator<StringText> cmp = new RollingHashSequenceComparator<StringText>(baseCmp);
+
+    assertThat(cmp.equals(a, 0, b, 0)).isTrue();
+    assertThat(cmp.equals(a, 1, b, 1)).isTrue();
+    assertThat(cmp.equals(a, 2, b, 2)).isFalse();
+  }
+
+}