blob: 1aa56d8eebdedb1c781b0946f2d5adb88dd131ae [file] [log] [blame]
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// @dart = 2.9
// VMOptions=--enable-isolate-groups --experimental-enable-isolate-groups-jit
// VMOptions=--no-enable-isolate-groups
import "dart:async";
import "dart:io";
import "dart:isolate";
import "dart:math";
import "package:async_helper/async_helper.dart";
import "package:expect/expect.dart";
const String loopbackIPv4String = "127.0.0.1";
void testArguments() {
Expect.throws(() => RawSynchronousSocket.connectSync(null, 0));
Expect
.throws(() => RawSynchronousSocket.connectSync(loopbackIPv4String, null));
Expect.throws(
() => RawSynchronousSocket.connectSync(loopbackIPv4String, 65536));
Expect.throws(() => RawSynchronousSocket.connectSync(loopbackIPv4String, -1));
}
// The connection attempt happens on the main Dart thread and the OS timeout can
// be arbitrarily long, causing timeout issues on the build bots. This isn't an
// issue with the async sockets since the lookup for a connect call happens on
// the IO service thread.
/*
void testInvalidConnect() {
// Connect to an unknown DNS name.
try {
var socket = RawSynchronousSocket.connectSync("ko.faar.__hest__", 0);
Expect.fail("Failure expected");
} catch (e) {
Expect.isTrue(e is SocketException);
}
// Connect to an unavaliable IP-address.
try {
var socket = RawSynchronousSocket.connectSync("1.2.3.4", 0);
Expect.fail("Failure expected");
} catch (e) {
Expect.isTrue(e is SocketException);
}
}
*/
void testSimpleConnect() {
asyncStart();
RawServerSocket.bind(InternetAddress.loopbackIPv4, 0).then((server) {
var socket =
RawSynchronousSocket.connectSync(loopbackIPv4String, server.port);
server.listen((serverSocket) {
Expect.equals(socket.address, serverSocket.remoteAddress);
Expect.equals(socket.port, serverSocket.remotePort);
Expect.equals(socket.remoteAddress, server.address);
Expect.equals(socket.remotePort, server.port);
socket.closeSync();
server.close();
asyncEnd();
});
});
}
void testServerListenAfterConnect() {
asyncStart();
RawServerSocket.bind(InternetAddress.loopbackIPv4, 0).then((server) {
Expect.isTrue(server.port > 0);
var client =
RawSynchronousSocket.connectSync(loopbackIPv4String, server.port);
server.listen((socket) {
client.closeSync();
server.close();
socket.close();
asyncEnd();
});
});
}
const messageSize = 1000;
// Configuration fields for the EchoServer.
enum EchoServerTypes {
// Max accumulated connections to server before close. Defaults to 1.
CONNECTION_COUNT,
// Sets the range of the fields to check in the list generated by
// createTestData().
OFFSET_END,
OFFSET_START,
// The port used to communicate with an isolate.
ISOLATE_SEND_PORT,
// The port of the newly created echo server.
SERVER_PORT
}
List<int> createTestData() {
return new List<int>.generate(messageSize, (index) => index & 0xff);
}
// Consumes data generated by a test and compares it against the original test
// data. The optional fields, start and end, are used to compare against
// segments of the original test data list. In other words, data.length == (end
// - start).
void verifyTestData(List<int> data, [int start = 0, int end]) {
assert(data != null);
List<int> expected = createTestData();
if (end == null) {
end = data.length;
}
end = min(messageSize, end);
Expect.equals(end - start, data.length);
for (int i = 0; i < (end - start); i++) {
Expect.equals(expected[start + i], data[i]);
}
}
// The echo server is spawned in a new isolate and is used to test various
// synchronous read/write operations by echoing any data received back to the
// sender. The server should shutdown automatically after a specified number of
// socket disconnections (default: 1).
Future echoServer(var sendPort) async {
RawServerSocket.bind(InternetAddress.loopbackIPv4, 0).then((server) async {
ReceivePort receivePort = new ReceivePort();
Map response = {
EchoServerTypes.ISOLATE_SEND_PORT: receivePort.sendPort,
EchoServerTypes.SERVER_PORT: server.port
};
sendPort.send(response);
Map limits = await receivePort.first;
int start = limits[EchoServerTypes.OFFSET_START];
int end = limits[EchoServerTypes.OFFSET_END];
int length = end - start;
int connection_count = limits[EchoServerTypes.CONNECTION_COUNT] ?? 1;
int connections = 0;
sendPort = limits[EchoServerTypes.ISOLATE_SEND_PORT];
server.listen((client) {
int bytesRead = 0;
int bytesWritten = 0;
bool closedEventReceived = false;
List<int> data = new List<int>(length);
client.writeEventsEnabled = false;
client.listen((event) {
switch (event) {
case RawSocketEvent.read:
Expect.isTrue(bytesWritten == 0);
Expect.isTrue(client.available() > 0);
var buffer = client.read(client.available());
data.setRange(bytesRead, bytesRead + buffer.length, buffer);
bytesRead += buffer.length;
// Once we've read all the data, we can echo it back. Otherwise,
// keep waiting for more bytes.
if (bytesRead >= length) {
verifyTestData(data, start, end);
client.writeEventsEnabled = true;
}
break;
case RawSocketEvent.write:
Expect.isFalse(client.writeEventsEnabled);
bytesWritten +=
client.write(data, bytesWritten, data.length - bytesWritten);
if (bytesWritten < length) {
client.writeEventsEnabled = true;
} else if (bytesWritten == length) {
// Close the socket for writing from the server since we're done
// writing to this socket. The connection is closed completely
// after the client closes the socket for reading from the server.
client.shutdown(SocketDirection.send);
}
break;
case RawSocketEvent.readClosed:
client.close();
break;
case RawSocketEvent.closed:
Expect.isFalse(closedEventReceived);
closedEventReceived = true;
break;
default:
throw "Unexpected event $event";
}
}, onDone: () {
Expect.isTrue(closedEventReceived);
connections++;
if (connections >= connection_count) {
server.close();
}
});
}, onDone: () {
// Let the client know we're shutting down then kill the isolate.
sendPort.send(null);
Isolate.current.kill();
});
});
}
Future testSimpleReadWrite({bool dropReads}) async {
asyncStart();
// This test creates a server and a client connects. The client writes data
// to the socket and the server echos it back. The client confirms the data it
// reads is the same as the data sent, then closes the socket, resulting in
// the closing of the server, which responds on receivePort with null to
// specify the echo server isolate is about to be killed. If an error occurs
// in the echo server, the exception and stack trace are sent to receivePort,
// which prints the exception and stack trace before eventually throwing an
// error.
ReceivePort receivePort = new ReceivePort();
Isolate echo = await Isolate.spawn(echoServer, receivePort.sendPort);
Map response = await receivePort.first;
SendPort sendPort = response[EchoServerTypes.ISOLATE_SEND_PORT];
int serverInternetPort = response[EchoServerTypes.SERVER_PORT];
receivePort = new ReceivePort();
echo.addErrorListener(receivePort.sendPort);
Map limits = {
EchoServerTypes.OFFSET_START: 0,
EchoServerTypes.OFFSET_END: messageSize,
EchoServerTypes.ISOLATE_SEND_PORT: receivePort.sendPort
};
sendPort.send(limits);
try {
var socket = RawSynchronousSocket.connectSync(
loopbackIPv4String, serverInternetPort);
List<int> data = createTestData();
socket.writeFromSync(data);
List<int> result = socket.readSync(data.length);
verifyTestData(result);
socket.shutdown(SocketDirection.send);
socket.closeSync();
} catch (e, stack) {
print("Echo test failed in the client");
rethrow;
}
// Wait for the server to shutdown before finishing the test.
var result = await receivePort.first;
if (result != null) {
throw "Echo test failed in server!\nError: ${result[0]}\nStack trace:" +
" ${result[1]}";
}
asyncEnd();
}
Future testPartialRead() async {
asyncStart();
// This test is based on testSimpleReadWrite, but instead of reading the
// entire echoed message at once, it reads it in two calls to readIntoSync.
ReceivePort receivePort = new ReceivePort();
Isolate echo = await Isolate.spawn(echoServer, receivePort.sendPort);
Map response = await receivePort.first;
SendPort sendPort = response[EchoServerTypes.ISOLATE_SEND_PORT];
int serverInternetPort = response[EchoServerTypes.SERVER_PORT];
List<int> data = createTestData();
receivePort = new ReceivePort();
echo.addErrorListener(receivePort.sendPort);
Map limits = {
EchoServerTypes.OFFSET_START: 0,
EchoServerTypes.OFFSET_END: 1000,
EchoServerTypes.ISOLATE_SEND_PORT: receivePort.sendPort
};
sendPort.send(limits);
try {
var socket = RawSynchronousSocket.connectSync(
loopbackIPv4String, serverInternetPort);
int half_length = (data.length / 2).toInt();
// Send the full data list to the server.
socket.writeFromSync(data);
List<int> result = new List<int>(data.length);
// Read half at a time and check that there's still more bytes available.
socket.readIntoSync(result, 0, half_length);
verifyTestData(result.sublist(0, half_length), 0, half_length);
Expect.isTrue(socket.available() == (data.length - half_length));
// Read the second half and verify again.
socket.readIntoSync(result, half_length);
verifyTestData(result);
Expect.isTrue(socket.available() == 0);
socket.closeSync();
} catch (e, stack) {
print("Echo test failed in the client.");
rethrow;
}
// Wait for the server to shutdown before finishing the test.
var result = await receivePort.first;
if (result != null) {
throw "Echo test failed in server!\nError: ${result[0]}\nStack trace:" +
" ${result[1]}";
}
asyncEnd();
}
Future testPartialWrite() async {
asyncStart();
// This test is based on testSimpleReadWrite, but instead of writing the
// entire data buffer at once, it writes different parts of the buffer over
// multiple calls to writeFromSync.
ReceivePort receivePort = new ReceivePort();
Isolate echo = await Isolate.spawn(echoServer, receivePort.sendPort);
Map response = await receivePort.first;
List<int> data = createTestData();
SendPort sendPort = response[EchoServerTypes.ISOLATE_SEND_PORT];
int startOffset = 32;
int endOffset = (data.length / 2).toInt();
int serverInternetPort = response[EchoServerTypes.SERVER_PORT];
receivePort = new ReceivePort();
echo.addErrorListener(receivePort.sendPort);
Map limits = {
EchoServerTypes.OFFSET_START: startOffset,
EchoServerTypes.OFFSET_END: endOffset,
EchoServerTypes.ISOLATE_SEND_PORT: receivePort.sendPort
};
sendPort.send(limits);
try {
var socket = RawSynchronousSocket.connectSync(
loopbackIPv4String, serverInternetPort);
List<int> data = createTestData();
// Write a subset of data to the server.
socket.writeFromSync(data, startOffset, endOffset);
// Grab the response and verify it's correct.
List<int> result = new List<int>(endOffset - startOffset);
socket.readIntoSync(result);
Expect.equals(result.length, endOffset - startOffset);
verifyTestData(result, startOffset, endOffset);
socket.closeSync();
} catch (e, stack) {
print("Echo test failed in the client.");
rethrow;
}
// Wait for the server to shutdown before finishing the test.
var result = await receivePort.first;
if (result != null) {
throw "Echo test failed in server!\nError: ${result[0]}\nStack trace:" +
" ${result[1]}";
}
asyncEnd();
}
Future testShutdown() async {
asyncStart();
// This test creates a server and a client connects. The client then tries to
// perform various operations after being shutdown in a specific direction, to
// ensure reads or writes cannot be performed if the socket has been shutdown
// for reading or writing.
ReceivePort receivePort = new ReceivePort();
Isolate echo = await Isolate.spawn(echoServer, receivePort.sendPort);
Map response = await receivePort.first;
SendPort sendPort = response[EchoServerTypes.ISOLATE_SEND_PORT];
int serverInternetPort = response[EchoServerTypes.SERVER_PORT];
List<int> data = createTestData();
receivePort = new ReceivePort();
echo.addErrorListener(receivePort.sendPort);
Map limits = {
EchoServerTypes.OFFSET_START: 0,
EchoServerTypes.OFFSET_END: data.length,
EchoServerTypes.ISOLATE_SEND_PORT: receivePort.sendPort,
// Tell the server to shutdown after 3 sockets disconnect.
EchoServerTypes.CONNECTION_COUNT: 3
};
sendPort.send(limits);
try {
var socket = RawSynchronousSocket.connectSync(
loopbackIPv4String, serverInternetPort);
// Close from both directions. Shouldn't be able to read/write to the
// socket.
socket.shutdown(SocketDirection.both);
Expect.throws(
() => socket.writeFromSync(data), (e) => e is SocketException);
Expect.throws(
() => socket.readSync(data.length), (e) => e is SocketException);
socket.closeSync();
// Close the socket for reading then try and perform a read. This should
// cause a SocketException.
socket = RawSynchronousSocket.connectSync(
loopbackIPv4String, serverInternetPort);
socket.shutdown(SocketDirection.receive);
// Throws exception when the socket is closed for RECEIVE.
Expect.throws(
() => socket.readSync(data.length), (e) => e is SocketException);
socket.closeSync();
// Close the socket for writing and try to do a write. This should cause an
// OSError to be throw as the pipe is closed for writing.
socket = RawSynchronousSocket.connectSync(
loopbackIPv4String, serverInternetPort);
socket.shutdown(SocketDirection.send);
Expect.throws(
() => socket.writeFromSync(data), (e) => e is SocketException);
socket.closeSync();
} catch (e, stack) {
print("Echo test failed in client.");
rethrow;
}
// Wait for the server to shutdown before finishing the test.
var result = await receivePort.first;
if (result != null) {
throw "Echo test failed in server!\nError: ${result[0]}\nStack trace:" +
" ${result[1]}";
}
asyncEnd();
}
Future testInvalidReadWriteOperations() {
asyncStart();
RawServerSocket.bind(InternetAddress.loopbackIPv4, 0).then((server) {
server.listen((socket) {});
List<int> data = createTestData();
var socket =
RawSynchronousSocket.connectSync(loopbackIPv4String, server.port);
// Invalid writeFromSync invocations
Expect.throwsRangeError(() => socket.writeFromSync(data, data.length + 1));
Expect
.throwsRangeError(() => socket.writeFromSync(data, 0, data.length + 1));
Expect.throwsRangeError(() => socket.writeFromSync(data, 1, 0));
Expect.throwsArgumentError(() => socket.writeFromSync(data, null));
// Invalid readIntoSync invocations
List<int> buffer = new List<int>(10);
Expect
.throwsRangeError(() => socket.readIntoSync(buffer, buffer.length + 1));
Expect.throwsRangeError(
() => socket.readIntoSync(buffer, 0, buffer.length + 1));
Expect.throwsRangeError(() => socket.readIntoSync(buffer, 1, 0));
Expect.throwsArgumentError(() => socket.readIntoSync(buffer, null));
// Invalid readSync invocation
Expect.throwsArgumentError(() => socket.readSync(-1));
server.close();
socket.closeSync();
asyncEnd();
});
}
void testClosedError() {
asyncStart();
RawServerSocket.bind(InternetAddress.loopbackIPv4, 0).then((server) {
server.listen((socket) {
socket.close();
});
var socket =
RawSynchronousSocket.connectSync(loopbackIPv4String, server.port);
server.close();
socket.closeSync();
Expect.throws(() => socket.remotePort, (e) => e is SocketException);
Expect.throws(() => socket.remoteAddress, (e) => e is SocketException);
asyncEnd();
});
}
main() async {
asyncStart();
testArguments();
// testInvalidConnect(); Long timeout for bad lookups, so disable for bots.
await testShutdown();
testSimpleConnect();
testServerListenAfterConnect();
await testSimpleReadWrite();
await testPartialRead();
await testPartialWrite();
testInvalidReadWriteOperations();
testClosedError();
asyncEnd();
}