blob: 312804ac0c7aa8e810a225a86240ec05dd78e8f7 [file] [log] [blame]
// 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.
/// @docImport 'commit.dart';
library;
import 'package:buildbucket/buildbucket_pb.dart' as bbv2;
import 'package:cocoon_common/task_status.dart';
import 'package:googleapis/firestore/v1.dart' hide Status;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import '../../../cocoon_service.dart';
import '../../service/firestore.dart';
import '../bbv2_extension.dart';
import '../ci_yaml/target.dart';
import '../task_ref.dart';
import 'base.dart';
import 'commit.dart' as fs;
const String kTaskCollectionId = 'tasks';
/// Represents the [documentName] of a Firestore document.
@immutable
final class TaskId extends AppDocumentId<Task> {
TaskId({
required this.commitSha,
required this.taskName,
required this.currentAttempt,
}) {
if (currentAttempt < 1) {
throw RangeError.value(
currentAttempt,
'currentAttempt',
'Must be at least 1',
);
}
}
/// Parse the inverse of [TaskId.documentName].
factory TaskId.parse(String documentName) {
final result = tryParse(documentName);
if (result == null) {
throw FormatException(
'Unexpected firestore task document name: "$documentName"',
);
}
return result;
}
/// Tries to parse the inverse of [TaskId.documentName].
///
/// If could not be parsed, returns `null`.
static TaskId? tryParse(String documentName) {
if (_parseDocumentName.matchAsPrefix(documentName) case final match?) {
final commitSha = match.group(1)!;
final taskName = match.group(2)!;
final currentAttempt = int.tryParse(match.group(3)!);
if (currentAttempt != null) {
return TaskId(
commitSha: commitSha,
taskName: taskName,
currentAttempt: currentAttempt,
);
}
}
return null;
}
/// Parses `{commitSha}_{taskName}_{currentAttempt}`.
///
/// This is gross because the [taskName] could also include underscores.
static final _parseDocumentName = RegExp(r'([a-z0-9]+)_(.*)_([0-9]+)$');
/// The commit SHA of the code being built.
final String commitSha;
/// The task name (i.e. from `.ci.yaml`).
final String taskName;
/// Which run (or re-run) attempt, starting at 1, this is.
final int currentAttempt;
@override
String get documentId {
return [commitSha, taskName, currentAttempt].join('_');
}
@override
AppDocumentMetadata<Task> get runtimeMetadata => Task.metadata;
}
/// Representation of each task (column) per _row_ on https://flutter-dashboard.appspot.com/#/build.
///
/// Provides enough information to render a build status without querying LUCI,
/// and is also used to do some light analysis-based tasks (based on recent
/// tasks). Each [commitSha] is associated with a [Commit.sha].
///
/// This documents layout is currently:
/// ```
/// /projects/flutter-dashboard/databases/cocoon/commits/
/// document: <this.commitSha>_<this.taskName>_<this.attempt>
///
/// See also: [TaskId].
final class Task extends AppDocument<Task> {
static const fieldBringup = 'bringup';
static const fieldBuildNumber = 'buildNumber';
static const fieldCommitSha = 'commitSha';
static const fieldCreateTimestamp = 'createTimestamp';
static const fieldEndTimestamp = 'endTimestamp';
static const fieldName = 'name';
static const fieldStartTimestamp = 'startTimestamp';
static const fieldStatus = 'status';
static const fieldTestFlaky = 'testFlaky';
static const fieldAttempt = 'attempt';
/// Returns a document ID for a task from the given parameters.
static AppDocumentId<Task> documentIdFor({
required String commitSha,
required String taskName,
required int currentAttempt,
}) {
return TaskId(
commitSha: commitSha,
taskName: taskName,
currentAttempt: currentAttempt,
);
}
@override
AppDocumentMetadata<Task> get runtimeMetadata => metadata;
/// Description of the document in Firestore.
static final metadata = AppDocumentMetadata<Task>(
collectionId: kTaskCollectionId,
fromDocument: Task.fromDocument,
);
/// Lookup [Task] from Firestore.
///
/// `documentName` follows `/projects/{project}/databases/{database}/documents/{document_path}`
static Future<Task> fromFirestore(
FirestoreService firestoreService,
AppDocumentId<Task> id,
) async {
final document = await firestoreService.getDocument(
p.posix.join(kDatabase, 'documents', kTaskCollectionId, id.documentId),
);
return Task.fromDocument(document);
}
factory Task({
required String builderName,
required int currentAttempt,
required String commitSha,
required bool bringup,
required int createTimestamp,
required int startTimestamp,
required int endTimestamp,
required TaskStatus status,
required bool testFlaky,
required int? buildNumber,
}) {
final name = TaskId(
taskName: builderName,
currentAttempt: currentAttempt,
commitSha: commitSha,
);
return Task._(
{
fieldName: builderName.toValue(),
fieldCommitSha: commitSha.toValue(),
fieldBringup: bringup.toValue(),
if (buildNumber != null) fieldBuildNumber: buildNumber.toValue(),
fieldCreateTimestamp: createTimestamp.toValue(),
fieldStartTimestamp: startTimestamp.toValue(),
fieldEndTimestamp: endTimestamp.toValue(),
fieldStatus: status.value.toValue(),
fieldTestFlaky: testFlaky.toValue(),
fieldAttempt: currentAttempt.toValue(),
},
name: p.posix.join(
kDatabase,
'documents',
kTaskCollectionId,
name.documentId,
),
);
}
/// Create [Task] from a task Document.
factory Task.fromDocument(Document document) {
return Task._(document.fields!, name: document.name!);
}
factory Task.initialFromTarget(Target target, {required fs.Commit commit}) {
return Task(
currentAttempt: 1,
createTimestamp: commit.createTimestamp,
bringup: target.isBringup,
status: TaskStatus.waitingForBackfill,
commitSha: commit.sha,
builderName: target.name,
buildNumber: null,
startTimestamp: 0,
endTimestamp: 0,
testFlaky: false,
);
}
Task._(Map<String, Value> fields, {required String name}) {
this
..fields = fields
..name = name;
}
/// Returns a Firestore [Write] that patches the [status] field for [id].
@useResult
static Write patchStatus(AppDocumentId<Task> id, TaskStatus status) {
return Write(
currentDocument: Precondition(exists: true),
update: Document(
name: p.posix.join(
kDatabase,
'documents',
kTaskCollectionId,
id.documentId,
),
fields: {fieldStatus: Value(stringValue: status.value)},
),
updateMask: DocumentMask(fieldPaths: [fieldStatus]),
);
}
/// The task was run successfully.
static const statusSucceeded = TaskStatus.succeeded;
/// The timestamp (in milliseconds since the Epoch) that this task was
/// created.
///
/// This is _not_ when the task first started running, as tasks start out in
/// the 'New' state until they've been picked up by an [Agent].
int get createTimestamp =>
int.parse(fields[fieldCreateTimestamp]!.integerValue!);
/// The timestamp (in milliseconds since the Epoch) that this task started
/// running.
///
/// Tasks may be run more than once. If this task has been run more than
/// once, this timestamp represents when the task was most recently started.
int get startTimestamp =>
int.parse(fields[fieldStartTimestamp]!.integerValue!);
/// The timestamp (in milliseconds since the Epoch) that this task last
/// finished running.
int get endTimestamp => int.parse(fields[fieldEndTimestamp]!.integerValue!);
/// The name of the task.
///
/// This is a human-readable name, typically a test name (e.g.
/// "hello_world__memory").
String get taskName => fields[fieldName]!.stringValue!;
/// The sha of the task commit.
String get commitSha => fields[fieldCommitSha]!.stringValue!;
/// The number of attempts that have been made to run this task successfully.
///
/// New tasks that have not yet been picked up by an [Agent] will have zero
/// attempts.
int get currentAttempt {
// TODO(matanlurey): Simplify this when existing documents are backfilled.
if (fields.containsKey(fieldAttempt)) {
return int.parse(fields[fieldAttempt]!.integerValue!);
}
// Read the attempts from the document name.
final documentId = p.basename(name!);
return TaskId.parse(documentId).currentAttempt;
}
///
/// See also:
///
/// * <https://github.com/flutter/flutter/blob/master/.ci.yaml>
///
/// A flaky (`bringup: true`) task will not block the tree.
bool get bringup => fields[fieldBringup]!.booleanValue!;
/// Whether the test execution of this task shows flake.
///
/// Test runner supports rerun, and this flag tracks if a flake happens.
///
/// See also:
/// * <https://github.com/flutter/flutter/blob/master/dev/devicelab/lib/framework/runner.dart>
bool get testFlaky => fields[fieldTestFlaky]!.booleanValue!;
/// The build number of luci build: https://chromium.googlesource.com/infra/luci/luci-go/+/master/buildbucket/proto/build.proto#146
int? get buildNumber => fields.containsKey(fieldBuildNumber)
? int.parse(fields[fieldBuildNumber]!.integerValue!)
: null;
/// The status of the task.
TaskStatus get status {
final rawValue = fields[fieldStatus]!.stringValue!;
return TaskStatus.from(rawValue);
}
void setStatus(TaskStatus status) {
fields[fieldStatus] = status.value.toValue();
}
void setEndTimestamp(int endTimestamp) {
fields[fieldEndTimestamp] = endTimestamp.toValue();
}
void setTestFlaky(bool testFlaky) {
fields[fieldTestFlaky] = testFlaky.toValue();
}
void updateFromBuild(bbv2.Build build) {
fields[fieldBuildNumber] = build.number.toValue();
fields[fieldCreateTimestamp] = build.createTime
.toDateTime()
.millisecondsSinceEpoch
.toValue();
fields[fieldStartTimestamp] = build.startTime
.toDateTime()
.millisecondsSinceEpoch
.toValue();
fields[fieldEndTimestamp] = build.endTime
.toDateTime()
.millisecondsSinceEpoch
.toValue();
_setStatusFromLuciStatus(build);
}
void resetAsRetry({int? attempt, DateTime? now}) {
attempt ??= currentAttempt + 1;
name = p.posix.join(
kDatabase,
'documents',
kTaskCollectionId,
Task.documentIdFor(
commitSha: commitSha,
currentAttempt: attempt,
taskName: taskName,
).documentId,
);
now ??= DateTime.now();
fields = <String, Value>{
fieldCreateTimestamp: now.millisecondsSinceEpoch.toValue(),
fieldEndTimestamp: 0.toValue(),
fieldBringup: bringup.toValue(),
fieldName: taskName.toValue(),
fieldStartTimestamp: 0.toValue(),
fieldStatus: TaskStatus.waitingForBackfill.value.toValue(),
fieldTestFlaky: false.toValue(),
fieldCommitSha: commitSha.toValue(),
fieldAttempt: attempt.toValue(),
};
}
void setBuildNumber(int buildNumber) {
fields[fieldBuildNumber] = buildNumber.toValue();
}
void _setStatusFromLuciStatus(bbv2.Build build) {
// Updates can come out of order. Ensure completed statuses are kept.
if (status.isComplete) {
return;
}
setStatus(build.status.toTaskStatus());
}
/// Returns an immutable reference to the commit modeled by `this`.
///
/// Changes to `this` are not reflected on the result.
TaskRef toRef() {
return TaskRef(
name: taskName,
currentAttempt: currentAttempt,
status: status,
commitSha: commitSha,
);
}
}