blob: f278ceca7b1ecb0fe736127892f27e381eb7b4b5 [file] [log] [blame]
// 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,
);