Skip to content
30 changes: 26 additions & 4 deletions shape-doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3342,8 +3342,9 @@ def refusals_ahead(ctx: Context, us) -> dict | None:
NONE WHERE THE QUESTION CANNOT BE ASKED: the pin does not read, the
checkout of the standard is a partial clone short of what the
comparison reads, where `prepare` would fetch (`partial_clone_row`,
#683), or `prepare` refuses. The `shape currency` row says which, and
the `pins` row keeps its gate.
#683), or `prepare` refuses or cannot read what it asks, the root's own
`source_repository:` among its reasons (#1203). The `shape currency` row
says which of the others, and the `pins` row keeps its gate.
"""
try:
pin, _rows = us.read_pin(ctx.root)
Expand All @@ -3354,9 +3355,24 @@ def refusals_ahead(ctx: Context, us) -> dict | None:
args = argparse.Namespace(root=str(ctx.root), upstream=str(ctx.shape),
at=None)
try:
# THE PIN'S `source_repository:` IS ASKED HERE, unlike in
# `check_shape_currency` (#1203, the review of PR #1269, S2): the
# `pins` row says `update-shape.py check` reports the copies
# `merged-away` and the `apply` it prints drops their rows, and for
# a root whose field that `check` refuses it exits 2 and does
# neither. A refusal here is "cannot ask", below, and the row keeps
# the gate's finding.
#
# AN `OSError` OF THAT ASK IS "CANNOT ASK" TOO (NIT B of the second
# review of PR #1269): `pin_source` stats the value as a path from
# the directory the doctor runs in, and a stat the system refuses
# (`locked/x` with `locked` unsearchable) is raised out of `prepare`
# as the `update-unreadable` that `check` names. Caught as
# `Refusal` alone it left here, and the row read `could not be run:
# [Errno 13] ...` where it had kept the gate's finding.
root, _pin, rows, _added, upstream, _pinned, _target, kind = \
us.prepare(args)
except Refusal:
except (Refusal, OSError):
return None
try:
ahead = {row.path: row.state for row in rows if row.is_conflict}
Expand Down Expand Up @@ -6421,8 +6437,14 @@ def check_shape_currency(ctx: Context) -> Row:
if partial is not None:
return partial
try:
# THE PIN'S `source_repository:` IS NOT ASKED HERE (#1203). A
# `Refusal` out of `prepare` is this checkout unable to answer
# (below), and that field is the root's: asked here, a hand edit of
# it was CANNOT ANSWER with a `fetch --all` that mends nothing.
# `update-shape.py check`, this row's NEXT where the root is behind,
# asks it, in the pin's name.
_root, _pin, rows, additions, upstream, pinned, target, _kind = \
us.prepare(args)
us.prepare(args, ask_source=False)
except Refusal as exc:
if exc.code == us.UPSTREAM_UNREADABLE:
# A READ THAT FAILED, NOT A COMMIT THAT IS NOT HERE (#683), so
Expand Down
2 changes: 1 addition & 1 deletion tests/test_doctor_unreadable_pin.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ def test_an_oserror_the_root_did_not_refuse_is_raised_as_it_was(
module, ctx = in_process(standard, project)
error = raised(standard, ctx.root)

def prepare(_args):
def prepare(_args, **_ask_source):
raise error

monkeypatch.setattr(ctx.update_shape(), "prepare", prepare)
Expand Down
108 changes: 107 additions & 1 deletion tests/test_update_shape_merged_away.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from conftest import REPO, WINDOWS_SKIP, clone_just_pushed, git, \
make_source_repo, run_script
from test_shape_doctor import ( # noqa: F401
commit_all, doctor, keep_as_committed_link, rows_of, standard,
DOCTOR, commit_all, doctor, keep_as_committed_link, rows_of, standard,
standard_ahead, standard_of_another_history, verdict_line,
)
#: A partial clone of the standard one commit on, from the file that owns
Expand All @@ -62,6 +62,13 @@
PROJECT = "Medx"
SHAPE_PIN = "contracts/shape-pin.yaml"

#: A mode that refuses the owner is a POSIX mode, and root reads through every
#: one (the mark of `tests/test_bump_leg_unopenable.py`).
needs_a_mode = pytest.mark.skipif(
sys.platform == "win32" or (hasattr(os, "geteuid") and os.geteuid() == 0),
reason="a mode that refuses the owner is a POSIX mode, and root reads "
"through every one (tests/test_bump_leg_unopenable.py)")

#: The issue's own source, with a `Makefile` beside its `.gitignore`: two
#: names the shape copies too, so the adoption writes both of its copies
#: under `shape/` and pins them there. `README.md` collides as well, and is
Expand Down Expand Up @@ -506,6 +513,20 @@ def assert_the_pins_row_is_the_gate(pins_row: dict) -> None:
assert "update-shape.py" not in pins_row["next"], pins_row["next"]


def source_written_as(root: Path, value: str) -> None:
"""The pin's `source_repository:` written double-quoted as `value`, as
a hand edit makes it, and committed (#1203)."""
pin = root / SHAPE_PIN
text = pin.read_text(encoding="utf-8")
edited, edits = re.subn(r"(?m)^source_repository: .*$",
f'source_repository: "{value}"', text)
assert edits == 1, text
pin.write_bytes(edited.encode("utf-8"))
git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "-m",
"The pin's source, as a hand edit wrote it", "--", SHAPE_PIN,
cwd=root)


def test_the_pins_row_keeps_the_gate_where_check_refuses_the_standard(
standard, root, tmp_path):
"""Run from a standard of another history, which does not carry the
Expand All @@ -522,6 +543,91 @@ def test_the_pins_row_keeps_the_gate_where_check_refuses_the_standard(
rows_of(doctor(stranger, root, "--json"))["pins"])


def test_the_pins_row_keeps_the_gate_where_check_refuses_the_pins_source(
standard, root):
"""Run over two collision copies merged away AND a pin whose
`source_repository:` is a hand edit (#1203, the review of PR #1269, S2).
`update-shape.py check` asks that field before it classifies a row and
refuses `update-upstream-unresolvable`, exit 2, in the pin's name: it
reports no copy `merged-away`, and the `apply` that would drop their
rows never starts. So `refusals_ahead` cannot ask its question here, as
where `check` refuses the standard, and the `pins` row keeps its gate.
With the field left unasked there (`ask_source=False`) the row said that
`check` reports them `merged-away` and that the `apply` it prints drops
their pin rows, and its NEXT was that `check`.

The `shape currency` row does not ask the field, and answers as it did:
it counts the two rows `merged-away`, exit 1 for the verdict, with no
CANNOT ANSWER. The scaffolded value, with the same two copies, is
`test_a_merged_away_copys_row_goes_and_the_project_is_green_again`: the
`pins` row names `check` and `merged_away` there."""
merge_away(root, *MERGED.values())
source_written_as(root, "has space/x")
checked = update_shape(standard, root, "check")
assert checked.returncode == 2, checked.stdout + checked.stderr
assert checked.stderr.startswith(
"REFUSED update-upstream-unresolvable: "), checked.stderr
assert "merged-away" not in checked.stdout, checked.stdout

result = doctor(standard, root, "--json")
rows = rows_of(result)
assert_the_pins_row_is_the_gate(rows["pins"])
currency = rows["shape-currency"]
assert currency["status"] == "FINDING", currency
assert "environment" not in currency["detail"], currency
assert currency["detail"]["counts"]["merged-away"] == 2, currency
assert result.returncode == 1, result.stdout + result.stderr


@needs_a_mode
def test_the_pins_row_keeps_the_gate_where_the_pins_source_cannot_be_read(
standard, root, tmp_path):
"""Run over two collision copies merged away AND a pin whose
`source_repository:` is `locked/x`, a folder of the directory the doctor
runs in that this user may not search (NIT B of the second review of PR
#1269). `pin_source` stats the value as a path, the stat is refused
`Permission denied`, and the `OSError` is raised out of `prepare`:
`update-shape.py check` names it `update-unreadable`, exit 2, and
reports no copy `merged-away`. `refusals_ahead` caught `Refusal` only,
so the error left it and the `pins` row read `could not be run: [Errno
13] ...`, where it kept the gate's finding before the field was asked.
It cannot ask its question here, as where `check` refuses the field in
the pin's name, and the `pins` row keeps its gate.

The `shape currency` row does not ask the field, and answers as it did:
it counts the two rows `merged-away`, with no CANNOT ANSWER. The mode is
given back in a `finally`."""
merge_away(root, *MERGED.values())
source_written_as(root, "locked/x")
here = tmp_path / "here"
locked = here / "locked"
locked.mkdir(parents=True)
locked.chmod(0)
try:
checked = run_script(standard / "update-shape.py", "check", "--root",
str(root), "--upstream", str(standard),
cwd=here)
result = run_script(standard / DOCTOR, "--root", str(root), "--json",
cwd=here)
finally:
locked.chmod(0o755)
assert checked.returncode == 2, checked.stdout + checked.stderr
assert checked.stderr.startswith(
"REFUSED update-unreadable: locked/x: cannot be read "), \
checked.stderr
assert "merged-away" not in checked.stdout, checked.stdout

rows = rows_of(result)
assert "could not be run" not in rows["pins"]["reason"], rows["pins"]
assert "shape-copy-missing" in rows["pins"]["reason"], rows["pins"]
assert_the_pins_row_is_the_gate(rows["pins"])
currency = rows["shape-currency"]
assert currency["status"] == "FINDING", currency
assert "environment" not in currency["detail"], currency
assert currency["detail"]["counts"]["merged-away"] == 2, currency
assert result.returncode == 1, result.stdout + result.stderr


#: The pinned copy the `both` and `upstream-removed` cases move upstream,
#: and the file the `unmapped` case pins that no upstream path maps to.
MOVED_COPY = "AGENTS-shape.md"
Expand Down
Loading
Loading