[current results] Implement ListTests API call

Change-Id: If0a950f9182ad40f4b3ae9c4804ae4d92619e9f2
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/151244
Reviewed-by: Alexander Thomas <athom@google.com>
diff --git a/current_results/bin/client.dart b/current_results/bin/client.dart
index 1b85b0a..9b183a9 100644
--- a/current_results/bin/client.dart
+++ b/current_results/bin/client.dart
@@ -12,10 +12,11 @@
   final runner = CommandRunner<void>(
       'client.dart', 'Send gRPC requests to current results server')
     ..addCommand(QueryCommand())
+    ..addCommand(ListTestsCommand())
     ..addCommand(FetchCommand())
-    ..argParser.addOption('host', help: 'Current results server to query')
+    ..argParser.addOption('host', help: 'current results server to query')
     ..argParser
-        .addOption('port', abbr: 'p', help: 'Port of current results server');
+        .addOption('port', abbr: 'p', help: 'port of current results server');
   await runner.run(args);
 }
 
@@ -37,9 +38,9 @@
 class QueryCommand extends gRpcCommand {
   QueryCommand() {
     argParser.addMultiOption('name',
-        abbr: 'n', help: 'Test name or prefix to fetch results for');
+        abbr: 'n', help: 'test name or prefix to fetch results for');
     argParser.addMultiOption('configuration',
-        abbr: 'c', help: 'Configuration to fetch results for');
+        abbr: 'c', help: 'configuration to fetch results for');
   }
   String get name => 'getResults';
   String get description => 'Send a GetResults gRPC request to the server';
@@ -53,6 +54,27 @@
   }
 }
 
+class ListTestsCommand extends gRpcCommand {
+  ListTestsCommand() {
+    argParser.addOption('prefix',
+        defaultsTo: '', help: 'test name prefix to fetch test names for');
+    argParser.addOption('limit',
+        defaultsTo: '0',
+        help: 'number of test names starting with prefix to return');
+  }
+
+  String get name => 'listTests';
+  String get description => 'Send a ListTests gRPC request to the server';
+
+  Future<void> runWithChannel(ClientChannel channel) async {
+    final query = ListTestsRequest()
+      ..prefix = argResults['prefix']
+      ..limit = int.parse(argResults['limit']);
+    final result = await QueryClient(channel).listTests(query);
+    print(result.toProto3Json());
+  }
+}
+
 class FetchCommand extends gRpcCommand {
   String get name => 'fetch';
   String get description => 'Send a Fetch gRPC request to the server';
diff --git a/current_results/lib/src/api_impl.dart b/current_results/lib/src/api_impl.dart
index d411f7f..00d03b9 100644
--- a/current_results/lib/src/api_impl.dart
+++ b/current_results/lib/src/api_impl.dart
@@ -25,9 +25,8 @@
 
   @override
   Future<ListTestsResponse> listTests(
-      ServiceCall call, ListTestsRequest query) async {
-    throw UnimplementedError();
-  }
+          ServiceCall call, ListTestsRequest query) =>
+      Future.value(current.listTests(query));
 
   @override
   Future<ListTestsResponse> listTestPathCompletions(
diff --git a/current_results/lib/src/slice.dart b/current_results/lib/src/slice.dart
index 3c80b65..c910630 100644
--- a/current_results/lib/src/slice.dart
+++ b/current_results/lib/src/slice.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:convert';
+import 'dart:math';
 
 import 'package:collection/collection.dart';
 import 'package:grpc/grpc.dart';
@@ -125,4 +126,21 @@
     }
     return response;
   }
+
+  query_api.ListTestsResponse listTests(query_api.ListTestsRequest query) {
+    var limit = min(query.limit, 100000);
+    if (limit == 0) limit = 20;
+    final prefix = query.prefix;
+    final response = query_api.ListTestsResponse();
+    final start = lowerBound(testNames, prefix);
+    final end = min(start + limit, testNames.length);
+    for (final name in testNames.getRange(start, end)) {
+      if (name.startsWith(prefix)) {
+        response.names.add(name);
+      } else {
+        break;
+      }
+    }
+    return response;
+  }
 }