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