Skip to content

Commit 960bbe6

Browse files
authored
Configurable package folding (#650)
1 parent ef9b4e2 commit 960bbe6

File tree

19 files changed

+452
-74
lines changed

19 files changed

+452
-74
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.12.23
2+
3+
* Add a `fold_stack_frames` field for `dart_test.yaml`. This will
4+
allow users to customize which packages' frames are folded.
5+
16
## 0.12.22+2
27

38
* Properly allocate ports when debugging Chrome and Dartium in an IPv6-only

doc/configuration.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ tags:
4545
* [`run_skipped`](#run_skipped)
4646
* [`pub_serve`](#pub_serve)
4747
* [`reporter`](#reporter)
48+
* [`fold_stack_frames`](#fold_stack_frames)
4849
* [Configuring Tags](#configuring-tags)
4950
* [`tags`](#tags)
5051
* [`add_tags`](#add_tags)
@@ -394,6 +395,35 @@ reporter: expanded
394395
This field is not supported in the
395396
[global configuration file](#global-configuration).
396397

398+
### `fold_stack_frames`
399+
400+
This field controls which packages' stack frames will be folded away
401+
when displaying stack traces. Packages contained in the `exclude`
402+
option will be folded. If `only` is provided, all packages not
403+
contained in this list will be folded. By default,
404+
frames from the `test` package and the `stream_channel`
405+
package are folded.
406+
407+
```yaml
408+
fold_stack_frames:
409+
except:
410+
- test
411+
- stream_channel
412+
```
413+
414+
Sample stack trace, note the absence of `package:test`
415+
and `package:stream_channel`:
416+
```
417+
test/sample_test.dart 7:5 main.<fn>
418+
===== asynchronous gap ===========================
419+
dart:async _Completer.completeError
420+
test/sample_test.dart 8:3 main.<fn>
421+
===== asynchronous gap ===========================
422+
dart:async _asyncThenWrapperHelper
423+
test/sample_test.dart 5:27 main.<fn>
424+
```
425+
426+
397427
## Configuring Tags
398428

399429
### `tags`

lib/src/frontend/stream_matcher.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'package:async/async.dart';
88
import 'package:matcher/matcher.dart';
99

1010
import '../utils.dart';
11+
import '../backend/invoker.dart';
12+
import 'test_chain.dart';
1113
import 'async_matcher.dart';
1214

1315
/// The type for [_StreamMatcher._matchQueue].
@@ -164,7 +166,8 @@ class _StreamMatcher extends AsyncMatcher implements StreamMatcher {
164166
return addBullet(event.asValue.value.toString());
165167
} else {
166168
var error = event.asError;
167-
var text = "${error.error}\n${testChain(error.stackTrace)}";
169+
var chain = testChain(error.stackTrace);
170+
var text = "${error.error}\n$chain";
168171
return prefixLines(text, " ", first: "! ");
169172
}
170173
}).join("\n");

lib/src/frontend/test_chain.dart

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:stack_trace/stack_trace.dart';
6+
7+
import '../backend/invoker.dart';
8+
import '../util/stack_trace_mapper.dart';
9+
10+
/// Converts [trace] into a Dart stack trace
11+
StackTraceMapper _mapper;
12+
13+
/// The list of packages to fold when producing [Chain]s.
14+
Set<String> _exceptPackages = new Set.from(['test', 'stream_channel']);
15+
16+
/// If non-empty, all packages not in this list will be folded when producing
17+
/// [Chain]s.
18+
Set<String> _onlyPackages = new Set();
19+
20+
/// Configure the resources used for test chaining.
21+
///
22+
/// [mapper] is used to convert traces into Dart stack traces.
23+
/// [exceptPackages] is the list of packages to fold when producing a [Chain].
24+
/// [onlyPackages] is the list of packages to keep in a [Chain]. If non-empty,
25+
/// all packages not in this will be folded.
26+
void configureTestChaining(
27+
{StackTraceMapper mapper,
28+
Set<String> exceptPackages,
29+
Set<String> onlyPackages}) {
30+
if (mapper != null) _mapper = mapper;
31+
if (exceptPackages != null) _exceptPackages = exceptPackages;
32+
if (onlyPackages != null) _onlyPackages = onlyPackages;
33+
}
34+
35+
/// Returns [stackTrace] converted to a [Chain] with all irrelevant frames
36+
/// folded together.
37+
///
38+
/// If [verbose] is `true`, returns the chain for [stackTrace] unmodified.
39+
Chain terseChain(StackTrace stackTrace, {bool verbose: false}) {
40+
var testTrace = _mapper?.mapStackTrace(stackTrace) ?? stackTrace;
41+
if (verbose) return new Chain.forTrace(testTrace);
42+
return new Chain.forTrace(testTrace).foldFrames((frame) {
43+
if (_onlyPackages.isNotEmpty) {
44+
return !_onlyPackages.contains(frame.package);
45+
}
46+
return _exceptPackages.contains(frame.package);
47+
}, terse: true);
48+
}
49+
50+
/// Converts [stackTrace] to a [Chain] following the test's configuration.
51+
Chain testChain(StackTrace stackTrace) => terseChain(stackTrace,
52+
verbose: Invoker.current?.liveTest?.test?.metadata?.verboseTrace ?? true);

lib/src/frontend/throws_matcher.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'package:matcher/matcher.dart';
88

99
import '../utils.dart';
1010
import 'async_matcher.dart';
11+
import '../frontend/test_chain.dart';
12+
import '../backend/invoker.dart';
1113

1214
/// This function is deprecated.
1315
///

lib/src/runner/browser/browser_manager.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ class BrowserManager {
238238
try {
239239
controller = await deserializeSuite(
240240
path, _platform, suiteConfig, await _environment, suiteChannel,
241-
mapTrace: mapper?.mapStackTrace);
241+
mapper: mapper);
242242
_controllers.add(controller);
243243
return controller.suite;
244244
} catch (_) {

lib/src/runner/configuration.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ class Configuration {
9797
/// See [shardIndex] for details.
9898
final int totalShards;
9999

100+
/// The list of packages to fold when producing [StackTrace]s.
101+
Set<String> get foldTraceExcept => _foldTraceExcept ?? new Set();
102+
final Set<String> _foldTraceExcept;
103+
104+
/// If non-empty, all packages not in this list will be folded when producing
105+
/// [StackTrace]s.
106+
Set<String> get foldTraceOnly => _foldTraceOnly ?? new Set();
107+
final Set<String> _foldTraceOnly;
108+
100109
/// The paths from which to load tests.
101110
List<String> get paths => _paths ?? ["test"];
102111
final List<String> _paths;
@@ -198,6 +207,8 @@ class Configuration {
198207
int shardIndex,
199208
int totalShards,
200209
Iterable<String> paths,
210+
Iterable<String> foldTraceExcept,
211+
Iterable<String> foldTraceOnly,
201212
Glob filename,
202213
Iterable<String> chosenPresets,
203214
Map<String, Configuration> presets,
@@ -238,6 +249,8 @@ class Configuration {
238249
shardIndex: shardIndex,
239250
totalShards: totalShards,
240251
paths: paths,
252+
foldTraceExcept: foldTraceExcept,
253+
foldTraceOnly: foldTraceOnly,
241254
filename: filename,
242255
chosenPresets: chosenPresetSet,
243256
presets: _withChosenPresets(presets, chosenPresetSet),
@@ -290,6 +303,8 @@ class Configuration {
290303
this.shardIndex,
291304
this.totalShards,
292305
Iterable<String> paths,
306+
Iterable<String> foldTraceExcept,
307+
Iterable<String> foldTraceOnly,
293308
Glob filename,
294309
Iterable<String> chosenPresets,
295310
Map<String, Configuration> presets,
@@ -307,6 +322,8 @@ class Configuration {
307322
: Uri.parse("http://localhost:$pubServePort"),
308323
_concurrency = concurrency,
309324
_paths = _list(paths),
325+
_foldTraceExcept = _set(foldTraceExcept),
326+
_foldTraceOnly = _set(foldTraceOnly),
310327
_filename = filename,
311328
chosenPresets =
312329
new UnmodifiableSetView(chosenPresets?.toSet() ?? new Set()),
@@ -347,6 +364,14 @@ class Configuration {
347364
return list;
348365
}
349366

367+
/// Returns a set from [input].
368+
static Set<T> _set<T>(Iterable<T> input) {
369+
if (input == null) return null;
370+
var set = new Set<T>.from(input);
371+
if (set.isEmpty) return null;
372+
return set;
373+
}
374+
350375
/// Returns an unmodifiable copy of [input] or an empty unmodifiable map.
351376
static Map/*<K, V>*/ _map/*<K, V>*/(Map/*<K, V>*/ input) {
352377
if (input == null || input.isEmpty) return const {};
@@ -369,6 +394,22 @@ class Configuration {
369394
if (this == Configuration.empty) return other;
370395
if (other == Configuration.empty) return this;
371396

397+
var foldTraceOnly = other._foldTraceOnly ?? _foldTraceOnly;
398+
var foldTraceExcept = other._foldTraceExcept ?? _foldTraceExcept;
399+
if (_foldTraceOnly != null) {
400+
if (other._foldTraceExcept != null) {
401+
foldTraceOnly = _foldTraceOnly.difference(other._foldTraceExcept);
402+
} else if (other._foldTraceOnly != null) {
403+
foldTraceOnly = other._foldTraceOnly.intersection(_foldTraceOnly);
404+
}
405+
} else if (_foldTraceExcept != null) {
406+
if (other._foldTraceOnly != null) {
407+
foldTraceOnly = other._foldTraceOnly.difference(_foldTraceExcept);
408+
} else if (other._foldTraceExcept != null) {
409+
foldTraceExcept = other._foldTraceExcept.union(_foldTraceExcept);
410+
}
411+
}
412+
372413
var result = new Configuration._(
373414
help: other._help ?? _help,
374415
version: other._version ?? _version,
@@ -382,6 +423,8 @@ class Configuration {
382423
shardIndex: other.shardIndex ?? shardIndex,
383424
totalShards: other.totalShards ?? totalShards,
384425
paths: other._paths ?? _paths,
426+
foldTraceExcept: foldTraceExcept,
427+
foldTraceOnly: foldTraceOnly,
385428
filename: other._filename ?? _filename,
386429
chosenPresets: chosenPresets.union(other.chosenPresets),
387430
presets: _mergeConfigMaps(presets, other.presets),
@@ -412,6 +455,8 @@ class Configuration {
412455
int shardIndex,
413456
int totalShards,
414457
Iterable<String> paths,
458+
Iterable<String> exceptPackages,
459+
Iterable<String> onlyPackages,
415460
Glob filename,
416461
Iterable<String> chosenPresets,
417462
Map<String, Configuration> presets,
@@ -450,6 +495,8 @@ class Configuration {
450495
shardIndex: shardIndex ?? this.shardIndex,
451496
totalShards: totalShards ?? this.totalShards,
452497
paths: paths ?? _paths,
498+
foldTraceExcept: exceptPackages ?? _foldTraceExcept,
499+
foldTraceOnly: onlyPackages ?? _foldTraceOnly,
453500
filename: filename ?? _filename,
454501
chosenPresets: chosenPresets ?? this.chosenPresets,
455502
presets: presets ?? this.presets,

lib/src/runner/configuration/load.dart

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ import '../configuration.dart';
2121
import '../configuration/suite.dart';
2222
import 'reporters.dart';
2323

24+
/// A regular expression matching a Dart identifier.
25+
///
26+
/// This also matches a package name, since they must be Dart identifiers.
27+
final identifierRegExp = new RegExp(r"[a-zA-Z_]\w*");
28+
29+
/// A regular expression matching allowed package names.
30+
///
31+
/// This allows dot-separated valid Dart identifiers. The dots are there for
32+
/// compatibility with Google's internal Dart packages, but they may not be used
33+
/// when publishing a package to pub.dartlang.org.
34+
final _packageName = new RegExp(
35+
"^${identifierRegExp.pattern}(\\.${identifierRegExp.pattern})*\$");
36+
2437
/// Loads configuration information from a YAML file at [path].
2538
///
2639
/// If [global] is `true`, this restricts the configuration file to only rules
@@ -74,6 +87,7 @@ class _ConfigurationLoader {
7487
Configuration _loadGlobalTestConfig() {
7588
var verboseTrace = _getBool("verbose_trace");
7689
var chainStackTraces = _getBool("chain_stack_traces");
90+
var foldStackFrames = _loadFoldedStackFrames();
7791
var jsTrace = _getBool("js_trace");
7892

7993
var timeout = _parseValue("timeout", (value) => new Timeout.parse(value));
@@ -108,7 +122,9 @@ class _ConfigurationLoader {
108122
jsTrace: jsTrace,
109123
timeout: timeout,
110124
presets: presets,
111-
chainStackTraces: chainStackTraces)
125+
chainStackTraces: chainStackTraces,
126+
foldTraceExcept: foldStackFrames["except"],
127+
foldTraceOnly: foldStackFrames["only"])
112128
.merge(_extractPresets/*<PlatformSelector>*/(
113129
onPlatform, (map) => new Configuration(onPlatform: map)));
114130

@@ -263,6 +279,44 @@ class _ConfigurationLoader {
263279
excludeTags: excludeTags);
264280
}
265281

282+
/// Returns a map representation of the `fold_stack_frames` configuration.
283+
///
284+
/// The key `except` will correspond to the list of packages to fold.
285+
/// The key `only` will correspond to the list of packages to keep in a
286+
/// test [Chain].
287+
Map<String, List<String>> _loadFoldedStackFrames() {
288+
var foldOptionSet = false;
289+
return _getMap("fold_stack_frames", key: (keyNode) {
290+
_validate(keyNode, "Must be a string", (value) => value is String);
291+
_validate(keyNode, 'Must be "only" or "except".',
292+
(value) => value == "only" || value == "except");
293+
294+
if (foldOptionSet) {
295+
throw new SourceSpanFormatException(
296+
'Can only contain one of "only" or "except".',
297+
keyNode.span,
298+
_source);
299+
}
300+
foldOptionSet = true;
301+
return keyNode.value;
302+
}, value: (valueNode) {
303+
_validate(
304+
valueNode,
305+
"Folded packages must be strings.",
306+
(valueList) =>
307+
valueList is YamlList &&
308+
valueList.every((value) => value is String));
309+
310+
_validate(
311+
valueNode,
312+
"Invalid package name.",
313+
(valueList) =>
314+
valueList.every((value) => _packageName.hasMatch(value)));
315+
316+
return valueNode.value;
317+
});
318+
}
319+
266320
/// Throws an exception with [message] if [test] returns `false` when passed
267321
/// [node]'s value.
268322
void _validate(YamlNode node, String message, bool test(value)) {

0 commit comments

Comments
 (0)