test.dart 35 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6
import 'dart:io';
7

Dan Field's avatar
Dan Field committed
8 9 10
import 'package:googleapis/bigquery/v2.dart' as bq;
import 'package:googleapis_auth/auth_io.dart' as auth;
import 'package:http/http.dart' as http;
11
import 'package:path/path.dart' as path;
12

Dan Field's avatar
Dan Field committed
13
import 'flutter_compact_formatter.dart';
14
import 'run_command.dart';
15

16
typedef ShardRunner = Future<void> Function();
17

18 19 20 21 22 23 24 25
/// A function used to validate the output of a test.
///
/// If the output matches expectations, the function shall return null.
///
/// If the output does not match expectations, the function shall return an
/// appropriate error message.
typedef OutputChecker = String Function(CapturedOutput);

26 27 28 29
final String flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script))));
final String flutter = path.join(flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter');
final String dart = path.join(flutterRoot, 'bin', 'cache', 'dart-sdk', 'bin', Platform.isWindows ? 'dart.exe' : 'dart');
final String pub = path.join(flutterRoot, 'bin', 'cache', 'dart-sdk', 'bin', Platform.isWindows ? 'pub.bat' : 'pub');
30
final String pubCache = path.join(flutterRoot, '.pub-cache');
31
final String toolRoot = path.join(flutterRoot, 'packages', 'flutter_tools');
32
final List<String> flutterTestArgs = <String>[];
33

34 35
final bool useFlutterTestFormatter = Platform.environment['FLUTTER_TEST_FORMATTER'] == 'true';

36 37
final bool noUseBuildRunner = Platform.environment['FLUTTER_TEST_NO_BUILD_RUNNER'] == 'true';

38
const Map<String, ShardRunner> _kShards = <String, ShardRunner>{
39
  'tests': _runTests,
40
  'web_tests': _runWebTests,
41
  'tool_tests': _runToolTests,
42
  'tool_coverage': _runToolCoverage,
43
  'build_tests': _runBuildTests,
44
  'coverage': _runCoverage,
45
  'integration_tests': _runIntegrationTests,
Dan Field's avatar
Dan Field committed
46
  'add2app_test': _runAdd2AppTest,
47 48
};

49
/// When you call this, you can pass additional arguments to pass custom
50
/// arguments to flutter test. For example, you might want to call this
51
/// script with the parameter --local-engine=host_debug_unopt to
52
/// use your own build of the engine.
53
///
54
/// To run the tool_tests part, run it with SHARD=tool_tests
55 56
///
/// For example:
57
/// SHARD=tool_tests bin/cache/dart-sdk/bin/dart dev/bots/test.dart
58
/// bin/cache/dart-sdk/bin/dart dev/bots/test.dart --local-engine=host_debug_unopt
59
Future<void> main(List<String> args) async {
60 61
  flutterTestArgs.addAll(args);

62 63
  final String shard = Platform.environment['SHARD'];
  if (shard != null) {
64 65 66 67 68
    if (!_kShards.containsKey(shard)) {
      print('Invalid shard: $shard');
      print('The available shards are: ${_kShards.keys.join(", ")}');
      exit(1);
    }
69
    print('${bold}SHARD=$shard$reset');
70 71 72 73 74
    await _kShards[shard]();
  } else {
    for (String currentShard in _kShards.keys) {
      print('${bold}SHARD=$currentShard$reset');
      await _kShards[currentShard]();
75
      print('');
76 77
    }
  }
78 79
}

80
Future<void> _runSmokeTests() async {
81 82
  // Verify that the tests actually return failure on failure and success on
  // success.
83
  final String automatedTests = path.join(flutterRoot, 'dev', 'automated_tests');
84 85 86
  // We run the "pass" and "fail" smoke tests first, and alone, because those
  // are particularly critical and sensitive. If one of these fails, there's no
  // point even trying the others.
87 88 89 90 91
  await _runFlutterTest(automatedTests,
    script: path.join('test_smoke_test', 'pass_test.dart'),
    printOutput: false,
  );
  await _runFlutterTest(automatedTests,
92
    script: path.join('test_smoke_test', 'fail_test.dart'),
93 94 95
    expectFailure: true,
    printOutput: false,
  );
96
  // We run the timeout tests individually because they are timing-sensitive.
97
  await _runFlutterTest(automatedTests,
98 99
    script: path.join('test_smoke_test', 'timeout_pass_test.dart'),
    expectFailure: false,
100 101
    printOutput: false,
  );
102
  await _runFlutterTest(automatedTests,
103
    script: path.join('test_smoke_test', 'timeout_fail_test.dart'),
104 105 106
    expectFailure: true,
    printOutput: false,
  );
107 108 109 110 111 112 113 114 115
  await _runFlutterTest(automatedTests,
    script: path.join('test_smoke_test', 'pending_timer_fail_test.dart'),
    expectFailure: true,
    printOutput: false,
    outputChecker: (CapturedOutput output) =>
      output.stdout.contains('failingPendingTimerTest')
      ? null
      : 'Failed to find the stack trace for the pending Timer.',
  );
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
  // We run the remaining smoketests in parallel, because they each take some
  // time to run (e.g. compiling), so we don't want to run them in series,
  // especially on 20-core machines...
  await Future.wait<void>(
    <Future<void>>[
      _runFlutterTest(automatedTests,
        script: path.join('test_smoke_test', 'crash1_test.dart'),
        expectFailure: true,
        printOutput: false,
      ),
      _runFlutterTest(automatedTests,
        script: path.join('test_smoke_test', 'crash2_test.dart'),
        expectFailure: true,
        printOutput: false,
      ),
      _runFlutterTest(automatedTests,
        script: path.join('test_smoke_test', 'syntax_error_test.broken_dart'),
        expectFailure: true,
        printOutput: false,
      ),
      _runFlutterTest(automatedTests,
        script: path.join('test_smoke_test', 'missing_import_test.broken_dart'),
        expectFailure: true,
        printOutput: false,
      ),
      _runFlutterTest(automatedTests,
        script: path.join('test_smoke_test', 'disallow_error_reporter_modification_test.dart'),
        expectFailure: true,
        printOutput: false,
      ),
146
      runCommand(flutter,
147 148
        <String>['drive', '--use-existing-app', '-t', path.join('test_driver', 'failure.dart')],
        workingDirectory: path.join(flutterRoot, 'packages', 'flutter_driver'),
149
        expectNonZeroExit: true,
150
        outputMode: OutputMode.discard,
151 152
      ),
    ],
153 154
  );

155 156
  // Verify that we correctly generated the version file.
  await _verifyVersion(path.join(flutterRoot, 'version'));
157 158
}

Dan Field's avatar
Dan Field committed
159
Future<bq.BigqueryApi> _getBigqueryApi() async {
160 161 162
  if (!useFlutterTestFormatter) {
    return null;
  }
Dan Field's avatar
Dan Field committed
163 164
  // TODO(dnfield): How will we do this on LUCI?
  final String privateKey = Platform.environment['GCLOUD_SERVICE_ACCOUNT_KEY'];
165 166 167 168 169
  // If we're on Cirrus and a non-collaborator is doing this, we can't get the key.
  if (privateKey == null || privateKey.isEmpty || privateKey.startsWith('ENCRYPTED[')) {
    return null;
  }
  try {
170
    final auth.ServiceAccountCredentials accountCredentials = auth.ServiceAccountCredentials(
171 172 173 174 175 176 177 178 179 180
      'flutter-ci-test-reporter@flutter-infra.iam.gserviceaccount.com',
      auth.ClientId.serviceAccount('114390419920880060881.apps.googleusercontent.com'),
      '-----BEGIN PRIVATE KEY-----\n$privateKey\n-----END PRIVATE KEY-----\n',
    );
    final List<String> scopes = <String>[bq.BigqueryApi.BigqueryInsertdataScope];
    final http.Client client = await auth.clientViaServiceAccount(accountCredentials, scopes);
    return bq.BigqueryApi(client);
  } catch (e) {
    print('Failed to get BigQuery API client.');
    print(e);
Dan Field's avatar
Dan Field committed
181 182 183 184
    return null;
  }
}

185 186 187
// Partition tool tests into two groups, see explanation on `_runToolCoverage`.
List<List<String>> _partitionToolTests() {
  final String toolTestDir = path.join(toolRoot, 'test');
188 189 190 191 192 193
  final List<String> pending = <String>[
    for (FileSystemEntity entity in Directory(toolTestDir).listSync(recursive: true))
      if (entity is File && entity.path.endsWith('_test.dart'))
        path.relative(entity.path, from: toolRoot),
  ];

194 195
  // Shuffle the tests to avoid giving an expensive test directory like
  // integration to a single run of tests.
196
  pending.shuffle();
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
  final int aboutHalf = pending.length ~/ 2;
  final List<String> groupA = pending.take(aboutHalf).toList();
  final List<String> groupB = pending.skip(aboutHalf).toList();
  return <List<String>>[groupA, groupB];
}

// Tools tests run with coverage enabled have much higher memory usage than
// our current CI infrastructure can support. We partition the tests into
// two sets and run them in separate invocations of dart to reduce peak memory
// usage. codecov.io automatically handles merging different coverage files
// together, so producing separate files is OK.
//
// See: https://github.com/flutter/flutter/issues/35025
Future<void> _runToolCoverage() async {
  final List<List<String>> tests = _partitionToolTests();
  // Precompile tests to speed up subsequent runs.
  await runCommand(
    pub,
    <String>['run', 'build_runner', 'build'],
    workingDirectory: toolRoot,
  );

  // The name of this subshard has to match the --file path provided at
  // the end of this test script in `.cirrus.yml`.
  const List<String> subshards = <String>['A', 'B'];
  for (int i = 0; i < tests.length; i++) {
    final List<String> testGroup = tests[i];
    await runCommand(
      dart,
226
      <String>[path.join('tool', 'tool_coverage.dart'), '--', ...testGroup],
227 228 229 230
      workingDirectory: toolRoot,
      environment: <String, String>{
        'FLUTTER_ROOT': flutterRoot,
        'SUBSHARD': subshards[i],
231
      },
232 233 234 235
    );
  }
}

236
Future<void> _runToolTests() async {
Dan Field's avatar
Dan Field committed
237
  final bq.BigqueryApi bigqueryApi = await _getBigqueryApi();
238 239
  await _runSmokeTests();

240 241 242 243 244 245
  // The flutter_tool will currently be snapshotted without asserts. We need
  // to force it to be regenerated with them enabled.
  if (!Platform.isWindows) {
    File(path.join(flutterRoot, 'bin', 'cache', 'flutter_tools.snapshot')).deleteSync();
    File(path.join(flutterRoot, 'bin', 'cache', 'flutter_tools.stamp')).deleteSync();
  }
246 247
  // reduce overhead of build_runner in the create case.
  if (noUseBuildRunner || Platform.environment['SUBSHARD'] == 'create') {
248 249 250
    await _pubRunTest(
      path.join(flutterRoot, 'packages', 'flutter_tools'),
      tableData: bigqueryApi?.tabledata,
251
      enableFlutterToolAsserts: !Platform.isWindows,
252 253 254 255 256 257
    );
  } else {
    await _buildRunnerTest(
      path.join(flutterRoot, 'packages', 'flutter_tools'),
      flutterRoot,
      tableData: bigqueryApi?.tabledata,
258
      enableFlutterToolAsserts: !Platform.isWindows,
259 260
    );
  }
261 262 263 264

  print('${bold}DONE: All tests successful.$reset');
}

265 266
/// Verifies that AOT, APK, and IPA (if on macOS) builds the
/// examples apps without crashing. It does not actually
267 268 269 270 271
/// launch the apps. That happens later in the devicelab. This is
/// just a smoke-test. In particular, this will verify we can build
/// when there are spaces in the path name for the Flutter SDK and
/// target app.
Future<void> _runBuildTests() async {
272 273 274 275 276 277 278 279 280
  final Stream<FileSystemEntity> exampleDirectories = Directory(path.join(flutterRoot, 'examples')).list();
  await for (FileSystemEntity fileEntity in exampleDirectories) {
    if (fileEntity is! Directory) {
      continue;
    }
    final String examplePath = fileEntity.path;

    await _flutterBuildAot(examplePath);
    await _flutterBuildApk(examplePath);
281
    await _flutterBuildIpa(examplePath);
282
  }
283 284 285 286 287 288 289
  // Web compilation tests.
  await _flutterBuildDart2js(path.join('dev', 'integration_tests', 'web'), path.join('lib', 'main.dart'));
  // Should fail to compile with dart:io.
  await _flutterBuildDart2js(path.join('dev', 'integration_tests', 'web_compile_tests'),
    path.join('lib', 'dart_io_import.dart'),
    expectNonZeroExit: true,
  );
290 291 292 293

  print('${bold}DONE: All build tests successful.$reset');
}

294
Future<void> _flutterBuildDart2js(String relativePathToApplication, String target, { bool expectNonZeroExit = false }) async {
295 296
  print('Running Dart2JS build tests...');
  await runCommand(flutter,
297
    <String>['build', 'web', '-v', '--target=$target'],
298
    workingDirectory: path.join(flutterRoot, relativePathToApplication),
299
    expectNonZeroExit: expectNonZeroExit,
300 301
    environment: <String, String>{
      'FLUTTER_WEB': 'true',
302
    },
303 304 305
  );
  print('Done.');
}
306

307 308 309 310 311 312 313 314 315
Future<void> _flutterBuildAot(String relativePathToApplication) async {
  print('Running AOT build tests...');
  await runCommand(flutter,
    <String>['build', 'aot', '-v'],
    workingDirectory: path.join(flutterRoot, relativePathToApplication),
    expectNonZeroExit: false,
  );
  print('Done.');
}
316

317
Future<void> _flutterBuildApk(String relativePathToApplication) async {
318 319 320
  if (
        (Platform.environment['ANDROID_HOME']?.isEmpty ?? true) &&
        (Platform.environment['ANDROID_SDK_ROOT']?.isEmpty ?? true)) {
321 322 323 324 325 326 327 328 329
    return;
  }
  print('Running APK build tests...');
  await runCommand(flutter,
    <String>['build', 'apk', '--debug', '-v'],
    workingDirectory: path.join(flutterRoot, relativePathToApplication),
    expectNonZeroExit: false,
  );
  print('Done.');
330 331
}

332
Future<void> _flutterBuildIpa(String relativePathToApplication) async {
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
  if (!Platform.isMacOS) {
    return;
  }
  print('Running IPA build tests...');
  // Install Cocoapods.  We don't have these checked in for the examples,
  // and build ios doesn't take care of it automatically.
  final File podfile = File(path.join(flutterRoot, relativePathToApplication, 'ios', 'Podfile'));
  if (podfile.existsSync()) {
    await runCommand('pod',
      <String>['install'],
      workingDirectory: podfile.parent.path,
      expectNonZeroExit: false,
    );
  }
  await runCommand(flutter,
    <String>['build', 'ios', '--no-codesign', '--debug', '-v'],
349
    workingDirectory: path.join(flutterRoot, relativePathToApplication),
350
    expectNonZeroExit: false,
351
  );
352
  print('Done.');
353 354
}

Dan Field's avatar
Dan Field committed
355 356 357 358 359 360 361 362 363 364 365 366 367 368
Future<void> _runAdd2AppTest() async {
  if (!Platform.isMacOS) {
    return;
  }
  print('Running Add2App iOS integration tests...');
  final String add2AppDir = path.join(flutterRoot, 'dev', 'integration_tests', 'ios_add2app');
  await runCommand('./build_and_test.sh',
    <String>[],
    workingDirectory: add2AppDir,
    expectNonZeroExit: false,
  );
  print('Done.');
}

369
Future<void> _runTests() async {
Dan Field's avatar
Dan Field committed
370
  final bq.BigqueryApi bigqueryApi = await _getBigqueryApi();
371
  await _runSmokeTests();
372
  final String subShard = Platform.environment['SUBSHARD'];
373

374 375 376 377 378
  Future<void> runWidgets() async {
    await _runFlutterTest(
      path.join(flutterRoot, 'packages', 'flutter'),
      tableData: bigqueryApi?.tabledata,
      tests: <String>[
379
        path.join('test', 'widgets') + path.separator,
380 381 382 383 384 385 386 387 388 389 390
      ],
    );
    // Only packages/flutter/test/widgets/widget_inspector_test.dart really
    // needs to be run with --track-widget-creation but it is nice to run
    // all of the tests in package:flutter with the flag to ensure that
    // the Dart kernel transformer triggered by the flag does not break anything.
    await _runFlutterTest(
      path.join(flutterRoot, 'packages', 'flutter'),
      options: <String>['--track-widget-creation'],
      tableData: bigqueryApi?.tabledata,
      tests: <String>[
391
        path.join('test', 'widgets') + path.separator,
392 393 394 395 396 397 398 399
      ],
    );
  }

  Future<void> runFrameworkOthers() async {
    final List<String> tests = Directory(path.join(flutterRoot, 'packages', 'flutter', 'test'))
      .listSync(followLinks: false, recursive: false)
      .whereType<Directory>()
400 401
      .where((Directory dir) => dir.path.endsWith('widgets') == false)
      .map((Directory dir) => path.join('test', path.basename(dir.path)) + path.separator)
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
      .toList();

    print('Running tests for: ${tests.join(';')}');

    await _runFlutterTest(
      path.join(flutterRoot, 'packages', 'flutter'),
      tableData: bigqueryApi?.tabledata,
      tests: tests,
    );
    // Only packages/flutter/test/widgets/widget_inspector_test.dart really
    // needs to be run with --track-widget-creation but it is nice to run
    // all of the tests in package:flutter with the flag to ensure that
    // the Dart kernel transformer triggered by the flag does not break anything.
    await _runFlutterTest(
      path.join(flutterRoot, 'packages', 'flutter'),
      options: <String>['--track-widget-creation'],
      tableData: bigqueryApi?.tabledata,
      tests: tests,
    );
  }

  Future<void> runExtras() async {
    await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_localizations'), tableData: bigqueryApi?.tabledata);
    await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_driver'), tableData: bigqueryApi?.tabledata);
    await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_test'), tableData: bigqueryApi?.tabledata);
    await _runFlutterTest(path.join(flutterRoot, 'packages', 'fuchsia_remote_debug_protocol'), tableData: bigqueryApi?.tabledata);
    await _pubRunTest(path.join(flutterRoot, 'dev', 'bots'), tableData: bigqueryApi?.tabledata);
    await _pubRunTest(path.join(flutterRoot, 'dev', 'devicelab'), tableData: bigqueryApi?.tabledata);
    await _pubRunTest(path.join(flutterRoot, 'dev', 'snippets'), tableData: bigqueryApi?.tabledata);
    await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'android_semantics_testing'), tableData: bigqueryApi?.tabledata);
    await _runFlutterTest(path.join(flutterRoot, 'dev', 'manual_tests'), tableData: bigqueryApi?.tabledata);
    await _runFlutterTest(path.join(flutterRoot, 'dev', 'tools', 'vitool'), tableData: bigqueryApi?.tabledata);
    await _runFlutterTest(path.join(flutterRoot, 'examples', 'hello_world'), tableData: bigqueryApi?.tabledata);
    await _runFlutterTest(path.join(flutterRoot, 'examples', 'layers'), tableData: bigqueryApi?.tabledata);
    await _runFlutterTest(path.join(flutterRoot, 'examples', 'stocks'), tableData: bigqueryApi?.tabledata);
    await _runFlutterTest(path.join(flutterRoot, 'examples', 'flutter_gallery'), tableData: bigqueryApi?.tabledata);
    // Regression test to ensure that code outside of package:flutter can run
    // with --track-widget-creation.
    await _runFlutterTest(path.join(flutterRoot, 'examples', 'flutter_gallery'), options: <String>['--track-widget-creation'], tableData: bigqueryApi?.tabledata);
    await _runFlutterTest(path.join(flutterRoot, 'examples', 'catalog'), tableData: bigqueryApi?.tabledata);
    // Smoke test for code generation.
    await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'codegen'), tableData: bigqueryApi?.tabledata, environment: <String, String>{
      'FLUTTER_EXPERIMENTAL_BUILD': 'true',
    });
  }
  switch (subShard) {
    case 'widgets':
      await runWidgets();
      break;
    case 'framework_other':
      await runFrameworkOthers();
      break;
    case 'extras':
      runExtras();
      break;
    default:
      print('Unknown sub-shard $subShard, running all tests!');
      await runWidgets();
      await runFrameworkOthers();
      await runExtras();

  }
464 465 466 467

  print('${bold}DONE: All tests successful.$reset');
}

468
Future<void> _runWebTests() async {
469
  // TODO(yjbanov): re-enable when web test cirrus flakiness is resolved
470
  await _runFlutterWebTest(path.join(flutterRoot, 'packages', 'flutter'), tests: <String>[
471
    // TODO(yjbanov): re-enable when flakiness is resolved
472
    // 'test/foundation/',
473 474 475 476 477 478 479 480
    // 'test/physics/',
    // 'test/rendering/',
    // 'test/services/',
    // 'test/painting/',
    // 'test/scheduler/',
    // 'test/semantics/',
    // 'test/widgets/',
    // 'test/material/',
481
  ]);
482
  await _runFlutterWebTest(path.join(flutterRoot, 'packages', 'flutter_web_plugins'), tests: <String>['test']);
483 484
}

485
Future<void> _runCoverage() async {
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
  final File coverageFile = File(path.join(flutterRoot, 'packages', 'flutter', 'coverage', 'lcov.info'));
  if (!coverageFile.existsSync()) {
    print('${red}Coverage file not found.$reset');
    print('Expected to find: ${coverageFile.absolute}');
    print('This file is normally obtained by running `flutter update-packages`.');
    exit(1);
  }
  coverageFile.deleteSync();
  await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter'),
    options: const <String>['--coverage'],
  );
  if (!coverageFile.existsSync()) {
    print('${red}Coverage file not found.$reset');
    print('Expected to find: ${coverageFile.absolute}');
    print('This file should have been generated by the `flutter test --coverage` script, but was not.');
    exit(1);
  }

  print('${bold}DONE: Coverage collection successful.$reset');
505 506
}

507 508 509
Future<void> _buildRunnerTest(
  String workingDirectory,
  String flutterRoot, {
Dan Field's avatar
Dan Field committed
510 511 512 513
  String testPath,
  bool enableFlutterToolAsserts = false,
  bq.TabledataResourceApi tableData,
}) async {
514 515 516 517 518 519 520 521 522 523
  final List<String> args = <String>[
    'run',
    'build_runner',
    'test',
    '--',
    if (useFlutterTestFormatter) '-rjson' else '-rcompact',
    '-j1',
    if (!hasColor) '--no-color',
    if (testPath != null) testPath,
  ];
524 525
  final Map<String, String> pubEnvironment = <String, String>{
    'FLUTTER_ROOT': flutterRoot,
526
    if (Directory(pubCache).existsSync()) 'PUB_CACHE': pubCache,
527 528 529 530 531 532 533 534 535
  };
  if (enableFlutterToolAsserts) {
    // If an existing env variable exists append to it, but only if
    // it doesn't appear to already include enable-asserts.
    String toolsArgs = Platform.environment['FLUTTER_TOOL_ARGS'] ?? '';
    if (!toolsArgs.contains('--enable-asserts'))
        toolsArgs += ' --enable-asserts';
    pubEnvironment['FLUTTER_TOOL_ARGS'] = toolsArgs.trim();
  }
Dan Field's avatar
Dan Field committed
536

Dan Field's avatar
Dan Field committed
537 538 539 540 541 542 543 544 545 546 547 548 549
  final String subShard = Platform.environment['SUBSHARD'];
  switch (subShard) {
    case 'integration':
      args.addAll(<String>['--tags', 'integration']);
      break;
    case 'create':
      args.addAll(<String>['--tags', 'create']);
      break;
    case 'tool':
      args.addAll(<String>['-x', 'integration', '-x', 'create']);
      break;
  }

550 551 552 553 554 555 556
  if (useFlutterTestFormatter) {
    final FlutterCompactFormatter formatter = FlutterCompactFormatter();
    final Stream<String> testOutput = runAndGetStdout(
      pub,
      args,
      workingDirectory: workingDirectory,
      environment: pubEnvironment,
557
      beforeExit: formatter.finish,
558 559 560 561 562 563 564 565
    );
    await _processTestOutput(formatter, testOutput, tableData);
  } else {
    await runCommand(
      pub,
      args,
      workingDirectory:workingDirectory,
      environment:pubEnvironment,
566
      removeLine: (String line) => line.contains('[INFO]'),
567 568
    );
  }
569 570
}

571
Future<void> _pubRunTest(
572 573
  String workingDirectory, {
  String testPath,
574
  bool enableFlutterToolAsserts = false,
Dan Field's avatar
Dan Field committed
575 576
  bq.TabledataResourceApi tableData,
}) async {
577 578 579 580 581 582 583 584 585 586 587
  final List<String> args = <String>[
    'run',
    'test',
    if (useFlutterTestFormatter) '-rjson' else '-rcompact',
    '-j1',
    if (!hasColor) '--no-color',
    if (testPath != null) testPath,
  ];
  final Map<String, String> pubEnvironment = <String, String>{
    if (Directory(pubCache).existsSync()) 'PUB_CACHE': pubCache,
  };
588 589 590 591 592 593 594 595
  if (enableFlutterToolAsserts) {
    // If an existing env variable exists append to it, but only if
    // it doesn't appear to already include enable-asserts.
    String toolsArgs = Platform.environment['FLUTTER_TOOL_ARGS'] ?? '';
    if (!toolsArgs.contains('--enable-asserts'))
        toolsArgs += ' --enable-asserts';
    pubEnvironment['FLUTTER_TOOL_ARGS'] = toolsArgs.trim();
  }
Dan Field's avatar
Dan Field committed
596 597 598 599 600 601 602 603 604

  final String subShard = Platform.environment['SUBSHARD'];
  switch (subShard) {
    case 'integration':
      args.addAll(<String>['--tags', 'integration']);
      break;
    case 'tool':
      args.addAll(<String>['--exclude-tags', 'integration']);
      break;
605
    case 'create':
606
      args.addAll(<String>[path.join('test', 'general.shard', 'commands', 'create_test.dart')]);
607
      break;
Dan Field's avatar
Dan Field committed
608 609
  }

610 611 612 613 614 615 616 617 618 619 620 621 622
  if (useFlutterTestFormatter) {
    final FlutterCompactFormatter formatter = FlutterCompactFormatter();
    final Stream<String> testOutput = runAndGetStdout(
      pub,
      args,
      workingDirectory: workingDirectory,
      beforeExit: formatter.finish,
    );
    await _processTestOutput(formatter, testOutput, tableData);
  } else {
    await runCommand(
      pub,
      args,
623
      workingDirectory: workingDirectory,
624 625
    );
  }
Dan Field's avatar
Dan Field committed
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655
}

enum CiProviders {
  cirrus,
  luci,
}

CiProviders _getCiProvider() {
  if (Platform.environment['CIRRUS_CI'] == 'true') {
    return CiProviders.cirrus;
  }
  if (Platform.environment['LUCI_CONTEXT'] != null) {
    return CiProviders.luci;
  }
  return null;
}

String _getCiProviderName() {
  switch(_getCiProvider()) {
    case CiProviders.cirrus:
      return 'cirrusci';
    case CiProviders.luci:
      return 'luci';
  }
  return 'unknown';
}

int _getPrNumber() {
  switch(_getCiProvider()) {
    case CiProviders.cirrus:
656 657 658
      return Platform.environment['CIRRUS_PR'] == null
          ? -1
          : int.tryParse(Platform.environment['CIRRUS_PR']);
Dan Field's avatar
Dan Field committed
659 660 661 662 663 664 665 666 667
    case CiProviders.luci:
      return -1; // LUCI doesn't know about this.
  }
  return -1;
}

Future<String> _getAuthors() async {
  final String exe = Platform.isWindows ? '.exe' : '';
  final String author = await runAndGetStdout(
668
    'git$exe', <String>['-c', 'log.showSignature=false', 'log', _getGitHash(), '--pretty="%an <%ae>"'],
Dan Field's avatar
Dan Field committed
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
    workingDirectory: flutterRoot,
  ).first;
  return author;
}

String _getCiUrl() {
  switch(_getCiProvider()) {
    case CiProviders.cirrus:
      return 'https://cirrus-ci.com/task/${Platform.environment['CIRRUS_TASK_ID']}';
    case CiProviders.luci:
      return 'https://ci.chromium.org/p/flutter/g/framework/console'; // TODO(dnfield): can we get a direct link to the actual build?
  }
  return '';
}

String _getGitHash() {
  switch(_getCiProvider()) {
    case CiProviders.cirrus:
      return Platform.environment['CIRRUS_CHANGE_IN_REPO'];
    case CiProviders.luci:
      return 'HEAD'; // TODO(dnfield): Set this in the env for LUCI.
  }
  return '';
}

694 695 696 697 698
Future<void> _processTestOutput(
  FlutterCompactFormatter formatter,
  Stream<String> testOutput,
  bq.TabledataResourceApi tableData,
) async {
Dan Field's avatar
Dan Field committed
699 700 701 702
  final Timer heartbeat = Timer.periodic(const Duration(seconds: 30), (Timer timer) {
    print('Processing...');
  });

Dan Field's avatar
Dan Field committed
703
  await testOutput.forEach(formatter.processRawOutput);
Dan Field's avatar
Dan Field committed
704
  heartbeat.cancel();
705
  formatter.finish();
Dan Field's avatar
Dan Field committed
706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753
  if (tableData == null || formatter.tests.isEmpty) {
    return;
  }
  final bq.TableDataInsertAllRequest request = bq.TableDataInsertAllRequest();
  final String authors = await _getAuthors();
  request.rows = List<bq.TableDataInsertAllRequestRows>.from(
    formatter.tests.map<bq.TableDataInsertAllRequestRows>((TestResult result) =>
      bq.TableDataInsertAllRequestRows.fromJson(<String, dynamic> {
        'json': <String, dynamic>{
          'source': <String, dynamic>{
            'provider': _getCiProviderName(),
            'url': _getCiUrl(),
            'platform': <String, dynamic>{
              'os': Platform.operatingSystem,
              'version': Platform.operatingSystemVersion,
            },
          },
          'test': <String, dynamic>{
            'name': result.name,
            'result': result.status.toString(),
            'file': result.path,
            'line': result.line,
            'column': result.column,
            'time': result.totalTime,
          },
          'git': <String, dynamic>{
            'author': authors,
            'pull_request': _getPrNumber(),
            'commit': _getGitHash(),
            'organization': 'flutter',
            'repository': 'flutter',
          },
          'error': result.status != TestStatus.failed ? null : <String, dynamic>{
            'message': result.errorMessage,
            'stack_trace': result.stackTrace,
          },
          'information': result.messages,
        },
      }),
    ),
    growable: false,
  );
  final bq.TableDataInsertAllResponse response = await tableData.insertAll(request, 'flutter-infra', 'tests', 'ci');
  if (response.insertErrors != null && response.insertErrors.isNotEmpty) {
    print('${red}BigQuery insert errors:');
    print(response.toJson());
    print(reset);
  }
754 755
}

756 757 758 759
class EvalResult {
  EvalResult({
    this.stdout,
    this.stderr,
760
    this.exitCode = 0,
761 762 763 764
  });

  final String stdout;
  final String stderr;
765
  final int exitCode;
766 767
}

768 769
Future<void> _runFlutterWebTest(String workingDirectory, {
  List<String> tests,
770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800
}) async {
  final List<String> allTests = <String>[];
  for (String testDirPath in tests) {
    final Directory testDir = Directory(path.join(workingDirectory, testDirPath));
    allTests.addAll(
      testDir.listSync(recursive: true)
        .whereType<File>()
        .where((File file) => file.path.endsWith('_test.dart'))
        .map((File file) => path.relative(file.path, from: workingDirectory))
    );
  }
  print(allTests.join('\n'));
  print('${allTests.length} tests total');

  // Maximum number of tests to run in a single `flutter test`. We found that
  // large batches can get flaky, possibly because we reuse a single instance
  // of the browser, and after many tests the browser's state gets corrupted.
  const int kBatchSize = 20;
  List<String> batch = <String>[];
  for (int i = 0; i < allTests.length; i += 1) {
    final String testFilePath = allTests[i];
    batch.add(testFilePath);
    if (batch.length == kBatchSize || i == allTests.length - 1) {
      await _runFlutterWebTestBatch(workingDirectory, batch: batch);
      batch = <String>[];
    }
  }
}

Future<void> _runFlutterWebTestBatch(String workingDirectory, {
  List<String> batch,
801
}) async {
802 803 804 805 806
  final List<String> args = <String>[
    'test',
    '-v',
    '--platform=chrome',
    ...?flutterTestArgs,
807
    ...batch,
808
  ];
809

810 811 812 813 814 815 816 817
  // TODO(jonahwilliams): fix relative path issues to make this unecessary.
  final Directory oldCurrent = Directory.current;
  Directory.current = Directory(path.join(flutterRoot, 'packages', 'flutter'));
  try {
    await runCommand(
      flutter,
      args,
      workingDirectory: workingDirectory,
818
      expectFlaky: false,
819 820 821 822 823 824 825 826
      environment: <String, String>{
        'FLUTTER_WEB': 'true',
        'FLUTTER_LOW_RESOURCE_MODE': 'true',
      },
    );
  } finally {
    Directory.current = oldCurrent;
  }
827 828
}

829
Future<void> _runFlutterTest(String workingDirectory, {
830
  String script,
831 832
  bool expectFailure = false,
  bool printOutput = true,
833
  OutputChecker outputChecker,
834 835
  List<String> options = const <String>[],
  bool skip = false,
Dan Field's avatar
Dan Field committed
836
  bq.TabledataResourceApi tableData,
837
  Map<String, String> environment,
838
  List<String> tests = const <String>[],
Dan Field's avatar
Dan Field committed
839
}) async {
James Lin's avatar
James Lin committed
840 841
  assert(!printOutput || outputChecker == null,
      'Output either can be printed or checked but not both');
842

843 844 845 846 847
  final List<String> args = <String>[
    'test',
    ...options,
    ...?flutterTestArgs,
  ];
Dan Field's avatar
Dan Field committed
848

849
  final bool shouldProcessOutput = useFlutterTestFormatter && !expectFailure && !options.contains('--coverage');
850
  if (shouldProcessOutput) {
Dan Field's avatar
Dan Field committed
851 852 853
    args.add('--machine');
  }

854 855 856 857 858 859 860 861 862 863 864 865
  if (script != null) {
    final String fullScriptPath = path.join(workingDirectory, script);
    if (!FileSystemEntity.isFileSync(fullScriptPath)) {
      print('Could not find test: $fullScriptPath');
      print('Working directory: $workingDirectory');
      print('Script: $script');
      if (!printOutput)
        print('This is one of the tests that does not normally print output.');
      if (skip)
        print('This is one of the tests that is normally skipped in this configuration.');
      exit(1);
    }
866
    args.add(script);
867
  }
868 869 870

  args.addAll(tests);

871
  if (!shouldProcessOutput) {
872 873 874 875 876 877 878 879 880 881 882 883 884
    OutputMode outputMode = OutputMode.discard;
    CapturedOutput output;

    if (outputChecker != null) {
      outputMode = OutputMode.capture;
      output = CapturedOutput();
    } else if (printOutput) {
      outputMode = OutputMode.print;
    }

    await runCommand(
      flutter,
      args,
Dan Field's avatar
Dan Field committed
885
      workingDirectory: workingDirectory,
886
      expectNonZeroExit: expectFailure,
887 888
      outputMode: outputMode,
      output: output,
Dan Field's avatar
Dan Field committed
889
      skip: skip,
890
      environment: environment,
Dan Field's avatar
Dan Field committed
891
    );
892 893 894 895 896 897 898 899 900 901 902

    if (outputChecker != null) {
      final String message = outputChecker(output);
      if (message != null) {
        print('$redLine');
        print(message);
        print('$redLine');
        exit(1);
      }
    }
    return;
Dan Field's avatar
Dan Field committed
903
  }
904 905

  if (useFlutterTestFormatter) {
906 907 908 909 910 911 912 913 914 915
    final FlutterCompactFormatter formatter = FlutterCompactFormatter();
    final Stream<String> testOutput = runAndGetStdout(
      flutter,
      args,
      workingDirectory: workingDirectory,
      expectNonZeroExit: expectFailure,
      beforeExit: formatter.finish,
      environment: environment,
    );
    await _processTestOutput(formatter, testOutput, tableData);
916 917 918 919 920 921 922 923
  } else {
    await runCommand(
      flutter,
      args,
      workingDirectory: workingDirectory,
      expectNonZeroExit: expectFailure,
    );
  }
924 925
}

