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());
282 seqs[1] = new Sequence("seq1", "fghjZXYnopq");
286 * Test that the addEdit command correctly merges insert gap commands when
289 @Test(groups = { "Functional" })
290 public void testAddEdit_multipleInsertGap()
293 * 3 insert gap in a row (aka mouse drag right):
295 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
296 new SequenceI[] { seqs[0] }, 1, 1, al);
298 SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
299 edited.setDatasetSequence(seqs[0].getDatasetSequence());
300 e = new EditCommand().new Edit(Action.INSERT_GAP,
301 new SequenceI[] { edited }, 2, 1, al);
303 edited = new Sequence("seq0", "a??bcdefghjk");
304 edited.setDatasetSequence(seqs[0].getDatasetSequence());
305 e = new EditCommand().new Edit(Action.INSERT_GAP,
306 new SequenceI[] { edited }, 3, 1, al);
308 assertEquals(1, testee.getSize());
309 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
310 assertEquals(1, testee.getEdit(0).getPosition());
311 assertEquals(3, testee.getEdit(0).getNumber());
314 * Add a non-contiguous edit - should not be merged.
316 e = new EditCommand().new Edit(Action.INSERT_GAP,
317 new SequenceI[] { edited }, 5, 2, al);
319 assertEquals(2, testee.getSize());
320 assertEquals(5, testee.getEdit(1).getPosition());
321 assertEquals(2, testee.getEdit(1).getNumber());
324 * Add a Delete after the Insert - should not be merged.
326 e = new EditCommand().new Edit(Action.DELETE_GAP,
327 new SequenceI[] { edited }, 6, 2, al);
329 assertEquals(3, testee.getSize());
330 assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
331 assertEquals(6, testee.getEdit(2).getPosition());
332 assertEquals(2, testee.getEdit(2).getNumber());
336 * Test that the addEdit command correctly merges delete gap commands when
339 @Test(groups = { "Functional" })
340 public void testAddEdit_multipleDeleteGap()
343 * 3 delete gap in a row (aka mouse drag left):
345 seqs[0].setSequence("a???bcdefghjk");
346 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
347 new SequenceI[] { seqs[0] }, 4, 1, al);
349 assertEquals(1, testee.getSize());
351 SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
352 edited.setDatasetSequence(seqs[0].getDatasetSequence());
353 e = new EditCommand().new Edit(Action.DELETE_GAP,
354 new SequenceI[] { edited }, 3, 1, al);
356 assertEquals(1, testee.getSize());
358 edited = new Sequence("seq0", "a?bcdefghjk");
359 edited.setDatasetSequence(seqs[0].getDatasetSequence());
360 e = new EditCommand().new Edit(Action.DELETE_GAP,
361 new SequenceI[] { edited }, 2, 1, al);
363 assertEquals(1, testee.getSize());
364 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
365 assertEquals(2, testee.getEdit(0).getPosition());
366 assertEquals(3, testee.getEdit(0).getNumber());
369 * Add a non-contiguous edit - should not be merged.
371 e = new EditCommand().new Edit(Action.DELETE_GAP,
372 new SequenceI[] { edited }, 2, 1, al);
374 assertEquals(2, testee.getSize());
375 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
376 assertEquals(2, testee.getEdit(1).getPosition());
377 assertEquals(1, testee.getEdit(1).getNumber());
380 * Add an Insert after the Delete - should not be merged.
382 e = new EditCommand().new Edit(Action.INSERT_GAP,
383 new SequenceI[] { edited }, 1, 1, al);
385 assertEquals(3, testee.getSize());
386 assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
387 assertEquals(1, testee.getEdit(2).getPosition());
388 assertEquals(1, testee.getEdit(2).getNumber());
392 * Test that the addEdit command correctly handles 'remove gaps' edits for the
393 * case when they appear contiguous but are acting on different sequences.
394 * They should not be merged.
396 @Test(groups = { "Functional" })
397 public void testAddEdit_removeAllGaps()
399 seqs[0].setSequence("a???bcdefghjk");
400 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
401 new SequenceI[] { seqs[0] }, 4, 1, al);
404 seqs[1].setSequence("f??ghjklmnopq");
405 Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[]
406 { seqs[1] }, 3, 1, al);
408 assertEquals(2, testee.getSize());
409 assertSame(e, testee.getEdit(0));
410 assertSame(e2, testee.getEdit(1));
414 * Test that the addEdit command correctly merges insert gap commands acting
415 * on a multi-sequence selection.
417 @Test(groups = { "Functional" })
418 public void testAddEdit_groupInsertGaps()
421 * 2 insert gap in a row (aka mouse drag right), on two sequences:
423 Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
424 seqs[0], seqs[1] }, 1, 1, al);
426 SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
427 seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
428 SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
429 seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
430 e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
431 seq1edited, seq2edited }, 2, 1, al);
434 assertEquals(1, testee.getSize());
435 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
436 assertEquals(1, testee.getEdit(0).getPosition());
437 assertEquals(2, testee.getEdit(0).getNumber());
438 assertEquals(seqs[0].getDatasetSequence(), testee.getEdit(0)
439 .getSequences()[0].getDatasetSequence());
440 assertEquals(seqs[1].getDatasetSequence(), testee.getEdit(0)
441 .getSequences()[1].getDatasetSequence());
445 * Test for 'undoing' a series of gap insertions.
447 * <li>Start: ABCDEF insert 2 at pos 1</li>
448 * <li>next: A--BCDEF insert 1 at pos 4</li>
449 * <li>next: A--B-CDEF insert 2 at pos 0</li>
450 * <li>last: --A--B-CDEF</li>
453 @Test(groups = { "Functional" })
454 public void testPriorState_multipleInserts()
456 EditCommand command = new EditCommand();
457 SequenceI seq = new Sequence("", "--A--B-CDEF");
458 SequenceI ds = new Sequence("", "ABCDEF");
459 seq.setDatasetSequence(ds);
460 SequenceI[] sqs = new SequenceI[] { seq };
461 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
463 e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
465 e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
468 Map<SequenceI, SequenceI> unwound = command.priorState(false);
469 assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
473 * Test for 'undoing' a series of gap deletions.
475 * <li>Start: A-B-C delete 1 at pos 1</li>
476 * <li>Next: AB-C delete 1 at pos 2</li>
480 @Test(groups = { "Functional" })
481 public void testPriorState_removeAllGaps()
483 EditCommand command = new EditCommand();
484 SequenceI seq = new Sequence("", "ABC");
485 SequenceI ds = new Sequence("", "ABC");
486 seq.setDatasetSequence(ds);
487 SequenceI[] sqs = new SequenceI[] { seq };
488 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
490 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
493 Map<SequenceI, SequenceI> unwound = command.priorState(false);
494 assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
498 * Test for 'undoing' a single delete edit.
500 @Test(groups = { "Functional" })
501 public void testPriorState_singleDelete()
503 EditCommand command = new EditCommand();
504 SequenceI seq = new Sequence("", "ABCDEF");
505 SequenceI ds = new Sequence("", "ABCDEF");
506 seq.setDatasetSequence(ds);
507 SequenceI[] sqs = new SequenceI[] { seq };
508 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
511 Map<SequenceI, SequenceI> unwound = command.priorState(false);
512 assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
516 * Test 'undoing' a single gap insertion edit command.
518 @Test(groups = { "Functional" })
519 public void testPriorState_singleInsert()
521 EditCommand command = new EditCommand();
522 SequenceI seq = new Sequence("", "AB---CDEF");
523 SequenceI ds = new Sequence("", "ABCDEF");
524 seq.setDatasetSequence(ds);
525 SequenceI[] sqs = new SequenceI[] { seq };
526 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
529 Map<SequenceI, SequenceI> unwound = command.priorState(false);
530 SequenceI prior = unwound.get(ds);
531 assertEquals("ABCDEF", prior.getSequenceAsString());
532 assertEquals(1, prior.getStart());
533 assertEquals(6, prior.getEnd());
537 * Test 'undoing' a single gap insertion edit command, on a sequence whose
538 * start residue is other than 1
540 @Test(groups = { "Functional" })
541 public void testPriorState_singleInsertWithOffset()
543 EditCommand command = new EditCommand();
544 SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
545 // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
546 // seq.setDatasetSequence(ds);
547 seq.createDatasetSequence();
548 SequenceI[] sqs = new SequenceI[] { seq };
549 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
552 Map<SequenceI, SequenceI> unwound = command.priorState(false);
553 SequenceI prior = unwound.get(seq.getDatasetSequence());
554 assertEquals("ABCDEF", prior.getSequenceAsString());
555 assertEquals(8, prior.getStart());
556 assertEquals(13, prior.getEnd());
560 * Test that mimics 'remove all gaps' action. This generates delete gap edits
561 * for contiguous gaps in each sequence separately.
563 @Test(groups = { "Functional" })
564 public void testPriorState_removeGapsMultipleSeqs()
566 EditCommand command = new EditCommand();
567 String original1 = "--ABC-DEF";
568 String original2 = "FG-HI--J";
569 String original3 = "M-NOPQ";
572 * Two edits for the first sequence
574 SequenceI seq = new Sequence("", "ABC-DEF");
575 SequenceI ds1 = new Sequence("", "ABCDEF");
576 seq.setDatasetSequence(ds1);
577 SequenceI[] sqs = new SequenceI[] { seq };
578 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
580 seq = new Sequence("", "ABCDEF");
581 seq.setDatasetSequence(ds1);
582 sqs = new SequenceI[] { seq };
583 e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
587 * Two edits for the second sequence
589 seq = new Sequence("", "FGHI--J");
590 SequenceI ds2 = new Sequence("", "FGHIJ");
591 seq.setDatasetSequence(ds2);
592 sqs = new SequenceI[] { seq };
593 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
595 seq = new Sequence("", "FGHIJ");
596 seq.setDatasetSequence(ds2);
597 sqs = new SequenceI[] { seq };
598 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
602 * One edit for the third sequence.
604 seq = new Sequence("", "MNOPQ");
605 SequenceI ds3 = new Sequence("", "MNOPQ");
606 seq.setDatasetSequence(ds3);
607 sqs = new SequenceI[] { seq };
608 e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
611 Map<SequenceI, SequenceI> unwound = command.priorState(false);
612 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
613 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
614 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
618 * Test that mimics 'remove all gapped columns' action. This generates a
619 * series Delete Gap edits that each act on all sequences that share a gapped
622 @Test(groups = { "Functional" })
623 public void testPriorState_removeGappedCols()
625 EditCommand command = new EditCommand();
626 String original1 = "--ABC--DEF";
627 String original2 = "-G-HI--J";
628 String original3 = "-M-NO--PQ";
631 * First edit deletes the first column.
633 SequenceI seq1 = new Sequence("", "-ABC--DEF");
634 SequenceI ds1 = new Sequence("", "ABCDEF");
635 seq1.setDatasetSequence(ds1);
636 SequenceI seq2 = new Sequence("", "G-HI--J");
637 SequenceI ds2 = new Sequence("", "GHIJ");
638 seq2.setDatasetSequence(ds2);
639 SequenceI seq3 = new Sequence("", "M-NO--PQ");
640 SequenceI ds3 = new Sequence("", "MNOPQ");
641 seq3.setDatasetSequence(ds3);
642 SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
643 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
647 * Second edit deletes what is now columns 4 and 5.
649 seq1 = new Sequence("", "-ABCDEF");
650 seq1.setDatasetSequence(ds1);
651 seq2 = new Sequence("", "G-HIJ");
652 seq2.setDatasetSequence(ds2);
653 seq3 = new Sequence("", "M-NOPQ");
654 seq3.setDatasetSequence(ds3);
655 sqs = new SequenceI[] { seq1, seq2, seq3 };
656 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
659 Map<SequenceI, SequenceI> unwound = command.priorState(false);
660 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
661 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
662 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
663 assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
664 assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
665 assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
669 * Test a cut action's relocation of sequence features
671 @Test(groups = { "Functional" })
672 public void testCut_withFeatures()
675 * create sequence features before, after and overlapping
676 * a cut of columns/residues 4-7
678 SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
679 seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
681 seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
683 seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
685 seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
687 seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
691 * add some contact features
693 SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
695 seq0.addSequenceFeature(internalContact); // should get deleted
696 SequenceFeature overlapLeftContact = new SequenceFeature(
697 "disulphide bond", "", 2, 6, 0f, null);
698 seq0.addSequenceFeature(overlapLeftContact); // should get deleted
699 SequenceFeature overlapRightContact = new SequenceFeature(
700 "disulphide bond", "", 5, 8, 0f, null);
701 seq0.addSequenceFeature(overlapRightContact); // should get deleted
702 SequenceFeature spanningContact = new SequenceFeature(
703 "disulphide bond", "", 2, 9, 0f, null);
704 seq0.addSequenceFeature(spanningContact); // should get shortened 3'
707 * cut columns 3-6 (base 0), residues d-g 4-7
709 Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
710 EditCommand.cut(ec, new AlignmentI[] { al });
712 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
713 SequenceFeatures.sortFeatures(sfs, true);
715 assertEquals(5, sfs.size()); // features internal to cut were deleted
716 SequenceFeature sf = sfs.get(0);
717 assertEquals("before", sf.getType());
718 assertEquals(1, sf.getBegin());
719 assertEquals(3, sf.getEnd());
721 assertEquals("disulphide bond", sf.getType());
722 assertEquals(2, sf.getBegin());
723 assertEquals(5, sf.getEnd()); // truncated by cut
725 assertEquals("overlap left", sf.getType());
726 assertEquals(2, sf.getBegin());
727 assertEquals(3, sf.getEnd()); // truncated by cut
729 assertEquals("after", sf.getType());
730 assertEquals(4, sf.getBegin()); // shifted left by cut
731 assertEquals(6, sf.getEnd()); // shifted left by cut
733 assertEquals("overlap right", sf.getType());
734 assertEquals(4, sf.getBegin()); // shifted left by cut
735 assertEquals(4, sf.getEnd()); // truncated by cut
739 * Test a cut action's relocation of sequence features, with full coverage of
740 * all possible feature and cut locations for a 5-position ungapped sequence
742 @Test(groups = { "Functional" })
743 public void testCut_withFeatures_exhaustive()
746 * create a sequence features on each subrange of 1-5
748 SequenceI seq0 = new Sequence("seq", "ABCDE");
751 seq0.setStart(start);
753 AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
754 alignment.setDataset(null);
755 for (int from = start; from <= end; from++)
757 for (int to = from; to <= end; to++)
759 String desc = String.format("%d-%d", from, to);
760 SequenceFeature sf = new SequenceFeature("test", desc, from, to,
762 sf.setValue("from", Integer.valueOf(from));
763 sf.setValue("to", Integer.valueOf(to));
764 seq0.addSequenceFeature(sf);
768 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
769 assertEquals(func(5), sfs.size());
772 * now perform all possible cuts of subranges of columns 1-5
773 * and validate the resulting remaining sequence features!
775 SequenceI[] sqs = new SequenceI[] { seq0 };
777 for (int from = 0; from < seq0.getLength(); from++)
779 for (int to = from; to < seq0.getLength(); to++)
781 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
782 - from + 1), alignment);
783 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
785 verifyCut(seq0, from, to, msg, start);
788 * undo and verify all restored
790 AlignmentI[] views = new AlignmentI[] { alignment };
791 ec.undoCommand(views);
792 sfs = seq0.getSequenceFeatures();
793 assertEquals("After undo of " + msg, func(5), sfs.size());
794 verifyUndo(from, to, sfs);
800 verifyCut(seq0, from, to, msg, start);
803 * undo ready for next cut
805 ec.undoCommand(views);
811 * Verify by inspection that the sequence features left on the sequence after
812 * a cut match the expected results. The trick to this is that we can parse
813 * each feature's original start-end positions from its description.
821 protected void verifyCut(SequenceI seq0, int from, int to,
822 final String msg, int seqStart)
824 List<SequenceFeature> sfs;
825 sfs = seq0.getSequenceFeatures();
827 Collections.sort(sfs, BY_DESCRIPTION);
830 * confirm the number of features has reduced by the
831 * number of features within the cut region i.e. by
832 * func(length of cut); exception is a cut at start or end of sequence,
833 * which retains the original coordinates, dataset sequence
834 * and all its features
836 boolean datasetRetained = from == 0 || to == 4;
839 // dataset and all features retained
840 assertEquals(msg, func(5), sfs.size());
842 else if (to - from == 4)
844 // all columns were cut
845 assertTrue(sfs.isEmpty());
849 // failure in checkFeatureRelocation is more informative!
850 assertEquals(msg + "wrong number of features left", func(5)
851 - func(to - from + 1), sfs.size());
855 * inspect individual features
857 for (SequenceFeature sf : sfs)
859 verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
865 * Check that after Undo, every feature has start/end that match its original
866 * "start" and "end" properties
872 protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
874 for (SequenceFeature sf : sfs)
876 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
877 final int oldTo = ((Integer) sf.getValue("to")).intValue();
878 String msg = String.format(
879 "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
881 assertEquals(msg + "start", oldFrom, sf.getBegin());
882 assertEquals(msg + "end", oldTo, sf.getEnd());
887 * Helper method to check a feature has been correctly relocated after a cut
891 * start of cut (first residue cut 1..)
893 * end of cut (last residue cut 1..)
897 private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
898 boolean newDataset, int seqStart)
900 // TODO handle the gapped sequence case as well
901 int cutSize = to - from + 1;
902 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
903 final int oldTo = ((Integer) sf.getValue("to")).intValue();
904 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
905 final int oldToPosition = oldTo - seqStart + 1; // 1..
907 String msg = String.format(
908 "Feature %s relocated to %d-%d after cut of %d-%d",
909 sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
912 // dataset retained with all features unchanged
913 assertEquals("0: " + msg, oldFrom, sf.getBegin());
914 assertEquals("0: " + msg, oldTo, sf.getEnd());
916 else if (oldToPosition < from)
918 // before cut region so unchanged
919 assertEquals("1: " + msg, oldFrom, sf.getBegin());
920 assertEquals("2: " + msg, oldTo, sf.getEnd());
922 else if (oldFromPosition > to)
924 // follows cut region - shift by size of cut
925 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
927 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
930 else if (oldFromPosition < from && oldToPosition > to)
932 // feature encloses cut region - shrink it right
933 assertEquals("5: " + msg, oldFrom, sf.getBegin());
934 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
936 else if (oldFromPosition < from)
938 // feature overlaps left side of cut region - truncated right
939 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
941 else if (oldToPosition > to)
943 // feature overlaps right side of cut region - truncated left
944 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
946 assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
951 // feature internal to cut - should have been deleted!
952 Assert.fail(msg + " - should have been deleted");
957 * Test a cut action's relocation of sequence features
959 @Test(groups = { "Functional" })
960 public void testCut_withFeatures5prime()
962 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
963 seq0.createDatasetSequence();
964 assertEquals(8, seq0.getStart());
965 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
967 SequenceI[] seqsArray = new SequenceI[] { seq0 };
968 AlignmentI alignment = new Alignment(seqsArray);
971 * cut columns of A-B; same dataset sequence is retained, aligned sequence
974 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
975 EditCommand.cut(ec, new AlignmentI[] { alignment });
978 * feature on CC(10-11) should still be on CC(10-11)
980 assertSame(seq0, alignment.getSequenceAt(0));
981 assertEquals(10, seq0.getStart());
982 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
983 assertEquals(1, sfs.size());
984 SequenceFeature sf = sfs.get(0);
985 assertEquals(10, sf.getBegin());
986 assertEquals(11, sf.getEnd());