// 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 'dart:math'; import 'package:flutter_devicelab/framework/adb.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; void generateMain(Directory appDir, String sentinel) { final String mainCode = ''' import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_driver/driver_extension.dart'; class ReassembleListener extends StatefulWidget { const ReassembleListener({Key key, this.child}) : super(key: key); final Widget child; @override _ReassembleListenerState createState() => _ReassembleListenerState(); } class _ReassembleListenerState extends State { @override initState() { super.initState(); print('$sentinel'); } @override void reassemble() { super.reassemble(); print('$sentinel'); } @override Widget build(BuildContext context) { return widget.child; } } void main() { runApp( ReassembleListener( child: Text( 'Hello, word!', textDirection: TextDirection.rtl, ) ) ); } '''; File(path.join(appDir.path, 'lib', 'fuchsia_main.dart')) .writeAsStringSync(mainCode, flush: true); } void main() { deviceOperatingSystem = DeviceOperatingSystem.fuchsia; task(() async { section('Checking environment variables'); if (Platform.environment['FUCHSIA_SSH_CONFIG'] == null && Platform.environment['FUCHSIA_BUILD_DIR'] == null) { throw Exception('No FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR set'); } final String flutterBinary = path.join(flutterDirectory.path, 'bin', 'flutter'); section('Downloading Fuchsia SDK and flutter runner'); // Download the Fuchsia SDK. final int precacheResult = await exec( flutterBinary, [ 'precache', '--fuchsia', '--flutter_runner', ] ); if (precacheResult != 0) { throw Exception('flutter precache failed with exit code $precacheResult'); } final Directory fuchsiaToolDirectory = Directory(path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'fuchsia', 'tools')); if (!fuchsiaToolDirectory.existsSync()) { throw Exception('Expected Fuchsia tool directory at ${fuchsiaToolDirectory.path}'); } final Device device = await devices.workingDevice; final Directory appDir = dir(path.join( flutterDirectory.path, 'dev', 'integration_tests', 'ui', )); await inDirectory(appDir, () async { final Random random = Random(); final Map> sentinelMessage = >{ 'sentinel-${random.nextInt(1<<32)}': Completer(), 'sentinel-${random.nextInt(1<<32)}': Completer(), }; Process runProcess; Process logsProcess; try { section('Creating lib/fuchsia_main.dart'); generateMain(appDir, sentinelMessage.keys.toList()[0]); section('Launching `flutter run` in ${appDir.path}'); runProcess = await startProcess( flutterBinary, [ 'run', '--suppress-analytics', '-d', device.deviceId, '-t', 'lib/fuchsia_main.dart', ], isBot: false, // We just want to test the output, not have any debugging info. ); logsProcess = await startProcess( flutterBinary, ['logs', '--suppress-analytics', '-d', device.deviceId], isBot: false, // We just want to test the output, not have any debugging info. ); Future eventOrExit(Future event) { return Future.any(>[ event, runProcess.exitCode, logsProcess.exitCode, ]); } logsProcess.stdout .transform(utf8.decoder) .transform(const LineSplitter()) .listen((String log) { print('logs:stdout: $log'); for (final String sentinel in sentinelMessage.keys) { if (log.contains(sentinel)) { if (sentinelMessage[sentinel].isCompleted) { throw Exception( 'Expected a single `$sentinel` message in the device log, but found more than one' ); } sentinelMessage[sentinel].complete(); break; } } }); final Completer hotReloadCompleter = Completer(); final Completer reloadedCompleter = Completer(); final RegExp observatoryRegexp = RegExp('An Observatory debugger and profiler on .+ is available at'); runProcess.stdout .transform(utf8.decoder) .transform(const LineSplitter()) .listen((String line) { print('run:stdout: $line'); if (observatoryRegexp.hasMatch(line)) { hotReloadCompleter.complete(); } else if (line.contains('Reloaded')) { reloadedCompleter.complete(); } }); final List runStderr = []; runProcess.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((String line) { runStderr.add(line); print('run:stderr: $line'); }); section('Waiting for hot reload availability'); await eventOrExit(hotReloadCompleter.future); section('Waiting for Dart VM'); // Wait for the first message in the log from the Dart VM. await eventOrExit(sentinelMessage.values.toList()[0].future); // Change the dart file. generateMain(appDir, sentinelMessage.keys.toList()[1]); section('Hot reload'); runProcess.stdin.write('r'); runProcess.stdin.flush(); await eventOrExit(reloadedCompleter.future); section('Waiting for Dart VM'); // Wait for the second message in the log from the Dart VM. await eventOrExit(sentinelMessage.values.toList()[1].future); section('Quitting flutter run'); runProcess.stdin.write('q'); runProcess.stdin.flush(); final int runExitCode = await runProcess.exitCode; if (runExitCode != 0 || runStderr.isNotEmpty) { throw Exception( 'flutter run exited with code $runExitCode and errors: ${runStderr.join('\n')}.' ); } } finally { runProcess.kill(); logsProcess.kill(); File(path.join(appDir.path, 'lib', 'fuchsia_main.dart')).deleteSync(); } for (final String sentinel in sentinelMessage.keys) { if (!sentinelMessage[sentinel].isCompleted) { throw Exception('Expected $sentinel in the device logs.'); } } }); return TaskResult.success(null); }); }