[builder] Add library for firestore helpers, rename tests

Change-Id: I025b342ba9fd85ccd26a7f04d31502fb8b89d7f1
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/204161
Reviewed-by: William Hesse <whesse@google.com>
Commit-Queue: William Hesse <whesse@google.com>
diff --git a/builder/lib/src/firestore.dart b/builder/lib/src/firestore.dart
index 69ebe66..608f134 100644
--- a/builder/lib/src/firestore.dart
+++ b/builder/lib/src/firestore.dart
@@ -5,168 +5,14 @@
 import 'dart:convert';
 import 'dart:io';
 import 'dart:math' show max, min;
+
 import 'package:builder/src/result.dart';
 import 'package:googleapis/firestore/v1.dart';
 import 'package:http/http.dart' as http;
 
-class ResultRecord {
-  final Map<String, Value> fields;
+import 'firestore_helpers.dart';
 
-  ResultRecord(this.fields);
-
-  bool get approved => fields['approved'].booleanValue;
-
-  @override
-  String toString() => jsonEncode(fields);
-
-  int get blamelistEndIndex {
-    return int.parse(fields['blamelist_end_index'].integerValue);
-  }
-
-  bool containsActiveConfiguration(String configuration) {
-    for (final value in fields['active_configurations'].arrayValue.values) {
-      if (value.stringValue != null && value.stringValue == configuration) {
-        return true;
-      }
-    }
-    return false;
-  }
-}
-
-Map<String, Value> taggedMap(Map<String, dynamic> fields) {
-  return fields.map((key, value) => MapEntry(key, taggedValue(value)));
-}
-
-Value taggedValue(dynamic value) {
-  if (value is int) {
-    return Value()..integerValue = '$value';
-  } else if (value is String) {
-    return Value()..stringValue = value;
-  } else if (value is bool) {
-    return Value()..booleanValue = value;
-  } else if (value is DateTime) {
-    return Value()..timestampValue = value.toUtc().toIso8601String();
-  } else if (value is List) {
-    return Value()
-      ..arrayValue = (ArrayValue()
-        ..values = value.map((element) => taggedValue(element)).toList());
-  } else if (value == null) {
-    return Value()..nullValue = 'NULL_VALUE';
-  } else {
-    throw Exception('unsupported value type ${value.runtimeType}');
-  }
-}
-
-dynamic getValue(Value value) {
-  if (value.integerValue != null) {
-    return int.parse(value.integerValue);
-  } else if (value.stringValue != null) {
-    return value.stringValue;
-  } else if (value.booleanValue != null) {
-    return value.booleanValue;
-  } else if (value.arrayValue != null) {
-    return value.arrayValue.values.map(getValue).toList();
-  } else if (value.timestampValue != null) {
-    return DateTime.parse(value.timestampValue);
-  } else if (value.nullValue != null) {
-    return null;
-  }
-  throw Exception('unsupported value ${value.toJson()}');
-}
-
-/// Converts a map with normal Dart values to a map where the values are
-/// JSON representation of firestore API values. For example: `{'x': 3}` is
-/// translated to `{'x': {'integerValue': 3}}`.
-Map<String, Object> taggedJsonMap(Map<String, dynamic> fields) {
-  return fields.map((key, value) => MapEntry(key, taggedValue(value).toJson()));
-}
-
-Map<String, dynamic> untagMap(Map<String, Value> map) {
-  return map.map((key, value) => MapEntry(key, getValue(value)));
-}
-
-List<CollectionSelector> inCollection(String name) {
-  return [CollectionSelector()..collectionId = name];
-}
-
-FieldReference field(String name) {
-  return FieldReference()..fieldPath = name;
-}
-
-Order orderBy(String fieldName, bool ascending) {
-  return Order()
-    ..field = field(fieldName)
-    ..direction = ascending ? 'ASCENDING' : 'DESCENDING';
-}
-
-Filter fieldEquals(String fieldName, dynamic value) {
-  return Filter()
-    ..fieldFilter = (FieldFilter()
-      ..field = field(fieldName)
-      ..op = 'EQUAL'
-      ..value = taggedValue(value));
-}
-
-Filter fieldLessThanOrEqual(String fieldName, dynamic value) {
-  return Filter()
-    ..fieldFilter = (FieldFilter()
-      ..field = field(fieldName)
-      ..op = 'LESS_THAN_OR_EQUAL'
-      ..value = taggedValue(value));
-}
-
-Filter fieldGreaterThanOrEqual(String fieldName, dynamic value) {
-  return Filter()
-    ..fieldFilter = (FieldFilter()
-      ..field = field(fieldName)
-      ..op = 'GREATER_THAN_OR_EQUAL'
-      ..value = taggedValue(value));
-}
-
-Filter arrayContains(String fieldName, dynamic value) {
-  return Filter()
-    ..fieldFilter = (FieldFilter()
-      ..field = field(fieldName)
-      ..op = 'ARRAY_CONTAINS'
-      ..value = taggedValue(value));
-}
-
-Filter compositeFilter(List<Filter> filters) {
-  return Filter()
-    ..compositeFilter = (CompositeFilter()
-      ..filters = filters
-      ..op = 'AND');
-}
-
-class DataWrapper {
-  final Map<String, Value> fields;
-  DataWrapper(Document document) : fields = document.fields;
-  DataWrapper.fields(this.fields);
-  int getInt(String name) {
-    final value = fields[name]?.integerValue;
-    if (value == null) {
-      return null;
-    }
-    return int.parse(value);
-  }
-
-  String getString(String name) {
-    return fields[name]?.stringValue;
-  }
-
-  bool getBool(String name) {
-    return fields[name]?.booleanValue;
-  }
-
-  List<dynamic> getList(String name) {
-    return fields[name]?.arrayValue?.values?.map(getValue)?.toList();
-  }
-
-  bool isNull(String name) {
-    return !fields.containsKey(name) ||
-        fields['name'].nullValue == 'NULL_VALUE';
-  }
-}
+export 'firestore_helpers.dart';
 
 class Commit {
   final DataWrapper wrapper;
diff --git a/builder/lib/src/firestore_helpers.dart b/builder/lib/src/firestore_helpers.dart
new file mode 100644
index 0000000..9d45900
--- /dev/null
+++ b/builder/lib/src/firestore_helpers.dart
@@ -0,0 +1,140 @@
+// Copyright (c) 2021, 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:googleapis/firestore/v1.dart';
+
+Map<String, Value> taggedMap(Map<String, dynamic> fields) {
+  return fields.map((key, value) => MapEntry(key, taggedValue(value)));
+}
+
+Value taggedValue(dynamic value) {
+  if (value is int) {
+    return Value()..integerValue = '$value';
+  } else if (value is String) {
+    return Value()..stringValue = value;
+  } else if (value is bool) {
+    return Value()..booleanValue = value;
+  } else if (value is DateTime) {
+    return Value()..timestampValue = value.toUtc().toIso8601String();
+  } else if (value is List) {
+    return Value()
+      ..arrayValue = (ArrayValue()
+        ..values = value.map((element) => taggedValue(element)).toList());
+  } else if (value == null) {
+    return Value()..nullValue = 'NULL_VALUE';
+  } else {
+    throw Exception('unsupported value type ${value.runtimeType}');
+  }
+}
+
+dynamic getValue(Value value) {
+  if (value.integerValue != null) {
+    return int.parse(value.integerValue);
+  } else if (value.stringValue != null) {
+    return value.stringValue;
+  } else if (value.booleanValue != null) {
+    return value.booleanValue;
+  } else if (value.arrayValue != null) {
+    return value.arrayValue.values.map(getValue).toList();
+  } else if (value.timestampValue != null) {
+    return DateTime.parse(value.timestampValue);
+  } else if (value.nullValue != null) {
+    return null;
+  }
+  throw Exception('unsupported value ${value.toJson()}');
+}
+
+/// Converts a map with normal Dart values to a map where the values are
+/// JSON representation of firestore API values. For example: `{'x': 3}` is
+/// translated to `{'x': {'integerValue': 3}}`.
+Map<String, Object> taggedJsonMap(Map<String, dynamic> fields) {
+  return fields.map((key, value) => MapEntry(key, taggedValue(value).toJson()));
+}
+
+Map<String, dynamic> untagMap(Map<String, Value> map) {
+  return map.map((key, value) => MapEntry(key, getValue(value)));
+}
+
+List<CollectionSelector> inCollection(String name) {
+  return [CollectionSelector()..collectionId = name];
+}
+
+FieldReference field(String name) {
+  return FieldReference()..fieldPath = name;
+}
+
+Order orderBy(String fieldName, bool ascending) {
+  return Order()
+    ..field = field(fieldName)
+    ..direction = ascending ? 'ASCENDING' : 'DESCENDING';
+}
+
+Filter fieldEquals(String fieldName, dynamic value) {
+  return Filter()
+    ..fieldFilter = (FieldFilter()
+      ..field = field(fieldName)
+      ..op = 'EQUAL'
+      ..value = taggedValue(value));
+}
+
+Filter fieldLessThanOrEqual(String fieldName, dynamic value) {
+  return Filter()
+    ..fieldFilter = (FieldFilter()
+      ..field = field(fieldName)
+      ..op = 'LESS_THAN_OR_EQUAL'
+      ..value = taggedValue(value));
+}
+
+Filter fieldGreaterThanOrEqual(String fieldName, dynamic value) {
+  return Filter()
+    ..fieldFilter = (FieldFilter()
+      ..field = field(fieldName)
+      ..op = 'GREATER_THAN_OR_EQUAL'
+      ..value = taggedValue(value));
+}
+
+Filter arrayContains(String fieldName, dynamic value) {
+  return Filter()
+    ..fieldFilter = (FieldFilter()
+      ..field = field(fieldName)
+      ..op = 'ARRAY_CONTAINS'
+      ..value = taggedValue(value));
+}
+
+Filter compositeFilter(List<Filter> filters) {
+  return Filter()
+    ..compositeFilter = (CompositeFilter()
+      ..filters = filters
+      ..op = 'AND');
+}
+
+class DataWrapper {
+  final Map<String, Value> fields;
+  DataWrapper(Document document) : fields = document.fields;
+  DataWrapper.fields(this.fields);
+  int getInt(String name) {
+    final value = fields[name]?.integerValue;
+    if (value == null) {
+      return null;
+    }
+    return int.parse(value);
+  }
+
+  String getString(String name) {
+    return fields[name]?.stringValue;
+  }
+
+  bool getBool(String name) {
+    return fields[name]?.booleanValue;
+  }
+
+  List<dynamic> getList(String name) {
+    return fields[name]?.arrayValue?.values?.map(getValue)?.toList();
+  }
+
+  bool isNull(String name) {
+    return !fields.containsKey(name) ||
+        fields['name'].nullValue == 'NULL_VALUE';
+  }
+}
diff --git a/builder/lib/src/result.dart b/builder/lib/src/result.dart
index b1271e9..3344f69 100644
--- a/builder/lib/src/result.dart
+++ b/builder/lib/src/result.dart
@@ -5,10 +5,35 @@
 // Field names and helper functions for result documents and
 // commit documents from Firestore.
 
-// Field names of Result document fields
+import 'dart:convert' show jsonEncode;
 
 import 'package:googleapis/firestore/v1.dart' show Value;
 
+class ResultRecord {
+  final Map<String, Value> fields;
+
+  ResultRecord(this.fields);
+
+  bool get approved => fields['approved'].booleanValue;
+
+  @override
+  String toString() => jsonEncode(fields);
+
+  int get blamelistEndIndex {
+    return int.parse(fields['blamelist_end_index'].integerValue);
+  }
+
+  bool containsActiveConfiguration(String configuration) {
+    for (final value in fields['active_configurations'].arrayValue.values) {
+      if (value.stringValue != null && value.stringValue == configuration) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
+
+// Field names of Result document fields
 const fName = 'name';
 const fResult = 'result';
 const fPreviousResult = 'previous_result';
diff --git a/builder/test/commits_cache_test.dart b/builder/test/commits_cache_test.dart
new file mode 100644
index 0000000..1d4e5e3
--- /dev/null
+++ b/builder/test/commits_cache_test.dart
@@ -0,0 +1,107 @@
+// Copyright (c) 2020, 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:googleapis/firestore/v1.dart';
+import 'package:googleapis_auth/auth_io.dart';
+import 'package:http/http.dart' as http;
+import 'package:test/test.dart';
+
+import '../lib/src/firestore.dart' as fs;
+import '../lib/src/commits_cache.dart';
+
+// These tests read and write data from the Firestore database.
+// If they are run against the production database, they will not
+// write data to the database.
+
+void main() async {
+  final baseClient = http.Client();
+  final client = await clientViaApplicationDefaultCredentials(
+      scopes: ['https://www.googleapis.com/auth/cloud-platform'],
+      baseClient: baseClient);
+  final firestore = fs.FirestoreService(FirestoreApi(client), client);
+  // create commits cache
+  final commits = TestingCommitsCache(firestore, baseClient);
+  test('Test fetch first commit', () async {
+    Future<void> fetchAndTestCommit(Map<String, dynamic> commit) async {
+      final fetched = await commits.getCommit(commit['hash']);
+      final copied = Map.from(fetched.toJson())
+        ..remove('author')
+        ..['hash'] = commit['hash'];
+      expect(copied, commit);
+    }
+
+    Future<void> fetchAndTestCommitByIndex(Map<String, dynamic> commit) async {
+      final fetched = await commits.getCommitByIndex(commit['index']);
+      final copied = Map.from(fetched.toJson())
+        ..remove('author')
+        ..['hash'] = commit['hash'];
+      expect(copied, commit);
+    }
+
+    expect(commits.startIndex, isNull);
+    await fetchAndTestCommit(commit68900);
+    expect(commits.startIndex, 68900);
+    expect(commits.endIndex, 68900);
+    await fetchAndTestCommit(commit68900);
+    expect(commits.startIndex, 68900);
+    expect(commits.endIndex, 68900);
+    await fetchAndTestCommitByIndex(commit68910);
+    expect(commits.startIndex, 68900);
+    expect(commits.endIndex, 68910);
+    await fetchAndTestCommitByIndex(commit68905);
+    expect(commits.startIndex, 68900);
+    expect(commits.endIndex, 68910);
+    await fetchAndTestCommit(commit68905);
+    expect(commits.startIndex, 68900);
+    expect(commits.endIndex, 68910);
+    await fetchAndTestCommitByIndex(commit68890);
+    expect(commits.startIndex, 68890);
+    expect(commits.endIndex, 68910);
+    await fetchAndTestCommitByIndex(commit68889);
+    expect(commits.startIndex, 68889);
+    expect(commits.endIndex, 68910);
+  });
+  tearDownAll(() => baseClient.close());
+}
+
+final commit68889 = <String, dynamic>{
+  'review': 136974,
+  'title': '[Cleanup] Removes deprecated --gc_at_instance_allocation.',
+  'index': 68889,
+  'created': DateTime.parse('2020-02-26 15:00:26.000Z'),
+  'hash': '9c05fde96b62556944befd18ec834c56d6854fda'
+};
+
+final commit68890 = <String, dynamic>{
+  'review': 136854,
+  'title':
+      'Add analyzer run support to steamroller and minor QOL improvements.',
+  'index': 68890,
+  'created': DateTime.parse('2020-02-26 16:57:46.000Z'),
+  'hash': '31053a8c0180b663858aadce1ff6c0eefcf78623'
+};
+
+final commit68900 = <String, dynamic>{
+  'review': 137322,
+  'title': 'Remove unused SdkPatcher.',
+  'index': 68900,
+  'created': DateTime.parse('2020-02-26 20:20:31.000Z'),
+  'hash': '118d220bfa7dc0f065b441e4edd584c2b9c0edc8',
+};
+
+final commit68905 = <String, dynamic>{
+  'review': 137286,
+  'title': '[dart2js] switch bot to use hostaserts once again',
+  'index': 68905,
+  'created': DateTime.parse('2020-02-26 21:41:47.000Z'),
+  'hash': '5055c98beeacb3996c256e37148b4dc3561735ee'
+};
+
+final commit68910 = <String, dynamic>{
+  'review': 137424,
+  'title': 'corpus index updates',
+  'index': 68910,
+  'created': DateTime.parse('2020-02-26 23:19:11.000Z'),
+  'hash': '8fb0e62babb213c98f4051f544fc80527bcecc18',
+};
diff --git a/builder/test/test_gerrit.dart b/builder/test/gerrit_test.dart
similarity index 100%
rename from builder/test/test_gerrit.dart
rename to builder/test/gerrit_test.dart
diff --git a/builder/test/test.dart b/builder/test/results_test.dart
similarity index 100%
rename from builder/test/test.dart
rename to builder/test/results_test.dart
diff --git a/builder/test/test_revert.dart b/builder/test/revert_test.dart
similarity index 100%
rename from builder/test/test_revert.dart
rename to builder/test/revert_test.dart