copier/tests/test_legacy_migration.py
2025-04-22 13:52:51 +02:00

320 lines
12 KiB
Python

import json
import platform
from pathlib import Path
from shutil import copytree
import pytest
from plumbum import local
from copier import run_copy, run_update
from copier._user_data import load_answersfile_data
from copier.errors import UserMessageError
from .helpers import BRACKET_ENVOPS_JSON, PROJECT_TEMPLATE, build_file_tree, git
SRC = Path(f"{PROJECT_TEMPLATE}_legacy_migrations").absolute()
# This fails on windows CI because, when the test tries to execute
# `migrations.py`, it doesn't understand that it should be interpreted
# by python.exe. Or maybe it fails because CI is using Git bash instead
# of WSL bash, which happened to work fine in real world tests.
# FIXME Some generous Windows power user please fix this test!
@pytest.mark.xfail(
condition=platform.system() == "Windows",
reason="Windows ignores shebang?",
strict=True,
)
@pytest.mark.parametrize("skip_tasks", [True, False])
def test_migrations_and_tasks(tmp_path: Path, skip_tasks: bool) -> None:
"""Check migrations and tasks are run properly."""
# Convert demo_migrations in a git repository with 2 versions
src, dst = tmp_path / "src", tmp_path / "dst"
copytree(SRC, src)
with local.cwd(src):
git("init")
git("add", ".")
git("commit", "-m1")
git("tag", "v1.0.0")
git("commit", "--allow-empty", "-m2")
git("tag", "v2.0")
# Copy it in v1
run_copy(
src_path=str(src),
dst_path=dst,
vcs_ref="v1.0.0",
unsafe=True,
skip_tasks=skip_tasks,
)
# Check copy was OK
if skip_tasks:
assert not (dst / "created-with-tasks.txt").exists()
assert (dst / "delete-in-tasks.txt").exists()
else:
assert (dst / "created-with-tasks.txt").read_text() == "task 1\ntask 2\n"
assert not (dst / "delete-in-tasks.txt").exists()
assert (dst / "delete-in-migration-v2.txt").is_file()
assert not (dst / "migrations.py").exists()
assert not (dst / "tasks.py").exists()
assert not list(dst.glob("*-before.txt"))
assert not list(dst.glob("*-after.txt"))
answers = load_answersfile_data(dst)
assert answers == {"_commit": "v1.0.0", "_src_path": str(src)}
# Save changes in downstream repo
with local.cwd(dst):
git("init")
git("add", ".")
git("commit", "-m1")
# Update it to v2
with pytest.deprecated_call():
run_update(
dst_path=dst,
defaults=True,
overwrite=True,
unsafe=True,
skip_tasks=skip_tasks,
)
# Check update was OK
if skip_tasks:
assert not (dst / "created-with-tasks.txt").exists()
assert (dst / "delete-in-tasks.txt").exists()
else:
assert (dst / "created-with-tasks.txt").read_text() == "task 1\ntask 2\n" * 2
assert not (dst / "delete-in-tasks.txt").exists()
assert not (dst / "delete-in-migration-v2.txt").exists()
assert not (dst / "migrations.py").exists()
assert not (dst / "tasks.py").exists()
assert (dst / "v1.0.0-v2-v2.0-before.json").is_file()
assert (dst / "v1.0.0-v2-v2.0-after.json").is_file()
assert (dst / "PEP440-1.0.0-2-2.0-before.json").is_file()
assert (dst / "PEP440-1.0.0-2-2.0-after.json").is_file()
answers = load_answersfile_data(dst)
assert answers == {"_commit": "v2.0", "_src_path": str(src)}
def test_pre_migration_modifies_answers(
tmp_path_factory: pytest.TempPathFactory,
) -> None:
"""Test support for answers modifications in pre-migrations."""
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
# v1 of template asks for a favourite song and writes it to songs.json
with local.cwd(src):
build_file_tree(
{
"[[ _copier_conf.answers_file ]].jinja": (
"[[ _copier_answers|tojson ]]"
),
"copier.yml": (
f"""\
_envops: {BRACKET_ENVOPS_JSON}
best_song: la vie en rose
"""
),
"songs.json.jinja": "[ [[ best_song|tojson ]] ]",
}
)
git("init")
git("add", ".")
git("commit", "-m1")
git("tag", "v1")
# User copies v1 template into subproject
with local.cwd(dst):
run_copy(src_path=str(src), defaults=True, overwrite=True)
answers = json.loads(Path(".copier-answers.yml").read_text())
assert answers["_commit"] == "v1"
assert answers["best_song"] == "la vie en rose"
assert json.loads(Path("songs.json").read_text()) == ["la vie en rose"]
git("init")
git("add", ".")
git("commit", "-m1")
with local.cwd(src):
build_file_tree(
{
# v2 of template supports multiple songs, has a different default
# and includes a data format migration script
"copier.yml": (
f"""\
_envops: {BRACKET_ENVOPS_JSON}
best_song_list:
default: [paranoid android]
_migrations:
- version: v2
before:
- - python
- -c
- |
import sys, json, pathlib
answers_path = pathlib.Path(*sys.argv[1:])
answers = json.loads(answers_path.read_text())
answers["best_song_list"] = [answers.pop("best_song")]
answers_path.write_text(json.dumps(answers))
- "[[ _copier_conf.dst_path ]]"
- "[[ _copier_conf.answers_file ]]"
"""
),
"songs.json.jinja": "[[ best_song_list|tojson ]]",
}
)
git("add", ".")
git("commit", "-m2")
git("tag", "v2")
# User updates subproject to v2 template
with local.cwd(dst):
with pytest.deprecated_call():
run_update(defaults=True, overwrite=True, unsafe=True)
answers = json.loads(Path(".copier-answers.yml").read_text())
assert answers["_commit"] == "v2"
assert "best_song" not in answers
assert answers["best_song_list"] == ["la vie en rose"]
assert json.loads(Path("songs.json").read_text()) == ["la vie en rose"]
def test_prereleases(tmp_path_factory: pytest.TempPathFactory) -> None:
"""Test prereleases support for copying and updating."""
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
with local.cwd(src):
# Build template in v1.0.0
build_file_tree(
{
"version.txt": "v1.0.0",
"[[ _copier_conf.answers_file ]].jinja": "[[_copier_answers|to_nice_yaml]]",
"copier.yaml": (
f"""\
_envops: {BRACKET_ENVOPS_JSON}
_migrations:
- version: v1.9
before:
- [python, -c, "import pathlib; pathlib.Path('v1.9').touch()"]
- version: v2.dev0
before:
- [python, -c, "import pathlib; pathlib.Path('v2.dev0').touch()"]
- version: v2.dev2
before:
- [python, -c, "import pathlib; pathlib.Path('v2.dev2').touch()"]
- version: v2.a1
before:
- [python, -c, "import pathlib; pathlib.Path('v2.a1').touch()"]
- version: v2.a2
before:
- [python, -c, "import pathlib; pathlib.Path('v2.a2').touch()"]
"""
),
}
)
git("init")
git("add", ".")
git("commit", "-mv1")
git("tag", "v1.0.0")
# Evolve template to v2.0.0.dev1
build_file_tree({"version.txt": "v2.0.0.dev1"})
git("commit", "-amv2dev1")
git("tag", "v2.0.0.dev1")
# Evolve template to v2.0.0.alpha1
build_file_tree({"version.txt": "v2.0.0.alpha1"})
git("commit", "-amv2a1")
git("tag", "v2.0.0.alpha1")
# Copying with use_prereleases=False copies v1
run_copy(src_path=str(src), dst_path=dst, defaults=True, overwrite=True)
answers = load_answersfile_data(dst)
assert answers["_commit"] == "v1.0.0"
assert (dst / "version.txt").read_text() == "v1.0.0"
assert not (dst / "v1.9").exists()
assert not (dst / "v2.dev0").exists()
assert not (dst / "v2.dev2").exists()
assert not (dst / "v2.a1").exists()
assert not (dst / "v2.a2").exists()
with local.cwd(dst):
# Commit subproject
git("init")
git("add", ".")
git("commit", "-mv1")
# Update it without prereleases; nothing changes
with pytest.deprecated_call():
run_update(defaults=True, overwrite=True)
assert not git("status", "--porcelain")
assert not (dst / "v1.9").exists()
assert not (dst / "v2.dev0").exists()
assert not (dst / "v2.dev2").exists()
assert not (dst / "v2.a1").exists()
assert not (dst / "v2.a2").exists()
# Update it with prereleases
with pytest.deprecated_call():
run_update(
dst_path=dst,
defaults=True,
overwrite=True,
use_prereleases=True,
unsafe=True,
)
answers = load_answersfile_data(dst)
assert answers["_commit"] == "v2.0.0.alpha1"
assert (dst / "version.txt").read_text() == "v2.0.0.alpha1"
assert (dst / "v1.9").exists()
assert (dst / "v2.dev0").exists()
assert (dst / "v2.dev2").exists()
assert (dst / "v2.a1").exists()
assert not (dst / "v2.a2").exists()
# It should fail if downgrading
with pytest.raises(UserMessageError), pytest.deprecated_call():
run_update(dst_path=dst, defaults=True, overwrite=True)
def test_pretend_mode(tmp_path_factory: pytest.TempPathFactory) -> None:
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
# Build template in v1
with local.cwd(src):
git("init")
build_file_tree(
{
"[[ _copier_conf.answers_file ]].jinja": "[[_copier_answers|to_nice_yaml]]",
"copier.yml": (
f"""\
_envops: {BRACKET_ENVOPS_JSON}
"""
),
}
)
git("add", ".")
git("commit", "-mv1")
git("tag", "v1")
run_copy(str(src), dst)
answers = load_answersfile_data(dst)
assert answers["_commit"] == "v1"
with local.cwd(dst):
git("init")
git("add", ".")
git("commit", "-mv1")
# Evolve template to v2
with local.cwd(src):
build_file_tree(
{
"[[ _copier_conf.answers_file ]].jinja": "[[_copier_answers|to_nice_yaml]]",
"copier.yml": (
f"""\
_envops: {BRACKET_ENVOPS_JSON}
_migrations:
- version: v2
before:
- touch v2-before.txt
after:
- touch v2-after.txt
"""
),
}
)
git("add", ".")
git("commit", "-mv2")
git("tag", "v2")
with pytest.deprecated_call():
run_update(dst_path=dst, overwrite=True, pretend=True, unsafe=True)
answers = load_answersfile_data(dst)
assert answers["_commit"] == "v1"
assert not (dst / "v2-before.txt").exists()
assert not (dst / "v2-after.txt").exists()