/* * Jalview - A Sequence Alignment Editor and Viewer (Version 2.9) * Copyright (C) 2015 The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.commands; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertSame; import jalview.commands.EditCommand.Action; import jalview.commands.EditCommand.Edit; import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; import jalview.datamodel.Sequence; import jalview.datamodel.SequenceI; import java.util.Map; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; /** * Unit tests for EditCommand * * @author gmcarstairs * */ public class EditCommandTest { private EditCommand testee; private SequenceI[] seqs; private Alignment al; @BeforeMethod(alwaysRun = true) public void setUp() { testee = new EditCommand(); seqs = new SequenceI[4]; seqs[0] = new Sequence("seq0", "abcdefghjk"); seqs[0].setDatasetSequence(new Sequence("seq0ds", "abcdefghjk")); seqs[1] = new Sequence("seq1", "fghjklmnopq"); seqs[1].setDatasetSequence(new Sequence("seq1ds", "fghjklmnopq")); seqs[2] = new Sequence("seq2", "qrstuvwxyz"); seqs[2].setDatasetSequence(new Sequence("seq2ds", "qrstuvwxyz")); seqs[3] = new Sequence("seq3", "1234567890"); seqs[3].setDatasetSequence(new Sequence("seq3ds", "1234567890")); al = new Alignment(seqs); al.setGapCharacter('?'); } /** * Test inserting gap characters */ @Test(groups = { "Functional" }) public void testAppendEdit_insertGap() { // set a non-standard gap character to prove it is actually used testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true); assertEquals("abcd???efghjk", seqs[0].getSequenceAsString()); assertEquals("fghj???klmnopq", seqs[1].getSequenceAsString()); assertEquals("qrst???uvwxyz", seqs[2].getSequenceAsString()); assertEquals("1234???567890", seqs[3].getSequenceAsString()); // todo: test for handling out of range positions? } /** * Test deleting characters from sequences. Note the deleteGap() action does * not check that only gap characters are being removed. */ @Test(groups = { "Functional" }) public void testAppendEdit_deleteGap() { testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true); assertEquals("abcdhjk", seqs[0].getSequenceAsString()); assertEquals("fghjnopq", seqs[1].getSequenceAsString()); assertEquals("qrstxyz", seqs[2].getSequenceAsString()); assertEquals("1234890", seqs[3].getSequenceAsString()); } /** * Test a cut action. The command should store the cut characters to support * undo. */ @Test(groups = { "Functional" }) public void testCut() { Edit ec = testee.new Edit(Action.CUT, seqs, 4, 3, al); testee.cut(ec, new AlignmentI[] { al }); assertEquals("abcdhjk", seqs[0].getSequenceAsString()); assertEquals("fghjnopq", seqs[1].getSequenceAsString()); assertEquals("qrstxyz", seqs[2].getSequenceAsString()); assertEquals("1234890", seqs[3].getSequenceAsString()); assertEquals("efg", new String(ec.string[0])); assertEquals("klm", new String(ec.string[1])); assertEquals("uvw", new String(ec.string[2])); assertEquals("567", new String(ec.string[3])); // TODO: case where whole sequence is deleted as nothing left; etc } /** * Test a Paste action, where this adds sequences to an alignment. */ @Test(groups = { "Functional" }, enabled = false) // TODO fix so it works public void testPaste_addToAlignment() { SequenceI[] newSeqs = new SequenceI[2]; newSeqs[0] = new Sequence("newseq0", "ACEFKL"); newSeqs[1] = new Sequence("newseq1", "JWMPDH"); Edit ec = testee.new Edit(Action.PASTE, newSeqs, 0, al.getWidth(), al); testee.paste(ec, new AlignmentI[] { al }); assertEquals(6, al.getSequences().size()); assertEquals("1234567890", seqs[3].getSequenceAsString()); assertEquals("ACEFKL", seqs[4].getSequenceAsString()); assertEquals("JWMPDH", seqs[5].getSequenceAsString()); } /** * Test insertGap followed by undo command */ @Test(groups = { "Functional" }) public void testUndo_insertGap() { // Edit ec = testee.new Edit(Action.INSERT_GAP, seqs, 4, 3, '?'); testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true); // check something changed assertEquals("abcd???efghjk", seqs[0].getSequenceAsString()); testee.undoCommand(new AlignmentI[] { al }); assertEquals("abcdefghjk", seqs[0].getSequenceAsString()); assertEquals("fghjklmnopq", seqs[1].getSequenceAsString()); assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString()); assertEquals("1234567890", seqs[3].getSequenceAsString()); } /** * Test deleteGap followed by undo command */ @Test(groups = { "Functional" }) public void testUndo_deleteGap() { testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true); // check something changed assertEquals("abcdhjk", seqs[0].getSequenceAsString()); testee.undoCommand(new AlignmentI[] { al }); // deleteGap doesn't 'remember' deleted characters, only gaps get put back assertEquals("abcd???hjk", seqs[0].getSequenceAsString()); assertEquals("fghj???nopq", seqs[1].getSequenceAsString()); assertEquals("qrst???xyz", seqs[2].getSequenceAsString()); assertEquals("1234???890", seqs[3].getSequenceAsString()); } /** * Test several commands followed by an undo command */ @Test(groups = { "Functional" }) public void testUndo_multipleCommands() { // delete positions 3/4/5 (counting from 1) testee.appendEdit(Action.DELETE_GAP, seqs, 2, 3, al, true); assertEquals("abfghjk", seqs[0].getSequenceAsString()); assertEquals("1267890", seqs[3].getSequenceAsString()); // insert 2 gaps after the second residue testee.appendEdit(Action.INSERT_GAP, seqs, 2, 2, al, true); assertEquals("ab??fghjk", seqs[0].getSequenceAsString()); assertEquals("12??67890", seqs[3].getSequenceAsString()); // delete positions 4/5/6 testee.appendEdit(Action.DELETE_GAP, seqs, 3, 3, al, true); assertEquals("ab?hjk", seqs[0].getSequenceAsString()); assertEquals("12?890", seqs[3].getSequenceAsString()); // undo edit commands testee.undoCommand(new AlignmentI[] { al }); assertEquals("ab?????hjk", seqs[0].getSequenceAsString()); assertEquals("12?????890", seqs[3].getSequenceAsString()); } /** * Unit test for JAL-1594 bug: click and drag sequence right to insert gaps - * undo did not remove them all. */ @Test(groups = { "Functional" }) public void testUndo_multipleInsertGaps() { testee.appendEdit(Action.INSERT_GAP, seqs, 4, 1, al, true); testee.appendEdit(Action.INSERT_GAP, seqs, 5, 1, al, true); testee.appendEdit(Action.INSERT_GAP, seqs, 6, 1, al, true); // undo edit commands testee.undoCommand(new AlignmentI[] { al }); assertEquals("abcdefghjk", seqs[0].getSequenceAsString()); assertEquals("1234567890", seqs[3].getSequenceAsString()); } /** * Test cut followed by undo command */ @Test(groups = { "Functional" }) public void testUndo_cut() { testee.appendEdit(Action.CUT, seqs, 4, 3, al, true); // check something changed assertEquals("abcdhjk", seqs[0].getSequenceAsString()); testee.undoCommand(new AlignmentI[] { al }); assertEquals("abcdefghjk", seqs[0].getSequenceAsString()); assertEquals("fghjklmnopq", seqs[1].getSequenceAsString()); assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString()); assertEquals("1234567890", seqs[3].getSequenceAsString()); } /** * Test the replace command (used to manually edit a sequence) */ @Test(groups = { "Functional" }) public void testReplace() { // seem to need a dataset sequence on the edited sequence here seqs[1].setDatasetSequence(seqs[1]); new EditCommand("", Action.REPLACE, "ZXY", new SequenceI[] { seqs[1] }, 4, 8, al); assertEquals("abcdefghjk", seqs[0].getSequenceAsString()); assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString()); assertEquals("1234567890", seqs[3].getSequenceAsString()); seqs[1] = new Sequence("seq1", "fghjZXYnopq"); } /** * Test that the addEdit command correctly merges insert gap commands when * possible. */ @Test(groups = { "Functional" }) public void testAddEdit_multipleInsertGap() { /* * 3 insert gap in a row (aka mouse drag right): */ Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] { seqs[0] }, 1, 1, al); testee.addEdit(e); SequenceI edited = new Sequence("seq0", "a?bcdefghjk"); edited.setDatasetSequence(seqs[0].getDatasetSequence()); e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] { edited }, 2, 1, al); testee.addEdit(e); edited = new Sequence("seq0", "a??bcdefghjk"); edited.setDatasetSequence(seqs[0].getDatasetSequence()); e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] { edited }, 3, 1, al); testee.addEdit(e); assertEquals(1, testee.getSize()); assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction()); assertEquals(1, testee.getEdit(0).getPosition()); assertEquals(3, testee.getEdit(0).getNumber()); /* * Add a non-contiguous edit - should not be merged. */ e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] { edited }, 5, 2, al); testee.addEdit(e); assertEquals(2, testee.getSize()); assertEquals(5, testee.getEdit(1).getPosition()); assertEquals(2, testee.getEdit(1).getNumber()); /* * Add a Delete after the Insert - should not be merged. */ e = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[] { edited }, 6, 2, al); testee.addEdit(e); assertEquals(3, testee.getSize()); assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction()); assertEquals(6, testee.getEdit(2).getPosition()); assertEquals(2, testee.getEdit(2).getNumber()); } /** * Test that the addEdit command correctly merges delete gap commands when * possible. */ @Test(groups = { "Functional" }) public void testAddEdit_multipleDeleteGap() { /* * 3 delete gap in a row (aka mouse drag left): */ seqs[0].setSequence("a???bcdefghjk"); Edit e = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[] { seqs[0] }, 4, 1, al); testee.addEdit(e); assertEquals(1, testee.getSize()); SequenceI edited = new Sequence("seq0", "a??bcdefghjk"); edited.setDatasetSequence(seqs[0].getDatasetSequence()); e = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[] { edited }, 3, 1, al); testee.addEdit(e); assertEquals(1, testee.getSize()); edited = new Sequence("seq0", "a?bcdefghjk"); edited.setDatasetSequence(seqs[0].getDatasetSequence()); e = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[] { edited }, 2, 1, al); testee.addEdit(e); assertEquals(1, testee.getSize()); assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction()); assertEquals(2, testee.getEdit(0).getPosition()); assertEquals(3, testee.getEdit(0).getNumber()); /* * Add a non-contiguous edit - should not be merged. */ e = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[] { edited }, 2, 1, al); testee.addEdit(e); assertEquals(2, testee.getSize()); assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction()); assertEquals(2, testee.getEdit(1).getPosition()); assertEquals(1, testee.getEdit(1).getNumber()); /* * Add an Insert after the Delete - should not be merged. */ e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] { edited }, 1, 1, al); testee.addEdit(e); assertEquals(3, testee.getSize()); assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction()); assertEquals(1, testee.getEdit(2).getPosition()); assertEquals(1, testee.getEdit(2).getNumber()); } /** * Test that the addEdit command correctly handles 'remove gaps' edits for the * case when they appear contiguous but are acting on different sequences. * They should not be merged. */ @Test(groups = { "Functional" }) public void testAddEdit_removeAllGaps() { seqs[0].setSequence("a???bcdefghjk"); Edit e = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[] { seqs[0] }, 4, 1, al); testee.addEdit(e); seqs[1].setSequence("f??ghjklmnopq"); Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP, new SequenceI[] { seqs[1] }, 3, 1, al); testee.addEdit(e2); assertEquals(2, testee.getSize()); assertSame(e, testee.getEdit(0)); assertSame(e2, testee.getEdit(1)); } /** * Test that the addEdit command correctly merges insert gap commands acting * on a multi-sequence selection. */ @Test(groups = { "Functional" }) public void testAddEdit_groupInsertGaps() { /* * 2 insert gap in a row (aka mouse drag right), on two sequences: */ Edit e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] { seqs[0], seqs[1] }, 1, 1, al); testee.addEdit(e); SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk"); seq1edited.setDatasetSequence(seqs[0].getDatasetSequence()); SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq"); seq2edited.setDatasetSequence(seqs[1].getDatasetSequence()); e = new EditCommand().new Edit(Action.INSERT_GAP, new SequenceI[] { seq1edited, seq2edited }, 2, 1, al); testee.addEdit(e); assertEquals(1, testee.getSize()); assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction()); assertEquals(1, testee.getEdit(0).getPosition()); assertEquals(2, testee.getEdit(0).getNumber()); assertEquals(seqs[0].getDatasetSequence(), testee.getEdit(0) .getSequences()[0].getDatasetSequence()); assertEquals(seqs[1].getDatasetSequence(), testee.getEdit(0) .getSequences()[1].getDatasetSequence()); } /** * Test for 'undoing' a series of gap insertions. * */ @Test(groups = { "Functional" }) public void testPriorState_multipleInserts() { EditCommand command = new EditCommand(); SequenceI seq = new Sequence("", "--A--B-CDEF"); SequenceI ds = new Sequence("", "ABCDEF"); seq.setDatasetSequence(ds); SequenceI[] seqs = new SequenceI[] { seq }; Edit e = command.new Edit(Action.INSERT_GAP, seqs, 1, 2, '-'); command.addEdit(e); e = command.new Edit(Action.INSERT_GAP, seqs, 4, 1, '-'); command.addEdit(e); e = command.new Edit(Action.INSERT_GAP, seqs, 0, 2, '-'); command.addEdit(e); Map unwound = command.priorState(false); assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString()); } /** * Test for 'undoing' a series of gap deletions. * */ @Test(groups = { "Functional" }) public void testPriorState_removeAllGaps() { EditCommand command = new EditCommand(); SequenceI seq = new Sequence("", "ABC"); SequenceI ds = new Sequence("", "ABC"); seq.setDatasetSequence(ds); SequenceI[] seqs = new SequenceI[] { seq }; Edit e = command.new Edit(Action.DELETE_GAP, seqs, 1, 1, '-'); command.addEdit(e); e = command.new Edit(Action.DELETE_GAP, seqs, 2, 1, '-'); command.addEdit(e); Map unwound = command.priorState(false); assertEquals("A-B-C", unwound.get(ds).getSequenceAsString()); } /** * Test for 'undoing' a single delete edit. */ @Test(groups = { "Functional" }) public void testPriorState_singleDelete() { EditCommand command = new EditCommand(); SequenceI seq = new Sequence("", "ABCDEF"); SequenceI ds = new Sequence("", "ABCDEF"); seq.setDatasetSequence(ds); SequenceI[] seqs = new SequenceI[] { seq }; Edit e = command.new Edit(Action.DELETE_GAP, seqs, 2, 2, '-'); command.addEdit(e); Map unwound = command.priorState(false); assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString()); } /** * Test 'undoing' a single gap insertion edit command. */ @Test(groups = { "Functional" }) public void testPriorState_singleInsert() { EditCommand command = new EditCommand(); SequenceI seq = new Sequence("", "AB---CDEF"); SequenceI ds = new Sequence("", "ABCDEF"); seq.setDatasetSequence(ds); SequenceI[] seqs = new SequenceI[] { seq }; Edit e = command.new Edit(Action.INSERT_GAP, seqs, 2, 3, '-'); command.addEdit(e); Map unwound = command.priorState(false); assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString()); } /** * Test that mimics 'remove all gaps' action. This generates delete gap edits * for contiguous gaps in each sequence separately. */ @Test(groups = { "Functional" }) public void testPriorState_removeGapsMultipleSeqs() { EditCommand command = new EditCommand(); String original1 = "--ABC-DEF"; String original2 = "FG-HI--J"; String original3 = "M-NOPQ"; /* * Two edits for the first sequence */ SequenceI seq = new Sequence("", "ABC-DEF"); SequenceI ds1 = new Sequence("", "ABCDEF"); seq.setDatasetSequence(ds1); SequenceI[] seqs = new SequenceI[] { seq }; Edit e = command.new Edit(Action.DELETE_GAP, seqs, 0, 2, '-'); command.addEdit(e); seq = new Sequence("", "ABCDEF"); seq.setDatasetSequence(ds1); seqs = new SequenceI[] { seq }; e = command.new Edit(Action.DELETE_GAP, seqs, 3, 1, '-'); command.addEdit(e); /* * Two edits for the second sequence */ seq = new Sequence("", "FGHI--J"); SequenceI ds2 = new Sequence("", "FGHIJ"); seq.setDatasetSequence(ds2); seqs = new SequenceI[] { seq }; e = command.new Edit(Action.DELETE_GAP, seqs, 2, 1, '-'); command.addEdit(e); seq = new Sequence("", "FGHIJ"); seq.setDatasetSequence(ds2); seqs = new SequenceI[] { seq }; e = command.new Edit(Action.DELETE_GAP, seqs, 4, 2, '-'); command.addEdit(e); /* * One edit for the third sequence. */ seq = new Sequence("", "MNOPQ"); SequenceI ds3 = new Sequence("", "MNOPQ"); seq.setDatasetSequence(ds3); seqs = new SequenceI[] { seq }; e = command.new Edit(Action.DELETE_GAP, seqs, 1, 1, '-'); command.addEdit(e); Map unwound = command.priorState(false); assertEquals(original1, unwound.get(ds1).getSequenceAsString()); assertEquals(original2, unwound.get(ds2).getSequenceAsString()); assertEquals(original3, unwound.get(ds3).getSequenceAsString()); } /** * Test that mimics 'remove all gapped columns' action. This generates a * series Delete Gap edits that each act on all sequences that share a gapped * column region. */ @Test(groups = { "Functional" }) public void testPriorState_removeGappedCols() { EditCommand command = new EditCommand(); String original1 = "--ABC--DEF"; String original2 = "-G-HI--J"; String original3 = "-M-NO--PQ"; /* * First edit deletes the first column. */ SequenceI seq1 = new Sequence("", "-ABC--DEF"); SequenceI ds1 = new Sequence("", "ABCDEF"); seq1.setDatasetSequence(ds1); SequenceI seq2 = new Sequence("", "G-HI--J"); SequenceI ds2 = new Sequence("", "GHIJ"); seq2.setDatasetSequence(ds2); SequenceI seq3 = new Sequence("", "M-NO--PQ"); SequenceI ds3 = new Sequence("", "MNOPQ"); seq3.setDatasetSequence(ds3); SequenceI[] seqs = new SequenceI[] { seq1, seq2, seq3 }; Edit e = command.new Edit(Action.DELETE_GAP, seqs, 0, 1, '-'); command.addEdit(e); /* * Second edit deletes what is now columns 4 and 5. */ seq1 = new Sequence("", "-ABCDEF"); seq1.setDatasetSequence(ds1); seq2 = new Sequence("", "G-HIJ"); seq2.setDatasetSequence(ds2); seq3 = new Sequence("", "M-NOPQ"); seq3.setDatasetSequence(ds3); seqs = new SequenceI[] { seq1, seq2, seq3 }; e = command.new Edit(Action.DELETE_GAP, seqs, 4, 2, '-'); command.addEdit(e); Map unwound = command.priorState(false); assertEquals(original1, unwound.get(ds1).getSequenceAsString()); assertEquals(original2, unwound.get(ds2).getSequenceAsString()); assertEquals(original3, unwound.get(ds3).getSequenceAsString()); assertEquals(ds1, unwound.get(ds1).getDatasetSequence()); assertEquals(ds2, unwound.get(ds2).getDatasetSequence()); assertEquals(ds3, unwound.get(ds3).getDatasetSequence()); } }