Unit tests, minor fixes

pull/492/head
Piero Toffanin 2018-07-30 11:55:46 -04:00
rodzic e7142a6e8d
commit 68a4dd6dbb
18 zmienionych plików z 203 dodań i 27 usunięć

Wyświetl plik

@ -1,5 +1,5 @@
from abc import ABC
from django.core.exceptions import MultipleObjectsReturned
from django.core.exceptions import MultipleObjectsReturned, ValidationError
from app.models import PluginDatum
import logging
@ -29,6 +29,8 @@ class DataStore(ABC):
# This should never happen
logger.warning("A plugin data store for the {} plugin returned multiple objects. This is potentially bad. The plugin developer needs to fix this! The data store will not be changed.".format(self.namespace))
PluginDatum.objects.filter(key=self.db_key(key), user=self.user).delete()
except ValidationError as e:
raise InvalidDataStoreValue(e)
def get_value(self, type, key, default=None):
datum = self.get_datum(key)
@ -83,3 +85,7 @@ class UserDataStore(DataStore):
class GlobalDataStore(DataStore):
pass
class InvalidDataStoreValue(Exception):
pass

Wyświetl plik

@ -170,7 +170,10 @@ def get_dynamic_script_handler(script_path, callback=None, **kwargs):
with open(script_path) as f:
tmpl = Template(f.read())
return HttpResponse(tmpl.substitute(template_params))
try:
return HttpResponse(tmpl.substitute(template_params))
except TypeError as e:
return HttpResponse("Template substitution failed with params: {}. {}".format(str(template_params), e))
return handleRequest

Wyświetl plik

@ -32,7 +32,7 @@ class PluginBase(ABC):
"""
return UserDataStore(self.get_name(), user)
def get_global_data_store(self, user):
def get_global_data_store(self):
"""
Helper function to instantiate a user data store associated
with this plugin

Wyświetl plik

@ -1 +1 @@
from app.api.tasks import TaskNestedView as TaskView
from app.api.tasks import TaskNestedView as TaskView

Wyświetl plik

