|
4 | 4 | package com.azure.storage.blob.nio; |
5 | 5 |
|
6 | 6 | import com.azure.storage.blob.BlobClient |
| 7 | +import com.azure.storage.blob.specialized.BlobOutputStream |
| 8 | +import org.mockito.Answers |
| 9 | +import org.mockito.Mockito |
7 | 10 | import spock.lang.Unroll |
8 | 11 |
|
9 | 12 | import java.nio.ByteBuffer |
@@ -63,6 +66,68 @@ class AzureSeekableByteChannelTest extends APISpec { |
63 | 66 | os.toByteArray() == fileContent |
64 | 67 | } |
65 | 68 |
|
| 69 | + def "Read loop until EOF"() { |
| 70 | + setup: |
| 71 | + def fileContent = new byte[sourceFileSize] |
| 72 | + fileStream.read(fileContent) |
| 73 | + def os = new ByteArrayOutputStream(sourceFileSize) |
| 74 | + def rand = new Random() |
| 75 | + long timeLimit = System.currentTimeMillis() + 60_000 // fail if test runs >= 1 minute |
| 76 | + |
| 77 | + when: |
| 78 | + while (System.currentTimeMillis() < timeLimit) { // ensures test duration is bounded |
| 79 | + def buffer = ByteBuffer.allocate(rand.nextInt(1024 * 1024)) |
| 80 | + int readAmount = readByteChannel.read(buffer) |
| 81 | + if (readAmount == -1) { |
| 82 | + break; // reached EOF |
| 83 | + } |
| 84 | + os.write(buffer.array(), 0, readAmount) // limit the write in case we allocated more than we needed |
| 85 | + } |
| 86 | + |
| 87 | + then: |
| 88 | + os.toByteArray() == fileContent |
| 89 | + System.currentTimeMillis() < timeLimit // else potential inf. loop if read() always returns 0 |
| 90 | + } |
| 91 | + |
| 92 | + def "Read respect dest buffer pos"() { |
| 93 | + setup: |
| 94 | + def fileContent = new byte[sourceFileSize] |
| 95 | + fileStream.read(fileContent) |
| 96 | + |
| 97 | + def rand = new Random() |
| 98 | + int initialOffset = rand.nextInt(512) + 1 // always > 0 |
| 99 | + byte[] randArray = new byte[2 * initialOffset + sourceFileSize] |
| 100 | + rand.nextBytes(randArray) // fill with random bytes |
| 101 | + |
| 102 | + // copy same random bytes, but in this copy some will eventually be overwritten by read() |
| 103 | + byte[] destArray = new byte[randArray.length] |
| 104 | + System.arraycopy(randArray, 0, destArray, 0, randArray.length) |
| 105 | + def dest = ByteBuffer.wrap(destArray) |
| 106 | + dest.position(initialOffset) // will have capacity on either side that should not be touched |
| 107 | + |
| 108 | + when: |
| 109 | + int readAmount = 0; |
| 110 | + while (readAmount != -1) { |
| 111 | + assert dest.position() != 0 |
| 112 | + readAmount = readByteChannel.read(dest) // backed by an array, but position != 0 |
| 113 | + } |
| 114 | + |
| 115 | + then: |
| 116 | + dest.position() == initialOffset + sourceFileSize |
| 117 | + compareInputStreams( // destination content should match file content at initial read position |
| 118 | + new ByteArrayInputStream(destArray, initialOffset, sourceFileSize), |
| 119 | + new ByteArrayInputStream(fileContent), |
| 120 | + sourceFileSize) |
| 121 | + compareInputStreams( // destination content should be untouched prior to initial position |
| 122 | + new ByteArrayInputStream(destArray, 0, initialOffset), |
| 123 | + new ByteArrayInputStream(randArray, 0, initialOffset), |
| 124 | + initialOffset) |
| 125 | + compareInputStreams( // destination content should be untouched past end of read |
| 126 | + new ByteArrayInputStream(destArray, initialOffset + sourceFileSize, initialOffset), |
| 127 | + new ByteArrayInputStream(randArray, initialOffset + sourceFileSize, initialOffset), |
| 128 | + initialOffset) |
| 129 | + } |
| 130 | + |
66 | 131 | def "Read fs close"() { |
67 | 132 | when: |
68 | 133 | fs.close() |
@@ -96,6 +161,61 @@ class AzureSeekableByteChannelTest extends APISpec { |
96 | 161 | compareInputStreams(writeBc.openInputStream(), new ByteArrayInputStream(fileContent), sourceFileSize) |
97 | 162 | } |
98 | 163 |
|
| 164 | + def "Write respect src buffer pos"() { |
| 165 | + setup: |
| 166 | + def rand = new Random() |
| 167 | + int initialOffset = rand.nextInt(512) + 1 // always > 0 |
| 168 | + def srcBufferContent = new byte[2 * initialOffset + sourceFileSize] |
| 169 | + rand.nextBytes(srcBufferContent) // fill with random bytes |
| 170 | + |
| 171 | + def fileContent = new byte[sourceFileSize] |
| 172 | + fileStream.read(fileContent) |
| 173 | + |
| 174 | + // place expected file content into source buffer at random location, retain other random bytes |
| 175 | + System.arraycopy(fileContent, 0, srcBufferContent, initialOffset, sourceFileSize) |
| 176 | + def srcBuffer = ByteBuffer.wrap(srcBufferContent) |
| 177 | + srcBuffer.position(initialOffset) |
| 178 | + srcBuffer.limit(initialOffset + sourceFileSize) |
| 179 | + |
| 180 | + // This test aims to observe the actual bytes written by the ByteChannel to the underlying OutputStream, |
| 181 | + // not just the number of bytes allegedly written as reported by its position. It would prefer to examine |
| 182 | + // the OutputStream directly, but the channel requires the specific NioBlobOutputStream implementation |
| 183 | + // and does not accept something generic like a ByteArrayOutputStream. NioBlobOutputStream is final, so |
| 184 | + // it cannot be subclassed or mocked and has little state of its own -- writes go to a BlobOutputStream. |
| 185 | + // That class is abstract, but its constructor is not accessible outside its package and cannot normally |
| 186 | + // be subclassed to provide custom behavior, but a runtime mocking framework like Mockito can. This is |
| 187 | + // the nearest accessible observation point, so the test mocks a BlobOutputStream such that all write |
| 188 | + // methods store data in ByteArrayOutputStream which it can later examine for its size and content. |
| 189 | + def actualOutput = new ByteArrayOutputStream(sourceFileSize) |
| 190 | + def blobOutputStream = Mockito.mock( |
| 191 | + BlobOutputStream.class, Mockito.withSettings().useConstructor(4096 /* block size */)) |
| 192 | + Mockito.doAnswer( { invoked -> actualOutput.write(invoked.getArgument(0)) } ) |
| 193 | + .when(blobOutputStream).write(Mockito.anyInt()) |
| 194 | + Mockito.doAnswer( { invoked -> actualOutput.writeBytes(invoked.getArgument(0)) } ) |
| 195 | + .when(blobOutputStream).write(Mockito.any(byte[].class)) |
| 196 | + Mockito.doAnswer( { invoked -> actualOutput.write( |
| 197 | + invoked.getArgument(0), invoked.getArgument(1), invoked.getArgument(2)) } ) |
| 198 | + .when(blobOutputStream).write(Mockito.any(byte[].class), Mockito.anyInt(), Mockito.anyInt()) |
| 199 | + def path = writeByteChannel.getPath() |
| 200 | + writeByteChannel = new AzureSeekableByteChannel(new NioBlobOutputStream(blobOutputStream, path), path) |
| 201 | + |
| 202 | + when: |
| 203 | + int written = 0 |
| 204 | + while (written < sourceFileSize) { |
| 205 | + written += writeByteChannel.write(srcBuffer) |
| 206 | + } |
| 207 | + writeByteChannel.close() |
| 208 | + |
| 209 | + then: |
| 210 | + srcBuffer.position() == initialOffset + sourceFileSize // src buffer position SHOULD be updated |
| 211 | + srcBuffer.limit() == srcBuffer.position() // limit SHOULD be unchanged (still at end of content) |
| 212 | + // the above report back to the caller, but this verifies the correct bytes are going to the blob: |
| 213 | + compareInputStreams( |
| 214 | + new ByteArrayInputStream(actualOutput.toByteArray()), |
| 215 | + new ByteArrayInputStream(fileContent), |
| 216 | + sourceFileSize) |
| 217 | + } |
| 218 | + |
99 | 219 | def "Write fs close"() { |
100 | 220 | when: |
101 | 221 | fs.close() |
@@ -219,10 +339,10 @@ class AzureSeekableByteChannelTest extends APISpec { |
219 | 339 | thrown(IllegalArgumentException) |
220 | 340 |
|
221 | 341 | when: |
222 | | - readByteChannel.position(sourceFileSize + 1) |
| 342 | + readByteChannel.position(sourceFileSize) // position is 0-based, so seeking to size --> EOF |
223 | 343 |
|
224 | 344 | then: |
225 | | - readByteChannel.read(ByteBuffer.allocate(1)) == -1 // Seeking past the end and then reading should indicate EOF |
| 345 | + readByteChannel.read(ByteBuffer.allocate(1)) == -1 // Seeking to the end and then reading should indicate EOF |
226 | 346 | } |
227 | 347 |
|
228 | 348 | def "Seek fs close"() { |
|
0 commit comments