| // 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:convert'; |
| |
| import 'package:appengine/appengine.dart'; |
| 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:gcloud/db.dart' as gcloud_db; |
| import 'package:gcloud/db.dart'; |
| import 'package:github/github.dart'; |
| import 'package:graphql/client.dart'; |
| import 'package:mockito/mockito.dart'; |
| import 'package:retry/retry.dart'; |
| import 'package:test/test.dart'; |
| |
| import '../src/datastore/fake_cocoon_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/request_handling/fake_logging.dart'; |
| import '../src/service/fake_graphql_client.dart'; |
| import '../src/utilities/mocks.dart'; |
| |
| void main() { |
| group('PushGoldStatusToGithub', () { |
| FakeConfig config; |
| FakeClientContext clientContext; |
| FakeAuthenticatedContext authContext; |
| FakeAuthenticationProvider auth; |
| FakeDatastoreDB db; |
| FakeLogging log; |
| ApiRequestHandlerTester tester; |
| PushGoldStatusToGithub handler; |
| FakeGraphQLClient cirrusGraphQLClient; |
| List<dynamic> statuses = <dynamic>[]; |
| String branch; |
| MockHttpClient mockHttpClient; |
| RepositorySlug slug; |
| RetryOptions retryOptions; |
| |
| setUp(() { |
| clientContext = FakeClientContext(); |
| authContext = FakeAuthenticatedContext(clientContext: clientContext); |
| auth = FakeAuthenticationProvider(clientContext: clientContext); |
| cirrusGraphQLClient = FakeGraphQLClient(); |
| db = FakeDatastoreDB(); |
| config = |
| FakeConfig(cirrusGraphQLClient: cirrusGraphQLClient, dbValue: db); |
| log = FakeLogging(); |
| tester = ApiRequestHandlerTester(context: authContext); |
| mockHttpClient = MockHttpClient(); |
| retryOptions = const RetryOptions( |
| delayFactor: Duration(milliseconds: 1), |
| maxDelay: Duration(milliseconds: 2), |
| maxAttempts: 2, |
| ); |
| handler = PushGoldStatusToGithub( |
| config, |
| auth, |
| datastoreProvider: (DatastoreDB db) { |
| return DatastoreService( |
| config.db, |
| 5, |
| retryOptions: retryOptions, |
| ); |
| }, |
| loggingProvider: () => log, |
| goldClient: mockHttpClient, |
| ); |
| |
| cirrusGraphQLClient.mutateResultForOptions = |
| (MutationOptions options) => QueryResult(); |
| |
| cirrusGraphQLClient.queryResultForOptions = (QueryOptions options) { |
| return createCirrusQueryResult(statuses, branch); |
| }; |
| |
| slug = RepositorySlug('flutter', 'flutter'); |
| statuses.clear(); |
| branch = 'test'; |
| }); |
| |
| group('in development environment', () { |
| setUp(() { |
| clientContext.isDevelopmentEnvironment = true; |
| }); |
| |
| test('Does nothing', () async { |
| config.githubClient = ThrowingGitHub(); |
| db.onCommit = |
| (List<gcloud_db.Model> insert, List<gcloud_db.Key> 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; |
| MockIssuesService issuesService; |
| MockRepositoriesService repositoriesService; |
| List<PullRequest> prsFromGitHub; |
| |
| setUp(() { |
| github = MockGitHub(); |
| pullRequestsService = MockPullRequestsService(); |
| issuesService = MockIssuesService(); |
| repositoriesService = MockRepositoriesService(); |
| when(github.pullRequests).thenReturn(pullRequestsService); |
| when(github.issues).thenReturn(issuesService); |
| when(github.repositories).thenReturn(repositoriesService); |
| when(pullRequestsService.list(any)).thenAnswer((Invocation _) { |
| return Stream<PullRequest>.fromIterable(prsFromGitHub); |
| }); |
| config.githubClient = github; |
| config.goldenBreakingChangeMessageValue = 'goldenBreakingChangeMessage'; |
| clientContext.isDevelopmentEnvironment = false; |
| }); |
| |
| GithubGoldStatusUpdate newStatusUpdate( |
| PullRequest pr, String statusUpdate, String sha, String description) { |
| return GithubGoldStatusUpdate( |
| key: db.emptyKey.append(GithubGoldStatusUpdate), |
| status: statusUpdate, |
| pr: pr.number, |
| head: sha, |
| updates: 0, |
| description: description, |
| repository: 'flutter/flutter', |
| ); |
| } |
| |
| PullRequest newPullRequest(int number, String sha, String baseRef, |
| {bool draft = false}) { |
| return PullRequest() |
| ..number = 123 |
| ..head = (PullRequestHead()..sha = 'abc') |
| ..base = (PullRequestHead()..ref = baseRef) |
| ..draft = draft; |
| } |
| |
| group('does not update GitHub or Datastore', () { |
| setUp(() { |
| db.onCommit = |
| (List<gcloud_db.Model> insert, List<gcloud_db.Key> 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(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| }); |
| |
| test('if there are no framework tests for this PR', () async { |
| statuses = <dynamic>[ |
| <String, String>{'status': 'EXECUTING', 'name': 'tool-test-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'tool-test-2'} |
| ]; |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(pr, '', '', ''); |
| db.values[status.key] = status; |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )); |
| |
| verifyNever(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )); |
| }); |
| |
| 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( |
| pr, |
| GithubGoldStatusUpdate.statusRunning, |
| 'abc', |
| 'This check is waiting for the all clear from Gold.', |
| ); |
| db.values[status.key] = status; |
| |
| // Checks still running |
| when(repositoriesService.listStatuses(slug, pr.head.sha)).thenAnswer( |
| (_) => Stream<RepositoryStatus>.value( |
| RepositoryStatus() |
| ..state = 'pending' |
| ..description = 'Flutter LUCI Build: Linux', |
| ), |
| ); |
| statuses = <dynamic>[ |
| <String, String>{'status': 'EXECUTING', 'name': 'framework-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-2'} |
| ]; |
| branch = 'pull/123'; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )); |
| |
| verifyNever(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )); |
| }); |
| |
| 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( |
| pr, |
| GithubGoldStatusUpdate.statusCompleted, |
| 'abc', |
| 'All golden file tests have passed.'); |
| db.values[status.key] = status; |
| |
| // Checks complete |
| statuses = <dynamic>[ |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-2'} |
| ]; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )); |
| |
| verifyNever(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )); |
| }); |
| |
| test( |
| 'same commit, cirrus checks complete, luci still running, last status running', |
| () async { |
| // Same commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate( |
| pr, |
| GithubGoldStatusUpdate.statusRunning, |
| 'abc', |
| 'This check is waiting for the all clear from Gold.', |
| ); |
| db.values[status.key] = status; |
| |
| // Luci running, Cirrus checks complete |
| when(repositoriesService.listStatuses(slug, pr.head.sha)).thenAnswer( |
| (_) => Stream<RepositoryStatus>.value( |
| RepositoryStatus() |
| ..state = 'pending' |
| ..description = 'Flutter LUCI Build: Linux', |
| ), |
| ); |
| statuses = <dynamic>[ |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-2'} |
| ]; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )); |
| |
| verifyNever(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )); |
| }); |
| |
| 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( |
| pr, |
| GithubGoldStatusUpdate.statusRunning, |
| 'abc', |
| 'Image changes have been found for ' |
| 'this pull request. Visit https://flutter-gold.skia.org/changelists ' |
| 'to view and triage (e.g. because this is an intentional change).'); |
| db.values[status.key] = status; |
| |
| // Checks complete |
| statuses = <dynamic>[ |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-2'} |
| ]; |
| |
| // Gold status is running |
| final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest(); |
| final MockHttpClientResponse mockHttpResponse = |
| MockHttpClientResponse(utf8.encode(tryjobDigests())); |
| when(mockHttpClient.getUrl(Uri.parse( |
| 'http://flutter-gold.skia.org/json/changelist/github/${pr.number}/${pr.head.sha}/untriaged'))) |
| .thenAnswer( |
| (_) => Future<MockHttpClientRequest>.value(mockHttpRequest)); |
| when(mockHttpRequest.close()).thenAnswer( |
| (_) => Future<MockHttpClientResponse>.value(mockHttpResponse)); |
| |
| // Already commented for this commit. |
| when(issuesService.listCommentsByIssue(slug, pr.number)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment() |
| ..body = 'Changes reported for pull request ' |
| '#${pr.number} at sha ${pr.head.sha}', |
| ), |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )); |
| |
| verifyNever(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )); |
| }); |
| |
| test('does nothing for branches not staged to land on master', |
| () async { |
| // New commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'release'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // All checks completed |
| when(repositoriesService.listStatuses(slug, pr.head.sha)).thenAnswer( |
| (_) => Stream<RepositoryStatus>.value( |
| RepositoryStatus() |
| ..state = 'success' |
| ..description = 'Flutter LUCI Build: Linux', |
| ), |
| ); |
| statuses = <dynamic>[ |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-2'} |
| ]; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )); |
| |
| verifyNever(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )); |
| }); |
| }); |
| |
| group('updates GitHub and Datastore', () { |
| test('new commit, checks running', () async { |
| // New commit |
| final PullRequest pr = newPullRequest(123, 'abc', 'master'); |
| prsFromGitHub = <PullRequest>[pr]; |
| final GithubGoldStatusUpdate status = newStatusUpdate(pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks running |
| when(repositoriesService.listStatuses(slug, pr.head.sha)).thenAnswer( |
| (_) => Stream<RepositoryStatus>.value( |
| RepositoryStatus() |
| ..state = 'pending' |
| ..description = 'Flutter LUCI Build: Linux', |
| ), |
| ); |
| statuses = <dynamic>[ |
| <String, String>{'status': 'EXECUTING', 'name': 'framework-1'}, |
| <String, String>{'status': 'EXECUTING', 'name': 'framework-2'} |
| ]; |
| branch = 'pull/123'; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 1); |
| expect(status.status, GithubGoldStatusUpdate.statusRunning); |
| expect(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )); |
| |
| verifyNever(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )); |
| }); |
| |
| 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(pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| when(repositoriesService.listStatuses(slug, pr.head.sha)).thenAnswer( |
| (_) => Stream<RepositoryStatus>.value( |
| RepositoryStatus() |
| ..state = 'success' |
| ..description = 'Flutter LUCI Build: Linux', |
| ), |
| ); |
| statuses = <dynamic>[ |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-2'} |
| ]; |
| branch = 'pull/123'; |
| |
| // Change detected by Gold |
| final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest(); |
| final MockHttpClientResponse mockHttpResponse = |
| MockHttpClientResponse(utf8.encode(tryjobEmpty())); |
| when(mockHttpClient.getUrl(Uri.parse( |
| 'http://flutter-gold.skia.org/json/changelist/github/${pr.number}/${pr.head.sha}/untriaged'))) |
| .thenAnswer( |
| (_) => Future<MockHttpClientRequest>.value(mockHttpRequest)); |
| when(mockHttpRequest.close()).thenAnswer( |
| (_) => Future<MockHttpClientResponse>.value(mockHttpResponse)); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 1); |
| expect(status.status, GithubGoldStatusUpdate.statusCompleted); |
| expect(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should not label or comment |
| verifyNever(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )); |
| |
| verifyNever(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )); |
| }); |
| |
| 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(pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| when(repositoriesService.listStatuses(slug, pr.head.sha)).thenAnswer( |
| (_) => Stream<RepositoryStatus>.value( |
| RepositoryStatus() |
| ..state = 'success' |
| ..description = 'Flutter LUCI Build: Linux', |
| ), |
| ); |
| statuses = <dynamic>[ |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-2'} |
| ]; |
| branch = 'pull/123'; |
| |
| // Change detected by Gold |
| final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest(); |
| final MockHttpClientResponse mockHttpResponse = |
| MockHttpClientResponse(utf8.encode(tryjobDigests())); |
| when(mockHttpClient.getUrl(Uri.parse( |
| 'http://flutter-gold.skia.org/json/changelist/github/${pr.number}/${pr.head.sha}/untriaged'))) |
| .thenAnswer( |
| (_) => Future<MockHttpClientRequest>.value(mockHttpRequest)); |
| when(mockHttpRequest.close()).thenAnswer( |
| (_) => Future<MockHttpClientResponse>.value(mockHttpResponse)); |
| |
| // 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(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should label and comment |
| verify(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )).called(1); |
| |
| verify(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )).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( |
| pr, |
| GithubGoldStatusUpdate.statusRunning, |
| 'abc', |
| 'This check is waiting for all other checks to be completed.'); |
| db.values[status.key] = status; |
| |
| // Checks complete |
| when(repositoriesService.listStatuses(slug, pr.head.sha)).thenAnswer( |
| (_) => Stream<RepositoryStatus>.value( |
| RepositoryStatus() |
| ..state = 'success' |
| ..description = 'Flutter LUCI Build: Linux', |
| ), |
| ); |
| statuses = <dynamic>[ |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-2'} |
| ]; |
| branch = 'pull/123'; |
| |
| // Gold status is running |
| final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest(); |
| final MockHttpClientResponse mockHttpResponse = |
| MockHttpClientResponse(utf8.encode(tryjobDigests())); |
| when(mockHttpClient.getUrl(Uri.parse( |
| 'http://flutter-gold.skia.org/json/changelist/github/${pr.number}/${pr.head.sha}/untriaged'))) |
| .thenAnswer( |
| (_) => Future<MockHttpClientRequest>.value(mockHttpRequest)); |
| when(mockHttpRequest.close()).thenAnswer( |
| (_) => Future<MockHttpClientResponse>.value(mockHttpResponse)); |
| |
| // 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(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should apply labels and make comment |
| verify(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )).called(1); |
| |
| verify(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )).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( |
| pr, |
| GithubGoldStatusUpdate.statusRunning, |
| 'abc', |
| 'This check is waiting for all other checks to be completed.'); |
| db.values[status.key] = status; |
| |
| // Checks complete |
| when(repositoriesService.listStatuses(slug, pr.head.sha)).thenAnswer( |
| (_) => Stream<RepositoryStatus>.value( |
| RepositoryStatus() |
| ..state = 'success' |
| ..description = 'Flutter LUCI Build: Linux', |
| ), |
| ); |
| statuses = <dynamic>[ |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-2'} |
| ]; |
| branch = 'pull/123'; |
| |
| // Gold status is running |
| final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest(); |
| final MockHttpClientResponse mockHttpResponse = |
| MockHttpClientResponse(utf8.encode(tryjobDigests())); |
| when(mockHttpClient.getUrl(Uri.parse( |
| 'http://flutter-gold.skia.org/json/changelist/github/${pr.number}/${pr.head.sha}/untriaged'))) |
| .thenAnswer( |
| (_) => Future<MockHttpClientRequest>.value(mockHttpRequest)); |
| when(mockHttpRequest.close()).thenAnswer( |
| (_) => Future<MockHttpClientResponse>.value(mockHttpResponse)); |
| |
| // Have not already commented for this commit. |
| when(issuesService.listCommentsByIssue(slug, pr.number)).thenAnswer( |
| (_) => Stream<IssueComment>.value( |
| IssueComment() |
| ..body = |
| 'Golden file changes have been found for this pull request.', |
| ), |
| ); |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 1); |
| expect(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should apply labels and make comment |
| verify(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )).called(1); |
| |
| verify(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains( |
| 'Golden file changes are available for triage from new commit,')), |
| )).called(1); |
| }); |
| |
| 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( |
| pr, |
| GithubGoldStatusUpdate.statusRunning, |
| 'abc', |
| 'This check is waiting for all other checks to be completed.'); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| when(repositoriesService.listStatuses(slug, pr.head.sha)).thenAnswer( |
| (_) => Stream<RepositoryStatus>.value( |
| RepositoryStatus() |
| ..state = 'success' |
| ..description = 'Flutter LUCI Build: Linux', |
| ), |
| ); |
| statuses = <dynamic>[ |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-2'} |
| ]; |
| branch = 'pull/123'; |
| |
| // New status: completed/triaged/no changes |
| final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest(); |
| final MockHttpClientResponse mockHttpResponse = |
| MockHttpClientResponse(utf8.encode(tryjobEmpty())); |
| when(mockHttpClient.getUrl(Uri.parse( |
| 'http://flutter-gold.skia.org/json/changelist/github/${pr.number}/${pr.head.sha}/untriaged'))) |
| .thenAnswer( |
| (_) => Future<MockHttpClientRequest>.value(mockHttpRequest)); |
| when(mockHttpRequest.close()).thenAnswer( |
| (_) => Future<MockHttpClientResponse>.value(mockHttpResponse)); |
| |
| 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(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should not label or comment |
| verifyNever(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )); |
| |
| verifyNever(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )); |
| }); |
| |
| test('delivers pending state 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(pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks completed |
| when(repositoriesService.listStatuses(slug, pr.head.sha)).thenAnswer( |
| (_) => Stream<RepositoryStatus>.value( |
| RepositoryStatus() |
| ..state = 'success' |
| ..description = 'Flutter LUCI Build: Linux', |
| ), |
| ); |
| statuses = <dynamic>[ |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-2'} |
| ]; |
| branch = 'pull/123'; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 1); |
| expect(status.status, GithubGoldStatusUpdate.statusRunning); |
| expect(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )); |
| |
| verifyNever(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )); |
| }); |
| |
| 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(pr, '', '', ''); |
| db.values[status.key] = status; |
| |
| // Checks failed |
| when(repositoriesService.listStatuses(slug, pr.head.sha)).thenAnswer( |
| (_) => Stream<RepositoryStatus>.value( |
| RepositoryStatus() |
| ..state = 'failed' |
| ..description = 'Flutter LUCI Build: Linux', |
| ), |
| ); |
| statuses = <dynamic>[ |
| <String, String>{'status': 'FAILED', 'name': 'framework-1'}, |
| <String, String>{'status': 'ABORTED', 'name': 'framework-2'} |
| ]; |
| branch = 'pull/123'; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 1); |
| expect(status.status, GithubGoldStatusUpdate.statusRunning); |
| expect(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )); |
| |
| verifyNever(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )); |
| }); |
| }); |
| |
| 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( |
| completedPR, |
| GithubGoldStatusUpdate.statusCompleted, |
| 'abc', |
| 'All golden file tests have passed'); |
| final GithubGoldStatusUpdate followUpStatus = |
| newStatusUpdate(followUpPR, '', '', ''); |
| db.values[completedStatus.key] = completedStatus; |
| db.values[followUpStatus.key] = followUpStatus; |
| |
| // Checks completed |
| when(repositoriesService.listStatuses(slug, any)).thenAnswer( |
| (_) => Stream<RepositoryStatus>.value( |
| RepositoryStatus() |
| ..state = 'success' |
| ..description = 'Flutter LUCI Build: Linux', |
| ), |
| ); |
| statuses = <dynamic>[ |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-2'} |
| ]; |
| branch = 'pull/123'; |
| |
| // New status: completed/triaged/no changes |
| final MockHttpClientRequest mockHttpRequest = MockHttpClientRequest(); |
| final MockHttpClientResponse mockHttpResponse = |
| MockHttpClientResponse(utf8.encode(tryjobEmpty())); |
| when(mockHttpClient.getUrl(Uri.parse( |
| 'http://flutter-gold.skia.org/json/changelist/github/${completedPR.number}/${completedPR.head.sha}/untriaged'))) |
| .thenAnswer( |
| (_) => Future<MockHttpClientRequest>.value(mockHttpRequest)); |
| when(mockHttpClient.getUrl(Uri.parse( |
| 'http://flutter-gold.skia.org/json/changelist/github/${followUpPR.number}/${followUpPR.head.sha}/untriaged'))) |
| .thenAnswer( |
| (_) => Future<MockHttpClientRequest>.value(mockHttpRequest)); |
| when(mockHttpRequest.close()).thenAnswer( |
| (_) => Future<MockHttpClientResponse>.value(mockHttpResponse)); |
| |
| 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(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), 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( |
| pr, |
| GithubGoldStatusUpdate.statusRunning, |
| 'abc', |
| 'This check is waiting for the all clear from Gold.', |
| ); |
| db.values[status.key] = status; |
| |
| // Luci running, Cirrus checks complete |
| when(repositoriesService.listStatuses(slug, pr.head.sha)).thenAnswer( |
| (_) => Stream<RepositoryStatus>.value( |
| RepositoryStatus()..state = 'pending', |
| ), |
| ); |
| statuses = <dynamic>[ |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-1'}, |
| <String, String>{'status': 'COMPLETED', 'name': 'framework-2'} |
| ]; |
| |
| final Body body = await tester.get<Body>(handler); |
| expect(body, same(Body.empty)); |
| expect(status.updates, 0); |
| expect(log.records.where(hasLevel(LogLevel.WARNING)), isEmpty); |
| expect(log.records.where(hasLevel(LogLevel.ERROR)), isEmpty); |
| |
| // Should not apply labels or make comments |
| verifyNever(issuesService.addLabelsToIssue( |
| slug, |
| pr.number, |
| <String>[ |
| 'will affect goldens', |
| 'severe: API break', |
| ], |
| )); |
| |
| verifyNever(issuesService.createComment( |
| slug, |
| pr.number, |
| argThat(contains(config.goldenBreakingChangeMessageValue)), |
| )); |
| }); |
| }); |
| }); |
| } |
| |
| QueryResult createCirrusQueryResult(List<dynamic> statuses, String branch) { |
| assert(statuses != null); |
| |
| return QueryResult( |
| data: <String, dynamic>{ |
| 'searchBuilds': <dynamic>[ |
| <String, dynamic>{ |
| 'id': '1', |
| 'branch': branch, |
| 'latestGroupTasks': <dynamic>[ |
| <String, dynamic>{ |
| 'id': '1', |
| 'name': statuses.first['name'], |
| 'status': statuses.first['status'] |
| }, |
| <String, dynamic>{ |
| 'id': '2', |
| 'name': statuses.last['name'], |
| 'status': statuses.last['status'] |
| } |
| ], |
| } |
| ], |
| }, |
| ); |
| } |
| |
| /// JSON response template for Skia Gold empty tryjob status request. |
| String tryjobEmpty() { |
| return ''' |
| { |
| "digests": null |
| } |
| '''; |
| } |
| |
| /// JSON response template for Skia Gold empty tryjob status request. |
| String tryjobDigests() { |
| return ''' |
| { |
| "digests": [ |
| "abcd", |
| "efgh", |
| "ijkl" |
| ] |
| } |
| '''; |
| } |