Improve plugin testing (#3517)

* refactor entrypoint into helpers

* Add lookup by metadata
This is geared towards plugins packaged in pkgs that differ in name from their top module

* Make module lookup predictable in changing pkg-envs

* remove no coverage from plugin packages

* ignore coverage for production loadin

* refactor plugin collection - move assigment out

* do not cover fs errors

* test custom dir loading

* test module meta fetcher

* add a bit more safety

* do not cover sanity checkers

* add folder loading test

* ignore again for cleaner diffs for now

* ignore safety catch

* rename test

* Add test for package installs

* fix docstring name

* depreciate test for now

* Fix for out of BASE_DIR paths

* ignore catch

* remove unneeded complexity

* add testing for outside folders

* more docstrings and simpler methods

* make call simpler
This commit is contained in:
Matthias Mair 2022-08-16 05:09:48 +02:00 committed by GitHub
parent 858d48afe7
commit 188ddc3be3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 130 additions and 20 deletions

View File

@ -43,7 +43,7 @@ class PluginAppConfig(AppConfig):
pass
# get plugins and init them
registry.collect_plugins()
registry.plugin_modules = registry.collect_plugins()
registry.load_plugins()
# drop out of maintenance

View File

@ -241,12 +241,15 @@ def get_module_meta(mdl_name):
# Get spec for module
spec = find_spec(mdl_name)
if not spec: # pragma: no cover
raise PackageNotFoundError(mdl_name)
# Try to get specific package for the module
result = None
for dist in distributions():
try:
relative = pathlib.Path(spec.origin).relative_to(dist.locate_file(''))
except ValueError:
except ValueError: # pragma: no cover
pass
else:
if relative in dist.files:
@ -254,7 +257,7 @@ def get_module_meta(mdl_name):
# Check if a distribution was found
# A no should not be possible here as a call can only be made on a discovered module but better save then sorry
if not result:
if not result: # pragma: no cover
raise PackageNotFoundError(mdl_name)
# Return metadata

View File

View File

@ -0,0 +1,10 @@
"""Very simple sample plugin"""
from plugin import InvenTreePlugin
class SimplePlugin(InvenTreePlugin):
"""A very simple plugin."""
NAME = 'SimplePlugin'
SLUG = "simple"

View File

@ -4,6 +4,7 @@
- Manages setup and teardown of plugin class instances
"""
import imp
import importlib
import logging
import os
@ -200,7 +201,7 @@ class PluginsRegistry:
if settings.TESTING:
custom_dirs = os.getenv('INVENTREE_PLUGIN_TEST_DIR', None)
else:
else: # pragma: no cover
custom_dirs = get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir')
# Load from user specified directories (unless in testing mode)
@ -215,7 +216,7 @@ class PluginsRegistry:
if not pd.exists():
try:
pd.mkdir(exist_ok=True)
except Exception:
except Exception: # pragma: no cover
logger.error(f"Could not create plugin directory '{pd}'")
continue
@ -225,24 +226,31 @@ class PluginsRegistry:
if not init_filename.exists():
try:
init_filename.write_text("# InvenTree plugin directory\n")
except Exception:
except Exception: # pragma: no cover
logger.error(f"Could not create file '{init_filename}'")
continue
if pd.exists() and pd.is_dir():
# By this point, we have confirmed that the directory at least exists
logger.info(f"Added plugin directory: '{pd}'")
dirs.append(pd)
if pd.exists() and pd.is_dir():
# Convert to python dot-path
if pd.is_relative_to(settings.BASE_DIR):
pd_path = '.'.join(pd.relative_to(settings.BASE_DIR).parts)
else:
pd_path = str(pd)
# Add path
dirs.append(pd_path)
logger.info(f"Added plugin directory: '{pd}' as '{pd_path}'")
return dirs
def collect_plugins(self):
"""Collect plugins from all possible ways of loading."""
"""Collect plugins from all possible ways of loading. Returned as list."""
if not settings.PLUGINS_ENABLED:
# Plugins not enabled, do nothing
return # pragma: no cover
self.plugin_modules = [] # clear
collected_plugins = []
# Collect plugins from paths
for plugin in self.plugin_dirs():
@ -258,26 +266,33 @@ class PluginsRegistry:
parent_path = str(parent_obj.parent)
plugin = parent_obj.name
modules = get_plugins(importlib.import_module(plugin), InvenTreePlugin, path=parent_path)
# Gather Modules
if parent_path:
raw_module = imp.load_source(plugin, str(parent_obj.joinpath('__init__.py')))
else:
raw_module = importlib.import_module(plugin)
modules = get_plugins(raw_module, InvenTreePlugin, path=parent_path)
if modules:
[self.plugin_modules.append(item) for item in modules]
[collected_plugins.append(item) for item in modules]
# Check if not running in testing mode and apps should be loaded from hooks
if (not settings.PLUGIN_TESTING) or (settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP):
# Collect plugins from setup entry points
for entry in get_entrypoints(): # pragma: no cover
for entry in get_entrypoints():
try:
plugin = entry.load()
plugin.is_package = True
plugin._get_package_metadata()
self.plugin_modules.append(plugin)
except Exception as error:
collected_plugins.append(plugin)
except Exception as error: # pragma: no cover
handle_error(error, do_raise=False, log_name='discovery')
# Log collected plugins
logger.info(f'Collected {len(self.plugin_modules)} plugins!')
logger.info(", ".join([a.__module__ for a in self.plugin_modules]))
logger.info(f'Collected {len(collected_plugins)} plugins!')
logger.info(", ".join([a.__module__ for a in collected_plugins]))
return collected_plugins
def install_plugin_file(self):
"""Make sure all plugins are installed in the current enviroment."""

View File

@ -2,7 +2,7 @@
from django.test import TestCase
from .helpers import render_template
from .helpers import get_module_meta, render_template
class HelperTests(TestCase):
@ -21,3 +21,15 @@ class HelperTests(TestCase):
response = render_template(ErrorSource(), 'sample/wrongsample.html', {'abc': 123})
self.assertTrue('lert alert-block alert-danger' in response)
self.assertTrue('Template file <em>sample/wrongsample.html</em>' in response)
def test_get_module_meta(self):
"""Test for get_module_meta."""
# We need a stable, known good that will be in enviroment for sure
# and it can't be stdlib because does might differ depending on the abstraction layer
# and version
meta = get_module_meta('django')
# Lets just hope they do not change the name or author
self.assertEqual(meta['Name'], 'Django')
self.assertEqual(meta['Author'], 'Django Software Foundation')

View File

@ -1,8 +1,14 @@
"""Unit tests for plugins."""
import os
import shutil
import subprocess
import tempfile
from datetime import datetime
from pathlib import Path
from unittest import mock
from django.test import TestCase
from django.test import TestCase, override_settings
import plugin.templatetags.plugin_extras as plugin_tags
from plugin import InvenTreePlugin, registry
@ -162,3 +168,67 @@ class InvenTreePluginTests(TestCase):
self.assertEqual(self.plugin_old.slug, 'old')
# check default value is used
self.assertEqual(self.plugin_old.get_meta_value('ABC', 'ABCD', '123'), '123')
class RegistryTests(TestCase):
"""Tests for registry loading methods."""
def run_package_test(self, directory):
"""General runner for testing package based installs."""
# Patch enviroment varible to add dir
envs = {'INVENTREE_PLUGIN_TEST_DIR': directory}
with mock.patch.dict(os.environ, envs):
# Reload to redicsover plugins
registry.reload_plugins(full_reload=True)
# Depends on the meta set in InvenTree/plugin/mock/simple:SimplePlugin
plg = registry.get_plugin('simple')
self.assertEqual(plg.slug, 'simple')
self.assertEqual(plg.human_name, 'SimplePlugin')
def test_custom_loading(self):
"""Test if data in custom dir is loaded correctly."""
test_dir = Path('plugin_test_dir')
# Patch env
envs = {'INVENTREE_PLUGIN_TEST_DIR': 'plugin_test_dir'}
with mock.patch.dict(os.environ, envs):
# Run plugin directory discovery again
registry.plugin_dirs()
# Check the directory was created
self.assertTrue(test_dir.exists())
# Clean folder up
shutil.rmtree(test_dir, ignore_errors=True)
def test_subfolder_loading(self):
"""Test that plugins in subfolders get loaded."""
self.run_package_test('InvenTree/plugin/mock')
def test_folder_loading(self):
"""Test that plugins in folders outside of BASE_DIR get loaded."""
# Run in temporary directory -> always a new random name
with tempfile.TemporaryDirectory() as tmp:
# Fill directory with sample data
new_dir = Path(tmp).joinpath('mock')
shutil.copytree(Path('InvenTree/plugin/mock').absolute(), new_dir)
# Run tests
self.run_package_test(str(new_dir))
@override_settings(PLUGIN_TESTING_SETUP=True)
def test_package_loading(self):
"""Test that package distributed plugins work."""
# Install sample package
subprocess.check_output('pip install inventree-zapier'.split())
# Reload to discover plugin
registry.reload_plugins(full_reload=True)
# Test that plugin was installed
plg = registry.get_plugin('zapier')
self.assertEqual(plg.slug, 'zapier')
self.assertEqual(plg.name, 'inventree_zapier')