build_macos_test.dart 14.3 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 6
// @dart = 2.8

7 8
import 'dart:async';

9
import 'package:args/command_runner.dart';
10
import 'package:file/memory.dart';
11
import 'package:flutter_tools/src/artifacts.dart';
12
import 'package:flutter_tools/src/base/file_system.dart';
13
import 'package:flutter_tools/src/base/logger.dart';
14
import 'package:flutter_tools/src/base/platform.dart';
15
import 'package:flutter_tools/src/build_info.dart';
16 17
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/build.dart';
18
import 'package:flutter_tools/src/commands/build_macos.dart';
19
import 'package:flutter_tools/src/features.dart';
20
import 'package:flutter_tools/src/ios/xcodeproj.dart';
21
import 'package:flutter_tools/src/project.dart';
22
import 'package:flutter_tools/src/reporting/reporting.dart';
23

24 25
import '../../src/common.dart';
import '../../src/context.dart';
26
import '../../src/fakes.dart';
27
import '../../src/test_flutter_command_runner.dart';
28

29 30
class FakeXcodeProjectInterpreterWithProfile extends FakeXcodeProjectInterpreter {
  @override
31
  Future<XcodeProjectInfo> getInfo(String projectPath, { String projectFilename }) async {
32 33 34 35
    return XcodeProjectInfo(
      <String>['Runner'],
      <String>['Debug', 'Profile', 'Release'],
      <String>['Runner'],
36
      BufferLogger.test(),
37 38 39 40
    );
  }
}

41 42 43 44
final Platform macosPlatform = FakePlatform(
  operatingSystem: 'macos',
  environment: <String, String>{
    'FLUTTER_ROOT': '/',
45
    'HOME': '/',
46 47 48 49 50 51 52 53 54
  }
);
final Platform notMacosPlatform = FakePlatform(
  operatingSystem: 'linux',
  environment: <String, String>{
    'FLUTTER_ROOT': '/',
  }
);

55
void main() {
56
  FileSystem fileSystem;
57
  TestUsage usage;
58

59 60
  setUpAll(() {
    Cache.disableLocking();
61
  });
62 63

  setUp(() {
64
    fileSystem = MemoryFileSystem.test();
65
    usage = TestUsage();
66
  });
67

68 69
  // Sets up the minimal mock project files necessary to look like a Flutter project.
  void createCoreMockProjectFiles() {
70 71 72
    fileSystem.file('pubspec.yaml').createSync();
    fileSystem.file('.packages').createSync();
    fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true);
73 74
  }

75 76
  // Sets up the minimal mock project files necessary for macOS builds to succeed.
  void createMinimalMockProjectFiles() {
77
    fileSystem.directory(fileSystem.path.join('macos', 'Runner.xcworkspace')).createSync(recursive: true);
78 79 80
    createCoreMockProjectFiles();
  }

81
  // Creates a FakeCommand for the xcodebuild call to build the app
82
  // in the given configuration.
83
  FakeCommand setUpFakeXcodeBuildHandler(String configuration, { bool verbose = false, void Function() onRun }) {
84 85 86 87 88 89 90 91 92 93 94 95 96
    final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
    final Directory flutterBuildDir = fileSystem.directory(getMacOSBuildDirectory());
    return FakeCommand(
      command: <String>[
        '/usr/bin/env',
        'xcrun',
        'xcodebuild',
        '-workspace', flutterProject.macos.xcodeWorkspace.path,
        '-configuration', configuration,
        '-scheme', 'Runner',
        '-derivedDataPath', flutterBuildDir.absolute.path,
        'OBJROOT=${fileSystem.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}',
        'SYMROOT=${fileSystem.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}',
97
        if (verbose)
98 99 100
          'VERBOSE_SCRIPT_LOGGING=YES'
        else
          '-quiet',
101 102 103 104 105 106 107
        'COMPILER_INDEX_STORE_ENABLE=NO',
      ],
      stdout: 'STDOUT STUFF',
      onRun: () {
        fileSystem.file(fileSystem.path.join('macos', 'Flutter', 'ephemeral', '.app_filename'))
          ..createSync(recursive: true)
          ..writeAsStringSync('example.app');
108 109 110
        if (onRun != null) {
          onRun();
        }
111 112
      }
    );
113 114
  }

115 116
  testUsingContext('macOS build fails when there is no macos project', () async {
    final BuildCommand command = BuildCommand();
117
    createCoreMockProjectFiles();
118

119
    expect(createTestCommandRunner(command).run(
120
      const <String>['build', 'macos', '--no-pub']
121 122 123
    ), throwsToolExit(message: 'No macOS desktop project configured. See '
      'https://flutter.dev/desktop#add-desktop-support-to-an-existing-flutter-app '
      'to learn about adding macOS support to a project.'));
124 125
  }, overrides: <Type, Generator>{
    Platform: () => macosPlatform,
126
    FileSystem: () => fileSystem,
127
    ProcessManager: () => FakeProcessManager.any(),
128
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
129 130 131 132
  });

  testUsingContext('macOS build fails on non-macOS platform', () async {
    final BuildCommand command = BuildCommand();
133 134 135
    fileSystem.file('pubspec.yaml').createSync();
    fileSystem.file(fileSystem.path.join('lib', 'main.dart'))
      .createSync(recursive: true);
136 137

    expect(createTestCommandRunner(command).run(
138
      const <String>['build', 'macos', '--no-pub']
139
    ), throwsToolExit(message: '"build macos" only supported on macOS hosts.'));
140 141
  }, overrides: <Type, Generator>{
    Platform: () => notMacosPlatform,
142
    FileSystem: () => fileSystem,
143
    ProcessManager: () => FakeProcessManager.any(),
144
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
145 146
  });

