# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for the fire module."""

import os
import sys
from unittest import mock

import fire
from fire import test_components as tc
from fire import testutils


class FireTest(testutils.BaseTestCase):

  def testFire(self):
    with mock.patch.object(sys, 'argv', ['progname']):
      fire.Fire(tc.Empty)
      fire.Fire(tc.OldStyleEmpty)
      fire.Fire(tc.WithInit)
    # Test both passing command as a sequence and as a string.
    self.assertEqual(fire.Fire(tc.NoDefaults, command='triple 4'), 12)
    self.assertEqual(fire.Fire(tc.WithDefaults, command=('double', '2')), 4)
    self.assertEqual(fire.Fire(tc.WithDefaults, command=['triple', '4']), 12)
    self.assertEqual(fire.Fire(tc.OldStyleWithDefaults,
                               command=['double', '2']), 4)
    self.assertEqual(fire.Fire(tc.OldStyleWithDefaults,
                               command=['triple', '4']), 12)

  def testFirePositionalCommand(self):
    # Test passing command as a positional argument.
    self.assertEqual(fire.Fire(tc.NoDefaults, 'double 2'), 4)
    self.assertEqual(fire.Fire(tc.NoDefaults, ['double', '2']), 4)

  def testFireInvalidCommandArg(self):
    with self.assertRaises(ValueError):
      # This is not a valid command.
      fire.Fire(tc.WithDefaults, command=10)

  def testFireDefaultName(self):
    with mock.patch.object(sys, 'argv',
                           [os.path.join('python-fire', 'fire',
                                         'base_filename.py')]):
      with self.assertOutputMatches(stdout='SYNOPSIS.*base_filename.py',
                                    stderr=None):
        fire.Fire(tc.Empty)

  def testFireNoArgs(self):
    self.assertEqual(fire.Fire(tc.MixedDefaults, command=['ten']), 10)

  def testFireExceptions(self):
    # Exceptions of Fire are printed to stderr and a FireExit is raised.
    with self.assertRaisesFireExit(2):
      fire.Fire(tc.Empty, command=['nomethod'])  # Member doesn't exist.
    with self.assertRaisesFireExit(2):
      fire.Fire(tc.NoDefaults, command=['double'])  # Missing argument.
    with self.assertRaisesFireExit(2):
      fire.Fire(tc.TypedProperties, command=['delta', 'x'])  # Missing key.

    # Exceptions of the target components are still raised.
    with self.assertRaises(ZeroDivisionError):
      fire.Fire(tc.NumberDefaults, command=['reciprocal', '0.0'])

  def testFireNamedArgs(self):
    self.assertEqual(fire.Fire(tc.WithDefaults,
                               command=['double', '--count', '5']), 10)
    self.assertEqual(fire.Fire(tc.WithDefaults,
                               command=['triple', '--count', '5']), 15)
    self.assertEqual(
        fire.Fire(tc.OldStyleWithDefaults, command=['double', '--count', '5']),
        10)
    self.assertEqual(
        fire.Fire(tc.OldStyleWithDefaults, command=['triple', '--count', '5']),
        15)

  def testFireNamedArgsSingleHyphen(self):
    self.assertEqual(fire.Fire(tc.WithDefaults,
                               command=['double', '-count', '5']), 10)
    self.assertEqual(fire.Fire(tc.WithDefaults,
                               command=['triple', '-count', '5']), 15)
    self.assertEqual(
        fire.Fire(tc.OldStyleWithDefaults, command=['double', '-count', '5']),
        10)
    self.assertEqual(
        fire.Fire(tc.OldStyleWithDefaults, command=['triple', '-count', '5']),
        15)

  def testFireNamedArgsWithEquals(self):
    self.assertEqual(fire.Fire(tc.WithDefaults,
                               command=['double', '--count=5']), 10)
    self.assertEqual(fire.Fire(tc.WithDefaults,
                               command=['triple', '--count=5']), 15)

  def testFireNamedArgsWithEqualsSingleHyphen(self):
    self.assertEqual(fire.Fire(tc.WithDefaults,
                               command=['double', '-count=5']), 10)
    self.assertEqual(fire.Fire(tc.WithDefaults,
                               command=['triple', '-count=5']), 15)

  def testFireAllNamedArgs(self):
    self.assertEqual(fire.Fire(tc.MixedDefaults, command=['sum', '1', '2']), 5)
    self.assertEqual(fire.Fire(tc.MixedDefaults,
                               command=['sum', '--alpha', '1', '2']), 5)
    self.assertEqual(fire.Fire(tc.MixedDefaults,
                               command=['sum', '--beta', '1', '2']), 4)
    self.assertEqual(fire.Fire(tc.MixedDefaults,
                               command=['sum', '1', '--alpha', '2']), 4)
    self.assertEqual(fire.Fire(tc.MixedDefaults,
                               command=['sum', '1', '--beta', '2']), 5)
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['sum', '--alpha', '1', '--beta', '2']), 5)
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['sum', '--beta', '1', '--alpha', '2']), 4)

  def testFireAllNamedArgsOneMissing(self):
    self.assertEqual(fire.Fire(tc.MixedDefaults, command=['sum']), 0)
    self.assertEqual(fire.Fire(tc.MixedDefaults, command=['sum', '1']), 1)
    self.assertEqual(fire.Fire(tc.MixedDefaults,
                               command=['sum', '--alpha', '1']), 1)
    self.assertEqual(fire.Fire(tc.MixedDefaults,
                               command=['sum', '--beta', '2']), 4)

  def testFirePartialNamedArgs(self):
    self.assertEqual(
        fire.Fire(tc.MixedDefaults, command=['identity', '1', '2']), (1, 2))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '--alpha', '1', '2']), (1, 2))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '--beta', '1', '2']), (2, 1))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '1', '--alpha', '2']), (2, 1))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '1', '--beta', '2']), (1, 2))
    self.assertEqual(
        fire.Fire(
            tc.MixedDefaults,
            command=['identity', '--alpha', '1', '--beta', '2']), (1, 2))
    self.assertEqual(
        fire.Fire(
            tc.MixedDefaults,
            command=['identity', '--beta', '1', '--alpha', '2']), (2, 1))

  def testFirePartialNamedArgsOneMissing(self):
    # Errors are written to standard out and a FireExit is raised.
    with self.assertRaisesFireExit(2):
      fire.Fire(tc.MixedDefaults,
                command=['identity'])  # Identity needs an arg.

    with self.assertRaisesFireExit(2):
      # Identity needs a value for alpha.
      fire.Fire(tc.MixedDefaults, command=['identity', '--beta', '2'])

    self.assertEqual(
        fire.Fire(tc.MixedDefaults, command=['identity', '1']), (1, '0'))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults, command=['identity', '--alpha', '1']),
        (1, '0'))

  def testFireAnnotatedArgs(self):
    self.assertEqual(fire.Fire(tc.Annotations, command=['double', '5']), 10)
    self.assertEqual(fire.Fire(tc.Annotations, command=['triple', '5']), 15)

  def testFireKeywordOnlyArgs(self):
    with self.assertRaisesFireExit(2):
      # Keyword arguments must be passed with flag syntax.
      fire.Fire(tc.py3.KeywordOnly, command=['double', '5'])

    self.assertEqual(
        fire.Fire(tc.py3.KeywordOnly, command=['double', '--count', '5']), 10)
    self.assertEqual(
        fire.Fire(tc.py3.KeywordOnly, command=['triple', '--count', '5']), 15)

  def testFireProperties(self):
    self.assertEqual(fire.Fire(tc.TypedProperties, command=['alpha']), True)
    self.assertEqual(fire.Fire(tc.TypedProperties, command=['beta']), (1, 2, 3))

  def testFireRecursion(self):
    self.assertEqual(
        fire.Fire(tc.TypedProperties,
                  command=['charlie', 'double', 'hello']), 'hellohello')
    self.assertEqual(fire.Fire(tc.TypedProperties,
                               command=['charlie', 'triple', 'w']), 'www')

  def testFireVarArgs(self):
    self.assertEqual(
        fire.Fire(tc.VarArgs,
                  command=['cumsums', 'a', 'b', 'c', 'd']),
        ['a', 'ab', 'abc', 'abcd'])
    self.assertEqual(
        fire.Fire(tc.VarArgs, command=['cumsums', '1', '2', '3', '4']),
        [1, 3, 6, 10])

  def testFireVarArgsWithNamedArgs(self):
    self.assertEqual(
        fire.Fire(tc.VarArgs, command=['varchars', '1', '2', 'c', 'd']),
        (1, 2, 'cd'))
    self.assertEqual(
        fire.Fire(tc.VarArgs, command=['varchars', '3', '4', 'c', 'd', 'e']),
        (3, 4, 'cde'))

  def testFireKeywordArgs(self):
    self.assertEqual(
        fire.Fire(
            tc.Kwargs,
            command=['props', '--name', 'David', '--age', '24']),
        {'name': 'David', 'age': 24})
    # Run this test both with a list command and a string command.
    self.assertEqual(
        fire.Fire(
            tc.Kwargs,
            command=['props', '--message',
                     '"This is a message it has -- in it"']),  # Quotes stripped
        {'message': 'This is a message it has -- in it'})
    self.assertEqual(
        fire.Fire(
            tc.Kwargs,
            command=['props', '--message',
                     'This is a message it has -- in it']),
        {'message': 'This is a message it has -- in it'})
    self.assertEqual(
        fire.Fire(
            tc.Kwargs,
            command='props --message "This is a message it has -- in it"'),
        {'message': 'This is a message it has -- in it'})
    self.assertEqual(
        fire.Fire(tc.Kwargs,
                  command=['upper', '--alpha', 'A', '--beta', 'B']),
        'ALPHA BETA')
    self.assertEqual(
        fire.Fire(
            tc.Kwargs,
            command=['upper', '--alpha', 'A', '--beta', 'B', '-', 'lower']),
        'alpha beta')

  def testFireKeywordArgsWithMissingPositionalArgs(self):
    self.assertEqual(
        fire.Fire(tc.Kwargs, command=['run', 'Hello', 'World', '--cell', 'is']),
        ('Hello', 'World', {'cell': 'is'}))
    self.assertEqual(
        fire.Fire(tc.Kwargs, command=['run', 'Hello', '--cell', 'ok']),
        ('Hello', None, {'cell': 'ok'}))

  def testFireObject(self):
    self.assertEqual(
        fire.Fire(tc.WithDefaults(), command=['double', '--count', '5']), 10)
    self.assertEqual(
        fire.Fire(tc.WithDefaults(), command=['triple', '--count', '5']), 15)

  def testFireDict(self):
    component = {
        'double': lambda x=0: 2 * x,
        'cheese': 'swiss',
    }
    self.assertEqual(fire.Fire(component, command=['double', '5']), 10)
    self.assertEqual(fire.Fire(component, command=['cheese']), 'swiss')

  def testFireObjectWithDict(self):
    self.assertEqual(
        fire.Fire(tc.TypedProperties, command=['delta', 'echo']), 'E')
    self.assertEqual(
        fire.Fire(tc.TypedProperties, command=['delta', 'echo', 'lower']), 'e')
    self.assertIsInstance(
        fire.Fire(tc.TypedProperties, command=['delta', 'nest']), dict)
    self.assertEqual(
        fire.Fire(tc.TypedProperties, command=['delta', 'nest', '0']), 'a')

  def testFireSet(self):
    component = tc.simple_set()
    result = fire.Fire(component, command=[])
    self.assertEqual(len(result), 3)

  def testFireFrozenset(self):
    component = tc.simple_frozenset()
    result = fire.Fire(component, command=[])
    self.assertEqual(len(result), 3)

  def testFireList(self):
    component = ['zero', 'one', 'two', 'three']
    self.assertEqual(fire.Fire(component, command=['2']), 'two')
    self.assertEqual(fire.Fire(component, command=['3']), 'three')
    self.assertEqual(fire.Fire(component, command=['-1']), 'three')

  def testFireObjectWithList(self):
    self.assertEqual(fire.Fire(tc.TypedProperties, command=['echo', '0']),
                     'alex')
    self.assertEqual(fire.Fire(tc.TypedProperties, command=['echo', '1']),
                     'bethany')

  def testFireObjectWithTuple(self):
    self.assertEqual(fire.Fire(tc.TypedProperties, command=['fox', '0']),
                     'carry')
    self.assertEqual(fire.Fire(tc.TypedProperties, command=['fox', '1']),
                     'divide')

  def testFireObjectWithListAsObject(self):
    self.assertEqual(
        fire.Fire(tc.TypedProperties, command=['echo', 'count', 'bethany']),
        1)

  def testFireObjectWithTupleAsObject(self):
    self.assertEqual(
        fire.Fire(tc.TypedProperties, command=['fox', 'count', 'divide']),
        1)

  def testFireNoComponent(self):
    self.assertEqual(fire.Fire(command=['tc', 'WithDefaults', 'double', '10']),
                     20)
    last_char = lambda text: text[-1]  # pylint: disable=unused-variable
    self.assertEqual(fire.Fire(command=['last_char', '"Hello"']), 'o')
    self.assertEqual(fire.Fire(command=['last-char', '"World"']), 'd')
    rset = lambda count=0: set(range(count))  # pylint: disable=unused-variable
    self.assertEqual(fire.Fire(command=['rset', '5']), {0, 1, 2, 3, 4})

  def testFireUnderscores(self):
    self.assertEqual(
        fire.Fire(tc.Underscores,
                  command=['underscore-example']), 'fish fingers')
    self.assertEqual(
        fire.Fire(tc.Underscores,
                  command=['underscore_example']), 'fish fingers')

  def testFireUnderscoresInArg(self):
    self.assertEqual(
        fire.Fire(tc.Underscores,
                  command=['underscore-function', 'example']), 'example')
    self.assertEqual(
        fire.Fire(tc.Underscores,
                  command=['underscore_function', '--underscore-arg=score']),
        'score')
    self.assertEqual(
        fire.Fire(tc.Underscores,
                  command=['underscore_function', '--underscore_arg=score']),
        'score')

  def testBoolParsing(self):
    self.assertEqual(fire.Fire(tc.BoolConverter, command=['as-bool', 'True']),
                     True)
    self.assertEqual(
        fire.Fire(tc.BoolConverter, command=['as-bool', 'False']), False)
    self.assertEqual(
        fire.Fire(tc.BoolConverter, command=['as-bool', '--arg=True']), True)
    self.assertEqual(
        fire.Fire(tc.BoolConverter, command=['as-bool', '--arg=False']), False)
    self.assertEqual(fire.Fire(tc.BoolConverter, command=['as-bool', '--arg']),
                     True)
    self.assertEqual(
        fire.Fire(tc.BoolConverter, command=['as-bool', '--noarg']), False)

  def testBoolParsingContinued(self):
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', 'True', 'False']), (True, False))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '--alpha=False', '10']), (False, 10))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '--alpha', '--beta', '10']), (True, 10))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '--alpha', '--beta=10']), (True, 10))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '--noalpha', '--beta']), (False, True))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults, command=['identity', '10', '--beta']),
        (10, True))

  def testBoolParsingSingleHyphen(self):
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-alpha=False', '10']), (False, 10))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-alpha', '-beta', '10']), (True, 10))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-alpha', '-beta=10']), (True, 10))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-noalpha', '-beta']), (False, True))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-alpha', '-10', '-beta']), (-10, True))

  def testBoolParsingLessExpectedCases(self):
    # Note: Does not return (True, 10).
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '--alpha', '10']), (10, '0'))
    # To get (True, 10), use one of the following:
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '--alpha', '--beta=10']),
        (True, 10))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', 'True', '10']), (True, 10))

    # Note: Does not return (True, '--test') or ('--test', 0).
    with self.assertRaisesFireExit(2):
      fire.Fire(tc.MixedDefaults, command=['identity', '--alpha', '--test'])

    self.assertEqual(
        fire.Fire(
            tc.MixedDefaults,
            command=['identity', '--alpha', 'True', '"--test"']),
        (True, '--test'))
    # To get ('--test', '0'), use one of the following:
    self.assertEqual(fire.Fire(tc.MixedDefaults,
                               command=['identity', '--alpha=--test']),
                     ('--test', '0'))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults, command=r'identity --alpha \"--test\"'),
        ('--test', '0'))

  def testSingleCharFlagParsing(self):
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-a']), (True, '0'))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-a', '--beta=10']), (True, 10))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-a', '-b']), (True, True))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-a', '42', '-b']), (42, True))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-a', '42', '-b', '10']), (42, 10))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '--alpha', 'True', '-b', '10']),
        (True, 10))
    with self.assertRaisesFireExit(2):
      # This test attempts to use an ambiguous shortcut flag on a function with
      # a naming conflict for the shortcut, triggering a FireError.
      fire.Fire(tc.SimilarArgNames, command=['identity', '-b'])

  def testSingleCharFlagParsingEqualSign(self):
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-a=True']), (True, '0'))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-a=3', '--beta=10']), (3, 10))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-a=False', '-b=15']), (False, 15))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-a', '42', '-b=12']), (42, 12))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-a=42', '-b', '10']), (42, 10))

  def testSingleCharFlagParsingExactMatch(self):
    self.assertEqual(
        fire.Fire(tc.SimilarArgNames,
                  command=['identity2', '-a']), (True, None))
    self.assertEqual(
        fire.Fire(tc.SimilarArgNames,
                  command=['identity2', '-a=10']), (10, None))
    self.assertEqual(
        fire.Fire(tc.SimilarArgNames,
                  command=['identity2', '--a']), (True, None))
    self.assertEqual(
        fire.Fire(tc.SimilarArgNames,
                  command=['identity2', '-alpha']), (None, True))
    self.assertEqual(
        fire.Fire(tc.SimilarArgNames,
                  command=['identity2', '-a', '-alpha']), (True, True))

  def testSingleCharFlagParsingCapitalLetter(self):
    self.assertEqual(
        fire.Fire(tc.CapitalizedArgNames,
                  command=['sum', '-D', '5', '-G', '10']), 15)

  def testBoolParsingWithNo(self):
    # In these examples --nothing always refers to the nothing argument:
    def fn1(thing, nothing):
      return thing, nothing

    self.assertEqual(fire.Fire(fn1, command=['--thing', '--nothing']),
                     (True, True))
    self.assertEqual(fire.Fire(fn1, command=['--thing', '--nonothing']),
                     (True, False))

    with self.assertRaisesFireExit(2):
      # In this case nothing=False (since rightmost setting of a flag gets
      # precedence), but it errors because thing has no value.
      fire.Fire(fn1, command=['--nothing', '--nonothing'])

    # In these examples, --nothing sets thing=False:
    def fn2(thing, **kwargs):
      return thing, kwargs
    self.assertEqual(fire.Fire(fn2, command=['--thing']), (True, {}))
    self.assertEqual(fire.Fire(fn2, command=['--nothing']), (False, {}))
    with self.assertRaisesFireExit(2):
      # In this case, nothing=True, but it errors because thing has no value.
      fire.Fire(fn2, command=['--nothing=True'])
    self.assertEqual(fire.Fire(fn2, command=['--nothing', '--nothing=True']),
                     (False, {'nothing': True}))

    def fn3(arg, **kwargs):
      return arg, kwargs
    self.assertEqual(fire.Fire(fn3, command=['--arg=value', '--thing']),
                     ('value', {'thing': True}))
    self.assertEqual(fire.Fire(fn3, command=['--arg=value', '--nothing']),
                     ('value', {'thing': False}))
    self.assertEqual(fire.Fire(fn3, command=['--arg=value', '--nonothing']),
                     ('value', {'nothing': False}))

  def testTraceFlag(self):
    with self.assertRaisesFireExit(0, 'Fire trace:\n'):
      fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '--trace'])
    with self.assertRaisesFireExit(0, 'Fire trace:\n'):
      fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '-t'])
    with self.assertRaisesFireExit(0, 'Fire trace:\n'):
      fire.Fire(tc.BoolConverter, command=['--', '--trace'])

  def testHelpFlag(self):
    with self.assertRaisesFireExit(0):
      fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '--help'])
    with self.assertRaisesFireExit(0):
      fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '-h'])
    with self.assertRaisesFireExit(0):
      fire.Fire(tc.BoolConverter, command=['--', '--help'])

  def testHelpFlagAndTraceFlag(self):
    with self.assertRaisesFireExit(0, 'Fire trace:\n.*SYNOPSIS'):
      fire.Fire(tc.BoolConverter,
                command=['as-bool', 'True', '--', '--help', '--trace'])
    with self.assertRaisesFireExit(0, 'Fire trace:\n.*SYNOPSIS'):
      fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '-h', '-t'])
    with self.assertRaisesFireExit(0, 'Fire trace:\n.*SYNOPSIS'):
      fire.Fire(tc.BoolConverter, command=['--', '-h', '--trace'])

  def testTabCompletionNoName(self):
    completion_script = fire.Fire(tc.NoDefaults, command=['--', '--completion'])
    self.assertIn('double', completion_script)
    self.assertIn('triple', completion_script)

  def testTabCompletion(self):
    completion_script = fire.Fire(
        tc.NoDefaults, command=['--', '--completion'], name='c')
    self.assertIn('double', completion_script)
    self.assertIn('triple', completion_script)

  def testTabCompletionWithDict(self):
    actions = {'multiply': lambda a, b: a * b}
    completion_script = fire.Fire(
        actions, command=['--', '--completion'], name='actCLI')
    self.assertIn('actCLI', completion_script)
    self.assertIn('multiply', completion_script)

  def testBasicSeparator(self):
    # '-' is the default separator.
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '+', '_']), ('+', '_'))
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '_', '+', '-']), ('_', '+'))

    # If we change the separator we can use '-' as an argument.
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['identity', '-', '_', '--', '--separator', '&']),
        ('-', '_'))

    # The separator triggers a function call, but there aren't enough arguments.
    with self.assertRaisesFireExit(2):
      fire.Fire(tc.MixedDefaults, command=['identity', '-', '_', '+'])

  def testNonComparable(self):
    """Fire should work with classes that disallow comparisons."""
    # Make sure this test passes both with a string command or a list command.
    self.assertIsInstance(
        fire.Fire(tc.NonComparable, command=''), tc.NonComparable)
    self.assertIsInstance(
        fire.Fire(tc.NonComparable, command=[]), tc.NonComparable)

    # The first separator instantiates the NonComparable object.
    # The second separator causes Fire to check if the separator was necessary.
    self.assertIsInstance(
        fire.Fire(tc.NonComparable, command=['-', '-']), tc.NonComparable)

  def testExtraSeparators(self):
    self.assertEqual(
        fire.Fire(
            tc.ReturnsObj,
            command=['get-obj', 'arg1', 'arg2', '-', '-', 'as-bool', 'True']),
        True)
    self.assertEqual(
        fire.Fire(
            tc.ReturnsObj,
            command=['get-obj', 'arg1', 'arg2', '-', '-', '-', 'as-bool',
                     'True']),
        True)

  def testSeparatorForChaining(self):
    # Without a separator all args are consumed by get_obj.
    self.assertIsInstance(
        fire.Fire(tc.ReturnsObj,
                  command=['get-obj', 'arg1', 'arg2', 'as-bool', 'True']),
        tc.BoolConverter)
    # With a separator only the preceding args are consumed by get_obj.
    self.assertEqual(
        fire.Fire(
            tc.ReturnsObj,
            command=['get-obj', 'arg1', 'arg2', '-', 'as-bool', 'True']), True)
    self.assertEqual(
        fire.Fire(tc.ReturnsObj,
                  command=['get-obj', 'arg1', 'arg2', '&', 'as-bool', 'True',
                           '--', '--separator', '&']),
        True)
    self.assertEqual(
        fire.Fire(tc.ReturnsObj,
                  command=['get-obj', 'arg1', '$$', 'as-bool', 'True', '--',
                           '--separator', '$$']),
        True)

  def testNegativeNumbers(self):
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['sum', '--alpha', '-3', '--beta', '-4']), -11)

  def testFloatForExpectedInt(self):
    self.assertEqual(
        fire.Fire(tc.MixedDefaults,
                  command=['sum', '--alpha', '2.2', '--beta', '3.0']), 8.2)
    self.assertEqual(
        fire.Fire(
            tc.NumberDefaults,
            command=['integer_reciprocal', '--divisor', '5.0']), 0.2)
    self.assertEqual(
        fire.Fire(tc.NumberDefaults, command=['integer_reciprocal', '4.0']),
        0.25)

  def testClassInstantiation(self):
    self.assertIsInstance(fire.Fire(tc.InstanceVars,
                                    command=['--arg1=a1', '--arg2=a2']),
                          tc.InstanceVars)
    with self.assertRaisesFireExit(2):
      # Cannot instantiate a class with positional args.
      fire.Fire(tc.InstanceVars, command=['a1', 'a2'])

  def testTraceErrors(self):
    # Class needs additional value but runs out of args.
    with self.assertRaisesFireExit(2):
      fire.Fire(tc.InstanceVars, command=['a1'])
    with self.assertRaisesFireExit(2):
      fire.Fire(tc.InstanceVars, command=['--arg1=a1'])

    # Routine needs additional value but runs out of args.
    with self.assertRaisesFireExit(2):
      fire.Fire(tc.InstanceVars, command=['a1', 'a2', '-', 'run', 'b1'])
    with self.assertRaisesFireExit(2):
      fire.Fire(tc.InstanceVars,
                command=['--arg1=a1', '--arg2=a2', '-', 'run b1'])

    # Extra args cannot be consumed.
    with self.assertRaisesFireExit(2):
      fire.Fire(tc.InstanceVars,
                command=['a1', 'a2', '-', 'run', 'b1', 'b2', 'b3'])
    with self.assertRaisesFireExit(2):
      fire.Fire(
          tc.InstanceVars,
          command=['--arg1=a1', '--arg2=a2', '-', 'run', 'b1', 'b2', 'b3'])

    # Cannot find member to access.
    with self.assertRaisesFireExit(2):
      fire.Fire(tc.InstanceVars, command=['a1', 'a2', '-', 'jog'])
    with self.assertRaisesFireExit(2):
      fire.Fire(tc.InstanceVars, command=['--arg1=a1', '--arg2=a2', '-', 'jog'])

  def testClassWithDefaultMethod(self):
    self.assertEqual(
        fire.Fire(tc.DefaultMethod, command=['double', '10']), 20
    )

  def testClassWithInvalidProperty(self):
    self.assertEqual(
        fire.Fire(tc.InvalidProperty, command=['double', '10']), 20
    )

  def testHelpKwargsDecorator(self):
    # Issue #190, follow the wrapped method instead of crashing.
    with self.assertRaisesFireExit(0):
      fire.Fire(tc.decorated_method, command=['-h'])
    with self.assertRaisesFireExit(0):
      fire.Fire(tc.decorated_method, command=['--help'])

  def testFireAsyncio(self):
    self.assertEqual(fire.Fire(tc.py3.WithAsyncio,
                               command=['double', '--count', '10']), 20)


if __name__ == '__main__':
  testutils.main()
