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