View Javadoc
1   package com.acumenvelocity.ath.common;
2   
3   import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
4   import static org.junit.jupiter.api.Assertions.assertEquals;
5   import static org.junit.jupiter.api.Assertions.assertNotNull;
6   import static org.junit.jupiter.api.Assertions.assertTimeout;
7   import static org.junit.jupiter.api.Assertions.assertTrue;
8   
9   import java.time.Duration;
10  import java.util.ArrayList;
11  import java.util.List;
12  
13  import org.junit.jupiter.api.BeforeEach;
14  import org.junit.jupiter.api.DisplayName;
15  import org.junit.jupiter.api.Test;
16  
17  import net.sf.okapi.common.resource.Code;
18  import net.sf.okapi.common.resource.TextFragment;
19  
20  class RearrangeCodesTest {
21  
22    private TextFragment textFragment;
23    private List<Code> codes;
24  
25    @BeforeEach
26    void setUp() {
27      textFragment = new TextFragment();
28      codes = new ArrayList<>();
29    }
30  
31    @Test
32    @DisplayName("Should handle null codes list")
33    void testNullCodes() {
34      textFragment.setCodedText("Plain text", null);
35      assertDoesNotThrow(() -> OkapiUtil.rearrangeCodes(null, textFragment));
36    }
37  
38    @Test
39    @DisplayName("Should handle null target TextFragment")
40    void testNullTextFragment() {
41      assertDoesNotThrow(() -> OkapiUtil.rearrangeCodes(codes, null));
42    }
43  
44    @Test
45    @DisplayName("Should handle TextFragment without codes")
46    void testTextFragmentWithoutCodes() {
47      textFragment.setCodedText("Plain text without codes", codes);
48      assertDoesNotThrow(() -> OkapiUtil.rearrangeCodes(codes, textFragment));
49      assertEquals("Plain text without codes", textFragment.getCodedText());
50    }
51  
52    @Test
53    @DisplayName("Should not modify properly ordered codes")
54    void testProperlyOrderedCodes() {
55      Code open = textFragment.append(TextFragment.TagType.OPENING, "b", "<b>");
56      int id = open.getId();
57      textFragment.append("bold text");
58      textFragment.append(TextFragment.TagType.CLOSING, "b", "</b>", id);
59  
60      String original = textFragment.getCodedText();
61      OkapiUtil.rearrangeCodes(codes, textFragment);
62      assertEquals(original, textFragment.getCodedText());
63    }
64  
65    @Test
66    @DisplayName("Should swap closing code that comes before opening code")
67    void testSwapClosingBeforeOpening() {
68      textFragment.append("Text before ");
69  
70      // Create codes manually with same ID
71      List<Code> fragCodes = new ArrayList<>();
72      Code open = new Code(TextFragment.TagType.OPENING, "tag1", "<tag1>");
73      open.setId(1);
74      fragCodes.add(open);
75  
76      Code close = new Code(TextFragment.TagType.CLOSING, "tag1", "</tag1>");
77      close.setId(1);
78      fragCodes.add(close);
79  
80      // Build coded text with CLOSING before OPENING (wrong order)
81      char openMarkerId = TextFragment.toChar(0);
82      char closeMarkerId = TextFragment.toChar(1);
83  
84      String current = textFragment.getCodedText();
85      String badPart = String.valueOf((char) TextFragment.MARKER_CLOSING) + closeMarkerId
86          + " middle text "
87          + String.valueOf((char) TextFragment.MARKER_OPENING) + openMarkerId
88          + " text after";
89  
90      textFragment.setCodedText(current + badPart, fragCodes);
91  
92      // Verify the codes are in wrong order before fix
93      int openingIndex = textFragment.getIndexForOpening(1);
94      int closingIndex = textFragment.getIndexForClosing(1);
95      int openingPosBefore = textFragment.getCodePosition(openingIndex);
96      int closingPosBefore = textFragment.getCodePosition(closingIndex);
97  
98      assertTrue(closingPosBefore < openingPosBefore,
99          "Before fix: closing should come before opening");
100 
101     // Apply the fix
102     OkapiUtil.rearrangeCodes(codes, textFragment);
103 
104     // Verify codes are now in correct order
105     openingIndex = textFragment.getIndexForOpening(1);
106     closingIndex = textFragment.getIndexForClosing(1);
107     int openingPosAfter = textFragment.getCodePosition(openingIndex);
108     int closingPosAfter = textFragment.getCodePosition(closingIndex);
109 
110     assertTrue(openingPosAfter < closingPosAfter,
111         "After fix: opening code should come before closing code");
112   }
113 
114   @Test
115   @DisplayName("Should handle multiple swapped code pairs")
116   void testMultipleSwappedPairs() {
117     List<Code> fragCodes = new ArrayList<>();
118 
119     // Pair 1
120     Code open1 = new Code(TextFragment.TagType.OPENING, "tag1", "<tag1>");
121     open1.setId(1);
122     fragCodes.add(open1);
123     Code close1 = new Code(TextFragment.TagType.CLOSING, "tag1", "</tag1>");
124     close1.setId(1);
125     fragCodes.add(close1);
126 
127     // Pair 2
128     Code open2 = new Code(TextFragment.TagType.OPENING, "tag2", "<tag2>");
129     open2.setId(2);
130     fragCodes.add(open2);
131     Code close2 = new Code(TextFragment.TagType.CLOSING, "tag2", "</tag2>");
132     close2.setId(2);
133     fragCodes.add(close2);
134 
135     char open1Char = TextFragment.toChar(0);
136     char close1Char = TextFragment.toChar(1);
137     char open2Char = TextFragment.toChar(2);
138     char close2Char = TextFragment.toChar(3);
139 
140     // Build coded text with both pairs in wrong order
141     String all = "Start "
142         + (char) TextFragment.MARKER_CLOSING + close1Char + " middle1 "
143         + (char) TextFragment.MARKER_OPENING + open1Char + " middle2 "
144         + (char) TextFragment.MARKER_CLOSING + close2Char + " middle3 "
145         + (char) TextFragment.MARKER_OPENING + open2Char + " end";
146 
147     textFragment.setCodedText(all, fragCodes);
148 
149     // Apply the fix
150     OkapiUtil.rearrangeCodes(codes, textFragment);
151 
152     // Verify both pairs are now in correct order
153     int open1Index = textFragment.getIndexForOpening(1);
154     int close1Index = textFragment.getIndexForClosing(1);
155     int open2Index = textFragment.getIndexForOpening(2);
156     int close2Index = textFragment.getIndexForClosing(2);
157 
158     int o1Pos = textFragment.getCodePosition(open1Index);
159     int c1Pos = textFragment.getCodePosition(close1Index);
160     int o2Pos = textFragment.getCodePosition(open2Index);
161     int c2Pos = textFragment.getCodePosition(close2Index);
162 
163     assertTrue(o1Pos < c1Pos, "Tag1 opening should come before closing");
164     assertTrue(o2Pos < c2Pos, "Tag2 opening should come before closing");
165   }
166 
167   @Test
168   @DisplayName("Should handle nested codes correctly")
169   void testNestedCodes() {
170     Code outerOpen = textFragment.append(TextFragment.TagType.OPENING, "outer", "<outer>");
171     int outerId = outerOpen.getId();
172     textFragment.append("text1 ");
173 
174     Code innerOpen = textFragment.append(TextFragment.TagType.OPENING, "inner", "<inner>");
175     int innerId = innerOpen.getId();
176     textFragment.append("nested");
177     textFragment.append(TextFragment.TagType.CLOSING, "inner", "</inner>", innerId);
178 
179     textFragment.append(" text2");
180     textFragment.append(TextFragment.TagType.CLOSING, "outer", "</outer>", outerId);
181 
182     String before = textFragment.getCodedText();
183 
184     OkapiUtil.rearrangeCodes(codes, textFragment);
185 
186     // Properly nested codes should remain unchanged
187     assertEquals(before, textFragment.getCodedText());
188   }
189 
190   @Test
191   @DisplayName("Should stop after max iterations to prevent infinite loops")
192   void testMaxIterationsPreventsInfiniteLoop() {
193     List<Code> allCodes = new ArrayList<>();
194     StringBuilder sb = new StringBuilder();
195 
196     // Create many swapped pairs
197     for (int i = 0; i < 15; i++) {
198       Code open = new Code(TextFragment.TagType.OPENING, "tag" + i, "<tag" + i + ">");
199       open.setId(i + 1);
200       allCodes.add(open);
201 
202       Code close = new Code(TextFragment.TagType.CLOSING, "tag" + i, "</tag" + i + ">");
203       close.setId(i + 1);
204       allCodes.add(close);
205 
206       char closeChar = TextFragment.toChar(2 * i + 1);
207       char openChar = TextFragment.toChar(2 * i);
208 
209       // Build with closing before opening
210       sb.append((char) TextFragment.MARKER_CLOSING).append(closeChar).append(" x ")
211           .append((char) TextFragment.MARKER_OPENING).append(openChar);
212     }
213 
214     textFragment.setCodedText(sb.toString(), allCodes);
215 
216     // Should complete without hanging despite many swapped pairs
217     assertTimeout(Duration.ofSeconds(5),
218         () -> OkapiUtil.rearrangeCodes(codes, textFragment));
219   }
220 
221   @Test
222   @DisplayName("Should preserve text content during rearrangement")
223   void testPreservesTextContent() {
224     textFragment.append("Before ");
225 
226     List<Code> fragCodes = new ArrayList<>();
227     Code open = new Code(TextFragment.TagType.OPENING, "tag1", "<tag1>");
228     open.setId(1);
229     fragCodes.add(open);
230 
231     Code close = new Code(TextFragment.TagType.CLOSING, "tag1", "</tag1>");
232     close.setId(1);
233     fragCodes.add(close);
234 
235     char openChar = TextFragment.toChar(0);
236     char closeChar = TextFragment.toChar(1);
237 
238     String current = textFragment.getCodedText();
239     String badPart = String.valueOf((char) TextFragment.MARKER_CLOSING) + closeChar
240         + "middle"
241         + String.valueOf((char) TextFragment.MARKER_OPENING) + openChar
242         + " after";
243 
244     textFragment.setCodedText(current + badPart, fragCodes);
245 
246     // Get text before rearrangement
247     String beforeText = textFragment.getText();
248 
249     OkapiUtil.rearrangeCodes(codes, textFragment);
250 
251     // Text content should be preserved
252     String afterText = textFragment.getText();
253     assertEquals(beforeText, afterText, "Text content should be preserved");
254   }
255 
256   @Test
257   @DisplayName("Should handle empty TextFragment")
258   void testEmptyTextFragment() {
259     textFragment.setCodedText("", codes);
260     assertDoesNotThrow(() -> OkapiUtil.rearrangeCodes(codes, textFragment));
261     assertEquals("", textFragment.getCodedText());
262   }
263 
264   @Test
265   @DisplayName("Should handle single code without pair")
266   void testSingleCodeWithoutPair() {
267     textFragment.append("Text ");
268     textFragment.append(TextFragment.TagType.OPENING, "solo", "<solo>");
269     textFragment.append(" more text");
270 
271     assertDoesNotThrow(() -> OkapiUtil.rearrangeCodes(codes, textFragment));
272     assertNotNull(textFragment.getCodedText());
273 
274     // Single unpaired code should be balanced as isolated
275     String after = textFragment.getCodedText();
276     assertNotNull(after);
277   }
278 
279   @Test
280   @DisplayName("Should handle adjacent swapped codes")
281   void testAdjacentSwappedCodes() {
282     List<Code> fragCodes = new ArrayList<>();
283 
284     Code open = new Code(TextFragment.TagType.OPENING, "span", "<span>");
285     open.setId(1);
286     fragCodes.add(open);
287 
288     Code close = new Code(TextFragment.TagType.CLOSING, "span", "</span>");
289     close.setId(1);
290     fragCodes.add(close);
291 
292     char openChar = TextFragment.toChar(0);
293     char closeChar = TextFragment.toChar(1);
294 
295     // Closing immediately before opening with no text between
296     String coded = String.valueOf((char) TextFragment.MARKER_CLOSING) + closeChar
297         + String.valueOf((char) TextFragment.MARKER_OPENING) + openChar;
298 
299     textFragment.setCodedText(coded, fragCodes);
300 
301     OkapiUtil.rearrangeCodes(codes, textFragment);
302 
303     // Verify correct order after fix
304     int openingIndex = textFragment.getIndexForOpening(1);
305     int closingIndex = textFragment.getIndexForClosing(1);
306     int openingPos = textFragment.getCodePosition(openingIndex);
307     int closingPos = textFragment.getCodePosition(closingIndex);
308 
309     assertTrue(openingPos < closingPos, "Opening should come before closing");
310   }
311 }