Add FakePlatform (#2)

diff --git a/lib/platform.dart b/lib/platform.dart
index c4eca7c..4d74f32 100644
--- a/lib/platform.dart
+++ b/lib/platform.dart
@@ -4,3 +4,4 @@
 
 export 'src/interface/local_platform.dart';
 export 'src/interface/platform.dart';
+export 'src/testing/fake_platform.dart';
diff --git a/lib/src/interface/platform.dart b/lib/src/interface/platform.dart
index 19454d9..f0d5ca7 100644
--- a/lib/src/interface/platform.dart
+++ b/lib/src/interface/platform.dart
@@ -2,6 +2,8 @@
 // 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.
 
+import 'dart:convert';
+
 /// Provides API parity with the `Platform` class in `dart:io`, but using
 /// instance properties rather than static properties. This difference enables
 /// the use of these APIs in tests, where you can provide mock implementations.
@@ -108,4 +110,22 @@
   /// version string of the current dart runtime, possibly followed by
   /// whitespace and other version and build details.
   String get version;
+
+  /// Returns a JSON-encoded representation of this platform.
+  String toJson() {
+    return const JsonEncoder.withIndent('  ').convert(<String, dynamic>{
+      'numberOfProcessors': numberOfProcessors,
+      'pathSeparator': pathSeparator,
+      'operatingSystem': operatingSystem,
+      'localHostname': localHostname,
+      'environment': environment,
+      'executable': executable,
+      'resolvedExecutable': resolvedExecutable,
+      'script': script.toString(),
+      'executableArguments': executableArguments,
+      'packageRoot': packageRoot,
+      'packageConfig': packageConfig,
+      'version': version,
+    });
+  }
 }
