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