JAL-2772 check array bounds for sequence access
[jalview.git] / test / jalview / datamodel / SequenceTest.java
index 97a75f9..23e8cf7 100644 (file)
@@ -106,15 +106,6 @@ public class SequenceTest
     // change sequence, should trigger an update of cached result
     sq.setSequence("ASDFASDFADSF");
     assertTrue(sq.isProtein());
-    /*
-     * in situ change of sequence doesn't change hashcode :-O
-     * (sequence should not expose internal implementation)
-     */
-    for (int i = 0; i < sq.getSequence().length; i++)
-    {
-      sq.getSequence()[i] = "acgtu".charAt(i % 5);
-    }
-    assertTrue(sq.isProtein()); // but it isn't
   }
 
   @Test(groups = { "Functional" })
@@ -286,6 +277,8 @@ public class SequenceTest
     SequenceI sq = new Sequence("test/8-13", "ABCDEF");
     assertEquals(8, sq.findPosition(0));
     // Sequence should now hold a cursor at [8, 0]
+    assertEquals("test:Pos8:Col1:startCol1:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
     SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     int token = (int) PA.getValue(sq, "changeCount");
     assertEquals(new SequenceCursor(sq, 8, 1, token), cursor);
@@ -294,11 +287,14 @@ public class SequenceTest
 
     /*
      * find F13 at column offset 5, cursor should update to [13, 6]
+     * endColumn is found and saved in cursor
      */
     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("test:Pos13:Col6:startCol1:endCol6:tok1",
+            PA.getValue(sq, "cursor").toString());
 
     // assertEquals(-1, seq.findPosition(6)); // fails
 
@@ -307,11 +303,15 @@ public class SequenceTest
     assertEquals(8, sq.findPosition(0));
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 8, 1, token), cursor);
+    assertEquals("test:Pos8:Col1:startCol1:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(9, sq.findPosition(1));
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor);
+    assertEquals("test:Pos9:Col2:startCol1:endCol0:tok1",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     // gap position 'finds' residue to the right (not the left as per javadoc)
@@ -319,11 +319,15 @@ public class SequenceTest
     assertEquals(10, sq.findPosition(2));
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor);
+    assertEquals("test:Pos9:Col2:startCol1:endCol0:tok2",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(10, sq.findPosition(3));
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor);
+    assertEquals("test:Pos10:Col4:startCol1:endCol0:tok3",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     // column[4] is the gap after C - returns D11
@@ -331,11 +335,16 @@ public class SequenceTest
     assertEquals(11, sq.findPosition(4));
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor);
+    assertEquals("test:Pos10:Col4:startCol1:endCol0:tok4",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(11, sq.findPosition(5)); // D
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor);
+    // lastCol has been found and saved in the cursor
+    assertEquals("test:Pos11:Col6:startCol1:endCol6:tok5",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     // returns 1 more than sequence length if off the end ?!?
@@ -344,38 +353,68 @@ public class SequenceTest
     sq.sequenceChanged();
     assertEquals(12, sq.findPosition(7));
 
+    /*
+     * first findPosition should also set firstResCol in cursor
+     */
     sq = new Sequence("test/8-13", "--AB-C-DEF--");
     assertEquals(8, sq.findPosition(0));
+    assertNull(PA.getValue(sq, "cursor"));
 
     sq.sequenceChanged();
     assertEquals(8, sq.findPosition(1));
+    assertNull(PA.getValue(sq, "cursor"));
 
     sq.sequenceChanged();
     assertEquals(8, sq.findPosition(2));
+    assertEquals("test:Pos8:Col3:startCol3:endCol0:tok2",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(9, sq.findPosition(3));
+    assertEquals("test:Pos9:Col4:startCol3:endCol0:tok3",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
+    // column[4] is a gap, returns next residue pos (C10)
+    // cursor is set to last residue found [B]
     assertEquals(10, sq.findPosition(4));
+    assertEquals("test:Pos9:Col4:startCol3:endCol0:tok4",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(10, sq.findPosition(5));
+    assertEquals("test:Pos10:Col6:startCol3:endCol0:tok5",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
+    // column[6] is a gap, returns next residue pos (D11)
+    // cursor is set to last residue found [C]
     assertEquals(11, sq.findPosition(6));
+    assertEquals("test:Pos10:Col6:startCol3:endCol0:tok6",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(11, sq.findPosition(7));
+    assertEquals("test:Pos11:Col8:startCol3:endCol0:tok7",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(12, sq.findPosition(8));
+    assertEquals("test:Pos12:Col9:startCol3:endCol0:tok8",
+            PA.getValue(sq, "cursor").toString());
 
+    /*
+     * when the last residue column is found, it is set in the cursor
+     */
     sq.sequenceChanged();
     assertEquals(13, sq.findPosition(9));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok9",
+            PA.getValue(sq, "cursor").toString());
 
     sq.sequenceChanged();
     assertEquals(14, sq.findPosition(10));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok10",
+            PA.getValue(sq, "cursor").toString());
 
     /*
      * findPosition for column beyond sequence length
@@ -383,8 +422,34 @@ public class SequenceTest
      */
     sq.sequenceChanged();
     assertEquals(14, sq.findPosition(11));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok11",
+            PA.getValue(sq, "cursor").toString());
+
     sq.sequenceChanged();
     assertEquals(14, sq.findPosition(99));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok12",
+            PA.getValue(sq, "cursor").toString());
+
+    /*
+     * gapped sequence ending in non-gap
+     */
+    sq = new Sequence("test/8-13", "--AB-C-DEF");
+    assertEquals(13, sq.findPosition(9));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
+    sq.sequenceChanged();
+    assertEquals(12, sq.findPosition(8));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    // sequenceChanged() invalidates cursor.lastResidueColumn
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals("test:Pos12:Col9:startCol3:endCol0:tok1",
+            cursor.toString());
+    // findPosition with cursor accepts base 1 column values
+    assertEquals(13, ((Sequence) sq).findPosition(10, cursor));
+    assertEquals(13, sq.findPosition(9)); // F13
+    // lastResidueColumn has now been found and saved in cursor
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok1",
+            PA.getValue(sq, "cursor").toString());
   }
 
   @Test(groups = { "Functional" })
@@ -1278,23 +1343,25 @@ public class SequenceTest
             null);
     sq.addSequenceFeature(sf0);
     // add feature on BCD
