Merge branch 'features/JAL-2526sequenceCursor' into
[jalview.git] / test / jalview / datamodel / SequenceTest.java
index e2e14b4..97a75f9 100644 (file)
@@ -28,6 +28,8 @@ import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.commands.EditCommand;
+import jalview.commands.EditCommand.Action;
 import jalview.datamodel.PDBEntry.Type;
 import jalview.gui.JvOptionPane;
 import jalview.util.MapList;
@@ -35,6 +37,7 @@ import jalview.util.MapList;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.BitSet;
 import java.util.List;
 import java.util.Vector;
 
@@ -76,6 +79,18 @@ public class SequenceTest
     assertEquals("Gap interval 1 end wrong", 4, gapInt.get(0)[1]);
     assertEquals("Gap interval 2 start wrong", 6, gapInt.get(1)[0]);
     assertEquals("Gap interval 2 end wrong", 8, gapInt.get(1)[1]);
+
+    BitSet gapfield = aseq.getInsertionsAsBits();
+    BitSet expectedgaps = new BitSet();
+    expectedgaps.set(2, 5);
+    expectedgaps.set(6, 9);
+
+    assertEquals(6, expectedgaps.cardinality());
+
+    assertEquals("getInsertionsAsBits didn't mark expected number of gaps",
+            6, gapfield.cardinality());
+
+    assertEquals("getInsertionsAsBits not correct.", expectedgaps, gapfield);
   }
 
   @Test(groups = ("Functional"))
@@ -224,72 +239,152 @@ public class SequenceTest
   @Test(groups = { "Functional" })
   public void testFindIndex()
   {
+    /* 
+     * call sequenceChanged() after each test to invalidate any cursor,
+     * forcing the 1-arg findIndex to be executed
+     */
     SequenceI sq = new Sequence("test", "ABCDEF");
     assertEquals(0, sq.findIndex(0));
-    PA.setValue(sq, "cursor", null);
+    sq.sequenceChanged();
     assertEquals(1, sq.findIndex(1));
-    PA.setValue(sq, "cursor", null);
+    sq.sequenceChanged();
     assertEquals(5, sq.findIndex(5));
-    PA.setValue(sq, "cursor", null);
+    sq.sequenceChanged();
     assertEquals(6, sq.findIndex(6));
-    PA.setValue(sq, "cursor", null);
+    sq.sequenceChanged();
     assertEquals(6, sq.findIndex(9));
 
     sq = new Sequence("test/8-13", "-A--B-C-D-E-F--");
     assertEquals(2, sq.findIndex(8));
-    PA.setValue(sq, "cursor", null);
+    sq.sequenceChanged();
     assertEquals(5, sq.findIndex(9));
-    PA.setValue(sq, "cursor", null);
+    sq.sequenceChanged();
     assertEquals(7, sq.findIndex(10));
 
     // before start returns 0
-    PA.setValue(sq, "cursor", null);
+    sq.sequenceChanged();
     assertEquals(0, sq.findIndex(0));
-    PA.setValue(sq, "cursor", null);
+    sq.sequenceChanged();
     assertEquals(0, sq.findIndex(-1));
 
     // beyond end returns last residue column
-    PA.setValue(sq, "cursor", null);
+    sq.sequenceChanged();
     assertEquals(13, sq.findIndex(99));
   }
 
   /**
-   * Tests for the method that returns a dataset sequence position (base 1) for
+   * Tests for the method that returns a dataset sequence position (start..) for
    * an aligned column position (base 0).
    */
   @Test(groups = { "Functional" })
   public void testFindPosition()
   {
-    SequenceI sq = new Sequence("test", "ABCDEF");
-    assertEquals(1, sq.findPosition(0));
-    assertEquals(6, sq.findPosition(5));
+    /* 
+     * call sequenceChanged() after each test to invalidate any cursor,
+     * forcing the 1-arg findPosition to be executed
+     */
+    SequenceI sq = new Sequence("test/8-13", "ABCDEF");
+    assertEquals(8, sq.findPosition(0));
+    // Sequence should now hold a cursor at [8, 0]
+    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    int token = (int) PA.getValue(sq, "changeCount");
+    assertEquals(new SequenceCursor(sq, 8, 1, token), cursor);
+
+    sq.sequenceChanged();
+
+    /*
+     * find F13 at column offset 5, cursor should update to [13, 6]
+     */
+    assertEquals(13, sq.findPosition(5));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(++token, (int) PA.getValue(sq, "changeCount"));
+    assertEquals(new SequenceCursor(sq, 13, 6, token), cursor);
+
     // assertEquals(-1, seq.findPosition(6)); // fails
 
-    sq = new Sequence("test", "AB-C-D--");
-    assertEquals(1, sq.findPosition(0));
-    assertEquals(2, sq.findPosition(1));
+    sq = new Sequence("test/8-11", "AB-C-D--");
+    token = (int) PA.getValue(sq, "changeCount"); // 0
+    assertEquals(8, sq.findPosition(0));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 8, 1, token), cursor);
+
+    sq.sequenceChanged();
+    assertEquals(9, sq.findPosition(1));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor);
+
+    sq.sequenceChanged();
     // gap position 'finds' residue to the right (not the left as per javadoc)
