OLD | NEW |
---|---|
(Empty) | |
1 import 'dart:io'; | |
Johnni Winther
2017/08/28 07:14:03
Add copyright header (for all .dart files):
// Co
| |
2 import 'dart:async'; | |
3 import 'dart:convert'; | |
4 import 'package:http/http.dart' as http; | |
5 import 'package:html/parser.dart' show parse; | |
6 import 'try.dart'; | |
7 import 'cache_new.dart'; | |
8 | |
9 const String LUCI_HOST = "luci-milo.appspot.com"; | |
10 // const String BUILD_CHROMIUM_HOST = "build.chromium.org"; | |
Johnni Winther
2017/08/28 07:14:03
Remove?
| |
11 | |
12 typedef void ModifyRequestFunction(HttpClientRequest request); | |
13 | |
14 /// Base class for communicating with Milo @ LogDog | |
15 /// Some information is found through the api | |
16 /// <https://docs.google.com/document/d/1HbPp7Sy7ofC | |
17 /// U7C9USqcE91VubGg_IIET2GUj9iknev4/edit#> | |
18 /// and some information is found via screen-scraping. | |
19 class LuciApi { | |
Johnni Winther
2017/08/28 07:14:04
Just call this [Luci].
| |
20 final HttpClient _client = new HttpClient(); | |
21 | |
22 LuciApi(); | |
23 | |
24 /// [getBuildBots] fetches all build bots from luci (we cannot | |
25 /// get this from the api). The format is: | |
26 /// <li> | |
27 /// <a href="/buildbot/client.crashpad/crashpad_win_x86_wow64_rel"> | |
28 /// crashpad_win_x86_wow64_rel</a> | |
29 /// </li> | |
30 /// <h3> client.dart </h3> | |
31 /// <li> | |
32 /// <a href="/buildbot/client.dart/analyze-linux-be">analyze-linux-be</a> | |
33 /// </li> | |
34 /// <li> | |
35 /// <a href="/buildbot/client.dart/analyze-linux-dev">analyze-linux-dev</a > | |
Johnni Winther
2017/08/28 07:14:04
Long line.
| |
36 /// </li> | |
37 /// <li> | |
38 /// <a href="/buildbot/client.dart/analyze-linux-stable"> | |
39 /// analyze-linux-stable</a> | |
40 /// </li> | |
41 /// <li> | |
42 /// <a href="/buildbot/client.dart/analyzer-linux-release-be"> | |
43 /// analyzer-linux-release-be</a> | |
44 /// </li> | |
45 /// | |
46 /// We look for the section header matching clients, then | |
47 /// if we are in the right section, we take the <li> element | |
48 /// and transform to a build bot | |
49 /// | |
50 Future<Try<List<LuciBuildBot>>> getAllBuildBots( | |
51 String client, WithCacheFunction withCache) async { | |
52 return await tryStartAsync(() => withCache( | |
53 () => _makeGetRequest( | |
54 new Uri(scheme: 'https', host: LUCI_HOST, path: "/")), | |
55 "all_buildbots")) | |
56 .then((Try<String> tryRes) => tryRes.bind(parse).bind((htmlDoc) { | |
57 var takeSection = | |
58 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
| |
59 // the document is not really suited for anything else | |
60 return htmlDoc.body.children.where((node) { | |
61 if (node.localName == "li") return takeSection; | |
62 if (node.localName != "h3") { | |
63 takeSection = false; | |
64 return false; | |
65 } | |
66 // current node is <h3> | |
67 takeSection = client == node.text.trim(); | |
68 return false; | |
69 }); | |
70 }).bind((elements) { | |
71 // here we hold an iterable of buildbot elements | |
72 // <li> | |
73 // <a href="/buildbot/client.dart/analyzer-linux-release-be"> | |
74 // analyzer-linux-release-be</a> | |
75 // </li> | |
76 return elements.map((element) { | |
77 var name = element.children[0].text; | |
78 var url = element.children[0].attributes['href']; | |
79 return new LuciBuildBot(client, name, url); | |
80 }).toList(); | |
81 })); | |
82 } | |
83 | |
84 /// [getPrimaryBuilders] fetches all primary builders | |
85 /// (the ones usually used by gardeners) by not including buildbots with | |
86 /// the name -dev, -stable or -integration. | |
87 Future<Try<List<LuciBuildBot>>> getPrimaryBuilders( | |
88 String client, WithCacheFunction withCache) async { | |
89 return await getAllBuildBots(client, withCache) | |
90 .then((Try<List<LuciBuildBot>> tryRes) { | |
91 return tryRes | |
92 .bind((buildBots) => buildBots.where((LuciBuildBot buildBot) { | |
93 return !(buildBot.name.contains("-dev") || | |
94 buildBot.name.contains("-stable") || | |
95 buildBot.name.contains("-integration")); | |
96 })); | |
97 }); | |
98 } | |
99 | |
100 /// Calling the Milo Api to get latest builds for this bot, | |
101 /// 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.
| |
102 Future<Try<List<BuildDetail>>> getBuildBotDetails( | |
103 String client, String botName, WithCacheFunction withCache, | |
104 [int amount = 20]) async { | |
105 var uri = new Uri( | |
106 scheme: "https", | |
107 host: LUCI_HOST, | |
108 path: "prpc/milo.Buildbot/GetBuildbotBuildsJSON"); | |
109 var body = { | |
110 "master": client, | |
111 "builder": botName, | |
112 "limit": amount, | |
113 "includeCurrent": true | |
114 }; | |
115 var result = await tryStartAsync(() => withCache( | |
116 () => _makePostRequest(uri, JSON.encode(body), { | |
117 HttpHeaders.CONTENT_TYPE: "application/json", | |
118 HttpHeaders.ACCEPT: "application/json" | |
119 }), | |
120 '${uri.path.toString()}_${botName}_$amount')); | |
Johnni Winther
2017/08/28 07:14:04
Just ${uri.path} since [path] is already a string.
| |
121 return result.bind(JSON.decode).bind((json) { | |
122 return json["builds"].map((b) { | |
123 var build = JSON.decode(UTF8.decode(BASE64.decode(b["data"]))); | |
124 return getBuildDetailFromJson(client, botName, build); | |
125 }).toList(); | |
126 }); | |
127 } | |
128 | |
129 /// Calling the Milo Api to get latest builds for this bot, | |
130 /// 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.
| |
131 Future<Try<BuildDetail>> getBuildBotBuildDetails(String client, | |
132 String botName, int buildNumber, WithCacheFunction withCache) async { | |
133 var uri = new Uri( | |
134 scheme: "https", | |
135 host: LUCI_HOST, | |
136 path: "prpc/milo.Buildbot/GetBuildbotBuildJSON"); | |
137 var body = {"master": client, "builder": botName, "buildNum": buildNumber}; | |
138 print(body); | |
139 var result = await tryStartAsync(() => withCache( | |
140 () => _makePostRequest(uri, JSON.encode(body), { | |
141 HttpHeaders.CONTENT_TYPE: "application/json", | |
142 HttpHeaders.ACCEPT: "application/json" | |
143 }), | |
144 '${uri.path.toString()}_${botName}_$buildNumber')); | |
145 return result.bind(JSON.decode).bind((json) { | |
146 var build = JSON.decode(UTF8.decode(BASE64.decode(json["data"]))); | |
147 return getBuildDetailFromJson(client, botName, build); | |
148 }); | |
149 } | |
150 | |
151 Future<String> _makeGetRequest(Uri uri) async { | |
152 var request = await _client.getUrl(uri); | |
153 var response = await request.close(); | |
154 if (response.statusCode != 200) { | |
155 response.drain(); | |
156 throw new HttpException(response.reasonPhrase, uri: uri); | |
157 } | |
158 return response.transform(UTF8.decoder).join(); | |
159 } | |
160 | |
161 Future<String> _makePostRequest( | |
162 Uri uri, Object body, Map<String, String> headers) async { | |
163 var response = await http.post(uri, body: body, headers: headers); | |
164 if (response.statusCode != 200) { | |
165 throw new HttpException(response.reasonPhrase, uri: uri); | |
166 } | |
167 // Prpc outputs a prefix to combat vulnerability | |
Johnni Winther
2017/08/28 07:14:04
Add period to end the sentence.
| |
168 if (response.body.startsWith(")]}'")) { | |
169 return response.body.substring(4); | |
170 } | |
171 return response.body; | |
172 } | |
173 | |
174 void close() { | |
175 _client.close(); | |
176 } | |
177 } | |
178 | |
179 BuildDetail getBuildDetailFromJson( | |
Johnni Winther
2017/08/28 07:14:03
Add documentation.
| |
180 String client, String botName, dynamic build) { | |
181 List<GitCommit> changes = build["sourceStamp"]["changes"].map((change) { | |
182 return new GitCommit(change["revision"], change["revLink"], change["who"], | |
183 change["comments"]) | |
184 ..changedFiles = change["files"].map((file) => file["name"]); | |
185 }).toList(); | |
186 | |
187 List<BuildProperty> properties = build["properties"].map((prop) { | |
188 return new BuildProperty(prop[0], prop[1].toString(), prop[2]); | |
189 }).toList(); | |
190 | |
191 List<BuildStep> steps = build["steps"].map((step) { | |
192 var start = | |
193 new DateTime.fromMillisecondsSinceEpoch(step["times"][0] * 1000); | |
194 DateTime end = null; | |
195 if (step["times"][1] != null) { | |
196 end = new DateTime.fromMillisecondsSinceEpoch(step["times"][1] * 1000); | |
197 } | |
198 return new BuildStep(step["name"], step["text"], step["results"].toString(), | |
199 start, end, step["step_number"], step["isStarted"], step["isFinished"]) | |
200 ..logs = step["logs"].map((log) => new BuildLog(log[0], log[1])); | |
201 }).toList(); | |
202 | |
203 DateTime end = null; | |
204 if (build["times"][1] != null) { | |
205 end = new DateTime.fromMillisecondsSinceEpoch(build["times"][1] * 1000); | |
206 } | |
207 | |
208 Timing timing = new Timing( | |
209 new DateTime.fromMillisecondsSinceEpoch(build["times"][0] * 1000), end); | |
210 | |
211 return new BuildDetail() | |
212 ..client = client | |
213 ..botName = botName | |
214 ..buildNumber = build["number"] | |
215 ..results = build["text"].join(' ') | |
216 ..finished = build["finished"] | |
217 ..blameList = build["blame"] | |
218 ..allChanges = changes | |
219 ..buildProperties = properties | |
220 ..steps = steps | |
221 ..timing = timing; | |
222 } | |
223 | |
224 // Structured classes to relay information from api and web pages | |
225 | |
226 /// [LuciBuildBot] holds information about a build bot | |
227 class LuciBuildBot { | |
228 String client; | |
229 String name; | |
230 String url; | |
Johnni Winther
2017/08/28 07:14:03
Make the fields final.
| |
231 | |
232 LuciBuildBot(this.client, this.name, this.url); | |
233 | |
234 @override | |
235 String toString() { | |
236 return "LuciBuildBot { client: $client, name: $name, url: $url }"; | |
237 } | |
238 } | |
239 | |
240 class BuildDetail { | |
Johnni Winther
2017/08/28 07:14:03
Add document to the class and all its fields.
| |
241 String client; | |
242 String botName; | |
243 int buildNumber; | |
244 String results; | |
245 bool finished; | |
246 List<BuildStep> steps; | |
247 List<BuildProperty> buildProperties; | |
248 List<String> blameList; | |
249 Timing timing; | |
250 List<GitCommit> allChanges; | |
Johnni Winther
2017/08/28 07:14:04
Add a constructor that takes these as argument and
| |
251 | |
252 @override | |
253 String toString() { | |
254 StringBuffer buffer = new StringBuffer(); | |
255 buffer.writeln("--------------------------------------"); | |
256 buffer.writeln(results); | |
257 buffer.writeln(timing); | |
258 buffer.writeln("----------------STEPS-----------------"); | |
259 if (steps != null) steps.forEach(buffer.writeln); | |
260 buffer.writeln("----------BUILD PROPERTIES------------"); | |
261 if (buildProperties != null) buildProperties.forEach(buffer.writeln); | |
262 buffer.writeln("-------------BLAME LIST---------------"); | |
263 if (blameList != null) blameList.forEach(buffer.writeln); | |
264 buffer.writeln("------------ALL CHANGES---------------"); | |
265 if (allChanges != null) allChanges.forEach(buffer.writeln); | |
266 return buffer.toString(); | |
267 } | |
268 } | |
269 | |
270 class BuildStep { | |
Johnni Winther
2017/08/28 07:14:03
Add document to the class and all its fields.
| |
271 final String name; | |
272 final String description; | |
273 final String result; | |
274 final DateTime start; | |
275 final DateTime end; | |
276 final int number; | |
277 final bool isStarted; | |
278 final bool isFinished; | |
279 List<BuildLog> logs; | |
280 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.
| |
281 this.number, this.isStarted, this.isFinished); | |
282 | |
283 @override | |
284 String toString() { | |
285 StringBuffer buffer = new StringBuffer(); | |
286 buffer.writeln( | |
287 "${result == '[0, []]' ? 'SUCCESS' : result}: $name - $description ($sta rt, $end)"); | |
Johnni Winther
2017/08/28 07:14:03
Long line.
| |
288 logs.forEach((subLink) { | |
289 buffer.writeln("\t${subLink}"); | |
290 }); | |
291 return buffer.toString(); | |
292 } | |
293 } | |
294 | |
295 class BuildLog { | |
Johnni Winther
2017/08/28 07:14:03
Add document to the class and its fields.
| |
296 String name; | |
297 String url; | |
298 BuildLog(this.name, this.url); | |
299 | |
300 @override | |
301 String toString() { | |
302 return "$name | $url"; | |
303 } | |
304 } | |
305 | |
306 class BuildProperty { | |
Johnni Winther
2017/08/28 07:14:03
Add document to the class and its fields.
| |
307 String name; | |
308 String value; | |
309 String source; | |
310 BuildProperty(this.name, this.value, this.source); | |
Johnni Winther
2017/08/28 07:14:04
Add an empty line before the constructor.
| |
311 | |
312 @override | |
313 String toString() { | |
314 return "$name\t$value\t$source"; | |
315 } | |
316 } | |
317 | |
318 class Timing { | |
319 final DateTime start; | |
320 final DateTime end; | |
321 Timing(this.start, this.end); | |
322 | |
323 @override | |
324 String toString() { | |
325 return "start: $start\tend: $end"; | |
326 } | |
327 } | |
328 | |
329 class GitCommit { | |
Johnni Winther
2017/08/28 07:14:03
Add document to the class and its fields.
| |
330 final String revision; | |
331 final String commitUrl; | |
332 final String changedBy; | |
333 final String comments; | |
334 List<String> changedFiles; | |
335 GitCommit(this.revision, this.commitUrl, this.changedBy, this.comments); | |
Johnni Winther
2017/08/28 07:14:04
Add an empty line before the constructor.
| |
336 | |
337 @override | |
338 String toString() { | |
339 StringBuffer buffer = new StringBuffer(); | |
340 buffer.writeln("revision: $revision"); | |
341 buffer.writeln("commitUrl: $commitUrl"); | |
342 buffer.writeln("changedBy: $changedBy"); | |
343 buffer.write("\n"); | |
344 buffer.writeln(comments); | |
345 buffer.write("\nfiles:\n"); | |
346 changedFiles.forEach(buffer.writeln); | |
347 return buffer.toString(); | |
348 } | |
349 } | |
OLD | NEW |