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());
295 * replace C- with XYZ
296 * NB arg4 = start column of selection for edit (base 0)
297 * arg5 = column after end of selection for edit
299 new EditCommand("", Action.REPLACE, "XYZ", new SequenceI[] { seq }, 2,
301 assertEquals("ABXYZ-DEF", seq.getSequenceAsString());
302 assertEquals("ABXYZDEF", seq.getDatasetSequence().getSequenceAsString());
306 * Test replace command when it doesn't cause a sequence edit (see comment in
308 @Test(groups = { "Functional" })
309 public void testReplaceFirstResiduesWithGaps()
311 // test replace when gaps are inserted at start. Start/end should change
312 // w.r.t. original edited sequence.
313 SequenceI dsseq = seqs[1].getDatasetSequence();
314 EditCommand edit = new EditCommand("", Action.REPLACE, "----",
316 { seqs[1] }, 0, 4, al);
319 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
320 // and ds is preserved
321 assertTrue(dsseq == seqs[1].getDatasetSequence());
322 // and it is unchanged
323 assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
324 // and that alignment sequence start has been adjusted
325 assertEquals(5, seqs[1].getStart());
326 assertEquals(11, seqs[1].getEnd());
328 AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
330 edit.undoCommand(views);
332 // dataset sequence unchanged
333 assertTrue(dsseq == seqs[1].getDatasetSequence());
335 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
336 // and start/end numbering also restored
337 assertEquals(1, seqs[1].getStart());
338 assertEquals(11, seqs[1].getEnd());
341 edit.undoCommand(views);
343 // and repeat asserts for the original edit
346 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
347 // and ds is preserved
348 assertTrue(dsseq == seqs[1].getDatasetSequence());
349 // and it is unchanged
350 assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
351 // and that alignment sequence start has been adjusted
352 assertEquals(5, seqs[1].getStart());
353 assertEquals(11, seqs[1].getEnd());
358 * Test that the addEdit command correctly merges insert gap commands when
361 @Test(groups = { "Functional" })
362 public void testAddEdit_multipleInsertGap()
365 * 3 insert gap in a row (aka mouse drag right):
367 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
368 new SequenceI[] { seqs[0] }, 1, 1, al);
370 SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
371 edited.setDatasetSequence(seqs[0].getDatasetSequence());
372 e = new EditCommand().new Edit(Action.INSERT_GAP,
373 new SequenceI[] { edited }, 2, 1, al);
375 edited = new Sequence("seq0", "a??bcdefghjk");
376 edited.setDatasetSequence(seqs[0].getDatasetSequence());
377 e = new EditCommand().new Edit(Action.INSERT_GAP,
378 new SequenceI[] { edited }, 3, 1, al);
380 assertEquals(1, testee.getSize());
381 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
382 assertEquals(1, testee.getEdit(0).getPosition());
383 assertEquals(3, testee.getEdit(0).getNumber());
386 * Add a non-contiguous edit - should not be merged.
388 e = new EditCommand().new Edit(Action.INSERT_GAP,
389 new SequenceI[] { edited }, 5, 2, al);
391 assertEquals(2, testee.getSize());
392 assertEquals(5, testee.getEdit(1).getPosition());
393 assertEquals(2, testee.getEdit(1).getNumber());
396 * Add a Delete after the Insert - should not be merged.
398 e = new EditCommand().new Edit(Action.DELETE_GAP,
399 new SequenceI[] { edited }, 6, 2, al);
401 assertEquals(3, testee.getSize());
402 assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
403 assertEquals(6, testee.getEdit(2).getPosition());
404 assertEquals(2, testee.getEdit(2).getNumber());
408 * Test that the addEdit command correctly merges delete gap commands when
411 @Test(groups = { "Functional" })
412 public void testAddEdit_multipleDeleteGap()
415 * 3 delete gap in a row (aka mouse drag left):
417 seqs[0].setSequence("a???bcdefghjk");
418 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
419 new SequenceI[] { seqs[0] }, 4, 1, al);
421 assertEquals(1, testee.getSize());
423 SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
424 edited.setDatasetSequence(seqs[0].getDatasetSequence());
425 e = new EditCommand().new Edit(Action.DELETE_GAP,
426 new SequenceI[] { edited }, 3, 1, al);
428 assertEquals(1, testee.getSize());
430 edited = new Sequence("seq0", "a?bcdefghjk");
431 edited.setDatasetSequence(seqs[0].getDatasetSequence());
432 e = new EditCommand().new Edit(Action.DELETE_GAP,
433 new SequenceI[] { edited }, 2, 1, al);
435 assertEquals(1, testee.getSize());
436 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
437 assertEquals(2, testee.getEdit(0).getPosition());
438 assertEquals(3, testee.getEdit(0).getNumber());
441 * Add a non-contiguous edit - should not be merged.
443 e = new EditCommand().new Edit(Action.DELETE_GAP,
444 new SequenceI[] { edited }, 2, 1, al);
446 assertEquals(2, testee.getSize());
447 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
448 assertEquals(2, testee.getEdit(1).getPosition());
449 assertEquals(1, testee.getEdit(1).getNumber());
452 * Add an Insert after the Delete - should not be merged.
454 e = new EditCommand().new Edit(Action.INSERT_GAP,
455 new SequenceI[] { edited }, 1, 1, al);
457 assertEquals(3, testee.getSize());
458 assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
459 assertEquals(1, testee.getEdit(2).getPosition());
460 assertEquals(1, testee.getEdit(2).getNumber());
464 * Test that the addEdit command correctly handles 'remove gaps' edits for the
465 * case when they appear contiguous but are acting on different sequences.
466 * They should not be merged.
468 @Test(groups = { "Functional" })
469 public void testAddEdit_removeAllGaps()
471 seqs[0].setSequence("a???bcdefghjk");
472 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
473 new SequenceI[] { seqs[0] }, 4, 1, al);
476 seqs[1].setSequence("f??ghjklmnopq");
477 Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[]
478 { seqs[1] }, 3, 1, al);
480 assertEquals(2, testee.getSize());
481 assertSame(e, testee.getEdit(0));
482 assertSame(e2, testee.getEdit(1));
486 * Test that the addEdit command correctly merges insert gap commands acting
487 * on a multi-sequence selection.
489 @Test(groups = { "Functional" })
490 public void testAddEdit_groupInsertGaps()
493 * 2 insert gap in a row (aka mouse drag right), on two sequences:
495 Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
496 seqs[0], seqs[1] }, 1, 1, al);
498 SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
499 seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
500 SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
501 seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
502 e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
503 seq1edited, seq2edited }, 2, 1, al);
506 assertEquals(1, testee.getSize());
507 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
508 assertEquals(1, testee.getEdit(0).getPosition());
509 assertEquals(2, testee.getEdit(0).getNumber());
510 assertEquals(seqs[0].getDatasetSequence(), testee.getEdit(0)
511 .getSequences()[0].getDatasetSequence());
512 assertEquals(seqs[1].getDatasetSequence(), testee.getEdit(0)
513 .getSequences()[1].getDatasetSequence());
517 * Test for 'undoing' a series of gap insertions.
519 * <li>Start: ABCDEF insert 2 at pos 1</li>
520 * <li>next: A--BCDEF insert 1 at pos 4</li>
521 * <li>next: A--B-CDEF insert 2 at pos 0</li>
522 * <li>last: --A--B-CDEF</li>
525 @Test(groups = { "Functional" })
526 public void testPriorState_multipleInserts()
528 EditCommand command = new EditCommand();
529 SequenceI seq = new Sequence("", "--A--B-CDEF");
530 SequenceI ds = new Sequence("", "ABCDEF");
531 seq.setDatasetSequence(ds);
532 SequenceI[] sqs = new SequenceI[] { seq };
533 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
535 e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
537 e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
540 Map<SequenceI, SequenceI> unwound = command.priorState(false);
541 assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
545 * Test for 'undoing' a series of gap deletions.
547 * <li>Start: A-B-C delete 1 at pos 1</li>
548 * <li>Next: AB-C delete 1 at pos 2</li>
552 @Test(groups = { "Functional" })
553 public void testPriorState_removeAllGaps()
555 EditCommand command = new EditCommand();
556 SequenceI seq = new Sequence("", "ABC");
557 SequenceI ds = new Sequence("", "ABC");
558 seq.setDatasetSequence(ds);
559 SequenceI[] sqs = new SequenceI[] { seq };
560 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
562 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
565 Map<SequenceI, SequenceI> unwound = command.priorState(false);
566 assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
570 * Test for 'undoing' a single delete edit.
572 @Test(groups = { "Functional" })
573 public void testPriorState_singleDelete()
575 EditCommand command = new EditCommand();
576 SequenceI seq = new Sequence("", "ABCDEF");
577 SequenceI ds = new Sequence("", "ABCDEF");
578 seq.setDatasetSequence(ds);
579 SequenceI[] sqs = new SequenceI[] { seq };
580 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
583 Map<SequenceI, SequenceI> unwound = command.priorState(false);
584 assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
588 * Test 'undoing' a single gap insertion edit command.
590 @Test(groups = { "Functional" })
591 public void testPriorState_singleInsert()
593 EditCommand command = new EditCommand();
594 SequenceI seq = new Sequence("", "AB---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, 2, 3, '-');
601 Map<SequenceI, SequenceI> unwound = command.priorState(false);
602 SequenceI prior = unwound.get(ds);
603 assertEquals("ABCDEF", prior.getSequenceAsString());
604 assertEquals(1, prior.getStart());
605 assertEquals(6, prior.getEnd());
609 * Test 'undoing' a single gap insertion edit command, on a sequence whose
610 * start residue is other than 1
612 @Test(groups = { "Functional" })
613 public void testPriorState_singleInsertWithOffset()
615 EditCommand command = new EditCommand();
616 SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
617 // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
618 // seq.setDatasetSequence(ds);
619 seq.createDatasetSequence();
620 SequenceI[] sqs = new SequenceI[] { seq };
621 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
624 Map<SequenceI, SequenceI> unwound = command.priorState(false);
625 SequenceI prior = unwound.get(seq.getDatasetSequence());
626 assertEquals("ABCDEF", prior.getSequenceAsString());
627 assertEquals(8, prior.getStart());
628 assertEquals(13, prior.getEnd());
632 * Test that mimics 'remove all gaps' action. This generates delete gap edits
633 * for contiguous gaps in each sequence separately.
635 @Test(groups = { "Functional" })
636 public void testPriorState_removeGapsMultipleSeqs()
638 EditCommand command = new EditCommand();
639 String original1 = "--ABC-DEF";
640 String original2 = "FG-HI--J";
641 String original3 = "M-NOPQ";
644 * Two edits for the first sequence
646 SequenceI seq = new Sequence("", "ABC-DEF");
647 SequenceI ds1 = new Sequence("", "ABCDEF");
648 seq.setDatasetSequence(ds1);
649 SequenceI[] sqs = new SequenceI[] { seq };
650 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
652 seq = new Sequence("", "ABCDEF");
653 seq.setDatasetSequence(ds1);
654 sqs = new SequenceI[] { seq };
655 e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
659 * Two edits for the second sequence
661 seq = new Sequence("", "FGHI--J");
662 SequenceI ds2 = new Sequence("", "FGHIJ");
663 seq.setDatasetSequence(ds2);
664 sqs = new SequenceI[] { seq };
665 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
667 seq = new Sequence("", "FGHIJ");
668 seq.setDatasetSequence(ds2);
669 sqs = new SequenceI[] { seq };
670 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
674 * One edit for the third sequence.
676 seq = new Sequence("", "MNOPQ");
677 SequenceI ds3 = new Sequence("", "MNOPQ");
678 seq.setDatasetSequence(ds3);
679 sqs = new SequenceI[] { seq };
680 e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
683 Map<SequenceI, SequenceI> unwound = command.priorState(false);
684 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
685 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
686 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
690 * Test that mimics 'remove all gapped columns' action. This generates a
691 * series Delete Gap edits that each act on all sequences that share a gapped
694 @Test(groups = { "Functional" })
695 public void testPriorState_removeGappedCols()
697 EditCommand command = new EditCommand();
698 String original1 = "--ABC--DEF";
699 String original2 = "-G-HI--J";
700 String original3 = "-M-NO--PQ";
703 * First edit deletes the first column.
705 SequenceI seq1 = new Sequence("", "-ABC--DEF");
706 SequenceI ds1 = new Sequence("", "ABCDEF");
707 seq1.setDatasetSequence(ds1);
708 SequenceI seq2 = new Sequence("", "G-HI--J");
709 SequenceI ds2 = new Sequence("", "GHIJ");
710 seq2.setDatasetSequence(ds2);
711 SequenceI seq3 = new Sequence("", "M-NO--PQ");
712 SequenceI ds3 = new Sequence("", "MNOPQ");
713 seq3.setDatasetSequence(ds3);
714 SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
715 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
719 * Second edit deletes what is now columns 4 and 5.
721 seq1 = new Sequence("", "-ABCDEF");
722 seq1.setDatasetSequence(ds1);
723 seq2 = new Sequence("", "G-HIJ");
724 seq2.setDatasetSequence(ds2);
725 seq3 = new Sequence("", "M-NOPQ");
726 seq3.setDatasetSequence(ds3);
727 sqs = new SequenceI[] { seq1, seq2, seq3 };
728 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
731 Map<SequenceI, SequenceI> unwound = command.priorState(false);
732 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
733 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
734 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
735 assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
736 assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
737 assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
741 * Test a cut action's relocation of sequence features
743 @Test(groups = { "Functional" })
744 public void testCut_withFeatures()
747 * create sequence features before, after and overlapping
748 * a cut of columns/residues 4-7
750 SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
751 seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
753 seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
755 seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
757 seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
759 seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
763 * add some contact features
765 SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
767 seq0.addSequenceFeature(internalContact); // should get deleted
768 SequenceFeature overlapLeftContact = new SequenceFeature(
769 "disulphide bond", "", 2, 6, 0f, null);
770 seq0.addSequenceFeature(overlapLeftContact); // should get deleted
771 SequenceFeature overlapRightContact = new SequenceFeature(
772 "disulphide bond", "", 5, 8, 0f, null);
773 seq0.addSequenceFeature(overlapRightContact); // should get deleted
774 SequenceFeature spanningContact = new SequenceFeature(
775 "disulphide bond", "", 2, 9, 0f, null);
776 seq0.addSequenceFeature(spanningContact); // should get shortened 3'
779 * cut columns 3-6 (base 0), residues d-g 4-7
781 Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
782 EditCommand.cut(ec, new AlignmentI[] { al });
784 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
785 SequenceFeatures.sortFeatures(sfs, true);
787 assertEquals(5, sfs.size()); // features internal to cut were deleted
788 SequenceFeature sf = sfs.get(0);
789 assertEquals("before", sf.getType());
790 assertEquals(1, sf.getBegin());
791 assertEquals(3, sf.getEnd());
793 assertEquals("disulphide bond", sf.getType());
794 assertEquals(2, sf.getBegin());
795 assertEquals(5, sf.getEnd()); // truncated by cut
797 assertEquals("overlap left", sf.getType());
798 assertEquals(2, sf.getBegin());
799 assertEquals(3, sf.getEnd()); // truncated by cut
801 assertEquals("after", sf.getType());
802 assertEquals(4, sf.getBegin()); // shifted left by cut
803 assertEquals(6, sf.getEnd()); // shifted left by cut
805 assertEquals("overlap right", sf.getType());
806 assertEquals(4, sf.getBegin()); // shifted left by cut
807 assertEquals(4, sf.getEnd()); // truncated by cut
811 * Test a cut action's relocation of sequence features, with full coverage of
812 * all possible feature and cut locations for a 5-position ungapped sequence
814 @Test(groups = { "Functional" })
815 public void testCut_withFeatures_exhaustive()
818 * create a sequence features on each subrange of 1-5
820 SequenceI seq0 = new Sequence("seq", "ABCDE");
823 seq0.setStart(start);
825 AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
826 alignment.setDataset(null);
829 * create a new alignment with shared dataset sequence
831 AlignmentI copy = new Alignment(
833 { alignment.getDataset().getSequenceAt(0).deriveSequence() });
834 SequenceI copySeq0 = copy.getSequenceAt(0);
836 for (int from = start; from <= end; from++)
838 for (int to = from; to <= end; to++)
840 String desc = String.format("%d-%d", from, to);
841 SequenceFeature sf = new SequenceFeature("test", desc, from, to,
843 sf.setValue("from", Integer.valueOf(from));
844 sf.setValue("to", Integer.valueOf(to));
845 seq0.addSequenceFeature(sf);
849 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
850 assertEquals(func(5), sfs.size());
851 assertEquals(sfs, copySeq0.getSequenceFeatures());
852 String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
855 * now perform all possible cuts of subranges of columns 1-5
856 * and validate the resulting remaining sequence features!
858 SequenceI[] sqs = new SequenceI[] { seq0 };
859 boolean checkDsSize = true;
861 for (int from = 0; from < seq0.getLength(); from++)
863 for (int to = from; to < seq0.getLength(); to++)
865 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
866 - from + 1), alignment);
867 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
868 boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
869 .getDatasetSequence();
871 verifyCut(seq0, from, to, msg, start);
874 * verify copy alignment dataset sequence unaffected
876 assertEquals("Original dataset sequence was modified",
877 copySequenceFeatures,
878 copySeq0.getSequenceFeatures().toString());
883 * verify a new dataset sequence has appeared
885 assertEquals("Wrong Dataset size after cut",
886 newDatasetSequence ? 2 : 1, alignment.getDataset()
890 * undo and verify all restored
892 AlignmentI[] views = new AlignmentI[] { alignment };
893 ec.undoCommand(views);
894 sfs = seq0.getSequenceFeatures();
895 assertEquals("After undo of " + msg, func(5), sfs.size());
896 verifyUndo(from, to, sfs);
899 * verify copy alignment dataset sequence still unaffected
901 assertEquals("Original dataset sequence was modified",
902 copySequenceFeatures,
903 copySeq0.getSequenceFeatures().toString());
908 * verify dataset sequence has shrunk
910 assertEquals("Wrong Dataset size after cut", 1,
911 alignment.getDataset().getHeight());
918 verifyCut(seq0, from, to, msg, start);
921 * verify copy alignment dataset sequence unaffected
923 assertEquals("Original dataset sequence was modified",
924 copySequenceFeatures,
925 copySeq0.getSequenceFeatures().toString());
930 * verify a new dataset sequence has appeared again
932 assertEquals("Wrong Dataset size after cut",
933 newDatasetSequence ? 2 : 1, alignment.getDataset()
938 * undo ready for next cut
940 ec.undoCommand(views);
943 * final verify that copy alignment dataset sequence is still unaffected
945 assertEquals("Original dataset sequence was modified",
946 copySequenceFeatures,
947 copySeq0.getSequenceFeatures().toString());
951 * and that dataset sequence has shrunk
953 assertEquals("Wrong Dataset size after cut", 1,
954 alignment.getDataset().getHeight());
961 * Verify by inspection that the sequence features left on the sequence after
962 * a cut match the expected results. The trick to this is that we can parse
963 * each feature's original start-end positions from its description.
971 protected void verifyCut(SequenceI seq0, int from, int to,
972 final String msg, int seqStart)
974 List<SequenceFeature> sfs;
975 sfs = seq0.getSequenceFeatures();
977 Collections.sort(sfs, BY_DESCRIPTION);
980 * confirm the number of features has reduced by the
981 * number of features within the cut region i.e. by
982 * func(length of cut); exception is a cut at start or end of sequence,
983 * which retains the original coordinates, dataset sequence
984 * and all its features
986 boolean datasetRetained = from == 0 || to == 4;
989 // dataset and all features retained
990 assertEquals(msg, func(5), sfs.size());
992 else if (to - from == 4)
994 // all columns were cut
995 assertTrue(sfs.isEmpty());
999 // failure in checkFeatureRelocation is more informative!
1000 assertEquals(msg + "wrong number of features left", func(5)
1001 - func(to - from + 1), sfs.size());
1005 * inspect individual features
1007 for (SequenceFeature sf : sfs)
1009 verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
1015 * Check that after Undo, every feature has start/end that match its original
1016 * "start" and "end" properties
1022 protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
1024 for (SequenceFeature sf : sfs)
1026 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1027 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1028 String msg = String.format(
1029 "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
1031 assertEquals(msg + "start", oldFrom, sf.getBegin());
1032 assertEquals(msg + "end", oldTo, sf.getEnd());
1037 * Helper method to check a feature has been correctly relocated after a cut
1041 * start of cut (first residue cut 1..)
1043 * end of cut (last residue cut 1..)
1047 private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1048 boolean newDataset, int seqStart)
1050 // TODO handle the gapped sequence case as well
1051 int cutSize = to - from + 1;
1052 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1053 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1054 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1055 final int oldToPosition = oldTo - seqStart + 1; // 1..
1057 String msg = String.format(
1058 "Feature %s relocated to %d-%d after cut of %d-%d",
1059 sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1062 // dataset retained with all features unchanged
1063 assertEquals("0: " + msg, oldFrom, sf.getBegin());
1064 assertEquals("0: " + msg, oldTo, sf.getEnd());
1066 else if (oldToPosition < from)
1068 // before cut region so unchanged
1069 assertEquals("1: " + msg, oldFrom, sf.getBegin());
1070 assertEquals("2: " + msg, oldTo, sf.getEnd());
1072 else if (oldFromPosition > to)
1074 // follows cut region - shift by size of cut
1075 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1077 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1080 else if (oldFromPosition < from && oldToPosition > to)
1082 // feature encloses cut region - shrink it right
1083 assertEquals("5: " + msg, oldFrom, sf.getBegin());
1084 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1086 else if (oldFromPosition < from)
1088 // feature overlaps left side of cut region - truncated right
1089 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1091 else if (oldToPosition > to)
1093 // feature overlaps right side of cut region - truncated left
1094 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1096 assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1101 // feature internal to cut - should have been deleted!
1102 Assert.fail(msg + " - should have been deleted");
1107 * Test a cut action's relocation of sequence features
1109 @Test(groups = { "Functional" })
1110 public void testCut_withFeatures5prime()
1112 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1113 seq0.createDatasetSequence();
1114 assertEquals(8, seq0.getStart());
1115 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
1117 SequenceI[] seqsArray = new SequenceI[] { seq0 };
1118 AlignmentI alignment = new Alignment(seqsArray);
1121 * cut columns of A-B; same dataset sequence is retained, aligned sequence
1124 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1125 EditCommand.cut(ec, new AlignmentI[] { alignment });
1128 * feature on CC(10-11) should still be on CC(10-11)
1130 assertSame(seq0, alignment.getSequenceAt(0));
1131 assertEquals(10, seq0.getStart());
1132 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1133 assertEquals(1, sfs.size());
1134 SequenceFeature sf = sfs.get(0);
1135 assertEquals(10, sf.getBegin());
1136 assertEquals(11, sf.getEnd());