blob: b6809aea6959753d317a4a9a928fe496540c3d83 [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 'dart:math' show max, min;
import 'package:firebase_admin_interop/firebase_admin_interop.dart';
import 'firestore.dart';
void info(Object message) {
print("Info: $message");
}
void error(Object message) {
print("Error: $message");
}
// Cloud functions run the cloud function many times in the same isolate.
// Use static initializer to run global initialization once.
Firestore firestore = createFirestore();
Firestore createFirestore() {
final app = FirebaseAdmin.instance.initializeApp();
return app.firestore()
..settings(FirestoreSettings(timestampsInSnapshots: true));
}
class FirestoreServiceImpl implements FirestoreService {
Future<bool> hasPatchset(String review, String patchset) => firestore
.document('reviews/$review/patchsets/$patchset')
.get()
.then((s) => s.exists);
Future<Map<String, dynamic>> getCommit(String hash) => firestore
.document('commits/$hash')
.get()
.then((s) => s.exists ? s.data.toMap() : null);
Future<Map<String, dynamic>> getCommitByIndex(int index) => firestore
.collection('commits')
.where('index', isEqualTo: index)
.get()
.then((s) => s.documents.first.data.toMap());
Future<Map<String, dynamic>> getLastCommit() async {
QuerySnapshot lastCommit = await firestore
.collection('commits')
.orderBy('index', descending: true)
.limit(1)
.get();
final result = lastCommit.documents.first.data.toMap();
result['id'] = lastCommit.documents.first.documentID;
return result;
}
Future<void> addCommit(String id, Map<String, dynamic> data) async {
data['created'] = Timestamp.fromDateTime(data['created']);
final docRef = firestore.document('commits/$id');
await docRef.setData(DocumentData.fromMap(data));
}
Future<void> updateConfiguration(String configuration, String builder) async {
final record =
await firestore.document('configurations/$configuration').get();
if (!record.exists || record.data.getString('builder') != builder) {
await firestore
.document('configurations/$configuration')
.setData(DocumentData.fromMap({'builder': builder}));
if (!record.exists) {
info('Configuration document $configuration -> $builder created');
} else {
info('Configuration document changed: $configuration -> $builder '
'(was ${record.data.getString("builder")}');
}
}
}
Future<void> updateBuildInfo(
String builder, int buildNumber, int index) async {
final documentRef = firestore.document('builds/$builder:$index');
final record = await documentRef.get();
if (!record.exists) {
await documentRef.setData(DocumentData.fromMap(
{'builder': builder, 'build_number': buildNumber, 'index': index}));
info('Created build record: '
'builder: $builder, build_number: $buildNumber, index: $index');
} else if (record.data.getInt('index') != index) {
error(
'Build $buildNumber of $builder had commit index ${record.data.getInt('index')},'
'should be $index.');
}
}
Future<String> findResult(
Map<String, dynamic> change, int startIndex, int endIndex) async {
String name = change['name'];
String result = change['result'];
String previousResult = change['previous_result'] ?? 'new test';
QuerySnapshot snapshot = await firestore
.collection('results')
.orderBy('blamelist_end_index', descending: true)
.where('name', isEqualTo: name)
.where('result', isEqualTo: result)
.where('previous_result', isEqualTo: previousResult)
.where('expected', isEqualTo: change['expected'])
.limit(5) // We will pick the right one, probably the latest.
.get();
bool blamelistIncludesChange(DocumentSnapshot groupDocument) {
var group = groupDocument.data;
var groupStart = group.getInt('blamelist_start_index');
var groupEnd = group.getInt('blamelist_end_index');
return startIndex <= groupEnd && endIndex >= groupStart;
}
return snapshot.documents
.firstWhere(blamelistIncludesChange, orElse: () => null)
?.documentID;
}
Future<void> storeResult(
Map<String, dynamic> change, int startIndex, int endIndex,
{bool approved = false}) =>
firestore.collection('results').add(DocumentData.fromMap({
'name': change['name'],
'result': change['result'],
'previous_result': change['previous_result'] ?? 'new test',
'expected': change['expected'],
'blamelist_start_index': startIndex,
'blamelist_end_index': endIndex,
'configurations': <String>[change['configuration']],
if (approved) 'approved': true
}));
Future<bool> updateResult(
String result, String configuration, int startIndex, int endIndex) {
DocumentReference reference = firestore.document('results/$result');
// Update the change group in a transaction.
Future<bool> updateGroup(Transaction transaction) =>
transaction.get(reference).then((resultSnapshot) {
final data = resultSnapshot.data;
bool approved = data.getBool('approved') ?? false;
// Add the new configuration and narrow the blamelist.
final newStart =
max(startIndex, data.getInt('blamelist_start_index'));
final newEnd = min(endIndex, data.getInt('blamelist_end_index'));
final update = UpdateData.fromMap({
'blamelist_start_index': newStart,
'blamelist_end_index': newEnd,
});
update.setFieldValue('configurations',
Firestore.fieldValues.arrayUnion([configuration]));
reference.updateData(update);
return approved;
});
return firestore.runTransaction(updateGroup);
}
Future<bool> storeTryChange(
Map<String, dynamic> change, int review, int patchset) async {
String name = change['name'];
String result = change['result'];
String expected = change['expected'];
String previousResult = change['previous_result'] ?? 'new test';
// Find an existing TryResult for this test on this patchset.
QuerySnapshot snapshot = await firestore
.collection('try_results')
.where('review', isEqualTo: review)
.where('patchset', isEqualTo: patchset)
.where('name', isEqualTo: name)
.where('result', isEqualTo: result)
.where('previous_result', isEqualTo: previousResult)
.where('expected', isEqualTo: expected)
.limit(1)
.get();
if (snapshot.isEmpty) {
// Is the previous result for this test on this review approved?
QuerySnapshot previous = await firestore
.collection('try_results')
.where('review', isEqualTo: review)
.where('name', isEqualTo: name)
.where('result', isEqualTo: result)
.where('previous_result', isEqualTo: previousResult)
.where('expected', isEqualTo: expected)
.orderBy('patchset', descending: true)
.limit(1)
.get();
// Create a TryResult for this test on this patchset.
final approved = previous.isNotEmpty &&
previous.documents.first.data.getBool('approved') == true;
await firestore.collection('try_results').add(DocumentData.fromMap({
'name': name,
'result': result,
'previous_result': previousResult,
'expected': expected,
'review': review,
'patchset': patchset,
'configurations': <String>[change['configuration']],
if (approved) 'approved': true
}));
return approved;
} else {
// Update the TryResult for this test, adding this configuration.
final update = UpdateData()
..setFieldValue('configurations',
Firestore.fieldValues.arrayUnion([change['configuration']]));
await snapshot.documents.first.reference.updateData(update);
// Return true if this result is approved
return snapshot.documents.first.data.getBool('approved') == true;
}
}
Future<void> storeReview(String review, Map<String, dynamic> data) =>
firestore.document('reviews/$review').setData(DocumentData.fromMap(data));
Future<void> storePatchset(
String review, int patchset, Map<String, dynamic> data) =>
firestore
.document('reviews/$review/patchsets/$patchset')
.setData(DocumentData.fromMap(data));
/// Returns true if a review record in the database has a landed_index field,
/// or if there is no record for the review in the database. Reviews with no
/// test failures have no record, and don't need to be linked when landing.
Future<bool> reviewIsLanded(int review) =>
firestore.document('reviews/$review').get().then((document) =>
!document.exists || document.data.getInt('landed_index') != null);
Future<void> linkReviewToCommit(int review, int index) => firestore
.document('reviews/$review')
.updateData(UpdateData.fromMap({'landed_index': index}));
Future<void> linkCommentsToCommit(int review, int index) async {
QuerySnapshot comments = await firestore
.collection('comments')
.where('review', isEqualTo: review)
.get();
if (comments.isEmpty) return;
final batch = firestore.batch();
for (final comment in comments.documents) {
batch.updateData(
comment.reference,
UpdateData.fromMap(
{'blamelist_start_index': index, 'blamelist_end_index': index}));
}
await batch.commit();
}
Future<List<Map<String, dynamic>>> tryApprovals(int review) async {
QuerySnapshot approvals = await firestore
.collection('try_results')
.where('approved', isEqualTo: true)
.where('review', isEqualTo: review)
.get();
return [for (final document in approvals.documents) document.data.toMap()];
}
Future<void> storeChunkStatus(String builder, int index, bool success) async {
final document = firestore.document('builds/$builder:$index');
Future<void> updateStatus(Transaction transaction) async {
final snapshot = await transaction.get(document);
final data = snapshot.data.toMap();
final int chunks = data['num_chunks'];
final int processedChunks = data['processed_chunks'] ?? 0;
final bool completed = chunks == processedChunks + 1;
final update = UpdateData.fromMap({
'processed_chunks': processedChunks + 1,
'success': (data['success'] ?? true) && success,
if (completed) 'completed': true
});
transaction.update(document, update);
}
return firestore.runTransaction(updateStatus);
}
Future<void> storeBuildChunkCount(
String builder, int index, int numChunks) async {
return firestore
.document('builds/$builder:$index')
.updateData(UpdateData.fromMap({'num_chunks': numChunks}));
}
Future<void> storeTryChunkStatus(
String builder, int review, int patchset, bool success) async {
final reference =
firestore.document('try_builds/$builder:$review:$patchset');
final snapshot = await reference.get();
if (!snapshot.exists || snapshot.data.getBool('completed') != null) {
await _createTryBuildRecord(builder, review, patchset);
}
Future<void> updateStatus(Transaction transaction) async {
final snapshot = await transaction.get(reference);
final data = snapshot.data.toMap();
final int chunks = data['num_chunks'];
final int processedChunks = data['processed_chunks'] ?? 0;
final bool completed = chunks == processedChunks + 1;
final update = UpdateData.fromMap({
'processed_chunks': processedChunks + 1,
'success': (data['success'] ?? true) && success,
if (completed) 'completed': true
});
transaction.update(reference, update);
}
return firestore.runTransaction(updateStatus);
}
Future<void> storeTryBuildChunkCount(
String builder, int review, int patchset, int numChunks) async {
final reference =
firestore.document('try_builds/$builder:$review:$patchset');
final snapshot = await reference.get();
if (!snapshot.exists || snapshot.data.getInt('num_chunks') != null) {
await _createTryBuildRecord(builder, review, patchset);
}
await reference.updateData(UpdateData.fromMap({'num_chunks': numChunks}));
}
Future<void> _createTryBuildRecord(
String builder, int review, int patchset) =>
firestore
.document('try_builds/$builder:$review:$patchset')
.setData(DocumentData.fromMap({
'builder': builder,
'review': review,
'patchset': patchset,
}));
}