Skip to content

Commit 2cde8b5

Browse files
paulirwinclaude
andauthored
Fix random TestRandomChains failures by excluding broken-offsets producers, #1072 (#1348)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cd6dfef commit 2cde8b5

5 files changed

Lines changed: 79 additions & 8 deletions

File tree

src/Lucene.Net.Analysis.Common/Analysis/Compound/HyphenationCompoundWordTokenFilter.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,11 @@ protected override void Decompose()
244244
m_tokens.Enqueue(new CompoundToken(this, start, partLength));
245245
}
246246
}
247-
else if (m_dictionary.Contains(m_termAtt.Buffer, start, partLength - 1))
247+
// LUCENENET specific - guard against a negative length (partLength == 0).
248+
// Upstream Java relies on CharArrayMap silently treating a negative length as
249+
// "not found", but Lucene.NET's CharArrayMap validates its arguments and throws
250+
// ArgumentOutOfRangeException. See the BOGUS/BROKEN/FUNKY/WACKO note above.
251+
else if (partLength - 1 >= 0 && m_dictionary.Contains(m_termAtt.Buffer, start, partLength - 1))
248252
{
249253
// check the dictionary again with a word that is one character
250254
// shorter

src/Lucene.Net.Analysis.Kuromoji/JapaneseKatakanaStemFilter.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ public sealed class JapaneseKatakanaStemFilter : TokenFilter
5151
public JapaneseKatakanaStemFilter(TokenStream input, int minimumLength)
5252
: base(input)
5353
{
54-
// LUCENENET: Added guard clause
55-
if (minimumLength < 0)
56-
throw new ArgumentOutOfRangeException(nameof(minimumLength), "Minimum length must be a non-negative integer.");
54+
// LUCENENET: Backport of the LUCENE-10352 guard clause. Enforcing minimumLength >= 1
55+
// prevents a zero-length term (e.g. the single empty token a KeywordTokenizer emits for
56+
// empty input) from reaching the term[length - 1] read in Stem() with length == 0.
57+
if (minimumLength < 1)
58+
throw new ArgumentOutOfRangeException(nameof(minimumLength), "minimumLength must be >=1");
5759

5860
this.minimumKatakanaLength = minimumLength;
5961
this.termAttr = AddAttribute<ICharTermAttribute>();

src/Lucene.Net.Tests.Analysis.Common/Analysis/Compound/TestCompoundWordTokenFilter.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Lucene.Net.Analysis.Core;
55
using Lucene.Net.Analysis.TokenAttributes;
66
using Lucene.Net.Analysis.Util;
7+
using Lucene.Net.Attributes;
78
using Lucene.Net.Util;
89
using NUnit.Framework;
910
using System.IO;
@@ -266,6 +267,41 @@ public virtual void TestRandomStrings()
266267
CheckRandomData(Random, b, 1000 * RandomMultiplier);
267268
}
268269

270+
/// <summary>
271+
/// LUCENENET-specific regression test for <a href="https://github.com/apache/lucenenet/issues/1072">#1072</a>.
272+
/// <para/>
273+
/// When a term has leading non-letter characters (which the hyphenator counts via
274+
/// <c>iIgnoreAtBeginning</c>) the resulting hyphenation points can contain two equal values,
275+
/// so <see cref="HyphenationCompoundWordTokenFilter"/>'s <c>Decompose()</c> computes a
276+
/// <c>partLength</c> of <c>0</c>. With <c>minSubwordSize == 0</c> that zero-length part is not
277+
/// filtered out, and the genitive-'s fallback then probes the dictionary with a length of
278+
/// <c>partLength - 1 == -1</c>. Upstream Java tolerates the negative length (its CharArrayMap
279+
/// silently treats it as "not found"), but Lucene.NET's CharArrayMap validates its arguments
280+
/// and throws <see cref="System.ArgumentOutOfRangeException"/>. The filter must not pass a
281+
/// negative length downstream. The input <c>"...rindfleisch"</c> yields hyphenation points
282+
/// <c>[0, 7, 11, 11]</c>, deterministically reproducing the crash that
283+
/// <c>TestRandomChains</c> hit under seed <c>0xc0ab8518c470366f:0x6ed44cef961049a2</c>.
284+
/// </summary>
285+
[Test]
286+
[LuceneNetSpecific] // Issue #1072
287+
public virtual void TestZeroLengthSubwordDoesNotThrow()
288+
{
289+
CharArraySet dict = makeDictionary("rind", "fleisch");
290+
291+
using var @is = this.GetType().getResourceAsStream("da_UTF8.xml");
292+
HyphenationTree hyphenator = HyphenationCompoundWordTokenFilter.GetHyphenationTree(@is);
293+
294+
// minSubwordSize == 0 is the configuration that lets a zero-length part through.
295+
HyphenationCompoundWordTokenFilter tf = new HyphenationCompoundWordTokenFilter(TEST_VERSION_CURRENT,
296+
new MockTokenizer(new StringReader("...rindfleisch"), MockTokenizer.WHITESPACE, false),
297+
hyphenator, dict, CompoundWordTokenFilterBase.DEFAULT_MIN_WORD_SIZE, 0, 100, false);
298+
299+
// Before the fix this threw ArgumentOutOfRangeException while decomposing. The leading
300+
// "..." shifts the matched subword offsets, so no subword survives the dictionary check;
301+
// the only required behavior is that decomposition completes without throwing.
302+
AssertTokenStreamContents(tf, new string[] { "...rindfleisch" });
303+
}
304+
269305
[Test]
270306
public virtual void TestEmptyTerm()
271307
{

src/Lucene.Net.Tests.Analysis.Common/Analysis/Core/TestRandomChains.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@ static TestRandomChains()
151151
foreach (ConstructorInfo ctor in c.GetConstructors())
152152
{
153153
brokenOffsetsConstructors[ctor] = ALWAYS;
154+
155+
// LUCENENET specific (#1072): Also fully exclude these broken-offsets producers
156+
// from the random chains, not just relax offset validation. brokenOffsetsConstructors
157+
// only flips offsetsAreCorrect=false (which suppresses ValidatingTokenFilter's checks),
158+
// but the offending filter still runs and can feed backwards offsets into a downstream
159+
// word-combiner (e.g. ShingleFilter), which then throws from OffsetAttribute.SetOffset.
160+
// Upstream Lucene resolved this in 9.1 (LUCENE-10352) by excluding these classes via the
161+
// @IgnoreRandomChains annotation. Until that annotation is backported, exclude them here.
162+
// TODO: Remove this when LUCENE-10352 (IgnoreRandomChains) is backported.
163+
brokenConstructors[ctor] = ALWAYS;
154164
}
155165
}
156166
}
@@ -1169,8 +1179,8 @@ private TokenFilterSpec NewFilterChain(Random random, Tokenizer tokenizer, bool
11691179
// hack: MockGraph/MockLookahead has assertions that will trip if they follow
11701180
// an offsets violator. so we can't use them after e.g. wikipediatokenizer
11711181
if (!spec.offsetsAreCorrect &&
1172-
(ctor.DeclaringType.Equals(typeof(MockGraphTokenFilter)))
1173-
|| ctor.DeclaringType.Equals(typeof(MockRandomLookaheadTokenFilter)))
1182+
(ctor.DeclaringType.Equals(typeof(MockGraphTokenFilter))
1183+
|| ctor.DeclaringType.Equals(typeof(MockRandomLookaheadTokenFilter))))
11741184
{
11751185
continue;
11761186
}
@@ -1284,7 +1294,6 @@ private static TEnum RandomEnum<TEnum>(Random random)
12841294
}
12851295

