module_test_ios.dart 13.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
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:io';

import 'package:flutter_devicelab/framework/framework.dart';
8
import 'package:flutter_devicelab/framework/ios.dart';
9
import 'package:flutter_devicelab/framework/task_result.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
    String simulatorDeviceId;
18
    section('Create Flutter module project');
19

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

35 36 37 38 39 40 41 42 43 44 45 46
      // 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'));

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

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

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

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

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

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

89
      if (!exists(ephemeralIOSHostApp)) {
90 91 92
        return TaskResult.failure('Failed to build ephemeral host .app');
      }

93
      if (!await _isAppAotBuild(ephemeralIOSHostApp)) {
94
        return TaskResult.failure(
95
          'Ephemeral host app ${ephemeralIOSHostApp.path} was not a profile build as expected'
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
        );
      }

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

114
      final Directory ephemeralSimulatorHostApp = Directory(path.join(
115 116 117 118
        projectDir.path,
        'build',
        'ios',
        'iphonesimulator',
119
        'Runner.app',
120 121
      ));

122
      if (!exists(ephemeralSimulatorHostApp)) {
123 124 125 126
        return TaskResult.failure('Failed to build ephemeral host .app');
      }

      if (!exists(File(path.join(
127
        ephemeralSimulatorHostApp.path,
128 129 130 131 132 133
        'Frameworks',
        'App.framework',
        'flutter_assets',
        'isolate_snapshot_data',
      )))) {
        return TaskResult.failure(
134
          'Ephemeral host app ${ephemeralSimulatorHostApp.path} was not a debug build as expected'
135 136 137
        );
      }

138 139 140 141 142 143 144 145 146 147 148 149
      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',
150
        // One dynamic framework, one static framework, and one that does not support iOS.
151
        '\ndependencies:\n  device_info: 0.4.2+4\n  google_sign_in: 4.5.1\n  android_alarm_manager: 0.4.5+11\n',
152 153 154 155 156 157 158 159 160 161 162 163 164 165
      );
      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',
166
          options: <String>['ios', '--no-codesign', '-v'],
167 168 169
        );
      });

170
      final bool ephemeralHostAppWithCocoaPodsBuilt = exists(ephemeralIOSHostApp);
171 172 173 174 175

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

176 177 178 179 180
      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')
181
        || !podfileLockOutput.contains(':path: Flutter/.symlinks/google_sign_in/ios')
182
        || podfileLockOutput.contains('android_alarm_manager')) {
183 184 185
        return TaskResult.failure('Building ephemeral host app Podfile.lock does not contain expected pods');
      }

186 187 188
      checkFileExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', 'device_info.framework', 'device_info'));

      // Static, no embedded framework.
189
      checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', 'google_sign_in.framework'));
190 191 192 193

      // Android-only, no embedded framework.
      checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', 'android_alarm_manager.framework'));

194 195 196 197 198 199 200 201 202 203
      section('Clean and pub get module');

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

      await inDirectory(projectDir, () async {
        await flutter('pub', options: <String>['get']);
      });

204
      section('Add to existing iOS Objective-C app');
205

206 207
      final Directory objectiveCHostApp = Directory(path.join(tempDir.path, 'hello_host_app'));
      mkdir(objectiveCHostApp);
208 209
      recursiveCopy(
        Directory(path.join(flutterDirectory.path, 'dev', 'integration_tests', 'ios_host_app')),
210
        objectiveCHostApp,
211 212
      );

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

      final bool existingAppBuilt = exists(File(path.join(
246
        objectiveCBuildDirectory.path,
247 248 249 250
        'Host.app',
        'Host',
      )));
      if (!existingAppBuilt) {
251
        return TaskResult.failure('Failed to build existing Objective-C app .app');
252 253
      }

254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
      checkFileExists(path.join(
        objectiveCBuildDirectory.path,
        'Host.app',
        'Frameworks',
        'Flutter.framework',
        'Flutter',
      ));

      checkFileExists(path.join(
        objectiveCBuildDirectory.path,
        'Host.app',
        'Frameworks',
        'App.framework',
        'flutter_assets',
        'isolate_snapshot_data',
      ));

271 272 273
      final String objectiveCAnalyticsOutput = objectiveCAnalyticsOutputFile.readAsStringSync();
      if (!objectiveCAnalyticsOutput.contains('cd24: ios')
          || !objectiveCAnalyticsOutput.contains('cd25: true')
274
          || !objectiveCAnalyticsOutput.contains('viewName: assemble')) {
275
        return TaskResult.failure(
276
          'Building outer Objective-C app produced the following analytics: "$objectiveCAnalyticsOutput" '
277
          'but not the expected strings: "cd24: ios", "cd25: true", "viewName: assemble"'
278 279 280
        );
      }

281
      section('Run platform unit tests');
282 283 284
      await testWithNewIOSSimulator('TestAdd2AppSim', (String deviceId) {
        simulatorDeviceId = deviceId;
        return inDirectory(objectiveCHostApp, () =>
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
          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',
            ],
303 304
          ));
        }
305 306
      );

307
      section('Fail building existing Objective-C iOS app if flutter script fails');
308 309
      final int xcodebuildExitCode = await inDirectory<int>(objectiveCHostApp, () =>
        exec(
310 311 312 313 314 315 316 317 318 319 320 321 322
          '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=-',
323
            'CONFIGURATION_BUILD_DIR=${objectiveCBuildDirectory.path}',
324
            'COMPILER_INDEX_STORE_ENABLE=NO',
325
          ],
326
          canFail: true,
327 328
        )
      );
329 330

      if (xcodebuildExitCode != 65) { // 65 returned on PhaseScriptExecution failure.
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
        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 {
347 348 349 350 351 352 353
        await exec(
          'pod',
          <String>['install'],
          environment: <String, String>{
            'LANG': 'en_US.UTF-8',
          },
        );
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
        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,
372
          },
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
        );
      });

      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')
388
          || !swiftAnalyticsOutput.contains('viewName: assemble')) {
389
        return TaskResult.failure(
390
          'Building outer Swift app produced the following analytics: "$swiftAnalyticsOutput" '
391
          'but not the expected strings: "cd24: ios", "cd25: true", "viewName: assemble"'
392
        );
393 394
      }

395 396 397 398
      return TaskResult.success(null);
    } catch (e) {
      return TaskResult.failure(e.toString());
    } finally {
399
      removeIOSimulator(simulatorDeviceId);
400
      rmTree(tempDir);
401 402 403
    }
  });
}
404 405 406 407 408 409

Future<bool> _isAppAotBuild(Directory app) async {
  final String binary = path.join(
    app.path,
    'Frameworks',
    'App.framework',
410
    'App',
411 412 413 414 415 416 417 418 419 420 421 422
  );

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

  return symbolTable.contains('kDartIsolateSnapshotInstructions');
}