[current_results] Add directory based results bucket for testing

Change-Id: Ibded115093a2f815176f93e49fd3e3375ac66a8a
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/198000
Commit-Queue: Karl Klose <karlklose@google.com>
Reviewed-by: William Hesse <whesse@google.com>
diff --git a/current_results/bin/local_test_server.dart b/current_results/bin/local_test_server.dart
new file mode 100644
index 0000000..744b1e3
--- /dev/null
+++ b/current_results/bin/local_test_server.dart
@@ -0,0 +1,46 @@
+// 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 'dart:io';
+
+import 'package:current_results/src/bucket.dart' show ResultsBucket;
+
+import 'server.dart' show startServer;
+
+/// A [ResultsBucket] that reads a local directory instead of fetching the
+/// data from cloud storage.
+///
+/// This class does not read `latest` files; the expected layout is a
+/// directory with multiple subdirectories, each containing a file called
+/// 'results.json'.
+class DirectoryBasedBucket implements ResultsBucket {
+  final Directory base;
+
+  DirectoryBasedBucket(String path) : base = Directory(path);
+
+  @override
+  Future<List<String>> configurationDirectories() {
+    return base
+        .list()
+        .where((entity) => entity is Directory)
+        .map((entity) => entity.path)
+        .toList();
+  }
+
+  @override
+  Future<List<String>> latestResults(String configurationDirectory) {
+    return File('$configurationDirectory/results.json').readAsLines();
+  }
+
+  @override
+  Future<DateTime> latestResultsDate(String configurationDirectory) {
+    return Future.value(DateTime.now());
+  }
+}
+
+void main(List<String> args) async {
+  var resultsBucket = DirectoryBasedBucket(args.single);
+  var port = int.tryParse(Platform.environment['PORT'] ?? '8080');
+  await startServer(port, resultsBucket);
+}
diff --git a/current_results/bin/server.dart b/current_results/bin/server.dart
index c7e6273..7a977bc 100644
--- a/current_results/bin/server.dart
+++ b/current_results/bin/server.dart
@@ -4,6 +4,8 @@
 
 import 'dart:io';
 
+import 'package:gcloud/storage.dart';
+import 'package:googleapis_auth/auth_io.dart';
 import 'package:grpc/grpc.dart';
 import 'package:pool/pool.dart';
 
@@ -12,21 +14,27 @@
 import 'package:current_results/src/slice.dart';
 import 'package:current_results/src/notifications.dart';
 
-final current = Slice();
-final bucket = ResultsBucket();
-final notifications = BucketNotifications();
-final grpcServer = Server([QueryService(current, notifications, bucket)]);
-
-void main(List<String> args) async {
-  await bucket.initialize();
-  await notifications.initialize();
-  await loadData();
+void main() async {
+  final client = await clientViaApplicationDefaultCredentials(scopes: [
+    'https://www.googleapis.com/auth/devstorage.read_only',
+  ]);
+  final bucket = Storage(client, 'dart-ci').bucket('dart-test-results');
+  final resultsBucket = ResultsBucket(bucket);
   var port = int.tryParse(Platform.environment['PORT'] ?? '8080');
+  await startServer(port, resultsBucket);
+}
+
+Future<void> startServer(int port, ResultsBucket bucket) async {
+  final notifications = BucketNotifications();
+  await notifications.initialize();
+  final current = await loadData(bucket);
+  final grpcServer = Server([QueryService(current, notifications, bucket)]);
   await grpcServer.serve(port: port);
   print('Grpc serving on port ${grpcServer.port}');
 }
 
-Future<void> loadData() async {
+Future<Slice> loadData(ResultsBucket bucket) async {
+  final result = Slice();
   final configurationDirectories = await bucket.configurationDirectories();
   await Pool(10).forEach(configurationDirectories,
       (String configurationDirectory) async {
@@ -35,10 +43,11 @@
           await bucket.latestResultsDate(configurationDirectory);
       if (DateTime.now().difference(resultsDate) <= maximumAge) {
         final results = await bucket.latestResults(configurationDirectory);
-        current.add(results);
+        result.add(results);
       }
     } catch (e, stack) {
       print('Error loading configuration $configurationDirectory: $e\n$stack');
     }
   }).drain();
+  return result;
 }
diff --git a/current_results/lib/src/bucket.dart b/current_results/lib/src/bucket.dart
index eb56208..4cdff4c 100644
--- a/current_results/lib/src/bucket.dart
+++ b/current_results/lib/src/bucket.dart
@@ -6,40 +6,32 @@
 import 'dart:convert';
 
 import 'package:gcloud/storage.dart';
-import 'package:googleapis_auth/auth_io.dart';
 
 /// Fetches la(st )test results from the dart-test-results GCS bucket.
 class ResultsBucket {
-  AuthClient client;
-  Bucket bucket;
+  final Bucket _bucket;
 
-  Future<void> initialize() async {
-    client = await clientViaApplicationDefaultCredentials(scopes: [
-      'https://www.googleapis.com/auth/devstorage.read_only',
-    ]);
-    final storage = Storage(client, 'dart-ci');
-    bucket = storage.bucket('dart-test-results');
-  }
+  ResultsBucket(this._bucket);
 
-  Future<List<String>> configurationDirectories() => bucket
+  Future<List<String>> configurationDirectories() => _bucket
       .list(prefix: 'configuration/master/')
       .where((entry) => entry.isDirectory)
       .map((entry) => entry.name)
       .toList();
 
   Future<DateTime> latestResultsDate(String configurationDirectory) async {
-    final info = await bucket.info('${configurationDirectory}latest');
+    final info = await _bucket.info('${configurationDirectory}latest');
     return info.updated;
   }
 
   Future<List<String>> latestResults(String configurationDirectory) async {
     try {
-      final revision = await bucket
+      final revision = await _bucket
           .read('${configurationDirectory}latest')
           .transform(ascii.decoder)
           .transform(LineSplitter())
           .single;
-      final results = await bucket
+      final results = await _bucket
           .read('$configurationDirectory$revision/results.json')
           .transform(utf8.decoder)
           .transform(LineSplitter())