diff --git a/lib/src/testing/fake_platform.dart b/lib/src/testing/fake_platform.dart
new file mode 100644
index 0000000..6256d14
--- /dev/null
+++ b/lib/src/testing/fake_platform.dart
@@ -0,0 +1,104 @@
+// Copyright (c) 2017, 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.
+
+import 'dart:convert';
+
+import '../interface/platform.dart';
+
+/// Provides a mutable implementation of the [Platform] interface.
+class FakePlatform extends Platform {
+  /// Creates a new [FakePlatform] with the specified properties.
+  ///
+  /// Unspecified properties will *not* be assigned default values (they will
+  /// remain `null`).
+  FakePlatform({
+    this.numberOfProcessors,
+    this.pathSeparator,
+    this.operatingSystem,
+    this.localHostname,
+    this.environment,
+    this.executable,
+    this.resolvedExecutable,
+    this.script,
+    this.executableArguments,
+    this.packageRoot,
+    this.packageConfig,
+    this.version,
+  });
+
+  /// Creates a new [FakePlatform] with properties whose initial values mirror
+  /// the specified [platform].
+  FakePlatform.fromPlatform(Platform platform)
+      : numberOfProcessors = platform.numberOfProcessors,
+        pathSeparator = platform.pathSeparator,
+        operatingSystem = platform.operatingSystem,
+        localHostname = platform.localHostname,
+        environment = new Map<String, String>.from(platform.environment),
+        executable = platform.executable,
+        resolvedExecutable = platform.resolvedExecutable,
+        script = platform.script,
+        executableArguments = new List<String>.from(platform.executableArguments),
+        packageRoot = platform.packageRoot,
+        packageConfig = platform.packageConfig,
+        version = platform.version;
+
+  /// Creates a new [FakePlatform] with properties extracted from the encoded
+  /// JSON string.
+  ///
+  /// [json] must be a JSON string that matches the encoding produced by
+  /// [toJson].
+  factory FakePlatform.fromJson(String json) {
+    Map<String, dynamic> map = new JsonDecoder().convert(json);
+    return new FakePlatform(
+      numberOfProcessors: map['numberOfProcessors'],
+      pathSeparator : map['pathSeparator'],
+      operatingSystem : map['operatingSystem'],
+      localHostname : map['localHostname'],
+      environment : map['environment'],
+      executable : map['executable'],
+      resolvedExecutable : map['resolvedExecutable'],
+      script : Uri.parse(map['script']),
+      executableArguments : map['executableArguments'],
+      packageRoot : map['packageRoot'],
+      packageConfig : map['packageConfig'],
+      version : map['version'],
+    );
+  }
+
+  @override
+  int numberOfProcessors;
+
+  @override
+  String pathSeparator;
+
+  @override
+  String operatingSystem;
+
+  @override
+  String localHostname;
+
+  @override
+  Map<String, String> environment;
+
+  @override
+  String executable;
+
+  @override
+  String resolvedExecutable;
+
+  @override
+  Uri script;
+
+  @override
+  List<String> executableArguments;
+
+  @override
+  String packageRoot;
+
+  @override
+  String packageConfig;
+
+  @override
+  String version;
+}
diff --git a/test/fake_platform_test.dart b/test/fake_platform_test.dart
new file mode 100644
index 0000000..5699c6b
--- /dev/null
+++ b/test/fake_platform_test.dart
@@ -0,0 +1,85 @@
+// Copyright (c) 2017, 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.
+
+import 'dart:io' as io;
+
+import 'package:platform/platform.dart';
+import 'package:test/test.dart';
+
+void _expectPlatformsEqual(Platform actual, Platform expected) {
+  expect(actual.numberOfProcessors, expected.numberOfProcessors);
+  expect(actual.pathSeparator, expected.pathSeparator);
+  expect(actual.operatingSystem, expected.operatingSystem);
+  expect(actual.localHostname, expected.localHostname);
+  expect(actual.environment, expected.environment);
+  expect(actual.executable, expected.executable);
+  expect(actual.resolvedExecutable, expected.resolvedExecutable);
+  expect(actual.script, expected.script);
+  expect(actual.executableArguments, expected.executableArguments);
+  expect(actual.packageRoot, expected.packageRoot);
+  expect(actual.packageConfig, expected.packageConfig);
+  expect(actual.version, expected.version);
+}
+
+void main() {
+  group('FakePlatform', () {
+    FakePlatform fake;
+    LocalPlatform local;
+
+    setUp(() {
+      fake = new FakePlatform();
+      local = new LocalPlatform();
+    });
+
+    group('fromPlatform', () {
+      setUp(() {
+        fake = new FakePlatform.fromPlatform(local);
+      });
+
+      test('copiesAllProperties', () {
+        _expectPlatformsEqual(fake, local);
+      });
+
+      test('convertsPropertiesToMutable', () {
+        String key = fake.environment.keys.first;
+
+        expect(fake.environment[key], local.environment[key]);
+        fake.environment[key] = 'FAKE';
+        expect(fake.environment[key], 'FAKE');
+
+        expect(fake.executableArguments.length,
+            local.executableArguments.length);
+        fake.executableArguments.add('ARG');
+        expect(fake.executableArguments.last, 'ARG');
+      });
+    });
+
+    group('json', () {
+      test('fromJson', () {
+        String json = new io.File('test/platform.json').readAsStringSync();
+        fake = new FakePlatform.fromJson(json);
+        expect(fake.numberOfProcessors, 8);
+        expect(fake.pathSeparator, '/');
+        expect(fake.operatingSystem, 'macos');
+        expect(fake.localHostname, 'platform.test.org');
+        expect(fake.environment, <String, String>{
+          'PATH': '/bin',
+          'PWD': '/platform',
+        });
+        expect(fake.executable, '/bin/dart');
+        expect(fake.resolvedExecutable, '/bin/dart');
+        expect(fake.script, new Uri.file('/platform/test/fake_platform_test.dart'));
+        expect(fake.executableArguments, <String>['--checked']);
+        expect(fake.packageRoot, null);
+        expect(fake.packageConfig, null);
+        expect(fake.version, '1.22.0');
+      });
+
+      test('fromJsonToJson', () {
+        fake = new FakePlatform.fromJson(local.toJson());
+        _expectPlatformsEqual(fake, local);
+      });
+    });
+  });
+}
diff --git a/test/platform.json b/test/platform.json
new file mode 100644
index 0000000..df772c6
--- /dev/null
+++ b/test/platform.json
@@ -0,0 +1,19 @@
+{
+  "numberOfProcessors": 8,
+  "pathSeparator": "/",
+  "operatingSystem": "macos",
+  "localHostname": "platform.test.org",
+  "environment": {
+    "PATH": "/bin",
+    "PWD": "/platform"
+  },
+  "executable": "/bin/dart",
+  "resolvedExecutable": "/bin/dart",
+  "script": "file:///platform/test/fake_platform_test.dart",
+  "executableArguments": [
+    "--checked"
+  ],
+  "packageRoot": null,
+  "packageConfig": null,
+  "version": "1.22.0"
+}
\ No newline at end of file