1
# Copyright (C) 2005, 2007 Canonical Ltd
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.
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.
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
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Tests for 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
25
from StringIO import StringIO
29
from bzrlib import plugin, tests
32
import bzrlib.commands
34
from bzrlib.symbol_versioning import zero_ninetyone
35
from bzrlib.tests import TestCase, TestCaseInTempDir
36
from bzrlib.osutils import pathjoin, abspath, normpath
40
import bzrlib.commands
41
class cmd_myplug(bzrlib.commands.Command):
42
'''Just a simple test plugin.'''
45
print 'Hello from my plugin'
48
# TODO: Write a test for plugin decoration of commands.
50
class TestLoadingPlugins(TestCaseInTempDir):
54
def test_plugins_with_the_same_name_are_not_loaded(self):
55
# This test tests that having two plugins in different directories does
56
# not result in both being loaded when they have the same name. get a
57
# file name we can use which is also a valid attribute for accessing in
58
# activeattributes. - we cannot give import parameters.
60
self.failIf(tempattribute in self.activeattributes)
61
# set a place for the plugins to record their loading, and at the same
62
# time validate that the location the plugins should record to is
64
bzrlib.tests.test_plugins.TestLoadingPlugins.activeattributes \
66
self.failUnless(tempattribute in self.activeattributes)
67
# create two plugin directories
70
# write a plugin that will record when its loaded in the
72
template = ("from bzrlib.tests.test_plugins import TestLoadingPlugins\n"
73
"TestLoadingPlugins.activeattributes[%r].append('%s')\n")
75
outfile = open(os.path.join('first', 'plugin.py'), 'w')
77
outfile.write(template % (tempattribute, 'first'))
82
outfile = open(os.path.join('second', 'plugin.py'), 'w')
84
outfile.write(template % (tempattribute, 'second'))
90
bzrlib.plugin.load_from_path(['first', 'second'])
91
self.assertEqual(['first'], self.activeattributes[tempattribute])
93
# remove the plugin 'plugin'
94
del self.activeattributes[tempattribute]
95
if 'bzrlib.plugins.plugin' in sys.modules:
96
del sys.modules['bzrlib.plugins.plugin']
97
if getattr(bzrlib.plugins, 'plugin', None):
98
del bzrlib.plugins.plugin
99
self.failIf(getattr(bzrlib.plugins, 'plugin', None))
101
def test_plugins_from_different_dirs_can_demand_load(self):
102
# This test tests that having two plugins in different
103
# directories with different names allows them both to be loaded, when
104
# we do a direct import statement.
105
# Determine a file name we can use which is also a valid attribute
106
# for accessing in activeattributes. - we cannot give import parameters.
107
tempattribute = "different-dirs"
108
self.failIf(tempattribute in self.activeattributes)
109
# set a place for the plugins to record their loading, and at the same
110
# time validate that the location the plugins should record to is
112
bzrlib.tests.test_plugins.TestLoadingPlugins.activeattributes \
114
self.failUnless(tempattribute in self.activeattributes)
115
# create two plugin directories
118
# write plugins that will record when they are loaded in the
119
# tempattribute list.
120
template = ("from bzrlib.tests.test_plugins import TestLoadingPlugins\n"
121
"TestLoadingPlugins.activeattributes[%r].append('%s')\n")
123
outfile = open(os.path.join('first', 'pluginone.py'), 'w')
125
outfile.write(template % (tempattribute, 'first'))
130
outfile = open(os.path.join('second', 'plugintwo.py'), 'w')
132
outfile.write(template % (tempattribute, 'second'))
137
oldpath = bzrlib.plugins.__path__
139
bzrlib.plugins.__path__ = ['first', 'second']
140
exec "import bzrlib.plugins.pluginone"
141
self.assertEqual(['first'], self.activeattributes[tempattribute])
142
exec "import bzrlib.plugins.plugintwo"
143
self.assertEqual(['first', 'second'],
144
self.activeattributes[tempattribute])
146
# remove the plugin 'plugin'
147
del self.activeattributes[tempattribute]
148
if getattr(bzrlib.plugins, 'pluginone', None):
149
del bzrlib.plugins.pluginone
150
if getattr(bzrlib.plugins, 'plugintwo', None):
151
del bzrlib.plugins.plugintwo
152
self.failIf(getattr(bzrlib.plugins, 'pluginone', None))
153
self.failIf(getattr(bzrlib.plugins, 'plugintwo', None))
155
def test_plugins_can_load_from_directory_with_trailing_slash(self):
156
# This test tests that a plugin can load from a directory when the
157
# directory in the path has a trailing slash.
158
# check the plugin is not loaded already
159
self.failIf(getattr(bzrlib.plugins, 'ts_plugin', None))
160
tempattribute = "trailing-slash"
161
self.failIf(tempattribute in self.activeattributes)
162
# set a place for the plugin to record its loading, and at the same
163
# time validate that the location the plugin should record to is
165
bzrlib.tests.test_plugins.TestLoadingPlugins.activeattributes \
167
self.failUnless(tempattribute in self.activeattributes)
168
# create a directory for the plugin
169
os.mkdir('plugin_test')
170
# write a plugin that will record when its loaded in the
171
# tempattribute list.
172
template = ("from bzrlib.tests.test_plugins import TestLoadingPlugins\n"
173
"TestLoadingPlugins.activeattributes[%r].append('%s')\n")
175
outfile = open(os.path.join('plugin_test', 'ts_plugin.py'), 'w')
177
outfile.write(template % (tempattribute, 'plugin'))
183
bzrlib.plugin.load_from_path(['plugin_test'+os.sep])
184
self.assertEqual(['plugin'], self.activeattributes[tempattribute])
186
# remove the plugin 'plugin'
187
del self.activeattributes[tempattribute]
188
if getattr(bzrlib.plugins, 'ts_plugin', None):
189
del bzrlib.plugins.ts_plugin
190
self.failIf(getattr(bzrlib.plugins, 'ts_plugin', None))
192
def test_plugin_with_bad_name_does_not_load(self):
193
# Create badly-named plugin
194
file('bad plugin-name..py', 'w').close()
198
handler = logging.StreamHandler(stream)
199
log = logging.getLogger('bzr')
200
log.addHandler(handler)
202
bzrlib.plugin.load_from_dir('.')
204
# Stop capturing output
207
log.removeHandler(handler)
209
self.assertContainsRe(stream.getvalue(),
210
r"Unable to load 'bad plugin-name\.' in '\.' as a plugin because"
211
" file path isn't a valid module name; try renaming it to"
212
" 'bad_plugin_name_'\.")
217
class TestAllPlugins(TestCaseInTempDir):
219
def test_plugin_appears_in_all_plugins(self):
220
# This test tests a new plugin appears in bzrlib.plugin.all_plugins().
221
# check the plugin is not loaded already
222
self.failIf(getattr(bzrlib.plugins, 'plugin', None))
223
# write a plugin that _cannot_ fail to load.
224
file('plugin.py', 'w').write("\n")
226
bzrlib.plugin.load_from_path(['.'])
227
all_plugins = self.applyDeprecated(zero_ninetyone,
228
bzrlib.plugin.all_plugins)
229
self.failUnless('plugin' in all_plugins)
230
self.failUnless(getattr(bzrlib.plugins, 'plugin', None))
231
self.assertEqual(all_plugins['plugin'], bzrlib.plugins.plugin)
233
# remove the plugin 'plugin'
234
if 'bzrlib.plugins.plugin' in sys.modules:
235
del sys.modules['bzrlib.plugins.plugin']
236
if getattr(bzrlib.plugins, 'plugin', None):
237
del bzrlib.plugins.plugin
238
self.failIf(getattr(bzrlib.plugins, 'plugin', None))
241
class TestPlugins(TestCaseInTempDir):
243
def setup_plugin(self, source=""):
244
# This test tests a new plugin appears in bzrlib.plugin.plugins().
245
# check the plugin is not loaded already
246
self.failIf(getattr(bzrlib.plugins, 'plugin', None))
247
# write a plugin that _cannot_ fail to load.
248
file('plugin.py', 'w').write(source + '\n')
249
self.addCleanup(self.teardown_plugin)
250
bzrlib.plugin.load_from_path(['.'])
252
def teardown_plugin(self):
253
# remove the plugin 'plugin'
254
if 'bzrlib.plugins.plugin' in sys.modules:
255
del sys.modules['bzrlib.plugins.plugin']
256
if getattr(bzrlib.plugins, 'plugin', None):
257
del bzrlib.plugins.plugin
258
self.failIf(getattr(bzrlib.plugins, 'plugin', None))
260
def test_plugin_appears_in_plugins(self):
262
self.failUnless('plugin' in bzrlib.plugin.plugins())
263
self.failUnless(getattr(bzrlib.plugins, 'plugin', None))
264
plugins = bzrlib.plugin.plugins()
265
plugin = plugins['plugin']
266
self.assertIsInstance(plugin, bzrlib.plugin.PlugIn)
267
self.assertEqual(bzrlib.plugins.plugin, plugin.module)
269
def test_trivial_plugin_get_path(self):
271
plugins = bzrlib.plugin.plugins()
272
plugin = plugins['plugin']
273
plugin_path = self.test_dir + '/plugin.py'
274
self.assertIsSameRealPath(plugin_path, normpath(plugin.path()))
276
def test_no_test_suite_gives_None_for_test_suite(self):
278
plugin = bzrlib.plugin.plugins()['plugin']
279
self.assertEqual(None, plugin.test_suite())
281
def test_test_suite_gives_test_suite_result(self):
282
source = """def test_suite(): return 'foo'"""
283
self.setup_plugin(source)
284
plugin = bzrlib.plugin.plugins()['plugin']
285
self.assertEqual('foo', plugin.test_suite())
287
def test_no_version_info(self):
289
plugin = bzrlib.plugin.plugins()['plugin']
290
self.assertEqual(None, plugin.version_info())
292
def test_with_version_info(self):
293
self.setup_plugin("version_info = (1, 2, 3, 'dev', 4)")
294
plugin = bzrlib.plugin.plugins()['plugin']
295
self.assertEqual((1, 2, 3, 'dev', 4), plugin.version_info())
297
def test_short_version_info_gets_padded(self):
298
# the gtk plugin has version_info = (1,2,3) rather than the 5-tuple.
300
self.setup_plugin("version_info = (1, 2, 3)")
301
plugin = bzrlib.plugin.plugins()['plugin']
302
self.assertEqual((1, 2, 3, 'final', 0), plugin.version_info())
304
def test_no_version_info___version__(self):
306
plugin = bzrlib.plugin.plugins()['plugin']
307
self.assertEqual("unknown", plugin.__version__)
309
def test___version__with_version_info(self):
310
self.setup_plugin("version_info = (1, 2, 3, 'dev', 4)")
311
plugin = bzrlib.plugin.plugins()['plugin']
312
self.assertEqual("1.2.3dev4", plugin.__version__)
314
def test_final__version__with_version_info(self):
315
self.setup_plugin("version_info = (1, 2, 3, 'final', 4)")
316
plugin = bzrlib.plugin.plugins()['plugin']
317
self.assertEqual("1.2.3", plugin.__version__)
320
class TestPluginHelp(TestCaseInTempDir):
322
def split_help_commands(self):
325
for line in self.run_bzr('help commands')[0].splitlines():
326
if not line.startswith(' '):
327
current = line.split()[0]
328
help[current] = help.get(current, '') + line
332
def test_plugin_help_builtins_unaffected(self):
333
# Check we don't get false positives
334
help_commands = self.split_help_commands()
335
for cmd_name in bzrlib.commands.builtin_command_names():
336
if cmd_name in bzrlib.commands.plugin_command_names():
339
help = bzrlib.commands.get_cmd_object(cmd_name).get_help_text()
340
except NotImplementedError:
341
# some commands have no help
344
self.assertNotContainsRe(help, 'plugin "[^"]*"')
346
if cmd_name in help_commands.keys():
347
# some commands are hidden
348
help = help_commands[cmd_name]
349
self.assertNotContainsRe(help, 'plugin "[^"]*"')
351
def test_plugin_help_shows_plugin(self):
352
# Create a test plugin
353
os.mkdir('plugin_test')
354
f = open(pathjoin('plugin_test', 'myplug.py'), 'w')
360
bzrlib.plugin.load_from_path(['plugin_test'])
361
bzrlib.commands.register_command( bzrlib.plugins.myplug.cmd_myplug)
362
help = self.run_bzr('help myplug')[0]
363
self.assertContainsRe(help, 'plugin "myplug"')
364
help = self.split_help_commands()['myplug']
365
self.assertContainsRe(help, '\[myplug\]')
368
if bzrlib.commands.plugin_cmds.get('myplug', None):
369
del bzrlib.commands.plugin_cmds['myplug']
370
# remove the plugin 'myplug'
371
if getattr(bzrlib.plugins, 'myplug', None):
372
delattr(bzrlib.plugins, 'myplug')
375
class TestPluginFromZip(TestCaseInTempDir):
377
def make_zipped_plugin(self, zip_name, filename):
378
z = zipfile.ZipFile(zip_name, 'w')
379
z.writestr(filename, PLUGIN_TEXT)
382
def check_plugin_load(self, zip_name, plugin_name):
383
self.assertFalse(plugin_name in dir(bzrlib.plugins),
384
'Plugin already loaded')
385
old_path = bzrlib.plugins.__path__
387
# this is normally done by load_plugins -> set_plugins_path
388
bzrlib.plugins.__path__ = [zip_name]
389
bzrlib.plugin.load_from_zip(zip_name)
390
self.assertTrue(plugin_name in dir(bzrlib.plugins),
391
'Plugin is not loaded')
394
if getattr(bzrlib.plugins, plugin_name, None):
395
delattr(bzrlib.plugins, plugin_name)
396
del sys.modules['bzrlib.plugins.' + plugin_name]
397
bzrlib.plugins.__path__ = old_path
399
def test_load_module(self):
400
self.make_zipped_plugin('./test.zip', 'ziplug.py')
401
self.check_plugin_load('./test.zip', 'ziplug')
403
def test_load_package(self):
404
self.make_zipped_plugin('./test.zip', 'ziplug/__init__.py')
405
self.check_plugin_load('./test.zip', 'ziplug')
408
class TestSetPluginsPath(TestCase):
410
def test_set_plugins_path(self):
411
"""set_plugins_path should set the module __path__ correctly."""
412
old_path = bzrlib.plugins.__path__
414
bzrlib.plugins.__path__ = []
415
expected_path = bzrlib.plugin.set_plugins_path()
416
self.assertEqual(expected_path, bzrlib.plugins.__path__)
418
bzrlib.plugins.__path__ = old_path
420
def test_set_plugins_path_with_trailing_slashes(self):
421
"""set_plugins_path should set the module __path__ based on
423
old_path = bzrlib.plugins.__path__
424
old_env = os.environ.get('BZR_PLUGIN_PATH')
426
bzrlib.plugins.__path__ = []
427
os.environ['BZR_PLUGIN_PATH'] = "first\\//\\" + os.pathsep + \
429
bzrlib.plugin.set_plugins_path()
430
expected_path = ['first', 'second',
431
os.path.dirname(bzrlib.plugins.__file__)]
432
self.assertEqual(expected_path, bzrlib.plugins.__path__)
434
bzrlib.plugins.__path__ = old_path
436
os.environ['BZR_PLUGIN_PATH'] = old_env
438
del os.environ['BZR_PLUGIN_PATH']
440
class TestHelpIndex(tests.TestCase):
441
"""Tests for the PluginsHelpIndex class."""
443
def test_default_constructable(self):
444
index = plugin.PluginsHelpIndex()
446
def test_get_topics_None(self):
447
"""Searching for None returns an empty list."""
448
index = plugin.PluginsHelpIndex()
449
self.assertEqual([], index.get_topics(None))
451
def test_get_topics_for_plugin(self):
452
"""Searching for plugin name gets its docstring."""
453
index = plugin.PluginsHelpIndex()
454
# make a new plugin here for this test, even if we're run with
456
self.assertFalse(sys.modules.has_key('bzrlib.plugins.demo_module'))
457
demo_module = FakeModule('', 'bzrlib.plugins.demo_module')
458
sys.modules['bzrlib.plugins.demo_module'] = demo_module
460
topics = index.get_topics('demo_module')
461
self.assertEqual(1, len(topics))
462
self.assertIsInstance(topics[0], plugin.ModuleHelpTopic)
463
self.assertEqual(demo_module, topics[0].module)
465
del sys.modules['bzrlib.plugins.demo_module']
467
def test_get_topics_no_topic(self):
468
"""Searching for something that is not a plugin returns []."""
469
# test this by using a name that cannot be a plugin - its not
470
# a valid python identifier.
471
index = plugin.PluginsHelpIndex()
472
self.assertEqual([], index.get_topics('nothing by this name'))
474
def test_prefix(self):
475
"""PluginsHelpIndex has a prefix of 'plugins/'."""
476
index = plugin.PluginsHelpIndex()
477
self.assertEqual('plugins/', index.prefix)
479
def test_get_plugin_topic_with_prefix(self):
480
"""Searching for plugins/demo_module returns help."""
481
index = plugin.PluginsHelpIndex()
482
self.assertFalse(sys.modules.has_key('bzrlib.plugins.demo_module'))
483
demo_module = FakeModule('', 'bzrlib.plugins.demo_module')
484
sys.modules['bzrlib.plugins.demo_module'] = demo_module
486
topics = index.get_topics('plugins/demo_module')
487
self.assertEqual(1, len(topics))
488
self.assertIsInstance(topics[0], plugin.ModuleHelpTopic)
489
self.assertEqual(demo_module, topics[0].module)
491
del sys.modules['bzrlib.plugins.demo_module']
494
class FakeModule(object):
495
"""A fake module to test with."""
497
def __init__(self, doc, name):
502
class TestModuleHelpTopic(tests.TestCase):
503
"""Tests for the ModuleHelpTopic class."""
505
def test_contruct(self):
506
"""Construction takes the module to document."""
507
mod = FakeModule('foo', 'foo')
508
topic = plugin.ModuleHelpTopic(mod)
509
self.assertEqual(mod, topic.module)
511
def test_get_help_text_None(self):
512
"""A ModuleHelpTopic returns the docstring for get_help_text."""
513
mod = FakeModule(None, 'demo')
514
topic = plugin.ModuleHelpTopic(mod)
515
self.assertEqual("Plugin 'demo' has no docstring.\n",
516
topic.get_help_text())
518
def test_get_help_text_no_carriage_return(self):
519
"""ModuleHelpTopic.get_help_text adds a \n if needed."""
520
mod = FakeModule('one line of help', 'demo')
521
topic = plugin.ModuleHelpTopic(mod)
522
self.assertEqual("one line of help\n",
523
topic.get_help_text())
525
def test_get_help_text_carriage_return(self):
526
"""ModuleHelpTopic.get_help_text adds a \n if needed."""
527
mod = FakeModule('two lines of help\nand more\n', 'demo')
528
topic = plugin.ModuleHelpTopic(mod)
529
self.assertEqual("two lines of help\nand more\n",
530
topic.get_help_text())
532
def test_get_help_text_with_additional_see_also(self):
533
mod = FakeModule('two lines of help\nand more', 'demo')
534
topic = plugin.ModuleHelpTopic(mod)
535
self.assertEqual("two lines of help\nand more\nSee also: bar, foo\n",
536
topic.get_help_text(['foo', 'bar']))
538
def test_get_help_topic(self):
539
"""The help topic for a plugin is its module name."""
540
mod = FakeModule('two lines of help\nand more', 'bzrlib.plugins.demo')
541
topic = plugin.ModuleHelpTopic(mod)
542
self.assertEqual('demo', topic.get_help_topic())
543
mod = FakeModule('two lines of help\nand more', 'bzrlib.plugins.foo_bar')
544
topic = plugin.ModuleHelpTopic(mod)
545
self.assertEqual('foo_bar', topic.get_help_topic())