| // Copyright 2019 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:convert'; |
| |
| import 'package:cocoon_service/src/model/appengine/commit.dart'; |
| import 'package:cocoon_service/src/model/appengine/task.dart' as datastore; |
| import 'package:cocoon_service/src/model/ci_yaml/target.dart'; |
| import 'package:cocoon_service/src/model/firestore/task.dart'; |
| import 'package:cocoon_service/src/model/luci/push_message.dart' as pm; |
| import 'package:cocoon_service/src/service/firestore.dart'; |
| import 'package:googleapis/firestore/v1.dart'; |
| import 'package:mockito/mockito.dart'; |
| import 'package:test/test.dart'; |
| import 'package:buildbucket/buildbucket_pb.dart' as bbv2; |
| |
| import '../../src/utilities/entity_generators.dart'; |
| import '../../src/utilities/mocks.dart'; |
| |
| void main() { |
| group('Task', () { |
| test('disallows illegal status', () { |
| final Task task = Task(); |
| expect(() => task.setStatus('unknown'), throwsArgumentError); |
| }); |
| |
| test('creates task document correctly from task data model', () async { |
| final datastore.Task task = generateTask(1); |
| final String commitSha = task.commitKey!.id!.split('/').last; |
| final Task taskDocument = taskToDocument(task); |
| expect(taskDocument.name, '$kDatabase/documents/$kTaskCollectionId/${commitSha}_${task.name}_${task.attempts}'); |
| expect(taskDocument.createTimestamp, task.createTimestamp); |
| expect(taskDocument.endTimestamp, task.endTimestamp); |
| expect(taskDocument.bringup, task.isFlaky); |
| expect(taskDocument.taskName, task.name); |
| expect(taskDocument.startTimestamp, task.startTimestamp); |
| expect(taskDocument.status, task.status); |
| expect(taskDocument.testFlaky, task.isTestFlaky); |
| expect(taskDocument.commitSha, commitSha); |
| }); |
| |
| test('creates task documents correctly from targets', () async { |
| final Commit commit = generateCommit(1); |
| final List<Target> targets = <Target>[ |
| generateTarget(1, platform: 'Mac'), |
| generateTarget(2, platform: 'Linux'), |
| ]; |
| final List<Task> taskDocuments = targetsToTaskDocuments(commit, targets); |
| expect(taskDocuments.length, 2); |
| expect( |
| taskDocuments[0].name, |
| '$kDatabase/documents/$kTaskCollectionId/${commit.sha}_${targets[0].value.name}_$kTaskInitialAttempt', |
| ); |
| expect(taskDocuments[0].fields![kTaskCreateTimestampField]!.integerValue, commit.timestamp.toString()); |
| expect(taskDocuments[0].fields![kTaskEndTimestampField]!.integerValue, '0'); |
| expect(taskDocuments[0].fields![kTaskBringupField]!.booleanValue, false); |
| expect(taskDocuments[0].fields![kTaskNameField]!.stringValue, targets[0].value.name); |
| expect(taskDocuments[0].fields![kTaskStartTimestampField]!.integerValue, '0'); |
| expect(taskDocuments[0].fields![kTaskStatusField]!.stringValue, Task.statusNew); |
| expect(taskDocuments[0].fields![kTaskTestFlakyField]!.booleanValue, false); |
| expect(taskDocuments[0].fields![kTaskCommitShaField]!.stringValue, commit.sha); |
| }); |
| |
| group('updateFromBuild', () { |
| test('updates if buildNumber is null', () { |
| final DateTime created = DateTime.utc(2022, 1, 11, 1, 1); |
| final DateTime started = DateTime.utc(2022, 1, 11, 1, 2); |
| final DateTime completed = DateTime.utc(2022, 1, 11, 1, 3); |
| final pm.Build build = generatePushMessageBuild( |
| 1, |
| createdTimestamp: created, |
| startedTimestamp: started, |
| completedTimestamp: completed, |
| ); |
| final Task task = generateFirestoreTask(1); |
| |
| expect(task.status, Task.statusNew); |
| expect(task.buildNumber, isNull); |
| expect(task.endTimestamp, 0); |
| expect(task.createTimestamp, 0); |
| expect(task.startTimestamp, 0); |
| |
| task.updateFromBuild(build); |
| |
| expect(task.status, Task.statusSucceeded); |
| expect(task.buildNumber, 1); |
| expect(task.createTimestamp, created.millisecondsSinceEpoch); |
| expect(task.startTimestamp, started.millisecondsSinceEpoch); |
| expect(task.endTimestamp, completed.millisecondsSinceEpoch); |
| }); |
| |
| test('defaults timestamps to 0', () { |
| final pm.Build build = generatePushMessageBuild(1); |
| final Task task = generateFirestoreTask(1); |
| |
| expect(task.endTimestamp, 0); |
| expect(task.createTimestamp, 0); |
| expect(task.startTimestamp, 0); |
| |
| task.updateFromBuild(build); |
| |
| expect(task.endTimestamp, 0); |
| expect(task.createTimestamp, 0); |
| expect(task.startTimestamp, 0); |
| }); |
| |
| test('does not update status if older status', () { |
| final pm.Build build = generatePushMessageBuild( |
| 1, |
| status: pm.Status.started, |
| ); |
| final Task task = generateFirestoreTask( |
| 1, |
| buildNumber: 1, |
| status: Task.statusSucceeded, |
| ); |
| |
| expect(task.buildNumber, 1); |
| expect(task.status, Task.statusSucceeded); |
| |
| task.updateFromBuild(build); |
| |
| expect(task.buildNumber, 1); |
| expect(task.status, Task.statusSucceeded); |
| }); |
| |
| test('handles cancelled build', () { |
| final pm.Build build = generatePushMessageBuild( |
| 1, |
| status: pm.Status.completed, |
| result: pm.Result.canceled, |
| ); |
| final Task task = generateFirestoreTask( |
| 1, |
| buildNumber: 1, |
| status: Task.statusNew, |
| ); |
| |
| expect(task.status, Task.statusNew); |
| task.updateFromBuild(build); |
| expect(task.status, Task.statusCancelled); |
| }); |
| |
| test('handles infra failed build', () { |
| final pm.Build build = generatePushMessageBuild( |
| 1, |
| status: pm.Status.completed, |
| result: pm.Result.failure, |
| failureReason: pm.FailureReason.infraFailure, |
| ); |
| final Task task = generateFirestoreTask( |
| 1, |
| buildNumber: 1, |
| status: Task.statusNew, |
| ); |
| |
| expect(task.status, Task.statusNew); |
| task.updateFromBuild(build); |
| expect(task.status, Task.statusInfraFailure); |
| }); |
| }); |
| |
| group('updateFromBuildV2', () { |
| test('update succeeds from buildbucket v2', () async { |
| final bbv2.BuildsV2PubSub pubSubCallBack = bbv2.BuildsV2PubSub().createEmptyInstance(); |
| pubSubCallBack.mergeFromProto3Json(jsonDecode(buildBucketV2Message) as Map<String, dynamic>); |
| final bbv2.Build build = pubSubCallBack.build; |
| |
| final Task task = generateFirestoreTask( |
| 1, |
| name: build.builder.builder, |
| commitSha: 'asldjflaksdjflkasjdflkasjf', |
| ); |
| |
| final DateTime createTimeDateTime = DateTime.parse('2024-03-27T23:36:11.895266929Z'); |
| final DateTime startTimeDateTime = DateTime.parse('2024-03-27T23:36:18.758986946Z'); |
| final DateTime endTimeDateTime = DateTime.parse('2024-03-27T23:51:20.758986946Z'); |
| |
| expect(task.status, Task.statusNew); |
| expect(task.buildNumber, isNull); |
| expect(task.endTimestamp, 0); |
| expect(task.createTimestamp, 0); |
| expect(task.startTimestamp, 0); |
| |
| task.updateFromBuildV2(build); |
| expect(task.status, 'Succeeded'); |
| |
| expect(task.buildNumber, 561); |
| expect(task.createTimestamp, createTimeDateTime.millisecondsSinceEpoch); |
| expect(task.startTimestamp, startTimeDateTime.millisecondsSinceEpoch); |
| expect(task.endTimestamp, endTimeDateTime.millisecondsSinceEpoch); |
| }); |
| }); |
| }); |
| |
| // TODO(chillers): There is a bug where `dart test` does not work in offline mode. |
| // Need to file issue and get traces. |
| group('Task.fromFirestore', () { |
| late MockFirestoreService mockFirestoreService; |
| |
| setUp(() { |
| mockFirestoreService = MockFirestoreService(); |
| }); |
| |
| test('generates task correctly', () async { |
| final Task firestoreTask = generateFirestoreTask(1); |
| when( |
| mockFirestoreService.getDocument( |
| captureAny, |
| ), |
| ).thenAnswer((Invocation invocation) { |
| return Future<Document>.value( |
| firestoreTask, |
| ); |
| }); |
| final Task resultedTask = await Task.fromFirestore( |
| firestoreService: mockFirestoreService, |
| documentName: 'test', |
| ); |
| expect(resultedTask.name, firestoreTask.name); |
| expect(resultedTask.fields, firestoreTask.fields); |
| }); |
| }); |
| |
| group('resert as retry', () { |
| test('success', () { |
| final Task task = generateFirestoreTask( |
| 1, |
| status: Task.statusFailed, |
| testFlaky: true, |
| ); |
| task.resetAsRetry(attempt: 2); |
| |
| expect(int.parse(task.name!.split('_').last), 2); |
| expect(task.status, Task.statusNew); |
| expect(task.testFlaky, false); |
| }); |
| }); |
| |
| test('task facade', () { |
| final Task taskDocument = generateFirestoreTask(1); |
| final Map<String, dynamic> expectedResult = <String, dynamic>{ |
| kTaskDocumentName: taskDocument.name, |
| kTaskCommitSha: taskDocument.commitSha, |
| kTaskCreateTimestamp: taskDocument.createTimestamp, |
| kTaskStartTimestamp: taskDocument.startTimestamp, |
| kTaskEndTimestamp: taskDocument.endTimestamp, |
| kTaskTaskName: taskDocument.taskName, |
| kTaskAttempts: taskDocument.attempts, |
| kTaskBringup: taskDocument.bringup, |
| kTaskTestFlaky: taskDocument.testFlaky, |
| kTaskBuildNumber: taskDocument.buildNumber, |
| kTaskStatus: taskDocument.status, |
| }; |
| expect(taskDocument.facade, expectedResult); |
| }); |
| } |
| |
| String buildBucketV2Message = ''' |
| { |
| "build": { |
| "id": "8752269309051025889", |
| "builder": { |
| "project": "flutter-dashboard", |
| "bucket": "flutter", |
| "builder": "Mac_arm64 module_test_ios" |
| }, |
| "number": 561, |
| "createdBy": "user:dart-internal-flutter-engine@dart-ci-internal.iam.gserviceaccount.com", |
| "createTime": "2024-03-27T23:36:11.895266929Z", |
| "startTime": "2024-03-27T23:36:18.758986946Z", |
| "updateTime": "2024-03-27T23:51:20.758986946Z", |
| "endTime": "2024-03-27T23:51:20.758986946Z", |
| "status": "SUCCESS", |
| "tags": [ |
| { |
| "key": "buildset", |
| "value": "commit/gitiles/flutter.googlesource.com/mirrors/engine/+/e76c956498841e1ab458577d3892003e553e4f3c" |
| }, |
| { |
| "key": "parent_buildbucket_id", |
| "value": "8752269371711734033" |
| }, |
| { |
| "key": "parent_task_id", |
| "value": "689b160e60417e11" |
| }, |
| { |
| "key": "user_agent", |
| "value": "recipe" |
| }, |
| { |
| "key": "build_address", |
| "value": "luci.flutter.prod/Mac_arm64 module_test_ios/271" |
| } |
| ], |
| "exe": { |
| "cipdPackage": "flutter/recipe_bundles/flutter.googlesource.com/recipes", |
| "cipdVersion": "refs/heads/flutter-3.19-candidate.1", |
| "cmd": [ |
| "luciexe" |
| ] |
| }, |
| "schedulingTimeout": "21600s", |
| "executionTimeout": "14400s", |
| "gracePeriod": "30s", |
| "ancestorIds": [ |
| "8752269474035875297", |
| "8752269371711734033" |
| ] |
| }, |
| "buildLargeFields": "eJycVE+LI8UbTvdMksmbX+aXVBh2tmYXg66CwdqazcyusjCoC+LJk6jHslL1JqlNd1VbVZ2ZnZsieBE8eBSEBb+AB8GzX0PRs99CunsyCC7+2T4V1PO8/dbzPs97+SvAzwC3oeNRmQLJmI7QLo1FsZnxeWkyjR4m0FfOLsxSWJkjGdH/51KJlQtRNGA4hdFSZQZtFBvpjZxnGMgL09twBAfandvMSS2k1d4ZLTQWgaSTFhxAX+PGKBTxSYGkQ3etswg3Yewx+ifCbdB7o1FkJkSS0hbcgIHHDGVAUTdXcaIvEQbQtU4sXS5JOklgH3ZUUZIubUufPziF3zvQbgi/daa/dGAJ6dISORPQpW3GcqmgT3v1gamihGvikO4z5ksbTY4sdxqhR7tXLQDQPcasY1l0cEDHjBUeq79EpqWPLOg1HMFurdmYjirNts03xX9KoG2NfSzJj8n0h6QaQ6Pzs+FfJ9CN0i8xBvJFMvs8gSN6c5GVMaLn0bks8IWzUYRyHjDCi3SyvayfzqVXK7PB8FD6aBZSxQBv0ze3mLDCLONFJuPC+Zxr6c+N5blULjy8NIWoT+IKLRZe5nju/BoeARTeFeijwUBOpzM4huGdbVUXLkTQa3JrSuEQ+kGvxQZ9MM6SHu3eO8WT42P1T/77PoW9bfPku3T2NIVv0+k3KQxgt3ZOm+4sVYAZ9OaVXIWMK/IyfcmVkf9FRl49ZluNw9MEBsaqrNQNL5CvktmXSaXMv6A3KrHmphKfNeLfvTQFvEvf+Y8l2BWOX0+oLvS3JjqAtkeZ5eR/tBqELlU0zsIjGGrvLAptcrSV4IHcnb0GhA7/lLmzOnB92nPh7D2p2L2Tyt+qKM8u3njwnKm+AZ25i8JoMqD9KlAn99nq/uz15/DFIexLrUXjjSDUJ9d5P4R+7Q5b5nP0pDdqVd/Hn74FGYzvNISr5cRrO3wwfR9egb2IeSG08YTSQ/6hy8ocA//I+TUP/Jwbzy94hFehp6RaYQ28RekzgTUChtCrlo547OaB7NDkuNpEKnPzqqt00vosaf0RAAD//yypqMk=" |
| } |
| '''; |