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);
318 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
319 // and ds is preserved
320 assertTrue(dsseq == seqs[1].getDatasetSequence());
321 // and it is unchanged
322 assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
323 // and that alignment sequence start has been adjusted
324 assertEquals(5, seqs[1].getStart());
325 AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
327 edit.undoCommand(views);
329 // dataset sequence unchanged
330 assertTrue(dsseq == seqs[1].getDatasetSequence());
332 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
333 // and start/end numbering also restored
334 assertEquals(1, seqs[1].getStart());
339 * Test that the addEdit command correctly merges insert gap commands when
342 @Test(groups = { "Functional" })
343 public void testAddEdit_multipleInsertGap()
346 * 3 insert gap in a row (aka mouse drag right):
348 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
349 new SequenceI[] { seqs[0] }, 1, 1, al);
351 SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
352 edited.setDatasetSequence(seqs[0].getDatasetSequence());
353 e = new EditCommand().new Edit(Action.INSERT_GAP,
354 new SequenceI[] { edited }, 2, 1, al);
356 edited = new Sequence("seq0", "a??bcdefghjk");
357 edited.setDatasetSequence(seqs[0].getDatasetSequence());
358 e = new EditCommand().new Edit(Action.INSERT_GAP,
359 new SequenceI[] { edited }, 3, 1, al);
361 assertEquals(1, testee.getSize());
362 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
363 assertEquals(1, testee.getEdit(0).getPosition());
364 assertEquals(3, testee.getEdit(0).getNumber());
367 * Add a non-contiguous edit - should not be merged.
369 e = new EditCommand().new Edit(Action.INSERT_GAP,
370 new SequenceI[] { edited }, 5, 2, al);
372 assertEquals(2, testee.getSize());
373 assertEquals(5, testee.getEdit(1).getPosition());
374 assertEquals(2, testee.getEdit(1).getNumber());
377 * Add a Delete after the Insert - should not be merged.
379 e = new EditCommand().new Edit(Action.DELETE_GAP,
380 new SequenceI[] { edited }, 6, 2, al);
382 assertEquals(3, testee.getSize());
383 assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
384 assertEquals(6, testee.getEdit(2).getPosition());
385 assertEquals(2, testee.getEdit(2).getNumber());
389 * Test that the addEdit command correctly merges delete gap commands when
392 @Test(groups = { "Functional" })
393 public void testAddEdit_multipleDeleteGap()
396 * 3 delete gap in a row (aka mouse drag left):
398 seqs[0].setSequence("a???bcdefghjk");
399 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
400 new SequenceI[] { seqs[0] }, 4, 1, al);
402 assertEquals(1, testee.getSize());
404 SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
405 edited.setDatasetSequence(seqs[0].getDatasetSequence());
406 e = new EditCommand().new Edit(Action.DELETE_GAP,
407 new SequenceI[] { edited }, 3, 1, al);
409 assertEquals(1, testee.getSize());
411 edited = new Sequence("seq0", "a?bcdefghjk");
412 edited.setDatasetSequence(seqs[0].getDatasetSequence());
413 e = new EditCommand().new Edit(Action.DELETE_GAP,
414 new SequenceI[] { edited }, 2, 1, al);
416 assertEquals(1, testee.getSize());
417 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
418 assertEquals(2, testee.getEdit(0).getPosition());
419 assertEquals(3, testee.getEdit(0).getNumber());
422 * Add a non-contiguous edit - should not be merged.
424 e = new EditCommand().new Edit(Action.DELETE_GAP,
425 new SequenceI[] { edited }, 2, 1, al);
427 assertEquals(2, testee.getSize());
428 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
429 assertEquals(2, testee.getEdit(1).getPosition());
430 assertEquals(1, testee.getEdit(1).getNumber());
433 * Add an Insert after the Delete - should not be merged.
435 e = new EditCommand().new Edit(Action.INSERT_GAP,
436 new SequenceI[] { edited }, 1, 1, al);
438 assertEquals(3, testee.getSize());
439 assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
440 assertEquals(1, testee.getEdit(2).getPosition());
441 assertEquals(1, testee.getEdit(2).getNumber());
445 * Test that the addEdit command correctly handles 'remove gaps' edits for the
446 * case when they appear contiguous but are acting on different sequences.
447 * They should not be merged.
449 @Test(groups = { "Functional" })
450 public void testAddEdit_removeAllGaps()
452 seqs[0].setSequence("a???bcdefghjk");
453 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
454 new SequenceI[] { seqs[0] }, 4, 1, al);
457 seqs[1].setSequence("f??ghjklmnopq");
458 Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[]
459 { seqs[1] }, 3, 1, al);
461 assertEquals(2, testee.getSize());
462 assertSame(e, testee.getEdit(0));
463 assertSame(e2, testee.getEdit(1));
467 * Test that the addEdit command correctly merges insert gap commands acting
468 * on a multi-sequence selection.
470 @Test(groups = { "Functional" })
471 public void testAddEdit_groupInsertGaps()
474 * 2 insert gap in a row (aka mouse drag right), on two sequences:
476 Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
477 seqs[0], seqs[1] }, 1, 1, al);
479 SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
480 seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
481 SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
482 seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
483 e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
484 seq1edited, seq2edited }, 2, 1, al);
487 assertEquals(1, testee.getSize());
488 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
489 assertEquals(1, testee.getEdit(0).getPosition());
490 assertEquals(2, testee.getEdit(0).getNumber());
491 assertEquals(seqs[0].getDatasetSequence(), testee.getEdit(0)
492 .getSequences()[0].getDatasetSequence());
493 assertEquals(seqs[1].getDatasetSequence(), testee.getEdit(0)
494 .getSequences()[1].getDatasetSequence());
498 * Test for 'undoing' a series of gap insertions.
500 * <li>Start: ABCDEF insert 2 at pos 1</li>
501 * <li>next: A--BCDEF insert 1 at pos 4</li>
502 * <li>next: A--B-CDEF insert 2 at pos 0</li>
503 * <li>last: --A--B-CDEF</li>
506 @Test(groups = { "Functional" })
507 public void testPriorState_multipleInserts()
509 EditCommand command = new EditCommand();
510 SequenceI seq = new Sequence("", "--A--B-CDEF");
511 SequenceI ds = new Sequence("", "ABCDEF");
512 seq.setDatasetSequence(ds);
513 SequenceI[] sqs = new SequenceI[] { seq };
514 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
516 e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
518 e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
521 Map<SequenceI, SequenceI> unwound = command.priorState(false);
522 assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
526 * Test for 'undoing' a series of gap deletions.
528 * <li>Start: A-B-C delete 1 at pos 1</li>
529 * <li>Next: AB-C delete 1 at pos 2</li>
533 @Test(groups = { "Functional" })
534 public void testPriorState_removeAllGaps()
536 EditCommand command = new EditCommand();
537 SequenceI seq = new Sequence("", "ABC");
538 SequenceI ds = new Sequence("", "ABC");
539 seq.setDatasetSequence(ds);
540 SequenceI[] sqs = new SequenceI[] { seq };
541 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
543 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
546 Map<SequenceI, SequenceI> unwound = command.priorState(false);
547 assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
551 * Test for 'undoing' a single delete edit.
553 @Test(groups = { "Functional" })
554 public void testPriorState_singleDelete()
556 EditCommand command = new EditCommand();
557 SequenceI seq = new Sequence("", "ABCDEF");
558 SequenceI ds = new Sequence("", "ABCDEF");
559 seq.setDatasetSequence(ds);
560 SequenceI[] sqs = new SequenceI[] { seq };
561 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
564 Map<SequenceI, SequenceI> unwound = command.priorState(false);
565 assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
569 * Test 'undoing' a single gap insertion edit command.
571 @Test(groups = { "Functional" })
572 public void testPriorState_singleInsert()
574 EditCommand command = new EditCommand();
575 SequenceI seq = new Sequence("", "AB---CDEF");
576 SequenceI ds = new Sequence("", "ABCDEF");
577 seq.setDatasetSequence(ds);
578 SequenceI[] sqs = new SequenceI[] { seq };
579 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
582 Map<SequenceI, SequenceI> unwound = command.priorState(false);
583 SequenceI prior = unwound.get(ds);
584 assertEquals("ABCDEF", prior.getSequenceAsString());
585 assertEquals(1, prior.getStart());
586 assertEquals(6, prior.getEnd());
590 * Test 'undoing' a single gap insertion edit command, on a sequence whose
591 * start residue is other than 1
593 @Test(groups = { "Functional" })
594 public void testPriorState_singleInsertWithOffset()
596 EditCommand command = new EditCommand();
597 SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
598 // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
599 // seq.setDatasetSequence(ds);
600 seq.createDatasetSequence();
601 SequenceI[] sqs = new SequenceI[] { seq };
602 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
605 Map<SequenceI, SequenceI> unwound = command.priorState(false);
606 SequenceI prior = unwound.get(seq.getDatasetSequence());
607 assertEquals("ABCDEF", prior.getSequenceAsString());
608 assertEquals(8, prior.getStart());
609 assertEquals(13, prior.getEnd());
613 * Test that mimics 'remove all gaps' action. This generates delete gap edits
614 * for contiguous gaps in each sequence separately.
616 @Test(groups = { "Functional" })
617 public void testPriorState_removeGapsMultipleSeqs()
619 EditCommand command = new EditCommand();
620 String original1 = "--ABC-DEF";
621 String original2 = "FG-HI--J";
622 String original3 = "M-NOPQ";
625 * Two edits for the first sequence
627 SequenceI seq = new Sequence("", "ABC-DEF");
628 SequenceI ds1 = new Sequence("", "ABCDEF");
629 seq.setDatasetSequence(ds1);
630 SequenceI[] sqs = new SequenceI[] { seq };
631 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
633 seq = new Sequence("", "ABCDEF");
634 seq.setDatasetSequence(ds1);
635 sqs = new SequenceI[] { seq };
636 e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
640 * Two edits for the second sequence
642 seq = new Sequence("", "FGHI--J");
643 SequenceI ds2 = new Sequence("", "FGHIJ");
644 seq.setDatasetSequence(ds2);
645 sqs = new SequenceI[] { seq };
646 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
648 seq = new Sequence("", "FGHIJ");
649 seq.setDatasetSequence(ds2);
650 sqs = new SequenceI[] { seq };
651 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
655 * One edit for the third sequence.
657 seq = new Sequence("", "MNOPQ");
658 SequenceI ds3 = new Sequence("", "MNOPQ");
659 seq.setDatasetSequence(ds3);
660 sqs = new SequenceI[] { seq };
661 e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
664 Map<SequenceI, SequenceI> unwound = command.priorState(false);
665 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
666 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
667 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
671 * Test that mimics 'remove all gapped columns' action. This generates a
672 * series Delete Gap edits that each act on all sequences that share a gapped
675 @Test(groups = { "Functional" })
676 public void testPriorState_removeGappedCols()
678 EditCommand command = new EditCommand();
679 String original1 = "--ABC--DEF";
680 String original2 = "-G-HI--J";
681 String original3 = "-M-NO--PQ";
684 * First edit deletes the first column.
686 SequenceI seq1 = new Sequence("", "-ABC--DEF");
687 SequenceI ds1 = new Sequence("", "ABCDEF");
688 seq1.setDatasetSequence(ds1);
689 SequenceI seq2 = new Sequence("", "G-HI--J");
690 SequenceI ds2 = new Sequence("", "GHIJ");
691 seq2.setDatasetSequence(ds2);
692 SequenceI seq3 = new Sequence("", "M-NO--PQ");
693 SequenceI ds3 = new Sequence("", "MNOPQ");
694 seq3.setDatasetSequence(ds3);
695 SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
696 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
700 * Second edit deletes what is now columns 4 and 5.
702 seq1 = new Sequence("", "-ABCDEF");
703 seq1.setDatasetSequence(ds1);
704 seq2 = new Sequence("", "G-HIJ");
705 seq2.setDatasetSequence(ds2);
706 seq3 = new Sequence("", "M-NOPQ");
707 seq3.setDatasetSequence(ds3);
708 sqs = new SequenceI[] { seq1, seq2, seq3 };
709 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
712 Map<SequenceI, SequenceI> unwound = command.priorState(false);
713 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
714 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
715 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
716 assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
717 assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
718 assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
722 * Test a cut action's relocation of sequence features
724 @Test(groups = { "Functional" })
725 public void testCut_withFeatures()
728 * create sequence features before, after and overlapping
729 * a cut of columns/residues 4-7
731 SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
732 seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
734 seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
736 seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
738 seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
740 seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
744 * add some contact features
746 SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
748 seq0.addSequenceFeature(internalContact); // should get deleted
749 SequenceFeature overlapLeftContact = new SequenceFeature(
750 "disulphide bond", "", 2, 6, 0f, null);
751 seq0.addSequenceFeature(overlapLeftContact); // should get deleted
752 SequenceFeature overlapRightContact = new SequenceFeature(
753 "disulphide bond", "", 5, 8, 0f, null);
754 seq0.addSequenceFeature(overlapRightContact); // should get deleted
755 SequenceFeature spanningContact = new SequenceFeature(
756 "disulphide bond", "", 2, 9, 0f, null);
757 seq0.addSequenceFeature(spanningContact); // should get shortened 3'
760 * cut columns 3-6 (base 0), residues d-g 4-7
762 Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
763 EditCommand.cut(ec, new AlignmentI[] { al });
765 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
766 SequenceFeatures.sortFeatures(sfs, true);
768 assertEquals(5, sfs.size()); // features internal to cut were deleted
769 SequenceFeature sf = sfs.get(0);
770 assertEquals("before", sf.getType());
771 assertEquals(1, sf.getBegin());
772 assertEquals(3, sf.getEnd());
774 assertEquals("disulphide bond", sf.getType());
775 assertEquals(2, sf.getBegin());
776 assertEquals(5, sf.getEnd()); // truncated by cut
778 assertEquals("overlap left", sf.getType());
779 assertEquals(2, sf.getBegin());
780 assertEquals(3, sf.getEnd()); // truncated by cut
782 assertEquals("after", sf.getType());
783 assertEquals(4, sf.getBegin()); // shifted left by cut
784 assertEquals(6, sf.getEnd()); // shifted left by cut
786 assertEquals("overlap right", sf.getType());
787 assertEquals(4, sf.getBegin()); // shifted left by cut
788 assertEquals(4, sf.getEnd()); // truncated by cut
792 * Test a cut action's relocation of sequence features, with full coverage of
793 * all possible feature and cut locations for a 5-position ungapped sequence
795 @Test(groups = { "Functional" })
796 public void testCut_withFeatures_exhaustive()
799 * create a sequence features on each subrange of 1-5
801 SequenceI seq0 = new Sequence("seq", "ABCDE");
804 seq0.setStart(start);
806 AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
807 alignment.setDataset(null);
810 * create a new alignment with shared dataset sequence
812 AlignmentI copy = new Alignment(
814 { alignment.getDataset().getSequenceAt(0).deriveSequence() });
815 SequenceI copySeq0 = copy.getSequenceAt(0);
817 for (int from = start; from <= end; from++)
819 for (int to = from; to <= end; to++)
821 String desc = String.format("%d-%d", from, to);
822 SequenceFeature sf = new SequenceFeature("test", desc, from, to,
824 sf.setValue("from", Integer.valueOf(from));
825 sf.setValue("to", Integer.valueOf(to));
826 seq0.addSequenceFeature(sf);
830 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
831 assertEquals(func(5), sfs.size());
832 assertEquals(sfs, copySeq0.getSequenceFeatures());
833 String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
836 * now perform all possible cuts of subranges of columns 1-5
837 * and validate the resulting remaining sequence features!
839 SequenceI[] sqs = new SequenceI[] { seq0 };
840 boolean checkDsSize = true;
842 for (int from = 0; from < seq0.getLength(); from++)
844 for (int to = from; to < seq0.getLength(); to++)
846 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
847 - from + 1), alignment);
848 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
849 boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
850 .getDatasetSequence();
852 verifyCut(seq0, from, to, msg, start);
855 * verify copy alignment dataset sequence unaffected
857 assertEquals("Original dataset sequence was modified",
858 copySequenceFeatures,
859 copySeq0.getSequenceFeatures().toString());
864 * verify a new dataset sequence has appeared
866 assertEquals("Wrong Dataset size after cut",
867 newDatasetSequence ? 2 : 1, alignment.getDataset()
871 * undo and verify all restored
873 AlignmentI[] views = new AlignmentI[] { alignment };
874 ec.undoCommand(views);
875 sfs = seq0.getSequenceFeatures();
876 assertEquals("After undo of " + msg, func(5), sfs.size());
877 verifyUndo(from, to, sfs);
880 * verify copy alignment dataset sequence still unaffected
882 assertEquals("Original dataset sequence was modified",
883 copySequenceFeatures,
884 copySeq0.getSequenceFeatures().toString());
889 * verify dataset sequence has shrunk
891 assertEquals("Wrong Dataset size after cut", 1,
892 alignment.getDataset().getHeight());
899 verifyCut(seq0, from, to, msg, start);
902 * verify copy alignment dataset sequence unaffected
904 assertEquals("Original dataset sequence was modified",
905 copySequenceFeatures,
906 copySeq0.getSequenceFeatures().toString());
911 * verify a new dataset sequence has appeared again
913 assertEquals("Wrong Dataset size after cut",
914 newDatasetSequence ? 2 : 1, alignment.getDataset()
919 * undo ready for next cut
921 ec.undoCommand(views);
924 * final verify that copy alignment dataset sequence is still unaffected
926 assertEquals("Original dataset sequence was modified",
927 copySequenceFeatures,
928 copySeq0.getSequenceFeatures().toString());
932 * and that dataset sequence has shrunk
934 assertEquals("Wrong Dataset size after cut", 1,
935 alignment.getDataset().getHeight());
942 * Verify by inspection that the sequence features left on the sequence after
943 * a cut match the expected results. The trick to this is that we can parse
944 * each feature's original start-end positions from its description.
952 protected void verifyCut(SequenceI seq0, int from, int to,
953 final String msg, int seqStart)
955 List<SequenceFeature> sfs;
956 sfs = seq0.getSequenceFeatures();
958 Collections.sort(sfs, BY_DESCRIPTION);
961 * confirm the number of features has reduced by the
962 * number of features within the cut region i.e. by
963 * func(length of cut); exception is a cut at start or end of sequence,
964 * which retains the original coordinates, dataset sequence
965 * and all its features
967 boolean datasetRetained = from == 0 || to == 4;
970 // dataset and all features retained
971 assertEquals(msg, func(5), sfs.size());
973 else if (to - from == 4)
975 // all columns were cut
976 assertTrue(sfs.isEmpty());
980 // failure in checkFeatureRelocation is more informative!
981 assertEquals(msg + "wrong number of features left", func(5)
982 - func(to - from + 1), sfs.size());
986 * inspect individual features
988 for (SequenceFeature sf : sfs)
990 verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
996 * Check that after Undo, every feature has start/end that match its original
997 * "start" and "end" properties
1003 protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
1005 for (SequenceFeature sf : sfs)
1007 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1008 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1009 String msg = String.format(
1010 "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
1012 assertEquals(msg + "start", oldFrom, sf.getBegin());
1013 assertEquals(msg + "end", oldTo, sf.getEnd());
1018 * Helper method to check a feature has been correctly relocated after a cut
1022 * start of cut (first residue cut 1..)
1024 * end of cut (last residue cut 1..)
1028 private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1029 boolean newDataset, int seqStart)
1031 // TODO handle the gapped sequence case as well
1032 int cutSize = to - from + 1;
1033 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1034 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1035 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1036 final int oldToPosition = oldTo - seqStart + 1; // 1..
1038 String msg = String.format(
1039 "Feature %s relocated to %d-%d after cut of %d-%d",
1040 sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1043 // dataset retained with all features unchanged
1044 assertEquals("0: " + msg, oldFrom, sf.getBegin());
1045 assertEquals("0: " + msg, oldTo, sf.getEnd());
1047 else if (oldToPosition < from)
1049 // before cut region so unchanged
1050 assertEquals("1: " + msg, oldFrom, sf.getBegin());
1051 assertEquals("2: " + msg, oldTo, sf.getEnd());
1053 else if (oldFromPosition > to)
1055 // follows cut region - shift by size of cut
1056 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1058 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1061 else if (oldFromPosition < from && oldToPosition > to)
1063 // feature encloses cut region - shrink it right
1064 assertEquals("5: " + msg, oldFrom, sf.getBegin());
1065 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1067 else if (oldFromPosition < from)
1069 // feature overlaps left side of cut region - truncated right
1070 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1072 else if (oldToPosition > to)
1074 // feature overlaps right side of cut region - truncated left
1075 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1077 assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1082 // feature internal to cut - should have been deleted!
1083 Assert.fail(msg + " - should have been deleted");
1088 * Test a cut action's relocation of sequence features
1090 @Test(groups = { "Functional" })
1091 public void testCut_withFeatures5prime()
1093 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1094 seq0.createDatasetSequence();
1095 assertEquals(8, seq0.getStart());
1096 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
1098 SequenceI[] seqsArray = new SequenceI[] { seq0 };
1099 AlignmentI alignment = new Alignment(seqsArray);
1102 * cut columns of A-B; same dataset sequence is retained, aligned sequence
1105 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1106 EditCommand.cut(ec, new AlignmentI[] { alignment });
1109 * feature on CC(10-11) should still be on CC(10-11)
1111 assertSame(seq0, alignment.getSequenceAt(0));
1112 assertEquals(10, seq0.getStart());
1113 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1114 assertEquals(1, sfs.size());
1115 SequenceFeature sf = sfs.get(0);
1116 assertEquals(10, sf.getBegin());
1117 assertEquals(11, sf.getEnd());