| # 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}' |