-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathupdate_blamelists.dart
186 lines (164 loc) · 6.74 KB
/
update_blamelists.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// Copyright (c) 2020, 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.
// This script is used by the bisection mechanism to update the blamelists
// of active, non-approved failures which include the commit of the current
// bisection build.
import 'dart:io';
import 'package:args/args.dart';
import 'package:test_runner/bot_results.dart';
import 'lib/src/firestore.dart';
const newTest = 'new test';
const skippedTest = 'skipped';
const maxAttempts = 20;
late FirestoreDatabase database;
class ResultRecord {
final Map<String, dynamic> data;
ResultRecord(this.data);
Map field(String name) => (data['fields'] as Map)[name];
int get blamelistStartIndex {
return int.parse(field('blamelist_start_index')['integerValue']);
}
set blamelistStartIndex(int index) {
field('blamelist_start_index')['integerValue'] = '$index';
}
int get blamelistEndIndex {
return int.parse(field('blamelist_end_index')['integerValue']);
}
String get result => field('result')['stringValue'] /*!*/;
String get previousResult => field('previous_result')['stringValue'] /*!*/;
String get name => field('name')['stringValue'] /*!*/;
String get updateTime => data['updateTime'] /*!*/;
}
Query unapprovedActiveFailuresQuery(String configuration) {
return Query(
'results',
CompositeFilter('AND', [
Field('approved').equals(Value.boolean(false)),
// TODO(karlklose): also search for inactive failures?
Field('active_configurations').contains(Value.string(configuration)),
// TODO(karlklose): add index to check for blamelist_start_index < ?
]));
}
Future<int> getCommitIndex(String commit) async {
try {
var document =
(await database.getDocument('commits', commit)).cast<String, dynamic>();
var index = (document['fields'] as Map)['index'] as Map;
if (index['integerValue'] == null) {
throw Exception('Expected an integer, but got "$index"');
}
return int.parse(index['integerValue']);
} catch (exception) {
print('Could not retrieve index for commit "$commit".\n');
rethrow;
}
}
/// Compute if the record should be updated based on the outcomes in the
/// result record and the new test result.
bool shouldUpdateRecord(ResultRecord resultRecord, Result? testResult) {
if (testResult == null || !testResult.matches) {
return false;
}
var baseline = testResult.expectation.toLowerCase();
if (resultRecord.previousResult.toLowerCase() != baseline) {
// Currently we only support the case where a bisection run improves the
// accuracy of a "Green" -> "Red" result record.
return false;
}
if (resultRecord.result.toLowerCase() == newTest ||
resultRecord.result.toLowerCase() == skippedTest) {
// Skipped tests are often configuration dependent, so it could be wrong
// to generalize their effect for the result record to different
// configurations.
return false;
}
return true;
}
Future<void> updateBlameLists(String configuration, String commit,
Map<String, Map<String, dynamic>> testResults) async {
int commitIndex = await getCommitIndex(commit);
var query = unapprovedActiveFailuresQuery(configuration);
bool needsRetry;
int attempts = 0;
do {
needsRetry = false;
var documentPaths = (await database.runQuery(query))
.cast<Map>()
.where((result) => result['document'] != null)
.map((result) => (result['document'] as Map)['name'] as String);
for (var documentPath in documentPaths) {
database.beginTransaction();
var documentName = documentPath.split('/').last;
final docMap = await database.getDocument('results', documentName);
var result = ResultRecord(docMap.cast<String, dynamic>());
if (commitIndex < result.blamelistStartIndex ||
commitIndex >= result.blamelistEndIndex) {
continue;
}
String name = result.name;
var testResultData = testResults['$configuration:$name'];
var testResult =
testResultData != null ? Result.fromMap(testResultData) : null;
if (!shouldUpdateRecord(result, testResult)) {
continue;
}
print('Found result record: $configuration:${result.name}: '
'${result.previousResult} -> ${result.result} '
'in ${result.blamelistStartIndex}..${result.blamelistEndIndex} '
'to update with ${testResult?.outcome} at $commitIndex.');
// We found a result representation for this test and configuration whose
// blamelist includes this results' commit but whose outcome is different
// then the outcome in the provided test results.
// This means that this commit should not be part of the result
// representation and we can update the lower bound of the commit range
// and the previous result.
var newStartIndex = commitIndex + 1;
if (newStartIndex > result.blamelistEndIndex) {
print('internal error: inconsistent results; skipping results entry');
continue;
}
result.blamelistStartIndex = newStartIndex;
var updateIndex = Update(['blamelist_start_index'], result.data);
if (!await database.commit(writes: [updateIndex])) {
// Committing the change to the database had a conflict, retry.
needsRetry = true;
if (++attempts == maxAttempts) {
throw Exception('Exceeded maximum retry attempts ($maxAttempts).');
}
print('Transaction failed, trying again!');
}
}
} while (needsRetry);
}
void main(List<String> arguments) async {
var parser = ArgParser()
..addOption('auth-token',
abbr: 'a',
help: 'path to a file containing the gcloud auth token (required)')
..addOption('results',
abbr: 'r',
help: 'path to a file containing the test results (required)')
..addFlag('staging', abbr: 's', help: 'use staging database');
var options = parser.parse(arguments);
if (options.rest.isNotEmpty ||
options.option('results') == null ||
options.option('auth-token') == null) {
print(parser.usage);
exit(1);
}
var results = await loadResultsMap(options.option('results')!);
if (results.isEmpty) {
print("No test results provided, nothing to update.");
return;
}
// Pick an arbitrary result entry to find configuration and commit hash.
var firstResult = Result.fromMap(results.values.first);
var commit = firstResult.commitHash!;
var configuration = firstResult.configuration;
var project = options.flag('staging') ? 'dart-ci-staging' : 'dart-ci';
database = FirestoreDatabase(
project, await readGcloudAuthToken(options.option('auth-token')!));
await updateBlameLists(configuration, commit, results);
database.closeClient();
}