|  | #!/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)); | 
|  | } | 
|  | } |