From 9a8c35f053b7151c971c6af0feb27d7e171d2a74 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah Date: Sat, 6 Jun 2026 11:09:28 -0700 Subject: [PATCH] Migrate caff and sort-in-place tools --- .github/workflows/tests.yml | 11 + CHANGELOG.md | 2 + CONTRIBUTING.md | 7 + README.md | 26 +- base_manifest.yaml | 2 + bin/caff | 19 ++ bin/sort-in-place | 19 ++ cli/bash/commands/caff/README.md | 11 + cli/bash/commands/caff/caff.sh | 136 +++++++++ cli/bash/commands/caff/tests/caff.bats | 271 ++++++++++++++++++ cli/bash/commands/sort-in-place/README.md | 11 + .../commands/sort-in-place/sort-in-place.sh | 76 +++++ .../sort-in-place/tests/sort-in-place.bats | 88 ++++++ docs/cli-layout.md | 9 + docs/tooling-boundary.md | 4 +- tests/bats_helper.bash | 11 + tests/validate.sh | 20 ++ 17 files changed, 713 insertions(+), 10 deletions(-) create mode 100755 bin/caff create mode 100755 bin/sort-in-place create mode 100644 cli/bash/commands/caff/README.md create mode 100644 cli/bash/commands/caff/caff.sh create mode 100644 cli/bash/commands/caff/tests/caff.bats create mode 100644 cli/bash/commands/sort-in-place/README.md create mode 100644 cli/bash/commands/sort-in-place/sort-in-place.sh create mode 100644 cli/bash/commands/sort-in-place/tests/sort-in-place.bats create mode 100644 tests/bats_helper.bash diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e222309..a86b788 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,5 +12,16 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install BATS + run: | + sudo apt-get update + sudo apt-get install -y bats + - name: Run validation run: tests/validate.sh + + - name: Run BATS tests + run: | + bats \ + cli/bash/commands/caff/tests/caff.bats \ + cli/bash/commands/sort-in-place/tests/sort-in-place.bats diff --git a/CHANGELOG.md b/CHANGELOG.md index a5eb14f..0c8ef0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to Base Platform Tools will be documented in this file. ### Added +- Migrated the `caff` and `sort-in-place` Bash utility CLIs from + `codeforester/base`. - Added the initial public repository scaffold for Base Platform Tools. - Added the Base-managed project manifest and validation contract. - Added the tooling boundary documentation. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e638ca7..b167bde 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -108,3 +108,10 @@ basectl test base-platform-tools ``` Use more specific tests once real tool implementations are added. + +For migrated Bash tools, run the BATS coverage: + +```bash +bats cli/bash/commands/caff/tests/caff.bats +bats cli/bash/commands/sort-in-place/tests/sort-in-place.bats +``` diff --git a/README.md b/README.md index 73b4eab..3a1272f 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,11 @@ The boundary is intentional: ## Current Status -This repository is in its initial scaffold stage. It has the initial CLI layout -for future Bash and Python tools, but it does not yet contain production utility -CLIs. +This repository is in its early stage. It has the initial CLI layout and hosts +the first migrated Bash utility CLIs from Base: -The existing `caff` and `sort` utilities remain in `codeforester/base` for now. -They can be migrated here later through separate issues and pull requests once -this repository baseline is stable. +- `caff` +- `sort-in-place` ## Getting Started @@ -68,6 +66,13 @@ List the commands declared by this repository: basectl run base-platform-tools --list ``` +Run the migrated utilities through Base: + +```bash +basectl run base-platform-tools caff -- --help +basectl run base-platform-tools sort-in-place -- --help +``` + ## What Belongs Here Good candidates for this repository include: @@ -100,12 +105,17 @@ Keep these outside Base Platform Tools: ```text . ├── bin/ -│ └── README.md +│ ├── README.md +│ ├── caff +│ └── sort-in-place ├── base_manifest.yaml ├── cli/ │ ├── README.md │ ├── bash/ -│ │ └── README.md +│ │ ├── README.md +│ │ └── commands/ +│ │ ├── caff/ +│ │ └── sort-in-place/ │ └── python/ │ ├── README.md │ └── base_platform_tools/ diff --git a/base_manifest.yaml b/base_manifest.yaml index 274d3c7..ec9a01b 100644 --- a/base_manifest.yaml +++ b/base_manifest.yaml @@ -7,5 +7,7 @@ test: command: tests/validate.sh commands: + caff: bin/caff cli-check: tests/validate.sh + sort-in-place: bin/sort-in-place validate: tests/validate.sh diff --git a/bin/caff b/bin/caff new file mode 100755 index 0000000..d2f4087 --- /dev/null +++ b/bin/caff @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +repo_root="$(cd -- "$(dirname -- "$0")/.." && pwd -P)" || exit 1 +command_path="$repo_root/cli/bash/commands/caff/caff.sh" + +if [[ -n "${BASE_HOME:-}" ]]; then + if [[ ! -x "$BASE_HOME/bin/basectl" ]]; then + printf 'ERROR: BASE_HOME is set but basectl was not found at %s/bin/basectl.\n' "$BASE_HOME" >&2 + exit 1 + fi + exec "$BASE_HOME/bin/basectl" "$command_path" "$@" +fi + +if command -v basectl >/dev/null 2>&1; then + exec basectl "$command_path" "$@" +fi + +printf 'ERROR: BASE_HOME is not set and basectl was not found on PATH.\n' >&2 +exit 1 diff --git a/bin/sort-in-place b/bin/sort-in-place new file mode 100755 index 0000000..1965c83 --- /dev/null +++ b/bin/sort-in-place @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +repo_root="$(cd -- "$(dirname -- "$0")/.." && pwd -P)" || exit 1 +command_path="$repo_root/cli/bash/commands/sort-in-place/sort-in-place.sh" + +if [[ -n "${BASE_HOME:-}" ]]; then + if [[ ! -x "$BASE_HOME/bin/basectl" ]]; then + printf 'ERROR: BASE_HOME is set but basectl was not found at %s/bin/basectl.\n' "$BASE_HOME" >&2 + exit 1 + fi + exec "$BASE_HOME/bin/basectl" "$command_path" "$@" +fi + +if command -v basectl >/dev/null 2>&1; then + exec basectl "$command_path" "$@" +fi + +printf 'ERROR: BASE_HOME is not set and basectl was not found on PATH.\n' >&2 +exit 1 diff --git a/cli/bash/commands/caff/README.md b/cli/bash/commands/caff/README.md new file mode 100644 index 0000000..1a71250 --- /dev/null +++ b/cli/bash/commands/caff/README.md @@ -0,0 +1,11 @@ +# `caff` + +Caffeinate a named process by finding its PID and running macOS `caffeinate` +against that process. + +Public invocation is exposed by the launcher at `bin/caff`; the implementation +lives here so command code, documentation, and tests stay together. + +This utility was migrated from `codeforester/base`. It belongs in Base Platform +Tools because it is a small operational helper, not part of Base's core +workspace orchestration surface. diff --git a/cli/bash/commands/caff/caff.sh b/cli/bash/commands/caff/caff.sh new file mode 100644 index 0000000..e33af90 --- /dev/null +++ b/cli/bash/commands/caff/caff.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash + +# +# caff: call caffeinate for a named process. +# + +caff_show_help() { + cat <<'EOF' +Caffeinate a named process. + +Usage: + caff [-s] + +Options: + -s Suppress the already-caffeinating message. + -h, --help Show this help text. +EOF +} + +caff_describe() { + printf '%s\n' "Caffeinate a named process" +} + +caff_first_pgrep_match() { + local pattern="$1" + local pgrep_output status + + pgrep_output="$(pgrep "$pattern" 2>/dev/null)" + status=$? + if [[ "$status" -eq 0 ]]; then + [[ -n "$pgrep_output" ]] || return 1 + printf '%s\n' "$pgrep_output" | sed -n '1p' + return 0 + fi + if [[ "$status" -eq 1 ]]; then + return 1 + fi + + print_error "Unable to query process list for '$pattern'." + return 2 +} + +caff_wait_pid_from_caffeinate_args() { + local args_line="$1" + local arg rest + local waiting_for_pid=false + local args=() + + read -r -a args <<< "$args_line" + for arg in "${args[@]:1}"; do + if [[ "$waiting_for_pid" == true ]]; then + printf '%s\n' "$arg" + return 0 + fi + + case "$arg" in + -w) + waiting_for_pid=true + ;; + -w[0-9]*) + printf '%s\n' "${arg#-w}" + return 0 + ;; + -*w*) + rest="${arg#*w}" + if [[ -n "$rest" ]]; then + printf '%s\n' "$rest" + return 0 + fi + waiting_for_pid=true + ;; + esac + done + + return 1 +} + +main() { + local silent=0 + local process_name + local target_pid + local caffeinate_pid + local caffeinated_pid + local pgrep_status + + case "${1:-}" in + -h|--help) + caff_show_help + return 0 + ;; + --describe) + caff_describe + return 0 + ;; + -s) + silent=1 + shift + ;; + esac + + if ! command -v caffeinate >/dev/null 2>&1; then + print_error "There is no caffeinate command on your system." + return 1 + fi + + if (($# != 1)); then + print_error "A process name is required." + caff_show_help >&2 + return 2 + fi + + process_name="$1" + target_pid="$(caff_first_pgrep_match "$process_name")" + pgrep_status=$? + if [[ "$pgrep_status" -eq 1 ]]; then + print_warn "'$process_name' process is not running." + return 1 + fi + [[ "$pgrep_status" -eq 0 ]] || return 1 + + caffeinate_pid="$(caff_first_pgrep_match caffeinate)" + pgrep_status=$? + if [[ "$pgrep_status" -eq 2 ]]; then + return 1 + fi + if [[ "$pgrep_status" -eq 0 ]]; then + caffeinated_pid="$(ps -o args= -p "$caffeinate_pid" | while IFS= read -r line; do caff_wait_pid_from_caffeinate_args "$line" && break; done)" + if [[ "$caffeinated_pid" == "$target_pid" ]]; then + ((silent)) || printf '%s\n' "Already caffeinating: $process_name pid=$target_pid, caffeinate pid=$caffeinate_pid" + return 0 + fi + fi + + printf '%s\n' "Caffeinating PID $target_pid" + caffeinate -iw "$target_pid" & disown +} diff --git a/cli/bash/commands/caff/tests/caff.bats b/cli/bash/commands/caff/tests/caff.bats new file mode 100644 index 0000000..9ccbd35 --- /dev/null +++ b/cli/bash/commands/caff/tests/caff.bats @@ -0,0 +1,271 @@ +#!/usr/bin/env bats + +load ../../../../../tests/bats_helper.bash + +setup() { + bpt_setup_test_tmpdir + TEST_HOME="$TEST_TMPDIR/home" + TEST_MOCKBIN="$TEST_TMPDIR/mockbin" + TEST_STATE_DIR="$TEST_TMPDIR/state" + mkdir -p "$TEST_HOME" "$TEST_MOCKBIN" "$TEST_STATE_DIR" +} + +teardown() { + bpt_teardown_test_tmpdir +} + +run_caff() { + run env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \ + BPT_COMMAND_PATH="$BASE_PLATFORM_TOOLS_REPO_ROOT/cli/bash/commands/caff/caff.sh" \ + bash -c ' + print_error() { printf "ERROR: %s\n" "$*" >&2; } + print_warn() { printf "WARN: %s\n" "$*" >&2; } + source "$BPT_COMMAND_PATH" + main "$@" + ' caff "$@" +} + +create_caffeinate_stub() { + cat > "$TEST_MOCKBIN/caffeinate" <<'EOF' +#!/usr/bin/env bash +if [[ -n "${CAFF_TEST_RECORD:-}" ]]; then + printf '%s\n' "$*" > "$CAFF_TEST_RECORD" +fi +sleep 0.2 +EOF + chmod +x "$TEST_MOCKBIN/caffeinate" +} + +create_pgrep_stub() { + cat > "$TEST_MOCKBIN/pgrep" <<'EOF' +#!/usr/bin/env bash +if [[ "${CAFF_TEST_PGREP_FAIL:-}" == "${1:-}" ]]; then + printf 'pgrep failed for %s\n' "$1" >&2 + exit 2 +fi +case "${1:-}" in + caffeinate) + [[ -n "${CAFF_TEST_CAFFEINATE_PID:-}" ]] && printf '%s\n' "$CAFF_TEST_CAFFEINATE_PID" + ;; + "${CAFF_TEST_PROCESS_NAME:-}") + [[ -n "${CAFF_TEST_TARGET_PID:-}" ]] && printf '%s\n' "$CAFF_TEST_TARGET_PID" + ;; +esac +EOF + chmod +x "$TEST_MOCKBIN/pgrep" +} + +create_ps_stub() { + cat > "$TEST_MOCKBIN/ps" <<'EOF' +#!/usr/bin/env bash +printf 'ARGS\n' +if [[ -n "${CAFF_TEST_PS_ARGS:-}" ]]; then + printf '%s\n' "$CAFF_TEST_PS_ARGS" +elif [[ -n "${CAFF_TEST_CAFFEINATED_PID:-}" ]]; then + printf 'caffeinate -iw %s\n' "$CAFF_TEST_CAFFEINATED_PID" +fi +EOF + chmod +x "$TEST_MOCKBIN/ps" +} + +create_core_tool_links_without_caffeinate() { + local tool + local tool_path + + for tool in uname dirname readlink basename; do + tool_path="$(command -v "$tool")" + ln -s "$tool_path" "$TEST_MOCKBIN/$tool" + done +} + +wait_for_record() { + local record_file="$1" + local attempt + + for attempt in 1 2 3 4 5; do + [[ -f "$record_file" ]] && return 0 + sleep 0.1 + done + + return 1 +} + +@test "caff prints help" { + run_caff --help + + [ "$status" -eq 0 ] + [[ "$output" == *"Usage:"* ]] + [[ "$output" == *"caff [-s] "* ]] +} + +@test "caff fails when caffeinate is unavailable" { + create_core_tool_links_without_caffeinate + + run env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:/bin:/usr/sbin:/sbin" \ + BPT_COMMAND_PATH="$BASE_PLATFORM_TOOLS_REPO_ROOT/cli/bash/commands/caff/caff.sh" \ + bash -c ' + print_error() { printf "ERROR: %s\n" "$*" >&2; } + print_warn() { printf "WARN: %s\n" "$*" >&2; } + source "$BPT_COMMAND_PATH" + main "$@" + ' caff worker + + [ "$status" -eq 1 ] + [[ "$output" == *"There is no caffeinate command on your system."* ]] +} + +@test "caff requires exactly one process name" { + create_caffeinate_stub + + run_caff + + [ "$status" -eq 2 ] + [[ "$output" == *"A process name is required."* ]] + [[ "$output" == *"Usage:"* ]] +} + +@test "caff warns when the target process is not running" { + create_caffeinate_stub + create_pgrep_stub + + run_caff worker + + [ "$status" -eq 1 ] + [[ "$output" == *"'worker' process is not running."* ]] +} + +@test "caff starts caffeinate for the first matching process" { + local record_file="$TEST_STATE_DIR/caffeinate.args" + + create_caffeinate_stub + create_pgrep_stub + create_ps_stub + + run env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \ + CAFF_TEST_PROCESS_NAME=worker \ + CAFF_TEST_TARGET_PID=1234 \ + CAFF_TEST_RECORD="$record_file" \ + BPT_COMMAND_PATH="$BASE_PLATFORM_TOOLS_REPO_ROOT/cli/bash/commands/caff/caff.sh" \ + bash -c ' + print_error() { printf "ERROR: %s\n" "$*" >&2; } + print_warn() { printf "WARN: %s\n" "$*" >&2; } + source "$BPT_COMMAND_PATH" + main "$@" + ' caff worker + + [ "$status" -eq 0 ] + [[ "$output" == *"Caffeinating PID 1234"* ]] + wait_for_record "$record_file" + [ "$(cat "$record_file")" = "-iw 1234" ] +} + +@test "caff does not start another caffeinate for an already caffeinated process" { + local record_file="$TEST_STATE_DIR/caffeinate.args" + + create_caffeinate_stub + create_pgrep_stub + create_ps_stub + + run env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \ + CAFF_TEST_PROCESS_NAME=worker \ + CAFF_TEST_TARGET_PID=1234 \ + CAFF_TEST_CAFFEINATE_PID=9999 \ + CAFF_TEST_CAFFEINATED_PID=1234 \ + CAFF_TEST_RECORD="$record_file" \ + BPT_COMMAND_PATH="$BASE_PLATFORM_TOOLS_REPO_ROOT/cli/bash/commands/caff/caff.sh" \ + bash -c ' + print_error() { printf "ERROR: %s\n" "$*" >&2; } + print_warn() { printf "WARN: %s\n" "$*" >&2; } + source "$BPT_COMMAND_PATH" + main "$@" + ' caff worker + + [ "$status" -eq 0 ] + [[ "$output" == *"Already caffeinating: worker pid=1234, caffeinate pid=9999"* ]] + [ ! -e "$record_file" ] +} + +@test "caff recognizes already caffeinated process when -w is a separate option" { + local record_file="$TEST_STATE_DIR/caffeinate.args" + + create_caffeinate_stub + create_pgrep_stub + create_ps_stub + + run env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \ + CAFF_TEST_PROCESS_NAME=worker \ + CAFF_TEST_TARGET_PID=1234 \ + CAFF_TEST_CAFFEINATE_PID=9999 \ + CAFF_TEST_PS_ARGS="caffeinate -i -w 1234" \ + CAFF_TEST_RECORD="$record_file" \ + BPT_COMMAND_PATH="$BASE_PLATFORM_TOOLS_REPO_ROOT/cli/bash/commands/caff/caff.sh" \ + bash -c ' + print_error() { printf "ERROR: %s\n" "$*" >&2; } + print_warn() { printf "WARN: %s\n" "$*" >&2; } + source "$BPT_COMMAND_PATH" + main "$@" + ' caff worker + + [ "$status" -eq 0 ] + [[ "$output" == *"Already caffeinating: worker pid=1234, caffeinate pid=9999"* ]] + [ ! -e "$record_file" ] +} + +@test "caff recognizes already caffeinated process when -w appears before other options" { + local record_file="$TEST_STATE_DIR/caffeinate.args" + + create_caffeinate_stub + create_pgrep_stub + create_ps_stub + + run env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \ + CAFF_TEST_PROCESS_NAME=worker \ + CAFF_TEST_TARGET_PID=1234 \ + CAFF_TEST_CAFFEINATE_PID=9999 \ + CAFF_TEST_PS_ARGS="caffeinate -w 1234 -i" \ + CAFF_TEST_RECORD="$record_file" \ + BPT_COMMAND_PATH="$BASE_PLATFORM_TOOLS_REPO_ROOT/cli/bash/commands/caff/caff.sh" \ + bash -c ' + print_error() { printf "ERROR: %s\n" "$*" >&2; } + print_warn() { printf "WARN: %s\n" "$*" >&2; } + source "$BPT_COMMAND_PATH" + main "$@" + ' caff worker + + [ "$status" -eq 0 ] + [[ "$output" == *"Already caffeinating: worker pid=1234, caffeinate pid=9999"* ]] + [ ! -e "$record_file" ] +} + +@test "caff reports pgrep errors instead of treating them as not running" { + create_caffeinate_stub + create_pgrep_stub + + run env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \ + CAFF_TEST_PGREP_FAIL=worker \ + BPT_COMMAND_PATH="$BASE_PLATFORM_TOOLS_REPO_ROOT/cli/bash/commands/caff/caff.sh" \ + bash -c ' + print_error() { printf "ERROR: %s\n" "$*" >&2; } + print_warn() { printf "WARN: %s\n" "$*" >&2; } + source "$BPT_COMMAND_PATH" + main "$@" + ' caff worker + + [ "$status" -eq 1 ] + [[ "$output" == *"Unable to query process list for 'worker'."* ]] + [[ "$output" != *"'worker' process is not running."* ]] +} diff --git a/cli/bash/commands/sort-in-place/README.md b/cli/bash/commands/sort-in-place/README.md new file mode 100644 index 0000000..6946df8 --- /dev/null +++ b/cli/bash/commands/sort-in-place/README.md @@ -0,0 +1,11 @@ +# `sort-in-place` + +Sort one or more text files in place, optionally with `sort -u` behavior. + +Public invocation is exposed by the launcher at `bin/sort-in-place`; the +implementation lives here so command code, documentation, and tests stay +together. + +This utility was migrated from `codeforester/base`. It belongs in Base Platform +Tools because it is a small operational helper, not part of Base's core +workspace orchestration surface. diff --git a/cli/bash/commands/sort-in-place/sort-in-place.sh b/cli/bash/commands/sort-in-place/sort-in-place.sh new file mode 100644 index 0000000..e1664fd --- /dev/null +++ b/cli/bash/commands/sort-in-place/sort-in-place.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +# +# sort-in-place: sort files in place. +# + +sort_in_place_show_help() { + cat <<'EOF' +Sort text files in place. + +Usage: + sort-in-place [-u] ... + +Options: + -u Remove duplicate lines while sorting. + -h, --help Show this help text. +EOF +} + +sort_in_place_describe() { + printf '%s\n' "Sort text files in place" +} + +main() { + local sort_args=() + local file + local temp_file + local rc=0 + + case "${1:-}" in + -h|--help) + sort_in_place_show_help + return 0 + ;; + --describe) + sort_in_place_describe + return 0 + ;; + -u) + sort_args=(-u) + shift + ;; + esac + + if (($# == 0)); then + print_error "At least one file is required." + sort_in_place_show_help >&2 + return 2 + fi + + for file in "$@"; do + if [[ ! -f "$file" ]]; then + print_warn "$file is not a regular file; skipping." + continue + fi + + temp_file="$file._tmp" + if [[ -f "$temp_file" ]]; then + print_warn "$temp_file already exists; skipping $file." + continue + fi + + if ! sort "${sort_args[@]}" "$file" > "$temp_file"; then + print_error "Can't write to '$temp_file'." + rc=1 + continue + fi + + if ! mv -- "$temp_file" "$file"; then + print_error "Can't move '$temp_file' to '$file'." + rc=1 + fi + done + + return "$rc" +} diff --git a/cli/bash/commands/sort-in-place/tests/sort-in-place.bats b/cli/bash/commands/sort-in-place/tests/sort-in-place.bats new file mode 100644 index 0000000..f9d2789 --- /dev/null +++ b/cli/bash/commands/sort-in-place/tests/sort-in-place.bats @@ -0,0 +1,88 @@ +#!/usr/bin/env bats + +load ../../../../../tests/bats_helper.bash + +setup() { + bpt_setup_test_tmpdir + TEST_HOME="$TEST_TMPDIR/home" + mkdir -p "$TEST_HOME" +} + +teardown() { + bpt_teardown_test_tmpdir +} + +run_sort_in_place() { + run env \ + HOME="$TEST_HOME" \ + PATH="/usr/bin:/bin:/usr/sbin:/sbin" \ + BPT_COMMAND_PATH="$BASE_PLATFORM_TOOLS_REPO_ROOT/cli/bash/commands/sort-in-place/sort-in-place.sh" \ + bash -c ' + print_error() { printf "ERROR: %s\n" "$*" >&2; } + print_warn() { printf "WARN: %s\n" "$*" >&2; } + source "$BPT_COMMAND_PATH" + main "$@" + ' sort-in-place "$@" +} + +@test "sort-in-place prints help" { + run_sort_in_place --help + + [ "$status" -eq 0 ] + [[ "$output" == *"Usage:"* ]] + [[ "$output" == *"sort-in-place [-u] ..."* ]] +} + +@test "sort-in-place requires at least one file" { + run_sort_in_place + + [ "$status" -eq 2 ] + [[ "$output" == *"At least one file is required."* ]] + [[ "$output" == *"Usage:"* ]] +} + +@test "sort-in-place sorts a file in place" { + local input_file="$TEST_TMPDIR/input.txt" + + printf 'b\na\nc\n' > "$input_file" + + run_sort_in_place "$input_file" + + [ "$status" -eq 0 ] + [ "$(cat "$input_file")" = $'a\nb\nc' ] +} + +@test "sort-in-place supports unique sorting" { + local input_file="$TEST_TMPDIR/input.txt" + + printf 'b\na\nb\na\n' > "$input_file" + + run_sort_in_place -u "$input_file" + + [ "$status" -eq 0 ] + [ "$(cat "$input_file")" = $'a\nb' ] +} + +@test "sort-in-place skips non-regular files" { + local missing_file="$TEST_TMPDIR/missing.txt" + + run_sort_in_place "$missing_file" + + [ "$status" -eq 0 ] + [[ "$output" == *"$missing_file is not a regular file; skipping."* ]] +} + +@test "sort-in-place skips a file when its temp file already exists" { + local input_file="$TEST_TMPDIR/input.txt" + local temp_file="$input_file._tmp" + + printf 'b\na\n' > "$input_file" + printf 'existing temp\n' > "$temp_file" + + run_sort_in_place "$input_file" + + [ "$status" -eq 0 ] + [[ "$output" == *"$temp_file already exists; skipping $input_file."* ]] + [ "$(cat "$input_file")" = $'b\na' ] + [ "$(cat "$temp_file")" = "existing temp" ] +} diff --git a/docs/cli-layout.md b/docs/cli-layout.md index 0b38615..fdec293 100644 --- a/docs/cli-layout.md +++ b/docs/cli-layout.md @@ -47,8 +47,10 @@ Expose tools through `base_manifest.yaml` so Base can list and run them: ```yaml commands: + caff: bin/caff example-bash: cli/bash/commands/example-bash/example-bash.sh example-python: bin/example-python + sort-in-place: bin/sort-in-place ``` Run them with: @@ -137,3 +139,10 @@ cli/python/base_platform_tools//tests/ Wire the useful test command into `base_manifest.yaml` when the tool is ready. The repository-level `tests/validate.sh` should stay a lightweight contract check for the whole repo. + +Current migrated Bash tool tests: + +```bash +bats cli/bash/commands/caff/tests/caff.bats +bats cli/bash/commands/sort-in-place/tests/sort-in-place.bats +``` diff --git a/docs/tooling-boundary.md b/docs/tooling-boundary.md index c8ef552..f99a174 100644 --- a/docs/tooling-boundary.md +++ b/docs/tooling-boundary.md @@ -80,8 +80,8 @@ Unsupported platforms should include a short reason. ## Migration Policy -Existing simple utilities such as `caff` and `sort` remain in `codeforester/base` -until this repository baseline is stable. +The initial migrated utilities are `caff` and `sort-in-place`, which came from +`codeforester/base`. When a utility moves here: diff --git a/tests/bats_helper.bash b/tests/bats_helper.bash new file mode 100644 index 0000000..34cb825 --- /dev/null +++ b/tests/bats_helper.bash @@ -0,0 +1,11 @@ +BASE_PLATFORM_TOOLS_REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)" + +bpt_setup_test_tmpdir() { + TEST_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/base-platform-tools-test.XXXXXX")" +} + +bpt_teardown_test_tmpdir() { + if [[ -n "${TEST_TMPDIR:-}" && -d "$TEST_TMPDIR" ]]; then + rm -rf "$TEST_TMPDIR" + fi +} diff --git a/tests/validate.sh b/tests/validate.sh index e06ade0..3a5ff72 100755 --- a/tests/validate.sh +++ b/tests/validate.sh @@ -43,29 +43,49 @@ main() { require_file README.md || failed=1 require_file bin/README.md || failed=1 + require_file bin/caff || failed=1 + require_file bin/sort-in-place || failed=1 require_file CONTRIBUTING.md || failed=1 require_file CHANGELOG.md || failed=1 require_file LICENSE || failed=1 require_file base_manifest.yaml || failed=1 require_file cli/README.md || failed=1 require_file cli/bash/README.md || failed=1 + require_file cli/bash/commands/caff/README.md || failed=1 + require_file cli/bash/commands/caff/caff.sh || failed=1 + require_file cli/bash/commands/caff/tests/caff.bats || failed=1 + require_file cli/bash/commands/sort-in-place/README.md || failed=1 + require_file cli/bash/commands/sort-in-place/sort-in-place.sh || failed=1 + require_file cli/bash/commands/sort-in-place/tests/sort-in-place.bats || failed=1 require_file cli/python/README.md || failed=1 require_file cli/python/base_platform_tools/__init__.py || failed=1 require_file docs/cli-layout.md || failed=1 require_file docs/tooling-boundary.md || failed=1 require_file .github/workflows/tests.yml || failed=1 + require_file tests/bats_helper.bash || failed=1 require_file tests/validate.sh || failed=1 require_executable tests/validate.sh || failed=1 + require_executable bin/caff || failed=1 + require_executable bin/sort-in-place || failed=1 require_text base_manifest.yaml '^schema_version: 1$' || failed=1 require_text base_manifest.yaml '^ name: base-platform-tools$' || failed=1 require_text base_manifest.yaml '^ command: tests/validate\.sh$' || failed=1 + require_text base_manifest.yaml '^ caff: bin/caff$' || failed=1 require_text base_manifest.yaml '^ cli-check: tests/validate\.sh$' || failed=1 + require_text base_manifest.yaml '^ sort-in-place: bin/sort-in-place$' || failed=1 require_text README.md 'Base owns the workstation control plane' || failed=1 require_text README.md 'CLI Layout' || failed=1 + require_text README.md 'basectl run base-platform-tools caff' || failed=1 require_text bin/README.md 'base-wrapper' || failed=1 + require_text bin/caff 'BASE_HOME' || failed=1 + require_text bin/sort-in-place 'BASE_HOME' || failed=1 require_text cli/README.md 'Base owns `basectl`' || failed=1 require_text cli/bash/README.md '#!/usr/bin/env basectl' || failed=1 + require_text cli/bash/commands/caff/README.md 'migrated from `codeforester/base`' || failed=1 + require_text cli/bash/commands/sort-in-place/README.md 'migrated from `codeforester/base`' || failed=1 + require_text cli/bash/commands/caff/tests/caff.bats 'tests/bats_helper.bash' || failed=1 + require_text cli/bash/commands/sort-in-place/tests/sort-in-place.bats 'tests/bats_helper.bash' || failed=1 require_text cli/python/README.md 'base_platform_tools.' || failed=1 require_text docs/cli-layout.md 'Do not copy `basectl` or `base-wrapper`' || failed=1 require_text docs/cli-layout.md 'PYTHONPATH="\$repo_root/cli/python' || failed=1