4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
853 lines
30 KiB
Python
853 lines
30 KiB
Python
"""Tests for the configure_path() function in install.sh."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# install.sh is a POSIX shell script that exercises zsh/bash/fish rc-file
|
|
# behaviour, and these tests drive it via ``subprocess.run(["bash", "-c", ...])``.
|
|
# On the GitHub Actions ``windows-latest`` runner, ``bash`` is resolved to
|
|
# ``wsl.exe`` and the runner has no installed WSL distribution — every
|
|
# ``_run`` call exits 1 with a "Windows Subsystem for Linux has no installed
|
|
# distributions" message and none of the asserted rc files get written.
|
|
# Skip the whole module rather than chase a Windows analogue for a Unix-only
|
|
# installer script.
|
|
pytestmark = pytest.mark.skipif(
|
|
sys.platform == "win32",
|
|
reason=(
|
|
"install.sh is POSIX-only; the Windows runner has no usable bash "
|
|
"(resolves to unconfigured WSL), so this module's subprocess-driven "
|
|
"tests cannot run there. See issue #1099."
|
|
),
|
|
)
|
|
|
|
INSTALL_SH = Path(__file__).parents[2] / "install.sh"
|
|
_INSTALL_SH_SHELL = shlex.quote(str(INSTALL_SH))
|
|
_LOCAL_BIN = ".local/bin"
|
|
_ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
|
|
|
|
def _strip_ansi(value: str) -> str:
|
|
return _ANSI_RE.sub("", value)
|
|
|
|
|
|
def _visible_terminal_text(value: str) -> str:
|
|
return _strip_ansi(value).replace("\r", "").replace("\n", "")
|
|
|
|
|
|
def _run(
|
|
tmp_path: Path, shell: str, platform: str = "linux", install_dir: str | None = None
|
|
) -> subprocess.CompletedProcess[str]:
|
|
fake_home = tmp_path / "home"
|
|
fake_home.mkdir(exist_ok=True)
|
|
idir = install_dir if install_dir is not None else str(fake_home / _LOCAL_BIN)
|
|
install_sh = _INSTALL_SH_SHELL
|
|
idir_shell = shlex.quote(idir)
|
|
home_shell = shlex.quote(str(fake_home))
|
|
|
|
script = textwrap.dedent(f"""\
|
|
__fn=$(awk 'p&&/^}}$/{{print;exit}} /^configure_path\\(\\)/{{p=1}} p{{print}}' {install_sh})
|
|
if [ -z "$__fn" ]; then
|
|
echo "configure_path not found in install.sh" >&2
|
|
exit 1
|
|
fi
|
|
log() {{ printf '%s\\n' "$*"; }}
|
|
warn() {{ printf 'Warning: %s\\n' "$*" >&2; }}
|
|
eval "$__fn"
|
|
INSTALL_DIR={idir_shell} platform="{platform}" HOME={home_shell} SHELL="{shell}" configure_path
|
|
""")
|
|
return subprocess.run(["bash", "-c", script], capture_output=True, text=True)
|
|
|
|
|
|
def _run_logging_snippet(body: str) -> subprocess.CompletedProcess[str]:
|
|
install_sh = _INSTALL_SH_SHELL
|
|
script = textwrap.dedent(f"""\
|
|
eval "$(awk '/^REPO=/{{exit}} {{print}}' {install_sh})"
|
|
eval "$(awk '
|
|
/^[a-z_][a-z_]*\\(\\)/ {{ in_fn=1 }}
|
|
in_fn {{ print }}
|
|
in_fn && /^\\}}$/ {{ in_fn=0 }}
|
|
' {install_sh})"
|
|
{body}
|
|
""")
|
|
return subprocess.run(["bash", "-c", script], capture_output=True, text=True)
|
|
|
|
|
|
def _find_release_metadata_step_block() -> str:
|
|
lines = INSTALL_SH.read_text().splitlines()
|
|
for i, line in enumerate(lines):
|
|
if line.strip() != 'release_tag=""':
|
|
continue
|
|
|
|
block = []
|
|
for candidate in lines[i + 1 :]:
|
|
block.append(candidate)
|
|
if candidate.strip() == "fi":
|
|
return "\n".join(block)
|
|
|
|
raise RuntimeError(f"Could not locate release metadata step block in {INSTALL_SH}.")
|
|
|
|
|
|
def _run_release_metadata_step(
|
|
install_channel: str = "release", version: str = ""
|
|
) -> subprocess.CompletedProcess[str]:
|
|
block = _find_release_metadata_step_block()
|
|
install_sh = _INSTALL_SH_SHELL
|
|
script = textwrap.dedent(f"""\
|
|
eval "$(awk '/^REPO=/{{exit}} {{print}}' {install_sh})"
|
|
eval "$(awk '
|
|
/^[a-z_][a-z_]*\\(\\)/ {{ in_fn=1 }}
|
|
in_fn {{ print }}
|
|
in_fn && /^\\}}$/ {{ in_fn=0 }}
|
|
' {install_sh})"
|
|
INSTALL_CHANNEL="{install_channel}"
|
|
version="{version}"
|
|
{block}
|
|
printf '%s\\n' "$metadata_step"
|
|
""")
|
|
return subprocess.run(["bash", "-c", script], capture_output=True, text=True)
|
|
|
|
|
|
def test_install_sh_logging_falls_back_to_plain_text_when_not_tty() -> None:
|
|
result = _run_logging_snippet(
|
|
"""
|
|
warn "check config"
|
|
success "installed"
|
|
step "[1/4] Fetching metadata"
|
|
"""
|
|
)
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert "\x1b[" not in result.stdout + result.stderr
|
|
assert "Warning: check config" in result.stderr
|
|
assert "Success: installed" in result.stdout
|
|
assert "[1/4] Fetching metadata" in result.stdout
|
|
|
|
|
|
def test_install_sh_die_falls_back_to_plain_text_when_not_tty() -> None:
|
|
result = _run_logging_snippet('die "missing curl"')
|
|
|
|
assert result.returncode == 1
|
|
assert "\x1b[" not in result.stderr
|
|
assert "Error: missing curl" in result.stderr
|
|
|
|
|
|
def test_install_sh_defines_tty_aware_ansi_formatting() -> None:
|
|
source = INSTALL_SH.read_text()
|
|
|
|
assert "if [ -t 1 ]; then" in source
|
|
assert "COLOR_GREEN=$'\\033[32m'" in source
|
|
assert "COLOR_YELLOW=$'\\033[33m'" in source
|
|
assert "COLOR_RED=$'\\033[31m'" in source
|
|
assert "success()" in source
|
|
|
|
|
|
def test_install_sh_success_screen_has_visual_structure() -> None:
|
|
result = _run_logging_snippet("print_success_screen 2026.4.1")
|
|
output = result.stdout + result.stderr
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert "--------------------------------------------" in output
|
|
assert "Success: Welcome to OpenSRE" in output
|
|
assert "opensre v2026.4.1 installed successfully" in output
|
|
assert "Next steps:" in output
|
|
|
|
|
|
def test_install_sh_contains_auto_onboarding_launch_hook() -> None:
|
|
source = INSTALL_SH.read_text()
|
|
|
|
assert "OPENSRE_AUTO_LAUNCH" in source
|
|
assert "launch_onboarding_after_install" in source
|
|
assert '"$installed_binary" onboard' in source
|
|
|
|
|
|
def test_install_sh_auto_onboarding_piped_installs_reattach_dev_tty() -> None:
|
|
"""Piped ``curl … | bash`` installs auto-launch onboarding via /dev/tty.
|
|
|
|
stdin is the curl pipe there, so the wizard's stdin is reattached to the
|
|
controlling terminal — but only when ``stty -g`` confirms the terminal can
|
|
actually be controlled. Where that tcgetattr fails, the full-screen wizard
|
|
would die with a terminal I/O error mid-render (issue #3273), so the
|
|
launch is skipped instead.
|
|
"""
|
|
source = INSTALL_SH.read_text()
|
|
|
|
assert "controlling_tty_usable" in source
|
|
assert "stty -g </dev/tty" in source
|
|
assert "run_onboarding </dev/tty" in source
|
|
# Only stdin is reattached; redirecting stdout to /dev/tty as well was the
|
|
# original #3273 failure mode.
|
|
assert "</dev/tty >/dev/tty 2>&1" not in source
|
|
|
|
|
|
def test_install_sh_auto_onboarding_piped_install_launches_via_pty(tmp_path: Path) -> None:
|
|
"""Simulate a piped install inside a real pty: onboarding must launch
|
|
with its stdin reattached to the terminal."""
|
|
import os
|
|
import pty
|
|
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
fake_binary = bin_dir / "opensre"
|
|
fake_binary.write_text(
|
|
"#!/usr/bin/env bash\n"
|
|
'if [ -t 0 ]; then echo "ONBOARD_STDIN_IS_TTY"; else echo "ONBOARD_STDIN_NOT_TTY"; fi\n'
|
|
)
|
|
fake_binary.chmod(0o755)
|
|
|
|
script = textwrap.dedent(f"""\
|
|
eval "$(awk '/^REPO=/{{exit}} {{print}}' {_INSTALL_SH_SHELL})"
|
|
eval "$(awk '
|
|
/^[a-z_][a-z_]*\\(\\)/ {{ in_fn=1 }}
|
|
in_fn {{ print }}
|
|
in_fn && /^\\}}$/ {{ in_fn=0 }}
|
|
' {_INSTALL_SH_SHELL})"
|
|
INSTALL_DIR={shlex.quote(str(bin_dir))}
|
|
BIN_NAME="opensre"
|
|
platform="linux"
|
|
launch_onboarding_after_install </dev/null
|
|
""")
|
|
|
|
pid, controller_fd = pty.fork()
|
|
if pid == 0: # pragma: no cover - child process replaced by bash
|
|
os.execvp("bash", ["bash", "-c", script])
|
|
|
|
chunks = []
|
|
try:
|
|
while True:
|
|
try:
|
|
chunk = os.read(controller_fd, 4096)
|
|
except OSError: # Linux raises EIO once the child side closes
|
|
break
|
|
if not chunk:
|
|
break
|
|
chunks.append(chunk)
|
|
finally:
|
|
os.close(controller_fd)
|
|
_, wait_status = os.waitpid(pid, 0)
|
|
|
|
output = b"".join(chunks).decode(errors="replace")
|
|
assert os.waitstatus_to_exitcode(wait_status) == 0, output
|
|
assert "Launching opensre onboard" in output
|
|
assert "ONBOARD_STDIN_IS_TTY" in output
|
|
|
|
|
|
def test_install_sh_auto_onboarding_skips_piped_install_on_windows(tmp_path: Path) -> None:
|
|
"""Git Bash /dev/tty emulation is not trusted: piped installs on the
|
|
windows platform never auto-launch."""
|
|
import os
|
|
import pty
|
|
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
fake_binary = bin_dir / "opensre"
|
|
fake_binary.write_text('#!/usr/bin/env bash\necho "ONBOARD_RAN"\n')
|
|
fake_binary.chmod(0o755)
|
|
|
|
script = textwrap.dedent(f"""\
|
|
eval "$(awk '/^REPO=/{{exit}} {{print}}' {_INSTALL_SH_SHELL})"
|
|
eval "$(awk '
|
|
/^[a-z_][a-z_]*\\(\\)/ {{ in_fn=1 }}
|
|
in_fn {{ print }}
|
|
in_fn && /^\\}}$/ {{ in_fn=0 }}
|
|
' {_INSTALL_SH_SHELL})"
|
|
INSTALL_DIR={shlex.quote(str(bin_dir))}
|
|
BIN_NAME="opensre"
|
|
platform="windows"
|
|
launch_onboarding_after_install </dev/null
|
|
""")
|
|
|
|
pid, controller_fd = pty.fork()
|
|
if pid == 0: # pragma: no cover - child process replaced by bash
|
|
os.execvp("bash", ["bash", "-c", script])
|
|
|
|
chunks = []
|
|
try:
|
|
while True:
|
|
try:
|
|
chunk = os.read(controller_fd, 4096)
|
|
except OSError:
|
|
break
|
|
if not chunk:
|
|
break
|
|
chunks.append(chunk)
|
|
finally:
|
|
os.close(controller_fd)
|
|
_, wait_status = os.waitpid(pid, 0)
|
|
|
|
output = b"".join(chunks).decode(errors="replace")
|
|
assert os.waitstatus_to_exitcode(wait_status) == 0, output
|
|
assert "ONBOARD_RAN" not in output
|
|
assert "Launching opensre onboard" not in output
|
|
|
|
|
|
def test_install_sh_auto_onboarding_noops_without_tty() -> None:
|
|
# stdin is redirected from /dev/null so the gate is exercised deterministically
|
|
# regardless of how the test runner's own stdin is wired.
|
|
result = _run_logging_snippet(
|
|
"""
|
|
INSTALL_DIR="/tmp"
|
|
BIN_NAME="opensre"
|
|
launch_onboarding_after_install </dev/null
|
|
"""
|
|
)
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert "Launching opensre onboard" not in result.stdout + result.stderr
|
|
|
|
|
|
def test_install_sh_has_step_for_explicit_version_fetch() -> None:
|
|
result = _run_release_metadata_step(version="2026.4.29")
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert "[1/6] Fetching release metadata for v2026.4.29" in result.stdout
|
|
|
|
|
|
def test_install_sh_defaults_to_main_build_channel() -> None:
|
|
source = INSTALL_SH.read_text()
|
|
|
|
assert 'INSTALL_CHANNEL="${OPENSRE_INSTALL_CHANNEL:-main}"' in source
|
|
assert 'MAIN_RELEASE_TAG="${OPENSRE_MAIN_RELEASE_TAG:-main-build}"' in source
|
|
assert "releases/tags/${MAIN_RELEASE_TAG}" in source
|
|
assert "releases/tags/nightly" not in source
|
|
|
|
|
|
def test_install_sh_defines_progress_helpers() -> None:
|
|
source = INSTALL_SH.read_text()
|
|
|
|
for helper in (
|
|
"is_interactive_terminal()",
|
|
"terminal_supports_unicode()",
|
|
"terminal_columns()",
|
|
"truncate_text()",
|
|
"friendly_progress_label()",
|
|
"print_installer_header()",
|
|
"progress_frame()",
|
|
"draw_progress()",
|
|
"finish_progress()",
|
|
"run_with_progress()",
|
|
"capture_with_progress()",
|
|
"binary_app_root()",
|
|
"install_binary_app()",
|
|
"print_binary_diagnostics()",
|
|
):
|
|
assert helper in source
|
|
|
|
assert "OPENSRE_INSTALL_VERBOSE" in source
|
|
assert "\\033[?25h" in source
|
|
assert 'trap \'kill "$command_pid"' in source
|
|
assert "\\033[2J" not in source
|
|
assert "preparing installer" not in source
|
|
|
|
|
|
def test_install_sh_draw_progress_fits_terminal_width_with_long_labels() -> None:
|
|
long_checksum = (
|
|
"[4/6] Downloading and verifying checksum (opensre_main_darwin-arm64.tar.gz.sha256)"
|
|
)
|
|
result = _run_logging_snippet(
|
|
f"""
|
|
terminal_columns() {{ printf '60\\n'; }}
|
|
terminal_supports_unicode() {{ return 1; }}
|
|
draw_progress {shlex.quote(long_checksum)} 9
|
|
"""
|
|
)
|
|
output = result.stdout + result.stderr
|
|
visible_segments = [
|
|
_visible_terminal_text(segment) for segment in re.split(r"[\r\n]", output) if segment
|
|
]
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert visible_segments
|
|
assert all(len(segment) <= 60 for segment in visible_segments)
|
|
assert "verifying checksum" in visible_segments[-1]
|
|
assert "opensre_main_darwin-arm64" not in visible_segments[-1]
|
|
|
|
|
|
def test_install_sh_animated_repaints_do_not_wrap_or_leave_long_label_residue() -> None:
|
|
long_checksum = (
|
|
"[4/6] Downloading and verifying checksum (opensre_main_darwin-arm64.tar.gz.sha256)"
|
|
)
|
|
result = _run_logging_snippet(
|
|
f"""
|
|
is_interactive_terminal() {{ return 0; }}
|
|
terminal_columns() {{ printf '56\\n'; }}
|
|
terminal_supports_unicode() {{ return 1; }}
|
|
run_with_progress {shlex.quote(long_checksum)} bash -c 'sleep 0.25'
|
|
"""
|
|
)
|
|
output = result.stdout + result.stderr
|
|
animated_segments = [
|
|
_visible_terminal_text(segment)
|
|
for segment in re.split(r"[\r\n]", output)
|
|
if "Installing OpenSRE" in _visible_terminal_text(segment)
|
|
]
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert animated_segments
|
|
assert all(len(segment) <= 56 for segment in animated_segments)
|
|
assert all("opensre_main_darwin-arm64" not in segment for segment in animated_segments)
|
|
|
|
|
|
def test_install_sh_header_is_stable_without_intro_animation() -> None:
|
|
result = _run_logging_snippet(
|
|
"""
|
|
is_interactive_terminal() { return 0; }
|
|
print_installer_header
|
|
"""
|
|
)
|
|
output = result.stdout + result.stderr
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert "OpenSRE Installer" in output
|
|
assert "Installing the OpenSRE CLI" in output
|
|
assert "preparing installer" not in output
|
|
assert "\x1b[2J" not in output
|
|
|
|
|
|
def test_install_sh_progress_plain_when_not_tty() -> None:
|
|
result = _run_logging_snippet(
|
|
"""
|
|
run_with_progress "Plain progress step" bash -c 'printf "work complete\\\\n"'
|
|
"""
|
|
)
|
|
output = result.stdout + result.stderr
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert "Plain progress step" in output
|
|
assert "work complete" in output
|
|
assert "\x1b[" not in output
|
|
assert "\r" not in output
|
|
|
|
|
|
def test_install_sh_capture_with_progress_keeps_stdout_value_clean() -> None:
|
|
result = _run_logging_snippet(
|
|
"""
|
|
is_interactive_terminal() { return 0; }
|
|
terminal_supports_unicode() { return 1; }
|
|
capture_with_progress captured_value "Capture value" bash -c 'printf "release-json"'
|
|
printf '\\nRESULT:%s\\n' "$captured_value"
|
|
"""
|
|
)
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert "RESULT:release-json" in result.stdout
|
|
assert "RESULT:Capture value" not in result.stdout
|
|
|
|
|
|
def test_install_sh_capture_with_progress_preserves_failure_status_and_logs() -> None:
|
|
result = _run_logging_snippet(
|
|
"""
|
|
if capture_with_progress captured_value "Failing capture step" bash -c 'echo hidden-out; echo hidden-err >&2; exit 7'; then
|
|
exit 99
|
|
else
|
|
progress_status=$?
|
|
fi
|
|
exit "$progress_status"
|
|
"""
|
|
)
|
|
output = result.stdout + result.stderr
|
|
|
|
assert result.returncode == 7
|
|
assert "Failing capture step" in output
|
|
assert "hidden-out" in output
|
|
assert "hidden-err" in output
|
|
|
|
|
|
def test_install_sh_run_with_progress_prints_captured_logs_on_failure() -> None:
|
|
result = _run_logging_snippet(
|
|
"""
|
|
is_interactive_terminal() { return 0; }
|
|
terminal_supports_unicode() { return 1; }
|
|
if run_with_progress "Failing progress step" bash -c 'echo hidden-out; echo hidden-err >&2; exit 7'; then
|
|
exit 99
|
|
else
|
|
progress_status=$?
|
|
fi
|
|
exit "$progress_status"
|
|
"""
|
|
)
|
|
output = result.stdout + result.stderr
|
|
|
|
assert result.returncode == 7
|
|
assert "Failing progress step failed" in output
|
|
assert "hidden-out" in output
|
|
assert "hidden-err" in output
|
|
|
|
|
|
def test_install_sh_verify_binary_failure_includes_diagnostics(tmp_path: Path) -> None:
|
|
fake_binary = tmp_path / "opensre"
|
|
fake_binary.write_text("#!/usr/bin/env sh\nexit 42\n", encoding="utf-8")
|
|
fake_binary.chmod(0o755)
|
|
|
|
result = _run_logging_snippet(
|
|
f"""
|
|
platform="linux"
|
|
verify_binary_version {shlex.quote(str(fake_binary))}
|
|
"""
|
|
)
|
|
output = result.stdout + result.stderr
|
|
|
|
assert result.returncode == 1
|
|
assert "Failed to execute opensre --version (exit 42)." in output
|
|
assert "Command output: <empty>" in output
|
|
assert "Binary diagnostics:" in output
|
|
assert str(fake_binary) in output
|
|
|
|
|
|
def test_install_sh_installs_pyinstaller_onedir_app(tmp_path: Path) -> None:
|
|
app_root = tmp_path / "opensre-app"
|
|
app_root.mkdir()
|
|
app_binary = app_root / "opensre"
|
|
app_binary.write_text("#!/usr/bin/env sh\nprintf 'opensre test\\n'\n", encoding="utf-8")
|
|
app_binary.chmod(0o755)
|
|
internal = app_root / "_internal"
|
|
internal.mkdir()
|
|
(internal / "payload.txt").write_text("bundled", encoding="utf-8")
|
|
install_dir = tmp_path / "bin"
|
|
destination = install_dir / "opensre"
|
|
|
|
result = _run_logging_snippet(
|
|
f"""
|
|
platform="linux"
|
|
BIN_NAME="opensre"
|
|
INSTALL_DIR={shlex.quote(str(install_dir))}
|
|
install_verified_binary {shlex.quote(str(app_binary))} {shlex.quote(str(destination))}
|
|
test -L {shlex.quote(str(destination))}
|
|
test -x {shlex.quote(str(destination))}
|
|
test -f {shlex.quote(str(install_dir / ".opensre-app" / "_internal" / "payload.txt"))}
|
|
{shlex.quote(str(destination))}
|
|
"""
|
|
)
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert "opensre test" in result.stdout
|
|
|
|
|
|
def test_install_sh_uses_six_step_extract_verify_install_labels() -> None:
|
|
source = INSTALL_SH.read_text()
|
|
|
|
assert "[4/6] Downloading and verifying checksum" in source
|
|
assert "[5/6] Extracting and verifying binary" in source
|
|
assert "[6/6] Installing ${BIN_NAME} to ${INSTALL_DIR}" in source
|
|
assert "[6/6] Extracting release archive" not in source
|
|
assert 'capture_with_progress installed_version "Verifying installed binary"' not in source
|
|
|
|
|
|
def test_zsh_writes_export_to_zshrc(tmp_path: Path) -> None:
|
|
result = _run(tmp_path, shell="/bin/zsh")
|
|
assert result.returncode == 0, result.stderr
|
|
zshrc = tmp_path / "home" / ".zshrc"
|
|
assert zshrc.exists()
|
|
assert f'export PATH="{tmp_path / "home" / _LOCAL_BIN}:$PATH"' in zshrc.read_text()
|
|
|
|
|
|
def test_bash_linux_writes_to_bashrc(tmp_path: Path) -> None:
|
|
result = _run(tmp_path, shell="/bin/bash", platform="linux")
|
|
assert result.returncode == 0, result.stderr
|
|
bashrc = tmp_path / "home" / ".bashrc"
|
|
assert bashrc.exists()
|
|
assert _LOCAL_BIN in bashrc.read_text()
|
|
|
|
|
|
def test_bash_macos_writes_to_bash_profile(tmp_path: Path) -> None:
|
|
result = _run(tmp_path, shell="/bin/bash", platform="darwin")
|
|
assert result.returncode == 0, result.stderr
|
|
bash_profile = tmp_path / "home" / ".bash_profile"
|
|
assert bash_profile.exists()
|
|
assert _LOCAL_BIN in bash_profile.read_text()
|
|
|
|
|
|
def test_fish_uses_fish_add_path(tmp_path: Path) -> None:
|
|
result = _run(tmp_path, shell="/usr/bin/fish")
|
|
assert result.returncode == 0, result.stderr
|
|
fish_config = tmp_path / "home" / ".config" / "fish" / "config.fish"
|
|
assert fish_config.exists()
|
|
assert "fish_add_path" in fish_config.read_text()
|
|
|
|
|
|
def test_unknown_shell_prints_manual_instructions(tmp_path: Path) -> None:
|
|
result = _run(tmp_path, shell="/bin/dash")
|
|
assert result.returncode == 0, result.stderr
|
|
home = tmp_path / "home"
|
|
assert not (home / ".zshrc").exists()
|
|
assert not (home / ".bashrc").exists()
|
|
assert not (home / ".bash_profile").exists()
|
|
assert "export PATH" in result.stdout or "export PATH" in result.stderr
|
|
|
|
|
|
def test_idempotent_no_duplicate_on_rerun(tmp_path: Path) -> None:
|
|
_run(tmp_path, shell="/bin/zsh")
|
|
_run(tmp_path, shell="/bin/zsh")
|
|
content = (tmp_path / "home" / ".zshrc").read_text()
|
|
export_lines = [ln for ln in content.splitlines() if _LOCAL_BIN in ln and "export PATH" in ln]
|
|
assert len(export_lines) == 1
|
|
|
|
|
|
def test_skips_when_install_dir_already_in_rc(tmp_path: Path) -> None:
|
|
home = tmp_path / "home"
|
|
home.mkdir(exist_ok=True)
|
|
idir = str(home / _LOCAL_BIN)
|
|
zshrc = home / ".zshrc"
|
|
zshrc.write_text(f'export PATH="$PATH:{idir}"\n')
|
|
original = zshrc.read_text()
|
|
|
|
result = _run(tmp_path, shell="/bin/zsh", install_dir=idir)
|
|
assert result.returncode == 0, result.stderr
|
|
assert zshrc.read_text() == original
|
|
|
|
|
|
def test_creates_rc_file_when_missing(tmp_path: Path) -> None:
|
|
result = _run(tmp_path, shell="/bin/zsh")
|
|
assert result.returncode == 0, result.stderr
|
|
assert (tmp_path / "home" / ".zshrc").exists()
|
|
|
|
|
|
def test_marker_comment_present(tmp_path: Path) -> None:
|
|
_run(tmp_path, shell="/bin/zsh")
|
|
content = (tmp_path / "home" / ".zshrc").read_text()
|
|
assert "# Added by opensre installer" in content
|
|
|
|
|
|
def test_post_install_message_mentions_source(tmp_path: Path) -> None:
|
|
result = _run(tmp_path, shell="/bin/zsh")
|
|
assert result.returncode == 0, result.stderr
|
|
combined = result.stdout + result.stderr
|
|
assert "source" in combined
|
|
|
|
|
|
def test_fish_creates_parent_dirs(tmp_path: Path) -> None:
|
|
result = _run(tmp_path, shell="/usr/bin/fish")
|
|
assert result.returncode == 0, result.stderr
|
|
assert (tmp_path / "home" / ".config" / "fish" / "config.fish").exists()
|
|
|
|
|
|
def test_readds_export_when_marker_present_but_line_removed(tmp_path: Path) -> None:
|
|
home = tmp_path / "home"
|
|
home.mkdir(exist_ok=True)
|
|
zshrc = home / ".zshrc"
|
|
zshrc.write_text("# Added by opensre installer\n")
|
|
|
|
result = _run(tmp_path, shell="/bin/zsh")
|
|
assert result.returncode == 0, result.stderr
|
|
content = zshrc.read_text()
|
|
assert _LOCAL_BIN in content
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers and tests for the post-install onboarding hint (issue #1153)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _run_post_install(
|
|
tmp_path: Path,
|
|
shell: str,
|
|
platform: str = "linux",
|
|
install_channel: str = "release",
|
|
installed_version: str = "2026.4.1",
|
|
dir_already_on_path: bool = False,
|
|
) -> subprocess.CompletedProcess[str]:
|
|
"""Run the real post-install function from install.sh with side-effects stubbed.
|
|
|
|
Unlike ``_run()``, which only calls ``configure_path()`` in isolation, this
|
|
helper calls ``finish_install()`` from install.sh (version print +
|
|
configure_path + onboarding hint) rather than copying those lines into the
|
|
test. That means if the hint is removed from install.sh the assertions will
|
|
correctly fail — there is no tautology.
|
|
|
|
The approach:
|
|
1. Load all function definitions from install.sh via awk.
|
|
2. Set every shell variable the post-install function needs.
|
|
3. Call the real post-install function.
|
|
"""
|
|
fake_home = tmp_path / "home"
|
|
fake_home.mkdir(exist_ok=True)
|
|
idir = str(fake_home / _LOCAL_BIN)
|
|
|
|
# When dir_already_on_path=True, configure_path() hits the early return
|
|
# and prints nothing. The onboarding hint must still appear.
|
|
path_value = f"{idir}:/usr/bin:/bin" if dir_already_on_path else "/usr/bin:/bin"
|
|
|
|
install_sh = _INSTALL_SH_SHELL
|
|
idir_shell = shlex.quote(idir)
|
|
home_shell = shlex.quote(str(fake_home))
|
|
|
|
script = textwrap.dedent(f"""\
|
|
# 1. Load every function definition from install.sh
|
|
eval "$(awk '
|
|
/^[a-z_][a-z_]*\\(\\)/ {{ in_fn=1 }}
|
|
in_fn {{ print }}
|
|
in_fn && /^\\}}$/ {{ in_fn=0 }}
|
|
' {install_sh})"
|
|
|
|
# 2. Set every variable the post-install function reads
|
|
BIN_NAME="opensre"
|
|
INSTALL_DIR={idir_shell}
|
|
INSTALL_CHANNEL="{install_channel}"
|
|
installed_version="{installed_version}"
|
|
platform="{platform}"
|
|
HOME={home_shell}
|
|
SHELL="{shell}"
|
|
PATH="{path_value}"
|
|
export HOME SHELL PATH
|
|
|
|
# 3. Execute the real post-install function from install.sh.
|
|
finish_install
|
|
""")
|
|
return subprocess.run(["bash", "-c", script], capture_output=True, text=True)
|
|
|
|
|
|
def test_install_sh_contains_onboarding_hint() -> None:
|
|
"""Contract test: the hint string must be present in install.sh source.
|
|
|
|
This is a direct grep of the script file — independent of any subprocess
|
|
execution — so it will fail immediately if the hint is removed from
|
|
install.sh even if the subprocess-based tests are somehow still passing.
|
|
"""
|
|
source = INSTALL_SH.read_text()
|
|
assert "${BIN_NAME:-opensre} onboard" in source, (
|
|
"install.sh does not contain the onboarding hint "
|
|
"(expected ``${BIN_NAME:-opensre} onboard`` in Next steps output)."
|
|
)
|
|
|
|
|
|
def test_install_ps1_contains_onboarding_hint() -> None:
|
|
"""Contract test: the hint string must be present in install.ps1 source."""
|
|
install_ps1 = Path(__file__).parents[2] / "install.ps1"
|
|
source = install_ps1.read_text()
|
|
assert "$exe onboard" in source, (
|
|
"install.ps1 does not contain the onboarding step "
|
|
'(expected a line with ``$exe onboard``, e.g. ``Write-Host " 1. Run $exe onboard"``).'
|
|
)
|
|
|
|
|
|
def test_onboarding_hint_shown_when_path_not_set(tmp_path: Path) -> None:
|
|
"""Hint appears on a first install where configure_path writes the rc file."""
|
|
result = _run_post_install(tmp_path, shell="/bin/zsh", dir_already_on_path=False)
|
|
assert result.returncode == 0, result.stderr
|
|
assert "opensre onboard" in result.stdout + result.stderr
|
|
|
|
|
|
def test_onboarding_hint_shown_when_path_already_set(tmp_path: Path) -> None:
|
|
"""Hint appears even when configure_path returns early (install dir already on PATH).
|
|
|
|
This is the silent-upgrade scenario that the old configure_path-only
|
|
helper could never cover: configure_path() hits the early return at
|
|
line 490 and outputs nothing, yet the user must still see the hint.
|
|
"""
|
|
result = _run_post_install(tmp_path, shell="/bin/zsh", dir_already_on_path=True)
|
|
assert result.returncode == 0, result.stderr
|
|
assert "opensre onboard" in result.stdout + result.stderr
|
|
|
|
|
|
def test_onboarding_hint_shown_for_bash_linux(tmp_path: Path) -> None:
|
|
"""Hint appears on bash/linux installs."""
|
|
result = _run_post_install(tmp_path, shell="/bin/bash", platform="linux")
|
|
assert result.returncode == 0, result.stderr
|
|
assert "opensre onboard" in result.stdout + result.stderr
|
|
|
|
|
|
def test_onboarding_hint_shown_for_main_channel(tmp_path: Path) -> None:
|
|
"""Hint appears when installing the rolling main build (not a versioned release)."""
|
|
result = _run_post_install(
|
|
tmp_path,
|
|
shell="/bin/zsh",
|
|
install_channel="main",
|
|
installed_version="main",
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
assert "opensre onboard" in result.stdout + result.stderr
|
|
|
|
|
|
def _run_ensure_on_path(
|
|
tmp_path: Path, path_dirs: list[str], platform: str = "linux"
|
|
) -> tuple[subprocess.CompletedProcess[str], Path]:
|
|
"""Call the real ensure_on_path() with INSTALL_DIR off PATH.
|
|
|
|
``path_dirs`` becomes the PATH visible to the function; the returned Path
|
|
is the populated install dir (never on that PATH).
|
|
"""
|
|
install_dir = tmp_path / "install-dir"
|
|
install_dir.mkdir()
|
|
binary = install_dir / "opensre"
|
|
binary.write_text("#!/usr/bin/env bash\necho opensre-ok\n")
|
|
binary.chmod(0o755)
|
|
|
|
path_value = ":".join([*path_dirs, "/usr/bin", "/bin"])
|
|
script = textwrap.dedent(f"""\
|
|
eval "$(awk '
|
|
/^[a-z_][a-z_]*\\(\\)/ {{ in_fn=1 }}
|
|
in_fn {{ print }}
|
|
in_fn && /^\\}}$/ {{ in_fn=0 }}
|
|
' {_INSTALL_SH_SHELL})"
|
|
log() {{ printf '%s\\n' "$*"; }}
|
|
warn() {{ printf 'Warning: %s\\n' "$*" >&2; }}
|
|
success() {{ printf 'Success: %s\\n' "$*"; }}
|
|
INSTALL_DIR={shlex.quote(str(install_dir))}
|
|
BIN_NAME="opensre"
|
|
platform="{platform}"
|
|
HOME={shlex.quote(str(tmp_path / "home"))}
|
|
SHELL="/bin/zsh"
|
|
PATH={shlex.quote(path_value)}
|
|
export HOME SHELL PATH
|
|
mkdir -p "$HOME"
|
|
ensure_on_path
|
|
""")
|
|
completed = subprocess.run(["bash", "-c", script], capture_output=True, text=True, cwd=tmp_path)
|
|
return completed, install_dir
|
|
|
|
|
|
def test_ensure_on_path_links_into_writable_path_dir(tmp_path: Path) -> None:
|
|
"""A writable dir already on PATH gets a symlink, so `opensre` works in
|
|
the current terminal immediately — no sudo, no rc-file edit needed."""
|
|
writable_dir = tmp_path / "on-path-bin"
|
|
writable_dir.mkdir()
|
|
|
|
result, install_dir = _run_ensure_on_path(tmp_path, [str(writable_dir)])
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
link = writable_dir / "opensre"
|
|
assert link.is_symlink()
|
|
assert link.resolve() == (install_dir / "opensre").resolve()
|
|
assert "Linked opensre into" in result.stdout
|
|
assert not (tmp_path / "home" / ".zshrc").exists()
|
|
|
|
|
|
def test_ensure_on_path_falls_back_to_rc_update_without_writable_dir(tmp_path: Path) -> None:
|
|
"""With no writable dir on PATH, the shell rc fallback still runs."""
|
|
result, _ = _run_ensure_on_path(tmp_path, [])
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
zshrc = tmp_path / "home" / ".zshrc"
|
|
assert zshrc.exists()
|
|
assert "install-dir" in zshrc.read_text()
|
|
|
|
|
|
def test_ensure_on_path_ignores_relative_path_entries(tmp_path: Path) -> None:
|
|
"""Relative PATH entries (e.g. ``.``) must never receive the symlink."""
|
|
result, _ = _run_ensure_on_path(tmp_path, ["."])
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert not (tmp_path / "opensre").exists()
|
|
assert (tmp_path / "home" / ".zshrc").exists()
|
|
|
|
|
|
def test_onboarding_hint_appears_after_version_line(tmp_path: Path) -> None:
|
|
"""The onboarding hint must appear AFTER the 'Installed opensre v...' line."""
|
|
result = _run_post_install(tmp_path, shell="/bin/zsh", installed_version="2026.4.1")
|
|
assert result.returncode == 0, result.stderr
|
|
output = result.stdout + result.stderr
|
|
installed_pos = output.find("Installed opensre")
|
|
onboard_pos = output.find("opensre onboard")
|
|
assert installed_pos != -1, "'Installed opensre' line missing from output"
|
|
assert onboard_pos != -1, "'opensre onboard' hint missing from output"
|
|
assert onboard_pos > installed_pos, (
|
|
"Onboarding hint must come after the install confirmation line"
|
|
)
|