Skip to content

Commit 93573de

Browse files
author
Michael Klimushyn
authored
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 #22009
1 parent b63ced5 commit 93573de

File tree

13 files changed

+386
-0
lines changed

13 files changed

+386
-0
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import 'dart:async';
2+
import 'dart:convert';
3+
import 'dart:io';
4+
5+
import 'package:meta/meta.dart';
6+
import 'package:path/path.dart' as path;
7+
8+
import 'package:flutter_devicelab/framework/adb.dart';
9+
import 'package:flutter_devicelab/framework/framework.dart';
10+
import 'package:flutter_devicelab/framework/utils.dart';
11+
12+
const String _kActivityId = 'io.flutter.examples.named_isolates/com.example.view.MainActivity';
13+
const String _kFirstIsolateName = 'first isolate name';
14+
const String _kSecondIsolateName = 'second isolate name';
15+
16+
void main() {
17+
task(() async {
18+
final AndroidDevice device = await devices.workingDevice;
19+
await device.unlock();
20+
21+
section('Compile and run the tester app');
22+
Completer<void> firstNameFound = Completer<void>();
23+
Completer<void> secondNameFound = Completer<void>();
24+
final Process runProcess = await _run(device: device, command: <String>['run'], stdoutListener: (String line) {
25+
if (line.contains(_kFirstIsolateName)) {
26+
firstNameFound.complete();
27+
} else if (line.contains(_kSecondIsolateName)) {
28+
secondNameFound.complete();
29+
}
30+
});
31+
32+
section('Verify all the debug isolate names are set');
33+
runProcess.stdin.write('l');
34+
await Future.wait<dynamic>(<Future<dynamic>>[firstNameFound.future, secondNameFound.future])
35+
.timeout(Duration(seconds: 1), onTimeout: () => throw 'Isolate names not found.');
36+
await _quitRunner(runProcess);
37+
38+
section('Attach to the second debug isolate');
39+
firstNameFound = Completer<void>();
40+
secondNameFound = Completer<void>();
41+
final String currentTime = (await device.shellEval('date', <String>['"+%F %R:%S.000"'])).trim();
42+
await device.shellExec('am', <String>['start', '-n', _kActivityId]);
43+
final String observatoryLine = await device.adb(<String>['logcat', '-e', 'Observatory listening on http:', '-m', '1', '-T', currentTime]);
44+
print('Found observatory line: $observatoryLine');
45+
final String observatoryPort = RegExp(r'Observatory listening on http://.*:([0-9]+)').firstMatch(observatoryLine)[1];
46+
print('Extracted observatory port: $observatoryPort');
47+
final Process attachProcess =
48+
await _run(device: device, command: <String>['attach', '--debug-port', observatoryPort, '--isolate-filter', '$_kSecondIsolateName'], stdoutListener: (String line) {
49+
if (line.contains(_kFirstIsolateName)) {
50+
firstNameFound.complete();
51+
} else if (line.contains(_kSecondIsolateName)) {
52+
secondNameFound.complete();
53+
}
54+
});
55+
attachProcess.stdin.write('l');
56+
await secondNameFound.future;
57+
if (firstNameFound.isCompleted)
58+
throw '--isolate-filter failed to attach to a specific isolate';
59+
await _quitRunner(attachProcess);
60+
61+
return TaskResult.success(null);
62+
});
63+
}
64+
65+
Future<Process> _run({@required Device device, @required List<String> command, @required Function(String) stdoutListener}) async {
66+
final Directory appDir = dir(path.join(flutterDirectory.path, 'dev/integration_tests/named_isolates'));
67+
Process runner;
68+
bool observatoryConnected = false;
69+
await inDirectory(appDir, () async {
70+
runner = await startProcess(
71+
path.join(flutterDirectory.path, 'bin', 'flutter'),
72+
<String>['--suppress-analytics', '-d', device.deviceId] + command,
73+
isBot: false, // we just want to test the output, not have any debugging info
74+
);
75+
final StreamController<String> stdout = StreamController<String>.broadcast();
76+
77+
// Mirror output to stdout, listen for ready message
78+
final Completer<void> appReady = Completer<void>();
79+
runner.stdout
80+
.transform<String>(utf8.decoder)
81+
.transform<String>(const LineSplitter())
82+
.listen((String line) {
83+
print('run:stdout: $line');
84+
stdout.add(line);
85+
if (parseServicePort(line) != null) {
86+
appReady.complete();
87+
observatoryConnected = true;
88+
}
89+
stdoutListener(line);
90+
});
91+
runner.stderr
92+
.transform<String>(utf8.decoder)
93+
.transform<String>(const LineSplitter())
94+
.listen((String line) {
95+
stderr.writeln('run:stderr: $line');
96+
});
97+
98+
// Wait for either the process to fail or for the run to begin.
99+
await Future.any<dynamic>(<Future<dynamic>>[ appReady.future, runner.exitCode ]);
100+
if (!observatoryConnected)
101+
throw 'Failed to find service port when running `${command.join(' ')}`';
102+
});
103+
return runner;
104+
}
105+
106+
Future<void> _quitRunner(Process runner) async {
107+
runner.stdin.write('q');
108+
final int result = await runner.exitCode;
109+
if (result != 0)
110+
throw 'Received unexpected exit code $result when quitting process.';
111+
}

