OILS / test / wild_report.py View on Github | oilshell.org

832 lines, 321 significant
1#!/usr/bin/env python2
2from __future__ import print_function
3"""wild_report.py."""
4
5import json
6import optparse
7import os
8import sys
9
10from vendor import jsontemplate
11
12# JSON Template Evaluation:
13#
14# - {.if}{.or} is confusing
15# I think there is even a bug with {.if}{.else}{.end} -- it accepts it but
16# doesn't do the right thing!
17# - {.if test} does work though, but it took me awhile to remember that or
18# - I forgot about {.link?} too
19# even find it in the source code. I don't like this separate predicate
20# language. Could just be PHP-ish I guess.
21# - Predicates are a little annoying.
22# - Lack of location information on undefined variables is annoying. It spews
23# a big stack trace.
24# - The styles thing seems awkward. Copied from srcbook.
25# - I don't have {total_secs|%.3f} , but the
26# LookupChain/DictRegistry/CallableRegistry thing is quite onerous.
27#
28# Good parts:
29# Just making one big dict is pretty nice.
30
31T = jsontemplate.Template
32
33
34def F(format_str):
35 # {x|commas}
36 if format_str == 'commas':
37 return lambda n: '{:,}'.format(n)
38
39 # {x|printf %.1f}
40 if format_str.startswith('printf '):
41 fmt = format_str[len('printf '):]
42 return lambda value: fmt % value
43
44 #'urlesc': urllib.quote_plus,
45 return None
46
47
48def MakeHtmlGroup(title_str, body_str):
49 """Make a group of templates that we can expand with a common style."""
50 return {
51 'TITLE': T(title_str, default_formatter='html', more_formatters=F),
52 'BODY': T(body_str, default_formatter='html', more_formatters=F),
53 'NAV': NAV_TEMPLATE,
54 }
55
56
57BODY_STYLE = jsontemplate.Template("""\
58<!DOCTYPE html>
59<html>
60 <head>
61 <meta name="viewport" content="width=device-width, initial-scale=1">
62 <title>{.template TITLE}</title>
63
64 <script type="text/javascript" src="{base_url}../../web/ajax.js"></script>
65 <script type="text/javascript" src="{base_url}../../web/table/table-sort.js"></script>
66 <link rel="stylesheet" type="text/css" href="{base_url}../../web/base.css" />
67 <link rel="stylesheet" type="text/css" href="{base_url}../../web/table/table-sort.css" />
68 <link rel="stylesheet" type="text/css" href="{base_url}../../web/wild.css" />
69 </head>
70
71 <body onload="initPage(gUrlHash, gTables, gTableStates, kStatusElem);"
72 onhashchange="onHashChange(gUrlHash, gTableStates, kStatusElem);"
73 class="width60">
74 <p id="status"></p>
75
76 <p style="text-align: right"><a href="/">oilshell.org</a></p>
77<p>
78{.template NAV}
79</p>
80
81{.template BODY}
82 </body>
83
84</html>
85""",
86 default_formatter='html')
87
88# NOTE: {.link} {.or id?} {.or} {.end} doesn't work? That is annoying.
89NAV_TEMPLATE = jsontemplate.Template("""\
90{.section nav}
91<span id="nav">
92{.repeated section @}
93 {.link?}
94 <a href="{link|htmltag}">{anchor}</a>
95 {.or}
96 {anchor}
97 {.end}
98{.alternates with}
99 /
100{.end}
101</span>
102{.end}
103""",
104 default_formatter='html')
105
106PAGE_TEMPLATES = {}
107
108# <a href="{base_url}osh-to-oil.html#{rel_path|htmltag}/{name|htmltag}">view</a>
109PAGE_TEMPLATES['FAILED'] = MakeHtmlGroup(
110 '{task}_failed', """\
111<h1>{failures|size} {task} failures</h1>
112
113{.repeated section failures}
114 <a href="{base_url}osh-to-oil.html#{rel_path|htmltag}">{rel_path|html}</a>
115 <pre>
116 {stderr}
117 </pre>
118{.end}
119""")
120
121# One is used for sort order. One is used for alignment.
122# type="string"
123# should we use the column css class as the sort order? Why not?
124
125# NOTES on columns:
126# - The col is used to COLOR the column when it's being sorted by
127# - But it can't be use to align text right. See
128# https://stackoverflow.com/questions/1238115/using-text-align-center-in-colgroup
129# - type="number" is used in table-sort.js for the sort order.
130# - We use CSS classes on individual cells like <td class="name"> to align
131# columns. That seems to be the only way to do it?
132
133PAGE_TEMPLATES['LISTING'] = MakeHtmlGroup(
134 'WILD/{rel_path} - Parsing and Translating Shell Scripts with Oil', """\
135
136{.section subtree_stats}
137<div id="summary">
138<ul>
139{.parse_failed?}
140 <li>
141 Attempted to parse <b>{num_files|commas}</b> shell scripts totalling
142 <b>{num_lines|commas}</b> lines.
143 </li>
144 {.not_shell?}
145 <li>
146 <b>{not_shell|commas}</b> files are known not to be shell.
147 {.if test top_level_links}
148 (<a href="not-shell.html">full list</a>)
149 {.end}
150 </li>
151 {.end}
152 {.not_osh?}
153 <li>
154 <b>{not_osh|commas}</b> files are known not to be OSH.
155 {.if test top_level_links}
156 (<a href="not-osh.html">full list</a>)
157 {.end}
158 </li>
159 {.end}
160 <li>
161 Failed to parse <b>{parse_failed|commas}</b> scripts, leaving
162 <b>{lines_parsed|commas}</b> lines parsed in <b>{parse_proc_secs|printf %.1f}</b>
163 seconds (<b>{lines_per_sec|printf %.1f}</b> lines/sec).
164 {.if test top_level_links}
165 (<a href="parse-failed.html">all failures</a>,
166 <a href="parse-failed.txt">text</a>)
167 {.end}
168 </li>
169{.or}
170 <li>
171 Successfully parsed <b>{num_files|commas}</b> shell scripts totalling
172 <b>{num_lines|commas}</b> lines
173 in <b>{parse_proc_secs|printf %.1f}</b> seconds
174 (<b>{lines_per_sec|printf %.1f}</b> lines/sec).
175 </li>
176{.end}
177
178<li>
179 <b>{osh2oil_failed|commas}</b> OSH-to-Oil translations failed.
180 {.if test top_level_links}
181 (<a href="osh2oil-failed.html">all failures</a>,
182 <a href="osh2oil-failed.txt">text</a>)
183 {.end}
184</li>
185</ul>
186</div>
187
188<p></p>
189{.end}
190
191
192{.section dirs}
193<table id="dirs">
194 <colgroup> <!-- for table-sort.js -->
195 <col type="number">
196 <col type="number">
197 <col type="number">
198 <col type="number">
199 <col type="number">
200 <col type="number">
201 <col type="number">
202 <col type="case-insensitive">
203 </colgroup>
204 <thead>
205 <tr>
206 <td>Files</td>
207 <td>Max Lines</td>
208 <td>Total Lines</td>
209 <!-- <td>Lines Parsed</td> -->
210 <td>Parse Failures</td>
211 <td>Max Parse Time (secs)</td>
212 <td>Total Parse Time (secs)</td>
213 <td>Translation Failures</td>
214 <td class="name">Directory</td>
215 </tr>
216 </thead>
217 <tbody>
218 {.repeated section @}
219 <tr>
220 <td>{num_files|commas}</td>
221 <td>{max_lines|commas}</td>
222 <td>{num_lines|commas}</td>
223 <!-- <td>{lines_parsed|commas}</td> -->
224 {.parse_failed?}
225 <td class="fail">{parse_failed|commas}</td>
226 {.or}
227 <td class="ok">{parse_failed|commas}</td>
228 {.end}
229 <td>{max_parse_secs|printf %.2f}</td>
230 <td>{parse_proc_secs|printf %.2f}</td>
231
232 {.osh2oil_failed?}
233 <!-- <td class="fail">{osh2oil_failed|commas}</td> -->
234 <td>{osh2oil_failed|commas}</td>
235 {.or}
236 <!-- <td class="ok">{osh2oil_failed|commas}</td> -->
237 <td>{osh2oil_failed|commas}</td>
238 {.end}
239
240 <td class="name">
241 <a href="{name|htmltag}/index.html">{name|html}/</a>
242 </td>
243 </tr>
244 {.end}
245 </tbody>
246</table>
247{.end}
248
249<p>
250</p>
251
252{.section files}
253<table id="files">
254 <colgroup> <!-- for table-sort.js -->
255 <col type="case-insensitive">
256 <col type="number">
257 <col type="case-insensitive">
258 <col type="number">
259 <col type="case-insensitive">
260 <col type="case-insensitive">
261 </colgroup>
262 <thead>
263 <tr>
264 <td>Side By Side</td>
265 <td>Lines</td>
266 <td>Parsed?</td>
267 <td>Parse Process Time (secs)</td>
268 <td>Translated?</td>
269 <td class="name">Filename</td>
270 </tr>
271 </thead>
272 <tbody>
273 {.repeated section @}
274 <tr>
275 <td>
276 <a href="{base_url}osh-to-oil.html#{rel_path|htmltag}/{name|htmltag}">view</a>
277 </td>
278 <td>{num_lines|commas}</td>
279 <td>
280 {.parse_failed?}
281 <a class="fail" href="#stderr_parse_{name}">FAIL</a>
282 <td>{parse_proc_secs}</td>
283 {.or}
284 <span class="ok">OK</a>
285 <td>{parse_proc_secs}</td>
286 {.end}
287 </td>
288
289 <td>
290 {.osh2oil_failed?}
291 <a class="fail" href="#stderr_osh2oil_{name}">FAIL</a>
292 {.or}
293 <a class="ok" href="{name}__ysh.txt">OK</a>
294 {.end}
295 </td>
296 <td class="name">
297 <a href="{name|htmltag}.txt">{name|html}</a>
298 </td>
299 </tr>
300 {.end}
301 </tbody>
302</table>
303{.end}
304
305{.if test empty}
306 <i>(empty dir)</i>
307{.end}
308
309{.section stderr}
310 <h2>stderr</h2>
311
312 <table id="stderr">
313
314 {.repeated section @}
315 <tr>
316 <td>
317 <a name="stderr_{action}_{name|htmltag}"></a>
318 {.if test parsing}
319 Parsing {name|html}
320 {.or}
321 Translating {name|html}
322 {.end}
323 </td>
324 <td>
325 <pre>
326 {contents|html}
327 </pre>
328 </td>
329 <tr/>
330 {.end}
331
332 </table>
333{.end}
334
335{.if test top_level_links}
336<a href="version-info.txt">Date and OSH version<a>
337{.end}
338
339<!-- page globals -->
340<script type="text/javascript">
341 var gUrlHash = new UrlHash(location.hash);
342 var gTableStates = {};
343 var kStatusElem = document.getElementById('status');
344
345 var gTables = [];
346 var e1 = document.getElementById('dirs');
347 var e2 = document.getElementById('files');
348
349 // If no hash, "redirect" to a state where we sort ascending by dir name and
350 // filename. TODO: These column numbers are a bit fragile.
351 var params = [];
352 if (e1) {
353 gTables.push(e1);
354 params.push('t:dirs=8a');
355 }
356 if (e2) {
357 gTables.push(e2);
358 params.push('t:files=7a');
359 }
360
361 function initPage(urlHash, gTables, tableStates, statusElem) {
362 makeTablesSortable(urlHash, gTables, tableStates);
363 /* Disable for now, this seems odd? Think about mutability of gUrlHash.
364 if (location.hash === '') {
365 document.location = '#' + params.join('&');
366 gUrlHash = new UrlHash(location.hash);
367 }
368 */
369 updateTables(urlHash, tableStates, statusElem);
370 }
371
372 function onHashChange(urlHash, tableStates, statusElem) {
373 updateTables(urlHash, tableStates, statusElem);
374 }
375</script>
376""")
377
378
379def log(msg, *args):
380 if msg:
381 msg = msg % args
382 print(msg, file=sys.stderr)
383
384
385class DirNode:
386 """Entry in the file system tree."""
387
388 def __init__(self):
389 self.files = {} # filename -> stats for success/failure, time, etc.
390 self.dirs = {} # subdir name -> DirNode object
391
392 self.subtree_stats = {} # name -> value
393
394 # show all the non-empty stderr here?
395 # __osh2oil.stderr.txt
396 # __parse.stderr.txt
397 self.stderr = []
398
399
400def UpdateNodes(node, path_parts, file_stats):
401 """Create a file node and update the stats of all its descendants in the FS
402 tree."""
403 first = path_parts[0]
404 rest = path_parts[1:]
405
406 for name, value in file_stats.iteritems():
407 # Sum numerical properties, but not strings
408 if isinstance(value, int) or isinstance(value, float):
409 if name in node.subtree_stats:
410 node.subtree_stats[name] += value
411 else:
412 # NOTE: Could be int or float!!!
413 node.subtree_stats[name] = value
414
415 # Calculate maximums
416 m = node.subtree_stats.get('max_parse_secs', 0.0)
417 node.subtree_stats['max_parse_secs'] = max(m,
418 file_stats['parse_proc_secs'])
419
420 m = node.subtree_stats.get('max_lines', 0) # integer
421 node.subtree_stats['max_lines'] = max(m, file_stats['num_lines'])
422
423 if rest: # update an intermediate node
424 if first in node.dirs:
425 child = node.dirs[first]
426 else:
427 child = DirNode()
428 node.dirs[first] = child
429
430 UpdateNodes(child, rest, file_stats)
431 else:
432 # TODO: Put these in different sections? Or least one below the other?
433
434 # Include stderr if non-empty, or if FAILED
435 parse_stderr = file_stats.pop('parse_stderr')
436 if parse_stderr or file_stats['parse_failed']:
437 node.stderr.append({
438 'parsing': True,
439 'action': 'parse',
440 'name': first,
441 'contents': parse_stderr,
442 })
443 osh2oil_stderr = file_stats.pop('osh2oil_stderr')
444
445 # TODO: Could disable this with a flag to concentrate on parse errors.
446 # Or just show parse errors all in one file.
447 if 1:
448 if osh2oil_stderr or file_stats['osh2oil_failed']:
449 node.stderr.append({
450 'parsing': False,
451 'action': 'osh2oil',
452 'name': first,
453 'contents': osh2oil_stderr,
454 })
455
456 # Attach to this dir
457 node.files[first] = file_stats
458
459
460def DebugPrint(node, indent=0):
461 """Debug print."""
462 ind = indent * ' '
463 #print('FILES', node.files.keys())
464 for name in node.files:
465 print('%s%s - %s' % (ind, name, node.files[name]))
466 for name, child in node.dirs.iteritems():
467 print('%s%s/ - %s' % (ind, name, child.subtree_stats))
468 DebugPrint(child, indent=indent + 1)
469
470
471def WriteJsonFiles(node, out_dir):
472 """Write a index.json file for every directory."""
473 path = os.path.join(out_dir, 'index.json')
474 with open(path, 'w') as f:
475 raise AssertionError # fix dir_totals
476 d = {'files': node.files, 'dirs': node.dir_totals}
477 json.dump(d, f)
478
479 log('Wrote %s', path)
480
481 for name, child in node.dirs.iteritems():
482 WriteJsonFiles(child, os.path.join(out_dir, name))
483
484
485def MakeNav(rel_path, root_name='WILD', offset=0):
486 """
487 Args:
488 offset: for doctools/src_tree.py to render files
489 """
490 assert not rel_path.startswith('/'), rel_path
491 assert not rel_path.endswith('/'), rel_path
492 # Get rid of ['']
493 parts = [root_name] + [p for p in rel_path.split('/') if p]
494 data = []
495 n = len(parts)
496 for i, p in enumerate(parts):
497 if i == n - 1:
498 link = None # Current page shouldn't have link
499 else:
500 # files need to link to .
501 link = '../' * (n - 1 - i + offset) + 'index.html'
502 data.append({'anchor': p, 'link': link})
503 return data
504
505
506def _Lower(s):
507 return s.lower()
508
509
510def WriteHtmlFiles(node, out_dir, rel_path='', base_url=''):
511 """Write a index.html file for every directory.
512
513 NOTE:
514 - osh-to-oil.html lives at $base_url
515 - table-sort.js lives at $base_url/../table-sort.js
516
517 wild/
518 table-sort.js
519 table-sort.css
520 www/
521 index.html
522 osh-to-oil.html
523
524 wild/
525 table-sort.js
526 table-sort.css
527 wild.wwz/ # Zip file
528 index.html
529 osh-to-oil.html
530
531 wwz latency is subject to caching headers.
532 """
533 files = []
534 for name in sorted(node.files, key=_Lower):
535 stats = node.files[name]
536 entry = dict(stats)
537 entry['name'] = name
538 # TODO: This should be internal time
539 entry[
540 'lines_per_sec'] = entry['lines_parsed'] / entry['parse_proc_secs']
541 files.append(entry)
542
543 dirs = []
544 for name in sorted(node.dirs, key=_Lower):
545 entry = dict(node.dirs[name].subtree_stats)
546 entry['name'] = name
547 # TODO: This should be internal time
548 entry[
549 'lines_per_sec'] = entry['lines_parsed'] / entry['parse_proc_secs']
550 dirs.append(entry)
551
552 # TODO: Is there a way to make this less redundant?
553 st = node.subtree_stats
554 try:
555 st['lines_per_sec'] = st['lines_parsed'] / st['parse_proc_secs']
556 except KeyError:
557 # This usually there were ZERO files.
558 print(node, st, repr(rel_path), file=sys.stderr)
559 raise
560
561 data = {
562 'rel_path': rel_path,
563 'subtree_stats': node.subtree_stats, # redundant totals
564 'files': files,
565 'dirs': dirs,
566 'base_url': base_url,
567 'stderr': node.stderr,
568 'nav': MakeNav(rel_path),
569 }
570 # Hack to add links for top level page:
571 if rel_path == '':
572 data['top_level_links'] = True
573
574 group = PAGE_TEMPLATES['LISTING']
575 body = BODY_STYLE.expand(data, group=group)
576
577 path = os.path.join(out_dir, 'index.html')
578 with open(path, 'w') as f:
579 f.write(body)
580
581 log('Wrote %s', path)
582
583 # Recursive
584 for name, child in node.dirs.iteritems():
585 child_out = os.path.join(out_dir, name)
586 child_rel = os.path.join(rel_path, name)
587 child_base = base_url + '../'
588 WriteHtmlFiles(child,
589 child_out,
590 rel_path=child_rel,
591 base_url=child_base)
592
593
594def _ReadTaskFile(path):
595 """Parses the a file that looks like '0 0.11', for the status code and
596 timing.
597
598 This is output by test/common.sh run-task-with-status.
599 """
600 try:
601 with open(path) as f:
602 parts = f.read().split()
603 status, secs = parts
604 except ValueError as e:
605 log('ERROR reading %s: %s', path, e)
606 raise
607 # Turn it into pass/fail
608 num_failed = 1 if int(status) >= 1 else 0
609 return num_failed, float(secs)
610
611
612def _ReadLinesToSet(path):
613 """Read blacklist files like not-shell.txt and not-osh.txt.
614
615 TODO: Consider adding globs here? There are a lot of FreeBSD and illumos
616 files we want to get rid of.
617
618 Or we could probably do that in the original 'find' expression.
619 """
620 result = set()
621 if not path:
622 return result
623
624 with open(path) as f:
625 for line in f:
626 # Allow comments. We assume filenames don't have #
627 i = line.find('#')
628 if i != -1:
629 line = line[:i]
630
631 line = line.strip()
632 if not line: # Lines that are blank or only comments.
633 continue
634
635 result.add(line)
636
637 return result
638
639
640def SumStats(stdin, in_dir, not_shell, not_osh, root_node, failures):
641 """Reads pairs of paths from stdin, and updates root_node."""
642 # Collect work into dirs
643 for line in stdin:
644 rel_path, abs_path = line.split()
645 #print proj, '-', abs_path, '-', rel_path
646
647 raw_base = os.path.join(in_dir, rel_path)
648 st = {}
649
650 st['not_shell'] = 1 if rel_path in not_shell else 0
651 st['not_osh'] = 1 if rel_path in not_osh else 0
652 if st['not_shell'] and st['not_osh']:
653 raise RuntimeError(
654 "%r can't be in both not-shell.txt and not-osh.txt" % rel_path)
655
656 expected_failure = bool(st['not_shell'] or st['not_osh'])
657
658 parse_task_path = raw_base + '__parse.task.txt'
659 parse_failed, st['parse_proc_secs'] = _ReadTaskFile(parse_task_path)
660 st['parse_failed'] = 0 if expected_failure else parse_failed
661
662 with open(raw_base + '__parse.stderr.txt') as f:
663 st['parse_stderr'] = f.read()
664
665 if st['not_shell']:
666 failures.not_shell.append({
667 'rel_path': rel_path,
668 'stderr': st['parse_stderr']
669 })
670 if st['not_osh']:
671 failures.not_osh.append({
672 'rel_path': rel_path,
673 'stderr': st['parse_stderr']
674 })
675 if st['parse_failed']:
676 failures.parse_failed.append({
677 'rel_path': rel_path,
678 'stderr': st['parse_stderr']
679 })
680
681 osh2oil_task_path = raw_base + '__ysh-ify.task.txt'
682 osh2oil_failed, st['osh2oil_proc_secs'] = _ReadTaskFile(
683 osh2oil_task_path)
684
685 # Only count translation failures if the parse succeeded!
686 st['osh2oil_failed'] = osh2oil_failed if not parse_failed else 0
687
688 with open(raw_base + '__ysh-ify.stderr.txt') as f:
689 st['osh2oil_stderr'] = f.read()
690
691 if st['osh2oil_failed']:
692 failures.osh2oil_failed.append({
693 'rel_path': rel_path,
694 'stderr': st['osh2oil_stderr']
695 })
696
697 wc_path = raw_base + '__wc.txt'
698 with open(wc_path) as f:
699 st['num_lines'] = int(f.read().split()[0])
700 # For lines per second calculation
701 st['lines_parsed'] = 0 if st['parse_failed'] else st['num_lines']
702
703 st['num_files'] = 1
704
705 path_parts = rel_path.split('/')
706 #print path_parts
707 UpdateNodes(root_node, path_parts, st)
708
709
710class Failures(object):
711 """Simple object that gets transformed to HTML and text."""
712
713 def __init__(self):
714 self.parse_failed = []
715 self.osh2oil_failed = []
716 self.not_shell = []
717 self.not_osh = []
718
719 def Write(self, out_dir):
720 with open(os.path.join(out_dir, 'parse-failed.txt'), 'w') as f:
721 for failure in self.parse_failed:
722 print(failure['rel_path'], file=f)
723
724 with open(os.path.join(out_dir, 'osh2oil-failed.txt'), 'w') as f:
725 for failure in self.osh2oil_failed:
726 print(failure['rel_path'], file=f)
727
728 base_url = ''
729
730 with open(os.path.join(out_dir, 'not-shell.html'), 'w') as f:
731 data = {
732 'task': 'not-shell',
733 'failures': self.not_shell,
734 'base_url': base_url
735 }
736 body = BODY_STYLE.expand(data, group=PAGE_TEMPLATES['FAILED'])
737 f.write(body)
738
739 with open(os.path.join(out_dir, 'not-osh.html'), 'w') as f:
740 data = {
741 'task': 'not-osh',
742 'failures': self.not_osh,
743 'base_url': base_url
744 }
745 body = BODY_STYLE.expand(data, group=PAGE_TEMPLATES['FAILED'])
746 f.write(body)
747
748 with open(os.path.join(out_dir, 'parse-failed.html'), 'w') as f:
749 data = {
750 'task': 'parse',
751 'failures': self.parse_failed,
752 'base_url': base_url
753 }
754 body = BODY_STYLE.expand(data, group=PAGE_TEMPLATES['FAILED'])
755 f.write(body)
756
757 with open(os.path.join(out_dir, 'osh2oil-failed.html'), 'w') as f:
758 data = {
759 'task': 'osh2oil',
760 'failures': self.osh2oil_failed,
761 'base_url': base_url
762 }
763 body = BODY_STYLE.expand(data, group=PAGE_TEMPLATES['FAILED'])
764 f.write(body)
765
766
767def Options():
768 """Returns an option parser instance."""
769 p = optparse.OptionParser('wild_report.py [options] ACTION...')
770 p.add_option('-v',
771 '--verbose',
772 dest='verbose',
773 action='store_true',
774 default=False,
775 help='Show details about test execution')
776 p.add_option(
777 '--not-shell',
778 default=None,
779 help="A file that contains a list of files that are known to be invalid "
780 "shell")
781 p.add_option(
782 '--not-osh',
783 default=None,
784 help="A file that contains a list of files that are known to be invalid "
785 "under the OSH language.")
786 return p
787
788
789def main(argv):
790 o = Options()
791 (opts, argv) = o.parse_args(argv)
792
793 action = argv[1]
794
795 if action == 'summarize-dirs':
796 in_dir = argv[2]
797 out_dir = argv[3]
798
799 not_shell = _ReadLinesToSet(opts.not_shell)
800 not_osh = _ReadLinesToSet(opts.not_osh)
801
802 # lines and size, oops
803
804 # TODO: Need read the manifest instead, and then go by dirname() I guess
805 # I guess it is a BFS so you can just assume?
806 # os.path.dirname() on the full path?
807 # Or maybe you need the output files?
808
809 root_node = DirNode()
810 failures = Failures()
811 SumStats(sys.stdin, in_dir, not_shell, not_osh, root_node, failures)
812
813 failures.Write(out_dir)
814
815 # Debug print
816 #DebugPrint(root_node)
817 #WriteJsonFiles(root_node, out_dir)
818
819 WriteHtmlFiles(root_node, out_dir)
820
821 else:
822 raise RuntimeError('Invalid action %r' % action)
823
824
825if __name__ == '__main__':
826 try:
827 main(sys.argv)
828 except RuntimeError as e:
829 print('FATAL: %s' % e, file=sys.stderr)
830 sys.exit(1)
831
832# vim: sw=2