1# (C) Copyright 2007-2019 Enthought, Inc., Austin, TX
2# All rights reserved.
3#
4# This software is provided without warranty under the terms of the BSD
5# license included in LICENSE.txt and may be redistributed only
6# under the conditions described in the aforementioned license.  The license
7# is also available online at http://www.enthought.com/licenses/BSD.txt
8# Thanks for using Enthought open source!
9""" Tests for plugins. """
10
11
12# Standard library imports.
13from os.path import exists, join
14
15# Enthought library imports.
16from envisage.api import Application, ExtensionPoint
17from envisage.api import IPluginActivator, Plugin, contributes_to
18from envisage.tests.ets_config_patcher import ETSConfigPatcher
19from traits.api import HasTraits, Instance, Int, Interface, List
20from traits.api import provides
21from traits.testing.unittest_tools import unittest
22
23
24def listener(obj, trait_name, old, new):
25    """ A useful trait change handler for testing! """
26
27    listener.obj = obj
28    listener.trait_name = trait_name
29    listener.old = old
30    listener.new = new
31
32
33class TestApplication(Application):
34    """ The type of application used in the tests. """
35
36    id = 'test'
37
38
39class PluginTestCase(unittest.TestCase):
40    """ Tests for plugins. """
41
42    def setUp(self):
43        ets_config_patcher = ETSConfigPatcher()
44        ets_config_patcher.start()
45        self.addCleanup(ets_config_patcher.stop)
46
47    def test_id_policy(self):
48        """ id policy """
49
50        # If no Id is specified then use 'module_name.class_name'.
51        p = Plugin()
52        self.assertEqual('envisage.plugin.Plugin', p.id)
53
54        # If an Id is specified make sure we use it!
55        p = Plugin(id='wilma')
56        self.assertEqual('wilma', p.id)
57
58        # Make sure setting the name doesn't interfere with the Id.
59        p = Plugin(name='fred', id='wilma')
60        self.assertEqual('wilma', p.id)
61        self.assertEqual('fred', p.name)
62
63    def test_name_policy(self):
64        """ name policy """
65
66        # If name is specified then use the plugin's class name.
67        p = Plugin()
68        self.assertEqual('Plugin', p.name)
69
70        # If a name is specified make sure we use it!
71        p = Plugin(name='wilma')
72        self.assertEqual('wilma', p.name)
73
74        # Try a camel case plugin class.
75        class ThisIsMyPlugin(Plugin):
76            pass
77
78        p = ThisIsMyPlugin()
79        self.assertEqual('This Is My Plugin', p.name)
80
81    def test_plugin_activator(self):
82        """ plugin activator. """
83
84        @provides(IPluginActivator)
85        class NullPluginActivator(HasTraits):
86            """ A plugin activator that does nothing! """
87
88            def start_plugin(self, plugin):
89                """ Start a plugin. """
90
91                self.started = plugin
92
93            def stop_plugin(self, plugin):
94                """ Stop a plugin. """
95
96                self.stopped = plugin
97
98        class PluginA(Plugin):
99            id = 'A'
100
101        class PluginB(Plugin):
102            id = 'B'
103
104        plugin_activator = NullPluginActivator()
105
106        a = PluginA(activator=plugin_activator)
107        b = PluginB()
108
109        application = TestApplication(plugins=[a, b])
110        application.start()
111
112        # Make sure A's plugin activator was called.
113        self.assertEqual(a, plugin_activator.started)
114
115        # Stop the application.
116        application.stop()
117
118        # Make sure A's plugin activator was called.
119        self.assertEqual(a, plugin_activator.stopped)
120
121    def test_service(self):
122        """ service """
123
124        class Foo(HasTraits):
125            pass
126
127        class Bar(HasTraits):
128            pass
129
130        class Baz(HasTraits):
131            pass
132
133        class PluginA(Plugin):
134            id = 'A'
135            foo = Instance(Foo, (), service=True)
136            bar = Instance(Bar, (), service=True)
137            baz = Instance(Baz, (), service=True)
138
139        a = PluginA()
140
141        application = TestApplication(plugins=[a])
142        application.start()
143
144        # Make sure the services were registered.
145        self.assertNotEqual(None, application.get_service(Foo))
146        self.assertEqual(a.foo, application.get_service(Foo))
147
148        self.assertNotEqual(None, application.get_service(Bar))
149        self.assertEqual(a.bar, application.get_service(Bar))
150
151        self.assertNotEqual(None, application.get_service(Baz))
152        self.assertEqual(a.baz, application.get_service(Baz))
153
154        application.stop()
155
156        # Make sure the service was unregistered.
157        self.assertEqual(None, application.get_service(Foo))
158        self.assertEqual(None, application.get_service(Bar))
159        self.assertEqual(None, application.get_service(Baz))
160
161    def test_service_protocol(self):
162        """ service protocol """
163
164        class IFoo(Interface):
165            pass
166
167        class IBar(Interface):
168            pass
169
170        @provides(IFoo, IBar)
171        class Foo(HasTraits):
172            pass
173
174        class PluginA(Plugin):
175            id = 'A'
176            foo = Instance(Foo, (), service=True, service_protocol=IBar)
177
178        a = PluginA()
179
180        application = TestApplication(plugins=[a])
181        application.start()
182
183        # Make sure the service was registered with the 'IBar' protocol.
184        self.assertNotEqual(None, application.get_service(IBar))
185        self.assertEqual(a.foo, application.get_service(IBar))
186
187        application.stop()
188
189        # Make sure the service was unregistered.
190        self.assertEqual(None, application.get_service(IBar))
191
192    def test_multiple_trait_contributions(self):
193        """ multiple trait contributions """
194
195        class PluginA(Plugin):
196            id = 'A'
197            x  = ExtensionPoint(List, id='x')
198
199        class PluginB(Plugin):
200            id = 'B'
201
202            x  = List([1, 2, 3], contributes_to='x')
203            y  = List([4, 5, 6], contributes_to='x')
204
205        a = PluginA()
206        b = PluginB()
207
208        application = TestApplication(plugins=[a, b])
209
210        # We should get an error because the plugin has multiple traits
211        # contributing to the same extension point.
212        with self.assertRaises(ValueError):
213            application.get_extensions("x")
214
215    def test_exception_in_trait_contribution(self):
216        """ exception in trait contribution """
217
218        class PluginA(Plugin):
219            id = 'A'
220            x  = ExtensionPoint(List, id='x')
221
222        class PluginB(Plugin):
223            id = 'B'
224
225            x  = List(contributes_to='x')
226
227            def _x_default(self):
228                """ Trait initializer. """
229
230                raise 1/0
231
232        a = PluginA()
233        b = PluginB()
234
235        application = TestApplication(plugins=[a, b])
236
237        # We should get an when we try to get the contributions to the
238        # extension point.
239        with self.assertRaises(ZeroDivisionError):
240            application.get_extensions("x")
241
242    def test_contributes_to(self):
243        """ contributes to """
244
245        class PluginA(Plugin):
246            id = 'A'
247            x  = ExtensionPoint(List, id='x')
248
249        class PluginB(Plugin):
250            id = 'B'
251            x  = List([1, 2, 3], contributes_to='x')
252
253        a = PluginA()
254        b = PluginB()
255
256        application = TestApplication(plugins=[a, b])
257
258        # We should get an error because the plugin has multiple traits
259        # contributing to the same extension point.
260        self.assertEqual([1, 2, 3], application.get_extensions('x'))
261
262    def test_contributes_to_decorator(self):
263        """ contributes to decorator """
264
265        class PluginA(Plugin):
266            id = 'A'
267            x  = ExtensionPoint(List, id='x')
268
269        class PluginB(Plugin):
270            id = 'B'
271
272            @contributes_to('x')
273            def _x_contributions(self):
274                return [1, 2, 3]
275
276        a = PluginA()
277        b = PluginB()
278
279        application = TestApplication(plugins=[a, b])
280        self.assertEqual([1, 2, 3], application.get_extensions('x'))
281
282    def test_contributes_to_decorator_ignored_if_trait_present(self):
283        """ contributes to decorator ignored if trait present """
284
285        class PluginA(Plugin):
286            id = 'A'
287            x  = ExtensionPoint(List, id='x')
288
289        class PluginB(Plugin):
290            id = 'B'
291            x  = List([1, 2, 3], contributes_to='x')
292
293            @contributes_to('x')
294            def _x_contributions(self):
295                return [4, 5, 6]
296
297        a = PluginA()
298        b = PluginB()
299
300        application = TestApplication(plugins=[a, b])
301        self.assertEqual([1, 2, 3], application.get_extensions('x'))
302
303    def test_add_plugins_to_empty_application(self):
304        """ add plugins to empty application """
305
306        class PluginA(Plugin):
307            id = 'A'
308            x  = ExtensionPoint(List(Int), id='x')
309
310            def _x_items_changed(self, event):
311                self.added   = event.added
312                self.removed = event.removed
313
314        class PluginB(Plugin):
315            id = 'B'
316            x  = List(Int, [1, 2, 3], contributes_to='x')
317
318        class PluginC(Plugin):
319            id = 'C'
320            x  = List(Int, [4, 5, 6], contributes_to='x')
321
322        a = PluginA()
323        b = PluginB()
324        c = PluginC()
325
326        # Create an empty application.
327        application = TestApplication()
328        application.start()
329
330        # Add the plugin that offers the extension point.
331        application.add_plugin(a)
332
333        #######################################################################
334        # fixme: Currently, we connect up extension point traits when the
335        # plugin is started. Is this right? Should we start plugins by default
336        # when we add them (and maybe have the ability to add a plugin without
337        # starting it?).
338        #
339        # I think we should start the plugin, otherwise you have the wierdness
340        # that the extension contributed by the plugin are available after
341        # the call to 'add_plugin', but the plugin isn't started?!?
342        #######################################################################
343
344        application.start_plugin(a)
345
346        #######################################################################
347        # fixme: Currently, we only fire changed events if an extension point
348        # has already been accessed! Is this right?
349        #######################################################################
350
351        self.assertEqual([], a.x)
352
353        # Add a plugin that contributes to the extension point.
354        application.add_plugin(b)
355
356        # Make sure that we pick up B's extensions and that the appropriate
357        # trait event was fired.
358        self.assertEqual([1, 2, 3], a.x)
359        self.assertEqual([1, 2, 3], a.added)
360
361        # Add another plugin that contributes to the extension point.
362        application.add_plugin(c)
363
364        self.assertEqual([1, 2, 3, 4, 5, 6], a.x)
365        self.assertEqual([4, 5, 6], a.added)
366
367        # Remove the first contributing plugin.
368        application.remove_plugin(b)
369
370        self.assertEqual([4, 5, 6], a.x)
371        self.assertEqual([1, 2, 3], a.removed)
372
373        # Remove the second contributing plugin.
374        application.remove_plugin(c)
375
376        self.assertEqual([], a.x)
377        self.assertEqual([4, 5, 6], a.removed)
378
379    def test_home(self):
380        """ home """
381
382        class PluginA(Plugin):
383            id = 'A'
384
385        class PluginB(Plugin):
386            id = 'B'
387
388        a = PluginA()
389        b = PluginB()
390
391        application = TestApplication(plugins=[a, b])
392
393        # Make sure that each plugin gets its own directory.
394        self.assertEqual(join(application.home, 'plugins', a.id), a.home)
395        self.assertEqual(join(application.home, 'plugins', b.id), b.home)
396
397        # Make sure that the directories got created.
398        self.assertTrue(exists(a.home))
399        self.assertTrue(exists(b.home))
400
401        # Create a new application with plugins with the same Id to make sure
402        # that it all works when the directories already exist.
403        a = PluginA()
404        b = PluginB()
405
406        application = TestApplication(plugins=[a, b])
407
408        # Make sure that each plugin gets its own directory.
409        self.assertEqual(join(application.home, 'plugins', a.id), a.home)
410        self.assertEqual(join(application.home, 'plugins', b.id), b.home)
411
412        # Make sure the directories got created.
413        self.assertTrue(exists(a.home))
414        self.assertTrue(exists(b.home))
415
416    def test_no_recursion(self):
417        """ Regression test for #119. """
418
419        class PluginA(Plugin):
420            id = 'A'
421            x  = ExtensionPoint(List, id='bob')
422
423        application = Application(plugins=[PluginA()])
424        application.get_extensions('bob')
425