blob: eb0ad2ff0cea6243e8e13bb73c8affbdb1e1dd97 [file] [log] [blame]
# Copyright 2020 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
""" Tool for generating markdown documentation for a test. """
import datetime
import difflib
from operator import itemgetter
import os
from pathlib import Path
from typing import IO, List, Tuple
import yaml
from types_ import CompatTest, FidlStep, SourceStep, HLCPP, LLCPP, RUST, DART, GO
# Lines of context to show in diffs. We can be very liberal with this number
# since we already filter out the boilerplate before passing code to difflib.
DIFF_CONTEXT = 12
# Languages to specify in markdown for syntax highlighting
MD_LANG = {
'fidl': 'fidl',
HLCPP: 'cpp',
LLCPP: 'cpp',
RUST: 'rust',
DART: 'dart',
GO: 'go',
}
DOC_DIR = 'docs/development/languages/fidl/guides/compatibility'
COMPAT_GUIDE_TOC_ENTRY = {
'title': 'Overview',
'path': '/docs/development/languages/fidl/guides/compatibility/README.md'
}
TOC_HEADER = '''
# Copyright {year} The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# WARNING: This file is machine generated by //src/tests/fidl/source_compatibility/gen, do not edit.
'''.format(year=datetime.datetime.now().year).lstrip()
DOC_HEADER = '''<!-- WARNING: This file is machine generated by //src/tests/fidl/source_compatibility/gen, do not edit. -->
Note: This document covers API impact only. For more details, see the
[ABI compatibility page]({path})
'''.format(path=COMPAT_GUIDE_TOC_ENTRY['path'])
def binding_title(binding: str) -> str:
""" Pretty prints a binding string for use as a markdown header. """
return binding.upper() if binding in [HLCPP, LLCPP
] else binding.capitalize()
def write_instructions(out: IO, ins: List[str]):
for i in ins:
out.write(f'- {i}\n')
out.write('\n')
def remove_boilerplate(lines: List[str]) -> List[str]:
""" Remove boilerplate lines. """
filtered = []
within_contents = False
for line in lines:
if '[START contents]' in line:
within_contents = True
elif '[END contents]' in line:
return filtered
elif within_contents:
filtered.append(line)
if not filtered:
raise RuntimeError('Did not find [START contents] tag in test file')
raise RuntimeError('Did not find [END contents] tag in test file')
def cat(out: IO, binding: str, path: Path):
""" Render contents of file at path to out. """
with open(path) as source:
lines = remove_boilerplate(source.readlines())
out.write(f'```{MD_LANG[binding]}\n{"".join(lines)}```\n\n')
def diff(out: IO, pre: Path, post: Path):
'''
Render a diff of pre and post to out.
'''
pre_lines = remove_boilerplate(open(pre).readlines())
post_lines = remove_boilerplate(open(post).readlines())
matcher = difflib.SequenceMatcher(
None, pre_lines, post_lines, autojunk=False)
for opcodes in matcher.get_grouped_opcodes(DIFF_CONTEXT):
out.write('```diff\n')
for tag, pre_start, pre_end, post_start, post_end in opcodes:
if tag == 'equal':
for line in pre_lines[pre_start:pre_end]:
out.write(' ' + line)
continue
if tag in {'replace', 'delete'}:
for line in pre_lines[pre_start:pre_end]:
out.write('- ' + line)
if tag in {'replace', 'insert'}:
for line in post_lines[post_start:post_end]:
out.write('+ ' + line)
out.write('\n```\n\n')
def generate_docs(test_root: Path, test: CompatTest, out: IO) -> str:
""" Generate transition documentation. """
out.write(DOC_HEADER)
# Title
out.write(f'# {test.title}\n\n')
# Overview
out.write('## Overview\n\n')
step_nums = sorted(
{s.step_num for t in test.bindings.values() for s in t.steps})
num_steps = step_nums[-1] if step_nums else 0
step_cols = [f'|[step {i}](#step-{i})' for i in step_nums]
out.write('-|[init](#init)' + ''.join(step_cols) + '\n')
out.write('|'.join(['---'] * (2 + num_steps)) + '\n')
fidl_step_nums = {
s.step_num
for t in test.bindings.values()
for s in t.steps
if isinstance(s, FidlStep)
}
out.write('fidl|[link](#fidl-init)')
for i in step_nums:
out.write('|')
if i in fidl_step_nums:
out.write(f'[link](#fidl-{i})')
out.write('\n')
bindings = sorted(test.bindings.items())
for b, transition in bindings:
out.write(f'{b}|[link](#{b}-init)')
src_step_nums = {
s.step_num for s in transition.steps if isinstance(s, SourceStep)
}
for i in step_nums:
out.write('|')
if i in src_step_nums:
out.write(f'[link](#{b}-{i})')
out.write('\n')
out.write('\n')
# Initial FIDL
out.write('## Initial State {#init}\n\n')
out.write(f'### FIDL {{#fidl-init}}\n\n')
starting_fidl = next(iter(test.bindings.values())).starting_fidl
cat(out, 'fidl', test_root / test.fidl[starting_fidl].source)
# Initial bindings
prev_fidl = test.fidl[starting_fidl].source
prev_srcs = {}
for b, t in bindings:
out.write(f'### {binding_title(b)} {{#{b}-init}}\n\n')
cat(out, b, test_root / t.starting_src)
prev_srcs[b] = t.starting_src
# Transition steps
remaining_steps = {b: list(t.steps) for b, t in bindings}
current_step = 1
while any(remaining_steps.values()):
is_first_write = True
remaining_steps = {k: v for k, v in remaining_steps.items() if v}
for b in remaining_steps:
step = remaining_steps[b][0]
if step.step_num != current_step:
continue
remaining_steps[b].pop(0)
# FIDL step
if isinstance(step, FidlStep) and is_first_write:
out.write(
f'## Update FIDL Library {{#step-{step.step_num}}}\n\n')
write_instructions(out, test.fidl[step.fidl].instructions)
source = test.fidl[step.fidl].source
diff(out, test_root / prev_fidl, test_root / source)
prev_fidl = source
is_first_write = False
# Binding step
elif isinstance(step, SourceStep):
if is_first_write:
out.write(
f'## Update Source Code {{#step-{step.step_num}}}\n\n')
is_first_write = False
out.write(
f'### {binding_title(b)} {{#{b}-{step.step_num}}}\n\n')
write_instructions(out, step.instructions)
diff(out, test_root / prev_srcs[b], test_root / step.source)
prev_srcs[b] = step.source
current_step += 1
def write_docs(test_root: Path, test: CompatTest):
doc_filename = test_root.name.replace('-', '_') + '.md'
doc_path = Path(
os.environ['FUCHSIA_DIR']) / DOC_DIR / to_filename(test_root)
with open(doc_path, 'w') as f:
generate_docs(test_root, test, f)
with open(test_root / 'README.md', 'w') as f:
f.write(f'See //{DOC_DIR}/{doc_filename}')
def regen_toc(all_tests: List[Tuple[Path, CompatTest]]):
lines = [to_yaml(**COMPAT_GUIDE_TOC_ENTRY)]
all_tests = sorted(all_tests, key=itemgetter(0))
for test_root, test in all_tests:
path = f'/{DOC_DIR}/{to_filename(test_root)}'
lines.append(to_yaml(test.title, path))
# print debug statements in markdown xref format, for easy copy/pasting
print(f'[example-{test_root.name}]: {path}')
toc = TOC_HEADER + 'toc:\n' + '\n'.join(lines) + '\n'
toc_path = Path(os.environ['FUCHSIA_DIR']) / DOC_DIR / '_toc.yaml'
with open(toc_path, 'w') as f:
f.write(toc)
def to_filename(test_root: Path) -> str:
return test_root.name.replace('-', '_') + '.md'
def to_yaml(title: str, path: str) -> str:
return f'- title: {title}\n path: {path}'