|  | #!/usr/bin/env dart | 
|  | // Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file | 
|  | // for details. 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:args/args.dart'; | 
|  | import 'package:glob/glob.dart'; | 
|  |  | 
|  | final parser = ArgParser() | 
|  | ..addMultiOption('bot', | 
|  | abbr: 'b', | 
|  | help: 'Select the bots matching the glob pattern [option is repeatable]', | 
|  | splitCommas: false) | 
|  | ..addFlag('verbose', abbr: 'v', help: 'Verbose output.', negatable: false) | 
|  | ..addFlag('help', help: 'Show the program usage.', negatable: false); | 
|  |  | 
|  | void printUsage() { | 
|  | print(""" | 
|  | Usage: ${Platform.executable} ${Platform.script} [OLDER_COMMIT] [NEWER_COMMIT] | 
|  |  | 
|  | The options are as follows: | 
|  |  | 
|  | ${parser.usage}"""); | 
|  | } | 
|  |  | 
|  | late bool verbose; | 
|  |  | 
|  | void main(List<String> args) async { | 
|  | final options = parser.parse(args); | 
|  | if (options.flag("help")) { | 
|  | printUsage(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | final commits = options.rest; | 
|  | if (commits.length < 2) { | 
|  | print('Need to supply at least two commits.'); | 
|  | printUsage(); | 
|  | exitCode = 1; | 
|  | return; | 
|  | } | 
|  | verbose = options.flag('verbose'); | 
|  |  | 
|  | final globs = List<Glob>.from( | 
|  | options.multiOption('bot').map((pattern) => Glob(pattern))); | 
|  | final vmBuilders = loadVmBuildersFromTestMatrix(globs); | 
|  |  | 
|  | final futures = <Future<List<Result>>>[]; | 
|  | for (final commit in commits) { | 
|  | final DateTime date = await getDateOfCommit(commit); | 
|  | futures.add(getResults(commit, date, vmBuilders)); | 
|  | } | 
|  |  | 
|  | final results = await Future.wait(futures); | 
|  | for (int i = 0; i < results.length - 1; i++) { | 
|  | final commitB = commits[i]; | 
|  | final commitA = commits[i + 1]; | 
|  |  | 
|  | print('\nResult changes between $commitB -> $commitA:'); | 
|  | final commonGroups = | 
|  | buildCommonGroups(commitA, commitB, results[i], results[i + 1]); | 
|  | for (final commonGroup in commonGroups) { | 
|  | final builders = commonGroup.builders; | 
|  |  | 
|  | print(''); | 
|  | for (final group in commonGroup.groups) { | 
|  | final diff = group.diffs.first; | 
|  | print('${group.test} ${diff.before} -> ${diff.after}'); | 
|  | } | 
|  | for (final b in extractBuilderPattern(builders)) { | 
|  | print('   on $b'); | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | Future<DateTime> getDateOfCommit(String commit) async { | 
|  | final result = await Process.run( | 
|  | 'git', ['show', '-s', '--format=%cd', '--date=iso-strict', commit]); | 
|  | if (result.exitCode != 0) { | 
|  | print('Could not determine date of commit $commit. Git reported:\n'); | 
|  | print(result.stdout); | 
|  | print(result.stderr); | 
|  | exit(1); | 
|  | } | 
|  | return DateTime.parse((result.stdout as String).trim()); | 
|  | } | 
|  |  | 
|  | Future<List<Result>> getResults( | 
|  | String commit, DateTime dateC, Set<String> builders) async { | 
|  | final DateTime date0 = dateC.add(const Duration(hours: 24)); | 
|  | final DateTime date2 = dateC.subtract(const Duration(hours: 24)); | 
|  | final query = ''' | 
|  | SELECT commit_time, builder_name, build_number, name, result, expected FROM `dart-ci.results.results` | 
|  | WHERE commit_hash="$commit" | 
|  | AND matches=false | 
|  | AND (_PARTITIONDATE = "${formatDate(date0)}" OR | 
|  | _PARTITIONDATE = "${formatDate(dateC)}" OR | 
|  | _PARTITIONDATE = "${formatDate(date2)}" ) | 
|  | AND (STARTS_WITH(builder_name, "vm-") OR | 
|  | STARTS_WITH(builder_name, "app-") OR | 
|  | STARTS_WITH(builder_name, "cross-")) | 
|  | AND ((flaky is NULL) OR flaky=false) | 
|  | ORDER BY name'''; | 
|  |  | 
|  | final arguments = <String>[ | 
|  | 'query', | 
|  | '--format=prettyjson', | 
|  | '--project_id=dart-ci', | 
|  | '--nouse_legacy_sql', | 
|  | '-n', | 
|  | '1000000', | 
|  | query, | 
|  | ]; | 
|  | if (verbose) { | 
|  | print('Executing query:\n    bq ${arguments.join(' ')}'); | 
|  | } | 
|  |  | 
|  | final result = await Process.run('bq', arguments); | 
|  | if (result.exitCode == 0) { | 
|  | File('$commit.json').writeAsStringSync(result.stdout); | 
|  | final resultsForCommit = json.decode(result.stdout); | 
|  |  | 
|  | final results = <Result>[]; | 
|  | for (final Map<String, dynamic> result in resultsForCommit) { | 
|  | final builderName = result['builder_name']; | 
|  | if (!builders.contains(builderName)) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | final failure = Result(commit, builderName, result['build_number'], | 
|  | result['name'], result['expected'], result['result']); | 
|  | results.add(failure); | 
|  | } | 
|  |  | 
|  | results.sort((Result a, Result b) { | 
|  | final c = a.name.compareTo(b.name); | 
|  | if (c != 0) return c; | 
|  | return a.builderName.compareTo(b.builderName); | 
|  | }); | 
|  |  | 
|  | return results; | 
|  | } else { | 
|  | print('Running the following query failed:\nbq ${arguments.join(' ')}'); | 
|  | print('Exit code: ${result.exitCode}'); | 
|  | final stdout = (result.stdout as String).trim(); | 
|  | if (stdout.isNotEmpty) { | 
|  | print('Stdout:\n$stdout'); | 
|  | } | 
|  | final stderr = (result.stderr as String).trim(); | 
|  | if (stderr.isNotEmpty) { | 
|  | print('Stderr:\n$stderr'); | 
|  | } | 
|  | return <Result>[]; | 
|  | } | 
|  | } | 
|  |  | 
|  | List<CommonGroup> buildCommonGroups(String commitA, String commitB, | 
|  | List<Result> commitResults, List<Result> commitResultsBefore) { | 
|  | // If a test has same outcome across many vm builders | 
|  | final diffs = <Diff>[]; | 
|  | int i = 0; | 
|  | int j = 0; | 
|  | while (i < commitResultsBefore.length && j < commitResults.length) { | 
|  | final a = commitResultsBefore[i]; | 
|  | final b = commitResults[j]; | 
|  |  | 
|  | // Is a smaller than b, then we had a failure before and no longer one. | 
|  | if (a.name.compareTo(b.name) < 0 || | 
|  | (a.name.compareTo(b.name) == 0 && | 
|  | a.builderName.compareTo(b.builderName) < 0)) { | 
|  | diffs.add(Diff(a, null)); | 
|  | i++; | 
|  | continue; | 
|  | } | 
|  |  | 
|  | // Is b smaller than a, then we had no failure before but have one now. | 
|  | if (b.name.compareTo(a.name) < 0 || | 
|  | (b.name.compareTo(a.name) == 0 && | 
|  | b.builderName.compareTo(a.builderName) < 0)) { | 
|  | diffs.add(Diff(null, b)); | 
|  | j++; | 
|  | continue; | 
|  | } | 
|  |  | 
|  | // Else we must have the same name and builder. | 
|  | if (a.name != b.name || a.builderName != b.builderName) throw 'BUG'; | 
|  |  | 
|  | if (a.expected != b.expected || a.result != b.result) { | 
|  | diffs.add(Diff(a, b)); | 
|  | } | 
|  | i++; | 
|  | j++; | 
|  | } | 
|  |  | 
|  | while (i < commitResultsBefore.length) { | 
|  | final a = commitResultsBefore[i++]; | 
|  | diffs.add(Diff(a, null)); | 
|  | } | 
|  |  | 
|  | while (j < commitResults.length) { | 
|  | final b = commitResults[j++]; | 
|  | diffs.add(Diff(null, b)); | 
|  | } | 
|  |  | 
|  | // If a test has same outcome across many vm builders | 
|  | final groups = <GroupedDiff>[]; | 
|  | int h = 0; | 
|  | while (h < diffs.length) { | 
|  | final d = diffs[h++]; | 
|  | final builders = <String>{}..add(d.builder); | 
|  | final groupDiffs = <Diff>[d]; | 
|  |  | 
|  | while (h < diffs.length) { | 
|  | final nd = diffs[h]; | 
|  | if (d.test == nd.test) { | 
|  | if (d.sameExpectationDifferenceAs(nd)) { | 
|  | builders.add(nd.builder); | 
|  | groupDiffs.add(nd); | 
|  | h++; | 
|  | continue; | 
|  | } | 
|  | } | 
|  | break; | 
|  | } | 
|  |  | 
|  | groups.add(GroupedDiff(d.test, builders.toList()..sort(), groupDiffs)); | 
|  | } | 
|  |  | 
|  | final commonGroups = <String, List<GroupedDiff>>{}; | 
|  | for (final group in groups) { | 
|  | final key = group.builders.join(' '); | 
|  | commonGroups.putIfAbsent(key, () => <GroupedDiff>[]).add(group); | 
|  | } | 
|  |  | 
|  | final commonGroupList = commonGroups.values | 
|  | .map((list) => CommonGroup(list.first.builders, list)) | 
|  | .toList(); | 
|  | commonGroupList | 
|  | .sort((a, b) => a.builders.length.compareTo(b.builders.length)); | 
|  | return commonGroupList; | 
|  | } | 
|  |  | 
|  | class CommonGroup { | 
|  | final List<String> builders; | 
|  | final List<GroupedDiff> groups; | 
|  | CommonGroup(this.builders, this.groups); | 
|  | } | 
|  |  | 
|  | class GroupedDiff { | 
|  | final String test; | 
|  | final List<String> builders; | 
|  | final List<Diff> diffs; | 
|  |  | 
|  | GroupedDiff(this.test, this.builders, this.diffs); | 
|  | } | 
|  |  | 
|  | class Diff { | 
|  | final Result? before; | 
|  | final Result? after; | 
|  |  | 
|  | Diff(this.before, this.after); | 
|  |  | 
|  | String get test => (before?.name ?? after?.name)!; | 
|  | String get builder => (before?.builderName ?? after?.builderName)!; | 
|  |  | 
|  | bool sameExpectationDifferenceAs(Diff other) { | 
|  | if ((before == null) != (other.before == null)) return false; | 
|  | if ((after == null) != (other.after == null)) return false; | 
|  |  | 
|  | if (before != null) { | 
|  | if (!before!.sameResult(other.before!)) return false; | 
|  | } | 
|  | if (after != null) { | 
|  | if (!after!.sameResult(other.after!)) return false; | 
|  | } | 
|  | return true; | 
|  | } | 
|  | } | 
|  |  | 
|  | class Result { | 
|  | final String commit; | 
|  | final String builderName; | 
|  | final String buildNumber; | 
|  | final String name; | 
|  | final String expected; | 
|  | final String result; | 
|  |  | 
|  | Result(this.commit, this.builderName, this.buildNumber, this.name, | 
|  | this.expected, this.result); | 
|  |  | 
|  | @override | 
|  | String toString() => '(expected: $expected, actual: $result)'; | 
|  |  | 
|  | bool sameResult(Result other) { | 
|  | return name == other.name && | 
|  | expected == other.expected && | 
|  | result == other.result; | 
|  | } | 
|  |  | 
|  | bool equals(Object other) { | 
|  | if (other is Result) { | 
|  | if (name != other.name) return false; | 
|  | if (builderName != other.builderName) return false; | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | @override | 
|  | int get hashCode => name.hashCode ^ builderName.hashCode; | 
|  |  | 
|  | @override | 
|  | bool operator ==(Object other) { | 
|  | // TODO: implement == | 
|  | return super == other; | 
|  | } | 
|  | } | 
|  |  | 
|  | String currentDate() { | 
|  | final timestamp = DateTime.now().toUtc().toIso8601String(); | 
|  | return timestamp.substring(0, timestamp.indexOf('T')); | 
|  | } | 
|  |  | 
|  | Set<String> loadVmBuildersFromTestMatrix(List<Glob> globs) { | 
|  | final contents = File('tools/bots/test_matrix.json').readAsStringSync(); | 
|  | final testMatrix = json.decode(contents) as Map<String, dynamic>; | 
|  |  | 
|  | final vmBuilders = <String>{}; | 
|  | for (final config in testMatrix['builder_configurations']) { | 
|  | for (final builder in (config as Map)['builders']) { | 
|  | // Cast to a string. | 
|  | builder as String; | 
|  |  | 
|  | if (builder.startsWith('vm-') || builder.startsWith('app-')) { | 
|  | vmBuilders.add(builder); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // This one is in the test_matrix.json but we don't run it on CI. | 
|  | vmBuilders.remove('vm-kernel-asan-linux-release-ia32'); | 
|  |  | 
|  | if (globs.isNotEmpty) { | 
|  | vmBuilders.removeWhere((String builder) { | 
|  | return !globs.any((Glob glob) => glob.matches(builder)); | 
|  | }); | 
|  | } | 
|  |  | 
|  | return vmBuilders; | 
|  | } | 
|  |  | 
|  | List<String> extractBuilderPattern(List<String> builders) { | 
|  | final all = Set<String>.from(builders); | 
|  |  | 
|  | String reduce(String builder, List<String> possibilities) { | 
|  | for (final pos in possibilities) { | 
|  | if (builder.contains(pos)) { | 
|  | final existing = <String>[]; | 
|  | final available = <String>[]; | 
|  | for (final pos2 in possibilities) { | 
|  | final builder2 = builder.replaceFirst(pos, pos2); | 
|  | if (all.contains(builder2)) { | 
|  | existing.add(builder2); | 
|  | available.add(pos2); | 
|  | } | 
|  | } | 
|  | if (existing.length > 1) { | 
|  | all.removeAll(existing); | 
|  | final replacement = | 
|  | builder.replaceFirst(pos, '{${available.join(',')}}'); | 
|  | all.add(replacement); | 
|  | return replacement; | 
|  | } | 
|  | } | 
|  | } | 
|  | return builder; | 
|  | } | 
|  |  | 
|  | for (String builder in builders) { | 
|  | if (all.contains(builder)) { | 
|  | builder = reduce(builder, const ['debug', 'release', 'product']); | 
|  | } | 
|  | } | 
|  | for (String builder in all.toList()) { | 
|  | if (all.contains(builder)) { | 
|  | builder = reduce(builder, const ['mac', 'linux', 'win']); | 
|  | } | 
|  | } | 
|  |  | 
|  | for (String builder in all.toList()) { | 
|  | if (all.contains(builder)) { | 
|  | builder = reduce(builder, const [ | 
|  | 'ia32', | 
|  | 'x64', | 
|  | 'simarm', | 
|  | 'simarm64', | 
|  | 'arm', | 
|  | 'arm64', | 
|  | ]); | 
|  | } | 
|  | } | 
|  | return all.toList()..sort(); | 
|  | } | 
|  |  | 
|  | String formatDate(DateTime date) { | 
|  | final s = date.toIso8601String(); | 
|  | return s.substring(0, s.indexOf('T')); | 
|  | } |