mirror of
https://github.com/copier-org/copier.git
synced 2025-05-05 15:32:54 +00:00
601 lines
18 KiB
Python
601 lines
18 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from collections.abc import Sequence
|
|
from datetime import datetime
|
|
|
|
import pexpect
|
|
import pytest
|
|
import yaml
|
|
from pexpect.popen_spawn import PopenSpawn
|
|
from plumbum import local
|
|
|
|
from copier._main import Worker
|
|
from copier._types import AnyByStrDict
|
|
from copier._user_data import load_answersfile_data
|
|
from copier.errors import InvalidTypeError
|
|
|
|
from .helpers import (
|
|
BRACKET_ENVOPS,
|
|
BRACKET_ENVOPS_JSON,
|
|
COPIER_PATH,
|
|
SUFFIX_TMPL,
|
|
Keyboard,
|
|
Spawn,
|
|
build_file_tree,
|
|
expect_prompt,
|
|
git,
|
|
git_init,
|
|
)
|
|
|
|
main_default = "copier"
|
|
main_question = {
|
|
"main": {"default": main_default},
|
|
"_envops": BRACKET_ENVOPS,
|
|
"_templates_suffix": SUFFIX_TMPL,
|
|
}
|
|
|
|
|
|
class Prompt:
|
|
def __init__(self, name: str, format: str, help: str | None = None) -> None:
|
|
self.name = name
|
|
self.format = format
|
|
self.help = help
|
|
|
|
def expect(self, tui: PopenSpawn) -> None:
|
|
expect_prompt(tui, self.name, self.format, self.help)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"questions_data, expected_value, expected_outputs",
|
|
[
|
|
(
|
|
{"templated_default": {"default": "[[ main ]]default"}},
|
|
"copierdefault",
|
|
[Prompt("templated_default", "str"), "copierdefault"],
|
|
),
|
|
(
|
|
{
|
|
"templated_type": {
|
|
"type": "[% if main == 'copier' %]int[% endif %]",
|
|
"default": "0",
|
|
},
|
|
},
|
|
0,
|
|
[Prompt("templated_type", "int"), "0"],
|
|
),
|
|
(
|
|
{
|
|
"templated_help": {
|
|
"default": main_default,
|
|
"help": "THIS [[ main ]] HELP IS TEMPLATED",
|
|
},
|
|
},
|
|
main_default,
|
|
[
|
|
Prompt("templated_help", "str", "THIS copier HELP IS TEMPLATED"),
|
|
"copier",
|
|
],
|
|
),
|
|
(
|
|
{
|
|
"templated_choices_dict_1": {
|
|
"default": "[[ main ]]",
|
|
"choices": {
|
|
"choice 1": "[[ main ]]",
|
|
"[[ main ]]": "value 2",
|
|
},
|
|
},
|
|
},
|
|
main_default,
|
|
["(Use arrow keys)", "choice 1", "copier"],
|
|
),
|
|
(
|
|
{
|
|
"templated_choices_dict_2": {
|
|
"default": "value 2",
|
|
"choices": {"choice 1": "[[ main ]]", "[[ main ]]": "value 2"},
|
|
},
|
|
},
|
|
"value 2",
|
|
["(Use arrow keys)", "choice 1", "copier"],
|
|
),
|
|
(
|
|
{
|
|
"templated_choices_string_list_1": {
|
|
"default": main_default,
|
|
"choices": ["[[ main ]]", "choice 2"],
|
|
},
|
|
},
|
|
main_default,
|
|
["(Use arrow keys)", "copier", "choice 2"],
|
|
),
|
|
(
|
|
{
|
|
"templated_choices_string_list_2": {
|
|
"default": "choice 1",
|
|
"choices": ["choice 1", "[[ main ]]"],
|
|
},
|
|
},
|
|
"choice 1",
|
|
["(Use arrow keys)", "choice 1", "copier"],
|
|
),
|
|
(
|
|
{
|
|
"templated_choices_tuple_list_1": {
|
|
"default": main_default,
|
|
"choices": [["name 1", "[[ main ]]"], ["[[ main ]]", "value 2"]],
|
|
},
|
|
},
|
|
main_default,
|
|
["(Use arrow keys)", "name 1", "copier"],
|
|
),
|
|
(
|
|
{
|
|
"templated_choices_tuple_list_2": {
|
|
"default": "value 2",
|
|
"choices": [["name 1", "[[ main ]]"], ["[[ main ]]", "value 2"]],
|
|
},
|
|
},
|
|
"value 2",
|
|
["name 1", "copier"],
|
|
),
|
|
(
|
|
{
|
|
"templated_choices_mixed_list": {
|
|
"default": "value 2",
|
|
"choices": ["[[ main ]]", ["[[ main ]]", "value 2"]],
|
|
},
|
|
},
|
|
"value 2",
|
|
["copier", "copier"],
|
|
),
|
|
],
|
|
)
|
|
def test_templated_prompt(
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
spawn: Spawn,
|
|
questions_data: AnyByStrDict,
|
|
expected_value: str | int,
|
|
expected_outputs: Sequence[str | Prompt],
|
|
) -> None:
|
|
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
|
questions_combined = {**main_question, **questions_data}
|
|
# There's always only 1 question; get its name
|
|
question_name = next(iter(questions_data))
|
|
build_file_tree(
|
|
{
|
|
(src / "copier.yml"): json.dumps(questions_combined),
|
|
(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, "main", "str")
|
|
tui.expect_exact(main_default)
|
|
tui.sendline()
|
|
for output in expected_outputs:
|
|
if isinstance(output, Prompt):
|
|
output.expect(tui)
|
|
else:
|
|
tui.expect_exact(output)
|
|
tui.sendline()
|
|
tui.expect_exact(pexpect.EOF)
|
|
answers = load_answersfile_data(dst)
|
|
assert answers[question_name] == expected_value
|
|
|
|
|
|
def test_templated_prompt_custom_envops(
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
) -> None:
|
|
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
|
build_file_tree(
|
|
{
|
|
(src / "copier.yml"): (
|
|
"""\
|
|
_envops:
|
|
block_start_string: "<%"
|
|
block_end_string: "%>"
|
|
comment_start_string: "<#"
|
|
comment_end_string: "#>"
|
|
variable_start_string: "<<"
|
|
variable_end_string: ">>"
|
|
|
|
powerlevel:
|
|
type: int
|
|
default: 9000
|
|
|
|
sentence:
|
|
type: str
|
|
default: "<% if powerlevel >= 9000 %>It's over 9000!<% else %>It's only << powerlevel >>...<% endif %>"
|
|
"""
|
|
),
|
|
(src / "result.jinja"): "<<sentence>>",
|
|
}
|
|
)
|
|
worker1 = Worker(str(src), dst, defaults=True, overwrite=True)
|
|
worker1.run_copy()
|
|
assert (dst / "result").read_text() == "It's over 9000!"
|
|
|
|
worker2 = Worker(
|
|
str(src), dst, data={"powerlevel": 1}, defaults=True, overwrite=True
|
|
)
|
|
worker2.run_copy()
|
|
assert (dst / "result").read_text() == "It's only 1..."
|
|
|
|
|
|
def test_templated_prompt_builtins(tmp_path_factory: pytest.TempPathFactory) -> None:
|
|
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
|
build_file_tree(
|
|
{
|
|
(src / "copier.yaml"): (
|
|
f"""\
|
|
_templates_suffix: {SUFFIX_TMPL}
|
|
_envops: {BRACKET_ENVOPS_JSON}
|
|
question1:
|
|
default: "[[ now() ]]"
|
|
question2:
|
|
default: "[[ make_secret() ]]"
|
|
"""
|
|
),
|
|
src / "now.tmpl": "[[ question1 ]]",
|
|
src / "make_secret.tmpl": "[[ question2 ]]",
|
|
}
|
|
)
|
|
with pytest.warns(FutureWarning) as warnings:
|
|
Worker(str(src), dst, defaults=True, overwrite=True).run_copy()
|
|
assert len([w for w in warnings if w.category is FutureWarning]) == 2
|
|
that_now = datetime.fromisoformat((dst / "now").read_text())
|
|
assert that_now <= datetime.utcnow()
|
|
assert len((dst / "make_secret").read_text()) == 128
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"questions, raises, returns",
|
|
(
|
|
({"question": {"default": "{{ not_valid }}"}}, None, ""),
|
|
({"question": {"help": "{{ not_valid }}"}}, None, "None"),
|
|
({"question": {"type": "{{ not_valid }}"}}, InvalidTypeError, "None"),
|
|
({"question": {"choices": ["{{ not_valid }}"]}}, None, "None"),
|
|
),
|
|
)
|
|
def test_templated_prompt_invalid(
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
questions: AnyByStrDict,
|
|
raises: type[BaseException] | None,
|
|
returns: str,
|
|
) -> None:
|
|
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
|
build_file_tree(
|
|
{
|
|
src / "copier.yml": yaml.safe_dump(questions),
|
|
src / "result.jinja": "{{question}}",
|
|
}
|
|
)
|
|
worker = Worker(str(src), dst, data={"question": ""}, overwrite=True)
|
|
if raises:
|
|
with pytest.raises(raises):
|
|
worker.run_copy()
|
|
else:
|
|
worker.run_copy()
|
|
assert (dst / "result").read_text() == returns
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"cloud, iac_choices",
|
|
[
|
|
(
|
|
"Any",
|
|
[
|
|
"Terraform",
|
|
"Cloud Formation (Requires AWS)",
|
|
"Azure Resource Manager (Requires Azure)",
|
|
"Deployment Manager (Requires GCP)",
|
|
],
|
|
),
|
|
(
|
|
"AWS",
|
|
[
|
|
"Terraform",
|
|
"Cloud Formation",
|
|
"Azure Resource Manager (Requires Azure)",
|
|
"Deployment Manager (Requires GCP)",
|
|
],
|
|
),
|
|
(
|
|
"Azure",
|
|
[
|
|
"Terraform",
|
|
"Cloud Formation (Requires AWS)",
|
|
"Azure Resource Manager",
|
|
"Deployment Manager (Requires GCP)",
|
|
],
|
|
),
|
|
(
|
|
"GCP",
|
|
[
|
|
"Terraform",
|
|
"Cloud Formation (Requires AWS)",
|
|
"Azure Resource Manager (Requires Azure)",
|
|
"Deployment Manager",
|
|
],
|
|
),
|
|
],
|
|
)
|
|
def test_templated_prompt_with_conditional_choices(
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
spawn: Spawn,
|
|
cloud: str,
|
|
iac_choices: str,
|
|
) -> None:
|
|
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
|
build_file_tree(
|
|
{
|
|
(src / "copier.yml"): (
|
|
"""\
|
|
cloud:
|
|
type: str
|
|
help: Which cloud provider do you use?
|
|
choices:
|
|
- Any
|
|
- AWS
|
|
- Azure
|
|
- GCP
|
|
|
|
iac:
|
|
type: str
|
|
help: Which IaC tool do you use?
|
|
choices:
|
|
Terraform: tf
|
|
Cloud Formation:
|
|
value: cf
|
|
validator: "{% if cloud != 'AWS' %}Requires AWS{% endif %}"
|
|
Azure Resource Manager:
|
|
value: arm
|
|
validator: "{% if cloud != 'Azure' %}Requires Azure{% endif %}"
|
|
Deployment Manager:
|
|
value: dm
|
|
validator: "{% if cloud != 'GCP' %}Requires GCP{% endif %}"
|
|
"""
|
|
),
|
|
}
|
|
)
|
|
tui = spawn(
|
|
COPIER_PATH + ("copy", f"--data=cloud={cloud}", str(src), str(dst)),
|
|
timeout=10,
|
|
)
|
|
expect_prompt(tui, "iac", "str", help="Which IaC tool do you use?")
|
|
for iac in iac_choices:
|
|
tui.expect_exact(iac)
|
|
tui.sendline()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"cloud, iac_choices",
|
|
[
|
|
("Any", ["Terraform"]),
|
|
("AWS", ["Terraform", "Cloud Formation"]),
|
|
("Azure", ["Terraform", "Azure Resource Manager"]),
|
|
("GCP", ["Terraform", "Deployment Manager"]),
|
|
],
|
|
)
|
|
def test_templated_prompt_with_templated_choices(
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
spawn: Spawn,
|
|
cloud: str,
|
|
iac_choices: str,
|
|
) -> None:
|
|
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
|
build_file_tree(
|
|
{
|
|
(src / "copier.yml"): (
|
|
"""\
|
|
cloud:
|
|
type: str
|
|
help: Which cloud provider do you use?
|
|
choices:
|
|
- Any
|
|
- AWS
|
|
- Azure
|
|
- GCP
|
|
|
|
iac:
|
|
type: str
|
|
help: Which IaC tool do you use?
|
|
choices: |
|
|
Terraform: tf
|
|
{%- if cloud == 'AWS' %}
|
|
Cloud Formation: cf
|
|
{%- endif %}
|
|
{%- if cloud == 'Azure' %}
|
|
Azure Resource Manager: arm
|
|
{%- endif %}
|
|
{%- if cloud == 'GCP' %}
|
|
Deployment Manager: dm
|
|
{%- endif %}
|
|
"""
|
|
),
|
|
}
|
|
)
|
|
tui = spawn(
|
|
COPIER_PATH + ("copy", f"--data=cloud={cloud}", str(src), str(dst)),
|
|
timeout=10,
|
|
)
|
|
expect_prompt(tui, "iac", "str", help="Which IaC tool do you use?")
|
|
for iac in iac_choices:
|
|
tui.expect_exact(iac)
|
|
tui.sendline()
|
|
|
|
|
|
def test_templated_prompt_update_previous_answer_disabled(
|
|
tmp_path_factory: pytest.TempPathFactory, spawn: Spawn
|
|
) -> None:
|
|
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
|
build_file_tree(
|
|
{
|
|
(src / "copier.yml"): (
|
|
"""\
|
|
cloud:
|
|
type: str
|
|
help: Which cloud provider do you use?
|
|
choices:
|
|
- Any
|
|
- AWS
|
|
- Azure
|
|
- GCP
|
|
iac:
|
|
type: str
|
|
help: Which IaC tool do you use?
|
|
choices:
|
|
Terraform: tf
|
|
Cloud Formation:
|
|
value: cf
|
|
validator: "{% if cloud != 'AWS' %}Requires AWS{% endif %}"
|
|
Azure Resource Manager:
|
|
value: arm
|
|
validator: "{% if cloud != 'Azure' %}Requires Azure{% endif %}"
|
|
Deployment Manager:
|
|
value: dm
|
|
validator: "{% if cloud != 'GCP' %}Requires GCP{% endif %}"
|
|
"""
|
|
),
|
|
(src / "{{ _copier_conf.answers_file }}.jinja"): (
|
|
"{{ _copier_answers|to_nice_yaml }}"
|
|
),
|
|
}
|
|
)
|
|
|
|
with local.cwd(src):
|
|
git_init("v1")
|
|
git("tag", "v1")
|
|
|
|
tui = spawn(COPIER_PATH + ("copy", str(src), str(dst)), timeout=10)
|
|
expect_prompt(tui, "cloud", "str", help="Which cloud provider do you use?")
|
|
tui.sendline(Keyboard.Down) # select "AWS"
|
|
expect_prompt(tui, "iac", "str", help="Which IaC tool do you use?")
|
|
tui.sendline(Keyboard.Down) # select "Cloud Formation"
|
|
tui.expect_exact(pexpect.EOF)
|
|
|
|
assert load_answersfile_data(dst) == {
|
|
"_src_path": str(src),
|
|
"_commit": "v1",
|
|
"cloud": "AWS",
|
|
"iac": "cf",
|
|
}
|
|
|
|
with local.cwd(dst):
|
|
git_init("v1")
|
|
|
|
tui = spawn(COPIER_PATH + ("update", str(dst)), timeout=10)
|
|
expect_prompt(tui, "cloud", "str", help="Which cloud provider do you use?")
|
|
tui.sendline(Keyboard.Down) # select "Azure"
|
|
expect_prompt(tui, "iac", "str", help="Which IaC tool do you use?")
|
|
tui.sendline() # select "Terraform" (first supported)
|
|
tui.expect_exact(pexpect.EOF)
|
|
|
|
assert load_answersfile_data(dst) == {
|
|
"_src_path": str(src),
|
|
"_commit": "v1",
|
|
"cloud": "Azure",
|
|
"iac": "tf",
|
|
}
|
|
|
|
|
|
def test_multiselect_choices_with_templated_default_value(
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
spawn: Spawn,
|
|
) -> None:
|
|
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
|
build_file_tree(
|
|
{
|
|
(src / "copier.yml"): (
|
|
"""\
|
|
python_version:
|
|
type: str
|
|
help: What version of python are you targeting?
|
|
default: "3.11"
|
|
choices:
|
|
- "3.8"
|
|
- "3.9"
|
|
- "3.10"
|
|
- "3.11"
|
|
- "3.12"
|
|
|
|
github_runner_python_version:
|
|
type: str
|
|
help: Which Python versions do you want to use on your Github Runner?
|
|
default: ["{{ python_version }}"]
|
|
multiselect: true
|
|
choices:
|
|
- "3.8"
|
|
- "3.9"
|
|
- "3.10"
|
|
- "3.11"
|
|
- "3.12"
|
|
"""
|
|
),
|
|
(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, "python_version", "str", help="What version of python are you targeting?"
|
|
)
|
|
tui.sendline() # select `3.11" (default value)
|
|
expect_prompt(
|
|
tui,
|
|
"github_runner_python_version",
|
|
"str",
|
|
help="Which Python versions do you want to use on your Github Runner?",
|
|
)
|
|
tui.sendline() # select "[3.11]" (default value)
|
|
tui.expect_exact(pexpect.EOF)
|
|
|
|
assert load_answersfile_data(dst) == {
|
|
"_src_path": str(src),
|
|
"python_version": "3.11",
|
|
"github_runner_python_version": ["3.11"],
|
|
}
|
|
|
|
|
|
def test_copier_phase_variable(
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
spawn: Spawn,
|
|
) -> None:
|
|
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
|
build_file_tree(
|
|
{
|
|
(src / "copier.yml"): """\
|
|
phase:
|
|
type: str
|
|
default: "{{ _copier_phase }}"
|
|
"""
|
|
}
|
|
)
|
|
tui = spawn(COPIER_PATH + ("copy", str(src), str(dst)), timeout=10)
|
|
expect_prompt(tui, "phase", "str")
|
|
tui.expect_exact("prompt")
|
|
|
|
|
|
def test_copier_conf_variable(
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
spawn: Spawn,
|
|
) -> None:
|
|
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
|
build_file_tree(
|
|
{
|
|
(src / "copier.yml"): """\
|
|
project_name:
|
|
type: str
|
|
default: "{{ _copier_conf.dst_path | basename }}"
|
|
"""
|
|
}
|
|
)
|
|
tui = spawn(COPIER_PATH + ("copy", str(src), str(dst / "test_project")), timeout=10)
|
|
expect_prompt(tui, "project_name", "str")
|
|
tui.expect_exact("test_project")
|