-    SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 9, 11, 2f,
+    SequenceFeature sfBCD = new SequenceFeature("Cath", "desc", 9, 11, 2f,
             null);
-    sq.addSequenceFeature(sf1);
+    sq.addSequenceFeature(sfBCD);
     // add feature on DE
-    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 11, 12, 2f,
+    SequenceFeature sfDE = new SequenceFeature("Cath", "desc", 11, 12, 2f,
             null);
-    sq.addSequenceFeature(sf2);
+    sq.addSequenceFeature(sfDE);
     // add contact feature at [B, H]
-    SequenceFeature sf3 = new SequenceFeature("Disulphide bond", "desc", 9,
-            15, 2f,
-            null);
-    sq.addSequenceFeature(sf3);
+    SequenceFeature sfContactBH = new SequenceFeature("Disulphide bond",
+            "desc", 9, 15, 2f, null);
+    sq.addSequenceFeature(sfContactBH);
     // add contact feature at [F, G]
-    SequenceFeature sf4 = new SequenceFeature("Disulfide Bond", "desc", 13,
-            14, 2f,
-            null);
-    sq.addSequenceFeature(sf4);
+    SequenceFeature sfContactFG = new SequenceFeature("Disulfide Bond",
+            "desc", 13, 14, 2f, null);
+    sq.addSequenceFeature(sfContactFG);
+    // add single position feature at [I]
+    SequenceFeature sfI = new SequenceFeature("Disulfide Bond",
+            "desc", 16, 16, null);
+    sq.addSequenceFeature(sfI);
 
     // no features in columns 1-2 (-A)
     List<SequenceFeature> found = sq.findFeatures(1, 2);
@@ -1303,20 +1370,29 @@ public class SequenceTest
     // 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));
+    assertTrue(found.contains(sfBCD));
+    assertTrue(found.contains(sfContactBH));
 
     // 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));
+    assertTrue(found.contains(sfBCD));
 
     // 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));