-    assertEquals(3, sq.findPosition(2));
-    assertEquals(3, sq.findPosition(3));
-    assertEquals(4, sq.findPosition(4));
-    assertEquals(4, sq.findPosition(5));
+    // cursor is set to the last residue position found [B 2]
+    assertEquals(10, sq.findPosition(2));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor);
+
+    sq.sequenceChanged();
+    assertEquals(10, sq.findPosition(3));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor);
+
+    sq.sequenceChanged();
+    // column[4] is the gap after C - returns D11
+    // cursor is set to [C 4]
+    assertEquals(11, sq.findPosition(4));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor);
+
+    sq.sequenceChanged();
+    assertEquals(11, sq.findPosition(5)); // D
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor);
+
+    sq.sequenceChanged();
     // returns 1 more than sequence length if off the end ?!?
-    assertEquals(5, sq.findPosition(6));
-    assertEquals(5, sq.findPosition(7));
+    assertEquals(12, sq.findPosition(6));
 
-    sq = new Sequence("test", "--AB-C-DEF--");
-    assertEquals(1, sq.findPosition(0));
-    assertEquals(1, sq.findPosition(1));
-    assertEquals(1, sq.findPosition(2));
-    assertEquals(2, sq.findPosition(3));
-    assertEquals(3, sq.findPosition(4));
-    assertEquals(3, sq.findPosition(5));
-    assertEquals(4, sq.findPosition(6));
-    assertEquals(4, sq.findPosition(7));
-    assertEquals(5, sq.findPosition(8));
-    assertEquals(6, sq.findPosition(9));
-    assertEquals(7, sq.findPosition(10));
-    assertEquals(7, sq.findPosition(11));
+    sq.sequenceChanged();
+    assertEquals(12, sq.findPosition(7));
+
+    sq = new Sequence("test/8-13", "--AB-C-DEF--");
+    assertEquals(8, sq.findPosition(0));
+
+    sq.sequenceChanged();
+    assertEquals(8, sq.findPosition(1));
+
+    sq.sequenceChanged();
+    assertEquals(8, sq.findPosition(2));
+
+    sq.sequenceChanged();
+    assertEquals(9, sq.findPosition(3));
+
+    sq.sequenceChanged();
+    assertEquals(10, sq.findPosition(4));
+
+    sq.sequenceChanged();
+    assertEquals(10, sq.findPosition(5));
+
+    sq.sequenceChanged();
+    assertEquals(11, sq.findPosition(6));
+
+    sq.sequenceChanged();
+    assertEquals(11, sq.findPosition(7));
+
+    sq.sequenceChanged();
+    assertEquals(12, sq.findPosition(8));
+
+    sq.sequenceChanged();
+    assertEquals(13, sq.findPosition(9));
+
+    sq.sequenceChanged();
+    assertEquals(14, sq.findPosition(10));
+
+    /*
+     * findPosition for column beyond sequence length
+     * returns 1 more than last residue position
+     */
+    sq.sequenceChanged();
+    assertEquals(14, sq.findPosition(11));
+    sq.sequenceChanged();
+    assertEquals(14, sq.findPosition(99));
   }
 
   @Test(groups = { "Functional" })
@@ -1170,24 +1265,58 @@ public class SequenceTest
     seq.setDatasetSequence(seq2);
   }
 
