Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 import 'dart:io'; | |
|
Johnni Winther
2017/08/28 07:14:03
Remove this file.
| |
| 2 import 'dart:async'; | |
| 3 import 'dart:convert'; | |
| 4 import 'package:base_lib/base_lib.dart'; | |
| 5 import 'package:html/parser.dart' show parse; | |
| 6 import 'package:html/dom.dart'; | |
| 7 | |
| 8 const String LUCI_HOST = "luci-milo.appspot.com"; | |
| 9 const String BUILD_CHROMIUM_HOST = "build.chromium.org"; | |
| 10 | |
| 11 /// Base class for communicating with Luci @ LogDog | |
| 12 /// Since some results can take a long time to get | |
| 13 /// and some results do not update that frequently, | |
| 14 /// [LuciApi] requires a cache to be used. If no | |
| 15 /// cache is needed, the [noCache] can be used. | |
| 16 class LuciApi { | |
| 17 final HttpClient _client = new HttpClient(); | |
| 18 | |
| 19 LuciApi(); | |
| 20 | |
| 21 /// [getBuildBots] fetches all build bots from luci. | |
| 22 /// The format is: | |
| 23 /// <li> | |
| 24 /// <a href="/buildbot/client.crashpad/crashpad_win_x86_wow64_rel">crashpa d_win_x86_wow64_rel</a> | |
| 25 /// </li> | |
| 26 /// <h3> client.dart </h3> | |
| 27 /// <li> | |
| 28 /// <a href="/buildbot/client.dart/analyze-linux-be">analyze-linux-be</a> | |
| 29 /// </li> | |
| 30 /// <li> | |
| 31 /// <a href="/buildbot/client.dart/analyze-linux-dev">analyze-linux-dev</a > | |
| 32 /// </li> | |
| 33 /// <li> | |
| 34 /// <a href="/buildbot/client.dart/analyze-linux-stable">analyze-linux-sta ble</a> | |
| 35 /// </li> | |
| 36 /// <li> | |
| 37 /// <a href="/buildbot/client.dart/analyzer-linux-release-be">analyzer-lin ux-release-be</a> | |
| 38 /// </li> | |
| 39 /// | |
| 40 /// We look for the section header matching clients, then | |
| 41 /// if we are in the right section, we take the <li> element | |
| 42 /// and transform to a build bot | |
| 43 /// | |
| 44 Future<Try<List<LuciBuildBot>>> getAllBuildBots( | |
| 45 String client, WithCache withCache) async { | |
| 46 var reqResult = await tryStartAsync(() => withCache( | |
| 47 () => _makeGetRequest( | |
| 48 new Uri(scheme: 'https', host: LUCI_HOST, path: "/")), | |
| 49 "all_buildbots")); | |
| 50 return reqResult.bind(parse).bind((htmlDoc) { | |
| 51 var takeSection = false; // this is really dirty, but the structure of | |
| 52 // the document is not really suited for anything else | |
| 53 return htmlDoc.body.children.where((node) { | |
| 54 if (node.localName == "li") return takeSection; | |
| 55 if (node.localName != "h3") { | |
| 56 takeSection = false; | |
| 57 return false; | |
| 58 } | |
| 59 // current node is <h3> | |
| 60 takeSection = client == node.text.trim(); | |
| 61 return false; | |
| 62 }); | |
| 63 }).bind((elements) { | |
| 64 // here we hold an iterable of buildbot elements | |
| 65 // <li> | |
| 66 // <a href="/buildbot/client.dart/analyzer-linux-release-be">analyzer- linux-release-be</a> | |
| 67 // </li> | |
| 68 return elements.map((element) { | |
| 69 var name = element.children[0].text; | |
| 70 var url = element.children[0].attributes['href']; | |
| 71 return new LuciBuildBot(client, name, url); | |
| 72 }).toList(); | |
| 73 }); | |
| 74 } | |
| 75 | |
| 76 /// [getPrimaryBuilders] fetches all primary builders (the ones usually used b y gardeners) | |
| 77 /// by just stopping when reaching the first -dev buildbot | |
| 78 Future<Try<List<LuciBuildBot>>> getPrimaryBuilders( | |
| 79 String client, WithCache withCache) async { | |
| 80 var reqResult = await tryStartAsync(() => withCache( | |
| 81 () => _makeGetRequest(new Uri( | |
| 82 scheme: 'https', | |
| 83 host: BUILD_CHROMIUM_HOST, | |
| 84 path: "/p/$client/builders")), | |
| 85 "primary_buildbots")); | |
| 86 return reqResult.bind(parse).bind((Document htmlDoc) { | |
| 87 // We take all <table><tbody><tr> | |
| 88 var mainTableRows = | |
| 89 htmlDoc.body.getElementsByTagName("table")[0].children[0].children; | |
| 90 // pick until we reach a dev builder bot | |
| 91 return mainTableRows.takeWhile((Element el) { | |
| 92 return !el.children[0].children[0].text.contains("-dev"); | |
| 93 }).map((Element el) { | |
| 94 return new LuciBuildBot(client, el.children[0].children[0].text, | |
| 95 el.children[0].children[0].attributes["href"]); | |
| 96 }); | |
| 97 }); | |
| 98 } | |
| 99 | |
| 100 /// Generates a suitable url to fetch information about a buildbot. | |
| 101 /// The field [amount] is the number of recent builds to fetch | |
| 102 Future<Try<LuciBuildBotDetail>> getBuildBotDetails( | |
| 103 String client, String botName, WithCache withCache, | |
| 104 [int amount = 25]) async { | |
| 105 var uri = new Uri( | |
| 106 scheme: "https", | |
| 107 host: LUCI_HOST, | |
| 108 path: "/buildbot/$client/$botName/", | |
| 109 query: "limit=$amount"); | |
| 110 var reqResult = await tryStartAsync( | |
| 111 () => withCache(() => _makeGetRequest(uri), uri.path.toString())); | |
| 112 return reqResult.bind(parse).bind((Document document) { | |
| 113 var detail = new LuciBuildBotDetail() | |
| 114 ..client = client | |
| 115 ..botName = botName | |
| 116 ..currentBuild = getCurrentBuild(document) | |
| 117 ..latestBuilds = getBuildOverviews(document); | |
| 118 return detail; | |
| 119 }); | |
| 120 } | |
| 121 | |
| 122 /// Get the [BuildDetail] information for the requested build. | |
| 123 /// The url requested is on the form /buildbot/$client/$botName/$buildNumber. | |
| 124 Future<Try<BuildDetail>> getBuildDetails(String client, String botName, | |
| 125 int buildNumber, WithCache withCache) async { | |
| 126 var uri = new Uri( | |
| 127 scheme: "https", | |
| 128 host: LUCI_HOST, | |
| 129 path: "/buildbot/$client/$botName/$buildNumber"); | |
| 130 var reqResult = await tryStartAsync( | |
| 131 () => withCache(() => _makeGetRequest(uri), uri.path.toString())); | |
| 132 return reqResult.bind(parse).bind((Document document) { | |
| 133 return new BuildDetail() | |
| 134 ..client = client | |
| 135 ..botName = botName | |
| 136 ..buildNumber = buildNumber | |
| 137 ..results = getBuildResults(document) | |
| 138 ..steps = getBuildSteps(document) | |
| 139 ..buildProperties = getBuildProperties(document) | |
| 140 ..blameList = getBuildBlameList(document) | |
| 141 ..timing = getBuildTiming(document) | |
| 142 ..allChanges = getBuildGitCommits(document); | |
| 143 }); | |
| 144 } | |
| 145 | |
| 146 Future<String> _makeGetRequest(Uri uri) async { | |
| 147 var request = await _client.getUrl(uri); | |
| 148 var response = await request.close(); | |
| 149 | |
| 150 if (response.statusCode != 200) { | |
| 151 response.drain(); | |
| 152 throw new HttpException(response.reasonPhrase, uri: uri); | |
| 153 } | |
| 154 | |
| 155 return response.transform(UTF8.decoder).join(); | |
| 156 } | |
| 157 | |
| 158 void close() { | |
| 159 _client.close(); | |
| 160 } | |
| 161 } | |
| 162 | |
| 163 // Functions to generate objects from the HTML representation | |
| 164 | |
| 165 CurrentBuild getCurrentBuild(Document document) { | |
| 166 var currentBuildColumn = document.body.getElementsByClassName("column")[1]; | |
| 167 // <h2>Current Builds (1):</h2> <------ we are here | |
| 168 // <ul> | |
| 169 // <li> | |
| 170 // <a href="1277">#1277</a> | |
| 171 if (currentBuildColumn.children.length > 1) { | |
| 172 var li = currentBuildColumn.children[1].children[0]; | |
| 173 return new CurrentBuild() | |
| 174 ..buildNumber = int.parse(li.children[0].attributes["href"]) | |
| 175 ..duration = li.text | |
| 176 .substring(li.text.indexOf("[") + 1, li.text.indexOf("]")) | |
| 177 .trim(); | |
| 178 } | |
| 179 return null; | |
| 180 } | |
| 181 | |
| 182 List<BuildOverview> getBuildOverviews(Document document) { | |
| 183 var main = document.body.getElementsByClassName("main").first; | |
| 184 var tables = main.getElementsByClassName("info"); | |
| 185 if (tables.length == 0) { | |
| 186 // happens when there is no builds at all | |
| 187 return new List<BuildOverview>(); | |
| 188 } | |
| 189 // We want to iterate over all table rows | |
| 190 // <table> <tbody> <tr> | |
| 191 return tables[0] | |
| 192 .children[0] | |
| 193 .children | |
| 194 .skip(1) | |
| 195 .map(getBuildOverviewFromTableRow) | |
| 196 .toList(); | |
| 197 } | |
| 198 | |
| 199 BuildOverview getBuildOverviewFromTableRow(Element tr) { | |
| 200 var bo = new BuildOverview() | |
| 201 ..time = tr.children[0].firstChild.text | |
| 202 ..mainRevision = tr.children[1].text | |
| 203 ..result = tr.children[2].text | |
| 204 ..buildNumber = int.parse(tr.children[3].text.substring(1)); | |
| 205 var changesText = tr.children[4].text; | |
| 206 bo.hasMultipleChanges = | |
| 207 changesText.contains(',') || changesText.contains("changes"); | |
| 208 bo.info = tr.children[5].innerHtml | |
| 209 .split("<br>") | |
| 210 .map((info) => info.trim()) | |
| 211 .toList(); | |
| 212 | |
| 213 return bo; | |
| 214 } | |
| 215 | |
| 216 String getBuildResults(Document document) { | |
| 217 // <html> <--- we are here | |
| 218 // ... | |
| 219 // <div class="column"> | |
| 220 // <h2>Results:</h2> | |
| 221 // <p class="success result">Build Successful | |
| 222 return document.getElementsByClassName("column")[0].children[1].text.trim(); | |
| 223 } | |
| 224 | |
| 225 List<Step> getBuildSteps(Document document) { | |
| 226 // <html> <--- we are here | |
| 227 // ... | |
| 228 // <ol id="steps" class="standard"> | |
| 229 // <li class="verbosity-Normal"> | |
| 230 return document | |
| 231 .getElementsByClassName("standard") | |
| 232 .first | |
| 233 .children | |
| 234 .map(getBuildStep) | |
| 235 .toList(); | |
| 236 } | |
| 237 | |
| 238 Step getBuildStep(Element li) { | |
| 239 // <li class="verbosity-Normal"> <--- we are here | |
| 240 // <div class="status-Success result"> | |
| 241 // <span class="duration" | |
| 242 // data-starttime="2017-08-21T17:01:41Z" | |
| 243 // data-endtime="2017-08-21T19:40:20Z"> | |
| 244 // ( 2 hrs 38 mins )</span> | |
| 245 // <b>steps</b> | |
| 246 // <span> | |
| 247 // <div class="step-text">running steps via annotated script</div> | |
| 248 // </span> | |
| 249 // </div> | |
| 250 // <ul> | |
| 251 // <li class="sublink"> | |
| 252 var divResult = li.children[0]; | |
| 253 return new Step( | |
| 254 divResult.children[1].text, | |
| 255 divResult.getElementsByClassName("step-text").first.text.trim(), | |
| 256 divResult.className.replaceAll("status-", "").replaceAll(" result", ""), | |
| 257 divResult.children.first.text.trim()) | |
| 258 ..subLinks = li | |
| 259 .getElementsByClassName("sublink") | |
| 260 .map((liSub) => new SubLink( | |
| 261 liSub.firstChild.text, liSub.firstChild.attributes["href"])) | |
| 262 .toList(); | |
| 263 } | |
| 264 | |
| 265 List<BuildProperty> getBuildProperties(Document document) { | |
| 266 // <html> <-- we are here | |
| 267 // ... | |
| 268 // <div class="column"> | |
| 269 // ... | |
| 270 // <div class="column"> | |
| 271 // <h2>Build Properties:</h2> | |
| 272 // <table class="info BuildProperties" width="100%"> | |
| 273 // <tbody> | |
| 274 // <tr> | |
| 275 // <th>Name</th> | |
| 276 // <th>Value</th> | |
| 277 // <th>Source</th> | |
| 278 // </tr> | |
| 279 var buildTable = document.getElementsByClassName("column")[1].children[1]; | |
| 280 return buildTable.children.first.children.skip(1).map((tr) { | |
| 281 // here we hold a <tr> with cells of information | |
| 282 return new BuildProperty(tr.children[0].text.trim(), | |
| 283 tr.children[1].text.trim(), tr.children[2].text.trim()); | |
| 284 }).toList(); | |
| 285 } | |
| 286 | |
| 287 List<String> getBuildBlameList(Document document) { | |
| 288 // <html> <-- we are here | |
| 289 // ... | |
| 290 // <div class="column"> | |
| 291 // ... | |
| 292 // <div class="column"> | |
| 293 // <h2>Build Properties:</h2> | |
| 294 // <table class="info BuildProperties" width="100%"> | |
| 295 // <h2>Blamelist:</h2> | |
| 296 // <ol> | |
| 297 // <li> | |
| 298 var blameList = document.getElementsByClassName("column")[1].children[3]; | |
| 299 return blameList.children.map((li) { | |
| 300 return li.text.replaceAll("ohnoyoudont", "").trim(); | |
| 301 }).toList(); | |
| 302 } | |
| 303 | |
| 304 Timing getBuildTiming(Document document) { | |
| 305 // <html> <-- we are here | |
| 306 // ... | |
| 307 // <div class="column"> | |
| 308 // ... | |
| 309 // <div class="column"> | |
| 310 // <h2>Build Properties:</h2> | |
| 311 // <table class="info BuildProperties" width="100%"> | |
| 312 // <h2>Blamelist:</h2> | |
| 313 // <ol> ... | |
| 314 // <h2>Timing:</h2> | |
| 315 // <table.. | |
| 316 var timingTableBody = | |
| 317 document.getElementsByClassName("column")[1].children[5].children.first; | |
| 318 var infos = timingTableBody.children | |
| 319 .map((tr) => tr.children.last.text.trim()) | |
| 320 .toList(); | |
| 321 return new Timing(infos[0], infos[1], infos[2]); | |
| 322 } | |
| 323 | |
| 324 List<GitCommit> getBuildGitCommits(Document document) { | |
| 325 // <html> <-- we are here | |
| 326 // ... | |
| 327 // <div class="column"> | |
| 328 // ... | |
| 329 // <div class="column"> | |
| 330 // ... | |
| 331 // <div class="column"> | |
| 332 // <h2>All Changes:</h2> | |
| 333 // <ol> | |
| 334 // <li> | |
| 335 var olChanges = document.getElementsByClassName("column")[2].children[1]; | |
| 336 return olChanges.children.map(getBuildGitCommit).toList(); | |
| 337 } | |
| 338 | |
| 339 GitCommit getBuildGitCommit(Element li) { | |
| 340 // <li> <--- we are here | |
| 341 // <h3>Deprecate MethodElement.getReifiedType</h3> | |
| 342 // <table class="info"> | |
| 343 // <tbody> | |
| 344 // <tr> | |
| 345 String title = li.children[0].text; | |
| 346 Element revisionAnchor = | |
| 347 li.children[1].children[0].children.last.children[1].firstChild; | |
| 348 String commitUrl = revisionAnchor.attributes["href"]; | |
| 349 String revision = revisionAnchor.text; | |
| 350 String changedBy = li.children[1].children[0].children.first.children[1].text | |
| 351 .replaceAll("ohnoyoudont", "") | |
| 352 .trim(); | |
| 353 String comments = li.getElementsByClassName("comments").first.text; | |
| 354 List<String> files = | |
| 355 li.getElementsByClassName("file").map((liFile) => liFile.text).toList(); | |
| 356 | |
| 357 return new GitCommit(title, revision, commitUrl, changedBy, comments) | |
| 358 ..changedFiles = files; | |
| 359 } | |
| 360 | |
| 361 // Structured classes to relay information scraped from the luci bot web pages | |
| 362 | |
| 363 /// [LuciBuildBot] holds information about a build bot | |
| 364 class LuciBuildBot { | |
| 365 String client; | |
| 366 String name; | |
| 367 String url; | |
| 368 | |
| 369 LuciBuildBot(this.client, this.name, this.url); | |
| 370 | |
| 371 @override | |
| 372 String toString() { | |
| 373 return "LuciBuildBot { client: $client, name: $name, url: $url }"; | |
| 374 } | |
| 375 } | |
| 376 | |
| 377 /// [LuciBuildBotDetail] holds information about a bots current build, | |
| 378 /// all latest builds (excluding the current) | |
| 379 class LuciBuildBotDetail { | |
| 380 String client; | |
| 381 String botName; | |
| 382 CurrentBuild currentBuild; | |
| 383 List<BuildOverview> latestBuilds; | |
| 384 | |
| 385 int latestBuildNumber() { | |
| 386 if (currentBuild == null && latestBuilds.length == 0) { | |
| 387 return -1; | |
| 388 } | |
| 389 return currentBuild != null | |
| 390 ? currentBuild.buildNumber | |
| 391 : latestBuilds[0].buildNumber; | |
| 392 } | |
| 393 | |
| 394 @override | |
| 395 String toString() { | |
| 396 StringBuffer buffer = new StringBuffer(); | |
| 397 buffer.writeln(currentBuild == null | |
| 398 ? "Current build: none\n" | |
| 399 : "Current build: ${currentBuild.buildNumber} (${currentBuild.duration}) \n"); | |
| 400 | |
| 401 buffer.writeln( | |
| 402 "time\t\t\t\t\trevision\t\t\t\t\tresult\tbuild #\tmult. rev.\tinfo"); | |
| 403 | |
| 404 latestBuilds.forEach((build) { | |
| 405 if (build.time.length > 31) { | |
| 406 buffer.writeln( | |
| 407 "${build.time}\t${build.mainRevision}\t${build.result}\t${build.buil dNumber}\t${build.hasMultipleChanges}\t\t${build.info.join(',')}"); | |
| 408 } else { | |
| 409 buffer.writeln( | |
| 410 "${build.time}\t\t${build.mainRevision}\t${build.result}\t${build.bu ildNumber}\t${build.hasMultipleChanges}\t\t${build.info.join(',')}"); | |
| 411 } | |
| 412 }); | |
| 413 | |
| 414 return buffer.toString(); | |
| 415 } | |
| 416 } | |
| 417 | |
| 418 /// [CurrentBuild] shows current build informaiton | |
| 419 class CurrentBuild { | |
| 420 int buildNumber; | |
| 421 String duration; | |
| 422 } | |
| 423 | |
| 424 /// [BuildOverview] has overview information about a build, such as time, mainRe vision and result | |
| 425 class BuildOverview { | |
| 426 String time; | |
| 427 String mainRevision; | |
| 428 String result; | |
| 429 int buildNumber; | |
| 430 bool hasMultipleChanges; | |
| 431 List<String> info; | |
| 432 } | |
| 433 | |
| 434 class BuildDetail { | |
| 435 String client; | |
| 436 String botName; | |
| 437 int buildNumber; | |
| 438 String results; | |
| 439 List<Step> steps; | |
| 440 List<BuildProperty> buildProperties; | |
| 441 List<String> blameList; | |
| 442 Timing timing; | |
| 443 List<GitCommit> allChanges; | |
| 444 | |
| 445 @override | |
| 446 String toString() { | |
| 447 StringBuffer buffer = new StringBuffer(); | |
| 448 buffer.writeln("--------------------------------------"); | |
| 449 buffer.writeln(results); | |
| 450 buffer.writeln(timing); | |
| 451 buffer.writeln("----------------STEPS-----------------"); | |
| 452 if (steps != null) steps.forEach(buffer.writeln); | |
| 453 buffer.writeln("----------BUILD PROPERTIES------------"); | |
| 454 if (buildProperties != null) buildProperties.forEach(buffer.writeln); | |
| 455 buffer.writeln("-------------BLAME LIST---------------"); | |
| 456 if (blameList != null) blameList.forEach(buffer.writeln); | |
| 457 buffer.writeln("------------ALL CHANGES---------------"); | |
| 458 if (allChanges != null) allChanges.forEach(buffer.writeln); | |
| 459 return buffer.toString(); | |
| 460 } | |
| 461 } | |
| 462 | |
| 463 class Step { | |
| 464 String name; | |
| 465 String description; | |
| 466 String result; | |
| 467 String time; | |
| 468 List<SubLink> subLinks; | |
| 469 Step(this.name, this.description, this.result, this.time); | |
| 470 | |
| 471 @override | |
| 472 String toString() { | |
| 473 StringBuffer buffer = new StringBuffer(); | |
| 474 buffer.writeln("$result: $name - $description ($time)"); | |
| 475 subLinks.forEach((subLink) { | |
| 476 buffer.writeln("\t${subLink}"); | |
| 477 }); | |
| 478 return buffer.toString(); | |
| 479 } | |
| 480 } | |
| 481 | |
| 482 class SubLink { | |
| 483 String name; | |
| 484 String url; | |
| 485 SubLink(this.name, this.url); | |
| 486 | |
| 487 @override | |
| 488 String toString() { | |
| 489 return "$name | $url"; | |
| 490 } | |
| 491 } | |
| 492 | |
| 493 class BuildProperty { | |
| 494 String name; | |
| 495 String value; | |
| 496 String source; | |
| 497 BuildProperty(this.name, this.value, this.source); | |
| 498 | |
| 499 @override | |
| 500 String toString() { | |
| 501 return "$name\t$value\t$source"; | |
| 502 } | |
| 503 } | |
| 504 | |
| 505 class Timing { | |
| 506 String start; | |
| 507 String end; | |
| 508 String elapsed; | |
| 509 Timing(this.start, this.end, this.elapsed); | |
| 510 | |
| 511 @override | |
| 512 String toString() { | |
| 513 return "start: $start\tend: $end\telapsed: $elapsed"; | |
| 514 } | |
| 515 } | |
| 516 | |
| 517 class GitCommit { | |
| 518 String title; | |
| 519 String revision; | |
| 520 String commitUrl; | |
| 521 String changedBy; | |
| 522 String comments; | |
| 523 List<String> changedFiles; | |
| 524 GitCommit( | |
| 525 this.title, this.revision, this.commitUrl, this.changedBy, this.comments); | |
| 526 | |
| 527 @override | |
| 528 String toString() { | |
| 529 StringBuffer buffer = new StringBuffer(); | |
| 530 | |
| 531 buffer.writeln(title); | |
| 532 buffer.writeln("revision: $revision"); | |
| 533 buffer.writeln("commitUrl: $commitUrl"); | |
| 534 buffer.writeln("changedBy: $changedBy"); | |
| 535 buffer.write("\n"); | |
| 536 buffer.writeln(comments); | |
| 537 buffer.write("\nfiles:\n"); | |
| 538 changedFiles.forEach(buffer.writeln); | |
| 539 return buffer.toString(); | |
| 540 } | |
| 541 } | |
| OLD | NEW |