Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 78 additions & 9 deletions packages/flutter_tools/lib/src/base/io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import 'dart:io' as io
Stdin,
StdinException,
Stdout,
StdoutException,
stdout;

import 'package:meta/meta.dart';
Expand Down Expand Up @@ -208,16 +209,64 @@ class _PosixProcessSignal extends ProcessSignal {

/// A class that wraps stdout, stderr, and stdin, and exposes the allowed
/// operations.
///
/// In particular, there are three ways that writing to stdout and stderr
/// can fail. A call to stdout.write() can fail:
/// * by throwing a regular synchronous exception,
/// * by throwing an exception asynchronously, and
/// * by completing the Future stdout.done with an error.
///
/// This class enapsulates all three so that we don't have to worry about it
/// anywhere else.
class Stdio {
const Stdio();
Stdio();

/// Tests can provide overrides to use instead of the stdout and stderr from
/// dart:io.
@visibleForTesting
Stdio.test({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this pattern

@required io.Stdout stdout,
@required io.IOSink stderr,
}) : _stdoutOverride = stdout, _stderrOverride = stderr;

io.Stdout _stdoutOverride;
io.IOSink _stderrOverride;

// These flags exist to remember when the done Futures on stdout and stderr
// complete to avoid trying to write to a closed stream sink, which would
// generate a [StateError].
bool _stdoutDone = false;
bool _stderrDone = false;

Stream<List<int>> get stdin => io.stdin;

@visibleForTesting
io.Stdout get stdout => io.stdout;
io.Stdout get stdout {
if (_stdout != null) {
return _stdout;
}
_stdout = _stdoutOverride ?? io.stdout;
_stdout.done.then(
(void _) { _stdoutDone = true; },
onError: (Object err, StackTrace st) { _stdoutDone = true; },
);
return _stdout;
}
io.Stdout _stdout;

@visibleForTesting
io.IOSink get stderr => io.stderr;
io.IOSink get stderr {
if (_stderr != null) {
return _stderr;
}
_stderr = _stderrOverride ?? io.stderr;
_stderr.done.then(
(void _) { _stderrDone = true; },
onError: (Object err, StackTrace st) { _stderrDone = true; },
);
return _stderr;
}
io.IOSink _stderr;

bool get hasTerminal => io.stdout.hasTerminal;

Expand Down Expand Up @@ -250,25 +299,45 @@ class Stdio {
return _stdinHasTerminal = true;
}

int get terminalColumns => hasTerminal ? io.stdout.terminalColumns : null;
int get terminalLines => hasTerminal ? io.stdout.terminalLines : null;
bool get supportsAnsiEscapes => hasTerminal && io.stdout.supportsAnsiEscapes;
int get terminalColumns => hasTerminal ? stdout.terminalColumns : null;
int get terminalLines => hasTerminal ? stdout.terminalLines : null;
bool get supportsAnsiEscapes => hasTerminal && stdout.supportsAnsiEscapes;

/// Writes [message] to [stderr], falling back on [fallback] if the write
/// throws any exception. The default fallback calls [print] on [message].
void stderrWrite(
String message, {
void Function(String, dynamic, StackTrace) fallback,
}) => _stdioWrite(stderr, message, fallback: fallback);
}) {
if (!_stderrDone) {
_stdioWrite(stderr, message, fallback: fallback);
return;
}
fallback == null ? print(message) : fallback(
message,
const io.StdoutException('stderr is done'),
StackTrace.current,
);
}

/// Writes [message] to [stdout], falling back on [fallback] if the write
/// throws any exception. The default fallback calls [print] on [message].
void stdoutWrite(
String message, {
void Function(String, dynamic, StackTrace) fallback,
}) => _stdioWrite(stdout, message, fallback: fallback);
}) {
if (!_stdoutDone) {
_stdioWrite(stdout, message, fallback: fallback);
return;
}
fallback == null ? print(message) : fallback(
message,
const io.StdoutException('stdout is done'),
StackTrace.current,
);
}

