Report original test location in the json_reporter if different (#853)

* add root_line, root_column, and root_package_url to the json reporter

* update docs, pubspec, changelog
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6623494..2548d8d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 0.12.40
+
+* Added some new optional fields to the json reporter, `root_line`,
+  `root_column`, and `root_url`. These will be present if `url` is not the same
+  as the suite url, and will represent the location in the original test suite
+  from which the call to `test` originated.
+
 ## 0.12.39
 
 * Change the default reporter and color defaults to be based on
diff --git a/doc/json_reporter.md b/doc/json_reporter.md
index 9c54fe7..a5a461a 100644
--- a/doc/json_reporter.md
+++ b/doc/json_reporter.md
@@ -343,6 +343,23 @@
   // The URL for the file in which the test was defined, or `null`.
   String url;
 
+  // The (1-based) line in the original test suite from which the test
+  // originated.
+  //
+  // Will only be present if `root_url` is different from `url`.
+  int root_line;
+
+  // The (1-based) line on in the original test suite from which the test
+  // originated.
+  //
+  // Will only be present if `root_url` is different from `url`.
+  int root_column;
+
+  // The URL for the original test suite in which the test was defined.
+  //
+  // Will only be present if different from `url`.
+  String root_url;
+
   // This field is deprecated and should not be used.
   Metadata metadata;
 }
diff --git a/lib/src/runner/reporter/json.dart b/lib/src/runner/reporter/json.dart
index ec27478..4c0f160 100644
--- a/lib/src/runner/reporter/json.dart
+++ b/lib/src/runner/reporter/json.dart
@@ -5,6 +5,8 @@
 import 'dart:async';
 import 'dart:convert';
 
+import 'package:path/path.dart' as p;
+
 import '../../backend/group.dart';
 import '../../backend/group_entry.dart';
 import '../../backend/live_test.dart';
@@ -131,7 +133,8 @@
             "metadata": _serializeMetadata(suiteConfig, liveTest.test.metadata)
           },
           liveTest.test,
-          liveTest.suite.platform.runtime)
+          liveTest.suite.platform.runtime,
+          liveTest.suite.path)
     });
 
     /// Convert the future to a stream so that the subscription can be paused or
@@ -218,7 +221,8 @@
               "testCount": group.testCount
             },
             group,
-            suite.platform.runtime)
+            suite.platform.runtime,
+            suite.path)
       });
       parentID = id;
       return id;
@@ -280,17 +284,32 @@
   }
 
   /// Modifies [map] to include line, column, and URL information from the first
-  /// frame of [entry.trace].
+  /// frame of [entry.trace], as well as the first line in the original file.
   ///
   /// Returns [map].
-  Map<String, dynamic> _addFrameInfo(SuiteConfiguration suiteConfig,
-      Map<String, dynamic> map, GroupEntry entry, Runtime runtime) {
+  Map<String, dynamic> _addFrameInfo(
+      SuiteConfiguration suiteConfig,
+      Map<String, dynamic> map,
+      GroupEntry entry,
+      Runtime runtime,
+      String suitePath) {
     var frame = entry.trace?.frames?.first;
-    if (suiteConfig.jsTrace && runtime.isJS) frame = null;
+    var rootFrame = entry.trace?.frames?.firstWhere(
+        (frame) => frame.uri.path == p.absolute(suitePath),
+        orElse: () => null);
+    if (suiteConfig.jsTrace && runtime.isJS) {
+      frame = null;
+      rootFrame = null;
+    }
 
     map["line"] = frame?.line;
     map["column"] = frame?.column;
     map["url"] = frame?.uri?.toString();
+    if (rootFrame != null && rootFrame != frame) {
+      map["root_line"] = rootFrame.line;
+      map["root_column"] = rootFrame.column;
+      map["root_url"] = rootFrame.uri.toString();
+    }
     return map;
   }
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index 5719dbe..7068ad3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: test
-version: 0.12.39
+version: 0.12.40
 author: Dart Team <misc@dartlang.org>
 description: A library for writing dart unit tests.
 homepage: https://github.com/dart-lang/test
diff --git a/test/common.dart b/test/common.dart
new file mode 100644
index 0000000..cde6147
--- /dev/null
+++ b/test/common.dart
@@ -0,0 +1,3 @@
+import 'package:test/test.dart';
+
+myTest(String name, Function testFn) => test(name, testFn);
diff --git a/test/runner/json_reporter_test.dart b/test/runner/json_reporter_test.dart
index 2dab958..2127357 100644
--- a/test/runner/json_reporter_test.dart
+++ b/test/runner/json_reporter_test.dart
@@ -492,6 +492,37 @@
         "chrome"
       ]);
     }, tags: ["chrome"], skip: "Broken by sdk#29693.");
