| // 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_functions_interop/firebase_functions_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>> getLastCommit(String hash) 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<void> storeChange( |
| 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(); |
| |
| // Find an existing change group with a blamelist that intersects this change. |
| |
| 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; |
| } |
| |
| DocumentSnapshot group = snapshot.documents |
| .firstWhere(blamelistIncludesChange, orElse: () => null); |
| |
| if (group == null) { |
| info("Adding group for $name"); |
| return firestore.collection('results').add(DocumentData.fromMap({ |
| 'name': name, |
| 'result': result, |
| 'previous_result': previousResult, |
| 'expected': change['expected'], |
| 'blamelist_start_index': startIndex, |
| 'blamelist_end_index': endIndex, |
| 'trivial_blamelist': startIndex == endIndex, |
| 'configurations': <String>[change['configuration']] |
| })); |
| } |
| |
| // Update the change group in a transaction. |
| // Add new configuration and narrow the blamelist. |
| Future<void> updateGroup(Transaction transaction) async { |
| final DocumentSnapshot groupSnapshot = |
| await transaction.get(group.reference); |
| final data = groupSnapshot.data; |
| final newStart = max(startIndex, data.getInt('blamelist_start_index')); |
| final newEnd = min(endIndex, data.getInt('blamelist_end_index')); |
| final updateMap = <String, dynamic>{ |
| 'blamelist_start_index': |
| max(startIndex, data.getInt('blamelist_start_index')), |
| 'blamelist_end_index': min(endIndex, data.getInt('blamelist_end_index')) |
| }; |
| updateMap['trivial_blamelist'] = (updateMap['blamelist_start_index'] == |
| updateMap['blamelist_end_index']); |
| final update = UpdateData.fromMap({ |
| 'blamelist_start_index': newStart, |
| 'blamelist_end_index': newEnd, |
| 'trivial_blamelist': newStart == newEnd |
| }); |
| update.setFieldValue('configurations', |
| Firestore.fieldValues.arrayUnion([change['configuration']])); |
| group.reference.updateData(update); |
| } |
| |
| return firestore.runTransaction(updateGroup); |
| } |
| |
| Future<void> storeTryChange( |
| Map<String, dynamic> change, int review, int patchset) async { |
| String name = change['name']; |
| String result = change['result']; |
| String expected = change['expected']; |
| String reviewPath = change['commit_hash']; |
| String previousResult = change['previous_result'] ?? 'new test'; |
| QuerySnapshot snapshot = await firestore |
| .collection('try_results') |
| .where('review_path', isEqualTo: reviewPath) |
| .where('name', isEqualTo: name) |
| .where('result', isEqualTo: result) |
| .where('previous_result', isEqualTo: previousResult) |
| .where('expected', isEqualTo: expected) |
| .limit(1) |
| .get(); |
| |
| if (snapshot.isEmpty) { |
| info("Adding group for $name"); |
| |
| return firestore.collection('try_results').add(DocumentData.fromMap({ |
| 'name': name, |
| 'result': result, |
| 'previous_result': previousResult, |
| 'expected': expected, |
| 'review_path': reviewPath, |
| 'review': review, |
| 'patchset': patchset, |
| 'configurations': <String>[change['configuration']] |
| })); |
| } else { |
| final update = UpdateData() |
| ..setFieldValue('configurations', |
| Firestore.fieldValues.arrayUnion([change['configuration']])); |
| snapshot.documents.first.reference.updateData(update); |
| } |
| } |
| |
| Future<void> storeReview(String review, Map<String, dynamic> data) => |
| firestore.document('reviews/$review').setData(DocumentData.fromMap(data)); |
| |
| Future<void> storePatchset( |
| String review, String patchset, Map<String, dynamic> data) => |
| firestore |
| .document('reviews/$review/patchsets/$patchset') |
| .setData(DocumentData.fromMap(data)); |
| } |