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 java.util.Locale;
25 import static org.testng.AssertJUnit.assertEquals;
26 import static org.testng.AssertJUnit.assertSame;
27 import static org.testng.AssertJUnit.assertTrue;
29 import jalview.commands.EditCommand.Action;
30 import jalview.commands.EditCommand.Edit;
31 import jalview.datamodel.Alignment;
32 import jalview.datamodel.AlignmentI;
33 import jalview.datamodel.Sequence;
34 import jalview.datamodel.SequenceFeature;
35 import jalview.datamodel.SequenceI;
36 import jalview.datamodel.features.SequenceFeatures;
37 import jalview.gui.JvOptionPane;
39 import java.nio.charset.Charset;
40 import java.util.Arrays;
41 import java.util.Collections;
42 import java.util.Comparator;
43 import java.util.List;
45 import java.util.spi.LocaleServiceProvider;
47 import org.testng.Assert;
48 import org.testng.annotations.BeforeClass;
49 import org.testng.annotations.BeforeMethod;
50 import org.testng.annotations.Test;
53 * Unit tests for EditCommand
58 public class EditCommandTest
60 private static Comparator<SequenceFeature> BY_DESCRIPTION = new Comparator<SequenceFeature>()
64 public int compare(SequenceFeature o1, SequenceFeature o2)
66 return o1.getDescription().compareTo(o2.getDescription());
70 private EditCommand testee;
72 private SequenceI[] seqs;
77 * compute n(n+1)/2 e.g.
78 * func(5) = 5 + 4 + 3 + 2 + 1 = 15
80 private static int func(int i)
82 return i * (i + 1) / 2;
85 @BeforeClass(alwaysRun = true)
86 public void setUpJvOptionPane()
88 JvOptionPane.setInteractiveMode(false);
89 JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
92 @BeforeMethod(alwaysRun = true)
95 testee = new EditCommand();
96 seqs = new SequenceI[4];
97 seqs[0] = new Sequence("seq0", "abcdefghjk");
98 seqs[0].setDatasetSequence(new Sequence("seq0ds", "ABCDEFGHJK"));
99 seqs[1] = new Sequence("seq1", "fghjklmnopq");
100 seqs[1].setDatasetSequence(new Sequence("seq1ds", "FGHJKLMNOPQ"));
101 seqs[2] = new Sequence("seq2", "qrstuvwxyz");
102 seqs[2].setDatasetSequence(new Sequence("seq2ds", "QRSTUVWXYZ"));
103 seqs[3] = new Sequence("seq3", "1234567890");
104 seqs[3].setDatasetSequence(new Sequence("seq3ds", "1234567890"));
105 al = new Alignment(seqs);
106 al.setGapCharacter('?');
110 * Test inserting gap characters
112 @Test(groups = { "Functional" })
113 public void testAppendEdit_insertGap()
115 // set a non-standard gap character to prove it is actually used
116 testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true);
117 assertEquals("abcd???efghjk", seqs[0].getSequenceAsString());
118 assertEquals("fghj???klmnopq", seqs[1].getSequenceAsString());
119 assertEquals("qrst???uvwxyz", seqs[2].getSequenceAsString());
120 assertEquals("1234???567890", seqs[3].getSequenceAsString());
122 // todo: test for handling out of range positions?
126 * Test deleting characters from sequences. Note the deleteGap() action does
127 * not check that only gap characters are being removed.
129 @Test(groups = { "Functional" })
130 public void testAppendEdit_deleteGap()
132 testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true);
133 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
134 assertEquals("fghjnopq", seqs[1].getSequenceAsString());
135 assertEquals("qrstxyz", seqs[2].getSequenceAsString());
136 assertEquals("1234890", seqs[3].getSequenceAsString());
140 * Test a cut action. The command should store the cut characters to support
143 @Test(groups = { "Functional" })
144 public void testCut()
146 Edit ec = testee.new Edit(Action.CUT, seqs, 4, 3, al);
147 EditCommand.cut(ec, new AlignmentI[] { al });
148 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
149 assertEquals("fghjnopq", seqs[1].getSequenceAsString());
150 assertEquals("qrstxyz", seqs[2].getSequenceAsString());
151 assertEquals("1234890", seqs[3].getSequenceAsString());
153 assertEquals("efg", new String(ec.string[0]));
154 assertEquals("klm", new String(ec.string[1]));
155 assertEquals("uvw", new String(ec.string[2]));
156 assertEquals("567", new String(ec.string[3]));
157 // TODO: case where whole sequence is deleted as nothing left; etc
161 * Test a Paste action, followed by Undo and Redo
163 @Test(groups = { "Functional" }, enabled = false)
164 public void testPaste_undo_redo()
166 // TODO code this test properly, bearing in mind that:
167 // Paste action requires something on the clipboard (Cut/Copy)
168 // - EditCommand.paste doesn't add sequences to the alignment
169 // ... that is done in AlignFrame.paste()
170 // ... unless as a Redo
173 SequenceI[] newSeqs = new SequenceI[2];
174 newSeqs[0] = new Sequence("newseq0", "ACEFKL");
175 newSeqs[1] = new Sequence("newseq1", "JWMPDH");
177 new EditCommand("Paste", Action.PASTE, newSeqs, 0, al.getWidth(), al);
178 assertEquals(6, al.getSequences().size());
179 assertEquals("1234567890", seqs[3].getSequenceAsString());
180 assertEquals("ACEFKL", seqs[4].getSequenceAsString());
181 assertEquals("JWMPDH", seqs[5].getSequenceAsString());
185 * Test insertGap followed by undo command
187 @Test(groups = { "Functional" })
188 public void testUndo_insertGap()
190 // Edit ec = testee.new Edit(Action.INSERT_GAP, seqs, 4, 3, '?');
191 testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true);
192 // check something changed
193 assertEquals("abcd???efghjk", seqs[0].getSequenceAsString());
194 testee.undoCommand(new AlignmentI[] { al });
195 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
196 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
197 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
198 assertEquals("1234567890", seqs[3].getSequenceAsString());
202 * Test deleteGap followed by undo command
204 @Test(groups = { "Functional" })
205 public void testUndo_deleteGap()
207 testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true);
208 // check something changed
209 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
210 testee.undoCommand(new AlignmentI[] { al });
211 // deleteGap doesn't 'remember' deleted characters, only gaps get put back
212 assertEquals("abcd???hjk", seqs[0].getSequenceAsString());
213 assertEquals("fghj???nopq", seqs[1].getSequenceAsString());
214 assertEquals("qrst???xyz", seqs[2].getSequenceAsString());
215 assertEquals("1234???890", seqs[3].getSequenceAsString());
219 * Test several commands followed by an undo command
221 @Test(groups = { "Functional" })
222 public void testUndo_multipleCommands()
224 // delete positions 3/4/5 (counting from 1)
225 testee.appendEdit(Action.DELETE_GAP, seqs, 2, 3, al, true);
226 assertEquals("abfghjk", seqs[0].getSequenceAsString());
227 assertEquals("1267890", seqs[3].getSequenceAsString());
229 // insert 2 gaps after the second residue
230 testee.appendEdit(Action.INSERT_GAP, seqs, 2, 2, al, true);
231 assertEquals("ab??fghjk", seqs[0].getSequenceAsString());
232 assertEquals("12??67890", seqs[3].getSequenceAsString());
234 // delete positions 4/5/6
235 testee.appendEdit(Action.DELETE_GAP, seqs, 3, 3, al, true);
236 assertEquals("ab?hjk", seqs[0].getSequenceAsString());
237 assertEquals("12?890", seqs[3].getSequenceAsString());
239 // undo edit commands
240 testee.undoCommand(new AlignmentI[] { al });
241 assertEquals("ab?????hjk", seqs[0].getSequenceAsString());
242 assertEquals("12?????890", seqs[3].getSequenceAsString());
246 * Unit test for JAL-1594 bug: click and drag sequence right to insert gaps -
247 * undo did not remove them all.
249 @Test(groups = { "Functional" })
250 public void testUndo_multipleInsertGaps()
252 testee.appendEdit(Action.INSERT_GAP, seqs, 4, 1, al, true);
253 testee.appendEdit(Action.INSERT_GAP, seqs, 5, 1, al, true);
254 testee.appendEdit(Action.INSERT_GAP, seqs, 6, 1, al, true);
256 // undo edit commands
257 testee.undoCommand(new AlignmentI[] { al });
258 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
259 assertEquals("1234567890", seqs[3].getSequenceAsString());
264 * Test cut followed by undo command
266 @Test(groups = { "Functional" })
267 public void testUndo_cut()
269 testee.appendEdit(Action.CUT, seqs, 4, 3, al, true);
270 // check something changed
271 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
272 testee.undoCommand(new AlignmentI[] { al });
273 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
274 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
275 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
276 assertEquals("1234567890", seqs[3].getSequenceAsString());
280 * Test the replace command (used to manually edit a sequence)
282 @Test(groups = { "Functional" })
283 public void testReplace()
285 // seem to need a dataset sequence on the edited sequence here
286 seqs[1].createDatasetSequence();
287 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
288 // NB command.number holds end position for a Replace command
289 new EditCommand("", Action.REPLACE, "Z-xY", new SequenceI[] { seqs[1] },
291 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
292 assertEquals("fghjZ-xYopq", seqs[1].getSequenceAsString());
293 // Dataset Sequence should always be uppercase
294 assertEquals("fghjZxYopq".toUpperCase(Locale.ROOT),
295 seqs[1].getDatasetSequence().getSequenceAsString());
296 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
297 assertEquals("1234567890", seqs[3].getSequenceAsString());
301 * Test the replace command (used to manually edit a sequence)
303 @Test(groups = { "Functional" })
304 public void testReplace_withGaps()
306 SequenceI seq = new Sequence("seq", "ABC--DEF");
307 seq.createDatasetSequence();
308 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
309 assertEquals(1, seq.getStart());
310 assertEquals(6, seq.getEnd());
313 * replace C- with XYZ
314 * NB arg4 = start column of selection for edit (base 0)
315 * arg5 = column after end of selection for edit
317 EditCommand edit = new EditCommand("", Action.REPLACE, "xyZ",
320 assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
321 assertEquals(1, seq.getStart());
322 assertEquals(8, seq.getEnd());
323 // Dataset sequence always uppercase
324 assertEquals("ABxyZDEF".toUpperCase(Locale.ROOT),
325 seq.getDatasetSequence().getSequenceAsString());
326 assertEquals(8, seq.getDatasetSequence().getEnd());
331 AlignmentI[] views = new AlignmentI[] {
332 new Alignment(new SequenceI[]
334 edit.undoCommand(views);
336 assertEquals("ABC--DEF", seq.getSequenceAsString());
337 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
338 assertEquals(1, seq.getStart());
339 assertEquals(6, seq.getEnd());
340 assertEquals(6, seq.getDatasetSequence().getEnd());
345 edit.doCommand(views);
347 assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
348 assertEquals(1, seq.getStart());
349 assertEquals(8, seq.getEnd());
350 // dataset sequence should be Uppercase
351 assertEquals("ABxyZDEF".toUpperCase(Locale.ROOT),
352 seq.getDatasetSequence().getSequenceAsString());
353 assertEquals(8, seq.getDatasetSequence().getEnd());
358 * Test replace command when it doesn't cause a sequence edit (see comment in
360 @Test(groups = { "Functional" })
361 public void testReplaceFirstResiduesWithGaps()
363 // test replace when gaps are inserted at start. Start/end should change
364 // w.r.t. original edited sequence.
365 SequenceI dsseq = seqs[1].getDatasetSequence();
366 EditCommand edit = new EditCommand("", Action.REPLACE, "----",
368 { seqs[1] }, 0, 4, al);
371 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
372 // and ds is preserved
373 assertTrue(dsseq == seqs[1].getDatasetSequence());
374 // and it is unchanged and UPPERCASE !
375 assertEquals("fghjklmnopq".toUpperCase(Locale.ROOT),
376 dsseq.getSequenceAsString());
377 // and that alignment sequence start has been adjusted
378 assertEquals(5, seqs[1].getStart());
379 assertEquals(11, seqs[1].getEnd());
381 AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
383 edit.undoCommand(views);
385 // dataset sequence unchanged
386 assertTrue(dsseq == seqs[1].getDatasetSequence());
388 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
389 // and start/end numbering also restored
390 assertEquals(1, seqs[1].getStart());
391 assertEquals(11, seqs[1].getEnd());
394 edit.undoCommand(views);
396 // and repeat asserts for the original edit
399 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
400 // and ds is preserved
401 assertTrue(dsseq == seqs[1].getDatasetSequence());
402 // and it is unchanged AND UPPERCASE !
403 assertEquals("fghjklmnopq".toUpperCase(Locale.ROOT),
404 dsseq.getSequenceAsString());
405 // and that alignment sequence start has been adjusted
406 assertEquals(5, seqs[1].getStart());
407 assertEquals(11, seqs[1].getEnd());
412 * Test that the addEdit command correctly merges insert gap commands when
415 @Test(groups = { "Functional" })
416 public void testAddEdit_multipleInsertGap()
419 * 3 insert gap in a row (aka mouse drag right):
421 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
423 { seqs[0] }, 1, 1, al);
425 SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
426 edited.setDatasetSequence(seqs[0].getDatasetSequence());
427 e = new EditCommand().new Edit(Action.INSERT_GAP,
429 { edited }, 2, 1, al);
431 edited = new Sequence("seq0", "a??bcdefghjk");
432 edited.setDatasetSequence(seqs[0].getDatasetSequence());
433 e = new EditCommand().new Edit(Action.INSERT_GAP,
435 { edited }, 3, 1, al);
437 assertEquals(1, testee.getSize());
438 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
439 assertEquals(1, testee.getEdit(0).getPosition());
440 assertEquals(3, testee.getEdit(0).getNumber());
443 * Add a non-contiguous edit - should not be merged.
445 e = new EditCommand().new Edit(Action.INSERT_GAP,
447 { edited }, 5, 2, al);
449 assertEquals(2, testee.getSize());
450 assertEquals(5, testee.getEdit(1).getPosition());
451 assertEquals(2, testee.getEdit(1).getNumber());
454 * Add a Delete after the Insert - should not be merged.
456 e = new EditCommand().new Edit(Action.DELETE_GAP,
458 { edited }, 6, 2, al);
460 assertEquals(3, testee.getSize());
461 assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
462 assertEquals(6, testee.getEdit(2).getPosition());
463 assertEquals(2, testee.getEdit(2).getNumber());
467 * Test that the addEdit command correctly merges delete gap commands when
470 @Test(groups = { "Functional" })
471 public void testAddEdit_multipleDeleteGap()
474 * 3 delete gap in a row (aka mouse drag left):
476 seqs[0].setSequence("a???bcdefghjk");
477 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
479 { seqs[0] }, 4, 1, al);
481 assertEquals(1, testee.getSize());
483 SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
484 edited.setDatasetSequence(seqs[0].getDatasetSequence());
485 e = new EditCommand().new Edit(Action.DELETE_GAP,
487 { edited }, 3, 1, al);
489 assertEquals(1, testee.getSize());
491 edited = new Sequence("seq0", "a?bcdefghjk");
492 edited.setDatasetSequence(seqs[0].getDatasetSequence());
493 e = new EditCommand().new Edit(Action.DELETE_GAP,
495 { edited }, 2, 1, al);
497 assertEquals(1, testee.getSize());
498 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
499 assertEquals(2, testee.getEdit(0).getPosition());
500 assertEquals(3, testee.getEdit(0).getNumber());
503 * Add a non-contiguous edit - should not be merged.
505 e = new EditCommand().new Edit(Action.DELETE_GAP,
507 { edited }, 2, 1, al);
509 assertEquals(2, testee.getSize());
510 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
511 assertEquals(2, testee.getEdit(1).getPosition());
512 assertEquals(1, testee.getEdit(1).getNumber());
515 * Add an Insert after the Delete - should not be merged.
517 e = new EditCommand().new Edit(Action.INSERT_GAP,
519 { edited }, 1, 1, al);
521 assertEquals(3, testee.getSize());
522 assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
523 assertEquals(1, testee.getEdit(2).getPosition());
524 assertEquals(1, testee.getEdit(2).getNumber());
528 * Test that the addEdit command correctly handles 'remove gaps' edits for the
529 * case when they appear contiguous but are acting on different sequences.
530 * They should not be merged.
532 @Test(groups = { "Functional" })
533 public void testAddEdit_removeAllGaps()
535 seqs[0].setSequence("a???bcdefghjk");
536 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
538 { seqs[0] }, 4, 1, al);
541 seqs[1].setSequence("f??ghjklmnopq");
542 Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP,
544 { seqs[1] }, 3, 1, al);
546 assertEquals(2, testee.getSize());
547 assertSame(e, testee.getEdit(0));
548 assertSame(e2, testee.getEdit(1));
552 * Test that the addEdit command correctly merges insert gap commands acting
553 * on a multi-sequence selection.
555 @Test(groups = { "Functional" })
556 public void testAddEdit_groupInsertGaps()
559 * 2 insert gap in a row (aka mouse drag right), on two sequences:
561 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
563 { seqs[0], seqs[1] }, 1, 1, al);
565 SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
566 seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
567 SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
568 seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
569 e = new EditCommand().new Edit(Action.INSERT_GAP,
571 { seq1edited, seq2edited }, 2, 1, al);
574 assertEquals(1, testee.getSize());
575 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
576 assertEquals(1, testee.getEdit(0).getPosition());
577 assertEquals(2, testee.getEdit(0).getNumber());
578 assertEquals(seqs[0].getDatasetSequence(),
579 testee.getEdit(0).getSequences()[0].getDatasetSequence());
580 assertEquals(seqs[1].getDatasetSequence(),
581 testee.getEdit(0).getSequences()[1].getDatasetSequence());
585 * Test for 'undoing' a series of gap insertions.
587 * <li>Start: ABCDEF insert 2 at pos 1</li>
588 * <li>next: A--BCDEF insert 1 at pos 4</li>
589 * <li>next: A--B-CDEF insert 2 at pos 0</li>
590 * <li>last: --A--B-CDEF</li>
593 @Test(groups = { "Functional" })
594 public void testPriorState_multipleInserts()
596 EditCommand command = new EditCommand();
597 SequenceI seq = new Sequence("", "--A--B-CDEF");
598 SequenceI ds = new Sequence("", "ABCDEF");
599 seq.setDatasetSequence(ds);
600 SequenceI[] sqs = new SequenceI[] { seq };
601 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
603 e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
605 e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
608 Map<SequenceI, SequenceI> unwound = command.priorState(false);
609 assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
613 * Test for 'undoing' a series of gap deletions.
615 * <li>Start: A-B-C delete 1 at pos 1</li>
616 * <li>Next: AB-C delete 1 at pos 2</li>
620 @Test(groups = { "Functional" })
621 public void testPriorState_removeAllGaps()
623 EditCommand command = new EditCommand();
624 SequenceI seq = new Sequence("", "ABC");
625 SequenceI ds = new Sequence("", "ABC");
626 seq.setDatasetSequence(ds);
627 SequenceI[] sqs = new SequenceI[] { seq };
628 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
630 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
633 Map<SequenceI, SequenceI> unwound = command.priorState(false);
634 assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
638 * Test for 'undoing' a single delete edit.
640 @Test(groups = { "Functional" })
641 public void testPriorState_singleDelete()
643 EditCommand command = new EditCommand();
644 SequenceI seq = new Sequence("", "ABCDEF");
645 SequenceI ds = new Sequence("", "ABCDEF");
646 seq.setDatasetSequence(ds);
647 SequenceI[] sqs = new SequenceI[] { seq };
648 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
651 Map<SequenceI, SequenceI> unwound = command.priorState(false);
652 assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
656 * Test 'undoing' a single gap insertion edit command.
658 @Test(groups = { "Functional" })
659 public void testPriorState_singleInsert()
661 EditCommand command = new EditCommand();
662 SequenceI seq = new Sequence("", "AB---CDEF");
663 SequenceI ds = new Sequence("", "ABCDEF");
664 seq.setDatasetSequence(ds);
665 SequenceI[] sqs = new SequenceI[] { seq };
666 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
669 Map<SequenceI, SequenceI> unwound = command.priorState(false);
670 SequenceI prior = unwound.get(ds);
671 assertEquals("ABCDEF", prior.getSequenceAsString());
672 assertEquals(1, prior.getStart());
673 assertEquals(6, prior.getEnd());
677 * Test 'undoing' a single gap insertion edit command, on a sequence whose
678 * start residue is other than 1
680 @Test(groups = { "Functional" })
681 public void testPriorState_singleInsertWithOffset()
683 EditCommand command = new EditCommand();
684 SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
685 // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
686 // seq.setDatasetSequence(ds);
687 seq.createDatasetSequence();
688 SequenceI[] sqs = new SequenceI[] { seq };
689 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
692 Map<SequenceI, SequenceI> unwound = command.priorState(false);
693 SequenceI prior = unwound.get(seq.getDatasetSequence());
694 assertEquals("ABCDEF", prior.getSequenceAsString());
695 assertEquals(8, prior.getStart());
696 assertEquals(13, prior.getEnd());
700 * Test that mimics 'remove all gaps' action. This generates delete gap edits
701 * for contiguous gaps in each sequence separately.
703 @Test(groups = { "Functional" })
704 public void testPriorState_removeGapsMultipleSeqs()
706 EditCommand command = new EditCommand();
707 String original1 = "--ABC-DEF";
708 String original2 = "FG-HI--J";
709 String original3 = "M-NOPQ";
712 * Two edits for the first sequence
714 SequenceI seq = new Sequence("", "ABC-DEF");
715 SequenceI ds1 = new Sequence("", "ABCDEF");
716 seq.setDatasetSequence(ds1);
717 SequenceI[] sqs = new SequenceI[] { seq };
718 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
720 seq = new Sequence("", "ABCDEF");
721 seq.setDatasetSequence(ds1);
722 sqs = new SequenceI[] { seq };
723 e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
727 * Two edits for the second sequence
729 seq = new Sequence("", "FGHI--J");
730 SequenceI ds2 = new Sequence("", "FGHIJ");
731 seq.setDatasetSequence(ds2);
732 sqs = new SequenceI[] { seq };
733 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
735 seq = new Sequence("", "FGHIJ");
736 seq.setDatasetSequence(ds2);
737 sqs = new SequenceI[] { seq };
738 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
742 * One edit for the third sequence.
744 seq = new Sequence("", "MNOPQ");
745 SequenceI ds3 = new Sequence("", "MNOPQ");
746 seq.setDatasetSequence(ds3);
747 sqs = new SequenceI[] { seq };
748 e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
751 Map<SequenceI, SequenceI> unwound = command.priorState(false);
752 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
753 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
754 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
758 * Test that mimics 'remove all gapped columns' action. This generates a
759 * series Delete Gap edits that each act on all sequences that share a gapped
762 @Test(groups = { "Functional" })
763 public void testPriorState_removeGappedCols()
765 EditCommand command = new EditCommand();
766 String original1 = "--ABC--DEF";
767 String original2 = "-G-HI--J";
768 String original3 = "-M-NO--PQ";
771 * First edit deletes the first column.
773 SequenceI seq1 = new Sequence("", "-ABC--DEF");
774 SequenceI ds1 = new Sequence("", "ABCDEF");
775 seq1.setDatasetSequence(ds1);
776 SequenceI seq2 = new Sequence("", "G-HI--J");
777 SequenceI ds2 = new Sequence("", "GHIJ");
778 seq2.setDatasetSequence(ds2);
779 SequenceI seq3 = new Sequence("", "M-NO--PQ");
780 SequenceI ds3 = new Sequence("", "MNOPQ");
781 seq3.setDatasetSequence(ds3);
782 SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
783 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
787 * Second edit deletes what is now columns 4 and 5.
789 seq1 = new Sequence("", "-ABCDEF");
790 seq1.setDatasetSequence(ds1);
791 seq2 = new Sequence("", "G-HIJ");
792 seq2.setDatasetSequence(ds2);
793 seq3 = new Sequence("", "M-NOPQ");
794 seq3.setDatasetSequence(ds3);
795 sqs = new SequenceI[] { seq1, seq2, seq3 };
796 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
799 Map<SequenceI, SequenceI> unwound = command.priorState(false);
800 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
801 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
802 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
803 assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
804 assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
805 assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
809 * Test a cut action's relocation of sequence features
811 @Test(groups = { "Functional" })
812 public void testCut_withFeatures()
815 * create sequence features before, after and overlapping
816 * a cut of columns/residues 4-7
818 SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
819 seq0.addSequenceFeature(
820 new SequenceFeature("before", "", 1, 3, 0f, null));
821 seq0.addSequenceFeature(
822 new SequenceFeature("overlap left", "", 2, 6, 0f, null));
823 seq0.addSequenceFeature(
824 new SequenceFeature("internal", "", 5, 6, 0f, null));
825 seq0.addSequenceFeature(
826 new SequenceFeature("overlap right", "", 7, 8, 0f, null));
827 seq0.addSequenceFeature(
828 new SequenceFeature("after", "", 8, 10, 0f, null));
831 * add some contact features
833 SequenceFeature internalContact = new SequenceFeature("disulphide bond",
835 seq0.addSequenceFeature(internalContact); // should get deleted
836 SequenceFeature overlapLeftContact = new SequenceFeature(
837 "disulphide bond", "", 2, 6, 0f, null);
838 seq0.addSequenceFeature(overlapLeftContact); // should get deleted
839 SequenceFeature overlapRightContact = new SequenceFeature(
840 "disulphide bond", "", 5, 8, 0f, null);
841 seq0.addSequenceFeature(overlapRightContact); // should get deleted
842 SequenceFeature spanningContact = new SequenceFeature("disulphide bond",
844 seq0.addSequenceFeature(spanningContact); // should get shortened 3'
847 * cut columns 3-6 (base 0), residues d-g 4-7
849 Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
850 EditCommand.cut(ec, new AlignmentI[] { al });
852 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
853 SequenceFeatures.sortFeatures(sfs, true);
855 assertEquals(5, sfs.size()); // features internal to cut were deleted
856 SequenceFeature sf = sfs.get(0);
857 assertEquals("before", sf.getType());
858 assertEquals(1, sf.getBegin());
859 assertEquals(3, sf.getEnd());
861 assertEquals("disulphide bond", sf.getType());
862 assertEquals(2, sf.getBegin());
863 assertEquals(5, sf.getEnd()); // truncated by cut
865 assertEquals("overlap left", sf.getType());
866 assertEquals(2, sf.getBegin());
867 assertEquals(3, sf.getEnd()); // truncated by cut
869 assertEquals("after", sf.getType());
870 assertEquals(4, sf.getBegin()); // shifted left by cut
871 assertEquals(6, sf.getEnd()); // shifted left by cut
873 assertEquals("overlap right", sf.getType());
874 assertEquals(4, sf.getBegin()); // shifted left by cut
875 assertEquals(4, sf.getEnd()); // truncated by cut
879 * Test a cut action's relocation of sequence features, with full coverage of
880 * all possible feature and cut locations for a 5-position ungapped sequence
882 @Test(groups = { "Functional" })
883 public void testCut_withFeatures_exhaustive()
886 * create a sequence features on each subrange of 1-5
888 SequenceI seq0 = new Sequence("seq", "ABCDE");
891 seq0.setStart(start);
893 AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
894 alignment.setDataset(null);
897 * create a new alignment with shared dataset sequence
899 AlignmentI copy = new Alignment(
901 { alignment.getDataset().getSequenceAt(0).deriveSequence() });
902 SequenceI copySeq0 = copy.getSequenceAt(0);
904 for (int from = start; from <= end; from++)
906 for (int to = from; to <= end; to++)
908 String desc = String.format("%d-%d", from, to);
909 SequenceFeature sf = new SequenceFeature("test", desc, from, to, 0f,
911 sf.setValue("from", Integer.valueOf(from));
912 sf.setValue("to", Integer.valueOf(to));
913 seq0.addSequenceFeature(sf);
917 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
918 assertEquals(func(5), sfs.size());
919 assertEquals(sfs, copySeq0.getSequenceFeatures());
920 String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
923 * now perform all possible cuts of subranges of columns 1-5
924 * and validate the resulting remaining sequence features!
926 SequenceI[] sqs = new SequenceI[] { seq0 };
928 for (int from = 0; from < seq0.getLength(); from++)
930 for (int to = from; to < seq0.getLength(); to++)
932 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from,
933 (to - from + 1), alignment);
934 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
935 boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
936 .getDatasetSequence();
938 verifyCut(seq0, from, to, msg, start);
941 * verify copy alignment dataset sequence unaffected
943 assertEquals("Original dataset sequence was modified",
944 copySequenceFeatures,
945 copySeq0.getSequenceFeatures().toString());
948 * verify any new dataset sequence was added to the
951 assertEquals("Wrong Dataset size after " + msg,
952 newDatasetSequence ? 2 : 1,
953 alignment.getDataset().getHeight());
956 * undo and verify all restored
958 AlignmentI[] views = new AlignmentI[] { alignment };
959 ec.undoCommand(views);
960 sfs = seq0.getSequenceFeatures();
961 assertEquals("After undo of " + msg, func(5), sfs.size());
962 verifyUndo(from, to, sfs);
965 * verify copy alignment dataset sequence still unaffected
966 * and alignment dataset has shrunk (if it was added to)
968 assertEquals("Original dataset sequence was modified",
969 copySequenceFeatures,
970 copySeq0.getSequenceFeatures().toString());
971 assertEquals("Wrong Dataset size after Undo of " + msg, 1,
972 alignment.getDataset().getHeight());
978 verifyCut(seq0, from, to, msg, start);
981 * verify copy alignment dataset sequence unaffected
982 * and any new dataset sequence readded to alignment dataset
984 assertEquals("Original dataset sequence was modified",
985 copySequenceFeatures,
986 copySeq0.getSequenceFeatures().toString());
987 assertEquals("Wrong Dataset size after Redo of " + msg,
988 newDatasetSequence ? 2 : 1,
989 alignment.getDataset().getHeight());
992 * undo ready for next cut
994 ec.undoCommand(views);
997 * final verify that copy alignment dataset sequence is still unaffected
998 * and that alignment dataset has shrunk
1000 assertEquals("Original dataset sequence was modified",
1001 copySequenceFeatures,
1002 copySeq0.getSequenceFeatures().toString());
1003 assertEquals("Wrong Dataset size after final Undo of " + msg, 1,
1004 alignment.getDataset().getHeight());
1010 * Verify by inspection that the sequence features left on the sequence after
1011 * a cut match the expected results. The trick to this is that we can parse
1012 * each feature's original start-end positions from its description.
1020 protected void verifyCut(SequenceI seq0, int from, int to,
1021 final String msg, int seqStart)
1023 List<SequenceFeature> sfs;
1024 sfs = seq0.getSequenceFeatures();
1026 Collections.sort(sfs, BY_DESCRIPTION);
1029 * confirm the number of features has reduced by the
1030 * number of features within the cut region i.e. by
1031 * func(length of cut); exception is a cut at start or end of sequence,
1032 * which retains the original coordinates, dataset sequence
1033 * and all its features
1035 boolean datasetRetained = from == 0 || to == 4;
1036 if (datasetRetained)
1038 // dataset and all features retained
1039 assertEquals(msg, func(5), sfs.size());
1041 else if (to - from == 4)
1043 // all columns were cut
1044 assertTrue(sfs.isEmpty());
1048 // failure in checkFeatureRelocation is more informative!
1049 assertEquals(msg + "wrong number of features left",
1050 func(5) - func(to - from + 1), sfs.size());
1054 * inspect individual features
1056 for (SequenceFeature sf : sfs)
1058 verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
1064 * Check that after Undo, every feature has start/end that match its original
1065 * "start" and "end" properties
1071 protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
1073 for (SequenceFeature sf : sfs)
1075 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1076 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1077 String msg = String.format("Undo cut of [%d-%d], feature at [%d-%d] ",
1078 from + 1, to + 1, oldFrom, oldTo);
1079 assertEquals(msg + "start", oldFrom, sf.getBegin());
1080 assertEquals(msg + "end", oldTo, sf.getEnd());
1085 * Helper method to check a feature has been correctly relocated after a cut
1089 * start of cut (first residue cut 1..)
1091 * end of cut (last residue cut 1..)
1095 private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1096 boolean newDataset, int seqStart)
1098 // TODO handle the gapped sequence case as well
1099 int cutSize = to - from + 1;
1100 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1101 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1102 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1103 final int oldToPosition = oldTo - seqStart + 1; // 1..
1105 String msg = String.format(
1106 "Feature %s relocated to %d-%d after cut of %d-%d",
1107 sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
1110 // dataset retained with all features unchanged
1111 assertEquals("0: " + msg, oldFrom, sf.getBegin());
1112 assertEquals("0: " + msg, oldTo, sf.getEnd());
1114 else if (oldToPosition < from)
1116 // before cut region so unchanged
1117 assertEquals("1: " + msg, oldFrom, sf.getBegin());
1118 assertEquals("2: " + msg, oldTo, sf.getEnd());
1120 else if (oldFromPosition > to)
1122 // follows cut region - shift by size of cut
1123 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1125 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1128 else if (oldFromPosition < from && oldToPosition > to)
1130 // feature encloses cut region - shrink it right
1131 assertEquals("5: " + msg, oldFrom, sf.getBegin());
1132 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1134 else if (oldFromPosition < from)
1136 // feature overlaps left side of cut region - truncated right
1137 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1139 else if (oldToPosition > to)
1141 // feature overlaps right side of cut region - truncated left
1142 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1144 assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1149 // feature internal to cut - should have been deleted!
1150 Assert.fail(msg + " - should have been deleted");
1155 * Test a cut action's relocation of sequence features
1157 @Test(groups = { "Functional" })
1158 public void testCut_withFeatures5prime()
1160 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1161 seq0.createDatasetSequence();
1162 assertEquals(8, seq0.getStart());
1163 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f, null));
1164 SequenceI[] seqsArray = new SequenceI[] { seq0 };
1165 AlignmentI alignment = new Alignment(seqsArray);
1168 * cut columns of A-B; same dataset sequence is retained, aligned sequence
1171 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1172 EditCommand.cut(ec, new AlignmentI[] { alignment });
1175 * feature on CC(10-11) should still be on CC(10-11)
1177 assertSame(seq0, alignment.getSequenceAt(0));
1178 assertEquals(10, seq0.getStart());
1179 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1180 assertEquals(1, sfs.size());
1181 SequenceFeature sf = sfs.get(0);
1182 assertEquals(10, sf.getBegin());
1183 assertEquals(11, sf.getEnd());
1185 private SequenceI mkDs(SequenceI as)
1187 SequenceI ds = as.createDatasetSequence();
1188 ds.setSequence(ds.getSequenceAsString().toUpperCase(Locale.ROOT));
1192 * Test that mimics 'remove all gapped columns' action. This generates a
1193 * series Delete Gap edits that each act on all sequences that share a gapped
1196 @Test(groups = { "Functional" })
1197 public void testLeftRight_Justify_and_preserves_gaps()
1199 EditCommand command = new EditCommand();
1200 String original1 = "--ABc--DEF";
1201 String original2 = "-G-Hi--J";
1202 String original3 = "-M-No--PQ";
1205 * Set up the sequence array for operations
1207 SequenceI seq1 = new Sequence("sq1", original1);
1208 SequenceI ds1 = mkDs(seq1);
1210 * and check we are preserving data - if the calls below fail, something has broken the Jalview dataset derivation process
1212 assertEquals("ABCDEF", seq1.getDatasetSequence().getSequenceAsString());
1213 assertEquals(original1,seq1.getSequenceAsString());
1214 SequenceI seq2 = new Sequence("sq2",original2);
1215 SequenceI ds2 = mkDs(seq2);
1216 SequenceI seq3 = new Sequence("sq3", original3);
1217 SequenceI ds3 = mkDs(seq3);
1218 List<SequenceI> sqs = Arrays.asList( seq1, seq2, seq3 );
1219 Alignment al = new Alignment(sqs.toArray(new SequenceI[0]));
1220 EditCommand lefj = new JustifyLeftOrRightCommand("Left J", true, sqs, 1, 7, al);
1221 String exp = "-ABcD---EF";
1222 // check without case conservation
1223 assertEquals(exp.toUpperCase(Locale.ROOT),seq1.getSequenceAsString().toUpperCase(Locale.ROOT));
1225 assertEquals(exp,seq1.getSequenceAsString());
1227 assertEquals("-GHiJ---",seq2.getSequenceAsString());
1228 assertEquals("-MNoP---Q",seq3.getSequenceAsString());
1229 lefj.undoCommand(new AlignmentI[] { al});
1230 assertEquals(original3,seq3.getSequenceAsString());
1231 assertEquals(original1,seq1.getSequenceAsString());
1232 assertEquals(original2,seq2.getSequenceAsString());
1234 EditCommand righj = new JustifyLeftOrRightCommand("Right J", false, sqs, 2, 7, al);
1235 assertEquals("----ABcDEF",seq1.getSequenceAsString());
1236 assertEquals("-G---HiJ",seq2.getSequenceAsString());
1237 assertEquals("-M---NoPQ",seq3.getSequenceAsString());
1238 righj.undoCommand(new AlignmentI[] { al});
1239 assertEquals(original3,seq3.getSequenceAsString());
1240 assertEquals(original1,seq1.getSequenceAsString());
1241 assertEquals(original2,seq2.getSequenceAsString());