Split analysis steps out of dev/bots/test.dart into dev/bots/analyze.dart (#21174)

* Split analysis steps out of dev/bots/test.dart into dev/bots/analyze.dart.

This allows to run analysis step with command line arguments that are only applicable to flutter analyze(like --dart-sdk, needed for dart-flutter-engine head-head-head bot).

* Add forgotten dev/bots/analyze.dart

* Refactor common code from analyze.dart and test.dart into run_command.dart

* Remove comments, add header
diff --git a/dev/bots/run_command.dart b/dev/bots/run_command.dart
new file mode 100644
index 0000000..5427741
--- /dev/null
+++ b/dev/bots/run_command.dart
@@ -0,0 +1,90 @@
+// 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:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+
+final bool hasColor = stdout.supportsAnsiEscapes;
+
+final String bold = hasColor ? '\x1B[1m' : '';
+final String red = hasColor ? '\x1B[31m' : '';
+final String green = hasColor ? '\x1B[32m' : '';
+final String yellow = hasColor ? '\x1B[33m' : '';
+final String cyan = hasColor ? '\x1B[36m' : '';
+final String reset = hasColor ? '\x1B[0m' : '';
+final String redLine = '$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset';
+const String arrow = '⏩';
+const String clock = '🕐';
+
+const Duration _kLongTimeout = Duration(minutes: 45);
+
+String elapsedTime(DateTime start) {
+  return new DateTime.now().difference(start).toString();
+}
+
+void printProgress(String action, String workingDir, String command) {
+  print('$arrow $action: cd $cyan$workingDir$reset; $yellow$command$reset');
+}
+
+Future<Null> runCommand(String executable, List<String> arguments, {
+  String workingDirectory,
+  Map<String, String> environment,
+  bool expectNonZeroExit = false,
+  int expectedExitCode,
+  String failureMessage,
+  bool printOutput = true,
+  bool skip = false,
+  Duration timeout = _kLongTimeout,
+}) async {
+  final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
+  final String relativeWorkingDir = path.relative(workingDirectory);
+  if (skip) {
+    printProgress('SKIPPING', relativeWorkingDir, commandDescription);
+    return null;
+  }
+  printProgress('RUNNING', relativeWorkingDir, commandDescription);
+
+  final DateTime start = new DateTime.now();
+  final Process process = await Process.start(executable, arguments,
+    workingDirectory: workingDirectory,
+    environment: environment,
+  );
+
+  Future<List<List<int>>> savedStdout, savedStderr;
+  if (printOutput) {
+    await Future.wait(<Future<void>>[
+      stdout.addStream(process.stdout),
+      stderr.addStream(process.stderr)
+    ]);
+  } else {
+    savedStdout = process.stdout.toList();
+    savedStderr = process.stderr.toList();
+  }
+
+  final int exitCode = await process.exitCode.timeout(timeout, onTimeout: () {
+    stderr.writeln('Process timed out after $timeout');
+    return expectNonZeroExit ? 0 : 1;
+  });
+  print('$clock ELAPSED TIME: $bold${elapsedTime(start)}$reset for $commandDescription in $relativeWorkingDir: ');
+  if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) {
+    if (failureMessage != null) {
+      print(failureMessage);
+    }
+    if (!printOutput) {
+      stdout.writeln(utf8.decode((await savedStdout).expand((List<int> ints) => ints).toList()));
+      stderr.writeln(utf8.decode((await savedStderr).expand((List<int> ints) => ints).toList()));
+    }
+    print(
+        '$redLine\n'
+            '${bold}ERROR:$red Last command exited with $exitCode (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset\n'
+            '${bold}Command:$cyan $commandDescription$reset\n'
+            '${bold}Relative working directory:$red $relativeWorkingDir$reset\n'
+            '$redLine'
+    );
+    exit(1);
+  }
+}