[cloud functions] Track the active failures, which have not been fixed.

Change-Id: Id1fe532101c5aba2b7553c5290a36ff78e8296af
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/128582
Reviewed-by: Alexander Thomas <athom@google.com>
diff --git a/functions/node/builder.dart b/functions/node/builder.dart
index 849844d..6836fa4 100644
--- a/functions/node/builder.dart
+++ b/functions/node/builder.dart
@@ -206,12 +206,15 @@
 
   Future<void> storeChange(Map<String, dynamic> change) async {
     countChanges++;
+    final failure = !change['matches'];
     var approved;
     String result = await firestore.findResult(change, startIndex, endIndex);
+    Map<String, dynamic> activeResult =
+        await firestore.findActiveResult(change);
     if (result == null) {
       approved = tryApprovals.contains(testResult(change));
       await firestore.storeResult(change, startIndex, endIndex,
-          approved: approved);
+          approved: approved, failure: failure);
       if (approved) {
         countApprovalsCopied++;
         if (countApprovalsCopied <= 10)
@@ -220,9 +223,25 @@
       }
     } else {
       approved = await firestore.updateResult(
-          result, change['configuration'], startIndex, endIndex);
+          result, change['configuration'], startIndex, endIndex,
+          failure: failure);
     }
-    if (!approved && !change['matches']) success = false;
+    if (failure && !approved) success = false;
+
+    if (activeResult != null) {
+      // Log error message if any expected invariants are violated
+      if (activeResult['blamelist_end_index'] >= startIndex ||
+          !activeResult['active_configurations']
+              .contains(change['configuration'])) {
+        error('Unexpected active result when processing new change:\n'
+            'Active result: $activeResult\n\n'
+            'Change: $change\n\n'
+            'approved: $approved');
+      }
+      // Removes the configuration from the list of active configurations.
+      // Mark the active result inactive when we remove the last active config.
+      firestore.updateActiveResult(activeResult, change['configuration']);
+    }
   }
 }
 
diff --git a/functions/node/firestore.dart b/functions/node/firestore.dart
index 1d5d397..292ccc4 100644
--- a/functions/node/firestore.dart
+++ b/functions/node/firestore.dart
@@ -26,15 +26,22 @@
 
   Future<void> storeResult(
       Map<String, dynamic> change, int startIndex, int endIndex,
-      {bool approved = false});
+      {bool approved, bool failure});
 
   Future<bool> updateResult(
-      String result, String configuration, int startIndex, int endIndex);
+      String result, String configuration, int startIndex, int endIndex,
+      {bool failure});
 
   Future<bool> storeTryChange(
       Map<String, dynamic> change, int review, int patchset);
 
+  Future<void> updateActiveResult(
+      Map<String, dynamic> activeResult, String configuration);
+
+  Future<Map<String, dynamic>> findActiveResult(Map<String, dynamic> change);
+
   Future<void> storeReview(String review, Map<String, dynamic> data);
+
   Future<void> storePatchset(
       String review, int patchset, Map<String, dynamic> data);
 
diff --git a/functions/node/firestore_impl.dart b/functions/node/firestore_impl.dart
index 06f3420..9c24675 100644
--- a/functions/node/firestore_impl.dart
+++ b/functions/node/firestore_impl.dart
@@ -120,7 +120,7 @@
 
   Future<void> storeResult(
           Map<String, dynamic> change, int startIndex, int endIndex,
-          {bool approved = false}) =>
+          {bool approved, bool failure}) =>
       firestore.collection('results').add(DocumentData.fromMap({
             'name': change['name'],
             'result': change['result'],
@@ -129,11 +129,15 @@
             'blamelist_start_index': startIndex,
             'blamelist_end_index': endIndex,
             'configurations': <String>[change['configuration']],
-            if (approved) 'approved': true
+            if (approved) 'approved': true,
+            if (failure) 'active': true,
+            if (failure)
+              'active_configurations': <String>[change['configuration']]
           }));
 
   Future<bool> updateResult(
-      String result, String configuration, int startIndex, int endIndex) {
+      String result, String configuration, int startIndex, int endIndex,
+      {bool failure}) {
     DocumentReference reference = firestore.document('results/$result');
 
     // Update the change group in a transaction.
@@ -151,6 +155,11 @@
           });
           update.setFieldValue('configurations',
               Firestore.fieldValues.arrayUnion([configuration]));
