Split analysis steps out of dev/bots/test.dart into dev/bots/analyze.dart (#21174) * Split analysis steps out of dev/bots/test.dart into dev/bots/analyze.dart. This allows to run analysis step with command line arguments that are only applicable to flutter analyze(like --dart-sdk, needed for dart-flutter-engine head-head-head bot). * Add forgotten dev/bots/analyze.dart * Refactor common code from analyze.dart and test.dart into run_command.dart * Remove comments, add header
diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart new file mode 100644 index 0000000..c0296c5 --- /dev/null +++ b/dev/bots/analyze.dart
@@ -0,0 +1,527 @@ +// 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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:meta/meta.dart'; + +import 'run_command.dart'; + +typedef Future<Null> ShardRunner(); + +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'); +final String pubCache = path.join(flutterRoot, '.pub-cache'); + +/// When you call this, you can pass additional arguments to pass custom +/// arguments to flutter analyze. For example, you might want to call this +/// script with the parameter --dart-sdk to use custom dart sdk. +/// +/// For example: +/// bin/cache/dart-sdk/bin/dart dev/bots/analyze.dart --dart-sdk=/tmp/dart-sdk +Future<Null> main(List<String> args) async { + await _verifyNoTestPackageImports(flutterRoot); + await _verifyGeneratedPluginRegistrants(flutterRoot); + await _verifyNoBadImportsInFlutter(flutterRoot); + await _verifyNoBadImportsInFlutterTools(flutterRoot); + await _verifyInternationalizations(); + + { + // Analyze all the Dart code in the repo. + final List<String> options = <String>['--flutter-repo']; + options.addAll(args); + await _runFlutterAnalyze(flutterRoot, options: options); + } + + // Ensure that all package dependencies are in sync. + await runCommand(flutter, <String>['update-packages', '--verify-only'], + workingDirectory: flutterRoot, + ); + + // Analyze all the sample code in the repo + await runCommand(dart, + <String>['--preview-dart-2', path.join(flutterRoot, 'dev', 'bots', 'analyze-sample-code.dart')], + workingDirectory: flutterRoot, + ); + + // Try with the --watch analyzer, to make sure it returns success also. + // The --benchmark argument exits after one run. + { + final List<String> options = <String>['--flutter-repo', '--watch', '--benchmark']; + options.addAll(args); + await _runFlutterAnalyze(flutterRoot, options: options); + } + + await _checkForTrailingSpaces(); + + // Try analysis against a big version of the gallery; generate into a temporary directory. + final Directory outDir = Directory.systemTemp.createTempSync('flutter_mega_gallery.'); + + try { + await runCommand(dart, + <String>[ + '--preview-dart-2', + path.join(flutterRoot, 'dev', 'tools', 'mega_gallery.dart'), + '--out', + outDir.path, + ], + workingDirectory: flutterRoot, + ); + { + final List<String> options = <String>['--watch', '--benchmark']; + options.addAll(args); + await _runFlutterAnalyze(outDir.path, options: options); + } + } finally { + outDir.deleteSync(recursive: true); + } + + print('${bold}DONE: Analysis successful.$reset'); +} + +Future<Null> _verifyInternationalizations() async { + final EvalResult genResult = await _evalCommand( + dart, + <String>[ + '--preview-dart-2', + path.join('dev', 'tools', 'gen_localizations.dart'), + ], + workingDirectory: flutterRoot, + ); + + final String localizationsFile = path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n', 'localizations.dart'); + final String expectedResult = await new File(localizationsFile).readAsString(); + + if (genResult.stdout.trim() != expectedResult.trim()) { + stderr + ..writeln('<<<<<<< $localizationsFile') + ..writeln(expectedResult.trim()) + ..writeln('=======') + ..writeln(genResult.stdout.trim()) + ..writeln('>>>>>>> gen_localizations') + ..writeln('The contents of $localizationsFile are different from that produced by gen_localizations.') + ..writeln() + ..writeln('Did you forget to run gen_localizations.dart after updating a .arb file?'); + exit(1); + } + print('Contents of $localizationsFile matches output of gen_localizations.dart script.'); +} + +Future<String> _getCommitRange() async { + // Using --fork-point is more conservative, and will result in the correct + // fork point, but when running locally, it may return nothing. Git is + // guaranteed to return a (reasonable, but maybe not optimal) result when not + // using --fork-point, so we fall back to that if we can't get a definitive + // fork point. See "git merge-base" documentation for more info. + EvalResult result = await _evalCommand( + 'git', + <String>['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], + workingDirectory: flutterRoot, + allowNonZeroExit: true, + ); + if (result.exitCode != 0) { + result = await _evalCommand( + 'git', + <String>['merge-base', 'FETCH_HEAD', 'HEAD'], + workingDirectory: flutterRoot, + ); + } + return result.stdout.trim(); +} + + +Future<Null> _checkForTrailingSpaces() async { + if (!Platform.isWindows) { + final String commitRange = Platform.environment.containsKey('TEST_COMMIT_RANGE') + ? Platform.environment['TEST_COMMIT_RANGE'] + : await _getCommitRange(); + final List<String> fileTypes = <String>[ + '*.dart', '*.cxx', '*.cpp', '*.cc', '*.c', '*.C', '*.h', '*.java', '*.mm', '*.m', '*.yml', + ]; + final EvalResult changedFilesResult = await _evalCommand( + 'git', <String>['diff', '-U0', '--no-color', '--name-only', commitRange, '--'] + fileTypes, + workingDirectory: flutterRoot, + ); + if (changedFilesResult.stdout == null || changedFilesResult.stdout.trim().isEmpty) { + print('No files found that need to be checked for trailing whitespace.'); + return; + } + // Only include files that actually exist, so that we don't try and grep for + // nonexistent files, which can occur when files are deleted or moved. + final List<String> changedFiles = changedFilesResult.stdout.split('\n').where((String filename) { + return new File(filename).existsSync(); + }).toList(); + if (changedFiles.isNotEmpty) { + await runCommand('grep', + <String>[ + '--line-number', + '--extended-regexp', + r'[[:blank:]]$', + ] + changedFiles, + workingDirectory: flutterRoot, + failureMessage: '${red}Whitespace detected at the end of source code lines.$reset\nPlease remove:', + expectNonZeroExit: true, // Just means a non-zero exit code is expected. + expectedExitCode: 1, // Indicates that zero lines were found. + ); + } + } +} + +class EvalResult { + EvalResult({ + this.stdout, + this.stderr, + this.exitCode = 0, + }); + + final String stdout; + final String stderr; + final int exitCode; +} + +Future<EvalResult> _evalCommand(String executable, List<String> arguments, { + @required String workingDirectory, + Map<String, String> environment, + bool skip = false, + bool allowNonZeroExit = false, +}) async { + final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}'; + final String relativeWorkingDir = path.relative(workingDirectory); + if (skip) { + printProgress('SKIPPING', relativeWorkingDir, commandDescription); + return null; + } + printProgress('RUNNING', relativeWorkingDir, commandDescription); + + final DateTime start = new DateTime.now(); + final Process process = await Process.start(executable, arguments, + workingDirectory: workingDirectory, + environment: environment, + ); + + final Future<List<List<int>>> savedStdout = process.stdout.toList(); + final Future<List<List<int>>> savedStderr = process.stderr.toList(); + final int exitCode = await process.exitCode; + final EvalResult result = new EvalResult( + stdout: utf8.decode((await savedStdout).expand((List<int> ints) => ints).toList()), + stderr: utf8.decode((await savedStderr).expand((List<int> ints) => ints).toList()), + exitCode: exitCode, + ); + + print('$clock ELAPSED TIME: $bold${elapsedTime(start)}$reset for $commandDescription in $relativeWorkingDir: '); + + if (exitCode != 0 && !allowNonZeroExit) { + stderr.write(result.stderr); + print( + '$redLine\n' + '${bold}ERROR:$red Last command exited with $exitCode.$reset\n' + '${bold}Command:$red $commandDescription$reset\n' + '${bold}Relative working directory:$red $relativeWorkingDir$reset\n' + '$redLine' + ); + exit(1); + } + + return result; +} + +Future<Null> _runFlutterAnalyze(String workingDirectory, { + List<String> options = const <String>[] +}) { + return runCommand(flutter, <String>['analyze']..addAll(options), + workingDirectory: workingDirectory, + ); +} + +Future<Null> _verifyNoTestPackageImports(String workingDirectory) async { + // TODO(ianh): Remove this whole test once https://github.com/dart-lang/matcher/issues/98 is fixed. + final List<String> shims = <String>[]; + final List<String> errors = new Directory(workingDirectory) + .listSync(recursive: true) + .where((FileSystemEntity entity) { + return entity is File && entity.path.endsWith('.dart'); + }) + .map<String>((FileSystemEntity entity) { + final File file = entity; + final String name = path.relative(file.path, from: workingDirectory); + if (name.startsWith('bin/cache') || + name == 'dev/bots/test.dart' || + name.startsWith('.pub-cache')) + return null; + final String data = file.readAsStringSync(); + if (data.contains("import 'package:test/test.dart'")) { + if (data.contains("// Defines a 'package:test' shim.")) { + shims.add(' $name'); + if (!data.contains('https://github.com/dart-lang/matcher/issues/98')) + return ' $name: Shims must link to the isInstanceOf issue.'; + if (data.contains("import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;") && + data.contains("export 'package:test/test.dart' hide TypeMatcher, isInstanceOf;")) + return null; + return ' $name: Shim seems to be missing the expected import/export lines.'; + } + final int count = 'package:test'.allMatches(data).length; + if (file.path.contains('/test_driver/') || + name.startsWith('dev/missing_dependency_tests/') || + name.startsWith('dev/automated_tests/') || + name.startsWith('packages/flutter/test/engine/') || + name.startsWith('examples/layers/test/smoketests/raw/') || + name.startsWith('examples/layers/test/smoketests/rendering/') || + name.startsWith('examples/flutter_gallery/test/calculator')) { + // We only exempt driver tests, some of our special trivial tests. + // Driver tests aren't typically expected to use TypeMatcher and company. + // The trivial tests don't typically do anything at all and it would be + // a pain to have to give them a shim. + if (!data.contains("import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;")) + return ' $name: test does not hide TypeMatcher and isInstanceOf from package:test; consider using a shim instead.'; + assert(count > 0); + if (count == 1) + return null; + return ' $name: uses \'package:test\' $count times.'; + } + if (name.startsWith('packages/flutter_test/')) { + // flutter_test has deep ties to package:test + return null; + } + if (data.contains("import 'package:test/test.dart' as test_package;") || + data.contains("import 'package:test/test.dart' as test_package show ")) { + if (count == 1) + return null; + } + return ' $name: uses \'package:test\' directly'; + } + return null; + }) + .where((String line) => line != null) + .toList() + ..sort(); + + // Fail if any errors + if (errors.isNotEmpty) { + print('$redLine'); + final String s1 = errors.length == 1 ? 's' : ''; + final String s2 = errors.length == 1 ? '' : 's'; + print('${bold}The following file$s2 use$s1 \'package:test\' incorrectly:$reset'); + print(errors.join('\n')); + print('Rather than depending on \'package:test\' directly, use one of the shims:'); + print(shims.join('\n')); + print('This insulates us from breaking changes in \'package:test\'.'); + print('$redLine\n'); + exit(1); + } +} + +Future<Null> _verifyNoBadImportsInFlutter(String workingDirectory) async { + final List<String> errors = <String>[]; + final String libPath = path.join(workingDirectory, 'packages', 'flutter', 'lib'); + final String srcPath = path.join(workingDirectory, 'packages', 'flutter', 'lib', 'src'); + // Verify there's one libPath/*.dart for each srcPath/*/. + final List<String> packages = new Directory(libPath).listSync() + .where((FileSystemEntity entity) => entity is File && path.extension(entity.path) == '.dart') + .map<String>((FileSystemEntity entity) => path.basenameWithoutExtension(entity.path)) + .toList()..sort(); + final List<String> directories = new Directory(srcPath).listSync() + .whereType<Directory>() + .map<String>((Directory entity) => path.basename(entity.path)) + .toList()..sort(); + if (!_matches(packages, directories)) { + errors.add( + 'flutter/lib/*.dart does not match flutter/lib/src/*/:\n' + 'These are the exported packages:\n' + + packages.map((String path) => ' lib/$path.dart').join('\n') + + 'These are the directories:\n' + + directories.map((String path) => ' lib/src/$path/').join('\n') + ); + } + // Verify that the imports are well-ordered. + final Map<String, Set<String>> dependencyMap = <String, Set<String>>{}; + for (String directory in directories) { + dependencyMap[directory] = _findDependencies(path.join(srcPath, directory), errors, checkForMeta: directory != 'foundation'); + } + for (String package in dependencyMap.keys) { + if (dependencyMap[package].contains(package)) { + errors.add( + 'One of the files in the $yellow$package$reset package imports that package recursively.' + ); + } + } + for (String package in dependencyMap.keys) { + final List<String> loop = _deepSearch(dependencyMap, package); + if (loop != null) { + errors.add( + '${yellow}Dependency loop:$reset ' + + loop.join(' depends on ') + ); + } + } + // Fail if any errors + if (errors.isNotEmpty) { + print('$redLine'); + if (errors.length == 1) { + print('${bold}An error was detected when looking at import dependencies within the Flutter package:$reset\n'); + } else { + print('${bold}Multiple errors were detected when looking at import dependencies within the Flutter package:$reset\n'); + } + print(errors.join('\n\n')); + print('$redLine\n'); + exit(1); + } +} + +bool _matches<T>(List<T> a, List<T> b) { + assert(a != null); + assert(b != null); + if (a.length != b.length) + return false; + for (int index = 0; index < a.length; index += 1) { + if (a[index] != b[index]) + return false; + } + return true; +} + +final RegExp _importPattern = new RegExp(r"import 'package:flutter/([^.]+)\.dart'"); +final RegExp _importMetaPattern = new RegExp(r"import 'package:meta/meta.dart'"); + +Set<String> _findDependencies(String srcPath, List<String> errors, { bool checkForMeta = false }) { + return new Directory(srcPath).listSync(recursive: true).where((FileSystemEntity entity) { + return entity is File && path.extension(entity.path) == '.dart'; + }).map<Set<String>>((FileSystemEntity entity) { + final Set<String> result = new Set<String>(); + final File file = entity; + for (String line in file.readAsLinesSync()) { + Match match = _importPattern.firstMatch(line); + if (match != null) + result.add(match.group(1)); + if (checkForMeta) { + match = _importMetaPattern.firstMatch(line); + if (match != null) { + errors.add( + '${file.path}\nThis package imports the ${yellow}meta$reset package.\n' + 'You should instead import the "foundation.dart" library.' + ); + } + } + } + return result; + }).reduce((Set<String> value, Set<String> element) { + value ??= new Set<String>(); + value.addAll(element); + return value; + }); +} + +List<T> _deepSearch<T>(Map<T, Set<T>> map, T start, [ Set<T> seen ]) { + for (T key in map[start]) { + if (key == start) + continue; // we catch these separately + if (seen != null && seen.contains(key)) + return <T>[start, key]; + final List<T> result = _deepSearch( + map, + key, + (seen == null ? new Set<T>.from(<T>[start]) : new Set<T>.from(seen))..add(key), + ); + if (result != null) { + result.insert(0, start); + // Only report the shortest chains. + // For example a->b->a, rather than c->a->b->a. + // Since we visit every node, we know the shortest chains are those + // that start and end on the loop. + if (result.first == result.last) + return result; + } + } + return null; +} + +Future<Null> _verifyNoBadImportsInFlutterTools(String workingDirectory) async { + final List<String> errors = <String>[]; + for (FileSystemEntity entity in new Directory(path.join(workingDirectory, 'packages', 'flutter_tools', 'lib')) + .listSync(recursive: true) + .where((FileSystemEntity entity) => entity is File && path.extension(entity.path) == '.dart')) { + final File file = entity; + if (file.readAsStringSync().contains('package:flutter_tools/')) { + errors.add('$yellow${file.path}$reset imports flutter_tools.'); + } + } + // Fail if any errors + if (errors.isNotEmpty) { + print('$redLine'); + if (errors.length == 1) { + print('${bold}An error was detected when looking at import dependencies within the flutter_tools package:$reset\n'); + } else { + print('${bold}Multiple errors were detected when looking at import dependencies within the flutter_tools package:$reset\n'); + } + print(errors.join('\n\n')); + print('$redLine\n'); + exit(1); + } +} + +Future<Null> _verifyGeneratedPluginRegistrants(String flutterRoot) async { + final Directory flutterRootDir = new Directory(flutterRoot); + + final Map<String, List<File>> packageToRegistrants = <String, List<File>>{}; + + for (FileSystemEntity entity in flutterRootDir.listSync(recursive: true)) { + if (entity is! File) + continue; + if (_isGeneratedPluginRegistrant(entity)) { + final String package = _getPackageFor(entity, flutterRootDir); + final List<File> registrants = packageToRegistrants.putIfAbsent(package, () => <File>[]); + registrants.add(entity); + } + } + + final Set<String> outOfDate = new Set<String>(); + + for (String package in packageToRegistrants.keys) { + final Map<File, String> fileToContent = <File, String>{}; + for (File f in packageToRegistrants[package]) { + fileToContent[f] = f.readAsStringSync(); + } + await runCommand(flutter, <String>['inject-plugins'], + workingDirectory: package, + printOutput: false, + ); + for (File registrant in fileToContent.keys) { + if (registrant.readAsStringSync() != fileToContent[registrant]) { + outOfDate.add(registrant.path); + } + } + } + + if (outOfDate.isNotEmpty) { + print('$redLine'); + print('${bold}The following GeneratedPluginRegistrants are out of date:$reset'); + for (String registrant in outOfDate) { + print(' - $registrant'); + } + print('\nRun "flutter inject-plugins" in the package that\'s out of date.'); + print('$redLine'); + exit(1); + } +} + +String _getPackageFor(File entity, Directory flutterRootDir) { + for (Directory dir = entity.parent; dir != flutterRootDir; dir = dir.parent) { + if (new File(path.join(dir.path, 'pubspec.yaml')).existsSync()) { + return dir.path; + } + } + throw new ArgumentError('$entity is not within a dart package.'); +} + +bool _isGeneratedPluginRegistrant(File file) { + final String filename = path.basename(file.path); + return !file.path.contains('.pub-cache') + && (filename == 'GeneratedPluginRegistrant.java' || + filename == 'GeneratedPluginRegistrant.h' || + filename == 'GeneratedPluginRegistrant.m'); +}
diff --git a/dev/bots/run_command.dart b/dev/bots/run_command.dart new file mode 100644 index 0000000..5427741 --- /dev/null +++ b/dev/bots/run_command.dart
@@ -0,0 +1,90 @@ +// 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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +final bool hasColor = stdout.supportsAnsiEscapes; + +final String bold = hasColor ? '\x1B[1m' : ''; +final String red = hasColor ? '\x1B[31m' : ''; +final String green = hasColor ? '\x1B[32m' : ''; +final String yellow = hasColor ? '\x1B[33m' : ''; +final String cyan = hasColor ? '\x1B[36m' : ''; +final String reset = hasColor ? '\x1B[0m' : ''; +final String redLine = '$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset'; +const String arrow = '⏩'; +const String clock = '🕐'; + +const Duration _kLongTimeout = Duration(minutes: 45); + +String elapsedTime(DateTime start) { + return new DateTime.now().difference(start).toString(); +} + +void printProgress(String action, String workingDir, String command) { + print('$arrow $action: cd $cyan$workingDir$reset; $yellow$command$reset'); +} + +Future<Null> runCommand(String executable, List<String> arguments, { + String workingDirectory, + Map<String, String> environment, + bool expectNonZeroExit = false, + int expectedExitCode, + String failureMessage, + bool printOutput = true, + bool skip = false, + Duration timeout = _kLongTimeout, +}) async { + final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}'; + final String relativeWorkingDir = path.relative(workingDirectory); + if (skip) { + printProgress('SKIPPING', relativeWorkingDir, commandDescription); + return null; + } + printProgress('RUNNING', relativeWorkingDir, commandDescription); + + final DateTime start = new DateTime.now(); + final Process process = await Process.start(executable, arguments, + workingDirectory: workingDirectory, + environment: environment, + ); + + Future<List<List<int>>> savedStdout, savedStderr; + if (printOutput) { + await Future.wait(<Future<void>>[ + stdout.addStream(process.stdout), + stderr.addStream(process.stderr) + ]); + } else { + savedStdout = process.stdout.toList(); + savedStderr = process.stderr.toList(); + } + + final int exitCode = await process.exitCode.timeout(timeout, onTimeout: () { + stderr.writeln('Process timed out after $timeout'); + return expectNonZeroExit ? 0 : 1; + }); + print('$clock ELAPSED TIME: $bold${elapsedTime(start)}$reset for $commandDescription in $relativeWorkingDir: '); + if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) { + if (failureMessage != null) { + print(failureMessage); + } + if (!printOutput) { + stdout.writeln(utf8.decode((await savedStdout).expand((List<int> ints) => ints).toList())); + stderr.writeln(utf8.decode((await savedStderr).expand((List<int> ints) => ints).toList())); + } + print( + '$redLine\n' + '${bold}ERROR:$red Last command exited with $exitCode (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset\n' + '${bold}Command:$cyan $commandDescription$reset\n' + '${bold}Relative working directory:$red $relativeWorkingDir$reset\n' + '$redLine' + ); + exit(1); + } +}
diff --git a/dev/bots/test.dart b/dev/bots/test.dart index 3fd6427..6f58cc1 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart
@@ -3,11 +3,11 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as path; -import 'package:meta/meta.dart'; + +import 'run_command.dart'; typedef Future<Null> ShardRunner(); @@ -17,25 +17,11 @@ final String pub = path.join(flutterRoot, 'bin', 'cache', 'dart-sdk', 'bin', Platform.isWindows ? 'pub.bat' : 'pub'); final String pubCache = path.join(flutterRoot, '.pub-cache'); final List<String> flutterTestArgs = <String>[]; -final bool hasColor = stdout.supportsAnsiEscapes; - -final String bold = hasColor ? '\x1B[1m' : ''; -final String red = hasColor ? '\x1B[31m' : ''; -final String green = hasColor ? '\x1B[32m' : ''; -final String yellow = hasColor ? '\x1B[33m' : ''; -final String cyan = hasColor ? '\x1B[36m' : ''; -final String reset = hasColor ? '\x1B[0m' : ''; -final String redLine = '$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset'; -const String arrow = '⏩'; -const String clock = '🕐'; const Map<String, ShardRunner> _kShards = <String, ShardRunner>{ - 'analyze': _analyzeRepo, 'tests': _runTests, 'tool_tests': _runToolTests, 'coverage': _runCoverage, - // 'docs': handled by docs.sh - // 'build_and_deploy_gallery': handled by deploy_gallery.sh }; const Duration _kLongTimeout = Duration(minutes: 45); @@ -46,10 +32,10 @@ /// script with the parameter --local-engine=host_debug_unopt to /// use your own build of the engine. /// -/// To run the analysis part, run it with SHARD=analyze +/// To run the tool_tests part, run it with SHARD=tool_tests /// /// For example: -/// SHARD=analyze bin/cache/dart-sdk/bin/dart dev/bots/test.dart +/// SHARD=tool_tests bin/cache/dart-sdk/bin/dart dev/bots/test.dart /// bin/cache/dart-sdk/bin/dart dev/bots/test.dart --local-engine=host_debug_unopt Future<Null> main(List<String> args) async { flutterTestArgs.addAll(args); @@ -72,146 +58,6 @@ } } -Future<Null> _verifyInternationalizations() async { - final EvalResult genResult = await _evalCommand( - dart, - <String>[ - '--preview-dart-2', - path.join('dev', 'tools', 'gen_localizations.dart'), - ], - workingDirectory: flutterRoot, - ); - - final String localizationsFile = path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n', 'localizations.dart'); - final String expectedResult = await new File(localizationsFile).readAsString(); - - if (genResult.stdout.trim() != expectedResult.trim()) { - stderr - ..writeln('<<<<<<< $localizationsFile') - ..writeln(expectedResult.trim()) - ..writeln('=======') - ..writeln(genResult.stdout.trim()) - ..writeln('>>>>>>> gen_localizations') - ..writeln('The contents of $localizationsFile are different from that produced by gen_localizations.') - ..writeln() - ..writeln('Did you forget to run gen_localizations.dart after updating a .arb file?'); - exit(1); - } - print('Contents of $localizationsFile matches output of gen_localizations.dart script.'); -} - -Future<String> _getCommitRange() async { - // Using --fork-point is more conservative, and will result in the correct - // fork point, but when running locally, it may return nothing. Git is - // guaranteed to return a (reasonable, but maybe not optimal) result when not - // using --fork-point, so we fall back to that if we can't get a definitive - // fork point. See "git merge-base" documentation for more info. - EvalResult result = await _evalCommand( - 'git', - <String>['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], - workingDirectory: flutterRoot, - allowNonZeroExit: true, - ); - if (result.exitCode != 0) { - result = await _evalCommand( - 'git', - <String>['merge-base', 'FETCH_HEAD', 'HEAD'], - workingDirectory: flutterRoot, - ); - } - return result.stdout.trim(); -} - - -Future<Null> _checkForTrailingSpaces() async { - if (!Platform.isWindows) { - final String commitRange = Platform.environment.containsKey('TEST_COMMIT_RANGE') - ? Platform.environment['TEST_COMMIT_RANGE'] - : await _getCommitRange(); - final List<String> fileTypes = <String>[ - '*.dart', '*.cxx', '*.cpp', '*.cc', '*.c', '*.C', '*.h', '*.java', '*.mm', '*.m', '*.yml', - ]; - final EvalResult changedFilesResult = await _evalCommand( - 'git', <String>['diff', '-U0', '--no-color', '--name-only', commitRange, '--'] + fileTypes, - workingDirectory: flutterRoot, - ); - if (changedFilesResult.stdout == null || changedFilesResult.stdout.trim().isEmpty) { - print('No files found that need to be checked for trailing whitespace.'); - return; - } - // Only include files that actually exist, so that we don't try and grep for - // nonexistent files, which can occur when files are deleted or moved. - final List<String> changedFiles = changedFilesResult.stdout.split('\n').where((String filename) { - return new File(filename).existsSync(); - }).toList(); - if (changedFiles.isNotEmpty) { - await _runCommand('grep', - <String>[ - '--line-number', - '--extended-regexp', - r'[[:blank:]]$', - ] + changedFiles, - workingDirectory: flutterRoot, - failureMessage: '${red}Whitespace detected at the end of source code lines.$reset\nPlease remove:', - expectNonZeroExit: true, // Just means a non-zero exit code is expected. - expectedExitCode: 1, // Indicates that zero lines were found. - ); - } - } -} - -Future<Null> _analyzeRepo() async { - await _verifyNoTestPackageImports(flutterRoot); - await _verifyGeneratedPluginRegistrants(flutterRoot); - await _verifyNoBadImportsInFlutter(flutterRoot); - await _verifyNoBadImportsInFlutterTools(flutterRoot); - await _verifyInternationalizations(); - - // Analyze all the Dart code in the repo. - await _runFlutterAnalyze(flutterRoot, - options: <String>['--flutter-repo'], - ); - - // Ensure that all package dependencies are in sync. - await _runCommand(flutter, <String>['update-packages', '--verify-only'], - workingDirectory: flutterRoot, - ); - - // Analyze all the sample code in the repo - await _runCommand(dart, - <String>['--preview-dart-2', path.join(flutterRoot, 'dev', 'bots', 'analyze-sample-code.dart')], - workingDirectory: flutterRoot, - ); - - // Try with the --watch analyzer, to make sure it returns success also. - // The --benchmark argument exits after one run. - await _runFlutterAnalyze(flutterRoot, - options: <String>['--flutter-repo', '--watch', '--benchmark'], - ); - - await _checkForTrailingSpaces(); - - // Try analysis against a big version of the gallery; generate into a temporary directory. - final Directory outDir = Directory.systemTemp.createTempSync('flutter_mega_gallery.'); - - try { - await _runCommand(dart, - <String>[ - '--preview-dart-2', - path.join(flutterRoot, 'dev', 'tools', 'mega_gallery.dart'), - '--out', - outDir.path, - ], - workingDirectory: flutterRoot, - ); - await _runFlutterAnalyze(outDir.path, options: <String>['--watch', '--benchmark']); - } finally { - outDir.deleteSync(recursive: true); - } - - print('${bold}DONE: Analysis successful.$reset'); -} - Future<Null> _runSmokeTests() async { // Verify that the tests actually return failure on failure and success on // success. @@ -278,7 +124,7 @@ printOutput: false, timeout: _kShortTimeout, ), - _runCommand(flutter, + runCommand(flutter, <String>['drive', '--use-existing-app', '-t', path.join('test_driver', 'failure.dart')], workingDirectory: path.join(flutterRoot, 'packages', 'flutter_driver'), expectNonZeroExit: true, @@ -374,7 +220,7 @@ toolsArgs += ' --enable-asserts'; pubEnvironment['FLUTTER_TOOL_ARGS'] = toolsArgs.trim(); } - return _runCommand( + return runCommand( pub, args, workingDirectory: workingDirectory, environment: pubEnvironment, @@ -393,115 +239,6 @@ final int exitCode; } -Future<EvalResult> _evalCommand(String executable, List<String> arguments, { - @required String workingDirectory, - Map<String, String> environment, - bool skip = false, - bool allowNonZeroExit = false, -}) async { - final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}'; - final String relativeWorkingDir = path.relative(workingDirectory); - if (skip) { - _printProgress('SKIPPING', relativeWorkingDir, commandDescription); - return null; - } - _printProgress('RUNNING', relativeWorkingDir, commandDescription); - - final DateTime start = new DateTime.now(); - final Process process = await Process.start(executable, arguments, - workingDirectory: workingDirectory, - environment: environment, - ); - - final Future<List<List<int>>> savedStdout = process.stdout.toList(); - final Future<List<List<int>>> savedStderr = process.stderr.toList(); - final int exitCode = await process.exitCode; - final EvalResult result = new EvalResult( - stdout: utf8.decode((await savedStdout).expand((List<int> ints) => ints).toList()), - stderr: utf8.decode((await savedStderr).expand((List<int> ints) => ints).toList()), - exitCode: exitCode, - ); - - print('$clock ELAPSED TIME: $bold${elapsedTime(start)}$reset for $commandDescription in $relativeWorkingDir: '); - - if (exitCode != 0 && !allowNonZeroExit) { - stderr.write(result.stderr); - print( - '$redLine\n' - '${bold}ERROR:$red Last command exited with $exitCode.$reset\n' - '${bold}Command:$red $commandDescription$reset\n' - '${bold}Relative working directory:$red $relativeWorkingDir$reset\n' - '$redLine' - ); - exit(1); - } - - return result; -} - -String elapsedTime(DateTime start) { - return new DateTime.now().difference(start).toString(); -} - -Future<Null> _runCommand(String executable, List<String> arguments, { - String workingDirectory, - Map<String, String> environment, - bool expectNonZeroExit = false, - int expectedExitCode, - String failureMessage, - bool printOutput = true, - bool skip = false, - Duration timeout = _kLongTimeout, -}) async { - final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}'; - final String relativeWorkingDir = path.relative(workingDirectory); - if (skip) { - _printProgress('SKIPPING', relativeWorkingDir, commandDescription); - return null; - } - _printProgress('RUNNING', relativeWorkingDir, commandDescription); - - final DateTime start = new DateTime.now(); - final Process process = await Process.start(executable, arguments, - workingDirectory: workingDirectory, - environment: environment, - ); - - Future<List<List<int>>> savedStdout, savedStderr; - if (printOutput) { - await Future.wait(<Future<void>>[ - stdout.addStream(process.stdout), - stderr.addStream(process.stderr) - ]); - } else { - savedStdout = process.stdout.toList(); - savedStderr = process.stderr.toList(); - } - - final int exitCode = await process.exitCode.timeout(timeout, onTimeout: () { - stderr.writeln('Process timed out after $timeout'); - return expectNonZeroExit ? 0 : 1; - }); - print('$clock ELAPSED TIME: $bold${elapsedTime(start)}$reset for $commandDescription in $relativeWorkingDir: '); - if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) { - if (failureMessage != null) { - print(failureMessage); - } - if (!printOutput) { - stdout.writeln(utf8.decode((await savedStdout).expand((List<int> ints) => ints).toList())); - stderr.writeln(utf8.decode((await savedStderr).expand((List<int> ints) => ints).toList())); - } - print( - '$redLine\n' - '${bold}ERROR:$red Last command exited with $exitCode (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset\n' - '${bold}Command:$cyan $commandDescription$reset\n' - '${bold}Relative working directory:$red $relativeWorkingDir$reset\n' - '$redLine' - ); - exit(1); - } -} - Future<Null> _runFlutterTest(String workingDirectory, { String script, bool expectFailure = false, @@ -527,7 +264,7 @@ } args.add(script); } - return _runCommand(flutter, args, + return runCommand(flutter, args, workingDirectory: workingDirectory, expectNonZeroExit: expectFailure, printOutput: printOutput, @@ -536,305 +273,6 @@ ); } -Future<Null> _runFlutterAnalyze(String workingDirectory, { - List<String> options = const <String>[] -}) { - return _runCommand(flutter, <String>['analyze']..addAll(options), - workingDirectory: workingDirectory, - ); -} - -Future<Null> _verifyNoTestPackageImports(String workingDirectory) async { - // TODO(ianh): Remove this whole test once https://github.com/dart-lang/matcher/issues/98 is fixed. - final List<String> shims = <String>[]; - final List<String> errors = new Directory(workingDirectory) - .listSync(recursive: true) - .where((FileSystemEntity entity) { - return entity is File && entity.path.endsWith('.dart'); - }) - .map<String>((FileSystemEntity entity) { - final File file = entity; - final String name = path.relative(file.path, from: workingDirectory); - if (name.startsWith('bin/cache') || - name == 'dev/bots/test.dart' || - name.startsWith('.pub-cache')) - return null; - final String data = file.readAsStringSync(); - if (data.contains("import 'package:test/test.dart'")) { - if (data.contains("// Defines a 'package:test' shim.")) { - shims.add(' $name'); - if (!data.contains('https://github.com/dart-lang/matcher/issues/98')) - return ' $name: Shims must link to the isInstanceOf issue.'; - if (data.contains("import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;") && - data.contains("export 'package:test/test.dart' hide TypeMatcher, isInstanceOf;")) - return null; - return ' $name: Shim seems to be missing the expected import/export lines.'; - } - final int count = 'package:test'.allMatches(data).length; - if (file.path.contains('/test_driver/') || - name.startsWith('dev/missing_dependency_tests/') || - name.startsWith('dev/automated_tests/') || - name.startsWith('packages/flutter/test/engine/') || - name.startsWith('examples/layers/test/smoketests/raw/') || - name.startsWith('examples/layers/test/smoketests/rendering/') || - name.startsWith('examples/flutter_gallery/test/calculator')) { - // We only exempt driver tests, some of our special trivial tests. - // Driver tests aren't typically expected to use TypeMatcher and company. - // The trivial tests don't typically do anything at all and it would be - // a pain to have to give them a shim. - if (!data.contains("import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;")) - return ' $name: test does not hide TypeMatcher and isInstanceOf from package:test; consider using a shim instead.'; - assert(count > 0); - if (count == 1) - return null; - return ' $name: uses \'package:test\' $count times.'; - } - if (name.startsWith('packages/flutter_test/')) { - // flutter_test has deep ties to package:test - return null; - } - if (data.contains("import 'package:test/test.dart' as test_package;") || - data.contains("import 'package:test/test.dart' as test_package show ")) { - if (count == 1) - return null; - } - return ' $name: uses \'package:test\' directly'; - } - return null; - }) - .where((String line) => line != null) - .toList() - ..sort(); - - // Fail if any errors - if (errors.isNotEmpty) { - print('$redLine'); - final String s1 = errors.length == 1 ? 's' : ''; - final String s2 = errors.length == 1 ? '' : 's'; - print('${bold}The following file$s2 use$s1 \'package:test\' incorrectly:$reset'); - print(errors.join('\n')); - print('Rather than depending on \'package:test\' directly, use one of the shims:'); - print(shims.join('\n')); - print('This insulates us from breaking changes in \'package:test\'.'); - print('$redLine\n'); - exit(1); - } -} - -Future<Null> _verifyNoBadImportsInFlutter(String workingDirectory) async { - final List<String> errors = <String>[]; - final String libPath = path.join(workingDirectory, 'packages', 'flutter', 'lib'); - final String srcPath = path.join(workingDirectory, 'packages', 'flutter', 'lib', 'src'); - // Verify there's one libPath/*.dart for each srcPath/*/. - final List<String> packages = new Directory(libPath).listSync() - .where((FileSystemEntity entity) => entity is File && path.extension(entity.path) == '.dart') - .map<String>((FileSystemEntity entity) => path.basenameWithoutExtension(entity.path)) - .toList()..sort(); - final List<String> directories = new Directory(srcPath).listSync() - .whereType<Directory>() - .map<String>((Directory entity) => path.basename(entity.path)) - .toList()..sort(); - if (!_matches(packages, directories)) { - errors.add( - 'flutter/lib/*.dart does not match flutter/lib/src/*/:\n' - 'These are the exported packages:\n' + - packages.map((String path) => ' lib/$path.dart').join('\n') + - 'These are the directories:\n' + - directories.map((String path) => ' lib/src/$path/').join('\n') - ); - } - // Verify that the imports are well-ordered. - final Map<String, Set<String>> dependencyMap = <String, Set<String>>{}; - for (String directory in directories) { - dependencyMap[directory] = _findDependencies(path.join(srcPath, directory), errors, checkForMeta: directory != 'foundation'); - } - for (String package in dependencyMap.keys) { - if (dependencyMap[package].contains(package)) { - errors.add( - 'One of the files in the $yellow$package$reset package imports that package recursively.' - ); - } - } - for (String package in dependencyMap.keys) { - final List<String> loop = _deepSearch(dependencyMap, package); - if (loop != null) { - errors.add( - '${yellow}Dependency loop:$reset ' + - loop.join(' depends on ') - ); - } - } - // Fail if any errors - if (errors.isNotEmpty) { - print('$redLine'); - if (errors.length == 1) { - print('${bold}An error was detected when looking at import dependencies within the Flutter package:$reset\n'); - } else { - print('${bold}Multiple errors were detected when looking at import dependencies within the Flutter package:$reset\n'); - } - print(errors.join('\n\n')); - print('$redLine\n'); - exit(1); - } -} - -bool _matches<T>(List<T> a, List<T> b) { - assert(a != null); - assert(b != null); - if (a.length != b.length) - return false; - for (int index = 0; index < a.length; index += 1) { - if (a[index] != b[index]) - return false; - } - return true; -} - -final RegExp _importPattern = new RegExp(r"import 'package:flutter/([^.]+)\.dart'"); -final RegExp _importMetaPattern = new RegExp(r"import 'package:meta/meta.dart'"); - -Set<String> _findDependencies(String srcPath, List<String> errors, { bool checkForMeta = false }) { - return new Directory(srcPath).listSync(recursive: true).where((FileSystemEntity entity) { - return entity is File && path.extension(entity.path) == '.dart'; - }).map<Set<String>>((FileSystemEntity entity) { - final Set<String> result = new Set<String>(); - final File file = entity; - for (String line in file.readAsLinesSync()) { - Match match = _importPattern.firstMatch(line); - if (match != null) - result.add(match.group(1)); - if (checkForMeta) { - match = _importMetaPattern.firstMatch(line); - if (match != null) { - errors.add( - '${file.path}\nThis package imports the ${yellow}meta$reset package.\n' - 'You should instead import the "foundation.dart" library.' - ); - } - } - } - return result; - }).reduce((Set<String> value, Set<String> element) { - value ??= new Set<String>(); - value.addAll(element); - return value; - }); -} - -List<T> _deepSearch<T>(Map<T, Set<T>> map, T start, [ Set<T> seen ]) { - for (T key in map[start]) { - if (key == start) - continue; // we catch these separately - if (seen != null && seen.contains(key)) - return <T>[start, key]; - final List<T> result = _deepSearch( - map, - key, - (seen == null ? new Set<T>.from(<T>[start]) : new Set<T>.from(seen))..add(key), - ); - if (result != null) { - result.insert(0, start); - // Only report the shortest chains. - // For example a->b->a, rather than c->a->b->a. - // Since we visit every node, we know the shortest chains are those - // that start and end on the loop. - if (result.first == result.last) - return result; - } - } - return null; -} - -Future<Null> _verifyNoBadImportsInFlutterTools(String workingDirectory) async { - final List<String> errors = <String>[]; - for (FileSystemEntity entity in new Directory(path.join(workingDirectory, 'packages', 'flutter_tools', 'lib')) - .listSync(recursive: true) - .where((FileSystemEntity entity) => entity is File && path.extension(entity.path) == '.dart')) { - final File file = entity; - if (file.readAsStringSync().contains('package:flutter_tools/')) { - errors.add('$yellow${file.path}$reset imports flutter_tools.'); - } - } - // Fail if any errors - if (errors.isNotEmpty) { - print('$redLine'); - if (errors.length == 1) { - print('${bold}An error was detected when looking at import dependencies within the flutter_tools package:$reset\n'); - } else { - print('${bold}Multiple errors were detected when looking at import dependencies within the flutter_tools package:$reset\n'); - } - print(errors.join('\n\n')); - print('$redLine\n'); - exit(1); - } -} - -void _printProgress(String action, String workingDir, String command) { - print('$arrow $action: cd $cyan$workingDir$reset; $yellow$command$reset'); -} - -Future<Null> _verifyGeneratedPluginRegistrants(String flutterRoot) async { - final Directory flutterRootDir = new Directory(flutterRoot); - - final Map<String, List<File>> packageToRegistrants = <String, List<File>>{}; - - for (FileSystemEntity entity in flutterRootDir.listSync(recursive: true)) { - if (entity is! File) - continue; - if (_isGeneratedPluginRegistrant(entity)) { - final String package = _getPackageFor(entity, flutterRootDir); - final List<File> registrants = packageToRegistrants.putIfAbsent(package, () => <File>[]); - registrants.add(entity); - } - } - - final Set<String> outOfDate = new Set<String>(); - - for (String package in packageToRegistrants.keys) { - final Map<File, String> fileToContent = <File, String>{}; - for (File f in packageToRegistrants[package]) { - fileToContent[f] = f.readAsStringSync(); - } - await _runCommand(flutter, <String>['inject-plugins'], - workingDirectory: package, - printOutput: false, - ); - for (File registrant in fileToContent.keys) { - if (registrant.readAsStringSync() != fileToContent[registrant]) { - outOfDate.add(registrant.path); - } - } - } - - if (outOfDate.isNotEmpty) { - print('$redLine'); - print('${bold}The following GeneratedPluginRegistrants are out of date:$reset'); - for (String registrant in outOfDate) { - print(' - $registrant'); - } - print('\nRun "flutter inject-plugins" in the package that\'s out of date.'); - print('$redLine'); - exit(1); - } -} - -String _getPackageFor(File entity, Directory flutterRootDir) { - for (Directory dir = entity.parent; dir != flutterRootDir; dir = dir.parent) { - if (new File(path.join(dir.path, 'pubspec.yaml')).existsSync()) { - return dir.path; - } - } - throw new ArgumentError('$entity is not within a dart package.'); -} - -bool _isGeneratedPluginRegistrant(File file) { - final String filename = path.basename(file.path); - return !file.path.contains('.pub-cache') - && (filename == 'GeneratedPluginRegistrant.java' || - filename == 'GeneratedPluginRegistrant.h' || - filename == 'GeneratedPluginRegistrant.m'); -} - Future<Null> _verifyVersion(String filename) async { if (!new File(filename).existsSync()) { print('$redLine');