add tool to check that deferred libraries contain the expected code

BUG=
R=sigmund@google.com

Review URL: https://codereview.chromium.org//1425953002 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6f4f16..f8e71fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,13 @@
 # Changelog
 
+## 0.2.2
+- Added `deferred_libary_check` tool
+
 ## 0.2.1
 - Merged `verify_deps` tool into `debug_info` tool
 
 ## 0.2.0
-- Added AllInfoJsonCodec
+- Added `AllInfoJsonCodec`
 - Added `verify_deps` tool
 
 ## 0.1.0
@@ -16,8 +19,7 @@
 - Added executable names
 
 ## 0.0.2
-- Add support for ConstantInfo
+- Add support for `ConstantInfo`
 
 ## 0.0.1
-
 - Initial version
diff --git a/README.md b/README.md
index 811877e..50a2692 100644
--- a/README.md
+++ b/README.md
@@ -69,6 +69,11 @@
     in many ways (e.g. to tally together all libraries that belong to a package,
     or all libraries that match certain name pattern).
 
+  * [`deferred_library_check`][deferred_lib]: a tool that verifies that code
+    was split into deferred parts as expected. This tool takes a specification
+    of the expected layout of code into deferred parts, and checks that the
+    output from `dart2js` meets the specification.
+
   * [`function_size_analysis`][function_analysis]: a tool that shows how much
     code was attributed to each function. This tool also uses dependency
     information to compute dominance and reachability data. This information can
@@ -188,6 +193,47 @@
 size. Currently dart2js's `--dump-info` is not complete, so numbers for
 bootstrapping code and lazy static initializers are missing.
 
+### Deferred library verification
+
+This tool checks that the output from dart2js meets a given specification,
+given in a YAML file. It can be run as follows:
+
+```bash
+pub global activate dart2js_info # only needed once
+dart2js_info_deferred_library_check out.js.info.json manifest.yaml
+```
+
+The format of the YAML file is:
+
+```yaml
+main:
+  packages:
+    - some_package
+    - other_package
+
+foo:
+  packages:
+    - foo
+    - bar
+
+baz:
+  packages:
+    - baz
+    - quux
+```
+
+The YAML file consists of a list of declarations, one for each deferred
+part expected in the output. At least one of these parts must be named
+"main"; this is the main part that contains the program entrypoint. Each
+top-level part contains a list of package names that are expected to be
+contained in that part. Any package that is not explicitly listed is
+expected to be in the main part. For instance, in the example YAML above
+the part named "baz" is expected to contain the packages "baz" and "quux".
+
+The names for parts given in the specification YAML file (besides "main")
+are arbitrary and just used for reporting when the output does not meet the
+specification.
+
 ### Function size analysis tool
 
 This command-line tool presents how much each function contributes to the total
@@ -268,6 +314,7 @@
 [tracker]: https://github.com/dart-lang/dart2js_info/issues
 [code_deps]: https://github.com/dart-lang/dart2js_info/blob/master/bin/code_deps.dart
 [lib_split]: https://github.com/dart-lang/dart2js_info/blob/master/bin/library_size_split.dart
+[lib_split]: https://github.com/dart-lang/dart2js_info/blob/master/bin/deferred_library_check.dart
 [coverage]: https://github.com/dart-lang/dart2js_info/blob/master/bin/coverage_log_server.dart
 [live]: https://github.com/dart-lang/dart2js_info/blob/master/bin/live_code_size_analysis.dart
 [function_analysis]: https://github.com/dart-lang/dart2js_info/blob/master/bin/function_size_analysis.dart
