[cloud functions] Fix logical error in active_failure build status. Add test.

Change-Id: Id822b0d9fd97157f40c3480b05847d4cc85f7602

TBR: athom@google.com
Change-Id: Id822b0d9fd97157f40c3480b05847d4cc85f7602
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/129880
Reviewed-by: William Hesse <whesse@google.com>
diff --git a/functions/node/firestore.dart b/functions/node/firestore.dart
index 292ccc4..b054b9e 100644
--- a/functions/node/firestore.dart
+++ b/functions/node/firestore.dart
@@ -17,6 +17,8 @@
 
   Future<void> addCommit(String id, Map<String, dynamic> data);
 
+  Future<List<String>> getConfigurations(String builder);
+
   Future<void> updateConfiguration(String configuration, String builder);
 
   Future<void> updateBuildInfo(String builder, int buildNumber, int index);
diff --git a/functions/node/firestore_impl.dart b/functions/node/firestore_impl.dart
index 70f352e..8fed82a 100644
--- a/functions/node/firestore_impl.dart
+++ b/functions/node/firestore_impl.dart
@@ -59,6 +59,14 @@
     await docRef.setData(DocumentData.fromMap(data));
   }
 
+  Future<List<String>> getConfigurations(String builder) async {
+    QuerySnapshot snapshot = await firestore
+        .collection('configurations')
+        .where('builder', isEqualTo: builder)
+        .get();
+    return [for (final document in snapshot.documents) document.documentID];
+  }
+
   Future<void> updateConfiguration(String configuration, String builder) async {
     final record =
         await firestore.document('configurations/$configuration').get();
@@ -304,6 +312,25 @@
 
   Future<void> storeChunkStatus(String builder, int index, bool success) async {
     final document = firestore.document('builds/$builder:$index');
+    // Compute activeFailures outside transaction, because it runs queries.
+    // Because "completed" might be true inside transaction, but not now,
+    // we must compute activeFailures always, not just on last chunk.
+    var activeFailures = false;
+    final configurations = await getConfigurations(builder);
+    for (final configuration in configurations) {
+      // Find out if there are any unapproved unfixed failures,
+      // which we call "active" failures, to give sticky redness.
+      final snapshot = await firestore
+          .collection('results')
+          .where('active_configurations', arrayContains: configuration)
+          .where('approved', isEqualTo: false)
+          .limit(1)
+          .get();
+      if (snapshot.isNotEmpty) {
+        activeFailures = true;
+        break;
+      }
+    }
 
     Future<void> updateStatus(Transaction transaction) async {
       final snapshot = await transaction.get(document);
@@ -311,19 +338,6 @@
       final int chunks = data['num_chunks'];
       final int processedChunks = data['processed_chunks'] ?? 0;
       final bool completed = chunks == processedChunks + 1;
-      bool activeFailures = false;
-      if (completed) {
-        // Find out if there are any unapproved unfixed failures,
-        // which we call "active" failures, to give sticky redness.
-        final snapshot = await firestore
-            .collection('results')
-            .where('active_configurations', arrayContains: builder)
-            .where('approved', isEqualTo: false)
-            .limit(1)
-            .get();
-        activeFailures = snapshot.isNotEmpty;
-      }
-
       final update = UpdateData.fromMap({
         'processed_chunks': processedChunks + 1,
         'success': (data['success'] ?? true) && success,
diff --git a/functions/node/test/firestore_test_nodejs.dart b/functions/node/test/firestore_test_nodejs.dart
index a422287..1ff458b 100644
--- a/functions/node/test/firestore_test_nodejs.dart
+++ b/functions/node/test/firestore_test_nodejs.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:firebase_admin_interop/firebase_admin_interop.dart';
+import 'package:firebase_functions_interop/firebase_functions_interop.dart';
 import 'package:test/test.dart';
 
 import '../firestore_impl.dart' as fs;
@@ -21,8 +22,15 @@
   final firestore = fs.FirestoreServiceImpl();
 
   test('Test chunk storing', () async {
-    final builder = 'test_builder';
+    final builder = testBuilder;
+    final configuration = testConfiguration;
     final index = 123;
+
+    await firestore.updateConfiguration(configuration, builder);
+    final failingResultReference = await fs.firestore
+        .collection('results')
+        .add(DocumentData.fromMap(activeFailureResult));
+
     final document = fs.firestore.document('builds/$builder:$index');
 
     await document.delete();
@@ -47,6 +55,10 @@
     expect(data['num_chunks'], 4);
     expect(data['processed_chunks'], 4);
     expect(data['completed'], isTrue);
+    expect(data['active_failures'], isTrue);
+    await document.delete();
+    await failingResultReference.delete();
+    await fs.firestore.document('configurations/$testConfiguration').delete();
   });
 
   group('Try results', () {
diff --git a/functions/node/test/test_data.dart b/functions/node/test/test_data.dart
index eebb69f..9ab4e31 100644
--- a/functions/node/test/test_data.dart
+++ b/functions/node/test/test_data.dart
@@ -85,6 +85,20 @@
   "changed": true
 };
 
+const Map<String, dynamic> activeFailureResult = {
+  "name": "test_suite/active_failing_test",
+  "configurations": [testConfiguration, 'configuration 2', 'configuration 3'],
+  "active": true,
+  "active_configurations": [testConfiguration, 'configuration 2'],
+  "approved": false,
+  "suite": "test_suite",
+  "result": "RuntimeError",
+  "expected": "Pass",
+  "previous_result": "Pass",
+  "blamelist_start_index": 67195,
+  "blamelist_end_index": 67195
+};
+
 const List<Map<String, dynamic>> tryjobResults = [
   {
     "review": 44445,
@@ -114,6 +128,7 @@
 
 const testBuilder = 'test_builder';
 const testBuildNumber = "308";
+const testConfiguration = 'test_configuration';
 const testReview = 123;
 const testPatchset = 3;
 const testPreviousPatchset = 1;