Only output errors during implicit resolution (#3689)

diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart
index 13c6e70..84f3921 100644
--- a/lib/src/command/dependency_services.dart
+++ b/lib/src/command/dependency_services.dart
@@ -448,7 +448,7 @@
             cache.sources,
             filePath: entrypoint.lockFilePath,
           );
-    await log.warningsOnlyUnlessTerminal(
+    await log.errorsOnlyUnlessTerminal(
       () async {
         final updatedPubspec = pubspecEditor.toString();
         // Resolve versions, this will update transitive dependencies that were
diff --git a/lib/src/command/global_run.dart b/lib/src/command/global_run.dart
index e64e811..22f05c7 100644
--- a/lib/src/command/global_run.dart
+++ b/lib/src/command/global_run.dart
@@ -78,7 +78,7 @@
       args,
       vmArgs: vmArgs,
       enableAsserts: argResults['enable-asserts'] || argResults['checked'],
-      recompile: (executable) => log.warningsOnlyUnlessTerminal(
+      recompile: (executable) => log.errorsOnlyUnlessTerminal(
           () => globalEntrypoint.precompileExecutable(executable)),
       alwaysUseSubprocess: alwaysUseSubprocess,
     );
diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart
index 7090412..6ec403b 100644
--- a/lib/src/command/lish.dart
+++ b/lib/src/command/lish.dart
@@ -219,7 +219,7 @@
   @override
   Future runProtected() async {
     if (argResults.wasParsed('server')) {
-      await log.warningsOnlyUnlessTerminal(() {
+      await log.errorsOnlyUnlessTerminal(() {
         log.message(
           '''
 The --server option is deprecated. Use `publish_to` in your pubspec.yaml or set
diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart
index 0ae3ab3..50422e7 100644
--- a/lib/src/command/outdated.dart
+++ b/lib/src/command/outdated.dart
@@ -4,7 +4,6 @@
 
 import 'dart:async';
 import 'dart:convert';
-import 'dart:io';
 import 'dart:math';
 
 import 'package:collection/collection.dart' show IterableExtension;
@@ -39,7 +38,7 @@
 
   /// Avoid showing spinning progress messages when not in a terminal, and
   /// when we are outputting machine-readable json.
-  bool get _shouldShowSpinner => stdout.hasTerminal && !argResults['json'];
+  bool get _shouldShowSpinner => terminalOutputForStdout && !argResults['json'];
 
   @override
   bool get takesArguments => false;
diff --git a/lib/src/command/run.dart b/lib/src/command/run.dart
index fd38930..3deaf76 100644
--- a/lib/src/command/run.dart
+++ b/lib/src/command/run.dart
@@ -53,7 +53,7 @@
   @override
   Future<void> runProtected() async {
     if (deprecated) {
-      await log.warningsOnlyUnlessTerminal(() {
+      await log.errorsOnlyUnlessTerminal(() {
         log.message('Deprecated. Use `dart run` instead.');
       });
     }
@@ -96,7 +96,7 @@
       Executable.adaptProgramName(package, executable),
       args,
       enableAsserts: argResults['enable-asserts'] || argResults['checked'],
-      recompile: (executable) => log.warningsOnlyUnlessTerminal(
+      recompile: (executable) => log.errorsOnlyUnlessTerminal(
           () => entrypoint.precompileExecutable(executable)),
       vmArgs: vmArgs,
       alwaysUseSubprocess: alwaysUseSubprocess,
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart
index defa20b..1fe147e 100644
--- a/lib/src/command/upgrade.dart
+++ b/lib/src/command/upgrade.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:io';
 
 import 'package:pub_semver/pub_semver.dart';
 import 'package:yaml_edit/yaml_edit.dart';
@@ -74,7 +73,7 @@
   }
 
   /// Avoid showing spinning progress messages when not in a terminal.
-  bool get _shouldShowSpinner => stdout.hasTerminal;
+  bool get _shouldShowSpinner => terminalOutputForStdout;
 
   bool get _dryRun => argResults['dry-run'];
 
diff --git a/lib/src/executable.dart b/lib/src/executable.dart
index 4c823d6..fecc272 100644
--- a/lib/src/executable.dart
+++ b/lib/src/executable.dart
@@ -310,7 +310,7 @@
   } on DataException catch (e) {
     log.fine('Resolution not up to date: ${e.message}. Redoing.');
     try {
-      await warningsOnlyUnlessTerminal(
+      await errorsOnlyUnlessTerminal(
         () => entrypoint.acquireDependencies(
           SolveType.get,
           analytics: analytics,
@@ -368,7 +368,7 @@
     if (!fileExists(snapshotPath) ||
         entrypoint.packageGraph.isPackageMutable(package)) {
       try {
-        await warningsOnlyUnlessTerminal(
+        await errorsOnlyUnlessTerminal(
           () => entrypoint.precompileExecutable(
             executable,
             additionalSources: additionalSources,
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 46d910c..7252803 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -27,6 +27,30 @@
 
 export 'package:http/http.dart' show ByteStream;
 
+/// Environment variable names that are recognized by pub.
+class EnvironmentKeys {
+  /// Overrides terminal detection for stdout.
+  ///
+  /// Supported values:
+  /// * missing or `''` (empty string): dart:io terminal detection is used.
+  /// * `"0"`: output as if no terminal is attached
+  ///   - no animations
+  ///   - no ANSI colors
+  ///   - use unicode characters
+  ///   - silent inside [log.errorsOnlyUnlessTerminal]).
+  /// * `"1"`: output as if a terminal is attached
+  ///   - animations
+  ///   - ANSI colors (can be overriden again with NO_COLOR)
+  ///   - no unicode on Windows
+  ///   - normal verbosity in output inside
+  ///   [log.errorsOnlyUnlessTerminal]).
+  ///
+  /// This variable is mainly for testing, and no forward compatibility
+  /// guarantees are given.
+  static const forceTerminalOutput = '_PUB_FORCE_TERMINAL_OUTPUT';
+  // TODO(sigurdm): Add other environment keys here.
+}
+
 /// The pool used for restricting access to asynchronous operations that consume
 /// file descriptors.
 ///
@@ -639,6 +663,25 @@
   }
 }
 
+/// Returns `true` if [stdout] should be treated as a terminal.
+///
+/// The detected behaviour can be overridden with the environment variable
+/// [EnvironmentKeys.forceTerminalOutput].
+bool get terminalOutputForStdout {
+  final environmentValue =
+      Platform.environment[EnvironmentKeys.forceTerminalOutput];
+  if (environmentValue == null || environmentValue == '') {
+    return stdout.hasTerminal;
+  } else if (environmentValue == '0') {
+    return false;
+  } else if (environmentValue == '1') {
+    return true;
+  } else {
+    throw DataException(
+        'Environment variable ${EnvironmentKeys.forceTerminalOutput} has unsupported value: $environmentValue.');
+  }
+}
+
 /// Flushes the stdout and stderr streams, then exits the program with the given
 /// status code.
 ///
diff --git a/lib/src/log.dart b/lib/src/log.dart
index 61c6768..d8937f3 100644
--- a/lib/src/log.dart
+++ b/lib/src/log.dart
@@ -410,10 +410,10 @@
 /// Unless the user has overriden the verbosity,
 ///
 /// This is useful to not pollute stdout when the output is piped somewhere.
-Future<T> warningsOnlyUnlessTerminal<T>(FutureOr<T> Function() callback) async {
+Future<T> errorsOnlyUnlessTerminal<T>(FutureOr<T> Function() callback) async {
   final oldVerbosity = verbosity;
-  if (verbosity == Verbosity.normal && !stdout.hasTerminal) {
-    verbosity = Verbosity.warning;
+  if (verbosity == Verbosity.normal && !terminalOutputForStdout) {
+    verbosity = Verbosity.error;
   }
   final result = await callback();
   verbosity = oldVerbosity;
diff --git a/lib/src/progress.dart b/lib/src/progress.dart
index b9c6931..a127ae5 100644
--- a/lib/src/progress.dart
+++ b/lib/src/progress.dart
@@ -5,6 +5,7 @@
 import 'dart:async';
 import 'dart:io';
 
+import 'io.dart';
 import 'log.dart' as log;
 import 'utils.dart';
 
@@ -39,9 +40,8 @@
     // The animation is only shown when it would be meaningful to a human.
     // That means we're writing a visible message to a TTY at normal log levels
     // with non-JSON output.
-    if (stdioType(stdout) != StdioType.terminal ||
+    if (terminalOutputForStdout ||
         !log.verbosity.isLevelVisible(level) ||
-        log.json.enabled ||
         fine ||
         log.verbosity.isLevelVisible(log.Level.fine)) {
       // Not animating, so just log the start and wait until the task is
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 0719bbb..72f8201 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -418,7 +418,7 @@
       return false;
     case ForceColorOption.auto:
       return (!Platform.environment.containsKey('NO_COLOR')) &&
-          stdout.hasTerminal &&
+          terminalOutputForStdout &&
           stdout.supportsAnsiEscapes;
   }
 }
@@ -439,7 +439,7 @@
     // The tests support unicode also on windows.
     runningFromTest ||
     // When not outputting to terminal we can also use unicode.
-    !stdout.hasTerminal ||
+    !terminalOutputForStdout ||
     !Platform.isWindows ||
     Platform.environment.containsKey('WT_SESSION');
 
diff --git a/test/embedding/embedding_test.dart b/test/embedding/embedding_test.dart
index 3aaf53b..16d2e7d 100644
--- a/test/embedding/embedding_test.dart
+++ b/test/embedding/embedding_test.dart
@@ -7,6 +7,7 @@
 
 import 'package:path/path.dart' as path;
 import 'package:path/path.dart' as p;
+import 'package:pub/src/io.dart' show EnvironmentKeys;
 import 'package:test/test.dart';
 import 'package:test_process/test_process.dart';
 
@@ -314,6 +315,69 @@
       ),
     );
   });
+
+  test('`embedding run` does not have output when successful and no terminal',
+      () async {
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'dependencies': {'foo': '^1.0.0'}
+      }),
+      d.dir('bin', [
+        d.file('myapp.dart', 'main() {print(42);}'),
+      ])
+    ]).create();
+
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+
+    final buffer = StringBuffer();
+    await runEmbeddingToBuffer(
+      ['run', 'myapp'],
+      buffer,
+      workingDirectory: d.path(appPath),
+      environment: {EnvironmentKeys.forceTerminalOutput: '0'},
+    );
+
+    expect(
+      buffer.toString(),
+      allOf(
+        isNot(contains('Resolving dependencies...')),
+        contains('42'),
+      ),
+    );
+  });
+  test('`embedding run` outputs info when successful and has a terminal',
+      () async {
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'dependencies': {'foo': '^1.0.0'}
+      }),
+      d.dir('bin', [
+        d.file('myapp.dart', 'main() {print(42);}'),
+      ])
+    ]).create();
+
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+
+    final buffer = StringBuffer();
+    await runEmbeddingToBuffer(
+      ['run', 'myapp'],
+      buffer,
+      workingDirectory: d.path(appPath),
+      environment: {EnvironmentKeys.forceTerminalOutput: '1'},
+    );
+    expect(
+      buffer.toString(),
+      allOf(
+        contains('Resolving dependencies'),
+        contains('+ foo 1.0.0'),
+        contains('42'),
+      ),
+    );
+  });
 }
 
 String _filter(String input) {
diff --git a/test/testdata/goldens/embedding/embedding_test/--color forces colors.txt b/test/testdata/goldens/embedding/embedding_test/--color forces colors.txt
index e4ad1d1..ed56eb3 100644
--- a/test/testdata/goldens/embedding/embedding_test/--color forces colors.txt
+++ b/test/testdata/goldens/embedding/embedding_test/--color forces colors.txt
@@ -1,14 +1,14 @@
 # GENERATED BY: test/embedding/embedding_test.dart
 
 $ tool/test-bin/pub_command_runner.dart pub --no-color get
-Resolving dependencies...
+Resolving dependencies... 
 + foo 1.0.0 (2.0.0 available)
 Changed 1 dependency!
 
 -------------------------------- END OF OUTPUT ---------------------------------
 
 $ tool/test-bin/pub_command_runner.dart pub --color get
-Resolving dependencies...
+Resolving dependencies... 
   foo 1.0.0 (2.0.0 available)
 Got dependencies!
 
diff --git a/test/testdata/goldens/embedding/embedding_test/run works, though hidden.txt b/test/testdata/goldens/embedding/embedding_test/run works, though hidden.txt
index 214712a..8eea7f3 100644
--- a/test/testdata/goldens/embedding/embedding_test/run works, though hidden.txt
+++ b/test/testdata/goldens/embedding/embedding_test/run works, though hidden.txt
@@ -1,7 +1,7 @@
 # GENERATED BY: test/embedding/embedding_test.dart
 
 $ tool/test-bin/pub_command_runner.dart pub get
-Resolving dependencies...
+Resolving dependencies... 
 Got dependencies!
 
 -------------------------------- END OF OUTPUT ---------------------------------