Create /api/get-branches to support frontend listing branches (#682)
* create get-branches API
* put common scripts to utils
* comments udpate
diff --git a/app_dart/bin/server.dart b/app_dart/bin/server.dart
index b7798e2..081bf0c 100644
--- a/app_dart/bin/server.dart
+++ b/app_dart/bin/server.dart
@@ -69,6 +69,12 @@
delegate: GetStatus(config),
),
'/api/public/get-timeseries-history': GetTimeSeriesHistory(config),
+ '/api/public/get-branches': CacheRequestHandler<Body>(
+ cache: cache,
+ config: config,
+ delegate: GetBranches(config),
+ ttl: const Duration(minutes: 15),
+ ),
};
return await runAppEngine((HttpRequest request) async {
diff --git a/app_dart/lib/cocoon_service.dart b/app_dart/lib/cocoon_service.dart
index 0570a34..33f08b7 100644
--- a/app_dart/lib/cocoon_service.dart
+++ b/app_dart/lib/cocoon_service.dart
@@ -9,6 +9,7 @@
export 'src/request_handlers/create_agent.dart';
export 'src/request_handlers/get_authentication_status.dart';
export 'src/request_handlers/get_benchmarks.dart';
+export 'src/request_handlers/get_branches.dart';
export 'src/request_handlers/get_build_status.dart';
export 'src/request_handlers/get_log.dart';
export 'src/request_handlers/get_status.dart';
diff --git a/app_dart/lib/src/datastore/cocoon_config.dart b/app_dart/lib/src/datastore/cocoon_config.dart
index af7f234..d739201 100644
--- a/app_dart/lib/src/datastore/cocoon_config.dart
+++ b/app_dart/lib/src/datastore/cocoon_config.dart
@@ -126,6 +126,8 @@
String get cqLabelName => 'CQ+1';
+ RepositorySlug get flutterSlug => const RepositorySlug('flutter', 'flutter');
+
String get waitingForTreeToGoGreenLabelName => 'waiting for tree to go green';
Future<ServiceAccountInfo> get deviceLabServiceAccount async {
diff --git a/app_dart/lib/src/request_handlers/get_branches.dart b/app_dart/lib/src/request_handlers/get_branches.dart
new file mode 100644
index 0000000..2f01c2f
--- /dev/null
+++ b/app_dart/lib/src/request_handlers/get_branches.dart
@@ -0,0 +1,51 @@
+// Copyright 2020 The Chromium Authors. 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:async';
+
+import 'package:github/server.dart';
+import 'package:meta/meta.dart';
+
+import '../datastore/cocoon_config.dart';
+import '../foundation/providers.dart';
+import '../foundation/typedefs.dart';
+import '../request_handlers/utils.dart';
+import '../request_handling/body.dart';
+import '../request_handling/request_handler.dart';
+
+/// Queries GitHub for the list of all available branches on
+/// [config.flutterSlug] repo, and returns list of branches
+/// that match pre-defined branch regular expressions.
+@immutable
+class GetBranches extends RequestHandler<Body> {
+ const GetBranches(
+ Config config, {
+ @visibleForTesting
+ this.branchHttpClientProvider = Providers.freshHttpClient,
+ @visibleForTesting this.gitHubBackoffCalculator = twoSecondLinearBackoff,
+ }) : assert(branchHttpClientProvider != null),
+ assert(gitHubBackoffCalculator != null),
+ super(config: config);
+
+ final HttpClientProvider branchHttpClientProvider;
+ final GitHubBackoffCalculator gitHubBackoffCalculator;
+
+ @override
+ Future<Body> get() async {
+ final GitHub github = await config.createGitHubClient();
+ final RepositorySlug slug = config.flutterSlug;
+ final Stream<Branch> branchList = github.repositories.listBranches(slug);
+ final List<String> regExps = await loadBranchRegExps(
+ branchHttpClientProvider, log, gitHubBackoffCalculator);
+ final List<String> branches = <String>[];
+
+ await for (Branch branch in branchList) {
+ if (regExps
+ .any((String regExp) => RegExp(regExp).hasMatch(branch.name))) {
+ branches.add(branch.name);
+ }
+ }
+ return Body.forJson(<String, List<String>>{'Branches': branches});
+ }
+}
diff --git a/app_dart/lib/src/request_handlers/refresh_github_commits.dart b/app_dart/lib/src/request_handlers/refresh_github_commits.dart
index 8119d26..8dd2a22 100644
--- a/app_dart/lib/src/request_handlers/refresh_github_commits.dart
+++ b/app_dart/lib/src/request_handlers/refresh_github_commits.dart
@@ -18,6 +18,7 @@
import '../model/appengine/commit.dart';
import '../model/appengine/task.dart';
import '../model/devicelab/manifest.dart';
+import '../request_handlers/utils.dart';
import '../request_handling/api_request_handler.dart';
import '../request_handling/authentication.dart';
import '../request_handling/body.dart';
@@ -25,20 +26,6 @@
import '../service/datastore.dart';
import '../service/github_service.dart';
-/// Signature for a function that calculates the backoff duration to wait in
-/// between requests when GitHub responds with an error.
-///
-/// The `attempt` argument is zero-based, so if the first attempt to request
-/// from GitHub fails, and we're backing off before making the second attempt,
-/// the `attempt` argument will be zero.
-typedef GitHubBackoffCalculator = Duration Function(int attempt);
-
-/// Default backoff calculator.
-@visibleForTesting
-Duration twoSecondLinearBackoff(int attempt) {
- return const Duration(seconds: 2) * (attempt + 1);
-}
-
/// Queries GitHub for the list of recent commits according to different branches,
/// and creates corresponding rows in the cloud datastore and the BigQuery for any commits
/// not yet there. Then creates new task rows in the datastore for any commits that
@@ -72,7 +59,8 @@
const RepositorySlug slug = RepositorySlug('flutter', 'flutter');
final Stream<Branch> branches = github.repositories.listBranches(slug);
final DatastoreService datastore = datastoreProvider();
- final List<String> regExps = await _loadBranchRegExps();
+ final List<String> regExps = await loadBranchRegExps(
+ branchHttpClientProvider, log, gitHubBackoffCalculator);
await for (Branch branch in branches) {
if (regExps
@@ -275,42 +263,4 @@
throw HttpStatusException(
HttpStatus.serviceUnavailable, 'GitHub not responding');
}
-
- Future<List<String>> _loadBranchRegExps() async {
- const String path =
- '/flutter/cocoon/master/app_dart/dev/branch_regexps.txt';
- final Uri url = Uri.https('raw.githubusercontent.com', path);
-
- final HttpClient client = branchHttpClientProvider();
- try {
- for (int attempt = 0; attempt < 3; attempt++) {
- final HttpClientRequest clientRequest = await client.getUrl(url);
-
- try {
- final HttpClientResponse clientResponse = await clientRequest.close();
- final int status = clientResponse.statusCode;
-
- if (status == HttpStatus.ok) {
- final String content =
- await utf8.decoder.bind(clientResponse).join();
- return content
- .split('\n')
- .map((String branch) => branch.trim())
- .toList();
- } else {
- log.warning(
- 'Attempt to download branch_regexps.txt failed (HTTP $status)');
- return <String>['master'];
- }
- } catch (error, stackTrace) {
- log.error(
- 'Attempt to download branch_regexps.txt failed:\n$error\n$stackTrace');
- }
- await Future<void>.delayed(gitHubBackoffCalculator(attempt));
- }
- } finally {
- client.close(force: true);
- }
- return <String>['master'];
- }
}
diff --git a/app_dart/lib/src/request_handlers/utils.dart b/app_dart/lib/src/request_handlers/utils.dart
new file mode 100644
index 0000000..0ff39f6
--- /dev/null
+++ b/app_dart/lib/src/request_handlers/utils.dart
@@ -0,0 +1,65 @@
+// Copyright 2020 The Chromium Authors. 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:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:appengine/appengine.dart';
+
+import '../foundation/typedefs.dart';
+
+/// Signature for a function that calculates the backoff duration to wait in
+/// between requests when GitHub responds with an error.
+///
+/// The [attempt] argument is zero-based, so if the first attempt to request
+/// from GitHub fails, and we're backing off before making the second attempt,
+/// the [attempt] argument will be zero.
+typedef GitHubBackoffCalculator = Duration Function(int attempt);
+
+/// Default backoff calculator.
+Duration twoSecondLinearBackoff(int attempt) {
+ return const Duration(seconds: 2) * (attempt + 1);
+}
+
+Future<List<String>> loadBranchRegExps(
+ HttpClientProvider branchHttpClientProvider,
+ Logging log,
+ GitHubBackoffCalculator gitHubBackoffCalculator) async {
+ const String path = '/flutter/cocoon/master/app_dart/dev/branch_regexps.txt';
+ final Uri url = Uri.https('raw.githubusercontent.com', path);
+
+ final HttpClient client = branchHttpClientProvider();
+ try {
+ for (int attempt = 0; attempt < 3; attempt++) {
+ final HttpClientRequest clientRequest = await client.getUrl(url);
+
+ try {
+ final HttpClientResponse clientResponse = await clientRequest.close();
+ final int status = clientResponse.statusCode;
+
+ if (status == HttpStatus.ok) {
+ final String content = await utf8.decoder.bind(clientResponse).join();
+ final List<String> branches = content
+ .split('\n')
+ .map((String branch) => branch.trim())
+ .toList();
+ branches.removeWhere((String branch) => branch.isEmpty);
+ return branches;
+ } else {
+ log.warning(
+ 'Attempt to download branch_regexps.txt failed (HTTP $status)');
+ }
+ } catch (error, stackTrace) {
+ log.error(
+ 'Attempt to download branch_regexps.txt failed:\n$error\n$stackTrace');
+ }
+ await Future<void>.delayed(gitHubBackoffCalculator(attempt));
+ }
+ } finally {
+ client.close(force: true);
+ }
+ log.error('GitHub not responding; giving up');
+ return <String>['master'];
+}
diff --git a/app_dart/test/request_handlers/get_branches_test.dart b/app_dart/test/request_handlers/get_branches_test.dart
new file mode 100644
index 0000000..e9e5cb1
--- /dev/null
+++ b/app_dart/test/request_handlers/get_branches_test.dart
@@ -0,0 +1,79 @@
+// Copyright 2020 The Chromium Authors. 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:convert';
+
+import 'package:cocoon_service/src/request_handlers/get_branches.dart';
+import 'package:cocoon_service/src/request_handling/body.dart';
+import 'package:github/server.dart';
+import 'package:mockito/mockito.dart';
+import 'package:test/test.dart';
+
+import '../src/datastore/fake_cocoon_config.dart';
+import '../src/request_handling/fake_http.dart';
+import '../src/request_handling/request_handler_tester.dart';
+
+const String branchRegExp = '''
+ master
+ ^flutter-[0-9]+\.[0-9]+-candidate\.[0-9]+
+ ''';
+
+void main() {
+ group('GetBranches', () {
+ FakeConfig config;
+ FakeHttpClient branchHttpClient;
+ RequestHandlerTester tester;
+ GetBranches handler;
+ List<String> githubBranches;
+
+ Stream<Branch> branchStream() async* {
+ for (String branchName in githubBranches) {
+ final CommitDataUser author = CommitDataUser('a', 1, 'b');
+ final GitCommit gitCommit = GitCommit();
+ final CommitData commitData = CommitData('sha', gitCommit, 'test',
+ 'test', 'test', author, author, <Map<String, dynamic>>[]);
+ final Branch branch = Branch(branchName, commitData);
+ yield branch;
+ }
+ }
+
+ setUp(() {
+ final MockGitHub github = MockGitHub();
+ final MockRepositoriesService repositories = MockRepositoriesService();
+
+ const RepositorySlug slug = RepositorySlug('flutter', 'flutter');
+ config = FakeConfig(githubClient: github, flutterSlugValue: slug);
+ branchHttpClient = FakeHttpClient();
+ tester = RequestHandlerTester();
+ handler = GetBranches(
+ config,
+ branchHttpClientProvider: () => branchHttpClient,
+ gitHubBackoffCalculator: (int attempt) => Duration.zero,
+ );
+
+ when(github.repositories).thenReturn(repositories);
+ when(repositories.listBranches(slug)).thenAnswer((Invocation _) {
+ return branchStream();
+ });
+ });
+
+ test('returns branches matching regExps', () async {
+ githubBranches = <String>['flutter-1.1-candidate.1', 'master', 'test'];
+
+ branchHttpClient.request.response.body = branchRegExp;
+
+ final Body body = await tester.get(handler);
+ final Map<String, dynamic> result = await utf8.decoder
+ .bind(body.serialize())
+ .transform(json.decoder)
+ .single as Map<String, dynamic>;
+
+ expect(result['Branches'], <String>['flutter-1.1-candidate.1', 'master']);
+ });
+ });
+}
+
+class MockGitHub extends Mock implements GitHub {}
+
+class MockRepositoriesService extends Mock implements RepositoriesService {}
diff --git a/app_dart/test/request_handlers/refresh_github_commits_test.dart b/app_dart/test/request_handlers/refresh_github_commits_test.dart
index 8ddc1aa..d12212a 100644
--- a/app_dart/test/request_handlers/refresh_github_commits_test.dart
+++ b/app_dart/test/request_handlers/refresh_github_commits_test.dart
@@ -238,14 +238,6 @@
expect(tester.log.records.where(hasLevel(LogLevel.ERROR)), isNotEmpty);
});
});
- group('GitHubBackoffCalculator', () {
- test('twoSecondLinearBackoff', () {
- expect(twoSecondLinearBackoff(0), const Duration(seconds: 2));
- expect(twoSecondLinearBackoff(1), const Duration(seconds: 4));
- expect(twoSecondLinearBackoff(2), const Duration(seconds: 6));
- expect(twoSecondLinearBackoff(3), const Duration(seconds: 8));
- });
- });
}
String toSha(Commit commit) => commit.sha;
diff --git a/app_dart/test/request_handlers/utils_test.dart b/app_dart/test/request_handlers/utils_test.dart
new file mode 100644
index 0000000..541a33c
--- /dev/null
+++ b/app_dart/test/request_handlers/utils_test.dart
@@ -0,0 +1,79 @@
+// Copyright 2020 The Chromium Authors. 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:appengine/appengine.dart';
+import 'package:test/test.dart';
+
+import 'package:cocoon_service/src/request_handlers/utils.dart';
+
+import '../src/request_handling/fake_http.dart';
+import '../src/request_handling/fake_logging.dart';
+
+const String branchRegExp = '''
+ master
+ ^flutter-[0-9]+\.[0-9]+-candidate\.[0-9]+
+ ''';
+
+void main() {
+ group('GetBranches', () {
+ FakeHttpClient branchHttpClient;
+ FakeLogging log;
+
+ setUp(() {
+ branchHttpClient = FakeHttpClient();
+ log = FakeLogging();
+ });
+
+ test('returns branches matching regExps', () async {
+ branchHttpClient.request.response.body = branchRegExp;
+ final List<String> branches = await loadBranchRegExps(
+ () => branchHttpClient, log, (int attempt) => Duration.zero);
+ expect(branches.length, 2);
+ });
+
+ test('retries regExps download upon HTTP failure', () async {
+ int retry = 0;
+ branchHttpClient.onIssueRequest = (FakeHttpClientRequest request) {
+ request.response.statusCode =
+ retry == 0 ? HttpStatus.serviceUnavailable : HttpStatus.ok;
+ retry++;
+ };
+
+ branchHttpClient.request.response.body = branchRegExp;
+ final List<String> branches = await loadBranchRegExps(
+ () => branchHttpClient, log, (int attempt) => Duration.zero);
+ expect(retry, 2);
+ expect(branches,
+ <String>['master', '^flutter-[0-9]+.[0-9]+-candidate.[0-9]+']);
+ expect(log.records.where(hasLevel(LogLevel.WARNING)), isNotEmpty);
+ expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty);
+ });
+
+ test('gives up regExps download after 3 tries', () async {
+ int retry = 0;
+ branchHttpClient.onIssueRequest =
+ (FakeHttpClientRequest request) => retry++;
+ branchHttpClient.request.response.statusCode =
+ HttpStatus.serviceUnavailable;
+ branchHttpClient.request.response.body = branchRegExp;
+ final List<String> branches = await loadBranchRegExps(
+ () => branchHttpClient, log, (int attempt) => Duration.zero);
+ expect(branches, <String>['master']);
+ expect(retry, 3);
+ expect(log.records.where(hasLevel(LogLevel.WARNING)), isNotEmpty);
+ expect(log.records.where(hasLevel(LogLevel.ERROR)), isNotEmpty);
+ });
+ });
+
+ group('GitHubBackoffCalculator', () {
+ test('twoSecondLinearBackoff', () {
+ expect(twoSecondLinearBackoff(0), const Duration(seconds: 2));
+ expect(twoSecondLinearBackoff(1), const Duration(seconds: 4));
+ expect(twoSecondLinearBackoff(2), const Duration(seconds: 6));
+ expect(twoSecondLinearBackoff(3), const Duration(seconds: 8));
+ });
+ });
+}
diff --git a/app_dart/test/src/datastore/fake_cocoon_config.dart b/app_dart/test/src/datastore/fake_cocoon_config.dart
index daee7e7..f1a0b30 100644
--- a/app_dart/test/src/datastore/fake_cocoon_config.dart
+++ b/app_dart/test/src/datastore/fake_cocoon_config.dart
@@ -42,6 +42,7 @@
this.taskLogServiceAccountValue,
this.rollerAccountsValue,
this.luciTryInfraFailureRetriesValue,
+ this.flutterSlugValue,
FakeDatastoreDB dbValue,
}) : dbValue = dbValue ?? FakeDatastoreDB();
@@ -70,6 +71,7 @@
ServiceAccountCredentials taskLogServiceAccountValue;
Set<String> rollerAccountsValue;
int luciTryInfraFailureRetriesValue;
+ RepositorySlug flutterSlugValue;
@override
int get luciTryInfraFailureRetries => luciTryInfraFailureRetriesValue;
@@ -150,6 +152,9 @@
waitingForTreeToGoGreenLabelNameValue;
@override
+ RepositorySlug get flutterSlug => flutterSlugValue;
+
+ @override
Future<ServiceAccountCredentials> get taskLogServiceAccount async =>
taskLogServiceAccountValue;