| // Copyright 2020 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| |
| import 'package:cocoon_service/src/model/appengine/github_gold_status_update.dart'; |
| import 'package:cocoon_service/src/request_handlers/push_gold_status_to_github.dart'; |
| import 'package:cocoon_service/src/request_handling/body.dart'; |
| import 'package:cocoon_service/src/service/datastore.dart'; |
| import 'package:cocoon_service/src/service/logging.dart'; |
| import 'package:gcloud/db.dart' as gcloud_db; |
| import 'package:gcloud/db.dart'; |
| import 'package:github/github.dart'; |
| import 'package:graphql/client.dart'; |
| import 'package:http/http.dart' as http; |
| import 'package:http/testing.dart'; |
| import 'package:logging/logging.dart'; |
| import 'package:mockito/mockito.dart'; |
| import 'package:retry/retry.dart'; |
| import 'package:test/test.dart'; |
| |
| import '../src/datastore/fake_config.dart'; |
| import '../src/datastore/fake_datastore.dart'; |
| import '../src/request_handling/api_request_handler_tester.dart'; |
| import '../src/request_handling/fake_authentication.dart'; |
| import '../src/service/fake_graphql_client.dart'; |
| import '../src/utilities/mocks.dart'; |
| |
| void main() { |
| const String kGoldenFileLabel = 'will affect goldens'; |
| |
| group('PushGoldStatusToGithub', () { |
| late FakeConfig config; |
| late FakeClientContext clientContext; |
| FakeAuthenticatedContext authContext; |
| late FakeAuthenticationProvider auth; |
| late FakeDatastoreDB db; |
| late ApiRequestHandlerTester tester; |
| late PushGoldStatusToGithub handler; |
| FakeGraphQLClient githubGraphQLClient; |
| List<dynamic> checkRuns = <dynamic>[]; |
| List<dynamic> engineCheckRuns = <dynamic>[]; |
| late MockClient mockHttpClient; |
| late RepositorySlug slug; |
| late RepositorySlug engineSlug; |
| late RetryOptions retryOptions; |
| |
| final List<LogRecord> records = <LogRecord>[]; |
| |
| setUp(() { |
| clientContext = FakeClientContext(); |
| authContext = FakeAuthenticatedContext(clientContext: clientContext); |
| auth = FakeAuthenticationProvider(clientContext: clientContext); |
| githubGraphQLClient = FakeGraphQLClient(); |
| db = FakeDatastoreDB(); |
| config = FakeConfig( |
| dbValue: db, |
| ); |
| tester = ApiRequestHandlerTester(context: authContext); |
| mockHttpClient = MockClient((_) async => http.Response('{}', HttpStatus.ok)); |
| retryOptions = const RetryOptions( |
| delayFactor: Duration(microseconds: 1), |
| maxDelay: Duration(microseconds: 2), |
| maxAttempts: 2, |
| ); |
| |
| githubGraphQLClient.mutateResultForOptions = (MutationOptions options) => createFakeQueryResult(); |
| githubGraphQLClient.queryResultForOptions = (QueryOptions options) { |
| if (options.variables['sRepoName'] == slug.name) { |
| return createGithubQueryResult(checkRuns); |
| } |
| if (options.variables['sRepoName'] == engineSlug.name) { |
| return createGithubQueryResult(engineCheckRuns); |
| } |
| return createGithubQueryResult(<dynamic>[]); |
| }; |
| config.githubGraphQLClient = githubGraphQLClient; |
| config.flutterGoldPendingValue = 'pending'; |
| config.flutterGoldChangesValue = 'changes'; |
| config.flutterGoldSuccessValue = 'success'; |
| config.flutterGoldAlertConstantValue = 'flutter gold alert'; |
| config.flutterGoldInitialAlertValue = 'initial'; |
| config.flutterGoldFollowUpAlertValue = 'follow-up'; |
| config.flutterGoldDraftChangeValue = 'draft'; |
| config.flutterGoldStalePRValue = 'stale'; |
| |
| handler = PushGoldStatusToGithub( |
| config: config, |
| authenticationProvider: auth, |
| datastoreProvider: (DatastoreDB db) { |
| return DatastoreService( |
| db, |
| 5, |
| retryOptions: retryOptions, |
| ); |
| }, |
| goldClient: mockHttpClient, |
| ingestionDelay: Duration.zero, |
| ); |
| |
| slug = RepositorySlug('flutter', 'flutter'); |
| engineSlug = RepositorySlug('flutter', 'engine'); |
| checkRuns.clear(); |
| engineCheckRuns.clear(); |
| records.clear(); |
| log.onRecord.listen((LogRecord record) => records.add(record)); |
| }); |
| |
| group('in development environment', () { |
| setUp(() { |
| clientContext.isDevelopmentEnvironment = true; |
| }); |
| |
| test('Does nothing', () async { |
| config.githubClient = ThrowingGitHub(); |
| db.onCommit = |
| (List<gcloud_db.Model<dynamic>> insert, List<gcloud_db.Key<dynamic>> deletes) => throw AssertionError(); |
| db.addOnQuery<GithubGoldStatusUpdate>((Iterable<GithubGoldStatusUpdate> results) { |
| throw AssertionError(); |
| }); |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| }); |
| }); |
| |
| group('in non-development environment', () { |
| MockGitHub github; |
| MockPullRequestsService pullRequestsService; |
| late MockIssuesService issuesService; |
| late MockRepositoriesService repositoriesService; |
| List<PullRequest> prsFromGitHub = <PullRequest>[]; |
| List<PullRequest> enginePrsFromGitHub = <PullRequest>[]; |
| |
| setUp(() { |
| github = MockGitHub(); |
| pullRequestsService = MockPullRequestsService(); |
| issuesService = MockIssuesService(); |
| repositoriesService = MockRepositoriesService(); |
| when(github.pullRequests).thenReturn(pullRequestsService); |
| when(github.issues).thenReturn(issuesService); |
| when(github.repositories).thenReturn(repositoriesService); |
| |
| prsFromGitHub.clear(); |
| when(pullRequestsService.list(slug)).thenAnswer((Invocation _) { |
| return Stream<PullRequest>.fromIterable(prsFromGitHub); |
| }); |
| |
| enginePrsFromGitHub.clear(); |
| when(pullRequestsService.list(engineSlug)).thenAnswer((Invocation _) { |
| return Stream<PullRequest>.fromIterable(enginePrsFromGitHub); |
| }); |
| |
| when(repositoriesService.createStatus(any, any, any)).thenAnswer((_) async => RepositoryStatus()); |
| when(issuesService.createComment(any, any, any)).thenAnswer((_) async => IssueComment()); |
| when(issuesService.addLabelsToIssue(any, any, any)).thenAnswer((_) async => <IssueLabel>[]); |
| config.githubClient = github; |
| clientContext.isDevelopmentEnvironment = false; |
| }); |
| |
| GithubGoldStatusUpdate newStatusUpdate( |
| RepositorySlug slug, |
| PullRequest pr, |
| String statusUpdate, |
| String sha, |
| String description, |
| ) { |
| return GithubGoldStatusUpdate( |
| key: db.emptyKey.append(GithubGoldStatusUpdate, id: pr.number), |
| status: statusUpdate, |
| pr: pr.number!, |
| head: sha, |
| updates: 0, |
| description: description, |
| repository: slug.fullName, |
| ); |
| } |
| |
| PullRequest newPullRequest(int number, String sha, String baseRef, {bool draft = false, DateTime? updated}) { |
| return PullRequest() |
| ..number = number |
| ..head = (PullRequestHead()..sha = sha) |
| ..base = (PullRequestHead()..ref = baseRef) |
| ..draft = draft |
| ..updatedAt = updated ?? DateTime.now(); |
| } |
| |
| group('does not update GitHub or Datastore', () { |
| setUp(() { |
| db.onCommit = |
| (List<gcloud_db.Model<dynamic>> insert, List<gcloud_db.Key<dynamic>> deletes) => throw AssertionError(); |
| when(repositoriesService.createStatus(any, any, any)).thenThrow(AssertionError()); |
| }); |
| |
| test('if there are no PRs', () async { |
| prsFromGitHub = <PullRequest>[]; |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| }); |
| |
| test('if there are no framework or web engine tests for this PR', () async { |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'tool-test1', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| final PullRequest flutterPr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[flutterPr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, flutterPr, '', '', ''); |
| db.values[status.key] = status; |
| |
| engineCheckRuns = <dynamic>[ |
| <String, String>{'name': 'linux-host1', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| final PullRequest enginePr = newPullRequest(456, 'def', 'main'); |
| enginePrsFromGitHub = <PullRequest>[enginePr]; |
| final GithubGoldStatusUpdate engineStatus = newStatusUpdate(engineSlug, enginePr, '', '', ''); |
| db.values[engineStatus.key] = engineStatus; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| flutterPr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| engineSlug, |
| enginePr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| flutterPr.number!, |
| argThat(contains(config.flutterGoldCommentID(flutterPr))), |
| ), |
| ); |
| verifyNever( |
| issuesService.createComment( |
| engineSlug, |
| enginePr.number!, |
| argThat(contains(config.flutterGoldCommentID(enginePr))), |
| ), |
| ); |
| }); |
| |
| test('if there are no framework tests for this PR, exclude web builds', () async { |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'web-test1', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, pr, '', '', ''); |
| db.values[status.key] = status; |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldCommentID(pr))), |
| ), |
| ); |
| }); |
| |
| test('same commit, checks running, last status running', () async { |
| // Same commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate( |
| slug, |
| pr, |
| GithubGoldStatusUpdate.statusRunning, |
| 'abc', |
| config.flutterGoldPendingValue!, |
| ); |
| db.values[status.key] = status; |
| |
| // Checks still running |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'in_progress', 'conclusion': 'neutral'}, |
| <String, String>{'name': 'web engine', 'status': 'in_progress', 'conclusion': 'neutral'}, |
| ]; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldCommentID(pr))), |
| ), |
| ); |
| }); |
| |
| test('same commit, checks complete, last status complete', () async { |
| // Same commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate( |
| slug, |
| pr, |
| GithubGoldStatusUpdate.statusCompleted, |
| 'abc', |
| config.flutterGoldSuccessValue!, |
| ); |
| db.values[status.key] = status; |
| |
| // Checks complete |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| <String, String>{'name': 'web engine', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldCommentID(pr))), |
| ), |
| ); |
| }); |
| |
| test('same commit, checks complete, last status & gold status is running/awaiting triage, should not comment', |
| () async { |
| // Same commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate( |
| slug, |
| pr, |
| GithubGoldStatusUpdate.statusRunning, |
| 'abc', |
| config.flutterGoldChangesValue!, |
| ); |
| db.values[status.key] = status; |
| |
| final PullRequest enginePr = newPullRequest(456, 'def', 'main'); |
| enginePrsFromGitHub = <PullRequest>[enginePr]; |
| final GithubGoldStatusUpdate engineStatus = newStatusUpdate( |
| engineSlug, |
| enginePr, |
| GithubGoldStatusUpdate.statusRunning, |
| 'def', |
| config.flutterGoldChangesValue!, |
| ); |
| db.values[engineStatus.key] = engineStatus; |
| |
| // Checks complete |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| engineCheckRuns = <dynamic>[ |
| <String, String>{'name': 'web engine', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| // Gold status is running |
| mockHttpClient = MockClient((http.Request request) async { |
| if (request.url.toString() == |
| 'https://flutter-gold.skia.org/json/v1/changelist_summary/github/${pr.number}') { |
| return http.Response(tryjobDigests(pr), HttpStatus.ok); |
| } |
| if (request.url.toString() == |
| 'https://flutter-engine-gold.skia.org/json/v1/changelist_summary/github/${enginePr.number}') { |
| return http.Response(tryjobDigests(enginePr), HttpStatus.ok); |
| } |
| throw const HttpException('Unexpected http request'); |
| }); |
| handler = PushGoldStatusToGithub( |
| config: config, |
| authenticationProvider: auth, |
| datastoreProvider: (DatastoreDB db) { |
| return DatastoreService( |
| config.db, |
| 5, |
| retryOptions: retryOptions, |
| ); |
| }, |
| goldClient: mockHttpClient, |
| ingestionDelay: Duration.zero, |
| ); |
| |
| // Already commented for this commit. |
| when(issuesService.listCommentsByIssue(slug, pr.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = config.flutterGoldCommentID(pr), |
| ), |
| ); |
| when(issuesService.listCommentsByIssue(engineSlug, enginePr.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = config.flutterGoldCommentID(enginePr), |
| ), |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| engineSlug, |
| enginePr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldCommentID(pr))), |
| ), |
| ); |
| verifyNever( |
| issuesService.createComment( |
| engineSlug, |
| enginePr.number!, |
| argThat(contains(config.flutterGoldCommentID(enginePr))), |
| ), |
| ); |
| }); |
| |
| test('does nothing for branches not staged to land on main/master', () async { |
| // New commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'release'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // All checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldCommentID(pr))), |
| ), |
| ); |
| }); |
| |
| test('does not post for draft PRs, does not query Gold', () async { |
| // New commit, draft PR |
| final PullRequest pr = newPullRequest(123, 'abc', 'master', draft: true); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| any, |
| ), |
| ); |
| }); |
| |
| test('does not post for draft PRs, does not query Gold', () async { |
| // New commit, draft PR |
| final PullRequest pr = newPullRequest(123, 'abc', 'master', draft: true); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'Linux', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| any, |
| ), |
| ); |
| }); |
| |
| test('does not post for stale PRs, does not query Gold, stale comment', () async { |
| // New commit, draft PR |
| final PullRequest pr = |
| newPullRequest(123, 'abc', 'master', updated: DateTime.now().subtract(const Duration(days: 30))); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| // Have not already commented for this commit. |
| when(issuesService.listCommentsByIssue(slug, pr.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = 'some other comment', |
| ), |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels, should comment to update |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verify( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldStalePRValue)), |
| ), |
| ).called(1); |
| }); |
| |
| test('will only comment once on stale PRs', () async { |
| // New commit, draft PR |
| final PullRequest pr = |
| newPullRequest(123, 'abc', 'master', updated: DateTime.now().subtract(const Duration(days: 30))); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| // Already commented to update. |
| when(issuesService.listCommentsByIssue(slug, pr.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = config.flutterGoldStalePRValue, |
| ), |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| any, |
| ), |
| ); |
| }); |
| |
| test('will not fire off stale warning for non-framework PRs', () async { |
| // New commit, draft PR |
| final PullRequest pr = |
| newPullRequest(123, 'abc', 'master', updated: DateTime.now().subtract(const Duration(days: 30))); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'tool-test-1', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| // Already commented to update. |
| when(issuesService.listCommentsByIssue(slug, pr.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = 'some other comment', |
| ), |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| any, |
| ), |
| ); |
| }); |
| }); |
| |
| group('updates GitHub and/or Datastore', () { |
| test('new commit, checks running', () async { |
| // New commit |
| final PullRequest flutterPr = newPullRequest(123, 'f-abc', 'master'); |
| prsFromGitHub = <PullRequest>[flutterPr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, flutterPr, '', '', ''); |
| |
| final PullRequest enginePr = newPullRequest(567, 'e-abc', 'main'); |
| enginePrsFromGitHub = <PullRequest>[enginePr]; |
| final GithubGoldStatusUpdate engineStatus = newStatusUpdate(engineSlug, enginePr, '', '', ''); |
| |
| db.values[status.key] = status; |
| db.values[engineStatus.key] = engineStatus; |
| |
| // Checks running |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'in_progress', 'conclusion': 'neutral'}, |
| ]; |
| engineCheckRuns = <dynamic>[ |
| <String, String>{'name': 'web engine', 'status': 'in_progress', 'conclusion': 'neutral'}, |
| ]; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 1); |
| expect(status.status, GithubGoldStatusUpdate.statusRunning); |
| expect(engineStatus.updates, 1); |
| expect(engineStatus.status, GithubGoldStatusUpdate.statusRunning); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| flutterPr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| engineSlug, |
| enginePr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| flutterPr.number!, |
| argThat(contains(config.flutterGoldCommentID(flutterPr))), |
| ), |
| ); |
| verifyNever( |
| issuesService.createComment( |
| engineSlug, |
| enginePr.number!, |
| argThat(contains(config.flutterGoldCommentID(enginePr))), |
| ), |
| ); |
| }); |
| |
| test('includes misc test shards', () async { |
| // New commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'misc', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| // Change detected by Gold |
| mockHttpClient = MockClient((http.Request request) async { |
| if (request.url.toString() == |
| 'https://flutter-gold.skia.org/json/v1/changelist_summary/github/${pr.number}') { |
| return http.Response(tryjobEmpty(), HttpStatus.ok); |
| } |
| throw const HttpException('Unexpected http request'); |
| }); |
| handler = PushGoldStatusToGithub( |
| config: config, |
| authenticationProvider: auth, |
| datastoreProvider: (DatastoreDB db) { |
| return DatastoreService( |
| config.db, |
| 5, |
| retryOptions: retryOptions, |
| ); |
| }, |
| goldClient: mockHttpClient, |
| ingestionDelay: Duration.zero, |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 1); |
| expect(status.status, GithubGoldStatusUpdate.statusCompleted); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not label or comment |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldCommentID(pr))), |
| ), |
| ); |
| }); |
| |
| test('new commit, checks complete, no changes detected', () async { |
| // New commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| // Change detected by Gold |
| mockHttpClient = MockClient((http.Request request) async { |
| if (request.url.toString() == |
| 'https://flutter-gold.skia.org/json/v1/changelist_summary/github/${pr.number}') { |
| return http.Response(tryjobEmpty(), HttpStatus.ok); |
| } |
| throw const HttpException('Unexpected http request'); |
| }); |
| handler = PushGoldStatusToGithub( |
| config: config, |
| authenticationProvider: auth, |
| datastoreProvider: (DatastoreDB db) { |
| return DatastoreService( |
| config.db, |
| 5, |
| retryOptions: retryOptions, |
| ); |
| }, |
| goldClient: mockHttpClient, |
| ingestionDelay: Duration.zero, |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 1); |
| expect(status.status, GithubGoldStatusUpdate.statusCompleted); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not label or comment |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldCommentID(pr))), |
| ), |
| ); |
| }); |
| |
| test('new commit, checks complete, change detected, should comment', () async { |
| // New commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| // Change detected by Gold |
| mockHttpClient = MockClient((http.Request request) async { |
| if (request.url.toString() == |
| 'https://flutter-gold.skia.org/json/v1/changelist_summary/github/${pr.number}') { |
| return http.Response(tryjobDigests(pr), HttpStatus.ok); |
| } |
| throw const HttpException('Unexpected http request'); |
| }); |
| handler = PushGoldStatusToGithub( |
| config: config, |
| authenticationProvider: auth, |
| datastoreProvider: (DatastoreDB db) { |
| return DatastoreService( |
| config.db, |
| 5, |
| retryOptions: retryOptions, |
| ); |
| }, |
| goldClient: mockHttpClient, |
| ingestionDelay: Duration.zero, |
| ); |
| |
| // Have not already commented for this commit. |
| when(issuesService.listCommentsByIssue(slug, pr.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = 'some other comment', |
| ), |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 1); |
| expect(status.status, GithubGoldStatusUpdate.statusRunning); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should label and comment |
| verify( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ).called(1); |
| |
| verify( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldCommentID(pr))), |
| ), |
| ).called(1); |
| }); |
| |
| test('same commit, checks complete, last status was waiting & gold status is needing triage, should comment', |
| () async { |
| // Same commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = |
| newStatusUpdate(slug, pr, GithubGoldStatusUpdate.statusRunning, 'abc', config.flutterGoldPendingValue!); |
| db.values[status.key] = status; |
| |
| // Checks complete |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| // Gold status is running |
| mockHttpClient = MockClient((http.Request request) async { |
| if (request.url.toString() == |
| 'https://flutter-gold.skia.org/json/v1/changelist_summary/github/${pr.number}') { |
| return http.Response(tryjobDigests(pr), HttpStatus.ok); |
| } |
| throw const HttpException('Unexpected http request'); |
| }); |
| handler = PushGoldStatusToGithub( |
| config: config, |
| authenticationProvider: auth, |
| datastoreProvider: (DatastoreDB db) { |
| return DatastoreService( |
| config.db, |
| 5, |
| retryOptions: retryOptions, |
| ); |
| }, |
| goldClient: mockHttpClient, |
| ingestionDelay: Duration.zero, |
| ); |
| |
| // Have not already commented for this commit. |
| when(issuesService.listCommentsByIssue(slug, pr.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = 'some other comment', |
| ), |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 1); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should apply labels and make comment |
| verify( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ).called(1); |
| |
| verify( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldCommentID(pr))), |
| ), |
| ).called(1); |
| }); |
| |
| test('uses shorter comment after first comment to reduce noise', () async { |
| // Same commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = |
| newStatusUpdate(slug, pr, GithubGoldStatusUpdate.statusRunning, 'abc', config.flutterGoldPendingValue!); |
| db.values[status.key] = status; |
| |
| // Checks complete |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'completed': 'in_progress', 'conclusion': 'success'}, |
| ]; |
| |
| // Gold status is running |
| mockHttpClient = MockClient((http.Request request) async { |
| if (request.url.toString() == |
| 'https://flutter-gold.skia.org/json/v1/changelist_summary/github/${pr.number}') { |
| return http.Response(tryjobDigests(pr), HttpStatus.ok); |
| } |
| throw const HttpException('Unexpected http request'); |
| }); |
| handler = PushGoldStatusToGithub( |
| config: config, |
| authenticationProvider: auth, |
| datastoreProvider: (DatastoreDB db) { |
| return DatastoreService( |
| config.db, |
| 5, |
| retryOptions: retryOptions, |
| ); |
| }, |
| goldClient: mockHttpClient, |
| ingestionDelay: Duration.zero, |
| ); |
| |
| // Have not already commented for this commit. |
| when(issuesService.listCommentsByIssue(slug, pr.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = config.flutterGoldInitialAlertValue, |
| ), |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 1); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should apply labels and make comment |
| verify( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ).called(1); |
| |
| verify( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldFollowUpAlertValue)), |
| ), |
| ).called(1); |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldInitialAlertValue)), |
| ), |
| ); |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldAlertConstantValue)), |
| ), |
| ); |
| }); |
| |
| test('same commit, checks complete, new status, should not comment', () async { |
| // Same commit: abc |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = |
| newStatusUpdate(slug, pr, GithubGoldStatusUpdate.statusRunning, 'abc', config.flutterGoldPendingValue!); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| // New status: completed/triaged/no changes |
| mockHttpClient = MockClient((http.Request request) async { |
| if (request.url.toString() == |
| 'https://flutter-gold.skia.org/json/v1/changelist_summary/github/${pr.number}') { |
| return http.Response(tryjobEmpty(), HttpStatus.ok); |
| } |
| throw const HttpException('Unexpected http request'); |
| }); |
| handler = PushGoldStatusToGithub( |
| config: config, |
| authenticationProvider: auth, |
| datastoreProvider: (DatastoreDB db) { |
| return DatastoreService( |
| config.db, |
| 5, |
| retryOptions: retryOptions, |
| ); |
| }, |
| goldClient: mockHttpClient, |
| ingestionDelay: Duration.zero, |
| ); |
| |
| when(issuesService.listCommentsByIssue(slug, pr.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = 'some other comment', |
| ), |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 1); |
| expect(status.status, GithubGoldStatusUpdate.statusCompleted); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not label or comment |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldCommentID(pr))), |
| ), |
| ); |
| }); |
| |
| test('will inform contributor of unresolved check for ATF draft status', () async { |
| // New commit, draft PR |
| final PullRequest pr = newPullRequest(123, 'abc', 'master', draft: true); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = |
| newStatusUpdate(slug, pr, GithubGoldStatusUpdate.statusRunning, 'abc', config.flutterGoldPendingValue!); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| when(issuesService.listCommentsByIssue(slug, pr.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = 'some other comment', |
| ), |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verify( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldDraftChangeValue)), |
| ), |
| ).called(1); |
| }); |
| |
| test('will only inform contributor of unresolved check for ATF draft status once', () async { |
| // New commit, draft PR |
| final PullRequest pr = newPullRequest(123, 'abc', 'master', draft: true); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = |
| newStatusUpdate(slug, pr, GithubGoldStatusUpdate.statusRunning, 'abc', config.flutterGoldPendingValue!); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| when(issuesService.listCommentsByIssue(slug, pr.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = config.flutterGoldDraftChangeValue, |
| ), |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldDraftChangeValue)), |
| ), |
| ); |
| }); |
| |
| test('delivers pending state for failing checks, does not query Gold', () async { |
| // New commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks failed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'failure'}, |
| ]; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 1); |
| expect(status.status, GithubGoldStatusUpdate.statusRunning); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldCommentID(pr))), |
| ), |
| ); |
| }); |
| }); |
| |
| test('Completed pull request does not skip follow-up prs with early return', () async { |
| final PullRequest completedPR = newPullRequest(123, 'abc', 'master'); |
| final PullRequest followUpPR = newPullRequest(456, 'def', 'master'); |
| prsFromGitHub = <PullRequest>[ |
| completedPR, |
| followUpPR, |
| ]; |
| final GithubGoldStatusUpdate completedStatus = newStatusUpdate( |
| slug, |
| completedPR, |
| GithubGoldStatusUpdate.statusCompleted, |
| 'abc', |
| config.flutterGoldSuccessValue!, |
| ); |
| final GithubGoldStatusUpdate followUpStatus = newStatusUpdate(slug, followUpPR, '', '', ''); |
| db.values[completedStatus.key] = completedStatus; |
| db.values[followUpStatus.key] = followUpStatus; |
| |
| // Checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| // New status: completed/triaged/no changes |
| mockHttpClient = MockClient((http.Request request) async { |
| if (request.url.toString() == |
| 'https://flutter-gold.skia.org/json/v1/changelist_summary/github/${completedPR.number}') { |
| return http.Response(tryjobEmpty(), HttpStatus.ok); |
| } |
| if (request.url.toString() == |
| 'https://flutter-gold.skia.org/json/v1/changelist_summary/github/${followUpPR.number}') { |
| return http.Response(tryjobEmpty(), HttpStatus.ok); |
| } |
| throw const HttpException('Unexpected http request'); |
| }); |
| handler = PushGoldStatusToGithub( |
| config: config, |
| authenticationProvider: auth, |
| datastoreProvider: (DatastoreDB db) { |
| return DatastoreService( |
| config.db, |
| 5, |
| retryOptions: retryOptions, |
| ); |
| }, |
| goldClient: mockHttpClient, |
| ingestionDelay: Duration.zero, |
| ); |
| |
| when(issuesService.listCommentsByIssue(slug, completedPR.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = 'some other comment', |
| ), |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(completedStatus.updates, 0); |
| expect(followUpStatus.updates, 1); |
| expect(completedStatus.status, GithubGoldStatusUpdate.statusCompleted); |
| expect(followUpStatus.status, GithubGoldStatusUpdate.statusCompleted); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| }); |
| |
| test('accounts for null status description when parsing for Luci builds', () async { |
| // Same commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate( |
| slug, |
| pr, |
| GithubGoldStatusUpdate.statusRunning, |
| 'abc', |
| config.flutterGoldPendingValue!, |
| ); |
| db.values[status.key] = status; |
| |
| // null status for luci build |
| checkRuns = <dynamic>[ |
| <String, String?>{ |
| 'name': 'framework', |
| 'status': null, |
| 'conclusion': null, |
| } |
| ]; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(records.where((LogRecord record) => record.level == Level.WARNING), isEmpty); |
| expect(records.where((LogRecord record) => record.level == Level.SEVERE), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever( |
| issuesService.addLabelsToIssue( |
| slug, |
| pr.number!, |
| <String>[ |
| kGoldenFileLabel, |
| ], |
| ), |
| ); |
| |
| verifyNever( |
| issuesService.createComment( |
| slug, |
| pr.number!, |
| argThat(contains(config.flutterGoldCommentID(pr))), |
| ), |
| ); |
| }); |
| |
| test('uses the correct Gold endpoint to get status', () async { |
| // New commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(slug, pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| final PullRequest enginePr = newPullRequest(456, 'def', 'main'); |
| enginePrsFromGitHub = <PullRequest>[enginePr]; |
| final GithubGoldStatusUpdate engineStatus = newStatusUpdate(engineSlug, enginePr, '', '', ''); |
| db.values[engineStatus.key] = engineStatus; |
| |
| // Checks completed |
| checkRuns = <dynamic>[ |
| <String, String>{'name': 'framework', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| engineCheckRuns = <dynamic>[ |
| <String, String>{'name': 'web engine', 'status': 'completed', 'conclusion': 'success'}, |
| ]; |
| |
| // Requests sent to Gold. |
| final List<String> goldRequests = <String>[]; |
| mockHttpClient = MockClient((http.Request request) async { |
| final String requestUrl = request.url.toString(); |
| goldRequests.add(requestUrl); |
| |
| final int prNumber = int.parse(requestUrl.split('/').last); |
| final PullRequest requestedPr; |
| if (prNumber == pr.number) { |
| requestedPr = pr; |
| } else if (prNumber == enginePr.number) { |
| requestedPr = enginePr; |
| } else { |
| throw HttpException('Unexpected http request for PR#$prNumber'); |
| } |
| return http.Response(tryjobDigests(requestedPr), HttpStatus.ok); |
| }); |
| handler = PushGoldStatusToGithub( |
| config: config, |
| authenticationProvider: auth, |
| datastoreProvider: (DatastoreDB db) { |
| return DatastoreService( |
| config.db, |
| 5, |
| retryOptions: retryOptions, |
| ); |
| }, |
| goldClient: mockHttpClient, |
| ingestionDelay: Duration.zero, |
| ); |
| |
| // Have not already commented for this commit. |
| when(issuesService.listCommentsByIssue(slug, pr.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = 'some other comment', |
| ), |
| ); |
| when(issuesService.listCommentsByIssue(engineSlug, enginePr.number!)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment()..body = 'some other comment', |
| ), |
| ); |
| |
| await tester.get<Body>(handler); |
| |
| expect(goldRequests, <String>[ |
| 'https://flutter-gold.skia.org/json/v1/changelist_summary/github/${pr.number}', |
| 'https://flutter-engine-gold.skia.org/json/v1/changelist_summary/github/${enginePr.number}', |
| ]); |
| }); |
| }); |
| }); |
| } |
| |
| QueryResult createGithubQueryResult(List<dynamic> statuses) { |
| return createFakeQueryResult( |
| data: <String, dynamic>{ |
| 'repository': <String, dynamic>{ |
| 'pullRequest': <String, dynamic>{ |
| 'commits': <String, dynamic>{ |
| 'nodes': <dynamic>[ |
| <String, dynamic>{ |
| 'commit': <String, dynamic>{ |
| 'checkSuites': <String, dynamic>{ |
| 'nodes': <dynamic>[ |
| <String, dynamic>{ |
| 'checkRuns': <String, dynamic>{'nodes': statuses}, |
| } |
| ], |
| }, |
| }, |
| } |
| ], |
| }, |
| }, |
| }, |
| }, |
| ); |
| } |
| |
| /// JSON response template for Skia Gold empty tryjob status request. |
| String tryjobEmpty() { |
| return ''' |
| { |
| "changelist_id": "123", |
| "patchsets": [ |
| { |
| "new_images": 0, |
| "new_untriaged_images": 0, |
| "total_untriaged_images": 0, |
| "patchset_id": "abc", |
| "patchset_order": 1 |
| } |
| ], |
| "outdated": false |
| } |
| '''; |
| } |
| |
| /// JSON response template for Skia Gold untriaged tryjob status request. |
| String tryjobDigests(PullRequest pr) { |
| return ''' |
| { |
| "changelist_id": "${pr.number!}", |
| "patchsets": [ |
| { |
| "new_images": 1, |
| "new_untriaged_images": 1, |
| "total_untriaged_images": 1, |
| "patchset_id": "${pr.head!.sha!}", |
| "patchset_order": 1 |
| } |
| ], |
| "outdated": false |
| } |
| '''; |
| } |