diff options
-rw-r--r-- | docs/_ext/testcoverage.py | 351 | ||||
-rw-r--r-- | docs/_ext/vyos.py | 264 | ||||
-rw-r--r-- | docs/coverage.rst | 43 |
3 files changed, 626 insertions, 32 deletions
diff --git a/docs/_ext/testcoverage.py b/docs/_ext/testcoverage.py new file mode 100644 index 00000000..70714d6b --- /dev/null +++ b/docs/_ext/testcoverage.py @@ -0,0 +1,351 @@ +''' +generate json with all commands from xml for vyos documentation coverage + +''' + + +import sys +import os +import json +import re +import logging + +from io import BytesIO +from lxml import etree as ET +import shutil + +default_constraint_err_msg = "Invalid value" +validator_dir = "" + + +input_data = [ + { + "kind": "cfgcmd", + "input_dir": "_include/vyos-1x/interface-definitions/", + "schema_file": "_include/vyos-1x/schema/interface_definition.rng", + "files": [] + }, + { + "kind": "opcmd", + "input_dir": "_include/vyos-1x/op-mode-definitions/", + "schema_file": "_include/vyos-1x/schema/op-mode-definition.rng", + "files": [] + } +] + +node_data = { + 'cfgcmd': {}, + 'opcmd': {}, +} + +def get_properties(p): + props = {} + props['valueless'] = False + + try: + if p.find("valueless") is not None: + props['valueless'] = True + except: + pass + + if p is None: + return props + + # Get the help string + try: + props["help"] = p.find("help").text + except: + pass + + # Get value help strings + try: + vhe = p.findall("valueHelp") + vh = [] + for v in vhe: + vh.append( (v.find("format").text, v.find("description").text) ) + props["val_help"] = vh + except: + props["val_help"] = [] + + # Get the constraint statements + error_msg = default_constraint_err_msg + # Get the error message if it's there + try: + error_msg = p.find("constraintErrorMessage").text + except: + pass + + + vce = p.find("constraint") + vc = [] + if vce is not None: + # The old backend doesn't support multiple validators in OR mode + # so we emulate it + + regexes = [] + regex_elements = vce.findall("regex") + if regex_elements is not None: + regexes = list(map(lambda e: e.text.strip(), regex_elements)) + if "" in regexes: + print("Warning: empty regex, node will be accepting any value") + + validator_elements = vce.findall("validator") + validators = [] + if validator_elements is not None: + for v in validator_elements: + v_name = os.path.join(validator_dir, v.get("name")) + + # XXX: lxml returns None for empty arguments + v_argument = None + try: + v_argument = v.get("argument") + except: + pass + if v_argument is None: + v_argument = "" + + validators.append("{0} {1}".format(v_name, v_argument)) + + + regex_args = " ".join(map(lambda s: "--regex \\\'{0}\\\'".format(s), regexes)) + validator_args = " ".join(map(lambda s: "--exec \\\"{0}\\\"".format(s), validators)) + validator_script = '${vyos_libexec_dir}/validate-value.py' + validator_string = "exec \"{0} {1} {2} --value \\\'$VAR(@)\\\'\"; \"{3}\"".format(validator_script, regex_args, validator_args, error_msg) + + props["constraint"] = validator_string + + # Get the completion help strings + try: + che = p.findall("completionHelp") + ch = "" + for c in che: + scripts = c.findall("script") + paths = c.findall("path") + lists = c.findall("list") + + # Current backend doesn't support multiple allowed: tags + # so we get to emulate it + comp_exprs = [] + for i in lists: + comp_exprs.append("echo \"{0}\"".format(i.text)) + for i in paths: + comp_exprs.append("/bin/cli-shell-api listNodes {0}".format(i.text)) + for i in scripts: + comp_exprs.append("sh -c \"{0}\"".format(i.text)) + comp_help = " && ".join(comp_exprs) + props["comp_help"] = comp_help + except: + props["comp_help"] = [] + + # Get priority + try: + props["priority"] = p.find("priority").text + except: + pass + + # Get "multi" + if p.find("multi") is not None: + props["multi"] = True + + # Get "valueless" + if p.find("valueless") is not None: + props["valueless"] = True + + return props + +def process_node(n, f): + + props_elem = n.find("properties") + children = n.find("children") + command = n.find("command") + children_nodes = [] + owner = n.get("owner") + node_type = n.tag + + name = n.get("name") + props = get_properties(props_elem) + + if node_type != "node": + if "valueless" not in props.keys(): + props["type"] = "txt" + if node_type == "tagNode": + props["tag"] = "True" + + if node_type == "node" and children is not None: + inner_nodes = children.iterfind("*") + index_child = 0 + for inner_n in inner_nodes: + children_nodes.append(process_node(inner_n, f)) + index_child = index_child + 1 + + if node_type == "tagNode" and children is not None: + inner_nodes = children.iterfind("*") + index_child = 0 + for inner_n in inner_nodes: + children_nodes.append(process_node(inner_n, f)) + index_child = index_child + 1 + else: + # This is a leaf node + pass + + if command is not None: + test_command = True + else: + test_command = False + node = { + 'name': name, + 'type': node_type, + 'children': children_nodes, + 'props': props, + 'command': test_command, + 'filename': f + } + return node + + + +def create_commands(data, parent_list=[], level=0): + result = [] + command = { + 'name': [], + 'help': None, + 'tag_help': [], + 'level': level, + 'no_childs': False, + 'filename': None + } + command['filename'] = data['filename'] + command['name'].extend(parent_list) + command['name'].append(data['name']) + + if data['type'] == 'tagNode': + command['name'].append("<" + data['name'] + ">") + + if 'val_help' in data['props'].keys(): + for val_help in data['props']['val_help']: + command['tag_help'].append(val_help) + + if len(data['children']) == 0: + command['no_childs'] = True + + if data['command']: + command['no_childs'] = True + + try: + help_text = data['props']['help'] + command['help'] = re.sub(r"[\n\t]*", "", help_text) + + except: + command['help'] = "" + + command['valueless'] = data['props']['valueless'] + + if 'children' in data.keys(): + children_bool = True + for child in data['children']: + result.extend(create_commands(child, command['name'], level + 1)) + + if command['no_childs']: + result.append(command) + + + + return result + + +def include_file(line, input_dir): + string = "" + if "#include <include" in line.strip(): + include_filename = line.strip().split('<')[1][:-1] + with open(input_dir + include_filename) as ifp: + iline = ifp.readline() + while iline: + string = string + include_file(iline.strip(), input_dir) + iline = ifp.readline() + else: + string = line + return string + + +def get_working_commands(): + for entry in input_data: + for (dirpath, dirnames, filenames) in os.walk(entry['input_dir']): + entry['files'].extend(filenames) + break + + for f in entry['files']: + + string = "" + with open(entry['input_dir'] + f) as fp: + line = fp.readline() + while line: + string = string + include_file(line.strip(), entry['input_dir']) + line = fp.readline() + + try: + xml = ET.parse(BytesIO(bytes(string, 'utf-8'))) + except Exception as e: + print("Failed to load interface definition file {0}".format(f)) + print(e) + sys.exit(1) + + try: + relaxng_xml = ET.parse(entry['schema_file']) + validator = ET.RelaxNG(relaxng_xml) + + if not validator.validate(xml): + print(validator.error_log) + print("Interface definition file {0} does not match the schema!".format(f)) + sys.exit(1) + except Exception as e: + print("Failed to load the XML schema {0}".format(entry['schema_file'])) + print(e) + sys.exit(1) + + root = xml.getroot() + nodes = root.iterfind("*") + for n in nodes: + node_data[entry['kind']][f] = process_node(n, f) + + # build config tree and sort + + config_tree_new = { + 'cfgcmd': {}, + 'opcmd': {}, + } + + for kind in node_data: + for entry in node_data[kind]: + node_0 = node_data[kind][entry]['name'] + + if node_0 not in config_tree_new[kind].keys(): + config_tree_new[kind][node_0] = { + 'name': node_0, + 'type': node_data[kind][entry]['type'], + 'props': node_data[kind][entry]['props'], + 'children': [], + 'command': node_data[kind][entry]['command'], + 'filename': node_data[kind][entry]['filename'], + } + config_tree_new[kind][node_0]['children'].extend(node_data[kind][entry]['children']) + + result = { + 'cfgcmd': [], + 'opcmd': [], + } + for kind in config_tree_new: + for e in config_tree_new[kind]: + result[kind].extend(create_commands(config_tree_new[kind][e])) + + for cmd in result['cfgcmd']: + cmd['cmd'] = " ".join(cmd['name']) + for cmd in result['opcmd']: + cmd['cmd'] = " ".join(cmd['name']) + return result + + + +if __name__ == "__main__": + res = get_working_commands() + print(json.dumps(res)) + #print(res['cfgcmd'][0])
\ No newline at end of file diff --git a/docs/_ext/vyos.py b/docs/_ext/vyos.py index 4001f0fe..cc408784 100644 --- a/docs/_ext/vyos.py +++ b/docs/_ext/vyos.py @@ -1,25 +1,41 @@ import re -import io +import json import os from docutils import io, nodes, utils, statemachine -from docutils.utils.error_reporting import SafeString, ErrorString from docutils.parsers.rst.roles import set_classes from docutils.parsers.rst import Directive, directives + from sphinx.util.docutils import SphinxDirective +from testcoverage import get_working_commands + def setup(app): app.add_config_value( 'vyos_phabricator_url', - 'https://phabricator.vyos.net/', '' + 'https://phabricator.vyos.net/', + 'html' + ) + + app.add_config_value( + 'vyos_working_commands', + get_working_commands(), + 'html' ) + app.add_config_value( + 'vyos_coverage', + { + 'cfgcmd': [0,len(app.config.vyos_working_commands['cfgcmd'])], + 'opcmd': [0,len(app.config.vyos_working_commands['opcmd'])] + }, + 'html' + ) + app.add_role('vytask', vytask_role) app.add_role('cfgcmd', cmd_role) app.add_role('opcmd', cmd_role) - print(app.config.vyos_phabricator_url) - app.add_node( inlinecmd, html=(inlinecmd.visit_span, inlinecmd.depart_span), @@ -46,9 +62,11 @@ def setup(app): text=(CmdHeader.visit_div, CmdHeader.depart_div) ) app.add_node(CfgcmdList) + app.add_node(CfgcmdListCoverage) app.add_directive('cfgcmdlist', CfgcmdlistDirective) app.add_node(OpcmdList) + app.add_node(OpcmdListCoverage) app.add_directive('opcmdlist', OpcmdlistDirective) app.add_directive('cfgcmd', CfgCmdDirective) @@ -56,15 +74,17 @@ def setup(app): app.add_directive('cmdinclude', CfgInclude) app.connect('doctree-resolved', process_cmd_nodes) - class CfgcmdList(nodes.General, nodes.Element): pass - class OpcmdList(nodes.General, nodes.Element): pass -import json +class CfgcmdListCoverage(nodes.General, nodes.Element): + pass + +class OpcmdListCoverage(nodes.General, nodes.Element): + pass class CmdHeader(nodes.General, nodes.Element): @@ -200,8 +220,8 @@ class CfgInclude(Directive): '(wrong locale?).' % (self.name, SafeString(path))) except IOError: - raise self.severe(u'Problems with "%s" directive path.' % - (self.name)) + raise self.severe(u'Problems with "%s" directive path:\n%s.' % + (self.name, ErrorString(error))) startline = self.options.get('start-line', None) endline = self.options.get('end-line', None) try: @@ -275,9 +295,18 @@ class CfgInclude(Directive): self.state, self.state_machine) return codeblock.run() - + new_include_lines = [] - + var_value0 = self.options.get('var0', '') + var_value1 = self.options.get('var1', '') + var_value2 = self.options.get('var2', '') + var_value3 = self.options.get('var3', '') + var_value4 = self.options.get('var4', '') + var_value5 = self.options.get('var5', '') + var_value6 = self.options.get('var6', '') + var_value7 = self.options.get('var7', '') + var_value8 = self.options.get('var8', '') + var_value9 = self.options.get('var9', '') for line in include_lines: for i in range(10): value = self.options.get(f'var{i}','') @@ -285,22 +314,41 @@ class CfgInclude(Directive): line = re.sub('\s?{{\s?var' + str(i) + '\s?}}',value,line) else: line = re.sub('{{\s?var' + str(i) + '\s?}}',value,line) - new_include_lines.append(line) self.state_machine.insert_input(new_include_lines, path) return [] class CfgcmdlistDirective(Directive): + has_content = False + required_arguments = 0 + option_spec = { + 'show-coverage': directives.flag + } def run(self): - return [CfgcmdList('')] + cfglist = CfgcmdList() + cfglist['coverage'] = False + if 'show-coverage' in self.options: + cfglist['coverage'] = True + return [cfglist] class OpcmdlistDirective(Directive): + has_content = False + required_arguments = 0 + option_spec = { + 'show-coverage': directives.flag + } def run(self): - return [OpcmdList('')] + oplist = OpcmdList() + oplist['coverage'] = False + if 'show-coverage' in self.options: + oplist['coverage'] = True + + return [oplist] + class CmdDirective(SphinxDirective): @@ -308,7 +356,8 @@ class CmdDirective(SphinxDirective): has_content = True custom_class = '' - def run(self): + def run(self): + title_list = [] content_list = [] title_text = '' @@ -386,7 +435,134 @@ class CfgCmdDirective(CmdDirective): custom_class = 'cfg' -def process_cmd_node(app, cmd, fromdocname): +def strip_cmd(cmd): + #cmd = re.sub('set','',cmd) + cmd = re.sub('\s\|\s','',cmd) + cmd = re.sub('<\S*>','',cmd) + cmd = re.sub('\[\S\]','',cmd) + cmd = re.sub('\s+','',cmd) + return cmd + +def build_row(app, fromdocname, rowdata): + row = nodes.row() + for cell in rowdata: + entry = nodes.entry() + row += entry + if isinstance(cell, list): + for item in cell: + if isinstance(item, dict): + entry += process_cmd_node(app, item, fromdocname, '') + else: + entry += nodes.paragraph(text=item) + elif isinstance(cell, bool): + if cell: + entry += nodes.paragraph(text="") + entry['classes'] = ['coverage-ok'] + else: + entry += nodes.paragraph(text="") + entry['classes'] = ['coverage-fail'] + else: + entry += nodes.paragraph(text=cell) + return row + + + +def process_coverage(app, fromdocname, doccmd, xmlcmd, cli_type): + coverage_list = {} + int_docs = 0 + int_xml = 0 + for cmd in doccmd: + coverage_item = { + 'doccmd': None, + 'xmlcmd': None, + 'doccmd_item': None, + 'xmlcmd_item': None, + 'indocs': False, + 'inxml': False, + 'xmlfilename': None + } + coverage_item['doccmd'] = cmd['cmd'] + coverage_item['doccmd_item'] = cmd + coverage_item['indocs'] = True + int_docs += 1 + coverage_list[strip_cmd(cmd['cmd'])] = dict(coverage_item) + + for cmd in xmlcmd: + + strip = strip_cmd(cmd['cmd']) + if strip not in coverage_list.keys(): + coverage_item = { + 'doccmd': None, + 'xmlcmd': None, + 'doccmd_item': None, + 'xmlcmd_item': None, + 'indocs': False, + 'inxml': False, + 'xmlfilename': None + } + coverage_item['xmlcmd'] = cmd['cmd'] + coverage_item['xmlcmd_item'] = cmd + coverage_item['inxml'] = True + coverage_item['xmlfilename'] = cmd['filename'] + int_xml += 1 + coverage_list[strip] = dict(coverage_item) + else: + #print("===BEGIN===") + #print(cmd) + #print(coverage_list[strip]) + #print(strip) + #print("===END====") + coverage_list[strip]['xmlcmd'] = cmd['cmd'] + coverage_list[strip]['xmlcmd_item'] = cmd + coverage_list[strip]['inxml'] = True + coverage_list[strip]['xmlfilename'] = cmd['filename'] + int_xml += 1 + + + + + table = nodes.table() + tgroup = nodes.tgroup(cols=3) + table += tgroup + + header = (f'{int_docs}/{len(coverage_list)} in Docs', f'{int_xml}/{len(coverage_list)} in XML', 'Command') + colwidths = (1, 1, 8) + table = nodes.table() + tgroup = nodes.tgroup(cols=len(header)) + table += tgroup + for colwidth in colwidths: + tgroup += nodes.colspec(colwidth=colwidth) + thead = nodes.thead() + tgroup += thead + thead += build_row(app, fromdocname, header) + tbody = nodes.tbody() + tgroup += tbody + for entry in sorted(coverage_list): + body_text_list = [] + if coverage_list[entry]['indocs']: + body_text_list.append(coverage_list[entry]['doccmd_item']) + else: + body_text_list.append('Not documented yet') + + if coverage_list[entry]['inxml']: + body_text_list.append("------------------") + body_text_list.append(str(coverage_list[entry]['xmlfilename']) + ":") + body_text_list.append(coverage_list[entry]['xmlcmd']) + else: + body_text_list.append('Nothing found in XML Definitions') + + + tbody += build_row(app, fromdocname, + ( + coverage_list[entry]['indocs'], + coverage_list[entry]['inxml'], + body_text_list + ) + ) + + return table + +def process_cmd_node(app, cmd, fromdocname, cli_type): para = nodes.paragraph() newnode = nodes.reference('', '') innernode = cmd['cmdnode'] @@ -401,21 +577,45 @@ def process_cmd_node(app, cmd, fromdocname): def process_cmd_nodes(app, doctree, fromdocname): - env = app.builder.env - - for node in doctree.traverse(CfgcmdList): - content = [] - - for cmd in sorted(env.vyos_cfgcmd, key=lambda i: i['cmd']): - content.append(process_cmd_node(app, cmd, fromdocname)) - node.replace_self(content) - - for node in doctree.traverse(OpcmdList): - content = [] + try: + env = app.builder.env + + for node in doctree.traverse(CfgcmdList): + content = [] + if node.attributes['coverage']: + node.replace_self( + process_coverage( + app, + fromdocname, + env.vyos_cfgcmd, + app.config.vyos_working_commands['cfgcmd'], + 'cfgcmd' + ) + ) + else: + for cmd in sorted(env.vyos_cfgcmd, key=lambda i: i['cmd']): + content.append(process_cmd_node(app, cmd, fromdocname, 'cfgcmd')) + node.replace_self(content) + + for node in doctree.traverse(OpcmdList): + content = [] + if node.attributes['coverage']: + node.replace_self( + process_coverage( + app, + fromdocname, + env.vyos_opcmd, + app.config.vyos_working_commands['opcmd'], + 'opcmd' + ) + ) + else: + for cmd in sorted(env.vyos_opcmd, key=lambda i: i['cmd']): + content.append(process_cmd_node(app, cmd, fromdocname, 'opcmd')) + node.replace_self(content) - for cmd in sorted(env.vyos_opcmd, key=lambda i: i['cmd']): - content.append(process_cmd_node(app, cmd, fromdocname)) - node.replace_self(content) + except Exception as inst: + print(inst) def vytask_role(name, rawtext, text, lineno, inliner, options={}, content=[]): @@ -430,4 +630,4 @@ def vytask_role(name, rawtext, text, lineno, inliner, options={}, content=[]): def cmd_role(name, rawtext, text, lineno, inliner, options={}, content=[]): node = nodes.literal(text, text) - return [node], [] + return [node], []
\ No newline at end of file diff --git a/docs/coverage.rst b/docs/coverage.rst new file mode 100644 index 00000000..f003f9ff --- /dev/null +++ b/docs/coverage.rst @@ -0,0 +1,43 @@ +:orphan: + +######## +Coverage +######## + +Overview over all commands, which are documented in the ``.. cfgcmd::`` or ``.. opcmd::`` Directives. + +| The build process take all xml definition files from `vyos-1x <https://github.com/vyos/vyos-1x>`_ and extract each leaf command or executable command. +| After this the commands are compare and shown in the follwoing two tables. +| The script compare only the fixed part of a command. All varables or values will be erase and then compare: + +for example there are these two commands: + + * documentation: ``interfaces ethernet <interface> address <address | dhcp | dhcpv6>``` + * xml: ``interface ethernet <ethernet> address <address>`` + +Now the script earse all in between ``<`` and ``>`` and simply compare the strings. + +**There are 2 kind of problems:** + +| ``Not documented yet`` +| A XML command are not found in ``.. cfgcmd::`` or ``.. opcmd::`` Commands +| The command should be documented + +| ``Nothing found in XML Definitions``: +| ``.. cfgcmd::`` or ``.. opcmd::`` Command are not found in a XML command +| Maybe the command where changed in the XML Definition, or the feature is not anymore in VyOS +| Some commands are not yet translated to XML + + +Configuration Commands +====================== + +.. cfgcmdlist:: + :show-coverage: + + +Operational Commands +==================== + +.. opcmdlist:: + :show-coverage:
\ No newline at end of file |