Merge branch 'bug/JAL-4313-make-visible-annot-fix' into JAL-1601-direct-jpred4-rest...
authorMateusz Warowny <mmzwarowny@dundee.ac.uk>
Mon, 30 Oct 2023 15:27:05 +0000 (16:27 +0100)
committerMateusz Warowny <mmzwarowny@dundee.ac.uk>
Mon, 30 Oct 2023 15:27:05 +0000 (16:27 +0100)
src/jalview/datamodel/AlignmentAnnotation.java
test/jalview/datamodel/AlignmentAnnotationTests.java
test/jalview/datamodel/AnnotationTest.java [new file with mode: 0644]
test/jalview/testutils/AnnotationsMatcher.java [new file with mode: 0644]
test/jalview/testutils/AnnotationsMatcherTest.java [new file with mode: 0644]
test/jalview/testutils/Matchers.java

index 4ebb27f..0e0239a 100755 (executable)
@@ -1581,7 +1581,7 @@ public class AlignmentAnnotation
   {
     if (annotations != null)
     {
-      makeVisibleAnnotation(0, annotations.length, hiddenColumns);
+      makeVisibleAnnotation(0, annotations.length - 1, hiddenColumns);
     }
   }
 
index f9e99d0..6ffbfa9 100644 (file)
  */
 package jalview.datamodel;
 
-import static org.testng.Assert.assertNull;
-import static org.testng.AssertJUnit.assertEquals;
-
 import jalview.analysis.AlignSeq;
 import jalview.gui.JvOptionPane;
 import jalview.io.AppletFormatAdapter;
 import jalview.io.FileFormat;
-
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import static jalview.datamodel.Annotation.EMPTY_ANNOTATION;
+import static jalview.testutils.Matchers.matchesAnnotations;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.testng.Assert.assertNull;
+import static org.testng.AssertJUnit.assertEquals;
+
 public class AlignmentAnnotationTests
 {
 
@@ -57,7 +61,7 @@ public class AlignmentAnnotationTests
     for (String key : alo.getProperties())
     {
       assertEquals("Property mismatch", alo.getProperty(key),
-              alc.getProperty(key));
+          alc.getProperty(key));
     }
   }
 
@@ -71,11 +75,11 @@ public class AlignmentAnnotationTests
     Annotation[] al = new Annotation[sq.getLength()];
     for (int i = 0; i < al.length; i++)
     {
-      al[i] = new Annotation(new Annotation("" + sq.getCharAt(i), "",
-              (char) 0, sq.findPosition(i)));
+      al[i] = new Annotation(new Annotation("" + sq.getCharAt(i), "", (char) 0,
+          sq.findPosition(i)));
     }
-    AlignmentAnnotation alan = new AlignmentAnnotation(
-            "For " + sq.getName(), "Fake alignment annot", al);
+    AlignmentAnnotation alan = new AlignmentAnnotation("For " + sq.getName(),
+        "Fake alignment annot", al);
     // create a sequence mapping for the annotation vector in its current state
     alan.createSequenceMapping(sq, sq.getStart(), false);
     alan.setProperty("CreatedBy", "createAnnotation");
@@ -90,20 +94,18 @@ public class AlignmentAnnotationTests
    */
   public static void testAnnotTransfer(AlignmentAnnotation ala)
   {
-    assertEquals(
-            "Failed - need annotation created by createAnnotation method",
-            ala.description, "Fake alignment annot");
+    assertEquals("Failed - need annotation created by createAnnotation method",
+        ala.description, "Fake alignment annot");
     ala.adjustForAlignment();
     for (int p = 0; p < ala.annotations.length; p++)
     {
       if (ala.annotations[p] != null)
       {
         assertEquals(
-                "Mismatch at position " + p
-                        + " between annotation position value and sequence"
-                        + ala.annotations[p],
-                (int) ala.annotations[p].value,
-                ala.sequenceRef.findPosition(p));
+            "Mismatch at position " + p
+                + " between annotation position value and sequence"
+                + ala.annotations[p],
+            (int) ala.annotations[p].value, ala.sequenceRef.findPosition(p));
       }
     }
   }
@@ -127,8 +129,7 @@ public class AlignmentAnnotationTests
     AlignmentAnnotation origTo = sqTo.getAnnotation()[0];
     createAnnotation(sqFrom);
     AlignmentAnnotation origFrom = sqFrom.getAnnotation()[0];
-    AlignSeq align = AlignSeq.doGlobalNWAlignment(sqFrom, sqTo,
-            AlignSeq.PEP);
+    AlignSeq align = AlignSeq.doGlobalNWAlignment(sqFrom, sqTo, AlignSeq.PEP);
     SequenceI alSeq1 = new Sequence(sqFrom.getName(), align.getAStr1());
     alSeq1.setStart(sqFrom.getStart() + align.getSeq1Start() - 1);
     alSeq1.setEnd(sqFrom.getStart() + align.getSeq1End() - 1);
