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 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
283 // NB command.number holds end position for a Replace command
284 new EditCommand("", Action.REPLACE, "Z-xY", new SequenceI[] { seqs[1] },
286 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
287 assertEquals("fghjZ-xYopq", seqs[1].getSequenceAsString());
288 assertEquals("fghjZxYopq",
289 seqs[1].getDatasetSequence().getSequenceAsString());
290 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
291 assertEquals("1234567890", seqs[3].getSequenceAsString());
295 * Test the replace command (used to manually edit a sequence)
297 @Test(groups = { "Functional" })
298 public void testReplace_withGaps()
300 SequenceI seq = new Sequence("seq", "ABC--DEF");
301 seq.createDatasetSequence();
302 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
303 assertEquals(1, seq.getStart());
304 assertEquals(6, seq.getEnd());
307 * replace C- with XYZ
308 * NB arg4 = start column of selection for edit (base 0)
309 * arg5 = column after end of selection for edit
311 EditCommand edit = new EditCommand("", Action.REPLACE, "xyZ",
315 assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
316 assertEquals(1, seq.getStart());
317 assertEquals(8, seq.getEnd());
318 assertEquals("ABxyZDEF",
319 seq.getDatasetSequence().getSequenceAsString());
320 assertEquals(8, seq.getDatasetSequence().getEnd());
325 AlignmentI[] views = new AlignmentI[]
326 { new Alignment(new SequenceI[] { seq }) };
327 edit.undoCommand(views);
329 assertEquals("ABC--DEF", seq.getSequenceAsString());
330 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
331 assertEquals(1, seq.getStart());
332 assertEquals(6, seq.getEnd());
333 assertEquals(6, seq.getDatasetSequence().getEnd());
338 edit.doCommand(views);
340 assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
341 assertEquals(1, seq.getStart());
342 assertEquals(8, seq.getEnd());
343 assertEquals("ABxyZDEF",
344 seq.getDatasetSequence().getSequenceAsString());
345 assertEquals(8, seq.getDatasetSequence().getEnd());
350 * Test replace command when it doesn't cause a sequence edit (see comment in
352 @Test(groups = { "Functional" })
353 public void testReplaceFirstResiduesWithGaps()
355 // test replace when gaps are inserted at start. Start/end should change
356 // w.r.t. original edited sequence.
357 SequenceI dsseq = seqs[1].getDatasetSequence();
358 EditCommand edit = new EditCommand("", Action.REPLACE, "----",
360 { seqs[1] }, 0, 4, al);
363 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
364 // and ds is preserved
365 assertTrue(dsseq == seqs[1].getDatasetSequence());
366 // and it is unchanged
367 assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
368 // and that alignment sequence start has been adjusted
369 assertEquals(5, seqs[1].getStart());
370 assertEquals(11, seqs[1].getEnd());
372 AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
374 edit.undoCommand(views);
376 // dataset sequence unchanged
377 assertTrue(dsseq == seqs[1].getDatasetSequence());
379 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
380 // and start/end numbering also restored
381 assertEquals(1, seqs[1].getStart());
382 assertEquals(11, seqs[1].getEnd());
385 edit.undoCommand(views);
387 // and repeat asserts for the original edit
390 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
391 // and ds is preserved
392 assertTrue(dsseq == seqs[1].getDatasetSequence());
393 // and it is unchanged
394 assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
395 // and that alignment sequence start has been adjusted
396 assertEquals(5, seqs[1].getStart());
397 assertEquals(11, seqs[1].getEnd());
402 * Test that the addEdit command correctly merges insert gap commands when
405 @Test(groups = { "Functional" })
406 public void testAddEdit_multipleInsertGap()
409 * 3 insert gap in a row (aka mouse drag right):
411 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
412 new SequenceI[] { seqs[0] }, 1, 1, al);
414 SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
415 edited.setDatasetSequence(seqs[0].getDatasetSequence());
416 e = new EditCommand().new Edit(Action.INSERT_GAP,
417 new SequenceI[] { edited }, 2, 1, al);
419 edited = new Sequence("seq0", "a??bcdefghjk");
420 edited.setDatasetSequence(seqs[0].getDatasetSequence());
421 e = new EditCommand().new Edit(Action.INSERT_GAP,
422 new SequenceI[] { edited }, 3, 1, al);
424 assertEquals(1, testee.getSize());
425 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
426 assertEquals(1, testee.getEdit(0).getPosition());
427 assertEquals(3, testee.getEdit(0).getNumber());
430 * Add a non-contiguous edit - should not be merged.
432 e = new EditCommand().new Edit(Action.INSERT_GAP,
433 new SequenceI[] { edited }, 5, 2, al);
435 assertEquals(2, testee.getSize());
436 assertEquals(5, testee.getEdit(1).getPosition());
437 assertEquals(2, testee.getEdit(1).getNumber());
440 * Add a Delete after the Insert - should not be merged.
442 e = new EditCommand().new Edit(Action.DELETE_GAP,
443 new SequenceI[] { edited }, 6, 2, al);
445 assertEquals(3, testee.getSize());
446 assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
447 assertEquals(6, testee.getEdit(2).getPosition());
448 assertEquals(2, testee.getEdit(2).getNumber());
452 * Test that the addEdit command correctly merges delete gap commands when
455 @Test(groups = { "Functional" })
456 public void testAddEdit_multipleDeleteGap()
459 * 3 delete gap in a row (aka mouse drag left):
461 seqs[0].setSequence("a???bcdefghjk");
462 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
463 new SequenceI[] { seqs[0] }, 4, 1, al);
465 assertEquals(1, testee.getSize());
467 SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
468 edited.setDatasetSequence(seqs[0].getDatasetSequence());
469 e = new EditCommand().new Edit(Action.DELETE_GAP,
470 new SequenceI[] { edited }, 3, 1, al);
472 assertEquals(1, testee.getSize());
474 edited = new Sequence("seq0", "a?bcdefghjk");
475 edited.setDatasetSequence(seqs[0].getDatasetSequence());
476 e = new EditCommand().new Edit(Action.DELETE_GAP,
477 new SequenceI[] { edited }, 2, 1, al);
479 assertEquals(1, testee.getSize());
480 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
481 assertEquals(2, testee.getEdit(0).getPosition());
482 assertEquals(3, testee.getEdit(0).getNumber());
485 * Add a non-contiguous edit - should not be merged.
487 e = new EditCommand().new Edit(Action.DELETE_GAP,
488 new SequenceI[] { edited }, 2, 1, al);
490 assertEquals(2, testee.getSize());
491 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
492 assertEquals(2, testee.getEdit(1).getPosition());
493 assertEquals(1, testee.getEdit(1).getNumber());
496 * Add an Insert after the Delete - should not be merged.
498 e = new EditCommand().new Edit(Action.INSERT_GAP,
499 new SequenceI[] { edited }, 1, 1, al);
501 assertEquals(3, testee.getSize());
502 assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
503 assertEquals(1, testee.getEdit(2).getPosition());
504 assertEquals(1, testee.getEdit(2).getNumber());
508 * Test that the addEdit command correctly handles 'remove gaps' edits for the
509 * case when they appear contiguous but are acting on different sequences.
510 * They should not be merged.
512 @Test(groups = { "Functional" })
513 public void testAddEdit_removeAllGaps()
515 seqs[0].setSequence("a???bcdefghjk");
516 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
517 new SequenceI[] { seqs[0] }, 4, 1, al);
520 seqs[1].setSequence("f??ghjklmnopq");
521 Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[]
522 { seqs[1] }, 3, 1, al);
524 assertEquals(2, testee.getSize());
525 assertSame(e, testee.getEdit(0));
526 assertSame(e2, testee.getEdit(1));
530 * Test that the addEdit command correctly merges insert gap commands acting
531 * on a multi-sequence selection.
533 @Test(groups = { "Functional" })
534 public void testAddEdit_groupInsertGaps()
537 * 2 insert gap in a row (aka mouse drag right), on two sequences:
539 Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
540 seqs[0], seqs[1] }, 1, 1, al);
542 SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
543 seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
544 SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
545 seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
546 e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
547 seq1edited, seq2edited }, 2, 1, al);
550 assertEquals(1, testee.getSize());
551 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
552 assertEquals(1, testee.getEdit(0).getPosition());
553 assertEquals(2, testee.getEdit(0).getNumber());
554 assertEquals(seqs[0].getDatasetSequence(), testee.getEdit(0)
555 .getSequences()[0].getDatasetSequence());
556 assertEquals(seqs[1].getDatasetSequence(), testee.getEdit(0)
557 .getSequences()[1].getDatasetSequence());
561 * Test for 'undoing' a series of gap insertions.
563 * <li>Start: ABCDEF insert 2 at pos 1</li>
564 * <li>next: A--BCDEF insert 1 at pos 4</li>
565 * <li>next: A--B-CDEF insert 2 at pos 0</li>
566 * <li>last: --A--B-CDEF</li>
569 @Test(groups = { "Functional" })
570 public void testPriorState_multipleInserts()
572 EditCommand command = new EditCommand();
573 SequenceI seq = new Sequence("", "--A--B-CDEF");
574 SequenceI ds = new Sequence("", "ABCDEF");
575 seq.setDatasetSequence(ds);
576 SequenceI[] sqs = new SequenceI[] { seq };
577 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
579 e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
581 e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
584 Map<SequenceI, SequenceI> unwound = command.priorState(false);
585 assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
589 * Test for 'undoing' a series of gap deletions.
591 * <li>Start: A-B-C delete 1 at pos 1</li>
592 * <li>Next: AB-C delete 1 at pos 2</li>
596 @Test(groups = { "Functional" })
597 public void testPriorState_removeAllGaps()
599 EditCommand command = new EditCommand();
600 SequenceI seq = new Sequence("", "ABC");
601 SequenceI ds = new Sequence("", "ABC");
602 seq.setDatasetSequence(ds);
603 SequenceI[] sqs = new SequenceI[] { seq };
604 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
606 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
609 Map<SequenceI, SequenceI> unwound = command.priorState(false);
610 assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
614 * Test for 'undoing' a single delete edit.
616 @Test(groups = { "Functional" })
617 public void testPriorState_singleDelete()
619 EditCommand command = new EditCommand();
620 SequenceI seq = new Sequence("", "ABCDEF");
621 SequenceI ds = new Sequence("", "ABCDEF");
622 seq.setDatasetSequence(ds);
623 SequenceI[] sqs = new SequenceI[] { seq };
624 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
627 Map<SequenceI, SequenceI> unwound = command.priorState(false);
628 assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
632 * Test 'undoing' a single gap insertion edit command.
634 @Test(groups = { "Functional" })
635 public void testPriorState_singleInsert()
637 EditCommand command = new EditCommand();
638 SequenceI seq = new Sequence("", "AB---CDEF");
639 SequenceI ds = new Sequence("", "ABCDEF");
640 seq.setDatasetSequence(ds);
641 SequenceI[] sqs = new SequenceI[] { seq };
642 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
645 Map<SequenceI, SequenceI> unwound = command.priorState(false);
646 SequenceI prior = unwound.get(ds);
647 assertEquals("ABCDEF", prior.getSequenceAsString());
648 assertEquals(1, prior.getStart());
649 assertEquals(6, prior.getEnd());
653 * Test 'undoing' a single gap insertion edit command, on a sequence whose
654 * start residue is other than 1
656 @Test(groups = { "Functional" })
657 public void testPriorState_singleInsertWithOffset()
659 EditCommand command = new EditCommand();
660 SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
661 // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
662 // seq.setDatasetSequence(ds);
663 seq.createDatasetSequence();
664 SequenceI[] sqs = new SequenceI[] { seq };
665 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
668 Map<SequenceI, SequenceI> unwound = command.priorState(false);
669 SequenceI prior = unwound.get(seq.getDatasetSequence());
670 assertEquals("ABCDEF", prior.getSequenceAsString());
671 assertEquals(8, prior.getStart());
672 assertEquals(13, prior.getEnd());
676 * Test that mimics 'remove all gaps' action. This generates delete gap edits
677 * for contiguous gaps in each sequence separately.
679 @Test(groups = { "Functional" })
680 public void testPriorState_removeGapsMultipleSeqs()
682 EditCommand command = new EditCommand();
683 String original1 = "--ABC-DEF";
684 String original2 = "FG-HI--J";
685 String original3 = "M-NOPQ";
688 * Two edits for the first sequence
690 SequenceI seq = new Sequence("", "ABC-DEF");
691 SequenceI ds1 = new Sequence("", "ABCDEF");
692 seq.setDatasetSequence(ds1);
693 SequenceI[] sqs = new SequenceI[] { seq };
694 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
696 seq = new Sequence("", "ABCDEF");
697 seq.setDatasetSequence(ds1);
698 sqs = new SequenceI[] { seq };
699 e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
703 * Two edits for the second sequence
705 seq = new Sequence("", "FGHI--J");
706 SequenceI ds2 = new Sequence("", "FGHIJ");
707 seq.setDatasetSequence(ds2);
708 sqs = new SequenceI[] { seq };
709 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
711 seq = new Sequence("", "FGHIJ");
712 seq.setDatasetSequence(ds2);
713 sqs = new SequenceI[] { seq };
714 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
718 * One edit for the third sequence.
720 seq = new Sequence("", "MNOPQ");
721 SequenceI ds3 = new Sequence("", "MNOPQ");
722 seq.setDatasetSequence(ds3);
723 sqs = new SequenceI[] { seq };
724 e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
727 Map<SequenceI, SequenceI> unwound = command.priorState(false);
728 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
729 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
730 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
734 * Test that mimics 'remove all gapped columns' action. This generates a
735 * series Delete Gap edits that each act on all sequences that share a gapped
738 @Test(groups = { "Functional" })
739 public void testPriorState_removeGappedCols()
741 EditCommand command = new EditCommand();
742 String original1 = "--ABC--DEF";
743 String original2 = "-G-HI--J";
744 String original3 = "-M-NO--PQ";
747 * First edit deletes the first column.
749 SequenceI seq1 = new Sequence("", "-ABC--DEF");
750 SequenceI ds1 = new Sequence("", "ABCDEF");
751 seq1.setDatasetSequence(ds1);
752 SequenceI seq2 = new Sequence("", "G-HI--J");
753 SequenceI ds2 = new Sequence("", "GHIJ");
754 seq2.setDatasetSequence(ds2);
755 SequenceI seq3 = new Sequence("", "M-NO--PQ");
756 SequenceI ds3 = new Sequence("", "MNOPQ");
757 seq3.setDatasetSequence(ds3);
758 SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
759 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
763 * Second edit deletes what is now columns 4 and 5.
765 seq1 = new Sequence("", "-ABCDEF");
766 seq1.setDatasetSequence(ds1);
767 seq2 = new Sequence("", "G-HIJ");
768 seq2.setDatasetSequence(ds2);
769 seq3 = new Sequence("", "M-NOPQ");
770 seq3.setDatasetSequence(ds3);
771 sqs = new SequenceI[] { seq1, seq2, seq3 };
772 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
775 Map<SequenceI, SequenceI> unwound = command.priorState(false);
776 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
777 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
778 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
779 assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
780 assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
781 assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
785 * Test a cut action's relocation of sequence features
787 @Test(groups = { "Functional" })
788 public void testCut_withFeatures()
791 * create sequence features before, after and overlapping
792 * a cut of columns/residues 4-7
794 SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
795 seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
797 seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
799 seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
801 seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
803 seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
807 * add some contact features
809 SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
811 seq0.addSequenceFeature(internalContact); // should get deleted
812 SequenceFeature overlapLeftContact = new SequenceFeature(
813 "disulphide bond", "", 2, 6, 0f, null);
814 seq0.addSequenceFeature(overlapLeftContact); // should get deleted
815 SequenceFeature overlapRightContact = new SequenceFeature(
816 "disulphide bond", "", 5, 8, 0f, null);
817 seq0.addSequenceFeature(overlapRightContact); // should get deleted
818 SequenceFeature spanningContact = new SequenceFeature(
819 "disulphide bond", "", 2, 9, 0f, null);
820 seq0.addSequenceFeature(spanningContact); // should get shortened 3'
823 * cut columns 3-6 (base 0), residues d-g 4-7
825 Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
826 EditCommand.cut(ec, new AlignmentI[] { al });
828 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
829 SequenceFeatures.sortFeatures(sfs, true);
831 assertEquals(5, sfs.size()); // features internal to cut were deleted
832 SequenceFeature sf = sfs.get(0);
833 assertEquals("before", sf.getType());
834 assertEquals(1, sf.getBegin());
835 assertEquals(3, sf.getEnd());
837 assertEquals("disulphide bond", sf.getType());
838 assertEquals(2, sf.getBegin());
839 assertEquals(5, sf.getEnd()); // truncated by cut
841 assertEquals("overlap left", sf.getType());
842 assertEquals(2, sf.getBegin());
843 assertEquals(3, sf.getEnd()); // truncated by cut
845 assertEquals("after", sf.getType());
846 assertEquals(4, sf.getBegin()); // shifted left by cut
847 assertEquals(6, sf.getEnd()); // shifted left by cut
849 assertEquals("overlap right", sf.getType());
850 assertEquals(4, sf.getBegin()); // shifted left by cut
851 assertEquals(4, sf.getEnd()); // truncated by cut
855 * Test a cut action's relocation of sequence features, with full coverage of
856 * all possible feature and cut locations for a 5-position ungapped sequence
858 @Test(groups = { "Functional" })
859 public void testCut_withFeatures_exhaustive()
862 * create a sequence features on each subrange of 1-5
864 SequenceI seq0 = new Sequence("seq", "ABCDE");
867 seq0.setStart(start);
869 AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
870 alignment.setDataset(null);
873 * create a new alignment with shared dataset sequence
875 AlignmentI copy = new Alignment(
877 { alignment.getDataset().getSequenceAt(0).deriveSequence() });
878 SequenceI copySeq0 = copy.getSequenceAt(0);
880 for (int from = start; from <= end; from++)
882 for (int to = from; to <= end; to++)
884 String desc = String.format("%d-%d", from, to);
885 SequenceFeature sf = new SequenceFeature("test", desc, from, to,
887 sf.setValue("from", Integer.valueOf(from));
888 sf.setValue("to", Integer.valueOf(to));
889 seq0.addSequenceFeature(sf);
893 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
894 assertEquals(func(5), sfs.size());
895 assertEquals(sfs, copySeq0.getSequenceFeatures());
896 String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
899 * now perform all possible cuts of subranges of columns 1-5
900 * and validate the resulting remaining sequence features!
902 SequenceI[] sqs = new SequenceI[] { seq0 };
904 for (int from = 0; from < seq0.getLength(); from++)
906 for (int to = from; to < seq0.getLength(); to++)
908 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
909 - from + 1), alignment);
910 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
911 boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
912 .getDatasetSequence();
914 verifyCut(seq0, from, to, msg, start);
917 * verify copy alignment dataset sequence unaffected
919 assertEquals("Original dataset sequence was modified",
920 copySequenceFeatures,
921 copySeq0.getSequenceFeatures().toString());
924 * verify any new dataset sequence was added to the
927 assertEquals("Wrong Dataset size after " + msg,
928 newDatasetSequence ? 2 : 1,
929 alignment.getDataset().getHeight());
932 * undo and verify all restored
934 AlignmentI[] views = new AlignmentI[] { alignment };
935 ec.undoCommand(views);
936 sfs = seq0.getSequenceFeatures();
937 assertEquals("After undo of " + msg, func(5), sfs.size());
938 verifyUndo(from, to, sfs);
941 * verify copy alignment dataset sequence still unaffected
942 * and alignment dataset has shrunk (if it was added to)
944 assertEquals("Original dataset sequence was modified",
945 copySequenceFeatures,
946 copySeq0.getSequenceFeatures().toString());
947 assertEquals("Wrong Dataset size after Undo of " + msg, 1,
948 alignment.getDataset().getHeight());
954 verifyCut(seq0, from, to, msg, start);
957 * verify copy alignment dataset sequence unaffected
958 * and any new dataset sequence readded to alignment dataset
960 assertEquals("Original dataset sequence was modified",
961 copySequenceFeatures,
962 copySeq0.getSequenceFeatures().toString());
963 assertEquals("Wrong Dataset size after Redo of " + msg,
964 newDatasetSequence ? 2 : 1,
965 alignment.getDataset().getHeight());
968 * undo ready for next cut
970 ec.undoCommand(views);
973 * final verify that copy alignment dataset sequence is still unaffected
974 * and that alignment dataset has shrunk
976 assertEquals("Original dataset sequence was modified",
977 copySequenceFeatures,
978 copySeq0.getSequenceFeatures().toString());
979 assertEquals("Wrong Dataset size after final Undo of " + msg, 1,
980 alignment.getDataset().getHeight());
986 * Verify by inspection that the sequence features left on the sequence after
987 * a cut match the expected results. The trick to this is that we can parse
988 * each feature's original start-end positions from its description.
996 protected void verifyCut(SequenceI seq0, int from, int to,
997 final String msg, int seqStart)
999 List<SequenceFeature> sfs;
1000 sfs = seq0.getSequenceFeatures();
1002 Collections.sort(sfs, BY_DESCRIPTION);
1005 * confirm the number of features has reduced by the
1006 * number of features within the cut region i.e. by
1007 * func(length of cut); exception is a cut at start or end of sequence,
1008 * which retains the original coordinates, dataset sequence
1009 * and all its features
1011 boolean datasetRetained = from == 0 || to == 4;
1012 if (datasetRetained)
1014 // dataset and all features retained
1015 assertEquals(msg, func(5), sfs.size());
1017 else if (to - from == 4)
1019 // all columns were cut
1020 assertTrue(sfs.isEmpty());
1024 // failure in checkFeatureRelocation is more informative!
1025 assertEquals(msg + "wrong number of features left", func(5)
1026 - func(to - from + 1), sfs.size());
1030 * inspect individual features
1032 for (SequenceFeature sf : sfs)
1034 verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
1040 * Check that after Undo, every feature has start/end that match its original
1041 * "start" and "end" properties
1047 protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
1049 for (SequenceFeature sf : sfs)
1051 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1052 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1053 String msg = String.format(
1054 "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
1056 assertEquals(msg + "start", oldFrom, sf.getBegin());
1057 assertEquals(msg + "end", oldTo, sf.getEnd());
1062 * Helper method to check a feature has been correctly relocated after a cut
1066 * start of cut (first residue cut 1..)
1068 * end of cut (last residue cut 1..)
1072 private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1073 boolean newDataset, int seqStart)
1075 // TODO handle the gapped sequence case as well
1076 int cutSize = to - from + 1;
1077 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1078 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1079 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1080 final int oldToPosition = oldTo - seqStart + 1; // 1..
1082 String msg = String.format(
1083 "Feature %s relocated to %d-%d after cut of %d-%d",
1084 sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1087 // dataset retained with all features unchanged
1088 assertEquals("0: " + msg, oldFrom, sf.getBegin());
1089 assertEquals("0: " + msg, oldTo, sf.getEnd());
1091 else if (oldToPosition < from)
1093 // before cut region so unchanged
1094 assertEquals("1: " + msg, oldFrom, sf.getBegin());
1095 assertEquals("2: " + msg, oldTo, sf.getEnd());
1097 else if (oldFromPosition > to)
1099 // follows cut region - shift by size of cut
1100 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1102 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1105 else if (oldFromPosition < from && oldToPosition > to)
1107 // feature encloses cut region - shrink it right
1108 assertEquals("5: " + msg, oldFrom, sf.getBegin());
1109 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1111 else if (oldFromPosition < from)
1113 // feature overlaps left side of cut region - truncated right
1114 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1116 else if (oldToPosition > to)
1118 // feature overlaps right side of cut region - truncated left
1119 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1121 assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1126 // feature internal to cut - should have been deleted!
1127 Assert.fail(msg + " - should have been deleted");
1132 * Test a cut action's relocation of sequence features
1134 @Test(groups = { "Functional" })
1135 public void testCut_withFeatures5prime()
1137 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1138 seq0.createDatasetSequence();
1139 assertEquals(8, seq0.getStart());
1140 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
1142 SequenceI[] seqsArray = new SequenceI[] { seq0 };
1143 AlignmentI alignment = new Alignment(seqsArray);
1146 * cut columns of A-B; same dataset sequence is retained, aligned sequence
1149 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1150 EditCommand.cut(ec, new AlignmentI[] { alignment });
1153 * feature on CC(10-11) should still be on CC(10-11)
1155 assertSame(seq0, alignment.getSequenceAt(0));
1156 assertEquals(10, seq0.getStart());
1157 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1158 assertEquals(1, sfs.size());
1159 SequenceFeature sf = sfs.get(0);
1160 assertEquals(10, sf.getBegin());
1161 assertEquals(11, sf.getEnd());