diff --git a/tests/test_versions_overlap_parent.py b/tests/test_versions_overlap_parent.py new file mode 100644 index 0000000000..1386a130cb --- /dev/null +++ b/tests/test_versions_overlap_parent.py @@ -0,0 +1,105 @@ +# 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 pkg_resources +import testtools +from testtools import matchers + +import versions_overlap_parent as vop + + +class TestVersionsOverlapParent(testtools.TestCase): + def test_increase_version(self): + self.assertThat(vop.increase_version('1.0'), matchers.Equals('1.1')) + + def test_decrease_version(self): + self.assertThat(vop.decrease_version('1.0'), matchers.Equals('1.9')) + + def _test_get_version_required(self, start_version, op, exp_version): + req = pkg_resources.Requirement.parse('pkg%s%s' % (op, start_version)) + self.assertThat(vop.get_version_required(req.specs[0]), + matchers.Equals(exp_version)) + + def test_get_version_required_eq(self): + self._test_get_version_required('1.0', '==', '1.0') + + def test_get_version_required_gt(self): + self._test_get_version_required('1.0', '>', '1.1') + + def test_get_version_required_ge(self): + self._test_get_version_required('1.0', '>=', '1.0') + + def test_get_version_required_lt(self): + self._test_get_version_required('1.0', '<', '1.9') + + def test_get_version_required_le(self): + self._test_get_version_required('1.0', '<=', '1.0') + + def test_get_version_required_ne(self): + self._test_get_version_required('1.0', '!=', '1.0') + + def test_RequirementsList_read_requirements_empty_line(self): + rl = vop.RequirementsList('something') + rl.read_requirements(['']) + self.assertThat(rl.reqs, matchers.Equals({})) + + def test_RequirementsList_read_requirements_comment_line(self): + rl = vop.RequirementsList('something') + rl.read_requirements(['# comment']) + self.assertThat(rl.reqs, matchers.Equals({})) + + def test_RequirementsList_read_requirements_skips(self): + # Lines starting with certain strings are skipped. + rl = vop.RequirementsList('something') + rl.read_requirements(['http://tarballs.openstack.org/something', + '-esomething', + '-fsomething']) + self.assertThat(rl.reqs, matchers.Equals({})) + + def test_RequirementsList_read_requirements_parse(self): + rl = vop.RequirementsList('something') + rl.read_requirements(['extras', + 'sphinx>=1.1.2,!=1.2.0,<1.3 # BSD', ]) + self.assertThat(rl.reqs['extras'].specs, + matchers.Equals([])) + exp_sphinx_specs = [('>=', '1.1.2'), ('!=', '1.2.0'), ('<', '1.3')] + self.assertThat(rl.reqs['sphinx'].specs, + matchers.Equals(exp_sphinx_specs)) + + def _compare(self, head_reqs, parent_reqs): + vop_obj = vop.VersionsOverlapParent() + vop_obj.set_head_requirements(head_reqs) + vop_obj.set_parent_requirements(parent_reqs) + vop_obj.compare_reqs() + + def test_VersionsOverlapParent_same(self): + # No problem if the requirements list is the same. + self._compare(['extras'], ['extras']) + + def test_VersionsOverlapParent_add(self): + # No problem if a new requirement is added + self._compare(['extras'], ['extras', 'new_requirement>=1.0']) + + def test_VersionsOverlapParent_remove(self): + # No problem if a requirement is removed. + self._compare(['extras', 'old_requirement>=1.0'], ['extras']) + + def test_VersionsOverlapParent_update_overlap(self): + # No problem if a requirement is updated and it overlaps + self._compare(['package>=1.0'], ['package>=1.0,<2.0']) + + def test_VersionsOverlapParent_update_nooverlap_fails(self): + # Fails if versions don't overlap. + cmp_fn = lambda: self._compare(['package>=1.0,<2.0'], ['package>=2.0']) + self.assertThat(cmp_fn, matchers.raises(Exception)) diff --git a/tox.ini b/tox.ini index 5fe5e7efa2..5f590bb34f 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,10 @@ deps = setuptools>3.4 [testenv:pep8] commands = flake8 +[testenv:versions-overlap-parent] +commands = + python versions_overlap_parent.py + [flake8] ignore = H803 exclude = .venv,.git,.tox,dist,doc,*egg,build diff --git a/versions_overlap_parent.py b/versions_overlap_parent.py new file mode 100644 index 0000000000..a841eb7713 --- /dev/null +++ b/versions_overlap_parent.py @@ -0,0 +1,149 @@ +#! /usr/bin/env python +# Copyright (C) 2011 OpenStack, LLC. +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2013 OpenStack Foundation +# 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 subprocess + +import pkg_resources + + +def increase_version(version_string): + """Returns simple increased version string.""" + items = version_string.split('.') + for i in range(len(items) - 1, 0, -1): + current_item = items[i] + if current_item.isdigit(): + current_item = int(current_item) + 1 + items[i] = str(current_item) + break + final = '.'.join(items) + return final + + +def decrease_version(version_string): + """Returns simple decreased version string.""" + items = version_string.split('.') + for i in range(len(items) - 1, 0, -1): + current_item = items[i] + if current_item.isdigit(): + current_item = int(current_item) - 1 + if current_item >= 0: + items[i] = str(current_item) + break + else: + # continue to parent + items[i] = '9' + + final = '.'.join(items) + return final + + +def get_version_required(req): + """Returns required version string depending on reqs.""" + operator = req[0] + version = req[1] + if operator == '>': + version = increase_version(version) + elif operator == '<': + version = decrease_version(version) + return version + + +class RequirementsList(object): + def __init__(self, name): + self.name = name + self.reqs = {} + + def read_requirements(self, req_lines): + """Read requirements.""" + for line in req_lines: + if '#' in line: + line = line[:line.find('#')] + line = line.strip() + if (not line or + line.startswith('http://tarballs.openstack.org/') or + line.startswith('-e') or + line.startswith('-f')): + continue + req = pkg_resources.Requirement.parse(line) + self.reqs[req.project_name.lower()] = req + + +def compare_reqs(parent_reqs, head_reqs): + failed = False + for req in head_reqs.reqs.values(): + name = req.project_name.lower() + parent_req = parent_reqs.reqs.get(name) + if not parent_req: + # head req didn't exist in parent reqs. + continue + + if req == parent_req: + # Requirement is the same, nothing to do. + continue + + # check if overlaps + for spec in req.specs: + version = get_version_required(spec) + if not parent_req.__contains__(version): + failed = True + print("Requirement %s does not overlap with parent " % + str(req)) + + if failed: + raise Exception("Problem with requirements, check previous output.") + + +class VersionsOverlapParent(object): + GLOBAL_REQUIREMENTS_FILENAME = 'global-requirements.txt' + + def set_head_requirements(self, head_requirements): + head_reqs = RequirementsList(name='HEAD') + head_reqs.read_requirements(head_requirements) + self._head_reqs = head_reqs + + def set_parent_requirements(self, parent_requirements): + parent_reqs = RequirementsList(name='parent') + parent_reqs.read_requirements(parent_requirements) + self._parent_reqs = parent_reqs + + def compare_reqs(self): + compare_reqs(self._parent_reqs, self._head_reqs) + + def run(self): + # Parse current commit requirements list. + with open(self.GLOBAL_REQUIREMENTS_FILENAME) as global_reqs: + self.set_head_requirements(global_reqs) + + # Read the global requirements file from the parent commit. + parent_global_reqs = subprocess.check_output( + ['git', 'show', 'HEAD~1:%s' % self.GLOBAL_REQUIREMENTS_FILENAME]) + + # Store parent commit requirements list. + self.set_parent_requirements(parent_global_reqs.splitlines()) + + self.compare_reqs() + + +def main(): + versions_overlap_parent = VersionsOverlapParent() + versions_overlap_parent.run() + + +if __name__ == '__main__': + + main()