Merge "Add the base microversions test part"
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index a5c0600..5bc0769 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -18,6 +18,7 @@
 from oslo_log import log as logging
 from tempest_lib import exceptions as lib_exc
 
+from tempest.common import api_version_utils
 from tempest.common import compute
 from tempest.common.utils import data_utils
 from tempest.common import waiters
@@ -29,7 +30,8 @@
 LOG = logging.getLogger(__name__)
 
 
-class BaseV2ComputeTest(tempest.test.BaseTestCase):
+class BaseV2ComputeTest(api_version_utils.BaseMicroversionTest,
+                        tempest.test.BaseTestCase):
     """Base test case class for all Compute API tests."""
 
     force_tenant_isolation = False
@@ -43,6 +45,12 @@
         super(BaseV2ComputeTest, cls).skip_checks()
         if not CONF.service_available.nova:
             raise cls.skipException("Nova is not available")
+        cfg_min_version = CONF.compute_feature_enabled.min_microversion
+        cfg_max_version = CONF.compute_feature_enabled.max_microversion
+        api_version_utils.check_skip_with_microversion(cls.min_microversion,
+                                                       cls.max_microversion,
+                                                       cfg_min_version,
+                                                       cfg_max_version)
 
     @classmethod
     def setup_credentials(cls):
