blob: 74020e21ed6bba0c17b265907a1eb506c51fb2da [file] [log] [blame] [edit]
#!/usr/bin/env python3
# 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.
import argparse
import os
import re
import sys
import textwrap
from pathlib import Path
from string import Template
FUCHSIA_DIR = Path(os.environ["FUCHSIA_DIR"])
# Paths for relevant docs.
ERRCAT_INDEX_FILE_PATH = FUCHSIA_DIR / "docs/reference/fidl/language/errcat.md"
REDIRECT_FILE_PATH = FUCHSIA_DIR / "docs/error/_redirects.yaml"
DOCS_DIR_PATH = FUCHSIA_DIR / "docs/reference/fidl/language/error-catalog"
DOC_TEMPLATE_PATH = FUCHSIA_DIR / "tools/fidl/scripts/add_errcat_templates/doc.md"
INCLUDECODE_TEMPLATE_PATH = FUCHSIA_DIR / "tools/fidl/scripts/add_errcat_templates/includecode.md"
# Paths for relevant fidc locations.
FIDLC_DIAGNOSTICS_FILE_PATH = FUCHSIA_DIR / "tools/fidl/fidlc/include/fidl/diagnostics.h"
FIDLC_ERRCAT_TESTS_FILE_PATH = FUCHSIA_DIR / "tools/fidl/fidlc/tests/errcat_good_tests.cc"
FIDLC_BUILD_GN_FILE_PATH = FUCHSIA_DIR / "tools/fidl/fidlc/tests/BUILD.gn"
FIDLC_BAD_TESTS_DIR_PATH = FUCHSIA_DIR / "tools/fidl/fidlc/tests/fidl/bad"
FIDLC_BAD_TEMPLATE_PATH = FUCHSIA_DIR / "tools/fidl/scripts/add_errcat_templates/bad.md"
FIDLC_GOOD_TESTS_DIR_PATH = FUCHSIA_DIR / "tools/fidl/fidlc/tests/fidl/good"
FIDLC_GOOD_TEMPLATE_PATH = FUCHSIA_DIR / "tools/fidl/scripts/add_errcat_templates/good.md"
REGEX_INDEX_ENTRY = '^<<error-catalog\/_fi-(\d+)\.md>>'
REGEX_REDIRECT_ENTRY = '^- from: \/fuchsia-src\/error\/fi-(\d+)'
REGEX_BAD_TEST_ENTRY = '\s*"fidl\/bad\/fi-(\d+)\.test.fidl",'
REGEX_GOOD_TEST_ENTRY = '\s*"fidl\/good\/fi-(\d+)\.test.fidl",'
REGEX_ERRCAT_TEST_FILE_ENTRY = 'TEST\(ErrcatTests, Good(\d+)'
# Use this weird string concatenation so that this string literal does not show up in search results
# when people grep for it.
DNS = "DO" + "NOT" + "SUBMIT"
def find_line_num(regex, line):
result = re.search(regex, line)
if result is None:
return -1
return int(result.groups(1)[0])
def insert_lines_at_line_num(path, lines, line_num, line):
lines.insert(line_num, line + "\n")
with open(path, "wt") as f:
f.write("".join(lines))
def insert_entry(path, entry_matcher, num, insert, offset=2):
# Only add an entry for this numeral if none already exists.
already_exists = False
with open(path, 'r') as f:
lines = f.readlines()
# Do a pass over the lines in the file to ensure that this entry does not already exist.
already_exists = any(
line.startswith(insert.split('\n')[0]) for line in lines)
# If we do need a file do another pass, inserting the supplied text in the correct position.
if not already_exists:
insert_before = None
for line_num in range(len(lines)):
line = lines[line_num]
entry_num = find_line_num(entry_matcher, line)
if entry_num == -1:
continue
insert_before = line_num
if entry_num > num:
if insert_before is None:
insert_before = line_num
insert_lines_at_line_num(path, lines, insert_before, insert)
insert_before = None
break
else:
insert_before = line_num
# Handle the edge case of the entry needing to be placed at the end of the list.
if insert_before is not None:
insert_lines_at_line_num(
path, lines, insert_before + offset, insert)
return int(not already_exists)
def substitute(template_path, subs):
subs['dns'] = DNS
# Create the file for this errcat entry, substituting the proper numeral along the way.
with open(template_path, 'r') as f:
template = Template(f.read())
return template.substitute(subs)
def create_file(file_path, template_path, subs):
# Only add this kind of file for this numeral if none exists.
if file_path.is_file():
return 0
# Create the file for this errcat entry, substituting the proper numeral along the way.
with open(template_path, 'r') as f:
template = Template(f.read())
with open(file_path, 'wt') as f:
f.write(substitute(template_path, subs))
return 1
def main(args):
"""Given an error numeral for the `fi-` domain, create a new markdown file to describe that
error, add it to the error catalog listing in errcat.md, add it to the redirect list in
_redirects.yaml, flip its entry in diagnostics.h from an `Undocumented[Error|Warning]Def`
specialization to a documented one, or any combination thereof.
Usage example:
tools/fidl/scripts/add_errcat_entry.py 123
tools/fidl/scripts/add_errcat_entry.py 1
tools/fidl/scripts/add_errcat_entry.py 1000
"""
progress = {
'index_entry_added': 0,
'redirect_entry_added': 0,
'diagnostic_specialization_swapped': 0,
'markdown_file_created': 0,
'bad_tests_added': 0,
'bad_files_created': 0,
'good_tests_added': 0,
'good_files_created': 0,
'good_test_cases_added': 0,
}
# Create the error id string, taking care to appropriately inject leading zeroes.
n = args.numeral
ns = "%04d" % args.numeral
# Only add an index entry for this numeral if none exists.
progress['index_entry_added'] = insert_entry(
ERRCAT_INDEX_FILE_PATH, REGEX_INDEX_ENTRY, n,
"<<error-catalog/_fi-%s.md>>\n" % ns)
# Only add a redirect entry for this numeral if none exists.
progress['redirect_entry_added'] = insert_entry(
REDIRECT_FILE_PATH, REGEX_REDIRECT_ENTRY, n,
'- from: /fuchsia-src/error/fi-%s\n to: /fuchsia-src/reference/fidl/language/errcat.md#fi-%s'
% (ns, ns))
# Replace the specialization used in diagnostics.h, if the error already exists in that file.
with open(FIDLC_DIAGNOSTICS_FILE_PATH, 'rt') as f:
text = f.read()
# Replace the specialization with one that is no longer `Undocumented*`.
(new_text, count) = re.subn(
"(constexpr )(Undocumented)((?:Error|Warning)Def<%d[,>])" % n,
r'\1\3',
text,
count=1)
if count == 1:
progress['diagnostic_specialization_swapped'] = True
with open(FIDLC_DIAGNOSTICS_FILE_PATH, "wt") as f:
f.write(new_text)
# Create entries for each "bad" example: one new file per case with ascending alphabetical
# suffixes ("-a", "-b", etc), and a corresponding entry in the BUILD.gn file for the tests.
bad_includecodes = []
b = 0
while b < args.bad:
b = b + 1
# 97 is "a" in ASCII.
suffix = chr(b + 96) if args.bad > 1 else ""
case_name = "fi-%s%s" % (ns, ("-" + suffix) if suffix else "")
flat_name = "%s%s" % (ns, suffix)
# Insert this test file into the BUILD.gn file.
progress['bad_tests_added'] += insert_entry(
FIDLC_BUILD_GN_FILE_PATH,
REGEX_BAD_TEST_ENTRY,
n,
' "fidl/bad/%s.test.fidl",' % case_name,
offset=1)
# Add any new files we need.
progress['bad_files_created'] += create_file(
FIDLC_BAD_TESTS_DIR_PATH / ("%s.test.fidl" % case_name),
FIDLC_BAD_TEMPLATE_PATH, {
'num': ns,
'case_name': case_name,
'flat_name': flat_name,
})
# Create the markdown entry for this case.
bad_includecodes.append(
substitute(
INCLUDECODE_TEMPLATE_PATH, {
'kind': "bad",
'case_name': case_name,
}))
# Create entries for each "good" example: one new file per case with ascending alphabetical
# suffixes ("-a", "-b", etc), a corresponding entry in the BUILD.gn file for the tests, and a
# new test in the errcat_good_tests.cc file.
good_includecodes = []
g = 0
while g < args.good:
g = g + 1
# 97 is "a" in ASCII.
suffix = chr(g + 96) if args.good > 1 else ""
case_name = "fi-%s%s" % (ns, ("-" + suffix) if suffix else "")
flat_name = "%s%s" % (ns, suffix)
# Insert this test file into the BUILD.gn file.
progress['good_tests_added'] += insert_entry(
FIDLC_BUILD_GN_FILE_PATH,
REGEX_GOOD_TEST_ENTRY,
n,
' "fidl/good/%s.test.fidl",' % case_name,
offset=1)
# Add any new files we need.
progress['good_files_created'] += create_file(
FIDLC_GOOD_TESTS_DIR_PATH / ("%s.test.fidl" % case_name),
FIDLC_GOOD_TEMPLATE_PATH, {
'num': ns,
'case_name': case_name,
'flat_name': flat_name,
})
# Create the markdown entry for this case.
good_includecodes.append(
substitute(
INCLUDECODE_TEMPLATE_PATH, {
'kind': "good",
'case_name': case_name,
}))
# Only add a test entry for the good test case if none exists. This will insert tests in
# alphabetical order, which is not ideal, but fine for the time being.
insert = textwrap.dedent(
"""TEST(ErrcatTests, Good%s) {
TestLibrary library;
library.AddFile("good/%s.test.fidl");
ASSERT_COMPILED(library);
}
""") % (flat_name, case_name)
progress['good_test_cases_added'] += insert_entry(
FIDLC_ERRCAT_TESTS_FILE_PATH,
REGEX_ERRCAT_TEST_FILE_ENTRY,
n,
insert,
offset=5)
# Create the markdown file for the actual doc.
progress['markdown_file_created'] = create_file(
DOCS_DIR_PATH / ("_fi-%s.md" % ns), DOC_TEMPLATE_PATH, {
'num': ns,
'good_includecodes': '\n'.join(good_includecodes),
'bad_includecodes': '\n'.join(bad_includecodes),
})
# Tell the user what a great job we've done.
print()
if all(action is False for action in list(progress.values())):
print(
"There is nothing to do for %s - perhaps you've already run this command, or someone else did this error already?\n"
% ns)
return 0
if progress['index_entry_added']:
print(
" * Added new index entry for fi-%s to %s." %
(ns, ERRCAT_INDEX_FILE_PATH))
if progress['redirect_entry_added']:
print(
" * Added new redirect entry for fi-%s to %s." %
(ns, REDIRECT_FILE_PATH))
if progress['diagnostic_specialization_swapped']:
print(
" * The DiagnosticDef specialization for fi-%s in %s now prints a permalink."
% (ns, FIDLC_DIAGNOSTICS_FILE_PATH))
if progress['markdown_file_created']:
print(
" * Added new markdown file for fi-%s at %s/_fi-%s.md." %
(ns, DOCS_DIR_PATH, ns))
if progress['bad_tests_added']:
print(
" * A BUILD rule for the %d new bad test cases for fi-%s has been added to %s."
% (progress['bad_tests_added'], ns, FIDLC_BUILD_GN_FILE_PATH))
if progress['good_tests_added']:
print(
" * A BUILD rule for the %d new good test case for fi-%s has been added to %s."
% (progress['good_tests_added'], ns, FIDLC_BUILD_GN_FILE_PATH))
if progress['good_test_cases_added']:
print(
" * %d good test cases for fi-%s has been added to %s." % (
progress['good_test_cases_added'], ns,
FIDLC_ERRCAT_TESTS_FILE_PATH))
# Add a line break, plus a horizontal rule, to emphasize the actionable reports.
if progress['bad_files_created'] or progress['good_files_created']:
print(
"\n----------------------------------------------------------------------\n"
)
if progress['bad_files_created']:
print(
" * Added %d bad examples for fi-%s at %s/fi-%s*.test.fidl. Resolve the TODO(%s)s!"
% (
progress['bad_files_created'], ns, FIDLC_BAD_TESTS_DIR_PATH, ns,
DNS))
if progress['good_files_created']:
print(
" * Added %d good examples for fi-%s at %s/fi-%s*.test.fidl. Resolve the TODO(%s)s!"
% (
progress['good_files_created'], ns, FIDLC_GOOD_TESTS_DIR_PATH,
ns, DNS))
print()
return 0
def non_zero_unsigned_int(arg):
as_int = int(arg)
if as_int < 0:
raise argparse.ArgumentTypeError(
"'%s' must be a non-zero unsigned integer" % as_int)
return as_int
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Add an entry to //docs/references/fidl/language/errcat.md')
parser.add_argument(
'numeral',
metavar='N',
type=int,
help='The numeral of the error being added to the errcat')
parser.add_argument(
"--bad",
type=non_zero_unsigned_int,
help='Number of "bad" examples to generate')
parser.add_argument(
"--good",
type=non_zero_unsigned_int,
help='Number of "good" examples to generate')
parser.set_defaults(good=1, bad=1)
main(parser.parse_args())