Unverified Commit 046c6d5f authored by Jacob Richman's avatar Jacob Richman Committed by GitHub

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.
parent a9e27811
...@@ -144,6 +144,8 @@ class WidgetInspectorService { ...@@ -144,6 +144,8 @@ class WidgetInspectorService {
final Map<Object, String> _objectToId = new Map<Object, String>.identity(); final Map<Object, String> _objectToId = new Map<Object, String>.identity();
int _nextId = 0; int _nextId = 0;
List<String> _pubRootDirectories;
/// Clear all InspectorService object references. /// Clear all InspectorService object references.
/// ///
/// Use this method only for testing to ensure that object references from one /// Use this method only for testing to ensure that object references from one
...@@ -258,6 +260,17 @@ class WidgetInspectorService { ...@@ -258,6 +260,17 @@ class WidgetInspectorService {
_decrementReferenceCount(referenceData); _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 /// Set the [WidgetInspector] selection to the object matching the specified
/// id if the object is valid object to set as the inspector selection. /// id if the object is valid object to set as the inspector selection.
/// ///
...@@ -359,10 +372,26 @@ class WidgetInspectorService { ...@@ -359,10 +372,26 @@ class WidgetInspectorService {
final _Location creationLocation = _getCreationLocation(value); final _Location creationLocation = _getCreationLocation(value);
if (creationLocation != null) { if (creationLocation != null) {
json['creationLocation'] = creationLocation.toJsonMap(); json['creationLocation'] = creationLocation.toJsonMap();
if (_isLocalCreationLocation(creationLocation)) {
json['createdByLocalProject'] = true;
}
} }
return json; 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) { String _serialize(DiagnosticsNode node, String groupName) {
return JSON.encode(_nodeToJson(node, groupName)); return JSON.encode(_nodeToJson(node, groupName));
} }
......
...@@ -498,4 +498,147 @@ void main() { ...@@ -498,4 +498,147 @@ void main() {
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>()); 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.
} }
...@@ -23,54 +23,72 @@ class TestCommand extends FlutterCommand { ...@@ -23,54 +23,72 @@ class TestCommand extends FlutterCommand {
TestCommand({ bool verboseHelp: false }) { TestCommand({ bool verboseHelp: false }) {
requiresPubspecYaml(); requiresPubspecYaml();
usesPubOption(); usesPubOption();
argParser.addOption('name', argParser.addOption(
'name',
help: 'A regular expression matching substrings of the names of tests to run.', help: 'A regular expression matching substrings of the names of tests to run.',
valueHelp: 'regexp', valueHelp: 'regexp',
allowMultiple: true, allowMultiple: true,
splitCommas: false, splitCommas: false,
); );
argParser.addOption('plain-name', argParser.addOption(
'plain-name',
help: 'A plain-text substring of the names of tests to run.', help: 'A plain-text substring of the names of tests to run.',
valueHelp: 'substring', valueHelp: 'substring',
allowMultiple: true, allowMultiple: true,
splitCommas: false, splitCommas: false,
); );
argParser.addFlag('start-paused', argParser.addFlag(
defaultsTo: false, 'start-paused',
negatable: false, defaultsTo: false,
help: 'Start in a paused mode and wait for a debugger to connect.\n' negatable: false,
'You must specify a single test file to run, explicitly.\n' help: 'Start in a paused mode and wait for a debugger to connect.\n'
'Instructions for connecting with a debugger and printed to the\n' 'You must specify a single test file to run, explicitly.\n'
'console once the test has started.' 'Instructions for connecting with a debugger and printed to the\n'
'console once the test has started.',
); );
argParser.addFlag('coverage', argParser.addFlag(
'coverage',
defaultsTo: false, defaultsTo: false,
negatable: false, negatable: false,
help: 'Whether to collect coverage information.' help: 'Whether to collect coverage information.',
); );
argParser.addFlag('merge-coverage', argParser.addFlag(
'merge-coverage',
defaultsTo: false, defaultsTo: false,
negatable: false, negatable: false,
help: 'Whether to merge coverage data with "coverage/lcov.base.info".\n' 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', argParser.addFlag(
negatable: false, 'ipv6',
hide: true, negatable: false,
help: 'Whether to use IPv6 for the test harness server socket.' 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', 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(
'track-widget-creation',
negatable: false,
hide: !verboseHelp,
help: 'Track widget creation locations.\n'
'This enables testing of features such as the widget inspector.',
); );
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.');
} }
@override @override
...@@ -200,17 +218,19 @@ class TestCommand extends FlutterCommand { ...@@ -200,17 +218,19 @@ class TestCommand extends FlutterCommand {
Cache.releaseLockEarly(); Cache.releaseLockEarly();
final int result = await runTests(files, final int result = await runTests(
workDir: workDir, files,
names: names, workDir: workDir,
plainNames: plainNames, names: names,
watcher: watcher, plainNames: plainNames,
enableObservatory: collector != null || startPaused, watcher: watcher,
startPaused: startPaused, enableObservatory: collector != null || startPaused,
ipv6: argResults['ipv6'], startPaused: startPaused,
machine: machine, ipv6: argResults['ipv6'],
previewDart2: argResults['preview-dart-2'], machine: machine,
); previewDart2: argResults['preview-dart-2'],
trackWidgetCreation: argResults['track-widget-creation'],
);
if (collector != null) { if (collector != null) {
if (!await _collectCoverageData(collector, mergeCoverageData: argResults['merge-coverage'])) if (!await _collectCoverageData(collector, mergeCoverageData: argResults['merge-coverage']))
......
...@@ -61,6 +61,7 @@ void installHook({ ...@@ -61,6 +61,7 @@ void installHook({
bool previewDart2: false, bool previewDart2: false,
int port: 0, int port: 0,
String precompiledDillPath, String precompiledDillPath,
bool trackWidgetCreation: false,
int observatoryPort, int observatoryPort,
InternetAddressType serverType: InternetAddressType.IP_V4, InternetAddressType serverType: InternetAddressType.IP_V4,
}) { }) {
...@@ -79,6 +80,7 @@ void installHook({ ...@@ -79,6 +80,7 @@ void installHook({
previewDart2: previewDart2, previewDart2: previewDart2,
port: port, port: port,
precompiledDillPath: precompiledDillPath, precompiledDillPath: precompiledDillPath,
trackWidgetCreation: trackWidgetCreation,
), ),
); );
} }
...@@ -97,7 +99,7 @@ class _CompilationRequest { ...@@ -97,7 +99,7 @@ class _CompilationRequest {
// This class is a wrapper around compiler that allows multiple isolates to // This class is a wrapper around compiler that allows multiple isolates to
// enqueue compilation requests, but ensures only one compilation at a time. // enqueue compilation requests, but ensures only one compilation at a time.
class _Compiler { class _Compiler {
_Compiler() { _Compiler(bool trackWidgetCreation) {
// Compiler maintains and updates single incremental dill file. // Compiler maintains and updates single incremental dill file.
// Incremental compilation requests done for each test copy that file away // Incremental compilation requests done for each test copy that file away
// for independent execution. // for independent execution.
...@@ -135,7 +137,8 @@ class _Compiler { ...@@ -135,7 +137,8 @@ class _Compiler {
compiler = new ResidentCompiler( compiler = new ResidentCompiler(
artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath), artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath),
packagesPath: PackageMap.globalPackagesPath); packagesPath: PackageMap.globalPackagesPath,
trackWidgetCreation: trackWidgetCreation);
} }
final StreamController<_CompilationRequest> compilerController = final StreamController<_CompilationRequest> compilerController =
...@@ -162,6 +165,7 @@ class _FlutterPlatform extends PlatformPlugin { ...@@ -162,6 +165,7 @@ class _FlutterPlatform extends PlatformPlugin {
this.previewDart2, this.previewDart2,
this.port, this.port,
this.precompiledDillPath, this.precompiledDillPath,
this.trackWidgetCreation,
}) : assert(shellPath != null); }) : assert(shellPath != null);
final String shellPath; final String shellPath;
...@@ -174,6 +178,7 @@ class _FlutterPlatform extends PlatformPlugin { ...@@ -174,6 +178,7 @@ class _FlutterPlatform extends PlatformPlugin {
final bool previewDart2; final bool previewDart2;
final int port; final int port;
final String precompiledDillPath; final String precompiledDillPath;
final bool trackWidgetCreation;
_Compiler compiler; _Compiler compiler;
...@@ -269,7 +274,7 @@ class _FlutterPlatform extends PlatformPlugin { ...@@ -269,7 +274,7 @@ class _FlutterPlatform extends PlatformPlugin {
if (previewDart2 && precompiledDillPath == null) { if (previewDart2 && precompiledDillPath == null) {
// Lazily instantiate compiler so it is built only if it is actually used. // 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); mainDart = await compiler.compile(mainDart);
if (mainDart == null) { if (mainDart == null) {
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'package:args/command_runner.dart';
// ignore: implementation_imports // ignore: implementation_imports
import 'package:test/src/executable.dart' as test; import 'package:test/src/executable.dart' as test;
...@@ -29,8 +30,16 @@ Future<int> runTests( ...@@ -29,8 +30,16 @@ Future<int> runTests(
bool ipv6: false, bool ipv6: false,
bool machine: false, bool machine: false,
bool previewDart2: false, bool previewDart2: false,
bool trackWidgetCreation: false,
TestWatcher watcher, TestWatcher watcher,
}) async { }) 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. // Compute the command-line arguments for package:test.
final List<String> testArgs = <String>[]; final List<String> testArgs = <String>[];
if (!terminal.supportsColor) if (!terminal.supportsColor)
...@@ -77,6 +86,7 @@ Future<int> runTests( ...@@ -77,6 +86,7 @@ Future<int> runTests(
startPaused: startPaused, startPaused: startPaused,
serverType: serverType, serverType: serverType,
previewDart2: previewDart2, previewDart2: previewDart2,
trackWidgetCreation: trackWidgetCreation,
); );
// Make the global packages path absolute. // Make the global packages path absolute.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment