OLD | NEW |
---|---|
(Empty) | |
1 import 'dart:io'; | |
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(); | |
Bill Hesse
2017/08/23 15:41:25
Please contact the makers of the application to se
| |
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 |