~bzr-pqm/bzr/bzr.dev

5557.1.7 by John Arbash Meinel
Merge in the bzr.dev 5582
1
# Copyright (C) 2005-2011 Canonical Ltd
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
2
#
750 by Martin Pool
- stubbed-out tests for python plugins
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
7
#
750 by Martin Pool
- stubbed-out tests for python plugins
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
2052.3.1 by John Arbash Meinel
Add tests to cleanup the copyright of all source files
12
#
750 by Martin Pool
- stubbed-out tests for python plugins
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
4183.7.1 by Sabin Iacob
update FSF mailing address
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
750 by Martin Pool
- stubbed-out tests for python plugins
16
17
"""Tests for plugins"""
18
1185.16.83 by mbp at sourcefrog
- notes on testability of plugins
19
# XXX: There are no plugin tests at the moment because the plugin module
20
# affects the global state of the process.  See bzrlib/plugins.py for more
21
# comments.
22
5086.1.3 by Vincent Ladeuil
Fix imports in test_plugins.
23
from cStringIO import StringIO
2967.4.5 by Daniel Watkins
Added test for badly-named plugins.
24
import logging
1185.16.83 by mbp at sourcefrog
- notes on testability of plugins
25
import os
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
26
import sys
750 by Martin Pool
- stubbed-out tests for python plugins
27
5086.1.3 by Vincent Ladeuil
Fix imports in test_plugins.
28
import bzrlib
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
29
from bzrlib import (
5268.5.1 by Vincent Ladeuil
Reproduce bug #591215.
30
    errors,
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
31
    osutils,
32
    plugin,
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
33
    plugins,
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
34
    tests,
5086.1.8 by Vincent Ladeuil
Fix warnings during autoload, add doc and a NEWS entry.
35
    trace,
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
36
    )
1141 by Martin Pool
- rename FunctionalTest to TestCaseInTempDir
37
1185.16.83 by mbp at sourcefrog
- notes on testability of plugins
38
1492 by Robert Collins
Support decoration of commands.
39
# TODO: Write a test for plugin decoration of commands.
1515 by Robert Collins
* Plugins with the same name in different directories in the bzr plugin
40
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
41
class BaseTestPlugins(tests.TestCaseInTempDir):
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
42
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
43
    def create_plugin(self, name, source=None, dir='.', file_name=None):
44
        if source is None:
45
            source = '''\
46
"""This is the doc for %s"""
47
''' % (name)
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
48
        if file_name is None:
49
            file_name = name + '.py'
50
        # 'source' must not fail to load
5086.1.7 by Vincent Ladeuil
Cleaner fix for bug #411413.
51
        path = osutils.pathjoin(dir, file_name)
52
        f = open(path, 'w')
53
        self.addCleanup(os.unlink, path)
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
54
        try:
55
            f.write(source + '\n')
56
        finally:
57
            f.close()
58
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
59
    def create_plugin_package(self, name, dir=None, source=None):
60
        if dir is None:
61
            dir = name
62
        if source is None:
63
            source = '''\
64
"""This is the doc for %s"""
65
dir_source = '%s'
66
''' % (name, dir)
67
        os.makedirs(dir)
5086.5.9 by Vincent Ladeuil
More tests.
68
        def cleanup():
69
            # Workaround lazy import random? madness
70
            osutils.rmtree(dir)
71
        self.addCleanup(cleanup)
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
72
        self.create_plugin(name, source, dir,
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
73
                           file_name='__init__.py')
74
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
75
    def _unregister_plugin(self, name):
76
        """Remove the plugin from sys.modules and the bzrlib namespace."""
77
        py_name = 'bzrlib.plugins.%s' % name
78
        if py_name in sys.modules:
79
            del sys.modules[py_name]
80
        if getattr(bzrlib.plugins, name, None) is not None:
81
            delattr(bzrlib.plugins, name)
82
5268.6.1 by Vincent Ladeuil
Drive-by fix of the submodule leak.
83
    def _unregister_plugin_submodule(self, plugin_name, submodule_name):
84
        """Remove the submodule from sys.modules and the bzrlib namespace."""
85
        py_name = 'bzrlib.plugins.%s.%s' % (plugin_name, submodule_name)
86
        if py_name in sys.modules:
87
            del sys.modules[py_name]
5268.6.3 by Vincent Ladeuil
BZR_PLUGINS_AT should use packages properly to handle relative imports.
88
        plugin = getattr(bzrlib.plugins, plugin_name, None)
89
        if plugin is not None:
90
            if getattr(plugin, submodule_name, None) is not None:
91
                delattr(plugin, submodule_name)
5268.6.1 by Vincent Ladeuil
Drive-by fix of the submodule leak.
92
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
93
    def assertPluginUnknown(self, name):
5784.1.1 by Martin Pool
Stop using failIf, failUnless, etc
94
        self.assertFalse(getattr(bzrlib.plugins, name, None) is not None)
95
        self.assertFalse('bzrlib.plugins.%s' % name in sys.modules)
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
96
97
    def assertPluginKnown(self, name):
5784.1.1 by Martin Pool
Stop using failIf, failUnless, etc
98
        self.assertTrue(getattr(bzrlib.plugins, name, None) is not None)
99
        self.assertTrue('bzrlib.plugins.%s' % name in sys.modules)
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
100
101
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
102
class TestLoadingPlugins(BaseTestPlugins):
1515 by Robert Collins
* Plugins with the same name in different directories in the bzr plugin
103
104
    activeattributes = {}
105
106
    def test_plugins_with_the_same_name_are_not_loaded(self):
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
107
        # This test tests that having two plugins in different directories does
108
        # not result in both being loaded when they have the same name.  get a
109
        # file name we can use which is also a valid attribute for accessing in
110
        # activeattributes. - we cannot give import parameters.
1515 by Robert Collins
* Plugins with the same name in different directories in the bzr plugin
111
        tempattribute = "0"
5784.1.1 by Martin Pool
Stop using failIf, failUnless, etc
112
        self.assertFalse(tempattribute in self.activeattributes)
1515 by Robert Collins
* Plugins with the same name in different directories in the bzr plugin
113
        # set a place for the plugins to record their loading, and at the same
114
        # time validate that the location the plugins should record to is
115
        # valid and correct.
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
116
        self.__class__.activeattributes [tempattribute] = []
5784.1.1 by Martin Pool
Stop using failIf, failUnless, etc
117
        self.assertTrue(tempattribute in self.activeattributes)
1515 by Robert Collins
* Plugins with the same name in different directories in the bzr plugin
118
        # create two plugin directories
119
        os.mkdir('first')
120
        os.mkdir('second')
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
121
        # write a plugin that will record when its loaded in the
1515 by Robert Collins
* Plugins with the same name in different directories in the bzr plugin
122
        # tempattribute list.
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
123
        template = ("from bzrlib.tests.test_plugins import TestLoadingPlugins\n"
124
                    "TestLoadingPlugins.activeattributes[%r].append('%s')\n")
2652.2.7 by Blake Winton
fix lines which were wider than 79 chars. Also handle files a little more safely.
125
126
        outfile = open(os.path.join('first', 'plugin.py'), 'w')
127
        try:
2911.6.1 by Blake Winton
Change 'print >> f,'s to 'f.write('s.
128
            outfile.write(template % (tempattribute, 'first'))
129
            outfile.write('\n')
2652.2.7 by Blake Winton
fix lines which were wider than 79 chars. Also handle files a little more safely.
130
        finally:
131
            outfile.close()
132
133
        outfile = open(os.path.join('second', 'plugin.py'), 'w')
134
        try:
2911.6.1 by Blake Winton
Change 'print >> f,'s to 'f.write('s.
135
            outfile.write(template % (tempattribute, 'second'))
136
            outfile.write('\n')
2652.2.7 by Blake Winton
fix lines which were wider than 79 chars. Also handle files a little more safely.
137
        finally:
