xaal.lib 0.7.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.1
2
+ Name: xaal.lib
3
+ Version: 0.7.2
4
+ Summary: xaal.lib is the official Python stack of xAAL protocol dedicated to home automation systems
5
+ Author-email: Jerome Kerdreux <Jerome.Kerdreux@imt-atlantique.fr>
6
+ License: GPL License
7
+ Keywords: xaal,home-automation
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
10
+ Description-Content-Type: text/x-rst
11
+ Requires-Dist: cbor2==5.4.2
12
+ Requires-Dist: pysodium
13
+ Requires-Dist: configobj
14
+ Requires-Dist: coloredlogs
15
+ Requires-Dist: decorator
16
+ Requires-Dist: tabulate
17
+ Requires-Dist: aioconsole
18
+
19
+
20
+ xaal.lib
21
+ ========
22
+ **xaal.lib** is the official Python stack to develop home-automation devices and gateways
23
+ with the xAAL protocol. For a full description of the protocol check out
24
+ http://recherche.imt-atlantique.fr/xaal/
25
+
26
+
27
+ Dependencies
28
+ ~~~~~~~~~~~~
29
+ xaal.lib depends on :
30
+ * cbor2
31
+ * pysodium
32
+ * configobj
33
+ * coloredlogs
34
+ * decorator
35
+ * tabulate
36
+ * aioconsole
37
+
38
+
39
+ Install
40
+ ~~~~~~~
41
+ Please refer to the official `full documentation to install the lib in a virtualenv
42
+ <https://redmine.telecom-bretagne.eu/svn/xaal/code/Python/branches/0.7/README.html>`_
43
+
44
+
45
+ Usage
46
+ ~~~~~
47
+ The main goal of xaal.lib is to provide an API to easily develop devices & gateways.
48
+ **xaal.lib.Engine send / receive / parse to|from xAAL Bus**.
49
+
50
+
51
+ To receive / parse / display incoming xAAL messages, you can simply try something like
52
+ this:
53
+
54
+ .. code-block:: python
55
+
56
+ from xaal.lib import Engine
57
+
58
+ def display(msg):
59
+ print(msg)
60
+
61
+ eng = Engine()
62
+ eng.subscribe(display)
63
+ eng.run()
64
+
65
+ The Engine will call the display function every time it receive a xAAL message.
66
+
67
+ Let's take a look at a simple lamp device :
68
+
69
+ .. code-block:: python
70
+
71
+ from xaal.lib import Device,Engine,tools
72
+
73
+ # create and configure the lamp device, with a random address
74
+ dev = Device("lamp.basic", tools.get_random_uuid())
75
+ dev.product_id = 'Dummy Lamp'
76
+ dev.url = 'http://www.acme.org'
77
+ dev.info = 'My fake lamp'
78
+
79
+ # add an xAAL attribute 'light'
80
+ light = dev.new_attribute('light')
81
+
82
+ # declare two device methods ON & OFF
83
+ def on():
84
+ light.value = True
85
+
86
+ def off():
87
+ light.value = False
88
+
89
+ dev.add_method('turn_on',on)
90
+ dev.add_method('turn_off',off)
91
+
92
+ # last step, create an engine and register the lamp
93
+ eng = Engine()
94
+ eng.add_device(dev)
95
+ eng.run()
96
+
97
+
98
+ FAQ
99
+ ~~~
100
+ The core engine run forever so how can I use it in webserver, GUI or to develop device
101
+ with IO. The whole API is absolutely not thread safe, so **don't use threads** unless you
102
+ exactly know what's going on. Anyways, you have several options to fix this issue:
103
+
104
+ * You can use you own loop and periodically call *eng.loop()*
105
+ for example, you can do something like this:
106
+
107
+ .. code:: python
108
+
109
+ while 1:
110
+ do_some_stuff()
111
+ eng.loop()
112
+
113
+ * You can use a engine timer, to perform some stuff.
114
+
115
+ .. code:: python
116
+
117
+ def read_io():
118
+ pass
119
+
120
+ # call the read_io function every 10 sec
121
+ eng.add_timer(read_io,10)
122
+ eng.run()
123
+
124
+ * Use the **AsyncEngine**. Python version > 3.8 provides async programming with **asyncio** package.
125
+ *AsyncEngine* use the same API as *Engine*, but it is a **asynchronous** engine. You can use
126
+ *coroutines* in device methods, timers functions and callbacks. It provides additionals features
127
+ like the *on_start* and *on_stop* callbacks.
128
+
129
+ * Use an alternate coroutine lib, you can use **gevent** or **greenlet** for example. Look at
130
+ apps/rest for a simple greenlet example.
131
+
@@ -0,0 +1,113 @@
1
+
2
+ xaal.lib
3
+ ========
4
+ **xaal.lib** is the official Python stack to develop home-automation devices and gateways
5
+ with the xAAL protocol. For a full description of the protocol check out
6
+ http://recherche.imt-atlantique.fr/xaal/
7
+
8
+
9
+ Dependencies
10
+ ~~~~~~~~~~~~
11
+ xaal.lib depends on :
12
+ * cbor2
13
+ * pysodium
14
+ * configobj
15
+ * coloredlogs
16
+ * decorator
17
+ * tabulate
18
+ * aioconsole
19
+
20
+
21
+ Install
22
+ ~~~~~~~
23
+ Please refer to the official `full documentation to install the lib in a virtualenv
24
+ <https://redmine.telecom-bretagne.eu/svn/xaal/code/Python/branches/0.7/README.html>`_
25
+
26
+
27
+ Usage
28
+ ~~~~~
29
+ The main goal of xaal.lib is to provide an API to easily develop devices & gateways.
30
+ **xaal.lib.Engine send / receive / parse to|from xAAL Bus**.
31
+
32
+
33
+ To receive / parse / display incoming xAAL messages, you can simply try something like
34
+ this:
35
+
36
+ .. code-block:: python
37
+
38
+ from xaal.lib import Engine
39
+
40
+ def display(msg):
41
+ print(msg)
42
+
43
+ eng = Engine()
44
+ eng.subscribe(display)
45
+ eng.run()
46
+
47
+ The Engine will call the display function every time it receive a xAAL message.
48
+
49
+ Let's take a look at a simple lamp device :
50
+
51
+ .. code-block:: python
52
+
53
+ from xaal.lib import Device,Engine,tools
54
+
55
+ # create and configure the lamp device, with a random address
56
+ dev = Device("lamp.basic", tools.get_random_uuid())
57
+ dev.product_id = 'Dummy Lamp'
58
+ dev.url = 'http://www.acme.org'
59
+ dev.info = 'My fake lamp'
60
+
61
+ # add an xAAL attribute 'light'
62
+ light = dev.new_attribute('light')
63
+
64
+ # declare two device methods ON & OFF
65
+ def on():
66
+ light.value = True
67
+
68
+ def off():
69
+ light.value = False
70
+
71
+ dev.add_method('turn_on',on)
72
+ dev.add_method('turn_off',off)
73
+
74
+ # last step, create an engine and register the lamp
75
+ eng = Engine()
76
+ eng.add_device(dev)
77
+ eng.run()
78
+
79
+
80
+ FAQ
81
+ ~~~
82
+ The core engine run forever so how can I use it in webserver, GUI or to develop device
83
+ with IO. The whole API is absolutely not thread safe, so **don't use threads** unless you
84
+ exactly know what's going on. Anyways, you have several options to fix this issue:
85
+
86
+ * You can use you own loop and periodically call *eng.loop()*
87
+ for example, you can do something like this:
88
+
89
+ .. code:: python
90
+
91
+ while 1:
92
+ do_some_stuff()
93
+ eng.loop()
94
+
95
+ * You can use a engine timer, to perform some stuff.
96
+
97
+ .. code:: python
98
+
99
+ def read_io():
100
+ pass
101
+
102
+ # call the read_io function every 10 sec
103
+ eng.add_timer(read_io,10)
104
+ eng.run()
105
+
106
+ * Use the **AsyncEngine**. Python version > 3.8 provides async programming with **asyncio** package.
107
+ *AsyncEngine* use the same API as *Engine*, but it is a **asynchronous** engine. You can use
108
+ *coroutines* in device methods, timers functions and callbacks. It provides additionals features
109
+ like the *on_start* and *on_stop* callbacks.
110
+
111
+ * Use an alternate coroutine lib, you can use **gevent** or **greenlet** for example. Look at
112
+ apps/rest for a simple greenlet example.
113
+
@@ -0,0 +1,11 @@
1
+ [project]
2
+ name = "xaal.lib"
3
+ version = "0.7.2"
4
+ description = "xaal.lib is the official Python stack of xAAL protocol dedicated to home automation systems"
5
+ readme = "README.rst"
6
+ authors = [ { name = "Jerome Kerdreux", email = "Jerome.Kerdreux@imt-atlantique.fr" } ]
7
+ license = { text = "GPL License"}
8
+ classifiers = ['Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules']
9
+ keywords = ['xaal', 'home-automation']
10
+ dependencies = ['cbor2==5.4.2', 'pysodium', 'configobj', 'coloredlogs', 'decorator', 'tabulate', 'aioconsole']
11
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,56 @@
1
+ import unittest
2
+ from xaal.lib import bindings
3
+
4
+
5
+ class TestUUID(unittest.TestCase):
6
+
7
+
8
+ def test_add_sub(self):
9
+ uuid = bindings.UUID("12345678-1234-1234-1234-123456789012")
10
+ uuid += 1
11
+ self.assertEqual(uuid,bindings.UUID("12345678-1234-1234-1234-123456789013"))
12
+ uuid -= 1
13
+ self.assertEqual(uuid,bindings.UUID("12345678-1234-1234-1234-123456789012"))
14
+
15
+ def test_random(self):
16
+ uuid = bindings.UUID.random()
17
+ self.assertIsInstance(uuid,bindings.UUID)
18
+
19
+ uuid = bindings.UUID.random_base()
20
+ self.assertIsInstance(uuid,bindings.UUID)
21
+
22
+ with self.assertRaises(bindings.UUIDError):
23
+ uuid = bindings.UUID.random_base(0)
24
+
25
+ def test_get_set(self):
26
+ uuid1 = bindings.UUID("12345678-1234-1234-1234-123456789012")
27
+ data = uuid1.get()
28
+ uuid2 = bindings.UUID.random()
29
+ uuid2.set(data)
30
+ self.assertEqual(uuid1,uuid2)
31
+
32
+ def test_str(self):
33
+ uuid = bindings.UUID("12345678-1234-1234-1234-123456789012")
34
+ self.assertEqual(str(uuid),"12345678-1234-1234-1234-123456789012")
35
+ self.assertEqual(uuid.str,"12345678-1234-1234-1234-123456789012")
36
+
37
+ def test_bytes(self):
38
+ uuid = bindings.UUID("12345678-1234-1234-1234-123456789012")
39
+ self.assertEqual(uuid.bytes,b'\x124Vx\x124\x124\x124\x124Vx\x90\x12')
40
+
41
+
42
+ class TestURL(unittest.TestCase):
43
+
44
+ def test_url(self):
45
+ url = bindings.URL("http://bar.com")
46
+ self.assertEqual(str(url), "http://bar.com")
47
+ url.set("http://foo.com")
48
+ print(url)
49
+ self.assertEqual(str(url), "http://foo.com")
50
+ self.assertEqual(url.str, "http://foo.com")
51
+ self.assertEqual(url.get(), "http://foo.com")
52
+ self.assertEqual(url.bytes, "http://foo.com")
53
+
54
+
55
+ if __name__ == '__main__':
56
+ unittest.main()
@@ -0,0 +1,62 @@
1
+ import unittest
2
+ from xaal.lib import cbor
3
+ from xaal.lib import bindings
4
+ import binascii
5
+
6
+
7
+ class TestCbor(unittest.TestCase):
8
+ def test_encode(self):
9
+ value = cbor.dumps(42)
10
+ self.assertEqual(value, b"\x18*") # 0x18 0x2a
11
+
12
+ def test_decode(self):
13
+ value = cbor.loads(b"\x18*")
14
+ self.assertEqual(value, 42)
15
+
16
+ def test_list(self):
17
+ value = []
18
+ value.append(7)
19
+ value.append(1234567890)
20
+ data = cbor.dumps(value)
21
+ print(data)
22
+ self.assertEqual(data, binascii.unhexlify(b"82071a499602d2"))
23
+
24
+ def test_uuid(self):
25
+ uuid = bindings.UUID.random()
26
+ cbor_value = cbor.dumps(uuid)
27
+ value = cbor.loads(cbor_value)
28
+ self.assertEqual(value, uuid)
29
+ self.assertEqual(type(value), bindings.UUID)
30
+
31
+ def test_url(self):
32
+ url = bindings.URL("http://www.example.com")
33
+ cbor_value = cbor.dumps(url)
34
+ value = cbor.loads(cbor_value)
35
+ self.assertEqual(value, url)
36
+ self.assertEqual(type(value), bindings.URL)
37
+
38
+ def test_cleanup(self):
39
+ data = [
40
+ 7,
41
+ "hello",
42
+ bindings.UUID("12345678-1234-1234-1234-123456789012"),
43
+ bindings.URL("http://www.example.com"),
44
+ {"hello": "world"},
45
+ ]
46
+ cbor_value = cbor.dumps(data)
47
+ value = cbor.loads(cbor_value)
48
+ cbor.cleanup(value)
49
+ self.assertEqual(
50
+ value,
51
+ [
52
+ 7,
53
+ "hello",
54
+ "12345678-1234-1234-1234-123456789012",
55
+ "http://www.example.com",
56
+ {"hello": "world"},
57
+ ],
58
+ )
59
+
60
+
61
+ if __name__ == "__main__":
62
+ unittest.main()
@@ -0,0 +1,195 @@
1
+ import unittest
2
+ from xaal.lib import tools
3
+ from xaal.lib import Device,DeviceError,Attribute,Engine
4
+ from xaal.lib.devices import Attributes
5
+
6
+
7
+ def new_dev() -> Device:
8
+ addr = tools.get_random_uuid()
9
+ dev = Device("light.basic",addr=addr)
10
+ return dev
11
+
12
+ class TestAttribute(unittest.TestCase):
13
+
14
+ def test_default(self):
15
+ attr = Attribute("foo",dev=new_dev(),default=1)
16
+ self.assertEqual(attr.value,1)
17
+ attr.value = 12
18
+ self.assertEqual(attr.value,12)
19
+
20
+ def test_with_engine(self):
21
+ dev = new_dev()
22
+ dev.engine = Engine()
23
+ attr = Attribute("foo",dev=dev,default=1,)
24
+ attr.value = 12
25
+ self.assertEqual(attr.value,12)
26
+
27
+ class TestAttributes(unittest.TestCase):
28
+
29
+ def test_default(self):
30
+ dev = new_dev()
31
+ attr = Attribute("foo",dev=dev,default=1)
32
+ attrs = Attributes()
33
+ attrs.append(attr)
34
+ self.assertEqual(attrs[0].value,1)
35
+ self.assertEqual(attrs["foo"],1)
36
+
37
+ with self.assertRaises(KeyError):
38
+ attrs["bar"]
39
+
40
+ attrs[0] = Attribute("bar",dev=dev,default=2)
41
+ self.assertEqual(attrs[0].value,2)
42
+
43
+ attrs["bar"] = 12
44
+ self.assertEqual(attrs["bar"],12)
45
+
46
+ with self.assertRaises(KeyError):
47
+ attrs["nop"]=12
48
+
49
+
50
+ class TestDevice(unittest.TestCase):
51
+
52
+ def test_init(self):
53
+ dev_type="light.basic"
54
+ addr = tools.get_random_uuid()
55
+ dev = Device(dev_type=dev_type,addr=addr)
56
+ self.assertEqual(dev.dev_type, dev_type)
57
+ self.assertEqual(dev.address, addr)
58
+
59
+ def test_address(self):
60
+ dev = new_dev()
61
+ # address
62
+ addr = tools.get_random_uuid()
63
+ dev.address = addr
64
+ self.assertEqual(dev.address, addr)
65
+ # none address
66
+ dev.address = None
67
+ self.assertEqual(dev.address, None)
68
+ # invalid address
69
+ with self.assertRaises(DeviceError):
70
+ dev.address = "foo"
71
+
72
+ def test_dev_type(self):
73
+ dev = new_dev()
74
+ dev.dev_type = "foo.basic"
75
+ self.assertEqual(dev.dev_type, "foo.basic")
76
+ with self.assertRaises(DeviceError):
77
+ dev.dev_type = "foo"
78
+
79
+ def test_version(self):
80
+ dev = new_dev()
81
+ # version
82
+ dev.version = 12
83
+ self.assertEqual(dev.version, '12')
84
+ dev.version = None
85
+ self.assertEqual(dev.version, None)
86
+
87
+ def test_url(self):
88
+ dev = new_dev()
89
+ dev.url = "http://foo.bar"
90
+ self.assertEqual(dev.url, "http://foo.bar")
91
+ dev.url = None
92
+ self.assertEqual(dev.url, None)
93
+
94
+ def test_attributes(self):
95
+ dev = new_dev()
96
+ attr = Attribute("foo",dev=dev,default=1)
97
+ # add
98
+ dev.add_attribute(attr)
99
+ self.assertEqual(len(dev.attributes),1)
100
+ self.assertEqual(dev.attributes[0],attr)
101
+ # del
102
+ dev.del_attribute(attr)
103
+ self.assertEqual(len(dev.attributes),0)
104
+ # new
105
+ dev.new_attribute("foo",default=2)
106
+ self.assertEqual(len(dev.attributes),1)
107
+ self.assertEqual(dev.attributes[0].value,2)
108
+ # get
109
+ self.assertEqual(dev.get_attribute("foo").value,2)
110
+ self.assertEqual(dev.get_attribute("bar"),None)
111
+
112
+ # set
113
+ attr = Attribute("bar",dev=dev,default=3)
114
+ dev.attributes =Attributes([attr,])
115
+ self.assertEqual(len(dev.attributes),1)
116
+ self.assertEqual(dev.attributes[0].value,3)
117
+ # only accepts Attributes not list
118
+ with self.assertRaises(DeviceError):
119
+ dev.attributes = [attr,]
120
+
121
+ def test_get_attributes(self):
122
+ dev = new_dev()
123
+ dev.new_attribute("foo",default=1)
124
+ # _get_attributes
125
+ self.assertEqual(dev._get_attributes()["foo"],1)
126
+ data = dev._get_attributes(["foo","bar"])
127
+ self.assertEqual(len(data),1)
128
+ self.assertEqual(data["foo"],1)
129
+
130
+ def test_get_description(self):
131
+ dev = new_dev()
132
+
133
+ dev.vendor_id = 0x1234
134
+ dev.product_id = 0x1234
135
+ dev.version = '2f'
136
+ dev.url = "http://foo.bar"
137
+ dev.schema = "http://schemas.foo.bar/schema.json"
138
+ dev.info = "FooBar"
139
+ dev.hw_id = 0xf12
140
+ group = tools.get_random_uuid()
141
+ dev.group_id = group
142
+
143
+ dev.unsupported_methods = ["foo_func"]
144
+ dev.unsupported_attributes = ["foo_attr"]
145
+ dev.unsupported_notifications = ["foo_notif"]
146
+
147
+ data = dev._get_description()
148
+ self.assertEqual(data["vendor_id"], 0x1234)
149
+ self.assertEqual(data["product_id"], 0x1234)
150
+ self.assertEqual(data["version"], '2f')
151
+ self.assertEqual(data["url"], "http://foo.bar")
152
+ self.assertEqual(data["schema"], "http://schemas.foo.bar/schema.json")
153
+ self.assertEqual(data["info"], "FooBar")
154
+ self.assertEqual(data["hw_id"],0xf12)
155
+ self.assertEqual(data["group_id"],group)
156
+
157
+ self.assertEqual(data["unsupported_methods"],["foo_func"])
158
+ self.assertEqual(data["unsupported_notifications"],["foo_notif"])
159
+ self.assertEqual(data["unsupported_attributes"],["foo_attr"])
160
+
161
+ def test_methods(self):
162
+ dev = new_dev()
163
+ # device has two methods by default
164
+ self.assertEqual(len(dev.methods),2)
165
+ self.assertEqual(len(dev.get_methods()),2)
166
+ def func():pass
167
+ dev.add_method("foo",func)
168
+ self.assertEqual(dev.methods["foo"],func)
169
+
170
+ def test_dump(self):
171
+ dev = new_dev()
172
+ dev.info = 'FooBar'
173
+ dev.new_attribute("foo",default=1)
174
+ dev.dump()
175
+
176
+ def test_alive(self):
177
+ import time
178
+ now = time.time()
179
+ dev = new_dev()
180
+ dev.alive_period = 2
181
+ dev.update_alive()
182
+ self.assertTrue(dev.next_alive > now)
183
+ self.assertTrue(dev.next_alive < now+3)
184
+ self.assertEqual(dev.get_timeout(),4)
185
+
186
+ def test_send_notification(self):
187
+ # not really a test here, just to check if the method is called
188
+ # check engine unittest for futher tests
189
+ dev = new_dev()
190
+ dev.engine = Engine()
191
+ dev.send_notification("foo")
192
+
193
+
194
+ if __name__ == '__main__':
195
+ unittest.main()
@@ -0,0 +1,97 @@
1
+ import time
2
+ import unittest
3
+ from xaal.lib import Engine, Device, MessageType, CallbackError
4
+ from xaal.lib import engine, tools
5
+ from xaal.lib.messages import Message
6
+
7
+ TEST_PORT = 6666
8
+
9
+
10
+ def new_engine():
11
+ engine = Engine(port=TEST_PORT)
12
+ return engine
13
+
14
+ class TestEngine(unittest.TestCase):
15
+
16
+ def test_devices(self):
17
+ dev = Device("test.basic", tools.get_random_uuid())
18
+ eng = Engine(port=TEST_PORT)
19
+ eng.add_device(dev)
20
+ self.assertEqual(eng.devices,[dev,])
21
+ eng.remove_device(dev)
22
+ self.assertEqual(eng.devices,[])
23
+ eng.add_devices([dev,])
24
+ self.assertEqual(eng.devices,[dev,])
25
+
26
+ def test_start_stop(self):
27
+ eng = Engine(port=TEST_PORT)
28
+ dev = Device("test.basic", tools.get_random_uuid())
29
+ eng.add_device(dev)
30
+ eng.start()
31
+ eng.start() # second start
32
+ self.assertEqual(engine.EngineState.started, eng.state)
33
+ eng.loop()
34
+ eng.stop()
35
+ self.assertEqual(engine.EngineState.halted, eng.state)
36
+
37
+ def test_timer(self):
38
+ eng = Engine(port=TEST_PORT)
39
+ t0 = time.time()
40
+ def _exit():
41
+ eng.shutdown()
42
+ eng.add_timer(_exit, 1, 1)
43
+ eng.run()
44
+ t = time.time() - t0
45
+ self.assertTrue(t > 1)
46
+ self.assertTrue(t < 2)
47
+
48
+ def test_timer_error(self):
49
+ eng = Engine(port=TEST_PORT)
50
+ eng.start()
51
+ def _error():
52
+ raise CallbackError(500,"test error")
53
+ eng.add_timer(_error,0,1)
54
+ eng.loop()
55
+ eng.stop()
56
+ self.assertEqual(engine.EngineState.halted, eng.state)
57
+
58
+ def test_run_action(self):
59
+ target = Device("test.basic", tools.get_random_uuid())
60
+
61
+ def action_1():
62
+ return "action_1"
63
+
64
+ def action_2(_value=None):
65
+ return "action_%s" % _value
66
+
67
+ def action_3():
68
+ raise Exception
69
+
70
+ target.add_method("action_1", action_1)
71
+ target.add_method("action_2", action_2)
72
+ target.add_method("action_3", action_3)
73
+
74
+ msg = Message()
75
+ msg.msg_type = MessageType.REQUEST
76
+ msg.targets = [target.address]
77
+ # simple test method
78
+ msg.action = "action_1"
79
+ result = engine.run_action(msg, target)
80
+ self.assertEqual(result, "action_1")
81
+ # test with value
82
+ msg.action = "action_2"
83
+ msg.body = {"value": "2"}
84
+ result = engine.run_action(msg, target)
85
+ self.assertEqual(result, "action_2")
86
+ # Exception in method
87
+ msg.action = "action_3"
88
+ with self.assertRaises(engine.XAALError):
89
+ result = engine.run_action(msg, target)
90
+ # unknown method
91
+ msg.action = "missing"
92
+ with self.assertRaises(engine.XAALError):
93
+ result = engine.run_action(msg, target)
94
+
95
+
96
+ if __name__ == "__main__":
97
+ unittest.main()