Skip to content

Commit 94cc141

Browse files
committed
Fix namespace and add more unit tests
1 parent 9787d7b commit 94cc141

3 files changed

Lines changed: 274 additions & 2 deletions

File tree

src/Lucene.Net.Tests._J-S/Lucene.Net.Tests._J-S.csproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@
3737
<Compile Include="..\Lucene.Net.Tests\Support\**\*.cs" LinkBase="Support" Exclude="..\Lucene.Net.Tests\Support\ExceptionHandling\**\*" />
3838
<Compile Include="..\Lucene.Net.Tests\Util\TestNumericUtils.cs" Link="Util\TestNumericUtils.cs" />
3939
<EmbeddedResource Include="..\Lucene.Net.Tests\Store\LUCENENET521.zip" Link="Store\LUCENENET521.zip" />
40+
<!--
41+
3.x CFS index fixtures, re-linked into the Store namespace so
42+
TestMultiMMap can exercise the MMapDirectory IndexInputSlicer
43+
OpenFullSlice path used by CompoundFileDirectory when reading
44+
CFS files from 3.x indexes.
45+
-->
46+
<EmbeddedResource Include="..\Lucene.Net.Tests\Index\index.30.cfs.zip" Link="Store\index.30.cfs.zip" />
47+
<EmbeddedResource Include="..\Lucene.Net.Tests\Index\index.31.cfs.zip" Link="Store\index.31.cfs.zip" />
4048
</ItemGroup>
4149

4250
<ItemGroup>

src/Lucene.Net.Tests/Store/TestMultiMMap.cs

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using J2N;
12
using Lucene.Net.Attributes;
23
using Lucene.Net.Documents;
34
using Lucene.Net.Index.Extensions;
@@ -1066,6 +1067,268 @@ public void TestDisposingCloneDoesNotAffectRootOrSiblings()
10661067
cloneB.Dispose();
10671068
}
10681069

