| // 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. |
| |
| // The purpose of this test is to verify the end-to-end behavior of |
| // "flutter run" and other such commands, as closely as possible to |
| // the default behavior. To that end, it avoids the use of any test |
| // features that are not critical (-dflutter-test being the primary |
| // example of a test feature that it does use). For example, no use |
| // is made of "--machine" in these tests. |
| |
| // There are a number of risks when it comes to writing a test such |
| // as this one. Typically these tests are hard to debug if they are |
| // in a failing condition, because they just hang as they await the |
| // next expected line that never comes. To avoid this, here we have |
| // the policy of looking for multiple lines, printing what expected |
| // lines were not seen when a short timeout expires (but timing out |
| // does not cause the test to fail, to reduce flakes), and wherever |
| // possible recording all output and comparing the actual output to |
| // the expected output only once the test is completed. |
| |
| // To aid in debugging, consider passing the `debug: true` argument |
| // to the runFlutter function. |
| |
| // This file intentionally assumes the tests run in order. |
| @Tags(<String>['no-shuffle']) |
| library; |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:meta/meta.dart'; |
| import 'package:process/process.dart'; |
| |
| import '../src/common.dart'; |
| import 'test_utils.dart' show fileSystem; |
| |
| const ProcessManager processManager = LocalProcessManager(); |
| final String flutterRoot = getFlutterRoot(); |
| final String flutterBin = fileSystem.path.join(flutterRoot, 'bin', 'flutter'); |
| |
| void debugPrint(String message) { |
| // This is called to intentionally print debugging output when a test is |
| // either taking too long or has failed. |
| // ignore: avoid_print |
| print(message); |
| } |
| |
| typedef LineHandler = String? Function(String line); |
| |
| abstract class Transition { |
| const Transition({this.handler, this.logging}); |
| |
| /// Callback that is invoked when the transition matches. |
| /// |
| /// This should not throw, even if the test is failing. (For example, don't use "expect" |
| /// in these callbacks.) Throwing here would prevent the [runFlutter] function from running |
| /// to completion, which would leave zombie `flutter` processes around. |
| final LineHandler? handler; |
| |
| /// Whether to enable or disable logging when this transition is matched. |
| /// |
| /// The default value, null, leaves the logging state unaffected. |
| final bool? logging; |
| |
| bool matches(String line); |
| |
| @protected |
| bool lineMatchesPattern(String line, Pattern pattern) { |
| if (pattern is String) { |
| return line == pattern; |
| } |
| return line.contains(pattern); |
| } |
| |
| @protected |
| String describe(Pattern pattern) { |
| if (pattern is String) { |
| return '"$pattern"'; |
| } |
| if (pattern is RegExp) { |
| return '/${pattern.pattern}/'; |
| } |
| return '$pattern'; |
| } |
| } |
| |
| class Barrier extends Transition { |
| const Barrier(this.pattern, {super.handler, super.logging}); |
| final Pattern pattern; |
| |
| @override |
| bool matches(String line) => lineMatchesPattern(line, pattern); |
| |
| @override |
| String toString() => describe(pattern); |
| } |
| |
| class Multiple extends Transition { |
| Multiple(List<Pattern> patterns, { |
| super.handler, |
| super.logging, |
| }) : _originalPatterns = patterns, |
| patterns = patterns.toList(); |
| |
| final List<Pattern> _originalPatterns; |
| final List<Pattern> patterns; |
| |
| @override |
| bool matches(String line) { |
| for (int index = 0; index < patterns.length; index += 1) { |
| if (lineMatchesPattern(line, patterns[index])) { |
| patterns.removeAt(index); |
| break; |
| } |
| } |
| return patterns.isEmpty; |
| } |
| |
| @override |
| String toString() { |
| if (patterns.isEmpty) { |
| return '${_originalPatterns.map(describe).join(', ')} (all matched)'; |
| } |
| return '${_originalPatterns.map(describe).join(', ')} (matched ${_originalPatterns.length - patterns.length} so far)'; |
| } |
| } |
| |
| class LogLine { |
| const LogLine(this.channel, this.stamp, this.message); |
| final String channel; |
| final String stamp; |
| final String message; |
| |
| bool get couldBeCrash => message.contains('Oops; flutter has exited unexpectedly:'); |
| |
| @override |
| String toString() => '$stamp $channel: $message'; |
| |
| void printClearly() { |
| debugPrint('$stamp $channel: ${clarify(message)}'); |
| } |
| |
| static String clarify(String line) { |
| return line.runes.map<String>((int rune) { |
| if (rune >= 0x20 && rune <= 0x7F) { |
| return String.fromCharCode(rune); |
| } |
| switch (rune) { |
| case 0x00: return '<NUL>'; |
| case 0x07: return '<BEL>'; |
| case 0x08: return '<TAB>'; |
| case 0x09: return '<BS>'; |
| case 0x0A: return '<LF>'; |
| case 0x0D: return '<CR>'; |
| } |
| return '<${rune.toRadixString(16).padLeft(rune <= 0xFF ? 2 : rune <= 0xFFFF ? 4 : 5, '0')}>'; |
| }).join(); |
| } |
| } |
| |
| class ProcessTestResult { |
| const ProcessTestResult(this.exitCode, this.logs); |
| final int exitCode; |
| final List<LogLine> logs; |
| |
| List<String> get stdout { |
| return logs |
| .where((LogLine log) => log.channel == 'stdout') |
| .map<String>((LogLine log) => log.message) |
| .toList(); |
| } |
| |
| List<String> get stderr { |
| return logs |
| .where((LogLine log) => log.channel == 'stderr') |
| .map<String>((LogLine log) => log.message) |
| .toList(); |
| } |
| |
| @override |
| String toString() => 'exit code $exitCode\nlogs:\n ${logs.join('\n ')}\n'; |
| } |
| |
| Future<ProcessTestResult> runFlutter( |
| List<String> arguments, |
| String workingDirectory, |
| List<Transition> transitions, { |
| bool debug = false, |
| bool logging = true, |
| Duration expectedMaxDuration = const Duration(minutes: 10), // must be less than test timeout of 15 minutes! See ../../dart_test.yaml. |
| }) async { |
| final Stopwatch clock = Stopwatch()..start(); |
| final Process process = await processManager.start( |
| <String>[flutterBin, ...arguments], |
| workingDirectory: workingDirectory, |
| ); |
| final List<LogLine> logs = <LogLine>[]; |
| int nextTransition = 0; |
| void describeStatus() { |
| if (transitions.isNotEmpty) { |
| debugPrint('Expected state transitions:'); |
| for (int index = 0; index < transitions.length; index += 1) { |
| debugPrint( |
| '${index.toString().padLeft(5)} ' |
| '${index < nextTransition ? 'ALREADY MATCHED ' : |
| index == nextTransition ? 'NOW WAITING FOR>' : |
| ' '} ${transitions[index]}'); |
| } |
| } |
| if (logs.isEmpty) { |
| debugPrint('So far nothing has been logged${ debug ? "" : "; use debug:true to print all output" }.'); |
| } else { |
| debugPrint('Log${ debug ? "" : " (only contains logged lines; use debug:true to print all output)" }:'); |
| for (final LogLine log in logs) { |
| log.printClearly(); |
| } |
| } |
| } |
| bool streamingLogs = false; |
| Timer? timeout; |
| void processTimeout() { |
| if (!streamingLogs) { |
| streamingLogs = true; |
| if (!debug) { |
| debugPrint('Test is taking a long time (${clock.elapsed.inSeconds} seconds so far).'); |
| } |
| describeStatus(); |
| debugPrint('(streaming all logs from this point on...)'); |
| } else { |
| debugPrint('(taking a long time...)'); |
| } |
| } |
| String stamp() => '[${(clock.elapsed.inMilliseconds / 1000.0).toStringAsFixed(1).padLeft(5)}s]'; |
| void processStdout(String line) { |
| final LogLine log = LogLine('stdout', stamp(), line); |
| if (logging) { |
| logs.add(log); |
| } |
| if (streamingLogs) { |
| log.printClearly(); |
| } |
| if (nextTransition < transitions.length && transitions[nextTransition].matches(line)) { |
| if (streamingLogs) { |
| debugPrint('(matched ${transitions[nextTransition]})'); |
| } |
| if (transitions[nextTransition].logging != null) { |
| if (!logging && transitions[nextTransition].logging!) { |
| logs.add(log); |
| } |
| logging = transitions[nextTransition].logging!; |
| if (streamingLogs) { |
| if (logging) { |
| debugPrint('(enabled logging)'); |
| } else { |
| debugPrint('(disabled logging)'); |
| } |
| } |
| } |
| if (transitions[nextTransition].handler != null) { |
| final String? command = transitions[nextTransition].handler!(line); |
| if (command != null) { |
| final LogLine inLog = LogLine('stdin', stamp(), command); |
| logs.add(inLog); |
| if (streamingLogs) { |
| inLog.printClearly(); |
| } |
| process.stdin.write(command); |
| } |
| } |
| nextTransition += 1; |
| timeout?.cancel(); |
| timeout = Timer(expectedMaxDuration ~/ 5, processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging. |
| } |
| } |
| void processStderr(String line) { |
| final LogLine log = LogLine('stdout', stamp(), line); |
| logs.add(log); |
| if (streamingLogs) { |
| log.printClearly(); |
| } |
| } |
| if (debug) { |
| processTimeout(); |
| } else { |
| timeout = Timer(expectedMaxDuration ~/ 2, processTimeout); // This is not a failure timeout, just when to start logging verbosely to help debugging. |
| } |
| process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(processStdout); |
| process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(processStderr); |
| unawaited(process.exitCode.timeout(expectedMaxDuration, onTimeout: () { // This is a failure timeout, must not be short. |
| debugPrint('${stamp()} (process is not quitting, trying to send a "q" just in case that helps)'); |
| debugPrint('(a functional test should never reach this point)'); |
| final LogLine inLog = LogLine('stdin', stamp(), 'q'); |
| logs.add(inLog); |
| if (streamingLogs) { |
| inLog.printClearly(); |
| } |
| process.stdin.write('q'); |
| return -1; // discarded |
| }).catchError((Object error) { |
| // ignore errors here, they will be reported on the next line |
| return -1; // discarded |
| })); |
| final int exitCode = await process.exitCode; |
| if (streamingLogs) { |
| debugPrint('${stamp()} (process terminated with exit code $exitCode)'); |
| } |
| timeout?.cancel(); |
| if (nextTransition < transitions.length) { |
| debugPrint('The subprocess terminated before all the expected transitions had been matched.'); |
| if (logs.any((LogLine line) => line.couldBeCrash)) { |
| debugPrint('The subprocess may in fact have crashed. Check the stderr logs below.'); |
| } |
| debugPrint('The transition that we were hoping to see next but that we never saw was:'); |
| debugPrint('${nextTransition.toString().padLeft(5)} NOW WAITING FOR> ${transitions[nextTransition]}'); |
| if (!streamingLogs) { |
| describeStatus(); |
| debugPrint('(process terminated with exit code $exitCode)'); |
| } |
| throw TestFailure('Missed some expected transitions.'); |
| } |
| if (streamingLogs) { |
| debugPrint('${stamp()} (completed execution successfully!)'); |
| } |
| return ProcessTestResult(exitCode, logs); |
| } |
| |
| const int progressMessageWidth = 64; |
| |
| void main() { |
| testWithoutContext('flutter run writes and clears pidfile appropriately', () async { |
| final String tempDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_overall_experience_test.').resolveSymbolicLinksSync(); |
| final String pidFile = fileSystem.path.join(tempDirectory, 'flutter.pid'); |
| final String testDirectory = fileSystem.path.join(flutterRoot, 'examples', 'hello_world'); |
| bool? existsDuringTest; |
| try { |
| expect(fileSystem.file(pidFile).existsSync(), isFalse); |
| final ProcessTestResult result = await runFlutter( |
| <String>['run', '-dflutter-tester', '--pid-file', pidFile], |
| testDirectory, |
| <Transition>[ |
| Barrier('q Quit (terminate the application on the device).', handler: (String line) { |
| existsDuringTest = fileSystem.file(pidFile).existsSync(); |
| return 'q'; |
| }), |
| const Barrier('Application finished.'), |
| ], |
| ); |
| expect(existsDuringTest, isNot(isNull)); |
| expect(existsDuringTest, isTrue); |
| expect(result.exitCode, 0, reason: 'subprocess failed; $result'); |
| expect(fileSystem.file(pidFile).existsSync(), isFalse); |
| // This first test ignores the stdout and stderr, so that if the |
| // first run outputs "building flutter", or the "there's a new |
| // flutter" banner, or other such first-run messages, they won't |
| // fail the tests. This does mean that running this test first is |
| // actually important in the case where you're running the tests |
| // manually. (On CI, all those messages are expected to be seen |
| // long before we get here, e.g. because we run "flutter doctor".) |
| } finally { |
| tryToDelete(fileSystem.directory(tempDirectory)); |
| } |
| }, skip: Platform.isWindows); // [intended] Windows doesn't support sending signals so we don't care if it can store the PID. |
| testWithoutContext('flutter run handle SIGUSR1/2', () async { |
| final String tempDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_overall_experience_test.').resolveSymbolicLinksSync(); |
| final String pidFile = fileSystem.path.join(tempDirectory, 'flutter.pid'); |
| final String testDirectory = fileSystem.path.join(flutterRoot, 'dev', 'integration_tests', 'ui'); |
| final String testScript = fileSystem.path.join('lib', 'commands.dart'); |
| late int pid; |
| try { |
| final ProcessTestResult result = await runFlutter( |
| <String>['run', '-dflutter-tester', '--report-ready', '--pid-file', pidFile, '--no-devtools', testScript], |
| testDirectory, |
| <Transition>[ |
| Multiple(<Pattern>['Flutter run key commands.', 'called paint'], handler: (String line) { |
| pid = int.parse(fileSystem.file(pidFile).readAsStringSync()); |
| processManager.killPid(pid, ProcessSignal.sigusr1); |
| return null; |
| }), |
| Barrier('Performing hot reload...'.padRight(progressMessageWidth), logging: true), |
| Multiple(<Pattern>[RegExp(r'^Reloaded 0 libraries in [0-9]+ms \(compile: \d+ ms, reload: \d+ ms, reassemble: \d+ ms\)\.$'), 'called reassemble', 'called paint'], handler: (String line) { |
| processManager.killPid(pid, ProcessSignal.sigusr2); |
| return null; |
| }), |
| Barrier('Performing hot restart...'.padRight(progressMessageWidth)), |
| // This could look like 'Restarted application in 1,237ms.' |
| Multiple(<Pattern>[RegExp(r'^Restarted application in .+m?s.$'), 'called main', 'called paint'], handler: (String line) { |
| return 'q'; |
| }), |
| const Barrier('Application finished.'), |
| ], |
| logging: false, // we ignore leading log lines to avoid making this test sensitive to e.g. the help message text |
| ); |
| // We check the output from the app (all starts with "called ...") and the output from the tool |
| // (everything else) separately, because their relative timing isn't guaranteed. Their rough timing |
| // is verified by the expected transitions above. |
| expect(result.stdout.where((String line) => line.startsWith('called ')), <Object>[ |
| // logs start after we receive the response to sending SIGUSR1 |
| // SIGUSR1: |
| 'called reassemble', |
| 'called paint', |
| // SIGUSR2: |
| 'called main', |
| 'called paint', |
| ]); |
| expect(result.stdout.where((String line) => !line.startsWith('called ')), <Object>[ |
| // logs start after we receive the response to sending SIGUSR1 |
| 'Performing hot reload...'.padRight(progressMessageWidth), |
| startsWith('Reloaded 0 libraries in '), |
| 'Performing hot restart...'.padRight(progressMessageWidth), |
| startsWith('Restarted application in '), |
| '', // this newline is the one for after we hit "q" |
| 'Application finished.', |
| 'ready', |
| ]); |
| expect(result.exitCode, 0); |
| } finally { |
| tryToDelete(fileSystem.directory(tempDirectory)); |
| } |
| }, skip: Platform.isWindows); // [intended] Windows doesn't support sending signals. |
| |
| testWithoutContext('flutter run can hot reload and hot restart, handle "p" key', () async { |
| final String tempDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_overall_experience_test.').resolveSymbolicLinksSync(); |
| final String testDirectory = fileSystem.path.join(flutterRoot, 'dev', 'integration_tests', 'ui'); |
| final String testScript = fileSystem.path.join('lib', 'commands.dart'); |
| try { |
| final ProcessTestResult result = await runFlutter( |
| <String>['run', '-dflutter-tester', '--report-ready', '--no-devtools', testScript], |
| testDirectory, |
| <Transition>[ |
| Multiple(<Pattern>['Flutter run key commands.', 'called main', 'called paint'], handler: (String line) { |
| return 'r'; |
| }), |
| Barrier('Performing hot reload...'.padRight(progressMessageWidth), logging: true), |
| Multiple(<Pattern>['ready', 'called reassemble', 'called paint'], handler: (String line) { |
| return 'R'; |
| }), |
| Barrier('Performing hot restart...'.padRight(progressMessageWidth)), |
| Multiple(<Pattern>['ready', 'called main', 'called paint'], handler: (String line) { |
| return 'p'; |
| }), |
| Multiple(<Pattern>['ready', 'called paint', 'called debugPaintSize'], handler: (String line) { |
| return 'p'; |
| }), |
| Multiple(<Pattern>['ready', 'called paint'], handler: (String line) { |
| return 'q'; |
| }), |
| const Barrier('Application finished.'), |
| ], |
| logging: false, // we ignore leading log lines to avoid making this test sensitive to e.g. the help message text |
| ); |
| // We check the output from the app (all starts with "called ...") and the output from the tool |
| // (everything else) separately, because their relative timing isn't guaranteed. Their rough timing |
| // is verified by the expected transitions above. |
| expect(result.stdout.where((String line) => line.startsWith('called ')), <Object>[ |
| // logs start after we initiate the hot reload |
| // hot reload: |
| 'called reassemble', |
| 'called paint', |
| // hot restart: |
| 'called main', |
| 'called paint', |
| // debugPaintSizeEnabled = true: |
| 'called paint', |
| 'called debugPaintSize', |
| // debugPaintSizeEnabled = false: |
| 'called paint', |
| ]); |
| expect(result.stdout.where((String line) => !line.startsWith('called ')), <Object>[ |
| // logs start after we receive the response to hitting "r" |
| 'Performing hot reload...'.padRight(progressMessageWidth), |
| startsWith('Reloaded 0 libraries in '), |
| 'ready', |
| '', // this newline is the one for after we hit "R" |
| 'Performing hot restart...'.padRight(progressMessageWidth), |
| startsWith('Restarted application in '), |
| 'ready', |
| '', // newline for after we hit "p" the first time |
| 'ready', |
| '', // newline for after we hit "p" the second time |
| 'ready', |
| '', // this newline is the one for after we hit "q" |
| 'Application finished.', |
| 'ready', |
| ]); |
| expect(result.exitCode, 0); |
| } finally { |
| tryToDelete(fileSystem.directory(tempDirectory)); |
| } |
| }); |
| |
| testWithoutContext('flutter error messages include a DevTools link', () async { |
| final String testDirectory = fileSystem.path.join(flutterRoot, 'dev', 'integration_tests', 'ui'); |
| final String tempDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_overall_experience_test.').resolveSymbolicLinksSync(); |
| final String testScript = fileSystem.path.join('lib', 'overflow.dart'); |
| try { |
| final ProcessTestResult result = await runFlutter( |
| <String>['run', '-dflutter-tester', testScript], |
| testDirectory, |
| <Transition>[ |
| Barrier(RegExp(r'^An Observatory debugger and profiler on Flutter test device is available at: ')), |
| Barrier(RegExp(r'^The Flutter DevTools debugger and profiler on Flutter test device is available at: '), handler: (String line) { |
| return 'r'; |
| }), |
| Barrier('Performing hot reload...'.padRight(progressMessageWidth), logging: true), |
| Barrier(RegExp(r'^Reloaded 0 libraries in [0-9]+ms.'), handler: (String line) { |
| return 'q'; |
| }), |
| ], |
| logging: false, |
| ); |
| expect(result.exitCode, 0); |
| expect(result.stdout, <Object>[ |
| startsWith('Performing hot reload...'), |
| '', |
| '══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════', |
| 'The following assertion was thrown during layout:', |
| 'A RenderFlex overflowed by 69200 pixels on the right.', |
| '', |
| 'The relevant error-causing widget was:', |
| matches(RegExp(r'^ Row .+flutter/dev/integration_tests/ui/lib/overflow\.dart:32:18$')), |
| '', |
| 'To inspect this widget in Flutter DevTools, visit:', |
| startsWith('http'), |
| '', |
| 'The overflowing RenderFlex has an orientation of Axis.horizontal.', |
| 'The edge of the RenderFlex that is overflowing has been marked in the rendering with a yellow and', |
| 'black striped pattern. This is usually caused by the contents being too big for the RenderFlex.', |
| 'Consider applying a flex factor (e.g. using an Expanded widget) to force the children of the', |
| 'RenderFlex to fit within the available space instead of being sized to their natural size.', |
| 'This is considered an error condition because it indicates that there is content that cannot be', |
| 'seen. If the content is legitimately bigger than the available space, consider clipping it with a', |
| 'ClipRect widget before putting it in the flex, or using a scrollable container rather than a Flex,', |
| 'like a ListView.', |
| matches(RegExp(r'^The specific RenderFlex in question is: RenderFlex#..... OVERFLOWING:$')), |
| startsWith(' creator: Row ← Test ← '), |
| contains(' ← '), |
| endsWith(' ⋯'), |
| ' parentData: <none> (can use size)', |
| ' constraints: BoxConstraints(w=800.0, h=600.0)', |
| ' size: Size(800.0, 600.0)', |
| ' direction: horizontal', |
| ' mainAxisAlignment: start', |
| ' mainAxisSize: max', |
| ' crossAxisAlignment: center', |
| ' textDirection: ltr', |
| ' verticalDirection: down', |
| '◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤', |
| '════════════════════════════════════════════════════════════════════════════════════════════════════', |
| '', |
| startsWith('Reloaded 0 libraries in '), |
| '', |
| 'Application finished.', |
| ]); |
| } finally { |
| tryToDelete(fileSystem.directory(tempDirectory)); |
| } |
| }); |
| |
| testWithoutContext('flutter run help output', () async { |
| // This test enables all logging so that it checks the exact text of starting up an application. |
| // The idea is to verify that we're not outputting spurious messages. |
| // WHEN EDITING THIS TEST PLEASE CAREFULLY CONSIDER WHETHER THE NEW OUTPUT IS AN IMPROVEMENT. |
| final String testDirectory = fileSystem.path.join(flutterRoot, 'examples', 'hello_world'); |
| final RegExp finalLine = RegExp(r'^The Flutter DevTools'); |
| final ProcessTestResult result = await runFlutter( |
| <String>['run', '-dflutter-tester'], |
| testDirectory, |
| <Transition>[ |
| Barrier(finalLine, handler: (String line) { |
| return 'h'; |
| }), |
| Barrier(finalLine, handler: (String line) { |
| return 'q'; |
| }), |
| const Barrier('Application finished.'), |
| ], |
| ); |
| expect(result.exitCode, 0); |
| expect(result.stderr, isEmpty); |
| expect(result.stdout, <Object>[ |
| startsWith('Launching '), |
| startsWith('Syncing files to device Flutter test device...'), |
| '', |
| 'Flutter run key commands.', |
| startsWith('r Hot reload.'), |
| 'R Hot restart.', |
| 'h List all available interactive commands.', |
| 'd Detach (terminate "flutter run" but leave application running).', |
| 'c Clear the screen', |
| 'q Quit (terminate the application on the device).', |
| '', |
| startsWith('An Observatory debugger and profiler on Flutter test device is available at: http://'), |
| startsWith('The Flutter DevTools debugger and profiler on Flutter test device is available at: http://'), |
| '', |
| 'Flutter run key commands.', |
| startsWith('r Hot reload.'), |
| 'R Hot restart.', |
| 'v Open Flutter DevTools.', |
| 'w Dump widget hierarchy to the console. (debugDumpApp)', |
| 't Dump rendering tree to the console. (debugDumpRenderTree)', |
| 'L Dump layer tree to the console. (debugDumpLayerTree)', |
| 'S Dump accessibility tree in traversal order. (debugDumpSemantics)', |
| 'U Dump accessibility tree in inverse hit test order. (debugDumpSemantics)', |
| 'i Toggle widget inspector. (WidgetsApp.showWidgetInspectorOverride)', |
| 'p Toggle the display of construction lines. (debugPaintSizeEnabled)', |
| 'I Toggle oversized image inversion. (debugInvertOversizedImages)', |
| 'o Simulate different operating systems. (defaultTargetPlatform)', |
| 'b Toggle platform brightness (dark and light mode). (debugBrightnessOverride)', |
| 'P Toggle performance overlay. (WidgetsApp.showPerformanceOverlay)', |
| 'a Toggle timeline events for all widget build methods. (debugProfileWidgetBuilds)', |
| 'M Write SkSL shaders to a unique file in the project directory.', |
| 'g Run source code generators.', |
| 'j Dump frame raster stats for the current frame. (Unsupported for web)', |
| 'h Repeat this help message.', |
| 'd Detach (terminate "flutter run" but leave application running).', |
| 'c Clear the screen', |
| 'q Quit (terminate the application on the device).', |
| '', |
| startsWith('An Observatory debugger and profiler on Flutter test device is available at: http://'), |
| startsWith('The Flutter DevTools debugger and profiler on Flutter test device is available at: http://'), |
| '', |
| 'Application finished.', |
| ]); |
| }); |
| } |