// Copyright 2014 The Flutter 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:file/memory.dart'; import 'package:flutter_tools/src/base/io.dart' as io; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/terminal.dart'; import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fakes.dart'; void main() { group('process exceptions', () { late FakeProcessManager fakeProcessManager; late ProcessUtils processUtils; setUp(() { fakeProcessManager = FakeProcessManager.empty(); processUtils = ProcessUtils(processManager: fakeProcessManager, logger: BufferLogger.test()); }); testWithoutContext( 'runAsync throwOnError: exceptions should be ProcessException objects', () async { fakeProcessManager.addCommand(const FakeCommand(command: ['false'], exitCode: 1)); expect( () async => processUtils.run(['false'], throwOnError: true), throwsProcessException(message: 'Process exited abnormally with exit code 1'), ); }, ); }); group('shutdownHooks', () { testWithoutContext('runInExpectedOrder', () async { int i = 1; int? cleanup; final ShutdownHooks shutdownHooks = ShutdownHooks(); shutdownHooks.addShutdownHook(() async { cleanup = i++; }); await shutdownHooks.runShutdownHooks(BufferLogger.test()); expect(cleanup, 1); }); }); group('output formatting', () { late FakeProcessManager processManager; late ProcessUtils processUtils; late BufferLogger logger; setUp(() { processManager = FakeProcessManager.empty(); logger = BufferLogger.test(); processUtils = ProcessUtils(processManager: processManager, logger: logger); }); testWithoutContext('Command output is not wrapped.', () async { final List testString = ['0123456789' * 10]; processManager.addCommand( FakeCommand( command: const ['command'], stdout: testString.join(), stderr: testString.join(), ), ); await processUtils.stream(['command']); expect(logger.statusText, equals('${testString[0]}\n')); expect(logger.errorText, equals('${testString[0]}\n')); }); testWithoutContext('Command output is filtered by mapFunction', () async { processManager.addCommand( const FakeCommand( command: ['command'], stdout: 'match\nno match', stderr: 'match\nno match', ), ); await processUtils.stream( ['command'], mapFunction: (String line) { if (line == 'match') { return line; } return null; }, ); expect(logger.statusText, equals('match\n')); expect(logger.errorText, equals('match\n')); }); }); group('run', () { late FakeProcessManager fakeProcessManager; late ProcessUtils processUtils; setUp(() { fakeProcessManager = FakeProcessManager.empty(); processUtils = ProcessUtils(processManager: fakeProcessManager, logger: BufferLogger.test()); }); testWithoutContext(' succeeds on success', () async { fakeProcessManager.addCommand(const FakeCommand(command: ['whoohoo'])); expect((await processUtils.run(['whoohoo'])).exitCode, 0); }); testWithoutContext(' fails on failure', () async { fakeProcessManager.addCommand(const FakeCommand(command: ['boohoo'], exitCode: 1)); expect((await processUtils.run(['boohoo'])).exitCode, 1); }); testWithoutContext(' throws on failure with throwOnError', () async { fakeProcessManager.addCommand(const FakeCommand(command: ['kaboom'], exitCode: 1)); expect( () => processUtils.run(['kaboom'], throwOnError: true), throwsProcessException(), ); }); testWithoutContext(' does not throw on allowed Failures', () async { fakeProcessManager.addCommand(const FakeCommand(command: ['kaboom'], exitCode: 1)); expect( (await processUtils.run( ['kaboom'], throwOnError: true, allowedFailures: (int c) => c == 1, )).exitCode, 1, ); }); testWithoutContext(' throws on disallowed failure', () async { fakeProcessManager.addCommand(const FakeCommand(command: ['kaboom'], exitCode: 2)); expect( () => processUtils.run( ['kaboom'], throwOnError: true, allowedFailures: (int c) => c == 1, ), throwsProcessException(), ); }); }); group('runSync', () { late FakeProcessManager fakeProcessManager; late ProcessUtils processUtils; late BufferLogger testLogger; setUp(() { fakeProcessManager = FakeProcessManager.empty(); testLogger = BufferLogger( terminal: AnsiTerminal(stdio: FakeStdio(), platform: FakePlatform()), outputPreferences: OutputPreferences(wrapText: true, wrapColumn: 40), ); processUtils = ProcessUtils(processManager: fakeProcessManager, logger: testLogger); }); testWithoutContext(' succeeds on success', () async { fakeProcessManager.addCommand(const FakeCommand(command: ['whoohoo'])); expect(processUtils.runSync(['whoohoo']).exitCode, 0); }); testWithoutContext(' fails on failure', () async { fakeProcessManager.addCommand(const FakeCommand(command: ['boohoo'], exitCode: 1)); expect(processUtils.runSync(['boohoo']).exitCode, 1); }); testWithoutContext('throws on failure with throwOnError', () async { const String stderr = 'Something went wrong.'; fakeProcessManager.addCommand( const FakeCommand(command: ['kaboom'], exitCode: 1, stderr: stderr), ); expect( () => processUtils.runSync(['kaboom'], throwOnError: true), throwsA( isA().having( (ProcessException error) => error.message, 'message', isNot(contains(stderr)), ), ), ); }); testWithoutContext( 'throws with stderr in exception on failure with verboseExceptions', () async { const String stderr = 'Something went wrong.'; fakeProcessManager.addCommand( const FakeCommand(command: ['verybad'], exitCode: 1, stderr: stderr), ); expect( () => processUtils.runSync( ['verybad'], throwOnError: true, verboseExceptions: true, ), throwsProcessException(message: stderr), ); }, ); testWithoutContext(' does not throw on allowed Failures', () async { fakeProcessManager.addCommand(const FakeCommand(command: ['kaboom'], exitCode: 1)); expect( processUtils .runSync(['kaboom'], throwOnError: true, allowedFailures: (int c) => c == 1) .exitCode, 1, ); }); testWithoutContext(' throws on disallowed failure', () async { fakeProcessManager.addCommand(const FakeCommand(command: ['kaboom'], exitCode: 2)); expect( () => processUtils.runSync( ['kaboom'], throwOnError: true, allowedFailures: (int c) => c == 1, ), throwsProcessException(), ); }); testWithoutContext(' prints stdout and stderr to trace on success', () async { fakeProcessManager.addCommand( const FakeCommand(command: ['whoohoo'], stdout: 'stdout', stderr: 'stderr'), ); expect(processUtils.runSync(['whoohoo']).exitCode, 0); expect(testLogger.traceText, contains('stdout')); expect(testLogger.traceText, contains('stderr')); }); testWithoutContext( ' prints stdout to status and stderr to error on failure with throwOnError', () async { fakeProcessManager.addCommand( const FakeCommand( command: ['kaboom'], exitCode: 1, stdout: 'stdout', stderr: 'stderr', ), ); expect( () => processUtils.runSync(['kaboom'], throwOnError: true), throwsProcessException(), ); expect(testLogger.statusText, contains('stdout')); expect(testLogger.errorText, contains('stderr')); }, ); testWithoutContext(' does not print stdout with hideStdout', () async { fakeProcessManager.addCommand( const FakeCommand(command: ['whoohoo'], stdout: 'stdout', stderr: 'stderr'), ); expect(processUtils.runSync(['whoohoo'], hideStdout: true).exitCode, 0); expect(testLogger.traceText.contains('stdout'), isFalse); expect(testLogger.traceText, contains('stderr')); }); }); group('exitsHappySync', () { late FakeProcessManager processManager; late ProcessUtils processUtils; setUp(() { processManager = FakeProcessManager.empty(); processUtils = ProcessUtils(processManager: processManager, logger: BufferLogger.test()); }); testWithoutContext('succeeds on success', () async { processManager.addCommand(const FakeCommand(command: ['whoohoo'])); expect(processUtils.exitsHappySync(['whoohoo']), isTrue); }); testWithoutContext('fails on failure', () async { processManager.addCommand(const FakeCommand(command: ['boohoo'], exitCode: 1)); expect(processUtils.exitsHappySync(['boohoo']), isFalse); }); testWithoutContext('catches Exception and returns false', () { processManager.addCommand( const FakeCommand( command: ['boohoo'], exception: ProcessException('Process failed', []), ), ); expect(processUtils.exitsHappySync(['boohoo']), isFalse); }); testWithoutContext('does not throw Exception and returns false if binary cannot run', () { processManager.excludedExecutables.add('nonesuch'); expect(processUtils.exitsHappySync(['nonesuch']), isFalse); }); testWithoutContext('does not catch ArgumentError', () async { processManager.addCommand( FakeCommand(command: const ['invalid'], exception: ArgumentError('Bad input')), ); expect(() => processUtils.exitsHappySync(['invalid']), throwsArgumentError); }); }); group('exitsHappy', () { late FakeProcessManager processManager; late ProcessUtils processUtils; setUp(() { processManager = FakeProcessManager.empty(); processUtils = ProcessUtils(processManager: processManager, logger: BufferLogger.test()); }); testWithoutContext('succeeds on success', () async { processManager.addCommand(const FakeCommand(command: ['whoohoo'])); expect(await processUtils.exitsHappy(['whoohoo']), isTrue); }); testWithoutContext('fails on failure', () async { processManager.addCommand(const FakeCommand(command: ['boohoo'], exitCode: 1)); expect(await processUtils.exitsHappy(['boohoo']), isFalse); }); testWithoutContext('catches Exception and returns false', () async { processManager.addCommand( const FakeCommand( command: ['boohoo'], exception: ProcessException('Process failed', []), ), ); expect(await processUtils.exitsHappy(['boohoo']), isFalse); }); testWithoutContext('does not throw Exception and returns false if binary cannot run', () async { processManager.excludedExecutables.add('nonesuch'); expect(await processUtils.exitsHappy(['nonesuch']), isFalse); }); testWithoutContext('does not catch ArgumentError', () async { processManager.addCommand( FakeCommand(command: const ['invalid'], exception: ArgumentError('Bad input')), ); expect(() async => processUtils.exitsHappy(['invalid']), throwsArgumentError); }); }); group('writeToStdinGuarded', () { testWithoutContext('handles any error thrown by stdin.flush', () async { final _ThrowsOnFlushIOSink stdin = _ThrowsOnFlushIOSink(); Object? errorPassedToCallback; await ProcessUtils.writeToStdinGuarded( stdin: stdin, content: 'message to stdin', onError: (Object error, StackTrace stackTrace) { errorPassedToCallback = error; }, ); expect( errorPassedToCallback, isNotNull, reason: 'onError callback should have been invoked.', ); expect(errorPassedToCallback, const TypeMatcher()); }); }); group('exitWithHooks', () { late MemoryFileSystem fileSystem; late BufferLogger logger; late Analytics analytics; setUp(() { fileSystem = MemoryFileSystem.test(); logger = BufferLogger.test(); final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(); analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: fileSystem.currentDirectory, dartVersion: fakeFlutterVersion.dartSdkVersion, fs: fileSystem, flutterChannel: fakeFlutterVersion.channel, flutterVersion: fakeFlutterVersion.getVersionString(), ); }); testUsingContext( 'prints analytics welcome message', () async { io.setExitFunctionForTests((int exitCode) {}); final ShutdownHooks shutdownHooks = ShutdownHooks(); await exitWithHooks(0, shutdownHooks: shutdownHooks); expect(logger.statusText, contains(analytics.getConsentMessage)); }, overrides: {Analytics: () => analytics, Logger: () => logger}, ); testUsingContext( 'does not print analytics welcome message if Analytics instance indicates it should not be printed', () async { io.setExitFunctionForTests((int exitCode) {}); analytics.clientShowedMessage(); final ShutdownHooks shutdownHooks = ShutdownHooks(); await exitWithHooks(0, shutdownHooks: shutdownHooks); expect(logger.statusText, isNot(contains(analytics.getConsentMessage))); }, overrides: {Analytics: () => analytics, Logger: () => logger}, ); }); } class _ThrowsOnFlushIOSink extends MemoryIOSink { @override Future flush() async { throw const SocketException('Write failed', osError: OSError('Broken pipe', 32)); } }