| // 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 'package:cocoon_service/cocoon_service.dart'; |
| import 'package:cocoon_service/src/request_handlers/check_for_waiting_pull_requests_queries.dart'; |
| import 'package:cocoon_service/src/service/logging.dart'; |
| import 'package:github/github.dart'; |
| |
| import 'package:graphql/client.dart'; |
| import 'package:logging/logging.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:mockito/src/mock.dart'; |
| import 'package:test/test.dart'; |
| |
| import '../src/datastore/fake_config.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/service/fake_graphql_client.dart'; |
| import '../src/utilities/mocks.dart'; |
| |
| const String base64LabelId = 'base_64_label_id'; |
| const String oid = 'deadbeef'; |
| const String title = 'some_title'; |
| const Map<String, dynamic> waitForGreenLabel = <String, dynamic>{ |
| 'name': 'waiting for tree to go green', |
| 'id': base64LabelId |
| }; |
| const Map<String, dynamic> overrideTreeStatusLabel = <String, dynamic>{ |
| 'name': 'warning: land on red to fix tree breakage', |
| 'id': 'otherid' |
| }; |
| const List<dynamic> waitForTreeGreenlabels = <dynamic>[ |
| {'name': 'waiting for tree to go green', 'id': base64LabelId} |
| ]; |
| |
| Map<String, dynamic> getMergePullRequestVariables(String number) { |
| return <String, dynamic>{ |
| 'id': number, |
| 'oid': oid, |
| 'title': '$title (#$number)', |
| }; |
| } |
| |
| void main() { |
| group('repos are processed independently', () { |
| late CheckForWaitingPullRequests handler; |
| late ApiRequestHandlerTester tester; |
| FakeHttpRequest request; |
| late FakeGraphQLClient githubGraphQLClient; |
| FakeGraphQLClient cirrusGraphQLClient; |
| FakeConfig config; |
| FakeClientContext clientContext; |
| FakeAuthenticationProvider auth; |
| final List<PullRequestHelper> flutterRepoPRs = <PullRequestHelper>[]; |
| final List<dynamic> statuses = <dynamic>[]; |
| String? branch; |
| |
| setUp(() { |
| request = FakeHttpRequest(); |
| |
| clientContext = FakeClientContext(); |
| auth = FakeAuthenticationProvider(clientContext: clientContext); |
| githubGraphQLClient = FakeGraphQLClient(); |
| cirrusGraphQLClient = FakeGraphQLClient(); |
| config = FakeConfig( |
| rollerAccountsValue: <String>{}, |
| githubGraphQLClient: githubGraphQLClient, |
| cirrusGraphQLClient: cirrusGraphQLClient, |
| supportedReposValue: <RepositorySlug>{ |
| Config.cocoonSlug, |
| Config.engineSlug, |
| Config.flutterSlug, |
| Config.packagesSlug, |
| Config.pluginsSlug, |
| }); |
| flutterRepoPRs.clear(); |
| statuses.clear(); |
| cirrusGraphQLClient.mutateResultForOptions = (MutationOptions options) => createFakeQueryResult(); |
| cirrusGraphQLClient.queryResultForOptions = (QueryOptions options) { |
| return createCirrusQueryResult(statuses, branch); |
| }; |
| tester = ApiRequestHandlerTester(request: request); |
| config.waitingForTreeToGoGreenLabelNameValue = 'waiting for tree to go green'; |
| |
| handler = CheckForWaitingPullRequests( |
| config, |
| auth, |
| ); |
| }); |
| |
| test('Continue with other repos if one fails', () async { |
| flutterRepoPRs.add(PullRequestHelper()); |
| |
| githubGraphQLClient.mutateResultForOptions = (MutationOptions options) => createFakeQueryResult(); |
| int errorIndex = 0; |
| githubGraphQLClient.queryResultForOptions = (QueryOptions options) { |
| if (errorIndex == 0) { |
| errorIndex++; |
| return createFakeQueryResult( |
| exception: OperationException( |
| graphqlErrors: <GraphQLError>[ |
| const GraphQLError(message: 'error'), |
| ], |
| ), |
| ); |
| } |
| return createQueryResult(flutterRepoPRs); |
| }; |
| final List<LogRecord> records = <LogRecord>[]; |
| log.onRecord.listen((LogRecord record) => records.add(record)); |
| await tester.get(handler); |
| final List<LogRecord> errorLogs = records.where((LogRecord logLine) => logLine.level == Level.SEVERE).toList(); |
| expect(errorLogs.length, 1); |
| expect( |
| errorLogs.first.message, contains('OperationException(linkException: null, graphqlErrors: [GraphQLError(')); |
| }); |
| }); |
| group('check for waiting pull requests', () { |
| late CheckForWaitingPullRequests handler; |
| |
| FakeHttpRequest request; |
| late FakeConfig config; |
| FakeClientContext clientContext; |
| FakeAuthenticationProvider auth; |
| late FakeGraphQLClient githubGraphQLClient; |
| FakeGraphQLClient cirrusGraphQLClient; |
| final MockGitHub mockGitHubClient = MockGitHub(); |
| final RepositoriesService mockRepositoriesService = MockRepositoriesService(); |
| |
| late ApiRequestHandlerTester tester; |
| |
| final List<PullRequestHelper> cocoonRepoPRs = <PullRequestHelper>[]; |
| final List<PullRequestHelper> flutterRepoPRs = <PullRequestHelper>[]; |
| final List<PullRequestHelper> engineRepoPRs = <PullRequestHelper>[]; |
| final List<PullRequestHelper> packageRepoPRs = <PullRequestHelper>[]; |
| final List<PullRequestHelper> pluginRepoPRs = <PullRequestHelper>[]; |
| List<dynamic> statuses = <dynamic>[]; |
| String? branch; |
| String totSha; |
| GitHubComparison? githubComparison; |
| |
| setUp(() { |
| request = FakeHttpRequest(); |
| clientContext = FakeClientContext(); |
| auth = FakeAuthenticationProvider(clientContext: clientContext); |
| |
| githubGraphQLClient = FakeGraphQLClient(); |
| cirrusGraphQLClient = FakeGraphQLClient(); |
| config = FakeConfig( |
| rollerAccountsValue: <String>{}, |
| cirrusGraphQLClient: cirrusGraphQLClient, |
| githubGraphQLClient: githubGraphQLClient, |
| githubClient: mockGitHubClient, |
| supportedReposValue: <RepositorySlug>{ |
| Config.cocoonSlug, |
| Config.engineSlug, |
| Config.flutterSlug, |
| Config.packagesSlug, |
| Config.pluginsSlug, |
| }); |
| config.overrideTreeStatusLabelValue = 'warning: land on red to fix tree breakage'; |
| branch = null; |
| totSha = 'abc'; |
| cocoonRepoPRs.clear(); |
| flutterRepoPRs.clear(); |
| engineRepoPRs.clear(); |
| pluginRepoPRs.clear(); |
| statuses.clear(); |
| PullRequestHelper._counter = 0; |
| githubComparison = GitHubComparison('test', 'test', 0, 0, 0, <CommitFile>[CommitFile(name: 'test')]); |
| |
| when(mockGitHubClient.repositories).thenReturn(mockRepositoriesService); |
| when(mockRepositoriesService.getCommit(RepositorySlug('flutter', 'flutter'), 'HEAD~')) |
| .thenAnswer((Invocation invocation) { |
| return Future<RepositoryCommit>.value(RepositoryCommit(sha: totSha)); |
| }); |
| when(mockRepositoriesService.getCommit(RepositorySlug('flutter', 'engine'), 'HEAD~')) |
| .thenAnswer((Invocation invocation) { |
| return Future<RepositoryCommit>.value(RepositoryCommit(sha: totSha)); |
| }); |
| when(mockRepositoriesService.getCommit(RepositorySlug('flutter', 'cocoon'), 'HEAD~')) |
| .thenAnswer((Invocation invocation) { |
| return Future<RepositoryCommit>.value(RepositoryCommit(sha: totSha)); |
| }); |
| when(mockRepositoriesService.compareCommits(RepositorySlug('flutter', 'flutter'), totSha, 'deadbeef')) |
| .thenAnswer((Invocation invocation) { |
| return Future<GitHubComparison>.value(githubComparison); |
| }); |
| when(mockRepositoriesService.compareCommits(RepositorySlug('flutter', 'engine'), totSha, 'deadbeef')) |
| .thenAnswer((Invocation invocation) { |
| return Future<GitHubComparison>.value(githubComparison); |
| }); |
| when(mockRepositoriesService.compareCommits(RepositorySlug('flutter', 'cocoon'), totSha, 'deadbeef')) |
| .thenAnswer((Invocation invocation) { |
| return Future<GitHubComparison>.value(githubComparison); |
| }); |
| |
| cirrusGraphQLClient.mutateResultForOptions = (MutationOptions options) => createFakeQueryResult(); |
| cirrusGraphQLClient.queryResultForOptions = (QueryOptions options) { |
| return createCirrusQueryResult(statuses, branch); |
| }; |
| |
| githubGraphQLClient.mutateResultForOptions = (MutationOptions options) => createFakeQueryResult(); |
| |
| githubGraphQLClient.queryResultForOptions = (QueryOptions options) { |
| expect(options.variables['sOwner'], 'flutter'); |
| expect(options.variables['sLabelName'], config.waitingForTreeToGoGreenLabelNameValue); |
| |
| final String? repoName = options.variables['sName'] as String?; |
| if (repoName == 'flutter') { |
| return createQueryResult(flutterRepoPRs); |
| } else if (repoName == 'engine') { |
| return createQueryResult(engineRepoPRs); |
| } else if (repoName == 'cocoon') { |
| return createQueryResult(cocoonRepoPRs); |
| } else if (repoName == 'packages') { |
| return createQueryResult(packageRepoPRs); |
| } else if (repoName == 'plugins') { |
| return createQueryResult(pluginRepoPRs); |
| } else { |
| fail('unexpected repo $repoName'); |
| } |
| }; |
| |
| tester = ApiRequestHandlerTester(request: request); |
| config.waitingForTreeToGoGreenLabelNameValue = 'waiting for tree to go green'; |
| config.githubGraphQLClient = githubGraphQLClient; |
| |
| handler = CheckForWaitingPullRequests( |
| config, |
| auth, |
| ); |
| }); |
| |
| void _verifyQueries() { |
| githubGraphQLClient.verifyQueries( |
| <QueryOptions>[ |
| QueryOptions( |
| document: labeledPullRequestsWithReviewsQuery, |
| fetchPolicy: FetchPolicy.noCache, |
| variables: <String, dynamic>{ |
| 'sOwner': 'flutter', |
| 'sName': 'cocoon', |
| 'sLabelName': config.waitingForTreeToGoGreenLabelNameValue, |
| }, |
| ), |
| QueryOptions( |
| document: labeledPullRequestsWithReviewsQuery, |
| fetchPolicy: FetchPolicy.noCache, |
| variables: <String, dynamic>{ |
| 'sOwner': 'flutter', |
| 'sName': 'engine', |
| 'sLabelName': config.waitingForTreeToGoGreenLabelNameValue, |
| }, |
| ), |
| QueryOptions( |
| document: labeledPullRequestsWithReviewsQuery, |
| fetchPolicy: FetchPolicy.noCache, |
| variables: <String, dynamic>{ |
| 'sOwner': 'flutter', |
| 'sName': 'flutter', |
| 'sLabelName': config.waitingForTreeToGoGreenLabelNameValue, |
| }, |
| ), |
| QueryOptions( |
| document: labeledPullRequestsWithReviewsQuery, |
| fetchPolicy: FetchPolicy.noCache, |
| variables: <String, dynamic>{ |
| 'sOwner': 'flutter', |
| 'sName': 'packages', |
| 'sLabelName': config.waitingForTreeToGoGreenLabelNameValue, |
| }, |
| ), |
| QueryOptions( |
| document: labeledPullRequestsWithReviewsQuery, |
| fetchPolicy: FetchPolicy.noCache, |
| variables: <String, dynamic>{ |
| 'sOwner': 'flutter', |
| 'sName': 'plugins', |
| 'sLabelName': config.waitingForTreeToGoGreenLabelNameValue, |
| }, |
| ), |
| ], |
| ); |
| } |
| |
| test('Errors can be logged', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| flutterRepoPRs.add(PullRequestHelper(labels: waitForTreeGreenlabels)); |
| final List<GraphQLError> errors = <GraphQLError>[ |
| const GraphQLError(message: 'message'), |
| ]; |
| final OperationException exception = OperationException(graphqlErrors: errors); |
| githubGraphQLClient.mutateResultForOptions = (_) => createFakeQueryResult(exception: exception); |
| final List<LogRecord> records = <LogRecord>[]; |
| log.onRecord.listen((LogRecord record) => records.add(record)); |
| await tester.get(handler); |
| final List<LogRecord> errorLogs = records.where((LogRecord record) => record.level == Level.SEVERE).toList(); |
| expect(errorLogs.length, errors.length); |
| expect(errorLogs.first.message, 'Failed to merge pr#: 0 with ${exception.toString()}'); |
| }); |
| |
| test('Merges unapproved PR from autoroller', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| config.rollerAccountsValue = <String>{'engine-roller', 'skia-roller'}; |
| flutterRepoPRs.add(PullRequestHelper( |
| author: 'engine-roller', |
| reviews: const <PullRequestReviewHelper>[], |
| labels: waitForTreeGreenlabels, |
| )); |
| engineRepoPRs.add( |
| PullRequestHelper( |
| author: 'skia-roller', |
| reviews: const <PullRequestReviewHelper>[], |
| labels: waitForTreeGreenlabels, |
| ), |
| ); |
| |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(engineRepoPRs.first.id), |
| ), |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(flutterRepoPRs.first.id), |
| ), |
| ], |
| ); |
| }); |
| |
| test('Merges unapproved PR from dependabot', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| config.rollerAccountsValue = <String>{'dependabot'}; |
| flutterRepoPRs.add(PullRequestHelper( |
| author: 'dependabot', reviews: const <PullRequestReviewHelper>[], labels: waitForTreeGreenlabels)); |
| engineRepoPRs.add(PullRequestHelper( |
| author: 'dependabot', reviews: const <PullRequestReviewHelper>[], labels: waitForTreeGreenlabels)); |
| |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(engineRepoPRs.first.id), |
| ), |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(flutterRepoPRs.first.id), |
| ), |
| ], |
| ); |
| }); |
| |
| test('Does not merge PR with in progress tests', () async { |
| statuses = <dynamic>[ |
| <String, String>{'id': '1', 'status': 'EXECUTING', 'name': 'test1'}, |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| branch = 'pull/0'; |
| |
| flutterRepoPRs.add(PullRequestHelper()); |
| |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[]); |
| }); |
| |
| test('Does not merge PR with in progress checks', () async { |
| branch = 'pull/0'; |
| final PullRequestHelper prInProgress = PullRequestHelper( |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.windowsInProgress, |
| ], |
| ); |
| flutterRepoPRs.add(prInProgress); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[]); |
| }); |
| |
| test('Does not merge PR with queued checks', () async { |
| branch = 'pull/0'; |
| final PullRequestHelper prQueued = PullRequestHelper( |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.macQueued, |
| ], |
| ); |
| flutterRepoPRs.add(prQueued); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[]); |
| }); |
| |
| test('Does not merge PR with requested checks', () async { |
| branch = 'pull/0'; |
| final PullRequestHelper prRequested = PullRequestHelper( |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.linuxRequested, |
| ], |
| ); |
| flutterRepoPRs.add(prRequested); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[]); |
| }); |
| |
| test('Does not merge PR with failed status', () async { |
| branch = 'pull/0'; |
| final PullRequestHelper prRequested = PullRequestHelper( |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.linuxRequested, |
| ], |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildFailure, |
| ], |
| ); |
| flutterRepoPRs.add(prRequested); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[]); |
| }); |
| |
| test('Merges PR with failed tree status if override tree status label is provided', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| branch = 'pull/0'; |
| final PullRequestHelper prRequested = PullRequestHelper( |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.luciCompletedSuccess, |
| ], |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildFailure, |
| ], |
| labels: <dynamic>[waitForGreenLabel, overrideTreeStatusLabel], |
| ); |
| flutterRepoPRs.add(prRequested); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[ |
| MutationOptions(document: mergePullRequestMutation, variables: <String, dynamic>{ |
| 'id': flutterRepoPRs.first.id, |
| 'oid': oid, |
| 'title': 'some_title (#0)', |
| }), |
| ]); |
| }); |
| |
| test('Merge a clean revert PR with in progress tests', () async { |
| githubComparison = GitHubComparison('abc', 'def', 0, 0, 0, <CommitFile>[]); |
| statuses = <dynamic>[ |
| <String, String>{'id': '1', 'status': 'EXECUTING', 'name': 'test1'}, |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| branch = 'pull/0'; |
| |
| final PullRequestHelper prRequested = PullRequestHelper( |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.linuxCompletedRunning, |
| ], |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildSuccess, |
| ], |
| lastCommitMessage: 'Revert "This is a test PR" This reverts commit abc.', |
| labels: waitForTreeGreenlabels, |
| ); |
| flutterRepoPRs.add(prRequested); |
| |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[ |
| MutationOptions(document: mergePullRequestMutation, variables: <String, dynamic>{ |
| 'id': flutterRepoPRs.first.id, |
| 'oid': oid, |
| 'title': 'some_title (#0)', |
| }), |
| ]); |
| }); |
| |
| test('Merge a clean revert PR ignoring latency', () async { |
| githubComparison = GitHubComparison('abc', 'def', 0, 0, 0, <CommitFile>[]); |
| statuses = <dynamic>[ |
| <String, String>{'id': '1', 'status': 'COMPLETED', 'name': 'test1'}, |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| branch = 'pull/0'; |
| |
| final PullRequestHelper prRequested = PullRequestHelper( |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.linuxCompletedRunning, |
| ], |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildSuccess, |
| ], |
| lastCommitMessage: 'Revert "This is a test PR" This reverts commit abc.', |
| labels: waitForTreeGreenlabels, |
| dateTime: DateTime.now().add(const Duration(minutes: -10)), |
| ); |
| flutterRepoPRs.add(prRequested); |
| |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[ |
| MutationOptions(document: mergePullRequestMutation, variables: <String, dynamic>{ |
| 'id': flutterRepoPRs.first.id, |
| 'oid': oid, |
| 'title': 'some_title (#0)', |
| }), |
| ]); |
| }); |
| |
| test('Merges PR with check that was neutral', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| branch = 'pull/0'; |
| final PullRequestHelper prRequested = PullRequestHelper( |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.luciCompletedNeutral, |
| ], |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildSuccess, |
| ], |
| labels: waitForTreeGreenlabels, |
| ); |
| flutterRepoPRs.add(prRequested); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[ |
| MutationOptions(document: mergePullRequestMutation, variables: <String, dynamic>{ |
| 'id': flutterRepoPRs.first.id, |
| 'oid': oid, |
| 'title': 'some_title (#0)', |
| }), |
| ]); |
| }); |
| |
| test('Merges PR with check that is successful but still considered running', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| branch = 'pull/0'; |
| final PullRequestHelper prRequested = PullRequestHelper( |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.linuxCompletedRunning, |
| ], |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildSuccess, |
| ], |
| labels: waitForTreeGreenlabels, |
| ); |
| flutterRepoPRs.add(prRequested); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[ |
| MutationOptions(document: mergePullRequestMutation, variables: <String, dynamic>{ |
| 'id': flutterRepoPRs.first.id, |
| 'oid': oid, |
| 'title': 'some_title (#0)', |
| }), |
| ]); |
| }); |
| |
| test('Does not merge PR with failed checks', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| githubComparison = GitHubComparison('abc', 'def', 0, 0, 0, <CommitFile>[CommitFile(name: 'test')]); |
| branch = 'pull/0'; |
| final PullRequestHelper prRequested = PullRequestHelper( |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.luciCompletedFailure, |
| CheckRunHelper.luciCompletedSuccess, |
| ], |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildSuccess, |
| ], |
| labels: waitForTreeGreenlabels, |
| ); |
| flutterRepoPRs.add(prRequested); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[ |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': flutterRepoPRs.first.id, |
| 'labelId': base64LabelId, |
| 'sBody': ''' |
| This pull request is not suitable for automatic merging in its current state. |
| |
| - The status or check suite [Linux](https://Linux) has failed. Please fix the issues identified (or deflake) before re-applying this label. |
| ''', |
| }, |
| ), |
| ]); |
| }); |
| |
| test('Does not fail with null checks', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| githubComparison = GitHubComparison('abc', 'def', 0, 0, 0, <CommitFile>[CommitFile(name: 'test')]); |
| branch = 'pull/0'; |
| final PullRequestHelper prRequested = |
| PullRequestHelper(lastCommitCheckRuns: const <CheckRunHelper>[], lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildFailure, |
| ], labels: <dynamic>[ |
| {'name': 'waiting for tree to go green', 'id': base64LabelId} |
| ]); |
| prRequested.lastCommitCheckRuns = null; |
| flutterRepoPRs.add(prRequested); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[ |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': flutterRepoPRs.first.id, |
| 'labelId': base64LabelId, |
| 'sBody': ''' |
| This pull request is not suitable for automatic merging in its current state. |
| |
| - This commit has no checks. Please check that ci.yaml validation has started and there are multiple checks. If not, try uploading an empty commit. |
| ''', |
| }, |
| ), |
| ]); |
| }); |
| |
| test('Empty validations do not merge', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| githubComparison = GitHubComparison('abc', 'def', 0, 0, 0, <CommitFile>[CommitFile(name: 'test')]); |
| branch = 'pull/0'; |
| final PullRequestHelper prRequested = PullRequestHelper( |
| lastCommitCheckRuns: const <CheckRunHelper>[], |
| lastCommitStatuses: const <StatusHelper>[], |
| labels: waitForTreeGreenlabels, |
| ); |
| flutterRepoPRs.add(prRequested); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[ |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': flutterRepoPRs.first.id, |
| 'labelId': base64LabelId, |
| 'sBody': ''' |
| This pull request is not suitable for automatic merging in its current state. |
| |
| - The status or check suite [tree status luci-flutter](https://flutter-dashboard.appspot.com/#/build) has failed. Please fix the issues identified (or deflake) before re-applying this label. |
| - This commit has no checks. Please check that ci.yaml validation has started and there are multiple checks. If not, try uploading an empty commit. |
| ''', |
| }, |
| ), |
| ]); |
| }); |
| |
| test('Merge PR with successful checks on repo without tree status', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| branch = 'pull/0'; |
| final PullRequestHelper prRequested = PullRequestHelper( |
| repo: 'cocoon', |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.luciCompletedSuccess, |
| ], |
| lastCommitStatuses: const <StatusHelper>[], |
| labels: waitForTreeGreenlabels, |
| ); |
| prRequested.lastCommitStatuses = null; |
| flutterRepoPRs.add(prRequested); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(flutterRepoPRs.first.id), |
| ), |
| ]); |
| }); |
| |
| test('Merge PR with successful status and checks', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| branch = 'pull/0'; |
| final PullRequestHelper prRequested = PullRequestHelper( |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.luciCompletedSuccess, |
| ], |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildSuccess, |
| ], |
| labels: waitForTreeGreenlabels, |
| ); |
| cocoonRepoPRs.add(prRequested); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(cocoonRepoPRs.first.id), |
| ), |
| ]); |
| }); |
| |
| test('Does not merge if non member does not have at least 2 member reviews', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| githubComparison = GitHubComparison('abc', 'def', 0, 0, 0, <CommitFile>[CommitFile(name: 'test')]); |
| branch = 'pull/0'; |
| final PullRequestHelper prRequested = PullRequestHelper( |
| authorAssociation: '', |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.luciCompletedSuccess, |
| ], |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildSuccess, |
| ], |
| labels: waitForTreeGreenlabels, |
| ); |
| flutterRepoPRs.add(prRequested); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[ |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': flutterRepoPRs.first.id, |
| 'sBody': '''This pull request is not suitable for automatic merging in its current state. |
| |
| - Please get at least one approved review if you are already a member or two member reviews if you are not a member before re-applying this label. __Reviewers__: If you left a comment approving, please use the "approve" review action instead. |
| ''', |
| 'labelId': base64LabelId, |
| }, |
| ), |
| ]); |
| }); |
| |
| test('Self review is disallowed', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| githubComparison = GitHubComparison('abc', 'def', 0, 0, 0, <CommitFile>[CommitFile(name: 'test')]); |
| branch = 'pull/0'; |
| final PullRequestHelper prRequested = PullRequestHelper( |
| author: 'some_rando', |
| authorAssociation: 'MEMBER', |
| reviews: <PullRequestReviewHelper>[ |
| const PullRequestReviewHelper( |
| authorName: 'some_rando', state: ReviewState.APPROVED, memberType: MemberType.MEMBER) |
| ], |
| lastCommitCheckRuns: const <CheckRunHelper>[ |
| CheckRunHelper.luciCompletedSuccess, |
| ], |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildSuccess, |
| ], |
| labels: waitForTreeGreenlabels, |
| ); |
| flutterRepoPRs.add(prRequested); |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[ |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': flutterRepoPRs.first.id, |
| 'sBody': '''This pull request is not suitable for automatic merging in its current state. |
| |
| - Please get at least one approved review if you are already a member or two member reviews if you are not a member before re-applying this label. __Reviewers__: If you left a comment approving, please use the "approve" review action instead. |
| ''', |
| 'labelId': base64LabelId, |
| }, |
| ), |
| ]); |
| }); |
| |
| test('Merge PR with complated tests', () async { |
| statuses = <dynamic>[ |
| <String, String>{'id': '1', 'status': 'SKIPPED', 'name': 'test1'}, |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| branch = 'pull/0'; |
| |
| flutterRepoPRs.add(PullRequestHelper(labels: waitForTreeGreenlabels)); |
| |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(flutterRepoPRs[0].id), |
| ), |
| ], |
| ); |
| }); |
| |
| test('Does not merge PR with failed tests', () async { |
| githubComparison = GitHubComparison('abc', 'def', 0, 0, 0, <CommitFile>[CommitFile(name: 'test')]); |
| statuses = <dynamic>[ |
| <String, String>{'id': '1', 'status': 'FAILED', 'name': 'test1'}, |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| branch = 'pull/0'; |
| |
| flutterRepoPRs.add(PullRequestHelper( |
| labels: waitForTreeGreenlabels, |
| )); |
| |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': flutterRepoPRs[0].id, |
| 'labelId': base64LabelId, |
| 'sBody': ''' |
| This pull request is not suitable for automatic merging in its current state. |
| |
| - The status or check suite [test1](https://cirrus-ci.com/task/1) has failed. Please fix the issues identified (or deflake) before re-applying this label. |
| ''', |
| }, |
| ), |
| ], |
| ); |
| }); |
| |
| test('Does not merge unapproved PR from a hacker', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| githubComparison = GitHubComparison('abc', 'def', 0, 0, 0, <CommitFile>[CommitFile(name: 'test')]); |
| config.rollerAccountsValue = <String>{'engine-roller', 'skia-roller'}; |
| flutterRepoPRs.add(PullRequestHelper( |
| author: 'engine-roller-hacker', |
| reviews: const <PullRequestReviewHelper>[], |
| labels: waitForTreeGreenlabels, |
| )); |
| engineRepoPRs.add(PullRequestHelper( |
| author: 'skia-roller-hacker', |
| reviews: const <PullRequestReviewHelper>[], |
| labels: waitForTreeGreenlabels, |
| )); |
| |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': engineRepoPRs.first.id, |
| 'sBody': '''This pull request is not suitable for automatic merging in its current state. |
| |
| - Please get at least one approved review if you are already a member or two member reviews if you are not a member before re-applying this label. __Reviewers__: If you left a comment approving, please use the "approve" review action instead. |
| ''', |
| 'labelId': base64LabelId, |
| }, |
| ), |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': flutterRepoPRs.first.id, |
| 'sBody': '''This pull request is not suitable for automatic merging in its current state. |
| |
| - Please get at least one approved review if you are already a member or two member reviews if you are not a member before re-applying this label. __Reviewers__: If you left a comment approving, please use the "approve" review action instead. |
| ''', |
| 'labelId': base64LabelId, |
| }, |
| ), |
| ], |
| ); |
| }); |
| |
| test('Merges first 2 PRs in list, all successful', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| flutterRepoPRs.add(PullRequestHelper(labels: waitForTreeGreenlabels)); |
| flutterRepoPRs.add(PullRequestHelper(labels: waitForTreeGreenlabels)); |
| flutterRepoPRs.add(PullRequestHelper(labels: waitForTreeGreenlabels)); // will be ignored. |
| engineRepoPRs.add(PullRequestHelper(labels: waitForTreeGreenlabels)); |
| |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(engineRepoPRs.first.id), |
| ), |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(flutterRepoPRs[0].id), |
| ), |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(flutterRepoPRs[1].id), |
| ), |
| ], |
| ); |
| }); |
| |
| test('Merges 1st and 3rd PR, 2nd failed', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| githubComparison = GitHubComparison('abc', 'def', 0, 0, 0, <CommitFile>[CommitFile(name: 'test')]); |
| flutterRepoPRs.add(PullRequestHelper(labels: waitForTreeGreenlabels)); |
| flutterRepoPRs.add(PullRequestHelper( |
| author: 'engine-roller-hacker', |
| reviews: const <PullRequestReviewHelper>[], |
| labels: waitForTreeGreenlabels, |
| )); |
| |
| flutterRepoPRs.add(PullRequestHelper(labels: waitForTreeGreenlabels)); |
| engineRepoPRs.add(PullRequestHelper(labels: waitForTreeGreenlabels)); |
| |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(engineRepoPRs.first.id), |
| ), |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(flutterRepoPRs[0].id), |
| ), |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': flutterRepoPRs[1].id, |
| 'sBody': '''This pull request is not suitable for automatic merging in its current state. |
| |
| - Please get at least one approved review if you are already a member or two member reviews if you are not a member before re-applying this label. __Reviewers__: If you left a comment approving, please use the "approve" review action instead. |
| ''', |
| 'labelId': base64LabelId, |
| }, |
| ), |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(flutterRepoPRs[2].id), |
| ), |
| ], |
| ); |
| }); |
| |
| test('Ignores PRs that are too new', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| flutterRepoPRs.add(PullRequestHelper( |
| dateTime: DateTime.now().add(const Duration(minutes: -50)), |
| labels: waitForTreeGreenlabels, |
| )); // too new |
| flutterRepoPRs.add(PullRequestHelper( |
| dateTime: DateTime.now().add(const Duration(minutes: -70)), |
| labels: waitForTreeGreenlabels, |
| )); // ok |
| engineRepoPRs.add(PullRequestHelper(labels: waitForTreeGreenlabels)); // default is two hours for this ctor. |
| |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(engineRepoPRs.first.id), |
| ), |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(flutterRepoPRs.last.id), |
| ), |
| ], |
| ); |
| }); |
| |
| test('Unlabels red PRs', () async { |
| githubComparison = GitHubComparison('abc', 'def', 0, 0, 0, <CommitFile>[CommitFile(name: 'test')]); |
| statuses = <dynamic>[ |
| <String, String>{'id': '1', 'status': 'FAILED', 'name': 'test1'}, |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| branch = 'pull/0'; |
| final PullRequestHelper prRed = PullRequestHelper( |
| lastCommitStatuses: const <StatusHelper>[ |
| StatusHelper.flutterBuildSuccess, |
| StatusHelper.otherStatusFailure, |
| ], |
| labels: waitForTreeGreenlabels, |
| ); |
| flutterRepoPRs.add(prRed); |
| |
| await tester.get(handler); |
| _verifyQueries(); |
| githubGraphQLClient.verifyMutations(<MutationOptions>[ |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': prRed.id, |
| 'sBody': '''This pull request is not suitable for automatic merging in its current state. |
| |
| - The status or check suite [other status](https://other status) has failed. Please fix the issues identified (or deflake) before re-applying this label. |
| - The status or check suite [test1](https://cirrus-ci.com/task/1) has failed. Please fix the issues identified (or deflake) before re-applying this label. |
| ''', |
| 'labelId': base64LabelId, |
| }, |
| ), |
| ]); |
| }); |
| |
| test('Allows member to change review', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| final PullRequestHelper prChangedReview = PullRequestHelper( |
| reviews: const <PullRequestReviewHelper>[ |
| changePleaseChange, |
| changePleaseApprove, |
| ], |
| labels: waitForTreeGreenlabels, |
| ); |
| |
| flutterRepoPRs.add(prChangedReview); |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(prChangedReview.id), |
| ), |
| ], |
| ); |
| }); |
| |
| test('Ignores non-member/owner reviews', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| githubComparison = GitHubComparison('abc', 'def', 0, 0, 0, <CommitFile>[CommitFile(name: 'test')]); |
| final PullRequestHelper prNonMemberApprove = PullRequestHelper( |
| reviews: const <PullRequestReviewHelper>[ |
| nonMemberApprove, |
| ], |
| labels: waitForTreeGreenlabels, |
| ); |
| final PullRequestHelper prNonMemberChangeRequest = PullRequestHelper( |
| reviews: const <PullRequestReviewHelper>[ |
| nonMemberChangeRequest, |
| ], |
| labels: waitForTreeGreenlabels, |
| ); |
| final PullRequestHelper prNonMemberChangeRequestWithMemberApprove = PullRequestHelper( |
| reviews: const <PullRequestReviewHelper>[ |
| ownerApprove, |
| nonMemberChangeRequest, |
| ], |
| labels: waitForTreeGreenlabels, |
| ); |
| |
| // Ignored approval from non-member |
| flutterRepoPRs.add(prNonMemberApprove); |
| // Ignored change reuqest from non-member (but still no approval from member) |
| flutterRepoPRs.add(prNonMemberChangeRequest); |
| // Ignored change request from non-member with approval from owner/member. |
| flutterRepoPRs.add(prNonMemberChangeRequestWithMemberApprove); |
| |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': prNonMemberApprove.id, |
| 'sBody': '''This pull request is not suitable for automatic merging in its current state. |
| |
| - Please get at least one approved review if you are already a member or two member reviews if you are not a member before re-applying this label. __Reviewers__: If you left a comment approving, please use the "approve" review action instead. |
| ''', |
| 'labelId': base64LabelId, |
| }, |
| ), |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': prNonMemberChangeRequest.id, |
| 'sBody': '''This pull request is not suitable for automatic merging in its current state. |
| |
| - Please get at least one approved review if you are already a member or two member reviews if you are not a member before re-applying this label. __Reviewers__: If you left a comment approving, please use the "approve" review action instead. |
| ''', |
| 'labelId': base64LabelId, |
| }, |
| ), |
| MutationOptions( |
| document: mergePullRequestMutation, |
| variables: getMergePullRequestVariables(prNonMemberChangeRequestWithMemberApprove.id), |
| ), |
| ], |
| ); |
| }); |
| |
| test('Remove labels', () async { |
| // Ensure there is at least one cirrus status. |
| statuses = <dynamic>[ |
| <String, String>{'id': '2', 'status': 'COMPLETED', 'name': 'test2'} |
| ]; |
| githubComparison = GitHubComparison('abc', 'def', 0, 0, 0, <CommitFile>[CommitFile(name: 'test')]); |
| final PullRequestHelper prOneBadReview = PullRequestHelper( |
| reviews: const <PullRequestReviewHelper>[ |
| changePleaseChange, |
| ], |
| labels: waitForTreeGreenlabels, |
| ); |
| final PullRequestHelper prOneGoodOneBadReview = PullRequestHelper( |
| reviews: const <PullRequestReviewHelper>[ |
| memberApprove, |
| changePleaseChange, |
| ], |
| labels: waitForTreeGreenlabels, |
| ); |
| final PullRequestHelper prNoReviews = PullRequestHelper( |
| reviews: const <PullRequestReviewHelper>[], |
| labels: waitForTreeGreenlabels, |
| ); |
| final PullRequestHelper prEverythingWrong = PullRequestHelper( |
| lastCommitStatuses: const <StatusHelper>[StatusHelper.flutterBuildFailure], |
| reviews: const <PullRequestReviewHelper>[changePleaseChange], |
| labels: waitForTreeGreenlabels, |
| ); |
| |
| flutterRepoPRs.add(prOneBadReview); |
| flutterRepoPRs.add(prOneGoodOneBadReview); |
| flutterRepoPRs.add(prNoReviews); |
| flutterRepoPRs.add(prEverythingWrong); |
| |
| await tester.get(handler); |
| |
| _verifyQueries(); |
| |
| githubGraphQLClient.verifyMutations( |
| <MutationOptions>[ |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': prOneBadReview.id, |
| 'sBody': '''This pull request is not suitable for automatic merging in its current state. |
| |
| - This pull request has changes requested by @change_please. Please resolve those before re-applying the label. |
| ''', |
| 'labelId': base64LabelId, |
| }, |
| ), |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': prOneGoodOneBadReview.id, |
| 'sBody': '''This pull request is not suitable for automatic merging in its current state. |
| |
| - This pull request has changes requested by @change_please. Please resolve those before re-applying the label. |
| ''', |
| 'labelId': base64LabelId, |
| }, |
| ), |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': prNoReviews.id, |
| 'sBody': '''This pull request is not suitable for automatic merging in its current state. |
| |
| - Please get at least one approved review if you are already a member or two member reviews if you are not a member before re-applying this label. __Reviewers__: If you left a comment approving, please use the "approve" review action instead. |
| ''', |
| 'labelId': base64LabelId, |
| }, |
| ), |
| MutationOptions( |
| document: removeLabelMutation, |
| variables: <String, dynamic>{ |
| 'id': prEverythingWrong.id, |
| 'sBody': '''This pull request is not suitable for automatic merging in its current state. |
| |
| - This pull request has changes requested by @change_please. Please resolve those before re-applying the label. |
| ''', |
| 'labelId': base64LabelId, |
| }, |
| ), |
| ], |
| ); |
| }); |
| }); |
| } |
| |
| enum ReviewState { |
| APPROVED, |
| CHANGES_REQUESTED, |
| } |
| |
| enum MemberType { |
| OWNER, |
| MEMBER, |
| OTHER, |
| } |
| |
| @immutable |
| class PullRequestReviewHelper { |
| const PullRequestReviewHelper({ |
| required this.authorName, |
| required this.state, |
| required this.memberType, |
| }); |
| |
| final String authorName; |
| final ReviewState state; |
| final MemberType memberType; |
| } |
| |
| @immutable |
| class StatusHelper { |
| const StatusHelper(this.name, this.state); |
| |
| static const StatusHelper cirrusSuccess = StatusHelper('Cirrus CI', 'SUCCESS'); |
| static const StatusHelper cirrusFailure = StatusHelper('Cirrus CI', 'FAILURE'); |
| static const StatusHelper flutterBuildSuccess = StatusHelper('luci-flutter', 'SUCCESS'); |
| static const StatusHelper flutterBuildFailure = StatusHelper('luci-flutter', 'FAILURE'); |
| static const StatusHelper otherStatusFailure = StatusHelper('other status', 'FAILURE'); |
| static const StatusHelper luciEngineBuildSuccess = StatusHelper('luci-engine', 'SUCCESS'); |
| static const StatusHelper luciEngineBuildFailure = StatusHelper('luci-engine', 'FAILURE'); |
| |
| final String name; |
| final String state; |
| } |
| |
| @immutable |
| class CheckRunHelper { |
| const CheckRunHelper(this.name, this.status, this.conclusion); |
| |
| static const CheckRunHelper luciCompletedSuccess = CheckRunHelper('Linux', 'COMPLETED', 'SUCCESS'); |
| static const CheckRunHelper luciCompletedFailure = CheckRunHelper('Linux', 'COMPLETED', 'FAILURE'); |
| static const CheckRunHelper luciCompletedNeutral = CheckRunHelper('Linux', 'COMPLETED', 'NEUTRAL'); |
| static const CheckRunHelper luciCompletedSkipped = CheckRunHelper('Linux', 'COMPLETED', 'SKIPPED'); |
| static const CheckRunHelper luciCompletedStale = CheckRunHelper('Linux', 'COMPLETED', 'STALE'); |
| static const CheckRunHelper luciCompletedTimedout = CheckRunHelper('Linux', 'COMPLETED', 'TIMED_OUT'); |
| static const CheckRunHelper windowsInProgress = CheckRunHelper('Windows', 'IN_PROGRESS', ''); |
| static const CheckRunHelper macQueued = CheckRunHelper('Mac', 'QUEUED', ''); |
| static const CheckRunHelper linuxRequested = CheckRunHelper('Linux', 'REQUESTED', ''); |
| // See https://github.com/flutter/flutter/issues/91908 |
| static const CheckRunHelper linuxCompletedRunning = CheckRunHelper('Linux', 'IN PROGRESS', 'SUCCESS'); |
| |
| final String name; |
| final String status; |
| final String conclusion; |
| } |
| |
| class PullRequestHelper { |
| PullRequestHelper({ |
| this.author = 'some_rando', |
| this.repo = 'flutter', |
| this.authorAssociation = 'MEMBER', |
| this.title = 'some_title', |
| this.reviews = const <PullRequestReviewHelper>[ |
| PullRequestReviewHelper(authorName: 'member', state: ReviewState.APPROVED, memberType: MemberType.MEMBER) |
| ], |
| this.lastCommitHash = oid, |
| this.lastCommitStatuses = const <StatusHelper>[StatusHelper.flutterBuildSuccess], |
| this.lastCommitCheckRuns = const <CheckRunHelper>[CheckRunHelper.luciCompletedSuccess], |
| this.lastCommitMessage = '', |
| this.dateTime, |
| this.labels = const <dynamic>[], |
| }) : _count = _counter++; |
| |
| static int _counter = 0; |
| |
| final int _count; |
| String get id => _count.toString(); |
| |
| final String repo; |
| final String author; |
| final String authorAssociation; |
| final String title; |
| final List<PullRequestReviewHelper> reviews; |
| final String lastCommitHash; |
| List<StatusHelper>? lastCommitStatuses; |
| List<CheckRunHelper>? lastCommitCheckRuns; |
| final String? lastCommitMessage; |
| final DateTime? dateTime; |
| List<dynamic> labels; |
| |
| RepositorySlug get slug => RepositorySlug('flutter', repo); |
| |
| Map<String, dynamic> toEntry() { |
| return <String, dynamic>{ |
| 'author': <String, dynamic>{'login': author}, |
| 'authorAssociation': authorAssociation, |
| 'id': id, |
| 'baseRepository': <String, dynamic>{ |
| 'nameWithOwner': slug.fullName, |
| }, |
| 'number': _count, |
| 'title': title, |
| 'reviews': <String, dynamic>{ |
| 'nodes': reviews.map((PullRequestReviewHelper review) { |
| return <String, dynamic>{ |
| 'author': <String, dynamic>{'login': review.authorName}, |
| 'authorAssociation': review.memberType.toString().replaceFirst('MemberType.', ''), |
| 'state': review.state.toString().replaceFirst('ReviewState.', ''), |
| }; |
| }).toList(), |
| }, |
| 'commits': <String, dynamic>{ |
| 'nodes': <dynamic>[ |
| <String, dynamic>{ |
| 'commit': <String, dynamic>{ |
| 'oid': lastCommitHash, |
| 'pushedDate': (dateTime ?? DateTime.now().add(const Duration(hours: -2))).toUtc().toIso8601String(), |
| 'message': lastCommitMessage, |
| 'status': <String, dynamic>{ |
| 'contexts': lastCommitStatuses != null |
| ? lastCommitStatuses!.map((StatusHelper status) { |
| return <String, dynamic>{ |
| 'context': status.name, |
| 'state': status.state, |
| 'targetUrl': 'https://${status.name}', |
| }; |
| }).toList() |
| : <dynamic>[] |
| }, |
| 'checkSuites': <String, dynamic>{ |
| 'nodes': lastCommitCheckRuns != null |
| ? <dynamic>[ |
| <String, dynamic>{ |
| 'checkRuns': <String, dynamic>{ |
| 'nodes': lastCommitCheckRuns!.map((CheckRunHelper status) { |
| return <String, dynamic>{ |
| 'name': status.name, |
| 'status': status.status, |
| 'conclusion': status.conclusion, |
| 'detailsUrl': 'https://${status.name}', |
| }; |
| }).toList(), |
| } |
| } |
| ] |
| : <dynamic>[] |
| }, |
| }, |
| } |
| ], |
| }, |
| 'labels': <String, dynamic>{ |
| 'nodes': labels, |
| }, |
| }; |
| } |
| } |
| |
| QueryResult createQueryResult(List<PullRequestHelper> pullRequests) { |
| return createFakeQueryResult( |
| data: <String, dynamic>{ |
| 'repository': <String, dynamic>{ |
| 'pullRequests': <String, dynamic>{ |
| 'nodes': pullRequests |
| .map<Map<String, dynamic>>( |
| (PullRequestHelper pullRequest) => pullRequest.toEntry(), |
| ) |
| .toList(), |
| }, |
| }, |
| }, |
| ); |
| } |
| |
| QueryResult createCirrusQueryResult(List<dynamic> statuses, String? branch) { |
| if (statuses.isEmpty) { |
| return createFakeQueryResult(); |
| } |
| return createFakeQueryResult( |
| data: <String, dynamic>{ |
| 'searchBuilds': <dynamic>[ |
| <String, dynamic>{ |
| 'id': '1', |
| 'branch': branch, |
| 'latestGroupTasks': statuses.map<Map<String, dynamic>>((dynamic status) { |
| return <String, dynamic>{ |
| 'id': status['id'], |
| 'name': status['name'], |
| 'status': status['status'], |
| }; |
| }).toList(), |
| } |
| ], |
| }, |
| ); |
| } |
| |
| const PullRequestReviewHelper ownerApprove = PullRequestReviewHelper( |
| authorName: 'owner', |
| memberType: MemberType.OWNER, |
| state: ReviewState.APPROVED, |
| ); |
| const PullRequestReviewHelper changePleaseChange = PullRequestReviewHelper( |
| authorName: 'change_please', |
| memberType: MemberType.MEMBER, |
| state: ReviewState.CHANGES_REQUESTED, |
| ); |
| const PullRequestReviewHelper changePleaseApprove = PullRequestReviewHelper( |
| authorName: 'change_please', |
| memberType: MemberType.MEMBER, |
| state: ReviewState.APPROVED, |
| ); |
| const PullRequestReviewHelper memberApprove = PullRequestReviewHelper( |
| authorName: 'member', |
| memberType: MemberType.MEMBER, |
| state: ReviewState.APPROVED, |
| ); |
| const PullRequestReviewHelper nonMemberApprove = PullRequestReviewHelper( |
| authorName: 'random_person', |
| memberType: MemberType.OTHER, |
| state: ReviewState.APPROVED, |
| ); |
| const PullRequestReviewHelper nonMemberChangeRequest = PullRequestReviewHelper( |
| authorName: 'random_person', |
| memberType: MemberType.OTHER, |
| state: ReviewState.CHANGES_REQUESTED, |
| ); |