JAL-1499 slight refactoring of handling interleaved data blocks
[jalview.git] / test / jalview / io / MegaFileTest.java
1 package jalview.io;
2
3 import static org.testng.AssertJUnit.assertEquals;
4 import static org.testng.AssertJUnit.assertFalse;
5 import static org.testng.AssertJUnit.assertNull;
6 import static org.testng.AssertJUnit.assertTrue;
7 import static org.testng.AssertJUnit.fail;
8
9 import jalview.datamodel.AlignmentAnnotation;
10 import jalview.datamodel.AlignmentI;
11 import jalview.datamodel.Annotation;
12 import jalview.datamodel.Sequence;
13 import jalview.datamodel.SequenceFeature;
14 import jalview.datamodel.SequenceI;
15
16 import java.io.IOException;
17 import java.util.List;
18 import java.util.Vector;
19
20 import org.testng.annotations.Test;
21
22 /*
23  * Unit tests for MegaFile - read and write in MEGA format(s).
24  */
25 public class MegaFileTest
26 {
27   private static final String TWENTY_CHARS = "9876543210abcdefghij";
28
29   private static final String THIRTY_CHARS = "0123456789klmnopqrstABCDEFGHIJ";
30
31   //@formatter:off
32   private static final String INTERLEAVED = 
33           "#MEGA\n"+ 
34           "TITLE: Interleaved sequence data\n\n" + 
35           "#U455   ABCDEF\n" + 
36           "#CPZANT  MNOPQR\n\n" + 
37           "#U455   KLMNOP\n" + 
38           "#CPZANT WXYZGC";
39
40   private static final String INTERLEAVED_NOHEADERS = 
41           "#U455   ABCDEF\n" 
42           + "#CPZANT MNOPQR\n\n" 
43           + "#U455   KLMNOP\n"
44           + "#CPZANT WXYZGC\n";
45
46   // interleaved sequences, with 50 residues
47   private static final String INTERLEAVED_50RESIDUES = 
48           "#MEGA\n"
49           + "!TITLE Interleaved sequence data\n\n"
50           + "#U455 " + THIRTY_CHARS + TWENTY_CHARS + "\n" 
51           + "#CPZANT " + TWENTY_CHARS + THIRTY_CHARS + "\n";
52
53   private static final String NONINTERLEAVED = 
54           "#MEGA\n"
55           + "!TITLE Noninterleaved sequence data\n\n" 
56           + "#U455  \n"
57           + "ABCFEDHIJ\n" 
58           + "MNOPQR\n\n" 
59           + "#CPZANT \n" 
60           + "KLMNOPWXYZ\n" 
61           + "CGATC\n";
62   
63   // this one starts interleaved then switches to non-interleaved
64   private static final String MIXED = 
65           "#MEGA\n"
66           + "!TITLE This is a mess\n\n" 
67           + "#CPZANT KLMNOPWXYZCGATC\n\n"
68           + "#U455\n  "
69           + "ABCFEDHIJ\n";
70
71   // interleaved with a new sequence appearing in the second block :-O
72   private static final String INTERLEAVED_SEQUENCE_ERROR = 
73           "#MEGA" + "\n"
74           + "!TITLE Interleaved sequence data\n\n"
75           + "#U455   ABCDEF\n" 
76           + "#CPZANT  MNOPQR\n\n"
77           + "#U455   GHIJKL\n" 
78           + "#U456   KLMNOP\n"; // wossis?
79
80   // interleaved with description, bases/gaps in triplet groups
81   private static final String INTERLEAVED_WITH_DESCRIPTION = 
82           "#MEGA\n"
83           + "!Title Data with description;\n"
84           + "!Format DataType=DNA  indel=-\tCodeTable=Standard Missing=? MatchChar=.;\n\n"
85           + "!Description\n" 
86           + "    Line one of description\n"
87           + "    Line two of description;\n\n"
88           + "#U455   C-- GTA\n" 
89           + "#CPZANT ATC -G-\n\n"
90           + "#U455   CGA --T\n" 
91           + "#CPZANT CA- -GC\n";
92
93   //@formatter:on
94
95   /**
96    * Test parse of interleaved mega format data.
97    * 
98    * @throws IOException
99    */
100   @Test(groups = { "Functional" })
101   public void testParse_interleaved() throws IOException
102   {
103     MegaFile testee = new MegaFile(INTERLEAVED, AppletFormatAdapter.PASTE);
104     assertEquals("Title not as expected", "Interleaved sequence data",
105             testee.getAlignmentProperty(MegaFile.PROP_TITLE));
106     Vector<SequenceI> seqs = testee.getSeqs();
107     // should be 2 sequences
108     assertEquals("Expected two sequences", 2, seqs.size());
109     // check sequence names correct and order preserved
110     assertEquals("First sequence id wrong", "U455", seqs.get(0).getName());
111     assertEquals("Second sequence id wrong", "CPZANT", seqs.get(1)
112             .getName());
113     // check sequence data
114     assertEquals("First sequence data wrong", "ABCDEFKLMNOP", seqs.get(0)
115             .getSequenceAsString());
116     assertEquals("Second sequence data wrong", "MNOPQRWXYZGC", seqs.get(1)
117             .getSequenceAsString());
118     assertTrue("File format is not flagged as interleaved",
119             testee.isInterleaved());
120   }
121
122   /**
123    * Test parse of noninterleaved mega format data.
124    * 
125    * @throws IOException
126    */
127   @Test(groups = { "Functional" })
128   public void testParse_nonInterleaved() throws IOException
129   {
130     MegaFile testee = new MegaFile(NONINTERLEAVED,
131             AppletFormatAdapter.PASTE);
132     assertEquals("Title not as expected", "Noninterleaved sequence data",
133             testee.getAlignmentProperty(MegaFile.PROP_TITLE));
134     Vector<SequenceI> seqs = testee.getSeqs();
135     // should be 2 sequences
136     assertEquals("Expected two sequences", 2, seqs.size());
137     // check sequence names correct and order preserved
138     assertEquals("First sequence id wrong", "U455", seqs.get(0).getName());
139     assertEquals("Second sequence id wrong", "CPZANT", seqs.get(1)
140             .getName());
141     // check sequence data
142     assertEquals("First sequence data wrong", "ABCFEDHIJMNOPQR", seqs
143             .get(0).getSequenceAsString());
144     assertEquals("Second sequence data wrong", "KLMNOPWXYZCGATC",
145             seqs.get(1).getSequenceAsString());
146     assertFalse("File format is not flagged as noninterleaved",
147             testee.isInterleaved());
148   }
149
150   /**
151    * Test parsing an interleaved file with an extra sequence appearing after the
152    * first block - should fail.
153    */
154   @Test(groups = { "Functional" })
155   public void testParse_interleavedExtraSequenceError()
156   {
157     try
158     {
159       new MegaFile(INTERLEAVED_SEQUENCE_ERROR, AppletFormatAdapter.PASTE);
160       fail("Expected extra sequence IOException");
161     } catch (IOException e)
162     {
163       assertEquals(
164               "Unexpected exception message",
165               "Parse error: misplaced new sequence starting at #U456   KLMNOP",
166               e.getMessage());
167     }
168   }
169
170   /**
171    * Test a mixed up file.
172    */
173   @Test(groups = { "Functional" })
174   public void testParse_mixedInterleavedNonInterleaved()
175   {
176     try
177     {
178       new MegaFile(MIXED, AppletFormatAdapter.PASTE);
179       fail("Expected mixed content exception");
180     } catch (IOException e)
181     {
182       assertEquals(
183               "Unexpected exception message",
184               "Parse error: interleaved was true but now seems to be false, at line: ABCFEDHIJ",
185               e.getMessage());
186     }
187
188   }
189
190   @Test(groups = { "Functional" })
191   public void testGetSequenceId()
192   {
193     assertEquals("AB123", MegaFile.getSequenceId("#AB123 CGATC"));
194     assertEquals("AB123", MegaFile.getSequenceId("#AB123    CGATC"));
195     assertEquals("AB123", MegaFile.getSequenceId("#AB123 CGC TAC"));
196     assertEquals("AB123", MegaFile.getSequenceId("#AB123"));
197     assertNull(MegaFile.getSequenceId("AB123 CTAG"));
198     assertNull(MegaFile.getSequenceId("AB123"));
199     assertNull(MegaFile.getSequenceId(""));
200     assertNull(MegaFile.getSequenceId(null));
201   }
202
203   @Test(groups = { "Functional" })
204   public void testGetMaxIdLength()
205   {
206     SequenceI[] seqs = new Sequence[2];
207     seqs[0] = new Sequence("Something", "GCATAC");
208     seqs[1] = new Sequence("SomethingElse", "GCATAC");
209     assertEquals(13, MegaFile.getMaxIdLength(seqs));
210     seqs[1] = new Sequence("DNA", "GCATAC");
211     assertEquals(9, MegaFile.getMaxIdLength(seqs));
212   }
213
214   @Test(groups = { "Functional" })
215   public void testGetMaxSequenceLength()
216   {
217     SequenceI[] seqs = new Sequence[2];
218     seqs[0] = new Sequence("Seq1", "GCATAC");
219     seqs[1] = new Sequence("Seq2", "GCATACTAG");
220     assertEquals(9, MegaFile.getMaxSequenceLength(seqs));
221     seqs[1] = new Sequence("Seq2", "GCA");
222     assertEquals(6, MegaFile.getMaxSequenceLength(seqs));
223   }
224
225   /**
226    * Test (parse and) print of interleaved mega format data.
227    * 
228    * @throws IOException
229    */
230   @Test(groups = { "Functional" })
231   public void testPrint_interleaved() throws IOException
232   {
233     MegaFile testee = new MegaFile(INTERLEAVED, AppletFormatAdapter.PASTE);
234     String printed = testee.print();
235     System.out.println(printed);
236     // normally output should match input
237     // we cheated here with a number of short input lines
238     // nb don't get Title in output if not calling print(AlignmentI)
239     //@formatter:off
240     String expected = 
241         "#MEGA\n\n" + 
242         "#U455   ABCDEF [6]\n" +
243         "#CPZANT MNOPQR [6]\n\n" + 
244         "#U455   KLMNOP [12]\n" + 
245         "#CPZANT WXYZGC [12]\n";
246     //@formatter:on
247     assertEquals("Print format wrong", expected, printed);
248   }
249
250   /**
251    * Test (parse and) print of interleaved data with no headers (acceptable).
252    * 
253    * @throws IOException
254    */
255   @Test(groups = { "Functional" })
256   public void testPrint_interleavedNoHeaders() throws IOException
257   {
258     MegaFile testee = new MegaFile(INTERLEAVED_NOHEADERS,
259             AppletFormatAdapter.PASTE);
260     String printed = testee.print();
261     System.out.println(printed);
262
263     //@formatter:off
264     assertEquals("Print format wrong", 
265     "#MEGA\n\n" + "#U455   ABCDEF [6]\n" 
266     + "#CPZANT MNOPQR [6]\n\n" 
267     + "#U455   KLMNOP [12]\n"
268     + "#CPZANT WXYZGC [12]\n",
269             printed);
270     //@formatter:on
271   }
272
273   /**
274    * Test (parse and) print of noninterleaved mega format data.
275    * 
276    * @throws IOException
277    */
278   @Test(groups = { "Functional" })
279   public void testPrint_noninterleaved() throws IOException
280   {
281     MegaFile testee = new MegaFile(NONINTERLEAVED,
282             AppletFormatAdapter.PASTE);
283     assertEquals(10, testee.getPositionsPerLine());
284     String printed = testee.print();
285     System.out.println(printed);
286     // normally output should match input
287     // we cheated here with a number of short input lines
288     String expected = "#MEGA\n\n" + "#U455\n"
289             + "ABCFEDHIJM [10]\nNOPQR [15]\n\n" + "#CPZANT\n"
290             + "KLMNOPWXYZ [10]\nCGATC [15]\n";
291     assertEquals("Print format wrong", expected, printed);
292   }
293
294   /**
295    * Test (parse and) print of interleaved mega format data extending to more
296    * than one line of output.
297    * 
298    * @throws IOException
299    */
300   @Test(groups = { "Functional" })
301   public void testPrint_interleavedMultiLine() throws IOException
302   {
303     MegaFile testee = new MegaFile(INTERLEAVED_50RESIDUES,
304             AppletFormatAdapter.PASTE);
305     assertEquals(50, testee.getPositionsPerLine());
306     /*
307      * now simulate choosing 20 residues per line on output
308      */
309     testee.setPositionsPerLine(20);
310     String printed = testee.print();
311     System.out.println(printed);
312     //@formatter:off
313     String expected = 
314             "#MEGA\n\n" + 
315             "#U455   0123456789 klmnopqrst [20]\n" + // first 20
316             "#CPZANT 9876543210 abcdefghij [20]\n\n" +
317             "#U455   ABCDEFGHIJ 9876543210 [40]\n" + // next 20
318             "#CPZANT 0123456789 klmnopqrst [40]\n\n" +
319             "#U455   abcdefghij [50]\n" + // last 10
320             "#CPZANT ABCDEFGHIJ [50]\n";
321     //@formatter:on
322     assertEquals("Print format wrong", expected, printed);
323   }
324
325   /**
326    * Test (parse and) print of noninterleaved mega format data extending to more
327    * than one line of output.
328    * 
329    * @throws IOException
330    */
331   @Test(groups = { "Functional" })
332   public void testPrint_noninterleavedMultiLine() throws IOException
333   {
334     final String NONINTERLEAVED_LONGERTHAN50 = "#SIXTY\n" + THIRTY_CHARS
335             + "\n" + TWENTY_CHARS + "9993332221\n";
336     MegaFile testee = new MegaFile(NONINTERLEAVED_LONGERTHAN50,
337             AppletFormatAdapter.PASTE);
338     assertEquals(30, testee.getPositionsPerLine());
339     testee.setPositionsPerLine(25);
340     String printed = testee.print();
341
342     /*
343      * 25 positions per line is rounded down to 20 (two blocks of 10)
344      */
345     String expected = "#MEGA\n\n" + "#SIXTY\n"
346             + "0123456789 klmnopqrst [20]\n"
347             + "ABCDEFGHIJ 9876543210 [40]\n"
348             + "abcdefghij 9993332221 [60]\n";
349     assertEquals("Print format wrong", expected, printed);
350   }
351
352   /**
353    * Test parse of data including description
354    * 
355    * @throws IOException
356    */
357   @Test(groups = { "Functional" })
358   public void testParse_withDescription() throws IOException
359   {
360     MegaFile testee = new MegaFile(INTERLEAVED_WITH_DESCRIPTION,
361             AppletFormatAdapter.PASTE);
362     assertEquals("Title not as expected", "Data with description",
363             testee.getAlignmentProperty(MegaFile.PROP_TITLE));
364
365     Vector<SequenceI> seqs = testee.getSeqs();
366     // should be 2 sequences
367     assertEquals("Expected two sequences", 2, seqs.size());
368     // check sequence names correct and order preserved
369     assertEquals("First sequence id wrong", "U455", seqs.get(0).getName());
370     assertEquals("Second sequence id wrong", "CPZANT", seqs.get(1)
371             .getName());
372     // check sequence data
373     assertEquals("First sequence data wrong", "C--GTACGA--T", seqs.get(0)
374             .getSequenceAsString());
375     assertEquals("Second sequence data wrong", "ATC-G-CA--GC", seqs.get(1)
376             .getSequenceAsString());
377     assertTrue("File format is not flagged as interleaved",
378             testee.isInterleaved());
379
380     assertEquals(
381             "Description property not parsed",
382             "    Line one of description\n" + "    Line two of description",
383             testee.getAlignmentProperty(MegaFile.PROP_DESCRIPTION));
384   }
385
386   @Test(groups = { "Functional" })
387   public void testGetNonCommentContent() throws FileFormatException
388   {
389     assertEquals("abcde", MegaFile.getNonCommentContent("abcde", 0));
390     assertEquals("CGT ACG GAC ",
391             MegaFile.getNonCommentContent("CGT ACG GAC [9]", 0));
392     assertEquals("", MegaFile.getNonCommentContent("abcde", 1));
393     assertEquals(" abcde",
394             MegaFile.getNonCommentContent("and others ] abcde", 1));
395     assertEquals(" abcde", MegaFile.getNonCommentContent(
396             "and others [including refs] ] abcde", 1));
397     assertEquals(" x ] abcde",
398             MegaFile.getNonCommentContent("and others ] x ] abcde", 1));
399   }
400
401   @Test(groups = { "Functional" })
402   public void testCommentDepth() throws FileFormatException
403   {
404     assertEquals(0, MegaFile.commentDepth("abcde", 0));
405     assertEquals(1, MegaFile.commentDepth("abc[de", 0));
406     assertEquals(3, MegaFile.commentDepth("ab[c[de", 1));
407     assertEquals(1, MegaFile.commentDepth("ab]c[d]e[f", 1));
408     assertEquals(0, MegaFile.commentDepth("a]b[c]d]e", 1));
409   }
410
411   @Test(groups = { "Functional" })
412   public void testGetValue()
413   {
414     assertEquals("Mega", MegaFile.getValue("Name=Mega"));
415     assertEquals("Mega", MegaFile.getValue("Name =Mega"));
416     assertEquals("Mega", MegaFile.getValue(" Name = Mega "));
417     assertEquals("Mega", MegaFile.getValue("Name = Mega; "));
418     assertEquals("Mega", MegaFile.getValue(" Name = Mega ; "));
419     assertEquals("Mega", MegaFile.getValue("\t!Name \t= \tMega ; "));
420     assertEquals("Mega", MegaFile.getValue("!Name \t\t Mega; "));
421     assertEquals("", MegaFile.getValue("Name"));
422   }
423
424   /**
425    * Test reading a MEGA file to an alignment then writing it out in MEGA
426    * format. Verify the output is (functionally) the same as the input.
427    * 
428    * @throws IOException
429    */
430   @Test(groups = "Functional")
431   public void testRoundTrip_Interleaved() throws IOException
432   {
433     AppletFormatAdapter fa = new AppletFormatAdapter();
434     AlignmentI al = fa.readFile(INTERLEAVED_WITH_DESCRIPTION,
435             AppletFormatAdapter.PASTE, "MEGA");
436     MegaFile output = new MegaFile();
437     String formatted = output.print(al);
438     //@formatter:off
439     String expected = 
440          "#MEGA\n!Title Data with description;\n" +
441          "!Description\n" +
442          "    Line one of description\n" +
443          "    Line two of description;\n" +
444          "!Format\n" +
445          "    DataType=DNA CodeTable=Standard\n" +
446          "    NSeqs=2 NSites=12\n" + // NSites includes gaps
447          "    Indel=- Identical=. Missing=?;\n\n" +
448          "#U455   C-- GTA [6]\n" +
449          "#CPZANT ATC -G- [6]\n\n" +
450          "#U455   CGA --T [12]\n" +
451          "#CPZANT CA- -GC [12]\n";
452     //@formatter:on
453     assertEquals("Roundtrip didn't match", expected,
454             formatted);
455   }
456
457   /**
458    * Test reading a MEGA file to an alignment then writing it out in MEGA
459    * format. Verify the output is (functionally) the same as the input.
460    * 
461    * @throws IOException
462    */
463   @Test(groups = "Functional")
464   public void testRoundTrip_multilineFormatWithComments()
465           throws IOException
466   {
467     AppletFormatAdapter fa = new AppletFormatAdapter();
468     //@formatter:off
469     AlignmentI al = fa.readFile("#MEGA\n"
470     + "!Title Data with description;\n"
471     + "[ this comment should be ignored\n"
472     + "including [this nested comment]\n"
473     + "]\n"
474     + "!Format \n"
475     + "DataType=DNA CodeTable=Standard\n"
476     + "indel=- Missing=? MatchChar=.;\n\n"
477     + "!Description\n" 
478     + "    Line one of description\n"
479     + "    Line two of description;\n\n"
480     + "#U455   CGC GTA\n" 
481     + "#CPZANT ATC GGG\n\n"
482     + "#U455   CGA TTT\n" 
483     + "#CPZANT CAA TGC\n",
484             AppletFormatAdapter.PASTE, "MEGA");
485     //@formatter:on
486     MegaFile output = new MegaFile();
487     String formatted = output.print(al);
488     //@formatter:off
489     String expected = 
490          "#MEGA\n!Title Data with description;\n" +
491          "!Description\n" +
492          "    Line one of description\n" +
493          "    Line two of description;\n" +
494          "!Format\n" +
495          "    DataType=DNA CodeTable=Standard\n" +
496          "    NSeqs=2 NSites=12\n" +
497          "    Indel=- Identical=. Missing=?;\n\n" +
498          "#U455   CGC GTA [6]\n" +
499          "#CPZANT ATC GGG [6]\n\n" +
500          "#U455   CGA TTT [12]\n" +
501          "#CPZANT CAA TGC [12]\n";
502     //@formatter:on
503     assertEquals("Roundtrip didn't match", expected,
504             formatted);
505   }
506
507   //@formatter:on
508   
509   /**
510    * Test parse of interleaved mega format data where the identity character is
511    * used in sequences after the first
512    * 
513    * @throws IOException
514    */
515   @Test(groups = { "Functional" })
516   public void testParse_interleavedWithIdentityAndTabs() throws IOException
517   {
518     //@formatter:off
519     // uses tab instead of space separators to check robustness
520     MegaFile testee = new MegaFile("#MEGA\n"+ 
521     "!TITLE\tInterleaved sequence data;\n" +
522     "!Format\tIdentical=.;\n\n" +
523     "#U455\tABCDEF\n" + 
524     "#CPZANT\tM..P.R\n\n" + 
525     "#U455\t\tKLMNOP\n" +
526     "#CPZANT\t..YZ..", AppletFormatAdapter.PASTE);
527     //@formatter:on
528     assertEquals("Title not as expected", "Interleaved sequence data",
529             testee.getAlignmentProperty(MegaFile.PROP_TITLE));
530     Vector<SequenceI> seqs = testee.getSeqs();
531     // should be 2 sequences
532     assertEquals("Expected two sequences", 2, seqs.size());
533     // check sequence names correct and order preserved
534     assertEquals("First sequence id wrong", "U455", seqs.get(0).getName());
535     assertEquals("Second sequence id wrong", "CPZANT", seqs.get(1)
536             .getName());
537     // check sequence data
538     assertEquals("First sequence data wrong", "ABCDEFKLMNOP", seqs.get(0)
539             .getSequenceAsString());
540     assertEquals("Second sequence data wrong", "MBCPERKLYZOP", seqs.get(1)
541             .getSequenceAsString());
542     assertTrue("File format is not flagged as interleaved",
543             testee.isInterleaved());
544   }
545
546   /**
547    * Test parse of noninterleaved format data including identity symbol
548    * 
549    * @throws IOException
550    */
551   @Test(groups = { "Functional" })
552   public void testParse_nonInterleavedWithIdentity() throws IOException
553   {
554     //@formatter:off
555     MegaFile testee = new MegaFile("#MEGA\n"
556     + "!TITLE Noninterleaved sequence data;\n"
557     + "!Format MatchChar=.;\n"
558     + "#U455  \n"
559     + "ABCFEDHIJ\n" 
560     + "MNOPQR\n\n" 
561     + "#CPZANT \n" 
562     + "KL..O..XYZ\n" 
563     + "CG..C\n",
564             AppletFormatAdapter.PASTE);
565     //@formatter:on
566     assertEquals("Title not as expected", "Noninterleaved sequence data",
567             testee.getAlignmentProperty(MegaFile.PROP_TITLE));
568     Vector<SequenceI> seqs = testee.getSeqs();
569     // should be 2 sequences
570     assertEquals("Expected two sequences", 2, seqs.size());
571     // check sequence names correct and order preserved
572     assertEquals("First sequence id wrong", "U455", seqs.get(0).getName());
573     assertEquals("Second sequence id wrong", "CPZANT", seqs.get(1)
574             .getName());
575     // check sequence data
576     assertEquals("First sequence data wrong", "ABCFEDHIJMNOPQR", seqs
577             .get(0).getSequenceAsString());
578     assertEquals("Second sequence data wrong", "KLCFODHXYZCGPQC",
579             seqs.get(1).getSequenceAsString());
580     assertFalse("File format is not flagged as noninterleaved",
581             testee.isInterleaved());
582   }
583
584   //@formatter:on
585   
586   /**
587    * Test parse of interleaved format data including position number comments.
588    * 
589    * @throws IOException
590    */
591   @Test(groups = { "Functional" })
592   public void testParse_interleavedWithPositionNumber() throws IOException
593   {
594     //@formatter:off
595     MegaFile testee = new MegaFile("#MEGA\n"+ 
596     "TITLE: Interleaved sequence data\n\n" + 
597     "#U455   ABCDEF [6]\n" + 
598     "#CPZANT  MNOPQR [6]\n\n" + 
599     "#U455   KLMNOP [12]\n" + 
600     "#CPZANT WXYZGC [12]\n", AppletFormatAdapter.PASTE);
601     //@formatter:on
602     assertEquals("Title not as expected", "Interleaved sequence data",
603             testee.getAlignmentProperty(MegaFile.PROP_TITLE));
604     Vector<SequenceI> seqs = testee.getSeqs();
605     // should be 2 sequences
606     assertEquals("Expected two sequences", 2, seqs.size());
607     // check sequence names correct and order preserved
608     assertEquals("First sequence id wrong", "U455", seqs.get(0).getName());
609     assertEquals("Second sequence id wrong", "CPZANT", seqs.get(1)
610             .getName());
611     // check sequence data
612     assertEquals("First sequence data wrong", "ABCDEFKLMNOP", seqs.get(0)
613             .getSequenceAsString());
614     assertEquals("Second sequence data wrong", "MNOPQRWXYZGC", seqs.get(1)
615             .getSequenceAsString());
616     assertTrue("File format is not flagged as interleaved",
617             testee.isInterleaved());
618   }
619
620   //@formatter:on
621   
622   /**
623    * Test parse of data with !Gene and !Domain statements.
624    * 
625    * @throws IOException
626    */
627   @Test(groups = { "Functional" })
628   public void testParse_geneDomains() throws IOException
629   {
630     //@formatter:off
631     String data = "#MEGA\n"+ 
632     "TITLE: Interleaved sequence data\n\n" + 
633     "#U455   CCCCCC\n" + 
634     "#CPZANT  TTTTTT\n\n" +
635     "!Domain=Exon1 Gene=Adh Property=Coding CodonStart=1;\n" +
636     "#U455   GGGGGG\n" + 
637     "#CPZANT AAAAAA\n\n" +
638     "!domain=Intron1 Property=Intron Gene=Adh;\n" +
639     "#U455   tttttt\n" + 
640     "#CPZANT cccccc\n\n" +
641     "!Domain=Exon2 Gene=Adh Property=Exon CodonStart=1;\n" +
642     "#U455   aaaaaa\n" + 
643     "#CPZANT gggggg\n\n" +
644     // explicit end of Exon2, implicit end of Adh:
645     "!Domain=Exon2 Property=domainend;\n" +
646     "!Domain=Intron1 Gene=Opsin Property=Noncoding;\n" +
647     "#U455   GGGGGG\n" + 
648     "#CPZANT AAAAAA\n\n" +
649     // end Opsin, start MEF2A
650     "!Domain=Exon1 Gene=MEF2A Property=Coding CodonStart=1;\n" +
651     "#U455   tttttt\n" + 
652     "#CPZANT cccccc\n\n" +
653     // end MEF2A
654     "!Domain=BindingSite;\n" +
655     "#U455   CCCCCC\n" + 
656     "#CPZANT TTTTTT\n\n";
657     //@formatter:on
658     MegaFile testee = new MegaFile(data, AppletFormatAdapter.PASTE);
659
660     Vector<SequenceI> seqs = testee.getSeqs();
661     // should be 2 sequences
662     assertEquals("Expected two sequences", 2, seqs.size());
663     // check sequence data
664     assertEquals("First sequence data wrong",
665             "CCCCCCGGGGGGttttttaaaaaaGGGGGGttttttCCCCCC", seqs.get(0)
666             .getSequenceAsString());
667     assertEquals("Second sequence data wrong",
668             "TTTTTTAAAAAAccccccggggggAAAAAAccccccTTTTTT", seqs.get(1)
669             .getSequenceAsString());
670
671     /*
672      * sequences should have features for Gene=Adh 7-24, Exon1 7-12, Intron1
673      * 13-18, Exon2 19-24, BindingSite 25-30
674      */
675     for (SequenceI seq : seqs) {
676       SequenceFeature[] sfs = seq.getSequenceFeatures();
677       // features are added in the order in which their end is found
678       // (Domain before Gene when they end together)
679       assertEquals(9, sfs.length);
680       // TODO settle which way round type/description go!
681       verifySequenceFeature(sfs[0], "Exon1 (Adh Coding)", "Domain", 7, 12);
682       verifySequenceFeature(sfs[1], "Intron1 (Adh Noncoding)", "Domain",
683               13, 18);
684       verifySequenceFeature(sfs[2], "Exon2 (Adh Coding)", "Domain", 19, 24);
685       verifySequenceFeature(sfs[3], "Adh", "Gene", 7, 24);
686       verifySequenceFeature(sfs[4], "Intron1 (Opsin Noncoding)", "Domain",
687               25, 30);
688       verifySequenceFeature(sfs[5], "Opsin", "Gene", 25, 30);
689       verifySequenceFeature(sfs[6], "Exon1 (MEF2A Coding)", "Domain", 31,
690               36);
691       verifySequenceFeature(sfs[7], "MEF2A", "Gene", 31, 36);
692       verifySequenceFeature(sfs[8], "BindingSite", "Domain", 37, 42);
693     }
694
695     /*
696      * verify gene and domain alignment annotations
697      */
698     assertEquals(2, testee.annotations.size());
699     AlignmentAnnotation ann = testee.annotations.get(0);
700     assertEquals("MEGA Gene", ann.label);
701     assertEquals(42, ann.annotations.length);
702     verifyAnnotation(ann, 0, 6, null);
703     verifyAnnotation(ann, 6, 24, "Adh");
704     verifyAnnotation(ann, 24, 30, "Opsin");
705     verifyAnnotation(ann, 30, 36, "MEF2A");
706     verifyAnnotation(ann, 37, 42, null);
707
708     ann = testee.annotations.get(1);
709     assertEquals("MEGA Domain", ann.label);
710     assertEquals(42, ann.annotations.length);
711     verifyAnnotation(ann, 0, 6, null);
712     verifyAnnotation(ann, 6, 12, "Exon1 (Adh Coding)");
713     verifyAnnotation(ann, 12, 18, "Intron1 (Adh Noncoding)");
714     verifyAnnotation(ann, 19, 24, "Exon2 (Adh Coding)");
715     verifyAnnotation(ann, 25, 30, "Intron1 (Opsin Noncoding)");
716     verifyAnnotation(ann, 31, 36, "Exon1 (MEF2A Coding)");
717     verifyAnnotation(ann, 37, 42, "BindingSite");
718
719   }
720
721   /**
722    * Helper method to verify a range of annotation positions all have the given
723    * description
724    * 
725    * @param ann
726    *          array of annotations to check
727    * @param from
728    *          start index to check
729    * @param to
730    *          end index to check (exclusive)
731    * @param description
732    *          value to assert
733    */
734   protected void verifyAnnotation(AlignmentAnnotation ann, int from,
735           int to, String description)
736   {
737     for (int pos = from; pos < to; pos++)
738     {
739       if (description == null)
740       {
741         assertNull(ann.annotations[pos]);
742       }
743       else
744       {
745         assertEquals(description, ann.annotations[pos].description);
746       }
747     }
748   }
749
750   /**
751    * Helper method to assert properties of a SequenceFeature
752    * 
753    * @param sf
754    * @param type
755    * @param description
756    * @param begin
757    * @param end
758    */
759   protected void verifySequenceFeature(SequenceFeature sf, String type,
760           String description, int begin, int end)
761   {
762     assertEquals(description, sf.type);
763     assertEquals(type, sf.description);
764     assertEquals(begin, sf.begin);
765     assertEquals(end, sf.end);
766   }
767   
768   /**
769    * Test parse of data including !Label statements. An underscore means no
770    * label, other characters are treated as alignment annotation.
771    * 
772    * @throws IOException
773    */
774   @Test(groups = { "Functional" })
775   public void testParse_withLabels() throws IOException
776   {
777     //@formatter:off
778     MegaFile testee = new MegaFile("#MEGA\n"+ 
779     "TITLE: Interleaved sequence data\n\n" + 
780     "#U455   ABC DEF\n" + 
781     "#CPZANT MNO PQR\n" +
782     "!Label  +-_ 23_\n\n" +
783     // a row with no labels = null annotation
784     "#U455   abc def\n" + 
785     "#CPZANT mno pqr\n\n" +
786     "#U455   KLM NOP\n" + 
787     "#CPZANT WXY ZGC\n" +
788     "!label  __3 +X_\n", AppletFormatAdapter.PASTE);
789     //@formatter:on
790     Vector<SequenceI> seqs = testee.getSeqs();
791     assertEquals("Expected two sequences", 2, seqs.size());
792     assertEquals("First sequence data wrong", "ABCDEFabcdefKLMNOP", seqs
793             .get(0)
794             .getSequenceAsString());
795     assertEquals("Second sequence data wrong", "MNOPQRmnopqrWXYZGC", seqs
796             .get(1)
797             .getSequenceAsString());
798
799     // check AlignmentAnnotation added with expected values
800     assertEquals(1, testee.annotations.size());
801     AlignmentAnnotation aa = testee.annotations.get(0);
802     assertNull(aa.sequenceRef);
803     assertEquals("MEGA Label", aa.label);
804     assertEquals(18, aa.annotations.length);
805     assertEquals("+, -, , 2, 3, , , , , , , , , , 3, +, X, , ",
806             aa.toString());
807   }
808
809   //@formatter:on
810   
811   /**
812    * Test case where a domain is implicitly terminated by starting a new gene
813    * 
814    * @throws IOException
815    */
816   @Test(groups = { "Functional" })
817   public void testParse_changeOfGeneEndsDomain() throws IOException
818   {
819     //@formatter:off
820     // uses tab instead of space separators to check robustness
821     MegaFile testee = new MegaFile("#MEGA\n"+ 
822     "!TITLE Interleaved sequence data;\n" +
823     "!Format Identical=.;\n\n" +
824     "!Gene=gene1 Domain=Exon1 Property=Coding;\n" +
825     "#U455 ABCDEF\n" + 
826     "#CPZANT M..P.R\n\n" + 
827     "!Gene=gene2;\n" +
828     "#U455 KLMNOP\n" +
829     "#CPZANT ..YZ..", AppletFormatAdapter.PASTE);
830     //@formatter:on
831     Vector<SequenceI> seqs = testee.getSeqs();
832     assertEquals("Expected two sequences", 2, seqs.size());
833     assertEquals("First sequence data wrong", "ABCDEFKLMNOP", seqs.get(0)
834             .getSequenceAsString());
835     assertEquals("Second sequence data wrong", "MBCPERKLYZOP", seqs.get(1)
836             .getSequenceAsString());
837     assertTrue("File format is not flagged as interleaved",
838             testee.isInterleaved());
839
840     for (SequenceI seq : seqs)
841     {
842       SequenceFeature[] sfs = seq.getSequenceFeatures();
843       assertEquals(3, sfs.length);
844       verifySequenceFeature(sfs[0], "Exon1 (gene1 Coding)", "Domain", 1, 6);
845       verifySequenceFeature(sfs[1], "gene1", "Gene", 1, 6);
846       verifySequenceFeature(sfs[2], "gene2", "Gene", 7, 12);
847     }
848   }
849
850   //@formatter:on
851   
852   /**
853    * Test case where the declared gap character is one Jalview does not support;
854    * it should be converted to a '-'
855    * 
856    * @throws IOException
857    */
858   @Test(groups = { "Functional" })
859   public void testParse_weirdGapCharacter() throws IOException
860   {
861     //@formatter:off
862     String data = "#MEGA\n"+ 
863     "!TITLE Interleaved sequence data;\n" +
864     "!Format Identical=. Indel=%;\n\n" +
865     "#U455 %BC%EF\n" + 
866     "#CPZANT M..P.R\n\n" + 
867     "#U455 KLMNOP\n" +
868     "#CPZANT .%%Z..";
869     AppletFormatAdapter fa = new AppletFormatAdapter();
870     AlignmentI al = fa.readFile(data,
871             AppletFormatAdapter.PASTE, "MEGA");
872     //@formatter:on
873     List<SequenceI> seqs = al.getSequences();
874     assertEquals("First sequence data wrong", "-BC-EFKLMNOP", seqs.get(0)
875             .getSequenceAsString());
876     assertEquals("Second sequence data wrong", "MBCPERK--ZOP", seqs.get(1)
877             .getSequenceAsString());
878     assertEquals('-', al.getGapCharacter());
879   }
880
881   /**
882    * Test reading a MEGA file to an alignment then writing it out in MEGA
883    * format. Includes !Label statements which should be converted to
884    * AlignmentAnnotation and back again.
885    * 
886    * @throws IOException
887    */
888   @Test(groups = "Functional")
889   public void testRoundTrip_withLabels() throws IOException
890   {
891     AppletFormatAdapter fa = new AppletFormatAdapter();
892
893     //@formatter:off
894     String data = "#MEGA\n"
895     + "#U455   C-- GTA\n" 
896     + "#CPZANT ATC -G-\n"
897     + "!Label F__E_H\n\n"
898     + "#U455   CGA --T\n" 
899     + "#CPZANT CA- -GC\n"
900     + "!Label FFH__E\n";
901     AlignmentI al = fa.readFile(data,
902             AppletFormatAdapter.PASTE, "MEGA");
903     AlignmentAnnotation aa = al.getAlignmentAnnotation()[0];
904     assertEquals("MEGA Label", aa.label);
905     assertEquals("F, , , E, , H, F, F, H, , , E, ",
906             aa.toString());
907
908     MegaFile output = new MegaFile();
909     String formatted = output.print(al);
910     String expected = 
911         "#MEGA\n" +
912         "!Format\n" +
913         "    DataType=Nucleotide CodeTable=Standard\n" +
914         "    NSeqs=2 NSites=12\n" +
915         "    Indel=-;\n\n" +
916         "#U455   C-- GTA [6]\n" +
917         "#CPZANT ATC -G- [6]\n" +
918         "!Label F__ E_H;\n\n" +  
919         "#U455   CGA --T [12]\n" +
920         "#CPZANT CA- -GC [12]\n" +
921         "!Label FFH __E;\n";
922     //@formatter:on
923     assertEquals("Roundtrip didn't match", expected,
924             formatted);
925   }
926
927   /**
928    * Test (parse and) print of MEGA data with !Gene statements.
929    * 
930    * @throws IOException
931    */
932   @Test(groups = { "Functional" })
933   public void testPrint_genes() throws IOException
934   {
935     /*
936      * to keep the test concise, input data is in the exact format that Jalview
937      * would output it; the important thing is functional equivalence of input
938      * and output
939      */
940     //@formatter:off
941     String data = "#MEGA\n\n"+ 
942       "#Seq1 ABCD [4]\n" + 
943       "#Seq2 MNOP [4]\n\n" + 
944       "!Domain=Exon1 Gene=Adh Property=Coding;\n" +
945       "#Seq1 EFGHI [9]\n" + 
946       "#Seq2 QRSTU [9]\n\n" +
947       "!Domain=Intron1 Gene=Adh Property=Noncoding;\n" +
948       "#Seq1 JK [11]\n" + 
949       "#Seq2 VW [11]\n\n" +
950       "!Domain=Intron1 Property=domainend;\n" +
951       "#Seq1 LMN [14]\n" + 
952       "#Seq2 XYZ [14]\n";
953     //@formatter:on
954     MegaFile testee = new MegaFile(data, AppletFormatAdapter.PASTE);
955     String printed = testee.print();
956     assertEquals("Print format wrong", data, printed);
957   }
958
959   @Test(groups = { "Functional" })
960   public void testGetDomainFromAnnotation()
961   {
962     Annotation[] anns = new Annotation[5];
963     anns[1] = new Annotation("", "Intron1", '0', 0f);
964     anns[2] = new Annotation("", "Intron2 (Aspx)", '0', 0f);
965     anns[3] = new Annotation("", "Intron3 (Aspy Coding)", '0', 0f);
966     anns[4] = new Annotation("", "Intron4 (Coding)", '0', 0f);
967     AlignmentAnnotation aa = new AlignmentAnnotation("", "", anns);
968     // no annotations:
969     assertNull(MegaFile.getDomainFromAnnotation(0, null));
970     // null annotation:
971     assertNull(MegaFile.getDomainFromAnnotation(0, aa));
972     // column out of range:
973     assertNull(MegaFile.getDomainFromAnnotation(5, aa));
974     // domain with no Gene or Property:
975     assertEquals("Intron1", MegaFile.getDomainFromAnnotation(1, aa));
976     // domain with Gene but no Property:
977     assertEquals("Intron2", MegaFile.getDomainFromAnnotation(2, aa));
978     // domain with Gene and Property:
979     assertEquals("Intron3", MegaFile.getDomainFromAnnotation(3, aa));
980     // domain with Property and no Gene:
981     assertEquals("Intron4", MegaFile.getDomainFromAnnotation(4, aa));
982   }
983
984   @Test(groups = { "Functional" })
985   public void testGetGeneFromAnnotation()
986   {
987     Annotation[] anns = new Annotation[3];
988     anns[1] = new Annotation("", "Aspx", '0', 0f);
989     AlignmentAnnotation aa = new AlignmentAnnotation("", "", anns);
990     // no annotations:
991     assertNull(MegaFile.getGeneFromAnnotation(0, null));
992     // null annotation:
993     assertNull(MegaFile.getGeneFromAnnotation(0, aa));
994     // column out of range:
995     assertNull(MegaFile.getGeneFromAnnotation(3, aa));
996     // gene annotation:
997     assertEquals("Aspx", MegaFile.getGeneFromAnnotation(1, aa));
998   }
999
1000   @Test(groups = { "Functional" })
1001   public void testGetPropertyFromAnnotation()
1002   {
1003     Annotation[] anns = new Annotation[5];
1004     anns[1] = new Annotation("", "Intron1", '0', 0f);
1005     anns[2] = new Annotation("", "Intron2 (Aspx)", '0', 0f);
1006     anns[3] = new Annotation("", "Intron3 (Aspy Noncoding)", '0', 0f);
1007     anns[4] = new Annotation("", "Exon1 (Aspx Coding)", '0', 0f);
1008     AlignmentAnnotation aa = new AlignmentAnnotation("", "", anns);
1009     // no annotations:
1010     assertNull(MegaFile.getPropertyFromAnnotation(0, null));
1011     // null annotation:
1012     assertNull(MegaFile.getPropertyFromAnnotation(0, aa));
1013     // column out of range:
1014     assertNull(MegaFile.getPropertyFromAnnotation(5, aa));
1015     // domain with no Gene or Property:
1016     assertNull(MegaFile.getPropertyFromAnnotation(1, aa));
1017     // domain with Gene but no Property:
1018     assertNull(MegaFile.getPropertyFromAnnotation(2, aa));
1019     // domain with Gene and Property:
1020     assertEquals("Noncoding", MegaFile.getPropertyFromAnnotation(3, aa));
1021     assertEquals("Coding", MegaFile.getPropertyFromAnnotation(4, aa));
1022   }
1023
1024   //@formatter:on
1025   
1026   /**
1027    * Test parse of interleaved data with no blank lines to separate blocks of
1028    * sequence data; to confirm we can handle this correctly
1029    * 
1030    * @throws IOException
1031    */
1032   @Test(groups = { "Functional" })
1033   public void testParse_interleaved_noBlankLines() throws IOException
1034   {
1035     String data = INTERLEAVED.replace("\n\n", "\n");
1036     MegaFile testee = new MegaFile(data, AppletFormatAdapter.PASTE);
1037     assertEquals("Title not as expected", "Interleaved sequence data",
1038             testee.getAlignmentProperty(MegaFile.PROP_TITLE));
1039     Vector<SequenceI> seqs = testee.getSeqs();
1040     // should be 2 sequences
1041     assertEquals("Expected two sequences", 2, seqs.size());
1042     // check sequence names correct and order preserved
1043     assertEquals("First sequence id wrong", "U455", seqs.get(0).getName());
1044     assertEquals("Second sequence id wrong", "CPZANT", seqs.get(1)
1045             .getName());
1046     // check sequence data
1047     assertEquals("First sequence data wrong", "ABCDEFKLMNOP", seqs.get(0)
1048             .getSequenceAsString());
1049     assertEquals("Second sequence data wrong", "MNOPQRWXYZGC", seqs.get(1)
1050             .getSequenceAsString());
1051     assertTrue("File format is not flagged as interleaved",
1052             testee.isInterleaved());
1053   }
1054 }