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()