| // 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_common/task_status.dart'; |
| import 'package:cocoon_server_test/mocks.dart'; |
| import 'package:cocoon_server_test/test_logging.dart'; |
| import 'package:cocoon_service/cocoon_service.dart'; |
| import 'package:cocoon_service/src/model/ci_yaml/ci_yaml.dart'; |
| import 'package:cocoon_service/src/model/ci_yaml/target.dart'; |
| import 'package:cocoon_service/src/model/firestore/ci_staging.dart'; |
| import 'package:cocoon_service/src/model/firestore/commit.dart' as fs; |
| import 'package:cocoon_service/src/model/firestore/pr_check_runs.dart'; |
| import 'package:cocoon_service/src/model/firestore/task.dart' as fs; |
| import 'package:cocoon_service/src/model/github/checks.dart' as cocoon_checks; |
| import 'package:cocoon_service/src/service/big_query.dart'; |
| import 'package:cocoon_service/src/service/flags/dynamic_config.dart'; |
| import 'package:cocoon_service/src/service/luci_build_service/engine_artifacts.dart'; |
| import 'package:cocoon_service/src/service/luci_build_service/pending_task.dart'; |
| import 'package:cocoon_service/src/service/scheduler/process_check_run_result.dart'; |
| import 'package:fixnum/fixnum.dart'; |
| import 'package:github/github.dart'; |
| import 'package:github/hooks.dart'; |
| import 'package:googleapis/bigquery/v2.dart'; |
| import 'package:mockito/mockito.dart'; |
| import 'package:test/test.dart'; |
| |
| import '../model/github/checks_test_data.dart'; |
| import '../src/fake_config.dart'; |
| import '../src/request_handling/fake_pubsub.dart'; |
| import '../src/service/fake_build_bucket_client.dart'; |
| import '../src/service/fake_ci_yaml_fetcher.dart'; |
| import '../src/service/fake_content_aware_hash_service.dart'; |
| import '../src/service/fake_firestore_service.dart'; |
| import '../src/service/fake_gerrit_service.dart'; |
| import '../src/service/fake_get_files_changed.dart'; |
| import '../src/service/fake_github_service.dart'; |
| import '../src/service/fake_luci_build_service.dart'; |
| import '../src/utilities/entity_generators.dart'; |
| import '../src/utilities/mocks.dart'; |
| import '../src/utilities/webhook_generators.dart'; |
| import 'scheduler/ci_yaml_strings.dart'; |
| import 'scheduler/create_check_run.dart'; |
| |
| void main() { |
| useTestLoggerPerTest(); |
| |
| late CacheService cache; |
| late FakeConfig config; |
| late FakeCiYamlFetcher ciYamlFetcher; |
| late FakeFirestoreService firestore; |
| late MockGithubChecksUtil mockGithubChecksUtil; |
| late Scheduler scheduler; |
| late FakeContentAwareHashService fakeContentAwareHash; |
| late FakeGetFilesChanged getFilesChanged; |
| late BigQueryService bigQuery; |
| |
| final pullRequest = generatePullRequest(id: 42); |
| |
| setUp(() { |
| ciYamlFetcher = FakeCiYamlFetcher(); |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml); |
| }); |
| |
| group('Scheduler', () { |
| setUp(() { |
| final tabledataResource = MockTabledataResource(); |
| // ignore: discarded_futures |
| when(tabledataResource.insertAll(any, any, any, any)).thenAnswer(( |
| _, |
| ) async { |
| return TableDataInsertAllResponse(); |
| }); |
| |
| cache = CacheService(inMemory: true); |
| getFilesChanged = FakeGetFilesChanged.inconclusive(); |
| firestore = FakeFirestoreService(); |
| |
| config = FakeConfig( |
| githubService: FakeGithubService(), |
| githubClient: MockGitHub(), |
| supportedReposValue: <RepositorySlug>{ |
| Config.flutterSlug, |
| Config.packagesSlug, |
| }, |
| ); |
| |
| fakeContentAwareHash = FakeContentAwareHashService(config: config); |
| |
| mockGithubChecksUtil = MockGithubChecksUtil(); |
| // Generate check runs based on the name hash code |
| when( |
| // ignore: discarded_futures |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| any, |
| output: anyNamed('output'), |
| ), |
| ).thenAnswer((Invocation invocation) async { |
| return generateCheckRun( |
| invocation.positionalArguments[2].hashCode, |
| name: invocation.positionalArguments[3] as String, |
| ); |
| }); |
| |
| bigQuery = BigQueryService.forTesting( |
| tabledataResource, |
| MockJobsResource(), |
| ); |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| ciYamlFetcher: ciYamlFetcher, |
| getFilesChanged: getFilesChanged, |
| luciBuildService: FakeLuciBuildService( |
| config: config, |
| githubChecksUtil: mockGithubChecksUtil, |
| gerritService: FakeGerritService( |
| branchesValue: <String>['master', 'main'], |
| ), |
| firestore: firestore, |
| ), |
| bigQuery: bigQuery, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| ); |
| |
| // ignore: discarded_futures |
| 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}, |
| }); |
| }); |
| }); |
| |
| test('fusion, getPresubmitTargets supports two ci.yamls', () async { |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionCiYaml); |
| |
| final presubmitTargets = await scheduler.getPresubmitTargets(pullRequest); |
| |
| expect([ |
| ...presubmitTargets.map((Target target) => target.name), |
| ], containsAll(<String>['Linux A'])); |
| presubmitTargets |
| ..clear() |
| ..addAll( |
| await scheduler.getPresubmitTargets( |
| pullRequest, |
| type: CiType.fusionEngine, |
| ), |
| ); |
| expect([ |
| ...presubmitTargets.map((Target target) => target.name), |
| ], containsAll(<String>['Linux Z'])); |
| }); |
| |
| group('add commits', () { |
| final pubsub = FakePubSub(); |
| List<fs.Commit> createCommitList( |
| List<String> shas, { |
| String repo = 'flutter', |
| String branch = 'master', |
| }) { |
| return List.generate( |
| shas.length, |
| (int index) => fs.Commit( |
| author: 'Username', |
| avatar: 'http://example.org/avatar.jpg', |
| branch: branch, |
| message: 'commit message', |
| repositoryPath: 'flutter/$repo', |
| sha: shas[index], |
| createTimestamp: DateTime.fromMillisecondsSinceEpoch( |
| int.parse(shas[index]), |
| ).millisecondsSinceEpoch, |
| ), |
| ); |
| } |
| |
| test('succeeds when GitHub returns no commits', () async { |
| await expectLater(scheduler.addCommits([]), completes); |
| }); |
| |
| test('inserts all relevant fields of the commit', () async { |
| config.supportedBranchesValue = <String>['main']; |
| expect(firestore, existsInStorage(fs.Commit.metadata, isEmpty)); |
| await scheduler.addCommits( |
| createCommitList(<String>['1'], repo: 'packages', branch: 'main'), |
| ); |
| |
| expect( |
| firestore, |
| existsInStorage(fs.Commit.metadata, [ |
| isCommit |
| .hasRepositoryPath('flutter/packages') |
| .hasSha('1') |
| .hasBranch('main') |
| .hasCreateTimestamp(1) |
| .hasAuthor('Username') |
| .hasAvatar('http://example.org/avatar.jpg') |
| .hasMessage('commit message'), |
| ]), |
| ); |
| }); |
| |
| test('skips scheduling for unsupported repos', () async { |
| config.supportedBranchesValue = <String>['master']; |
| await scheduler.addCommits( |
| createCommitList(<String>['1'], repo: 'not-supported'), |
| ); |
| |
| expect(firestore, existsInStorage(fs.Commit.metadata, isEmpty)); |
| }); |
| |
| test('schedules cocoon based targets', () async { |
| final luciBuildService = MockLuciBuildService(); |
| when( |
| luciBuildService.schedulePostsubmitBuilds( |
| commit: anyNamed('commit'), |
| toBeScheduled: captureAnyNamed('toBeScheduled'), |
| ), |
| ).thenAnswer((_) async => []); |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luciBuildService, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| // This test is testing `GuaranteedPolicy` get scheduled - there's only one now. |
| await scheduler.addCommits( |
| createCommitList(<String>['1'], branch: 'main', repo: 'packages'), |
| ); |
| final List<Object?> captured = verify( |
| luciBuildService.schedulePostsubmitBuilds( |
| commit: anyNamed('commit'), |
| toBeScheduled: captureAnyNamed('toBeScheduled'), |
| ), |
| ).captured; |
| final toBeScheduled = captured.first as List<Object?>; |
| expect(toBeScheduled.length, 2); |
| final tuples = toBeScheduled.cast<PendingTask>(); |
| final scheduledTargetNames = tuples.map((tuple) => tuple.taskName); |
| expect(scheduledTargetNames, ['Linux A', 'Linux runIf']); |
| |
| // Tasks triggered by cocoon are marked as in progress |
| expect( |
| firestore, |
| existsInStorage(fs.Task.metadata, [ |
| isTask.hasTaskName('Linux A').hasStatus(TaskStatus.inProgress), |
| isTask.hasTaskName('Linux runIf').hasStatus(TaskStatus.inProgress), |
| isTask |
| .hasTaskName('Google Internal Roll') |
| .hasStatus(TaskStatus.waitingForBackfill), |
| ]), |
| ); |
| }); |
| |
| test('schedules cocoon based targets with content hash', () async { |
| final luciBuildService = MockLuciBuildService(); |
| const contentHash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; |
| fakeContentAwareHash.hashByCommit['1'] = contentHash; |
| when( |
| luciBuildService.schedulePostsubmitBuilds( |
| commit: anyNamed('commit'), |
| toBeScheduled: anyNamed('toBeScheduled'), |
| contentHash: contentHash, |
| ), |
| ).thenAnswer((_) async => []); |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luciBuildService, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| // This test is testing `GuaranteedPolicy` get scheduled - there's only one now. |
| await scheduler.addCommits( |
| createCommitList(<String>['1'], branch: 'main', repo: 'packages'), |
| ); |
| verify( |
| luciBuildService.schedulePostsubmitBuilds( |
| commit: anyNamed('commit'), |
| toBeScheduled: anyNamed('toBeScheduled'), |
| contentHash: contentHash, |
| ), |
| ).called(1); |
| }); |
| |
| test( |
| 'schedules cocoon based targets - multiple batch requests', |
| () async { |
| final mockBuildBucketClient = MockBuildBucketClient(); |
| final luciBuildService = FakeLuciBuildService( |
| config: config, |
| buildBucketClient: mockBuildBucketClient, |
| gerritService: FakeGerritService(), |
| githubChecksUtil: mockGithubChecksUtil, |
| pubsub: pubsub, |
| firestore: firestore, |
| ); |
| when( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| any, |
| output: anyNamed('output'), |
| ), |
| ).thenAnswer((_) async => generateCheckRun(1, name: 'Linux A')); |
| |
| when(mockBuildBucketClient.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', |
| ), |
| ), |
| ], |
| ); |
| }); |
| config.batchSizeValue = 1; |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luciBuildService, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| await scheduler.addCommits( |
| createCommitList(<String>['1'], repo: 'packages', branch: 'main'), |
| ); |
| expect(pubsub.messages.length, 2); |
| }, |
| ); |
| }); |
| |
| group('add pull request', () { |
| test('creates expected commit', () async { |
| final mergedPr = generatePullRequest(repo: 'packages', branch: 'main'); |
| await scheduler.addPullRequest(mergedPr); |
| |
| expect( |
| firestore, |
| existsInStorage(fs.Commit.metadata, [ |
| isCommit |
| .hasRepositoryPath('flutter/packages') |
| .hasSha('abc') |
| .hasBranch('main') |
| .hasCreateTimestamp(1) |
| .hasAuthor('dash') |
| .hasAvatar('dashatar') |
| .hasMessage('example message'), |
| ]), |
| ); |
| |
| expect( |
| firestore, |
| existsInStorage(fs.Task.metadata, [ |
| isTask.hasStatus(TaskStatus.inProgress), |
| isTask.hasStatus(TaskStatus.inProgress), |
| isTask.hasStatus(TaskStatus.waitingForBackfill), |
| ]), |
| ); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/167842. |
| test('marks all tasks as new', () async { |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionCiYaml); |
| |
| final mergedPr = generatePullRequest(repo: 'flutter', branch: 'master'); |
| await scheduler.addPullRequest(mergedPr); |
| |
| expect( |
| firestore, |
| existsInStorage(fs.Commit.metadata, [ |
| isCommit |
| .hasRepositoryPath('flutter/flutter') |
| .hasSha('abc') |
| .hasBranch('master') |
| .hasCreateTimestamp(1) |
| .hasAuthor('dash') |
| .hasAvatar('dashatar') |
| .hasMessage('example message'), |
| ]), |
| ); |
| |
| expect( |
| firestore, |
| existsInStorage( |
| fs.Task.metadata, |
| allOf( |
| hasLength(6), |
| everyElement(isTask.hasStatus(TaskStatus.waitingForBackfill)), |
| ), |
| ), |
| ); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/168738. |
| test('experimental branches build the engine, skip tests', () async { |
| ciYamlFetcher.setCiYamlFrom(otherBranchCiYaml, engine: fusionCiYaml); |
| |
| final mergedPr = generatePullRequest( |
| repo: 'flutter', |
| branch: 'ios-experimental', |
| ); |
| await scheduler.addPullRequest(mergedPr); |
| |
| expect( |
| firestore, |
| existsInStorage(fs.Commit.metadata, [ |
| isCommit |
| .hasRepositoryPath('flutter/flutter') |
| .hasSha('abc') |
| .hasBranch('ios-experimental') |
| .hasCreateTimestamp(1) |
| .hasAuthor('dash') |
| .hasAvatar('dashatar') |
| .hasMessage('example message'), |
| ]), |
| ); |
| |
| expect( |
| firestore, |
| existsInStorage( |
| fs.Task.metadata, |
| unorderedEquals([ |
| // release_build: "true" |
| isTask |
| .hasTaskName('Linux engine_build') |
| .hasStatus(TaskStatus.waitingForBackfill), |
| |
| // engine tests based on engine build |
| isTask |
| .hasTaskName('Linux engine_presubmit') |
| .hasStatus(TaskStatus.skipped), |
| isTask |
| .hasTaskName('Linux runIf engine') |
| .hasStatus(TaskStatus.skipped), |
| isTask.hasTaskName('Linux Z').hasStatus(TaskStatus.skipped), |
| |
| // framework tests based on engine build |
| isTask.hasTaskName('Linux A').hasStatus(TaskStatus.skipped), |
| ]), |
| ), |
| ); |
| }); |
| |
| test('skips all tasks if release candidate branch', () async { |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionCiYaml); |
| |
| final mergedPr = generatePullRequest( |
| branch: 'flutter-0.42-candidate.0', |
| ); |
| await scheduler.addPullRequest(mergedPr); |
| |
| expect( |
| firestore, |
| existsInStorage( |
| fs.Task.metadata, |
| everyElement(isTask.hasStatus(TaskStatus.skipped)), |
| ), |
| ); |
| }); |
| |
| test('schedules tasks against merged PRs', () async { |
| final mergedPr = generatePullRequest(repo: 'packages', branch: 'main'); |
| await scheduler.addPullRequest(mergedPr); |
| |
| expect(firestore, existsInStorage(fs.Commit.metadata, hasLength(1))); |
| |
| expect( |
| firestore, |
| existsInStorage(fs.Task.metadata, [ |
| isTask.hasTaskName('Linux A'), |
| isTask.hasTaskName('Linux runIf'), |
| isTask.hasTaskName('Google Internal Roll'), |
| ]), |
| ); |
| }); |
| |
| test('schedules tasks against merged PRs (fusion)', () async { |
| // NOTE: The scheduler doesn't actually do anything except for write backfill requests - unless its a release. |
| // When backfills are picked up, they'll go through the same flow (schedulePostsubmitBuilds). |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionCiYaml); |
| final mergedPr = generatePullRequest(); |
| await scheduler.addPullRequest(mergedPr); |
| |
| expect(firestore, existsInStorage(fs.Commit.metadata, hasLength(1))); |
| |
| expect( |
| firestore, |
| existsInStorage(fs.Task.metadata, [ |
| isTask.hasTaskName('Linux A'), |
| isTask.hasTaskName('Linux runIf'), |
| isTask.hasTaskName('Google Internal Roll'), |
| isTask.hasTaskName('Linux Z'), |
| isTask.hasTaskName('Linux engine_presubmit'), |
| isTask.hasTaskName('Linux runIf engine'), |
| ]), |
| ); |
| }); |
| |
| test( |
| 'guarantees scheduling of tasks against merged release branch PR', |
| () async { |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionCiYaml); |
| final mergedPr = generatePullRequest( |
| branch: 'flutter-3.2-candidate.5', |
| ); |
| await scheduler.addPullRequest(mergedPr); |
| |
| expect(firestore, existsInStorage(fs.Commit.metadata, hasLength(1))); |
| expect( |
| firestore, |
| existsInStorage(fs.Task.metadata, [ |
| isTask.hasTaskName('Linux A'), |
| isTask.hasTaskName('Linux runIf'), |
| isTask.hasTaskName('Google Internal Roll'), |
| isTask.hasTaskName('Linux Z'), |
| isTask.hasTaskName('Linux engine_presubmit'), |
| isTask.hasTaskName('Linux runIf engine'), |
| ]), |
| ); |
| }, |
| ); |
| |
| test( |
| 'release candidate branch commit filters builders not in default branch', |
| () async { |
| ciYamlFetcher.setCiYamlFrom(r''' |
| enabled_branches: |
| - main |
| - flutter-\d+\.\d+-candidate\.\d+ |
| targets: |
| - name: Linux A |
| properties: |
| custom: abc |
| ''', engine: r''); |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: FakeLuciBuildService( |
| config: config, |
| githubChecksUtil: mockGithubChecksUtil, |
| gerritService: FakeGerritService( |
| branchesValue: <String>['master', 'main'], |
| ), |
| firestore: firestore, |
| ), |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| final mergedPr = generatePullRequest( |
| repo: Config.flutterSlug.name, |
| branch: 'flutter-3.10-candidate.1', |
| ); |
| await scheduler.addPullRequest(mergedPr); |
| |
| expect(firestore, existsInStorage(fs.Commit.metadata, hasLength(1))); |
| |
| expect( |
| firestore, |
| existsInStorage(fs.Task.metadata, [isTask.hasTaskName('Linux A')]), |
| ); |
| }, |
| ); |
| |
| test('does not schedule tasks against non-merged PRs', () async { |
| final notMergedPr = generatePullRequest(merged: false); |
| await scheduler.addPullRequest(notMergedPr); |
| |
| expect(firestore, existsInStorage(fs.Commit.metadata, isEmpty)); |
| expect(firestore, existsInStorage(fs.Task.metadata, isEmpty)); |
| }); |
| |
| test('does not schedule tasks against already added PRs', () async { |
| firestore.putDocument(generateFirestoreCommit(1)); |
| |
| final alreadyLandedPr = generatePullRequest(headSha: '1'); |
| await scheduler.addPullRequest(alreadyLandedPr); |
| |
| expect(firestore, existsInStorage(fs.Commit.metadata, hasLength(1))); |
| |
| expect(firestore, existsInStorage(fs.Task.metadata, isEmpty)); |
| }); |
| |
| test('creates expected commit from release branch PR', () async { |
| ciYamlFetcher.setCiYamlFrom(r''' |
| enabled_branches: |
| - main |
| - flutter-\d+\.\d+-candidate\.\d+ |
| targets: |
| - name: Linux A |
| properties: |
| custom: abc |
| ''', engine: r''); |
| |
| final mergedPr = generatePullRequest(branch: '1.26'); |
| await scheduler.addPullRequest(mergedPr); |
| |
| expect(firestore, existsInStorage(fs.Commit.metadata, hasLength(1))); |
| }); |
| }); |
| |
| group('process check run', () { |
| test('rerequested ci.yaml check retriggers presubmit', () async { |
| final mockGithubService = MockGithubService(); |
| final mockGithubClient = MockGitHub(); |
| config = FakeConfig(githubService: mockGithubService); |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: FakeLuciBuildService( |
| config: config, |
| githubChecksUtil: mockGithubChecksUtil, |
| firestore: firestore, |
| ), |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| 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(repo: 'packages', branch: 'main'), |
| ); |
| getFilesChanged.cannedFiles = ['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': Config.kCiYamlCheckName, |
| 'check_suite': <String, dynamic>{'id': 2}, |
| }); |
| }); |
| final checkRunEventJson = |
| jsonDecode(checkRunString(repository: 'flutter')) |
| as Map<String, dynamic>; |
| checkRunEventJson['check_run']['name'] = Config.kCiYamlCheckName; |
| final checkRunEvent = cocoon_checks.CheckRunEvent.fromJson( |
| checkRunEventJson, |
| ); |
| expect( |
| await scheduler.processCheckRun(checkRunEvent), |
| const ProcessCheckRunResult.success(), |
| ); |
| verify( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| Config.kCiYamlCheckName, |
| output: anyNamed('output'), |
| ), |
| ); |
| // Verfies Linux A was created |
| verify( |
| mockGithubChecksUtil.createCheckRun(any, any, any, any), |
| ).called(1); |
| }); |
| |
| test( |
| 'rerequested fusion (engine) ci.yaml check retriggers presubmit', |
| () async { |
| final mockGithubService = MockGithubService(); |
| final mockGithubClient = MockGitHub(); |
| config = FakeConfig(githubService: mockGithubService); |
| |
| final pullRequest = generatePullRequest( |
| headSha: '66d6bd9a3f79a36fe4f5178ccefbc781488a596c', |
| ); |
| |
| // Enable fusion (modern flutter/flutter merged) |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionCiYaml); |
| config.maxFilesChangedForSkippingEnginePhaseValue = 0; |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: FakeLuciBuildService( |
| config: config, |
| githubChecksUtil: mockGithubChecksUtil, |
| firestore: firestore, |
| ), |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| await PrCheckRuns.initializeDocument( |
| firestoreService: firestore, |
| pullRequest: pullRequest, |
| checks: [generateCheckRun(1, name: 'Linux engine_presubmit')], |
| ); |
| |
| 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 => pullRequest); |
| getFilesChanged.cannedFiles = ['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': 'Linux engine_presubmit', |
| 'check_suite': <String, dynamic>{'id': 2}, |
| }); |
| }); |
| final checkRunEventJson = |
| jsonDecode(checkRunString(repository: 'flutter')) |
| as Map<String, dynamic>; |
| checkRunEventJson['check_run']['name'] = 'Linux engine_presubmit'; |
| final checkRunEvent = cocoon_checks.CheckRunEvent.fromJson( |
| checkRunEventJson, |
| ); |
| expect( |
| await scheduler.processCheckRun(checkRunEvent), |
| const ProcessCheckRunResult.success(), |
| ); |
| verify( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| 'Linux engine_presubmit', |
| output: anyNamed('output'), |
| ), |
| ); |
| }, |
| ); |
| |
| test('rerequested merge queue guard check is ignored', () async { |
| final mockGithubService = MockGithubService(); |
| final mockGithubClient = MockGitHub(); |
| config = FakeConfig(githubService: mockGithubService); |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: FakeLuciBuildService( |
| config: config, |
| githubChecksUtil: mockGithubChecksUtil, |
| firestore: firestore, |
| ), |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| 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()); |
| getFilesChanged.cannedFiles = ['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': Config.kCiYamlCheckName, |
| 'check_suite': <String, dynamic>{'id': 2}, |
| }); |
| }); |
| final checkRunEventJson = |
| jsonDecode(checkRunString()) as Map<String, dynamic>; |
| checkRunEventJson['check_run']['name'] = Config.kMergeQueueLockName; |
| final checkRunEvent = cocoon_checks.CheckRunEvent.fromJson( |
| checkRunEventJson, |
| ); |
| expect( |
| await scheduler.processCheckRun(checkRunEvent), |
| const ProcessCheckRunResult.success(), |
| ); |
| verifyNever( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| Config.kMergeQueueLockName, |
| output: anyNamed('output'), |
| ), |
| ); |
| // Verfies Linux A was created |
| verifyNever(mockGithubChecksUtil.createCheckRun(any, any, any, any)); |
| }); |
| |
| 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 Firestore. |
| final pullRequest = generatePullRequest( |
| headSha: '66d6bd9a3f79a36fe4f5178ccefbc781488a596c', |
| ); |
| |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionCiYaml); |
| |
| final luci = MockLuciBuildService(); |
| when( |
| luci.scheduleTryBuilds( |
| targets: anyNamed('targets'), |
| pullRequest: anyNamed('pullRequest'), |
| engineArtifacts: anyNamed('engineArtifacts'), |
| ), |
| ).thenAnswer((inv) async { |
| return []; |
| }); |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| luciBuildService: luci, |
| ciYamlFetcher: ciYamlFetcher, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| await PrCheckRuns.initializeDocument( |
| firestoreService: firestore, |
| pullRequest: pullRequest, |
| checks: [generateCheckRun(1, name: 'Linux A')], |
| ); |
| |
| final checkrun = jsonDecode(checkRunString()) as Map<String, dynamic>; |
| checkrun['name'] = checkrun['check_run']['name'] = 'Linux A'; |
| final checkRunEvent = cocoon_checks.CheckRunEvent.fromJson(checkrun); |
| |
| expect( |
| await scheduler.processCheckRun(checkRunEvent), |
| const ProcessCheckRunResult.success(), |
| ); |
| |
| expect( |
| firestore, |
| existsInStorage(PrCheckRuns.metadata, [ |
| isPrCheckRun |
| .hasCheckRuns({'Linux A': '1'}) |
| .hasPullRequest( |
| isA<PullRequest>().having( |
| (p) => p.number, |
| 'number', |
| pullRequest.number, |
| ), |
| ), |
| ]), |
| ); |
| }); |
| |
| test('rerequested postsubmit check triggers postsubmit build', () async { |
| // Set up Firestore with postsubmit entities matching [checkRunString]. |
| config = FakeConfig( |
| postsubmitSupportedReposValue: {RepositorySlug('flutter', 'cocoon')}, |
| ); |
| |
| final commit = generateFirestoreCommit( |
| 1, |
| sha: '66d6bd9a3f79a36fe4f5178ccefbc781488a596c', |
| branch: 'independent_agent', |
| owner: 'flutter', |
| repo: 'cocoon', |
| ); |
| |
| firestore.putDocument( |
| generateFirestoreCommit( |
| 1, |
| sha: '66d6bd9a3f79a36fe4f5178ccefbc781488a596c', |
| branch: 'independent_agent', |
| owner: 'flutter', |
| repo: 'cocoon', |
| ), |
| ); |
| firestore.putDocument( |
| generateFirestoreCommit( |
| 1, |
| sha: '66d6bd9a3f79a36fe4f5178ccefbc781488a592c', |
| branch: 'master', |
| owner: 'flutter', |
| repo: 'cocoon', |
| ), |
| ); |
| firestore.putDocument( |
| generateFirestoreTask(1, name: 'test1', commitSha: commit.sha), |
| ); |
| |
| // Set up ci.yaml with task name and branch name from [checkRunString]. |
| ciYamlFetcher.setCiYamlFrom(r''' |
| enabled_branches: |
| - independent_agent |
| - master |
| targets: |
| - name: test1 |
| '''); |
| |
| // Set up mock buildbucket to validate which bucket is requested. |
| final mockBuildbucket = MockBuildBucketClient(); |
| when(mockBuildbucket.batch(any)).thenAnswer((i) async { |
| return FakeBuildBucketClient().batch( |
| i.positionalArguments[0] as bbv2.BatchRequest, |
| ); |
| }); |
| when( |
| mockBuildbucket.scheduleBuild( |
| any, |
| buildBucketUri: anyNamed('buildBucketUri'), |
| ), |
| ).thenAnswer((realInvocation) async { |
| final scheduleBuildRequest = |
| realInvocation.positionalArguments[0] |
| as bbv2.ScheduleBuildRequest; |
| // Ensure this is an attempt to schedule a postsubmit build by |
| // verifying that bucket == 'prod'. |
| expect(scheduleBuildRequest.builder.bucket, equals('prod')); |
| return bbv2.Build(builder: bbv2.BuilderID(), id: Int64()); |
| }); |
| final pubsub = FakePubSub(); |
| final luciBuildService = FakeLuciBuildService( |
| config: config, |
| githubChecksUtil: mockGithubChecksUtil, |
| buildBucketClient: mockBuildbucket, |
| gerritService: FakeGerritService( |
| branchesValue: <String>['master', 'main'], |
| ), |
| pubsub: pubsub, |
| firestore: firestore, |
| ); |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luciBuildService, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| final checkRunEvent = cocoon_checks.CheckRunEvent.fromJson( |
| jsonDecode(checkRunString()) as Map<String, dynamic>, |
| ); |
| expect( |
| await scheduler.processCheckRun(checkRunEvent), |
| const ProcessCheckRunResult.success(), |
| ); |
| verify( |
| mockGithubChecksUtil.createCheckRun(any, any, any, any), |
| ).called(1); |
| expect(pubsub.messages.length, 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 checkRunEvent = cocoon_checks.CheckRunEvent.fromJson( |
| jsonDecode(checkRunWithEmptyPullRequests) as Map<String, dynamic>, |
| ); |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| luciBuildService: FakeLuciBuildService( |
| config: config, |
| firestore: firestore, |
| ), |
| ciYamlFetcher: ciYamlFetcher, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| expect( |
| await scheduler.processCheckRun(checkRunEvent), |
| isA<UserErrorResult>().having( |
| (e) => e.message, |
| 'message', |
| contains('Asked to reschedule presubmits for unknown sha/PR'), |
| ), |
| ); |
| verifyNever(mockGithubChecksUtil.createCheckRun(any, any, any, any)); |
| }); |
| |
| group('completed action', () { |
| test('works for non fusion cases', () async { |
| final text = checkRunEventFor(repo: 'packages'); |
| expect( |
| await scheduler.processCheckRun( |
| cocoon_checks.CheckRunEvent.fromJson( |
| json.decode(text) as Map<String, Object?>, |
| ), |
| ), |
| const ProcessCheckRunResult.success(), |
| ); |
| }); |
| |
| group('in fusion', () { |
| test( |
| 'ignores default check runs that have no side effects', |
| () async { |
| await CiStaging.initializeDocument( |
| firestoreService: firestore, |
| slug: Config.flutterSlug, |
| sha: 'abc123', |
| stage: CiStage.fusionTests, |
| tasks: ['foo', 'bar'], |
| checkRunGuard: '{}', |
| ); |
| |
| for (final ignored in Scheduler.kCheckRunsToIgnore) { |
| expect( |
| await scheduler.processCheckRunCompleted( |
| createCocoonCheckRun(name: ignored, sha: 'abc123'), |
| createGithubRepository().slug(), |
| ), |
| isTrue, |
| ); |
| } |
| |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging.hasCheckRuns({ |
| 'foo': TaskConclusion.scheduled, |
| 'bar': TaskConclusion.scheduled, |
| }), |
| ]), |
| ); |
| }, |
| ); |
| |
| test('ignores invalid conclusions', () async { |
| final document = await CiStaging.initializeDocument( |
| firestoreService: firestore, |
| slug: Config.flutterSlug, |
| sha: 'abc123', |
| stage: CiStage.fusionTests, |
| tasks: ['Bar bar'], |
| checkRunGuard: '{}', |
| ); |
| |
| firestore.failOnWriteDocument(document); |
| |
| expect( |
| await scheduler.processCheckRunCompleted( |
| createCocoonCheckRun(name: 'Bar bar', sha: 'abc123'), |
| createGithubRepository().slug(), |
| ), |
| isFalse, |
| ); |
| |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging.hasCheckRuns({'Bar bar': TaskConclusion.scheduled}), |
| ]), |
| ); |
| |
| verifyNever( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| any, |
| any, |
| status: anyNamed('status'), |
| conclusion: anyNamed('conclusion'), |
| output: anyNamed('output'), |
| ), |
| ); |
| }); |
| |
| test('does not complete with remaining tests', () async { |
| await CiStaging.initializeDocument( |
| firestoreService: firestore, |
| slug: Config.flutterSlug, |
| sha: 'abc123', |
| stage: CiStage.fusionEngineBuild, |
| tasks: ['Foo foo', 'Bar bar'], |
| checkRunGuard: '{}', |
| ); |
| |
| expect( |
| await scheduler.processCheckRunCompleted( |
| createCocoonCheckRun(name: 'Bar bar', sha: 'abc123'), |
| createGithubRepository().slug(), |
| ), |
| isFalse, |
| ); |
| |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging.hasCheckRuns({ |
| 'Foo foo': TaskConclusion.scheduled, |
| 'Bar bar': TaskConclusion.success, |
| }), |
| ]), |
| ); |
| |
| verifyNever( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| any, |
| any, |
| status: anyNamed('status'), |
| conclusion: anyNamed('conclusion'), |
| output: anyNamed('output'), |
| ), |
| ); |
| }); |
| |
| // The merge guard is not closed until both engine build and tests |
| // complete and are successful. |
| // This behavior is explained here: |
| // https://github.com/flutter/flutter/issues/159898#issuecomment-2597209435 |
| test( |
| 'failed tests neither unlock merge queue guard nor schedule test stage', |
| () async { |
| await PrCheckRuns.initializeDocument( |
| firestoreService: firestore, |
| pullRequest: pullRequest, |
| checks: [createGithubCheckRun(name: 'Bar bar')], |
| ); |
| |
| await CiStaging.initializeDocument( |
| firestoreService: firestore, |
| slug: Config.flutterSlug, |
| sha: 'abc123', |
| stage: CiStage.fusionEngineBuild, |
| tasks: ['Bar bar'], |
| checkRunGuard: checkRunFor(name: 'GUARD TEST'), |
| ); |
| |
| expect( |
| await scheduler.processCheckRunCompleted( |
| createCocoonCheckRun(name: 'Bar bar', sha: 'abc123'), |
| createGithubRepository().slug(), |
| ), |
| isTrue, |
| ); |
| |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging.hasCheckRuns({'Bar bar': TaskConclusion.success}), |
| ]), |
| ); |
| |
| verifyNever( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| any, |
| any, |
| status: anyNamed('status'), |
| conclusion: anyNamed('conclusion'), |
| output: anyNamed('output'), |
| ), |
| ); |
| }, |
| ); |
| |
| test('schedules tests after engine stage', () async { |
| final githubService = config.githubService = MockGithubService(); |
| final githubClient = MockGitHub(); |
| when(githubService.github).thenReturn(githubClient); |
| when( |
| githubService.searchIssuesAndPRs( |
| any, |
| any, |
| sort: anyNamed('sort'), |
| pages: anyNamed('pages'), |
| ), |
| ).thenAnswer((_) async => [generateIssue(42)]); |
| |
| final pullRequest = generatePullRequest(); |
| when( |
| githubService.getPullRequest(any, any), |
| ).thenAnswer((_) async => pullRequest); |
| getFilesChanged.cannedFiles = ['abc/def']; |
| when( |
| mockGithubChecksUtil.listCheckSuitesForRef( |
| any, |
| any, |
| ref: anyNamed('ref'), |
| ), |
| ).thenAnswer( |
| (_) async => [ |
| // From check_run.check_suite.id in [checkRunString]. |
| generateCheckSuite(668083231), |
| ], |
| ); |
| |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionCiYaml); |
| final luci = MockLuciBuildService(); |
| when( |
| luci.scheduleTryBuilds( |
| targets: anyNamed('targets'), |
| pullRequest: anyNamed('pullRequest'), |
| engineArtifacts: anyNamed('engineArtifacts'), |
| ), |
| ).thenAnswer((inv) async { |
| return []; |
| }); |
| |
| final gitHubChecksService = MockGithubChecksService(); |
| when( |
| gitHubChecksService.githubChecksUtil, |
| ).thenReturn(mockGithubChecksUtil); |
| when( |
| gitHubChecksService.findMatchingPullRequest(any, any, any), |
| ).thenAnswer((inv) async { |
| return pullRequest; |
| }); |
| |
| // Cocoon creates a Firestore document to track the tasks in the |
| // test stage. |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| getFilesChanged: getFilesChanged, |
| githubChecksService: gitHubChecksService, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| await CiStaging.initializeDocument( |
| firestoreService: firestore, |
| slug: Config.flutterSlug, |
| sha: 'testSha', |
| stage: CiStage.fusionEngineBuild, |
| tasks: ['Bar bar'], |
| checkRunGuard: checkRunFor(name: 'GUARD TEST'), |
| ); |
| |
| expect( |
| await scheduler.processCheckRunCompleted( |
| createCocoonCheckRun(name: 'Bar bar', sha: 'testSha'), |
| createGithubRepository().slug(), |
| ), |
| isTrue, |
| ); |
| |
| verify( |
| gitHubChecksService.findMatchingPullRequest( |
| Config.flutterSlug, |
| 'testSha', |
| 668083231, |
| ), |
| ).called(1); |
| |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging.hasStage(CiStage.fusionEngineBuild).hasCheckRuns({ |
| 'Bar bar': TaskConclusion.success, |
| }), |
| isCiStaging.hasStage(CiStage.fusionTests).hasCheckRuns({ |
| 'Linux A': TaskConclusion.scheduled, |
| 'Linux Z': TaskConclusion.scheduled, |
| 'Linux engine_presubmit': TaskConclusion.scheduled, |
| }), |
| ]), |
| ); |
| |
| verifyNever( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| any, |
| any, |
| status: anyNamed('status'), |
| conclusion: anyNamed('conclusion'), |
| output: anyNamed('output'), |
| ), |
| ); |
| |
| final result = verify( |
| luci.scheduleTryBuilds( |
| targets: captureAnyNamed('targets'), |
| pullRequest: captureAnyNamed('pullRequest'), |
| engineArtifacts: anyNamed('engineArtifacts'), |
| ), |
| ); |
| expect(result.callCount, 1); |
| final captured = result.captured; |
| expect(captured[0], hasLength(3)); |
| // see the blend of fusionCiYaml and singleCiYaml |
| expect(captured[0][0].name, 'Linux A'); |
| expect(captured[0][1].name, 'Linux Z'); |
| expect(captured[0][2].name, 'Linux engine_presubmit'); |
| expect(captured[1], pullRequest); |
| }); |
| |
| test( |
| 'processCheckRunCompleted not failed when check suite id is 0', |
| () async { |
| final githubService = config.githubService = MockGithubService(); |
| final githubClient = MockGitHub(); |
| when(githubService.github).thenReturn(githubClient); |
| when( |
| githubService.searchIssuesAndPRs( |
| any, |
| any, |
| sort: anyNamed('sort'), |
| pages: anyNamed('pages'), |
| ), |
| ).thenAnswer((_) async => [generateIssue(42)]); |
| |
| final pullRequest = generatePullRequest(); |
| when( |
| githubService.getPullRequest(any, any), |
| ).thenAnswer((_) async => pullRequest); |
| getFilesChanged.cannedFiles = ['abc/def']; |
| when( |
| mockGithubChecksUtil.listCheckSuitesForRef( |
| any, |
| any, |
| ref: anyNamed('ref'), |
| ), |
| ).thenAnswer( |
| (_) async => [ |
| // From check_run.check_suite.id in [checkRunString]. |
| generateCheckSuite(668083231), |
| ], |
| ); |
| |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionCiYaml); |
| final luci = MockLuciBuildService(); |
| when( |
| luci.scheduleTryBuilds( |
| targets: anyNamed('targets'), |
| pullRequest: anyNamed('pullRequest'), |
| engineArtifacts: anyNamed('engineArtifacts'), |
| ), |
| ).thenAnswer((inv) async { |
| return []; |
| }); |
| |
| final gitHubChecksService = MockGithubChecksService(); |
| when( |
| gitHubChecksService.githubChecksUtil, |
| ).thenReturn(mockGithubChecksUtil); |
| when( |
| gitHubChecksService.findMatchingPullRequest(any, any, any), |
| ).thenAnswer((inv) async { |
| return pullRequest; |
| }); |
| |
| // Cocoon creates a Firestore document to track the tasks in the |
| // test stage. |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| getFilesChanged: getFilesChanged, |
| githubChecksService: gitHubChecksService, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| await CiStaging.initializeDocument( |
| firestoreService: firestore, |
| slug: Config.flutterSlug, |
| sha: 'testSha', |
| stage: CiStage.fusionEngineBuild, |
| tasks: ['Bar bar'], |
| checkRunGuard: checkRunFor(name: 'GUARD TEST'), |
| ); |
| |
| expect( |
| await scheduler.processCheckRunCompleted( |
| createCocoonCheckRun( |
| name: 'Bar bar', |
| sha: 'testSha', |
| checkSuiteId: 0, |
| ), |
| createGithubRepository().slug(), |
| ), |
| isTrue, |
| ); |
| |
| verify( |
| gitHubChecksService.findMatchingPullRequest( |
| Config.flutterSlug, |
| 'testSha', |
| 0, |
| ), |
| ).called(1); |
| |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging.hasStage(CiStage.fusionEngineBuild).hasCheckRuns({ |
| 'Bar bar': TaskConclusion.success, |
| }), |
| isCiStaging.hasStage(CiStage.fusionTests).hasCheckRuns({ |
| 'Linux A': TaskConclusion.scheduled, |
| 'Linux Z': TaskConclusion.scheduled, |
| 'Linux engine_presubmit': TaskConclusion.scheduled, |
| }), |
| ]), |
| ); |
| |
| verifyNever( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| any, |
| any, |
| status: anyNamed('status'), |
| conclusion: anyNamed('conclusion'), |
| output: anyNamed('output'), |
| ), |
| ); |
| |
| final result = verify( |
| luci.scheduleTryBuilds( |
| targets: captureAnyNamed('targets'), |
| pullRequest: captureAnyNamed('pullRequest'), |
| engineArtifacts: anyNamed('engineArtifacts'), |
| ), |
| ); |
| expect(result.callCount, 1); |
| final captured = result.captured; |
| expect(captured[0], hasLength(3)); |
| // see the blend of fusionCiYaml and singleCiYaml |
| expect(captured[0][0].name, 'Linux A'); |
| expect(captured[0][1].name, 'Linux Z'); |
| expect(captured[0][2].name, 'Linux engine_presubmit'); |
| expect(captured[1], pullRequest); |
| }, |
| ); |
| |
| test('tracks test check runs in firestore', () async { |
| final githubService = config.githubService = MockGithubService(); |
| final githubClient = MockGitHub(); |
| final luci = MockLuciBuildService(); |
| final gitHubChecksService = MockGithubChecksService(); |
| |
| when(githubService.github).thenReturn(githubClient); |
| when( |
| gitHubChecksService.githubChecksUtil, |
| ).thenReturn(mockGithubChecksUtil); |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| getFilesChanged: getFilesChanged, |
| githubChecksService: gitHubChecksService, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| await CiStaging.initializeDocument( |
| firestoreService: firestore, |
| slug: Config.flutterSlug, |
| sha: 'testSha', |
| stage: CiStage.fusionEngineBuild, |
| tasks: [], |
| checkRunGuard: checkRunFor(name: 'GUARD TEST'), |
| ); |
| |
| await CiStaging.initializeDocument( |
| firestoreService: firestore, |
| slug: Config.flutterSlug, |
| sha: 'testSha', |
| stage: CiStage.fusionTests, |
| tasks: ['Bar bar'], |
| checkRunGuard: checkRunFor(name: 'GUARD TEST'), |
| ); |
| |
| expect( |
| await scheduler.processCheckRunCompleted( |
| createCocoonCheckRun(name: 'Bar bar', sha: 'testSha'), |
| createGithubRepository().slug(), |
| ), |
| isTrue, |
| ); |
| |
| // The first invocation looks in the fusionEngineBuild stage, which |
| // returns "missing" result. |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging |
| .hasStage(CiStage.fusionEngineBuild) |
| .hasCheckRuns(isEmpty), |
| isCiStaging.hasStage(CiStage.fusionTests).hasCheckRuns({ |
| 'Bar bar': TaskConclusion.success, |
| }), |
| ]), |
| ); |
| |
| // Because tests completed, and completed successfully, the guard is |
| // unlocked, allowing the PR to land. |
| verify( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| argThat(equals(RepositorySlug('flutter', 'flutter'))), |
| argThat( |
| predicate<CheckRun>((arg) { |
| expect(arg.name, 'GUARD TEST'); |
| return true; |
| }), |
| ), |
| status: argThat( |
| equals(CheckRunStatus.completed), |
| named: 'status', |
| ), |
| conclusion: argThat( |
| equals(CheckRunConclusion.success), |
| named: 'conclusion', |
| ), |
| output: anyNamed('output'), |
| ), |
| ).called(1); |
| }); |
| |
| test( |
| 'writes failure comment if moving to next phase fails', |
| () async { |
| final githubService = config.githubService = MockGithubService(); |
| final githubClient = MockGitHub(); |
| final luci = MockLuciBuildService(); |
| final gitHubChecksService = MockGithubChecksService(); |
| |
| when(githubService.github).thenReturn(githubClient); |
| when( |
| gitHubChecksService.githubChecksUtil, |
| ).thenReturn(mockGithubChecksUtil); |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| getFilesChanged: getFilesChanged, |
| githubChecksService: gitHubChecksService, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| when( |
| gitHubChecksService.findMatchingPullRequest(any, any, any), |
| ).thenAnswer((inv) async { |
| return pullRequest; |
| }); |
| |
| final checkRunEvent = cocoon_checks.CheckRunEvent.fromJson( |
| jsonDecode(checkRunString()) as Map<String, dynamic>, |
| ); |
| |
| final mockGithubService = MockGithubService(); |
| config.githubService = mockGithubService; |
| |
| when( |
| mockGithubService.createComment( |
| any, |
| issueNumber: anyNamed('issueNumber'), |
| body: anyNamed('body'), |
| ), |
| ).thenAnswer((_) async => null); |
| |
| await scheduler.proceedToCiTestingStage( |
| checkRun: checkRunEvent.checkRun!, |
| slug: RepositorySlug('flutter', 'flutter'), |
| sha: 'abc1234', |
| mergeQueueGuard: checkRunFor(name: 'merge queue guard'), |
| logCrumb: 'test', |
| ); |
| |
| verify( |
| mockGithubService.createComment( |
| RepositorySlug('flutter', 'flutter'), |
| issueNumber: argThat( |
| equals(pullRequest.number), |
| named: 'issueNumber', |
| ), |
| body: argThat( |
| contains('CI had a failure that stopped further tests'), |
| named: 'body', |
| ), |
| ), |
| ); |
| }, |
| ); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/164031. |
| test('uses the built-from-source engine artifacts', () async { |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionCiYaml); |
| |
| final githubService = config.githubService = MockGithubService(); |
| final githubClient = MockGitHub(); |
| final luci = MockLuciBuildService(); |
| final gitHubChecksService = MockGithubChecksService(); |
| |
| when(githubService.github).thenReturn(githubClient); |
| when( |
| gitHubChecksService.githubChecksUtil, |
| ).thenReturn(mockGithubChecksUtil); |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| getFilesChanged: getFilesChanged, |
| githubChecksService: gitHubChecksService, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| when( |
| gitHubChecksService.findMatchingPullRequest(any, any, any), |
| ).thenAnswer((inv) async { |
| return pullRequest; |
| }); |
| |
| final checkRuns = <CheckRun>[]; |
| when( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| any, |
| output: anyNamed('output'), |
| conclusion: anyNamed('conclusion'), |
| ), |
| ).thenAnswer((inv) async { |
| final slug = inv.positionalArguments[1] as RepositorySlug; |
| final sha = inv.positionalArguments[2] as String; |
| final name = inv.positionalArguments[3] as String?; |
| checkRuns.add( |
| createGithubCheckRun( |
| id: 1, |
| owner: slug.owner, |
| repo: slug.name, |
| sha: sha, |
| name: name, |
| ), |
| ); |
| return checkRuns.last; |
| }); |
| |
| EngineArtifacts? engineArtifacts; |
| when( |
| luci.scheduleTryBuilds( |
| targets: anyNamed('targets'), |
| pullRequest: anyNamed('pullRequest'), |
| engineArtifacts: anyNamed('engineArtifacts'), |
| ), |
| ).thenAnswer((Invocation i) async { |
| engineArtifacts = |
| i.namedArguments[#engineArtifacts] as EngineArtifacts?; |
| return []; |
| }); |
| |
| final checkRunEvent = cocoon_checks.CheckRunEvent.fromJson( |
| jsonDecode(checkRunString()) as Map<String, dynamic>, |
| ); |
| |
| await scheduler.proceedToCiTestingStage( |
| checkRun: checkRunEvent.checkRun!, |
| slug: RepositorySlug('flutter', 'flutter'), |
| sha: 'abc1234', |
| mergeQueueGuard: checkRunFor(name: 'merge queue guard'), |
| logCrumb: 'test', |
| ); |
| |
| // Ensure that we used the HEAD SHA as as FLUTTER_PREBUILT_ENGINE_VERSION, |
| // since the engine was built from source. |
| // |
| // See https://github.com/flutter/flutter/issues/164031. |
| expect( |
| engineArtifacts, |
| EngineArtifacts.builtFromSource( |
| commitSha: pullRequest.head!.sha!, |
| ), |
| reason: |
| 'Should be set to HEAD (i.e. the current SHA), since the engine was built from source.', |
| ); |
| }); |
| |
| test( |
| 'does not fail the merge queue guard when a test check run fails (presubmit)', |
| () async { |
| final githubService = config.githubService = MockGithubService(); |
| final githubClient = MockGitHub(); |
| final luci = MockLuciBuildService(); |
| final gitHubChecksService = MockGithubChecksService(); |
| |
| when(githubService.github).thenReturn(githubClient); |
| when( |
| gitHubChecksService.githubChecksUtil, |
| ).thenReturn(mockGithubChecksUtil); |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| getFilesChanged: getFilesChanged, |
| githubChecksService: gitHubChecksService, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| await CiStaging.initializeDocument( |
| firestoreService: firestore, |
| slug: Config.flutterSlug, |
| sha: 'testSha', |
| stage: CiStage.fusionEngineBuild, |
| tasks: [], |
| checkRunGuard: checkRunFor(name: 'GUARD TEST'), |
| ); |
| |
| await CiStaging.initializeDocument( |
| firestoreService: firestore, |
| slug: Config.flutterSlug, |
| sha: 'testSha', |
| stage: CiStage.fusionTests, |
| tasks: ['Bar bar'], |
| checkRunGuard: checkRunFor(name: 'GUARD TEST'), |
| ); |
| |
| expect( |
| await scheduler.processCheckRunCompleted( |
| createCocoonCheckRun( |
| name: 'Bar bar', |
| sha: 'testSha', |
| conclusion: 'failure', |
| ), |
| createGithubRepository().slug(), |
| ), |
| isTrue, |
| ); |
| |
| // The first invocation looks in the fusionEngineBuild stage, which |
| // returns "missing" result. |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging |
| .hasStage(CiStage.fusionEngineBuild) |
| .hasCheckRuns(isEmpty), |
| isCiStaging.hasStage(CiStage.fusionTests).hasCheckRuns({ |
| 'Bar bar': TaskConclusion.failure, |
| }), |
| ]), |
| ); |
| |
| // The test stage completed, but with failures. The merge queue |
| // guard should stay open to prevent the pull request from landing. |
| verifyNever( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| any, |
| any, |
| status: anyNamed('status'), |
| conclusion: anyNamed('conclusion'), |
| output: anyNamed('output'), |
| ), |
| ); |
| }, |
| ); |
| |
| test( |
| 'fails the merge queue guard when a test check run fails (merge group)', |
| () async { |
| final githubService = config.githubService = MockGithubService(); |
| final githubClient = MockGitHub(); |
| final luci = MockLuciBuildService(); |
| final gitHubChecksService = MockGithubChecksService(); |
| |
| when(githubService.github).thenReturn(githubClient); |
| when( |
| gitHubChecksService.githubChecksUtil, |
| ).thenReturn(mockGithubChecksUtil); |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| getFilesChanged: getFilesChanged, |
| githubChecksService: gitHubChecksService, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| const headBranch = |
| 'gh-readonly-queue/master/pr-15-c9affbbb12aa40cb3afbe94b9ea6b119a256bebf'; |
| await CiStaging.initializeDocument( |
| firestoreService: firestore, |
| slug: Config.flutterSlug, |
| sha: 'testSha', |
| stage: CiStage.fusionEngineBuild, |
| tasks: ['Bar bar'], |
| checkRunGuard: checkRunFor( |
| name: 'GUARD TEST', |
| headBranch: headBranch, |
| ), |
| ); |
| |
| expect( |
| await scheduler.processCheckRunCompleted( |
| createCocoonCheckRun( |
| name: 'Bar bar', |
| sha: 'testSha', |
| conclusion: 'failure', |
| headBranch: headBranch, |
| ), |
| createGithubRepository().slug(), |
| ), |
| isTrue, |
| ); |
| |
| // The first invocation looks in the fusionEngineBuild stage, which |
| // returns "missing" result. |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging.hasStage(CiStage.fusionEngineBuild).hasCheckRuns({ |
| 'Bar bar': TaskConclusion.failure, |
| }), |
| ]), |
| ); |
| |
| // The test stage completed, but with failures. The merge queue |
| // guard should stay open to prevent the pull request from landing. |
| verify( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| any, |
| any, |
| status: anyNamed('status'), |
| conclusion: CheckRunConclusion.failure, |
| output: anyNamed('output'), |
| ), |
| ).called(1); |
| |
| expect(fakeContentAwareHash.completedShas, [ |
| (commitSha: 'testSha', successful: false), |
| ]); |
| }, |
| ); |
| |
| test('closes merge queue guard in merge group success', () async { |
| final githubService = config.githubService = MockGithubService(); |
| final githubClient = MockGitHub(); |
| final luci = MockLuciBuildService(); |
| final gitHubChecksService = MockGithubChecksService(); |
| |
| when(githubService.github).thenReturn(githubClient); |
| when( |
| gitHubChecksService.githubChecksUtil, |
| ).thenReturn(mockGithubChecksUtil); |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| getFilesChanged: getFilesChanged, |
| githubChecksService: gitHubChecksService, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| const headBranch = |
| 'gh-readonly-queue/master/pr-15-c9affbbb12aa40cb3afbe94b9ea6b119a256bebf'; |
| await CiStaging.initializeDocument( |
| firestoreService: firestore, |
| slug: Config.flutterSlug, |
| sha: 'testSha', |
| stage: CiStage.fusionEngineBuild, |
| tasks: ['Bar bar'], |
| checkRunGuard: checkRunFor( |
| name: 'GUARD TEST', |
| headBranch: headBranch, |
| ), |
| ); |
| |
| expect( |
| await scheduler.processCheckRunCompleted( |
| createCocoonCheckRun( |
| name: 'Bar bar', |
| sha: 'testSha', |
| headBranch: headBranch, |
| ), |
| createGithubRepository().slug(), |
| ), |
| isTrue, |
| ); |
| |
| // The first invocation looks in the fusionEngineBuild stage, which |
| // returns "missing" result. |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging.hasStage(CiStage.fusionEngineBuild).hasCheckRuns({ |
| 'Bar bar': TaskConclusion.success, |
| }), |
| ]), |
| ); |
| |
| // The test stage completed, but with failures. The merge queue |
| // guard should stay open to prevent the pull request from landing. |
| verify( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| any, |
| any, |
| status: anyNamed('status'), |
| conclusion: CheckRunConclusion.success, |
| output: anyNamed('output'), |
| ), |
| ).called(1); |
| |
| expect(fakeContentAwareHash.completedShas, [ |
| (commitSha: 'testSha', successful: true), |
| ]); |
| }); |
| |
| test( |
| 'schedules tests after engine stage - with pr caching', |
| () async { |
| final githubService = config.githubService = MockGithubService(); |
| final githubClient = MockGitHub(); |
| when(githubService.github).thenReturn(githubClient); |
| when( |
| githubService.searchIssuesAndPRs( |
| any, |
| any, |
| sort: anyNamed('sort'), |
| pages: anyNamed('pages'), |
| ), |
| ).thenAnswer((_) async => [generateIssue(42)]); |
| |
| final pullRequest = generatePullRequest(); |
| when( |
| githubService.getPullRequest(any, any), |
| ).thenAnswer((_) async => pullRequest); |
| getFilesChanged.cannedFiles = ['abc/def']; |
| when( |
| mockGithubChecksUtil.listCheckSuitesForRef( |
| any, |
| any, |
| ref: anyNamed('ref'), |
| ), |
| ).thenAnswer( |
| (_) async => [ |
| // From check_run.check_suite.id in [checkRunString]. |
| generateCheckSuite(668083231), |
| ], |
| ); |
| |
| await PrCheckRuns.initializeDocument( |
| firestoreService: firestore, |
| checks: [generateCheckRun(1, name: 'Bar bar')], |
| pullRequest: pullRequest, |
| ); |
| |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionCiYaml); |
| final luci = MockLuciBuildService(); |
| when( |
| luci.scheduleTryBuilds( |
| targets: anyNamed('targets'), |
| pullRequest: anyNamed('pullRequest'), |
| engineArtifacts: anyNamed('engineArtifacts'), |
| ), |
| ).thenAnswer((inv) async { |
| return []; |
| }); |
| |
| final gitHubChecksService = MockGithubChecksService(); |
| when( |
| gitHubChecksService.githubChecksUtil, |
| ).thenReturn(mockGithubChecksUtil); |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: gitHubChecksService, |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| await CiStaging.initializeDocument( |
| firestoreService: firestore, |
| slug: Config.flutterSlug, |
| sha: 'testSha', |
| stage: CiStage.fusionEngineBuild, |
| tasks: ['Bar bar'], |
| checkRunGuard: checkRunFor(name: 'GUARD TEST'), |
| ); |
| |
| expect( |
| await scheduler.processCheckRunCompleted( |
| createCocoonCheckRun(name: 'Bar bar', sha: 'testSha'), |
| createGithubRepository().slug(), |
| ), |
| isTrue, |
| ); |
| |
| verifyNever( |
| gitHubChecksService.findMatchingPullRequest(any, any, any), |
| ); |
| |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging.hasStage(CiStage.fusionEngineBuild).hasCheckRuns({ |
| 'Bar bar': TaskConclusion.success, |
| }), |
| isCiStaging.hasStage(CiStage.fusionTests).hasCheckRuns({ |
| 'Linux A': TaskConclusion.scheduled, |
| 'Linux Z': TaskConclusion.scheduled, |
| 'Linux engine_presubmit': TaskConclusion.scheduled, |
| }), |
| ]), |
| ); |
| |
| verifyNever( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| any, |
| any, |
| status: anyNamed('status'), |
| conclusion: anyNamed('conclusion'), |
| output: anyNamed('output'), |
| ), |
| ); |
| |
| final result = verify( |
| luci.scheduleTryBuilds( |
| targets: captureAnyNamed('targets'), |
| pullRequest: captureAnyNamed('pullRequest'), |
| engineArtifacts: anyNamed('engineArtifacts'), |
| ), |
| ); |
| expect(result.callCount, 1); |
| final captured = result.captured; |
| expect(captured[0], hasLength(3)); |
| // see the blend of fusionCiYaml and singleCiYaml |
| expect(captured[0][0].name, 'Linux A'); |
| expect(captured[0][1].name, 'Linux Z'); |
| expect(captured[0][2].name, 'Linux engine_presubmit'); |
| expect( |
| captured[1], |
| isA<PullRequest>().having( |
| (p) => p.number, |
| 'number', |
| pullRequest.number, |
| ), |
| ); |
| }, |
| ); |
| // end of group |
| }); |
| }); |
| }); |
| |
| group('presubmit', () { |
| test('gets only enabled .ci.yaml builds', () async { |
| ciYamlFetcher.setCiYamlFrom(r''' |
| 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 |
| '''); |
| final presubmitTargets = await scheduler.getPresubmitTargets( |
| pullRequest, |
| ); |
| expect( |
| presubmitTargets.map((Target target) => target.name).toList(), |
| containsAll(<String>['Linux A', 'Linux C']), |
| ); |
| }); |
| |
| test('checks for release branches', () async { |
| const branch = 'flutter-1.24-candidate.1'; |
| ciYamlFetcher.setCiYamlFrom(r''' |
| enabled_branches: |
| - master |
| targets: |
| - name: Linux A |
| presubmit: true |
| scheduler: luci |
| '''); |
| 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 branch = 'flutter-1.24-candidate.1'; |
| ciYamlFetcher.setCiYamlFrom(''' |
| enabled_branches: |
| - main |
| - master |
| - flutter-\\d+.\\d+-candidate.\\d+ |
| targets: |
| - name: Linux A |
| scheduler: luci |
| '''); |
| final targets = await scheduler.getPresubmitTargets( |
| generatePullRequest(branch: branch), |
| ); |
| expect(targets.single.name, 'Linux A'); |
| }); |
| |
| test('triggers expected presubmit build checks', () async { |
| getFilesChanged.cannedFiles = ['README.md']; |
| await scheduler.triggerPresubmitTargets( |
| pullRequest: generatePullRequest(branch: 'main', repo: 'packages'), |
| ); |
| expect( |
| verify( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| captureAny, |
| output: captureAnyNamed('output'), |
| ), |
| ).captured, |
| <Object?>[ |
| Config.kMergeQueueLockName, |
| const CheckRunOutput( |
| title: Config.kMergeQueueLockName, |
| summary: Scheduler.kMergeQueueLockDescription, |
| ), |
| Config.kCiYamlCheckName, |
| const CheckRunOutput( |
| title: Config.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 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, |
| <Object?>[ |
| Config.kMergeQueueLockName, |
| const CheckRunOutput( |
| title: Config.kMergeQueueLockName, |
| summary: Scheduler.kMergeQueueLockDescription, |
| ), |
| Config.kCiYamlCheckName, |
| // No other targets should be created. |
| const CheckRunOutput( |
| title: Config.kCiYamlCheckName, |
| summary: |
| 'If this check is stuck pending, push an empty commit to retrigger the checks', |
| ), |
| ], |
| ); |
| }); |
| |
| test('Unlocks merge group on revert request.', () async { |
| final releasePullRequest = generatePullRequest( |
| labels: [IssueLabel(name: 'revert of')], |
| ); |
| |
| releasePullRequest.user = User(login: 'auto-submit[bot]'); |
| |
| await scheduler.triggerPresubmitTargets( |
| pullRequest: releasePullRequest, |
| ); |
| expect( |
| verify( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| any, |
| any, |
| status: captureAnyNamed('status'), |
| conclusion: captureAnyNamed('conclusion'), |
| output: captureAnyNamed('output'), |
| ), |
| ).captured, |
| <Object?>[ |
| CheckRunStatus.completed, |
| CheckRunConclusion.success, |
| null, |
| CheckRunStatus.completed, |
| CheckRunConclusion.success, |
| null, |
| ], |
| ); |
| }); |
| |
| test( |
| 'filters out presubmit targets that do not exist in main and do not filter targets not in main', |
| () async { |
| ciYamlFetcher.setCiYamlFrom(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 |
| '''); |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: FakeLuciBuildService( |
| config: config, |
| githubChecksUtil: mockGithubChecksUtil, |
| gerritService: FakeGerritService( |
| branchesValue: <String>['master', 'main'], |
| ), |
| firestore: firestore, |
| ), |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| final pr = generatePullRequest( |
| repo: Config.flutterSlug.name, |
| branch: 'flutter-3.10-candidate.1', |
| ); |
| final targets = await scheduler.getPresubmitTargets(pr); |
| expect( |
| targets.map((Target target) => target.name).toList(), |
| containsAll(<String>['Linux A', 'Linux B']), |
| ); |
| }, |
| ); |
| |
| test( |
| 'triggers all presubmit build checks when diff cannot be found', |
| () async { |
| final mockGithubService = MockGithubService(); |
| getFilesChanged.cannedFiles = null; |
| scheduler = Scheduler( |
| cache: cache, |
| config: FakeConfig( |
| // tabledataResource: tabledataResource, |
| githubService: mockGithubService, |
| githubClient: MockGitHub(), |
| ), |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: FakeLuciBuildService( |
| config: config, |
| githubChecksUtil: mockGithubChecksUtil, |
| gerritService: FakeGerritService( |
| branchesValue: <String>['master'], |
| ), |
| firestore: firestore, |
| ), |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| await scheduler.triggerPresubmitTargets( |
| pullRequest: generatePullRequest(branch: 'main', repo: 'packages'), |
| ); |
| expect( |
| verify( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| captureAny, |
| output: captureAnyNamed('output'), |
| ), |
| ).captured, |
| <Object?>[ |
| Config.kMergeQueueLockName, |
| const CheckRunOutput( |
| title: Config.kMergeQueueLockName, |
| summary: Scheduler.kMergeQueueLockDescription, |
| ), |
| Config.kCiYamlCheckName, |
| const CheckRunOutput( |
| title: Config.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 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, |
| <Object?>[ |
| Config.kMergeQueueLockName, |
| const CheckRunOutput( |
| title: Config.kMergeQueueLockName, |
| summary: Scheduler.kMergeQueueLockDescription, |
| ), |
| Config.kCiYamlCheckName, |
| const CheckRunOutput( |
| title: Config.kCiYamlCheckName, |
| summary: |
| 'If this check is stuck pending, push an empty commit to retrigger the checks', |
| ), |
| ], |
| ); |
| }, |
| ); |
| |
| test('ci.yaml validation passes with default config', () async { |
| when(mockGithubChecksUtil.getCheckRun(any, any, any)).thenAnswer( |
| (Invocation invocation) async => |
| createGithubCheckRun(id: 0, repo: 'packages'), |
| ); |
| await scheduler.triggerPresubmitTargets( |
| pullRequest: generatePullRequest(repo: 'packages', branch: 'main'), |
| ); |
| expect( |
| verify( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| any, |
| any, |
| status: captureAnyNamed('status'), |
| conclusion: captureAnyNamed('conclusion'), |
| output: anyNamed('output'), |
| ), |
| ).captured, |
| <Object?>[ |
| CheckRunStatus.completed, |
| CheckRunConclusion.success, |
| CheckRunStatus.completed, |
| CheckRunConclusion.success, |
| ], |
| ); |
| }); |
| |
| test('ci.yaml validation failure', () async { |
| ciYamlFetcher.failCiYamlValidation = true; |
| |
| final capturedUpdates = |
| <(String, CheckRunStatus, CheckRunConclusion)>[]; |
| |
| when( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| any, |
| any, |
| status: anyNamed('status'), |
| conclusion: anyNamed('conclusion'), |
| output: anyNamed('output'), |
| ), |
| ).thenAnswer((inv) async { |
| final checkRun = inv.positionalArguments[2] as CheckRun; |
| capturedUpdates.add(( |
| checkRun.name!, |
| inv.namedArguments[#status], |
| inv.namedArguments[#conclusion], |
| )); |
| }); |
| |
| await scheduler.triggerPresubmitTargets(pullRequest: pullRequest); |
| |
| expect(capturedUpdates, <(String, CheckRunStatus, CheckRunConclusion)>[ |
| ( |
| 'ci.yaml validation', |
| CheckRunStatus.completed, |
| CheckRunConclusion.failure, |
| ), |
| ]); |
| }); |
| |
| test('ci.yaml validation fails on not enabled branch', () async { |
| final 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, |
| <Object?>[CheckRunStatus.completed, CheckRunConclusion.failure], |
| ); |
| }); |
| |
| test('triggers only specificed targets', () async { |
| final presubmitTargets = <Target>[generateTarget(1), generateTarget(2)]; |
| final presubmitTriggerTargets = scheduler.filterTargets( |
| presubmitTargets, |
| <String>['Linux 1'], |
| ); |
| expect(presubmitTriggerTargets.length, 1); |
| }); |
| |
| test( |
| 'triggers all presubmit targets when trigger list is null', |
| () async { |
| final presubmitTargets = <Target>[ |
| generateTarget(1), |
| generateTarget(2), |
| ]; |
| final presubmitTriggerTargets = scheduler.filterTargets( |
| presubmitTargets, |
| null, |
| ); |
| expect(presubmitTriggerTargets.length, 2); |
| }, |
| ); |
| |
| test( |
| 'triggers all presubmit targets when trigger list is empty', |
| () async { |
| final presubmitTargets = <Target>[ |
| generateTarget(1), |
| generateTarget(2), |
| ]; |
| final presubmitTriggerTargets = scheduler.filterTargets( |
| presubmitTargets, |
| <String>[], |
| ); |
| expect(presubmitTriggerTargets.length, 2); |
| }, |
| ); |
| |
| test( |
| 'triggers only targets that are contained in the trigger list', |
| () async { |
| final presubmitTargets = <Target>[ |
| generateTarget(1), |
| generateTarget(2), |
| ]; |
| final presubmitTriggerTargets = scheduler.filterTargets( |
| presubmitTargets, |
| <String>['Linux 1', 'Linux 3'], |
| ); |
| expect(presubmitTriggerTargets.length, 1); |
| expect(presubmitTargets[0].name, 'Linux 1'); |
| }, |
| ); |
| |
| test('in fusion gathers creates engine builds', () async { |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionCiYaml); |
| final luci = MockLuciBuildService(); |
| when( |
| luci.scheduleTryBuilds( |
| targets: anyNamed('targets'), |
| pullRequest: anyNamed('pullRequest'), |
| engineArtifacts: anyNamed('engineArtifacts'), |
| ), |
| ).thenAnswer((inv) async { |
| return []; |
| }); |
| final mockGithubService = MockGithubService(); |
| final checkRuns = <CheckRun>[]; |
| when( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| any, |
| output: anyNamed('output'), |
| ), |
| ).thenAnswer((inv) async { |
| final slug = inv.positionalArguments[1] as RepositorySlug; |
| final sha = inv.positionalArguments[2] as String; |
| final name = inv.positionalArguments[3] as String?; |
| checkRuns.add( |
| createGithubCheckRun( |
| id: 1, |
| owner: slug.owner, |
| repo: slug.name, |
| sha: sha, |
| name: name, |
| ), |
| ); |
| return checkRuns.last; |
| }); |
| |
| getFilesChanged.cannedFiles = ['abc/def', 'engine/src/flutter/FILE']; |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: FakeConfig( |
| // tabledataResource: tabledataResource, |
| githubService: mockGithubService, |
| githubClient: MockGitHub(), |
| maxFilesChangedForSkippingEnginePhaseValue: 0, |
| ), |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| await scheduler.triggerPresubmitTargets(pullRequest: pullRequest); |
| final results = verify( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| captureAny, |
| output: captureAnyNamed('output'), |
| ), |
| ).captured; |
| stdout.writeAll(results); |
| |
| final result = verify( |
| luci.scheduleTryBuilds( |
| targets: captureAnyNamed('targets'), |
| pullRequest: anyNamed('pullRequest'), |
| engineArtifacts: anyNamed('engineArtifacts'), |
| ), |
| ); |
| expect(result.callCount, 1); |
| final captured = result.captured; |
| expect(captured[0], hasLength(1)); |
| // see the blend of fusionCiYaml and singleCiYaml |
| expect(captured[0][0].name, 'Linux engine_build'); |
| |
| expect(checkRuns, hasLength(2)); |
| verify( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| Config.flutterSlug, |
| checkRuns[1], |
| status: argThat(equals(CheckRunStatus.completed), named: 'status'), |
| conclusion: argThat( |
| equals(CheckRunConclusion.success), |
| named: 'conclusion', |
| ), |
| output: anyNamed('output'), |
| ), |
| ).called(1); |
| |
| verifyNever( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| Config.flutterSlug, |
| checkRuns[0], |
| status: anyNamed('status'), |
| conclusion: anyNamed('conclusion'), |
| output: anyNamed('output'), |
| ), |
| ); |
| }); |
| }); |
| |
| test('busted CheckRun does not kill the system', () { |
| final data = scheduler.checkRunFromString( |
| '{"name":"Merge Queue Guard","id":33947747856,"external_id":"","status":"queued","head_sha":"","check_suite":{"id":31681571627},"details_url":"https://flutter-dashboard.appspot.com","started_at":"2024-12-05T01:05:24.000Z","conclusion":"null"}', |
| ); |
| expect(data.name, 'Merge Queue Guard'); |
| expect(data.id, 33947747856); |
| expect(data.conclusion, CheckRunConclusion.empty); |
| }); |
| |
| group('merge groups', () { |
| test('schedule some work on prod', () async { |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionDualCiYaml); |
| final luci = MockLuciBuildService(); |
| when( |
| luci.getAvailableBuilderSet( |
| project: anyNamed('project'), |
| bucket: anyNamed('bucket'), |
| ), |
| ).thenAnswer((inv) async { |
| return {'Mac engine_build', 'Linux engine_build'}; |
| }); |
| when( |
| luci.scheduleTryBuilds( |
| targets: anyNamed('targets'), |
| pullRequest: anyNamed('pullRequest'), |
| engineArtifacts: anyNamed('engineArtifacts'), |
| ), |
| ).thenAnswer((inv) async { |
| return []; |
| }); |
| final mockGithubService = MockGithubService(); |
| final checkRuns = <CheckRun>[]; |
| when( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| any, |
| output: anyNamed('output'), |
| ), |
| ).thenAnswer((inv) async { |
| final slug = inv.positionalArguments[1] as RepositorySlug; |
| final sha = inv.positionalArguments[2] as String; |
| final name = inv.positionalArguments[3] as String?; |
| checkRuns.add( |
| createGithubCheckRun( |
| id: 1, |
| owner: slug.owner, |
| repo: slug.name, |
| sha: sha, |
| name: name, |
| ), |
| ); |
| return checkRuns.last; |
| }); |
| getFilesChanged.cannedFiles = ['abc/def']; |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: FakeConfig( |
| // tabledataResource: tabledataResource, |
| githubService: mockGithubService, |
| githubClient: MockGitHub(), |
| ), |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| final mergeGroupEvent = cocoon_checks.MergeGroupEvent.fromJson( |
| json.decode( |
| generateMergeGroupEventString( |
| repository: 'flutter/flutter', |
| action: 'checks_requested', |
| message: 'Implement an amazing feature', |
| ), |
| ) |
| as Map<String, Object?>, |
| ); |
| |
| await scheduler.handleMergeGroupEvent(mergeGroupEvent: mergeGroupEvent); |
| |
| expect(fakeContentAwareHash.triggered, [ |
| 'refs/heads/gh-readonly-queue/main/pr-15-c9affbbb12aa40cb3afbe94b9ea6b119a256bebf', |
| ]); |
| |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging |
| .hasSha('c9affbbb12aa40cb3afbe94b9ea6b119a256bebf') |
| .hasCheckRuns({ |
| 'Linux engine_build': TaskConclusion.scheduled, |
| 'Mac engine_build': TaskConclusion.scheduled, |
| }), |
| ]), |
| ); |
| |
| verify( |
| luci.getAvailableBuilderSet( |
| project: argThat(equals('flutter'), named: 'project'), |
| bucket: argThat(equals('prod'), named: 'bucket'), |
| ), |
| ).called(1); |
| |
| verify( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| captureAny, |
| output: captureAnyNamed('output'), |
| ), |
| ).called(1); |
| final result = verify( |
| luci.scheduleMergeGroupBuilds( |
| targets: captureAnyNamed('targets'), |
| commit: anyNamed('commit'), |
| ), |
| ); |
| expect(result.callCount, 1); |
| expect( |
| result.captured.cast<List<Target>>()[0].map((target) => target.name), |
| ['Linux engine_build', 'Mac engine_build'], |
| ); |
| |
| expect(checkRuns, hasLength(1)); |
| verifyNever( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| Config.flutterSlug, |
| checkRuns[0], |
| status: anyNamed('status'), |
| conclusion: anyNamed('conclusion'), |
| output: anyNamed('output'), |
| ), |
| ); |
| }); |
| |
| test('does not schedule work if waitOnContentHash', () async { |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionDualCiYaml); |
| final luci = MockLuciBuildService(); |
| when( |
| luci.getAvailableBuilderSet( |
| project: anyNamed('project'), |
| bucket: anyNamed('bucket'), |
| ), |
| ).thenAnswer((inv) async { |
| return {'Mac engine_build', 'Linux engine_build'}; |
| }); |
| when( |
| luci.scheduleTryBuilds( |
| targets: anyNamed('targets'), |
| pullRequest: anyNamed('pullRequest'), |
| engineArtifacts: anyNamed('engineArtifacts'), |
| ), |
| ).thenAnswer((inv) async { |
| return []; |
| }); |
| final mockGithubService = MockGithubService(); |
| final checkRuns = <CheckRun>[]; |
| when( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| any, |
| output: anyNamed('output'), |
| ), |
| ).thenAnswer((inv) async { |
| final slug = inv.positionalArguments[1] as RepositorySlug; |
| final sha = inv.positionalArguments[2] as String; |
| final name = inv.positionalArguments[3] as String?; |
| checkRuns.add( |
| createGithubCheckRun( |
| id: 1, |
| owner: slug.owner, |
| repo: slug.name, |
| sha: sha, |
| name: name, |
| ), |
| ); |
| return checkRuns.last; |
| }); |
| getFilesChanged.cannedFiles = ['abc/def']; |
| |
| final config = FakeConfig( |
| // tabledataResource: tabledataResource, |
| githubService: mockGithubService, |
| githubClient: MockGitHub(), |
| dynamicConfig: DynamicConfig.fromJson({ |
| 'contentAwareHashing': {'waitOnContentHash': true}, |
| }), |
| ); |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: config, |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| final mergeGroupEvent = cocoon_checks.MergeGroupEvent.fromJson( |
| json.decode( |
| generateMergeGroupEventString( |
| repository: 'flutter/flutter', |
| action: 'checks_requested', |
| message: 'Implement an amazing feature', |
| ), |
| ) |
| as Map<String, Object?>, |
| ); |
| |
| await scheduler.handleMergeGroupEvent(mergeGroupEvent: mergeGroupEvent); |
| |
| expect(fakeContentAwareHash.triggered, [ |
| 'refs/heads/gh-readonly-queue/main/pr-15-c9affbbb12aa40cb3afbe94b9ea6b119a256bebf', |
| ]); |
| |
| expect( |
| firestore, |
| isNot( |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging |
| .hasSha('c9affbbb12aa40cb3afbe94b9ea6b119a256bebf') |
| .hasCheckRuns({ |
| 'Linux engine_build': TaskConclusion.scheduled, |
| 'Mac engine_build': TaskConclusion.scheduled, |
| }), |
| ]), |
| ), |
| ); |
| |
| verifyNever( |
| luci.getAvailableBuilderSet( |
| project: argThat(equals('flutter'), named: 'project'), |
| bucket: argThat(equals('prod'), named: 'bucket'), |
| ), |
| ); |
| |
| verifyNever( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| captureAny, |
| output: captureAnyNamed('output'), |
| ), |
| ); |
| verifyNever( |
| luci.scheduleMergeGroupBuilds( |
| targets: captureAnyNamed('targets'), |
| commit: anyNamed('commit'), |
| ), |
| ); |
| |
| expect(checkRuns, isEmpty); |
| }); |
| |
| test('handles missing builders', () async { |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionDualCiYaml); |
| final luci = MockLuciBuildService(); |
| when( |
| luci.getAvailableBuilderSet( |
| project: anyNamed('project'), |
| bucket: anyNamed('bucket'), |
| ), |
| ).thenAnswer((inv) async { |
| return {'Mac engine_build'}; |
| }); |
| when( |
| luci.scheduleTryBuilds( |
| targets: anyNamed('targets'), |
| pullRequest: anyNamed('pullRequest'), |
| engineArtifacts: anyNamed('engineArtifacts'), |
| ), |
| ).thenAnswer((inv) async { |
| return []; |
| }); |
| final mockGithubService = MockGithubService(); |
| final checkRuns = <CheckRun>[]; |
| when( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| any, |
| output: anyNamed('output'), |
| ), |
| ).thenAnswer((inv) async { |
| final slug = inv.positionalArguments[1] as RepositorySlug; |
| final sha = inv.positionalArguments[2] as String; |
| final name = inv.positionalArguments[3] as String?; |
| checkRuns.add( |
| createGithubCheckRun( |
| id: 1, |
| owner: slug.owner, |
| repo: slug.name, |
| sha: sha, |
| name: name, |
| ), |
| ); |
| return checkRuns.last; |
| }); |
| getFilesChanged.cannedFiles = ['abc/def']; |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: FakeConfig( |
| // tabledataResource: tabledataResource, |
| githubService: mockGithubService, |
| githubClient: MockGitHub(), |
| ), |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| final mergeGroupEvent = cocoon_checks.MergeGroupEvent.fromJson( |
| json.decode( |
| generateMergeGroupEventString( |
| repository: 'flutter/flutter', |
| action: 'checks_requested', |
| message: 'Implement an amazing feature', |
| ), |
| ) |
| as Map<String, Object?>, |
| ); |
| |
| await scheduler.handleMergeGroupEvent(mergeGroupEvent: mergeGroupEvent); |
| |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging |
| .hasSha('c9affbbb12aa40cb3afbe94b9ea6b119a256bebf') |
| .hasCheckRuns(contains('Mac engine_build')), |
| ]), |
| ); |
| |
| verify( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| captureAny, |
| output: captureAnyNamed('output'), |
| ), |
| ).called(1); |
| final result = verify( |
| luci.scheduleMergeGroupBuilds( |
| targets: captureAnyNamed('targets'), |
| commit: anyNamed('commit'), |
| ), |
| ); |
| expect(result.callCount, 1); |
| expect( |
| result.captured.cast<List<Target>>()[0].map((target) => target.name), |
| ['Mac engine_build'], |
| ); |
| |
| expect(checkRuns, hasLength(1)); |
| verifyNever( |
| mockGithubChecksUtil.updateCheckRun( |
| any, |
| Config.flutterSlug, |
| checkRuns[0], |
| status: anyNamed('status'), |
| conclusion: anyNamed('conclusion'), |
| output: anyNamed('output'), |
| ), |
| ); |
| }); |
| |
| test('fails the merge queue guard if fails to schedule checks', () async { |
| ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionDualCiYaml); |
| final luci = MockLuciBuildService(); |
| when( |
| luci.getAvailableBuilderSet( |
| project: anyNamed('project'), |
| bucket: anyNamed('bucket'), |
| ), |
| ).thenAnswer((inv) async { |
| return {'Mac engine_build', 'Linux engine_build'}; |
| }); |
| when( |
| luci.scheduleMergeGroupBuilds( |
| targets: anyNamed('targets'), |
| commit: anyNamed('commit'), |
| ), |
| ).thenThrow('Emulating failure'); |
| |
| final mockGithubService = MockGithubService(); |
| final checkRuns = <CheckRun>[]; |
| when( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| any, |
| output: anyNamed('output'), |
| ), |
| ).thenAnswer((inv) async { |
| final slug = inv.positionalArguments[1] as RepositorySlug; |
| final sha = inv.positionalArguments[2] as String; |
| final name = inv.positionalArguments[3] as String?; |
| checkRuns.add( |
| createGithubCheckRun( |
| id: 1, |
| owner: slug.owner, |
| repo: slug.name, |
| sha: sha, |
| name: name, |
| ), |
| ); |
| return checkRuns.last; |
| }); |
| getFilesChanged.cannedFiles = ['abc/def']; |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: FakeConfig( |
| // tabledataResource: tabledataResource, |
| githubService: mockGithubService, |
| githubClient: MockGitHub(), |
| ), |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: luci, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| |
| final mergeGroupEvent = cocoon_checks.MergeGroupEvent.fromJson( |
| json.decode( |
| generateMergeGroupEventString( |
| repository: 'flutter/flutter', |
| action: 'checks_requested', |
| message: 'Implement an amazing feature', |
| ), |
| ) |
| as Map<String, Object?>, |
| ); |
| |
| await scheduler.handleMergeGroupEvent(mergeGroupEvent: mergeGroupEvent); |
| |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging |
| .hasSha('c9affbbb12aa40cb3afbe94b9ea6b119a256bebf') |
| .hasCheckRuns( |
| allOf( |
| contains('Linux engine_build'), |
| contains('Mac engine_build'), |
| ), |
| ), |
| ]), |
| ); |
| |
| verify( |
| luci.getAvailableBuilderSet( |
| project: argThat(equals('flutter'), named: 'project'), |
| bucket: argThat(equals('prod'), named: 'bucket'), |
| ), |
| ).called(1); |
| |
| verify( |
| mockGithubChecksUtil.createCheckRun( |
| any, |
| any, |
| any, |
| captureAny, |
| output: captureAnyNamed('output'), |
| ), |
| ).called(1); |
| final result = verify( |
| luci.scheduleMergeGroupBuilds( |
| targets: captureAnyNamed('targets'), |
| commit: anyNamed('commit'), |
| ), |
| ); |
| expect(result.callCount, 1); |
| expect( |
| result.captured.cast<List<Target>>()[0].map((target) => target.name), |
| ['Linux engine_build', 'Mac engine_build'], |
| ); |
| |
| expect(checkRuns, hasLength(1)); |
| |
| // Expect the merge queue guard to be completed with failure. |
| final mergeQueueGuard = checkRuns.single; |
| expect(mergeQueueGuard.name, 'Merge Queue Guard'); |
| expect( |
| verify( |
| await mockGithubChecksUtil.updateCheckRun( |
| any, |
| Config.flutterSlug, |
| mergeQueueGuard, |
| status: captureAnyNamed('status'), |
| conclusion: captureAnyNamed('conclusion'), |
| output: anyNamed('output'), |
| ), |
| ).captured, |
| [CheckRunStatus.completed, CheckRunConclusion.failure], |
| ); |
| }); |
| }); |
| |
| group('framework-only PR optimization', () { |
| late MockGithubService mockGithubService; |
| late _CapturingFakeLuciBuildService fakeLuciBuildService; |
| |
| setUp(() { |
| mockGithubService = MockGithubService(); |
| fakeLuciBuildService = _CapturingFakeLuciBuildService(); |
| ciYamlFetcher.setCiYamlFrom( |
| singleCiYamlWithLinuxAnalyze, |
| engine: fusionCiYaml, |
| ); |
| |
| scheduler = Scheduler( |
| cache: cache, |
| config: FakeConfig( |
| githubService: mockGithubService, |
| githubClient: MockGitHub(), |
| maxFilesChangedForSkippingEnginePhaseValue: 29, |
| ), |
| githubChecksService: GithubChecksService( |
| config, |
| githubChecksUtil: mockGithubChecksUtil, |
| ), |
| getFilesChanged: getFilesChanged, |
| ciYamlFetcher: ciYamlFetcher, |
| luciBuildService: fakeLuciBuildService, |
| contentAwareHash: fakeContentAwareHash, |
| firestore: firestore, |
| bigQuery: bigQuery, |
| ); |
| }); |
| |
| test('still runs engine builds (DEPS)', () async { |
| getFilesChanged.cannedFiles = [ |
| 'DEPS', |
| 'packages/flutter/lib/material.dart', |
| ]; |
| final pullRequest = generatePullRequest(authorLogin: 'joe-flutter'); |
| |
| await scheduler.triggerPresubmitTargets(pullRequest: pullRequest); |
| expect( |
| fakeLuciBuildService.scheduledTryBuilds.map((t) => t.name), |
| ['Linux engine_build'], |
| reason: 'Should still run engine phase', |
| ); |
| }); |
| |
| test('still runs engine builds (engine/**)', () async { |
| getFilesChanged.cannedFiles = [ |
| 'engine/src/flutter/BUILD.gn', |
| 'packages/flutter/lib/material.dart', |
| ]; |
| final pullRequest = generatePullRequest(authorLogin: 'joe-flutter'); |
| |
| await scheduler.triggerPresubmitTargets(pullRequest: pullRequest); |
| expect( |
| fakeLuciBuildService.scheduledTryBuilds.map((t) => t.name), |
| ['Linux engine_build'], |
| reason: 'Should still run engine phase', |
| ); |
| }); |
| |
| test( |
| 'still runs engine builds (>=X files in changedFilesCount)', |
| () async { |
| getFilesChanged.cannedFiles = [ |
| // Irrelevant, never called. |
| ]; |
| config.maxFilesChangedForSkippingEnginePhaseValue = 1000; |
| |
| final pullRequest = generatePullRequest( |
| authorLogin: 'joe-flutter', |
| changedFilesCount: config.maxFilesChangedForSkippingEnginePhase, |
| ); |
| |
| await scheduler.triggerPresubmitTargets(pullRequest: pullRequest); |
| expect( |
| fakeLuciBuildService.scheduledTryBuilds.map((t) => t.name), |
| ['Linux engine_build'], |
| reason: 'Should still run engine phase', |
| ); |
| }, |
| ); |
| |
| test('skips engine builds', () async { |
| getFilesChanged.cannedFiles = ['packages/flutter/lib/material.dart']; |
| final pullRequest = generatePullRequest(); |
| |
| await scheduler.triggerPresubmitTargets(pullRequest: pullRequest); |
| expect( |
| fakeLuciBuildService.engineArtifacts, |
| EngineArtifacts.usingExistingEngine( |
| commitSha: pullRequest.base!.sha!, |
| ), |
| reason: 'Should use the base ref for the engine artifacts', |
| ); |
| expect( |
| fakeLuciBuildService.scheduledTryBuilds.map((t) => t.name), |
| ['Linux A', 'Linux analyze'], |
| reason: 'Should skip Linux engine_build', |
| ); |
| |
| expect( |
| firestore, |
| existsInStorage(CiStaging.metadata, [ |
| isCiStaging |
| .hasStage(CiStage.fusionEngineBuild) |
| .hasCheckRuns(isEmpty), |
| isCiStaging.hasStage(CiStage.fusionTests).hasCheckRuns({ |
| 'Linux A': TaskConclusion.scheduled, |
| 'Linux analyze': TaskConclusion.scheduled, |
| }), |
| ]), |
| ); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/167124. |
| test('skips all tests except "Linux analyze"', () async { |
| getFilesChanged.cannedFiles = ['CHANGELOG.md']; |
| final pullRequest = generatePullRequest(); |
| |
| await scheduler.triggerPresubmitTargets(pullRequest: pullRequest); |
| expect( |
| fakeLuciBuildService.engineArtifacts, |
| EngineArtifacts.usingExistingEngine( |
| commitSha: pullRequest.base!.sha!, |
| ), |
| reason: 'Should use the base ref for the engine artifacts', |
| ); |
| expect( |
| fakeLuciBuildService.scheduledTryBuilds.map((t) => t.name), |
| ['Linux analyze'], |
| reason: 'Only scheduled a special-cased build', |
| ); |
| }); |
| }); |
| }); |
| } |
| |
| final class _CapturingFakeLuciBuildService extends Fake |
| implements LuciBuildService { |
| List<Target> scheduledTryBuilds = []; |
| EngineArtifacts? engineArtifacts; |
| |
| @override |
| Future<List<Target>> scheduleTryBuilds({ |
| required List<Target> targets, |
| required PullRequest pullRequest, |
| CheckSuiteEvent? checkSuiteEvent, |
| EngineArtifacts? engineArtifacts, |
| }) async { |
| scheduledTryBuilds = targets; |
| this.engineArtifacts = engineArtifacts; |
| return targets; |
| } |
| |
| @override |
| Future<void> cancelBuilds({ |
| required PullRequest pullRequest, |
| required String reason, |
| }) async {} |
| } |