+    assertTrue(found.contains(sfBCD));
+    assertTrue(found.contains(sfDE));
+    assertTrue(found.contains(sfContactFG));
+
+    // columns 10-11 (--) should find nothing
+    found = sq.findFeatures(10, 11);
+    assertEquals(0, found.size());
+
+    // columns 14-14 (I) should find variant feature
+    found = sq.findFeatures(14, 14);
+    assertEquals(1, found.size());
+    assertTrue(found.contains(sfI));
   }
 
   @Test(groups = { "Functional" })
@@ -1339,41 +1415,41 @@ public class SequenceTest
   {
     Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
   
-    // find F pos given A
+    // find F pos given A - lastCol gets set in cursor
     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);
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
 
-    // find A pos given F
+    // find A pos given F - first residue column is saved in cursor
     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);
+    assertEquals("test:Pos8:Col2:startCol2:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
   
-    // find C pos given C
+    // find C pos given C (neither startCol nor endCol is set)
     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);
+    assertEquals("test:Pos10:Col6:startCol0:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
 
     // 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);
+    assertEquals("test:Pos9:Col5:startCol0:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
 
     // 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);
+    assertEquals("test:Pos11:Col7:startCol0:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
 
     // find 'residue' for column 12 given cursor for B
     // returns 1 more than last residue position; cursor is updated to [F 10]
+    // lastCol position is saved in cursor
     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);
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
 
     /*
      * findPosition for column beyond length of sequence
@@ -1381,8 +1457,8 @@ public class SequenceTest
      * 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);
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
 
     /*
      * and the case without a trailing gap
@@ -1390,13 +1466,14 @@ public class SequenceTest
     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);
+    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals("test:Pos10:Col6:startCol0:endCol0:tok0",
+            cursor.toString());
     // now 'find' 99 from C
-    // cursor is set to [F 10]
+    // cursor is set to [F 10] and saved lastCol
     assertEquals(14, sq.findPosition(99, cursor));
-    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
-    assertEquals(new SequenceCursor(sq, 13, 10, token), cursor);
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
   }
 
   @Test
@@ -1414,11 +1491,11 @@ public class SequenceTest
     assertTrue(sq.isValidCursor(cursor));
 
     /*
-     * column position outside [0 - length-1] is rejected
+     * column position outside [0 - length] is rejected
      */
     cursor = new SequenceCursor(sq, 13, -1, changeCount);
     assertFalse(sq.isValidCursor(cursor));
-    cursor = new SequenceCursor(sq, 13, 9, changeCount);
+    cursor = new SequenceCursor(sq, 13, 10, changeCount);
     assertFalse(sq.isValidCursor(cursor));
     cursor = new SequenceCursor(sq, 7, 8, changeCount);
     assertFalse(sq.isValidCursor(cursor));
@@ -1499,4 +1576,132 @@ public class SequenceTest
     cursor = (SequenceCursor) PA.getValue(sq, "cursor");
     assertEquals(new SequenceCursor(sq, 13, 10, ++token), cursor);
   }
+
+  @Test(groups = { "Functional" })
+  public void testGetSequence()
+  {
+    String seqstring = "-A--BCD-EF--";
+    Sequence sq = new Sequence("test/8-13", seqstring);
+    sq.createDatasetSequence();
+    assertTrue(Arrays.equals(sq.getSequence(), seqstring.toCharArray()));
+    assertTrue(Arrays.equals(sq.getDatasetSequence().getSequence(),
+            "ABCDEF".toCharArray()));
+
+    // verify a copy of the sequence array is returned
+    char[] theSeq = (char[]) PA.getValue(sq, "sequence");
+    assertNotSame(theSeq, sq.getSequence());
+    theSeq = (char[]) PA.getValue(sq.getDatasetSequence(), "sequence");
+    assertNotSame(theSeq, sq.getDatasetSequence().getSequence());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testReplace()
+  {
+    String seqstring = "-A--BCD-EF--";
+    SequenceI sq = new Sequence("test/8-13", seqstring);
+    assertEquals(0, PA.getValue(sq, "changeCount"));
+
+    assertEquals(0, sq.replace('A', 'A')); // same char
+    assertEquals(seqstring, sq.getSequenceAsString());
+    assertEquals(0, PA.getValue(sq, "changeCount"));
+
+    assertEquals(0, sq.replace('X', 'Y')); // not there
+    assertEquals(seqstring, sq.getSequenceAsString());
+    assertEquals(0, PA.getValue(sq, "changeCount"));
+
+    assertEquals(1, sq.replace('A', 'K'));
+    assertEquals("-K--BCD-EF--", sq.getSequenceAsString());
+    assertEquals(1, PA.getValue(sq, "changeCount"));
+
+    assertEquals(6, sq.replace('-', '.'));
+    assertEquals(".K..BCD.EF..", sq.getSequenceAsString());
+    assertEquals(2, PA.getValue(sq, "changeCount"));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFindPositions()
+  {
+    SequenceI sq = new Sequence("test/8-13", "-ABC---DE-F--");
+
+    /*
+     * invalid inputs
+     */
+    assertNull(sq.findPositions(6, 5));
+    assertNull(sq.findPositions(0, 5));
+    assertNull(sq.findPositions(-1, 5));
+
+    /*
+     * all gapped ranges
+     */
+    assertNull(sq.findPositions(1, 1)); // 1-based columns
+    assertNull(sq.findPositions(5, 5));
+    assertNull(sq.findPositions(5, 6));
+    assertNull(sq.findPositions(5, 7));
+
+    /*
+     * all ungapped ranges
+     */
+    assertEquals(new Range(8, 8), sq.findPositions(2, 2)); // A
+    assertEquals(new Range(8, 9), sq.findPositions(2, 3)); // AB
+    assertEquals(new Range(8, 10), sq.findPositions(2, 4)); // ABC
+    assertEquals(new Range(9, 10), sq.findPositions(3, 4)); // BC
+
+    /*
+     * gap to ungapped range
+     */
+    assertEquals(new Range(8, 10), sq.findPositions(1, 4)); // ABC
+    assertEquals(new Range(11, 12), sq.findPositions(6, 9)); // DE
+
+    /*
+     * ungapped to gapped range
+     */
+    assertEquals(new Range(10, 10), sq.findPositions(4, 5)); // C
+    assertEquals(new Range(9, 13), sq.findPositions(3, 11)); // BCDEF
+
+    /*
+     * ungapped to ungapped enclosing gaps
+     */
+    assertEquals(new Range(10, 11), sq.findPositions(4, 8)); // CD
+    assertEquals(new Range(8, 13), sq.findPositions(2, 11)); // ABCDEF
+
+    /*
+     * gapped to gapped enclosing ungapped
+     */
+    assertEquals(new Range(8, 10), sq.findPositions(1, 5)); // ABC
+    assertEquals(new Range(11, 12), sq.findPositions(5, 10)); // DE
+    assertEquals(new Range(8, 13), sq.findPositions(1, 13)); // the lot
+    assertEquals(new Range(8, 13), sq.findPositions(1, 99));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFindFeatures_largeEndPos()
+  {
+    /*
+     * imitate a PDB sequence where end is larger than end position
+     */
+    SequenceI sq = new Sequence("test", "-ABC--DEF--", 1, 20);
+    sq.createDatasetSequence();
+  
+    assertTrue(sq.findFeatures(1, 9).isEmpty());
+    // should be no array bounds exception - JAL-2772
+    assertTrue(sq.findFeatures(1, 15).isEmpty());
+  
+    // add feature on BCD
+    SequenceFeature sfBCD = new SequenceFeature("Cath", "desc", 2, 4, 2f,
+            null);
+    sq.addSequenceFeature(sfBCD);
+  
+    // no features in columns 1-2 (-A)
+    List<SequenceFeature> found = sq.findFeatures(1, 2);
+    assertTrue(found.isEmpty());
+  
+    // columns 1-6 (-ABC--) includes BCD
+    found = sq.findFeatures(1, 6);
+    assertEquals(1, found.size());
+    assertTrue(found.contains(sfBCD));
+
+    // columns 10-11 (--) should find nothing
+    found = sq.findFeatures(10, 11);
+    assertEquals(0, found.size());
+  }
 }