dd5b9cd7d7
Change-Id: I1bd930c11e0463ba0de1938ba417b6664c8cdb28 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/376023 Reviewed-by: Nate Bosch <nbosch@google.com> Commit-Queue: Devon Carew <devoncarew@google.com> Reviewed-by: Alexander Thomas <athom@google.com>
187 lines
6.7 KiB
Dart
187 lines
6.7 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<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();
|
|
}
|