Merge lp:~jimbaker/pyjuju/generate-html into lp:pyjuju/ftests
- generate-html
- Merge into ftests
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Gustavo Niemeyer | ||||
Approved revision: | 16 | ||||
Merged at revision: | 2 | ||||
Proposed branch: | lp:~jimbaker/pyjuju/generate-html | ||||
Merge into: | lp:pyjuju/ftests | ||||
Diff against target: |
499 lines (+461/-0) 7 files modified
f_tests.mustache (+24/-0) pystache/LICENSE (+20/-0) pystache/__init__.py (+8/-0) pystache/loader.py (+47/-0) pystache/template.py (+178/-0) pystache/view.py (+94/-0) waterfall.py (+90/-0) |
||||
To merge this branch: | bzr merge lp:~jimbaker/pyjuju/generate-html | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gustavo Niemeyer | Approve | ||
Review via email: mp+73924@code.launchpad.net |
Commit message
Description of the change
Implements waterfall.py, which writes to stdout an HTML summary of the results, using a waterfall style. This report links the output of failed and successful tests. This follows the review by Gustavo below. Note that the formatting is intentionally bare; it will be addressed in a future branch that adds CSS and expands f_tests.mustache in a minimal fashion.
A future stacked branch will implemented an opinionated driver that will manage working with bzr and driving churn; this driver will be called butler.py, to be in the spirit of that name.
Jim Baker (jimbaker) wrote : | # |
On Fri, Sep 2, 2011 at 6:42 PM, Gustavo Niemeyer <email address hidden>wrote:
> Review: Needs Fixing
> [1]
>
> This tool is doing three different things:
>
> 1. Running bzr update
> 2. Running churn
> 3. Converting churn output to html
>
> The tool we discussed should do 3 only.
>
> Let it take as input a directory full of churn output directories, and
> produce the output based on the directory name. So we run it like:
>
> $ butler.py watefall/ > waterfall.html
>
> Given that within the waterfall dir one might expect:
>
> /waterfall
> /341
> some-test-name.out
> other-test-
> /342
> some-test-name.out
> other-test-name.out
>
> etc. butler will then have the 341, 342, etc in the rows, and the test
> names in the columns, with OK/FAILED in the cells, as you've done.
>
Done. I believe the mechanism I have used in working with mustache templates
to generate this table is the best approach.
>
> Once this is in place, let's create a tiny but opinionated driver
> that knows how to update a branch *revision by revision* and drive
> churn and butler from outside. This driver must get as input _just_
> the build directory, and will have three things inside it.
>
> So
>
> $ whatever build/
>
> Generates
>
> build/
> ensemble/
> waterfall/
> waterfall.html
>
> But once you run
>
> $ whatever build/
>
> again, it will notice that there's already content inside it,
> and will just update revision by revision the branch between
> the latest revision in the waterfall and the current known
> tip of the trunk.
>
> With that in place, we can put this logic in a machine, and have it
> rsyncing this content out into a public web server.
>
> Do not use bzrlib, btw. Use bzr itself. We're not doing anything
> fancy, and the command line API won't change.
>
Sounds good. Given that butler is supposed to be managing the results of
everything, I renamed butler.py to waterfall.py, leaving the name butler
available for this opinionated driver. That will be done in a separate
branch.
>
> [2]
>
> Please put a copy of pystache within the tree.
>
Done. This actually involved some debugging effort because pystache, in its
interaction with MarkupSafe, was somehow behaving differently when placed
here, vs normal package install with tools like pip. Modified accordingly.
So pystache as added here just has package code + (renamed) license file.
> [3]
>
> 96 + <td>{{#failed}}<a
> href="{
> 97 + {{^failed}
>
> We have the succeeded output as well. Let's offer it so people can
> investigate differences.
>
Done
>
> It might also be better to have "ok" (lowercased) rather than "Succeeded",
> to make the
> "FAILED" one much more outstanding.
>
Done. The mustache template will need some more work re styling, but it's
functional now. This can be done in a future branch.
Gustavo Niemeyer (niemeyer) wrote : | # |
Looks great, thanks Jim. Only a couple of minor comments, and it can go in IMO.
Then, for the driver! I hope we can see a waterfall working this week still!
[4]
=== added file 'pystache-LICENSE'
Please rename this to LICENSE and put it within the pystache dir itself.
[5]
+OUTPUT_STATUS_RE = re.compile(
This regex makes things more complex than it helps. E.g.:
FAILED_SUFFIX = ".out.FAILED"
OK_SUFFIX = ".out"
(...)
if name.endswith(
test_name = name[:-
etc.
Jim Baker (jimbaker) wrote : | # |
On Thu, Sep 8, 2011 at 6:10 PM, Gustavo Niemeyer <email address hidden>wrote:
> Review: Approve
>
> Looks great, thanks Jim. Only a couple of minor comments, and it can go in
> IMO.
>
> Then, for the driver! I hope we can see a waterfall working this week
> still!
>
Please see the corresponding butler branch in review
>
> [4]
>
> === added file 'pystache-LICENSE'
>
> Please rename this to LICENSE and put it within the pystache dir itself.
>
Renamed accordingly
>
> [5]
>
> +OUTPUT_STATUS_RE =
> re.compile(
>
> This regex makes things more complex than it helps. E.g.:
>
> FAILED_SUFFIX = ".out.FAILED"
> OK_SUFFIX = ".out"
> (...)
> if name.endswith(
> test_name = name[:-
>
> etc.
>
>
Done
Gustavo Niemeyer (niemeyer) wrote : | # |
Thanks Jim, please merge it on ftests.
Preview Diff
1 | === added file 'f_tests.mustache' |
2 | --- f_tests.mustache 1970-01-01 00:00:00 +0000 |
3 | +++ f_tests.mustache 2011-09-09 22:36:28 +0000 |
4 | @@ -0,0 +1,24 @@ |
5 | +<table> |
6 | + <thead> |
7 | + <tr> |
8 | + <th>Run/Test</th> |
9 | + {{#test_names}} |
10 | + <th>{{name}}</th> |
11 | + {{/test_names}} |
12 | + </tr> |
13 | + </thead> |
14 | + <tbody> |
15 | + {{#results}} |
16 | + <tr> |
17 | + <td>{{run_name}}</td> |
18 | + {{#tests}} |
19 | + <td>{{#test_name}} |
20 | + <a href="{{path}}"> |
21 | + {{#failed}}<strong>FAILED</strong> |
22 | + {{/failed}}{{^failed}}ok{{/failed}}</a> |
23 | + {{/test_name}}</td> |
24 | + {{/tests}} |
25 | + </tr> |
26 | + {{/results}} |
27 | + </tbody> |
28 | +</table> |
29 | |
30 | === added directory 'pystache' |
31 | === added file 'pystache/LICENSE' |
32 | --- pystache/LICENSE 1970-01-01 00:00:00 +0000 |
33 | +++ pystache/LICENSE 2011-09-09 22:36:28 +0000 |
34 | @@ -0,0 +1,20 @@ |
35 | +Copyright (c) 2009 Chris Wanstrath |
36 | + |
37 | +Permission is hereby granted, free of charge, to any person obtaining |
38 | +a copy of this software and associated documentation files (the |
39 | +"Software"), to deal in the Software without restriction, including |
40 | +without limitation the rights to use, copy, modify, merge, publish, |
41 | +distribute, sublicense, and/or sell copies of the Software, and to |
42 | +permit persons to whom the Software is furnished to do so, subject to |
43 | +the following conditions: |
44 | + |
45 | +The above copyright notice and this permission notice shall be |
46 | +included in all copies or substantial portions of the Software. |
47 | + |
48 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
49 | +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
50 | +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
51 | +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
52 | +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
53 | +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
54 | +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
55 | |
56 | === added file 'pystache/__init__.py' |
57 | --- pystache/__init__.py 1970-01-01 00:00:00 +0000 |
58 | +++ pystache/__init__.py 2011-09-09 22:36:28 +0000 |
59 | @@ -0,0 +1,8 @@ |
60 | +from pystache.template import Template |
61 | +from pystache.view import View |
62 | +from pystache.loader import Loader |
63 | + |
64 | +def render(template, context=None, **kwargs): |
65 | + context = context and context.copy() or {} |
66 | + context.update(kwargs) |
67 | + return Template(template, context).render() |
68 | |
69 | === added file 'pystache/loader.py' |
70 | --- pystache/loader.py 1970-01-01 00:00:00 +0000 |
71 | +++ pystache/loader.py 2011-09-09 22:36:28 +0000 |
72 | @@ -0,0 +1,47 @@ |
73 | +import os |
74 | + |
75 | +class Loader(object): |
76 | + |
77 | + template_extension = 'mustache' |
78 | + template_path = '.' |
79 | + template_encoding = None |
80 | + |
81 | + def load_template(self, template_name, template_dirs=None, encoding=None, extension=None): |
82 | + '''Returns the template string from a file or throws IOError if it non existent''' |
83 | + if None == template_dirs: |
84 | + template_dirs = self.template_path |
85 | + |
86 | + if encoding is not None: |
87 | + self.template_encoding = encoding |
88 | + |
89 | + if extension is not None: |
90 | + self.template_extension = extension |
91 | + |
92 | + file_name = template_name + '.' + self.template_extension |
93 | + |
94 | + # Given a single directory we'll load from it |
95 | + if isinstance(template_dirs, basestring): |
96 | + file_path = os.path.join(template_dirs, file_name) |
97 | + |
98 | + return self._load_template_file(file_path) |
99 | + |
100 | + # Given a list of directories we'll check each for our file |
101 | + for path in template_dirs: |
102 | + file_path = os.path.join(path, file_name) |
103 | + if os.path.exists(file_path): |
104 | + return self._load_template_file(file_path) |
105 | + |
106 | + raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(template_dirs),)) |
107 | + |
108 | + def _load_template_file(self, file_path): |
109 | + '''Loads and returns the template file from disk''' |
110 | + f = open(file_path, 'r') |
111 | + |
112 | + try: |
113 | + template = f.read() |
114 | + if self.template_encoding: |
115 | + template = unicode(template, self.template_encoding) |
116 | + finally: |
117 | + f.close() |
118 | + |
119 | + return template |
120 | \ No newline at end of file |
121 | |
122 | === added file 'pystache/template.py' |
123 | --- pystache/template.py 1970-01-01 00:00:00 +0000 |
124 | +++ pystache/template.py 2011-09-09 22:36:28 +0000 |
125 | @@ -0,0 +1,178 @@ |
126 | +import re |
127 | +import cgi |
128 | +import collections |
129 | +import os |
130 | +import copy |
131 | + |
132 | + |
133 | +# Modified from original version (0.3.1) to not use MarkupSafe. Using |
134 | +# MarkupSafe for HTML escaping will cause the expansion of certain |
135 | +# values (lists in particular) to fail in a rather spectacular and |
136 | +# recursive fashion. Rather curiously, this behavior only occurs when |
137 | +# the code is used directly, instead of being part of an installed |
138 | +# package. |
139 | +escape = lambda x: cgi.escape(unicode(x)) |
140 | +literal = unicode |
141 | + |
142 | + |
143 | +class Modifiers(dict): |
144 | + """Dictionary with a decorator for assigning functions to keys.""" |
145 | + |
146 | + def set(self, key): |
147 | + """ |
148 | + Decorator function to set the given key to the decorated function. |
149 | + |
150 | + >>> modifiers = {} |
151 | + >>> @modifiers.set('P') |
152 | + ... def render_tongue(self, tag_name=None, context=None): |
153 | + ... return ":P %s" % tag_name |
154 | + >>> modifiers |
155 | + {'P': <function render_tongue at 0x...>} |
156 | + """ |
157 | + |
158 | + def setter(func): |
159 | + self[key] = func |
160 | + return func |
161 | + return setter |
162 | + |
163 | + |
164 | +class Template(object): |
165 | + |
166 | + tag_re = None |
167 | + |
168 | + otag = '{{' |
169 | + |
170 | + ctag = '}}' |
171 | + |
172 | + modifiers = Modifiers() |
173 | + |
174 | + def __init__(self, template=None, context=None, **kwargs): |
175 | + from view import View |
176 | + |
177 | + self.template = template |
178 | + |
179 | + if kwargs: |
180 | + context.update(kwargs) |
181 | + |
182 | + self.view = context if isinstance(context, View) else View(context=context) |
183 | + self._compile_regexps() |
184 | + |
185 | + def _compile_regexps(self): |
186 | + tags = { |
187 | + 'otag': re.escape(self.otag), |
188 | + 'ctag': re.escape(self.ctag) |
189 | + } |
190 | + |
191 | + section = r"%(otag)s[\#|^]([^\}]*)%(ctag)s\s*(.+?\s*)%(otag)s/\1%(ctag)s" |
192 | + self.section_re = re.compile(section % tags, re.M|re.S) |
193 | + |
194 | + tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" |
195 | + self.tag_re = re.compile(tag % tags) |
196 | + |
197 | + def _render_sections(self, template, view): |
198 | + while True: |
199 | + match = self.section_re.search(template) |
200 | + if match is None: |
201 | + break |
202 | + |
203 | + section, section_name, inner = match.group(0, 1, 2) |
204 | + section_name = section_name.strip() |
205 | + it = self.view.get(section_name, None) |
206 | + replacer = '' |
207 | + |
208 | + # Callable |
209 | + if it and isinstance(it, collections.Callable): |
210 | + replacer = it(inner) |
211 | + # Dictionary |
212 | + elif it and hasattr(it, 'keys') and hasattr(it, '__getitem__'): |
213 | + if section[2] != '^': |
214 | + replacer = self._render_dictionary(inner, it) |
215 | + # Lists |
216 | + elif it and hasattr(it, '__iter__'): |
217 | + if section[2] != '^': |
218 | + replacer = self._render_list(inner, it) |
219 | + # Other objects |
220 | + elif it and isinstance(it, object): |
221 | + if section[2] != '^': |
222 | + replacer = self._render_dictionary(inner, it) |
223 | + # Falsey and Negated or Truthy and Not Negated |
224 | + elif (not it and section[2] == '^') or (it and section[2] != '^'): |
225 | + replacer = self._render_dictionary(inner, it) |
226 | + |
227 | + template = literal(template.replace(section, replacer)) |
228 | + |
229 | + return template |
230 | + |
231 | + def _render_tags(self, template): |
232 | + while True: |
233 | + match = self.tag_re.search(template) |
234 | + if match is None: |
235 | + break |
236 | + |
237 | + tag, tag_type, tag_name = match.group(0, 1, 2) |
238 | + tag_name = tag_name.strip() |
239 | + func = self.modifiers[tag_type] |
240 | + replacement = func(self, tag_name) |
241 | + template = template.replace(tag, replacement) |
242 | + |
243 | + return template |
244 | + |
245 | + def _render_dictionary(self, template, context): |
246 | + self.view.context_list.insert(0, context) |
247 | + template = Template(template, self.view) |
248 | + out = template.render() |
249 | + self.view.context_list.pop(0) |
250 | + return out |
251 | + |
252 | + def _render_list(self, template, listing): |
253 | + insides = [] |
254 | + for item in listing: |
255 | + insides.append(self._render_dictionary(template, item)) |
256 | + |
257 | + return ''.join(insides) |
258 | + |
259 | + @modifiers.set(None) |
260 | + def _render_tag(self, tag_name): |
261 | + raw = self.view.get(tag_name, '') |
262 | + |
263 | + # For methods with no return value |
264 | + if not raw and raw is not 0: |
265 | + if tag_name == '.': |
266 | + raw = self.view.context_list[0] |
267 | + else: |
268 | + return '' |
269 | + |
270 | + return escape(raw) |
271 | + |
272 | + @modifiers.set('!') |
273 | + def _render_comment(self, tag_name): |
274 | + return '' |
275 | + |
276 | + @modifiers.set('>') |
277 | + def _render_partial(self, template_name): |
278 | + from pystache import Loader |
279 | + markup = Loader().load_template(template_name, self.view.template_path, encoding=self.view.template_encoding) |
280 | + template = Template(markup, self.view) |
281 | + return template.render() |
282 | + |
283 | + @modifiers.set('=') |
284 | + def _change_delimiter(self, tag_name): |
285 | + """Changes the Mustache delimiter.""" |
286 | + self.otag, self.ctag = tag_name.split(' ') |
287 | + self._compile_regexps() |
288 | + return '' |
289 | + |
290 | + @modifiers.set('{') |
291 | + @modifiers.set('&') |
292 | + def render_unescaped(self, tag_name): |
293 | + """Render a tag without escaping it.""" |
294 | + return literal(self.view.get(tag_name, '')) |
295 | + |
296 | + def render(self, encoding=None): |
297 | + template = self._render_sections(self.template, self.view) |
298 | + result = self._render_tags(template) |
299 | + |
300 | + if encoding is not None: |
301 | + result = result.encode(encoding) |
302 | + |
303 | + return result |
304 | |
305 | === added file 'pystache/view.py' |
306 | --- pystache/view.py 1970-01-01 00:00:00 +0000 |
307 | +++ pystache/view.py 2011-09-09 22:36:28 +0000 |
308 | @@ -0,0 +1,94 @@ |
309 | +from pystache import Template |
310 | +import os.path |
311 | +import re |
312 | +from types import * |
313 | + |
314 | +def get_or_attr(context_list, name, default=None): |
315 | + if not context_list: |
316 | + return default |
317 | + |
318 | + for obj in context_list: |
319 | + try: |
320 | + return obj[name] |
321 | + except KeyError: |
322 | + pass |
323 | + except: |
324 | + try: |
325 | + return getattr(obj, name) |
326 | + except AttributeError: |
327 | + pass |
328 | + return default |
329 | + |
330 | +class View(object): |
331 | + |
332 | + template_name = None |
333 | + template_path = None |
334 | + template = None |
335 | + template_encoding = None |
336 | + template_extension = 'mustache' |
337 | + |
338 | + def __init__(self, template=None, context=None, **kwargs): |
339 | + self.template = template |
340 | + context = context or {} |
341 | + context.update(**kwargs) |
342 | + |
343 | + self.context_list = [context] |
344 | + |
345 | + def get(self, attr, default=None): |
346 | + attr = get_or_attr(self.context_list, attr, getattr(self, attr, default)) |
347 | + if hasattr(attr, '__call__') and type(attr) is UnboundMethodType: |
348 | + return attr() |
349 | + else: |
350 | + return attr |
351 | + |
352 | + def get_template(self, template_name): |
353 | + if not self.template: |
354 | + from pystache import Loader |
355 | + template_name = self._get_template_name(template_name) |
356 | + self.template = Loader().load_template(template_name, self.template_path, encoding=self.template_encoding, extension=self.template_extension) |
357 | + |
358 | + return self.template |
359 | + |
360 | + def _get_template_name(self, template_name=None): |
361 | + """TemplatePartial => template_partial |
362 | + Takes a string but defaults to using the current class' name or |
363 | + the `template_name` attribute |
364 | + """ |
365 | + if template_name: |
366 | + return template_name |
367 | + |
368 | + template_name = self.__class__.__name__ |
369 | + |
370 | + def repl(match): |
371 | + return '_' + match.group(0).lower() |
372 | + |
373 | + return re.sub('[A-Z]', repl, template_name)[1:] |
374 | + |
375 | + def _get_context(self): |
376 | + context = {} |
377 | + for item in self.context_list: |
378 | + if hasattr(item, 'keys') and hasattr(item, '__getitem__'): |
379 | + context.update(item) |
380 | + return context |
381 | + |
382 | + def render(self, encoding=None): |
383 | + return Template(self.get_template(self.template_name), self).render(encoding=encoding) |
384 | + |
385 | + def __contains__(self, needle): |
386 | + return needle in self.context or hasattr(self, needle) |
387 | + |
388 | + def __getitem__(self, attr): |
389 | + val = self.get(attr, None) |
390 | + |
391 | + if not val and val is not 0: |
392 | + raise KeyError("Key '%s' does not exist in View" % attr) |
393 | + return val |
394 | + |
395 | + def __getattr__(self, attr): |
396 | + if attr == 'context': |
397 | + return self._get_context() |
398 | + |
399 | + raise AttributeError("Attribute '%s' does not exist in View" % attr) |
400 | + |
401 | + def __str__(self): |
402 | + return self.render() |
403 | \ No newline at end of file |
404 | |
405 | === added file 'pystache/waterfall.html' |
406 | === added file 'waterfall.py' |
407 | --- waterfall.py 1970-01-01 00:00:00 +0000 |
408 | +++ waterfall.py 2011-09-09 22:36:28 +0000 |
409 | @@ -0,0 +1,90 @@ |
410 | +# waterfall - Generate waterfall report from test runs |
411 | + |
412 | +import argparse |
413 | +import os |
414 | +import re |
415 | + |
416 | +from pystache import View |
417 | + |
418 | + |
419 | +FAILED_SUFFIX = ".out.FAILED" |
420 | +OK_SUFFIX = ".out" |
421 | + |
422 | + |
423 | +def main(): |
424 | + """Write waterfall summary of test results to stdout""" |
425 | + parser = argparse.ArgumentParser( |
426 | + description="Creates waterfall report from test runs") |
427 | + parser.add_argument("waterfall", |
428 | + help="Directory of test runs resulting from churn") |
429 | + args = parser.parse_args() |
430 | + waterfall(args.waterfall) |
431 | + |
432 | + |
433 | +class FTests(View): |
434 | + """Pull in template from f_tests.mustache. |
435 | + |
436 | + Uses pystache's default class name to filename mapping.""" |
437 | + |
438 | + |
439 | +def wrap_names(names): |
440 | + """Wraps names in a dict for rendering by pystache.""" |
441 | + for name in names: |
442 | + yield dict(name=name) |
443 | + |
444 | + |
445 | +def whipped(run_names, test_names, results): |
446 | + """Returns the results as a dense version suitable for rendering.""" |
447 | + for run_name in run_names: |
448 | + tests = [results.get((test_name, run_name), dict(empty=True)) |
449 | + for test_name in test_names] |
450 | + yield dict(run_name=run_name, tests=tests) |
451 | + |
452 | + |
453 | +def waterfall(waterfall_path): |
454 | + """Given a directory of churn runs, writes to stdout a waterfall report. |
455 | + |
456 | + `waterfall_path` contains a set of directories, one per run. Each |
457 | + run contains one file per test. NOTE This function assumes there |
458 | + are no non-run directories in `waterfall_path`. If that is |
459 | + relaxed, some filtering obviously needs to be done here. |
460 | + |
461 | + Other than providing an iterator of a listing of test results per |
462 | + run, via the :func:`whipped`, all display formating is done |
463 | + through `f_tests.mustache`. In particular, any desired styles |
464 | + should be setup there. |
465 | + """ |
466 | + # First, process test results, with an entry per run, test pair. |
467 | + # The resulting `results` dict is sparse |
468 | + results = {} |
469 | + test_names = set() |
470 | + run_names = sorted(os.listdir(waterfall_path)) |
471 | + for run_name in run_names: |
472 | + run_path = os.path.join(waterfall_path, run_name) |
473 | + for name in sorted(os.listdir(run_path)): |
474 | + test_name = None |
475 | + if name.endswith(FAILED_SUFFIX): |
476 | + test_name = name[:-len(FAILED_SUFFIX)] |
477 | + failed = True |
478 | + elif name.endswith(OK_SUFFIX): |
479 | + test_name = name[:-len(OK_SUFFIX)] |
480 | + failed = False |
481 | + if test_name: |
482 | + test_names.add(test_name) |
483 | + results[test_name, run_name] = { |
484 | + "run_name": run_name, |
485 | + "test_name": test_name, |
486 | + "failed": failed, |
487 | + "path": os.path.join(run_path, name)} |
488 | + |
489 | + # Next, transform `results` into an equivalent dense version |
490 | + # before passing onto pystache for rendering into HTML. |
491 | + test_names = sorted(test_names) |
492 | + wrapped_test_names = wrap_names(test_names) |
493 | + dense_results = whipped(run_names, test_names, results) |
494 | + |
495 | + print FTests(test_names=wrapped_test_names, results=dense_results).render() |
496 | + |
497 | + |
498 | +if __name__ == "__main__": |
499 | + main() |
[1]
This tool is doing three different things:
1. Running bzr update
2. Running churn
3. Converting churn output to html
The tool we discussed should do 3 only.
Let it take as input a directory full of churn output directories, and
produce the output based on the directory name. So we run it like:
$ butler.py watefall/ > waterfall.html
Given that within the waterfall dir one might expect:
/waterfall
some- test-name. out
other- test-name. out.FAILED
some- test-name. out
other- test-name. out
/341
/342
etc. butler will then have the 341, 342, etc in the rows, and the test
names in the columns, with OK/FAILED in the cells, as you've done.
Once this is in place, let's create a tiny but opinionated driver
that knows how to update a branch *revision by revision* and drive
churn and butler from outside. This driver must get as input _just_
the build directory, and will have three things inside it.
So
$ whatever build/
Generates
build/
ensemble/
waterfall/
waterfall.html
But once you run
$ whatever build/
again, it will notice that there's already content inside it,
and will just update revision by revision the branch between
the latest revision in the waterfall and the current known
tip of the trunk.
With that in place, we can put this logic in a machine, and have it
rsyncing this content out into a public web server.
Do not use bzrlib, btw. Use bzr itself. We're not doing anything
fancy, and the command line API won't change.
[2]
Please put a copy of pystache within the tree.
[3]
96 + <td>{{#failed}}<a href="{ {path}} "><strong> FAILED< /strong> </a>{{/ failed} } }Succeeded{ {/failed} }
97 + {{^failed}
We have the succeeded output as well. Let's offer it so people can
investigate differences.
It might also be better to have "ok" (lowercased) rather than "Succeeded", to make the
"FAILED" one much more outstanding.