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