dev/devicelab/manifest.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,13 @@ tasks:
283283
stage: devicelab
284284
required_agent_capabilities: ["linux/android"]
285285

286+
named_isolates_test:
287+
description: >
288+
Tests naming and attaching to specific isolates.
289+
stage: devicelab
290+
required_agent_capabilities: ["linux/android"]
291+
flaky: true
292+
286293
flutter_create_offline_test_linux:
287294
description: >
288295
Tests the `flutter create --offline` command.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Integration app for testing multiple named isolates.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
def localProperties = new Properties()
2+
def localPropertiesFile = rootProject.file('local.properties')
3+
if (localPropertiesFile.exists()) {
4+
localPropertiesFile.withInputStream { stream ->
5+
localProperties.load(stream)
6+
}
7+
}
8+
9+
def flutterRoot = localProperties.getProperty('flutter.sdk')
10+
if (flutterRoot == null) {
11+
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12+
}
13+
14+
apply plugin: 'com.android.application'
15+
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
16+
17+
android {
18+
compileSdkVersion 27
19+
20+
lintOptions {
21+
disable 'InvalidPackage'
22+
}
23+
24+
defaultConfig {
25+
applicationId "io.flutter.examples.named_isolates"
26+
minSdkVersion 16
27+
targetSdkVersion 27
28+
versionCode 1
29+
versionName "0.0.1"
30+
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
31+
}
32+
33+
buildTypes {
34+
release {
35+
// TODO: Add your own signing config for the release build.
36+
// Signing with the debug keys for now, so `flutter run --release` works.
37+
signingConfig signingConfigs.debug
38+
}
39+
}
40+
}
41+
42+
flutter {
43+
source '../..'
44+
}
45+
46+
dependencies {
47+
testImplementation 'junit:junit:4.12'
48+
androidTestImplementation 'com.android.support.test:runner:1.0.2'
49+
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
50+
implementation 'com.android.support:appcompat-v7:27.1.1'
51+
implementation 'com.android.support:design:27.1.1'
52+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
package="com.example.view">
3+
4+
<!-- The INTERNET permission is required for development. Specifically, flutter needs it to communicate with the running application
5+
to allow setting breakpoints, to provide hot reload, etc.
6+
-->
7+
<uses-permission android:name="android.permission.INTERNET"/>
8+
9+
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
10+
calls FlutterMain.startInitialization(this); in its onCreate method.
11+
In most cases you can leave this as-is, but you if you want to provide
12+
additional functionality it is fine to subclass or reimplement
13+
FlutterApplication and put your custom class here. -->
14+
<application android:name="io.flutter.app.FlutterApplication" android:label="named_isolates">
15+
<activity android:name=".MainActivity"
16+
android:launchMode="singleTop"
17+
android:theme="@style/Theme.AppCompat"
18+
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
19+
android:hardwareAccelerated="true"
20+
android:windowSoftInputMode="adjustResize">
21+
<intent-filter>
22+
<action android:name="android.intent.action.MAIN"/>
23+
<category android:name="android.intent.category.LAUNCHER"/>
24+
</intent-filter>
25+
</activity>
26+
</application>
27+
</manifest>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.example.view;
2+
3+
import android.content.Intent;
4+
import android.os.Bundle;
5+
import android.support.design.widget.FloatingActionButton;
6+
import android.support.v7.app.ActionBar;
7+
import android.support.v7.app.AppCompatActivity;
8+
import android.view.View;
9+
import android.widget.TextView;
10+
import io.flutter.plugin.common.BasicMessageChannel;
11+
import io.flutter.plugin.common.BasicMessageChannel.MessageHandler;
12+
import io.flutter.plugin.common.BasicMessageChannel.Reply;
13+
import io.flutter.plugin.common.StringCodec;
14+
import io.flutter.view.FlutterMain;
15+
import io.flutter.view.FlutterRunArguments;
16+
import io.flutter.view.FlutterView;
17+
import java.util.ArrayList;
18+
19+
public class MainActivity extends AppCompatActivity {
20+
private FlutterView firstFlutterView;
21+
private FlutterView secondFlutterView;
22+
23+
@Override
24+
protected void onCreate(Bundle savedInstanceState) {
25+
super.onCreate(savedInstanceState);
26+
27+
FlutterMain.ensureInitializationComplete(getApplicationContext(), null);
28+
setContentView(R.layout.flutter_view_layout);
29+
ActionBar supportActionBar = getSupportActionBar();
30+
if (supportActionBar != null) {
31+
supportActionBar.hide();
32+
}
33+
34+
FlutterRunArguments firstRunArguments = new FlutterRunArguments();
35+
firstRunArguments.bundlePath = FlutterMain.findAppBundlePath(getApplicationContext());
36+
firstRunArguments.entrypoint = "first";
37+
firstFlutterView = findViewById(R.id.first);
38+
firstFlutterView.runFromBundle(firstRunArguments);
39+
40+
FlutterRunArguments secondRunArguments = new FlutterRunArguments();
41+
secondRunArguments.bundlePath = FlutterMain.findAppBundlePath(getApplicationContext());
42+
secondRunArguments.entrypoint = "second";
43+
secondFlutterView = findViewById(R.id.second);
44+
secondFlutterView.runFromBundle(secondRunArguments);
45+
}
46+
47+
@Override
48+
protected void onDestroy() {
49+
if (firstFlutterView != null) {
50+
firstFlutterView.destroy();
51+
}
52+
if (secondFlutterView != null) {
53+
secondFlutterView.destroy();
54+
}
55+
super.onDestroy();
56+
}
57+
58+
@Override
59+
protected void onPause() {
60+
super.onPause();
61+
firstFlutterView.onPause();
62+
secondFlutterView.onPause();
63+
}
64+
65+
@Override
66+
protected void onPostResume() {
67+
super.onPostResume();
68+
firstFlutterView.onPostResume();
69+
secondFlutterView.onPostResume();
70+
}
71+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
4+
android:orientation="vertical"
5+
android:layout_width="match_parent"
6+
android:layout_height="match_parent"
7+
>
8+
9+
<io.flutter.view.FlutterView
10+
android:id="@+id/first"
11+
android:layout_width="match_parent"
12+
android:layout_height="match_parent"
13+
android:layout_weight="1"
14+
/>
15+
16+
<io.flutter.view.FlutterView
17+
android:id="@+id/second"
18+
android:layout_width="match_parent"
19+
android:layout_height="match_parent"
20+
android:layout_weight="1"
21+
/>
22+
23+
</LinearLayout>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
buildscript {
2+
repositories {
3+
google()
4+
jcenter()
5+
}
6+
7+
dependencies {
8+
classpath 'com.android.tools.build:gradle:3.1.2'
9+
}
10+
}
11+
12+
allprojects {
13+
repositories {
14+
google()
15+
jcenter()
16+
}
17+
}
18+
19+
rootProject.buildDir = '../build'
20+
subprojects {
21+
project.buildDir = "${rootProject.buildDir}/${project.name}"
22+
}
23+
subprojects {
24+
project.evaluationDependsOn(':app')
25+
}
26+
27+
task clean(type: Delete) {
28+
delete rootProject.buildDir
29+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.gradle.jvmargs=-Xmx1536M
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#Fri Jun 23 08:50:38 CEST 2017
2+
distributionBase=GRADLE_USER_HOME
3+
distributionPath=wrapper/dists
4+
zipStoreBase=GRADLE_USER_HOME
5+
zipStorePath=wrapper/dists
6+
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
include ':app'
2+
3+
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
4+
5+
def plugins = new Properties()
6+
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
7+
if (pluginsFile.exists()) {
8+
pluginsFile.withInputStream { stream -> plugins.load(stream) }
9+
}
10+
11+
plugins.each { name, path ->
12+
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
13+
include ":$name"
14+
project(":$name").projectDir = pluginDirectory
15+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import 'dart:ui' as ui;
2+
import 'package:flutter/material.dart';
3+
4+
// named_isolates_test depends on these values.
5+
const String _kFirstIsolateName = 'first isolate name';
6+
const String _kSecondIsolateName = 'second isolate name';
7+
8+
void first() {
9+
_run(_kFirstIsolateName);
10+
}
11+
12+
void second() {
13+
_run(_kSecondIsolateName);
14+
}
15+
16+
void _run(String name) {
17+
ui.window.setIsolateDebugName(name);
18+
runApp(Center(child: Text(name, textDirection: TextDirection.ltr)));
19+
}
20+
21+
// `first` and `second` are the actual entrypoints to this app, but dart specs
22+
// require a main function.
23+
void main() { }

0 commit comments

Comments
 (0)