kopia lustrzana https://github.com/OpenDroneMap/WebODM
TestWatch class to monitor and intercept on demand multithreaded calls, changed location of assets during testing
rodzic
26339e0c32
commit
209a1b5603
|
@ -0,0 +1 @@
|
||||||
|
media_test/
|
|
@ -1,24 +1,11 @@
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
|
import logging
|
||||||
from django import db
|
from django import db
|
||||||
from webodm import settings
|
from webodm import settings
|
||||||
|
from app.testwatch import testWatch
|
||||||
|
|
||||||
|
logger = logging.getLogger('app.logger')
|
||||||
# TODO: design class such that:
|
|
||||||
# 1. test cases can choose which functions to intercept (prevent from executing)
|
|
||||||
# 2. test cases can see how many times a function has been called (and with which parameters)
|
|
||||||
# 3. test cases can pause until a function has been called
|
|
||||||
class TestWatch:
|
|
||||||
stats = {}
|
|
||||||
|
|
||||||
def called(self, func, *args, **kwargs):
|
|
||||||
list = TestWatch.stats[func] if func in TestWatch.stats else []
|
|
||||||
list.append({'f': func, 'args': args, 'kwargs': kwargs})
|
|
||||||
print(list)
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
TestWatch.stats = {}
|
|
||||||
|
|
||||||
testWatch = TestWatch()
|
|
||||||
|
|
||||||
def background(func):
|
def background(func):
|
||||||
"""
|
"""
|
||||||
|
@ -30,9 +17,7 @@ def background(func):
|
||||||
if 'background' in kwargs: del kwargs['background']
|
if 'background' in kwargs: del kwargs['background']
|
||||||
|
|
||||||
if background:
|
if background:
|
||||||
if settings.TESTING:
|
if testWatch.hook_pre(func, *args, **kwargs): return
|
||||||
# During testing, intercept all background requests and execute them on the same thread
|
|
||||||
testWatch.called(func.__name__, *args, **kwargs)
|
|
||||||
|
|
||||||
# Create a function that closes all
|
# Create a function that closes all
|
||||||
# db connections at the end of the thread
|
# db connections at the end of the thread
|
||||||
|
@ -44,6 +29,7 @@ def background(func):
|
||||||
ret = func(*args, **kwargs)
|
ret = func(*args, **kwargs)
|
||||||
finally:
|
finally:
|
||||||
db.connections.close_all()
|
db.connections.close_all()
|
||||||
|
testWatch.hook_post(func, *args, **kwargs)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
t = Thread(target=execute_and_close_db)
|
t = Thread(target=execute_and_close_db)
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from django import db
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from app import scheduler
|
|
||||||
from app.models import Project, Task, ImageUpload
|
from app.models import Project, Task, ImageUpload
|
||||||
from app.tests.classes import BootTransactionTestCase
|
from app.tests.classes import BootTransactionTestCase
|
||||||
from nodeodm import status_codes
|
from nodeodm import status_codes
|
||||||
from nodeodm.models import ProcessingNode
|
from nodeodm.models import ProcessingNode
|
||||||
|
from app.testwatch import testWatch
|
||||||
|
|
||||||
# We need to test the task API in a TransactionTestCase because
|
# We need to test the task API in a TransactionTestCase because
|
||||||
# task processing happens on a separate thread, and normal TestCases
|
# task processing happens on a separate thread, and normal TestCases
|
||||||
|
@ -133,6 +131,7 @@ class TestApiTask(BootTransactionTestCase):
|
||||||
# Neither should an individual tile
|
# Neither should an individual tile
|
||||||
# Z/X/Y coords are choosen based on node-odm test dataset for orthophoto_tiles/
|
# Z/X/Y coords are choosen based on node-odm test dataset for orthophoto_tiles/
|
||||||
res = client.get("/api/projects/{}/tasks/{}/tiles/16/16020/42443.png".format(project.id, task.id))
|
res = client.get("/api/projects/{}/tasks/{}/tiles/16/16020/42443.png".format(project.id, task.id))
|
||||||
|
print(res.status_code)
|
||||||
self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND)
|
self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
# Cannot access a tiles.json we have no access to
|
# Cannot access a tiles.json we have no access to
|
||||||
|
@ -160,6 +159,8 @@ class TestApiTask(BootTransactionTestCase):
|
||||||
})
|
})
|
||||||
self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND)
|
self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
testWatch.clear()
|
||||||
|
|
||||||
# Assign processing node to task via API
|
# Assign processing node to task via API
|
||||||
res = client.patch("/api/projects/{}/tasks/{}/".format(project.id, task.id), {
|
res = client.patch("/api/projects/{}/tasks/{}/".format(project.id, task.id), {
|
||||||
'processing_node': pnode.id
|
'processing_node': pnode.id
|
||||||
|
@ -167,28 +168,12 @@ class TestApiTask(BootTransactionTestCase):
|
||||||
self.assertTrue(res.status_code == status.HTTP_200_OK)
|
self.assertTrue(res.status_code == status.HTTP_200_OK)
|
||||||
|
|
||||||
# On update scheduler.processing_pending_tasks should have been called in the background
|
# On update scheduler.processing_pending_tasks should have been called in the background
|
||||||
time.sleep(DELAY)
|
testWatch.wait_until_call("app.scheduler.process_pending_tasks", timeout=5)
|
||||||
|
|
||||||
print("HERE")
|
|
||||||
from app.background import testWatch
|
|
||||||
print(testWatch.stats)
|
|
||||||
|
|
||||||
# Processing should have completed
|
# Processing should have completed
|
||||||
task.refresh_from_db()
|
task.refresh_from_db()
|
||||||
self.assertTrue(task.status == status_codes.RUNNING)
|
self.assertTrue(task.status == status_codes.RUNNING)
|
||||||
|
|
||||||
|
|
||||||
# TODO: need a way to prevent multithreaded code from executing
|
|
||||||
# and a way to notify our test case that multithreaded code should have
|
|
||||||
# executed
|
|
||||||
|
|
||||||
# TODO: at this point we might not even need a TransactionTestCase?
|
|
||||||
|
|
||||||
#from app import scheduler
|
|
||||||
#scheduler.process_pending_tasks(background=True)
|
|
||||||
|
|
||||||
# time.sleep(3)
|
|
||||||
|
|
||||||
# TODO: check
|
# TODO: check
|
||||||
# TODO: what happens when nodes go offline, or an offline node is assigned to a task
|
# TODO: what happens when nodes go offline, or an offline node is assigned to a task
|
||||||
# TODO: check raw/non-raw assets once task is finished processing
|
# TODO: check raw/non-raw assets once task is finished processing
|
||||||
|
@ -196,4 +181,3 @@ class TestApiTask(BootTransactionTestCase):
|
||||||
|
|
||||||
# Teardown processing node
|
# Teardown processing node
|
||||||
node_odm.terminate()
|
node_odm.terminate()
|
||||||
#time.sleep(20)
|
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from app.testwatch import TestWatch
|
||||||
|
|
||||||
|
|
||||||
|
def test(a, b):
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
class TestTestWatch(TestCase):
|
||||||
|
def test_methods(self):
|
||||||
|
tw = TestWatch()
|
||||||
|
|
||||||
|
self.assertTrue(tw.get_calls_count("app.tests.test_testwatch.test") == 0)
|
||||||
|
self.assertTrue(tw.get_calls_count("app.tests.test_testwatch.nonexistant") == 0)
|
||||||
|
|
||||||
|
# Test watch count
|
||||||
|
tw.hook_pre(test, 1, 2)
|
||||||
|
test(1, 2)
|
||||||
|
tw.hook_post(test, 1, 2)
|
||||||
|
|
||||||
|
self.assertTrue(tw.get_calls_count("app.tests.test_testwatch.test") == 1)
|
||||||
|
|
||||||
|
tw.hook_pre(test, 1, 2)
|
||||||
|
test(1, 2)
|
||||||
|
tw.hook_post(test, 1, 2)
|
||||||
|
|
||||||
|
self.assertTrue(tw.get_calls_count("app.tests.test_testwatch.test") == 2)
|
||||||
|
|
||||||
|
@TestWatch.watch(testWatch=tw)
|
||||||
|
def test2(d):
|
||||||
|
d['flag'] = not d['flag']
|
||||||
|
|
||||||
|
# Test intercept
|
||||||
|
tw.intercept("app.tests.test_testwatch.test2")
|
||||||
|
d = {'flag': True}
|
||||||
|
test2(d)
|
||||||
|
self.assertTrue(d['flag'])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import time
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from webodm import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger('app.logger')
|
||||||
|
|
||||||
|
class TestWatch:
|
||||||
|
def __init__(self):
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self._calls = {}
|
||||||
|
self._intercept_list = {}
|
||||||
|
|
||||||
|
def func_to_name(f):
|
||||||
|
return "{}.{}".format(f.__module__, f.__name__)
|
||||||
|
|
||||||
|
def intercept(self, fname):
|
||||||
|
self._intercept_list[fname] = True
|
||||||
|
|
||||||
|
def should_prevent_execution(self, func):
|
||||||
|
return TestWatch.func_to_name(func) in self._intercept_list
|
||||||
|
|
||||||
|
def get_calls(self, fname):
|
||||||
|
return self._calls[fname] if fname in self._calls else []
|
||||||
|
|
||||||
|
def get_calls_count(self, fname):
|
||||||
|
return len(self.get_calls(fname))
|
||||||
|
|
||||||
|
def wait_until_call(self, fname, count = 1, timeout = 30):
|
||||||
|
SLEEP_INTERVAL = 0.125
|
||||||
|
TIMEOUT_LIMIT = timeout / SLEEP_INTERVAL
|
||||||
|
c = 0
|
||||||
|
while self.get_calls_count(fname) < count and c < TIMEOUT_LIMIT:
|
||||||
|
time.sleep(SLEEP_INTERVAL)
|
||||||
|
c += 1
|
||||||
|
|
||||||
|
if c >= TIMEOUT_LIMIT:
|
||||||
|
raise TimeoutError("wait_until_call has timed out waiting for {}".format(fname))
|
||||||
|
|
||||||
|
return self.get_calls(fname)
|
||||||
|
|
||||||
|
def log_call(self, func, *args, **kwargs):
|
||||||
|
fname = TestWatch.func_to_name(func)
|
||||||
|
logger.info("{} called".format(fname))
|
||||||
|
list = self._calls[fname] if fname in self._calls else []
|
||||||
|
list.append({'f': fname, 'args': args, 'kwargs': kwargs})
|
||||||
|
self._calls[fname] = list
|
||||||
|
|
||||||
|
def hook_pre(self, func, *args, **kwargs):
|
||||||
|
if settings.TESTING and self.should_prevent_execution(func):
|
||||||
|
logger.info(func.__name__ + " intercepted")
|
||||||
|
self.log_call(func, *args, **kwargs)
|
||||||
|
return True # Intercept
|
||||||
|
return False # Do not intercept
|
||||||
|
|
||||||
|
def hook_post(self, func, *args, **kwargs):
|
||||||
|
if settings.TESTING:
|
||||||
|
self.log_call(func, *args, **kwargs)
|
||||||
|
|
||||||
|
def watch(**kwargs):
|
||||||
|
"""
|
||||||
|
Decorator that adds pre/post hook calls
|
||||||
|
"""
|
||||||
|
tw = kwargs.get('testWatch', testWatch)
|
||||||
|
def outer(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if tw.hook_pre(func, *args, **kwargs): return
|
||||||
|
ret = func(*args, **kwargs)
|
||||||
|
tw.hook_post(func, *args, **kwargs)
|
||||||
|
return ret
|
||||||
|
return wrapper
|
||||||
|
return outer
|
||||||
|
|
||||||
|
testWatch = TestWatch()
|
|
@ -224,6 +224,8 @@ REST_FRAMEWORK = {
|
||||||
}
|
}
|
||||||
|
|
||||||
TESTING = sys.argv[1:2] == ['test']
|
TESTING = sys.argv[1:2] == ['test']
|
||||||
|
if TESTING:
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'app', 'media_test')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .local_settings import *
|
from .local_settings import *
|
||||||
|
|
Ładowanie…
Reference in New Issue