You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@uima.apache.org by re...@apache.org on 2021/03/08 16:48:16 UTC

[uima-uimafit] branch feature/UIMA-6336-Align-select-behaviors-of-uimaFITv2-with-uimaFITv3 created (now 873beb2)

This is an automated email from the ASF dual-hosted git repository.

rec pushed a change to branch feature/UIMA-6336-Align-select-behaviors-of-uimaFITv2-with-uimaFITv3
in repository https://gitbox.apache.org/repos/asf/uima-uimafit.git.


      at 873beb2  [UIMA-6336] Align select behaviors of uimaFITv2 with uimaFITv3

This branch includes the following new commits:

     new 873beb2  [UIMA-6336] Align select behaviors of uimaFITv2 with uimaFITv3

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[uima-uimafit] 01/01: [UIMA-6336] Align select behaviors of uimaFITv2 with uimaFITv3

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rec pushed a commit to branch feature/UIMA-6336-Align-select-behaviors-of-uimaFITv2-with-uimaFITv3
in repository https://gitbox.apache.org/repos/asf/uima-uimafit.git

commit 873beb2ddeb0f5ae94538cb2c4fe25c6c3b6949d
Author: Richard Eckart de Castilho <re...@apache.org>
AuthorDate: Mon Mar 8 17:48:14 2021 +0100

    [UIMA-6336] Align select behaviors of uimaFITv2 with uimaFITv3
    
    - Backport tests from v3
    - Adjust behavior of several select* calls to align with the v3 behavior
---
 .../java/org/apache/uima/fit/util/CasUtil.java     |  75 ++++---
 .../uima/fit/util/AnnotationPredicateTestData.java | 137 ++++++++++++
 .../apache/uima/fit/util/AnnotationPredicates.java | 234 +++++++++++++++++++++
 .../java/org/apache/uima/fit/util/CasUtilTest.java | 137 +++++++++++-
 .../org/apache/uima/fit/util/JCasUtilTest.java     |  16 +-
 .../org/apache/uima/fit/util/SelectionAssert.java  | 187 ++++++++++++++++
 6 files changed, 754 insertions(+), 32 deletions(-)

diff --git a/uimafit-core/src/main/java/org/apache/uima/fit/util/CasUtil.java b/uimafit-core/src/main/java/org/apache/uima/fit/util/CasUtil.java
index c8f72b7..c2cbe36 100644
--- a/uimafit-core/src/main/java/org/apache/uima/fit/util/CasUtil.java
+++ b/uimafit-core/src/main/java/org/apache/uima/fit/util/CasUtil.java
@@ -1099,7 +1099,7 @@ public final class CasUtil {
       throw new IllegalArgumentException("Type [" + type.getName() + "] is not an annotation type");
     }
 
-    List<AnnotationFS> precedingAnnotations = new LinkedList<AnnotationFS>();
+    List<AnnotationFS> precedingAnnotations = new ArrayList<AnnotationFS>();
 
     // Seek annotation in index
     // withSnapshotIterators() not needed here since we copy the FSes to a list anyway    
@@ -1115,11 +1115,18 @@ public final class CasUtil {
     }
     
     int anchorBegin = anchor.getBegin();
-    int anchorEnd = anchor.getEnd();
 