diff --git a/tempest/common/api_version_request.py b/tempest/common/api_version_request.py
new file mode 100644
index 0000000..72a11ea
--- /dev/null
+++ b/tempest/common/api_version_request.py
@@ -0,0 +1,152 @@
+# Copyright 2014 IBM Corp.
+#
+#    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 re
+
+from tempest import exceptions
+
+
+# Define the minimum and maximum version of the API across all of the
+# REST API. The format of the version is:
+# X.Y where:
+#
+# - X will only be changed if a significant backwards incompatible API
+# change is made which affects the API as whole. That is, something
+# that is only very very rarely incremented.
+#
+# - Y when you make any change to the API. Note that this includes
+# semantic changes which may not affect the input or output formats or
+# even originate in the API code layer. We are not distinguishing
+# between backwards compatible and backwards incompatible changes in
+# the versioning system. It must be made clear in the documentation as
+# to what is a backwards compatible change and what is a backwards
+# incompatible one.
+
+class APIVersionRequest(object):
+    """This class represents an API Version Request.
+
+    This class provides convenience methods for manipulation
+    and comparison of version numbers that we need to do to
+    implement microversions.
+    """
+
+    # NOTE: This 'latest' version is a magic number, we assume any
+    # projects(Nova, etc.) never achieve this number.
+    latest_ver_major = 99999
+    latest_ver_minor = 99999
+
+    def __init__(self, version_string=None):
+        """Create an API version request object.
+
+        :param version_string: String representation of APIVersionRequest.
+            Correct format is 'X.Y', where 'X' and 'Y' are int values.
+            None value should be used to create Null APIVersionRequest,
+            which is equal to 0.0
+        """
+        self.ver_major = 0
+        self.ver_minor = 0
+
+        if version_string is not None:
+            match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$",
+                             version_string)
+            if match:
+                self.ver_major = int(match.group(1))
+                self.ver_minor = int(match.group(2))
+            elif version_string == 'latest':
+                self.ver_major = self.latest_ver_major
+                self.ver_minor = self.latest_ver_minor
+            else:
+                raise exceptions.InvalidAPIVersionString(
+                    version=version_string)
+
+    def __str__(self):
+        """Debug/Logging representation of object."""
+        return ("API Version Request: %s" % self.get_string())
+
+    def is_null(self):
+        return self.ver_major == 0 and self.ver_minor == 0
+
+    def _format_type_error(self, other):
+        return TypeError("'%(other)s' should be an instance of '%(cls)s'" %
+                         {"other": other, "cls": self.__class__})
+
+    def __lt__(self, other):
+        if not isinstance(other, APIVersionRequest):
+            raise self._format_type_error(other)
+
+        return ((self.ver_major, self.ver_minor) <
+                (other.ver_major, other.ver_minor))
+
+    def __eq__(self, other):
+        if not isinstance(other, APIVersionRequest):
+            raise self._format_type_error(other)
+
+        return ((self.ver_major, self.ver_minor) ==
+                (other.ver_major, other.ver_minor))
+
+    def __gt__(self, other):
+        if not isinstance(other, APIVersionRequest):
+            raise self._format_type_error(other)
+
+        return ((self.ver_major, self.ver_minor) >
+                (other.ver_major, other.ver_minor))
+
+    def __le__(self, other):
+        return self < other or self == other
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __ge__(self, other):
+        return self > other or self == other
+
+    def matches(self, min_version, max_version):
+        """Matches the version object.
+
+        Returns whether the version object represents a version
+        greater than or equal to the minimum version and less than
+        or equal to the maximum version.
+
+        @param min_version: Minimum acceptable version.
+        @param max_version: Maximum acceptable version.
+        @returns: boolean
+
+        If min_version is null then there is no minimum limit.
+        If max_version is null then there is no maximum limit.
+        If self is null then raise ValueError
+        """
+
+        if self.is_null():
+            raise ValueError
+        if max_version.is_null() and min_version.is_null():
+            return True
+        elif max_version.is_null():
+            return min_version <= self
+        elif min_version.is_null():
+            return self <= max_version
+        else:
+            return min_version <= self <= max_version
+
+    def get_string(self):
+        """Version string representation.
+
+        Converts object to string representation which if used to create
+        an APIVersionRequest object results in the same version request.
+        """
+        if self.is_null():
+            return None
+        if (self.ver_major == self.latest_ver_major and
+            self.ver_minor == self.latest_ver_minor):
+            return 'latest'
+        return "%s.%s" % (self.ver_major, self.ver_minor)
diff --git a/tempest/common/api_version_utils.py b/tempest/common/api_version_utils.py
new file mode 100644
index 0000000..c499f23
--- /dev/null
+++ b/tempest/common/api_version_utils.py
@@ -0,0 +1,64 @@
+# Copyright 2015 NEC Corporation.  All rights reserved.
+#
+#    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 testtools
+
+from tempest.common import api_version_request
+from tempest import exceptions
+
+
+class BaseMicroversionTest(object):
+    """Mixin class for API microversion test class."""
+
+    # NOTE: Basically, each microversion is small API change and we
+    # can use the same tests for most microversions in most cases.
+    # So it is nice to define the test class as possible to run
+    # for all microversions. We need to define microversion range
+    # (min_microversion, max_microversion) on each test class if necessary.
+    min_microversion = None
+    max_microversion = 'latest'
+
+
+def check_skip_with_microversion(test_min_version, test_max_version,
+                                 cfg_min_version, cfg_max_version):
+    min_version = api_version_request.APIVersionRequest(test_min_version)
+    max_version = api_version_request.APIVersionRequest(test_max_version)
+    config_min_version = api_version_request.APIVersionRequest(cfg_min_version)
+    config_max_version = api_version_request.APIVersionRequest(cfg_max_version)
+    if ((min_version > max_version) or
+       (config_min_version > config_max_version)):
+        msg = ("Min version is greater than Max version. Test Class versions "
+               "[%s - %s]. configration versions [%s - %s]."
+               % (min_version.get_string(),
+                  max_version.get_string(),
+                  config_min_version.get_string(),
+                  config_max_version.get_string()))
+        raise exceptions.InvalidConfiguration(msg)
+
+    # NOTE: Select tests which are in range of configuration like
+    #               config min           config max
+    # ----------------+--------------------------+----------------
+    # ...don't-select|
+    #            ...select...  ...select...  ...select...
+    #                                             |don't-select...
+    # ......................select............................
+    if (max_version < config_min_version or
+        config_max_version < min_version):
+        msg = ("The microversion range[%s - %s] of this test is out of the "
+               "configration range[%s - %s]."
+               % (min_version.get_string(),
+                  max_version.get_string(),
+                  config_min_version.get_string(),
+                  config_max_version.get_string()))
+        raise testtools.TestCase.skipException(msg)
diff --git a/tempest/config.py b/tempest/config.py
index 8c3656f..26dda2d 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -341,6 +341,22 @@
                                       title="Enabled Compute Service Features")
 
 ComputeFeaturesGroup = [
+    cfg.StrOpt('min_microversion',
+               default=None,
+               help="Lower version of the test target microversion range. "
+                    "The format is 'X.Y', where 'X' and 'Y' are int values. "
+                    "Tempest selects tests based on the range between "
+                    "min_microversion and max_microversion. "
+                    "If both values are None, Tempest avoids tests which "
+                    "require a microversion."),
+    cfg.StrOpt('max_microversion',
+               default=None,
+               help="Upper version of the test target microversion range. "
+                    "The format is 'X.Y', where 'X' and 'Y' are int values. "
+                    "Tempest selects tests based on the range between "
+                    "min_microversion and max_microversion. "
+                    "If both values are None, Tempest avoids tests which "
+                    "require a microversion."),
     cfg.BoolOpt('disk_config',
                 default=True,
                 help="If false, skip disk config tests"),
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 031df7f..8537898 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -176,6 +176,11 @@
     message = "Invalid structure of table with details"
 
 
+class InvalidAPIVersionString(TempestException):
+    msg_fmt = ("API Version String %(version)s is of invalid format. Must "
+               "be of format MajorNum.MinorNum.")
+
+
 class CommandFailed(Exception):
     def __init__(self, returncode, cmd, output, stderr):
         super(CommandFailed, self).__init__()
diff --git a/tempest/tests/common/test_api_version_request.py b/tempest/tests/common/test_api_version_request.py
new file mode 100644
index 0000000..38fbfc1
--- /dev/null
+++ b/tempest/tests/common/test_api_version_request.py
@@ -0,0 +1,146 @@
+# Copyright 2014 IBM Corp.
+#
+#    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.
+
+from tempest.common import api_version_request
+from tempest import exceptions
+from tempest.tests import base
+
+
+class APIVersionRequestTests(base.TestCase):
+    def test_valid_version_strings(self):
+        def _test_string(version, exp_major, exp_minor):
+            v = api_version_request.APIVersionRequest(version)
+            self.assertEqual(v.ver_major, exp_major)
+            self.assertEqual(v.ver_minor, exp_minor)
+
+        _test_string("1.1", 1, 1)
+        _test_string("2.10", 2, 10)
+        _test_string("5.234", 5, 234)
+        _test_string("12.5", 12, 5)
+        _test_string("2.0", 2, 0)
+        _test_string("2.200", 2, 200)
+
+    def test_null_version(self):
+        v = api_version_request.APIVersionRequest()
+        self.assertTrue(v.is_null())
+
+    def test_invalid_version_strings(self):
+        self.assertRaises(exceptions.InvalidAPIVersionString,
+                          api_version_request.APIVersionRequest, "2")
+
+        self.assertRaises(exceptions.InvalidAPIVersionString,
+                          api_version_request.APIVersionRequest, "200")
+
+        self.assertRaises(exceptions.InvalidAPIVersionString,
+                          api_version_request.APIVersionRequest, "2.1.4")
+
+        self.assertRaises(exceptions.InvalidAPIVersionString,
+                          api_version_request.APIVersionRequest, "200.23.66.3")
+
+        self.assertRaises(exceptions.InvalidAPIVersionString,
+                          api_version_request.APIVersionRequest, "5 .3")
+
+        self.assertRaises(exceptions.InvalidAPIVersionString,
+                          api_version_request.APIVersionRequest, "5. 3")
+
+        self.assertRaises(exceptions.InvalidAPIVersionString,
+                          api_version_request.APIVersionRequest, "5.03")
+
+        self.assertRaises(exceptions.InvalidAPIVersionString,
+                          api_version_request.APIVersionRequest, "02.1")
+
+        self.assertRaises(exceptions.InvalidAPIVersionString,
+                          api_version_request.APIVersionRequest, "2.001")
+
+        self.assertRaises(exceptions.InvalidAPIVersionString,
+                          api_version_request.APIVersionRequest, "")
+
+        self.assertRaises(exceptions.InvalidAPIVersionString,
+                          api_version_request.APIVersionRequest, " 2.1")
+
+        self.assertRaises(exceptions.InvalidAPIVersionString,
+                          api_version_request.APIVersionRequest, "2.1 ")
+
+    def test_version_comparisons(self):
+        vers2_0 = api_version_request.APIVersionRequest("2.0")
+        vers2_5 = api_version_request.APIVersionRequest("2.5")
+        vers5_23 = api_version_request.APIVersionRequest("5.23")
+        v_null = api_version_request.APIVersionRequest()
+        v_latest = api_version_request.APIVersionRequest('latest')
+
+        self.assertTrue(v_null < vers2_5)
+        self.assertTrue(vers2_0 < vers2_5)
+        self.assertTrue(vers2_0 <= vers2_5)
+        self.assertTrue(vers2_0 <= vers2_0)
+        self.assertTrue(vers2_5 > v_null)
+        self.assertTrue(vers5_23 > vers2_5)
+        self.assertTrue(vers2_0 >= vers2_0)
+        self.assertTrue(vers5_23 >= vers2_5)
+        self.assertTrue(vers2_0 != vers2_5)
+        self.assertTrue(vers2_0 == vers2_0)
+        self.assertTrue(vers2_0 != v_null)
+        self.assertTrue(v_null == v_null)
+        self.assertTrue(vers2_0 <= v_latest)
+        self.assertTrue(vers2_0 != v_latest)
+        self.assertTrue(v_latest == v_latest)
+        self.assertRaises(TypeError, vers2_0.__lt__, "2.1")
+
+    def test_version_matches(self):
+        vers2_0 = api_version_request.APIVersionRequest("2.0")
+        vers2_5 = api_version_request.APIVersionRequest("2.5")
+        vers2_45 = api_version_request.APIVersionRequest("2.45")
+        vers3_3 = api_version_request.APIVersionRequest("3.3")
+        vers3_23 = api_version_request.APIVersionRequest("3.23")
+        vers4_0 = api_version_request.APIVersionRequest("4.0")
+        v_null = api_version_request.APIVersionRequest()
+        v_latest = api_version_request.APIVersionRequest('latest')
+
+        def _check_version_matches(version, version1, version2, check=True):
+            if check:
+                msg = "Version %s does not matches with [%s - %s] range"
+                self.assertTrue(version.matches(version1, version2),
+                                msg % (version.get_string(),
+                                       version1.get_string(),
+                                       version2.get_string()))
+            else:
+                msg = "Version %s matches with [%s - %s] range"
+                self.assertFalse(version.matches(version1, version2),
+                                 msg % (version.get_string(),
+                                        version1.get_string(),
+                                        version2.get_string()))
+
+        _check_version_matches(vers2_5, vers2_0, vers2_45)
+        _check_version_matches(vers2_5, vers2_0, v_null)
+        _check_version_matches(vers2_0, vers2_0, vers2_5)
+        _check_version_matches(vers3_3, vers2_5, vers3_3)
+        _check_version_matches(vers3_3, v_null, vers3_3)
+        _check_version_matches(vers3_3, v_null, vers4_0)
+        _check_version_matches(vers2_0, vers2_5, vers2_45, False)
+        _check_version_matches(vers3_23, vers2_5, vers3_3, False)
+        _check_version_matches(vers2_5, vers2_45, vers2_0, False)
+        _check_version_matches(vers2_5, vers2_0, v_latest)
+        _check_version_matches(v_latest, v_latest, v_latest)
+        _check_version_matches(vers2_5, v_latest, v_latest, False)
+        _check_version_matches(v_latest, vers2_0, vers4_0, False)
+
+        self.assertRaises(ValueError, v_null.matches, vers2_0, vers2_45)
+
+    def test_get_string(self):
+        vers_string = ["3.23", "latest"]
+        for ver in vers_string:
+            ver_obj = api_version_request.APIVersionRequest(ver)
+            self.assertEqual(ver, ver_obj.get_string())
+
+        self.assertIsNotNone(
+            api_version_request.APIVersionRequest().get_string)
diff --git a/tempest/tests/common/test_api_version_utils.py b/tempest/tests/common/test_api_version_utils.py
new file mode 100644
index 0000000..33024b6
--- /dev/null
+++ b/tempest/tests/common/test_api_version_utils.py
@@ -0,0 +1,194 @@
+# Copyright 2015 NEC Corporation.  All rights reserved.
+#
+#    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.
+
+from oslo_config import cfg
+import testtools
+
+from tempest.api.compute import base as compute_base
+from tempest.common import api_version_utils
+from tempest import config
+from tempest import exceptions
+from tempest.tests import base
+from tempest.tests import fake_config
+
+
+class VersionTestNoneTolatest(compute_base.BaseV2ComputeTest):
+    min_microversion = None
+    max_microversion = 'latest'
+
+
+class VersionTestNoneTo2_2(compute_base.BaseV2ComputeTest):
+    min_microversion = None
+    max_microversion = '2.2'
+
+
+class VersionTest2_3ToLatest(compute_base.BaseV2ComputeTest):
+    min_microversion = '2.3'
+    max_microversion = 'latest'
+
+
+class VersionTest2_5To2_10(compute_base.BaseV2ComputeTest):
+    min_microversion = '2.5'
+    max_microversion = '2.10'
+
+
+class VersionTest2_10To2_10(compute_base.BaseV2ComputeTest):
+    min_microversion = '2.10'
+    max_microversion = '2.10'
+
+
+class InvalidVersionTest(compute_base.BaseV2ComputeTest):
+    min_microversion = '2.11'
+    max_microversion = '2.1'
+
+
+class TestMicroversionsTestsClass(base.TestCase):
+
+    def setUp(self):
+        super(TestMicroversionsTestsClass, self).setUp()
+        self.useFixture(fake_config.ConfigFixture())
+        self.stubs.Set(config, 'TempestConfigPrivate',
+                       fake_config.FakePrivate)
+
+    def _test_version(self, cfg_min, cfg_max,
+                      expected_pass_tests,
+                      expected_skip_tests):
+        cfg.CONF.set_default('min_microversion',
+                             cfg_min, group='compute-feature-enabled')
+        cfg.CONF.set_default('max_microversion',
+                             cfg_max, group='compute-feature-enabled')
+        try:
+            for test_class in expected_pass_tests:
+                test_class.skip_checks()
+            for test_class in expected_skip_tests:
+                self.assertRaises(testtools.TestCase.skipException,
+                                  test_class.skip_checks)
+        except testtools.TestCase.skipException as e:
+            raise testtools.TestCase.failureException(e.message)
+
+    def test_config_version_none_none(self):
+        expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2]
+        expected_skip_tests = [VersionTest2_3ToLatest, VersionTest2_5To2_10,
+                               VersionTest2_10To2_10]
+        self._test_version(None, None,
+                           expected_pass_tests,
+                           expected_skip_tests)
+
+    def test_config_version_none_23(self):
+        expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2,
+                               VersionTest2_3ToLatest]
+        expected_skip_tests = [VersionTest2_5To2_10, VersionTest2_10To2_10]
+        self._test_version(None, '2.3',
+                           expected_pass_tests,
+                           expected_skip_tests)
+
+    def test_config_version_22_latest(self):
+        expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2,
+                               VersionTest2_3ToLatest, VersionTest2_5To2_10,
+                               VersionTest2_10To2_10]
+        expected_skip_tests = []
+        self._test_version('2.2', 'latest',
+                           expected_pass_tests,
+                           expected_skip_tests)
+
+    def test_config_version_22_23(self):
+        expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2,
+                               VersionTest2_3ToLatest]
+        expected_skip_tests = [VersionTest2_5To2_10, VersionTest2_10To2_10]
+        self._test_version('2.2', '2.3',
+                           expected_pass_tests,
+                           expected_skip_tests)
+
+    def test_config_version_210_210(self):
+        expected_pass_tests = [VersionTestNoneTolatest,
+                               VersionTest2_3ToLatest,
+                               VersionTest2_5To2_10,
+                               VersionTest2_10To2_10]
+        expected_skip_tests = [VersionTestNoneTo2_2]
+        self._test_version('2.10', '2.10',
+                           expected_pass_tests,
+                           expected_skip_tests)
+
+    def test_config_version_none_latest(self):
+        expected_pass_tests = [VersionTestNoneTolatest, VersionTestNoneTo2_2,
+                               VersionTest2_3ToLatest, VersionTest2_5To2_10,
+                               VersionTest2_10To2_10]
+        expected_skip_tests = []
+        self._test_version(None, 'latest',
+                           expected_pass_tests,
+                           expected_skip_tests)
+
+    def test_config_version_latest_latest(self):
+        expected_pass_tests = [VersionTestNoneTolatest, VersionTest2_3ToLatest]
+        expected_skip_tests = [VersionTestNoneTo2_2, VersionTest2_5To2_10,
+                               VersionTest2_10To2_10]
+        self._test_version('latest', 'latest',
+                           expected_pass_tests,
+                           expected_skip_tests)
+
+    def test_config_invalid_version(self):
+        cfg.CONF.set_default('min_microversion',
+                             '2.5', group='compute-feature-enabled')
+        cfg.CONF.set_default('max_microversion',
+                             '2.1', group='compute-feature-enabled')
+        self.assertRaises(exceptions.InvalidConfiguration,
+                          VersionTestNoneTolatest.skip_checks)
+
+    def test_config_version_invalid_test_version(self):
+        cfg.CONF.set_default('min_microversion',
+                             None, group='compute-feature-enabled')
+        cfg.CONF.set_default('max_microversion',
+                             '2.13', group='compute-feature-enabled')
+        self.assertRaises(exceptions.InvalidConfiguration,
+                          InvalidVersionTest.skip_checks)
+
+
+class TestVersionSkipLogic(base.TestCase):
+
+    def _test_version(self, test_min_version, test_max_version,
+                      cfg_min_version, cfg_max_version, expected_skip=False):
+        try:
+            api_version_utils.check_skip_with_microversion(test_min_version,
+                                                           test_max_version,
+                                                           cfg_min_version,
+                                                           cfg_max_version)
+        except testtools.TestCase.skipException as e:
+            if not expected_skip:
+                raise testtools.TestCase.failureException(e.message)
+
+    def test_version_min_in_range(self):
+        self._test_version('2.2', '2.10', '2.1', '2.7')
+
+    def test_version_max_in_range(self):
+        self._test_version('2.1', '2.3', '2.2', '2.7')
+
+    def test_version_cfg_in_range(self):
+        self._test_version('2.2', '2.9', '2.3', '2.7')
+
+    def test_version_equal(self):
+        self._test_version('2.2', '2.2', '2.2', '2.2')
+
+    def test_version_below_cfg_min(self):
+        self._test_version('2.2', '2.4', '2.5', '2.7', expected_skip=True)
+
+    def test_version_above_cfg_max(self):
+        self._test_version('2.8', '2.9', '2.3', '2.7', expected_skip=True)
+
+    def test_version_min_greater_than_max(self):
+        self.assertRaises(exceptions.InvalidConfiguration,
+                          self._test_version, '2.8', '2.7', '2.3', '2.7')
+
+    def test_cfg_version_min_greater_than_max(self):
+        self.assertRaises(exceptions.InvalidConfiguration,
+                          self._test_version, '2.2', '2.7', '2.9', '2.7')