138
            outfile.close()
139
1515 by Robert Collins
* Plugins with the same name in different directories in the bzr plugin
140
        try:
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
141
            bzrlib.plugin.load_from_path(['first', 'second'])
142
            self.assertEqual(['first'], self.activeattributes[tempattribute])
143
        finally:
144
            # remove the plugin 'plugin'
145
            del self.activeattributes[tempattribute]
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
146
            self._unregister_plugin('plugin')
147
        self.assertPluginUnknown('plugin')
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
148
149
    def test_plugins_from_different_dirs_can_demand_load(self):
5784.1.1 by Martin Pool
Stop using failIf, failUnless, etc
150
        self.assertFalse('bzrlib.plugins.pluginone' in sys.modules)
151
        self.assertFalse('bzrlib.plugins.plugintwo' in sys.modules)
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
152
        # This test tests that having two plugins in different
153
        # directories with different names allows them both to be loaded, when
154
        # we do a direct import statement.
155
        # Determine a file name we can use which is also a valid attribute
156
        # for accessing in activeattributes. - we cannot give import parameters.
157
        tempattribute = "different-dirs"
5784.1.1 by Martin Pool
Stop using failIf, failUnless, etc
158
        self.assertFalse(tempattribute in self.activeattributes)
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
159
        # set a place for the plugins to record their loading, and at the same
160
        # time validate that the location the plugins should record to is
161
        # valid and correct.
162
        bzrlib.tests.test_plugins.TestLoadingPlugins.activeattributes \
163
            [tempattribute] = []
5784.1.1 by Martin Pool
Stop using failIf, failUnless, etc
164
        self.assertTrue(tempattribute in self.activeattributes)
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
165
        # create two plugin directories
166
        os.mkdir('first')
167
        os.mkdir('second')
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
168
        # write plugins that will record when they are loaded in the
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
169
        # tempattribute list.
170
        template = ("from bzrlib.tests.test_plugins import TestLoadingPlugins\n"
171
                    "TestLoadingPlugins.activeattributes[%r].append('%s')\n")
2652.2.7 by Blake Winton
fix lines which were wider than 79 chars. Also handle files a little more safely.
172
173
        outfile = open(os.path.join('first', 'pluginone.py'), 'w')
174
        try:
2911.6.1 by Blake Winton
Change 'print >> f,'s to 'f.write('s.
175
            outfile.write(template % (tempattribute, 'first'))
176
            outfile.write('\n')
2652.2.7 by Blake Winton
fix lines which were wider than 79 chars. Also handle files a little more safely.
177
        finally:
178
            outfile.close()
179
180
        outfile = open(os.path.join('second', 'plugintwo.py'), 'w')
181
        try:
2911.6.1 by Blake Winton
Change 'print >> f,'s to 'f.write('s.
182
            outfile.write(template % (tempattribute, 'second'))
183
            outfile.write('\n')
2652.2.7 by Blake Winton
fix lines which were wider than 79 chars. Also handle files a little more safely.
184
        finally:
185
            outfile.close()
186
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
187
        oldpath = bzrlib.plugins.__path__
188
        try:
5784.1.1 by Martin Pool
Stop using failIf, failUnless, etc
189
            self.assertFalse('bzrlib.plugins.pluginone' in sys.modules)
190
            self.assertFalse('bzrlib.plugins.plugintwo' in sys.modules)
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
191
            bzrlib.plugins.__path__ = ['first', 'second']
192
            exec "import bzrlib.plugins.pluginone"
193
            self.assertEqual(['first'], self.activeattributes[tempattribute])
194
            exec "import bzrlib.plugins.plugintwo"
195
            self.assertEqual(['first', 'second'],
196
                self.activeattributes[tempattribute])
1515 by Robert Collins
* Plugins with the same name in different directories in the bzr plugin
197
        finally:
198
            # remove the plugin 'plugin'
199
            del self.activeattributes[tempattribute]
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
200
            self._unregister_plugin('pluginone')
201
            self._unregister_plugin('plugintwo')
202
        self.assertPluginUnknown('pluginone')
203
        self.assertPluginUnknown('plugintwo')
1516 by Robert Collins
* bzrlib.plugin.all_plugins has been changed from an attribute to a
204
2652.2.1 by Blake Winton
Add a test for BZR_PLUGIN_PATH, and code and another test to allow BZR_PLUGIN_PATH to contain trailing slashes.
205
    def test_plugins_can_load_from_directory_with_trailing_slash(self):
206
        # This test tests that a plugin can load from a directory when the
207
        # directory in the path has a trailing slash.
2652.2.7 by Blake Winton
fix lines which were wider than 79 chars. Also handle files a little more safely.
208
        # check the plugin is not loaded already
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
209
        self.assertPluginUnknown('ts_plugin')
2652.2.7 by Blake Winton
fix lines which were wider than 79 chars. Also handle files a little more safely.
210
        tempattribute = "trailing-slash"
5784.1.1 by Martin Pool
Stop using failIf, failUnless, etc
211
        self.assertFalse(tempattribute in self.activeattributes)
2652.2.3 by Blake Winton
Understand the code and comments of the test, instead of just cargo-culting them.
212
        # set a place for the plugin to record its loading, and at the same
213
        # time validate that the location the plugin should record to is
2652.2.1 by Blake Winton
Add a test for BZR_PLUGIN_PATH, and code and another test to allow BZR_PLUGIN_PATH to contain trailing slashes.
214
        # valid and correct.
215
        bzrlib.tests.test_plugins.TestLoadingPlugins.activeattributes \
216
            [tempattribute] = []
5784.1.1 by Martin Pool
Stop using failIf, failUnless, etc
217
        self.assertTrue(tempattribute in self.activeattributes)
2652.2.3 by Blake Winton
Understand the code and comments of the test, instead of just cargo-culting them.
218
        # create a directory for the plugin
219
        os.mkdir('plugin_test')
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
220
        # write a plugin that will record when its loaded in the
2652.2.1 by Blake Winton
Add a test for BZR_PLUGIN_PATH, and code and another test to allow BZR_PLUGIN_PATH to contain trailing slashes.
221
        # tempattribute list.
222
        template = ("from bzrlib.tests.test_plugins import TestLoadingPlugins\n"
223
                    "TestLoadingPlugins.activeattributes[%r].append('%s')\n")
2652.2.7 by Blake Winton
fix lines which were wider than 79 chars. Also handle files a little more safely.
224
225
        outfile = open(os.path.join('plugin_test', 'ts_plugin.py'), 'w')
226
        try:
2911.6.1 by Blake Winton
Change 'print >> f,'s to 'f.write('s.
227
            outfile.write(template % (tempattribute, 'plugin'))
2911.6.4 by Blake Winton
Fix test failures
228
            outfile.write('\n')
2652.2.7 by Blake Winton
fix lines which were wider than 79 chars. Also handle files a little more safely.
229
        finally:
230
            outfile.close()
231
2652.2.1 by Blake Winton
Add a test for BZR_PLUGIN_PATH, and code and another test to allow BZR_PLUGIN_PATH to contain trailing slashes.
232
        try:
2652.2.3 by Blake Winton
Understand the code and comments of the test, instead of just cargo-culting them.
233
            bzrlib.plugin.load_from_path(['plugin_test'+os.sep])
234
            self.assertEqual(['plugin'], self.activeattributes[tempattribute])
2652.2.1 by Blake Winton
Add a test for BZR_PLUGIN_PATH, and code and another test to allow BZR_PLUGIN_PATH to contain trailing slashes.
235
        finally:
236
            del self.activeattributes[tempattribute]
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
237
            self._unregister_plugin('ts_plugin')
238
        self.assertPluginUnknown('ts_plugin')
