diff --git a/dev/devicelab/bin/tasks/run_release_test_macos.dart b/dev/devicelab/bin/tasks/run_release_test_macos.dart index 253f5bde7e..7f2b9464fc 100644 --- a/dev/devicelab/bin/tasks/run_release_test_macos.dart +++ b/dev/devicelab/bin/tasks/run_release_test_macos.dart @@ -2,122 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - import 'package:flutter_devicelab/framework/devices.dart'; import 'package:flutter_devicelab/framework/framework.dart'; -import 'package:flutter_devicelab/framework/task_result.dart'; -import 'package:flutter_devicelab/framework/utils.dart'; -import 'package:path/path.dart' as path; +import 'package:flutter_devicelab/tasks/run_tests.dart'; /// Basic launch test for desktop operating systems. void main() { - task(() async { - deviceOperatingSystem = DeviceOperatingSystem.macos; - final Device device = await devices.workingDevice; - // TODO(cbracken): https://github.com/flutter/flutter/issues/87508#issuecomment-1043753201 - // Switch to dev/integration_tests/ui once we have CocoaPods working on M1 Macs. - final Directory appDir = dir(path.join(flutterDirectory.path, 'examples/hello_world')); - await inDirectory(appDir, () async { - final Completer ready = Completer(); - final List stdout = []; - final List stderr = []; - - print('run: starting...'); - final List options = [ - '--release', - '-d', - device.deviceId, - ]; - final Process run = await startFlutter( - 'run', - options: options, - isBot: false, - ); - int? runExitCode; - run.stdout - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - print('run:stdout: $line'); - if ( - !line.startsWith('Building flutter tool...') && - !line.startsWith('Running "flutter pub get" in ui...') && - !line.startsWith('Resolving dependencies...') && - // Catch engine piped output from unrelated concurrent Flutter apps - !line.contains(RegExp(r'[A-Z]\/flutter \([0-9]+\):')) && - // Empty lines could be due to the progress spinner breaking up. - line.length > 1 - ) { - stdout.add(line); - } - if (line.contains('Quit (terminate the application on the device).')) { - ready.complete(); - } - }); - run.stderr - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - print('run:stderr: $line'); - stderr.add(line); - }); - unawaited(run.exitCode.then((int exitCode) { runExitCode = exitCode; })); - await Future.any(>[ ready.future, run.exitCode ]); - if (runExitCode != null) { - throw 'Failed to run test app; runner unexpected exited, with exit code $runExitCode.'; - } - run.stdin.write('q'); - - await run.exitCode; - - _findNextMatcherInList( - stdout, - (String line) => line.startsWith('Launching lib/main.dart on ') && line.endsWith(' in release mode...'), - 'Launching lib/main.dart on', - ); - - _findNextMatcherInList( - stdout, - (String line) => line.contains('Quit (terminate the application on the device).'), - 'q Quit (terminate the application on the device)', - ); - - _findNextMatcherInList( - stdout, - (String line) => line == 'Application finished.', - 'Application finished.', - ); - }); - return TaskResult.success(null); - }); -} - -void _findNextMatcherInList( - List list, - bool Function(String testLine) matcher, - String errorMessageExpectedLine -) { - final List copyOfListForErrorMessage = List.from(list); - - while (list.isNotEmpty) { - final String nextLine = list.first; - list.removeAt(0); - - if (matcher(nextLine)) { - return; - } - } - - throw ''' -Did not find expected line - -$errorMessageExpectedLine - -in flutter run --release stdout - -$copyOfListForErrorMessage - '''; + deviceOperatingSystem = DeviceOperatingSystem.macos; + task(createMacOSRunReleaseTest()); } diff --git a/dev/devicelab/lib/tasks/run_tests.dart b/dev/devicelab/lib/tasks/run_tests.dart new file mode 100644 index 0000000000..1fb94f5919 --- /dev/null +++ b/dev/devicelab/lib/tasks/run_tests.dart @@ -0,0 +1,158 @@ +// 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 'dart:convert'; +import 'dart:io'; + +import '../framework/devices.dart'; +import '../framework/framework.dart'; +import '../framework/task_result.dart'; +import '../framework/utils.dart'; + +TaskFunction createMacOSRunReleaseTest() { + return DesktopRunOutputTest( + // TODO(cbracken): https://github.com/flutter/flutter/issues/87508#issuecomment-1043753201 + // Switch to dev/integration_tests/ui once we have CocoaPods working on M1 Macs. + '${flutterDirectory.path}/examples/hello_world', + 'lib/main.dart', + release: true, + ); +} + +class DesktopRunOutputTest extends RunOutputTask { + DesktopRunOutputTest( + super.testDirectory, + super.testTarget, { + required super.release, + } + ); + + @override + TaskResult verify(List stdout, List stderr) { + _findNextMatcherInList( + stdout, + (String line) => line.startsWith('Launching lib/main.dart on ') && + line.endsWith(' in ${release ? 'release' : 'debug'} mode...'), + 'Launching lib/main.dart on', + ); + + _findNextMatcherInList( + stdout, + (String line) => line.contains('Quit (terminate the application on the device).'), + 'q Quit (terminate the application on the device)', + ); + + _findNextMatcherInList( + stdout, + (String line) => line == 'Application finished.', + 'Application finished.', + ); + + return TaskResult.success(null); + } +} + +/// Test that the output of `flutter run` is expected. +abstract class RunOutputTask { + RunOutputTask( + this.testDirectory, + this.testTarget, { + required this.release, + } + ); + + /// The directory where the app under test is defined. + final String testDirectory; + /// The main entry-point file of the application, as run on the device. + final String testTarget; + /// Whether to run the app in release mode. + final bool release; + + Future call() { + return inDirectory(testDirectory, () async { + final Device device = await devices.workingDevice; + await device.unlock(); + final String deviceId = device.deviceId; + + final Completer ready = Completer(); + final List stdout = []; + final List stderr = []; + + final List options = [ + testTarget, + '-d', + deviceId, + if (release) '--release', + ]; + + final Process run = await startFlutter( + 'run', + options: options, + isBot: false, + ); + + int? runExitCode; + run.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + print('run:stdout: $line'); + stdout.add(line); + if (line.contains('Quit (terminate the application on the device).')) { + ready.complete(); + } + }); + run.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + print('run:stderr: $line'); + stderr.add(line); + }); + unawaited(run.exitCode.then((int exitCode) { runExitCode = exitCode; })); + await Future.any(>[ ready.future, run.exitCode ]); + if (runExitCode != null) { + throw 'Failed to run test app; runner unexpected exited, with exit code $runExitCode.'; + } + run.stdin.write('q'); + + await run.exitCode; + + return verify(stdout, stderr); + }); + } + + /// Verify the output of `flutter run`. + TaskResult verify(List stdout, List stderr) => throw UnimplementedError('verify is not implemented'); + + /// Helper that verifies a line in [list] matches [matcher]. + /// The [list] is updated to contain the lines remaining after the match. + void _findNextMatcherInList( + List list, + bool Function(String testLine) matcher, + String errorMessageExpectedLine + ) { + final List copyOfListForErrorMessage = List.from(list); + + while (list.isNotEmpty) { + final String nextLine = list.first; + list.removeAt(0); + + if (matcher(nextLine)) { + return; + } + } + + throw ''' +Did not find expected line + +$errorMessageExpectedLine + +in flutter run ${release ? '--release' : ''} stdout + +$copyOfListForErrorMessage +'''; + } +}