1255c58c7823b2588cd5f05d8b900fa313361bb2
[jalview.git] / test / jalview / commands / EditCommandTest.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
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.
11  *  
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.
16  * 
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.
20  */
21 package jalview.commands;
22
23 import java.util.Locale;
24
25 import static org.testng.AssertJUnit.assertEquals;
26 import static org.testng.AssertJUnit.assertSame;
27 import static org.testng.AssertJUnit.assertTrue;
28
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;
38
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;
44 import java.util.Map;
45 import java.util.spi.LocaleServiceProvider;
46
47 import org.testng.Assert;
48 import org.testng.annotations.BeforeClass;
49 import org.testng.annotations.BeforeMethod;
50 import org.testng.annotations.Test;
51
52 /**
53  * Unit tests for EditCommand
54  * 
55  * @author gmcarstairs
56  *
57  */
58 public class EditCommandTest
59 {
60   private static Comparator<SequenceFeature> BY_DESCRIPTION = new Comparator<SequenceFeature>()
61   {
62
63     @Override
64     public int compare(SequenceFeature o1, SequenceFeature o2)
65     {
66       return o1.getDescription().compareTo(o2.getDescription());
67     }
68   };
69
70   private EditCommand testee;
71
72   private SequenceI[] seqs;
73
74   private Alignment al;
75
76   /*
77    * compute n(n+1)/2 e.g. 
78    * func(5) = 5 + 4 + 3 + 2 + 1 = 15
79    */
80   private static int func(int i)
81   {
82     return i * (i + 1) / 2;
83   }
84
85   @BeforeClass(alwaysRun = true)
86   public void setUpJvOptionPane()
87   {
88     JvOptionPane.setInteractiveMode(false);
89     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
90   }
91
92   @BeforeMethod(alwaysRun = true)
93   public void setUp()
94   {
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('?');
107   }
108
109   /**
110    * Test inserting gap characters
111    */
112   @Test(groups = { "Functional" })
113   public void testAppendEdit_insertGap()
114   {
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());
121
122     // todo: test for handling out of range positions?
123   }
124
125   /**
126    * Test deleting characters from sequences. Note the deleteGap() action does
127    * not check that only gap characters are being removed.
128    */
129   @Test(groups = { "Functional" })
130   public void testAppendEdit_deleteGap()
131   {
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());
137   }
138
139   /**
140    * Test a cut action. The command should store the cut characters to support
141    * undo.
142    */
143   @Test(groups = { "Functional" })
144   public void testCut()
145   {
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());
152
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
158   }
159
160   /**
161    * Test a Paste action, followed by Undo and Redo
162    */
163   @Test(groups = { "Functional" }, enabled = false)
164   public void testPaste_undo_redo()
165   {
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
171     // ...
172
173     SequenceI[] newSeqs = new SequenceI[2];
174     newSeqs[0] = new Sequence("newseq0", "ACEFKL");
175     newSeqs[1] = new Sequence("newseq1", "JWMPDH");
176
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());
182   }
183
184   /**
185    * Test insertGap followed by undo command
186    */
187   @Test(groups = { "Functional" })
188   public void testUndo_insertGap()
189   {
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());
199   }
200
201   /**
202    * Test deleteGap followed by undo command
203    */
204   @Test(groups = { "Functional" })
205   public void testUndo_deleteGap()
206   {
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());
216   }
217
218   /**
219    * Test several commands followed by an undo command
220    */
221   @Test(groups = { "Functional" })
222   public void testUndo_multipleCommands()
223   {
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());
228
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());
233
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());
238
239     // undo edit commands
240     testee.undoCommand(new AlignmentI[] { al });
241     assertEquals("ab?????hjk", seqs[0].getSequenceAsString());
242     assertEquals("12?????890", seqs[3].getSequenceAsString());
243   }
244
245   /**
246    * Unit test for JAL-1594 bug: click and drag sequence right to insert gaps -
247    * undo did not remove them all.
248    */
249   @Test(groups = { "Functional" })
250   public void testUndo_multipleInsertGaps()
251   {
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);
255
256     // undo edit commands
257     testee.undoCommand(new AlignmentI[] { al });
258     assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
259     assertEquals("1234567890", seqs[3].getSequenceAsString());
260
261   }
262
263   /**
264    * Test cut followed by undo command
265    */
266   @Test(groups = { "Functional" })
267   public void testUndo_cut()
268   {
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());
277   }
278
279   /**
280    * Test the replace command (used to manually edit a sequence)
281    */
282   @Test(groups = { "Functional" })
283   public void testReplace()
284   {
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] },
290             4, 8, al);
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());
298   }
299
300   /**
301    * Test the replace command (used to manually edit a sequence)
302    */
303   @Test(groups = { "Functional" })
304   public void testReplace_withGaps()
305   {
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());
311
312     /*
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
316      */
317     EditCommand edit = new EditCommand("", Action.REPLACE, "xyZ",
318             new SequenceI[]
319             { seq }, 2, 4, al);
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());
327
328     /*
329      * undo the edit
330      */
331     AlignmentI[] views = new AlignmentI[] {
332         new Alignment(new SequenceI[]
333         { seq }) };
334     edit.undoCommand(views);
335
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());
341
342     /*
343      * redo the edit
344      */
345     edit.doCommand(views);
346
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());
354
355   }
356
357   /**
358    * Test replace command when it doesn't cause a sequence edit (see comment in
359    */
360   @Test(groups = { "Functional" })
361   public void testReplaceFirstResiduesWithGaps()
362   {
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, "----",
367             new SequenceI[]
368             { seqs[1] }, 0, 4, al);
369
370     // trimmed start
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());
380
381     AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
382     // and undo
383     edit.undoCommand(views);
384
385     // dataset sequence unchanged
386     assertTrue(dsseq == seqs[1].getDatasetSequence());
387     // restore sequence
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());
392
393     // now redo
394     edit.undoCommand(views);
395
396     // and repeat asserts for the original edit
397
398     // trimmed start
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());
408
409   }
410
411   /**
412    * Test that the addEdit command correctly merges insert gap commands when
413    * possible.
414    */
415   @Test(groups = { "Functional" })
416   public void testAddEdit_multipleInsertGap()
417   {
418     /*
419      * 3 insert gap in a row (aka mouse drag right):
420      */
421     Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
422             new SequenceI[]
423             { seqs[0] }, 1, 1, al);
424     testee.addEdit(e);
425     SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
426     edited.setDatasetSequence(seqs[0].getDatasetSequence());
427     e = new EditCommand().new Edit(Action.INSERT_GAP,
428             new SequenceI[]
429             { edited }, 2, 1, al);
430     testee.addEdit(e);
431     edited = new Sequence("seq0", "a??bcdefghjk");
432     edited.setDatasetSequence(seqs[0].getDatasetSequence());
433     e = new EditCommand().new Edit(Action.INSERT_GAP,
434             new SequenceI[]
435             { edited }, 3, 1, al);
436     testee.addEdit(e);
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());
441
442     /*
443      * Add a non-contiguous edit - should not be merged.
444      */
445     e = new EditCommand().new Edit(Action.INSERT_GAP,
446             new SequenceI[]
447             { edited }, 5, 2, al);
448     testee.addEdit(e);
449     assertEquals(2, testee.getSize());
450     assertEquals(5, testee.getEdit(1).getPosition());
451     assertEquals(2, testee.getEdit(1).getNumber());
452
453     /*
454      * Add a Delete after the Insert - should not be merged.
455      */
456     e = new EditCommand().new Edit(Action.DELETE_GAP,
457             new SequenceI[]
458             { edited }, 6, 2, al);
459     testee.addEdit(e);
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());
464   }
465
466   /**
467    * Test that the addEdit command correctly merges delete gap commands when
468    * possible.
469    */
470   @Test(groups = { "Functional" })
471   public void testAddEdit_multipleDeleteGap()
472   {
473     /*
474      * 3 delete gap in a row (aka mouse drag left):
475      */
476     seqs[0].setSequence("a???bcdefghjk");
477     Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
478             new SequenceI[]
479             { seqs[0] }, 4, 1, al);
480     testee.addEdit(e);
481     assertEquals(1, testee.getSize());
482
483     SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
484     edited.setDatasetSequence(seqs[0].getDatasetSequence());
485     e = new EditCommand().new Edit(Action.DELETE_GAP,
486             new SequenceI[]
487             { edited }, 3, 1, al);
488     testee.addEdit(e);
489     assertEquals(1, testee.getSize());
490
491     edited = new Sequence("seq0", "a?bcdefghjk");
492     edited.setDatasetSequence(seqs[0].getDatasetSequence());
493     e = new EditCommand().new Edit(Action.DELETE_GAP,
494             new SequenceI[]
495             { edited }, 2, 1, al);
496     testee.addEdit(e);
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());
501
502     /*
503      * Add a non-contiguous edit - should not be merged.
504      */
505     e = new EditCommand().new Edit(Action.DELETE_GAP,
506             new SequenceI[]
507             { edited }, 2, 1, al);
508     testee.addEdit(e);
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());
513
514     /*
515      * Add an Insert after the Delete - should not be merged.
516      */
517     e = new EditCommand().new Edit(Action.INSERT_GAP,
518             new SequenceI[]
519             { edited }, 1, 1, al);
520     testee.addEdit(e);
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());
525   }
526
527   /**
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.
531    */
532   @Test(groups = { "Functional" })
533   public void testAddEdit_removeAllGaps()
534   {
535     seqs[0].setSequence("a???bcdefghjk");
536     Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
537             new SequenceI[]
538             { seqs[0] }, 4, 1, al);
539     testee.addEdit(e);
540
541     seqs[1].setSequence("f??ghjklmnopq");
542     Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP,
543             new SequenceI[]
544             { seqs[1] }, 3, 1, al);
545     testee.addEdit(e2);
546     assertEquals(2, testee.getSize());
547     assertSame(e, testee.getEdit(0));
548     assertSame(e2, testee.getEdit(1));
549   }
550
551   /**
552    * Test that the addEdit command correctly merges insert gap commands acting
553    * on a multi-sequence selection.
554    */
555   @Test(groups = { "Functional" })
556   public void testAddEdit_groupInsertGaps()
557   {
558     /*
559      * 2 insert gap in a row (aka mouse drag right), on two sequences:
560      */
561     Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
562             new SequenceI[]
563             { seqs[0], seqs[1] }, 1, 1, al);
564     testee.addEdit(e);
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,
570             new SequenceI[]
571             { seq1edited, seq2edited }, 2, 1, al);
572     testee.addEdit(e);
573
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());
582   }
583
584   /**
585    * Test for 'undoing' a series of gap insertions.
586    * <ul>
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>
591    * </ul>
592    */
593   @Test(groups = { "Functional" })
594   public void testPriorState_multipleInserts()
595   {
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, '-');
602     command.addEdit(e);
603     e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
604     command.addEdit(e);
605     e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
606     command.addEdit(e);
607
608     Map<SequenceI, SequenceI> unwound = command.priorState(false);
609     assertEquals("ABCDEF", unwound.get(ds).getSequenceAsString());
610   }
611
612   /**
613    * Test for 'undoing' a series of gap deletions.
614    * <ul>
615    * <li>Start: A-B-C delete 1 at pos 1</li>
616    * <li>Next: AB-C delete 1 at pos 2</li>
617    * <li>End: ABC</li>
618    * </ul>
619    */
620   @Test(groups = { "Functional" })
621   public void testPriorState_removeAllGaps()
622   {
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, '-');
629     command.addEdit(e);
630     e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
631     command.addEdit(e);
632
633     Map<SequenceI, SequenceI> unwound = command.priorState(false);
634     assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
635   }
636
637   /**
638    * Test for 'undoing' a single delete edit.
639    */
640   @Test(groups = { "Functional" })
641   public void testPriorState_singleDelete()
642   {
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, '-');
649     command.addEdit(e);
650
651     Map<SequenceI, SequenceI> unwound = command.priorState(false);
652     assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
653   }
654
655   /**
656    * Test 'undoing' a single gap insertion edit command.
657    */
658   @Test(groups = { "Functional" })
659   public void testPriorState_singleInsert()
660   {
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, '-');
667     command.addEdit(e);
668
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());
674   }
675
676   /**
677    * Test 'undoing' a single gap insertion edit command, on a sequence whose
678    * start residue is other than 1
679    */
680   @Test(groups = { "Functional" })
681   public void testPriorState_singleInsertWithOffset()
682   {
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, '-');
690     command.addEdit(e);
691
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());
697   }
698
699   /**
700    * Test that mimics 'remove all gaps' action. This generates delete gap edits
701    * for contiguous gaps in each sequence separately.
702    */
703   @Test(groups = { "Functional" })
704   public void testPriorState_removeGapsMultipleSeqs()
705   {
706     EditCommand command = new EditCommand();
707     String original1 = "--ABC-DEF";
708     String original2 = "FG-HI--J";
709     String original3 = "M-NOPQ";
710
711     /*
712      * Two edits for the first sequence
713      */
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, '-');
719     command.addEdit(e);
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, '-');
724     command.addEdit(e);
725
726     /*
727      * Two edits for the second sequence
728      */
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, '-');
734     command.addEdit(e);
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, '-');
739     command.addEdit(e);
740
741     /*
742      * One edit for the third sequence.
743      */
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, '-');
749     command.addEdit(e);
750
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());
755   }
756
757   /**
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
760    * column region.
761    */
762   @Test(groups = { "Functional" })
763   public void testPriorState_removeGappedCols()
764   {
765     EditCommand command = new EditCommand();
766     String original1 = "--ABC--DEF";
767     String original2 = "-G-HI--J";
768     String original3 = "-M-NO--PQ";
769
770     /*
771      * First edit deletes the first column.
772      */
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, '-');
784     command.addEdit(e);
785
786     /*
787      * Second edit deletes what is now columns 4 and 5.
788      */
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, '-');
797     command.addEdit(e);
798
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());
806   }
807
808   /**
809    * Test a cut action's relocation of sequence features
810    */
811   @Test(groups = { "Functional" })
812   public void testCut_withFeatures()
813   {
814     /*
815      * create sequence features before, after and overlapping
816      * a cut of columns/residues 4-7
817      */
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));
829
830     /*
831      * add some contact features
832      */
833     SequenceFeature internalContact = new SequenceFeature("disulphide bond",
834             "", 5, 6, 0f, null);
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",
843             "", 2, 9, 0f, null);
844     seq0.addSequenceFeature(spanningContact); // should get shortened 3'
845
846     /*
847      * cut columns 3-6 (base 0), residues d-g 4-7
848      */
849     Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
850     EditCommand.cut(ec, new AlignmentI[] { al });
851
852     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
853     SequenceFeatures.sortFeatures(sfs, true);
854
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());
860     sf = sfs.get(1);
861     assertEquals("disulphide bond", sf.getType());
862     assertEquals(2, sf.getBegin());
863     assertEquals(5, sf.getEnd()); // truncated by cut
864     sf = sfs.get(2);
865     assertEquals("overlap left", sf.getType());
866     assertEquals(2, sf.getBegin());
867     assertEquals(3, sf.getEnd()); // truncated by cut
868     sf = sfs.get(3);
869     assertEquals("after", sf.getType());
870     assertEquals(4, sf.getBegin()); // shifted left by cut
871     assertEquals(6, sf.getEnd()); // shifted left by cut
872     sf = sfs.get(4);
873     assertEquals("overlap right", sf.getType());
874     assertEquals(4, sf.getBegin()); // shifted left by cut
875     assertEquals(4, sf.getEnd()); // truncated by cut
876   }
877
878   /**
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
881    */
882   @Test(groups = { "Functional" })
883   public void testCut_withFeatures_exhaustive()
884   {
885     /*
886      * create a sequence features on each subrange of 1-5
887      */
888     SequenceI seq0 = new Sequence("seq", "ABCDE");
889     int start = 8;
890     int end = 12;
891     seq0.setStart(start);
892     seq0.setEnd(end);
893     AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
894     alignment.setDataset(null);
895
896     /*
897      * create a new alignment with shared dataset sequence
898      */
899     AlignmentI copy = new Alignment(
900             new SequenceI[]
901             { alignment.getDataset().getSequenceAt(0).deriveSequence() });
902     SequenceI copySeq0 = copy.getSequenceAt(0);
903
904     for (int from = start; from <= end; from++)
905     {
906       for (int to = from; to <= end; to++)
907       {
908         String desc = String.format("%d-%d", from, to);
909         SequenceFeature sf = new SequenceFeature("test", desc, from, to, 0f,
910                 null);
911         sf.setValue("from", Integer.valueOf(from));
912         sf.setValue("to", Integer.valueOf(to));
913         seq0.addSequenceFeature(sf);
914       }
915     }
916     // sanity check
917     List<SequenceFeature> sfs = seq0.getSequenceFeatures();
918     assertEquals(func(5), sfs.size());
919     assertEquals(sfs, copySeq0.getSequenceFeatures());
920     String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
921
922     /*
923      * now perform all possible cuts of subranges of columns 1-5
924      * and validate the resulting remaining sequence features!
925      */
926     SequenceI[] sqs = new SequenceI[] { seq0 };
927
928     for (int from = 0; from < seq0.getLength(); from++)
929     {
930       for (int to = from; to < seq0.getLength(); to++)
931       {
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();
937
938         verifyCut(seq0, from, to, msg, start);
939
940         /*
941          * verify copy alignment dataset sequence unaffected
942          */
943         assertEquals("Original dataset sequence was modified",
944                 copySequenceFeatures,
945                 copySeq0.getSequenceFeatures().toString());
946
947         /*
948          * verify any new dataset sequence was added to the
949          * alignment dataset
950          */
951         assertEquals("Wrong Dataset size after " + msg,
952                 newDatasetSequence ? 2 : 1,
953                 alignment.getDataset().getHeight());
954
955         /*
956          * undo and verify all restored
957          */
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);
963
964         /*
965          * verify copy alignment dataset sequence still unaffected
966          * and alignment dataset has shrunk (if it was added to)
967          */
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());
973
974         /*
975          * redo and verify
976          */
977         ec.doCommand(views);
978         verifyCut(seq0, from, to, msg, start);
979
980         /*
981          * verify copy alignment dataset sequence unaffected
982          * and any new dataset sequence readded to alignment dataset
983          */
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());
990
991         /*
992          * undo ready for next cut
993          */
994         ec.undoCommand(views);
995
996         /*
997          * final verify that copy alignment dataset sequence is still unaffected
998          * and that alignment dataset has shrunk
999          */
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());
1005       }
1006     }
1007   }
1008
1009   /**
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.
1013    * 
1014    * @param seq0
1015    * @param from
1016    * @param to
1017    * @param msg
1018    * @param seqStart
1019    */
1020   protected void verifyCut(SequenceI seq0, int from, int to,
1021           final String msg, int seqStart)
1022   {
1023     List<SequenceFeature> sfs;
1024     sfs = seq0.getSequenceFeatures();
1025
1026     Collections.sort(sfs, BY_DESCRIPTION);
1027
1028     /*
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
1034      */
1035     boolean datasetRetained = from == 0 || to == 4;
1036     if (datasetRetained)
1037     {
1038       // dataset and all features retained
1039       assertEquals(msg, func(5), sfs.size());
1040     }
1041     else if (to - from == 4)
1042     {
1043       // all columns were cut
1044       assertTrue(sfs.isEmpty());
1045     }
1046     else
1047     {
1048       // failure in checkFeatureRelocation is more informative!
1049       assertEquals(msg + "wrong number of features left",
1050               func(5) - func(to - from + 1), sfs.size());
1051     }
1052
1053     /*
1054      * inspect individual features
1055      */
1056     for (SequenceFeature sf : sfs)
1057     {
1058       verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
1059               seqStart);
1060     }
1061   }
1062
1063   /**
1064    * Check that after Undo, every feature has start/end that match its original
1065    * "start" and "end" properties
1066    * 
1067    * @param from
1068    * @param to
1069    * @param sfs
1070    */
1071   protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
1072   {
1073     for (SequenceFeature sf : sfs)
1074     {
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());
1081     }
1082   }
1083
1084   /**
1085    * Helper method to check a feature has been correctly relocated after a cut
1086    * 
1087    * @param sf
1088    * @param from
1089    *          start of cut (first residue cut 1..)
1090    * @param to
1091    *          end of cut (last residue cut 1..)
1092    * @param newDataset
1093    * @param seqStart
1094    */
1095   private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
1096           boolean newDataset, int seqStart)
1097   {
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..
1104
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);
1108     if (!newDataset)
1109     {
1110       // dataset retained with all features unchanged
1111       assertEquals("0: " + msg, oldFrom, sf.getBegin());
1112       assertEquals("0: " + msg, oldTo, sf.getEnd());
1113     }
1114     else if (oldToPosition < from)
1115     {
1116       // before cut region so unchanged
1117       assertEquals("1: " + msg, oldFrom, sf.getBegin());
1118       assertEquals("2: " + msg, oldTo, sf.getEnd());
1119     }
1120     else if (oldFromPosition > to)
1121     {
1122       // follows cut region - shift by size of cut
1123       assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1124               sf.getBegin());
1125       assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1126               sf.getEnd());
1127     }
1128     else if (oldFromPosition < from && oldToPosition > to)
1129     {
1130       // feature encloses cut region - shrink it right
1131       assertEquals("5: " + msg, oldFrom, sf.getBegin());
1132       assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1133     }
1134     else if (oldFromPosition < from)
1135     {
1136       // feature overlaps left side of cut region - truncated right
1137       assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1138     }
1139     else if (oldToPosition > to)
1140     {
1141       // feature overlaps right side of cut region - truncated left
1142       assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1143               sf.getBegin());
1144       assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
1145               sf.getEnd());
1146     }
1147     else
1148     {
1149       // feature internal to cut - should have been deleted!
1150       Assert.fail(msg + " - should have been deleted");
1151     }
1152   }
1153
1154   /**
1155    * Test a cut action's relocation of sequence features
1156    */
1157   @Test(groups = { "Functional" })
1158   public void testCut_withFeatures5prime()
1159   {
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);
1166
1167     /*
1168      * cut columns of A-B; same dataset sequence is retained, aligned sequence
1169      * start becomes 10
1170      */
1171     Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1172     EditCommand.cut(ec, new AlignmentI[] { alignment });
1173
1174     /*
1175      * feature on CC(10-11) should still be on CC(10-11)
1176      */
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());
1184   }
1185   private SequenceI mkDs(SequenceI as)
1186   {
1187     SequenceI ds = as.createDatasetSequence();
1188     ds.setSequence(ds.getSequenceAsString().toUpperCase(Locale.ROOT));
1189     return ds;
1190   }
1191   /**
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
1194    * column region.
1195    */
1196   @Test(groups = { "Functional" })
1197   public void testLeftRight_Justify_and_preserves_gaps()
1198   {
1199     EditCommand command = new EditCommand();
1200     String original1 = "--ABc--DEF";
1201     String original2 = "-G-Hi--J";
1202     String original3 = "-M-No--PQ";
1203
1204     /*
1205      * Set up the sequence array for operations
1206      */
1207     SequenceI seq1 = new Sequence("sq1", original1);
1208     SequenceI ds1 = mkDs(seq1);
1209     /*
1210      * and check we are preserving data - if the calls below fail, something has broken the Jalview dataset derivation process
1211      */
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));
1224     // check case
1225     assertEquals(exp,seq1.getSequenceAsString());
1226     // and other seqs
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());
1233     
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());
1242         
1243   }
1244 }