module_test_ios.dart 13 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
// 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';

import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:path/path.dart' as path;

12
/// Tests that the Flutter module project template works and supports
13
/// adding Flutter to an existing iOS app.
14
Future<void> main() async {
15
  await task(() async {
16
    section('Create Flutter module project');
17

18
    final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
19 20 21 22 23
    final Directory projectDir = Directory(path.join(tempDir.path, 'hello'));
    try {
      await inDirectory(tempDir, () async {
        await flutter(
          'create',
24 25 26 27
          options: <String>[
            '--org',
            'io.flutter.devicelab',
            '--template=module',
28
            'hello',
29
          ],
30 31 32
        );
      });

33
      section('Build ephemeral host app in release mode without CocoaPods');
34 35 36 37

      await inDirectory(projectDir, () async {
        await flutter(
          'build',
38
          options: <String>['ios', '--no-codesign'],
39 40 41
        );
      });

42
      final Directory ephemeralReleaseHostApp = Directory(path.join(
43 44 45 46
        projectDir.path,
        'build',
        'ios',
        'iphoneos',
47
        'Runner.app',
48
      ));
49

50
      if (!exists(ephemeralReleaseHostApp)) {
51 52 53
        return TaskResult.failure('Failed to build ephemeral host .app');
      }

54 55 56 57 58 59
      if (!await _isAppAotBuild(ephemeralReleaseHostApp)) {
        return TaskResult.failure(
          'Ephemeral host app ${ephemeralReleaseHostApp.path} was not a release build as expected'
        );
      }

60 61 62 63 64 65 66
      if (await _hasDebugSymbols(ephemeralReleaseHostApp)) {
        return TaskResult.failure(
          "Ephemeral host app ${ephemeralReleaseHostApp.path}'s App.framework's "
          "debug symbols weren't stripped in release mode"
        );
      }

67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
      section('Clean build');

      await inDirectory(projectDir, () async {
        await flutter('clean');
      });

      section('Build ephemeral host app in profile mode without CocoaPods');

      await inDirectory(projectDir, () async {
        await flutter(
          'build',
          options: <String>['ios', '--no-codesign', '--profile'],
        );
      });

      final Directory ephemeralProfileHostApp = Directory(path.join(
        projectDir.path,
        'build',
        'ios',
        'iphoneos',
87
        'Runner.app',
88 89 90 91 92 93 94 95 96 97 98 99
      ));

      if (!exists(ephemeralProfileHostApp)) {
        return TaskResult.failure('Failed to build ephemeral host .app');
      }

      if (!await _isAppAotBuild(ephemeralProfileHostApp)) {
        return TaskResult.failure(
          'Ephemeral host app ${ephemeralProfileHostApp.path} was not a profile build as expected'
        );
      }

100 101 102 103 104 105
      if (!await _hasDebugSymbols(ephemeralProfileHostApp)) {
        return TaskResult.failure(
          "Ephemeral host app ${ephemeralProfileHostApp.path}'s App.framework does not contain debug symbols"
        );
      }

106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
      section('Clean build');

      await inDirectory(projectDir, () async {
        await flutter('clean');
      });

      section('Build ephemeral host app in debug mode for simulator without CocoaPods');

      await inDirectory(projectDir, () async {
        await flutter(
          'build',
          options: <String>['ios', '--no-codesign', '--simulator', '--debug'],
        );
      });

      final Directory ephemeralDebugHostApp = Directory(path.join(
        projectDir.path,
        'build',
        'ios',
        'iphonesimulator',
126
        'Runner.app',
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
      ));

      if (!exists(ephemeralDebugHostApp)) {
        return TaskResult.failure('Failed to build ephemeral host .app');
      }

      if (!exists(File(path.join(
        ephemeralDebugHostApp.path,
        'Frameworks',
        'App.framework',
        'flutter_assets',
        'isolate_snapshot_data',
      )))) {
        return TaskResult.failure(
          'Ephemeral host app ${ephemeralDebugHostApp.path} was not a debug build as expected'
        );
      }

145 146 147 148 149 150 151 152 153 154 155 156
      section('Clean build');

      await inDirectory(projectDir, () async {
        await flutter('clean');
      });

      section('Add plugins');

      final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
      String content = await pubspec.readAsString();
      content = content.replaceFirst(
        '\ndependencies:\n',
157
        '\ndependencies:\n  device_info:\n  google_maps_flutter:\n', // One dynamic and one static framework.
158 159 160 161 162 163 164 165 166 167 168 169 170 171
      );
      await pubspec.writeAsString(content, flush: true);
      await inDirectory(projectDir, () async {
        await flutter(
          'packages',
          options: <String>['get'],
        );
      });

      section('Build ephemeral host app with CocoaPods');

      await inDirectory(projectDir, () async {
        await flutter(
          'build',
172
          options: <String>['ios', '--no-codesign'],
173 174 175 176 177 178 179 180
        );
      });

      final bool ephemeralHostAppWithCocoaPodsBuilt = exists(Directory(path.join(
        projectDir.path,
        'build',
        'ios',
        'iphoneos',
181
        'Runner.app',
182 183 184 185 186 187
      )));

      if (!ephemeralHostAppWithCocoaPodsBuilt) {
        return TaskResult.failure('Failed to build ephemeral host .app with CocoaPods');
      }

188 189 190 191 192
      final File podfileLockFile = File(path.join(projectDir.path, '.ios', 'Podfile.lock'));
      final String podfileLockOutput = podfileLockFile.readAsStringSync();
      if (!podfileLockOutput.contains(':path: Flutter/engine')
        || !podfileLockOutput.contains(':path: Flutter/FlutterPluginRegistrant')
        || !podfileLockOutput.contains(':path: Flutter/.symlinks/device_info/ios')
193
        || !podfileLockOutput.contains(':path: Flutter/.symlinks/google_maps_flutter/ios')) {
194 195 196
        return TaskResult.failure('Building ephemeral host app Podfile.lock does not contain expected pods');
      }

197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
      section('Clean build');

      await inDirectory(projectDir, () async {
        await flutter('clean');
      });

      section('Make iOS host app editable');

      await inDirectory(projectDir, () async {
        await flutter(
          'make-host-app-editable',
          options: <String>['ios'],
        );
      });

      section('Build editable host app');

      await inDirectory(projectDir, () async {
        await flutter(
          'build',
217
          options: <String>['ios', '--no-codesign'],
218 219 220 221 222 223 224 225
        );
      });

      final bool editableHostAppBuilt = exists(Directory(path.join(
        projectDir.path,
        'build',
        'ios',
        'iphoneos',
226
        'Runner.app',
227 228 229 230 231
      )));

      if (!editableHostAppBuilt) {
        return TaskResult.failure('Failed to build editable host .app');
      }
232

233
      section('Add to existing iOS Objective-C app');
234

235 236
      final Directory objectiveCHostApp = Directory(path.join(tempDir.path, 'hello_host_app'));
      mkdir(objectiveCHostApp);
237 238
      recursiveCopy(
        Directory(path.join(flutterDirectory.path, 'dev', 'integration_tests', 'ios_host_app')),
239
        objectiveCHostApp,
240 241
      );

242 243 244
      final File objectiveCAnalyticsOutputFile = File(path.join(tempDir.path, 'analytics-objc.log'));
      final Directory objectiveCBuildDirectory = Directory(path.join(tempDir.path, 'build-objc'));
      await inDirectory(objectiveCHostApp, () async {
245 246 247 248 249 250 251
        await exec(
          'pod',
          <String>['install'],
          environment: <String, String>{
            'LANG': 'en_US.UTF-8',
          },
        );
252 253 254 255 256 257 258 259 260 261 262
        await exec(
          'xcodebuild',
          <String>[
            '-workspace',
            'Host.xcworkspace',
            '-scheme',
            'Host',
            '-configuration',
            'Debug',
            'CODE_SIGNING_ALLOWED=NO',
            'CODE_SIGNING_REQUIRED=NO',
Dan Field's avatar
Dan Field committed
263 264
            'CODE_SIGN_IDENTITY=-',
            'EXPANDED_CODE_SIGN_IDENTITY=-',
265
            'CONFIGURATION_BUILD_DIR=${objectiveCBuildDirectory.path}',
266
            'COMPILER_INDEX_STORE_ENABLE=NO',
267
          ],
268
          environment: <String, String> {
269
            'FLUTTER_ANALYTICS_LOG_FILE': objectiveCAnalyticsOutputFile.path,
270
          },
271 272 273 274
        );
      });

      final bool existingAppBuilt = exists(File(path.join(
275
        objectiveCBuildDirectory.path,
276 277 278 279
        'Host.app',
        'Host',
      )));
      if (!existingAppBuilt) {
280
        return TaskResult.failure('Failed to build existing Objective-C app .app');
281 282
      }

283 284 285 286
      final String objectiveCAnalyticsOutput = objectiveCAnalyticsOutputFile.readAsStringSync();
      if (!objectiveCAnalyticsOutput.contains('cd24: ios')
          || !objectiveCAnalyticsOutput.contains('cd25: true')
          || !objectiveCAnalyticsOutput.contains('viewName: build/bundle')) {
287
        return TaskResult.failure(
288
          'Building outer Objective-C app produced the following analytics: "$objectiveCAnalyticsOutput"'
289 290 291 292
          'but not the expected strings: "cd24: ios", "cd25: true", "viewName: build/bundle"'
        );
      }

293
      section('Fail building existing Objective-C iOS app if flutter script fails');
294
      int xcodebuildExitCode = 0;
295
      await inDirectory(objectiveCHostApp, () async {
296 297 298 299 300 301 302 303 304 305 306 307 308 309
        xcodebuildExitCode = await exec(
          'xcodebuild',
          <String>[
            '-workspace',
            'Host.xcworkspace',
            '-scheme',
            'Host',
            '-configuration',
            'Debug',
            'ARCHS=i386', // i386 is not supported in Debug mode.
            'CODE_SIGNING_ALLOWED=NO',
            'CODE_SIGNING_REQUIRED=NO',
            'CODE_SIGN_IDENTITY=-',
            'EXPANDED_CODE_SIGN_IDENTITY=-',
310
            'CONFIGURATION_BUILD_DIR=${objectiveCBuildDirectory.path}',
311
            'COMPILER_INDEX_STORE_ENABLE=NO',
312
          ],
313
          canFail: true,
314 315 316 317
        );
      });

      if (xcodebuildExitCode != 65) { // 65 returned on PhaseScriptExecution failure.
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
        return TaskResult.failure('Host Objective-C app build succeeded though flutter script failed');
      }

      section('Add to existing iOS Swift app');

      final Directory swiftHostApp = Directory(path.join(tempDir.path, 'hello_host_app_swift'));
      mkdir(swiftHostApp);
      recursiveCopy(
        Directory(path.join(flutterDirectory.path, 'dev', 'integration_tests', 'ios_host_app_swift')),
        swiftHostApp,
      );

      final File swiftAnalyticsOutputFile = File(path.join(tempDir.path, 'analytics-swift.log'));
      final Directory swiftBuildDirectory = Directory(path.join(tempDir.path, 'build-swift'));

      await inDirectory(swiftHostApp, () async {
334 335 336 337 338 339 340
        await exec(
          'pod',
          <String>['install'],
          environment: <String, String>{
            'LANG': 'en_US.UTF-8',
          },
        );
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
        await exec(
          'xcodebuild',
          <String>[
            '-workspace',
            'Host.xcworkspace',
            '-scheme',
            'Host',
            '-configuration',
            'Debug',
            'CODE_SIGNING_ALLOWED=NO',
            'CODE_SIGNING_REQUIRED=NO',
            'CODE_SIGN_IDENTITY=-',
            'EXPANDED_CODE_SIGN_IDENTITY=-',
            'CONFIGURATION_BUILD_DIR=${swiftBuildDirectory.path}',
            'COMPILER_INDEX_STORE_ENABLE=NO',
          ],
          environment: <String, String> {
            'FLUTTER_ANALYTICS_LOG_FILE': swiftAnalyticsOutputFile.path,
359
          },
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
        );
      });

      final bool existingSwiftAppBuilt = exists(File(path.join(
        swiftBuildDirectory.path,
        'Host.app',
        'Host',
      )));
      if (!existingSwiftAppBuilt) {
        return TaskResult.failure('Failed to build existing Swift app .app');
      }

      final String swiftAnalyticsOutput = swiftAnalyticsOutputFile.readAsStringSync();
      if (!swiftAnalyticsOutput.contains('cd24: ios')
          || !swiftAnalyticsOutput.contains('cd25: true')
          || !swiftAnalyticsOutput.contains('viewName: build/bundle')) {
        return TaskResult.failure(
          'Building outer Swift app produced the following analytics: "$swiftAnalyticsOutput"'
          'but not the expected strings: "cd24: ios", "cd25: true", "viewName: build/bundle"'
        );
380 381
      }

382 383 384 385
      return TaskResult.success(null);
    } catch (e) {
      return TaskResult.failure(e.toString());
    } finally {
386
      rmTree(tempDir);
387 388 389
    }
  });
}
390 391 392 393 394 395

Future<bool> _isAppAotBuild(Directory app) async {
  final String binary = path.join(
    app.path,
    'Frameworks',
    'App.framework',
396
    'App',
397 398 399 400 401 402 403 404 405 406 407 408
  );

  final String symbolTable = await eval(
    'nm',
    <String> [
      '-gU',
      binary,
    ],
  );

  return symbolTable.contains('kDartIsolateSnapshotInstructions');
}
409 410 411 412 413 414

Future<bool> _hasDebugSymbols(Directory app) async {
  final String binary = path.join(
    app.path,
    'Frameworks',
    'App.framework',
415
    'App',
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
  );

  final String symbolTable = await eval(
    'dsymutil',
    <String> [
      '--dump-debug-map',
      binary,
    ],
    // The output is huge.
    printStdout: false,
  );

  // Search for some random Flutter framework Dart function which should always
  // be in App.framework.
  return symbolTable.contains('BuildOwner_reassemble');
}