blob: 19a6459ca33c40ddcb1ba456c8a4aea361bb838e [file] [log] [blame]
// Copyright 2020 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:convert';
import 'dart:core';
import 'package:cocoon_service/src/foundation/utils.dart';
import 'package:cocoon_service/src/model/appengine/commit.dart';
import 'package:cocoon_service/src/model/appengine/task.dart';
import 'package:cocoon_service/src/model/ci_yaml/target.dart';
import 'package:cocoon_service/src/model/luci/buildbucket.dart';
import 'package:cocoon_service/src/model/luci/push_message.dart' as push_message;
import 'package:cocoon_service/src/request_handling/exceptions.dart';
import 'package:cocoon_service/src/service/NoBuildFoundException.dart';
import 'package:cocoon_service/src/service/config.dart';
import 'package:cocoon_service/src/service/datastore.dart';
import 'package:cocoon_service/src/service/logging.dart';
import 'package:cocoon_service/src/service/luci_build_service.dart';
import 'package:github/github.dart';
import 'package:cocoon_service/src/model/github/checks.dart' as cocoon_checks;
import 'package:logging/logging.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import '../src/datastore/fake_config.dart';
import '../src/request_handling/fake_pubsub.dart';
import '../src/service/fake_gerrit_service.dart';
import '../src/service/fake_github_service.dart';
import '../src/utilities/entity_generators.dart';
import '../src/utilities/mocks.dart';
import '../src/utilities/push_message.dart';
import '../src/utilities/webhook_generators.dart';
void main() {
late FakeConfig config;
FakeGithubService githubService;
late MockBuildBucketClient mockBuildBucketClient;
late LuciBuildService service;
late RepositorySlug slug;
final MockGithubChecksUtil mockGithubChecksUtil = MockGithubChecksUtil();
late FakePubSub pubsub;
final List<Target> targets = <Target>[
generateTarget(1, properties: <String, String>{'os': 'abc'}),
];
final PullRequest pullRequest = generatePullRequest(id: 1, repo: 'cocoon');
group('getBuilds', () {
final Build macBuild = generateBuild(999, name: 'Mac', status: Status.started);
final Build linuxBuild = generateBuild(998, name: 'Linux', bucket: 'try', status: Status.started);
setUp(() {
githubService = FakeGithubService();
config = FakeConfig(githubService: githubService);
mockBuildBucketClient = MockBuildBucketClient();
pubsub = FakePubSub();
service = LuciBuildService(
config: config,
buildBucketClient: mockBuildBucketClient,
gerritService: FakeGerritService(),
pubsub: pubsub,
);
slug = RepositorySlug('flutter', 'cocoon');
});
test('Null build', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[macBuild],
),
),
],
);
});
final Iterable<Build> builds = await service.getTryBuilds(
Config.flutterSlug,
'shasha',
'abcd',
);
expect(builds.first, macBuild);
});
test('Existing prod build', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[],
),
),
],
);
});
final Iterable<Build> builds = await service.getProdBuilds(
slug,
'commit123',
'abcd',
);
expect(builds, isEmpty);
});
test('Existing try build', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[linuxBuild],
),
),
],
);
});
final Iterable<Build> builds = await service.getTryBuilds(
Config.flutterSlug,
'shasha',
'abcd',
);
expect(builds.first, linuxBuild);
});
});
group('getBuilders', () {
setUp(() {
githubService = FakeGithubService();
config = FakeConfig(githubService: githubService);
mockBuildBucketClient = MockBuildBucketClient();
pubsub = FakePubSub();
service = LuciBuildService(
config: config,
buildBucketClient: mockBuildBucketClient,
gerritService: FakeGerritService(),
pubsub: pubsub,
);
slug = RepositorySlug('flutter', 'flutter');
});
test('with one rpc call', () async {
when(mockBuildBucketClient.listBuilders(any)).thenAnswer((_) async {
return const ListBuildersResponse(
builders: [
BuilderItem(id: BuilderId(bucket: 'prod', project: 'flutter', builder: 'test1')),
BuilderItem(id: BuilderId(bucket: 'prod', project: 'flutter', builder: 'test2')),
],
);
});
final Set<String> builders = await service.getAvailableBuilderSet();
expect(builders.length, 2);
expect(builders.contains('test1'), isTrue);
});
test('with more than one rpc calls', () async {
int retries = -1;
when(mockBuildBucketClient.listBuilders(any)).thenAnswer((_) async {
retries++;
if (retries == 0) {
return const ListBuildersResponse(
builders: [
BuilderItem(id: BuilderId(bucket: 'prod', project: 'flutter', builder: 'test1')),
BuilderItem(id: BuilderId(bucket: 'prod', project: 'flutter', builder: 'test2')),
],
nextPageToken: 'token',
);
} else if (retries == 1) {
return const ListBuildersResponse(
builders: [
BuilderItem(id: BuilderId(bucket: 'prod', project: 'flutter', builder: 'test3')),
BuilderItem(id: BuilderId(bucket: 'prod', project: 'flutter', builder: 'test4')),
],
);
} else {
return const ListBuildersResponse(builders: []);
}
});
final Set<String> builders = await service.getAvailableBuilderSet();
expect(builders.length, 4);
expect(builders, <String>{'test1', 'test2', 'test3', 'test4'});
});
});
group('buildsForRepositoryAndPr', () {
final Build macBuild = generateBuild(999, name: 'Mac', status: Status.started);
final Build linuxBuild = generateBuild(998, name: 'Linux', status: Status.started);
setUp(() {
githubService = FakeGithubService();
config = FakeConfig(githubService: githubService);
mockBuildBucketClient = MockBuildBucketClient();
pubsub = FakePubSub();
service = LuciBuildService(
config: config,
buildBucketClient: mockBuildBucketClient,
pubsub: pubsub,
);
slug = RepositorySlug('flutter', 'cocoon');
});
test('Empty responses are handled correctly', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[],
),
),
],
);
});
final Map<String?, Build?> builds = await service.tryBuildsForPullRequest(pullRequest);
expect(builds.keys, isEmpty);
});
test('Response returning a couple of builds', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[macBuild],
),
),
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[linuxBuild],
),
),
],
);
});
final Map<String?, Build?> builds = await service.tryBuildsForPullRequest(pullRequest);
expect(builds, equals(<String, Build>{'Mac': macBuild, 'Linux': linuxBuild}));
});
});
group('scheduleBuilds', () {
setUp(() {
githubService = FakeGithubService();
config = FakeConfig(githubService: githubService);
mockBuildBucketClient = MockBuildBucketClient();
pubsub = FakePubSub();
service = LuciBuildService(
config: config,
buildBucketClient: mockBuildBucketClient,
githubChecksUtil: mockGithubChecksUtil,
gerritService: FakeGerritService(branchesValue: <String>['master']),
pubsub: pubsub,
);
slug = RepositorySlug('flutter', 'cocoon');
});
test('schedule try builds successfully', () async {
final PullRequest pullRequest = generatePullRequest();
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return BatchResponse(
responses: <Response>[
Response(
scheduleBuild: generateBuild(1),
),
],
);
});
when(mockGithubChecksUtil.createCheckRun(any, any, any, any))
.thenAnswer((_) async => generateCheckRun(1, name: 'Linux 1'));
final List<Target> scheduledTargets = await service.scheduleTryBuilds(
pullRequest: pullRequest,
targets: targets,
);
final Iterable<String> scheduledTargetNames = scheduledTargets.map((Target target) => target.value.name);
expect(scheduledTargetNames, <String>['Linux 1']);
final BatchRequest batchRequest = pubsub.messages.single as BatchRequest;
expect(batchRequest.requests!.single.scheduleBuild, isNotNull);
final ScheduleBuildRequest scheduleBuild = batchRequest.requests!.single.scheduleBuild!;
expect(scheduleBuild.builderId.bucket, 'try');
expect(scheduleBuild.builderId.builder, 'Linux 1');
expect(scheduleBuild.notify?.pubsubTopic, 'projects/flutter-dashboard/topics/luci-builds');
final Map<String, dynamic> userData =
jsonDecode(String.fromCharCodes(base64Decode(scheduleBuild.notify!.userData!))) as Map<String, dynamic>;
expect(userData, <String, dynamic>{
'repo_owner': 'flutter',
'repo_name': 'flutter',
'user_agent': 'flutter-cocoon',
'check_run_id': 1,
'commit_sha': 'abc',
'commit_branch': 'master',
'builder_name': 'Linux 1'
});
final Map<String, dynamic> properties = scheduleBuild.properties!;
final List<RequestedDimension> dimensions = scheduleBuild.dimensions!;
expect(properties, <String, dynamic>{
'os': 'abc',
'dependencies': <dynamic>[],
'bringup': false,
'git_branch': 'master',
'git_url': 'https://github.com/flutter/flutter',
'git_ref': 'refs/pull/123/head',
'exe_cipd_version': 'refs/heads/main'
});
expect(dimensions.length, 1);
expect(dimensions[0].key, 'os');
expect(dimensions[0].value, 'abc');
});
test('try to schedule builds already started', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[
generateBuild(998, name: 'Linux 1', status: Status.started),
],
),
),
],
);
});
final List<LogRecord> records = <LogRecord>[];
log.onRecord.listen((LogRecord record) => records.add(record));
await service.scheduleTryBuilds(
pullRequest: pullRequest,
targets: targets,
);
expect(
records.where(
(LogRecord record) => record.message.contains('Linux 1 has already been scheduled for this pull request'),
),
hasLength(1),
);
});
test('try to schedule builds already passed', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[
generateBuild(998, name: 'Linux 1', status: Status.success),
],
),
),
],
);
});
final List<LogRecord> records = <LogRecord>[];
log.onRecord.listen((LogRecord record) => records.add(record));
await service.scheduleTryBuilds(
pullRequest: pullRequest,
targets: targets,
);
expect(
records.where(
(LogRecord record) => record.message.contains('Linux 1 has already been scheduled for this pull request'),
),
hasLength(1),
);
});
test('try to schedule builds already scheduled', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[
generateBuild(998, name: 'Linux 1', status: Status.scheduled),
],
),
),
],
);
});
final List<LogRecord> records = <LogRecord>[];
log.onRecord.listen((LogRecord record) => records.add(record));
await service.scheduleTryBuilds(
pullRequest: pullRequest,
targets: targets,
);
expect(records[0].message, 'Linux 1 has already been scheduled for this pull request');
});
test('Schedule builds throws when current list of targets is empty', () async {
when(mockGithubChecksUtil.createCheckRun(any, any, any, any)).thenAnswer((_) async {
return CheckRun.fromJson(const <String, dynamic>{
'id': 1,
'started_at': '2020-05-10T02:49:31Z',
'check_suite': <String, dynamic>{'id': 2}
});
});
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[],
);
});
await expectLater(
service.scheduleTryBuilds(
pullRequest: pullRequest,
targets: <Target>[],
),
throwsA(isA<InternalServerError>()),
);
});
test('Try to schedule build on a unsupported repo', () async {
expect(
() async => await service.scheduleTryBuilds(
targets: targets,
pullRequest: generatePullRequest(repo: 'nonsupported'),
),
throwsA(const TypeMatcher<BadRequestException>()),
);
});
});
group('schedulePostsubmitBuilds', () {
setUp(() {
mockBuildBucketClient = MockBuildBucketClient();
pubsub = FakePubSub();
service = LuciBuildService(
config: FakeConfig(),
buildBucketClient: mockBuildBucketClient,
githubChecksUtil: mockGithubChecksUtil,
pubsub: pubsub,
);
});
test('schedule postsubmit builds successfully', () async {
final Commit commit = generateCommit(0);
when(mockBuildBucketClient.listBuilders(any)).thenAnswer((_) async {
return const ListBuildersResponse(
builders: [
BuilderItem(id: BuilderId(bucket: 'prod', project: 'flutter', builder: 'Linux 1')),
],
);
});
final Tuple<Target, Task, int> toBeScheduled = Tuple<Target, Task, int>(
generateTarget(
1,
properties: <String, String>{
'os': 'debian-10.12',
},
),
generateTask(1),
LuciBuildService.kDefaultPriority,
);
await service.schedulePostsubmitBuilds(
commit: commit,
toBeScheduled: <Tuple<Target, Task, int>>[
toBeScheduled,
],
);
// Only one batch request should be published
expect(pubsub.messages.length, 1);
final BatchRequest request = pubsub.messages.first as BatchRequest;
expect(request.requests?.single.scheduleBuild, isNotNull);
final ScheduleBuildRequest scheduleBuild = request.requests!.single.scheduleBuild!;
expect(scheduleBuild.builderId.bucket, 'prod');
expect(scheduleBuild.builderId.builder, 'Linux 1');
expect(scheduleBuild.notify?.pubsubTopic, 'projects/flutter-dashboard/topics/luci-builds-prod');
final Map<String, dynamic> userData =
jsonDecode(String.fromCharCodes(base64Decode(scheduleBuild.notify!.userData!))) as Map<String, dynamic>;
expect(userData, <String, dynamic>{
'commit_key': 'flutter/flutter/master/1',
'task_key': '1',
});
final Map<String, dynamic> properties = scheduleBuild.properties!;
expect(properties, <String, dynamic>{
'dependencies': <dynamic>[],
'bringup': false,
'git_branch': 'master',
'exe_cipd_version': 'refs/heads/master',
'os': 'debian-10.12',
});
expect(scheduleBuild.exe, <String, String>{
'cipdVersion': 'refs/heads/master',
});
expect(scheduleBuild.dimensions, isNotEmpty);
expect(
scheduleBuild.dimensions!.singleWhere((RequestedDimension dimension) => dimension.key == 'os').value,
'debian-10.12',
);
});
test('schedule postsubmit builds with correct userData for checkRuns', () async {
when(mockGithubChecksUtil.createCheckRun(any, any, any, any))
.thenAnswer((_) async => generateCheckRun(1, name: 'Linux 1'));
final Commit commit = generateCommit(0, repo: 'packages');
when(mockBuildBucketClient.listBuilders(any)).thenAnswer((_) async {
return const ListBuildersResponse(
builders: [
BuilderItem(id: BuilderId(bucket: 'prod', project: 'flutter', builder: 'Linux 1')),
],
);
});
final Tuple<Target, Task, int> toBeScheduled = Tuple<Target, Task, int>(
generateTarget(
1,
properties: <String, String>{
'os': 'debian-10.12',
},
slug: RepositorySlug('flutter', 'packages'),
),
generateTask(1),
LuciBuildService.kDefaultPriority,
);
await service.schedulePostsubmitBuilds(
commit: commit,
toBeScheduled: <Tuple<Target, Task, int>>[
toBeScheduled,
],
);
// Only one batch request should be published
expect(pubsub.messages.length, 1);
final BatchRequest request = pubsub.messages.first as BatchRequest;
expect(request.requests?.single.scheduleBuild, isNotNull);
final ScheduleBuildRequest scheduleBuild = request.requests!.single.scheduleBuild!;
expect(scheduleBuild.builderId.bucket, 'prod');
expect(scheduleBuild.builderId.builder, 'Linux 1');
expect(scheduleBuild.notify?.pubsubTopic, 'projects/flutter-dashboard/topics/luci-builds-prod');
final Map<String, dynamic> userData =
jsonDecode(String.fromCharCodes(base64Decode(scheduleBuild.notify!.userData!))) as Map<String, dynamic>;
expect(userData, <String, dynamic>{
'commit_key': 'flutter/flutter/master/1',
'task_key': '1',
'check_run_id': 1,
'commit_sha': '0',
'commit_branch': 'master',
'builder_name': 'Linux 1',
'repo_owner': 'flutter',
'repo_name': 'packages'
});
});
test('reschedule using checkrun event fails gracefully', () async {
when(mockGithubChecksUtil.createCheckRun(any, any, any, any))
.thenAnswer((_) async => generateCheckRun(1, name: 'Linux 1'));
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[],
),
)
],
);
});
final pushMessage = generateCheckRunEvent(action: 'created', numberOfPullRequests: 1);
final Map<String, dynamic> jsonMap = json.decode(pushMessage.data!);
final Map<String, dynamic> jsonSubMap = json.decode(jsonMap['2']);
final cocoon_checks.CheckRunEvent checkRunEvent = cocoon_checks.CheckRunEvent.fromJson(jsonSubMap);
expect(
() async => await service.reschedulePostsubmitBuildUsingCheckRunEvent(
checkRunEvent,
commit: generateCommit(0),
task: generateTask(0),
target: generateTarget(0),
),
throwsA(const TypeMatcher<NoBuildFoundException>()),
);
});
test('do not create postsubmit checkrun for bringup: true target', () async {
when(mockGithubChecksUtil.createCheckRun(any, any, any, any))
.thenAnswer((_) async => generateCheckRun(1, name: 'Linux 1'));
final Commit commit = generateCommit(0, repo: 'packages');
when(mockBuildBucketClient.listBuilders(any)).thenAnswer((_) async {
return const ListBuildersResponse(
builders: [
BuilderItem(id: BuilderId(bucket: 'prod', project: 'flutter', builder: 'Linux 1')),
],
);
});
final Tuple<Target, Task, int> toBeScheduled = Tuple<Target, Task, int>(
generateTarget(
1,
properties: <String, String>{
'os': 'debian-10.12',
},
bringup: true,
slug: RepositorySlug('flutter', 'packages'),
),
generateTask(1),
LuciBuildService.kDefaultPriority,
);
await service.schedulePostsubmitBuilds(
commit: commit,
toBeScheduled: <Tuple<Target, Task, int>>[
toBeScheduled,
],
);
// Only one batch request should be published
expect(pubsub.messages.length, 1);
final BatchRequest request = pubsub.messages.first as BatchRequest;
expect(request.requests?.single.scheduleBuild, isNotNull);
final ScheduleBuildRequest scheduleBuild = request.requests!.single.scheduleBuild!;
expect(scheduleBuild.builderId.bucket, 'staging');
expect(scheduleBuild.builderId.builder, 'Linux 1');
expect(scheduleBuild.notify?.pubsubTopic, 'projects/flutter-dashboard/topics/luci-builds-prod');
final Map<String, dynamic> userData =
jsonDecode(String.fromCharCodes(base64Decode(scheduleBuild.notify!.userData!))) as Map<String, dynamic>;
// No check run related data.
expect(userData, <String, dynamic>{'commit_key': 'flutter/flutter/master/1', 'task_key': '1'});
});
test('Skip non-existing builder', () async {
final Commit commit = generateCommit(0);
when(mockBuildBucketClient.listBuilders(any)).thenAnswer((_) async {
return const ListBuildersResponse(
builders: [
BuilderItem(id: BuilderId(bucket: 'prod', project: 'flutter', builder: 'Linux 2')),
],
);
});
final Tuple<Target, Task, int> toBeScheduled1 = Tuple<Target, Task, int>(
generateTarget(
1,
properties: <String, String>{
'os': 'debian-10.12',
},
),
generateTask(1),
LuciBuildService.kDefaultPriority,
);
final Tuple<Target, Task, int> toBeScheduled2 = Tuple<Target, Task, int>(
generateTarget(
2,
properties: <String, String>{
'os': 'debian-10.12',
},
),
generateTask(1),
LuciBuildService.kDefaultPriority,
);
await service.schedulePostsubmitBuilds(
commit: commit,
toBeScheduled: <Tuple<Target, Task, int>>[
toBeScheduled1,
toBeScheduled2,
],
);
expect(pubsub.messages.length, 1);
final BatchRequest request = pubsub.messages.first as BatchRequest;
// Only existing builder: `Linux 2` is scheduled.
expect(request.requests?.length, 1);
expect(request.requests?.single.scheduleBuild, isNotNull);
final ScheduleBuildRequest scheduleBuild = request.requests!.single.scheduleBuild!;
expect(scheduleBuild.builderId.bucket, 'prod');
expect(scheduleBuild.builderId.builder, 'Linux 2');
});
});
group('cancelBuilds', () {
setUp(() {
config = FakeConfig();
mockBuildBucketClient = MockBuildBucketClient();
pubsub = FakePubSub();
service = LuciBuildService(
config: config,
buildBucketClient: mockBuildBucketClient,
pubsub: pubsub,
);
slug = RepositorySlug('flutter', 'cocoon');
});
test('Cancel builds when build list is empty', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[],
);
});
await service.cancelBuilds(pullRequest, 'new builds');
verify(mockBuildBucketClient.batch(any)).called(1);
});
test('Cancel builds that are scheduled', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[
generateBuild(998, name: 'Linux', status: Status.started),
],
),
)
],
);
});
await service.cancelBuilds(pullRequest, 'new builds');
expect(
verify(mockBuildBucketClient.batch(captureAny)).captured[1].requests[0].cancelBuild.toJson(),
json.decode('{"id": "998", "summaryMarkdown": "new builds"}'),
);
});
test('Cancel builds from unsuported repo', () async {
expect(
() async => await service.cancelBuilds(
generatePullRequest(repo: 'notsupported'),
'new builds',
),
throwsA(const TypeMatcher<BadRequestException>()),
);
});
});
group('failedBuilds', () {
setUp(() {
githubService = FakeGithubService();
config = FakeConfig(githubService: githubService);
mockBuildBucketClient = MockBuildBucketClient();
pubsub = FakePubSub();
service = LuciBuildService(
config: config,
buildBucketClient: mockBuildBucketClient,
pubsub: pubsub,
);
slug = RepositorySlug('flutter', 'flutter');
});
test('Failed builds from an empty list', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return const BatchResponse(
responses: <Response>[],
);
});
final List<Build?> result = await service.failedBuilds(pullRequest, <Target>[]);
expect(result, isEmpty);
});
test('Failed builds from a list of builds with failures', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
return BatchResponse(
responses: <Response>[
Response(
searchBuilds: SearchBuildsResponse(
builds: <Build>[
generateBuild(998, name: 'Linux 1', status: Status.failure),
],
),
)
],
);
});
final List<Build?> result = await service.failedBuilds(pullRequest, <Target>[generateTarget(1)]);
expect(result, hasLength(1));
});
});
group('rescheduleBuild', () {
late push_message.BuildPushMessage buildPushMessage;
setUp(() {
config = FakeConfig();
mockBuildBucketClient = MockBuildBucketClient();
pubsub = FakePubSub();
service = LuciBuildService(
config: config,
buildBucketClient: mockBuildBucketClient,
pubsub: pubsub,
);
final Map<String, dynamic> json = jsonDecode(
buildPushMessageString(
'COMPLETED',
result: 'FAILURE',
builderName: 'Linux Host Engine',
userData: '{}',
),
) as Map<String, dynamic>;
buildPushMessage = push_message.BuildPushMessage.fromJson(json);
});
test('Reschedule an existing build', () async {
when(mockBuildBucketClient.scheduleBuild(any)).thenAnswer((_) async => generateBuild(1));
final build = await service.rescheduleBuild(
commitSha: 'abc',
builderName: 'mybuild',
buildPushMessage: buildPushMessage,
);
expect(build.id, '1');
expect(build.status, Status.success);
verify(mockBuildBucketClient.scheduleBuild(any)).called(1);
});
});
group('checkRerunBuilder', () {
late Commit commit;
late Commit totCommit;
late DatastoreService datastore;
setUp(() {
config = FakeConfig();
mockBuildBucketClient = MockBuildBucketClient();
pubsub = FakePubSub();
service = LuciBuildService(
config: config,
buildBucketClient: mockBuildBucketClient,
pubsub: pubsub,
);
datastore = DatastoreService(config.db, 5);
});
test('Pass repo and properties correctly', () async {
totCommit = generateCommit(1, repo: 'engine', branch: 'main');
config.db.values[totCommit.key] = totCommit;
config.maxLuciTaskRetriesValue = 1;
final Task task = generateTask(
1,
status: Task.statusFailed,
parent: totCommit,
buildNumber: 1,
);
final Target target = generateTarget(1);
expect(task.attempts, 1);
expect(task.status, Task.statusFailed);
final bool rerunFlag = await service.checkRerunBuilder(
commit: totCommit,
task: task,
target: target,
datastore: datastore,
);
expect(pubsub.messages.length, 1);
final ScheduleBuildRequest scheduleBuildRequest =
(pubsub.messages.single as BatchRequest).requests!.single.scheduleBuild!;
final Map<String, dynamic> properties = scheduleBuildRequest.properties!;
for (String key in Config.defaultProperties.keys) {
expect(properties.containsKey(key), true);
}
expect(scheduleBuildRequest.priority, LuciBuildService.kRerunPriority);
expect(scheduleBuildRequest.gitilesCommit?.project, 'mirrors/engine');
expect(rerunFlag, isTrue);
expect(task.attempts, 2);
expect(task.status, Task.statusInProgress);
});
test('Rerun a test failed builder', () async {
totCommit = generateCommit(1);
config.db.values[totCommit.key] = totCommit;
config.maxLuciTaskRetriesValue = 1;
final Task task = generateTask(
1,
status: Task.statusFailed,
parent: totCommit,
buildNumber: 1,
);
final Target target = generateTarget(1);
final bool rerunFlag = await service.checkRerunBuilder(
commit: totCommit,
task: task,
target: target,
datastore: datastore,
);
expect(rerunFlag, isTrue);
});
test('Rerun an infra failed builder', () async {
totCommit = generateCommit(1);
config.db.values[totCommit.key] = totCommit;
config.maxLuciTaskRetriesValue = 1;
final Task task = generateTask(
1,
status: Task.statusInfraFailure,
parent: totCommit,
buildNumber: 1,
);
final Target target = generateTarget(1);
final bool rerunFlag = await service.checkRerunBuilder(
commit: totCommit,
task: task,
target: target,
datastore: datastore,
);
expect(rerunFlag, isTrue);
});
test('Do not rerun a successful builder', () async {
totCommit = generateCommit(1);
config.db.values[totCommit.key] = totCommit;
config.maxLuciTaskRetriesValue = 1;
final Task task = generateTask(
1,
status: Task.statusSucceeded,
parent: totCommit,
buildNumber: 1,
);
final Target target = generateTarget(1);
final bool rerunFlag = await service.checkRerunBuilder(
commit: totCommit,
task: task,
target: target,
datastore: datastore,
);
expect(rerunFlag, isFalse);
});
test('Do not rerun a builder exceeding retry limit', () async {
totCommit = generateCommit(1);
config.db.values[totCommit.key] = totCommit;
config.maxLuciTaskRetriesValue = 1;
final Task task = generateTask(
1,
status: Task.statusInfraFailure,
parent: totCommit,
buildNumber: 1,
attempts: 2,
);
final Target target = generateTarget(1);
final bool rerunFlag = await service.checkRerunBuilder(
commit: totCommit,
task: task,
target: target,
datastore: datastore,
);
expect(rerunFlag, isFalse);
});
test('Do not rerun a builder when not tip of tree', () async {
totCommit = generateCommit(2, sha: 'def');
commit = generateCommit(1, sha: 'abc');
config.db.values[totCommit.key] = totCommit;
config.db.values[commit.key] = commit;
config.maxLuciTaskRetriesValue = 1;
final Task task = generateTask(
1,
status: Task.statusInfraFailure,
parent: commit,
buildNumber: 1,
);
final Target target = generateTarget(1);
final bool rerunFlag = await service.checkRerunBuilder(
commit: commit,
task: task,
target: target,
datastore: datastore,
);
expect(rerunFlag, isFalse);
});
});
}