+
+    test("the root suite if applicable", () {
+      return _expectReport("""
+      customTest('success 1', () {});
+      test('success 2', () {});
+    """, [
+        _start,
+        _allSuites(),
+        _suite(0),
+        _testStart(1, "loading test.dart", groupIDs: []),
+        _testDone(1, hidden: true),
+        _group(2, testCount: 2),
+        _testStart(3, "success 1",
+            line: 3,
+            column: 50,
+            url: p.toUri(p.join(d.sandbox, "common.dart")).toString(),
+            root_column: 7,
+            root_line: 7,
+            root_url: p.toUri(p.join(d.sandbox, "test.dart")).toString()),
+        _testDone(3),
+        _testStart(4, "success 2", line: 8, column: 7),
+        _testDone(4),
+        _done()
+      ], externalLibraries: {
+        'common.dart': """
+import 'package:test/test.dart';
+
+void customTest(String name, Function testFn) => test(name, testFn);
+""",
+      });
+    });
   });
 
   test(
@@ -519,19 +550,29 @@
 
 /// Asserts that the tests defined by [tests] produce the JSON events in
 /// [expected].
+///
+/// If [externalLibraries] are provided it should be a map of relative file
+/// paths to contents. All libraries will be added as imports to the test, and
+/// files will be created for them.
 Future _expectReport(String tests, List<Map> expected,
-    {List<String> args}) async {
-  d.file("test.dart", """
-    import 'dart:async';
+    {List<String> args, Map<String, String> externalLibraries}) async {
+  args ??= [];
+  externalLibraries ??= {};
+  var testContent = new StringBuffer("""
+import 'dart:async';
 
-    import 'package:test/test.dart';
+import 'package:test/test.dart';
 
-    void main() {
-$tests
-    }
-  """).create();
+""");
+  for (var entry in externalLibraries.entries) {
+    testContent.writeln("import '${entry.key}';");
+    await d.file(entry.key, entry.value).create();
+  }
+  testContent..writeln("void main() {")..writeln(tests)..writeln("}");
 
-  var test = await runTest(["test.dart"]..addAll(args ?? []), reporter: "json");
+  await d.file("test.dart", testContent.toString()).create();
+
+  var test = await runTest(["test.dart"]..addAll(args), reporter: "json");
   await test.shouldExit();
 
   var stdoutLines = await test.stdoutStream().toList();
@@ -622,13 +663,23 @@
 /// reason. If it's a [String], the test is expected to be marked as skipped
 /// with that reason.
 Map _testStart(int id, String name,
-    {int suiteID, Iterable<int> groupIDs, int line, int column, skip}) {
+    {int suiteID,
+    Iterable<int> groupIDs,
+    int line,
+    int column,
+    String url,
+    skip,
+    int root_line,
+    int root_column,
+    String root_url}) {
   if ((line == null) != (column == null)) {
     throw new ArgumentError(
         "line and column must either both be null or both be passed");
   }
 
-  return {
+  url ??=
+      line == null ? null : p.toUri(p.join(d.sandbox, "test.dart")).toString();
+  var expected = {
     "type": "testStart",
     "test": {
       "id": id,
@@ -638,11 +689,20 @@
       "metadata": _metadata(skip: skip),
       "line": line,
       "column": column,
-      "url": line == null
-          ? null
-          : p.toUri(p.join(d.sandbox, "test.dart")).toString()
+      "url": url,
     }
   };
+  var testObj = expected['test'] as Map<String, dynamic>;
+  if (root_line != null) {
+    testObj['root_line'] = root_line;
+  }
+  if (root_column != null) {
+    testObj['root_column'] = root_column;
+  }
+  if (root_url != null) {
+    testObj['root_url'] = root_url;
+  }
+  return expected;
 }
 
 /// Returns the event emitted by the JSON reporter indicating that a test