from __future__ import annotations import platform import subprocess import sys from collections.abc import Mapping from pathlib import Path from typing import Any, Protocol, Union import pexpect import pytest import yaml from pexpect.popen_spawn import PopenSpawn from plumbum import local from copier._types import StrOrPath from copier._user_data import load_answersfile_data from .helpers import ( BRACKET_ENVOPS, BRACKET_ENVOPS_JSON, COPIER_PATH, SUFFIX_TMPL, Keyboard, Spawn, build_file_tree, expect_prompt, git, git_save, ) if sys.version_info < (3, 10): from typing_extensions import TypeAlias else: from typing import TypeAlias MARIO_TREE: Mapping[StrOrPath, str | bytes] = { "copier.yml": ( f"""\ _templates_suffix: {SUFFIX_TMPL} _envops: {BRACKET_ENVOPS_JSON} in_love: type: bool default: yes your_name: type: str default: Mario help: If you have a name, tell me now. your_enemy: type: str default: Bowser secret: yes help: Secret enemy name what_enemy_does: type: str default: "[[ your_enemy ]] hates [[ your_name ]]" """ ), "[[ _copier_conf.answers_file ]].tmpl": "[[_copier_answers|to_nice_yaml]]", } MARIO_TREE_WITH_NEW_FIELD: Mapping[StrOrPath, str | bytes] = { "copier.yml": ( f"""\ _templates_suffix: {SUFFIX_TMPL} _envops: {BRACKET_ENVOPS_JSON} in_love: type: bool default: yes your_name: type: str default: Mario help: If you have a name, tell me now. your_enemy: type: str default: Bowser secret: yes help: Secret enemy name your_sister: type: str default: Luigi help: Your sister's name what_enemy_does: type: str default: "[[ your_enemy ]] hates [[ your_name ]]" """ ), "[[ _copier_conf.answers_file ]].tmpl": "[[_copier_answers|to_nice_yaml]]", } @pytest.mark.parametrize( "name, args", [ ("Mario", ()), # Default name in the template ("Luigi", ("--data=your_name=Luigi",)), ("None", ("--data=your_name=None",)), ], ) def test_copy_default_advertised( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, name: str, args: tuple[str, ...], ) -> None: """Test that the questions for the user are OK""" src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) with local.cwd(src): build_file_tree(MARIO_TREE) git_save(tag="v1") git("commit", "--allow-empty", "-m", "v2") git("tag", "v2") with local.cwd(dst): # Copy the v1 template tui = spawn( COPIER_PATH + ("copy", str(src), ".", "--vcs-ref=v1") + args, timeout=10 ) # Check what was captured expect_prompt(tui, "in_love", "bool") tui.expect_exact("(Y/n)") tui.sendline() tui.expect_exact("Yes") if not args: expect_prompt( tui, "your_name", "str", help="If you have a name, tell me now." ) tui.expect_exact(name) tui.sendline() expect_prompt(tui, "your_enemy", "str", help="Secret enemy name") tui.expect_exact("******") tui.sendline() expect_prompt(tui, "what_enemy_does", "str") tui.expect_exact(f"Bowser hates {name}") tui.sendline() tui.expect_exact(pexpect.EOF) assert load_answersfile_data(".").get("_commit") == "v1" # Update subproject git_save() assert load_answersfile_data(".").get("_commit") == "v1" tui = spawn(COPIER_PATH + ("update",), timeout=30) # Check what was captured expect_prompt(tui, "in_love", "bool") tui.expect_exact("(Y/n)") tui.sendline() expect_prompt(tui, "your_name", "str", help="If you have a name, tell me now.") tui.expect_exact(name) tui.sendline() expect_prompt(tui, "your_enemy", "str", help="Secret enemy name") tui.expect_exact("******") tui.sendline() expect_prompt(tui, "what_enemy_does", "str") tui.expect_exact(f"Bowser hates {name}") tui.sendline() tui.expect_exact(pexpect.EOF) assert load_answersfile_data(".").get("_commit") == "v2" @pytest.mark.parametrize( "name, args", [ ("Mario", ()), # Default name in the template ("Luigi", ("--data=your_name=Luigi",)), ("None", ("--data=your_name=None",)), ], ) @pytest.mark.parametrize("update_action", ("update", "recopy")) def test_update_skip_answered( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, name: str, update_action: str, args: tuple[str, ...], ) -> None: """Test that the questions for the user are OK""" src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) with local.cwd(src): build_file_tree(MARIO_TREE) git_save(tag="v1") git("commit", "--allow-empty", "-m", "v2") git("tag", "v2") with local.cwd(dst): # Copy the v1 template tui = spawn( COPIER_PATH + ("copy", str(src), ".", "--vcs-ref=v1") + args, timeout=10 ) # Check what was captured expect_prompt(tui, "in_love", "bool") tui.expect_exact("(Y/n)") tui.sendline() tui.expect_exact("Yes") if not args: expect_prompt( tui, "your_name", "str", help="If you have a name, tell me now." ) tui.expect_exact(name) tui.sendline() expect_prompt(tui, "your_enemy", "str", help="Secret enemy name") tui.expect_exact("******") tui.sendline() expect_prompt(tui, "what_enemy_does", "str") tui.expect_exact(f"Bowser hates {name}") tui.sendline() tui.expect_exact(pexpect.EOF) assert load_answersfile_data(".").get("_commit") == "v1" # Update subproject git_save() tui = spawn( COPIER_PATH + ( update_action, "--skip-answered", ), timeout=30, ) # Check what was captured expect_prompt(tui, "your_enemy", "str", help="Secret enemy name") tui.expect_exact("******") tui.sendline() tui.expect_exact(pexpect.EOF) assert load_answersfile_data(".").get("_commit") == "v2" @pytest.mark.parametrize( "name, args", [ ("Mario", ()), # Default name in the template ("Luigi", ("--data=your_name=Luigi",)), ("None", ("--data=your_name=None",)), ], ) def test_update_with_new_field_in_new_version_skip_answered( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, name: str, args: tuple[str, ...], ) -> None: """Test that the questions for the user are OK""" src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) with local.cwd(src): build_file_tree(MARIO_TREE) git_save(tag="v1") build_file_tree(MARIO_TREE_WITH_NEW_FIELD) git_save(tag="v2") with local.cwd(dst): # Copy the v1 template tui = spawn( COPIER_PATH + ("copy", str(src), ".", "--vcs-ref=v1") + args, timeout=10 ) # Check what was captured expect_prompt(tui, "in_love", "bool") tui.expect_exact("(Y/n)") tui.sendline() tui.expect_exact("Yes") if not args: expect_prompt( tui, "your_name", "str", help="If you have a name, tell me now." ) tui.expect_exact(name) tui.sendline() expect_prompt(tui, "your_enemy", "str", help="Secret enemy name") tui.expect_exact("******") tui.sendline() expect_prompt(tui, "what_enemy_does", "str") tui.expect_exact(f"Bowser hates {name}") tui.sendline() tui.expect_exact(pexpect.EOF) assert load_answersfile_data(".").get("_commit") == "v1" # Update subproject git_save() tui = spawn( COPIER_PATH + ( "update", "-A", ), timeout=30, ) # Check what was captured expect_prompt(tui, "your_enemy", "str", help="Secret enemy name") tui.expect_exact("******") tui.sendline() expect_prompt(tui, "your_sister", "str", help="Your sister's name") tui.expect_exact("Luigi") tui.sendline() tui.expect_exact(pexpect.EOF) assert load_answersfile_data(".").get("_commit") == "v2" @pytest.mark.parametrize( "question_1", # All these values evaluate to true ( True, 1, 1.1, -1, -0.001, "1", "1.1", "1st", "-1", "0.1", "00001", "000.0001", "-0001", "-000.001", "+001", "+000.001", "hello! 🤗", ), ) @pytest.mark.parametrize( "question_2_when, asks", ( (True, True), (False, False), ("trUe", True), ("faLse", False), ("Yes", True), ("nO", False), ("Y", True), ("N", False), ("on", True), ("off", False), ("~", False), ("NonE", False), ("nulL", False), ("[[ question_1 ]]", True), ("[[ not question_1 ]]", False), ("[% if question_1 %]YES[% endif %]", True), ("[% if question_1 %]FALSE[% endif %]", False), ), ) def test_when( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, question_1: str | int | float | bool, question_2_when: bool | str, asks: bool, ) -> None: """Test that the 2nd question is skipped or not, properly.""" src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) questions = { "_envops": BRACKET_ENVOPS, "_templates_suffix": SUFFIX_TMPL, "question_1": question_1, "question_2": {"default": "something", "type": "yaml", "when": question_2_when}, } build_file_tree( { (src / "copier.yml"): yaml.dump(questions), (src / "[[ _copier_conf.answers_file ]].tmpl"): ( "[[ _copier_answers|to_nice_yaml ]]" ), (src / "context.yml.tmpl"): ( """\ question_2: [[ question_2 ]] """ ), } ) tui = spawn(COPIER_PATH + ("copy", str(src), str(dst)), timeout=10) expect_prompt(tui, "question_1", type(question_1).__name__) tui.sendline() if asks: expect_prompt(tui, "question_2", "yaml") tui.sendline() tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert answers == { "_src_path": str(src), "question_1": question_1, **({"question_2": "something"} if asks else {}), } context = yaml.safe_load((dst / "context.yml").read_text()) assert context == {"question_2": "something"} def test_placeholder(tmp_path_factory: pytest.TempPathFactory, spawn: Spawn) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) build_file_tree( { (src / "copier.yml"): yaml.dump( { "_envops": BRACKET_ENVOPS, "_templates_suffix": SUFFIX_TMPL, "question_1": "answer 1", "question_2": { "type": "str", "help": "write a list of answers", "placeholder": "Write something like [[ question_1 ]], but better", }, } ), (src / "[[ _copier_conf.answers_file ]].tmpl"): ( "[[ _copier_answers|to_nice_yaml ]]" ), } ) tui = spawn(COPIER_PATH + ("copy", str(src), str(dst)), timeout=10) expect_prompt(tui, "question_1", "str") tui.expect_exact("answer 1") tui.sendline() expect_prompt(tui, "question_2", "str", help="write a list of answers") tui.expect_exact("Write something like answer 1, but better") tui.sendline() tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert answers == { "_src_path": str(src), "question_1": "answer 1", "question_2": "", } @pytest.mark.parametrize("type_", ["str", "yaml", "json"]) def test_multiline( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, type_: str ) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) build_file_tree( { (src / "copier.yml"): yaml.dump( { "_envops": BRACKET_ENVOPS, "_templates_suffix": SUFFIX_TMPL, "question_1": "answer 1", "question_2": {"type": type_}, "question_3": {"type": type_, "multiline": True}, "question_4": { "type": type_, "multiline": "[[ question_1 == 'answer 1' ]]", }, "question_5": { "type": type_, "multiline": "[[ question_1 != 'answer 1' ]]", }, } ), (src / "[[ _copier_conf.answers_file ]].tmpl"): ( "[[ _copier_answers|to_nice_yaml ]]" ), } ) tui = spawn(COPIER_PATH + ("copy", str(src), str(dst)), timeout=10) expect_prompt(tui, "question_1", "str") tui.expect_exact("answer 1") tui.sendline() expect_prompt(tui, "question_2", type_) tui.sendline('"answer 2"') expect_prompt(tui, "question_3", type_) tui.sendline('"answer 3"') tui.send(Keyboard.Alt + Keyboard.Enter) expect_prompt(tui, "question_4", type_) tui.sendline('"answer 4"') tui.send(Keyboard.Alt + Keyboard.Enter) expect_prompt(tui, "question_5", type_) tui.sendline('"answer 5"') tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) if type_ == "str": assert answers == { "_src_path": str(src), "question_1": "answer 1", "question_2": '"answer 2"', "question_3": ('"answer 3"\n'), "question_4": ('"answer 4"\n'), "question_5": ('"answer 5"'), } else: assert answers == { "_src_path": str(src), "question_1": "answer 1", "question_2": ("answer 2"), "question_3": ("answer 3"), "question_4": ("answer 4"), "question_5": ("answer 5"), } @pytest.mark.parametrize( "choices", ( [1, 2, 3], [["one", 1], ["two", 2], ["three", 3]], {"one": 1, "two": 2, "three": 3}, ), ) def test_update_choice( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, choices: list[int] | list[list[str | int]] | dict[str, int], ) -> None: """Choices are properly remembered and selected in TUI when updating.""" src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) # Create template build_file_tree( { (src / "copier.yml"): ( f"""\ _templates_suffix: {SUFFIX_TMPL} _envops: {BRACKET_ENVOPS_JSON} pick_one: type: float default: 3 choices: {choices} """ ), (src / "[[ _copier_conf.answers_file ]].tmpl"): ( "[[ _copier_answers|to_nice_yaml ]]" ), } ) with local.cwd(src): git("init") git("add", ".") git("commit", "-m one") git("tag", "v1") # Copy tui = spawn(COPIER_PATH + ("copy", str(src), str(dst)), timeout=10) expect_prompt(tui, "pick_one", "float") tui.sendline(Keyboard.Up) tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert answers["pick_one"] == 2.0 with local.cwd(dst): git("init") git("add", ".") git("commit", "-m1") # Update tui = spawn( COPIER_PATH + ( "update", str(dst), ), timeout=10, ) expect_prompt(tui, "pick_one", "float") tui.sendline(Keyboard.Down) tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert answers["pick_one"] == 3.0 @pytest.mark.skip("TODO: fix this") def test_multiline_defaults( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn ) -> None: """Multiline questions with scalar defaults produce multiline defaults.""" src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) build_file_tree( { (src / "copier.yaml"): ( """\ yaml_single: type: yaml default: &default - one - two - three: - four yaml_multi: type: yaml multiline: "{{ 'me' == ('m' + 'e') }}" default: *default json_single: type: json default: *default json_multi: type: json multiline: yes default: *default """ ), (src / "{{ _copier_conf.answers_file }}.jinja"): ( "{{ _copier_answers|to_nice_yaml }}" ), } ) tui = spawn(COPIER_PATH + ("copy", str(src), str(dst)), timeout=10) expect_prompt(tui, "yaml_single", "yaml") # This test will always fail here, because python prompt toolkit gives # syntax highlighting to YAML and JSON outputs, encoded into terminal # colors and all that stuff. # TODO Fix this test some day. tui.expect_exact("[one, two, {three: [four]}]") tui.send(Keyboard.Enter) expect_prompt(tui, "yaml_multi", "yaml") tui.expect_exact("> - one\n - two\n - three:\n - four") tui.send(Keyboard.Alt + Keyboard.Enter) expect_prompt(tui, "json_single", "json") tui.expect_exact('["one", "two", {"three": ["four"]}]') tui.send(Keyboard.Enter) expect_prompt(tui, "json_multi", "json") tui.expect_exact( '> [\n "one",\n "two",\n {\n "three": [\n "four"\n ]\n }\n ]' ) tui.send(Keyboard.Alt + Keyboard.Enter) tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert ( answers["yaml_single"] == answers["yaml_multi"] == answers["json_single"] == answers["json_multi"] == ["one", "two", {"three": ["four"]}] ) def test_partial_interrupt( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, ) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) build_file_tree( { (src / "copier.yml"): yaml.safe_dump( { "question_1": "answer 1", "question_2": "answer 2", } ), } ) tui = spawn(COPIER_PATH + ("copy", str(src), str(dst)), timeout=10) expect_prompt(tui, "question_1", "str") tui.expect_exact("answer 1") # Answer the first question using the default. tui.sendline() # Send a ctrl-c. tui.send(Keyboard.ControlC + Keyboard.Enter) # This string (or similar) should show in the output if we've gracefully caught the # interrupt. # We won't want our custom exception (CopierAnswersInterrupt) propagating # to an uncaught stacktrace. tui.expect_exact("Execution stopped by user\n") tui.expect_exact(pexpect.EOF) def test_var_name_value_allowed( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn ) -> None: """Test that a question var name "value" causes no name collision. See https://github.com/copier-org/copier/pull/780 for more details. """ src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) # Create template build_file_tree( { (src / "copier.yaml"): ( """\ value: type: str default: string """ ), (src / "{{ _copier_conf.answers_file }}.jinja"): ( "{{ _copier_answers|to_nice_yaml }}" ), } ) # Copy tui = spawn(COPIER_PATH + ("copy", str(src), str(dst)), timeout=10) expect_prompt(tui, "value", "str") tui.expect_exact("string") tui.send(Keyboard.Alt + Keyboard.Enter) tui.expect_exact(pexpect.EOF) # Check answers file answers = load_answersfile_data(dst) assert answers["value"] == "string" @pytest.mark.parametrize( "type_name, expected_answer", [ ("str", ""), ("int", ValueError("Invalid input")), ("float", ValueError("Invalid input")), ("json", ValueError("Invalid input")), ("yaml", None), ], ) def test_required_text_question( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, type_name: str, expected_answer: str | None | ValueError, ) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) build_file_tree( { (src / "copier.yml"): yaml.dump( { "_envops": BRACKET_ENVOPS, "_templates_suffix": SUFFIX_TMPL, "question": {"type": type_name}, } ), (src / "[[ _copier_conf.answers_file ]].tmpl"): ( "[[ _copier_answers|to_nice_yaml ]]" ), } ) tui = spawn(COPIER_PATH + ("copy", str(src), str(dst)), timeout=10) expect_prompt(tui, "question", type_name) tui.expect_exact("") tui.sendline() if isinstance(expected_answer, ValueError): tui.expect_exact(str(expected_answer)) assert not (dst / ".copier-answers.yml").exists() else: tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert answers == { "_src_path": str(src), "question": expected_answer, } def test_required_bool_question( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn ) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) build_file_tree( { (src / "copier.yml"): yaml.dump( { "_envops": BRACKET_ENVOPS, "_templates_suffix": SUFFIX_TMPL, "question": {"type": "bool"}, } ), (src / "[[ _copier_conf.answers_file ]].tmpl"): ( "[[ _copier_answers|to_nice_yaml ]]" ), } ) tui = spawn(COPIER_PATH + ("copy", str(src), str(dst)), timeout=10) expect_prompt(tui, "question", "bool") tui.expect_exact("(y/N)") tui.sendline() tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert answers == { "_src_path": str(src), "question": False, } @pytest.mark.parametrize( "type_name, choices, expected_answer", [ ("str", ["one", "two", "three"], "one"), ("int", [1, 2, 3], 1), ("float", [1.0, 2.0, 3.0], 1.0), ("json", ["[1]", "[2]", "[3]"], [1]), ("yaml", ["- 1", "- 2", "- 3"], [1]), ], ) def test_required_choice_question( tmp_path_factory: pytest.TempPathFactory, spawn: Spawn, type_name: str, choices: list[Any], expected_answer: Any, ) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) build_file_tree( { (src / "copier.yml"): yaml.dump( { "_envops": BRACKET_ENVOPS, "_templates_suffix": SUFFIX_TMPL, "question": { "type": type_name, "choices": choices, }, } ), (src / "[[ _copier_conf.answers_file ]].tmpl"): ( "[[ _copier_answers|to_nice_yaml ]]" ), } ) tui = spawn(COPIER_PATH + ("copy", str(src), str(dst)), timeout=10) expect_prompt(tui, "question", type_name) tui.sendline() tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert answers == { "_src_path": str(src), "question": expected_answer, } QuestionType: TypeAlias = str QuestionChoices: TypeAlias = Union[list[Any], dict[str, Any]] ParsedValues: TypeAlias = list[Any] _CHOICES: dict[str, tuple[QuestionType, QuestionChoices, ParsedValues]] = { "str": ("str", ["one", "two", "three"], ["one", "two", "three"]), "int": ("int", [1, 2, 3], [1, 2, 3]), "int-label-list": ("int", [["one", 1], ["two", 2], ["three", 3]], [1, 2, 3]), "int-label-dict": ("int", {"1. one": 1, "2. two": 2, "3. three": 3}, [1, 2, 3]), "float": ("float", [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]), "json": ("json", ["[1]", "[2]", "[3]"], [[1], [2], [3]]), "yaml": ("yaml", ["- 1", "- 2", "- 3"], [[1], [2], [3]]), } CHOICES = [pytest.param(*specs, id=id) for id, specs in _CHOICES.items()] class QuestionTreeFixture(Protocol): def __call__(self, **kwargs) -> tuple[Path, Path]: ... @pytest.fixture def question_tree(tmp_path_factory: pytest.TempPathFactory) -> QuestionTreeFixture: def builder(**question) -> tuple[Path, Path]: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) build_file_tree( { (src / "copier.yml"): yaml.dump( { "_envops": BRACKET_ENVOPS, "_templates_suffix": SUFFIX_TMPL, "question": question, } ), (src / "[[ _copier_conf.answers_file ]].tmpl"): ( "[[ _copier_answers|to_nice_yaml ]]" ), } ) return src, dst return builder class CopierFixture(Protocol): def __call__(self, *args, **kwargs) -> PopenSpawn: ... @pytest.fixture def copier(spawn: Spawn) -> CopierFixture: """Multiple choices are properly remembered and selected in TUI when updating.""" def fixture(*args, **kwargs) -> PopenSpawn: return spawn(COPIER_PATH + args, **kwargs) return fixture @pytest.mark.parametrize("type_name, choices, values", CHOICES) def test_multiselect_choices_question_single_answer( question_tree: QuestionTreeFixture, copier: CopierFixture, type_name: QuestionType, choices: QuestionChoices, values: ParsedValues, ) -> None: src, dst = question_tree(type=type_name, choices=choices, multiselect=True) tui = copier("copy", str(src), str(dst), timeout=10) expect_prompt(tui, "question", type_name) tui.send(" ") # select 1 tui.sendline() tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert answers["question"] == values[:1] @pytest.mark.parametrize("type_name, choices, values", CHOICES) def test_multiselect_choices_question_multiple_answers( question_tree: QuestionTreeFixture, copier: CopierFixture, type_name: QuestionType, choices: QuestionChoices, values: ParsedValues, ) -> None: src, dst = question_tree(type=type_name, choices=choices, multiselect=True) tui = copier("copy", str(src), str(dst), timeout=10) expect_prompt(tui, "question", type_name) tui.send(" ") # select 0 tui.send(Keyboard.Down) tui.send(" ") # select 1 tui.sendline() tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert answers["question"] == values[:2] @pytest.mark.parametrize("type_name, choices, values", CHOICES) def test_multiselect_choices_question_with_default( question_tree: QuestionTreeFixture, copier: CopierFixture, type_name: QuestionType, choices: QuestionChoices, values: ParsedValues, ) -> None: src, dst = question_tree( type=type_name, choices=choices, multiselect=True, default=values ) tui = copier("copy", str(src), str(dst), timeout=10) expect_prompt(tui, "question", type_name) tui.send(" ") # toggle first tui.sendline() tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert answers["question"] == values[1:] @pytest.mark.parametrize("type_name, choices, values", CHOICES) def test_update_multiselect_choices( question_tree: QuestionTreeFixture, copier: CopierFixture, type_name: QuestionType, choices: QuestionChoices, values: ParsedValues, ) -> None: """Multiple choices are properly remembered and selected in TUI when updating.""" src, dst = question_tree( type=type_name, choices=choices, multiselect=True, default=values ) with local.cwd(src): git("init") git("add", ".") git("commit", "-m one") git("tag", "v1") # Copy tui = copier("copy", str(src), str(dst), timeout=10) expect_prompt(tui, "question", type_name) tui.send(" ") # toggle first tui.sendline() tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert answers["question"] == values[1:] with local.cwd(dst): git("init") git("add", ".") git("commit", "-m1") # Update tui = copier("update", str(dst), timeout=10) expect_prompt(tui, "question", type_name) tui.send(" ") # toggle first tui.sendline() tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert answers["question"] == values def test_multiselect_choices_validator( question_tree: QuestionTreeFixture, copier: CopierFixture ) -> None: """Multiple choices are validated.""" src, dst = question_tree( type="str", choices=["one", "two", "three"], multiselect=True, validator=("[%- if not question -%]At least one choice required[%- endif -%]"), ) tui = copier("copy", str(src), str(dst), timeout=10) expect_prompt(tui, "question", "str") tui.sendline() tui.expect_exact("At least one choice required") tui.send(" ") # select "one" tui.sendline() tui.expect_exact(pexpect.EOF) answers = load_answersfile_data(dst) assert answers["question"] == ["one"] def test_secret_validator( question_tree: QuestionTreeFixture, copier: CopierFixture ) -> None: """Secret question answer is validated.""" default = "s3cret" src, dst = question_tree( type="str", default=default, secret=True, validator="[% if question|length < 3 %]too short[% endif %]", ) tui = copier("copy", str(src), str(dst), timeout=10) expect_prompt(tui, "question", "str") # Delete default value to fail validation for _ in range(len(default)): tui.send(Keyboard.Backspace) tui.sendline() tui.expect_exact("too short") # Enter default value again to pass validation tui.sendline(default) tui.expect_exact("******") tui.expect_exact(pexpect.EOF) # Related to https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1243#issuecomment-706668723) @pytest.mark.xfail( platform.system() == "Windows", reason="prompt-toolkit in subprocess call fails on Windows", ) def test_interactive_session_required_for_question_prompt( question_tree: QuestionTreeFixture, ) -> None: """Answering a question prompt requires an interactive session.""" src, dst = question_tree(type="str") process = subprocess.run( (*COPIER_PATH, "copy", str(src), str(dst)), stdin=subprocess.PIPE, # Prevents interactive input capture_output=True, timeout=10, ) assert process.returncode == 1 assert ( b"Interactive session required: Use `--defaults` and/or `--data`/`--data-file`" ) in process.stderr # Related to https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1243#issuecomment-706668723) @pytest.mark.xfail( platform.system() == "Windows", reason="prompt-toolkit in subprocess call fails on Windows", ) def test_interactive_session_required_for_overwrite_prompt( tmp_path_factory: pytest.TempPathFactory, ) -> None: """Overwriting a file without `--overwrite` flag requires an interactive session.""" src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) build_file_tree( { (src / "foo.txt.jinja"): "bar", (dst / "foo.txt"): "baz", } ) process = subprocess.run( (*COPIER_PATH, "copy", str(src), str(dst)), stdin=subprocess.PIPE, # Prevents interactive input capture_output=True, timeout=10, ) assert process.returncode == 1 assert ( b"Interactive session required: Consider using `--overwrite`" in process.stderr )