Clover icon

Coverage Report

  1. Project Clover database Tue Mar 10 2026 14:58:44 GMT
  2. Package jalview.commands

File EditCommandTest.java

 

Code metrics

36
508
35
1
1,249
795
54
0.11
14.51
35
1.54

Classes

Class Line # Actions
EditCommandTest 58 508 54
0.00%
 

Contributing tests

No tests hitting this source file were found.

Source view

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  0 toggle @Override
64    public int compare(SequenceFeature o1, SequenceFeature o2)
65    {
66  0 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  0 toggle private static int func(int i)
81    {
82  0 return i * (i + 1) / 2;
83    }
84   
 
85  0 toggle @BeforeClass(alwaysRun = true)
86    public void setUpJvOptionPane()
87    {
88  0 JvOptionPane.setInteractiveMode(false);
89  0 JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
90    }
91   
 
92  0 toggle @BeforeMethod(alwaysRun = true)
93    public void setUp()
94    {
95  0 testee = new EditCommand();
96  0 seqs = new SequenceI[4];
97  0 seqs[0] = new Sequence("seq0", "abcdefghjk");
98  0 seqs[0].setDatasetSequence(new Sequence("seq0ds", "ABCDEFGHJK"));
99  0 seqs[1] = new Sequence("seq1", "fghjklmnopq");
100  0 seqs[1].setDatasetSequence(new Sequence("seq1ds", "FGHJKLMNOPQ"));
101  0 seqs[2] = new Sequence("seq2", "qrstuvwxyz");
102  0 seqs[2].setDatasetSequence(new Sequence("seq2ds", "QRSTUVWXYZ"));
103  0 seqs[3] = new Sequence("seq3", "1234567890");
104  0 seqs[3].setDatasetSequence(new Sequence("seq3ds", "1234567890"));
105  0 al = new Alignment(seqs);
106  0 al.setGapCharacter('?');
107    }
108   
109    /**
110    * Test inserting gap characters
111    */
 
112  0 toggle @Test(groups = { "Functional" })
113    public void testAppendEdit_insertGap()
114    {
115    // set a non-standard gap character to prove it is actually used
116  0 testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true);
117  0 assertEquals("abcd???efghjk", seqs[0].getSequenceAsString());
118  0 assertEquals("fghj???klmnopq", seqs[1].getSequenceAsString());
119  0 assertEquals("qrst???uvwxyz", seqs[2].getSequenceAsString());
120  0 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  0 toggle @Test(groups = { "Functional" })
130    public void testAppendEdit_deleteGap()
131    {
132  0 testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true);
133  0 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
134  0 assertEquals("fghjnopq", seqs[1].getSequenceAsString());
135  0 assertEquals("qrstxyz", seqs[2].getSequenceAsString());
136  0 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  0 toggle @Test(groups = { "Functional" })
144    public void testCut()
145    {
146  0 Edit ec = testee.new Edit(Action.CUT, seqs, 4, 3, al);
147  0 EditCommand.cut(ec, new AlignmentI[] { al });
148  0 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
149  0 assertEquals("fghjnopq", seqs[1].getSequenceAsString());
150  0 assertEquals("qrstxyz", seqs[2].getSequenceAsString());
151  0 assertEquals("1234890", seqs[3].getSequenceAsString());
152   
153  0 assertEquals("efg", new String(ec.string[0]));
154  0 assertEquals("klm", new String(ec.string[1]));
155  0 assertEquals("uvw", new String(ec.string[2]));
156  0 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  0 toggle @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  0 SequenceI[] newSeqs = new SequenceI[2];
174  0 newSeqs[0] = new Sequence("newseq0", "ACEFKL");
175  0 newSeqs[1] = new Sequence("newseq1", "JWMPDH");
176   
177  0 new EditCommand("Paste", Action.PASTE, newSeqs, 0, al.getWidth(), al);
178  0 assertEquals(6, al.getSequences().size());
179  0 assertEquals("1234567890", seqs[3].getSequenceAsString());
180  0 assertEquals("ACEFKL", seqs[4].getSequenceAsString());
181  0 assertEquals("JWMPDH", seqs[5].getSequenceAsString());
182    }
183   
184    /**
185    * Test insertGap followed by undo command
186    */
 
187  0 toggle @Test(groups = { "Functional" })
188    public void testUndo_insertGap()
189    {
190    // Edit ec = testee.new Edit(Action.INSERT_GAP, seqs, 4, 3, '?');
191  0 testee.appendEdit(Action.INSERT_GAP, seqs, 4, 3, al, true);
192    // check something changed
193  0 assertEquals("abcd???efghjk", seqs[0].getSequenceAsString());
194  0 testee.undoCommand(new AlignmentI[] { al });
195  0 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
196  0 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
197  0 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
198  0 assertEquals("1234567890", seqs[3].getSequenceAsString());
199    }
200   
201    /**
202    * Test deleteGap followed by undo command
203    */
 
204  0 toggle @Test(groups = { "Functional" })
205    public void testUndo_deleteGap()
206    {
207  0 testee.appendEdit(Action.DELETE_GAP, seqs, 4, 3, al, true);
208    // check something changed
209  0 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
210  0 testee.undoCommand(new AlignmentI[] { al });
211    // deleteGap doesn't 'remember' deleted characters, only gaps get put back
212  0 assertEquals("abcd???hjk", seqs[0].getSequenceAsString());
213  0 assertEquals("fghj???nopq", seqs[1].getSequenceAsString());
214  0 assertEquals("qrst???xyz", seqs[2].getSequenceAsString());
215  0 assertEquals("1234???890", seqs[3].getSequenceAsString());
216    }
217   
218    /**
219    * Test several commands followed by an undo command
220    */
 
221  0 toggle @Test(groups = { "Functional" })
222    public void testUndo_multipleCommands()
223    {
224    // delete positions 3/4/5 (counting from 1)
225  0 testee.appendEdit(Action.DELETE_GAP, seqs, 2, 3, al, true);
226  0 assertEquals("abfghjk", seqs[0].getSequenceAsString());
227  0 assertEquals("1267890", seqs[3].getSequenceAsString());
228   
229    // insert 2 gaps after the second residue
230  0 testee.appendEdit(Action.INSERT_GAP, seqs, 2, 2, al, true);
231  0 assertEquals("ab??fghjk", seqs[0].getSequenceAsString());
232  0 assertEquals("12??67890", seqs[3].getSequenceAsString());
233   
234    // delete positions 4/5/6
235  0 testee.appendEdit(Action.DELETE_GAP, seqs, 3, 3, al, true);
236  0 assertEquals("ab?hjk", seqs[0].getSequenceAsString());
237  0 assertEquals("12?890", seqs[3].getSequenceAsString());
238   
239    // undo edit commands
240  0 testee.undoCommand(new AlignmentI[] { al });
241  0 assertEquals("ab?????hjk", seqs[0].getSequenceAsString());
242  0 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  0 toggle @Test(groups = { "Functional" })
250    public void testUndo_multipleInsertGaps()
251    {
252  0 testee.appendEdit(Action.INSERT_GAP, seqs, 4, 1, al, true);
253  0 testee.appendEdit(Action.INSERT_GAP, seqs, 5, 1, al, true);
254  0 testee.appendEdit(Action.INSERT_GAP, seqs, 6, 1, al, true);
255   
256    // undo edit commands
257  0 testee.undoCommand(new AlignmentI[] { al });
258  0 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
259  0 assertEquals("1234567890", seqs[3].getSequenceAsString());
260   
261    }
262   
263    /**
264    * Test cut followed by undo command
265    */
 
266  0 toggle @Test(groups = { "Functional" })
267    public void testUndo_cut()
268    {
269  0 testee.appendEdit(Action.CUT, seqs, 4, 3, al, true);
270    // check something changed
271  0 assertEquals("abcdhjk", seqs[0].getSequenceAsString());
272  0 testee.undoCommand(new AlignmentI[] { al });
273  0 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
274  0 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
275  0 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
276  0 assertEquals("1234567890", seqs[3].getSequenceAsString());
277    }
278   
279    /**
280    * Test the replace command (used to manually edit a sequence)
281    */
 
282  0 toggle @Test(groups = { "Functional" })
283    public void testReplace()
284    {
285    // seem to need a dataset sequence on the edited sequence here
286  0 seqs[1].createDatasetSequence();
287  0 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
288    // NB command.number holds end position for a Replace command
289  0 new EditCommand("", Action.REPLACE, "Z-xY", new SequenceI[] { seqs[1] },
290    4, 8, al);
291  0 assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
292  0 assertEquals("fghjZ-xYopq", seqs[1].getSequenceAsString());
293    // Dataset Sequence should always be uppercase
294  0 assertEquals("fghjZxYopq".toUpperCase(Locale.ROOT),
295    seqs[1].getDatasetSequence().getSequenceAsString());
296  0 assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
297  0 assertEquals("1234567890", seqs[3].getSequenceAsString());
298    }
299   
300    /**
301    * Test the replace command (used to manually edit a sequence)
302    */
 
303  0 toggle @Test(groups = { "Functional" })
304    public void testReplace_withGaps()
305    {
306  0 SequenceI seq = new Sequence("seq", "ABC--DEF");
307  0 seq.createDatasetSequence();
308  0 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
309  0 assertEquals(1, seq.getStart());
310  0 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  0 EditCommand edit = new EditCommand("", Action.REPLACE, "xyZ",
318    new SequenceI[]
319    { seq }, 2, 4, al);
320  0 assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
321  0 assertEquals(1, seq.getStart());
322  0 assertEquals(8, seq.getEnd());
323    // Dataset sequence always uppercase
324  0 assertEquals("ABxyZDEF".toUpperCase(Locale.ROOT),
325    seq.getDatasetSequence().getSequenceAsString());
326  0 assertEquals(8, seq.getDatasetSequence().getEnd());
327   
328    /*
329    * undo the edit
330    */
331  0 AlignmentI[] views = new AlignmentI[] {
332    new Alignment(new SequenceI[]
333    { seq }) };
334  0 edit.undoCommand(views);
335   
336  0 assertEquals("ABC--DEF", seq.getSequenceAsString());
337  0 assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
338  0 assertEquals(1, seq.getStart());
339  0 assertEquals(6, seq.getEnd());
340  0 assertEquals(6, seq.getDatasetSequence().getEnd());
341   
342    /*
343    * redo the edit
344    */
345  0 edit.doCommand(views);
346   
347  0 assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
348  0 assertEquals(1, seq.getStart());
349  0 assertEquals(8, seq.getEnd());
350    // dataset sequence should be Uppercase
351  0 assertEquals("ABxyZDEF".toUpperCase(Locale.ROOT),
352    seq.getDatasetSequence().getSequenceAsString());
353  0 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  0 toggle @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  0 SequenceI dsseq = seqs[1].getDatasetSequence();
366  0 EditCommand edit = new EditCommand("", Action.REPLACE, "----",
367    new SequenceI[]
368    { seqs[1] }, 0, 4, al);
369   
370    // trimmed start
371  0 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
372    // and ds is preserved
373  0 assertTrue(dsseq == seqs[1].getDatasetSequence());
374    // and it is unchanged and UPPERCASE !
375  0 assertEquals("fghjklmnopq".toUpperCase(Locale.ROOT),
376    dsseq.getSequenceAsString());
377    // and that alignment sequence start has been adjusted
378  0 assertEquals(5, seqs[1].getStart());
379  0 assertEquals(11, seqs[1].getEnd());
380   
381  0 AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
382    // and undo
383  0 edit.undoCommand(views);
384   
385    // dataset sequence unchanged
386  0 assertTrue(dsseq == seqs[1].getDatasetSequence());
387    // restore sequence
388  0 assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
389    // and start/end numbering also restored
390  0 assertEquals(1, seqs[1].getStart());
391  0 assertEquals(11, seqs[1].getEnd());
392   
393    // now redo
394  0 edit.undoCommand(views);
395   
396    // and repeat asserts for the original edit
397   
398    // trimmed start
399  0 assertEquals("----klmnopq", seqs[1].getSequenceAsString());
400    // and ds is preserved
401  0 assertTrue(dsseq == seqs[1].getDatasetSequence());
402    // and it is unchanged AND UPPERCASE !
403  0 assertEquals("fghjklmnopq".toUpperCase(Locale.ROOT),
404    dsseq.getSequenceAsString());
405    // and that alignment sequence start has been adjusted
406  0 assertEquals(5, seqs[1].getStart());
407  0 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  0 toggle @Test(groups = { "Functional" })
416    public void testAddEdit_multipleInsertGap()
417    {
418    /*
419    * 3 insert gap in a row (aka mouse drag right):
420    */
421  0 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
422    new SequenceI[]
423    { seqs[0] }, 1, 1, al);
424  0 testee.addEdit(e);
425  0 SequenceI edited = new Sequence("seq0", "a?bcdefghjk");
426  0 edited.setDatasetSequence(seqs[0].getDatasetSequence());
427  0 e = new EditCommand().new Edit(Action.INSERT_GAP,
428    new SequenceI[]
429    { edited }, 2, 1, al);
430  0 testee.addEdit(e);
431  0 edited = new Sequence("seq0", "a??bcdefghjk");
432  0 edited.setDatasetSequence(seqs[0].getDatasetSequence());
433  0 e = new EditCommand().new Edit(Action.INSERT_GAP,
434    new SequenceI[]
435    { edited }, 3, 1, al);
436  0 testee.addEdit(e);
437  0 assertEquals(1, testee.getSize());
438  0 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
439  0 assertEquals(1, testee.getEdit(0).getPosition());
440  0 assertEquals(3, testee.getEdit(0).getNumber());
441   
442    /*
443    * Add a non-contiguous edit - should not be merged.
444    */
445  0 e = new EditCommand().new Edit(Action.INSERT_GAP,
446    new SequenceI[]
447    { edited }, 5, 2, al);
448  0 testee.addEdit(e);
449  0 assertEquals(2, testee.getSize());
450  0 assertEquals(5, testee.getEdit(1).getPosition());
451  0 assertEquals(2, testee.getEdit(1).getNumber());
452   
453    /*
454    * Add a Delete after the Insert - should not be merged.
455    */
456  0 e = new EditCommand().new Edit(Action.DELETE_GAP,
457    new SequenceI[]
458    { edited }, 6, 2, al);
459  0 testee.addEdit(e);
460  0 assertEquals(3, testee.getSize());
461  0 assertEquals(Action.DELETE_GAP, testee.getEdit(2).getAction());
462  0 assertEquals(6, testee.getEdit(2).getPosition());
463  0 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  0 toggle @Test(groups = { "Functional" })
471    public void testAddEdit_multipleDeleteGap()
472    {
473    /*
474    * 3 delete gap in a row (aka mouse drag left):
475    */
476  0 seqs[0].setSequence("a???bcdefghjk");
477  0 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
478    new SequenceI[]
479    { seqs[0] }, 4, 1, al);
480  0 testee.addEdit(e);
481  0 assertEquals(1, testee.getSize());
482   
483  0 SequenceI edited = new Sequence("seq0", "a??bcdefghjk");
484  0 edited.setDatasetSequence(seqs[0].getDatasetSequence());
485  0 e = new EditCommand().new Edit(Action.DELETE_GAP,
486    new SequenceI[]
487    { edited }, 3, 1, al);
488  0 testee.addEdit(e);
489  0 assertEquals(1, testee.getSize());
490   
491  0 edited = new Sequence("seq0", "a?bcdefghjk");
492  0 edited.setDatasetSequence(seqs[0].getDatasetSequence());
493  0 e = new EditCommand().new Edit(Action.DELETE_GAP,
494    new SequenceI[]
495    { edited }, 2, 1, al);
496  0 testee.addEdit(e);
497  0 assertEquals(1, testee.getSize());
498  0 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
499  0 assertEquals(2, testee.getEdit(0).getPosition());
500  0 assertEquals(3, testee.getEdit(0).getNumber());
501   
502    /*
503    * Add a non-contiguous edit - should not be merged.
504    */
505  0 e = new EditCommand().new Edit(Action.DELETE_GAP,
506    new SequenceI[]
507    { edited }, 2, 1, al);
508  0 testee.addEdit(e);
509  0 assertEquals(2, testee.getSize());
510  0 assertEquals(Action.DELETE_GAP, testee.getEdit(0).getAction());
511  0 assertEquals(2, testee.getEdit(1).getPosition());
512  0 assertEquals(1, testee.getEdit(1).getNumber());
513   
514    /*
515    * Add an Insert after the Delete - should not be merged.
516    */
517  0 e = new EditCommand().new Edit(Action.INSERT_GAP,
518    new SequenceI[]
519    { edited }, 1, 1, al);
520  0 testee.addEdit(e);
521  0 assertEquals(3, testee.getSize());
522  0 assertEquals(Action.INSERT_GAP, testee.getEdit(2).getAction());
523  0 assertEquals(1, testee.getEdit(2).getPosition());
524  0 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  0 toggle @Test(groups = { "Functional" })
533    public void testAddEdit_removeAllGaps()
534    {
535  0 seqs[0].setSequence("a???bcdefghjk");
536  0 Edit e = new EditCommand().new Edit(Action.DELETE_GAP,
537    new SequenceI[]
538    { seqs[0] }, 4, 1, al);
539  0 testee.addEdit(e);
540   
541  0 seqs[1].setSequence("f??ghjklmnopq");
542  0 Edit e2 = new EditCommand().new Edit(Action.DELETE_GAP,
543    new SequenceI[]
544    { seqs[1] }, 3, 1, al);
545  0 testee.addEdit(e2);
546  0 assertEquals(2, testee.getSize());
547  0 assertSame(e, testee.getEdit(0));
548  0 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  0 toggle @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  0 Edit e = new EditCommand().new Edit(Action.INSERT_GAP,
562    new SequenceI[]
563    { seqs[0], seqs[1] }, 1, 1, al);
564  0 testee.addEdit(e);
565  0 SequenceI seq1edited = new Sequence("seq0", "a?bcdefghjk");
566  0 seq1edited.setDatasetSequence(seqs[0].getDatasetSequence());
567  0 SequenceI seq2edited = new Sequence("seq1", "f?ghjklmnopq");
568  0 seq2edited.setDatasetSequence(seqs[1].getDatasetSequence());
569  0 e = new EditCommand().new Edit(Action.INSERT_GAP,
570    new SequenceI[]
571    { seq1edited, seq2edited }, 2, 1, al);
572  0 testee.addEdit(e);
573   
574  0 assertEquals(1, testee.getSize());
575  0 assertEquals(Action.INSERT_GAP, testee.getEdit(0).getAction());
576  0 assertEquals(1, testee.getEdit(0).getPosition());
577  0 assertEquals(2, testee.getEdit(0).getNumber());
578  0 assertEquals(seqs[0].getDatasetSequence(),
579    testee.getEdit(0).getSequences()[0].getDatasetSequence());
580  0 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  0 toggle @Test(groups = { "Functional" })
594    public void testPriorState_multipleInserts()
595    {
596  0 EditCommand command = new EditCommand();
597  0 SequenceI seq = new Sequence("", "--A--B-CDEF");
598  0 SequenceI ds = new Sequence("", "ABCDEF");
599  0 seq.setDatasetSequence(ds);
600  0 SequenceI[] sqs = new SequenceI[] { seq };
601  0 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 1, 2, '-');
602  0 command.addEdit(e);
603  0 e = command.new Edit(Action.INSERT_GAP, sqs, 4, 1, '-');
604  0 command.addEdit(e);
605  0 e = command.new Edit(Action.INSERT_GAP, sqs, 0, 2, '-');
606  0 command.addEdit(e);
607   
608  0 Map<SequenceI, SequenceI> unwound = command.priorState(false);
609  0 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  0 toggle @Test(groups = { "Functional" })
621    public void testPriorState_removeAllGaps()
622    {
623  0 EditCommand command = new EditCommand();
624  0 SequenceI seq = new Sequence("", "ABC");
625  0 SequenceI ds = new Sequence("", "ABC");
626  0 seq.setDatasetSequence(ds);
627  0 SequenceI[] sqs = new SequenceI[] { seq };
628  0 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
629  0 command.addEdit(e);
630  0 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
631  0 command.addEdit(e);
632   
633  0 Map<SequenceI, SequenceI> unwound = command.priorState(false);
634  0 assertEquals("A-B-C", unwound.get(ds).getSequenceAsString());
635    }
636   
637    /**
638    * Test for 'undoing' a single delete edit.
639    */
 
640  0 toggle @Test(groups = { "Functional" })
641    public void testPriorState_singleDelete()
642    {
643  0 EditCommand command = new EditCommand();
644  0 SequenceI seq = new Sequence("", "ABCDEF");
645  0 SequenceI ds = new Sequence("", "ABCDEF");
646  0 seq.setDatasetSequence(ds);
647  0 SequenceI[] sqs = new SequenceI[] { seq };
648  0 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 2, 2, '-');
649  0 command.addEdit(e);
650   
651  0 Map<SequenceI, SequenceI> unwound = command.priorState(false);
652  0 assertEquals("AB--CDEF", unwound.get(ds).getSequenceAsString());
653    }
654   
655    /**
656    * Test 'undoing' a single gap insertion edit command.
657    */
 
658  0 toggle @Test(groups = { "Functional" })
659    public void testPriorState_singleInsert()
660    {
661  0 EditCommand command = new EditCommand();
662  0 SequenceI seq = new Sequence("", "AB---CDEF");
663  0 SequenceI ds = new Sequence("", "ABCDEF");
664  0 seq.setDatasetSequence(ds);
665  0 SequenceI[] sqs = new SequenceI[] { seq };
666  0 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
667  0 command.addEdit(e);
668   
669  0 Map<SequenceI, SequenceI> unwound = command.priorState(false);
670  0 SequenceI prior = unwound.get(ds);
671  0 assertEquals("ABCDEF", prior.getSequenceAsString());
672  0 assertEquals(1, prior.getStart());
673  0 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  0 toggle @Test(groups = { "Functional" })
681    public void testPriorState_singleInsertWithOffset()
682    {
683  0 EditCommand command = new EditCommand();
684  0 SequenceI seq = new Sequence("", "AB---CDEF", 8, 13);
685    // SequenceI ds = new Sequence("", "ABCDEF", 8, 13);
686    // seq.setDatasetSequence(ds);
687  0 seq.createDatasetSequence();
688  0 SequenceI[] sqs = new SequenceI[] { seq };
689  0 Edit e = command.new Edit(Action.INSERT_GAP, sqs, 2, 3, '-');
690  0 command.addEdit(e);
691   
692  0 Map<SequenceI, SequenceI> unwound = command.priorState(false);
693  0 SequenceI prior = unwound.get(seq.getDatasetSequence());
694  0 assertEquals("ABCDEF", prior.getSequenceAsString());
695  0 assertEquals(8, prior.getStart());
696  0 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  0 toggle @Test(groups = { "Functional" })
704    public void testPriorState_removeGapsMultipleSeqs()
705    {
706  0 EditCommand command = new EditCommand();
707  0 String original1 = "--ABC-DEF";
708  0 String original2 = "FG-HI--J";
709  0 String original3 = "M-NOPQ";
710   
711    /*
712    * Two edits for the first sequence
713    */
714  0 SequenceI seq = new Sequence("", "ABC-DEF");
715  0 SequenceI ds1 = new Sequence("", "ABCDEF");
716  0 seq.setDatasetSequence(ds1);
717  0 SequenceI[] sqs = new SequenceI[] { seq };
718  0 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 2, '-');
719  0 command.addEdit(e);
720  0 seq = new Sequence("", "ABCDEF");
721  0 seq.setDatasetSequence(ds1);
722  0 sqs = new SequenceI[] { seq };
723  0 e = command.new Edit(Action.DELETE_GAP, sqs, 3, 1, '-');
724  0 command.addEdit(e);
725   
726    /*
727    * Two edits for the second sequence
728    */
729  0 seq = new Sequence("", "FGHI--J");
730  0 SequenceI ds2 = new Sequence("", "FGHIJ");
731  0 seq.setDatasetSequence(ds2);
732  0 sqs = new SequenceI[] { seq };
733  0 e = command.new Edit(Action.DELETE_GAP, sqs, 2, 1, '-');
734  0 command.addEdit(e);
735  0 seq = new Sequence("", "FGHIJ");
736  0 seq.setDatasetSequence(ds2);
737  0 sqs = new SequenceI[] { seq };
738  0 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
739  0 command.addEdit(e);
740   
741    /*
742    * One edit for the third sequence.
743    */
744  0 seq = new Sequence("", "MNOPQ");
745  0 SequenceI ds3 = new Sequence("", "MNOPQ");
746  0 seq.setDatasetSequence(ds3);
747  0 sqs = new SequenceI[] { seq };
748  0 e = command.new Edit(Action.DELETE_GAP, sqs, 1, 1, '-');
749  0 command.addEdit(e);
750   
751  0 Map<SequenceI, SequenceI> unwound = command.priorState(false);
752  0 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
753  0 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
754  0 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  0 toggle @Test(groups = { "Functional" })
763    public void testPriorState_removeGappedCols()
764    {
765  0 EditCommand command = new EditCommand();
766  0 String original1 = "--ABC--DEF";
767  0 String original2 = "-G-HI--J";
768  0 String original3 = "-M-NO--PQ";
769   
770    /*
771    * First edit deletes the first column.
772    */
773  0 SequenceI seq1 = new Sequence("", "-ABC--DEF");
774  0 SequenceI ds1 = new Sequence("", "ABCDEF");
775  0 seq1.setDatasetSequence(ds1);
776  0 SequenceI seq2 = new Sequence("", "G-HI--J");
777  0 SequenceI ds2 = new Sequence("", "GHIJ");
778  0 seq2.setDatasetSequence(ds2);
779  0 SequenceI seq3 = new Sequence("", "M-NO--PQ");
780  0 SequenceI ds3 = new Sequence("", "MNOPQ");
781  0 seq3.setDatasetSequence(ds3);
782  0 SequenceI[] sqs = new SequenceI[] { seq1, seq2, seq3 };
783  0 Edit e = command.new Edit(Action.DELETE_GAP, sqs, 0, 1, '-');
784  0 command.addEdit(e);
785   
786    /*
787    * Second edit deletes what is now columns 4 and 5.
788    */
789  0 seq1 = new Sequence("", "-ABCDEF");
790  0 seq1.setDatasetSequence(ds1);
791  0 seq2 = new Sequence("", "G-HIJ");
792  0 seq2.setDatasetSequence(ds2);
793  0 seq3 = new Sequence("", "M-NOPQ");
794  0 seq3.setDatasetSequence(ds3);
795  0 sqs = new SequenceI[] { seq1, seq2, seq3 };
796  0 e = command.new Edit(Action.DELETE_GAP, sqs, 4, 2, '-');
797  0 command.addEdit(e);
798   
799  0 Map<SequenceI, SequenceI> unwound = command.priorState(false);
800  0 assertEquals(original1, unwound.get(ds1).getSequenceAsString());
801  0 assertEquals(original2, unwound.get(ds2).getSequenceAsString());
802  0 assertEquals(original3, unwound.get(ds3).getSequenceAsString());
803  0 assertEquals(ds1, unwound.get(ds1).getDatasetSequence());
804  0 assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
805  0 assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
806    }
807   
808    /**
809    * Test a cut action's relocation of sequence features
810    */
 
811  0 toggle @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  0 SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
819  0 seq0.addSequenceFeature(
820    new SequenceFeature("before", "", 1, 3, 0f, null));
821  0 seq0.addSequenceFeature(
822    new SequenceFeature("overlap left", "", 2, 6, 0f, null));
823  0 seq0.addSequenceFeature(
824    new SequenceFeature("internal", "", 5, 6, 0f, null));
825  0 seq0.addSequenceFeature(
826    new SequenceFeature("overlap right", "", 7, 8, 0f, null));
827  0 seq0.addSequenceFeature(
828    new SequenceFeature("after", "", 8, 10, 0f, null));
829   
830    /*
831    * add some contact features
832    */
833  0 SequenceFeature internalContact = new SequenceFeature("disulphide bond",
834    "", 5, 6, 0f, null);
835  0 seq0.addSequenceFeature(internalContact); // should get deleted
836  0 SequenceFeature overlapLeftContact = new SequenceFeature(
837    "disulphide bond", "", 2, 6, 0f, null);
838  0 seq0.addSequenceFeature(overlapLeftContact); // should get deleted
839  0 SequenceFeature overlapRightContact = new SequenceFeature(
840    "disulphide bond", "", 5, 8, 0f, null);
841  0 seq0.addSequenceFeature(overlapRightContact); // should get deleted
842  0 SequenceFeature spanningContact = new SequenceFeature("disulphide bond",
843    "", 2, 9, 0f, null);
844  0 seq0.addSequenceFeature(spanningContact); // should get shortened 3'
845   
846    /*
847    * cut columns 3-6 (base 0), residues d-g 4-7
848    */
849  0 Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
850  0 EditCommand.cut(ec, new AlignmentI[] { al });
851   
852  0 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
853  0 SequenceFeatures.sortFeatures(sfs, true);
854   
855  0 assertEquals(5, sfs.size()); // features internal to cut were deleted
856  0 SequenceFeature sf = sfs.get(0);
857  0 assertEquals("before", sf.getType());
858  0 assertEquals(1, sf.getBegin());
859  0 assertEquals(3, sf.getEnd());
860  0 sf = sfs.get(1);
861  0 assertEquals("disulphide bond", sf.getType());
862  0 assertEquals(2, sf.getBegin());
863  0 assertEquals(5, sf.getEnd()); // truncated by cut
864  0 sf = sfs.get(2);
865  0 assertEquals("overlap left", sf.getType());
866  0 assertEquals(2, sf.getBegin());
867  0 assertEquals(3, sf.getEnd()); // truncated by cut
868  0 sf = sfs.get(3);
869  0 assertEquals("after", sf.getType());
870  0 assertEquals(4, sf.getBegin()); // shifted left by cut
871  0 assertEquals(6, sf.getEnd()); // shifted left by cut
872  0 sf = sfs.get(4);
873  0 assertEquals("overlap right", sf.getType());
874  0 assertEquals(4, sf.getBegin()); // shifted left by cut
875  0 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  0 toggle @Test(groups = { "Functional" })
883    public void testCut_withFeatures_exhaustive()
884    {
885    /*
886    * create a sequence features on each subrange of 1-5
887    */
888  0 SequenceI seq0 = new Sequence("seq", "ABCDE");
889  0 int start = 8;
890  0 int end = 12;
891  0 seq0.setStart(start);
892  0 seq0.setEnd(end);
893  0 AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
894  0 alignment.setDataset(null);
895   
896    /*
897    * create a new alignment with shared dataset sequence
898    */
899  0 AlignmentI copy = new Alignment(
900    new SequenceI[]
901    { alignment.getDataset().getSequenceAt(0).deriveSequence() });
902  0 SequenceI copySeq0 = copy.getSequenceAt(0);
903   
904  0 for (int from = start; from <= end; from++)
905    {
906  0 for (int to = from; to <= end; to++)
907    {
908  0 String desc = String.format("%d-%d", from, to);
909  0 SequenceFeature sf = new SequenceFeature("test", desc, from, to, 0f,
910    null);
911  0 sf.setValue("from", Integer.valueOf(from));
912  0 sf.setValue("to", Integer.valueOf(to));
913  0 seq0.addSequenceFeature(sf);
914    }
915    }
916    // sanity check
917  0 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
918  0 assertEquals(func(5), sfs.size());
919  0 assertEquals(sfs, copySeq0.getSequenceFeatures());
920  0 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  0 SequenceI[] sqs = new SequenceI[] { seq0 };
927   
928  0 for (int from = 0; from < seq0.getLength(); from++)
929    {
930  0 for (int to = from; to < seq0.getLength(); to++)
931    {
932  0 EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from,
933    (to - from + 1), alignment);
934  0 final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
935  0 boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
936    .getDatasetSequence();
937   
938  0 verifyCut(seq0, from, to, msg, start);
939   
940    /*
941    * verify copy alignment dataset sequence unaffected
942    */
943  0 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  0 assertEquals("Wrong Dataset size after " + msg,
952  0 newDatasetSequence ? 2 : 1,
953    alignment.getDataset().getHeight());
954   
955    /*
956    * undo and verify all restored
957    */
958  0 AlignmentI[] views = new AlignmentI[] { alignment };
959  0 ec.undoCommand(views);
960  0 sfs = seq0.getSequenceFeatures();
961  0 assertEquals("After undo of " + msg, func(5), sfs.size());
962  0 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  0 assertEquals("Original dataset sequence was modified",
969    copySequenceFeatures,
970    copySeq0.getSequenceFeatures().toString());
971  0 assertEquals("Wrong Dataset size after Undo of " + msg, 1,
972    alignment.getDataset().getHeight());
973   
974    /*
975    * redo and verify
976    */
977  0 ec.doCommand(views);
978  0 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  0 assertEquals("Original dataset sequence was modified",
985    copySequenceFeatures,
986    copySeq0.getSequenceFeatures().toString());
987  0 assertEquals("Wrong Dataset size after Redo of " + msg,
988  0 newDatasetSequence ? 2 : 1,
989    alignment.getDataset().getHeight());
990   
991    /*
992    * undo ready for next cut
993    */
994  0 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  0 assertEquals("Original dataset sequence was modified",
1001    copySequenceFeatures,
1002    copySeq0.getSequenceFeatures().toString());
1003  0 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  0 toggle protected void verifyCut(SequenceI seq0, int from, int to,
1021    final String msg, int seqStart)
1022    {
1023  0 List<SequenceFeature> sfs;
1024  0 sfs = seq0.getSequenceFeatures();
1025   
1026  0 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  0 boolean datasetRetained = from == 0 || to == 4;
1036  0 if (datasetRetained)
1037    {
1038    // dataset and all features retained
1039  0 assertEquals(msg, func(5), sfs.size());
1040    }
1041  0 else if (to - from == 4)
1042    {
1043    // all columns were cut
1044  0 assertTrue(sfs.isEmpty());
1045    }
1046    else
1047    {
1048    // failure in checkFeatureRelocation is more informative!
1049  0 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  0 for (SequenceFeature sf : sfs)
1057    {
1058  0 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  0 toggle protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
1072    {
1073  0 for (SequenceFeature sf : sfs)
1074    {
1075  0 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1076  0 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1077  0 String msg = String.format("Undo cut of [%d-%d], feature at [%d-%d] ",
1078    from + 1, to + 1, oldFrom, oldTo);
1079  0 assertEquals(msg + "start", oldFrom, sf.getBegin());
1080  0 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  0 toggle 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  0 int cutSize = to - from + 1;
1100  0 final int oldFrom = ((Integer) sf.getValue("from")).intValue();
1101  0 final int oldTo = ((Integer) sf.getValue("to")).intValue();
1102  0 final int oldFromPosition = oldFrom - seqStart + 1; // 1..
1103  0 final int oldToPosition = oldTo - seqStart + 1; // 1..
1104   
1105  0 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  0 if (!newDataset)
1109    {
1110    // dataset retained with all features unchanged
1111  0 assertEquals("0: " + msg, oldFrom, sf.getBegin());
1112  0 assertEquals("0: " + msg, oldTo, sf.getEnd());
1113    }
1114  0 else if (oldToPosition < from)
1115    {
1116    // before cut region so unchanged
1117  0 assertEquals("1: " + msg, oldFrom, sf.getBegin());
1118  0 assertEquals("2: " + msg, oldTo, sf.getEnd());
1119    }
1120  0 else if (oldFromPosition > to)
1121    {
1122    // follows cut region - shift by size of cut
1123  0 assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
1124    sf.getBegin());
1125  0 assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
1126    sf.getEnd());
1127    }
1128  0 else if (oldFromPosition < from && oldToPosition > to)
1129    {
1130    // feature encloses cut region - shrink it right
1131  0 assertEquals("5: " + msg, oldFrom, sf.getBegin());
1132  0 assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
1133    }
1134  0 else if (oldFromPosition < from)
1135    {
1136    // feature overlaps left side of cut region - truncated right
1137  0 assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
1138    }
1139  0 else if (oldToPosition > to)
1140    {
1141    // feature overlaps right side of cut region - truncated left
1142  0 assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
1143    sf.getBegin());
1144  0 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  0 Assert.fail(msg + " - should have been deleted");
1151    }
1152    }
1153   
1154    /**
1155    * Test a cut action's relocation of sequence features
1156    */
 
1157  0 toggle @Test(groups = { "Functional" })
1158    public void testCut_withFeatures5prime()
1159    {
1160  0 SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
1161  0 seq0.createDatasetSequence();
1162  0 assertEquals(8, seq0.getStart());
1163  0 seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f, null));
1164  0 SequenceI[] seqsArray = new SequenceI[] { seq0 };
1165  0 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  0 Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
1172  0 EditCommand.cut(ec, new AlignmentI[] { alignment });
1173   
1174    /*
1175    * feature on CC(10-11) should still be on CC(10-11)
1176    */
1177  0 assertSame(seq0, alignment.getSequenceAt(0));
1178  0 assertEquals(10, seq0.getStart());
1179  0 List<SequenceFeature> sfs = seq0.getSequenceFeatures();
1180  0 assertEquals(1, sfs.size());
1181  0 SequenceFeature sf = sfs.get(0);
1182  0 assertEquals(10, sf.getBegin());
1183  0 assertEquals(11, sf.getEnd());
1184    }
1185   
 
1186  0 toggle private SequenceI mkDs(SequenceI as)
1187    {
1188  0 SequenceI ds = as.createDatasetSequence();
1189  0 ds.setSequence(ds.getSequenceAsString().toUpperCase(Locale.ROOT));
1190  0 return ds;
1191    }
1192   
1193    /**
1194    * Test that mimics 'remove all gapped columns' action. This generates a
1195    * series Delete Gap edits that each act on all sequences that share a gapped
1196    * column region.
1197    */
 
1198  0 toggle @Test(groups = { "Functional" })
1199    public void testLeftRight_Justify_and_preserves_gaps()
1200    {
1201  0 EditCommand command = new EditCommand();
1202  0 String original1 = "--ABc--DEF";
1203  0 String original2 = "-G-Hi--J";
1204  0 String original3 = "-M-No--PQ";
1205   
1206    /*
1207    * Set up the sequence array for operations
1208    */
1209  0 SequenceI seq1 = new Sequence("sq1", original1);
1210  0 SequenceI ds1 = mkDs(seq1);
1211    /*
1212    * and check we are preserving data - if the calls below fail, something has broken the Jalview dataset derivation process
1213    */
1214  0 assertEquals("ABCDEF", seq1.getDatasetSequence().getSequenceAsString());
1215  0 assertEquals(original1, seq1.getSequenceAsString());
1216  0 SequenceI seq2 = new Sequence("sq2", original2);
1217  0 SequenceI ds2 = mkDs(seq2);
1218  0 SequenceI seq3 = new Sequence("sq3", original3);
1219  0 SequenceI ds3 = mkDs(seq3);
1220  0 List<SequenceI> sqs = Arrays.asList(seq1, seq2, seq3);
1221  0 Alignment al = new Alignment(sqs.toArray(new SequenceI[0]));
1222  0 EditCommand lefj = new JustifyLeftOrRightCommand("Left J", true, sqs, 1,
1223    7, al);
1224  0 String exp = "-ABcD---EF";
1225    // check without case conservation
1226  0 assertEquals(exp.toUpperCase(Locale.ROOT),
1227    seq1.getSequenceAsString().toUpperCase(Locale.ROOT));
1228    // check case
1229  0 assertEquals(exp, seq1.getSequenceAsString());
1230    // and other seqs
1231  0 assertEquals("-GHiJ---", seq2.getSequenceAsString());
1232  0 assertEquals("-MNoP---Q", seq3.getSequenceAsString());
1233  0 lefj.undoCommand(new AlignmentI[] { al });
1234  0 assertEquals(original3, seq3.getSequenceAsString());
1235  0 assertEquals(original1, seq1.getSequenceAsString());
1236  0 assertEquals(original2, seq2.getSequenceAsString());
1237   
1238  0 EditCommand righj = new JustifyLeftOrRightCommand("Right J", false, sqs,
1239    2, 7, al);
1240  0 assertEquals("----ABcDEF", seq1.getSequenceAsString());
1241  0 assertEquals("-G---HiJ", seq2.getSequenceAsString());
1242  0 assertEquals("-M---NoPQ", seq3.getSequenceAsString());
1243  0 righj.undoCommand(new AlignmentI[] { al });
1244  0 assertEquals(original3, seq3.getSequenceAsString());
1245  0 assertEquals(original1, seq1.getSequenceAsString());
1246  0 assertEquals(original2, seq2.getSequenceAsString());
1247   
1248    }
1249    }