2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
4
using Microsoft . Win32 . SafeHandles ;
5
- using System ;
6
- using System . Diagnostics ;
7
- using System . IO ;
8
5
using Microsoft . DotNet . RemoteExecutor ;
9
6
using Xunit ;
7
+ using System . Collections . Generic ;
8
+ using System . Threading . Tasks ;
10
9
11
10
namespace System . IO . Tests
12
11
{
12
+ [ Collection ( nameof ( DisableParallelization ) ) ] // make sure no other tests are calling GC.Collect()
13
13
public class FileStream_Dispose : FileSystemTest
14
14
{
15
15
[ Fact ]
16
16
public void DisposeClosesHandle ( )
17
17
{
18
18
SafeFileHandle handle ;
19
- using ( FileStream fs = new FileStream ( GetTestFilePath ( ) , FileMode . Create ) )
19
+ using ( FileStream fs = new FileStream ( GetTestFilePath ( ) , FileMode . Create ) )
20
20
{
21
21
handle = fs . SafeFileHandle ;
22
22
}
@@ -199,7 +199,7 @@ public void Finalizer_CallsVirtualDispose_FalseArg()
199
199
public void DisposeFlushesWriteBuffer ( )
200
200
{
201
201
string fileName = GetTestFilePath ( ) ;
202
- using ( FileStream fs = new FileStream ( fileName , FileMode . Create ) )
202
+ using ( FileStream fs = new FileStream ( fileName , FileMode . Create ) )
203
203
{
204
204
fs . Write ( TestBuffer , 0 , TestBuffer . Length ) ;
205
205
}
@@ -213,20 +213,143 @@ public void DisposeFlushesWriteBuffer()
213
213
}
214
214
}
215
215
216
+ public class DerivedFileStreamWithFinalizer : FileStream
217
+ {
218
+ public static int DisposeTrueCalled = 0 ;
219
+ public static int DisposeFalseCalled = 0 ;
220
+
221
+ public DerivedFileStreamWithFinalizer ( string path , FileMode mode , FileAccess access , FileShare share , int bufferSize , FileOptions options )
222
+ : base ( path , mode , access , share , bufferSize , options )
223
+ {
224
+ }
225
+
226
+ public DerivedFileStreamWithFinalizer ( SafeFileHandle handle , FileAccess access , int bufferSize , bool isAsync )
227
+ : base ( handle , access , bufferSize , isAsync )
228
+ {
229
+ }
230
+
231
+ public DerivedFileStreamWithFinalizer ( IntPtr handle , FileAccess access , bool ownsHandle )
232
+ #pragma warning disable CS0618 // Type or member is obsolete
233
+ : base ( handle , access , ownsHandle )
234
+ #pragma warning restore CS0618 // Type or member is obsolete
235
+ {
236
+ }
237
+
238
+ ~ DerivedFileStreamWithFinalizer ( ) => Dispose ( false ) ;
239
+
240
+ protected override void Dispose ( bool disposing )
241
+ {
242
+ if ( disposing )
243
+ {
244
+ DisposeTrueCalled ++ ;
245
+ }
246
+ else
247
+ {
248
+ DisposeFalseCalled ++ ;
249
+ }
250
+
251
+ base . Dispose ( disposing ) ;
252
+ }
253
+ }
254
+
255
+ public class DerivedFileStreamWithoutFinalizer : FileStream
256
+ {
257
+ public static int DisposeTrueCalled = 0 ;
258
+ public static int DisposeFalseCalled = 0 ;
259
+
260
+ public DerivedFileStreamWithoutFinalizer ( string path , FileMode mode , FileAccess access , FileShare share , int bufferSize , FileOptions options )
261
+ : base ( path , mode , access , share , bufferSize , options )
262
+ {
263
+ }
264
+
265
+ public DerivedFileStreamWithoutFinalizer ( SafeFileHandle handle , FileAccess access , int bufferSize , bool isAsync )
266
+ : base ( handle , access , bufferSize , isAsync )
267
+ {
268
+ }
269
+
270
+ public DerivedFileStreamWithoutFinalizer ( IntPtr handle , FileAccess access , bool ownsHandle )
271
+ #pragma warning disable CS0618 // Type or member is obsolete
272
+ : base ( handle , access , ownsHandle )
273
+ #pragma warning restore CS0618 // Type or member is obsolete
274
+ {
275
+ }
276
+
277
+ protected override void Dispose ( bool disposing )
278
+ {
279
+ if ( disposing )
280
+ {
281
+ DisposeTrueCalled ++ ;
282
+ }
283
+ else
284
+ {
285
+ DisposeFalseCalled ++ ;
286
+ }
287
+
288
+ base . Dispose ( disposing ) ;
289
+ }
290
+ }
291
+
216
292
[ ConditionalFact ( typeof ( PlatformDetection ) , nameof ( PlatformDetection . IsPreciseGcSupported ) ) ]
217
293
public void FinalizeFlushesWriteBuffer ( )
294
+ => VerifyFlushedBufferOnFinalization (
295
+ filePath => new FileStream ( filePath , FileMode . Create , FileAccess . Write , FileShare . ReadWrite | FileShare . Delete , bufferSize : 4096 , useAsync : false ) ) ;
296
+
297
+ [ ConditionalFact ( typeof ( PlatformDetection ) , nameof ( PlatformDetection . IsPreciseGcSupported ) ) ]
298
+ public void FinalizeFlushesWriteBufferForDerivedFileStreamWithFinalizerCreatedFromPath ( )
299
+ => VerifyFlushedBufferOnFinalization (
300
+ filePath => new DerivedFileStreamWithFinalizer ( filePath , FileMode . Create , FileAccess . Write , FileShare . ReadWrite | FileShare . Delete , bufferSize : 4096 , FileOptions . None ) ) ;
301
+
302
+ [ ConditionalFact ( typeof ( PlatformDetection ) , nameof ( PlatformDetection . IsPreciseGcSupported ) ) ]
303
+ public void FinalizeFlushesWriteBufferForDerivedFileStreamWithFinalizerCreatedFromSafeFileHandle ( )
304
+ => VerifyFlushedBufferOnFinalization (
305
+ filePath => new DerivedFileStreamWithFinalizer (
306
+ File . OpenHandle ( filePath , FileMode . Create , FileAccess . Write , FileShare . ReadWrite | FileShare . Delete , FileOptions . None ) ,
307
+ FileAccess . Write , bufferSize : 4096 , isAsync : false ) ) ;
308
+
309
+ [ ConditionalFact ( typeof ( PlatformDetection ) , nameof ( PlatformDetection . IsPreciseGcSupported ) ) ]
310
+ public void FinalizeFlushesWriteBufferForDerivedFileStreamWithFinalizerCreatedFromIntPtr ( )
311
+ => VerifyFlushedBufferOnFinalization (
312
+ filePath => new DerivedFileStreamWithFinalizer (
313
+ File . OpenHandle ( filePath , FileMode . Create , FileAccess . Write , FileShare . ReadWrite | FileShare . Delete , FileOptions . None ) . DangerousGetHandle ( ) ,
314
+ FileAccess . Write ,
315
+ ownsHandle : true ) ) ;
316
+
317
+ [ ConditionalFact ( typeof ( PlatformDetection ) , nameof ( PlatformDetection . IsPreciseGcSupported ) ) ]
318
+ public void FinalizeFlushesWriteBufferForDerivedFileStreamWithoutFinalizerCreatedFromPath ( )
319
+ => VerifyFlushedBufferOnFinalization (
320
+ filePath => new DerivedFileStreamWithoutFinalizer ( filePath , FileMode . Create , FileAccess . Write , FileShare . ReadWrite | FileShare . Delete , bufferSize : 4096 , FileOptions . None ) ) ;
321
+
322
+ [ ConditionalFact ( typeof ( PlatformDetection ) , nameof ( PlatformDetection . IsPreciseGcSupported ) ) ]
323
+ public void FinalizeFlushesWriteBufferForDerivedFileStreamWithoutFinalizerCreatedFromSafeFileHandle ( )
324
+ => VerifyFlushedBufferOnFinalization (
325
+ filePath => new DerivedFileStreamWithoutFinalizer (
326
+ File . OpenHandle ( filePath , FileMode . Create , FileAccess . Write , FileShare . ReadWrite | FileShare . Delete , FileOptions . None ) ,
327
+ FileAccess . Write , bufferSize : 4096 , isAsync : false ) ) ;
328
+
329
+ [ ConditionalFact ( typeof ( PlatformDetection ) , nameof ( PlatformDetection . IsPreciseGcSupported ) ) ]
330
+ public void FinalizeFlushesWriteBufferForDerivedFileStreamWithoutFinalizerCreatedFromIntPtr ( )
331
+ => VerifyFlushedBufferOnFinalization (
332
+ filePath => new DerivedFileStreamWithoutFinalizer (
333
+ File . OpenHandle ( filePath , FileMode . Create , FileAccess . Write , FileShare . ReadWrite | FileShare . Delete , FileOptions . None ) . DangerousGetHandle ( ) ,
334
+ FileAccess . Write ,
335
+ ownsHandle : true ) ) ;
336
+
337
+ private void VerifyFlushedBufferOnFinalization ( Func < string , FileStream > factory )
218
338
{
219
339
string fileName = GetTestFilePath ( ) ;
220
340
221
341
// use a separate method to be sure that fs isn't rooted at time of GC.
222
- Action leakFs = ( ) =>
342
+ Func < string , Type > leakFs = filePath =>
223
343
{
224
344
// we must specify useAsync:false, otherwise the finalizer just kicks off an async write.
225
- FileStream fs = new FileStream ( fileName , FileMode . Create , FileAccess . Write , FileShare . ReadWrite | FileShare . Delete , bufferSize : 4096 , useAsync : false ) ;
345
+ FileStream fs = factory ( filePath ) ;
226
346
fs . Write ( TestBuffer , 0 , TestBuffer . Length ) ;
227
- fs = null ;
347
+ return fs . GetType ( ) ;
228
348
} ;
229
- leakFs ( ) ;
349
+
350
+ int beforeWithFinalizer = DerivedFileStreamWithFinalizer . DisposeFalseCalled ;
351
+ int beforeWithoutFinalizer = DerivedFileStreamWithoutFinalizer . DisposeFalseCalled ;
352
+ Type type = leakFs ( fileName ) ;
230
353
231
354
GC . Collect ( ) ;
232
355
GC . WaitForPendingFinalizers ( ) ;
@@ -238,6 +361,97 @@ public void FinalizeFlushesWriteBuffer()
238
361
fsr . Read ( buffer , 0 , buffer . Length ) ;
239
362
Assert . Equal ( TestBuffer , buffer ) ;
240
363
}
364
+
365
+ if ( type == typeof ( DerivedFileStreamWithFinalizer ) )
366
+ {
367
+ // derived type finalizer implicitly calls base type finalizer, hence +2
368
+ Assert . Equal ( beforeWithFinalizer + 2 , DerivedFileStreamWithFinalizer . DisposeFalseCalled ) ;
369
+ Assert . Equal ( 0 , DerivedFileStreamWithFinalizer . DisposeTrueCalled ) ;
370
+ }
371
+ else if ( type == typeof ( DerivedFileStreamWithoutFinalizer ) )
372
+ {
373
+ Assert . Equal ( beforeWithoutFinalizer + 1 , DerivedFileStreamWithoutFinalizer . DisposeFalseCalled ) ;
374
+ Assert . Equal ( 0 , DerivedFileStreamWithoutFinalizer . DisposeTrueCalled ) ;
375
+ }
376
+ }
377
+
378
+ // this type exists so DerivedFileStreamStrategy can be tested as well
379
+ public class DerivedFileStream : FileStream
380
+ {
381
+ public DerivedFileStream ( string path , FileMode mode , FileAccess access , FileShare share , int bufferSize , FileOptions options )
382
+ : base ( path , mode , access , share , bufferSize , options )
383
+ {
384
+ }
385
+ }
386
+
387
+ public static IEnumerable < object [ ] > GetFileStreamDisposeSuppressesStrategyFinalizationArgs ( )
388
+ {
389
+ foreach ( int bufferSize in new [ ] { 1 , 4096 } )
390
+ {
391
+ foreach ( FileOptions fileOptions in new [ ] { FileOptions . Asynchronous , FileOptions . None } )
392
+ {
393
+ yield return new object [ ] { bufferSize , fileOptions } ;
394
+ }
395
+ }
396
+ }
397
+
398
+ [ ConditionalTheory ( typeof ( PlatformDetection ) , nameof ( PlatformDetection . IsPreciseGcSupported ) ) ]
399
+ [ MemberData ( nameof ( GetFileStreamDisposeSuppressesStrategyFinalizationArgs ) ) ]
400
+ public Task DisposeSuppressesStrategyFinalization ( int bufferSize , FileOptions options )
401
+ => VerifyStrategyFinalization (
402
+ ( ) => new FileStream ( GetTestFilePath ( ) , FileMode . Create , FileAccess . Write , FileShare . None , bufferSize , options | FileOptions . DeleteOnClose ) ,
403
+ false ) ;
404
+
405
+ [ ConditionalTheory ( typeof ( PlatformDetection ) , nameof ( PlatformDetection . IsPreciseGcSupported ) ) ]
406
+ [ MemberData ( nameof ( GetFileStreamDisposeSuppressesStrategyFinalizationArgs ) ) ]
407
+ public Task DisposeSuppressesStrategyFinalizationAsync ( int bufferSize , FileOptions options )
408
+ => VerifyStrategyFinalization (
409
+ ( ) => new FileStream ( GetTestFilePath ( ) , FileMode . Create , FileAccess . Write , FileShare . None , bufferSize , options | FileOptions . DeleteOnClose ) ,
410
+ true ) ;
411
+
412
+ [ ConditionalTheory ( typeof ( PlatformDetection ) , nameof ( PlatformDetection . IsPreciseGcSupported ) ) ]
413
+ [ MemberData ( nameof ( GetFileStreamDisposeSuppressesStrategyFinalizationArgs ) ) ]
414
+ public Task DerivedFileStreamDisposeSuppressesStrategyFinalization ( int bufferSize , FileOptions options )
415
+ => VerifyStrategyFinalization (
416
+ ( ) => new DerivedFileStream ( GetTestFilePath ( ) , FileMode . Create , FileAccess . Write , FileShare . None , bufferSize , options | FileOptions . DeleteOnClose ) ,
417
+ false ) ;
418
+
419
+ [ ConditionalTheory ( typeof ( PlatformDetection ) , nameof ( PlatformDetection . IsPreciseGcSupported ) ) ]
420
+ [ MemberData ( nameof ( GetFileStreamDisposeSuppressesStrategyFinalizationArgs ) ) ]
421
+ public Task DerivedFileStreamDisposeSuppressesStrategyFinalizationAsync ( int bufferSize , FileOptions options )
422
+ => VerifyStrategyFinalization (
423
+ ( ) => new DerivedFileStream ( GetTestFilePath ( ) , FileMode . Create , FileAccess . Write , FileShare . None , bufferSize , options | FileOptions . DeleteOnClose ) ,
424
+ true ) ;
425
+
426
+ private static async Task VerifyStrategyFinalization ( Func < FileStream > factory , bool useAsync )
427
+ {
428
+ WeakReference weakReference = await EnsureFileStreamIsNotRooted ( factory , useAsync ) ;
429
+ Assert . True ( weakReference . IsAlive ) ;
430
+ GC . Collect ( ) ;
431
+ Assert . False ( weakReference . IsAlive ) ;
432
+
433
+ // separate method to avoid JIT lifetime-extension issues
434
+ static async Task < WeakReference > EnsureFileStreamIsNotRooted ( Func < FileStream > factory , bool useAsync )
435
+ {
436
+ FileStream fs = factory ( ) ;
437
+ WeakReference weakReference = new WeakReference (
438
+ ( Stream ) typeof ( FileStream )
439
+ . GetField ( "_strategy" , Reflection . BindingFlags . NonPublic | Reflection . BindingFlags . Instance )
440
+ . GetValue ( fs ) ,
441
+ trackResurrection : true ) ;
442
+
443
+ Assert . True ( weakReference . IsAlive ) ;
444
+
445
+ if ( useAsync )
446
+ {
447
+ await fs . DisposeAsync ( ) ;
448
+ }
449
+ {
450
+ fs . Dispose ( ) ;
451
+ }
452
+
453
+ return weakReference ;
454
+ }
241
455
}
242
456
}
243
457
}
0 commit comments