mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-01 04:56:45 +00:00
Merge pull request #3014 from matmair/matmair/issue3005
Add more tests for offload_task
This commit is contained in:
commit
334025b844
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
import warnings
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -11,6 +12,8 @@ from django.utils import timezone
|
|||||||
|
|
||||||
from django.core.exceptions import AppRegistryNotReady
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
from django.core import mail as django_mail
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("inventree")
|
logger = logging.getLogger("inventree")
|
||||||
@ -52,6 +55,15 @@ def schedule_task(taskname, **kwargs):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def raise_warning(msg):
|
||||||
|
"""Log and raise a warning"""
|
||||||
|
logger.warning(msg)
|
||||||
|
|
||||||
|
# If testing is running raise a warning that can be asserted
|
||||||
|
if settings.TESTING:
|
||||||
|
warnings.warn(msg)
|
||||||
|
|
||||||
|
|
||||||
def offload_task(taskname, *args, force_sync=False, **kwargs):
|
def offload_task(taskname, *args, force_sync=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
Create an AsyncTask if workers are running.
|
Create an AsyncTask if workers are running.
|
||||||
@ -67,6 +79,11 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
|
|||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
from InvenTree.status import is_worker_running
|
from InvenTree.status import is_worker_running
|
||||||
|
except AppRegistryNotReady: # pragma: no cover
|
||||||
|
logger.warning(f"Could not offload task '{taskname}' - app registry not ready")
|
||||||
|
return
|
||||||
|
except (OperationalError, ProgrammingError): # pragma: no cover
|
||||||
|
raise_warning(f"Could not offload task '{taskname}' - database not ready")
|
||||||
|
|
||||||
if is_worker_running() and not force_sync: # pragma: no cover
|
if is_worker_running() and not force_sync: # pragma: no cover
|
||||||
# Running as asynchronous task
|
# Running as asynchronous task
|
||||||
@ -74,21 +91,26 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
|
|||||||
task = AsyncTask(taskname, *args, **kwargs)
|
task = AsyncTask(taskname, *args, **kwargs)
|
||||||
task.run()
|
task.run()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - Function not found")
|
raise_warning(f"WARNING: '{taskname}' not started - Function not found")
|
||||||
|
else:
|
||||||
|
|
||||||
|
if callable(taskname):
|
||||||
|
# function was passed - use that
|
||||||
|
_func = taskname
|
||||||
else:
|
else:
|
||||||
# Split path
|
# Split path
|
||||||
try:
|
try:
|
||||||
app, mod, func = taskname.split('.')
|
app, mod, func = taskname.split('.')
|
||||||
app_mod = app + '.' + mod
|
app_mod = app + '.' + mod
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - Malformed function path")
|
raise_warning(f"WARNING: '{taskname}' not started - Malformed function path")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Import module from app
|
# Import module from app
|
||||||
try:
|
try:
|
||||||
_mod = importlib.import_module(app_mod)
|
_mod = importlib.import_module(app_mod)
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'")
|
raise_warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Retrieve function
|
# Retrieve function
|
||||||
@ -102,18 +124,12 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
|
|||||||
if not _func:
|
if not _func:
|
||||||
_func = eval(func) # pragma: no cover
|
_func = eval(func) # pragma: no cover
|
||||||
except NameError:
|
except NameError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
|
raise_warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Workers are not running: run it as synchronous task
|
# Workers are not running: run it as synchronous task
|
||||||
_func(*args, **kwargs)
|
_func(*args, **kwargs)
|
||||||
|
|
||||||
except AppRegistryNotReady: # pragma: no cover
|
|
||||||
logger.warning(f"Could not offload task '{taskname}' - app registry not ready")
|
|
||||||
return
|
|
||||||
except (OperationalError, ProgrammingError): # pragma: no cover
|
|
||||||
logger.warning(f"Could not offload task '{taskname}' - database not ready")
|
|
||||||
|
|
||||||
|
|
||||||
def heartbeat():
|
def heartbeat():
|
||||||
"""
|
"""
|
||||||
@ -205,25 +221,25 @@ def check_for_updates():
|
|||||||
response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
|
response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}')
|
raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}') # pragma: no cover
|
||||||
|
|
||||||
data = json.loads(response.text)
|
data = json.loads(response.text)
|
||||||
|
|
||||||
tag = data.get('tag_name', None)
|
tag = data.get('tag_name', None)
|
||||||
|
|
||||||
if not tag:
|
if not tag:
|
||||||
raise ValueError("'tag_name' missing from GitHub response")
|
raise ValueError("'tag_name' missing from GitHub response") # pragma: no cover
|
||||||
|
|
||||||
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
|
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
|
||||||
|
|
||||||
if len(match.groups()) != 3:
|
if len(match.groups()) != 3: # pragma: no cover
|
||||||
logger.warning(f"Version '{tag}' did not match expected pattern")
|
logger.warning(f"Version '{tag}' did not match expected pattern")
|
||||||
return
|
return
|
||||||
|
|
||||||
latest_version = [int(x) for x in match.groups()]
|
latest_version = [int(x) for x in match.groups()]
|
||||||
|
|
||||||
if len(latest_version) != 3:
|
if len(latest_version) != 3:
|
||||||
raise ValueError(f"Version '{tag}' is not correct format")
|
raise ValueError(f"Version '{tag}' is not correct format") # pragma: no cover
|
||||||
|
|
||||||
logger.info(f"Latest InvenTree version: '{tag}'")
|
logger.info(f"Latest InvenTree version: '{tag}'")
|
||||||
|
|
||||||
@ -288,7 +304,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
|
|||||||
recipients = [recipients]
|
recipients = [recipients]
|
||||||
|
|
||||||
offload_task(
|
offload_task(
|
||||||
'django.core.mail.send_mail',
|
django_mail.send_mail,
|
||||||
subject,
|
subject,
|
||||||
body,
|
body,
|
||||||
from_email,
|
from_email,
|
||||||
|
@ -2,10 +2,20 @@
|
|||||||
Unit tests for task management
|
Unit tests for task management
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django_q.models import Schedule
|
from django_q.models import Schedule
|
||||||
|
|
||||||
|
from error_report.models import Error
|
||||||
|
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
|
|
||||||
|
threshold = timezone.now() - timedelta(days=30)
|
||||||
|
threshold_low = threshold - timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
class ScheduledTaskTests(TestCase):
|
class ScheduledTaskTests(TestCase):
|
||||||
@ -41,3 +51,79 @@ class ScheduledTaskTests(TestCase):
|
|||||||
# But the 'minutes' should have been updated
|
# But the 'minutes' should have been updated
|
||||||
t = Schedule.objects.get(func=task)
|
t = Schedule.objects.get(func=task)
|
||||||
self.assertEqual(t.minutes, 5)
|
self.assertEqual(t.minutes, 5)
|
||||||
|
|
||||||
|
|
||||||
|
def get_result():
|
||||||
|
"""Demo function for test_offloading"""
|
||||||
|
return 'abc'
|
||||||
|
|
||||||
|
|
||||||
|
class InvenTreeTaskTests(TestCase):
|
||||||
|
"""Unit tests for tasks"""
|
||||||
|
|
||||||
|
def test_offloading(self):
|
||||||
|
"""Test task offloading"""
|
||||||
|
|
||||||
|
# Run with function ref
|
||||||
|
InvenTree.tasks.offload_task(get_result)
|
||||||
|
|
||||||
|
# Run with string ref
|
||||||
|
InvenTree.tasks.offload_task('InvenTree.test_tasks.get_result')
|
||||||
|
|
||||||
|
# Error runs
|
||||||
|
# Malformed taskname
|
||||||
|
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree' not started - Malformed function path"):
|
||||||
|
InvenTree.tasks.offload_task('InvenTree')
|
||||||
|
|
||||||
|
# Non exsistent app
|
||||||
|
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTreeABC.test_tasks.doesnotmatter' not started - No module named 'InvenTreeABC.test_tasks'"):
|
||||||
|
InvenTree.tasks.offload_task('InvenTreeABC.test_tasks.doesnotmatter')
|
||||||
|
|
||||||
|
# Non exsistent function
|
||||||
|
with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree.test_tasks.doesnotexsist' not started - No function named 'doesnotexsist'"):
|
||||||
|
InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexsist')
|
||||||
|
|
||||||
|
def test_task_hearbeat(self):
|
||||||
|
"""Test the task heartbeat"""
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat)
|
||||||
|
|
||||||
|
def test_task_delete_successful_tasks(self):
|
||||||
|
"""Test the task delete_successful_tasks"""
|
||||||
|
from django_q.models import Success
|
||||||
|
|
||||||
|
Success.objects.create(name='abc', func='abc', stopped=threshold, started=threshold_low)
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.delete_successful_tasks)
|
||||||
|
results = Success.objects.filter(started__lte=threshold)
|
||||||
|
self.assertEqual(len(results), 0)
|
||||||
|
|
||||||
|
def test_task_delete_old_error_logs(self):
|
||||||
|
"""Test the task delete_old_error_logs"""
|
||||||
|
|
||||||
|
# Create error
|
||||||
|
error_obj = Error.objects.create()
|
||||||
|
error_obj.when = threshold_low
|
||||||
|
error_obj.save()
|
||||||
|
|
||||||
|
# Check that it is not empty
|
||||||
|
errors = Error.objects.filter(when__lte=threshold,)
|
||||||
|
self.assertNotEqual(len(errors), 0)
|
||||||
|
|
||||||
|
# Run action
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.delete_old_error_logs)
|
||||||
|
|
||||||
|
# Check that it is empty again
|
||||||
|
errors = Error.objects.filter(when__lte=threshold,)
|
||||||
|
self.assertEqual(len(errors), 0)
|
||||||
|
|
||||||
|
def test_task_check_for_updates(self):
|
||||||
|
"""Test the task check_for_updates"""
|
||||||
|
# Check that setting should be empty
|
||||||
|
self.assertEqual(InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION'), '')
|
||||||
|
|
||||||
|
# Get new version
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.check_for_updates)
|
||||||
|
|
||||||
|
# Check that setting is not empty
|
||||||
|
response = InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION')
|
||||||
|
self.assertNotEqual(response, '')
|
||||||
|
self.assertTrue(bool(response))
|
||||||
|
@ -795,13 +795,9 @@ class CurrencyRefreshView(RedirectView):
|
|||||||
On a POST request we will attempt to refresh the exchange rates
|
On a POST request we will attempt to refresh the exchange rates
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from InvenTree.tasks import offload_task
|
from InvenTree.tasks import offload_task, update_exchange_rates
|
||||||
|
|
||||||
# Define associated task from InvenTree.tasks list of methods
|
offload_task(update_exchange_rates, force_sync=True)
|
||||||
taskname = 'InvenTree.tasks.update_exchange_rates'
|
|
||||||
|
|
||||||
# Run it
|
|
||||||
offload_task(taskname, force_sync=True)
|
|
||||||
|
|
||||||
return redirect(reverse_lazy('settings'))
|
return redirect(reverse_lazy('settings'))
|
||||||
|
|
||||||
|
@ -1139,12 +1139,13 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
|
|||||||
"""
|
"""
|
||||||
Callback function to be executed after a Build instance is saved
|
Callback function to be executed after a Build instance is saved
|
||||||
"""
|
"""
|
||||||
|
from . import tasks as build_tasks
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
# A new Build has just been created
|
# A new Build has just been created
|
||||||
|
|
||||||
# Run checks on required parts
|
# Run checks on required parts
|
||||||
InvenTree.tasks.offload_task('build.tasks.check_build_stock', instance)
|
InvenTree.tasks.offload_task(build_tasks.check_build_stock, instance)
|
||||||
|
|
||||||
|
|
||||||
class BuildOrderAttachment(InvenTreeAttachment):
|
class BuildOrderAttachment(InvenTreeAttachment):
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from common.models import NotificationEntry
|
from common.models import NotificationEntry
|
||||||
|
from . import tasks as common_tasks
|
||||||
from InvenTree.tasks import offload_task
|
from InvenTree.tasks import offload_task
|
||||||
|
|
||||||
|
|
||||||
@ -14,4 +15,4 @@ class TaskTest(TestCase):
|
|||||||
|
|
||||||
# check empty run
|
# check empty run
|
||||||
self.assertEqual(NotificationEntry.objects.all().count(), 0)
|
self.assertEqual(NotificationEntry.objects.all().count(), 0)
|
||||||
offload_task('common.tasks.delete_old_notifications',)
|
offload_task(common_tasks.delete_old_notifications,)
|
||||||
|
@ -24,6 +24,7 @@ from plugin.registry import registry
|
|||||||
|
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
from plugin.base.label import label as plugin_label
|
||||||
|
|
||||||
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
||||||
from .serializers import StockItemLabelSerializer, StockLocationLabelSerializer, PartLabelSerializer
|
from .serializers import StockItemLabelSerializer, StockLocationLabelSerializer, PartLabelSerializer
|
||||||
@ -156,7 +157,7 @@ class LabelPrintMixin:
|
|||||||
|
|
||||||
# Offload a background task to print the provided label
|
# Offload a background task to print the provided label
|
||||||
offload_task(
|
offload_task(
|
||||||
'plugin.base.label.label.print_label',
|
plugin_label.print_label,
|
||||||
plugin.plugin_slug(),
|
plugin.plugin_slug(),
|
||||||
image,
|
image,
|
||||||
label_instance=label_instance,
|
label_instance=label_instance,
|
||||||
|
@ -59,7 +59,6 @@ from order import models as OrderModels
|
|||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
import part.settings as part_settings
|
import part.settings as part_settings
|
||||||
from stock import models as StockModels
|
from stock import models as StockModels
|
||||||
|
|
||||||
from plugin.models import MetadataMixin
|
from plugin.models import MetadataMixin
|
||||||
|
|
||||||
|
|
||||||
@ -2291,12 +2290,13 @@ def after_save_part(sender, instance: Part, created, **kwargs):
|
|||||||
"""
|
"""
|
||||||
Function to be executed after a Part is saved
|
Function to be executed after a Part is saved
|
||||||
"""
|
"""
|
||||||
|
from part import tasks as part_tasks
|
||||||
|
|
||||||
if not created and not InvenTree.ready.isImportingData():
|
if not created and not InvenTree.ready.isImportingData():
|
||||||
# Check part stock only if we are *updating* the part (not creating it)
|
# Check part stock only if we are *updating* the part (not creating it)
|
||||||
|
|
||||||
# Run this check in the background
|
# Run this check in the background
|
||||||
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance)
|
InvenTree.tasks.offload_task(part_tasks.notify_low_stock_if_required, instance)
|
||||||
|
|
||||||
|
|
||||||
class PartAttachment(InvenTreeAttachment):
|
class PartAttachment(InvenTreeAttachment):
|
||||||
|
@ -49,6 +49,6 @@ def notify_low_stock_if_required(part: part.models.Part):
|
|||||||
for p in parts:
|
for p in parts:
|
||||||
if p.is_part_low_on_stock():
|
if p.is_part_low_on_stock():
|
||||||
InvenTree.tasks.offload_task(
|
InvenTree.tasks.offload_task(
|
||||||
'part.tasks.notify_low_stock',
|
notify_low_stock,
|
||||||
p
|
p
|
||||||
)
|
)
|
||||||
|
@ -37,7 +37,7 @@ def trigger_event(event, *args, **kwargs):
|
|||||||
logger.debug(f"Event triggered: '{event}'")
|
logger.debug(f"Event triggered: '{event}'")
|
||||||
|
|
||||||
offload_task(
|
offload_task(
|
||||||
'plugin.events.register_event',
|
register_event,
|
||||||
event,
|
event,
|
||||||
*args,
|
*args,
|
||||||
**kwargs
|
**kwargs
|
||||||
@ -72,7 +72,7 @@ def register_event(event, *args, **kwargs):
|
|||||||
|
|
||||||
# Offload a separate task for each plugin
|
# Offload a separate task for each plugin
|
||||||
offload_task(
|
offload_task(
|
||||||
'plugin.events.process_event',
|
process_event,
|
||||||
slug,
|
slug,
|
||||||
event,
|
event,
|
||||||
*args,
|
*args,
|
||||||
|
@ -56,7 +56,7 @@ class LocatePluginView(APIView):
|
|||||||
try:
|
try:
|
||||||
StockItem.objects.get(pk=item_pk)
|
StockItem.objects.get(pk=item_pk)
|
||||||
|
|
||||||
offload_task('plugin.registry.call_function', plugin, 'locate_stock_item', item_pk)
|
offload_task(registry.call_function, plugin, 'locate_stock_item', item_pk)
|
||||||
|
|
||||||
data['item'] = item_pk
|
data['item'] = item_pk
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ class LocatePluginView(APIView):
|
|||||||
try:
|
try:
|
||||||
StockLocation.objects.get(pk=location_pk)
|
StockLocation.objects.get(pk=location_pk)
|
||||||
|
|
||||||
offload_task('plugin.registry.call_function', plugin, 'locate_stock_location', location_pk)
|
offload_task(registry.call_function, plugin, 'locate_stock_location', location_pk)
|
||||||
|
|
||||||
data['location'] = location_pk
|
data['location'] = location_pk
|
||||||
|
|
||||||
|
@ -2020,10 +2020,11 @@ def after_delete_stock_item(sender, instance: StockItem, **kwargs):
|
|||||||
"""
|
"""
|
||||||
Function to be executed after a StockItem object is deleted
|
Function to be executed after a StockItem object is deleted
|
||||||
"""
|
"""
|
||||||
|
from part import tasks as part_tasks
|
||||||
|
|
||||||
if not InvenTree.ready.isImportingData():
|
if not InvenTree.ready.isImportingData():
|
||||||
# Run this check in the background
|
# Run this check in the background
|
||||||
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part)
|
InvenTree.tasks.offload_task(part_tasks.notify_low_stock_if_required, instance.part)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=StockItem, dispatch_uid='stock_item_post_save_log')
|
@receiver(post_save, sender=StockItem, dispatch_uid='stock_item_post_save_log')
|
||||||
@ -2031,10 +2032,11 @@ def after_save_stock_item(sender, instance: StockItem, created, **kwargs):
|
|||||||
"""
|
"""
|
||||||
Hook function to be executed after StockItem object is saved/updated
|
Hook function to be executed after StockItem object is saved/updated
|
||||||
"""
|
"""
|
||||||
|
from part import tasks as part_tasks
|
||||||
|
|
||||||
if not InvenTree.ready.isImportingData():
|
if not InvenTree.ready.isImportingData():
|
||||||
# Run this check in the background
|
# Run this check in the background
|
||||||
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part)
|
InvenTree.tasks.offload_task(part_tasks.notify_low_stock_if_required, instance.part)
|
||||||
|
|
||||||
|
|
||||||
class StockItemAttachment(InvenTreeAttachment):
|
class StockItemAttachment(InvenTreeAttachment):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user