CircleBorder (#12570)

diff --git a/packages/flutter/lib/painting.dart b/packages/flutter/lib/painting.dart
index 8297e76..b218755 100644
--- a/packages/flutter/lib/painting.dart
+++ b/packages/flutter/lib/painting.dart
@@ -25,6 +25,7 @@
 export 'src/painting/box_decoration.dart';
 export 'src/painting/box_fit.dart';
 export 'src/painting/box_shadow.dart';
+export 'src/painting/circle_border.dart';
 export 'src/painting/colors.dart';
 export 'src/painting/decoration.dart';
 export 'src/painting/edge_insets.dart';
diff --git a/packages/flutter/lib/src/painting/box_border.dart b/packages/flutter/lib/src/painting/box_border.dart
index fb987d5..41a9da7 100644
--- a/packages/flutter/lib/src/painting/box_border.dart
+++ b/packages/flutter/lib/src/painting/box_border.dart
@@ -484,7 +484,7 @@
   @override
   String toString() {
     if (isUniform)
-      return 'Border.all($top)';
+      return '$runtimeType.all($top)';
     final List<String> arguments = <String>[];
     if (top != BorderSide.none)
       arguments.add('top: $top');
@@ -494,7 +494,7 @@
       arguments.add('bottom: $bottom');
     if (left != BorderSide.none)
       arguments.add('left: $left');
-    return 'Border(${arguments.join(", ")})';
+    return '$runtimeType(${arguments.join(", ")})';
   }
 }
 
@@ -807,6 +807,6 @@
       arguments.add('end: $end');
     if (bottom != BorderSide.none)
       arguments.add('bottom: $bottom');
-    return 'BorderDirectional(${arguments.join(", ")})';
+    return '$runtimeType(${arguments.join(", ")})';
   }
 }
diff --git a/packages/flutter/lib/src/painting/circle_border.dart b/packages/flutter/lib/src/painting/circle_border.dart
new file mode 100644
index 0000000..220ae61
--- /dev/null
+++ b/packages/flutter/lib/src/painting/circle_border.dart
@@ -0,0 +1,98 @@
+// Copyright 2017 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 'dart:math' as math;
+
+import 'basic_types.dart';
+import 'borders.dart';
+import 'edge_insets.dart';
+
+/// A border that fits a circle within the available space.
+///
+/// Typically used with [ShapeDecoration] to draw a circle.
+///
+/// The [dimensions] assume that the border is being used in a square space.
+/// When applied to a rectangular space, the border paints in the center of the
+/// rectangle.
+///
+/// See also:
+///
+///  * [BorderSide], which is used to describe each side of the box.
+///  * [Border], which, when used with [BoxDecoration], can also
+///    describe a circle.
+class CircleBorder extends ShapeBorder {
+  /// Create a circle border.
+  ///
+  /// The [side] argument must not be null.
+  const CircleBorder(this.side) : assert(side != null);
+
+  /// The style of this border.
+  final BorderSide side;
+
+  @override
+  EdgeInsetsGeometry get dimensions {
+    return new EdgeInsets.all(side.width);
+  }
+
+  @override
+  ShapeBorder scale(double t) => new CircleBorder(side.scale(t));
+
+  @override
+  ShapeBorder lerpFrom(ShapeBorder a, double t) {
+    if (a is CircleBorder)
+      return new CircleBorder(BorderSide.lerp(a.side, side, t));
+    return super.lerpFrom(a, t);
+  }
+
+  @override
+  ShapeBorder lerpTo(ShapeBorder b, double t) {
+    if (b is CircleBorder)
+      return new CircleBorder(BorderSide.lerp(side, b.side, t));
+    return super.lerpTo(b, t);
+  }
+
+  @override
+  Path getInnerPath(Rect rect, { TextDirection textDirection }) {
+    return new Path()
+      ..addOval(new Rect.fromCircle(
+        center: rect.center,
+        radius: math.max(0.0, rect.shortestSide / 2.0 - side.width),
+      ));
+  }
+
+  @override
+  Path getOuterPath(Rect rect, { TextDirection textDirection }) {
+    return new Path()
+      ..addOval(new Rect.fromCircle(
+        center: rect.center,
+        radius: rect.shortestSide / 2.0,
+      ));
+  }
+
+  @override
+  void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) {
+    switch (side.style) {
+      case BorderStyle.none:
+        break;
+      case BorderStyle.solid:
+        canvas.drawCircle(rect.center, (rect.shortestSide - side.width) / 2.0, side.toPaint());
+    }
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    if (runtimeType != other.runtimeType)
+      return false;
+    final CircleBorder typedOther = other;
+    return side == typedOther.side;
+  }
+
+  @override
+  int get hashCode => side.hashCode;
+
+  @override
+  String toString() {
+    return '$runtimeType($side)';
+  }
+}
diff --git a/packages/flutter/test/painting/border_rtl_test.dart b/packages/flutter/test/painting/border_rtl_test.dart
index d5c6598..d29b650 100644
--- a/packages/flutter/test/painting/border_rtl_test.dart
+++ b/packages/flutter/test/painting/border_rtl_test.dart
@@ -115,202 +115,204 @@
     expect(() => BoxBorder.lerp(new SillyBorder(), const Border(), 2.0), throwsFlutterError);
   });
 