-    // No need to do additional seeks here (as done in selectCovered) because the current method
-    // does not have to worry about type priorities - it never returns annotations that have
-    // the same offset as the reference annotation.
+    // Zero-width annotations are in the index *after* the wider annotations starting at the same
+    // location, but we would consider a zero-width annotation at the beginning of a larger
+    // reference annotation to be preceding the larger one. So we need to seek right
+    // for any relevant zero-with annotations.
+    while (itr.isValid() && itr.get().getBegin() == anchorBegin) {
+      itr.moveToNext();
+      if (!itr.isValid()) {
+        itr.moveToLast();
+        break;
+      }
+    }
     
     // make sure we're past the beginning of the reference annotation
     while (itr.isValid() && itr.get().getEnd() > anchorBegin) {
@@ -1133,11 +1140,11 @@ public final class CasUtil {
       int curEnd = cur.getEnd();
 
       if (
-              curEnd <= anchorBegin && 
-              (cur.getBegin() != curEnd || anchorBegin != curEnd) &&
-              (anchorBegin != anchorEnd || curEnd != anchorBegin)
+              (curEnd <= anchorBegin
+              || (cur.getBegin() == curEnd && curEnd == anchorBegin))
+              && cur != anchor
       ) {
-        precedingAnnotations.add(itr.get());
+        precedingAnnotations.add(cur);
         i++;
       }
     }
@@ -1154,14 +1161,14 @@ public final class CasUtil {
    *          a CAS.
    * @param type
    *          a UIMA type.
-   * @param annotation
+   * @param anchor
    *          anchor annotation
    * @param count
    *          number of annotations to collect
    * @return List of aType annotations following anchor annotation
    * @see <a href="package-summary.html#SortOrder">Order of selected feature structures</a>
    */
-  public static List<AnnotationFS> selectFollowing(CAS cas, Type type, AnnotationFS annotation,
+  public static List<AnnotationFS> selectFollowing(CAS cas, Type type, AnnotationFS anchor,
           int count) {
     if (!cas.getTypeSystem().subsumes(cas.getAnnotationType(), type)) {
       throw new IllegalArgumentException("Type [" + type.getName() + "] is not an annotation type");
@@ -1170,7 +1177,10 @@ public final class CasUtil {
     // Seek annotation in index
     // withSnapshotIterators() not needed here since we copy the FSes to a list anyway    
     FSIterator<AnnotationFS> itr = cas.getAnnotationIndex(type).iterator();
-    itr.moveTo(annotation);
+    itr.moveTo(anchor);
+    
+    int anchorBegin = anchor.getBegin();
+    int anchorEnd = anchor.getEnd();
 
     // When seeking forward, there is no need to check if the insertion point is beyond the
     // index. If it was, there would be nothing beyond it that could be found and returned.
@@ -1182,24 +1192,41 @@ public final class CasUtil {
     // does not have to worry about type priorities - it never returns annotations that have
     // the same offset as the reference annotation.
 
+    if (anchorBegin == anchorEnd) {
+      // zero-width annotations appear *after* larger annotations with the same start position in
+      // the index but the larger annotations are considered to be *following* the zero-width, so we
+      // have to look to the left for larger annotations...
+      if (itr.isValid()) {
+        itr.moveToPrevious();
+        while (itr.isValid() && itr.get().getBegin() == anchorBegin) {
+          itr.moveToPrevious();
+        }
+        
+        if (!itr.isValid()) {
+          itr.moveToFirst();
+        }
+        else {
+          itr.moveToNext();
+        }
+      }
+      else {
+        itr.moveToFirst();
+      }
+    }
+    
     // make sure we're past the end of the reference annotation
-    while (itr.isValid() && itr.get().getBegin() < annotation.getEnd()) {
+    while (itr.isValid() && itr.get().getBegin() < anchorEnd) {
       itr.moveToNext();
     }
 
     // add annotations from the iterator into the result list
-    int refEnd = annotation.getEnd();
-    List<AnnotationFS> followingAnnotations = new LinkedList<AnnotationFS>();
-    for (int i = 0; i < count && itr.isValid(); i++, itr.moveToNext()) {
-      AnnotationFS fs = itr.get();
-      int begin = fs.getBegin();
-      int end = fs.getEnd();
-      if (begin == end && refEnd == begin) {
-        // Skip zero-width annotation at the end of the reference annotation. These are considered
-        // to be "coveredBy" instead of following
-        continue;
+    List<AnnotationFS> followingAnnotations = new ArrayList<AnnotationFS>();
+    for (int i = 0; i < count && itr.isValid(); itr.moveToNext()) {
+      AnnotationFS cur = itr.get();
+      if (cur != anchor && cur.getBegin() >= anchorEnd) {
+        followingAnnotations.add(cur);
+        i ++;
       }
-      followingAnnotations.add(itr.get());
     }
     
     return followingAnnotations;
diff --git a/uimafit-core/src/test/java/org/apache/uima/fit/util/AnnotationPredicateTestData.java b/uimafit-core/src/test/java/org/apache/uima/fit/util/AnnotationPredicateTestData.java
new file mode 100644
index 0000000..12dd85e
--- /dev/null
+++ b/uimafit-core/src/test/java/org/apache/uima/fit/util/AnnotationPredicateTestData.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.uima.fit.util;
+
+import static java.lang.Integer.MAX_VALUE;
+import static java.util.Arrays.asList;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.BEGINNING_WITH;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.COLOCATED;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.COVERED_BY;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.COVERING;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.ENDING_WITH;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.FOLLOWING;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.OVERLAPPING;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.OVERLAPPING_AT_BEGIN;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.OVERLAPPING_AT_END;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.PRECEDING;
+
+import java.util.List;
+
+import org.apache.uima.fit.util.SelectionAssert.TestCase;
+
+public class AnnotationPredicateTestData {
+  public static enum RelativePosition {
+    COLOCATED,
+    OVERLAPPING,
+    OVERLAPPING_AT_BEGIN,
+    OVERLAPPING_AT_END,
+    COVERING,
+    COVERED_BY,
+    PRECEDING,
+    FOLLOWING,
+    BEGINNING_WITH,
+    ENDING_WITH
+  }
+  
+  // Used as fixed references for the annotation relation cases.
+  private static final int BEGIN = 10;
+  private static final int END = 20;
+  private static final int Z_POS = 10;
+
+  public static final TestCase T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13;
+  
+  public static final List<TestCase> NON_ZERO_WIDTH_TEST_CASES = asList(
+      T1 = new TestCase("1) Y begins and ends after X (### [---])", 
+          p -> p.apply(BEGIN, END, END + 1, MAX_VALUE),
+          asList(PRECEDING)),
+      T2 = new TestCase("2) Y begins at X's end and ends after X (###[---])", 
+          p -> p.apply(BEGIN, END, END, MAX_VALUE),
+          asList(PRECEDING)),
+      T3 = new TestCase("3) Y begins within and ends after X (##[#--])", 
+          p -> p.apply(BEGIN, END, END - 1 , MAX_VALUE),
+          asList(OVERLAPPING, OVERLAPPING_AT_BEGIN)),
+      T4 = new TestCase("4) Y begins and ends at X's boundries ([###])", 
+          p -> p.apply(BEGIN, END, BEGIN, END),
+          asList(OVERLAPPING, COLOCATED, COVERED_BY, COVERING, BEGINNING_WITH, ENDING_WITH)),
+      T5 = new TestCase("5) Y begins and ends within X (#[#]#)", 
+          p -> p.apply(BEGIN, END, BEGIN + 1, END - 1),
+          asList(OVERLAPPING, COVERING)),
+      T6 = new TestCase("6) Y begins at and ends before X's boundries ([##]#)", 
+          p -> p.apply(BEGIN, END, BEGIN, END - 1),
+          asList(OVERLAPPING, COVERING, BEGINNING_WITH, OVERLAPPING_AT_END)),
+      T7 = new TestCase("7) Y begins after and ends at X's boundries (#[##])", 
+          p -> p.apply(BEGIN, END, BEGIN + 1, END),
+          asList(OVERLAPPING, COVERING, ENDING_WITH, OVERLAPPING_AT_BEGIN)),
+      T8 = new TestCase("8) Y begins before and ends after X's boundries ([-###-])", 
+          p -> p.apply(BEGIN, END, BEGIN - 1, END + 1),
+          asList(OVERLAPPING, COVERED_BY)),
+      T9 = new TestCase("9) X starts where Y begins and ends within Y ([##-])", 
+          p -> p.apply(BEGIN, END, BEGIN, END + 1),
+          asList(OVERLAPPING, COVERED_BY, BEGINNING_WITH)),
+      T10 = new TestCase("10) X starts within Y and ends where Y ends ([-##])", 
+          p -> p.apply(BEGIN, END, BEGIN - 1, END),
+          asList(OVERLAPPING, COVERED_BY, ENDING_WITH)),
+      T11 = new TestCase("11) Y begins before and ends within X ([--#]##)", 
+          p -> p.apply(BEGIN, END, 0, BEGIN + 1),
+          asList(OVERLAPPING, OVERLAPPING_AT_END)),
+      T12 = new TestCase("12) Y begins before and ends where X begins ([---]###)", 
+          p -> p.apply(BEGIN, END, 0, BEGIN),
+          asList(FOLLOWING)),
+      T13 = new TestCase("13) Y begins and ends before X begins ([---] ###)", 
+          p -> p.apply(BEGIN, END, 0, BEGIN - 1),
+          asList(FOLLOWING)));
+
+  public static final TestCase TZ1, TZ2, TZ3, TZ4, TZ5, TZ6, TZ7, TZ8, TZ9, TZ10, TZ11;
+
+  public static final List<TestCase> ZERO_WIDTH_TEST_CASES = asList(
+      TZ1 = new TestCase("Z1) Zero-width X before Y start (# [---])", 
+          p -> p.apply(Z_POS, Z_POS, Z_POS + 10, Z_POS + 20),
+          asList(PRECEDING)),
+      TZ2 = new TestCase("Z2) Zero-width Y after X's end (### |)", 
+          p -> p.apply(BEGIN, END, END + 1, END + 1),
+          asList(PRECEDING)),
+      TZ3 = new TestCase("Z3) Zero-width X at Y's start (#---])", 
+          p -> p.apply(Z_POS, Z_POS, Z_POS, Z_POS + 10),
+          asList(PRECEDING, OVERLAPPING, COVERED_BY, BEGINNING_WITH)),
+      TZ4 = new TestCase("Z4) Zero-width X at Y's end ([---#)", 
+          p -> p.apply(Z_POS, Z_POS, Z_POS-10, Z_POS),
+          asList(FOLLOWING, OVERLAPPING, COVERED_BY, ENDING_WITH)),
+      TZ5 = new TestCase("Z5) Zero-width Y where X begins (|###)", 
+          p -> p.apply(BEGIN, END, BEGIN, BEGIN),
+          asList(FOLLOWING, OVERLAPPING, COVERING, BEGINNING_WITH)),
+      TZ6 = new TestCase("Z6) Zero-width Y within X (#|#)", 
+          p -> p.apply(BEGIN, END, BEGIN + 1, BEGIN + 1),
+          asList(OVERLAPPING, COVERING)),
+      TZ7 = new TestCase("Z7) Zero-width Y at X's end (###|)", 
+          p -> p.apply(BEGIN, END, END, END),
+          asList(PRECEDING, OVERLAPPING, COVERING, ENDING_WITH)),
+      TZ8 = new TestCase("Z8) Zero-width X with Y (-|-)", 
+          p -> p.apply(Z_POS, Z_POS, Z_POS - 5, Z_POS + 5),
+          asList(OVERLAPPING, COVERED_BY)),
+      TZ9 = new TestCase("Z9) Zero-width X after Y's end ([---] #)", 
+          p -> p.apply(Z_POS, Z_POS, Z_POS - 10, Z_POS - 5),
+          asList(FOLLOWING)),
+      TZ10 = new TestCase("Z10) Zero-width Y before X begins (| ###)", 
+          p -> p.apply(BEGIN, END, BEGIN - 1, BEGIN - 1),
+          asList(FOLLOWING)),
+      TZ11 = new TestCase("Z11) Zero-width X matches zero-width Y start/end (#)", 
+          p -> p.apply(Z_POS, Z_POS, Z_POS, Z_POS),
+          asList(FOLLOWING, PRECEDING, OVERLAPPING, COVERED_BY, COVERING, COLOCATED, BEGINNING_WITH, 
+              ENDING_WITH)));
+}
diff --git a/uimafit-core/src/test/java/org/apache/uima/fit/util/AnnotationPredicates.java b/uimafit-core/src/test/java/org/apache/uima/fit/util/AnnotationPredicates.java
new file mode 100644
index 0000000..d503799
--- /dev/null
+++ b/uimafit-core/src/test/java/org/apache/uima/fit/util/AnnotationPredicates.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.uima.fit.util;
+
+import org.apache.uima.cas.text.AnnotationFS;
+
+/**
+ * This class is back-ported for testing only from UIMAv3. It is used to check that the behavior
+ * of uimaFIT v2 and uimaFITv2 with respect to the select utilities is aligned.
+ */
+final class AnnotationPredicates {
+  public static boolean coveredBy(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aYBegin <= aXBegin && aXEnd <= aYEnd;
+  }
+
+  public static boolean coveredBy(AnnotationFS aX, int aYBegin, int aYEnd) {
+    return aYBegin <= aX.getBegin() && aX.getEnd() <= aYEnd;
+  }
+
+  /**
+   * Y is starting before or at the same position as A and ends after or at the
+   * same position as X.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X is covered by Y.
+   */
+  public static boolean coveredBy(AnnotationFS aX, AnnotationFS aY) {
+    return aY.getBegin() <= aX.getBegin() && aX.getEnd() <= aY.getEnd();
+  }
+
+  public static boolean covering(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aXBegin <= aYBegin && aYEnd <= aXEnd;
+  }
+
+  public static boolean covering(AnnotationFS aX, int aYBegin, int aYEnd) {
+    return aX.getBegin() <= aYBegin && aYEnd <= aX.getEnd();
+  }
+
+  /**
+   * X is starting before or at the same position as Y and ends after or at the
+   * same position as Y.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X is covering Y.
+   */
+  public static boolean covering(AnnotationFS aX, AnnotationFS aY) {
+    return aX.getBegin() <= aY.getBegin() && aY.getEnd() <= aX.getEnd();
+  }
+
+  public static boolean colocated(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aXBegin == aYBegin && aXEnd == aYEnd;
+  }
+
+  public static boolean colocated(AnnotationFS aX, int aYBegin, int aYEnd) {
+    return aX.getBegin() == aYBegin && aX.getEnd() == aYEnd;
+  }
+
+  /**
+   * X starts and ends at the same position as Y.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X is at the same location as Y.
+   */
+  public static boolean colocated(AnnotationFS aX, AnnotationFS aY) {
+    return aX.getBegin() == aY.getBegin() && aX.getEnd() == aY.getEnd();
+  }
+
+  public static boolean overlapping(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aYBegin == aXBegin || aYEnd == aXEnd || (aXBegin < aYEnd && aYBegin < aXEnd);
+  }
+
+  public static boolean overlapping(AnnotationFS aX, int aYBegin, int aYEnd) {
+    int xBegin = aX.getBegin();
+    int xEnd = aX.getEnd();
+    return aYBegin == xBegin || aYEnd == xEnd || (xBegin < aYEnd && aYBegin < xEnd);
+  }
+
+  /**
+   * The intersection of the spans X and Y is non-empty. If either X or Y have a
+   * zero-width, then the intersection is considered to be non-empty if the begin
+   * of X is either within Y or the same as the begin of Y - and vice versa.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X overlaps with Y in any way.
+   */
+  public static boolean overlapping(AnnotationFS aX, AnnotationFS aY) {
+    int xBegin = aX.getBegin();
+    int xEnd = aX.getEnd();
+    int yBegin = aY.getBegin();
+    int yEnd = aY.getEnd();
+    return yBegin == xBegin || yEnd == xEnd || (xBegin < yEnd && yBegin < xEnd);
+  }
+
+  public static boolean overlappingAtBegin(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aXBegin < aYBegin && aYBegin < aXEnd && aXEnd <= aYEnd;
+  }
+
+  public static boolean overlappingAtBegin(AnnotationFS aX, int aYBegin, int aYEnd) {
+    int xEnd = aX.getEnd();
+    return aYBegin < xEnd && xEnd <= aYEnd && aX.getBegin() < aYBegin;
+  }
+
+  /**
+   * X is starting before or at the same position as Y and ends before Y ends.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X overlaps Y on the left.
+   */
+  public static boolean overlappingAtBegin(AnnotationFS aX, AnnotationFS aY) {
+    int xEnd = aX.getEnd();
+    int yBegin = aY.getBegin();
+    return yBegin < xEnd && xEnd <= aY.getEnd() && aX.getBegin() < yBegin;
+  }
+
+  public static boolean overlappingAtEnd(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aYBegin <= aXBegin && aXBegin < aYEnd && aYEnd < aXEnd;
+  }
+
+  public static boolean overlappingAtEnd(AnnotationFS aX, int aYBegin, int aYEnd) {
+    int xBegin = aX.getBegin();
+    return aYBegin <= xBegin && xBegin < aYEnd && aYEnd < aX.getEnd();
+  }
+
+  /**
+   * X is starting after Y starts and ends after or at the same position as Y.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X overlaps Y on the right.
+   */
+  public static boolean overlappingAtEnd(AnnotationFS aX, AnnotationFS aY) {
+    int xBegin = aX.getBegin();
+    int yEnd = aY.getEnd();
+    return xBegin < yEnd && aY.getBegin() <= xBegin && yEnd < aX.getEnd();
+  }
+
+  public static boolean following(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aXBegin >= aYEnd;
+  }
+
+  public static boolean following(AnnotationFS aX, int aYBegin, int aYEnd) {
+    return aX.getBegin() >= aYEnd;
+  }
+
+  /**
+   * X starts at or after the position that Y ends.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X is right of Y.
+   */
+  public static boolean following(AnnotationFS aX, AnnotationFS aY) {
+    return aX.getBegin() >= aY.getEnd();
+  }
+
+  public static boolean preceding(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aYBegin >= aXEnd;
+  }
+
+  public static boolean preceding(AnnotationFS aX, int aYBegin, int aYEnd) {
+    return aYBegin >= aX.getEnd();
+  }
+
+  /**
+   * X ends before or at the position that Y starts.
+   * 
+   * @param aX
+   *          X
+   * @param aY
+   *          Y
+   * @return whether X left of Y.
+   */
+  public static boolean preceding(AnnotationFS aX, AnnotationFS aY) {
+    return aY.getBegin() >= aX.getEnd();
+  }
+  
+  public static boolean beginningWith(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aXBegin == aYBegin;
+  }
+
+  public static boolean beginningWith(AnnotationFS aX, int aYBegin, int aYEnd) {
+    return aX.getBegin() == aYBegin;
+  }
+
+  public static boolean beginningWith(AnnotationFS aX, AnnotationFS aY) {
+    return aX.getBegin() == aY.getBegin();
+  }
+
+  public static boolean endingWith(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+    return aXEnd == aYEnd;
+  }
+  public static boolean endingWith(AnnotationFS aX, int aYBegin, int aYEnd) {
+    return aX.getEnd() == aYEnd;
+  }
+
+  public static boolean endingWith(AnnotationFS aX, AnnotationFS aY) {
+    return aX.getEnd() == aY.getEnd();
+  }
+}
diff --git a/uimafit-core/src/test/java/org/apache/uima/fit/util/CasUtilTest.java b/uimafit-core/src/test/java/org/apache/uima/fit/util/CasUtilTest.java
index 769502e..8589108 100644
--- a/uimafit-core/src/test/java/org/apache/uima/fit/util/CasUtilTest.java
+++ b/uimafit-core/src/test/java/org/apache/uima/fit/util/CasUtilTest.java
@@ -18,24 +18,48 @@
  */
 package org.apache.uima.fit.util;
 
+import static java.lang.Integer.MAX_VALUE;
 import static java.util.Arrays.asList;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.StreamSupport.stream;
 import static org.apache.uima.fit.factory.TypeSystemDescriptionFactory.createTypeSystemDescription;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.NON_ZERO_WIDTH_TEST_CASES;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.ZERO_WIDTH_TEST_CASES;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.COLOCATED;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.COVERED_BY;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.COVERING;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.FOLLOWING;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.PRECEDING;
+import static org.apache.uima.fit.util.AnnotationPredicates.colocated;
+import static org.apache.uima.fit.util.AnnotationPredicates.coveredBy;
+import static org.apache.uima.fit.util.AnnotationPredicates.covering;
+import static org.apache.uima.fit.util.AnnotationPredicates.following;
+import static org.apache.uima.fit.util.AnnotationPredicates.preceding;
+import static org.apache.uima.fit.util.CasUtil.exists;
 import static org.apache.uima.fit.util.CasUtil.getAnnotationType;
 import static org.apache.uima.fit.util.CasUtil.getType;
 import static org.apache.uima.fit.util.CasUtil.iterator;
 import static org.apache.uima.fit.util.CasUtil.iteratorFS;
 import static org.apache.uima.fit.util.CasUtil.select;
+import static org.apache.uima.fit.util.CasUtil.selectAt;
 import static org.apache.uima.fit.util.CasUtil.selectByIndex;
+import static org.apache.uima.fit.util.CasUtil.selectCovered;
+import static org.apache.uima.fit.util.CasUtil.selectCovering;
 import static org.apache.uima.fit.util.CasUtil.selectFS;
+import static org.apache.uima.fit.util.CasUtil.selectFollowing;
+import static org.apache.uima.fit.util.CasUtil.selectPreceding;
 import static org.apache.uima.fit.util.CasUtil.toText;
-import static org.apache.uima.fit.util.CasUtil.exists;
+import static org.apache.uima.fit.util.SelectionAssert.assertSelection;
+import static org.apache.uima.fit.util.SelectionAssert.assertSelectionIsEqualOnRandomData;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
+import java.util.List;
 
 import org.apache.uima.UIMAException;
 import org.apache.uima.cas.ArrayFS;
@@ -45,6 +69,7 @@ import org.apache.uima.cas.Type;
 import org.apache.uima.cas.text.AnnotationFS;
 import org.apache.uima.fit.ComponentTestBase;
 import org.apache.uima.fit.type.Token;
+import org.apache.uima.fit.util.SelectionAssert.TestCase;
 import org.apache.uima.jcas.cas.TOP;
 import org.apache.uima.jcas.tcas.Annotation;
 import org.apache.uima.util.CasCreationUtils;
@@ -55,6 +80,9 @@ import org.junit.Test;
  * 
  */
 public class CasUtilTest extends ComponentTestBase {
+  private List<TestCase> defaultPredicatesTestCases = union(NON_ZERO_WIDTH_TEST_CASES,
+          ZERO_WIDTH_TEST_CASES);
+  
   @Test
   public void testGetType() {
     String text = "Rot wood cheeses dew?";
@@ -205,4 +233,111 @@ public class CasUtilTest extends ComponentTestBase {
 
     assertTrue(exists(cas, tokenType));
   }
+  
+  @Test
+  public void thatSelectFollowingBehaviorAlignsWithPrecedingPredicate() throws Exception {
+    // In order to find annotations that X is preceding, we select the following annotations
+    assertSelection(
+        PRECEDING,
+        (cas, type, x, y) -> selectFollowing(cas, type, x, MAX_VALUE).contains(y),
+        defaultPredicatesTestCases);
+  }
+  
+  @Test
+  public void thatSelectPrecedingBehaviorAlignsWithPrecedingPredicateOnRandomData() throws Exception
+  {
+    assertSelectionIsEqualOnRandomData(
+        (cas, type, context) -> stream(cas.getAnnotationIndex(type).spliterator(), false)
+            .filter(candidate -> preceding(candidate, context))
+            .collect(toList()),
+        (cas, type, context) -> selectPreceding(cas, type, context, MAX_VALUE));
+  }
+
+  @Test
+  public void thatSelectPrecedingBehaviorAlignsWithFollowingPredicate() throws Exception {
+    // In order to find annotations that X is following, we select the preceding annotations
+    assertSelection(
+        FOLLOWING,
+        (cas, type, x, y) -> selectPreceding(cas, type, x, MAX_VALUE).contains(y),
+        defaultPredicatesTestCases);
+  }
+  
+  @Test
+  public void thatSelectFollowingBehaviorAlignsWithFollowingPredicateOnRandomData() throws Exception
+  {
+    assertSelectionIsEqualOnRandomData(
+        (cas, type, context) -> stream(cas.getAnnotationIndex(type).spliterator(), false)
+            .filter(candidate -> following(candidate, context))
+            .collect(toList()),
+        (cas, type, context) -> selectFollowing(cas, type, context, MAX_VALUE));
+  }
+
+  @Test
+  public void thatSelectCoveringBehaviorAlignsWithCoveredByPredicate() throws Exception {
+    // X covered by Y means that Y is covering X, so we need to select the covering annotations
+    // below.
+    assertSelection(
+        COVERED_BY,
+        (cas, type, x, y) -> selectCovering(cas, type, x).contains(y),
+        defaultPredicatesTestCases);
+  }
+  
+  @Test
+  public void thatSelectCoveredBehaviorAlignsWithCoveredByPredicateOnRandomData() throws Exception
+  {
+    assertSelectionIsEqualOnRandomData(
+        (cas, type, context) -> stream(cas.getAnnotationIndex(type).spliterator(), false)
+            .filter(candidate -> coveredBy(candidate, context))
+            .collect(toList()),
+        (cas, type, context) -> selectCovered(cas, type, context));
+  }
+
+  @Test
+  public void thatSelectCoveredBehaviorAlignsWithCoveringPredicate() throws Exception {
+    // X covering Y means that Y is covered by Y, so we need to select the covered by annotations
+    // below.
+    assertSelection(
+        COVERING,
+        (cas, type, x, y) -> selectCovered(cas, type, x).contains(y),
+        defaultPredicatesTestCases);
+  }
+
+  @Test
+  public void thatSelectFsBehaviorAlignsWithCoveringPredicateOnRandomData() throws Exception
+  {
+    assertSelectionIsEqualOnRandomData(
+        (cas, type, context) -> stream(cas.getAnnotationIndex(type).spliterator(), false)
+            .filter(candidate -> covering(candidate, context))
+            .collect(toList()),
+        (cas, type, context) -> selectCovering(cas, type, context));
+  }
+  
+  @Test
+  public void thatSelectAtBehaviorAlignsWithColocatedPredicate() throws Exception {
+    // X covering Y means that Y is covered by Y, so we need to select the covered by annotations
+    // below.
+    assertSelection(
+        COLOCATED,
+        (cas, type, x, y) -> selectAt(cas, type, x.getBegin(), x.getEnd()).contains(y),
+        defaultPredicatesTestCases);
+  }  
+
+  @Test
+  public void thatSelectAtBehaviorAlignsWithColocatedPredicateOnRandomData() throws Exception
+  {
+    assertSelectionIsEqualOnRandomData(
+        (cas, type, context) -> stream(cas.getAnnotationIndex(type).spliterator(), false)
+            .filter(candidate -> colocated(candidate, context))
+            .collect(toList()),
+        (cas, type, context) -> selectAt(cas, type, context.getBegin(), context.getEnd()));
+  }
+  
+  @SafeVarargs
+  public static <T> List<T> union(List<T>... aLists) {
+      List<T> all = new ArrayList<>();
+      for (List<T> list : aLists) {
+        all.addAll(list);
+      }
+      return all;
+  }
 }
diff --git a/uimafit-core/src/test/java/org/apache/uima/fit/util/JCasUtilTest.java b/uimafit-core/src/test/java/org/apache/uima/fit/util/JCasUtilTest.java
index 7094af4..091a6e1 100644
--- a/uimafit-core/src/test/java/org/apache/uima/fit/util/JCasUtilTest.java
+++ b/uimafit-core/src/test/java/org/apache/uima/fit/util/JCasUtilTest.java
@@ -318,7 +318,9 @@ public class JCasUtilTest extends ComponentTestBase {
     // print(a1);
     // System.out.println("--- Optimized");
     // print(a2);
-    assertEquals("Container: [" + t.getBegin() + ".." + t.getEnd() + "]", a1, a2);
+    assertThat(a2)
+        .as("Container: [" + t.getBegin() + ".." + t.getEnd() + "]")
+        .containsExactlyElementsOf((Iterable) a1);
   }
 
   @Test
@@ -668,7 +670,7 @@ public class JCasUtilTest extends ComponentTestBase {
   }
 
   @Test
-  public void thatSelectFollowingDoesNotFindZeroWidthAnnotationAtEnd()
+  public void thatSelectFollowingDoesFindZeroWidthAnnotationAtEnd()
   {
     Annotation a1 = new Annotation(jCas, 10, 20);
     Annotation a2 = new Annotation(jCas, 20, 20);
@@ -678,11 +680,11 @@ public class JCasUtilTest extends ComponentTestBase {
     List<Annotation> selection = selectFollowing(Annotation.class, a1, MAX_VALUE);
     
     assertThat(selection)
-            .isEmpty();
+            .containsExactly(a2);
   }
 
   @Test
-  public void thatSelectPrecedingDoesNotFindZeroWidthAnnotationAtStart()
+  public void thatSelectPrecedingDoesFindZeroWidthAnnotationAtStart()
   {
     Annotation a1 = new Annotation(jCas, 10, 20);
     Annotation a2 = new Annotation(jCas, 10, 10);
@@ -692,11 +694,11 @@ public class JCasUtilTest extends ComponentTestBase {
     List<Annotation> selection = selectPreceding(Annotation.class, a1, MAX_VALUE);
     
     assertThat(selection)
-            .isEmpty();
+            .containsExactly(a2);
   }
 
   @Test
-  public void thatSelectPrecedingOnZeroWidthDoesNotFindAnnotationEndingAtSameLocation()
+  public void thatSelectPrecedingOnZeroWidthDoesFindAnnotationEndingAtSameLocation()
   {
     Annotation a1 = new Annotation(jCas, 10, 20);
     Annotation a2 = new Annotation(jCas, 20, 20);
@@ -706,7 +708,7 @@ public class JCasUtilTest extends ComponentTestBase {
     List<Annotation> selection = selectPreceding(Annotation.class, a2, MAX_VALUE);
     
     assertThat(selection)
-            .isEmpty();
+            .containsExactly(a1);
   }
 
   @Test
diff --git a/uimafit-core/src/test/java/org/apache/uima/fit/util/SelectionAssert.java b/uimafit-core/src/test/java/org/apache/uima/fit/util/SelectionAssert.java
new file mode 100644
index 0000000..22f21cd
--- /dev/null
+++ b/uimafit-core/src/test/java/org/apache/uima/fit/util/SelectionAssert.java
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.uima.fit.util;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.function.Function;
+
+import org.apache.uima.UIMAFramework;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.text.AnnotationFS;
+import org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition;
+import org.apache.uima.resource.metadata.TypeSystemDescription;
+import org.apache.uima.util.CasCreationUtils;
+import org.assertj.core.api.AutoCloseableSoftAssertions;
+
+public class SelectionAssert {
+  public static void assertSelection(RelativePosition aCondition, RelativeAnnotationPredicate aPredicate, 
+      List<TestCase> aTestCases)
+      throws Exception {
+    CAS cas = CasCreationUtils.createCas((TypeSystemDescription) null, null, null);
+    Type type = cas.getAnnotationType();
+
+    try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) {
+      for (TestCase testCase : aTestCases) {
+        cas.reset();
+
+        // Create annotations
+        AnnotationFS x = cas.createAnnotation(type, 0, 0);
+        AnnotationFS y = cas.createAnnotation(type, 0, 0);
+
+        // Position the annotations according to the test data
+        testCase.getTest().apply((beginA, endA, beginB, endB) -> {
+          FSUtil.setFeature(x, CAS.FEATURE_BASE_NAME_BEGIN, beginA);
+          FSUtil.setFeature(x, CAS.FEATURE_BASE_NAME_END, endA);
+          FSUtil.setFeature(y, CAS.FEATURE_BASE_NAME_BEGIN, beginB);
+          FSUtil.setFeature(y, CAS.FEATURE_BASE_NAME_END, endB);
+          cas.addFsToIndexes(x);
+          cas.addFsToIndexes(y);
+          return true;
+        });
+
+        softly.assertThat(aPredicate.apply(cas, type, x, y)).as(testCase.getDescription())
+            .isEqualTo(testCase.getValidPositions().contains(aCondition));
+      }
+    }
+  }
+
+  public static void assertSelectionIsEqualOnRandomData(TypeByContextSelector aExpected, TypeByContextSelector aActual)
+      throws Exception {
+    final int ITERATIONS = 30;
+    final int TYPES = 5;
+
+    TypeSystemDescription tsd = UIMAFramework.getResourceSpecifierFactory().createTypeSystemDescription();
+    
+    Map<String, Type> types = new LinkedHashMap<>();
+    for (int i = 0; i < TYPES; i++) {
+      String typeName = "test.Type" + (i + 1);
+      tsd.addType(typeName, "", CAS.TYPE_NAME_ANNOTATION);
+      types.put(typeName, null);
+    }
+    
+    CAS randomCas = CasCreationUtils.createCas(tsd, null, null, null);
+
+    for (String typeName : types.keySet()) {
+      types.put(typeName, randomCas.getTypeSystem().getType(typeName));
+    }
+    
+    System.out.print("Iteration: ");
+    try {
+      Iterator<Type> ti = types.values().iterator();
+      Type type1 = ti.next();
+      Type type2 = ti.next();
+      
+      for (int i = 0; i < ITERATIONS; i++) {
+        if (i % 10 == 0) {
+          System.out.print(i);
+        }
+        else {
+          System.out.print(".");
+        }
+  
+        initRandomCas(randomCas, 3 * i, 0, types.values().toArray(new Type[types.size()]));
+  
+        for (AnnotationFS context : randomCas.getAnnotationIndex(type1)) {
+          List<AnnotationFS> expected = aExpected.select(randomCas, type2, context);
+          List<AnnotationFS> actual = aActual.select(randomCas, type2, context);
+  
+          assertThat(actual)
+              .as("Selected [%s] with context [%s]@[%d..%d]", type2.getShortName(), 
+                  type1.getShortName(), context.getBegin(), context.getEnd())
+              .containsExactlyElementsOf(expected);
+        }
+      }
+      System.out.print(ITERATIONS);
+    }
+    finally {
+      System.out.println();
+    }
+  }
+
+  private static void initRandomCas(CAS aCas, int aSize, int aMinimumWidth, Type... aTypes) {
+    Random rnd = new Random();
+
+    List<Type> types = new ArrayList<>(asList(aTypes));
+
+    // Shuffle the types
+    for (int n = 0; n < 10; n++) {
+      Type t = types.remove(rnd.nextInt(types.size()));
+      types.add(t);
+    }
+
+    // Randomly generate annotations
+    for (int n = 0; n < aSize; n++) {
+      for (Type t : types) {
+        int begin = rnd.nextInt(100);
+        int end = begin + rnd.nextInt(30) + aMinimumWidth;
+        aCas.addFsToIndexes(aCas.createAnnotation(t, begin, end));
+      }
+    }
+  }
+
+  @FunctionalInterface
+  public static interface RelativeAnnotationPredicate {
+    boolean apply(CAS cas, Type type, AnnotationFS x, AnnotationFS y);
+  }
+
+  @FunctionalInterface
+  public static interface TypeByContextSelector {
+    List<AnnotationFS> select(CAS aCas, Type aType, AnnotationFS aContext);
+  }
+  
+  @FunctionalInterface
+  public static interface RelativePositionPredicate {
+    boolean apply(int beginA, int endA, int beginB, int endB);
+  }
+
+  public static class TestCase {
+    private final String description;
+
+    private final Function<RelativePositionPredicate, Boolean> predicate;
+    
+    private final List<RelativePosition> validPositions;
+
+    public TestCase(String aDescription, Function<RelativePositionPredicate, Boolean> aPredicate, List<RelativePosition> aValidPositions) {
+      description = aDescription;
+      predicate = aPredicate;
+      validPositions = aValidPositions;
+    }
+
+    public String getDescription() {
+      return description;
+    }
+
+    public Function<RelativePositionPredicate, Boolean> getTest() {
+      return predicate;
+    }
+    
+    public List<RelativePosition> getValidPositions() {
+      return validPositions;
+    }
+  }
+}