Skip to content
72 changes: 65 additions & 7 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from __future__ import annotations

import importlib.util
import os
import re
import select
Expand All @@ -36,6 +37,7 @@
import sys
import time
from argparse import Namespace
from collections.abc import Mapping, Sequence
from pathlib import Path

import pytest
Expand Down Expand Up @@ -65,7 +67,9 @@


def clone_just_pushed(remote: Path, dest: Path,
branch: str | None = None
branch: str | None = None, *, recurse: bool = True,
bare: bool = False, config: Sequence[str] = (),
env: Mapping[str, str] | None = None
) -> subprocess.CompletedProcess:
"""`git clone --recurse-submodules` of a bare remote a scaffold has JUST
pushed into, READ THROUGH GIT AND NOT THROUGH ITS FILES (#440).
Expand All @@ -90,18 +94,73 @@ def clone_just_pushed(remote: Path, dest: Path,
branch into the SOURCE, a plain repository, which is then `remote`.
The two legs it pushed into `--local-remote-dir` are read by the
rewrite only while that directory sits under `remote`'s parent.

The keywords are for the tests whose subject is the clone itself or the
environment it is made in (#544). `recurse=False` is a plain `git clone`,
which leaves a submodule's mount empty; `bare` is a bare copy of the
remote, with no submodules to read; `config` is more `-c` arguments for
the one command (`["-c", "core.autocrlf=true"]`); and `env` is variables
laid over this process's own, as `git_under` and `raw` lay them. The
output is decoded as UTF-8 whatever the platform's code page: a refusal
names a path, and a project's name can be any text.
"""
remote = Path(remote)
remotes = remote.parent.resolve()
uri = remotes.as_uri().rstrip("/") + "/"
prefixes = dict.fromkeys([os.path.join(str(remotes), ""),
remotes.as_posix().rstrip("/") + "/"])
rewrites = [arg for prefix in prefixes
for arg in ("-c", f"url.{uri}.insteadOf={prefix}")]
if bare:
mode = ["--bare"]
else:
mode = ["--recurse-submodules"] if recurse else []
return subprocess.run(
["git", *FILE_PROTOCOL, *rewrites, "clone", "-q", "--no-local",
*(["-b", branch] if branch else []),
"--recurse-submodules", str(remote), str(dest)],
capture_output=True, text=True, check=False)
["git", *FILE_PROTOCOL, *config, *rewrites, "clone", "-q",
"--no-local", *(["-b", branch] if branch else []), *mode,
str(remote), str(dest)],
capture_output=True, encoding="utf-8", errors="replace", check=False,
env=None if env is None else {**os.environ, **env})


def cloned_just_pushed(remote: Path, dest: Path, branch: str | None = None,
**options) -> Path:
"""`clone_just_pushed`, failing the test where the clone fails; `dest`.
`options` are the ones `clone_just_pushed` takes."""
cloned = clone_just_pushed(remote, dest, branch, **options)
assert cloned.returncode == 0, (f"git clone of {remote} failed:\n"
f"{cloned.stderr}{cloned.stdout}")
return Path(dest)


def load_without_bytecode(path: Path, name: str):
"""`path` run as a module called `name`, and no bytecode written for it
(#544; it is `load()` of `test_bootstrap_spelling.py`, #456, moved here).

`SourceFileLoader` caches what it compiles in a `__pycache__` beside the
source, and these sources are the TEMPLATES: a tree other tests read file
by file as text (`test_scaffold_leg_path_collisions.py` does, and a `.pyc`
is not UTF-8), and a tree a materializer walks. `.gitignore` hides the
stray, so `git status` never says it is there. The flag is process-wide, so
it is restored; it also covers the `repo_shape` a root imports as it runs.
"""
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
wrote_bytecode = sys.dont_write_bytecode
sys.dont_write_bytecode = True
try:
spec.loader.exec_module(module)
finally:
sys.dont_write_bytecode = wrote_bytecode
return module


