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..68a4b78f5da259dd95e08df584423fadb65d20e6 |
| --- /dev/null |
| +++ b/tools/gardening/lib/src/_luci_api.dart |
| @@ -0,0 +1,541 @@ |
| +import 'dart:io'; |
|
Johnni Winther
2017/08/28 07:14:03
Remove this file.
|
| +import 'dart:async'; |
| +import 'dart:convert'; |
| +import 'package:base_lib/base_lib.dart'; |
| +import 'package:html/parser.dart' show parse; |
| +import 'package:html/dom.dart'; |
| + |
| +const String LUCI_HOST = "luci-milo.appspot.com"; |
| +const String BUILD_CHROMIUM_HOST = "build.chromium.org"; |
| + |
| +/// Base class for communicating with Luci @ LogDog |
| +/// Since some results can take a long time to get |
| +/// and some results do not update that frequently, |
| +/// [LuciApi] requires a cache to be used. If no |
| +/// cache is needed, the [noCache] can be used. |
| +class LuciApi { |
| + final HttpClient _client = new HttpClient(); |
| + |
| + LuciApi(); |
| + |
| + /// [getBuildBots] fetches all build bots from luci. |
| + /// 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> |
| + /// </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, WithCache withCache) async { |
| + var reqResult = await tryStartAsync(() => withCache( |
| + () => _makeGetRequest( |
| + new Uri(scheme: 'https', host: LUCI_HOST, path: "/")), |
| + "all_buildbots")); |
| + return reqResult.bind(parse).bind((htmlDoc) { |
| + var takeSection = false; // this is really dirty, but the structure of |
| + // 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 just stopping when reaching the first -dev buildbot |
| + Future<Try<List<LuciBuildBot>>> getPrimaryBuilders( |
| + String client, WithCache withCache) async { |
| + var reqResult = await tryStartAsync(() => withCache( |
| + () => _makeGetRequest(new Uri( |
| + scheme: 'https', |
| + host: BUILD_CHROMIUM_HOST, |
| + path: "/p/$client/builders")), |
| + "primary_buildbots")); |
| + return reqResult.bind(parse).bind((Document htmlDoc) { |
| + // We take all <table><tbody><tr> |
| + var mainTableRows = |
| + htmlDoc.body.getElementsByTagName("table")[0].children[0].children; |
| + // pick until we reach a dev builder bot |
| + return mainTableRows.takeWhile((Element el) { |
| + return !el.children[0].children[0].text.contains("-dev"); |
| + }).map((Element el) { |
| + return new LuciBuildBot(client, el.children[0].children[0].text, |
| + el.children[0].children[0].attributes["href"]); |
| + }); |
| + }); |
| + } |
| + |
| + /// Generates a suitable url to fetch information about a buildbot. |
| + /// The field [amount] is the number of recent builds to fetch |
| + Future<Try<LuciBuildBotDetail>> getBuildBotDetails( |
| + String client, String botName, WithCache withCache, |
| + [int amount = 25]) async { |
| + var uri = new Uri( |
| + scheme: "https", |
| + host: LUCI_HOST, |
| + path: "/buildbot/$client/$botName/", |
| + query: "limit=$amount"); |
| + var reqResult = await tryStartAsync( |
| + () => withCache(() => _makeGetRequest(uri), uri.path.toString())); |
| + return reqResult.bind(parse).bind((Document document) { |
| + var detail = new LuciBuildBotDetail() |
| + ..client = client |
| + ..botName = botName |
| + ..currentBuild = getCurrentBuild(document) |
| + ..latestBuilds = getBuildOverviews(document); |
| + return detail; |
| + }); |
| + } |
| + |
| + /// Get the [BuildDetail] information for the requested build. |
| + /// The url requested is on the form /buildbot/$client/$botName/$buildNumber. |
| + Future<Try<BuildDetail>> getBuildDetails(String client, String botName, |
| + int buildNumber, WithCache withCache) async { |
| + var uri = new Uri( |
| + scheme: "https", |
| + host: LUCI_HOST, |
| + path: "/buildbot/$client/$botName/$buildNumber"); |
| + var reqResult = await tryStartAsync( |
| + () => withCache(() => _makeGetRequest(uri), uri.path.toString())); |
| + return reqResult.bind(parse).bind((Document document) { |
| + return new BuildDetail() |
| + ..client = client |
| + ..botName = botName |
| + ..buildNumber = buildNumber |
| + ..results = getBuildResults(document) |
| + ..steps = getBuildSteps(document) |
| + ..buildProperties = getBuildProperties(document) |
| + ..blameList = getBuildBlameList(document) |
| + ..timing = getBuildTiming(document) |
| + ..allChanges = getBuildGitCommits(document); |
| + }); |
| + } |
| + |
| + 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(); |
| + } |
| + |
| + void close() { |
| + _client.close(); |
| + } |
| +} |
| + |
| +// Functions to generate objects from the HTML representation |
| + |
| +CurrentBuild getCurrentBuild(Document document) { |
| + var currentBuildColumn = document.body.getElementsByClassName("column")[1]; |
| + // <h2>Current Builds (1):</h2> <------ we are here |
| + // <ul> |
| + // <li> |
| + // <a href="1277">#1277</a> |
| + if (currentBuildColumn.children.length > 1) { |
| + var li = currentBuildColumn.children[1].children[0]; |
| + return new CurrentBuild() |
| + ..buildNumber = int.parse(li.children[0].attributes["href"]) |
| + ..duration = li.text |
| + .substring(li.text.indexOf("[") + 1, li.text.indexOf("]")) |
| + .trim(); |
| + } |
| + return null; |
| +} |
| + |
| +List<BuildOverview> getBuildOverviews(Document document) { |
| + var main = document.body.getElementsByClassName("main").first; |
| + var tables = main.getElementsByClassName("info"); |
| + if (tables.length == 0) { |
| + // happens when there is no builds at all |
| + return new List<BuildOverview>(); |
| + } |
| + // We want to iterate over all table rows |
| + // <table> <tbody> <tr> |
| + return tables[0] |
| + .children[0] |
| + .children |
| + .skip(1) |
| + .map(getBuildOverviewFromTableRow) |
| + .toList(); |
| +} |
| + |
| +BuildOverview getBuildOverviewFromTableRow(Element tr) { |
| + var bo = new BuildOverview() |
| + ..time = tr.children[0].firstChild.text |
| + ..mainRevision = tr.children[1].text |
| + ..result = tr.children[2].text |
| + ..buildNumber = int.parse(tr.children[3].text.substring(1)); |
| + var changesText = tr.children[4].text; |
| + bo.hasMultipleChanges = |
| + changesText.contains(',') || changesText.contains("changes"); |
| + bo.info = tr.children[5].innerHtml |
| + .split("<br>") |
| + .map((info) => info.trim()) |
| + .toList(); |
| + |
| + return bo; |
| +} |
| + |
| +String getBuildResults(Document document) { |
| + // <html> <--- we are here |
| + // ... |
| + // <div class="column"> |
| + // <h2>Results:</h2> |
| + // <p class="success result">Build Successful |
| + return document.getElementsByClassName("column")[0].children[1].text.trim(); |
| +} |
| + |
| +List<Step> getBuildSteps(Document document) { |
| + // <html> <--- we are here |
| + // ... |
| + // <ol id="steps" class="standard"> |
| + // <li class="verbosity-Normal"> |
| + return document |
| + .getElementsByClassName("standard") |
| + .first |
| + .children |
| + .map(getBuildStep) |
| + .toList(); |
| +} |
| + |
| +Step getBuildStep(Element li) { |
| + // <li class="verbosity-Normal"> <--- we are here |
| + // <div class="status-Success result"> |
| + // <span class="duration" |
| + // data-starttime="2017-08-21T17:01:41Z" |
| + // data-endtime="2017-08-21T19:40:20Z"> |
| + // ( 2 hrs 38 mins )</span> |
| + // <b>steps</b> |
| + // <span> |
| + // <div class="step-text">running steps via annotated script</div> |
| + // </span> |
| + // </div> |
| + // <ul> |
| + // <li class="sublink"> |
| + var divResult = li.children[0]; |
| + return new Step( |
| + divResult.children[1].text, |
| + divResult.getElementsByClassName("step-text").first.text.trim(), |
| + divResult.className.replaceAll("status-", "").replaceAll(" result", ""), |
| + divResult.children.first.text.trim()) |
| + ..subLinks = li |
| + .getElementsByClassName("sublink") |
| + .map((liSub) => new SubLink( |
| + liSub.firstChild.text, liSub.firstChild.attributes["href"])) |
| + .toList(); |
| +} |
| + |
| +List<BuildProperty> getBuildProperties(Document document) { |
| + // <html> <-- we are here |
| + // ... |
| + // <div class="column"> |
| + // ... |
| + // <div class="column"> |
| + // <h2>Build Properties:</h2> |
| + // <table class="info BuildProperties" width="100%"> |
| + // <tbody> |
| + // <tr> |
| + // <th>Name</th> |
| + // <th>Value</th> |
| + // <th>Source</th> |
| + // </tr> |
| + var buildTable = document.getElementsByClassName("column")[1].children[1]; |
| + return buildTable.children.first.children.skip(1).map((tr) { |
| + // here we hold a <tr> with cells of information |
| + return new BuildProperty(tr.children[0].text.trim(), |
| + tr.children[1].text.trim(), tr.children[2].text.trim()); |
| + }).toList(); |
| +} |
| + |
| +List<String> getBuildBlameList(Document document) { |
| + // <html> <-- we are here |
| + // ... |
| + // <div class="column"> |
| + // ... |
| + // <div class="column"> |
| + // <h2>Build Properties:</h2> |
| + // <table class="info BuildProperties" width="100%"> |
| + // <h2>Blamelist:</h2> |
| + // <ol> |
| + // <li> |
| + var blameList = document.getElementsByClassName("column")[1].children[3]; |
| + return blameList.children.map((li) { |
| + return li.text.replaceAll("ohnoyoudont", "").trim(); |
| + }).toList(); |
| +} |
| + |
| +Timing getBuildTiming(Document document) { |
| + // <html> <-- we are here |
| + // ... |
| + // <div class="column"> |
| + // ... |
| + // <div class="column"> |
| + // <h2>Build Properties:</h2> |
| + // <table class="info BuildProperties" width="100%"> |
| + // <h2>Blamelist:</h2> |
| + // <ol> ... |
| + // <h2>Timing:</h2> |
| + // <table.. |
| + var timingTableBody = |
| + document.getElementsByClassName("column")[1].children[5].children.first; |
| + var infos = timingTableBody.children |
| + .map((tr) => tr.children.last.text.trim()) |
| + .toList(); |
| + return new Timing(infos[0], infos[1], infos[2]); |
| +} |
| + |
| +List<GitCommit> getBuildGitCommits(Document document) { |
| + // <html> <-- we are here |
| + // ... |
| + // <div class="column"> |
| + // ... |
| + // <div class="column"> |
| + // ... |
| + // <div class="column"> |
| + // <h2>All Changes:</h2> |
| + // <ol> |
| + // <li> |
| + var olChanges = document.getElementsByClassName("column")[2].children[1]; |
| + return olChanges.children.map(getBuildGitCommit).toList(); |
| +} |
| + |
| +GitCommit getBuildGitCommit(Element li) { |
| + // <li> <--- we are here |
| + // <h3>Deprecate MethodElement.getReifiedType</h3> |
| + // <table class="info"> |
| + // <tbody> |
| + // <tr> |
| + String title = li.children[0].text; |
| + Element revisionAnchor = |
| + li.children[1].children[0].children.last.children[1].firstChild; |
| + String commitUrl = revisionAnchor.attributes["href"]; |
| + String revision = revisionAnchor.text; |
| + String changedBy = li.children[1].children[0].children.first.children[1].text |
| + .replaceAll("ohnoyoudont", "") |
| + .trim(); |
| + String comments = li.getElementsByClassName("comments").first.text; |
| + List<String> files = |
| + li.getElementsByClassName("file").map((liFile) => liFile.text).toList(); |
| + |
| + return new GitCommit(title, revision, commitUrl, changedBy, comments) |
| + ..changedFiles = files; |
| +} |
| + |
| +// Structured classes to relay information scraped from the luci bot web pages |
| + |
| +/// [LuciBuildBot] holds information about a build bot |
| +class LuciBuildBot { |
| + String client; |
| + String name; |
| + String url; |
| + |
| + LuciBuildBot(this.client, this.name, this.url); |
| + |
| + @override |
| + String toString() { |
| + return "LuciBuildBot { client: $client, name: $name, url: $url }"; |
| + } |
| +} |
| + |
| +/// [LuciBuildBotDetail] holds information about a bots current build, |
| +/// all latest builds (excluding the current) |
| +class LuciBuildBotDetail { |
| + String client; |
| + String botName; |
| + CurrentBuild currentBuild; |
| + List<BuildOverview> latestBuilds; |
| + |
| + int latestBuildNumber() { |
| + if (currentBuild == null && latestBuilds.length == 0) { |
| + return -1; |
| + } |
| + return currentBuild != null |
| + ? currentBuild.buildNumber |
| + : latestBuilds[0].buildNumber; |
| + } |
| + |
| + @override |
| + String toString() { |
| + StringBuffer buffer = new StringBuffer(); |
| + buffer.writeln(currentBuild == null |
| + ? "Current build: none\n" |
| + : "Current build: ${currentBuild.buildNumber} (${currentBuild.duration})\n"); |
| + |
| + buffer.writeln( |
| + "time\t\t\t\t\trevision\t\t\t\t\tresult\tbuild #\tmult. rev.\tinfo"); |
| + |
| + latestBuilds.forEach((build) { |
| + if (build.time.length > 31) { |
| + buffer.writeln( |
| + "${build.time}\t${build.mainRevision}\t${build.result}\t${build.buildNumber}\t${build.hasMultipleChanges}\t\t${build.info.join(',')}"); |
| + } else { |
| + buffer.writeln( |
| + "${build.time}\t\t${build.mainRevision}\t${build.result}\t${build.buildNumber}\t${build.hasMultipleChanges}\t\t${build.info.join(',')}"); |
| + } |
| + }); |
| + |
| + return buffer.toString(); |
| + } |
| +} |
| + |
| +/// [CurrentBuild] shows current build informaiton |
| +class CurrentBuild { |
| + int buildNumber; |
| + String duration; |
| +} |
| + |
| +/// [BuildOverview] has overview information about a build, such as time, mainRevision and result |
| +class BuildOverview { |
| + String time; |
| + String mainRevision; |
| + String result; |
| + int buildNumber; |
| + bool hasMultipleChanges; |
| + List<String> info; |
| +} |
| + |
| +class BuildDetail { |
| + String client; |
| + String botName; |
| + int buildNumber; |
| + String results; |
| + List<Step> steps; |
| + List<BuildProperty> buildProperties; |
| + List<String> blameList; |
| + Timing timing; |
| + List<GitCommit> 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(); |
| + } |
| +} |
| + |
| +class Step { |
| + String name; |
| + String description; |
| + String result; |
| + String time; |
| + List<SubLink> subLinks; |
| + Step(this.name, this.description, this.result, this.time); |
| + |
| + @override |
| + String toString() { |
| + StringBuffer buffer = new StringBuffer(); |
| + buffer.writeln("$result: $name - $description ($time)"); |
| + subLinks.forEach((subLink) { |
| + buffer.writeln("\t${subLink}"); |
| + }); |
| + return buffer.toString(); |
| + } |
| +} |
| + |
| +class SubLink { |
| + String name; |
| + String url; |
| + SubLink(this.name, this.url); |
| + |
| + @override |
| + String toString() { |
| + return "$name | $url"; |
| + } |
| +} |
| + |
| +class BuildProperty { |
| + String name; |
| + String value; |
| + String source; |
| + BuildProperty(this.name, this.value, this.source); |
| + |
| + @override |
| + String toString() { |
| + return "$name\t$value\t$source"; |
| + } |
| +} |
| + |
| +class Timing { |
| + String start; |
| + String end; |
| + String elapsed; |
| + Timing(this.start, this.end, this.elapsed); |
| + |
| + @override |
| + String toString() { |
| + return "start: $start\tend: $end\telapsed: $elapsed"; |
| + } |
| +} |
| + |
| +class GitCommit { |
| + String title; |
| + String revision; |
| + String commitUrl; |
| + String changedBy; |
| + String comments; |
| + List<String> changedFiles; |
| + GitCommit( |
| + this.title, this.revision, this.commitUrl, this.changedBy, this.comments); |
| + |
| + @override |
| + String toString() { |
| + StringBuffer buffer = new StringBuffer(); |
| + |
| + buffer.writeln(title); |
| + 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(); |
| + } |
| +} |