blob: e96971abcf3e18ddc460243be981dc325513bfca [file] [log] [blame]
// Copyright 2019 The Flutter 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:cocoon_service/src/model/appengine/commit.dart';
import 'package:cocoon_service/src/model/appengine/task.dart';
import 'package:cocoon_service/src/request_handlers/refresh_github_commits.dart';
import 'package:cocoon_service/src/request_handling/body.dart';
import 'package:cocoon_service/src/request_handling/exceptions.dart';
import 'package:cocoon_service/src/service/datastore.dart';
import 'package:gcloud/db.dart' as gcloud_db;
import 'package:gcloud/db.dart';
import 'package:googleapis/bigquery/v2.dart';
import 'package:github/github.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import '../src/datastore/fake_cocoon_config.dart';
import '../src/datastore/fake_datastore.dart';
import '../src/request_handling/api_request_handler_tester.dart';
import '../src/request_handling/fake_authentication.dart';
import '../src/request_handling/fake_http.dart';
import '../src/request_handling/fake_logging.dart';
import '../src/service/fake_github_service.dart';
import '../src/utilities/mocks.dart';
const String singleTaskManifestYaml = '''
tasks:
linux_test:
stage: devicelab
required_agent_capabilities: ["linux/android"]
''';
void main() {
group('RefreshGithubCommits', () {
FakeConfig config;
FakeAuthenticationProvider auth;
FakeDatastoreDB db;
FakeHttpClient httpClient;
FakeHttpClient branchHttpClient;
ApiRequestHandlerTester tester;
RefreshGithubCommits handler;
List<String> githubCommits;
int yieldedCommitCount;
List<RepositoryCommit> commitList(int lastCommitTimestampMills) {
List<RepositoryCommit> commits = <RepositoryCommit>[];
for (String sha in githubCommits) {
final User author = User()
..login = 'Username'
..avatarUrl = 'http://example.org/avatar.jpg';
final GitCommitUser committer = GitCommitUser(
'Username',
'Username@abc.com',
DateTime.fromMillisecondsSinceEpoch(int.parse(sha)));
final GitCommit gitCommit = GitCommit()..committer = committer;
commits.add(RepositoryCommit()
..sha = sha
..author = author
..commit = gitCommit);
}
if (lastCommitTimestampMills == 0) {
commits = commits.take(1).toList();
}
return commits;
}
Commit shaToCommit(String sha, String branch) {
return Commit(
key: db.emptyKey.append(Commit, id: 'flutter/flutter/$branch/$sha'),
sha: sha,
branch: branch,
timestamp: int.parse(sha));
}
setUp(() {
final MockRepositoriesService repositories = MockRepositoriesService();
final FakeGithubService githubService = FakeGithubService();
final MockTabledataResourceApi tabledataResourceApi =
MockTabledataResourceApi();
when(tabledataResourceApi.insertAll(any, any, any, any)).thenAnswer((_) {
return Future<TableDataInsertAllResponse>.value(null);
});
yieldedCommitCount = 0;
db = FakeDatastoreDB();
config = FakeConfig(
tabledataResourceApi: tabledataResourceApi,
githubService: githubService,
dbValue: db);
auth = FakeAuthenticationProvider();
httpClient = FakeHttpClient();
branchHttpClient = FakeHttpClient();
tester = ApiRequestHandlerTester();
handler = RefreshGithubCommits(
config,
auth,
datastoreProvider: (DatastoreDB db) => DatastoreService(config.db, 5),
httpClientProvider: () => httpClient,
branchHttpClientProvider: () => branchHttpClient,
gitHubBackoffCalculator: (int attempt) => Duration.zero,
);
githubService.listCommitsBranch = (String branch, int hours) {
return commitList(hours);
};
when(githubService.github.repositories).thenReturn(repositories);
});
test('succeeds when GitHub returns no commits', () async {
githubCommits = <String>[];
config.flutterBranchesValue = <String>['master'];
final Body body = await tester.get<Body>(handler);
expect(yieldedCommitCount, 0);
expect(db.values, isEmpty);
expect(await body.serialize().toList(), isEmpty);
expect(tester.log.records.where(hasLevel(LogLevel.WARNING)), isEmpty);
expect(tester.log.records.where(hasLevel(LogLevel.ERROR)), isEmpty);
});
test('checks branch property for commits', () async {
githubCommits = <String>['1'];
config.flutterBranchesValue = <String>[
'flutter-1.1-candidate.1',
'master'
];
expect(db.values.values.whereType<Commit>().length, 0);
httpClient.request.response.body = singleTaskManifestYaml;
await tester.get<Body>(handler);
final Commit commit = db.values.values.whereType<Commit>().first;
expect(db.values.values.whereType<Commit>().length, 2);
expect(commit.branch, 'flutter-1.1-candidate.1');
});
test('stops requesting GitHub commits when it finds an existing commit',
() async {
githubCommits = <String>['1', '2', '3', '4', '5', '6', '7', '8', '9'];
config.flutterBranchesValue = <String>['master'];
const List<String> dbCommits = <String>['3', '4', '5', '6'];
for (String sha in dbCommits) {
final Commit commit = shaToCommit(sha, 'master');
db.values[commit.key] = commit;
}
expect(db.values.values.whereType<Commit>().length, 4);
expect(db.values.values.whereType<Task>().length, 0);
httpClient.request.response.body = singleTaskManifestYaml;
final Body body = await tester.get<Body>(handler);
expect(db.values.values.whereType<Commit>().length, 6);
expect(db.values.values.whereType<Task>().length, 10);
expect(await body.serialize().toList(), isEmpty);
expect(tester.log.records.where(hasLevel(LogLevel.WARNING)), isEmpty);
expect(tester.log.records.where(hasLevel(LogLevel.ERROR)), isEmpty);
});
test('inserts the latest single commit if a new branch is found', () async {
githubCommits = <String>['1', '2', '3', '4', '5', '6', '7', '8', '9'];
config.flutterBranchesValue = <String>['flutter-0.0-candidate.0'];
expect(db.values.values.whereType<Commit>().length, 0);
httpClient.request.response.body = singleTaskManifestYaml;
await tester.get<Body>(handler);
expect(db.values.values.whereType<Commit>().length, 1);
});
test('skips commits for which transaction commit fails', () async {
githubCommits = <String>['2', '3', '4'];
config.flutterBranchesValue = <String>['master'];
/// This test is simulating an existing branch, which must already
/// have at least one commit in the datastore.
final Commit commit = shaToCommit('1', 'master');
db.values[commit.key] = commit;
db.onCommit =
(List<gcloud_db.Model> inserts, List<gcloud_db.Key> deletes) {
if (inserts
.whereType<Commit>()
.where((Commit commit) => commit.sha == '3')
.isNotEmpty) {
throw StateError('Commit failed');
}
};
httpClient.request.response.body = singleTaskManifestYaml;
final Body body = await tester.get<Body>(handler);
expect(db.values.values.whereType<Commit>().length, 3);
expect(db.values.values.whereType<Task>().length, 10);
expect(db.values.values.whereType<Commit>().map<String>(toSha),
<String>['1', '2', '4']);
expect(db.values.values.whereType<Commit>().map<int>(toTimestamp),
<int>[1, 2, 4]);
expect(await body.serialize().toList(), isEmpty);
expect(tester.log.records.where(hasLevel(LogLevel.WARNING)), isEmpty);
expect(tester.log.records.where(hasLevel(LogLevel.ERROR)), isNotEmpty);
});
test('retries manifest download upon HTTP failure', () async {
int retry = 0;
httpClient.onIssueRequest = (FakeHttpClientRequest request) {
request.response.statusCode =
retry == 0 ? HttpStatus.serviceUnavailable : HttpStatus.ok;
retry++;
};
githubCommits = <String>['1'];
config.flutterBranchesValue = <String>['master'];
httpClient.request.response.body = singleTaskManifestYaml;
final Body body = await tester.get<Body>(handler);
expect(retry, 2);
expect(db.values.values.whereType<Commit>().length, 1);
expect(db.values.values.whereType<Task>().length, 5);
expect(await body.serialize().toList(), isEmpty);
expect(tester.log.records.where(hasLevel(LogLevel.WARNING)), isNotEmpty);
expect(tester.log.records.where(hasLevel(LogLevel.ERROR)), isEmpty);
});
test('gives up manifest download after 3 tries', () async {
int retry = 0;
httpClient.onIssueRequest = (FakeHttpClientRequest request) => retry++;
githubCommits = <String>['1'];
config.flutterBranchesValue = <String>['master'];
httpClient.request.response.body = singleTaskManifestYaml;
httpClient.request.response.statusCode = HttpStatus.serviceUnavailable;
await expectLater(
tester.get<Body>(handler), throwsA(isA<HttpStatusException>()));
expect(retry, 3);
expect(db.values.values.whereType<Commit>(), isEmpty);
expect(db.values.values.whereType<Task>(), isEmpty);
expect(tester.log.records.where(hasLevel(LogLevel.WARNING)), isNotEmpty);
expect(tester.log.records.where(hasLevel(LogLevel.ERROR)), isNotEmpty);
});
});
}
String toSha(Commit commit) => commit.sha;
int toTimestamp(Commit commit) => commit.timestamp;