2652.2.1 by Blake Winton
Add a test for BZR_PLUGIN_PATH, and code and another test to allow BZR_PLUGIN_PATH to contain trailing slashes.
239
3766.3.2 by Robert Collins
Fix reporting of incompatible api plugin load errors, fixing bug 279451.
240
    def load_and_capture(self, name):
241
        """Load plugins from '.' capturing the output.
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
242
3766.3.2 by Robert Collins
Fix reporting of incompatible api plugin load errors, fixing bug 279451.
243
        :param name: The name of the plugin.
244
        :return: A string with the log from the plugin loading call.
245
        """
2967.4.5 by Daniel Watkins
Added test for badly-named plugins.
246
        # Capture output
247
        stream = StringIO()
3766.3.2 by Robert Collins
Fix reporting of incompatible api plugin load errors, fixing bug 279451.
248
        try:
249
            handler = logging.StreamHandler(stream)
250
            log = logging.getLogger('bzr')
251
            log.addHandler(handler)
252
            try:
253
                try:
254
                    bzrlib.plugin.load_from_path(['.'])
255
                finally:
256
                    if 'bzrlib.plugins.%s' % name in sys.modules:
257
                        del sys.modules['bzrlib.plugins.%s' % name]
258
                    if getattr(bzrlib.plugins, name, None):
259
                        delattr(bzrlib.plugins, name)
260
            finally:
261
                # Stop capturing output
262
                handler.flush()
263
                handler.close()
264
                log.removeHandler(handler)
265
            return stream.getvalue()
266
        finally:
267
            stream.close()
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
268
3766.3.2 by Robert Collins
Fix reporting of incompatible api plugin load errors, fixing bug 279451.
269
    def test_plugin_with_bad_api_version_reports(self):
5616.7.3 by Martin Pool
Put plugin warnings into both the apport and plain crash report
270
        """Try loading a plugin that requests an unsupported api.
271
        
5616.7.12 by Martin Pool
Comment correction
272
        Observe that it records the problem but doesn't complain on stderr.
5616.7.7 by Martin Pool
Paper over test global state dependency
273
274
        See https://bugs.launchpad.net/bzr/+bug/704195
5616.7.3 by Martin Pool
Put plugin warnings into both the apport and plain crash report
275
        """
5616.7.2 by Martin Pool
Include plugin warnings in apport crash
276
        self.overrideAttr(plugin, 'plugin_warnings', {})
3766.3.2 by Robert Collins
Fix reporting of incompatible api plugin load errors, fixing bug 279451.
277
        name = 'wants100.py'
278
        f = file(name, 'w')
279
        try:
280
            f.write("import bzrlib.api\n"
281
                "bzrlib.api.require_any_api(bzrlib, [(1, 0, 0)])\n")
282
        finally:
283
            f.close()
284
        log = self.load_and_capture(name)
5616.7.1 by Martin Pool
Record but don't show warnings about updated plugins
285
        self.assertNotContainsRe(log,
286
            r"It requested API version")
287
        self.assertEquals(
288
            ['wants100'],
5616.7.2 by Martin Pool
Include plugin warnings in apport crash
289
            plugin.plugin_warnings.keys())
5616.7.1 by Martin Pool
Record but don't show warnings about updated plugins
290
        self.assertContainsRe(
5616.7.3 by Martin Pool
Put plugin warnings into both the apport and plain crash report
291
            plugin.plugin_warnings['wants100'][0],
3766.3.2 by Robert Collins
Fix reporting of incompatible api plugin load errors, fixing bug 279451.
292
            r"It requested API version")
293
294
    def test_plugin_with_bad_name_does_not_load(self):
295
        # The file name here invalid for a python module.
296
        name = 'bzr-bad plugin-name..py'
297
        file(name, 'w').close()
298
        log = self.load_and_capture(name)
299
        self.assertContainsRe(log,
3290.1.1 by James Westby
Strip "bzr_" from the start of the suggested plugin name.
300
            r"Unable to load 'bzr-bad plugin-name\.' in '\.' as a plugin "
301
            "because the file path isn't a valid module name; try renaming "
302
            "it to 'bad_plugin_name_'\.")
2967.4.5 by Daniel Watkins
Added test for badly-named plugins.
303
1516 by Robert Collins
* bzrlib.plugin.all_plugins has been changed from an attribute to a
304
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
305
class TestPlugins(BaseTestPlugins):
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
306
307
    def setup_plugin(self, source=""):
308
        # This test tests a new plugin appears in bzrlib.plugin.plugins().
309
        # check the plugin is not loaded already
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
310
        self.assertPluginUnknown('plugin')
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
311
        # write a plugin that _cannot_ fail to load.
2911.6.1 by Blake Winton
Change 'print >> f,'s to 'f.write('s.
312
        file('plugin.py', 'w').write(source + '\n')
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
313
        self.addCleanup(self.teardown_plugin)
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
314
        plugin.load_from_path(['.'])
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
315
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
316
    def teardown_plugin(self):
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
317
        self._unregister_plugin('plugin')
318
        self.assertPluginUnknown('plugin')
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
319
320
    def test_plugin_appears_in_plugins(self):
321
        self.setup_plugin()
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
322
        self.assertPluginKnown('plugin')
323
        p = plugin.plugins()['plugin']
324
        self.assertIsInstance(p, bzrlib.plugin.PlugIn)
325
        self.assertEqual(p.module, plugins.plugin)
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
326
327
    def test_trivial_plugin_get_path(self):
328
        self.setup_plugin()
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
329
        p = plugin.plugins()['plugin']
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
330
        plugin_path = self.test_dir + '/plugin.py'
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
331
        self.assertIsSameRealPath(plugin_path, osutils.normpath(p.path()))
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
332
3193.2.1 by Alexander Belchenko
show path to plugin module as *.py instead of *.pyc if python source available
333
    def test_plugin_get_path_py_not_pyc(self):
5086.1.3 by Vincent Ladeuil
Fix imports in test_plugins.
334
        # first import creates plugin.pyc
335
        self.setup_plugin()
3193.2.1 by Alexander Belchenko
show path to plugin module as *.py instead of *.pyc if python source available
336
        self.teardown_plugin()
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
337
        plugin.load_from_path(['.']) # import plugin.pyc
338
        p = plugin.plugins()['plugin']
3193.2.1 by Alexander Belchenko
show path to plugin module as *.py instead of *.pyc if python source available
339
        plugin_path = self.test_dir + '/plugin.py'
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
340
        self.assertIsSameRealPath(plugin_path, osutils.normpath(p.path()))
3193.2.1 by Alexander Belchenko
show path to plugin module as *.py instead of *.pyc if python source available
341
342
    def test_plugin_get_path_pyc_only(self):
5086.1.3 by Vincent Ladeuil
Fix imports in test_plugins.
343
        # first import creates plugin.pyc (or plugin.pyo depending on __debug__)
344
        self.setup_plugin()
3193.2.1 by Alexander Belchenko
show path to plugin module as *.py instead of *.pyc if python source available
345
        self.teardown_plugin()
346
        os.unlink(self.test_dir + '/plugin.py')
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
347
        plugin.load_from_path(['.']) # import plugin.pyc (or .pyo)
348
        p = plugin.plugins()['plugin']
3193.2.1 by Alexander Belchenko
show path to plugin module as *.py instead of *.pyc if python source available
349
        if __debug__:
350
            plugin_path = self.test_dir + '/plugin.pyc'
351
        else:
352
            plugin_path = self.test_dir + '/plugin.pyo'
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
353
        self.assertIsSameRealPath(plugin_path, osutils.normpath(p.path()))
3193.2.1 by Alexander Belchenko
show path to plugin module as *.py instead of *.pyc if python source available
354
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
355
    def test_no_test_suite_gives_None_for_test_suite(self):
356
        self.setup_plugin()
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
357
        p = plugin.plugins()['plugin']
358
        self.assertEqual(None, p.test_suite())
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
359
360
    def test_test_suite_gives_test_suite_result(self):