926
Future<void> _verifyVersion(String filename) async {
927
  if (!File(filename).existsSync()) {
928
    print('$redLine');
929
    print('The version logic failed to create the Flutter version file.');
930
    print('$redLine');
931 932
    exit(1);
  }
933
  final String version = await File(filename).readAsString();
934
  if (version == '0.0.0-unknown') {
935
    print('$redLine');
936
    print('The version logic failed to determine the Flutter version.');
937
    print('$redLine');
938 939
    exit(1);
  }
940
  final RegExp pattern = RegExp(r'^\d+\.\d+\.\d+(?:|-pre\.\d+|\+hotfix\.\d+)$');
941
  if (!version.contains(pattern)) {
942
    print('$redLine');
943
    print('The version logic generated an invalid version string.');
944
    print('$redLine');
945 946
    exit(1);
  }
947
}
948 949

Future<void> _runIntegrationTests() async {
950
  final String subShard = Platform.environment['SUBSHARD'];
951

952 953 954 955 956 957 958 959 960 961 962 963 964 965 966
  switch (subShard) {
    case 'gradle1':
    case 'gradle2':
      // This runs some gradle integration tests if the subshard is Android.
      await _androidGradleTests(subShard);
      break;
    default:
      await _runDevicelabTest('dartdocs');

      if (Platform.isLinux) {
        await _runDevicelabTest('flutter_create_offline_test_linux');
      } else if (Platform.isWindows) {
        await _runDevicelabTest('flutter_create_offline_test_windows');
      } else if (Platform.isMacOS) {
        await _runDevicelabTest('flutter_create_offline_test_mac');
967 968
// TODO(jmagman): Re-enable once flakiness is resolved.
//        await _runDevicelabTest('module_test_ios');
969 970 971
      }
      // This does less work if the subshard isn't Android.
      await _androidPluginTest();
972 973 974 975 976 977 978 979 980 981 982 983
  }
}

Future<void> _runDevicelabTest(String testName, {Map<String, String> env}) async {
  await runCommand(
    dart,
    <String>['bin/run.dart', '-t', testName],
    workingDirectory: path.join(flutterRoot, 'dev', 'devicelab'),
    environment: env,
  );
}

984
String get androidSdkRoot {
985 986 987 988
  final String androidSdkRoot = (Platform.environment['ANDROID_HOME']?.isEmpty ?? true)
      ? Platform.environment['ANDROID_SDK_ROOT']
      : Platform.environment['ANDROID_HOME'];
  if (androidSdkRoot == null || androidSdkRoot.isEmpty) {
989 990 991 992 993 994 995 996
    return null;
  }
  return androidSdkRoot;
}

Future<void> _androidPluginTest() async {
  if (androidSdkRoot == null) {
    print('No Android SDK detected, skipping Android Plugin test.');
997 998 999 1000 1001 1002 1003 1004
    return;
  }

  final Map<String, String> env = <String, String> {
    'ANDROID_HOME': androidSdkRoot,
    'ANDROID_SDK_ROOT': androidSdkRoot,
  };

1005 1006 1007 1008
  await _runDevicelabTest('plugin_test', env: env);
}

Future<void> _androidGradleTests(String subShard) async {
1009
  // TODO(dnfield): gradlew is crashing on the cirrus image and it's not clear why.
1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020
  if (androidSdkRoot == null || Platform.isWindows) {
    print('No Android SDK detected or on Windows, skipping Android gradle test.');
    return;
  }

  final Map<String, String> env = <String, String> {
    'ANDROID_HOME': androidSdkRoot,
    'ANDROID_SDK_ROOT': androidSdkRoot,
  };

  if (subShard == 'gradle1') {
1021 1022
    await _runDevicelabTest('gradle_plugin_light_apk_test', env: env);
    await _runDevicelabTest('gradle_plugin_fat_apk_test', env: env);
1023
    await _runDevicelabTest('gradle_r8_test', env: env);
1024
    await _runDevicelabTest('gradle_non_android_plugin_test', env: env);
1025
    await _runDevicelabTest('gradle_jetifier_test', env: env);
1026 1027
  }
  if (subShard == 'gradle2') {
1028
    await _runDevicelabTest('gradle_plugin_bundle_test', env: env);
1029
    await _runDevicelabTest('module_test', env: env);
1030
    await _runDevicelabTest('module_host_with_custom_build_test', env: env);
1031
  }
1032
}