Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add check for circular dependencies #165

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>