361
        source = """def test_suite(): return 'foo'"""
362
        self.setup_plugin(source)
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
363
        p = plugin.plugins()['plugin']
364
        self.assertEqual('foo', p.test_suite())
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
365
3302.8.21 by Vincent Ladeuil
Fixed as per Robert's review.
366
    def test_no_load_plugin_tests_gives_None_for_load_plugin_tests(self):
3302.8.10 by Vincent Ladeuil
Prepare bzrlib.plugin to use the new test loader.
367
        self.setup_plugin()
5086.1.3 by Vincent Ladeuil
Fix imports in test_plugins.
368
        loader = tests.TestUtil.TestLoader()
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
369
        p = plugin.plugins()['plugin']
370
        self.assertEqual(None, p.load_plugin_tests(loader))
3302.8.10 by Vincent Ladeuil
Prepare bzrlib.plugin to use the new test loader.
371
3302.8.21 by Vincent Ladeuil
Fixed as per Robert's review.
372
    def test_load_plugin_tests_gives_load_plugin_tests_result(self):
3302.8.10 by Vincent Ladeuil
Prepare bzrlib.plugin to use the new test loader.
373
        source = """
374
def load_tests(standard_tests, module, loader):
375
    return 'foo'"""
376
        self.setup_plugin(source)
5086.1.3 by Vincent Ladeuil
Fix imports in test_plugins.
377
        loader = tests.TestUtil.TestLoader()
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
378
        p = plugin.plugins()['plugin']
379
        self.assertEqual('foo', p.load_plugin_tests(loader))
380
381
    def check_version_info(self, expected, source='', name='plugin'):
382
        self.setup_plugin(source)
383
        self.assertEqual(expected, plugin.plugins()[name].version_info())
3302.8.10 by Vincent Ladeuil
Prepare bzrlib.plugin to use the new test loader.
384
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
385
    def test_no_version_info(self):
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
386
        self.check_version_info(None)
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
387
388
    def test_with_version_info(self):
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
389
        self.check_version_info((1, 2, 3, 'dev', 4),
390
                                "version_info = (1, 2, 3, 'dev', 4)")
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
391
392
    def test_short_version_info_gets_padded(self):
393
        # the gtk plugin has version_info = (1,2,3) rather than the 5-tuple.
394
        # so we adapt it
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
395
        self.check_version_info((1, 2, 3, 'final', 0),
396
                                "version_info = (1, 2, 3)")
397
398
    def check_version(self, expected, source=None, name='plugin'):
399
        self.setup_plugin(source)
400
        self.assertEqual(expected, plugins[name].__version__)
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
401
402
    def test_no_version_info___version__(self):
403
        self.setup_plugin()
404
        plugin = bzrlib.plugin.plugins()['plugin']
405
        self.assertEqual("unknown", plugin.__version__)
406
3777.6.7 by Marius Kruger
* Can now also handle non-iteratable and string plugin versions.
407
    def test_str__version__with_version_info(self):
408
        self.setup_plugin("version_info = '1.2.3'")
409
        plugin = bzrlib.plugin.plugins()['plugin']
410
        self.assertEqual("1.2.3", plugin.__version__)
411
412
    def test_noniterable__version__with_version_info(self):
413
        self.setup_plugin("version_info = (1)")
414
        plugin = bzrlib.plugin.plugins()['plugin']
415
        self.assertEqual("1", plugin.__version__)
416
417
    def test_1__version__with_version_info(self):
418
        self.setup_plugin("version_info = (1,)")
419
        plugin = bzrlib.plugin.plugins()['plugin']
420
        self.assertEqual("1", plugin.__version__)
421
422
    def test_1_2__version__with_version_info(self):
3777.6.5 by Marius Kruger
add 2 more tests for plugin version numbers
423
        self.setup_plugin("version_info = (1, 2)")
424
        plugin = bzrlib.plugin.plugins()['plugin']
425
        self.assertEqual("1.2", plugin.__version__)
426
3777.6.7 by Marius Kruger
* Can now also handle non-iteratable and string plugin versions.
427
    def test_1_2_3__version__with_version_info(self):
3777.6.5 by Marius Kruger
add 2 more tests for plugin version numbers
428
        self.setup_plugin("version_info = (1, 2, 3)")
429
        plugin = bzrlib.plugin.plugins()['plugin']
430
        self.assertEqual("1.2.3", plugin.__version__)
431
432
    def test_candidate__version__with_version_info(self):
3777.6.4 by Marius Kruger
fix tests
433
        self.setup_plugin("version_info = (1, 2, 3, 'candidate', 1)")
434
        plugin = bzrlib.plugin.plugins()['plugin']
435
        self.assertEqual("1.2.3rc1", plugin.__version__)
436
437
    def test_dev__version__with_version_info(self):
438
        self.setup_plugin("version_info = (1, 2, 3, 'dev', 0)")
439
        plugin = bzrlib.plugin.plugins()['plugin']
440
        self.assertEqual("1.2.3dev", plugin.__version__)
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
441
3777.6.7 by Marius Kruger
* Can now also handle non-iteratable and string plugin versions.
442
    def test_dev_fallback__version__with_version_info(self):
443
        self.setup_plugin("version_info = (1, 2, 3, 'dev', 4)")
444
        plugin = bzrlib.plugin.plugins()['plugin']
4634.50.6 by John Arbash Meinel
Handle a plugin fallback versioning issue.
445
        self.assertEqual("1.2.3dev4", plugin.__version__)
3777.6.7 by Marius Kruger
* Can now also handle non-iteratable and string plugin versions.
446
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
447
    def test_final__version__with_version_info(self):
3777.6.4 by Marius Kruger
fix tests
448
        self.setup_plugin("version_info = (1, 2, 3, 'final', 0)")
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
449
        plugin = bzrlib.plugin.plugins()['plugin']
450
        self.assertEqual("1.2.3", plugin.__version__)
451
4634.50.6 by John Arbash Meinel
Handle a plugin fallback versioning issue.
452
    def test_final_fallback__version__with_version_info(self):
453
        self.setup_plugin("version_info = (1, 2, 3, 'final', 2)")
454
        plugin = bzrlib.plugin.plugins()['plugin']
5851.2.2 by Martin Pool
Format plugin version as 1.2.3.2 not 1.2.3.final.2
455
        self.assertEqual("1.2.3.2", plugin.__version__)
4634.50.6 by John Arbash Meinel
Handle a plugin fallback versioning issue.
456
2762.2.1 by Robert Collins
* ``bzr plugins`` now lists the version number for each plugin in square
457
5086.1.3 by Vincent Ladeuil
Fix imports in test_plugins.
458
class TestPluginHelp(tests.TestCaseInTempDir):
1733.2.5 by Michael Ellerman
Show which plugin (if any) provides a command.
459
460
    def split_help_commands(self):
461
        help = {}
462
        current = None
3908.1.1 by Andrew Bennetts
Try harder to avoid loading plugins during the test suite.
463
        out, err = self.run_bzr('--no-plugins help commands')
464
        for line in out.splitlines():
2034.1.2 by Aaron Bentley
Fix testcase
465
            if not line.startswith(' '):
466
                current = line.split()[0]
1733.2.5 by Michael Ellerman
Show which plugin (if any) provides a command.
467
            help[current] = help.get(current, '') + line
468
469
        return help
470
471
    def test_plugin_help_builtins_unaffected(self):
472
        # Check we don't get false positives
473
        help_commands = self.split_help_commands()
474
        for cmd_name in bzrlib.commands.builtin_command_names():
475
            if cmd_name in bzrlib.commands.plugin_command_names():
476
                continue
477
            try:
2432.1.12 by Robert Collins
Relocate command help onto Command.
478
                help = bzrlib.commands.get_cmd_object(cmd_name).get_help_text()
