1# Copyright (C) 2005-2012, 2016 Canonical Ltd 2# 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. 7# 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. 12# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 16 17import errno 18import inspect 19import sys 20 21from .. import ( 22 builtins, 23 commands, 24 config, 25 errors, 26 option, 27 tests, 28 trace, 29 ) 30from ..commands import display_command 31from . import TestSkipped 32 33 34class TestCommands(tests.TestCase): 35 36 def test_all_commands_have_help(self): 37 commands._register_builtin_commands() 38 commands_without_help = set() 39 base_doc = inspect.getdoc(commands.Command) 40 for cmd_name in commands.all_command_names(): 41 cmd = commands.get_cmd_object(cmd_name) 42 cmd_help = cmd.help() 43 if not cmd_help or cmd_help == base_doc: 44 commands_without_help.append(cmd_name) 45 self.assertLength(0, commands_without_help) 46 47 def test_display_command(self): 48 """EPIPE message is selectively suppressed""" 49 def pipe_thrower(): 50 raise IOError(errno.EPIPE, "Bogus pipe error") 51 self.assertRaises(IOError, pipe_thrower) 52 53 @display_command 54 def non_thrower(): 55 pipe_thrower() 56 non_thrower() 57 58 @display_command 59 def other_thrower(): 60 raise IOError(errno.ESPIPE, "Bogus pipe error") 61 self.assertRaises(IOError, other_thrower) 62 63 def test_unicode_command(self): 64 # This error is thrown when we can't find the command in the 65 # list of available commands 66 self.assertRaises(errors.CommandError, 67 commands.run_bzr, [u'cmd\xb5']) 68 69 def test_unicode_option(self): 70 # This error is actually thrown by optparse, when it 71 # can't find the given option 72 import optparse 73 if optparse.__version__ == "1.5.3": 74 raise TestSkipped("optparse 1.5.3 can't handle unicode options") 75 self.assertRaises(errors.CommandError, 76 commands.run_bzr, ['log', u'--option\xb5']) 77 78 @staticmethod 79 def get_command(options): 80 class cmd_foo(commands.Command): 81 __doc__ = 'Bar' 82 83 takes_options = options 84 85 return cmd_foo() 86 87 def test_help_hidden(self): 88 c = self.get_command([option.Option('foo', hidden=True)]) 89 self.assertNotContainsRe(c.get_help_text(), '--foo') 90 91 def test_help_not_hidden(self): 92 c = self.get_command([option.Option('foo', hidden=False)]) 93 self.assertContainsRe(c.get_help_text(), '--foo') 94 95 96class TestInsideCommand(tests.TestCaseInTempDir): 97 98 def test_command_see_config_overrides(self): 99 def run(cmd): 100 # We override the run() command method so we can observe the 101 # overrides from inside. 102 c = config.GlobalStack() 103 self.assertEqual('12', c.get('xx')) 104 self.assertEqual('foo', c.get('yy')) 105 self.overrideAttr(builtins.cmd_rocks, 'run', run) 106 self.run_bzr(['rocks', '-Oxx=12', '-Oyy=foo']) 107 c = config.GlobalStack() 108 # Ensure that we don't leak outside of the command 109 self.assertEqual(None, c.get('xx')) 110 self.assertEqual(None, c.get('yy')) 111 112 113class TestInvokedAs(tests.TestCase): 114 115 def test_invoked_as(self): 116 """The command object knows the actual name used to invoke it.""" 117 commands.install_bzr_command_hooks() 118 commands._register_builtin_commands() 119 # get one from the real get_cmd_object. 120 c = commands.get_cmd_object('ci') 121 self.assertIsInstance(c, builtins.cmd_commit) 122 self.assertEqual(c.invoked_as, 'ci') 123 124 125class TestGetAlias(tests.TestCase): 126 127 def _get_config(self, config_text): 128 my_config = config.GlobalConfig.from_string(config_text) 129 return my_config 130 131 def test_simple(self): 132 my_config = self._get_config("[ALIASES]\n" 133 "diff=diff -r -2..-1\n") 134 self.assertEqual([u'diff', u'-r', u'-2..-1'], 135 commands.get_alias("diff", config=my_config)) 136 137 def test_single_quotes(self): 138 my_config = self._get_config("[ALIASES]\n" 139 "diff=diff -r -2..-1 --diff-options " 140 "'--strip-trailing-cr -wp'\n") 141 self.assertEqual([u'diff', u'-r', u'-2..-1', u'--diff-options', 142 u'--strip-trailing-cr -wp'], 143 commands.get_alias("diff", config=my_config)) 144 145 def test_double_quotes(self): 146 my_config = self._get_config("[ALIASES]\n" 147 "diff=diff -r -2..-1 --diff-options " 148 "\"--strip-trailing-cr -wp\"\n") 149 self.assertEqual([u'diff', u'-r', u'-2..-1', u'--diff-options', 150 u'--strip-trailing-cr -wp'], 151 commands.get_alias("diff", config=my_config)) 152 153 def test_unicode(self): 154 my_config = self._get_config("[ALIASES]\n" 155 u'iam=whoami "Erik B\u00e5gfors <erik@bagfors.nu>"\n') 156 self.assertEqual([u'whoami', u'Erik B\u00e5gfors <erik@bagfors.nu>'], 157 commands.get_alias("iam", config=my_config)) 158 159 160class TestSeeAlso(tests.TestCase): 161 """Tests for the see also functional of Command.""" 162 163 @staticmethod 164 def _get_command_with_see_also(see_also): 165 class ACommand(commands.Command): 166 __doc__ = """A sample command.""" 167 _see_also = see_also 168 return ACommand() 169 170 def test_default_subclass_no_see_also(self): 171 command = self._get_command_with_see_also([]) 172 self.assertEqual([], command.get_see_also()) 173 174 def test__see_also(self): 175 """When _see_also is defined, it sets the result of get_see_also().""" 176 command = self._get_command_with_see_also(['bar', 'foo']) 177 self.assertEqual(['bar', 'foo'], command.get_see_also()) 178 179 def test_deduplication(self): 180 """Duplicates in _see_also are stripped out.""" 181 command = self._get_command_with_see_also(['foo', 'foo']) 182 self.assertEqual(['foo'], command.get_see_also()) 183 184 def test_sorted(self): 185 """_see_also is sorted by get_see_also.""" 186 command = self._get_command_with_see_also(['foo', 'bar']) 187 self.assertEqual(['bar', 'foo'], command.get_see_also()) 188 189 def test_additional_terms(self): 190 """Additional terms can be supplied and are deduped and sorted.""" 191 command = self._get_command_with_see_also(['foo', 'bar']) 192 self.assertEqual(['bar', 'foo', 'gam'], 193 command.get_see_also(['gam', 'bar', 'gam'])) 194 195 196class TestRegisterLazy(tests.TestCase): 197 198 def setUp(self): 199 super(TestRegisterLazy, self).setUp() 200 import breezy.tests.fake_command 201 del sys.modules['breezy.tests.fake_command'] 202 global lazy_command_imported 203 lazy_command_imported = False 204 commands.install_bzr_command_hooks() 205 206 @staticmethod 207 def remove_fake(): 208 commands.plugin_cmds.remove('fake') 209 210 def assertIsFakeCommand(self, cmd_obj): 211 from breezy.tests.fake_command import cmd_fake 212 self.assertIsInstance(cmd_obj, cmd_fake) 213 214 def test_register_lazy(self): 215 """Ensure lazy registration works""" 216 commands.plugin_cmds.register_lazy('cmd_fake', [], 217 'breezy.tests.fake_command') 218 self.addCleanup(self.remove_fake) 219 self.assertFalse(lazy_command_imported) 220 fake_instance = commands.get_cmd_object('fake') 221 self.assertTrue(lazy_command_imported) 222 self.assertIsFakeCommand(fake_instance) 223 224 def test_get_unrelated_does_not_import(self): 225 commands.plugin_cmds.register_lazy('cmd_fake', [], 226 'breezy.tests.fake_command') 227 self.addCleanup(self.remove_fake) 228 commands.get_cmd_object('status') 229 self.assertFalse(lazy_command_imported) 230 231 def test_aliases(self): 232 commands.plugin_cmds.register_lazy('cmd_fake', ['fake_alias'], 233 'breezy.tests.fake_command') 234 self.addCleanup(self.remove_fake) 235 fake_instance = commands.get_cmd_object('fake_alias') 236 self.assertIsFakeCommand(fake_instance) 237 238 239class TestExtendCommandHook(tests.TestCase): 240 241 def test_fires_on_get_cmd_object(self): 242 # The extend_command(cmd) hook fires when commands are delivered to the 243 # ui, not simply at registration (because lazy registered plugin 244 # commands are registered). 245 # when they are simply created. 246 hook_calls = [] 247 commands.install_bzr_command_hooks() 248 commands.Command.hooks.install_named_hook( 249 "extend_command", hook_calls.append, None) 250 # create a command, should not fire 251 252 class cmd_test_extend_command_hook(commands.Command): 253 __doc__ = """A sample command.""" 254 self.assertEqual([], hook_calls) 255 # -- as a builtin 256 # register the command class, should not fire 257 try: 258 commands.builtin_command_registry.register( 259 cmd_test_extend_command_hook) 260 self.assertEqual([], hook_calls) 261 # and ask for the object, should fire 262 cmd = commands.get_cmd_object('test-extend-command-hook') 263 # For resilience - to ensure all code paths hit it - we 264 # fire on everything returned in the 'cmd_dict', which is currently 265 # all known commands, so assert that cmd is in hook_calls 266 self.assertSubset([cmd], hook_calls) 267 del hook_calls[:] 268 finally: 269 commands.builtin_command_registry.remove( 270 'test-extend-command-hook') 271 # -- as a plugin lazy registration 272 try: 273 # register the command class, should not fire 274 commands.plugin_cmds.register_lazy('cmd_fake', [], 275 'breezy.tests.fake_command') 276 self.assertEqual([], hook_calls) 277 # and ask for the object, should fire 278 cmd = commands.get_cmd_object('fake') 279 self.assertEqual([cmd], hook_calls) 280 finally: 281 commands.plugin_cmds.remove('fake') 282 283 284class TestGetCommandHook(tests.TestCase): 285 286 def test_fires_on_get_cmd_object(self): 287 # The get_command(cmd) hook fires when commands are delivered to the 288 # ui. 289 commands.install_bzr_command_hooks() 290 hook_calls = [] 291 292 class ACommand(commands.Command): 293 __doc__ = """A sample command.""" 294 295 def get_cmd(cmd_or_None, cmd_name): 296 hook_calls.append(('called', cmd_or_None, cmd_name)) 297 if cmd_name in ('foo', 'info'): 298 return ACommand() 299 commands.Command.hooks.install_named_hook( 300 "get_command", get_cmd, None) 301 # create a command directly, should not fire 302 cmd = ACommand() 303 self.assertEqual([], hook_calls) 304 # ask by name, should fire and give us our command 305 cmd = commands.get_cmd_object('foo') 306 self.assertEqual([('called', None, 'foo')], hook_calls) 307 self.assertIsInstance(cmd, ACommand) 308 del hook_calls[:] 309 # ask by a name that is supplied by a builtin - the hook should still 310 # fire and we still get our object, but we should see the builtin 311 # passed to the hook. 312 cmd = commands.get_cmd_object('info') 313 self.assertIsInstance(cmd, ACommand) 314 self.assertEqual(1, len(hook_calls)) 315 self.assertEqual('info', hook_calls[0][2]) 316 self.assertIsInstance(hook_calls[0][1], builtins.cmd_info) 317 318 319class TestCommandNotFound(tests.TestCase): 320 321 def setUp(self): 322 super(TestCommandNotFound, self).setUp() 323 commands._register_builtin_commands() 324 commands.install_bzr_command_hooks() 325 326 def test_not_found_no_suggestion(self): 327 e = self.assertRaises(errors.CommandError, 328 commands.get_cmd_object, 'idontexistand') 329 self.assertEqual('unknown command "idontexistand"', str(e)) 330 331 def test_not_found_with_suggestion(self): 332 e = self.assertRaises(errors.CommandError, 333 commands.get_cmd_object, 'statue') 334 self.assertEqual('unknown command "statue". Perhaps you meant "status"', 335 str(e)) 336 337 338class TestGetMissingCommandHook(tests.TestCase): 339 340 def hook_missing(self): 341 """Hook get_missing_command for testing.""" 342 self.hook_calls = [] 343 344 class ACommand(commands.Command): 345 __doc__ = """A sample command.""" 346 347 def get_missing_cmd(cmd_name): 348 self.hook_calls.append(('called', cmd_name)) 349 if cmd_name in ('foo', 'info'): 350 return ACommand() 351 commands.Command.hooks.install_named_hook( 352 "get_missing_command", get_missing_cmd, None) 353 self.ACommand = ACommand 354 355 def test_fires_on_get_cmd_object(self): 356 # The get_missing_command(cmd) hook fires when commands are delivered to the 357 # ui. 358 self.hook_missing() 359 # create a command directly, should not fire 360 self.cmd = self.ACommand() 361 self.assertEqual([], self.hook_calls) 362 # ask by name, should fire and give us our command 363 cmd = commands.get_cmd_object('foo') 364 self.assertEqual([('called', 'foo')], self.hook_calls) 365 self.assertIsInstance(cmd, self.ACommand) 366 del self.hook_calls[:] 367 # ask by a name that is supplied by a builtin - the hook should not 368 # fire and we still get our object. 369 commands.install_bzr_command_hooks() 370 cmd = commands.get_cmd_object('info') 371 self.assertNotEqual(None, cmd) 372 self.assertEqual(0, len(self.hook_calls)) 373 374 def test_skipped_on_HelpCommandIndex_get_topics(self): 375 # The get_missing_command(cmd_name) hook is not fired when 376 # looking up help topics. 377 self.hook_missing() 378 topic = commands.HelpCommandIndex() 379 topics = topic.get_topics('foo') 380 self.assertEqual([], self.hook_calls) 381 382 383class TestListCommandHook(tests.TestCase): 384 385 def test_fires_on_all_command_names(self): 386 # The list_commands() hook fires when all_command_names() is invoked. 387 hook_calls = [] 388 commands.install_bzr_command_hooks() 389 390 def list_my_commands(cmd_names): 391 hook_calls.append('called') 392 cmd_names.update(['foo', 'bar']) 393 return cmd_names 394 commands.Command.hooks.install_named_hook( 395 "list_commands", list_my_commands, None) 396 # Get a command, which should not trigger the hook. 397 cmd = commands.get_cmd_object('info') 398 self.assertEqual([], hook_calls) 399 # Get all command classes (for docs and shell completion). 400 cmds = list(commands.all_command_names()) 401 self.assertEqual(['called'], hook_calls) 402 self.assertSubset(['foo', 'bar'], cmds) 403 404 405class TestPreAndPostCommandHooks(tests.TestCase): 406 class TestError(Exception): 407 __doc__ = """A test exception.""" 408 409 def test_pre_and_post_hooks(self): 410 hook_calls = [] 411 412 def pre_command(cmd): 413 self.assertEqual([], hook_calls) 414 hook_calls.append('pre') 415 416 def post_command(cmd): 417 self.assertEqual(['pre', 'run'], hook_calls) 418 hook_calls.append('post') 419 420 def run(cmd): 421 self.assertEqual(['pre'], hook_calls) 422 hook_calls.append('run') 423 424 self.overrideAttr(builtins.cmd_rocks, 'run', run) 425 commands.install_bzr_command_hooks() 426 commands.Command.hooks.install_named_hook( 427 "pre_command", pre_command, None) 428 commands.Command.hooks.install_named_hook( 429 "post_command", post_command, None) 430 431 self.assertEqual([], hook_calls) 432 self.run_bzr(['rocks', '-Oxx=12', '-Oyy=foo']) 433 self.assertEqual(['pre', 'run', 'post'], hook_calls) 434 435 def test_post_hook_provided_exception(self): 436 hook_calls = [] 437 438 def post_command(cmd): 439 hook_calls.append('post') 440 441 def run(cmd): 442 hook_calls.append('run') 443 raise self.TestError() 444 445 self.overrideAttr(builtins.cmd_rocks, 'run', run) 446 commands.install_bzr_command_hooks() 447 commands.Command.hooks.install_named_hook( 448 "post_command", post_command, None) 449 450 self.assertEqual([], hook_calls) 451 self.assertRaises(self.TestError, commands.run_bzr, [u'rocks']) 452 self.assertEqual(['run', 'post'], hook_calls) 453 454 def test_pre_command_error(self): 455 """Ensure an CommandError in pre_command aborts the command""" 456 457 hook_calls = [] 458 459 def pre_command(cmd): 460 hook_calls.append('pre') 461 # verify that all subclasses of CommandError caught too 462 raise commands.BzrOptionError() 463 464 def post_command(cmd, e): 465 self.fail('post_command should not be called') 466 467 def run(cmd): 468 self.fail('command should not be called') 469 470 self.overrideAttr(builtins.cmd_rocks, 'run', run) 471 commands.install_bzr_command_hooks() 472 commands.Command.hooks.install_named_hook( 473 "pre_command", pre_command, None) 474 commands.Command.hooks.install_named_hook( 475 "post_command", post_command, None) 476 477 self.assertEqual([], hook_calls) 478 self.assertRaises(errors.CommandError, 479 commands.run_bzr, [u'rocks']) 480 self.assertEqual(['pre'], hook_calls) 481 482 483class GuessCommandTests(tests.TestCase): 484 485 def setUp(self): 486 super(GuessCommandTests, self).setUp() 487 commands._register_builtin_commands() 488 commands.install_bzr_command_hooks() 489 490 def test_guess_override(self): 491 self.assertEqual('ci', commands.guess_command('ic')) 492 493 def test_guess(self): 494 commands.get_cmd_object('status') 495 self.assertEqual('status', commands.guess_command('statue')) 496 497 def test_none(self): 498 self.assertIs(None, commands.guess_command('nothingisevenclose')) 499