/*
* Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
* Copyright (C) $$Year-Rel$$ 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 java.util.Locale;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertSame;
import static org.testng.AssertJUnit.assertTrue;
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.SequenceFeature;
import jalview.datamodel.SequenceI;
import jalview.datamodel.features.SequenceFeatures;
import jalview.gui.JvOptionPane;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
/**
* Unit tests for EditCommand
*
* @author gmcarstairs
*
*/
public class EditCommandTest
{
private static Comparator BY_DESCRIPTION = new Comparator()
{
@Override
public int compare(SequenceFeature o1, SequenceFeature o2)
{
return o1.getDescription().compareTo(o2.getDescription());
}
};
private EditCommand testee;
private SequenceI[] seqs;
private Alignment al;
/*
* compute n(n+1)/2 e.g.
* func(5) = 5 + 4 + 3 + 2 + 1 = 15
*/
private static int func(int i)
{
return i * (i + 1) / 2;
}
@BeforeClass(alwaysRun = true)
public void setUpJvOptionPane()
{
JvOptionPane.setInteractiveMode(false);
JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
}
@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);
EditCommand.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, followed by Undo and Redo
*/
@Test(groups = { "Functional" }, enabled = false)
public void testPaste_undo_redo()
{
// TODO code this test properly, bearing in mind that:
// Paste action requires something on the clipboard (Cut/Copy)
// - EditCommand.paste doesn't add sequences to the alignment
// ... that is done in AlignFrame.paste()
// ... unless as a Redo
// ...
SequenceI[] newSeqs = new SequenceI[2];
newSeqs[0] = new Sequence("newseq0", "ACEFKL");
newSeqs[1] = new Sequence("newseq1", "JWMPDH");
new EditCommand("Paste", Action.PASTE, newSeqs, 0, al.getWidth(), 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].createDatasetSequence();
assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
// NB command.number holds end position for a Replace command
new EditCommand("", Action.REPLACE, "Z-xY", new SequenceI[] { seqs[1] },
4, 8, al);
assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
assertEquals("fghjZ-xYopq", seqs[1].getSequenceAsString());
// Dataset Sequence should always be uppercase
assertEquals("fghjZxYopq".toUpperCase(Locale.ROOT),
seqs[1].getDatasetSequence().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_withGaps()
{
SequenceI seq = new Sequence("seq", "ABC--DEF");
seq.createDatasetSequence();
assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
assertEquals(1, seq.getStart());
assertEquals(6, seq.getEnd());
/*
* replace C- with XYZ
* NB arg4 = start column of selection for edit (base 0)
* arg5 = column after end of selection for edit
*/
EditCommand edit = new EditCommand("", Action.REPLACE, "xyZ",
new SequenceI[]
{ seq }, 2, 4, al);
assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
assertEquals(1, seq.getStart());
assertEquals(8, seq.getEnd());
// Dataset sequence always uppercase
assertEquals("ABxyZDEF".toUpperCase(Locale.ROOT),
seq.getDatasetSequence().getSequenceAsString());
assertEquals(8, seq.getDatasetSequence().getEnd());
/*
* undo the edit
*/
AlignmentI[] views = new AlignmentI[] {
new Alignment(new SequenceI[]
{ seq }) };
edit.undoCommand(views);
assertEquals("ABC--DEF", seq.getSequenceAsString());
assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
assertEquals(1, seq.getStart());
assertEquals(6, seq.getEnd());
assertEquals(6, seq.getDatasetSequence().getEnd());
/*
* redo the edit
*/
edit.doCommand(views);
assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
assertEquals(1, seq.getStart());
assertEquals(8, seq.getEnd());
// dataset sequence should be Uppercase
assertEquals("ABxyZDEF".toUpperCase(Locale.ROOT),
seq.getDatasetSequence().getSequenceAsString());
assertEquals(8, seq.getDatasetSequence().getEnd());
}
/**
* Test replace command when it doesn't cause a sequence edit (see comment in
*/
@Test(groups = { "Functional" })
public void testReplaceFirstResiduesWithGaps()
{
// test replace when gaps are inserted at start. Start/end should change
// w.r.t. original edited sequence.
SequenceI dsseq = seqs[1].getDatasetSequence();
EditCommand edit = new EditCommand("", Action.REPLACE, "----",
new SequenceI[]
{ seqs[1] }, 0, 4, al);
// trimmed start
assertEquals("----klmnopq", seqs[1].getSequenceAsString());
// and ds is preserved
assertTrue(dsseq == seqs[1].getDatasetSequence());
// and it is unchanged and UPPERCASE !
assertEquals("fghjklmnopq".toUpperCase(Locale.ROOT),
dsseq.getSequenceAsString());
// and that alignment sequence start has been adjusted
assertEquals(5, seqs[1].getStart());
assertEquals(11, seqs[1].getEnd());
AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
// and undo
edit.undoCommand(views);
// dataset sequence unchanged
assertTrue(dsseq == seqs[1].getDatasetSequence());
// restore sequence
assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
// and start/end numbering also restored
assertEquals(1, seqs[1].getStart());
assertEquals(11, seqs[1].getEnd());
// now redo
edit.undoCommand(views);
// and repeat asserts for the original edit
// trimmed start
assertEquals("----klmnopq", seqs[1].getSequenceAsString());
// and ds is preserved
assertTrue(dsseq == seqs[1].getDatasetSequence());
// and it is unchanged AND UPPERCASE !
assertEquals("fghjklmnopq".toUpperCase(Locale.ROOT),
dsseq.getSequenceAsString());
// and that alignment sequence start has been adjusted
assertEquals(5, seqs[1].getStart());
assertEquals(11, seqs[1].getEnd());
}
/**
* 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.
*
* - Start: ABCDEF insert 2 at pos 1
* - next: A--BCDEF insert 1 at pos 4
* - next: A--B-CDEF insert 2 at pos 0
* - last: --A--B-CDEF
*
*/
@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[] sqs = new SequenceI[] { seq };
Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
command.addEdit(e);
e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
command.addEdit(e);
e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
command.addEdit(e);
Map unwound = command.priorState(false);
assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
}
/**
* Test for 'undoing' a series of gap deletions.
*
* - Start: A-B-C delete 1 at pos 1
* - Next: AB-C delete 1 at pos 2
* - End: ABC
*
*/
@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[] sqs = new SequenceI[] { seq };
Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
command.addEdit(e);
e = command.new Edit(Action.DELETE_GAP, sqs, 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[] sqs = new SequenceI[] { seq };
Edit e = command.new Edit(Action.DELETE_GAP, sqs, 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[] sqs = new SequenceI[] { seq };
Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
command.addEdit(e);
Map unwound = command.priorState(false);
SequenceI prior = unwound.get(ds);
assertEquals("ABCDEF", prior.getSequenceAsString());
assertEquals(1, prior.getStart());
assertEquals(6, prior.getEnd());
}
/**
* Test 'undoing' a single gap insertion edit command, on a sequence whose
* start residue is other than 1
*/
@Test(groups = { "Functional" })
public void testPriorState_singleInsertWithOffset()
{
EditCommand command = new EditCommand();
SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
// SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
// seq.setDatasetSequence(ds);
seq.createDatasetSequence();
SequenceI[] sqs = new SequenceI[] { seq };
Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
command.addEdit(e);
Map unwound = command.priorState(false);
SequenceI prior = unwound.get(seq.getDatasetSequence());
assertEquals("ABCDEF", prior.getSequenceAsString());
assertEquals(8, prior.getStart());
assertEquals(13, prior.getEnd());
}
/**
* 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[] sqs = new SequenceI[] { seq };
Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
command.addEdit(e);
seq = new Sequence("", "ABCDEF");
seq.setDatasetSequence(ds1);
sqs = new SequenceI[] { seq };
e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
command.addEdit(e);
/*
* Two edits for the second sequence
*/
seq = new Sequence("", "FGHI--J");
SequenceI ds2 = new Sequence("", "FGHIJ");
seq.setDatasetSequence(ds2);
sqs = new SequenceI[] { seq };
e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
command.addEdit(e);
seq = new Sequence("", "FGHIJ");
seq.setDatasetSequence(ds2);
sqs = new SequenceI[] { seq };
e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
command.addEdit(e);
/*
* One edit for the third sequence.
*/
seq = new Sequence("", "MNOPQ");
SequenceI ds3 = new Sequence("", "MNOPQ");
seq.setDatasetSequence(ds3);
sqs = new SequenceI[] { seq };
e = command.new Edit(Action.DELETE_GAP, sqs, 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[] sqs = new SequenceI[] { seq1, seq2, seq3 };
Edit e = command.new Edit(Action.DELETE_GAP, sqs, 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);
sqs = new SequenceI[] { seq1, seq2, seq3 };
e = command.new Edit(Action.DELETE_GAP, sqs, 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());
}
/**
* Test a cut action's relocation of sequence features
*/
@Test(groups = { "Functional" })
public void testCut_withFeatures()
{
/*
* create sequence features before, after and overlapping
* a cut of columns/residues 4-7
*/
SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
seq0.addSequenceFeature(
new SequenceFeature("before", "", 1, 3, 0f, null));
seq0.addSequenceFeature(
new SequenceFeature("overlap left", "", 2, 6, 0f, null));
seq0.addSequenceFeature(
new SequenceFeature("internal", "", 5, 6, 0f, null));
seq0.addSequenceFeature(
new SequenceFeature("overlap right", "", 7, 8, 0f, null));
seq0.addSequenceFeature(
new SequenceFeature("after", "", 8, 10, 0f, null));
/*
* add some contact features
*/
SequenceFeature internalContact = new SequenceFeature("disulphide bond",
"", 5, 6, 0f, null);
seq0.addSequenceFeature(internalContact); // should get deleted
SequenceFeature overlapLeftContact = new SequenceFeature(
"disulphide bond", "", 2, 6, 0f, null);
seq0.addSequenceFeature(overlapLeftContact); // should get deleted
SequenceFeature overlapRightContact = new SequenceFeature(
"disulphide bond", "", 5, 8, 0f, null);
seq0.addSequenceFeature(overlapRightContact); // should get deleted
SequenceFeature spanningContact = new SequenceFeature("disulphide bond",
"", 2, 9, 0f, null);
seq0.addSequenceFeature(spanningContact); // should get shortened 3'
/*
* cut columns 3-6 (base 0), residues d-g 4-7
*/
Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
EditCommand.cut(ec, new AlignmentI[] { al });
List sfs = seq0.getSequenceFeatures();
SequenceFeatures.sortFeatures(sfs, true);
assertEquals(5, sfs.size()); // features internal to cut were deleted
SequenceFeature sf = sfs.get(0);
assertEquals("before", sf.getType());
assertEquals(1, sf.getBegin());
assertEquals(3, sf.getEnd());
sf = sfs.get(1);
assertEquals("disulphide bond", sf.getType());
assertEquals(2, sf.getBegin());
assertEquals(5, sf.getEnd()); // truncated by cut
sf = sfs.get(2);
assertEquals("overlap left", sf.getType());
assertEquals(2, sf.getBegin());
assertEquals(3, sf.getEnd()); // truncated by cut
sf = sfs.get(3);
assertEquals("after", sf.getType());
assertEquals(4, sf.getBegin()); // shifted left by cut
assertEquals(6, sf.getEnd()); // shifted left by cut
sf = sfs.get(4);
assertEquals("overlap right", sf.getType());
assertEquals(4, sf.getBegin()); // shifted left by cut
assertEquals(4, sf.getEnd()); // truncated by cut
}
/**
* Test a cut action's relocation of sequence features, with full coverage of
* all possible feature and cut locations for a 5-position ungapped sequence
*/
@Test(groups = { "Functional" })
public void testCut_withFeatures_exhaustive()
{
/*
* create a sequence features on each subrange of 1-5
*/
SequenceI seq0 = new Sequence("seq", "ABCDE");
int start = 8;
int end = 12;
seq0.setStart(start);
seq0.setEnd(end);
AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
alignment.setDataset(null);
/*
* create a new alignment with shared dataset sequence
*/
AlignmentI copy = new Alignment(
new SequenceI[]
{ alignment.getDataset().getSequenceAt(0).deriveSequence() });
SequenceI copySeq0 = copy.getSequenceAt(0);
for (int from = start; from <= end; from++)
{
for (int to = from; to <= end; to++)
{
String desc = String.format("%d-%d", from, to);
SequenceFeature sf = new SequenceFeature("test", desc, from, to, 0f,
null);
sf.setValue("from", Integer.valueOf(from));
sf.setValue("to", Integer.valueOf(to));
seq0.addSequenceFeature(sf);
}
}
// sanity check
List sfs = seq0.getSequenceFeatures();
assertEquals(func(5), sfs.size());
assertEquals(sfs, copySeq0.getSequenceFeatures());
String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
/*
* now perform all possible cuts of subranges of columns 1-5
* and validate the resulting remaining sequence features!
*/
SequenceI[] sqs = new SequenceI[] { seq0 };
for (int from = 0; from < seq0.getLength(); from++)
{
for (int to = from; to < seq0.getLength(); to++)
{
EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from,
(to - from + 1), alignment);
final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
.getDatasetSequence();
verifyCut(seq0, from, to, msg, start);
/*
* verify copy alignment dataset sequence unaffected
*/
assertEquals("Original dataset sequence was modified",
copySequenceFeatures,
copySeq0.getSequenceFeatures().toString());
/*
* verify any new dataset sequence was added to the
* alignment dataset
*/
assertEquals("Wrong Dataset size after " + msg,
newDatasetSequence ? 2 : 1,
alignment.getDataset().getHeight());
/*
* undo and verify all restored
*/
AlignmentI[] views = new AlignmentI[] { alignment };
ec.undoCommand(views);
sfs = seq0.getSequenceFeatures();
assertEquals("After undo of " + msg, func(5), sfs.size());
verifyUndo(from, to, sfs);
/*
* verify copy alignment dataset sequence still unaffected
* and alignment dataset has shrunk (if it was added to)
*/
assertEquals("Original dataset sequence was modified",
copySequenceFeatures,
copySeq0.getSequenceFeatures().toString());
assertEquals("Wrong Dataset size after Undo of " + msg, 1,
alignment.getDataset().getHeight());
/*
* redo and verify
*/
ec.doCommand(views);
verifyCut(seq0, from, to, msg, start);
/*
* verify copy alignment dataset sequence unaffected
* and any new dataset sequence readded to alignment dataset
*/
assertEquals("Original dataset sequence was modified",
copySequenceFeatures,
copySeq0.getSequenceFeatures().toString());
assertEquals("Wrong Dataset size after Redo of " + msg,
newDatasetSequence ? 2 : 1,
alignment.getDataset().getHeight());
/*
* undo ready for next cut
*/
ec.undoCommand(views);
/*
* final verify that copy alignment dataset sequence is still unaffected
* and that alignment dataset has shrunk
*/
assertEquals("Original dataset sequence was modified",
copySequenceFeatures,
copySeq0.getSequenceFeatures().toString());
assertEquals("Wrong Dataset size after final Undo of " + msg, 1,
alignment.getDataset().getHeight());
}
}
}
/**
* Verify by inspection that the sequence features left on the sequence after
* a cut match the expected results. The trick to this is that we can parse
* each feature's original start-end positions from its description.
*
* @param seq0
* @param from
* @param to
* @param msg
* @param seqStart
*/
protected void verifyCut(SequenceI seq0, int from, int to,
final String msg, int seqStart)
{
List sfs;
sfs = seq0.getSequenceFeatures();
Collections.sort(sfs, BY_DESCRIPTION);
/*
* confirm the number of features has reduced by the
* number of features within the cut region i.e. by
* func(length of cut); exception is a cut at start or end of sequence,
* which retains the original coordinates, dataset sequence
* and all its features
*/
boolean datasetRetained = from == 0 || to == 4;
if (datasetRetained)
{
// dataset and all features retained
assertEquals(msg, func(5), sfs.size());
}
else if (to - from == 4)
{
// all columns were cut
assertTrue(sfs.isEmpty());
}
else
{
// failure in checkFeatureRelocation is more informative!
assertEquals(msg + "wrong number of features left",
func(5) - func(to - from + 1), sfs.size());
}
/*
* inspect individual features
*/
for (SequenceFeature sf : sfs)
{
verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
seqStart);
}
}
/**
* Check that after Undo, every feature has start/end that match its original
* "start" and "end" properties
*
* @param from
* @param to
* @param sfs
*/
protected void verifyUndo(int from, int to, List sfs)
{
for (SequenceFeature sf : sfs)
{
final int oldFrom = ((Integer) sf.getValue("from")).intValue();
final int oldTo = ((Integer) sf.getValue("to")).intValue();
String msg = String.format("Undo cut of [%d-%d], feature at [%d-%d] ",
from + 1, to + 1, oldFrom, oldTo);
assertEquals(msg + "start", oldFrom, sf.getBegin());
assertEquals(msg + "end", oldTo, sf.getEnd());
}
}
/**
* Helper method to check a feature has been correctly relocated after a cut
*
* @param sf
* @param from
* start of cut (first residue cut 1..)
* @param to
* end of cut (last residue cut 1..)
* @param newDataset
* @param seqStart
*/
private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
boolean newDataset, int seqStart)
{
// TODO handle the gapped sequence case as well
int cutSize = to - from + 1;
final int oldFrom = ((Integer) sf.getValue("from")).intValue();
final int oldTo = ((Integer) sf.getValue("to")).intValue();
final int oldFromPosition = oldFrom - seqStart + 1; // 1..
final int oldToPosition = oldTo - seqStart + 1; // 1..
String msg = String.format(
"Feature %s relocated to %d-%d after cut of %d-%d",
sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
if (!newDataset)
{
// dataset retained with all features unchanged
assertEquals("0: " + msg, oldFrom, sf.getBegin());
assertEquals("0: " + msg, oldTo, sf.getEnd());
}
else if (oldToPosition < from)
{
// before cut region so unchanged
assertEquals("1: " + msg, oldFrom, sf.getBegin());
assertEquals("2: " + msg, oldTo, sf.getEnd());
}
else if (oldFromPosition > to)
{
// follows cut region - shift by size of cut
assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
sf.getBegin());
assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
sf.getEnd());
}
else if (oldFromPosition < from && oldToPosition > to)
{
// feature encloses cut region - shrink it right
assertEquals("5: " + msg, oldFrom, sf.getBegin());
assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
}
else if (oldFromPosition < from)
{
// feature overlaps left side of cut region - truncated right
assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
}
else if (oldToPosition > to)
{
// feature overlaps right side of cut region - truncated left
assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
sf.getBegin());
assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
sf.getEnd());
}
else
{
// feature internal to cut - should have been deleted!
Assert.fail(msg + " - should have been deleted");
}
}
/**
* Test a cut action's relocation of sequence features
*/
@Test(groups = { "Functional" })
public void testCut_withFeatures5prime()
{
SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
seq0.createDatasetSequence();
assertEquals(8, seq0.getStart());
seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f, null));
SequenceI[] seqsArray = new SequenceI[] { seq0 };
AlignmentI alignment = new Alignment(seqsArray);
/*
* cut columns of A-B; same dataset sequence is retained, aligned sequence
* start becomes 10
*/
Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
EditCommand.cut(ec, new AlignmentI[] { alignment });
/*
* feature on CC(10-11) should still be on CC(10-11)
*/
assertSame(seq0, alignment.getSequenceAt(0));
assertEquals(10, seq0.getStart());
List sfs = seq0.getSequenceFeatures();
assertEquals(1, sfs.size());
SequenceFeature sf = sfs.get(0);
assertEquals(10, sf.getBegin());
assertEquals(11, sf.getEnd());
}
}