1733.2.5 by Michael Ellerman
Show which plugin (if any) provides a command.
479
            except NotImplementedError:
480
                # some commands have no help
481
                pass
482
            else:
2666.1.1 by Ian Clatworthy
Bazaar User Reference generated from online help
483
                self.assertNotContainsRe(help, 'plugin "[^"]*"')
1733.2.5 by Michael Ellerman
Show which plugin (if any) provides a command.
484
2432.1.12 by Robert Collins
Relocate command help onto Command.
485
            if cmd_name in help_commands.keys():
1733.2.5 by Michael Ellerman
Show which plugin (if any) provides a command.
486
                # some commands are hidden
487
                help = help_commands[cmd_name]
2666.1.1 by Ian Clatworthy
Bazaar User Reference generated from online help
488
                self.assertNotContainsRe(help, 'plugin "[^"]*"')
1733.2.5 by Michael Ellerman
Show which plugin (if any) provides a command.
489
490
    def test_plugin_help_shows_plugin(self):
491
        # Create a test plugin
492
        os.mkdir('plugin_test')
5086.1.3 by Vincent Ladeuil
Fix imports in test_plugins.
493
        f = open(osutils.pathjoin('plugin_test', 'myplug.py'), 'w')
5086.1.2 by Vincent Ladeuil
Cosmetic changes.
494
        f.write("""\
5086.1.3 by Vincent Ladeuil
Fix imports in test_plugins.
495
from bzrlib import commands
496
class cmd_myplug(commands.Command):
5131.2.1 by Martin
Permit bzrlib to run under python -OO by explictly assigning to __doc__ for user-visible docstrings
497
    __doc__ = '''Just a simple test plugin.'''
5086.1.2 by Vincent Ladeuil
Cosmetic changes.
498
    aliases = ['mplg']
499
    def run(self):
500
        print 'Hello from my plugin'
501
502
"""
503
)
1733.2.5 by Michael Ellerman
Show which plugin (if any) provides a command.
504
        f.close()
505
506
        try:
507
            # Check its help
2256.2.2 by Robert Collins
Allow 'import bzrlib.plugins.NAME' to work when the plugin NAME has not
508
            bzrlib.plugin.load_from_path(['plugin_test'])
1733.2.5 by Michael Ellerman
Show which plugin (if any) provides a command.
509
            bzrlib.commands.register_command( bzrlib.plugins.myplug.cmd_myplug)
2530.3.4 by Martin Pool
Deprecate run_bzr_captured in favour of just run_bzr
510
            help = self.run_bzr('help myplug')[0]
2666.1.1 by Ian Clatworthy
Bazaar User Reference generated from online help
511
            self.assertContainsRe(help, 'plugin "myplug"')
1733.2.5 by Michael Ellerman
Show which plugin (if any) provides a command.
512
            help = self.split_help_commands()['myplug']
2034.1.4 by Aaron Bentley
Change angle brackets to square brackets
513
            self.assertContainsRe(help, '\[myplug\]')
1733.2.5 by Michael Ellerman
Show which plugin (if any) provides a command.
514
        finally:
2204.3.2 by Alexander Belchenko
cherrypicking: test_plugin_help_shows_plugin: fix cleanup after test
515
            # unregister command
3785.1.1 by Aaron Bentley
Switch from dict to Registry for plugin_cmds
516
            if 'myplug' in bzrlib.commands.plugin_cmds:
517
                bzrlib.commands.plugin_cmds.remove('myplug')
2204.3.2 by Alexander Belchenko
cherrypicking: test_plugin_help_shows_plugin: fix cleanup after test
518
            # remove the plugin 'myplug'
519
            if getattr(bzrlib.plugins, 'myplug', None):
520
                delattr(bzrlib.plugins, 'myplug')
2215.4.1 by Alexander Belchenko
Bugfix #68124: Allow plugins import from zip archives.
521
522
2432.1.25 by Robert Collins
Return plugin module docstrings for 'bzr help plugin'.
523
class TestHelpIndex(tests.TestCase):
524
    """Tests for the PluginsHelpIndex class."""
525
526
    def test_default_constructable(self):
527
        index = plugin.PluginsHelpIndex()
528
529
    def test_get_topics_None(self):
530
        """Searching for None returns an empty list."""
531
        index = plugin.PluginsHelpIndex()
532
        self.assertEqual([], index.get_topics(None))
533
2475.1.1 by Martin Pool
Rename test_plugin tests and the example module used there.
534
    def test_get_topics_for_plugin(self):
535
        """Searching for plugin name gets its docstring."""
2432.1.25 by Robert Collins
Return plugin module docstrings for 'bzr help plugin'.
536
        index = plugin.PluginsHelpIndex()
2475.1.1 by Martin Pool
Rename test_plugin tests and the example module used there.
537
        # make a new plugin here for this test, even if we're run with
538
        # --no-plugins
539
        self.assertFalse(sys.modules.has_key('bzrlib.plugins.demo_module'))
540
        demo_module = FakeModule('', 'bzrlib.plugins.demo_module')
541
        sys.modules['bzrlib.plugins.demo_module'] = demo_module
2457.1.1 by Robert Collins
(robertc) Fix bzr --no-plugins selftest which was broken by the help indices patch. (Robert Collins, Martin Pool)
542
        try:
2475.1.1 by Martin Pool
Rename test_plugin tests and the example module used there.
543
            topics = index.get_topics('demo_module')
2457.1.1 by Robert Collins
(robertc) Fix bzr --no-plugins selftest which was broken by the help indices patch. (Robert Collins, Martin Pool)
544
            self.assertEqual(1, len(topics))
545
            self.assertIsInstance(topics[0], plugin.ModuleHelpTopic)
546
            self.assertEqual(demo_module, topics[0].module)
547
        finally:
2475.1.1 by Martin Pool
Rename test_plugin tests and the example module used there.
548
            del sys.modules['bzrlib.plugins.demo_module']
2432.1.25 by Robert Collins
Return plugin module docstrings for 'bzr help plugin'.
549
550
    def test_get_topics_no_topic(self):
551
        """Searching for something that is not a plugin returns []."""
552
        # test this by using a name that cannot be a plugin - its not
553
        # a valid python identifier.
554
        index = plugin.PluginsHelpIndex()
555
        self.assertEqual([], index.get_topics('nothing by this name'))
556
557
    def test_prefix(self):
558
        """PluginsHelpIndex has a prefix of 'plugins/'."""
559
        index = plugin.PluginsHelpIndex()
560
        self.assertEqual('plugins/', index.prefix)
561
2475.1.1 by Martin Pool
Rename test_plugin tests and the example module used there.
562
    def test_get_plugin_topic_with_prefix(self):
563
        """Searching for plugins/demo_module returns help."""
2432.1.25 by Robert Collins
Return plugin module docstrings for 'bzr help plugin'.
564
        index = plugin.PluginsHelpIndex()
2475.1.1 by Martin Pool
Rename test_plugin tests and the example module used there.
565
        self.assertFalse(sys.modules.has_key('bzrlib.plugins.demo_module'))
566
        demo_module = FakeModule('', 'bzrlib.plugins.demo_module')
567
        sys.modules['bzrlib.plugins.demo_module'] = demo_module
2457.1.1 by Robert Collins
(robertc) Fix bzr --no-plugins selftest which was broken by the help indices patch. (Robert Collins, Martin Pool)
568
        try:
2475.1.1 by Martin Pool
Rename test_plugin tests and the example module used there.
569
            topics = index.get_topics('plugins/demo_module')
2457.1.1 by Robert Collins
(robertc) Fix bzr --no-plugins selftest which was broken by the help indices patch. (Robert Collins, Martin Pool)
570
            self.assertEqual(1, len(topics))
571
            self.assertIsInstance(topics[0], plugin.ModuleHelpTopic)
572
            self.assertEqual(demo_module, topics[0].module)
