Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -902,12 +902,20 @@ except Exception as exc:
*'{CORE_TEMPLATE}'*) ;;
*) echo "Error: wrap strategy missing {CORE_TEMPLATE} placeholder" >&2; return 2 ;;
esac
while [[ "$layer_content" == *'{CORE_TEMPLATE}'* ]]; do
local before="${layer_content%%\{CORE_TEMPLATE\}*}"
local after="${layer_content#*\{CORE_TEMPLATE\}}"
layer_content="${before}${content}${after}"
# Consume the wrapper left to right instead of rewriting it in
# place. Rewriting re-scanned the string just modified, so base
# content holding a literal {CORE_TEMPLATE} reintroduced the
# token every pass and the loop never terminated. Advancing over
# ``rest`` bounds the work by the tokens in the original wrapper
# and leaves inserted content untouched, matching the single-pass
# semantics of .Replace()/.replace() in the PowerShell and Python
# ports.
local wrapped="" rest="$layer_content"
while [[ "$rest" == *'{CORE_TEMPLATE}'* ]]; do
wrapped="${wrapped}${rest%%\{CORE_TEMPLATE\}*}${content}"
rest="${rest#*\{CORE_TEMPLATE\}}"
done
content="$layer_content"
content="${wrapped}${rest}"
;;
*) echo "Error: unknown strategy '$strat'" >&2; return 2 ;;
esac
Expand Down
12 changes: 11 additions & 1 deletion tests/parity_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,25 @@ def clean_env() -> dict[str, str]:


def run(
cmd: list[str], repo: Path, env: dict[str, str] | None = None
cmd: list[str],
repo: Path,
env: dict[str, str] | None = None,
timeout: float | None = None,
) -> subprocess.CompletedProcess[str]:
"""Run a script variant.

``timeout`` guards cases whose regression mode is a hang rather than a bad
value; without it such a failure would stall the suite instead of failing
it. ``subprocess.TimeoutExpired`` propagates so the test reports the hang.
"""
return subprocess.run(
cmd,
cwd=repo,
capture_output=True,
text=True,
check=False,
env=env if env is not None else clean_env(),
timeout=timeout,
)


Expand Down
34 changes: 34 additions & 0 deletions tests/test_resolve_template_python_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,40 @@ def test_all_variants_preserve_composition_parity(
)


@requires_bash
def test_all_variants_treat_core_token_in_core_content_as_literal(
tmp_path: Path,
) -> None:
"""Core content holding a literal ``{CORE_TEMPLATE}`` must not be re-expanded.

The wrap strategy fills the placeholders present in the *wrapper*. A token
that arrives as part of the composed core content is data, not a slot, so it
survives into the output untouched. Rescanning the substituted string instead
reintroduces a token on every pass and never terminates, so the regression
mode here is a hang rather than a wrong value -- hence the timeout, without
which a reintroduced bug would stall the suite instead of failing it.
"""
repo = make_repo(tmp_path)
install_scripts(repo, SCRIPT)
expected = install_composition_stack(repo, TEMPLATE, "# Core {CORE_TEMPLATE}\n")

results = [
run(bash_cmd(repo, SCRIPT, TEMPLATE, "--json"), repo, timeout=30),
run(py_cmd(repo, SCRIPT, TEMPLATE, "--json"), repo, timeout=30),
]
if HAS_POWERSHELL:
results.append(run(ps_cmd(repo, SCRIPT, TEMPLATE, "-Json"), repo, timeout=30))

assert all(result.returncode == 0 for result in results)
assert all(result.stderr == "" for result in results)
# The wrapper contributes exactly one placeholder, so exactly one literal
# token -- the one carried in by the core content -- remains in the output.
assert expected.count("{CORE_TEMPLATE}") == 1
assert all(
json_stdout(result)["TEMPLATE_CONTENT"] == expected for result in results
)


@requires_bash
def test_all_variants_read_utf8_registry_under_ascii_locale(
tmp_path: Path,
Expand Down