This is an automated email from the ASF dual-hosted git repository.
mawiesne pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opennlp.git
The following commit(s) were added to refs/heads/main by this push:
new 47fd462a0 OPENNLP-1859: Add tests for BilouCodec encode/decode and
outcome compatibility (#1135)
47fd462a0 is described below
commit 47fd462a0d3f41d8a2b228f87691587a7b49ed87
Author: Vasiliy Mikhailov <[email protected]>
AuthorDate: Thu Jul 2 16:42:58 2026 +0300
OPENNLP-1859: Add tests for BilouCodec encode/decode and outcome
compatibility (#1135)
* Add edge-case tests for BilouCodec encode/decode and outcome compatibility
* Rewrite test comments to describe behaviour instead of production line
numbers
* Make testCompatibilityContinueWithoutStartOrLast a distinct case
({B_CONTINUE, A_UNIT}) per review: it duplicated WithoutStart and
short-circuited in the last-validation loop instead of exercising the
cont-with-no-matching-start-or-last branch; 81 tests green
---
.../opennlp/tools/namefind/BilouCodecTest.java | 381 +++++++++++++++++++++
1 file changed, 381 insertions(+)
diff --git
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/namefind/BilouCodecTest.java
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/namefind/BilouCodecTest.java
index e13abbfc0..5711df331 100644
---
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/namefind/BilouCodecTest.java
+++
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/namefind/BilouCodecTest.java
@@ -46,6 +46,9 @@ public class BilouCodecTest {
private static final String B_UNIT = B_TYPE + "-" + BilouCodec.UNIT;
private static final String C_TYPE = "ctype";
+ private static final String C_START = C_TYPE + "-" + BilouCodec.START;
+ private static final String C_CONTINUE = C_TYPE + "-" + BilouCodec.CONTINUE;
+ private static final String C_LAST = C_TYPE + "-" + BilouCodec.LAST;
private static final String C_UNIT = C_TYPE + "-" + BilouCodec.UNIT;
private static final String OTHER = BilouCodec.OTHER;
@@ -580,5 +583,383 @@ public class BilouCodecTest {
Assertions.assertFalse(codec.areOutcomesCompatible(new String[] {A_START,
B_LAST, OTHER}));
}
+ // ---- Additional tests to improve mutation coverage ----
+
+ /**
+ * Decode with LAST tag appearing without prior START (dangling LAST).
+ * The guard (start != -1) should prevent creating a span.
+ */
+ @Test
+ void testDecodeDanglingLast() {
+ List<String> encoded = Arrays.asList(A_LAST, OTHER);
+ Span[] actual = codec.decode(encoded);
+ Assertions.assertEquals(0, actual.length, "Dangling LAST without START
should produce no spans");
+ }
+
+ /**
+ * Decode with LAST at position 0 without prior START.
+ */
+ @Test
+ void testDecodeLastAtStart() {
+ List<String> encoded = Arrays.asList(A_LAST);
+ Span[] actual = codec.decode(encoded);
+ Assertions.assertEquals(0, actual.length, "LAST at position 0 without
START should produce no spans");
+ }
+
+ /**
+ * Decode with multiple dangling LAST tags interspersed.
+ */
+ @Test
+ void testDecodeMultipleDanglingLast() {
+ List<String> encoded = Arrays.asList(A_LAST, OTHER, B_LAST, A_UNIT);
+ Span[] actual = codec.decode(encoded);
+ Span[] expected = new Span[] {new Span(3, 4, A_TYPE)};
+ Assertions.assertArrayEquals(expected, actual);
+ }
+
+ /**
+ * Decode with START, LAST pair to verify exact span boundaries (end + 1).
+ */
+ @Test
+ void testDecodeStartLastBoundaries() {
+ List<String> encoded = Arrays.asList(OTHER, A_START, A_LAST, OTHER);
+ Span[] actual = codec.decode(encoded);
+ Span[] expected = new Span[] {new Span(1, 3, A_TYPE)};
+ Assertions.assertArrayEquals(expected, actual, "Span should cover
positions 1 to 3");
+ }
+
+ /**
+ * Encode span with null type - single token (UNIT path).
+ */
+ @Test
+ void testEncodeNullTypeUnit() {
+ Span[] spans = new Span[] {new Span(1, 2, null)};
+ String[] actual = codec.encode(spans, 3);
+ String[] expected = new String[] {OTHER, "default-unit", OTHER};
+ Assertions.assertArrayEquals(expected, actual, "Null type single-token
span should use default-unit");
+ }
+
+ /**
+ * Encode span with null type - two tokens (START + LAST, no CONTINUE).
+ */
+ @Test
+ void testEncodeNullTypeTwoTokens() {
+ Span[] spans = new Span[] {new Span(0, 2, null)};
+ String[] actual = codec.encode(spans, 4);
+ String[] expected = new String[] {"default-start", "default-last", OTHER,
OTHER};
+ Assertions.assertArrayEquals(expected, actual,
+ "Null type two-token span should use default-start and default-last");
+ }
+
+ /**
+ * Encode span with null type - four tokens (START + CONTINUE*2 + LAST).
+ * Exercises the CONTINUE inner loop.
+ */
+ @Test
+ void testEncodeNullTypeLong() {
+ Span[] spans = new Span[] {new Span(0, 4, null)};
+ String[] actual = codec.encode(spans, 6);
+ String[] expected = new String[] {"default-start", "default-cont",
"default-cont",
+ "default-last", OTHER, OTHER};
+ Assertions.assertArrayEquals(expected, actual,
+ "Null type long span should use default-start, default-cont,
default-last");
+ }
+
+ /**
+ * areOutcomesCompatible: unit-only outcomes should be compatible.
+ * Exercises the UNIT parsing path.
+ */
+ @Test
+ void testCompatibilityUnitOnly() {
+ Assertions.assertTrue(codec.areOutcomesCompatible(new String[] {A_UNIT}),
+ "Unit-only outcomes should be compatible");
+ }
+
+ /**
+ * areOutcomesCompatible: multiple unit-only outcomes should be compatible.
+ */
+ @Test
+ void testCompatibilityMultipleUnitsOnly() {
+ Assertions.assertTrue(codec.areOutcomesCompatible(new String[] {A_UNIT,
B_UNIT, C_UNIT}),
+ "Multiple unit-only outcomes should be compatible");
+ }
+
+ /**
+ * areOutcomesCompatible: CONTINUE without matching START should fail.
+ * Exercises the cont validation (start.contains check).
+ */
+ @Test
+ void testCompatibilityContinueWithoutStart() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(new String[]
{B_CONTINUE, B_LAST, B_UNIT}),
+ "CONTINUE without matching START should be incompatible");
+ }
+
+ /**
+ * areOutcomesCompatible: CONTINUE without matching LAST should fail.
+ * Exercises the cont validation (last.contains check).
+ */
+ @Test
+ void testCompatibilityContinueWithoutLast() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(new String[] {B_START,
B_CONTINUE, B_UNIT}),
+ "CONTINUE without matching LAST should be incompatible");
+ }
+
+ /**
+ * areOutcomesCompatible: CONTINUE without matching START or LAST should
fail.
+ * Both contains checks should return false.
+ */
+ @Test
+ void testCompatibilityContinueWithoutStartOrLast() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(new String[]
{B_CONTINUE, A_UNIT}),
+ "CONTINUE without matching START or LAST should be incompatible");
+ }
+
+ /**
+ * areOutcomesCompatible: UNIT + START without matching LAST should fail.
+ * Exercises the start validation (last.contains check).
+ */
+ @Test
+ void testCompatibilityUnitStartNoLast() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(new String[] {A_UNIT,
A_START}),
+ "UNIT with START but no matching LAST should be incompatible");
+ }
+
+ /**
+ * areOutcomesCompatible: UNIT + START + CONTINUE without LAST should fail.
+ * Exercises both start and cont validation paths.
+ */
+ @Test
+ void testCompatibilityUnitStartContinueNoLast() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(new String[] {A_UNIT,
A_START, A_CONTINUE}),
+ "UNIT with START and CONTINUE but no LAST should be incompatible");
+ }
+
+ /**
+ * areOutcomesCompatible: only START without LAST should fail.
+ */
+ @Test
+ void testCompatibilityStartOnly() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(new String[] {A_START}),
+ "START without matching LAST should be incompatible");
+ }
+
+ /**
+ * areOutcomesCompatible: only CONTINUE should fail.
+ */
+ @Test
+ void testCompatibilityContinueOnly() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(new String[]
{A_CONTINUE}),
+ "CONTINUE without matching START or LAST should be incompatible");
+ }
+
+ /**
+ * areOutcomesCompatible: only LAST without START should fail.
+ */
+ @Test
+ void testCompatibilityLastOnly() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(new String[] {A_LAST}),
+ "LAST without matching START should be incompatible");
+ }
+
+ /**
+ * areOutcomesCompatible: START + LAST + CONTINUE for different type should
fail.
+ * The CONTINUE type has no matching START or LAST.
+ */
+ @Test
+ void testCompatibilityStartLastOtherContinue() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(new String[] {A_START,
A_LAST, B_CONTINUE}),
+ "CONTINUE for different type without matching START/LAST should be
incompatible");
+ }
+
+ /**
+ * areOutcomesCompatible: UNIT + START + LAST + CONTINUE for different type
should fail.
+ */
+ @Test
+ void testCompatibilityUnitStartLastOtherContinue() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(new String[] {A_UNIT,
A_START, A_LAST, B_CONTINUE}),
+ "CONTINUE for different type should be incompatible even with other
valid types");
+ }
+
+ /**
+ * areOutcomesCompatible: only OTHER should fail (no start or unit).
+ */
+ @Test
+ void testCompatibilityOtherOnly() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(new String[] {OTHER}),
+ "Only OTHER outcomes should be incompatible (no start or unit)");
+ }
+
+ /**
+ * areOutcomesCompatible: multiple OTHER should fail.
+ */
+ @Test
+ void testCompatibilityMultipleOtherOnly() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(new String[] {OTHER,
OTHER, OTHER}),
+ "Multiple OTHER outcomes should be incompatible (no start or unit)");
+ }
+
+ /**
+ * areOutcomesCompatible: empty array should fail.
+ */
+ @Test
+ void testCompatibilityEmptyArray() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(new String[] {}),
+ "Empty outcomes array should be incompatible");
+ }
+
+ /**
+ * Decode with only OTHER tags should produce no spans.
+ */
+ @Test
+ void testDecodeOnlyOther() {
+ List<String> encoded = Arrays.asList(OTHER, OTHER, OTHER);
+ Span[] actual = codec.decode(encoded);
+ Assertions.assertEquals(0, actual.length, "Only OTHER tags should produce
no spans");
+ }
+
+ /**
+ * Decode with only CONTINUE tags should produce no spans.
+ */
+ @Test
+ void testDecodeOnlyContinue() {
+ List<String> encoded = Arrays.asList(A_CONTINUE, A_CONTINUE);
+ Span[] actual = codec.decode(encoded);
+ Assertions.assertEquals(0, actual.length, "Only CONTINUE tags should
produce no spans");
+ }
+
+ /**
+ * Decode with START but no LAST should produce no spans.
+ */
+ @Test
+ void testDecodeStartWithoutLast() {
+ List<String> encoded = Arrays.asList(A_START, A_CONTINUE, OTHER);
+ Span[] actual = codec.decode(encoded);
+ Assertions.assertEquals(0, actual.length, "START without LAST should
produce no spans");
+ }
+
+ /**
+ * Decode with START, CONTINUE, LAST at the end of the list.
+ */
+ @Test
+ void testDecodeSpanAtEnd() {
+ List<String> encoded = Arrays.asList(OTHER, A_START, A_CONTINUE, A_LAST);
+ Span[] actual = codec.decode(encoded);
+ Span[] expected = new Span[] {new Span(1, 4, A_TYPE)};
+ Assertions.assertArrayEquals(expected, actual, "Span at end should be
decoded correctly");
+ }
+
+ /**
+ * Decode with multiple consecutive spans.
+ */
+ @Test
+ void testDecodeMultipleConsecutiveSpans() {
+ List<String> encoded = Arrays.asList(A_START, A_LAST, B_START, B_LAST);
+ Span[] actual = codec.decode(encoded);
+ Span[] expected = new Span[] {new Span(0, 2, A_TYPE), new Span(2, 4,
B_TYPE)};
+ Assertions.assertArrayEquals(expected, actual, "Multiple consecutive spans
should be decoded correctly");
+ }
+
+ /**
+ * Decode with four-token span (START, CONTINUE, CONTINUE, LAST).
+ */
+ @Test
+ void testDecodeFourTokenSpan() {
+ List<String> encoded = Arrays.asList(A_START, A_CONTINUE, A_CONTINUE,
A_LAST);
+ Span[] actual = codec.decode(encoded);
+ Span[] expected = new Span[] {new Span(0, 4, A_TYPE)};
+ Assertions.assertArrayEquals(expected, actual, "Four-token span should be
decoded correctly");
+ }
+
+ /**
+ * Encode with null type and typed spans mixed.
+ */
+ @Test
+ void testEncodeMixedNullAndTypedTypes() {
+ Span[] spans = new Span[] {new Span(0, 2, null), new Span(3, 4, A_TYPE)};
+ String[] actual = codec.encode(spans, 5);
+ String[] expected = new String[] {"default-start", "default-last", OTHER,
A_UNIT, OTHER};
+ Assertions.assertArrayEquals(expected, actual, "Mixed null and typed spans
should encode correctly");
+ }
+
+ /**
+ * Encode with five-token span (START + CONTINUE*3 + LAST).
+ */
+ @Test
+ void testEncodeFiveTokenSpan() {
+ Span[] spans = new Span[] {new Span(0, 5, A_TYPE)};
+ String[] actual = codec.encode(spans, 7);
+ String[] expected = new String[] {A_START, A_CONTINUE, A_CONTINUE,
A_CONTINUE, A_LAST, OTHER, OTHER};
+ Assertions.assertArrayEquals(expected, actual, "Five-token span should
have three CONTINUE tags");
+ }
+
+ /**
+ * areOutcomesCompatible: default-prefixed outcomes should work.
+ */
+ @Test
+ void testCompatibilityDefaultPrefix() {
+ Assertions.assertTrue(codec.areOutcomesCompatible(
+ new String[] {"default-start", "default-last", OTHER}),
+ "Default-prefixed START and LAST should be compatible");
+ }
+
+ /**
+ * areOutcomesCompatible: default-prefixed with CONTINUE should work.
+ */
+ @Test
+ void testCompatibilityDefaultPrefixWithContinue() {
+ Assertions.assertTrue(codec.areOutcomesCompatible(
+ new String[] {"default-start", "default-cont", "default-last", OTHER}),
+ "Default-prefixed START, CONTINUE, LAST should be compatible");
+ }
+
+ /**
+ * areOutcomesCompatible: default-prefixed START without LAST should fail.
+ */
+ @Test
+ void testCompatibilityDefaultPrefixStartNoLast() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(
+ new String[] {"default-start", A_UNIT}),
+ "Default-prefixed START without matching LAST should be incompatible");
+ }
+
+ /**
+ * areOutcomesCompatible: mixed default and typed outcomes with mismatch.
+ */
+ @Test
+ void testCompatibilityMixedDefaultTypedMismatch() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(
+ new String[] {"default-start", "default-last", A_CONTINUE}),
+ "Typed CONTINUE without matching typed START/LAST should be
incompatible");
+ }
+
+ /**
+ * areOutcomesCompatible: three types all valid.
+ */
+ @Test
+ void testCompatibilityThreeTypesValid() {
+ Assertions.assertTrue(codec.areOutcomesCompatible(
+ new String[] {A_START, A_LAST, B_START, B_LAST, C_START, C_LAST,
OTHER}),
+ "Three valid types should be compatible");
+ }
+
+ /**
+ * areOutcomesCompatible: three types with one missing LAST.
+ */
+ @Test
+ void testCompatibilityThreeTypesOneMissingLast() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(
+ new String[] {A_START, A_LAST, B_START, B_LAST, C_START, OTHER}),
+ "Three types with one missing LAST should be incompatible");
+ }
+
+ /**
+ * areOutcomesCompatible: three types with one having CONTINUE but no START.
+ */
+ @Test
+ void testCompatibilityThreeTypesOneContinueNoStart() {
+ Assertions.assertFalse(codec.areOutcomesCompatible(
+ new String[] {A_START, A_LAST, B_START, B_LAST, C_CONTINUE, C_LAST,
OTHER}),
+ "Three types with one CONTINUE missing START should be incompatible");
+ }
}