12861296
[Test]
1287-
[AwaitsFix(BugUrl = "https://github.com/apache/lucenenet/issues/271#issuecomment-973005744")] // LUCENENET TODO: this test occasionally fails
12881297
public void TestRandomChains_()
12891298
{
12901299
int numIterations = AtLeast(20);
@@ -1311,7 +1320,6 @@ public void TestRandomChains_()
13111320

13121321
// we might regret this decision...
13131322
[Test]
1314-
[AwaitsFix(BugUrl = "https://github.com/apache/lucenenet/issues/271#issuecomment-973005744")] // LUCENENET TODO: this test occasionally fails
13151323
public void TestRandomChainsWithLargeStrings()
13161324
{
13171325
int numIterations = AtLeast(20);

src/Lucene.Net.Tests.Analysis.Kuromoji/TestJapaneseKatakanaStemFilter.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Lucene.Net.Analysis.Core;
22
using Lucene.Net.Analysis.Miscellaneous;
33
using Lucene.Net.Analysis.Util;
4+
using Lucene.Net.Attributes;
45
using NUnit.Framework;
56
using System;
67

@@ -96,5 +97,25 @@ public void TestEmptyTerm()
9697

9798
CheckOneTerm(a, "", "");
9899
}
100+
101+
/// <summary>
102+
/// LUCENENET-specific regression test for <a href="https://github.com/apache/lucenenet/issues/1072">#1072</a>.
103+
/// <para/>
104+
/// Verifies the LUCENE-10352 guard: the constructor must reject a <c>minimumLength</c> less than
105+
/// <c>1</c>. A <c>minimumLength</c> of <c>0</c> would let a zero-length token (such as the single empty
106+
/// token a <see cref="KeywordTokenizer"/> emits for empty input) skip the length check in <c>Stem()</c>,
107+
/// after which the <c>term[length - 1]</c> access reads <c>term[-1]</c> and throws. This surfaced as a
108+
/// residual <c>TestRandomChains</c> failure reported on PR #1348 (e.g. seed
109+
/// <c>0x951afd480395f9b7:0x63fc16274945f1a3</c>).
110+
/// </summary>
111+
[Test]
112+
[LuceneNetSpecific] // Issue #1072
113+
public void TestMinimumLengthLessThanOneThrows()
114+
{
115+
Tokenizer tokenizer = new KeywordTokenizer(new System.IO.StringReader(""));
116+
117+
Assert.Throws<ArgumentOutOfRangeException>(() => new JapaneseKatakanaStemFilter(tokenizer, minimumLength: 0));
118+
Assert.Throws<ArgumentOutOfRangeException>(() => new JapaneseKatakanaStemFilter(tokenizer, minimumLength: -1));
119+
}
99120
}
100121
}

0 commit comments

Comments
 (0)