提交 b2f7ce38 编写于 作者: L Lucas Meneghel Rodrigues

Merge pull request #452 from ruda/introduce_avocado_remote_submodule

Share functionality of remote and vm plugins inside the  new submodule `avocado.remote`.
......@@ -9,178 +9,18 @@
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2014
# Copyright: Red Hat Inc. 2014-2015
# Author: Ruda Moura <rmoura@redhat.com>
"""Run tests on a remote machine."""
import getpass
import json
import os
from avocado.core import data_dir
from avocado.core import status
from avocado.core import exceptions
from avocado.plugins import plugin
from avocado.result import HumanTestResult
from avocado.runner import TestRunner
from avocado.test import RemoteTest
from avocado.utils import archive
from avocado.remote import RemoteTestResult, RemoteTestRunner
from avocado.utils import remote
class RemoteTestRunner(TestRunner):
""" Tooled TestRunner to run on remote machine using ssh """
remote_test_dir = '~/avocado/tests'
def run_test(self, urls):
"""
Run tests.
:param urls: a string with test URLs.
:return: a dictionary with test results.
"""
avocado_cmd = ('cd %s; avocado run --force-job-id %s --json - '
'--archive %s' % (self.remote_test_dir,
self.result.stream.job_unique_id,
" ".join(urls)))
result = self.result.remote.run(avocado_cmd, ignore_status=True,
timeout=None)
if result.exit_status == 127:
raise exceptions.JobError('Remote machine does not have avocado '
'installed')
json_result = None
for json_output in result.stdout.splitlines():
# We expect dictionary:
if json_output.startswith('{') and json_output.endswith('}'):
try:
json_result = json.loads(json_output)
except ValueError:
pass
if json_result is None:
raise ValueError("Could not parse JSON from avocado remote output:"
"\n%s" % result.stdout)
for t_dict in json_result['tests']:
logdir = os.path.dirname(self.result.stream.debuglog)
logdir = os.path.join(logdir, 'test-results')
logdir = os.path.join(logdir, os.path.relpath(t_dict['url'], '/'))
t_dict['logdir'] = logdir
t_dict['logfile'] = os.path.join(logdir, 'debug.log')
return json_result
def run_suite(self, test_suite):
"""
Run one or more tests and report with test result.
:param params_list: a list of param dicts.
:return: a list of test failures.
"""
del test_suite # using self.result.urls instead
failures = []
self.result.setup()
results = self.run_test(self.result.urls)
remote_log_dir = os.path.dirname(results['debuglog'])
self.result.start_tests()
for tst in results['tests']:
test = RemoteTest(name=tst['test'],
time=tst['time'],
start=tst['start'],
end=tst['end'],
status=tst['status'],
logdir=tst['logdir'],
logfile=tst['logfile'],
fail_reason=tst['fail_reason']
)
state = test.get_state()
self.result.start_test(state)
self.result.check_test(state)
if not status.mapping[state['status']]:
failures.append(state['tagged_name'])
local_log_dir = os.path.dirname(self.result.stream.debuglog)
zip_filename = remote_log_dir + '.zip'
zip_path_filename = os.path.join(local_log_dir,
os.path.basename(zip_filename))
self.result.remote.receive_files(local_log_dir, zip_filename)
archive.uncompress(zip_path_filename, local_log_dir)
os.remove(zip_path_filename)
self.result.end_tests()
self.result.tear_down()
return failures
class RemoteTestResult(HumanTestResult):
"""
Remote Machine Test Result class.
"""
def __init__(self, stream, args):
"""
Creates an instance of RemoteTestResult.
:param stream: an instance of :class:`avocado.core.output.View`.
:param args: an instance of :class:`argparse.Namespace`.
"""
HumanTestResult.__init__(self, stream, args)
self.test_dir = os.getcwd()
self.remote_test_dir = '~/avocado/tests'
self.urls = self.args.url
self.remote = None # Remote runner initialized during setup
self.output = '-'
self.command_line_arg_name = '--remote-hostname'
def _copy_tests(self):
"""
Gather test directories and copy them recursively to
$remote_test_dir + $test_absolute_path.
:note: Default tests execution is translated into absolute paths too
"""
# TODO: Use `avocado.loader.TestLoader` instead
self.remote.makedir(self.remote_test_dir)
if self.args.remote_no_copy: # Leave everything as is
return
paths = set()
for i in xrange(len(self.urls)):
url = self.urls[i]
if not os.path.exists(url): # use test_dir path + py
url = os.path.join(data_dir.get_test_dir(), '%s.py' % url)
url = os.path.abspath(url) # always use abspath; avoid clashes
# modify url to remote_path + abspath
paths.add(os.path.dirname(url))
self.urls[i] = self.remote_test_dir + url
previous = ' NOT ABSOLUTE PATH'
for path in sorted(paths):
if os.path.commonprefix((path, previous)) == previous:
continue # already copied
rpath = self.remote_test_dir + path
self.remote.makedir(rpath)
self.remote.send_files(path, os.path.dirname(rpath))
previous = path
def setup(self):
""" Setup remote environment and copy test directories """
self.stream.notify(event='message',
msg=("LOGIN : %s@%s:%d"
% (self.args.remote_username,
self.args.remote_hostname,
self.args.remote_port)))
self.remote = remote.Remote(self.args.remote_hostname,
self.args.remote_username,
self.args.remote_password,
self.args.remote_port,
quiet=True)
self._copy_tests()
def tear_down(self):
""" Cleanup after test execution """
pass
class RunRemote(plugin.Plugin):
"""
......
......@@ -9,80 +9,19 @@
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2014
# Copyright: Red Hat Inc. 2014-2015
# Author: Ruda Moura <rmoura@redhat.com>
"""Run tests on Virtual Machine."""
import getpass
from avocado.core import exceptions
from avocado.plugins import plugin
from avocado.plugins.remote import RemoteTestResult
from avocado.plugins.remote import RemoteTestRunner
from avocado.remote import VMTestResult
from avocado.remote import RemoteTestRunner
from avocado.utils import virt
class VMTestResult(RemoteTestResult):
"""
Virtual Machine Test Result class.
"""
def __init__(self, stream, args):
super(VMTestResult, self).__init__(stream, args)
self.vm = None
self.command_line_arg_name = '--vm-domain'
def setup(self):
# Super called after VM is found and initialized
if self.args.vm_domain is None:
e_msg = ('Please set Virtual Machine Domain with option '
'--vm-domain.')
self.stream.notify(event='error', msg=e_msg)
raise exceptions.TestSetupFail(e_msg)
if self.args.vm_hostname is None:
e_msg = ('Please set Virtual Machine hostname with option '
'--vm-hostname.')
self.stream.notify(event='error', msg=e_msg)
raise exceptions.TestSetupFail(e_msg)
self.stream.notify(event='message', msg="DOMAIN : %s"
% self.args.vm_domain)
self.vm = virt.vm_connect(self.args.vm_domain,
self.args.vm_hypervisor_uri)
if self.vm is None:
self.stream.notify(event='error',
msg="Could not connect to VM '%s'"
% self.args.vm_domain)
raise exceptions.TestSetupFail()
if self.vm.start() is False:
self.stream.notify(event='error', msg="Could not start VM '%s'"
% self.args.vm_domain)
raise exceptions.TestSetupFail()
assert self.vm.domain.isActive() is not False
if self.args.vm_cleanup is True:
self.vm.create_snapshot()
if self.vm.snapshot is None:
self.stream.notify(event='error', msg="Could not create "
"snapshot on VM '%s'" % self.args.vm_domain)
raise exceptions.TestSetupFail()
try:
# Finish remote setup and copy the tests
self.args.remote_hostname = self.args.vm_hostname
self.args.remote_username = self.args.vm_username
self.args.remote_password = self.args.vm_password
self.args.remote_no_copy = self.args.vm_no_copy
super(VMTestResult, self).setup()
except Exception:
self.tear_down()
raise
def tear_down(self):
super(VMTestResult, self).tear_down()
if self.args.vm_cleanup is True and self.vm.snapshot is not None:
self.vm.restore_snapshot()
class RunVM(plugin.Plugin):
"""
......
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2014-2015
# Author: Ruda Moura <rmoura@redhat.com>
from avocado.remote.test import RemoteTest
from avocado.remote.result import RemoteTestResult, VMTestResult
from avocado.remote.runner import RemoteTestRunner
# This rogram is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2014-2015
# Author: Ruda Moura <rmoura@redhat.com>
"""Remote test results."""
import os
from avocado.core import exceptions
from avocado.core import data_dir
from avocado.result import HumanTestResult
from avocado.utils import remote
from avocado.utils import virt
class RemoteTestResult(HumanTestResult):
"""
Remote Machine Test Result class.
"""
def __init__(self, stream, args):
"""
Creates an instance of RemoteTestResult.
:param stream: an instance of :class:`avocado.core.output.View`.
:param args: an instance of :class:`argparse.Namespace`.
"""
HumanTestResult.__init__(self, stream, args)
self.test_dir = os.getcwd()
self.remote_test_dir = '~/avocado/tests'
self.urls = self.args.url
self.remote = None # Remote runner initialized during setup
self.output = '-'
self.command_line_arg_name = '--remote-hostname'
def _copy_tests(self):
"""
Gather test directories and copy them recursively to
$remote_test_dir + $test_absolute_path.
:note: Default tests execution is translated into absolute paths too
"""
# TODO: Use `avocado.loader.TestLoader` instead
self.remote.makedir(self.remote_test_dir)
if self.args.remote_no_copy: # Leave everything as is
return
paths = set()
for i in xrange(len(self.urls)):
url = self.urls[i]
if not os.path.exists(url): # use test_dir path + py
url = os.path.join(data_dir.get_test_dir(), '%s.py' % url)
url = os.path.abspath(url) # always use abspath; avoid clashes
# modify url to remote_path + abspath
paths.add(os.path.dirname(url))
self.urls[i] = self.remote_test_dir + url
previous = ' NOT ABSOLUTE PATH'
for path in sorted(paths):
if os.path.commonprefix((path, previous)) == previous:
continue # already copied
rpath = self.remote_test_dir + path
self.remote.makedir(rpath)
self.remote.send_files(path, os.path.dirname(rpath))
previous = path
def setup(self):
""" Setup remote environment and copy test directories """
self.stream.notify(event='message',
msg=("LOGIN : %s@%s:%d"
% (self.args.remote_username,
self.args.remote_hostname,
self.args.remote_port)))
self.remote = remote.Remote(self.args.remote_hostname,
self.args.remote_username,
self.args.remote_password,
self.args.remote_port,
quiet=True)
self._copy_tests()
def tear_down(self):
""" Cleanup after test execution """
pass
class VMTestResult(RemoteTestResult):
"""
Virtual Machine Test Result class.
"""
def __init__(self, stream, args):
super(VMTestResult, self).__init__(stream, args)
self.vm = None
self.command_line_arg_name = '--vm-domain'
def setup(self):
# Super called after VM is found and initialized
if self.args.vm_domain is None:
e_msg = ('Please set Virtual Machine Domain with option '
'--vm-domain.')
self.stream.notify(event='error', msg=e_msg)
raise exceptions.TestSetupFail(e_msg)
if self.args.vm_hostname is None:
e_msg = ('Please set Virtual Machine hostname with option '
'--vm-hostname.')
self.stream.notify(event='error', msg=e_msg)
raise exceptions.TestSetupFail(e_msg)
self.stream.notify(event='message', msg="DOMAIN : %s"
% self.args.vm_domain)
self.vm = virt.vm_connect(self.args.vm_domain,
self.args.vm_hypervisor_uri)
if self.vm is None:
self.stream.notify(event='error',
msg="Could not connect to VM '%s'"
% self.args.vm_domain)
raise exceptions.TestSetupFail()
if self.vm.start() is False:
self.stream.notify(event='error', msg="Could not start VM '%s'"
% self.args.vm_domain)
raise exceptions.TestSetupFail()
assert self.vm.domain.isActive() is not False
if self.args.vm_cleanup is True:
self.vm.create_snapshot()
if self.vm.snapshot is None:
self.stream.notify(event='error', msg="Could not create "
"snapshot on VM '%s'" % self.args.vm_domain)
raise exceptions.TestSetupFail()
try:
# Finish remote setup and copy the tests
self.args.remote_hostname = self.args.vm_hostname
self.args.remote_username = self.args.vm_username
self.args.remote_password = self.args.vm_password
self.args.remote_no_copy = self.args.vm_no_copy
super(VMTestResult, self).setup()
except Exception:
self.tear_down()
raise
def tear_down(self):
super(VMTestResult, self).tear_down()
if self.args.vm_cleanup is True and self.vm.snapshot is not None:
self.vm.restore_snapshot()
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2014-2015
# Author: Ruda Moura <rmoura@redhat.com>
"""Remote test runner."""
import json
import os
from avocado.core import status
from avocado.core import exceptions
from avocado.utils import archive
from avocado.runner import TestRunner
from avocado.remote.test import RemoteTest
class RemoteTestRunner(TestRunner):
""" Tooled TestRunner to run on remote machine using ssh """
remote_test_dir = '~/avocado/tests'
def run_test(self, urls):
"""
Run tests.
:param urls: a string with test URLs.
:return: a dictionary with test results.
"""
avocado_cmd = ('cd %s; avocado run --force-job-id %s --json - '
'--archive %s' % (self.remote_test_dir,
self.result.stream.job_unique_id,
" ".join(urls)))
result = self.result.remote.run(avocado_cmd, ignore_status=True,
timeout=None)
if result.exit_status == 127:
raise exceptions.JobError('Remote machine does not have avocado '
'installed')
json_result = None
for json_output in result.stdout.splitlines():
# We expect dictionary:
if json_output.startswith('{') and json_output.endswith('}'):
try:
json_result = json.loads(json_output)
except ValueError:
pass
if json_result is None:
raise ValueError("Could not parse JSON from avocado remote output:"
"\n%s" % result.stdout)
for t_dict in json_result['tests']:
logdir = os.path.dirname(self.result.stream.debuglog)
logdir = os.path.join(logdir, 'test-results')
logdir = os.path.join(logdir, os.path.relpath(t_dict['url'], '/'))
t_dict['logdir'] = logdir
t_dict['logfile'] = os.path.join(logdir, 'debug.log')
return json_result
def run_suite(self, test_suite):
"""
Run one or more tests and report with test result.
:param params_list: a list of param dicts.
:return: a list of test failures.
"""
del test_suite # using self.result.urls instead
failures = []
self.result.setup()
results = self.run_test(self.result.urls)
remote_log_dir = os.path.dirname(results['debuglog'])
self.result.start_tests()
for tst in results['tests']:
test = RemoteTest(name=tst['test'],
time=tst['time'],
start=tst['start'],
end=tst['end'],
status=tst['status'],
logdir=tst['logdir'],
logfile=tst['logfile'],
fail_reason=tst['fail_reason'])
state = test.get_state()
self.result.start_test(state)
self.result.check_test(state)
if not status.mapping[state['status']]:
failures.append(state['tagged_name'])
local_log_dir = os.path.dirname(self.result.stream.debuglog)
zip_filename = remote_log_dir + '.zip'
zip_path_filename = os.path.join(local_log_dir,
os.path.basename(zip_filename))
self.result.remote.receive_files(local_log_dir, zip_filename)
archive.uncompress(zip_path_filename, local_log_dir)
os.remove(zip_path_filename)
self.result.end_tests()
self.result.tear_down()
return failures
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2014-2015
# Author: Ruda Moura <rmoura@redhat.com>
"""Remote test class."""
class RemoteTest(object):
"""
Mimics :class:`avocado.test.Test` for remote tests.
"""
def __init__(self, name, status, time, start, end, fail_reason, logdir,
logfile):
note = "Not supported yet"
self.name = name
self.tagged_name = name
self.status = status
self.time_elapsed = time
self.time_start = start
self.time_end = end
self.fail_class = note
self.traceback = note
self.text_output = note
self.fail_reason = fail_reason
self.whiteboard = ''
self.job_unique_id = ''
self.logdir = logdir
self.logfile = logfile
def get_state(self):
"""
Serialize selected attributes representing the test state
:returns: a dictionary containing relevant test state data
:rtype: dict
"""
d = self.__dict__
d['class_name'] = self.__class__.__name__
return d
......@@ -615,39 +615,3 @@ class NotATest(Test):
e_msg = ('File %s is not executable and does not contain an avocado '
'test class in it ' % self.name)
raise exceptions.NotATestError(e_msg)
class RemoteTest(object):
"""
Mimics :class:`avocado.test.Test` for remote tests.
"""
def __init__(self, name, status, time, start, end, fail_reason, logdir,
logfile):
note = "Not supported yet"
self.name = name
self.tagged_name = name
self.status = status
self.time_elapsed = time
self.time_start = start
self.time_end = end
self.fail_class = note
self.traceback = note
self.text_output = note
self.fail_reason = fail_reason
self.whiteboard = ''
self.job_unique_id = ''
self.logdir = logdir
self.logfile = logfile
def get_state(self):
"""
Serialize selected attributes representing the test state
:returns: a dictionary containing relevant test state data
:rtype: dict
"""
d = self.__dict__
d['class_name'] = self.__class__.__name__
return d
......@@ -5,7 +5,10 @@ import os
from flexmock import flexmock, flexmock_teardown
from avocado.plugins import remote
from avocado import remote
from avocado.utils import archive
from avocado.utils import remote as utils_remote
from avocado.core import data_dir
cwd = os.getcwd()
......@@ -56,10 +59,10 @@ class RemoteTestRunnerTest(unittest.TestCase):
(Remote.should_receive('receive_files')
.with_args('/local/path', '/home/user/avocado/logs/run-2014-05-26-'
'15.45.37.zip')).once().ordered()
(flexmock(remote.archive).should_receive('uncompress')
(flexmock(archive).should_receive('uncompress')
.with_args('/local/path/run-2014-05-26-15.45.37.zip', '/local/path')
.once().ordered())
(flexmock(remote.os).should_receive('remove')
(flexmock(os).should_receive('remove')
.with_args('/local/path/run-2014-05-26-15.45.37.zip').once()
.ordered())
Results.should_receive('end_tests').once().ordered()
......@@ -82,23 +85,23 @@ class RemoteTestResultTest(unittest.TestCase):
def setUp(self):
Remote = flexmock()
Stream = flexmock()
(flexmock(remote.os).should_receive('getcwd')
(flexmock(os).should_receive('getcwd')
.and_return('/current/directory').ordered())
Stream.should_receive('notify').once().ordered()
remote_remote = flexmock(remote.remote)
remote_remote = flexmock(utils_remote)
(remote_remote.should_receive('Remote')
.with_args('hostname', 'username', 'password', 22, quiet=True)
.once().ordered()
.and_return(Remote))
(Remote.should_receive('makedir').with_args('~/avocado/tests')
.once().ordered())
(flexmock(remote.os.path).should_receive('exists')
(flexmock(os.path).should_receive('exists')
.with_args('/tests/sleeptest').once().and_return(True).ordered())
(flexmock(remote.os.path).should_receive('exists')
(flexmock(os.path).should_receive('exists')
.with_args('/tests/other/test').once().and_return(True).ordered())
(flexmock(remote.os.path).should_receive('exists')
(flexmock(os.path).should_receive('exists')
.with_args('passtest').once().and_return(False).ordered())
(flexmock(remote.data_dir).should_receive('get_test_dir').once()
(flexmock(data_dir).should_receive('get_test_dir').once()
.and_return('/path/to/default/tests/location').ordered())
(Remote.should_receive('makedir')
.with_args("~/avocado/tests/path/to/default/tests/location")
......
#!/usr/bin/env python
import unittest
import os
from flexmock import flexmock, flexmock_teardown
from avocado.plugins import vm, remote
from avocado.remote import VMTestResult, RemoteTestResult
from avocado.utils import virt
JSON_RESULTS = ('Something other than json\n'
......@@ -23,19 +25,17 @@ class VMTestResultTest(unittest.TestCase):
def setUp(self):
# remote.RemoteTestResult.__init__()
Stream = flexmock()
(flexmock(remote.os).should_receive('getcwd')
(flexmock(os).should_receive('getcwd')
.and_return('/current/directory').once().ordered())
# vm.VMTestResult.setup()
(Stream.should_receive('notify')
.with_args(msg="DOMAIN : domain", event="message"))
mock_vm = flexmock(snapshot=True,
domain=flexmock(isActive=lambda: True))
virt = flexmock(vm.virt)
virt.should_receive('vm_connect').and_return(mock_vm).once().ordered()
flexmock(virt).should_receive('vm_connect').and_return(mock_vm).once().ordered()
mock_vm.should_receive('start').and_return(True).once().ordered()
mock_vm.should_receive('create_snapshot').once().ordered()
RemoteTestResult = flexmock(remote.RemoteTestResult)
RemoteTestResult.should_receive('setup').once().ordered()
flexmock(RemoteTestResult).should_receive('setup').once().ordered()
# vm.RemoteTestResult()
Args = flexmock(test_result_total=1,
url=['/tests/sleeptest', '/tests/other/test',
......@@ -48,7 +48,7 @@ class VMTestResultTest(unittest.TestCase):
vm_cleanup=True,
vm_no_copy=False,
vm_hypervisor_uri='my_hypervisor_uri')
self.remote = vm.VMTestResult(Stream, Args)
self.remote = VMTestResult(Stream, Args)
# vm.RemoteTestResult.tear_down()
RemoteTestResult.should_receive('tear_down').once().ordered()
mock_vm.should_receive('restore_snapshot').once().ordered()
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册