Skip to content

Commit 6a2bb6f

Browse files
paulirwinclaude
andcommitted
Add MockDirectoryWrapper leak-gate test for MMap slice/clone paths
Wraps a real MMapDirectory in MockDirectoryWrapper and exercises the root input, slicer, and slice disposal paths. On dispose the wrapper throws "cannot close: there are still open files" if any directly-opened input/slicer/slice was not disposed, so this gates the MMap-specific disposal paths end to end (TestRandomChunkSizes already covers the OpenInput-via-IndexWriter path). Documents that clones are not tracked by upstream MockDirectoryWrapper and that this gate is at the IndexInput level, distinct from the FileStream-disposal seam test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1e20009 commit 6a2bb6f

1 file changed

Lines changed: 66 additions & 0 deletions

File tree

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,72 @@ public void TestMultipleSlicesReadDistinctData()
12431243
}
12441244
}
12451245

1246+
// LUCENENET specific: PR #1267 review item. End-to-end leak gate for the
1247+
// MMap-specific disposal paths. MockDirectoryWrapper tracks every
1248+
// directly-opened IndexInput, IndexInputSlicer, and slice and, on Dispose,
1249+
// throws "cannot close: there are still open files" if any was not disposed.
1250+
// Wrapping a real MMapDirectory means a missing Dispose on an input, slicer,
1251+
// or slice fails here. (TestRandomChunkSizes covers the OpenInput-via-
1252+
// IndexWriter path through MockDirectoryWrapper; this adds explicit coverage
1253+
// for the slicer/slice paths.)
1254+
//
1255+
// Caveats so nobody over-reads this gate:
1256+
// - Clones are NOT tracked: upstream MockIndexInputWrapper.Clone leaves the
1257+
// open-file count alone (see the commented LUCENE-686 block there), so a
1258+
// leaked clone would not fail this test. The clones below verify clone
1259+
// read behavior, not clone-disposal leakage.
1260+
// - This gate is at the Lucene IndexInput level: it asserts inputs/slices
1261+
// are disposed, not that SharedMapping released its backing FileStream -
1262+
// that lower-level invariant is pinned by
1263+
// TestDisposeDisposesBackingFileStream_NonEmptyFile.
1264+
[Test, LuceneNetSpecific]
1265+
public void TestNoOpenHandlesAfterDispose_SliceAndClonePaths()
1266+
{
1267+
var dirPath = CreateTempDir("testMMapNoOpenHandles");
1268+
var mmapDir = new MMapDirectory(dirPath);
1269+
// MockDirectoryWrapper takes ownership of mmapDir and disposes it.
1270+
// dir is NOT in a using: its Dispose() is the assertion under test
1271+
// (it throws if a handle leaked), so it must run only on the success
1272+
// path, last. A using would also dispose it while unwinding an earlier
1273+
// assertion failure, and the resulting "still open files" throw would
1274+
// mask the real failure. The inputs below ARE in usings: that still
1275+
// exercises their Dispose() (the path under test) while guaranteeing
1276+
// cleanup if an assertion in this method throws. usings dispose LIFO,
1277+
// which gives the correct order (clone before slice before slicer).
1278+
var dir = new MockDirectoryWrapper(Random, mmapDir);
1279+
1280+
const string name = "bytes";
1281+
using (var io = dir.CreateOutput(name, NewIOContext(Random)))
1282+
{
1283+
for (int i = 0; i < 1024; i++) io.WriteInt32(i);
1284+
}
1285+
1286+
// Root input + a clone of it.
1287+
using (var input = dir.OpenInput(name, NewIOContext(Random)))
1288+
using (var inputClone = (IndexInput)input.Clone())
1289+
{
1290+
Assert.AreEqual(42, ReadInt32At(inputClone, 42));
1291+
}
1292+
1293+
// Slicer + slice + a clone of the slice.
1294+
using (var slicer = dir.CreateSlicer(name, NewIOContext(Random)))
1295+
using (var slice = slicer.OpenSlice("half", 0, 1024 * sizeof(int) / 2))
1296+
using (var sliceClone = (IndexInput)slice.Clone())
1297+
{
1298+
Assert.AreEqual(7, ReadInt32At(sliceClone, 7));
1299+
}
1300+
1301+
// If any of the above was left open, this throws
1302+
// "MockDirectoryWrapper: cannot close: there are still open files".
1303+
dir.Dispose();
1304+
}
1305+
1306+
private static int ReadInt32At(IndexInput input, long intIndex)
1307+
{
1308+
input.Seek(intIndex * sizeof(int));
1309+
return input.ReadInt32();
1310+
}
1311+
12461312
// Disposing a single slice must not affect its sibling slices from
12471313
// the same slicer. In the new design each OpenSlice has its own
12481314
// View, so slice.Dispose closes that slice's view only. Slicer

0 commit comments

Comments
 (0)