mac_test.dart 15.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:file/file.dart';
6
import 'package:file/memory.dart';
7
import 'package:flutter_tools/src/artifacts.dart';
8
import 'package:flutter_tools/src/base/file_system.dart';
9
import 'package:flutter_tools/src/base/logger.dart';
10
import 'package:flutter_tools/src/base/process.dart';
11
import 'package:flutter_tools/src/build_info.dart';
12
import 'package:flutter_tools/src/cache.dart';
13
import 'package:flutter_tools/src/device.dart';
14
import 'package:flutter_tools/src/ios/code_signing.dart';
15
import 'package:flutter_tools/src/ios/mac.dart';
16
import 'package:flutter_tools/src/ios/xcresult.dart';
17
import 'package:flutter_tools/src/project.dart';
18
import 'package:flutter_tools/src/reporting/reporting.dart';
19
import 'package:test/fake.dart';
20

21
import '../../src/common.dart';
22
import '../../src/fake_process_manager.dart';
23
import '../../src/fakes.dart';
24 25

void main() {
26
  late BufferLogger logger;
27 28 29 30 31

  setUp(() {
    logger = BufferLogger.test();
  });

32
  group('IMobileDevice', () {
33 34
    late Artifacts artifacts;
    late Cache cache;
35 36

    setUp(() {
37 38 39 40
      artifacts = Artifacts.test();
      cache = Cache.test(
        artifacts: <ArtifactSet>[
          FakeDyldEnvironmentArtifact(),
41 42 43
        ],
        processManager: FakeProcessManager.any(),
      );
44 45
    });

46
    group('screenshot', () {
47 48
      late FakeProcessManager fakeProcessManager;
      late File outputFile;
49 50

      setUp(() {
51
        fakeProcessManager = FakeProcessManager.empty();
52
        outputFile = MemoryFileSystem.test().file('image.png');
53 54
      });

55
      testWithoutContext('error if idevicescreenshot is not installed', () async {
56
        // Let `idevicescreenshot` fail with exit code 1.
57 58
        fakeProcessManager.addCommand(FakeCommand(
          command: <String>[
59
            'HostArtifact.idevicescreenshot',
60 61 62 63 64 65 66 67 68
            outputFile.path,
            '--udid',
            '1234',
          ],
          environment: const <String, String>{
            'DYLD_LIBRARY_PATH': '/path/to/libraries',
          },
          exitCode: 1,
        ));
69

70
        final IMobileDevice iMobileDevice = IMobileDevice(
71 72
          artifacts: artifacts,
          cache: cache,
73
          processManager: fakeProcessManager,
74 75 76
          logger: logger,
        );

77
        expect(() async => iMobileDevice.takeScreenshot(
78
          outputFile,
79
          '1234',
80
          DeviceConnectionInterface.attached,
81
        ), throwsA(anything));
82
        expect(fakeProcessManager, hasNoRemainingExpectations);
83 84
      });

85
      testWithoutContext('idevicescreenshot captures and returns USB screenshot', () async {
86 87
        fakeProcessManager.addCommand(FakeCommand(
          command: <String>[
88
            'HostArtifact.idevicescreenshot', outputFile.path, '--udid', '1234',
89 90 91
          ],
          environment: const <String, String>{'DYLD_LIBRARY_PATH': '/path/to/libraries'},
        ));
92

93
        final IMobileDevice iMobileDevice = IMobileDevice(
94 95
          artifacts: artifacts,
          cache: cache,
96
          processManager: fakeProcessManager,
97 98 99
          logger: logger,
        );

100
        await iMobileDevice.takeScreenshot(
101
          outputFile,
102
          '1234',
103
          DeviceConnectionInterface.attached,
104
        );
105
        expect(fakeProcessManager, hasNoRemainingExpectations);
106
      });
107 108

      testWithoutContext('idevicescreenshot captures and returns network screenshot', () async {
109 110
        fakeProcessManager.addCommand(FakeCommand(
          command: <String>[
111
            'HostArtifact.idevicescreenshot', outputFile.path, '--udid', '1234', '--network',
112 113 114
          ],
          environment: const <String, String>{'DYLD_LIBRARY_PATH': '/path/to/libraries'},
        ));
115 116

        final IMobileDevice iMobileDevice = IMobileDevice(
117 118
          artifacts: artifacts,
          cache: cache,
119
          processManager: fakeProcessManager,
120 121 122 123
          logger: logger,
        );

        await iMobileDevice.takeScreenshot(
124
          outputFile,
125
          '1234',
126
          DeviceConnectionInterface.wireless,
127
        );
128
        expect(fakeProcessManager, hasNoRemainingExpectations);
129
      });
130 131 132
    });
  });

133
  group('Diagnose Xcode build failure', () {
134 135
    late Map<String, String> buildSettings;
    late TestUsage testUsage;
136 137

    setUp(() {
xster's avatar
xster committed
138 139 140
      buildSettings = <String, String>{
        'PRODUCT_BUNDLE_IDENTIFIER': 'test.app',
      };
141
      testUsage = TestUsage();
142 143
    });

144
    testWithoutContext('Sends analytics when bitcode fails', () async {
145 146 147 148 149 150 151
      const List<String> buildCommands = <String>['xcrun', 'cc', 'blah'];
      final XcodeBuildResult buildResult = XcodeBuildResult(
        success: false,
        stdout: 'BITCODE_ENABLED = YES',
        xcodeBuildExecution: XcodeBuildExecution(
          buildCommands: buildCommands,
          appDirectory: '/blah/blah',
152
          environmentType: EnvironmentType.physical,
153 154 155 156
          buildSettings: buildSettings,
        ),
      );

157 158 159 160
      await diagnoseXcodeBuildFailure(buildResult, testUsage, logger);
      expect(testUsage.events, contains(
        TestUsageEvent(
          'build',
161
          'ios',
162
          label: 'xcode-bitcode-failure',
163 164 165 166
          parameters: CustomDimensions(
            buildEventCommand: buildCommands.toString(),
            buildEventSettings: buildSettings.toString(),
          ),
167 168
        ),
      ));
169 170
    });

171 172 173 174 175
    testWithoutContext('fallback to stdout: No provisioning profile shows message', () async {
      final Map<String, String> buildSettingsWithDevTeam = <String, String>{
        'PRODUCT_BUNDLE_IDENTIFIER': 'test.app',
        'DEVELOPMENT_TEAM': 'a team',
      };
176
      final XcodeBuildResult buildResult = XcodeBuildResult(
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
        success: false,
        stdout: '''
Launching lib/main.dart on iPhone in debug mode...
Signing iOS app for device deployment using developer identity: "iPhone Developer: test@flutter.io (1122334455)"
Running Xcode build...                                1.3s
Failed to build iOS app
Error output from Xcode build:

    ** BUILD FAILED **


    The following build commands failed:
    	Check dependencies
    (1 failure)
Xcode's output:

    Build settings from command line:
        ARCHS = arm64
        BUILD_DIR = /Users/blah/blah
        DEVELOPMENT_TEAM = AABBCCDDEE
        ONLY_ACTIVE_ARCH = YES
        SDKROOT = iphoneos10.3

    === CLEAN TARGET Runner OF PROJECT Runner WITH CONFIGURATION Release ===

    Check dependencies
203
    [BCEROR]"Runner" requires a provisioning profile. Select a provisioning profile in the Signing & Capabilities editor.
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
    [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3'
    [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3'
    [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3'

    Create product structure
    /bin/mkdir -p /Users/blah/Runner.app

    Clean.Remove clean /Users/blah/Runner.app.dSYM
        builtin-rm -rf /Users/blah/Runner.app.dSYM

    Clean.Remove clean /Users/blah/Runner.app
        builtin-rm -rf /Users/blah/Runner.app

    Clean.Remove clean /Users/blah/Runner-dfvicjniknvzghgwsthwtgcjhtsk/Build/Intermediates/Runner.build/Release-iphoneos/Runner.build
        builtin-rm -rf /Users/blah/Runner-dfvicjniknvzghgwsthwtgcjhtsk/Build/Intermediates/Runner.build/Release-iphoneos/Runner.build

    ** CLEAN SUCCEEDED **

    === BUILD TARGET Runner OF PROJECT Runner WITH CONFIGURATION Release ===

    Check dependencies
225
    No profiles for 'com.example.test' were found:  Xcode couldn't find a provisioning profile matching 'com.example.test'.
226 227 228 229 230 231 232
    Code signing is required for product type 'Application' in SDK 'iOS 10.3'
    Code signing is required for product type 'Application' in SDK 'iOS 10.3'
    Code signing is required for product type 'Application' in SDK 'iOS 10.3'

Could not build the precompiled application for the device.

Error launching application on iPhone.''',
233
        xcodeBuildExecution: XcodeBuildExecution(
xster's avatar
xster committed
234 235
          buildCommands: <String>['xcrun', 'xcodebuild', 'blah'],
          appDirectory: '/blah/blah',
236
          environmentType: EnvironmentType.physical,
237
          buildSettings: buildSettingsWithDevTeam,
238 239 240
        ),
      );

241
      await diagnoseXcodeBuildFailure(buildResult, testUsage, logger);
242
      expect(
243
        logger.errorText,
244
        contains(noProvisioningProfileInstruction),
245
      );
246
    });
247

248 249 250 251 252 253 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 282 283 284 285
    testWithoutContext('fallback to stdout: Ineligible destinations', () async {
      final Map<String, String> buildSettingsWithDevTeam = <String, String>{
        'PRODUCT_BUNDLE_IDENTIFIER': 'test.app',
        'DEVELOPMENT_TEAM': 'a team',
      };
      final XcodeBuildResult buildResult = XcodeBuildResult(
        success: false,
        stderr: '''
Launching lib/main.dart on iPhone in debug mode...
Signing iOS app for device deployment using developer identity: "iPhone Developer: test@flutter.io (1122334455)"
Running Xcode build...                                1.3s
Failed to build iOS app
Error output from Xcode build:

    xcodebuild: error: Unable to find a destination matching the provided destination specifier:
               		{ id:1234D567-890C-1DA2-34E5-F6789A0123C4 }

               	Ineligible destinations for the "Runner" scheme:
               		{ platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device, error:iOS 17.0 is not installed. To use with Xcode, first download and install the platform }

Could not build the precompiled application for the device.

Error launching application on iPhone.''',
        xcodeBuildExecution: XcodeBuildExecution(
          buildCommands: <String>['xcrun', 'xcodebuild', 'blah'],
          appDirectory: '/blah/blah',
          environmentType: EnvironmentType.physical,
          buildSettings: buildSettingsWithDevTeam,
        ),
      );

      await diagnoseXcodeBuildFailure(buildResult, testUsage, logger);
      expect(
        logger.errorText,
        contains(missingPlatformInstructions('iOS 17.0')),
      );
    });

286
    testWithoutContext('No development team shows message', () async {
287
      final XcodeBuildResult buildResult = XcodeBuildResult(
288 289
        success: false,
        stdout: '''
290
Running "flutter pub get" in flutter_gallery...  0.6s
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
Launching lib/main.dart on x in release mode...
Running pod install...                                1.2s
Running Xcode build...                                1.4s
Failed to build iOS app
Error output from Xcode build:

    ** BUILD FAILED **


    The following build commands failed:
    	Check dependencies
    (1 failure)
Xcode's output:

    blah

    Check dependencies
    [BCEROR]Signing for "Runner" requires a development team. Select a development team in the project editor.

310 311 312 313 314 315 316 317
Could not build the precompiled application for the device.''',
        xcodeBuildExecution: XcodeBuildExecution(
          buildCommands: <String>['xcrun', 'xcodebuild', 'blah'],
          appDirectory: '/blah/blah',
          environmentType: EnvironmentType.physical,
          buildSettings: buildSettings,
        ),
      );
318

319 320 321 322 323 324
      await diagnoseXcodeBuildFailure(buildResult, testUsage, logger);
      expect(
        logger.errorText,
        contains('Building a deployable iOS app requires a selected Development Team with a \nProvisioning Profile.'),
      );
    });
325

326 327 328 329 330 331 332 333 334 335 336 337
    testWithoutContext('does not show no development team message when other Xcode issues detected', () async {
      final XcodeBuildResult buildResult = XcodeBuildResult(
        success: false,
        stdout: '''
Running "flutter pub get" in flutter_gallery...  0.6s
Launching lib/main.dart on x in release mode...
Running pod install...                                1.2s
Running Xcode build...                                1.4s
Failed to build iOS app
Error output from Xcode build:

    ** BUILD FAILED **
338 339


340 341 342 343 344
    The following build commands failed:
    	Check dependencies
    (1 failure)
Xcode's output:

345 346 347
    blah

    Check dependencies
348
    [BCEROR]Signing for "Runner" requires a development team. Select a development team in the project editor.
349 350

Could not build the precompiled application for the device.''',
351
        xcodeBuildExecution: XcodeBuildExecution(
xster's avatar
xster committed
352 353
          buildCommands: <String>['xcrun', 'xcodebuild', 'blah'],
          appDirectory: '/blah/blah',
354
          environmentType: EnvironmentType.physical,
xster's avatar
xster committed
355
          buildSettings: buildSettings,
356
        ),
357 358 359
        xcResult: XCResult.test(issues: <XCResultIssue>[
          XCResultIssue.test(message: 'Target aot_assembly_release failed', subType: 'Error'),
        ])
360 361
      );

362
      await diagnoseXcodeBuildFailure(buildResult, testUsage, logger);
363 364
      expect(logger.errorText, contains('Error (Xcode): Target aot_assembly_release failed'));
      expect(logger.errorText, isNot(contains('Building a deployable iOS app requires a selected Development Team')));
365
    });
366
  });
367 368

  group('Upgrades project.pbxproj for old asset usage', () {
369 370 371 372 373 374 375 376 377 378 379 380 381
    const String flutterAssetPbxProjLines =
      '/* flutter_assets */\n'
      '/* App.framework\n'
      'another line';

    const String appFlxPbxProjLines =
      '/* app.flx\n'
      '/* App.framework\n'
      'another line';

    const String cleanPbxProjLines =
      '/* App.framework\n'
      'another line';
382

383
    testWithoutContext('upgradePbxProjWithFlutterAssets', () async {
384 385
      final File pbxprojFile = MemoryFileSystem.test().file('project.pbxproj')
        ..writeAsStringSync(flutterAssetPbxProjLines);
386
      final FakeIosProject project = FakeIosProject(pbxprojFile);
387

388
      bool result = upgradePbxProjWithFlutterAssets(project, logger);
389 390
      expect(result, true);
      expect(
391
        logger.statusText,
392 393
        contains('Removing obsolete reference to flutter_assets'),
      );
394
      logger.clear();
395

396
      pbxprojFile.writeAsStringSync(appFlxPbxProjLines);
397
      result = upgradePbxProjWithFlutterAssets(project, logger);
398 399
      expect(result, true);
      expect(
400
        logger.statusText,
401 402
        contains('Removing obsolete reference to app.flx'),
      );
403
      logger.clear();
404

405
      pbxprojFile.writeAsStringSync(cleanPbxProjLines);
406
      result = upgradePbxProjWithFlutterAssets(project, logger);
407 408
      expect(result, true);
      expect(
409
        logger.statusText,
410 411 412 413
        isEmpty,
      );
    });
  });
414 415

  group('remove Finder extended attributes', () {
416
    late Directory projectDirectory;
417 418
    setUp(() {
      final MemoryFileSystem fs = MemoryFileSystem.test();
419
      projectDirectory = fs.directory('flutter_project');
420 421 422 423 424 425 426 427 428
    });

    testWithoutContext('removes xattr', () async {
      final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
        FakeCommand(command: <String>[
          'xattr',
          '-r',
          '-d',
          'com.apple.FinderInfo',
429
          projectDirectory.path,
430
        ]),
431 432
      ]);

433
      await removeFinderExtendedAttributes(projectDirectory, ProcessUtils(processManager: processManager, logger: logger), logger);
434
      expect(processManager, hasNoRemainingExpectations);
435 436 437 438
    });

    testWithoutContext('ignores errors', () async {
      final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
439 440 441 442 443 444 445 446 447 448
        FakeCommand(
          command: <String>[
            'xattr',
            '-r',
            '-d',
            'com.apple.FinderInfo',
            projectDirectory.path,
          ],
          exitCode: 1,
        ),
449 450
      ]);

451
      await removeFinderExtendedAttributes(projectDirectory, ProcessUtils(processManager: processManager, logger: logger), logger);
452
      expect(logger.traceText, contains('Failed to remove xattr com.apple.FinderInfo'));
453
      expect(processManager, hasNoRemainingExpectations);
454 455
    });
  });
456
}
457 458 459 460 461 462 463

class FakeIosProject extends Fake implements IosProject {
  FakeIosProject(this.xcodeProjectInfoFile);
  @override
  final File xcodeProjectInfoFile;

  @override
464
  Future<String> hostAppBundleName(BuildInfo? buildInfo) async => 'UnitTestRunner.app';
465 466

  @override
467
  Directory get xcodeProject => xcodeProjectInfoFile.fileSystem.directory('Runner.xcodeproj');
468
}