X-Git-Url: http://source.jalview.org/gitweb/?p=jalview.git;a=blobdiff_plain;f=test%2Fjalview%2Fcommands%2FEditCommandTest.java;fp=test%2Fjalview%2Fcommands%2FEditCommandTest.java;h=216065764cd647690d9c4b271b1f21c530819f95;hp=155f00eee8972351d8f38ecf5a66c123eb7aaa4e;hb=a677798c7fc2b2a143f8491b7279129e4367e053;hpb=4a0a4a012676c3ebee341d667eab07ea60c5770e diff --git a/test/jalview/commands/EditCommandTest.java b/test/jalview/commands/EditCommandTest.java index 155f00e..2160657 100644 --- a/test/jalview/commands/EditCommandTest.java +++ b/test/jalview/commands/EditCommandTest.java @@ -21,8 +21,8 @@ package jalview.commands; import static org.testng.AssertJUnit.assertEquals; -import static org.testng.AssertJUnit.assertNull; import static org.testng.AssertJUnit.assertSame; +import static org.testng.AssertJUnit.assertTrue; import jalview.commands.EditCommand.Action; import jalview.commands.EditCommand.Edit; @@ -34,6 +34,8 @@ 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; @@ -50,6 +52,22 @@ import org.testng.annotations.Test; */ 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 @@ -66,12 +84,6 @@ public class EditCommandTest JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION); } - private EditCommand testee; - - private SequenceI[] seqs; - - private Alignment al; - @BeforeMethod(alwaysRun = true) public void setUp() { @@ -141,18 +153,23 @@ public class EditCommandTest } /** - * Test a Paste action, where this adds sequences to an alignment. + * Test a Paste action, followed by Undo and Redo */ @Test(groups = { "Functional" }, enabled = false) - // TODO fix so it works - public void testPaste_addToAlignment() + 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"); - Edit ec = testee.new Edit(Action.PASTE, newSeqs, 0, al.getWidth(), al); - EditCommand.paste(ec, new AlignmentI[] { al }); + 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()); @@ -262,12 +279,123 @@ public class EditCommandTest { // seem to need a dataset sequence on the edited sequence here seqs[1].createDatasetSequence(); - new EditCommand("", Action.REPLACE, "ZXY", new SequenceI[] { seqs[1] }, + 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()); + assertEquals("fghjZxYopq", + seqs[1].getDatasetSequence().getSequenceAsString()); assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString()); assertEquals("1234567890", seqs[3].getSequenceAsString()); - seqs[1] = new Sequence("seq1", "fghjZXYnopq"); + } + + /** + * 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()); + assertEquals("ABxyZDEF", + 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()); + assertEquals("ABxyZDEF", + 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 + assertEquals("fghjklmnopq", 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 + assertEquals("fghjklmnopq", dsseq.getSequenceAsString()); + // and that alignment sequence start has been adjusted + assertEquals(5, seqs[1].getStart()); + assertEquals(11, seqs[1].getEnd()); + } /** @@ -663,7 +791,7 @@ public class EditCommandTest * create sequence features before, after and overlapping * a cut of columns/residues 4-7 */ - SequenceI seq0 = seqs[0]; + SequenceI seq0 = seqs[0]; // abcdefghjk/1-10 seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f, null)); seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6, @@ -675,29 +803,52 @@ public class EditCommandTest 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(4, sfs.size()); // feature internal to cut has been deleted + 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(2); - assertEquals("overlap right", sf.getType()); - assertEquals(4, sf.getBegin()); // shifted left by cut - assertEquals(5, 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 } /** @@ -711,16 +862,28 @@ public class EditCommandTest * 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); - for (int from = 1; from <= seq0.getLength(); from++) + + /* + * 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 <= seq0.getLength(); to++) + 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); + 0f, null); sf.setValue("from", Integer.valueOf(from)); sf.setValue("to", Integer.valueOf(to)); seq0.addSequenceFeature(sf); @@ -729,109 +892,234 @@ public class EditCommandTest // 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 1-5 (followed by Undo) + * now perform all possible cuts of subranges of columns 1-5 * and validate the resulting remaining sequence features! */ SequenceI[] sqs = new SequenceI[] { seq0 }; - // goal is to have this passing for all from/to values!! - // for (int from = 0; from < seq0.getLength(); from++) - // { - // for (int to = from; to < seq0.getLength(); to++) - for (int from = 1; from < 3; from++) + for (int from = 0; from < seq0.getLength(); from++) { - for (int to = 2; to < 3; to++) + for (int to = from; to < seq0.getLength(); to++) { - testee.appendEdit(Action.CUT, sqs, from, (to - from + 1), - alignment, true); + 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()); /* - * confirm the number of features has reduced by the - * number of features within the cut region i.e. by - * func(length of cut) + * redo and verify */ - String msg = String.format("Cut %d-%d ", from, to); - if (to - from == 4) - { - // all columns cut - assertNull(sfs); - } - else - { - assertEquals(msg + "wrong number of features left", func(5) - - func(to - from + 1), sfs.size()); - } + ec.doCommand(views); + verifyCut(seq0, from, to, msg, start); /* - * inspect individual features + * verify copy alignment dataset sequence unaffected + * and any new dataset sequence readded to alignment dataset */ - if (sfs != null) - { - for (SequenceFeature sf : sfs) - { - checkFeatureRelocation(sf, from + 1, to + 1); - } - } + 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 */ - testee.undoCommand(new AlignmentI[] { alignment }); - assertEquals(func(5), seq0.getSequenceFeatures().size()); + 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) + * start of cut (first residue cut 1..) * @param to - * end of cut (last residue cut) + * end of cut (last residue cut 1..) + * @param newDataset + * @param seqStart */ - private void checkFeatureRelocation(SequenceFeature sf, int from, int to) + 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; - int oldFrom = ((Integer) sf.getValue("from")).intValue(); - int oldTo = ((Integer) sf.getValue("to")).intValue(); + 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 (oldTo < from) + 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 (oldFrom > to) + else if (oldFromPosition > to) { // follows cut region - shift by size of cut - assertEquals("3: " + msg, oldFrom - cutSize, sf.getBegin()); - assertEquals("4: " + msg, oldTo - cutSize, sf.getEnd()); + assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom, + sf.getBegin()); + assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo, + sf.getEnd()); } - else if (oldFrom < from && oldTo > to) + 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 (oldFrom < from) + else if (oldFromPosition < from) { // feature overlaps left side of cut region - truncated right - assertEquals("7: " + msg, from - 1, sf.getEnd()); + assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd()); } - else if (oldTo > to) + else if (oldToPosition > to) { // feature overlaps right side of cut region - truncated left - assertEquals("8: " + msg, from, sf.getBegin()); - assertEquals("9: " + msg, from + oldTo - to - 1, sf.getEnd()); + assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1, + sf.getBegin()); + assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo, + sf.getEnd()); } else { @@ -844,30 +1132,32 @@ public class EditCommandTest * Test a cut action's relocation of sequence features */ @Test(groups = { "Functional" }) - public void testCut_gappedWithFeatures() + 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); + /* - * create sequence features before, after and overlapping - * a cut of columns/residues 4-7 + * cut columns of A-B; same dataset sequence is retained, aligned sequence + * start becomes 10 */ - SequenceI seq0 = new Sequence("seq", "A-BCC"); - seq0.addSequenceFeature(new SequenceFeature("", "", 3, 4, 0f, - null)); - AlignmentI alignment = new Alignment(new SequenceI[] { seq0 }); - // cut columns of A-B - Edit ec = testee.new Edit(Action.CUT, seqs, 0, 3, alignment); // cols 0-3 - // base 0 + Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment); EditCommand.cut(ec, new AlignmentI[] { alignment }); /* - * feature on CC(3-4) should now be on CC(1-2) + * 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(1, sf.getBegin()); - assertEquals(2, sf.getEnd()); - - // TODO add further cases including Undo - see JAL-2541 + assertEquals(10, sf.getBegin()); + assertEquals(11, sf.getEnd()); } }