147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
  testUsingContext('macOS build fails when feature is disabled', () async {
    final BuildCommand command = BuildCommand();
    fileSystem.file('pubspec.yaml').createSync();
    fileSystem.file(fileSystem.path.join('lib', 'main.dart'))
        .createSync(recursive: true);

    expect(createTestCommandRunner(command).run(
        const <String>['build', 'macos', '--no-pub']
    ), throwsToolExit(message: '"build macos" is not currently supported. To enable, run "flutter config --enable-macos-desktop".'));
  }, overrides: <Type, Generator>{
    Platform: () => macosPlatform,
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.any(),
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: false),
  });

163
  testUsingContext('macOS build forwards error stdout to status logger error', () async {
164 165 166 167
    final BuildCommand command = BuildCommand();
    createMinimalMockProjectFiles();

    await createTestCommandRunner(command).run(
168
      const <String>['build', 'macos', '--debug', '--no-pub']
169
    );
170
    expect(testLogger.statusText, isNot(contains('STDOUT STUFF')));
171 172
    expect(testLogger.traceText, isNot(contains('STDOUT STUFF')));
    expect(testLogger.errorText, contains('STDOUT STUFF'));
173
  }, overrides: <Type, Generator>{
174 175
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
176
      setUpFakeXcodeBuildHandler('Debug')
177
    ]),
178 179
    Platform: () => macosPlatform,
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
180 181
  });

182
  testUsingContext('macOS build invokes xcode build (debug)', () async {
183
    final BuildCommand command = BuildCommand();
184 185 186
    createMinimalMockProjectFiles();

    await createTestCommandRunner(command).run(
187
      const <String>['build', 'macos', '--debug', '--no-pub']
188 189
    );
  }, overrides: <Type, Generator>{
190 191
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
192
      setUpFakeXcodeBuildHandler('Debug')
193
    ]),
194 195 196 197
    Platform: () => macosPlatform,
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
  });

198 199 200 201 202
  testUsingContext('macOS build invokes xcode build (debug) with verbosity', () async {
    final BuildCommand command = BuildCommand();
    createMinimalMockProjectFiles();

    await createTestCommandRunner(command).run(
203
      const <String>['build', 'macos', '--debug', '--no-pub', '-v']
204 205 206 207
    );
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
208
      setUpFakeXcodeBuildHandler('Debug', verbose: true)
209 210 211 212 213 214
    ]),
    Platform: () => macosPlatform,
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
  });


215 216 217 218 219
  testUsingContext('macOS build invokes xcode build (profile)', () async {
    final BuildCommand command = BuildCommand();
    createMinimalMockProjectFiles();

    await createTestCommandRunner(command).run(
220
      const <String>['build', 'macos', '--profile', '--no-pub']
221 222
    );
  }, overrides: <Type, Generator>{
223 224
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
225
      setUpFakeXcodeBuildHandler('Profile')
226
    ]),
227 228 229 230 231 232 233 234
    Platform: () => macosPlatform,
    XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithProfile(),
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
  });

  testUsingContext('macOS build invokes xcode build (release)', () async {
    final BuildCommand command = BuildCommand();
    createMinimalMockProjectFiles();
235

236
    await createTestCommandRunner(command).run(
237
      const <String>['build', 'macos', '--release', '--no-pub']
238
    );
239
  }, overrides: <Type, Generator>{
240 241
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
242
      setUpFakeXcodeBuildHandler('Release')
243
    ]),
244
    Platform: () => macosPlatform,
245 246 247
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
  });