573
        finally:
2475.1.1 by Martin Pool
Rename test_plugin tests and the example module used there.
574
            del sys.modules['bzrlib.plugins.demo_module']
2432.1.25 by Robert Collins
Return plugin module docstrings for 'bzr help plugin'.
575
576
577
class FakeModule(object):
578
    """A fake module to test with."""
579
580
    def __init__(self, doc, name):
581
        self.__doc__ = doc
582
        self.__name__ = name
583
584
585
class TestModuleHelpTopic(tests.TestCase):
586
    """Tests for the ModuleHelpTopic class."""
587
588
    def test_contruct(self):
589
        """Construction takes the module to document."""
590
        mod = FakeModule('foo', 'foo')
591
        topic = plugin.ModuleHelpTopic(mod)
592
        self.assertEqual(mod, topic.module)
593
594
    def test_get_help_text_None(self):
595
        """A ModuleHelpTopic returns the docstring for get_help_text."""
596
        mod = FakeModule(None, 'demo')
597
        topic = plugin.ModuleHelpTopic(mod)
598
        self.assertEqual("Plugin 'demo' has no docstring.\n",
599
            topic.get_help_text())
600
601
    def test_get_help_text_no_carriage_return(self):
602
        """ModuleHelpTopic.get_help_text adds a \n if needed."""
603
        mod = FakeModule('one line of help', 'demo')
604
        topic = plugin.ModuleHelpTopic(mod)
605
        self.assertEqual("one line of help\n",
606
            topic.get_help_text())
607
608
    def test_get_help_text_carriage_return(self):
609
        """ModuleHelpTopic.get_help_text adds a \n if needed."""
610
        mod = FakeModule('two lines of help\nand more\n', 'demo')
611
        topic = plugin.ModuleHelpTopic(mod)
612
        self.assertEqual("two lines of help\nand more\n",
613
            topic.get_help_text())
614
615
    def test_get_help_text_with_additional_see_also(self):
616
        mod = FakeModule('two lines of help\nand more', 'demo')
617
        topic = plugin.ModuleHelpTopic(mod)
618
        self.assertEqual("two lines of help\nand more\nSee also: bar, foo\n",
619
            topic.get_help_text(['foo', 'bar']))
2432.1.29 by Robert Collins
Add get_help_topic to ModuleHelpTopic.
620
621
    def test_get_help_topic(self):
622
        """The help topic for a plugin is its module name."""
2432.1.30 by Robert Collins
Fix the ModuleHelpTopic get_help_topic to be tested with closer to real world data and strip the bzrlib.plugins. prefix from the name.
623
        mod = FakeModule('two lines of help\nand more', 'bzrlib.plugins.demo')
2432.1.29 by Robert Collins
Add get_help_topic to ModuleHelpTopic.
624
        topic = plugin.ModuleHelpTopic(mod)
625
        self.assertEqual('demo', topic.get_help_topic())
2432.1.30 by Robert Collins
Fix the ModuleHelpTopic get_help_topic to be tested with closer to real world data and strip the bzrlib.plugins. prefix from the name.
626
        mod = FakeModule('two lines of help\nand more', 'bzrlib.plugins.foo_bar')
2432.1.29 by Robert Collins
Add get_help_topic to ModuleHelpTopic.
627
        topic = plugin.ModuleHelpTopic(mod)
628
        self.assertEqual('foo_bar', topic.get_help_topic())
3835.2.7 by Aaron Bentley
Add tests for plugins
629
630
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
631
class TestLoadFromPath(tests.TestCaseInTempDir):
632
633
    def setUp(self):
634
        super(TestLoadFromPath, self).setUp()
635
        # Change bzrlib.plugin to think no plugins have been loaded yet.
4985.1.5 by Vincent Ladeuil
Deploying the new overrideAttr facility further reduces the complexity
636
        self.overrideAttr(bzrlib.plugins, '__path__', [])
637
        self.overrideAttr(plugin, '_loaded', False)
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
638
639
        # Monkey-patch load_from_path to stop it from actually loading anything.
4985.1.5 by Vincent Ladeuil
Deploying the new overrideAttr facility further reduces the complexity
640
        self.overrideAttr(plugin, 'load_from_path', lambda dirs: None)
3835.2.7 by Aaron Bentley
Add tests for plugins
641
642
    def test_set_plugins_path_with_args(self):
643
        plugin.set_plugins_path(['a', 'b'])
644
        self.assertEqual(['a', 'b'], bzrlib.plugins.__path__)
645
646
    def test_set_plugins_path_defaults(self):
647
        plugin.set_plugins_path()
648
        self.assertEqual(plugin.get_standard_plugins_path(),
649
                         bzrlib.plugins.__path__)
650
651
    def test_get_standard_plugins_path(self):
652
        path = plugin.get_standard_plugins_path()
653
        for directory in path:
4412.2.1 by Vincent Ladeuil
Fix some OSX test regressions (well actual test bugs indeed).
654
            self.assertNotContainsRe(directory, r'\\/$')
3835.2.7 by Aaron Bentley
Add tests for plugins
655
        try:
656
            from distutils.sysconfig import get_python_lib
657
        except ImportError:
658
            pass
659
        else:
660
            if sys.platform != 'win32':
661
                python_lib = get_python_lib()
662
                for directory in path:
663
                    if directory.startswith(python_lib):
664
                        break
665
                else:
666
                    self.fail('No path to global plugins')
667
668
    def test_get_standard_plugins_path_env(self):
5570.3.9 by Vincent Ladeuil
More use cases for overrideEnv, _cleanEnvironment *may* contain too much variables now.
669
        self.overrideEnv('BZR_PLUGIN_PATH', 'foo/')
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
670
        path = plugin.get_standard_plugins_path()
671
        for directory in path:
672
            self.assertNotContainsRe(directory, r'\\/$')
3835.2.7 by Aaron Bentley
Add tests for plugins
673
674
    def test_load_plugins(self):
675
        plugin.load_plugins(['.'])
676
        self.assertEqual(bzrlib.plugins.__path__, ['.'])
677
        # subsequent loads are no-ops
678
        plugin.load_plugins(['foo'])
679
        self.assertEqual(bzrlib.plugins.__path__, ['.'])
680
681
    def test_load_plugins_default(self):
682
        plugin.load_plugins()
683
        path = plugin.get_standard_plugins_path()
684
        self.assertEqual(path, bzrlib.plugins.__path__)
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
685
686
5086.1.2 by Vincent Ladeuil
Cosmetic changes.
687
class TestEnvPluginPath(tests.TestCase):
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
688
689
    def setUp(self):
690
        super(TestEnvPluginPath, self).setUp()
4985.1.5 by Vincent Ladeuil
Deploying the new overrideAttr facility further reduces the complexity
691
        self.overrideAttr(plugin, 'DEFAULT_PLUGIN_PATH', None)
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
692
693
        self.user = plugin.get_user_plugin_path()
694
        self.site = plugin.get_site_plugin_path()
695
        self.core = plugin.get_core_plugin_path()
696
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
697
    def _list2paths(self, *args):
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
698
        paths = []
699
        for p in args:
700
            plugin._append_new_path(paths, p)
701
        return paths
702
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
703
    def _set_path(self, *args):
704
        path = os.pathsep.join(self._list2paths(*args))
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
705
        self.overrideEnv('BZR_PLUGIN_PATH', path)
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
706
707
    def check_path(self, expected_dirs, setting_dirs):
708
        if setting_dirs:
709
            self._set_path(*setting_dirs)
710
        actual = plugin.get_standard_plugins_path()
711
        self.assertEquals(self._list2paths(*expected_dirs), actual)
712
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
713
    def test_default(self):
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
714
        self.check_path([self.user, self.core, self.site],
715
                        None)
716
717
    def test_adhoc_policy(self):
