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