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