diff --git a/bin/deferred_library_check.dart b/bin/deferred_library_check.dart
new file mode 100644
index 0000000..0c092c5
--- /dev/null
+++ b/bin/deferred_library_check.dart
@@ -0,0 +1,144 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A command-line tool that verifies that deferred libraries split the code as
+/// expected.
+/// This tool checks that the output from dart2js meets a given specification,
+/// given in a YAML file. The format of the YAML file is:
+///
+///     main:
+///       packages:
+///         - some_package
+///         - other_package
+///
+///     foo:
+///       packages:
+///         - foo
+///         - bar
+///
+///     baz:
+///       packages:
+///         - baz
+///         - quux
+///
+/// The YAML file consists of a list of declarations, one for each deferred
+/// part expected in the output. At least one of these parts must be named
+/// "main"; this is the main part that contains the program entrypoint. Each
+/// top-level part contains a list of package names that are expected to be
+/// contained in that part. Any package that is not explicitly listed is
+/// expected to be in the main part. For instance, in the example YAML above
+/// the part named "baz" is expected to contain the packages "baz" and "quux".
+///
+/// The names for parts given in the specification YAML file (besides "main")
+/// are arbitrary and just used for reporting when the output does not meet the
+/// specification.
+library dart2js_info.bin.deferred_library_check;
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:dart2js_info/info.dart';
+import 'package:quiver/collection.dart';
+import 'package:yaml/yaml.dart';
+
+Future main(List<String> args) async {
+  if (args.length < 2) {
+    usage();
+    exit(1);
+  }
+  var info = await infoFromFile(args[0]);
+  var manifest = await manifestFromFile(args[1]);
+
+  // For each part in the manifest, record the expected "packages" for that
+  // part.
+  var packages = <String, String>{};
+  for (var part in manifest.keys) {
+    for (var package in manifest[part]['packages']) {
+      if (packages.containsKey(package)) {
+        print('You cannot specify that package "$package" maps to both parts '
+            '"$part" and "${packages[package]}".');
+        exit(1);
+      }
+      packages[package] = part;
+    }
+  }
+
+  var guessedPartMapping = new BiMap<String, String>();
+  guessedPartMapping['main'] = 'main';
+
+  bool anyFailed = false;
+
+  checkInfo(BasicInfo info) {
+    var lib = getLibraryOf(info);
+    if (lib != null && isPackageUri(lib.uri)) {
+      var packageName = getPackageName(lib.uri);
+      var outputUnitName = info.outputUnit.name;
+      var expectedPart;
+      if (packages.containsKey(packageName)) {
+        expectedPart = packages[packageName];
+      } else {
+        expectedPart = 'main';
+      }
+      var expectedOutputUnit = guessedPartMapping[expectedPart];
+      if (expectedOutputUnit == null) {
+        guessedPartMapping[expectedPart] = outputUnitName;
+      } else {
+        if (expectedOutputUnit != outputUnitName) {
+          // TODO(het): add options for how to treat unspecified packages
+          if (!packages.containsKey(packageName)) {
+            print('"${info.name}" from package "$packageName" was not declared '
+                'to be in an explicit part but was not in the main part');
+          } else {
+            var actualPart = guessedPartMapping.inverse[outputUnitName];
+            print('"${info.name}" from package "$packageName" was specified to '
+                'be in part $expectedPart but is in part $actualPart');
+          }
+          anyFailed = true;
+        }
+      }
+    }
+  }
+
+  info.functions.forEach(checkInfo);
+  info.fields.forEach(checkInfo);
+  if (anyFailed) {
+    print('The dart2js output did not meet the specification.');
+  } else {
+    print('The dart2js output meets the specification');
+  }
+}
+
+LibraryInfo getLibraryOf(Info info) {
+  var current = info;
+  while (current is! LibraryInfo) {
+    if (current == null) {
+      return null;
+    }
+    current = current.parent;
+  }
+  return current;
+}
+
+bool isPackageUri(Uri uri) => uri.scheme == 'package';
+
+String getPackageName(Uri uri) {
+  assert(isPackageUri(uri));
+  return uri.pathSegments.first;
+}
+
+Future<AllInfo> infoFromFile(String fileName) async {
+  var file = await new File(fileName).readAsString();
+  return new AllInfoJsonCodec().decode(JSON.decode(file));
+}
+
+Future manifestFromFile(String fileName) async {
+  var file = await new File(fileName).readAsString();
+  return loadYaml(file);
+}
+
+void usage() {
+  print('''
+usage: dart2js_info_deferred_library_check dump.info.json manifest.yaml''');
+}
diff --git a/lib/json_info_codec.dart b/lib/json_info_codec.dart
index 1293d30..6ee7634 100644
--- a/lib/json_info_codec.dart
+++ b/lib/json_info_codec.dart
@@ -48,11 +48,21 @@
       result.dependencies[idMap[k]] = deps.map((d) => idMap[d]).toList();
     });
 
+    result.outputUnits.addAll(json['outputUnits'].map(parseOutputUnit));
+
     result.program = parseProgram(json['program']);
     // todo: version, etc
     return result;
   }
 
+  OutputUnitInfo parseOutputUnit(Map json) {
+    OutputUnitInfo result = parseId(json['id']);
+    result
+      ..name = json['name']
+      ..size = json['size'];
+    return result;
+  }
+
   LibraryInfo parseLibrary(Map json) {
     LibraryInfo result = parseId(json['id']);
     result
diff --git a/pubspec.yaml b/pubspec.yaml
index 81b8c2d..0b4ae5c 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: dart2js_info
-version: 0.2.1
+version: 0.2.2
 description: >
   Libraries and tools to process data produced when running dart2js with
   --dump-info.
@@ -12,6 +12,7 @@
   shelf: ^0.6.1+2
   yaml: ^2.1.0
   charcode: ^1.1.0
+  quiver: ^0.21.0
 
 environment:
   sdk: '>=1.11.0 <2.0.0'
@@ -22,6 +23,7 @@
   dart2js_info_code_deps:               code_deps
   dart2js_info_coverage_log_server:     coverage_log_server
   dart2js_info_debug_info:              debug_info
+  dart2js_info_deferred_library_check:  deferred_library_check
   dart2js_info_function_size_analysis:  function_size_analysis
   dart2js_info_library_size_split:      library_size_split
   dart2js_info_live_code_size_analysis: live_code_size_analysis