Track which Widget objects were created by the local project. (#15041)
Make flutter test support the --track-widget-creation flag.
Add widget creation location tests. Tests are skipped when
--track-widget-creation flag is not passed.
diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart
index 95a3f80..9c1f9b3 100644
--- a/packages/flutter/lib/src/widgets/widget_inspector.dart
+++ b/packages/flutter/lib/src/widgets/widget_inspector.dart
@@ -144,6 +144,8 @@
final Map<Object, String> _objectToId = new Map<Object, String>.identity();
int _nextId = 0;
+ List<String> _pubRootDirectories;
+
/// Clear all InspectorService object references.
///
/// Use this method only for testing to ensure that object references from one
@@ -258,6 +260,17 @@
_decrementReferenceCount(referenceData);
}
+ /// Set the list of directories that should be considered part of the local
+ /// project.
+ ///
+ /// The local project directories are used to distinguish widgets created by
+ /// the local project over widgets created from inside the framework.
+ void setPubRootDirectories(List<Object> pubRootDirectories) {
+ _pubRootDirectories = pubRootDirectories.map<String>(
+ (Object directory) => Uri.parse(directory).path,
+ ).toList();
+ }
+
/// Set the [WidgetInspector] selection to the object matching the specified
/// id if the object is valid object to set as the inspector selection.
///
@@ -359,10 +372,26 @@
final _Location creationLocation = _getCreationLocation(value);
if (creationLocation != null) {
json['creationLocation'] = creationLocation.toJsonMap();
+ if (_isLocalCreationLocation(creationLocation)) {
+ json['createdByLocalProject'] = true;
+ }
}
return json;
}
+ bool _isLocalCreationLocation(_Location location) {
+ if (_pubRootDirectories == null || location == null || location.file == null) {
+ return false;
+ }
+ final String file = Uri.parse(location.file).path;
+ for (String directory in _pubRootDirectories) {
+ if (file.startsWith(directory)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
String _serialize(DiagnosticsNode node, String groupName) {
return JSON.encode(_nodeToJson(node, groupName));
}
diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart
index 58fb53d..8d1249a 100644
--- a/packages/flutter/test/widgets/widget_inspector_test.dart
+++ b/packages/flutter/test/widgets/widget_inspector_test.dart
@@ -498,4 +498,147 @@
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
});
+
+ testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async {
+ final WidgetInspectorService service = WidgetInspectorService.instance;
+
+ await tester.pumpWidget(
+ new Directionality(
+ textDirection: TextDirection.ltr,
+ child: new Stack(
+ children: const <Widget>[
+ const Text('a'),
+ const Text('b', textDirection: TextDirection.ltr),
+ const Text('c', textDirection: TextDirection.ltr),
+ ],
+ ),
+ ),
+ );
+ final Element elementA = find.text('a').evaluate().first;
+ final Element elementB = find.text('b').evaluate().first;
+
+ service.disposeAllGroups();
+ service.setPubRootDirectories(<Object>[]);
+ service.setSelection(elementA, 'my-group');
+ final Map<String, Object> jsonA = JSON.decode(service.getSelectedWidget(null, 'my-group'));
+ final Map<String, Object> creationLocationA = jsonA['creationLocation'];
+ expect(creationLocationA, isNotNull);
+ final String fileA = creationLocationA['file'];
+ final int lineA = creationLocationA['line'];
+ final int columnA = creationLocationA['column'];
+ final List<Object> parameterLocationsA = creationLocationA['parameterLocations'];
+
+ service.setSelection(elementB, 'my-group');
+ final Map<String, Object> jsonB = JSON.decode(service.getSelectedWidget(null, 'my-group'));
+ final Map<String, Object> creationLocationB = jsonB['creationLocation'];
+ expect(creationLocationB, isNotNull);
+ final String fileB = creationLocationB['file'];
+ final int lineB = creationLocationB['line'];
+ final int columnB = creationLocationB['column'];
+ final List<Object> parameterLocationsB = creationLocationB['parameterLocations'];
+ expect(fileA, endsWith('widget_inspector_test.dart'));
+ expect(fileA, equals(fileB));
+ // We don't hardcode the actual lines the widgets are created on as that
+ // would make this test fragile.
+ expect(lineA + 1, equals(lineB));
+ // Column numbers are more stable than line numbers.
+ expect(columnA, equals(19));
+ expect(columnA, equals(columnB));
+ expect(parameterLocationsA.length, equals(1));
+ final Map<String, Object> paramA = parameterLocationsA[0];
+ expect(paramA['name'], equals('data'));
+ expect(paramA['line'], equals(lineA));
+ expect(paramA['column'], equals(24));
+
+ expect(parameterLocationsB.length, equals(2));
+ final Map<String, Object> paramB1 = parameterLocationsB[0];
+ expect(paramB1['name'], equals('data'));
+ expect(paramB1['line'], equals(lineB));
+ expect(paramB1['column'], equals(24));
+ final Map<String, Object> paramB2 = parameterLocationsB[1];
+ expect(paramB2['name'], equals('textDirection'));
+ expect(paramB2['line'], equals(lineB));
+ expect(paramB2['column'], equals(29));
+ }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
+
+ testWidgets('WidgetInspectorService setPubRootDirectories', (WidgetTester tester) async {
+ final WidgetInspectorService service = WidgetInspectorService.instance;
+
+ await tester.pumpWidget(
+ new Directionality(
+ textDirection: TextDirection.ltr,
+ child: new Stack(
+ children: const <Widget>[
+ const Text('a'),
+ const Text('b', textDirection: TextDirection.ltr),
+ const Text('c', textDirection: TextDirection.ltr),
+ ],
+ ),
+ ),
+ );
+ final Element elementA = find.text('a').evaluate().first;
+
+ service.disposeAllGroups();
+ service.setPubRootDirectories(<Object>[]);
+ service.setSelection(elementA, 'my-group');
+ Map<String, Object> json = JSON.decode(service.getSelectedWidget(null, 'my-group'));
+ Map<String, Object> creationLocation = json['creationLocation'];
+ expect(creationLocation, isNotNull);
+ final String fileA = creationLocation['file'];
+ expect(fileA, endsWith('widget_inspector_test.dart'));
+ expect(json, isNot(contains('createdByLocalProject')));
+ final List<String> segments = Uri.parse(fileA).pathSegments;
+ // Strip a couple subdirectories away to generate a plausible pub root
+ // directory.
+ final String pubRootTest = '/' + segments.take(segments.length - 2).join('/');
+ service.setPubRootDirectories(<Object>[pubRootTest]);
+
+ service.setSelection(elementA, 'my-group');
+ expect(JSON.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
+
+ service.setPubRootDirectories(<Object>['/invalid/$pubRootTest']);
+ expect(JSON.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
+
+ service.setPubRootDirectories(<Object>['file://$pubRootTest']);
+ expect(JSON.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
+
+ service.setPubRootDirectories(<Object>['$pubRootTest/different']);
+ expect(JSON.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
+
+ service.setPubRootDirectories(<Object>[
+ '/invalid/$pubRootTest',
+ pubRootTest,
+ ]);
+ expect(JSON.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
+
+ // The RichText child of the Text widget is created by the core framework
+ // not the current package.
+ final Element richText = find.descendant(
+ of: find.text('a'),
+ matching: find.byType(RichText),
+ ).evaluate().first;
+ service.setSelection(richText, 'my-group');
+ service.setPubRootDirectories(<Object>[pubRootTest]);
+ json = JSON.decode(service.getSelectedWidget(null, 'my-group'));
+ expect(json, isNot(contains('createdByLocalProject')));
+ creationLocation = json['creationLocation'];
+ expect(creationLocation, isNotNull);
+ // This RichText widget is created by the build method of the Text widget
+ // thus the creation location is in text.dart not basic.dart
+ final List<String> pathSegmentsFramework = Uri.parse(creationLocation['file']).pathSegments;
+ expect(pathSegmentsFramework.join('/'), endsWith('/packages/flutter/lib/src/widgets/text.dart'));
+
+ // Strip off /src/widgets/text.dart.
+ final String pubRootFramework = '/' + pathSegmentsFramework.take(pathSegmentsFramework.length - 3).join('/');
+ service.setPubRootDirectories(<Object>[pubRootFramework]);
+ expect(JSON.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
+ service.setSelection(elementA, 'my-group');
+ expect(JSON.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
+
+ service.setPubRootDirectories(<Object>[pubRootFramework, pubRootTest]);
+ service.setSelection(elementA, 'my-group');
+ expect(JSON.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
+ service.setSelection(richText, 'my-group');
+ expect(JSON.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
+ }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
}
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index f6586b2..138a174 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -23,54 +23,72 @@
TestCommand({ bool verboseHelp: false }) {
requiresPubspecYaml();
usesPubOption();
- argParser.addOption('name',
+ argParser.addOption(
+ 'name',
help: 'A regular expression matching substrings of the names of tests to run.',
valueHelp: 'regexp',
allowMultiple: true,
splitCommas: false,
);
- argParser.addOption('plain-name',
+ argParser.addOption(
+ 'plain-name',
help: 'A plain-text substring of the names of tests to run.',
valueHelp: 'substring',
allowMultiple: true,
splitCommas: false,
);
- argParser.addFlag('start-paused',
- defaultsTo: false,
- negatable: false,
- help: 'Start in a paused mode and wait for a debugger to connect.\n'
- 'You must specify a single test file to run, explicitly.\n'
- 'Instructions for connecting with a debugger and printed to the\n'
- 'console once the test has started.'
- );
- argParser.addFlag('coverage',
+ argParser.addFlag(
+ 'start-paused',
defaultsTo: false,
negatable: false,
- help: 'Whether to collect coverage information.'
+ help: 'Start in a paused mode and wait for a debugger to connect.\n'
+ 'You must specify a single test file to run, explicitly.\n'
+ 'Instructions for connecting with a debugger and printed to the\n'
+ 'console once the test has started.',
);
- argParser.addFlag('merge-coverage',
+ argParser.addFlag(
+ 'coverage',
+ defaultsTo: false,
+ negatable: false,
+ help: 'Whether to collect coverage information.',
+ );
+ argParser.addFlag(
+ 'merge-coverage',
defaultsTo: false,
negatable: false,
help: 'Whether to merge coverage data with "coverage/lcov.base.info".\n'
- 'Implies collecting coverage data. (Requires lcov)'
+ 'Implies collecting coverage data. (Requires lcov)',
);
- argParser.addFlag('ipv6',
- negatable: false,
- hide: true,
- help: 'Whether to use IPv6 for the test harness server socket.'
+ argParser.addFlag(
+ 'ipv6',
+ negatable: false,
+ hide: true,
+ help: 'Whether to use IPv6 for the test harness server socket.',
);
- argParser.addOption('coverage-path',
+ argParser.addOption(
+ 'coverage-path',
defaultsTo: 'coverage/lcov.info',
- help: 'Where to store coverage information (if coverage is enabled).'
+ help: 'Where to store coverage information (if coverage is enabled).',
);
- argParser.addFlag('machine',
- hide: !verboseHelp,
- negatable: false,
- help: 'Handle machine structured JSON command input\n'
- 'and provide output and progress in machine friendly format.');
- argParser.addFlag('preview-dart-2',
- hide: !verboseHelp,
- help: 'Preview Dart 2.0 functionality.');
+ argParser.addFlag(
+ 'machine',
+ hide: !verboseHelp,
+ negatable: false,
+ help: 'Handle machine structured JSON command input\n'
+ 'and provide output and progress in machine friendly format.',
+ );
+ argParser.addFlag(
+ 'preview-dart-2',
+ hide: !verboseHelp,
+ help: 'Preview Dart 2.0 functionality.',
+ );
+ argParser.addFlag(
+ 'track-widget-creation',
+ negatable: false,
+ hide: !verboseHelp,
+ help: 'Track widget creation locations.\n'
+ 'This enables testing of features such as the widget inspector.',
+ );
}
@override
@@ -200,17 +218,19 @@
Cache.releaseLockEarly();
- final int result = await runTests(files,
- workDir: workDir,
- names: names,
- plainNames: plainNames,
- watcher: watcher,
- enableObservatory: collector != null || startPaused,
- startPaused: startPaused,
- ipv6: argResults['ipv6'],
- machine: machine,
- previewDart2: argResults['preview-dart-2'],
- );
+ final int result = await runTests(
+ files,
+ workDir: workDir,
+ names: names,
+ plainNames: plainNames,
+ watcher: watcher,
+ enableObservatory: collector != null || startPaused,
+ startPaused: startPaused,
+ ipv6: argResults['ipv6'],
+ machine: machine,
+ previewDart2: argResults['preview-dart-2'],
+ trackWidgetCreation: argResults['track-widget-creation'],
+ );
if (collector != null) {
if (!await _collectCoverageData(collector, mergeCoverageData: argResults['merge-coverage']))
diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart
index 7aee95d..81d9ba3 100644
--- a/packages/flutter_tools/lib/src/test/flutter_platform.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart
@@ -61,6 +61,7 @@
bool previewDart2: false,
int port: 0,
String precompiledDillPath,
+ bool trackWidgetCreation: false,
int observatoryPort,
InternetAddressType serverType: InternetAddressType.IP_V4,
}) {
@@ -79,6 +80,7 @@
previewDart2: previewDart2,
port: port,
precompiledDillPath: precompiledDillPath,
+ trackWidgetCreation: trackWidgetCreation,
),
);
}
@@ -97,7 +99,7 @@
// This class is a wrapper around compiler that allows multiple isolates to
// enqueue compilation requests, but ensures only one compilation at a time.
class _Compiler {
- _Compiler() {
+ _Compiler(bool trackWidgetCreation) {
// Compiler maintains and updates single incremental dill file.
// Incremental compilation requests done for each test copy that file away
// for independent execution.
@@ -135,7 +137,8 @@
compiler = new ResidentCompiler(
artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath),
- packagesPath: PackageMap.globalPackagesPath);
+ packagesPath: PackageMap.globalPackagesPath,
+ trackWidgetCreation: trackWidgetCreation);
}
final StreamController<_CompilationRequest> compilerController =
@@ -162,6 +165,7 @@
this.previewDart2,
this.port,
this.precompiledDillPath,
+ this.trackWidgetCreation,
}) : assert(shellPath != null);
final String shellPath;
@@ -174,6 +178,7 @@
final bool previewDart2;
final int port;
final String precompiledDillPath;
+ final bool trackWidgetCreation;
_Compiler compiler;
@@ -269,7 +274,7 @@
if (previewDart2 && precompiledDillPath == null) {
// Lazily instantiate compiler so it is built only if it is actually used.
- compiler ??= new _Compiler();
+ compiler ??= new _Compiler(trackWidgetCreation);
mainDart = await compiler.compile(mainDart);
if (mainDart == null) {
diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart
index 79d78ec..d79573d 100644
--- a/packages/flutter_tools/lib/src/test/runner.dart
+++ b/packages/flutter_tools/lib/src/test/runner.dart
@@ -4,6 +4,7 @@
import 'dart:async';
+import 'package:args/command_runner.dart';
// ignore: implementation_imports
import 'package:test/src/executable.dart' as test;
@@ -29,8 +30,16 @@
bool ipv6: false,
bool machine: false,
bool previewDart2: false,
+ bool trackWidgetCreation: false,
TestWatcher watcher,
}) async {
+ if (trackWidgetCreation && !previewDart2) {
+ throw new UsageException(
+ '--track-widget-creation is valid only when --preview-dart-2 is specified.',
+ null,
+ );
+ }
+
// Compute the command-line arguments for package:test.
final List<String> testArgs = <String>[];
if (!terminal.supportsColor)
@@ -77,6 +86,7 @@
startPaused: startPaused,
serverType: serverType,
previewDart2: previewDart2,
+ trackWidgetCreation: trackWidgetCreation,
);
// Make the global packages path absolute.