mirror of
https://github.com/inventree/InvenTree.git
synced 2025-09-14 06:31:27 +00:00
Machine registry improvements (#10150)
* Add wrapper function for machine registry * Decorate entrypoint functions * Docstrings * Fix for boolean setting * Add playwright tests * Use proper entrypoints * Ensure settings are fetched correctly * Prevent recursion of machine registry decorator * Fix machine status display * Enhanced warning msg * Add simple machine sample printer * Adds playwright tests for machine UI * re-throw exception * Define 'machine' plugin mixin class * Adjust machine discovery * Use plugin mixins for registering machine types and drivers * Adjust unit test * Remove plugin static files when deactivating * Force machine reload when plugin registry changes * Add plugins specific to testing framework * Add test for plugin loading sequence * Add session caching - Significantly reduce DB hits * Enhanced unit testing and test plugins * Refactor unit tests * Further unit test fixes * Adjust instance rendering * Display table of available drivers * Cleanup * ADjust unit test * Tweak unit test * Add docs on new mixin type * Tweak machine overview docs * Tweak playwright tests * Additional unit test * Add unit test for calling machine func * Enhanced playwright tests * Account for database not being ready
This commit is contained in:
@@ -230,7 +230,7 @@ class ApiTests(InvenTreeAPITestCase):
|
||||
response = self.get(reverse('api-status-all'))
|
||||
|
||||
# 10 built-in state classes, plus the added GeneralState class
|
||||
self.assertEqual(len(response.data), 12)
|
||||
self.assertEqual(len(response.data), 11)
|
||||
|
||||
# Test the BuildStatus model
|
||||
build_status = response.data['BuildStatus']
|
||||
@@ -270,7 +270,7 @@ class ApiTests(InvenTreeAPITestCase):
|
||||
)
|
||||
response = self.get(reverse('api-status-all'))
|
||||
|
||||
self.assertEqual(len(response.data), 12)
|
||||
self.assertEqual(len(response.data), 11)
|
||||
|
||||
stock_status_cstm = response.data['StockStatus']
|
||||
self.assertEqual(stock_status_cstm['status_class'], 'StockStatus')
|
||||
|
@@ -167,7 +167,7 @@ class MachineTypesList(APIView):
|
||||
@extend_schema(responses={200: MachineSerializers.MachineTypeSerializer(many=True)})
|
||||
def get(self, request):
|
||||
"""List all machine types."""
|
||||
machine_types = list(registry.machine_types.values())
|
||||
machine_types = list(registry.get_machine_types())
|
||||
results = MachineSerializers.MachineTypeSerializer(
|
||||
machine_types, many=True
|
||||
).data
|
||||
@@ -175,10 +175,7 @@ class MachineTypesList(APIView):
|
||||
|
||||
|
||||
class MachineDriverList(APIView):
|
||||
"""List API Endpoint for all discovered machine drivers.
|
||||
|
||||
- GET: List all machine drivers
|
||||
"""
|
||||
"""List API Endpoint for all discovered machine driver types."""
|
||||
|
||||
permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
|
||||
|
||||
@@ -187,9 +184,9 @@ class MachineDriverList(APIView):
|
||||
)
|
||||
def get(self, request):
|
||||
"""List all machine drivers."""
|
||||
drivers = registry.drivers.values()
|
||||
if machine_type := request.query_params.get('machine_type', None):
|
||||
drivers = filter(lambda d: d.machine_type == machine_type, drivers)
|
||||
machine_type = request.query_params.get('machine_type', None)
|
||||
|
||||
drivers = registry.get_driver_types(machine_type)
|
||||
|
||||
results = MachineSerializers.MachineDriverSerializer(
|
||||
list(drivers), many=True
|
||||
|
@@ -36,7 +36,8 @@ class MachineConfig(AppConfig):
|
||||
|
||||
try:
|
||||
logger.info('Loading InvenTree machines')
|
||||
registry.initialize(main=isInMainThread())
|
||||
if not registry.is_ready:
|
||||
registry.initialize(main=isInMainThread())
|
||||
except (OperationalError, ProgrammingError):
|
||||
# Database might not yet be ready
|
||||
logger.warn('Database was not ready for initializing machines')
|
||||
|
@@ -1,20 +1,96 @@
|
||||
"""Machine registry."""
|
||||
|
||||
from typing import Union, cast
|
||||
import functools
|
||||
from typing import Any, Optional, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db.utils import IntegrityError, OperationalError, ProgrammingError
|
||||
|
||||
import structlog
|
||||
|
||||
import InvenTree.cache
|
||||
from common.settings import get_global_setting, set_global_setting
|
||||
from InvenTree.exceptions import log_error
|
||||
from InvenTree.helpers_mixin import get_shared_class_instance_state_mixin
|
||||
from machine.machine_type import BaseDriver, BaseMachineType
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
||||
def machine_registry_entrypoint(
|
||||
check_reload: bool = True, check_ready: bool = True, default_value: Any = None
|
||||
) -> Any:
|
||||
"""Decorator for any method which should be registered as a machine registry entrypoint.
|
||||
|
||||
This decorator ensures that the plugin registry is up-to-date,
|
||||
and reloads the machine registry if necessary.
|
||||
"""
|
||||
|
||||
def decorator(method):
|
||||
"""Internal decorator for the machine registry entrypoint."""
|
||||
|
||||
@functools.wraps(method)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
"""Wrapper function to ensure the machine registry is up-to-date."""
|
||||
# Ensure the plugin registry is up-to-date
|
||||
from plugin import registry as plg_registry
|
||||
|
||||
logger.debug("machine_registry_entrypoint: '%s'", method.__name__)
|
||||
|
||||
if check_ready and not self.ready:
|
||||
logger.warning(
|
||||
"Machine registry is not ready - cannot call method '%s'",
|
||||
method.__name__,
|
||||
)
|
||||
|
||||
return default_value
|
||||
|
||||
do_reload = False
|
||||
|
||||
if InvenTree.cache.get_session_cache('machine_registry_checked'):
|
||||
# Short circuit if we have already checked within this session
|
||||
pass
|
||||
|
||||
elif not getattr(self, '__checking_reload', False):
|
||||
# Avoid recursive reloads
|
||||
do_reload = True
|
||||
self.__checking_reload = True
|
||||
|
||||
if check_reload:
|
||||
if plg_registry.check_reload():
|
||||
# The plugin registry changed - update the machine registry too
|
||||
logger.info(
|
||||
'Plugin registry changed - reloading machine registry'
|
||||
)
|
||||
self.reload_machines()
|
||||
|
||||
else:
|
||||
# Check if the machine registry needs to be reloaded
|
||||
self._check_reload()
|
||||
|
||||
self.__checking_reload = False
|
||||
|
||||
InvenTree.cache.set_session_cache('machine_registry_checked', True)
|
||||
|
||||
# Call the original method
|
||||
try:
|
||||
result = method(self, *args, **kwargs)
|
||||
except Exception as e:
|
||||
log_error(method.__name__, scope='machine_registry')
|
||||
result = default_value
|
||||
raise e
|
||||
finally:
|
||||
# If we reloaded the registry, we need to update the registry hash
|
||||
if do_reload:
|
||||
self._update_registry_hash()
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class MachineRegistry(
|
||||
get_shared_class_instance_state_mixin(lambda _x: 'machine:registry')
|
||||
):
|
||||
@@ -32,6 +108,8 @@ class MachineRegistry(
|
||||
|
||||
self.base_drivers: list[type[BaseDriver]] = []
|
||||
|
||||
self.ready: bool = False
|
||||
|
||||
# Keep an internal hash of the machine registry state
|
||||
self._hash = None
|
||||
|
||||
@@ -40,47 +118,65 @@ class MachineRegistry(
|
||||
"""List of registry errors."""
|
||||
return cast(list[Union[str, Exception]], self.get_shared_state('errors', []))
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
"""Check if the machine registry is ready."""
|
||||
return self.ready
|
||||
|
||||
def handle_error(self, error: Union[Exception, str]):
|
||||
"""Helper function for capturing errors with the machine registry."""
|
||||
self.set_shared_state('errors', [*self.errors, error])
|
||||
|
||||
@machine_registry_entrypoint(check_reload=False, check_ready=False)
|
||||
def initialize(self, main: bool = False):
|
||||
"""Initialize the machine registry."""
|
||||
# clear cache for machines (only needed for global redis cache)
|
||||
if main and hasattr(cache, 'delete_pattern'): # pragma: no cover
|
||||
cache.delete_pattern('machine:*')
|
||||
|
||||
self.discover_machine_types()
|
||||
self.discover_drivers()
|
||||
self.load_machines(main=main)
|
||||
self.ready = True
|
||||
self.reload_machines(main=main)
|
||||
|
||||
def discover_machine_types(self):
|
||||
"""Discovers all machine types by inferring all classes that inherit the BaseMachineType class."""
|
||||
import InvenTree.helpers
|
||||
"""Discovers all machine types by discovering all plugins which implement the Machine mixin class."""
|
||||
from plugin import PluginMixinEnum
|
||||
from plugin.registry import registry as plugin_registry
|
||||
|
||||
logger.debug('Collecting machine types')
|
||||
|
||||
machine_types: dict[str, type[BaseMachineType]] = {}
|
||||
base_drivers: list[type[BaseDriver]] = []
|
||||
|
||||
discovered_machine_types: set[type[BaseMachineType]] = (
|
||||
InvenTree.helpers.inheritors(BaseMachineType)
|
||||
)
|
||||
for machine_type in discovered_machine_types:
|
||||
for plugin in plugin_registry.with_mixin(PluginMixinEnum.MACHINE):
|
||||
try:
|
||||
machine_type.validate()
|
||||
except NotImplementedError as error:
|
||||
self.handle_error(error)
|
||||
continue
|
||||
for machine_type in plugin.get_machine_types():
|
||||
if not issubclass(machine_type, BaseMachineType):
|
||||
logger.error(
|
||||
'INVE-E12: Plugin %s returned invalid machine type',
|
||||
plugin.slug,
|
||||
)
|
||||
continue
|
||||
|
||||
if machine_type.SLUG in machine_types:
|
||||
self.handle_error(
|
||||
ValueError(f"Cannot re-register machine type '{machine_type.SLUG}'")
|
||||
try:
|
||||
machine_type.validate()
|
||||
except NotImplementedError as error:
|
||||
self.handle_error(error)
|
||||
continue
|
||||
|
||||
if machine_type.SLUG in machine_types:
|
||||
self.handle_error(
|
||||
ValueError(
|
||||
f"Cannot re-register machine type '{machine_type.SLUG}'"
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
machine_types[machine_type.SLUG] = machine_type
|
||||
base_drivers.append(machine_type.base_driver)
|
||||
|
||||
except Exception as error:
|
||||
log_error(
|
||||
'discover_machine_types',
|
||||
plugin=plugin.slug,
|
||||
scope='MachineRegistry',
|
||||
)
|
||||
continue
|
||||
|
||||
machine_types[machine_type.SLUG] = machine_type
|
||||
base_drivers.append(machine_type.base_driver)
|
||||
self.handle_error(error)
|
||||
|
||||
self.machine_types = machine_types
|
||||
self.base_drivers = base_drivers
|
||||
@@ -88,39 +184,47 @@ class MachineRegistry(
|
||||
logger.debug('Found %s machine types', len(self.machine_types.keys()))
|
||||
|
||||
def discover_drivers(self):
|
||||
"""Discovers all machine drivers by inferring all classes that inherit the BaseDriver class."""
|
||||
import InvenTree.helpers
|
||||
"""Discovers all machine drivers by discovering all plugins which implement the Machine mixin class."""
|
||||
from plugin import PluginMixinEnum
|
||||
from plugin.registry import registry as plugin_registry
|
||||
|
||||
logger.debug('Collecting machine drivers')
|
||||
|
||||
drivers: dict[str, type[BaseDriver]] = {}
|
||||
|
||||
discovered_drivers: set[type[BaseDriver]] = InvenTree.helpers.inheritors(
|
||||
BaseDriver
|
||||
)
|
||||
for driver in discovered_drivers:
|
||||
# skip discovered drivers that define a base driver for a machine type
|
||||
if driver in self.base_drivers:
|
||||
continue
|
||||
|
||||
for plugin in plugin_registry.with_mixin(PluginMixinEnum.MACHINE):
|
||||
try:
|
||||
driver.validate()
|
||||
except NotImplementedError as error:
|
||||
self.handle_error(error)
|
||||
continue
|
||||
for driver in plugin.get_machine_drivers():
|
||||
if not issubclass(driver, BaseDriver):
|
||||
logger.error(
|
||||
'INVE-E12: Plugin %s returned invalid driver type',
|
||||
plugin.slug,
|
||||
)
|
||||
continue
|
||||
|
||||
if driver.SLUG in drivers:
|
||||
self.handle_error(
|
||||
ValueError(f"Cannot re-register driver '{driver.SLUG}'")
|
||||
try:
|
||||
driver.validate()
|
||||
except NotImplementedError as error:
|
||||
self.handle_error(error)
|
||||
continue
|
||||
|
||||
if driver.SLUG in drivers:
|
||||
self.handle_error(
|
||||
ValueError(f"Cannot re-register driver '{driver.SLUG}'")
|
||||
)
|
||||
continue
|
||||
|
||||
drivers[driver.SLUG] = driver
|
||||
except Exception as error:
|
||||
log_error(
|
||||
'discover_drivers', plugin=plugin.slug, scope='MachineRegistry'
|
||||
)
|
||||
continue
|
||||
|
||||
drivers[driver.SLUG] = driver
|
||||
self.handle_error(error)
|
||||
|
||||
self.drivers = drivers
|
||||
|
||||
logger.debug('Found %s machine drivers', len(self.drivers.keys()))
|
||||
|
||||
@machine_registry_entrypoint()
|
||||
def get_driver_instance(self, slug: str):
|
||||
"""Return or create a driver instance if needed."""
|
||||
if slug not in self.driver_instances:
|
||||
@@ -132,15 +236,23 @@ class MachineRegistry(
|
||||
|
||||
return self.driver_instances.get(slug, None)
|
||||
|
||||
@machine_registry_entrypoint()
|
||||
def load_machines(self, main: bool = False):
|
||||
"""Load all machines defined in the database into the machine registry."""
|
||||
# Imports need to be in this level to prevent early db model imports
|
||||
from machine.models import MachineConfig
|
||||
|
||||
for machine_config in MachineConfig.objects.all():
|
||||
self.add_machine(
|
||||
machine_config, initialize=False, update_registry_hash=False
|
||||
)
|
||||
try:
|
||||
from machine.models import MachineConfig
|
||||
|
||||
for machine_config in MachineConfig.objects.all():
|
||||
self.add_machine(
|
||||
machine_config, initialize=False, update_registry_hash=False
|
||||
)
|
||||
except (OperationalError, ProgrammingError):
|
||||
logger.warning('Database is not ready - cannot load machines')
|
||||
|
||||
self._update_registry_hash()
|
||||
return
|
||||
|
||||
# initialize machines only in main thread
|
||||
if main:
|
||||
@@ -159,11 +271,21 @@ class MachineRegistry(
|
||||
|
||||
self._update_registry_hash()
|
||||
|
||||
def reload_machines(self):
|
||||
def reload_machines(self, main: bool = False):
|
||||
"""Reload all machines from the database."""
|
||||
self.drivers = {}
|
||||
self.driver_instances = {}
|
||||
self.machines = {}
|
||||
self.load_machines()
|
||||
|
||||
InvenTree.cache.set_session_cache('machine_registry_checked', False)
|
||||
|
||||
self.set_shared_state('errors', [])
|
||||
|
||||
self.discover_machine_types()
|
||||
self.discover_drivers()
|
||||
self.load_machines(main=main)
|
||||
|
||||
@machine_registry_entrypoint()
|
||||
def add_machine(self, machine_config, initialize=True, update_registry_hash=True):
|
||||
"""Add a machine to the machine registry."""
|
||||
machine_type = self.machine_types.get(machine_config.machine_type, None)
|
||||
@@ -180,6 +302,7 @@ class MachineRegistry(
|
||||
if update_registry_hash:
|
||||
self._update_registry_hash()
|
||||
|
||||
@machine_registry_entrypoint()
|
||||
def update_machine(
|
||||
self, old_machine_state, machine_config, update_registry_hash=True
|
||||
):
|
||||
@@ -190,15 +313,18 @@ class MachineRegistry(
|
||||
if update_registry_hash:
|
||||
self._update_registry_hash()
|
||||
|
||||
@machine_registry_entrypoint()
|
||||
def restart_machine(self, machine):
|
||||
"""Restart a machine."""
|
||||
machine.restart()
|
||||
|
||||
@machine_registry_entrypoint()
|
||||
def remove_machine(self, machine: BaseMachineType):
|
||||
"""Remove a machine from the registry."""
|
||||
self.machines.pop(str(machine.pk), None)
|
||||
self._update_registry_hash()
|
||||
|
||||
@machine_registry_entrypoint(default_value=False)
|
||||
def get_machines(self, **kwargs):
|
||||
"""Get loaded machines from registry (By default only initialized machines).
|
||||
|
||||
@@ -210,8 +336,6 @@ class MachineRegistry(
|
||||
active: (bool)
|
||||
base_driver: base driver (class)
|
||||
"""
|
||||
self._check_reload()
|
||||
|
||||
allowed_fields = [
|
||||
'name',
|
||||
'machine_type',
|
||||
@@ -253,17 +377,40 @@ class MachineRegistry(
|
||||
|
||||
return list(filter(filter_machine, self.machines.values()))
|
||||
|
||||
@machine_registry_entrypoint(default_value=[])
|
||||
def get_machine_types(self):
|
||||
"""Get all machine types."""
|
||||
return list(self.machine_types.values())
|
||||
|
||||
@machine_registry_entrypoint()
|
||||
def get_machine(self, pk: Union[str, UUID]):
|
||||
"""Get machine from registry by pk."""
|
||||
self._check_reload()
|
||||
return self.machines.get(str(pk), None)
|
||||
|
||||
def get_drivers(self, machine_type: str):
|
||||
"""Get all drivers for a specific machine type."""
|
||||
@machine_registry_entrypoint(default_value=[])
|
||||
def get_driver_types(self, machine_type: Optional[str] = None):
|
||||
"""Return a list of all registered driver types.
|
||||
|
||||
Arguments:
|
||||
machine_type: Optional machine type to filter drivers by their machine type
|
||||
"""
|
||||
return [
|
||||
driver
|
||||
for driver in self.drivers.values()
|
||||
if machine_type is None or driver.machine_type == machine_type
|
||||
]
|
||||
|
||||
@machine_registry_entrypoint(default_value=[])
|
||||
def get_drivers(self, machine_type: Optional[str] = None):
|
||||
"""Get all drivers for a specific machine type.
|
||||
|
||||
Arguments:
|
||||
machine_type: Optional machine type to filter drivers by their machine type
|
||||
"""
|
||||
return [
|
||||
driver
|
||||
for driver in self.driver_instances.values()
|
||||
if driver.machine_type == machine_type
|
||||
if machine_type is None or driver.machine_type == machine_type
|
||||
]
|
||||
|
||||
def _calculate_registry_hash(self):
|
||||
@@ -290,6 +437,14 @@ class MachineRegistry(
|
||||
|
||||
def _check_reload(self):
|
||||
"""Check if the registry needs to be reloaded, and reload it."""
|
||||
from plugin import registry as plg_registry
|
||||
|
||||
do_reload: bool = False
|
||||
plugin_registry_hash = getattr(self, '_plugin_registry_hash', None)
|
||||
|
||||
if plugin_registry_hash != plg_registry.registry_hash:
|
||||
do_reload = True
|
||||
|
||||
if not self._hash:
|
||||
self._hash = self._calculate_registry_hash()
|
||||
|
||||
@@ -301,14 +456,19 @@ class MachineRegistry(
|
||||
|
||||
if reg_hash and reg_hash != self._hash:
|
||||
logger.info('Machine registry has changed - reloading machines')
|
||||
self.reload_machines()
|
||||
return True
|
||||
do_reload = True
|
||||
|
||||
return False
|
||||
if do_reload:
|
||||
self.reload_machines()
|
||||
|
||||
return do_reload
|
||||
|
||||
def _update_registry_hash(self):
|
||||
"""Save the current registry hash."""
|
||||
from plugin import registry as plg_registry
|
||||
|
||||
self._hash = self._calculate_registry_hash()
|
||||
self._plugin_registry_hash = plg_registry.registry_hash
|
||||
|
||||
try:
|
||||
old_hash = get_global_setting('_MACHINE_REGISTRY_HASH')
|
||||
@@ -324,9 +484,10 @@ class MachineRegistry(
|
||||
except Exception as exc:
|
||||
logger.exception('Failed to update machine registry hash: %s', str(exc))
|
||||
|
||||
@machine_registry_entrypoint()
|
||||
def call_machine_function(
|
||||
self, machine_id: str, function_name: str, *args, **kwargs
|
||||
):
|
||||
) -> Any:
|
||||
"""Call a named function against a machine instance.
|
||||
|
||||
Arguments:
|
||||
@@ -337,8 +498,6 @@ class MachineRegistry(
|
||||
|
||||
raise_error = kwargs.pop('raise_error', True)
|
||||
|
||||
self._check_reload()
|
||||
|
||||
# Fetch the machine instance based on the provided UUID
|
||||
machine = self.get_machine(machine_id)
|
||||
|
||||
@@ -372,5 +531,12 @@ registry: MachineRegistry = MachineRegistry()
|
||||
|
||||
|
||||
def call_machine_function(machine_id: str, function: str, *args, **kwargs):
|
||||
"""Global helper function to call a specific function on a machine instance."""
|
||||
"""Global helper function to call a specific function on a machine instance.
|
||||
|
||||
Arguments:
|
||||
machine_id: The UUID of the machine to call the function against
|
||||
function: The name of the function to call
|
||||
*args: Positional arguments to pass to the function
|
||||
**kwargs: Keyword arguments to pass to the function
|
||||
"""
|
||||
return registry.call_machine_function(machine_id, function, *args, **kwargs)
|
||||
|
@@ -167,7 +167,8 @@ class MachineDriverSerializer(BaseMachineClassSerializer):
|
||||
|
||||
def get_errors(self, obj) -> list[str]:
|
||||
"""Serializer method for the errors field."""
|
||||
driver_instance = registry.driver_instances.get(obj.SLUG, None)
|
||||
driver_instance = registry.get_driver_instance(obj.SLUG)
|
||||
|
||||
if driver_instance is None:
|
||||
return []
|
||||
return [str(err) for err in driver_instance.errors]
|
||||
|
@@ -7,10 +7,10 @@ from django.urls import reverse
|
||||
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||
from machine import registry
|
||||
from machine.machine_type import BaseDriver, BaseMachineType
|
||||
from machine.machine_types import LabelPrinterBaseDriver
|
||||
from machine.machine_type import BaseDriver
|
||||
from machine.models import MachineConfig
|
||||
from machine.tests import TestMachineRegistryMixin
|
||||
from plugin.registry import registry as plg_registry
|
||||
from stock.models import StockLocation
|
||||
|
||||
|
||||
@@ -21,57 +21,11 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Setup some testing drivers/machines."""
|
||||
|
||||
class TestingLabelPrinterDriver(LabelPrinterBaseDriver):
|
||||
"""Test driver for label printing."""
|
||||
|
||||
SLUG = 'test-label-printer-api'
|
||||
NAME = 'Test label printer'
|
||||
DESCRIPTION = 'This is a test label printer driver for testing.'
|
||||
|
||||
MACHINE_SETTINGS = {
|
||||
'TEST_SETTING': {
|
||||
'name': 'Test setting',
|
||||
'description': 'This is a test setting',
|
||||
}
|
||||
}
|
||||
|
||||
def restart_machine(self, machine: BaseMachineType):
|
||||
"""Override restart_machine."""
|
||||
machine.set_status_text('Restarting...')
|
||||
|
||||
def print_label(self, *args, **kwargs) -> None:
|
||||
"""Override print_label."""
|
||||
|
||||
class TestingLabelPrinterDriverError1(LabelPrinterBaseDriver):
|
||||
"""Test driver for label printing."""
|
||||
|
||||
SLUG = 'test-label-printer-error'
|
||||
NAME = 'Test label printer error'
|
||||
DESCRIPTION = 'This is a test label printer driver for testing.'
|
||||
|
||||
def print_label(self, *args, **kwargs) -> None:
|
||||
"""Override print_label."""
|
||||
|
||||
class TestingLabelPrinterDriverError2(LabelPrinterBaseDriver):
|
||||
"""Test driver for label printing."""
|
||||
|
||||
SLUG = 'test-label-printer-error'
|
||||
NAME = 'Test label printer error'
|
||||
DESCRIPTION = 'This is a test label printer driver for testing.'
|
||||
|
||||
def print_label(self, *args, **kwargs) -> None:
|
||||
"""Override print_label."""
|
||||
|
||||
class TestingLabelPrinterDriverNotImplemented(LabelPrinterBaseDriver):
|
||||
"""Test driver for label printing."""
|
||||
|
||||
SLUG = 'test-label-printer-not-implemented'
|
||||
NAME = 'Test label printer error not implemented'
|
||||
DESCRIPTION = 'This is a test label printer driver for testing.'
|
||||
|
||||
registry.initialize()
|
||||
|
||||
# Ensure the test plugin is loaded
|
||||
plg_registry.set_plugin_state('label-printer-test-plugin', True)
|
||||
|
||||
super().setUp()
|
||||
|
||||
def test_machine_type_list(self):
|
||||
@@ -99,6 +53,8 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase):
|
||||
|
||||
def test_machine_driver_list(self):
|
||||
"""Test machine driver list API endpoint."""
|
||||
# Enable the built-in
|
||||
|
||||
response = self.get(reverse('api-machine-drivers'))
|
||||
driver = [a for a in response.data if a['slug'] == 'test-label-printer-api']
|
||||
self.assertEqual(len(driver), 1)
|
||||
@@ -116,7 +72,11 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase):
|
||||
'driver_errors': [],
|
||||
},
|
||||
)
|
||||
self.assertEqual(driver['provider_file'], __file__)
|
||||
|
||||
# Check that the driver is provided from the correct plugin file
|
||||
self.assertTrue(
|
||||
driver['provider_file'].endswith('plugin/testing/label_machines.py')
|
||||
)
|
||||
|
||||
# Test driver with errors
|
||||
driver_instance = cast(
|
||||
@@ -133,7 +93,11 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase):
|
||||
|
||||
def test_machine_status(self):
|
||||
"""Test machine status API endpoint."""
|
||||
response = self.get(reverse('api-machine-registry-status'))
|
||||
# Force a registry reload to ensure all machines are registered
|
||||
registry.reload_machines()
|
||||
|
||||
url = reverse('api-machine-registry-status')
|
||||
response = self.get(url)
|
||||
errors_msgs = [e['message'] for e in response.data['registry_errors']]
|
||||
|
||||
required_patterns = [
|
||||
@@ -197,8 +161,9 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase):
|
||||
}
|
||||
|
||||
# Create a machine
|
||||
# Note: Many DB hits as the entire machine registry is reloaded
|
||||
response = self.post(
|
||||
reverse('api-machine-list'), machine_data, max_query_count=150
|
||||
reverse('api-machine-list'), machine_data, max_query_count=300
|
||||
)
|
||||
|
||||
self.assertEqual(response.data, {**response.data, **machine_data})
|
||||
@@ -290,6 +255,29 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase):
|
||||
[(s['config_type'], s['key']) for s in response.data],
|
||||
)
|
||||
|
||||
def test_machine_settings_list(self):
|
||||
"""Test machine settings list API endpoint."""
|
||||
machine = MachineConfig.objects.create(
|
||||
machine_type='label-printer',
|
||||
driver='test-label-printer-api',
|
||||
name='Test Machine',
|
||||
active=True,
|
||||
)
|
||||
|
||||
url = reverse('api-machine-settings', kwargs={'pk': machine.pk})
|
||||
response = self.get(url)
|
||||
|
||||
self.assertEqual(len(response.data), 2)
|
||||
|
||||
keys = [s['key'] for s in response.data]
|
||||
|
||||
self.assertIn('LOCATION', keys)
|
||||
self.assertIn('TEST_SETTING', keys)
|
||||
|
||||
for item in response.data:
|
||||
for key in ['api_url', 'pk', 'typ', 'key']:
|
||||
self.assertIn(key, item)
|
||||
|
||||
def test_machine_restart(self):
|
||||
"""Test machine restart API endpoint."""
|
||||
machine = MachineConfig.objects.create(
|
||||
|
@@ -1,17 +1,12 @@
|
||||
"""Machine app tests."""
|
||||
|
||||
from typing import cast
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
from django.apps import apps
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase
|
||||
from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus
|
||||
from machine.machine_types.label_printer import LabelPrinterBaseDriver
|
||||
from machine.models import MachineConfig
|
||||
from machine.registry import registry
|
||||
from part.models import Part
|
||||
@@ -42,102 +37,90 @@ class TestDriverMachineInterface(TestMachineRegistryMixin, TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Setup some testing drivers/machines."""
|
||||
|
||||
class TestingMachineBaseDriver(BaseDriver):
|
||||
"""Test base driver for testing machines."""
|
||||
|
||||
machine_type = 'testing-type'
|
||||
|
||||
class TestingMachineType(BaseMachineType):
|
||||
"""Test machine type for testing."""
|
||||
|
||||
SLUG = 'testing-type'
|
||||
NAME = 'Testing machine type'
|
||||
DESCRIPTION = 'This is a test machine type for testing.'
|
||||
|
||||
base_driver = TestingMachineBaseDriver
|
||||
|
||||
class TestingMachineTypeStatus(MachineStatus):
|
||||
"""Test machine status."""
|
||||
|
||||
UNKNOWN = 100, 'Unknown', 'secondary'
|
||||
|
||||
MACHINE_STATUS = TestingMachineTypeStatus
|
||||
default_machine_status = MACHINE_STATUS.UNKNOWN
|
||||
|
||||
class TestingDriver(TestingMachineBaseDriver):
|
||||
"""Test driver for testing machines."""
|
||||
|
||||
SLUG = 'test-driver'
|
||||
NAME = 'Test Driver'
|
||||
DESCRIPTION = 'This is a test driver for testing.'
|
||||
|
||||
MACHINE_SETTINGS = {
|
||||
'TEST_SETTING': {'name': 'Test Setting', 'description': 'Test setting'}
|
||||
}
|
||||
|
||||
# mock driver implementation
|
||||
self.driver_mocks = {
|
||||
k: Mock()
|
||||
for k in [
|
||||
'init_driver',
|
||||
'init_machine',
|
||||
'update_machine',
|
||||
'restart_machine',
|
||||
]
|
||||
}
|
||||
|
||||
for key, value in self.driver_mocks.items():
|
||||
setattr(TestingDriver, key, value)
|
||||
|
||||
self.machine1 = MachineConfig.objects.create(
|
||||
name='Test Machine 1',
|
||||
machine_type='testing-type',
|
||||
driver='test-driver',
|
||||
active=True,
|
||||
)
|
||||
self.machine2 = MachineConfig.objects.create(
|
||||
name='Test Machine 2',
|
||||
machine_type='testing-type',
|
||||
driver='test-driver',
|
||||
active=True,
|
||||
)
|
||||
self.machine3 = MachineConfig.objects.create(
|
||||
name='Test Machine 3',
|
||||
machine_type='testing-type',
|
||||
driver='test-driver',
|
||||
active=False,
|
||||
)
|
||||
self.machines = [self.machine1, self.machine2, self.machine3]
|
||||
|
||||
# init registry
|
||||
plg_registry.reload_plugins()
|
||||
registry.initialize(main=True)
|
||||
|
||||
# mock machine implementation
|
||||
self.machine_mocks = {
|
||||
m: {k: MagicMock() for k in ['update', 'restart']} for m in self.machines
|
||||
}
|
||||
for machine_config, mock_dict in self.machine_mocks.items():
|
||||
for key, mock in mock_dict.items():
|
||||
mock.side_effect = getattr(machine_config.machine, key)
|
||||
setattr(machine_config.machine, key, mock)
|
||||
|
||||
super().setUp()
|
||||
|
||||
def test_machine_lifecycle(self):
|
||||
"""Test the machine lifecycle."""
|
||||
# Check initial conditions of the machine registry
|
||||
self.assertEqual(len(registry.get_machine_types()), 1)
|
||||
self.assertEqual(len(registry.get_driver_types()), 0)
|
||||
self.assertEqual(len(registry.get_machines()), 0)
|
||||
|
||||
# Enable sample plugin
|
||||
plg_registry.set_plugin_state('sample-printer-machine-plugin', True)
|
||||
|
||||
# Check state now
|
||||
self.assertEqual(len(registry.get_machine_types()), 1)
|
||||
self.assertEqual(len(registry.get_driver_types()), 1)
|
||||
self.assertEqual(len(registry.get_machines()), 0)
|
||||
|
||||
# Enable test plugin
|
||||
plg_registry.set_plugin_state('label-printer-test-plugin', True)
|
||||
|
||||
# Check state now
|
||||
self.assertEqual(len(registry.get_machine_types()), 1)
|
||||
self.assertEqual(len(registry.get_driver_types()), 3)
|
||||
self.assertEqual(len(registry.get_machines()), 0)
|
||||
|
||||
# Check for expected machine registry errors
|
||||
self.assertEqual(len(registry.errors), 2)
|
||||
self.assertIn(
|
||||
"Cannot re-register driver 'test-label-printer-error'",
|
||||
str(registry.errors[0]),
|
||||
)
|
||||
self.assertIn(
|
||||
'did not override the required attributes', str(registry.errors[1])
|
||||
)
|
||||
|
||||
# Check for expected machine types
|
||||
for slug in [
|
||||
'sample-printer-driver',
|
||||
'test-label-printer-api',
|
||||
'test-label-printer-error',
|
||||
]:
|
||||
instance = registry.get_driver_instance(slug)
|
||||
self.assertIsNotNone(instance, f"Driver '{slug}' should be registered")
|
||||
|
||||
# Next, un-register some plugins
|
||||
plg_registry.set_plugin_state('label-printer-test-plugin', False)
|
||||
|
||||
self.assertEqual(len(registry.get_driver_types()), 1)
|
||||
self.assertEqual(len(registry.errors), 0)
|
||||
|
||||
driver = registry.get_driver_types()[0]
|
||||
self.assertEqual(driver.SLUG, 'sample-printer-driver')
|
||||
|
||||
# Create some new label printing machines
|
||||
machines = [
|
||||
MachineConfig.objects.create(
|
||||
name=f'Test Machine {i}',
|
||||
machine_type='label-printer',
|
||||
driver=driver.SLUG,
|
||||
active=i < 3,
|
||||
)
|
||||
for i in range(1, 4)
|
||||
]
|
||||
|
||||
self.assertEqual(MachineConfig.objects.count(), 3)
|
||||
|
||||
# test that the registry is initialized correctly
|
||||
self.assertEqual(len(registry.machines), 3)
|
||||
self.assertEqual(len(registry.driver_instances), 1)
|
||||
|
||||
# test get_machines
|
||||
self.assertEqual(len(registry.get_machines()), 2)
|
||||
self.assertEqual(len(registry.get_machines(initialized=None)), 3)
|
||||
self.assertEqual(len(registry.get_machines(active=True)), 2)
|
||||
self.assertEqual(len(registry.get_machines(active=False, initialized=False)), 1)
|
||||
|
||||
self.assertEqual(len(registry.get_machines(name='Test Machine 1')), 1)
|
||||
|
||||
self.assertEqual(
|
||||
len(registry.get_machines(name='Test Machine 1', active=False)), 0
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
len(registry.get_machines(name='Test Machine 1', active=True)), 1
|
||||
)
|
||||
@@ -150,104 +133,87 @@ class TestDriverMachineInterface(TestMachineRegistryMixin, TestCase):
|
||||
registry.get_machines(unknown_filter='test')
|
||||
|
||||
# test get_machine
|
||||
self.assertEqual(registry.get_machine(self.machine1.pk), self.machine1.machine)
|
||||
|
||||
# test get_drivers
|
||||
self.assertEqual(len(registry.get_drivers('testing-type')), 1)
|
||||
self.assertEqual(registry.get_drivers('testing-type')[0].SLUG, 'test-driver')
|
||||
|
||||
# test that init hooks where called correctly
|
||||
CALL_COUNT = range(1, 5) # Due to interplay between plugin and machine registry
|
||||
self.assertIn(self.driver_mocks['init_driver'].call_count, CALL_COUNT)
|
||||
self.assertIn(self.driver_mocks['init_machine'].call_count, CALL_COUNT)
|
||||
machine = machines[0]
|
||||
self.assertEqual(registry.get_machine(machine.pk), machine.machine)
|
||||
|
||||
# Test machine restart hook
|
||||
registry.restart_machine(self.machine1.machine)
|
||||
|
||||
self.assertEqual(self.machine_mocks[self.machine1]['restart'].call_count, 1)
|
||||
|
||||
# Test machine update hook
|
||||
self.machine1.name = 'Test Machine 1 - Updated'
|
||||
self.machine1.save()
|
||||
self.driver_mocks['update_machine'].assert_called_once()
|
||||
self.assertEqual(self.machine_mocks[self.machine1]['update'].call_count, 1)
|
||||
old_machine_state, machine = self.driver_mocks['update_machine'].call_args.args
|
||||
self.assertEqual(old_machine_state['name'], 'Test Machine 1')
|
||||
self.assertEqual(machine.name, 'Test Machine 1 - Updated')
|
||||
self.assertEqual(self.machine1.machine, machine)
|
||||
self.machine_mocks[self.machine1]['update'].reset_mock()
|
||||
|
||||
# get ref to machine 1
|
||||
machine1: BaseMachineType = self.machine1.machine # type: ignore
|
||||
self.assertIsNotNone(machine1)
|
||||
|
||||
# Test machine setting update hook
|
||||
self.assertEqual(machine1.get_setting('TEST_SETTING', 'D'), '')
|
||||
machine1.set_setting('TEST_SETTING', 'D', 'test-value')
|
||||
self.assertEqual(self.machine_mocks[self.machine1]['update'].call_count, 2)
|
||||
old_machine_state, machine = self.driver_mocks['update_machine'].call_args.args
|
||||
self.assertEqual(old_machine_state['settings']['D', 'TEST_SETTING'], '')
|
||||
self.assertEqual(machine1.get_setting('TEST_SETTING', 'D'), 'test-value')
|
||||
self.assertEqual(self.machine1.machine, machine)
|
||||
registry.restart_machine(machine.machine)
|
||||
|
||||
# Test remove machine
|
||||
self.assertEqual(len(registry.get_machines()), 2)
|
||||
registry.remove_machine(machine1)
|
||||
registry.remove_machine(machine)
|
||||
self.assertEqual(len(registry.get_machines()), 1)
|
||||
|
||||
|
||||
class TestLabelPrinterMachineType(TestMachineRegistryMixin, InvenTreeAPITestCase):
|
||||
class TestLabelPrinterMachineType(InvenTreeAPITestCase):
|
||||
"""Test the label printer machine type."""
|
||||
|
||||
fixtures = ['category', 'part', 'location', 'stock']
|
||||
|
||||
def setUp(self):
|
||||
"""Setup the label printer machine type."""
|
||||
super().setUp()
|
||||
def test_registration(self):
|
||||
"""Test that the machine is correctly registered from the plugin."""
|
||||
PLG_KEY = 'label-printer-test-plugin'
|
||||
DRIVER_KEY = 'test-label-printer-api'
|
||||
|
||||
class TestingLabelPrinterDriver(LabelPrinterBaseDriver):
|
||||
"""Label printer driver for testing."""
|
||||
# Test that the machine is only available when the plugin is enabled
|
||||
for enabled in [False, True, False, True, False]:
|
||||
plg_registry.set_plugin_state(PLG_KEY, enabled)
|
||||
machine = registry.get_driver_instance(DRIVER_KEY)
|
||||
if enabled:
|
||||
self.assertIsNotNone(machine)
|
||||
else:
|
||||
self.assertIsNone(machine)
|
||||
|
||||
SLUG = 'testing-label-printer'
|
||||
NAME = 'Testing Label Printer'
|
||||
DESCRIPTION = 'This is a test label printer driver for testing.'
|
||||
def create_machine(self):
|
||||
"""Create a new label printing machine."""
|
||||
registry.initialize(main=True)
|
||||
|
||||
class PrintingOptionsSerializer(
|
||||
LabelPrinterBaseDriver.PrintingOptionsSerializer
|
||||
):
|
||||
"""Test printing options serializer."""
|
||||
PLG_KEY = 'label-printer-test-plugin'
|
||||
DRIVER_KEY = 'test-label-printer-api'
|
||||
|
||||
test_option = serializers.IntegerField()
|
||||
# Ensure that the driver is initialized
|
||||
plg_registry.set_plugin_state(PLG_KEY, True)
|
||||
|
||||
def print_label(self, *args, **kwargs):
|
||||
"""Mock print label method so that there are no errors."""
|
||||
driver = registry.get_driver_instance(DRIVER_KEY)
|
||||
self.assertIsNotNone(driver)
|
||||
|
||||
self.machine = MachineConfig.objects.create(
|
||||
machine_config = MachineConfig.objects.create(
|
||||
name='Test Label Printer',
|
||||
machine_type='label-printer',
|
||||
driver='testing-label-printer',
|
||||
driver=DRIVER_KEY,
|
||||
active=True,
|
||||
)
|
||||
|
||||
registry.initialize(main=True)
|
||||
driver_instance = cast(
|
||||
TestingLabelPrinterDriver,
|
||||
registry.get_driver_instance('testing-label-printer'),
|
||||
)
|
||||
machine = registry.get_machine(machine_config.pk)
|
||||
self.assertIsNotNone(machine)
|
||||
self.assertIsNotNone(machine.base_driver)
|
||||
self.assertIsNotNone(machine.driver)
|
||||
|
||||
self.print_label = Mock()
|
||||
driver_instance.print_label = self.print_label
|
||||
return machine
|
||||
|
||||
self.print_labels = Mock(side_effect=driver_instance.print_labels)
|
||||
driver_instance.print_labels = self.print_labels
|
||||
def test_call_function(self):
|
||||
"""Test arbitrary function calls against a machine."""
|
||||
from machine.registry import call_machine_function
|
||||
|
||||
machine = self.create_machine()
|
||||
|
||||
with self.assertRaises(AttributeError):
|
||||
call_machine_function(machine.pk, 'fake_function', custom_arg=123)
|
||||
|
||||
result = call_machine_function(machine.pk, 'custom_func', x=3, y=4)
|
||||
|
||||
self.assertEqual(result, 12)
|
||||
|
||||
def test_print_label(self):
|
||||
"""Test the print label method."""
|
||||
plugin_ref = 'inventreelabelmachine'
|
||||
|
||||
machine = self.create_machine()
|
||||
|
||||
# setup the label app
|
||||
apps.get_app_config('report').create_default_labels() # type: ignore
|
||||
plg_registry.reload_plugins()
|
||||
|
||||
config = cast(PluginConfig, plg_registry.get_plugin(plugin_ref).plugin_config()) # type: ignore
|
||||
config.active = True
|
||||
config.save()
|
||||
@@ -257,53 +223,56 @@ class TestLabelPrinterMachineType(TestMachineRegistryMixin, InvenTreeAPITestCase
|
||||
|
||||
url = reverse('api-label-print')
|
||||
|
||||
self.post(
|
||||
with self.assertLogs('inventree', level='WARNING') as cm:
|
||||
self.post(
|
||||
url,
|
||||
{
|
||||
'plugin': config.key,
|
||||
'items': [a.pk for a in parts],
|
||||
'template': template.pk,
|
||||
'machine': str(machine.pk),
|
||||
'driver_options': {'copies': '3', 'fake_option': 99},
|
||||
},
|
||||
expected_code=201,
|
||||
)
|
||||
|
||||
# 4 entries for each printed label
|
||||
self.assertEqual(len(cm.output), 10)
|
||||
|
||||
# Check for expected messages
|
||||
messages = [
|
||||
'Printing Label: TestingLabelPrinterDriver',
|
||||
f'machine: {machine.pk}',
|
||||
f'label: {template.pk}',
|
||||
f'item: {parts[0].pk}',
|
||||
f'item: {parts[1].pk}',
|
||||
'options: copies: 3',
|
||||
]
|
||||
|
||||
for message in messages:
|
||||
result = False
|
||||
for item in cm.records:
|
||||
if message in str(item):
|
||||
result = True
|
||||
break
|
||||
|
||||
self.assertTrue(result, f'Message not found: {message}')
|
||||
|
||||
# test with non existing machine
|
||||
response = self.post(
|
||||
url,
|
||||
{
|
||||
'plugin': config.key,
|
||||
'machine': 'dummy-uuid-which-does-not-exist',
|
||||
'driver_options': {'copies': '1', 'test_option': '2'},
|
||||
'items': [a.pk for a in parts],
|
||||
'template': template.pk,
|
||||
'machine': str(self.machine.pk),
|
||||
'driver_options': {'copies': '1', 'test_option': '2'},
|
||||
},
|
||||
expected_code=201,
|
||||
)
|
||||
|
||||
# test the print labels method call
|
||||
self.print_labels.assert_called_once()
|
||||
self.assertEqual(self.print_labels.call_args.args[0], self.machine.machine)
|
||||
self.assertEqual(self.print_labels.call_args.args[1], template)
|
||||
|
||||
self.assertIn('printing_options', self.print_labels.call_args.kwargs)
|
||||
self.assertEqual(
|
||||
self.print_labels.call_args.kwargs['printing_options'],
|
||||
{'copies': 1, 'test_option': 2},
|
||||
)
|
||||
|
||||
return
|
||||
# TODO re-activate test
|
||||
|
||||
# test the single print label method calls
|
||||
self.assertEqual(self.print_label.call_count, 2)
|
||||
self.assertEqual(self.print_label.call_args.args[0], self.machine.machine)
|
||||
self.assertEqual(self.print_label.call_args.args[1], template)
|
||||
self.assertEqual(self.print_label.call_args.args[2], parts[1])
|
||||
self.assertIn('printing_options', self.print_labels.call_args.kwargs)
|
||||
self.assertEqual(
|
||||
self.print_labels.call_args.kwargs['printing_options'],
|
||||
{'copies': 1, 'test_option': 2},
|
||||
)
|
||||
|
||||
# test with non existing machine
|
||||
self.post(
|
||||
url,
|
||||
{
|
||||
'machine': self.placeholder_uuid,
|
||||
'driver_options': {'copies': '1', 'test_option': '2'},
|
||||
},
|
||||
expected_code=400,
|
||||
)
|
||||
|
||||
self.assertIn('is not a valid choice', str(response.data['machine']))
|
||||
|
||||
|
||||
class AdminTest(AdminTestCase):
|
||||
"""Tests for the admin interface integration."""
|
||||
|
@@ -0,0 +1,41 @@
|
||||
"""Plugin mixin for registering machine drivers."""
|
||||
|
||||
import structlog
|
||||
|
||||
from machine.machine_type import BaseDriver, BaseMachineType
|
||||
from plugin import PluginMixinEnum
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
||||
class MachineDriverMixin:
|
||||
"""Mixin class for registering machine driver types.
|
||||
|
||||
This mixin class can be used to register custom machine types or drivers.
|
||||
|
||||
get_machine_types:
|
||||
- Register a custom class of machine
|
||||
- Returns a list of BaseMachineType objects
|
||||
|
||||
get_machine_drivers:
|
||||
- Register custom machine drivers for existing machine types
|
||||
- Returns a list of BaseDriver objects
|
||||
"""
|
||||
|
||||
class MixinMeta:
|
||||
"""Meta class for MachineDriverMixin."""
|
||||
|
||||
MIXIN_NAME = 'MachineDriver'
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the mixin and register it."""
|
||||
super().__init__()
|
||||
self.add_mixin(PluginMixinEnum.MACHINE, True, __class__)
|
||||
|
||||
def get_machine_types(self) -> list[BaseMachineType]:
|
||||
"""Register custom machine types."""
|
||||
return []
|
||||
|
||||
def get_machine_drivers(self) -> list[BaseDriver]:
|
||||
"""Register custom machine drivers."""
|
||||
return []
|
@@ -0,0 +1,28 @@
|
||||
"""Base plugin which defines the built-in machine types."""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from machine.machine_type import BaseDriver, BaseMachineType
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import MachineDriverMixin
|
||||
|
||||
|
||||
class InvenTreeMachineTypes(MachineDriverMixin, InvenTreePlugin):
|
||||
"""Plugin which provides built-in machine type definitions."""
|
||||
|
||||
NAME = 'InvenTreeMachines'
|
||||
SLUG = 'inventree-machines'
|
||||
TITLE = _('InvenTree Machines')
|
||||
DESCRIPTION = _('Built-in machine types for InvenTree')
|
||||
AUTHOR = _('InvenTree contributors')
|
||||
VERSION = '1.0.0'
|
||||
|
||||
def get_machine_types(self) -> list[BaseMachineType]:
|
||||
"""Return all built-in machine types."""
|
||||
from machine.machine_types.label_printer import LabelPrinterMachine
|
||||
|
||||
return [LabelPrinterMachine]
|
||||
|
||||
def get_machine_drivers(self) -> list[BaseDriver]:
|
||||
"""Return all built-in machine drivers."""
|
||||
return []
|
@@ -8,6 +8,7 @@ from plugin.base.integration.APICallMixin import APICallMixin
|
||||
from plugin.base.integration.AppMixin import AppMixin
|
||||
from plugin.base.integration.CurrencyExchangeMixin import CurrencyExchangeMixin
|
||||
from plugin.base.integration.DataExport import DataExportMixin
|
||||
from plugin.base.integration.MachineMixin import MachineDriverMixin
|
||||
from plugin.base.integration.NavigationMixin import NavigationMixin
|
||||
from plugin.base.integration.NotificationMixin import NotificationMixin
|
||||
from plugin.base.integration.ReportMixin import ReportMixin
|
||||
@@ -32,6 +33,7 @@ __all__ = [
|
||||
'IconPackMixin',
|
||||
'LabelPrintingMixin',
|
||||
'LocateMixin',
|
||||
'MachineDriverMixin',
|
||||
'MailMixin',
|
||||
'NavigationMixin',
|
||||
'NotificationMixin',
|
||||
|
@@ -272,6 +272,10 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
|
||||
offload_task(
|
||||
plugin.staticfiles.copy_plugin_static_files, self.key, group='plugin'
|
||||
)
|
||||
else:
|
||||
offload_task(
|
||||
plugin.staticfiles.clear_plugin_static_files, self.key, group='plugin'
|
||||
)
|
||||
|
||||
|
||||
class PluginSetting(common.models.BaseInvenTreeSetting):
|
||||
|
@@ -67,6 +67,7 @@ class PluginMixinEnum(StringEnum):
|
||||
ICON_PACK = 'icon_pack'
|
||||
LABELS = 'labels'
|
||||
LOCATE = 'locate'
|
||||
MACHINE = 'machine'
|
||||
MAIL = 'mail'
|
||||
NAVIGATION = 'navigation'
|
||||
NOTIFICATION = 'notification'
|
||||
|
@@ -94,6 +94,7 @@ class PluginsRegistry:
|
||||
'bom-exporter',
|
||||
'inventree-exporter',
|
||||
'inventree-ui-notification',
|
||||
'inventree-machines',
|
||||
'inventree-email-notification',
|
||||
'inventreecurrencyexchange',
|
||||
'inventreelabel',
|
||||
@@ -489,6 +490,9 @@ class PluginsRegistry:
|
||||
# If in TEST or DEBUG mode, load plugins from the 'samples' directory
|
||||
dirs.append('plugin.samples')
|
||||
|
||||
if settings.TESTING:
|
||||
dirs.append('plugin.testing')
|
||||
|
||||
if settings.TESTING:
|
||||
custom_dirs = os.getenv('INVENTREE_PLUGIN_TEST_DIR', None)
|
||||
else: # pragma: no cover
|
||||
|
@@ -0,0 +1,63 @@
|
||||
"""Sample plugin for registering custom machines."""
|
||||
|
||||
from django.db import models
|
||||
|
||||
import structlog
|
||||
|
||||
from machine.machine_type import BaseDriver
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.machine import BaseMachineType
|
||||
from plugin.machine.machine_types import LabelPrinterBaseDriver, LabelPrinterMachine
|
||||
from plugin.mixins import MachineDriverMixin, SettingsMixin
|
||||
from report.models import LabelTemplate
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
||||
class SamplePrinterDriver(LabelPrinterBaseDriver):
|
||||
"""Sample printer driver."""
|
||||
|
||||
SLUG = 'sample-printer-driver'
|
||||
NAME = 'Sample Label Printer Driver'
|
||||
DESCRIPTION = 'Sample label printing driver for InvenTree'
|
||||
|
||||
MACHINE_SETTINGS = {
|
||||
'CONNECTION': {
|
||||
'name': 'Connection String',
|
||||
'description': 'Custom string for connecting to the printer',
|
||||
'default': '123-xx123:8000',
|
||||
}
|
||||
}
|
||||
|
||||
def init_machine(self, machine: BaseMachineType) -> None:
|
||||
"""Machine initialization hook."""
|
||||
|
||||
def print_label(
|
||||
self,
|
||||
machine: LabelPrinterMachine,
|
||||
label: LabelTemplate,
|
||||
item: models.Model,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Send the label to the printer."""
|
||||
print('MOCK LABEL PRINTING:')
|
||||
print('- machine:', machine)
|
||||
print('- label:', label)
|
||||
print('- item:', item)
|
||||
|
||||
|
||||
class SamplePrinterMachine(MachineDriverMixin, SettingsMixin, InvenTreePlugin):
|
||||
"""A very simple example of a 'printer' machine plugin.
|
||||
|
||||
This plugin class simply prints a message to the logger.
|
||||
"""
|
||||
|
||||
NAME = 'SamplePrinterMachine'
|
||||
SLUG = 'sample-printer-machine-plugin'
|
||||
TITLE = 'Sample dummy plugin for printing labels'
|
||||
|
||||
VERSION = '0.1'
|
||||
|
||||
def get_machine_drivers(self) -> list[BaseDriver]:
|
||||
"""Return a list of drivers registered by this plugin."""
|
||||
return [SamplePrinterDriver]
|
@@ -489,9 +489,12 @@ class RegistryTests(TestQueryMixin, PluginRegistryMixin, TestCase):
|
||||
# Start with a 'clean slate'
|
||||
PluginConfig.objects.all().delete()
|
||||
|
||||
# Change this value whenever a new mandatory plugin is added
|
||||
N_MANDATORY_PLUGINS = 10
|
||||
|
||||
registry.reload_plugins(full_reload=True, collect=True)
|
||||
mandatory = registry.MANDATORY_PLUGINS
|
||||
self.assertEqual(len(mandatory), 9)
|
||||
self.assertEqual(len(mandatory), N_MANDATORY_PLUGINS)
|
||||
|
||||
# Check that the mandatory plugins are loaded
|
||||
self.assertEqual(
|
||||
|
0
src/backend/InvenTree/plugin/testing/__init__.py
Normal file
0
src/backend/InvenTree/plugin/testing/__init__.py
Normal file
99
src/backend/InvenTree/plugin/testing/label_machines.py
Normal file
99
src/backend/InvenTree/plugin/testing/label_machines.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Plugins for testing label machines."""
|
||||
|
||||
import structlog
|
||||
|
||||
from machine.machine_type import BaseDriver
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.machine import BaseMachineType
|
||||
from plugin.machine.machine_types import LabelPrinterBaseDriver
|
||||
from plugin.mixins import MachineDriverMixin, SettingsMixin
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
||||
class TestingLabelPrinterDriver(LabelPrinterBaseDriver):
|
||||
"""Test driver for label printing."""
|
||||
|
||||
SLUG = 'test-label-printer-api'
|
||||
NAME = 'Test label printer'
|
||||
DESCRIPTION = 'This is a test label printer driver for testing.'
|
||||
|
||||
MACHINE_SETTINGS = {
|
||||
'TEST_SETTING': {
|
||||
'name': 'Test setting',
|
||||
'description': 'This is a test setting',
|
||||
}
|
||||
}
|
||||
|
||||
def restart_machine(self, machine: BaseMachineType):
|
||||
"""Override restart_machine."""
|
||||
machine.set_status_text('Restarting...')
|
||||
|
||||
def print_label(self, machine, label, item, **kwargs) -> None:
|
||||
"""Override print_label."""
|
||||
# Simply output some warning messages,
|
||||
# which we can check for in the unit test
|
||||
logger.warn('Printing Label: TestingLabelPrinterDriver')
|
||||
logger.warn(f'machine: {machine.pk}')
|
||||
logger.warn(f'label: {label.pk}')
|
||||
logger.warn(f'item: {item.pk}')
|
||||
|
||||
for k, v in kwargs['printing_options'].items():
|
||||
logger.warn(f'options: {k}: {v}')
|
||||
|
||||
def custom_func(self, *args, x=0, y=0):
|
||||
"""A custom function for the driver."""
|
||||
return x * y
|
||||
|
||||
|
||||
class TestingLabelPrinterDriverError1(LabelPrinterBaseDriver):
|
||||
"""Test driver for label printing."""
|
||||
|
||||
SLUG = 'test-label-printer-error'
|
||||
NAME = 'Test label printer error'
|
||||
DESCRIPTION = 'This is a test label printer driver for testing.'
|
||||
|
||||
def print_label(self, *args, **kwargs) -> None:
|
||||
"""Override print_label."""
|
||||
|
||||
|
||||
class TestingLabelPrinterDriverError2(LabelPrinterBaseDriver):
|
||||
"""Test driver for label printing."""
|
||||
|
||||
SLUG = 'test-label-printer-error'
|
||||
|
||||
NAME = 'Test label printer error'
|
||||
DESCRIPTION = 'This is a test label printer driver for testing.'
|
||||
|
||||
def print_label(self, *args, **kwargs) -> None:
|
||||
"""Override print_label."""
|
||||
|
||||
|
||||
class TestingLabelPrinterDriverNotImplemented(LabelPrinterBaseDriver):
|
||||
"""Test driver for label printing."""
|
||||
|
||||
SLUG = 'test-label-printer-not-implemented'
|
||||
NAME = 'Test label printer error not implemented'
|
||||
DESCRIPTION = 'This is a test label printer driver for testing.'
|
||||
|
||||
|
||||
class LabelPrinterMachineTest(MachineDriverMixin, SettingsMixin, InvenTreePlugin):
|
||||
"""A test plugin for label printer machines.
|
||||
|
||||
This plugin registers multiple driver types for unit testing.
|
||||
"""
|
||||
|
||||
NAME = 'LabelPrinterMachineTest'
|
||||
SLUG = 'label-printer-test-plugin'
|
||||
TITLE = 'Test plugin for label printer machines'
|
||||
|
||||
VERSION = '0.1'
|
||||
|
||||
def get_machine_drivers(self) -> list[BaseDriver]:
|
||||
"""Return a list of drivers registered by this plugin."""
|
||||
return [
|
||||
TestingLabelPrinterDriver,
|
||||
TestingLabelPrinterDriverError1,
|
||||
TestingLabelPrinterDriverError2,
|
||||
TestingLabelPrinterDriverNotImplemented,
|
||||
]
|
Reference in New Issue
Block a user