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);
788 * create a new alignment with shared dataset sequence
790 AlignmentI copy = new Alignment(
792 { alignment.getDataset().getSequenceAt(0).deriveSequence() });
793 SequenceI copySeq0 = copy.getSequenceAt(0);
795 for (int from = start; from <= end; from++)
797 for (int to = from; to <= end; to++)
799 String desc = String.format("%d-%d", from, to);
800 SequenceFeature sf = new SequenceFeature("test", desc, from, to,
802 sf.setValue("from", Integer.valueOf(from));
803 sf.setValue("to", Integer.valueOf(to));
804 seq0.addSequenceFeature(sf);
808 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
809 assertEquals(func(5), sfs.size());
810 assertEquals(sfs, copySeq0.getSequenceFeatures());
811 String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
813 * now perform all possible cuts of subranges of columns 1-5
814 * and validate the resulting remaining sequence features!
816 SequenceI[] sqs = new SequenceI[] { seq0 };
817 boolean checkDsSize = false;
819 for (int from = 0; from < seq0.getLength(); from++)
821 for (int to = from; to < seq0.getLength(); to++)
823 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
824 - from + 1), alignment);
825 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
827 verifyCut(seq0, from, to, msg, start);
830 * verify copy alignment dataset sequence unaffected
832 assertEquals("Original dataset sequence was modified",
833 copySequenceFeatures,
834 copySeq0.getSequenceFeatures().toString());
838 * verify a new dataset sequence has appeared
840 assertEquals("Wrong Dataset size after cut",
841 copySeq0.getDatasetSequence() == seq0.getDatasetSequence()
844 alignment.getDataset().getHeight());
847 * undo and verify all restored
849 AlignmentI[] views = new AlignmentI[] { alignment };
850 ec.undoCommand(views);
851 sfs = seq0.getSequenceFeatures();
852 assertEquals("After undo of " + msg, func(5), sfs.size());
853 verifyUndo(from, to, sfs);
856 * verify copy alignment dataset sequence still unaffected
858 assertEquals("Original dataset sequence was modified",
859 copySequenceFeatures,
860 copySeq0.getSequenceFeatures().toString());
865 * verify dataset sequence has shrunk
867 assertEquals("Wrong Dataset size after cut",
868 copySeq0.getDatasetSequence() == seq0.getDatasetSequence()
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 copySeq0.getDatasetSequence() == seq0.getDatasetSequence()
895 alignment.getDataset().getHeight());
898 * undo ready for next cut
900 ec.undoCommand(views);
903 * final verify that copy alignment dataset sequence is still unaffected
905 assertEquals("Original dataset sequence was modified",
906 copySequenceFeatures,
907 copySeq0.getSequenceFeatures().toString());
911 * and that dataset sequence has shrunk
913 assertEquals("Wrong Dataset size after cut",
914 copySeq0.getDatasetSequence() == seq0.getDatasetSequence()
917 alignment.getDataset().getHeight());
924 * Verify by inspection that the sequence features left on the sequence after
925 * a cut match the expected results. The trick to this is that we can parse
926 * each feature's original start-end positions from its description.
934 protected void verifyCut(SequenceI seq0, int from, int to,
935 final String msg, int seqStart)
937 List<SequenceFeature> sfs;
938 sfs = seq0.getSequenceFeatures();
940 Collections.sort(sfs, BY_DESCRIPTION);
943 * confirm the number of features has reduced by the
944 * number of features within the cut region i.e. by
945 * func(length of cut); exception is a cut at start or end of sequence,
946 * which retains the original coordinates, dataset sequence
947 * and all its features
949 boolean datasetRetained = from == 0 || to == 4;
952 // dataset and all features retained
953 assertEquals(msg, func(5), sfs.size());
955 else if (to - from == 4)
957 // all columns were cut
958 assertTrue(sfs.isEmpty());
962 // failure in checkFeatureRelocation is more informative!
963 assertEquals(msg + "wrong number of features left", func(5)
964 - func(to - from + 1), sfs.size());
968 * inspect individual features
970 for (SequenceFeature sf : sfs)
972 verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
978 * Check that after Undo, every feature has start/end that match its original
979 * "start" and "end" properties
985 protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
987 for (SequenceFeature sf : sfs)
989 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
990 final int oldTo = ((Integer) sf.getValue("to")).intValue();
991 String msg = String.format(
992 "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
994 assertEquals(msg + "start", oldFrom, sf.getBegin());
995 assertEquals(msg + "end", oldTo, sf.getEnd());
1000 * Helper method to check a feature has been correctly relocated after a cut
1004 * start of cut (first residue cut 1..)
1006 * end of cut (last residue cut 1..)
1010 private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1011 boolean newDataset, int seqStart)
1013 // TODO handle the gapped sequence case as well
1014 int cutSize = to - from + 1;
1015 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1016 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1017 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1018 final int oldToPosition = oldTo - seqStart + 1; // 1..
1020 String msg = String.format(
1021 "Feature %s relocated to %d-%d after cut of %d-%d",
1022 sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1025 // dataset retained with all features unchanged
1026 assertEquals("0: " + msg, oldFrom, sf.getBegin());
1027 assertEquals("0: " + msg, oldTo, sf.getEnd());
1029 else if (oldToPosition < from)
1031 // before cut region so unchanged
1032 assertEquals("1: " + msg, oldFrom, sf.getBegin());
1033 assertEquals("2: " + msg, oldTo, sf.getEnd());
1035 else if (oldFromPosition > to)
1037 // follows cut region - shift by size of cut
1038 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1040 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1043 else if (oldFromPosition < from && oldToPosition > to)
1045 // feature encloses cut region - shrink it right
1046 assertEquals("5: " + msg, oldFrom, sf.getBegin());
1047 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1049 else if (oldFromPosition < from)
1051 // feature overlaps left side of cut region - truncated right
1052 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1054 else if (oldToPosition > to)
1056 // feature overlaps right side of cut region - truncated left
1057 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1059 assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1064 // feature internal to cut - should have been deleted!
1065 Assert.fail(msg + " - should have been deleted");
1070 * Test a cut action's relocation of sequence features
1072 @Test(groups = { "Functional" })
1073 public void testCut_withFeatures5prime()
1075 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1076 seq0.createDatasetSequence();
1077 assertEquals(8, seq0.getStart());
1078 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
1080 SequenceI[] seqsArray = new SequenceI[] { seq0 };
1081 AlignmentI alignment = new Alignment(seqsArray);
1084 * cut columns of A-B; same dataset sequence is retained, aligned sequence
1087 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1088 EditCommand.cut(ec, new AlignmentI[] { alignment });
1091 * feature on CC(10-11) should still be on CC(10-11)
1093 assertSame(seq0, alignment.getSequenceAt(0));
1094 assertEquals(10, seq0.getStart());
1095 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1096 assertEquals(1, sfs.size());
1097 SequenceFeature sf = sfs.get(0);
1098 assertEquals(10, sf.getBegin());
1099 assertEquals(11, sf.getEnd());