1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Collections.Generic; 6 using System.Linq; 7 using System.Threading; 8 using System.Threading.Tasks; 9 using Xunit; 10 11 namespace System.IO.Tests 12 { 13 public partial class StreamCopyToTests 14 { 15 [Fact] IfCanSeekIsFalseLengthAndPositionShouldNotBeCalled()16 public void IfCanSeekIsFalseLengthAndPositionShouldNotBeCalled() 17 { 18 var baseStream = new DelegateStream( 19 canReadFunc: () => true, 20 canSeekFunc: () => false, 21 readFunc: (buffer, offset, count) => 0); 22 var trackingStream = new CallTrackingStream(baseStream); 23 24 var dest = Stream.Null; 25 trackingStream.CopyTo(dest); 26 27 Assert.InRange(trackingStream.TimesCalled(nameof(trackingStream.CanSeek)), 0, 1); 28 Assert.Equal(0, trackingStream.TimesCalled(nameof(trackingStream.Length))); 29 Assert.Equal(0, trackingStream.TimesCalled(nameof(trackingStream.Position))); 30 // We can't override CopyTo since it's not virtual, so checking TimesCalled 31 // for CopyTo will result in 0. Instead, we check that Read was called, 32 // and validate the parameters passed there. 33 Assert.Equal(1, trackingStream.TimesCalled(nameof(trackingStream.Read))); 34 35 byte[] outerBuffer = trackingStream.ReadBuffer; 36 int outerOffset = trackingStream.ReadOffset; 37 int outerCount = trackingStream.ReadCount; 38 39 Assert.NotNull(outerBuffer); 40 Assert.InRange(outerOffset, 0, outerBuffer.Length - outerCount); 41 Assert.InRange(outerCount, 1, int.MaxValue); // the buffer can't be size 0 42 } 43 44 [Fact] AsyncIfCanSeekIsFalseLengthAndPositionShouldNotBeCalled()45 public async Task AsyncIfCanSeekIsFalseLengthAndPositionShouldNotBeCalled() 46 { 47 var baseStream = new DelegateStream( 48 canReadFunc: () => true, 49 canSeekFunc: () => false, 50 readFunc: (buffer, offset, count) => 0); 51 var trackingStream = new CallTrackingStream(baseStream); 52 53 var dest = Stream.Null; 54 await trackingStream.CopyToAsync(dest); 55 56 Assert.InRange(trackingStream.TimesCalled(nameof(trackingStream.CanSeek)), 0, 1); 57 Assert.Equal(0, trackingStream.TimesCalled(nameof(trackingStream.Length))); 58 Assert.Equal(0, trackingStream.TimesCalled(nameof(trackingStream.Position))); 59 Assert.Equal(1, trackingStream.TimesCalled(nameof(trackingStream.CopyToAsync))); 60 61 Assert.Same(dest, trackingStream.CopyToAsyncDestination); 62 Assert.InRange(trackingStream.CopyToAsyncBufferSize, 1, int.MaxValue); 63 Assert.Equal(CancellationToken.None, trackingStream.CopyToAsyncCancellationToken); 64 } 65 66 [Fact] IfCanSeekIsTrueLengthAndPositionShouldOnlyBeCalledOnce()67 public void IfCanSeekIsTrueLengthAndPositionShouldOnlyBeCalledOnce() 68 { 69 var baseStream = new DelegateStream( 70 canReadFunc: () => true, 71 canSeekFunc: () => true, 72 readFunc: (buffer, offset, count) => 0, 73 lengthFunc: () => 0L, 74 positionGetFunc: () => 0L); 75 var trackingStream = new CallTrackingStream(baseStream); 76 77 var dest = Stream.Null; 78 trackingStream.CopyTo(dest); 79 80 Assert.InRange(trackingStream.TimesCalled(nameof(trackingStream.Length)), 0, 1); 81 Assert.InRange(trackingStream.TimesCalled(nameof(trackingStream.Position)), 0, 1); 82 } 83 84 [Fact] AsyncIfCanSeekIsTrueLengthAndPositionShouldOnlyBeCalledOnce()85 public async Task AsyncIfCanSeekIsTrueLengthAndPositionShouldOnlyBeCalledOnce() 86 { 87 var baseStream = new DelegateStream( 88 canReadFunc: () => true, 89 canSeekFunc: () => true, 90 readFunc: (buffer, offset, count) => 0, 91 lengthFunc: () => 0L, 92 positionGetFunc: () => 0L); 93 var trackingStream = new CallTrackingStream(baseStream); 94 95 var dest = Stream.Null; 96 await trackingStream.CopyToAsync(dest); 97 98 Assert.InRange(trackingStream.TimesCalled(nameof(trackingStream.Length)), 0, 1); 99 Assert.InRange(trackingStream.TimesCalled(nameof(trackingStream.Position)), 0, 1); 100 } 101 102 [Theory] 103 [MemberData(nameof(LengthIsLessThanOrEqualToPosition))] IfLengthIsLessThanOrEqualToPositionCopyToShouldStillBeCalledWithAPositiveBufferSize(long length, long position)104 public void IfLengthIsLessThanOrEqualToPositionCopyToShouldStillBeCalledWithAPositiveBufferSize(long length, long position) 105 { 106 // Streams with their Lengths <= their Positions, e.g. 107 // new MemoryStream { Position = 3 }.SetLength(1) 108 // should still be called CopyTo{Async} on with a 109 // bufferSize of at least 1. 110 111 var baseStream = new DelegateStream( 112 canReadFunc: () => true, 113 canSeekFunc: () => true, 114 lengthFunc: () => length, 115 positionGetFunc: () => position, 116 readFunc: (buffer, offset, count) => 0); 117 var trackingStream = new CallTrackingStream(baseStream); 118 119 var dest = Stream.Null; 120 trackingStream.CopyTo(dest); 121 122 // CopyTo is not virtual, so we can't override it in 123 // CallTrackingStream and record the arguments directly. 124 // Instead, validate the arguments passed to Read. 125 126 Assert.Equal(1, trackingStream.TimesCalled(nameof(trackingStream.Read))); 127 128 byte[] outerBuffer = trackingStream.ReadBuffer; 129 int outerOffset = trackingStream.ReadOffset; 130 int outerCount = trackingStream.ReadCount; 131 132 Assert.NotNull(outerBuffer); 133 Assert.InRange(outerOffset, 0, outerBuffer.Length - outerCount); 134 Assert.InRange(outerCount, 1, int.MaxValue); 135 } 136 137 [Theory] 138 [MemberData(nameof(LengthIsLessThanOrEqualToPosition))] AsyncIfLengthIsLessThanOrEqualToPositionCopyToShouldStillBeCalledWithAPositiveBufferSize(long length, long position)139 public async Task AsyncIfLengthIsLessThanOrEqualToPositionCopyToShouldStillBeCalledWithAPositiveBufferSize(long length, long position) 140 { 141 var baseStream = new DelegateStream( 142 canReadFunc: () => true, 143 canSeekFunc: () => true, 144 lengthFunc: () => length, 145 positionGetFunc: () => position, 146 readFunc: (buffer, offset, count) => 0); 147 var trackingStream = new CallTrackingStream(baseStream); 148 149 var dest = Stream.Null; 150 await trackingStream.CopyToAsync(dest); 151 152 Assert.Same(dest, trackingStream.CopyToAsyncDestination); 153 Assert.InRange(trackingStream.CopyToAsyncBufferSize, 1, int.MaxValue); 154 Assert.Equal(CancellationToken.None, trackingStream.CopyToAsyncCancellationToken); 155 } 156 157 [Theory] 158 [MemberData(nameof(LengthMinusPositionPositiveOverflows))] IfLengthMinusPositionPositiveOverflowsBufferSizeShouldStillBePositive(long length, long position)159 public void IfLengthMinusPositionPositiveOverflowsBufferSizeShouldStillBePositive(long length, long position) 160 { 161 // The new implementation of Stream.CopyTo calculates the bytes left 162 // in the Stream by calling Length - Position. This can overflow to a 163 // negative number, so this tests that if that happens we don't send 164 // in a negative bufferSize. 165 166 var baseStream = new DelegateStream( 167 canReadFunc: () => true, 168 canSeekFunc: () => true, 169 lengthFunc: () => length, 170 positionGetFunc: () => position, 171 readFunc: (buffer, offset, count) => 0); 172 var trackingStream = new CallTrackingStream(baseStream); 173 174 var dest = Stream.Null; 175 trackingStream.CopyTo(dest); 176 177 // CopyTo is not virtual, so we can't override it in 178 // CallTrackingStream and record the arguments directly. 179 // Instead, validate the arguments passed to Read. 180 181 Assert.Equal(1, trackingStream.TimesCalled(nameof(trackingStream.Read))); 182 183 byte[] outerBuffer = trackingStream.ReadBuffer; 184 int outerOffset = trackingStream.ReadOffset; 185 int outerCount = trackingStream.ReadCount; 186 187 Assert.NotNull(outerBuffer); 188 Assert.InRange(outerOffset, 0, outerBuffer.Length - outerCount); 189 Assert.InRange(outerCount, 1, int.MaxValue); 190 } 191 192 [Theory] 193 [MemberData(nameof(LengthMinusPositionPositiveOverflows))] AsyncIfLengthMinusPositionPositiveOverflowsBufferSizeShouldStillBePositive(long length, long position)194 public async Task AsyncIfLengthMinusPositionPositiveOverflowsBufferSizeShouldStillBePositive(long length, long position) 195 { 196 var baseStream = new DelegateStream( 197 canReadFunc: () => true, 198 canSeekFunc: () => true, 199 lengthFunc: () => length, 200 positionGetFunc: () => position, 201 readFunc: (buffer, offset, count) => 0); 202 var trackingStream = new CallTrackingStream(baseStream); 203 204 var dest = Stream.Null; 205 await trackingStream.CopyToAsync(dest); 206 207 // Note: We can't check how many times ReadAsync was called 208 // here, since trackingStream overrides CopyToAsync and forwards 209 // to the inner (non-tracking) stream for the implementation 210 211 Assert.Same(dest, trackingStream.CopyToAsyncDestination); 212 Assert.InRange(trackingStream.CopyToAsyncBufferSize, 1, int.MaxValue); 213 Assert.Equal(CancellationToken.None, trackingStream.CopyToAsyncCancellationToken); 214 } 215 216 [Theory] 217 [MemberData(nameof(LengthIsGreaterThanPositionAndDoesNotOverflow))] IfLengthIsGreaterThanPositionAndDoesNotOverflowEverythingShouldGoNormally(long length, long position)218 public void IfLengthIsGreaterThanPositionAndDoesNotOverflowEverythingShouldGoNormally(long length, long position) 219 { 220 const int ReadLimit = 7; 221 222 // Lambda state 223 byte[] outerBuffer = null; 224 int? outerOffset = null; 225 int? outerCount = null; 226 int readsLeft = ReadLimit; 227 228 var srcBase = new DelegateStream( 229 canReadFunc: () => true, 230 canSeekFunc: () => true, 231 lengthFunc: () => length, 232 positionGetFunc: () => position, 233 readFunc: (buffer, offset, count) => 234 { 235 Assert.NotNull(buffer); 236 Assert.InRange(offset, 0, buffer.Length - count); 237 Assert.InRange(count, 1, int.MaxValue); 238 239 // CopyTo should always pass in the same buffer/offset/count 240 241 if (outerBuffer != null) Assert.Same(outerBuffer, buffer); 242 else outerBuffer = buffer; 243 244 if (outerOffset != null) Assert.Equal(outerOffset, offset); 245 else outerOffset = offset; 246 247 if (outerCount != null) Assert.Equal(outerCount, count); 248 else outerCount = count; 249 250 return --readsLeft; // CopyTo will call Read on this ReadLimit times before stopping 251 }); 252 253 var src = new CallTrackingStream(srcBase); 254 255 var destBase = new DelegateStream( 256 canWriteFunc: () => true, 257 writeFunc: (buffer, offset, count) => 258 { 259 Assert.Same(outerBuffer, buffer); 260 Assert.Equal(outerOffset, offset); 261 Assert.Equal(readsLeft, count); 262 }); 263 264 var dest = new CallTrackingStream(destBase); 265 src.CopyTo(dest); 266 267 Assert.Equal(ReadLimit, src.TimesCalled(nameof(src.Read))); 268 Assert.Equal(ReadLimit - 1, dest.TimesCalled(nameof(dest.Write))); 269 } 270 271 [Theory] 272 [MemberData(nameof(LengthIsGreaterThanPositionAndDoesNotOverflow))] AsyncIfLengthIsGreaterThanPositionAndDoesNotOverflowEverythingShouldGoNormally(long length, long position)273 public async Task AsyncIfLengthIsGreaterThanPositionAndDoesNotOverflowEverythingShouldGoNormally(long length, long position) 274 { 275 const int ReadLimit = 7; 276 277 // Lambda state 278 byte[] outerBuffer = null; 279 int? outerOffset = null; 280 int? outerCount = null; 281 int readsLeft = ReadLimit; 282 283 var srcBase = new DelegateStream( 284 canReadFunc: () => true, 285 canSeekFunc: () => true, 286 lengthFunc: () => length, 287 positionGetFunc: () => position, 288 readFunc: (buffer, offset, count) => 289 { 290 Assert.NotNull(buffer); 291 Assert.InRange(offset, 0, buffer.Length - count); 292 Assert.InRange(count, 1, int.MaxValue); 293 294 // CopyTo should always pass in the same buffer/offset/count 295 296 if (outerBuffer != null) Assert.Same(outerBuffer, buffer); 297 else outerBuffer = buffer; 298 299 if (outerOffset != null) Assert.Equal(outerOffset, offset); 300 else outerOffset = offset; 301 302 if (outerCount != null) Assert.Equal(outerCount, count); 303 else outerCount = count; 304 305 return --readsLeft; // CopyTo will call Read on this ReadLimit times before stopping 306 }); 307 308 var src = new CallTrackingStream(srcBase); 309 310 var destBase = new DelegateStream( 311 canWriteFunc: () => true, 312 writeFunc: (buffer, offset, count) => 313 { 314 Assert.Same(outerBuffer, buffer); 315 Assert.Equal(outerOffset, offset); 316 Assert.Equal(readsLeft, count); 317 }); 318 319 var dest = new CallTrackingStream(destBase); 320 await src.CopyToAsync(dest); 321 322 // Since we override CopyToAsync in CallTrackingStream, 323 // src.Read will actually not get called ReadLimit 324 // times, src.Inner.Read will. So, we just assert that 325 // CopyToAsync was called once for src. 326 327 Assert.Equal(1, src.TimesCalled(nameof(src.CopyToAsync))); 328 Assert.Equal(ReadLimit - 1, dest.TimesCalled(nameof(dest.WriteAsync))); // dest.WriteAsync will still get called repeatedly 329 } 330 331 // Member data 332 LengthIsLessThanOrEqualToPosition()333 public static IEnumerable<object[]> LengthIsLessThanOrEqualToPosition() 334 { 335 yield return new object[] { 5L, 5L }; // same number 336 yield return new object[] { 3L, 5L }; // length is less than position 337 yield return new object[] { -1L, -1L }; // negative numbers 338 yield return new object[] { 0L, 0L }; // both zero 339 yield return new object[] { -500L, 0L }; // negative number and zero 340 yield return new object[] { 0L, 500L }; // zero and positive number 341 yield return new object[] { -500L, 500L }; // negative and positive number 342 yield return new object[] { long.MinValue, long.MaxValue }; // length - position <= 0 will fail (overflow), but length <= position won't 343 } 344 LengthMinusPositionPositiveOverflows()345 public static IEnumerable<object[]> LengthMinusPositionPositiveOverflows() 346 { 347 yield return new object[] { long.MaxValue, long.MinValue }; // length - position will be -1 348 yield return new object[] { 1L, -long.MaxValue }; 349 } 350 LengthIsGreaterThanPositionAndDoesNotOverflow()351 public static IEnumerable<object[]> LengthIsGreaterThanPositionAndDoesNotOverflow() 352 { 353 yield return new object[] { 5L, 3L }; 354 yield return new object[] { -3L, -6L }; 355 yield return new object[] { 0L, -3L }; 356 yield return new object[] { long.MaxValue, 0 }; // should not overflow or OOM 357 yield return new object[] { 85000, 123 }; // at least in the current implementation, we max out the bufferSize at 81920 358 } 359 } 360 } 361