~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.
6437.20.1 by Wouter van Heyst
ensure files actually hit the disk under pypy
312
        with file('plugin.py', 'w') as f: f.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)
6059.3.6 by Vincent Ladeuil
Fix tests failing on pqm.
618
        self.assertEqual("two lines of help\nand more\n\n:See 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())
6059.3.6 by Vincent Ladeuil
Fix tests failing on pqm.
626
        mod = FakeModule('two lines of help\nand more',
627
                         'bzrlib.plugins.foo_bar')
2432.1.29 by Robert Collins
Add get_help_topic to ModuleHelpTopic.
628
        topic = plugin.ModuleHelpTopic(mod)
629
        self.assertEqual('foo_bar', topic.get_help_topic())
3835.2.7 by Aaron Bentley
Add tests for plugins
630
631
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
632
class TestLoadFromPath(tests.TestCaseInTempDir):
633
634
    def setUp(self):
635
        super(TestLoadFromPath, self).setUp()
636
        # 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
637
        self.overrideAttr(bzrlib.plugins, '__path__', [])
638
        self.overrideAttr(plugin, '_loaded', False)
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
639
640
        # 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
641
        self.overrideAttr(plugin, 'load_from_path', lambda dirs: None)
3835.2.7 by Aaron Bentley
Add tests for plugins
642
643
    def test_set_plugins_path_with_args(self):
644
        plugin.set_plugins_path(['a', 'b'])
645
        self.assertEqual(['a', 'b'], bzrlib.plugins.__path__)
646
647
    def test_set_plugins_path_defaults(self):
648
        plugin.set_plugins_path()
649
        self.assertEqual(plugin.get_standard_plugins_path(),
650
                         bzrlib.plugins.__path__)
651
652
    def test_get_standard_plugins_path(self):
653
        path = plugin.get_standard_plugins_path()
654
        for directory in path:
4412.2.1 by Vincent Ladeuil
Fix some OSX test regressions (well actual test bugs indeed).
655
            self.assertNotContainsRe(directory, r'\\/$')
3835.2.7 by Aaron Bentley
Add tests for plugins
656
        try:
657
            from distutils.sysconfig import get_python_lib
658
        except ImportError:
659
            pass
660
        else:
661
            if sys.platform != 'win32':
662
                python_lib = get_python_lib()
663
                for directory in path:
664
                    if directory.startswith(python_lib):
665
                        break
666
                else:
667
                    self.fail('No path to global plugins')
668
669
    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.
670
        self.overrideEnv('BZR_PLUGIN_PATH', 'foo/')
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
671
        path = plugin.get_standard_plugins_path()
672
        for directory in path:
673
            self.assertNotContainsRe(directory, r'\\/$')
3835.2.7 by Aaron Bentley
Add tests for plugins
674
675
    def test_load_plugins(self):
676
        plugin.load_plugins(['.'])
677
        self.assertEqual(bzrlib.plugins.__path__, ['.'])
678
        # subsequent loads are no-ops
679
        plugin.load_plugins(['foo'])
680
        self.assertEqual(bzrlib.plugins.__path__, ['.'])
681
682
    def test_load_plugins_default(self):
683
        plugin.load_plugins()
684
        path = plugin.get_standard_plugins_path()
685
        self.assertEqual(path, bzrlib.plugins.__path__)
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
686
687
5086.1.2 by Vincent Ladeuil
Cosmetic changes.
688
class TestEnvPluginPath(tests.TestCase):
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
689
690
    def setUp(self):
691
        super(TestEnvPluginPath, self).setUp()
4985.1.5 by Vincent Ladeuil
Deploying the new overrideAttr facility further reduces the complexity
692
        self.overrideAttr(plugin, 'DEFAULT_PLUGIN_PATH', None)
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
693
694
        self.user = plugin.get_user_plugin_path()
695
        self.site = plugin.get_site_plugin_path()
696
        self.core = plugin.get_core_plugin_path()
697
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
698
    def _list2paths(self, *args):
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
699
        paths = []
700
        for p in args:
701
            plugin._append_new_path(paths, p)
702
        return paths
703
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
704
    def _set_path(self, *args):
705
        path = os.pathsep.join(self._list2paths(*args))
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
706
        self.overrideEnv('BZR_PLUGIN_PATH', path)
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
707
708
    def check_path(self, expected_dirs, setting_dirs):
709
        if setting_dirs:
710
            self._set_path(*setting_dirs)
711
        actual = plugin.get_standard_plugins_path()
712
        self.assertEquals(self._list2paths(*expected_dirs), actual)
713
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
714
    def test_default(self):
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
715
        self.check_path([self.user, self.core, self.site],
716
                        None)
717
718
    def test_adhoc_policy(self):
719
        self.check_path([self.user, self.core, self.site],
720
                        ['+user', '+core', '+site'])
721
722
    def test_fallback_policy(self):
723
        self.check_path([self.core, self.site, self.user],
724
                        ['+core', '+site', '+user'])
725
726
    def test_override_policy(self):
727
        self.check_path([self.user, self.site, self.core],
728
                        ['+user', '+site', '+core'])
729
730
    def test_disable_user(self):
731
        self.check_path([self.core, self.site], ['-user'])
732
733
    def test_disable_user_twice(self):
734
        # Ensures multiple removals don't left cruft
735
        self.check_path([self.core, self.site], ['-user', '-user'])
736
4628.2.5 by Vincent Ladeuil
Fixes prompted by review.
737
    def test_duplicates_are_removed(self):
738
        self.check_path([self.user, self.core, self.site],
739
                        ['+user', '+user'])
740
        # And only the first reference is kept (since the later references will
5086.1.2 by Vincent Ladeuil
Cosmetic changes.
741
        # only produce '<plugin> already loaded' mutters)
4628.2.5 by Vincent Ladeuil
Fixes prompted by review.
742
        self.check_path([self.user, self.core, self.site],
743
                        ['+user', '+user', '+core',
744
                         '+user', '+site', '+site',
745
                         '+core'])
746
5086.1.5 by Vincent Ladeuil
Fix typo in test name.
747
    def test_disable_overrides_enable(self):
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
748
        self.check_path([self.core, self.site], ['-user', '+user'])
749
750
    def test_disable_core(self):
4628.2.3 by Vincent Ladeuil
Update doc and add NEWS entry.
751
        self.check_path([self.site], ['-core'])
752
        self.check_path([self.user, self.site], ['+user', '-core'])
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
753
754
    def test_disable_site(self):
4628.2.3 by Vincent Ladeuil
Update doc and add NEWS entry.
755
        self.check_path([self.core], ['-site'])
756
        self.check_path([self.user, self.core], ['-site', '+user'])
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
757
758
    def test_override_site(self):
4628.2.3 by Vincent Ladeuil
Update doc and add NEWS entry.
759
        self.check_path(['mysite', self.user, self.core],
760
                        ['mysite', '-site', '+user'])
761
        self.check_path(['mysite', self.core],
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
762
                        ['mysite', '-site'])
763
764
    def test_override_core(self):
4628.2.3 by Vincent Ladeuil
Update doc and add NEWS entry.
765
        self.check_path(['mycore', self.user, self.site],
766
                        ['mycore', '-core', '+user', '+site'])
767
        self.check_path(['mycore', self.site],
4628.2.2 by Vincent Ladeuil
Add [+-]{user|core|site} handling in BZR_PLUGIN_PATH.
768
                        ['mycore', '-core'])
769
770
    def test_my_plugin_only(self):
771
        self.check_path(['myplugin'], ['myplugin', '-user', '-core', '-site'])
772
773
    def test_my_plugin_first(self):
774
        self.check_path(['myplugin', self.core, self.site, self.user],
775
                        ['myplugin', '+core', '+site', '+user'])
4628.2.1 by Vincent Ladeuil
Start introducing accessors for plugin paths.
776
4628.2.5 by Vincent Ladeuil
Fixes prompted by review.
777
    def test_bogus_references(self):
778
        self.check_path(['+foo', '-bar', self.core, self.site],
779
                        ['+foo', '-bar'])
5086.1.4 by Vincent Ladeuil
Slight plugin tests rewriting.
780
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
781
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
782
class TestDisablePlugin(BaseTestPlugins):
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
783
5086.1.7 by Vincent Ladeuil
Cleaner fix for bug #411413.
784
    def setUp(self):
785
        super(TestDisablePlugin, self).setUp()
786
        self.create_plugin_package('test_foo')
787
        # Make sure we don't pollute the plugins namespace
788
        self.overrideAttr(plugins, '__path__')
789
        # Be paranoid in case a test fail
790
        self.addCleanup(self._unregister_plugin, 'test_foo')
5086.1.8 by Vincent Ladeuil
Fix warnings during autoload, add doc and a NEWS entry.
791
792
    def test_cannot_import(self):
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
793
        self.overrideEnv('BZR_DISABLE_PLUGINS', 'test_foo')
5086.1.10 by Vincent Ladeuil
Fixed as per review comments.
794
        plugin.set_plugins_path(['.'])
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
795
        try:
796
            import bzrlib.plugins.test_foo
797
        except ImportError:
798
            pass
5086.1.7 by Vincent Ladeuil
Cleaner fix for bug #411413.
799
        self.assertPluginUnknown('test_foo')
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
800
5086.1.9 by Vincent Ladeuil
Fix bogus helpers and add a test.
801
    def test_regular_load(self):
802
        self.overrideAttr(plugin, '_loaded', False)
803
        plugin.load_plugins(['.'])
804
        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
805
        self.assertDocstring("This is the doc for test_foo",
806
                             bzrlib.plugins.test_foo)
5086.1.9 by Vincent Ladeuil
Fix bogus helpers and add a test.
807
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
808
    def test_not_loaded(self):
5086.1.8 by Vincent Ladeuil
Fix warnings during autoload, add doc and a NEWS entry.
809
        self.warnings = []
810
        def captured_warning(*args, **kwargs):
811
            self.warnings.append((args, kwargs))
812
        self.overrideAttr(trace, 'warning', captured_warning)
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
813
        # Reset the flag that protect against double loading
5086.1.8 by Vincent Ladeuil
Fix warnings during autoload, add doc and a NEWS entry.
814
        self.overrideAttr(plugin, '_loaded', False)
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
815
        self.overrideEnv('BZR_DISABLE_PLUGINS', 'test_foo')
5086.5.4 by Vincent Ladeuil
Merge for fixes from 411413-plugin-path
816
        plugin.load_plugins(['.'])
5086.1.6 by Vincent Ladeuil
Crude fix for bug #411413.
817
        self.assertPluginUnknown('test_foo')
5086.1.8 by Vincent Ladeuil
Fix warnings during autoload, add doc and a NEWS entry.
818
        # Make sure we don't warn about the plugin ImportError since this has
819
        # been *requested* by the user.
820
        self.assertLength(0, self.warnings)
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
821
822
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
823
5268.5.1 by Vincent Ladeuil
Reproduce bug #591215.
824
class TestLoadPluginAtSyntax(tests.TestCase):
825
826
    def _get_paths(self, paths):
827
        return plugin._get_specific_plugin_paths(paths)
828
829
    def test_empty(self):
830
        self.assertEquals([], self._get_paths(None))
831
        self.assertEquals([], self._get_paths(''))
832
833
    def test_one_path(self):
834
        self.assertEquals([('b', 'man')], self._get_paths('b@man'))
835
836
    def test_bogus_path(self):
5268.5.2 by Vincent Ladeuil
Catch the wrong path descriptions in BZR_PLUGINS_AT.
837
        # We need a '@'
838
        self.assertRaises(errors.BzrCommandError, self._get_paths, 'batman')
839
        # Too much '@' isn't good either
840
        self.assertRaises(errors.BzrCommandError, self._get_paths,
841
                          'batman@mobile@cave')
842
        # An empty description probably indicates a problem
843
        self.assertRaises(errors.BzrCommandError, self._get_paths,
844
                          os.pathsep.join(['batman@cave', '', 'robin@mobile']))
5268.5.1 by Vincent Ladeuil
Reproduce bug #591215.
845
846
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
847
class TestLoadPluginAt(BaseTestPlugins):
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
848
849
    def setUp(self):
850
        super(TestLoadPluginAt, self).setUp()
851
        # Make sure we don't pollute the plugins namespace
852
        self.overrideAttr(plugins, '__path__')
853
        # Reset the flag that protect against double loading
854
        self.overrideAttr(plugin, '_loaded', False)
855
        # Create the same plugin in two directories
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
856
        self.create_plugin_package('test_foo', dir='non-standard-dir')
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
857
        # The "normal" directory, we use 'standard' instead of 'plugins' to
858
        # avoid depending on the precise naming.
859
        self.create_plugin_package('test_foo', dir='standard/test_foo')
5268.6.1 by Vincent Ladeuil
Drive-by fix of the submodule leak.
860
        # All the tests will load the 'test_foo' plugin from various locations
861
        self.addCleanup(self._unregister_plugin, 'test_foo')
5616.7.7 by Martin Pool
Paper over test global state dependency
862
        # Unfortunately there's global cached state for the specific
863
        # registered paths.
864
        self.addCleanup(plugin.PluginImporter.reset)
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
865
5086.5.14 by Vincent Ladeuil
Fix bug #552922 by controlling which files can be used to load a plugin.
866
    def assertTestFooLoadedFrom(self, path):
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
867
        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
868
        self.assertDocstring('This is the doc for test_foo',
869
                             bzrlib.plugins.test_foo)
5086.5.14 by Vincent Ladeuil
Fix bug #552922 by controlling which files can be used to load a plugin.
870
        self.assertEqual(path, bzrlib.plugins.test_foo.dir_source)
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
871
872
    def test_regular_load(self):
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
873
        plugin.load_plugins(['standard'])
874
        self.assertTestFooLoadedFrom('standard/test_foo')
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
875
876
    def test_import(self):
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
877
        self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
878
        plugin.set_plugins_path(['standard'])
5086.5.3 by Vincent Ladeuil
First shot at loading plugins from a specific directory.
879
        try:
880
            import bzrlib.plugins.test_foo
881
        except ImportError:
882
            pass
5086.5.8 by Vincent Ladeuil
Make sure we can load from a non-standard directory name.
883
        self.assertTestFooLoadedFrom('non-standard-dir')
884
885
    def test_loading(self):
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
886
        self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
887
        plugin.load_plugins(['standard'])
5086.5.9 by Vincent Ladeuil
More tests.
888
        self.assertTestFooLoadedFrom('non-standard-dir')
889
890
    def test_compiled_loaded(self):
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
891
        self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
892
        plugin.load_plugins(['standard'])
5086.5.9 by Vincent Ladeuil
More tests.
893
        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
894
        self.assertIsSameRealPath('non-standard-dir/__init__.py',
895
                                  bzrlib.plugins.test_foo.__file__)
5086.5.9 by Vincent Ladeuil
More tests.
896
897
        # Try importing again now that the source has been compiled
898
        self._unregister_plugin('test_foo')
899
        plugin._loaded = False
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
900
        plugin.load_plugins(['standard'])
5086.5.9 by Vincent Ladeuil
More tests.
901
        self.assertTestFooLoadedFrom('non-standard-dir')
5086.5.11 by Vincent Ladeuil
Fix pqm failure.
902
        if __debug__:
903
            suffix = 'pyc'
904
        else:
905
            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
906
        self.assertIsSameRealPath('non-standard-dir/__init__.%s' % suffix,
907
                                  bzrlib.plugins.test_foo.__file__)
5086.5.9 by Vincent Ladeuil
More tests.
908
909
    def test_submodule_loading(self):
910
        # We create an additional directory under the one for test_foo
911
        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.
912
        self.addCleanup(self._unregister_plugin_submodule,
913
                        'test_foo', 'test_bar')
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
914
        self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
915
        plugin.set_plugins_path(['standard'])
5086.5.9 by Vincent Ladeuil
More tests.
916
        import bzrlib.plugins.test_foo
917
        self.assertEqual('bzrlib.plugins.test_foo',
918
                         bzrlib.plugins.test_foo.__package__)
919
        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
920
        self.assertIsSameRealPath('non-standard-dir/test_bar/__init__.py',
921
                                  bzrlib.plugins.test_foo.test_bar.__file__)
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
922
5268.6.2 by Vincent Ladeuil
Reproduce bug #588959.
923
    def test_relative_submodule_loading(self):
924
        self.create_plugin_package('test_foo', dir='another-dir', source='''
925
import test_bar
926
''')
927
        # We create an additional directory under the one for test_foo
928
        self.create_plugin_package('test_bar', dir='another-dir/test_bar')
929
        self.addCleanup(self._unregister_plugin_submodule,
930
                        'test_foo', 'test_bar')
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
931
        self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@another-dir')
5268.6.2 by Vincent Ladeuil
Reproduce bug #588959.
932
        plugin.set_plugins_path(['standard'])
933
        import bzrlib.plugins.test_foo
934
        self.assertEqual('bzrlib.plugins.test_foo',
935
                         bzrlib.plugins.test_foo.__package__)
936
        self.assertIsSameRealPath('another-dir/test_bar/__init__.py',
937
                                  bzrlib.plugins.test_foo.test_bar.__file__)
938
5086.5.15 by Vincent Ladeuil
Fixed as per Ian's review.
939
    def test_loading_from___init__only(self):
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
940
        # We rename the existing __init__.py file to ensure that we don't load
941
        # a random file
942
        init = 'non-standard-dir/__init__.py'
943
        random = 'non-standard-dir/setup.py'
944
        os.rename(init, random)
945
        self.addCleanup(os.rename, random, init)
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
946
        self.overrideEnv('BZR_PLUGINS_AT', 'test_foo@non-standard-dir')
5086.5.13 by Vincent Ladeuil
Reproduce bug #552922.
947
        plugin.load_plugins(['standard'])
948
        self.assertPluginUnknown('test_foo')
5086.5.14 by Vincent Ladeuil
Fix bug #552922 by controlling which files can be used to load a plugin.
949
950
    def test_loading_from_specific_file(self):
951
        plugin_dir = 'non-standard-dir'
952
        plugin_file_name = 'iamtestfoo.py'
953
        plugin_path = osutils.pathjoin(plugin_dir, plugin_file_name)
954
        source = '''\
955
"""This is the doc for %s"""
956
dir_source = '%s'
957
''' % ('test_foo', plugin_path)
958
        self.create_plugin('test_foo', source=source,
959
                           dir=plugin_dir, file_name=plugin_file_name)
5570.3.12 by Vincent Ladeuil
Replace osutils.set_or_unset_env calls with self.overrideEnv.
960
        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.
961
        plugin.load_plugins(['standard'])
962
        self.assertTestFooLoadedFrom(plugin_path)
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
963
964
965
class TestDescribePlugins(BaseTestPlugins):
966
967
    def test_describe_plugins(self):
5616.7.11 by Martin Pool
Additional tests and fixes for refactored describe_plugins.
968
        class DummyModule(object):
969
            __doc__ = 'Hi there'
970
        class DummyPlugin(object):
971
            __version__ = '0.1.0'
972
            module = DummyModule()
973
        def dummy_plugins():
974
            return { 'good': DummyPlugin() }
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
975
        self.overrideAttr(plugin, 'plugin_warnings',
976
            {'bad': ['Failed to load (just testing)']})
5616.7.11 by Martin Pool
Additional tests and fixes for refactored describe_plugins.
977
        self.overrideAttr(plugin, 'plugins', dummy_plugins)
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
978
        self.assertEquals("""\
979
bad (failed to load)
980
  ** Failed to load (just testing)
981
5616.7.11 by Martin Pool
Additional tests and fixes for refactored describe_plugins.
982
good 0.1.0
983
  Hi there
984
5616.7.10 by Martin Pool
Clean up describe_plugins to sort loaded and unloaded plugins together.
985
""", ''.join(plugin.describe_plugins()))