718
        self.check_path([self.user, self.core, self.site],
719
                        ['+user', '+core', '+site'])
720
721
    def test_fallback_policy(self):
722
        self.check_path([self.core, self.site, self.user],
723
                        ['+core', '+site', '+user'])
724
725
    def test_override_policy(self):
726
        self.check_path([self.user, self.site, self.core],
727
                        ['+user', '+site', '+core'])
728
729
    def test_disable_user(self):
730
        self.check_path([self.core, self.site], ['-user'])
731
732
    def test_disable_user_twice(self):
733
        # Ensures multiple removals don't left cruft
734
        self.check_path([self.core, self.site], ['-user', '-user'])
735
4628.2.5 by Vincent Ladeuil
Fixes prompted by review.
736
    def test_duplicates_are_removed(self):
737
        self.check_path([self.user, self.core, self.site],
738
                        ['+user', '+user'])
739
        # And only the first reference is kept (since the later references will
5086.1.2 by Vincent Ladeuil
Cosmetic changes.
740
        # only produce '<plugin> already loaded' mutters)
4628.2.5 by Vincent Ladeuil
Fixes prompted by review.
741
        self.check_path([self.user, self.core, self.site],
742
                        ['+user', '+user', '+core',
743
                         '+user', '+site', '+site',
744
                         '+core'])
745
5086.1.5 by Vincent Ladeuil
Fix typo in test name.
746
    def test_disable_overrides_enable(self):
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
747
        self.check_path([self.core, self.site], ['-user', '+user'])
748
749
    def test_disable_core(self):
4628.2.3 by Vincent Ladeuil
Update doc and add NEWS entry.
750
        self.check_path([self.site], ['-core'])
751
        self.check_path([self.user, self.site], ['+user', '-core'])
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
752
753
    def test_disable_site(self):
4628.2.3 by Vincent Ladeuil
Update doc and add NEWS entry.
754
        self.check_path([self.core], ['-site'])
755
        self.check_path([self.user, self.core], ['-site', '+user'])
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
756
757
    def test_override_site(self):
4628.2.3 by Vincent Ladeuil
Update doc and add NEWS entry.
758
        self.check_path(['mysite', self.user, self.core],
759
                        ['mysite', '-site', '+user'])
760
        self.check_path(['mysite', self.core],
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
761
                        ['mysite', '-site'])
762
763
    def test_override_core(self):
4628.2.3 by Vincent Ladeuil
Update doc and add NEWS entry.
764
        self.check_path(['mycore', self.user, self.site],
765
                        ['mycore', '-core', '+user', '+site'])
766
        self.check_path(['mycore', self.site],
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
767
                        ['mycore', '-core'])
768
769
    def test_my_plugin_only(self):
770
        self.check_path(['myplugin'], ['myplugin', '-user', '-core', '-site'])
771
772
    def test_my_plugin_first(self):
773
        self.check_path(['myplugin', self.core, self.site, self.user],
774
                        ['myplugin', '+core', '+site', '+user'])
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
775
4628.2.5 by Vincent Ladeuil
Fixes prompted by review.
776
    def test_bogus_references(self):
777
        self.check_path(['+foo', '-bar', self.core, self.site],
778
                        ['+foo', '-bar'])
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
779
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
780
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
781
class TestDisablePlugin(BaseTestPlugins):
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
782
5086.1.7 by Vincent Ladeuil
Cleaner fix for bug #411413.
783
    def setUp(self):
784
        super(TestDisablePlugin, self).setUp()
785
        self.create_plugin_package('test_foo')
786
        # Make sure we don't pollute the plugins namespace
787
        self.overrideAttr(plugins, '__path__')
788
        # Be paranoid in case a test fail
789
        self.addCleanup(self._unregister_plugin, 'test_foo')
5086.1.8 by Vincent Ladeuil
Fix warnings during autoload, add doc and a NEWS entry.
790
791
    def test_cannot_import(self):
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
792
        self.overrideEnv('BZR_DISABLE_PLUGINS', 'test_foo')
5086.1.10 by Vincent Ladeuil
Fixed as per review comments.
793
        plugin.set_plugins_path(['.'])
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
794
        try:
795
            import bzrlib.plugins.test_foo
796
        except ImportError:
797
            pass
5086.1.7 by Vincent Ladeuil
Cleaner fix for bug #411413.
798
        self.assertPluginUnknown('test_foo')
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
799
5086.1.9 by Vincent Ladeuil
Fix bogus helpers and add a test.
800
    def test_regular_load(self):
801
        self.overrideAttr(plugin, '_loaded', False)
802
        plugin.load_plugins(['.'])
803
        self.assertPluginKnown('test_foo')
5131.2.1 by Martin
Permit bzrlib to run under python -OO by explictly assigning to __doc__ for user-visible docstrings
804
        self.assertDocstring("This is the doc for test_foo",
805
                             bzrlib.plugins.test_foo)
5086.1.9 by Vincent Ladeuil
Fix bogus helpers and add a test.
806
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
807
    def test_not_loaded(self):
5086.1.8 by Vincent Ladeuil
Fix warnings during autoload, add doc and a NEWS entry.
808
        self.warnings = []
809
        def captured_warning(*args, **kwargs):
810
            self.warnings.append((args, kwargs))
811
        self.overrideAttr(trace, 'warning', captured_warning)
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
812
        # Reset the flag that protect against double loading
5086.1.8 by Vincent Ladeuil
Fix warnings during autoload, add doc and a NEWS entry.
813
        self.overrideAttr(plugin, '_loaded', False)
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
814
        self.overrideEnv('BZR_DISABLE_PLUGINS', 'test_foo')
5086.5.4 by Vincent Ladeuil
Merge for fixes from 411413-plugin-path
815
        plugin.load_plugins(['.'])
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
816
        self.assertPluginUnknown('test_foo')
5086.1.8 by Vincent Ladeuil
Fix warnings during autoload, add doc and a NEWS entry.
817
        # Make sure we don't warn about the plugin ImportError since this has
818
        # been *requested* by the user.
819
        self.assertLength(0, self.warnings)
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
820
821
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
822
5268.5.1 by Vincent Ladeuil
Reproduce bug #591215.
823
class TestLoadPluginAtSyntax(tests.TestCase):
824
825
    def _get_paths(self, paths):
826
        return plugin._get_specific_plugin_paths(paths)
827
828
    def test_empty(self):
829
        self.assertEquals([], self._get_paths(None))
830
        self.assertEquals([], self._get_paths(''))
831
832
    def test_one_path(self):
833
        self.assertEquals([('b', 'man')], self._get_paths('b@man'))
834
835
    def test_bogus_path(self):
5268.5.2 by Vincent Ladeuil
Catch the wrong path descriptions in BZR_PLUGINS_AT.
836
        # We need a '@'
837
        self.assertRaises(errors.BzrCommandError, self._get_paths, 'batman')
838
        # Too much '@' isn't good either
839
        self.assertRaises(errors.BzrCommandError, self._get_paths,
840
                          'batman@mobile@cave')
841
        # An empty description probably indicates a problem
842
        self.assertRaises(errors.BzrCommandError, self._get_paths,
843
                          os.pathsep.join(['batman@cave', '', 'robin@mobile']))
5268.5.1 by Vincent Ladeuil
Reproduce bug #591215.
844
845
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
846
class TestLoadPluginAt(BaseTestPlugins):
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
847
848
    def setUp(self):
849
        super(TestLoadPluginAt, self).setUp()
850
        # Make sure we don't pollute the plugins namespace
851
        self.overrideAttr(plugins, '__path__')
852
        # Reset the flag that protect against double loading
853
        self.overrideAttr(plugin, '_loaded', False)
854
        # Create the same plugin in two directories
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
855
        self.create_plugin_package('test_foo', dir='non-standard-dir')
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
856
        # The "normal" directory, we use 'standard' instead of 'plugins' to
