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