JAL-2541 clean unit test of cut/undo (ungapped sequence)
[jalview.git] / test / jalview / commands / EditCommandTest.java
index 3223042..a415338 100644 (file)
@@ -22,17 +22,22 @@ package jalview.commands;
 
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertSame;
+import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.commands.EditCommand.Action;
 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.datamodel.features.SequenceFeatures;
 import jalview.gui.JvOptionPane;
 
+import java.util.List;
 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,271 @@ 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]; // abcdefghjk/1-10
+    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));
+
+    /*
+     * add some contact features
+     */
+    SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
+            6, 0f, null);
+    seq0.addSequenceFeature(internalContact); // should get deleted
+    SequenceFeature overlapLeftContact = new SequenceFeature(
+            "disulphide bond", "", 2, 6, 0f, null);
+    seq0.addSequenceFeature(overlapLeftContact); // should get deleted
+    SequenceFeature overlapRightContact = new SequenceFeature(
+            "disulphide bond", "", 5, 8, 0f, null);
+    seq0.addSequenceFeature(overlapRightContact); // should get deleted
+    SequenceFeature spanningContact = new SequenceFeature(
+            "disulphide bond", "", 2, 9, 0f, null);
+    seq0.addSequenceFeature(spanningContact); // should get shortened 3'
+
+    /*
+     * cut columns 3-6 (base 0), residues d-g 4-7
+     */
+    Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
+    EditCommand.cut(ec, new AlignmentI[] { al });
+
+    List<SequenceFeature> sfs = seq0.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(sfs, true);
+
+    assertEquals(5, sfs.size()); // features internal to cut were deleted
+    SequenceFeature sf = sfs.get(0);
+    assertEquals("before", sf.getType());
+    assertEquals(1, sf.getBegin());
+    assertEquals(3, sf.getEnd());
+    sf = sfs.get(1);
+    assertEquals("disulphide bond", sf.getType());
+    assertEquals(2, sf.getBegin());
+    assertEquals(5, sf.getEnd()); // truncated by cut
+    sf = sfs.get(2);
+    assertEquals("overlap left", sf.getType());
+    assertEquals(2, sf.getBegin());
+    assertEquals(3, sf.getEnd()); // truncated by cut
+    sf = sfs.get(3);
+    assertEquals("after", sf.getType());
+    assertEquals(4, sf.getBegin()); // shifted left by cut
+    assertEquals(6, sf.getEnd()); // shifted left by cut
+    sf = sfs.get(4);
+    assertEquals("overlap right", sf.getType());
+    assertEquals(4, sf.getBegin()); // shifted left by cut
+    assertEquals(4, sf.getEnd()); // truncated 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
+    List<SequenceFeature> sfs = seq0.getSequenceFeatures();
+    assertEquals(func(5), sfs.size());
+
+    /*
+     * 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 };
+
+    for (int from = 0; from < seq0.getLength(); from++)
+    {
+      for (int to = from; to < seq0.getLength(); 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 + 1, to + 1);
+        if (to - from == 4)
+        {
+          // all columns were cut
+          assertTrue(sfs.isEmpty());
+        }
+        else
+        {
+          assertEquals(msg + "wrong number of features left", func(5)
+                  - func(to - from + 1), sfs.size());
+        }
+
+        /*
+         * inspect individual features
+         */
+        for (SequenceFeature sf : sfs)
+        {
+          checkFeatureRelocation(sf, from + 1, to + 1, from > 0);
+        }
+
+        /*
+         * undo ready for next cut
+         */
+        testee.undoCommand(new AlignmentI[] { alignment });
+        sfs = seq0.getSequenceFeatures();
+        assertEquals("After undo of " + msg, func(5), sfs.size());
+        verifyUndo(from, to, sfs);
+      }
+    }
+  }
+
+  /**
+   * Check that after Undo, every feature has start/end that match its original
+   * "start" and "end" properties
+   * 
+   * @param from
+   * @param to
+   * @param sfs
+   */
+  protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
+  {
+    for (SequenceFeature sf : sfs)
+    {
+      final int oldFrom = ((Integer) sf.getValue("from")).intValue();
+      final int oldTo = ((Integer) sf.getValue("to")).intValue();
+      String msg = String.format(
+              "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
+              oldFrom, oldTo);
+      assertEquals(msg + "start", oldFrom, sf.getBegin());
+      assertEquals(msg + "end", oldTo, sf.getEnd());
+    }
+  }
+
+  /**
+   * 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)
+   * @param newDataset
+   */
+  private void checkFeatureRelocation(SequenceFeature sf, int from, int to,
+          boolean newDataset)
+  {
+    // TODO handle the gapped sequence case as well
+    int cutSize = to - from + 1;
+    final int oldFrom = ((Integer) sf.getValue("from")).intValue();
+    final 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, newDataset ? oldFrom - cutSize : oldFrom,
+              sf.getBegin());
+      assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
+              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, newDataset ? from : to + 1, sf.getBegin());
+      assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
+              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_withFeatures5prime()
+  {
+    SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
+    seq0.createDatasetSequence();
+    assertEquals(8, seq0.getStart());
+    seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
+            null));
+    SequenceI[] seqsArray = new SequenceI[] { seq0 };
+    AlignmentI alignment = new Alignment(seqsArray);
+
+    /*
+     * cut columns of A-B; same dataset sequence is retained, aligned sequence
+     * start becomes 10
+     */
+    Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
+    EditCommand.cut(ec, new AlignmentI[] { alignment });
+  
+    /*
+     * feature on CC(10-11) should still be on CC(10-11)
+     */
+    assertSame(seq0, alignment.getSequenceAt(0));
+    assertEquals(10, seq0.getStart());
+    List<SequenceFeature> sfs = seq0.getSequenceFeatures();
+    assertEquals(1, sfs.size());
+    SequenceFeature sf = sfs.get(0);
+    assertEquals(10, sf.getBegin());
+    assertEquals(11, sf.getEnd());
+
+    // TODO add further cases including Undo - see JAL-2541
+  }
 }