Merge lp:~canonical-platform-qa/qakit/add-vis into lp:qakit
- add-vis
- Merge into trunk
Proposed by
Allan LeSage
Status: | Merged |
---|---|
Approved by: | Allan LeSage |
Approved revision: | 117 |
Merged at revision: | 127 |
Proposed branch: | lp:~canonical-platform-qa/qakit/add-vis |
Merge into: | lp:qakit |
Diff against target: |
1346 lines (+1273/-0) 14 files modified
qakit/vis/dashboard/main.js (+55/-0) qakit/vis/dashboard/ust_rc-proposed_arale_regression.html (+48/-0) qakit/vis/dashboard/ust_rc-proposed_arale_sanity.html (+48/-0) qakit/vis/dashboard/ust_rc-proposed_krillin_regression.html (+48/-0) qakit/vis/dashboard/ust_rc-proposed_krillin_sanity.html (+48/-0) qakit/vis/dashboard/ust_rc_arale_regression.html (+48/-0) qakit/vis/dashboard/ust_rc_arale_sanity.html (+48/-0) qakit/vis/dashboard/ust_rc_krillin_regression.html (+48/-0) qakit/vis/dashboard/ust_rc_krillin_sanity.html (+48/-0) qakit/vis/jenkins_results.py (+70/-0) qakit/vis/retrieve_jenkins_results.py (+229/-0) qakit/vis/tests/test_retrieve_jenkins_results.py (+47/-0) qakit/vis/tests/test_visualize.py (+194/-0) qakit/vis/visualize.py (+294/-0) |
To merge this branch: | bzr merge lp:~canonical-platform-qa/qakit/add-vis |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Canonical Platform QA Team | Pending | ||
Review via email: mp+299606@code.launchpad.net |
Commit message
Add a vis tool to visualize test suite performance.
Description of the change
Add a 'vis' tool for visualizing test suite status.
Presently we're using a few html files to present this datatables/
To post a comment you must log in.
Revision history for this message
Sergio Cazzolato (sergio-j-cazzolato) wrote : | # |
- 113. By Allan LeSage
-
Move dashboard-type files to a dashboard dir.
- 114. By Allan LeSage
-
Context manager for json writer.
- 115. By Allan LeSage
-
Use statistics to compute mean.
- 116. By Allan LeSage
-
Move to dao, fix tests.
- 117. By Allan LeSage
-
Typo!
Revision history for this message
Allan LeSage (allanlesage) wrote : | # |
Made all the requested changes, thanks cachio for a further review!
Revision history for this message
Sergio Cazzolato (sergio-j-cazzolato) wrote : | # |
Organized now! go ahead with this change
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'qakit/vis' | |||
2 | === added directory 'qakit/vis/dashboard' | |||
3 | === added file 'qakit/vis/dashboard/main.js' | |||
4 | --- qakit/vis/dashboard/main.js 1970-01-01 00:00:00 +0000 | |||
5 | +++ qakit/vis/dashboard/main.js 2016-07-13 18:36:50 +0000 | |||
6 | @@ -0,0 +1,55 @@ | |||
7 | 1 | |||
8 | 2 | function createdCellFtn(cell, cellData, rowData, rowIndex, colIndex) { | ||
9 | 3 | if (cellData.skipped == true) { | ||
10 | 4 | // skipped tests get dashed borders | ||
11 | 5 | $(cell).css('border-style', 'dashed'); | ||
12 | 6 | $(cell).css('border-width', 'medium'); | ||
13 | 7 | } else { | ||
14 | 8 | // tests get colors | ||
15 | 9 | $(cell).css('background-color', cellData.color); | ||
16 | 10 | }; | ||
17 | 11 | if (colIndex > 2) { | ||
18 | 12 | $(cell).html("<a href=" + cellData.link + "> </a>"); | ||
19 | 13 | } else if (colIndex == 2) { | ||
20 | 14 | // no links in the trend column pls | ||
21 | 15 | $(cell).html(""); | ||
22 | 16 | }; | ||
23 | 17 | } | ||
24 | 18 | |||
25 | 19 | function visTable(data, selector) { | ||
26 | 20 | var tableData = data.data.map(function(obj) { | ||
27 | 21 | var row = [ obj.section, obj.test_name ]; | ||
28 | 22 | row.push(obj.trend); | ||
29 | 23 | obj.builds.forEach(function(build) { | ||
30 | 24 | row.push(build); | ||
31 | 25 | }); | ||
32 | 26 | return row; | ||
33 | 27 | }); | ||
34 | 28 | var tableColumns = data.header.map(function(obj) { | ||
35 | 29 | return { | ||
36 | 30 | title: obj, | ||
37 | 31 | createdCell: createdCellFtn, | ||
38 | 32 | orderable: false, | ||
39 | 33 | searchable: false, | ||
40 | 34 | // hide Section column | ||
41 | 35 | visible: (obj == 'Section' ? false : true), | ||
42 | 36 | }; | ||
43 | 37 | }); | ||
44 | 38 | $(selector).dataTable({ | ||
45 | 39 | data: tableData, | ||
46 | 40 | columns: tableColumns, | ||
47 | 41 | searching: false, | ||
48 | 42 | order: [[0, 'dsc']], | ||
49 | 43 | bDestroy: true, | ||
50 | 44 | lengthChange: false, | ||
51 | 45 | lengthMenu: [-1], | ||
52 | 46 | }); | ||
53 | 47 | } | ||
54 | 48 | |||
55 | 49 | function draw(filename) { | ||
56 | 50 | $.getJSON(filename, function(data) { | ||
57 | 51 | visTable(data, "#visTable"); | ||
58 | 52 | }).fail(function(data, status) { | ||
59 | 53 | console.log("Faled to load vis JSON."); | ||
60 | 54 | }); | ||
61 | 55 | } | ||
62 | 0 | 56 | ||
63 | === added file 'qakit/vis/dashboard/ust_rc-proposed_arale_regression.html' | |||
64 | --- qakit/vis/dashboard/ust_rc-proposed_arale_regression.html 1970-01-01 00:00:00 +0000 | |||
65 | +++ qakit/vis/dashboard/ust_rc-proposed_arale_regression.html 2016-07-13 18:36:50 +0000 | |||
66 | @@ -0,0 +1,48 @@ | |||
67 | 1 | <!DOCTYPE html> | ||
68 | 2 | <html> | ||
69 | 3 | <head> | ||
70 | 4 | <meta charset="utf-8"> | ||
71 | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
72 | 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
73 | 7 | |||
74 | 8 | <title>Platform QA - VIS</title> | ||
75 | 9 | |||
76 | 10 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"> | ||
77 | 11 | <link href="http://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet" type="text/css"> | ||
78 | 12 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> | ||
79 | 13 | <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.css"/> | ||
80 | 14 | <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> | ||
81 | 15 | <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js"></script> | ||
82 | 16 | <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> | ||
83 | 17 | <script type="text/javascript" src="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.js"></script> | ||
84 | 18 | <script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script> | ||
85 | 19 | <script src="main.js"></script> | ||
86 | 20 | <style> | ||
87 | 21 | .ar { | ||
88 | 22 | text-align: right; | ||
89 | 23 | } | ||
90 | 24 | .vertical{ | ||
91 | 25 | writing-mode:tb-rl; | ||
92 | 26 | white-space:nowrap; | ||
93 | 27 | display:block; | ||
94 | 28 | bottom:0; | ||
95 | 29 | height:20px; | ||
96 | 30 | } | ||
97 | 31 | td a { | ||
98 | 32 | display:block; | ||
99 | 33 | width:100%; | ||
100 | 34 | text-decoration:none; | ||
101 | 35 | } | ||
102 | 36 | </style> | ||
103 | 37 | </head> | ||
104 | 38 | <body> | ||
105 | 39 | <div class="col-md-12"> | ||
106 | 40 | <table id="visTable" class="table table-striped table-bordered"></table> | ||
107 | 41 | </div> | ||
108 | 42 | <script> | ||
109 | 43 | window.onload = function() { | ||
110 | 44 | draw("ust_rc-proposed_arale_regression.json"); | ||
111 | 45 | } | ||
112 | 46 | </script> | ||
113 | 47 | </body> | ||
114 | 48 | </html> | ||
115 | 0 | 49 | ||
116 | === added file 'qakit/vis/dashboard/ust_rc-proposed_arale_sanity.html' | |||
117 | --- qakit/vis/dashboard/ust_rc-proposed_arale_sanity.html 1970-01-01 00:00:00 +0000 | |||
118 | +++ qakit/vis/dashboard/ust_rc-proposed_arale_sanity.html 2016-07-13 18:36:50 +0000 | |||
119 | @@ -0,0 +1,48 @@ | |||
120 | 1 | <!DOCTYPE html> | ||
121 | 2 | <html> | ||
122 | 3 | <head> | ||
123 | 4 | <meta charset="utf-8"> | ||
124 | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
125 | 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
126 | 7 | |||
127 | 8 | <title>Platform QA - VIS</title> | ||
128 | 9 | |||
129 | 10 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"> | ||
130 | 11 | <link href="http://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet" type="text/css"> | ||
131 | 12 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> | ||
132 | 13 | <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.css"/> | ||
133 | 14 | <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> | ||
134 | 15 | <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js"></script> | ||
135 | 16 | <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> | ||
136 | 17 | <script type="text/javascript" src="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.js"></script> | ||
137 | 18 | <script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script> | ||
138 | 19 | <script src="main.js"></script> | ||
139 | 20 | <style> | ||
140 | 21 | .ar { | ||
141 | 22 | text-align: right; | ||
142 | 23 | } | ||
143 | 24 | .vertical{ | ||
144 | 25 | writing-mode:tb-rl; | ||
145 | 26 | white-space:nowrap; | ||
146 | 27 | display:block; | ||
147 | 28 | bottom:0; | ||
148 | 29 | height:20px; | ||
149 | 30 | } | ||
150 | 31 | td a { | ||
151 | 32 | display:block; | ||
152 | 33 | width:100%; | ||
153 | 34 | text-decoration:none; | ||
154 | 35 | } | ||
155 | 36 | </style> | ||
156 | 37 | </head> | ||
157 | 38 | <body> | ||
158 | 39 | <div class="col-md-12"> | ||
159 | 40 | <table id="visTable" class="table table-striped table-bordered"></table> | ||
160 | 41 | </div> | ||
161 | 42 | <script> | ||
162 | 43 | window.onload = function() { | ||
163 | 44 | draw("ust_rc-proposed_arale_sanity.json"); | ||
164 | 45 | } | ||
165 | 46 | </script> | ||
166 | 47 | </body> | ||
167 | 48 | </html> | ||
168 | 0 | 49 | ||
169 | === added file 'qakit/vis/dashboard/ust_rc-proposed_krillin_regression.html' | |||
170 | --- qakit/vis/dashboard/ust_rc-proposed_krillin_regression.html 1970-01-01 00:00:00 +0000 | |||
171 | +++ qakit/vis/dashboard/ust_rc-proposed_krillin_regression.html 2016-07-13 18:36:50 +0000 | |||
172 | @@ -0,0 +1,48 @@ | |||
173 | 1 | <!DOCTYPE html> | ||
174 | 2 | <html> | ||
175 | 3 | <head> | ||
176 | 4 | <meta charset="utf-8"> | ||
177 | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
178 | 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
179 | 7 | |||
180 | 8 | <title>Platform QA - VIS</title> | ||
181 | 9 | |||
182 | 10 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"> | ||
183 | 11 | <link href="http://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet" type="text/css"> | ||
184 | 12 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> | ||
185 | 13 | <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.css"/> | ||
186 | 14 | <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> | ||
187 | 15 | <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js"></script> | ||
188 | 16 | <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> | ||
189 | 17 | <script type="text/javascript" src="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.js"></script> | ||
190 | 18 | <script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script> | ||
191 | 19 | <script src="main.js"></script> | ||
192 | 20 | <style> | ||
193 | 21 | .ar { | ||
194 | 22 | text-align: right; | ||
195 | 23 | } | ||
196 | 24 | .vertical{ | ||
197 | 25 | writing-mode:tb-rl; | ||
198 | 26 | white-space:nowrap; | ||
199 | 27 | display:block; | ||
200 | 28 | bottom:0; | ||
201 | 29 | height:20px; | ||
202 | 30 | } | ||
203 | 31 | td a { | ||
204 | 32 | display:block; | ||
205 | 33 | width:100%; | ||
206 | 34 | text-decoration:none; | ||
207 | 35 | } | ||
208 | 36 | </style> | ||
209 | 37 | </head> | ||
210 | 38 | <body> | ||
211 | 39 | <div class="col-md-12"> | ||
212 | 40 | <table id="visTable" class="table table-striped table-bordered"></table> | ||
213 | 41 | </div> | ||
214 | 42 | <script> | ||
215 | 43 | window.onload = function() { | ||
216 | 44 | draw("ust_rc-proposed_krillin_regression.json"); | ||
217 | 45 | } | ||
218 | 46 | </script> | ||
219 | 47 | </body> | ||
220 | 48 | </html> | ||
221 | 0 | 49 | ||
222 | === added file 'qakit/vis/dashboard/ust_rc-proposed_krillin_sanity.html' | |||
223 | --- qakit/vis/dashboard/ust_rc-proposed_krillin_sanity.html 1970-01-01 00:00:00 +0000 | |||
224 | +++ qakit/vis/dashboard/ust_rc-proposed_krillin_sanity.html 2016-07-13 18:36:50 +0000 | |||
225 | @@ -0,0 +1,48 @@ | |||
226 | 1 | <!DOCTYPE html> | ||
227 | 2 | <html> | ||
228 | 3 | <head> | ||
229 | 4 | <meta charset="utf-8"> | ||
230 | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
231 | 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
232 | 7 | |||
233 | 8 | <title>Platform QA - VIS</title> | ||
234 | 9 | |||
235 | 10 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"> | ||
236 | 11 | <link href="http://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet" type="text/css"> | ||
237 | 12 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> | ||
238 | 13 | <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.css"/> | ||
239 | 14 | <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> | ||
240 | 15 | <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js"></script> | ||
241 | 16 | <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> | ||
242 | 17 | <script type="text/javascript" src="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.js"></script> | ||
243 | 18 | <script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script> | ||
244 | 19 | <script src="main.js"></script> | ||
245 | 20 | <style> | ||
246 | 21 | .ar { | ||
247 | 22 | text-align: right; | ||
248 | 23 | } | ||
249 | 24 | .vertical{ | ||
250 | 25 | writing-mode:tb-rl; | ||
251 | 26 | white-space:nowrap; | ||
252 | 27 | display:block; | ||
253 | 28 | bottom:0; | ||
254 | 29 | height:20px; | ||
255 | 30 | } | ||
256 | 31 | td a { | ||
257 | 32 | display:block; | ||
258 | 33 | width:100%; | ||
259 | 34 | text-decoration:none; | ||
260 | 35 | } | ||
261 | 36 | </style> | ||
262 | 37 | </head> | ||
263 | 38 | <body> | ||
264 | 39 | <div class="col-md-12"> | ||
265 | 40 | <table id="visTable" class="table table-striped table-bordered"></table> | ||
266 | 41 | </div> | ||
267 | 42 | <script> | ||
268 | 43 | window.onload = function() { | ||
269 | 44 | draw("ust_rc-proposed_krillin_sanity.json"); | ||
270 | 45 | } | ||
271 | 46 | </script> | ||
272 | 47 | </body> | ||
273 | 48 | </html> | ||
274 | 0 | 49 | ||
275 | === added file 'qakit/vis/dashboard/ust_rc_arale_regression.html' | |||
276 | --- qakit/vis/dashboard/ust_rc_arale_regression.html 1970-01-01 00:00:00 +0000 | |||
277 | +++ qakit/vis/dashboard/ust_rc_arale_regression.html 2016-07-13 18:36:50 +0000 | |||
278 | @@ -0,0 +1,48 @@ | |||
279 | 1 | <!DOCTYPE html> | ||
280 | 2 | <html> | ||
281 | 3 | <head> | ||
282 | 4 | <meta charset="utf-8"> | ||
283 | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
284 | 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
285 | 7 | |||
286 | 8 | <title>Platform QA - VIS</title> | ||
287 | 9 | |||
288 | 10 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"> | ||
289 | 11 | <link href="http://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet" type="text/css"> | ||
290 | 12 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> | ||
291 | 13 | <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.css"/> | ||
292 | 14 | <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> | ||
293 | 15 | <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js"></script> | ||
294 | 16 | <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> | ||
295 | 17 | <script type="text/javascript" src="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.js"></script> | ||
296 | 18 | <script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script> | ||
297 | 19 | <script src="main.js"></script> | ||
298 | 20 | <style> | ||
299 | 21 | .ar { | ||
300 | 22 | text-align: right; | ||
301 | 23 | } | ||
302 | 24 | .vertical{ | ||
303 | 25 | writing-mode:tb-rl; | ||
304 | 26 | white-space:nowrap; | ||
305 | 27 | display:block; | ||
306 | 28 | bottom:0; | ||
307 | 29 | height:20px; | ||
308 | 30 | } | ||
309 | 31 | td a { | ||
310 | 32 | display:block; | ||
311 | 33 | width:100%; | ||
312 | 34 | text-decoration:none; | ||
313 | 35 | } | ||
314 | 36 | </style> | ||
315 | 37 | </head> | ||
316 | 38 | <body> | ||
317 | 39 | <div class="col-md-12"> | ||
318 | 40 | <table id="visTable" class="table table-striped table-bordered"></table> | ||
319 | 41 | </div> | ||
320 | 42 | <script> | ||
321 | 43 | window.onload = function() { | ||
322 | 44 | draw("ust_rc_arale_regression.json"); | ||
323 | 45 | } | ||
324 | 46 | </script> | ||
325 | 47 | </body> | ||
326 | 48 | </html> | ||
327 | 0 | 49 | ||
328 | === added file 'qakit/vis/dashboard/ust_rc_arale_sanity.html' | |||
329 | --- qakit/vis/dashboard/ust_rc_arale_sanity.html 1970-01-01 00:00:00 +0000 | |||
330 | +++ qakit/vis/dashboard/ust_rc_arale_sanity.html 2016-07-13 18:36:50 +0000 | |||
331 | @@ -0,0 +1,48 @@ | |||
332 | 1 | <!DOCTYPE html> | ||
333 | 2 | <html> | ||
334 | 3 | <head> | ||
335 | 4 | <meta charset="utf-8"> | ||
336 | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
337 | 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
338 | 7 | |||
339 | 8 | <title>Platform QA - VIS</title> | ||
340 | 9 | |||
341 | 10 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"> | ||
342 | 11 | <link href="http://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet" type="text/css"> | ||
343 | 12 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> | ||
344 | 13 | <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.css"/> | ||
345 | 14 | <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> | ||
346 | 15 | <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js"></script> | ||
347 | 16 | <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> | ||
348 | 17 | <script type="text/javascript" src="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.js"></script> | ||
349 | 18 | <script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script> | ||
350 | 19 | <script src="main.js"></script> | ||
351 | 20 | <style> | ||
352 | 21 | .ar { | ||
353 | 22 | text-align: right; | ||
354 | 23 | } | ||
355 | 24 | .vertical{ | ||
356 | 25 | writing-mode:tb-rl; | ||
357 | 26 | white-space:nowrap; | ||
358 | 27 | display:block; | ||
359 | 28 | bottom:0; | ||
360 | 29 | height:20px; | ||
361 | 30 | } | ||
362 | 31 | td a { | ||
363 | 32 | display:block; | ||
364 | 33 | width:100%; | ||
365 | 34 | text-decoration:none; | ||
366 | 35 | } | ||
367 | 36 | </style> | ||
368 | 37 | </head> | ||
369 | 38 | <body> | ||
370 | 39 | <div class="col-md-12"> | ||
371 | 40 | <table id="visTable" class="table table-striped table-bordered"></table> | ||
372 | 41 | </div> | ||
373 | 42 | <script> | ||
374 | 43 | window.onload = function() { | ||
375 | 44 | draw("ust_rc_arale_sanity.json"); | ||
376 | 45 | } | ||
377 | 46 | </script> | ||
378 | 47 | </body> | ||
379 | 48 | </html> | ||
380 | 0 | 49 | ||
381 | === added file 'qakit/vis/dashboard/ust_rc_krillin_regression.html' | |||
382 | --- qakit/vis/dashboard/ust_rc_krillin_regression.html 1970-01-01 00:00:00 +0000 | |||
383 | +++ qakit/vis/dashboard/ust_rc_krillin_regression.html 2016-07-13 18:36:50 +0000 | |||
384 | @@ -0,0 +1,48 @@ | |||
385 | 1 | <!DOCTYPE html> | ||
386 | 2 | <html> | ||
387 | 3 | <head> | ||
388 | 4 | <meta charset="utf-8"> | ||
389 | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
390 | 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
391 | 7 | |||
392 | 8 | <title>Platform QA - VIS</title> | ||
393 | 9 | |||
394 | 10 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"> | ||
395 | 11 | <link href="http://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet" type="text/css"> | ||
396 | 12 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> | ||
397 | 13 | <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.css"/> | ||
398 | 14 | <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> | ||
399 | 15 | <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js"></script> | ||
400 | 16 | <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> | ||
401 | 17 | <script type="text/javascript" src="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.js"></script> | ||
402 | 18 | <script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script> | ||
403 | 19 | <script src="main.js"></script> | ||
404 | 20 | <style> | ||
405 | 21 | .ar { | ||
406 | 22 | text-align: right; | ||
407 | 23 | } | ||
408 | 24 | .vertical{ | ||
409 | 25 | writing-mode:tb-rl; | ||
410 | 26 | white-space:nowrap; | ||
411 | 27 | display:block; | ||
412 | 28 | bottom:0; | ||
413 | 29 | height:20px; | ||
414 | 30 | } | ||
415 | 31 | td a { | ||
416 | 32 | display:block; | ||
417 | 33 | width:100%; | ||
418 | 34 | text-decoration:none; | ||
419 | 35 | } | ||
420 | 36 | </style> | ||
421 | 37 | </head> | ||
422 | 38 | <body> | ||
423 | 39 | <div class="col-md-12"> | ||
424 | 40 | <table id="visTable" class="table table-striped table-bordered"></table> | ||
425 | 41 | </div> | ||
426 | 42 | <script> | ||
427 | 43 | window.onload = function() { | ||
428 | 44 | draw("ust_rc_krillin_regression.json"); | ||
429 | 45 | } | ||
430 | 46 | </script> | ||
431 | 47 | </body> | ||
432 | 48 | </html> | ||
433 | 0 | 49 | ||
434 | === added file 'qakit/vis/dashboard/ust_rc_krillin_sanity.html' | |||
435 | --- qakit/vis/dashboard/ust_rc_krillin_sanity.html 1970-01-01 00:00:00 +0000 | |||
436 | +++ qakit/vis/dashboard/ust_rc_krillin_sanity.html 2016-07-13 18:36:50 +0000 | |||
437 | @@ -0,0 +1,48 @@ | |||
438 | 1 | <!DOCTYPE html> | ||
439 | 2 | <html> | ||
440 | 3 | <head> | ||
441 | 4 | <meta charset="utf-8"> | ||
442 | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
443 | 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
444 | 7 | |||
445 | 8 | <title>Platform QA - VIS</title> | ||
446 | 9 | |||
447 | 10 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"> | ||
448 | 11 | <link href="http://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet" type="text/css"> | ||
449 | 12 | <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> | ||
450 | 13 | <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.css"/> | ||
451 | 14 | <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> | ||
452 | 15 | <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js"></script> | ||
453 | 16 | <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> | ||
454 | 17 | <script type="text/javascript" src="https://cdn.datatables.net/s/bs/dt-1.10.10,b-1.1.0,fh-3.1.0,r-2.0.0/datatables.min.js"></script> | ||
455 | 18 | <script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script> | ||
456 | 19 | <script src="main.js"></script> | ||
457 | 20 | <style> | ||
458 | 21 | .ar { | ||
459 | 22 | text-align: right; | ||
460 | 23 | } | ||
461 | 24 | .vertical{ | ||
462 | 25 | writing-mode:tb-rl; | ||
463 | 26 | white-space:nowrap; | ||
464 | 27 | display:block; | ||
465 | 28 | bottom:0; | ||
466 | 29 | height:20px; | ||
467 | 30 | } | ||
468 | 31 | td a { | ||
469 | 32 | display:block; | ||
470 | 33 | width:100%; | ||
471 | 34 | text-decoration:none; | ||
472 | 35 | } | ||
473 | 36 | </style> | ||
474 | 37 | </head> | ||
475 | 38 | <body> | ||
476 | 39 | <div class="col-md-12"> | ||
477 | 40 | <table id="visTable" class="table table-striped table-bordered"></table> | ||
478 | 41 | </div> | ||
479 | 42 | <script> | ||
480 | 43 | window.onload = function() { | ||
481 | 44 | draw("ust_rc_krillin_sanity.json"); | ||
482 | 45 | } | ||
483 | 46 | </script> | ||
484 | 47 | </body> | ||
485 | 48 | </html> | ||
486 | 0 | 49 | ||
487 | === added file 'qakit/vis/jenkins_results.py' | |||
488 | --- qakit/vis/jenkins_results.py 1970-01-01 00:00:00 +0000 | |||
489 | +++ qakit/vis/jenkins_results.py 2016-07-13 18:36:50 +0000 | |||
490 | @@ -0,0 +1,70 @@ | |||
491 | 1 | #!/usr/bin/env python3 | ||
492 | 2 | # UEQA Vis | ||
493 | 3 | # Copyright (C) 2016 Canonical | ||
494 | 4 | # | ||
495 | 5 | # This program is free software: you can redistribute it and/or modify | ||
496 | 6 | # it under the terms of the GNU General Public License as published by | ||
497 | 7 | # the Free Software Foundation, either version 3 of the License, or | ||
498 | 8 | # (at your option) any later version. | ||
499 | 9 | # | ||
500 | 10 | # This program is distributed in the hope that it will be useful, | ||
501 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
502 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
503 | 13 | # GNU General Public License for more details. | ||
504 | 14 | # | ||
505 | 15 | # You should have received a copy of the GNU General Public License | ||
506 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
507 | 17 | |||
508 | 18 | |||
509 | 19 | import pymongo | ||
510 | 20 | |||
511 | 21 | |||
512 | 22 | class JenkinsResults: | ||
513 | 23 | |||
514 | 24 | def __init__(self, db): | ||
515 | 25 | self.db = db | ||
516 | 26 | |||
517 | 27 | def get_build_test_results(self, job_name, build_number): | ||
518 | 28 | """Query db for test results. | ||
519 | 29 | |||
520 | 30 | NOTE that we don't return skipped tests. | ||
521 | 31 | |||
522 | 32 | """ | ||
523 | 33 | return self.db.tests.find( | ||
524 | 34 | {'job_name': job_name, | ||
525 | 35 | 'build_number': build_number, | ||
526 | 36 | 'status': {'$ne': 'SKIPPED'}} | ||
527 | 37 | ) | ||
528 | 38 | |||
529 | 39 | def get_test_result(self, job_name, build_number, test_name): | ||
530 | 40 | """Query db for a single test result.""" | ||
531 | 41 | return self.db.tests.find_one( | ||
532 | 42 | {'test_name': test_name, | ||
533 | 43 | 'job_name': job_name, | ||
534 | 44 | 'build_number': build_number}) | ||
535 | 45 | |||
536 | 46 | def get_trailing_test_results(self, job_name, test_name, limit=10): | ||
537 | 47 | """Return last 'limit' test_name results.""" | ||
538 | 48 | return self.db.tests.find( | ||
539 | 49 | {'test_name': test_name, | ||
540 | 50 | 'job_name': job_name, | ||
541 | 51 | 'status': {'$ne': 'SKIPPED'}}).sort( | ||
542 | 52 | 'build_number', pymongo.DESCENDING).limit(limit) | ||
543 | 53 | |||
544 | 54 | def get_test_names(self, job_name): | ||
545 | 55 | """Get all test names associated with a Jenkins job from db.""" | ||
546 | 56 | return self.db.tests.find( | ||
547 | 57 | {'job_name': job_name}).distinct('test_name') | ||
548 | 58 | |||
549 | 59 | def get_build_numbers(self, job_name): | ||
550 | 60 | """Get all build numbers associated with a Jenkins job from db.""" | ||
551 | 61 | return sorted( | ||
552 | 62 | self.db.builds.find({'job_name': job_name}).distinct('number'), | ||
553 | 63 | reverse=True) | ||
554 | 64 | |||
555 | 65 | def get_build(self, job_name, build_number): | ||
556 | 66 | """Return a specific build.""" | ||
557 | 67 | return self.db.builds.find_one( | ||
558 | 68 | {'job_name': job_name, | ||
559 | 69 | 'number': build_number} | ||
560 | 70 | ) | ||
561 | 0 | 71 | ||
562 | === added file 'qakit/vis/retrieve_jenkins_results.py' | |||
563 | --- qakit/vis/retrieve_jenkins_results.py 1970-01-01 00:00:00 +0000 | |||
564 | +++ qakit/vis/retrieve_jenkins_results.py 2016-07-13 18:36:50 +0000 | |||
565 | @@ -0,0 +1,229 @@ | |||
566 | 1 | #!/usr/bin/python3 | ||
567 | 2 | |||
568 | 3 | # QA KPI Utilities | ||
569 | 4 | # Copyright (C) 2015-2016 Canonical | ||
570 | 5 | # | ||
571 | 6 | # This program is free software: you can redistribute it and/or modify | ||
572 | 7 | # it under the terms of the GNU General Public License as published by | ||
573 | 8 | # the Free Software Foundation, either version 3 of the License, or | ||
574 | 9 | # (at your option) any later version. | ||
575 | 10 | # | ||
576 | 11 | # This program is distributed in the hope that it will be useful, | ||
577 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
578 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
579 | 14 | # GNU General Public License for more details. | ||
580 | 15 | # | ||
581 | 16 | # You should have received a copy of the GNU General Public License | ||
582 | 17 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
583 | 18 | |||
584 | 19 | import argparse | ||
585 | 20 | import logging | ||
586 | 21 | import sys | ||
587 | 22 | |||
588 | 23 | import bson | ||
589 | 24 | import jenkinsapi | ||
590 | 25 | import pymongo | ||
591 | 26 | |||
592 | 27 | logger = logging.getLogger(__name__) | ||
593 | 28 | handler = logging.StreamHandler() | ||
594 | 29 | handler.setLevel(logging.DEBUG) | ||
595 | 30 | logger.addHandler(handler) | ||
596 | 31 | logger.setLevel(logging.DEBUG) | ||
597 | 32 | |||
598 | 33 | |||
599 | 34 | def filter_job_names(job_names, pattern): | ||
600 | 35 | return list(filter(lambda x: pattern in x, job_names)) | ||
601 | 36 | |||
602 | 37 | |||
603 | 38 | class JenkinsMongoSession(): | ||
604 | 39 | |||
605 | 40 | def __init__(self, db, baseurl, username=None, password=None): | ||
606 | 41 | super().__init__() | ||
607 | 42 | self.db = db | ||
608 | 43 | self.jenkins = jenkinsapi.jenkins.Jenkins( | ||
609 | 44 | baseurl, username, password) | ||
610 | 45 | |||
611 | 46 | def get_job(self, job_name): | ||
612 | 47 | return self.jenkins[job_name] | ||
613 | 48 | |||
614 | 49 | def retrieve_build_resultset(self, db_build_id): | ||
615 | 50 | """Retrieve the resultset of a known build in our db.""" | ||
616 | 51 | db_build = self.db.builds.find_one( | ||
617 | 52 | {'_id': bson.ObjectId(db_build_id)}) | ||
618 | 53 | job_name = db_build['job_name'] | ||
619 | 54 | jenkins_job = self.get_job(job_name) | ||
620 | 55 | build_number = db_build['number'] | ||
621 | 56 | jenkins_build = jenkins_job.get_build(build_number) | ||
622 | 57 | try: | ||
623 | 58 | resultset = jenkins_build.get_resultset() | ||
624 | 59 | except jenkinsapi.custom_exceptions.NoResults: | ||
625 | 60 | return {} | ||
626 | 61 | test_results = [] | ||
627 | 62 | for test_name in resultset.keys(): | ||
628 | 63 | result = resultset[test_name] | ||
629 | 64 | test_result = { | ||
630 | 65 | 'test_name': test_name, | ||
631 | 66 | 'status': result.status, | ||
632 | 67 | 'build_number': build_number, | ||
633 | 68 | 'build_timestamp': db_build['timestamp'], | ||
634 | 69 | 'build_name': db_build['description'], | ||
635 | 70 | 'job_name': job_name, | ||
636 | 71 | } | ||
637 | 72 | test_results.append(test_result) | ||
638 | 73 | return test_results | ||
639 | 74 | |||
640 | 75 | def retrieve_and_insert_build_resultset(self, db_build_id): | ||
641 | 76 | """Retrieve test results from Jenkins, inserting into our db.""" | ||
642 | 77 | build_resultset = self.retrieve_build_resultset(db_build_id) | ||
643 | 78 | return self.db.tests.insert(build_resultset) | ||
644 | 79 | |||
645 | 80 | def retrieve_build(self, job_name, build_number): | ||
646 | 81 | """Retrieve build from Jenkins.""" | ||
647 | 82 | logger.info( | ||
648 | 83 | 'Retrieving {} build #{}.'.format(job_name, build_number) | ||
649 | 84 | ) | ||
650 | 85 | job = self.get_job(job_name) | ||
651 | 86 | build = job.get_build(build_number) | ||
652 | 87 | data = build.get_data(build.python_api_url(build.baseurl)) | ||
653 | 88 | actions = build.get_actions() | ||
654 | 89 | total = failed = skipped = 0 | ||
655 | 90 | try: | ||
656 | 91 | total = actions['totalCount'] | ||
657 | 92 | failed = actions['failCount'] | ||
658 | 93 | skipped = actions['skipCount'] | ||
659 | 94 | except KeyError: | ||
660 | 95 | # we don't have a real result here but we'll still save | ||
661 | 96 | pass | ||
662 | 97 | return dict({ | ||
663 | 98 | 'job_name': job_name, | ||
664 | 99 | 'total': total, | ||
665 | 100 | 'failed': failed, | ||
666 | 101 | 'skipped': skipped}, | ||
667 | 102 | **data) | ||
668 | 103 | |||
669 | 104 | def retrieve_and_insert_build(self, job_name, build_number): | ||
670 | 105 | """Retrieve build from Jenkins and deposit in db.""" | ||
671 | 106 | build_results = self.retrieve_build(job_name, build_number) | ||
672 | 107 | return self.db.builds.insert(build_results) | ||
673 | 108 | |||
674 | 109 | def get_db_build_ids_for_job(self, job_name): | ||
675 | 110 | """Get a list of known build ids for the given job for comparison.""" | ||
676 | 111 | return [ | ||
677 | 112 | build['number'] for build in self.db.builds.find( | ||
678 | 113 | {'job_name': job_name}, | ||
679 | 114 | {'number': 1, '_id': 0}) | ||
680 | 115 | ] | ||
681 | 116 | |||
682 | 117 | def get_missing_builds(self, job_name): | ||
683 | 118 | """Return a list of build ids in Jenkins we're missing in db.""" | ||
684 | 119 | job = self.get_job(job_name) | ||
685 | 120 | jenkins_build_ids = set(job.get_build_ids()) | ||
686 | 121 | db_build_ids = set(self.get_db_build_ids_for_job(job_name)) | ||
687 | 122 | missing_builds = [ | ||
688 | 123 | build_id for build_id in | ||
689 | 124 | # take the set-difference | ||
690 | 125 | list(jenkins_build_ids.difference(db_build_ids)) | ||
691 | 126 | # filter out running builds | ||
692 | 127 | if not job.get_build(build_id).is_running()] | ||
693 | 128 | logger.info( | ||
694 | 129 | 'For job {}, missing builds {}.'.format(job_name, missing_builds)) | ||
695 | 130 | return missing_builds | ||
696 | 131 | |||
697 | 132 | def retrieve_data_for_job(self, job_name): | ||
698 | 133 | """Retrieve builds and results from Jenkins, inserting into our db.""" | ||
699 | 134 | logger.debug('Retrieving data for job {}.'.format(job_name)) | ||
700 | 135 | build_numbers = self.get_missing_builds(job_name) | ||
701 | 136 | for build_number in reversed(build_numbers): | ||
702 | 137 | db_build_id = self.retrieve_and_insert_build( | ||
703 | 138 | job_name, build_number) | ||
704 | 139 | self.retrieve_and_insert_build_resultset(db_build_id) | ||
705 | 140 | |||
706 | 141 | def retrieve_jenkins_results(self, job_names=None, pattern=None): | ||
707 | 142 | """Retrieve builds and results either by name or matching a pattern.""" | ||
708 | 143 | if pattern is not None: | ||
709 | 144 | all_job_names = self.jenkins.get_jobs_list() | ||
710 | 145 | job_names = filter_job_names(all_job_names, pattern) | ||
711 | 146 | for job_name in job_names: | ||
712 | 147 | self.retrieve_data_for_job(job_name) | ||
713 | 148 | |||
714 | 149 | |||
715 | 150 | def _parse_arguments(): | ||
716 | 151 | parser = argparse.ArgumentParser( | ||
717 | 152 | "Retrieve Jenkins build results, storing in a mongo db.") | ||
718 | 153 | parser.add_argument( | ||
719 | 154 | '-v', | ||
720 | 155 | '--verbose', | ||
721 | 156 | help='Verbose logging.', | ||
722 | 157 | default=False, | ||
723 | 158 | action='store_true', | ||
724 | 159 | ) | ||
725 | 160 | parser.add_argument( | ||
726 | 161 | '--vis-db', | ||
727 | 162 | help='MongoDB collection in which to store data.', | ||
728 | 163 | type=str, | ||
729 | 164 | default='vis', | ||
730 | 165 | required=False, | ||
731 | 166 | ) | ||
732 | 167 | parser.add_argument( | ||
733 | 168 | '--jenkins-url', | ||
734 | 169 | help='Jenkins URL from which to collect data.', | ||
735 | 170 | type=str, | ||
736 | 171 | required=True, | ||
737 | 172 | ) | ||
738 | 173 | parser.add_argument( | ||
739 | 174 | '--jenkins-user', | ||
740 | 175 | help='Jenkins username.', | ||
741 | 176 | type=str, | ||
742 | 177 | required=False, | ||
743 | 178 | ) | ||
744 | 179 | parser.add_argument( | ||
745 | 180 | '--jenkins-password', | ||
746 | 181 | help='Jenkins API token for Jenkins user.', | ||
747 | 182 | type=str, | ||
748 | 183 | required=False, | ||
749 | 184 | ) | ||
750 | 185 | parser.add_argument( | ||
751 | 186 | '--mongo-host', | ||
752 | 187 | help='MongoDB host IP.', | ||
753 | 188 | type=str, | ||
754 | 189 | default='127.0.0.1', | ||
755 | 190 | required=False, | ||
756 | 191 | ) | ||
757 | 192 | parser.add_argument( | ||
758 | 193 | nargs='*', | ||
759 | 194 | help='Job names from which to collect data.', | ||
760 | 195 | type=str, | ||
761 | 196 | dest='job_names', | ||
762 | 197 | ) | ||
763 | 198 | parser.add_argument( | ||
764 | 199 | '-p', | ||
765 | 200 | '--pattern', | ||
766 | 201 | help='Job name search pattern.', | ||
767 | 202 | type=str, | ||
768 | 203 | ) | ||
769 | 204 | args = parser.parse_args() | ||
770 | 205 | if args.pattern and args.job_names: | ||
771 | 206 | print("Please specify either job names or pattern.") | ||
772 | 207 | parser.print_help() | ||
773 | 208 | sys.exit(1) | ||
774 | 209 | return args | ||
775 | 210 | |||
776 | 211 | |||
777 | 212 | def main(): | ||
778 | 213 | args = _parse_arguments() | ||
779 | 214 | if args.verbose: | ||
780 | 215 | logger.setLevel(logging.DEBUG) | ||
781 | 216 | conn = pymongo.MongoClient(host=args.mongo_host) | ||
782 | 217 | db = getattr(conn, args.vis_db) | ||
783 | 218 | session = JenkinsMongoSession( | ||
784 | 219 | db, | ||
785 | 220 | args.jenkins_url, | ||
786 | 221 | args.jenkins_user, | ||
787 | 222 | args.jenkins_password, | ||
788 | 223 | ) | ||
789 | 224 | session.retrieve_jenkins_results( | ||
790 | 225 | args.job_names, args.pattern) | ||
791 | 226 | |||
792 | 227 | |||
793 | 228 | if __name__ == '__main__': | ||
794 | 229 | sys.exit(main()) | ||
795 | 0 | 230 | ||
796 | === added directory 'qakit/vis/tests' | |||
797 | === added file 'qakit/vis/tests/__init__.py' | |||
798 | === added file 'qakit/vis/tests/test_retrieve_jenkins_results.py' | |||
799 | --- qakit/vis/tests/test_retrieve_jenkins_results.py 1970-01-01 00:00:00 +0000 | |||
800 | +++ qakit/vis/tests/test_retrieve_jenkins_results.py 2016-07-13 18:36:50 +0000 | |||
801 | @@ -0,0 +1,47 @@ | |||
802 | 1 | #!/usr/bin/env python3 | ||
803 | 2 | # UEQA VIS | ||
804 | 3 | # Copyright (C) 2016 Canonical | ||
805 | 4 | # | ||
806 | 5 | # This program is free software: you can redistribute it and/or modify | ||
807 | 6 | # it under the terms of the GNU General Public License as published by | ||
808 | 7 | # the Free Software Foundation, either version 3 of the License, or | ||
809 | 8 | # (at your option) any later version. | ||
810 | 9 | # | ||
811 | 10 | # This program is distributed in the hope that it will be useful, | ||
812 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
813 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
814 | 13 | # GNU General Public License for more details. | ||
815 | 14 | # | ||
816 | 15 | # You should have received a copy of the GNU General Public License | ||
817 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
818 | 17 | |||
819 | 18 | import unittest | ||
820 | 19 | import unittest.mock as mock | ||
821 | 20 | |||
822 | 21 | import qakit.vis.retrieve_jenkins_results as retrieve | ||
823 | 22 | |||
824 | 23 | |||
825 | 24 | JOB_NAMES = [ | ||
826 | 25 | 'ust_rc-proposed_krillin_sanity', | ||
827 | 26 | 'ust_rc-proposed_krillin_regression', | ||
828 | 27 | 'ust_rc-proposed_arale_sanity', | ||
829 | 28 | 'foo', | ||
830 | 29 | ] | ||
831 | 30 | |||
832 | 31 | |||
833 | 32 | class RegexTestCase(unittest.TestCase): | ||
834 | 33 | |||
835 | 34 | def test_vanilla(self): | ||
836 | 35 | result = retrieve.filter_job_names(JOB_NAMES, 'ust_') | ||
837 | 36 | self.assertEqual( | ||
838 | 37 | ['ust_rc-proposed_krillin_sanity', | ||
839 | 38 | 'ust_rc-proposed_krillin_regression', | ||
840 | 39 | 'ust_rc-proposed_arale_sanity'], | ||
841 | 40 | result) | ||
842 | 41 | |||
843 | 42 | def test_just_krillin(self): | ||
844 | 43 | result = retrieve.filter_job_names(JOB_NAMES, 'krillin') | ||
845 | 44 | self.assertEqual( | ||
846 | 45 | ['ust_rc-proposed_krillin_sanity', | ||
847 | 46 | 'ust_rc-proposed_krillin_regression'], | ||
848 | 47 | result) | ||
849 | 0 | 48 | ||
850 | === added file 'qakit/vis/tests/test_visualize.py' | |||
851 | --- qakit/vis/tests/test_visualize.py 1970-01-01 00:00:00 +0000 | |||
852 | +++ qakit/vis/tests/test_visualize.py 2016-07-13 18:36:50 +0000 | |||
853 | @@ -0,0 +1,194 @@ | |||
854 | 1 | #!/usr/bin/env python3 | ||
855 | 2 | # UEQA VIS | ||
856 | 3 | # Copyright (C) 2014-2016 Canonical | ||
857 | 4 | # | ||
858 | 5 | # This program is free software: you can redistribute it and/or modify | ||
859 | 6 | # it under the terms of the GNU General Public License as published by | ||
860 | 7 | # the Free Software Foundation, either version 3 of the License, or | ||
861 | 8 | # (at your option) any later version. | ||
862 | 9 | # | ||
863 | 10 | # This program is distributed in the hope that it will be useful, | ||
864 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
865 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
866 | 13 | # GNU General Public License for more details. | ||
867 | 14 | # | ||
868 | 15 | # You should have received a copy of the GNU General Public License | ||
869 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
870 | 17 | |||
871 | 18 | import unittest | ||
872 | 19 | import unittest.mock as mock | ||
873 | 20 | |||
874 | 21 | import qakit.vis.visualize as visualize | ||
875 | 22 | |||
876 | 23 | |||
877 | 24 | class StatusToFloatTestCase(unittest.TestCase): | ||
878 | 25 | |||
879 | 26 | def test_passed(self): | ||
880 | 27 | result = visualize.status_to_float('PASSED') | ||
881 | 28 | self.assertEqual(1.0, result) | ||
882 | 29 | |||
883 | 30 | def test_failed(self): | ||
884 | 31 | result = visualize.status_to_float('FAILED') | ||
885 | 32 | self.assertEqual(0.0, result) | ||
886 | 33 | |||
887 | 34 | def test_fixed(self): | ||
888 | 35 | result = visualize.status_to_float('FIXED') | ||
889 | 36 | self.assertEqual(1.0, result) | ||
890 | 37 | |||
891 | 38 | def test_regression(self): | ||
892 | 39 | result = visualize.status_to_float('REGRESSION') | ||
893 | 40 | self.assertEqual(0.0, result) | ||
894 | 41 | |||
895 | 42 | def test_gibberish(self): | ||
896 | 43 | result = visualize.status_to_float('GIBBERISH') | ||
897 | 44 | self.assertEqual(0.0, result) | ||
898 | 45 | |||
899 | 46 | |||
900 | 47 | class ComputeColorTestCase(unittest.TestCase): | ||
901 | 48 | |||
902 | 49 | def test_compute_color_pass(self): | ||
903 | 50 | self.assertEqual('rgb(0, 255, 0)', | ||
904 | 51 | visualize.compute_color(1.0)) | ||
905 | 52 | |||
906 | 53 | def test_compute_color_fail(self): | ||
907 | 54 | self.assertEqual('rgb(255, 0, 0)', | ||
908 | 55 | visualize.compute_color(0.0)) | ||
909 | 56 | |||
910 | 57 | def test_compute_color_none(self): | ||
911 | 58 | self.assertEqual('rgb(255, 255, 255)', | ||
912 | 59 | visualize.compute_color(None)) | ||
913 | 60 | |||
914 | 61 | |||
915 | 62 | class DbVisTestCase(unittest.TestCase): | ||
916 | 63 | |||
917 | 64 | def setUp(self): | ||
918 | 65 | super().setUp() | ||
919 | 66 | self.db = mock.Mock( | ||
920 | 67 | vis=mock.Mock()) | ||
921 | 68 | |||
922 | 69 | |||
923 | 70 | @unittest.skip('Temporarily deprecated.') | ||
924 | 71 | class GetPassRateTestCase(unittest.TestCase): | ||
925 | 72 | |||
926 | 73 | def test_pass_rate(self): | ||
927 | 74 | pass_rate = 0.9 | ||
928 | 75 | pass_rate_dict = {'pass_rate': pass_rate} | ||
929 | 76 | db = mock.Mock( | ||
930 | 77 | dashboard_tests_pass_rates=mock.Mock( | ||
931 | 78 | find_one=mock.Mock( | ||
932 | 79 | return_value=pass_rate_dict))) | ||
933 | 80 | self.assertEqual( | ||
934 | 81 | pass_rate, | ||
935 | 82 | visualize.get_pass_rate( | ||
936 | 83 | 'fake_suite_name', | ||
937 | 84 | 'fake_build_name', | ||
938 | 85 | db)) | ||
939 | 86 | |||
940 | 87 | def test_pass_rate_no_results(self): | ||
941 | 88 | db = mock.Mock( | ||
942 | 89 | dashboard_tests_pass_rates=mock.Mock( | ||
943 | 90 | find_one=mock.Mock( | ||
944 | 91 | side_effect=TypeError))) | ||
945 | 92 | self.assertEqual( | ||
946 | 93 | None, | ||
947 | 94 | visualize.get_pass_rate( | ||
948 | 95 | 'fake_suite_name', | ||
949 | 96 | 'fake_build_name', | ||
950 | 97 | db)) | ||
951 | 98 | |||
952 | 99 | |||
953 | 100 | class GetUrlPathFromTestNameTestCase(unittest.TestCase): | ||
954 | 101 | |||
955 | 102 | def test_vanilla(self): | ||
956 | 103 | result = visualize.get_url_path_from_test_name( | ||
957 | 104 | 'ubuntu_system_tests.tests.test_calls.CallsTestCase.' | ||
958 | 105 | 'test_receive_incoming_call_from_contact') | ||
959 | 106 | self.assertEqual( | ||
960 | 107 | 'ubuntu_system_tests.tests.test_calls/CallsTestCase/' | ||
961 | 108 | 'test_receive_incoming_call_from_contact', | ||
962 | 109 | result) | ||
963 | 110 | |||
964 | 111 | def test_more_nesting(self): | ||
965 | 112 | result = visualize.get_url_path_from_test_name( | ||
966 | 113 | 'ubuntu_system_tests.tests.webapps.test_here.HereTestCase.' | ||
967 | 114 | 'test_retrieve_location') | ||
968 | 115 | self.assertEqual( | ||
969 | 116 | 'ubuntu_system_tests.tests.webapps.test_here/HereTestCase/' | ||
970 | 117 | 'test_retrieve_location', | ||
971 | 118 | result) | ||
972 | 119 | |||
973 | 120 | def test_odd_test_name_directs_to_build_test_result(self): | ||
974 | 121 | result = visualize.get_url_path_from_test_name( | ||
975 | 122 | 'subunit.parser') | ||
976 | 123 | self.assertEqual('', result) | ||
977 | 124 | |||
978 | 125 | |||
979 | 126 | class ComputePassRateFromListOfResultsTestCase(unittest.TestCase): | ||
980 | 127 | |||
981 | 128 | def test_vanilla(self): | ||
982 | 129 | result = visualize.compute_pass_rate_from_statuses( | ||
983 | 130 | ['PASSED', 'PASSED']) | ||
984 | 131 | self.assertEqual(1.0, result) | ||
985 | 132 | |||
986 | 133 | def test_half_passed(self): | ||
987 | 134 | result = visualize.compute_pass_rate_from_statuses( | ||
988 | 135 | ['PASSED', 'FAILED']) | ||
989 | 136 | self.assertEqual(0.5, result) | ||
990 | 137 | |||
991 | 138 | |||
992 | 139 | class SplitTestNameTestCase(unittest.TestCase): | ||
993 | 140 | |||
994 | 141 | def test_vanilla(self): | ||
995 | 142 | result = visualize.split_test_name( | ||
996 | 143 | 'foo.bar.FooTestCase.test_something') | ||
997 | 144 | self.assertEqual( | ||
998 | 145 | ('foo.bar', 'FooTestCase.test_something'), | ||
999 | 146 | result, | ||
1000 | 147 | ) | ||
1001 | 148 | |||
1002 | 149 | def test_deeper_nesting(self): | ||
1003 | 150 | result = visualize.split_test_name( | ||
1004 | 151 | 'foo.bar.baz.FooTestCase.test_something') | ||
1005 | 152 | self.assertEqual( | ||
1006 | 153 | ('foo.bar.baz', 'FooTestCase.test_something'), | ||
1007 | 154 | result, | ||
1008 | 155 | ) | ||
1009 | 156 | |||
1010 | 157 | def test_gibberish(self): | ||
1011 | 158 | result = visualize.split_test_name( | ||
1012 | 159 | 'gibberish') | ||
1013 | 160 | self.assertEqual( | ||
1014 | 161 | ('gibberish', 'gibberish'), | ||
1015 | 162 | result, | ||
1016 | 163 | ) | ||
1017 | 164 | |||
1018 | 165 | def test_gibberish_with_dots(self): | ||
1019 | 166 | result = visualize.split_test_name( | ||
1020 | 167 | 'gibberish.test_something') | ||
1021 | 168 | self.assertEqual( | ||
1022 | 169 | ('gibberish.test_something', 'gibberish.test_something'), | ||
1023 | 170 | result, | ||
1024 | 171 | ) | ||
1025 | 172 | |||
1026 | 173 | |||
1027 | 174 | class GetColorFromStatus(unittest.TestCase): | ||
1028 | 175 | |||
1029 | 176 | def test_skipped(self): | ||
1030 | 177 | result = visualize.get_color_from_status('SKIPPED') | ||
1031 | 178 | self.assertEqual('white', result) | ||
1032 | 179 | |||
1033 | 180 | def test_passed(self): | ||
1034 | 181 | result = visualize.get_color_from_status('PASSED') | ||
1035 | 182 | self.assertEqual('lime', result) | ||
1036 | 183 | |||
1037 | 184 | def test_failed(self): | ||
1038 | 185 | result = visualize.get_color_from_status('FAILED') | ||
1039 | 186 | self.assertEqual('red', result) | ||
1040 | 187 | |||
1041 | 188 | def test_regression(self): | ||
1042 | 189 | result = visualize.get_color_from_status('REGRESSION') | ||
1043 | 190 | self.assertEqual('red', result) | ||
1044 | 191 | |||
1045 | 192 | def test_fixed(self): | ||
1046 | 193 | result = visualize.get_color_from_status('FIXED') | ||
1047 | 194 | self.assertEqual('lime', result) | ||
1048 | 0 | 195 | ||
1049 | === added file 'qakit/vis/visualize.py' | |||
1050 | --- qakit/vis/visualize.py 1970-01-01 00:00:00 +0000 | |||
1051 | +++ qakit/vis/visualize.py 2016-07-13 18:36:50 +0000 | |||
1052 | @@ -0,0 +1,294 @@ | |||
1053 | 1 | #!/usr/bin/env python3 | ||
1054 | 2 | # UEQA Vis | ||
1055 | 3 | # Copyright (C) 2014-2016 Canonical | ||
1056 | 4 | # | ||
1057 | 5 | # This program is free software: you can redistribute it and/or modify | ||
1058 | 6 | # it under the terms of the GNU General Public License as published by | ||
1059 | 7 | # the Free Software Foundation, either version 3 of the License, or | ||
1060 | 8 | # (at your option) any later version. | ||
1061 | 9 | # | ||
1062 | 10 | # This program is distributed in the hope that it will be useful, | ||
1063 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1064 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1065 | 13 | # GNU General Public License for more details. | ||
1066 | 14 | # | ||
1067 | 15 | # You should have received a copy of the GNU General Public License | ||
1068 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1069 | 17 | |||
1070 | 18 | import argparse | ||
1071 | 19 | import json | ||
1072 | 20 | import math | ||
1073 | 21 | import statistics | ||
1074 | 22 | import sys | ||
1075 | 23 | |||
1076 | 24 | import pymongo | ||
1077 | 25 | |||
1078 | 26 | import jenkins_results | ||
1079 | 27 | |||
1080 | 28 | |||
1081 | 29 | def get_color_from_status(status): | ||
1082 | 30 | """Return the color for the given cell. | ||
1083 | 31 | |||
1084 | 32 | PASSED (or FIXED) = green, | ||
1085 | 33 | FAILED (or REGRESSED) = red, | ||
1086 | 34 | SKIPPED = white | ||
1087 | 35 | |||
1088 | 36 | """ | ||
1089 | 37 | red = 'red' | ||
1090 | 38 | white = 'white' | ||
1091 | 39 | # lime happens to help with trend color-calculations | ||
1092 | 40 | green = 'lime' | ||
1093 | 41 | return { | ||
1094 | 42 | 'SKIPPED': white, | ||
1095 | 43 | 'PASSED': green, | ||
1096 | 44 | 'FIXED': green, | ||
1097 | 45 | 'REGRESSION': red, | ||
1098 | 46 | 'FAILED': red, | ||
1099 | 47 | }[status] | ||
1100 | 48 | |||
1101 | 49 | |||
1102 | 50 | def compute_color(pass_rate): | ||
1103 | 51 | """Return a red-green color representing the given pass rate. | ||
1104 | 52 | |||
1105 | 53 | Use a sqrt curve to highlight the difference between 0.0 and | ||
1106 | 54 | 0.01: invert the value, apply sqrt, then invert the value | ||
1107 | 55 | again. | ||
1108 | 56 | |||
1109 | 57 | """ | ||
1110 | 58 | if pass_rate is None: | ||
1111 | 59 | # white indicates "no result" | ||
1112 | 60 | return 'rgb(255, 255, 255)' | ||
1113 | 61 | p = abs( | ||
1114 | 62 | 1.0 - math.sqrt( | ||
1115 | 63 | abs(1.0 - float(pass_rate)) | ||
1116 | 64 | ) | ||
1117 | 65 | ) * 255.0 | ||
1118 | 66 | return 'rgb({}, {}, 0)'.format( | ||
1119 | 67 | int(255 - p), | ||
1120 | 68 | int(p), | ||
1121 | 69 | ) | ||
1122 | 70 | |||
1123 | 71 | |||
1124 | 72 | def status_to_float(status): | ||
1125 | 73 | """Convert JUnit test status to float for averaging.""" | ||
1126 | 74 | if status == 'PASSED' or status == 'FIXED': | ||
1127 | 75 | return 1.0 | ||
1128 | 76 | else: | ||
1129 | 77 | return 0.0 | ||
1130 | 78 | |||
1131 | 79 | |||
1132 | 80 | def compute_pass_rate_from_statuses(results): | ||
1133 | 81 | """Compute pass rate from a list of JUnit test statuses.""" | ||
1134 | 82 | float_results = [status_to_float(result) for result in results] | ||
1135 | 83 | try: | ||
1136 | 84 | return statistics.mean(float_results) | ||
1137 | 85 | except statistics.StatisticsError: | ||
1138 | 86 | return None | ||
1139 | 87 | |||
1140 | 88 | |||
1141 | 89 | def compute_color_from_statuses(statuses): | ||
1142 | 90 | """Compute a red-green color from a list of JUnit test statuses.""" | ||
1143 | 91 | return compute_color(compute_pass_rate_from_statuses(statuses)) | ||
1144 | 92 | |||
1145 | 93 | |||
1146 | 94 | def get_build_pass_rate(dao, job_name, build_number): | ||
1147 | 95 | """Calculate pass rate for a specified build in the given db.""" | ||
1148 | 96 | results = dao.get_build_test_results(job_name, build_number) | ||
1149 | 97 | try: | ||
1150 | 98 | statuses = [result['status'] for result in results] | ||
1151 | 99 | return compute_pass_rate_from_statuses(statuses) | ||
1152 | 100 | except TypeError: | ||
1153 | 101 | # no result in db | ||
1154 | 102 | return None | ||
1155 | 103 | |||
1156 | 104 | |||
1157 | 105 | def get_url_path_from_test_name(test_name): | ||
1158 | 106 | """Return url path of Jenkins report for test. | ||
1159 | 107 | |||
1160 | 108 | Jenkins displays the result (incl. e.g. traceback) for a given | ||
1161 | 109 | test at specific URL path under the build URL. | ||
1162 | 110 | |||
1163 | 111 | """ | ||
1164 | 112 | split_test_name = test_name.split('.') | ||
1165 | 113 | if len(split_test_name) < 5: | ||
1166 | 114 | return '' | ||
1167 | 115 | try: | ||
1168 | 116 | url_path = '/'.join( | ||
1169 | 117 | ['.'.join( | ||
1170 | 118 | split_test_name[:-2]), | ||
1171 | 119 | split_test_name[-2], | ||
1172 | 120 | split_test_name[-1]]) | ||
1173 | 121 | except IndexError: | ||
1174 | 122 | url_path = '' | ||
1175 | 123 | return url_path | ||
1176 | 124 | |||
1177 | 125 | |||
1178 | 126 | def get_test_link_for_build(build_url, test_name): | ||
1179 | 127 | """Return a link to the test under the given Jenkins build.""" | ||
1180 | 128 | link = get_url_path_from_test_name(test_name) | ||
1181 | 129 | return build_url + 'testReport/' + link | ||
1182 | 130 | |||
1183 | 131 | |||
1184 | 132 | def get_cell_data(dao, job_name, build_number, test_name): | ||
1185 | 133 | """Return a dict representing the given test for rendering.""" | ||
1186 | 134 | cell = {'test_name': test_name, | ||
1187 | 135 | 'job_name': job_name, | ||
1188 | 136 | 'build_number': build_number} | ||
1189 | 137 | test_result = dao.get_test_result(**cell) | ||
1190 | 138 | build = dao.get_build(job_name, build_number) | ||
1191 | 139 | if not test_result: | ||
1192 | 140 | return dict({'status': None, 'color': None, 'link': None}, **cell) | ||
1193 | 141 | cell['status'] = test_result['status'] | ||
1194 | 142 | try: | ||
1195 | 143 | cell['color'] = get_color_from_status(test_result['status']) | ||
1196 | 144 | except TypeError: | ||
1197 | 145 | # no run for test, default to white | ||
1198 | 146 | cell['color'] = 'rgb{255, 255, 255}' | ||
1199 | 147 | cell['skipped'] = test_result['status'] == 'SKIPPED' | ||
1200 | 148 | cell['link'] = get_test_link_for_build(build['url'], test_name) | ||
1201 | 149 | return cell | ||
1202 | 150 | |||
1203 | 151 | |||
1204 | 152 | def get_trend_cell(dao, job_name, test_name, limit=10): | ||
1205 | 153 | """Return a dict representing the pass rate trend.""" | ||
1206 | 154 | cell = {'test_name': test_name, | ||
1207 | 155 | 'job_name': job_name} | ||
1208 | 156 | test_results = dao.get_trailing_test_results(**cell) | ||
1209 | 157 | cell['color'] = compute_color_from_statuses( | ||
1210 | 158 | [test_result['status'] for test_result in test_results] | ||
1211 | 159 | ) | ||
1212 | 160 | return cell | ||
1213 | 161 | |||
1214 | 162 | |||
1215 | 163 | def split_test_name(test_name): | ||
1216 | 164 | """Return a tuple of (section, test_name) for given test name. | ||
1217 | 165 | |||
1218 | 166 | Typically tests are named as | ||
1219 | 167 | |||
1220 | 168 | foo.bar.baz.TestCaseClass.test_something | ||
1221 | 169 | |||
1222 | 170 | so return a "section" as 'foo.bar.baz', test_name as | ||
1223 | 171 | TestCaseClass.test_something. | ||
1224 | 172 | |||
1225 | 173 | Non-conforming test names (e.g. 'gibberish') are granted their own | ||
1226 | 174 | section names: ('gibberish', 'gibberish'). | ||
1227 | 175 | |||
1228 | 176 | """ | ||
1229 | 177 | split_test_name = test_name.split('.') | ||
1230 | 178 | if len(split_test_name) <= 2: | ||
1231 | 179 | return (test_name, test_name) | ||
1232 | 180 | test_name = '.'.join(split_test_name[-2:]) | ||
1233 | 181 | section_name = '.'.join(split_test_name[:-2]) | ||
1234 | 182 | return (section_name, test_name) | ||
1235 | 183 | |||
1236 | 184 | |||
1237 | 185 | def get_test_row(dao, job_name, test_name, build_numbers): | ||
1238 | 186 | """Return a dict of test results for rendering.""" | ||
1239 | 187 | test_name_split = split_test_name(test_name) | ||
1240 | 188 | row = { | ||
1241 | 189 | 'section': test_name_split[0], | ||
1242 | 190 | 'test_name': test_name_split[1], | ||
1243 | 191 | } | ||
1244 | 192 | row['trend'] = get_trend_cell(dao, job_name, test_name) | ||
1245 | 193 | builds = [] | ||
1246 | 194 | for build_number in build_numbers: | ||
1247 | 195 | builds.append(get_cell_data( | ||
1248 | 196 | dao, job_name, build_number, test_name)) | ||
1249 | 197 | row['builds'] = builds | ||
1250 | 198 | return row | ||
1251 | 199 | |||
1252 | 200 | |||
1253 | 201 | def get_report( | ||
1254 | 202 | dao, | ||
1255 | 203 | job_name): | ||
1256 | 204 | """Generate a JSON representation from db data.""" | ||
1257 | 205 | build_numbers = dao.get_build_numbers(job_name) | ||
1258 | 206 | pass_rates = [] | ||
1259 | 207 | for build_number in build_numbers: | ||
1260 | 208 | pass_rates.append(get_build_pass_rate(dao, job_name, build_number)) | ||
1261 | 209 | |||
1262 | 210 | report = {} | ||
1263 | 211 | report['header'] = ['Section', 'Test Name', 'Trend'] | ||
1264 | 212 | |||
1265 | 213 | # TODO: this may belong in the DataTables js, easier to compose here | ||
1266 | 214 | def get_pass_rate_build_number_header(pass_rate, build_number): | ||
1267 | 215 | try: | ||
1268 | 216 | # express pass_rate as a percentage | ||
1269 | 217 | return "{} ({:2.1f}%)".format(build_number, pass_rate*100) | ||
1270 | 218 | except TypeError: | ||
1271 | 219 | # when we encounter a None: | ||
1272 | 220 | return "{}".format(build_number) | ||
1273 | 221 | |||
1274 | 222 | report['header'].extend([ | ||
1275 | 223 | get_pass_rate_build_number_header(pass_rate, build_number) | ||
1276 | 224 | for build_number, pass_rate in | ||
1277 | 225 | zip(build_numbers, pass_rates) | ||
1278 | 226 | ]) | ||
1279 | 227 | |||
1280 | 228 | test_names = dao.get_test_names(job_name) | ||
1281 | 229 | data = [] | ||
1282 | 230 | for test_name in test_names: | ||
1283 | 231 | data.append( | ||
1284 | 232 | get_test_row( | ||
1285 | 233 | dao, | ||
1286 | 234 | job_name, | ||
1287 | 235 | test_name, | ||
1288 | 236 | build_numbers, | ||
1289 | 237 | ) | ||
1290 | 238 | ) | ||
1291 | 239 | report['data'] = data | ||
1292 | 240 | return report | ||
1293 | 241 | |||
1294 | 242 | |||
1295 | 243 | def write_json_report( | ||
1296 | 244 | dao, | ||
1297 | 245 | job_name, | ||
1298 | 246 | output_filepath=None): | ||
1299 | 247 | """Write a JSON report of test performance to the given filepath.""" | ||
1300 | 248 | json_report = get_report(dao, job_name) | ||
1301 | 249 | with open(output_filepath, 'w') as f: | ||
1302 | 250 | f.write(json.dumps(json_report, indent=4)) | ||
1303 | 251 | |||
1304 | 252 | |||
1305 | 253 | def _parse_arguments(): | ||
1306 | 254 | parser = argparse.ArgumentParser( | ||
1307 | 255 | "Write a JSON report of test performance.") | ||
1308 | 256 | parser.add_argument( | ||
1309 | 257 | help='Jenkins job to visualize.', | ||
1310 | 258 | type=str, | ||
1311 | 259 | dest='jenkins_job') | ||
1312 | 260 | parser.add_argument( | ||
1313 | 261 | '-o', | ||
1314 | 262 | help="Output filepath for JSON report.", | ||
1315 | 263 | type=str, | ||
1316 | 264 | dest='output_filepath', | ||
1317 | 265 | required=True) | ||
1318 | 266 | parser.add_argument( | ||
1319 | 267 | '--mongo-host', | ||
1320 | 268 | help='MongoDB host IP.', | ||
1321 | 269 | type=str, | ||
1322 | 270 | default='127.0.0.1', | ||
1323 | 271 | required=False) | ||
1324 | 272 | parser.add_argument( | ||
1325 | 273 | '--vis-db', | ||
1326 | 274 | help='MongoDB database from which to generate report.', | ||
1327 | 275 | type=str, | ||
1328 | 276 | default='vis', | ||
1329 | 277 | required=False) | ||
1330 | 278 | return parser.parse_args() | ||
1331 | 279 | |||
1332 | 280 | |||
1333 | 281 | def main(): | ||
1334 | 282 | args = _parse_arguments() | ||
1335 | 283 | conn = pymongo.MongoClient(host=args.mongo_host) | ||
1336 | 284 | db = getattr(conn, args.vis_db) | ||
1337 | 285 | dao = jenkins_results.JenkinsResults(db) | ||
1338 | 286 | return write_json_report( | ||
1339 | 287 | dao, | ||
1340 | 288 | args.jenkins_job, | ||
1341 | 289 | args.output_filepath, | ||
1342 | 290 | ) | ||
1343 | 291 | |||
1344 | 292 | |||
1345 | 293 | if __name__ == '__main__': | ||
1346 | 294 | sys.exit(main()) |
I made a high level review, I left some comments inline.