From 005209703e8270be01336da11cefe8f74984b01f Mon Sep 17 00:00:00 2001 From: Micheal Wilkinson Date: Thu, 19 Mar 2026 10:57:25 +0000 Subject: [PATCH] Adding a set of behavioural tests --- README.markdown => README.md | 51 ++++++-- docker/behavior/Dockerfile | 13 ++ script/run-behavior-suite-docker.sh | 50 +++++++ test/behavior/behavior_suite.sh | 193 ++++++++++++++++++++++++++++ 4 files changed, 296 insertions(+), 11 deletions(-) rename README.markdown => README.md (76%) create mode 100644 docker/behavior/Dockerfile create mode 100755 script/run-behavior-suite-docker.sh create mode 100755 test/behavior/behavior_suite.sh diff --git a/README.markdown b/README.md similarity index 76% rename from README.markdown rename to README.md index 1ba457f..68a398b 100644 --- a/README.markdown +++ b/README.md @@ -13,8 +13,8 @@ Homesick is sorta like [rip](http://github.com/defunkt/rip), but for dotfiles. I We call a repository that is compatible with homesick to be a 'castle'. To act as a castle, a repository must be organized like so: -* Contains a 'home' directory -* 'home' contains any number of files and directories that begin with '.' +- Contains a 'home' directory +- 'home' contains any number of files and directories that begin with '.' To get started, install homesick first: @@ -82,6 +82,35 @@ If you ever want to see what version of homesick you have type: homesick version|-v|--version +## Docker Behavior Validation (Ruby vs Go) + +To preserve behavior while migrating from Ruby to Go, this repository includes a containerized behavior suite that runs Homesick commands in a clean environment and validates filesystem and git outcomes. + +The suite creates a dedicated git repository inside the container to act as a test castle fixture, then validates behavior for: + +- clone +- link / unlink +- track +- list / show_path +- status / diff +- version + +Run against the current Ruby implementation: + + ./script/run-behavior-suite-docker.sh + +Show full command output (including internal Homesick status lines) when needed: + + ./script/run-behavior-suite-docker.sh --verbose + +This runner now builds an Alpine-based container and installs runtime dependencies with `apk`, so behavior validation is not tied to a Ruby base image. + +Run against a future Go binary (example path): + + HOMESICK_CMD="/workspace/dist/homesick" ./script/run-behavior-suite-docker.sh + +The command under test is controlled with the `HOMESICK_CMD` environment variable. By running the same suite for both implementations, you can verify parity at the behavior level. + ## .homesick_subdir `homesick link` basically makes symlink to only first depth in `castle/home`. If you want to link nested files/directories, please use .homesick_subdir. @@ -134,7 +163,7 @@ Or `homesick track NESTED_FILE CASTLE` adds a line automatically. For example: castle/.homesick_subdir .config - .emacs.d + .emacs.d home directory @@ -164,17 +193,17 @@ and castle Homesick is tested on the following Ruby versions: -* 2.2.6 -* 2.3.3 -* 2.4.0 +- 2.2.6 +- 2.3.3 +- 2.4.0 ## Note on Patches/Pull Requests -* Fork the project. -* Make your feature addition or bug fix. -* Add tests for it. This is important so I don't break it in a future version unintentionally. -* Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) -* Send me a pull request. Bonus points for topic branches. +- Fork the project. +- Make your feature addition or bug fix. +- Add tests for it. This is important so I don't break it in a future version unintentionally. +- Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) +- Send me a pull request. Bonus points for topic branches. ## Need homesick without the ruby dependency? diff --git a/docker/behavior/Dockerfile b/docker/behavior/Dockerfile new file mode 100644 index 0000000..41bffbc --- /dev/null +++ b/docker/behavior/Dockerfile @@ -0,0 +1,13 @@ +FROM alpine:3.21 + +RUN apk add --no-cache \ + bash \ + ca-certificates \ + git \ + ruby \ + ruby-thor + +WORKDIR /workspace +COPY . /workspace + +ENTRYPOINT ["/workspace/test/behavior/behavior_suite.sh"] diff --git a/script/run-behavior-suite-docker.sh b/script/run-behavior-suite-docker.sh new file mode 100755 index 0000000..af33205 --- /dev/null +++ b/script/run-behavior-suite-docker.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${HOMESICK_CMD:=ruby /workspace/bin/homesick}" +behavior_verbose="${BEHAVIOR_VERBOSE:-0}" + +while [[ $# -gt 0 ]]; do + case "$1" in + -v|--verbose) + echo "Enabling verbose output for behavior suite" + behavior_verbose=1 + ;; + *) + echo "Unknown argument: $1" >&2 + echo "Usage: $0 [--verbose]" >&2 + exit 1 + ;; + esac + shift +done + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/.." && pwd)" + +run_docker_build() { + echo "Building Docker image for behavior suite..." + local build_log + + if [[ "$behavior_verbose" == "1" ]]; then + docker-buildx build -f "$repo_root/docker/behavior/Dockerfile" -t homesick-behavior:latest "$repo_root" + return + fi + + build_log="$(mktemp)" + if ! docker-buildx build -f "$repo_root/docker/behavior/Dockerfile" -t homesick-behavior:latest "$repo_root" >"$build_log" 2>&1; then + cat "$build_log" >&2 + rm -f "$build_log" + exit 1 + fi + + rm -f "$build_log" +} + +run_docker_build + +echo "Running behavior suite in Docker container..." +docker run --rm \ + -e HOMESICK_CMD="$HOMESICK_CMD" \ + -e BEHAVIOR_VERBOSE="$behavior_verbose" \ + homesick-behavior:latest diff --git a/test/behavior/behavior_suite.sh b/test/behavior/behavior_suite.sh new file mode 100755 index 0000000..4bceaaf --- /dev/null +++ b/test/behavior/behavior_suite.sh @@ -0,0 +1,193 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${HOMESICK_CMD:=ruby /workspace/bin/homesick}" +: "${BEHAVIOR_VERBOSE:=0}" + +RUN_OUTPUT="" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +pass() { + if [[ -t 1 ]]; then + printf ' \033[32mPassed\033[0m\n' + else + echo " Passed" + fi +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + -v|--verbose) + BEHAVIOR_VERBOSE=1 + ;; + *) + fail "unknown argument: $1" + ;; + esac + shift + done +} + +run_git() { + if [[ "$BEHAVIOR_VERBOSE" == "1" ]]; then + git "$@" + else + git "$@" >/dev/null 2>&1 + fi +} + +assert_path_exists() { + local path="$1" + [[ -e "$path" ]] || fail "expected path to exist: $path" +} + +assert_path_missing() { + local path="$1" + [[ ! -e "$path" ]] || fail "expected path to be missing: $path" +} + +assert_symlink_target() { + local link_path="$1" + local expected_target="$2" + [[ -L "$link_path" ]] || fail "expected symlink: $link_path" + local actual_target + actual_target="$(readlink "$link_path")" + [[ "$actual_target" == "$expected_target" ]] || fail "expected symlink target '$expected_target' but got '$actual_target'" +} + +run_homesick() { + local out_file + local output + out_file="$(mktemp)" + if ! bash -lc "$HOMESICK_CMD $*" >"$out_file" 2>&1; then + cat "$out_file" >&2 + rm -f "$out_file" + fail "homesick command failed: $*" + fi + + output="$(cat "$out_file")" + RUN_OUTPUT="$output" + + if [[ "$BEHAVIOR_VERBOSE" == "1" && -n "$output" ]]; then + printf '%s\n' "$output" + fi + + rm -f "$out_file" +} + +setup_remote_castle() { + local remote_dir="$1" + local work_dir="$2" + + mkdir -p "$remote_dir" + run_git init --bare "$remote_dir/base.git" + + mkdir -p "$work_dir/base" + pushd "$work_dir/base" >/dev/null + run_git init + run_git config user.email "behavior@test.local" + run_git config user.name "Behavior Test" + + mkdir -p home/.config/myapp + echo "set number" > home/.vimrc + echo "export PATH=\"$PATH:$HOME/bin\"" > home/.zshrc + echo "option=true" > home/.config/myapp/config.toml + printf '.config\n' > .homesick_subdir + + run_git add . + run_git commit -m "initial castle" + run_git remote add origin "$remote_dir/base.git" + run_git push -u origin master + popd >/dev/null +} + +setup_local_test_file() { + mkdir -p "$HOME/.local/bin" + echo "#!/usr/bin/env bash" > "$HOME/.local/bin/tool" + chmod +x "$HOME/.local/bin/tool" +} + +run_suite() { + local tmp_root + tmp_root="$(mktemp -d)" + trap "rm -rf '$tmp_root'" EXIT + + export HOME="$tmp_root/home" + mkdir -p "$HOME" + + local remote_root="$tmp_root/remote" + local work_root="$tmp_root/work" + + setup_remote_castle "$remote_root" "$work_root" + + echo "[1/7] clone" + run_homesick "clone file://$remote_root/base.git parity-castle" + assert_path_exists "$HOME/.homesick/repos/parity-castle/.git" + assert_path_exists "$HOME/.homesick/repos/parity-castle/home/.vimrc" + pass + + echo "[2/7] link" + run_homesick "link parity-castle" + assert_symlink_target "$HOME/.vimrc" "$HOME/.homesick/repos/parity-castle/home/.vimrc" + assert_symlink_target "$HOME/.zshrc" "$HOME/.homesick/repos/parity-castle/home/.zshrc" + assert_path_exists "$HOME/.config/myapp" + assert_symlink_target "$HOME/.config/myapp" "$HOME/.homesick/repos/parity-castle/home/.config/myapp" + pass + + echo "[3/7] unlink" + run_homesick "unlink parity-castle" + assert_path_missing "$HOME/.vimrc" + assert_path_missing "$HOME/.zshrc" + assert_path_exists "$HOME/.config" + assert_path_missing "$HOME/.config/myapp" + pass + + echo "[4/7] relink + track" + run_homesick "link parity-castle" + setup_local_test_file + run_homesick "track $HOME/.local/bin/tool parity-castle" + assert_symlink_target "$HOME/.local/bin/tool" "$HOME/.homesick/repos/parity-castle/home/.local/bin/tool" + assert_path_exists "$HOME/.homesick/repos/parity-castle/.homesick_subdir" + grep -q "^.local/bin$" "$HOME/.homesick/repos/parity-castle/.homesick_subdir" || fail "expected .homesick_subdir to contain .local/bin" + pass + + echo "[5/7] list and show_path" + local list_output + run_homesick "list" + list_output="$RUN_OUTPUT" + [[ "$list_output" == *"parity-castle"* ]] || fail "expected list output to include parity-castle" + local show_path_output + run_homesick "show_path parity-castle" + show_path_output="$RUN_OUTPUT" + [[ "$show_path_output" == *"$HOME/.homesick/repos/parity-castle"* ]] || fail "expected show_path output to include parity-castle path" + pass + + echo "[6/7] status and diff" + echo "change" >> "$HOME/.vimrc" + local status_output + run_homesick "status parity-castle" + status_output="$RUN_OUTPUT" + [[ "$status_output" == *"modified:"* ]] || fail "expected status output to include modified file" + local diff_output + run_homesick "diff parity-castle" + diff_output="$RUN_OUTPUT" + [[ "$diff_output" == *"diff --git"* ]] || fail "expected diff output to include git diff" + pass + + echo "[7/7] version" + local version_output + run_homesick "version" + version_output="$RUN_OUTPUT" + [[ "$version_output" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "expected semantic version output, got: $version_output" + pass + + echo "PASS: behavior suite completed for command: $HOMESICK_CMD" +} + +parse_args "$@" +run_suite