From f9765c136f9138581b69e5875b1c57d10c94022f Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Mon, 23 May 2022 22:57:34 -0700 Subject: [PATCH] [tool] Add tests for FakeProcessManager (#104456) Adds a bit more clarifying documentation to the implementation of the outputFollowsExit case, and adds tests that verify the behaviour of stderr, stdout of processes launched via FakeProcessManager. Specifically: * Verifies that stderr, stdout are not emitted immediately after process exit if outputFollowsExit is true. They must be emitted at least one turn through the event loop later. * Verifies that ProcessResult.stderr, stdout have the type documented according to the encoding passted to Process.run/runSync: * List if null is passed as the encoding. * String (in the default system encoding) if no encoding is specified. * String (in the specified encoding) if an encoding is specified. This is additional testing relating to refactoring landed in: https://github.com/flutter/flutter/pull/103947 Issue: https://github.com/flutter/flutter/issues/102451 --- .../fake_process_manager_test.dart | 148 ++++++++++++++++++ .../test/src/fake_process_manager.dart | 4 + 2 files changed, 152 insertions(+) diff --git a/packages/flutter_tools/test/general.shard/fake_process_manager_test.dart b/packages/flutter_tools/test/general.shard/fake_process_manager_test.dart index f2d5154848..bfa6478613 100644 --- a/packages/flutter_tools/test/general.shard/fake_process_manager_test.dart +++ b/packages/flutter_tools/test/general.shard/fake_process_manager_test.dart @@ -3,7 +3,10 @@ // 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'; @@ -172,4 +175,149 @@ void main() { 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: ['faketool'])); + + final Process process = await manager.start(['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: ['faketool'], + stderr: 'hello', + stdout: 'world', + outputFollowsExit: true, + )); + + final List stderrBytes = []; + final List stdoutBytes = []; + + // Start the process. + final Process process = await manager.start(['faketool']); + final StreamSubscription> stderrSubscription = process.stderr.listen((List chunk) { stderrBytes.addAll(chunk); }); + final StreamSubscription> stdoutSubscription = process.stdout.listen((List 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(>[ + 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: ['faketool'])); + + final ProcessResult result = await manager.run(['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: ['faketool'])); + + final ProcessResult result = await manager.run(['faketool']); + expect(result.exitCode, 0); + expect(result.stdout, isA()); + expect(result.stderr, isA()); + }); + + testWithoutContext('stderr, stdout are List if encoding is null', () async { + manager.addCommand(const FakeCommand(command: ['faketool'])); + + final ProcessResult result = await manager.run( + ['faketool'], + stderrEncoding: null, + stdoutEncoding: null, + ); + expect(result.exitCode, 0); + expect(result.stdout, isA>()); + expect(result.stderr, isA>()); + }); + + testWithoutContext('stderr, stdout are String if encoding is specified', () async { + manager.addCommand(const FakeCommand(command: ['faketool'])); + + final ProcessResult result = await manager.run( + ['faketool'], + stderrEncoding: utf8, + stdoutEncoding: utf8, + ); + expect(result.exitCode, 0); + expect(result.stdout, isA()); + expect(result.stderr, isA()); + }); + }); + + group('runSync', () { + testWithoutContext('can run a fake command', () { + manager.addCommand(const FakeCommand(command: ['faketool'])); + + final ProcessResult result = manager.runSync(['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: ['faketool'])); + + final ProcessResult result = manager.runSync(['faketool']); + expect(result.exitCode, 0); + expect(result.stdout, isA()); + expect(result.stderr, isA()); + }); + + testWithoutContext('stderr, stdout are List if encoding is null', () { + manager.addCommand(const FakeCommand(command: ['faketool'])); + + final ProcessResult result = manager.runSync( + ['faketool'], + stderrEncoding: null, + stdoutEncoding: null, + ); + expect(result.exitCode, 0); + expect(result.stdout, isA>()); + expect(result.stderr, isA>()); + }); + + testWithoutContext('stderr, stdout are String if encoding is specified', () { + manager.addCommand(const FakeCommand(command: ['faketool'])); + + final ProcessResult result = manager.runSync( + ['faketool'], + stderrEncoding: utf8, + stdoutEncoding: utf8, + ); + expect(result.exitCode, 0); + expect(result.stdout, isA()); + expect(result.stderr, isA()); + }); + }); + }); } diff --git a/packages/flutter_tools/test/src/fake_process_manager.dart b/packages/flutter_tools/test/src/fake_process_manager.dart index db77d428f6..59194a0343 100644 --- a/packages/flutter_tools/test/src/fake_process_manager.dart +++ b/packages/flutter_tools/test/src/fake_process_manager.dart @@ -158,6 +158,8 @@ class FakeProcess implements io.Process { } else if (outputFollowsExit) { // Wait for the process to exit before emitting stderr. this.stderr = Stream>.fromFuture(this.exitCode.then((_) { + // Return a Future so stderr isn't immediately available to those who + // await exitCode, but is available asynchronously later. return Future>(() => _stderr); })); } else { @@ -169,6 +171,8 @@ class FakeProcess implements io.Process { } else if (outputFollowsExit) { // Wait for the process to exit before emitting stdout. this.stdout = Stream>.fromFuture(this.exitCode.then((_) { + // Return a Future so stdout isn't immediately available to those who + // await exitCode, but is available asynchronously later. return Future>(() => _stdout); })); } else {