Update worker mode to use the bazel protos directly instead of json

BUG=
R=paulberry@google.com

Review URL: https://codereview.chromium.org/1868663002 .
This commit is contained in:
Jacob MacDonald 2016-04-06 13:49:32 -07:00
parent 855bb4cdea
commit 92419d092b
10 changed files with 529 additions and 403 deletions

3
DEPS
View file

@ -71,6 +71,7 @@ vars = {
"ply_rev": "@604b32590ffad5cbb82e4afef1d305512d06ae93",
"plugin_tag": "@0.1.0",
"pool_tag": "@1.2.1",
"protobuf_tag": "@0.5.0+1",
"pub_rev": "@c1405b945c6d818c8cfe78334e8d4b11fd913103",
"pub_cache_tag": "@v0.1.0",
"pub_semver_tag": "@1.2.1",
@ -225,6 +226,8 @@ deps = {
(Var("github_mirror") % "plugin") + Var("plugin_tag"),
Var("dart_root") + "/third_party/pkg/pool":
(Var("github_mirror") % "pool") + Var("pool_tag"),
Var("dart_root") + "/third_party/pkg/protobuf":
(Var("github_dartlang") % "dart-protobuf") + Var("protobuf_tag"),
Var("dart_root") + "/third_party/pkg/pub_semver":
(Var("github_mirror") % "pub_semver") + Var("pub_semver_tag"),
Var("dart_root") + "/third_party/pkg/pub":

View file

@ -4,10 +4,11 @@
library analyzer_cli.src.build_mode;
import 'dart:convert';
import 'dart:core' hide Resource;
import 'dart:io' as io;
import 'package:protobuf/protobuf.dart';
import 'package:analyzer/dart/ast/ast.dart' show CompilationUnit;
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/file_system.dart';
@ -30,6 +31,9 @@ import 'package:analyzer_cli/src/driver.dart';
import 'package:analyzer_cli/src/error_formatter.dart';
import 'package:analyzer_cli/src/options.dart';
import 'message_grouper.dart';
import 'worker_protocol.pb.dart';
/**
* Analyzer used when the "--build-mode" option is supplied.
*/
@ -283,29 +287,19 @@ class BuildMode {
}
}
/**
* Interface that every worker related data object has.
*/
abstract class WorkDataObject {
/**
* Translate the data in this class into a JSON map.
*/
Map<String, Object> toJson();
}
/**
* Connection between a worker and input / output.
*/
abstract class WorkerConnection {
/**
* Read a new line. Block until a line is read. Return `null` if EOF.
* Read a new [WorkRequest]. Returns [null] when there are no more requests.
*/
String readLineSync();
WorkRequest readRequest();
/**
* Write the given [json] as a new line to the output.
* Write the given [response] as bytes to the output.
*/
void writeJson(Map<String, Object> json);
void writeResponse(WorkResponse response);
}
/**
@ -322,8 +316,11 @@ class WorkerLoop {
WorkerLoop(this.connection);
factory WorkerLoop.std() {
WorkerConnection connection = new _StdWorkerConnection();
factory WorkerLoop.std({io.Stdin stdinStream, io.Stdout stdoutStream}) {
stdinStream ??= io.stdin;
stdoutStream ??= io.stdout;
WorkerConnection connection =
new StdWorkerConnection(stdinStream, stdoutStream);
return new WorkerLoop(connection);
}
@ -339,7 +336,7 @@ class WorkerLoop {
*/
bool performSingle() {
try {
WorkRequest request = _readRequest();
WorkRequest request = connection.readRequest();
if (request == null) {
return true;
}
@ -351,11 +348,15 @@ class WorkerLoop {
// Analyze and respond.
analyze(options);
String msg = _getErrorOutputBuffersText();
_writeResponse(new WorkResponse(EXIT_CODE_OK, msg));
connection.writeResponse(new WorkResponse()
..exitCode = EXIT_CODE_OK
..output = msg);
} catch (e, st) {
String msg = _getErrorOutputBuffersText();
msg += '$e \n $st';
_writeResponse(new WorkResponse(EXIT_CODE_ERROR, msg));
connection.writeResponse(new WorkResponse()
..exitCode = EXIT_CODE_ERROR
..output = msg);
}
return false;
}
@ -389,182 +390,34 @@ class WorkerLoop {
}
return msg;
}
/**
* Read a new [WorkRequest]. Return `null` if EOF.
* Throw [ArgumentError] if cannot be parsed.
*/
WorkRequest _readRequest() {
String line = connection.readLineSync();
if (line == null) {
return null;
}
Object json = JSON.decode(line);
if (json is Map) {
return new WorkRequest.fromJson(json);
} else {
throw new ArgumentError('The request line is not a JSON object: $line');
}
}
void _writeResponse(WorkResponse response) {
Map<String, Object> json = response.toJson();
connection.writeJson(json);
}
}
/**
* Input file.
*/
class WorkInput implements WorkDataObject {
final String path;
final List<int> digest;
WorkInput(this.path, this.digest);
factory WorkInput.fromJson(Map<String, Object> json) {
// Parse path.
Object path2 = json['path'];
if (path2 == null) {
throw new ArgumentError('The field "path" is missing.');
}
if (path2 is! String) {
throw new ArgumentError('The field "path" must be a string.');
}
// Parse digest.
List<int> digest = const <int>[];
{
Object digestJson = json['digest'];
if (digestJson != null) {
if (digestJson is List && digestJson.every((e) => e is int)) {
digest = digestJson;
} else {
throw new ArgumentError(
'The field "digest" should be a list of int.');
}
}
}
// OK
return new WorkInput(path2, digest);
}
@override
Map<String, Object> toJson() {
Map<String, Object> json = <String, Object>{};
if (path != null) {
json['path'] = path;
}
if (digest != null) {
json['digest'] = digest;
}
return json;
}
}
/**
* Single work unit that Bazel sends to the worker.
*/
class WorkRequest implements WorkDataObject {
/**
* Command line arguments for this request.
*/
final List<String> arguments;
/**
* Input files that the worker is allowed to read during execution of this
* request.
*/
final List<WorkInput> inputs;
WorkRequest(this.arguments, this.inputs);
factory WorkRequest.fromJson(Map<String, Object> json) {
// Parse arguments.
List<String> arguments = const <String>[];
{
Object argumentsJson = json['arguments'];
if (argumentsJson != null) {
if (argumentsJson is List && argumentsJson.every((e) => e is String)) {
arguments = argumentsJson;
} else {
throw new ArgumentError(
'The field "arguments" should be a list of strings.');
}
}
}
// Parse inputs.
List<WorkInput> inputs = const <WorkInput>[];
{
Object inputsJson = json['inputs'];
if (inputsJson != null) {
if (inputsJson is List &&
inputsJson.every((e) {
return e is Map && e.keys.every((key) => key is String);
})) {
inputs = inputsJson
.map((Map input) => new WorkInput.fromJson(input))
.toList();
} else {
throw new ArgumentError(
'The field "inputs" should be a list of objects.');
}
}
}
// No inputs.
if (arguments.isEmpty && inputs.isEmpty) {
throw new ArgumentError('Both "arguments" and "inputs" cannot be empty.');
}
// OK
return new WorkRequest(arguments, inputs);
}
@override
Map<String, Object> toJson() {
Map<String, Object> json = <String, Object>{};
if (arguments != null) {
json['arguments'] = arguments;
}
if (inputs != null) {
json['inputs'] = inputs.map((input) => input.toJson()).toList();
}
return json;
}
}
/**
* Result that the worker sends back to Bazel when it finished its work on a
* [WorkRequest] message.
*/
class WorkResponse implements WorkDataObject {
final int exitCode;
final String output;
WorkResponse(this.exitCode, this.output);
@override
Map<String, Object> toJson() {
Map<String, Object> json = <String, Object>{};
if (exitCode != null) {
json['exit_code'] = exitCode;
}
if (output != null) {
json['output'] = output;
}
return json;
}
}
/**
* Default implementation of [WorkerConnection] that works with stdio.
*/
class _StdWorkerConnection implements WorkerConnection {
class StdWorkerConnection implements WorkerConnection {
final MessageGrouper _messageGrouper;
final io.Stdout _stdoutStream;
StdWorkerConnection(io.Stdin stdinStream, this._stdoutStream)
: _messageGrouper = new MessageGrouper(stdinStream);
@override
String readLineSync() {
return io.stdin.readLineSync();
WorkRequest readRequest() {
var buffer = _messageGrouper.next;
if (buffer == null) return null;
return new WorkRequest.fromBuffer(buffer);
}
@override
void writeJson(Map<String, Object> json) {
io.stdout.writeln(JSON.encode(json));
void writeResponse(WorkResponse response) {
var responseBuffer = response.writeToBuffer();
var writer = new CodedBufferWriter();
writer.writeInt32NoTag(responseBuffer.length);
writer.writeRawBytes(responseBuffer);
_stdoutStream.add(writer.toBuffer());
}
}

View file

@ -0,0 +1,112 @@
// Copyright (c) 2016, 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.
import 'dart:io';
import 'dart:typed_data';
/// Groups stdin input into messages by interpreting it as
/// base-128 encoded lengths interleaved with raw data.
///
/// The base-128 encoding is in little-endian order, with the high bit set on
/// all bytes but the last. This was chosen since it's the same as the
/// base-128 encoding used by protobufs, so it allows a modest amount of code
/// reuse at the other end of the protocol.
///
/// Possible future improvements to consider (should a debugging need arise):
/// - Put a magic number at the beginning of the stream.
/// - Use a guard byte between messages to sanity check that the encoder and
/// decoder agree on the encoding of lengths.
class MessageGrouper {
final _state = new _MessageGrouperState();
final Stdin _stdin;
MessageGrouper(this._stdin);
/// Blocks until the next full message is received, and then returns it.
///
/// Returns null at end of file.
List<int> get next {
var message;
while (message == null) {
var nextByte = _stdin.readByteSync();
if (nextByte == -1) return null;
message = _state.handleInput(nextByte);
}
return message;
}
}
/// State held by the [MessageGrouper] while waiting for additional data to
/// arrive.
class _MessageGrouperState {
/// `true` means we are waiting to receive bytes of base-128 encoded length.
/// Some bytes of length may have been received already.
///
/// `false` means we are waiting to receive more bytes of message data. Some
/// bytes of message data may have been received already.
bool waitingForLength = true;
/// If [waitingForLength] is `true`, the decoded value of the length bytes
/// received so far (if any). If [waitingForLength] is `false`, the decoded
/// length that was most recently received.
int length = 0;
/// If [waitingForLength] is `true`, the amount by which the next received
/// length byte must be left-shifted; otherwise undefined.
int lengthShift = 0;
/// If [waitingForLength] is `false`, a [Uint8List] which is ready to receive
/// message data. Otherwise null.
Uint8List message;
/// If [waitingForLength] is `false`, the number of message bytes that have
/// been received so far. Otherwise zero.
int numMessageBytesReceived;
_MessageGrouperState() {
reset();
}
/// Handle one byte at a time.
///
/// Returns a [List<int>] of message bytes if [byte] was the last byte in a
/// message, otherwise returns [null].
List<int> handleInput(int byte) {
if (waitingForLength) {
length |= (byte & 0x7f) << lengthShift;
if ((byte & 0x80) == 0) {
waitingForLength = false;
message = new Uint8List(length);
if (length == 0) {
// There is no message data to wait for, so just go ahead and deliver the
// empty message.
var messageToReturn = message;
reset();
return messageToReturn;
}
} else {
lengthShift += 7;
}
} else {
message[numMessageBytesReceived] = byte;
numMessageBytesReceived++;
if (numMessageBytesReceived == length) {
var messageToReturn = message;
reset();
return messageToReturn;
}
}
return null;
}
/// Reset the state so that we are ready to receive the next base-128 encoded
/// length.
void reset() {
waitingForLength = true;
length = 0;
lengthShift = 0;
message = null;
numMessageBytesReceived = 0;
}
}

View file

@ -263,6 +263,12 @@ class CommandLineOptions {
}
static CommandLineOptions _parse(List<String> args) {
// Check if the args are in a file (bazel worker mode).
if (args.last.startsWith('@')) {
var argsFile = new File(args.last.substring(1));
args = argsFile.readAsLinesSync();
}
args = args.expand((String arg) => arg.split('=')).toList();
var parser = new CommandLineParser()
..addFlag('batch',

View file

@ -0,0 +1,133 @@
///
// Generated code. Do not modify.
///
library blaze.worker_worker_protocol;
import 'package:protobuf/protobuf.dart';
class Input extends GeneratedMessage {
static final BuilderInfo _i = new BuilderInfo('Input')
..a(1, 'path', PbFieldType.OS)
..a(2, 'digest', PbFieldType.OY)
..hasRequiredFields = false
;
Input() : super();
Input.fromBuffer(List<int> i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromBuffer(i, r);
Input.fromJson(String i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromJson(i, r);
Input clone() => new Input()..mergeFromMessage(this);
BuilderInfo get info_ => _i;
static Input create() => new Input();
static PbList<Input> createRepeated() => new PbList<Input>();
static Input getDefault() {
if (_defaultInstance == null) _defaultInstance = new _ReadonlyInput();
return _defaultInstance;
}
static Input _defaultInstance;
static void $checkItem(Input v) {
if (v is !Input) checkItemFailed(v, 'Input');
}
String get path => $_get(0, 1, '');
void set path(String v) { $_setString(0, 1, v); }
bool hasPath() => $_has(0, 1);
void clearPath() => clearField(1);
List<int> get digest => $_get(1, 2, null);
void set digest(List<int> v) { $_setBytes(1, 2, v); }
bool hasDigest() => $_has(1, 2);
void clearDigest() => clearField(2);
}
class _ReadonlyInput extends Input with ReadonlyMessageMixin {}
class WorkRequest extends GeneratedMessage {
static final BuilderInfo _i = new BuilderInfo('WorkRequest')
..p(1, 'arguments', PbFieldType.PS)
..pp(2, 'inputs', PbFieldType.PM, Input.$checkItem, Input.create)
..hasRequiredFields = false
;
WorkRequest() : super();
WorkRequest.fromBuffer(List<int> i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromBuffer(i, r);
WorkRequest.fromJson(String i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromJson(i, r);
WorkRequest clone() => new WorkRequest()..mergeFromMessage(this);
BuilderInfo get info_ => _i;
static WorkRequest create() => new WorkRequest();
static PbList<WorkRequest> createRepeated() => new PbList<WorkRequest>();
static WorkRequest getDefault() {
if (_defaultInstance == null) _defaultInstance = new _ReadonlyWorkRequest();
return _defaultInstance;
}
static WorkRequest _defaultInstance;
static void $checkItem(WorkRequest v) {
if (v is !WorkRequest) checkItemFailed(v, 'WorkRequest');
}
List<String> get arguments => $_get(0, 1, null);
List<Input> get inputs => $_get(1, 2, null);
}
class _ReadonlyWorkRequest extends WorkRequest with ReadonlyMessageMixin {}
class WorkResponse extends GeneratedMessage {
static final BuilderInfo _i = new BuilderInfo('WorkResponse')
..a(1, 'exitCode', PbFieldType.O3)
..a(2, 'output', PbFieldType.OS)
..hasRequiredFields = false
;
WorkResponse() : super();
WorkResponse.fromBuffer(List<int> i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromBuffer(i, r);
WorkResponse.fromJson(String i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromJson(i, r);
WorkResponse clone() => new WorkResponse()..mergeFromMessage(this);
BuilderInfo get info_ => _i;
static WorkResponse create() => new WorkResponse();
static PbList<WorkResponse> createRepeated() => new PbList<WorkResponse>();
static WorkResponse getDefault() {
if (_defaultInstance == null) _defaultInstance = new _ReadonlyWorkResponse();
return _defaultInstance;
}
static WorkResponse _defaultInstance;
static void $checkItem(WorkResponse v) {
if (v is !WorkResponse) checkItemFailed(v, 'WorkResponse');
}
int get exitCode => $_get(0, 1, 0);
void set exitCode(int v) { $_setUnsignedInt32(0, 1, v); }
bool hasExitCode() => $_has(0, 1);
void clearExitCode() => clearField(1);
String get output => $_get(1, 2, '');
void set output(String v) { $_setString(1, 2, v); }
bool hasOutput() => $_has(1, 2);
void clearOutput() => clearField(2);
}
class _ReadonlyWorkResponse extends WorkResponse with ReadonlyMessageMixin {}
const Input$json = const {
'1': 'Input',
'2': const [
const {'1': 'path', '3': 1, '4': 1, '5': 9},
const {'1': 'digest', '3': 2, '4': 1, '5': 12},
],
};
const WorkRequest$json = const {
'1': 'WorkRequest',
'2': const [
const {'1': 'arguments', '3': 1, '4': 3, '5': 9},
const {'1': 'inputs', '3': 2, '4': 3, '5': 11, '6': '.blaze.worker.Input'},
],
};
const WorkResponse$json = const {
'1': 'WorkResponse',
'2': const [
const {'1': 'exit_code', '3': 1, '4': 1, '5': 5},
const {'1': 'output', '3': 2, '4': 1, '5': 9},
],
};

View file

@ -12,6 +12,7 @@ dependencies:
linter: ^0.1.10
package_config: ^0.1.1
plugin: ^0.1.0
protobuf: ^0.5.0
yaml: ^2.1.2
dev_dependencies:
test_reflective_loader: '>=0.0.3 <0.1.0'

View file

@ -6,6 +6,7 @@ import 'boot_loader_test.dart' as boot_loader;
import 'build_mode_test.dart' as build_mode_test;
import 'driver_test.dart' as driver;
import 'error_test.dart' as error;
import 'message_grouper_test.dart' as message_grouper;
import 'options_test.dart' as options;
import 'package_prefix_test.dart' as package_prefix;
import 'perf_report_test.dart' as perf;
@ -24,6 +25,7 @@ main() {
//sdk_ext.main();
//strong_mode.main();
error.main();
message_grouper.main();
options.main();
perf.main();
plugin_manager.main();

View file

@ -4,38 +4,59 @@
library analyzer_cli.test.built_mode;
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:analyzer_cli/src/build_mode.dart';
import 'package:analyzer_cli/src/driver.dart';
import 'package:analyzer_cli/src/options.dart';
import 'package:analyzer_cli/src/worker_protocol.pb.dart';
import 'package:protobuf/protobuf.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'package:typed_mock/typed_mock.dart';
import 'package:unittest/unittest.dart';
import 'utils.dart';
main() {
defineReflectiveTests(WorkerLoopTest);
defineReflectiveTests(WorkInputTest);
defineReflectiveTests(WorkRequestTest);
}
typedef void _TestWorkerLoopAnalyze(CommandLineOptions options);
@reflectiveTest
class WorkerLoopTest {
final _TestWorkerConnection connection = new _TestWorkerConnection();
final TestStdinStream stdinStream = new TestStdinStream();
final TestStdoutStream stdoutStream = new TestStdoutStream();
_TestWorkerConnection connection;
WorkerLoopTest() {
connection = new _TestWorkerConnection(this.stdinStream, this.stdoutStream);
}
void setUp() {}
List<int> _serializeProto(GeneratedMessage message) {
var buffer = message.writeToBuffer();
var writer = new CodedBufferWriter();
writer.writeInt32NoTag(buffer.length);
writer.writeRawBytes(buffer);
return writer.toBuffer();
}
test_run() {
_setInputLine(JSON.encode({
'arguments': [
'--build-summary-input=/tmp/1.sum',
'--build-summary-input=/tmp/2.sum',
'package:foo/foo.dart|/inputs/foo/lib/foo.dart',
'package:foo/bar.dart|/inputs/foo/lib/bar.dart'
],
}));
var request = new WorkRequest();
request.arguments.addAll([
'--build-summary-input=/tmp/1.sum',
'--build-summary-input=/tmp/2.sum',
'package:foo/foo.dart|/inputs/foo/lib/foo.dart',
'package:foo/bar.dart|/inputs/foo/lib/bar.dart',
]);
stdinStream.addInputBytes(_serializeProto(request));
new _TestWorkerLoop(connection, (CommandLineOptions options) {
expect(options.buildSummaryInputs,
unorderedEquals(['/tmp/1.sum', '/tmp/2.sum']));
@ -51,235 +72,71 @@ class WorkerLoopTest {
errorSink.writeln('errorSink b');
}).run();
expect(connection.outputList, hasLength(1));
expect(connection.outputList[0], {
'exit_code': WorkerLoop.EXIT_CODE_OK,
'output': allOf(contains('errorSink a'), contains('errorSink a'),
contains('outSink a'), contains('outSink b'))
});
var response = connection.outputList[0];
expect(response.exitCode, WorkerLoop.EXIT_CODE_OK);
expect(
response.output,
allOf(contains('errorSink a'), contains('errorSink a'),
contains('outSink a'), contains('outSink b')));
// Check that a serialized version was written to std out.
expect(stdoutStream.writes, hasLength(1));
expect(stdoutStream.writes[0], _serializeProto(response));
}
test_run_invalidOptions() {
_setInputLine(JSON.encode({
'arguments': ['--unknown-option', '/foo.dart', '/bar.dart',],
}));
var request = new WorkRequest();
request.arguments.addAll(['--unknown-option', '/foo.dart', '/bar.dart']);
stdinStream.addInputBytes(_serializeProto(request));
new _TestWorkerLoop(connection).run();
expect(connection.outputList, hasLength(1));
expect(connection.outputList[0],
{'exit_code': WorkerLoop.EXIT_CODE_ERROR, 'output': anything});
var response = connection.outputList[0];
expect(response.exitCode, WorkerLoop.EXIT_CODE_ERROR);
expect(response.output, anything);
}
test_run_invalidRequest_noArgumentsInputs() {
_setInputLine('{}');
stdinStream.addInputBytes(_serializeProto(new WorkRequest()));
new _TestWorkerLoop(connection).run();
expect(connection.outputList, hasLength(1));
expect(connection.outputList[0],
{'exit_code': WorkerLoop.EXIT_CODE_ERROR, 'output': anything});
var response = connection.outputList[0];
expect(response.exitCode, WorkerLoop.EXIT_CODE_ERROR);
expect(response.output, anything);
}
test_run_invalidRequest_notJson() {
_setInputLine('not a JSON string');
test_run_invalidRequest_randomBytes() {
stdinStream.addInputBytes([1, 2, 3]);
new _TestWorkerLoop(connection).run();
expect(connection.outputList, hasLength(1));
expect(connection.outputList[0],
{'exit_code': WorkerLoop.EXIT_CODE_ERROR, 'output': anything});
var response = connection.outputList[0];
expect(response.exitCode, WorkerLoop.EXIT_CODE_ERROR);
expect(response.output, anything);
}
test_run_stopAtEOF() {
when(connection.readLineSync()).thenReturnList([null]);
stdinStream.addInputBytes([-1]);
new _TestWorkerLoop(connection).run();
}
void _setInputLine(String line) {
when(connection.readLineSync()).thenReturnList([line, null]);
}
}
@reflectiveTest
class WorkInputTest {
test_fromJson() {
WorkInput input = new WorkInput.fromJson({
'path': '/my/path',
'digest': [1, 2, 3, 4, 5]
});
expect(input.path, '/my/path');
expect(input.digest, <int>[1, 2, 3, 4, 5]);
}
test_fromJson_digest_isMissing() {
WorkInput input = new WorkInput.fromJson({'path': '/my/path',});
expect(input.path, '/my/path');
expect(input.digest, <int>[]);
}
test_fromJson_digest_isNotList() {
expect(() {
new WorkInput.fromJson({'path': '/my/path', 'digest': 0});
}, throwsArgumentError);
}
test_fromJson_digest_isNotListOfInt() {
expect(() {
new WorkInput.fromJson({
'path': '/my/path',
'digest': ['a', 'b', 'c']
});
}, throwsArgumentError);
}
test_fromJson_path_isMissing() {
expect(() {
new WorkInput.fromJson({
'digest': [1, 2, 3, 4, 5]
});
}, throwsArgumentError);
}
test_fromJson_path_isNotString() {
expect(() {
new WorkInput.fromJson({
'path': 0,
'digest': [1, 2, 3, 4, 5]
});
}, throwsArgumentError);
}
test_toJson() {
WorkInput input = new WorkInput('/my/path', <int>[1, 2, 3, 4, 5]);
Map<String, Object> json = input.toJson();
expect(json, {
'path': '/my/path',
'digest': [1, 2, 3, 4, 5]
});
}
test_toJson_withoutDigest() {
WorkInput input = new WorkInput('/my/path', null);
Map<String, Object> json = input.toJson();
expect(json, {'path': '/my/path'});
}
}
@reflectiveTest
class WorkRequestTest {
test_fromJson() {
WorkRequest request = new WorkRequest.fromJson({
'arguments': ['--arg1', '--arg2', '--arg3'],
'inputs': [
{
'path': '/my/path1',
'digest': [11, 12, 13]
},
{
'path': '/my/path2',
'digest': [21, 22, 23]
}
]
});
expect(request.arguments, ['--arg1', '--arg2', '--arg3']);
expect(request.inputs, hasLength(2));
expect(request.inputs[0].path, '/my/path1');
expect(request.inputs[0].digest, <int>[11, 12, 13]);
expect(request.inputs[1].path, '/my/path2');
expect(request.inputs[1].digest, <int>[21, 22, 23]);
}
test_fromJson_arguments_isMissing() {
WorkRequest request = new WorkRequest.fromJson({
'inputs': [
{
'path': '/my/path1',
'digest': [11, 12, 13]
},
]
});
expect(request.arguments, isEmpty);
expect(request.inputs, hasLength(1));
expect(request.inputs[0].path, '/my/path1');
expect(request.inputs[0].digest, <int>[11, 12, 13]);
}
test_fromJson_arguments_isNotList() {
expect(() {
new WorkRequest.fromJson({'arguments': 0, 'inputs': []});
}, throwsArgumentError);
}
test_fromJson_arguments_isNotListOfString() {
expect(() {
new WorkRequest.fromJson({
'arguments': [0, 1, 2],
'inputs': []
});
}, throwsArgumentError);
}
test_fromJson_inputs_isMissing() {
WorkRequest request = new WorkRequest.fromJson({
'arguments': ['--arg1', '--arg2', '--arg3'],
});
expect(request.arguments, ['--arg1', '--arg2', '--arg3']);
expect(request.inputs, hasLength(0));
}
test_fromJson_inputs_isNotList() {
expect(() {
new WorkRequest.fromJson({
'arguments': ['--arg1', '--arg2', '--arg3'],
'inputs': 0
});
}, throwsArgumentError);
}
test_fromJson_inputs_isNotListOfObject() {
expect(() {
new WorkRequest.fromJson({
'arguments': ['--arg1', '--arg2', '--arg3'],
'inputs': [0, 1, 2]
});
}, throwsArgumentError);
}
test_fromJson_noArgumentsInputs() {
expect(() {
new WorkRequest.fromJson({});
}, throwsArgumentError);
}
test_toJson() {
WorkRequest request = new WorkRequest(<String>[
'--arg1',
'--arg2',
'--arg3'
], <WorkInput>[
new WorkInput('/my/path1', <int>[11, 12, 13]),
new WorkInput('/my/path2', <int>[21, 22, 23])
]);
Map<String, Object> json = request.toJson();
expect(json, {
'arguments': ['--arg1', '--arg2', '--arg3'],
'inputs': [
{
'path': '/my/path1',
'digest': [11, 12, 13]
},
{
'path': '/my/path2',
'digest': [21, 22, 23]
}
]
});
}
}
/**
* [WorkerConnection] mock.
* A [StdWorkerConnection] which records its responses.
*/
class _TestWorkerConnection extends TypedMock implements WorkerConnection {
final outputList = <Map<String, Object>>[];
class _TestWorkerConnection extends StdWorkerConnection {
final outputList = <WorkResponse>[];
_TestWorkerConnection(Stdin stdinStream, Stdout stdoutStream)
: super(stdinStream, stdoutStream);
@override
void writeJson(Map<String, Object> json) {
outputList.add(json);
void writeResponse(WorkResponse response) {
super.writeResponse(response);
outputList.add(response);
}
}

View file

@ -0,0 +1,123 @@
// Copyright (c) 2016, 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.
import 'package:analyzer_cli/src/message_grouper.dart';
import 'package:unittest/unittest.dart';
import 'utils.dart';
main() {
MessageGrouper messageGrouper;
TestStdinStream stdinStream;
setUp(() {
stdinStream = new TestStdinStream();
messageGrouper = new MessageGrouper(stdinStream);
});
group('message_grouper', () {
/// Check that if the message grouper produces the [expectedOutput] in
/// response to the corresponding [input].
void check(List<int> input, List<List<int>> expectedOutput) {
stdinStream.addInputBytes(input);
for (var chunk in expectedOutput) {
expect(messageGrouper.next, equals(chunk));
}
}
/// Make a simple message having the given [length]
List<int> makeMessage(int length) {
var result = <int>[];
for (int i = 0; i < length; i++) {
result.add(i & 0xff);
}
return result;
}
test('Empty message', () {
check([0], [[]]);
});
test('Short message', () {
check([
5,
10,
20,
30,
40,
50
], [
[10, 20, 30, 40, 50]
]);
});
test('Message with 2-byte length', () {
var len = 0x155;
var msg = makeMessage(len);
var encodedLen = [0xd5, 0x02];
check([]..addAll(encodedLen)..addAll(msg), [msg]);
});
test('Message with 3-byte length', () {
var len = 0x4103;
var msg = makeMessage(len);
var encodedLen = [0x83, 0x82, 0x01];
check([]..addAll(encodedLen)..addAll(msg), [msg]);
});
test('Multiple messages', () {
check([
2,
10,
20,
2,
30,
40
], [
[10, 20],
[30, 40]
]);
});
test('Empty message at start', () {
check([
0,
2,
10,
20
], [
[],
[10, 20]
]);
});
test('Empty message at end', () {
check([
2,
10,
20,
0
], [
[10, 20],
[]
]);
});
test('Empty message in the middle', () {
check([
2,
10,
20,
0,
2,
30,
40
], [
[10, 20],
[],
[30, 40]
]);
});
});
}

View file

@ -4,6 +4,7 @@
library analyzer_cli.test.utils;
import 'dart:collection';
import 'dart:io';
import 'dart:mirrors';
@ -11,6 +12,7 @@ import 'package:analyzer/analyzer.dart';
import 'package:analyzer/src/generated/java_io.dart';
import 'package:path/path.dart' as pathos;
import 'package:path/path.dart' as path;
import 'package:typed_mock/typed_mock.dart';
import 'package:unittest/unittest.dart';
/// Gets the test directory in a way that works with
@ -63,3 +65,37 @@ dynamic withTempDir(fn(String path)) {
}
class _TestUtils {}
/**
* A [Stdin] mock.
*/
class TestStdinStream extends TypedMock implements Stdin {
final pendingBytes = new Queue<int>();
// Adds all the input bytes to this stream.
void addInputBytes(List<int> bytes) {
pendingBytes.addAll(bytes);
}
@override
int readByteSync() {
if (pendingBytes.isEmpty) {
return -1;
} else {
return pendingBytes.removeFirst();
}
}
}
/**
* A [Stdout] mock.
*/
class TestStdoutStream extends TypedMock implements Stdout {
final writes = <List<int>>[];
@override
void add(List<int> bytes) {
super.add(bytes);
writes.add(bytes);
}
}