blob: c09b2ef536060900bdbbcb5b5f80fd553a79de50 [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.
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';
import 'commit.dart';
import 'task.dart';
part 'stage.g.dart';
/// A group of related [Task]s run against a particular [Commit].
///
/// Stages are grouped by the infrastructure family that runs them, such as
/// LUCI, DeviceLab on Linux, DeviceLab on Windows, etc.
@immutable
@JsonSerializable(createFactory: false, ignoreUnannotated: true)
class Stage implements Comparable<Stage> {
const Stage(this.name, this.commit, this.tasks, this.taskStatus);
const Stage._(this.name, this.commit, this.tasks, this.taskStatus);
/// The fixed ordering of the stages (by name).
///
/// Unknown stages will be placed at the end of any ordering.
static const List<String?> _order = <String?>[
'chromebot',
'devicelab',
'devicelab_win',
'devicelab_ios',
];
/// Arbitrarily large index to represent the "end of the ordering".
static const int _endOfList = 1000000;
/// The name of the stage (e.g. 'devicelab', 'devicelab_win').
///
/// This is guaranteed to be non-null.
@JsonKey(name: 'Name')
final String? name;
/// The commit that owns this stage.
///
/// All [tasks] will be run against this commit.
final Commit? commit;
/// The list of tasks in this stage.
///
/// These tasks will be run against [commit]. This list is guaranteed to be
/// non-empty.
final List<Task> tasks;
/// Representation of [tasks] used for JSON serialization.
@JsonKey(name: 'Tasks')
List<SerializableTask> get serializableTasks {
return tasks.map<SerializableTask>((Task task) => SerializableTask(task)).toList();
}
/// The aggregate status, accounting for all [tasks] in this stage.
///
/// The status is defined as follows:
///
/// * If all tasks in this stage succeeded, then [Task.statusSucceeded]
/// * If at least one task in this stage failed, then [Task.statusFailed]
/// * If all tasks have the same status, then that status
/// * Else [Task.statusInProgress]
@JsonKey(name: 'Status')
final String taskStatus;
/// Whether this stage is managed by the Flutter device lab.
///
/// Stages such as 'chromebot' are not managed by the Flutter
/// device lab.
bool get isManagedByDeviceLab => name!.startsWith('devicelab');
@override
int compareTo(Stage other) => _orderIndex(this).compareTo(_orderIndex(other));
static int _orderIndex(Stage stage) {
int index = _order.indexOf(stage.name);
if (index == -1) {
// Put unknown stages last.
index = _endOfList;
}
return index;
}
/// Serializes this object to a JSON primitive.
Map<String, dynamic> toJson() => _$StageToJson(this);
@override
String toString() {
final StringBuffer buf = StringBuffer()
..write('$runtimeType(')
..write('name: $name')
..write(', commit: ${commit?.sha}')
..write(', tasks: ${tasks.length}')
..write(', taskStatus: $taskStatus')
..write(')');
return buf.toString();
}
}
/// A mutable class used to build instances of [Stage].
class StageBuilder {
/// The name of the stage.
///
/// See also:
/// * [Stage.name]
String? name;
/// The commit that owns the stage.
///
/// See also:
/// * [Stage.commit]
Commit? commit;
/// The tasks within the stage, run against [commit].
///
/// See also:
/// * [Stage.commit]
List<Task> tasks = <Task>[];
/// Builds a [Stage] from the information in this builder.
///
/// Throws a [StateError] if [name] is null, [commit] is null, or [tasks] is
/// empty.
Stage build() {
if (name == null) {
throw StateError('Cannot build a stage with no name');
}
if (commit == null) {
throw StateError('Cannot build a stage with no commit ($name)');
}
if (tasks.isEmpty) {
throw StateError('Cannot build a stage with no tasks ($name)');
}
return Stage._(name, commit, List<Task>.unmodifiable(tasks), _taskStatus);
}
String get _taskStatus {
assert(tasks.isNotEmpty);
bool isSucceeded(Task task) => task.status == Task.statusSucceeded;
bool isFailed(Task task) => task.status == Task.statusFailed;
if (tasks.every(isSucceeded)) {
return Task.statusSucceeded;
}
if (tasks.any(isFailed)) {
return Task.statusFailed;
}
final String? commonStatus =
tasks.map<String?>((Task task) => task.status).reduce((String? a, String? b) => a == b ? a : null);
return commonStatus ?? Task.statusInProgress;
}
}