diff options
Diffstat (limited to 'test/sanity/code-smell/docs-build.py')
-rwxr-xr-x | test/sanity/code-smell/docs-build.py | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/test/sanity/code-smell/docs-build.py b/test/sanity/code-smell/docs-build.py new file mode 100755 index 00000000..80eca15f --- /dev/null +++ b/test/sanity/code-smell/docs-build.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import re +import shutil +import subprocess +import sys +import tempfile + + +def main(): + base_dir = os.getcwd() + os.path.sep + docs_dir = os.path.abspath('docs/docsite') + + # TODO: Remove this temporary hack to constrain 'cryptography' when we have + # a better story for dealing with it. + tmpfd, tmp = tempfile.mkstemp() + requirements_txt = os.path.join(base_dir, 'requirements.txt') + shutil.copy2(requirements_txt, tmp) + lines = [] + with open(requirements_txt, 'r') as f: + for line in f.readlines(): + if line.strip() == 'cryptography': + line = 'cryptography < 3.4\n' + lines.append(line) + + with open(requirements_txt, 'w') as f: + f.writelines(lines) + + try: + cmd = ['make', 'core_singlehtmldocs'] + sphinx = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=docs_dir) + stdout, stderr = sphinx.communicate() + finally: + shutil.move(tmp, requirements_txt) + + stdout = stdout.decode('utf-8') + stderr = stderr.decode('utf-8') + + if sphinx.returncode != 0: + sys.stderr.write("Command '%s' failed with status code: %d\n" % (' '.join(cmd), sphinx.returncode)) + + if stdout.strip(): + stdout = simplify_stdout(stdout) + + sys.stderr.write("--> Standard Output\n") + sys.stderr.write("%s\n" % stdout.strip()) + + if stderr.strip(): + sys.stderr.write("--> Standard Error\n") + sys.stderr.write("%s\n" % stderr.strip()) + + sys.exit(1) + + with open('docs/docsite/rst_warnings', 'r') as warnings_fd: + output = warnings_fd.read().strip() + lines = output.splitlines() + + known_warnings = { + 'block-quote-missing-blank-line': r'^Block quote ends without a blank line; unexpected unindent.$', + 'literal-block-lex-error': r'^Could not lex literal_block as "[^"]*". Highlighting skipped.$', + 'duplicate-label': r'^duplicate label ', + 'undefined-label': r'undefined label: ', + 'unknown-document': r'unknown document: ', + 'toc-tree-missing-document': r'toctree contains reference to nonexisting document ', + 'reference-target-not-found': r'[^ ]* reference target not found: ', + 'not-in-toc-tree': r"document isn't included in any toctree$", + 'unexpected-indentation': r'^Unexpected indentation.$', + 'definition-list-missing-blank-line': r'^Definition list ends without a blank line; unexpected unindent.$', + 'explicit-markup-missing-blank-line': r'Explicit markup ends without a blank line; unexpected unindent.$', + 'toc-tree-glob-pattern-no-match': r"^toctree glob pattern '[^']*' didn't match any documents$", + 'unknown-interpreted-text-role': '^Unknown interpreted text role "[^"]*".$', + } + + for line in lines: + match = re.search('^(?P<path>[^:]+):((?P<line>[0-9]+):)?((?P<column>[0-9]+):)? (?P<level>WARNING|ERROR): (?P<message>.*)$', line) + + if not match: + path = 'docs/docsite/rst/index.rst' + lineno = 0 + column = 0 + code = 'unknown' + message = line + + # surface unknown lines while filtering out known lines to avoid excessive output + print('%s:%d:%d: %s: %s' % (path, lineno, column, code, message)) + continue + + path = match.group('path') + lineno = int(match.group('line') or 0) + column = int(match.group('column') or 0) + level = match.group('level').lower() + message = match.group('message') + + path = os.path.abspath(path) + + if path.startswith(base_dir): + path = path[len(base_dir):] + + if path.startswith('rst/'): + path = 'docs/docsite/' + path # fix up paths reported relative to `docs/docsite/` + + if level == 'warning': + code = 'warning' + + for label, pattern in known_warnings.items(): + if re.search(pattern, message): + code = label + break + else: + code = 'error' + + print('%s:%d:%d: %s: %s' % (path, lineno, column, code, message)) + + +def simplify_stdout(value): + """Simplify output by omitting earlier 'rendering: ...' messages.""" + lines = value.strip().splitlines() + + rendering = [] + keep = [] + + def truncate_rendering(): + """Keep last rendering line (if any) with a message about omitted lines as needed.""" + if not rendering: + return + + notice = rendering[-1] + + if len(rendering) > 1: + notice += ' (%d previous rendering line(s) omitted)' % (len(rendering) - 1) + + keep.append(notice) + # Could change to rendering.clear() if we do not support python2 + rendering[:] = [] + + for line in lines: + if line.startswith('rendering: '): + rendering.append(line) + continue + + truncate_rendering() + keep.append(line) + + truncate_rendering() + + result = '\n'.join(keep) + + return result + + +if __name__ == '__main__': + main() |