summaryrefslogtreecommitdiffstats
path: root/test/sanity/code-smell/docs-build.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/sanity/code-smell/docs-build.py')
-rw-r--r--test/sanity/code-smell/docs-build.py152
1 files changed, 152 insertions, 0 deletions
diff --git a/test/sanity/code-smell/docs-build.py b/test/sanity/code-smell/docs-build.py
new file mode 100644
index 0000000..aaa6937
--- /dev/null
+++ b/test/sanity/code-smell/docs-build.py
@@ -0,0 +1,152 @@
+from __future__ import annotations
+
+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.run(cmd, stdin=subprocess.DEVNULL, capture_output=True, cwd=docs_dir, check=False, text=True)
+ finally:
+ shutil.move(tmp, requirements_txt)
+
+ stdout = sphinx.stdout
+ stderr = sphinx.stderr
+
+ 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()