blob: 3a468b5426cffad3a2960e262a08c82303878f3d [file] [log] [blame]
// Copyright (c) 2019, 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.
import 'package:collection/collection.dart';
import 'package:http/http.dart' as http show Client;
import 'package:pool/pool.dart';
import 'commits_cache.dart';
import 'firestore.dart';
import 'gerrit_change.dart';
import 'result.dart';
import 'status.dart';
class ChangeCounter {
static const maxReportedSuccesses = 1000;
static const maxReportedFailures = 1000;
int changes = 0;
int passes = 0;
int failures = 0;
int unapprovedFailures = 0;
int newFlakes = 0;
bool hasTruncatedChanges = false;
bool get hasTooManyFlakes => newFlakes >= 10;
bool get hasTooManyPassingChanges => passes > maxReportedSuccesses;
bool get hasTooManyFailingChanges => failures > maxReportedFailures;
void count(Map<String, dynamic> change) {
++changes;
change[fMatches] ? ++passes : ++failures;
if (change[fFlaky] && !change[fPreviousFlaky]) ++newFlakes;
}
bool isNotReported(Map<String, dynamic> change) {
if (change[fMatches] && hasTooManyPassingChanges ||
!change[fMatches] && hasTooManyFailingChanges) {
hasTruncatedChanges = true;
return true;
}
return false;
}
List<String> report() => [
if (changes > 0) 'Stored $changes changes',
if (hasTruncatedChanges) 'Did not store all results. Truncating.',
if (hasTooManyPassingChanges)
'Only $maxReportedSuccesses new passes stored',
if (hasTooManyFailingChanges)
'Only $maxReportedFailures new failures stored',
if (unapprovedFailures > 0)
'$unapprovedFailures unapproved failing tests found',
if (failures > 0) '$failures failing tests found',
if (passes > 0) '$passes passing tests found',
if (newFlakes > 0) '$newFlakes new flaky tests found',
];
}
class Tryjob {
final http.Client httpClient;
final FirestoreService firestore;
final CommitsCache commits;
final counter = ChangeCounter();
TryBuildInfo info;
final TestNameLock testNameLock = TestNameLock();
String baseRevision;
bool success = true;
late List<SafeDocument> landedResults;
Map<String, SafeDocument> lastLandedResultByName = {};
final String buildbucketID;
Tryjob(this.info, this.buildbucketID, this.baseRevision, this.commits,
this.firestore, this.httpClient);
void log(String string) => firestore.log(string);
Future<void> update() async {
await GerritInfo(info.review, info.patchset, firestore, httpClient)
.update();
}
bool isNotLandedResult(Map<String, dynamic> change) {
return change[fResult] !=
lastLandedResultByName[change[fName]]?.getString(fResult);
}
Future<BuildStatus> process(List<Map<String, dynamic>> results) async {
await update();
log('storing ${results.length} change(s)');
final resultsByConfiguration = groupBy<Map<String, dynamic>, String>(
results, (result) => result['configuration']);
for (final configuration in resultsByConfiguration.keys) {
if (info.previousCommitHash != null) {
landedResults = await fetchLandedResults(configuration);
// Map will contain the last result with each name.
lastLandedResultByName = {
for (final result in landedResults) result.getString(fName): result
};
}
final changes =
resultsByConfiguration[configuration]!.where(isNotLandedResult);
await Pool(30).forEach(changes, guardedStoreChange).drain();
}
if (counter.hasTooManyFlakes) {
success = false;
}
await firestore.recordTryBuild(
info, buildbucketID, success, counter.hasTruncatedChanges);
final status = BuildStatus()
..success = success
..truncatedResults = counter.hasTruncatedChanges;
final report = [
'Processed ${results.length} results from ${info.builderName} build ${info.buildNumber}',
'Tryjob on CL ${info.review} patchset ${info.patchset}',
if (!success) 'Found unapproved new failures',
...counter.report(),
'${firestore.documentsFetched} documents fetched',
'${firestore.documentsWritten} documents written',
];
log(report.join('\n'));
return status;
}
Future<void> guardedStoreChange(Map<String, dynamic> change) =>
testNameLock.guardedCall(storeChange, change);
Future<void> storeChange(Map<String, dynamic> change) async {
transformChange(change);
counter.count(change);
if (counter.isNotReported(change)) return;
final approved =
await firestore.storeTryChange(change, info.review, info.patchset);
if (!approved && isFailure(change)) {
counter.unapprovedFailures++;
success = false;
}
}
Future<List<SafeDocument>> fetchLandedResults(String configuration) async {
final resultsBase = await commits.getCommit(info.previousCommitHash!);
final rebaseBase = await commits.getCommit(baseRevision);
if (resultsBase.index > rebaseBase.index) {
print('Try build is rebased on $baseRevision, which is before '
'the commit ${info.previousCommitHash} with CI comparison results');
return [];
}
final reviews = [
for (var index = resultsBase.index + 1;
index <= rebaseBase.index;
++index)
(await commits.getCommitByIndex(index)).review
];
return [
for (final landedReview in reviews)
if (landedReview != null)
...await firestore.tryResults(landedReview, configuration)
];
}
}