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 java.util.Locale;
25 import static org.testng.AssertJUnit.assertEquals;
26 import static org.testng.AssertJUnit.assertSame;
27 import static org.testng.AssertJUnit.assertTrue;
29 import jalview.commands.EditCommand.Action;
30 import jalview.commands.EditCommand.Edit;
31 import jalview.datamodel.Alignment;
32 import jalview.datamodel.AlignmentI;
33 import jalview.datamodel.Sequence;
34 import jalview.datamodel.SequenceFeature;
35 import jalview.datamodel.SequenceI;
36 import jalview.datamodel.features.SequenceFeatures;
37 import jalview.gui.JvOptionPane;
39 import java.util.Collections;
40 import java.util.Comparator;
41 import java.util.List;
44 import org.testng.Assert;
45 import org.testng.annotations.BeforeClass;
46 import org.testng.annotations.BeforeMethod;
47 import org.testng.annotations.Test;
50 * Unit tests for EditCommand
55 public class EditCommandTest
57 private static Comparator<SequenceFeature> BY_DESCRIPTION = new Comparator<SequenceFeature>()
61 public int compare(SequenceFeature o1, SequenceFeature o2)
63 return o1.getDescription().compareTo(o2.getDescription());
67 private EditCommand testee;
69 private SequenceI[] seqs;
74 * compute n(n+1)/2 e.g.
75 * func(5) = 5 + 4 + 3 + 2 + 1 = 15
77 private static int func(int i)
79 return i * (i + 1) / 2;
82 @BeforeClass(alwaysRun = true)
83 public void setUpJvOptionPane()
85 JvOptionPane.setInteractiveMode(false);
86 JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
89 @BeforeMethod(alwaysRun = true)
92 testee = new EditCommand();
93 seqs = new SequenceI[4];
94 seqs[0] = new Sequence("seq0", "abcdefghjk");
95 seqs[0].setDatasetSequence(new Sequence("seq0ds", "ABCDEFGHJK"));
96 seqs[1] = new Sequence("seq1", "fghjklmnopq");
97 seqs[1].setDatasetSequence(new Sequence("seq1ds", "FGHJKLMNOPQ"));
98 seqs[2] = new Sequence("seq2", "qrstuvwxyz");
99 seqs[2].setDatasetSequence(new Sequence("seq2ds", "QRSTUVWXYZ"));
100 seqs[3] = new Sequence("seq3", "1234567890");
101 seqs[3].setDatasetSequence(new Sequence("seq3ds", "1234567890"));
102 al = new Alignment(seqs);
103 al.setGapCharacter('?');
107 * Test inserting gap characters
109 @Test(groups = { "Functional" })
110 public void testAppendEdit_insertGap()
112 // set a non-standard gap character to prove it is actually used
113 testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true);
114 assertEquals("abcd???efghjk", seqs[0].getSequenceAsString());
115 assertEquals("fghj???klmnopq", seqs[1].getSequenceAsString());
116 assertEquals("qrst???uvwxyz", seqs[2].getSequenceAsString());
117 assertEquals("1234???567890", seqs[3].getSequenceAsString());
119 // todo: test for handling out of range positions?
123 * Test deleting characters from sequences. Note the deleteGap() action does
124 * not check that only gap characters are being removed.
126 @Test(groups = { "Functional" })
127 public void testAppendEdit_deleteGap()
129 testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true);
130 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
131 assertEquals("fghjnopq", seqs[1].getSequenceAsString());
132 assertEquals("qrstxyz", seqs[2].getSequenceAsString());
133 assertEquals("1234890", seqs[3].getSequenceAsString());
137 * Test a cut action. The command should store the cut characters to support
140 @Test(groups = { "Functional" })
141 public void testCut()
143 Edit ec = testee.new Edit(Action.CUT, seqs, 4, 3, al);
144 EditCommand.cut(ec, new AlignmentI[] { al });
145 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
146 assertEquals("fghjnopq", seqs[1].getSequenceAsString());
147 assertEquals("qrstxyz", seqs[2].getSequenceAsString());
148 assertEquals("1234890", seqs[3].getSequenceAsString());
150 assertEquals("efg", new String(ec.string[0]));
151 assertEquals("klm", new String(ec.string[1]));
152 assertEquals("uvw", new String(ec.string[2]));
153 assertEquals("567", new String(ec.string[3]));
154 // TODO: case where whole sequence is deleted as nothing left; etc
158 * Test a Paste action, followed by Undo and Redo
160 @Test(groups = { "Functional" }, enabled = false)
161 public void testPaste_undo_redo()
163 // TODO code this test properly, bearing in mind that:
164 // Paste action requires something on the clipboard (Cut/Copy)
165 // - EditCommand.paste doesn't add sequences to the alignment
166 // ... that is done in AlignFrame.paste()
167 // ... unless as a Redo
170 SequenceI[] newSeqs = new SequenceI[2];
171 newSeqs[0] = new Sequence("newseq0", "ACEFKL");
172 newSeqs[1] = new Sequence("newseq1", "JWMPDH");
174 new EditCommand("Paste", Action.PASTE, newSeqs, 0, al.getWidth(), al);
175 assertEquals(6, al.getSequences().size());
176 assertEquals("1234567890", seqs[3].getSequenceAsString());
177 assertEquals("ACEFKL", seqs[4].getSequenceAsString());
178 assertEquals("JWMPDH", seqs[5].getSequenceAsString());
182 * Test insertGap followed by undo command
184 @Test(groups = { "Functional" })
185 public void testUndo_insertGap()
187 // Edit ec = testee.new Edit(Action.INSERT_GAP, seqs, 4, 3, '?');
188 testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true);
189 // check something changed
190 assertEquals("abcd???efghjk", seqs[0].getSequenceAsString());
191 testee.undoCommand(new AlignmentI[] { al });
192 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
193 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
194 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
195 assertEquals("1234567890", seqs[3].getSequenceAsString());
199 * Test deleteGap followed by undo command
201 @Test(groups = { "Functional" })
202 public void testUndo_deleteGap()
204 testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true);
205 // check something changed
206 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
207 testee.undoCommand(new AlignmentI[] { al });
208 // deleteGap doesn't 'remember' deleted characters, only gaps get put back
209 assertEquals("abcd???hjk", seqs[0].getSequenceAsString());
210 assertEquals("fghj???nopq", seqs[1].getSequenceAsString());
211 assertEquals("qrst???xyz", seqs[2].getSequenceAsString());
212 assertEquals("1234???890", seqs[3].getSequenceAsString());
216 * Test several commands followed by an undo command
218 @Test(groups = { "Functional" })
219 public void testUndo_multipleCommands()
221 // delete positions 3/4/5 (counting from 1)
222 testee.appendEdit(Action.DELETE_GAP, seqs, 2, 3, al, true);
223 assertEquals("abfghjk", seqs[0].getSequenceAsString());
224 assertEquals("1267890", seqs[3].getSequenceAsString());
226 // insert 2 gaps after the second residue
227 testee.appendEdit(Action.INSERT_GAP, seqs, 2, 2, al, true);
228 assertEquals("ab??fghjk", seqs[0].getSequenceAsString());
229 assertEquals("12??67890", seqs[3].getSequenceAsString());
231 // delete positions 4/5/6
232 testee.appendEdit(Action.DELETE_GAP, seqs, 3, 3, al, true);
233 assertEquals("ab?hjk", seqs[0].getSequenceAsString());
234 assertEquals("12?890", seqs[3].getSequenceAsString());
236 // undo edit commands
237 testee.undoCommand(new AlignmentI[] { al });
238 assertEquals("ab?????hjk", seqs[0].getSequenceAsString());
239 assertEquals("12?????890", seqs[3].getSequenceAsString());
243 * Unit test for JAL-1594 bug: click and drag sequence right to insert gaps -
244 * undo did not remove them all.
246 @Test(groups = { "Functional" })
247 public void testUndo_multipleInsertGaps()
249 testee.appendEdit(Action.INSERT_GAP, seqs, 4, 1, al, true);
250 testee.appendEdit(Action.INSERT_GAP, seqs, 5, 1, al, true);
251 testee.appendEdit(Action.INSERT_GAP, seqs, 6, 1, al, true);
253 // undo edit commands
254 testee.undoCommand(new AlignmentI[] { al });
255 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
256 assertEquals("1234567890", seqs[3].getSequenceAsString());
261 * Test cut followed by undo command
263 @Test(groups = { "Functional" })
264 public void testUndo_cut()
266 testee.appendEdit(Action.CUT, seqs, 4, 3, al, true);
267 // check something changed
268 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
269 testee.undoCommand(new AlignmentI[] { al });
270 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
271 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
272 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
273 assertEquals("1234567890", seqs[3].getSequenceAsString());
277 * Test the replace command (used to manually edit a sequence)
279 @Test(groups = { "Functional" })
280 public void testReplace()
282 // seem to need a dataset sequence on the edited sequence here
283 seqs[1].createDatasetSequence();
284 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
285 // NB command.number holds end position for a Replace command
286 new EditCommand("", Action.REPLACE, "Z-xY", new SequenceI[] { seqs[1] },
288 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
289 assertEquals("fghjZ-xYopq", seqs[1].getSequenceAsString());
290 // Dataset Sequence should always be uppercase
291 assertEquals("fghjZxYopq".toUpperCase(Locale.ROOT),
292 seqs[1].getDatasetSequence().getSequenceAsString());
293 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
294 assertEquals("1234567890", seqs[3].getSequenceAsString());
298 * Test the replace command (used to manually edit a sequence)
300 @Test(groups = { "Functional" })
301 public void testReplace_withGaps()
303 SequenceI seq = new Sequence("seq", "ABC--DEF");
304 seq.createDatasetSequence();
305 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
306 assertEquals(1, seq.getStart());
307 assertEquals(6, seq.getEnd());
310 * replace C- with XYZ
311 * NB arg4 = start column of selection for edit (base 0)
312 * arg5 = column after end of selection for edit
314 EditCommand edit = new EditCommand("", Action.REPLACE, "xyZ",
318 assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
319 assertEquals(1, seq.getStart());
320 assertEquals(8, seq.getEnd());
321 // Dataset sequence always uppercase
322 assertEquals("ABxyZDEF".toUpperCase(Locale.ROOT),
323 seq.getDatasetSequence().getSequenceAsString());
324 assertEquals(8, seq.getDatasetSequence().getEnd());
329 AlignmentI[] views = new AlignmentI[]
330 { new Alignment(new SequenceI[] { seq }) };
331 edit.undoCommand(views);
333 assertEquals("ABC--DEF", seq.getSequenceAsString());
334 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
335 assertEquals(1, seq.getStart());
336 assertEquals(6, seq.getEnd());
337 assertEquals(6, seq.getDatasetSequence().getEnd());
342 edit.doCommand(views);
344 assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
345 assertEquals(1, seq.getStart());
346 assertEquals(8, seq.getEnd());
347 // dataset sequence should be Uppercase
348 assertEquals("ABxyZDEF".toUpperCase(Locale.ROOT),
349 seq.getDatasetSequence().getSequenceAsString());
350 assertEquals(8, seq.getDatasetSequence().getEnd());
355 * Test replace command when it doesn't cause a sequence edit (see comment in
357 @Test(groups = { "Functional" })
358 public void testReplaceFirstResiduesWithGaps()
360 // test replace when gaps are inserted at start. Start/end should change
361 // w.r.t. original edited sequence.
362 SequenceI dsseq = seqs[1].getDatasetSequence();
363 EditCommand edit = new EditCommand("", Action.REPLACE, "----",
365 { seqs[1] }, 0, 4, al);
368 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
369 // and ds is preserved
370 assertTrue(dsseq == seqs[1].getDatasetSequence());
371 // and it is unchanged and UPPERCASE !
372 assertEquals("fghjklmnopq".toUpperCase(Locale.ROOT), dsseq.getSequenceAsString());
373 // and that alignment sequence start has been adjusted
374 assertEquals(5, seqs[1].getStart());
375 assertEquals(11, seqs[1].getEnd());
377 AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
379 edit.undoCommand(views);
381 // dataset sequence unchanged
382 assertTrue(dsseq == seqs[1].getDatasetSequence());
384 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
385 // and start/end numbering also restored
386 assertEquals(1, seqs[1].getStart());
387 assertEquals(11, seqs[1].getEnd());
390 edit.undoCommand(views);
392 // and repeat asserts for the original edit
395 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
396 // and ds is preserved
397 assertTrue(dsseq == seqs[1].getDatasetSequence());
398 // and it is unchanged AND UPPERCASE !
399 assertEquals("fghjklmnopq".toUpperCase(Locale.ROOT), dsseq.getSequenceAsString());
400 // and that alignment sequence start has been adjusted
401 assertEquals(5, seqs[1].getStart());
402 assertEquals(11, seqs[1].getEnd());
407 * Test that the addEdit command correctly merges insert gap commands when
410 @Test(groups = { "Functional" })
411 public void testAddEdit_multipleInsertGap()
414 * 3 insert gap in a row (aka mouse drag right):
416 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
417 new SequenceI[] { seqs[0] }, 1, 1, al);
419 SequenceI 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 }, 2, 1, al);
424 edited = new Sequence("seq0", "a??bcdefghjk");
425 edited.setDatasetSequence(seqs[0].getDatasetSequence());
426 e = new EditCommand().new Edit(Action.INSERT_GAP,
427 new SequenceI[] { edited }, 3, 1, al);
429 assertEquals(1, testee.getSize());
430 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
431 assertEquals(1, testee.getEdit(0).getPosition());
432 assertEquals(3, testee.getEdit(0).getNumber());
435 * Add a non-contiguous edit - should not be merged.
437 e = new EditCommand().new Edit(Action.INSERT_GAP,
438 new SequenceI[] { edited }, 5, 2, al);
440 assertEquals(2, testee.getSize());
441 assertEquals(5, testee.getEdit(1).getPosition());
442 assertEquals(2, testee.getEdit(1).getNumber());
445 * Add a Delete after the Insert - should not be merged.
447 e = new EditCommand().new Edit(Action.DELETE_GAP,
448 new SequenceI[] { edited }, 6, 2, al);
450 assertEquals(3, testee.getSize());
451 assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
452 assertEquals(6, testee.getEdit(2).getPosition());
453 assertEquals(2, testee.getEdit(2).getNumber());
457 * Test that the addEdit command correctly merges delete gap commands when
460 @Test(groups = { "Functional" })
461 public void testAddEdit_multipleDeleteGap()
464 * 3 delete gap in a row (aka mouse drag left):
466 seqs[0].setSequence("a???bcdefghjk");
467 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
468 new SequenceI[] { seqs[0] }, 4, 1, al);
470 assertEquals(1, testee.getSize());
472 SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
473 edited.setDatasetSequence(seqs[0].getDatasetSequence());
474 e = new EditCommand().new Edit(Action.DELETE_GAP,
475 new SequenceI[] { edited }, 3, 1, al);
477 assertEquals(1, testee.getSize());
479 edited = new Sequence("seq0", "a?bcdefghjk");
480 edited.setDatasetSequence(seqs[0].getDatasetSequence());
481 e = new EditCommand().new Edit(Action.DELETE_GAP,
482 new SequenceI[] { edited }, 2, 1, al);
484 assertEquals(1, testee.getSize());
485 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
486 assertEquals(2, testee.getEdit(0).getPosition());
487 assertEquals(3, testee.getEdit(0).getNumber());
490 * Add a non-contiguous edit - should not be merged.
492 e = new EditCommand().new Edit(Action.DELETE_GAP,
493 new SequenceI[] { edited }, 2, 1, al);
495 assertEquals(2, testee.getSize());
496 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
497 assertEquals(2, testee.getEdit(1).getPosition());
498 assertEquals(1, testee.getEdit(1).getNumber());
501 * Add an Insert after the Delete - should not be merged.
503 e = new EditCommand().new Edit(Action.INSERT_GAP,
504 new SequenceI[] { edited }, 1, 1, al);
506 assertEquals(3, testee.getSize());
507 assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
508 assertEquals(1, testee.getEdit(2).getPosition());
509 assertEquals(1, testee.getEdit(2).getNumber());
513 * Test that the addEdit command correctly handles 'remove gaps' edits for the
514 * case when they appear contiguous but are acting on different sequences.
515 * They should not be merged.
517 @Test(groups = { "Functional" })
518 public void testAddEdit_removeAllGaps()
520 seqs[0].setSequence("a???bcdefghjk");
521 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
522 new SequenceI[] { seqs[0] }, 4, 1, al);
525 seqs[1].setSequence("f??ghjklmnopq");
526 Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[]
527 { seqs[1] }, 3, 1, al);
529 assertEquals(2, testee.getSize());
530 assertSame(e, testee.getEdit(0));
531 assertSame(e2, testee.getEdit(1));
535 * Test that the addEdit command correctly merges insert gap commands acting
536 * on a multi-sequence selection.
538 @Test(groups = { "Functional" })
539 public void testAddEdit_groupInsertGaps()
542 * 2 insert gap in a row (aka mouse drag right), on two sequences:
544 Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
545 seqs[0], seqs[1] }, 1, 1, al);
547 SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
548 seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
549 SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
550 seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
551 e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
552 seq1edited, seq2edited }, 2, 1, al);
555 assertEquals(1, testee.getSize());
556 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
557 assertEquals(1, testee.getEdit(0).getPosition());
558 assertEquals(2, testee.getEdit(0).getNumber());
559 assertEquals(seqs[0].getDatasetSequence(), testee.getEdit(0)
560 .getSequences()[0].getDatasetSequence());
561 assertEquals(seqs[1].getDatasetSequence(), testee.getEdit(0)
562 .getSequences()[1].getDatasetSequence());
566 * Test for 'undoing' a series of gap insertions.
568 * <li>Start: ABCDEF insert 2 at pos 1</li>
569 * <li>next: A--BCDEF insert 1 at pos 4</li>
570 * <li>next: A--B-CDEF insert 2 at pos 0</li>
571 * <li>last: --A--B-CDEF</li>
574 @Test(groups = { "Functional" })
575 public void testPriorState_multipleInserts()
577 EditCommand command = new EditCommand();
578 SequenceI seq = new Sequence("", "--A--B-CDEF");
579 SequenceI ds = new Sequence("", "ABCDEF");
580 seq.setDatasetSequence(ds);
581 SequenceI[] sqs = new SequenceI[] { seq };
582 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
584 e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
586 e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
589 Map<SequenceI, SequenceI> unwound = command.priorState(false);
590 assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
594 * Test for 'undoing' a series of gap deletions.
596 * <li>Start: A-B-C delete 1 at pos 1</li>
597 * <li>Next: AB-C delete 1 at pos 2</li>
601 @Test(groups = { "Functional" })
602 public void testPriorState_removeAllGaps()
604 EditCommand command = new EditCommand();
605 SequenceI seq = new Sequence("", "ABC");
606 SequenceI ds = new Sequence("", "ABC");
607 seq.setDatasetSequence(ds);
608 SequenceI[] sqs = new SequenceI[] { seq };
609 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
611 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
614 Map<SequenceI, SequenceI> unwound = command.priorState(false);
615 assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
619 * Test for 'undoing' a single delete edit.
621 @Test(groups = { "Functional" })
622 public void testPriorState_singleDelete()
624 EditCommand command = new EditCommand();
625 SequenceI seq = new Sequence("", "ABCDEF");
626 SequenceI ds = new Sequence("", "ABCDEF");
627 seq.setDatasetSequence(ds);
628 SequenceI[] sqs = new SequenceI[] { seq };
629 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
632 Map<SequenceI, SequenceI> unwound = command.priorState(false);
633 assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
637 * Test 'undoing' a single gap insertion edit command.
639 @Test(groups = { "Functional" })
640 public void testPriorState_singleInsert()
642 EditCommand command = new EditCommand();
643 SequenceI seq = new Sequence("", "AB---CDEF");
644 SequenceI ds = new Sequence("", "ABCDEF");
645 seq.setDatasetSequence(ds);
646 SequenceI[] sqs = new SequenceI[] { seq };
647 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
650 Map<SequenceI, SequenceI> unwound = command.priorState(false);
651 SequenceI prior = unwound.get(ds);
652 assertEquals("ABCDEF", prior.getSequenceAsString());
653 assertEquals(1, prior.getStart());
654 assertEquals(6, prior.getEnd());
658 * Test 'undoing' a single gap insertion edit command, on a sequence whose
659 * start residue is other than 1
661 @Test(groups = { "Functional" })
662 public void testPriorState_singleInsertWithOffset()
664 EditCommand command = new EditCommand();
665 SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
666 // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
667 // seq.setDatasetSequence(ds);
668 seq.createDatasetSequence();
669 SequenceI[] sqs = new SequenceI[] { seq };
670 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
673 Map<SequenceI, SequenceI> unwound = command.priorState(false);
674 SequenceI prior = unwound.get(seq.getDatasetSequence());
675 assertEquals("ABCDEF", prior.getSequenceAsString());
676 assertEquals(8, prior.getStart());
677 assertEquals(13, prior.getEnd());
681 * Test that mimics 'remove all gaps' action. This generates delete gap edits
682 * for contiguous gaps in each sequence separately.
684 @Test(groups = { "Functional" })
685 public void testPriorState_removeGapsMultipleSeqs()
687 EditCommand command = new EditCommand();
688 String original1 = "--ABC-DEF";
689 String original2 = "FG-HI--J";
690 String original3 = "M-NOPQ";
693 * Two edits for the first sequence
695 SequenceI seq = new Sequence("", "ABC-DEF");
696 SequenceI ds1 = new Sequence("", "ABCDEF");
697 seq.setDatasetSequence(ds1);
698 SequenceI[] sqs = new SequenceI[] { seq };
699 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
701 seq = new Sequence("", "ABCDEF");
702 seq.setDatasetSequence(ds1);
703 sqs = new SequenceI[] { seq };
704 e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
708 * Two edits for the second sequence
710 seq = new Sequence("", "FGHI--J");
711 SequenceI ds2 = new Sequence("", "FGHIJ");
712 seq.setDatasetSequence(ds2);
713 sqs = new SequenceI[] { seq };
714 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
716 seq = new Sequence("", "FGHIJ");
717 seq.setDatasetSequence(ds2);
718 sqs = new SequenceI[] { seq };
719 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
723 * One edit for the third sequence.
725 seq = new Sequence("", "MNOPQ");
726 SequenceI ds3 = new Sequence("", "MNOPQ");
727 seq.setDatasetSequence(ds3);
728 sqs = new SequenceI[] { seq };
729 e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
732 Map<SequenceI, SequenceI> unwound = command.priorState(false);
733 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
734 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
735 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
739 * Test that mimics 'remove all gapped columns' action. This generates a
740 * series Delete Gap edits that each act on all sequences that share a gapped
743 @Test(groups = { "Functional" })
744 public void testPriorState_removeGappedCols()
746 EditCommand command = new EditCommand();
747 String original1 = "--ABC--DEF";
748 String original2 = "-G-HI--J";
749 String original3 = "-M-NO--PQ";
752 * First edit deletes the first column.
754 SequenceI seq1 = new Sequence("", "-ABC--DEF");
755 SequenceI ds1 = new Sequence("", "ABCDEF");
756 seq1.setDatasetSequence(ds1);
757 SequenceI seq2 = new Sequence("", "G-HI--J");
758 SequenceI ds2 = new Sequence("", "GHIJ");
759 seq2.setDatasetSequence(ds2);
760 SequenceI seq3 = new Sequence("", "M-NO--PQ");
761 SequenceI ds3 = new Sequence("", "MNOPQ");
762 seq3.setDatasetSequence(ds3);
763 SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
764 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
768 * Second edit deletes what is now columns 4 and 5.
770 seq1 = new Sequence("", "-ABCDEF");
771 seq1.setDatasetSequence(ds1);
772 seq2 = new Sequence("", "G-HIJ");
773 seq2.setDatasetSequence(ds2);
774 seq3 = new Sequence("", "M-NOPQ");
775 seq3.setDatasetSequence(ds3);
776 sqs = new SequenceI[] { seq1, seq2, seq3 };
777 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
780 Map<SequenceI, SequenceI> unwound = command.priorState(false);
781 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
782 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
783 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
784 assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
785 assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
786 assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
790 * Test a cut action's relocation of sequence features
792 @Test(groups = { "Functional" })
793 public void testCut_withFeatures()
796 * create sequence features before, after and overlapping
797 * a cut of columns/residues 4-7
799 SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
800 seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
802 seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
804 seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
806 seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
808 seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
812 * add some contact features
814 SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
816 seq0.addSequenceFeature(internalContact); // should get deleted
817 SequenceFeature overlapLeftContact = new SequenceFeature(
818 "disulphide bond", "", 2, 6, 0f, null);
819 seq0.addSequenceFeature(overlapLeftContact); // should get deleted
820 SequenceFeature overlapRightContact = new SequenceFeature(
821 "disulphide bond", "", 5, 8, 0f, null);
822 seq0.addSequenceFeature(overlapRightContact); // should get deleted
823 SequenceFeature spanningContact = new SequenceFeature(
824 "disulphide bond", "", 2, 9, 0f, null);
825 seq0.addSequenceFeature(spanningContact); // should get shortened 3'
828 * cut columns 3-6 (base 0), residues d-g 4-7
830 Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
831 EditCommand.cut(ec, new AlignmentI[] { al });
833 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
834 SequenceFeatures.sortFeatures(sfs, true);
836 assertEquals(5, sfs.size()); // features internal to cut were deleted
837 SequenceFeature sf = sfs.get(0);
838 assertEquals("before", sf.getType());
839 assertEquals(1, sf.getBegin());
840 assertEquals(3, sf.getEnd());
842 assertEquals("disulphide bond", sf.getType());
843 assertEquals(2, sf.getBegin());
844 assertEquals(5, sf.getEnd()); // truncated by cut
846 assertEquals("overlap left", sf.getType());
847 assertEquals(2, sf.getBegin());
848 assertEquals(3, sf.getEnd()); // truncated by cut
850 assertEquals("after", sf.getType());
851 assertEquals(4, sf.getBegin()); // shifted left by cut
852 assertEquals(6, sf.getEnd()); // shifted left by cut
854 assertEquals("overlap right", sf.getType());
855 assertEquals(4, sf.getBegin()); // shifted left by cut
856 assertEquals(4, sf.getEnd()); // truncated by cut
860 * Test a cut action's relocation of sequence features, with full coverage of
861 * all possible feature and cut locations for a 5-position ungapped sequence
863 @Test(groups = { "Functional" })
864 public void testCut_withFeatures_exhaustive()
867 * create a sequence features on each subrange of 1-5
869 SequenceI seq0 = new Sequence("seq", "ABCDE");
872 seq0.setStart(start);
874 AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
875 alignment.setDataset(null);
878 * create a new alignment with shared dataset sequence
880 AlignmentI copy = new Alignment(
882 { alignment.getDataset().getSequenceAt(0).deriveSequence() });
883 SequenceI copySeq0 = copy.getSequenceAt(0);
885 for (int from = start; from <= end; from++)
887 for (int to = from; to <= end; to++)
889 String desc = String.format("%d-%d", from, to);
890 SequenceFeature sf = new SequenceFeature("test", desc, from, to,
892 sf.setValue("from", Integer.valueOf(from));
893 sf.setValue("to", Integer.valueOf(to));
894 seq0.addSequenceFeature(sf);
898 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
899 assertEquals(func(5), sfs.size());
900 assertEquals(sfs, copySeq0.getSequenceFeatures());
901 String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
904 * now perform all possible cuts of subranges of columns 1-5
905 * and validate the resulting remaining sequence features!
907 SequenceI[] sqs = new SequenceI[] { seq0 };
909 for (int from = 0; from < seq0.getLength(); from++)
911 for (int to = from; to < seq0.getLength(); to++)
913 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
914 - from + 1), alignment);
915 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
916 boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
917 .getDatasetSequence();
919 verifyCut(seq0, from, to, msg, start);
922 * verify copy alignment dataset sequence unaffected
924 assertEquals("Original dataset sequence was modified",
925 copySequenceFeatures,
926 copySeq0.getSequenceFeatures().toString());
929 * verify any new dataset sequence was added to the
932 assertEquals("Wrong Dataset size after " + msg,
933 newDatasetSequence ? 2 : 1,
934 alignment.getDataset().getHeight());
937 * undo and verify all restored
939 AlignmentI[] views = new AlignmentI[] { alignment };
940 ec.undoCommand(views);
941 sfs = seq0.getSequenceFeatures();
942 assertEquals("After undo of " + msg, func(5), sfs.size());
943 verifyUndo(from, to, sfs);
946 * verify copy alignment dataset sequence still unaffected
947 * and alignment dataset has shrunk (if it was added to)
949 assertEquals("Original dataset sequence was modified",
950 copySequenceFeatures,
951 copySeq0.getSequenceFeatures().toString());
952 assertEquals("Wrong Dataset size after Undo of " + msg, 1,
953 alignment.getDataset().getHeight());
959 verifyCut(seq0, from, to, msg, start);
962 * verify copy alignment dataset sequence unaffected
963 * and any new dataset sequence readded to alignment dataset
965 assertEquals("Original dataset sequence was modified",
966 copySequenceFeatures,
967 copySeq0.getSequenceFeatures().toString());
968 assertEquals("Wrong Dataset size after Redo of " + msg,
969 newDatasetSequence ? 2 : 1,
970 alignment.getDataset().getHeight());
973 * undo ready for next cut
975 ec.undoCommand(views);
978 * final verify that copy alignment dataset sequence is still unaffected
979 * and that alignment dataset has shrunk
981 assertEquals("Original dataset sequence was modified",
982 copySequenceFeatures,
983 copySeq0.getSequenceFeatures().toString());
984 assertEquals("Wrong Dataset size after final Undo of " + msg, 1,
985 alignment.getDataset().getHeight());
991 * Verify by inspection that the sequence features left on the sequence after
992 * a cut match the expected results. The trick to this is that we can parse
993 * each feature's original start-end positions from its description.
1001 protected void verifyCut(SequenceI seq0, int from, int to,
1002 final String msg, int seqStart)
1004 List<SequenceFeature> sfs;
1005 sfs = seq0.getSequenceFeatures();
1007 Collections.sort(sfs, BY_DESCRIPTION);
1010 * confirm the number of features has reduced by the
1011 * number of features within the cut region i.e. by
1012 * func(length of cut); exception is a cut at start or end of sequence,
1013 * which retains the original coordinates, dataset sequence
1014 * and all its features
1016 boolean datasetRetained = from == 0 || to == 4;
1017 if (datasetRetained)
1019 // dataset and all features retained
1020 assertEquals(msg, func(5), sfs.size());
1022 else if (to - from == 4)
1024 // all columns were cut
1025 assertTrue(sfs.isEmpty());
1029 // failure in checkFeatureRelocation is more informative!
1030 assertEquals(msg + "wrong number of features left", func(5)
1031 - func(to - from + 1), sfs.size());
1035 * inspect individual features
1037 for (SequenceFeature sf : sfs)
1039 verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
1045 * Check that after Undo, every feature has start/end that match its original
1046 * "start" and "end" properties
1052 protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
1054 for (SequenceFeature sf : sfs)
1056 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1057 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1058 String msg = String.format(
1059 "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
1061 assertEquals(msg + "start", oldFrom, sf.getBegin());
1062 assertEquals(msg + "end", oldTo, sf.getEnd());
1067 * Helper method to check a feature has been correctly relocated after a cut
1071 * start of cut (first residue cut 1..)
1073 * end of cut (last residue cut 1..)
1077 private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1078 boolean newDataset, int seqStart)
1080 // TODO handle the gapped sequence case as well
1081 int cutSize = to - from + 1;
1082 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1083 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1084 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1085 final int oldToPosition = oldTo - seqStart + 1; // 1..
1087 String msg = String.format(
1088 "Feature %s relocated to %d-%d after cut of %d-%d",
1089 sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1092 // dataset retained with all features unchanged
1093 assertEquals("0: " + msg, oldFrom, sf.getBegin());
1094 assertEquals("0: " + msg, oldTo, sf.getEnd());
1096 else if (oldToPosition < from)
1098 // before cut region so unchanged
1099 assertEquals("1: " + msg, oldFrom, sf.getBegin());
1100 assertEquals("2: " + msg, oldTo, sf.getEnd());
1102 else if (oldFromPosition > to)
1104 // follows cut region - shift by size of cut
1105 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1107 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1110 else if (oldFromPosition < from && oldToPosition > to)
1112 // feature encloses cut region - shrink it right
1113 assertEquals("5: " + msg, oldFrom, sf.getBegin());
1114 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1116 else if (oldFromPosition < from)
1118 // feature overlaps left side of cut region - truncated right
1119 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1121 else if (oldToPosition > to)
1123 // feature overlaps right side of cut region - truncated left
1124 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1126 assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1131 // feature internal to cut - should have been deleted!
1132 Assert.fail(msg + " - should have been deleted");
1137 * Test a cut action's relocation of sequence features
1139 @Test(groups = { "Functional" })
1140 public void testCut_withFeatures5prime()
1142 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1143 seq0.createDatasetSequence();
1144 assertEquals(8, seq0.getStart());
1145 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
1147 SequenceI[] seqsArray = new SequenceI[] { seq0 };
1148 AlignmentI alignment = new Alignment(seqsArray);
1151 * cut columns of A-B; same dataset sequence is retained, aligned sequence
1154 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1155 EditCommand.cut(ec, new AlignmentI[] { alignment });
1158 * feature on CC(10-11) should still be on CC(10-11)
1160 assertSame(seq0, alignment.getSequenceAt(0));
1161 assertEquals(10, seq0.getStart());
1162 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1163 assertEquals(1, sfs.size());
1164 SequenceFeature sf = sfs.get(0);
1165 assertEquals(10, sf.getBegin());
1166 assertEquals(11, sf.getEnd());