blob: c6706b50466341d423d14ceebcd61356ab446923 [file] [log] [blame]
// 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"
]
}
''';
}