Implemented basic class for functional tests
Functional tests uses rsyncd instances and file system
PEP8 fixes
Related-Bug: #1570260
Partial-Bug: #1575759
Change-Id: Id6533aba293b4a50be04967b68fb1827dec8141a
diff --git a/trsync/tests/base.py b/trsync/tests/base.py
index 185fd6f..36f0adc 100644
--- a/trsync/tests/base.py
+++ b/trsync/tests/base.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright 2010-2011 OpenStack Foundation
-# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
+# Copyright (c) 2015-2016, Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
@@ -20,4 +19,4 @@
class TestCase(base.BaseTestCase):
- """Test case base class for all unit tests."""
\ No newline at end of file
+ """Test case base class for all unit tests."""
diff --git a/trsync/tests/rsync_base.py b/trsync/tests/rsync_base.py
new file mode 100644
index 0000000..ea97660
--- /dev/null
+++ b/trsync/tests/rsync_base.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015-2016, Mirantis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import filecmp
+import os
+import pkgutil
+
+from trsync.tests import base
+from trsync.tests import rsync_remotes as remotes
+from trsync.utils import utils as utils
+
+
+logger = utils.logger.getChild('TestRsyncUrl')
+
+
+class TestRsyncBase(base.TestCase):
+
+ """Test case base class for all functional tests"""
+ rsyncd = utils.bunch()
+
+ @property
+ def testname(self):
+ return self.__module__ + "." + self.__class__.__name__ + \
+ "." + self._get_test_method().__name__
+
+ def getDataFile(self, name):
+ path, _ = os.path.split(name)
+ if not os.path.isdir(path):
+ os.makedirs(path)
+ with open(name, 'w') as outf:
+ outf.write('TEST DATA')
+ return name
+
+ def setUp(self):
+ super(TestRsyncBase, self).setUp()
+ self.rsyncd[self.testname] = list()
+ for importer, modname, ispkg in \
+ pkgutil.iter_modules(remotes.__path__, remotes.__name__ + '.'):
+ module = __import__(modname, fromlist='dummy')
+ self.rsyncd[self.testname].append(module.Instance(self.testname))
+
+ def tearDown(self):
+ super(TestRsyncBase, self).tearDown()
+ for module in self.rsyncd[self.testname]:
+ module.stop()
+
+ def assertDirsEqual(self, left, right):
+ diff = filecmp.dircmp(left, right)
+ self.assertListEqual(diff.diff_files, [])
+ self.assertListEqual(diff.left_only, [])
+ self.assertListEqual(diff.right_only, [])
diff --git a/trsync/tests/rsync_remotes/__init__.py b/trsync/tests/rsync_remotes/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/trsync/tests/rsync_remotes/__init__.py
diff --git a/trsync/tests/rsync_remotes/path.py b/trsync/tests/rsync_remotes/path.py
new file mode 100644
index 0000000..3807c67
--- /dev/null
+++ b/trsync/tests/rsync_remotes/path.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015-2016, Mirantis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+import os
+import shutil
+
+
+logging.basicConfig()
+log = logging.getLogger(__name__ + 'Instance')
+log.setLevel('DEBUG')
+
+
+class Instance(object):
+
+ """Provide an temporal rsync daemon on custom port"""
+
+ def __init__(self, name):
+ self._log = log.getChild(name)
+ self._name = name
+ self._root_dir = '/tmp/trsync_test/path'
+ self._data_dir = os.path.join(self._root_dir, name)
+
+ self._init_files()
+
+ def stop(self):
+ if os.path.isdir(self._data_dir):
+ self._log.debug('Removed directory "%s"', self._data_dir)
+ shutil.rmtree(self._data_dir)
+
+ @property
+ def url(self):
+ return self.path
+
+ @property
+ def path(self):
+ return self._data_dir
+
+ def _init_files(self):
+ if os.path.isdir(self._data_dir):
+ self._log.debug('Directory %s already exists. Removing...'
+ '', self._data_dir)
+ shutil.rmtree(self._data_dir)
+ self._log.debug('Creating rsync local directory %s', self.path)
+ os.makedirs(self.path)
diff --git a/trsync/tests/rsync_remotes/rsync2.conf b/trsync/tests/rsync_remotes/rsync2.conf
new file mode 100644
index 0000000..a7a28a9
--- /dev/null
+++ b/trsync/tests/rsync_remotes/rsync2.conf
@@ -0,0 +1,53 @@
+# sample rsyncd.conf configuration file
+
+# GLOBAL OPTIONS
+
+#motd file=/etc/motd
+#log file=/var/log/rsyncd
+# for pid file, do not use /var/run/rsync.pid if
+# you are going to run rsync out of the init.d script.
+# The init.d script does its own pid file handling,
+# so omit the "pid file" line completely in that case.
+# pid file=/var/run/rsyncd.pid
+pid file = {{pid_file}}
+#syslog facility=daemon
+#socket options=
+port = {{port}}
+
+# MODULE OPTIONS
+
+#[ftp]
+[{{module}}]
+
+# comment = public archive
+ comment = {{comment}}
+# path = /var/www/pub
+ path = {{path}}
+ use chroot = no
+# max connections=10
+# lock file = /var/lock/rsyncd
+ lock file = /var/lock/{{name}}
+# the default for read only is yes...
+# read only = yes
+ read only = no
+ list = yes
+# uid = nobody
+# gid = nogroup
+# exclude =
+# exclude from =
+# include =
+# include from =
+# auth users =
+# secrets file = /etc/rsyncd.secrets
+ strict modes = yes
+# hosts allow =
+ hosts allow = {{hosts_allow}}
+# hosts deny =
+ ignore errors = no
+ ignore nonreadable = yes
+ transfer logging = no
+# log format = %t: host %h (%a) %o %f (%l bytes). Total %b bytes.
+ timeout = 600
+# refuse options = checksum dry-run
+ dont compress = *.gz *.tgz *.zip *.z *.rpm *.deb *.iso *.bz2 *.tbz
+ munge symlinks = no
diff --git a/trsync/tests/rsync_remotes/rsync2.py b/trsync/tests/rsync_remotes/rsync2.py
new file mode 100644
index 0000000..d6f6082
--- /dev/null
+++ b/trsync/tests/rsync_remotes/rsync2.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015-2016, Mirantis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+import os
+import shutil
+import signal
+import socket
+
+from jinja2 import Template
+from time import sleep
+
+from trsync.utils import shell as shell
+from trsync.utils.utils import bunch as bunch
+
+
+logging.basicConfig()
+log = logging.getLogger(__name__ + 'Instance')
+log.setLevel('DEBUG')
+
+
+class Instance(object):
+
+ """Provide an temporal rsync daemon on custom port"""
+
+ def __init__(self, name):
+ self._log = log.getChild(name)
+ self._name = name
+ self._root_dir = '/tmp/trsync_test/rsync2'
+ self._data_dir = os.path.join(self._root_dir, name)
+
+ self._cfg = bunch()
+ self._cfg.module = name
+ self._cfg.comment = 'rsyncd instance for {}'.format(self._name)
+ self._cfg.path = os.path.join(self._data_dir, self._name)
+ self._cfg.hosts_allow = 'localhost 127.0.0.1'
+ self._cfg.port = self._get_port()
+ self._cfg.pid_file = os.path.join(self._data_dir, self._name + '.pid')
+ self._cfg.config = os.path.join(self._data_dir, self._name + '.conf')
+
+ self._init_files()
+ self._run()
+
+ def _run(self):
+
+ sh = shell.Shell()
+ self._cmd = '/usr/bin/rsync --verbose --daemon --port {} --config {}'\
+ ''.format(self._cfg.port, self._cfg.config)
+ self._log.debug('Starting rsync daemon "{}"'.format(self._cmd))
+ sh.shell(self._cmd)
+ retry_time = 3.0
+ sleep_time = 0.0
+ while not os.path.isfile(self._cfg.pid_file):
+ sleep(0.2)
+ sleep_time += 0.2
+ if sleep_time >= retry_time:
+ raise RuntimeError('pid-file "{}" not found'
+ ''.format(self._cfg.pid_file))
+ self._pid = int(open(self._cfg.pid_file).read().strip())
+
+ def stop(self):
+ self._log.debug('Stoping rsync daemon "%s" (PID=%d)',
+ self._cmd, self._pid)
+ os.kill(self._pid, signal.SIGTERM)
+ if os.path.isdir(self._data_dir):
+ self._log.debug('Removed directory "%s"', self._data_dir)
+ shutil.rmtree(self._data_dir)
+
+ @property
+ def url(self):
+ return 'rsync://localhost:{port}/{module}'.format(**self._cfg)
+
+ @property
+ def path(self):
+ return self._cfg.path
+
+ def _get_config(self):
+ tpl_path, _ = os.path.split(os.path.realpath(__file__))
+ template = Template(open(os.path.join(tpl_path, 'rsync2.conf')).read())
+ return template.render(**self._cfg)
+
+ def _get_port(self):
+ portrange = [
+ int(_) for _
+ in open('/proc/sys/net/ipv4/ip_local_port_range').read().split()
+ ]
+ self._log.debug('Portrange is %s' % portrange)
+ for port in xrange(*portrange):
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self._log.debug('Trying to use port %d...' % port)
+ result = sock.connect_ex(('127.0.0.1', port))
+ self._log.debug('Result is %d' % result)
+ if result != 0:
+ self._log.debug('Port %s assigned', port)
+ return port
+ else:
+ self._log.debug('Port %d in use', port)
+ else:
+ raise RuntimeError("Can't assign port number for rsyncd")
+
+ def _init_files(self):
+ if os.path.isdir(self._data_dir):
+ self._log.debug('Directory %s already exists. Removing...'
+ '', self._data_dir)
+ shutil.rmtree(self._data_dir)
+ self._log.debug('Creating module directory %s', self.path)
+ os.makedirs(self.path)
+ self._log.debug('Creating config %s', self._cfg.config)
+ with open(self._cfg.config, 'w') as config_file:
+ config_file.write(self._get_config())