Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reorganize test run in automation #1624

Merged
merged 3 commits into from
Jan 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ src/build
# Auxiliary files
command_coverage.txt

#Test artifacts
# Test artifacts
private_config.json
scripts/smart_create_gen/config.ini
test_results/

# Code coverage
.coverage
9 changes: 1 addition & 8 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,4 @@ install:
- pip install -qqq virtualenv # used by package_verify script
- python scripts/dev_setup.py
script:
- export PYTHONPATH=$PATHONPATH:./src
- python -m azure.cli -h
- python ./scripts/run_pylint.py
- nosetests -v --processes=-1 --process-timeout=600 -w src/azure-cli/azure/cli/tests
- nosetests -v --processes=-1 --process-timeout=600 -w src/azure-cli-core/azure/cli/core/tests
- python scripts/command_modules/test.py
- sh scripts/package_verify.sh
- python scripts/license/verify.py
- ./scripts/build_in_travis.sh
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ adal==0.4.3
applicationinsights==0.10.0
argcomplete==1.3.0
colorama==0.3.7
coverage==4.2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this file only apply to dev-setup or all installation methods? It would seem that if someone did a curl install of the CLI for production use, then they wouldn't need (or probably want) packages related strictly to testing and development.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for test only. Where do we list the requirements for dev environment?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a requirements.txt file in the CLI's root directory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I don't think this requirement.txt should impact the user dependencies which are defined in the setup.py as far as I know. @derekbekoe

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I don't think this requirement.txt should impact the user dependencies which are defined in the setup.py as far as I know. @derekbekoe

Correct.

jmespath
mock==1.3.0
nose==1.3.7
Expand Down
5 changes: 5 additions & 0 deletions scripts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

5 changes: 5 additions & 0 deletions scripts/automation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

7 changes: 7 additions & 0 deletions scripts/automation/coverage/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

"""Code coverage related automation code"""

110 changes: 110 additions & 0 deletions scripts/automation/coverage/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# --------------------------------------------------------------------------------------------
# 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 os.path
import sys
import itertools

from coverage import Coverage

import azure.cli.core.application as cli_application
from ..tests.nose_helper import get_nose_runner
from ..utilities.path import get_core_modules_paths_with_tests, \
get_command_modules_paths_with_tests, get_repo_root, get_test_results_dir, make_dirs


# TODO: Fix track command logic in vcr_test_base.py.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this is the traditional line or branch based code coverage metric, or is it a port of the command coverage logic (which needs to be augmented, but that's a separate thing).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the code I ported from the command coverage logic. In my test, the command coverage doesn't work. I may not have the full context so I add this comment for following up.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the code is in place but not actively being used right now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. It is not actively being used.

def run_command_coverage(output_file='command_coverage.txt', output_dir=None):
class CoverageContext(object):
def __enter__(self):
os.environ['AZURE_CLI_TEST_TRACK_COMMANDS'] = '1'
return self

def __exit__(self, exc_type, exc_val, exc_tb):
del os.environ['AZURE_CLI_TEST_TRACK_COMMANDS']

if output_dir is None:
from ..utilities.path import get_test_results_dir
output_dir = get_test_results_dir(with_timestamp=True, prefix='cmd_cov')

coverage_file = os.path.join(output_dir, output_file)
if os.path.isfile(coverage_file):
os.remove(coverage_file)

config = cli_application.Configuration([])
cli_application.APPLICATION = cli_application.Application(config)

cmd_table = config.get_command_table()
cmd_list = cmd_table.keys()
cmd_set = set(cmd_list)

print('Running tests...')
with CoverageContext():
test_result = run_all_tests()
if not test_result:
print("Tests failed")
sys.exit(1)
else:
print('Tests passed.')

commands_tested_with_params = [line.rstrip('\n') for line in open(coverage_file)]

commands_tested = []
for tested_command in commands_tested_with_params:
for c in cmd_list:
if tested_command.startswith(c):
commands_tested.append(c)

commands_tested_set = set(commands_tested)
untested = list(cmd_set - commands_tested_set)
print()
print("Untested commands")
print("=================")
print('\n'.join(sorted(untested)))
percentage_tested = (len(commands_tested_set) * 100.0 / len(cmd_set))
print()
print('Total commands {}, Tested commands {}, Untested commands {}'.format(
len(cmd_set),
len(commands_tested_set),
len(cmd_set) - len(commands_tested_set)))
print('COMMAND COVERAGE {0:.2f}%'.format(percentage_tested))


class CoverageContext(object):
def __init__(self):
self._cov = Coverage(cover_pylib=False)
self._cov.start()

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self._cov.stop()


def run_code_coverage():
# create test results folder
test_results_folder = get_test_results_dir(with_timestamp=True, prefix='cover')

# get test runner
run_nose = get_nose_runner(test_results_folder, xunit_report=False, exclude_integration=True,
code_coverage=True, parallel=False)

# list test modules
test_modules = itertools.chain(get_core_modules_paths_with_tests(),
get_command_modules_paths_with_tests())

# run code coverage on each project
for index, (name, _, test_path) in enumerate(test_modules):
with CoverageContext():
run_nose(name, test_path)