@@ -137,21 +138,23 @@ public class AlignmentAnnotationTests
     alSeq2.setStart(sqTo.getStart() + align.getSeq2Start() - 1);
     alSeq2.setEnd(sqTo.getStart() + align.getSeq2End() - 1);
     alSeq2.setDatasetSequence(sqTo);
-    System.out.println(new AppletFormatAdapter().formatSequences(
-            FileFormat.Stockholm, new Alignment(new SequenceI[]
-            { sqFrom, alSeq1, sqTo, alSeq2 }), true));
+    System.out
+        .println(new AppletFormatAdapter()
+            .formatSequences(FileFormat.Stockholm,
+                new Alignment(new SequenceI[] { sqFrom, alSeq1, sqTo, alSeq2 }),
+                true));
 
     Mapping mp = align.getMappingFromS1(false);
 
     AlignmentAnnotation almap1 = new AlignmentAnnotation(
-            sqTo.getAnnotation()[0]);
+        sqTo.getAnnotation()[0]);
     almap1.liftOver(sqFrom, mp);
     assertEquals(almap1.sequenceRef, sqFrom);
     alSeq1.addAlignmentAnnotation(almap1);
     almap1.setSequenceRef(alSeq1);
     almap1.adjustForAlignment();
     AlignmentAnnotation almap2 = new AlignmentAnnotation(
-            sqFrom.getAnnotation()[0]);
+        sqFrom.getAnnotation()[0]);
     almap2.liftOver(sqTo, mp);
     assertEquals(almap2.sequenceRef, sqTo);
 
@@ -162,15 +165,15 @@ public class AlignmentAnnotationTests
     AlignmentI all = new Alignment(new SequenceI[] { alSeq1, alSeq2 });
     all.addAnnotation(almap1);
     all.addAnnotation(almap2);
-    System.out.println(new AppletFormatAdapter()
+    System.out
+        .println(new AppletFormatAdapter()
             .formatSequences(FileFormat.Stockholm, all, true));
 
     for (int p = 0; p < alSeq1.getLength(); p++)
     {
       Annotation orig1, trans1, orig2, trans2;
       trans2 = almap2.annotations[p];
-      orig2 = origFrom.annotations[alSeq1.findPosition(p)
-              - sqFrom.getStart()];
+      orig2 = origFrom.annotations[alSeq1.findPosition(p) - sqFrom.getStart()];
       orig1 = origTo.annotations[alSeq2.findPosition(p) - sqTo.getStart()];
       trans1 = almap1.annotations[p];
       if (trans1 == trans2)
@@ -178,20 +181,18 @@ public class AlignmentAnnotationTests
         System.out.println("Pos " + p + " mismatch");
         continue;
       }
-      assertEquals(
-              "Mismatch on Original From and transferred annotation on 2",
-              (orig2 != null) ? orig2.toString() : null,
-              (trans2 != null) ? trans2.toString() : null);
-      assertEquals(
-              "Mismatch on Original To and transferred annotation on 1",
-              (orig1 != null) ? orig1.toString() : null,
-              (trans1 != null) ? trans1.toString() : null);
+      assertEquals("Mismatch on Original From and transferred annotation on 2",
+          (orig2 != null) ? orig2.toString() : null,
+          (trans2 != null) ? trans2.toString() : null);
+      assertEquals("Mismatch on Original To and transferred annotation on 1",
+          (orig1 != null) ? orig1.toString() : null,
+          (trans1 != null) ? trans1.toString() : null);
       String alm1 = "" + (almap1.annotations.length > p
-              ? almap1.annotations[p].displayCharacter
-              : "Out of range");
+          ? almap1.annotations[p].displayCharacter
+          : "Out of range");
       String alm2 = "" + (almap2.annotations.length > p
-              ? almap2.annotations[p].displayCharacter
-              : "Out of range");
+          ? almap2.annotations[p].displayCharacter
+          : "Out of range");
       assertEquals("Position " + p + " " + alm1 + " " + alm2, alm1, alm2);
     }
   }
@@ -205,10 +206,10 @@ public class AlignmentAnnotationTests
     /*
      * Annotate positions 3/4/5 (CDE) with values 1/2/3
      */
