blob: bf808f05b08fad1d147eebc2ca26f5684dfebafa [file] [log] [blame]
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. 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:io';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'cache_new.dart';
const String LUCI_HOST = "luci-milo.appspot.com";
const String CBE_HOST = "chrome-build-extract.appspot.com";
typedef void ModifyRequestFunction(HttpClientRequest request);
/// Base class for communicating with [Luci]
/// Some information is found through the api
/// <https://luci-milo.appspot.com/rpcexplorer/services/milo.Buildbot/>,
/// some information is found via Cbe
/// <https://chrome-build-extract.appspot.com/get_master/<client>> and
/// and some raw log files is found via [Luci]/log/raw.
class LuciApi {
final HttpClient _client = new HttpClient();
LuciApi();
/// [getJsonFromChromeBuildExtract] gets json from Cbe, with information
/// about all bots and current builds.
Future<dynamic> getJsonFromChromeBuildExtract(
String client, WithCacheFunction withCache) async {
return withCache(
() => _makeGetRequest(new Uri(
scheme: 'https',
host: CBE_HOST,
path: "/get_master/${client}")),
"cbe")
.then(jsonDecode);
}
/// Calling the Milo Api to get latest builds for this bot,
/// where the field [amount] is the number of recent builds to fetch.
Future<List<BuildDetail>> getBuildBotDetails(
String client, String botName, WithCacheFunction withCache,
[int amount = 20]) async {
var uri = new Uri(
scheme: "https",
host: LUCI_HOST,
path: "prpc/milo.Buildbot/GetBuildbotBuildsJSON");
var body = {
"master": client,
"builder": botName,
"limit": amount,
"includeCurrent": true
};
return withCache(
() => _makePostRequest(uri, jsonEncode(body), {
HttpHeaders.CONTENT_TYPE: "application/json",
HttpHeaders.ACCEPT: "application/json"
}),
'${uri.path}_${botName}_$amount')
.then(jsonDecode)
.then((json) {
return json["builds"].map((b) {
var build = jsonDecode(utf8.decode(base64Decode(b["data"])));
return getBuildDetailFromJson(client, botName, build);
}).toList();
});
}
/// Calling the Milo Api to get information about a specific build
/// where the field [buildNumber] is the build number to fetch.
Future<BuildDetail> getBuildBotBuildDetails(String client, String botName,
int buildNumber, WithCacheFunction withCache) async {
var uri = new Uri(
scheme: "https",
host: LUCI_HOST,
path: "prpc/milo.Buildbot/GetBuildbotBuildJSON");
var body = {"master": client, "builder": botName, "buildNum": buildNumber};
return withCache(
() => _makePostRequest(uri, jsonEncode(body), {
HttpHeaders.CONTENT_TYPE: "application/json",
HttpHeaders.ACCEPT: "application/json"
}),
'${uri.path}_${botName}_$buildNumber')
.then(jsonDecode)
.then((json) {
var build = jsonDecode(utf8.decode(base64Decode(json["data"])));
return getBuildDetailFromJson(client, botName, build);
});
}
/// [_makeGetRequest] performs a get request to [uri].
Future<String> _makeGetRequest(Uri uri) async {
var request = await _client.getUrl(uri);
var response = await request.close();
if (response.statusCode != 200) {
response.drain();
throw new HttpException(response.reasonPhrase, uri: uri);
}
return response.transform(utf8.decoder).join();
}
/// [_makePostRequest] performs a post request to [uri], where the posted
/// body is the string representation of [body]. For adding custom headers
/// use the map [headers].
Future<String> _makePostRequest(
Uri uri, Object body, Map<String, String> headers) async {
var response = await http.post(uri, body: body, headers: headers);
if (response.statusCode != 200) {
throw new HttpException(response.reasonPhrase, uri: uri);
}
// Prpc outputs a prefix to combat vulnerability.
if (response.body.startsWith(")]}'")) {
return response.body.substring(4);
}
return response.body;
}
/// Closes the Http client connection
void close() {
_client.close();
}
}
/// [getBuildDetailFromJson] parses json [build] to a class [BuildDetail]
BuildDetail getBuildDetailFromJson(
String client, String botName, dynamic build) {
List<GitCommit> changes = build["sourceStamp"]["changes"].map((change) {
return new GitCommit(
change["revision"],
change["revLink"],
change["who"],
change["comments"],
change["files"].map((file) => file["name"]).toList());
}).toList();
List<BuildProperty> properties = build["properties"].map((prop) {
return new BuildProperty(prop[0], prop[1].toString(), prop[2]);
}).toList();
DateTime parseDateTime(num value) {
if (value == null) return null;
return new DateTime.fromMillisecondsSinceEpoch((value * 1000).round());
}
List<BuildStep> steps = build["steps"].map((Map step) {
DateTime start = parseDateTime(step["times"][0]);
DateTime end = parseDateTime(step["times"][1]);
return new BuildStep(
step["name"],
step["text"].join(', '),
step["results"].toString(),
start,
end,
step["step_number"],
step["isStarted"],
step["isFinished"],
step["logs"].map((log) => new BuildLog(log[0], log[1])).toList());
}).toList();
Timing timing = new Timing(
parseDateTime(build["times"][0]), parseDateTime(build["times"][1]));
return new BuildDetail(
client,
botName,
build["number"],
build["text"].join(' '),
build["finished"],
steps,
properties,
build["blame"],
timing,
changes);
}
/// [BuildDetail] holds data detailing a specific build
class BuildDetail {
final String client;
final String botName;
final int buildNumber;
final String results;
final bool finished;
final List<BuildStep> steps;
final List<BuildProperty> buildProperties;
final List<String> blameList;
final Timing timing;
final List<GitCommit> allChanges;
BuildDetail(
this.client,
this.botName,
this.buildNumber,
this.results,
this.finished,
this.steps,
this.buildProperties,
this.blameList,
this.timing,
this.allChanges);
@override
String toString() {
StringBuffer buffer = new StringBuffer();
buffer.writeln("--------------------------------------");
buffer.writeln(results);
buffer.writeln(timing);
buffer.writeln("----------------STEPS-----------------");
if (steps != null) steps.forEach(buffer.writeln);
buffer.writeln("----------BUILD PROPERTIES------------");
if (buildProperties != null) buildProperties.forEach(buffer.writeln);
buffer.writeln("-------------BLAME LIST---------------");
if (blameList != null) blameList.forEach(buffer.writeln);
buffer.writeln("------------ALL CHANGES---------------");
if (allChanges != null) allChanges.forEach(buffer.writeln);
return buffer.toString();
}
}
/// [BuildStep] holds data detailing a specific build
class BuildStep {
final String name;
final String description;
final String result;
final DateTime start;
final DateTime end;
final int number;
final bool isStarted;
final bool isFinished;
final List<BuildLog> logs;
BuildStep(this.name, this.description, this.result, this.start, this.end,
this.number, this.isStarted, this.isFinished, this.logs);
@override
String toString() {
StringBuffer buffer = new StringBuffer();
buffer.writeln("${result == '[0, []]' ? 'SUCCESS' : result}: "
"$name - $description ($start, $end)");
logs.forEach((subLink) {
buffer.writeln("\t${subLink}");
});
return buffer.toString();
}
}
/// [BuildLog] holds log-information for a specific build.
class BuildLog {
final String name;
final String url;
BuildLog(this.name, this.url);
@override
String toString() {
return "$name | $url";
}
}
/// [BuildProperty] descibes build properties of a specific build.
class BuildProperty {
final String name;
final String value;
final String source;
BuildProperty(this.name, this.value, this.source);
@override
String toString() {
return "$name\t$value\t$source";
}
}
/// [Timing] is a class to hold timing information for builds and steps.
class Timing {
final DateTime start;
final DateTime end;
Timing(this.start, this.end);
@override
String toString() {
return "start: $start\tend: $end";
}
}
/// [GitCommit] holds data about a specific commit.
class GitCommit {
final String revision;
final String commitUrl;
final String changedBy;
final String comments;
final List<String> changedFiles;
GitCommit(this.revision, this.commitUrl, this.changedBy, this.comments,
this.changedFiles);
@override
String toString() {
StringBuffer buffer = new StringBuffer();
buffer.writeln("revision: $revision");
buffer.writeln("commitUrl: $commitUrl");
buffer.writeln("changedBy: $changedBy");
buffer.write("\n");
buffer.writeln(comments);
buffer.write("\nfiles:\n");
changedFiles.forEach(buffer.writeln);
return buffer.toString();
}
}