blob: 682f182ccec8d257d297988e993a9631c7121948 [file]
// 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),
];
}
}