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
This commit is contained in:
Michael Klimushyn 2018-10-23 09:30:00 -07:00 committed by GitHub
parent b63ced55b4
commit 93573de216
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 386 additions and 0 deletions

View file

@ -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.';
}

View file

@ -283,6 +283,13 @@ tasks:
stage: devicelab stage: devicelab
required_agent_capabilities: ["linux/android"] 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: flutter_create_offline_test_linux:
description: > description: >
Tests the `flutter create --offline` command. Tests the `flutter create --offline` command.

View file

@ -0,0 +1 @@
Integration app for testing multiple named isolates.

View file

@ -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'
}

View file

@ -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>

View file

@ -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();
}
}

View file

@ -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>

View file

@ -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
}

View file

@ -0,0 +1 @@
org.gradle.jvmargs=-Xmx1536M

View file

@ -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

View file

@ -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
}

View file

@ -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() { }

View file

@ -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