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