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