blob: 2f0930073240851ca2b8c76ccb3134becc08f7d4 [file] [log] [blame]
// Copyright (c) 2015, 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.
library test.runner.reporter.json;
import 'dart:async';
import 'dart:convert';
import '../../backend/group.dart';
import '../../backend/live_test.dart';
import '../../backend/metadata.dart';
import '../../frontend/expect.dart';
import '../../utils.dart';
import '../engine.dart';
import '../load_suite.dart';
import '../reporter.dart';
import '../version.dart';
/// A reporter that prints machine-readable JSON-formatted test results.
class JsonReporter implements Reporter {
/// Whether to use verbose stack traces.
final bool _verboseTrace;
/// The engine used to run the tests.
final Engine _engine;
/// A stopwatch that tracks the duration of the full run.
final _stopwatch = new Stopwatch();
/// Whether we've started [_stopwatch].
///
/// We can't just use `_stopwatch.isRunning` because the stopwatch is stopped
/// when the reporter is paused.
var _stopwatchStarted = false;
/// Whether the reporter is paused.
var _paused = false;
/// The set of all subscriptions to various streams.
final _subscriptions = new Set<StreamSubscription>();
/// An expando that associates unique IDs with [LiveTest]s.
final _liveTestIDs = new Map<LiveTest, int>();
/// An expando that associates unique IDs with [Group]s.
final _groupIDs = new Map<Group, int>();
/// The next ID to associate with a [LiveTest].
var _nextID = 0;
/// Watches the tests run by [engine] and prints their results as JSON.
///
/// If [verboseTrace] is `true`, this will print core library frames.
static JsonReporter watch(Engine engine, {bool verboseTrace: false}) {
return new JsonReporter._(engine, verboseTrace: verboseTrace);
}
JsonReporter._(this._engine, {bool verboseTrace: false})
: _verboseTrace = verboseTrace {
_subscriptions.add(_engine.onTestStarted.listen(_onTestStarted));
/// Convert the future to a stream so that the subscription can be paused or
/// canceled.
_subscriptions.add(_engine.success.asStream().listen(_onDone));
_emit("start", {
"protocolVersion": "0.1.0",
"runnerVersion": testVersion
});
}
void pause() {
if (_paused) return;
_paused = true;
_stopwatch.stop();
for (var subscription in _subscriptions) {
subscription.pause();
}
}
void resume() {
if (!_paused) return;
_paused = false;
if (_stopwatchStarted) _stopwatch.start();
for (var subscription in _subscriptions) {
subscription.resume();
}
}
void cancel() {
for (var subscription in _subscriptions) {
subscription.cancel();
}
_subscriptions.clear();
}
/// A callback called when the engine begins running [liveTest].
void _onTestStarted(LiveTest liveTest) {
if (!_stopwatchStarted) {
_stopwatchStarted = true;
_stopwatch.start();
}
// Don't emit groups for load suites. They're always empty and they provide
// unnecessary clutter.
var groupIDs = liveTest.suite is LoadSuite
? []
: _idsForGroups(liveTest.groups);
var id = _nextID++;
_liveTestIDs[liveTest] = id;
_emit("testStart", {
"test": {
"id": id,
"name": liveTest.test.name,
"groupIDs": groupIDs,
"metadata": _serializeMetadata(liveTest.test.metadata)
}
});
/// Convert the future to a stream so that the subscription can be paused or
/// canceled.
_subscriptions.add(liveTest.onComplete.asStream().listen((_) =>
_onComplete(liveTest)));
_subscriptions.add(liveTest.onError.listen((error) =>
_onError(liveTest, error.error, error.stackTrace)));
_subscriptions.add(liveTest.onPrint.listen((line) {
_emit("print", {
"testID": id,
"message": line
});
}));
}
/// Returns a list of the IDs for all the groups in [groups].
///
/// If a group doesn't have an ID yet, this assigns one and emits a new event
/// for that group.
List<int> _idsForGroups(Iterable<Group> groups) {
var parentID;
return groups.map((group) {
if (_groupIDs.containsKey(group)) {
parentID = _groupIDs[group];
return parentID;
}
var id = _nextID++;
_groupIDs[group] = id;
_emit("group", {
"group": {
"id": id,
"parentID": parentID,
"name": group.name,
"metadata": _serializeMetadata(group.metadata)
}
});
parentID = id;
return id;
}).toList();
}
/// Serializes [metadata] into a JSON-protocol-compatible map.
Map _serializeMetadata(Metadata metadata) =>
{"skip": metadata.skip, "skipReason": metadata.skipReason};
/// A callback called when [liveTest] finishes running.
void _onComplete(LiveTest liveTest) {
_emit("testDone", {
"testID": _liveTestIDs[liveTest],
"result": liveTest.state.result.toString(),
"hidden": !_engine.liveTests.contains(liveTest)
});
}
/// A callback called when [liveTest] throws [error].
void _onError(LiveTest liveTest, error, StackTrace stackTrace) {
_emit("error", {
"testID": _liveTestIDs[liveTest],
"error": error.toString(),
"stackTrace": terseChain(stackTrace, verbose: _verboseTrace).toString(),
"isFailure": error is TestFailure
});
}
/// A callback called when the engine is finished running tests.
///
/// [success] will be `true` if all tests passed, `false` if some tests
/// failed, and `null` if the engine was closed prematurely.
void _onDone(bool success) {
cancel();
_stopwatch.stop();
_emit("done", {"success": success});
}
/// Emits an event with the given type and attributes.
void _emit(String type, Map attributes) {
attributes["type"] = type;
attributes["time"] = _stopwatch.elapsed.inMilliseconds;
print(JSON.encode(attributes));
}
}