Configurable package folding (#650)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a72e51..d13790d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.12.23
+
+* Add a `fold_stack_frames` field for `dart_test.yaml`. This will
+ allow users to customize which packages' frames are folded.
+
## 0.12.22+2
* Properly allocate ports when debugging Chrome and Dartium in an IPv6-only
diff --git a/doc/configuration.md b/doc/configuration.md
index 7f29442..24cf7c0 100644
--- a/doc/configuration.md
+++ b/doc/configuration.md
@@ -45,6 +45,7 @@
* [`run_skipped`](#run_skipped)
* [`pub_serve`](#pub_serve)
* [`reporter`](#reporter)
+ * [`fold_stack_frames`](#fold_stack_frames)
* [Configuring Tags](#configuring-tags)
* [`tags`](#tags)
* [`add_tags`](#add_tags)
@@ -394,6 +395,35 @@
This field is not supported in the
[global configuration file](#global-configuration).
+### `fold_stack_frames`
+
+This field controls which packages' stack frames will be folded away
+when displaying stack traces. Packages contained in the `exclude`
+option will be folded. If `only` is provided, all packages not
+contained in this list will be folded. By default,
+frames from the `test` package and the `stream_channel`
+package are folded.
+
+```yaml
+fold_stack_frames:
+ except:
+ - test
+ - stream_channel
+```
+
+Sample stack trace, note the absence of `package:test`
+and `package:stream_channel`:
+```
+test/sample_test.dart 7:5 main.<fn>
+===== asynchronous gap ===========================
+dart:async _Completer.completeError
+test/sample_test.dart 8:3 main.<fn>
+===== asynchronous gap ===========================
+dart:async _asyncThenWrapperHelper
+test/sample_test.dart 5:27 main.<fn>
+```
+
+
## Configuring Tags
### `tags`
diff --git a/lib/src/frontend/stream_matcher.dart b/lib/src/frontend/stream_matcher.dart
index 6c90674..9e166ca 100644
--- a/lib/src/frontend/stream_matcher.dart
+++ b/lib/src/frontend/stream_matcher.dart
@@ -8,6 +8,8 @@
import 'package:matcher/matcher.dart';
import '../utils.dart';
+import '../backend/invoker.dart';
+import 'test_chain.dart';
import 'async_matcher.dart';
/// The type for [_StreamMatcher._matchQueue].
@@ -164,7 +166,8 @@
return addBullet(event.asValue.value.toString());
} else {
var error = event.asError;
- var text = "${error.error}\n${testChain(error.stackTrace)}";
+ var chain = testChain(error.stackTrace);
+ var text = "${error.error}\n$chain";
return prefixLines(text, " ", first: "! ");
}
}).join("\n");
diff --git a/lib/src/frontend/test_chain.dart b/lib/src/frontend/test_chain.dart
new file mode 100644
index 0000000..41bd9e7
--- /dev/null
+++ b/lib/src/frontend/test_chain.dart
@@ -0,0 +1,52 @@
+// 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.
+
+import 'package:stack_trace/stack_trace.dart';
+
+import '../backend/invoker.dart';
+import '../util/stack_trace_mapper.dart';
+
+/// Converts [trace] into a Dart stack trace
+StackTraceMapper _mapper;
+
+/// The list of packages to fold when producing [Chain]s.
+Set<String> _exceptPackages = new Set.from(['test', 'stream_channel']);
+
+/// If non-empty, all packages not in this list will be folded when producing
+/// [Chain]s.
+Set<String> _onlyPackages = new Set();
+
+/// Configure the resources used for test chaining.
+///
+/// [mapper] is used to convert traces into Dart stack traces.
+/// [exceptPackages] is the list of packages to fold when producing a [Chain].
+/// [onlyPackages] is the list of packages to keep in a [Chain]. If non-empty,
+/// all packages not in this will be folded.
+void configureTestChaining(
+ {StackTraceMapper mapper,
+ Set<String> exceptPackages,
+ Set<String> onlyPackages}) {
+ if (mapper != null) _mapper = mapper;
+ if (exceptPackages != null) _exceptPackages = exceptPackages;
+ if (onlyPackages != null) _onlyPackages = onlyPackages;
+}
+
+/// Returns [stackTrace] converted to a [Chain] with all irrelevant frames
+/// folded together.
+///
+/// If [verbose] is `true`, returns the chain for [stackTrace] unmodified.
+Chain terseChain(StackTrace stackTrace, {bool verbose: false}) {
+ var testTrace = _mapper?.mapStackTrace(stackTrace) ?? stackTrace;
+ if (verbose) return new Chain.forTrace(testTrace);
+ return new Chain.forTrace(testTrace).foldFrames((frame) {
+ if (_onlyPackages.isNotEmpty) {
+ return !_onlyPackages.contains(frame.package);
+ }
+ return _exceptPackages.contains(frame.package);
+ }, terse: true);
+}
+
+/// Converts [stackTrace] to a [Chain] following the test's configuration.
+Chain testChain(StackTrace stackTrace) => terseChain(stackTrace,
+ verbose: Invoker.current?.liveTest?.test?.metadata?.verboseTrace ?? true);
diff --git a/lib/src/frontend/throws_matcher.dart b/lib/src/frontend/throws_matcher.dart
index 8518c0f..75c5d23 100644
--- a/lib/src/frontend/throws_matcher.dart
+++ b/lib/src/frontend/throws_matcher.dart
@@ -8,6 +8,8 @@
import '../utils.dart';
import 'async_matcher.dart';
+import '../frontend/test_chain.dart';
+import '../backend/invoker.dart';
/// This function is deprecated.
///
diff --git a/lib/src/runner/browser/browser_manager.dart b/lib/src/runner/browser/browser_manager.dart
index 7f25b38..841b0b3 100644
--- a/lib/src/runner/browser/browser_manager.dart
+++ b/lib/src/runner/browser/browser_manager.dart
@@ -238,7 +238,7 @@
try {
controller = await deserializeSuite(
path, _platform, suiteConfig, await _environment, suiteChannel,
- mapTrace: mapper?.mapStackTrace);
+ mapper: mapper);
_controllers.add(controller);
return controller.suite;
} catch (_) {
diff --git a/lib/src/runner/configuration.dart b/lib/src/runner/configuration.dart
index fc43ed0..06cc76c 100644
--- a/lib/src/runner/configuration.dart
+++ b/lib/src/runner/configuration.dart
@@ -97,6 +97,15 @@
/// See [shardIndex] for details.
final int totalShards;
+ /// The list of packages to fold when producing [StackTrace]s.
+ Set<String> get foldTraceExcept => _foldTraceExcept ?? new Set();
+ final Set<String> _foldTraceExcept;
+
+ /// If non-empty, all packages not in this list will be folded when producing
+ /// [StackTrace]s.
+ Set<String> get foldTraceOnly => _foldTraceOnly ?? new Set();
+ final Set<String> _foldTraceOnly;
+
/// The paths from which to load tests.
List<String> get paths => _paths ?? ["test"];
final List<String> _paths;
@@ -198,6 +207,8 @@
int shardIndex,
int totalShards,
Iterable<String> paths,
+ Iterable<String> foldTraceExcept,
+ Iterable<String> foldTraceOnly,
Glob filename,
Iterable<String> chosenPresets,
Map<String, Configuration> presets,
@@ -238,6 +249,8 @@
shardIndex: shardIndex,
totalShards: totalShards,
paths: paths,
+ foldTraceExcept: foldTraceExcept,
+ foldTraceOnly: foldTraceOnly,
filename: filename,
chosenPresets: chosenPresetSet,
presets: _withChosenPresets(presets, chosenPresetSet),
@@ -290,6 +303,8 @@
this.shardIndex,
this.totalShards,
Iterable<String> paths,
+ Iterable<String> foldTraceExcept,
+ Iterable<String> foldTraceOnly,
Glob filename,
Iterable<String> chosenPresets,
Map<String, Configuration> presets,
@@ -307,6 +322,8 @@
: Uri.parse("http://localhost:$pubServePort"),
_concurrency = concurrency,
_paths = _list(paths),
+ _foldTraceExcept = _set(foldTraceExcept),
+ _foldTraceOnly = _set(foldTraceOnly),
_filename = filename,
chosenPresets =
new UnmodifiableSetView(chosenPresets?.toSet() ?? new Set()),
@@ -347,6 +364,14 @@
return list;
}
+ /// Returns a set from [input].
+ static Set<T> _set<T>(Iterable<T> input) {
+ if (input == null) return null;
+ var set = new Set<T>.from(input);
+ if (set.isEmpty) return null;
+ return set;
+ }
+
/// Returns an unmodifiable copy of [input] or an empty unmodifiable map.
static Map/*<K, V>*/ _map/*<K, V>*/(Map/*<K, V>*/ input) {
if (input == null || input.isEmpty) return const {};
@@ -369,6 +394,22 @@
if (this == Configuration.empty) return other;
if (other == Configuration.empty) return this;
+ var foldTraceOnly = other._foldTraceOnly ?? _foldTraceOnly;
+ var foldTraceExcept = other._foldTraceExcept ?? _foldTraceExcept;
+ if (_foldTraceOnly != null) {
+ if (other._foldTraceExcept != null) {
+ foldTraceOnly = _foldTraceOnly.difference(other._foldTraceExcept);
+ } else if (other._foldTraceOnly != null) {
+ foldTraceOnly = other._foldTraceOnly.intersection(_foldTraceOnly);
+ }
+ } else if (_foldTraceExcept != null) {
+ if (other._foldTraceOnly != null) {
+ foldTraceOnly = other._foldTraceOnly.difference(_foldTraceExcept);
+ } else if (other._foldTraceExcept != null) {
+ foldTraceExcept = other._foldTraceExcept.union(_foldTraceExcept);
+ }
+ }
+
var result = new Configuration._(
help: other._help ?? _help,
version: other._version ?? _version,
@@ -382,6 +423,8 @@
shardIndex: other.shardIndex ?? shardIndex,
totalShards: other.totalShards ?? totalShards,
paths: other._paths ?? _paths,
+ foldTraceExcept: foldTraceExcept,
+ foldTraceOnly: foldTraceOnly,
filename: other._filename ?? _filename,
chosenPresets: chosenPresets.union(other.chosenPresets),
presets: _mergeConfigMaps(presets, other.presets),
@@ -412,6 +455,8 @@
int shardIndex,
int totalShards,
Iterable<String> paths,
+ Iterable<String> exceptPackages,
+ Iterable<String> onlyPackages,
Glob filename,
Iterable<String> chosenPresets,
Map<String, Configuration> presets,
@@ -450,6 +495,8 @@
shardIndex: shardIndex ?? this.shardIndex,
totalShards: totalShards ?? this.totalShards,
paths: paths ?? _paths,
+ foldTraceExcept: exceptPackages ?? _foldTraceExcept,
+ foldTraceOnly: onlyPackages ?? _foldTraceOnly,
filename: filename ?? _filename,
chosenPresets: chosenPresets ?? this.chosenPresets,
presets: presets ?? this.presets,
diff --git a/lib/src/runner/configuration/load.dart b/lib/src/runner/configuration/load.dart
index 00e89ad..3a51edd 100644
--- a/lib/src/runner/configuration/load.dart
+++ b/lib/src/runner/configuration/load.dart
@@ -21,6 +21,19 @@
import '../configuration/suite.dart';
import 'reporters.dart';
+/// A regular expression matching a Dart identifier.
+///
+/// This also matches a package name, since they must be Dart identifiers.
+final identifierRegExp = new RegExp(r"[a-zA-Z_]\w*");
+
+/// A regular expression matching allowed package names.
+///
+/// This allows dot-separated valid Dart identifiers. The dots are there for
+/// compatibility with Google's internal Dart packages, but they may not be used
+/// when publishing a package to pub.dartlang.org.
+final _packageName = new RegExp(
+ "^${identifierRegExp.pattern}(\\.${identifierRegExp.pattern})*\$");
+
/// Loads configuration information from a YAML file at [path].
///
/// If [global] is `true`, this restricts the configuration file to only rules
@@ -74,6 +87,7 @@
Configuration _loadGlobalTestConfig() {
var verboseTrace = _getBool("verbose_trace");
var chainStackTraces = _getBool("chain_stack_traces");
+ var foldStackFrames = _loadFoldedStackFrames();
var jsTrace = _getBool("js_trace");
var timeout = _parseValue("timeout", (value) => new Timeout.parse(value));
@@ -108,7 +122,9 @@
jsTrace: jsTrace,
timeout: timeout,
presets: presets,
- chainStackTraces: chainStackTraces)
+ chainStackTraces: chainStackTraces,
+ foldTraceExcept: foldStackFrames["except"],
+ foldTraceOnly: foldStackFrames["only"])
.merge(_extractPresets/*<PlatformSelector>*/(
onPlatform, (map) => new Configuration(onPlatform: map)));
@@ -263,6 +279,44 @@
excludeTags: excludeTags);
}
+ /// Returns a map representation of the `fold_stack_frames` configuration.
+ ///
+ /// The key `except` will correspond to the list of packages to fold.
+ /// The key `only` will correspond to the list of packages to keep in a
+ /// test [Chain].
+ Map<String, List<String>> _loadFoldedStackFrames() {
+ var foldOptionSet = false;
+ return _getMap("fold_stack_frames", key: (keyNode) {
+ _validate(keyNode, "Must be a string", (value) => value is String);
+ _validate(keyNode, 'Must be "only" or "except".',
+ (value) => value == "only" || value == "except");
+
+ if (foldOptionSet) {
+ throw new SourceSpanFormatException(
+ 'Can only contain one of "only" or "except".',
+ keyNode.span,
+ _source);
+ }
+ foldOptionSet = true;
+ return keyNode.value;
+ }, value: (valueNode) {
+ _validate(
+ valueNode,
+ "Folded packages must be strings.",
+ (valueList) =>
+ valueList is YamlList &&
+ valueList.every((value) => value is String));
+
+ _validate(
+ valueNode,
+ "Invalid package name.",
+ (valueList) =>
+ valueList.every((value) => _packageName.hasMatch(value)));
+
+ return valueNode.value;
+ });
+ }
+
/// Throws an exception with [message] if [test] returns `false` when passed
/// [node]'s value.
void _validate(YamlNode node, String message, bool test(value)) {
diff --git a/lib/src/runner/plugin/platform_helpers.dart b/lib/src/runner/plugin/platform_helpers.dart
index bddaea7..841b075 100644
--- a/lib/src/runner/plugin/platform_helpers.dart
+++ b/lib/src/runner/plugin/platform_helpers.dart
@@ -14,6 +14,7 @@
import '../../backend/test_platform.dart';
import '../../util/io.dart';
import '../../util/remote_exception.dart';
+import '../../util/stack_trace_mapper.dart';
import '../application_exception.dart';
import '../configuration.dart';
import '../configuration/suite.dart';
@@ -46,9 +47,7 @@
SuiteConfiguration suiteConfig,
Environment environment,
StreamChannel channel,
- {StackTrace mapTrace(StackTrace trace)}) async {
- if (mapTrace == null) mapTrace = (trace) => trace;
-
+ {StackTraceMapper mapper}) async {
var disconnector = new Disconnector();
var suiteChannel = new MultiChannel(channel.transform(disconnector));
@@ -60,6 +59,9 @@
'path': path,
'collectTraces': Configuration.current.reporter == 'json',
'noRetry': Configuration.current.noRetry,
+ 'stackTraceMapper': mapper?.serialize(),
+ 'foldTraceExcept': Configuration.current.foldTraceExcept.toList(),
+ 'foldTraceOnly': Configuration.current.foldTraceOnly.toList(),
});
var completer = new Completer();
@@ -72,9 +74,9 @@
// If we've already provided a controller, send the error to the
// LoadSuite. This will cause the virtual load test to fail, which will
// notify the user of the error.
- loadSuiteZone.handleUncaughtError(error, mapTrace(stackTrace));
+ loadSuiteZone.handleUncaughtError(error, stackTrace);
} else {
- completer.completeError(error, mapTrace(stackTrace));
+ completer.completeError(error);
}
}
@@ -93,11 +95,11 @@
case "error":
var asyncError = RemoteException.deserialize(response["error"]);
handleError(new LoadException(path, asyncError.error),
- mapTrace(asyncError.stackTrace));
+ asyncError.stackTrace);
break;
case "success":
- var deserializer = new _Deserializer(suiteChannel, mapTrace);
+ var deserializer = new _Deserializer(suiteChannel);
completer.complete(deserializer.deserializeGroup(response["root"]));
break;
}
@@ -130,10 +132,7 @@
/// The channel over which tests communicate.
final MultiChannel _channel;
- /// The function used to errors' map stack traces.
- final _MapTrace _mapTrace;
-
- _Deserializer(this._channel, this._mapTrace);
+ _Deserializer(this._channel);
/// Deserializes [group] into a concrete [Group].
Group deserializeGroup(Map group) {
@@ -160,7 +159,6 @@
var metadata = new Metadata.deserialize(test['metadata']);
var trace = test['trace'] == null ? null : new Trace.parse(test['trace']);
var testChannel = _channel.virtualChannel(test['channel']);
- return new RunnerTest(
- test['name'], metadata, trace, testChannel, _mapTrace);
+ return new RunnerTest(test['name'], metadata, trace, testChannel);
}
}
diff --git a/lib/src/runner/remote_listener.dart b/lib/src/runner/remote_listener.dart
index 5812174..a61fd0e 100644
--- a/lib/src/runner/remote_listener.dart
+++ b/lib/src/runner/remote_listener.dart
@@ -15,7 +15,9 @@
import '../backend/suite.dart';
import '../backend/test.dart';
import '../backend/test_platform.dart';
+import '../frontend/test_chain.dart';
import '../util/remote_exception.dart';
+import '../util/stack_trace_mapper.dart';
import '../utils.dart';
class RemoteListener {
@@ -46,6 +48,8 @@
new StreamChannelController(allowForeignErrors: false, sync: true);
var channel = new MultiChannel(controller.local);
+ var verboseChain = true;
+
var printZone = hidePrints ? null : Zone.current;
runZoned(() async {
var main;
@@ -55,7 +59,7 @@
_sendLoadException(channel, "No top-level main() function defined.");
return;
} catch (error, stackTrace) {
- _sendError(channel, error, stackTrace);
+ _sendError(channel, error, stackTrace, verboseChain);
return;
}
@@ -72,10 +76,17 @@
if (message['asciiGlyphs'] ?? false) glyph.ascii = true;
var metadata = new Metadata.deserialize(message['metadata']);
+ verboseChain = metadata.verboseTrace;
var declarer = new Declarer(
metadata: metadata,
collectTraces: message['collectTraces'],
noRetry: message['noRetry']);
+
+ configureTestChaining(
+ mapper: StackTraceMapper.deserialize(message['stackTraceMapper']),
+ exceptPackages: _deserializeSet(message['foldTraceExcept']),
+ onlyPackages: _deserializeSet(message['foldTraceOnly']));
+
await declarer.declare(main);
var os =
@@ -85,7 +96,7 @@
platform: platform, os: os, path: message['path']);
new RemoteListener._(suite, printZone)._listen(channel);
}, onError: (error, stackTrace) {
- _sendError(channel, error, stackTrace);
+ _sendError(channel, error, stackTrace, verboseChain);
}, zoneSpecification: new ZoneSpecification(print: (_, __, ___, line) {
if (printZone != null) printZone.print(line);
channel.sink.add({"type": "print", "line": line});
@@ -94,6 +105,13 @@
return controller.foreign;
}
+ /// Returns a [Set] from a JSON serialized list.
+ static Set<String> _deserializeSet(List<String> list) {
+ if (list == null) return null;
+ if (list.isEmpty) return null;
+ return new Set.from(list);
+ }
+
/// Sends a message over [channel] indicating that the tests failed to load.
///
/// [message] should describe the failure.
@@ -102,10 +120,12 @@
}
/// Sends a message over [channel] indicating an error from user code.
- static void _sendError(StreamChannel channel, error, StackTrace stackTrace) {
+ static void _sendError(
+ StreamChannel channel, error, StackTrace stackTrace, bool verboseChain) {
channel.sink.add({
"type": "error",
- "error": RemoteException.serialize(error, stackTrace)
+ "error": RemoteException.serialize(
+ error, terseChain(stackTrace, verbose: verboseChain))
});
}
@@ -182,8 +202,10 @@
liveTest.onError.listen((asyncError) {
channel.sink.add({
"type": "error",
- "error":
- RemoteException.serialize(asyncError.error, asyncError.stackTrace)
+ "error": RemoteException.serialize(
+ asyncError.error,
+ terseChain(asyncError.stackTrace,
+ verbose: liveTest.test.metadata.verboseTrace))
});
});
diff --git a/lib/src/runner/reporter/compact.dart b/lib/src/runner/reporter/compact.dart
index a964672..cb4cc9a 100644
--- a/lib/src/runner/reporter/compact.dart
+++ b/lib/src/runner/reporter/compact.dart
@@ -212,9 +212,7 @@
if (error is! LoadException) {
print(indent(error.toString()));
- var chain =
- terseChain(stackTrace, verbose: liveTest.test.metadata.verboseTrace);
- print(indent(chain.toString()));
+ print(indent('$stackTrace'));
return;
}
@@ -225,7 +223,7 @@
error.innerError is! IsolateSpawnException &&
error.innerError is! FormatException &&
error.innerError is! String) {
- print(indent(terseChain(stackTrace).toString()));
+ print(indent('$stackTrace'));
}
}
diff --git a/lib/src/runner/reporter/expanded.dart b/lib/src/runner/reporter/expanded.dart
index 35f289d..00d7915 100644
--- a/lib/src/runner/reporter/expanded.dart
+++ b/lib/src/runner/reporter/expanded.dart
@@ -201,9 +201,7 @@
if (error is! LoadException) {
print(indent(error.toString()));
- var chain =
- terseChain(stackTrace, verbose: liveTest.test.metadata.verboseTrace);
- print(indent(chain.toString()));
+ print(indent('$stackTrace'));
return;
}
@@ -213,7 +211,7 @@
if (error.innerError is! IsolateSpawnException &&
error.innerError is! FormatException &&
error.innerError is! String) {
- print(indent(terseChain(stackTrace).toString()));
+ print(indent('$stackTrace'));
}
}
diff --git a/lib/src/runner/reporter/json.dart b/lib/src/runner/reporter/json.dart
index 374ad21..fba28a6 100644
--- a/lib/src/runner/reporter/json.dart
+++ b/lib/src/runner/reporter/json.dart
@@ -250,9 +250,7 @@
_emit("error", {
"testID": _liveTestIDs[liveTest],
"error": error.toString(),
- "stackTrace":
- terseChain(stackTrace, verbose: liveTest.test.metadata.verboseTrace)
- .toString(),
+ "stackTrace": '$stackTrace',
"isFailure": error is TestFailure
});
}
diff --git a/lib/src/runner/runner_test.dart b/lib/src/runner/runner_test.dart
index 70b8eab..d3b4edd 100644
--- a/lib/src/runner/runner_test.dart
+++ b/lib/src/runner/runner_test.dart
@@ -19,8 +19,6 @@
import '../utils.dart';
import 'spawn_hybrid.dart';
-typedef StackTrace _MapTrace(StackTrace trace);
-
/// A test running remotely, controlled by a stream channel.
class RunnerTest extends Test {
final String name;
@@ -30,16 +28,9 @@
/// The channel used to communicate with the test's [IframeListener].
final MultiChannel _channel;
- /// The function used to reformat errors' stack traces.
- final _MapTrace _mapTrace;
+ RunnerTest(this.name, this.metadata, this.trace, this._channel);
- RunnerTest(
- this.name, this.metadata, Trace trace, this._channel, _MapTrace mapTrace)
- : trace = trace == null ? null : new Trace.from(mapTrace(trace)),
- _mapTrace = mapTrace;
-
- RunnerTest._(
- this.name, this.metadata, this.trace, this._channel, this._mapTrace);
+ RunnerTest._(this.name, this.metadata, this.trace, this._channel);
LiveTest load(Suite suite, {Iterable<Group> groups}) {
var controller;
@@ -54,7 +45,7 @@
switch (message['type']) {
case 'error':
var asyncError = RemoteException.deserialize(message['error']);
- var stackTrace = _mapTrace(asyncError.stackTrace);
+ var stackTrace = asyncError.stackTrace;
controller.addError(asyncError.error, stackTrace);
break;
@@ -108,7 +99,7 @@
Test forPlatform(TestPlatform platform, {OperatingSystem os}) {
if (!metadata.testOn.evaluate(platform, os: os)) return null;
- return new RunnerTest._(name, metadata.forPlatform(platform, os: os), trace,
- _channel, _mapTrace);
+ return new RunnerTest._(
+ name, metadata.forPlatform(platform, os: os), trace, _channel);
}
}
diff --git a/lib/src/util/stack_trace_mapper.dart b/lib/src/util/stack_trace_mapper.dart
index 2299f47..52375d9 100644
--- a/lib/src/util/stack_trace_mapper.dart
+++ b/lib/src/util/stack_trace_mapper.dart
@@ -2,6 +2,7 @@
// 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:collection/collection.dart';
import 'package:package_resolver/package_resolver.dart';
import 'package:source_map_stack_trace/source_map_stack_trace.dart' as mapper;
import 'package:source_maps/source_maps.dart';
@@ -9,22 +10,74 @@
/// A class for mapping JS stack traces to Dart stack traces using source maps.
class StackTraceMapper {
/// The parsed source map.
- final Mapping _mapping;
+ ///
+ /// This is initialized lazily in `mapStackTrace()`.
+ Mapping _mapping;
/// The package resolution information passed to dart2js.
final SyncPackageResolver _packageResolver;
- /// The URI of the SDK root from which dart2js loaded its sources.
+ /// The URL of the SDK root from which dart2js loaded its sources.
final Uri _sdkRoot;
- StackTraceMapper(String contents,
+ /// The contents of the source map.
+ final String _mapContents;
+
+ /// The URL of the source map.
+ final Uri _mapUrl;
+
+ StackTraceMapper(this._mapContents,
{Uri mapUrl, SyncPackageResolver packageResolver, Uri sdkRoot})
- : _mapping = parseExtended(contents, mapUrl: mapUrl),
+ : _mapUrl = mapUrl,
_packageResolver = packageResolver,
_sdkRoot = sdkRoot;
/// Converts [trace] into a Dart stack trace.
- StackTrace mapStackTrace(StackTrace trace) =>
- mapper.mapStackTrace(_mapping, trace,
- packageResolver: _packageResolver, sdkRoot: _sdkRoot);
+ StackTrace mapStackTrace(StackTrace trace) {
+ _mapping ??= parseExtended(_mapContents, mapUrl: _mapUrl);
+ return mapper.mapStackTrace(_mapping, trace,
+ packageResolver: _packageResolver, sdkRoot: _sdkRoot);
+ }
+
+ /// Returns a Map representation which is suitable for JSON serialization.
+ Map<String, dynamic> serialize() {
+ return {
+ 'mapContents': _mapContents,
+ 'sdkRoot': _sdkRoot?.toString(),
+ 'packageConfigMap':
+ _serializePackageConfigMap(_packageResolver.packageConfigMap),
+ 'packageRoot': _packageResolver.packageRoot?.toString(),
+ 'mapUrl': _mapUrl?.toString(),
+ };
+ }
+
+ /// Returns a [StackTraceMapper] contained in the provided serialized
+ /// representation.
+ static StackTraceMapper deserialize(Map serialized) {
+ if (serialized == null) return null;
+ String packageRoot = serialized['packageRoot'] as String ?? '';
+ return new StackTraceMapper(serialized['mapContents'],
+ sdkRoot: Uri.parse(serialized['sdkRoot']),
+ packageResolver: packageRoot.isNotEmpty
+ ? new SyncPackageResolver.root(Uri.parse(serialized['packageRoot']))
+ : new SyncPackageResolver.config(
+ _deserializePackageConfigMap(serialized['packageConfigMap'])),
+ mapUrl: Uri.parse(serialized['mapUrl']));
+ }
+
+ /// Converts a [packageConfigMap] into a format suitable for JSON
+ /// serialization.
+ static Map<String, String> _serializePackageConfigMap(
+ Map<String, Uri> packageConfigMap) {
+ if (packageConfigMap == null) return null;
+ return mapMap(packageConfigMap, value: (_, value) => '$value');
+ }
+
+ /// Converts a serialized package config map into a format suitable for
+ /// the [PackageResolver]
+ static Map<String, Uri> _deserializePackageConfigMap(
+ Map<String, String> serialized) {
+ if (serialized == null) return null;
+ return mapMap(serialized, value: (_, value) => Uri.parse(value));
+ }
}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 060fff8..9b4abe6 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -10,7 +10,6 @@
import 'package:async/async.dart' hide StreamQueue;
import 'package:matcher/matcher.dart';
import 'package:path/path.dart' as p;
-import 'package:stack_trace/stack_trace.dart';
import 'package:term_glyph/term_glyph.dart' as glyph;
import 'backend/invoker.dart';
@@ -169,24 +168,6 @@
/// Returns [str] without any color codes.
String withoutColors(String str) => str.replaceAll(_colorCode, '');
-/// Returns [stackTrace] converted to a [Chain] with all irrelevant frames
-/// folded together.
-///
-/// If [verbose] is `true`, returns the chain for [stackTrace] unmodified.
-Chain terseChain(StackTrace stackTrace, {bool verbose: false}) {
- if (verbose) return new Chain.forTrace(stackTrace);
- return new Chain.forTrace(stackTrace).foldFrames(
- (frame) => frame.package == 'test' || frame.package == 'stream_channel',
- terse: true);
-}
-
-/// Converts [stackTrace] to a [Chain] following the test's configuration.
-Chain testChain(StackTrace stackTrace) {
- // TODO(nweiz): Follow more configuration when #527 is fixed.
- return terseChain(stackTrace,
- verbose: Invoker.current.liveTest.test.metadata.verboseTrace);
-}
-
/// Flattens nested [Iterable]s inside an [Iterable] into a single [List]
/// containing only non-[Iterable] elements.
List flatten(Iterable nested) {
diff --git a/pubspec.yaml b/pubspec.yaml
index c93525a..c91bf4c 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: test
-version: 0.12.22+2
+version: 0.12.23
author: Dart Team <misc@dartlang.org>
description: A library for writing dart unit tests.
homepage: https://github.com/dart-lang/test
diff --git a/test/frontend/test_chain_test.dart b/test/frontend/test_chain_test.dart
new file mode 100644
index 0000000..1a36039
--- /dev/null
+++ b/test/frontend/test_chain_test.dart
@@ -0,0 +1,85 @@
+// 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.
+
+@TestOn("vm")
+
+import 'dart:convert';
+
+import 'package:test_descriptor/test_descriptor.dart' as d;
+import 'package:test/test.dart';
+
+import '../io.dart';
+
+void main() {
+ setUp(() async {
+ await d
+ .file(
+ "test.dart",
+ """
+ import 'dart:async';
+
+ import 'package:test/test.dart';
+
+ void main() {
+ test("failure", () async{
+ await new Future((){});
+ await new Future((){});
+ throw "oh no";
+ });
+ }
+ """)
+ .create();
+ });
+ test("folds packages contained in the except list", () async {
+ await d
+ .file(
+ "dart_test.yaml",
+ JSON.encode({
+ "fold_stack_frames": {
+ "except": ["stream_channel"]
+ }
+ }))
+ .create();
+ var test = await runTest(["test.dart"]);
+ expect(test.stdoutStream(), neverEmits(contains('package:stream_channel')));
+ await test.shouldExit(1);
+ });
+
+ test("by default folds both stream_channel and test packages", () async {
+ var test = await runTest(["test.dart"]);
+ expect(test.stdoutStream(), neverEmits(contains('package:test')));
+ expect(test.stdoutStream(), neverEmits(contains('package:stream_channel')));
+ await test.shouldExit(1);
+ });
+
+ test("folds all packages not contained in the only list", () async {
+ await d
+ .file(
+ "dart_test.yaml",
+ JSON.encode({
+ "fold_stack_frames": {
+ "only": ["test"]
+ }
+ }))
+ .create();
+ var test = await runTest(["test.dart"]);
+ expect(test.stdoutStream(), neverEmits(contains('package:stream_channel')));
+ await test.shouldExit(1);
+ });
+
+ test("does not fold packages in the only list", () async {
+ await d
+ .file(
+ "dart_test.yaml",
+ JSON.encode({
+ "fold_stack_frames": {
+ "only": ["test"]
+ }
+ }))
+ .create();
+ var test = await runTest(["test.dart"]);
+ expect(test.stdoutStream(), emitsThrough(contains('package:test')));
+ await test.shouldExit(1);
+ });
+}
diff --git a/test/runner/configuration/top_level_error_test.dart b/test/runner/configuration/top_level_error_test.dart
index 0d93d63..712cf5e 100644
--- a/test/runner/configuration/top_level_error_test.dart
+++ b/test/runner/configuration/top_level_error_test.dart
@@ -13,6 +13,67 @@
import '../../io.dart';
void main() {
+ test("rejects an invalid fold_stack_frames", () async {
+ await d
+ .file("dart_test.yaml", JSON.encode({"fold_stack_frames": "flup"}))
+ .create();
+
+ var test = await runTest(["test.dart"]);
+ expect(test.stderr,
+ containsInOrder(["fold_stack_frames must be a map", "^^^^^^"]));
+ await test.shouldExit(exit_codes.data);
+ });
+
+ test("rejects multiple fold_stack_frames keys", () async {
+ await d
+ .file(
+ "dart_test.yaml",
+ JSON.encode({
+ "fold_stack_frames": {
+ "except": ["blah"],
+ "only": ["blah"]
+ }
+ }))
+ .create();
+
+ var test = await runTest(["test.dart"]);
+ expect(
+ test.stderr,
+ containsInOrder(
+ ['Can only contain one of "only" or "except".', "^^^^^^"]));
+ await test.shouldExit(exit_codes.data);
+ });
+
+ test("rejects invalid fold_stack_frames keys", () async {
+ await d
+ .file(
+ "dart_test.yaml",
+ JSON.encode({
+ "fold_stack_frames": {"invalid": "blah"}
+ }))
+ .create();
+
+ var test = await runTest(["test.dart"]);
+ expect(test.stderr,
+ containsInOrder(['Must be "only" or "except".', "^^^^^^"]));
+ await test.shouldExit(exit_codes.data);
+ });
+
+ test("rejects invalid fold_stack_frames values", () async {
+ await d
+ .file(
+ "dart_test.yaml",
+ JSON.encode({
+ "fold_stack_frames": {"only": "blah"}
+ }))
+ .create();
+
+ var test = await runTest(["test.dart"]);
+ expect(test.stderr,
+ containsInOrder(["Folded packages must be strings", "^^^^^^"]));
+ await test.shouldExit(exit_codes.data);
+ });
+
test("rejects an invalid pause_after_load", () async {
await d
.file("dart_test.yaml", JSON.encode({"pause_after_load": "flup"}))