-  @Test
-  public void testFindPositions()
+  @Test(groups = { "Functional" })
+  public void testFindFeatures()
   {
-    SequenceI sq = new Sequence("Seq", "ABC--DE-F", 8, 13);
-
-    Range range = sq.findPositions(1, 4); // BC
-    assertEquals(new Range(9, 10), range);
-
-    range = sq.findPositions(2, 4); // C
-    assertEquals(new Range(10, 10), range);
-
-    assertNull(sq.findPositions(3, 4)); // all gaps
+    SequenceI sq = new Sequence("test/8-16", "-ABC--DEF--GHI--");
+    sq.createDatasetSequence();
 
-    range = sq.findPositions(2, 6); // CDE
-    assertEquals(new Range(10, 12), range);
+    assertTrue(sq.findFeatures(1, 99).isEmpty());
 
-    range = sq.findPositions(3, 7); // DE
-    assertEquals(new Range(11, 12), range);
+    // add non-positional feature
+    SequenceFeature sf0 = new SequenceFeature("Cath", "desc", 0, 0, 2f,
+            null);
+    sq.addSequenceFeature(sf0);
+    // add feature on BCD
+    SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 9, 11, 2f,
+            null);
+    sq.addSequenceFeature(sf1);
+    // add feature on DE
+    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 11, 12, 2f,
+            null);
+    sq.addSequenceFeature(sf2);
+    // add contact feature at [B, H]
+    SequenceFeature sf3 = new SequenceFeature("Disulphide bond", "desc", 9,
+            15, 2f,
+            null);
+    sq.addSequenceFeature(sf3);
+    // add contact feature at [F, G]
+    SequenceFeature sf4 = new SequenceFeature("Disulfide Bond", "desc", 13,
+            14, 2f,
+            null);
+    sq.addSequenceFeature(sf4);
+
+    // no features in columns 1-2 (-A)
+    List<SequenceFeature> found = sq.findFeatures(1, 2);
+    assertTrue(found.isEmpty());
+
+    // columns 1-6 (-ABC--) includes BCD and B/H feature but not DE
+    found = sq.findFeatures(1, 6);
+    assertEquals(2, found.size());
+    assertTrue(found.contains(sf1));
+    assertTrue(found.contains(sf3));
+
+    // columns 5-6 (--) includes (enclosing) BCD but not (contact) B/H feature
+    found = sq.findFeatures(5, 6);
+    assertEquals(1, found.size());
+    assertTrue(found.contains(sf1));
+
+    // columns 7-10 (DEF-) includes BCD, DE, F/G but not B/H feature
+    found = sq.findFeatures(7, 10);
+    assertEquals(3, found.size());
+    assertTrue(found.contains(sf1));
+    assertTrue(found.contains(sf2));
+    assertTrue(found.contains(sf4));
   }
 
   @Test(groups = { "Functional" })
@@ -1212,64 +1341,162 @@ public class SequenceTest
   
     // find F pos given A
     assertEquals(13, sq.findPosition(10, new SequenceCursor(sq, 8, 2, 0)));
-  
+    int token = (int) PA.getValue(sq, "changeCount"); // 0
+    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 13, 10, token), cursor);
+
     // find A pos given F
     assertEquals(8, sq.findPosition(2, new SequenceCursor(sq, 13, 10, 0)));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 8, 2, token), cursor);
   
     // find C pos given C
     assertEquals(10, sq.findPosition(6, new SequenceCursor(sq, 10, 6, 0)));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 10, 6, token), cursor);
 
     // now the grey area - what residue position for a gapped column? JAL-2562
 
     // find 'residue' for column 3 given cursor for D (so working left)
+    // returns B9; cursor is updated to [B 5]
     assertEquals(9, sq.findPosition(3, new SequenceCursor(sq, 11, 7, 0)));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 9, 5, token), cursor);
 
     // find 'residue' for column 8 given cursor for D (so working right)
+    // returns E12; cursor is updated to [D 7]
     assertEquals(12, sq.findPosition(8, new SequenceCursor(sq, 11, 7, 0)));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 11, 7, token), cursor);
 
     // find 'residue' for column 12 given cursor for B
+    // returns 1 more than last residue position; cursor is updated to [F 10]
     assertEquals(14, sq.findPosition(12, new SequenceCursor(sq, 9, 5, 0)));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 13, 10, token), cursor);
+
+    /*
+     * findPosition for column beyond length of sequence
+     * returns 1 more than the last residue position
+     * cursor is set to last real residue position [F 10]
+     */
+    assertEquals(14, sq.findPosition(99, new SequenceCursor(sq, 8, 2, 0)));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 13, 10, token), cursor);
+
+    /*
+     * and the case without a trailing gap
+     */
+    sq = new Sequence("test/8-13", "-A--BCD-EF");
+    // first find C from A
+    assertEquals(10, sq.findPosition(6, new SequenceCursor(sq, 8, 2, 0)));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 10, 6, token), cursor);
+    // now 'find' 99 from C
+    // cursor is set to [F 10]
+    assertEquals(14, sq.findPosition(99, cursor));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 13, 10, token), cursor);
   }
 
   @Test
