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, where this adds sequences to an alignment.
158 @Test(groups = { "Functional" }, enabled = true)
159 // TODO fix so it works
160 public void testPaste_addToAlignment()
162 SequenceI[] newSeqs = new SequenceI[2];
163 newSeqs[0] = new Sequence("newseq0", "ACEFKL");
164 newSeqs[1] = new Sequence("newseq1", "JWMPDH");
166 Edit ec = testee.new Edit(Action.PASTE, newSeqs, 0, al.getWidth(), al);
167 EditCommand.paste(ec, new AlignmentI[] { al });
168 assertEquals(6, al.getSequences().size());
169 assertEquals("1234567890", seqs[3].getSequenceAsString());
170 assertEquals("ACEFKL", seqs[4].getSequenceAsString());
171 assertEquals("JWMPDH", seqs[5].getSequenceAsString());
175 * Test insertGap followed by undo command
177 @Test(groups = { "Functional" })
178 public void testUndo_insertGap()
180 // Edit ec = testee.new Edit(Action.INSERT_GAP, seqs, 4, 3, '?');
181 testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true);
182 // check something changed
183 assertEquals("abcd???efghjk", seqs[0].getSequenceAsString());
184 testee.undoCommand(new AlignmentI[] { al });
185 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
186 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
187 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
188 assertEquals("1234567890", seqs[3].getSequenceAsString());
192 * Test deleteGap followed by undo command
194 @Test(groups = { "Functional" })
195 public void testUndo_deleteGap()
197 testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true);
198 // check something changed
199 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
200 testee.undoCommand(new AlignmentI[] { al });
201 // deleteGap doesn't 'remember' deleted characters, only gaps get put back
202 assertEquals("abcd???hjk", seqs[0].getSequenceAsString());
203 assertEquals("fghj???nopq", seqs[1].getSequenceAsString());
204 assertEquals("qrst???xyz", seqs[2].getSequenceAsString());
205 assertEquals("1234???890", seqs[3].getSequenceAsString());
209 * Test several commands followed by an undo command
211 @Test(groups = { "Functional" })
212 public void testUndo_multipleCommands()
214 // delete positions 3/4/5 (counting from 1)
215 testee.appendEdit(Action.DELETE_GAP, seqs, 2, 3, al, true);
216 assertEquals("abfghjk", seqs[0].getSequenceAsString());
217 assertEquals("1267890", seqs[3].getSequenceAsString());
219 // insert 2 gaps after the second residue
220 testee.appendEdit(Action.INSERT_GAP, seqs, 2, 2, al, true);
221 assertEquals("ab??fghjk", seqs[0].getSequenceAsString());
222 assertEquals("12??67890", seqs[3].getSequenceAsString());
224 // delete positions 4/5/6
225 testee.appendEdit(Action.DELETE_GAP, seqs, 3, 3, al, true);
226 assertEquals("ab?hjk", seqs[0].getSequenceAsString());
227 assertEquals("12?890", seqs[3].getSequenceAsString());
229 // undo edit commands
230 testee.undoCommand(new AlignmentI[] { al });
231 assertEquals("ab?????hjk", seqs[0].getSequenceAsString());
232 assertEquals("12?????890", seqs[3].getSequenceAsString());
236 * Unit test for JAL-1594 bug: click and drag sequence right to insert gaps -
237 * undo did not remove them all.
239 @Test(groups = { "Functional" })
240 public void testUndo_multipleInsertGaps()
242 testee.appendEdit(Action.INSERT_GAP, seqs, 4, 1, al, true);
243 testee.appendEdit(Action.INSERT_GAP, seqs, 5, 1, al, true);
244 testee.appendEdit(Action.INSERT_GAP, seqs, 6, 1, al, true);
246 // undo edit commands
247 testee.undoCommand(new AlignmentI[] { al });
248 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
249 assertEquals("1234567890", seqs[3].getSequenceAsString());
254 * Test cut followed by undo command
256 @Test(groups = { "Functional" })
257 public void testUndo_cut()
259 testee.appendEdit(Action.CUT, seqs, 4, 3, al, true);
260 // check something changed
261 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
262 testee.undoCommand(new AlignmentI[] { al });
263 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
264 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
265 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
266 assertEquals("1234567890", seqs[3].getSequenceAsString());
270 * Test the replace command (used to manually edit a sequence)
272 @Test(groups = { "Functional" })
273 public void testReplace()
275 // seem to need a dataset sequence on the edited sequence here
276 seqs[1].createDatasetSequence();
277 new EditCommand("", Action.REPLACE, "ZXY", new SequenceI[] { seqs[1] },
279 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
280 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
281 assertEquals("1234567890", seqs[3].getSequenceAsString());
285 * Test the replace command (used to manually edit a sequence)
287 @Test(groups = { "Functional" })
288 public void testReplace_withGaps()
290 SequenceI seq = new Sequence("seq", "ABC--DEF");
291 seq.createDatasetSequence();
292 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
293 assertEquals(1, seq.getStart());
294 assertEquals(6, seq.getEnd());
297 * replace C- with XYZ
298 * NB arg4 = start column of selection for edit (base 0)
299 * arg5 = column after end of selection for edit
301 EditCommand edit = new EditCommand("", Action.REPLACE, "XYZ",
305 assertEquals("ABXYZ-DEF", seq.getSequenceAsString());
306 assertEquals(1, seq.getStart());
307 assertEquals(7, seq.getEnd());
308 assertEquals("ABXYZDEF", seq.getDatasetSequence().getSequenceAsString());
309 assertEquals(7, seq.getDatasetSequence().getEnd());
313 { new Alignment(new SequenceI[] { seq }) });
315 assertEquals("ABC--DEF", seq.getSequenceAsString());
316 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
317 assertEquals(1, seq.getStart());
318 assertEquals(6, seq.getEnd());
319 assertEquals(6, seq.getDatasetSequence().getEnd());
323 { new Alignment(new SequenceI[] { seq }) });
325 assertEquals("ABXYZ-DEF", seq.getSequenceAsString());
326 assertEquals(1, seq.getStart());
327 assertEquals(7, seq.getEnd());
328 assertEquals("ABXYZDEF",
329 seq.getDatasetSequence().getSequenceAsString());
330 assertEquals(7, seq.getDatasetSequence().getEnd());
335 * Test replace command when it doesn't cause a sequence edit (see comment in
337 @Test(groups = { "Functional" })
338 public void testReplaceFirstResiduesWithGaps()
340 // test replace when gaps are inserted at start. Start/end should change
341 // w.r.t. original edited sequence.
342 SequenceI dsseq = seqs[1].getDatasetSequence();
343 EditCommand edit = new EditCommand("", Action.REPLACE, "----",
345 { seqs[1] }, 0, 4, al);
348 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
349 // and ds is preserved
350 assertTrue(dsseq == seqs[1].getDatasetSequence());
351 // and it is unchanged
352 assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
353 // and that alignment sequence start has been adjusted
354 assertEquals(5, seqs[1].getStart());
355 assertEquals(11, seqs[1].getEnd());
357 AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
359 edit.undoCommand(views);
361 // dataset sequence unchanged
362 assertTrue(dsseq == seqs[1].getDatasetSequence());
364 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
365 // and start/end numbering also restored
366 assertEquals(1, seqs[1].getStart());
367 assertEquals(11, seqs[1].getEnd());
370 edit.undoCommand(views);
372 // and repeat asserts for the original edit
375 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
376 // and ds is preserved
377 assertTrue(dsseq == seqs[1].getDatasetSequence());
378 // and it is unchanged
379 assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
380 // and that alignment sequence start has been adjusted
381 assertEquals(5, seqs[1].getStart());
382 assertEquals(11, seqs[1].getEnd());
387 * Test that the addEdit command correctly merges insert gap commands when
390 @Test(groups = { "Functional" })
391 public void testAddEdit_multipleInsertGap()
394 * 3 insert gap in a row (aka mouse drag right):
396 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
397 new SequenceI[] { seqs[0] }, 1, 1, al);
399 SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
400 edited.setDatasetSequence(seqs[0].getDatasetSequence());
401 e = new EditCommand().new Edit(Action.INSERT_GAP,
402 new SequenceI[] { edited }, 2, 1, al);
404 edited = new Sequence("seq0", "a??bcdefghjk");
405 edited.setDatasetSequence(seqs[0].getDatasetSequence());
406 e = new EditCommand().new Edit(Action.INSERT_GAP,
407 new SequenceI[] { edited }, 3, 1, al);
409 assertEquals(1, testee.getSize());
410 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
411 assertEquals(1, testee.getEdit(0).getPosition());
412 assertEquals(3, testee.getEdit(0).getNumber());
415 * Add a non-contiguous edit - should not be merged.
417 e = new EditCommand().new Edit(Action.INSERT_GAP,
418 new SequenceI[] { edited }, 5, 2, al);
420 assertEquals(2, testee.getSize());
421 assertEquals(5, testee.getEdit(1).getPosition());
422 assertEquals(2, testee.getEdit(1).getNumber());
425 * Add a Delete after the Insert - should not be merged.
427 e = new EditCommand().new Edit(Action.DELETE_GAP,
428 new SequenceI[] { edited }, 6, 2, al);
430 assertEquals(3, testee.getSize());
431 assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
432 assertEquals(6, testee.getEdit(2).getPosition());
433 assertEquals(2, testee.getEdit(2).getNumber());
437 * Test that the addEdit command correctly merges delete gap commands when
440 @Test(groups = { "Functional" })
441 public void testAddEdit_multipleDeleteGap()
444 * 3 delete gap in a row (aka mouse drag left):
446 seqs[0].setSequence("a???bcdefghjk");
447 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
448 new SequenceI[] { seqs[0] }, 4, 1, al);
450 assertEquals(1, testee.getSize());
452 SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
453 edited.setDatasetSequence(seqs[0].getDatasetSequence());
454 e = new EditCommand().new Edit(Action.DELETE_GAP,
455 new SequenceI[] { edited }, 3, 1, al);
457 assertEquals(1, testee.getSize());
459 edited = new Sequence("seq0", "a?bcdefghjk");
460 edited.setDatasetSequence(seqs[0].getDatasetSequence());
461 e = new EditCommand().new Edit(Action.DELETE_GAP,
462 new SequenceI[] { edited }, 2, 1, al);
464 assertEquals(1, testee.getSize());
465 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
466 assertEquals(2, testee.getEdit(0).getPosition());
467 assertEquals(3, testee.getEdit(0).getNumber());
470 * Add a non-contiguous edit - should not be merged.
472 e = new EditCommand().new Edit(Action.DELETE_GAP,
473 new SequenceI[] { edited }, 2, 1, al);
475 assertEquals(2, testee.getSize());
476 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
477 assertEquals(2, testee.getEdit(1).getPosition());
478 assertEquals(1, testee.getEdit(1).getNumber());
481 * Add an Insert after the Delete - should not be merged.
483 e = new EditCommand().new Edit(Action.INSERT_GAP,
484 new SequenceI[] { edited }, 1, 1, al);
486 assertEquals(3, testee.getSize());
487 assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
488 assertEquals(1, testee.getEdit(2).getPosition());
489 assertEquals(1, testee.getEdit(2).getNumber());
493 * Test that the addEdit command correctly handles 'remove gaps' edits for the
494 * case when they appear contiguous but are acting on different sequences.
495 * They should not be merged.
497 @Test(groups = { "Functional" })
498 public void testAddEdit_removeAllGaps()
500 seqs[0].setSequence("a???bcdefghjk");
501 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
502 new SequenceI[] { seqs[0] }, 4, 1, al);
505 seqs[1].setSequence("f??ghjklmnopq");
506 Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[]
507 { seqs[1] }, 3, 1, al);
509 assertEquals(2, testee.getSize());
510 assertSame(e, testee.getEdit(0));
511 assertSame(e2, testee.getEdit(1));
515 * Test that the addEdit command correctly merges insert gap commands acting
516 * on a multi-sequence selection.
518 @Test(groups = { "Functional" })
519 public void testAddEdit_groupInsertGaps()
522 * 2 insert gap in a row (aka mouse drag right), on two sequences:
524 Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
525 seqs[0], seqs[1] }, 1, 1, al);
527 SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
528 seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
529 SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
530 seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
531 e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
532 seq1edited, seq2edited }, 2, 1, al);
535 assertEquals(1, testee.getSize());
536 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
537 assertEquals(1, testee.getEdit(0).getPosition());
538 assertEquals(2, testee.getEdit(0).getNumber());
539 assertEquals(seqs[0].getDatasetSequence(), testee.getEdit(0)
540 .getSequences()[0].getDatasetSequence());
541 assertEquals(seqs[1].getDatasetSequence(), testee.getEdit(0)
542 .getSequences()[1].getDatasetSequence());
546 * Test for 'undoing' a series of gap insertions.
548 * <li>Start: ABCDEF insert 2 at pos 1</li>
549 * <li>next: A--BCDEF insert 1 at pos 4</li>
550 * <li>next: A--B-CDEF insert 2 at pos 0</li>
551 * <li>last: --A--B-CDEF</li>
554 @Test(groups = { "Functional" })
555 public void testPriorState_multipleInserts()
557 EditCommand command = new EditCommand();
558 SequenceI seq = new Sequence("", "--A--B-CDEF");
559 SequenceI ds = new Sequence("", "ABCDEF");
560 seq.setDatasetSequence(ds);
561 SequenceI[] sqs = new SequenceI[] { seq };
562 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
564 e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
566 e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
569 Map<SequenceI, SequenceI> unwound = command.priorState(false);
570 assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
574 * Test for 'undoing' a series of gap deletions.
576 * <li>Start: A-B-C delete 1 at pos 1</li>
577 * <li>Next: AB-C delete 1 at pos 2</li>
581 @Test(groups = { "Functional" })
582 public void testPriorState_removeAllGaps()
584 EditCommand command = new EditCommand();
585 SequenceI seq = new Sequence("", "ABC");
586 SequenceI ds = new Sequence("", "ABC");
587 seq.setDatasetSequence(ds);
588 SequenceI[] sqs = new SequenceI[] { seq };
589 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
591 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
594 Map<SequenceI, SequenceI> unwound = command.priorState(false);
595 assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
599 * Test for 'undoing' a single delete edit.
601 @Test(groups = { "Functional" })
602 public void testPriorState_singleDelete()
604 EditCommand command = new EditCommand();
605 SequenceI seq = new Sequence("", "ABCDEF");
606 SequenceI ds = new Sequence("", "ABCDEF");
607 seq.setDatasetSequence(ds);
608 SequenceI[] sqs = new SequenceI[] { seq };
609 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
612 Map<SequenceI, SequenceI> unwound = command.priorState(false);
613 assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
617 * Test 'undoing' a single gap insertion edit command.
619 @Test(groups = { "Functional" })
620 public void testPriorState_singleInsert()
622 EditCommand command = new EditCommand();
623 SequenceI seq = new Sequence("", "AB---CDEF");
624 SequenceI ds = new Sequence("", "ABCDEF");
625 seq.setDatasetSequence(ds);
626 SequenceI[] sqs = new SequenceI[] { seq };
627 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
630 Map<SequenceI, SequenceI> unwound = command.priorState(false);
631 SequenceI prior = unwound.get(ds);
632 assertEquals("ABCDEF", prior.getSequenceAsString());
633 assertEquals(1, prior.getStart());
634 assertEquals(6, prior.getEnd());
638 * Test 'undoing' a single gap insertion edit command, on a sequence whose
639 * start residue is other than 1
641 @Test(groups = { "Functional" })
642 public void testPriorState_singleInsertWithOffset()
644 EditCommand command = new EditCommand();
645 SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
646 // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
647 // seq.setDatasetSequence(ds);
648 seq.createDatasetSequence();
649 SequenceI[] sqs = new SequenceI[] { seq };
650 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
653 Map<SequenceI, SequenceI> unwound = command.priorState(false);
654 SequenceI prior = unwound.get(seq.getDatasetSequence());
655 assertEquals("ABCDEF", prior.getSequenceAsString());
656 assertEquals(8, prior.getStart());
657 assertEquals(13, prior.getEnd());
661 * Test that mimics 'remove all gaps' action. This generates delete gap edits
662 * for contiguous gaps in each sequence separately.
664 @Test(groups = { "Functional" })
665 public void testPriorState_removeGapsMultipleSeqs()
667 EditCommand command = new EditCommand();
668 String original1 = "--ABC-DEF";
669 String original2 = "FG-HI--J";
670 String original3 = "M-NOPQ";
673 * Two edits for the first sequence
675 SequenceI seq = new Sequence("", "ABC-DEF");
676 SequenceI ds1 = new Sequence("", "ABCDEF");
677 seq.setDatasetSequence(ds1);
678 SequenceI[] sqs = new SequenceI[] { seq };
679 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
681 seq = new Sequence("", "ABCDEF");
682 seq.setDatasetSequence(ds1);
683 sqs = new SequenceI[] { seq };
684 e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
688 * Two edits for the second sequence
690 seq = new Sequence("", "FGHI--J");
691 SequenceI ds2 = new Sequence("", "FGHIJ");
692 seq.setDatasetSequence(ds2);
693 sqs = new SequenceI[] { seq };
694 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
696 seq = new Sequence("", "FGHIJ");
697 seq.setDatasetSequence(ds2);
698 sqs = new SequenceI[] { seq };
699 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
703 * One edit for the third sequence.
705 seq = new Sequence("", "MNOPQ");
706 SequenceI ds3 = new Sequence("", "MNOPQ");
707 seq.setDatasetSequence(ds3);
708 sqs = new SequenceI[] { seq };
709 e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
712 Map<SequenceI, SequenceI> unwound = command.priorState(false);
713 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
714 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
715 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
719 * Test that mimics 'remove all gapped columns' action. This generates a
720 * series Delete Gap edits that each act on all sequences that share a gapped
723 @Test(groups = { "Functional" })
724 public void testPriorState_removeGappedCols()
726 EditCommand command = new EditCommand();
727 String original1 = "--ABC--DEF";
728 String original2 = "-G-HI--J";
729 String original3 = "-M-NO--PQ";
732 * First edit deletes the first column.
734 SequenceI seq1 = new Sequence("", "-ABC--DEF");
735 SequenceI ds1 = new Sequence("", "ABCDEF");
736 seq1.setDatasetSequence(ds1);
737 SequenceI seq2 = new Sequence("", "G-HI--J");
738 SequenceI ds2 = new Sequence("", "GHIJ");
739 seq2.setDatasetSequence(ds2);
740 SequenceI seq3 = new Sequence("", "M-NO--PQ");
741 SequenceI ds3 = new Sequence("", "MNOPQ");
742 seq3.setDatasetSequence(ds3);
743 SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
744 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
748 * Second edit deletes what is now columns 4 and 5.
750 seq1 = new Sequence("", "-ABCDEF");
751 seq1.setDatasetSequence(ds1);
752 seq2 = new Sequence("", "G-HIJ");
753 seq2.setDatasetSequence(ds2);
754 seq3 = new Sequence("", "M-NOPQ");
755 seq3.setDatasetSequence(ds3);
756 sqs = new SequenceI[] { seq1, seq2, seq3 };
757 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
760 Map<SequenceI, SequenceI> unwound = command.priorState(false);
761 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
762 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
763 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
764 assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
765 assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
766 assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
770 * Test a cut action's relocation of sequence features
772 @Test(groups = { "Functional" })
773 public void testCut_withFeatures()
776 * create sequence features before, after and overlapping
777 * a cut of columns/residues 4-7
779 SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
780 seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
782 seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
784 seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
786 seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
788 seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
792 * add some contact features
794 SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
796 seq0.addSequenceFeature(internalContact); // should get deleted
797 SequenceFeature overlapLeftContact = new SequenceFeature(
798 "disulphide bond", "", 2, 6, 0f, null);
799 seq0.addSequenceFeature(overlapLeftContact); // should get deleted
800 SequenceFeature overlapRightContact = new SequenceFeature(
801 "disulphide bond", "", 5, 8, 0f, null);
802 seq0.addSequenceFeature(overlapRightContact); // should get deleted
803 SequenceFeature spanningContact = new SequenceFeature(
804 "disulphide bond", "", 2, 9, 0f, null);
805 seq0.addSequenceFeature(spanningContact); // should get shortened 3'
808 * cut columns 3-6 (base 0), residues d-g 4-7
810 Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
811 EditCommand.cut(ec, new AlignmentI[] { al });
813 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
814 SequenceFeatures.sortFeatures(sfs, true);
816 assertEquals(5, sfs.size()); // features internal to cut were deleted
817 SequenceFeature sf = sfs.get(0);
818 assertEquals("before", sf.getType());
819 assertEquals(1, sf.getBegin());
820 assertEquals(3, sf.getEnd());
822 assertEquals("disulphide bond", sf.getType());
823 assertEquals(2, sf.getBegin());
824 assertEquals(5, sf.getEnd()); // truncated by cut
826 assertEquals("overlap left", sf.getType());
827 assertEquals(2, sf.getBegin());
828 assertEquals(3, sf.getEnd()); // truncated by cut
830 assertEquals("after", sf.getType());
831 assertEquals(4, sf.getBegin()); // shifted left by cut
832 assertEquals(6, sf.getEnd()); // shifted left by cut
834 assertEquals("overlap right", sf.getType());
835 assertEquals(4, sf.getBegin()); // shifted left by cut
836 assertEquals(4, sf.getEnd()); // truncated by cut
840 * Test a cut action's relocation of sequence features, with full coverage of
841 * all possible feature and cut locations for a 5-position ungapped sequence
843 @Test(groups = { "Functional" })
844 public void testCut_withFeatures_exhaustive()
847 * create a sequence features on each subrange of 1-5
849 SequenceI seq0 = new Sequence("seq", "ABCDE");
852 seq0.setStart(start);
854 AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
855 alignment.setDataset(null);
858 * create a new alignment with shared dataset sequence
860 AlignmentI copy = new Alignment(
862 { alignment.getDataset().getSequenceAt(0).deriveSequence() });
863 SequenceI copySeq0 = copy.getSequenceAt(0);
865 for (int from = start; from <= end; from++)
867 for (int to = from; to <= end; to++)
869 String desc = String.format("%d-%d", from, to);
870 SequenceFeature sf = new SequenceFeature("test", desc, from, to,
872 sf.setValue("from", Integer.valueOf(from));
873 sf.setValue("to", Integer.valueOf(to));
874 seq0.addSequenceFeature(sf);
878 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
879 assertEquals(func(5), sfs.size());
880 assertEquals(sfs, copySeq0.getSequenceFeatures());
881 String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
884 * now perform all possible cuts of subranges of columns 1-5
885 * and validate the resulting remaining sequence features!
887 SequenceI[] sqs = new SequenceI[] { seq0 };
888 boolean checkDsSize = true;
890 for (int from = 0; from < seq0.getLength(); from++)
892 for (int to = from; to < seq0.getLength(); to++)
894 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
895 - from + 1), alignment);
896 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
897 boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
898 .getDatasetSequence();
900 verifyCut(seq0, from, to, msg, start);
903 * verify copy alignment dataset sequence unaffected
905 assertEquals("Original dataset sequence was modified",
906 copySequenceFeatures,
907 copySeq0.getSequenceFeatures().toString());
912 * verify a new dataset sequence has appeared
914 assertEquals("Wrong Dataset size after cut",
915 newDatasetSequence ? 2 : 1, alignment.getDataset()
919 * undo and verify all restored
921 AlignmentI[] views = new AlignmentI[] { alignment };
922 ec.undoCommand(views);
923 sfs = seq0.getSequenceFeatures();
924 assertEquals("After undo of " + msg, func(5), sfs.size());
925 verifyUndo(from, to, sfs);
928 * verify copy alignment dataset sequence still unaffected
930 assertEquals("Original dataset sequence was modified",
931 copySequenceFeatures,
932 copySeq0.getSequenceFeatures().toString());
937 * verify dataset sequence has shrunk
939 assertEquals("Wrong Dataset size after cut", 1,
940 alignment.getDataset().getHeight());
947 verifyCut(seq0, from, to, msg, start);
950 * verify copy alignment dataset sequence unaffected
952 assertEquals("Original dataset sequence was modified",
953 copySequenceFeatures,
954 copySeq0.getSequenceFeatures().toString());
959 * verify a new dataset sequence has appeared again
961 assertEquals("Wrong Dataset size after cut",
962 newDatasetSequence ? 2 : 1, alignment.getDataset()
967 * undo ready for next cut
969 ec.undoCommand(views);
972 * final verify that copy alignment dataset sequence is still unaffected
974 assertEquals("Original dataset sequence was modified",
975 copySequenceFeatures,
976 copySeq0.getSequenceFeatures().toString());
980 * and that dataset sequence has shrunk
982 assertEquals("Wrong Dataset size after cut", 1,
983 alignment.getDataset().getHeight());
990 * Verify by inspection that the sequence features left on the sequence after
991 * a cut match the expected results. The trick to this is that we can parse
992 * each feature's original start-end positions from its description.
1000 protected void verifyCut(SequenceI seq0, int from, int to,
1001 final String msg, int seqStart)
1003 List<SequenceFeature> sfs;
1004 sfs = seq0.getSequenceFeatures();
1006 Collections.sort(sfs, BY_DESCRIPTION);
1009 * confirm the number of features has reduced by the
1010 * number of features within the cut region i.e. by
1011 * func(length of cut); exception is a cut at start or end of sequence,
1012 * which retains the original coordinates, dataset sequence
1013 * and all its features
1015 boolean datasetRetained = from == 0 || to == 4;
1016 if (datasetRetained)
1018 // dataset and all features retained
1019 assertEquals(msg, func(5), sfs.size());
1021 else if (to - from == 4)
1023 // all columns were cut
1024 assertTrue(sfs.isEmpty());
1028 // failure in checkFeatureRelocation is more informative!
1029 assertEquals(msg + "wrong number of features left", func(5)
1030 - func(to - from + 1), sfs.size());
1034 * inspect individual features
1036 for (SequenceFeature sf : sfs)
1038 verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
1044 * Check that after Undo, every feature has start/end that match its original
1045 * "start" and "end" properties
1051 protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
1053 for (SequenceFeature sf : sfs)
1055 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1056 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1057 String msg = String.format(
1058 "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
1060 assertEquals(msg + "start", oldFrom, sf.getBegin());
1061 assertEquals(msg + "end", oldTo, sf.getEnd());
1066 * Helper method to check a feature has been correctly relocated after a cut
1070 * start of cut (first residue cut 1..)
1072 * end of cut (last residue cut 1..)
1076 private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1077 boolean newDataset, int seqStart)
1079 // TODO handle the gapped sequence case as well
1080 int cutSize = to - from + 1;
1081 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1082 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1083 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1084 final int oldToPosition = oldTo - seqStart + 1; // 1..
1086 String msg = String.format(
1087 "Feature %s relocated to %d-%d after cut of %d-%d",
1088 sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1091 // dataset retained with all features unchanged
1092 assertEquals("0: " + msg, oldFrom, sf.getBegin());
1093 assertEquals("0: " + msg, oldTo, sf.getEnd());
1095 else if (oldToPosition < from)
1097 // before cut region so unchanged
1098 assertEquals("1: " + msg, oldFrom, sf.getBegin());
1099 assertEquals("2: " + msg, oldTo, sf.getEnd());
1101 else if (oldFromPosition > to)
1103 // follows cut region - shift by size of cut
1104 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1106 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1109 else if (oldFromPosition < from && oldToPosition > to)
1111 // feature encloses cut region - shrink it right
1112 assertEquals("5: " + msg, oldFrom, sf.getBegin());
1113 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1115 else if (oldFromPosition < from)
1117 // feature overlaps left side of cut region - truncated right
1118 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1120 else if (oldToPosition > to)
1122 // feature overlaps right side of cut region - truncated left
1123 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1125 assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1130 // feature internal to cut - should have been deleted!
1131 Assert.fail(msg + " - should have been deleted");
1136 * Test a cut action's relocation of sequence features
1138 @Test(groups = { "Functional" })
1139 public void testCut_withFeatures5prime()
1141 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1142 seq0.createDatasetSequence();
1143 assertEquals(8, seq0.getStart());
1144 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
1146 SequenceI[] seqsArray = new SequenceI[] { seq0 };
1147 AlignmentI alignment = new Alignment(seqsArray);
1150 * cut columns of A-B; same dataset sequence is retained, aligned sequence
1153 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1154 EditCommand.cut(ec, new AlignmentI[] { alignment });
1157 * feature on CC(10-11) should still be on CC(10-11)
1159 assertSame(seq0, alignment.getSequenceAt(0));
1160 assertEquals(10, seq0.getStart());
1161 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1162 assertEquals(1, sfs.size());
1163 SequenceFeature sf = sfs.get(0);
1164 assertEquals(10, sf.getBegin());
1165 assertEquals(11, sf.getEnd());