#: `load_without_bytecode`'s counterpart for a CHILD process (#544). `family.py
#: siblings` runs the standard's `templates/family-root/scripts/siblings.py`
#: through `runpy`, and that file imports `bootstrap` from beside it, so
#: without this in the child's environment the import writes
#: `__pycache__/bootstrap.*.pyc` into the template tree.
NO_BYTECODE = {"PYTHONDONTWRITEBYTECODE": "1"}


#: THE DEFAULT IDENTITY `git()` GIVES GIT (#279). A commit needs an author and
Expand Down Expand Up @@ -396,8 +455,7 @@ def scaffolded(tmp_path_factory) -> dict:
)
assert result.returncode == 0, result.stderr + result.stdout
clones.mkdir()
proc = clone_just_pushed(remotes / f"{PROJECT}.git", clones / PROJECT)
assert proc.returncode == 0, proc.stderr
cloned_just_pushed(remotes / f"{PROJECT}.git", clones / PROJECT)
return {
"base": base, "remotes": remotes, "work": work,
"clone": clones / PROJECT, "stdout": result.stdout,
Expand Down
10 changes: 4 additions & 6 deletions tests/test_adopt_clone_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@

import pytest

from conftest import (ADOPT, FILE_PROTOCOL, REPO, git, make_source_repo,
resolve, run_script, write_plan)
from conftest import (ADOPT, FILE_PROTOCOL, REPO, cloned_just_pushed, git,
make_source_repo, resolve, run_script, write_plan)

sys.path.insert(0, str(REPO / "scripts"))
from shape_materialize import SHAPE_REPOSITORY # noqa: E402
Expand Down Expand Up @@ -339,10 +339,8 @@ def test_a_clean_recursive_clone_succeeds_and_the_projects_validators_pass(
not contain` the planted commit (exit 128). `bootstrap.py` is the last of
the validators: it places each leg AT its pin, so it fails where a gitlink
and a pin disagree."""
clone = tmp_path / PROJECT
git_under(adopted.clean, *FILE_PROTOCOL, "clone", "-q",
"--recurse-submodules", "-b", BRANCH, str(adopted.source),
str(clone), cwd=tmp_path)
clone = cloned_just_pushed(adopted.source, tmp_path / PROJECT, BRANCH,
env=adopted.clean)
for role in ROLES:
assert git_under(adopted.clean, "rev-parse", "HEAD", cwd=clone / role) \
== pushed_tip(adopted.remotes, role)
Expand Down
23 changes: 7 additions & 16 deletions tests/test_adopt_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import pytest

from conftest import (ADOPT, FILE_PROTOCOL, REPO, git, make_source_repo,
from conftest import (ADOPT, REPO, cloned_just_pushed, git, make_source_repo,
resolve, run_script, write_plan)

sys.path.insert(0, str(REPO / "scripts"))
Expand Down Expand Up @@ -61,13 +61,8 @@ def adopted(tmp_path_factory) -> dict:
@pytest.fixture
def adopted_clone(adopted, tmp_path):
"""A recursive clone of the source at the split branch."""
target = tmp_path / PROJECT
proc = subprocess.run(
["git", *FILE_PROTOCOL, "clone", "-q", "--recurse-submodules",
"-b", "adopt/three-repo-shape", str(adopted["source"]), str(target)],
capture_output=True, text=True, check=False)
assert proc.returncode == 0, proc.stderr
return target
return cloned_just_pushed(adopted["source"], tmp_path / PROJECT,
"adopt/three-repo-shape")


# --- what execute refuses --------------------------------------------------
Expand Down Expand Up @@ -122,10 +117,8 @@ def test_both_legs_exist_with_more_than_one_commit(adopted):
def test_a_files_history_is_followable_inside_its_leg(adopted, tmp_path):
"""The point of `git filter-repo`: the commits that made a file come with
it. The specification was edited in a second commit; the leg has both."""
clone = tmp_path / "spec-leg"
subprocess.run(["git", "clone", "-q",
str(adopted["remotes"] / f"{PROJECT}-spec.git"),
str(clone)], check=True)
clone = cloned_just_pushed(adopted["remotes"] / f"{PROJECT}-spec.git",
tmp_path / "spec-leg")
log = git("log", "--oneline", "--", "specs/001-feature/spec.md",
cwd=clone).stdout.splitlines()
assert len(log) == 2, f"expected two commits for the spec file, got {log}"
Expand All @@ -139,10 +132,8 @@ def test_a_files_history_is_followable_inside_its_leg(adopted, tmp_path):
def test_the_legs_keep_the_paths_they_had_in_the_source(adopted, tmp_path):
"""Paths are NOT flattened: `scripts/validate.py` stays there, which is
what makes `../spec/contracts` from `code/` the correct read-across."""
clone = tmp_path / "code-leg"
subprocess.run(["git", "clone", "-q",
str(adopted["remotes"] / f"{PROJECT}-code.git"),
str(clone)], check=True)
clone = cloned_just_pushed(adopted["remotes"] / f"{PROJECT}-code.git",
tmp_path / "code-leg")
assert (clone / "src" / "app" / "main.py").is_file()
assert (clone / "tests" / "test_main.py").is_file()
assert not (clone / "specs").exists()
Expand Down
22 changes: 19 additions & 3 deletions tests/test_adopt_execute_refusals.py
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,13 @@ def a_whole_pin(product: str) -> str:
#: then passes. A `paths:` with no entries is valid as a TYPE and leaves every
#: source path uncovered, which is `plan-uncovered`'s to say, as it was. A
#: `pins:` is a list of mappings, as `plan` writes it since #273.
#:
#: A TEXT in the third place is what `check` says instead, because the key
#: is read as it always was (#544, #435's `source is None` and `legs is None`
#: arms): `source:` bare or absent is `plan-no-source`, a refusal, exit 2, and
#: `legs:` bare is the names' own `naming-role-mismatch`, a finding, exit 1.
#: Neither is `plan-bad-source` or `plan-bad-legs`, which say a value is the
#: wrong TYPE, and a bare key has no value at all.
GOOD_KEYS = {
"pins-empty": ("pins", "pins: []\n", True),
"pins-bare": ("pins", "pins:\n", True),
Expand All @@ -1029,24 +1036,33 @@ def a_whole_pin(product: str) -> str:
"allow-absent": ("allow_empty_legs", "", True),
"allow-both": ("allow_empty_legs", "allow_empty_legs: [spec, code]\n",
True),
"source-bare": ("source", "source:\n", "REFUSED plan-no-source"),
"source-absent": ("source", "", "REFUSED plan-no-source"),
"legs-bare": ("legs", "legs:\n", "FINDING naming-role-mismatch"),
"paths-empty": ("paths", "paths: []\n", False),
"paths-bare": ("paths", "paths:\n", False),
"paths-absent": ("paths", "", False),
}


@pytest.mark.parametrize("edit", sorted(GOOD_KEYS))
def test_a_list_an_empty_list_a_bare_key_and_no_key_are_not_findings(
def test_a_list_an_empty_list_a_bare_key_and_no_key_are_not_plan_bad_findings(
adopter, world, edit):
"""#386 (d): what `plan` writes, and the ways of writing none, are not
findings of this kind; a plan with no entries is `plan-uncovered`, as it
was, and never `plan-bad-paths`."""
was, and never `plan-bad-paths`. A `source:` or `legs:` left bare, or no
`source:` at all, is read as it always was too (#544), never as the wrong
type."""
key, block, passes = GOOD_KEYS[edit]
text = with_key(world.answered, key, block)
checked = run_check(write_edited(world, text))
assert_no_traceback(checked)
assert "FINDING plan-bad-" not in checked.stderr, checked.stderr
if passes:
if isinstance(passes, str):
assert f"{passes}: " in checked.stderr, checked.stderr
assert checked.returncode == (2 if passes.startswith("REFUSED")
else 1), checked.stderr
elif passes:
assert checked.returncode == 0, checked.stderr + checked.stdout
plan, source = open_plan(adopter, world, text)
assert adopter._refuse_what_check_finds(plan, source) is None
Expand Down
13 changes: 5 additions & 8 deletions tests/test_adopt_gitmodules_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@

import pytest

from conftest import ADOPT, FILE_PROTOCOL, GITLINK, GITMODULES, as_human, \
exported_in_a_clone, findings, git, in_the_history, link, \
make_source_repo, planned, put_gitmodules, refused_before_any_leg, \
from conftest import ADOPT, GITLINK, GITMODULES, as_human, \
cloned_just_pushed, exported_in_a_clone, findings, git, in_the_history, \
link, make_source_repo, planned, put_gitmodules, refused_before_any_leg, \
run_script

needs_filter_repo = pytest.mark.skipif(
Expand Down Expand Up @@ -454,10 +454,7 @@ def test_a_refused_value_git_never_reads_adopts_and_clones(tmp_path, where):
adopted = execute_in(plan, tmp_path)
assert adopted.returncode == 0, adopted.stderr + adopted.stdout
assert "adoption verified" in adopted.stdout
clone = tmp_path / "clone"
cloned = git(*FILE_PROTOCOL, "clone", "-q", "--recurse-submodules", "-b",
"adopt/three-repo-shape", str(source), str(clone),
cwd=tmp_path, check=False)
assert cloned.returncode == 0, cloned.stderr
clone = cloned_just_pushed(source, tmp_path / "clone",
"adopt/three-repo-shape")
assert (clone / "code" / GITMODULES).read_bytes() \
== with_line("update = bogus").encode()
23 changes: 8 additions & 15 deletions tests/test_adopt_ignored_shape_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@

import pytest

from conftest import (ADOPT, FILE_PROTOCOL, REPO, clear_ambient_pin_sources,
git, make_source_repo, resolve, run_script, write_plan)
from conftest import (ADOPT, REPO, clear_ambient_pin_sources,
cloned_just_pushed, git, make_source_repo, resolve,
run_script, write_plan)

sys.path.insert(0, str(REPO / "scripts"))
from repo_shape import parse_yaml # noqa: E402
Expand Down Expand Up @@ -559,9 +560,7 @@ def test_the_split_lands_the_files_the_pin_names(adopted):
@needs_filter_repo
def test_the_assembly_bootstraps_where_it_failed_with_shape_copy_missing(
adopted, tmp_path):
clone = tmp_path / PROJECT
git(*FILE_PROTOCOL, "clone", "-q", "--recurse-submodules", "-b", BRANCH,
str(adopted.source), str(clone), cwd=tmp_path)
clone = cloned_just_pushed(adopted.source, tmp_path / PROJECT, BRANCH)
bootstrapped = run_script(clone / "scripts" / "bootstrap.py", cwd=clone)
output = bootstrapped.stderr + bootstrapped.stdout
assert "shape-copy-missing" not in output, output
Expand Down Expand Up @@ -601,13 +600,8 @@ def test_a_checkout_that_writes_crlf_still_bootstraps(adopted, tmp_path, key,
shape copy, its `* text=auto` alone governs it, and it IS checked out
with CRLF, so the setting reached the checkout and the pass means what
it says."""
clone = tmp_path / PROJECT
cloned = subprocess.run(
["git", *FILE_PROTOCOL, "clone", "-q", "--recurse-submodules", "-b",
BRANCH, str(adopted.source), str(clone)], cwd=str(tmp_path),
env=command_scope(key, value), capture_output=True, text=True,
check=False)
assert cloned.returncode == 0, cloned.stderr
clone = cloned_just_pushed(adopted.source, tmp_path / PROJECT, BRANCH,
env=command_scope(key, value))
if adopted.own:
assert b"\r\n" in (clone / README).read_bytes(), \
"this clone did not write CRLF, so it tests nothing"
Expand Down Expand Up @@ -752,9 +746,8 @@ def test_a_root_kept_symlink_the_shape_writes_through_adopts(
def test_the_assembly_with_a_root_kept_symlink_bootstraps(
adopted_through_a_link, tmp_path):
assert adopted_through_a_link.result.returncode == 0
clone = tmp_path / PROJECT
git(*FILE_PROTOCOL, "clone", "-q", "--recurse-submodules", "-b", BRANCH,
str(adopted_through_a_link.source), str(clone), cwd=tmp_path)
clone = cloned_just_pushed(adopted_through_a_link.source,
tmp_path / PROJECT, BRANCH)
bootstrapped = run_script(clone / "scripts" / "bootstrap.py", cwd=clone)
output = bootstrapped.stderr + bootstrapped.stdout
assert bootstrapped.returncode == 0, output
Expand Down
12 changes: 6 additions & 6 deletions tests/test_adopt_mount_ignored.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
import pytest

from conftest import (ADOPT, DEFAULT_EDITS, FILE_PROTOCOL, SYNTHETIC_TREE,
git, make_source_repo, resolve, run_script, write_plan)
cloned_just_pushed, git, make_source_repo, resolve,
run_script, write_plan)

#: What the end-to-end tests need and the mount tests do not. Spelled here
#: rather than imported from another test module, so that the files do not
Expand Down Expand Up @@ -193,9 +194,9 @@ def head(repo: Path) -> str:
return git("rev-parse", "HEAD", cwd=repo).stdout.strip()


def clone(source: Path, destination: Path, *options: str) -> None:
git(*FILE_PROTOCOL, "clone", "-q", *options, str(source),
str(destination), cwd=destination.parent)
def clone(source: Path, destination: Path) -> None:
git(*FILE_PROTOCOL, "clone", "-q", str(source), str(destination),
cwd=destination.parent)


def assembly_of(source: Path, tmp_path: Path) -> Path:
Expand Down Expand Up @@ -621,8 +622,7 @@ def test_the_assembly_whose_gitignore_hides_its_mounts_bootstraps(adopted,
"""With #167's forced staging of the shape's own files, nothing else
stands between this source and a working assembly."""
assert adopted.result.returncode == 0, adopted.result.stderr
checkout = tmp_path / PROJECT
clone(adopted.source, checkout, "--recurse-submodules", "-b", BRANCH)
checkout = cloned_just_pushed(adopted.source, tmp_path / PROJECT, BRANCH)
bootstrapped = run_script(checkout / "scripts" / "bootstrap.py",
cwd=checkout)
output = bootstrapped.stderr + bootstrapped.stdout
Expand Down
20 changes: 6 additions & 14 deletions tests/test_adopt_pins.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@

import pytest

from conftest import (ADOPT, FILE_PROTOCOL, REPO, SCAFFOLD, SYNTHETIC_TREE,
WINDOWS_SKIP, as_human, findings, git, make_source_repo,
planned, refused_before_any_leg, resolve, rmtree,
run_script)
from conftest import (ADOPT, REPO, SCAFFOLD, SYNTHETIC_TREE, WINDOWS_SKIP,
as_human, cloned_just_pushed, findings, git,
make_source_repo, planned, refused_before_any_leg,
resolve, rmtree, run_script)

sys.path.insert(0, str(REPO / "scripts"))
import shape_materialize # noqa: E402
Expand Down Expand Up @@ -231,11 +231,7 @@ def adopted(request, tmp_path_factory, glass):
"--work-dir", str(base / "work"), env=env)
clone = base / "clone" / PROJECT
if executed.returncode == 0:
proc = subprocess.run(
["git", *FILE_PROTOCOL, "clone", "-q", "--recurse-submodules",
"-b", BRANCH, str(source), str(clone)],
capture_output=True, check=False)
assert proc.returncode == 0, proc.stderr.decode(UTF8, "replace")
cloned_just_pushed(source, clone, BRANCH)
return {"base": base, "source": source, "plan": plan, "env": env,
"repository": repository, "commit": commit,
"readable": readable, "planned": planned_run,
Expand Down Expand Up @@ -372,11 +368,7 @@ def adopted_as_text(request, tmp_path_factory):
"--work-dir", str(base / "work"), env=env)
clone = base / "clone" / TEXT_PROJECT
if executed.returncode == 0:
proc = subprocess.run(
["git", *FILE_PROTOCOL, "clone", "-q", "--recurse-submodules",
"-b", BRANCH, str(source), str(clone)],
capture_output=True, check=False)
assert proc.returncode == 0, proc.stderr.decode(UTF8, "replace")
cloned_just_pushed(source, clone, BRANCH)
return {"product": product, "spelled": spelled, "commit": commit,
"readable": readable, "source": source, "plan": plan, "env": env,
"checked": checked, "executed": executed, "clone": clone}
Expand Down
Loading
Loading