# -*- coding: utf-8 -*-
###########################################################################
# Copyright (c), The AiiDA team. All rights reserved. #
# This file is part of the AiiDA code. #
# #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core #
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
"""Unit tests for the InteractiveOption."""
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import
import unittest
import click
from click.testing import CliRunner
from click.types import IntParamType
from . import NON_INTERACTIVE
from .interactive import InteractiveOption
[docs]class Only42IntParamType(IntParamType):
"""
Param type that only accepts 42 as valid value
"""
name = 'only42int'
[docs] def convert(self, value, param, ctx):
newval = super(Only42IntParamType, self).convert(value, param, ctx)
if newval != 42:
self.fail("Type validation: invalid, should be 42")
return newval
[docs] def __repr__(self):
return 'ONLY42INT'
[docs]class InteractiveOptionTest(unittest.TestCase):
"""Unit tests for InteractiveOption."""
# pylint: disable=too-many-public-methods, missing-docstring
[docs] def simple_command(self, **kwargs):
"""Return a simple command with one InteractiveOption, kwargs get relayed to the option."""
# pylint: disable=no-self-use
@click.command()
@click.option('--opt', prompt='Opt', cls=InteractiveOption, **kwargs)
@NON_INTERACTIVE()
def cmd(opt, non_interactive):
"""test command for InteractiveOption"""
# pylint: disable=unused-argument
click.echo(str(opt))
return cmd
[docs] @classmethod
def setUpClass(cls):
cls.runner = CliRunner()
[docs] def prompt_output(self, cli_input, converted=None):
"""Return expected output of simple_command, given a commandline cli_input string."""
# pylint: disable=no-self-use
return "Opt: {}\n{}\n".format(cli_input, converted or cli_input)
[docs] def test_prompt_str(self):
"""
scenario: using InteractiveOption with type=str
behaviour: giving no option prompts, accepts a string
"""
cmd = self.simple_command(type=str)
runner = CliRunner()
result = runner.invoke(cmd, [], input='TEST\n')
expected = self.prompt_output('TEST')
self.assertIsNone(result.exception)
self.assertIn(expected, result.output)
[docs] def test_prompt_help_default(self):
"""
scenario: using InteractiveOption with type=str and no help parameter and invoking without options
behaviour: entering '?' leads to a default help message being printed and prompt repeated
"""
cmd = self.simple_command(type=str)
runner = CliRunner()
result = runner.invoke(cmd, [], input='?\nTEST\n')
expected_1 = 'Opt: ?\n'
expected_2 = 'Expecting text\n'
expected_3 = 'Opt: TEST\nTEST\n'
self.assertIsNone(result.exception)
self.assertIn(expected_1, result.output)
self.assertIn(expected_2, result.output)
self.assertIn(expected_3, result.output)
[docs] def test_prompt_help_custom(self):
"""
scenario: using InteractiveOption with type=str and help message and invoking without options
behaviour: entering '?' leads to the given help message being printed and the prompt repeated
"""
cmd = self.simple_command(type=str, help='Please enter some text')
runner = CliRunner()
result = runner.invoke(cmd, [], input='?\nTEST\n')
expected_1 = 'Opt: ?\n'
expected_2 = 'Please enter some text\n'
expected_3 = 'Opt: TEST\nTEST\n'
self.assertIsNone(result.exception)
self.assertIn(expected_1, result.output)
self.assertIn(expected_2, result.output)
self.assertIn(expected_3, result.output)
[docs] def test_prompt_simple(self):
"""
scenario: using InteractiveOption with type=bool
behaviour: giving no option prompts, accepts 'true'
"""
params = [(bool, 'true', 'True'), (int, '98', '98'), (float, '3.14e-7', '3.14e-07')]
for ptype, cli_input, output in params:
cmd = self.simple_command(type=ptype, help='help msg')
runner = CliRunner()
result = runner.invoke(cmd, [], input='\n?\n{}\n'.format(cli_input))
expected_1 = 'Opt: \nOpt: ?\n'
expected_2 = 'help msg\n'
expected_2 += self.prompt_output(cli_input, output)
self.assertIsNone(result.exception)
self.assertIn(expected_1, result.output)
self.assertIn(expected_2, result.output)
[docs] @staticmethod
def strip_line(text):
"""returns text without the last line"""
return text.rsplit('\n')[0]
[docs] def test_prompt_complex(self):
"""
scenario: using InteractiveOption with type=float
behaviour: giving no option prompts, accepts 3.14e-7
"""
params = [(click.File(), __file__), (click.Path(exists=True), __file__)]
for ptype, cli_input in params:
cmd = self.simple_command(type=ptype, help='help msg')
runner = CliRunner()
result = runner.invoke(cmd, [], input='\n?\n{}\n'.format(cli_input))
expected_1 = 'Opt: \nOpt: ?\n'
expected_2 = 'help msg\n'
expected_2 += self.strip_line(self.prompt_output(cli_input))
self.assertIsNone(result.exception)
self.assertIn(expected_1, result.output)
self.assertIn(expected_2, result.output)
[docs] def test_default_value_prompt(self):
"""
scenario: using InteractiveOption with a default value, invoke without options
behaviour: prompt, showing the default value, take default on empty cli_input.
"""
returns = []
cmd = self.simple_command(default='default')
result = self.runner.invoke(cmd, [], input='\n')
returns.append(result)
expected = 'Opt [default]: \ndefault\n'
self.assertIsNone(result.exception)
self.assertIn(expected, result.output)
result = self.runner.invoke(cmd, [], input='TEST\n')
returns.append(result)
expected = 'Opt [default]: TEST\nTEST\n'
self.assertIsNone(result.exception)
self.assertIn(expected, result.output)
return returns
[docs] def test_default_value_empty_opt(self):
"""
scenario: InteractiveOption with default value, invoke with empty option (--opt=)
behaviour: accept empty string as input
"""
cmd = self.simple_command(default='default')
runner = CliRunner()
result = runner.invoke(cmd, ['--opt='])
expected = '\n'
self.assertIsNone(result.exception)
self.assertEqual(result.output, expected)
[docs] def test_opt_given_valid(self):
"""
scenario: InteractiveOption, invoked with a valid value on the cmdline
behaviour: accept valid value
"""
cmd = self.simple_command(type=int)
runner = CliRunner()
result = runner.invoke(cmd, ['--opt=4'])
expected = '4\n'
self.assertIsNone(result.exception)
self.assertEqual(result.output, expected)
[docs] def test_opt_given_invalid(self):
"""
scenario: InteractiveOption, invoked with a valid value on the cmdline
behaviour: accept valid value
"""
cmd = self.simple_command(type=int)
runner = CliRunner()
result = runner.invoke(cmd, ['--opt=foo'])
self.assertIsNotNone(result.exception)
self.assertIn('Invalid value', result.output)
[docs] def test_non_interactive(self):
"""
scenario: InteractiveOption, invoked with only --non-interactive (and the option is required)
behaviout: fail
"""
cmd = self.simple_command(required=True)
runner = CliRunner()
result = runner.invoke(cmd, ['--non-interactive'])
self.assertIsNotNone(result.exception)
self.assertIn('Usage: ', result.output)
self.assertIn('Missing option', result.output)
[docs] def test_non_interactive_default(self):
"""
scenario: InteractiveOption, invoked with only --non-interactive
behaviour: fail
"""
cmd = self.simple_command(default='default')
runner = CliRunner()
result = runner.invoke(cmd, ['--non-interactive'])
self.assertIsNone(result.exception)
self.assertEqual(result.output, 'default\n')
[docs] @staticmethod
def user_callback(_ctx, param, value):
"""
A fake user callback ued for testing.
:param _ctx: The click context
:param param: The parameter name
:param value: The parameter value
:return: The validated parameter
"""
if not value:
return -1
if value != 42:
raise click.BadParameter('invalid', param=param)
return value
[docs] def test_after_callback_valid(self):
"""
scenario: InteractiveOption with a user callback
action: invoke with valid value
behaviour: user callback runs & succeeds
"""
cmd = self.simple_command(callback=self.user_callback, type=int)
result = self.runner.invoke(cmd, ['--opt=42'])
self.assertIsNone(result.exception)
self.assertEqual(result.output, '42\n')
[docs] def test_after_callback_invalid(self):
"""
scenario: InteractiveOption with a user callback
action: invoke with invalid value of right type
behaviour: user callback runs & succeeds
"""
cmd = self.simple_command(callback=self.user_callback, type=int)
result = self.runner.invoke(cmd, ['--opt=234234'])
self.assertIsNotNone(result.exception)
self.assertIn('Invalid value', result.output)
self.assertIn('invalid', result.output)
[docs] def test_after_callback_wrong_typ(self):
"""
scenario: InteractiveOption with a user callback
action: invoke with invalid value of wrong type
behaviour: user callback does not run
"""
cmd = self.simple_command(callback=self.user_callback, type=int)
result = self.runner.invoke(cmd, ['--opt=bla'])
self.assertIsNotNone(result.exception)
self.assertIn('Invalid value', result.output)
self.assertIn('bla', result.output)
[docs] def test_after_callback_empty(self):
"""
scenario: InteractiveOption with a user callback
action: invoke with invalid value of wrong type
behaviour: user callback does not run
"""
cmd = self.simple_command(callback=self.user_callback, type=int)
result = self.runner.invoke(cmd, ['--opt='])
self.assertIsNotNone(result.exception)
self.assertIn('Invalid value', result.output)
self.assertNotIn('empty', result.output)
[docs] def test_after_validation_interactive(self):
"""
Test that the type validation gets called on values entered at a prompt.
Scenario:
* InteractiveOption with custom type and prompt set
* invoked without passing the options
* on prompt: first enter an invalid value, then a valid one
Behaviour:
* Prompt for the value
* reject invalid value, prompt again
* accept valid value
"""
cmd = self.simple_command(callback=self.user_callback, type=Only42IntParamType())
result = self.runner.invoke(cmd, [], input='23\n42\n')
self.assertIsNone(result.exception)
self.assertIn('Opt: 23\n', result.output)
self.assertIn('Type validation: invalid', result.output)
self.assertIn('Opt: 42\n42\n', result.output)
[docs] def test_after_callback_default_noninteractive(self):
"""
Test that the callback gets called on the default, in line with click 6 behaviour.
Scenario:
* InteractiveOption with user callback and invalid default
* invoke with no options and --non-interactive
Behaviour:
* the default value gets passed through the callback and rejected
"""
# pylint: disable=invalid-name
cmd = self.simple_command(callback=self.user_callback, type=int, default=23)
result = self.runner.invoke(cmd, ['--non-interactive'])
self.assertIsNotNone(result.exception)
self.assertIn('Invalid value', result.output)
[docs] def test_default_empty_empty_cli(self):
"""Test that default="" allows to pass an empty cli option."""
cmd = self.simple_command(default="", type=str)
result = self.runner.invoke(cmd, ['--opt='])
self.assertIsNone(result.exception)
self.assertEqual(result.output, '\n')
[docs] def test_default_empty_prompt(self):
"""Test that default="" allows to pass an empty cli option."""
cmd = self.simple_command(default="", type=str)
result = self.runner.invoke(cmd, input='\n')
expected = 'Opt []: \n\n'
self.assertIsNone(result.exception)
self.assertIn(expected, result.output)
[docs] def test_prompt_dynamic_default(self):
"""Test that dynamic defaults for prompting still work."""
[docs] def test_not_required_noninteractive(self):
cmd = self.simple_command(required=False)
result = self.runner.invoke(cmd, ['--non-interactive'])
self.assertIsNone(result.exception)
# I strip, there is typically a \n at the end
self.assertEqual(result.output, 'None\n')
[docs] def test_not_required_interactive(self):
cmd = self.simple_command(required=False)
result = self.runner.invoke(cmd, input='value\n')
expected = 'Opt: value\nvalue\n'
self.assertIsNone(result.exception)
self.assertIn(expected, result.output)
[docs] def test_not_required_noninteractive_default(self):
cmd = self.simple_command(required=False, default='')
result = self.runner.invoke(cmd, ['--non-interactive'])
self.assertIsNone(result.exception)
self.assertEqual(result.output, '\n')
[docs] def test_not_required_interactive_default(self):
cmd = self.simple_command(required=False, default='')
result = self.runner.invoke(cmd, input='\nnot needed\n')
expected = 'Opt []: \n\n'
self.assertIsNone(result.exception)
self.assertIn(expected, result.output)