Skip to content
Merged
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
24 changes: 24 additions & 0 deletions .github/workflows/install-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Install Script Tests

on:
push:
branches:
- "**"
pull_request:

jobs:
test-install-script:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Lint install.sh
if: runner.os == 'Linux'
run: shellcheck install.sh

- name: Run install.sh tests
run: sh test/install_test.sh
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ Thumbs.db
*.png
*.pdf
!web/*.js
_docs
.scratchpad/
CLAUDE.md
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ VERSION?=$(shell cat version)
LDFLAGS=-ldflags '-s -w -X github.com/coolamit/mermaid-cli/internal/cli.Version=$(VERSION)'

# Declare phony targets
.PHONY: ssh-cmd up down build clean test tidy format build-linux-x64 build-linux-arm64 build-macos-x64 build-macos-arm64 build-all docker-up docker-down docker-run docker-run-aloof docker-clean
.PHONY: ssh-cmd up down build clean test test-install tidy format build-linux-x64 build-linux-arm64 build-macos-x64 build-macos-arm64 build-all docker-up docker-down docker-run docker-run-aloof docker-clean

# Common function definitions
define CURRENT_HOMESTEAD_STATUS
Expand Down Expand Up @@ -62,6 +62,10 @@ build:
test:
@$(call SSH_EXEC,$(GO) clean -testcache && $(GO) test ./...)

# Tests for install.sh (pure sh, runs on host — no VM needed)
test-install:
@sh test/install_test.sh

tidy:
@$(call SSH_EXEC,$(GO) mod tidy)

Expand Down
34 changes: 23 additions & 11 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ need() {
command -v "$1" >/dev/null 2>&1 || die "Required tool '$1' not found. Please install it and try again."
}

# Print why a browser is required and how to proceed, then exit.
# $1 = reason automatic installation was not possible
browser_required() {
echo ""
echo " mmd-cli requires Chrome or Chromium to render diagrams."
echo " $1"
echo ""
echo " Please install one of the following, then run this installer again:"
echo " Google Chrome - https://www.google.com/chrome/"
echo " Chromium - https://www.chromium.org/getting-involved/download-chromium/"
echo ""
die "Chrome or Chromium is required. Install one and re-run this script."
}

# ── Detect OS and architecture ───────────────────────────────────────────────

detect_platform() {
Expand Down Expand Up @@ -81,20 +95,15 @@ ensure_chrome() {
info "Installing Chromium via yum..."
sudo yum install -y chromium
else
die "Could not detect a supported package manager. Please install Chrome or Chromium manually."
browser_required "No supported package manager (apt/dnf/yum) was found to install one automatically."
fi
;;
macos)
if command -v brew >/dev/null 2>&1; then
info "Installing Chromium via Homebrew..."
brew install --cask chromium
info "Installing Google Chrome via Homebrew..."
brew install --cask google-chrome
else
echo ""
echo " Please install Chrome or Chromium manually:"
echo " Google Chrome - https://www.google.com/chrome/"
echo " Chromium - https://www.chromium.org/getting-involved/download-chromium/"
echo ""
die "No supported installation method found for Chrome/Chromium on macOS."
browser_required "Homebrew is not available to install one automatically."
fi
;;
esac
Expand All @@ -119,7 +128,7 @@ get_version() {
info "Fetching latest release version..."
VERSION=$(curl -sSL "https://api.github.com/repos/${REPO}/releases/latest" \
| grep '"tag_name"' \
| sed -E 's/.*"tag_name":\s*"([^"]+)".*/\1/')
| sed -E 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/')

[ -n "$VERSION" ] || die "Could not determine latest version. Check https://github.com/${REPO}/releases"

Expand Down Expand Up @@ -217,4 +226,7 @@ main() {
info "Done!"
}

main "$@"
# Skip execution when sourced for testing (see test/install_test.sh)
if [ -z "${INSTALL_SH_TEST:-}" ]; then
main "$@"
fi
20 changes: 20 additions & 0 deletions test/fixtures/github_release.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"url": "https://api.github.com/repos/coolamit/mermaid-cli/releases/200000001",
"html_url": "https://github.com/coolamit/mermaid-cli/releases/tag/v0.1.1",
"id": 200000001,
"tag_name": "v0.1.1",
"target_commitish": "master",
"name": "v0.1.1",
"draft": false,
"prerelease": false,
"created_at": "2026-02-11T00:00:00Z",
"published_at": "2026-02-11T00:30:00Z",
"assets": [
{
"name": "mmd-cli_0.1.1_macos_arm64.tar.gz",
"content_type": "application/gzip",
"browser_download_url": "https://github.com/coolamit/mermaid-cli/releases/download/v0.1.1/mmd-cli_0.1.1_macos_arm64.tar.gz"
}
],
"body": "Release notes"
}
153 changes: 153 additions & 0 deletions test/install_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/bin/sh
# Unit tests for install.sh.
#
# Usage: sh test/install_test.sh
#
# The script under test is sourced with INSTALL_SH_TEST=1 so its functions can
# be called without running the installer. Each test runs in a subshell because
# install.sh enables `set -e` and its failure paths call exit. External
# commands (curl, uname) are shadowed with shell functions where needed.
#
# Set INSTALL_SH to test a different copy of the script (used to verify that
# these tests catch known-bad versions).

