diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a72e514f..d13790d03 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 7f294429b..24cf7c038 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -45,6 +45,7 @@ tags: * [`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 @@ reporter: expanded 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. +===== asynchronous gap =========================== +dart:async _Completer.completeError +test/sample_test.dart 8:3 main. +===== asynchronous gap =========================== +dart:async _asyncThenWrapperHelper +test/sample_test.dart 5:27 main. +``` + + ## Configuring Tags ### `tags` diff --git a/lib/src/frontend/stream_matcher.dart b/lib/src/frontend/stream_matcher.dart index 6c906744b..9e166ca49 100644 --- a/lib/src/frontend/stream_matcher.dart +++ b/lib/src/frontend/stream_matcher.dart @@ -8,6 +8,8 @@ import 'package:async/async.dart'; 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 @@ class _StreamMatcher extends AsyncMatcher implements StreamMatcher { 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 000000000..41bd9e74c --- /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 _exceptPackages = new Set.from(['test', 'stream_channel']); + +/// If non-empty, all packages not in this list will be folded when producing +/// [Chain]s. +Set _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 exceptPackages, + Set 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 8518c0f94..75c5d230b 100644 --- a/lib/src/frontend/throws_matcher.dart +++ b/lib/src/frontend/throws_matcher.dart @@ -8,6 +8,8 @@ import 'package:matcher/matcher.dart'; 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 7f25b3886..841b0b3b5 100644 --- a/lib/src/runner/browser/browser_manager.dart +++ b/lib/src/runner/browser/browser_manager.dart @@ -238,7 +238,7 @@ class BrowserManager { 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 fc43ed03e..06cc76cd7 100644 --- a/lib/src/runner/configuration.dart +++ b/lib/src/runner/configuration.dart @@ -97,6 +97,15 @@ class Configuration { /// See [shardIndex] for details. final int totalShards; + /// The list of packages to fold when producing [StackTrace]s. + Set get foldTraceExcept => _foldTraceExcept ?? new Set(); + final Set _foldTraceExcept; + + /// If non-empty, all packages not in this list will be folded when producing + /// [StackTrace]s. + Set get foldTraceOnly => _foldTraceOnly ?? new Set(); + final Set _foldTraceOnly; + /// The paths from which to load tests. List get paths => _paths ?? ["test"]; final List _paths; @@ -198,6 +207,8 @@ class Configuration { int shardIndex, int totalShards, Iterable paths, + Iterable foldTraceExcept, + Iterable foldTraceOnly, Glob filename, Iterable chosenPresets, Map presets, @@ -238,6 +249,8 @@ class Configuration { shardIndex: shardIndex, totalShards: totalShards, paths: paths, + foldTraceExcept: foldTraceExcept, + foldTraceOnly: foldTraceOnly, filename: filename, chosenPresets: chosenPresetSet, presets: _withChosenPresets(presets, chosenPresetSet), @@ -290,6 +303,8 @@ class Configuration { this.shardIndex, this.totalShards, Iterable paths, + Iterable foldTraceExcept, + Iterable foldTraceOnly, Glob filename, Iterable chosenPresets, Map presets, @@ -307,6 +322,8 @@ class Configuration { : 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 @@ class Configuration { return list; } + /// Returns a set from [input]. + static Set _set(Iterable input) { + if (input == null) return null; + var set = new Set.from(input); + if (set.isEmpty) return null; + return set; + } + /// Returns an unmodifiable copy of [input] or an empty unmodifiable map. static Map/**/ _map/**/(Map/**/ input) { if (input == null || input.isEmpty) return const {}; @@ -369,6 +394,22 @@ class Configuration { 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 @@ class Configuration { 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 @@ class Configuration { int shardIndex, int totalShards, Iterable paths, + Iterable exceptPackages, + Iterable onlyPackages, Glob filename, Iterable chosenPresets, Map presets, @@ -450,6 +495,8 @@ class Configuration { 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 00e89ad60..3a51edd57 100644 --- a/lib/src/runner/configuration/load.dart +++ b/lib/src/runner/configuration/load.dart @@ -21,6 +21,19 @@ import '../configuration.dart'; 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 @@ class _ConfigurationLoader { 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 @@ class _ConfigurationLoader { jsTrace: jsTrace, timeout: timeout, presets: presets, - chainStackTraces: chainStackTraces) + chainStackTraces: chainStackTraces, + foldTraceExcept: foldStackFrames["except"], + foldTraceOnly: foldStackFrames["only"]) .merge(_extractPresets/**/( onPlatform, (map) => new Configuration(onPlatform: map))); @@ -263,6 +279,44 @@ class _ConfigurationLoader { 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> _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 bddaea79b..841b0756a 100644 --- a/lib/src/runner/plugin/platform_helpers.dart +++ b/lib/src/runner/plugin/platform_helpers.dart @@ -14,6 +14,7 @@ import '../../backend/test.dart'; 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 @@ Future deserializeSuite( 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 @@ Future deserializeSuite( '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 @@ Future deserializeSuite( // 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 @@ Future deserializeSuite( 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 @@ class _Deserializer { /// 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 @@ class _Deserializer { 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 5812174d3..a61fd0edf 100644 --- a/lib/src/runner/remote_listener.dart +++ b/lib/src/runner/remote_listener.dart @@ -15,7 +15,9 @@ import '../backend/operating_system.dart'; 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 @@ class RemoteListener { 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 @@ class RemoteListener { _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 @@ class RemoteListener { 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 @@ class RemoteListener { 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 @@ class RemoteListener { return controller.foreign; } + /// Returns a [Set] from a JSON serialized list. + static Set _deserializeSet(List 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 @@ class RemoteListener { } /// 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 @@ class RemoteListener { 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 a964672b4..cb4cc9acb 100644 --- a/lib/src/runner/reporter/compact.dart +++ b/lib/src/runner/reporter/compact.dart @@ -212,9 +212,7 @@ class CompactReporter implements Reporter { 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 @@ class CompactReporter implements Reporter { 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 35f289de3..00d7915be 100644 --- a/lib/src/runner/reporter/expanded.dart +++ b/lib/src/runner/reporter/expanded.dart @@ -201,9 +201,7 @@ class ExpandedReporter implements Reporter { 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 @@ class ExpandedReporter implements Reporter { 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 374ad2136..fba28a6ca 100644 --- a/lib/src/runner/reporter/json.dart +++ b/lib/src/runner/reporter/json.dart @@ -250,9 +250,7 @@ class JsonReporter implements Reporter { _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 70b8eaba1..d3b4eddb2 100644 --- a/lib/src/runner/runner_test.dart +++ b/lib/src/runner/runner_test.dart @@ -19,8 +19,6 @@ import '../util/remote_exception.dart'; 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 @@ class RunnerTest extends Test { /// 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, 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); - 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 groups}) { var controller; @@ -54,7 +45,7 @@ class RunnerTest extends Test { 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 @@ class RunnerTest extends Test { 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 2299f4704..52375d9f1 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 @@ import 'package:source_maps/source_maps.dart'; /// 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 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 _serializePackageConfigMap( + Map 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 _deserializePackageConfigMap( + Map 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 060fff8f4..9b4abe6b7 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -10,7 +10,6 @@ import 'dart:typed_data'; 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 @@ final _colorCode = new RegExp('\u001b\\[[0-9;]+m'); /// 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 c93525a0f..c91bf4c57 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 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 000000000..1a3603937 --- /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 0d93d63a7..712cf5ec9 100644 --- a/test/runner/configuration/top_level_error_test.dart +++ b/test/runner/configuration/top_level_error_test.dart @@ -13,6 +13,67 @@ import 'package:test/test.dart'; 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"}))