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 // Dataset Sequence should always be uppercase
289 assertEquals("fghjZxYopq".toUpperCase(),
290 seqs[1].getDatasetSequence().getSequenceAsString());
291 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
292 assertEquals("1234567890", seqs[3].getSequenceAsString());
296 * Test the replace command (used to manually edit a sequence)
298 @Test(groups = { "Functional" })
299 public void testReplace_withGaps()
301 SequenceI seq = new Sequence("seq", "ABC--DEF");
302 seq.createDatasetSequence();
303 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
304 assertEquals(1, seq.getStart());
305 assertEquals(6, seq.getEnd());
308 * replace C- with XYZ
309 * NB arg4 = start column of selection for edit (base 0)
310 * arg5 = column after end of selection for edit
312 EditCommand edit = new EditCommand("", Action.REPLACE, "xyZ",
316 assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
317 assertEquals(1, seq.getStart());
318 assertEquals(8, seq.getEnd());
319 // Dataset sequence always uppercase
320 assertEquals("ABxyZDEF".toUpperCase(),
321 seq.getDatasetSequence().getSequenceAsString());
322 assertEquals(8, seq.getDatasetSequence().getEnd());
327 AlignmentI[] views = new AlignmentI[]
328 { new Alignment(new SequenceI[] { seq }) };
329 edit.undoCommand(views);
331 assertEquals("ABC--DEF", seq.getSequenceAsString());
332 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
333 assertEquals(1, seq.getStart());
334 assertEquals(6, seq.getEnd());
335 assertEquals(6, seq.getDatasetSequence().getEnd());
340 edit.doCommand(views);
342 assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
343 assertEquals(1, seq.getStart());
344 assertEquals(8, seq.getEnd());
345 // dataset sequence should be Uppercase
346 assertEquals("ABxyZDEF".toUpperCase(),
347 seq.getDatasetSequence().getSequenceAsString());
348 assertEquals(8, seq.getDatasetSequence().getEnd());
353 * Test replace command when it doesn't cause a sequence edit (see comment in
355 @Test(groups = { "Functional" })
356 public void testReplaceFirstResiduesWithGaps()
358 // test replace when gaps are inserted at start. Start/end should change
359 // w.r.t. original edited sequence.
360 SequenceI dsseq = seqs[1].getDatasetSequence();
361 EditCommand edit = new EditCommand("", Action.REPLACE, "----",
363 { seqs[1] }, 0, 4, al);
366 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
367 // and ds is preserved
368 assertTrue(dsseq == seqs[1].getDatasetSequence());
369 // and it is unchanged and UPPERCASE !
370 assertEquals("fghjklmnopq".toUpperCase(), dsseq.getSequenceAsString());
371 // and that alignment sequence start has been adjusted
372 assertEquals(5, seqs[1].getStart());
373 assertEquals(11, seqs[1].getEnd());
375 AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
377 edit.undoCommand(views);
379 // dataset sequence unchanged
380 assertTrue(dsseq == seqs[1].getDatasetSequence());
382 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
383 // and start/end numbering also restored
384 assertEquals(1, seqs[1].getStart());
385 assertEquals(11, seqs[1].getEnd());
388 edit.undoCommand(views);
390 // and repeat asserts for the original edit
393 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
394 // and ds is preserved
395 assertTrue(dsseq == seqs[1].getDatasetSequence());
396 // and it is unchanged AND UPPERCASE !
397 assertEquals("fghjklmnopq".toUpperCase(), dsseq.getSequenceAsString());
398 // and that alignment sequence start has been adjusted
399 assertEquals(5, seqs[1].getStart());
400 assertEquals(11, seqs[1].getEnd());
405 * Test that the addEdit command correctly merges insert gap commands when
408 @Test(groups = { "Functional" })
409 public void testAddEdit_multipleInsertGap()
412 * 3 insert gap in a row (aka mouse drag right):
414 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
415 new SequenceI[] { seqs[0] }, 1, 1, al);
417 SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
418 edited.setDatasetSequence(seqs[0].getDatasetSequence());
419 e = new EditCommand().new Edit(Action.INSERT_GAP,
420 new SequenceI[] { edited }, 2, 1, al);
422 edited = new Sequence("seq0", "a??bcdefghjk");
423 edited.setDatasetSequence(seqs[0].getDatasetSequence());
424 e = new EditCommand().new Edit(Action.INSERT_GAP,
425 new SequenceI[] { edited }, 3, 1, al);
427 assertEquals(1, testee.getSize());
428 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
429 assertEquals(1, testee.getEdit(0).getPosition());
430 assertEquals(3, testee.getEdit(0).getNumber());
433 * Add a non-contiguous edit - should not be merged.
435 e = new EditCommand().new Edit(Action.INSERT_GAP,
436 new SequenceI[] { edited }, 5, 2, al);
438 assertEquals(2, testee.getSize());
439 assertEquals(5, testee.getEdit(1).getPosition());
440 assertEquals(2, testee.getEdit(1).getNumber());
443 * Add a Delete after the Insert - should not be merged.
445 e = new EditCommand().new Edit(Action.DELETE_GAP,
446 new SequenceI[] { edited }, 6, 2, al);
448 assertEquals(3, testee.getSize());
449 assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
450 assertEquals(6, testee.getEdit(2).getPosition());
451 assertEquals(2, testee.getEdit(2).getNumber());
455 * Test that the addEdit command correctly merges delete gap commands when
458 @Test(groups = { "Functional" })
459 public void testAddEdit_multipleDeleteGap()
462 * 3 delete gap in a row (aka mouse drag left):
464 seqs[0].setSequence("a???bcdefghjk");
465 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
466 new SequenceI[] { seqs[0] }, 4, 1, al);
468 assertEquals(1, testee.getSize());
470 SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
471 edited.setDatasetSequence(seqs[0].getDatasetSequence());
472 e = new EditCommand().new Edit(Action.DELETE_GAP,
473 new SequenceI[] { edited }, 3, 1, al);
475 assertEquals(1, testee.getSize());
477 edited = new Sequence("seq0", "a?bcdefghjk");
478 edited.setDatasetSequence(seqs[0].getDatasetSequence());
479 e = new EditCommand().new Edit(Action.DELETE_GAP,
480 new SequenceI[] { edited }, 2, 1, al);
482 assertEquals(1, testee.getSize());
483 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
484 assertEquals(2, testee.getEdit(0).getPosition());
485 assertEquals(3, testee.getEdit(0).getNumber());
488 * Add a non-contiguous edit - should not be merged.
490 e = new EditCommand().new Edit(Action.DELETE_GAP,
491 new SequenceI[] { edited }, 2, 1, al);
493 assertEquals(2, testee.getSize());
494 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
495 assertEquals(2, testee.getEdit(1).getPosition());
496 assertEquals(1, testee.getEdit(1).getNumber());
499 * Add an Insert after the Delete - should not be merged.
501 e = new EditCommand().new Edit(Action.INSERT_GAP,
502 new SequenceI[] { edited }, 1, 1, al);
504 assertEquals(3, testee.getSize());
505 assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
506 assertEquals(1, testee.getEdit(2).getPosition());
507 assertEquals(1, testee.getEdit(2).getNumber());
511 * Test that the addEdit command correctly handles 'remove gaps' edits for the
512 * case when they appear contiguous but are acting on different sequences.
513 * They should not be merged.
515 @Test(groups = { "Functional" })
516 public void testAddEdit_removeAllGaps()
518 seqs[0].setSequence("a???bcdefghjk");
519 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
520 new SequenceI[] { seqs[0] }, 4, 1, al);
523 seqs[1].setSequence("f??ghjklmnopq");
524 Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[]
525 { seqs[1] }, 3, 1, al);
527 assertEquals(2, testee.getSize());
528 assertSame(e, testee.getEdit(0));
529 assertSame(e2, testee.getEdit(1));
533 * Test that the addEdit command correctly merges insert gap commands acting
534 * on a multi-sequence selection.
536 @Test(groups = { "Functional" })
537 public void testAddEdit_groupInsertGaps()
540 * 2 insert gap in a row (aka mouse drag right), on two sequences:
542 Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
543 seqs[0], seqs[1] }, 1, 1, al);
545 SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
546 seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
547 SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
548 seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
549 e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
550 seq1edited, seq2edited }, 2, 1, al);
553 assertEquals(1, testee.getSize());
554 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
555 assertEquals(1, testee.getEdit(0).getPosition());
556 assertEquals(2, testee.getEdit(0).getNumber());
557 assertEquals(seqs[0].getDatasetSequence(), testee.getEdit(0)
558 .getSequences()[0].getDatasetSequence());
559 assertEquals(seqs[1].getDatasetSequence(), testee.getEdit(0)
560 .getSequences()[1].getDatasetSequence());
564 * Test for 'undoing' a series of gap insertions.
566 * <li>Start: ABCDEF insert 2 at pos 1</li>
567 * <li>next: A--BCDEF insert 1 at pos 4</li>
568 * <li>next: A--B-CDEF insert 2 at pos 0</li>
569 * <li>last: --A--B-CDEF</li>
572 @Test(groups = { "Functional" })
573 public void testPriorState_multipleInserts()
575 EditCommand command = new EditCommand();
576 SequenceI seq = new Sequence("", "--A--B-CDEF");
577 SequenceI ds = new Sequence("", "ABCDEF");
578 seq.setDatasetSequence(ds);
579 SequenceI[] sqs = new SequenceI[] { seq };
580 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
582 e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
584 e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
587 Map<SequenceI, SequenceI> unwound = command.priorState(false);
588 assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
592 * Test for 'undoing' a series of gap deletions.
594 * <li>Start: A-B-C delete 1 at pos 1</li>
595 * <li>Next: AB-C delete 1 at pos 2</li>
599 @Test(groups = { "Functional" })
600 public void testPriorState_removeAllGaps()
602 EditCommand command = new EditCommand();
603 SequenceI seq = new Sequence("", "ABC");
604 SequenceI ds = new Sequence("", "ABC");
605 seq.setDatasetSequence(ds);
606 SequenceI[] sqs = new SequenceI[] { seq };
607 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
609 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
612 Map<SequenceI, SequenceI> unwound = command.priorState(false);
613 assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
617 * Test for 'undoing' a single delete edit.
619 @Test(groups = { "Functional" })
620 public void testPriorState_singleDelete()
622 EditCommand command = new EditCommand();
623 SequenceI seq = new Sequence("", "ABCDEF");
624 SequenceI ds = new Sequence("", "ABCDEF");
625 seq.setDatasetSequence(ds);
626 SequenceI[] sqs = new SequenceI[] { seq };
627 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
630 Map<SequenceI, SequenceI> unwound = command.priorState(false);
631 assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
635 * Test 'undoing' a single gap insertion edit command.
637 @Test(groups = { "Functional" })
638 public void testPriorState_singleInsert()
640 EditCommand command = new EditCommand();
641 SequenceI seq = new Sequence("", "AB---CDEF");
642 SequenceI ds = new Sequence("", "ABCDEF");
643 seq.setDatasetSequence(ds);
644 SequenceI[] sqs = new SequenceI[] { seq };
645 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
648 Map<SequenceI, SequenceI> unwound = command.priorState(false);
649 SequenceI prior = unwound.get(ds);
650 assertEquals("ABCDEF", prior.getSequenceAsString());
651 assertEquals(1, prior.getStart());
652 assertEquals(6, prior.getEnd());
656 * Test 'undoing' a single gap insertion edit command, on a sequence whose
657 * start residue is other than 1
659 @Test(groups = { "Functional" })
660 public void testPriorState_singleInsertWithOffset()
662 EditCommand command = new EditCommand();
663 SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
664 // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
665 // seq.setDatasetSequence(ds);
666 seq.createDatasetSequence();
667 SequenceI[] sqs = new SequenceI[] { seq };
668 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
671 Map<SequenceI, SequenceI> unwound = command.priorState(false);
672 SequenceI prior = unwound.get(seq.getDatasetSequence());
673 assertEquals("ABCDEF", prior.getSequenceAsString());
674 assertEquals(8, prior.getStart());
675 assertEquals(13, prior.getEnd());
679 * Test that mimics 'remove all gaps' action. This generates delete gap edits
680 * for contiguous gaps in each sequence separately.
682 @Test(groups = { "Functional" })
683 public void testPriorState_removeGapsMultipleSeqs()
685 EditCommand command = new EditCommand();
686 String original1 = "--ABC-DEF";
687 String original2 = "FG-HI--J";
688 String original3 = "M-NOPQ";
691 * Two edits for the first sequence
693 SequenceI seq = new Sequence("", "ABC-DEF");
694 SequenceI ds1 = new Sequence("", "ABCDEF");
695 seq.setDatasetSequence(ds1);
696 SequenceI[] sqs = new SequenceI[] { seq };
697 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
699 seq = new Sequence("", "ABCDEF");
700 seq.setDatasetSequence(ds1);
701 sqs = new SequenceI[] { seq };
702 e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
706 * Two edits for the second sequence
708 seq = new Sequence("", "FGHI--J");
709 SequenceI ds2 = new Sequence("", "FGHIJ");
710 seq.setDatasetSequence(ds2);
711 sqs = new SequenceI[] { seq };
712 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
714 seq = new Sequence("", "FGHIJ");
715 seq.setDatasetSequence(ds2);
716 sqs = new SequenceI[] { seq };
717 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
721 * One edit for the third sequence.
723 seq = new Sequence("", "MNOPQ");
724 SequenceI ds3 = new Sequence("", "MNOPQ");
725 seq.setDatasetSequence(ds3);
726 sqs = new SequenceI[] { seq };
727 e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
730 Map<SequenceI, SequenceI> unwound = command.priorState(false);
731 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
732 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
733 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
737 * Test that mimics 'remove all gapped columns' action. This generates a
738 * series Delete Gap edits that each act on all sequences that share a gapped
741 @Test(groups = { "Functional" })
742 public void testPriorState_removeGappedCols()
744 EditCommand command = new EditCommand();
745 String original1 = "--ABC--DEF";
746 String original2 = "-G-HI--J";
747 String original3 = "-M-NO--PQ";
750 * First edit deletes the first column.
752 SequenceI seq1 = new Sequence("", "-ABC--DEF");
753 SequenceI ds1 = new Sequence("", "ABCDEF");
754 seq1.setDatasetSequence(ds1);
755 SequenceI seq2 = new Sequence("", "G-HI--J");
756 SequenceI ds2 = new Sequence("", "GHIJ");
757 seq2.setDatasetSequence(ds2);
758 SequenceI seq3 = new Sequence("", "M-NO--PQ");
759 SequenceI ds3 = new Sequence("", "MNOPQ");
760 seq3.setDatasetSequence(ds3);
761 SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
762 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
766 * Second edit deletes what is now columns 4 and 5.
768 seq1 = new Sequence("", "-ABCDEF");
769 seq1.setDatasetSequence(ds1);
770 seq2 = new Sequence("", "G-HIJ");
771 seq2.setDatasetSequence(ds2);
772 seq3 = new Sequence("", "M-NOPQ");
773 seq3.setDatasetSequence(ds3);
774 sqs = new SequenceI[] { seq1, seq2, seq3 };
775 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
778 Map<SequenceI, SequenceI> unwound = command.priorState(false);
779 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
780 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
781 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
782 assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
783 assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
784 assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
788 * Test a cut action's relocation of sequence features
790 @Test(groups = { "Functional" })
791 public void testCut_withFeatures()
794 * create sequence features before, after and overlapping
795 * a cut of columns/residues 4-7
797 SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
798 seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
800 seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
802 seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
804 seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
806 seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
810 * add some contact features
812 SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
814 seq0.addSequenceFeature(internalContact); // should get deleted
815 SequenceFeature overlapLeftContact = new SequenceFeature(
816 "disulphide bond", "", 2, 6, 0f, null);
817 seq0.addSequenceFeature(overlapLeftContact); // should get deleted
818 SequenceFeature overlapRightContact = new SequenceFeature(
819 "disulphide bond", "", 5, 8, 0f, null);
820 seq0.addSequenceFeature(overlapRightContact); // should get deleted
821 SequenceFeature spanningContact = new SequenceFeature(
822 "disulphide bond", "", 2, 9, 0f, null);
823 seq0.addSequenceFeature(spanningContact); // should get shortened 3'
826 * cut columns 3-6 (base 0), residues d-g 4-7
828 Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
829 EditCommand.cut(ec, new AlignmentI[] { al });
831 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
832 SequenceFeatures.sortFeatures(sfs, true);
834 assertEquals(5, sfs.size()); // features internal to cut were deleted
835 SequenceFeature sf = sfs.get(0);
836 assertEquals("before", sf.getType());
837 assertEquals(1, sf.getBegin());
838 assertEquals(3, sf.getEnd());
840 assertEquals("disulphide bond", sf.getType());
841 assertEquals(2, sf.getBegin());
842 assertEquals(5, sf.getEnd()); // truncated by cut
844 assertEquals("overlap left", sf.getType());
845 assertEquals(2, sf.getBegin());
846 assertEquals(3, sf.getEnd()); // truncated by cut
848 assertEquals("after", sf.getType());
849 assertEquals(4, sf.getBegin()); // shifted left by cut
850 assertEquals(6, sf.getEnd()); // shifted left by cut
852 assertEquals("overlap right", sf.getType());
853 assertEquals(4, sf.getBegin()); // shifted left by cut
854 assertEquals(4, sf.getEnd()); // truncated by cut
858 * Test a cut action's relocation of sequence features, with full coverage of
859 * all possible feature and cut locations for a 5-position ungapped sequence
861 @Test(groups = { "Functional" })
862 public void testCut_withFeatures_exhaustive()
865 * create a sequence features on each subrange of 1-5
867 SequenceI seq0 = new Sequence("seq", "ABCDE");
870 seq0.setStart(start);
872 AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
873 alignment.setDataset(null);
876 * create a new alignment with shared dataset sequence
878 AlignmentI copy = new Alignment(
880 { alignment.getDataset().getSequenceAt(0).deriveSequence() });
881 SequenceI copySeq0 = copy.getSequenceAt(0);
883 for (int from = start; from <= end; from++)
885 for (int to = from; to <= end; to++)
887 String desc = String.format("%d-%d", from, to);
888 SequenceFeature sf = new SequenceFeature("test", desc, from, to,
890 sf.setValue("from", Integer.valueOf(from));
891 sf.setValue("to", Integer.valueOf(to));
892 seq0.addSequenceFeature(sf);
896 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
897 assertEquals(func(5), sfs.size());
898 assertEquals(sfs, copySeq0.getSequenceFeatures());
899 String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
902 * now perform all possible cuts of subranges of columns 1-5
903 * and validate the resulting remaining sequence features!
905 SequenceI[] sqs = new SequenceI[] { seq0 };
907 for (int from = 0; from < seq0.getLength(); from++)
909 for (int to = from; to < seq0.getLength(); to++)
911 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
912 - from + 1), alignment);
913 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
914 boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
915 .getDatasetSequence();
917 verifyCut(seq0, from, to, msg, start);
920 * verify copy alignment dataset sequence unaffected
922 assertEquals("Original dataset sequence was modified",
923 copySequenceFeatures,
924 copySeq0.getSequenceFeatures().toString());
927 * verify any new dataset sequence was added to the
930 assertEquals("Wrong Dataset size after " + msg,
931 newDatasetSequence ? 2 : 1,
932 alignment.getDataset().getHeight());
935 * undo and verify all restored
937 AlignmentI[] views = new AlignmentI[] { alignment };
938 ec.undoCommand(views);
939 sfs = seq0.getSequenceFeatures();
940 assertEquals("After undo of " + msg, func(5), sfs.size());
941 verifyUndo(from, to, sfs);
944 * verify copy alignment dataset sequence still unaffected
945 * and alignment dataset has shrunk (if it was added to)
947 assertEquals("Original dataset sequence was modified",
948 copySequenceFeatures,
949 copySeq0.getSequenceFeatures().toString());
950 assertEquals("Wrong Dataset size after Undo of " + msg, 1,
951 alignment.getDataset().getHeight());
957 verifyCut(seq0, from, to, msg, start);
960 * verify copy alignment dataset sequence unaffected
961 * and any new dataset sequence readded to alignment dataset
963 assertEquals("Original dataset sequence was modified",
964 copySequenceFeatures,
965 copySeq0.getSequenceFeatures().toString());
966 assertEquals("Wrong Dataset size after Redo of " + msg,
967 newDatasetSequence ? 2 : 1,
968 alignment.getDataset().getHeight());
971 * undo ready for next cut
973 ec.undoCommand(views);
976 * final verify that copy alignment dataset sequence is still unaffected
977 * and that alignment dataset has shrunk
979 assertEquals("Original dataset sequence was modified",
980 copySequenceFeatures,
981 copySeq0.getSequenceFeatures().toString());
982 assertEquals("Wrong Dataset size after final Undo of " + msg, 1,
983 alignment.getDataset().getHeight());
989 * Verify by inspection that the sequence features left on the sequence after
990 * a cut match the expected results. The trick to this is that we can parse
991 * each feature's original start-end positions from its description.
999 protected void verifyCut(SequenceI seq0, int from, int to,
1000 final String msg, int seqStart)
1002 List<SequenceFeature> sfs;
1003 sfs = seq0.getSequenceFeatures();
1005 Collections.sort(sfs, BY_DESCRIPTION);
1008 * confirm the number of features has reduced by the
1009 * number of features within the cut region i.e. by
1010 * func(length of cut); exception is a cut at start or end of sequence,
1011 * which retains the original coordinates, dataset sequence
1012 * and all its features
1014 boolean datasetRetained = from == 0 || to == 4;
1015 if (datasetRetained)
1017 // dataset and all features retained
1018 assertEquals(msg, func(5), sfs.size());
1020 else if (to - from == 4)
1022 // all columns were cut
1023 assertTrue(sfs.isEmpty());
1027 // failure in checkFeatureRelocation is more informative!
1028 assertEquals(msg + "wrong number of features left", func(5)
1029 - func(to - from + 1), sfs.size());
1033 * inspect individual features
1035 for (SequenceFeature sf : sfs)
1037 verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
1043 * Check that after Undo, every feature has start/end that match its original
1044 * "start" and "end" properties
1050 protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
1052 for (SequenceFeature sf : sfs)
1054 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1055 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1056 String msg = String.format(
1057 "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
1059 assertEquals(msg + "start", oldFrom, sf.getBegin());
1060 assertEquals(msg + "end", oldTo, sf.getEnd());
1065 * Helper method to check a feature has been correctly relocated after a cut
1069 * start of cut (first residue cut 1..)
1071 * end of cut (last residue cut 1..)
1075 private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1076 boolean newDataset, int seqStart)
1078 // TODO handle the gapped sequence case as well
1079 int cutSize = to - from + 1;
1080 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1081 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1082 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1083 final int oldToPosition = oldTo - seqStart + 1; // 1..
1085 String msg = String.format(
1086 "Feature %s relocated to %d-%d after cut of %d-%d",
1087 sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1090 // dataset retained with all features unchanged
1091 assertEquals("0: " + msg, oldFrom, sf.getBegin());
1092 assertEquals("0: " + msg, oldTo, sf.getEnd());
1094 else if (oldToPosition < from)
1096 // before cut region so unchanged
1097 assertEquals("1: " + msg, oldFrom, sf.getBegin());
1098 assertEquals("2: " + msg, oldTo, sf.getEnd());
1100 else if (oldFromPosition > to)
1102 // follows cut region - shift by size of cut
1103 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1105 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1108 else if (oldFromPosition < from && oldToPosition > to)
1110 // feature encloses cut region - shrink it right
1111 assertEquals("5: " + msg, oldFrom, sf.getBegin());
1112 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1114 else if (oldFromPosition < from)
1116 // feature overlaps left side of cut region - truncated right
1117 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1119 else if (oldToPosition > to)
1121 // feature overlaps right side of cut region - truncated left
1122 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1124 assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1129 // feature internal to cut - should have been deleted!
1130 Assert.fail(msg + " - should have been deleted");
1135 * Test a cut action's relocation of sequence features
1137 @Test(groups = { "Functional" })
1138 public void testCut_withFeatures5prime()
1140 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1141 seq0.createDatasetSequence();
1142 assertEquals(8, seq0.getStart());
1143 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
1145 SequenceI[] seqsArray = new SequenceI[] { seq0 };
1146 AlignmentI alignment = new Alignment(seqsArray);
1149 * cut columns of A-B; same dataset sequence is retained, aligned sequence
1152 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1153 EditCommand.cut(ec, new AlignmentI[] { alignment });
1156 * feature on CC(10-11) should still be on CC(10-11)
1158 assertSame(seq0, alignment.getSequenceAt(0));
1159 assertEquals(10, seq0.getStart());
1160 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1161 assertEquals(1, sfs.size());
1162 SequenceFeature sf = sfs.get(0);
1163 assertEquals(10, sf.getBegin());
1164 assertEquals(11, sf.getEnd());