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