JAL-2480 move ContiguousI and Range to jalview.datamodel
[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.assertNull;
25 import static org.testng.AssertJUnit.assertSame;
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.List;
38 import java.util.Map;
39
40 import org.testng.Assert;
41 import org.testng.annotations.BeforeClass;
42 import org.testng.annotations.BeforeMethod;
43 import org.testng.annotations.Test;
44
45 /**
46  * Unit tests for EditCommand
47  * 
48  * @author gmcarstairs
49  *
50  */
51 public class EditCommandTest
52 {
53   /*
54    * compute n(n+1)/2 e.g. 
55    * func(5) = 5 + 4 + 3 + 2 + 1 = 15
56    */
57   private static int func(int i)
58   {
59     return i * (i + 1) / 2;
60   }
61
62   @BeforeClass(alwaysRun = true)
63   public void setUpJvOptionPane()
64   {
65     JvOptionPane.setInteractiveMode(false);
66     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
67   }
68
69   private EditCommand testee;
70
71   private SequenceI[] seqs;
72
73   private Alignment al;
74
75   @BeforeMethod(alwaysRun = true)
76   public void setUp()
77   {
78     testee = new EditCommand();
79     seqs = new SequenceI[4];
80     seqs[0] = new Sequence("seq0", "abcdefghjk");
81     seqs[0].setDatasetSequence(new Sequence("seq0ds", "abcdefghjk"));
82     seqs[1] = new Sequence("seq1", "fghjklmnopq");
83     seqs[1].setDatasetSequence(new Sequence("seq1ds", "fghjklmnopq"));
84     seqs[2] = new Sequence("seq2", "qrstuvwxyz");
85     seqs[2].setDatasetSequence(new Sequence("seq2ds", "qrstuvwxyz"));
86     seqs[3] = new Sequence("seq3", "1234567890");
87     seqs[3].setDatasetSequence(new Sequence("seq3ds", "1234567890"));
88     al = new Alignment(seqs);
89     al.setGapCharacter('?');
90   }
91
92   /**
93    * Test inserting gap characters
94    */
95   @Test(groups = { "Functional" })
96   public void testAppendEdit_insertGap()
97   {
98     // set a non-standard gap character to prove it is actually used
99     testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true);
100     assertEquals("abcd???efghjk", seqs[0].getSequenceAsString());
101     assertEquals("fghj???klmnopq", seqs[1].getSequenceAsString());
102     assertEquals("qrst???uvwxyz", seqs[2].getSequenceAsString());
103     assertEquals("1234???567890", seqs[3].getSequenceAsString());
104
105     // todo: test for handling out of range positions?
106   }
107
108   /**
109    * Test deleting characters from sequences. Note the deleteGap() action does
110    * not check that only gap characters are being removed.
111    */
112   @Test(groups = { "Functional" })
113   public void testAppendEdit_deleteGap()
114   {
115     testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true);
116     assertEquals("abcdhjk", seqs[0].getSequenceAsString());
117     assertEquals("fghjnopq", seqs[1].getSequenceAsString());
118     assertEquals("qrstxyz", seqs[2].getSequenceAsString());
119     assertEquals("1234890", seqs[3].getSequenceAsString());
120   }
121
122   /**
123    * Test a cut action. The command should store the cut characters to support
124    * undo.
125    */
126   @Test(groups = { "Functional" })
127   public void testCut()
128   {
129     Edit ec = testee.new Edit(Action.CUT, seqs, 4, 3, al);
130     EditCommand.cut(ec, new AlignmentI[] { al });
131     assertEquals("abcdhjk", seqs[0].getSequenceAsString());
132     assertEquals("fghjnopq", seqs[1].getSequenceAsString());
133     assertEquals("qrstxyz", seqs[2].getSequenceAsString());
134     assertEquals("1234890", seqs[3].getSequenceAsString());
135
136     assertEquals("efg", new String(ec.string[0]));
137     assertEquals("klm", new String(ec.string[1]));
138     assertEquals("uvw", new String(ec.string[2]));
139     assertEquals("567", new String(ec.string[3]));
140     // TODO: case where whole sequence is deleted as nothing left; etc
141   }
142
143   /**
144    * Test a Paste action, where this adds sequences to an alignment.
145    */
146   @Test(groups = { "Functional" }, enabled = false)
147   // TODO fix so it works
148   public void testPaste_addToAlignment()
149   {
150     SequenceI[] newSeqs = new SequenceI[2];
151     newSeqs[0] = new Sequence("newseq0", "ACEFKL");
152     newSeqs[1] = new Sequence("newseq1", "JWMPDH");
153
154     Edit ec = testee.new Edit(Action.PASTE, newSeqs, 0, al.getWidth(), al);
155     EditCommand.paste(ec, new AlignmentI[] { al });
156     assertEquals(6, al.getSequences().size());
157     assertEquals("1234567890", seqs[3].getSequenceAsString());
158     assertEquals("ACEFKL", seqs[4].getSequenceAsString());
159     assertEquals("JWMPDH", seqs[5].getSequenceAsString());
160   }
161
162   /**
163    * Test insertGap followed by undo command
164    */
165   @Test(groups = { "Functional" })
166   public void testUndo_insertGap()
167   {
168     // Edit ec = testee.new Edit(Action.INSERT_GAP, seqs, 4, 3, '?');
169     testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true);
170     // check something changed
171     assertEquals("abcd???efghjk", seqs[0].getSequenceAsString());
172     testee.undoCommand(new AlignmentI[] { al });
173     assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
174     assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
175     assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
176     assertEquals("1234567890", seqs[3].getSequenceAsString());
177   }
178
179   /**
180    * Test deleteGap followed by undo command
181    */
182   @Test(groups = { "Functional" })
183   public void testUndo_deleteGap()
184   {
185     testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true);
186     // check something changed
187     assertEquals("abcdhjk", seqs[0].getSequenceAsString());
188     testee.undoCommand(new AlignmentI[] { al });
189     // deleteGap doesn't 'remember' deleted characters, only gaps get put back
190     assertEquals("abcd???hjk", seqs[0].getSequenceAsString());
191     assertEquals("fghj???nopq", seqs[1].getSequenceAsString());
192     assertEquals("qrst???xyz", seqs[2].getSequenceAsString());
193     assertEquals("1234???890", seqs[3].getSequenceAsString());
194   }
195
196   /**
197    * Test several commands followed by an undo command
198    */
199   @Test(groups = { "Functional" })
200   public void testUndo_multipleCommands()
201   {
202     // delete positions 3/4/5 (counting from 1)
203     testee.appendEdit(Action.DELETE_GAP, seqs, 2, 3, al, true);
204     assertEquals("abfghjk", seqs[0].getSequenceAsString());
205     assertEquals("1267890", seqs[3].getSequenceAsString());
206
207     // insert 2 gaps after the second residue
208     testee.appendEdit(Action.INSERT_GAP, seqs, 2, 2, al, true);
209     assertEquals("ab??fghjk", seqs[0].getSequenceAsString());
210     assertEquals("12??67890", seqs[3].getSequenceAsString());
211
212     // delete positions 4/5/6
213     testee.appendEdit(Action.DELETE_GAP, seqs, 3, 3, al, true);
214     assertEquals("ab?hjk", seqs[0].getSequenceAsString());
215     assertEquals("12?890", seqs[3].getSequenceAsString());
216
217     // undo edit commands
218     testee.undoCommand(new AlignmentI[] { al });
219     assertEquals("ab?????hjk", seqs[0].getSequenceAsString());
220     assertEquals("12?????890", seqs[3].getSequenceAsString());
221   }
222
223   /**
224    * Unit test for JAL-1594 bug: click and drag sequence right to insert gaps -
225    * undo did not remove them all.
226    */
227   @Test(groups = { "Functional" })
228   public void testUndo_multipleInsertGaps()
229   {
230     testee.appendEdit(Action.INSERT_GAP, seqs, 4, 1, al, true);
231     testee.appendEdit(Action.INSERT_GAP, seqs, 5, 1, al, true);
232     testee.appendEdit(Action.INSERT_GAP, seqs, 6, 1, al, true);
233
234     // undo edit commands
235     testee.undoCommand(new AlignmentI[] { al });
236     assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
237     assertEquals("1234567890", seqs[3].getSequenceAsString());
238
239   }
240
241   /**
242    * Test cut followed by undo command
243    */
244   @Test(groups = { "Functional" })
245   public void testUndo_cut()
246   {
247     testee.appendEdit(Action.CUT, seqs, 4, 3, al, true);
248     // check something changed
249     assertEquals("abcdhjk", seqs[0].getSequenceAsString());
250     testee.undoCommand(new AlignmentI[] { al });
251     assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
252     assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
253     assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
254     assertEquals("1234567890", seqs[3].getSequenceAsString());
255   }
256
257   /**
258    * Test the replace command (used to manually edit a sequence)
259    */
260   @Test(groups = { "Functional" })
261   public void testReplace()
262   {
263     // seem to need a dataset sequence on the edited sequence here
264     seqs[1].createDatasetSequence();
265     new EditCommand("", Action.REPLACE, "ZXY", new SequenceI[] { seqs[1] },
266             4, 8, al);
267     assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
268     assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
269     assertEquals("1234567890", seqs[3].getSequenceAsString());
270     seqs[1] = new Sequence("seq1", "fghjZXYnopq");
271   }
272
273   /**
274    * Test that the addEdit command correctly merges insert gap commands when
275    * possible.
276    */
277   @Test(groups = { "Functional" })
278   public void testAddEdit_multipleInsertGap()
279   {
280     /*
281      * 3 insert gap in a row (aka mouse drag right):
282      */
283     Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
284             new SequenceI[] { seqs[0] }, 1, 1, al);
285     testee.addEdit(e);
286     SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
287     edited.setDatasetSequence(seqs[0].getDatasetSequence());
288     e = new EditCommand().new Edit(Action.INSERT_GAP,
289             new SequenceI[] { edited }, 2, 1, al);
290     testee.addEdit(e);
291     edited = new Sequence("seq0", "a??bcdefghjk");
292     edited.setDatasetSequence(seqs[0].getDatasetSequence());
293     e = new EditCommand().new Edit(Action.INSERT_GAP,
294             new SequenceI[] { edited }, 3, 1, al);
295     testee.addEdit(e);
296     assertEquals(1, testee.getSize());
297     assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
298     assertEquals(1, testee.getEdit(0).getPosition());
299     assertEquals(3, testee.getEdit(0).getNumber());
300
301     /*
302      * Add a non-contiguous edit - should not be merged.
303      */
304     e = new EditCommand().new Edit(Action.INSERT_GAP,
305             new SequenceI[] { edited }, 5, 2, al);
306     testee.addEdit(e);
307     assertEquals(2, testee.getSize());
308     assertEquals(5, testee.getEdit(1).getPosition());
309     assertEquals(2, testee.getEdit(1).getNumber());
310
311     /*
312      * Add a Delete after the Insert - should not be merged.
313      */
314     e = new EditCommand().new Edit(Action.DELETE_GAP,
315             new SequenceI[] { edited }, 6, 2, al);
316     testee.addEdit(e);
317     assertEquals(3, testee.getSize());
318     assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
319     assertEquals(6, testee.getEdit(2).getPosition());
320     assertEquals(2, testee.getEdit(2).getNumber());
321   }
322
323   /**
324    * Test that the addEdit command correctly merges delete gap commands when
325    * possible.
326    */
327   @Test(groups = { "Functional" })
328   public void testAddEdit_multipleDeleteGap()
329   {
330     /*
331      * 3 delete gap in a row (aka mouse drag left):
332      */
333     seqs[0].setSequence("a???bcdefghjk");
334     Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
335             new SequenceI[] { seqs[0] }, 4, 1, al);
336     testee.addEdit(e);
337     assertEquals(1, testee.getSize());
338
339     SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
340     edited.setDatasetSequence(seqs[0].getDatasetSequence());
341     e = new EditCommand().new Edit(Action.DELETE_GAP,
342             new SequenceI[] { edited }, 3, 1, al);
343     testee.addEdit(e);
344     assertEquals(1, testee.getSize());
345
346     edited = new Sequence("seq0", "a?bcdefghjk");
347     edited.setDatasetSequence(seqs[0].getDatasetSequence());
348     e = new EditCommand().new Edit(Action.DELETE_GAP,
349             new SequenceI[] { edited }, 2, 1, al);
350     testee.addEdit(e);
351     assertEquals(1, testee.getSize());
352     assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
353     assertEquals(2, testee.getEdit(0).getPosition());
354     assertEquals(3, testee.getEdit(0).getNumber());
355
356     /*
357      * Add a non-contiguous edit - should not be merged.
358      */
359     e = new EditCommand().new Edit(Action.DELETE_GAP,
360             new SequenceI[] { edited }, 2, 1, al);
361     testee.addEdit(e);
362     assertEquals(2, testee.getSize());
363     assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
364     assertEquals(2, testee.getEdit(1).getPosition());
365     assertEquals(1, testee.getEdit(1).getNumber());
366
367     /*
368      * Add an Insert after the Delete - should not be merged.
369      */
370     e = new EditCommand().new Edit(Action.INSERT_GAP,
371             new SequenceI[] { edited }, 1, 1, al);
372     testee.addEdit(e);
373     assertEquals(3, testee.getSize());
374     assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
375     assertEquals(1, testee.getEdit(2).getPosition());
376     assertEquals(1, testee.getEdit(2).getNumber());
377   }
378
379   /**
380    * Test that the addEdit command correctly handles 'remove gaps' edits for the
381    * case when they appear contiguous but are acting on different sequences.
382    * They should not be merged.
383    */
384   @Test(groups = { "Functional" })
385   public void testAddEdit_removeAllGaps()
386   {
387     seqs[0].setSequence("a???bcdefghjk");
388     Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
389             new SequenceI[] { seqs[0] }, 4, 1, al);
390     testee.addEdit(e);
391
392     seqs[1].setSequence("f??ghjklmnopq");
393     Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[]
394     { seqs[1] }, 3, 1, al);
395     testee.addEdit(e2);
396     assertEquals(2, testee.getSize());
397     assertSame(e, testee.getEdit(0));
398     assertSame(e2, testee.getEdit(1));
399   }
400
401   /**
402    * Test that the addEdit command correctly merges insert gap commands acting
403    * on a multi-sequence selection.
404    */
405   @Test(groups = { "Functional" })
406   public void testAddEdit_groupInsertGaps()
407   {
408     /*
409      * 2 insert gap in a row (aka mouse drag right), on two sequences:
410      */
411     Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
412         seqs[0], seqs[1] }, 1, 1, al);
413     testee.addEdit(e);
414     SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
415     seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
416     SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
417     seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
418     e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
419         seq1edited, seq2edited }, 2, 1, al);
420     testee.addEdit(e);
421
422     assertEquals(1, testee.getSize());
423     assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
424     assertEquals(1, testee.getEdit(0).getPosition());
425     assertEquals(2, testee.getEdit(0).getNumber());
426     assertEquals(seqs[0].getDatasetSequence(), testee.getEdit(0)
427             .getSequences()[0].getDatasetSequence());
428     assertEquals(seqs[1].getDatasetSequence(), testee.getEdit(0)
429             .getSequences()[1].getDatasetSequence());
430   }
431
432   /**
433    * Test for 'undoing' a series of gap insertions.
434    * <ul>
435    * <li>Start: ABCDEF insert 2 at pos 1</li>
436    * <li>next: A--BCDEF insert 1 at pos 4</li>
437    * <li>next: A--B-CDEF insert 2 at pos 0</li>
438    * <li>last: --A--B-CDEF</li>
439    * </ul>
440    */
441   @Test(groups = { "Functional" })
442   public void testPriorState_multipleInserts()
443   {
444     EditCommand command = new EditCommand();
445     SequenceI seq = new Sequence("", "--A--B-CDEF");
446     SequenceI ds = new Sequence("", "ABCDEF");
447     seq.setDatasetSequence(ds);
448     SequenceI[] sqs = new SequenceI[] { seq };
449     Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
450     command.addEdit(e);
451     e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
452     command.addEdit(e);
453     e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
454     command.addEdit(e);
455
456     Map<SequenceI, SequenceI> unwound = command.priorState(false);
457     assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
458   }
459
460   /**
461    * Test for 'undoing' a series of gap deletions.
462    * <ul>
463    * <li>Start: A-B-C delete 1 at pos 1</li>
464    * <li>Next: AB-C delete 1 at pos 2</li>
465    * <li>End: ABC</li>
466    * </ul>
467    */
468   @Test(groups = { "Functional" })
469   public void testPriorState_removeAllGaps()
470   {
471     EditCommand command = new EditCommand();
472     SequenceI seq = new Sequence("", "ABC");
473     SequenceI ds = new Sequence("", "ABC");
474     seq.setDatasetSequence(ds);
475     SequenceI[] sqs = new SequenceI[] { seq };
476     Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
477     command.addEdit(e);
478     e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
479     command.addEdit(e);
480
481     Map<SequenceI, SequenceI> unwound = command.priorState(false);
482     assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
483   }
484
485   /**
486    * Test for 'undoing' a single delete edit.
487    */
488   @Test(groups = { "Functional" })
489   public void testPriorState_singleDelete()
490   {
491     EditCommand command = new EditCommand();
492     SequenceI seq = new Sequence("", "ABCDEF");
493     SequenceI ds = new Sequence("", "ABCDEF");
494     seq.setDatasetSequence(ds);
495     SequenceI[] sqs = new SequenceI[] { seq };
496     Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
497     command.addEdit(e);
498
499     Map<SequenceI, SequenceI> unwound = command.priorState(false);
500     assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
501   }
502
503   /**
504    * Test 'undoing' a single gap insertion edit command.
505    */
506   @Test(groups = { "Functional" })
507   public void testPriorState_singleInsert()
508   {
509     EditCommand command = new EditCommand();
510     SequenceI seq = new Sequence("", "AB---CDEF");
511     SequenceI ds = new Sequence("", "ABCDEF");
512     seq.setDatasetSequence(ds);
513     SequenceI[] sqs = new SequenceI[] { seq };
514     Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
515     command.addEdit(e);
516
517     Map<SequenceI, SequenceI> unwound = command.priorState(false);
518     SequenceI prior = unwound.get(ds);
519     assertEquals("ABCDEF", prior.getSequenceAsString());
520     assertEquals(1, prior.getStart());
521     assertEquals(6, prior.getEnd());
522   }
523
524   /**
525    * Test 'undoing' a single gap insertion edit command, on a sequence whose
526    * start residue is other than 1
527    */
528   @Test(groups = { "Functional" })
529   public void testPriorState_singleInsertWithOffset()
530   {
531     EditCommand command = new EditCommand();
532     SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
533     // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
534     // seq.setDatasetSequence(ds);
535     seq.createDatasetSequence();
536     SequenceI[] sqs = new SequenceI[] { seq };
537     Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
538     command.addEdit(e);
539
540     Map<SequenceI, SequenceI> unwound = command.priorState(false);
541     SequenceI prior = unwound.get(seq.getDatasetSequence());
542     assertEquals("ABCDEF", prior.getSequenceAsString());
543     assertEquals(8, prior.getStart());
544     assertEquals(13, prior.getEnd());
545   }
546
547   /**
548    * Test that mimics 'remove all gaps' action. This generates delete gap edits
549    * for contiguous gaps in each sequence separately.
550    */
551   @Test(groups = { "Functional" })
552   public void testPriorState_removeGapsMultipleSeqs()
553   {
554     EditCommand command = new EditCommand();
555     String original1 = "--ABC-DEF";
556     String original2 = "FG-HI--J";
557     String original3 = "M-NOPQ";
558
559     /*
560      * Two edits for the first sequence
561      */
562     SequenceI seq = new Sequence("", "ABC-DEF");
563     SequenceI ds1 = new Sequence("", "ABCDEF");
564     seq.setDatasetSequence(ds1);
565     SequenceI[] sqs = new SequenceI[] { seq };
566     Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
567     command.addEdit(e);
568     seq = new Sequence("", "ABCDEF");
569     seq.setDatasetSequence(ds1);
570     sqs = new SequenceI[] { seq };
571     e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
572     command.addEdit(e);
573
574     /*
575      * Two edits for the second sequence
576      */
577     seq = new Sequence("", "FGHI--J");
578     SequenceI ds2 = new Sequence("", "FGHIJ");
579     seq.setDatasetSequence(ds2);
580     sqs = new SequenceI[] { seq };
581     e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
582     command.addEdit(e);
583     seq = new Sequence("", "FGHIJ");
584     seq.setDatasetSequence(ds2);
585     sqs = new SequenceI[] { seq };
586     e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
587     command.addEdit(e);
588
589     /*
590      * One edit for the third sequence.
591      */
592     seq = new Sequence("", "MNOPQ");
593     SequenceI ds3 = new Sequence("", "MNOPQ");
594     seq.setDatasetSequence(ds3);
595     sqs = new SequenceI[] { seq };
596     e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
597     command.addEdit(e);
598
599     Map<SequenceI, SequenceI> unwound = command.priorState(false);
600     assertEquals(original1, unwound.get(ds1).getSequenceAsString());
601     assertEquals(original2, unwound.get(ds2).getSequenceAsString());
602     assertEquals(original3, unwound.get(ds3).getSequenceAsString());
603   }
604
605   /**
606    * Test that mimics 'remove all gapped columns' action. This generates a
607    * series Delete Gap edits that each act on all sequences that share a gapped
608    * column region.
609    */
610   @Test(groups = { "Functional" })
611   public void testPriorState_removeGappedCols()
612   {
613     EditCommand command = new EditCommand();
614     String original1 = "--ABC--DEF";
615     String original2 = "-G-HI--J";
616     String original3 = "-M-NO--PQ";
617
618     /*
619      * First edit deletes the first column.
620      */
621     SequenceI seq1 = new Sequence("", "-ABC--DEF");
622     SequenceI ds1 = new Sequence("", "ABCDEF");
623     seq1.setDatasetSequence(ds1);
624     SequenceI seq2 = new Sequence("", "G-HI--J");
625     SequenceI ds2 = new Sequence("", "GHIJ");
626     seq2.setDatasetSequence(ds2);
627     SequenceI seq3 = new Sequence("", "M-NO--PQ");
628     SequenceI ds3 = new Sequence("", "MNOPQ");
629     seq3.setDatasetSequence(ds3);
630     SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
631     Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
632     command.addEdit(e);
633
634     /*
635      * Second edit deletes what is now columns 4 and 5.
636      */
637     seq1 = new Sequence("", "-ABCDEF");
638     seq1.setDatasetSequence(ds1);
639     seq2 = new Sequence("", "G-HIJ");
640     seq2.setDatasetSequence(ds2);
641     seq3 = new Sequence("", "M-NOPQ");
642     seq3.setDatasetSequence(ds3);
643     sqs = new SequenceI[] { seq1, seq2, seq3 };
644     e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
645     command.addEdit(e);
646
647     Map<SequenceI, SequenceI> unwound = command.priorState(false);
648     assertEquals(original1, unwound.get(ds1).getSequenceAsString());
649     assertEquals(original2, unwound.get(ds2).getSequenceAsString());
650     assertEquals(original3, unwound.get(ds3).getSequenceAsString());
651     assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
652     assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
653     assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
654   }
655
656   /**
657    * Test a cut action's relocation of sequence features
658    */
659   @Test(groups = { "Functional" })
660   public void testCut_withFeatures()
661   {
662     /*
663      * create sequence features before, after and overlapping
664      * a cut of columns/residues 4-7
665      */
666     SequenceI seq0 = seqs[0];
667     seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
668             null));
669     seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
670             0f, null));
671     seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
672             null));
673     seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
674             0f, null));
675     seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
676             null));
677
678     Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
679     EditCommand.cut(ec, new AlignmentI[] { al });
680
681     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
682     SequenceFeatures.sortFeatures(sfs, true);
683
684     assertEquals(4, sfs.size()); // feature internal to cut has been deleted
685     SequenceFeature sf = sfs.get(0);
686     assertEquals("before", sf.getType());
687     assertEquals(1, sf.getBegin());
688     assertEquals(3, sf.getEnd());
689     sf = sfs.get(1);
690     assertEquals("overlap left", sf.getType());
691     assertEquals(2, sf.getBegin());
692     assertEquals(3, sf.getEnd()); // truncated by cut
693     sf = sfs.get(2);
694     assertEquals("overlap right", sf.getType());
695     assertEquals(4, sf.getBegin()); // shifted left by cut
696     assertEquals(5, sf.getEnd()); // truncated by cut
697     sf = sfs.get(3);
698     assertEquals("after", sf.getType());
699     assertEquals(4, sf.getBegin()); // shifted left by cut
700     assertEquals(6, sf.getEnd()); // shifted left by cut
701   }
702
703   /**
704    * Test a cut action's relocation of sequence features, with full coverage of
705    * all possible feature and cut locations for a 5-position ungapped sequence
706    */
707   @Test(groups = { "Functional" })
708   public void testCut_withFeatures_exhaustive()
709   {
710     /*
711      * create a sequence features on each subrange of 1-5
712      */
713     SequenceI seq0 = new Sequence("seq", "ABCDE");
714     AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
715     alignment.setDataset(null);
716     for (int from = 1; from <= seq0.getLength(); from++)
717     {
718       for (int to = from; to <= seq0.getLength(); to++)
719       {
720         String desc = String.format("%d-%d", from, to);
721         SequenceFeature sf = new SequenceFeature("test", desc, from, to,
722                 0f,
723                 null);
724         sf.setValue("from", Integer.valueOf(from));
725         sf.setValue("to", Integer.valueOf(to));
726         seq0.addSequenceFeature(sf);
727       }
728     }
729     // sanity check
730     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
731     assertEquals(func(5), sfs.size());
732
733     /*
734      * now perform all possible cuts of subranges of 1-5 (followed by Undo)
735      * and validate the resulting remaining sequence features!
736      */
737     SequenceI[] sqs = new SequenceI[] { seq0 };
738
739     // goal is to have this passing for all from/to values!!
740     // for (int from = 0; from < seq0.getLength(); from++)
741     // {
742     // for (int to = from; to < seq0.getLength(); to++)
743     for (int from = 1; from < 3; from++)
744     {
745       for (int to = 2; to < 3; to++)
746       {
747         testee.appendEdit(Action.CUT, sqs, from, (to - from + 1),
748                 alignment, true);
749
750         sfs = seq0.getSequenceFeatures();
751
752         /*
753          * confirm the number of features has reduced by the
754          * number of features within the cut region i.e. by
755          * func(length of cut)
756          */
757         String msg = String.format("Cut %d-%d ", from, to);
758         if (to - from == 4)
759         {
760           // all columns cut
761           assertNull(sfs);
762         }
763         else
764         {
765           assertEquals(msg + "wrong number of features left", func(5)
766                   - func(to - from + 1), sfs.size());
767         }
768
769         /*
770          * inspect individual features
771          */
772         if (sfs != null)
773         {
774           for (SequenceFeature sf : sfs)
775           {
776             checkFeatureRelocation(sf, from + 1, to + 1);
777           }
778         }
779         /*
780          * undo ready for next cut
781          */
782         testee.undoCommand(new AlignmentI[] { alignment });
783         assertEquals(func(5), seq0.getSequenceFeatures().size());
784       }
785     }
786   }
787
788   /**
789    * Helper method to check a feature has been correctly relocated after a cut
790    * 
791    * @param sf
792    * @param from
793    *          start of cut (first residue cut)
794    * @param to
795    *          end of cut (last residue cut)
796    */
797   private void checkFeatureRelocation(SequenceFeature sf, int from, int to)
798   {
799     // TODO handle the gapped sequence case as well
800     int cutSize = to - from + 1;
801     int oldFrom = ((Integer) sf.getValue("from")).intValue();
802     int oldTo = ((Integer) sf.getValue("to")).intValue();
803
804     String msg = String.format(
805             "Feature %s relocated to %d-%d after cut of %d-%d",
806             sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
807     if (oldTo < from)
808     {
809       // before cut region so unchanged
810       assertEquals("1: " + msg, oldFrom, sf.getBegin());
811       assertEquals("2: " + msg, oldTo, sf.getEnd());
812     }
813     else if (oldFrom > to)
814     {
815       // follows cut region - shift by size of cut
816       assertEquals("3: " + msg, oldFrom - cutSize, sf.getBegin());
817       assertEquals("4: " + msg, oldTo - cutSize, sf.getEnd());
818     }
819     else if (oldFrom < from && oldTo > to)
820     {
821       // feature encloses cut region - shrink it right
822       assertEquals("5: " + msg, oldFrom, sf.getBegin());
823       assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
824     }
825     else if (oldFrom < from)
826     {
827       // feature overlaps left side of cut region - truncated right
828       assertEquals("7: " + msg, from - 1, sf.getEnd());
829     }
830     else if (oldTo > to)
831     {
832       // feature overlaps right side of cut region - truncated left
833       assertEquals("8: " + msg, from, sf.getBegin());
834       assertEquals("9: " + msg, from + oldTo - to - 1, sf.getEnd());
835     }
836     else
837     {
838       // feature internal to cut - should have been deleted!
839       Assert.fail(msg + " - should have been deleted");
840     }
841   }
842
843   /**
844    * Test a cut action's relocation of sequence features
845    */
846   @Test(groups = { "Functional" })
847   public void testCut_gappedWithFeatures()
848   {
849     /*
850      * create sequence features before, after and overlapping
851      * a cut of columns/residues 4-7
852      */
853     SequenceI seq0 = new Sequence("seq", "A-BCC");
854     seq0.addSequenceFeature(new SequenceFeature("", "", 3, 4, 0f,
855             null));
856     AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
857     // cut columns of A-B
858     Edit ec = testee.new Edit(Action.CUT, seqs, 0, 3, alignment); // cols 0-3
859                                                                   // base 0
860     EditCommand.cut(ec, new AlignmentI[] { alignment });
861   
862     /*
863      * feature on CC(3-4) should now be on CC(1-2)
864      */
865     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
866     assertEquals(1, sfs.size());
867     SequenceFeature sf = sfs.get(0);
868     assertEquals(1, sf.getBegin());
869     assertEquals(2, sf.getEnd());
870
871     // TODO add further cases including Undo - see JAL-2541
872   }
873 }