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 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());
307 * Test that the addEdit command correctly merges insert gap commands when
310 @Test(groups = { "Functional" })
311 public void testAddEdit_multipleInsertGap()
314 * 3 insert gap in a row (aka mouse drag right):
316 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
317 new SequenceI[] { seqs[0] }, 1, 1, al);
319 SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
320 edited.setDatasetSequence(seqs[0].getDatasetSequence());
321 e = new EditCommand().new Edit(Action.INSERT_GAP,
322 new SequenceI[] { edited }, 2, 1, al);
324 edited = new Sequence("seq0", "a??bcdefghjk");
325 edited.setDatasetSequence(seqs[0].getDatasetSequence());
326 e = new EditCommand().new Edit(Action.INSERT_GAP,
327 new SequenceI[] { edited }, 3, 1, al);
329 assertEquals(1, testee.getSize());
330 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
331 assertEquals(1, testee.getEdit(0).getPosition());
332 assertEquals(3, testee.getEdit(0).getNumber());
335 * Add a non-contiguous edit - should not be merged.
337 e = new EditCommand().new Edit(Action.INSERT_GAP,
338 new SequenceI[] { edited }, 5, 2, al);
340 assertEquals(2, testee.getSize());
341 assertEquals(5, testee.getEdit(1).getPosition());
342 assertEquals(2, testee.getEdit(1).getNumber());
345 * Add a Delete after the Insert - should not be merged.
347 e = new EditCommand().new Edit(Action.DELETE_GAP,
348 new SequenceI[] { edited }, 6, 2, al);
350 assertEquals(3, testee.getSize());
351 assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
352 assertEquals(6, testee.getEdit(2).getPosition());
353 assertEquals(2, testee.getEdit(2).getNumber());
357 * Test that the addEdit command correctly merges delete gap commands when
360 @Test(groups = { "Functional" })
361 public void testAddEdit_multipleDeleteGap()
364 * 3 delete gap in a row (aka mouse drag left):
366 seqs[0].setSequence("a???bcdefghjk");
367 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
368 new SequenceI[] { seqs[0] }, 4, 1, al);
370 assertEquals(1, testee.getSize());
372 SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
373 edited.setDatasetSequence(seqs[0].getDatasetSequence());
374 e = new EditCommand().new Edit(Action.DELETE_GAP,
375 new SequenceI[] { edited }, 3, 1, al);
377 assertEquals(1, testee.getSize());
379 edited = new Sequence("seq0", "a?bcdefghjk");
380 edited.setDatasetSequence(seqs[0].getDatasetSequence());
381 e = new EditCommand().new Edit(Action.DELETE_GAP,
382 new SequenceI[] { edited }, 2, 1, al);
384 assertEquals(1, testee.getSize());
385 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
386 assertEquals(2, testee.getEdit(0).getPosition());
387 assertEquals(3, testee.getEdit(0).getNumber());
390 * Add a non-contiguous edit - should not be merged.
392 e = new EditCommand().new Edit(Action.DELETE_GAP,
393 new SequenceI[] { edited }, 2, 1, al);
395 assertEquals(2, testee.getSize());
396 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
397 assertEquals(2, testee.getEdit(1).getPosition());
398 assertEquals(1, testee.getEdit(1).getNumber());
401 * Add an Insert after the Delete - should not be merged.
403 e = new EditCommand().new Edit(Action.INSERT_GAP,
404 new SequenceI[] { edited }, 1, 1, al);
406 assertEquals(3, testee.getSize());
407 assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
408 assertEquals(1, testee.getEdit(2).getPosition());
409 assertEquals(1, testee.getEdit(2).getNumber());
413 * Test that the addEdit command correctly handles 'remove gaps' edits for the
414 * case when they appear contiguous but are acting on different sequences.
415 * They should not be merged.
417 @Test(groups = { "Functional" })
418 public void testAddEdit_removeAllGaps()
420 seqs[0].setSequence("a???bcdefghjk");
421 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
422 new SequenceI[] { seqs[0] }, 4, 1, al);
425 seqs[1].setSequence("f??ghjklmnopq");
426 Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[]
427 { seqs[1] }, 3, 1, al);
429 assertEquals(2, testee.getSize());
430 assertSame(e, testee.getEdit(0));
431 assertSame(e2, testee.getEdit(1));
435 * Test that the addEdit command correctly merges insert gap commands acting
436 * on a multi-sequence selection.
438 @Test(groups = { "Functional" })
439 public void testAddEdit_groupInsertGaps()
442 * 2 insert gap in a row (aka mouse drag right), on two sequences:
444 Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
445 seqs[0], seqs[1] }, 1, 1, al);
447 SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
448 seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
449 SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
450 seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
451 e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] {
452 seq1edited, seq2edited }, 2, 1, al);
455 assertEquals(1, testee.getSize());
456 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
457 assertEquals(1, testee.getEdit(0).getPosition());
458 assertEquals(2, testee.getEdit(0).getNumber());
459 assertEquals(seqs[0].getDatasetSequence(), testee.getEdit(0)
460 .getSequences()[0].getDatasetSequence());
461 assertEquals(seqs[1].getDatasetSequence(), testee.getEdit(0)
462 .getSequences()[1].getDatasetSequence());
466 * Test for 'undoing' a series of gap insertions.
468 * <li>Start: ABCDEF insert 2 at pos 1</li>
469 * <li>next: A--BCDEF insert 1 at pos 4</li>
470 * <li>next: A--B-CDEF insert 2 at pos 0</li>
471 * <li>last: --A--B-CDEF</li>
474 @Test(groups = { "Functional" })
475 public void testPriorState_multipleInserts()
477 EditCommand command = new EditCommand();
478 SequenceI seq = new Sequence("", "--A--B-CDEF");
479 SequenceI ds = new Sequence("", "ABCDEF");
480 seq.setDatasetSequence(ds);
481 SequenceI[] sqs = new SequenceI[] { seq };
482 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
484 e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
486 e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
489 Map<SequenceI, SequenceI> unwound = command.priorState(false);
490 assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
494 * Test for 'undoing' a series of gap deletions.
496 * <li>Start: A-B-C delete 1 at pos 1</li>
497 * <li>Next: AB-C delete 1 at pos 2</li>
501 @Test(groups = { "Functional" })
502 public void testPriorState_removeAllGaps()
504 EditCommand command = new EditCommand();
505 SequenceI seq = new Sequence("", "ABC");
506 SequenceI ds = new Sequence("", "ABC");
507 seq.setDatasetSequence(ds);
508 SequenceI[] sqs = new SequenceI[] { seq };
509 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
511 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
514 Map<SequenceI, SequenceI> unwound = command.priorState(false);
515 assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
519 * Test for 'undoing' a single delete edit.
521 @Test(groups = { "Functional" })
522 public void testPriorState_singleDelete()
524 EditCommand command = new EditCommand();
525 SequenceI seq = new Sequence("", "ABCDEF");
526 SequenceI ds = new Sequence("", "ABCDEF");
527 seq.setDatasetSequence(ds);
528 SequenceI[] sqs = new SequenceI[] { seq };
529 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
532 Map<SequenceI, SequenceI> unwound = command.priorState(false);
533 assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
537 * Test 'undoing' a single gap insertion edit command.
539 @Test(groups = { "Functional" })
540 public void testPriorState_singleInsert()
542 EditCommand command = new EditCommand();
543 SequenceI seq = new Sequence("", "AB---CDEF");
544 SequenceI ds = new Sequence("", "ABCDEF");
545 seq.setDatasetSequence(ds);
546 SequenceI[] sqs = new SequenceI[] { seq };
547 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
550 Map<SequenceI, SequenceI> unwound = command.priorState(false);
551 SequenceI prior = unwound.get(ds);
552 assertEquals("ABCDEF", prior.getSequenceAsString());
553 assertEquals(1, prior.getStart());
554 assertEquals(6, prior.getEnd());
558 * Test 'undoing' a single gap insertion edit command, on a sequence whose
559 * start residue is other than 1
561 @Test(groups = { "Functional" })
562 public void testPriorState_singleInsertWithOffset()
564 EditCommand command = new EditCommand();
565 SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
566 // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
567 // seq.setDatasetSequence(ds);
568 seq.createDatasetSequence();
569 SequenceI[] sqs = new SequenceI[] { seq };
570 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
573 Map<SequenceI, SequenceI> unwound = command.priorState(false);
574 SequenceI prior = unwound.get(seq.getDatasetSequence());
575 assertEquals("ABCDEF", prior.getSequenceAsString());
576 assertEquals(8, prior.getStart());
577 assertEquals(13, prior.getEnd());
581 * Test that mimics 'remove all gaps' action. This generates delete gap edits
582 * for contiguous gaps in each sequence separately.
584 @Test(groups = { "Functional" })
585 public void testPriorState_removeGapsMultipleSeqs()
587 EditCommand command = new EditCommand();
588 String original1 = "--ABC-DEF";
589 String original2 = "FG-HI--J";
590 String original3 = "M-NOPQ";
593 * Two edits for the first sequence
595 SequenceI seq = new Sequence("", "ABC-DEF");
596 SequenceI ds1 = new Sequence("", "ABCDEF");
597 seq.setDatasetSequence(ds1);
598 SequenceI[] sqs = new SequenceI[] { seq };
599 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
601 seq = new Sequence("", "ABCDEF");
602 seq.setDatasetSequence(ds1);
603 sqs = new SequenceI[] { seq };
604 e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
608 * Two edits for the second sequence
610 seq = new Sequence("", "FGHI--J");
611 SequenceI ds2 = new Sequence("", "FGHIJ");
612 seq.setDatasetSequence(ds2);
613 sqs = new SequenceI[] { seq };
614 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
616 seq = new Sequence("", "FGHIJ");
617 seq.setDatasetSequence(ds2);
618 sqs = new SequenceI[] { seq };
619 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
623 * One edit for the third sequence.
625 seq = new Sequence("", "MNOPQ");
626 SequenceI ds3 = new Sequence("", "MNOPQ");
627 seq.setDatasetSequence(ds3);
628 sqs = new SequenceI[] { seq };
629 e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
632 Map<SequenceI, SequenceI> unwound = command.priorState(false);
633 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
634 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
635 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
639 * Test that mimics 'remove all gapped columns' action. This generates a
640 * series Delete Gap edits that each act on all sequences that share a gapped
643 @Test(groups = { "Functional" })
644 public void testPriorState_removeGappedCols()
646 EditCommand command = new EditCommand();
647 String original1 = "--ABC--DEF";
648 String original2 = "-G-HI--J";
649 String original3 = "-M-NO--PQ";
652 * First edit deletes the first column.
654 SequenceI seq1 = new Sequence("", "-ABC--DEF");
655 SequenceI ds1 = new Sequence("", "ABCDEF");
656 seq1.setDatasetSequence(ds1);
657 SequenceI seq2 = new Sequence("", "G-HI--J");
658 SequenceI ds2 = new Sequence("", "GHIJ");
659 seq2.setDatasetSequence(ds2);
660 SequenceI seq3 = new Sequence("", "M-NO--PQ");
661 SequenceI ds3 = new Sequence("", "MNOPQ");
662 seq3.setDatasetSequence(ds3);
663 SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
664 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
668 * Second edit deletes what is now columns 4 and 5.
670 seq1 = new Sequence("", "-ABCDEF");
671 seq1.setDatasetSequence(ds1);
672 seq2 = new Sequence("", "G-HIJ");
673 seq2.setDatasetSequence(ds2);
674 seq3 = new Sequence("", "M-NOPQ");
675 seq3.setDatasetSequence(ds3);
676 sqs = new SequenceI[] { seq1, seq2, seq3 };
677 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
680 Map<SequenceI, SequenceI> unwound = command.priorState(false);
681 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
682 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
683 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
684 assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
685 assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
686 assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
690 * Test a cut action's relocation of sequence features
692 @Test(groups = { "Functional" })
693 public void testCut_withFeatures()
696 * create sequence features before, after and overlapping
697 * a cut of columns/residues 4-7
699 SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
700 seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
702 seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
704 seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
706 seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
708 seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
712 * add some contact features
714 SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
716 seq0.addSequenceFeature(internalContact); // should get deleted
717 SequenceFeature overlapLeftContact = new SequenceFeature(
718 "disulphide bond", "", 2, 6, 0f, null);
719 seq0.addSequenceFeature(overlapLeftContact); // should get deleted
720 SequenceFeature overlapRightContact = new SequenceFeature(
721 "disulphide bond", "", 5, 8, 0f, null);
722 seq0.addSequenceFeature(overlapRightContact); // should get deleted
723 SequenceFeature spanningContact = new SequenceFeature(
724 "disulphide bond", "", 2, 9, 0f, null);
725 seq0.addSequenceFeature(spanningContact); // should get shortened 3'
728 * cut columns 3-6 (base 0), residues d-g 4-7
730 Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
731 EditCommand.cut(ec, new AlignmentI[] { al });
733 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
734 SequenceFeatures.sortFeatures(sfs, true);
736 assertEquals(5, sfs.size()); // features internal to cut were deleted
737 SequenceFeature sf = sfs.get(0);
738 assertEquals("before", sf.getType());
739 assertEquals(1, sf.getBegin());
740 assertEquals(3, sf.getEnd());
742 assertEquals("disulphide bond", sf.getType());
743 assertEquals(2, sf.getBegin());
744 assertEquals(5, sf.getEnd()); // truncated by cut
746 assertEquals("overlap left", sf.getType());
747 assertEquals(2, sf.getBegin());
748 assertEquals(3, sf.getEnd()); // truncated by cut
750 assertEquals("after", sf.getType());
751 assertEquals(4, sf.getBegin()); // shifted left by cut
752 assertEquals(6, sf.getEnd()); // shifted left by cut
754 assertEquals("overlap right", sf.getType());
755 assertEquals(4, sf.getBegin()); // shifted left by cut
756 assertEquals(4, sf.getEnd()); // truncated by cut
760 * Test a cut action's relocation of sequence features, with full coverage of
761 * all possible feature and cut locations for a 5-position ungapped sequence
763 @Test(groups = { "Functional" })
764 public void testCut_withFeatures_exhaustive()
767 * create a sequence features on each subrange of 1-5
769 SequenceI seq0 = new Sequence("seq", "ABCDE");
772 seq0.setStart(start);
774 AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
775 alignment.setDataset(null);
777 * create a new alignment with shared dataset sequence
779 AlignmentI copy = new Alignment(
781 { alignment.getDataset().getSequenceAt(0).deriveSequence() });
782 SequenceI copySeq0 = copy.getSequenceAt(0);
784 for (int from = start; from <= end; from++)
786 for (int to = from; to <= end; to++)
788 String desc = String.format("%d-%d", from, to);
789 SequenceFeature sf = new SequenceFeature("test", desc, from, to,
791 sf.setValue("from", Integer.valueOf(from));
792 sf.setValue("to", Integer.valueOf(to));
793 seq0.addSequenceFeature(sf);
797 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
798 assertEquals(func(5), sfs.size());
799 assertEquals(sfs, copySeq0.getSequenceFeatures());
800 String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
802 * now perform all possible cuts of subranges of columns 1-5
803 * and validate the resulting remaining sequence features!
805 SequenceI[] sqs = new SequenceI[] { seq0 };
806 boolean checkDsSize = false;
808 for (int from = 0; from < seq0.getLength(); from++)
810 for (int to = from; to < seq0.getLength(); to++)
812 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
813 - from + 1), alignment);
814 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
816 verifyCut(seq0, from, to, msg, start);
819 * verify copy alignment dataset sequence unaffected
821 assertEquals("Original dataset sequence was modified",
822 copySequenceFeatures,
823 copySeq0.getSequenceFeatures().toString());
827 * verify a new dataset sequence has appeared
829 assertEquals("Wrong Dataset size after cut",
830 copySeq0.getDatasetSequence() == seq0.getDatasetSequence()
833 alignment.getDataset().getHeight());
836 * undo and verify all restored
838 AlignmentI[] views = new AlignmentI[] { alignment };
839 ec.undoCommand(views);
840 sfs = seq0.getSequenceFeatures();
841 assertEquals("After undo of " + msg, func(5), sfs.size());
842 verifyUndo(from, to, sfs);
845 * verify copy alignment dataset sequence still unaffected
847 assertEquals("Original dataset sequence was modified",
848 copySequenceFeatures,
849 copySeq0.getSequenceFeatures().toString());
854 * verify dataset sequence has shrunk
856 assertEquals("Wrong Dataset size after cut",
857 copySeq0.getDatasetSequence() == seq0.getDatasetSequence()
860 alignment.getDataset().getHeight());
866 verifyCut(seq0, from, to, msg, start);
869 * verify copy alignment dataset sequence unaffected
871 assertEquals("Original dataset sequence was modified",
872 copySequenceFeatures,
873 copySeq0.getSequenceFeatures().toString());
878 * verify a new dataset sequence has appeared again
880 assertEquals("Wrong Dataset size after cut",
881 copySeq0.getDatasetSequence() == seq0.getDatasetSequence()
884 alignment.getDataset().getHeight());
887 * undo ready for next cut
889 ec.undoCommand(views);
892 * final verify that copy alignment dataset sequence is still unaffected
894 assertEquals("Original dataset sequence was modified",
895 copySequenceFeatures,
896 copySeq0.getSequenceFeatures().toString());
900 * and that dataset sequence has shrunk
902 assertEquals("Wrong Dataset size after cut",
903 copySeq0.getDatasetSequence() == seq0.getDatasetSequence()
906 alignment.getDataset().getHeight());
913 * Verify by inspection that the sequence features left on the sequence after
914 * a cut match the expected results. The trick to this is that we can parse
915 * each feature's original start-end positions from its description.
923 protected void verifyCut(SequenceI seq0, int from, int to,
924 final String msg, int seqStart)
926 List<SequenceFeature> sfs;
927 sfs = seq0.getSequenceFeatures();
929 Collections.sort(sfs, BY_DESCRIPTION);
932 * confirm the number of features has reduced by the
933 * number of features within the cut region i.e. by
934 * func(length of cut); exception is a cut at start or end of sequence,
935 * which retains the original coordinates, dataset sequence
936 * and all its features
938 boolean datasetRetained = from == 0 || to == 4;
941 // dataset and all features retained
942 assertEquals(msg, func(5), sfs.size());
944 else if (to - from == 4)
946 // all columns were cut
947 assertTrue(sfs.isEmpty());
951 // failure in checkFeatureRelocation is more informative!
952 assertEquals(msg + "wrong number of features left", func(5)
953 - func(to - from + 1), sfs.size());
957 * inspect individual features
959 for (SequenceFeature sf : sfs)
961 verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
967 * Check that after Undo, every feature has start/end that match its original
968 * "start" and "end" properties
974 protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
976 for (SequenceFeature sf : sfs)
978 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
979 final int oldTo = ((Integer) sf.getValue("to")).intValue();
980 String msg = String.format(
981 "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
983 assertEquals(msg + "start", oldFrom, sf.getBegin());
984 assertEquals(msg + "end", oldTo, sf.getEnd());
989 * Helper method to check a feature has been correctly relocated after a cut
993 * start of cut (first residue cut 1..)
995 * end of cut (last residue cut 1..)
999 private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1000 boolean newDataset, int seqStart)
1002 // TODO handle the gapped sequence case as well
1003 int cutSize = to - from + 1;
1004 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1005 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1006 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1007 final int oldToPosition = oldTo - seqStart + 1; // 1..
1009 String msg = String.format(
1010 "Feature %s relocated to %d-%d after cut of %d-%d",
1011 sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1014 // dataset retained with all features unchanged
1015 assertEquals("0: " + msg, oldFrom, sf.getBegin());
1016 assertEquals("0: " + msg, oldTo, sf.getEnd());
1018 else if (oldToPosition < from)
1020 // before cut region so unchanged
1021 assertEquals("1: " + msg, oldFrom, sf.getBegin());
1022 assertEquals("2: " + msg, oldTo, sf.getEnd());
1024 else if (oldFromPosition > to)
1026 // follows cut region - shift by size of cut
1027 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1029 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1032 else if (oldFromPosition < from && oldToPosition > to)
1034 // feature encloses cut region - shrink it right
1035 assertEquals("5: " + msg, oldFrom, sf.getBegin());
1036 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1038 else if (oldFromPosition < from)
1040 // feature overlaps left side of cut region - truncated right
1041 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1043 else if (oldToPosition > to)
1045 // feature overlaps right side of cut region - truncated left
1046 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1048 assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1053 // feature internal to cut - should have been deleted!
1054 Assert.fail(msg + " - should have been deleted");
1059 * Test a cut action's relocation of sequence features
1061 @Test(groups = { "Functional" })
1062 public void testCut_withFeatures5prime()
1064 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1065 seq0.createDatasetSequence();
1066 assertEquals(8, seq0.getStart());
1067 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
1069 SequenceI[] seqsArray = new SequenceI[] { seq0 };
1070 AlignmentI alignment = new Alignment(seqsArray);
1073 * cut columns of A-B; same dataset sequence is retained, aligned sequence
1076 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1077 EditCommand.cut(ec, new AlignmentI[] { alignment });
1080 * feature on CC(10-11) should still be on CC(10-11)
1082 assertSame(seq0, alignment.getSequenceAt(0));
1083 assertEquals(10, seq0.getStart());
1084 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1085 assertEquals(1, sfs.size());
1086 SequenceFeature sf = sfs.get(0);
1087 assertEquals(10, sf.getBegin());
1088 assertEquals(11, sf.getEnd());