// Helper for safeStderrWrite and safeStdoutWrite.
// Helper for [stderrWrite] and [stdoutWrite].
void _stdioWrite(io.IOSink sink, String message, {
void Function(String, dynamic, StackTrace) fallback,
}) {
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter_tools/lib/src/context_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ Future<T> runInContext<T>(
ShutdownHooks: () => ShutdownHooks(logger: globals.logger),
Signals: () => Signals(),
SimControl: () => SimControl(),
Stdio: () => const Stdio(),
Stdio: () => Stdio(),
SystemClock: () => const SystemClock(),
TimeoutConfiguration: () => const TimeoutConfiguration(),
Usage: () => Usage(
Expand Down
16 changes: 9 additions & 7 deletions packages/flutter_tools/lib/src/globals.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,16 @@ final AnsiTerminal _defaultAnsiTerminal = AnsiTerminal(
);

/// The global Stdio wrapper.
Stdio get stdio => context.get<Stdio>() ?? const Stdio();

PlistParser get plistParser => context.get<PlistParser>() ?? (_defaultInstance ??= PlistParser(
fileSystem: fs,
processManager: processManager,
logger: logger,
Stdio get stdio => context.get<Stdio>() ?? (_stdioInstance ??= Stdio());
Stdio _stdioInstance;

PlistParser get plistParser => context.get<PlistParser>() ?? (
_plistInstance ??= PlistParser(
fileSystem: fs,
processManager: processManager,
logger: logger,
));
PlistParser _defaultInstance;
PlistParser _plistInstance;

/// The [ChromeLauncher] instance.
ChromeLauncher get chromeLauncher => context.get<ChromeLauncher>();
Expand Down
59 changes: 47 additions & 12 deletions packages/flutter_tools/test/general.shard/base/logger_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,6 @@ final String resetColor = RegExp.escape(AnsiTerminal.resetColor);

class MockStdout extends Mock implements Stdout {}

class ThrowingStdio extends Stdio {
ThrowingStdio(this.stdout, this.stderr);

@override
final Stdout stdout;

@override
final IOSink stderr;
}

void main() {
group('AppContext', () {
FakeStopwatch fakeStopWatch;
Expand Down Expand Up @@ -94,9 +84,11 @@ void main() {
testWithoutContext('Logger does not throw when stdio write throws synchronously', () async {
final MockStdout stdout = MockStdout();
final MockStdout stderr = MockStdout();
final ThrowingStdio stdio = ThrowingStdio(stdout, stderr);
final Stdio stdio = Stdio.test(stdout: stdout, stderr: stderr);
bool stdoutThrew = false;
bool stderrThrew = false;
final Completer<void> stdoutError = Completer<void>();
final Completer<void> stderrError = Completer<void>();
when(stdout.write(any)).thenAnswer((_) {
stdoutThrew = true;
throw 'Error';
Expand All @@ -105,6 +97,8 @@ void main() {
stderrThrew = true;
throw 'Error';
});
when(stdout.done).thenAnswer((_) => stdoutError.future);
when(stderr.done).thenAnswer((_) => stderrError.future);
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: stdio,
Expand All @@ -123,7 +117,9 @@ void main() {
testWithoutContext('Logger does not throw when stdio write throws asynchronously', () async {
final MockStdout stdout = MockStdout();
final MockStdout stderr = MockStdout();
final ThrowingStdio stdio = ThrowingStdio(stdout, stderr);
final Stdio stdio = Stdio.test(stdout: stdout, stderr: stderr);
final Completer<void> stdoutError = Completer<void>();
final Completer<void> stderrError = Completer<void>();
bool stdoutThrew = false;
bool stderrThrew = false;
final Completer<void> stdoutCompleter = Completer<void>();
Expand All @@ -142,6 +138,8 @@ void main() {
throw 'Error';
}, null);
});
when(stdout.done).thenAnswer((_) => stdoutError.future);
when(stderr.done).thenAnswer((_) => stderrError.future);
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: stdio,
Expand All @@ -159,6 +157,43 @@ void main() {
expect(stderrThrew, true);
});

testWithoutContext('Logger does not throw when stdio completes done with an error', () async {
final MockStdout stdout = MockStdout();
final MockStdout stderr = MockStdout();
final Stdio stdio = Stdio.test(stdout: stdout, stderr: stderr);
final Completer<void> stdoutError = Completer<void>();
final Completer<void> stderrError = Completer<void>();
final Completer<void> stdoutCompleter = Completer<void>();
final Completer<void> stderrCompleter = Completer<void>();
when(stdout.write(any)).thenAnswer((_) {
Zone.current.runUnaryGuarded<void>((_) {
stdoutError.completeError(Exception('Some pipe error'));
stdoutCompleter.complete();
}, null);
});
when(stderr.write(any)).thenAnswer((_) {
Zone.current.runUnaryGuarded<void>((_) {
stderrError.completeError(Exception('Some pipe error'));
stderrCompleter.complete();
}, null);
});
when(stdout.done).thenAnswer((_) => stdoutError.future);
when(stderr.done).thenAnswer((_) => stderrError.future);
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: stdio,
platform: _kNoAnsiPlatform,
),
stdio: stdio,
outputPreferences: OutputPreferences.test(),
timeoutConfiguration: const TimeoutConfiguration(),
);
logger.printStatus('message');
logger.printError('error message');
await stdoutCompleter.future;
await stderrCompleter.future;
});

group('Spinners', () {
mocks.MockStdio mockStdio;
FakeStopwatch mockStopwatch;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ void main() {

await verifyCrashReportSent(requestInfo);
}, overrides: <Type, Generator>{
Stdio: () => const _NoStderr(),
Stdio: () => _NoStderr(),
});

testUsingContext('should print an explanatory message when there is a SocketException', () async {
Expand All @@ -77,7 +77,7 @@ void main() {
expect(await exitCodeCompleter.future, 1);
expect(testLogger.errorText, contains('Failed to send crash report due to a network error'));
}, overrides: <Type, Generator>{
Stdio: () => const _NoStderr(),
Stdio: () => _NoStderr(),
});

testUsingContext('should print an explanatory message when there is an HttpException', () async {
Expand All @@ -98,7 +98,7 @@ void main() {
expect(await exitCodeCompleter.future, 1);
expect(testLogger.errorText, contains('Failed to send crash report due to a network error'));
}, overrides: <Type, Generator>{
Stdio: () => const _NoStderr(),
Stdio: () => _NoStderr(),
});

testUsingContext('should send crash reports when async throws', () async {
Expand All @@ -120,7 +120,7 @@ void main() {
expect(await exitCodeCompleter.future, 1);
await verifyCrashReportSent(requestInfo);
}, overrides: <Type, Generator>{
Stdio: () => const _NoStderr(),
Stdio: () => _NoStderr(),
});

testUsingContext('should send only one crash report when async throws many', () async {
Expand Down Expand Up @@ -151,7 +151,7 @@ void main() {
await verifyCrashReportSent(requestInfo, crashes: 4);
}, overrides: <Type, Generator>{
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
Stdio: () => const _NoStderr(),
Stdio: () => _NoStderr(),
});

testUsingContext('should not send a crash report if on a user-branch', () async {
Expand Down Expand Up @@ -183,7 +183,7 @@ void main() {

expect(testLogger.traceText, isNot(contains('Crash report sent')));
}, overrides: <Type, Generator>{
Stdio: () => const _NoStderr(),
Stdio: () => _NoStderr(),
});

testUsingContext('can override base URL', () async {
Expand Down Expand Up @@ -223,7 +223,7 @@ void main() {
},
script: Uri(scheme: 'data'),
),
Stdio: () => const _NoStderr(),
Stdio: () => _NoStderr(),
});
});
}
Expand Down Expand Up @@ -400,7 +400,7 @@ class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider {
}

class _NoStderr extends Stdio {
const _NoStderr();
_NoStderr();

@override
IOSink get stderr => const _NoopIOSink();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ const String _kInitialVersion = 'v1.9.1+hotfix.6';
const String _kBranch = 'stable';
const FileSystem fileSystem = LocalFileSystem();
const ProcessManager processManager = LocalProcessManager();
final Stdio stdio = Stdio();
final ProcessUtils processUtils = ProcessUtils(processManager: processManager, logger: StdoutLogger(
terminal: AnsiTerminal(
platform: const LocalPlatform(),
stdio: const Stdio(),
stdio: stdio,
),
stdio: const Stdio(),
stdio: stdio,
outputPreferences: OutputPreferences.test(wrapText: true),
timeoutConfiguration: const TimeoutConfiguration(),
));
Expand Down