Merge pull request #3014 from matmair/matmair/issue3005

Add more tests for offload_task
This commit is contained in:
Oliver 2022-05-17 11:11:20 +10:00 committed by GitHub
commit 334025b844
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 144 additions and 41 deletions

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
import re
import json
import warnings
import requests
import logging
@ -11,6 +12,8 @@ from django.utils import timezone
from django.core.exceptions import AppRegistryNotReady
from django.db.utils import OperationalError, ProgrammingError
from django.core import mail as django_mail
from django.conf import settings
logger = logging.getLogger("inventree")
@ -52,6 +55,15 @@ def schedule_task(taskname, **kwargs):
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):
"""
Create an AsyncTask if workers are running.
@ -67,28 +79,38 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
import importlib
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
# Running as asynchronous task
try:
task = AsyncTask(taskname, *args, **kwargs)
task.run()
except ImportError:
logger.warning(f"WARNING: '{taskname}' not started - Function not found")
if is_worker_running() and not force_sync: # pragma: no cover
# Running as asynchronous task
try:
task = AsyncTask(taskname, *args, **kwargs)
task.run()
except ImportError:
raise_warning(f"WARNING: '{taskname}' not started - Function not found")
else:
if callable(taskname):
# function was passed - use that
_func = taskname
else:
# Split path
try:
app, mod, func = taskname.split('.')
app_mod = app + '.' + mod
except ValueError:
logger.warning(f"WARNING: '{taskname}' not started - Malformed function path")
raise_warning(f"WARNING: '{taskname}' not started - Malformed function path")
return
# Import module from app
try:
_mod = importlib.import_module(app_mod)
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
# Retrieve function
@ -102,17 +124,11 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
if not _func:
_func = eval(func) # pragma: no cover
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
# Workers are not running: run it as synchronous task
_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")
# Workers are not running: run it as synchronous task
_func(*args, **kwargs)
def heartbeat():
@ -205,25 +221,25 @@ def check_for_updates():
response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
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)
tag = data.get('tag_name', None)
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)
if len(match.groups()) != 3:
if len(match.groups()) != 3: # pragma: no cover
logger.warning(f"Version '{tag}' did not match expected pattern")
return
latest_version = [int(x) for x in match.groups()]
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}'")
@ -288,7 +304,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
recipients = [recipients]
offload_task(
'django.core.mail.send_mail',
django_mail.send_mail,
subject,
body,
from_email,

View File

@ -2,10 +2,20 @@
Unit tests for task management
"""
from datetime import timedelta
from django.utils import timezone
from django.test import TestCase
from django_q.models import Schedule
from error_report.models import Error
import InvenTree.tasks
from common.models import InvenTreeSetting
threshold = timezone.now() - timedelta(days=30)
threshold_low = threshold - timedelta(days=1)
class ScheduledTaskTests(TestCase):
@ -41,3 +51,79 @@ class ScheduledTaskTests(TestCase):
# But the 'minutes' should have been updated
t = Schedule.objects.get(func=task)
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))

View File

@ -795,13 +795,9 @@ class CurrencyRefreshView(RedirectView):
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
taskname = 'InvenTree.tasks.update_exchange_rates'
# Run it
offload_task(taskname, force_sync=True)
offload_task(update_exchange_rates, force_sync=True)
return redirect(reverse_lazy('settings'))

View File

@ -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
"""
from . import tasks as build_tasks
if created:
# A new Build has just been created
# 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):

View File

@ -2,6 +2,7 @@
from django.test import TestCase
from common.models import NotificationEntry
from . import tasks as common_tasks
from InvenTree.tasks import offload_task
@ -14,4 +15,4 @@ class TaskTest(TestCase):
# check empty run
self.assertEqual(NotificationEntry.objects.all().count(), 0)
offload_task('common.tasks.delete_old_notifications',)
offload_task(common_tasks.delete_old_notifications,)

View File

@ -24,6 +24,7 @@ from plugin.registry import registry
from stock.models import StockItem, StockLocation
from part.models import Part
from plugin.base.label import label as plugin_label
from .models import StockItemLabel, StockLocationLabel, PartLabel
from .serializers import StockItemLabelSerializer, StockLocationLabelSerializer, PartLabelSerializer
@ -156,7 +157,7 @@ class LabelPrintMixin:
# Offload a background task to print the provided label
offload_task(
'plugin.base.label.label.print_label',
plugin_label.print_label,
plugin.plugin_slug(),
image,
label_instance=label_instance,

View File

@ -59,7 +59,6 @@ from order import models as OrderModels
from company.models import SupplierPart
import part.settings as part_settings
from stock import models as StockModels
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
"""
from part import tasks as part_tasks
if not created and not InvenTree.ready.isImportingData():
# Check part stock only if we are *updating* the part (not creating it)
# 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):

View File

@ -49,6 +49,6 @@ def notify_low_stock_if_required(part: part.models.Part):
for p in parts:
if p.is_part_low_on_stock():
InvenTree.tasks.offload_task(
'part.tasks.notify_low_stock',
notify_low_stock,
p
)

View File

@ -37,7 +37,7 @@ def trigger_event(event, *args, **kwargs):
logger.debug(f"Event triggered: '{event}'")
offload_task(
'plugin.events.register_event',
register_event,
event,
*args,
**kwargs
@ -72,7 +72,7 @@ def register_event(event, *args, **kwargs):
# Offload a separate task for each plugin
offload_task(
'plugin.events.process_event',
process_event,
slug,
event,
*args,

View File

@ -56,7 +56,7 @@ class LocatePluginView(APIView):
try:
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
@ -69,7 +69,7 @@ class LocatePluginView(APIView):
try:
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

View File

@ -2020,10 +2020,11 @@ def after_delete_stock_item(sender, instance: StockItem, **kwargs):
"""
Function to be executed after a StockItem object is deleted
"""
from part import tasks as part_tasks
if not InvenTree.ready.isImportingData():
# 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')
@ -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
"""
from part import tasks as part_tasks
if not InvenTree.ready.isImportingData():
# 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):