Commit adac9275 authored by Devon Carew's avatar Devon Carew

add google analytics to flutter_tools (#3523)

* add google analytics

* send in the run target type

* track device type targets

* use the real GA code

* review comments

* rev to usage 2.0

* rev to 2.2.0 of usage; add tests

* review comments
parent 51b1550d
...@@ -15,6 +15,7 @@ import 'src/base/process.dart'; ...@@ -15,6 +15,7 @@ import 'src/base/process.dart';
import 'src/base/utils.dart'; import 'src/base/utils.dart';
import 'src/commands/analyze.dart'; import 'src/commands/analyze.dart';
import 'src/commands/build.dart'; import 'src/commands/build.dart';
import 'src/commands/config.dart';
import 'src/commands/create.dart'; import 'src/commands/create.dart';
import 'src/commands/daemon.dart'; import 'src/commands/daemon.dart';
import 'src/commands/devices.dart'; import 'src/commands/devices.dart';
...@@ -56,6 +57,7 @@ Future<Null> main(List<String> args) async { ...@@ -56,6 +57,7 @@ Future<Null> main(List<String> args) async {
FlutterCommandRunner runner = new FlutterCommandRunner(verboseHelp: verboseHelp) FlutterCommandRunner runner = new FlutterCommandRunner(verboseHelp: verboseHelp)
..addCommand(new AnalyzeCommand()) ..addCommand(new AnalyzeCommand())
..addCommand(new BuildCommand()) ..addCommand(new BuildCommand())
..addCommand(new ConfigCommand())
..addCommand(new CreateCommand()) ..addCommand(new CreateCommand())
..addCommand(new DaemonCommand(hidden: !verboseHelp)) ..addCommand(new DaemonCommand(hidden: !verboseHelp))
..addCommand(new DevicesCommand()) ..addCommand(new DevicesCommand())
...@@ -82,10 +84,18 @@ Future<Null> main(List<String> args) async { ...@@ -82,10 +84,18 @@ Future<Null> main(List<String> args) async {
context[DeviceManager] = new DeviceManager(); context[DeviceManager] = new DeviceManager();
Doctor.initGlobal(); Doctor.initGlobal();
if (flutterUsage.isFirstRun) {
printStatus(
'The Flutter tool anonymously reports feature usage statistics and basic crash reports to Google to\n'
'help Google contribute improvements to Flutter over time. Use "flutter config" to control this\n'
'behavior. See Google\'s privacy policy: https://www.google.com/intl/en/policies/privacy/\n'
);
}
dynamic result = await runner.run(args); dynamic result = await runner.run(args);
if (result is int) if (result is int)
exit(result); _exit(result);
}, onError: (dynamic error, Chain chain) { }, onError: (dynamic error, Chain chain) {
if (error is UsageException) { if (error is UsageException) {
stderr.writeln(error.message); stderr.writeln(error.message);
...@@ -93,14 +103,16 @@ Future<Null> main(List<String> args) async { ...@@ -93,14 +103,16 @@ Future<Null> main(List<String> args) async {
stderr.writeln("Run 'flutter -h' (or 'flutter <command> -h') for available " stderr.writeln("Run 'flutter -h' (or 'flutter <command> -h') for available "
"flutter commands and options."); "flutter commands and options.");
// Argument error exit code. // Argument error exit code.
exit(64); _exit(64);
} else if (error is ProcessExit) { } else if (error is ProcessExit) {
// We've caught an exit code. // We've caught an exit code.
exit(error.exitCode); _exit(error.exitCode);
} else { } else {
// We've crashed; emit a log report. // We've crashed; emit a log report.
stderr.writeln(); stderr.writeln();
flutterUsage.sendException(error, chain);
if (Platform.environment.containsKey('FLUTTER_DEV')) { if (Platform.environment.containsKey('FLUTTER_DEV')) {
// If we're working on the tools themselves, just print the stack trace. // If we're working on the tools themselves, just print the stack trace.
stderr.writeln('$error'); stderr.writeln('$error');
...@@ -118,7 +130,7 @@ Future<Null> main(List<String> args) async { ...@@ -118,7 +130,7 @@ Future<Null> main(List<String> args) async {
'please let us know at https://github.com/flutter/flutter/issues.'); 'please let us know at https://github.com/flutter/flutter/issues.');
} }
exit(1); _exit(1);
} }
}); });
} }
...@@ -159,3 +171,21 @@ String _doctorText() { ...@@ -159,3 +171,21 @@ String _doctorText() {
return 'encountered exception: $error\n\n${trace.toString().trim()}\n'; return 'encountered exception: $error\n\n${trace.toString().trim()}\n';
} }
} }
Future<Null> _exit(int code) async {
// Send any last analytics calls that are in progress without overly delaying
// the tool's exit (we wait a maximum of 250ms).
if (flutterUsage.enabled) {
Stopwatch stopwatch = new Stopwatch()..start();
await flutterUsage.ensureAnalyticsSent();
printTrace('ensureAnalyticsSent: ${stopwatch.elapsedMilliseconds}ms');
}
// Write any buffered output.
logger.flush();
// Give the task / timer queue one cycle through before we hard exit.
await Timer.run(() {
exit(code);
});
}
// 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 'dart:async';
import '../globals.dart';
import '../runner/flutter_command.dart';
class ConfigCommand extends FlutterCommand {
ConfigCommand() {
String usageStatus = flutterUsage.enabled ? 'enabled' : 'disabled';
argParser.addFlag('analytics',
negatable: true,
help: 'Enable or disable reporting anonymously tool usage statistics and crash reports.\n(currently $usageStatus)');
}
@override
final String name = 'config';
@override
final String description =
'Configure Flutter settings.\n\n'
'The Flutter tool anonymously reports feature usage statistics and basic crash reports to help improve\n'
'Flutter tools over time. See Google\'s privacy policy: www.google.com/intl/en/policies/privacy';
@override
final List<String> aliases = <String>['configure'];
@override
bool get requiresProjectRoot => false;
/// Return `null` to disable tracking of the `config` command.
@override
String get usagePath => null;
@override
Future<int> runInProject() async {
if (argResults.wasParsed('analytics')) {
bool value = argResults['analytics'];
flutterUsage.enabled = value;
printStatus('Analytics reporting ${value ? 'enabled' : 'disabled'}.');
} else {
printStatus(usage);
}
return 0;
}
}
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import '../android/android.dart' as android; import '../android/android.dart' as android;
...@@ -14,19 +13,10 @@ import '../base/utils.dart'; ...@@ -14,19 +13,10 @@ import '../base/utils.dart';
import '../cache.dart'; import '../cache.dart';
import '../dart/pub.dart'; import '../dart/pub.dart';
import '../globals.dart'; import '../globals.dart';
import '../runner/flutter_command.dart';
import '../template.dart'; import '../template.dart';
class CreateCommand extends Command { class CreateCommand extends FlutterCommand {
@override
final String name = 'create';
@override
final String description = 'Create a new Flutter project.\n\n'
'If run on a project that already exists, this will repair the project, recreating any files that are missing.';
@override
final List<String> aliases = <String>['init'];
CreateCommand() { CreateCommand() {
argParser.addFlag('pub', argParser.addFlag('pub',
defaultsTo: true, defaultsTo: true,
...@@ -45,11 +35,24 @@ class CreateCommand extends Command { ...@@ -45,11 +35,24 @@ class CreateCommand extends Command {
); );
} }
@override
final String name = 'create';
@override
final String description = 'Create a new Flutter project.\n\n'
'If run on a project that already exists, this will repair the project, recreating any files that are missing.';
@override
final List<String> aliases = <String>['init'];
@override
bool get requiresProjectRoot => false;
@override @override
String get invocation => "${runner.executableName} $name <output directory>"; String get invocation => "${runner.executableName} $name <output directory>";
@override @override
Future<int> run() async { Future<int> runInProject() async {
if (argResults.rest.isEmpty) { if (argResults.rest.isEmpty) {
printStatus('No option specified for the output directory.'); printStatus('No option specified for the output directory.');
printStatus(usage); printStatus(usage);
......
...@@ -4,11 +4,10 @@ ...@@ -4,11 +4,10 @@
import 'dart:async'; import 'dart:async';
import 'package:args/command_runner.dart';
import '../globals.dart'; import '../globals.dart';
import '../runner/flutter_command.dart';
class PrecacheCommand extends Command { class PrecacheCommand extends FlutterCommand {
@override @override
final String name = 'precache'; final String name = 'precache';
...@@ -16,7 +15,10 @@ class PrecacheCommand extends Command { ...@@ -16,7 +15,10 @@ class PrecacheCommand extends Command {
final String description = 'Populates the Flutter tool\'s cache of binary artifacts.'; final String description = 'Populates the Flutter tool\'s cache of binary artifacts.';
@override @override
Future<int> run() async { bool get requiresProjectRoot => false;
@override
Future<int> runInProject() async {
if (cache.isUpToDate()) if (cache.isUpToDate())
printStatus('Already up-to-date.'); printStatus('Already up-to-date.');
else else
......
...@@ -82,6 +82,17 @@ class RunCommand extends RunCommandBase { ...@@ -82,6 +82,17 @@ class RunCommand extends RunCommandBase {
@override @override
bool get requiresDevice => true; bool get requiresDevice => true;
@override
String get usagePath {
Device device = deviceForCommand;
if (device == null)
return name;
// Return 'run/ios'.
return '$name/${getNameForTargetPlatform(device.platform)}';
}
@override @override
Future<int> runInProject() async { Future<int> runInProject() async {
bool clearLogs = argResults['clear-logs']; bool clearLogs = argResults['clear-logs'];
......
...@@ -12,12 +12,6 @@ import '../globals.dart'; ...@@ -12,12 +12,6 @@ import '../globals.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
class SkiaCommand extends FlutterCommand { class SkiaCommand extends FlutterCommand {
@override
final String name = 'skia';
@override
final String description = 'Retrieve the last frame rendered by a Flutter app as a Skia picture.';
SkiaCommand() { SkiaCommand() {
argParser.addOption('output-file', help: 'Write the Skia picture file to this path.'); argParser.addOption('output-file', help: 'Write the Skia picture file to this path.');
argParser.addOption('skiaserve', help: 'Post the picture to a skiaserve debugger at this URL.'); argParser.addOption('skiaserve', help: 'Post the picture to a skiaserve debugger at this URL.');
...@@ -26,6 +20,12 @@ class SkiaCommand extends FlutterCommand { ...@@ -26,6 +20,12 @@ class SkiaCommand extends FlutterCommand {
help: 'Local port where the diagnostic server is listening.'); help: 'Local port where the diagnostic server is listening.');
} }
@override
final String name = 'skia';
@override
final String description = 'Retrieve the last frame rendered by a Flutter app as a Skia picture.';
@override @override
Future<int> runInProject() async { Future<int> runInProject() async {
File outputFile; File outputFile;
......
...@@ -9,13 +9,15 @@ import 'cache.dart'; ...@@ -9,13 +9,15 @@ import 'cache.dart';
import 'device.dart'; import 'device.dart';
import 'doctor.dart'; import 'doctor.dart';
import 'toolchain.dart'; import 'toolchain.dart';
import 'usage.dart';
DeviceManager get deviceManager => context[DeviceManager]; DeviceManager get deviceManager => context[DeviceManager];
Logger get logger => context[Logger]; Logger get logger => context[Logger];
AndroidSdk get androidSdk => context[AndroidSdk]; AndroidSdk get androidSdk => context[AndroidSdk];
Doctor get doctor => context[Doctor];
Cache get cache => Cache.instance; Cache get cache => Cache.instance;
Doctor get doctor => context[Doctor];
ToolConfiguration get tools => ToolConfiguration.instance; ToolConfiguration get tools => ToolConfiguration.instance;
Usage get flutterUsage => Usage.instance;
/// Display an error level message to the user. Commands should use this if they /// Display an error level message to the user. Commands should use this if they
/// fail in some way. /// fail in some way.
......
...@@ -15,6 +15,7 @@ import '../flx.dart' as flx; ...@@ -15,6 +15,7 @@ import '../flx.dart' as flx;
import '../globals.dart'; import '../globals.dart';
import '../package_map.dart'; import '../package_map.dart';
import '../toolchain.dart'; import '../toolchain.dart';
import '../usage.dart';
import 'flutter_command_runner.dart'; import 'flutter_command_runner.dart';
typedef bool Validator(); typedef bool Validator();
...@@ -94,13 +95,19 @@ abstract class FlutterCommand extends Command { ...@@ -94,13 +95,19 @@ abstract class FlutterCommand extends Command {
applicationPackages ??= new ApplicationPackageStore(); applicationPackages ??= new ApplicationPackageStore();
} }
/// The path to send to Google Analytics. Return `null` here to disable
/// tracking of the command.
String get usagePath => name;
@override @override
Future<int> run() { Future<int> run() {
Stopwatch stopwatch = new Stopwatch()..start(); Stopwatch stopwatch = new Stopwatch()..start();
UsageTimer analyticsTimer = usagePath == null ? null : flutterUsage.startTimer(name);
return _run().then((int exitCode) { return _run().then((int exitCode) {
int ms = stopwatch.elapsedMilliseconds; int ms = stopwatch.elapsedMilliseconds;
printTrace("'flutter $name' took ${ms}ms; exiting with code $exitCode."); printTrace("'flutter $name' took ${ms}ms; exiting with code $exitCode.");
analyticsTimer?.finish();
return exitCode; return exitCode;
}); });
} }
...@@ -160,6 +167,10 @@ abstract class FlutterCommand extends Command { ...@@ -160,6 +167,10 @@ abstract class FlutterCommand extends Command {
_setupToolchain(); _setupToolchain();
_setupApplicationPackages(); _setupApplicationPackages();
String commandPath = usagePath;
if (commandPath != null)
flutterUsage.sendCommand(usagePath);
return await runInProject(); return await runInProject();
} }
......
...@@ -84,13 +84,15 @@ class FlutterCommandRunner extends CommandRunner { ...@@ -84,13 +84,15 @@ class FlutterCommandRunner extends CommandRunner {
argParser.addOption('host-debug-build-path', argParser.addOption('host-debug-build-path',
hide: !verboseHelp, hide: !verboseHelp,
help: help:
'Path to your host Debug out directory (i.e. the one that runs on your workstation, not a device), if you are building Flutter locally.\n' 'Path to your host Debug out directory (i.e. the one that runs on your workstation, not a device),\n'
'if you are building Flutter locally.\n'
'This path is relative to --engine-src-path. Not normally required.', 'This path is relative to --engine-src-path. Not normally required.',
defaultsTo: 'out/Debug/'); defaultsTo: 'out/Debug/');
argParser.addOption('host-release-build-path', argParser.addOption('host-release-build-path',
hide: !verboseHelp, hide: !verboseHelp,
help: help:
'Path to your host Release out directory (i.e. the one that runs on your workstation, not a device), if you are building Flutter locally.\n' 'Path to your host Release out directory (i.e. the one that runs on your workstation, not a device),\n'
'if you are building Flutter locally.\n'
'This path is relative to --engine-src-path. Not normally required.', 'This path is relative to --engine-src-path. Not normally required.',
defaultsTo: 'out/Release/'); defaultsTo: 'out/Release/');
...@@ -232,6 +234,7 @@ class FlutterCommandRunner extends CommandRunner { ...@@ -232,6 +234,7 @@ class FlutterCommandRunner extends CommandRunner {
} }
if (globalResults['version']) { if (globalResults['version']) {
flutterUsage.sendCommand('version');
printStatus(FlutterVersion.getVersion(ArtifactStore.flutterRoot).toString()); printStatus(FlutterVersion.getVersion(ArtifactStore.flutterRoot).toString());
return new Future<int>.value(0); return new Future<int>.value(0);
} }
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:io';
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/process.dart'; import '../base/process.dart';
...@@ -54,4 +56,21 @@ class FlutterVersion { ...@@ -54,4 +56,21 @@ class FlutterVersion {
static FlutterVersion getVersion([String flutterRoot]) { static FlutterVersion getVersion([String flutterRoot]) {
return new FlutterVersion(flutterRoot != null ? flutterRoot : ArtifactStore.flutterRoot); return new FlutterVersion(flutterRoot != null ? flutterRoot : ArtifactStore.flutterRoot);
} }
static String getVersionString() {
final String cwd = ArtifactStore.flutterRoot;
String commit = _runSync('git', <String>['rev-parse', 'HEAD'], cwd);
if (commit.length > 8)
commit = commit.substring(0, 8);
String branch = _runSync('git', <String>['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
return '$commit/$branch';
}
}
String _runSync(String executable, List<String> arguments, String cwd) {
ProcessResult results = Process.runSync(executable, arguments, workingDirectory: cwd);
return results.exitCode == 0 ? results.stdout.trim() : '';
} }
// 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 'dart:async';
import 'package:usage/src/usage_impl_io.dart';
import 'package:usage/usage.dart';
import 'base/context.dart';
import 'runner/version.dart';
// TODO(devoncarew): We'll need to do some work on the user agent in order to
// correctly track usage by operating system (dart-lang/usage/issues/70).
// TODO(devoncarew): We'll want to find a way to send (sanitized) command parameters.
const String _kFlutterUA = 'UA-67589403-5';
class Usage {
Usage() {
_analytics = new AnalyticsIO(_kFlutterUA, 'flutter', FlutterVersion.getVersionString());
_analytics.analyticsOpt = AnalyticsOpt.optOut;
}
/// Returns [Usage] active in the current app context.
static Usage get instance => context[Usage] ?? (context[Usage] = new Usage());
Analytics _analytics;
bool get isFirstRun => _analytics.firstRun;
bool get enabled => _analytics.enabled;
/// Enable or disable reporting analytics.
set enabled(bool value) {
_analytics.enabled = value;
}
void sendCommand(String command) {
if (!isFirstRun)
_analytics.sendScreenView(command);
}
void sendEvent(String category, String parameter) {
if (!isFirstRun)
_analytics.sendEvent(category, parameter);
}
UsageTimer startTimer(String event) {
if (isFirstRun)
return new _MockUsageTimer();
else
return new UsageTimer._(event, _analytics.startTimer(event));
}
void sendException(dynamic exception, StackTrace trace) {
if (!isFirstRun)
_analytics.sendException('${exception.runtimeType}; ${sanitizeStacktrace(trace)}');
}
/// Fires whenever analytics data is sent over the network; public for testing.
Stream<Map<String, dynamic>> get onSend => _analytics.onSend;
/// Returns when the last analytics event has been sent, or after a fixed
/// (short) delay, whichever is less.
Future<Null> ensureAnalyticsSent() {
// TODO(devoncarew): This may delay tool exit and could cause some analytics
// events to not be reported. Perhaps we could send the analytics pings
// out-of-process from flutter_tools?
return _analytics.waitForLastPing(timeout: new Duration(milliseconds: 250));
}
}
class UsageTimer {
UsageTimer._(this.event, this._timer);
final String event;
final AnalyticsTimer _timer;
void finish() {
_timer.finish();
}
}
class _MockUsageTimer implements UsageTimer {
@override
String event;
@override
AnalyticsTimer _timer;
@override
void finish() { }
}
...@@ -20,6 +20,7 @@ dependencies: ...@@ -20,6 +20,7 @@ dependencies:
path: ^1.3.0 path: ^1.3.0
pub_semver: ^1.0.0 pub_semver: ^1.0.0
stack_trace: ^1.4.0 stack_trace: ^1.4.0
usage: ^2.2.0
web_socket_channel: ^1.0.0 web_socket_channel: ^1.0.0
xml: ^2.4.1 xml: ^2.4.1
yaml: ^2.1.3 yaml: ^2.1.3
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
// fix lands. // fix lands.
import 'adb_test.dart' as adb_test; import 'adb_test.dart' as adb_test;
import 'analytics_test.dart' as analytics_test;
import 'analyze_duplicate_names_test.dart' as analyze_duplicate_names_test; import 'analyze_duplicate_names_test.dart' as analyze_duplicate_names_test;
import 'analyze_test.dart' as analyze_test; import 'analyze_test.dart' as analyze_test;
import 'android_device_test.dart' as android_device_test; import 'android_device_test.dart' as android_device_test;
...@@ -31,6 +32,7 @@ import 'upgrade_test.dart' as upgrade_test; ...@@ -31,6 +32,7 @@ import 'upgrade_test.dart' as upgrade_test;
void main() { void main() {
adb_test.main(); adb_test.main();
analytics_test.main();
analyze_duplicate_names_test.main(); analyze_duplicate_names_test.main();
analyze_test.main(); analyze_test.main();
android_device_test.main(); android_device_test.main();
......
// 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 'dart:io';
import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/commands/create.dart';
import 'package:flutter_tools/src/commands/config.dart';
import 'package:flutter_tools/src/commands/doctor.dart';
import 'package:flutter_tools/src/globals.dart';
import 'package:flutter_tools/src/usage.dart';
import 'package:test/test.dart';
import 'src/common.dart';
import 'src/context.dart';
void main() {
group('analytics', () {
Directory temp;
bool wasEnabled;
setUp(() {
ArtifactStore.flutterRoot = '../..';
wasEnabled = flutterUsage.enabled;
temp = Directory.systemTemp.createTempSync('flutter_tools');
});
tearDown(() {
flutterUsage.enabled = wasEnabled;
temp.deleteSync(recursive: true);
});
// Ensure we don't send anything when analytics is disabled.
testUsingContext('doesn\'t send when disabled', () async {
int count = 0;
flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
flutterUsage.enabled = false;
CreateCommand command = new CreateCommand();
CommandRunner runner = createTestCommandRunner(command);
int code = await runner.run(<String>['create', '--no-pub', temp.path]);
expect(code, equals(0));
expect(count, 0);
flutterUsage.enabled = true;
code = await runner.run(<String>['create', '--no-pub', temp.path]);
expect(code, equals(0));
expect(count, flutterUsage.isFirstRun ? 0 : 2);
count = 0;
flutterUsage.enabled = false;
DoctorCommand doctorCommand = new DoctorCommand();
runner = createTestCommandRunner(doctorCommand);
code = await runner.run(<String>['doctor']);
expect(code, equals(0));
expect(count, 0);
}, overrides: <Type, dynamic>{
Usage: new Usage()
});
// Ensure we con't send for the 'flutter config' command.
testUsingContext('config doesn\'t send', () async {
int count = 0;
flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
flutterUsage.enabled = false;
ConfigCommand command = new ConfigCommand();
CommandRunner runner = createTestCommandRunner(command);
await runner.run(<String>['config']);
expect(count, 0);
flutterUsage.enabled = true;
await runner.run(<String>['config']);
expect(count, 0);
}, overrides: <Type, dynamic>{
Usage: new Usage()
});
});
}
...@@ -11,6 +11,7 @@ import 'package:flutter_tools/src/commands/create.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter_tools/src/commands/create.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'src/common.dart';
import 'src/context.dart'; import 'src/context.dart';
void main() { void main() {
...@@ -37,9 +38,9 @@ void main() { ...@@ -37,9 +38,9 @@ void main() {
// Verify that we can regenerate over an existing project. // Verify that we can regenerate over an existing project.
testUsingContext('can re-gen over existing project', () async { testUsingContext('can re-gen over existing project', () async {
ArtifactStore.flutterRoot = '../..'; ArtifactStore.flutterRoot = '../..';
CreateCommand command = new CreateCommand(); CreateCommand command = new CreateCommand();
CommandRunner runner = new CommandRunner('test_flutter', '') CommandRunner runner = createTestCommandRunner(command);
..addCommand(command);
int code = await runner.run(<String>['create', '--no-pub', temp.path]); int code = await runner.run(<String>['create', '--no-pub', temp.path]);
expect(code, equals(0)); expect(code, equals(0));
...@@ -52,8 +53,7 @@ void main() { ...@@ -52,8 +53,7 @@ void main() {
testUsingContext('fails when file exists', () async { testUsingContext('fails when file exists', () async {
ArtifactStore.flutterRoot = '../..'; ArtifactStore.flutterRoot = '../..';
CreateCommand command = new CreateCommand(); CreateCommand command = new CreateCommand();
CommandRunner runner = new CommandRunner('test_flutter', '') CommandRunner runner = createTestCommandRunner(command);
..addCommand(command);
File existingFile = new File("${temp.path.toString()}/bad"); File existingFile = new File("${temp.path.toString()}/bad");
if (!existingFile.existsSync()) existingFile.createSync(); if (!existingFile.existsSync()) existingFile.createSync();
int code = await runner.run(<String>['create', existingFile.path]); int code = await runner.run(<String>['create', existingFile.path]);
...@@ -65,8 +65,7 @@ void main() { ...@@ -65,8 +65,7 @@ void main() {
Future<Null> _createAndAnalyzeProject(Directory dir, List<String> createArgs) async { Future<Null> _createAndAnalyzeProject(Directory dir, List<String> createArgs) async {
ArtifactStore.flutterRoot = '../..'; ArtifactStore.flutterRoot = '../..';
CreateCommand command = new CreateCommand(); CreateCommand command = new CreateCommand();
CommandRunner runner = new CommandRunner('test_flutter', '') CommandRunner runner = createTestCommandRunner(command);
..addCommand(command);
List<String> args = <String>['create']; List<String> args = <String>['create'];
args.addAll(createArgs); args.addAll(createArgs);
args.add(dir.path); args.add(dir.path);
......
...@@ -12,7 +12,7 @@ import 'package:flutter_tools/src/device.dart'; ...@@ -12,7 +12,7 @@ import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/doctor.dart'; import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/ios/simulators.dart'; import 'package:flutter_tools/src/ios/simulators.dart';
import 'package:flutter_tools/src/usage.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -45,6 +45,9 @@ void testUsingContext(String description, dynamic testMethod(), { ...@@ -45,6 +45,9 @@ void testUsingContext(String description, dynamic testMethod(), {
if (!overrides.containsKey(SimControl)) if (!overrides.containsKey(SimControl))
testContext[SimControl] = new MockSimControl(); testContext[SimControl] = new MockSimControl();
if (!overrides.containsKey(Usage))
testContext[Usage] = new MockUsage();
if (!overrides.containsKey(OperatingSystemUtils)) { if (!overrides.containsKey(OperatingSystemUtils)) {
MockOperatingSystemUtils os = new MockOperatingSystemUtils(); MockOperatingSystemUtils os = new MockOperatingSystemUtils();
when(os.isWindows).thenReturn(false); when(os.isWindows).thenReturn(false);
...@@ -112,3 +115,42 @@ class MockSimControl extends Mock implements SimControl { ...@@ -112,3 +115,42 @@ class MockSimControl extends Mock implements SimControl {
class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {} class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}
class MockIOSSimulatorUtils extends Mock implements IOSSimulatorUtils {} class MockIOSSimulatorUtils extends Mock implements IOSSimulatorUtils {}
class MockUsage implements Usage {
@override
bool get isFirstRun => false;
@override
bool get enabled => true;
@override
set enabled(bool value) { }
@override
void sendCommand(String command) { }
@override
void sendEvent(String category, String parameter) { }
@override
UsageTimer startTimer(String event) => new _MockUsageTimer(event);
@override
void sendException(dynamic exception, StackTrace trace) { }
@override
Stream<Map<String, dynamic>> get onSend => null;
@override
Future<Null> ensureAnalyticsSent() => new Future<Null>.value();
}
class _MockUsageTimer implements UsageTimer {
_MockUsageTimer(this.event);
@override
final String event;
@override
void finish() { }
}
...@@ -3,4 +3,8 @@ set -ex ...@@ -3,4 +3,8 @@ set -ex
# Download dependencies flutter # Download dependencies flutter
./bin/flutter --version ./bin/flutter --version
# Disable analytics on the bots (to avoid skewing analytics data).
./bin/flutter config --no-analytics
./bin/flutter update-packages ./bin/flutter update-packages
#!/bin/bash #!/bin/bash
set -ex set -ex
export PATH="$PWD/bin:$PATH" export PATH="$PWD/bin:$PWD/bin/cache/dart-sdk/bin:$PATH"
# analyze all the Dart code in the repo # analyze all the Dart code in the repo
flutter analyze --flutter-repo flutter analyze --flutter-repo
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment