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 {
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 @@ class WidgetInspectorService {
_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 @@ class WidgetInspectorService {
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));
}
......
......@@ -498,4 +498,147 @@ void main() {
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 {
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(
'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(
'coverage',
defaultsTo: 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,
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(
'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
......@@ -200,17 +218,19 @@ class TestCommand extends FlutterCommand {
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']))
......
......@@ -61,6 +61,7 @@ void installHook({
bool previewDart2: false,
int port: 0,
String precompiledDillPath,
bool trackWidgetCreation: false,
int observatoryPort,
InternetAddressType serverType: InternetAddressType.IP_V4,
}) {
......@@ -79,6 +80,7 @@ void installHook({
previewDart2: previewDart2,
port: port,
precompiledDillPath: precompiledDillPath,
trackWidgetCreation: trackWidgetCreation,
),
);
}
......@@ -97,7 +99,7 @@ class _CompilationRequest {
// 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 @@ class _Compiler {
compiler = new ResidentCompiler(
artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath),
packagesPath: PackageMap.globalPackagesPath);
packagesPath: PackageMap.globalPackagesPath,
trackWidgetCreation: trackWidgetCreation);
}
final StreamController<_CompilationRequest> compilerController =
......@@ -162,6 +165,7 @@ class _FlutterPlatform extends PlatformPlugin {
this.previewDart2,
this.port,
this.precompiledDillPath,
this.trackWidgetCreation,
}) : assert(shellPath != null);
final String shellPath;
......@@ -174,6 +178,7 @@ class _FlutterPlatform extends PlatformPlugin {
final bool previewDart2;
final int port;
final String precompiledDillPath;
final bool trackWidgetCreation;
_Compiler compiler;
......@@ -269,7 +274,7 @@ class _FlutterPlatform extends PlatformPlugin {
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) {
......
......@@ -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 @@ Future<int> runTests(
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 @@ Future<int> runTests(
startPaused: startPaused,
serverType: serverType,
previewDart2: previewDart2,
trackWidgetCreation: trackWidgetCreation,
);
// 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