857
        # avoid depending on the precise naming.
858
        self.create_plugin_package('test_foo', dir='standard/test_foo')
5268.6.1 by Vincent Ladeuil
Drive-by fix of the submodule leak.
859
        # All the tests will load the 'test_foo' plugin from various locations
860
        self.addCleanup(self._unregister_plugin, 'test_foo')
5616.7.7 by Martin Pool
Paper over test global state dependency
861
        # Unfortunately there's global cached state for the specific
862
        # registered paths.
863
        self.addCleanup(plugin.PluginImporter.reset)
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
864
5086.5.14 by Vincent Ladeuil
Fix bug #552922 by controlling which files can be used to load a plugin.
865
    def assertTestFooLoadedFrom(self, path):
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
866
        self.assertPluginKnown('test_foo')
5131.2.1 by Martin
Permit bzrlib to run under python -OO by explictly assigning to __doc__ for user-visible docstrings
867
        self.assertDocstring('This is the doc for test_foo',
868
                             bzrlib.plugins.test_foo)
5086.5.14 by Vincent Ladeuil
Fix bug #552922 by controlling which files can be used to load a plugin.
869
        self.assertEqual(path, bzrlib.plugins.test_foo.dir_source)
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
870
871
    def test_regular_load(self):
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
872
        plugin.load_plugins(['standard'])
873
        self.assertTestFooLoadedFrom('standard/test_foo')
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
874
875
    def test_import(self):
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
876
        self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
877
        plugin.set_plugins_path(['standard'])
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
878
        try:
879
            import bzrlib.plugins.test_foo
880
        except ImportError:
881
            pass
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
882
        self.assertTestFooLoadedFrom('non-standard-dir')
883
884
    def test_loading(self):
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
885
        self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
886
        plugin.load_plugins(['standard'])
5086.5.9 by Vincent Ladeuil
More tests.
887
        self.assertTestFooLoadedFrom('non-standard-dir')
888
889
    def test_compiled_loaded(self):
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
890
        self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
891
        plugin.load_plugins(['standard'])
5086.5.9 by Vincent Ladeuil
More tests.
892
        self.assertTestFooLoadedFrom('non-standard-dir')
5235.1.1 by Martin
Make BZR_PLUGINS_AT tests that check filenames use a path-based assertion method rather than just string comparison
893
        self.assertIsSameRealPath('non-standard-dir/__init__.py',
894
                                  bzrlib.plugins.test_foo.__file__)
5086.5.9 by Vincent Ladeuil
More tests.
895
896
        # Try importing again now that the source has been compiled
897
        self._unregister_plugin('test_foo')
898
        plugin._loaded = False
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
899
        plugin.load_plugins(['standard'])
5086.5.9 by Vincent Ladeuil
More tests.
900
        self.assertTestFooLoadedFrom('non-standard-dir')
5086.5.11 by Vincent Ladeuil
Fix pqm failure.
901
        if __debug__:
902
            suffix = 'pyc'
903
        else:
904
            suffix = 'pyo'
5235.1.1 by Martin
Make BZR_PLUGINS_AT tests that check filenames use a path-based assertion method rather than just string comparison
905
        self.assertIsSameRealPath('non-standard-dir/__init__.%s' % suffix,
906
                                  bzrlib.plugins.test_foo.__file__)
5086.5.9 by Vincent Ladeuil
More tests.
907
908
    def test_submodule_loading(self):
909
        # We create an additional directory under the one for test_foo
910
        self.create_plugin_package('test_bar', dir='non-standard-dir/test_bar')
5268.6.1 by Vincent Ladeuil
Drive-by fix of the submodule leak.
911
        self.addCleanup(self._unregister_plugin_submodule,
912
                        'test_foo', 'test_bar')
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
913
        self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
914
        plugin.set_plugins_path(['standard'])
5086.5.9 by Vincent Ladeuil
More tests.
915
        import bzrlib.plugins.test_foo
916
        self.assertEqual('bzrlib.plugins.test_foo',
917
                         bzrlib.plugins.test_foo.__package__)
918
        import bzrlib.plugins.test_foo.test_bar
5235.1.1 by Martin
Make BZR_PLUGINS_AT tests that check filenames use a path-based assertion method rather than just string comparison
919
        self.assertIsSameRealPath('non-standard-dir/test_bar/__init__.py',
920
                                  bzrlib.plugins.test_foo.test_bar.__file__)
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
921
5268.6.2 by Vincent Ladeuil
Reproduce bug #588959.
922
    def test_relative_submodule_loading(self):
923
        self.create_plugin_package('test_foo', dir='another-dir', source='''
924
import test_bar
925
''')
926
        # We create an additional directory under the one for test_foo
927
        self.create_plugin_package('test_bar', dir='another-dir/test_bar')
928
        self.addCleanup(self._unregister_plugin_submodule,
929
                        'test_foo', 'test_bar')
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
930
        self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@another-dir')
5268.6.2 by Vincent Ladeuil
Reproduce bug #588959.
931
        plugin.set_plugins_path(['standard'])
932
        import bzrlib.plugins.test_foo
933
        self.assertEqual('bzrlib.plugins.test_foo',
934
                         bzrlib.plugins.test_foo.__package__)
935
        self.assertIsSameRealPath('another-dir/test_bar/__init__.py',
936
                                  bzrlib.plugins.test_foo.test_bar.__file__)
937
5086.5.15 by Vincent Ladeuil
Fixed as per Ian's review.
938
    def test_loading_from___init__only(self):
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
939
        # We rename the existing __init__.py file to ensure that we don't load
940
        # a random file
941
        init = 'non-standard-dir/__init__.py'
942
        random = 'non-standard-dir/setup.py'
943
        os.rename(init, random)
944
        self.addCleanup(os.rename, random, init)
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
945
        self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
946
        plugin.load_plugins(['standard'])
947
        self.assertPluginUnknown('test_foo')
5086.5.14 by Vincent Ladeuil
Fix bug #552922 by controlling which files can be used to load a plugin.
948
949
    def test_loading_from_specific_file(self):
950
        plugin_dir = 'non-standard-dir'
951
        plugin_file_name = 'iamtestfoo.py'
952
        plugin_path = osutils.pathjoin(plugin_dir, plugin_file_name)
953
        source = '''\
954
"""This is the doc for %s"""
955
dir_source = '%s'
956
''' % ('test_foo', plugin_path)
957
        self.create_plugin('test_foo', source=source,
958
                           dir=plugin_dir, file_name=plugin_file_name)
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
959
        self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@%s' % plugin_path)
5086.5.14 by Vincent Ladeuil
Fix bug #552922 by controlling which files can be used to load a plugin.
960
        plugin.load_plugins(['standard'])
961
        self.assertTestFooLoadedFrom(plugin_path)
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
962
963
964
class TestDescribePlugins(BaseTestPlugins):
965
966
    def test_describe_plugins(self):
5616.7.11 by Martin Pool
Additional tests and fixes for refactored describe_plugins.
967
        class DummyModule(object):
968
            __doc__ = 'Hi there'
969
        class DummyPlugin(object):
970
            __version__ = '0.1.0'
971
            module = DummyModule()
972
        def dummy_plugins():
973
            return { 'good': DummyPlugin() }
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
974
        self.overrideAttr(plugin, 'plugin_warnings',
975
            {'bad': ['Failed to load (just testing)']})
5616.7.11 by Martin Pool
Additional tests and fixes for refactored describe_plugins.
976
        self.overrideAttr(plugin, 'plugins', dummy_plugins)
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
977
        self.assertEquals("""\
978
bad (failed to load)
979
  ** Failed to load (just testing)
980
5616.7.11 by Martin Pool
Additional tests and fixes for refactored describe_plugins.
981
good 0.1.0
982
  Hi there
983
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
984
""", ''.join(plugin.describe_plugins()))