module_test_ios.dart 12.8 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
// 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';
9
import 'package:flutter_devicelab/framework/ios.dart';
10 11 12
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:path/path.dart' as path;

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

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

34 35 36 37 38 39 40 41 42 43 44 45
      // Copy test dart files to new module app.
      final Directory flutterModuleLibSource = Directory(path.join(flutterDirectory.path, 'dev', 'integration_tests', 'ios_host_app', 'flutterapp', 'lib'));
      final Directory flutterModuleLibDestination = Directory(path.join(projectDir.path, 'lib'));

      // These test files don't have a .dart prefix so the analyzer will ignore them. They aren't in a
      // package and don't work on their own outside of the test module just created.
      final File main = File(path.join(flutterModuleLibSource.path, 'main'));
      main.copySync(path.join(flutterModuleLibDestination.path, 'main.dart'));

      final File marquee = File(path.join(flutterModuleLibSource.path, 'marquee'));
      marquee.copySync(path.join(flutterModuleLibDestination.path, 'marquee.dart'));

46
      section('Build ephemeral host app in release mode without CocoaPods');
47 48 49 50

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

55
      final Directory ephemeralReleaseHostApp = Directory(path.join(
56 57 58 59
        projectDir.path,
        'build',
        'ios',
        'iphoneos',
60
        'Runner.app',
61
      ));
62

63
      if (!exists(ephemeralReleaseHostApp)) {
64 65 66
        return TaskResult.failure('Failed to build ephemeral host .app');
      }

67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
      if (!await _isAppAotBuild(ephemeralReleaseHostApp)) {
        return TaskResult.failure(
          'Ephemeral host app ${ephemeralReleaseHostApp.path} was not a release build as expected'
        );
      }

      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',
93
        'Runner.app',
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
      ));

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

      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
      section('Add to existing iOS Objective-C app');
198

199 200
      final Directory objectiveCHostApp = Directory(path.join(tempDir.path, 'hello_host_app'));
      mkdir(objectiveCHostApp);
201 202
      recursiveCopy(
        Directory(path.join(flutterDirectory.path, 'dev', 'integration_tests', 'ios_host_app')),
203
        objectiveCHostApp,
204 205
      );

206 207 208
      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 {
209 210 211 212 213 214 215
        await exec(
          'pod',
          <String>['install'],
          environment: <String, String>{
            'LANG': 'en_US.UTF-8',
          },
        );
216 217 218 219 220 221 222 223 224 225 226
        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
227 228
            'CODE_SIGN_IDENTITY=-',
            'EXPANDED_CODE_SIGN_IDENTITY=-',
229
            'CONFIGURATION_BUILD_DIR=${objectiveCBuildDirectory.path}',
230
            'COMPILER_INDEX_STORE_ENABLE=NO',
231
          ],
232
          environment: <String, String> {
233
            'FLUTTER_ANALYTICS_LOG_FILE': objectiveCAnalyticsOutputFile.path,
234
          },
235 236 237 238
        );
      });

      final bool existingAppBuilt = exists(File(path.join(
239
        objectiveCBuildDirectory.path,
240 241 242 243
        'Host.app',
        'Host',
      )));
      if (!existingAppBuilt) {
244
        return TaskResult.failure('Failed to build existing Objective-C app .app');
245 246
      }

247 248 249
      final String objectiveCAnalyticsOutput = objectiveCAnalyticsOutputFile.readAsStringSync();
      if (!objectiveCAnalyticsOutput.contains('cd24: ios')
          || !objectiveCAnalyticsOutput.contains('cd25: true')
250
          || !objectiveCAnalyticsOutput.contains('viewName: assemble')) {
251
        return TaskResult.failure(
252
          'Building outer Objective-C app produced the following analytics: "$objectiveCAnalyticsOutput" '
253
          'but not the expected strings: "cd24: ios", "cd25: true", "viewName: assemble"'
254 255 256
        );
      }

257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
      section('Run platform unit tests');
      await testWithNewiOSSimulator('TestAdd2AppSim', (String deviceId) =>
        inDirectory(objectiveCHostApp, () =>
          exec(
            'xcodebuild',
            <String>[
              '-workspace',
              'Host.xcworkspace',
              '-scheme',
              'Host',
              '-configuration',
              'Debug',
              '-destination',
              'id=$deviceId',
              'test',
              'CODE_SIGNING_ALLOWED=NO',
              'CODE_SIGNING_REQUIRED=NO',
              'CODE_SIGN_IDENTITY=-',
              'EXPANDED_CODE_SIGN_IDENTITY=-',
              'COMPILER_INDEX_STORE_ENABLE=NO',
            ],
          )
        )
      );

282
      section('Fail building existing Objective-C iOS app if flutter script fails');
283 284
      final int xcodebuildExitCode = await inDirectory<int>(objectiveCHostApp, () =>
        exec(
285 286 287 288 289 290 291 292 293 294 295 296 297
          '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=-',
298
            'CONFIGURATION_BUILD_DIR=${objectiveCBuildDirectory.path}',
299
            'COMPILER_INDEX_STORE_ENABLE=NO',
300
          ],
301
          canFail: true,
302 303
        )
      );
304 305

      if (xcodebuildExitCode != 65) { // 65 returned on PhaseScriptExecution failure.
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
        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 {
322 323 324 325 326 327 328
        await exec(
          'pod',
          <String>['install'],
          environment: <String, String>{
            'LANG': 'en_US.UTF-8',
          },
        );
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
        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,
347
          },
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
        );
      });

      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')
363
          || !swiftAnalyticsOutput.contains('viewName: assemble')) {
364
        return TaskResult.failure(
365
          'Building outer Swift app produced the following analytics: "$swiftAnalyticsOutput" '
366
          'but not the expected strings: "cd24: ios", "cd25: true", "viewName: assemble"'
367
        );
368 369
      }

370 371 372 373
      return TaskResult.success(null);
    } catch (e) {
      return TaskResult.failure(e.toString());
    } finally {
374
      rmTree(tempDir);
375 376 377
    }
  });
}
378 379 380 381 382 383

Future<bool> _isAppAotBuild(Directory app) async {
  final String binary = path.join(
    app.path,
    'Frameworks',
    'App.framework',
384
    'App',
385 386 387 388 389 390 391 392 393 394 395 396
  );

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

  return symbolTable.contains('kDartIsolateSnapshotInstructions');
}