+          if (failure) {
+            update.setBool('active', true);
+            update.setFieldValue('active_configurations',
+                Firestore.fieldValues.arrayUnion([configuration]));
+          }
           reference.updateData(update);
           return approved;
         });
@@ -212,6 +221,40 @@
     }
   }
 
+  Future<void> updateActiveResult(
+      Map<String, dynamic> activeResult, String configuration) async {
+    final updateData = UpdateData();
+    if (activeResult['configurations'].length > 1) {
+      updateData.setFieldValue(
+          'configurations', Firestore.fieldValues.arrayRemove([configuration]));
+    } else {
+      updateData.setFieldValue(
+          'active_configurations', Firestore.fieldValues.delete());
+      updateData.setFieldValue('active', Firestore.fieldValues.delete());
+    }
+    return firestore
+        .document('results/${activeResult['id']}')
+        .updateData(updateData);
+  }
+
+  Future<Map<String, dynamic>> findActiveResult(
+      Map<String, dynamic> change) async {
+    QuerySnapshot snapshot = await firestore
+        .collection('results')
+        .where('active_configurations', arrayContains: change['configuration'])
+        .where('active', isEqualTo: true)
+        .where('name', isEqualTo: change['name'])
+        .get();
+    if (snapshot.isEmpty) return null;
+    if (snapshot.documents.length > 1) {
+      error('Multiple active results for the same configuration and test\n'
+          'result 1: ${snapshot.documents[0].data.toMap()}\n'
+          'result 2: ${snapshot.documents[1].data.toMap()}\n');
+    }
+    final result = snapshot.documents.first;
+    return result.data.toMap()..['id'] = result.documentID;
+  }
+
   Future<void> storeReview(String review, Map<String, dynamic> data) =>
       firestore.document('reviews/$review').setData(DocumentData.fromMap(data));
 
diff --git a/functions/node/test/test.dart b/functions/node/test/test.dart
index 5dbb946..46d6768 100644
--- a/functions/node/test/test.dart
+++ b/functions/node/test/test.dart
@@ -112,6 +112,7 @@
       verify(firestore.getCommitByIndex(53)),
       verify(firestore.tryApprovals(77779))
     ]);
+    verifyNoMoreInteractions(firestore);
   });
 
   test("copy approvals from try results", () async {
@@ -143,7 +144,51 @@
       firestore.updateConfiguration(
           "dart2js-new-rti-linux-x64-d8", "dart2js-rti-linux-x64-d8"),
       firestore.findResult(any, 53, 54),
-      firestore.storeResult(any, 53, 54, approved: true)
+      firestore.storeResult(any, 53, 54,
+          approved: argThat(isTrue, named: 'approved'),
+          failure: argThat(isTrue, named: 'failure'))
+    ]);
+  });
+
+  test("update previous active result", () async {
+    final activeResult = {
+      'blamelist_end_index': 52,
+      'configurations': [
+        landedCommitChange['configuration'],
+        'another configuration'
+      ],
+      'active_configurations': [
+        landedCommitChange['configuration'],
+        'another configuration'
+      ],
+      'active': true
+    };
+
+    final firestore = FirestoreServiceMock();
+    final client = HttpClientMock();
+
+    when(firestore.findActiveResult(any))
+        .thenAnswer((_) => Future.value(activeResult));
+
+    final builder =
+        Build(landedCommitHash, landedCommitChange, null, firestore, client);
+    builder.startIndex = 53;
+    builder.endIndex = 54;
+
+    await builder.storeChange(landedCommitChange);
+
+    expect(builder.countApprovalsCopied, 0);
+    expect(builder.countChanges, 1);
+    expect(builder.success, false);
+    verifyZeroInteractions(client);
+    verifyInOrder([
+      firestore.findResult(any, 53, 54),
+      firestore.findActiveResult(landedCommitChange),
+      firestore.storeResult(any, 53, 54,
+          approved: argThat(isFalse, named: 'approved'),
+          failure: argThat(isTrue, named: 'failure')),
+      firestore.updateActiveResult(
+          activeResult, landedCommitChange['configuration'])
     ]);
   });
 }