1 package com.acumenvelocity.ath.common;
2
3 import static org.junit.jupiter.api.Assertions.assertEquals;
4 import static org.junit.jupiter.api.Assertions.assertFalse;
5 import static org.junit.jupiter.api.Assertions.assertNotNull;
6 import static org.junit.jupiter.api.Assertions.assertNull;
7 import static org.junit.jupiter.api.Assertions.assertTrue;
8
9 import java.util.ArrayList;
10 import java.util.List;
11
12 import org.junit.jupiter.api.DisplayName;
13 import org.junit.jupiter.api.Nested;
14 import org.junit.jupiter.api.Test;
15
16 import com.acumenvelocity.ath.common.AlignmentData.CombinedAlignmentOutput;
17 import com.acumenvelocity.ath.model.InlineCode;
18 import com.acumenvelocity.ath.model.x.LayeredTextX;
19 import com.fasterxml.jackson.databind.JsonNode;
20
21 import net.sf.okapi.common.LocaleId;
22 import net.sf.okapi.common.resource.Code;
23 import net.sf.okapi.common.resource.TextFragment;
24 import net.sf.okapi.common.resource.TextFragment.TagType;
25
26 class TestConversionUtil {
27
28 @Nested
29 @DisplayName("toTextFragment tests")
30 class ToTextFragmentTests {
31
32 @Test
33 @DisplayName("should return null when input is null")
34 void testNullInput() {
35 assertNull(ConversionUtil.toTextFragment(null));
36 }
37
38 @Test
39 @DisplayName("should convert simple text without codes")
40 void testSimpleTextWithoutCodes() {
41 LayeredTextX fs = new LayeredTextX();
42 fs.setText("Hello World");
43 fs.setCodes(new ArrayList<>());
44
45 TextFragment tf = ConversionUtil.toTextFragment(fs);
46
47 assertNotNull(tf);
48 assertEquals("Hello World", tf.getText());
49 assertFalse(tf.hasCode());
50 }
51
52 @Test
53 @DisplayName("should convert text with single placeholder code")
54 void testTextWithPlaceholderCode() {
55 LayeredTextX fs = new LayeredTextX();
56 fs.setText("Hello World");
57
58 List<InlineCode> codes = new ArrayList<>();
59 InlineCode ic = new InlineCode();
60 ic.setPosition(5);
61 ic.setId(1);
62 ic.setType("image");
63 ic.setTagType(TagType.PLACEHOLDER);
64 ic.setData("<img src='test.png'/>");
65 codes.add(ic);
66
67 fs.setCodes(codes);
68
69 TextFragment tf = ConversionUtil.toTextFragment(fs);
70
71 assertNotNull(tf);
72 assertEquals("Hello World", tf.getText());
73 assertTrue(tf.hasCode());
74 assertEquals(1, tf.getCodes().size());
75
76 Code code = tf.getCodes().get(0);
77 assertEquals(1, code.getId());
78 assertEquals("image", code.getType());
79 assertEquals("<img src='test.png'/>", code.getData());
80 assertEquals(TagType.PLACEHOLDER, code.getTagType());
81 }
82
83 @Test
84 @DisplayName("should convert text with opening and closing codes")
85 void testTextWithOpeningClosingCodes() {
86 LayeredTextX fs = new LayeredTextX();
87 fs.setText("Hello World");
88
89 List<InlineCode> codes = new ArrayList<>();
90
91 InlineCode opening = new InlineCode();
92 opening.setPosition(0);
93 opening.setId(1);
94 opening.setType("bold");
95 opening.setTagType(TagType.OPENING);
96 opening.setData("<b>");
97 codes.add(opening);
98
99 InlineCode closing = new InlineCode();
100 closing.setPosition(5);
101 closing.setId(1);
102 closing.setType("bold");
103 closing.setTagType(TagType.CLOSING);
104 closing.setData("</b>");
105 codes.add(closing);
106
107 fs.setCodes(codes);
108
109 TextFragment tf = ConversionUtil.toTextFragment(fs);
110
111 assertNotNull(tf);
112 assertEquals("Hello World", tf.getText());
113 assertTrue(tf.hasCode());
114 assertEquals(2, tf.getCodes().size());
115
116 assertEquals(TagType.OPENING, tf.getCodes().get(0).getTagType());
117 assertEquals(TagType.CLOSING, tf.getCodes().get(1).getTagType());
118 assertEquals(1, tf.getCodes().get(0).getId());
119 assertEquals(1, tf.getCodes().get(1).getId());
120 }
121
122 @Test
123 @DisplayName("round-trip should preserve paired opening/closing codes at end of text (position 2)")
124 void testRoundTripPairedCodesAtEndOfText() {
125
126 LayeredTextX original = new LayeredTextX();
127 original.setText("5.");
128
129 List<InlineCode> codes = new ArrayList<>();
130
131 InlineCode opening = new InlineCode();
132 opening.setPosition(2);
133 opening.setId(1);
134 opening.setType("run1");
135 opening.setTagType(TagType.OPENING);
136 opening.setData("<run1>");
137 codes.add(opening);
138
139 InlineCode closing = new InlineCode();
140 closing.setPosition(2);
141 closing.setId(1);
142 closing.setType("run1");
143 closing.setTagType(TagType.CLOSING);
144 closing.setData("</run1>");
145 codes.add(closing);
146
147 original.setCodes(codes);
148
149
150 TextFragment tf = ConversionUtil.toTextFragment(original);
151
152 assertNotNull(tf);
153 assertEquals("5.", tf.getText());
154 assertEquals("5.<run1></run1>", tf.toText());
155 assertEquals(2, tf.getCodes().size());
156
157
158 assertEquals(2, tf.getCodePosition(0));
159 assertEquals(4, tf.getCodePosition(1));
160
161
162 LayeredTextX result = ConversionUtil.toLayeredText(tf, LocaleId.ENGLISH);
163
164
165 assertNotNull(result);
166 assertEquals(original.getText(), result.getText());
167 assertEquals("5.<run1></run1>", result.getTextWithCodes());
168 assertEquals(2, result.getCodes().size());
169
170
171 assertEquals(2, result.getCodes().get(0).getPosition());
172 assertEquals(2, result.getCodes().get(1).getPosition());
173
174
175 InlineCode resultOpening = result.getCodes().get(0);
176 assertEquals(1, resultOpening.getId());
177 assertEquals("run1", resultOpening.getType());
178 assertEquals("<run1>", resultOpening.getData());
179 assertEquals(TagType.OPENING, resultOpening.getTagType());
180
181
182 InlineCode resultClosing = result.getCodes().get(1);
183 assertEquals(1, resultClosing.getId());
184 assertEquals("run1", resultClosing.getType());
185 assertEquals("</run1>", resultClosing.getData());
186 assertEquals(TagType.CLOSING, resultClosing.getTagType());
187 }
188
189 @Test
190 @DisplayName("should handle codes in non-sequential position order")
191 void testCodesOutOfOrder() {
192 LayeredTextX fs = new LayeredTextX();
193 fs.setText("Hello World");
194
195 List<InlineCode> codes = new ArrayList<>();
196
197
198 InlineCode closing = new InlineCode();
199 closing.setPosition(11);
200 closing.setId(1);
201 closing.setType("bold");
202 closing.setTagType(TagType.CLOSING);
203 closing.setData("</b>");
204 codes.add(closing);
205
206
207 InlineCode opening = new InlineCode();
208 opening.setPosition(0);
209 opening.setId(1);
210 opening.setType("bold");
211 opening.setTagType(TagType.OPENING);
212 opening.setData("<b>");
213 codes.add(opening);
214
215 fs.setCodes(codes);
216
217 TextFragment tf = ConversionUtil.toTextFragment(fs);
218
219 assertNotNull(tf);
220 assertEquals("Hello World", tf.getText());
221 assertTrue(tf.hasCode());
222 assertEquals(2, tf.getCodes().size());
223 }
224
225 @Test
226 @DisplayName("should convert flags correctly")
227 void testFlagConversion() {
228 LayeredTextX fs = new LayeredTextX();
229 fs.setText("Test");
230
231 List<InlineCode> codes = new ArrayList<>();
232 InlineCode ic = new InlineCode();
233 ic.setPosition(0);
234 ic.setId(1);
235 ic.setType("test");
236 ic.setTagType(TagType.PLACEHOLDER);
237 ic.setData("<test/>");
238 ic.setFlag(0x01 | 0x02 | 0x04);
239 codes.add(ic);
240
241 fs.setCodes(codes);
242
243 TextFragment tf = ConversionUtil.toTextFragment(fs);
244
245 Code code = tf.getCodes().get(0);
246 assertTrue(code.hasReference());
247 assertTrue(code.isCloneable());
248 assertTrue(code.isDeleteable());
249 }
250
251 @Test
252 @DisplayName("should preserve additional properties")
253 void testAdditionalProperties() {
254 LayeredTextX fs = new LayeredTextX();
255 fs.setText("Test");
256
257 List<InlineCode> codes = new ArrayList<>();
258 InlineCode ic = new InlineCode();
259 ic.setPosition(0);
260 ic.setId(5);
261 ic.setType("link");
262 ic.setTagType(TagType.PLACEHOLDER);
263 ic.setData("<a href='test'>");
264 ic.setOuterData("<a href='test'>");
265 ic.setDisplayText("Link");
266 ic.setOriginalId("abc123");
267 codes.add(ic);
268
269 fs.setCodes(codes);
270
271 TextFragment tf = ConversionUtil.toTextFragment(fs);
272
273 Code code = tf.getCodes().get(0);
274 assertEquals(5, code.getId());
275 assertEquals("<a href='test'>", code.getOuterData());
276 assertEquals("Link", code.getDisplayText());
277 assertEquals("abc123", code.getOriginalId());
278 }
279
280 @Test
281 @DisplayName("should handle empty text")
282 void testEmptyText() {
283 LayeredTextX fs = new LayeredTextX();
284 fs.setText("");
285 fs.setCodes(new ArrayList<>());
286
287 TextFragment tf = ConversionUtil.toTextFragment(fs);
288
289 assertNotNull(tf);
290 assertEquals("", tf.getText());
291 }
292
293 @Test
294 @DisplayName("should handle null text")
295 void testNullText() {
296 LayeredTextX fs = new LayeredTextX();
297 fs.setText(null);
298 fs.setCodes(new ArrayList<>());
299
300 TextFragment tf = ConversionUtil.toTextFragment(fs);
301
302 assertNotNull(tf);
303 assertEquals("", tf.getText());
304 }
305 }
306
307 @Nested
308 @DisplayName("toLayeredText tests")
309 class ToLayeredTextTests {
310
311 @Test
312 @DisplayName("should return null when input is null")
313 void testNullInput() {
314 assertNull(ConversionUtil.toLayeredText(null, LocaleId.ENGLISH));
315 }
316
317 @Test
318 @DisplayName("should convert simple text without codes")
319 void testSimpleTextWithoutCodes() {
320 TextFragment tf = new TextFragment("Hello World");
321
322 LayeredTextX fs = ConversionUtil.toLayeredText(tf, LocaleId.ENGLISH);
323
324 assertNotNull(fs);
325 assertEquals("Hello World", fs.getText());
326 assertEquals("Hello World", fs.getTextWithCodes());
327 assertTrue(fs.getCodes().isEmpty());
328 }
329
330 @Test
331 @DisplayName("should convert text with single placeholder code")
332 void testTextWithPlaceholderCode() {
333 TextFragment tf = new TextFragment("Hello ");
334 tf.append(TagType.PLACEHOLDER, "image", "<img src='test.png'/>");
335 tf.append(" World");
336
337 LayeredTextX fs = ConversionUtil.toLayeredText(tf, LocaleId.ENGLISH);
338
339 assertNotNull(fs);
340 assertEquals("Hello World", fs.getText());
341 assertEquals(1, fs.getCodes().size());
342
343 InlineCode ic = fs.getCodes().get(0);
344 assertEquals(6, ic.getPosition());
345 assertEquals("image", ic.getType());
346 assertEquals("<img src='test.png'/>", ic.getData());
347 assertEquals(TagType.PLACEHOLDER, ic.getTagType());
348 }
349
350 @Test
351 @DisplayName("should convert text with opening and closing codes")
352 void testTextWithOpeningClosingCodes() {
353 TextFragment tf = new TextFragment();
354 tf.append(TagType.OPENING, "bold", "<b>", 1);
355 tf.append("Hello");
356 tf.append(TagType.CLOSING, "bold", "</b>", 1);
357 tf.append(" World");
358
359 LayeredTextX fs = ConversionUtil.toLayeredText(tf, LocaleId.ENGLISH);
360
361 assertNotNull(fs);
362 assertEquals("Hello World", fs.getText());
363 assertEquals(2, fs.getCodes().size());
364
365 InlineCode opening = fs.getCodes().get(0);
366 assertEquals(0, opening.getPosition());
367 assertEquals(1, opening.getId());
368 assertEquals("bold", opening.getType());
369 assertEquals(TagType.OPENING, opening.getTagType());
370
371 InlineCode closing = fs.getCodes().get(1);
372 assertEquals(5, closing.getPosition());
373 assertEquals(1, closing.getId());
374 assertEquals("bold", closing.getType());
375 assertEquals(TagType.CLOSING, closing.getTagType());
376 }
377
378 @Test
379 @DisplayName("should calculate positions correctly with multiple codes")
380 void testPositionCalculation() {
381 TextFragment tf = new TextFragment();
382 tf.append("AB");
383 tf.append(TagType.PLACEHOLDER, "code1", "<c1/>", 1);
384 tf.append("CD");
385 tf.append(TagType.PLACEHOLDER, "code2", "<c2/>", 2);
386 tf.append("EF");
387
388 LayeredTextX fs = ConversionUtil.toLayeredText(tf, LocaleId.ENGLISH);
389
390 assertEquals("ABCDEF", fs.getText());
391 assertEquals(2, fs.getCodes().size());
392
393 assertEquals(2, fs.getCodes().get(0).getPosition());
394 assertEquals(4, fs.getCodes().get(1).getPosition());
395 }
396
397 @Test
398 @DisplayName("should convert flags correctly")
399 void testFlagConversion() {
400 TextFragment tf = new TextFragment();
401 Code code = tf.append(TagType.PLACEHOLDER, "test", "<test/>");
402 code.setReferenceFlag(true);
403 code.setCloneable(true);
404 code.setDeleteable(true);
405
406 LayeredTextX fs = ConversionUtil.toLayeredText(tf, LocaleId.ENGLISH);
407
408 InlineCode ic = fs.getCodes().get(0);
409 assertEquals(0x07, ic.getFlag());
410 assertTrue((ic.getFlag() & 0x01) != 0);
411 assertTrue((ic.getFlag() & 0x02) != 0);
412 assertTrue((ic.getFlag() & 0x04) != 0);
413 }
414
415 @Test
416 @DisplayName("should preserve additional properties")
417 void testAdditionalProperties() {
418 TextFragment tf = new TextFragment();
419 Code code = tf.append(TagType.PLACEHOLDER, "link", "<a href='test'>", 5);
420 code.setOuterData("<a href='test'>");
421 code.setDisplayText("Link");
422 code.setOriginalId("abc123");
423
424 LayeredTextX fs = ConversionUtil.toLayeredText(tf, LocaleId.ENGLISH);
425
426 InlineCode ic = fs.getCodes().get(0);
427 assertEquals(5, ic.getId());
428 assertEquals("<a href='test'>", ic.getOuterData());
429 assertEquals("Link", ic.getDisplayText());
430 assertEquals("abc123", ic.getOriginalId());
431 }
432
433 @Test
434 @DisplayName("should handle empty text")
435 void testEmptyText() {
436 TextFragment tf = new TextFragment("");
437
438 LayeredTextX fs = ConversionUtil.toLayeredText(tf, LocaleId.ENGLISH);
439
440 assertNotNull(fs);
441 assertEquals("", fs.getText());
442 assertTrue(fs.getCodes().isEmpty());
443 }
444
445 @Test
446 @DisplayName("should set textWithCodes correctly")
447 void testTextWithCodes() {
448 TextFragment tf = new TextFragment();
449 tf.append(TagType.OPENING, "bold", "<b>", 1);
450 tf.append("Hello");
451 tf.append(TagType.CLOSING, "bold", "</b>", 1);
452
453 LayeredTextX fs = ConversionUtil.toLayeredText(tf, LocaleId.ENGLISH);
454
455 assertEquals("<b>Hello</b>", fs.getTextWithCodes());
456 }
457 }
458
459 @Nested
460 @DisplayName("Round-trip conversion tests")
461 class RoundTripTests {
462
463 @Test
464 @DisplayName("should maintain data integrity through round-trip conversion")
465 void testRoundTripSimple() {
466 LayeredTextX original = new LayeredTextX();
467 original.setText("Hello World");
468
469 List<InlineCode> codes = new ArrayList<>();
470 InlineCode ic = new InlineCode();
471 ic.setPosition(6);
472 ic.setId(1);
473 ic.setType("emphasis");
474 ic.setTagType(TagType.PLACEHOLDER);
475 ic.setData("<em/>");
476 codes.add(ic);
477
478 original.setCodes(codes);
479
480 TextFragment tf = ConversionUtil.toTextFragment(original);
481 LayeredTextX result = ConversionUtil.toLayeredText(tf, LocaleId.ENGLISH);
482
483 assertEquals(original.getText(), result.getText());
484 assertEquals(original.getCodes().size(), result.getCodes().size());
485
486 InlineCode resultCode = result.getCodes().get(0);
487 InlineCode originalCode = original.getCodes().get(0);
488
489 assertEquals(originalCode.getPosition(), resultCode.getPosition());
490 assertEquals(originalCode.getType(), resultCode.getType());
491 assertEquals(originalCode.getData(), resultCode.getData());
492 assertEquals(originalCode.getTagType(), resultCode.getTagType());
493 }
494
495 @Test
496 @DisplayName("should maintain paired codes through round-trip")
497 void testRoundTripPairedCodes() {
498 LayeredTextX original = new LayeredTextX();
499 original.setText("Bold text here");
500
501 List<InlineCode> codes = new ArrayList<>();
502
503 InlineCode opening = new InlineCode();
504 opening.setPosition(0);
505 opening.setId(1);
506 opening.setType("bold");
507 opening.setTagType(TagType.OPENING);
508 opening.setData("<b>");
509 codes.add(opening);
510
511 InlineCode closing = new InlineCode();
512 closing.setPosition(4);
513 closing.setId(1);
514 closing.setType("bold");
515 closing.setTagType(TagType.CLOSING);
516 closing.setData("</b>");
517 codes.add(closing);
518
519 original.setCodes(codes);
520
521 TextFragment tf = ConversionUtil.toTextFragment(original);
522 LayeredTextX result = ConversionUtil.toLayeredText(tf, LocaleId.ENGLISH);
523
524 assertEquals(original.getText(), result.getText());
525 assertEquals(2, result.getCodes().size());
526
527 assertEquals(TagType.OPENING, result.getCodes().get(0).getTagType());
528 assertEquals(TagType.CLOSING, result.getCodes().get(1).getTagType());
529 assertEquals(result.getCodes().get(0).getId(), result.getCodes().get(1).getId());
530 }
531
532 @Test
533 @DisplayName("should preserve all properties through round-trip")
534 void testRoundTripAllProperties() {
535 LayeredTextX original = new LayeredTextX();
536 original.setText("Test");
537
538 List<InlineCode> codes = new ArrayList<>();
539 InlineCode ic = new InlineCode();
540 ic.setPosition(0);
541 ic.setId(5);
542 ic.setType("link");
543 ic.setTagType(TagType.PLACEHOLDER);
544 ic.setData("<a>");
545 ic.setOuterData("<a>");
546 ic.setDisplayText("Link");
547 ic.setOriginalId("link1");
548 ic.setFlag(0x07);
549 codes.add(ic);
550
551 original.setCodes(codes);
552
553 TextFragment tf = ConversionUtil.toTextFragment(original);
554 LayeredTextX result = ConversionUtil.toLayeredText(tf, LocaleId.ENGLISH);
555
556 InlineCode resultCode = result.getCodes().get(0);
557
558 assertEquals(ic.getId(), resultCode.getId());
559 assertEquals(ic.getType(), resultCode.getType());
560 assertEquals(ic.getData(), resultCode.getData());
561 assertEquals(ic.getOuterData(), resultCode.getOuterData());
562 assertEquals(ic.getDisplayText(), resultCode.getDisplayText());
563 assertEquals(ic.getOriginalId(), resultCode.getOriginalId());
564 assertEquals(ic.getFlag(), resultCode.getFlag());
565 }
566 }
567
568
569
570
571
572 @Nested
573 @DisplayName("JacksonUtil.fromJson tests")
574 class JacksonUtilTests {
575
576 private static final String COMBINED_ALIGNMENT_JSON = "{\n" +
577 " \"alignments\": [\n" +
578 " {\n" +
579 " \"paragraphAlignment\": {\n" +
580 " \"type\": \"MULTI_MATCH\",\n" +
581 " \"sourceParagraphPositions\": [ 0 ],\n" +
582 " \"targetParagraphPositions\": [ 0, 1 ]\n" +
583 " },\n" +
584 " \"sentenceAlignments\": [\n" +
585 " {\n" +
586 " \"type\": \"MULTI_MATCH\",\n" +
587 " \"sourcePositions\": [ 0 ],\n" +
588 " \"targetPositions\": [ 0, 1 ]\n" +
589 " }\n" +
590 " ]\n" +
591 " },\n" +
592 " {\n" +
593 " \"paragraphAlignment\": {\n" +
594 " \"type\": \"MULTI_MATCH\",\n" +
595 " \"sourceParagraphPositions\": [ 1 ],\n" +
596 " \"targetParagraphPositions\": [ 2, 3 ]\n" +
597 " },\n" +
598 " \"sentenceAlignments\": [\n" +
599 " {\n" +
600 " \"type\": \"MULTI_MATCH\",\n" +
601 " \"sourcePositions\": [ 0 ],\n" +
602 " \"targetPositions\": [ 0, 1 ]\n" +
603 " }\n" +
604 " ]\n" +
605 " },\n" +
606 " {\n" +
607 " \"paragraphAlignment\": {\n" +
608 " \"type\": \"MULTI_MATCH\",\n" +
609 " \"sourceParagraphPositions\": [ 2 ],\n" +
610 " \"targetParagraphPositions\": [ 4, 5, 6, 7 ]\n" +
611 " },\n" +
612 " \"sentenceAlignments\": [\n" +
613 " {\n" +
614 " \"type\": \"MULTI_MATCH\",\n" +
615 " \"sourcePositions\": [ 0 ],\n" +
616 " \"targetPositions\": [ 0, 1, 2, 3 ]\n" +
617 " }\n" +
618 " ]\n" +
619 " },\n" +
620 " {\n" +
621 " \"paragraphAlignment\": {\n" +
622 " \"type\": \"MATCH\",\n" +
623 " \"sourceParagraphPositions\": [ 3 ],\n" +
624 " \"targetParagraphPositions\": [ 8 ]\n" +
625 " },\n" +
626 " \"sentenceAlignments\": [\n" +
627 " {\n" +
628 " \"type\": \"MATCH\",\n" +
629 " \"sourcePositions\": [ 0 ],\n" +
630 " \"targetPositions\": [ 0 ]\n" +
631 " }\n" +
632 " ]\n" +
633 " }\n" +
634 " ]\n" +
635 "}";
636
637 @Test
638 @DisplayName("JacksonUtil.fromJson should correctly deserialize CombinedAlignmentOutput and support round-trip")
639 void testFromJson_CombinedAlignmentOutput() {
640
641
642
643 CombinedAlignmentOutput output = JacksonUtil.fromJson(
644 COMBINED_ALIGNMENT_JSON,
645 CombinedAlignmentOutput.class);
646
647 assertNotNull(output, "Deserialized object must not be null");
648
649
650 String roundTrippedJson = JacksonUtil.toJson(output, false);
651
652
653
654
655 JsonNode originalNode = JacksonUtil.makeNode(COMBINED_ALIGNMENT_JSON);
656 JsonNode roundTrippedNode = JacksonUtil.makeNode(roundTrippedJson);
657
658 assertNotNull(originalNode, "Original JSON must be parseable");
659 assertNotNull(roundTrippedNode, "Round-tripped JSON must be parseable");
660
661 assertEquals(
662 originalNode.toString(),
663 roundTrippedNode.toString(),
664 "Round-tripped JSON must be identical (structure + values) to the original");
665
666
667
668
669 assertEquals(4, output.alignments.size(), "Should have 4 alignments");
670
671
672 var a1 = output.alignments.get(0);
673 assertEquals("MULTI_MATCH", a1.paragraphAlignment.type);
674 assertEquals(List.of(0), a1.paragraphAlignment.sourceParagraphPositions);
675 assertEquals(List.of(0, 1), a1.paragraphAlignment.targetParagraphPositions);
676 var s1 = a1.sentenceAlignments.get(0);
677 assertEquals("MULTI_MATCH", s1.type);
678 assertEquals(List.of(0), s1.sourcePositions);
679 assertEquals(List.of(0, 1), s1.targetPositions);
680
681
682 var a2 = output.alignments.get(1);
683 assertEquals("MULTI_MATCH", a2.paragraphAlignment.type);
684 assertEquals(List.of(1), a2.paragraphAlignment.sourceParagraphPositions);
685 assertEquals(List.of(2, 3), a2.paragraphAlignment.targetParagraphPositions);
686
687
688 var a3 = output.alignments.get(2);
689 assertEquals("MULTI_MATCH", a3.paragraphAlignment.type);
690 assertEquals(List.of(2), a3.paragraphAlignment.sourceParagraphPositions);
691 assertEquals(List.of(4, 5, 6, 7), a3.paragraphAlignment.targetParagraphPositions);
692 var s3 = a3.sentenceAlignments.get(0);
693 assertEquals(List.of(0, 1, 2, 3), s3.targetPositions);
694
695
696 var a4 = output.alignments.get(3);
697 assertEquals("MATCH", a4.paragraphAlignment.type);
698 assertEquals(List.of(3), a4.paragraphAlignment.sourceParagraphPositions);
699 assertEquals(List.of(8), a4.paragraphAlignment.targetParagraphPositions);
700 var s4 = a4.sentenceAlignments.get(0);
701 assertEquals("MATCH", s4.type);
702 assertEquals(List.of(0), s4.sourcePositions);
703 assertEquals(List.of(0), s4.targetPositions);
704 }
705
706 @Test
707 @DisplayName("JacksonUtil.fromJson should return null on malformed JSON")
708 void testFromJson_MalformedJson_ReturnsNull() {
709 String malformed = "{ invalid: json }";
710 CombinedAlignmentOutput result = JacksonUtil.fromJson(malformed,
711 CombinedAlignmentOutput.class);
712 assertNull(result, "Should return null on invalid JSON");
713 }
714 }
715 }