copier/tests/test_updatediff.py
Timothée Mazzucotelli bd83f15bd6
feat: Add extensions option allowing to load additional Jinja2 extensions (#370)
Co-authored-by: Jairo Llopis <Yajo@users.noreply.github.com>
2021-03-27 22:07:56 +00:00

414 lines
14 KiB
Python

import platform
from pathlib import Path
from textwrap import dedent
import pytest
from plumbum import local
from plumbum.cmd import git
from copier import Worker, copy
from copier.cli import CopierApp
from .helpers import BRACKET_ENVOPS_JSON, SUFFIX_TMPL, build_file_tree
def test_updatediff(tmp_path_factory):
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
# Prepare repo bundle
repo = src / "repo"
bundle = src / "demo_updatediff_repo.bundle"
last_commit = ""
build_file_tree(
{
repo
/ ".copier-answers.yml.jinja": """\
# Changes here will be overwritten by Copier
{{ _copier_answers|to_nice_yaml }}
""",
repo
/ "copier.yml": """\
_envops:
"keep_trailing_newline": True
project_name: to become a pirate
author_name: Guybrush
""",
repo
/ "README.txt.jinja": """
Let me introduce myself.
My name is {{author_name}}, and my project is {{project_name}}.
Thanks for your attention.
""",
}
)
with local.cwd(repo):
git("init")
git("add", ".")
git("commit", "-m", "Guybrush wants to be a pirate")
git("tag", "v0.0.1")
build_file_tree(
{
repo
/ "copier.yml": """\
_envops:
"keep_trailing_newline": True
project_name: to become a pirate
author_name: Guybrush
_migrations:
- version: v0.0.1
before:
- touch before-v0.0.1
after:
- touch after-v0.0.1
- version: v0.0.2
before:
- touch before-v0.0.2
after:
- touch after-v0.0.2
- version: v1.0.0
before:
- touch before-v1.0.0
after:
- touch after-v1.0.0
""",
}
)
with local.cwd(repo):
git("init")
git("add", ".")
git("commit", "-m", "Add migrations")
git("tag", "v0.0.2")
build_file_tree(
{
repo
/ "copier.yml": """\
_envops:
"keep_trailing_newline": True
project_name: to rule
author_name: Elaine
_migrations:
- version: v0.0.1
before:
- touch before-v0.0.1
after:
- touch after-v0.0.1
- version: v0.0.2
before:
- touch before-v0.0.2
after:
- touch after-v0.0.2
- version: v1.0.0
before:
- touch before-v1.0.0
after:
- touch after-v1.0.0
""",
repo
/ "README.txt.jinja": """
Let me introduce myself.
My name is {{author_name}}.
My project is {{project_name}}.
Thanks for your attention.
""",
}
)
with local.cwd(repo):
git("init")
git("add", ".")
git("commit", "-m", "Elaine wants to rule")
git("bundle", "create", bundle, "--all")
last_commit = git("describe", "--tags").strip()
# Generate repo bundle
target = dst / "target"
readme = target / "README.txt"
answers = target / ".copier-answers.yml"
commit = git["commit", "--all"]
# Run copier 1st time, with specific tag
CopierApp.invoke("copy", str(bundle), str(target), force=True, vcs_ref="v0.0.1")
# Check it's copied OK
assert answers.read_text() == dedent(
f"""\
# Changes here will be overwritten by Copier
_commit: v0.0.1
_src_path: {bundle}
author_name: Guybrush
project_name: to become a pirate\n
"""
)
assert readme.read_text() == dedent(
"""
Let me introduce myself.
My name is Guybrush, and my project is to become a pirate.
Thanks for your attention.
"""
)
# Init destination as a new independent git repo
with local.cwd(target):
git("init")
# Configure git in case you're running in CI
git("config", "user.name", "Copier Test")
git("config", "user.email", "test@copier")
# Commit changes
git("add", ".")
commit("-m", "hello world")
# Emulate the user modifying the README by hand
with open(readme, "w") as readme_fd:
readme_fd.write(
dedent(
"""
Let me introduce myself.
My name is Guybrush, and my project is to become a pirate.
Thanks for your grog.
"""
)
)
commit("-m", "I prefer grog")
# Update target to latest tag and check it's updated in answers file
CopierApp.invoke(force=True)
assert answers.read_text() == dedent(
f"""\
# Changes here will be overwritten by Copier
_commit: v0.0.2
_src_path: {bundle}
author_name: Guybrush
project_name: to become a pirate\n
"""
)
# Check migrations were executed properly
assert not (target / "before-v0.0.1").is_file()
assert not (target / "after-v0.0.1").is_file()
assert (target / "before-v0.0.2").is_file()
assert (target / "after-v0.0.2").is_file()
(target / "before-v0.0.2").unlink()
(target / "after-v0.0.2").unlink()
assert not (target / "before-v1.0.0").is_file()
assert not (target / "after-v1.0.0").is_file()
commit("-m", "Update template to v0.0.2")
# Update target to latest commit, which is still untagged
CopierApp.invoke(force=True, vcs_ref="HEAD")
# Check no new migrations were executed
assert not (target / "before-v0.0.1").is_file()
assert not (target / "after-v0.0.1").is_file()
assert not (target / "before-v0.0.2").is_file()
assert not (target / "after-v0.0.2").is_file()
assert not (target / "before-v1.0.0").is_file()
assert not (target / "after-v1.0.0").is_file()
# Check it's updated OK
assert answers.read_text() == dedent(
f"""\
# Changes here will be overwritten by Copier
_commit: {last_commit}
_src_path: {bundle}
author_name: Guybrush
project_name: to become a pirate\n
"""
)
assert readme.read_text() == dedent(
"""
Let me introduce myself.
My name is Guybrush.
My project is to become a pirate.
Thanks for your grog.
"""
)
commit("-m", f"Update template to {last_commit}")
assert not git("status", "--porcelain")
# No more updates exist, so updating again should change nothing
CopierApp.invoke(force=True, vcs_ref="HEAD")
assert not git("status", "--porcelain")
# If I change an option, it updates properly
copy(
data={"author_name": "Largo LaGrande", "project_name": "to steal a lot"},
force=True,
vcs_ref="HEAD",
)
assert readme.read_text() == dedent(
"""
Let me introduce myself.
My name is Largo LaGrande.
My project is to steal a lot.
Thanks for your grog.
"""
)
commit("-m", "Subproject evolved")
# Reapply template ignoring subproject evolution
Worker(
data={"author_name": "Largo LaGrande", "project_name": "to steal a lot"},
force=True,
vcs_ref="HEAD",
).run_copy()
assert readme.read_text() == dedent(
"""
Let me introduce myself.
My name is Largo LaGrande.
My project is to steal a lot.
Thanks for your attention.
"""
)
# This fails on Windows because there's some problem while detecting
# the diff. It seems like an older Git version were being used, while
# that's not the case...
# FIXME Some generous Windows power user please fix this test!
@pytest.mark.xfail(
condition=platform.system() == "Windows", reason="Git broken on Windows?"
)
def test_commit_hooks_respected(tmp_path_factory):
"""Commit hooks are taken into account when producing the update diff."""
# Prepare source template v1
src, dst1, dst2 = map(tmp_path_factory.mktemp, ("src", "dst1", "dst2"))
with local.cwd(src):
build_file_tree(
{
"copier.yml": f"""
_envops: {BRACKET_ENVOPS_JSON}
_templates_suffix: {SUFFIX_TMPL}
_tasks:
- git init
- pre-commit install
- pre-commit run -a || true
what: grog
""",
"[[ _copier_conf.answers_file ]].tmpl": """
[[ _copier_answers|to_nice_yaml ]]
""",
".pre-commit-config.yaml": r"""
repos:
- repo: https://github.com/prettier/prettier
rev: 2.0.4
hooks:
- id: prettier
- repo: local
hooks:
- id: forbidden-files
name: forbidden files
entry: found forbidden files; remove them
language: fail
files: "\\.rej$"
""",
"life.yml.tmpl": """
# Following code should be reformatted by pre-commit after copying
Line 1: hello
Line 2: [[ what ]]
Line 3: bye
""",
}
)
git("init")
git("add", ".")
git("commit", "-m", "commit 1")
git("tag", "v1")
# Copy source template
copy(src_path=str(src), dst_path=dst1, force=True)
with local.cwd(dst1):
life = Path("life.yml")
git("add", ".")
# 1st commit fails because pre-commit reformats life.yml
git("commit", "-am", "failed commit", retcode=1)
# 2nd commit works because it's already formatted
git("commit", "-am", "copied v1")
assert life.read_text() == dedent(
"""\
# Following code should be reformatted by pre-commit after copying
Line 1: hello
Line 2: grog
Line 3: bye
"""
)
# Evolve source template to v2
with local.cwd(src):
build_file_tree(
{
"life.yml.tmpl": """
# Following code should be reformatted by pre-commit after copying
Line 1: hello world
Line 2: grow up
Line 3: [[ what ]]
Line 4: grow old
Line 5: bye bye world
""",
}
)
git("init")
git("add", ".")
git("commit", "-m", "commit 2")
git("tag", "v2")
# Update subproject to v2
copy(dst_path=dst1, force=True)
with local.cwd(dst1):
git("commit", "-am", "copied v2")
assert life.read_text() == dedent(
"""\
# Following code should be reformatted by pre-commit after copying
Line 1: hello world
Line 2: grow up
Line 3: grog
Line 4: grow old
Line 5: bye bye world
"""
)
# No .rej files created (update diff was smart)
assert not git("status", "--porcelain")
# Subproject evolves
life.write_text(
dedent(
"""\
Line 1: hello world
Line 2: grow up
Line 2.5: make friends
Line 3: grog
Line 4: grow old
Line 4.5: no more work
Line 5: bye bye world
"""
)
)
git("commit", "-am", "subproject is evolved")
# A new subproject appears, which is a shallow clone of the 1st one.
# Using file:// prefix to allow local shallow clones.
git("clone", "--depth=1", f"file://{dst1}", dst2)
with local.cwd(dst2):
# Subproject re-updates just to change some values
copy(data={"what": "study"}, force=True)
git("commit", "-am", "re-updated to change values after evolving")
# Subproject evolution was respected up to sane possibilities.
# In an ideal world, this file would be exactly the same as what's written
# a few lines above, just changing "grog" for "study". However, that's nearly
# impossible to achieve, because each change hunk needs at least 1 line of
# context to let git apply that patch smartly, and that context couldn't be
# found because we changed data when updating, so the sanest thing we can
# do is to provide a .rej file to notify those
# unresolvable diffs. OTOH, some other changes are be applied.
# If some day you are able to produce that ideal result, you should be
# happy to modify these asserts.
assert life.read_text() == dedent(
"""\
Line 1: hello world
Line 2: grow up
Line 3: study
Line 4: grow old
Line 4.5: no more work
Line 5: bye bye world
"""
)
# This time a .rej file is unavoidable
assert Path(f"{life}.rej").is_file()