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.