| #!/usr/bin/env dart |
| // Copyright (c) 2018, 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. |
| |
| // Update the flakiness data with a set of fresh results. |
| |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:args/args.dart'; |
| |
| import 'package:test_runner/bot_results.dart'; |
| |
| void main(List<String> args) async { |
| final parser = ArgParser(); |
| parser.addFlag('help', help: 'Show the program usage.', negatable: false); |
| parser.addOption('input', abbr: 'i', help: 'Input flakiness file.'); |
| parser.addOption('output', abbr: 'o', help: 'Output flakiness file.'); |
| parser.addOption('build-id', help: 'Logdog ID of this buildbot run'); |
| parser.addOption('commit', help: 'Commit hash of this buildbot run'); |
| parser.addFlag('no-forgive', help: 'Don\'t remove any flaky records'); |
| |
| final options = parser.parse(args); |
| if (options.flag('help')) { |
| print(''' |
| Usage: update_flakiness.dart [OPTION]... [RESULT-FILE]... |
| Update the flakiness data with a set of fresh results. |
| |
| The options are as follows: |
| |
| ${parser.usage}'''); |
| return; |
| } |
| final parameters = options.rest; |
| |
| // Load the existing flakiness data, if any. |
| final data = options.option('input') != null |
| ? await loadResultsMap(options.option('input')!) |
| : <String, Map<String, dynamic>>{}; |
| |
| final resultsForInactiveFlakiness = { |
| for (final flakyTest in data.keys) |
| if (data[flakyTest]!['active'] == false) flakyTest: <String>{} |
| }; |
| |
| final now = DateTime.now(); |
| final nowString = now.toIso8601String(); |
| // Incrementally update the flakiness data with each observed result. |
| for (final path in parameters) { |
| final results = await loadResults(path); |
| for (final resultObject in results) { |
| final String configuration = resultObject['configuration']; |
| final String name = resultObject['name']; |
| final String result = resultObject['result']; |
| final key = '$configuration:$name'; |
| resultsForInactiveFlakiness[key]?.add(result); |
| final testData = data[key] ??= {}; |
| testData['configuration'] = configuration; |
| testData['name'] = name; |
| testData['expected'] = resultObject['expected']; |
| final List<dynamic> outcomes = testData['outcomes'] ??= <dynamic>[]; |
| if (!outcomes.contains(result)) { |
| outcomes |
| ..add(result) |
| ..sort(); |
| testData['last_new_result_seen'] = nowString; |
| } |
| if (testData['current'] == result) { |
| var currentCounter = testData['current_counter'] as int; |
| testData['current_counter'] = currentCounter + 1; |
| } else { |
| testData['current'] = result; |
| testData['current_counter'] = 1; |
| } |
| Map<String, dynamic> mapField(String key) => |
| testData[key] ??= <String, dynamic>{}; |
| mapField('occurrences')[result] = |
| (mapField('occurrences')[result] as int? ?? 0) + 1; |
| mapField('first_seen')[result] ??= nowString; |
| mapField('last_seen')[result] = nowString; |
| mapField('matches')[result] = resultObject['matches']; |
| if (options.option('build-id') != null) { |
| mapField('build_ids')[result] = options.option('build-id')!; |
| } |
| if (options.option('commit') != null) { |
| mapField('commits')[result] = options.option('commit')!; |
| } |
| } |
| } |
| |
| // Write out the new flakiness data. |
| final flakinessHorizon = now.subtract(Duration(days: 7)); |
| final sink = options.option('output') != null |
| ? File(options.option('output')!).openWrite() |
| : stdout; |
| final keys = data.keys.toList()..sort(); |
| for (final key in keys) { |
| final testData = data[key]!; |
| if ((testData['outcomes'] as List).length < 2) continue; |
| // Reactivate inactive flaky results that are flaky again. |
| if (testData['active'] == false) { |
| if (resultsForInactiveFlakiness[key]!.length > 1) { |
| testData['active'] = true; |
| testData['reactivation_count'] = |
| (testData['reactivation_count'] as int? ?? 0) + 1; |
| } |
| } else if (options.flag('no-forgive')) { |
| testData['active'] = true; |
| } else if (testData['current_counter'] as int >= 100) { |
| // Forgive tests that have been stable for 100 builds. |
| testData['active'] = false; |
| } else { |
| // Forgive tests that have been stable since flakiness horizon (one week). |
| final resultTimes = [ |
| for (final timeString in (testData['last_seen'] as Map).values) |
| DateTime.parse(timeString) |
| ]..sort(); |
| // The latest timestamp is the current result. The one before that is the |
| // timestamp of the latest flake. Timestamps are sorted from earliest to latest. |
| if (resultTimes[resultTimes.length - 2].isBefore(flakinessHorizon)) { |
| testData['active'] = false; |
| } else { |
| testData['active'] = true; |
| } |
| } |
| sink.writeln(jsonEncode(testData)); |
| } |
| } |