dart-sdk/tools/bots/update_blamelists.dart
William Hesse 022d92e391 [infra] Fix utility used when bisecting CI failures
The bisection capability for failing builds on the CI is not much
used, and it is failing when updating the build information in Firestore.

A simple error in the script is causing the current error, so
this CL fixes a simple error in tools/bots/update_blamelists.dart.

Bug: https://github.com/dart-lang/sdk/issues/51978
Change-Id: Iba71e6115427145d20458d9e56f4cfa73fa232c9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/302923
Auto-Submit: William Hesse <whesse@google.com>
Commit-Queue: Jonas Termansen <sortie@google.com>
Reviewed-by: Jonas Termansen <sortie@google.com>
2023-05-15 11:02:47 +00:00

185 lines
6.5 KiB
Dart

// 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 data;
ResultRecord(this.data);
Map field(String name) => data['fields'][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 {
Map document = await database.getDocument('commits', commit);
var index = document['fields']['index'];
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 documents = (await database.runQuery(query))
.where((result) => result['document'] != null)
.map((result) => result['document']['name']);
for (var documentPath in documents) {
database.beginTransaction();
var documentName = documentPath.split('/').last;
var result =
ResultRecord(await database.getDocument('results', documentName));
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);
}
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['results'] == null ||
options['auth-token'] == null) {
print(parser.usage);
exit(1);
}
var results = await loadResultsMap(options['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['staging'] ? 'dart-ci-staging' : 'dart-ci';
database = FirestoreDatabase(
project, await readGcloudAuthToken(options['auth-token']));
await updateBlameLists(configuration, commit, results);
database.closeClient();
}