diff --git a/dev/devicelab/bin/tasks/named_isolates_test.dart b/dev/devicelab/bin/tasks/named_isolates_test.dart new file mode 100644 index 00000000000..0373f7db370 --- /dev/null +++ b/dev/devicelab/bin/tasks/named_isolates_test.dart @@ -0,0 +1,111 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; + +import 'package:flutter_devicelab/framework/adb.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; + +const String _kActivityId = 'io.flutter.examples.named_isolates/com.example.view.MainActivity'; +const String _kFirstIsolateName = 'first isolate name'; +const String _kSecondIsolateName = 'second isolate name'; + +void main() { + task(() async { + final AndroidDevice device = await devices.workingDevice; + await device.unlock(); + + section('Compile and run the tester app'); + Completer firstNameFound = Completer(); + Completer secondNameFound = Completer(); + final Process runProcess = await _run(device: device, command: ['run'], stdoutListener: (String line) { + if (line.contains(_kFirstIsolateName)) { + firstNameFound.complete(); + } else if (line.contains(_kSecondIsolateName)) { + secondNameFound.complete(); + } + }); + + section('Verify all the debug isolate names are set'); + runProcess.stdin.write('l'); + await Future.wait(>[firstNameFound.future, secondNameFound.future]) + .timeout(Duration(seconds: 1), onTimeout: () => throw 'Isolate names not found.'); + await _quitRunner(runProcess); + + section('Attach to the second debug isolate'); + firstNameFound = Completer(); + secondNameFound = Completer(); + final String currentTime = (await device.shellEval('date', ['"+%F %R:%S.000"'])).trim(); + await device.shellExec('am', ['start', '-n', _kActivityId]); + final String observatoryLine = await device.adb(['logcat', '-e', 'Observatory listening on http:', '-m', '1', '-T', currentTime]); + print('Found observatory line: $observatoryLine'); + final String observatoryPort = RegExp(r'Observatory listening on http://.*:([0-9]+)').firstMatch(observatoryLine)[1]; + print('Extracted observatory port: $observatoryPort'); + final Process attachProcess = + await _run(device: device, command: ['attach', '--debug-port', observatoryPort, '--isolate-filter', '$_kSecondIsolateName'], stdoutListener: (String line) { + if (line.contains(_kFirstIsolateName)) { + firstNameFound.complete(); + } else if (line.contains(_kSecondIsolateName)) { + secondNameFound.complete(); + } + }); + attachProcess.stdin.write('l'); + await secondNameFound.future; + if (firstNameFound.isCompleted) + throw '--isolate-filter failed to attach to a specific isolate'; + await _quitRunner(attachProcess); + + return TaskResult.success(null); + }); +} + +Future _run({@required Device device, @required List command, @required Function(String) stdoutListener}) async { + final Directory appDir = dir(path.join(flutterDirectory.path, 'dev/integration_tests/named_isolates')); + Process runner; + bool observatoryConnected = false; + await inDirectory(appDir, () async { + runner = await startProcess( + path.join(flutterDirectory.path, 'bin', 'flutter'), + ['--suppress-analytics', '-d', device.deviceId] + command, + isBot: false, // we just want to test the output, not have any debugging info + ); + final StreamController stdout = StreamController.broadcast(); + + // Mirror output to stdout, listen for ready message + final Completer appReady = Completer(); + runner.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + print('run:stdout: $line'); + stdout.add(line); + if (parseServicePort(line) != null) { + appReady.complete(); + observatoryConnected = true; + } + stdoutListener(line); + }); + runner.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + stderr.writeln('run:stderr: $line'); + }); + + // Wait for either the process to fail or for the run to begin. + await Future.any(>[ appReady.future, runner.exitCode ]); + if (!observatoryConnected) + throw 'Failed to find service port when running `${command.join(' ')}`'; + }); + return runner; +} + +Future _quitRunner(Process runner) async { + runner.stdin.write('q'); + final int result = await runner.exitCode; + if (result != 0) + throw 'Received unexpected exit code $result when quitting process.'; +} diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml index 0cc7e5a6783..c82accdf20f 100644 --- a/dev/devicelab/manifest.yaml +++ b/dev/devicelab/manifest.yaml @@ -283,6 +283,13 @@ tasks: stage: devicelab required_agent_capabilities: ["linux/android"] + named_isolates_test: + description: > + Tests naming and attaching to specific isolates. + stage: devicelab + required_agent_capabilities: ["linux/android"] + flaky: true + flutter_create_offline_test_linux: description: > Tests the `flutter create --offline` command. diff --git a/dev/integration_tests/named_isolates/README.md b/dev/integration_tests/named_isolates/README.md new file mode 100644 index 00000000000..a9f3fc5e052 --- /dev/null +++ b/dev/integration_tests/named_isolates/README.md @@ -0,0 +1 @@ +Integration app for testing multiple named isolates. \ No newline at end of file diff --git a/dev/integration_tests/named_isolates/android/app/build.gradle b/dev/integration_tests/named_isolates/android/app/build.gradle new file mode 100644 index 00000000000..603ec13c89c --- /dev/null +++ b/dev/integration_tests/named_isolates/android/app/build.gradle @@ -0,0 +1,52 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withInputStream { stream -> + localProperties.load(stream) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 27 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.examples.named_isolates" + minSdkVersion 16 + targetSdkVersion 27 + versionCode 1 + versionName "0.0.1" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support:design:27.1.1' +} diff --git a/dev/integration_tests/named_isolates/android/app/src/main/AndroidManifest.xml b/dev/integration_tests/named_isolates/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..3bdbf101e74 --- /dev/null +++ b/dev/integration_tests/named_isolates/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/dev/integration_tests/named_isolates/android/app/src/main/java/com/example/view/MainActivity.java b/dev/integration_tests/named_isolates/android/app/src/main/java/com/example/view/MainActivity.java new file mode 100644 index 00000000000..5b497e75540 --- /dev/null +++ b/dev/integration_tests/named_isolates/android/app/src/main/java/com/example/view/MainActivity.java @@ -0,0 +1,71 @@ +package com.example.view; + +import android.content.Intent; +import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.View; +import android.widget.TextView; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BasicMessageChannel.MessageHandler; +import io.flutter.plugin.common.BasicMessageChannel.Reply; +import io.flutter.plugin.common.StringCodec; +import io.flutter.view.FlutterMain; +import io.flutter.view.FlutterRunArguments; +import io.flutter.view.FlutterView; +import java.util.ArrayList; + +public class MainActivity extends AppCompatActivity { + private FlutterView firstFlutterView; + private FlutterView secondFlutterView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + FlutterMain.ensureInitializationComplete(getApplicationContext(), null); + setContentView(R.layout.flutter_view_layout); + ActionBar supportActionBar = getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.hide(); + } + + FlutterRunArguments firstRunArguments = new FlutterRunArguments(); + firstRunArguments.bundlePath = FlutterMain.findAppBundlePath(getApplicationContext()); + firstRunArguments.entrypoint = "first"; + firstFlutterView = findViewById(R.id.first); + firstFlutterView.runFromBundle(firstRunArguments); + + FlutterRunArguments secondRunArguments = new FlutterRunArguments(); + secondRunArguments.bundlePath = FlutterMain.findAppBundlePath(getApplicationContext()); + secondRunArguments.entrypoint = "second"; + secondFlutterView = findViewById(R.id.second); + secondFlutterView.runFromBundle(secondRunArguments); + } + + @Override + protected void onDestroy() { + if (firstFlutterView != null) { + firstFlutterView.destroy(); + } + if (secondFlutterView != null) { + secondFlutterView.destroy(); + } + super.onDestroy(); + } + + @Override + protected void onPause() { + super.onPause(); + firstFlutterView.onPause(); + secondFlutterView.onPause(); + } + + @Override + protected void onPostResume() { + super.onPostResume(); + firstFlutterView.onPostResume(); + secondFlutterView.onPostResume(); + } +} diff --git a/dev/integration_tests/named_isolates/android/app/src/main/res/layout/flutter_view_layout.xml b/dev/integration_tests/named_isolates/android/app/src/main/res/layout/flutter_view_layout.xml new file mode 100644 index 00000000000..2187f697b51 --- /dev/null +++ b/dev/integration_tests/named_isolates/android/app/src/main/res/layout/flutter_view_layout.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/dev/integration_tests/named_isolates/android/build.gradle b/dev/integration_tests/named_isolates/android/build.gradle new file mode 100644 index 00000000000..d4225c7905b --- /dev/null +++ b/dev/integration_tests/named_isolates/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.1.2' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/dev/integration_tests/named_isolates/android/gradle.properties b/dev/integration_tests/named_isolates/android/gradle.properties new file mode 100644 index 00000000000..8bd86f68051 --- /dev/null +++ b/dev/integration_tests/named_isolates/android/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx1536M diff --git a/dev/integration_tests/named_isolates/android/gradle/wrapper/gradle-wrapper.properties b/dev/integration_tests/named_isolates/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..9372d0f3f41 --- /dev/null +++ b/dev/integration_tests/named_isolates/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/dev/integration_tests/named_isolates/android/settings.gradle b/dev/integration_tests/named_isolates/android/settings.gradle new file mode 100644 index 00000000000..115da6cb4f4 --- /dev/null +++ b/dev/integration_tests/named_isolates/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withInputStream { stream -> plugins.load(stream) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/dev/integration_tests/named_isolates/lib/main.dart b/dev/integration_tests/named_isolates/lib/main.dart new file mode 100644 index 00000000000..ec09c9e6588 --- /dev/null +++ b/dev/integration_tests/named_isolates/lib/main.dart @@ -0,0 +1,23 @@ +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; + +// named_isolates_test depends on these values. +const String _kFirstIsolateName = 'first isolate name'; +const String _kSecondIsolateName = 'second isolate name'; + +void first() { + _run(_kFirstIsolateName); +} + +void second() { + _run(_kSecondIsolateName); +} + +void _run(String name) { + ui.window.setIsolateDebugName(name); + runApp(Center(child: Text(name, textDirection: TextDirection.ltr))); +} + +// `first` and `second` are the actual entrypoints to this app, but dart specs +// require a main function. +void main() { } diff --git a/dev/integration_tests/named_isolates/pubspec.yaml b/dev/integration_tests/named_isolates/pubspec.yaml new file mode 100644 index 00000000000..236102634c2 --- /dev/null +++ b/dev/integration_tests/named_isolates/pubspec.yaml @@ -0,0 +1,20 @@ +name: named_isolates +description: Tester app for naming specific isolates. + +environment: + # The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite. + sdk: ">=2.0.0-dev.68.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +flutter: + uses-material-design: true + +# PUBSPEC CHECKSUM: d53c