@ -14,6 +14,14 @@ export default class ApiFactory{
// We could just use events, but methods
// are more robust as we can detect more easily if
// things break
// TODO: we should consider refactoring this code
// to use functions instead of events. Originally
// we chose to use events because that would have
// decreased coupling, but since all API pubsub activity
// evolved to require a call to the PluginsAPI object, we might have
// added a bunch of complexity for no real advantage here.
const addEndpoint = (obj, eventName, preTrigger = () => {}) => {
const emitResponse = response => {
// Timeout needed for modules that have no dependencies

Wyświetl plik

@ -16,11 +16,12 @@ import worker
from django.utils import timezone
from app.models import Project, Task, ImageUpload
from app.models.task import task_directory_path, full_task_directory_path
from app.plugins.signals import task_completed, task_removed, task_removing
from app.tests.classes import BootTransactionTestCase
from nodeodm import status_codes
from nodeodm.models import ProcessingNode, OFFLINE_MINUTES
from app.testwatch import testWatch
from .utils import start_processing_node, clear_test_media_root
from .utils import start_processing_node, clear_test_media_root, catch_signal
# We need to test the task API in a TransactionTestCase because
# task processing happens on a separate thread, and normal TestCases
@ -279,9 +280,17 @@ class TestApiTask(BootTransactionTestCase):
time.sleep(DELAY)
# Calling process pending tasks should finish the process
worker.tasks.process_pending_tasks()
task.refresh_from_db()
self.assertTrue(task.status == status_codes.COMPLETED)
# and invoke the plugins completed signal
with catch_signal(task_completed) as handler:
worker.tasks.process_pending_tasks()
task.refresh_from_db()
self.assertTrue(task.status == status_codes.COMPLETED)
handler.assert_called_with(
sender=Task,
task_id=task.id,
signal=task_completed,
)
# Can download assets
for asset in list(task.ASSETS_MAP.keys()):
@ -362,9 +371,15 @@ class TestApiTask(BootTransactionTestCase):
task.refresh_from_db()
self.assertTrue(task.status == status_codes.CANCELED)
# Remove a task
res = client.post("/api/projects/{}/tasks/{}/remove/".format(project.id, task.id))
self.assertTrue(res.status_code == status.HTTP_200_OK)
# Remove a task and verify that it calls the proper plugins signals
with catch_signal(task_removing) as h1:
with catch_signal(task_removed) as h2:
res = client.post("/api/projects/{}/tasks/{}/remove/".format(project.id, task.id))
self.assertTrue(res.status_code == status.HTTP_200_OK)
h1.assert_called_once_with(sender=Task, task_id=task.id, signal=task_removing)
h2.assert_called_once_with(sender=Task, task_id=task.id, signal=task_removed)
# task is processed right away
# Has been removed along with assets

Wyświetl plik

@ -1,9 +1,14 @@
import os
from django.contrib.auth.models import User
from django.test import Client
from rest_framework import status
from app.models import Project
from app.models import Task
from app.plugins import UserDataStore
from app.plugins import get_plugin_by_name
from app.plugins.data_store import InvalidDataStoreValue
from .classes import BootTestCase
from app.plugins.grass_engine import grass, GrassEngineException
@ -28,6 +33,9 @@ class TestPlugins(BootTestCase):
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertTemplateUsed(res, 'plugins/test/templates/app.html')
# Form was rendered correctly
self.assertContains(res, '<input type="text" name="testField" class="form-control" required id="id_testField" />', count=1, status_code=200, html=True)
# It uses regex properly
res = client.get('/plugins/test/app_mountpoint/a')
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
@ -48,6 +56,37 @@ class TestPlugins(BootTestCase):
test_plugin = get_plugin_by_name("test")
self.assertTrue(os.path.exists(test_plugin.get_path("public/node_modules")))
# A webpack file and build directory have been created as a
# result of the build_jsx_components directive
self.assertTrue(os.path.exists(test_plugin.get_path("public/webpack.config.js")))
self.assertTrue(os.path.exists(test_plugin.get_path("public/build")))
self.assertTrue(os.path.exists(test_plugin.get_path("public/build/component.js")))
# Test task view
user = User.objects.get(username="testuser")
project = Project.objects.get(owner=user)
task = Task.objects.create(project=project, name="Test")
client.logout()
# Cannot see the task view without logging-in
res = client.get('/plugins/test/task/{}/'.format(task.id))
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
client.login(username='testuser', password='test1234')
res = client.get('/plugins/test/task/{}/'.format(task.id))
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertContains(res, str(task.id))
# Test dynamic script
res = client.get('/plugins/test/app_dynamic_script.js')
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertTrue(res.content.decode('utf-8') == '') # Empty
res = client.get('/plugins/test/app_dynamic_script.js?print=1')
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertTrue(res.content.decode('utf-8') == "console.log('Hello WebODM');") # Empty
def test_grass_engine(self):
cwd = os.path.dirname(os.path.realpath(__file__))
grass_scripts_dir = os.path.join(cwd, "grass_scripts")
@ -86,3 +125,63 @@ class TestPlugins(BootTestCase):
with self.assertRaises(GrassEngineException):
ctx.execute(os.path.join(grass_scripts_dir, "nonexistant_script.grass"))
def test_plugin_datastore(self):
test_plugin = get_plugin_by_name("test")
user = User.objects.get(username='testuser')
other_user = User.objects.get(username='testuser2')
uds = test_plugin.get_user_data_store(user)
other_uds = test_plugin.get_user_data_store(other_user)
gds = test_plugin.get_global_data_store()
# No key
self.assertFalse(uds.has_key('mykey'))
# Default value works
self.assertTrue(uds.get_string('mykey', 'default') == 'default')
# Still no key should have been added
self.assertFalse(uds.has_key('mykey'))
# Add key
(object, created) = uds.set_string('mykey', 'value')
self.assertTrue(object.string_value == 'value')
self.assertTrue(created)
self.assertTrue(uds.has_key('mykey'))
# Key is not visible in global datastore
self.assertFalse(gds.has_key('mykey'))
# Key is not visible in another user's data store
self.assertFalse(other_uds.has_key('mykey'))
# Key is not visible in another's plugin data store
# for the same user
other_puds = UserDataStore('test2', user)
self.assertFalse(other_puds.has_key('mykey'))
# Deleting a non-existing key return False
self.assertFalse(uds.del_key('nonexistant'))
# Deleting a valid key returns True
self.assertTrue(uds.del_key('mykey'))
self.assertFalse(uds.has_key('mykey'))
# Various data types setter/getter work
uds.set_int('myint', 5)
self.assertTrue(uds.get_int('myint') == 5)
uds.set_float('myfloat', 10.0)
self.assertTrue(uds.get_float('myfloat', 50.0) == 10.0)
uds.set_bool('mybool', True)
self.assertTrue(uds.get_bool('mybool'))
uds.set_json('myjson', {'test': 123})
self.assertTrue('test' in uds.get_json('myjson'))
# Invalid types
self.assertRaises(InvalidDataStoreValue, uds.set_bool, 'invalidbool', 5)

Wyświetl plik

@ -6,6 +6,11 @@ import subprocess
import logging
from unittest import mock
from contextlib import contextmanager
import random
from webodm import settings
logger = logging.getLogger('app.logger')
@ -26,4 +31,13 @@ def clear_test_media_root():
logger.info("Cleaning up {}".format(settings.MEDIA_ROOT))
shutil.rmtree(settings.MEDIA_ROOT)
else:
logger.warning("We did not remove MEDIA_ROOT because we couldn't find a _test suffix in its path.")
logger.warning("We did not remove MEDIA_ROOT because we couldn't find a _test suffix in its path.")
@contextmanager
def catch_signal(signal):
"""Catch django signal and return the mocked call."""
handler = mock.Mock()
signal.connect(handler, dispatch_uid=str(random.random()))
yield handler
signal.disconnect(handler)

Wyświetl plik

@ -1,6 +1,5 @@
PluginsAPI.Dashboard.addTaskActionButton([
'openaerialmap/build/ShareButton.js',
'openaerialmap/build/ShareButton.css'
'openaerialmap/build/ShareButton.js'
],function(args, ShareButton){
var task = args.task;

Wyświetl plik

@ -40,7 +40,7 @@ class Plugin(PluginBase):
return [
MountPoint('$', self.home_view()),
MountPoint('main.js', self.get_dynamic_script(
MountPoint('main.js$', self.get_dynamic_script(
'load_buttons.js',
load_buttons_cb
)

Wyświetl plik

@ -1,4 +1,3 @@
import './ShareButton.scss';
import React from 'react';
import PropTypes from 'prop-types';
import ShareDialog from './ShareDialog';

Wyświetl plik

@ -1,3 +0,0 @@
.oam-share-button{
}

Wyświetl plik

@ -26,10 +26,4 @@
{% include "app/plugins/templates/form.html" %}
<button type="submit" class="btn btn-primary"><i class="fa fa-save fa-fw"></i> Set Token</i></button>
</form>
<!--{% if form.token.value %}
<div class="oam-form">
<h4>Advanced Settings</h4>
</div>
{% endif %}-->
{% endblock %}

Wyświetl plik

@ -0,0 +1 @@
console.log('Hello ${name}');

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "POSM GCP Interface",
"webodmMinVersion": "0.5.0",
"webodmMinVersion": "0.6.0",
"description": "A plugin to create GCP files from images",
"version": "0.1.0",
"author": "Piero Toffanin",

Wyświetl plik

@ -1,5 +1,20 @@
from rest_framework import status
from rest_framework.response import Response
from app.plugins import PluginBase, Menu, MountPoint
from app.plugins.views import TaskView
from django.shortcuts import render
from django import forms
class TestForm(forms.Form):
testField = forms.CharField(label='Test')
class TestTaskView(TaskView):
def get(self, request, pk=None):
task = self.get_and_check_task(request, pk)
return Response(task.id, status=status.HTTP_200_OK)
class Plugin(PluginBase):
@ -12,9 +27,24 @@ class Plugin(PluginBase):
def include_css_files(self):
return ['test.css']
def build_jsx_components(self):
return ['component.jsx']
def app_mount_points(self):
# Show script only if '?print=1' is set
def dynamic_cb(request):
if 'print' in request.GET:
return {'name': 'WebODM'} # Test template substitution
else:
return False
return [
MountPoint('/app_mountpoint/$', lambda request: render(request, self.template_path("app.html"), {'title': 'Test'}))
MountPoint('/app_mountpoint/$', lambda request: render(request, self.template_path("app.html"), {
'title': 'Test',
'test_form': TestForm()
})),
MountPoint('task/(?P<pk>[^/.]+)/', TestTaskView.as_view()),
MountPoint('/app_dynamic_script.js$', self.get_dynamic_script('dynamic.js', dynamic_cb))
]

Wyświetl plik

@ -0,0 +1,5 @@
import React from 'react'; // can import React
module.exports = {
es6func: () => {} // ES6 transpiler works
};

Wyświetl plik

@ -2,4 +2,10 @@
{% block content %}
Hello world!
<form>
{% csrf_token %}
{% include "app/plugins/templates/form.html" with form=test_form %}
<button type="submit">Submit</button>
</form>
{% endblock %}