JAL-3054 table tooltip follow mouse in a sensible manner
[jalview.git] / test / jalview / datamodel / features / NCListTest.java
index ca4288c..2c7f752 100644 (file)
@@ -1,13 +1,21 @@
 package jalview.datamodel.features;
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
 import static org.testng.Assert.assertTrue;
 
+import jalview.datamodel.ContiguousI;
+import jalview.datamodel.Range;
+import jalview.datamodel.SequenceFeature;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Random;
 
+import junit.extensions.PA;
+
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
@@ -160,45 +168,137 @@ public class NCListTest
   }
 
   /**
-   * Do a number of pseudo-random (reproducible) builds of an NCList, with
-   * checks for validity of the data structure, and searches for (pseudo-random)
-   * overlap ranges, for greater (but not perfect!) confidence that corner cases
-   * are being handled correctly.
+   * Do a number of pseudo-random (reproducible) builds of an NCList, to
+   * exercise as many methods of the class as possible while generating the
+   * range of possible structure topologies
+   * <ul>
+   * <li>verify that add adds an entry and increments size</li>
+   * <li>...except where the entry is already contained (by equals test)</li>
+   * <li>verify that the structure is valid at all stages of construction</li>
+   * <li>generate, run and verify a range of overlap queries</li>
+   * <li>tear down the structure by deleting entries, verifying correctness at
+   * each stage</li>
+   * </ul>
    */
   @Test(groups = "Functional", dataProvider = "scalesOfLife")
-  public void testAdd_FindOverlaps_pseudoRandom(Integer scale)
+  public void test_pseudoRandom(Integer scale)
   {
-    NCList<Range> ncl = new NCList<Range>();
-    int count = 0;
-    List<Range> ranges = new ArrayList<Range>(scale);
+    NCList<SequenceFeature> ncl = new NCList<SequenceFeature>();
+    List<SequenceFeature> features = new ArrayList<SequenceFeature>(scale);
     
-    for (int i = 0; i < scale; i++)
+    testAdd_pseudoRandom(scale, ncl, features);
+
+    /*
+     * sort the list of added ranges - this doesn't affect the test,
+     * just makes it easier to inspect the data in the debugger
+     */
+    Collections.sort(features, sorter);
+
+    testFindOverlaps_pseudoRandom(ncl, scale, features);
+
+    testDelete_pseudoRandom(ncl, features);
+  }
+
+  /**
+   * Pick randomly selected entries to delete in turn, checking the NCList size
+   * and validity at each stage, until it is empty
+   * 
+   * @param ncl
+   * @param features
+   */
+  protected void testDelete_pseudoRandom(NCList<SequenceFeature> ncl,
+          List<SequenceFeature> features)
+  {
+    int deleted = 0;
+
+    while (!features.isEmpty())
+    {
+      assertEquals(ncl.size(), features.size());
+      int toDelete = random.nextInt(features.size());
+      SequenceFeature entry = features.get(toDelete);
+      assertTrue(ncl.contains(entry), String.format(
+              "NCList doesn't contain entry [%d] '%s'!", deleted,
+              entry.toString()));
+
+      ncl.delete(entry);
+      assertFalse(ncl.contains(entry), String.format(
+              "NCList still contains deleted entry [%d] '%s'!", deleted,
+              entry.toString()));
+      features.remove(toDelete);
+      deleted++;
+
+      assertTrue(ncl.isValid(), String.format(
+              "NCList invalid after %d deletions, last deleted was '%s'",
+              deleted, entry.toString()));
+
+      /*
+       * brute force check that deleting one entry didn't delete any others
+       */
+      for (int i = 0; i < features.size(); i++)
+      {
+        SequenceFeature sf = features.get(i);
+        assertTrue(ncl.contains(sf), String.format(
+                        "NCList doesn't contain entry [%d] %s after deleting '%s'!",
+                        i, sf.toString(), entry.toString()));
+      }
+    }
+    assertEquals(ncl.size(), 0); // all gone
+  }
+
+  /**
+   * Randomly generate entries and add them to the NCList, checking its validity
+   * and size at each stage. A few entries should be duplicates (by equals test)
+   * so not get added.
+   * 
+   * @param scale
+   * @param ncl
+   * @param features
+   */
+  protected void testAdd_pseudoRandom(Integer scale,
+          NCList<SequenceFeature> ncl,
+          List<SequenceFeature> features)
+  {
+    int count = 0;
+    final int size = 50;
+
+    for (int i = 0; i < size; i++)
     {
       int r1 = random.nextInt(scale + 1);
       int r2 = random.nextInt(scale + 1);
       int from = Math.min(r1, r2);
       int to = Math.max(r1, r2);
-      Range range = new Range(from, to);
-      ncl.add(range);
-      ranges.add(range);
+
+      /*
+       * choice of two feature values means that occasionally an identical
+       * feature may be generated, in which case it should not be added 
+       */
+      float value = (float) i % 2;
+      SequenceFeature feature = new SequenceFeature("Pfam", "", from, to,
+              value, "group");
+
+      /*
+       * add to NCList - with duplicate entries (by equals) disallowed
+       */
+      ncl.add(feature, false);
+      if (features.contains(feature))
+      {
+        System.out.println("Duplicate feature generated "
+                + feature.toString());
+      }
+      else
+      {
+        features.add(feature);
+        count++;
+      }
     
       /*
        * check list format is valid at each stage of its construction
        */
       assertTrue(ncl.isValid(),
               String.format("Failed for scale = %d, i=%d", scale, i));
-      count++;
-      assertEquals(ncl.getSize(), count);
+      assertEquals(ncl.size(), count);
     }
     // System.out.println(ncl.prettyPrint());
-    
-    /*
-     * sort the list of added ranges - this doesn't affect the test,
-     * just makes it easier to inspect the data in the debugger
-     */
-    Collections.sort(ranges, sorter);
-    
-    testFindOverlaps(ncl, scale, ranges);
   }
 
   /**
@@ -209,11 +309,12 @@ public class NCListTest
    *          the NCList to query
    * @param scale
    *          ncl maximal range is [0, scale]
-   * @param ranges
+   * @param features
    *          a list of the ranges stored in ncl
    */
