Use collection expansion to add frame info (#1441)

Expand a map with the frame info into the full message with `...`. This
keeps the structure more clear and makes it obvious that no other side
effects happen.

Refactor the method finding the frame info. Use `firstWhereOrNull` over
a for loop to find the frame matching the suite path. Separate out two
returns to make it clear that there are always `null` values for some
keys in the map, but not others.
diff --git a/pkgs/test_core/lib/src/runner/reporter/json.dart b/pkgs/test_core/lib/src/runner/reporter/json.dart
index 11710da..f31f185 100644
--- a/pkgs/test_core/lib/src/runner/reporter/json.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/json.dart
@@ -8,24 +8,23 @@
 import 'dart:convert';
 import 'dart:io' show pid;
 
+import 'package:collection/collection.dart';
 import 'package:path/path.dart' as p;
-
 import 'package:stack_trace/stack_trace.dart';
 import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
-import 'package:test_api/src/backend/group_entry.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
-import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
 import 'package:test_api/src/frontend/expect.dart'; // ignore: implementation_imports
 
-import '../runner_suite.dart';
-import '../suite.dart';
 import '../configuration.dart';
 import '../engine.dart';
 import '../load_suite.dart';
 import '../reporter.dart';
+import '../runner_suite.dart';
+import '../suite.dart';
 import '../version.dart';
 
 /// A reporter that prints machine-readable JSON-formatted test results.
@@ -134,18 +133,15 @@
     var id = _nextID++;
     _liveTestIDs[liveTest] = id;
     _emit('testStart', {
-      'test': _addFrameInfo(
-          suiteConfig,
-          {
-            'id': id,
-            'name': liveTest.test.name,
-            'suiteID': suiteID,
-            'groupIDs': groupIDs,
-            'metadata': _serializeMetadata(suiteConfig, liveTest.test.metadata)
-          },
-          liveTest.test,
-          liveTest.suite.platform.runtime,
-          liveTest.suite.path)
+      'test': {
+        'id': id,
+        'name': liveTest.test.name,
+        'suiteID': suiteID,
+        'groupIDs': groupIDs,
+        'metadata': _serializeMetadata(suiteConfig, liveTest.test.metadata),
+        ..._frameInfo(suiteConfig, liveTest.test.trace,
+            liveTest.suite.platform.runtime, liveTest.suite.path),
+      }
     });
 
     // Convert the future to a stream so that the subscription can be paused or
@@ -222,19 +218,16 @@
 
       var suiteConfig = _configFor(suite);
       _emit('group', {
-        'group': _addFrameInfo(
-            suiteConfig,
-            {
-              'id': id,
-              'suiteID': _idForSuite(suite),
-              'parentID': parentID,
-              'name': group.name,
-              'metadata': _serializeMetadata(suiteConfig, group.metadata),
-              'testCount': group.testCount
-            },
-            group,
-            suite.platform.runtime,
-            suite.path)
+        'group': {
+          'id': id,
+          'suiteID': _idForSuite(suite),
+          'parentID': parentID,
+          'name': group.name,
+          'metadata': _serializeMetadata(suiteConfig, group.metadata),
+          'testCount': group.testCount,
+          ..._frameInfo(
+              suiteConfig, group.trace, suite.platform.runtime, suite.path)
+        }
       });
       parentID = id;
       return id;
@@ -295,39 +288,32 @@
     _sink.writeln(jsonEncode(attributes));
   }
 
-  /// Modifies [map] to include line, column, and URL information from the first
-  /// frame of [entry.trace], as well as the first line in the original file.
+  /// Returns a map with the line, column, and URL information for the first
+  /// frame of [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,
-      String suitePath) {
-    var frame = entry.trace?.frames?.first;
-    Frame /*?*/ rootFrame;
-    for (var frame in entry.trace?.frames ?? <Frame>[]) {
-      if (frame.uri.scheme == 'file' &&
-          frame.uri.toFilePath() == p.absolute(suitePath)) {
-        rootFrame = frame;
-        break;
+  /// If javascript traces are enabled and the test is on a javascript platform,
+  /// or if the [trace] is null or empty, then the line, column, and url will
+  /// all be `null`.
+  Map<String, dynamic> _frameInfo(SuiteConfiguration suiteConfig,
+      Trace /*?*/ trace, Runtime runtime, String suitePath) {
+    var absoluteSuitePath = p.absolute(suitePath);
+    var frame = trace?.frames?.first;
+    if (frame == null || (suiteConfig.jsTrace && runtime.isJS)) {
+      return {'line': null, 'column': null, 'url': null};
+    }
+
+    var rootFrame = trace?.frames?.firstWhereOrNull((frame) =>
+        frame.uri.scheme == 'file' &&
+        frame.uri.toFilePath() == absoluteSuitePath);
+    return {
+      'line': frame.line,
+      'column': frame.column,
+      'url': frame.uri.toString(),
+      if (rootFrame != null && rootFrame != frame) ...{
+        'root_line': rootFrame.line,
+        'root_column': rootFrame.column,
+        'root_url': rootFrame.uri.toString(),
       }
-    }
-
-    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;
+    };
   }
 }