]> git.proxmox.com Git - rustc.git/blame - src/etc/htmldocck.py
New upstream version 1.62.1+dfsg1
[rustc.git] / src / etc / htmldocck.py
CommitLineData
0731742a
XL
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
85aaf69f
SL
3
4r"""
5htmldocck.py is a custom checker script for Rustdoc HTML outputs.
6
7# How and why?
8
9The principle is simple: This script receives a path to generated HTML
10documentation and a "template" script, which has a series of check
0bf4aa26 11commands like `@has` or `@matches`. Each command is used to check if
85aaf69f 12some pattern is present or not present in the particular file or in
0bf4aa26
XL
13a particular node of the HTML tree. In many cases, the template script
14happens to be the source code given to rustdoc.
85aaf69f
SL
15
16While it indeed is possible to test in smaller portions, it has been
17hard to construct tests in this fashion and major rendering errors were
0bf4aa26
XL
18discovered much later. This script is designed to make black-box and
19regression testing of Rustdoc easy. This does not preclude the needs for
20unit testing, but can be used to complement related tests by quickly
85aaf69f
SL
21showing the expected renderings.
22
23In order to avoid one-off dependencies for this task, this script uses
24a reasonably working HTML parser and the existing XPath implementation
0bf4aa26 25from Python's standard library. Hopefully, we won't render
85aaf69f
SL
26non-well-formed HTML.
27
28# Commands
29
30Commands start with an `@` followed by a command name (letters and
31hyphens), and zero or more arguments separated by one or more whitespace
0bf4aa26
XL
32characters and optionally delimited with single or double quotes. The `@`
33mark cannot be preceded by a non-whitespace character. Other lines
34(including every text up to the first `@`) are ignored, but it is
35recommended to avoid the use of `@` in the template file.
85aaf69f
SL
36
37There are a number of supported commands:
38
0bf4aa26 39* `@has PATH` checks for the existence of the given file.
85aaf69f
SL
40
41 `PATH` is relative to the output directory. It can be given as `-`
42 which repeats the most recently used `PATH`.
43
44* `@has PATH PATTERN` and `@matches PATH PATTERN` checks for
0bf4aa26
XL
45 the occurrence of the given pattern `PATTERN` in the specified file.
46 Only one occurrence of the pattern is enough.
85aaf69f
SL
47
48 For `@has`, `PATTERN` is a whitespace-normalized (every consecutive
49 whitespace being replaced by one single space character) string.
50 The entire file is also whitespace-normalized including newlines.
51
52 For `@matches`, `PATTERN` is a Python-supported regular expression.
0bf4aa26
XL
53 The file remains intact but the regexp is matched without the `MULTILINE`
54 and `IGNORECASE` options. You can still use a prefix `(?m)` or `(?i)`
85aaf69f
SL
55 to override them, and `\A` and `\Z` for definitely matching
56 the beginning and end of the file.
57
58 (The same distinction goes to other variants of these commands.)
59
60* `@has PATH XPATH PATTERN` and `@matches PATH XPATH PATTERN` checks for
0bf4aa26
XL
61 the presence of the given XPath `XPATH` in the specified HTML file,
62 and also the occurrence of the given pattern `PATTERN` in the matching
63 node or attribute. Only one occurrence of the pattern in the match
64 is enough.
85aaf69f
SL
65
66 `PATH` should be a valid and well-formed HTML file. It does *not*
67 accept arbitrary HTML5; it should have matching open and close tags
68 and correct entity references at least.
69
0bf4aa26 70 `XPATH` is an XPath expression to match. The XPath is fairly limited:
85aaf69f
SL
71 `tag`, `*`, `.`, `//`, `..`, `[@attr]`, `[@attr='value']`, `[tag]`,
72 `[POS]` (element located in given `POS`), `[last()-POS]`, `text()`
73 and `@attr` (both as the last segment) are supported. Some examples:
74
75 - `//pre` or `.//pre` matches any element with a name `pre`.
76 - `//a[@href]` matches any element with an `href` attribute.
77 - `//*[@class="impl"]//code` matches any element with a name `code`,
78 which is an ancestor of some element which `class` attr is `impl`.
79 - `//h1[@class="fqn"]/span[1]/a[last()]/@class` matches a value of
80 `class` attribute in the last `a` element (can be followed by more
81 elements that are not `a`) inside the first `span` in the `h1` with
0bf4aa26 82 a class of `fqn`. Note that there cannot be any additional elements
85aaf69f
SL
83 between them due to the use of `/` instead of `//`.
84
85 Do not try to use non-absolute paths, it won't work due to the flawed
86 ElementTree implementation. The script rejects them.
87
88 For the text matches (i.e. paths not ending with `@attr`), any
89 subelements are flattened into one string; this is handy for ignoring
0bf4aa26
XL
90 highlights for example. If you want to simply check for the presence of
91 a given node or attribute, use an empty string (`""`) as a `PATTERN`.
85aaf69f 92
a2a8927a 93* `@count PATH XPATH COUNT` checks for the occurrence of the given XPath
0bf4aa26
XL
94 in the specified file. The number of occurrences must match the given
95 count.
c34b1796 96
a2a8927a
XL
97* `@snapshot NAME PATH XPATH` creates a snapshot test named NAME.
98 A snapshot test captures a subtree of the DOM, at the location
99 determined by the XPath, and compares it to a pre-recorded value
100 in a file. The file's name is the test's name with the `.rs` extension
101 replaced with `.NAME.html`, where NAME is the snapshot's name.
102
103 htmldocck supports the `--bless` option to accept the current subtree
104 as expected, saving it to the file determined by the snapshot's name.
105 compiletest's `--bless` flag is forwarded to htmldocck.
106
ea8adc8c
XL
107* `@has-dir PATH` checks for the existence of the given directory.
108
85aaf69f
SL
109All conditions can be negated with `!`. `@!has foo/type.NoSuch.html`
110checks if the given file does not exist, for example.
111
112"""
113
0731742a
XL
114from __future__ import absolute_import, print_function, unicode_literals
115
116import codecs
117import io
85aaf69f
SL
118import sys
119import os.path
120import re
121import shlex
122from collections import namedtuple
3b2f2976
XL
123try:
124 from html.parser import HTMLParser
125except ImportError:
126 from HTMLParser import HTMLParser
f035d41b
XL
127try:
128 from xml.etree import cElementTree as ET
129except ImportError:
130 from xml.etree import ElementTree as ET
85aaf69f 131
3b2f2976 132try:
0731742a 133 from html.entities import name2codepoint
3b2f2976 134except ImportError:
0731742a 135 from htmlentitydefs import name2codepoint
85aaf69f
SL
136
137# "void elements" (no closing tag) from the HTML Standard section 12.1.2
3dfed10e
XL
138VOID_ELEMENTS = {'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
139 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'}
85aaf69f 140
3b2f2976
XL
141# Python 2 -> 3 compatibility
142try:
143 unichr
144except NameError:
145 unichr = chr
85aaf69f 146
74b04a01 147
17df50a5
XL
148channel = os.environ["DOC_RUST_LANG_ORG_CHANNEL"]
149
a2a8927a
XL
150# Initialized in main
151rust_test_path = None
152bless = None
153
85aaf69f
SL
154class CustomHTMLParser(HTMLParser):
155 """simplified HTML parser.
156
157 this is possible because we are dealing with very regular HTML from
158 rustdoc; we only have to deal with i) void elements and ii) empty
159 attributes."""
160 def __init__(self, target=None):
161 HTMLParser.__init__(self)
162 self.__builder = target or ET.TreeBuilder()
163
164 def handle_starttag(self, tag, attrs):
3dfed10e 165 attrs = {k: v or '' for k, v in attrs}
85aaf69f
SL
166 self.__builder.start(tag, attrs)
167 if tag in VOID_ELEMENTS:
168 self.__builder.end(tag)
169
170 def handle_endtag(self, tag):
171 self.__builder.end(tag)
172
173 def handle_startendtag(self, tag, attrs):
3dfed10e 174 attrs = {k: v or '' for k, v in attrs}
85aaf69f
SL
175 self.__builder.start(tag, attrs)
176 self.__builder.end(tag)
177
178 def handle_data(self, data):
179 self.__builder.data(data)
180
181 def handle_entityref(self, name):
0731742a 182 self.__builder.data(unichr(name2codepoint[name]))
85aaf69f
SL
183
184 def handle_charref(self, name):
185 code = int(name[1:], 16) if name.startswith(('x', 'X')) else int(name, 10)
0731742a 186 self.__builder.data(unichr(code))
85aaf69f
SL
187
188 def close(self):
189 HTMLParser.close(self)
190 return self.__builder.close()
191
74b04a01 192
9cc50fc6 193Command = namedtuple('Command', 'negated cmd args lineno context')
85aaf69f 194
74b04a01 195
9cc50fc6
SL
196class FailedCheck(Exception):
197 pass
198
74b04a01 199
9cc50fc6
SL
200class InvalidCheck(Exception):
201 pass
85aaf69f 202
74b04a01 203
85aaf69f
SL
204def concat_multi_lines(f):
205 """returns a generator out of the file object, which
206 - removes `\\` then `\n` then a shared prefix with the previous line then
207 optional whitespace;
208 - keeps a line number (starting from 0) of the first line being
209 concatenated."""
74b04a01 210 lastline = None # set to the last line when the last line has a backslash
85aaf69f
SL
211 firstlineno = None
212 catenated = ''
213 for lineno, line in enumerate(f):
214 line = line.rstrip('\r\n')
215
216 # strip the common prefix from the current line if needed
217 if lastline is not None:
3b2f2976
XL
218 common_prefix = os.path.commonprefix([line, lastline])
219 line = line[len(common_prefix):].lstrip()
85aaf69f
SL
220
221 firstlineno = firstlineno or lineno
222 if line.endswith('\\'):
9346a6ac
AL
223 if lastline is None:
224 lastline = line[:-1]
85aaf69f
SL
225 catenated += line[:-1]
226 else:
227 yield firstlineno, catenated + line
228 lastline = None
229 firstlineno = None
230 catenated = ''
231
232 if lastline is not None:
9cc50fc6 233 print_err(lineno, line, 'Trailing backslash at the end of the file')
85aaf69f 234
74b04a01 235
85aaf69f 236LINE_PATTERN = re.compile(r'''
5869c6ff 237 (?<=(?<!\S))(?P<invalid>!?)@(?P<negated>!?)
85aaf69f
SL
238 (?P<cmd>[A-Za-z]+(?:-[A-Za-z]+)*)
239 (?P<args>.*)$
0731742a 240''', re.X | re.UNICODE)
85aaf69f
SL
241
242
243def get_commands(template):
0731742a 244 with io.open(template, encoding='utf-8') as f:
85aaf69f
SL
245 for lineno, line in concat_multi_lines(f):
246 m = LINE_PATTERN.search(line)
247 if not m:
248 continue
249
250 negated = (m.group('negated') == '!')
251 cmd = m.group('cmd')
5869c6ff
XL
252 if m.group('invalid') == '!':
253 print_err(
254 lineno,
255 line,
256 'Invalid command: `!@{0}{1}`, (help: try with `@!{1}`)'.format(
257 '!' if negated else '',
258 cmd,
259 ),
260 )
261 continue
85aaf69f
SL
262 args = m.group('args')
263 if args and not args[:1].isspace():
9cc50fc6
SL
264 print_err(lineno, line, 'Invalid template syntax')
265 continue
0731742a
XL
266 try:
267 args = shlex.split(args)
268 except UnicodeEncodeError:
269 args = [arg.decode('utf-8') for arg in shlex.split(args.encode('utf-8'))]
9cc50fc6 270 yield Command(negated=negated, cmd=cmd, args=args, lineno=lineno+1, context=line)
85aaf69f
SL
271
272
273def _flatten(node, acc):
274 if node.text:
275 acc.append(node.text)
276 for e in node:
277 _flatten(e, acc)
278 if e.tail:
279 acc.append(e.tail)
280
281
282def flatten(node):
283 acc = []
284 _flatten(node, acc)
285 return ''.join(acc)
286
287
04454e1e
FG
288def make_xml(text):
289 xml = ET.XML('<xml>%s</xml>' % text)
290 return xml
291
292
85aaf69f 293def normalize_xpath(path):
17df50a5 294 path = path.replace("{{channel}}", channel)
85aaf69f 295 if path.startswith('//'):
74b04a01 296 return '.' + path # avoid warnings
85aaf69f
SL
297 elif path.startswith('.//'):
298 return path
299 else:
9cc50fc6 300 raise InvalidCheck('Non-absolute XPath is not supported due to implementation issues')
85aaf69f
SL
301
302
303class CachedFiles(object):
304 def __init__(self, root):
305 self.root = root
306 self.files = {}
307 self.trees = {}
308 self.last_path = None
309
310 def resolve_path(self, path):
311 if path != '-':
312 path = os.path.normpath(path)
313 self.last_path = path
314 return path
315 elif self.last_path is None:
9cc50fc6 316 raise InvalidCheck('Tried to use the previous path in the first command')
85aaf69f
SL
317 else:
318 return self.last_path
319
320 def get_file(self, path):
321 path = self.resolve_path(path)
9cc50fc6 322 if path in self.files:
85aaf69f 323 return self.files[path]
9cc50fc6
SL
324
325 abspath = os.path.join(self.root, path)
326 if not(os.path.exists(abspath) and os.path.isfile(abspath)):
327 raise FailedCheck('File does not exist {!r}'.format(path))
328
0731742a 329 with io.open(abspath, encoding='utf-8') as f:
9cc50fc6
SL
330 data = f.read()
331 self.files[path] = data
332 return data
85aaf69f
SL
333
334 def get_tree(self, path):
335 path = self.resolve_path(path)
9cc50fc6 336 if path in self.trees:
85aaf69f 337 return self.trees[path]
9cc50fc6
SL
338
339 abspath = os.path.join(self.root, path)
340 if not(os.path.exists(abspath) and os.path.isfile(abspath)):
341 raise FailedCheck('File does not exist {!r}'.format(path))
342
0731742a 343 with io.open(abspath, encoding='utf-8') as f:
85aaf69f 344 try:
0731742a 345 tree = ET.fromstringlist(f.readlines(), CustomHTMLParser())
85aaf69f
SL
346 except Exception as e:
347 raise RuntimeError('Cannot parse an HTML file {!r}: {}'.format(path, e))
9cc50fc6
SL
348 self.trees[path] = tree
349 return self.trees[path]
85aaf69f 350
ea8adc8c
XL
351 def get_dir(self, path):
352 path = self.resolve_path(path)
353 abspath = os.path.join(self.root, path)
354 if not(os.path.exists(abspath) and os.path.isdir(abspath)):
355 raise FailedCheck('Directory does not exist {!r}'.format(path))
356
85aaf69f
SL
357
358def check_string(data, pat, regexp):
17df50a5 359 pat = pat.replace("{{channel}}", channel)
85aaf69f 360 if not pat:
74b04a01 361 return True # special case a presence testing
85aaf69f 362 elif regexp:
0731742a 363 return re.search(pat, data, flags=re.UNICODE) is not None
85aaf69f
SL
364 else:
365 data = ' '.join(data.split())
366 pat = ' '.join(pat.split())
367 return pat in data
368
369
370def check_tree_attr(tree, path, attr, pat, regexp):
371 path = normalize_xpath(path)
372 ret = False
373 for e in tree.findall(path):
9cc50fc6 374 if attr in e.attrib:
85aaf69f 375 value = e.attrib[attr]
85aaf69f 376 else:
9cc50fc6
SL
377 continue
378
379 ret = check_string(value, pat, regexp)
380 if ret:
381 break
85aaf69f
SL
382 return ret
383
384
385def check_tree_text(tree, path, pat, regexp):
386 path = normalize_xpath(path)
387 ret = False
94b46f34
XL
388 try:
389 for e in tree.findall(path):
390 try:
391 value = flatten(e)
392 except KeyError:
393 continue
394 else:
395 ret = check_string(value, pat, regexp)
396 if ret:
397 break
74b04a01 398 except Exception:
94b46f34 399 print('Failed to get path "{}"'.format(path))
0731742a 400 raise
85aaf69f
SL
401 return ret
402
403
a7813a04 404def get_tree_count(tree, path):
c34b1796 405 path = normalize_xpath(path)
a7813a04 406 return len(tree.findall(path))
c34b1796 407
74b04a01 408
04454e1e 409def check_snapshot(snapshot_name, actual_tree, normalize_to_text):
a2a8927a
XL
410 assert rust_test_path.endswith('.rs')
411 snapshot_path = '{}.{}.{}'.format(rust_test_path[:-3], snapshot_name, 'html')
412 try:
413 with open(snapshot_path, 'r') as snapshot_file:
414 expected_str = snapshot_file.read()
415 except FileNotFoundError:
416 if bless:
417 expected_str = None
418 else:
419 raise FailedCheck('No saved snapshot value')
420
5099ac24 421 if not normalize_to_text:
04454e1e 422 actual_str = ET.tostring(actual_tree).decode('utf-8')
5099ac24 423 else:
04454e1e
FG
424 actual_str = flatten(actual_tree)
425
426 # Conditions:
427 # 1. Is --bless
428 # 2. Are actual and expected tree different
429 # 3. Are actual and expected text different
430 if not expected_str \
431 or (not normalize_to_text and \
432 not compare_tree(make_xml(actual_str), make_xml(expected_str), stderr)) \
433 or (normalize_to_text and actual_str != expected_str):
a2a8927a 434
a2a8927a
XL
435 if bless:
436 with open(snapshot_path, 'w') as snapshot_file:
437 snapshot_file.write(actual_str)
438 else:
439 print('--- expected ---\n')
440 print(expected_str)
441 print('\n\n--- actual ---\n')
442 print(actual_str)
443 print()
444 raise FailedCheck('Actual snapshot value is different than expected')
445
04454e1e
FG
446
447# Adapted from https://github.com/formencode/formencode/blob/3a1ba9de2fdd494dd945510a4568a3afeddb0b2e/formencode/doctest_xml_compare.py#L72-L120
448def compare_tree(x1, x2, reporter=None):
449 if x1.tag != x2.tag:
450 if reporter:
451 reporter('Tags do not match: %s and %s' % (x1.tag, x2.tag))
452 return False
453 for name, value in x1.attrib.items():
454 if x2.attrib.get(name) != value:
455 if reporter:
456 reporter('Attributes do not match: %s=%r, %s=%r'
457 % (name, value, name, x2.attrib.get(name)))
458 return False
459 for name in x2.attrib:
460 if name not in x1.attrib:
461 if reporter:
462 reporter('x2 has an attribute x1 is missing: %s'
463 % name)
464 return False
465 if not text_compare(x1.text, x2.text):
466 if reporter:
467 reporter('text: %r != %r' % (x1.text, x2.text))
468 return False
469 if not text_compare(x1.tail, x2.tail):
470 if reporter:
471 reporter('tail: %r != %r' % (x1.tail, x2.tail))
472 return False
473 cl1 = list(x1)
474 cl2 = list(x2)
475 if len(cl1) != len(cl2):
476 if reporter:
477 reporter('children length differs, %i != %i'
478 % (len(cl1), len(cl2)))
479 return False
480 i = 0
481 for c1, c2 in zip(cl1, cl2):
482 i += 1
483 if not compare_tree(c1, c2, reporter=reporter):
484 if reporter:
485 reporter('children %i do not match: %s'
486 % (i, c1.tag))
487 return False
488 return True
489
490
491def text_compare(t1, t2):
492 if not t1 and not t2:
493 return True
494 if t1 == '*' or t2 == '*':
495 return True
496 return (t1 or '').strip() == (t2 or '').strip()
497
498
9cc50fc6 499def stderr(*args):
0731742a
XL
500 if sys.version_info.major < 3:
501 file = codecs.getwriter('utf-8')(sys.stderr)
502 else:
503 file = sys.stderr
504
505 print(*args, file=file)
c34b1796 506
74b04a01 507
9cc50fc6
SL
508def print_err(lineno, context, err, message=None):
509 global ERR_COUNT
510 ERR_COUNT += 1
511 stderr("{}: {}".format(lineno, message or err))
512 if message and err:
513 stderr("\t{}".format(err))
514
515 if context:
516 stderr("\t{}".format(context))
517
74b04a01 518
9cc50fc6
SL
519ERR_COUNT = 0
520
74b04a01 521
9cc50fc6
SL
522def check_command(c, cache):
523 try:
524 cerr = ""
74b04a01 525 if c.cmd == 'has' or c.cmd == 'matches': # string test
85aaf69f 526 regexp = (c.cmd == 'matches')
74b04a01 527 if len(c.args) == 1 and not regexp: # @has <path> = file existence
85aaf69f
SL
528 try:
529 cache.get_file(c.args[0])
530 ret = True
9cc50fc6 531 except FailedCheck as err:
3b2f2976 532 cerr = str(err)
85aaf69f 533 ret = False
74b04a01 534 elif len(c.args) == 2: # @has/matches <path> <pat> = string test
9cc50fc6 535 cerr = "`PATTERN` did not match"
85aaf69f 536 ret = check_string(cache.get_file(c.args[0]), c.args[1], regexp)
74b04a01 537 elif len(c.args) == 3: # @has/matches <path> <pat> <match> = XML tree test
9cc50fc6 538 cerr = "`XPATH PATTERN` did not match"
85aaf69f
SL
539 tree = cache.get_tree(c.args[0])
540 pat, sep, attr = c.args[1].partition('/@')
74b04a01 541 if sep: # attribute
9cc50fc6
SL
542 tree = cache.get_tree(c.args[0])
543 ret = check_tree_attr(tree, pat, attr, c.args[2], regexp)
74b04a01 544 else: # normalized text
85aaf69f
SL
545 pat = c.args[1]
546 if pat.endswith('/text()'):
547 pat = pat[:-7]
548 ret = check_tree_text(cache.get_tree(c.args[0]), pat, c.args[2], regexp)
549 else:
9cc50fc6 550 raise InvalidCheck('Invalid number of @{} arguments'.format(c.cmd))
85aaf69f 551
74b04a01
XL
552 elif c.cmd == 'count': # count test
553 if len(c.args) == 3: # @count <path> <pat> <count> = count test
a7813a04
XL
554 expected = int(c.args[2])
555 found = get_tree_count(cache.get_tree(c.args[0]), c.args[1])
556 cerr = "Expected {} occurrences but found {}".format(expected, found)
557 ret = expected == found
c34b1796 558 else:
9cc50fc6 559 raise InvalidCheck('Invalid number of @{} arguments'.format(c.cmd))
a2a8927a
XL
560
561 elif c.cmd == 'snapshot': # snapshot test
562 if len(c.args) == 3: # @snapshot <snapshot-name> <html-path> <xpath>
563 [snapshot_name, html_path, pattern] = c.args
564 tree = cache.get_tree(html_path)
565 xpath = normalize_xpath(pattern)
5099ac24
FG
566 normalize_to_text = False
567 if xpath.endswith('/text()'):
568 xpath = xpath[:-7]
569 normalize_to_text = True
570
a2a8927a
XL
571 subtrees = tree.findall(xpath)
572 if len(subtrees) == 1:
573 [subtree] = subtrees
574 try:
5099ac24 575 check_snapshot(snapshot_name, subtree, normalize_to_text)
a2a8927a
XL
576 ret = True
577 except FailedCheck as err:
578 cerr = str(err)
579 ret = False
580 elif len(subtrees) == 0:
581 raise FailedCheck('XPATH did not match')
582 else:
583 raise FailedCheck('Expected 1 match, but found {}'.format(len(subtrees)))
584 else:
585 raise InvalidCheck('Invalid number of @{} arguments'.format(c.cmd))
586
74b04a01
XL
587 elif c.cmd == 'has-dir': # has-dir test
588 if len(c.args) == 1: # @has-dir <path> = has-dir test
ea8adc8c
XL
589 try:
590 cache.get_dir(c.args[0])
591 ret = True
592 except FailedCheck as err:
593 cerr = str(err)
594 ret = False
595 else:
596 raise InvalidCheck('Invalid number of @{} arguments'.format(c.cmd))
a2a8927a 597
85aaf69f 598 elif c.cmd == 'valid-html':
9cc50fc6 599 raise InvalidCheck('Unimplemented @valid-html')
85aaf69f
SL
600
601 elif c.cmd == 'valid-links':
9cc50fc6 602 raise InvalidCheck('Unimplemented @valid-links')
a2a8927a 603
85aaf69f 604 else:
9cc50fc6 605 raise InvalidCheck('Unrecognized @{}'.format(c.cmd))
85aaf69f
SL
606
607 if ret == c.negated:
9cc50fc6
SL
608 raise FailedCheck(cerr)
609
610 except FailedCheck as err:
611 message = '@{}{} check failed'.format('!' if c.negated else '', c.cmd)
3b2f2976 612 print_err(c.lineno, c.context, str(err), message)
9cc50fc6 613 except InvalidCheck as err:
3b2f2976 614 print_err(c.lineno, c.context, str(err))
9cc50fc6 615
74b04a01 616
9cc50fc6
SL
617def check(target, commands):
618 cache = CachedFiles(target)
619 for c in commands:
620 check_command(c, cache)
85aaf69f 621
74b04a01 622
85aaf69f 623if __name__ == '__main__':
a2a8927a
XL
624 if len(sys.argv) not in [3, 4]:
625 stderr('Usage: {} <doc dir> <template> [--bless]'.format(sys.argv[0]))
9cc50fc6
SL
626 raise SystemExit(1)
627
a2a8927a
XL
628 rust_test_path = sys.argv[2]
629 if len(sys.argv) > 3 and sys.argv[3] == '--bless':
630 bless = True
631 else:
632 # We only support `--bless` at the end of the arguments.
633 # This assert is to prevent silent failures.
634 assert '--bless' not in sys.argv
635 bless = False
636 check(sys.argv[1], get_commands(rust_test_path))
9cc50fc6
SL
637 if ERR_COUNT:
638 stderr("\nEncountered {} errors".format(ERR_COUNT))
85aaf69f 639 raise SystemExit(1)