-  public void testFindPositions_withCursor()
+  public void testIsValidCursor()
   {
     Sequence sq = new Sequence("Seq", "ABC--DE-F", 8, 13);
-  
-    // find positions for columns 1-4 (BC--) given E cursor
-    Range range = sq.findPositions(1, 4, new SequenceCursor(sq, 12, 7, 0)); // BC
-    assertEquals(new Range(9, 10), range);
+    assertFalse(sq.isValidCursor(null));
 
-    // repeat using B cursor
-    range = sq.findPositions(1, 4, new SequenceCursor(sq, 9, 2, 0)); // BC
-    assertEquals(new Range(9, 10), range);
-  
-    // find positions for columns 2-4 (C--) given A cursor
-    range = sq.findPositions(2, 4, new SequenceCursor(sq, 8, 1, 0)); // C
-    assertEquals(new Range(10, 10), range);
-  
-    // gapped region
-    assertNull(sq.findPositions(3, 4, new SequenceCursor(sq, 10, 3, 0)));
-    assertNull(sq.findPositions(3, 4, new SequenceCursor(sq, 12, 7, 0)));
+    /*
+     * cursor is valid if it has valid sequence ref and changeCount token
+     * and positions within the range of the sequence
+     */
+    int changeCount = (int) PA.getValue(sq, "changeCount");
+    SequenceCursor cursor = new SequenceCursor(sq, 13, 1, changeCount);
+    assertTrue(sq.isValidCursor(cursor));
+
+    /*
+     * column position outside [0 - length-1] is rejected
+     */
+    cursor = new SequenceCursor(sq, 13, -1, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(sq, 13, 9, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(sq, 7, 8, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(sq, 14, 2, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+
+    /*
+     * wrong sequence is rejected
+     */
+    cursor = new SequenceCursor(null, 13, 1, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(new Sequence("Seq", "abc"), 13, 1,
+            changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+
+    /*
+     * wrong token value is rejected
+     */
+    cursor = new SequenceCursor(sq, 13, 1, changeCount + 1);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(sq, 13, 1, changeCount - 1);
+    assertFalse(sq.isValidCursor(cursor));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFindPosition_withCursorAndEdits()
+  {
+    Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
   
-    // find positions for columns 2-6 (C--DE) given B cursor
-    range = sq.findPositions(2, 6, new SequenceCursor(sq, 9, 2, 0)); // CDE
-    assertEquals(new Range(10, 12), range);
+    // find F pos given A
+    assertEquals(13, sq.findPosition(10, new SequenceCursor(sq, 8, 2, 0)));
+    int token = (int) PA.getValue(sq, "changeCount"); // 0
+    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 13, 10, token), cursor);
 
-    // repeat using C as cursor
-    range = sq.findPositions(2, 6, new SequenceCursor(sq, 10, 3, 0));
-    assertEquals(new Range(10, 12), range);
+    /*
+     * setSequence should invalidate the cursor cached by the sequence
+     */
+    sq.setSequence("-A-BCD-EF---"); // one gap removed
+    assertEquals(8, sq.getStart()); // sanity check
+    assertEquals(11, sq.findPosition(5)); // D11
+    // cursor should now be at [D 6]
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor);
 
-    // repeat using D as cursor
-    range = sq.findPositions(2, 6, new SequenceCursor(sq, 11, 6, 0));
-    assertEquals(new Range(10, 12), range);
+    /*
+     * deleteChars should invalidate the cached cursor
+     */
+    sq.deleteChars(2, 5); // delete -BC
+    assertEquals("-AD-EF---", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart()); // sanity check
+    assertEquals(10, sq.findPosition(4)); // E10
+    // cursor should now be at [E 5]
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 10, 5, ++token), cursor);
 
-    // repeat using E as cursor
-    range = sq.findPositions(2, 6, new SequenceCursor(sq, 12, 7, 0));
-    assertEquals(new Range(10, 12), range);
+    /*
+     * Edit to insert gaps should invalidate the cached cursor
+     * insert 2 gaps at column[3] to make -AD---EF---
+     */
+    SequenceI[] seqs = new SequenceI[] { sq };
+    AlignmentI al = new Alignment(seqs);
+    new EditCommand().appendEdit(Action.INSERT_GAP, seqs, 3, 2, al, true);
+    assertEquals("-AD---EF---", sq.getSequenceAsString());
+    assertEquals(10, sq.findPosition(4)); // E10
+    // cursor should now be at [D 3]
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 9, 3, ++token), cursor);
 
-    // repeat using F as cursor
-    range = sq.findPositions(2, 6, new SequenceCursor(sq, 13, 9, 0));
-    assertEquals(new Range(10, 12), range);
+    /*
+     * insertCharAt should invalidate the cached cursor
+     * insert CC at column[4] to make -AD-CC--EF---
+     */
+    sq.insertCharAt(4, 2, 'C');
+    assertEquals("-AD-CC--EF---", sq.getSequenceAsString());
+    assertEquals(13, sq.findPosition(9)); // F13
+    // cursor should now be at [F 10]
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 13, 10, ++token), cursor);
   }
 }