Skip to content

Commit

Permalink
add check for circular dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
anxdpanic committed Sep 20, 2020
1 parent dade408 commit 1c3223d
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 8 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ It can also be used locally for detecting problems in your addons.

- Check if all PO files are valid

- Check for circular dependencies

All of the validation and checks are done according to the kodi [addon rules](https://kodi.wiki/view/Add-on_rules)

## Installation
Expand Down
27 changes: 19 additions & 8 deletions kodi_addon_checker/addons/Repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,31 @@

class Repository():
def __init__(self, version, path):
"""Get information of all the addons
:version: Kodi version name for the repository
:path: path to the Kodi repository file
"""
super().__init__()
self.version = version
self.path = path

# Recover from unreliable mirrors
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(max_retries=5)
session.mount('http://', adapter)
session.mount('https://', adapter)
content = ''

content = session.get(path, timeout=(30, 30)).content
if path.startswith('http'):
# Recover from unreliable mirrors
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(max_retries=5)
session.mount('http://', adapter)
session.mount('https://', adapter)

if path.endswith('.gz'):
with gzip.open(BytesIO(content), 'rb') as xml_file:
content = session.get(path, timeout=(30, 30)).content

if path.endswith('.gz'):
with gzip.open(BytesIO(content), 'rb') as xml_file:
content = xml_file.read()

if not content and path.endswith('.xml'):
with open(path, 'rb') as xml_file:
content = xml_file.read()

tree = ET.fromstring(content)
Expand Down
2 changes: 2 additions & 0 deletions kodi_addon_checker/check_addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ def start(addon_path, args, all_repo_addons, config=None):

check_dependencies.check_reverse_dependencies(addon_report, addon_id, args.branch, all_repo_addons)

check_dependencies.check_circular_dependencies(addon_report, all_repo_addons, parsed_xml, args.branch)

check_files.check_file_permission(addon_report, file_index)

check_files.check_for_invalid_xml_files(addon_report, file_index)
Expand Down
64 changes: 64 additions & 0 deletions kodi_addon_checker/check_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,70 @@ def check_reverse_dependencies(report: Report, addon: str, branch_name: str, all
.format(", ".join(sorted([r.id for r in rdependsLowerBranch])), len(rdependsLowerBranch))))


def check_circular_dependencies(report: Report, all_repo_addons: dict, parsed_xml, branch_name: str):
"""Check for any circular dependencies in addon.xml file and reports them
:report: current report for adding result records
:all_repo_addons: dictionary return by all_repo_addon() function
:parsed_xml: parsed addon.xml file
:branch_name: name of the kodi branch/version
"""
addon = Addon(parsed_xml)

dependency_tree = create_dependency_tree(addon, all_repo_addons, branch_name)

if dependency_tree:
circular_dependencies = []

for depend in list(dependency_tree.keys()):
if addon.id in dependency_tree.get(depend, []):
circular_dependencies.append(depend)

if circular_dependencies:
report.add(Record(PROBLEM, "Circular dependencies: {}"
.format(", ".join(sorted(set(circular_dependencies))))))


def create_dependency_tree(addon: Addon, all_repo_addons: dict, branch_name: str):
"""Create a dict of dependencies and their dependencies based on addon.
:addon: root addon for dependency tree, class kodi_addon_checker.addons.Addon.Addon
:all_repo_addons: dictionary return by all_repo_addon() function
:branch_name: name of the kodi branch/version
"""
ignore_list = _get_ignore_list(KodiVersion(branch_name))

dependencies = []
dependency_tree = {}

# add dependencies of `addon` to dependencies list
def add_dependencies(_addon):
new_depends = []
for dependency in _addon.dependencies:
# don't check ignored for dependencies
if dependency.id in ignore_list:
continue
new_depends.append(dependency)

if _addon.id not in dependency_tree:
dependency_tree[_addon.id] = []
if new_depends:
dependency_tree[_addon.id] = [d.id for d in new_depends]
dependencies.extend(new_depends)

add_dependencies(addon)

i = 0
# add all dependencies and their dependencies
while i < len(dependencies):
if dependencies[i].id not in dependency_tree:
for _, repo in sorted(all_repo_addons.items()):
found_addon = repo.find(dependencies[i].id)
if found_addon:
add_dependencies(found_addon)
i += 1

