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