diff --git a/.gitattributes b/.gitattributes index a3551495ed88e390322059de0db8a6f4db690a7f..5cacfbd2c454252bc13797a681b37512db351662 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2668,6 +2668,7 @@ LCS/PyCommon/postgres.py -text LCS/PyCommon/test/python-coverage.sh eol=lf LCS/PyCommon/test/t_dbcredentials.run eol=lf LCS/PyCommon/test/t_dbcredentials.sh eol=lf +LCS/PyCommon/test/t_methodtrigger.sh eol=lf LCS/PyCommon/util.py -text LCS/Tools/src/checkcomp.py -text LCS/Tools/src/countalllines -text diff --git a/LCS/PyCommon/CMakeLists.txt b/LCS/PyCommon/CMakeLists.txt index 42cf8512e8926b7335609fc647cfa424befb37e8..6fc29b3e2de25fb95aa74915ee03af9fbab874c4 100644 --- a/LCS/PyCommon/CMakeLists.txt +++ b/LCS/PyCommon/CMakeLists.txt @@ -10,6 +10,7 @@ add_subdirectory(test) set(_py_files dbcredentials.py factory.py + methodtrigger.py util.py postgres.py datetimeutils.py) diff --git a/LCS/PyCommon/methodtrigger.py b/LCS/PyCommon/methodtrigger.py new file mode 100644 index 0000000000000000000000000000000000000000..800d8d11ea19f00c3c7f1bb0cad9d6f2801c3faf --- /dev/null +++ b/LCS/PyCommon/methodtrigger.py @@ -0,0 +1,68 @@ +from threading import Lock, Condition + +__all__ = ["MethodTrigger"] + +class MethodTrigger: + """ + Set a flag when a specific method is called, possibly asynchronously. Caller can wait on this flag. + + Example: + + class Foo(object): + def bar(self): + pass + + foo = Foo() + trigger = MethodTrigger(foo, "bar") + + if trigger.wait(): # Waits for 10 seconds for foo.bar() to get called + print "foo.bar() got called" + else + # This will happen, as foo.bar() wasn't called + print "foo.bar() did not get called" + + Calls that were made before the trigger has been installed will not get recorded. + """ + + def __init__(self, obj, method): + assert isinstance(obj, object), "Object %s does not derive from object." % (obj,) + + self.obj = obj + self.method = method + self.old_func = obj.__getattribute__(method) + + self.called = False + self.args = [] + self.kwargs = {} + + self.lock = Lock() + self.cond = Condition(self.lock) + + # Patch the target method + obj.__setattr__(method, self.trigger) + + def trigger(self, *args, **kwargs): + # Save the call parameters + self.args = args + self.kwargs = kwargs + + # Call the original method + self.old_func(*args, **kwargs) + + # Restore the original method + self.obj.__setattr__(self.method, self.old_func) + + # Release waiting thread + with self.lock: + self.called = True + self.cond.notify() + + def wait(self, timeout=10.0): + # Wait for method to get called + with self.lock: + if self.called: + return True + + self.cond.wait(timeout) + + return self.called diff --git a/LCS/PyCommon/test/CMakeLists.txt b/LCS/PyCommon/test/CMakeLists.txt index a2abf73a98a57ed76555c5beb9d870804ff264b8..79c9b43bfa8f22c32c1e1d4278b3ff298192a581 100644 --- a/LCS/PyCommon/test/CMakeLists.txt +++ b/LCS/PyCommon/test/CMakeLists.txt @@ -7,3 +7,4 @@ file(COPY DESTINATION ${CMAKE_BINARY_DIR}/bin) lofar_add_test(t_dbcredentials) +lofar_add_test(t_methodtrigger) diff --git a/LCS/PyCommon/test/t_methodtrigger.py b/LCS/PyCommon/test/t_methodtrigger.py new file mode 100644 index 0000000000000000000000000000000000000000..5b96ebcf6de523055e99d1094a2ea395cee0d25f --- /dev/null +++ b/LCS/PyCommon/test/t_methodtrigger.py @@ -0,0 +1,124 @@ +import unittest +from lofar.common.methodtrigger import MethodTrigger + +from threading import Thread +import time + +class TestMethodTrigger(unittest.TestCase): + def setUp(self): + # Create a basic object + class TestClass(object): + def func(self): + pass + + self.testobj = TestClass() + + # Install trigger + self.trigger = MethodTrigger(self.testobj, "func") + + def test_no_call(self): + """ Do not trigger. """ + + # Wait for trigger + self.assertFalse(self.trigger.wait(0.1)) + + def test_serial_call(self): + """ Trigger and wait serially. """ + + # Call function + self.testobj.func() + + # Wait for trigger + self.assertTrue(self.trigger.wait(0.1)) + + def test_parallel_call(self): + """ Trigger and wait in parallel. """ + + class wait_thread(Thread): + def __init__(self, trigger): + Thread.__init__(self) + self.result = None + self.trigger = trigger + + def run(self): + self.result = self.trigger.wait(1.0) + + class call_thread(Thread): + def __init__(self,func): + Thread.__init__(self) + self.func = func + + def run(self): + time.sleep(0.5) + self.func() + + # Start threads + t1 = wait_thread(self.trigger) + t1.start() + t2 = call_thread(self.testobj.func) + t2.start() + + # Wait for them to finish + t1.join() + t2.join() + + # Inspect result + self.assertTrue(t1.result) + +class TestArgs(unittest.TestCase): + def setUp(self): + # Create a basic object + class TestClass(object): + def func(self, a, b, c=None, d=None): + pass + + self.testobj = TestClass() + + # Install trigger + self.trigger = MethodTrigger(self.testobj, "func") + + def test_args(self): + """ Trigger and check args. """ + + # Call function + self.testobj.func(1, 2) + + # Wait for trigger + self.assertTrue(self.trigger.wait(0.1)) + + # Check stored arguments + self.assertEqual(self.trigger.args, (1, 2)) + + def test_kwargs(self): + """ Trigger and check kwargs. """ + + # Call function + self.testobj.func(a=1, b=2) + + # Wait for trigger + self.assertTrue(self.trigger.wait(0.1)) + + # Check stored arguments + self.assertEqual(self.trigger.kwargs, {"a": 1, "b": 2}) + + def test_full(self): + """ Trigger and check both args and kwargs. """ + + # Call function + self.testobj.func(1, 2, c=3, d=4) + + # Wait for trigger + self.assertTrue(self.trigger.wait(0.1)) + + # Check stored arguments + self.assertEqual(self.trigger.args, (1, 2)) + self.assertEqual(self.trigger.kwargs, {"c": 3, "d": 4}) + +def main(argv): + unittest.main(verbosity=2) + +if __name__ == "__main__": + # run all tests + import sys + main(sys.argv[1:]) + diff --git a/LCS/PyCommon/test/t_methodtrigger.sh b/LCS/PyCommon/test/t_methodtrigger.sh new file mode 100755 index 0000000000000000000000000000000000000000..c786a2cbcdafae4571c5b6fba84fa21c96381ad6 --- /dev/null +++ b/LCS/PyCommon/test/t_methodtrigger.sh @@ -0,0 +1,2 @@ +#!/bin/sh +./runctest.sh t_methodtrigger