diff --git a/docs/cli.md b/docs/cli.md index 6211f0d4..369c4781 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -940,7 +940,9 @@ usage: validate-repository-naming.py [-h] [--policy POLICY] [--pins PINS] [--referent-chain REFERENT_CHAIN] [--link-source [PRODUCT=]PATH] - [--explain] [--quiet] [names ...] + [--link-source-label FLAG] + [--link-remedy LINK=TEXT] [--explain] + [--quiet] [names ...] Classify repository names against `contracts/repository-naming.yaml`. @@ -993,6 +995,17 @@ options: declared-unverified, and a source that cannot be read is named with the system's reason: both are warnings and not findings, an explicit source included. + --link-source-label FLAG + the caller's own name for the bare --link-source it + passes, such as --pin-source (a FLAG that starts with + - is given after an =): the link-source-unreadable + WARNING and the not-used NOTE name that value by FLAG, + and neither says a value holding = was read as a PATH + --link-remedy LINK=TEXT + the caller's words for what would let LINK's tree be + read, offered in place of this script's own where that + link is declared-unverified. Repeatable; the last + value for a link wins. --explain print every family each name satisfies, why one of them won, and what the others are recorded as --quiet diff --git a/scaffold-project.py b/scaffold-project.py index cf72e7af..2806c24d 100755 --- a/scaffold-project.py +++ b/scaffold-project.py @@ -88,11 +88,6 @@ VISIBILITY_CHOICES, NamingPolicy, Refusal, accepts_role, checked_value, git_out, link_pins_from_trees, repo_basename, shell_line, tree_digest, ) -# The variable a chain link's tree is read through, and which links share -# one, for the sources the scaffold offers a link it could not read (#610). -from repo_shape import ( # noqa: E402 - pin_product_key, pin_source_env_name, pin_source_env_shared, -) from shape_materialize import ( # noqa: E402 RULESET_HINT, SHAPE_REPOSITORY, CommandFailed, copy_tree, default_reference, descendant_note, election_date, env_commit, @@ -117,6 +112,11 @@ # Whether `--elected-by`, `--reference` and `--name` are text a file can # hold: the question `adopt-project.py` asks of the same values (#282). from shape_materialize import unwritable_reason # noqa: E402 +# The words a chain link that is not reachable is offered: what THIS tool +# reads (#610), which setup's step (4) hands the naming gate too (#661). +from shape_materialize import ( # noqa: E402 + scaffold_link_remedies as _link_remedies, +) # The shape half of the leg-path collision rule, which `adopt-project.py` # asks of its own mount paths (#172), imported as one unit (#197), with the # one spelling of the file a `--pin` adds to what the shape writes. @@ -1054,48 +1054,6 @@ def _declaration(args) -> tuple[dict, set, tuple, dict]: return pins, declared_pins, chain, link_pins -def _link_remedies(chain: tuple) -> dict[str, str]: - """What would let each chain link's own tree be read, in the scaffold's - own sources: `{pin_product_key(link): the words}` for the - `declared-unverified` WARNING about a tree that is not reachable (#610). - - That warning offered "a sibling checkout, SHAPE_PIN_SOURCE_, or - --link-source =": the naming validator's three, and two of - them the scaffold does not have. It has no `--link-source`, and it reads - no checkout beside anything (`link_pins_from_trees(chain, None, ...)`). - It reads `SHAPE_PIN_SOURCE_` for every link, unless another link - of the chain shares the variable (#275), and its `--pin-source` for the - FIRST link alone (#441), the pinned product's clone. A later link that - shares its variable has neither, which only a hand-written chain can - hold, and the warning says so. - - Keyed by `pin_product_key`, the key `_chain_link_findings` finds a - holder's entry by (#444). Keyed by `casefold()`, `openß` and `openss`, - two links of one chain, wrote one entry, and the first link was offered - the later one's words. - """ - links = [repo_basename(link) for link in chain] - shared = pin_source_env_shared(links) - remedies: dict[str, str] = {} - for index, link in enumerate(links[:-1]): - key = pin_product_key(link) - sources = [] if key in shared else [pin_source_env_name(link)] - if not index: - sources.append(f"a --pin-source clone of {link} that holds its " - "project.yaml") - if not sources: - remedies[key] = ( - "the scaffold reads no source for it: " - f"{pin_source_env_name(link)} is another link's too, and " - "--pin-source is the first link's tree alone") - elif len(sources) == 1: - remedies[key] = f"{sources[0]} would let it be read" - else: - remedies[key] = (f"{sources[0]}, or {sources[1]}, " - "would let it be read") - return remedies - - def _check_referent_chain(policy, assembly_name: str, declared_pins: set, chain: tuple, link_pins: dict) -> None: """(d) A declared `--referent-chain` reaches the referent the name claims, diff --git a/scripts/shape_materialize.py b/scripts/shape_materialize.py index 053554eb..e17f6580 100644 --- a/scripts/shape_materialize.py +++ b/scripts/shape_materialize.py @@ -64,9 +64,13 @@ # which products share a `SHAPE_PIN_SOURCE_` variable, the one answer # the project's own `validate-pins.py` reports on (#275), with two # declarations ONE product when they share a pin file (`pin_product_key`). +# `scaffold_link_remedies` reads a chain with the same names: each link's +# variable (`pin_source_env_name`), whether another link shares it +# (`pin_source_env_shared`), and the link's name (`repo_basename`) (#661). from repo_shape import ( # noqa: E402 GH_TREE_API, NEUTRAL_PRODUCT_OWNER, PIN_SOURCE_ENV_PREFIX, - pin_product_key, pin_source_env_collisions, tree_digest, + pin_product_key, pin_source_env_collisions, pin_source_env_name, + pin_source_env_shared, repo_basename, tree_digest, ) # How a `--pin-source` that is not printable is named, the one spelling the # scaffold's chain WARNING gives it too (#610). @@ -2926,6 +2930,48 @@ def pin_digest_from_source(source: Path, commit: str, repository: str) -> str: ) from exc +def scaffold_link_remedies(chain) -> dict[str, str]: + """What would let each chain link's own tree be read, in the scaffold's + own sources: `{pin_product_key(link): the words}` for the + `declared-unverified` WARNING about a tree that is not reachable (#610). + + That warning offered "a sibling checkout, SHAPE_PIN_SOURCE_, or + --link-source =": the naming validator's three, and two of + them the scaffold does not have. It has no `--link-source`, and it reads + no checkout beside anything (`link_pins_from_trees(chain, None, ...)`). + It reads `SHAPE_PIN_SOURCE_` for every link, unless another link + of the chain shares the variable (#275), and its `--pin-source` for the + FIRST link alone (#441), the pinned product's clone. A later link that + shares its variable has neither, which only a hand-written chain can + hold, and the warning says so. + + Keyed by `pin_product_key`, the key `_chain_link_findings` finds a + holder's entry by (#444). Keyed by `casefold()`, `openß` and `openss`, + two links of one chain, wrote one entry, and the first link was offered + the later one's words. + """ + links = [repo_basename(link) for link in chain] + shared = pin_source_env_shared(links) + remedies: dict[str, str] = {} + for index, link in enumerate(links[:-1]): + key = pin_product_key(link) + sources = [] if key in shared else [pin_source_env_name(link)] + if not index: + sources.append(f"a --pin-source clone of {link} that holds its " + "project.yaml") + if not sources: + remedies[key] = ( + "the scaffold reads no source for it: " + f"{pin_source_env_name(link)} is another link's too, and " + "--pin-source is the first link's tree alone") + elif len(sources) == 1: + remedies[key] = f"{sources[0]} would let it be read" + else: + remedies[key] = (f"{sources[0]}, or {sources[1]}, " + "would let it be read") + return remedies + + def shown_source(source) -> str: """A `--pin-source` path as a refusal names it: as it is, unless a character in it is not printable (`str.isprintable`), and then as diff --git a/scripts/validate-repository-naming.py b/scripts/validate-repository-naming.py index cd16aa5e..fcd5a1c7 100755 --- a/scripts/validate-repository-naming.py +++ b/scripts/validate-repository-naming.py @@ -65,6 +65,22 @@ empty one — is refused (#442): an empty path is the current directory, and `.` is how to mean that. +A CALLER THAT PASSES A SOURCE FOR SOMEONE ELSE SAYS IT IN ITS OWN WORDS +(#661). `--link-source-label=FLAG` is the flag the person typed for the bare +PATH (setup's step (4) passes `--pin-source`): the `link-source-unreadable` +WARNING and the "not used" NOTE name that value by FLAG, and neither says a +value holding `=` was read as a PATH. The `link-source-no-path` REFUSED +line, and the NOTE's advice to name the link (`--link-source =PATH`) +where the chains begin at more than one link, keep this script's own flag. +`--link-remedy LINK=TEXT` is what would let LINK's tree be read in that +caller's sources, offered in place of this script's own for a link that is +declared-unverified. A project's own runs pass neither. Every `--link-source` +value and path this prints, and the path of a `SHAPE_PIN_SOURCE_` +or of a checkout beside the project it names, is named as it is, or as +Python's `repr` spells it where a character in it is not printable +(`_shown`). A caller's FLAG and the words it offers for a link, a chain +link's name, and the `--project` and `--policy` paths are printed as given. + EXIT CODES, the same three in every validator this standard ships: 0 every name classified (and, with `--project`, every declared role and the topic agreed with the classification) @@ -94,6 +110,7 @@ from __future__ import annotations import argparse +import inspect import os import sys from pathlib import Path @@ -121,6 +138,30 @@ def _shown(path) -> str: return text if text.isprintable() else repr(text) +def _library_takes_remedies() -> bool: + """Does this root's `scripts/repo_shape.py` take `remedies=`? The + skew rule `validate-manifest.py` states (#661): an older library has + none, and passing it would end the gate in a `TypeError`.""" + try: + return "remedies" in inspect.signature( + link_pins_from_trees).parameters + except (TypeError, ValueError): + return False + + +def _link_remedy(value: str) -> tuple[str, str]: + """One `--link-remedy LINK=TEXT`: the link, by `repo_basename`, and + the words. The text before the first `=` is the link; a value with no + `=`, no link or no words is refused, exit 2.""" + link, sep, text = value.partition("=") + link = repo_basename(link.strip()) + if not (sep and link and text.strip()): + raise argparse.ArgumentTypeError( + f"{_shown(value)} is not LINK=TEXT: name a chain link, then `=`, " + "then the words for what would let its tree be read") + return link, text + + class Target: """One name to classify, with everything DECLARED about it. @@ -203,7 +244,15 @@ class LinkSources: products' trees. """ - def __init__(self, values: list[str], links, firsts=()) -> None: + def __init__(self, values: list[str], links, firsts=(), + label: str | None = None, remedies=()) -> None: + #: The caller's own name for the bare PATH it passes, or None + #: (`--link-source-label`, #661). + self.label = label + #: `{pin_product_key(link): the caller's words}` from + #: `--link-remedy`, the last value given for a link wins (#661). + self.remedies: dict[str, str] = { + pin_product_key(link): text for link, text in remedies} #: Every link of every chain this run reads, in its first spelling. self.links: list[str] = [] names: dict[str, str] = {} @@ -276,10 +325,12 @@ def link_pins(self, policy: NamingPolicy, target: Target) -> dict: first = repo_basename(str(target.chain[0])) self.firsts.setdefault(pin_product_key(first), first) tried: dict = {} + offered = ({"remedies": self.remedies} + if self.remedies and _library_takes_remedies() else {}) found = link_pins_from_trees( target.chain, target.root, {key: path for key, (_typed, path) in self.keyed.items()}, - self._bare_path(), tried=tried) + self._bare_path(), tried=tried, **offered) # ONLY A LINK THAT HOLDS A PIN COUNTS. The lookup asks for every link, # the last one too, but only a holder's own declaration is ever read # (`_chain_link_findings`): a source asked only for the chain's end @@ -317,11 +368,15 @@ def _record(self, link: str, rule: str, how: str, path: Path, if why is None: self.used.add(typed) return + if rule == "bare" and self.label: + how = self.label links = self.unread.setdefault(typed, (path, how, why, []))[3] elif why is None or (rule == "sibling" and why in ( LINK_SOURCE_ABSENT, LINK_SOURCE_NO_MANIFEST)): return else: + # A checkout beside the project is named in `how` too (#661). + how = how.replace(str(path), _shown(path)) links = self.unanswered.setdefault((path, how, why), []) if link not in links: links.append(link) @@ -331,10 +386,10 @@ def read_as_path(self, typed: str) -> str: or `""` — the only trace in the run of a mistyped link name. Nothing when no chain names a link: the note has already said so.""" if self.reading.get(typed) is not None or "=" not in typed \ - or not self.links: + or not self.links or self.label: return "" key = typed.partition("=")[0] - what = f"`{key}` is not" if key else "nothing before its `=` is" + what = f"`{_shown(key)}` is not" if key else "nothing before its `=` is" return (f"It was read as a PATH: {what} a link of a referent chain " f"here (links: {', '.join(self.links)}).") @@ -345,7 +400,8 @@ def _also_a_directory(self, typed: str) -> str: `OSError` on every Python.""" if self.reading.get(typed) is None or not os.path.isdir(typed): return "" - return (f"`{typed}` is also a directory; `--link-source ./{typed}` " + named = _shown(typed) + return (f"`{named}` is also a directory; `--link-source ./{named}` " "reads it as one.") def unread_lines(self) -> list[str]: @@ -366,8 +422,8 @@ def unread_lines(self) -> list[str]: self.read_as_path(typed), self._also_a_directory(typed)) if line)) for (path, how, why), links in self.unanswered.items(): - lines.append(f"link-source-unreadable: {path} ({how}) could not " - f"answer for {', '.join(links)}: {why}") + lines.append(f"link-source-unreadable: {_shown(path)} ({how}) " + f"could not answer for {', '.join(links)}: {why}") return lines def no_path_refusal(self) -> str: @@ -375,7 +431,7 @@ def no_path_refusal(self) -> str: each by the link it is keyed to, or `""` when there is none.""" if not self.no_path: return "" - said = [f"`--link-source {typed}` names no path for " + said = [f"`--link-source {_shown(typed)}` names no path for " f"{self.reading[typed]}" if typed else "`--link-source ''` names no path" for typed in self.no_path] @@ -394,7 +450,8 @@ def _not_asked_why(self) -> str: return ("a bare PATH is one first link's tree, and the chains " "read here begin at " + ", ".join(self.firsts.values()) + ": name the link it is, as --link-source =PATH") - sources = ("a more specific source (a keyed --link-source, " + keyed = "" if self.label else "a keyed --link-source, " + sources = (f"a more specific source ({keyed}" "SHAPE_PIN_SOURCE_, or a checkout beside the " "project)") if self.asked <= set(self.firsts): @@ -413,9 +470,10 @@ def unused_lines(self) -> list[str]: for typed, link in self.reading.items(): if typed in self.used or typed in self.unread: continue + flag = self._flag(link) later = self.replaced.get(typed) if later is not None: - why = f"the later `--link-source {later}` replaced it" + why = f"the later `{flag} {_shown(later)}` replaced it" elif link is not None: why = f"no name classified here reads {link}'s own declaration" elif not self.links: @@ -426,10 +484,15 @@ def unused_lines(self) -> list[str]: else: why = self._not_asked_why() lines.append(" ".join(filter(None, ( - f"--link-source {typed}: not used — {why}.", + f"{flag} {_shown(typed)}: not used — {why}.", self.read_as_path(typed))))) return lines + def _flag(self, link: str | None) -> str: + """The flag a value is named by: `--link-source`, or for a bare + PATH the caller's label where it gave one (#661).""" + return self.label if link is None and self.label else "--link-source" + def _no_family_lines(policy: NamingPolicy, name: str) -> list[str]: """The `--explain` lines for a name that matches no family at all. @@ -961,6 +1024,22 @@ def main(argv: list[str] | None = None) -> int: "a source that cannot be read is named with the " "system's reason: both are warnings and not " "findings, an explicit source included.") + parser.add_argument("--link-source-label", default=None, + metavar="FLAG", + help="the caller's own name for the bare " + "--link-source it passes, such as --pin-source " + "(a FLAG that starts with - is given after an " + "=): the link-source-unreadable WARNING and the " + "not-used NOTE name that value by FLAG, and " + "neither says a value holding = was read as a " + "PATH") + parser.add_argument("--link-remedy", action="append", default=[], + type=_link_remedy, metavar="LINK=TEXT", + help="the caller's words for what would let LINK's " + "tree be read, offered in place of this " + "script's own where that link is " + "declared-unverified. Repeatable; the last " + "value for a link wins.") parser.add_argument("--explain", action="store_true", help="print every family each name satisfies, why one " "of them won, and what the others are recorded as") @@ -992,7 +1071,8 @@ def main(argv: list[str] | None = None) -> int: # reads is known and before any name is classified (#338, #441). sources = LinkSources(args.link_source, [ *cli_chain, *(_project_chain_links(data) if data is not None - else ())], _first_links_read(policy, targets, data)) + else ())], _first_links_read(policy, targets, data), + args.link_source_label, args.link_remedy) if sources.no_path: print(sources.no_path_refusal(), file=sys.stderr) return 2 diff --git a/setup-project.py b/setup-project.py index 0150046d..1b2cab7f 100755 --- a/setup-project.py +++ b/setup-project.py @@ -1542,6 +1542,18 @@ def shape_scripts(shape_root: Path): return repo_shape +def scaffold_link_remedies(shape_root: Path): + """`scripts/shape_materialize.py`'s `scaffold_link_remedies` out of + the checkout this run is using: what the scaffold's plan offers a chain + link whose tree it cannot read (#610), so step (4) can hand the naming + check the same words (#661) and one run says one fact one way. + Deferred for the reason `shape_scripts` gives, from the same directory. + """ + shape_scripts(shape_root) + import shape_materialize # noqa: E402 (deferred; see shape_scripts) + return shape_materialize.scaffold_link_remedies + + def family_landing(parent, family: str): """The clone's PARENT once `--family` has had its say. A PURE path. @@ -2033,7 +2045,7 @@ def as_a_bare_link_source(path: str) -> str: return "./" + path if re.match(r"[^/\\:=]+=", path) else path -def naming_declarations(passthrough: list) -> list: +def naming_declarations(passthrough: list, link_remedies=None) -> list: """The naming validator's arguments for what the passthrough declares about the project (#538): the products of its `--pin`s, and its `--referent-chain`, with the tree its links are read from (#608). @@ -2070,19 +2082,37 @@ def naming_declarations(passthrough: list) -> list: BLANK value is a path and goes as it is, and the rule reads the LAST value alone: `--pin-source src --pin-source ''` is empty, `--pin-source '' --pin-source src` is not. + + THE NAMING CHECK SPEAKS IN THIS RUN'S WORDS (#661). Its own name for a + bare source is `--link-source`, and for a link it cannot read it offers + "a sibling checkout, SHAPE_PIN_SOURCE_, or --link-source + =": a flag this file does not take, which typed after `--` + ends step (5) in the scaffold's `unrecognized arguments`. So, with a + chain of two links at least and `link_remedies` (`_main` passes the + scaffold's own, `scaffold_link_remedies`), it is told + `--link-source-label=--pin-source` and, for each holder, + `--link-remedy==`: what step (5) reads, said as step (5) + says it. The chain is read as the scaffold reads it + (`referent_chain_links`: split on commas, each stripped, an empty one + dropped). """ products = [name for name in ( pin.strip().split("@", 1)[0].rsplit("/", 1)[-1] for pin in passed_values(passthrough, "--pin")) if name] chains = passed_values(passthrough, "--referent-chain") sources = passed_values(passthrough, "--pin-source") - has_links = bool(chains) and sum( - 1 for link in chains[-1].split(",") if link.strip()) > 1 + chain = tuple(link.strip() for link in chains[-1].split(",") + if link.strip()) if chains else () + has_links = len(chain) > 1 names_a_source = bool(sources) and bool(str(sources[-1])) + spoken = link_remedies(chain) if has_links and link_remedies else None return ((["--pins=" + ",".join(products)] if products else []) + (["--referent-chain=" + chains[-1]] if chains else []) + (["--link-source=" + as_a_bare_link_source(sources[-1])] - if has_links and names_a_source else [])) + if has_links and names_a_source else []) + + (["--link-source-label=--pin-source"] + + ["--link-remedy=%s=%s" % pair for pair in spoken.items()] + if spoken is not None else [])) # --------------------------------------------------------------------------- @@ -2667,7 +2697,8 @@ def _main(argv, invocation_dir: str) -> int: clone = landing.clone_path(opts.project) refuse_a_taken_clone_path(clone, opts.project) check_names(shape_root, opts.project, opts.family, - naming_declarations(opts.passthrough)) + naming_declarations(opts.passthrough, + scaffold_link_remedies(shape_root))) args = scaffold_args(opts, org) if not plan_and_confirm(opts, shape_root, org, args, clone): return 0 diff --git a/templates/assembly-root/scripts/validate-manifest.py b/templates/assembly-root/scripts/validate-manifest.py index e7d2603f..ab6344f2 100755 --- a/templates/assembly-root/scripts/validate-manifest.py +++ b/templates/assembly-root/scripts/validate-manifest.py @@ -89,6 +89,7 @@ from __future__ import annotations import argparse +import inspect import re import sys from pathlib import Path @@ -98,7 +99,8 @@ CHAIN_RECORD_FIELD, COMMIT_RE, PATH_NOT_CANONICAL, PROJECT_ID_RE, SHA256_RE, TREE_DIGEST_DEFINITION, VISIBILITY_CHOICES, NamingPolicy, Refusal, accepts_role, find_repo_root, link_pins_from_trees, load_yaml, - path_spelling_fault, repo_basename, + path_spelling_fault, pin_product_key, pin_source_env_name, + pin_source_env_shared, repo_basename, ) DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") @@ -154,9 +156,68 @@ def _chain_and_links(naming, root: Path | None) -> tuple[tuple, dict]: pin on `openDox` is a fact in openXdox's tree; a checkout beside this root (or `SHAPE_PIN_SOURCE_`) is read when there is one, and its absence is a warning, never a failure. Nothing here goes to a host. + + THE WARNING OFFERS WHAT THIS READS (#661): `_link_remedies`, + handed over only where `link_pins_from_trees` takes `remedies=` + (`_library_takes_remedies`). """ chain = _recorded_chain(naming) if isinstance(naming, dict) else () - return chain, (link_pins_from_trees(chain, root) if chain else {}) + if not chain: + return chain, {} + offered = ({"remedies": _link_remedies(chain)} + if _library_takes_remedies() else {}) + return chain, link_pins_from_trees(chain, root, **offered) + + +def _link_remedies(chain) -> dict[str, str]: + """What would let each chain link's own tree be read, in THIS + validator's sources: `{pin_product_key(link): the words}`, for the + `declared-unverified` WARNING about a tree that is not reachable + (#661). + + The library's own words are the naming gate's: "a sibling checkout, + SHAPE_PIN_SOURCE_, or --link-source =". This file + takes `--root`, `--policy` and `--quiet` and no `--link-source`, so the + offer was a flag it refuses as `unrecognized arguments`. It reads + `SHAPE_PIN_SOURCE_`, unless another link of the chain shares the + variable (#275), and a checkout beside this root (`resolve_link_source`'s + rules 2 and 3, as `_chain_and_links` asks), so it offers those. Every + holder is offered both, or the checkout alone where another link shares + its variable; the chain's end holds no pin and has no entry. + + Keyed by `pin_product_key`, the key `_chain_link_findings` looks a + holder's entry up by (#444), and the links are read as that function + reads them: by `repo_basename`, a blank entry dropped. + """ + links = [repo_basename(str(link)) for link in chain if str(link).strip()] + shared = pin_source_env_shared(links) + remedies: dict[str, str] = {} + for link in links[:-1]: + beside = f"a checkout of {link} beside this root" + if pin_product_key(link) in shared: + words = f"{beside} would let it be read" + else: + words = (f"{pin_source_env_name(link)}, or {beside}, would let " + "it be read") + remedies[pin_product_key(link)] = words + return remedies + + +def _library_takes_remedies() -> bool: + """Does this root's `scripts/repo_shape.py` take `remedies=`? + + THE SKEW RULE (#661). This file is copied into every project and + may run beside a `repo_shape.py` older than itself (a half-applied + update, `update-shape.py apply --accept-local`). Before #610 the library + had no `remedies=`, and passing it ended this validator in a `TypeError` + before it checked a leg. So it is passed only where the function's own + signature names it, and an older library's warning keeps that library's + words.""" + try: + return "remedies" in inspect.signature( + link_pins_from_trees).parameters + except (TypeError, ValueError): + return False def _naming_chain_field_finding(leg_role, naming: dict) -> list[str]: diff --git a/tests/conftest.py b/tests/conftest.py index 33cbf476..63655f76 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1769,9 +1769,9 @@ def assert_an_empty_pin_source_is_the_plans_to_refuse(run, it, made the naming check WARN about `--link-source`, `.` and a folder the plan never reads. The naming check is given no source, so it says of the chain what it says of any it cannot verify, `[declared-unverified]`, and - its hint there, `--link-source =`, one per holder it - cannot verify, is the one spelling of the flag in a run that never typed - it.""" + its hint there offers the sources the plan reads, in the plan's words + (#661, L2-75): no line names `--link-source`, a flag the run never typed + and setup does not take.""" result = run("--project", "codexDox", "--org", "demoorg", "--local-remote-dir", str(tmp_path / "remotes"), "--into", str(tmp_path / "work"), @@ -1783,14 +1783,12 @@ def assert_an_empty_pin_source_is_the_plans_to_refuse(run, assert result.returncode == 2, heard assert "REFUSED pin-source-no-path:" in result.stderr, result.stderr assert "link-source-no-path" not in heard, heard - # The validator's own hint for a chain it cannot verify names the flag, - # `--link-source openXdox=`, whatever the run typed, so it is the - # one spelling left out; a `--link-source` that is a REPORT is not. - assert "--link-source" not in re.sub(r"--link-source \w+=", "", - heard), heard + assert "--link-source" not in heard, heard step_four = result.stdout.split("(4) naming policy", 1)[-1].split( "(5) the plan", 1)[0] assert "[declared-unverified]" in step_four, step_four + assert f"not reachable here ({FIRST_LINK_WORDS})." in step_four, ( + step_four) assert "do not satisfy the naming policy" not in result.stderr, ( result.stderr) assert not (tmp_path / "remotes").exists() @@ -1823,3 +1821,94 @@ def assert_a_chain_of_one_link_has_no_source_to_note(run, assert "NOTE --link-source" not in step_four + result.stderr, ( step_four + result.stderr) assert not (tmp_path / "remotes").exists() + + +#: What step (4) is asked to say of a chain (#661, L2-75): each case's +#: passthrough after `--`, as `(--pin-source, --referent-chain, what the +#: link tree declares, the variable set for the first link)`. +STEP_FOUR_WORDS = { + "three-links-first-tree": ("tree", "openXdox,openYdox,openDox", + "openYdox", False), + "three-links-no-source": ("", "openXdox,openYdox,openDox", None, False), + "blank": (" ", "openXdox,openDox", None, False), + "holds-an-equals": ("openXdox=layer", "openXdox,openDox", None, False), + "variable-first": ("tree", "openXdox,openDox", "openDox", True), +} + +#: The scaffold's own words for the first link and a later one (#610). +FIRST_LINK_WORDS = ("SHAPE_PIN_SOURCE_OPENXDOX, or a --pin-source clone of " + "openXdox that holds its project.yaml, would let it be " + "read") +LATER_LINK_WORDS = "SHAPE_PIN_SOURCE_OPENYDOX would let it be read" + + +def assert_step_four_speaks_in_the_plans_words(run, tmp_path: Path, + monkeypatch, case: str + ) -> None: + """Step (4) names no `--link-source`, a flag setup does not take (#661, + L2-75): a holder it cannot read is offered the sources step (5) reads, + in step (5)'s words, one offer per holder; a `--pin-source` it could not + read, or did not need, is named `--pin-source`; and a value holding + `=` is not said to have been read as a PATH. On main step (4) offered + `--link-source =` and labelled the source `(--link-source)`; + typed as printed, the offer ended step (5) in `unrecognized + arguments`.""" + clear_ambient_pin_sources(monkeypatch) + source, chain, declares, variable = STEP_FOUR_WORDS[case] + commit = "0123456789abcdef" * 2 + "01234567" + if declares is not None: + tree = tmp_path / "openXdox" + tree.mkdir() + git("init", "-q", "-b", "main", cwd=tree) + (tree / "project.yaml").write_bytes( + ("schema_version: 1\nkind: project-manifest\n" + f"neutral_product_pins:\n - {declares}\n").encode("utf-8")) + git("add", "project.yaml", cwd=tree) + git("commit", "-q", "-m", "seed", cwd=tree) + commit = git("rev-parse", "HEAD", cwd=tree).stdout.strip() + source = str(tree) + if variable: + monkeypatch.setenv("SHAPE_PIN_SOURCE_OPENXDOX", source) + result = run("--project", "codexDox", "--org", "demoorg", + "--local-remote-dir", str(tmp_path / "remotes"), + "--into", str(tmp_path / "work"), + "--", "--pin", f"openXdox@{commit}", + "--referent-chain", chain, "--pin-source", source) + heard = result.stdout + result.stderr + step_four = result.stdout.split("(4) naming policy", 1)[-1].split( + "(5) the plan", 1)[0] + assert "(5) the plan" in result.stdout, heard + assert result.returncode == 2, heard + assert "--link-source" not in step_four + result.stderr, heard + offered = [line.strip() for line in step_four.splitlines() + if line.strip().startswith("WARNING declared-unverified: ")] + if case == "three-links-first-tree": + assert offered == [ + "WARNING declared-unverified: openYdox declaring a pin on openDox " + "is a fact in openYdox's own tree, which is not reachable here " + f"({LATER_LINK_WORDS}). The recorded chain still classifies."] + # One run says one fact one way: the plan's own WARNING. + assert offered[0] + "\n" in result.stderr, result.stderr + elif case == "three-links-no-source": + assert [line.partition("not reachable here (")[2] for line in offered + ] == [f"{FIRST_LINK_WORDS}). The recorded chain still " + "classifies.", + f"{LATER_LINK_WORDS}). The recorded chain still " + "classifies."] + assert "REFUSED pin-source-no-path:" in result.stderr + elif case == "variable-first": + assert "[verified]" in step_four, step_four + assert (f"NOTE --pin-source {source}: not used — each link whose own " + "declaration is read had a more specific source " + "(SHAPE_PIN_SOURCE_, or a checkout beside the " + "project).") in result.stderr, result.stderr + else: + assert (f"WARNING link-source-unreadable: {source} (--pin-source) " + "could not answer for openXdox: no such file or directory" + ) in result.stderr, result.stderr + assert "read as a PATH" not in result.stderr, result.stderr + assert offered, step_four + assert offered[0].endswith( + f"({FIRST_LINK_WORDS}). The recorded chain still classifies." + ), offered + assert not (tmp_path / "remotes").exists() diff --git a/tests/test_chain_remedy_callers.py b/tests/test_chain_remedy_callers.py new file mode 100644 index 00000000..343ee7a9 --- /dev/null +++ b/tests/test_chain_remedy_callers.py @@ -0,0 +1,389 @@ +# SPDX-License-Identifier: Apache-2.0 +"""What a chain link that is not reachable is offered, by each caller in its +own sources, and how the naming gate spells what it prints (#661). + + * L2-68. The root's `validate-manifest.py` printed the naming gate's + words, "a sibling checkout, SHAPE_PIN_SOURCE_, or --link-source + = would let it be read", and it takes no `--link-source`: + typed as printed, `unrecognized arguments`. It offers the variable and + a checkout beside the root, the two it reads, and passes them as + `remedies=` only where its root's `scripts/repo_shape.py` takes the + keyword, so it still runs beside an older library. + * L2-75. Setup's step (4) showed the gate's offer and its `(--link-source)` + label for a value typed as `--pin-source`. The gate takes the caller's + label for a bare source (`--link-source-label`) and the caller's words + for each holder (`--link-remedy`); setup's own half is in + `tests/conftest.py` (`assert_step_four_speaks_in_the_plans_words`). + * L2-69. The gate named `--link-source` values and paths raw, so an ESC + reached the terminal. One row per print, each with an ESC. + +No network and no GitHub. +""" + +from __future__ import annotations + +import subprocess +import sys + +import pytest + +from conftest import (REPO, clear_ambient_pin_sources, load_without_bytecode, + run_script) +from test_link_source_chain_defaults import ( # noqa: F401 + VALIDATOR, manifest, naming, tree, +) +from test_manifest_validator import as_a_descendant_by_chain, edit, link_tree +from test_pinned_copy_text import CONTROL_ARGV, CONTROL_NAMES + +sys.path.insert(0, str(REPO / "scripts")) +from repo_shape import shown_path # noqa: E402 + +TEMPLATE_MANIFEST = (REPO / "templates" / "assembly-root" / "scripts" / + "validate-manifest.py") + +#: The sentence a holder's `declared-unverified` WARNING ends with, and what +#: the root's `validate-manifest.py` offers in it (L2-68). +UNREACHED = ("declared-unverified: {holder} declaring a pin on {held} is a " + "fact in {holder}'s own tree, which is not reachable here " + "({offer}). The recorded chain still classifies.") +OWN_OFFER = ("SHAPE_PIN_SOURCE_{var}, or a checkout of {link} beside this " + "root, would let it be read") +#: The naming gate's own words, which an older library still prints. +GATE_OFFER = ("a sibling checkout, SHAPE_PIN_SOURCE_{var}, or --link-source " + "{link}= would let it be read") + +#: Libraries older than the copies: `remedies=` is new with #610 (#634). +#: `pre-634` is main just before #634's squash, the first parent of +#: `30676c90`; `v1.0.1` is the release every project pins today. +OLDER_LIBRARIES = { + "pre-634": "de926142e69dcd4fcb986f57573f4f35c276c966", + "v1.0.1": "v1.0.1", +} + + +def offer(words: str, link: str) -> str: + return words.format(var=link.upper(), link=link) + + +def manifest_warnings(result) -> list[str]: + return [line.partition("leg 'assembly': ")[2] + for line in result.stderr.splitlines() + if line.startswith("WARNING naming-referent-chain: ")] + + +def a_chain_root(project, chain=("openXdox", "openDox")): + """The scaffolded root recast as `codexDox`, pinning `openXdox` with its + pin file, and recording `chain`.""" + as_a_descendant_by_chain(project) + (project / "contracts" / "openxdox-pin.yaml").write_text( + "schema_version: 1\nkind: pinned_contract_manifest\n") + if tuple(chain) != ("openXdox", "openDox"): + # The leg's own record, on a line of its own: the manifest's comment + # block above it spells a chain the same way. + edit(project, "\n referent_chain: [openXdox, openDox]\n", + f"\n referent_chain: [{', '.join(chain)}]\n") + return project + + +# --- L2-68: the root's validate-manifest.py offers what it reads ------------ + +@pytest.mark.parametrize("chain", [ + pytest.param(("openXdox", "openDox"), id="two-links"), + pytest.param(("openXdox", "openYdox", "openDox"), id="three-links"), + pytest.param(("opensoft/openXdox", "openDox"), id="qualified"), +]) +def test_the_manifest_gate_offers_the_sources_it_reads(project, monkeypatch, + chain): + """Each holder is offered its variable and a checkout beside the root, + in its own words, and nothing names `--link-source`. A link recorded + `/` is the link ``, as the chain's own reader takes it + (`_chain_links`): its entry, and the words, go by the name alone.""" + clear_ambient_pin_sources(monkeypatch) + root = a_chain_root(project, chain) + result = run_script(root / "scripts" / "validate-manifest.py", cwd=root) + assert result.returncode == 0, result.stderr + assert "manifest ok" in result.stdout + names = [link.rpartition("/")[2] for link in chain] + assert manifest_warnings(result) == [ + UNREACHED.format(holder=holder, held=held, + offer=offer(OWN_OFFER, holder)) + for holder, held in zip(names, names[1:])] + assert "--link-source" not in result.stderr + + +@pytest.mark.parametrize("where", ["variable", "beside-the-root"]) +def test_each_source_the_manifest_gate_offers_reads_the_link(project, + monkeypatch, + tmp_path, where): + """The offer is one the validator HAS: either source it names, given + the link's tree, verifies the chain.""" + clear_ambient_pin_sources(monkeypatch) + root = a_chain_root(project) + env = {} + if where == "variable": + env["SHAPE_PIN_SOURCE_OPENXDOX"] = str( + link_tree(tmp_path / "elsewhere" / "openXdox", "openDox")) + else: + link_tree(root.parent / "openXdox", "openDox") + result = run_script(root / "scripts" / "validate-manifest.py", cwd=root, + env=env) + assert result.returncode == 0, result.stderr + assert "WARNING" not in result.stderr, result.stderr + + +@pytest.mark.parametrize("first, keyed", [ + pytest.param("open-x", "open-x", id="lowercase"), + pytest.param("Open-X", "open-x", id="mixed-case"), +]) +def test_a_shared_variable_is_not_offered_by_the_manifest_gate(first, keyed): + """Two holders that share one variable (#275): the validator reads it + for neither, so neither is offered it, and the entry is keyed by + `pin_product_key`, the key `_chain_link_findings` looks it up by.""" + module = load_without_bytecode(TEMPLATE_MANIFEST, "chain_remedy_manifest") + assert module._link_remedies((first, "open_x", "openDox")) == { + keyed: f"a checkout of {first} beside this root would let it be read", + "open_x": "a checkout of open_x beside this root would let it be read"} + + +def older_library(ref: str) -> bytes: + """`scripts/repo_shape.py` at `ref`, or a skip where this checkout does + not hold that history. It must be a library with no `remedies` at all, + or the row would test nothing older than the copies.""" + shown = subprocess.run(["git", "show", f"{ref}:scripts/repo_shape.py"], + cwd=str(REPO), capture_output=True, check=False) + if shown.returncode != 0: + pytest.skip(f"needs {ref} in this checkout's history") + assert b"def link_pins_from_trees(" in shown.stdout, ref + assert b"remedies" not in shown.stdout, ref + return shown.stdout + + +@pytest.mark.parametrize("ref", list(OLDER_LIBRARIES.values()), + ids=list(OLDER_LIBRARIES)) +def test_the_new_copies_run_beside_an_older_library(project, monkeypatch, ref): + """THE SKEW RULE. A root may keep a `scripts/repo_shape.py` older than its + copies (`update-shape.py apply --accept-local`), and before #610 the + library had no `remedies=`: passing it ended both validators in a + `TypeError`. Beside such a library each runs, and its WARNING keeps that + library's words.""" + clear_ambient_pin_sources(monkeypatch) + root = a_chain_root(project) + (root / "scripts" / "repo_shape.py").write_bytes(older_library(ref)) + own = run_script(root / "scripts" / "validate-manifest.py", cwd=root) + assert own.returncode == 0, own.stderr + assert "Traceback" not in own.stderr, own.stderr + assert manifest_warnings(own) == [UNREACHED.format( + holder="openXdox", held="openDox", offer=offer(GATE_OFFER, + "openXdox"))] + gate = run_script(root / "scripts" / "validate-repository-naming.py", + "--project", "project.yaml", "--link-remedy", + "openXdox=OURS would do", cwd=root) + assert gate.returncode == 0, gate.stderr + assert "Traceback" not in gate.stderr, gate.stderr + assert offer(GATE_OFFER, "openXdox") in gate.stderr, gate.stderr + + +# --- L2-75: the gate takes the caller's label and words ---------------------- + +THREE = ("--pins", "openXdox", "--referent-chain", "openXdox,openYdox,openInk") + + +def gate_warnings(result) -> list[str]: + return [line.partition("codexInk: ")[2] + for line in result.stderr.splitlines() + if line.startswith("WARNING codexInk: ")] + + +def test_the_gate_offers_each_holder_the_callers_words(tmp_path, monkeypatch): + """One offer per holder, each in its own words, found by + `pin_product_key` (the second is typed in capitals).""" + clear_ambient_pin_sources(monkeypatch) + result = naming(tmp_path, *THREE, "--link-remedy", + "openXdox=FIRST would do", "--link-remedy", + "OPENYDOX=LATER would do", "codexInk") + assert result.returncode == 0, result.stderr + assert gate_warnings(result) == [ + UNREACHED.format(holder="openXdox", held="openYdox", + offer="FIRST would do"), + UNREACHED.format(holder="openYdox", held="openInk", + offer="LATER would do")] + assert "--link-source" not in result.stderr + + +def test_a_holder_given_no_words_keeps_the_gates_own(tmp_path, monkeypatch): + clear_ambient_pin_sources(monkeypatch) + result = naming(tmp_path, *THREE, "--link-remedy", + "openYdox=LATER would do", "codexInk") + assert gate_warnings(result) == [ + UNREACHED.format(holder="openXdox", held="openYdox", + offer=offer(GATE_OFFER, "openXdox")), + UNREACHED.format(holder="openYdox", held="openInk", + offer="LATER would do")] + + +def test_the_last_words_for_a_link_win(tmp_path, monkeypatch): + clear_ambient_pin_sources(monkeypatch) + result = naming(tmp_path, *THREE, "--link-remedy", "openXdox=A", + "--link-remedy", "openXdox=B", "codexInk") + assert gate_warnings(result)[0].endswith( + "not reachable here (B). The recorded chain still classifies.") + + +@pytest.mark.parametrize("value", [ + pytest.param("openXdox", id="no-equals"), + pytest.param("=words", id="no-link"), + pytest.param("openXdox= ", id="no-words"), +]) +def test_words_that_are_not_link_equals_text_are_refused(tmp_path, value): + result = naming(tmp_path, *THREE, "--link-remedy", value, "codexInk") + assert result.returncode == 2, result.stderr + assert "argument --link-remedy:" in result.stderr, result.stderr + assert "is not LINK=TEXT" in result.stderr, result.stderr + + +def test_a_labelled_bare_source_is_named_by_its_label(tmp_path, monkeypatch): + """A source the gate could not read, named by the caller's flag.""" + clear_ambient_pin_sources(monkeypatch) + nowhere = tmp_path / "nowhere" + result = naming(tmp_path, *THREE, "--link-source", str(nowhere), + "--link-source-label=--pin-source", "codexInk") + assert (f"WARNING link-source-unreadable: {nowhere} (--pin-source) could " + "not answer for openXdox: no such file or directory\n" + ) in result.stderr, result.stderr + + +def test_a_labelled_source_holding_an_equals_is_not_said_to_be_a_path( + tmp_path, monkeypatch): + """The caller's flag has no `LINK=` form, so "It was read as a PATH" + would be about a reading nobody asked for.""" + clear_ambient_pin_sources(monkeypatch) + result = naming(tmp_path, *THREE, "--link-source", "./openXdox=layer", + "--link-source-label=--pin-source", "codexInk") + assert ("WARNING link-source-unreadable: openXdox=layer (--pin-source) " + "could not answer for openXdox") in result.stderr, result.stderr + assert "read as a PATH" not in result.stderr, result.stderr + + +def test_an_unused_labelled_source_is_noted_by_its_label(tmp_path, + monkeypatch): + """The first link's variable answered, so the bare source was not asked: + the NOTE names it by the caller's flag, and lists only the sources such + a caller has.""" + clear_ambient_pin_sources(monkeypatch) + first = tree(tmp_path / "trees" / "openXdox", "openInk") + spare = tree(tmp_path / "spare", "openInk") + result = run_script(VALIDATOR, "--pins", "openXdox", "--referent-chain", + "openXdox,openInk", "--link-source", str(spare), + "--link-source-label=--pin-source", "codexInk", + cwd=tmp_path, + env={"SHAPE_PIN_SOURCE_OPENXDOX": str(first)}) + assert (f"NOTE --pin-source {spare}: not used — each link whose own " + "declaration is read had a more specific source " + "(SHAPE_PIN_SOURCE_, or a checkout beside the project)." + "\n") in result.stderr, result.stderr + assert "--link-source" not in result.stderr + + +# --- L2-69: every --link-source value and path the gate prints is spelled --- + +def assert_clean(result) -> None: + said = result.stdout + result.stderr + assert "\x1b" not in said, said + assert not [line for line in said.splitlines() + if line.startswith("WARNING forged-line")], said + + +@CONTROL_ARGV +@pytest.mark.parametrize("name", CONTROL_NAMES) +def test_a_key_read_as_a_path_is_spelled(tmp_path, monkeypatch, name): + """`read_as_path`: the text before `=`, which names no link.""" + clear_ambient_pin_sources(monkeypatch) + result = naming(tmp_path, *THREE, "--link-source", f"{name}=b/openXdox", + "codexInk") + assert_clean(result) + assert f"It was read as a PATH: `{shown_path(name)}` is not a link" in ( + result.stderr), result.stderr + + +@CONTROL_ARGV +@pytest.mark.parametrize("name", CONTROL_NAMES) +def test_a_keyed_value_that_is_also_a_directory_is_spelled(tmp_path, + monkeypatch, name): + """`_also_a_directory`: the value, in the sentence and in the command.""" + clear_ambient_pin_sources(monkeypatch) + typed = f"openXdox={name}" + (tmp_path / typed).mkdir() + result = naming(tmp_path, *THREE, "--link-source", typed, "codexInk") + assert_clean(result) + named = shown_path(typed) + assert (f"`{named}` is also a directory; `--link-source ./{named}` reads " + "it as one.") in result.stderr, result.stderr + + +@CONTROL_ARGV +@pytest.mark.parametrize("name", CONTROL_NAMES) +def test_a_variable_the_gate_could_not_read_is_spelled(tmp_path, name): + """`unread_lines`' second loop, for `SHAPE_PIN_SOURCE_`.""" + folder = tmp_path / name + result = run_script(VALIDATOR, *THREE, "codexInk", cwd=tmp_path, + env={"SHAPE_PIN_SOURCE_OPENXDOX": str(folder)}) + assert_clean(result) + assert (f"WARNING link-source-unreadable: {shown_path(str(folder))} " + "($SHAPE_PIN_SOURCE_OPENXDOX) could not answer for openXdox" + ) in result.stderr, result.stderr + + +@CONTROL_ARGV +@pytest.mark.parametrize("name", CONTROL_NAMES) +def test_a_checkout_beside_the_project_the_gate_could_not_read_is_spelled( + tmp_path, monkeypatch, name): + """`unread_lines`' second loop, for a checkout beside the project: its + path, and the path inside its `how`.""" + clear_ambient_pin_sources(monkeypatch) + parent = tmp_path / name + (parent / "codexInk").mkdir(parents=True) + beside = manifest(parent / "openXdox", "neutral_product_pins: 5\n") + result = naming(parent / "codexInk", *THREE, "codexInk") + assert_clean(result) + shown = shown_path(str(beside)) + assert (f"WARNING link-source-unreadable: {shown} (sibling checkout " + f"({shown})) could not answer for openXdox") in result.stderr, ( + result.stderr) + + +@CONTROL_ARGV +def test_a_value_that_names_no_path_is_spelled(tmp_path): + """`no_path_refusal`: a keyed value names a link of the chain, so the + value holds a control character only where the link does; the value is + spelled, and the link, a chain's name, is printed as the chain gives + it, as every chain line is.""" + link = "openX\x1b[31mdox" + result = naming(tmp_path, "--pins", link, "--referent-chain", + f"{link},openInk", "--link-source", f"{link}=", + "codexInk") + assert result.returncode == 2, result.stderr + assert (f"`--link-source {shown_path(link + '=')}` names no path for " + ) in result.stderr, result.stderr + assert f"--link-source {link}=" not in result.stderr + + +@CONTROL_ARGV +@pytest.mark.parametrize("name", CONTROL_NAMES) +def test_an_unused_link_source_and_its_replacement_are_spelled(tmp_path, + name): + """`unused_lines`: the value, and the later value that replaced it.""" + result = naming(tmp_path, "--link-source", "plain", "--link-source", + name, "codexInk") + assert_clean(result) + assert (f"NOTE --link-source plain: not used — the later `--link-source " + f"{shown_path(name)}` replaced it.") in result.stderr + assert (f"NOTE --link-source {shown_path(name)}: not used — no name " + "classified here records a referent chain.") in result.stderr + + +def test_a_printable_value_is_named_as_typed(tmp_path): + """The control: a printable value and its replacement, byte for byte.""" + result = naming(tmp_path, "--link-source", "a", "--link-source", + "café", "codexInk") + assert ("NOTE --link-source a: not used — the later `--link-source " + "café` replaced it.") in result.stderr, result.stderr diff --git a/tests/test_naming_policy.py b/tests/test_naming_policy.py index 8ebf37be..e3133209 100644 --- a/tests/test_naming_policy.py +++ b/tests/test_naming_policy.py @@ -2071,8 +2071,10 @@ def test_the_projects_own_manifest_gate_keeps_its_warning_for_a_tree_it_reached( own = run_script(project / "scripts" / "validate-manifest.py", cwd=project) assert "Traceback" not in own.stderr, own.stderr - assert "which is not reachable here (a sibling checkout, " in ( - own.stdout + own.stderr), own.stdout + own.stderr + assert ("which is not reachable here (SHAPE_PIN_SOURCE_OPENXDOX, or a " + "checkout of openXdox beside this root, would let it be read)" + ) in own.stdout + own.stderr, own.stdout + own.stderr + assert "--link-source" not in own.stdout + own.stderr assert "was reached" not in own.stdout + own.stderr diff --git a/tests/test_setup_project_py.py b/tests/test_setup_project_py.py index 5b8bb65b..535aa995 100644 --- a/tests/test_setup_project_py.py +++ b/tests/test_setup_project_py.py @@ -68,6 +68,12 @@ assert_a_chain_of_one_link_has_no_source_to_note, assert_an_empty_pin_source_is_the_plans_to_refuse) +from conftest import (STEP_FOUR_WORDS, load_without_bytecode, + assert_step_four_speaks_in_the_plans_words) + +sys.path.insert(0, str(REPO / "scripts")) +import shape_materialize # noqa: E402 + SETUP_PROJECT = REPO / "setup-project.py" SETUP_SH = REPO / "setup.sh" DEGRADE_LINE = "authority is not wallet-carried in this org" @@ -2905,3 +2911,56 @@ def test_a_terminal_whose_read_fails_is_the_end_of_the_answer(monkeypatch): monkeypatch.setattr(module, "os", hung_up) assert module.ask("Type yes to continue: ") == "" assert hung_up.closed == [7] + + +# --- #661 (L2-75): step (4) speaks in the plan's words ---------------------- + +@pytest.mark.parametrize("passthrough,spoken", [ + # A chain of two links at least: the label, and each HOLDER's words, in + # the scaffold's own (`shape_materialize.scaffold_link_remedies`). + (["--referent-chain", "openXdox,openDox"], + ["--referent-chain=openXdox,openDox", "--link-source-label=--pin-source", + "--link-remedy=openxdox=SHAPE_PIN_SOURCE_OPENXDOX, or a --pin-source " + "clone of openXdox that holds its project.yaml, would let it be read"]), + (["--referent-chain", "openXdox,openYdox,openDox", "--pin-source", "src"], + ["--referent-chain=openXdox,openYdox,openDox", "--link-source=src", + "--link-source-label=--pin-source", + "--link-remedy=openxdox=SHAPE_PIN_SOURCE_OPENXDOX, or a --pin-source " + "clone of openXdox that holds its project.yaml, would let it be read", + "--link-remedy=openydox=SHAPE_PIN_SOURCE_OPENYDOX would let it be " + "read"]), + # ...read as the scaffold reads the chain: blanks dropped, the last kept. + (["--referent-chain", "openXdox, ,openDox"], + ["--referent-chain=openXdox, ,openDox", + "--link-source-label=--pin-source", + "--link-remedy=openxdox=SHAPE_PIN_SOURCE_OPENXDOX, or a --pin-source " + "clone of openXdox that holds its project.yaml, would let it be read"]), + # A chain of one link has no holder, so nothing is said for it. + (["--referent-chain", "openDox", "--pin-source", "src"], + ["--referent-chain=openDox"]), + (["--referent-chain", "openXdox,openDox", "--referent-chain", " , "], + ["--referent-chain= , "]), + ([], [])]) +def test_each_holder_is_given_the_plans_words(passthrough, spoken): + """#661, L2-75: with the scaffold's words, step (4) tells the naming + check `--pin-source` is the flag and what each holder is offered.""" + module = entry_point_module() + assert module.naming_declarations( + passthrough, shape_materialize.scaffold_link_remedies) == spoken + + +def test_step_four_and_the_plan_share_one_set_of_words(): + """ONE definition: setup asks the checkout's `shape_materialize` for the + words the scaffold's own `_link_remedies` is.""" + module = entry_point_module() + assert module.scaffold_link_remedies(REPO) is ( + shape_materialize.scaffold_link_remedies) + scaffold = load_without_bytecode(REPO / "scaffold-project.py", + "scaffold_for_setup_words") + assert scaffold._link_remedies is shape_materialize.scaffold_link_remedies + + +@pytest.mark.parametrize("case", STEP_FOUR_WORDS) +def test_step_four_speaks_in_the_plans_words(tmp_path, monkeypatch, case): + assert_step_four_speaks_in_the_plans_words(run_entry, tmp_path, + monkeypatch, case) diff --git a/tests/test_setup_sh.py b/tests/test_setup_sh.py index 1256c698..312e2b71 100644 --- a/tests/test_setup_sh.py +++ b/tests/test_setup_sh.py @@ -51,6 +51,9 @@ assert_a_chain_of_one_link_has_no_source_to_note, assert_an_empty_pin_source_is_the_plans_to_refuse) +from conftest import (STEP_FOUR_WORDS, + assert_step_four_speaks_in_the_plans_words) + SETUP = REPO / "setup.sh" DEGRADE_LINE = "authority is not wallet-carried in this org" @@ -1145,3 +1148,9 @@ def test_the_one_liners_preflight_offers_are_asked_at_the_terminal(tmp_path, calls = (stub / "calls.log").read_text(encoding="utf-8").splitlines() logins = [call for call in calls if call.startswith("auth login|")] assert logins == ([] if typed == "no" else ["auth login|terminal"]), calls + + +@pytest.mark.parametrize("case", STEP_FOUR_WORDS) +def test_step_four_speaks_in_the_plans_words(tmp_path, monkeypatch, case): + assert_step_four_speaks_in_the_plans_words(run_setup, tmp_path, + monkeypatch, case)