Rollup merge of #91209 - camelid:snapshot, r=jyn514
Implement `@snapshot` check for htmldocck 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 r? `@GuillaumeGomez`
This commit is contained in:
commit
420ddd0b7e
@ -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> <xpath>
|
||||
[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 <path> = 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: {} <doc dir> <template>'.format(sys.argv[0]))
|
||||
if len(sys.argv) not in [3, 4]:
|
||||
stderr('Usage: {} <doc dir> <template> [--bless]'.format(sys.argv[0]))
|
||||
raise SystemExit(1)
|
||||
|
||||
check(sys.argv[1], get_commands(sys.argv[2]))
|
||||
rust_test_path = sys.argv[2]
|
||||
if len(sys.argv) > 3 and sys.argv[3] == '--bless':
|
||||
bless = True
|
||||
else:
|
||||
# We only support `--bless` at the end of the arguments.
|
||||
# This assert is to prevent silent failures.
|
||||
assert '--bless' not in sys.argv
|
||||
bless = False
|
||||
check(sys.argv[1], get_commands(rust_test_path))
|
||||
if ERR_COUNT:
|
||||
stderr("\nEncountered {} errors".format(ERR_COUNT))
|
||||
raise SystemExit(1)
|
||||
|
@ -0,0 +1,4 @@
|
||||
<div class="docblock"><p>Hello world!
|
||||
Goodbye!
|
||||
Hello again!</p>
|
||||
</div>
|
@ -0,0 +1,4 @@
|
||||
<div class="docblock"><p>Hello world!</p>
|
||||
<p>Goodbye!
|
||||
Hello again!</p>
|
||||
</div>
|
@ -1,10 +1,7 @@
|
||||
#![crate_name = "foo"]
|
||||
|
||||
// @has 'foo/struct.S1.html'
|
||||
// @count - '//details[@class="rustdoc-toggle top-doc"]/div[@class="docblock"]/p' \
|
||||
// 1
|
||||
// @has - '//details[@class="rustdoc-toggle top-doc"]/div[@class="docblock"]/p[1]' \
|
||||
// 'Hello world! Goodbye! Hello again!'
|
||||
// @snapshot S1_top-doc - '//details[@class="rustdoc-toggle top-doc"]/div[@class="docblock"]'
|
||||
|
||||
#[doc = "Hello world!\n\n"]
|
||||
/// Goodbye!
|
||||
@ -12,12 +9,7 @@
|
||||
pub struct S1;
|
||||
|
||||
// @has 'foo/struct.S2.html'
|
||||
// @count - '//details[@class="rustdoc-toggle top-doc"]/div[@class="docblock"]/p' \
|
||||
// 2
|
||||
// @has - '//details[@class="rustdoc-toggle top-doc"]/div[@class="docblock"]/p[1]' \
|
||||
// 'Hello world!'
|
||||
// @has - '//details[@class="rustdoc-toggle top-doc"]/div[@class="docblock"]/p[2]' \
|
||||
// 'Goodbye! Hello again!'
|
||||
// @snapshot S2_top-doc - '//details[@class="rustdoc-toggle top-doc"]/div[@class="docblock"]'
|
||||
|
||||
/// Hello world!
|
||||
///
|
||||
|
@ -2219,12 +2219,12 @@ fn run_rustdoc_test(&self) {
|
||||
self.check_rustdoc_test_option(proc_res);
|
||||
} else {
|
||||
let root = self.config.find_rust_src_root().unwrap();
|
||||
let res = self.cmd2procres(
|
||||
Command::new(&self.config.docck_python)
|
||||
.arg(root.join("src/etc/htmldocck.py"))
|
||||
.arg(&out_dir)
|
||||
.arg(&self.testpaths.file),
|
||||
);
|
||||
let mut cmd = Command::new(&self.config.docck_python);
|
||||
cmd.arg(root.join("src/etc/htmldocck.py")).arg(&out_dir).arg(&self.testpaths.file);
|
||||
if self.config.bless {
|
||||
cmd.arg("--bless");
|
||||
}
|
||||
let res = self.cmd2procres(&mut cmd);
|
||||
if !res.status.success() {
|
||||
self.fatal_proc_rec_with_ctx("htmldocck failed!", &res, |mut this| {
|
||||
this.compare_to_default_rustdoc(&out_dir)
|
||||
|
Loading…
Reference in New Issue
Block a user