// 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:fake_async/fake_async.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/convert.dart' show utf8; import '../src/common.dart'; import '../src/fake_process_manager.dart'; void main() { group(FakeProcess, () { testWithoutContext('exits with specified exit code', () async { final FakeProcess process = FakeProcess(exitCode: 42); expect(await process.exitCode, 42); }); testWithoutContext('exits with specified stderr, stdout', () async { final FakeProcess process = FakeProcess( stderr: 'stderr\u{FFFD}'.codeUnits, stdout: 'stdout\u{FFFD}'.codeUnits, ); await process.exitCode; // Verify that no encoding changes have been applied to output. // // In the past, we had hardcoded UTF-8 encoding for these streams in // FakeProcess. When a specific encoding is desired, it can be specified // on FakeCommand or in the encoding parameter of FakeProcessManager.run // or FakeProcessManager.runAsync. expect((await process.stderr.toList()).expand((List<int> x) => x), 'stderr\u{FFFD}'.codeUnits); expect((await process.stdout.toList()).expand((List<int> x) => x), 'stdout\u{FFFD}'.codeUnits); }); testWithoutContext('exits after specified delay (if no completer specified)', () { final bool done = FakeAsync().run<bool>((FakeAsync time) { final FakeProcess process = FakeProcess( duration: const Duration(seconds: 30), ); bool hasExited = false; unawaited(process.exitCode.then((int _) { hasExited = true; })); // Verify process hasn't exited before specified delay. time.elapse(const Duration(seconds: 15)); expect(hasExited, isFalse); // Verify process has exited after specified delay. time.elapse(const Duration(seconds: 20)); expect(hasExited, isTrue); return true; }); expect(done, isTrue); }); testWithoutContext('exits when completer completes (if no duration specified)', () { final bool done = FakeAsync().run<bool>((FakeAsync time) { final Completer<void> completer = Completer<void>(); final FakeProcess process = FakeProcess( completer: completer, ); bool hasExited = false; unawaited(process.exitCode.then((int _) { hasExited = true; })); // Verify process hasn't exited when all async tasks flushed. time.elapse(Duration.zero); expect(hasExited, isFalse); // Verify process has exited after completer completes. completer.complete(); time.flushMicrotasks(); expect(hasExited, isTrue); return true; }); expect(done, isTrue); }); testWithoutContext('when completer and duration are specified, does not exit until completer is completed', () { final bool done = FakeAsync().run<bool>((FakeAsync time) { final Completer<void> completer = Completer<void>(); final FakeProcess process = FakeProcess( duration: const Duration(seconds: 30), completer: completer, ); bool hasExited = false; unawaited(process.exitCode.then((int _) { hasExited = true; })); // Verify process hasn't exited before specified delay. time.elapse(const Duration(seconds: 15)); expect(hasExited, isFalse); // Verify process hasn't exited until the completer completes. time.elapse(const Duration(seconds: 20)); expect(hasExited, isFalse); // Verify process exits after the completer completes. completer.complete(); time.flushMicrotasks(); expect(hasExited, isTrue); return true; }); expect(done, isTrue); }); testWithoutContext('when completer and duration are specified, does not exit until duration has elapsed', () { final bool done = FakeAsync().run<bool>((FakeAsync time) { final Completer<void> completer = Completer<void>(); final FakeProcess process = FakeProcess( duration: const Duration(seconds: 30), completer: completer, ); bool hasExited = false; unawaited(process.exitCode.then((int _) { hasExited = true; })); // Verify process hasn't exited before specified delay. time.elapse(const Duration(seconds: 15)); expect(hasExited, isFalse); // Verify process does not exit until duration has elapsed. completer.complete(); expect(hasExited, isFalse); // Verify process exits after the duration elapses. time.elapse(const Duration(seconds: 20)); expect(hasExited, isTrue); return true; }); expect(done, isTrue); }); testWithoutContext('process exit is asynchronous', () async { final FakeProcess process = FakeProcess(); bool hasExited = false; unawaited(process.exitCode.then((int _) { hasExited = true; })); // Verify process hasn't completed. expect(hasExited, isFalse); // Flush async tasks. Verify process completes. await Future<void>.delayed(Duration.zero); expect(hasExited, isTrue); }); testWithoutContext('stderr, stdout stream data after exit when outputFollowsExit is true', () async { final FakeProcess process = FakeProcess( stderr: 'stderr'.codeUnits, stdout: 'stdout'.codeUnits, outputFollowsExit: true, ); final List<int> stderr = <int>[]; final List<int> stdout = <int>[]; process.stderr.listen(stderr.addAll); process.stdout.listen(stdout.addAll); // Ensure that no bytes have been received at process exit. await process.exitCode; expect(stderr, isEmpty); expect(stdout, isEmpty); // Flush all remaining async work. Ensure stderr, stdout is received. await Future<void>.delayed(Duration.zero); expect(stderr, 'stderr'.codeUnits); expect(stdout, 'stdout'.codeUnits); }); }); group(FakeProcessManager, () { late FakeProcessManager manager; setUp(() { manager = FakeProcessManager.empty(); }); group('start', () { testWithoutContext('can run a fake command', () async { manager.addCommand(const FakeCommand(command: <String>['faketool'])); final Process process = await manager.start(<String>['faketool']); expect(await process.exitCode, 0); expect(await utf8.decodeStream(process.stdout), isEmpty); expect(await utf8.decodeStream(process.stderr), isEmpty); }); testWithoutContext('outputFollowsExit delays stderr, stdout until after process exit', () async { manager.addCommand(const FakeCommand( command: <String>['faketool'], stderr: 'hello', stdout: 'world', outputFollowsExit: true, )); final List<int> stderrBytes = <int>[]; final List<int> stdoutBytes = <int>[]; // Start the process. final Process process = await manager.start(<String>['faketool']); final StreamSubscription<List<int>> stderrSubscription = process.stderr.listen((List<int> chunk) { stderrBytes.addAll(chunk); }); final StreamSubscription<List<int>> stdoutSubscription = process.stdout.listen((List<int> chunk) { stdoutBytes.addAll(chunk); }); // Immediately after exit, no output is emitted. await process.exitCode; expect(utf8.decode(stderrBytes), isEmpty); expect(utf8.decode(stdoutBytes), isEmpty); // Output is emitted asynchronously after process exit. await Future.wait(<Future<void>>[ stderrSubscription.asFuture(), stdoutSubscription.asFuture(), ]); expect(utf8.decode(stderrBytes), 'hello'); expect(utf8.decode(stdoutBytes), 'world'); // Clean up stream subscriptions. await stderrSubscription.cancel(); await stdoutSubscription.cancel(); }); }); group('run', () { testWithoutContext('can run a fake command', () async { manager.addCommand(const FakeCommand(command: <String>['faketool'])); final ProcessResult result = await manager.run(<String>['faketool']); expect(result.exitCode, 0); expect(result.stdout, isEmpty); expect(result.stderr, isEmpty); }); testWithoutContext('stderr, stdout are String if encoding is unspecified', () async { manager.addCommand(const FakeCommand(command: <String>['faketool'])); final ProcessResult result = await manager.run(<String>['faketool']); expect(result.exitCode, 0); expect(result.stdout, isA<String>()); expect(result.stderr, isA<String>()); }); testWithoutContext('stderr, stdout are List<int> if encoding is null', () async { manager.addCommand(const FakeCommand(command: <String>['faketool'])); final ProcessResult result = await manager.run( <String>['faketool'], stderrEncoding: null, stdoutEncoding: null, ); expect(result.exitCode, 0); expect(result.stdout, isA<List<int>>()); expect(result.stderr, isA<List<int>>()); }); testWithoutContext('stderr, stdout are String if encoding is specified', () async { manager.addCommand(const FakeCommand(command: <String>['faketool'])); final ProcessResult result = await manager.run( <String>['faketool'], stderrEncoding: utf8, stdoutEncoding: utf8, ); expect(result.exitCode, 0); expect(result.stdout, isA<String>()); expect(result.stderr, isA<String>()); }); }); group('runSync', () { testWithoutContext('can run a fake command', () { manager.addCommand(const FakeCommand(command: <String>['faketool'])); final ProcessResult result = manager.runSync(<String>['faketool']); expect(result.exitCode, 0); expect(result.stdout, isEmpty); expect(result.stderr, isEmpty); }); testWithoutContext('stderr, stdout are String if encoding is unspecified', () { manager.addCommand(const FakeCommand(command: <String>['faketool'])); final ProcessResult result = manager.runSync(<String>['faketool']); expect(result.exitCode, 0); expect(result.stdout, isA<String>()); expect(result.stderr, isA<String>()); }); testWithoutContext('stderr, stdout are List<int> if encoding is null', () { manager.addCommand(const FakeCommand(command: <String>['faketool'])); final ProcessResult result = manager.runSync( <String>['faketool'], stderrEncoding: null, stdoutEncoding: null, ); expect(result.exitCode, 0); expect(result.stdout, isA<List<int>>()); expect(result.stderr, isA<List<int>>()); }); testWithoutContext('stderr, stdout are String if encoding is specified', () { manager.addCommand(const FakeCommand(command: <String>['faketool'])); final ProcessResult result = manager.runSync( <String>['faketool'], stderrEncoding: utf8, stdoutEncoding: utf8, ); expect(result.exitCode, 0); expect(result.stdout, isA<String>()); expect(result.stderr, isA<String>()); }); }); }); }