flutter_attach_test_android.dart 7.26 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11 12 13 14 15
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:io';

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';

Future<void> testReload(Process process, { Future<void> Function() onListening }) async {
  section('Testing hot reload, restart and quit');
16 17 18 19 20
  final Completer<void> listening = Completer<void>();
  final Completer<void> ready = Completer<void>();
  final Completer<void> reloaded = Completer<void>();
  final Completer<void> restarted = Completer<void>();
  final Completer<void> finished = Completer<void>();
21 22 23 24 25 26 27 28
  final List<String> stdout = <String>[];
  final List<String> stderr = <String>[];

  if (onListening == null)
    listening.complete();

  int exitCode;
  process.stdout
29 30
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
31 32 33
      .listen((String line) {
    print('attach:stdout: $line');
    stdout.add(line);
34
    if (line.contains('Waiting') && onListening != null)
35
      listening.complete(onListening());
36
    if (line.contains('Quit (terminate the application on the device)'))
37 38 39
      ready.complete();
    if (line.contains('Reloaded '))
      reloaded.complete();
40
    if (line.contains('Restarted application in '))
41 42 43 44 45
      restarted.complete();
    if (line.contains('Application finished'))
      finished.complete();
  });
  process.stderr
46 47
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
48 49
      .listen((String line) {
    print('run:stderr: $line');
50
    stdout.add(line);
51 52
  });

53
  process.exitCode.then<void>((int processExitCode) { exitCode = processExitCode; });
54

55
  Future<dynamic> eventOrExit(Future<void> event) {
56 57 58 59
    return Future.any<dynamic>(<Future<dynamic>>[
      event,
      process.exitCode,
      // Keep the test from running for 15 minutes if it gets stuck.
60
      Future<void>.delayed(const Duration(minutes: 1)).then<void>((void _) {
61 62
        throw StateError('eventOrExit timed out');
      }),
63
    ]);
64 65 66
  }

  await eventOrExit(listening.future);
67
  await eventOrExit(ready.future);
68 69

  if (exitCode != null)
70
    throw TaskResult.failure('Failed to attach to test app; command unexpected exited, with exit code $exitCode.');
71 72

  process.stdin.write('r');
73 74
  print('run:stdin: r');
  await process.stdin.flush();
75
  await eventOrExit(reloaded.future);
76

77
  process.stdin.write('R');
78 79
  print('run:stdin: R');
  await process.stdin.flush();
80
  await eventOrExit(restarted.future);
81

82
  process.stdin.write('q');
83 84
  print('run:stdin: q');
  await process.stdin.flush();
85 86 87 88 89
  await eventOrExit(finished.future);

  await process.exitCode;

  if (stderr.isNotEmpty)
90
    throw TaskResult.failure('flutter attach had output on standard error.');
91 92

  if (exitCode != 0)
93
    throw TaskResult.failure('exit code was not 0');
94 95 96 97 98 99 100
}

void main() {
  const String kAppId = 'com.yourcompany.integration_ui';
  const String kActivityId = '$kAppId/com.yourcompany.integration_ui.MainActivity';

  task(() async {
101
    final AndroidDevice device = await devices.workingDevice as AndroidDevice;
102 103 104
    await device.unlock();
    final Directory appDir = dir(path.join(flutterDirectory.path, 'dev/integration_tests/ui'));
    await inDirectory(appDir, () async {
105
      section('Building');
106 107 108 109 110
      final String buildStdout = await eval(
          path.join(flutterDirectory.path, 'bin', 'flutter'),
          <String>['--suppress-analytics', 'build', 'apk', '--debug', 'lib/main.dart'],
      );
      final String lastLine = buildStdout.split('\n').last;
111
      final RegExp builtRegExp = RegExp(r'Built (.+)( \(|\.$)');
112 113 114 115
      final String apkPath = builtRegExp.firstMatch(lastLine)[1];

      section('Installing $apkPath');

116
      await device.adb(<String>['install', '-r', apkPath]);
117 118

      try {
119
        section('Launching `flutter attach`');
120 121
        Process attachProcess = await startProcess(
          path.join(flutterDirectory.path, 'bin', 'flutter'),
122
          <String>['-v', '--suppress-analytics', 'attach', '-d', device.deviceId],
123 124 125 126 127 128 129
          isBot: false, // we just want to test the output, not have any debugging info
        );

        await testReload(attachProcess, onListening: () async {
          await device.shellExec('am', <String>['start', '-n', kActivityId]);
        });

130
        // Give the device the time to really shut down the app.
131
        await Future<void>.delayed(const Duration(milliseconds: 200));
132 133 134
        // After the delay, force-stopping it shouldn't do anything, but doesn't hurt.
        await device.shellExec('am', <String>['force-stop', kAppId]);

135
        String currentTime = (await device.shellEval('date', <String>['"+%F %R:%S.000"'])).trim();
136
        print('Start time on device: $currentTime');
137
        section('Relaunching application');
138 139
        await device.shellExec('am', <String>['start', '-n', kActivityId]);

140
        // If the next line fails, your device may not support regexp search.
141 142
        final String observatoryLine = await device.adb(<String>['logcat', '-e', 'Observatory listening on http:', '-m', '1', '-T', currentTime]);
        print('Found observatory line: $observatoryLine');
143
        final String observatoryUri = RegExp(r'Observatory listening on ((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)').firstMatch(observatoryLine)[1];
144
        print('Extracted observatory port: $observatoryUri');
145

146
        section('Launching attach with given port');
147 148
        attachProcess = await startProcess(
          path.join(flutterDirectory.path, 'bin', 'flutter'),
149
          <String>['-v', '--suppress-analytics', 'attach', '--debug-uri',
150
          observatoryUri, '-d', device.deviceId],
151 152 153 154
          isBot: false, // we just want to test the output, not have any debugging info
        );
        await testReload(attachProcess);

155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
        // Give the device the time to really shut down the app.
        await Future<void>.delayed(const Duration(milliseconds: 200));
        // After the delay, force-stopping it shouldn't do anything, but doesn't hurt.
        await device.shellExec('am', <String>['force-stop', kAppId]);

        section('Attaching after relaunching application');
        await device.shellExec('am', <String>['start', '-n', kActivityId]);

        // Let the application launch. Sync to the next time an observatory is ready.
        currentTime = (await device.shellEval('date', <String>['"+%F %R:%S.000"'])).trim();
        await device.adb(<String>['logcat', '-e', 'Observatory listening on http:', '-m', '1', '-T', currentTime]);

        // Attach again now that the VM is already running.
        attachProcess = await startProcess(
          path.join(flutterDirectory.path, 'bin', 'flutter'),
170
          <String>['-v', '--suppress-analytics', 'attach', '-d', device.deviceId],
171 172 173 174
          isBot: false, // we just want to test the output, not have any debugging info
        );
        // Verify that it can discover the observatory port from past logs.
        await testReload(attachProcess);
175 176 177
      } catch (err, st) {
        print('Uncaught exception: $err\n$st');
        rethrow;
178 179 180 181 182
      } finally {
        section('Uninstalling');
        await device.adb(<String>['uninstall', kAppId]);
      }
    });
183
    return TaskResult.success(null);
184 185
  });
}