Add checks api funtionality to cocoon. (#811)
* Use dart format instead of flutter format.
* Address code review comments and sync changes landed on different PRs.
* Fix analyzer errors.
* Log when not all the params to update status are available.
diff --git a/app_dart/bin/server.dart b/app_dart/bin/server.dart
index 55b991a..c44360d 100644
--- a/app_dart/bin/server.dart
+++ b/app_dart/bin/server.dart
@@ -7,6 +7,7 @@
import 'package:appengine/appengine.dart';
import 'package:cocoon_service/cocoon_service.dart';
import 'package:cocoon_service/src/model/appengine/service_account_info.dart';
+import 'package:cocoon_service/src/service/github_checks_service.dart';
import 'package:cocoon_service/src/service/github_status_service.dart';
import 'package:cocoon_service/src/service/luci_build_service.dart';
import 'package:gcloud/db.dart';
@@ -42,6 +43,11 @@
luciBuildService,
);
+ /// Github checks api service used to provide luci test execution status on the Github UI.
+ final GithubChecksService githubChecksService = GithubChecksService(
+ config,
+ );
+
final Map<String, RequestHandler<dynamic>> handlers =
<String, RequestHandler<dynamic>>{
'/api/append-log': AppendLog(config, authProvider),
@@ -57,6 +63,7 @@
buildBucketClient,
luciBuildService,
githubStatusService,
+ githubChecksService,
),
'/api/luci-status-handler': LuciStatusHandler(config, buildBucketClient),
'/api/push-build-status-to-github':
diff --git a/app_dart/lib/src/request_handlers/github_webhook.dart b/app_dart/lib/src/request_handlers/github_webhook.dart
index 76f8367..930ec94 100644
--- a/app_dart/lib/src/request_handlers/github_webhook.dart
+++ b/app_dart/lib/src/request_handlers/github_webhook.dart
@@ -6,6 +6,8 @@
import 'dart:convert';
import 'dart:io';
+import 'package:cocoon_service/src/model/github/checks.dart';
+import 'package:cocoon_service/src/service/github_checks_service.dart';
import 'package:cocoon_service/src/service/github_status_service.dart';
import 'package:cocoon_service/src/service/luci_build_service.dart';
import 'package:crypto/crypto.dart';
@@ -37,7 +39,7 @@
@immutable
class GithubWebhook extends RequestHandler<Body> {
GithubWebhook(Config config, this.buildBucketClient, this.luciBuildService,
- this.githubStatusService,
+ this.githubStatusService, this.githubChecksService,
{HttpClient skiaClient})
: assert(buildBucketClient != null),
skiaClient = skiaClient ?? HttpClient(),
@@ -56,6 +58,9 @@
/// LUCI service class to communicate with buildBucket service.
final LuciBuildService luciBuildService;
+ /// Github checks service. Used to provide build status to github.
+ final GithubChecksService githubChecksService;
+
@override
Future<Body> post() async {
final String gitHubEvent = request.headers.value('X-GitHub-Event');
@@ -63,7 +68,6 @@
request.headers.value('X-Hub-Signature') == null) {
throw const BadRequestException('Missing required headers.');
}
-
final List<int> requestBytes = await request.expand((_) => _).toList();
final String hmacSignature = request.headers.value('X-Hub-Signature');
if (!await _validateRequest(hmacSignature, requestBytes)) {
@@ -76,6 +80,19 @@
case 'pull_request':
await _handlePullRequest(stringRequest);
break;
+ case 'check_suite':
+ final CheckSuiteEvent checkSuiteEvent = CheckSuiteEvent.fromJson(
+ jsonDecode(stringRequest) as Map<String, dynamic>,
+ );
+ await githubChecksService.handleCheckSuite(
+ checkSuiteEvent, luciBuildService);
+ break;
+ case 'check_run':
+ final CheckRunEvent checkRunEvent = CheckRunEvent.fromJson(
+ jsonDecode(stringRequest) as Map<String, dynamic>,
+ );
+ await githubChecksService.handleCheckRun(
+ checkRunEvent, luciBuildService);
}
return Body.empty;
diff --git a/app_dart/lib/src/request_handlers/luci_status.dart b/app_dart/lib/src/request_handlers/luci_status.dart
index a1444ee..faef14d 100644
--- a/app_dart/lib/src/request_handlers/luci_status.dart
+++ b/app_dart/lib/src/request_handlers/luci_status.dart
@@ -5,9 +5,9 @@
import 'dart:convert';
import 'dart:io';
-import 'package:appengine/appengine.dart';
import 'package:cocoon_service/src/foundation/providers.dart';
import 'package:cocoon_service/src/service/buildbucket.dart';
+import 'package:cocoon_service/src/service/github_checks_service.dart';
import 'package:cocoon_service/src/service/github_status_service.dart';
import 'package:cocoon_service/src/service/luci_build_service.dart';
import 'package:github/github.dart';
@@ -61,9 +61,13 @@
await config.deviceLabServiceAccount;
final LuciBuildService luciBuildService =
LuciBuildService(config, buildBucketClient, serviceAccountInfo);
- final Logging log = loggingProvider();
final GithubStatusService githubStatusService =
GithubStatusService(config, luciBuildService);
+ final GithubChecksService githubChecksService = GithubChecksService(config);
+
+ // Set logger in all the service classes.
+ luciBuildService.setLogger(log);
+ githubChecksService.setLogger(log);
if (!await _authenticateRequest(request.headers)) {
throw const Unauthorized();
@@ -90,6 +94,11 @@
.firstWhere((String tag) => tag.startsWith(shaPrefix))
.substring(shaPrefix.length);
log.debug('Setting status: ${buildPushMessage.toJson()} for $builderName');
+ await githubChecksService.updateCheckStatus(
+ buildPushMessage,
+ luciBuildService,
+ slug,
+ );
switch (buildPushMessage.build.status) {
case Status.completed:
await _markCompleted(
diff --git a/app_dart/lib/src/service/github_checks_service.dart b/app_dart/lib/src/service/github_checks_service.dart
new file mode 100644
index 0000000..fd7c915
--- /dev/null
+++ b/app_dart/lib/src/service/github_checks_service.dart
@@ -0,0 +1,191 @@
+// 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:convert';
+
+import 'package:appengine/appengine.dart';
+import 'package:cocoon_service/src/foundation/github_checks_util.dart';
+import 'package:cocoon_service/src/model/github/checks.dart';
+import 'package:cocoon_service/src/model/luci/buildbucket.dart';
+import 'package:github/github.dart' as github;
+
+import '../../cocoon_service.dart';
+import '../model/luci/push_message.dart' as push_message;
+import 'luci_build_service.dart';
+
+/// Controls triggering builds and updating their status in the Github UI.
+class GithubChecksService {
+ GithubChecksService(this.config, {GithubChecksUtil githubChecksUtil})
+ : githubChecksUtil = githubChecksUtil ?? const GithubChecksUtil();
+
+ Config config;
+ GithubChecksUtil githubChecksUtil;
+ Logging log;
+
+ static Set<github.CheckRunConclusion> failedStatesSet =
+ <github.CheckRunConclusion>{
+ github.CheckRunConclusion.cancelled,
+ github.CheckRunConclusion.failure,
+ };
+
+ // This method has to be called before calling any other methods.
+ void setLogger(Logging log) {
+ this.log = log;
+ }
+
+ /// Takes a [CheckSuiteEvent] and trigger all the relevant builds if this is a
+ /// new commit or only failed builds if the event was generated by a click on
+ /// the re-run all button in the Github UI.
+ /// Relevant API docs:
+ /// https://docs.github.com/en/rest/reference/checks#create-a-check-suite
+ /// https://docs.github.com/en/rest/reference/checks#rerequest-a-check-suite
+ Future<void> handleCheckSuite(CheckSuiteEvent checkSuiteEvent,
+ LuciBuildService luciBuilderService) async {
+ final github.RepositorySlug slug = checkSuiteEvent.repository.slug();
+ final github.GitHub gitHubClient =
+ await config.createGitHubClient(slug.owner, slug.name);
+ final github.PullRequest pullRequest =
+ checkSuiteEvent.checkSuite.pullRequests[0];
+ final int pullRequestNumber = pullRequest.number;
+ final String commitSha = checkSuiteEvent.checkSuite.headSha;
+ switch (checkSuiteEvent.action) {
+ case 'requested':
+ // Trigger all try builders.
+ await luciBuilderService.scheduleBuilds(
+ prNumber: pullRequestNumber,
+ commitSha: commitSha,
+ slug: checkSuiteEvent.repository.slug(),
+ checkSuiteEvent: checkSuiteEvent,
+ );
+ break;
+
+ case 'rerequested':
+ // Trigger only the builds that failed.
+ final List<Build> builds = await luciBuilderService.failedBuilds(
+ slug, pullRequestNumber, commitSha);
+ final Map<String, github.CheckRun> checkRuns =
+ await githubChecksUtil.allCheckRuns(
+ gitHubClient,
+ checkSuiteEvent,
+ );
+
+ for (Build build in builds) {
+ final github.CheckRun checkRun = checkRuns[build.builderId.builder];
+ await luciBuilderService.rescheduleUsingCheckSuiteEvent(
+ checkSuiteEvent,
+ checkRun,
+ );
+ }
+ break;
+ }
+ }
+
+ /// Reschedules a failed build using a [CheckRunEvent]. The CheckRunEvent is
+ /// generated when someone clicks the re-run button from a failed build from
+ /// the Github UI.
+ /// Relevant APIs:
+ /// https://developer.github.com/v3/checks/runs/#check-runs-and-requested-actions
+ Future<void> handleCheckRun(
+ CheckRunEvent checkRunEvent, LuciBuildService luciBuildService) async {
+ switch (checkRunEvent.action) {
+ case 'rerequested':
+ final String builderName = checkRunEvent.checkRun.name;
+ final bool success =
+ await luciBuildService.rescheduleUsingCheckRunEvent(checkRunEvent);
+ log.debug('BuilderName: $builderName State: $success');
+ }
+ }
+
+ /// Updates the Github build status using a [BuildPushMessage] sent by LUCI in
+ /// a pub/sub notification.
+ /// Relevant APIs:
+ /// https://docs.github.com/en/rest/reference/checks#update-a-check-run
+ Future<bool> updateCheckStatus(
+ push_message.BuildPushMessage buildPushMessage,
+ LuciBuildService luciBuildService,
+ github.RepositorySlug slug,
+ ) async {
+ final github.GitHub gitHubClient =
+ await config.createGitHubClient(slug.owner, slug.name);
+ final push_message.Build build = buildPushMessage.build;
+ if (buildPushMessage.userData.isEmpty) {
+ return false;
+ }
+ final Map<String, dynamic> userData =
+ jsonDecode(buildPushMessage.userData) as Map<String, dynamic>;
+ if (!userData.containsKey('check_run_id') ||
+ !userData.containsKey('repo_owner') ||
+ !userData.containsKey('repo_name')) {
+ log.error(
+ 'UserData did not contain check_run_id,'
+ 'repo_owner, or repo_name: $userData',
+ );
+ return false;
+ }
+ final github.CheckRun checkRun = await githubChecksUtil.getCheckRun(
+ gitHubClient,
+ slug,
+ userData['check_run_id'] as int,
+ );
+ final github.CheckRunStatus status = statusForResult(build.status);
+ final github.CheckRunConclusion conclusion =
+ (buildPushMessage.build.result != null)
+ ? conclusionForResult(buildPushMessage.build.result)
+ : null;
+ // Do not override url for completed status.
+ final String url = status == github.CheckRunStatus.completed
+ ? checkRun.detailsUrl
+ : buildPushMessage.build.url;
+ github.CheckRunOutput output;
+ // If status has completed with failure then provide more details.
+ if (status == github.CheckRunStatus.completed &&
+ failedStatesSet.contains(conclusion)) {
+ final Build build = await luciBuildService.getBuildById(
+ buildPushMessage.build.id,
+ fields: 'id,builder,summaryMarkdown');
+ output = github.CheckRunOutput(
+ title: checkRun.name, summary: build.summaryMarkdown);
+ }
+ await githubChecksUtil.updateCheckRun(
+ gitHubClient,
+ slug,
+ checkRun,
+ status: status,
+ conclusion: conclusion,
+ detailsUrl: url,
+ output: output,
+ );
+ return true;
+ }
+
+ /// Transforms a [push_message.Result] to a [github.CheckRunConclusion].
+ /// Relevant APIs:
+ /// https://developer.github.com/v3/checks/runs/#check-runs
+ github.CheckRunConclusion conclusionForResult(push_message.Result result) {
+ switch (result) {
+ case push_message.Result.canceled:
+ return github.CheckRunConclusion.cancelled;
+ case push_message.Result.failure:
+ return github.CheckRunConclusion.failure;
+ case push_message.Result.success:
+ return github.CheckRunConclusion.success;
+ }
+ throw StateError('unreachable');
+ }
+
+ /// Transforms a [ush_message.Status] to a [github.CheckRunStatus].
+ /// Relevant APIs:
+ /// https://developer.github.com/v3/checks/runs/#check-runs
+ github.CheckRunStatus statusForResult(push_message.Status status) {
+ switch (status) {
+ case push_message.Status.completed:
+ return github.CheckRunStatus.completed;
+ case push_message.Status.scheduled:
+ return github.CheckRunStatus.queued;
+ case push_message.Status.started:
+ return github.CheckRunStatus.inProgress;
+ }
+ throw StateError('unreachable');
+ }
+}
diff --git a/app_dart/test/model/github/checks_test_data.dart b/app_dart/test/model/github/checks_test_data.dart
index 5acee14..331e10f 100644
--- a/app_dart/test/model/github/checks_test_data.dart
+++ b/app_dart/test/model/github/checks_test_data.dart
@@ -4,9 +4,11 @@
/// Json messages as dart string used for checks model tests.
-const String checkSuiteString = '''\
+String checkSuiteString = checkSuiteTemplate('requested');
+
+String checkSuiteTemplate(String action) => '''\
{
- "action": "requested",
+ "action": "$action",
"check_suite": {
"id": 694267587,
"node_id": "MDEwOkNoZWNrU3VpdGU2OTQyNjc1ODc=",
diff --git a/app_dart/test/request_handlers/github_webhook_test.dart b/app_dart/test/request_handlers/github_webhook_test.dart
index b092c9e..13406b8 100644
--- a/app_dart/test/request_handlers/github_webhook_test.dart
+++ b/app_dart/test/request_handlers/github_webhook_test.dart
@@ -18,6 +18,7 @@
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
+import '../model/github/checks_test_data.dart';
import '../src/datastore/fake_cocoon_config.dart';
import '../src/request_handling/fake_http.dart';
import '../src/request_handling/request_handler_tester.dart';
@@ -29,7 +30,7 @@
FakeHttpRequest request;
FakeConfig config;
- MockGitHubClient gitHubClient;
+ MockGitHub gitHubClient;
MockIssuesService issuesService;
MockPullRequestsService pullRequestsService;
MockBuildBucketClient mockBuildBucketClient;
@@ -39,6 +40,7 @@
LuciBuildService luciBuildService;
ServiceAccountInfo serviceAccountInfo;
GithubStatusService githubStatusService;
+ MockGithubChecksService mockGithubChecksService;
const String keyString = 'not_a_real_key';
@@ -53,7 +55,7 @@
serviceAccountInfo = const ServiceAccountInfo(email: serviceAccountEmail);
request = FakeHttpRequest();
config = FakeConfig(deviceLabServiceAccountValue: serviceAccountInfo);
- gitHubClient = MockGitHubClient();
+ gitHubClient = MockGitHub();
mockHttpClient = MockHttpClient();
issuesService = MockIssuesService();
pullRequestsService = MockPullRequestsService();
@@ -73,8 +75,10 @@
luciBuildService,
);
- webhook = GithubWebhook(
- config, mockBuildBucketClient, luciBuildService, githubStatusService,
+ mockGithubChecksService = MockGithubChecksService();
+
+ webhook = GithubWebhook(config, mockBuildBucketClient, luciBuildService,
+ githubStatusService, mockGithubChecksService,
skiaClient: mockHttpClient);
when(gitHubClient.issues).thenReturn(issuesService);
@@ -1497,6 +1501,29 @@
);
});
});
+ group('checksAPI', () {
+ void _generateRequest(String bodyString) {
+ request.body = bodyString;
+ final Uint8List body = utf8.encode(request.body) as Uint8List;
+ final Uint8List key = utf8.encode(keyString) as Uint8List;
+ final String hmac = getHmac(body, key);
+ request.headers.set('X-Hub-Signature', 'sha1=$hmac');
+ }
+
+ test('CheckSuite Event is delegated to GithubChecksService', () async {
+ _generateRequest(checkSuiteString);
+ request.headers.set('X-GitHub-Event', 'check_suite');
+ await tester.post(webhook);
+ verify(mockGithubChecksService.handleCheckSuite(any, any)).called(1);
+ });
+
+ test('CheckRun Event is delegated to GithubChecksService', () async {
+ _generateRequest(checkRunString);
+ request.headers.set('X-GitHub-Event', 'check_run');
+ await tester.post(webhook);
+ verify(mockGithubChecksService.handleCheckRun(any, any)).called(1);
+ });
+ });
});
}
diff --git a/app_dart/test/request_handlers/push_engine_status_to_github_test.dart b/app_dart/test/request_handlers/push_engine_status_to_github_test.dart
index 7edb895..5884e94 100644
--- a/app_dart/test/request_handlers/push_engine_status_to_github_test.dart
+++ b/app_dart/test/request_handlers/push_engine_status_to_github_test.dart
@@ -134,8 +134,6 @@
// ignore: must_be_immutable
class MockLuciService extends Mock implements LuciService {}
-class MockGitHub extends Mock implements GitHub {}
-
class MockIssuesService extends Mock implements IssuesService {}
class MockPullRequestsService extends Mock implements PullRequestsService {}
diff --git a/app_dart/test/service/github_checks_service_test.dart b/app_dart/test/service/github_checks_service_test.dart
new file mode 100644
index 0000000..0fcede7
--- /dev/null
+++ b/app_dart/test/service/github_checks_service_test.dart
@@ -0,0 +1,203 @@
+// 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:convert';
+
+import 'package:cocoon_service/src/model/github/checks.dart';
+import 'package:cocoon_service/src/model/luci/buildbucket.dart';
+import 'package:cocoon_service/src/model/luci/push_message.dart'
+ as push_message;
+import 'package:cocoon_service/src/service/github_checks_service.dart';
+
+import 'package:github/github.dart' as github;
+import 'package:github/github.dart';
+import 'package:mockito/mockito.dart';
+import 'package:test/test.dart';
+
+import '../model/github/checks_test_data.dart';
+import '../src/datastore/fake_cocoon_config.dart';
+import '../src/request_handling/fake_logging.dart';
+import '../src/utilities/mocks.dart' as mocks;
+import '../src/utilities/mocks.dart';
+
+void main() {
+ FakeConfig config;
+ mocks.MockLuciBuildService mockLuciBuildService;
+ MockGitHub mockGitHub;
+ MockGithubChecksUtil mockGithubChecksUtil;
+ GithubChecksService githubChecksService;
+ github.CheckRun checkRun;
+ RepositorySlug slug;
+
+ const Build linuxBuild = Build(
+ id: 998,
+ builderId: BuilderId(
+ project: 'flutter',
+ bucket: 'prod',
+ builder: 'Linux',
+ ),
+ status: Status.failure,
+ );
+
+ setUp(() {
+ config = FakeConfig();
+ mockGithubChecksUtil = MockGithubChecksUtil();
+ githubChecksService = GithubChecksService(
+ config,
+ githubChecksUtil: mockGithubChecksUtil,
+ );
+ githubChecksService.setLogger(FakeLogging());
+ slug = RepositorySlug('flutter', 'cocoon');
+ mockLuciBuildService = mocks.MockLuciBuildService();
+ mockGitHub = MockGitHub();
+ config.githubClient = mockGitHub;
+ checkRun = github.CheckRun.fromJson(
+ jsonDecode(
+ '{"name": "Linux", "id": 123, "external_id": "678", "status": "completed", "started_at": "2020-05-10T02:49:31Z", "head_sha": "the_sha", "check_suite": {"id": 456}}')
+ as Map<String, dynamic>,
+ );
+ final Map<String, github.CheckRun> checkRuns = <String, github.CheckRun>{
+ 'Linux': checkRun
+ };
+ when(mockGithubChecksUtil.allCheckRuns(any, any)).thenAnswer((_) async {
+ return checkRuns;
+ });
+ });
+
+ group('handleCheckSuiteEvent', () {
+ test('requested triggers all builds', () async {
+ final RepositorySlug slug = RepositorySlug('abc', 'cocoon');
+ final CheckSuiteEvent checkSuiteEvent = CheckSuiteEvent.fromJson(
+ jsonDecode(checkSuiteString) as Map<String, dynamic>);
+ await githubChecksService.handleCheckSuite(
+ checkSuiteEvent, mockLuciBuildService);
+ expect(
+ verify(mockLuciBuildService.scheduleBuilds(
+ commitSha: captureAnyNamed('commitSha'),
+ prNumber: captureAnyNamed('prNumber'),
+ slug: captureAnyNamed('slug'),
+ checkSuiteEvent: anyNamed('checkSuiteEvent'),
+ )).captured,
+ <dynamic>['dabc07b74c555c9952f7b63e139f2bb83b75250f', 758, slug],
+ );
+ });
+ test('re-requested triggers failed builds only', () async {
+ when(mockLuciBuildService.failedBuilds(any, any, any))
+ .thenAnswer((_) async {
+ return <Build>[linuxBuild];
+ });
+ final CheckSuiteEvent checkSuiteEvent = CheckSuiteEvent.fromJson(
+ jsonDecode(checkSuiteTemplate('rerequested'))
+ as Map<String, dynamic>);
+ await githubChecksService.handleCheckSuite(
+ checkSuiteEvent,
+ mockLuciBuildService,
+ );
+ expect(
+ verify(mockLuciBuildService.rescheduleUsingCheckSuiteEvent(
+ captureAny, captureAny))
+ .captured,
+ <dynamic>[
+ checkSuiteEvent,
+ checkRun,
+ ]);
+ });
+ });
+ group('handleCheckRunEvent', () {
+ test('rerequested triggers triggers a luci build', () async {
+ final CheckRunEvent checkRunEvent = CheckRunEvent.fromJson(
+ jsonDecode(checkRunString) as Map<String, dynamic>,
+ );
+ await githubChecksService.handleCheckRun(
+ checkRunEvent,
+ mockLuciBuildService,
+ );
+ expect(
+ verify(mockLuciBuildService.rescheduleUsingCheckRunEvent(
+ captureAny,
+ )).captured,
+ <dynamic>[checkRunEvent]);
+ });
+ });
+ group('updateCheckStatus', () {
+ test('Userdata is empty', () async {
+ final push_message.BuildPushMessage buildMessage =
+ push_message.BuildPushMessage.fromJson(
+ jsonDecode(buildPushMessageJsonTemplate(''))
+ as Map<String, dynamic>);
+ final bool success = await githubChecksService.updateCheckStatus(
+ buildMessage, mockLuciBuildService, slug);
+ expect(success, isFalse);
+ });
+ test('Userdata does not contain check_run_id', () async {
+ final push_message.BuildPushMessage buildMessage =
+ push_message.BuildPushMessage.fromJson(
+ jsonDecode(buildPushMessageJsonTemplate('{\\"retries\\": 1}'))
+ as Map<String, dynamic>);
+ final bool success = await githubChecksService.updateCheckStatus(
+ buildMessage, mockLuciBuildService, slug);
+ expect(success, isFalse);
+ });
+ test('Userdata contain check_run_id', () async {
+ when(mockGithubChecksUtil.getCheckRun(any, any, any))
+ .thenAnswer((_) async => checkRun);
+ final push_message.BuildPushMessage buildPushMessage =
+ push_message.BuildPushMessage.fromJson(
+ jsonDecode(buildPushMessageJsonTemplate('{\\"check_run_id\\": 1,'
+ '\\"repo_owner\\": \\"flutter\\",'
+ '\\"repo_name\\": \\"cocoon\\"}')) as Map<String, dynamic>);
+ await githubChecksService.updateCheckStatus(
+ buildPushMessage, mockLuciBuildService, slug);
+ expect(
+ verify(mockGithubChecksUtil.updateCheckRun(
+ any,
+ any,
+ captureAny,
+ status: anyNamed('status'),
+ conclusion: anyNamed('conclusion'),
+ detailsUrl: anyNamed('detailsUrl'),
+ output: anyNamed('output'),
+ )).captured,
+ <github.CheckRun>[checkRun]);
+ });
+ });
+}
+
+String buildPushMessageJsonTemplate(String jsonUserData) => '''{
+ "build": {
+ "bucket": "luci.flutter.prod",
+ "canary": false,
+ "canary_preference": "PROD",
+ "created_by": "user:dnfield@google.com",
+ "created_ts": "1565049186247524",
+ "experimental": true,
+ "id": "8905920700440101120",
+ "parameters_json": "{\\"builder_name\\": \\"Linux Coverage\\", \\"properties\\": {\\"git_ref\\": \\"refs/pull/37647/head\\", \\"git_url\\": \\"https://github.com/flutter/flutter\\"}}",
+ "project": "flutter",
+ "result_details_json": "{\\"properties\\": {}, \\"swarming\\": {\\"bot_dimensions\\": {\\"caches\\": [\\"flutter_openjdk_install\\", \\"git\\", \\"goma_v2\\", \\"vpython\\"], \\"cores\\": [\\"8\\"], \\"cpu\\": [\\"x86\\", \\"x86-64\\", \\"x86-64-Broadwell_GCE\\", \\"x86-64-avx2\\"], \\"gce\\": [\\"1\\"], \\"gpu\\": [\\"none\\"], \\"id\\": [\\"luci-flutter-prod-xenial-2-bnrz\\"], \\"image\\": [\\"chrome-xenial-19052201-9cb74617499\\"], \\"inside_docker\\": [\\"0\\"], \\"kvm\\": [\\"1\\"], \\"locale\\": [\\"en_US.UTF-8\\"], \\"machine_type\\": [\\"n1-standard-8\\"], \\"os\\": [\\"Linux\\", \\"Ubuntu\\", \\"Ubuntu-16.04\\"], \\"pool\\": [\\"luci.flutter.prod\\"], \\"python\\": [\\"2.7.12\\"], \\"server_version\\": [\\"4382-5929880\\"], \\"ssd\\": [\\"0\\"], \\"zone\\": [\\"us\\", \\"us-central\\", \\"us-central1\\", \\"us-central1-c\\"]}}}",
+ "service_account": "flutter-prod-builder@chops-service-accounts.iam.gserviceaccount.com",
+ "started_ts": "1565049193786080",
+ "status": "STARTED",
+ "result": "FAILURE",
+ "status_changed_ts": "1565049194386647",
+ "tags": [
+ "build_address:luci.flutter.prod/Linux Coverage/1698",
+ "builder:Linux Coverage",
+ "buildset:pr/git/37647",
+ "buildset:sha/git/0d78fc94f890a64af140ce0a2671ac5fc636f59b",
+ "swarming_hostname:chromium-swarm.appspot.com",
+ "swarming_tag:log_location:logdog://logs.chromium.org/flutter/buildbucket/cr-buildbucket.appspot.com/8905920700440101120/+/annotations",
+ "swarming_tag:luci_project:flutter",
+ "swarming_tag:os:Linux",
+ "swarming_tag:recipe_name:flutter/flutter",
+ "swarming_tag:recipe_package:infra/recipe_bundles/chromium.googlesource.com/chromium/tools/build",
+ "swarming_task_id:467d04f2f022d510"
+ ],
+ "updated_ts": "1565049194391321",
+ "url": "https://ci.chromium.org/b/8905920700440101120",
+ "utcnow_ts": "1565049194653640"
+ },
+ "hostname": "cr-buildbucket.appspot.com",
+ "user_data": "$jsonUserData"
+}''';
diff --git a/app_dart/test/src/utilities/mocks.dart b/app_dart/test/src/utilities/mocks.dart
index f446a69..4fce83d 100644
--- a/app_dart/test/src/utilities/mocks.dart
+++ b/app_dart/test/src/utilities/mocks.dart
@@ -5,14 +5,17 @@
import 'dart:async';
import 'dart:io';
+import 'package:cocoon_service/src/foundation/github_checks_util.dart';
import 'package:cocoon_service/src/service/access_token_provider.dart';
import 'package:cocoon_service/src/service/buildbucket.dart';
+import 'package:cocoon_service/src/service/github_checks_service.dart';
import 'package:cocoon_service/src/service/luci.dart';
import 'package:cocoon_service/src/service/reservation_provider.dart';
import 'package:cocoon_service/src/service/task_provider.dart';
import 'package:github/github.dart';
import 'package:googleapis/bigquery/v2.dart';
import 'package:mockito/mockito.dart';
+import 'package:cocoon_service/src/service/luci_build_service.dart';
import '../request_handling/fake_http.dart';
@@ -79,3 +82,9 @@
// ignore: must_be_immutable, Test mock.
class MockBuildBucketClient extends Mock implements BuildBucketClient {}
+
+class MockGithubChecksService extends Mock implements GithubChecksService {}
+
+class MockLuciBuildService extends Mock implements LuciBuildService {}
+
+class MockGithubChecksUtil extends Mock implements GithubChecksUtil {}
diff --git a/test_utilities/bin/flutter_test_runner.sh b/test_utilities/bin/flutter_test_runner.sh
index 6bff0d4..5b9d967 100755
--- a/test_utilities/bin/flutter_test_runner.sh
+++ b/test_utilities/bin/flutter_test_runner.sh
@@ -23,7 +23,7 @@
flutter packages get
flutter analyze
-flutter format --line-length=120 --set-exit-if-changed lib/ test/
+dart format --line-length=120 --set-exit-if-changed lib/ test/
flutter test --test-randomize-ordering-seed=random
popd > /dev/null