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