JAL-2541 removed redundant test flag
[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     new EditCommand("", Action.REPLACE, "ZXY", new SequenceI[] { seqs[1] },
283             4, 8, al);
284     assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
285     assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
286     assertEquals("1234567890", seqs[3].getSequenceAsString());
287   }
288
289   /**
290    * Test the replace command (used to manually edit a sequence)
291    */
292   @Test(groups = { "Functional" })
293   public void testReplace_withGaps()
294   {
295     SequenceI seq = new Sequence("seq", "ABC--DEF");
296     seq.createDatasetSequence();
297     assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
298     assertEquals(1, seq.getStart());
299     assertEquals(6, seq.getEnd());
300
301     /*
302      * replace C- with XYZ
303      * NB arg4 = start column of selection for edit (base 0)
304      * arg5 = column after end of selection for edit
305      */
306     EditCommand edit = new EditCommand("", Action.REPLACE, "XYZ",
307             new SequenceI[]
308             { seq }, 2,
309             4, al);
310     assertEquals("ABXYZ-DEF", seq.getSequenceAsString());
311     assertEquals(1, seq.getStart());
312     assertEquals(8, seq.getEnd());
313     assertEquals("ABXYZDEF", seq.getDatasetSequence().getSequenceAsString());
314     assertEquals(8, seq.getDatasetSequence().getEnd());
315
316     /*
317      * undo the edit
318      */
319     AlignmentI[] views = new AlignmentI[]
320     { new Alignment(new SequenceI[] { seq }) };
321     edit.undoCommand(views);
322
323     assertEquals("ABC--DEF", seq.getSequenceAsString());
324     assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
325     assertEquals(1, seq.getStart());
326     assertEquals(6, seq.getEnd());
327     assertEquals(6, seq.getDatasetSequence().getEnd());
328
329     /*
330      * redo the edit
331      */
332     edit.doCommand(views);
333
334     assertEquals("ABXYZ-DEF", seq.getSequenceAsString());
335     assertEquals(1, seq.getStart());
336     assertEquals(8, seq.getEnd());
337     assertEquals("ABXYZDEF",
338             seq.getDatasetSequence().getSequenceAsString());
339     assertEquals(8, seq.getDatasetSequence().getEnd());
340
341   }
342
343   /**
344    * Test replace command when it doesn't cause a sequence edit (see comment in
345    */
346   @Test(groups = { "Functional" })
347   public void testReplaceFirstResiduesWithGaps()
348   {
349     // test replace when gaps are inserted at start. Start/end should change
350     // w.r.t. original edited sequence.
351     SequenceI dsseq = seqs[1].getDatasetSequence();
352     EditCommand edit = new EditCommand("", Action.REPLACE, "----",
353             new SequenceI[]
354             { seqs[1] }, 0, 4, al);
355
356     // trimmed start
357     assertEquals("----klmnopq", seqs[1].getSequenceAsString());
358     // and ds is preserved
359     assertTrue(dsseq == seqs[1].getDatasetSequence());
360     // and it is unchanged
361     assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
362     // and that alignment sequence start has been adjusted
363     assertEquals(5, seqs[1].getStart());
364     assertEquals(11, seqs[1].getEnd());
365
366     AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
367     // and undo
368     edit.undoCommand(views);
369
370     // dataset sequence unchanged
371     assertTrue(dsseq == seqs[1].getDatasetSequence());
372     // restore sequence
373     assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
374     // and start/end numbering also restored
375     assertEquals(1, seqs[1].getStart());
376     assertEquals(11, seqs[1].getEnd());
377
378     // now redo
379     edit.undoCommand(views);
380
381     // and repeat asserts for the original edit
382
383     // trimmed start
384     assertEquals("----klmnopq", seqs[1].getSequenceAsString());
385     // and ds is preserved
386     assertTrue(dsseq == seqs[1].getDatasetSequence());
387     // and it is unchanged
388     assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
389     // and that alignment sequence start has been adjusted
390     assertEquals(5, seqs[1].getStart());
391     assertEquals(11, seqs[1].getEnd());
392
393   }
394
395   /**
396    * Test that the addEdit command correctly merges insert gap commands when
397    * possible.
398    */
399   @Test(groups = { "Functional" })
400   public void testAddEdit_multipleInsertGap()
401   {
402     /*
403      * 3 insert gap in a row (aka mouse drag right):
404      */
405     Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
406             new SequenceI[] { seqs[0] }, 1, 1, al);
407     testee.addEdit(e);
408     SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
409     edited.setDatasetSequence(seqs[0].getDatasetSequence());
410     e = new EditCommand().new Edit(Action.INSERT_GAP,
411             new SequenceI[] { edited }, 2, 1, al);
412     testee.addEdit(e);
413     edited = new Sequence("seq0", "a??bcdefghjk");
414     edited.setDatasetSequence(seqs[0].getDatasetSequence());
415     e = new EditCommand().new Edit(Action.INSERT_GAP,
416             new SequenceI[] { edited }, 3, 1, al);
417     testee.addEdit(e);
418     assertEquals(1, testee.getSize());
419     assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
420     assertEquals(1, testee.getEdit(0).getPosition());
421     assertEquals(3, testee.getEdit(0).getNumber());
422
423     /*
424      * Add a non-contiguous edit - should not be merged.
425      */
426     e = new EditCommand().new Edit(Action.INSERT_GAP,
427             new SequenceI[] { edited }, 5, 2, al);
428     testee.addEdit(e);
429     assertEquals(2, testee.getSize());
430     assertEquals(5, testee.getEdit(1).getPosition());
431     assertEquals(2, testee.getEdit(1).getNumber());
432
433     /*
434      * Add a Delete after the Insert - should not be merged.
435      */
436     e = new EditCommand().new Edit(Action.DELETE_GAP,
437             new SequenceI[] { edited }, 6, 2, al);
438     testee.addEdit(e);
439     assertEquals(3, testee.getSize());
440     assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
441     assertEquals(6, testee.getEdit(2).getPosition());
442     assertEquals(2, testee.getEdit(2).getNumber());
443   }
444
445   /**
446    * Test that the addEdit command correctly merges delete gap commands when
447    * possible.
448    */
449   @Test(groups = { "Functional" })
450   public void testAddEdit_multipleDeleteGap()
451   {
452     /*
453      * 3 delete gap in a row (aka mouse drag left):
454      */
455     seqs[0].setSequence("a???bcdefghjk");
456     Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
457             new SequenceI[] { seqs[0] }, 4, 1, al);
458     testee.addEdit(e);
459     assertEquals(1, testee.getSize());
460
461     SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
462     edited.setDatasetSequence(seqs[0].getDatasetSequence());
463     e = new EditCommand().new Edit(Action.DELETE_GAP,
464             new SequenceI[] { edited }, 3, 1, al);
465     testee.addEdit(e);
466     assertEquals(1, testee.getSize());
467
468     edited = new Sequence("seq0", "a?bcdefghjk");
469     edited.setDatasetSequence(seqs[0].getDatasetSequence());
470     e = new EditCommand().new Edit(Action.DELETE_GAP,
471             new SequenceI[] { edited }, 2, 1, al);
472     testee.addEdit(e);
473     assertEquals(1, testee.getSize());
474     assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
475     assertEquals(2, testee.getEdit(0).getPosition());
476     assertEquals(3, testee.getEdit(0).getNumber());
477
478     /*
479      * Add a non-contiguous edit - should not be merged.
480      */
481     e = new EditCommand().new Edit(Action.DELETE_GAP,
482             new SequenceI[] { edited }, 2, 1, al);
483     testee.addEdit(e);
484     assertEquals(2, testee.getSize());
485     assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
486     assertEquals(2, testee.getEdit(1).getPosition());
487     assertEquals(1, testee.getEdit(1).getNumber());
488
489     /*
490      * Add an Insert after the Delete - should not be merged.
491      */
492     e = new EditCommand().new Edit(Action.INSERT_GAP,
493             new SequenceI[] { edited }, 1, 1, al);
494     testee.addEdit(e);
495     assertEquals(3, testee.getSize());
496     assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
497     assertEquals(1, testee.getEdit(2).getPosition());
498     assertEquals(1, testee.getEdit(2).getNumber());
499   }
500
501   /**
502    * Test that the addEdit command correctly handles 'remove gaps' edits for the
503    * case when they appear contiguous but are acting on different sequences.
504    * They should not be merged.
505    */
506   @Test(groups = { "Functional" })
507   public void testAddEdit_removeAllGaps()
508   {
509     seqs[0].setSequence("a???bcdefghjk");
510     Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
511             new SequenceI[] { seqs[0] }, 4, 1, al);
512     testee.addEdit(e);
513
514     seqs[1].setSequence("f??ghjklmnopq");
515     Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[]
516     { seqs[1] }, 3, 1, al);
517     testee.addEdit(e2);
518     assertEquals(2, testee.getSize());
519     assertSame(e, testee.getEdit(0));
520     assertSame(e2, testee.getEdit(1));
521   }
522
523   /**
524    * Test that the addEdit command correctly merges insert gap commands acting
525    * on a multi-sequence selection.
526    */
527   @Test(groups = { "Functional" })
528   public void testAddEdit_groupInsertGaps()
529   {
530     /*
531      * 2 insert gap in a row (aka mouse drag right), on two sequences:
532      */
533     Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
534         seqs[0], seqs[1] }, 1, 1, al);
535     testee.addEdit(e);
536     SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
537     seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
538     SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
539     seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
540     e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
541         seq1edited, seq2edited }, 2, 1, al);
542     testee.addEdit(e);
543
544     assertEquals(1, testee.getSize());
545     assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
546     assertEquals(1, testee.getEdit(0).getPosition());
547     assertEquals(2, testee.getEdit(0).getNumber());
548     assertEquals(seqs[0].getDatasetSequence(), testee.getEdit(0)
549             .getSequences()[0].getDatasetSequence());
550     assertEquals(seqs[1].getDatasetSequence(), testee.getEdit(0)
551             .getSequences()[1].getDatasetSequence());
552   }
553
554   /**
555    * Test for 'undoing' a series of gap insertions.
556    * <ul>
557    * <li>Start: ABCDEF insert 2 at pos 1</li>
558    * <li>next: A--BCDEF insert 1 at pos 4</li>
559    * <li>next: A--B-CDEF insert 2 at pos 0</li>
560    * <li>last: --A--B-CDEF</li>
561    * </ul>
562    */
563   @Test(groups = { "Functional" })
564   public void testPriorState_multipleInserts()
565   {
566     EditCommand command = new EditCommand();
567     SequenceI seq = new Sequence("", "--A--B-CDEF");
568     SequenceI ds = new Sequence("", "ABCDEF");
569     seq.setDatasetSequence(ds);
570     SequenceI[] sqs = new SequenceI[] { seq };
571     Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
572     command.addEdit(e);
573     e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
574     command.addEdit(e);
575     e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
576     command.addEdit(e);
577
578     Map<SequenceI, SequenceI> unwound = command.priorState(false);
579     assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
580   }
581
582   /**
583    * Test for 'undoing' a series of gap deletions.
584    * <ul>
585    * <li>Start: A-B-C delete 1 at pos 1</li>
586    * <li>Next: AB-C delete 1 at pos 2</li>
587    * <li>End: ABC</li>
588    * </ul>
589    */
590   @Test(groups = { "Functional" })
591   public void testPriorState_removeAllGaps()
592   {
593     EditCommand command = new EditCommand();
594     SequenceI seq = new Sequence("", "ABC");
595     SequenceI ds = new Sequence("", "ABC");
596     seq.setDatasetSequence(ds);
597     SequenceI[] sqs = new SequenceI[] { seq };
598     Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
599     command.addEdit(e);
600     e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
601     command.addEdit(e);
602
603     Map<SequenceI, SequenceI> unwound = command.priorState(false);
604     assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
605   }
606
607   /**
608    * Test for 'undoing' a single delete edit.
609    */
610   @Test(groups = { "Functional" })
611   public void testPriorState_singleDelete()
612   {
613     EditCommand command = new EditCommand();
614     SequenceI seq = new Sequence("", "ABCDEF");
615     SequenceI ds = new Sequence("", "ABCDEF");
616     seq.setDatasetSequence(ds);
617     SequenceI[] sqs = new SequenceI[] { seq };
618     Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
619     command.addEdit(e);
620
621     Map<SequenceI, SequenceI> unwound = command.priorState(false);
622     assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
623   }
624
625   /**
626    * Test 'undoing' a single gap insertion edit command.
627    */
628   @Test(groups = { "Functional" })
629   public void testPriorState_singleInsert()
630   {
631     EditCommand command = new EditCommand();
632     SequenceI seq = new Sequence("", "AB---CDEF");
633     SequenceI ds = new Sequence("", "ABCDEF");
634     seq.setDatasetSequence(ds);
635     SequenceI[] sqs = new SequenceI[] { seq };
636     Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
637     command.addEdit(e);
638
639     Map<SequenceI, SequenceI> unwound = command.priorState(false);
640     SequenceI prior = unwound.get(ds);
641     assertEquals("ABCDEF", prior.getSequenceAsString());
642     assertEquals(1, prior.getStart());
643     assertEquals(6, prior.getEnd());
644   }
645
646   /**
647    * Test 'undoing' a single gap insertion edit command, on a sequence whose
648    * start residue is other than 1
649    */
650   @Test(groups = { "Functional" })
651   public void testPriorState_singleInsertWithOffset()
652   {
653     EditCommand command = new EditCommand();
654     SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
655     // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
656     // seq.setDatasetSequence(ds);
657     seq.createDatasetSequence();
658     SequenceI[] sqs = new SequenceI[] { seq };
659     Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
660     command.addEdit(e);
661
662     Map<SequenceI, SequenceI> unwound = command.priorState(false);
663     SequenceI prior = unwound.get(seq.getDatasetSequence());
664     assertEquals("ABCDEF", prior.getSequenceAsString());
665     assertEquals(8, prior.getStart());
666     assertEquals(13, prior.getEnd());
667   }
668
669   /**
670    * Test that mimics 'remove all gaps' action. This generates delete gap edits
671    * for contiguous gaps in each sequence separately.
672    */
673   @Test(groups = { "Functional" })
674   public void testPriorState_removeGapsMultipleSeqs()
675   {
676     EditCommand command = new EditCommand();
677     String original1 = "--ABC-DEF";
678     String original2 = "FG-HI--J";
679     String original3 = "M-NOPQ";
680
681     /*
682      * Two edits for the first sequence
683      */
684     SequenceI seq = new Sequence("", "ABC-DEF");
685     SequenceI ds1 = new Sequence("", "ABCDEF");
686     seq.setDatasetSequence(ds1);
687     SequenceI[] sqs = new SequenceI[] { seq };
688     Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
689     command.addEdit(e);
690     seq = new Sequence("", "ABCDEF");
691     seq.setDatasetSequence(ds1);
692     sqs = new SequenceI[] { seq };
693     e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
694     command.addEdit(e);
695
696     /*
697      * Two edits for the second sequence
698      */
699     seq = new Sequence("", "FGHI--J");
700     SequenceI ds2 = new Sequence("", "FGHIJ");
701     seq.setDatasetSequence(ds2);
702     sqs = new SequenceI[] { seq };
703     e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
704     command.addEdit(e);
705     seq = new Sequence("", "FGHIJ");
706     seq.setDatasetSequence(ds2);
707     sqs = new SequenceI[] { seq };
708     e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
709     command.addEdit(e);
710
711     /*
712      * One edit for the third sequence.
713      */
714     seq = new Sequence("", "MNOPQ");
715     SequenceI ds3 = new Sequence("", "MNOPQ");
716     seq.setDatasetSequence(ds3);
717     sqs = new SequenceI[] { seq };
718     e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
719     command.addEdit(e);
720
721     Map<SequenceI, SequenceI> unwound = command.priorState(false);
722     assertEquals(original1, unwound.get(ds1).getSequenceAsString());
723     assertEquals(original2, unwound.get(ds2).getSequenceAsString());
724     assertEquals(original3, unwound.get(ds3).getSequenceAsString());
725   }
726
727   /**
728    * Test that mimics 'remove all gapped columns' action. This generates a
729    * series Delete Gap edits that each act on all sequences that share a gapped
730    * column region.
731    */
732   @Test(groups = { "Functional" })
733   public void testPriorState_removeGappedCols()
734   {
735     EditCommand command = new EditCommand();
736     String original1 = "--ABC--DEF";
737     String original2 = "-G-HI--J";
738     String original3 = "-M-NO--PQ";
739
740     /*
741      * First edit deletes the first column.
742      */
743     SequenceI seq1 = new Sequence("", "-ABC--DEF");
744     SequenceI ds1 = new Sequence("", "ABCDEF");
745     seq1.setDatasetSequence(ds1);
746     SequenceI seq2 = new Sequence("", "G-HI--J");
747     SequenceI ds2 = new Sequence("", "GHIJ");
748     seq2.setDatasetSequence(ds2);
749     SequenceI seq3 = new Sequence("", "M-NO--PQ");
750     SequenceI ds3 = new Sequence("", "MNOPQ");
751     seq3.setDatasetSequence(ds3);
752     SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
753     Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
754     command.addEdit(e);
755
756     /*
757      * Second edit deletes what is now columns 4 and 5.
758      */
759     seq1 = new Sequence("", "-ABCDEF");
760     seq1.setDatasetSequence(ds1);
761     seq2 = new Sequence("", "G-HIJ");
762     seq2.setDatasetSequence(ds2);
763     seq3 = new Sequence("", "M-NOPQ");
764     seq3.setDatasetSequence(ds3);
765     sqs = new SequenceI[] { seq1, seq2, seq3 };
766     e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
767     command.addEdit(e);
768
769     Map<SequenceI, SequenceI> unwound = command.priorState(false);
770     assertEquals(original1, unwound.get(ds1).getSequenceAsString());
771     assertEquals(original2, unwound.get(ds2).getSequenceAsString());
772     assertEquals(original3, unwound.get(ds3).getSequenceAsString());
773     assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
774     assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
775     assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
776   }
777
778   /**
779    * Test a cut action's relocation of sequence features
780    */
781   @Test(groups = { "Functional" })
782   public void testCut_withFeatures()
783   {
784     /*
785      * create sequence features before, after and overlapping
786      * a cut of columns/residues 4-7
787      */
788     SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
789     seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
790             null));
791     seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
792             0f, null));
793     seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
794             null));
795     seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
796             0f, null));
797     seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
798             null));
799
800     /*
801      * add some contact features
802      */
803     SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
804             6, 0f, null);
805     seq0.addSequenceFeature(internalContact); // should get deleted
806     SequenceFeature overlapLeftContact = new SequenceFeature(
807             "disulphide bond", "", 2, 6, 0f, null);
808     seq0.addSequenceFeature(overlapLeftContact); // should get deleted
809     SequenceFeature overlapRightContact = new SequenceFeature(
810             "disulphide bond", "", 5, 8, 0f, null);
811     seq0.addSequenceFeature(overlapRightContact); // should get deleted
812     SequenceFeature spanningContact = new SequenceFeature(
813             "disulphide bond", "", 2, 9, 0f, null);
814     seq0.addSequenceFeature(spanningContact); // should get shortened 3'
815
816     /*
817      * cut columns 3-6 (base 0), residues d-g 4-7
818      */
819     Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
820     EditCommand.cut(ec, new AlignmentI[] { al });
821
822     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
823     SequenceFeatures.sortFeatures(sfs, true);
824
825     assertEquals(5, sfs.size()); // features internal to cut were deleted
826     SequenceFeature sf = sfs.get(0);
827     assertEquals("before", sf.getType());
828     assertEquals(1, sf.getBegin());
829     assertEquals(3, sf.getEnd());
830     sf = sfs.get(1);
831     assertEquals("disulphide bond", sf.getType());
832     assertEquals(2, sf.getBegin());
833     assertEquals(5, sf.getEnd()); // truncated by cut
834     sf = sfs.get(2);
835     assertEquals("overlap left", sf.getType());
836     assertEquals(2, sf.getBegin());
837     assertEquals(3, sf.getEnd()); // truncated by cut
838     sf = sfs.get(3);
839     assertEquals("after", sf.getType());
840     assertEquals(4, sf.getBegin()); // shifted left by cut
841     assertEquals(6, sf.getEnd()); // shifted left by cut
842     sf = sfs.get(4);
843     assertEquals("overlap right", sf.getType());
844     assertEquals(4, sf.getBegin()); // shifted left by cut
845     assertEquals(4, sf.getEnd()); // truncated by cut
846   }
847
848   /**
849    * Test a cut action's relocation of sequence features, with full coverage of
850    * all possible feature and cut locations for a 5-position ungapped sequence
851    */
852   @Test(groups = { "Functional" })
853   public void testCut_withFeatures_exhaustive()
854   {
855     /*
856      * create a sequence features on each subrange of 1-5
857      */
858     SequenceI seq0 = new Sequence("seq", "ABCDE");
859     int start = 8;
860     int end = 12;
861     seq0.setStart(start);
862     seq0.setEnd(end);
863     AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
864     alignment.setDataset(null);
865
866     /*
867      * create a new alignment with shared dataset sequence
868      */
869     AlignmentI copy = new Alignment(
870             new SequenceI[]
871             { alignment.getDataset().getSequenceAt(0).deriveSequence() });
872     SequenceI copySeq0 = copy.getSequenceAt(0);
873
874     for (int from = start; from <= end; from++)
875     {
876       for (int to = from; to <= end; to++)
877       {
878         String desc = String.format("%d-%d", from, to);
879         SequenceFeature sf = new SequenceFeature("test", desc, from, to,
880                 0f, null);
881         sf.setValue("from", Integer.valueOf(from));
882         sf.setValue("to", Integer.valueOf(to));
883         seq0.addSequenceFeature(sf);
884       }
885     }
886     // sanity check
887     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
888     assertEquals(func(5), sfs.size());
889     assertEquals(sfs, copySeq0.getSequenceFeatures());
890     String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
891
892     /*
893      * now perform all possible cuts of subranges of columns 1-5
894      * and validate the resulting remaining sequence features!
895      */
896     SequenceI[] sqs = new SequenceI[] { seq0 };
897
898     for (int from = 0; from < seq0.getLength(); from++)
899     {
900       for (int to = from; to < seq0.getLength(); to++)
901       {
902         EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
903                 - from + 1), alignment);
904         final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
905         boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
906                 .getDatasetSequence();
907
908         verifyCut(seq0, from, to, msg, start);
909
910         /*
911          * verify copy alignment dataset sequence unaffected
912          */
913         assertEquals("Original dataset sequence was modified",
914                 copySequenceFeatures,
915                 copySeq0.getSequenceFeatures().toString());
916
917         /*
918          * verify any new dataset sequence was added to the
919          * alignment dataset
920          */
921         assertEquals("Wrong Dataset size after " + msg,
922                 newDatasetSequence ? 2 : 1,
923                 alignment.getDataset().getHeight());
924
925         /*
926          * undo and verify all restored
927          */
928         AlignmentI[] views = new AlignmentI[] { alignment };
929         ec.undoCommand(views);
930         sfs = seq0.getSequenceFeatures();
931         assertEquals("After undo of " + msg, func(5), sfs.size());
932         verifyUndo(from, to, sfs);
933
934         /*
935          * verify copy alignment dataset sequence still unaffected
936          * and alignment dataset has shrunk (if it was added to)
937          */
938         assertEquals("Original dataset sequence was modified",
939                 copySequenceFeatures,
940                 copySeq0.getSequenceFeatures().toString());
941         assertEquals("Wrong Dataset size after Undo of " + msg, 1,
942                 alignment.getDataset().getHeight());
943
944         /*
945          * redo and verify
946          */
947         ec.doCommand(views);
948         verifyCut(seq0, from, to, msg, start);
949
950         /*
951          * verify copy alignment dataset sequence unaffected
952          * and any new dataset sequence readded to alignment dataset
953          */
954         assertEquals("Original dataset sequence was modified",
955                 copySequenceFeatures,
956                 copySeq0.getSequenceFeatures().toString());
957         assertEquals("Wrong Dataset size after Redo of " + msg,
958                 newDatasetSequence ? 2 : 1,
959                 alignment.getDataset().getHeight());
960
961         /*
962          * undo ready for next cut
963          */
964         ec.undoCommand(views);
965
966         /*
967          * final verify that copy alignment dataset sequence is still unaffected
968          * and that alignment dataset has shrunk
969          */
970         assertEquals("Original dataset sequence was modified",
971                 copySequenceFeatures,
972                 copySeq0.getSequenceFeatures().toString());
973         assertEquals("Wrong Dataset size after final Undo of " + msg, 1,
974                 alignment.getDataset().getHeight());
975       }
976     }
977   }
978
979   /**
980    * Verify by inspection that the sequence features left on the sequence after
981    * a cut match the expected results. The trick to this is that we can parse
982    * each feature's original start-end positions from its description.
983    * 
984    * @param seq0
985    * @param from
986    * @param to
987    * @param msg
988    * @param seqStart
989    */
990   protected void verifyCut(SequenceI seq0, int from, int to,
991           final String msg, int seqStart)
992   {
993     List<SequenceFeature> sfs;
994     sfs = seq0.getSequenceFeatures();
995
996     Collections.sort(sfs, BY_DESCRIPTION);
997
998     /*
999      * confirm the number of features has reduced by the
1000      * number of features within the cut region i.e. by
1001      * func(length of cut); exception is a cut at start or end of sequence, 
1002      * which retains the original coordinates, dataset sequence 
1003      * and all its features
1004      */
1005     boolean datasetRetained = from == 0 || to == 4;
1006     if (datasetRetained)
1007     {
1008       // dataset and all features retained
1009       assertEquals(msg, func(5), sfs.size());
1010     }
1011     else if (to - from == 4)
1012     {
1013       // all columns were cut
1014       assertTrue(sfs.isEmpty());
1015     }
1016     else
1017     {
1018       // failure in checkFeatureRelocation is more informative!
1019       assertEquals(msg + "wrong number of features left", func(5)
1020               - func(to - from + 1), sfs.size());
1021     }
1022
1023     /*
1024      * inspect individual features
1025      */
1026     for (SequenceFeature sf : sfs)
1027     {
1028       verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
1029               seqStart);
1030     }
1031   }
1032
1033   /**
1034    * Check that after Undo, every feature has start/end that match its original
1035    * "start" and "end" properties
1036    * 
1037    * @param from
1038    * @param to
1039    * @param sfs
1040    */
1041   protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
1042   {
1043     for (SequenceFeature sf : sfs)
1044     {
1045       final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1046       final int oldTo = ((Integer) sf.getValue("to")).intValue();
1047       String msg = String.format(
1048               "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
1049               oldFrom, oldTo);
1050       assertEquals(msg + "start", oldFrom, sf.getBegin());
1051       assertEquals(msg + "end", oldTo, sf.getEnd());
1052     }
1053   }
1054
1055   /**
1056    * Helper method to check a feature has been correctly relocated after a cut
1057    * 
1058    * @param sf
1059    * @param from
1060    *          start of cut (first residue cut 1..)
1061    * @param to
1062    *          end of cut (last residue cut 1..)
1063    * @param newDataset
1064    * @param seqStart
1065    */
1066   private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1067  boolean newDataset, int seqStart)
1068   {
1069     // TODO handle the gapped sequence case as well
1070     int cutSize = to - from + 1;
1071     final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1072     final int oldTo = ((Integer) sf.getValue("to")).intValue();
1073     final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1074     final int oldToPosition = oldTo - seqStart + 1; // 1..
1075
1076     String msg = String.format(
1077             "Feature %s relocated to %d-%d after cut of %d-%d",
1078             sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1079     if (!newDataset)
1080     {
1081       // dataset retained with all features unchanged
1082       assertEquals("0: " + msg, oldFrom, sf.getBegin());
1083       assertEquals("0: " + msg, oldTo, sf.getEnd());
1084     }
1085     else if (oldToPosition < from)
1086     {
1087       // before cut region so unchanged
1088       assertEquals("1: " + msg, oldFrom, sf.getBegin());
1089       assertEquals("2: " + msg, oldTo, sf.getEnd());
1090     }
1091     else if (oldFromPosition > to)
1092     {
1093       // follows cut region - shift by size of cut
1094       assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1095               sf.getBegin());
1096       assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1097               sf.getEnd());
1098     }
1099     else if (oldFromPosition < from && oldToPosition > to)
1100     {
1101       // feature encloses cut region - shrink it right
1102       assertEquals("5: " + msg, oldFrom, sf.getBegin());
1103       assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1104     }
1105     else if (oldFromPosition < from)
1106     {
1107       // feature overlaps left side of cut region - truncated right
1108       assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1109     }
1110     else if (oldToPosition > to)
1111     {
1112       // feature overlaps right side of cut region - truncated left
1113       assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1114               sf.getBegin());
1115       assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1116               sf.getEnd());
1117     }
1118     else
1119     {
1120       // feature internal to cut - should have been deleted!
1121       Assert.fail(msg + " - should have been deleted");
1122     }
1123   }
1124
1125   /**
1126    * Test a cut action's relocation of sequence features
1127    */
1128   @Test(groups = { "Functional" })
1129   public void testCut_withFeatures5prime()
1130   {
1131     SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1132     seq0.createDatasetSequence();
1133     assertEquals(8, seq0.getStart());
1134     seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
1135             null));
1136     SequenceI[] seqsArray = new SequenceI[] { seq0 };
1137     AlignmentI alignment = new Alignment(seqsArray);
1138
1139     /*
1140      * cut columns of A-B; same dataset sequence is retained, aligned sequence
1141      * start becomes 10
1142      */
1143     Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1144     EditCommand.cut(ec, new AlignmentI[] { alignment });
1145   
1146     /*
1147      * feature on CC(10-11) should still be on CC(10-11)
1148      */
1149     assertSame(seq0, alignment.getSequenceAt(0));
1150     assertEquals(10, seq0.getStart());
1151     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1152     assertEquals(1, sfs.size());
1153     SequenceFeature sf = sfs.get(0);
1154     assertEquals(10, sf.getBegin());
1155     assertEquals(11, sf.getEnd());
1156   }
1157 }