JAL-2541 initial unit tests for edit cut relocate features
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 18 May 2017 13:15:32 +0000 (14:15 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Thu, 18 May 2017 13:15:32 +0000 (14:15 +0100)
test/jalview/commands/EditCommandTest.java

index 3223042..8781a93 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.commands;
 
 import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertSame;
 
 import jalview.commands.EditCommand.Action;
@@ -28,11 +29,15 @@ import jalview.commands.EditCommand.Edit;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
 
+import java.util.Arrays;
+import java.util.Comparator;
 import java.util.Map;
 
+import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -45,6 +50,14 @@ import org.testng.annotations.Test;
  */
 public class EditCommandTest
 {
+  /*
+   * compute n(n+1)/2 e.g. 
+   * func(5) = 5 + 4 + 3 + 2 + 1 = 15
+   */
+  private static int func(int i)
+  {
+    return i * (i + 1) / 2;
+  }
 
   @BeforeClass(alwaysRun = true)
   public void setUpJvOptionPane()
@@ -639,4 +652,228 @@ public class EditCommandTest
     assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
     assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
   }
+
+  /**
+   * Test a cut action's relocation of sequence features
+   */
+  @Test(groups = { "Functional" })
+  public void testCut_withFeatures()
+  {
+    /*
+     * create sequence features before, after and overlapping
+     * a cut of columns/residues 4-7
+     */
+    SequenceI seq0 = seqs[0];
+    seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
+            null));
+    seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
+            0f, null));
+    seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
+            null));
+    seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
+            0f, null));
+    seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
+            null));
+
+    Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
+    EditCommand.cut(ec, new AlignmentI[] { al });
+
+    SequenceFeature[] sfs = seq0.getSequenceFeatures();
+    Arrays.sort(sfs, new Comparator<SequenceFeature>()
+    {
+      @Override
+      public int compare(SequenceFeature o1, SequenceFeature o2)
+      {
+        return Integer.compare(o1.getBegin(), o2.getBegin());
+      }
+    });
+    assertEquals(4, sfs.length); // feature internal to cut has been deleted
+    SequenceFeature sf = sfs[0];
+    assertEquals("before", sf.getType());
+    assertEquals(1, sf.getBegin());
+    assertEquals(3, sf.getEnd());
+    sf = sfs[1];
+    assertEquals("overlap left", sf.getType());
+    assertEquals(2, sf.getBegin());
+    assertEquals(3, sf.getEnd()); // truncated by cut
+    sf = sfs[2];
+    assertEquals("overlap right", sf.getType());
+    assertEquals(4, sf.getBegin()); // shifted left by cut
+    assertEquals(5, sf.getEnd()); // truncated by cut
+    sf = sfs[3];
+    assertEquals("after", sf.getType());
+    assertEquals(4, sf.getBegin()); // shifted left by cut
+    assertEquals(6, sf.getEnd()); // shifted left by cut
+  }
+
+  /**
+   * Test a cut action's relocation of sequence features, with full coverage of
+   * all possible feature and cut locations for a 5-position ungapped sequence
+   */
+  @Test(groups = { "Functional" })
+  public void testCut_withFeatures_exhaustive()
+  {
+    /*
+     * create a sequence features on each subrange of 1-5
+     */
+    SequenceI seq0 = new Sequence("seq", "ABCDE");
+    AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
+    alignment.setDataset(null);
+    for (int from = 1; from <= seq0.getLength(); from++)
+    {
+      for (int to = from; to <= seq0.getLength(); to++)
+      {
+        String desc = String.format("%d-%d", from, to);
+        SequenceFeature sf = new SequenceFeature("test", desc, from, to,
+                0f,
+                null);
+        sf.setValue("from", Integer.valueOf(from));
+        sf.setValue("to", Integer.valueOf(to));
+        seq0.addSequenceFeature(sf);
+      }
+    }
+    // sanity check
+    SequenceFeature[] sfs = seq0.getSequenceFeatures();
+    assertEquals(func(5), sfs.length);
+
+    /*
+     * now perform all possible cuts of subranges of 1-5 (followed by Undo)
+     * and validate the resulting remaining sequence features!
+     */
+    SequenceI[] sqs = new SequenceI[] { seq0 };
+
+    // goal is to have this passing for all from/to values!!
+    // for (int from = 0; from < seq0.getLength(); from++)
+    // {
+    // for (int to = from; to < seq0.getLength(); to++)
+    for (int from = 1; from < 3; from++)
+    {
+      for (int to = 2; to < 3; to++)
+      {
+        testee.appendEdit(Action.CUT, sqs, from, (to - from + 1),
+                alignment, true);
+
+        sfs = seq0.getSequenceFeatures();
+
+        /*
+         * confirm the number of features has reduced by the
+         * number of features within the cut region i.e. by
+         * func(length of cut)
+         */
+        String msg = String.format("Cut %d-%d ", from, to);
+        if (to - from == 4)
+        {
+          // all columns cut
+          assertNull(sfs);
+        }
+        else
+        {
+          assertEquals(msg + "wrong number of features left", func(5)
+                - func(to - from + 1), sfs.length);
+        }
+
+        /*
+         * inspect individual features
+         */
+        if (sfs != null)
+        {
+          for (SequenceFeature sf : sfs)
+          {
+            checkFeatureRelocation(sf, from + 1, to + 1);
+          }
+        }
+        /*
+         * undo ready for next cut
+         */
+        testee.undoCommand(new AlignmentI[] { alignment });
+        assertEquals(func(5), seq0.getSequenceFeatures().length);
+      }
+    }
+  }
+
+  /**
+   * Helper method to check a feature has been correctly relocated after a cut
+   * 
+   * @param sf
+   * @param from
+   *          start of cut (first residue cut)
+   * @param to
+   *          end of cut (last residue cut)
+   */
+  private void checkFeatureRelocation(SequenceFeature sf, int from, int to)
+  {
+    // TODO handle the gapped sequence case as well
+    int cutSize = to - from + 1;
+    int oldFrom = ((Integer) sf.getValue("from")).intValue();
+    int oldTo = ((Integer) sf.getValue("to")).intValue();
+
+    String msg = String.format(
+            "Feature %s relocated to %d-%d after cut of %d-%d",
+            sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
+    if (oldTo < from)
+    {
+      // before cut region so unchanged
+      assertEquals("1: " + msg, oldFrom, sf.getBegin());
+      assertEquals("2: " + msg, oldTo, sf.getEnd());
+    }
+    else if (oldFrom > to)
+    {
+      // follows cut region - shift by size of cut
+      assertEquals("3: " + msg, oldFrom - cutSize, sf.getBegin());
+      assertEquals("4: " + msg, oldTo - cutSize, sf.getEnd());
+    }
+    else if (oldFrom < from && oldTo > to)
+    {
+      // feature encloses cut region - shrink it right
+      assertEquals("5: " + msg, oldFrom, sf.getBegin());
+      assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
+    }
+    else if (oldFrom < from)
+    {
+      // feature overlaps left side of cut region - truncated right
+      assertEquals("7: " + msg, from - 1, sf.getEnd());
+    }
+    else if (oldTo > to)
+    {
+      // feature overlaps right side of cut region - truncated left
+      assertEquals("8: " + msg, from, sf.getBegin());
+      assertEquals("9: " + msg, from + oldTo - to - 1, sf.getEnd());
+    }
+    else
+    {
+      // feature internal to cut - should have been deleted!
+      Assert.fail(msg + " - should have been deleted");
+    }
+  }
+
+  /**
+   * Test a cut action's relocation of sequence features
+   */
+  @Test(groups = { "Functional" })
+  public void testCut_gappedWithFeatures()
+  {
+    /*
+     * create sequence features before, after and overlapping
+     * a cut of columns/residues 4-7
+     */
+    SequenceI seq0 = new Sequence("seq", "A-BCC");
+    seq0.addSequenceFeature(new SequenceFeature("", "", 3, 4, 0f,
+            null));
+    AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
+    // cut columns of A-B
+    Edit ec = testee.new Edit(Action.CUT, seqs, 0, 3, alignment); // cols 0-3
+                                                                  // base 0
+    EditCommand.cut(ec, new AlignmentI[] { alignment });
+  
+    /*
+     * feature on CC(3-4) should now be on CC(1-2)
+     */
+    SequenceFeature[] sfs = seq0.getSequenceFeatures();
+    assertEquals(1, sfs.length);
+    SequenceFeature sf = sfs[0];
+    assertEquals(1, sf.getBegin());
+    assertEquals(2, sf.getEnd());
+
+    // TODO add further cases including Undo - see JAL-2541
+  }
 }