2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
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.
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.
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.
21 package jalview.commands;
23 import static org.testng.AssertJUnit.assertEquals;
24 import static org.testng.AssertJUnit.assertSame;
25 import static org.testng.AssertJUnit.assertTrue;
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;
37 import java.util.Collections;
38 import java.util.Comparator;
39 import java.util.List;
42 import org.testng.Assert;
43 import org.testng.annotations.BeforeClass;
44 import org.testng.annotations.BeforeMethod;
45 import org.testng.annotations.Test;
48 * Unit tests for EditCommand
53 public class EditCommandTest
55 private static Comparator<SequenceFeature> BY_DESCRIPTION = new Comparator<SequenceFeature>()
59 public int compare(SequenceFeature o1, SequenceFeature o2)
61 return o1.getDescription().compareTo(o2.getDescription());
65 private EditCommand testee;
67 private SequenceI[] seqs;
72 * compute n(n+1)/2 e.g.
73 * func(5) = 5 + 4 + 3 + 2 + 1 = 15
75 private static int func(int i)
77 return i * (i + 1) / 2;
80 @BeforeClass(alwaysRun = true)
81 public void setUpJvOptionPane()
83 JvOptionPane.setInteractiveMode(false);
84 JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
87 @BeforeMethod(alwaysRun = true)
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('?');
105 * Test inserting gap characters
107 @Test(groups = { "Functional" })
108 public void testAppendEdit_insertGap()
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());
117 // todo: test for handling out of range positions?
121 * Test deleting characters from sequences. Note the deleteGap() action does
122 * not check that only gap characters are being removed.
124 @Test(groups = { "Functional" })
125 public void testAppendEdit_deleteGap()
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());
135 * Test a cut action. The command should store the cut characters to support
138 @Test(groups = { "Functional" })
139 public void testCut()
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());
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
156 * Test a Paste action, followed by Undo and Redo
158 @Test(groups = { "Functional" }, enabled = false)
159 public void testPaste_undo_redo()
161 // TODO code this test properly, bearing in mind that:
162 // Paste action requires something on the clipboard (Cut/Copy)
163 // - EditCommand.paste doesn't add sequences to the alignment
164 // ... that is done in AlignFrame.paste()
165 // ... unless as a Redo
168 SequenceI[] newSeqs = new SequenceI[2];
169 newSeqs[0] = new Sequence("newseq0", "ACEFKL");
170 newSeqs[1] = new Sequence("newseq1", "JWMPDH");
172 new EditCommand("Paste", Action.PASTE, newSeqs, 0, al.getWidth(), al);
173 assertEquals(6, al.getSequences().size());
174 assertEquals("1234567890", seqs[3].getSequenceAsString());
175 assertEquals("ACEFKL", seqs[4].getSequenceAsString());
176 assertEquals("JWMPDH", seqs[5].getSequenceAsString());
180 * Test insertGap followed by undo command
182 @Test(groups = { "Functional" })
183 public void testUndo_insertGap()
185 // Edit ec = testee.new Edit(Action.INSERT_GAP, seqs, 4, 3, '?');
186 testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true);
187 // check something changed
188 assertEquals("abcd???efghjk", seqs[0].getSequenceAsString());
189 testee.undoCommand(new AlignmentI[] { al });
190 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
191 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
192 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
193 assertEquals("1234567890", seqs[3].getSequenceAsString());
197 * Test deleteGap followed by undo command
199 @Test(groups = { "Functional" })
200 public void testUndo_deleteGap()
202 testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true);
203 // check something changed
204 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
205 testee.undoCommand(new AlignmentI[] { al });
206 // deleteGap doesn't 'remember' deleted characters, only gaps get put back
207 assertEquals("abcd???hjk", seqs[0].getSequenceAsString());
208 assertEquals("fghj???nopq", seqs[1].getSequenceAsString());
209 assertEquals("qrst???xyz", seqs[2].getSequenceAsString());
210 assertEquals("1234???890", seqs[3].getSequenceAsString());
214 * Test several commands followed by an undo command
216 @Test(groups = { "Functional" })
217 public void testUndo_multipleCommands()
219 // delete positions 3/4/5 (counting from 1)
220 testee.appendEdit(Action.DELETE_GAP, seqs, 2, 3, al, true);
221 assertEquals("abfghjk", seqs[0].getSequenceAsString());
222 assertEquals("1267890", seqs[3].getSequenceAsString());
224 // insert 2 gaps after the second residue
225 testee.appendEdit(Action.INSERT_GAP, seqs, 2, 2, al, true);
226 assertEquals("ab??fghjk", seqs[0].getSequenceAsString());
227 assertEquals("12??67890", seqs[3].getSequenceAsString());
229 // delete positions 4/5/6
230 testee.appendEdit(Action.DELETE_GAP, seqs, 3, 3, al, true);
231 assertEquals("ab?hjk", seqs[0].getSequenceAsString());
232 assertEquals("12?890", seqs[3].getSequenceAsString());
234 // undo edit commands
235 testee.undoCommand(new AlignmentI[] { al });
236 assertEquals("ab?????hjk", seqs[0].getSequenceAsString());
237 assertEquals("12?????890", seqs[3].getSequenceAsString());
241 * Unit test for JAL-1594 bug: click and drag sequence right to insert gaps -
242 * undo did not remove them all.
244 @Test(groups = { "Functional" })
245 public void testUndo_multipleInsertGaps()
247 testee.appendEdit(Action.INSERT_GAP, seqs, 4, 1, al, true);
248 testee.appendEdit(Action.INSERT_GAP, seqs, 5, 1, al, true);
249 testee.appendEdit(Action.INSERT_GAP, seqs, 6, 1, al, true);
251 // undo edit commands
252 testee.undoCommand(new AlignmentI[] { al });
253 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
254 assertEquals("1234567890", seqs[3].getSequenceAsString());
259 * Test cut followed by undo command
261 @Test(groups = { "Functional" })
262 public void testUndo_cut()
264 testee.appendEdit(Action.CUT, seqs, 4, 3, al, true);
265 // check something changed
266 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
267 testee.undoCommand(new AlignmentI[] { al });
268 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
269 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
270 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
271 assertEquals("1234567890", seqs[3].getSequenceAsString());
275 * Test the replace command (used to manually edit a sequence)
277 @Test(groups = { "Functional" })
278 public void testReplace()
280 // seem to need a dataset sequence on the edited sequence here
281 seqs[1].createDatasetSequence();
282 new EditCommand("", Action.REPLACE, "ZXY", new SequenceI[] { seqs[1] },
284 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
285 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
286 assertEquals("1234567890", seqs[3].getSequenceAsString());
290 * Test the replace command (used to manually edit a sequence)
292 @Test(groups = { "Functional" })
293 public void testReplace_withGaps()
295 SequenceI seq = new Sequence("seq", "ABC--DEF");
296 seq.createDatasetSequence();
297 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
298 assertEquals(1, seq.getStart());
299 assertEquals(6, seq.getEnd());
302 * replace C- with XYZ
303 * NB arg4 = start column of selection for edit (base 0)
304 * arg5 = column after end of selection for edit
306 EditCommand edit = new EditCommand("", Action.REPLACE, "XYZ",
310 assertEquals("ABXYZ-DEF", seq.getSequenceAsString());
311 assertEquals(1, seq.getStart());
312 assertEquals(8, seq.getEnd());
313 assertEquals("ABXYZDEF", seq.getDatasetSequence().getSequenceAsString());
314 assertEquals(8, seq.getDatasetSequence().getEnd());
319 AlignmentI[] views = new AlignmentI[]
320 { new Alignment(new SequenceI[] { seq }) };
321 edit.undoCommand(views);
323 assertEquals("ABC--DEF", seq.getSequenceAsString());
324 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
325 assertEquals(1, seq.getStart());
326 assertEquals(6, seq.getEnd());
327 assertEquals(6, seq.getDatasetSequence().getEnd());
332 edit.doCommand(views);
334 assertEquals("ABXYZ-DEF", seq.getSequenceAsString());
335 assertEquals(1, seq.getStart());
336 assertEquals(8, seq.getEnd());
337 assertEquals("ABXYZDEF",
338 seq.getDatasetSequence().getSequenceAsString());
339 assertEquals(8, seq.getDatasetSequence().getEnd());
344 * Test replace command when it doesn't cause a sequence edit (see comment in
346 @Test(groups = { "Functional" })
347 public void testReplaceFirstResiduesWithGaps()
349 // test replace when gaps are inserted at start. Start/end should change
350 // w.r.t. original edited sequence.
351 SequenceI dsseq = seqs[1].getDatasetSequence();
352 EditCommand edit = new EditCommand("", Action.REPLACE, "----",
354 { seqs[1] }, 0, 4, al);
357 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
358 // and ds is preserved
359 assertTrue(dsseq == seqs[1].getDatasetSequence());
360 // and it is unchanged
361 assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
362 // and that alignment sequence start has been adjusted
363 assertEquals(5, seqs[1].getStart());
364 assertEquals(11, seqs[1].getEnd());
366 AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
368 edit.undoCommand(views);
370 // dataset sequence unchanged
371 assertTrue(dsseq == seqs[1].getDatasetSequence());
373 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
374 // and start/end numbering also restored
375 assertEquals(1, seqs[1].getStart());
376 assertEquals(11, seqs[1].getEnd());
379 edit.undoCommand(views);
381 // and repeat asserts for the original edit
384 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
385 // and ds is preserved
386 assertTrue(dsseq == seqs[1].getDatasetSequence());
387 // and it is unchanged
388 assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
389 // and that alignment sequence start has been adjusted
390 assertEquals(5, seqs[1].getStart());
391 assertEquals(11, seqs[1].getEnd());
396 * Test that the addEdit command correctly merges insert gap commands when
399 @Test(groups = { "Functional" })
400 public void testAddEdit_multipleInsertGap()
403 * 3 insert gap in a row (aka mouse drag right):
405 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
406 new SequenceI[] { seqs[0] }, 1, 1, al);
408 SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
409 edited.setDatasetSequence(seqs[0].getDatasetSequence());
410 e = new EditCommand().new Edit(Action.INSERT_GAP,
411 new SequenceI[] { edited }, 2, 1, al);
413 edited = new Sequence("seq0", "a??bcdefghjk");
414 edited.setDatasetSequence(seqs[0].getDatasetSequence());
415 e = new EditCommand().new Edit(Action.INSERT_GAP,
416 new SequenceI[] { edited }, 3, 1, al);
418 assertEquals(1, testee.getSize());
419 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
420 assertEquals(1, testee.getEdit(0).getPosition());
421 assertEquals(3, testee.getEdit(0).getNumber());
424 * Add a non-contiguous edit - should not be merged.
426 e = new EditCommand().new Edit(Action.INSERT_GAP,
427 new SequenceI[] { edited }, 5, 2, al);
429 assertEquals(2, testee.getSize());
430 assertEquals(5, testee.getEdit(1).getPosition());
431 assertEquals(2, testee.getEdit(1).getNumber());
434 * Add a Delete after the Insert - should not be merged.
436 e = new EditCommand().new Edit(Action.DELETE_GAP,
437 new SequenceI[] { edited }, 6, 2, al);
439 assertEquals(3, testee.getSize());
440 assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
441 assertEquals(6, testee.getEdit(2).getPosition());
442 assertEquals(2, testee.getEdit(2).getNumber());
446 * Test that the addEdit command correctly merges delete gap commands when
449 @Test(groups = { "Functional" })
450 public void testAddEdit_multipleDeleteGap()
453 * 3 delete gap in a row (aka mouse drag left):
455 seqs[0].setSequence("a???bcdefghjk");
456 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
457 new SequenceI[] { seqs[0] }, 4, 1, al);
459 assertEquals(1, testee.getSize());
461 SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
462 edited.setDatasetSequence(seqs[0].getDatasetSequence());
463 e = new EditCommand().new Edit(Action.DELETE_GAP,
464 new SequenceI[] { edited }, 3, 1, al);
466 assertEquals(1, testee.getSize());
468 edited = new Sequence("seq0", "a?bcdefghjk");
469 edited.setDatasetSequence(seqs[0].getDatasetSequence());
470 e = new EditCommand().new Edit(Action.DELETE_GAP,
471 new SequenceI[] { edited }, 2, 1, al);
473 assertEquals(1, testee.getSize());
474 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
475 assertEquals(2, testee.getEdit(0).getPosition());
476 assertEquals(3, testee.getEdit(0).getNumber());
479 * Add a non-contiguous edit - should not be merged.
481 e = new EditCommand().new Edit(Action.DELETE_GAP,
482 new SequenceI[] { edited }, 2, 1, al);
484 assertEquals(2, testee.getSize());
485 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
486 assertEquals(2, testee.getEdit(1).getPosition());
487 assertEquals(1, testee.getEdit(1).getNumber());
490 * Add an Insert after the Delete - should not be merged.
492 e = new EditCommand().new Edit(Action.INSERT_GAP,
493 new SequenceI[] { edited }, 1, 1, al);
495 assertEquals(3, testee.getSize());
496 assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
497 assertEquals(1, testee.getEdit(2).getPosition());
498 assertEquals(1, testee.getEdit(2).getNumber());
502 * Test that the addEdit command correctly handles 'remove gaps' edits for the
503 * case when they appear contiguous but are acting on different sequences.
504 * They should not be merged.
506 @Test(groups = { "Functional" })
507 public void testAddEdit_removeAllGaps()
509 seqs[0].setSequence("a???bcdefghjk");
510 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
511 new SequenceI[] { seqs[0] }, 4, 1, al);
514 seqs[1].setSequence("f??ghjklmnopq");
515 Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[]
516 { seqs[1] }, 3, 1, al);
518 assertEquals(2, testee.getSize());
519 assertSame(e, testee.getEdit(0));
520 assertSame(e2, testee.getEdit(1));
524 * Test that the addEdit command correctly merges insert gap commands acting
525 * on a multi-sequence selection.
527 @Test(groups = { "Functional" })
528 public void testAddEdit_groupInsertGaps()
531 * 2 insert gap in a row (aka mouse drag right), on two sequences:
533 Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
534 seqs[0], seqs[1] }, 1, 1, al);
536 SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
537 seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
538 SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
539 seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
540 e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
541 seq1edited, seq2edited }, 2, 1, al);
544 assertEquals(1, testee.getSize());
545 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
546 assertEquals(1, testee.getEdit(0).getPosition());
547 assertEquals(2, testee.getEdit(0).getNumber());
548 assertEquals(seqs[0].getDatasetSequence(), testee.getEdit(0)
549 .getSequences()[0].getDatasetSequence());
550 assertEquals(seqs[1].getDatasetSequence(), testee.getEdit(0)
551 .getSequences()[1].getDatasetSequence());
555 * Test for 'undoing' a series of gap insertions.
557 * <li>Start: ABCDEF insert 2 at pos 1</li>
558 * <li>next: A--BCDEF insert 1 at pos 4</li>
559 * <li>next: A--B-CDEF insert 2 at pos 0</li>
560 * <li>last: --A--B-CDEF</li>
563 @Test(groups = { "Functional" })
564 public void testPriorState_multipleInserts()
566 EditCommand command = new EditCommand();
567 SequenceI seq = new Sequence("", "--A--B-CDEF");
568 SequenceI ds = new Sequence("", "ABCDEF");
569 seq.setDatasetSequence(ds);
570 SequenceI[] sqs = new SequenceI[] { seq };
571 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
573 e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
575 e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
578 Map<SequenceI, SequenceI> unwound = command.priorState(false);
579 assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
583 * Test for 'undoing' a series of gap deletions.
585 * <li>Start: A-B-C delete 1 at pos 1</li>
586 * <li>Next: AB-C delete 1 at pos 2</li>
590 @Test(groups = { "Functional" })
591 public void testPriorState_removeAllGaps()
593 EditCommand command = new EditCommand();
594 SequenceI seq = new Sequence("", "ABC");
595 SequenceI ds = new Sequence("", "ABC");
596 seq.setDatasetSequence(ds);
597 SequenceI[] sqs = new SequenceI[] { seq };
598 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
600 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
603 Map<SequenceI, SequenceI> unwound = command.priorState(false);
604 assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
608 * Test for 'undoing' a single delete edit.
610 @Test(groups = { "Functional" })
611 public void testPriorState_singleDelete()
613 EditCommand command = new EditCommand();
614 SequenceI seq = new Sequence("", "ABCDEF");
615 SequenceI ds = new Sequence("", "ABCDEF");
616 seq.setDatasetSequence(ds);
617 SequenceI[] sqs = new SequenceI[] { seq };
618 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
621 Map<SequenceI, SequenceI> unwound = command.priorState(false);
622 assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
626 * Test 'undoing' a single gap insertion edit command.
628 @Test(groups = { "Functional" })
629 public void testPriorState_singleInsert()
631 EditCommand command = new EditCommand();
632 SequenceI seq = new Sequence("", "AB---CDEF");
633 SequenceI ds = new Sequence("", "ABCDEF");
634 seq.setDatasetSequence(ds);
635 SequenceI[] sqs = new SequenceI[] { seq };
636 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
639 Map<SequenceI, SequenceI> unwound = command.priorState(false);
640 SequenceI prior = unwound.get(ds);
641 assertEquals("ABCDEF", prior.getSequenceAsString());
642 assertEquals(1, prior.getStart());
643 assertEquals(6, prior.getEnd());
647 * Test 'undoing' a single gap insertion edit command, on a sequence whose
648 * start residue is other than 1
650 @Test(groups = { "Functional" })
651 public void testPriorState_singleInsertWithOffset()
653 EditCommand command = new EditCommand();
654 SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
655 // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
656 // seq.setDatasetSequence(ds);
657 seq.createDatasetSequence();
658 SequenceI[] sqs = new SequenceI[] { seq };
659 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
662 Map<SequenceI, SequenceI> unwound = command.priorState(false);
663 SequenceI prior = unwound.get(seq.getDatasetSequence());
664 assertEquals("ABCDEF", prior.getSequenceAsString());
665 assertEquals(8, prior.getStart());
666 assertEquals(13, prior.getEnd());
670 * Test that mimics 'remove all gaps' action. This generates delete gap edits
671 * for contiguous gaps in each sequence separately.
673 @Test(groups = { "Functional" })
674 public void testPriorState_removeGapsMultipleSeqs()
676 EditCommand command = new EditCommand();
677 String original1 = "--ABC-DEF";
678 String original2 = "FG-HI--J";
679 String original3 = "M-NOPQ";
682 * Two edits for the first sequence
684 SequenceI seq = new Sequence("", "ABC-DEF");
685 SequenceI ds1 = new Sequence("", "ABCDEF");
686 seq.setDatasetSequence(ds1);
687 SequenceI[] sqs = new SequenceI[] { seq };
688 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
690 seq = new Sequence("", "ABCDEF");
691 seq.setDatasetSequence(ds1);
692 sqs = new SequenceI[] { seq };
693 e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
697 * Two edits for the second sequence
699 seq = new Sequence("", "FGHI--J");
700 SequenceI ds2 = new Sequence("", "FGHIJ");
701 seq.setDatasetSequence(ds2);
702 sqs = new SequenceI[] { seq };
703 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
705 seq = new Sequence("", "FGHIJ");
706 seq.setDatasetSequence(ds2);
707 sqs = new SequenceI[] { seq };
708 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
712 * One edit for the third sequence.
714 seq = new Sequence("", "MNOPQ");
715 SequenceI ds3 = new Sequence("", "MNOPQ");
716 seq.setDatasetSequence(ds3);
717 sqs = new SequenceI[] { seq };
718 e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
721 Map<SequenceI, SequenceI> unwound = command.priorState(false);
722 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
723 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
724 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
728 * Test that mimics 'remove all gapped columns' action. This generates a
729 * series Delete Gap edits that each act on all sequences that share a gapped
732 @Test(groups = { "Functional" })
733 public void testPriorState_removeGappedCols()
735 EditCommand command = new EditCommand();
736 String original1 = "--ABC--DEF";
737 String original2 = "-G-HI--J";
738 String original3 = "-M-NO--PQ";
741 * First edit deletes the first column.
743 SequenceI seq1 = new Sequence("", "-ABC--DEF");
744 SequenceI ds1 = new Sequence("", "ABCDEF");
745 seq1.setDatasetSequence(ds1);
746 SequenceI seq2 = new Sequence("", "G-HI--J");
747 SequenceI ds2 = new Sequence("", "GHIJ");
748 seq2.setDatasetSequence(ds2);
749 SequenceI seq3 = new Sequence("", "M-NO--PQ");
750 SequenceI ds3 = new Sequence("", "MNOPQ");
751 seq3.setDatasetSequence(ds3);
752 SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
753 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
757 * Second edit deletes what is now columns 4 and 5.
759 seq1 = new Sequence("", "-ABCDEF");
760 seq1.setDatasetSequence(ds1);
761 seq2 = new Sequence("", "G-HIJ");
762 seq2.setDatasetSequence(ds2);
763 seq3 = new Sequence("", "M-NOPQ");
764 seq3.setDatasetSequence(ds3);
765 sqs = new SequenceI[] { seq1, seq2, seq3 };
766 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
769 Map<SequenceI, SequenceI> unwound = command.priorState(false);
770 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
771 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
772 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
773 assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
774 assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
775 assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
779 * Test a cut action's relocation of sequence features
781 @Test(groups = { "Functional" })
782 public void testCut_withFeatures()
785 * create sequence features before, after and overlapping
786 * a cut of columns/residues 4-7
788 SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
789 seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
791 seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
793 seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
795 seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
797 seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
801 * add some contact features
803 SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
805 seq0.addSequenceFeature(internalContact); // should get deleted
806 SequenceFeature overlapLeftContact = new SequenceFeature(
807 "disulphide bond", "", 2, 6, 0f, null);
808 seq0.addSequenceFeature(overlapLeftContact); // should get deleted
809 SequenceFeature overlapRightContact = new SequenceFeature(
810 "disulphide bond", "", 5, 8, 0f, null);
811 seq0.addSequenceFeature(overlapRightContact); // should get deleted
812 SequenceFeature spanningContact = new SequenceFeature(
813 "disulphide bond", "", 2, 9, 0f, null);
814 seq0.addSequenceFeature(spanningContact); // should get shortened 3'
817 * cut columns 3-6 (base 0), residues d-g 4-7
819 Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
820 EditCommand.cut(ec, new AlignmentI[] { al });
822 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
823 SequenceFeatures.sortFeatures(sfs, true);
825 assertEquals(5, sfs.size()); // features internal to cut were deleted
826 SequenceFeature sf = sfs.get(0);
827 assertEquals("before", sf.getType());
828 assertEquals(1, sf.getBegin());
829 assertEquals(3, sf.getEnd());
831 assertEquals("disulphide bond", sf.getType());
832 assertEquals(2, sf.getBegin());
833 assertEquals(5, sf.getEnd()); // truncated by cut
835 assertEquals("overlap left", sf.getType());
836 assertEquals(2, sf.getBegin());
837 assertEquals(3, sf.getEnd()); // truncated by cut
839 assertEquals("after", sf.getType());
840 assertEquals(4, sf.getBegin()); // shifted left by cut
841 assertEquals(6, sf.getEnd()); // shifted left by cut
843 assertEquals("overlap right", sf.getType());
844 assertEquals(4, sf.getBegin()); // shifted left by cut
845 assertEquals(4, sf.getEnd()); // truncated by cut
849 * Test a cut action's relocation of sequence features, with full coverage of
850 * all possible feature and cut locations for a 5-position ungapped sequence
852 @Test(groups = { "Functional" })
853 public void testCut_withFeatures_exhaustive()
856 * create a sequence features on each subrange of 1-5
858 SequenceI seq0 = new Sequence("seq", "ABCDE");
861 seq0.setStart(start);
863 AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
864 alignment.setDataset(null);
867 * create a new alignment with shared dataset sequence
869 AlignmentI copy = new Alignment(
871 { alignment.getDataset().getSequenceAt(0).deriveSequence() });
872 SequenceI copySeq0 = copy.getSequenceAt(0);
874 for (int from = start; from <= end; from++)
876 for (int to = from; to <= end; to++)
878 String desc = String.format("%d-%d", from, to);
879 SequenceFeature sf = new SequenceFeature("test", desc, from, to,
881 sf.setValue("from", Integer.valueOf(from));
882 sf.setValue("to", Integer.valueOf(to));
883 seq0.addSequenceFeature(sf);
887 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
888 assertEquals(func(5), sfs.size());
889 assertEquals(sfs, copySeq0.getSequenceFeatures());
890 String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
893 * now perform all possible cuts of subranges of columns 1-5
894 * and validate the resulting remaining sequence features!
896 SequenceI[] sqs = new SequenceI[] { seq0 };
898 for (int from = 0; from < seq0.getLength(); from++)
900 for (int to = from; to < seq0.getLength(); to++)
902 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
903 - from + 1), alignment);
904 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
905 boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
906 .getDatasetSequence();
908 verifyCut(seq0, from, to, msg, start);
911 * verify copy alignment dataset sequence unaffected
913 assertEquals("Original dataset sequence was modified",
914 copySequenceFeatures,
915 copySeq0.getSequenceFeatures().toString());
918 * verify any new dataset sequence was added to the
921 assertEquals("Wrong Dataset size after " + msg,
922 newDatasetSequence ? 2 : 1,
923 alignment.getDataset().getHeight());
926 * undo and verify all restored
928 AlignmentI[] views = new AlignmentI[] { alignment };
929 ec.undoCommand(views);
930 sfs = seq0.getSequenceFeatures();
931 assertEquals("After undo of " + msg, func(5), sfs.size());
932 verifyUndo(from, to, sfs);
935 * verify copy alignment dataset sequence still unaffected
936 * and alignment dataset has shrunk (if it was added to)
938 assertEquals("Original dataset sequence was modified",
939 copySequenceFeatures,
940 copySeq0.getSequenceFeatures().toString());
941 assertEquals("Wrong Dataset size after Undo of " + msg, 1,
942 alignment.getDataset().getHeight());
948 verifyCut(seq0, from, to, msg, start);
951 * verify copy alignment dataset sequence unaffected
952 * and any new dataset sequence readded to alignment dataset
954 assertEquals("Original dataset sequence was modified",
955 copySequenceFeatures,
956 copySeq0.getSequenceFeatures().toString());
957 assertEquals("Wrong Dataset size after Redo of " + msg,
958 newDatasetSequence ? 2 : 1,
959 alignment.getDataset().getHeight());
962 * undo ready for next cut
964 ec.undoCommand(views);
967 * final verify that copy alignment dataset sequence is still unaffected
968 * and that alignment dataset has shrunk
970 assertEquals("Original dataset sequence was modified",
971 copySequenceFeatures,
972 copySeq0.getSequenceFeatures().toString());
973 assertEquals("Wrong Dataset size after final Undo of " + msg, 1,
974 alignment.getDataset().getHeight());
980 * Verify by inspection that the sequence features left on the sequence after
981 * a cut match the expected results. The trick to this is that we can parse
982 * each feature's original start-end positions from its description.
990 protected void verifyCut(SequenceI seq0, int from, int to,
991 final String msg, int seqStart)
993 List<SequenceFeature> sfs;
994 sfs = seq0.getSequenceFeatures();
996 Collections.sort(sfs, BY_DESCRIPTION);
999 * confirm the number of features has reduced by the
1000 * number of features within the cut region i.e. by
1001 * func(length of cut); exception is a cut at start or end of sequence,
1002 * which retains the original coordinates, dataset sequence
1003 * and all its features
1005 boolean datasetRetained = from == 0 || to == 4;
1006 if (datasetRetained)
1008 // dataset and all features retained
1009 assertEquals(msg, func(5), sfs.size());
1011 else if (to - from == 4)
1013 // all columns were cut
1014 assertTrue(sfs.isEmpty());
1018 // failure in checkFeatureRelocation is more informative!
1019 assertEquals(msg + "wrong number of features left", func(5)
1020 - func(to - from + 1), sfs.size());
1024 * inspect individual features
1026 for (SequenceFeature sf : sfs)
1028 verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
1034 * Check that after Undo, every feature has start/end that match its original
1035 * "start" and "end" properties
1041 protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
1043 for (SequenceFeature sf : sfs)
1045 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1046 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1047 String msg = String.format(
1048 "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
1050 assertEquals(msg + "start", oldFrom, sf.getBegin());
1051 assertEquals(msg + "end", oldTo, sf.getEnd());
1056 * Helper method to check a feature has been correctly relocated after a cut
1060 * start of cut (first residue cut 1..)
1062 * end of cut (last residue cut 1..)
1066 private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1067 boolean newDataset, int seqStart)
1069 // TODO handle the gapped sequence case as well
1070 int cutSize = to - from + 1;
1071 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1072 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1073 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1074 final int oldToPosition = oldTo - seqStart + 1; // 1..
1076 String msg = String.format(
1077 "Feature %s relocated to %d-%d after cut of %d-%d",
1078 sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1081 // dataset retained with all features unchanged
1082 assertEquals("0: " + msg, oldFrom, sf.getBegin());
1083 assertEquals("0: " + msg, oldTo, sf.getEnd());
1085 else if (oldToPosition < from)
1087 // before cut region so unchanged
1088 assertEquals("1: " + msg, oldFrom, sf.getBegin());
1089 assertEquals("2: " + msg, oldTo, sf.getEnd());
1091 else if (oldFromPosition > to)
1093 // follows cut region - shift by size of cut
1094 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1096 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1099 else if (oldFromPosition < from && oldToPosition > to)
1101 // feature encloses cut region - shrink it right
1102 assertEquals("5: " + msg, oldFrom, sf.getBegin());
1103 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1105 else if (oldFromPosition < from)
1107 // feature overlaps left side of cut region - truncated right
1108 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1110 else if (oldToPosition > to)
1112 // feature overlaps right side of cut region - truncated left
1113 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1115 assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1120 // feature internal to cut - should have been deleted!
1121 Assert.fail(msg + " - should have been deleted");
1126 * Test a cut action's relocation of sequence features
1128 @Test(groups = { "Functional" })
1129 public void testCut_withFeatures5prime()
1131 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1132 seq0.createDatasetSequence();
1133 assertEquals(8, seq0.getStart());
1134 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
1136 SequenceI[] seqsArray = new SequenceI[] { seq0 };
1137 AlignmentI alignment = new Alignment(seqsArray);
1140 * cut columns of A-B; same dataset sequence is retained, aligned sequence
1143 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1144 EditCommand.cut(ec, new AlignmentI[] { alignment });
1147 * feature on CC(10-11) should still be on CC(10-11)
1149 assertSame(seq0, alignment.getSequenceAt(0));
1150 assertEquals(10, seq0.getStart());
1151 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1152 assertEquals(1, sfs.size());
1153 SequenceFeature sf = sfs.get(0);
1154 assertEquals(10, sf.getBegin());
1155 assertEquals(11, sf.getEnd());