TEST_DIR=$(cd "$(dirname "$0")" && pwd)
REPO_DIR=$(dirname "$TEST_DIR")
INSTALL_SH="${INSTALL_SH:-$REPO_DIR/install.sh}"
FIXTURE="$TEST_DIR/fixtures/github_release.json"

PASS=0
FAIL=0

pass() { PASS=$((PASS + 1)); printf 'ok - %s\n' "$1"; }
fail() { FAIL=$((FAIL + 1)); printf 'FAIL - %s\n' "$1"; }

assert_eq() {
# $1 = test name, $2 = expected, $3 = actual
if [ "$2" = "$3" ]; then
pass "$1"
else
fail "$1 (expected '$2', got '$3')"
fi
}

assert_contains() {
# $1 = test name, $2 = needle, $3 = haystack
case "$3" in
*"$2"*) pass "$1" ;;
*) fail "$1 (output does not contain '$2')" ;;
esac
}

[ -f "$INSTALL_SH" ] || { echo "install.sh not found at $INSTALL_SH" >&2; exit 1; }
[ -f "$FIXTURE" ] || { echo "fixture not found at $FIXTURE" >&2; exit 1; }

# ── Syntax check ─────────────────────────────────────────────────────────────

if sh -n "$INSTALL_SH" 2>/dev/null; then
pass "install.sh parses (sh -n)"
else
fail "install.sh parses (sh -n)"
fi

# ── get_version: parse latest release from GitHub API ────────────────────────
# Regression test for issue #3: the sed pattern must work on BSD sed (macOS)
# as well as GNU sed (Linux).

out=$(
INSTALL_SH_TEST=1
. "$INSTALL_SH"
curl() { cat "$FIXTURE"; }
get_version >/dev/null 2>&1
printf '%s|%s' "$VERSION" "$VERSION_NUM"
) || out="(subshell failed: $out)"
assert_eq "get_version parses tag_name from API response" "v0.1.1|0.1.1" "$out"

# ── get_version: explicit version argument ───────────────────────────────────

out=$(
INSTALL_SH_TEST=1
. "$INSTALL_SH"
get_version "v1.2.3" >/dev/null 2>&1
printf '%s|%s' "$VERSION" "$VERSION_NUM"
) || out="(subshell failed: $out)"
assert_eq "get_version strips leading v from explicit version" "v1.2.3|1.2.3" "$out"

# ── detect_platform: OS/arch mapping ─────────────────────────────────────────

out=$(
INSTALL_SH_TEST=1
. "$INSTALL_SH"
uname() { case "$1" in (-s) echo "Linux" ;; (-m) echo "x86_64" ;; esac; }
detect_platform >/dev/null 2>&1
printf '%s|%s' "$OS" "$ARCH"
) || out="(subshell failed: $out)"
assert_eq "detect_platform maps Linux/x86_64" "linux|x64" "$out"

out=$(
INSTALL_SH_TEST=1
. "$INSTALL_SH"
uname() { case "$1" in (-s) echo "Darwin" ;; (-m) echo "arm64" ;; esac; }
detect_platform >/dev/null 2>&1
printf '%s|%s' "$OS" "$ARCH"
) || out="(subshell failed: $out)"
assert_eq "detect_platform maps Darwin/arm64" "macos|arm64" "$out"

rc=0
(
INSTALL_SH_TEST=1
. "$INSTALL_SH"
uname() { echo "FreeBSD"; }
detect_platform
) >/dev/null 2>&1 || rc=$?
assert_eq "detect_platform rejects unsupported OS with exit 1" "1" "$rc"

# ── has_chrome: browser detection ────────────────────────────────────────────

STUB_DIR=$(mktemp -d)
trap 'rm -rf "$STUB_DIR"' EXIT

# Negative case only works where the hardcoded macOS app bundles are absent
# (they exist on developer Macs and on GitHub's macOS runners).
if [ -d "/Applications/Google Chrome.app" ] || [ -d "/Applications/Chromium.app" ]; then
echo "skip - has_chrome returns 1 when no browser present (browser app installed on this machine)"
else
rc=0
(
INSTALL_SH_TEST=1
. "$INSTALL_SH"
PATH="$STUB_DIR"
has_chrome
) >/dev/null 2>&1 || rc=$?
assert_eq "has_chrome returns 1 when no browser present" "1" "$rc"
fi

printf '#!/bin/sh\n' > "$STUB_DIR/chromium"
chmod +x "$STUB_DIR/chromium"
rc=0
(
INSTALL_SH_TEST=1
. "$INSTALL_SH"
PATH="$STUB_DIR"
has_chrome
) >/dev/null 2>&1 || rc=$?
assert_eq "has_chrome finds chromium on PATH" "0" "$rc"

# ── browser_required: message and exit code ──────────────────────────────────

rc=0
out=$(
INSTALL_SH_TEST=1
. "$INSTALL_SH"
browser_required "Test reason line." 2>&1
) || rc=$?
assert_eq "browser_required exits 1" "1" "$rc"
assert_contains "browser_required states the requirement" "requires Chrome or Chromium" "$out"
assert_contains "browser_required echoes the reason" "Test reason line." "$out"
assert_contains "browser_required says to re-run the installer" "run this installer again" "$out"

# ── Summary ──────────────────────────────────────────────────────────────────

echo ""
echo "passed: $PASS failed: $FAIL"
[ "$FAIL" -eq 0 ] || exit 1
Loading