JAL-3438 spotless for 2.11.2.0
[jalview.git] / test / jalview / commands / EditCommandTest.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.commands;
22
23 import java.util.Locale;
24
25 import static org.testng.AssertJUnit.assertEquals;
26 import static org.testng.AssertJUnit.assertSame;
27 import static org.testng.AssertJUnit.assertTrue;
28
29 import jalview.commands.EditCommand.Action;
30 import jalview.commands.EditCommand.Edit;
31 import jalview.datamodel.Alignment;
32 import jalview.datamodel.AlignmentI;
33 import jalview.datamodel.Sequence;
34 import jalview.datamodel.SequenceFeature;
35 import jalview.datamodel.SequenceI;
36 import jalview.datamodel.features.SequenceFeatures;
37 import jalview.gui.JvOptionPane;
38
39 import java.util.Collections;
40 import java.util.Comparator;
41 import java.util.List;
42 import java.util.Map;
43
44 import org.testng.Assert;
45 import org.testng.annotations.BeforeClass;
46 import org.testng.annotations.BeforeMethod;
47 import org.testng.annotations.Test;
48
49 /**
50  * Unit tests for EditCommand
51  * 
52  * @author gmcarstairs
53  *
54  */
55 public class EditCommandTest
56 {
57   private static Comparator<SequenceFeature> BY_DESCRIPTION = new Comparator<SequenceFeature>()
58   {
59
60     @Override
61     public int compare(SequenceFeature o1, SequenceFeature o2)
62     {
63       return o1.getDescription().compareTo(o2.getDescription());
64     }
65   };
66
67   private EditCommand testee;
68
69   private SequenceI[] seqs;
70
71   private Alignment al;
72
73   /*
74    * compute n(n+1)/2 e.g. 
75    * func(5) = 5 + 4 + 3 + 2 + 1 = 15
76    */
77   private static int func(int i)
78   {
79     return i * (i + 1) / 2;
80   }
81
82   @BeforeClass(alwaysRun = true)
83   public void setUpJvOptionPane()
84   {
85     JvOptionPane.setInteractiveMode(false);
86     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
87   }
88
89   @BeforeMethod(alwaysRun = true)
90   public void setUp()
91   {
92     testee = new EditCommand();
93     seqs = new SequenceI[4];
94     seqs[0] = new Sequence("seq0", "abcdefghjk");
95     seqs[0].setDatasetSequence(new Sequence("seq0ds", "ABCDEFGHJK"));
96     seqs[1] = new Sequence("seq1", "fghjklmnopq");
97     seqs[1].setDatasetSequence(new Sequence("seq1ds", "FGHJKLMNOPQ"));
98     seqs[2] = new Sequence("seq2", "qrstuvwxyz");
99     seqs[2].setDatasetSequence(new Sequence("seq2ds", "QRSTUVWXYZ"));
100     seqs[3] = new Sequence("seq3", "1234567890");
101     seqs[3].setDatasetSequence(new Sequence("seq3ds", "1234567890"));
102     al = new Alignment(seqs);
103     al.setGapCharacter('?');
104   }
105
106   /**
107    * Test inserting gap characters
108    */
109   @Test(groups = { "Functional" })
110   public void testAppendEdit_insertGap()
111   {
112     // set a non-standard gap character to prove it is actually used
113     testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true);
114     assertEquals("abcd???efghjk", seqs[0].getSequenceAsString());
115     assertEquals("fghj???klmnopq", seqs[1].getSequenceAsString());
116     assertEquals("qrst???uvwxyz", seqs[2].getSequenceAsString());
117     assertEquals("1234???567890", seqs[3].getSequenceAsString());
118
119     // todo: test for handling out of range positions?
120   }
121
122   /**
123    * Test deleting characters from sequences. Note the deleteGap() action does
124    * not check that only gap characters are being removed.
125    */
126   @Test(groups = { "Functional" })
127   public void testAppendEdit_deleteGap()
128   {
129     testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true);
130     assertEquals("abcdhjk", seqs[0].getSequenceAsString());
131     assertEquals("fghjnopq", seqs[1].getSequenceAsString());
132     assertEquals("qrstxyz", seqs[2].getSequenceAsString());
133     assertEquals("1234890", seqs[3].getSequenceAsString());
134   }
135
136   /**
137    * Test a cut action. The command should store the cut characters to support
138    * undo.
139    */
140   @Test(groups = { "Functional" })
141   public void testCut()
142   {
143     Edit ec = testee.new Edit(Action.CUT, seqs, 4, 3, al);
144     EditCommand.cut(ec, new AlignmentI[] { al });
145     assertEquals("abcdhjk", seqs[0].getSequenceAsString());
146     assertEquals("fghjnopq", seqs[1].getSequenceAsString());
147     assertEquals("qrstxyz", seqs[2].getSequenceAsString());
148     assertEquals("1234890", seqs[3].getSequenceAsString());
149
150     assertEquals("efg", new String(ec.string[0]));
151     assertEquals("klm", new String(ec.string[1]));
152     assertEquals("uvw", new String(ec.string[2]));
153     assertEquals("567", new String(ec.string[3]));
154     // TODO: case where whole sequence is deleted as nothing left; etc
155   }
156
157   /**
158    * Test a Paste action, followed by Undo and Redo
159    */
160   @Test(groups = { "Functional" }, enabled = false)
161   public void testPaste_undo_redo()
162   {
163     // TODO code this test properly, bearing in mind that:
164     // Paste action requires something on the clipboard (Cut/Copy)
165     // - EditCommand.paste doesn't add sequences to the alignment
166     // ... that is done in AlignFrame.paste()
167     // ... unless as a Redo
168     // ...
169
170     SequenceI[] newSeqs = new SequenceI[2];
171     newSeqs[0] = new Sequence("newseq0", "ACEFKL");
172     newSeqs[1] = new Sequence("newseq1", "JWMPDH");
173
174     new EditCommand("Paste", Action.PASTE, newSeqs, 0, al.getWidth(), al);
175     assertEquals(6, al.getSequences().size());
176     assertEquals("1234567890", seqs[3].getSequenceAsString());
177     assertEquals("ACEFKL", seqs[4].getSequenceAsString());
178     assertEquals("JWMPDH", seqs[5].getSequenceAsString());
179   }
180
181   /**
182    * Test insertGap followed by undo command
183    */
184   @Test(groups = { "Functional" })
185   public void testUndo_insertGap()
186   {
187     // Edit ec = testee.new Edit(Action.INSERT_GAP, seqs, 4, 3, '?');
188     testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true);
189     // check something changed
190     assertEquals("abcd???efghjk", seqs[0].getSequenceAsString());
191     testee.undoCommand(new AlignmentI[] { al });
192     assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
193     assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
194     assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
195     assertEquals("1234567890", seqs[3].getSequenceAsString());
196   }
197
198   /**
199    * Test deleteGap followed by undo command
200    */
201   @Test(groups = { "Functional" })
202   public void testUndo_deleteGap()
203   {
204     testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true);
205     // check something changed
206     assertEquals("abcdhjk", seqs[0].getSequenceAsString());
207     testee.undoCommand(new AlignmentI[] { al });
208     // deleteGap doesn't 'remember' deleted characters, only gaps get put back
209     assertEquals("abcd???hjk", seqs[0].getSequenceAsString());
210     assertEquals("fghj???nopq", seqs[1].getSequenceAsString());
211     assertEquals("qrst???xyz", seqs[2].getSequenceAsString());
212     assertEquals("1234???890", seqs[3].getSequenceAsString());
213   }
214
215   /**
216    * Test several commands followed by an undo command
217    */
218   @Test(groups = { "Functional" })
219   public void testUndo_multipleCommands()
220   {
221     // delete positions 3/4/5 (counting from 1)
222     testee.appendEdit(Action.DELETE_GAP, seqs, 2, 3, al, true);
223     assertEquals("abfghjk", seqs[0].getSequenceAsString());
224     assertEquals("1267890", seqs[3].getSequenceAsString());
225
226     // insert 2 gaps after the second residue
227     testee.appendEdit(Action.INSERT_GAP, seqs, 2, 2, al, true);
228     assertEquals("ab??fghjk", seqs[0].getSequenceAsString());
229     assertEquals("12??67890", seqs[3].getSequenceAsString());
230
231     // delete positions 4/5/6
232     testee.appendEdit(Action.DELETE_GAP, seqs, 3, 3, al, true);
233     assertEquals("ab?hjk", seqs[0].getSequenceAsString());
234     assertEquals("12?890", seqs[3].getSequenceAsString());
235
236     // undo edit commands
237     testee.undoCommand(new AlignmentI[] { al });
238     assertEquals("ab?????hjk", seqs[0].getSequenceAsString());
239     assertEquals("12?????890", seqs[3].getSequenceAsString());
240   }
241
242   /**
243    * Unit test for JAL-1594 bug: click and drag sequence right to insert gaps -
244    * undo did not remove them all.
245    */
246   @Test(groups = { "Functional" })
247   public void testUndo_multipleInsertGaps()
248   {
249     testee.appendEdit(Action.INSERT_GAP, seqs, 4, 1, al, true);
250     testee.appendEdit(Action.INSERT_GAP, seqs, 5, 1, al, true);
251     testee.appendEdit(Action.INSERT_GAP, seqs, 6, 1, al, true);
252
253     // undo edit commands
254     testee.undoCommand(new AlignmentI[] { al });
255     assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
256     assertEquals("1234567890", seqs[3].getSequenceAsString());
257
258   }
259
260   /**
261    * Test cut followed by undo command
262    */
263   @Test(groups = { "Functional" })
264   public void testUndo_cut()
265   {
266     testee.appendEdit(Action.CUT, seqs, 4, 3, al, true);
267     // check something changed
268     assertEquals("abcdhjk", seqs[0].getSequenceAsString());
269     testee.undoCommand(new AlignmentI[] { al });
270     assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
271     assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
272     assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
273     assertEquals("1234567890", seqs[3].getSequenceAsString());
274   }
275
276   /**
277    * Test the replace command (used to manually edit a sequence)
278    */
279   @Test(groups = { "Functional" })
280   public void testReplace()
281   {
282     // seem to need a dataset sequence on the edited sequence here
283     seqs[1].createDatasetSequence();
284     assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
285     // NB command.number holds end position for a Replace command
286     new EditCommand("", Action.REPLACE, "Z-xY", new SequenceI[] { seqs[1] },
287             4, 8, al);
288     assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
289     assertEquals("fghjZ-xYopq", seqs[1].getSequenceAsString());
290     // Dataset Sequence should always be uppercase
291     assertEquals("fghjZxYopq".toUpperCase(Locale.ROOT),
292             seqs[1].getDatasetSequence().getSequenceAsString());
293     assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
294     assertEquals("1234567890", seqs[3].getSequenceAsString());
295   }
296
297   /**
298    * Test the replace command (used to manually edit a sequence)
299    */
300   @Test(groups = { "Functional" })
301   public void testReplace_withGaps()
302   {
303     SequenceI seq = new Sequence("seq", "ABC--DEF");
304     seq.createDatasetSequence();
305     assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
306     assertEquals(1, seq.getStart());
307     assertEquals(6, seq.getEnd());
308
309     /*
310      * replace C- with XYZ
311      * NB arg4 = start column of selection for edit (base 0)
312      * arg5 = column after end of selection for edit
313      */
314     EditCommand edit = new EditCommand("", Action.REPLACE, "xyZ",
315             new SequenceI[]
316             { seq }, 2, 4, al);
317     assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
318     assertEquals(1, seq.getStart());
319     assertEquals(8, seq.getEnd());
320     // Dataset sequence always uppercase
321     assertEquals("ABxyZDEF".toUpperCase(Locale.ROOT),
322             seq.getDatasetSequence().getSequenceAsString());
323     assertEquals(8, seq.getDatasetSequence().getEnd());
324
325     /*
326      * undo the edit
327      */
328     AlignmentI[] views = new AlignmentI[] {
329         new Alignment(new SequenceI[]
330         { seq }) };
331     edit.undoCommand(views);
332
333     assertEquals("ABC--DEF", seq.getSequenceAsString());
334     assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
335     assertEquals(1, seq.getStart());
336     assertEquals(6, seq.getEnd());
337     assertEquals(6, seq.getDatasetSequence().getEnd());
338
339     /*
340      * redo the edit
341      */
342     edit.doCommand(views);
343
344     assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
345     assertEquals(1, seq.getStart());
346     assertEquals(8, seq.getEnd());
347     // dataset sequence should be Uppercase
348     assertEquals("ABxyZDEF".toUpperCase(Locale.ROOT),
349             seq.getDatasetSequence().getSequenceAsString());
350     assertEquals(8, seq.getDatasetSequence().getEnd());
351
352   }
353
354   /**
355    * Test replace command when it doesn't cause a sequence edit (see comment in
356    */
357   @Test(groups = { "Functional" })
358   public void testReplaceFirstResiduesWithGaps()
359   {
360     // test replace when gaps are inserted at start. Start/end should change
361     // w.r.t. original edited sequence.
362     SequenceI dsseq = seqs[1].getDatasetSequence();
363     EditCommand edit = new EditCommand("", Action.REPLACE, "----",
364             new SequenceI[]
365             { seqs[1] }, 0, 4, al);
366
367     // trimmed start
368     assertEquals("----klmnopq", seqs[1].getSequenceAsString());
369     // and ds is preserved
370     assertTrue(dsseq == seqs[1].getDatasetSequence());
371     // and it is unchanged and UPPERCASE !
372     assertEquals("fghjklmnopq".toUpperCase(Locale.ROOT),
373             dsseq.getSequenceAsString());
374     // and that alignment sequence start has been adjusted
375     assertEquals(5, seqs[1].getStart());
376     assertEquals(11, seqs[1].getEnd());
377
378     AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
379     // and undo
380     edit.undoCommand(views);
381
382     // dataset sequence unchanged
383     assertTrue(dsseq == seqs[1].getDatasetSequence());
384     // restore sequence
385     assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
386     // and start/end numbering also restored
387     assertEquals(1, seqs[1].getStart());
388     assertEquals(11, seqs[1].getEnd());
389
390     // now redo
391     edit.undoCommand(views);
392
393     // and repeat asserts for the original edit
394
395     // trimmed start
396     assertEquals("----klmnopq", seqs[1].getSequenceAsString());
397     // and ds is preserved
398     assertTrue(dsseq == seqs[1].getDatasetSequence());
399     // and it is unchanged AND UPPERCASE !
400     assertEquals("fghjklmnopq".toUpperCase(Locale.ROOT),
401             dsseq.getSequenceAsString());
402     // and that alignment sequence start has been adjusted
403     assertEquals(5, seqs[1].getStart());
404     assertEquals(11, seqs[1].getEnd());
405
406   }
407
408   /**
409    * Test that the addEdit command correctly merges insert gap commands when
410    * possible.
411    */
412   @Test(groups = { "Functional" })
413   public void testAddEdit_multipleInsertGap()
414   {
415     /*
416      * 3 insert gap in a row (aka mouse drag right):
417      */
418     Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
419             new SequenceI[]
420             { seqs[0] }, 1, 1, al);
421     testee.addEdit(e);
422     SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
423     edited.setDatasetSequence(seqs[0].getDatasetSequence());
424     e = new EditCommand().new Edit(Action.INSERT_GAP,
425             new SequenceI[]
426             { edited }, 2, 1, al);
427     testee.addEdit(e);
428     edited = new Sequence("seq0", "a??bcdefghjk");
429     edited.setDatasetSequence(seqs[0].getDatasetSequence());
430     e = new EditCommand().new Edit(Action.INSERT_GAP,
431             new SequenceI[]
432             { edited }, 3, 1, al);
433     testee.addEdit(e);
434     assertEquals(1, testee.getSize());
435     assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
436     assertEquals(1, testee.getEdit(0).getPosition());
437     assertEquals(3, testee.getEdit(0).getNumber());
438
439     /*
440      * Add a non-contiguous edit - should not be merged.
441      */
442     e = new EditCommand().new Edit(Action.INSERT_GAP,
443             new SequenceI[]
444             { edited }, 5, 2, al);
445     testee.addEdit(e);
446     assertEquals(2, testee.getSize());
447     assertEquals(5, testee.getEdit(1).getPosition());
448     assertEquals(2, testee.getEdit(1).getNumber());
449
450     /*
451      * Add a Delete after the Insert - should not be merged.
452      */
453     e = new EditCommand().new Edit(Action.DELETE_GAP,
454             new SequenceI[]
455             { edited }, 6, 2, al);
456     testee.addEdit(e);
457     assertEquals(3, testee.getSize());
458     assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
459     assertEquals(6, testee.getEdit(2).getPosition());
460     assertEquals(2, testee.getEdit(2).getNumber());
461   }
462
463   /**
464    * Test that the addEdit command correctly merges delete gap commands when
465    * possible.
466    */
467   @Test(groups = { "Functional" })
468   public void testAddEdit_multipleDeleteGap()
469   {
470     /*
471      * 3 delete gap in a row (aka mouse drag left):
472      */
473     seqs[0].setSequence("a???bcdefghjk");
474     Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
475             new SequenceI[]
476             { seqs[0] }, 4, 1, al);
477     testee.addEdit(e);
478     assertEquals(1, testee.getSize());
479
480     SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
481     edited.setDatasetSequence(seqs[0].getDatasetSequence());
482     e = new EditCommand().new Edit(Action.DELETE_GAP,
483             new SequenceI[]
484             { edited }, 3, 1, al);
485     testee.addEdit(e);
486     assertEquals(1, testee.getSize());
487
488     edited = new Sequence("seq0", "a?bcdefghjk");
489     edited.setDatasetSequence(seqs[0].getDatasetSequence());
490     e = new EditCommand().new Edit(Action.DELETE_GAP,
491             new SequenceI[]
492             { edited }, 2, 1, al);
493     testee.addEdit(e);
494     assertEquals(1, testee.getSize());
495     assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
496     assertEquals(2, testee.getEdit(0).getPosition());
497     assertEquals(3, testee.getEdit(0).getNumber());
498
499     /*
500      * Add a non-contiguous edit - should not be merged.
501      */
502     e = new EditCommand().new Edit(Action.DELETE_GAP,
503             new SequenceI[]
504             { edited }, 2, 1, al);
505     testee.addEdit(e);
506     assertEquals(2, testee.getSize());
507     assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
508     assertEquals(2, testee.getEdit(1).getPosition());
509     assertEquals(1, testee.getEdit(1).getNumber());
510
511     /*
512      * Add an Insert after the Delete - should not be merged.
513      */
514     e = new EditCommand().new Edit(Action.INSERT_GAP,
515             new SequenceI[]
516             { edited }, 1, 1, al);
517     testee.addEdit(e);
518     assertEquals(3, testee.getSize());
519     assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
520     assertEquals(1, testee.getEdit(2).getPosition());
521     assertEquals(1, testee.getEdit(2).getNumber());
522   }
523
524   /**
525    * Test that the addEdit command correctly handles 'remove gaps' edits for the
526    * case when they appear contiguous but are acting on different sequences.
527    * They should not be merged.
528    */
529   @Test(groups = { "Functional" })
530   public void testAddEdit_removeAllGaps()
531   {
532     seqs[0].setSequence("a???bcdefghjk");
533     Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
534             new SequenceI[]
535             { seqs[0] }, 4, 1, al);
536     testee.addEdit(e);
537
538     seqs[1].setSequence("f??ghjklmnopq");
539     Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP,
540             new SequenceI[]
541             { seqs[1] }, 3, 1, al);
542     testee.addEdit(e2);
543     assertEquals(2, testee.getSize());
544     assertSame(e, testee.getEdit(0));
545     assertSame(e2, testee.getEdit(1));
546   }
547
548   /**
549    * Test that the addEdit command correctly merges insert gap commands acting
550    * on a multi-sequence selection.
551    */
552   @Test(groups = { "Functional" })
553   public void testAddEdit_groupInsertGaps()
554   {
555     /*
556      * 2 insert gap in a row (aka mouse drag right), on two sequences:
557      */
558     Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
559             new SequenceI[]
560             { seqs[0], seqs[1] }, 1, 1, al);
561     testee.addEdit(e);
562     SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
563     seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
564     SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
565     seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
566     e = new EditCommand().new Edit(Action.INSERT_GAP,
567             new SequenceI[]
568             { seq1edited, seq2edited }, 2, 1, al);
569     testee.addEdit(e);
570
571     assertEquals(1, testee.getSize());
572     assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
573     assertEquals(1, testee.getEdit(0).getPosition());
574     assertEquals(2, testee.getEdit(0).getNumber());
575     assertEquals(seqs[0].getDatasetSequence(),
576             testee.getEdit(0).getSequences()[0].getDatasetSequence());
577     assertEquals(seqs[1].getDatasetSequence(),
578             testee.getEdit(0).getSequences()[1].getDatasetSequence());
579   }
580
581   /**
582    * Test for 'undoing' a series of gap insertions.
583    * <ul>
584    * <li>Start: ABCDEF insert 2 at pos 1</li>
585    * <li>next: A--BCDEF insert 1 at pos 4</li>
586    * <li>next: A--B-CDEF insert 2 at pos 0</li>
587    * <li>last: --A--B-CDEF</li>
588    * </ul>
589    */
590   @Test(groups = { "Functional" })
591   public void testPriorState_multipleInserts()
592   {
593     EditCommand command = new EditCommand();
594     SequenceI seq = new Sequence("", "--A--B-CDEF");
595     SequenceI ds = new Sequence("", "ABCDEF");
596     seq.setDatasetSequence(ds);
597     SequenceI[] sqs = new SequenceI[] { seq };
598     Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
599     command.addEdit(e);
600     e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
601     command.addEdit(e);
602     e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
603     command.addEdit(e);
604
605     Map<SequenceI, SequenceI> unwound = command.priorState(false);
606     assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
607   }
608
609   /**
610    * Test for 'undoing' a series of gap deletions.
611    * <ul>
612    * <li>Start: A-B-C delete 1 at pos 1</li>
613    * <li>Next: AB-C delete 1 at pos 2</li>
614    * <li>End: ABC</li>
615    * </ul>
616    */
617   @Test(groups = { "Functional" })
618   public void testPriorState_removeAllGaps()
619   {
620     EditCommand command = new EditCommand();
621     SequenceI seq = new Sequence("", "ABC");
622     SequenceI ds = new Sequence("", "ABC");
623     seq.setDatasetSequence(ds);
624     SequenceI[] sqs = new SequenceI[] { seq };
625     Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
626     command.addEdit(e);
627     e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
628     command.addEdit(e);
629
630     Map<SequenceI, SequenceI> unwound = command.priorState(false);
631     assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
632   }
633
634   /**
635    * Test for 'undoing' a single delete edit.
636    */
637   @Test(groups = { "Functional" })
638   public void testPriorState_singleDelete()
639   {
640     EditCommand command = new EditCommand();
641     SequenceI seq = new Sequence("", "ABCDEF");
642     SequenceI ds = new Sequence("", "ABCDEF");
643     seq.setDatasetSequence(ds);
644     SequenceI[] sqs = new SequenceI[] { seq };
645     Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
646     command.addEdit(e);
647
648     Map<SequenceI, SequenceI> unwound = command.priorState(false);
649     assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
650   }
651
652   /**
653    * Test 'undoing' a single gap insertion edit command.
654    */
655   @Test(groups = { "Functional" })
656   public void testPriorState_singleInsert()
657   {
658     EditCommand command = new EditCommand();
659     SequenceI seq = new Sequence("", "AB---CDEF");
660     SequenceI ds = new Sequence("", "ABCDEF");
661     seq.setDatasetSequence(ds);
662     SequenceI[] sqs = new SequenceI[] { seq };
663     Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
664     command.addEdit(e);
665
666     Map<SequenceI, SequenceI> unwound = command.priorState(false);
667     SequenceI prior = unwound.get(ds);
668     assertEquals("ABCDEF", prior.getSequenceAsString());
669     assertEquals(1, prior.getStart());
670     assertEquals(6, prior.getEnd());
671   }
672
673   /**
674    * Test 'undoing' a single gap insertion edit command, on a sequence whose
675    * start residue is other than 1
676    */
677   @Test(groups = { "Functional" })
678   public void testPriorState_singleInsertWithOffset()
679   {
680     EditCommand command = new EditCommand();
681     SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
682     // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
683     // seq.setDatasetSequence(ds);
684     seq.createDatasetSequence();
685     SequenceI[] sqs = new SequenceI[] { seq };
686     Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
687     command.addEdit(e);
688
689     Map<SequenceI, SequenceI> unwound = command.priorState(false);
690     SequenceI prior = unwound.get(seq.getDatasetSequence());
691     assertEquals("ABCDEF", prior.getSequenceAsString());
692     assertEquals(8, prior.getStart());
693     assertEquals(13, prior.getEnd());
694   }
695
696   /**
697    * Test that mimics 'remove all gaps' action. This generates delete gap edits
698    * for contiguous gaps in each sequence separately.
699    */
700   @Test(groups = { "Functional" })
701   public void testPriorState_removeGapsMultipleSeqs()
702   {
703     EditCommand command = new EditCommand();
704     String original1 = "--ABC-DEF";
705     String original2 = "FG-HI--J";
706     String original3 = "M-NOPQ";
707
708     /*
709      * Two edits for the first sequence
710      */
711     SequenceI seq = new Sequence("", "ABC-DEF");
712     SequenceI ds1 = new Sequence("", "ABCDEF");
713     seq.setDatasetSequence(ds1);
714     SequenceI[] sqs = new SequenceI[] { seq };
715     Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
716     command.addEdit(e);
717     seq = new Sequence("", "ABCDEF");
718     seq.setDatasetSequence(ds1);
719     sqs = new SequenceI[] { seq };
720     e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
721     command.addEdit(e);
722
723     /*
724      * Two edits for the second sequence
725      */
726     seq = new Sequence("", "FGHI--J");
727     SequenceI ds2 = new Sequence("", "FGHIJ");
728     seq.setDatasetSequence(ds2);
729     sqs = new SequenceI[] { seq };
730     e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
731     command.addEdit(e);
732     seq = new Sequence("", "FGHIJ");
733     seq.setDatasetSequence(ds2);
734     sqs = new SequenceI[] { seq };
735     e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
736     command.addEdit(e);
737
738     /*
739      * One edit for the third sequence.
740      */
741     seq = new Sequence("", "MNOPQ");
742     SequenceI ds3 = new Sequence("", "MNOPQ");
743     seq.setDatasetSequence(ds3);
744     sqs = new SequenceI[] { seq };
745     e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
746     command.addEdit(e);
747
748     Map<SequenceI, SequenceI> unwound = command.priorState(false);
749     assertEquals(original1, unwound.get(ds1).getSequenceAsString());
750     assertEquals(original2, unwound.get(ds2).getSequenceAsString());
751     assertEquals(original3, unwound.get(ds3).getSequenceAsString());
752   }
753
754   /**
755    * Test that mimics 'remove all gapped columns' action. This generates a
756    * series Delete Gap edits that each act on all sequences that share a gapped
757    * column region.
758    */
759   @Test(groups = { "Functional" })
760   public void testPriorState_removeGappedCols()
761   {
762     EditCommand command = new EditCommand();
763     String original1 = "--ABC--DEF";
764     String original2 = "-G-HI--J";
765     String original3 = "-M-NO--PQ";
766
767     /*
768      * First edit deletes the first column.
769      */
770     SequenceI seq1 = new Sequence("", "-ABC--DEF");
771     SequenceI ds1 = new Sequence("", "ABCDEF");
772     seq1.setDatasetSequence(ds1);
773     SequenceI seq2 = new Sequence("", "G-HI--J");
774     SequenceI ds2 = new Sequence("", "GHIJ");
775     seq2.setDatasetSequence(ds2);
776     SequenceI seq3 = new Sequence("", "M-NO--PQ");
777     SequenceI ds3 = new Sequence("", "MNOPQ");
778     seq3.setDatasetSequence(ds3);
779     SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
780     Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
781     command.addEdit(e);
782
783     /*
784      * Second edit deletes what is now columns 4 and 5.
785      */
786     seq1 = new Sequence("", "-ABCDEF");
787     seq1.setDatasetSequence(ds1);
788     seq2 = new Sequence("", "G-HIJ");
789     seq2.setDatasetSequence(ds2);
790     seq3 = new Sequence("", "M-NOPQ");
791     seq3.setDatasetSequence(ds3);
792     sqs = new SequenceI[] { seq1, seq2, seq3 };
793     e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
794     command.addEdit(e);
795
796     Map<SequenceI, SequenceI> unwound = command.priorState(false);
797     assertEquals(original1, unwound.get(ds1).getSequenceAsString());
798     assertEquals(original2, unwound.get(ds2).getSequenceAsString());
799     assertEquals(original3, unwound.get(ds3).getSequenceAsString());
800     assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
801     assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
802     assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
803   }
804
805   /**
806    * Test a cut action's relocation of sequence features
807    */
808   @Test(groups = { "Functional" })
809   public void testCut_withFeatures()
810   {
811     /*
812      * create sequence features before, after and overlapping
813      * a cut of columns/residues 4-7
814      */
815     SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
816     seq0.addSequenceFeature(
817             new SequenceFeature("before", "", 1, 3, 0f, null));
818     seq0.addSequenceFeature(
819             new SequenceFeature("overlap left", "", 2, 6, 0f, null));
820     seq0.addSequenceFeature(
821             new SequenceFeature("internal", "", 5, 6, 0f, null));
822     seq0.addSequenceFeature(
823             new SequenceFeature("overlap right", "", 7, 8, 0f, null));
824     seq0.addSequenceFeature(
825             new SequenceFeature("after", "", 8, 10, 0f, null));
826
827     /*
828      * add some contact features
829      */
830     SequenceFeature internalContact = new SequenceFeature("disulphide bond",
831             "", 5, 6, 0f, null);
832     seq0.addSequenceFeature(internalContact); // should get deleted
833     SequenceFeature overlapLeftContact = new SequenceFeature(
834             "disulphide bond", "", 2, 6, 0f, null);
835     seq0.addSequenceFeature(overlapLeftContact); // should get deleted
836     SequenceFeature overlapRightContact = new SequenceFeature(
837             "disulphide bond", "", 5, 8, 0f, null);
838     seq0.addSequenceFeature(overlapRightContact); // should get deleted
839     SequenceFeature spanningContact = new SequenceFeature("disulphide bond",
840             "", 2, 9, 0f, null);
841     seq0.addSequenceFeature(spanningContact); // should get shortened 3'
842
843     /*
844      * cut columns 3-6 (base 0), residues d-g 4-7
845      */
846     Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
847     EditCommand.cut(ec, new AlignmentI[] { al });
848
849     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
850     SequenceFeatures.sortFeatures(sfs, true);
851
852     assertEquals(5, sfs.size()); // features internal to cut were deleted
853     SequenceFeature sf = sfs.get(0);
854     assertEquals("before", sf.getType());
855     assertEquals(1, sf.getBegin());
856     assertEquals(3, sf.getEnd());
857     sf = sfs.get(1);
858     assertEquals("disulphide bond", sf.getType());
859     assertEquals(2, sf.getBegin());
860     assertEquals(5, sf.getEnd()); // truncated by cut
861     sf = sfs.get(2);
862     assertEquals("overlap left", sf.getType());
863     assertEquals(2, sf.getBegin());
864     assertEquals(3, sf.getEnd()); // truncated by cut
865     sf = sfs.get(3);
866     assertEquals("after", sf.getType());
867     assertEquals(4, sf.getBegin()); // shifted left by cut
868     assertEquals(6, sf.getEnd()); // shifted left by cut
869     sf = sfs.get(4);
870     assertEquals("overlap right", sf.getType());
871     assertEquals(4, sf.getBegin()); // shifted left by cut
872     assertEquals(4, sf.getEnd()); // truncated by cut
873   }
874
875   /**
876    * Test a cut action's relocation of sequence features, with full coverage of
877    * all possible feature and cut locations for a 5-position ungapped sequence
878    */
879   @Test(groups = { "Functional" })
880   public void testCut_withFeatures_exhaustive()
881   {
882     /*
883      * create a sequence features on each subrange of 1-5
884      */
885     SequenceI seq0 = new Sequence("seq", "ABCDE");
886     int start = 8;
887     int end = 12;
888     seq0.setStart(start);
889     seq0.setEnd(end);
890     AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
891     alignment.setDataset(null);
892
893     /*
894      * create a new alignment with shared dataset sequence
895      */
896     AlignmentI copy = new Alignment(
897             new SequenceI[]
898             { alignment.getDataset().getSequenceAt(0).deriveSequence() });
899     SequenceI copySeq0 = copy.getSequenceAt(0);
900
901     for (int from = start; from <= end; from++)
902     {
903       for (int to = from; to <= end; to++)
904       {
905         String desc = String.format("%d-%d", from, to);
906         SequenceFeature sf = new SequenceFeature("test", desc, from, to, 0f,
907                 null);
908         sf.setValue("from", Integer.valueOf(from));
909         sf.setValue("to", Integer.valueOf(to));
910         seq0.addSequenceFeature(sf);
911       }
912     }
913     // sanity check
914     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
915     assertEquals(func(5), sfs.size());
916     assertEquals(sfs, copySeq0.getSequenceFeatures());
917     String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
918
919     /*
920      * now perform all possible cuts of subranges of columns 1-5
921      * and validate the resulting remaining sequence features!
922      */
923     SequenceI[] sqs = new SequenceI[] { seq0 };
924
925     for (int from = 0; from < seq0.getLength(); from++)
926     {
927       for (int to = from; to < seq0.getLength(); to++)
928       {
929         EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from,
930                 (to - from + 1), alignment);
931         final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
932         boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
933                 .getDatasetSequence();
934
935         verifyCut(seq0, from, to, msg, start);
936
937         /*
938          * verify copy alignment dataset sequence unaffected
939          */
940         assertEquals("Original dataset sequence was modified",
941                 copySequenceFeatures,
942                 copySeq0.getSequenceFeatures().toString());
943
944         /*
945          * verify any new dataset sequence was added to the
946          * alignment dataset
947          */
948         assertEquals("Wrong Dataset size after " + msg,
949                 newDatasetSequence ? 2 : 1,
950                 alignment.getDataset().getHeight());
951
952         /*
953          * undo and verify all restored
954          */
955         AlignmentI[] views = new AlignmentI[] { alignment };
956         ec.undoCommand(views);
957         sfs = seq0.getSequenceFeatures();
958         assertEquals("After undo of " + msg, func(5), sfs.size());
959         verifyUndo(from, to, sfs);
960
961         /*
962          * verify copy alignment dataset sequence still unaffected
963          * and alignment dataset has shrunk (if it was added to)
964          */
965         assertEquals("Original dataset sequence was modified",
966                 copySequenceFeatures,
967                 copySeq0.getSequenceFeatures().toString());
968         assertEquals("Wrong Dataset size after Undo of " + msg, 1,
969                 alignment.getDataset().getHeight());
970
971         /*
972          * redo and verify
973          */
974         ec.doCommand(views);
975         verifyCut(seq0, from, to, msg, start);
976
977         /*
978          * verify copy alignment dataset sequence unaffected
979          * and any new dataset sequence readded to alignment dataset
980          */
981         assertEquals("Original dataset sequence was modified",
982                 copySequenceFeatures,
983                 copySeq0.getSequenceFeatures().toString());
984         assertEquals("Wrong Dataset size after Redo of " + msg,
985                 newDatasetSequence ? 2 : 1,
986                 alignment.getDataset().getHeight());
987
988         /*
989          * undo ready for next cut
990          */
991         ec.undoCommand(views);
992
993         /*
994          * final verify that copy alignment dataset sequence is still unaffected
995          * and that alignment dataset has shrunk
996          */
997         assertEquals("Original dataset sequence was modified",
998                 copySequenceFeatures,
999                 copySeq0.getSequenceFeatures().toString());
1000         assertEquals("Wrong Dataset size after final Undo of " + msg, 1,
1001                 alignment.getDataset().getHeight());
1002       }
1003     }
1004   }
1005
1006   /**
1007    * Verify by inspection that the sequence features left on the sequence after
1008    * a cut match the expected results. The trick to this is that we can parse
1009    * each feature's original start-end positions from its description.
1010    * 
1011    * @param seq0
1012    * @param from
1013    * @param to
1014    * @param msg
1015    * @param seqStart
1016    */
1017   protected void verifyCut(SequenceI seq0, int from, int to,
1018           final String msg, int seqStart)
1019   {
1020     List<SequenceFeature> sfs;
1021     sfs = seq0.getSequenceFeatures();
1022
1023     Collections.sort(sfs, BY_DESCRIPTION);
1024
1025     /*
1026      * confirm the number of features has reduced by the
1027      * number of features within the cut region i.e. by
1028      * func(length of cut); exception is a cut at start or end of sequence, 
1029      * which retains the original coordinates, dataset sequence 
1030      * and all its features
1031      */
1032     boolean datasetRetained = from == 0 || to == 4;
1033     if (datasetRetained)
1034     {
1035       // dataset and all features retained
1036       assertEquals(msg, func(5), sfs.size());
1037     }
1038     else if (to - from == 4)
1039     {
1040       // all columns were cut
1041       assertTrue(sfs.isEmpty());
1042     }
1043     else
1044     {
1045       // failure in checkFeatureRelocation is more informative!
1046       assertEquals(msg + "wrong number of features left",
1047               func(5) - func(to - from + 1), sfs.size());
1048     }
1049
1050     /*
1051      * inspect individual features
1052      */
1053     for (SequenceFeature sf : sfs)
1054     {
1055       verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
1056               seqStart);
1057     }
1058   }
1059
1060   /**
1061    * Check that after Undo, every feature has start/end that match its original
1062    * "start" and "end" properties
1063    * 
1064    * @param from
1065    * @param to
1066    * @param sfs
1067    */
1068   protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
1069   {
1070     for (SequenceFeature sf : sfs)
1071     {
1072       final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1073       final int oldTo = ((Integer) sf.getValue("to")).intValue();
1074       String msg = String.format("Undo cut of [%d-%d], feature at [%d-%d] ",
1075               from + 1, to + 1, oldFrom, oldTo);
1076       assertEquals(msg + "start", oldFrom, sf.getBegin());
1077       assertEquals(msg + "end", oldTo, sf.getEnd());
1078     }
1079   }
1080
1081   /**
1082    * Helper method to check a feature has been correctly relocated after a cut
1083    * 
1084    * @param sf
1085    * @param from
1086    *          start of cut (first residue cut 1..)
1087    * @param to
1088    *          end of cut (last residue cut 1..)
1089    * @param newDataset
1090    * @param seqStart
1091    */
1092   private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1093           boolean newDataset, int seqStart)
1094   {
1095     // TODO handle the gapped sequence case as well
1096     int cutSize = to - from + 1;
1097     final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1098     final int oldTo = ((Integer) sf.getValue("to")).intValue();
1099     final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1100     final int oldToPosition = oldTo - seqStart + 1; // 1..
1101
1102     String msg = String.format(
1103             "Feature %s relocated to %d-%d after cut of %d-%d",
1104             sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1105     if (!newDataset)
1106     {
1107       // dataset retained with all features unchanged
1108       assertEquals("0: " + msg, oldFrom, sf.getBegin());
1109       assertEquals("0: " + msg, oldTo, sf.getEnd());
1110     }
1111     else if (oldToPosition < from)
1112     {
1113       // before cut region so unchanged
1114       assertEquals("1: " + msg, oldFrom, sf.getBegin());
1115       assertEquals("2: " + msg, oldTo, sf.getEnd());
1116     }
1117     else if (oldFromPosition > to)
1118     {
1119       // follows cut region - shift by size of cut
1120       assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1121               sf.getBegin());
1122       assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1123               sf.getEnd());
1124     }
1125     else if (oldFromPosition < from && oldToPosition > to)
1126     {
1127       // feature encloses cut region - shrink it right
1128       assertEquals("5: " + msg, oldFrom, sf.getBegin());
1129       assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1130     }
1131     else if (oldFromPosition < from)
1132     {
1133       // feature overlaps left side of cut region - truncated right
1134       assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1135     }
1136     else if (oldToPosition > to)
1137     {
1138       // feature overlaps right side of cut region - truncated left
1139       assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1140               sf.getBegin());
1141       assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1142               sf.getEnd());
1143     }
1144     else
1145     {
1146       // feature internal to cut - should have been deleted!
1147       Assert.fail(msg + " - should have been deleted");
1148     }
1149   }
1150
1151   /**
1152    * Test a cut action's relocation of sequence features
1153    */
1154   @Test(groups = { "Functional" })
1155   public void testCut_withFeatures5prime()
1156   {
1157     SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1158     seq0.createDatasetSequence();
1159     assertEquals(8, seq0.getStart());
1160     seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f, null));
1161     SequenceI[] seqsArray = new SequenceI[] { seq0 };
1162     AlignmentI alignment = new Alignment(seqsArray);
1163
1164     /*
1165      * cut columns of A-B; same dataset sequence is retained, aligned sequence
1166      * start becomes 10
1167      */
1168     Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1169     EditCommand.cut(ec, new AlignmentI[] { alignment });
1170
1171     /*
1172      * feature on CC(10-11) should still be on CC(10-11)
1173      */
1174     assertSame(seq0, alignment.getSequenceAt(0));
1175     assertEquals(10, seq0.getStart());
1176     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1177     assertEquals(1, sfs.size());
1178     SequenceFeature sf = sfs.get(0);
1179     assertEquals(10, sf.getBegin());
1180     assertEquals(11, sf.getEnd());
1181   }
1182 }