| // Copyright 2022 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. |
| |
| // ignore_for_file: constant_identifier_names |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'package:auto_submit/service/config.dart'; |
| |
| import 'package:auto_submit/requests/check_pull_request.dart'; |
| import 'package:auto_submit/requests/check_pull_request_queries.dart'; |
| import 'package:auto_submit/service/log.dart'; |
| import 'package:github/github.dart'; |
| import 'package:googleapis/bigquery/v2.dart'; |
| import 'package:googleapis/pubsub/v1.dart' as pub; |
| import 'package:graphql/client.dart' hide Request, Response; |
| import 'package:logging/logging.dart'; |
| import 'package:mockito/mockito.dart'; |
| import 'package:test/test.dart'; |
| |
| import '../service/bigquery_test.dart'; |
| import '../src/service/fake_bigquery_service.dart'; |
| import './github_webhook_test_data.dart'; |
| import '../src/request_handling/fake_pubsub.dart'; |
| import '../src/request_handling/fake_authentication.dart'; |
| import '../src/service/fake_config.dart'; |
| import '../src/service/fake_github_service.dart'; |
| import '../src/service/fake_graphql_client.dart'; |
| import '../utilities/mocks.dart'; |
| import '../utilities/utils.dart' hide createQueryResult; |
| |
| const String oid = '6dcb09b5b57875f334f61aebed695e2e4193db5e'; |
| const String title = 'some_title'; |
| |
| void main() { |
| group('Check CheckPullRequest', () { |
| late CheckPullRequest checkPullRequest; |
| late FakeConfig config; |
| late FakeCronAuthProvider auth; |
| late FakeGraphQLClient githubGraphQLClient; |
| final FakeGithubService githubService = FakeGithubService(); |
| late MockJobsResource jobsResource; |
| late FakeBigqueryService bigqueryService; |
| late MockPullRequestsService pullRequests; |
| final MockGitHub gitHub = MockGitHub(); |
| late FakePubSub pubsub; |
| late PullRequestHelper flutterRequest; |
| late PullRequestHelper cocoonRequest; |
| late List<QueryOptions> expectedOptions; |
| late QueryOptions flutterOption; |
| late QueryOptions cocoonOption; |
| const String testTopic = 'test-topic'; |
| const String rollorAuthor = "engine-flutter-autoroll"; |
| const String labelName = "warning: land on red to fix tree breakage"; |
| const String cocoonRepo = 'cocoon'; |
| const String noAutosubmitLabel = 'no_autosubmit'; |
| |
| setUp(() { |
| githubGraphQLClient = FakeGraphQLClient(); |
| auth = FakeCronAuthProvider(); |
| pubsub = FakePubSub(); |
| expectedOptions = <QueryOptions>[]; |
| |
| githubGraphQLClient.mutateResultForOptions = (MutationOptions options) => createFakeQueryResult(); |
| |
| githubGraphQLClient.queryResultForOptions = (QueryOptions options) { |
| expect(options.variables['sOwner'], 'flutter'); |
| final String? repoName = options.variables['sName'] as String?; |
| if (repoName == 'flutter') { |
| return createQueryResult(flutterRequest); |
| } else if (repoName == 'cocoon') { |
| return createQueryResult(cocoonRequest); |
| } else { |
| fail('unexpected repo $repoName'); |
| } |
| }; |
| |
| flutterOption = QueryOptions( |
| document: pullRequestWithReviewsQuery, |
| fetchPolicy: FetchPolicy.noCache, |
| variables: const <String, dynamic>{ |
| 'sOwner': 'flutter', |
| 'sName': 'flutter', |
| 'sPrNumber': 0, |
| }, |
| ); |
| cocoonOption = QueryOptions( |
| document: pullRequestWithReviewsQuery, |
| fetchPolicy: FetchPolicy.noCache, |
| variables: const <String, dynamic>{ |
| 'sOwner': 'flutter', |
| 'sName': 'cocoon', |
| 'sPrNumber': 1, |
| }, |
| ); |
| |
| githubService.checkRunsData = checkRunsMock; |
| githubService.compareTwoCommitsData = compareTwoCommitsMock; |
| githubService.successMergeData = successMergeMock; |
| githubService.createCommentData = createCommentMock; |
| githubService.commitData = commitMock; |
| jobsResource = MockJobsResource(); |
| bigqueryService = FakeBigqueryService(jobsResource); |
| config = FakeConfig(githubService: githubService, githubGraphQLClient: githubGraphQLClient, githubClient: gitHub); |
| config.bigqueryService = bigqueryService; |
| pullRequests = MockPullRequestsService(); |
| when(gitHub.pullRequests).thenReturn(pullRequests); |
| when(pullRequests.get(any, any)).thenAnswer((_) async => PullRequest(number: 123, state: 'open')); |
| |
| when(jobsResource.query(captureAny, any)).thenAnswer((Invocation invocation) { |
| return Future<QueryResponse>.value( |
| QueryResponse.fromJson(jsonDecode(insertDeleteUpdateSuccessResponse) as Map<dynamic, dynamic>), |
| ); |
| }); |
| }); |
| |
| void verifyQueries(List<QueryOptions> expectedOptions) { |
| githubGraphQLClient.verifyQueries(expectedOptions); |
| } |
| |
| test('Multiple identical messages are processed once', () async { |
| final PullRequest pullRequest1 = generatePullRequest(prNumber: 0, repoName: cocoonRepo); |
| githubService.pullRequestData = pullRequest1; |
| for (int i = 0; i < 2; i++) { |
| unawaited(pubsub.publish('auto-submit-queue-sub', pullRequest1)); |
| } |
| |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| cocoonRequest = PullRequestHelper(prNumber: 0, lastCommitHash: oid); |
| await checkPullRequest.get(); |
| |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(pullRequest1.number!.toString(), pullRequest1.number!.toString()), |
| ), |
| ], |
| ); |
| expect(0, pubsub.messagesQueue.length); |
| }); |
| |
| test('Closed PRs are not processed', () async { |
| final PullRequest pullRequest1 = generatePullRequest(prNumber: 0, repoName: cocoonRepo, state: 'close'); |
| githubService.pullRequestData = pullRequest1; |
| when(pullRequests.get(any, any)).thenAnswer((_) async => PullRequest(number: 0, state: 'close')); |
| for (int i = 0; i < 2; i++) { |
| unawaited(pubsub.publish('auto-submit-queue-sub', pullRequest1)); |
| } |
| |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| cocoonRequest = PullRequestHelper(prNumber: 0, lastCommitHash: oid); |
| await checkPullRequest.get(); |
| |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[], |
| ); |
| expect(0, pubsub.messagesQueue.length); |
| }); |
| |
| test('Merge exception is handled correctly', () async { |
| final PullRequest pullRequest1 = generatePullRequest(prNumber: 0); |
| final PullRequest pullRequest2 = generatePullRequest(prNumber: 1, repoName: cocoonRepo); |
| githubService.pullRequestData = pullRequest1; |
| int errorIndex = 0; |
| final List<PullRequest> pullRequests = <PullRequest>[pullRequest1, pullRequest2]; |
| for (PullRequest pr in pullRequests) { |
| unawaited(pubsub.publish(testTopic, pr)); |
| } |
| |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| flutterRequest = PullRequestHelper( |
| prNumber: 0, |
| lastCommitHash: oid, |
| ); |
| cocoonRequest = PullRequestHelper(prNumber: 1, lastCommitHash: oid); |
| githubGraphQLClient.mutateResultForOptions = (_) { |
| if (errorIndex == 0) { |
| errorIndex++; |
| throw const GraphQLError(message: 'error'); |
| } |
| return createQueryResult(cocoonRequest); |
| }; |
| final List<LogRecord> records = <LogRecord>[]; |
| log.onRecord.listen((LogRecord record) => records.add(record)); |
| // this is the test. |
| await checkPullRequest.get(); |
| // every failure is now acknowledged from the queue. |
| expect(pubsub.messagesQueue.length, 0); |
| final List<LogRecord> errorLogs = records.where((LogRecord record) => record.level == Level.SEVERE).toList(); |
| expect(errorLogs.length, 1); |
| expect(errorLogs[0].message.contains('Failed to merge'), true); |
| pubsub.messagesQueue.clear(); |
| }); |
| |
| test('Merges PR with successful status and checks', () async { |
| final PullRequest pullRequest1 = generatePullRequest(prNumber: 0); |
| final PullRequest pullRequest2 = generatePullRequest(prNumber: 1, repoName: cocoonRepo); |
| githubService.pullRequestData = pullRequest1; |
| |
| final List<PullRequest> pullRequests = <PullRequest>[pullRequest1, pullRequest2]; |
| for (PullRequest pr in pullRequests) { |
| unawaited(pubsub.publish(testTopic, pr)); |
| } |
| |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| flutterRequest = PullRequestHelper(prNumber: 0, lastCommitHash: oid); |
| cocoonRequest = PullRequestHelper(prNumber: 1, lastCommitHash: oid); |
| |
| await checkPullRequest.get(); |
| expectedOptions.add(flutterOption); |
| expectedOptions.add(cocoonOption); |
| verifyQueries(expectedOptions); |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(pullRequest1.number!.toString(), pullRequest1.number!.toString()), |
| ), |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(pullRequest2.number!.toString(), pullRequest2.number!.toString()), |
| ), |
| ], |
| ); |
| assert(pubsub.messagesQueue.isEmpty); |
| }); |
| |
| test('Merges unapproved PR from autoroller', () async { |
| final PullRequest pullRequest = generatePullRequest(prNumber: 0, author: rollorAuthor); |
| githubService.pullRequestData = pullRequest; |
| unawaited(pubsub.publish(testTopic, pullRequest)); |
| |
| checkPullRequest = CheckPullRequest( |
| config: config, |
| pubsub: pubsub, |
| cronAuthProvider: auth, |
| approverProvider: (Config config) => MockApproverService(), |
| ); |
| |
| flutterRequest = PullRequestHelper( |
| prNumber: 0, |
| author: 'dependabot', |
| reviews: const <PullRequestReviewHelper>[], |
| lastCommitHash: oid, |
| ); |
| cocoonRequest = PullRequestHelper( |
| prNumber: 1, |
| author: 'dependabot', |
| reviews: const <PullRequestReviewHelper>[], |
| ); |
| |
| await checkPullRequest.get(); |
| expectedOptions.add(flutterOption); |
| verifyQueries(expectedOptions); |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(pullRequest.number!.toString(), pullRequest.number!.toString()), |
| ), |
| ], |
| ); |
| assert(pubsub.messagesQueue.isEmpty); |
| }); |
| |
| test('Merges PR with failed tree status if override tree status label is provided', () async { |
| PullRequest pullRequest = generatePullRequest(prNumber: 0, labelName: labelName); |
| githubService.pullRequestData = pullRequest; |
| unawaited(pubsub.publish(testTopic, pullRequest)); |
| |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| |
| flutterRequest = PullRequestHelper( |
| prNumber: 0, |
| lastCommitHash: oid, |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildFailure, |
| ], |
| ); |
| |
| await checkPullRequest.get(); |
| expectedOptions.add(flutterOption); |
| verifyQueries(expectedOptions); |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(pullRequest.number!.toString(), pullRequest.number!.toString()), |
| ), |
| ], |
| ); |
| assert(pubsub.messagesQueue.isEmpty); |
| }); |
| |
| test('Merges a clean revert PR with in progress tests', () async { |
| PullRequest pullRequest = generatePullRequest(prNumber: 0); |
| githubService.pullRequestData = pullRequest; |
| unawaited(pubsub.publish(testTopic, pullRequest)); |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| |
| flutterRequest = PullRequestHelper( |
| prNumber: 0, |
| lastCommitHash: oid, |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildSuccess, |
| ], |
| lastCommitMessage: 'Revert "This is a test PR" This reverts commit abc.', |
| ); |
| |
| await checkPullRequest.get(); |
| expectedOptions.add(flutterOption); |
| verifyQueries(expectedOptions); |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(pullRequest.number!.toString(), pullRequest.number!.toString()), |
| ), |
| ], |
| ); |
| assert(pubsub.messagesQueue.isEmpty); |
| }); |
| |
| test('Merges PR with successful checks on repo without tree status', () async { |
| PullRequest pullRequest = generatePullRequest(prNumber: 1, repoName: cocoonRepo); |
| githubService.pullRequestData = pullRequest; |
| unawaited(pubsub.publish(testTopic, pullRequest)); |
| |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| |
| cocoonRequest = PullRequestHelper( |
| lastCommitHash: oid, |
| lastCommitStatuses: const <StatusHelper>[], |
| ); |
| |
| await checkPullRequest.get(); |
| expectedOptions.add(cocoonOption); |
| verifyQueries(expectedOptions); |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables('0', pullRequest.number!.toString()), |
| ), |
| ], |
| ); |
| assert(pubsub.messagesQueue.isEmpty); |
| }); |
| |
| test('Merges PR with neutral status checkrun', () async { |
| PullRequest pullRequest1 = generatePullRequest(prNumber: 0); |
| PullRequest pullRequest2 = generatePullRequest(prNumber: 1, repoName: cocoonRepo); |
| githubService.pullRequestData = pullRequest1; |
| final List<PullRequest> pullRequests = <PullRequest>[pullRequest1, pullRequest2]; |
| for (PullRequest pr in pullRequests) { |
| unawaited(pubsub.publish(testTopic, pr)); |
| } |
| githubService.checkRunsData = neutralCheckRunsMock; |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| flutterRequest = PullRequestHelper(prNumber: 0, lastCommitHash: oid); |
| cocoonRequest = PullRequestHelper(prNumber: 1, lastCommitHash: oid); |
| |
| await checkPullRequest.get(); |
| expectedOptions.add(flutterOption); |
| expectedOptions.add(cocoonOption); |
| verifyQueries(expectedOptions); |
| assert(pubsub.messagesQueue.isEmpty); |
| }); |
| |
| test('Removes the label for the PR with failed tests', () async { |
| PullRequest pullRequest1 = generatePullRequest(prNumber: 0); |
| PullRequest pullRequest2 = generatePullRequest(prNumber: 1, repoName: cocoonRepo); |
| githubService.pullRequestData = pullRequest1; |
| final List<PullRequest> pullRequests = <PullRequest>[pullRequest1, pullRequest2]; |
| for (PullRequest pr in pullRequests) { |
| unawaited(pubsub.publish(testTopic, pr)); |
| } |
| githubService.checkRunsData = failedCheckRunsMock; |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| flutterRequest = PullRequestHelper(prNumber: 0, lastCommitHash: oid); |
| cocoonRequest = PullRequestHelper(prNumber: 1, lastCommitHash: oid); |
| |
| await checkPullRequest.get(); |
| expectedOptions.add(flutterOption); |
| expectedOptions.add(cocoonOption); |
| verifyQueries(expectedOptions); |
| assert(pubsub.messagesQueue.isEmpty); |
| }); |
| |
| test('Removes the label for the PR with failed status', () async { |
| PullRequest pullRequest = generatePullRequest(prNumber: 0); |
| githubService.pullRequestData = pullRequest; |
| unawaited(pubsub.publish(testTopic, pullRequest)); |
| |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| |
| flutterRequest = PullRequestHelper( |
| lastCommitHash: oid, |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildSuccess, |
| StatusHelper.otherStatusFailure, |
| ], |
| ); |
| |
| await checkPullRequest.get(); |
| expectedOptions.add(flutterOption); |
| verifyQueries(expectedOptions); |
| assert(pubsub.messagesQueue.isEmpty); |
| }); |
| |
| test('Removes the label if non member does not have at least 2 member reviews', () async { |
| PullRequest pullRequest = generatePullRequest(prNumber: 0, authorAssociation: ''); |
| githubService.pullRequestData = pullRequest; |
| unawaited(pubsub.publish(testTopic, pullRequest)); |
| |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| |
| flutterRequest = PullRequestHelper( |
| authorAssociation: '', |
| lastCommitHash: oid, |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildSuccess, |
| ], |
| ); |
| |
| await checkPullRequest.get(); |
| expectedOptions.add(flutterOption); |
| verifyQueries(expectedOptions); |
| assert(pubsub.messagesQueue.isEmpty); |
| }); |
| |
| test('Removes the label for the PR with null checks and statuses', () async { |
| PullRequest pullRequest = generatePullRequest(prNumber: 0); |
| githubService.pullRequestData = pullRequest; |
| unawaited(pubsub.publish(testTopic, pullRequest)); |
| |
| githubService.checkRunsData = emptyCheckRunsMock; |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| |
| flutterRequest = PullRequestHelper( |
| lastCommitHash: oid, |
| lastCommitStatuses: const <StatusHelper>[], |
| ); |
| |
| await checkPullRequest.get(); |
| expectedOptions.add(flutterOption); |
| verifyQueries(expectedOptions); |
| assert(pubsub.messagesQueue.isEmpty); |
| }); |
| |
| test('Does not merge PR with in progress checks', () async { |
| PullRequest pullRequest1 = generatePullRequest(prNumber: 0); |
| PullRequest pullRequest2 = generatePullRequest(prNumber: 1, repoName: cocoonRepo); |
| githubService.pullRequestData = pullRequest1; |
| final List<PullRequest> pullRequests = <PullRequest>[pullRequest1, pullRequest2]; |
| for (PullRequest pr in pullRequests) { |
| unawaited(pubsub.publish(testTopic, pr)); |
| } |
| githubService.checkRunsData = inProgressCheckRunsMock; |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| flutterRequest = PullRequestHelper(prNumber: 0); |
| cocoonRequest = PullRequestHelper(prNumber: 1); |
| await checkPullRequest.get(); |
| expectedOptions.add(flutterOption); |
| expectedOptions.add(cocoonOption); |
| verifyQueries(expectedOptions); |
| expect(pubsub.messagesQueue.length, 2); |
| pubsub.messagesQueue.clear(); |
| }); |
| |
| test('Does not merge PR if no autosubmit label any more', () async { |
| PullRequest pullRequest1 = generatePullRequest(prNumber: 0, autosubmitLabel: noAutosubmitLabel); |
| PullRequest pullRequest2 = |
| generatePullRequest(prNumber: 1, autosubmitLabel: noAutosubmitLabel, repoName: cocoonRepo); |
| githubService.pullRequestData = pullRequest1; |
| final List<PullRequest> pullRequests = <PullRequest>[pullRequest1, pullRequest2]; |
| for (PullRequest pr in pullRequests) { |
| unawaited(pubsub.publish(testTopic, pr)); |
| } |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| flutterRequest = PullRequestHelper(prNumber: 0); |
| cocoonRequest = PullRequestHelper(prNumber: 1); |
| await checkPullRequest.get(); |
| assert(pubsub.messagesQueue.isEmpty); |
| }); |
| |
| test('Self review is disallowed', () async { |
| PullRequest pullRequest = generatePullRequest(prNumber: 0, author: 'some_rando'); |
| githubService.pullRequestData = pullRequest; |
| unawaited(pubsub.publish(testTopic, pullRequest)); |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| flutterRequest = PullRequestHelper( |
| author: 'some_rando', |
| lastCommitHash: oid, |
| authorAssociation: 'MEMBER', |
| reviews: <PullRequestReviewHelper>[ |
| const PullRequestReviewHelper( |
| authorName: 'some_rando', |
| state: ReviewState.APPROVED, |
| memberType: MemberType.MEMBER, |
| ) |
| ], |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildSuccess, |
| ], |
| ); |
| await checkPullRequest.get(); |
| expectedOptions.add(flutterOption); |
| verifyQueries(expectedOptions); |
| assert(pubsub.messagesQueue.isEmpty); |
| }); |
| |
| test('All messages are pulled', () async { |
| for (int i = 0; i < 3; i++) { |
| final PullRequest pullRequest = generatePullRequest(prNumber: i, repoName: cocoonRepo); |
| unawaited(pubsub.publish('auto-submit-queue-sub', pullRequest)); |
| } |
| |
| checkPullRequest = CheckPullRequest(config: config, pubsub: pubsub, cronAuthProvider: auth); |
| cocoonRequest = PullRequestHelper(prNumber: 0, lastCommitHash: oid); |
| final List<pub.ReceivedMessage> messages = await checkPullRequest.pullMessages(); |
| expect(messages.length, 3); |
| }); |
| }); |
| } |
| |
| QueryResult createQueryResult(PullRequestHelper pullRequest) { |
| return createFakeQueryResult( |
| data: <String, dynamic>{ |
| 'repository': <String, dynamic>{ |
| 'pullRequest': pullRequest.toEntry().cast<String, dynamic>(), |
| }, |
| }, |
| ); |
| } |
| |
| Map<String, dynamic> getMergePullRequestVariables(String id, String number) { |
| return <String, dynamic>{ |
| 'id': id, |
| 'oid': oid, |
| 'title': '$title (#$number)', |
| }; |
| } |