248 249 250 251 252
  testUsingContext('macOS build supports standard desktop build options', () async {
    final BuildCommand command = BuildCommand();
    createMinimalMockProjectFiles();
    fileSystem.file('lib/other.dart')
      .createSync(recursive: true);
253 254
    fileSystem.file('foo/bar.sksl.json')
      .createSync(recursive: true);
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276

    await createTestCommandRunner(command).run(
      const <String>[
        'build',
        'macos',
        '--target=lib/other.dart',
        '--no-pub',
        '--track-widget-creation',
        '--split-debug-info=foo/',
        '--enable-experiment=non-nullable',
        '--obfuscate',
        '--dart-define=foo.bar=2',
        '--dart-define=fizz.far=3',
        '--tree-shake-icons',
        '--bundle-sksl-path=foo/bar.sksl.json',
      ]
    );
    final List<String> contents = fileSystem
      .file('./macos/Flutter/ephemeral/Flutter-Generated.xcconfig')
      .readAsLinesSync();

    expect(contents, containsAll(<String>[
277 278 279 280 281 282 283
      'FLUTTER_APPLICATION_PATH=/',
      'FLUTTER_TARGET=lib/other.dart',
      'FLUTTER_BUILD_DIR=build',
      'FLUTTER_BUILD_NAME=1.0.0',
      'FLUTTER_BUILD_NUMBER=1',
      'EXCLUDED_ARCHS=arm64',
      'DART_DEFINES=Zm9vLmJhcj0y,Zml6ei5mYXI9Mw==',
284
      'DART_OBFUSCATION=true',
285 286
      'EXTRA_FRONT_END_OPTIONS=--enable-experiment=non-nullable',
      'EXTRA_GEN_SNAPSHOT_OPTIONS=--enable-experiment=non-nullable',
287 288 289 290
      'SPLIT_DEBUG_INFO=foo/',
      'TRACK_WIDGET_CREATION=true',
      'TREE_SHAKE_ICONS=true',
      'BUNDLE_SKSL_PATH=foo/bar.sksl.json',
291 292
      'PACKAGE_CONFIG=/.dart_tool/package_config.json',
      'COCOAPODS_PARALLEL_CODE_SIGN=true',
293 294 295 296
    ]));
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
297
      setUpFakeXcodeBuildHandler('Release')
298 299 300
    ]),
    Platform: () => macosPlatform,
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
301
    Artifacts: () => Artifacts.test(),
302 303
  });

304 305 306 307 308 309 310 311 312
  testUsingContext('macOS build supports build-name and build-number', () async {
    final BuildCommand command = BuildCommand();
    createMinimalMockProjectFiles();

    await createTestCommandRunner(command).run(
      const <String>[
        'build',
        'macos',
        '--debug',
313
        '--no-pub',
314 315 316 317 318 319 320 321 322 323 324 325 326
        '--build-name=1.2.3',
        '--build-number=42',
      ],
    );
    final String contents = fileSystem
      .file('./macos/Flutter/ephemeral/Flutter-Generated.xcconfig')
      .readAsStringSync();

    expect(contents, contains('FLUTTER_BUILD_NAME=1.2.3'));
    expect(contents, contains('FLUTTER_BUILD_NUMBER=42'));
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
327
      setUpFakeXcodeBuildHandler('Debug')
328 329 330 331 332
    ]),
    Platform: () => macosPlatform,
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
  });

333 334 335
  testUsingContext('Refuses to build for macOS when feature is disabled', () {
    final CommandRunner<void> runner = createTestCommandRunner(BuildCommand());

336
    expect(() => runner.run(<String>['build', 'macos', '--no-pub']),
337
      throwsToolExit());
338 339
  }, overrides: <Type, Generator>{
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: false),
340
  });
341 342

  testUsingContext('hidden when not enabled on macOS host', () {
343
    expect(BuildMacosCommand(verboseHelp: false).hidden, true);
344 345
  }, overrides: <Type, Generator>{
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: false),
346
    Platform: () => macosPlatform,
347 348 349
  });

  testUsingContext('Not hidden when enabled and on macOS host', () {
350
    expect(BuildMacosCommand(verboseHelp: false).hidden, false);
351 352
  }, overrides: <Type, Generator>{
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
353
    Platform: () => macosPlatform,
354
  });
355 356 357 358 359 360 361 362 363

  testUsingContext('Performs code size analysis and sends analytics', () async {
    final BuildCommand command = BuildCommand();
    createMinimalMockProjectFiles();

    fileSystem.file('build/macos/Build/Products/Release/Runner.app/App')
      ..createSync(recursive: true)
      ..writeAsBytesSync(List<int>.generate(10000, (int index) => 0));

364 365
    await createTestCommandRunner(command).run(
      const <String>['build', 'macos', '--no-pub', '--analyze-size']
366 367 368
    );

    expect(testLogger.statusText, contains('A summary of your macOS bundle analysis can be found at'));
369
    expect(testLogger.statusText, contains('flutter pub global activate devtools; flutter pub global run devtools --appSizeBase='));
370 371 372
    expect(usage.events, contains(
      const TestUsageEvent('code-size-analysis', 'macos'),
    ));
373 374 375
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
376
      setUpFakeXcodeBuildHandler('Release', onRun: () {
377 378
        fileSystem.file('build/flutter_size_01/snapshot.x86_64.json')
          ..createSync(recursive: true)
379 380 381 382 383 384 385 386 387
          ..writeAsStringSync('''
[
  {
    "l": "dart:_internal",
    "c": "SubListIterable",
    "n": "[Optimized] skip",
    "s": 2400
  }
]''');
388 389 390 391 392 393 394 395 396 397 398
        fileSystem.file('build/flutter_size_01/trace.x86_64.json')
          ..createSync(recursive: true)
          ..writeAsStringSync('{}');
      }),
    ]),
    Platform: () => macosPlatform,
    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
    FileSystemUtils: () => FileSystemUtils(fileSystem: fileSystem, platform: macosPlatform),
    Usage: () => usage,
  });
}