blob: 9ad7ea0545a0985d4eaded60bb8fec09d03ae5ef [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 'dart:async';
import 'package:meta/meta.dart';
import '../model/appengine/commit.dart';
import '../model/appengine/github_build_status_update.dart';
import '../model/appengine/stage.dart';
import '../model/appengine/task.dart';
import 'datastore.dart';
/// Function signature for a [BuildStatusService] provider.
typedef BuildStatusServiceProvider = BuildStatusService Function(
DatastoreService datastoreService);
/// Class that calculates the current build status.
class BuildStatusService {
const BuildStatusService(this.datastoreService)
: assert(datastoreService != null);
final DatastoreService datastoreService;
/// Creates and returns a [DatastoreService] using [db] and [maxEntityGroups].
static BuildStatusService defaultProvider(DatastoreService datastoreService) {
return BuildStatusService(datastoreService);
}
@visibleForTesting
static const int numberOfCommitsToReferenceForTreeStatus = 20;
/// Calculates and returns the "overall" status of the Flutter build.
///
/// This calculation operates by looking for the most recent success or
/// failure for every (non-flaky) task in the manifest.
///
/// Take the example build dashboard below:
/// ✔ = passed, ✖ = failed, ☐ = new, ░ = in progress, s = skipped
/// +---+---+---+---+
/// | A | B | C | D |
/// +---+---+---+---+
/// | ✔ | ☐ | ░ | s |
/// +---+---+---+---+
/// | ✔ | ░ | ✔ | ✖ |
/// +---+---+---+---+
/// | ✔ | ✖ | ✔ | ✔ |
/// +---+---+---+---+
/// This build will fail because only of Task B. Task D is not included in
/// the latest commit status, so it does not impact the build status.
/// Task B fails because its last known status was to be failing, even though
/// there is currently a newer version that is in progress.
Future<BuildStatus> calculateCumulativeStatus({String branch}) async {
final List<CommitStatus> statuses = await retrieveCommitStatus(
limit: numberOfCommitsToReferenceForTreeStatus,
branch: branch,
).toList();
if (statuses.isEmpty) {
return BuildStatus.failed;
}
final Map<String, bool> tasksInProgress =
_findTasksRelevantToLatestStatus(statuses);
if (tasksInProgress.isEmpty) {
return BuildStatus.failed;
}
for (CommitStatus status in statuses) {
for (Stage stage in status.stages) {
for (Task task in stage.tasks) {
/// If a task [isRelevantToLatestStatus] but has not run yet, we look
/// for a previous run of the task from the previous commit.
final bool isRelevantToLatestStatus =
tasksInProgress.containsKey(task.name);
/// Tasks that are not relevant to the latest status will have a
/// null value in the map.
final bool taskInProgress = tasksInProgress[task.name] ?? true;
if (isRelevantToLatestStatus && taskInProgress) {
if (task.isFlaky || _isSuccessful(task)) {
/// This task no longer needs to be checked to see if it causing
/// the build status to fail.
tasksInProgress[task.name] = false;
} else if (_isFailed(task) || _isRerunning(task)) {
return BuildStatus.failed;
}
}
}
}
}
return BuildStatus.succeeded;
}
/// Creates a map of the tasks that need to be checked for the build status.
///
/// This is based on the most recent [CommitStatus] and all of its tasks.
Map<String, bool> _findTasksRelevantToLatestStatus(
List<CommitStatus> statuses) {
final Map<String, bool> tasks = <String, bool>{};
for (Stage stage in statuses.first.stages) {
for (Task task in stage.tasks) {
tasks[task.name] = true;
}
}
return tasks;
}
/// Retrieves the comprehensive status of every task that runs per commit.
///
/// The returned stream will be ordered by most recent commit first, then
/// the next newest, and so on.
Stream<CommitStatus> retrieveCommitStatus(
{int limit, int timestamp, String branch}) async* {
await for (Commit commit in datastoreService.queryRecentCommits(
limit: limit, timestamp: timestamp, branch: branch)) {
final List<Stage> stages =
await datastoreService.queryTasksGroupedByStage(commit);
yield CommitStatus(commit, stages);
}
}
bool _isFailed(Task task) {
return task.status == Task.statusFailed;
}
bool _isSuccessful(Task task) {
return task.status == Task.statusSucceeded;
}
bool _isRerunning(Task task) {
return task.attempts > 1 &&
(task.status == Task.statusInProgress || task.status == Task.statusNew);
}
}
/// Class that holds the status for all tasks corresponding to a particular
/// commit.
///
/// Tasks may still be running, and thus their status is subject to change.
/// Put another way, this class holds information that is a snapshot in time.
@immutable
class CommitStatus {
/// Creates a new [CommitStatus].
const CommitStatus(this.commit, this.stages);
/// The commit against which all the tasks in [stages] are run.
final Commit commit;
/// The partitioned stages, each of which holds a bucket of tasks that
/// belong in the stage.
final List<Stage> stages;
}
@immutable
class BuildStatus {
const BuildStatus._(this.value);
final String value;
static const BuildStatus succeeded =
BuildStatus._(GithubBuildStatusUpdate.statusSuccess);
static const BuildStatus failed =
BuildStatus._(GithubBuildStatusUpdate.statusFailure);
String get githubStatus => value;
@override
String toString() => value;
}