-  void verifyPath(Path path, {
-    Iterable<Offset> includes: const <Offset>[],
-    Iterable<Offset> excludes: const <Offset>[],
-  }) {
-    for (Offset offset in includes)
-      expect(path.contains(offset), isTrue, reason: 'Offset $offset should be inside the path.');
-    for (Offset offset in excludes)
-      expect(path.contains(offset), isFalse, reason: 'Offset $offset should be outside the path.');
-  }
-
   test('BoxBorder.getInnerPath / BoxBorder.getOuterPath', () {
     // for Border, BorderDirectional
     final Border border = const Border(top: const BorderSide(width: 10.0), right: const BorderSide(width: 20.0));
     final BorderDirectional borderDirectional = const BorderDirectional(top: const BorderSide(width: 10.0), end: const BorderSide(width: 20.0));
-    verifyPath(
+    expect(
       border.getOuterPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.rtl),
-      includes: <Offset>[
-        const Offset(50.0, 60.0),
-        const Offset(60.0, 60.0),
-        const Offset(60.0, 70.0),
-        const Offset(80.0, 190.0),
-        const Offset(109.0, 189.0),
-        const Offset(110.0, 80.0),
-        const Offset(110.0, 190.0),
-      ],
-      excludes: <Offset>[
-        const Offset(40.0, 60.0),
-        const Offset(50.0, 50.0),
-        const Offset(111.0, 190.0),
-        const Offset(110.0, 191.0),
-        const Offset(111.0, 191.0),
-        const Offset(0.0, 0.0),
-        const Offset(-10.0, -10.0),
-        const Offset(0.0, -10.0),
-        const Offset(-10.0, 0.0),
-        const Offset(1000.0, 1000.0),
-      ],
+      isPathThat(
+        includes: <Offset>[
+          const Offset(50.0, 60.0),
+          const Offset(60.0, 60.0),
+          const Offset(60.0, 70.0),
+          const Offset(80.0, 190.0),
+          const Offset(109.0, 189.0),
+          const Offset(110.0, 80.0),
+          const Offset(110.0, 190.0),
+        ],
+        excludes: <Offset>[
+          const Offset(40.0, 60.0),
+          const Offset(50.0, 50.0),
+          const Offset(111.0, 190.0),
+          const Offset(110.0, 191.0),
+          const Offset(111.0, 191.0),
+          const Offset(0.0, 0.0),
+          const Offset(-10.0, -10.0),
+          const Offset(0.0, -10.0),
+          const Offset(-10.0, 0.0),
+          const Offset(1000.0, 1000.0),
+        ],
+      ),
     );
-    verifyPath(
+    expect(
       border.getInnerPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.rtl),
       // inner path is a rect from 50.0,70.0 to 90.0,190.0
-      includes: <Offset>[
-        const Offset(50.0, 70.0),
-        const Offset(55.0, 70.0),
-        const Offset(50.0, 75.0),
-        const Offset(70.0, 70.0),
-        const Offset(70.0, 71.0),
-        const Offset(71.0, 70.0),
-        const Offset(71.0, 71.0),
-        const Offset(80.0, 180.0),
-        const Offset(80.0, 190.0),
-        const Offset(89.0, 189.0),
-        const Offset(90.0, 190.0),
-      ],
-      excludes: <Offset>[
-        const Offset(40.0, 60.0),
-        const Offset(50.0, 50.0),
-        const Offset(50.0, 60.0),
-        const Offset(60.0, 60.0),
-        const Offset(0.0, 0.0),
-        const Offset(-10.0, -10.0),
-        const Offset(0.0, -10.0),
-        const Offset(-10.0, 0.0),
-        const Offset(110.0, 80.0),
-        const Offset(89.0, 191.0),
-        const Offset(90.0, 191.0),
-        const Offset(91.0, 189.0),
-        const Offset(91.0, 190.0),
-        const Offset(91.0, 191.0),
-        const Offset(109.0, 189.0),
-        const Offset(110.0, 190.0),
-        const Offset(1000.0, 1000.0),
-      ],
+      isPathThat(
+        includes: <Offset>[
+          const Offset(50.0, 70.0),
+          const Offset(55.0, 70.0),
+          const Offset(50.0, 75.0),
+          const Offset(70.0, 70.0),
+          const Offset(70.0, 71.0),
+          const Offset(71.0, 70.0),
+          const Offset(71.0, 71.0),
+          const Offset(80.0, 180.0),
+          const Offset(80.0, 190.0),
+          const Offset(89.0, 189.0),
+          const Offset(90.0, 190.0),
+        ],
+        excludes: <Offset>[
+          const Offset(40.0, 60.0),
+          const Offset(50.0, 50.0),
+          const Offset(50.0, 60.0),
+          const Offset(60.0, 60.0),
+          const Offset(0.0, 0.0),
+          const Offset(-10.0, -10.0),
+          const Offset(0.0, -10.0),
+          const Offset(-10.0, 0.0),
+          const Offset(110.0, 80.0),
+          const Offset(89.0, 191.0),
+          const Offset(90.0, 191.0),
+          const Offset(91.0, 189.0),
+          const Offset(91.0, 190.0),
+          const Offset(91.0, 191.0),
+          const Offset(109.0, 189.0),
+          const Offset(110.0, 190.0),
+          const Offset(1000.0, 1000.0),
+        ],
+      )
     );
-    verifyPath(
+    expect(
       borderDirectional.getOuterPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.rtl),
-      includes: <Offset>[
-        const Offset(50.0, 60.0),
-        const Offset(60.0, 60.0),
-        const Offset(60.0, 70.0),
-        const Offset(80.0, 190.0),
-        const Offset(109.0, 189.0),
-        const Offset(110.0, 80.0),
-        const Offset(110.0, 190.0),
-      ],
-      excludes: <Offset>[
-        const Offset(40.0, 60.0),
-        const Offset(50.0, 50.0),
-        const Offset(111.0, 190.0),
-        const Offset(110.0, 191.0),
-        const Offset(111.0, 191.0),
-        const Offset(0.0, 0.0),
-        const Offset(-10.0, -10.0),
-        const Offset(0.0, -10.0),
-        const Offset(-10.0, 0.0),
-        const Offset(1000.0, 1000.0),
-      ],
+      isPathThat(
+        includes: <Offset>[
+          const Offset(50.0, 60.0),
+          const Offset(60.0, 60.0),
+          const Offset(60.0, 70.0),
+          const Offset(80.0, 190.0),
+          const Offset(109.0, 189.0),
+          const Offset(110.0, 80.0),
+          const Offset(110.0, 190.0),
+        ],
+        excludes: <Offset>[
+          const Offset(40.0, 60.0),
+          const Offset(50.0, 50.0),
+          const Offset(111.0, 190.0),
+          const Offset(110.0, 191.0),
+          const Offset(111.0, 191.0),
+          const Offset(0.0, 0.0),
+          const Offset(-10.0, -10.0),
+          const Offset(0.0, -10.0),
+          const Offset(-10.0, 0.0),
+          const Offset(1000.0, 1000.0),
+        ],
+      ),
     );
-    verifyPath(
+    expect(
       borderDirectional.getInnerPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.rtl),
       // inner path is a rect from 70.0,70.0 to 110.0,190.0
-      includes: <Offset>[
-        const Offset(70.0, 70.0),
-        const Offset(70.0, 71.0),
-        const Offset(71.0, 70.0),
-        const Offset(71.0, 71.0),
-        const Offset(80.0, 180.0),
-        const Offset(80.0, 190.0),
-        const Offset(89.0, 189.0),
-        const Offset(90.0, 190.0),
-        const Offset(91.0, 189.0),
-        const Offset(91.0, 190.0),
-        const Offset(109.0, 189.0),
-        const Offset(110.0, 80.0),
-        const Offset(110.0, 190.0),
-      ],
-      excludes: <Offset>[
-        const Offset(40.0, 60.0),
-        const Offset(50.0, 50.0),
-        const Offset(50.0, 60.0),
-        const Offset(50.0, 70.0),
-        const Offset(50.0, 75.0),
-        const Offset(55.0, 70.0),
-        const Offset(60.0, 60.0),
-        const Offset(0.0, 0.0),
-        const Offset(-10.0, -10.0),
-        const Offset(0.0, -10.0),
-        const Offset(-10.0, 0.0),
-        const Offset(89.0, 191.0),
-        const Offset(90.0, 191.0),
-        const Offset(91.0, 191.0),
-        const Offset(110.0, 191.0),
-        const Offset(111.0, 190.0),
-        const Offset(111.0, 191.0),
-        const Offset(1000.0, 1000.0),
-      ],
+      isPathThat(
+        includes: <Offset>[
+          const Offset(70.0, 70.0),
+          const Offset(70.0, 71.0),
+          const Offset(71.0, 70.0),
+          const Offset(71.0, 71.0),
+          const Offset(80.0, 180.0),
+          const Offset(80.0, 190.0),
+          const Offset(89.0, 189.0),
+          const Offset(90.0, 190.0),
+          const Offset(91.0, 189.0),
+          const Offset(91.0, 190.0),
+          const Offset(109.0, 189.0),
+          const Offset(110.0, 80.0),
+          const Offset(110.0, 190.0),
+        ],
+        excludes: <Offset>[
+          const Offset(40.0, 60.0),
+          const Offset(50.0, 50.0),
+          const Offset(50.0, 60.0),
+          const Offset(50.0, 70.0),
+          const Offset(50.0, 75.0),
+          const Offset(55.0, 70.0),
+          const Offset(60.0, 60.0),
+          const Offset(0.0, 0.0),
+          const Offset(-10.0, -10.0),
+          const Offset(0.0, -10.0),
+          const Offset(-10.0, 0.0),
+          const Offset(89.0, 191.0),
+          const Offset(90.0, 191.0),
+          const Offset(91.0, 191.0),
+          const Offset(110.0, 191.0),
+          const Offset(111.0, 190.0),
+          const Offset(111.0, 191.0),
+          const Offset(1000.0, 1000.0),
+        ],
+      ),
     );
-    verifyPath(
+    expect(
       borderDirectional.getOuterPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.ltr),
-      includes: <Offset>[
-        const Offset(50.0, 60.0),
-        const Offset(60.0, 60.0),
-        const Offset(60.0, 70.0),
-        const Offset(80.0, 190.0),
-        const Offset(109.0, 189.0),
-        const Offset(110.0, 80.0),
-        const Offset(110.0, 190.0),
-      ],
-      excludes: <Offset>[
-        const Offset(40.0, 60.0),
-        const Offset(50.0, 50.0),
-        const Offset(111.0, 190.0),
-        const Offset(110.0, 191.0),
-        const Offset(111.0, 191.0),
-        const Offset(0.0, 0.0),
-        const Offset(-10.0, -10.0),
-        const Offset(0.0, -10.0),
-        const Offset(-10.0, 0.0),
-        const Offset(1000.0, 1000.0),
-      ],
+      isPathThat(
+        includes: <Offset>[
+          const Offset(50.0, 60.0),
+          const Offset(60.0, 60.0),
+          const Offset(60.0, 70.0),
+          const Offset(80.0, 190.0),
+          const Offset(109.0, 189.0),
+          const Offset(110.0, 80.0),
+          const Offset(110.0, 190.0),
+        ],
+        excludes: <Offset>[
+          const Offset(40.0, 60.0),
+          const Offset(50.0, 50.0),
+          const Offset(111.0, 190.0),
+          const Offset(110.0, 191.0),
+          const Offset(111.0, 191.0),
+          const Offset(0.0, 0.0),
+          const Offset(-10.0, -10.0),
+          const Offset(0.0, -10.0),
+          const Offset(-10.0, 0.0),
+          const Offset(1000.0, 1000.0),
+        ],
+      ),
     );
-    verifyPath(
+    expect(
       borderDirectional.getInnerPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.ltr),
       // inner path is a rect from 50.0,70.0 to 90.0,190.0
-      includes: <Offset>[
-        const Offset(50.0, 70.0),
-        const Offset(50.0, 75.0),
-        const Offset(55.0, 70.0),
-        const Offset(70.0, 70.0),
-        const Offset(70.0, 71.0),
-        const Offset(71.0, 70.0),
-        const Offset(71.0, 71.0),
-        const Offset(80.0, 180.0),
-        const Offset(80.0, 190.0),
-        const Offset(89.0, 189.0),
-        const Offset(90.0, 190.0),
-      ],
-      excludes: <Offset>[
-        const Offset(50.0, 50.0),
-        const Offset(40.0, 60.0),
-        const Offset(50.0, 60.0),
-        const Offset(60.0, 60.0),
-        const Offset(0.0, 0.0),
-        const Offset(-10.0, -10.0),
-        const Offset(0.0, -10.0),
-        const Offset(-10.0, 0.0),
-        const Offset(110.0, 80.0),
-        const Offset(89.0, 191.0),
-        const Offset(90.0, 191.0),
-        const Offset(91.0, 189.0),
-        const Offset(91.0, 190.0),
-        const Offset(91.0, 191.0),
-        const Offset(109.0, 189.0),
-        const Offset(110.0, 190.0),
-        const Offset(1000.0, 1000.0),
-      ],
+      isPathThat(
+        includes: <Offset>[
+          const Offset(50.0, 70.0),
+          const Offset(50.0, 75.0),
+          const Offset(55.0, 70.0),
+          const Offset(70.0, 70.0),
+          const Offset(70.0, 71.0),
+          const Offset(71.0, 70.0),
+          const Offset(71.0, 71.0),
+          const Offset(80.0, 180.0),
+          const Offset(80.0, 190.0),
+          const Offset(89.0, 189.0),
+          const Offset(90.0, 190.0),
+        ],
+        excludes: <Offset>[
+          const Offset(50.0, 50.0),
+          const Offset(40.0, 60.0),
+          const Offset(50.0, 60.0),
+          const Offset(60.0, 60.0),
+          const Offset(0.0, 0.0),
+          const Offset(-10.0, -10.0),
+          const Offset(0.0, -10.0),
+          const Offset(-10.0, 0.0),
+          const Offset(110.0, 80.0),
+          const Offset(89.0, 191.0),
+          const Offset(90.0, 191.0),
+          const Offset(91.0, 189.0),
+          const Offset(91.0, 190.0),
+          const Offset(91.0, 191.0),
+          const Offset(109.0, 189.0),
+          const Offset(110.0, 190.0),
+          const Offset(1000.0, 1000.0),
+        ],
+      ),
     );
   });
 
