module_test.dart 10.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// 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:io';
7

8
import 'package:flutter_devicelab/framework/apk_utils.dart';
9 10 11 12
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:path/path.dart' as path;

13
final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew';
14
final String gradlewExecutable = Platform.isWindows ? '.\\$gradlew' : './$gradlew';
15
final String fileReadWriteMode = Platform.isWindows ? 'rw-rw-rw-' : 'rw-r--r--';
16

17
/// Tests that the Flutter module project template works and supports
18
/// adding Flutter to an existing Android app.
19
Future<void> main() async {
20 21
  await task(() async {

22 23 24 25
    section('Find Java');

    final String javaHome = await findJavaHome();
    if (javaHome == null)
26
      return TaskResult.failure('Could not find Java');
27 28
    print('\nUsing JAVA_HOME=$javaHome');

29
    section('Create Flutter module project');
30

31
    final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
32
    final Directory projectDir = Directory(path.join(tempDir.path, 'hello'));
33
    try {
34
      await inDirectory(tempDir, () async {
35 36
        await flutter(
          'create',
37
          options: <String>['--org', 'io.flutter.devicelab', '--template=module', 'hello'],
38 39 40
        );
      });

41 42 43 44
      section('Add read-only asset');

      final File readonlyTxtAssetFile = await File(path.join(
        projectDir.path,
45 46
        'assets',
        'read-only.txt'
47 48 49 50 51 52 53 54 55 56 57 58 59
      ))
      .create(recursive: true);

      if (!exists(readonlyTxtAssetFile)) {
        return TaskResult.failure('Failed to create read-only asset');
      }

      if (!Platform.isWindows) {
        await exec('chmod', <String>[
          '444',
          readonlyTxtAssetFile.path
        ]);
      }
60

61
      final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
62
      String content = await pubspec.readAsString();
63 64 65 66 67 68 69 70 71
      content = content.replaceFirst(
        '\n  # assets:\n',
        '\n  assets:\n    - assets/read-only.txt\n',
      );
      await pubspec.writeAsString(content, flush: true);

      section('Add plugins');

      content = await pubspec.readAsString();
72 73
      content = content.replaceFirst(
        '\ndependencies:\n',
74
        '\ndependencies:\n  device_info: 0.4.1\n  package_info: 0.4.0+9\n',
75 76
      );
      await pubspec.writeAsString(content, flush: true);
77
      await inDirectory(projectDir, () async {
78 79 80 81 82
        await flutter(
          'packages',
          options: <String>['get'],
        );
      });
83

84
      section('Build Flutter module library archive');
85

86
      await inDirectory(Directory(path.join(projectDir.path, '.android')), () async {
87
        await exec(
88
          gradlewExecutable,
89 90 91
          <String>['flutter:assembleDebug'],
          environment: <String, String>{ 'JAVA_HOME': javaHome },
        );
92 93
      });

94
      final bool aarBuilt = exists(File(path.join(
95
        projectDir.path,
96 97
        '.android',
        'Flutter',
98 99 100 101 102 103 104
        'build',
        'outputs',
        'aar',
        'flutter-debug.aar',
      )));

      if (!aarBuilt) {
105
        return TaskResult.failure('Failed to build .aar');
106 107
      }

108 109
      section('Build ephemeral host app');

110
      await inDirectory(projectDir, () async {
111 112 113 114 115 116
        await flutter(
          'build',
          options: <String>['apk'],
        );
      });

117
      final bool ephemeralHostApkBuilt = exists(File(path.join(
118
        projectDir.path,
119 120 121 122 123 124 125 126
        'build',
        'host',
        'outputs',
        'apk',
        'release',
        'app-release.apk',
      )));

127
      if (!ephemeralHostApkBuilt) {
128
        return TaskResult.failure('Failed to build ephemeral host .apk');
129 130
      }

131 132
      section('Clean build');

133
      await inDirectory(projectDir, () async {
134 135 136
        await flutter('clean');
      });

137
      section('Make Android host app editable');
138

139
      await inDirectory(projectDir, () async {
140
        await flutter(
141
          'make-host-app-editable',
142 143 144 145
          options: <String>['android'],
        );
      });

146
      section('Build editable host app');
147

148
      await inDirectory(projectDir, () async {
149 150 151 152 153 154
        await flutter(
          'build',
          options: <String>['apk'],
        );
      });

155 156
      final bool editableHostApkBuilt = exists(File(path.join(
        projectDir.path,
157 158 159 160 161 162 163 164
        'build',
        'host',
        'outputs',
        'apk',
        'release',
        'app-release.apk',
      )));

165 166
      if (!editableHostApkBuilt) {
        return TaskResult.failure('Failed to build editable host .apk');
167 168
      }

169
      section('Add to existing Android app');
170

171
      final Directory hostApp = Directory(path.join(tempDir.path, 'hello_host_app'));
172 173
      mkdir(hostApp);
      recursiveCopy(
174 175 176 177 178
        Directory(
          path.join(
            flutterDirectory.path,
            'dev',
            'integration_tests',
179
            'android_host_app_v2_embedding',
180 181
          ),
        ),
182 183 184
        hostApp,
      );
      copy(
185
        File(path.join(projectDir.path, '.android', gradlew)),
186 187 188
        hostApp,
      );
      copy(
189
        File(path.join(projectDir.path, '.android', 'gradle', 'wrapper', 'gradle-wrapper.jar')),
190
        Directory(path.join(hostApp.path, 'gradle', 'wrapper')),
191 192
      );

193 194
      final File analyticsOutputFile = File(path.join(tempDir.path, 'analytics.log'));

195 196
      section('Build debug host APK');

197
      await inDirectory(hostApp, () async {
198 199 200 201
        if (!Platform.isWindows) {
          await exec('chmod', <String>['+x', 'gradlew']);
        }
        await exec(gradlewExecutable,
202
          <String>['app:assembleDebug'],
203 204 205 206
          environment: <String, String>{
            'JAVA_HOME': javaHome,
            'FLUTTER_ANALYTICS_LOG_FILE': analyticsOutputFile.path,
          },
207
        );
208 209
      });

210 211 212
      section('Check debug APK exists');

      final String debugHostApk = path.join(
213 214 215 216 217 218 219
        hostApp.path,
        'app',
        'build',
        'outputs',
        'apk',
        'debug',
        'app-debug.apk',
220 221 222 223 224 225 226
      );
      if (!exists(File(debugHostApk))) {
        return TaskResult.failure('Failed to build debug host APK');
      }

      section('Check files in debug APK');

227
      checkCollectionContains<String>(<String>[
228
        ...flutterAssets,
229 230
        ...debugAssets,
        ...baseApkFiles,
231 232 233 234 235 236 237 238 239 240
      ], await getFilesInApk(debugHostApk));

      section('Check debug AndroidManifest.xml');

      final String androidManifestDebug = await getAndroidManifest(debugHostApk);
      if (!androidManifestDebug.contains('''
        <meta-data
            android:name="flutterProjectType"
            android:value="module" />''')
      ) {
241
        return TaskResult.failure("Debug host APK doesn't contain metadata: flutterProjectType = module ");
242
      }
243 244

      final String analyticsOutput = analyticsOutputFile.readAsStringSync();
245
      if (!analyticsOutput.contains('cd24: android')
246
          || !analyticsOutput.contains('cd25: true')
247
          || !analyticsOutput.contains('viewName: assemble')) {
248
        return TaskResult.failure(
249
          'Building outer app produced the following analytics: "$analyticsOutput" '
250
          'but not the expected strings: "cd24: android", "cd25: true" and '
251
          '"viewName: assemble"'
252 253 254
        );
      }

255 256
      section('Check file access modes for read-only asset from Flutter module');

257
      final String readonlyDebugAssetFilePath = path.joinAll(<String>[
258 259 260 261 262 263 264
        hostApp.path,
        'app',
        'build',
        'intermediates',
        'merged_assets',
        'debug',
        'out',
265 266 267 268
        'flutter_assets',
        'assets',
        'read-only.txt',
      ]);
269 270 271 272 273 274 275 276 277 278 279
      final File readonlyDebugAssetFile = File(readonlyDebugAssetFilePath);
      if (!exists(readonlyDebugAssetFile)) {
        return TaskResult.failure('Failed to copy read-only asset file');
      }

      String modes = readonlyDebugAssetFile.statSync().modeString();
      print('\nread-only.txt file access modes = $modes');
      if (modes != null && modes.compareTo(fileReadWriteMode) != 0) {
        return TaskResult.failure('Failed to make assets user-readable and writable');
      }

280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
      section('Build release host APK');

      await inDirectory(hostApp, () async {
        await exec(gradlewExecutable,
          <String>['app:assembleRelease'],
          environment: <String, String>{
            'JAVA_HOME': javaHome,
            'FLUTTER_ANALYTICS_LOG_FILE': analyticsOutputFile.path,
          },
        );
      });

      final String releaseHostApk = path.join(
        hostApp.path,
        'app',
        'build',
        'outputs',
        'apk',
        'release',
        'app-release-unsigned.apk',
      );
      if (!exists(File(releaseHostApk))) {
        return TaskResult.failure('Failed to build release host APK');
      }

      section('Check files in release APK');

307
      checkCollectionContains<String>(<String>[
308
        ...flutterAssets,
309
        ...baseApkFiles,
310 311 312 313 314 315 316 317 318 319 320 321 322 323
        'lib/arm64-v8a/libapp.so',
        'lib/arm64-v8a/libflutter.so',
        'lib/armeabi-v7a/libapp.so',
        'lib/armeabi-v7a/libflutter.so',
      ], await getFilesInApk(releaseHostApk));

      section('Check release AndroidManifest.xml');

      final String androidManifestRelease = await getAndroidManifest(debugHostApk);
      if (!androidManifestRelease.contains('''
        <meta-data
            android:name="flutterProjectType"
            android:value="module" />''')
      ) {
324
        return TaskResult.failure("Release host APK doesn't contain metadata: flutterProjectType = module ");
325
      }
326 327 328

      section('Check file access modes for read-only asset from Flutter module');

329
      final String readonlyReleaseAssetFilePath = path.joinAll(<String>[
330 331 332 333 334 335 336
        hostApp.path,
        'app',
        'build',
        'intermediates',
        'merged_assets',
        'release',
        'out',
337 338 339 340
        'flutter_assets',
        'assets',
        'read-only.txt',
      ]);
341 342 343 344 345 346 347 348 349 350 351
      final File readonlyReleaseAssetFile = File(readonlyReleaseAssetFilePath);
      if (!exists(readonlyReleaseAssetFile)) {
        return TaskResult.failure('Failed to copy read-only asset file');
      }

      modes = readonlyReleaseAssetFile.statSync().modeString();
      print('\nread-only.txt file access modes = $modes');
      if (modes != null && modes.compareTo(fileReadWriteMode) != 0) {
        return TaskResult.failure('Failed to make assets user-readable and writable');
      }

352
      return TaskResult.success(null);
353 354
    } on TaskResult catch (taskResult) {
      return taskResult;
355
    } catch (e) {
356
      return TaskResult.failure(e.toString());
357
    } finally {
358
      rmTree(tempDir);
359 360 361
    }
  });
}