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 = false)
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 replace command when it doesn't cause a sequence edit (see comment in
287 @Test(groups = { "Functional" })
288 public void testReplaceFirstResiduesWithGaps()
290 // test replace when gaps are inserted at start. Start/end should change
291 // w.r.t. original edited sequence.
292 SequenceI dsseq = seqs[1].getDatasetSequence();
293 EditCommand edit = new EditCommand("", Action.REPLACE, "----",
295 { seqs[1] }, 0, 4, al);
297 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
298 // and ds is preserved
299 assertTrue(dsseq == seqs[1].getDatasetSequence());
300 // and it is unchanged
301 assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
302 // and that alignment sequence start has been adjusted
303 assertEquals(5, seqs[1].getStart());
304 AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
306 edit.undoCommand(views);
308 // dataset sequence unchanged
309 assertTrue(dsseq == seqs[1].getDatasetSequence());
311 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
312 // and start/end numbering also restored
313 assertEquals(1, seqs[1].getStart());
318 * Test that the addEdit command correctly merges insert gap commands when
321 @Test(groups = { "Functional" })
322 public void testAddEdit_multipleInsertGap()
325 * 3 insert gap in a row (aka mouse drag right):
327 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
328 new SequenceI[] { seqs[0] }, 1, 1, al);
330 SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
331 edited.setDatasetSequence(seqs[0].getDatasetSequence());
332 e = new EditCommand().new Edit(Action.INSERT_GAP,
333 new SequenceI[] { edited }, 2, 1, al);
335 edited = new Sequence("seq0", "a??bcdefghjk");
336 edited.setDatasetSequence(seqs[0].getDatasetSequence());
337 e = new EditCommand().new Edit(Action.INSERT_GAP,
338 new SequenceI[] { edited }, 3, 1, al);
340 assertEquals(1, testee.getSize());
341 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
342 assertEquals(1, testee.getEdit(0).getPosition());
343 assertEquals(3, testee.getEdit(0).getNumber());
346 * Add a non-contiguous edit - should not be merged.
348 e = new EditCommand().new Edit(Action.INSERT_GAP,
349 new SequenceI[] { edited }, 5, 2, al);
351 assertEquals(2, testee.getSize());
352 assertEquals(5, testee.getEdit(1).getPosition());
353 assertEquals(2, testee.getEdit(1).getNumber());
356 * Add a Delete after the Insert - should not be merged.
358 e = new EditCommand().new Edit(Action.DELETE_GAP,
359 new SequenceI[] { edited }, 6, 2, al);
361 assertEquals(3, testee.getSize());
362 assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
363 assertEquals(6, testee.getEdit(2).getPosition());
364 assertEquals(2, testee.getEdit(2).getNumber());
368 * Test that the addEdit command correctly merges delete gap commands when
371 @Test(groups = { "Functional" })
372 public void testAddEdit_multipleDeleteGap()
375 * 3 delete gap in a row (aka mouse drag left):
377 seqs[0].setSequence("a???bcdefghjk");
378 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
379 new SequenceI[] { seqs[0] }, 4, 1, al);
381 assertEquals(1, testee.getSize());
383 SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
384 edited.setDatasetSequence(seqs[0].getDatasetSequence());
385 e = new EditCommand().new Edit(Action.DELETE_GAP,
386 new SequenceI[] { edited }, 3, 1, al);
388 assertEquals(1, testee.getSize());
390 edited = new Sequence("seq0", "a?bcdefghjk");
391 edited.setDatasetSequence(seqs[0].getDatasetSequence());
392 e = new EditCommand().new Edit(Action.DELETE_GAP,
393 new SequenceI[] { edited }, 2, 1, al);
395 assertEquals(1, testee.getSize());
396 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
397 assertEquals(2, testee.getEdit(0).getPosition());
398 assertEquals(3, testee.getEdit(0).getNumber());
401 * Add a non-contiguous edit - should not be merged.
403 e = new EditCommand().new Edit(Action.DELETE_GAP,
404 new SequenceI[] { edited }, 2, 1, al);
406 assertEquals(2, testee.getSize());
407 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
408 assertEquals(2, testee.getEdit(1).getPosition());
409 assertEquals(1, testee.getEdit(1).getNumber());
412 * Add an Insert after the Delete - should not be merged.
414 e = new EditCommand().new Edit(Action.INSERT_GAP,
415 new SequenceI[] { edited }, 1, 1, al);
417 assertEquals(3, testee.getSize());
418 assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
419 assertEquals(1, testee.getEdit(2).getPosition());
420 assertEquals(1, testee.getEdit(2).getNumber());
424 * Test that the addEdit command correctly handles 'remove gaps' edits for the
425 * case when they appear contiguous but are acting on different sequences.
426 * They should not be merged.
428 @Test(groups = { "Functional" })
429 public void testAddEdit_removeAllGaps()
431 seqs[0].setSequence("a???bcdefghjk");
432 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
433 new SequenceI[] { seqs[0] }, 4, 1, al);
436 seqs[1].setSequence("f??ghjklmnopq");
437 Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[]
438 { seqs[1] }, 3, 1, al);
440 assertEquals(2, testee.getSize());
441 assertSame(e, testee.getEdit(0));
442 assertSame(e2, testee.getEdit(1));
446 * Test that the addEdit command correctly merges insert gap commands acting
447 * on a multi-sequence selection.
449 @Test(groups = { "Functional" })
450 public void testAddEdit_groupInsertGaps()
453 * 2 insert gap in a row (aka mouse drag right), on two sequences:
455 Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
456 seqs[0], seqs[1] }, 1, 1, al);
458 SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
459 seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
460 SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
461 seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
462 e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
463 seq1edited, seq2edited }, 2, 1, al);
466 assertEquals(1, testee.getSize());
467 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
468 assertEquals(1, testee.getEdit(0).getPosition());
469 assertEquals(2, testee.getEdit(0).getNumber());
470 assertEquals(seqs[0].getDatasetSequence(), testee.getEdit(0)
471 .getSequences()[0].getDatasetSequence());
472 assertEquals(seqs[1].getDatasetSequence(), testee.getEdit(0)
473 .getSequences()[1].getDatasetSequence());
477 * Test for 'undoing' a series of gap insertions.
479 * <li>Start: ABCDEF insert 2 at pos 1</li>
480 * <li>next: A--BCDEF insert 1 at pos 4</li>
481 * <li>next: A--B-CDEF insert 2 at pos 0</li>
482 * <li>last: --A--B-CDEF</li>
485 @Test(groups = { "Functional" })
486 public void testPriorState_multipleInserts()
488 EditCommand command = new EditCommand();
489 SequenceI seq = new Sequence("", "--A--B-CDEF");
490 SequenceI ds = new Sequence("", "ABCDEF");
491 seq.setDatasetSequence(ds);
492 SequenceI[] sqs = new SequenceI[] { seq };
493 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
495 e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
497 e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
500 Map<SequenceI, SequenceI> unwound = command.priorState(false);
501 assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
505 * Test for 'undoing' a series of gap deletions.
507 * <li>Start: A-B-C delete 1 at pos 1</li>
508 * <li>Next: AB-C delete 1 at pos 2</li>
512 @Test(groups = { "Functional" })
513 public void testPriorState_removeAllGaps()
515 EditCommand command = new EditCommand();
516 SequenceI seq = new Sequence("", "ABC");
517 SequenceI ds = new Sequence("", "ABC");
518 seq.setDatasetSequence(ds);
519 SequenceI[] sqs = new SequenceI[] { seq };
520 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
522 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
525 Map<SequenceI, SequenceI> unwound = command.priorState(false);
526 assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
530 * Test for 'undoing' a single delete edit.
532 @Test(groups = { "Functional" })
533 public void testPriorState_singleDelete()
535 EditCommand command = new EditCommand();
536 SequenceI seq = new Sequence("", "ABCDEF");
537 SequenceI ds = new Sequence("", "ABCDEF");
538 seq.setDatasetSequence(ds);
539 SequenceI[] sqs = new SequenceI[] { seq };
540 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
543 Map<SequenceI, SequenceI> unwound = command.priorState(false);
544 assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
548 * Test 'undoing' a single gap insertion edit command.
550 @Test(groups = { "Functional" })
551 public void testPriorState_singleInsert()
553 EditCommand command = new EditCommand();
554 SequenceI seq = new Sequence("", "AB---CDEF");
555 SequenceI ds = new Sequence("", "ABCDEF");
556 seq.setDatasetSequence(ds);
557 SequenceI[] sqs = new SequenceI[] { seq };
558 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
561 Map<SequenceI, SequenceI> unwound = command.priorState(false);
562 SequenceI prior = unwound.get(ds);
563 assertEquals("ABCDEF", prior.getSequenceAsString());
564 assertEquals(1, prior.getStart());
565 assertEquals(6, prior.getEnd());
569 * Test 'undoing' a single gap insertion edit command, on a sequence whose
570 * start residue is other than 1
572 @Test(groups = { "Functional" })
573 public void testPriorState_singleInsertWithOffset()
575 EditCommand command = new EditCommand();
576 SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
577 // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
578 // seq.setDatasetSequence(ds);
579 seq.createDatasetSequence();
580 SequenceI[] sqs = new SequenceI[] { seq };
581 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
584 Map<SequenceI, SequenceI> unwound = command.priorState(false);
585 SequenceI prior = unwound.get(seq.getDatasetSequence());
586 assertEquals("ABCDEF", prior.getSequenceAsString());
587 assertEquals(8, prior.getStart());
588 assertEquals(13, prior.getEnd());
592 * Test that mimics 'remove all gaps' action. This generates delete gap edits
593 * for contiguous gaps in each sequence separately.
595 @Test(groups = { "Functional" })
596 public void testPriorState_removeGapsMultipleSeqs()
598 EditCommand command = new EditCommand();
599 String original1 = "--ABC-DEF";
600 String original2 = "FG-HI--J";
601 String original3 = "M-NOPQ";
604 * Two edits for the first sequence
606 SequenceI seq = new Sequence("", "ABC-DEF");
607 SequenceI ds1 = new Sequence("", "ABCDEF");
608 seq.setDatasetSequence(ds1);
609 SequenceI[] sqs = new SequenceI[] { seq };
610 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
612 seq = new Sequence("", "ABCDEF");
613 seq.setDatasetSequence(ds1);
614 sqs = new SequenceI[] { seq };
615 e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
619 * Two edits for the second sequence
621 seq = new Sequence("", "FGHI--J");
622 SequenceI ds2 = new Sequence("", "FGHIJ");
623 seq.setDatasetSequence(ds2);
624 sqs = new SequenceI[] { seq };
625 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
627 seq = new Sequence("", "FGHIJ");
628 seq.setDatasetSequence(ds2);
629 sqs = new SequenceI[] { seq };
630 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
634 * One edit for the third sequence.
636 seq = new Sequence("", "MNOPQ");
637 SequenceI ds3 = new Sequence("", "MNOPQ");
638 seq.setDatasetSequence(ds3);
639 sqs = new SequenceI[] { seq };
640 e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
643 Map<SequenceI, SequenceI> unwound = command.priorState(false);
644 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
645 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
646 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
650 * Test that mimics 'remove all gapped columns' action. This generates a
651 * series Delete Gap edits that each act on all sequences that share a gapped
654 @Test(groups = { "Functional" })
655 public void testPriorState_removeGappedCols()
657 EditCommand command = new EditCommand();
658 String original1 = "--ABC--DEF";
659 String original2 = "-G-HI--J";
660 String original3 = "-M-NO--PQ";
663 * First edit deletes the first column.
665 SequenceI seq1 = new Sequence("", "-ABC--DEF");
666 SequenceI ds1 = new Sequence("", "ABCDEF");
667 seq1.setDatasetSequence(ds1);
668 SequenceI seq2 = new Sequence("", "G-HI--J");
669 SequenceI ds2 = new Sequence("", "GHIJ");
670 seq2.setDatasetSequence(ds2);
671 SequenceI seq3 = new Sequence("", "M-NO--PQ");
672 SequenceI ds3 = new Sequence("", "MNOPQ");
673 seq3.setDatasetSequence(ds3);
674 SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
675 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
679 * Second edit deletes what is now columns 4 and 5.
681 seq1 = new Sequence("", "-ABCDEF");
682 seq1.setDatasetSequence(ds1);
683 seq2 = new Sequence("", "G-HIJ");
684 seq2.setDatasetSequence(ds2);
685 seq3 = new Sequence("", "M-NOPQ");
686 seq3.setDatasetSequence(ds3);
687 sqs = new SequenceI[] { seq1, seq2, seq3 };
688 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
691 Map<SequenceI, SequenceI> unwound = command.priorState(false);
692 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
693 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
694 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
695 assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
696 assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
697 assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
701 * Test a cut action's relocation of sequence features
703 @Test(groups = { "Functional" })
704 public void testCut_withFeatures()
707 * create sequence features before, after and overlapping
708 * a cut of columns/residues 4-7
710 SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
711 seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
713 seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
715 seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
717 seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
719 seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
723 * add some contact features
725 SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
727 seq0.addSequenceFeature(internalContact); // should get deleted
728 SequenceFeature overlapLeftContact = new SequenceFeature(
729 "disulphide bond", "", 2, 6, 0f, null);
730 seq0.addSequenceFeature(overlapLeftContact); // should get deleted
731 SequenceFeature overlapRightContact = new SequenceFeature(
732 "disulphide bond", "", 5, 8, 0f, null);
733 seq0.addSequenceFeature(overlapRightContact); // should get deleted
734 SequenceFeature spanningContact = new SequenceFeature(
735 "disulphide bond", "", 2, 9, 0f, null);
736 seq0.addSequenceFeature(spanningContact); // should get shortened 3'
739 * cut columns 3-6 (base 0), residues d-g 4-7
741 Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
742 EditCommand.cut(ec, new AlignmentI[] { al });
744 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
745 SequenceFeatures.sortFeatures(sfs, true);
747 assertEquals(5, sfs.size()); // features internal to cut were deleted
748 SequenceFeature sf = sfs.get(0);
749 assertEquals("before", sf.getType());
750 assertEquals(1, sf.getBegin());
751 assertEquals(3, sf.getEnd());
753 assertEquals("disulphide bond", sf.getType());
754 assertEquals(2, sf.getBegin());
755 assertEquals(5, sf.getEnd()); // truncated by cut
757 assertEquals("overlap left", sf.getType());
758 assertEquals(2, sf.getBegin());
759 assertEquals(3, sf.getEnd()); // truncated by cut
761 assertEquals("after", sf.getType());
762 assertEquals(4, sf.getBegin()); // shifted left by cut
763 assertEquals(6, sf.getEnd()); // shifted left by cut
765 assertEquals("overlap right", sf.getType());
766 assertEquals(4, sf.getBegin()); // shifted left by cut
767 assertEquals(4, sf.getEnd()); // truncated by cut
771 * Test a cut action's relocation of sequence features, with full coverage of
772 * all possible feature and cut locations for a 5-position ungapped sequence
774 @Test(groups = { "Functional" })
775 public void testCut_withFeatures_exhaustive()
778 * create a sequence features on each subrange of 1-5
780 SequenceI seq0 = new Sequence("seq", "ABCDE");
783 seq0.setStart(start);
785 AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
786 alignment.setDataset(null);
789 * create a new alignment with shared dataset sequence
791 AlignmentI copy = new Alignment(
793 { alignment.getDataset().getSequenceAt(0).deriveSequence() });
794 SequenceI copySeq0 = copy.getSequenceAt(0);
796 for (int from = start; from <= end; from++)
798 for (int to = from; to <= end; to++)
800 String desc = String.format("%d-%d", from, to);
801 SequenceFeature sf = new SequenceFeature("test", desc, from, to,
803 sf.setValue("from", Integer.valueOf(from));
804 sf.setValue("to", Integer.valueOf(to));
805 seq0.addSequenceFeature(sf);
809 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
810 assertEquals(func(5), sfs.size());
811 assertEquals(sfs, copySeq0.getSequenceFeatures());
812 String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
815 * now perform all possible cuts of subranges of columns 1-5
816 * and validate the resulting remaining sequence features!
818 SequenceI[] sqs = new SequenceI[] { seq0 };
819 boolean checkDsSize = true;
821 for (int from = 0; from < seq0.getLength(); from++)
823 for (int to = from; to < seq0.getLength(); to++)
825 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
826 - from + 1), alignment);
827 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
828 boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
829 .getDatasetSequence();
831 verifyCut(seq0, from, to, msg, start);
834 * verify copy alignment dataset sequence unaffected
836 assertEquals("Original dataset sequence was modified",
837 copySequenceFeatures,
838 copySeq0.getSequenceFeatures().toString());
843 * verify a new dataset sequence has appeared
845 assertEquals("Wrong Dataset size after cut",
846 newDatasetSequence ? 2 : 1, alignment.getDataset()
850 * undo and verify all restored
852 AlignmentI[] views = new AlignmentI[] { alignment };
853 ec.undoCommand(views);
854 sfs = seq0.getSequenceFeatures();
855 assertEquals("After undo of " + msg, func(5), sfs.size());
856 verifyUndo(from, to, sfs);
859 * verify copy alignment dataset sequence still unaffected
861 assertEquals("Original dataset sequence was modified",
862 copySequenceFeatures,
863 copySeq0.getSequenceFeatures().toString());
868 * verify dataset sequence has shrunk
870 assertEquals("Wrong Dataset size after cut", 1,
871 alignment.getDataset().getHeight());
877 verifyCut(seq0, from, to, msg, start);
880 * verify copy alignment dataset sequence unaffected
882 assertEquals("Original dataset sequence was modified",
883 copySequenceFeatures,
884 copySeq0.getSequenceFeatures().toString());
889 * verify a new dataset sequence has appeared again
891 assertEquals("Wrong Dataset size after cut",
892 newDatasetSequence ? 2 : 1, alignment.getDataset()
896 * undo ready for next cut
898 ec.undoCommand(views);
901 * final verify that copy alignment dataset sequence is still unaffected
903 assertEquals("Original dataset sequence was modified",
904 copySequenceFeatures,
905 copySeq0.getSequenceFeatures().toString());
909 * and that dataset sequence has shrunk
911 assertEquals("Wrong Dataset size after cut", 1,
912 alignment.getDataset().getHeight());
919 * Verify by inspection that the sequence features left on the sequence after
920 * a cut match the expected results. The trick to this is that we can parse
921 * each feature's original start-end positions from its description.
929 protected void verifyCut(SequenceI seq0, int from, int to,
930 final String msg, int seqStart)
932 List<SequenceFeature> sfs;
933 sfs = seq0.getSequenceFeatures();
935 Collections.sort(sfs, BY_DESCRIPTION);
938 * confirm the number of features has reduced by the
939 * number of features within the cut region i.e. by
940 * func(length of cut); exception is a cut at start or end of sequence,
941 * which retains the original coordinates, dataset sequence
942 * and all its features
944 boolean datasetRetained = from == 0 || to == 4;
947 // dataset and all features retained
948 assertEquals(msg, func(5), sfs.size());
950 else if (to - from == 4)
952 // all columns were cut
953 assertTrue(sfs.isEmpty());
957 // failure in checkFeatureRelocation is more informative!
958 assertEquals(msg + "wrong number of features left", func(5)
959 - func(to - from + 1), sfs.size());
963 * inspect individual features
965 for (SequenceFeature sf : sfs)
967 verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
973 * Check that after Undo, every feature has start/end that match its original
974 * "start" and "end" properties
980 protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
982 for (SequenceFeature sf : sfs)
984 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
985 final int oldTo = ((Integer) sf.getValue("to")).intValue();
986 String msg = String.format(
987 "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
989 assertEquals(msg + "start", oldFrom, sf.getBegin());
990 assertEquals(msg + "end", oldTo, sf.getEnd());
995 * Helper method to check a feature has been correctly relocated after a cut
999 * start of cut (first residue cut 1..)
1001 * end of cut (last residue cut 1..)
1005 private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1006 boolean newDataset, int seqStart)
1008 // TODO handle the gapped sequence case as well
1009 int cutSize = to - from + 1;
1010 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1011 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1012 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1013 final int oldToPosition = oldTo - seqStart + 1; // 1..
1015 String msg = String.format(
1016 "Feature %s relocated to %d-%d after cut of %d-%d",
1017 sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1020 // dataset retained with all features unchanged
1021 assertEquals("0: " + msg, oldFrom, sf.getBegin());
1022 assertEquals("0: " + msg, oldTo, sf.getEnd());
1024 else if (oldToPosition < from)
1026 // before cut region so unchanged
1027 assertEquals("1: " + msg, oldFrom, sf.getBegin());
1028 assertEquals("2: " + msg, oldTo, sf.getEnd());
1030 else if (oldFromPosition > to)
1032 // follows cut region - shift by size of cut
1033 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1035 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1038 else if (oldFromPosition < from && oldToPosition > to)
1040 // feature encloses cut region - shrink it right
1041 assertEquals("5: " + msg, oldFrom, sf.getBegin());
1042 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1044 else if (oldFromPosition < from)
1046 // feature overlaps left side of cut region - truncated right
1047 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1049 else if (oldToPosition > to)
1051 // feature overlaps right side of cut region - truncated left
1052 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1054 assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1059 // feature internal to cut - should have been deleted!
1060 Assert.fail(msg + " - should have been deleted");
1065 * Test a cut action's relocation of sequence features
1067 @Test(groups = { "Functional" })
1068 public void testCut_withFeatures5prime()
1070 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1071 seq0.createDatasetSequence();
1072 assertEquals(8, seq0.getStart());
1073 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
1075 SequenceI[] seqsArray = new SequenceI[] { seq0 };
1076 AlignmentI alignment = new Alignment(seqsArray);
1079 * cut columns of A-B; same dataset sequence is retained, aligned sequence
1082 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1083 EditCommand.cut(ec, new AlignmentI[] { alignment });
1086 * feature on CC(10-11) should still be on CC(10-11)
1088 assertSame(seq0, alignment.getSequenceAt(0));
1089 assertEquals(10, seq0.getStart());
1090 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1091 assertEquals(1, sfs.size());
1092 SequenceFeature sf = sfs.get(0);
1093 assertEquals(10, sf.getBegin());
1094 assertEquals(11, sf.getEnd());