blob: 16a0617eaadf29e0edc62c3abbd4982b6ead1a4f [file] [log] [blame]
# pylint: disable=C0301,C0103,C0111
from __future__ import print_function
from sys import platform
import os
import signal
import psutil
import subprocess
from time import monotonic
import xml.etree.ElementTree as ET
import glob
from tests_engine import TestResult
this_path = os.path.abspath(os.path.dirname(__file__))
def install_cli_arguments(parser):
parser.add_argument("--properties-file", action="store", help="Location of properties file.")
parser.add_argument("--skip-building", action="store_true", help="Do not build tests before run.")
parser.add_argument("--force-net-framework-version", action="store", dest="framework_ver_override", help="Override target .NET Framework version when building tests.")
class NUnitTestSuite(object):
nunit_path = os.path.join(this_path, './../lib/resources/tools/nunit3/nunit3-console.exe')
def __init__(self, path):
#super(NUnitTestSuite, self).__init__(path)
self.path = path
def check(self, options, number_of_runs): #API requires this method
pass
def get_output_dir(self, options, iteration_index, suite_retry_index):
# Unused mechanism, this exists to keep a uniform interface with
# robot_tests_provider.py.
return options.results_directory
# NOTE: if we switch to using msbuild on all platforms, we can get rid of this function and only use the '-' prefix
def build_params(self, *params):
def __decorate_build_param(p):
if self.builder == 'xbuild':
return '/' + p
else:
return '-' + p
ret = []
for i in params:
ret += [__decorate_build_param(i)]
return ret
def prepare(self, options):
if not options.skip_building:
print("Building {0}".format(self.path))
if options.runner == 'dotnet':
self.builder = 'dotnet'
params = ['build', '--verbosity', 'quiet', '--configuration', options.configuration, '/p:NET=true']
else:
if platform == "win32":
self.builder = 'MSBuild.exe'
else:
self.builder = 'xbuild'
params = self.build_params('p:PropertiesLocation={0}'.format(options.properties_file), 'p:OutputPath={0}'.format(options.results_directory), 'nologo', 'verbosity:quiet', 'p:OutputDir=tests_output', 'p:Configuration={0}'.format(options.configuration))
if options.framework_ver_override:
params += self.build_params(f'p:TargetFrameworkVersion=v{options.framework_ver_override}')
result = subprocess.call([self.builder, *params, self.path])
if result != 0:
print("Building project `{}` failed with error code: {}".format(self.path, result))
return result
else:
print('Skipping the build')
return 0
def _cleanup_dangling(self, process, proc_name, test_agent_name):
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
if proc_name in (proc.info['name'] or ''):
flat_cmdline = ' '.join(proc.info['cmdline'] or [])
if test_agent_name in flat_cmdline and '--pid={}'.format(process.pid) in flat_cmdline:
# let's kill it
print('KILLING A DANGLING {} test process {}'.format(test_agent_name, proc.info['pid']))
os.kill(proc.info['pid'], signal.SIGTERM)
def run(self, options, run_id, iteration_index=1, suite_retry_index=0):
# The iteration_index and suite_retry_index arguments are not implemented.
# They exist for the sake of a uniform interface with robot_tests_provider.
print('Running ' + self.path)
project_file = os.path.split(self.path)[1]
output_file = os.path.join(options.results_directory, 'results-{}.xml'.format(project_file))
if options.runner == 'dotnet':
print('Using native dotnet test runner -' + self.path, flush=True)
# we don't build here - we had problems with concurrently occurring builds when copying files to one output directory
# so we run test with --no-build and build tests in previous stage
args = ['dotnet', 'test', "--no-build", "--logger", "console;verbosity=detailed", "--logger", "trx;LogFileName={}".format(output_file), '--configuration', options.configuration, self.path]
else:
args = [NUnitTestSuite.nunit_path, '--domain=None', '--noheader', '--labels=Before', '--result={}'.format(output_file), project_file.replace("csproj", "dll")]
# Unfortunately, debugging like this won't work on .NET, see: https://github.com/dotnet/sdk/issues/4994
# The easiest workaround is to set VSTEST_HOST_DEBUG=1 in your environment
if options.stop_on_error:
args.append('--stoponerror')
if (platform.startswith("linux") or platform == "darwin") and options.runner != 'dotnet':
args.insert(0, 'mono')
if options.port is not None:
if options.suspend:
print('Waiting for a debugger at port: {}'.format(options.port))
args.insert(1, '--debug')
args.insert(2, '--debugger-agent=transport=dt_socket,server=y,suspend={0},address=127.0.0.1:{1}'.format('y' if options.suspend else 'n', options.port))
elif options.debug_mode:
args.insert(1, '--debug')
where_conditions = []
if options.fixture:
if options.runner == 'dotnet':
where_conditions.append(options.fixture)
else:
where_conditions.append('test =~ .*{}.*'.format(options.fixture))
cat = 'TestCategory' if options.runner == 'dotnet' else 'cat'
equals = '=' if options.runner == 'dotnet' else '=='
if options.exclude:
for category in options.exclude:
where_conditions.append('{} != {}'.format(cat, category))
if options.include:
for category in options.include:
where_conditions.append('{} {} {}'.format(cat, equals, category))
if where_conditions:
if options.runner == 'dotnet':
args.append('--filter')
args.append(' & '.join('({})'.format(x) for x in where_conditions))
else:
args.append('--where= ' + ' and '.join(['({})'.format(x) for x in where_conditions]))
if options.run_gdb:
args = ['gdb', '-ex', 'handle SIGXCPU SIG33 SIG35 SIG36 SIGPWR nostop noprint', '--args'] + args
startTimestamp = monotonic()
if options.runner == 'dotnet':
args += ['--', 'NUnit.DisplayName=FullName']
process = subprocess.Popen(args)
print('dotnet test runner PID is {}'.format(process.pid), flush=True)
else:
process = subprocess.Popen(args, cwd=options.results_directory)
print('NUnit3 runner PID is {}'.format(process.pid), flush=True)
process.wait()
if options.runner == 'dotnet':
self._cleanup_dangling(process, 'dotnet', 'dotnet test')
else:
self._cleanup_dangling(process, 'mono', 'nunit-agent.exe')
result = process.returncode == 0
endTimestamp = monotonic()
print('Suite ' + self.path + (' finished successfully!' if result else ' failed!') + ' in ' + str(round(endTimestamp - startTimestamp, 2)) + ' seconds.', flush=True)
return TestResult(result, [output_file])
def cleanup(self, options):
pass
def should_retry_suite(self, options, iteration_index, suite_retry_index):
# Unused mechanism, this exists to keep a uniform interface with
# robot_tests_provider.py.
return False
@staticmethod
def find_failed_tests(path, files_pattern='*.csproj.xml'):
test_files = glob.glob(os.path.join(path, files_pattern))
ret = {'mandatory': []}
for test_file in test_files:
tree = ET.parse(test_file)
root = tree.getroot()
# we analyze both types of output files (nunit and dotnet test) to avoid passing options as parameter
# the cost should be negligible in the context of compiling and running test suites
# nunit runner
for test in root.iter('test-case'):
if test.attrib['result'] == 'Failed':
ret['mandatory'].append(test.attrib['fullname'])
# dotnet runner
xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010"
for test in root.iter(f"{{{xmlns}}}UnitTestResult"):
if test.attrib['outcome'] == 'Failed':
ret['mandatory'].append(test.attrib['testName'])
if not ret['mandatory']:
return None
return ret
@staticmethod
def find_rerun_tests(path):
# Unused mechanism, this exists to keep a uniform interface with
# robot_tests_provider.py.
return None