Chromium Code Reviews| Index: tools/gardening/lib/src/luci_api.dart |
| diff --git a/tools/gardening/lib/src/luci_api.dart b/tools/gardening/lib/src/luci_api.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..fe280c766ef0540845f371e8330968de55f2205b |
| --- /dev/null |
| +++ b/tools/gardening/lib/src/luci_api.dart |
| @@ -0,0 +1,349 @@ |
| +import 'dart:io'; |
|
Johnni Winther
2017/08/28 07:14:03
Add copyright header (for all .dart files):
// Co
|
| +import 'dart:async'; |
| +import 'dart:convert'; |
| +import 'package:http/http.dart' as http; |
| +import 'package:html/parser.dart' show parse; |
| +import 'try.dart'; |
| +import 'cache_new.dart'; |
| + |
| +const String LUCI_HOST = "luci-milo.appspot.com"; |
| +// const String BUILD_CHROMIUM_HOST = "build.chromium.org"; |
|
Johnni Winther
2017/08/28 07:14:03
Remove?
|
| + |
| +typedef void ModifyRequestFunction(HttpClientRequest request); |
| + |
| +/// Base class for communicating with Milo @ LogDog |
| +/// Some information is found through the api |
| +/// <https://docs.google.com/document/d/1HbPp7Sy7ofC |
| +/// U7C9USqcE91VubGg_IIET2GUj9iknev4/edit#> |
| +/// and some information is found via screen-scraping. |
| +class LuciApi { |
|
Johnni Winther
2017/08/28 07:14:04
Just call this [Luci].
|
| + final HttpClient _client = new HttpClient(); |
| + |
| + LuciApi(); |
| + |
| + /// [getBuildBots] fetches all build bots from luci (we cannot |
| + /// get this from the api). The format is: |
| + /// <li> |
| + /// <a href="/buildbot/client.crashpad/crashpad_win_x86_wow64_rel"> |
| + /// crashpad_win_x86_wow64_rel</a> |
| + /// </li> |
| + /// <h3> client.dart </h3> |
| + /// <li> |
| + /// <a href="/buildbot/client.dart/analyze-linux-be">analyze-linux-be</a> |
| + /// </li> |
| + /// <li> |
| + /// <a href="/buildbot/client.dart/analyze-linux-dev">analyze-linux-dev</a> |
|
Johnni Winther
2017/08/28 07:14:04
Long line.
|
| + /// </li> |
| + /// <li> |
| + /// <a href="/buildbot/client.dart/analyze-linux-stable"> |
| + /// analyze-linux-stable</a> |
| + /// </li> |
| + /// <li> |
| + /// <a href="/buildbot/client.dart/analyzer-linux-release-be"> |
| + /// analyzer-linux-release-be</a> |
| + /// </li> |
| + /// |
| + /// We look for the section header matching clients, then |
| + /// if we are in the right section, we take the <li> element |
| + /// and transform to a build bot |
| + /// |
| + Future<Try<List<LuciBuildBot>>> getAllBuildBots( |
| + String client, WithCacheFunction withCache) async { |
| + return await tryStartAsync(() => withCache( |
| + () => _makeGetRequest( |
| + new Uri(scheme: 'https', host: LUCI_HOST, path: "/")), |
| + "all_buildbots")) |
| + .then((Try<String> tryRes) => tryRes.bind(parse).bind((htmlDoc) { |
| + var takeSection = |
| + false; // this is really dirty, but the structure of |
|
Johnni Winther
2017/08/28 07:14:04
Move the comment about the variable declaration (d
|
| + // the document is not really suited for anything else |
| + return htmlDoc.body.children.where((node) { |
| + if (node.localName == "li") return takeSection; |
| + if (node.localName != "h3") { |
| + takeSection = false; |
| + return false; |
| + } |
| + // current node is <h3> |
| + takeSection = client == node.text.trim(); |
| + return false; |
| + }); |
| + }).bind((elements) { |
| + // here we hold an iterable of buildbot elements |
| + // <li> |
| + // <a href="/buildbot/client.dart/analyzer-linux-release-be"> |
| + // analyzer-linux-release-be</a> |
| + // </li> |
| + return elements.map((element) { |
| + var name = element.children[0].text; |
| + var url = element.children[0].attributes['href']; |
| + return new LuciBuildBot(client, name, url); |
| + }).toList(); |
| + })); |
| + } |
| + |
| + /// [getPrimaryBuilders] fetches all primary builders |
| + /// (the ones usually used by gardeners) by not including buildbots with |
| + /// the name -dev, -stable or -integration. |
| + Future<Try<List<LuciBuildBot>>> getPrimaryBuilders( |
| + String client, WithCacheFunction withCache) async { |
| + return await getAllBuildBots(client, withCache) |
| + .then((Try<List<LuciBuildBot>> tryRes) { |
| + return tryRes |
| + .bind((buildBots) => buildBots.where((LuciBuildBot buildBot) { |
| + return !(buildBot.name.contains("-dev") || |
| + buildBot.name.contains("-stable") || |
| + buildBot.name.contains("-integration")); |
| + })); |
| + }); |
| + } |
| + |
| + /// Calling the Milo Api to get latest builds for this bot, |
| + /// where the field [amount] is the number of recent builds to fetch |
|
Johnni Winther
2017/08/28 07:14:04
Add period to end the sentence.
|
| + Future<Try<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 |
| + }; |
| + var result = await tryStartAsync(() => withCache( |
| + () => _makePostRequest(uri, JSON.encode(body), { |
| + HttpHeaders.CONTENT_TYPE: "application/json", |
| + HttpHeaders.ACCEPT: "application/json" |
| + }), |
| + '${uri.path.toString()}_${botName}_$amount')); |
|
Johnni Winther
2017/08/28 07:14:04
Just ${uri.path} since [path] is already a string.
|
| + return result.bind(JSON.decode).bind((json) { |
| + return json["builds"].map((b) { |
| + var build = JSON.decode(UTF8.decode(BASE64.decode(b["data"]))); |
| + return getBuildDetailFromJson(client, botName, build); |
| + }).toList(); |
| + }); |
| + } |
| + |
| + /// Calling the Milo Api to get latest builds for this bot, |
| + /// where the field [amount] is the number of recent builds to fetch |
|
Johnni Winther
2017/08/28 07:14:03
Add period to end the sentence.
|
| + Future<Try<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}; |
| + print(body); |
| + var result = await tryStartAsync(() => withCache( |
| + () => _makePostRequest(uri, JSON.encode(body), { |
| + HttpHeaders.CONTENT_TYPE: "application/json", |
| + HttpHeaders.ACCEPT: "application/json" |
| + }), |
| + '${uri.path.toString()}_${botName}_$buildNumber')); |
| + return result.bind(JSON.decode).bind((json) { |
| + var build = JSON.decode(UTF8.decode(BASE64.decode(json["data"]))); |
| + return getBuildDetailFromJson(client, botName, build); |
| + }); |
| + } |
| + |
| + 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(); |
| + } |
| + |
| + 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 |
|
Johnni Winther
2017/08/28 07:14:04
Add period to end the sentence.
|
| + if (response.body.startsWith(")]}'")) { |
| + return response.body.substring(4); |
| + } |
| + return response.body; |
| + } |
| + |
| + void close() { |
| + _client.close(); |
| + } |
| +} |
| + |
| +BuildDetail getBuildDetailFromJson( |
|
Johnni Winther
2017/08/28 07:14:03
Add documentation.
|
| + 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"]) |
| + ..changedFiles = change["files"].map((file) => file["name"]); |
| + }).toList(); |
| + |
| + List<BuildProperty> properties = build["properties"].map((prop) { |
| + return new BuildProperty(prop[0], prop[1].toString(), prop[2]); |
| + }).toList(); |
| + |
| + List<BuildStep> steps = build["steps"].map((step) { |
| + var start = |
| + new DateTime.fromMillisecondsSinceEpoch(step["times"][0] * 1000); |
| + DateTime end = null; |
| + if (step["times"][1] != null) { |
| + end = new DateTime.fromMillisecondsSinceEpoch(step["times"][1] * 1000); |
| + } |
| + return new BuildStep(step["name"], step["text"], step["results"].toString(), |
| + start, end, step["step_number"], step["isStarted"], step["isFinished"]) |
| + ..logs = step["logs"].map((log) => new BuildLog(log[0], log[1])); |
| + }).toList(); |
| + |
| + DateTime end = null; |
| + if (build["times"][1] != null) { |
| + end = new DateTime.fromMillisecondsSinceEpoch(build["times"][1] * 1000); |
| + } |
| + |
| + Timing timing = new Timing( |
| + new DateTime.fromMillisecondsSinceEpoch(build["times"][0] * 1000), end); |
| + |
| + return new BuildDetail() |
| + ..client = client |
| + ..botName = botName |
| + ..buildNumber = build["number"] |
| + ..results = build["text"].join(' ') |
| + ..finished = build["finished"] |
| + ..blameList = build["blame"] |
| + ..allChanges = changes |
| + ..buildProperties = properties |
| + ..steps = steps |
| + ..timing = timing; |
| +} |
| + |
| +// Structured classes to relay information from api and web pages |
| + |
| +/// [LuciBuildBot] holds information about a build bot |
| +class LuciBuildBot { |
| + String client; |
| + String name; |
| + String url; |
|
Johnni Winther
2017/08/28 07:14:03
Make the fields final.
|
| + |
| + LuciBuildBot(this.client, this.name, this.url); |
| + |
| + @override |
| + String toString() { |
| + return "LuciBuildBot { client: $client, name: $name, url: $url }"; |
| + } |
| +} |
| + |
| +class BuildDetail { |
|
Johnni Winther
2017/08/28 07:14:03
Add document to the class and all its fields.
|
| + String client; |
| + String botName; |
| + int buildNumber; |
| + String results; |
| + bool finished; |
| + List<BuildStep> steps; |
| + List<BuildProperty> buildProperties; |
| + List<String> blameList; |
| + Timing timing; |
| + List<GitCommit> allChanges; |
|
Johnni Winther
2017/08/28 07:14:04
Add a constructor that takes these as argument and
|
| + |
| + @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(); |
| + } |
| +} |
| + |
| +class BuildStep { |
|
Johnni Winther
2017/08/28 07:14:03
Add document to the class and all its fields.
|
| + final String name; |
| + final String description; |
| + final String result; |
| + final DateTime start; |
| + final DateTime end; |
| + final int number; |
| + final bool isStarted; |
| + final bool isFinished; |
| + List<BuildLog> logs; |
| + BuildStep(this.name, this.description, this.result, this.start, this.end, |
|
Johnni Winther
2017/08/28 07:14:04
Add an empty line before the constructor.
|
| + this.number, this.isStarted, this.isFinished); |
| + |
| + @override |
| + String toString() { |
| + StringBuffer buffer = new StringBuffer(); |
| + buffer.writeln( |
| + "${result == '[0, []]' ? 'SUCCESS' : result}: $name - $description ($start, $end)"); |
|
Johnni Winther
2017/08/28 07:14:03
Long line.
|
| + logs.forEach((subLink) { |
| + buffer.writeln("\t${subLink}"); |
| + }); |
| + return buffer.toString(); |
| + } |
| +} |
| + |
| +class BuildLog { |
|
Johnni Winther
2017/08/28 07:14:03
Add document to the class and its fields.
|
| + String name; |
| + String url; |
| + BuildLog(this.name, this.url); |
| + |
| + @override |
| + String toString() { |
| + return "$name | $url"; |
| + } |
| +} |
| + |
| +class BuildProperty { |
|
Johnni Winther
2017/08/28 07:14:03
Add document to the class and its fields.
|
| + String name; |
| + String value; |
| + String source; |
| + BuildProperty(this.name, this.value, this.source); |
|
Johnni Winther
2017/08/28 07:14:04
Add an empty line before the constructor.
|
| + |
| + @override |
| + String toString() { |
| + return "$name\t$value\t$source"; |
| + } |
| +} |
| + |
| +class Timing { |
| + final DateTime start; |
| + final DateTime end; |
| + Timing(this.start, this.end); |
| + |
| + @override |
| + String toString() { |
| + return "start: $start\tend: $end"; |
| + } |
| +} |
| + |
| +class GitCommit { |
|
Johnni Winther
2017/08/28 07:14:03
Add document to the class and its fields.
|
| + final String revision; |
| + final String commitUrl; |
| + final String changedBy; |
| + final String comments; |
| + List<String> changedFiles; |
| + GitCommit(this.revision, this.commitUrl, this.changedBy, this.comments); |
|
Johnni Winther
2017/08/28 07:14:04
Add an empty line before the constructor.
|
| + |
| + @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(); |
| + } |
| +} |