E2E test setting and using isolate names (#23388)
Adds an integration devicelab test that runs an Android app with two
custom named isolates. Tests that the isolate names are present and that
it's possible to attach to just one of the isolates.
Fixes flutter/flutter#22009
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 0000000..0373f7d
--- /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<void> firstNameFound = Completer<void>();
+ Completer<void> secondNameFound = Completer<void>();
+ final Process runProcess = await _run(device: device, command: <String>['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<dynamic>(<Future<dynamic>>[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<void>();
+ secondNameFound = Completer<void>();
+ final String currentTime = (await device.shellEval('date', <String>['"+%F %R:%S.000"'])).trim();
+ await device.shellExec('am', <String>['start', '-n', _kActivityId]);
+ final String observatoryLine = await device.adb(<String>['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: <String>['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<Process> _run({@required Device device, @required List<String> 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'),
+ <String>['--suppress-analytics', '-d', device.deviceId] + command,
+ isBot: false, // we just want to test the output, not have any debugging info
+ );
+ final StreamController<String> stdout = StreamController<String>.broadcast();
+
+ // Mirror output to stdout, listen for ready message
+ final Completer<void> appReady = Completer<void>();
+ runner.stdout
+ .transform<String>(utf8.decoder)
+ .transform<String>(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<String>(utf8.decoder)
+ .transform<String>(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<dynamic>(<Future<dynamic>>[ appReady.future, runner.exitCode ]);
+ if (!observatoryConnected)
+ throw 'Failed to find service port when running `${command.join(' ')}`';
+ });
+ return runner;
+}
+
+Future<void> _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 0cc7e5a..c82accd 100644
--- a/dev/devicelab/manifest.yaml
+++ b/dev/devicelab/manifest.yaml
@@ -283,6 +283,13 @@
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 0000000..a9f3fc5
--- /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 0000000..603ec13
--- /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 0000000..3bdbf10
--- /dev/null
+++ b/dev/integration_tests/named_isolates/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.view">
+
+ <!-- The INTERNET permission is required for development. Specifically, flutter needs it to communicate with the running application
+ to allow setting breakpoints, to provide hot reload, etc.
+ -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+
+ <!-- io.flutter.app.FlutterApplication is an android.app.Application that
+ calls FlutterMain.startInitialization(this); in its onCreate method.
+ In most cases you can leave this as-is, but you if you want to provide
+ additional functionality it is fine to subclass or reimplement
+ FlutterApplication and put your custom class here. -->
+ <application android:name="io.flutter.app.FlutterApplication" android:label="named_isolates">
+ <activity android:name=".MainActivity"
+ android:launchMode="singleTop"
+ android:theme="@style/Theme.AppCompat"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
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 0000000..5b497e7
--- /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 0000000..2187f69
--- /dev/null
+++ b/dev/integration_tests/named_isolates/android/app/src/main/res/layout/flutter_view_layout.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ >
+
+ <io.flutter.view.FlutterView
+ android:id="@+id/first"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ />
+
+ <io.flutter.view.FlutterView
+ android:id="@+id/second"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ />
+
+</LinearLayout>
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 0000000..d4225c7
--- /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 0000000..8bd86f6
--- /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 0000000..9372d0f
--- /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 0000000..115da6c
--- /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 0000000..ec09c9e
--- /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 0000000..2361026
--- /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