-    Annotation[] anns = new Annotation[] { null, null, new Annotation(1),
-        new Annotation(2), new Annotation(3) };
+    Annotation[] anns = new Annotation[] {
+        null, null, new Annotation(1), new Annotation(2), new Annotation(3) };
     AlignmentAnnotation ann = new AlignmentAnnotation("SS",
-            "secondary structure", anns);
+        "secondary structure", anns);
     seq.addAlignmentAnnotation(ann);
 
     /*
@@ -247,7 +248,7 @@ public class AlignmentAnnotationTests
   public void testGetDefaultRnaHelixSymbol()
   {
     AlignmentAnnotation ann = new AlignmentAnnotation("SS",
-            "secondary structure", null);
+        "secondary structure", null);
     assertEquals("(", ann.getDefaultRnaHelixSymbol(4));
 
     Annotation[] anns = new Annotation[20];
@@ -276,7 +277,7 @@ public class AlignmentAnnotationTests
     for (int i = 0; i < expected.length(); i++)
     {
       assertEquals("column " + i, String.valueOf(expected.charAt(i)),
-              ann.getDefaultRnaHelixSymbol(i));
+          ann.getDefaultRnaHelixSymbol(i));
     }
 
     /*
@@ -296,7 +297,7 @@ public class AlignmentAnnotationTests
     for (int i = 0; i < expected.length(); i++)
     {
       assertEquals("column " + i, String.valueOf(expected.charAt(i)),
-              ann.getDefaultRnaHelixSymbol(i));
+          ann.getDefaultRnaHelixSymbol(i));
     }
   }
 
@@ -319,28 +320,32 @@ public class AlignmentAnnotationTests
     AlignmentAnnotation ann = null;
 
     ann = new AlignmentAnnotation("an", "some an", null);
-    Assert.assertFalse(ann.isQuantitative(),
+    Assert
+        .assertFalse(ann.isQuantitative(),
             "Empty annotation set should not be quantitative.");
 
     ann = new AlignmentAnnotation("an", "some an",
-            new Annotation[]
-            { newAnnotation("4"), newAnnotation("1"), newAnnotation("1"),
-                newAnnotation("0.1"), newAnnotation("0.3") });
-    Assert.assertTrue(ann.isQuantitative(),
+        new Annotation[] {
+            newAnnotation("4"), newAnnotation("1"), newAnnotation("1"),
+            newAnnotation("0.1"), newAnnotation("0.3") });
+    Assert
+        .assertTrue(ann.isQuantitative(),
             "All numbers annotation set should be quantitative.");
 
     ann = new AlignmentAnnotation("an", "some an",
-            new Annotation[]
-            { newAnnotation("E"), newAnnotation("E"), newAnnotation("E"),
-                newAnnotation("E"), newAnnotation("E") });
-    Assert.assertFalse(ann.isQuantitative(),
+        new Annotation[] {
+            newAnnotation("E"), newAnnotation("E"), newAnnotation("E"),
+            newAnnotation("E"), newAnnotation("E") });
+    Assert
+        .assertFalse(ann.isQuantitative(),
             "All 'E' annotation set should not be quantitative.");
 
     ann = new AlignmentAnnotation("an", "some an",
-            new Annotation[]
-            { newAnnotation("E"), newAnnotation("1"), newAnnotation("2"),
-                newAnnotation("3"), newAnnotation("E") });
-    Assert.assertTrue(ann.isQuantitative(),
+        new Annotation[] {
+            newAnnotation("E"), newAnnotation("1"), newAnnotation("2"),
+            newAnnotation("3"), newAnnotation("E") });
+    Assert
+        .assertTrue(ann.isQuantitative(),
             "Mixed 'E' annotation set should be quantitative.");
   }
 
@@ -348,16 +353,15 @@ public class AlignmentAnnotationTests
   public void testMakeVisibleAnnotation()
   {
     HiddenColumns h = new HiddenColumns();
-    Annotation[] anns = new Annotation[] { null, null, new Annotation(1),
-        new Annotation(2), new Annotation(3), null, null, new Annotation(4),
-        new Annotation(5), new Annotation(6), new Annotation(7),
-        new Annotation(8) };
-    AlignmentAnnotation ann = new AlignmentAnnotation("an", "some an",
-            anns);
+    Annotation[] anns = new Annotation[] {
+        null, null, new Annotation(1), new Annotation(2), new Annotation(3),
+        null, null, new Annotation(4), new Annotation(5), new Annotation(6),
+        new Annotation(7), new Annotation(8) };
+    AlignmentAnnotation ann = new AlignmentAnnotation("an", "some an", anns);
 
     // null annotations
     AlignmentAnnotation emptyann = new AlignmentAnnotation("an", "some ann",
-            null);
+        null);
     emptyann.makeVisibleAnnotation(h);
     assertNull(emptyann.annotations);
 
@@ -387,10 +391,10 @@ public class AlignmentAnnotationTests
     assertEquals(3.0f, ann.annotations[1].value);
     assertNull(ann.annotations[2]);
 
-    anns = new Annotation[] { null, null, new Annotation(1),
-        new Annotation(2), new Annotation(3), null, null, new Annotation(4),
-        new Annotation(5), new Annotation(6), new Annotation(7),
-        new Annotation(8) };
+    anns = new Annotation[] {
+        null, null, new Annotation(1), new Annotation(2), new Annotation(3),
+        null, null, new Annotation(4), new Annotation(5), new Annotation(6),
+        new Annotation(7), new Annotation(8) };
     ann = new AlignmentAnnotation("an", "some an", anns);
     h.hideColumns(4, 7);
     ann.makeVisibleAnnotation(1, 9, h);
@@ -401,10 +405,10 @@ public class AlignmentAnnotationTests
     assertEquals(5.0f, ann.annotations[3].value);
     assertEquals(6.0f, ann.annotations[4].value);
 
-    anns = new Annotation[] { null, null, new Annotation(1),
-        new Annotation(2), new Annotation(3), null, null, new Annotation(4),
-        new Annotation(5), new Annotation(6), new Annotation(7),
-        new Annotation(8) };
+    anns = new Annotation[] {
+        null, null, new Annotation(1), new Annotation(2), new Annotation(3),
+        null, null, new Annotation(4), new Annotation(5), new Annotation(6),
+        new Annotation(7), new Annotation(8) };
     ann = new AlignmentAnnotation("an", "some an", anns);
     h.hideColumns(1, 2);
     ann.makeVisibleAnnotation(1, 9, h);
@@ -413,12 +417,12 @@ public class AlignmentAnnotationTests
     assertEquals(5.0f, ann.annotations[1].value);
     assertEquals(6.0f, ann.annotations[2].value);
 
-    anns = new Annotation[] { null, null, new Annotation(1),
-        new Annotation(2), new Annotation(3), null, null, new Annotation(4),
-        new Annotation(5), new Annotation(6), new Annotation(7),
-        new Annotation(8), new Annotation(9), new Annotation(10),
-        new Annotation(11), new Annotation(12), new Annotation(13),
-        new Annotation(14), new Annotation(15) };
+    anns = new Annotation[] {
+        null, null, new Annotation(1), new Annotation(2), new Annotation(3),
+        null, null, new Annotation(4), new Annotation(5), new Annotation(6),
+        new Annotation(7), new Annotation(8), new Annotation(9),
+        new Annotation(10), new Annotation(11), new Annotation(12),
+        new Annotation(13), new Annotation(14), new Annotation(15) };
     ann = new AlignmentAnnotation("an", "some an", anns);
     h = new HiddenColumns();
     h.hideColumns(5, 18);
@@ -431,4 +435,154 @@ public class AlignmentAnnotationTests
     assertNull(ann.annotations[0]);
     assertNull(ann.annotations[4]);
   }
+
+  @Test(groups = "Functional")
+  public void testMakeVisibleAnnotation_NullAnnotationsAndNoColsHidden()
+  {
+    AlignmentAnnotation ann = new AlignmentAnnotation("label", "desc", null);
+    HiddenColumns hc = new HiddenColumns();
+    ann.makeVisibleAnnotation(hc);
+    assertThat(ann.annotations, is(nullValue()));
+  }
+
+  @Test(groups = "Functional")
+  public void testMakeVisibleAnnotation_NullAnnotationsAndTrim()
+  {
+    AlignmentAnnotation ann = new AlignmentAnnotation("label", "desc", null);
+    HiddenColumns hc = new HiddenColumns();
+    ann.makeVisibleAnnotation(3, 5, hc);
+    assertThat(ann.annotations, is(nullValue()));
+  }
+
+  @Test(groups = "Functional")
+  public void testMakeVisibleAnnotation_NoColsHidden()
+  {
+    Annotation[] annots = new Annotation[] {
+        EMPTY_ANNOTATION, EMPTY_ANNOTATION, new Annotation(1),
+        new Annotation(2), new Annotation(3), EMPTY_ANNOTATION,
+        EMPTY_ANNOTATION, new Annotation(4), new Annotation(5),
+        EMPTY_ANNOTATION, new Annotation(6), new Annotation(7),
+        new Annotation(8) };
+    AlignmentAnnotation ann = new AlignmentAnnotation("label", "desc", annots);
+    HiddenColumns hc = new HiddenColumns();
+    ann.makeVisibleAnnotation(hc);
+    assertThat(ann.annotations, matchesAnnotations(annots));
+  }
+
+  @Test(groups = "Functional")
+  public void testMakeVisibleAnnotation_HideCols()
+  {
+    Annotation[] annots = new Annotation[] {
+        new Annotation(0), new Annotation(1), new Annotation(2),
+        new Annotation(3), new Annotation(4), new Annotation(5),
+        new Annotation(6), new Annotation(7), new Annotation(8) };
+    AlignmentAnnotation ann = new AlignmentAnnotation("label", "desc", annots);
+    HiddenColumns hc = new HiddenColumns();
+    hc.hideColumns(2, 6);
+    ann.makeVisibleAnnotation(hc);
+    var expected = new Annotation[] {
+        new Annotation(0), new Annotation(1), new Annotation(7),
+        new Annotation(8) };
+    assertThat(ann.annotations, matchesAnnotations(expected));
+  }
+
+  @Test(groups = "Functional")
+  public void testMakeVisibleAnnotation_ExplicitFullWidthAndHideCols()
+  {
+    Annotation[] annots = new Annotation[] {
+        new Annotation(0), new Annotation(1), new Annotation(2),
+        new Annotation(3), new Annotation(4), new Annotation(5),
+        new Annotation(6), new Annotation(7), new Annotation(8),
+        new Annotation(9), new Annotation(10), new Annotation(11) };
+    AlignmentAnnotation ann = new AlignmentAnnotation("label", "desc", annots);
+    HiddenColumns hc = new HiddenColumns();
+    hc.hideColumns(4, 7);
+    ann.makeVisibleAnnotation(0, 11, hc);
+    assertThat(ann.annotations,
+        matchesAnnotations(new Annotation(0), new Annotation(1),
+            new Annotation(2), new Annotation(3), new Annotation(8),
+            new Annotation(9), new Annotation(10), new Annotation(11)));
+  }
+
+  @Test(groups = "Functional")
+  public void testMakeVisibleAnnotation_ExplicitFullWidthAndHideCols2()
+  {
+    Annotation[] annots = new Annotation[] {
+        new Annotation(0), new Annotation(1), new Annotation(2),
+        new Annotation(3), new Annotation(4), new Annotation(5),
+        new Annotation(6), new Annotation(7), new Annotation(8), };
+    AlignmentAnnotation ann = new AlignmentAnnotation("label", "desc", annots);
+    HiddenColumns hc = new HiddenColumns();
+    hc.hideColumns(4, 7);
+    ann.makeVisibleAnnotation(0, 8, hc);
+    assertThat(ann.annotations,
+        matchesAnnotations(new Annotation(0), new Annotation(1),
+            new Annotation(2), new Annotation(3), new Annotation(8)));
+  }
+
+  @Test(groups = "Functional")
+  public void testMakeVisibleAnnotation_HideColsWithEmptyAnnots()
+  {
+    Annotation[] annots = new Annotation[] {
+        EMPTY_ANNOTATION, EMPTY_ANNOTATION, new Annotation(1),
+        new Annotation(2), new Annotation(3), EMPTY_ANNOTATION,
+        EMPTY_ANNOTATION, new Annotation(4), new Annotation(5),
+        EMPTY_ANNOTATION, new Annotation(6), new Annotation(7),
+        new Annotation(8) };
+    AlignmentAnnotation ann = new AlignmentAnnotation("label", "desc", annots);
+    HiddenColumns hc = new HiddenColumns();
+    hc.hideColumns(1, 3);
+    hc.hideColumns(8, 9);
+    ann.makeVisibleAnnotation(hc);
+    var expected = new Annotation[] {
+        EMPTY_ANNOTATION, new Annotation(3), EMPTY_ANNOTATION, EMPTY_ANNOTATION,
+        new Annotation(4), new Annotation(6), new Annotation(7),
+        new Annotation(8) };
+    assertThat(ann.annotations, matchesAnnotations(expected));
+  }
+
+  @Test(groups = "Functional")
+  public void testMakeVisibleAnnotation_HideColsWithNullAnnots()
+  {
+    Annotation[] annots = new Annotation[] {
+        null, null, new Annotation(2), null, new Annotation(4),
+        new Annotation(5), null, null };
+    AlignmentAnnotation ann = new AlignmentAnnotation("label", "desc", annots);
+    HiddenColumns hc = new HiddenColumns();
+    hc.hideColumns(2, 4);
+    ann.makeVisibleAnnotation(hc);
+    var expected = new Annotation[] {
+        null, null, new Annotation(5), null, null };
+    assertThat(ann.annotations, matchesAnnotations(expected));
+  }
+
+  @Test(groups = "Functional")
+  public void testMakeVisibleAnnotation_Truncate()
+  {
+    Annotation[] annots = new Annotation[] {
+        new Annotation(0), new Annotation(1), new Annotation(2),
+        new Annotation(3), new Annotation(4), new Annotation(5),
+        new Annotation(6), new Annotation(7) };
+    AlignmentAnnotation ann = new AlignmentAnnotation("label", "desc", annots);
+    ann.makeVisibleAnnotation(3, 6, new HiddenColumns());
+    assertThat(ann.annotations, matchesAnnotations(new Annotation(3),
+        new Annotation(4), new Annotation(5), new Annotation(6)));
+  }
+
+  @Test(groups = "Functional")
+  public void testMakeVisibleAnnotation_TruncateAndHideColumns()
+  {
+    Annotation[] annots = new Annotation[] {
+        new Annotation(0), new Annotation(1), new Annotation(2),
+        new Annotation(3), new Annotation(4), new Annotation(5),
+        new Annotation(6), new Annotation(7), new Annotation(8),
+        new Annotation(9), new Annotation(10), new Annotation(11) };
+    AlignmentAnnotation ann = new AlignmentAnnotation("label", "desc", annots);
+    HiddenColumns hc = new HiddenColumns();
+    hc.hideColumns(4, 7);
+    ann.makeVisibleAnnotation(1, 9, hc);
+    assertThat(ann.annotations,
+        matchesAnnotations(new Annotation(1), new Annotation(2),
+            new Annotation(3), new Annotation(8), new Annotation(9)));
+  }
 }
diff --git a/test/jalview/datamodel/AnnotationTest.java b/test/jalview/datamodel/AnnotationTest.java
new file mode 100644 (file)
index 0000000..a8f258d
--- /dev/null
@@ -0,0 +1,82 @@
+package jalview.datamodel;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+
+import java.awt.Color;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+public class AnnotationTest
+{
+  @Test(groups = "Functional")
+  public void testConstructor_ValueOnly()
+  {
+    var annotation = new Annotation(0.5f);
+    assertThat(annotation.displayCharacter, nullValue());
+    assertThat(annotation.description, nullValue());
+    assertThat(annotation.secondaryStructure, is(' '));
+    assertThat(annotation.value, is(0.5f));
+    assertThat(annotation.colour, is(nullValue()));
+  }
+
+  @Test(groups = "Functional")
+  public void testCopyConstructor_NullValue_EmptyAnnotationCreated()
+  {
+    var annotation = new Annotation((Annotation) null);
+    assertThat(annotation.displayCharacter, equalTo(""));
+    assertThat(annotation.description, equalTo(""));
+    assertThat(annotation.secondaryStructure, is(' '));
+    assertThat(annotation.value, is(0.0f));
+    assertThat(annotation.colour, is(nullValue()));
+  }
+
+  @DataProvider
+  public Object[] emptyAnnotations()
+  {
+    return new Object[] {
+        Annotation.EMPTY_ANNOTATION, new Annotation(0.0f),
+        new Annotation((Annotation) null),
+        new Annotation(Annotation.EMPTY_ANNOTATION),
+        new Annotation("", "", ' ', 0.0f),
+        new Annotation(null, null, ' ', 0.0f),
+        new Annotation("", "", '\0', 0.0f), new Annotation("", null, ' ', 0.0f),
+        new Annotation(null, "", ' ', 0.0f), new Annotation(" ", "", ' ', 0.0f),
+        new Annotation(" .", "", ' ', 0.0f), new Annotation("", " ", ' ', 0.0f),
+        new Annotation("", "", ' ', 0.0f, null),
+        new Annotation("", " ", ' ', 0.0f),
+        new Annotation("", "\n", ' ', 0.0f), };
+  }
+
+  @Test(groups = "Functional", dataProvider = "emptyAnnotations")
+  public void testIsWhitespace_EmptyAnnotations(Annotation annot)
+  {
+    assertThat("Annotation " + annot + " is not whitespace, but should be",
+        annot.isWhitespace());
+  }
+
+  @DataProvider
+  public Object[] nonEmptyAnnotations()
+  {
+    return new Object[] {
+        new Annotation(0.4f),
+        new Annotation(new Annotation(0.1f)),
+        new Annotation("A", "", ' ', 0.0f),
+        new Annotation("", "", ' ', 0.0f, Color.WHITE),
+        new Annotation(null, null, ' ', -0.1f),
+        new Annotation(null, null, 'A', 0.0f),
+        new Annotation(null, "desc", ' ', 0.0f),
+        new Annotation("0", "<nil>", '\0', 0.0f),
+    };
+  }
+  
+  @Test(groups = "Functional", dataProvider = "nonEmptyAnnotations")
+  public void testIsWhitespace_NonEmptyAnnotation(Annotation annot)
+  {
+    assertThat("Annotation " + annot + " is whitespace, but should not be",
+        !annot.isWhitespace());
+  }
+}
diff --git a/test/jalview/testutils/AnnotationsMatcher.java b/test/jalview/testutils/AnnotationsMatcher.java
new file mode 100644 (file)
index 0000000..cc64d27
--- /dev/null
@@ -0,0 +1,82 @@
+package jalview.testutils;
+
+import java.util.List;
+import java.util.Objects;
+import static java.util.Objects.requireNonNullElse;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+import jalview.datamodel.Annotation;
+
+public class AnnotationsMatcher extends TypeSafeMatcher<Annotation[]>
+{
+  final List<Annotation> annotations;
+
+  public AnnotationsMatcher(List<Annotation> annotations)
+  {
+    this.annotations = annotations;
+  }
+
+  @Override
+  public boolean matchesSafely(Annotation[] items)
+  {
+    if (annotations.size() != items.length)
+      return false;
+    for (int i = 0; i < annotations.size(); i++)
+    {
+      var actual = items[i];
+      var expected = annotations.get(i);
+      if (!annotationsEqual(actual, expected))
+        return false;
+    }
+    return true;
+  }
+
+  static boolean annotationsEqual(Annotation a, Annotation b)
+  {
+    if (a == null && b == null)
+      return true;
+    if ((a == null) != (b == null)) // if one is null but the other is not
+      return false;
+    return a.secondaryStructure == b.secondaryStructure && a.value == b.value
+        && Objects.equals(a.colour, b.colour)
+        && Objects
+            .equals(requireNonNullElse(a.displayCharacter, ""),
+                requireNonNullElse(b.displayCharacter, ""))
+        && Objects
+            .equals(requireNonNullElse(a.description, ""),
+                requireNonNullElse(b.description, ""));
+  }
+
+  @Override
+  public void describeTo(Description description)
+  {
+    description.appendText("annotations ").appendValue(annotations);
+  }
+
+  @Override
+  public void describeMismatchSafely(Annotation[] items,
+      Description description)
+  {
+    if (annotations.size() != items.length)
+    {
+      description.appendText("but had length ").appendValue(items.length);
+      return;
+    }
+    boolean first = true;
+    for (int i = 0; i < annotations.size(); i++)
+    {
+      var actual = items[i];
+      var expected = annotations.get(i);
+      if (!annotationsEqual(actual, expected))
+      {
+        description
+            .appendText(first ? "but " : ", ")
+            .appendText("element [" + i + "] was ")
+            .appendValue(items[i]);
+        first = false;
+      }
+    }
+  }
+}
diff --git a/test/jalview/testutils/AnnotationsMatcherTest.java b/test/jalview/testutils/AnnotationsMatcherTest.java
new file mode 100644 (file)
index 0000000..3fbdab6
--- /dev/null
@@ -0,0 +1,174 @@
+package jalview.testutils;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import jalview.datamodel.Annotation;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.awt.Color;
+import java.util.*;
+
+public class AnnotationsMatcherTest
+{
+  @DataProvider
+  public Object[][] matchingAnnotationsLists()
+  {
+    return new Object[][] {
+        {
+            new Annotation[] {
+                new Annotation("C", "", 'C', 0.0f),
+                new Annotation("C", "", 'C', 0.1f),
+                new Annotation("B", "", 'B', 0.2f) },
+            new Annotation[] {
+                new Annotation("C", "", 'C', 0.0f),
+                new Annotation("C", "", 'C', 0.1f),
+                new Annotation("B", "", 'B', 0.2f) } },
+        {
+            new Annotation[] {
+                new Annotation("X", "xxx", 'X', 1.3f),
+                new Annotation("V", "vvv", 'V', 1.5f),
+                new Annotation("B", "bbb", 'B', 2.5f) },
+            new Annotation[] {
+                new Annotation("X", "xxx", 'X', 1.3f),
+                new Annotation("V", "vvv", 'V', 1.5f),
+                new Annotation("B", "bbb", 'B', 2.5f) } },
+        {
+            new Annotation[] {
+                new Annotation("A", "", 'A', 0.2f, Color.RED),
+                new Annotation("A", "", 'A', 0.3f, Color.GREEN),
+                new Annotation("C", "", 'C', 0.4f, Color.YELLOW),
+                new Annotation("C", "", 'C', 0.5f) },
+            new Annotation[] {
+                new Annotation("A", "", 'A', 0.2f, Color.RED),
+                new Annotation("A", "", 'A', 0.3f, Color.GREEN),
+                new Annotation("C", "", 'C', 0.4f, Color.YELLOW),
+                new Annotation("C", "", 'C', 0.5f) } },
+        {
+            new Annotation[] {
+                Annotation.EMPTY_ANNOTATION, Annotation.EMPTY_ANNOTATION },
+            new Annotation[] {
+                Annotation.EMPTY_ANNOTATION, Annotation.EMPTY_ANNOTATION } },
+        {
+            new Annotation[] { null, null, null, null },
+            new Annotation[] { null, null, null, null } } };
+  }
+
+  @Test(groups = { "Functional" }, dataProvider = "matchingAnnotationsLists")
+  public void testMatchesSafely_matchingAnnotations(Annotation[] template,
+      Annotation[] testdata)
+  {
+    assert new AnnotationsMatcher(Arrays.asList(template))
+        .matchesSafely(testdata);
+  }
+
+  @DataProvider
+  public Object[][] matchingAnnotations()
+  {
+    return new Object[][] {
+        {
+            new Annotation("A", "aaa", 'A', 0.1f),
+            new Annotation("A", "aaa", 'A', 0.1f) },
+        {
+            new Annotation("A", "abcdef", 'A', 0.1f, Color.RED),
+            new Annotation("A", "abcdef", 'A', 0.1f, Color.RED) },
+        {
+            new Annotation("", "xxxx", ' ', 0.0f),
+            new Annotation("", "xxxx", ' ', 0.0f) },
+        {
+            new Annotation("", "", ' ', 0.0f),
+            new Annotation("", "", ' ', 0.0f) },
+        {
+            new Annotation(null, null, ' ', 0.0f),
+            new Annotation(null, null, ' ', 0.0f) },
+        {
+            new Annotation(null, "", ' ', 0.0f),
+            new Annotation("", null, ' ', 0.0f) },
+        { new Annotation(0f), Annotation.EMPTY_ANNOTATION },
+        { new Annotation(1f), new Annotation("", "", ' ', 1f, null) }, };
+  }
+
+  @Test(groups = { "Functional" }, dataProvider = "matchingAnnotations")
+  public void testAnnotationsEqual_matchingAnnotations(Annotation first,
+      Annotation second)
+  {
+    assert AnnotationsMatcher.annotationsEqual(first, second);
+  }
+
+  @DataProvider
+  public Object[][] mismatchingAnnotations()
+  {
+    return new Object[][] {
+        {
+            new Annotation("A", "aaa", 'A', 1f),
+            new Annotation("A", "aaa", 'B', 1f) },
+        { new Annotation(1f), new Annotation(2f) },
+        { new Annotation(1f), new Annotation("A", "", 'A', 1f) } };
+  }
+
+  @Test(groups = { "Functional" }, dataProvider = "mismatchingAnnotations")
+  public void testAnnotationsEqual_mismatchingAnnotations(Annotation first,
+      Annotation second)
+  {
+    assert !AnnotationsMatcher.annotationsEqual(first, second);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testAnnotationsEqual_nullsMatch()
+  {
+    assert AnnotationsMatcher.annotationsEqual(null, null);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testAnnotationsEqual_nullNotMatchEmpty()
+  {
+    assert !AnnotationsMatcher
+        .annotationsEqual(null, Annotation.EMPTY_ANNOTATION);
+    assert !AnnotationsMatcher
+        .annotationsEqual(Annotation.EMPTY_ANNOTATION, null);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testAnnotationsEqual_compareNullDisplayCharToEmpty()
+  {
+    assert AnnotationsMatcher
+        .annotationsEqual(new Annotation("", "description", 'A', 0.1f),
+            new Annotation(null, "description", 'A', 0.1f));
+    assert AnnotationsMatcher
+        .annotationsEqual(new Annotation(null, "description", 'A', 0.1f),
+            new Annotation("", "description", 'A', 0.1f));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testAnnotationsEqual_compareNullDescriptionToEmpty()
+  {
+    assert AnnotationsMatcher
+        .annotationsEqual(new Annotation("A", null, 'A', 0.2f),
+            new Annotation("A", "", 'A', 0.2f));
+    assert AnnotationsMatcher
+        .annotationsEqual(new Annotation("A", "", 'A', 0.2f),
+            new Annotation("A", null, 'A', 0.2f));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testAnnotationsEqual_compareNullDisplayCharToBlank()
+  {
+    assert !AnnotationsMatcher
+        .annotationsEqual(new Annotation(" ", "aaa", 'A', 0.03f),
+            new Annotation(null, "aaa", 'A', 0.03f));
+    assert !AnnotationsMatcher
+        .annotationsEqual(new Annotation(null, "aaa", 'A', 0.04f),
+            new Annotation(" ", "aaa", 'A', 0.04f));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testAnnotationsEqual_compareNullDescriptionToBlank()
+  {
+    assert !AnnotationsMatcher
+        .annotationsEqual(new Annotation("A", null, 'A', 0.0f),
+            new Annotation("A", " ", 'A', 0.0f));
+    assert !AnnotationsMatcher
+        .annotationsEqual(new Annotation("A", " ", 'A', 0.01f),
+            new Annotation("A", null, 'A', 0.01f));
+  }
+}
index ceb07a0..682d1bf 100644 (file)
@@ -1,7 +1,11 @@
 package jalview.testutils;
 
+import java.util.Arrays;
+import java.util.List;
+
 import org.hamcrest.Matcher;
 
+import jalview.datamodel.Annotation;
 import jalview.datamodel.SequenceI;
 
 public class Matchers
@@ -10,4 +14,21 @@ public class Matchers
   {
     return new SequenceStringMatcher(sequence);
   }
+
+  public static Matcher<SequenceI> matchesSequenceString(SequenceI sequence)
+  {
+    return new SequenceStringMatcher(sequence.getSequenceAsString());
+  }
+
+  public static Matcher<Annotation[]> matchesAnnotations(
+      List<Annotation> annotations)
+  {
+    return new AnnotationsMatcher(annotations);
+  }
+
+  public static Matcher<Annotation[]> matchesAnnotations(
+      Annotation... annotations)
+  {
+    return new AnnotationsMatcher(Arrays.asList(annotations));
+  }
 }