blob: 49ec9bdbdfcb91291a9a93ef8bfc0de18d1ea24e [file] [log] [blame]
// Copyright 2019 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 'package:cocoon_service/src/model/appengine/agent.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/reserve_task.dart';
import 'package:cocoon_service/src/request_handling/exceptions.dart';
import 'package:cocoon_service/src/service/access_token_provider.dart';
import 'package:cocoon_service/src/service/datastore.dart';
import 'package:googleapis_auth/auth.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import '../src/datastore/fake_cocoon_config.dart';
import '../src/request_handling/api_request_handler_tester.dart';
import '../src/request_handling/fake_authentication.dart';
void main() {
FakeConfig config;
MockTaskProvider taskProvider;
MockReservationProvider reservationProvider;
MockAccessTokenProvider accessTokenProvider;
Agent agent;
setUp(() {
config = FakeConfig();
taskProvider = MockTaskProvider();
reservationProvider = MockReservationProvider();
accessTokenProvider = MockAccessTokenProvider();
agent =
Agent(key: config.db.emptyKey.append(Agent, id: 'aid'), agentId: 'aid');
});
group('ReserveTask', () {
ApiRequestHandlerTester tester;
ReserveTask handler;
setUp(() {
tester = ApiRequestHandlerTester();
handler = ReserveTask(
config,
FakeAuthenticationProvider(),
taskProvider: taskProvider,
reservationProvider: reservationProvider,
accessTokenProvider: accessTokenProvider,
);
});
test('throws 400 if no agent in context or request', () {
tester.requestData = <String, dynamic>{};
expect(() => tester.post(handler), throwsA(isA<BadRequestException>()));
});
test('throws 400 if context and request disagree on agent id', () {
tester
..context = FakeAuthenticatedContext(agent: agent)
..requestData = <String, dynamic>{'AgentID': 'bar'};
expect(() => tester.post(handler), throwsA(isA<BadRequestException>()));
});
test('throws 400 if context has agent but request does not', () {
tester
..context = FakeAuthenticatedContext(agent: agent)
..requestData = <String, dynamic>{};
expect(() => tester.post(handler), throwsA(isA<BadRequestException>()));
});
group('when request is well-formed', () {
setUp(() {
tester
..context = FakeAuthenticatedContext(agent: agent)
..requestData = <String, dynamic>{'AgentID': 'aid'};
});
test('returns empty response if no task available', () async {
when(taskProvider.findNextTask(agent))
.thenAnswer((Invocation invocation) {
return Future<FullTask>.value(null);
});
final ReserveTaskResponse response = await tester.post(handler);
expect(response.task, isNull);
expect(response.commit, isNull);
expect(response.accessToken, isNull);
verify(taskProvider.findNextTask(agent)).called(1);
});
test('returns full response if task is available', () async {
final Task task = Task(name: 'foo_test');
final Commit commit = Commit(sha: 'abc');
when(taskProvider.findNextTask(agent))
.thenAnswer((Invocation invocation) {
return Future<FullTask>.value(FullTask(task, commit));
});
when(accessTokenProvider.createAccessToken(scopes: anyNamed('scopes')))
.thenAnswer((Invocation invocation) {
return Future<AccessToken>.value(
AccessToken('type', 'data', DateTime.utc(2019)));
});
final ReserveTaskResponse response = await tester.post(handler);
expect(response.task.name, 'foo_test');
expect(response.commit.sha, 'abc');
expect(response.accessToken.data, 'data');
verify(taskProvider.findNextTask(agent)).called(1);
verify(reservationProvider.secureReservation(task, 'aid')).called(1);
verify(accessTokenProvider.createAccessToken(
scopes: anyNamed('scopes')))
.called(1);
});
test('retries until reservation can be secured', () async {
final Task task = Task(name: 'foo_test');
final Commit commit = Commit(sha: 'abc');
when(taskProvider.findNextTask(agent))
.thenAnswer((Invocation invocation) {
return Future<FullTask>.value(FullTask(task, commit));
});
int reservationAttempt = 0;
when(reservationProvider.secureReservation(task, 'aid'))
.thenAnswer((Invocation invocation) {
if (reservationAttempt == 0) {
reservationAttempt += 1;
throw const ReservationLostException();
} else {
return Future<void>.value();
}
});
when(accessTokenProvider.createAccessToken(
scopes: anyNamed('scopes'),
)).thenAnswer((Invocation invocation) {
return Future<AccessToken>.value(
AccessToken('type', 'data', DateTime.utc(2019)));
});
final ReserveTaskResponse response = await tester.post(handler);
expect(response.task.name, 'foo_test');
expect(response.commit.sha, 'abc');
expect(response.accessToken.data, 'data');
verify(taskProvider.findNextTask(agent)).called(2);
verify(reservationProvider.secureReservation(task, 'aid')).called(2);
verify(accessTokenProvider.createAccessToken(
scopes: anyNamed('scopes')))
.called(1);
});
test('Looks up agent if not provided in the context', () async {
tester.context = FakeAuthenticatedContext();
config.db.values[agent.key] = agent;
when(taskProvider.findNextTask(agent))
.thenAnswer((Invocation invocation) {
return Future<FullTask>.value(null);
});
final ReserveTaskResponse response = await tester.post(handler);
expect(response.task, isNull);
expect(response.commit, isNull);
expect(response.accessToken, isNull);
verify(taskProvider.findNextTask(agent)).called(1);
});
});
});
group('TaskProvider', () {
int taskIdCounter;
Agent agent;
Commit commit;
TaskProvider taskProvider;
Task newTask() {
final String taskId = 'test_${taskIdCounter++}';
return Task(
key: commit.key.append(Task, id: taskId),
name: taskId,
status: Task.statusNew,
stageName: 'devicelab',
attempts: 0,
isFlaky: false,
requiredCapabilities: <String>['linux/android'],
);
}
setUp(() {
taskIdCounter = 1;
agent = Agent(agentId: 'aid', capabilities: <String>['linux/android']);
commit =
Commit(key: config.db.emptyKey.append(Commit, id: 'abc'), sha: 'abc');
taskProvider = TaskProvider(datastore: DatastoreService(db: config.db));
});
test('if no commits in query returns null', () async {
expect(await taskProvider.findNextTask(agent), isNull);
});
group('if commits in query', () {
void setTaskResults(List<Task> tasks) {
for (Task task in tasks) {
config.db.values[task.key] = task;
}
}
setUp(() {
config.db.values[commit.key] = commit;
});
test('throws if task has no required capabilities', () async {
setTaskResults(<Task>[
newTask()..requiredCapabilities.clear(),
]);
expect(taskProvider.findNextTask(agent),
throwsA(isA<InvalidTaskException>()));
});
test('returns available task', () async {
setTaskResults(<Task>[
newTask()..name = 'a',
]);
final FullTask result = await taskProvider.findNextTask(agent);
expect(result.task.name, 'a');
expect(result.commit, commit);
});
test('skips tasks where agent capabilities are insufficient', () async {
setTaskResults(<Task>[
newTask()..requiredCapabilities[0] = 'mac/ios',
]);
expect(await taskProvider.findNextTask(agent), isNull);
});
test('skips tasks that are not managed by devicelab', () async {
setTaskResults(<Task>[
newTask()..stageName = 'cirrus',
]);
expect(await taskProvider.findNextTask(agent), isNull);
});
test('only considers tasks with status "new"', () async {
setTaskResults(<Task>[
newTask()..status = Task.statusInProgress,
newTask()..status = Task.statusSucceeded,
newTask()..status = Task.statusFailed,
]);
expect(await taskProvider.findNextTask(agent), isNull);
});
test('picks the task with fewest attempts first', () async {
setTaskResults(<Task>[
newTask()
..name = 'c'
..attempts = 3,
newTask()
..name = 'a'
..attempts = 1,
newTask()
..name = 'b'
..attempts = 2,
]);
final FullTask result = await taskProvider.findNextTask(agent);
expect(result.task.name, 'a');
});
});
});
}
class MockTaskProvider extends Mock implements TaskProvider {}
class MockReservationProvider extends Mock implements ReservationProvider {}
class MockAccessTokenProvider extends Mock implements AccessTokenProvider {}