return dependency_tree


def _get_ignore_list(kodi_version: KodiVersion):
"""Generate an dependency ignore list based
on the branch name
Expand Down
61 changes: 61 additions & 0 deletions tests/test_check_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import unittest

from os.path import abspath, dirname, join
import xml.etree.ElementTree as ET

from kodi_addon_checker.addons.Addon import Addon
from kodi_addon_checker.addons.Repository import Repository
from kodi_addon_checker.check_dependencies import check_circular_dependencies
from kodi_addon_checker.check_dependencies import create_dependency_tree
from kodi_addon_checker.common import load_plugins
from kodi_addon_checker.record import Record
from kodi_addon_checker.reporter import ReportManager
from kodi_addon_checker.report import Report

HERE = abspath(dirname(__file__))


class TestCheckDependencies(unittest.TestCase):
"""Test dependency checks
"""

def setUp(self):
"""Test setup
"""
load_plugins()
ReportManager.enable(["array"])
self.report = Report("")
self.branch = 'krypton'
self.path = join(HERE, 'test_data', 'Circular_depend')

def test_check_circular_dependency(self):
"""Test circular dependency check
"""
addon_xml = join(self.path, "addon.xml")
addons_xml = join(self.path, "addons.xml")

parsed_xml = ET.parse(addon_xml).getroot()
all_repo_addons = {self.branch: Repository(self.branch, addons_xml)}

check_circular_dependencies(self.report, all_repo_addons, parsed_xml, self.branch)

records = [Record.__str__(r) for r in ReportManager.getEnabledReporters()[0].reports]
output = [record for record in records if record.startswith("ERROR: Circular")]
expected = ["ERROR: Circular dependencies: plugin.test.one"]

self.assertListEqual(expected, output)

def test_dependency_tree_creation(self):
"""Test dependency tree creation
"""
addon_xml = join(self.path, "addon.xml")
addons_xml = join(self.path, "addons.xml")

parsed_xml = ET.parse(addon_xml).getroot()
addon = Addon(parsed_xml)
all_repo_addons = {self.branch: Repository(self.branch, addons_xml)}

output = create_dependency_tree(addon, all_repo_addons, self.branch)
expected = {'plugin.test.two': ['plugin.test.one'], 'plugin.test.one': ['plugin.test.two']}

self.assertEqual(expected, output)
23 changes: 23 additions & 0 deletions tests/test_data/Circular_depend/addon.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version='1.0' encoding='utf-8'?>
<addon id="plugin.test.two" version="0.0.1" name="Testing" provider-name="Tester">
<requires>
<import addon="xbmc.python" version="2.25.0"/>
<import addon="plugin.test.one"/>
</requires>
<extension point="xbmc.python.pluginsource" library="addon.py">
<provides>video</provides>
</extension>
<extension point="xbmc.addon.metadata">
<summary lang="en_GB">Testing</summary>
<description lang="en_GB">Testing</description>
<language>en</language>
<disclaimer>This is just a test</disclaimer>
<platform>all</platform>
<license>GPL-3.0-only</license>
<assets>
<icon>icon.png</icon>
<fanart>fanart.jpg</fanart>
</assets>
<news>Testing 123</news>
</extension>
</addon>
25 changes: 25 additions & 0 deletions tests/test_data/Circular_depend/addons.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version='1.0' encoding='utf-8'?>
<addons>
<addon id="plugin.test.one" version="0.0.1" name="Testing" provider-name="Tester">
<requires>
<import addon="xbmc.python" version="2.25.0"/>
<import addon="plugin.test.two"/>
</requires>
<extension point="xbmc.python.pluginsource" library="addon.py">
<provides>video</provides>
</extension>
<extension point="xbmc.addon.metadata">
<summary lang="en_GB">Testing</summary>
<description lang="en_GB">Testing</description>
<language>en</language>
<disclaimer>This is just a test</disclaimer>
<platform>all</platform>
<license>GPL-3.0-only</license>
<assets>
<icon>icon.png</icon>
<fanart>fanart.jpg</fanart>
</assets>
<news>Testing 123</news>
</extension>
</addon>
</addons>

0 comments on commit 1c3223d

Please sign in to comment.