summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/_ext/testcoverage.py351
-rw-r--r--docs/_ext/vyos.py264
-rw-r--r--docs/coverage.rst43
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