diff --git a/packages/flutter/test/painting/circle_border_test.dart b/packages/flutter/test/painting/circle_border_test.dart
new file mode 100644
index 0000000..90f2405
--- /dev/null
+++ b/packages/flutter/test/painting/circle_border_test.dart
@@ -0,0 +1,76 @@
+// Copyright 2017 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/painting.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../rendering/mock_canvas.dart';
+
+void main() {
+  test('CircleBorder', () {
+    final CircleBorder c10 = const CircleBorder(const BorderSide(width: 10.0));
+    final CircleBorder c15 = const CircleBorder(const BorderSide(width: 15.0));
+    final CircleBorder c20 = const CircleBorder(const BorderSide(width: 20.0));
+    expect(c10.dimensions, const EdgeInsets.all(10.0));
+    expect(c10.scale(2.0), c20);
+    expect(c20.scale(0.5), c10);
+    expect(ShapeBorder.lerp(c10, c20, 0.0), c10);
+    expect(ShapeBorder.lerp(c10, c20, 0.5), c15);
+    expect(ShapeBorder.lerp(c10, c20, 1.0), c20);
+    final Matcher isUnitCircle = isPathThat(
+      includes: <Offset>[
+        const Offset(-0.6035617555492896, 0.2230970398703236),
+        const Offset(-0.7738478165627277, 0.5640447581420576),
+        const Offset(-0.46090034164788385, -0.692017006684612),
+        const Offset(-0.2138540316101296, -0.09997005339529785),
+        const Offset(-0.46919827227410416, 0.29581721423767027),
+        const Offset(-0.43628713652733153, 0.5065324817995975),
+        const Offset(0.0, 0.0),
+        const Offset(0.49296904381712725, -0.5922438805080081),
+        const Offset(0.2901141594861445, -0.3181478162967859),
+        const Offset(0.45229946324502146, 0.4324593232323706),
+        const Offset(0.11827752132593572, 0.806442226027837),
+        const Offset(0.8854165569581154, -0.08604230149167624),
+      ],
+      excludes: <Offset>[
+        const Offset(-100.0, -100.0),
+        const Offset(-100.0, 100.0),
+        const Offset(-1.1104403014186688, -1.1234939207590569),
+        const Offset(-1.1852827482514838, -0.5029551986333607),
+        const Offset(-1.0253256532179804, -0.02034402043932526),
+        const Offset(-1.4488532714237397, 0.4948740308904742),
+        const Offset(-1.03142206223176, 0.81070400258819),
+        const Offset(-1.006747917852356, 1.3712062218039343),
+        const Offset(-0.5241429900291878, -1.2852518410112541),
+        const Offset(-0.8879593765104428, -0.9999680025850874),
+        const Offset(-0.9120835110799488, -0.4361605900585557),
+        const Offset(-0.8184877240407303, 1.1202520775469589),
+        const Offset(-0.15746058420492282, -1.1905035795387513),
+        const Offset(-0.11519948876183506, 1.3848147258237393),
+        const Offset(0.0035741796943844495, -1.3383908620447724),
+        const Offset(0.34408827443814394, 1.4514436242950461),
+        const Offset(0.709487222145941, -1.3468012918181573),
+        const Offset(0.6287522653614315, -0.8315879623940617),
+        const Offset(0.9716071801865485, 0.24311969613525442),
+        const Offset(0.7632982576031955, 0.8329765574976169),
+        const Offset(0.9923766847309081, 1.0592617071813715),
+        const Offset(1.2696730082820435, -1.0353385446957046),
+        const Offset(1.4266154921521208, -0.8382633931857755),
+        const Offset(1.298035226938996, -0.11544603567954526),
+        const Offset(1.4143230992455558, 0.10842501221141165),
+        const Offset(1.465352952354424, 0.6999947490821032),
+        const Offset(1.0462985816010146, 1.3874230508561505),
+        const Offset(100.0, -100.0),
+        const Offset(100.0, 100.0),
+      ],
+    );
+    expect(c10.getInnerPath(new Rect.fromCircle(center: Offset.zero, radius: 1.0).inflate(10.0)), isUnitCircle);
+    expect(c10.getOuterPath(new Rect.fromCircle(center: Offset.zero, radius: 1.0)), isUnitCircle);
+    expect(
+      (Canvas canvas) => c10.paint(canvas, new Rect.fromLTWH(10.0, 20.0, 30.0, 40.0)),
+      paints
+        ..circle(x: 25.0, y: 40.0, radius: 10.0, strokeWidth: 10.0)
+    );
+  });
+}
diff --git a/packages/flutter/test/rendering/mock_canvas.dart b/packages/flutter/test/rendering/mock_canvas.dart
index 3f37a5c..d29255e 100644
--- a/packages/flutter/test/rendering/mock_canvas.dart
+++ b/packages/flutter/test/rendering/mock_canvas.dart
@@ -327,6 +327,65 @@
   void something(PaintPatternPredicate predicate);
 }
 
