JAL-3438 spotless for 2.11.2.0
[jalview.git] / test / jalview / datamodel / ColumnSelectionTest.java
index 63f80b2..d581345 100644 (file)
@@ -22,17 +22,35 @@ package jalview.datamodel;
 
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
-import static org.testng.AssertJUnit.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.AssertJUnit.fail;
+
+import jalview.analysis.AlignmentGenerator;
+import jalview.gui.JvOptionPane;
+import jalview.viewmodel.annotationfilter.AnnotationFilterParameter;
+import jalview.viewmodel.annotationfilter.AnnotationFilterParameter.SearchableAnnotationField;
+import jalview.viewmodel.annotationfilter.AnnotationFilterParameter.ThresholdType;
 
 import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
 import java.util.List;
 
+import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
 public class ColumnSelectionTest
 {
 
+  @BeforeClass(alwaysRun = true)
+  public void setUpJvOptionPane()
+  {
+    JvOptionPane.setInteractiveMode(false);
+    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+  }
+
   @Test(groups = { "Functional" })
   public void testAddElement()
   {
@@ -45,6 +63,30 @@ public class ColumnSelectionTest
     assertEquals("[2, 5, 3]", sel.toString());
   }
 
+  @Test(groups = { "Functional" })
+  public void testSetElementsFrom()
+  {
+    ColumnSelection fromcs = new ColumnSelection();
+    ColumnSelection tocs = new ColumnSelection();
+    HiddenColumns hidden = new HiddenColumns();
+
+    fromcs.addElement(2);
+    fromcs.addElement(3);
+    fromcs.addElement(5);
+
+    tocs.setElementsFrom(fromcs, hidden);
+    assertTrue(tocs.equals(fromcs));
+
+    hidden.hideColumns(4, 6);
+    tocs.setElementsFrom(fromcs, hidden);
+
+    // expect cols 2 and 3 to be selected but not 5
+    ColumnSelection expectcs = new ColumnSelection();
+    expectcs.addElement(2);
+    expectcs.addElement(3);
+    assertTrue(tocs.equals(expectcs));
+  }
+
   /**
    * Test the remove method - in particular to verify that remove(int i) removes
    * the element whose value is i, _NOT_ the i'th element.
@@ -61,168 +103,16 @@ public class ColumnSelectionTest
     cs.removeElement(1);
     List<Integer> sel = cs.getSelected();
     assertEquals(2, sel.size());
-    assertEquals(new Integer(2), sel.get(0));
-    assertEquals(new Integer(5), sel.get(1));
+    assertEquals(Integer.valueOf(2), sel.get(0));
+    assertEquals(Integer.valueOf(5), sel.get(1));
 
     // removing an element in the list removes it
     cs.removeElement(2);
+    // ...and also from the read-only view
     assertEquals(1, sel.size());
-    assertEquals(new Integer(5), sel.get(0));
-  }
-
-  /**
-   * Test the method that finds the visible column position of an alignment
-   * column, allowing for hidden columns.
-   */
-  @Test(groups = { "Functional" })
-  public void testFindColumnPosition()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    assertEquals(5, cs.findColumnPosition(5));
-
-    // hiding column 6 makes no difference
-    cs.hideColumns(6, 6);
-    assertEquals(5, cs.findColumnPosition(5));
-
-    // hiding column 4 moves column 5 to column 4
-    cs.hideColumns(4, 4);
-    assertEquals(4, cs.findColumnPosition(5));
-
-    // hiding columns 1 and 2 moves column 5 to column 2
-    cs.hideColumns(1, 2);
-    assertEquals(2, cs.findColumnPosition(5));
-  }
-
-  /**
-   * Test the code used to locate the reference sequence ruler origin
-   */
-  @Test(groups = { "Functional" })
-  public void testLocateVisibleBoundsofSequence()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    SequenceI seq = new Sequence("RefSeq", "-A-SD-ASD--E---");
-    assertEquals(2, seq.findIndex(seq.getStart()));
-
-    // no hidden columns
-    assertEquals(
-            Arrays.toString(new int[] { seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1, seq.getStart(),
-                seq.getEnd(), seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1 }),
-            Arrays.toString(cs.locateVisibleBoundsOfSequence(seq)));
-
-    // hidden column on gap after end of sequence - should not affect bounds
-    cs.hideColumns(13);
-    assertEquals(
-            Arrays.toString(new int[] { seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1, seq.getStart(),
-                seq.getEnd(), seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1 }),
-            Arrays.toString(cs.locateVisibleBoundsOfSequence(seq)));
-
-    cs.revealAllHiddenColumns();
-    // hidden column on gap before beginning of sequence - should vis bounds by
-    // one
-    cs.hideColumns(0);
-    assertEquals(
-            Arrays.toString(new int[] { seq.findIndex(seq.getStart()) - 2,
-                seq.findIndex(seq.getEnd()) - 2, seq.getStart(),
-                seq.getEnd(), seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1 }),
-            Arrays.toString(cs.locateVisibleBoundsOfSequence(seq)));
-
-    cs.revealAllHiddenColumns();
-    // hide columns around most of sequence - leave one residue remaining
-    cs.hideColumns(1, 3);
-    cs.hideColumns(6, 11);
-    assertEquals("-D",
-            cs.getVisibleSequenceStrings(0, 5, new SequenceI[] { seq })[0]);
-    assertEquals(
-            Arrays.toString(new int[] { 1, 1, 3, 3,
-                seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1 }),
-            Arrays.toString(cs.locateVisibleBoundsOfSequence(seq)));
-    cs.revealAllHiddenColumns();
-
-    // hide whole sequence - should just get location of hidden region
-    // containing sequence
-    cs.hideColumns(1, 11);
-    assertEquals(
-            Arrays.toString(new int[] { 0, 1, 0, 0,
-                seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1 }),
-            Arrays.toString(cs.locateVisibleBoundsOfSequence(seq)));
-
-  }
-
-  @Test(groups = { "Functional" })
-  public void testHideColumns()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    cs.hideColumns(5);
-    List<int[]> hidden = cs.getHiddenColumns();
-    assertEquals(1, hidden.size());
-    assertEquals("[5, 5]", Arrays.toString(hidden.get(0)));
-
-    cs.hideColumns(3);
-    assertEquals(2, hidden.size());
-    // two hidden ranges, in order:
-    assertSame(hidden, cs.getHiddenColumns());
-    assertEquals("[3, 3]", Arrays.toString(hidden.get(0)));
-    assertEquals("[5, 5]", Arrays.toString(hidden.get(1)));
-
-    // hiding column 4 expands [3, 3] to [3, 4]
-    // not fancy enough to coalesce this into [3, 5] though
-    cs.hideColumns(4);
-    hidden = cs.getHiddenColumns();
-    assertEquals(2, hidden.size());
-    assertEquals("[3, 4]", Arrays.toString(hidden.get(0)));
-    assertEquals("[5, 5]", Arrays.toString(hidden.get(1)));
-
-    // clear hidden columns (note they are added to selected)
-    cs.revealAllHiddenColumns();
-    // it is now actually null but getter returns an empty list
-    assertTrue(cs.getHiddenColumns().isEmpty());
-
-    cs.hideColumns(3, 6);
-    hidden = cs.getHiddenColumns();
-    int[] firstHiddenRange = hidden.get(0);
-    assertEquals("[3, 6]", Arrays.toString(firstHiddenRange));
-
-    // adding a subrange of already hidden should do nothing
-    cs.hideColumns(4, 5);
-    assertEquals(1, hidden.size());
-    assertSame(firstHiddenRange, cs.getHiddenColumns().get(0));
-    cs.hideColumns(3, 5);
-    assertEquals(1, hidden.size());
-    assertSame(firstHiddenRange, cs.getHiddenColumns().get(0));
-    cs.hideColumns(4, 6);
-    assertEquals(1, hidden.size());
-    assertSame(firstHiddenRange, cs.getHiddenColumns().get(0));
-    cs.hideColumns(3, 6);
-    assertEquals(1, hidden.size());
-    assertSame(firstHiddenRange, cs.getHiddenColumns().get(0));
-
-    cs.revealAllHiddenColumns();
-    cs.hideColumns(2, 4);
-    hidden = cs.getHiddenColumns();
-    assertEquals(1, hidden.size());
-    assertEquals("[2, 4]", Arrays.toString(hidden.get(0)));
-
-    // extend contiguous with 2 positions overlap
-    cs.hideColumns(3, 5);
-    assertEquals(1, hidden.size());
-    assertEquals("[2, 5]", Arrays.toString(hidden.get(0)));
-
-    // extend contiguous with 1 position overlap
-    cs.hideColumns(5, 6);
-    assertEquals(1, hidden.size());
-    assertEquals("[2, 6]", Arrays.toString(hidden.get(0)));
-
-    // extend contiguous with overlap both ends:
-    cs.hideColumns(1, 7);
-    assertEquals(1, hidden.size());
-    assertEquals("[1, 7]", Arrays.toString(hidden.get(0)));
+    sel = cs.getSelected();
+    assertEquals(1, sel.size());
+    assertEquals(Integer.valueOf(5), sel.get(0));
   }
 
   /**
@@ -234,50 +124,57 @@ public class ColumnSelectionTest
   @Test(groups = { "Functional" })
   public void testHideColumns_withSelection()
   {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    AlignmentI al = gen.generate(50, 20, 123, 5, 5);
+
     ColumnSelection cs = new ColumnSelection();
     // select columns 4-6
     cs.addElement(4);
     cs.addElement(5);
     cs.addElement(6);
     // hide column 5 (and adjacent):
-    cs.hideColumns(5);
+    cs.hideSelectedColumns(5, al.getHiddenColumns());
     // 4,5,6 now hidden:
-    List<int[]> hidden = cs.getHiddenColumns();
-    assertEquals(1, hidden.size());
-    assertEquals("[4, 6]", Arrays.toString(hidden.get(0)));
+    Iterator<int[]> regions = al.getHiddenColumns().iterator();
+    assertEquals(1, al.getHiddenColumns().getNumberOfRegions());
+    assertEquals("[4, 6]", Arrays.toString(regions.next()));
     // none now selected:
     assertTrue(cs.getSelected().isEmpty());
 
     // repeat, hiding column 4 (5 and 6)
+    al = gen.generate(50, 20, 123, 5, 5);
     cs = new ColumnSelection();
     cs.addElement(4);
     cs.addElement(5);
     cs.addElement(6);
-    cs.hideColumns(4);
-    hidden = cs.getHiddenColumns();
-    assertEquals(1, hidden.size());
-    assertEquals("[4, 6]", Arrays.toString(hidden.get(0)));
+    cs.hideSelectedColumns(4, al.getHiddenColumns());
+    regions = al.getHiddenColumns().iterator();
+    assertEquals(1, al.getHiddenColumns().getNumberOfRegions());
+    assertEquals("[4, 6]", Arrays.toString(regions.next()));
     assertTrue(cs.getSelected().isEmpty());
 
     // repeat, hiding column (4, 5 and) 6
+    al = gen.generate(50, 20, 123, 5, 5);
     cs = new ColumnSelection();
     cs.addElement(4);
     cs.addElement(5);
     cs.addElement(6);
-    cs.hideColumns(6);
-    hidden = cs.getHiddenColumns();
-    assertEquals(1, hidden.size());
-    assertEquals("[4, 6]", Arrays.toString(hidden.get(0)));
+    cs.hideSelectedColumns(6, al.getHiddenColumns());
+    regions = al.getHiddenColumns().iterator();
+    assertEquals(1, al.getHiddenColumns().getNumberOfRegions());
+    assertEquals("[4, 6]", Arrays.toString(regions.next()));
     assertTrue(cs.getSelected().isEmpty());
 
     // repeat, with _only_ adjacent columns selected
+    al = gen.generate(50, 20, 123, 5, 5);
     cs = new ColumnSelection();
     cs.addElement(4);
     cs.addElement(6);
-    cs.hideColumns(5);
-    hidden = cs.getHiddenColumns();
-    assertEquals(1, hidden.size());
-    assertEquals("[4, 6]", Arrays.toString(hidden.get(0)));
+    cs.hideSelectedColumns(5, al.getHiddenColumns());
+    regions = al.getHiddenColumns().iterator();
+    assertEquals(1, al.getHiddenColumns().getNumberOfRegions());
+    assertEquals("[4, 6]", Arrays.toString(regions.next()));
     assertTrue(cs.getSelected().isEmpty());
   }
 
@@ -287,22 +184,28 @@ public class ColumnSelectionTest
   @Test(groups = { "Functional" })
   public void testHideSelectedColumns()
   {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    AlignmentI al = gen.generate(50, 20, 123, 5, 5);
+
     ColumnSelection cs = new ColumnSelection();
     int[] sel = { 2, 3, 4, 7, 8, 9, 20, 21, 22 };
     for (int col : sel)
     {
       cs.addElement(col);
     }
-    cs.hideColumns(15, 18);
 
-    cs.hideSelectedColumns();
+    HiddenColumns cols = al.getHiddenColumns();
+    cols.hideColumns(15, 18);
+
+    cs.hideSelectedColumns(al);
     assertTrue(cs.getSelected().isEmpty());
-    List<int[]> hidden = cs.getHiddenColumns();
-    assertEquals(4, hidden.size());
-    assertEquals("[2, 4]", Arrays.toString(hidden.get(0)));
-    assertEquals("[7, 9]", Arrays.toString(hidden.get(1)));
-    assertEquals("[15, 18]", Arrays.toString(hidden.get(2)));
-    assertEquals("[20, 22]", Arrays.toString(hidden.get(3)));
+    Iterator<int[]> regions = cols.iterator();
+    assertEquals(4, cols.getNumberOfRegions());
+    assertEquals("[2, 4]", Arrays.toString(regions.next()));
+    assertEquals("[7, 9]", Arrays.toString(regions.next()));
+    assertEquals("[15, 18]", Arrays.toString(regions.next()));
+    assertEquals("[20, 22]", Arrays.toString(regions.next()));
   }
 
   /**
@@ -310,10 +213,14 @@ public class ColumnSelectionTest
    * this fails, HideSelectedColumns may also fail
    */
   @Test(groups = { "Functional" })
