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