blob: fa0e8bfefabf450430677548860f05f875ed023c [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:math' as math;
import 'package:fixnum/fixnum.dart';
import 'package:cocoon_service/protos.dart';
import '../logic/qualified_task.dart';
import 'cocoon.dart';
/// [CocoonService] for local development purposes.
///
/// This creates fake data that mimicks what production will send.
class DevelopmentCocoonService implements CocoonService {
DevelopmentCocoonService(this.now) : _random = math.Random(now.millisecondsSinceEpoch);
final math.Random _random;
final DateTime now;
@override
Future<CocoonResponse<List<CommitStatus>>> fetchCommitStatuses({
CommitStatus lastCommitStatus,
String branch,
}) async {
return CocoonResponse<List<CommitStatus>>.data(_createFakeCommitStatuses(lastCommitStatus));
}
@override
Future<CocoonResponse<bool>> fetchTreeBuildStatus({
String branch,
}) async {
return CocoonResponse<bool>.data(_random.nextBool());
}
@override
Future<CocoonResponse<List<Agent>>> fetchAgentStatuses() async {
return CocoonResponse<List<Agent>>.data(_createFakeAgentStatuses());
}
@override
Future<CocoonResponse<List<String>>> fetchFlutterBranches() async {
return const CocoonResponse<List<String>>.data(<String>['master', 'dev', 'beta', 'stable']);
}
@override
Future<bool> rerunTask(Task task, String accessToken) async {
return false;
}
@override
Future<bool> downloadLog(Task task, String idToken, String commitSha) async {
return false;
}
@override
Future<CocoonResponse<String>> createAgent(String agentId, List<String> capabilities, String idToken) async =>
const CocoonResponse<String>.data('abc123');
@override
Future<CocoonResponse<String>> authorizeAgent(Agent agent, String idToken) async =>
const CocoonResponse<String>.data('def345');
@override
Future<void> reserveTask(Agent agent, String idToken) => null;
static const List<String> _agentKinds = <String>[
'linux',
'linux-vm',
'mac',
'windows',
];
List<Agent> _createFakeAgentStatuses() {
return List<Agent>.generate(
10,
(int i) => Agent()
..agentId = 'fake-${_agentKinds[i % _agentKinds.length]}-${i ~/ _agentKinds.length}'
..capabilities.add('dash')
..isHealthy = _random.nextBool()
..isHidden = false
..healthCheckTimestamp = Int64.parseInt(now.millisecondsSinceEpoch.toString())
..healthDetails = 'ssh-connectivity: succeeded\n'
'Last known IP address: flutter-devicelab-linux-vm-1\n\n'
'android-device-ZY223D6B7B: succeeded\n'
'has-healthy-devices: succeeded\n'
'Found 1 healthy devices\n\n'
'cocoon-authentication: succeeded\n'
'cocoon-connection: succeeded\n'
'able-to-perform-health-check: succeeded\n',
);
}
static const int _commitGap = 2 * 60 * 1000; // 2 minutes between commits
List<CommitStatus> _createFakeCommitStatuses(CommitStatus lastCommitStatus) {
final int baseTimestamp =
lastCommitStatus != null ? (lastCommitStatus.commit.timestamp.toInt()) : now.millisecondsSinceEpoch;
final List<CommitStatus> result = <CommitStatus>[];
for (int index = 0; index < 25; index += 1) {
final int commitTimestamp = baseTimestamp - ((index + 1) * _commitGap);
final math.Random random = math.Random(commitTimestamp);
final Commit commit = _createFakeCommit(commitTimestamp, random);
final CommitStatus status = CommitStatus()
..branch = 'master'
..commit = commit
..stages.addAll(_createFakeStages(commitTimestamp, commit, random));
result.add(status);
}
return result;
}
final List<String> _authors = <String>['alice', 'bob', 'charlie', 'dobb', 'eli', 'fred'];
Commit _createFakeCommit(int commitTimestamp, math.Random random) {
final int author = random.nextInt(_authors.length);
return Commit()
..key = (RootKey()..child = (Key()..name = '$commitTimestamp'))
..author = _authors[author]
..authorAvatarUrl = 'https://avatars2.githubusercontent.com/u/${2148558 + author}?v=4'
..repository = 'flutter/cocoon'
..sha = commitTimestamp.hashCode.toRadixString(16).padLeft(32, '0')
..timestamp = Int64(commitTimestamp)
..branch = 'master';
}
static const List<String> _stages = <String>[
'cirrus',
'chromebot',
'devicelab',
'devicelab_win',
'devicelab_ios',
];
static const List<int> _stageCount = <int>[
2,
3,
50,
25,
30,
];
List<Stage> _createFakeStages(int commitTimestamp, Commit commit, math.Random random) {
final List<Stage> stages = <Stage>[];
assert(_stages.length == _stageCount.length);
for (int stage = 0; stage < _stages.length; stage += 1) {
stages.add(
Stage()
..commit = commit
..name = _stages[stage]
..tasks.addAll(List<Task>.generate(
_stageCount[stage], (int i) => _createFakeTask(commitTimestamp, i, _stages[stage], random))),
);
}
return stages;
}
static const List<String> _statuses = <String>[
'New',
'In Progress',
'Succeeded',
'Succeeded Flaky',
'Failed',
'Underperformed',
'Underperfomed In Progress',
'Skipped',
];
static const Map<String, int> _minAttempts = <String, int>{
'New': 0,
'In Progress': 1,
'Succeeded': 1,
'Succeeded Flaky': 1,
'Failed': 1,
'Underperformed': 1,
'Underperfomed In Progress': 1,
'Skipped': 0,
};
static const Map<String, int> _maxAttempts = <String, int>{
'New': 0,
'In Progress': 1,
'Succeeded': 1,
'Succeeded Flaky': 2,
'Failed': 2,
'Underperformed': 2,
'Underperfomed In Progress': 2,
'Skipped': 0,
};
Task _createFakeTask(int commitTimestamp, int index, String stageName, math.Random random) {
final int age = (now.millisecondsSinceEpoch - commitTimestamp) ~/ _commitGap;
assert(age >= 0);
// The [statusesProbability] list is an list of proportional
// weights to give each of the values in _statuses when randomly
// determining the status. So e.g. if one is 150, another 50, and
// the rest 0, then the first has a 75% chance of being picked,
// the second a 25% chance, and the rest a 0% chance.
final List<int> statusesProbability = <int>[
// bigger = more probable
math.max(index % 2, 20 - age * 2), // blue
math.max(0, 10 - age * 2), // spinny
math.min(10 + age * 2, 100), // green
math.min(1 + age ~/ 3, 30), // yellow
if (index % 15 == 0) // red
5
else if (index % 25 == 0) // red
15
else
1,
1, // orange
1, // orange spinny
if (index == now.millisecondsSinceEpoch % 20) // white
math.max(0, 1000 - age * 20)
else if (index == now.millisecondsSinceEpoch % 22)
math.max(0, 1000 - age * 10)
else
0,
];
// max is the sum of all the values in statusesProbability.
final int max = statusesProbability.fold(0, (int c, int p) => c + p);
// weightedIndex is the random number in the range 0 <= weightedIndex < max.
int weightedIndex = random.nextInt(max);
// statusIndex is the actual index into _statuses that corresponds
// to the randomly selected weightedIndex. So if
// statusesProbability is 10,20,30 and weightedIndex is 15, then
// the statusIndex will be 1 (corresponding to the second entry,
// the one with weight 20, since lists are zero-indexed).
int statusIndex = 0;
while (weightedIndex > statusesProbability[statusIndex]) {
weightedIndex -= statusesProbability[statusIndex];
statusIndex += 1;
}
// Finally we get the actual status using statusIndex as an index into _statuses.
final String status = _statuses[statusIndex];
final int minAttempts = _minAttempts[status];
final int maxAttempts = _maxAttempts[status];
final int attempts = minAttempts + random.nextInt(maxAttempts - minAttempts + 1);
final Task task = Task()
..createTimestamp = Int64(commitTimestamp + index)
..startTimestamp = Int64(commitTimestamp + index + 10000)
..endTimestamp = Int64(commitTimestamp + index + 10000 + random.nextInt(1000 * 60 * 15))
..name = 'task $index'
..attempts = attempts
..isFlaky = index == now.millisecondsSinceEpoch % 13
..requiredCapabilities.add('[linux/android]')
..reservedForAgentId = 'linux1'
..stageName = stageName
..status = status;
if (stageName == StageName.luci) {
task
..buildNumberList = '$index'
..builderName = 'Linux'
..luciBucket = 'luci.flutter.prod';
}
return task;
}
}