/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.j2objc.io;
import com.google.j2objc.annotations.AutoreleasePool;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Random;
import junit.framework.TestCase;
/*-[
@interface ComGoogleJ2objcIoAsyncPipedNSInputStreamAdapterTest_NativeInputStreamConsumer ()
<NSStreamDelegate>
@end
]-*/
/**
* Tests for AsyncPipedNSInputStreamAdapter.
*
* @author Lukhnos Liu
*/
public class AsyncPipedNSInputStreamAdapterTest extends TestCase {
// Use a large data source to create some memory pressure, and make read and write buffer sizes
// "misaligned" with the stream buffer size to test handling of leftover data.
private static final int SOURCE_DATA_SIZE = 1024 * 1024;
private static final int STREAM_BUFFER_SIZE = 128 * 1024;
private static final int READ_BUFFER_SIZE = 120000;
private static final int WRITE_CHUNK_SIZE = STREAM_BUFFER_SIZE + 1111;
private static final int PARTIAL_SIZE = 212345;
byte[] randomData;
/**
* An NSInputStream consumer that uses a run loop in the current thread to block until the
* delegate method sees the end.
*/
static class NativeInputStreamConsumer {
final byte[] readBuffer = new byte[READ_BUFFER_SIZE];
final int stopReadingAt;
Object accumulatedData;
NativeInputStreamConsumer() {
stopReadingAt = -1;
}
NativeInputStreamConsumer(int stopReadingAt) {
this.stopReadingAt = stopReadingAt;
}
native byte[] getBytes() /*-[
return [IOSByteArray arrayWithNSData:(NSData *)accumulatedData_];
]-*/;
native void readUntilEnd(Object inputStream) /*-[
if (!accumulatedData_) {
accumulatedData_ = [[NSMutableData alloc] init];
}
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[(NSInputStream *)inputStream setDelegate:self];
[(NSInputStream *)inputStream scheduleInRunLoop:runLoop forMode:NSRunLoopCommonModes];
[(NSInputStream *)inputStream open];
CFRunLoopRun();
]-*/;
/*-[
- (void)closeAndQuit:(NSStream *)aStream {
[aStream close];
[aStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
// For NSInputStream, we need to call this explicitly to quit the runloop.
CFRunLoopStop(CFRunLoopGetCurrent());
}
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventNone:
case NSStreamEventOpenCompleted:
case NSStreamEventHasSpaceAvailable:
break;
case NSStreamEventErrorOccurred:
case NSStreamEventEndEncountered:
break;
case NSStreamEventHasBytesAvailable: {
if (stopReadingAt_ >= 0 && [accumulatedData_ length] >= stopReadingAt_) {
[self closeAndQuit:aStream];
} else {
uint8_t *ptr = (uint8_t *)[readBuffer_ byteRefAtIndex:0];
jint readSize = [readBuffer_ length];
if (stopReadingAt_ >= 0) {
jint remainder = stopReadingAt_ - (jint)[(NSMutableData *)accumulatedData_ length];
if (remainder < readSize) {
readSize = remainder;
}
}
NSInteger bytesRead = [(NSInputStream *)aStream read:ptr maxLength:readSize];
if (bytesRead > 0) {
[(NSMutableData *)accumulatedData_ appendBytes:(uint8_t *)ptr length:bytesRead];
if (stopReadingAt_ >= 0 && [accumulatedData_ length] >= stopReadingAt_) {
[self closeAndQuit:aStream];
}
} else {
[self closeAndQuit:aStream];
}
}
break;
}
}
}
]-*/
}
/**
* An asynchronous data provider.
*/
static class DataProvider implements AsyncPipedNSInputStreamAdapter.Delegate {
int offset;
final byte[] data;
final int dataSize;
DataProvider(byte[] data) {
this.data = data;
dataSize = this.data.length;
}
DataProvider(byte[] data, int stopWritingAt) {
this.data = data;
dataSize = stopWritingAt;
}
int getTotalWritten() {
return offset;
}
@Override
public void offerData(OutputStream stream) {
try {
int remaining = dataSize - offset;
int len = (remaining > WRITE_CHUNK_SIZE) ? WRITE_CHUNK_SIZE : remaining;
if (len == 0) {
stream.close();
} else {
stream.write(data, offset, len);
offset += len;
if (offset >= dataSize) {
stream.close();
}
}
} catch (IOException e) {
throw new AssertionError(e);
}
}
}
@Override
protected void setUp() throws Exception {
randomData = new byte[SOURCE_DATA_SIZE];
new Random().nextBytes(randomData);
}
@Override
protected void tearDown() throws Exception {
// Reduce memory pressure.
randomData = null;
}
@AutoreleasePool
public void testFullWriteAndRead() {
DataProvider provider = new DataProvider(randomData);
NativeInputStreamConsumer consumer = new NativeInputStreamConsumer();
Object stream = AsyncPipedNSInputStreamAdapter.create(provider, STREAM_BUFFER_SIZE);
consumer.readUntilEnd(stream);
assertTrue("The entire source is read", Arrays.equals(randomData, consumer.getBytes()));
}
@AutoreleasePool
public void testNothingWritten() {
DataProvider provider = new DataProvider(randomData, 0);
NativeInputStreamConsumer consumer = new NativeInputStreamConsumer();
Object stream = AsyncPipedNSInputStreamAdapter.create(provider, STREAM_BUFFER_SIZE);
consumer.readUntilEnd(stream);
assertEquals(0, provider.getTotalWritten());
assertEquals(0, consumer.getBytes().length);
}
@AutoreleasePool
public void testNothingRead() {
DataProvider provider = new DataProvider(randomData);
NativeInputStreamConsumer consumer = new NativeInputStreamConsumer(0);
Object stream = AsyncPipedNSInputStreamAdapter.create(provider, STREAM_BUFFER_SIZE);
consumer.readUntilEnd(stream);
assertTrue("May provide more than actually read", provider.getTotalWritten() >= 0);
assertEquals(0, consumer.getBytes().length);
}
@AutoreleasePool
public void testPartialRead() {
DataProvider provider = new DataProvider(randomData);
NativeInputStreamConsumer consumer = new NativeInputStreamConsumer(PARTIAL_SIZE);
Object stream = AsyncPipedNSInputStreamAdapter.create(provider, STREAM_BUFFER_SIZE);
consumer.readUntilEnd(stream);
assertTrue("May provide more than actually read", provider.getTotalWritten() >= PARTIAL_SIZE);
assertEquals(PARTIAL_SIZE, consumer.getBytes().length);
assertTrue(Arrays.equals(Arrays.copyOfRange(randomData, 0, PARTIAL_SIZE), consumer.getBytes()));
}
@AutoreleasePool
public void testPartialWrite() {
DataProvider provider = new DataProvider(randomData, PARTIAL_SIZE);
NativeInputStreamConsumer consumer = new NativeInputStreamConsumer();
Object stream = AsyncPipedNSInputStreamAdapter.create(provider, STREAM_BUFFER_SIZE);
consumer.readUntilEnd(stream);
assertEquals(PARTIAL_SIZE, provider.getTotalWritten());
assertEquals(PARTIAL_SIZE, consumer.getBytes().length);
assertTrue(
"Part of the source is read",
Arrays.equals(Arrays.copyOfRange(randomData, 0, PARTIAL_SIZE), consumer.getBytes()));
}
@AutoreleasePool
public void testTrivialCreate() {
DataProvider provider = new DataProvider(randomData);
Object stream = AsyncPipedNSInputStreamAdapter.create(provider, STREAM_BUFFER_SIZE);
assertNotNull(stream);
// This is to test that the background thread created by the adapter is retaining the stream
// objects properly. If it were not, the program would crash after this method exits and its
// outer autorelease pool drains.
}
}