-  protected void testFindOverlaps(NCList<Range> ncl, int scale,
-          List<Range> ranges)
+  protected void testFindOverlaps_pseudoRandom(NCList<SequenceFeature> ncl,
+          int scale,
+          List<SequenceFeature> features)
   {
     int halfScale = scale / 2;
     int minIterations = 20;
@@ -269,7 +370,7 @@ public class NCListTest
         }
       }
 
-      verifyFindOverlaps(ncl, from, to, ranges);
+      verifyFindOverlaps(ncl, from, to, features);
     }
   }
 
@@ -280,20 +381,20 @@ public class NCListTest
    * @param ncl
    * @param from
    * @param to
-   * @param ranges
+   * @param features
    */
-  protected void verifyFindOverlaps(NCList<Range> ncl, int from, int to,
-          List<Range> ranges)
+  protected void verifyFindOverlaps(NCList<SequenceFeature> ncl, int from,
+          int to, List<SequenceFeature> features)
   {
-    List<Range> overlaps = ncl.findOverlaps(from, to);
+    List<SequenceFeature> overlaps = ncl.findOverlaps(from, to);
 
     /*
      * check returned entries do indeed overlap from-to range
      */
-    for (Range r : overlaps)
+    for (ContiguousI sf : overlaps)
     {
-      int begin = r.getBegin();
-      int end = r.getEnd();
+      int begin = sf.getBegin();
+      int end = sf.getEnd();
       assertTrue(begin <= to && end >= from, String.format(
               "[%d, %d] does not overlap query range [%d, %d]", begin, end,
               from, to));
@@ -303,13 +404,13 @@ public class NCListTest
      * check overlapping ranges are included in the results
      * (the test above already shows non-overlapping ranges are not)
      */
-    for (Range r : ranges)
+    for (ContiguousI sf : features)
     {
-      int begin = r.getBegin();
-      int end = r.getEnd();
+      int begin = sf.getBegin();
+      int end = sf.getEnd();
       if (begin <= to && end >= from)
       {
-        boolean found = overlaps.contains(r);
+        boolean found = overlaps.contains(sf);
         assertTrue(found, String.format(
                 "[%d, %d] missing in query range [%d, %d]", begin, end,
                 from, to));
@@ -355,7 +456,46 @@ public class NCListTest
   @Test(groups = "Functional")
   public void testDelete()
   {
-    assertTrue(false, "todo");
+    List<Range> ranges = new ArrayList<Range>();
+    Range r1 = new Range(20, 30);
+    ranges.add(r1);
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertTrue(ncl.getEntries().contains(r1));
+
+    Range r2 = new Range(20, 30);
+    assertFalse(ncl.delete(null)); // null argument
+    assertFalse(ncl.delete(r2)); // never added
+    assertTrue(ncl.delete(r1)); // success
+    assertTrue(ncl.getEntries().isEmpty());
+
+    /*
+     * tests where object.equals() == true
+     */
+    NCList<SequenceFeature> features = new NCList<SequenceFeature>();
+    SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    features.add(sf1);
+    assertEquals(sf1, sf2); // sf1.equals(sf2)
+    assertFalse(features.delete(sf2)); // equality is not enough for deletion
+    assertTrue(features.getEntries().contains(sf1)); // still there!
+    assertTrue(features.delete(sf1));
+    assertTrue(features.getEntries().isEmpty()); // gone now
+
+    /*
+     * test with duplicate objects in NCList
+     */
+    features.add(sf1);
+    features.add(sf1);
+    assertEquals(features.getEntries().size(), 2);
+    assertSame(features.getEntries().get(0), sf1);
+    assertSame(features.getEntries().get(1), sf1);
+    assertTrue(features.delete(sf1)); // first match only is deleted
+    assertTrue(features.contains(sf1));
+    assertEquals(features.size(), 1);
+    assertTrue(features.delete(sf1));
+    assertTrue(features.getEntries().isEmpty());
   }
 
   @Test(groups = "Functional")
@@ -389,4 +529,154 @@ public class NCListTest
     assertEquals(ncl.toString(), "[15-25, 20-30, 25-35, 40-50, 45-55]");
     assertTrue(ncl.isValid());
   }
+
+  /**
+   * Test the contains method (which uses object equals test)
+   */
+  @Test(groups = "Functional")
+  public void testContains()
+  {
+    NCList<SequenceFeature> ncl = new NCList<SequenceFeature>();
+    SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    SequenceFeature sf3 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "anothergroup");
+    ncl.add(sf1);
+
+    assertTrue(ncl.contains(sf1));
+    assertTrue(ncl.contains(sf2)); // sf1.equals(sf2)
+    assertFalse(ncl.contains(sf3)); // !sf1.equals(sf3)
+
+    /*
+     * make some deeper structure in the NCList
+     */
+    SequenceFeature sf4 = new SequenceFeature("type", "desc", 2, 9, 2f,
+            "group");
+    ncl.add(sf4);
+    assertTrue(ncl.contains(sf4));
+    SequenceFeature sf5 = new SequenceFeature("type", "desc", 4, 5, 2f,
+            "group");
+    SequenceFeature sf6 = new SequenceFeature("type", "desc", 6, 8, 2f,
+            "group");
+    ncl.add(sf5);
+    ncl.add(sf6);
+    assertTrue(ncl.contains(sf5));
+    assertTrue(ncl.contains(sf6));
+  }
+
+  @Test(groups = "Functional")
+  public void testIsValid()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    Range r1 = new Range(40, 50);
+    ranges.add(r1);
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertTrue(ncl.isValid());
+
+    Range r2 = new Range(42, 44);
+    ncl.add(r2);
+    assertTrue(ncl.isValid());
+    Range r3 = new Range(46, 48);
+    ncl.add(r3);
+    assertTrue(ncl.isValid());
+    Range r4 = new Range(43, 43);
+    ncl.add(r4);
+    assertTrue(ncl.isValid());
+
+    assertEquals(ncl.toString(), "[40-50 [42-44 [43-43], 46-48]]");
+    assertTrue(ncl.isValid());
+
+    PA.setValue(r1, "start", 43);
+    assertFalse(ncl.isValid()); // r2 not inside r1
+    PA.setValue(r1, "start", 40);
+    assertTrue(ncl.isValid());
+
+    PA.setValue(r3, "start", 41);
+    assertFalse(ncl.isValid()); // r3 should precede r2
+    PA.setValue(r3, "start", 46);
+    assertTrue(ncl.isValid());
+
+    PA.setValue(r4, "start", 41);
+    assertFalse(ncl.isValid()); // r4 not inside r2
+    PA.setValue(r4, "start", 43);
+    assertTrue(ncl.isValid());
+
+    PA.setValue(r4, "start", 44);
+    assertFalse(ncl.isValid()); // r4 has reverse range
+  }
+
+  @Test(groups = "Functional")
+  public void testPrettyPrint()
+  {
+    /*
+     * construct NCList from a list of ranges
+     * they are sorted then assembled into NCList subregions
+     * notice that 42-42 end up inside 41-46
+     */
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(40, 50));
+    ranges.add(new Range(45, 55));
+    ranges.add(new Range(40, 45));
+    ranges.add(new Range(41, 46));
+    ranges.add(new Range(42, 42));
+    ranges.add(new Range(42, 42));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertTrue(ncl.isValid());
+    assertEquals(ncl.toString(),
+            "[40-50 [40-45], 41-46 [42-42 [42-42]], 45-55]");
+    String expected = "40-50\n  40-45\n41-46\n  42-42\n    42-42\n45-55\n";
+    assertEquals(ncl.prettyPrint(), expected);
+
+    /*
+     * repeat but now add ranges one at a time
+     * notice that 42-42 end up inside 40-50 so we get
+     * a different but equal valid NCList structure
+     */
+    ranges.clear();
+    ncl = new NCList<Range>(ranges);
+    ncl.add(new Range(40, 50));
+    ncl.add(new Range(45, 55));
+    ncl.add(new Range(40, 45));
+    ncl.add(new Range(41, 46));
+    ncl.add(new Range(42, 42));
+    ncl.add(new Range(42, 42));
+    assertTrue(ncl.isValid());
+    assertEquals(ncl.toString(),
+            "[40-50 [40-45 [42-42 [42-42]], 41-46], 45-55]");
+    expected = "40-50\n  40-45\n    42-42\n      42-42\n  41-46\n45-55\n";
+    assertEquals(ncl.prettyPrint(), expected);
+  }
+
+  /**
+   * A test that shows different valid trees can be constructed from the same
+   * set of ranges, depending on the order of construction
+   */
+  @Test(groups = "Functional")
+  public void testConstructor_alternativeTrees()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(10, 60));
+    ranges.add(new Range(20, 30));
+    ranges.add(new Range(40, 50));
+  
+    /*
+     * constructor with greedy traversal of sorted ranges to build nested
+     * containment lists results in 20-30 inside 10-60, 40-50 a sibling
+     */
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[10-60 [20-30], 40-50]");
+    assertTrue(ncl.isValid());
+
+    /*
+     * adding ranges one at a time results in 40-50 
+     * a sibling of 20-30 inside 10-60
+     */
+    ncl = new NCList<Range>(new Range(10, 60));
+    ncl.add(new Range(20, 30));
+    ncl.add(new Range(40, 50));
+    assertEquals(ncl.toString(), "[10-60 [20-30, 40-50]]");
+    assertTrue(ncl.isValid());
+  }
 }