import shutil
shutil.move('.coverage', os.path.join(test_results_folder, '.coverage.{}'.format(name)))


if __name__ == '__main__':
run_code_coverage()
5 changes: 5 additions & 0 deletions scripts/automation/setup/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

5 changes: 5 additions & 0 deletions scripts/automation/style/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

28 changes: 28 additions & 0 deletions scripts/automation/style/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import os.path
import multiprocessing
from subprocess import check_call
from itertools import chain
from ..utilities.path import get_command_modules_paths, get_core_modules_paths, get_repo_root


def run_pylint():
modules_list = ' '.join(os.path.join(path, 'azure') for _, path in
chain(get_command_modules_paths(), get_core_modules_paths()))
arguments = '{} --rcfile={} -j {} -r n -d I0013'.format(
modules_list,
os.path.join(get_repo_root(), 'pylintrc'),
multiprocessing.cpu_count())

print('pylint arguments: ' + arguments)

check_call(('python -m pylint ' + arguments).split())

print('Pylint done')

if __name__ == '__main__':
run_pylint()
5 changes: 5 additions & 0 deletions scripts/automation/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

58 changes: 58 additions & 0 deletions scripts/automation/tests/nose_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from datetime import datetime


def get_nose_runner(report_folder, parallel=True, process_timeout=600, process_restart=True,
xunit_report=False, exclude_integration=True, code_coverage=False):
"""Create a nose execution method"""

def _run_nose(name, working_dir):
import nose
import os.path

if not report_folder \
or not os.path.exists(report_folder) \
or not os.path.isdir(report_folder):
raise ValueError('Report folder {} does not exist'.format(report_folder))

arguments = ['-w', working_dir, '-v']
if parallel:
arguments += ['--processes=-1', '--process-timeout={}'.format(process_timeout)]
if process_restart:
arguments += ['--process-restartworker']

if xunit_report:
log_file = os.path.join(report_folder, name + '-report.xml')
arguments += ['--with-xunit', '--xunit-file', log_file]
else:
log_file = None

if exclude_integration:
arguments += ['--ignore-files=integration*']

if code_coverage:
# coverage_config = os.path.join(os.path.dirname(__file__), '.coveragerc')
# coverage_folder = os.path.join(report_folder, 'code_coverage')
# make_dirs(coverage_folder)
# if not os.path.exists(coverage_folder) or not os.path.isdir(coverage_folder):
# raise Exception('Failed to create folder {} for code coverage result'
# .format(coverage_folder))

arguments += ['--with-coverage']

debug_file = os.path.join(report_folder, name + '-debug.log')
arguments += ['--debug-log={}'.format(debug_file)]

print()
print('<<< Run {} >>>'.format(name))
start = datetime.now()
result = nose.run(argv=arguments)
end = datetime.now()

return result, start, end, log_file

return _run_nose
44 changes: 44 additions & 0 deletions scripts/automation/tests/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import itertools
import sys

from .nose_helper import get_nose_runner
from ..utilities.display import print_records
from ..utilities.path import get_command_modules_paths_with_tests, \
get_core_modules_paths_with_tests,\
get_test_results_dir


def run_all():
# create test results folder
test_results_folder = get_test_results_dir(with_timestamp=True, prefix='tests')

# get test runner
run_nose = get_nose_runner(test_results_folder, xunit_report=True, exclude_integration=True)

# get test list
modules_to_test = itertools.chain(
get_core_modules_paths_with_tests(),
get_command_modules_paths_with_tests())

# run tests
passed = True
module_results = []
for name, _, test_path in modules_to_test:
result, start, end, log_file = run_nose(name, test_path)
passed &= result
record = (name, start.strftime('%H:%M:%D'), str((end - start).total_seconds()),
'Pass' if result else 'Fail')

module_results.append(record)

print_records(module_results, title='test results')

return passed

if __name__ == '__main__':
sys.exit(0 if run_all() else 1)
5 changes: 5 additions & 0 deletions scripts/automation/utilities/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

7 changes: 7 additions & 0 deletions scripts/automation/utilities/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------


COMMAND_MODULE_PREFIX = 'azure-cli-'
47 changes: 47 additions & 0 deletions scripts/automation/utilities/display.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------


def get_print_format(records):
"""Find the best format to display the given list of records in table format"""
if not records:
raise ValueError('missing parameter records')

if not isinstance(records, list):
raise ValueError('records is not a list')

size = len(records[0])
max_len = [0] * size

col_index = list(range(size))
for rec in records:
if len(rec) != size:
raise ValueError('size of elements in the records set are not equal')

for i in col_index:
max_len[i] = max(max_len[i], len(str(rec[i])))

recommend_format = ''
for each in max_len:
recommend_format += '{:' + str(each + 2) + '}'

return recommend_format, max_len


def print_records(records, print_format=None, title=None, foot_notes=None):
"""Print a list of tuples with a print format."""
print_format = print_format or get_print_format(records)[0]
if print_format is None:
raise ValueError('print format is required')

print()
print("Summary" + ': {}'.format(title) if title is not None else '')
print("==========================")
for rec in records:
print(print_format.format(*rec))
print("==========================")
if foot_notes:
for each in foot_notes:
print('* ' + each)
Loading