-  public void testgetSelectedRanges()
+  public void testGetSelectedRanges()
   {
+    /*
+     * getSelectedRanges returns ordered columns regardless
+     * of the order in which they are added
+     */
     ColumnSelection cs = new ColumnSelection();
-    int[] sel = { 2, 3, 4, 7, 8, 9, 20, 21, 22 };
+    int[] sel = { 4, 3, 7, 21, 9, 20, 8, 22, 2 };
     for (int col : sel)
     {
       cs.addElement(col);
@@ -331,104 +238,27 @@ public class ColumnSelectionTest
     assertEquals("[0, 4]", Arrays.toString(range.get(0)));
   }
 
-  /**
-   * Test the method that reveals a range of hidden columns given the start
-   * column of the range
-   */
-  @Test(groups = { "Functional" })
-  public void testRevealHiddenColumns()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    cs.hideColumns(5, 8);
-    cs.addElement(10);
-    cs.revealHiddenColumns(5);
-    // hidden columns list now null but getter returns empty list:
-    assertTrue(cs.getHiddenColumns().isEmpty());
-    // revealed columns are marked as selected (added to selection):
-    assertEquals("[10, 5, 6, 7, 8]", cs.getSelected().toString());
-
-    // calling with a column other than the range start does nothing:
-    cs = new ColumnSelection();
-    cs.hideColumns(5, 8);
-    List<int[]> hidden = cs.getHiddenColumns();
-    cs.revealHiddenColumns(6);
-    assertSame(hidden, cs.getHiddenColumns());
-    assertTrue(cs.getSelected().isEmpty());
-  }
-
-  @Test(groups = { "Functional" })
-  public void testRevealAllHiddenColumns()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    cs.hideColumns(5, 8);
-    cs.hideColumns(2, 3);
-    cs.addElement(11);
-    cs.addElement(1);
-    cs.revealAllHiddenColumns();
-
-    /*
-     * revealing hidden columns adds them (in order) to the (unordered)
-     * selection list
-     */
-    assertTrue(cs.getHiddenColumns().isEmpty());
-    assertEquals("[11, 1, 2, 3, 5, 6, 7, 8]", cs.getSelected().toString());
-  }
-
-  @Test(groups = { "Functional" })
-  public void testIsVisible()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    cs.hideColumns(2, 4);
-    cs.hideColumns(6, 7);
-    assertTrue(cs.isVisible(0));
-    assertTrue(cs.isVisible(-99));
-    assertTrue(cs.isVisible(1));
-    assertFalse(cs.isVisible(2));
-    assertFalse(cs.isVisible(3));
-    assertFalse(cs.isVisible(4));
-    assertTrue(cs.isVisible(5));
-    assertFalse(cs.isVisible(6));
-    assertFalse(cs.isVisible(7));
-  }
-
-  @Test(groups = { "Functional" })
-  public void testGetVisibleContigs()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    cs.hideColumns(3, 6);
-    cs.hideColumns(8, 9);
-    cs.hideColumns(12, 12);
-
-    // start position is inclusive, end position exclusive:
-    int[] visible = cs.getVisibleContigs(1, 13);
-    assertEquals("[1, 2, 7, 7, 10, 11]", Arrays.toString(visible));
-
-    visible = cs.getVisibleContigs(4, 14);
-    assertEquals("[7, 7, 10, 11, 13, 13]", Arrays.toString(visible));
-
-    visible = cs.getVisibleContigs(3, 10);
-    assertEquals("[7, 7]", Arrays.toString(visible));
-
-    visible = cs.getVisibleContigs(4, 6);
-    assertEquals("[]", Arrays.toString(visible));
-  }
-
   @Test(groups = { "Functional" })
   public void testInvertColumnSelection()
   {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    AlignmentI al = gen.generate(50, 20, 123, 5, 5);
+
     ColumnSelection cs = new ColumnSelection();
     cs.addElement(4);
     cs.addElement(6);
     cs.addElement(8);
-    cs.hideColumns(3, 3);
-    cs.hideColumns(6, 6);
+
+    HiddenColumns cols = al.getHiddenColumns();
+    cols.hideColumns(3, 3);
+    cols.hideColumns(6, 6);
 
     // invert selection from start (inclusive) to end (exclusive)
-    // hidden columns are _not_ changed
-    cs.invertColumnSelection(2, 9);
+    cs.invertColumnSelection(2, 9, al);
     assertEquals("[2, 5, 7]", cs.getSelected().toString());
 
-    cs.invertColumnSelection(1, 9);
+    cs.invertColumnSelection(1, 9, al);
     assertEquals("[1, 4, 8]", cs.getSelected().toString());
   }
 
