Merge lp:~toykeeper/qakit/add-pt-to-html into lp:qakit
- add-pt-to-html
- Merge into trunk
Status: | Needs review |
---|---|
Proposed branch: | lp:~toykeeper/qakit/add-pt-to-html |
Merge into: | lp:qakit |
Diff against target: |
812 lines (+777/-0) 5 files modified
qakit/practitest/all-suites.sh (+9/-0) qakit/practitest/practitest.py (+126/-0) qakit/practitest/pt-export.py (+495/-0) qakit/practitest/scripts.js (+97/-0) qakit/practitest/style.css (+50/-0) |
To merge this branch: | bzr merge lp:~toykeeper/qakit/add-pt-to-html |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Canonical Platform QA Team | Pending | ||
Review via email: mp+259446@code.launchpad.net |
Commit message
Added a PractiTest suite exporter for easy viewing of cases.
Description of the change
Added a PractiTest suite exporter for easy viewing of cases.
- 12. By Selene ToyKeeper
-
Made details much wider (is its own table row, made some code more complex),
made entire row clickable to expand/collapse details,
made search filter each word individually,
added expand/collapse all links. - 13. By Selene ToyKeeper
-
Added -r / --regenerate CLI option.
PT can only export every 2 hours, so it helps to be able to control
whether the export is re-requested or not. - 14. By Selene ToyKeeper
-
Added support for new test suite schema. Choose a schema based on project ID.
Added a count of matching tests in current view. - 15. By Selene ToyKeeper
-
Fixed pep8 lint complaints.
(except for the ones relating to vim's indent settings
and commented-out lines of code) - 16. By Selene ToyKeeper
-
Small bug fix in filter function; could match strings which spanned multiple html tags.
Allowed linewrap in "domain" column.
Minor code cleaning. - 17. By Selene ToyKeeper
-
Added a script to export all current PT projects.
- 18. By Selene ToyKeeper
-
Ensure row colors will still alternate after a search.
Allan LeSage (allanlesage) wrote : | # |
This is big! I'm looking through but will need a more thorough review than I'm able to give presently.
Selene ToyKeeper (toykeeper) wrote : | # |
It might help to first merge bzr+ssh:
Max Brustkern (nuclearbob) wrote : | # |
I'm not sure how to do it, but if you set that branch as a prerequisite, it'll only show the new diff.
Brendan Donegan (brendan-donegan) wrote : | # |
You need to resubmit the MP and can specify the prerequisite there
On Fri, May 22, 2015 at 10:17 PM, Max Brustkern <<email address hidden>
> wrote:
> I'm not sure how to do it, but if you set that branch as a prerequisite,
> it'll only show the new diff.
> --
> https:/
> Your team Canonical Platform QA Team is requested to review the proposed
> merge of lp:~toykeeper/qakit/add-pt-to-html into lp:qakit.
>
- 19. By Selene ToyKeeper
-
Added ability to filter during conversion, and to dump external IDs.
Made it easier to filter client-side html by automated status. - 20. By Selene ToyKeeper
-
Renamed pt-to-html to pt-export.
- 21. By Selene ToyKeeper
-
merged updates from trunk
Unmerged revisions
- 21. By Selene ToyKeeper
-
merged updates from trunk
- 20. By Selene ToyKeeper
-
Renamed pt-to-html to pt-export.
- 19. By Selene ToyKeeper
-
Added ability to filter during conversion, and to dump external IDs.
Made it easier to filter client-side html by automated status. - 18. By Selene ToyKeeper
-
Ensure row colors will still alternate after a search.
- 17. By Selene ToyKeeper
-
Added a script to export all current PT projects.
- 16. By Selene ToyKeeper
-
Small bug fix in filter function; could match strings which spanned multiple html tags.
Allowed linewrap in "domain" column.
Minor code cleaning. - 15. By Selene ToyKeeper
-
Fixed pep8 lint complaints.
(except for the ones relating to vim's indent settings
and commented-out lines of code) - 14. By Selene ToyKeeper
-
Added support for new test suite schema. Choose a schema based on project ID.
Added a count of matching tests in current view. - 13. By Selene ToyKeeper
-
Added -r / --regenerate CLI option.
PT can only export every 2 hours, so it helps to be able to control
whether the export is re-requested or not. - 12. By Selene ToyKeeper
-
Made details much wider (is its own table row, made some code more complex),
made entire row clickable to expand/collapse details,
made search filter each word individually,
added expand/collapse all links.
Preview Diff
1 | === added file 'qakit/practitest/all-suites.sh' |
2 | --- qakit/practitest/all-suites.sh 1970-01-01 00:00:00 +0000 |
3 | +++ qakit/practitest/all-suites.sh 2015-08-10 20:32:55 +0000 |
4 | @@ -0,0 +1,9 @@ |
5 | +#!/bin/sh |
6 | + |
7 | +# LANG is required to avoid python unicode issues |
8 | +export LANG=en_US.UTF-8 |
9 | +export PYTHONPATH=../.. |
10 | +for suite in 1294 1548 ; do |
11 | + perl -i.old -pe 's/^(PRACTITEST_PROJECT_ID) = (\d+)/$1 = '$suite'/;' config.ini |
12 | + ./pt-export.py $* -o /tmp/$suite.html |
13 | +done |
14 | |
15 | === modified file 'qakit/practitest/practitest.py' |
16 | --- qakit/practitest/practitest.py 2015-06-30 14:44:59 +0000 |
17 | +++ qakit/practitest/practitest.py 2015-08-10 20:32:55 +0000 |
18 | @@ -17,7 +17,11 @@ |
19 | |
20 | import json |
21 | import logging |
22 | +import os |
23 | import requests |
24 | +import subprocess |
25 | +import time |
26 | +import xlrd |
27 | |
28 | from pprint import pformat |
29 | |
30 | @@ -324,3 +328,125 @@ |
31 | logging.debug(pformat(result)) |
32 | logging.info('Updating issue pt:{} / lp:{}'.format(ptid, lpid)) |
33 | return self._put(url, data=result) |
34 | + |
35 | + def request_export(self, entity='Step'): |
36 | + """Ask the server to queue an export for all data of one type. |
37 | + |
38 | + Will create a list of Issues, Tests , TestSets, Requirements or Steps, |
39 | + in a CSV format, to be able to export these entities to a regular |
40 | + spreadsheet. The method will put the export in the queue, and usually |
41 | + within couple of minutes it's ready to download. |
42 | + |
43 | + Parameters: |
44 | + :param entity: one of the following: 'Issue', 'Test', 'TestSet', |
45 | + 'Requirement', 'Step' |
46 | + |
47 | + Returns the following: |
48 | + id: the id of the export - a reference to receive the output, |
49 | + hours_gap: the # of hours until the user can re-create the same report |
50 | + hours_to_delete: the # of hours until the system will auto-delete this |
51 | + file |
52 | + """ |
53 | + #print('request_export(%s)' % (entity)) |
54 | + url = 'https://prod.practitest.com/api/exporter.json' |
55 | + data = dict(project_id=self.project_id, entity=entity) |
56 | + val = self._post(url, data).json() |
57 | + if 'error' in val: |
58 | + raise ValueError(val['error']) |
59 | + return val |
60 | + |
61 | + def poll_export(self, id_): |
62 | + """Will check the status of a requested export. |
63 | + Parameters: |
64 | + :param id_: from the request_export method above |
65 | + |
66 | + Returns: |
67 | + id: the id of this export, |
68 | + updated_at: the time it was updated |
69 | + status: the of this export (In Queue, working, completed), |
70 | + in_progress: true if it's not completed, |
71 | + url: the location to take the file from (via wget or curl) |
72 | + """ |
73 | + print('poll_export(%s)' % (id_)) |
74 | + url = 'https://prod.practitest.com/api/exporter/%s.json' % (id_) |
75 | + val = self._get(url).json() |
76 | + return val |
77 | + |
78 | + def get_completed_export(self, url): |
79 | + """Download a completed export file and return its local filesystem path. |
80 | + """ |
81 | + save_path = '/tmp/export.%s' % (time.strftime('%Y-%m-%d_%H:%M:%S')) |
82 | + #print('get_completed_export(%s) -> %s' % (url, save_path)) |
83 | + cmd = ('wget', '-o', '/dev/null', '-O', save_path, url) |
84 | + subprocess.check_call(cmd) |
85 | + # PT sends a .xslx in a .zip file :( |
86 | + unzipped_dir = '%s.d' % (save_path) |
87 | + if not os.path.exists(unzipped_dir): os.makedirs(unzipped_dir) |
88 | + prev_dir = os.getcwd() |
89 | + os.chdir(unzipped_dir) |
90 | + cmd = ('unzip', save_path) |
91 | + subprocess.check_call(cmd) |
92 | + os.chdir(prev_dir) |
93 | + # find the unzipped .xlsx file |
94 | + matches = [x for x in os.listdir(unzipped_dir) if x.endswith('.xlsx')] |
95 | + if len(matches) != 1: |
96 | + raise ValueError('PT export file had unexpected contents.') |
97 | + xlsx_path = os.path.join(unzipped_dir, matches[0]) |
98 | + #print('get_completed_export() done') |
99 | + return xlsx_path |
100 | + |
101 | + def load_csv_export(self, path): |
102 | + """Load a .csv export file saved by get_completed_export(). |
103 | + """ |
104 | + #print('load_csv_export(%s)' % (path)) |
105 | + wb = xlrd.open_workbook(path) |
106 | + names = wb.sheet_names() |
107 | + if len(names) != 1: |
108 | + raise ValueError('XLSX sheet layout not as expected') |
109 | + sheet = wb.sheet_by_index(0) |
110 | + return sheet |
111 | + |
112 | + def request_and_wait_for_exports(self, entities=None, ids=None): |
113 | + #print('request_and_wait_for_exports(): %s' % (', '.join(entities))) |
114 | + if not entities: entities=[] |
115 | + if not ids: ids=[] |
116 | + |
117 | + requested = [] |
118 | + for i, entity in enumerate(entities): |
119 | + if len(ids) > i: # already downloaded, ID known |
120 | + continue |
121 | + d = dict(done=False, entity=entity) |
122 | + j = self.request_export(entity) |
123 | + print(j) |
124 | + for f in ('id', 'hours_gap', 'hours_to_delete'): |
125 | + d[f] = j[f] |
126 | + #print('request_export(%s): %s=%s' % (entity, f, d[f])) |
127 | + requested.append(d) |
128 | + |
129 | + for i, id_ in enumerate(ids): |
130 | + if len(entities) > i: # already downloaded, ID known |
131 | + entity = entities[i] |
132 | + else: |
133 | + entity = 'unknown' |
134 | + d = dict(done=False, id=id_, entity=entity) |
135 | + requested.append(d) |
136 | + |
137 | + while True: |
138 | + for d in requested: |
139 | + if not d['done']: |
140 | + j = self.poll_export(d['id']) |
141 | + for f in ('updated_at', 'status', 'in_progress', 'url'): |
142 | + d[f] = j[f] |
143 | + print('poll_export(%s): %s=%s' % (d['id'], f, d[f])) |
144 | + print('request_and_wait_for_export(%s): id=%s, status=%s' |
145 | + % (d['entity'], d['id'], d['status'])) |
146 | + if not d['in_progress']: |
147 | + d['saved_path'] = self.get_completed_export(d['url']) |
148 | + d['data'] = self.load_csv_export(d['saved_path']) |
149 | + d['done'] = True |
150 | + |
151 | + done = all([d['done'] for d in requested]) |
152 | + if done: break |
153 | + time.sleep(5) |
154 | + |
155 | + return requested |
156 | |
157 | === added file 'qakit/practitest/pt-export.py' |
158 | --- qakit/practitest/pt-export.py 1970-01-01 00:00:00 +0000 |
159 | +++ qakit/practitest/pt-export.py 2015-08-10 20:32:55 +0000 |
160 | @@ -0,0 +1,495 @@ |
161 | +#!/usr/bin/env python3 |
162 | + |
163 | +# Export PractiTest data to HTML for viewing without an account |
164 | + |
165 | +# Copyright (C) 2015 Canonical |
166 | +# |
167 | +# Author: Selene Scriven |
168 | +# |
169 | +# This program is free software: you can redistribute it and/or modify |
170 | +# it under the terms of the GNU General Public License as published by |
171 | +# the Free Software Foundation, either version 3 of the License, or |
172 | +# (at your option) any later version. |
173 | +# |
174 | +# This program is distributed in the hope that it will be useful, |
175 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
176 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
177 | +# GNU General Public License for more details. |
178 | +# |
179 | +# You should have received a copy of the GNU General Public License |
180 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
181 | + |
182 | +import configparser |
183 | +import os |
184 | +import sys |
185 | + |
186 | +import practitest |
187 | + |
188 | +#from pprint import pprint |
189 | + |
190 | +cfg = None |
191 | + |
192 | + |
193 | +def main(args): |
194 | + """pt-export: Export PractiTest data to plain HTML. |
195 | + Usage: pt-export [options] [filters] |
196 | + -h --help Show this help message and exit |
197 | + -c --cfg PATH Read config from PATH (default: config.ini) |
198 | + -o --out PATH Write output to PATH |
199 | + -r --regenerate Ask PT for a new export instead of previous one. |
200 | + --dump-extid Export external IDs in a tab-separated format. |
201 | + |
202 | + Filters can be any of the following: (values are not case sensitive) |
203 | + Column=value Require test column to have an exact value. |
204 | + Column like value Require value to be somewhere in test column. |
205 | + any=value Require at least one column to have an exact value. |
206 | + any like value Require at least one column to contain value. |
207 | + value Same as 'any like value'. |
208 | + """ |
209 | + global cfg |
210 | + cfg_path = 'config.ini' |
211 | + out_path = '' |
212 | + out_fmt = 'html' |
213 | + regenerate = False |
214 | + filters = [] |
215 | + |
216 | + # Very simple CLI, nothing fancy needed |
217 | + i = 0 |
218 | + while i < len(args): |
219 | + a = args[i] |
220 | + if a in ('-h', '--help'): |
221 | + return usage() |
222 | + elif a in ('-c', '--cfg', '--config'): |
223 | + i += 1 |
224 | + cfg_path = args[i] |
225 | + elif a in ('-o', '--out'): |
226 | + i += 1 |
227 | + out_path = args[i] |
228 | + elif a in ('-r', '--regenerate'): |
229 | + regenerate = True |
230 | + elif a in ('--dump-extid',): |
231 | + out_fmt = 'extid' |
232 | + elif a in ('-u', '--force-unicode'): |
233 | + # TODO: Find a way to force UTF-8 for all I/O instead of ascii |
234 | + #os.environ['LANG'] = 'en_US.UTF-8' |
235 | + # Instead, run the script like this: |
236 | + # LANG=en_US.UTF-8 ./pt-export.py |
237 | + pass |
238 | + else: |
239 | + #return usage() |
240 | + filters.append(a) |
241 | + i += 1 |
242 | + |
243 | + cfg = read_config(cfg_path) |
244 | + if cfg['practitest_project_id'] not in ('1294', '1548'): |
245 | + raise ValueError('Unknown project ID: %s' |
246 | + % (cfg['practitest_project_id'])) |
247 | + PT = practitest.PractitestSession( |
248 | + cfg['practitest_project_id'], |
249 | + cfg['practitest_api_key'], |
250 | + cfg['practitest_api_secret_key'], |
251 | + cfg['practitest_user_email']) |
252 | + |
253 | + export_file(PT, out_path, regenerate, out_fmt, filters) |
254 | + |
255 | + |
256 | +def usage(): |
257 | + print(main.__doc__) |
258 | + |
259 | + |
260 | +def export_file(PT, out_path, regenerate, out_fmt, filters): |
261 | + global cfg |
262 | + |
263 | + print('Downloading tests...') |
264 | + if regenerate: |
265 | + exports = PT.request_and_wait_for_exports(entities=['Test']) |
266 | + else: |
267 | + if cfg['practitest_project_id'] == '1294': |
268 | + exports = PT.request_and_wait_for_exports(entities=['Test'], |
269 | + ids=[41]) |
270 | + elif cfg['practitest_project_id'] == '1548': |
271 | + exports = PT.request_and_wait_for_exports(entities=['Test'], |
272 | + ids=[330]) |
273 | + |
274 | + # So, PT includes steps in 'Test' export, even though steps aren't |
275 | + # in the Test API... not necessary to load 'Step' export too. |
276 | + tests = exports[0]['data'] |
277 | + |
278 | + print('Parsing tests...') |
279 | + tests = interpret_test_sheet(tests) |
280 | + |
281 | + print('Filtering tests...') |
282 | + tests = filter_tests(tests, filters) |
283 | + |
284 | + print('Formatting tests...') |
285 | + if out_fmt == 'html': |
286 | + lines = exported_tests_to_html(tests) |
287 | + elif out_fmt == 'extid': |
288 | + lines = dump_extid(tests) |
289 | + else: |
290 | + raise ValueError('Unknown output format: %s' % out_fmt) |
291 | + |
292 | + print('Saving...') |
293 | + if out_path: |
294 | + open(out_path, 'w').writelines(lines) |
295 | + |
296 | + |
297 | +def filter_tests(tests, filters): |
298 | + """Interpret a list of filters, apply them to tests, return all matches. |
299 | + Return all tests by default. |
300 | + Filters are a list in any of the following formats: |
301 | + Column=value |
302 | + Column like value |
303 | + value |
304 | + The 'Column' part may be 'any' to match all columns. |
305 | + The plain 'value' format translates to 'any like value'. |
306 | + """ |
307 | + |
308 | + if not filters: return tests |
309 | + |
310 | + # First, interpret the filters |
311 | + parsed_filters = [] |
312 | + for f in filters: |
313 | + if '=' in f: |
314 | + left, right = f.split('=') |
315 | + right = right.lower() |
316 | + parsed_filters.append(('=', left, right)) |
317 | + elif ' like ' in f: |
318 | + left, right = f.split(' like ') |
319 | + right = right.lower() |
320 | + parsed_filters.append(('like', left, right)) |
321 | + else: |
322 | + parsed_filters.append(('like', 'any', f)) |
323 | + |
324 | + # Apply the filters |
325 | + filtered = [] |
326 | + for test in tests: |
327 | + match = True |
328 | + |
329 | + for type_, key, needle in parsed_filters: |
330 | + values = [] |
331 | + if key == 'any': |
332 | + for key in test: |
333 | + values.append(str(test[key]).lower()) |
334 | + else: |
335 | + if key not in test: |
336 | + match = False |
337 | + continue |
338 | + val = str(test[key]).lower() |
339 | + values = [val] |
340 | + |
341 | + found = False |
342 | + for val in values: |
343 | + if type_ == '=': |
344 | + if val == needle: |
345 | + found = True |
346 | + elif type_ == 'like': |
347 | + if needle in val: |
348 | + found = True |
349 | + if not found: |
350 | + match = False |
351 | + |
352 | + if match: |
353 | + filtered.append(test) |
354 | + |
355 | + return filtered |
356 | + |
357 | + |
358 | +def dump_extid(tests): |
359 | + """Make a report of test ID, external test ID, and test name |
360 | + (in a tab-separated format, nulls will be represented as '-') |
361 | + """ |
362 | + lines = [] |
363 | + columns = ('id', 'External ID', 'Name') |
364 | + for t in tests: |
365 | + fields = [] |
366 | + for c in columns: |
367 | + v = str(t[c]) |
368 | + if not v: v = '-' |
369 | + fields.append(v) |
370 | + line = '\t'.join(fields) + '\n' |
371 | + lines.append(line) |
372 | + |
373 | + return lines |
374 | + |
375 | + |
376 | +def interpret_test_sheet(tests): |
377 | + """Convert the PT xlsx export into a usable object tree. |
378 | + The input file is an awkward cross between a database and a spreadsheet, |
379 | + with related tables (Steps) broken out into an embedded sheet rectangle |
380 | + for each parent object... so it's a little awkward. |
381 | + """ |
382 | + |
383 | + # pull column names from sheet |
384 | + # save it into a format of test_columns[title] = (index, conversion func) |
385 | + test_columns = dict() |
386 | + columns = tests.row_values(0) |
387 | + for i, c in enumerate(columns): |
388 | + #print('col %i: %s' % (i, c)) |
389 | + test_columns[c] = [i, str] |
390 | + |
391 | + def int_or_none(x): |
392 | + if x == '': |
393 | + return None |
394 | + else: |
395 | + return int(x) |
396 | + |
397 | + # convert non-string columns |
398 | + test_columns['id'][1] = int_or_none |
399 | + test_columns['Step position'][1] = int_or_none |
400 | + |
401 | + def tests_with_steps(tests): |
402 | + merged = [] |
403 | + for i in range(1, tests.nrows): |
404 | + row = tests.row_values(i) |
405 | + rowlen = tests.row_len(i) |
406 | + |
407 | + # is this is a continuation of the previous row? |
408 | + if row[0]: |
409 | + t = dict() |
410 | + merged.append(t) |
411 | + t['steps'] = [] # start with no test steps |
412 | + child = False |
413 | + else: |
414 | + t = merged[-1] |
415 | + child = True |
416 | + |
417 | + step = dict() |
418 | + for name, (col, func) in test_columns.items(): |
419 | + # skip empty child cells |
420 | + if child and not row[col]: |
421 | + continue |
422 | + try: |
423 | + val = func(row[col]) |
424 | + except Exception as e: |
425 | + print('Warn: row %i conversion error: %s' % (i, str(e))) |
426 | + val = row[col] |
427 | + if name.startswith('Step '): |
428 | + step[name] = val |
429 | + else: |
430 | + if child: |
431 | + t[name] = t[name] + '\n' + val |
432 | + else: |
433 | + t[name] = val |
434 | + if ('Step position' in step) \ |
435 | + and (step['Step position'] is not None): |
436 | + t['steps'].append(step) |
437 | + |
438 | + #print('%s: %s (%i steps)' % (t['id'], t['Name'], len(t['steps']))) |
439 | + #pprint(t['steps']) |
440 | + |
441 | + return merged |
442 | + |
443 | + all_tests = tests_with_steps(tests) |
444 | + |
445 | + return all_tests |
446 | + |
447 | + |
448 | +def exported_tests_to_html(tests): |
449 | + |
450 | + def cr_to_br(t): |
451 | + return t.replace('\n', '<br />') |
452 | + |
453 | + def nobr(t): |
454 | + return str(t).replace(' ', ' ') |
455 | + |
456 | + def step_field(step, key): |
457 | + if key in step: |
458 | + val = step[key] |
459 | + if isinstance(val, str): |
460 | + val = cr_to_br(val) |
461 | + return val |
462 | + else: |
463 | + return '' |
464 | + |
465 | + def fmt_steps(steps, test, tr_class): |
466 | + fmt_steps.counter += 1 |
467 | + lines = [] |
468 | + if len(steps) > 0: |
469 | + label = '%i steps' % (len(steps)) |
470 | + else: |
471 | + label = '...' |
472 | + lines.append('%s <br />' % (label)) |
473 | + hide_id = 'steps-%s' % (fmt_steps.counter) |
474 | + #lines.append('''<span class="clickable" ' |
475 | + # 'onclick="toggle_display('%s')">%s</span><br />''' |
476 | + # % (hide_id, label)) |
477 | + lines.append('</td></tr>') |
478 | + lines.append('<tr class="%s child"><td></td><td></td><td colspan="%s">' |
479 | + % (tr_class, len(columns) - 2)) |
480 | + |
481 | + lines.append('<table class="steps" id="%s"><tr><td>' % (hide_id)) |
482 | + |
483 | + for extra in ['External ID', 'Description', 'Pre-requisites and setup', |
484 | + 'Whiteboard', 'Comments']: |
485 | + val = get_field(test, extra) |
486 | + if val and (val != 'N/A'): |
487 | + lines.append('<b>%s</b>:<br />%s<br />' |
488 | + % (extra, cr_to_br(val))) |
489 | + |
490 | + if len(steps) > 0: |
491 | + lines.append('<b>Steps</b>:<br />') |
492 | + lines.append('<table>') |
493 | + lines.append('<tr>') |
494 | + for header in ('#', '', 'desc', 'result'): |
495 | + lines.append('<th align="left">%s</th>' % (header)) |
496 | + lines.append('</tr>') |
497 | + i = 0 |
498 | + for step in steps: |
499 | + i += 1 |
500 | + lines.append('<tr class="step-%s">' % (i % 2)) |
501 | + for field in ('Step position', 'Step name', |
502 | + 'Step description', 'Step expected_results'): |
503 | + lines.append('<td>%s </td>' % step_field(step, field)) |
504 | + lines.append('</tr>') |
505 | + lines.append('</table>') |
506 | + |
507 | + lines.append('</td></tr></table>') |
508 | + return '\n'.join(lines) |
509 | + fmt_steps.counter = 0 |
510 | + |
511 | + def fmt_automated(x): |
512 | + if x.lower() == 'yes': return 'automated' |
513 | + else: return 'manual' |
514 | + |
515 | + if cfg['practitest_project_id'] == '1294': |
516 | + columns = [ |
517 | + ('id', 'ID', str, False), |
518 | + ('Test Level', 'Suite', str, False), |
519 | + ('Subsystem', 'Category', nobr, False), |
520 | + ('Component', 'Subcat', str, False), |
521 | + ('Name', 'Title', str, False), |
522 | + ('Status', 'Status', str, False), |
523 | + ('Tags', 'Tags', str, False), |
524 | + ('Devices', 'Devices', str, False), |
525 | + ('Release', 'Release', str, False), |
526 | + ('Automated', 'Automated', fmt_automated, False), |
527 | + ('steps', 'Steps', fmt_steps, True), |
528 | + ] |
529 | + elif cfg['practitest_project_id'] == '1548': |
530 | + columns = [ |
531 | + ('id', 'ID', str, False), |
532 | + ('Test Level', 'Suite', str, False), |
533 | + ('Image Part', 'Part', str, False), |
534 | + ('Test Domain', 'Domain', str, False), |
535 | + ('Application', 'App', str, False), |
536 | + ('Name', 'Title', str, False), |
537 | + ('Status', 'Status', str, False), |
538 | + ('Tags', 'Tags', str, False), |
539 | + ('Devices', 'Devices', str, False), |
540 | + ('Release', 'Release', str, False), |
541 | + ('Automated', 'Automated', fmt_automated, False), |
542 | + ('steps', 'Steps', fmt_steps, True), |
543 | + ] |
544 | + |
545 | + # Sort by various fields... |
546 | + if cfg['practitest_project_id'] == '1294': |
547 | + foo = [( |
548 | + t['Test Level'] or '', |
549 | + t['Subsystem'] or '', |
550 | + t['Component'] or '', |
551 | + t['id'], t) for t in tests] |
552 | + elif cfg['practitest_project_id'] == '1548': |
553 | + foo = [( |
554 | + t['Test Level'] or '', |
555 | + t['Test Domain'] or '', |
556 | + t['Image Part'] or '', |
557 | + t['Application'] or '', |
558 | + t['id'], t) for t in tests] |
559 | + foo.sort() |
560 | + tests = [x[-1] for x in foo] |
561 | + |
562 | + lines = [] |
563 | + lines.append('<html>') |
564 | + lines.append('<head>') |
565 | + lines.append('<title>All Tests</title>') |
566 | + lines.append('<style><!--') |
567 | + lines.append(open('style.css').read()) |
568 | + lines.append('--></style>') |
569 | + lines.append('<script>') |
570 | + lines.append(open('scripts.js').read()) |
571 | + lines.append('</script>') |
572 | + lines.append('</head>') |
573 | + lines.append('<body>') |
574 | + |
575 | + lines.append('Filter:') |
576 | + lines.append('<input type="search" class="light-table-filter" ' |
577 | + 'name="filter-string" data-table="test-table" ' |
578 | + 'placeholder="search string"></input>') |
579 | + |
580 | + lines.append(' - ') |
581 | + lines.append('<span id="numtests">%i</span> tests' % (len(tests))) |
582 | + lines.append(' - ') |
583 | + lines.append('<span class="clickable" onclick="toggle_all_steps(1)">' |
584 | + 'Expand All</span>') |
585 | + lines.append(' - ') |
586 | + lines.append('<span class="clickable" onclick="toggle_all_steps(0)">' |
587 | + 'Collapse All</span>') |
588 | + |
589 | + lines.append('<hr />') |
590 | + |
591 | + lines.append('<table id="all" class="test-table table">') |
592 | + |
593 | + def table_header(): |
594 | + lines.append('<thead><tr>') |
595 | + for field, title, func, full in columns: |
596 | + lines.append('<th align="left">%s</th>' % (title)) |
597 | + lines.append('</tr></thead>') |
598 | + |
599 | + def none_to_empty(v): |
600 | + if v is None: |
601 | + return '' |
602 | + return v |
603 | + |
604 | + def get_field(test, key): |
605 | + if key in test: |
606 | + return none_to_empty(test[key]) |
607 | + return None |
608 | + |
609 | + table_header() |
610 | + lines.append('<tbody>') |
611 | + i = 0 |
612 | + for test in tests: |
613 | + i += 1 |
614 | + #if (i%25) == 1: # repeat header every 25 tests |
615 | + # table_header() |
616 | + tr_class = 'tests-%s' % (i % 2) |
617 | + hide_id = 'steps-%s' % (fmt_steps.counter + 1) |
618 | + lines.append('''<tr class="%s" onclick="toggle_display('%s')">''' |
619 | + % (tr_class, hide_id)) |
620 | + for field, title, func, full in columns: |
621 | + val = get_field(test, field) |
622 | + if full: |
623 | + val = func(val, test, tr_class) |
624 | + else: |
625 | + val = func(val) |
626 | + # the extra space here prevents a bug where |
627 | + # a search for "emul" would match "Device</td><td>Multimedia" |
628 | + lines.append('<td>{} </td>'.format(val)) |
629 | + lines.append('</tr>') |
630 | + |
631 | + lines.append('</tbody>') |
632 | + lines.append('</table>') |
633 | + |
634 | + lines.append('</body>') |
635 | + lines.append('</html>') |
636 | + |
637 | + return lines |
638 | + |
639 | + |
640 | +def read_config(path, section='DEFAULT'): |
641 | + """Get values from config.ini as a dict (env vars override file)""" |
642 | + # Read config as dict |
643 | + cfg = configparser.ConfigParser() |
644 | + cfg.read(path) |
645 | + cfg = dict(cfg[section]) |
646 | + # let env vars override config file |
647 | + for k in cfg: |
648 | + if k.upper() in os.environ: |
649 | + cfg[k] = os.environ[k.upper()] |
650 | + return cfg |
651 | + |
652 | + |
653 | +if __name__ == "__main__": |
654 | + import sys |
655 | + main(sys.argv[1:]) |
656 | |
657 | === added file 'qakit/practitest/scripts.js' |
658 | --- qakit/practitest/scripts.js 1970-01-01 00:00:00 +0000 |
659 | +++ qakit/practitest/scripts.js 2015-08-10 20:32:55 +0000 |
660 | @@ -0,0 +1,97 @@ |
661 | +function show(target) { |
662 | + document.getElementById(target).style.display = 'block'; |
663 | +} |
664 | + |
665 | +function hide(target) { |
666 | + document.getElementById(target).style.display = 'none'; |
667 | +} |
668 | + |
669 | +function toggle_display(target) { |
670 | + if (document.getElementById(target).style.display == 'block') { |
671 | + hide(target); |
672 | + } else { |
673 | + show(target); |
674 | + } |
675 | +} |
676 | + |
677 | +function toggle_all_steps(expand) { |
678 | + for (i=1; ; i++) { |
679 | + target = "steps-" + i; |
680 | + if (document.getElementById(target)) { |
681 | + if (expand == 1) { |
682 | + show(target); |
683 | + } else if (expand == 0) { |
684 | + hide(target); |
685 | + } else if (expand == -1) { |
686 | + toggle_display(target); |
687 | + } |
688 | + } else { |
689 | + break; |
690 | + } |
691 | + } |
692 | +} |
693 | + |
694 | +(function(document) { |
695 | + 'use strict'; |
696 | + |
697 | + var LightTableFilter = (function(Arr) { |
698 | + |
699 | + var _input; |
700 | + var matches = 0; |
701 | + |
702 | + function _onInputEvent(e) { |
703 | + _input = e.target; |
704 | + matches = 0; |
705 | + var tables = document.getElementsByClassName(_input.getAttribute('data-table')); |
706 | + Arr.forEach.call(tables, function(table) { |
707 | + Arr.forEach.call(table.tBodies, function(tbody) { |
708 | + // rows come in pairs; main row and collapse-able details |
709 | + var row; |
710 | + for(row=0; row<tbody.rows.length; row+=2) { |
711 | + _filter(tbody.rows[row],tbody.rows[row+1]); |
712 | + } |
713 | + }); |
714 | + }); |
715 | + document.getElementById("numtests").innerHTML = matches; |
716 | + } |
717 | + |
718 | + function _filter(row1,row2) { |
719 | + var text = row1.textContent.toLowerCase() |
720 | + + row2.textContent.toLowerCase(); |
721 | + // search for each word separately; require all words to match |
722 | + var vals = _input.value.toLowerCase().split(' '); |
723 | + var show = 1; |
724 | + var i, val; |
725 | + for(i=0; i<vals.length; i++) { |
726 | + val = vals[i]; |
727 | + if(text.indexOf(val) < 0) { show = 0; } |
728 | + } |
729 | + // count how many items will be visible |
730 | + if (show) { matches ++; } |
731 | + // hide non-matching rows |
732 | + row1.style.display = show ? 'table-row' : 'none'; |
733 | + row2.style.display = row1.style.display; |
734 | + // preserve alternating row colors |
735 | + var newclass = "tests-" + (matches%2); |
736 | + row1.setAttribute('class', newclass); |
737 | + row2.setAttribute('class', newclass); |
738 | + } |
739 | + |
740 | + return { |
741 | + init: function() { |
742 | + var inputs = document.getElementsByClassName('light-table-filter'); |
743 | + Arr.forEach.call(inputs, function(input) { |
744 | + input.oninput = _onInputEvent; |
745 | + }); |
746 | + } |
747 | + }; |
748 | + })(Array.prototype); |
749 | + |
750 | + document.addEventListener('readystatechange', function() { |
751 | + if (document.readyState === 'complete') { |
752 | + LightTableFilter.init(); |
753 | + } |
754 | + }); |
755 | + |
756 | +})(document); |
757 | + |
758 | |
759 | === added file 'qakit/practitest/style.css' |
760 | --- qakit/practitest/style.css 1970-01-01 00:00:00 +0000 |
761 | +++ qakit/practitest/style.css 2015-08-10 20:32:55 +0000 |
762 | @@ -0,0 +1,50 @@ |
763 | +table { |
764 | + border: none; |
765 | + border-collapse: collapse; |
766 | +} |
767 | + |
768 | +#all { |
769 | +} |
770 | + |
771 | +#all th { |
772 | + background-color: #ffeedd; |
773 | +} |
774 | + |
775 | +#all td { |
776 | + vertical-align: top; |
777 | + padding-right: 10px; |
778 | +} |
779 | + |
780 | +.tests-0 td { |
781 | + background-color: #eeeeee; |
782 | +} |
783 | + |
784 | +.tests-1 td { |
785 | + background-color: #ffffff; |
786 | +} |
787 | + |
788 | +.steps { |
789 | + display: none; |
790 | +} |
791 | + |
792 | +.steps th { |
793 | + background-color: #ffffff; |
794 | +} |
795 | + |
796 | +.steps, .steps td { |
797 | + border: none; |
798 | + border-bottom: none; |
799 | +} |
800 | + |
801 | +.steps .step-0 td { |
802 | + background-color: #dddddd; |
803 | +} |
804 | + |
805 | +.steps .step-1 td { |
806 | + background-color: #eeeeee; |
807 | +} |
808 | + |
809 | +.clickable { |
810 | + text-decoration: underline; |
811 | + color: #4444cc; |
812 | +} |
Probably not ready for actual merging yet, but it does function.
Mostly needs cleanup.