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",
317 assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
318 assertEquals(1, seq.getStart());
319 assertEquals(8, seq.getEnd());
320 // Dataset sequence always uppercase
321 assertEquals("ABxyZDEF".toUpperCase(Locale.ROOT),
322 seq.getDatasetSequence().getSequenceAsString());
323 assertEquals(8, seq.getDatasetSequence().getEnd());
328 AlignmentI[] views = new AlignmentI[] {
329 new Alignment(new SequenceI[]
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),
373 dsseq.getSequenceAsString());
374 // and that alignment sequence start has been adjusted
375 assertEquals(5, seqs[1].getStart());
376 assertEquals(11, seqs[1].getEnd());
378 AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
380 edit.undoCommand(views);
382 // dataset sequence unchanged
383 assertTrue(dsseq == seqs[1].getDatasetSequence());
385 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
386 // and start/end numbering also restored
387 assertEquals(1, seqs[1].getStart());
388 assertEquals(11, seqs[1].getEnd());
391 edit.undoCommand(views);
393 // and repeat asserts for the original edit
396 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
397 // and ds is preserved
398 assertTrue(dsseq == seqs[1].getDatasetSequence());
399 // and it is unchanged AND UPPERCASE !
400 assertEquals("fghjklmnopq".toUpperCase(Locale.ROOT),
401 dsseq.getSequenceAsString());
402 // and that alignment sequence start has been adjusted
403 assertEquals(5, seqs[1].getStart());
404 assertEquals(11, seqs[1].getEnd());
409 * Test that the addEdit command correctly merges insert gap commands when
412 @Test(groups = { "Functional" })
413 public void testAddEdit_multipleInsertGap()
416 * 3 insert gap in a row (aka mouse drag right):
418 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
420 { seqs[0] }, 1, 1, al);
422 SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
423 edited.setDatasetSequence(seqs[0].getDatasetSequence());
424 e = new EditCommand().new Edit(Action.INSERT_GAP,
426 { edited }, 2, 1, al);
428 edited = new Sequence("seq0", "a??bcdefghjk");
429 edited.setDatasetSequence(seqs[0].getDatasetSequence());
430 e = new EditCommand().new Edit(Action.INSERT_GAP,
432 { edited }, 3, 1, al);
434 assertEquals(1, testee.getSize());
435 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
436 assertEquals(1, testee.getEdit(0).getPosition());
437 assertEquals(3, testee.getEdit(0).getNumber());
440 * Add a non-contiguous edit - should not be merged.
442 e = new EditCommand().new Edit(Action.INSERT_GAP,
444 { edited }, 5, 2, al);
446 assertEquals(2, testee.getSize());
447 assertEquals(5, testee.getEdit(1).getPosition());
448 assertEquals(2, testee.getEdit(1).getNumber());
451 * Add a Delete after the Insert - should not be merged.
453 e = new EditCommand().new Edit(Action.DELETE_GAP,
455 { edited }, 6, 2, al);
457 assertEquals(3, testee.getSize());
458 assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
459 assertEquals(6, testee.getEdit(2).getPosition());
460 assertEquals(2, testee.getEdit(2).getNumber());
464 * Test that the addEdit command correctly merges delete gap commands when
467 @Test(groups = { "Functional" })
468 public void testAddEdit_multipleDeleteGap()
471 * 3 delete gap in a row (aka mouse drag left):
473 seqs[0].setSequence("a???bcdefghjk");
474 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
476 { seqs[0] }, 4, 1, al);
478 assertEquals(1, testee.getSize());
480 SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
481 edited.setDatasetSequence(seqs[0].getDatasetSequence());
482 e = new EditCommand().new Edit(Action.DELETE_GAP,
484 { edited }, 3, 1, al);
486 assertEquals(1, testee.getSize());
488 edited = new Sequence("seq0", "a?bcdefghjk");
489 edited.setDatasetSequence(seqs[0].getDatasetSequence());
490 e = new EditCommand().new Edit(Action.DELETE_GAP,
492 { edited }, 2, 1, al);
494 assertEquals(1, testee.getSize());
495 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
496 assertEquals(2, testee.getEdit(0).getPosition());
497 assertEquals(3, testee.getEdit(0).getNumber());
500 * Add a non-contiguous edit - should not be merged.
502 e = new EditCommand().new Edit(Action.DELETE_GAP,
504 { edited }, 2, 1, al);
506 assertEquals(2, testee.getSize());
507 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
508 assertEquals(2, testee.getEdit(1).getPosition());
509 assertEquals(1, testee.getEdit(1).getNumber());
512 * Add an Insert after the Delete - should not be merged.
514 e = new EditCommand().new Edit(Action.INSERT_GAP,
516 { edited }, 1, 1, al);
518 assertEquals(3, testee.getSize());
519 assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
520 assertEquals(1, testee.getEdit(2).getPosition());
521 assertEquals(1, testee.getEdit(2).getNumber());
525 * Test that the addEdit command correctly handles 'remove gaps' edits for the
526 * case when they appear contiguous but are acting on different sequences.
527 * They should not be merged.
529 @Test(groups = { "Functional" })
530 public void testAddEdit_removeAllGaps()
532 seqs[0].setSequence("a???bcdefghjk");
533 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
535 { seqs[0] }, 4, 1, al);
538 seqs[1].setSequence("f??ghjklmnopq");
539 Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP,
541 { seqs[1] }, 3, 1, al);
543 assertEquals(2, testee.getSize());
544 assertSame(e, testee.getEdit(0));
545 assertSame(e2, testee.getEdit(1));
549 * Test that the addEdit command correctly merges insert gap commands acting
550 * on a multi-sequence selection.
552 @Test(groups = { "Functional" })
553 public void testAddEdit_groupInsertGaps()
556 * 2 insert gap in a row (aka mouse drag right), on two sequences:
558 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
560 { seqs[0], seqs[1] }, 1, 1, al);
562 SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
563 seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
564 SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
565 seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
566 e = new EditCommand().new Edit(Action.INSERT_GAP,
568 { seq1edited, seq2edited }, 2, 1, al);
571 assertEquals(1, testee.getSize());
572 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
573 assertEquals(1, testee.getEdit(0).getPosition());
574 assertEquals(2, testee.getEdit(0).getNumber());
575 assertEquals(seqs[0].getDatasetSequence(),
576 testee.getEdit(0).getSequences()[0].getDatasetSequence());
577 assertEquals(seqs[1].getDatasetSequence(),
578 testee.getEdit(0).getSequences()[1].getDatasetSequence());
582 * Test for 'undoing' a series of gap insertions.
584 * <li>Start: ABCDEF insert 2 at pos 1</li>
585 * <li>next: A--BCDEF insert 1 at pos 4</li>
586 * <li>next: A--B-CDEF insert 2 at pos 0</li>
587 * <li>last: --A--B-CDEF</li>
590 @Test(groups = { "Functional" })
591 public void testPriorState_multipleInserts()
593 EditCommand command = new EditCommand();
594 SequenceI seq = new Sequence("", "--A--B-CDEF");
595 SequenceI ds = new Sequence("", "ABCDEF");
596 seq.setDatasetSequence(ds);
597 SequenceI[] sqs = new SequenceI[] { seq };
598 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
600 e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
602 e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
605 Map<SequenceI, SequenceI> unwound = command.priorState(false);
606 assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
610 * Test for 'undoing' a series of gap deletions.
612 * <li>Start: A-B-C delete 1 at pos 1</li>
613 * <li>Next: AB-C delete 1 at pos 2</li>
617 @Test(groups = { "Functional" })
618 public void testPriorState_removeAllGaps()
620 EditCommand command = new EditCommand();
621 SequenceI seq = new Sequence("", "ABC");
622 SequenceI ds = new Sequence("", "ABC");
623 seq.setDatasetSequence(ds);
624 SequenceI[] sqs = new SequenceI[] { seq };
625 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
627 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
630 Map<SequenceI, SequenceI> unwound = command.priorState(false);
631 assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
635 * Test for 'undoing' a single delete edit.
637 @Test(groups = { "Functional" })
638 public void testPriorState_singleDelete()
640 EditCommand command = new EditCommand();
641 SequenceI seq = new Sequence("", "ABCDEF");
642 SequenceI ds = new Sequence("", "ABCDEF");
643 seq.setDatasetSequence(ds);
644 SequenceI[] sqs = new SequenceI[] { seq };
645 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
648 Map<SequenceI, SequenceI> unwound = command.priorState(false);
649 assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
653 * Test 'undoing' a single gap insertion edit command.
655 @Test(groups = { "Functional" })
656 public void testPriorState_singleInsert()
658 EditCommand command = new EditCommand();
659 SequenceI seq = new Sequence("", "AB---CDEF");
660 SequenceI ds = new Sequence("", "ABCDEF");
661 seq.setDatasetSequence(ds);
662 SequenceI[] sqs = new SequenceI[] { seq };
663 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
666 Map<SequenceI, SequenceI> unwound = command.priorState(false);
667 SequenceI prior = unwound.get(ds);
668 assertEquals("ABCDEF", prior.getSequenceAsString());
669 assertEquals(1, prior.getStart());
670 assertEquals(6, prior.getEnd());
674 * Test 'undoing' a single gap insertion edit command, on a sequence whose
675 * start residue is other than 1
677 @Test(groups = { "Functional" })
678 public void testPriorState_singleInsertWithOffset()
680 EditCommand command = new EditCommand();
681 SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
682 // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
683 // seq.setDatasetSequence(ds);
684 seq.createDatasetSequence();
685 SequenceI[] sqs = new SequenceI[] { seq };
686 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
689 Map<SequenceI, SequenceI> unwound = command.priorState(false);
690 SequenceI prior = unwound.get(seq.getDatasetSequence());
691 assertEquals("ABCDEF", prior.getSequenceAsString());
692 assertEquals(8, prior.getStart());
693 assertEquals(13, prior.getEnd());
697 * Test that mimics 'remove all gaps' action. This generates delete gap edits
698 * for contiguous gaps in each sequence separately.
700 @Test(groups = { "Functional" })
701 public void testPriorState_removeGapsMultipleSeqs()
703 EditCommand command = new EditCommand();
704 String original1 = "--ABC-DEF";
705 String original2 = "FG-HI--J";
706 String original3 = "M-NOPQ";
709 * Two edits for the first sequence
711 SequenceI seq = new Sequence("", "ABC-DEF");
712 SequenceI ds1 = new Sequence("", "ABCDEF");
713 seq.setDatasetSequence(ds1);
714 SequenceI[] sqs = new SequenceI[] { seq };
715 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
717 seq = new Sequence("", "ABCDEF");
718 seq.setDatasetSequence(ds1);
719 sqs = new SequenceI[] { seq };
720 e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
724 * Two edits for the second sequence
726 seq = new Sequence("", "FGHI--J");
727 SequenceI ds2 = new Sequence("", "FGHIJ");
728 seq.setDatasetSequence(ds2);
729 sqs = new SequenceI[] { seq };
730 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
732 seq = new Sequence("", "FGHIJ");
733 seq.setDatasetSequence(ds2);
734 sqs = new SequenceI[] { seq };
735 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
739 * One edit for the third sequence.
741 seq = new Sequence("", "MNOPQ");
742 SequenceI ds3 = new Sequence("", "MNOPQ");
743 seq.setDatasetSequence(ds3);
744 sqs = new SequenceI[] { seq };
745 e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
748 Map<SequenceI, SequenceI> unwound = command.priorState(false);
749 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
750 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
751 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
755 * Test that mimics 'remove all gapped columns' action. This generates a
756 * series Delete Gap edits that each act on all sequences that share a gapped
759 @Test(groups = { "Functional" })
760 public void testPriorState_removeGappedCols()
762 EditCommand command = new EditCommand();
763 String original1 = "--ABC--DEF";
764 String original2 = "-G-HI--J";
765 String original3 = "-M-NO--PQ";
768 * First edit deletes the first column.
770 SequenceI seq1 = new Sequence("", "-ABC--DEF");
771 SequenceI ds1 = new Sequence("", "ABCDEF");
772 seq1.setDatasetSequence(ds1);
773 SequenceI seq2 = new Sequence("", "G-HI--J");
774 SequenceI ds2 = new Sequence("", "GHIJ");
775 seq2.setDatasetSequence(ds2);
776 SequenceI seq3 = new Sequence("", "M-NO--PQ");
777 SequenceI ds3 = new Sequence("", "MNOPQ");
778 seq3.setDatasetSequence(ds3);
779 SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
780 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
784 * Second edit deletes what is now columns 4 and 5.
786 seq1 = new Sequence("", "-ABCDEF");
787 seq1.setDatasetSequence(ds1);
788 seq2 = new Sequence("", "G-HIJ");
789 seq2.setDatasetSequence(ds2);
790 seq3 = new Sequence("", "M-NOPQ");
791 seq3.setDatasetSequence(ds3);
792 sqs = new SequenceI[] { seq1, seq2, seq3 };
793 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
796 Map<SequenceI, SequenceI> unwound = command.priorState(false);
797 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
798 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
799 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
800 assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
801 assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
802 assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
806 * Test a cut action's relocation of sequence features
808 @Test(groups = { "Functional" })
809 public void testCut_withFeatures()
812 * create sequence features before, after and overlapping
813 * a cut of columns/residues 4-7
815 SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
816 seq0.addSequenceFeature(
817 new SequenceFeature("before", "", 1, 3, 0f, null));
818 seq0.addSequenceFeature(
819 new SequenceFeature("overlap left", "", 2, 6, 0f, null));
820 seq0.addSequenceFeature(
821 new SequenceFeature("internal", "", 5, 6, 0f, null));
822 seq0.addSequenceFeature(
823 new SequenceFeature("overlap right", "", 7, 8, 0f, null));
824 seq0.addSequenceFeature(
825 new SequenceFeature("after", "", 8, 10, 0f, null));
828 * add some contact features
830 SequenceFeature internalContact = new SequenceFeature("disulphide bond",
832 seq0.addSequenceFeature(internalContact); // should get deleted
833 SequenceFeature overlapLeftContact = new SequenceFeature(
834 "disulphide bond", "", 2, 6, 0f, null);
835 seq0.addSequenceFeature(overlapLeftContact); // should get deleted
836 SequenceFeature overlapRightContact = new SequenceFeature(
837 "disulphide bond", "", 5, 8, 0f, null);
838 seq0.addSequenceFeature(overlapRightContact); // should get deleted
839 SequenceFeature spanningContact = new SequenceFeature("disulphide bond",
841 seq0.addSequenceFeature(spanningContact); // should get shortened 3'
844 * cut columns 3-6 (base 0), residues d-g 4-7
846 Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
847 EditCommand.cut(ec, new AlignmentI[] { al });
849 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
850 SequenceFeatures.sortFeatures(sfs, true);
852 assertEquals(5, sfs.size()); // features internal to cut were deleted
853 SequenceFeature sf = sfs.get(0);
854 assertEquals("before", sf.getType());
855 assertEquals(1, sf.getBegin());
856 assertEquals(3, sf.getEnd());
858 assertEquals("disulphide bond", sf.getType());
859 assertEquals(2, sf.getBegin());
860 assertEquals(5, sf.getEnd()); // truncated by cut
862 assertEquals("overlap left", sf.getType());
863 assertEquals(2, sf.getBegin());
864 assertEquals(3, sf.getEnd()); // truncated by cut
866 assertEquals("after", sf.getType());
867 assertEquals(4, sf.getBegin()); // shifted left by cut
868 assertEquals(6, sf.getEnd()); // shifted left by cut
870 assertEquals("overlap right", sf.getType());
871 assertEquals(4, sf.getBegin()); // shifted left by cut
872 assertEquals(4, sf.getEnd()); // truncated by cut
876 * Test a cut action's relocation of sequence features, with full coverage of
877 * all possible feature and cut locations for a 5-position ungapped sequence
879 @Test(groups = { "Functional" })
880 public void testCut_withFeatures_exhaustive()
883 * create a sequence features on each subrange of 1-5
885 SequenceI seq0 = new Sequence("seq", "ABCDE");
888 seq0.setStart(start);
890 AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
891 alignment.setDataset(null);
894 * create a new alignment with shared dataset sequence
896 AlignmentI copy = new Alignment(
898 { alignment.getDataset().getSequenceAt(0).deriveSequence() });
899 SequenceI copySeq0 = copy.getSequenceAt(0);
901 for (int from = start; from <= end; from++)
903 for (int to = from; to <= end; to++)
905 String desc = String.format("%d-%d", from, to);
906 SequenceFeature sf = new SequenceFeature("test", desc, from, to, 0f,
908 sf.setValue("from", Integer.valueOf(from));
909 sf.setValue("to", Integer.valueOf(to));
910 seq0.addSequenceFeature(sf);
914 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
915 assertEquals(func(5), sfs.size());
916 assertEquals(sfs, copySeq0.getSequenceFeatures());
917 String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
920 * now perform all possible cuts of subranges of columns 1-5
921 * and validate the resulting remaining sequence features!
923 SequenceI[] sqs = new SequenceI[] { seq0 };
925 for (int from = 0; from < seq0.getLength(); from++)
927 for (int to = from; to < seq0.getLength(); to++)
929 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from,
930 (to - from + 1), alignment);
931 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
932 boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
933 .getDatasetSequence();
935 verifyCut(seq0, from, to, msg, start);
938 * verify copy alignment dataset sequence unaffected
940 assertEquals("Original dataset sequence was modified",
941 copySequenceFeatures,
942 copySeq0.getSequenceFeatures().toString());
945 * verify any new dataset sequence was added to the
948 assertEquals("Wrong Dataset size after " + msg,
949 newDatasetSequence ? 2 : 1,
950 alignment.getDataset().getHeight());
953 * undo and verify all restored
955 AlignmentI[] views = new AlignmentI[] { alignment };
956 ec.undoCommand(views);
957 sfs = seq0.getSequenceFeatures();
958 assertEquals("After undo of " + msg, func(5), sfs.size());
959 verifyUndo(from, to, sfs);
962 * verify copy alignment dataset sequence still unaffected
963 * and alignment dataset has shrunk (if it was added to)
965 assertEquals("Original dataset sequence was modified",
966 copySequenceFeatures,
967 copySeq0.getSequenceFeatures().toString());
968 assertEquals("Wrong Dataset size after Undo of " + msg, 1,
969 alignment.getDataset().getHeight());
975 verifyCut(seq0, from, to, msg, start);
978 * verify copy alignment dataset sequence unaffected
979 * and any new dataset sequence readded to alignment dataset
981 assertEquals("Original dataset sequence was modified",
982 copySequenceFeatures,
983 copySeq0.getSequenceFeatures().toString());
984 assertEquals("Wrong Dataset size after Redo of " + msg,
985 newDatasetSequence ? 2 : 1,
986 alignment.getDataset().getHeight());
989 * undo ready for next cut
991 ec.undoCommand(views);
994 * final verify that copy alignment dataset sequence is still unaffected
995 * and that alignment dataset has shrunk
997 assertEquals("Original dataset sequence was modified",
998 copySequenceFeatures,
999 copySeq0.getSequenceFeatures().toString());
1000 assertEquals("Wrong Dataset size after final Undo of " + msg, 1,
1001 alignment.getDataset().getHeight());
1007 * Verify by inspection that the sequence features left on the sequence after
1008 * a cut match the expected results. The trick to this is that we can parse
1009 * each feature's original start-end positions from its description.
1017 protected void verifyCut(SequenceI seq0, int from, int to,
1018 final String msg, int seqStart)
1020 List<SequenceFeature> sfs;
1021 sfs = seq0.getSequenceFeatures();
1023 Collections.sort(sfs, BY_DESCRIPTION);
1026 * confirm the number of features has reduced by the
1027 * number of features within the cut region i.e. by
1028 * func(length of cut); exception is a cut at start or end of sequence,
1029 * which retains the original coordinates, dataset sequence
1030 * and all its features
1032 boolean datasetRetained = from == 0 || to == 4;
1033 if (datasetRetained)
1035 // dataset and all features retained
1036 assertEquals(msg, func(5), sfs.size());
1038 else if (to - from == 4)
1040 // all columns were cut
1041 assertTrue(sfs.isEmpty());
1045 // failure in checkFeatureRelocation is more informative!
1046 assertEquals(msg + "wrong number of features left",
1047 func(5) - func(to - from + 1), sfs.size());
1051 * inspect individual features
1053 for (SequenceFeature sf : sfs)
1055 verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
1061 * Check that after Undo, every feature has start/end that match its original
1062 * "start" and "end" properties
1068 protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
1070 for (SequenceFeature sf : sfs)
1072 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1073 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1074 String msg = String.format("Undo cut of [%d-%d], feature at [%d-%d] ",
1075 from + 1, to + 1, oldFrom, oldTo);
1076 assertEquals(msg + "start", oldFrom, sf.getBegin());
1077 assertEquals(msg + "end", oldTo, sf.getEnd());
1082 * Helper method to check a feature has been correctly relocated after a cut
1086 * start of cut (first residue cut 1..)
1088 * end of cut (last residue cut 1..)
1092 private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1093 boolean newDataset, int seqStart)
1095 // TODO handle the gapped sequence case as well
1096 int cutSize = to - from + 1;
1097 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1098 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1099 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1100 final int oldToPosition = oldTo - seqStart + 1; // 1..
1102 String msg = String.format(
1103 "Feature %s relocated to %d-%d after cut of %d-%d",
1104 sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1107 // dataset retained with all features unchanged
1108 assertEquals("0: " + msg, oldFrom, sf.getBegin());
1109 assertEquals("0: " + msg, oldTo, sf.getEnd());
1111 else if (oldToPosition < from)
1113 // before cut region so unchanged
1114 assertEquals("1: " + msg, oldFrom, sf.getBegin());
1115 assertEquals("2: " + msg, oldTo, sf.getEnd());
1117 else if (oldFromPosition > to)
1119 // follows cut region - shift by size of cut
1120 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1122 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1125 else if (oldFromPosition < from && oldToPosition > to)
1127 // feature encloses cut region - shrink it right
1128 assertEquals("5: " + msg, oldFrom, sf.getBegin());
1129 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1131 else if (oldFromPosition < from)
1133 // feature overlaps left side of cut region - truncated right
1134 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1136 else if (oldToPosition > to)
1138 // feature overlaps right side of cut region - truncated left
1139 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1141 assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1146 // feature internal to cut - should have been deleted!
1147 Assert.fail(msg + " - should have been deleted");
1152 * Test a cut action's relocation of sequence features
1154 @Test(groups = { "Functional" })
1155 public void testCut_withFeatures5prime()
1157 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1158 seq0.createDatasetSequence();
1159 assertEquals(8, seq0.getStart());
1160 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f, null));
1161 SequenceI[] seqsArray = new SequenceI[] { seq0 };
1162 AlignmentI alignment = new Alignment(seqsArray);
1165 * cut columns of A-B; same dataset sequence is retained, aligned sequence
1168 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1169 EditCommand.cut(ec, new AlignmentI[] { alignment });
1172 * feature on CC(10-11) should still be on CC(10-11)
1174 assertSame(seq0, alignment.getSequenceAt(0));
1175 assertEquals(10, seq0.getStart());
1176 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1177 assertEquals(1, sfs.size());
1178 SequenceFeature sf = sfs.get(0);
1179 assertEquals(10, sf.getBegin());
1180 assertEquals(11, sf.getEnd());