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