From a56ca875ae73b692c12624b1bbecf950c9e28e34 Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Tue, 14 Nov 2017 16:16:48 -0800 Subject: [PATCH] Add recipe_diff function and helpers. This takes a pair of Recipe objects and returns a list of diff dicts that include what was changed between the two recipes. --- src/pylorax/api/recipes.py | 80 +++++++++++++++++++++++++++++++++++ tests/pylorax/test_recipes.py | 43 +++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/src/pylorax/api/recipes.py b/src/pylorax/api/recipes.py index 2fce5a1b..13dc74f2 100644 --- a/src/pylorax/api/recipes.py +++ b/src/pylorax/api/recipes.py @@ -718,3 +718,83 @@ def is_parent_diff(repo, filename, tree, parent): diff_opts.set_pathspec([filename]) diff = Git.Diff.new_tree_to_tree(repo, parent.get_tree(), tree, diff_opts) return diff.get_num_deltas() > 0 + +def find_name(name, lst): + """Find the dict matching the name in a list and return it. + + :param name: Name to search for + :type name: str + :param lst: List of dict's with "name" field + :returns: First dict with matching name, or None + :rtype: dict or None + """ + for e in lst: + if e["name"] == name: + return e + return None + +def diff_items(title, old_items, new_items): + """Return the differences between two lists of dicts. + + :param title: Title of the entry + :type title: str + :param old_items: List of item dicts with "name" field + :type old_items: list(dict) + :param new_items: List of item dicts with "name" field + :type new_items: list(dict) + :returns: List of diff dicts with old/new entries + :rtype: list(dict) + """ + diffs = [] + old_names = set(m["name"] for m in old_items) + new_names = set(m["name"] for m in new_items) + + added_items = new_names.difference(old_names) + added_items = sorted(added_items, key=lambda n: n.lower()) + + removed_items = old_names.difference(new_names) + removed_items = sorted(removed_items, key=lambda n: n.lower()) + + same_items = old_names.intersection(new_names) + same_items = sorted(same_items, key=lambda n: n.lower()) + + for name in added_items: + diffs.append({"old":None, + "new":{title:find_name(name, new_items)}}) + + for name in removed_items: + diffs.append({"old":{title:find_name(name, old_items)}, + "new":None}) + + for name in same_items: + old_item = find_name(name, old_items) + new_item = find_name(name, new_items) + if old_item != new_item: + diffs.append({"old":{title:old_item}, + "new":{title:new_item}}) + + return diffs + + +def recipe_diff(old_recipe, new_recipe): + """Diff two versions of a recipe + + :param old_recipe: The old version of the recipe + :type old_recipe: Recipe + :param new_recipe: The new version of the recipe + :type new_recipe: Recipe + :returns: A list of diff dict entries with old/new + :rtype: list(dict) + """ + + diffs = [] + # These cannot be added or removed, just different + for element in ["name", "description", "version"]: + if old_recipe[element] != new_recipe[element]: + diffs.append({"old":{element.title():old_recipe[element]}, + "new":{element.title():new_recipe[element]}}) + + diffs.extend(diff_items("Module", old_recipe["modules"], new_recipe["modules"])) + diffs.extend(diff_items("Package", old_recipe["packages"], new_recipe["packages"])) + + return diffs diff --git a/tests/pylorax/test_recipes.py b/tests/pylorax/test_recipes.py index d88abd3e..dfc54a27 100644 --- a/tests/pylorax/test_recipes.py +++ b/tests/pylorax/test_recipes.py @@ -39,6 +39,25 @@ class BasicRecipeTest(unittest.TestCase): result_dict = eval(f_dict.read()) self.input_toml.append((f_toml.read(), result_dict)) + self.old_modules = [recipes.RecipeModule("toml", "2.1"), + recipes.RecipeModule("bash", "4.*"), + recipes.RecipeModule("httpd", "3.7.*")] + self.old_packages = [recipes.RecipePackage("python", "2.7.*"), + recipes.RecipePackage("parted", "3.2")] + self.new_modules = [recipes.RecipeModule("toml", "2.1"), + recipes.RecipeModule("httpd", "3.8.*"), + recipes.RecipeModule("openssh", "2.8.1")] + self.new_packages = [recipes.RecipePackage("python", "2.7.*"), + recipes.RecipePackage("parted", "3.2"), + recipes.RecipePackage("git", "2.13.*")] + self.modules_result = [{"new": {"Modules": {"version": "2.8.1", "name": "openssh"}}, + "old": None}, + {"new": None, + "old": {"Modules": {"name": "bash", "version": "4.*"}}}, + {"new": {"Modules": {"version": "3.8.*", "name": "httpd"}}, + "old": {"Modules": {"version": "3.7.*", "name": "httpd"}}}] + self.packages_result = [{"new": {"Packages": {"name": "git", "version": "2.13.*"}}, "old": None}] + @classmethod def tearDownClass(self): pass @@ -97,6 +116,30 @@ class BasicRecipeTest(unittest.TestCase): new_version = recipe.bump_version("0.0.1") self.assertEqual(new_version, "0.1.1") + def find_name_test(self): + """Test the find_name function""" + test_list = [{"name":"dog"}, {"name":"cat"}, {"name":"squirrel"}] + + self.assertEqual(recipes.find_name("dog", test_list), {"name":"dog"}) + self.assertEqual(recipes.find_name("cat", test_list), {"name":"cat"}) + self.assertEqual(recipes.find_name("squirrel", test_list), {"name":"squirrel"}) + + def diff_items_test(self): + """Test the diff_items function""" + self.assertEqual(recipes.diff_items("Modules", self.old_modules, self.new_modules), self.modules_result) + self.assertEqual(recipes.diff_items("Packages", self.old_packages, self.new_packages), self.packages_result) + + def recipe_diff_test(self): + """Test the recipe_diff function""" + old_recipe = recipes.Recipe("test-recipe", "A recipe used for testing", "0.1.1", self.old_modules, self.old_packages) + new_recipe = recipes.Recipe("test-recipe", "A recipe used for testing", "0.3.1", self.new_modules, self.new_packages) + result = [{'new': {'Version': '0.3.1'}, 'old': {'Version': '0.1.1'}}, + {'new': {'Module': {'name': 'openssh', 'version': '2.8.1'}}, 'old': None}, + {'new': None, 'old': {'Module': {'name': 'bash', 'version': '4.*'}}}, + {'new': {'Module': {'name': 'httpd', 'version': '3.8.*'}}, + 'old': {'Module': {'name': 'httpd', 'version': '3.7.*'}}}, + {'new': {'Package': {'name': 'git', 'version': '2.13.*'}}, 'old': None}] + self.assertEqual(recipes.recipe_diff(old_recipe, new_recipe), result) class GitRecipesTest(unittest.TestCase): @classmethod