From e2846a779d4b211e16e72eb7c01f74d48abb6cca Mon Sep 17 00:00:00 2001 From: Noah Lev Date: Wed, 24 Nov 2021 18:06:23 -0800 Subject: [PATCH 1/2] Implement `@snapshot` check for htmldocck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This form of check allows performing snapshot tests (à la `src/test/ui`) on rustdoc HTML output, making it easier to create and update tests. See this Zulip thread [1] for more information about the motivation for this change. [1]: https://zulip-archive.rust-lang.org/stream/266220-rustdoc/topic/HTML.20snapshot.20tests.html#262651142 --- src/etc/htmldocck.py | 80 ++++++++++++++++++++++++++-- src/tools/compiletest/src/runtest.rs | 12 ++--- 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/src/etc/htmldocck.py b/src/etc/htmldocck.py index 8647db5a45d..48a341ffe08 100644 --- a/src/etc/htmldocck.py +++ b/src/etc/htmldocck.py @@ -90,10 +90,20 @@ There are a number of supported commands: highlights for example. If you want to simply check for the presence of a given node or attribute, use an empty string (`""`) as a `PATTERN`. -* `@count PATH XPATH COUNT' checks for the occurrence of the given XPath +* `@count PATH XPATH COUNT` checks for the occurrence of the given XPath in the specified file. The number of occurrences must match the given count. +* `@snapshot NAME PATH XPATH` creates a snapshot test named NAME. + A snapshot test captures a subtree of the DOM, at the location + determined by the XPath, and compares it to a pre-recorded value + in a file. The file's name is the test's name with the `.rs` extension + replaced with `.NAME.html`, where NAME is the snapshot's name. + + htmldocck supports the `--bless` option to accept the current subtree + as expected, saving it to the file determined by the snapshot's name. + compiletest's `--bless` flag is forwarded to htmldocck. + * `@has-dir PATH` checks for the existence of the given directory. All conditions can be negated with `!`. `@!has foo/type.NoSuch.html` @@ -137,6 +147,10 @@ except NameError: channel = os.environ["DOC_RUST_LANG_ORG_CHANNEL"] +# Initialized in main +rust_test_path = None +bless = None + class CustomHTMLParser(HTMLParser): """simplified HTML parser. @@ -387,6 +401,32 @@ def get_tree_count(tree, path): return len(tree.findall(path)) +def check_snapshot(snapshot_name, tree): + assert rust_test_path.endswith('.rs') + snapshot_path = '{}.{}.{}'.format(rust_test_path[:-3], snapshot_name, 'html') + try: + with open(snapshot_path, 'r') as snapshot_file: + expected_str = snapshot_file.read() + except FileNotFoundError: + if bless: + expected_str = None + else: + raise FailedCheck('No saved snapshot value') + + actual_str = ET.tostring(tree).decode('utf-8') + + if expected_str != actual_str: + if bless: + with open(snapshot_path, 'w') as snapshot_file: + snapshot_file.write(actual_str) + else: + print('--- expected ---\n') + print(expected_str) + print('\n\n--- actual ---\n') + print(actual_str) + print() + raise FailedCheck('Actual snapshot value is different than expected') + def stderr(*args): if sys.version_info.major < 3: file = codecs.getwriter('utf-8')(sys.stderr) @@ -448,6 +488,28 @@ def check_command(c, cache): ret = expected == found else: raise InvalidCheck('Invalid number of @{} arguments'.format(c.cmd)) + + elif c.cmd == 'snapshot': # snapshot test + if len(c.args) == 3: # @snapshot + [snapshot_name, html_path, pattern] = c.args + tree = cache.get_tree(html_path) + xpath = normalize_xpath(pattern) + subtrees = tree.findall(xpath) + if len(subtrees) == 1: + [subtree] = subtrees + try: + check_snapshot(snapshot_name, subtree) + ret = True + except FailedCheck as err: + cerr = str(err) + ret = False + elif len(subtrees) == 0: + raise FailedCheck('XPATH did not match') + else: + raise FailedCheck('Expected 1 match, but found {}'.format(len(subtrees))) + else: + raise InvalidCheck('Invalid number of @{} arguments'.format(c.cmd)) + elif c.cmd == 'has-dir': # has-dir test if len(c.args) == 1: # @has-dir = has-dir test try: @@ -458,11 +520,13 @@ def check_command(c, cache): ret = False else: raise InvalidCheck('Invalid number of @{} arguments'.format(c.cmd)) + elif c.cmd == 'valid-html': raise InvalidCheck('Unimplemented @valid-html') elif c.cmd == 'valid-links': raise InvalidCheck('Unimplemented @valid-links') + else: raise InvalidCheck('Unrecognized @{}'.format(c.cmd)) @@ -483,11 +547,19 @@ def check(target, commands): if __name__ == '__main__': - if len(sys.argv) != 3: - stderr('Usage: {}