blob: a09cf125294a00c232782e4e1c74d92e3aa3f274 [file] [log] [blame]
// Copyright 2021 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 'dart:io';
import 'package:buildbucket/buildbucket_pb.dart' as bbv2;
import 'package:cocoon_service/src/foundation/utils.dart';
import 'package:cocoon_service/src/model/appengine/commit.dart';
import 'package:cocoon_service/src/model/appengine/stage.dart';
import 'package:cocoon_service/src/model/appengine/task.dart';
import 'package:cocoon_service/src/model/ci_yaml/target.dart';
import 'package:cocoon_service/src/model/github/checks.dart' as cocoon_checks;
import 'package:cocoon_service/src/service/build_status_provider.dart';
import 'package:cocoon_service/src/service/cache_service.dart';
import 'package:cocoon_service/src/service/config.dart';
import 'package:cocoon_service/src/service/datastore.dart';
import 'package:cocoon_service/src/service/github_checks_service_v2.dart';
import 'package:cocoon_service/src/service/scheduler.dart';
import 'package:cocoon_service/src/service/scheduler_v2.dart';
import 'package:gcloud/db.dart' as gcloud_db;
import 'package:gcloud/db.dart';
import 'package:github/github.dart';
import 'package:googleapis/bigquery/v2.dart';
import 'package:googleapis/firestore/v1.dart' hide Status;
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import '../model/github/checks_test_data.dart';
import '../src/datastore/fake_config.dart';
import '../src/datastore/fake_datastore.dart';
import '../src/service/fake_build_status_provider.dart';
import '../src/request_handling/fake_pubsub.dart';
import '../src/service/fake_gerrit_service.dart';
import '../src/service/fake_github_service.dart';
import '../src/service/fake_luci_build_service_v2.dart';
import '../src/utilities/entity_generators.dart';
import '../src/utilities/mocks.dart';
const String singleCiYaml = r'''
enabled_branches:
- master
- main
- flutter-\d+\.\d+-candidate\.\d+
targets:
- name: Linux A
properties:
custom: abc
- name: Linux B
enabled_branches:
- stable
scheduler: luci
- name: Linux runIf
runIf:
- .ci.yaml
- dev/**
- name: Google Internal Roll
postsubmit: true
presubmit: false
scheduler: google_internal
''';
void main() {
late CacheService cache;
late FakeConfig config;
late FakeDatastoreDB db;
late FakeBuildStatusService buildStatusService;
late MockClient httpClient;
late MockFirestoreService mockFirestoreService;
late MockGithubChecksUtil mockGithubChecksUtil;
late SchedulerV2 scheduler;
final PullRequest pullRequest = generatePullRequest(id: 42);
Commit shaToCommit(String sha, {String branch = 'master'}) {
return Commit(
key: db.emptyKey.append(Commit, id: 'flutter/flutter/$branch/$sha'),
repository: 'flutter/flutter',
sha: sha,
branch: branch,
timestamp: int.parse(sha),
);
}
group('Scheduler', () {
setUp(() {
final MockTabledataResource tabledataResource = MockTabledataResource();
when(tabledataResource.insertAll(any, any, any, any)).thenAnswer((_) async {
return TableDataInsertAllResponse();
});
cache = CacheService(inMemory: true);
db = FakeDatastoreDB();
mockFirestoreService = MockFirestoreService();
buildStatusService = FakeBuildStatusService(
commitStatuses: <CommitStatus>[
CommitStatus(generateCommit(1), const <Stage>[]),
CommitStatus(generateCommit(1, branch: 'main', repo: Config.engineSlug.name), const <Stage>[]),
],
);
config = FakeConfig(
tabledataResource: tabledataResource,
dbValue: db,
githubService: FakeGithubService(),
githubClient: MockGitHub(),
firestoreService: mockFirestoreService,
supportedReposValue: <RepositorySlug>{
Config.engineSlug,
Config.flutterSlug,
},
);
httpClient = MockClient((http.Request request) async {
if (request.url.path.contains('.ci.yaml')) {
return http.Response(singleCiYaml, 200);
}
throw Exception('Failed to find ${request.url.path}');
});
mockGithubChecksUtil = MockGithubChecksUtil();
// Generate check runs based on the name hash code
when(mockGithubChecksUtil.createCheckRun(any, any, any, any, output: anyNamed('output')))
.thenAnswer((Invocation invocation) async => generateCheckRun(invocation.positionalArguments[2].hashCode));
scheduler = SchedulerV2(
cache: cache,
config: config,
datastoreProvider: (DatastoreDB db) => DatastoreService(db, 2),
buildStatusProvider: (_, __) => buildStatusService,
githubChecksService: GithubChecksServiceV2(config, githubChecksUtil: mockGithubChecksUtil),
httpClientProvider: () => httpClient,
luciBuildService: FakeLuciBuildServiceV2(
config: config,
githubChecksUtil: mockGithubChecksUtil,
gerritService: FakeGerritService(
branchesValue: <String>['master', 'main'],
),
),
);
when(mockGithubChecksUtil.createCheckRun(any, any, any, any)).thenAnswer((_) async {
return CheckRun.fromJson(const <String, dynamic>{
'id': 1,
'started_at': '2020-05-10T02:49:31Z',
'check_suite': <String, dynamic>{'id': 2},
});
});
});
group('add commits', () {
final FakePubSub pubsub = FakePubSub();
List<Commit> createCommitList(
List<String> shas, {
String repo = 'flutter',
String branch = 'master',
}) {
return List<Commit>.generate(
shas.length,
(int index) => Commit(
author: 'Username',
authorAvatarUrl: 'http://example.org/avatar.jpg',
branch: branch,
key: db.emptyKey.append(Commit, id: 'flutter/$repo/$branch/${shas[index]}'),
message: 'commit message',
repository: 'flutter/$repo',
sha: shas[index],
timestamp: DateTime.fromMillisecondsSinceEpoch(int.parse(shas[index])).millisecondsSinceEpoch,
),
);
}
test('succeeds when GitHub returns no commits', () async {
await scheduler.addCommits(<Commit>[]);
expect(db.values, isEmpty);
});
test('inserts all relevant fields of the commit', () async {
when(
mockFirestoreService.writeViaTransaction(
captureAny,
),
).thenAnswer((Invocation invocation) {
return Future<CommitResponse>.value(CommitResponse());
});
config.supportedBranchesValue = <String>['master'];
expect(db.values.values.whereType<Commit>().length, 0);
await scheduler.addCommits(createCommitList(<String>['1']));
expect(db.values.values.whereType<Commit>().length, 1);
final Commit commit = db.values.values.whereType<Commit>().single;
expect(commit.repository, 'flutter/flutter');
expect(commit.branch, 'master');
expect(commit.sha, '1');
expect(commit.timestamp, 1);
expect(commit.author, 'Username');
expect(commit.authorAvatarUrl, 'http://example.org/avatar.jpg');
expect(commit.message, 'commit message');
});
test('skips scheduling for unsupported repos', () async {
config.supportedBranchesValue = <String>['master'];
await scheduler.addCommits(createCommitList(<String>['1'], repo: 'not-supported'));
expect(db.values.values.whereType<Commit>().length, 0);
});
test('skips commits for which transaction commit fails', () async {
when(
mockFirestoreService.writeViaTransaction(
captureAny,
),
).thenAnswer((Invocation invocation) {
return Future<CommitResponse>.value(CommitResponse());
});
config.supportedBranchesValue = <String>['master'];
// Existing commits should not be duplicated.
final Commit commit = shaToCommit('1');
db.values[commit.key] = commit;
db.onCommit = (List<gcloud_db.Model<dynamic>> inserts, List<gcloud_db.Key<dynamic>> deletes) {
if (inserts.whereType<Commit>().where((Commit commit) => commit.sha == '3').isNotEmpty) {
throw StateError('Commit failed');
}
};
// Commits are expect from newest to oldest timestamps
await scheduler.addCommits(createCommitList(<String>['2', '3', '4']));
expect(db.values.values.whereType<Commit>().length, 3);
// The 2 new commits are scheduled 3 tasks, existing commit has none.
expect(db.values.values.whereType<Task>().length, 2 * 3);
// Check commits were added, but 3 was not
expect(db.values.values.whereType<Commit>().map<String>(toSha), containsAll(<String>['1', '2', '4']));
expect(db.values.values.whereType<Commit>().map<String>(toSha), isNot(contains('3')));
});
test('skips commits for which task transaction fails', () async {
when(
mockFirestoreService.writeViaTransaction(
captureAny,
),
).thenAnswer((Invocation invocation) {
return Future<CommitResponse>.value(CommitResponse());
});
config.supportedBranchesValue = <String>['master'];
// Existing commits should not be duplicated.
final Commit commit = shaToCommit('1');
db.values[commit.key] = commit;
db.onCommit = (List<gcloud_db.Model<dynamic>> inserts, List<gcloud_db.Key<dynamic>> deletes) {
if (inserts.whereType<Task>().where((Task task) => task.createTimestamp == 3).isNotEmpty) {
throw StateError('Task failed');
}
};
// Commits are expect from newest to oldest timestamps
await scheduler.addCommits(createCommitList(<String>['2', '3', '4']));
expect(db.values.values.whereType<Commit>().length, 3);
// The 2 new commits are scheduled 3 tasks, existing commit has none.
expect(db.values.values.whereType<Task>().length, 2 * 3);
// Check commits were added, but 3 was not
expect(db.values.values.whereType<Commit>().map<String>(toSha), containsAll(<String>['1', '2', '4']));
expect(db.values.values.whereType<Commit>().map<String>(toSha), isNot(contains('3')));
});
test('schedules cocoon based targets', () async {
when(
mockFirestoreService.writeViaTransaction(
captureAny,
),
).thenAnswer((Invocation invocation) {
return Future<CommitResponse>.value(CommitResponse());
});
final MockLuciBuildServiceV2 luciBuildService = MockLuciBuildServiceV2();
when(
luciBuildService.schedulePostsubmitBuilds(
commit: anyNamed('commit'),
toBeScheduled: captureAnyNamed('toBeScheduled'),
),
).thenAnswer((_) => Future<List<Tuple<Target, Task, int>>>.value(<Tuple<Target, Task, int>>[]));
buildStatusService = FakeBuildStatusService(
commitStatuses: <CommitStatus>[
CommitStatus(generateCommit(1, repo: 'engine', branch: 'main'), const <Stage>[]),
],
);
scheduler = SchedulerV2(
cache: cache,
config: config,
buildStatusProvider: (_, __) => buildStatusService,
datastoreProvider: (DatastoreDB db) => DatastoreService(db, 2),
githubChecksService: GithubChecksServiceV2(config, githubChecksUtil: mockGithubChecksUtil),
httpClientProvider: () => httpClient,
luciBuildService: luciBuildService,
);
await scheduler.addCommits(createCommitList(<String>['1'], repo: 'engine', branch: 'main'));
final List<dynamic> captured = verify(
luciBuildService.schedulePostsubmitBuilds(
commit: anyNamed('commit'),
toBeScheduled: captureAnyNamed('toBeScheduled'),
),
).captured;
final List<dynamic> toBeScheduled = captured.first as List<dynamic>;
expect(toBeScheduled.length, 2);
final Iterable<Tuple<Target, Task, int>> tuples =
toBeScheduled.map((dynamic tuple) => tuple as Tuple<Target, Task, int>);
final Iterable<String> scheduledTargetNames =
tuples.map((Tuple<Target, Task, int> tuple) => tuple.second.name!);
expect(scheduledTargetNames, ['Linux A', 'Linux runIf']);
// Tasks triggered by cocoon are marked as in progress
final Iterable<Task> tasks = db.values.values.whereType<Task>();
expect(tasks.singleWhere((Task task) => task.name == 'Linux A').status, Task.statusInProgress);
});
test('schedules cocoon based targets - multiple batch requests', () async {
when(
mockFirestoreService.writeViaTransaction(
captureAny,
),
).thenAnswer((Invocation invocation) {
return Future<CommitResponse>.value(CommitResponse());
});
final MockBuildBucketV2Client mockBuildBucketV2Client = MockBuildBucketV2Client();
final FakeLuciBuildServiceV2 luciBuildServiceV2 = FakeLuciBuildServiceV2(
config: config,
buildBucketV2Client: mockBuildBucketV2Client,
gerritService: FakeGerritService(),
githubChecksUtil: mockGithubChecksUtil,
pubsub: pubsub,
);
when(mockGithubChecksUtil.createCheckRun(any, any, any, any, output: anyNamed('output')))
.thenAnswer((_) async => generateCheckRun(1, name: 'Linux A'));
when(mockBuildBucketV2Client.listBuilders(any)).thenAnswer((_) async {
return bbv2.ListBuildersResponse(
builders: [
bbv2.BuilderItem(id: bbv2.BuilderID(bucket: 'prod', project: 'flutter', builder: 'Linux A')),
bbv2.BuilderItem(id: bbv2.BuilderID(bucket: 'prod', project: 'flutter', builder: 'Linux runIf')),
],
);
});
buildStatusService = FakeBuildStatusService(
commitStatuses: <CommitStatus>[
CommitStatus(generateCommit(1, repo: 'engine', branch: 'main'), const <Stage>[]),
],
);
config.batchSizeValue = 1;
scheduler = SchedulerV2(
cache: cache,
config: config,
buildStatusProvider: (_, __) => buildStatusService,
datastoreProvider: (DatastoreDB db) => DatastoreService(db, 2),
githubChecksService: GithubChecksServiceV2(config, githubChecksUtil: mockGithubChecksUtil),
httpClientProvider: () => httpClient,
luciBuildService: luciBuildServiceV2,
);
await scheduler.addCommits(createCommitList(<String>['1'], repo: 'engine', branch: 'main'));
expect(pubsub.messages.length, 2);
});
});
group('add pull request', () {
test('creates expected commit', () async {
when(
mockFirestoreService.writeViaTransaction(
captureAny,
),
).thenAnswer((Invocation invocation) {
return Future<CommitResponse>.value(CommitResponse());
});
final PullRequest mergedPr = generatePullRequest();
await scheduler.addPullRequest(mergedPr);
expect(db.values.values.whereType<Commit>().length, 1);
final Commit commit = db.values.values.whereType<Commit>().single;
expect(commit.repository, 'flutter/flutter');
expect(commit.branch, 'master');
expect(commit.sha, 'abc');
expect(commit.timestamp, 1);
expect(commit.author, 'dash');
expect(commit.authorAvatarUrl, 'dashatar');
expect(commit.message, 'example message');
final List<dynamic> captured = verify(mockFirestoreService.writeViaTransaction(captureAny)).captured;
expect(captured.length, 1);
final List<Write> commitResponse = captured[0] as List<Write>;
expect(commitResponse.length, 4);
});
test('schedules tasks against merged PRs', () async {
when(
mockFirestoreService.writeViaTransaction(
captureAny,
),
).thenAnswer((Invocation invocation) {
return Future<CommitResponse>.value(CommitResponse());
});
final PullRequest mergedPr = generatePullRequest();
await scheduler.addPullRequest(mergedPr);
expect(db.values.values.whereType<Commit>().length, 1);
expect(db.values.values.whereType<Task>().length, 3);
});
test('guarantees scheduling of tasks against merged release branch PR', () async {
when(
mockFirestoreService.writeViaTransaction(
captureAny,
),
).thenAnswer((Invocation invocation) {
return Future<CommitResponse>.value(CommitResponse());
});
final PullRequest mergedPr = generatePullRequest(branch: 'flutter-3.2-candidate.5');
await scheduler.addPullRequest(mergedPr);
expect(db.values.values.whereType<Commit>().length, 1);
expect(db.values.values.whereType<Task>().length, 3);
// Ensure all tasks have been marked in progress
expect(db.values.values.whereType<Task>().where((Task task) => task.status == Task.statusNew), isEmpty);
});
test('guarantees scheduling of tasks against merged engine PR', () async {
when(
mockFirestoreService.writeViaTransaction(
captureAny,
),
).thenAnswer((Invocation invocation) {
return Future<CommitResponse>.value(CommitResponse());
});
final PullRequest mergedPr = generatePullRequest(
repo: Config.engineSlug.name,
branch: Config.defaultBranch(Config.engineSlug),
);
await scheduler.addPullRequest(mergedPr);
expect(db.values.values.whereType<Commit>().length, 1);
expect(db.values.values.whereType<Task>().length, 3);
// Ensure all tasks under cocoon scheduler have been marked in progress
expect(db.values.values.whereType<Task>().where((Task task) => task.status == Task.statusInProgress).length, 2);
});
test('Release candidate branch commit filters builders not in default branch', () async {
when(
mockFirestoreService.writeViaTransaction(
captureAny,
),
).thenAnswer((Invocation invocation) {
return Future<CommitResponse>.value(CommitResponse());
});
const String totCiYaml = r'''
enabled_branches:
- main
- flutter-\d+\.\d+-candidate\.\d+
targets:
- name: Linux A
properties:
custom: abc
''';
httpClient = MockClient((http.Request request) async {
if (request.url.path == '/flutter/engine/abc/.ci.yaml') {
return http.Response(totCiYaml, HttpStatus.ok);
}
if (request.url.path == '/flutter/engine/1/.ci.yaml') {
return http.Response(singleCiYaml, HttpStatus.ok);
}
print(request.url.path);
throw Exception('Failed to find ${request.url.path}');
});
scheduler = SchedulerV2(
cache: cache,
config: config,
datastoreProvider: (DatastoreDB db) => DatastoreService(db, 2),
buildStatusProvider: (_, __) => buildStatusService,
githubChecksService: GithubChecksServiceV2(config, githubChecksUtil: mockGithubChecksUtil),
httpClientProvider: () => httpClient,
luciBuildService: FakeLuciBuildServiceV2(
config: config,
githubChecksUtil: mockGithubChecksUtil,
gerritService: FakeGerritService(
branchesValue: <String>['master', 'main'],
),
),
);
final PullRequest mergedPr = generatePullRequest(
repo: Config.engineSlug.name,
branch: 'flutter-3.10-candidate.1',
);
await scheduler.addPullRequest(mergedPr);
final List<Task> tasks = db.values.values.whereType<Task>().toList();
expect(db.values.values.whereType<Commit>().length, 1);
expect(tasks, hasLength(1));
expect(tasks.first.name, 'Linux A');
// Ensure all tasks under cocoon scheduler have been marked in progress
expect(db.values.values.whereType<Task>().where((Task task) => task.status == Task.statusInProgress).length, 1);
});
test('does not schedule tasks against non-merged PRs', () async {
final PullRequest notMergedPr = generatePullRequest(merged: false);
await scheduler.addPullRequest(notMergedPr);
expect(db.values.values.whereType<Commit>().map<String>(toSha).length, 0);
expect(db.values.values.whereType<Task>().length, 0);
});
test('does not schedule tasks against already added PRs', () async {
// Existing commits should not be duplicated.
final Commit commit = shaToCommit('1');
db.values[commit.key] = commit;
final PullRequest alreadyLandedPr = generatePullRequest(sha: '1');
await scheduler.addPullRequest(alreadyLandedPr);
expect(db.values.values.whereType<Commit>().map<String>(toSha).length, 1);
// No tasks should be scheduled as that is done on commit insert.
expect(db.values.values.whereType<Task>().length, 0);
});
test('creates expected commit from release branch PR', () async {
when(
mockFirestoreService.writeViaTransaction(
captureAny,
),
).thenAnswer((Invocation invocation) {
return Future<CommitResponse>.value(CommitResponse());
});
final PullRequest mergedPr = generatePullRequest(branch: '1.26');
await scheduler.addPullRequest(mergedPr);
expect(db.values.values.whereType<Commit>().length, 1);
final Commit commit = db.values.values.whereType<Commit>().single;
expect(commit.repository, 'flutter/flutter');
expect(commit.branch, '1.26');
expect(commit.sha, 'abc');
expect(commit.timestamp, 1);
expect(commit.author, 'dash');
expect(commit.authorAvatarUrl, 'dashatar');
expect(commit.message, 'example message');
});
});
group('process check run', () {
test('rerequested ci.yaml check retriggers presubmit', () async {
final MockGithubService mockGithubService = MockGithubService();
final MockGitHub mockGithubClient = MockGitHub();
buildStatusService =
FakeBuildStatusService(commitStatuses: <CommitStatus>[CommitStatus(generateCommit(1), const <Stage>[])]);
config = FakeConfig(
githubService: mockGithubService,
firestoreService: mockFirestoreService,
);
scheduler = SchedulerV2(
cache: cache,
config: config,
buildStatusProvider: (_, __) => buildStatusService,
githubChecksService: GithubChecksServiceV2(config, githubChecksUtil: mockGithubChecksUtil),
httpClientProvider: () => httpClient,
luciBuildService: FakeLuciBuildServiceV2(
config: config,
githubChecksUtil: mockGithubChecksUtil,
),
);
when(mockGithubService.github).thenReturn(mockGithubClient);
when(mockGithubService.searchIssuesAndPRs(any, any, sort: anyNamed('sort'), pages: anyNamed('pages')))
.thenAnswer((_) async => [generateIssue(3)]);
when(mockGithubChecksUtil.listCheckSuitesForRef(any, any, ref: anyNamed('ref'))).thenAnswer(
(_) async => [
// From check_run.check_suite.id in [checkRunString].
generateCheckSuite(668083231),
],
);
when(mockGithubService.getPullRequest(any, any)).thenAnswer((_) async => generatePullRequest());
when(mockGithubService.listFiles(any)).thenAnswer((_) async => ['abc/def']);
when(
mockGithubChecksUtil.createCheckRun(
any,
any,
any,
any,
output: anyNamed('output'),
),
).thenAnswer((_) async {
return CheckRun.fromJson(const <String, dynamic>{
'id': 1,
'started_at': '2020-05-10T02:49:31Z',
'name': Scheduler.kCiYamlCheckName,
'check_suite': <String, dynamic>{'id': 2},
});
});
final Map<String, dynamic> checkRunEventJson = jsonDecode(checkRunString) as Map<String, dynamic>;
checkRunEventJson['check_run']['name'] = Scheduler.kCiYamlCheckName;
final cocoon_checks.CheckRunEvent checkRunEvent = cocoon_checks.CheckRunEvent.fromJson(checkRunEventJson);
expect(await scheduler.processCheckRun(checkRunEvent), true);
verify(
mockGithubChecksUtil.createCheckRun(
any,
any,
any,
Scheduler.kCiYamlCheckName,
output: anyNamed('output'),
),
);
// Verfies Linux A was created
verify(mockGithubChecksUtil.createCheckRun(any, any, any, any)).called(1);
});
// test('rerequested presubmit check triggers presubmit build', () async {
// // Note that we're not inserting any commits into the db, because
// // only postsubmit commits are stored in the datastore.
// config = FakeConfig(dbValue: db);
// db = FakeDatastoreDB();
// // Set up mock buildbucket to validate which bucket is requested.
// final MockBuildBucketV2Client mockBuildbucket = MockBuildBucketV2Client();
// when(mockBuildbucket.batch(any)).thenAnswer((i) async {
// return FakeBuildBucketV2Client().batch(i.positionalArguments[0]);
// });
// when(mockBuildbucket.scheduleBuild(any, buildBucketUri: anyNamed('buildBucketUri')))
// .thenAnswer((realInvocation) async {
// final ScheduleBuildRequest scheduleBuildRequest = realInvocation.positionalArguments[0];
// // Ensure this is an attempt to schedule a presubmit build by
// // verifying that bucket == 'try'.
// expect(scheduleBuildRequest.builderId.bucket, equals('try'));
// return bbv2.Build(builder: bbv2.BuilderID(), id: Int64());
// });
// scheduler = SchedulerV2(
// cache: cache,
// config: config,
// githubChecksService: GithubChecksServiceV2(config, githubChecksUtil: mockGithubChecksUtil),
// luciBuildService: FakeLuciBuildServiceV2(
// config: config,
// githubChecksUtil: mockGithubChecksUtil,
// buildBucketV2Client: mockBuildbucket,
// ),
// );
// final cocoon_checks.CheckRunEvent checkRunEvent = cocoon_checks.CheckRunEvent.fromJson(
// jsonDecode(checkRunString) as Map<String, dynamic>,
// );
// expect(await scheduler.processCheckRun(checkRunEvent), true);
// verify(mockBuildbucket.scheduleBuild(any, buildBucketUri: anyNamed('buildBucketUri'))).called(1);
// verify(mockGithubChecksUtil.createCheckRun(any, any, any, any)).called(1);
// });
// test('rerequested postsubmit check triggers postsubmit build', () async {
// // Set up datastore with postsubmit entities matching [checkRunString].
// db = FakeDatastoreDB();
// config = FakeConfig(
// dbValue: db,
// postsubmitSupportedReposValue: {RepositorySlug('abc', 'cocoon')},
// firestoreService: mockFirestoreService,
// );
// final Commit commit = generateCommit(
// 1,
// sha: '66d6bd9a3f79a36fe4f5178ccefbc781488a596c',
// branch: 'independent_agent',
// owner: 'abc',
// repo: 'cocoon',
// );
// final Commit commitToT = generateCommit(
// 1,
// sha: '66d6bd9a3f79a36fe4f5178ccefbc781488a592c',
// branch: 'master',
// owner: 'abc',
// repo: 'cocoon',
// );
// config.db.values[commit.key] = commit;
// config.db.values[commitToT.key] = commitToT;
// final Task task = generateTask(1, name: 'test1', parent: commit);
// config.db.values[task.key] = task;
// // Set up ci.yaml with task name and branch name from [checkRunString].
// httpClient = MockClient((http.Request request) async {
// if (request.url.path.contains('.ci.yaml')) {
// return http.Response(
// r'''
// enabled_branches:
// - independent_agent
// - master
// targets:
// - name: test1
// ''',
// 200,
// );
// }
// throw Exception('Failed to find ${request.url.path}');
// });
// // Set up mock buildbucket to validate which bucket is requested.
// final MockBuildBucketV2Client mockBuildbucket = MockBuildBucketV2Client();
// when(mockBuildbucket.batch(any)).thenAnswer((i) async {
// return FakeBuildBucketV2Client().batch(i.positionalArguments[0]);
// });
// when(mockBuildbucket.scheduleBuild(any, buildBucketUri: anyNamed('buildBucketUri')))
// .thenAnswer((realInvocation) async {
// final ScheduleBuildRequest scheduleBuildRequest = realInvocation.positionalArguments[0];
// // Ensure this is an attempt to schedule a postsubmit build by
// // verifying that bucket == 'prod'.
// expect(scheduleBuildRequest.builderId.bucket, equals('prod'));
// return bbv2.Build(builder: bbv2.BuilderID(), id: Int64());
// });
// scheduler = SchedulerV2(
// cache: cache,
// config: config,
// githubChecksService: GithubChecksServiceV2(config, githubChecksUtil: mockGithubChecksUtil),
// httpClientProvider: () => httpClient,
// luciBuildService: FakeLuciBuildServiceV2(
// config: config,
// githubChecksUtil: mockGithubChecksUtil,
// buildBucketV2Client: mockBuildbucket,
// gerritService: FakeGerritService(
// branchesValue: <String>['master', 'main'],
// ),
// ),
// );
// final cocoon_checks.CheckRunEvent checkRunEvent = cocoon_checks.CheckRunEvent.fromJson(
// jsonDecode(checkRunString) as Map<String, dynamic>,
// );
// expect(await scheduler.processCheckRun(checkRunEvent), true);
// verify(mockBuildbucket.scheduleBuild(any, buildBucketUri: anyNamed('buildBucketUri'))).called(1);
// verify(mockGithubChecksUtil.createCheckRun(any, any, any, any)).called(1);
// });
// test('rerequested does not fail on empty pull request list', () async {
// when(mockGithubChecksUtil.createCheckRun(any, any, any, any)).thenAnswer((_) async {
// return CheckRun.fromJson(const <String, dynamic>{
// 'id': 1,
// 'started_at': '2020-05-10T02:49:31Z',
// 'check_suite': <String, dynamic>{'id': 2},
// });
// });
// final cocoon_checks.CheckRunEvent checkRunEvent = cocoon_checks.CheckRunEvent.fromJson(
// jsonDecode(checkRunWithEmptyPullRequests) as Map<String, dynamic>,
// );
// expect(await scheduler.processCheckRun(checkRunEvent), true);
// verify(mockGithubChecksUtil.createCheckRun(any, any, any, any)).called(1);
// });
});
group('presubmit', () {
test('gets only enabled .ci.yaml builds', () async {
httpClient = MockClient((http.Request request) async {
if (request.url.path.contains('.ci.yaml')) {
return http.Response(
'''
enabled_branches:
- master
targets:
- name: Linux A
presubmit: true
scheduler: luci
- name: Linux B
scheduler: luci
enabled_branches:
- stable
presubmit: true
- name: Linux C
scheduler: luci
enabled_branches:
- master
presubmit: true
- name: Linux D
scheduler: luci
bringup: true
presubmit: true
- name: Google-internal roll
scheduler: google_internal
enabled_branches:
- master
presubmit: true
''',
200,
);
}
throw Exception('Failed to find ${request.url.path}');
});
final List<Target> presubmitTargets = await scheduler.getPresubmitTargets(pullRequest);
expect(
presubmitTargets.map((Target target) => target.value.name).toList(),
containsAll(<String>['Linux A', 'Linux C']),
);
});
group('treats postsubmit as presubmit if a label is present', () {
final IssueLabel runAllTests = IssueLabel(name: 'test: all');
setUp(() async {
httpClient = MockClient((http.Request request) async {
if (request.url.path.contains('.ci.yaml')) {
return http.Response(
'''
enabled_branches:
- main
- master
targets:
- name: Linux Presubmit
presubmit: true
scheduler: luci
- name: Linux Conditional Presubmit (runIf)
presubmit: true
scheduler: luci
runIf:
- .ci.yaml
- DEPS
- dev/run_if/**
- name: Linux Conditional Presubmit (runIfNot)
presubmit: true
scheduler: luci
runIfNot:
- dev/run_if_not/**
- name: Linux Postsubmit
presubmit: false
scheduler: luci
- name: Linux Cache
presubmit: false
scheduler: luci
properties:
cache_name: "builder"
''',
200,
);
}
throw Exception('Failed to find ${request.url.path}');
});
});
test('with a specific label in the flutter/engine repo', () async {
final enginePr = generatePullRequest(
branch: Config.defaultBranch(Config.engineSlug),
labels: <IssueLabel>[runAllTests],
repo: Config.engineSlug.name,
);
final List<Target> presubmitTargets = await scheduler.getPresubmitTargets(enginePr);
expect(
presubmitTargets.map((Target target) => target.value.name).toList(),
<String>[
// Always runs.
'Linux Presubmit',
// test: all label is present, so runIf is skipped.
'Linux Conditional Presubmit (runIf)',
'Linux Conditional Presubmit (runIfNot)',
// test: all label is present, so postsubmit is treated as presubmit.
'Linux Postsubmit',
],
);
});
test('with a specific label in the flutter/flutter repo', () async {
final frameworkPr = generatePullRequest(
branch: Config.defaultBranch(Config.flutterSlug),
labels: <IssueLabel>[runAllTests],
repo: Config.flutterSlug.name,
);
final List<Target> presubmitTargets = await scheduler.getPresubmitTargets(frameworkPr);
expect(
presubmitTargets.map((Target target) => target.value.name).toList(),
<String>[
// Always runs.
'Linux Presubmit',
'Linux Conditional Presubmit (runIfNot)',
],
);
});
test('without a specific label', () async {
final enginePr = generatePullRequest(
branch: Config.defaultBranch(Config.engineSlug),
labels: <IssueLabel>[],
repo: Config.engineSlug.name,
);
final List<Target> presubmitTargets = await scheduler.getPresubmitTargets(enginePr);
expect(
presubmitTargets.map((Target target) => target.value.name).toList(),
(<String>[
// Always runs.
'Linux Presubmit',
'Linux Conditional Presubmit (runIfNot)',
]),
);
});
});
test('checks for release branches', () async {
const String branch = 'flutter-1.24-candidate.1';
httpClient = MockClient((http.Request request) async {
if (request.url.path.contains('.ci.yaml')) {
return http.Response(
'''
enabled_branches:
- master
targets:
- name: Linux A
presubmit: true
scheduler: luci
''',
200,
);
}
throw Exception('Failed to find ${request.url.path}');
});
expect(
scheduler.getPresubmitTargets(generatePullRequest(branch: branch)),
throwsA(predicate((Exception e) => e.toString().contains('$branch is not enabled'))),
);
});
test('checks for release branch regex', () async {
const String branch = 'flutter-1.24-candidate.1';
httpClient = MockClient((http.Request request) async {
if (request.url.path.contains('.ci.yaml')) {
return http.Response(
'''
enabled_branches:
- main
- master
- flutter-\\d+.\\d+-candidate.\\d+
targets:
- name: Linux A
scheduler: luci
''',
200,
);
}
throw Exception('Failed to find ${request.url.path}');
});
final List<Target> targets = await scheduler.getPresubmitTargets(generatePullRequest(branch: branch));
expect(targets.single.value.name, 'Linux A');
});
test('triggers expected presubmit build checks', () async {
await scheduler.triggerPresubmitTargets(pullRequest: pullRequest);
expect(
verify(mockGithubChecksUtil.createCheckRun(any, any, any, captureAny, output: captureAnyNamed('output')))
.captured,
<dynamic>[
Scheduler.kCiYamlCheckName,
const CheckRunOutput(
title: Scheduler.kCiYamlCheckName,
summary: 'If this check is stuck pending, push an empty commit to retrigger the checks',
),
'Linux A',
null,
// Linux runIf is not run as this is for tip of tree and the files weren't affected
],
);
});
test('Do not schedule other targets on revert request.', () async {
final PullRequest releasePullRequest = generatePullRequest(
labels: [IssueLabel(name: 'revert of')],
);
releasePullRequest.user = User(login: 'auto-submit[bot]');
await scheduler.triggerPresubmitTargets(pullRequest: releasePullRequest);
expect(
verify(mockGithubChecksUtil.createCheckRun(any, any, any, captureAny, output: captureAnyNamed('output')))
.captured,
<dynamic>[
Scheduler.kCiYamlCheckName,
// No other targets should be created.
const CheckRunOutput(
title: Scheduler.kCiYamlCheckName,
summary: 'If this check is stuck pending, push an empty commit to retrigger the checks',
),
],
);
});
test('filters out presubmit targets that do not exist in main and do not filter targets not in main', () async {
const String singleCiYaml = r'''
enabled_branches:
- master
- main
- flutter-\d+\.\d+-candidate\.\d+
targets:
- name: Linux A
properties:
custom: abc
- name: Linux B
enabled_branches:
- flutter-\d+\.\d+-candidate\.\d+
scheduler: luci
- name: Linux C
enabled_branches:
- main
- flutter-\d+\.\d+-candidate\.\d+
scheduler: luci
''';
const String totCiYaml = r'''
enabled_branches:
- main
- flutter-\d+\.\d+-candidate\.\d+
targets:
- name: Linux A
bringup: true
properties:
custom: abc
''';
httpClient = MockClient((http.Request request) async {
if (request.url.path == '/flutter/engine/1/.ci.yaml') {
return http.Response(totCiYaml, HttpStatus.ok);
}
if (request.url.path == '/flutter/engine/abc/.ci.yaml') {
return http.Response(singleCiYaml, HttpStatus.ok);
}
print(request.url.path);
throw Exception('Failed to find ${request.url.path}');
});
scheduler = SchedulerV2(
cache: cache,
config: config,
datastoreProvider: (DatastoreDB db) => DatastoreService(db, 2),
buildStatusProvider: (_, __) => buildStatusService,
githubChecksService: GithubChecksServiceV2(config, githubChecksUtil: mockGithubChecksUtil),
httpClientProvider: () => httpClient,
luciBuildService: FakeLuciBuildServiceV2(
config: config,
githubChecksUtil: mockGithubChecksUtil,
gerritService: FakeGerritService(
branchesValue: <String>['master', 'main'],
),
),
);
final PullRequest pr = generatePullRequest(
repo: Config.engineSlug.name,
branch: 'flutter-3.10-candidate.1',
);
final List<Target> targets = await scheduler.getPresubmitTargets(pr);
expect(
targets.map((Target target) => target.value.name).toList(),
containsAll(<String>['Linux A', 'Linux B']),
);
});
test('triggers all presubmit build checks when diff cannot be found', () async {
final MockGithubService mockGithubService = MockGithubService();
when(mockGithubService.listFiles(pullRequest))
.thenThrow(GitHubError(GitHub(), 'Requested Resource was Not Found'));
buildStatusService =
FakeBuildStatusService(commitStatuses: <CommitStatus>[CommitStatus(generateCommit(1), const <Stage>[])]);
scheduler = SchedulerV2(
cache: cache,
config: FakeConfig(
// tabledataResource: tabledataResource,
dbValue: db,
githubService: mockGithubService,
githubClient: MockGitHub(),
firestoreService: mockFirestoreService,
),
buildStatusProvider: (_, __) => buildStatusService,
datastoreProvider: (DatastoreDB db) => DatastoreService(db, 2),
githubChecksService: GithubChecksServiceV2(config, githubChecksUtil: mockGithubChecksUtil),
httpClientProvider: () => httpClient,
luciBuildService: FakeLuciBuildServiceV2(
config: config,
githubChecksUtil: mockGithubChecksUtil,
gerritService: FakeGerritService(branchesValue: <String>['master']),
),
);
await scheduler.triggerPresubmitTargets(pullRequest: pullRequest);
expect(
verify(mockGithubChecksUtil.createCheckRun(any, any, any, captureAny, output: captureAnyNamed('output')))
.captured,
<dynamic>[
Scheduler.kCiYamlCheckName,
const CheckRunOutput(
title: Scheduler.kCiYamlCheckName,
summary: 'If this check is stuck pending, push an empty commit to retrigger the checks',
),
'Linux A',
null,
// runIf requires a diff in dev, so an error will cause it to be triggered
'Linux runIf',
null,
],
);
});
test('triggers all presubmit targets on release branch pull request', () async {
final PullRequest releasePullRequest = generatePullRequest(
branch: 'flutter-1.24-candidate.1',
);
await scheduler.triggerPresubmitTargets(pullRequest: releasePullRequest);
expect(
verify(mockGithubChecksUtil.createCheckRun(any, any, any, captureAny, output: captureAnyNamed('output')))
.captured,
<dynamic>[
Scheduler.kCiYamlCheckName,
const CheckRunOutput(
title: Scheduler.kCiYamlCheckName,
summary: 'If this check is stuck pending, push an empty commit to retrigger the checks',
),
'Linux A',
null,
'Linux runIf',
null,
],
);
});
test('ci.yaml validation passes with default config', () async {
when(mockGithubChecksUtil.getCheckRun(any, any, any))
.thenAnswer((Invocation invocation) async => createCheckRun(id: 0));
await scheduler.triggerPresubmitTargets(pullRequest: pullRequest);
expect(
verify(
mockGithubChecksUtil.updateCheckRun(
any,
any,
any,
status: captureAnyNamed('status'),
conclusion: captureAnyNamed('conclusion'),
output: anyNamed('output'),
),
).captured,
<dynamic>[CheckRunStatus.completed, CheckRunConclusion.success],
);
});
test('ci.yaml validation fails with empty config', () async {
httpClient = MockClient((http.Request request) async {
if (request.url.path.contains('.ci.yaml')) {
return http.Response('', 200);
}
throw Exception('Failed to find ${request.url.path}');
});
await scheduler.triggerPresubmitTargets(pullRequest: pullRequest);
expect(
verify(
mockGithubChecksUtil.updateCheckRun(
any,
any,
any,
status: captureAnyNamed('status'),
conclusion: captureAnyNamed('conclusion'),
output: anyNamed('output'),
),
).captured,
<dynamic>[CheckRunStatus.completed, CheckRunConclusion.failure],
);
});
test('ci.yaml validation fails on not enabled branch', () async {
final PullRequest pullRequest = generatePullRequest(branch: 'not-valid');
await scheduler.triggerPresubmitTargets(pullRequest: pullRequest);
expect(
verify(
mockGithubChecksUtil.updateCheckRun(
any,
any,
any,
status: captureAnyNamed('status'),
conclusion: captureAnyNamed('conclusion'),
output: anyNamed('output'),
),
).captured,
<dynamic>[CheckRunStatus.completed, CheckRunConclusion.failure],
);
});
test('ci.yaml validation fails with config with unknown dependencies', () async {
httpClient = MockClient((http.Request request) async {
if (request.url.path.contains('.ci.yaml')) {
return http.Response(
'''
enabled_branches:
- master
targets:
- name: A
builder: Linux A
dependencies:
- B
''',
200,
);
}
throw Exception('Failed to find ${request.url.path}');
});
await scheduler.triggerPresubmitTargets(pullRequest: pullRequest);
expect(
verify(
mockGithubChecksUtil.updateCheckRun(
any,
any,
any,
status: anyNamed('status'),
conclusion: anyNamed('conclusion'),
output: captureAnyNamed('output'),
),
).captured.first.text,
'FormatException: ERROR: A depends on B which does not exist',
);
});
// test('retries only triggers failed builds only', () async {
// final MockBuildBucketV2Client mockBuildbucket = MockBuildBucketV2Client();
// buildStatusService =
// FakeBuildStatusService(commitStatuses: <CommitStatus>[CommitStatus(generateCommit(1), const <Stage>[])]);
// final FakePubSub pubsub = FakePubSub();
// scheduler = SchedulerV2(
// cache: cache,
// config: config,
// datastoreProvider: (DatastoreDB db) => DatastoreService(db, 2),
// githubChecksService: GithubChecksServiceV2(config, githubChecksUtil: mockGithubChecksUtil),
// buildStatusProvider: (_, __) => buildStatusService,
// httpClientProvider: () => httpClient,
// luciBuildService: FakeLuciBuildServiceV2(
// config: config,
// githubChecksUtil: mockGithubChecksUtil,
// buildBucketV2Client: mockBuildbucket,
// gerritService: FakeGerritService(branchesValue: <String>['master']),
// pubsub: pubsub,
// ),
// );
// when(mockBuildbucket.batch(any)).thenAnswer(
// (_) async => bbv2.BatchResponse(
// responses: <bbv2.BatchResponse_Response>[
// bbv2.BatchResponse_Response(
// searchBuilds: bbv2.SearchBuildsResponse(
// builds: <bbv2.Build>[
// generateBuild(1000, name: 'Linux', bucket: 'try'),
// generateBuild(2000, name: 'Linux Coverage', bucket: 'try'),
// generateBuild(3000, name: 'Mac', bucket: 'try', status: Status.scheduled),
// generateBuild(4000, name: 'Windows', bucket: 'try', status: Status.started),
// generateBuild(5000, name: 'Linux A', bucket: 'try', status: Status.failure),
// ],
// ),
// ),
// ],
// ),
// );
// when(mockBuildbucket.scheduleBuild(any))
// .thenAnswer((_) async => generateBuild(5001, name: 'Linux A', bucket: 'try', status: Status.scheduled));
// // Only Linux A should be retried
// final Map<String, CheckRun> checkRuns = <String, CheckRun>{
// 'Linux': createCheckRun(name: 'Linux', id: 100),
// 'Linux Coverage': createCheckRun(name: 'Linux Coverage', id: 200),
// 'Mac': createCheckRun(name: 'Mac', id: 300, status: CheckRunStatus.queued),
// 'Windows': createCheckRun(name: 'Windows', id: 400, status: CheckRunStatus.inProgress),
// 'Linux A': createCheckRun(name: 'Linux A', id: 500),
// };
// when(mockGithubChecksUtil.allCheckRuns(any, any)).thenAnswer((_) async {
// return checkRuns;
// });
// final CheckSuiteEvent checkSuiteEvent =
// CheckSuiteEvent.fromJson(jsonDecode(checkSuiteTemplate('rerequested')) as Map<String, dynamic>);
// await scheduler.retryPresubmitTargets(
// pullRequest: pullRequest,
// checkSuiteEvent: checkSuiteEvent,
// );
// expect(pubsub.messages.length, 1);
// final BatchRequest batchRequest = pubsub.messages.single as BatchRequest;
// expect(batchRequest.requests!.length, 1);
// // Schedule build should have been sent
// expect(batchRequest.requests!.single.scheduleBuild, isNotNull);
// final ScheduleBuildRequest scheduleBuildRequest = batchRequest.requests!.single.scheduleBuild!;
// // Verify expected parameters to schedule build
// expect(scheduleBuildRequest.builderId.builder, 'Linux A');
// expect(scheduleBuildRequest.properties!['custom'], 'abc');
// });
// test('pass github_build_label to properties', () async {
// final MockBuildBucketClient mockBuildbucket = MockBuildBucketClient();
// buildStatusService =
// FakeBuildStatusService(commitStatuses: <CommitStatus>[CommitStatus(generateCommit(1), const <Stage>[])]);
// final FakePubSub pubsub = FakePubSub();
// scheduler = Scheduler(
// cache: cache,
// config: config,
// datastoreProvider: (DatastoreDB db) => DatastoreService(db, 2),
// githubChecksService: GithubChecksService(config, githubChecksUtil: mockGithubChecksUtil),
// buildStatusProvider: (_, __) => buildStatusService,
// httpClientProvider: () => httpClient,
// luciBuildService: FakeLuciBuildService(
// config: config,
// githubChecksUtil: mockGithubChecksUtil,
// buildbucket: mockBuildbucket,
// gerritService: FakeGerritService(branchesValue: <String>['master']),
// pubsub: pubsub,
// ),
// );
// when(mockBuildbucket.batch(any)).thenAnswer(
// (_) async => BatchResponse(
// responses: <Response>[
// Response(
// searchBuilds: SearchBuildsResponse(
// builds: <Build>[
// generateBuild(1000, name: 'Linux', bucket: 'try'),
// generateBuild(2000, name: 'Linux Coverage', bucket: 'try'),
// generateBuild(3000, name: 'Mac', bucket: 'try', status: Status.scheduled),
// generateBuild(4000, name: 'Windows', bucket: 'try', status: Status.started),
// generateBuild(5000, name: 'Linux A', bucket: 'try', status: Status.failure),
// ],
// ),
// ),
// ],
// ),
// );
// when(mockBuildbucket.scheduleBuild(any))
// .thenAnswer((_) async => generateBuild(5001, name: 'Linux A', bucket: 'try', status: Status.scheduled));
// // Only Linux A should be retried
// final Map<String, CheckRun> checkRuns = <String, CheckRun>{
// 'Linux': createCheckRun(name: 'Linux', id: 100),
// 'Linux Coverage': createCheckRun(name: 'Linux Coverage', id: 200),
// 'Mac': createCheckRun(name: 'Mac', id: 300, status: CheckRunStatus.queued),
// 'Windows': createCheckRun(name: 'Windows', id: 400, status: CheckRunStatus.inProgress),
// 'Linux A': createCheckRun(name: 'Linux A', id: 500),
// };
// when(mockGithubChecksUtil.allCheckRuns(any, any)).thenAnswer((_) async {
// return checkRuns;
// });
// final CheckSuiteEvent checkSuiteEvent =
// CheckSuiteEvent.fromJson(jsonDecode(checkSuiteTemplate('rerequested')) as Map<String, dynamic>);
// await scheduler.retryPresubmitTargets(
// pullRequest: pullRequest,
// checkSuiteEvent: checkSuiteEvent,
// );
// expect(pubsub.messages.length, 1);
// final BatchRequest batchRequest = pubsub.messages.single as BatchRequest;
// expect(batchRequest.requests!.length, 1);
// // Schedule build should have been sent
// expect(batchRequest.requests!.single.scheduleBuild, isNotNull);
// final ScheduleBuildRequest scheduleBuildRequest = batchRequest.requests!.single.scheduleBuild!;
// // Verify expected parameters to schedule build
// expect(scheduleBuildRequest.builderId.builder, 'Linux A');
// expect(scheduleBuildRequest.properties!['custom'], 'abc');
// });
test('triggers only specificed targets', () async {
final List<Target> presubmitTargets = <Target>[generateTarget(1), generateTarget(2)];
final List<Target> presubmitTriggerTargets = scheduler.getTriggerList(presubmitTargets, <String>['Linux 1']);
expect(presubmitTriggerTargets.length, 1);
});
test('triggers all presubmit targets when trigger list is null', () async {
final List<Target> presubmitTargets = <Target>[generateTarget(1), generateTarget(2)];
final List<Target> presubmitTriggerTargets = scheduler.getTriggerList(presubmitTargets, null);
expect(presubmitTriggerTargets.length, 2);
});
test('triggers all presubmit targets when trigger list is empty', () async {
final List<Target> presubmitTargets = <Target>[generateTarget(1), generateTarget(2)];
final List<Target> presubmitTriggerTargets = scheduler.getTriggerList(presubmitTargets, <String>[]);
expect(presubmitTriggerTargets.length, 2);
});
test('triggers only targets that are contained in the trigger list', () async {
final List<Target> presubmitTargets = <Target>[generateTarget(1), generateTarget(2)];
final List<Target> presubmitTriggerTargets =
scheduler.getTriggerList(presubmitTargets, <String>['Linux 1', 'Linux 3']);
expect(presubmitTriggerTargets.length, 1);
expect(presubmitTargets[0].value.name, 'Linux 1');
});
});
});
}
CheckRun createCheckRun({String? name, required int id, CheckRunStatus status = CheckRunStatus.completed}) {
final int externalId = id * 2;
final String checkRunJson =
'{"name": "$name", "id": $id, "external_id": "{$externalId}", "status": "$status", "started_at": "2020-05-10T02:49:31Z", "head_sha": "the_sha", "check_suite": {"id": 456}}';
return CheckRun.fromJson(jsonDecode(checkRunJson) as Map<String, dynamic>);
}
String toSha(Commit commit) => commit.sha!;
int toTimestamp(Commit commit) => commit.timestamp!;