[CICD] Add CLI Command Test Coverage (#323)
* add command coverage * commit * regex * command coverage 1.0 * upload html * modify report * refactor * add annotation * Update test_cmdcov.py * modify detect command function * detect from live test file * update func _get_all_commands * Update __init__.py * update sort function * update sort function * add level argument * add cli own * add EXCLUDE MODULES * Update component.css * delete unused code * delete unused file * modify regex * add help info * add description * exclude command wait * detect from recording file * delete exception already fixed * add newline * add license headers * fix tox * refactoring * fix bug * exclude deprecated commands * fix tox * fix tox * add network exclude commands * add number sign exclude * add comment * add linter rule comcov * update * regex * add log add exec_state * add rule type command_coverage * update diff branches * support exclusion * linter refactoring * move constant and regex out * suppport linter_exclusion.yml in cmdcov * add license headers * fix pylint * fix flake8 * Update test_cmdcov.py * fix bug for linter rule * add resource skip commands * update 1. Change the selected color to pink 2. Mouse hover shows percentage details 3. Modify Coverage Report description * role commands exclusion * modify HISTORY.rst and setup.py * resource skip two commands - hard to test * bug fixes: search argument * modify command coverage to command test coverage * bug fixes: Add three new scenarios * update CLI own and exclude commands * move private dns out of cli own * add log for exclusions * exclude vm host restart * config * Update test_config.py * update * update * update
This commit is contained in:
Родитель
64cd348d24
Коммит
276b24a124
|
@ -12,3 +12,4 @@ azdev.egg-info/
|
|||
pip-wheel-metadata/*
|
||||
build/
|
||||
dist/
|
||||
cmd_coverage
|
||||
|
|
|
@ -3,6 +3,11 @@
|
|||
Release History
|
||||
===============
|
||||
|
||||
0.1.49
|
||||
++++++
|
||||
* Add Command Coverage Report (#323)
|
||||
* Add Linter rule missing_command_coverage and missing_parameter_coverage (#323)
|
||||
|
||||
0.1.48
|
||||
++++++
|
||||
* `azdev command-change meta-export`: Add option deprecation info and ignore `cmd` arg (#381)
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
# license information.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
__VERSION__ = '0.1.48'
|
||||
__VERSION__ = '0.1.49'
|
||||
|
|
|
@ -31,6 +31,9 @@ def load_command_table(self, _):
|
|||
g.command('list-command-table', 'list_command_table')
|
||||
g.command('diff-command-tables', 'diff_command_tables')
|
||||
|
||||
with CommandGroup(self, '', operation_group('cmdcov')) as g:
|
||||
g.command('cmdcov', 'run_cmdcov')
|
||||
|
||||
with CommandGroup(self, 'verify', operation_group('pypi')) as g:
|
||||
g.command('history', 'check_history')
|
||||
|
||||
|
|
|
@ -280,3 +280,14 @@ helps['extension generate-docs'] = """
|
|||
long-summary: >
|
||||
This command installs the extensions in a temporary directory and sets it as the extensions dir when generating reference docs.
|
||||
"""
|
||||
|
||||
helps['cmdcov'] = """
|
||||
short-summary: Run command test coverage and generate CLI command test coverage report.
|
||||
examples:
|
||||
- name: Check all CLI modules command test coverage.
|
||||
text: azdev cmdcov CLI
|
||||
- name: Check one or serveral modules command test coverage.
|
||||
text: azdev cmdcov vm storage
|
||||
- name: Check CLI modules command test coverage in argument level.
|
||||
text: azdev cmdcov CLI --level argument
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
# -----------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import time
|
||||
import yaml
|
||||
|
||||
from knack.log import get_logger
|
||||
from knack.util import CLIError
|
||||
from azdev.utilities import (
|
||||
heading, display, get_path_table, require_azure_cli, filter_by_git_diff)
|
||||
from azdev.utilities.path import get_cli_repo_path, get_ext_repo_paths
|
||||
from .cmdcov import CmdcovManager
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
try:
|
||||
with open(os.path.join(get_cli_repo_path(), 'scripts', 'ci', 'cmdcov.yml'), 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
EXCLUDE_MODULES = config['EXCLUDE_MODULES']
|
||||
except CLIError as ex:
|
||||
logger.warning('Failed to load cmdcov.yml: %s', ex)
|
||||
|
||||
|
||||
# pylint:disable=too-many-locals, too-many-statements, too-many-branches, duplicate-code
|
||||
def run_cmdcov(modules=None, git_source=None, git_target=None, git_repo=None, level='command'):
|
||||
"""
|
||||
:param modules:
|
||||
:param git_source:
|
||||
:param git_target:
|
||||
:param git_target:
|
||||
:return: None
|
||||
"""
|
||||
require_azure_cli()
|
||||
from azure.cli.core import get_default_cli # pylint: disable=import-error
|
||||
from azure.cli.core.file_util import ( # pylint: disable=import-error
|
||||
get_all_help, create_invoker_and_load_cmds_and_args)
|
||||
|
||||
heading('CLI Command Test Coverage')
|
||||
|
||||
# allow user to run only on CLI or extensions
|
||||
cli_only = modules == ['CLI']
|
||||
# ext_only = modules == ['EXT']
|
||||
enable_cli_own = bool(cli_only or modules is None)
|
||||
if cli_only:
|
||||
modules = None
|
||||
# if cli_only or ext_only:
|
||||
# modules = None
|
||||
|
||||
selected_modules = get_path_table(include_only=modules)
|
||||
|
||||
# filter down to only modules that have changed based on git diff
|
||||
selected_modules = filter_by_git_diff(selected_modules, git_source, git_target, git_repo)
|
||||
|
||||
if not any(selected_modules.values()):
|
||||
logger.warning('No commands selected to check.')
|
||||
|
||||
if EXCLUDE_MODULES:
|
||||
selected_modules['mod'] = {k: v for k, v in selected_modules['mod'].items() if k not in EXCLUDE_MODULES}
|
||||
selected_modules['ext'] = {k: v for k, v in selected_modules['ext'].items() if k not in EXCLUDE_MODULES}
|
||||
|
||||
if cli_only:
|
||||
selected_mod_names = list(selected_modules['mod'].keys())
|
||||
selected_mod_paths = list(selected_modules['mod'].values())
|
||||
# elif ext_only:
|
||||
# selected_mod_names = list(selected_modules['ext'].keys())
|
||||
# selected_mod_paths = list(selected_modules['ext'].values())
|
||||
else:
|
||||
selected_mod_names = list(selected_modules['mod'].keys())
|
||||
selected_mod_paths = list(selected_modules['mod'].values())
|
||||
|
||||
if selected_mod_names:
|
||||
display('Modules: {}\n'.format(', '.join(selected_mod_names)))
|
||||
|
||||
start = time.time()
|
||||
display('Initializing cmdcov with command table and help files...')
|
||||
az_cli = get_default_cli()
|
||||
|
||||
# load commands, args, and help
|
||||
create_invoker_and_load_cmds_and_args(az_cli)
|
||||
loaded_help = get_all_help(az_cli)
|
||||
|
||||
stop = time.time()
|
||||
logger.info('Commands and help loaded in %i sec', stop - start)
|
||||
|
||||
# format loaded help
|
||||
loaded_help = {data.command: data for data in loaded_help if data.command}
|
||||
|
||||
linter_exclusions = {}
|
||||
|
||||
# collect rule exclusions from selected mod paths
|
||||
for path in selected_mod_paths:
|
||||
mod_exclusion_path = os.path.join(path, 'linter_exclusions.yml')
|
||||
if os.path.isfile(mod_exclusion_path):
|
||||
with open(mod_exclusion_path) as f:
|
||||
mod_exclusions = yaml.safe_load(f)
|
||||
merge_exclusions(linter_exclusions, mod_exclusions or {})
|
||||
|
||||
# collect rule exclusions from global exclusion paths
|
||||
global_exclusion_paths = [os.path.join(get_cli_repo_path(), 'linter_exclusions.yml')]
|
||||
try:
|
||||
global_exclusion_paths.extend([os.path.join(path, 'linter_exclusions.yml')
|
||||
for path in (get_ext_repo_paths() or [])])
|
||||
except CLIError:
|
||||
pass
|
||||
for path in global_exclusion_paths:
|
||||
if os.path.isfile(path):
|
||||
with open(path) as f:
|
||||
mod_exclusions = yaml.safe_load(f)
|
||||
merge_exclusions(linter_exclusions, mod_exclusions or {})
|
||||
|
||||
cmdcov_manager = CmdcovManager(selected_mod_names=selected_mod_names,
|
||||
selected_mod_paths=selected_mod_paths,
|
||||
loaded_help=loaded_help,
|
||||
level=level,
|
||||
enable_cli_own=enable_cli_own,
|
||||
exclusions=linter_exclusions)
|
||||
cmdcov_manager.run()
|
||||
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
def merge_exclusions(left_exclusion, right_exclusion):
|
||||
for command_name, value in right_exclusion.items():
|
||||
for rule_name in value.get('rule_exclusions', []):
|
||||
left_exclusion.setdefault(command_name, {}).setdefault('rule_exclusions', []).append(rule_name)
|
||||
for param_name in value.get('parameters', {}):
|
||||
for rule_name in value.get('parameters', {}).get(param_name, {}).get('rule_exclusions', []):
|
||||
left_exclusion.setdefault(command_name, {}).setdefault('parameters', {}).setdefault(param_name, {}).setdefault('rule_exclusions', []).append(rule_name)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
# _get_all_tested_commands(['a'], ['b'])
|
||||
# regex2()
|
||||
# regex3()
|
|
@ -0,0 +1,465 @@
|
|||
# -----------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import yaml
|
||||
|
||||
from jinja2 import FileSystemLoader, Environment
|
||||
from knack.log import get_logger
|
||||
from knack.util import CLIError
|
||||
from azdev.operations.regex import get_all_tested_commands_from_regex
|
||||
from azdev.utilities.path import get_azdev_repo_path, get_cli_repo_path, find_files
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
try:
|
||||
with open(os.path.join(get_cli_repo_path(), 'scripts', 'ci', 'cmdcov.yml'), 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
ENCODING = config['ENCODING']
|
||||
GLOBAL_PARAMETERS = config['GLOBAL_PARAMETERS']
|
||||
GENERIC_UPDATE_PARAMETERS = config['GENERIC_UPDATE_PARAMETERS']
|
||||
WAIT_CONDITION_PARAMETERS = config['WAIT_CONDITION_PARAMETERS']
|
||||
OTHER_PARAMETERS = config['OTHER_PARAMETERS']
|
||||
RED = config['RED']
|
||||
ORANGE = config['ORANGE']
|
||||
GREEN = config['GREEN']
|
||||
BLUE = config['BLUE']
|
||||
GOLD = config['GOLD']
|
||||
RED_PCT = config['RED_PCT']
|
||||
ORANGE_PCT = config['ORANGE_PCT']
|
||||
GREEN_PCT = config['GREEN_PCT']
|
||||
BLUE_PCT = config['BLUE_PCT']
|
||||
CLI_OWN_MODULES = config['CLI_OWN_MODULES']
|
||||
EXCLUDE_COMMANDS = config['EXCLUDE_COMMANDS']
|
||||
GLOBAL_EXCLUDE_COMMANDS = config['GLOBAL_EXCLUDE_COMMANDS']
|
||||
except CLIError as ex:
|
||||
logger.warning('Failed to load cmdcov.yml: %s', ex)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class CmdcovManager:
|
||||
|
||||
def __init__(self, selected_mod_names=None, selected_mod_paths=None, loaded_help=None, level=None,
|
||||
enable_cli_own=None, exclusions=None):
|
||||
self.selected_mod_names = selected_mod_names
|
||||
self.selected_mod_paths = selected_mod_paths
|
||||
self.loaded_help = loaded_help
|
||||
self.level = level
|
||||
self.enable_cli_own = enable_cli_own
|
||||
self.all_commands = {m: [] for m in self.selected_mod_names}
|
||||
self.all_tested_commands = {m: [] for m in self.selected_mod_names}
|
||||
self.all_live_commands = []
|
||||
self.all_untested_commands = {}
|
||||
self.command_test_coverage = {'Total': [0, 0, 0]}
|
||||
self.date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
self.report_date = '-'.join(self.date.replace(':', '-').split())
|
||||
self.cmdcov_path = os.path.join(get_azdev_repo_path(), 'azdev', 'operations', 'cmdcov')
|
||||
self.template_path = os.path.join(self.cmdcov_path, 'template')
|
||||
self.exclusions = exclusions
|
||||
|
||||
def run(self):
|
||||
self._get_all_commands()
|
||||
self._get_all_tested_commands_from_regex()
|
||||
self._get_all_tested_commands_from_record()
|
||||
self._run_command_test_coverage()
|
||||
self._get_all_tested_commands_from_live()
|
||||
self._run_command_test_coverage_enhance()
|
||||
html_file = self._render_html()
|
||||
if self.enable_cli_own:
|
||||
command_test_coverage = {k: v for k, v in self.command_test_coverage.items() if k in CLI_OWN_MODULES}
|
||||
total_tested = 0
|
||||
total_untested = 0
|
||||
command_test_coverage['Total'] = [0, 0, 0]
|
||||
for module in command_test_coverage.keys():
|
||||
total_tested += command_test_coverage[module][0] if command_test_coverage[module] else 0
|
||||
total_untested += command_test_coverage[module][1] if command_test_coverage[module] else 0
|
||||
command_test_coverage['Total'][0] = total_tested
|
||||
command_test_coverage['Total'][1] = total_untested
|
||||
command_test_coverage['Total'][2] = f'{total_tested / (total_tested + total_untested):.3%}'
|
||||
self._render_cli_html(command_test_coverage)
|
||||
self._browse(html_file)
|
||||
|
||||
def _get_all_commands(self):
|
||||
"""
|
||||
GLOBAL_EXCLUDE_COMMANDS: List[str]
|
||||
EXCLUDE_COMMANDS: Dict[str: List[str]]
|
||||
exclusions_comands: List[str]
|
||||
exclude_parameters: List[List[str]]
|
||||
exclusions_parameters: List[Tuple[str, str]]
|
||||
get all commands from loaded_help
|
||||
"""
|
||||
exclude_parameters = []
|
||||
exclude_parameters += GLOBAL_PARAMETERS + GENERIC_UPDATE_PARAMETERS + WAIT_CONDITION_PARAMETERS + \
|
||||
OTHER_PARAMETERS
|
||||
exclude_parameters = [sorted(i) for i in exclude_parameters]
|
||||
|
||||
# some module like vm have multiple command like vm vmss disk snapshot ...
|
||||
# pylint: disable=too-many-nested-blocks, too-many-boolean-expressions
|
||||
exclusions_comands = []
|
||||
exclusions_parameters = []
|
||||
for c, v in self.exclusions.items():
|
||||
if 'parameters' in v:
|
||||
for p, r in v['parameters'].items():
|
||||
if 'missing_parameter_test_coverage' in r['rule_exclusions']:
|
||||
exclusions_parameters.append((c, p))
|
||||
elif 'rule_exclusions' in v:
|
||||
if 'missing_command_test_coverage' in v['rule_exclusions']:
|
||||
exclusions_comands.append(c)
|
||||
for _, y in self.loaded_help.items():
|
||||
if hasattr(y, 'command_source') and y.command_source in self.selected_mod_names:
|
||||
module = y.command_source
|
||||
elif hasattr(y, 'command_source') and hasattr(y.command_source, 'extension_name') and \
|
||||
y.command_source.extension_name in self.selected_mod_names:
|
||||
module = y.command_source.extension_name
|
||||
else:
|
||||
continue
|
||||
if not y.deprecate_info:
|
||||
if y.command.split()[-1] not in GLOBAL_EXCLUDE_COMMANDS and \
|
||||
y.command not in EXCLUDE_COMMANDS.get(module, []) and \
|
||||
y.command not in exclusions_comands:
|
||||
if self.level == 'argument':
|
||||
for parameter in y.parameters:
|
||||
# TODO support linter_exclusions.yml
|
||||
if sorted(parameter.name_source) not in exclude_parameters:
|
||||
opt_list = [opt for opt in parameter.name_source if opt.startswith('-')]
|
||||
if opt_list:
|
||||
self.all_commands[module].append(f'{y.command} {opt_list}')
|
||||
else:
|
||||
self.all_commands[module].append(f'{y.command}')
|
||||
|
||||
def _get_all_tested_commands_from_regex(self):
|
||||
"""
|
||||
get all tested commands from test_*.py
|
||||
"""
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
for idx, path in enumerate(self.selected_mod_paths):
|
||||
test_dir = os.path.join(path, 'tests')
|
||||
files = find_files(test_dir, '*.py')
|
||||
for f in files:
|
||||
with open(os.path.join(test_dir, f), 'r', encoding=ENCODING) as f:
|
||||
lines = f.readlines()
|
||||
ref = get_all_tested_commands_from_regex(lines)
|
||||
self.all_tested_commands[self.selected_mod_names[idx]] += ref
|
||||
|
||||
def _get_all_tested_commands_from_record(self):
|
||||
"""
|
||||
get all tested commands from recording files
|
||||
"""
|
||||
for idx, path in enumerate(self.selected_mod_paths):
|
||||
test_dir = os.path.join(path, 'tests')
|
||||
files = find_files(test_dir, 'test*.yaml')
|
||||
for f in files:
|
||||
with open(os.path.join(test_dir, f)) as f:
|
||||
# safe_load can not determine a constructor for the tag: !!python/unicode
|
||||
records = yaml.load(f, Loader=yaml.Loader) or {}
|
||||
for record in records['interactions']:
|
||||
# ['acr agentpool create']
|
||||
command = record['request']['headers'].get('CommandName', [''])[0]
|
||||
# ['-n -r']
|
||||
argument = record['request']['headers'].get('ParameterSetName', [''])[0]
|
||||
if command or argument:
|
||||
cmd = command + ' ' + argument
|
||||
self.all_tested_commands[self.selected_mod_names[idx]].append(cmd)
|
||||
|
||||
def _get_all_tested_commands_from_live(self):
|
||||
with open(os.path.join(self.cmdcov_path, 'tested_command.txt'), 'r') as f:
|
||||
self.all_live_commands = f.readlines()
|
||||
|
||||
def _run_command_test_coverage(self):
|
||||
"""
|
||||
all_commands: All commands that need to be test
|
||||
all_tested_commands: All commands already tested
|
||||
command_test_coverage: {{module1: pct}, {module2: pct}}
|
||||
module: vm
|
||||
pct: xx.xxx%
|
||||
"""
|
||||
import ast
|
||||
for module in self.all_commands.keys():
|
||||
self.command_test_coverage[module] = []
|
||||
self.all_untested_commands[module] = []
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
for module in self.all_commands.keys():
|
||||
count = 0
|
||||
for command in self.all_commands[module]:
|
||||
exist_flag = False
|
||||
prefix = command.rsplit('[', maxsplit=1)[0]
|
||||
opt_list = ast.literal_eval('[' + command.rsplit('[', maxsplit=1)[1]) if self.level == 'argument' \
|
||||
else []
|
||||
for cmd in self.all_tested_commands[module]:
|
||||
if prefix in cmd or \
|
||||
module == 'rdbms' and prefix.split(maxsplit=1)[1] in cmd:
|
||||
if self.level == 'argument':
|
||||
for opt in opt_list:
|
||||
if opt in cmd:
|
||||
count += 1
|
||||
exist_flag = True
|
||||
if exist_flag:
|
||||
break
|
||||
else:
|
||||
count += 1
|
||||
exist_flag = True
|
||||
if exist_flag:
|
||||
break
|
||||
if exist_flag:
|
||||
break
|
||||
else:
|
||||
self.all_untested_commands[module].append(command)
|
||||
try:
|
||||
self.command_test_coverage[module] = [count, len(self.all_untested_commands[module]),
|
||||
f'{count / len(self.all_commands[module]):.3%}']
|
||||
except ZeroDivisionError:
|
||||
self.command_test_coverage[module] = [0, 0, 'N/A']
|
||||
self.command_test_coverage['Total'][0] += count
|
||||
self.command_test_coverage['Total'][1] += len(self.all_untested_commands[module])
|
||||
self.command_test_coverage['Total'][2] = f'''{self.command_test_coverage["Total"][0] /
|
||||
(self.command_test_coverage["Total"][0] +
|
||||
self.command_test_coverage["Total"][1]):.3%}'''
|
||||
logger.warning(self.command_test_coverage)
|
||||
return self.command_test_coverage
|
||||
|
||||
def _run_command_test_coverage_enhance(self):
|
||||
"""
|
||||
all_untest_commands: {[module]:[],}
|
||||
all_tested_commands_from_file: []
|
||||
command_test_coverage: {[module: [test, untested, pct]
|
||||
module: vm
|
||||
percentage: xx.xxx%
|
||||
"""
|
||||
import ast
|
||||
total_tested = 0
|
||||
total_untested = 0
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
for module, untested_commands in self.all_untested_commands.items():
|
||||
for cmd_idx, command in enumerate(untested_commands):
|
||||
exist_flag = False
|
||||
prefix = command.rsplit('[', maxsplit=1)[0]
|
||||
opt_list = ast.literal_eval('[' + command.rsplit('[', maxsplit=1)[1]) if self.level == 'argument' \
|
||||
else []
|
||||
for cmd in self.all_live_commands:
|
||||
if prefix in cmd:
|
||||
if self.level == 'argument':
|
||||
for opt in opt_list:
|
||||
if opt in cmd:
|
||||
self.command_test_coverage[module][0] += 1
|
||||
untested_commands.pop(cmd_idx)
|
||||
exist_flag = True
|
||||
if exist_flag:
|
||||
break
|
||||
else:
|
||||
self.command_test_coverage[module][0] += 1
|
||||
untested_commands.pop(cmd_idx)
|
||||
exist_flag = True
|
||||
if exist_flag:
|
||||
break
|
||||
if exist_flag:
|
||||
break
|
||||
try:
|
||||
self.command_test_coverage[module][1] = len(untested_commands)
|
||||
self.command_test_coverage[module][2] = f'''{self.command_test_coverage[module][0] /
|
||||
(self.command_test_coverage[module][0] +
|
||||
self.command_test_coverage[module][1]):.3%}'''
|
||||
except ZeroDivisionError:
|
||||
self.command_test_coverage[module] = [0, 0, 'N/A']
|
||||
total_tested += self.command_test_coverage[module][0] if self.command_test_coverage[module] else 0
|
||||
total_untested += self.command_test_coverage[module][1] if self.command_test_coverage[module] else 0
|
||||
self.command_test_coverage['Total'][0] = total_tested
|
||||
self.command_test_coverage['Total'][1] = total_untested
|
||||
self.command_test_coverage['Total'][2] = f'{total_tested / (total_tested + total_untested):.3%}'
|
||||
logger.warning(self.command_test_coverage)
|
||||
|
||||
def _render_html(self):
|
||||
"""
|
||||
:return: Return a HTML string
|
||||
"""
|
||||
html_path = self.get_html_path()
|
||||
description = 'Command' if self.level == 'command' else 'Command Argument'
|
||||
j2_loader = FileSystemLoader(self.template_path)
|
||||
env = Environment(loader=j2_loader)
|
||||
j2_tmpl = env.get_template('./index.j2')
|
||||
for item in self.command_test_coverage.values():
|
||||
color, percentage = self._get_color(item)
|
||||
item.append({'color': color, 'percentage': percentage})
|
||||
total = self.command_test_coverage.pop('Total')
|
||||
|
||||
content = j2_tmpl.render(description=description,
|
||||
enable_cli_own=self.enable_cli_own,
|
||||
date=self.date,
|
||||
Total=total,
|
||||
command_test_coverage=self.command_test_coverage)
|
||||
index_html = os.path.join(html_path, 'index.html')
|
||||
with open(index_html, 'w', encoding=ENCODING) as f:
|
||||
f.write(content)
|
||||
|
||||
# render child html
|
||||
for module, coverage in self.command_test_coverage.items():
|
||||
if coverage:
|
||||
self._render_child_html(module, coverage, self.all_untested_commands[module])
|
||||
|
||||
# copy source
|
||||
css_source = os.path.join(self.cmdcov_path, 'component.css')
|
||||
ico_source = os.path.join(self.cmdcov_path, 'favicon.ico')
|
||||
js_source = os.path.join(self.cmdcov_path, 'component.js')
|
||||
try:
|
||||
shutil.copy(css_source, html_path)
|
||||
shutil.copy(ico_source, html_path)
|
||||
shutil.copy(js_source, html_path)
|
||||
except IOError as e:
|
||||
logger.error("Unable to copy file %s", e)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logger.error("Unexpected error: %s", sys.exc_info())
|
||||
|
||||
return index_html
|
||||
|
||||
def _render_cli_html(self, command_test_coverage):
|
||||
"""
|
||||
render cli own html string
|
||||
"""
|
||||
html_path = self.get_html_path()
|
||||
description = 'Command' if self.level == 'command' else 'Command Argument'
|
||||
j2_loader = FileSystemLoader(self.template_path)
|
||||
env = Environment(loader=j2_loader)
|
||||
j2_tmpl = env.get_template('./index2.j2')
|
||||
for module, item in command_test_coverage.items():
|
||||
color, percentage = self._get_color(item)
|
||||
command_test_coverage[module].append({'color': color, 'percentage': percentage})
|
||||
total = command_test_coverage.pop('Total')
|
||||
|
||||
content = j2_tmpl.render(description=description,
|
||||
date=self.date,
|
||||
Total=total,
|
||||
command_test_coverage=command_test_coverage)
|
||||
index_html = os.path.join(html_path, 'index2.html')
|
||||
with open(index_html, 'w', encoding=ENCODING) as f:
|
||||
f.write(content)
|
||||
|
||||
def _render_child_html(self, module, coverage, untested_commands):
|
||||
"""
|
||||
render every module html
|
||||
"""
|
||||
html_path = self.get_html_path()
|
||||
j2_loader = FileSystemLoader(self.template_path)
|
||||
env = Environment(loader=j2_loader)
|
||||
j2_tmpl = env.get_template('./module.j2')
|
||||
content = j2_tmpl.render(module=module,
|
||||
enable_cli_own=self.enable_cli_own,
|
||||
date=self.date,
|
||||
coverage=coverage,
|
||||
untested_commands=untested_commands)
|
||||
with open(f'{html_path}/{module}.html', 'w', encoding=ENCODING) as f:
|
||||
f.write(content)
|
||||
|
||||
@staticmethod
|
||||
def _get_color(coverage):
|
||||
"""
|
||||
:param coverage:
|
||||
:return: color and percentage
|
||||
"""
|
||||
|
||||
percentage = int(round(float(coverage[2][:-1]), 0)) if coverage[2] != 'N/A' else coverage[2]
|
||||
if percentage == 'N/A':
|
||||
color = 'N/A'
|
||||
elif percentage < RED_PCT:
|
||||
color = RED
|
||||
elif percentage < ORANGE_PCT:
|
||||
color = ORANGE
|
||||
elif percentage < GREEN_PCT:
|
||||
color = GREEN
|
||||
elif percentage < BLUE_PCT:
|
||||
color = BLUE
|
||||
else:
|
||||
color = GOLD
|
||||
|
||||
return color, percentage
|
||||
|
||||
def get_html_path(self):
|
||||
"""
|
||||
:return: html_path
|
||||
"""
|
||||
root_path = get_azdev_repo_path()
|
||||
html_path = os.path.join(root_path, 'cmd_coverage', self.level, f'{self.report_date}')
|
||||
if not os.path.exists(html_path):
|
||||
os.makedirs(html_path)
|
||||
return html_path
|
||||
|
||||
@staticmethod
|
||||
def get_container_name():
|
||||
"""
|
||||
Generate container name in storage account. It is also an identifier of the pipeline run.
|
||||
:return:
|
||||
"""
|
||||
import datetime
|
||||
import random
|
||||
import string
|
||||
logger.warning('Enter get_container_name()')
|
||||
container_time = datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
|
||||
random_id = ''.join(random.choice(string.digits) for _ in range(6))
|
||||
name = container_time + '-' + random_id
|
||||
logger.warning('Exit get_container_name()')
|
||||
return name
|
||||
|
||||
@staticmethod
|
||||
def upload_files(container, html_path, account_key):
|
||||
"""
|
||||
Upload html and json files to container
|
||||
"""
|
||||
logger.warning('Enter upload_files()')
|
||||
|
||||
# Create container
|
||||
cmd = 'az storage container create -n {} --account-name clitestresultstac --account-key {}' \
|
||||
' --public-access container'.format(container, account_key)
|
||||
os.system(cmd)
|
||||
|
||||
# Upload files
|
||||
for root, dirs, files in os.walk(html_path):
|
||||
logger.debug(dirs)
|
||||
for name in files:
|
||||
if name.endswith('html') or name.endswith('css'):
|
||||
fullpath = os.path.join(root, name)
|
||||
cmd = 'az storage blob upload -f {} -c {} -n {} --account-name clitestresultstac'
|
||||
cmd = cmd.format(fullpath, container, name)
|
||||
logger.warning('Running: %s', cmd)
|
||||
os.system(cmd)
|
||||
|
||||
logger.warning('Exit upload_files()')
|
||||
|
||||
@staticmethod
|
||||
def _browse(uri, browser_name=None): # throws ImportError, webbrowser.Error
|
||||
"""Browse uri with named browser. Default browser is customizable by $BROWSER"""
|
||||
|
||||
def is_wsl():
|
||||
# "Official" way of detecting WSL: https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364
|
||||
# Run `uname -a` to get 'release' without python
|
||||
# - WSL 1: '4.4.0-19041-Microsoft'
|
||||
# - WSL 2: '4.19.128-microsoft-standard'
|
||||
import platform
|
||||
uname = platform.uname()
|
||||
platform_name = getattr(uname, 'system', uname[0]).lower()
|
||||
release = getattr(uname, 'release', uname[2]).lower()
|
||||
return platform_name == 'linux' and 'microsoft' in release
|
||||
|
||||
import webbrowser # Lazy import. Some distro may not have this.
|
||||
if browser_name:
|
||||
browser_opened = webbrowser.get(browser_name).open(uri)
|
||||
else:
|
||||
# This one can survive BROWSER=nonexist, while get(None).open(...) can not
|
||||
browser_opened = webbrowser.open(uri)
|
||||
logger.warning(uri)
|
||||
|
||||
# In WSL which doesn't have www-browser, try launching browser with PowerShell
|
||||
if not browser_opened and is_wsl():
|
||||
try:
|
||||
import subprocess
|
||||
# https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe
|
||||
# Ampersand (&) should be quoted
|
||||
exit_code = subprocess.call(
|
||||
['powershell.exe', '-NoProfile', '-Command', 'Start-Process "{}"'.format(uri)])
|
||||
browser_opened = exit_code == 0
|
||||
except FileNotFoundError: # WSL might be too old
|
||||
pass
|
||||
return browser_opened
|
|
@ -0,0 +1,235 @@
|
|||
*, *:after, *:before { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
/* Container styles */
|
||||
.container .button a {
|
||||
color: #31bc86;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.container .button a:hover, a:focus {
|
||||
color: #7c8d87;
|
||||
}
|
||||
|
||||
.container > header {
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
background: rgba(0,0,0,0.01);
|
||||
}
|
||||
|
||||
.container > header h1 {
|
||||
font-size: 2.625em;
|
||||
line-height: 1.3;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.container > header span {
|
||||
display: block;
|
||||
font-size: 45%;
|
||||
opacity: 0.6;
|
||||
padding-top: 0.5em
|
||||
}
|
||||
|
||||
/* Component styles */
|
||||
.component {
|
||||
line-height: 1.5em;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.component h3 {
|
||||
font-size: 1.11em;
|
||||
margin-block-start: 0em;
|
||||
margin-block-end: 0em;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 3em;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border: 1px solid black;
|
||||
padding: 0.75em 1.5em;
|
||||
text-align: left;
|
||||
}
|
||||
td.err {
|
||||
background-color: #e992b9;
|
||||
color: #fff;
|
||||
font-size: 0.75em;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #31bc86;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tbody th {
|
||||
background-color: #2ea879;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background-color: rgba(129,208,177,.3);
|
||||
}
|
||||
|
||||
/* circle styles */
|
||||
.detail {
|
||||
display: none;
|
||||
}
|
||||
.column-percentage:hover .detail {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.single-chart {
|
||||
width: 100%;
|
||||
justify-content: space-around ;
|
||||
}
|
||||
|
||||
.circular-chart {
|
||||
display: block;
|
||||
margin: 0px auto;
|
||||
max-width: 100%;
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
.circle-bg {
|
||||
fill: none;
|
||||
stroke: #eee;
|
||||
stroke-width: 3.8;
|
||||
}
|
||||
|
||||
.circle {
|
||||
fill: none;
|
||||
stroke-width: 2.8;
|
||||
stroke-linecap: round;
|
||||
animation: progress 1s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
0% {
|
||||
stroke-dasharray: 0 100;
|
||||
}
|
||||
}
|
||||
|
||||
.circular-chart.red .circle {
|
||||
stroke: #e71837;
|
||||
}
|
||||
|
||||
.circular-chart.orange .circle {
|
||||
stroke: #ff9f00;
|
||||
}
|
||||
|
||||
.circular-chart.green .circle {
|
||||
stroke: #4CC790;
|
||||
}
|
||||
|
||||
.circular-chart.blue .circle {
|
||||
stroke: #3c9ee5;
|
||||
}
|
||||
|
||||
.circular-chart.gold .circle {
|
||||
stroke: #ffd700;
|
||||
fill: #ffd700;
|
||||
}
|
||||
|
||||
.percentage {
|
||||
fill: #666;
|
||||
font-size: 0.6em;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
/* medal styles */
|
||||
.medal {
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.ribbon {
|
||||
width: 40px;
|
||||
height: 35px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ribbon:before,
|
||||
.ribbon:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 17.5px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.ribbon:before {
|
||||
right: 0;
|
||||
background: #30110E;
|
||||
transform: skew(-28deg);
|
||||
}
|
||||
|
||||
.ribbon:after {
|
||||
background: #EF7E76;
|
||||
transform: skew(28deg);
|
||||
}
|
||||
|
||||
.coin {
|
||||
border: 1px solid #CA5D3E;
|
||||
border-radius: 50%;
|
||||
background: #F0CD73;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
position: relative;
|
||||
margin: -7.5px auto 0 auto;
|
||||
box-shadow: 0px 0px 3px 0px #989898;
|
||||
}
|
||||
|
||||
.coin:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 17.5px;
|
||||
height: 17.5px;
|
||||
border-radius: inherit;
|
||||
box-shadow: 0 0 0 9px #D9B867;
|
||||
}
|
||||
|
||||
/* Buttons Style */
|
||||
.button {
|
||||
padding-top: 1em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.button a {
|
||||
display: inline-block;
|
||||
margin: 0.5em;
|
||||
padding: 0.7em 1.1em;
|
||||
outline: none;
|
||||
border: 2px solid #31bc86;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 700;
|
||||
color: #DB7093;
|
||||
}
|
||||
|
||||
.button a:hover,
|
||||
.button a.current-page,
|
||||
.button a.current-page:hover {
|
||||
border-color: #DB7093;
|
||||
color: #DB7093;
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
var tag=1;
|
||||
function sortNumberAS(a, b)
|
||||
{
|
||||
return a - b
|
||||
}
|
||||
function sortNumberDesc(a, b)
|
||||
{
|
||||
return b - a
|
||||
}
|
||||
function sortStrAS(a, b)
|
||||
{
|
||||
x = a.toUpperCase();
|
||||
y = b.toUpperCase();
|
||||
if (x < y) {
|
||||
return -1;
|
||||
}
|
||||
if (x > y) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
function sortStrDesc(a, b)
|
||||
{
|
||||
x = a.toUpperCase();
|
||||
y = b.toUpperCase();
|
||||
if (x < y) {
|
||||
return 1;
|
||||
}
|
||||
if (x > y) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
function sortTextAS(a, b)
|
||||
{
|
||||
if (a==='Not applicable'){
|
||||
a = -1
|
||||
} else if (a===''){
|
||||
a = 100
|
||||
} else {
|
||||
a = parseFloat(a.substr(0,a.length-1))
|
||||
}
|
||||
if (b==='N/A'){
|
||||
b = -1
|
||||
} else if (b===''){
|
||||
b = 100
|
||||
} else {
|
||||
b = parseFloat(b.substr(0,b.length-1))
|
||||
}
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
function sortTextDesc(a, b)
|
||||
{
|
||||
if (a==='N/A'){
|
||||
a = -1
|
||||
} else if (a===''){
|
||||
a = 100
|
||||
} else {
|
||||
a = parseFloat(a.substr(0,a.length-1))
|
||||
}
|
||||
if (b==='N/A'){
|
||||
b = -1
|
||||
} else if (b===''){
|
||||
b = 100
|
||||
} else {
|
||||
b = parseFloat(b.substr(0,b.length-1))
|
||||
}
|
||||
if (a < b) {
|
||||
return 1;
|
||||
}
|
||||
if (a > b) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
function SortTable(obj){
|
||||
var column=obj.id
|
||||
var tdModule=document.getElementsByName("td-module");
|
||||
var tdTested=document.getElementsByName("td-tested");
|
||||
var tdUntested=document.getElementsByName("td-untested");
|
||||
var tdPercentage=document.getElementsByName("td-percentage");
|
||||
var tdDetail=document.getElementsByClassName("detail")
|
||||
var tdReport=document.getElementsByName("td-report");
|
||||
var tdModuleArray=[];
|
||||
var tdTestedArray=[];
|
||||
var tdUntestedArray=[];
|
||||
var tdPercentageArray=[];
|
||||
var tdReportArray=[];
|
||||
for(var i=0;i<tdModule.length;i++){
|
||||
tdModuleArray.push(tdModule[i].innerHTML);
|
||||
}
|
||||
for(var i=0;i<tdTested.length;i++){
|
||||
tdTestedArray.push(tdTested[i].innerHTML);
|
||||
}
|
||||
for(var i=0;i<tdUntested.length;i++){
|
||||
tdUntestedArray.push(tdUntested[i].innerHTML);
|
||||
}
|
||||
for(var i=0;i<tdPercentage.length;i++){
|
||||
tdPercentageArray.push(tdPercentage[i].innerHTML);
|
||||
}
|
||||
for(var i=0;i<tdReport.length;i++){
|
||||
tdReportArray.push(tdReport[i].innerHTML);
|
||||
}
|
||||
var columnArray=[];
|
||||
for(var i=0;i<tdModule.length;i++){
|
||||
if(column==='th-module'){
|
||||
columnArray.push(tdModule[i].innerHTML);
|
||||
}else if(column==='th-tested'){
|
||||
columnArray.push(parseInt(tdTested[i].innerHTML));
|
||||
}else if(column==='th-untested'){
|
||||
columnArray.push(parseInt(tdUntested[i].innerHTML));
|
||||
}else if(column==='th-percentage'){
|
||||
columnArray.push(tdDetail[i].innerHTML)
|
||||
}else if(column==='th-report'){
|
||||
columnArray.push(tdReport[i].innerHTML);
|
||||
}
|
||||
}
|
||||
var orginArray=[];
|
||||
for(var i=0;i<columnArray.length;i++){
|
||||
orginArray.push(columnArray[i]);
|
||||
}
|
||||
var newArray = columnArray.slice(1,)
|
||||
if(obj.className=="as"){
|
||||
if(column==='th-tested' || column==='th-untested'){
|
||||
newArray.sort(sortNumberAS);
|
||||
}else if(column==='th-module' || column==='th-report'){
|
||||
newArray.sort(sortStrAS);
|
||||
}else{
|
||||
newArray.sort(sortTextAS);
|
||||
}
|
||||
obj.className="desc";
|
||||
}else{
|
||||
if(column==='th-tested' || column==='th-untested'){
|
||||
newArray.sort(sortNumberDesc);
|
||||
}else if(column==='th-module' || column==='th-report'){
|
||||
newArray.sort(sortStrDesc);
|
||||
}else{
|
||||
newArray.sort(sortTextDesc);
|
||||
}
|
||||
obj.className="as";
|
||||
}
|
||||
columnArray = $.merge([columnArray[0]], newArray);
|
||||
for(var i=1;i<columnArray.length;i++){
|
||||
for(var j=1;j<orginArray.length;j++){
|
||||
if(orginArray[j]==columnArray[i]){
|
||||
document.getElementsByName("td-module")[i].innerHTML=tdModuleArray[j];
|
||||
document.getElementsByName("td-tested")[i].innerHTML=tdTestedArray[j];
|
||||
document.getElementsByName("td-untested")[i].innerHTML=tdUntestedArray[j];
|
||||
document.getElementsByName("td-percentage")[i].innerHTML=tdPercentageArray[j];
|
||||
document.getElementsByName("td-report")[i].innerHTML=tdReportArray[j];
|
||||
orginArray[j]=null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 2.2 KiB |
|
@ -0,0 +1,35 @@
|
|||
{% macro render_percentage(detail, module) %}
|
||||
{% if module['color'] == 'N/A' -%}
|
||||
<td name="td-percentage" class="column-percentage">
|
||||
<div style="text-align: center">N/A</div>
|
||||
<div class="detail">Not applicable</div>
|
||||
</td>
|
||||
{% elif module['color'] != 'gold' -%}
|
||||
<td name="td-percentage" class="column-percentage">
|
||||
<div class="single-chart">
|
||||
<svg viewBox="0 0 36 36" class="circular-chart {{ module['color'] }}">
|
||||
<path class="circle-bg"
|
||||
d="M18 2.0845
|
||||
a 15.9155 15.9155 0 0 1 0 31.831
|
||||
a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
/>
|
||||
<path class="circle"
|
||||
stroke-dasharray="{{ module['percentage'] }}, 100"
|
||||
d="M18 2.0845
|
||||
a 15.9155 15.9155 0 0 1 0 31.831
|
||||
a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
/>
|
||||
<text x="18" y="20.35" class="percentage">{{ module['percentage'] }}%</text>
|
||||
</svg>
|
||||
<div class="detail">{{ detail }}</div>
|
||||
</div>
|
||||
</td>
|
||||
{% else -%}
|
||||
<td name="td-percentage" class="medal column-percentage">
|
||||
<div class="ribbon"></div>
|
||||
<div class="coin"></div>
|
||||
<div class="detail">100.000%</div>
|
||||
</td>
|
||||
{% endif -%}
|
||||
{% endmacro %}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
{% from "_macros.j2" import render_percentage %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>CLI {{ description }} Test Coverage</title>
|
||||
<link rel="stylesheet" type="text/css" href="component.css"/>
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<script type="text/javascript" src="http://code.jquery.com/jquery-1.12.4.min.js"></script>
|
||||
<script type="text/javascript" src="./component.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>CLI {{ description }} Test Coverage Report
|
||||
<span>Please scroll down to see the every module test coverage.<br>
|
||||
Any question please contact Azure Cli Team.</span>
|
||||
</h1>
|
||||
{% if enable_cli_own == true -%}
|
||||
<nav class="button">
|
||||
<a class="current-page" href="index.html">ALL</a>
|
||||
<a href="index2.html">CLI OWN</a>'
|
||||
</nav>
|
||||
{% else -%}
|
||||
<nav class="button">
|
||||
<a class="current-page" href="index.html">ALL</a>
|
||||
</nav>
|
||||
{% endif -%}
|
||||
</header>
|
||||
<div class="component">
|
||||
<h3>Date: {{ date }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="th-module" onclick="SortTable(this)" class="as">Module</th>
|
||||
<th id="th-tested" onclick="SortTable(this)" class="as">Tested</th>
|
||||
<th id="th-untested" onclick="SortTable(this)" class="as">Untested</th>
|
||||
<th id="th-percentage" onclick="SortTable(this)" class="as">Percentage</th>
|
||||
<th id="th-report" onclick="SortTable(this)" class="as">Reports</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td name="td-module">Total</td>
|
||||
<td name="td-tested">{{ Total[0] }}</td>
|
||||
<td name="td-untested">{{ Total[1] }}</td>
|
||||
{{ render_percentage(Total[2], Total[3]) }}
|
||||
<td name="td-report">N/A</td>
|
||||
</tr>
|
||||
{% for module, cov in command_test_coverage.items() %}
|
||||
<tr>
|
||||
<td name="td-module">{{ module }}</td>
|
||||
<td name="td-tested">{{ cov[0] }}</td>
|
||||
<td name="td-untested">{{ cov[1] }}</td>
|
||||
{{ render_percentage(cov[2], cov[3]) }}
|
||||
<td name="td-report"><a href="{{ module }}.html">{{ module }} test coverage report</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="contact">This is the {{ description.lower() }} test coverage report of CLI.<br>
|
||||
Any question please contact Azure Cli Team.</p>
|
||||
</div>
|
||||
</div><!-- /container -->
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,60 @@
|
|||
{% from "_macros.j2" import render_percentage %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>CLI Own {{ description }} Test Coverage</title>
|
||||
<link rel="stylesheet" type="text/css" href="component.css"/>
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<script type="text/javascript" src="http://code.jquery.com/jquery-1.12.4.min.js"></script>
|
||||
<script type="text/javascript" src="./component.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>CLI Own {{ description }} Test Coverage Report
|
||||
<span>Please scroll down to see the every module test coverage.<br>
|
||||
Any question please contact Azure Cli Team.</span>
|
||||
</h1>
|
||||
<nav class="button">
|
||||
<a href="index.html">ALL</a>
|
||||
<a class="current-page" href="index2.html">CLI OWN</a>'
|
||||
</nav>
|
||||
</header>
|
||||
<div class="component">
|
||||
<h3>Date: {{ date }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="th-module" onclick="SortTable(this)" class="as">Module</th>
|
||||
<th id="th-tested" onclick="SortTable(this)" class="as">Tested</th>
|
||||
<th id="th-untested" onclick="SortTable(this)" class="as">Untested</th>
|
||||
<th id="th-percentage" onclick="SortTable(this)" class="as">Percentage</th>
|
||||
<th id="th-report" onclick="SortTable(this)" class="as">Reports</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td name="td-module">Total</td>
|
||||
<td name="td-tested">{{ Total[0] }}</td>
|
||||
<td name="td-untested">{{ Total[1] }}</td>
|
||||
{{ render_percentage(Total[2], Total[3]) }}
|
||||
<td name="td-report">N/A</td>
|
||||
</tr>
|
||||
{% for module, cov in command_test_coverage.items() %}
|
||||
<tr>
|
||||
<td name="td-module">{{ module }}</td>
|
||||
<td name="td-tested">{{ cov[0] }}</td>
|
||||
<td name="td-untested">{{ cov[1] }}</td>
|
||||
{{ render_percentage(cov[2], cov[3]) }}
|
||||
<td name="td-report"><a href="{{ module }}.html">{{ module }} test coverage report</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="contact">This is the {{ description.lower() }} test coverage report of CLI Own.<br>
|
||||
Any question please contact Azure Cli Team.</p>
|
||||
</div>
|
||||
</div><!-- /container -->
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,52 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ module }} Test Coverage Detail</title>
|
||||
<link rel="stylesheet" type="text/css" href="component.css"/>
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>{{ module }} Test Coverage Report
|
||||
<span>This is the test coverage report of {{ module }}. Please scroll down to see the untested details.<br>
|
||||
Any question please contact Azure Cli Team.</span>
|
||||
</h1>
|
||||
{% if enable_cli_own == true -%}
|
||||
<nav class="button">
|
||||
<a class="current-page" href="index.html">ALL</a>
|
||||
<a href="index2.html">CLI OWN</a>
|
||||
</nav>
|
||||
{% else -%}
|
||||
<nav class="button">
|
||||
<a href="index.html">ALL</a>
|
||||
</nav>
|
||||
{% endif -%}
|
||||
</header>
|
||||
<div class="component">
|
||||
<h3>Date: {{ date }}</h3>
|
||||
<h3>Tested: {{ coverage[0] }}, Untested: {{ coverage[1] }}, Percentage: {{ coverage[2] }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Module</th>
|
||||
<th>Untested</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cmd in untested_commands %}
|
||||
<tr>
|
||||
<td>{{ module }}</td>
|
||||
<td>{{ cmd }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="contact">This is the test coverage report of {{ module }} module.<br>
|
||||
Any question please contact Azure Cli Team.<br>
|
||||
</p>
|
||||
</div>
|
||||
</div><!-- /container -->
|
||||
</body>
|
||||
</html>
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -152,7 +152,10 @@ def run_linter(modules=None, rule_types=None, rules=None, ci_exclusions=None,
|
|||
rule_inclusions=rules,
|
||||
use_ci_exclusions=ci_exclusions,
|
||||
min_severity=min_severity,
|
||||
update_global_exclusion=update_global_exclusion)
|
||||
update_global_exclusion=update_global_exclusion,
|
||||
git_source=git_source,
|
||||
git_target=git_target,
|
||||
git_repo=git_repo)
|
||||
|
||||
subheading('Results')
|
||||
logger.info('Running linter: %i commands, %i help entries',
|
||||
|
@ -161,9 +164,12 @@ def run_linter(modules=None, rule_types=None, rules=None, ci_exclusions=None,
|
|||
run_params=not rule_types or 'params' in rule_types,
|
||||
run_commands=not rule_types or 'commands' in rule_types,
|
||||
run_command_groups=not rule_types or 'command_groups' in rule_types,
|
||||
run_help_files_entries=not rule_types or 'help_entries' in rule_types)
|
||||
run_help_files_entries=not rule_types or 'help_entries' in rule_types,
|
||||
run_command_test_coverage=not rule_types or 'command_test_coverage' in rule_types,
|
||||
)
|
||||
display(os.linesep + 'Run custom pylint rules.')
|
||||
exit_code += pylint_rules(selected_modules)
|
||||
print(exit_code)
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
|
|
|
@ -4,18 +4,26 @@
|
|||
# license information.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import inspect
|
||||
from importlib import import_module
|
||||
from pkgutil import iter_modules
|
||||
from difflib import context_diff
|
||||
from enum import Enum
|
||||
from importlib import import_module
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
from pkgutil import iter_modules
|
||||
import yaml
|
||||
from knack.log import get_logger
|
||||
|
||||
from azdev.operations.regex import (
|
||||
get_all_tested_commands_from_regex,
|
||||
search_argument,
|
||||
search_argument_context,
|
||||
search_command,
|
||||
search_command_group)
|
||||
from azdev.utilities import diff_branches_detail
|
||||
from azdev.utilities.path import get_cli_repo_path, get_ext_repo_paths
|
||||
from .util import share_element, exclude_commands, LinterError
|
||||
|
||||
|
||||
PACKAGE_NAME = 'azdev.operations.linter'
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
|
@ -37,8 +45,9 @@ class LinterSeverity(Enum):
|
|||
return sorted(LinterSeverity, key=lambda sev: sev.value)
|
||||
|
||||
|
||||
class Linter: # pylint: disable=too-many-public-methods
|
||||
def __init__(self, command_loader=None, help_file_entries=None, loaded_help=None):
|
||||
class Linter: # pylint: disable=too-many-public-methods, too-many-instance-attributes
|
||||
def __init__(self, command_loader=None, help_file_entries=None, loaded_help=None, git_source=None, git_target=None,
|
||||
git_repo=None, exclusions=None):
|
||||
self._all_yaml_help = help_file_entries
|
||||
self._loaded_help = loaded_help
|
||||
self._command_loader = command_loader
|
||||
|
@ -50,6 +59,10 @@ class Linter: # pylint: disable=too-many-public-methods
|
|||
self._parameters[command_name] = set()
|
||||
for name in command.arguments:
|
||||
self._parameters[command_name].add(name)
|
||||
self.git_source = git_source
|
||||
self.git_target = git_target
|
||||
self.git_repo = git_repo
|
||||
self.exclusions = exclusions
|
||||
|
||||
@property
|
||||
def commands(self):
|
||||
|
@ -180,19 +193,163 @@ class Linter: # pylint: disable=too-many-public-methods
|
|||
return help_entry.short_summary or help_entry.long_summary
|
||||
return help_entry
|
||||
|
||||
def get_command_test_coverage(self):
|
||||
diff_index = diff_branches_detail(repo=self.git_repo, target=self.git_target, source=self.git_source)
|
||||
commands, parameters = self._detect_new_command(diff_index)
|
||||
all_tested_command = self._detect_tested_command(diff_index)
|
||||
return self._run_command_test_coverage(commands, parameters, all_tested_command)
|
||||
|
||||
# pylint: disable=too-many-locals, too-many-nested-blocks, too-many-branches, too-many-statements
|
||||
def _detect_new_command(self, diff_index):
|
||||
"""
|
||||
exclude_comands: List[str]
|
||||
exclude_parameters: List[tuple[str, str]]
|
||||
commands: List[str]
|
||||
parameters: List[str, List[str]]
|
||||
"""
|
||||
YELLOW = '\x1b[33m'
|
||||
parameters = []
|
||||
commands = []
|
||||
lines = []
|
||||
exclude_comands = []
|
||||
exclude_parameters = []
|
||||
for c, v in self.exclusions.items():
|
||||
if 'parameters' in v:
|
||||
for p, r in v['parameters'].items():
|
||||
if 'missing_parameter_test_coverage' in r['rule_exclusions']:
|
||||
exclude_parameters.append((c, p))
|
||||
elif 'rule_exclusions' in v:
|
||||
if 'missing_command_test_coverage' in v['rule_exclusions']:
|
||||
exclude_comands.append(c)
|
||||
_logger.debug('exclude_parameters: %s', exclude_parameters)
|
||||
_logger.debug('exclude_comands: %s', exclude_comands)
|
||||
|
||||
for diff in diff_index:
|
||||
filename = diff.a_path.split('/')[-1]
|
||||
if 'params' in filename or 'commands' in filename:
|
||||
lines = list(
|
||||
context_diff(diff.a_blob.data_stream.read().decode("utf-8").splitlines(True) if diff.a_blob else [],
|
||||
diff.b_blob.data_stream.read().decode("utf-8").splitlines(True) if diff.b_blob else [],
|
||||
'Original', 'Current'))
|
||||
for row_num, line in enumerate(lines):
|
||||
if 'params.py' in filename:
|
||||
params, param_name = search_argument(line)
|
||||
if params:
|
||||
offset = -1
|
||||
while row_num > 0:
|
||||
row_num -= 1
|
||||
# Match row num '--- 156,163 ----'
|
||||
sub_pattern = r'--- (\d{0,}),(?:\d{0,}) ----'
|
||||
idx = re.findall(sub_pattern, lines[row_num])
|
||||
offset += 1
|
||||
if idx:
|
||||
idx = int(idx[0]) + offset
|
||||
break
|
||||
with open(os.path.join(get_cli_repo_path(), diff.a_path), encoding='utf-8') as f:
|
||||
param_lines = f.readlines()
|
||||
cmds = search_argument_context(idx, param_lines)
|
||||
for cmd in cmds:
|
||||
if cmd not in exclude_comands and \
|
||||
not list(filter(lambda x, c=cmd, p=param_name: c in x[0] and p in x[1], exclude_parameters)): # pylint: disable=line-too-long
|
||||
parameters.append([cmd, params])
|
||||
else:
|
||||
print('%sCommand [%s, %s] not test and exclude in linter_exclusions.yml' % (
|
||||
YELLOW, cmd, params))
|
||||
|
||||
if 'commands.py' in filename:
|
||||
command = search_command(line)
|
||||
if command:
|
||||
offset = -1
|
||||
while row_num > 0:
|
||||
row_num -= 1
|
||||
# Match row num '--- 156,163 ----'
|
||||
sub_pattern = r'--- (\d{0,}),(?:\d{0,}) ----'
|
||||
idx = re.findall(sub_pattern, lines[row_num])
|
||||
offset += 1
|
||||
if idx:
|
||||
idx = int(idx[0]) + offset
|
||||
break
|
||||
with open(os.path.join(get_cli_repo_path(), diff.a_path), encoding='utf-8') as f:
|
||||
cmd_lines = f.readlines()
|
||||
cmd = search_command_group(idx, cmd_lines, command)
|
||||
if cmd:
|
||||
if cmd in exclude_comands:
|
||||
print('%sCommand %s not test and exclude in linter_exclusions.yml' % (YELLOW, cmd))
|
||||
else:
|
||||
commands.append(cmd)
|
||||
_logger.debug('New add parameters: %s', parameters)
|
||||
_logger.debug('New add commands: %s', commands)
|
||||
return commands, parameters
|
||||
|
||||
def _detect_tested_command(self, diff_index):
|
||||
all_tested_command = []
|
||||
# get tested command by regex
|
||||
for diff in diff_index:
|
||||
filename = diff.a_path.split('/')[-1]
|
||||
if re.findall(r'test_.*.py', filename) and os.path.exists(os.path.join(get_cli_repo_path(), diff.a_path)):
|
||||
with open(os.path.join(self.git_repo, diff.a_path), encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
ref = get_all_tested_commands_from_regex(lines)
|
||||
all_tested_command += ref
|
||||
# get tested command by recording file
|
||||
if re.findall(r'test_.*.yaml', filename) and os.path.exists(os.path.join(get_cli_repo_path(), diff.a_path)):
|
||||
with open(os.path.join(self.git_repo, diff.a_path)) as f:
|
||||
records = yaml.load(f, Loader=yaml.Loader) or {}
|
||||
for record in records['interactions']:
|
||||
# parse command ['acr agentpool create']
|
||||
command = record['request']['headers'].get('CommandName', [''])[0]
|
||||
# parse argument ['-n -r']
|
||||
argument = record['request']['headers'].get('ParameterSetName', [''])[0]
|
||||
if command or argument:
|
||||
all_tested_command.append(command + ' ' + argument)
|
||||
_logger.debug('All tested command: %s', all_tested_command)
|
||||
return all_tested_command
|
||||
|
||||
@staticmethod
|
||||
def _run_command_test_coverage(commands, parameters, all_tested_command):
|
||||
flag = False
|
||||
exec_state = True
|
||||
for command, opt_list in parameters:
|
||||
for opt in opt_list:
|
||||
for code in all_tested_command:
|
||||
if command in code and opt in code:
|
||||
_logger.debug("Find '%s' test case in '%s'", command + ' ' + opt, code)
|
||||
flag = True
|
||||
break
|
||||
else:
|
||||
_logger.error("Can not find '%s' test case", command + ' ' + opt)
|
||||
_logger.error("Please add some scenario tests for the new parameter")
|
||||
_logger.error(
|
||||
"Or add the parameter with missing_parameter_test_coverage rule in linter_exclusions.yml")
|
||||
exec_state = False
|
||||
if flag:
|
||||
break
|
||||
for command in commands:
|
||||
for code in all_tested_command:
|
||||
if command in code:
|
||||
_logger.debug("Find '%s' test case in '%s'", command, code)
|
||||
break
|
||||
else:
|
||||
_logger.error("Can not find '%s' test case", command)
|
||||
_logger.error("Please add some scenario tests for the new command")
|
||||
_logger.error("Or add the command with missing_command_test_coverage rule in linter_exclusions.yml")
|
||||
exec_state = False
|
||||
return exec_state
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class LinterManager:
|
||||
|
||||
_RULE_TYPES = {'help_file_entries', 'command_groups', 'commands', 'params'}
|
||||
_RULE_TYPES = {'help_file_entries', 'command_groups', 'commands', 'params', 'command_test_coverage'}
|
||||
|
||||
def __init__(self, command_loader=None, help_file_entries=None, loaded_help=None, exclusions=None,
|
||||
rule_inclusions=None, use_ci_exclusions=None, min_severity=None, update_global_exclusion=None):
|
||||
rule_inclusions=None, use_ci_exclusions=None, min_severity=None, update_global_exclusion=None,
|
||||
git_source=None, git_target=None, git_repo=None):
|
||||
# default to running only rules of the highest severity
|
||||
self.min_severity = min_severity or LinterSeverity.get_ordered_members()[-1]
|
||||
self.linter = Linter(command_loader=command_loader, help_file_entries=help_file_entries,
|
||||
loaded_help=loaded_help)
|
||||
self._exclusions = exclusions or {}
|
||||
self.linter = Linter(command_loader=command_loader, help_file_entries=help_file_entries,
|
||||
loaded_help=loaded_help, git_source=git_source, git_target=git_target, git_repo=git_repo,
|
||||
exclusions=self._exclusions)
|
||||
self._rules = {rule_type: {} for rule_type in LinterManager._RULE_TYPES} # initialize empty rules
|
||||
self._ci_exclusions = {}
|
||||
self._rule_inclusions = rule_inclusions
|
||||
|
@ -234,7 +391,8 @@ class LinterManager:
|
|||
def exit_code(self):
|
||||
return self._exit_code
|
||||
|
||||
def run(self, run_params=None, run_commands=None, run_command_groups=None, run_help_files_entries=None):
|
||||
def run(self, run_params=None, run_commands=None, run_command_groups=None,
|
||||
run_help_files_entries=None, run_command_test_coverage=None):
|
||||
paths = import_module('{}.rules'.format(PACKAGE_NAME)).__path__
|
||||
|
||||
if paths:
|
||||
|
@ -256,9 +414,11 @@ class LinterManager:
|
|||
|
||||
# run all rule-checks
|
||||
if run_help_files_entries and self._rules.get('help_file_entries'):
|
||||
# print('help_file_entries')
|
||||
self._run_rules('help_file_entries')
|
||||
|
||||
if run_command_groups and self._rules.get('command_groups'):
|
||||
# print('command_groups')
|
||||
self._run_rules('command_groups')
|
||||
|
||||
if run_commands and self._rules.get('commands'):
|
||||
|
@ -267,6 +427,9 @@ class LinterManager:
|
|||
if run_params and self._rules.get('params'):
|
||||
self._run_rules('params')
|
||||
|
||||
if run_command_test_coverage and self._rules.get('command_test_coverage'):
|
||||
self._run_rules('command_test_coverage')
|
||||
|
||||
if not self.exit_code:
|
||||
print(os.linesep + 'No violations found for linter rules.')
|
||||
|
||||
|
@ -297,13 +460,18 @@ class LinterManager:
|
|||
YELLOW = '\x1b[33m'
|
||||
CYAN = '\x1b[36m'
|
||||
RESET = '\x1b[39m'
|
||||
# print('enter _run_rules')
|
||||
for rule_name, (rule_func, linter_callable, rule_severity) in self._rules.get(rule_group).items():
|
||||
# print('enter_items')
|
||||
severity_str = rule_severity.name
|
||||
# use new linter if needed
|
||||
with LinterScope(self, linter_callable):
|
||||
# print('enter_with')
|
||||
# if the rule's severity is lower than the linter's severity skip it.
|
||||
if self._linter_severity_is_applicable(rule_severity, rule_name):
|
||||
# print('enter violations', rule_func)
|
||||
violations = sorted(rule_func()) or []
|
||||
# print('enter to find')
|
||||
if violations:
|
||||
if rule_severity == LinterSeverity.HIGH:
|
||||
sev_color = RED
|
||||
|
@ -314,13 +482,14 @@ class LinterManager:
|
|||
|
||||
# pylint: disable=duplicate-string-formatting-argument
|
||||
print('- {} FAIL{} - {}{}{} severity: {}'.format(RED, RESET, sev_color,
|
||||
severity_str, RESET, rule_name,))
|
||||
severity_str, RESET, rule_name, ))
|
||||
for violation_msg, entity_name, name in violations:
|
||||
print(violation_msg)
|
||||
self._save_violations(entity_name, name)
|
||||
print()
|
||||
else:
|
||||
print('- {} pass{}: {} '.format(GREEN, RESET, rule_name))
|
||||
# print('enter_end')
|
||||
|
||||
def _linter_severity_is_applicable(self, rule_severity, rule_name):
|
||||
if self.min_severity.value > rule_severity.value:
|
||||
|
@ -336,7 +505,9 @@ class LinterManager:
|
|||
self._violiations.setdefault(command_name, {}).setdefault('rule_exclusions', []).append(rule_name)
|
||||
else:
|
||||
command_name, param_name = entity_name
|
||||
self._violiations.setdefault(command_name, {}).setdefault('parameters', {}).setdefault(param_name, {}).setdefault('rule_exclusions', []).append(rule_name)
|
||||
self._violiations.setdefault(command_name, {}).setdefault('parameters', {}).setdefault(param_name,
|
||||
{}).setdefault(
|
||||
'rule_exclusions', []).append(rule_name)
|
||||
|
||||
|
||||
class RuleError(Exception):
|
||||
|
@ -351,6 +522,7 @@ class LinterScope:
|
|||
Linter Context manager. used when calling a rule function. Allows substitution of main linter for a linter
|
||||
that takes into account any applicable exclusions, if applicable.
|
||||
"""
|
||||
|
||||
def __init__(self, linter_manager, linter_callable):
|
||||
self.linter_manager = linter_manager
|
||||
self.linter = linter_callable()
|
||||
|
|
|
@ -17,6 +17,28 @@ class BaseRule:
|
|||
self.severity = severity
|
||||
|
||||
|
||||
# command_test_rule run once
|
||||
class CommandCoverageRule(BaseRule):
|
||||
|
||||
def __call__(self, func):
|
||||
def add_to_linter(linter_manager):
|
||||
def wrapper():
|
||||
linter = linter_manager.linter
|
||||
try:
|
||||
func(linter)
|
||||
except RuleError as ex:
|
||||
linter_manager.mark_rule_failure(self.severity)
|
||||
yield (_create_violation_msg(ex, 'repo: {}, src: {}, tgt: {}',
|
||||
linter.git_repo, linter.git_source, linter.git_target),
|
||||
(linter.git_source, linter.git_target),
|
||||
func.__name__)
|
||||
|
||||
linter_manager.add_rule('command_test_coverage', func.__name__, wrapper, self.severity)
|
||||
|
||||
add_to_linter.linter_rule = True
|
||||
return add_to_linter
|
||||
|
||||
|
||||
# help_file_entry_rule
|
||||
class HelpFileEntryRule(BaseRule):
|
||||
|
||||
|
@ -68,7 +90,7 @@ def _get_decorator(func, rule_group, print_format, severity):
|
|||
def add_to_linter(linter_manager):
|
||||
def wrapper():
|
||||
linter = linter_manager.linter
|
||||
|
||||
# print('enter add to linter', len(getattr(linter, rule_group)))
|
||||
for iter_entity in getattr(linter, rule_group):
|
||||
exclusions = linter_manager.exclusions.get(iter_entity, {}).get('rule_exclusions', [])
|
||||
if func.__name__ not in exclusions:
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# -----------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
from ..rule_decorators import CommandCoverageRule
|
||||
from ..linter import RuleError, LinterSeverity
|
||||
|
||||
|
||||
@CommandCoverageRule(LinterSeverity.HIGH)
|
||||
def missing_command_test_coverage(linter):
|
||||
if not linter.get_command_test_coverage():
|
||||
raise RuleError('Missing Command Test Coverage')
|
|
@ -70,6 +70,7 @@ def faulty_help_example_rule(linter, help_entry):
|
|||
|
||||
@HelpFileEntryRule(LinterSeverity.HIGH)
|
||||
def faulty_help_example_parameters_rule(linter, help_entry):
|
||||
# print(linter, help_entry)
|
||||
parser = linter.command_parser
|
||||
violations = []
|
||||
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
# -----------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
|
||||
from knack.log import get_logger
|
||||
from knack.util import CLIError
|
||||
from azdev.utilities.path import get_cli_repo_path
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
try:
|
||||
with open(os.path.join(get_cli_repo_path(), 'scripts', 'ci', 'cmdcov.yml'), 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
CMD_PATTERN = config['CMD_PATTERN']
|
||||
QUO_PATTERN = config['QUO_PATTERN']
|
||||
END_PATTERN = config['END_PATTERN']
|
||||
DOCS_END_PATTERN = config['DOCS_END_PATTERN']
|
||||
NOT_END_PATTERN = config['NOT_END_PATTERN']
|
||||
NUMBER_SIGN_PATTERN = config['NUMBER_SIGN_PATTERN']
|
||||
except CLIError as ex:
|
||||
logger.warning('Failed to load cmdcov.yml: %s', ex)
|
||||
|
||||
|
||||
def get_all_tested_commands_from_regex(lines):
|
||||
"""
|
||||
get all tested commands from test_*.py
|
||||
"""
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
ref = []
|
||||
total_lines = len(lines)
|
||||
row_num = 0
|
||||
count = 1
|
||||
while row_num < total_lines:
|
||||
re_idx = None
|
||||
if re.findall(NUMBER_SIGN_PATTERN, lines[row_num]):
|
||||
row_num += 1
|
||||
continue
|
||||
if re.findall(CMD_PATTERN[0], lines[row_num]):
|
||||
re_idx = 0
|
||||
if re_idx is None and re.findall(CMD_PATTERN[1], lines[row_num]):
|
||||
re_idx = 1
|
||||
if re_idx is None and re.findall(CMD_PATTERN[2], lines[row_num]):
|
||||
re_idx = 2
|
||||
if re_idx is None and re.findall(CMD_PATTERN[3], lines[row_num]):
|
||||
re_idx = 3
|
||||
if re_idx is not None:
|
||||
command = re.findall(CMD_PATTERN[re_idx], lines[row_num])[0]
|
||||
while row_num < total_lines:
|
||||
if (re_idx in [0, 1] and not re.findall(END_PATTERN, lines[row_num])) or \
|
||||
(re_idx == 2 and (row_num + 1) < total_lines and
|
||||
re.findall(NOT_END_PATTERN, lines[row_num + 1])):
|
||||
row_num += 1
|
||||
cmd = re.findall(QUO_PATTERN, lines[row_num])
|
||||
if cmd:
|
||||
command += cmd[0][1]
|
||||
elif re_idx == 3 and (row_num + 1) < total_lines \
|
||||
and not re.findall(DOCS_END_PATTERN, lines[row_num]):
|
||||
row_num += 1
|
||||
command += lines[row_num][:-1]
|
||||
else:
|
||||
command = command + ' ' + str(count)
|
||||
ref.append(command)
|
||||
row_num += 1
|
||||
count += 1
|
||||
break
|
||||
else:
|
||||
command = command + ' ' + str(count)
|
||||
ref.append(command)
|
||||
row_num += 1
|
||||
count += 1
|
||||
break
|
||||
else:
|
||||
row_num += 1
|
||||
return ref
|
||||
|
||||
|
||||
def search_argument_context(row_num, lines):
|
||||
cmds = []
|
||||
while row_num > 0:
|
||||
row_num -= 1
|
||||
# Match `with self.argument_context('') as c:`
|
||||
sub_pattern0 = r'with self.argument_context\(\'(.*?)\'[\),]'
|
||||
# Match `with self.argument_context(scope) as c:`
|
||||
sub_pattern1 = r'with self.argument_context\(scope[\),]'
|
||||
# Match `with self.argument_context(\'{} stop\'.format(scope)) as c:',
|
||||
sub_pattern2 = r'with self.argument_context\(\'(.*)\'.format\(scope\)\)'
|
||||
ref0 = re.findall(sub_pattern0, lines[row_num])
|
||||
ref1 = re.findall(sub_pattern1, lines[row_num])
|
||||
ref2 = re.findall(sub_pattern2, lines[row_num])
|
||||
# Match `with self.argument_context('') as c:`
|
||||
if ref0:
|
||||
cmds = ref0
|
||||
break
|
||||
# Match `with self.argument_context(scope) as c:`
|
||||
if ref1:
|
||||
sub_pattern = r'for scope in (.*):'
|
||||
cmds = json.loads(
|
||||
re.findall(sub_pattern, lines[row_num - 1])[0].replace('\'', '"'))
|
||||
break
|
||||
# Match `with self.argument_context(\'{} stop\'.format(scope)) as c:',
|
||||
if ref2:
|
||||
sub_pattern = r'for scope in (.*):'
|
||||
format_strings = json.loads(
|
||||
re.findall(sub_pattern, lines[row_num - 1])[0].replace('\'', '"'))
|
||||
for c in ref2:
|
||||
for f in format_strings:
|
||||
cmds.append(c.replace('{}', f))
|
||||
break
|
||||
return cmds
|
||||
|
||||
|
||||
def search_argument(line):
|
||||
params = []
|
||||
param_name = ''
|
||||
# Match ` + c.argument('xxx')?`
|
||||
pattern = r'\+\s+c.argument\((.*)\)?'
|
||||
ref = re.findall(pattern, line)
|
||||
if ref:
|
||||
# strip ' and \' and )
|
||||
param_name = ref[0].split(',')[0].strip(r"'\'\)")
|
||||
if 'options_list' in ref[0]:
|
||||
# Match ` options_list=xxx, or options_list=xxx)`
|
||||
sub_pattern = r'options_list=\[(.*?)\]'
|
||||
params = re.findall(sub_pattern, ref[0])[0].replace('\'', '').replace('"', '').split()
|
||||
else:
|
||||
# if options_list not exist, generate by parameter name
|
||||
params = ['--' + param_name.replace('_', '-')]
|
||||
return params, param_name
|
||||
|
||||
|
||||
def search_command_group(row_num, lines, command):
|
||||
cmd = ''
|
||||
while row_num > 0:
|
||||
row_num -= 1
|
||||
# Match `with self.command_group('local-context',`
|
||||
sub_pattern = r'with self.command_group\(\'(.*?)\','
|
||||
group = re.findall(sub_pattern, lines[row_num])
|
||||
if group:
|
||||
cmd = group[0] + ' ' + command
|
||||
break
|
||||
return cmd
|
||||
|
||||
|
||||
def search_command(line):
|
||||
command = ''
|
||||
# Match `+ g.*command(xxx)`
|
||||
pattern = r'\+\s+g.(?:\w+)?command\((.*)\)'
|
||||
ref = re.findall(pattern, line)
|
||||
if ref:
|
||||
command = ref[0].split(',')[0].strip("'")
|
||||
return command
|
|
@ -0,0 +1,277 @@
|
|||
# -----------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for
|
||||
# license information.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
from pprint import pprint
|
||||
from azdev.operations.regex import (
|
||||
get_all_tested_commands_from_regex,
|
||||
search_argument,
|
||||
search_argument_context,
|
||||
search_command,
|
||||
search_command_group)
|
||||
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
# one line test
|
||||
def test_one_line_regex():
|
||||
lines = [
|
||||
# start with self.cmd.
|
||||
'self.cmd(\'image builder create -n {tmpl_02} -g {rg} --identity {ide} --scripts {script} --image-source {img_src} --build-timeout 22\')\n',
|
||||
# start with blanks, match self.cmd("").
|
||||
' self.cmd("role assignment create --assignee {assignee} --role {role} --scope {scope}")\n',
|
||||
# start with blanks, match self.cmd('').
|
||||
' self.cmd(\'role assignment create --assignee {assignee} --role {role} --scope {scope}\')\n',
|
||||
# start with multiple blanks, characters, self.cmd
|
||||
' identity_id = self.cmd(\'identity create -g {rg} -n {ide}\').get_output_in_json()[\'clientId\']\n',
|
||||
# start with blanks, match self.cmd, use fstring, ''
|
||||
' self.cmd(f\'afd profile usage -g {resource_group} --profile-name {profile_name}\', checks=usage_checks)',
|
||||
# start with blanks, match self.cmd, use fstring, ""
|
||||
' self.cmd(f"afd profile usage -g {resource_group} --profile-name {profile_name}", checks=usage_checks)',
|
||||
# one line docstring '''
|
||||
' self.cmd("""afd profile usage -g {resource_group} --profile-name {profile_name}""", checks=usage_checks)',
|
||||
# one line docstring """
|
||||
' self.cmd("""afd profile usage -g {resource_group} --profile-name {profile_name}""", checks=usage_checks)',
|
||||
# .format
|
||||
' self.cmd("afd profile usage -g {} --profile-name {}".format(group, name)',
|
||||
# %s
|
||||
' self.cmd("afd profile usage -g %s --profile-name %s", group, name)',
|
||||
# end with hashtag, should match.
|
||||
' self.cmd(f"afd profile usage -g {resource_group} --profile-name {profile_name}", checks=usage_checks) # xxx',
|
||||
# start with hashtag, shouldn't match.
|
||||
' # self.cmd(f"afd profile usage -g {resource_group} --profile-name {profile_name}", checks=usage_checks)',
|
||||
# start with blanks, match *_cmd = ''.
|
||||
' stop_cmd = \'aks stop --resource-group={resource_group} --name={name}\'\n',
|
||||
# start with blanks, match *_cmd = "".
|
||||
' enable_cmd = "aks enable-addons --addons confcom --resource-group={resource_group} --name={name} -o json"\n',
|
||||
# start with blanks, match *_cmd = f''.
|
||||
' disable_cmd = f\'aks disable-addons --addons confcom --resource-group={resource_group} --name={name} -o json\'\n',
|
||||
# start with blanks, match *_cmd = f"".
|
||||
' browse_cmd = f"aks browse --resource-group={resource_group} --name={name} --listen-address=127.0.0.1 --listen-port=8080 --disable-browser"\n',
|
||||
]
|
||||
ref = get_all_tested_commands_from_regex(lines)
|
||||
pprint(ref, width=1000)
|
||||
assert len(ref) == 15
|
||||
|
||||
|
||||
# multiple lines test
|
||||
def test_multiple_lines_regex():
|
||||
lines = [
|
||||
# start with blanks, self.cmd, one cmd line, multiple checks.
|
||||
' self.cmd(\'aks list -g {resource_group}\', checks=[\n',
|
||||
' self.check(\'[0].type\', \'{resource_type}\'),\n',
|
||||
' StringContainCheck(aks_name),\n',
|
||||
' StringContainCheck(resource_group)\n',
|
||||
' ])\n',
|
||||
# start with blanks, self.cmd, multiple cmd lines, multiple checks.
|
||||
' self.cmd(\'image builder create -n {tmpl_02} -g {rg} --identity {ide} --scripts {script} --image-source {img_src} --build-timeout 22\'\n',
|
||||
' \' --managed-image-destinations img_1=westus \' + out_3,\n',
|
||||
' checks=[\n',
|
||||
' self.check(\'name\', \'{tmpl_02}\'), self.check(\'provisioningState\', \'Succeeded\'),\n',
|
||||
' self.check(\'length(distribute)\', 2),\n',
|
||||
' self.check(\'distribute[0].imageId\', \'/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/images/img_1\'),\n',
|
||||
' self.check(\'distribute[0].location\', \'westus\'),\n',
|
||||
' self.check(\'distribute[0].runOutputName\', \'img_1\'),\n',
|
||||
' self.check(\'distribute[0].type\', \'ManagedImage\'),\n',
|
||||
' self.check(\'distribute[1].imageId\', \'/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/images/img_2\'),\n',
|
||||
' self.check(\'distribute[1].location\', \'centralus\'),\n',
|
||||
' self.check(\'distribute[1].runOutputName\', \'img_2\'),\n',
|
||||
' self.check(\'distribute[1].type\', \'ManagedImage\'),\n',
|
||||
' self.check(\'buildTimeoutInMinutes\', 22)\n',
|
||||
' ])\n',
|
||||
# start with blanks, characters, self.cmd, but have line break at self.cmd(
|
||||
' ipprefix_id = self.cmd(\n',
|
||||
' \'az network public-ip prefix create -g {rg} -n {ipprefix_name} --location {location} --length 29\'). \\n',
|
||||
' get_output_in_json().get("id")\n',
|
||||
# start with blanks, match *_cmd = '', multiple lines.
|
||||
' create_cmd = \'aks create -g {resource_group} -n {name} -p {dns_name_prefix} --ssh-key-value {ssh_key_value} \' \\n',
|
||||
' \'-l {location} --service-principal {service_principal} --client-secret {client_secret} -k {k8s_version} \' \\n',
|
||||
' \'--node-vm-size {vm_size} \' \\n',
|
||||
' \'--tags scenario_test -c 1 --no-wait\'\n',
|
||||
' update_cmd = \'aks update --resource-group={resource_group} --name={name} \' \\n',
|
||||
' \'--aad-admin-group-object-ids 00000000-0000-0000-0000-000000000002 \' \\n',
|
||||
' \'--aad-tenant-id 00000000-0000-0000-0000-000000000003 -o json\'\n',
|
||||
' enable_autoscaler_cmd = \'aks update --resource-group={resource_group} --name={name} \' \\n',
|
||||
' \'--tags {tags} --enable-cluster-autoscaler --min-count 2 --max-count 5\'\n',
|
||||
' disable_autoscaler_cmd = \'aks update --resource-group={resource_group} --name={name} \' \\n',
|
||||
' \'--tags {tags} --disable-cluster-autoscaler\'\n',
|
||||
' create_spot_node_pool_cmd = \'aks nodepool add \' \\n',
|
||||
' \'--resource-group={resource_group} \' \\n',
|
||||
' \'--cluster-name={name} \' \\n',
|
||||
' \'-n {spot_node_pool_name} \' \\n',
|
||||
' \'--priority Spot \' \\n',
|
||||
' \'--spot-max-price {spot_max_price} \' \\n',
|
||||
' \'-c 1\'\n',
|
||||
' create_ppg_node_pool_cmd = \'aks nodepool add \' \\n',
|
||||
' \'--resource-group={resource_group} \' \\n',
|
||||
' \'--cluster-name={name} \' \\n',
|
||||
' \'-n {node_pool_name} \' \\n',
|
||||
' \'--ppg={ppg} \'\n',
|
||||
' upgrade_node_image_only_cluster_cmd = \'aks upgrade \' \\n',
|
||||
' \'-g {resource_group} \' \\n',
|
||||
' \'-n {name} \' \\n',
|
||||
' \'--node-image-only \' \\n',
|
||||
' \'--yes\'\n',
|
||||
' upgrade_node_image_only_nodepool_cmd = \'aks nodepool upgrade \' \\n',
|
||||
' \'--resource-group={resource_group} \' \\n',
|
||||
' \'--cluster-name={name} \' \\n',
|
||||
' \'-n {node_pool_name} \' \\n',
|
||||
' \'--node-image-only \' \\n',
|
||||
' \'--no-wait\'\n',
|
||||
' get_nodepool_cmd = \'aks nodepool show \' \\n',
|
||||
' \'--resource-group={resource_group} \' \\n',
|
||||
' \'--cluster-name={name} \' \\n',
|
||||
' \'-n {node_pool_name} \'\n',
|
||||
# start with blanks, match *_cmd = '', multiple lines, use .format
|
||||
' install_cmd = \'aks install-cli --client-version={} --install-location={} --base-src-url={} \' \\n',
|
||||
' \'--kubelogin-version={} --kubelogin-install-location={} --kubelogin-base-src-url={}\'.format(version,\n',
|
||||
' ctl_temp_file,\n',
|
||||
' "",\n',
|
||||
' version,\n',
|
||||
' login_temp_file,\n',
|
||||
' "")\n',
|
||||
# start with blanks, match *_cmd, use docstring """
|
||||
' create_cmd = """storage account create -n {sc} -g {rg} -l eastus2euap --enable-files-adds --domain-name\n',
|
||||
' {domain_name} --net-bios-domain-name {net_bios_domain_name} --forest-name {forest_name} --domain-guid\n',
|
||||
' {domain_guid} --domain-sid {domain_sid} --azure-storage-sid {azure_storage_sid}"""\n',
|
||||
' update_cmd = """storage account update -n {sc} -g {rg} --enable-files-adds --domain-name {domain_name}\n',
|
||||
' --net-bios-domain-name {net_bios_domain_name} --forest-name {forest_name} --domain-guid {domain_guid}\n',
|
||||
' --domain-sid {domain_sid} --azure-storage-sid {azure_storage_sid}"""\n',
|
||||
# start with blanks, match *_cmd, use docstring '''
|
||||
' update_cmd = \'\'\'storage account update -n {sc} -g {rg} --enable-files-adds --domain-name {domain_name}\n',
|
||||
' --net-bios-domain-name {net_bios_domain_name} --forest-name {forest_name} --domain-guid {domain_guid}\n',
|
||||
' --domain-sid {domain_sid} --azure-storage-sid {azure_storage_sid}\'\'\'\n',
|
||||
# start with blanks, match *_cmd*, .format
|
||||
' create_cmd1 = \'az storage account create -n {} -g {} --routing-choice MicrosoftRouting --publish-microsoft-endpoint true\'.format('
|
||||
' name1, resource_group)',
|
||||
# start with blanks, match .cmd
|
||||
' test.cmd(\'az billing account list \'',
|
||||
' \'--expand "soldTo,billingProfiles,billingProfiles/invoiceSections"\',',
|
||||
' checks=[])',
|
||||
# start with blanks, match *Command
|
||||
' runCommand = \'aks command invoke -g {resource_group} -n {name} -o json -c "kubectl get pods -A"\'',
|
||||
' self.cmd(runCommand, [',
|
||||
' self.check(\'provisioningState\', \'Succeeded\'),',
|
||||
' self.check(\'exitCode\', 0),',
|
||||
' ])',
|
||||
# start with blanks, match *command, use fstring, single quotation marks
|
||||
' command = f\'afd origin-group update -g {resource_group_name} --profile-name {profile_name} \' ',
|
||||
' f\'--origin-group-name {origin_group_name}\'',
|
||||
# string splicing (+)
|
||||
' self.cmd(\'spring-cloud app deployment create -g {resourceGroup} -s {serviceName} --app {app} -n green\'\n',
|
||||
' + \' --container-image {containerImage} --registry-username PLACEHOLDER --registry-password PLACEHOLDER\',\n',
|
||||
' checks=[\n',
|
||||
' self.check(\'name\', \'green\'),\n',
|
||||
' ])\n',
|
||||
# --format vs .format
|
||||
' self.cmd(\n',
|
||||
' \'appconfig kv import -n {config_store_name} -s {import_source} --path "{strict_import_file_path}" --format {imported_format} --profile {profile} --strict -y\')',
|
||||
]
|
||||
ref = get_all_tested_commands_from_regex(lines)
|
||||
pprint(ref, width=1000)
|
||||
assert len(ref) == 22
|
||||
|
||||
|
||||
def test_detect_new_command():
|
||||
commands = []
|
||||
lines = [
|
||||
'with self.command_group(\'disk\', compute_disk_sdk, operation_group=\'disks\', min_api=\'2017-03-30\') as g:',
|
||||
# 1.`+ g.command(xxx)`
|
||||
'+ g.command(\'list-instances\', \'list\', command_type=compute_vmss_vm_sdk)',
|
||||
# 2.`+ g.custom_command(xxx)`
|
||||
'+ g.custom_command(\'create\', \'create_managed_disk\', supports_no_wait=True, table_transformer=transform_disk_show_table_output, validator=process_disk_or_snapshot_create_namespace)',
|
||||
# 3.`+ g.custom_show_command(xxx)`
|
||||
'+ g.custom_show_command(\'show\', \'get_vmss\', table_transformer=get_vmss_table_output_transformer(self, False))',
|
||||
# 4.`+ g.wait_command(xxx)`
|
||||
'+ g.wait_command(\'wait\', getter_name=\'get_vmss\', getter_type=compute_custom)',
|
||||
]
|
||||
for row_num, line in enumerate(lines):
|
||||
command = search_command(line)
|
||||
if command:
|
||||
cmd = search_command_group(row_num, lines, command)
|
||||
if cmd:
|
||||
commands.append(cmd)
|
||||
pprint(commands)
|
||||
assert commands == ['disk list-instances', 'disk create', 'disk show', 'disk wait']
|
||||
|
||||
|
||||
def test_detect_new_params():
|
||||
parameters = []
|
||||
lines = [
|
||||
# without scope
|
||||
' with self.argument_context(\'disk\') as c:',
|
||||
'+ c.argument(\'network_policy\')',
|
||||
'+ c.argument(\'zone\', zone_type, min_api=\'2017-03-30\', options_list=[\'--zone\']) ',
|
||||
# scope
|
||||
' for scope in [\'disk\', \'snapshot\']:',
|
||||
' with self.argument_context(scope) as c:',
|
||||
'+ c.argument(\'size_gb\', options_list=[\'--size-gb\', \'-z\'], help=\'size in GB. Max size: 4095 GB (certain preview disks can be larger).\', type=int)',
|
||||
# scope with multi args
|
||||
' for scope in [\'signalr create\', \'signalr update\']:',
|
||||
' with self.argument_context(scope, arg_group=\'Network Rule\') as c:',
|
||||
'+ c.argument(\'default_action\', arg_type=get_enum_type([\'Allow\', \'Deny\']), help=\'Default action to apply when no rule matches.\', required=False)',
|
||||
# scope AND format
|
||||
' for scope in [\'create\', \'update\']:',
|
||||
' with self.argument_context(\'vmss run-command {}\'.format(scope)) as c:',
|
||||
'+ c.argument(\'vmss_name\', run_cmd_vmss_name)',
|
||||
# scope AND format
|
||||
' for scope in [\'vm\', \'vmss\']:',
|
||||
' with self.argument_context(\'{} stop\'.format(scope)) as c:',
|
||||
'+ c.argument(\'skip_shutdown\', action=\'store_true\', help=\'Skip shutdown and power-off immediately.\', min_api=\'2019-03-01\')',
|
||||
# multiple `[]`
|
||||
' with self.argument_context(\'acr connected-registry create\') as c:',
|
||||
'+ c.argument(\'client_token_list\', options_list=[\'--client-tokens\'], nargs=\'+\', help=\'Specify the client access to the repositories in the connected registry. It can be in the format [TOKEN_NAME01] [TOKEN_NAME02]...\')',
|
||||
'+ c.argument(\'notifications\', options_list=[\'--notifications\'], nargs=\'+\', help=\'List of artifact pattern for which notifications need to be generated. Use the format "--notifications [PATTERN1 PATTERN2 ...]".\')',
|
||||
# multiple lines
|
||||
' with self.argument_context(\'webapp update\') as c:',
|
||||
'+ c.argument(\'skip_custom_domain_verification\',',
|
||||
'+ help="If true, custom (non *.azurewebsites.net) domains associated with web app are not verified",',
|
||||
'+ arg_type=get_three_state_flag(return_label=True), deprecate_info=c.deprecate(expiration=\'3.0.0\'))',
|
||||
# options_list=[""] double quotes
|
||||
' with self.argument_context(\'webapp update\') as c:',
|
||||
'+ c.argument(\'minimum_elastic_instance_count\', options_list=["--minimum-elastic-instance-count", "-i"], type=int, is_preview=True, help="Minimum number of instances. App must be in an elastic scale App Service Plan.")',
|
||||
'+ c.argument(\'prewarmed_instance_count\', options_list=["--prewarmed-instance-count", "-w"], type=int, is_preview=True, help="Number of preWarmed instances. App must be in an elastic scale App Service Plan.")',
|
||||
# self.argument_context with multi args
|
||||
' with self.argument_context(\'appconfig kv import\', arg_group=\'File\') as c:',
|
||||
'+ c.argument(\'strict\', validator=validate_strict_import, arg_type=get_three_state_flag(), help="Delete all other key-values in the store with specified prefix and label", is_preview=True)',
|
||||
' with self.argument_context(\'snapshot\', resource_type=ResourceType.MGMT_COMPUTE, operation_group=\'snapshots\') as c:',
|
||||
'+ c.argument(\'snapshot_name\', existing_snapshot_name, id_part=\'name\', completer=get_resource_name_completion_list(\'Microsoft.Compute/snapshots\'))',
|
||||
]
|
||||
# pattern = r'\+\s+c.argument\((.*)\)?'
|
||||
for row_num, line in enumerate(lines):
|
||||
params, _ = search_argument(line)
|
||||
if params:
|
||||
cmds = search_argument_context(row_num, lines)
|
||||
# print(cmds)
|
||||
for cmd in cmds:
|
||||
parameters.append([cmd, params])
|
||||
continue
|
||||
pprint(parameters)
|
||||
assert parameters == [
|
||||
['disk', ['--network-policy']],
|
||||
['disk', ['--zone']],
|
||||
['disk', ['--size-gb,', '-z']],
|
||||
['snapshot', ['--size-gb,', '-z']],
|
||||
['signalr create', ['--default-action']],
|
||||
['signalr update', ['--default-action']],
|
||||
['vmss run-command create', ['--vmss-name']],
|
||||
['vmss run-command update', ['--vmss-name']],
|
||||
['vm stop', ['--skip-shutdown']],
|
||||
['vmss stop', ['--skip-shutdown']],
|
||||
['acr connected-registry create', ['--client-tokens']],
|
||||
['acr connected-registry create', ['--notifications']],
|
||||
['webapp update', ['--skip-custom-domain-verification']],
|
||||
['webapp update', ['--minimum-elastic-instance-count,', '-i']],
|
||||
['webapp update', ['--prewarmed-instance-count,', '-w']],
|
||||
['appconfig kv import', ['--strict']],
|
||||
['snapshot', ['--snapshot-name']],
|
||||
]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_one_line_regex()
|
||||
test_multiple_lines_regex()
|
||||
test_detect_new_command()
|
||||
test_detect_new_params()
|
|
@ -83,7 +83,7 @@ def load_arguments(self, _):
|
|||
with ArgumentsContext(self, 'linter') as c:
|
||||
c.positional('modules', modules_type)
|
||||
c.argument('rules', options_list=['--rules', '-r'], nargs='+', help='Space-separated list of rules to run. Omit to run all rules.')
|
||||
c.argument('rule_types', options_list=['--rule-types', '-t'], nargs='+', choices=['params', 'commands', 'command_groups', 'help_entries'], help='Space-separated list of rule types to run. Omit to run all.')
|
||||
c.argument('rule_types', options_list=['--rule-types', '-t'], nargs='+', choices=['params', 'commands', 'command_groups', 'help_entries', 'command_test_coverage'], help='Space-separated list of rule types to run. Omit to run all.')
|
||||
c.argument('ci_exclusions', action='store_true', help='Force application of CI exclusions list when run locally.')
|
||||
c.argument('include_whl_extensions',
|
||||
action='store_true',
|
||||
|
@ -113,7 +113,6 @@ def load_arguments(self, _):
|
|||
with ArgumentsContext(self, 'statistics diff-command-tables') as c:
|
||||
c.argument('table_path', help='command table json file')
|
||||
c.argument('diff_table_path', help='command table json file to diff')
|
||||
|
||||
# endregion
|
||||
|
||||
with ArgumentsContext(self, 'command-change meta-export') as c:
|
||||
|
@ -130,6 +129,12 @@ def load_arguments(self, _):
|
|||
help='format to print diff and suggest message')
|
||||
c.argument('output_file', help='command meta diff json file path to store')
|
||||
|
||||
# region cmdcov
|
||||
with ArgumentsContext(self, 'cmdcov') as c:
|
||||
c.positional('modules', modules_type)
|
||||
c.argument('level', choices=['command', 'argument'], help='Run command test coverage in command level or argument level.')
|
||||
# endregion
|
||||
|
||||
with ArgumentsContext(self, 'perf') as c:
|
||||
c.argument('runs', type=int, help='Number of runs to average performance over.')
|
||||
|
||||
|
|
|
@ -42,7 +42,8 @@ from .display import (
|
|||
)
|
||||
from .git_util import (
|
||||
diff_branches,
|
||||
filter_by_git_diff
|
||||
filter_by_git_diff,
|
||||
diff_branches_detail
|
||||
)
|
||||
from .path import (
|
||||
extract_module_name,
|
||||
|
@ -106,5 +107,6 @@ __all__ = [
|
|||
'get_path_table',
|
||||
'get_name_index',
|
||||
'require_virtual_env',
|
||||
'require_azure_cli'
|
||||
'require_azure_cli',
|
||||
'diff_branches_detail',
|
||||
]
|
||||
|
|
|
@ -90,5 +90,40 @@ def diff_branches(repo, target, source):
|
|||
logger.info('git --no-pager diff %s..%s --name-only -- .\n', target_commit, source_commit)
|
||||
|
||||
diff_index = target_commit.diff(source_commit)
|
||||
|
||||
return [diff.b_path for diff in diff_index]
|
||||
|
||||
|
||||
def diff_branches_detail(repo, target, source):
|
||||
""" Returns compare results of files that have changed in a given repo between two branches.
|
||||
Only focus on these files: _params.py, commands.py, test_*.py """
|
||||
try:
|
||||
import git # pylint: disable=unused-import,unused-variable
|
||||
import git.exc as git_exc
|
||||
import gitdb
|
||||
except ImportError as ex:
|
||||
raise CLIError(ex)
|
||||
|
||||
from git import Repo
|
||||
try:
|
||||
git_repo = Repo(repo)
|
||||
except (git_exc.NoSuchPathError, git_exc.InvalidGitRepositoryError):
|
||||
raise CLIError('invalid git repo: {}'.format(repo))
|
||||
|
||||
def get_commit(branch):
|
||||
try:
|
||||
return git_repo.commit(branch)
|
||||
except gitdb.exc.BadName:
|
||||
raise CLIError('usage error, invalid branch: {}'.format(branch))
|
||||
|
||||
if source:
|
||||
source_commit = get_commit(source)
|
||||
else:
|
||||
source_commit = git_repo.head.commit
|
||||
target_commit = get_commit(target)
|
||||
|
||||
logger.info('Filtering down to modules which have changed based on:')
|
||||
logger.info('cd %s', repo)
|
||||
logger.info('git --no-pager diff %s..%s --name-only -- .\n', target_commit, source_commit)
|
||||
|
||||
diff_index = target_commit.diff(source_commit)
|
||||
return diff_index
|
||||
|
|
1
setup.py
1
setup.py
|
@ -62,6 +62,7 @@ setup(
|
|||
'azdev.operations.extensions',
|
||||
'azdev.operations.statistics',
|
||||
'azdev.operations.command_change',
|
||||
'azdev.operations.cmdcov',
|
||||
'azdev.utilities',
|
||||
],
|
||||
install_requires=[
|
||||
|
|
Загрузка…
Ссылка в новой задаче