From 93573de2165c750fdeefcd2d620e2b8bd494fed6 Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Tue, 23 Oct 2018 09:30:00 -0700 Subject: [PATCH] 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 --- .../bin/tasks/named_isolates_test.dart | 111 ++++++++++++++++++ dev/devicelab/manifest.yaml | 7 ++ .../named_isolates/README.md | 1 + .../named_isolates/android/app/build.gradle | 52 ++++++++ .../android/app/src/main/AndroidManifest.xml | 27 +++++ .../java/com/example/view/MainActivity.java | 71 +++++++++++ .../main/res/layout/flutter_view_layout.xml | 23 ++++ .../named_isolates/android/build.gradle | 29 +++++ .../named_isolates/android/gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.properties | 6 + .../named_isolates/android/settings.gradle | 15 +++ .../named_isolates/lib/main.dart | 23 ++++ .../named_isolates/pubspec.yaml | 20 ++++ 13 files changed, 386 insertions(+) create mode 100644 dev/devicelab/bin/tasks/named_isolates_test.dart create mode 100644 dev/integration_tests/named_isolates/README.md create mode 100644 dev/integration_tests/named_isolates/android/app/build.gradle create mode 100644 dev/integration_tests/named_isolates/android/app/src/main/AndroidManifest.xml create mode 100644 dev/integration_tests/named_isolates/android/app/src/main/java/com/example/view/MainActivity.java create mode 100644 dev/integration_tests/named_isolates/android/app/src/main/res/layout/flutter_view_layout.xml create mode 100644 dev/integration_tests/named_isolates/android/build.gradle create mode 100644 dev/integration_tests/named_isolates/android/gradle.properties create mode 100644 dev/integration_tests/named_isolates/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 dev/integration_tests/named_isolates/android/settings.gradle create mode 100644 dev/integration_tests/named_isolates/lib/main.dart create mode 100644 dev/integration_tests/named_isolates/pubspec.yaml 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