Improve test coverage for animation.dart (#4718)

We now have 100% coverage of animation.dart and animation_controller.dart.
Also, add some basic tools for working with lcov files. These tools need much
more polish.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dbae460..5163907 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -142,6 +142,23 @@
 If you've never submitted code before, you must add your (or your
 organization's) name and contact info to the [AUTHORS](AUTHORS) file.
 
+Tools for tracking an improving test coverage
+---------------------------------------------
+
+We strive for a high degree of test coverage for the Flutter framework. We use
+Coveralls to [track our test coverage](https://coveralls.io/github/flutter/flutter?branch=master).
+You can download our current coverage data from cloud storage and visualize it
+in Atom as follows:
+
+ * `mkdir packages/flutter/coverage`
+ * Download the latest `lcov.info` file produced by Travis using
+   `curl https://storage.googleapis.com/flutter_infra/flutter/coverage/lcov.info -o packages/flutter/coverage/lcov.info`
+ * Install the [lcov-info](https://atom.io/packages/lcov-info) package for Atom.
+ * Open a file in `packages/flutter/lib` in Atom and type `Ctrl+Alt+C`.
+
+See [issue 4719](https://github.com/flutter/flutter/issues/4719) for ideas about
+how to improve this workflow.
+
 Working on the engine and the framework at the same time
 --------------------------------------------------------
 
diff --git a/dev/tools/coverage.dart b/dev/tools/coverage.dart
new file mode 100644
index 0000000..87f54f3
--- /dev/null
+++ b/dev/tools/coverage.dart
@@ -0,0 +1,64 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Downloads and merges line coverage data files for package:flutter.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:path/path.dart' as path;
+
+const String kBaseLcov = 'packages/flutter/coverage/lcov.base.info';
+const String kTargetLcov = 'packages/flutter/coverage/lcov.info';
+const String kSourceLcov = 'packages/flutter/coverage/lcov.source.info';
+
+Future<int> main(List<String> args) async {
+  if (path.basename(Directory.current.path) == 'tools')
+    Directory.current = Directory.current.parent.parent;
+
+  ProcessResult result = Process.runSync('which', <String>['lcov']);
+  if (result.exitCode != 0) {
+    print('Cannot find lcov. Consider running "apt-get install lcov".\n');
+    return 1;
+  }
+
+  if (!FileSystemEntity.isFileSync(kBaseLcov)) {
+    print(
+      'Cannot find "$kBaseLcov". Consider downloading it from from cloud storage.\n'
+      'https://storage.googleapis.com/flutter_infra/flutter/coverage/lcov.info\n'
+    );
+    return 1;
+  }
+
+  ArgParser argParser = new ArgParser();
+  argParser.addFlag('merge', negatable: false);
+  ArgResults results = argParser.parse(args);
+
+  if (FileSystemEntity.isFileSync(kTargetLcov)) {
+    if (results['merge']) {
+      new File(kTargetLcov).renameSync(kSourceLcov);
+    } else {
+      print('"$kTargetLcov" already exists. Did you want to --merge?\n');
+      return 1;
+    }
+  }
+
+  if (results['merge']) {
+    if (!FileSystemEntity.isFileSync(kSourceLcov)) {
+      print('Cannot merge because "$kSourceLcov" does not exist.\n');
+      return 1;
+    }
+
+    ProcessResult result = Process.runSync('lcov', <String>[
+      '--add-tracefile', kBaseLcov,
+      '--add-tracefile', kSourceLcov,
+      '--output-file', kTargetLcov,
+    ]);
+    return result.exitCode;
+  }
+
+  print('No operation requested. Did you want to --merge?\n');
+  return 0;
+}
diff --git a/packages/flutter/.gitignore b/packages/flutter/.gitignore
index 5c642d9..548f44b 100644
--- a/packages/flutter/.gitignore
+++ b/packages/flutter/.gitignore
@@ -2,3 +2,4 @@
 .pub/
 pubspec.lock
 doc/api/
+coverage/
diff --git a/packages/flutter/test/animation/animation_controller_test.dart b/packages/flutter/test/animation/animation_controller_test.dart
index 247ffde..9011253 100644
--- a/packages/flutter/test/animation/animation_controller_test.dart
+++ b/packages/flutter/test/animation/animation_controller_test.dart
@@ -2,9 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter_test/flutter_test.dart';
 import 'package:flutter/animation.dart';
 import 'package:flutter/widgets.dart';
-import 'package:test/test.dart';
 
 void main() {
   setUp(() {
@@ -171,4 +171,22 @@
     expect(controller.lastElapsedDuration, equals(const Duration(milliseconds: 20)));
     controller.stop();
   });
+
+  test('toString control test', () {
+    AnimationController controller = new AnimationController(
+      duration: const Duration(milliseconds: 100)
+    );
+    expect(controller.toString(), isOneLineDescription);
+    controller.forward();
+    WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 20));
+    WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 30));
+    expect(controller.toString(), isOneLineDescription);
+    WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 120));
+    expect(controller.toString(), isOneLineDescription);
+    controller.reverse();
+    WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 20));
+    WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 30));
+    expect(controller.toString(), isOneLineDescription);
+    controller.stop();
+  });
 }
diff --git a/packages/flutter/test/animation/animations_test.dart b/packages/flutter/test/animation/animations_test.dart
new file mode 100644
index 0000000..55a414e
--- /dev/null
+++ b/packages/flutter/test/animation/animations_test.dart
@@ -0,0 +1,28 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/animation.dart';
+import 'package:flutter/widgets.dart';
+
+void main() {
+  setUp(() {
+    WidgetsFlutterBinding.ensureInitialized();
+    WidgetsBinding.instance.resetEpoch();
+  });
+
+  test('toString control test', () {
+    expect(kAlwaysCompleteAnimation.toString(), isOneLineDescription);
+    expect(kAlwaysDismissedAnimation.toString(), isOneLineDescription);
+  });
+
+  test('toString control test', () {
+    ProxyAnimation animation = new ProxyAnimation();
+    expect(animation.value, 0.0);
+    expect(animation.status, AnimationStatus.dismissed);
+    expect(animation.toString(), isOneLineDescription);
+    animation.parent = kAlwaysDismissedAnimation;
+    expect(animation.toString(), isOneLineDescription);
+  });
+}
diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart
index 4577eac..d076bff 100644
--- a/packages/flutter_test/lib/src/matchers.dart
+++ b/packages/flutter_test/lib/src/matchers.dart
@@ -51,6 +51,12 @@
 /// [Card] widget ancestors.
 const Matcher isNotInCard = const _IsNotInCard();
 
+/// Asserts that a string is a plausible one-line description of an object.
+///
+/// Specifically, this matcher checks that the string does not contains newline
+/// characters and does not have leading or trailing whitespace.
+const Matcher isOneLineDescription = const _IsOneLineDescription();
+
 class _FindsWidgetMatcher extends Matcher {
   const _FindsWidgetMatcher(this.min, this.max);
 
@@ -182,3 +188,17 @@
   @override
   Description describe(Description description) => description.add('not in card');
 }
+
+class _IsOneLineDescription extends Matcher {
+  const _IsOneLineDescription();
+
+  @override
+  bool matches(String description, Map<dynamic, dynamic> matchState) {
+    return description.isNotEmpty &&
+        !description.contains('\n') &&
+        description.trim() == description;
+  }
+
+  @override
+  Description describe(Description description) => description.add('one line description');
+}