1070+
// Multiple slices over the same file must each see their own region
1071+
// of data, not stale bytes from a neighboring slice. Each OpenSlice
1072+
// in the new design opens its own MemoryMappedFile+view over the
1073+
// given (offset, length) window, so bounds are enforced per-slice
1074+
// and reads are independent.
1075+
[Test, LuceneNetSpecific]
1076+
public void TestMultipleSlicesReadDistinctData()
1077+
{
1078+
var dirPath = CreateTempDir("testMultipleSlicesDistinct");
1079+
using var mmapDir = new MMapDirectory(dirPath);
1080+
const string name = "bytes";
1081+
const int regionSize = 4096;
1082+
const int regions = 4;
1083+
1084+
// Write four contiguous 4KiB regions, each filled with a distinct
1085+
// byte pattern (0x11, 0x22, 0x33, 0x44). Later we open a slice
1086+
// over each region and check that reads return the right pattern.
1087+
using (var io = mmapDir.CreateOutput(name, NewIOContext(Random)))
1088+
{
1089+
var buf = new byte[regionSize];
1090+
for (int r = 0; r < regions; r++)
1091+
{
1092+
byte fill = (byte)(0x11 * (r + 1));
1093+
for (int i = 0; i < buf.Length; i++) buf[i] = fill;
1094+
io.WriteBytes(buf, 0, buf.Length);
1095+
}
1096+
}
1097+
1098+
using var slicer = mmapDir.CreateSlicer(name, NewIOContext(Random));
1099+
1100+
var slices = new IndexInput[regions];
1101+
try
1102+
{
1103+
for (int r = 0; r < regions; r++)
1104+
{
1105+
slices[r] = slicer.OpenSlice("slice" + r, r * regionSize, regionSize);
1106+
Assert.AreEqual(regionSize, slices[r].Length, $"slice {r} length");
1107+
}
1108+
1109+
// Each slice sees only its own pattern.
1110+
for (int r = 0; r < regions; r++)
1111+
{
1112+
byte expected = (byte)(0x11 * (r + 1));
1113+
for (int i = 0; i < regionSize; i++)
1114+
{
1115+
byte b = slices[r].ReadByte();
1116+
if (b != expected)
1117+
{
1118+
Assert.Fail(
1119+
$"slice {r} at offset {i}: expected 0x{expected:X2}, got 0x{b:X2}");
1120+
}
1121+
}
1122+
1123+
// Reading past the slice's length must fail with EOF.
1124+
try
1125+
{
1126+
slices[r].ReadByte();
1127+
Assert.Fail($"slice {r}: read past end of slice must throw EOF");
1128+
}
1129+
catch (EndOfStreamException)
1130+
{
1131+
// expected
1132+
}
1133+
}
1134+
1135+
// Concurrent reads across sibling slices: each worker scans
1136+
// its own slice fully and asserts the pattern. If the slices
1137+
// were accidentally aliased to the same underlying view,
1138+
// racing Seek() calls would cross-contaminate.
1139+
var errors = new System.Collections.Concurrent.ConcurrentBag<string>();
1140+
var tasks = new System.Threading.Tasks.Task[regions];
1141+
for (int r = 0; r < regions; r++)
1142+
{
1143+
int idx = r;
1144+
byte expected = (byte)(0x11 * (idx + 1));
1145+
var clone = (IndexInput)slices[idx].Clone();
1146+
tasks[idx] = System.Threading.Tasks.Task.Run(() =>
1147+
{
1148+
for (int pass = 0; pass < 50; pass++)
1149+
{
1150+
clone.Seek(0);
1151+
for (int i = 0; i < regionSize; i++)
1152+
{
1153+
byte b = clone.ReadByte();
1154+
if (b != expected)
1155+
{
1156+
errors.Add($"slice {idx} pass {pass} offset {i}: expected 0x{expected:X2}, got 0x{b:X2}");
1157+
return;
1158+
}
1159+
}
1160+
}
1161+
});
1162+
}
1163+
System.Threading.Tasks.Task.WaitAll(tasks);
1164+
if (!errors.IsEmpty)
1165+
{
1166+
Assert.Fail("Cross-slice contamination detected:\n" + string.Join("\n", errors));
1167+
}
1168+
}
1169+
finally
1170+
{
1171+
foreach (var s in slices) s?.Dispose();
1172+
}
1173+
}
1174+
1175+
// Disposing a single slice must not affect its sibling slices from
1176+
// the same slicer. In the new design each OpenSlice has its own
1177+
// View, so slice.Dispose closes that slice's view only. Slicer
1178+
// cascade Dispose is covered by TestCloneSliceSafety.
1179+
[Test, LuceneNetSpecific]
1180+
public void TestDisposingOneSliceDoesNotAffectSiblings()
1181+
{
1182+
var dirPath = CreateTempDir("testSliceSiblingIsolation");
1183+
using var mmapDir = new MMapDirectory(dirPath);
1184+
const string name = "bytes";
1185+
using (var io = mmapDir.CreateOutput(name, NewIOContext(Random)))
1186+
{
1187+
for (int i = 0; i < 8; i++) io.WriteInt32(i);
1188+
}
1189+
1190+
using var slicer = mmapDir.CreateSlicer(name, NewIOContext(Random));
1191+
var sliceA = slicer.OpenSlice("sliceA", 0, 16);
1192+
var sliceB = slicer.OpenSlice("sliceB", 16, 16);
1193+
1194+
sliceA.Dispose();
1195+
1196+
// Sibling must continue to work and return its own data.
1197+
Assert.AreEqual(4, sliceB.ReadInt32());
1198+
Assert.AreEqual(5, sliceB.ReadInt32());
1199+
1200+
// Disposed slice must throw on read.
1201+
try
1202+
{
1203+
sliceA.ReadInt32();
1204+
Assert.Fail("Disposed slice must throw AlreadyClosed");
1205+
}
1206+
catch (Exception e) when (e.IsAlreadyClosedException())
1207+
{
1208+
// expected
1209+
}
1210+
1211+
sliceB.Dispose();
1212+
}
1213+
1214+
// Open a 3.0 CFS index with MMapDirectory and read it end-to-end
1215+
// through DirectoryReader. 3.0 (pre-3.1) CFS files are the only
1216+
// remaining caller of IndexInputSlicer.OpenFullSlice, which in our
1217+
// new slicer requires a disposed-state check before it can trust
1218+
// descriptor.Length. If that path is broken, DirectoryReader.Open
1219+
// will throw while reading segment headers.
1220+
[Test, LuceneNetSpecific]
1221+
public void TestRead3xCfsIndex_ViaMMap()
1222+
{
1223+
var indexDir = CreateTempDir("test3xCfsIndex");
1224+
using (var zip = GetType().FindAndGetManifestResourceStream("index.30.cfs.zip"))
1225+
{
1226+
Assert.IsNotNull(zip, "expected index.30.cfs.zip to be embedded");
1227+
TestUtil.Unzip(zip, indexDir);
1228+
}
1229+
1230+
using var mmapDir = new MMapDirectory(indexDir);
1231+
using var reader = Index.DirectoryReader.Open(mmapDir);
1232+
1233+
Assert.IsTrue(reader.MaxDoc > 0, "3.0 index should contain documents");
1234+
1235+
// Touch each leaf to force real reads through the CFS +
1236+
// OpenFullSlice path. This will throw AVE on a broken mmap
1237+
// teardown or on a bad OpenFullSlice.
1238+
int totalDocs = 0;
1239+
foreach (var leaf in reader.Leaves)
1240+
{
1241+
var atomic = leaf.AtomicReader;
1242+
for (int i = 0; i < atomic.MaxDoc; i++)
1243+
{
1244+
if (atomic.LiveDocs != null && !atomic.LiveDocs.Get(i))
1245+
continue;
1246+
var doc = atomic.Document(i);
1247+
Assert.IsNotNull(doc, $"doc {i} in leaf {leaf} should not be null");
1248+
totalDocs++;
1249+
}
1250+
}
1251+
Assert.IsTrue(totalDocs > 0, "at least one live 3.0 document should be readable");
1252+
}
1253+
1254+
// Directly exercise the OpenFullSlice entry point on a 3.0 .cfs
1255+
// file. OpenFullSlice is [Obsolete("Only for reading CFS files
1256+
// from 3.x indexes.")] — the test pins that it still produces an
1257+
// IndexInput spanning the whole file and that its bytes match the
1258+
// plain OpenInput read of the same file.
1259+
[Test, LuceneNetSpecific]
1260+
public void TestOpenFullSlice_On3xCfsFile_MatchesOpenInput()
1261+
{
1262+
var indexDir = CreateTempDir("test3xOpenFullSlice");
1263+
using (var zip = GetType().FindAndGetManifestResourceStream("index.30.cfs.zip"))
1264+
{
1265+
Assert.IsNotNull(zip, "expected index.30.cfs.zip to be embedded");
1266+
TestUtil.Unzip(zip, indexDir);
1267+
}
1268+
1269+
// Pick the first .cfs file in the unzipped 3.0 index.
1270+
string cfsName = null;
1271+
foreach (var f in indexDir.GetFiles("*.cfs"))
1272+
{
1273+
cfsName = f.Name;
1274+
break;
1275+
}
1276+
Assert.IsNotNull(cfsName, "expected at least one .cfs file in the 3.0 index");
1277+
1278+
using var mmapDir = new MMapDirectory(indexDir);
1279+
1280+
byte[] viaOpenInput;
1281+
using (var input = mmapDir.OpenInput(cfsName, NewIOContext(Random)))
1282+
{
1283+
viaOpenInput = new byte[input.Length];
1284+
input.ReadBytes(viaOpenInput, 0, viaOpenInput.Length);
1285+
}
1286+
1287+
byte[] viaFullSlice;
1288+
using (var slicer = mmapDir.CreateSlicer(cfsName, NewIOContext(Random)))
1289+
{
1290+
#pragma warning disable 612, 618
1291+
using var full = slicer.OpenFullSlice();
1292+
#pragma warning restore 612, 618
1293+
Assert.AreEqual(viaOpenInput.Length, full.Length,
1294+
"OpenFullSlice length must match OpenInput length");
1295+
viaFullSlice = new byte[full.Length];
1296+
full.ReadBytes(viaFullSlice, 0, viaFullSlice.Length);
1297+
}
1298+
1299+
Assert.AreEqual(viaOpenInput, viaFullSlice,
1300+
"OpenFullSlice bytes must match OpenInput bytes for the same CFS file");
1301+
}
1302+
1303+
// OpenFullSlice on a slicer that has been disposed must throw
1304+
// AlreadyClosedException, not ObjectDisposedException leaking from
1305+
// the underlying FileStream. Part of the review-item-6 fix.
1306+
[Test, LuceneNetSpecific]
1307+
public void TestOpenFullSlice_AfterDispose_ThrowsAlreadyClosed()
1308+
{
1309+
var dirPath = CreateTempDir("testFullSliceAfterDispose");
1310+
using var mmapDir = new MMapDirectory(dirPath);
1311+
using (var io = mmapDir.CreateOutput("bytes", NewIOContext(Random)))
1312+
{
1313+
io.WriteInt32(42);
1314+
}
1315+
1316+
var slicer = mmapDir.CreateSlicer("bytes", NewIOContext(Random));
1317+
slicer.Dispose();
1318+
1319+
try
1320+
{
1321+
#pragma warning disable 612, 618
1322+
slicer.OpenFullSlice();
1323+
#pragma warning restore 612, 618
1324+
Assert.Fail("OpenFullSlice on disposed slicer must throw AlreadyClosed");
1325+
}
1326+
catch (Exception e) when (e.IsAlreadyClosedException())
1327+
{
1328+
// expected
1329+
}
1330+
}
1331+
10691332
[Test, LuceneNetSpecific]
10701333
public void TestDisposeIndexInput()
10711334
{

src/Lucene.Net/Store/MMapDirectory.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.IO;
55
using System.IO.MemoryMappedFiles;
66
using System.Threading;
7+
using SCG = System.Collections.Generic;
78

89
namespace Lucene.Net.Store
910
{
@@ -216,8 +217,8 @@ private sealed class IndexInputSlicerAnonymousClass : IndexInputSlicer
216217
// Track issued slices so that Dispose cascades. Lucene's
217218
// contract is that after slicer.Dispose, reads from any slice
218219
// (or clone of a slice) throw AlreadyClosedException.
219-
private readonly System.Collections.Generic.List<MemoryMappedViewAccessorIndexInput> issuedSlices
220-
= new System.Collections.Generic.List<MemoryMappedViewAccessorIndexInput>();
220+
private readonly SCG.List<MemoryMappedViewAccessorIndexInput> issuedSlices
221+
= new SCG.List<MemoryMappedViewAccessorIndexInput>();
221222
private readonly object issuedSlicesLock = new object();
222223

223224
public IndexInputSlicerAnonymousClass(MMapDirectory outerInstance, IOContext context, string file, FileStream descriptor)

0 commit comments

Comments
 (0)