| // 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/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: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'; |
| |
| 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('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); |
| }); |
| }); |
| } |