@@ -463,4 +293,436 @@ public class ColumnSelectionTest
     cs.addElement(0);
     assertEquals(0, cs.getMin());
   }
+
+  @Test(groups = { "Functional" })
+  public void testEquals()
+  {
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(0);
+    cs.addElement(513);
+    cs.addElement(1);
+
+    // same selections added in a different order
+    ColumnSelection cs2 = new ColumnSelection();
+    cs2.addElement(1);
+    cs2.addElement(513);
+    cs2.addElement(0);
+
+    assertTrue(cs.equals(cs2));
+    assertTrue(cs.equals(cs));
+    assertTrue(cs2.equals(cs));
+    assertTrue(cs2.equals(cs2));
+
+    cs2.addElement(12);
+    assertFalse(cs.equals(cs2));
+    assertFalse(cs2.equals(cs));
+
+    cs2.removeElement(12);
+    assertTrue(cs.equals(cs2));
+  }
+
+  /*
+      cs2.hideSelectedColumns(88);
+      assertFalse(cs.equals(cs2));
+      /*
+       * unhiding a column adds it to selection!
+       */
+  /*    cs2.revealHiddenColumns(88);
+      assertFalse(cs.equals(cs2));
+      cs.addElement(88);
+      assertTrue(cs.equals(cs2));
+    */
+
+  /**
+   * Test the method that returns selected columns, in the order in which they
+   * were added
+   */
+  @Test(groups = { "Functional" })
+  public void testGetSelected()
+  {
+    ColumnSelection cs = new ColumnSelection();
+    int[] sel = { 4, 3, 7, 21 };
+    for (int col : sel)
+    {
+      cs.addElement(col);
+    }
+
+    List<Integer> selected = cs.getSelected();
+    assertEquals(4, selected.size());
+    assertEquals("[4, 3, 7, 21]", selected.toString());
+
+    /*
+     * getSelected returns a read-only view of the list
+     * verify the view follows any changes in it
+     */
+    cs.removeElement(7);
+    cs.addElement(1);
+    cs.removeElement(4);
+    assertEquals("[3, 21, 1]", selected.toString());
+  }
+
+  /**
+   * Test to verify that the list returned by getSelection cannot be modified
+   */
+  @Test(groups = { "Functional" })
+  public void testGetSelected_isReadOnly()
+  {
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(3);
+
+    List<Integer> selected = cs.getSelected();
+    try
+    {
+      selected.clear();
+      fail("expected exception");
+    } catch (UnsupportedOperationException e)
+    {
+      // expected
+    }
+    try
+    {
+      selected.add(1);
+      fail("expected exception");
+    } catch (UnsupportedOperationException e)
+    {
+      // expected
+    }
+    try
+    {
+      selected.remove(3);
+      fail("expected exception");
+    } catch (UnsupportedOperationException e)
+    {
+      // expected
+    }
+    try
+    {
+      Collections.sort(selected);
+      fail("expected exception");
+    } catch (UnsupportedOperationException e)
+    {
+      // expected
+    }
+  }
+
+  /**
+   * Test that demonstrates a ConcurrentModificationException is thrown if you
+   * change the selection while iterating over it
+   */
+  @Test(
+    groups = "Functional",
+    expectedExceptions =
+    { ConcurrentModificationException.class })
+  public void testGetSelected_concurrentModification()
+  {
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(0);
+    cs.addElement(1);
+    cs.addElement(2);
+
+    /*
+     * simulate changing the list under us (e.g. in a separate
+     * thread) while iterating over it -> ConcurrentModificationException
+     */
+    List<Integer> selected = cs.getSelected();
+    for (Integer col : selected)
+    {
+      if (col.intValue() == 0)
+      {
+        cs.removeElement(1);
+      }
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testMarkColumns()
+  {
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(5); // this will be cleared
+    BitSet toMark = new BitSet();
+    toMark.set(1);
+    toMark.set(3);
+    toMark.set(6);
+    toMark.set(9);
+
+    assertTrue(cs.markColumns(toMark, 3, 8, false, false, false));
+    List<Integer> selected = cs.getSelected();
+    assertEquals(2, selected.size());
+    assertTrue(selected.contains(3));
+    assertTrue(selected.contains(6));
+  }
+
+  @Test(groups = "Functional")
+  public void testMarkColumns_extend()
+  {
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(1);
+    cs.addElement(5);
+    BitSet toMark = new BitSet();
+    toMark.set(1);
+    toMark.set(3);
+    toMark.set(6);
+    toMark.set(9);
+
+    /*
+     * extending selection of {3, 6} should leave {1, 3, 5, 6} selected
+     */
+    assertTrue(cs.markColumns(toMark, 3, 8, false, true, false));
+    List<Integer> selected = cs.getSelected();
+    assertEquals(4, selected.size());
+    assertTrue(selected.contains(1));
+    assertTrue(selected.contains(3));
+    assertTrue(selected.contains(5));
+    assertTrue(selected.contains(6));
+  }
+
+  @Test(groups = "Functional")
+  public void testMarkColumns_invert()
+  {
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(5); // this will be cleared
+    BitSet toMark = new BitSet();
+    toMark.set(1);
+    toMark.set(3);
+    toMark.set(6);
+    toMark.set(9);
+
+    /*
+     * inverted selection of {3, 6} should select {4, 5, 7, 8}
+     */
+    assertTrue(cs.markColumns(toMark, 3, 8, true, false, false));
+    List<Integer> selected = cs.getSelected();
+    assertEquals(4, selected.size());
+    assertTrue(selected.contains(4));
+    assertTrue(selected.contains(5));
+    assertTrue(selected.contains(7));
+    assertTrue(selected.contains(8));
+  }
+
+  @Test(groups = "Functional")
+  public void testMarkColumns_toggle()
+  {
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(1); // outside change range
+    cs.addElement(3);
+    cs.addElement(4);
+    cs.addElement(10); // outside change range
+    BitSet toMark = new BitSet();
+    toMark.set(1);
+    toMark.set(3);
+    toMark.set(6);
+    toMark.set(9);
+
+    /*
+     * toggling state of {3, 6} should leave {1, 4, 6, 10} selected
+     */
+    assertTrue(cs.markColumns(toMark, 3, 8, false, false, true));
+    List<Integer> selected = cs.getSelected();
+    assertEquals(4, selected.size());
+    assertTrue(selected.contains(1));
+    assertTrue(selected.contains(4));
+    assertTrue(selected.contains(6));
+    assertTrue(selected.contains(10));
+  }
+
+  @Test(groups = "Functional")
+  public void testCopyConstructor()
+  {
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(3);
+    cs.addElement(1);
+
+    ColumnSelection cs2 = new ColumnSelection(cs);
+    assertTrue(cs2.hasSelectedColumns());
+
+    // order of column selection is preserved
+    assertEquals("[3, 1]", cs2.getSelected().toString());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testStretchGroup_expand()
+  {
+    /*
+     * test that emulates clicking column 4 (selected)
+     * and dragging right to column 5 (all base 0)
+     */
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(4);
+    SequenceGroup sg = new SequenceGroup();
+    sg.setStartRes(4);
+    sg.setEndRes(4);
+    cs.stretchGroup(5, sg, 4, 4);
+    assertEquals(cs.getSelected().size(), 2);
+    assertTrue(cs.contains(4));
+    assertTrue(cs.contains(5));
+    assertEquals(sg.getStartRes(), 4);
+    assertEquals(sg.getEndRes(), 5);
+
+    /*
+     * emulate drag right with columns 10-20 already selected
+     */
+    cs.clear();
+    for (int i = 10; i <= 20; i++)
+    {
+      cs.addElement(i);
+    }
+    assertEquals(cs.getSelected().size(), 11);
+    sg = new SequenceGroup();
+    sg.setStartRes(10);
+    sg.setEndRes(20);
+    cs.stretchGroup(21, sg, 10, 20);
+    assertEquals(cs.getSelected().size(), 12);
+    assertTrue(cs.contains(10));
+    assertTrue(cs.contains(21));
+    assertEquals(sg.getStartRes(), 10);
+    assertEquals(sg.getEndRes(), 21);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testStretchGroup_shrink()
+  {
+    /*
+     * emulate drag left to 19 with columns 10-20 already selected
+     */
+    ColumnSelection cs = new ColumnSelection();
+    for (int i = 10; i <= 20; i++)
+    {
+      cs.addElement(i);
+    }
+    assertEquals(cs.getSelected().size(), 11);
+    SequenceGroup sg = new SequenceGroup();
+    sg.setStartRes(10);
+    sg.setEndRes(20);
+    cs.stretchGroup(19, sg, 10, 20);
+    assertEquals(cs.getSelected().size(), 10);
+    assertTrue(cs.contains(10));
+    assertTrue(cs.contains(19));
+    assertFalse(cs.contains(20));
+    assertEquals(sg.getStartRes(), 10);
+    assertEquals(sg.getEndRes(), 19);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFilterAnnotations()
+  {
+    ColumnSelection cs = new ColumnSelection();
+
+    /*
+     * filter with no conditions clears the selection
+     */
+    Annotation[] anns = new Annotation[] { null };
+    AnnotationFilterParameter filter = new AnnotationFilterParameter();
+    cs.addElement(3);
+    int added = cs.filterAnnotations(anns, filter);
+    assertEquals(0, added);
+    assertTrue(cs.isEmpty());
+
+    /*
+     * select on description (regex)
+     */
+    filter.setRegexString("w.rld");
+    filter.addRegexSearchField(SearchableAnnotationField.DESCRIPTION);
+    Annotation helix = new Annotation("(", "hello", '<', 2f);
+    Annotation sheet = new Annotation("(", "world", '<', 2f);
+    added = cs.filterAnnotations(new Annotation[] { null, helix, sheet },
+            filter);
+    assertEquals(1, added);
+    assertTrue(cs.contains(2));
+
+    /*
+     * select on label (invalid regex, exact match)
+     */
+    filter = new AnnotationFilterParameter();
+    filter.setRegexString("(");
+    filter.addRegexSearchField(SearchableAnnotationField.DISPLAY_STRING);
+    added = cs.filterAnnotations(new Annotation[] { null, helix, sheet },
+            filter);
+    assertEquals(2, added);
+    assertTrue(cs.contains(1));
+    assertTrue(cs.contains(2));
+
+    /*
+     * select Helix (secondary structure symbol H)
+     */
+    filter = new AnnotationFilterParameter();
+    filter.setFilterAlphaHelix(true);
+    helix = new Annotation("x", "desc", 'H', 0f);
+    sheet = new Annotation("x", "desc", 'E', 1f);
+    Annotation turn = new Annotation("x", "desc", 'S', 2f);
+    Annotation ann4 = new Annotation("x", "desc", 'Y', 3f);
+    added = cs
+            .filterAnnotations(new Annotation[]
+            { null, helix, sheet, turn, ann4 }, filter);
+    assertEquals(1, added);
+    assertTrue(cs.contains(1));
+
+    /*
+     * select Helix and Sheet (E)
+     */
+    filter.setFilterBetaSheet(true);
+    added = cs
+            .filterAnnotations(new Annotation[]
+            { null, helix, sheet, turn, ann4 }, filter);
+    assertEquals(2, added);
+    assertTrue(cs.contains(1));
+    assertTrue(cs.contains(2));
+
+    /*
+     * select Sheet and Turn (S)
+     */
+    filter.setFilterAlphaHelix(false);
+    filter.setFilterTurn(true);
+    added = cs
+            .filterAnnotations(new Annotation[]
+            { null, helix, sheet, turn, ann4 }, filter);
+    assertEquals(2, added);
+    assertTrue(cs.contains(2));
+    assertTrue(cs.contains(3));
+
+    /*
+     * select value < 2f (ann1, ann2)
+     */
+    filter = new AnnotationFilterParameter();
+    filter.setThresholdType(ThresholdType.BELOW_THRESHOLD);
+    filter.setThresholdValue(2f);
+    added = cs
+            .filterAnnotations(new Annotation[]
+            { null, helix, sheet, turn, ann4 }, filter);
+    assertEquals(2, added);
+    assertTrue(cs.contains(1));
+    assertTrue(cs.contains(2));
+
+    /*
+     * select value > 2f (ann4 only)
+     */
+    filter.setThresholdType(ThresholdType.ABOVE_THRESHOLD);
+    added = cs
+            .filterAnnotations(new Annotation[]
+            { null, helix, sheet, turn, ann4 }, filter);
+    assertEquals(1, added);
+    assertTrue(cs.contains(4));
+
+    /*
+     * select >2f or Helix
+     */
+    filter.setFilterAlphaHelix(true);
+    added = cs
+            .filterAnnotations(new Annotation[]
+            { null, helix, sheet, turn, ann4 }, filter);
+    assertEquals(2, added);
+    assertTrue(cs.contains(1));
+    assertTrue(cs.contains(4));
+
+    /*
+     * select < 1f or Helix; one annotation matches both
+     * return value should only count it once
+     */
+    filter.setThresholdType(ThresholdType.BELOW_THRESHOLD);
+    filter.setThresholdValue(1f);
+    added = cs
+            .filterAnnotations(new Annotation[]
+            { null, helix, sheet, turn, ann4 }, filter);
+    assertEquals(1, added);
+    assertTrue(cs.contains(1));
+  }
 }