+/// Matches a [Path] that contains (as defined by [Path.contains]) the given
+/// `includes` points and does not contain the given `excludes` points.
+Matcher isPathThat({
+  Iterable<Offset> includes: const <Offset>[],
+  Iterable<Offset> excludes: const <Offset>[],
+}) {
+  return new _PathMatcher(includes.toList(), excludes.toList());
+}
+
+class _PathMatcher extends Matcher {
+  _PathMatcher(this.includes, this.excludes);
+
+  List<Offset> includes;
+  List<Offset> excludes;
+
+  @override
+  bool matches(Object object, Map<dynamic, dynamic> matchState) {
+    if (object is! Path) {
+      matchState[this] = 'The given object ($object) was not a Path.';
+      return false;
+    }
+    final Path path = object;
+    final List<String> errors = <String>[];
+    for (Offset offset in includes) {
+      if (!path.contains(offset))
+        errors.add('Offset $offset should be inside the path, but is not.');
+    }
+    for (Offset offset in excludes) {
+      if (path.contains(offset))
+        errors.add('Offset $offset should be outside the path, but is not.');
+    }
+    if (errors.isEmpty)
+      return true;
+    matchState[this] = 'Not all the given points were inside or outside the path as expected:\n  ${errors.join("\n  ")}';
+    return false;
+  }
+
+  @override
+  Description describe(Description description) {
+    String points(List<Offset> list) {
+      final int count = list.length;
+      if (count == 1)
+        return 'one particular point';
+      return '$count particular points';
+    }
+    return description.add('A Path that contains ${points(includes)} but does not contain ${points(excludes)}.');
+  }
+
+  @override
+  Description describeMismatch(
+    dynamic item,
+    Description description,
+    Map<dynamic, dynamic> matchState,
+    bool verbose,
+  ) {
+    return description.add(matchState[this]);
+  }
+}
+
 class _MismatchedCall {
   const _MismatchedCall(this.message, this.callIntroduction, this.call) : assert(call != null);
   final String message;