Consider all non-interactive terminals to be bots (#34179)


diff --git a/packages/flutter_tools/lib/src/base/io.dart b/packages/flutter_tools/lib/src/base/io.dart
index 7ae40ae..9f9b882 100644
--- a/packages/flutter_tools/lib/src/base/io.dart
+++ b/packages/flutter_tools/lib/src/base/io.dart
@@ -26,7 +26,7 @@
 /// increase the API surface that we have to test in Flutter tools, and the APIs
 /// in `dart:io` can sometimes be hard to use in tests.
 import 'dart:async';
-import 'dart:io' as io show exit, IOSink, ProcessSignal, stderr, stdin, stdout;
+import 'dart:io' as io show exit, IOSink, ProcessSignal, stderr, stdin, Stdout, stdout;
 
 import 'package:meta/meta.dart';
 
@@ -72,6 +72,7 @@
         Stdin,
         StdinException,
         // stdout,           NO! Use `io.dart`
+        Stdout,
         Socket,
         SocketException,
         systemEncoding,
@@ -156,7 +157,7 @@
   const Stdio();
 
   Stream<List<int>> get stdin => io.stdin;
-  io.IOSink get stdout => io.stdout;
+  io.Stdout get stdout => io.stdout;
   io.IOSink get stderr => io.stderr;
 
   bool get hasTerminal => io.stdout.hasTerminal;
@@ -165,7 +166,7 @@
   bool get supportsAnsiEscapes => hasTerminal ? io.stdout.supportsAnsiEscapes : false;
 }
 
-Stdio get stdio => context.get<Stdio>();
-io.IOSink get stdout => stdio.stdout;
+Stdio get stdio => context.get<Stdio>() ?? const Stdio();
+io.Stdout get stdout => stdio.stdout;
 Stream<List<int>> get stdin => stdio.stdin;
 io.IOSink get stderr => stdio.stderr;
diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart
index 1b6d2c3..9b612e2 100644
--- a/packages/flutter_tools/lib/src/base/utils.dart
+++ b/packages/flutter_tools/lib/src/base/utils.dart
@@ -21,8 +21,20 @@
   const BotDetector();
 
   bool get isRunningOnBot {
-    return platform.environment['BOT'] != 'false'
-       && (platform.environment['BOT'] == 'true'
+    if (
+        // Explicitly stated to not be a bot.
+        platform.environment['BOT'] == 'false'
+
+        // Set by the IDEs to the IDE name, so a strong signal that this is not a bot.
+        || platform.environment.containsKey('FLUTTER_HOST')
+    ) {
+      return false;
+    }
+
+    return platform.environment['BOT'] == 'true'
+
+        // Non-interactive terminals are assumed to be bots.
+        || !io.stdout.hasTerminal
 
         // https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables
         || platform.environment['TRAVIS'] == 'true'
@@ -43,7 +55,8 @@
 
         // Properties on Flutter's Chrome Infra bots.
         || platform.environment['CHROME_HEADLESS'] == '1'
-        || platform.environment.containsKey('BUILDBOT_BUILDERNAME'));
+        || platform.environment.containsKey('BUILDBOT_BUILDERNAME')
+        || platform.environment.containsKey('SWARMING_TASK_ID');
   }
 }
 
diff --git a/packages/flutter_tools/test/base/utils_test.dart b/packages/flutter_tools/test/base/utils_test.dart
new file mode 100644
index 0000000..8c1d885
--- /dev/null
+++ b/packages/flutter_tools/test/base/utils_test.dart
@@ -0,0 +1,56 @@
+// Copyright 2019 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_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/utils.dart';
+import 'package:platform/platform.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/mocks.dart';
+
+void main() {
+  group('BotDetector', () {
+    FakePlatform fakePlatform;
+    MockStdio mockStdio;
+    BotDetector botDetector;
+
+    setUp(() {
+      fakePlatform = FakePlatform()..environment = <String, String>{};
+      mockStdio = MockStdio();
+      botDetector = const BotDetector();
+    });
+
+    group('isRunningOnBot', () {
+      testUsingContext('returns false unconditionally if BOT=false is set', () async {
+        fakePlatform.environment['BOT'] = 'false';
+        fakePlatform.environment['TRAVIS'] = 'true';
+        expect(botDetector.isRunningOnBot, isFalse);
+      }, overrides: <Type, Generator>{
+        Stdio: () => mockStdio,
+        Platform: () => fakePlatform,
+      });
+
+      testUsingContext('returns false unconditionally if FLUTTER_HOST is set', () async {
+        fakePlatform.environment['FLUTTER_HOST'] = 'foo';
+        fakePlatform.environment['TRAVIS'] = 'true';
+        expect(botDetector.isRunningOnBot, isFalse);
+      }, overrides: <Type, Generator>{
+        Stdio: () => mockStdio,
+        Platform: () => fakePlatform,
+      });
+
+      testUsingContext('returns true for non-interactive terminals', () async {
+        mockStdio.stdout.hasTerminal = true;
+        expect(botDetector.isRunningOnBot, isFalse);
+        mockStdio.stdout.hasTerminal = false;
+        expect(botDetector.isRunningOnBot, isTrue);
+      }, overrides: <Type, Generator>{
+        Stdio: () => mockStdio,
+        Platform: () => fakePlatform,
+      });
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 8c318bc..5e93576 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -4,7 +4,7 @@
 
 import 'dart:async';
 import 'dart:convert';
-import 'dart:io' as io show IOSink, ProcessSignal;
+import 'dart:io' as io show IOSink, ProcessSignal, Stdout, StdoutException;
 
 import 'package:flutter_tools/src/android/android_device.dart';
 import 'package:flutter_tools/src/android/android_sdk.dart' show AndroidSdk;
@@ -342,17 +342,56 @@
   Future<void> flush() async { }
 }
 
+class MemoryStdout extends MemoryIOSink implements io.Stdout {
+  @override
+  bool get hasTerminal => _hasTerminal;
+  set hasTerminal(bool value) {
+    assert(value != null);
+    _hasTerminal = value;
+  }
+  bool _hasTerminal = true;
+
+  @override
+  io.IOSink get nonBlocking => this;
+
+  @override
+  bool get supportsAnsiEscapes => _supportsAnsiEscapes;
+  set supportsAnsiEscapes(bool value) {
+    assert(value != null);
+    _supportsAnsiEscapes = value;
+  }
+  bool _supportsAnsiEscapes = true;
+
+  @override
+  int get terminalColumns {
+    if (_terminalColumns != null)
+      return _terminalColumns;
+    throw const io.StdoutException('unspecified mock value');
+  }
+  set terminalColumns(int value) => _terminalColumns = value;
+  int _terminalColumns;
+
+  @override
+  int get terminalLines {
+    if (_terminalLines != null)
+      return _terminalLines;
+    throw const io.StdoutException('unspecified mock value');
+  }
+  set terminalLines(int value) => _terminalLines = value;
+  int _terminalLines;
+}
+
 /// A Stdio that collects stdout and supports simulated stdin.
 class MockStdio extends Stdio {
-  final MemoryIOSink _stdout = MemoryIOSink();
+  final MemoryStdout _stdout = MemoryStdout();
   final MemoryIOSink _stderr = MemoryIOSink();
   final StreamController<List<int>> _stdin = StreamController<List<int>>();
 
   @override
-  IOSink get stdout => _stdout;
+  MemoryStdout get stdout => _stdout;
 
   @override
-  IOSink get stderr => _stderr;
+  MemoryIOSink get stderr => _stderr;
 
   @override
   Stream<List<int>> get stdin => _stdin.stream;