Skip to content

Commit 612b3aa

Browse files
committed
tests/cli(test[add]): Cover path-mode workflow
1 parent 62dc040 commit 612b3aa

File tree

1 file changed

+223
-1
lines changed

1 file changed

+223
-1
lines changed

tests/cli/test_add.py

Lines changed: 223 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
from __future__ import annotations
44

5+
import argparse
6+
import logging
7+
import subprocess
58
import typing as t
69

710
import pytest
811

9-
from vcspull.cli.add import add_repo
12+
from vcspull.cli.add import add_repo, handle_add_command
13+
from vcspull.util import contract_user_home
1014

1115
if t.TYPE_CHECKING:
1216
import pathlib
@@ -29,6 +33,17 @@ class AddRepoFixture(t.NamedTuple):
2933
expected_log_messages: list[str]
3034

3135

36+
def init_git_repo(repo_path: pathlib.Path, remote_url: str | None) -> None:
37+
"""Initialize a git repository with an optional origin remote."""
38+
repo_path.mkdir(parents=True, exist_ok=True)
39+
subprocess.run(["git", "init", "-q", str(repo_path)], check=True)
40+
if remote_url:
41+
subprocess.run(
42+
["git", "-C", str(repo_path), "remote", "add", "origin", remote_url],
43+
check=True,
44+
)
45+
46+
3247
ADD_REPO_FIXTURES: list[AddRepoFixture] = [
3348
AddRepoFixture(
3449
test_id="simple-add-default-workspace",
@@ -297,3 +312,210 @@ def test_add_repo_creates_new_file(
297312

298313
assert "./" in config
299314
assert "newrepo" in config["./"]
315+
316+
317+
def test_add_repo_merges_duplicate_workspace_roots(
318+
tmp_path: pathlib.Path,
319+
monkeypatch: MonkeyPatch,
320+
caplog: t.Any,
321+
) -> None:
322+
"""Duplicate workspace roots are merged without losing repositories."""
323+
import yaml
324+
325+
caplog.set_level(logging.INFO)
326+
327+
monkeypatch.setenv("HOME", str(tmp_path))
328+
monkeypatch.chdir(tmp_path)
329+
330+
config_file = tmp_path / ".vcspull.yaml"
331+
config_file.write_text(
332+
(
333+
"~/study/python/:\n"
334+
" repo1:\n"
335+
" repo: git+https://example.com/repo1.git\n"
336+
"~/study/python/:\n"
337+
" repo2:\n"
338+
" repo: git+https://example.com/repo2.git\n"
339+
),
340+
encoding="utf-8",
341+
)
342+
343+
add_repo(
344+
name="pytest-docker",
345+
url="git+https://github.com/avast/pytest-docker.git",
346+
config_file_path_str=str(config_file),
347+
path=str(tmp_path / "study/python/pytest-docker"),
348+
workspace_root_path="~/study/python/",
349+
dry_run=False,
350+
)
351+
352+
with config_file.open() as fh:
353+
merged_config = yaml.safe_load(fh)
354+
355+
assert "~/study/python/" in merged_config
356+
repos = merged_config["~/study/python/"]
357+
assert set(repos.keys()) == {"repo1", "repo2", "pytest-docker"}
358+
359+
assert "Merged" in caplog.text
360+
361+
362+
class PathAddFixture(t.NamedTuple):
363+
"""Fixture describing CLI path-mode add scenarios."""
364+
365+
test_id: str
366+
remote_url: str | None
367+
assume_yes: bool
368+
prompt_response: str | None
369+
dry_run: bool
370+
expected_written: bool
371+
expected_url_kind: str # "remote" or "path"
372+
override_name: str | None
373+
expected_warning: str | None
374+
375+
376+
PATH_ADD_FIXTURES: list[PathAddFixture] = [
377+
PathAddFixture(
378+
test_id="path-auto-confirm",
379+
remote_url="https://github.com/avast/pytest-docker",
380+
assume_yes=True,
381+
prompt_response=None,
382+
dry_run=False,
383+
expected_written=True,
384+
expected_url_kind="remote",
385+
override_name=None,
386+
expected_warning=None,
387+
),
388+
PathAddFixture(
389+
test_id="path-interactive-accept",
390+
remote_url="https://github.com/example/project",
391+
assume_yes=False,
392+
prompt_response="y",
393+
dry_run=False,
394+
expected_written=True,
395+
expected_url_kind="remote",
396+
override_name="project-alias",
397+
expected_warning=None,
398+
),
399+
PathAddFixture(
400+
test_id="path-interactive-decline",
401+
remote_url="https://github.com/example/decline",
402+
assume_yes=False,
403+
prompt_response="n",
404+
dry_run=False,
405+
expected_written=False,
406+
expected_url_kind="remote",
407+
override_name=None,
408+
expected_warning=None,
409+
),
410+
PathAddFixture(
411+
test_id="path-no-remote",
412+
remote_url=None,
413+
assume_yes=True,
414+
prompt_response=None,
415+
dry_run=False,
416+
expected_written=True,
417+
expected_url_kind="path",
418+
override_name=None,
419+
expected_warning="Unable to determine git remote",
420+
),
421+
]
422+
423+
424+
@pytest.mark.parametrize(
425+
list(PathAddFixture._fields),
426+
PATH_ADD_FIXTURES,
427+
ids=[fixture.test_id for fixture in PATH_ADD_FIXTURES],
428+
)
429+
def test_handle_add_command_path_mode(
430+
test_id: str,
431+
remote_url: str | None,
432+
assume_yes: bool,
433+
prompt_response: str | None,
434+
dry_run: bool,
435+
expected_written: bool,
436+
expected_url_kind: str,
437+
override_name: str | None,
438+
expected_warning: str | None,
439+
tmp_path: pathlib.Path,
440+
monkeypatch: MonkeyPatch,
441+
caplog: t.Any,
442+
) -> None:
443+
"""CLI path mode prompts and adds repositories appropriately."""
444+
caplog.set_level(logging.INFO)
445+
446+
monkeypatch.setenv("HOME", str(tmp_path))
447+
monkeypatch.chdir(tmp_path)
448+
449+
repo_path = tmp_path / "study/python/pytest-docker"
450+
init_git_repo(repo_path, remote_url)
451+
452+
config_file = tmp_path / ".vcspull.yaml"
453+
454+
expected_input = prompt_response if prompt_response is not None else "y"
455+
monkeypatch.setattr("builtins.input", lambda _: expected_input)
456+
457+
args = argparse.Namespace(
458+
target=str(repo_path),
459+
url=None,
460+
override_name=override_name,
461+
config=str(config_file),
462+
path=None,
463+
workspace_root_path=None,
464+
dry_run=dry_run,
465+
assume_yes=assume_yes,
466+
)
467+
468+
handle_add_command(args)
469+
470+
log_output = caplog.text
471+
contracted_path = contract_user_home(repo_path)
472+
473+
assert "Found new repository to import" in log_output
474+
assert contracted_path in log_output
475+
476+
if dry_run:
477+
assert "skipped (dry-run)" in log_output
478+
479+
if assume_yes:
480+
assert "auto-confirm" in log_output
481+
482+
if expected_warning is not None:
483+
assert expected_warning in log_output
484+
485+
repo_name = override_name or repo_path.name
486+
487+
if expected_written:
488+
import yaml
489+
490+
assert config_file.exists()
491+
with config_file.open(encoding="utf-8") as fh:
492+
config_data = yaml.safe_load(fh)
493+
494+
workspace = "~/study/python/"
495+
assert workspace in config_data
496+
assert repo_name in config_data[workspace]
497+
498+
repo_entry = config_data[workspace][repo_name]
499+
expected_url: str
500+
if expected_url_kind == "remote" and remote_url is not None:
501+
cleaned_remote = remote_url.strip()
502+
expected_url = (
503+
cleaned_remote
504+
if cleaned_remote.startswith("git+")
505+
else f"git+{cleaned_remote}"
506+
)
507+
else:
508+
expected_url = str(repo_path)
509+
510+
assert repo_entry == {"repo": expected_url}
511+
else:
512+
if config_file.exists():
513+
import yaml
514+
515+
with config_file.open(encoding="utf-8") as fh:
516+
config_data = yaml.safe_load(fh)
517+
if config_data is not None:
518+
workspace = config_data.get("~/study/python/")
519+
if workspace is not None:
520+
assert repo_name not in workspace
521+
assert "Aborted import" in log_output

0 commit comments

Comments
 (0)