From 4c96a8c87a1c8e9a482590452639d493c4bbe107 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Tue, 10 Mar 2026 02:59:45 -0400 Subject: [PATCH 01/35] improve error messaging --- test/runners/test_generation/scripts/sh/generate_matcher.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/test/runners/test_generation/scripts/sh/generate_matcher.sh b/test/runners/test_generation/scripts/sh/generate_matcher.sh index 0aecbc6..4aea15f 100755 --- a/test/runners/test_generation/scripts/sh/generate_matcher.sh +++ b/test/runners/test_generation/scripts/sh/generate_matcher.sh @@ -55,6 +55,7 @@ fi if [ ! -s "$OUT_FL" ]; then if [ "$allow_no_output" != "true" ]; then printf "\n ❌ Error: zelta produced no output\n" + printf " πŸ› οΈβš™οΈβž‘οΈ If that is expected, πŸ’‘ add option 'allow_no_output: true' to your test\n" printf "****-> review and update zelta cmd: \"%s\"\n" "$zelta_cmd" exit 1 else From 219272f7ada75ee2aa8143fa1cdb6c9db12398cc Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Tue, 10 Mar 2026 03:17:16 -0400 Subject: [PATCH 02/35] add prune and policy test, add zelta policy config file generator, support local and full paths on generators, add better debugging support, add all tests runner --- test/runners/test_generation/bin/debug_gen.sh | 49 +++++---------- .../test_generation/bin/generate_all_tests.sh | 8 +++ .../bin/generate_policy_test.sh | 14 +++++ .../bin/generate_prune_test.sh | 16 +++++ ...ew_tests.sh => generate_tests_40_50_60.sh} | 6 +- .../bin/generate_zelta_policy_config.sh | 27 ++++++++ .../bin/setup_debug_state.bash | 62 +++++++++++++++++++ .../config/test_defs/070_zelta_prune_test.yml | 16 +++++ .../test_defs/080_zelta_policy_test.yml | 15 +++++ 9 files changed, 177 insertions(+), 36 deletions(-) create mode 100755 test/runners/test_generation/bin/generate_all_tests.sh create mode 100755 test/runners/test_generation/bin/generate_policy_test.sh create mode 100755 test/runners/test_generation/bin/generate_prune_test.sh rename test/runners/test_generation/bin/{generate_new_tests.sh => generate_tests_40_50_60.sh} (91%) create mode 100755 test/runners/test_generation/bin/generate_zelta_policy_config.sh create mode 100644 test/runners/test_generation/bin/setup_debug_state.bash create mode 100644 test/runners/test_generation/config/test_defs/070_zelta_prune_test.yml create mode 100644 test/runners/test_generation/config/test_defs/080_zelta_policy_test.yml diff --git a/test/runners/test_generation/bin/debug_gen.sh b/test/runners/test_generation/bin/debug_gen.sh index 12cf650..c733cf6 100755 --- a/test/runners/test_generation/bin/debug_gen.sh +++ b/test/runners/test_generation/bin/debug_gen.sh @@ -1,44 +1,29 @@ -# This is a helper for debugging and testing your generated tests -# This can be helpful if you use the generate_new_tests.sh approach -# and your test confirmation fails. -# -# Why? Because you can fine-tune the tree setup and run your test commands -# by hand to determine the cause of problems. Your test yml definition may -# need additional setup or a modified command. So the ability to iteratively -# test out the new spec can help you resolve problems. -# -# SPECS - the shellspec examples you run to setup the tree before running your test -# NEW_SPEC - the generated test you are debugging or verifying - -REPO_ROOT=$(git rev-parse --show-toplevel) - -# standard locations under test -RUNNERS_DIR="$REPO_ROOT/test/runners" -TEST_GEN_DIR="$REPO_ROOT/test/runners/test_generation" - -# tree setup utility -. "$TEST_GEN_DIR/lib/orchestration/setup_tree.sh" +#!/usr/bin/env bash +# Get the directory where this script is located +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +# SPECS - the shellspec examples you run to setup the tree before running your test # prepare the zfs tree with the state represented by running the following examples/tests -SPECS="test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh" +# TODO: change the SPECS variable to include all tests you need to run before the new test +SPECS="test/01*_spec.sh|test/02*_spec.sh|test/03_*_spec.sh|test/040_*_spec.sh|test/050_*_spec.sh|test/060_*_spec.sh" + +if ! . "$SCRIPT_DIR/setup_debug_state.bash" "$SPECS"; then + printf "\n ❌ ERROR: debug state setup failed\n" + exit 1 +fi # use the directory where your generated test is created # by default we're using a temp directory off of test/runners/test_generation -NEW_SPEC="$TEST_GEN_DIR/tmp/050_zelta_revert_spec.sh" +# TODO: change the NEWS_SPEC variable to be spec of the new test you debugging +NEW_SPEC="$TEST_GEN_DIR/tmp/070_zelta_prune_spec.sh" echo "confirming new spec: {$NEW_SPEC}" -if setup_tree "$SPECS"; then - printf "\n βœ… initial tree setup succeeded for specs: %s\n" "$SPECS" -else - printf "\n ❌ Goodbye, initial tree setup failed for specs: %s\n" "$SPECS" - exit 1 -fi - -# -. "$RUNNERS_DIR/env/setup_debug_env.sh" # show a detailed trace of the commands you are executing in your new test -TRACE_OPTIONS="--xtrace --shell /opt/homebrew/bin/bash" +# macOS BASH_SH=/opt/homebrew/bin/bash +# Arch BASH_SH=/usr/bin/bash +BASH_SH=/usr/bin/bash +TRACE_OPTIONS="--xtrace --shell $BASH_SH" # if you don't want/need a detailed trace unset the options var #unset TRACE_OPTIONS diff --git a/test/runners/test_generation/bin/generate_all_tests.sh b/test/runners/test_generation/bin/generate_all_tests.sh new file mode 100755 index 0000000..961bec8 --- /dev/null +++ b/test/runners/test_generation/bin/generate_all_tests.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Get the directory where this script is located +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +"$SCRIPT_DIR/generate_tests_40_50_60.sh" +"$SCRIPT_DIR/generate_prune_test.sh" +"$SCRIPT_DIR/generate_policy_test.sh" + diff --git a/test/runners/test_generation/bin/generate_policy_test.sh b/test/runners/test_generation/bin/generate_policy_test.sh new file mode 100755 index 0000000..b754260 --- /dev/null +++ b/test/runners/test_generation/bin/generate_policy_test.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Get the directory where this script is located +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" +GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" + +if ! "$GENERATE_TEST" \ + "$CONFIG_DIR/080_zelta_policy_test.yml" \ + "test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh|test/050_*_spec.sh|test/060_*_spec.sh|test/070_*_spec.sh"; then + + printf "\n ❌ Failed to generate 080 test\n" + exit 1 +fi diff --git a/test/runners/test_generation/bin/generate_prune_test.sh b/test/runners/test_generation/bin/generate_prune_test.sh new file mode 100755 index 0000000..aaac3ef --- /dev/null +++ b/test/runners/test_generation/bin/generate_prune_test.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Get the directory where this script is located +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" +GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" + +# Generate tests for 40,50,60 examples + +if ! "$GENERATE_TEST" \ + "$CONFIG_DIR/070_zelta_prune_test.yml" \ + "test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh|test/050_*_spec.sh|test/060_*_spec.sh"; then + + printf "\n ❌ Failed to generate 070 test\n" + exit 1 +fi diff --git a/test/runners/test_generation/bin/generate_new_tests.sh b/test/runners/test_generation/bin/generate_tests_40_50_60.sh similarity index 91% rename from test/runners/test_generation/bin/generate_new_tests.sh rename to test/runners/test_generation/bin/generate_tests_40_50_60.sh index 31a0efd..3c1dcbc 100755 --- a/test/runners/test_generation/bin/generate_new_tests.sh +++ b/test/runners/test_generation/bin/generate_tests_40_50_60.sh @@ -1,7 +1,6 @@ -#!/bin/sh - +#!/usr/bin/env bash # Get the directory where this script is located -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" @@ -24,7 +23,6 @@ if ! "$GENERATE_TEST" \ exit 1 fi - if ! "$GENERATE_TEST" \ "$CONFIG_DIR/060_zelta_clone_test.yml" \ "test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh|test/050_*_spec.sh"; then diff --git a/test/runners/test_generation/bin/generate_zelta_policy_config.sh b/test/runners/test_generation/bin/generate_zelta_policy_config.sh new file mode 100755 index 0000000..9299dff --- /dev/null +++ b/test/runners/test_generation/bin/generate_zelta_policy_config.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +# called by shellspec test for policy, this script +# will generate a basic zelta policy file for the +# current SANDBOX environment variables + +# Get the directory where this script is located +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG_DIR="$TEST_GEN_DIR/config" +ZELTA_TEST_POLICY_CONFIG_FILE="$CONFIG_DIR/zelta_test_policy.conf" + +# remove any existing policy file +rm -f "$ZELTA_TEST_POLICY_CONFIG_FILE" + +CUR_TIME_STAMP=$(date -u +%Y-%m-%d_%H.%M.%S) +BACKUP_NAME="zelta_policy_backup_${CUR_TIME_STAMP}" + +# generate new policy file +cat < $ZELTA_TEST_POLICY_CONFIG_FILE +# shellspec auto generated test zelta policy file at: ($CUR_TIME_STAMP) +# NOTE: any modification will be lost +BACKUP_SITE: + ${SANDBOX_ZELTA_SRC_REMOTE}: + datasets: + - ${SANDBOX_ZELTA_SRC_DS}: ${SANDBOX_ZELTA_TGT_REMOTE}:${SANDBOX_ZELTA_TGT_DS}/${BACKUP_NAME} +EOF diff --git a/test/runners/test_generation/bin/setup_debug_state.bash b/test/runners/test_generation/bin/setup_debug_state.bash new file mode 100644 index 0000000..7195e9a --- /dev/null +++ b/test/runners/test_generation/bin/setup_debug_state.bash @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +# Check if arguments were provided +if [ $# -ne 1 ]; then + echo "Missing SPECS argument!" + echo "Usage: $0 'specs list'" + return 1 +fi + +SPECS=$1 + +#VAR="${VAR-default_value}" +# *** only source this script in bash, do not execute it + +# This is a helper for setting up a test environment with the datasets +# as they would exist after running all the specs defined by SPECS +# +# This can be helpful if you use the generate_new_tests.sh approach +# and your test confirmation fails. +# +# Why? Because you can fine-tune the tree setup and run your test commands +# by hand to determine the cause of problems. Your test yml definition may +# need additional setup or a modified command. So the ability to iteratively +# test out the new spec can help you resolve problems. + +REPO_ROOT=$(git rev-parse --show-toplevel) + +# standard locations under test +RUNNERS_DIR="$REPO_ROOT/test/runners" +TEST_GEN_DIR="$REPO_ROOT/test/runners/test_generation" + +# Requires Bash β€” will produce syntax errors in fish, csh, etc. +# Usage: source setup.bash + +# Guard: reject non-Bash POSIX shells (sh, dash, zsh) +if [ -z "$BASH_VERSION" ] || case "$SHELLOPTS" in *posix*) true;; *) false;; esac; then + echo "Error: This script requires Bash (not sh)." >&2 + return 1 2>/dev/null || exit 1 +fi + +# Guard: reject direct execution +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + echo "Error: This script must be sourced, not executed." >&2 + echo "Usage: source ${BASH_SOURCE[0]}" >&2 + exit 1 +fi + +# tree setup utility +. "$TEST_GEN_DIR/lib/orchestration/setup_tree.sh" + +if setup_tree "$SPECS"; then + printf "\n βœ… initial tree setup succeeded for specs: %s\n" "$SPECS" +else + printf "\n ❌ Goodbye, initial tree setup failed for specs: %s\n" "$SPECS" + exit 1 +fi + +# setup the debugging environment +. "$RUNNERS_DIR/env/setup_debug_env.sh" + +# now you can execute commands as if they were in a test +# that runs after all the SPECS hae completed \ No newline at end of file diff --git a/test/runners/test_generation/config/test_defs/070_zelta_prune_test.yml b/test/runners/test_generation/config/test_defs/070_zelta_prune_test.yml new file mode 100644 index 0000000..d8101ce --- /dev/null +++ b/test/runners/test_generation/config/test_defs/070_zelta_prune_test.yml @@ -0,0 +1,16 @@ +output_dir: tmp +shellspec_name: "060_zelta_clone_spec" +describe_desc: "Test clone" +skip_if_list: + - condition: if 'SANDBOX_ZELTA_SRC_DS undefined' test -z "$SANDBOX_ZELTA_SRC_DS" + +test_list: + - test_name: clone_sub2 + it_desc: zelta clone sub2 - %{when_command} + when_command: zelta clone "$SANDBOX_ZELTA_SRC_EP/sub2" "$SANDBOX_ZELTA_SRC_EP/copy_of_sub2" + + - test_name: zfs_list_for_clone + setup_scripts: + - "test/test_helper.sh" + it_desc: verifies the clone - %{when_command} + when_command: src_exec zfs list -ro name,origin $SANDBOX_ZELTA_SRC_DS/copy_of_sub2 diff --git a/test/runners/test_generation/config/test_defs/080_zelta_policy_test.yml b/test/runners/test_generation/config/test_defs/080_zelta_policy_test.yml new file mode 100644 index 0000000..c63a5c1 --- /dev/null +++ b/test/runners/test_generation/config/test_defs/080_zelta_policy_test.yml @@ -0,0 +1,15 @@ +output_dir: tmp +shellspec_name: "070_zelta_prune_spec" +describe_desc: "Test prune" +skip_if_list: + - condition: if 'SANDBOX_ZELTA_SRC_DS undefined' test -z "$SANDBOX_ZELTA_SRC_DS" + - condition: if 'SANDBOX_ZELTA_TGT_DS undefined' test -z "$SANDBOX_ZELTA_TGT_DS" + +test_list: + - test_name: backup_with_snapshot + it_desc: backup with snapshot - %{when_command} + when_command: zelta backup --snapshot "$SANDBOX_ZELTA_SRC_EP" "$SANDBOX_ZELTA_TGT_EP" + + - test_name: prune_check + it_desc: only suggest snapshots existing on target - %{when_command} + when_command: zelta prune --keep-snap-num=0 --keep-snap-days=0 "$SANDBOX_ZELTA_SRC_EP" "$SANDBOX_ZELTA_TGT_EP" From 7a2e0fd4e72d0dad5b112c753c1ec84506c07e0b Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Tue, 10 Mar 2026 03:19:33 -0400 Subject: [PATCH 03/35] add prune and policy test, remove warnings from clone and revert for FreeBSD compatibility --- .../test_defs/050_zelta_revert_test.yml | 5 +++-- .../config/test_defs/060_zelta_clone_test.yml | 5 +++-- .../config/test_defs/070_zelta_prune_test.yml | 19 +++++++++---------- .../test_defs/080_zelta_policy_test.yml | 19 ++++++++++--------- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/test/runners/test_generation/config/test_defs/050_zelta_revert_test.yml b/test/runners/test_generation/config/test_defs/050_zelta_revert_test.yml index 5423773..31fb8a1 100644 --- a/test/runners/test_generation/config/test_defs/050_zelta_revert_test.yml +++ b/test/runners/test_generation/config/test_defs/050_zelta_revert_test.yml @@ -25,8 +25,9 @@ test_list: when_command: zelta snapshot --snap-name "another_test" "$SANDBOX_ZELTA_SRC_EP" - test_name: revert - it_desc: revert to last snapshot - %{when_command} - when_command: zelta revert "$SANDBOX_ZELTA_SRC_EP"@manual_test + it_desc: revert to last snapshot (ignore warnings) - %{when_command} + allow_no_output: true + when_command: zelta revert -qq "$SANDBOX_ZELTA_SRC_EP"@manual_test - test_name: rotate_after_revert it_desc: rotates after divergence - %{when_command} diff --git a/test/runners/test_generation/config/test_defs/060_zelta_clone_test.yml b/test/runners/test_generation/config/test_defs/060_zelta_clone_test.yml index d8101ce..fac22d7 100644 --- a/test/runners/test_generation/config/test_defs/060_zelta_clone_test.yml +++ b/test/runners/test_generation/config/test_defs/060_zelta_clone_test.yml @@ -6,8 +6,9 @@ skip_if_list: test_list: - test_name: clone_sub2 - it_desc: zelta clone sub2 - %{when_command} - when_command: zelta clone "$SANDBOX_ZELTA_SRC_EP/sub2" "$SANDBOX_ZELTA_SRC_EP/copy_of_sub2" + it_desc: zelta clone sub2 (ignore warnings) - %{when_command} + allow_no_output: true + when_command: zelta clone -qq "$SANDBOX_ZELTA_SRC_EP/sub2" "$SANDBOX_ZELTA_SRC_EP/copy_of_sub2" - test_name: zfs_list_for_clone setup_scripts: diff --git a/test/runners/test_generation/config/test_defs/070_zelta_prune_test.yml b/test/runners/test_generation/config/test_defs/070_zelta_prune_test.yml index d8101ce..c63a5c1 100644 --- a/test/runners/test_generation/config/test_defs/070_zelta_prune_test.yml +++ b/test/runners/test_generation/config/test_defs/070_zelta_prune_test.yml @@ -1,16 +1,15 @@ output_dir: tmp -shellspec_name: "060_zelta_clone_spec" -describe_desc: "Test clone" +shellspec_name: "070_zelta_prune_spec" +describe_desc: "Test prune" skip_if_list: - condition: if 'SANDBOX_ZELTA_SRC_DS undefined' test -z "$SANDBOX_ZELTA_SRC_DS" + - condition: if 'SANDBOX_ZELTA_TGT_DS undefined' test -z "$SANDBOX_ZELTA_TGT_DS" test_list: - - test_name: clone_sub2 - it_desc: zelta clone sub2 - %{when_command} - when_command: zelta clone "$SANDBOX_ZELTA_SRC_EP/sub2" "$SANDBOX_ZELTA_SRC_EP/copy_of_sub2" + - test_name: backup_with_snapshot + it_desc: backup with snapshot - %{when_command} + when_command: zelta backup --snapshot "$SANDBOX_ZELTA_SRC_EP" "$SANDBOX_ZELTA_TGT_EP" - - test_name: zfs_list_for_clone - setup_scripts: - - "test/test_helper.sh" - it_desc: verifies the clone - %{when_command} - when_command: src_exec zfs list -ro name,origin $SANDBOX_ZELTA_SRC_DS/copy_of_sub2 + - test_name: prune_check + it_desc: only suggest snapshots existing on target - %{when_command} + when_command: zelta prune --keep-snap-num=0 --keep-snap-days=0 "$SANDBOX_ZELTA_SRC_EP" "$SANDBOX_ZELTA_TGT_EP" diff --git a/test/runners/test_generation/config/test_defs/080_zelta_policy_test.yml b/test/runners/test_generation/config/test_defs/080_zelta_policy_test.yml index c63a5c1..8e0e0af 100644 --- a/test/runners/test_generation/config/test_defs/080_zelta_policy_test.yml +++ b/test/runners/test_generation/config/test_defs/080_zelta_policy_test.yml @@ -1,15 +1,16 @@ output_dir: tmp -shellspec_name: "070_zelta_prune_spec" -describe_desc: "Test prune" +shellspec_name: "080_zelta_policy_spec" +describe_desc: "Test zelta policy" skip_if_list: - condition: if 'SANDBOX_ZELTA_SRC_DS undefined' test -z "$SANDBOX_ZELTA_SRC_DS" - condition: if 'SANDBOX_ZELTA_TGT_DS undefined' test -z "$SANDBOX_ZELTA_TGT_DS" + - condition: if 'SANDBOX_ZELTA_SRC_REMOTE undefined' test -z "$SANDBOX_ZELTA_SRC_REMOTE" + - condition: if 'SANDBOX_ZELTA_TGT_REMOTE undefined' test -z "$SANDBOX_ZELTA_TGT_REMOTE" test_list: - - test_name: backup_with_snapshot - it_desc: backup with snapshot - %{when_command} - when_command: zelta backup --snapshot "$SANDBOX_ZELTA_SRC_EP" "$SANDBOX_ZELTA_TGT_EP" - - - test_name: prune_check - it_desc: only suggest snapshots existing on target - %{when_command} - when_command: zelta prune --keep-snap-num=0 --keep-snap-days=0 "$SANDBOX_ZELTA_SRC_EP" "$SANDBOX_ZELTA_TGT_EP" + - test_name: policy_check + setup_scripts: + # generate zelta policy config file from env variables + - "test/runners/test_generation/bin/generate_zelta_policy_config.sh" + it_desc: test zelta policy - %{when_command} + when_command: zelta policy -C ./test/runners/test_generation/config/zelta_test_policy.conf From f0f7470994831369a5dc8ae0cee4fdb9e56e82a7 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Tue, 10 Mar 2026 03:20:41 -0400 Subject: [PATCH 04/35] regenerate tests, add new prune and policy test --- test/040_zelta_tests_spec.sh | 2 +- test/050_zelta_revert_spec.sh | 37 ++------------------ test/060_zelta_clone_spec.sh | 25 ++------------ test/070_zelta_prune_spec.sh | 65 +++++++++++++++++++++++++++++++++++ test/080_zelta_policy_spec.sh | 35 +++++++++++++++++++ 5 files changed, 107 insertions(+), 57 deletions(-) create mode 100644 test/070_zelta_prune_spec.sh create mode 100644 test/080_zelta_policy_spec.sh diff --git a/test/040_zelta_tests_spec.sh b/test/040_zelta_tests_spec.sh index 930ef66..f602efa 100644 --- a/test/040_zelta_tests_spec.sh +++ b/test/040_zelta_tests_spec.sh @@ -1,5 +1,5 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-02-12 13:28:34 -0500 +# Generated at: 2026-03-10 03:07:56 -0400 # Source: 040_zelta_tests_spec # WARNING: This file was automatically generated. Manual edits may be lost. diff --git a/test/050_zelta_revert_spec.sh b/test/050_zelta_revert_spec.sh index 99d0fac..2b05087 100644 --- a/test/050_zelta_revert_spec.sh +++ b/test/050_zelta_revert_spec.sh @@ -1,5 +1,5 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-02-12 13:29:24 -0500 +# Generated at: 2026-03-10 03:08:42 -0400 # Source: 050_zelta_revert_spec # WARNING: This file was automatically generated. Manual edits may be lost. @@ -53,25 +53,6 @@ output_for_snapshot_again() { return 0 } -output_for_revert() { - while IFS= read -r line; do - # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') - case "$normalized" in - "renaming '${SANDBOX_ZELTA_SRC_DS}' to '${SANDBOX_ZELTA_SRC_DS}_manual_test'"|\ - "cloned 12/12 datasets to ${SANDBOX_ZELTA_SRC_DS}"|\ - "snapshotting: @zelta_"*""|\ - "to retain replica history, run: zelta rotate '${SANDBOX_ZELTA_SRC_DS}' 'TARGET'") - ;; - *) - printf "Unexpected line format: %s\n" "$line" >&2 - return 1 - ;; - esac - done - return 0 -} - output_for_rotate_after_revert() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces @@ -118,20 +99,8 @@ Describe 'Test revert' The status should be success End - It "revert to last snapshot - zelta revert \"$SANDBOX_ZELTA_SRC_EP\"@manual_test" - When call zelta revert "$SANDBOX_ZELTA_SRC_EP"@manual_test - The output should satisfy output_for_revert - The error should equal "warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root -warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root -warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root -warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root -warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root -warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root -warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root -warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root -warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root -warning: unexpected 'zfs clone' output: cannot open '${SANDBOX_ZELTA_SRC_DS}_manual_test/sub5@manual_test': dataset does not exist -warning: unexpected 'zfs clone' output: cannot open '${SANDBOX_ZELTA_SRC_DS}_manual_test/sub5/child1@manual_test': dataset does not exist" + It "revert to last snapshot (ignore warnings) - zelta revert -qq \"$SANDBOX_ZELTA_SRC_EP\"@manual_test" + When call zelta revert -qq "$SANDBOX_ZELTA_SRC_EP"@manual_test The status should be success End diff --git a/test/060_zelta_clone_spec.sh b/test/060_zelta_clone_spec.sh index 7b4d46c..399a9d0 100644 --- a/test/060_zelta_clone_spec.sh +++ b/test/060_zelta_clone_spec.sh @@ -1,24 +1,8 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-02-12 13:30:29 -0500 +# Generated at: 2026-03-10 03:09:43 -0400 # Source: 060_zelta_clone_spec # WARNING: This file was automatically generated. Manual edits may be lost. -output_for_clone_sub2() { - while IFS= read -r line; do - # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') - case "$normalized" in - "cloned 2/2 datasets to ${SANDBOX_ZELTA_SRC_DS}/copy_of_sub2") - ;; - *) - printf "Unexpected line format: %s\n" "$line" >&2 - return 1 - ;; - esac - done - return 0 -} - output_for_zfs_list_for_clone() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces @@ -40,11 +24,8 @@ output_for_zfs_list_for_clone() { Describe 'Test clone' Skip if 'SANDBOX_ZELTA_SRC_DS undefined' test -z "$SANDBOX_ZELTA_SRC_DS" - It "zelta clone sub2 - zelta clone \"$SANDBOX_ZELTA_SRC_EP/sub2\" \"$SANDBOX_ZELTA_SRC_EP/copy_of_sub2\"" - When call zelta clone "$SANDBOX_ZELTA_SRC_EP/sub2" "$SANDBOX_ZELTA_SRC_EP/copy_of_sub2" - The output should satisfy output_for_clone_sub2 - The error should equal "warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root -warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root" + It "zelta clone sub2 (ignore warnings) - zelta clone -qq \"$SANDBOX_ZELTA_SRC_EP/sub2\" \"$SANDBOX_ZELTA_SRC_EP/copy_of_sub2\"" + When call zelta clone -qq "$SANDBOX_ZELTA_SRC_EP/sub2" "$SANDBOX_ZELTA_SRC_EP/copy_of_sub2" The status should be success End diff --git a/test/070_zelta_prune_spec.sh b/test/070_zelta_prune_spec.sh new file mode 100644 index 0000000..8e06a63 --- /dev/null +++ b/test/070_zelta_prune_spec.sh @@ -0,0 +1,65 @@ +# Auto-generated ShellSpec test file +# Generated at: 2026-03-10 03:10:49 -0400 +# Source: 070_zelta_prune_spec +# WARNING: This file was automatically generated. Manual edits may be lost. + +output_for_backup_with_snapshot() { + while IFS= read -r line; do + # normalize whitespace, remove leading/trailing spaces + normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + case "$normalized" in + "snapshotting: @zelta_"*""|\ + "syncing 12 datasets"|\ + "* sent, 12 streams received in * seconds") + ;; + *) + printf "Unexpected line format: %s\n" "$line" >&2 + return 1 + ;; + esac + done + return 0 +} + +output_for_prune_check() { + while IFS= read -r line; do + # normalize whitespace, remove leading/trailing spaces + normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + case "$normalized" in + "${SANDBOX_ZELTA_SRC_DS}@zelta_"*""|\ + "${SANDBOX_ZELTA_SRC_DS}/sub1@zelta_"*""|\ + "${SANDBOX_ZELTA_SRC_DS}/sub1/child@zelta_"*""|\ + "${SANDBOX_ZELTA_SRC_DS}/sub2@zelta_"*""|\ + "${SANDBOX_ZELTA_SRC_DS}/sub2/orphan@zelta_"*""|\ + "${SANDBOX_ZELTA_SRC_DS}/sub3@zelta_"*""|\ + "${SANDBOX_ZELTA_SRC_DS}/sub3/space name@zelta_"*""|\ + "${SANDBOX_ZELTA_SRC_DS}/sub4@zelta_"*""|\ + "${SANDBOX_ZELTA_SRC_DS}/sub4/encrypted@zelta_"*""|\ + "${SANDBOX_ZELTA_SRC_DS}/sub4/zvol@zelta_"*"") + ;; + *) + printf "Unexpected line format: %s\n" "$line" >&2 + return 1 + ;; + esac + done + return 0 +} + +Describe 'Test prune' + Skip if 'SANDBOX_ZELTA_SRC_DS undefined' test -z "$SANDBOX_ZELTA_SRC_DS" + Skip if 'SANDBOX_ZELTA_TGT_DS undefined' test -z "$SANDBOX_ZELTA_TGT_DS" + + It "backup with snapshot - zelta backup --snapshot \"$SANDBOX_ZELTA_SRC_EP\" \"$SANDBOX_ZELTA_TGT_EP\"" + When call zelta backup --snapshot "$SANDBOX_ZELTA_SRC_EP" "$SANDBOX_ZELTA_TGT_EP" + The output should satisfy output_for_backup_with_snapshot + The status should be success + End + + It "only suggest snapshots existing on target - zelta prune --keep-snap-num=0 --keep-snap-days=0 \"$SANDBOX_ZELTA_SRC_EP\" \"$SANDBOX_ZELTA_TGT_EP\"" + When call zelta prune --keep-snap-num=0 --keep-snap-days=0 "$SANDBOX_ZELTA_SRC_EP" "$SANDBOX_ZELTA_TGT_EP" + The output should satisfy output_for_prune_check + The status should be success + End + +End diff --git a/test/080_zelta_policy_spec.sh b/test/080_zelta_policy_spec.sh new file mode 100644 index 0000000..a776af2 --- /dev/null +++ b/test/080_zelta_policy_spec.sh @@ -0,0 +1,35 @@ +# Auto-generated ShellSpec test file +# Generated at: 2026-03-10 03:12:05 -0400 +# Source: 080_zelta_policy_spec +# WARNING: This file was automatically generated. Manual edits may be lost. + +output_for_policy_check() { + while IFS= read -r line; do + # normalize whitespace, remove leading/trailing spaces + normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + case "$normalized" in + "[BACKUP_SITE: ${SANDBOX_ZELTA_TGT_EP}/zelta_policy_backup_2026-03-10_07.11.59] ${SANDBOX_ZELTA_SRC_EP}: syncing 12 datasets"|\ + "[BACKUP_SITE: ${SANDBOX_ZELTA_TGT_EP}/zelta_policy_backup_2026-03-10_07.11.59] ${SANDBOX_ZELTA_SRC_EP}: * sent, 22 streams received in * seconds") + ;; + *) + printf "Unexpected line format: %s\n" "$line" >&2 + return 1 + ;; + esac + done + return 0 +} + +Describe 'Test zelta policy' + Skip if 'SANDBOX_ZELTA_SRC_DS undefined' test -z "$SANDBOX_ZELTA_SRC_DS" + Skip if 'SANDBOX_ZELTA_TGT_DS undefined' test -z "$SANDBOX_ZELTA_TGT_DS" + Skip if 'SANDBOX_ZELTA_SRC_REMOTE undefined' test -z "$SANDBOX_ZELTA_SRC_REMOTE" + Skip if 'SANDBOX_ZELTA_TGT_REMOTE undefined' test -z "$SANDBOX_ZELTA_TGT_REMOTE" + + It "test zelta policy - zelta policy -C ./test/runners/test_generation/config/zelta_test_policy.conf" + When call zelta policy -C ./test/runners/test_generation/config/zelta_test_policy.conf + The output should satisfy output_for_policy_check + The status should be success + End + +End From 9a7d1bd98826b85963d06cbd1e6d545900fbce1e Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Tue, 10 Mar 2026 03:22:26 -0400 Subject: [PATCH 05/35] add more context on sudoers update to include Ubuntu and FreeBSD --- test/README.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/test/README.md b/test/README.md index de8fca2..96e31b3 100644 --- a/test/README.md +++ b/test/README.md @@ -13,14 +13,25 @@ ### If testing remotely: - Setup your test user on your source and target machines - - update sudoers, for example on Linux + - update sudoers - create /etc/sudoers.d/zelta-tester - ``` - # Allow (mytestuser) to run ZFS commands without password for zelta testing - # NOTE: This is for test environments only - DO NOT use in production - # CAUTION: The wildcards show intent only, with globbing other commands may be allowed as well - (mytestuser) ALL=(ALL) NOPASSWD: /usr/bin/dd *, /usr/bin/rm -f /tmp/*, /usr/bin/truncate *, /usr/sbin/zpool *, /usr/sbin/zfs * - ``` + - add this comment to the file + ``` + # Allow (mytestuser) to run ZFS commands without password for zelta testing + # NOTE: This is for test environments only - DO NOT use in production + # CAUTION: The wildcards show intent only, with globbing other commands may be allowed as well + ``` + + - Ubuntu entry + ``` + (mytestuser) ALL=(ALL) NOPASSWD: /usr/bin/dd *, /usr/bin/rm -f /tmp/*, /usr/bin/truncate *, /usr/sbin/zpool *, /usr/sbin/zfs * + ``` + + - FreeBSD entry + ``` + (mytestuser) ALL=(ALL) NOPASSWD: /bin/dd *, /bin/rm -f /tmp/*, /usr/bin/truncate *, /sbin/zpool *, /sbin/zfs * + ``` + - TODO: confirm if usr/bin/mount *, /usr/bin/mkdir * are needed - setup zfs allow on your source and target machines will be set up automatically for your test pools From 6f1cb369e6d53f89b7d1a7c30838616ef72cafe8 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Tue, 10 Mar 2026 03:22:59 -0400 Subject: [PATCH 06/35] add zman alias --- test/runners/doc/README_AliasHelpers.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/runners/doc/README_AliasHelpers.md b/test/runners/doc/README_AliasHelpers.md index 0421576..130f512 100644 --- a/test/runners/doc/README_AliasHelpers.md +++ b/test/runners/doc/README_AliasHelpers.md @@ -48,4 +48,8 @@ alias zrenv="zcd && . $ZELTA_ENV/reset_env.sh" # setup env vars for your test environment # setup pools, datasets and remotes env vars alias ztenv="zcd && . $ZELTA_ENV/test_env.sh" + +# access zelta man pages, relies on prior setting fo ZELTA_DOC env var +# which is the location of the ZELTA man pages +alias zman='man -M "$ZELTA_DOC"' ``` From d8278d19a86d12db07574bd99e8c91e2a0389fb4 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Thu, 12 Mar 2026 02:11:50 -0400 Subject: [PATCH 07/35] fix wildcarding, move all subs to ruby and out of awk, refactor test generators, support patterns and selectors on setup_debug_state, allow local policies, update Gemfile, rubocop config, show sandbox env vars, regeneate tests --- test/040_zelta_tests_spec.sh | 6 +- test/050_zelta_revert_spec.sh | 6 +- test/060_zelta_clone_spec.sh | 2 +- test/070_zelta_prune_spec.sh | 4 +- test/080_zelta_policy_spec.sh | 15 ++- test/runners/env/setup_debug_env.sh | 1 + test/runners/env/test_env.sh | 12 +- test/runners/test_generation/Gemfile | 1 + test/runners/test_generation/Gemfile.lock | 34 +++++ .../bin/generate_40_divergent_test.sh | 16 +++ .../bin/generate_50_revert_test.sh | 16 +++ ..._40_50_60.sh => generate_60_clone_test.sh} | 20 +-- ...rune_test.sh => generate_70_prune_test.sh} | 2 +- ...icy_test.sh => generate_80_policy_test.sh} | 2 + .../test_generation/bin/generate_all_tests.sh | 9 +- .../bin/generate_zelta_policy_config.sh | 16 ++- .../bin/setup_debug_state.bash | 20 ++- .../test_defs/080_zelta_policy_test.yml | 11 +- .../config/zelta_test_policy.conf | 6 + .../lib/orchestration/setup_tree.sh | 57 ++++++-- .../lib/ruby/env_substitutor.rb | 40 ++++++ .../test_generation/lib/ruby/path_config.rb | 14 ++ .../lib/ruby/test_generator.rb | 126 ++++++------------ .../scripts/awk/generate_case_stmt_func.awk | 14 +- 24 files changed, 281 insertions(+), 169 deletions(-) create mode 100755 test/runners/test_generation/bin/generate_40_divergent_test.sh create mode 100755 test/runners/test_generation/bin/generate_50_revert_test.sh rename test/runners/test_generation/bin/{generate_tests_40_50_60.sh => generate_60_clone_test.sh} (53%) rename test/runners/test_generation/bin/{generate_prune_test.sh => generate_70_prune_test.sh} (93%) rename test/runners/test_generation/bin/{generate_policy_test.sh => generate_80_policy_test.sh} (94%) create mode 100644 test/runners/test_generation/config/zelta_test_policy.conf create mode 100644 test/runners/test_generation/lib/ruby/env_substitutor.rb create mode 100644 test/runners/test_generation/lib/ruby/path_config.rb diff --git a/test/040_zelta_tests_spec.sh b/test/040_zelta_tests_spec.sh index f602efa..86a689b 100644 --- a/test/040_zelta_tests_spec.sh +++ b/test/040_zelta_tests_spec.sh @@ -1,5 +1,5 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-10 03:07:56 -0400 +# Generated at: 2026-03-10 23:54:05 -0400 # Source: 040_zelta_tests_spec # WARNING: This file was automatically generated. Manual edits may be lost. @@ -41,7 +41,7 @@ output_for_rotate_after_divergence() { "renaming '${SANDBOX_ZELTA_TGT_DS}' to '${SANDBOX_ZELTA_TGT_DS}_start'"|\ "to ensure target is up-to-date, run: zelta backup ${SANDBOX_ZELTA_SRC_EP} ${SANDBOX_ZELTA_TGT_EP}"|\ "no source: ${SANDBOX_ZELTA_TGT_DS}/sub1/kid"|\ - "* sent, 10 streams received in * seconds") + ""*" sent, 10 streams received in "*" seconds") ;; *) printf "Unexpected line format: %s\n" "$line" >&2 @@ -87,7 +87,7 @@ output_for_backup_after_rotate() { case "$normalized" in "syncing 10 datasets"|\ "10 datasets up-to-date"|\ - "* sent, 3 streams received in * seconds") + ""*" sent, 3 streams received in "*" seconds") ;; *) printf "Unexpected line format: %s\n" "$line" >&2 diff --git a/test/050_zelta_revert_spec.sh b/test/050_zelta_revert_spec.sh index 2b05087..bbb3c8c 100644 --- a/test/050_zelta_revert_spec.sh +++ b/test/050_zelta_revert_spec.sh @@ -1,5 +1,5 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-10 03:08:42 -0400 +# Generated at: 2026-03-10 23:54:51 -0400 # Source: 050_zelta_revert_spec # WARNING: This file was automatically generated. Manual edits may be lost. @@ -26,7 +26,7 @@ output_for_backup_after_delta() { case "$normalized" in "source is written; snapshotting: @zelta_"*""|\ "syncing 12 datasets"|\ - "* sent, 22 streams received in * seconds") + ""*" sent, 22 streams received in "*" seconds") ;; *) printf "Unexpected line format: %s\n" "$line" >&2 @@ -62,7 +62,7 @@ output_for_rotate_after_revert() { "to ensure target is up-to-date, run: zelta backup ${SANDBOX_ZELTA_SRC_EP} ${SANDBOX_ZELTA_TGT_EP}"|\ "no source: ${SANDBOX_ZELTA_TGT_DS}/sub5"|\ "no source: ${SANDBOX_ZELTA_TGT_DS}/sub5/child1"|\ - "* sent, 10 streams received in * seconds") + ""*" sent, 10 streams received in "*" seconds") ;; *) printf "Unexpected line format: %s\n" "$line" >&2 diff --git a/test/060_zelta_clone_spec.sh b/test/060_zelta_clone_spec.sh index 399a9d0..95eb892 100644 --- a/test/060_zelta_clone_spec.sh +++ b/test/060_zelta_clone_spec.sh @@ -1,5 +1,5 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-10 03:09:43 -0400 +# Generated at: 2026-03-10 23:55:50 -0400 # Source: 060_zelta_clone_spec # WARNING: This file was automatically generated. Manual edits may be lost. diff --git a/test/070_zelta_prune_spec.sh b/test/070_zelta_prune_spec.sh index 8e06a63..8c5b05d 100644 --- a/test/070_zelta_prune_spec.sh +++ b/test/070_zelta_prune_spec.sh @@ -1,5 +1,5 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-10 03:10:49 -0400 +# Generated at: 2026-03-10 23:56:54 -0400 # Source: 070_zelta_prune_spec # WARNING: This file was automatically generated. Manual edits may be lost. @@ -10,7 +10,7 @@ output_for_backup_with_snapshot() { case "$normalized" in "snapshotting: @zelta_"*""|\ "syncing 12 datasets"|\ - "* sent, 12 streams received in * seconds") + ""*" sent, 12 streams received in "*" seconds") ;; *) printf "Unexpected line format: %s\n" "$line" >&2 diff --git a/test/080_zelta_policy_spec.sh b/test/080_zelta_policy_spec.sh index a776af2..6816d7d 100644 --- a/test/080_zelta_policy_spec.sh +++ b/test/080_zelta_policy_spec.sh @@ -1,5 +1,5 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-10 03:12:05 -0400 +# Generated at: 2026-03-10 23:58:06 -0400 # Source: 080_zelta_policy_spec # WARNING: This file was automatically generated. Manual edits may be lost. @@ -8,8 +8,8 @@ output_for_policy_check() { # normalize whitespace, remove leading/trailing spaces normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in - "[BACKUP_SITE: ${SANDBOX_ZELTA_TGT_EP}/zelta_policy_backup_2026-03-10_07.11.59] ${SANDBOX_ZELTA_SRC_EP}: syncing 12 datasets"|\ - "[BACKUP_SITE: ${SANDBOX_ZELTA_TGT_EP}/zelta_policy_backup_2026-03-10_07.11.59] ${SANDBOX_ZELTA_SRC_EP}: * sent, 22 streams received in * seconds") + "[BACKUP_SITE: ${SANDBOX_ZELTA_TGT_EP}/zelta_policy_backup_"*"] ${SANDBOX_ZELTA_SRC_EP}: syncing 12 datasets"|\ + "[BACKUP_SITE: ${SANDBOX_ZELTA_TGT_EP}/zelta_policy_backup_"*"] ${SANDBOX_ZELTA_SRC_EP}: "*" sent, 22 streams received in "*" seconds") ;; *) printf "Unexpected line format: %s\n" "$line" >&2 @@ -23,8 +23,13 @@ output_for_policy_check() { Describe 'Test zelta policy' Skip if 'SANDBOX_ZELTA_SRC_DS undefined' test -z "$SANDBOX_ZELTA_SRC_DS" Skip if 'SANDBOX_ZELTA_TGT_DS undefined' test -z "$SANDBOX_ZELTA_TGT_DS" - Skip if 'SANDBOX_ZELTA_SRC_REMOTE undefined' test -z "$SANDBOX_ZELTA_SRC_REMOTE" - Skip if 'SANDBOX_ZELTA_TGT_REMOTE undefined' test -z "$SANDBOX_ZELTA_TGT_REMOTE" + #Skip if 'SANDBOX_ZELTA_SRC_REMOTE undefined' test -z "$SANDBOX_ZELTA_SRC_REMOTE" + #Skip if 'SANDBOX_ZELTA_TGT_REMOTE undefined' test -z "$SANDBOX_ZELTA_TGT_REMOTE" + + It "generate zelta policy - ./test/runners/test_generation/bin/generate_zelta_policy_config.sh" + When call ./test/runners/test_generation/bin/generate_zelta_policy_config.sh + The status should be success + End It "test zelta policy - zelta policy -C ./test/runners/test_generation/config/zelta_test_policy.conf" When call zelta policy -C ./test/runners/test_generation/config/zelta_test_policy.conf diff --git a/test/runners/env/setup_debug_env.sh b/test/runners/env/setup_debug_env.sh index 8293103..a7c5b9a 100644 --- a/test/runners/env/setup_debug_env.sh +++ b/test/runners/env/setup_debug_env.sh @@ -7,6 +7,7 @@ printf "\n*\n* Running in DEBUG MODE, sourcing setup files\n*\n" if . test/runners/env/set_reuse_tmp_env.sh; then . test/runners/env/test_env.sh # set dataset, pools and remote env vars . test/test_helper.sh # make all the helper functions available + env | grep -i sandbox else return 1 fi diff --git a/test/runners/env/test_env.sh b/test/runners/env/test_env.sh index 6fb6d8a..06f5897 100644 --- a/test/runners/env/test_env.sh +++ b/test/runners/env/test_env.sh @@ -9,7 +9,15 @@ export SANDBOX_ZELTA_SRC_DS=apool/treetop export SANDBOX_ZELTA_TGT_DS=bpool/backups # remotes setup + +# unset the current remotes +unset SANDBOX_ZELTA_SRC_REMOTE +unset SANDBOX_ZELTA_TGT_REMOTE + # * leave these undefined if you're running locally # * the endpoints are defined automatically and are REMOTE + DS -export SANDBOX_ZELTA_SRC_REMOTE=dever@zfsdev # Ubuntu source -export SANDBOX_ZELTA_TGT_REMOTE=dever@zfsdev # Ubuntu remote +#export SANDBOX_ZELTA_SRC_REMOTE=dever@uvm1 # Ubuntu source +#export SANDBOX_ZELTA_TGT_REMOTE=dever@uvm1 # Ubuntu remote + +#export SANDBOX_ZELTA_SRC_REMOTE=dever@lusvr01 # FreeBSD source +#export SANDBOX_ZELTA_TGT_REMOTE=dever@lusvr01 # FreeBSD remote diff --git a/test/runners/test_generation/Gemfile b/test/runners/test_generation/Gemfile index 27aee2b..f7a03d7 100644 --- a/test/runners/test_generation/Gemfile +++ b/test/runners/test_generation/Gemfile @@ -3,3 +3,4 @@ source 'https://rubygems.org' gem 'json-schema', '~> 4.0' +gem 'rubocop', '~> 1.60' diff --git a/test/runners/test_generation/Gemfile.lock b/test/runners/test_generation/Gemfile.lock index 8d8f3f6..5132158 100644 --- a/test/runners/test_generation/Gemfile.lock +++ b/test/runners/test_generation/Gemfile.lock @@ -3,9 +3,42 @@ GEM specs: addressable (2.8.8) public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) + json (2.19.1) json-schema (4.3.1) addressable (>= 2.8) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + mcp (0.8.0) + json-schema (>= 4.1) + parallel (1.27.0) + parser (3.3.10.2) + ast (~> 2.4.1) + racc + prism (1.9.0) public_suffix (7.0.2) + racc (1.8.1) + rainbow (3.1.1) + regexp_parser (2.11.3) + rubocop (1.85.1) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + mcp (~> 0.6) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + ruby-progressbar (1.13.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) PLATFORMS arm64-darwin-23 @@ -13,6 +46,7 @@ PLATFORMS DEPENDENCIES json-schema (~> 4.0) + rubocop (~> 1.60) BUNDLED WITH 2.6.4 diff --git a/test/runners/test_generation/bin/generate_40_divergent_test.sh b/test/runners/test_generation/bin/generate_40_divergent_test.sh new file mode 100755 index 0000000..bb304e6 --- /dev/null +++ b/test/runners/test_generation/bin/generate_40_divergent_test.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Get the directory where this script is located +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" +GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" + +# Generate tests for 40 examples + +if ! "$GENERATE_TEST" \ + "$CONFIG_DIR/040_zelta_tests.yml" \ + "test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh"; then + + printf "\n ❌ Failed to generate 040 test\n" + exit 1 +fi \ No newline at end of file diff --git a/test/runners/test_generation/bin/generate_50_revert_test.sh b/test/runners/test_generation/bin/generate_50_revert_test.sh new file mode 100755 index 0000000..9fbbfdf --- /dev/null +++ b/test/runners/test_generation/bin/generate_50_revert_test.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Get the directory where this script is located +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" +GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" + +# Generate tests for 50 examples + +if ! "$GENERATE_TEST" \ + "$CONFIG_DIR/050_zelta_revert_test.yml" \ + "test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh"; then + + printf "\n ❌ Failed to generate 050 test\n" + exit 1 +fi \ No newline at end of file diff --git a/test/runners/test_generation/bin/generate_tests_40_50_60.sh b/test/runners/test_generation/bin/generate_60_clone_test.sh similarity index 53% rename from test/runners/test_generation/bin/generate_tests_40_50_60.sh rename to test/runners/test_generation/bin/generate_60_clone_test.sh index 3c1dcbc..ee296b0 100755 --- a/test/runners/test_generation/bin/generate_tests_40_50_60.sh +++ b/test/runners/test_generation/bin/generate_60_clone_test.sh @@ -5,23 +5,7 @@ TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" -# Generate tests for 40,50,60 examples - -if ! "$GENERATE_TEST" \ - "$CONFIG_DIR/040_zelta_tests.yml" \ - "test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh"; then - - printf "\n ❌ Failed to generate 040 test\n" - exit 1 -fi - -if ! "$GENERATE_TEST" \ - "$CONFIG_DIR/050_zelta_revert_test.yml" \ - "test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh"; then - - printf "\n ❌ Failed to generate 050 test\n" - exit 1 -fi +# Generate tests for 60 examples if ! "$GENERATE_TEST" \ "$CONFIG_DIR/060_zelta_clone_test.yml" \ @@ -29,4 +13,4 @@ if ! "$GENERATE_TEST" \ printf "\n ❌ Failed to generate 060 test\n" exit 1 -fi +fi \ No newline at end of file diff --git a/test/runners/test_generation/bin/generate_prune_test.sh b/test/runners/test_generation/bin/generate_70_prune_test.sh similarity index 93% rename from test/runners/test_generation/bin/generate_prune_test.sh rename to test/runners/test_generation/bin/generate_70_prune_test.sh index aaac3ef..611ef73 100755 --- a/test/runners/test_generation/bin/generate_prune_test.sh +++ b/test/runners/test_generation/bin/generate_70_prune_test.sh @@ -5,7 +5,7 @@ TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" -# Generate tests for 40,50,60 examples +# Generate tests for 70 examples if ! "$GENERATE_TEST" \ "$CONFIG_DIR/070_zelta_prune_test.yml" \ diff --git a/test/runners/test_generation/bin/generate_policy_test.sh b/test/runners/test_generation/bin/generate_80_policy_test.sh similarity index 94% rename from test/runners/test_generation/bin/generate_policy_test.sh rename to test/runners/test_generation/bin/generate_80_policy_test.sh index b754260..6c6b4e6 100755 --- a/test/runners/test_generation/bin/generate_policy_test.sh +++ b/test/runners/test_generation/bin/generate_80_policy_test.sh @@ -5,6 +5,8 @@ TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" +# Generate tests for 80 examples + if ! "$GENERATE_TEST" \ "$CONFIG_DIR/080_zelta_policy_test.yml" \ "test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh|test/050_*_spec.sh|test/060_*_spec.sh|test/070_*_spec.sh"; then diff --git a/test/runners/test_generation/bin/generate_all_tests.sh b/test/runners/test_generation/bin/generate_all_tests.sh index 961bec8..41c3e0d 100755 --- a/test/runners/test_generation/bin/generate_all_tests.sh +++ b/test/runners/test_generation/bin/generate_all_tests.sh @@ -2,7 +2,8 @@ # Get the directory where this script is located SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -"$SCRIPT_DIR/generate_tests_40_50_60.sh" -"$SCRIPT_DIR/generate_prune_test.sh" -"$SCRIPT_DIR/generate_policy_test.sh" - +"$SCRIPT_DIR/generate_40_divergent_test.sh" +"$SCRIPT_DIR/generate_50_revert_test.sh" +"$SCRIPT_DIR/generate_60_clone_test.sh" +"$SCRIPT_DIR/generate_70_prune_test.sh" +"$SCRIPT_DIR/generate_80_policy_test.sh" diff --git a/test/runners/test_generation/bin/generate_zelta_policy_config.sh b/test/runners/test_generation/bin/generate_zelta_policy_config.sh index 9299dff..9851b5e 100755 --- a/test/runners/test_generation/bin/generate_zelta_policy_config.sh +++ b/test/runners/test_generation/bin/generate_zelta_policy_config.sh @@ -10,6 +10,18 @@ TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" CONFIG_DIR="$TEST_GEN_DIR/config" ZELTA_TEST_POLICY_CONFIG_FILE="$CONFIG_DIR/zelta_test_policy.conf" + +if [ -n "$SANDBOX_ZELTA_SRC_REMOTE" ]; then + src_host="$SANDBOX_ZELTA_SRC_REMOTE" +else + src_host="localhost" +fi + +tgt_host="" +if [ -n "${SANDBOX_ZELTA_TGT_REMOTE}" ]; then + tgt_host="${SANDBOX_ZELTA_TGT_REMOTE}:" +fi + # remove any existing policy file rm -f "$ZELTA_TEST_POLICY_CONFIG_FILE" @@ -21,7 +33,7 @@ cat < $ZELTA_TEST_POLICY_CONFIG_FILE # shellspec auto generated test zelta policy file at: ($CUR_TIME_STAMP) # NOTE: any modification will be lost BACKUP_SITE: - ${SANDBOX_ZELTA_SRC_REMOTE}: + ${src_host}: datasets: - - ${SANDBOX_ZELTA_SRC_DS}: ${SANDBOX_ZELTA_TGT_REMOTE}:${SANDBOX_ZELTA_TGT_DS}/${BACKUP_NAME} + - ${SANDBOX_ZELTA_SRC_DS}: ${tgt_host}${SANDBOX_ZELTA_TGT_DS}/${BACKUP_NAME} EOF diff --git a/test/runners/test_generation/bin/setup_debug_state.bash b/test/runners/test_generation/bin/setup_debug_state.bash index 7195e9a..2646017 100644 --- a/test/runners/test_generation/bin/setup_debug_state.bash +++ b/test/runners/test_generation/bin/setup_debug_state.bash @@ -1,14 +1,12 @@ #!/usr/bin/env bash # Check if arguments were provided -if [ $# -ne 1 ]; then - echo "Missing SPECS argument!" - echo "Usage: $0 'specs list'" +if [ $# -lt 1 ] || [ $# -gt 2 ]; then + echo "Invalid number of arguments!" + echo "Usage: $0 'pattern specs list' 'selector specs list" return 1 fi -SPECS=$1 - #VAR="${VAR-default_value}" # *** only source this script in bash, do not execute it @@ -42,21 +40,21 @@ fi if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then echo "Error: This script must be sourced, not executed." >&2 echo "Usage: source ${BASH_SOURCE[0]}" >&2 - exit 1 + return 1 fi # tree setup utility . "$TEST_GEN_DIR/lib/orchestration/setup_tree.sh" -if setup_tree "$SPECS"; then - printf "\n βœ… initial tree setup succeeded for specs: %s\n" "$SPECS" +if setup_tree "$@"; then + printf "\n βœ… initial tree setup succeeded for specs: %s\n" "$@" else - printf "\n ❌ Goodbye, initial tree setup failed for specs: %s\n" "$SPECS" - exit 1 + printf "\n ❌ Goodbye, initial tree setup failed for specs: %s\n" "$@" + return 1 fi # setup the debugging environment . "$RUNNERS_DIR/env/setup_debug_env.sh" # now you can execute commands as if they were in a test -# that runs after all the SPECS hae completed \ No newline at end of file +# that runs after all the SPECS hae completed diff --git a/test/runners/test_generation/config/test_defs/080_zelta_policy_test.yml b/test/runners/test_generation/config/test_defs/080_zelta_policy_test.yml index 8e0e0af..5ad2eff 100644 --- a/test/runners/test_generation/config/test_defs/080_zelta_policy_test.yml +++ b/test/runners/test_generation/config/test_defs/080_zelta_policy_test.yml @@ -4,13 +4,14 @@ describe_desc: "Test zelta policy" skip_if_list: - condition: if 'SANDBOX_ZELTA_SRC_DS undefined' test -z "$SANDBOX_ZELTA_SRC_DS" - condition: if 'SANDBOX_ZELTA_TGT_DS undefined' test -z "$SANDBOX_ZELTA_TGT_DS" - - condition: if 'SANDBOX_ZELTA_SRC_REMOTE undefined' test -z "$SANDBOX_ZELTA_SRC_REMOTE" - - condition: if 'SANDBOX_ZELTA_TGT_REMOTE undefined' test -z "$SANDBOX_ZELTA_TGT_REMOTE" test_list: + - test_name: generate_policy_config + it_desc: generate zelta policy - %{when_command} + allow_no_output: true + # generate zelta policy config file from env variables + when_command: ./test/runners/test_generation/bin/generate_zelta_policy_config.sh + - test_name: policy_check - setup_scripts: - # generate zelta policy config file from env variables - - "test/runners/test_generation/bin/generate_zelta_policy_config.sh" it_desc: test zelta policy - %{when_command} when_command: zelta policy -C ./test/runners/test_generation/config/zelta_test_policy.conf diff --git a/test/runners/test_generation/config/zelta_test_policy.conf b/test/runners/test_generation/config/zelta_test_policy.conf new file mode 100644 index 0000000..e21d2ec --- /dev/null +++ b/test/runners/test_generation/config/zelta_test_policy.conf @@ -0,0 +1,6 @@ +# shellspec auto generated test zelta policy file at: (2026-03-12_04.50.29) +# NOTE: any modification will be lost +BACKUP_SITE: + localhost: + datasets: + - apool/treetop: bpool/backups/zelta_policy_backup_2026-03-12_04.50.29 diff --git a/test/runners/test_generation/lib/orchestration/setup_tree.sh b/test/runners/test_generation/lib/orchestration/setup_tree.sh index cb1dbce..37a5580 100644 --- a/test/runners/test_generation/lib/orchestration/setup_tree.sh +++ b/test/runners/test_generation/lib/orchestration/setup_tree.sh @@ -6,19 +6,48 @@ REPO_ROOT=${REPO_ROOT:=$(git rev-parse --show-toplevel)} echo "REPO ROOT: $REPO_ROOT" setup_tree() { - setup_specs=$1 - trace_options=$2 - - cd "$REPO_ROOT" || exit 1 - . ./test/test_helper.sh - . ./test/runners/env/helpers.sh - setup_env "1" # setup debug environment - clean_ds_and_pools # reset tree - - if shellspec $trace_options --pattern "$setup_specs"; then - printf "\n βœ… setup succeeded for specs: %s\n" "$setup_specs" - else - printf "\n ❌ setup failed for specs: %s\n" "$setup_specs" - exit 1 + pattern_specs=$1 + selector_specs=$2 + trace_options=$3 + + cd "$REPO_ROOT" || exit 1 + . ./test/test_helper.sh + . ./test/runners/env/helpers.sh + setup_env "1" # setup debug environment + clean_ds_and_pools # reset tree + + cmd1=() + if [ -n "$pattern_specs" ]; then + cmd1=(shellspec) + if [ -n "$trace_options" ]; then + cmd1+=("$trace_options") + fi + cmd1+=(--pattern "$pattern_specs") + fi + + cmd2=() + if [ -n "$selector_specs" ]; then + cmd2=(shellspec) + if [ -n "$trace_options" ]; then + cmd2+=("$trace_options") + fi + cmd2+=("$selector_specs") + fi + + set -x + if [ ${#cmd1[@]} -gt 0 ] && ! "${cmd1[@]}"; then + printf "\n ❌ setup failed for command: %s\n" "${cmd1[*]}" + set +x + return 1 + fi + + if [ ${#cmd2[@]} -gt 0 ] && ! "${cmd2[@]}"; then + printf "\n ❌ setup failed for command: %s\n" "${cmd2[*]}" + set +x + return 1 fi + set +x + + printf "\n βœ… setup succeeded\n" + } diff --git a/test/runners/test_generation/lib/ruby/env_substitutor.rb b/test/runners/test_generation/lib/ruby/env_substitutor.rb new file mode 100644 index 0000000..f306795 --- /dev/null +++ b/test/runners/test_generation/lib/ruby/env_substitutor.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# EnvSubstitutor - Handles environment variable substitution in test output +class EnvSubstitutor + attr_reader :sorted_env_map + + def initialize(env_var_names) + @sorted_env_map = build_sorted_env_map(env_var_names) + end + + def substitute(line) + # Substitute env var names for values (longest first) + # Use a placeholder to prevent already-substituted values from being re-matched + replaced = line + placeholder_map = {} + @sorted_env_map.each_with_index do |(name, value), idx| + placeholder = "__ENV_PLACEHOLDER_#{idx}__" + replaced.gsub!(value, placeholder) + placeholder_map[placeholder] = "${#{name}}" + end + + # Replace placeholders with actual env var references + placeholder_map.each do |placeholder, replacement| + replaced.gsub!(placeholder, replacement) + end + replaced + end + + private + + def build_sorted_env_map(env_var_names) + # Parse and sort env vars by value length (descending) + env_map = env_var_names.split(':').each_with_object({}) do |name, hash| + hash[name] = ENV[name] if ENV[name] + end + + # Sort by value length descending to replace longest matches first + env_map.sort_by { |_name, value| -value.length } + end +end diff --git a/test/runners/test_generation/lib/ruby/path_config.rb b/test/runners/test_generation/lib/ruby/path_config.rb new file mode 100644 index 0000000..e47ab74 --- /dev/null +++ b/test/runners/test_generation/lib/ruby/path_config.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# PathConfig - Manages file paths for test generation +class PathConfig + attr_reader :output_dir, :wip_file_path, :final_file_path + + def initialize(output_dir, shellspec_name, test_gen_dir) + @output_dir = output_dir.start_with?('/') ? output_dir : File.join(test_gen_dir, output_dir) + @wip_file_path = File.join(@output_dir, "#{shellspec_name}_wip.sh") + # remove _spec to prevent shellspec from finding the WIP file + @wip_file_path.sub!('_spec', '') + @final_file_path = File.join(@output_dir, "#{shellspec_name}.sh") + end +end diff --git a/test/runners/test_generation/lib/ruby/test_generator.rb b/test/runners/test_generation/lib/ruby/test_generator.rb index 1acf825..30b8f54 100755 --- a/test/runners/test_generation/lib/ruby/test_generator.rb +++ b/test/runners/test_generation/lib/ruby/test_generator.rb @@ -10,6 +10,8 @@ require 'time' require_relative 'placeholders' require_relative 'sys_exec' +require_relative 'env_substitutor' +require_relative 'path_config' # TestGenerator - Generates ShellSpec test files from YAML configuration class TestGenerator @@ -22,8 +24,8 @@ class TestGenerator private_constant :REPO_ROOT, :TEST_GEN_DIR, :GENERATE_MATCHER_SH_SCRIPT - attr_reader :config, :output_dir, :shellspec_name, :describe_desc, :test_list, :skip_if_list, - :matcher_files, :wip_file_path, :final_file_path, :env_var_names, :sorted_env_map + attr_reader :config, :shellspec_name, :describe_desc, :test_list, :skip_if_list, + :matcher_files, :paths def initialize(yaml_file_path, env_var_names = DEFAULT_ENV_VAR_NAMES) # Resolve path relative to this file's directory if it's a relative path @@ -36,20 +38,14 @@ def initialize(yaml_file_path, env_var_names = DEFAULT_ENV_VAR_NAMES) @shellspec_name = @config['shellspec_name'] @describe_desc = @config['describe_desc'] - - # Resolve output_dir relative to test_generation directory - output_dir = @config['output_dir'] - @output_dir = output_dir.start_with?('/') ? output_dir : File.join(TEST_GEN_DIR, output_dir) - @test_list = @config['test_list'] || [] @skip_if_list = @config['skip_if_list'] || [] @matcher_files = [] - @wip_file_path = File.join(@output_dir, "#{@shellspec_name}_wip.sh") - # remove _spec to prevent shellspec from finding the WIP file - @wip_file_path.sub!('_spec', '') - @final_file_path = File.join(@output_dir, "#{@shellspec_name}.sh") - @env_var_names = env_var_names - @sorted_env_map = build_sorted_env_map + + # Initialize helper objects + @paths = PathConfig.new(@config['output_dir'], @shellspec_name, TEST_GEN_DIR) + @env_substitutor = EnvSubstitutor.new(env_var_names) + puts "Loading configuration from: #{@config.inspect}\n" puts '=' * 60 end @@ -64,16 +60,6 @@ def generate private - def build_sorted_env_map - # Parse and sort env vars by value length (descending) - env_map = @env_var_names.split(':').each_with_object({}) do |name, hash| - hash[name] = ENV[name] if ENV[name] - end - - # Sort by value length descending to replace longest matches first - env_map.sort_by { |_name, value| -value.length } - end - def matcher_func_name(test_name) "output_for_#{test_name}" end @@ -84,12 +70,12 @@ def validate_config!(schema_path = File.join(TEST_GEN_DIR, 'config', 'test_confi end def create_output_directory - FileUtils.mkdir_p(@output_dir) - puts "Created output directory: #{@output_dir}" + FileUtils.mkdir_p(@paths.output_dir) + puts "Created output directory: #{@paths.output_dir}" end def create_wip_file - File.open(@wip_file_path, 'w') do |file| + File.open(@paths.wip_file_path, 'w') do |file| file.puts "Describe '#{@describe_desc}'" # Add Skip If statements for each condition @@ -98,7 +84,7 @@ def create_wip_file end file.puts '' unless @skip_if_list.empty? end - puts "Created WIP file: #{@wip_file_path}" + puts "Created WIP file: #{@paths.wip_file_path}" end def process_tests @@ -121,7 +107,7 @@ def process_tests end # Close Describe block - File.open(@wip_file_path, 'a') do |file| + File.open(@paths.wip_file_path, 'a') do |file| file.puts 'End' end end @@ -141,13 +127,13 @@ def generate_matcher_files(test_name, when_command, setup_scripts, allow_no_outp # Add allow_no_output flag allow_no_output_flag = allow_no_output ? "true" : "false" - cmd = "#{matcher_script} \"#{full_command}\" #{matcher_function_name} #{@output_dir} #{allow_no_output_flag}" + cmd = "#{matcher_script} \"#{full_command}\" #{matcher_function_name} #{@paths.output_dir} #{allow_no_output_flag}" SysExec.run(cmd, timeout: 10) unless allow_no_output # Track the generated matcher file func_name = matcher_func_name(test_name) - matcher_file = File.join(@output_dir, func_name, "#{func_name}.sh") + matcher_file = File.join(@paths.output_dir, func_name, "#{func_name}.sh") # Post-process the matcher file to apply env substitutions if File.exist?(matcher_file) @@ -206,67 +192,39 @@ def build_command_with_setup(when_command, setup_scripts) end def append_it_clause(test_name, it_desc, when_command, allow_no_output) - File.open(@wip_file_path, 'a') do |file| + File.open(@paths.wip_file_path, 'a') do |file| file.puts " It \"#{it_desc.gsub('"', '\\"')}\"" func_name = matcher_func_name(test_name) - # TODO: clean up all the trial and error with shellspec error output, documented approaches don't work! # Check for stderr output - stderr_file = File.join(@output_dir, func_name, "#{func_name}_stderr.out") + stderr_file = File.join(@paths.output_dir, func_name, "#{func_name}_stderr.out") expected_error = nil if File.exist?(stderr_file) && !File.zero?(stderr_file) expected_error = format_expected_error(stderr_file) - #file.puts expected_error - #status_line = ' The status should be failure' - else - #status_line = ' The status should equal 0' end - # TODO: zelta exits with 0 even when there is error output - #status_line = ' The status should equal 0' + # TODO: double check if zelta exits with 0 even when there is error output status_line = ' The status should be success' file.puts " When call #{when_command}" - file.puts " The output should satisfy #{matcher_func_name(test_name)}" unless allow_no_output + # NOTE: this style of checking error output was the only one that worked for me, inline equal file.puts " The error should equal \"#{expected_error}\"\n" if expected_error file.puts status_line - file.puts ' End' file.puts '' end end - def v1_format_expected_error(stderr_file) - lines = read_stderr_file(stderr_file) - result = " expected_error=%text\n" - lines.each do |line| - result += " #|#{line}\n" - end - "#{result} End\n" - end - # expected_error() { %text - # #|warning: insufficient snapshots; performing full backup for 3 datasets - # #|warning: missing `zfs allow` permissions: readonly,mountpoint - # } - def v2_format_expected_error(stderr_file) - lines = read_stderr_file(stderr_file) - result = " expected_error() { %text\n" - lines.each do |line| - result += " #|#{line}\n" - end - "#{result} }\n" - end - def format_expected_error(stderr_file) lines = read_stderr_file(stderr_file) lines.map! { |line| normalize_output_line(line) } lines.join("\n") end - def normalize_output_line(line) + def clean_up_output_line(line) # Normalize whitespace normalized = line.gsub(/\s+/, ' ').strip @@ -274,6 +232,9 @@ def normalize_output_line(line) normalized.gsub!(/@zelta_\d{4}-\d{2}-\d{2}_\d{2}\.\d{2}\.\d{2}/, '@zelta_"*"') normalized.gsub!(/_zelta_\d{4}-\d{2}-\d{2}_\d{2}\.\d{2}\.\d{2}/, '_zelta_"*"') + # replace timestamp for generated zelta policy files is YYYY-MM-DD_HH.MM.SS with * + normalized.gsub!(/_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}\.[0-9]{2}\.[0-9]{2}/, '_"*"') + # Escape backticks normalized.gsub!('`', '\\\`') @@ -281,26 +242,20 @@ def normalize_output_line(line) if normalized =~ /(\d+[KMGT]? sent, )(\d+ streams)( received in \d+\.\d+ seconds)/ stream_count = $2 normalized.gsub!(/\d+[KMGT]? sent, \d+ streams received in \d+\.\d+ seconds/, - "* sent, #{stream_count} received in * seconds") - end - - # Substitute env var names for values (longest first) - # Use a placeholder to prevent already-substituted values from being re-matched - placeholder_map = {} - @sorted_env_map.each_with_index do |(name, value), idx| - placeholder = "__ENV_PLACEHOLDER_#{idx}__" - normalized.gsub!(value, placeholder) - placeholder_map[placeholder] = "${#{name}}" + "\"*\" sent, #{stream_count} received in \"*\" seconds") end - - # Replace placeholders with actual env var references - placeholder_map.each do |placeholder, replacement| - normalized.gsub!(placeholder, replacement) - end - normalized end + def normalize_output_line(line) + result = @env_substitutor.substitute( + clean_up_output_line(line) + ) + puts("line subs; before: #{line}") + puts("line subs; before: #{result}") + result + end + def read_stderr_file(stderr_file) File.readlines(stderr_file).map(&:chomp) rescue StandardError => e @@ -309,7 +264,7 @@ def read_stderr_file(stderr_file) end def assemble_final_file - File.open(@final_file_path, 'w') do |final| + File.open(@paths.final_file_path, 'w') do |final| final.puts '# Auto-generated ShellSpec test file' final.puts "# Generated at: #{Time.now}" final.puts "# Source: #{@shellspec_name}" @@ -325,9 +280,9 @@ def assemble_final_file end # Copy the WIP file content - final.puts File.read(@wip_file_path) if File.exist?(@wip_file_path) + final.puts File.read(@paths.wip_file_path) if File.exist?(@paths.wip_file_path) end - puts "Assembled final test file: #{@final_file_path}" + puts "Assembled final test file: #{@paths.final_file_path}" end def report_summary @@ -337,16 +292,16 @@ def report_summary puts "YAML Configuration: #{@config.inspect}" puts "ShellSpec Name: #{@shellspec_name}" puts "Description: #{@describe_desc}" - puts "Output Directory: #{@output_dir}" + puts "Output Directory: #{@paths.output_dir}" puts "Tests Processed: #{@test_list.length}" puts "Matcher Files Generated: #{@matcher_files.length}" puts "\nGenerated Files:" - puts " - WIP File: #{@wip_file_path}" + puts " - WIP File: #{@paths.wip_file_path}" @matcher_files.each do |file| puts " - Matcher: #{file}" end puts "\nFinal ShellSpec Test File:" - puts " Location: #{@final_file_path}" + puts " Location: #{@paths.final_file_path}" puts '=' * 60 puts "__SHELLSPEC_NAME__:#{@shellspec_name}" end @@ -380,3 +335,4 @@ def run_generator # Script execution run_generator if __FILE__ == $PROGRAM_NAME + diff --git a/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk b/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk index ba51731..82793ed 100644 --- a/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk +++ b/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk @@ -8,20 +8,8 @@ # Process data lines { + # normalize whitespace to a single space gsub(/[[:space:]]+/, " ", $0) - gsub(/@zelta_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}\.[0-9]{2}\.[0-9]{2}/, "@zelta_\"*\"",$0) - gsub(/_zelta_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}\.[0-9]{2}\.[0-9]{2}/, "_zelta_\"*\"",$0) - gsub(/`/, "\\`", $0) - - # wildcard time and quantity sent - if (match($0, /[0-9]+[KMGT]? sent, [0-9]+ streams/)) { - # Extract the part with streams - streams_part = substr($0, RSTART, RLENGTH) - # Extract just the number before " streams" - match(streams_part, /[0-9]+ streams/) - stream_count = substr(streams_part, RSTART, RLENGTH) - gsub(/[0-9]+[KMGT]? sent, [0-9]+ streams received in [0-9]+\.[0-9]+ seconds/, "* sent, " stream_count " received in * seconds", $0) - } # remove trailing spaces sub(/[[:space:]]+$/, "", $0) From b1b77730596b7502a33660b0ca162ed099d24e40 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Thu, 12 Mar 2026 03:00:39 -0400 Subject: [PATCH 08/35] streamline test gen, regenerate all tests --- test/040_zelta_tests_spec.sh | 2 +- test/050_zelta_revert_spec.sh | 2 +- test/060_zelta_clone_spec.sh | 2 +- test/070_zelta_prune_spec.sh | 2 +- test/080_zelta_policy_spec.sh | 4 +-- test/runners/env/test_env.sh | 4 +-- .../bin/generate_40_divergent_test.sh | 16 +++--------- .../bin/generate_50_revert_test.sh | 16 +++--------- .../bin/generate_60_clone_test.sh | 16 +++--------- .../bin/generate_70_prune_test.sh | 16 +++--------- .../bin/generate_80_policy_test.sh | 16 +++--------- .../test_generation/bin/generate_all_tests.sh | 2 ++ .../test_generation/bin/generate_test.sh | 25 +++++++++++++++++++ .../bin/setup_debug_state.bash | 15 ++++++----- .../config/zelta_test_policy.conf | 4 +-- 15 files changed, 58 insertions(+), 84 deletions(-) create mode 100755 test/runners/test_generation/bin/generate_test.sh diff --git a/test/040_zelta_tests_spec.sh b/test/040_zelta_tests_spec.sh index 86a689b..10e2be4 100644 --- a/test/040_zelta_tests_spec.sh +++ b/test/040_zelta_tests_spec.sh @@ -1,5 +1,5 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-10 23:54:05 -0400 +# Generated at: 2026-03-12 02:42:58 -0400 # Source: 040_zelta_tests_spec # WARNING: This file was automatically generated. Manual edits may be lost. diff --git a/test/050_zelta_revert_spec.sh b/test/050_zelta_revert_spec.sh index bbb3c8c..9a279fd 100644 --- a/test/050_zelta_revert_spec.sh +++ b/test/050_zelta_revert_spec.sh @@ -1,5 +1,5 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-10 23:54:51 -0400 +# Generated at: 2026-03-12 02:43:45 -0400 # Source: 050_zelta_revert_spec # WARNING: This file was automatically generated. Manual edits may be lost. diff --git a/test/060_zelta_clone_spec.sh b/test/060_zelta_clone_spec.sh index 95eb892..b8b0bac 100644 --- a/test/060_zelta_clone_spec.sh +++ b/test/060_zelta_clone_spec.sh @@ -1,5 +1,5 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-10 23:55:50 -0400 +# Generated at: 2026-03-12 02:44:46 -0400 # Source: 060_zelta_clone_spec # WARNING: This file was automatically generated. Manual edits may be lost. diff --git a/test/070_zelta_prune_spec.sh b/test/070_zelta_prune_spec.sh index 8c5b05d..81e1c45 100644 --- a/test/070_zelta_prune_spec.sh +++ b/test/070_zelta_prune_spec.sh @@ -1,5 +1,5 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-10 23:56:54 -0400 +# Generated at: 2026-03-12 02:45:52 -0400 # Source: 070_zelta_prune_spec # WARNING: This file was automatically generated. Manual edits may be lost. diff --git a/test/080_zelta_policy_spec.sh b/test/080_zelta_policy_spec.sh index 6816d7d..7c6221c 100644 --- a/test/080_zelta_policy_spec.sh +++ b/test/080_zelta_policy_spec.sh @@ -1,5 +1,5 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-10 23:58:06 -0400 +# Generated at: 2026-03-12 02:47:09 -0400 # Source: 080_zelta_policy_spec # WARNING: This file was automatically generated. Manual edits may be lost. @@ -23,8 +23,6 @@ output_for_policy_check() { Describe 'Test zelta policy' Skip if 'SANDBOX_ZELTA_SRC_DS undefined' test -z "$SANDBOX_ZELTA_SRC_DS" Skip if 'SANDBOX_ZELTA_TGT_DS undefined' test -z "$SANDBOX_ZELTA_TGT_DS" - #Skip if 'SANDBOX_ZELTA_SRC_REMOTE undefined' test -z "$SANDBOX_ZELTA_SRC_REMOTE" - #Skip if 'SANDBOX_ZELTA_TGT_REMOTE undefined' test -z "$SANDBOX_ZELTA_TGT_REMOTE" It "generate zelta policy - ./test/runners/test_generation/bin/generate_zelta_policy_config.sh" When call ./test/runners/test_generation/bin/generate_zelta_policy_config.sh diff --git a/test/runners/env/test_env.sh b/test/runners/env/test_env.sh index 06f5897..fb84a41 100644 --- a/test/runners/env/test_env.sh +++ b/test/runners/env/test_env.sh @@ -19,5 +19,5 @@ unset SANDBOX_ZELTA_TGT_REMOTE #export SANDBOX_ZELTA_SRC_REMOTE=dever@uvm1 # Ubuntu source #export SANDBOX_ZELTA_TGT_REMOTE=dever@uvm1 # Ubuntu remote -#export SANDBOX_ZELTA_SRC_REMOTE=dever@lusvr01 # FreeBSD source -#export SANDBOX_ZELTA_TGT_REMOTE=dever@lusvr01 # FreeBSD remote +export SANDBOX_ZELTA_SRC_REMOTE=dever@lusvr01 # FreeBSD source +export SANDBOX_ZELTA_TGT_REMOTE=dever@lusvr01 # FreeBSD remote diff --git a/test/runners/test_generation/bin/generate_40_divergent_test.sh b/test/runners/test_generation/bin/generate_40_divergent_test.sh index bb304e6..0de67d5 100755 --- a/test/runners/test_generation/bin/generate_40_divergent_test.sh +++ b/test/runners/test_generation/bin/generate_40_divergent_test.sh @@ -1,16 +1,6 @@ #!/usr/bin/env bash -# Get the directory where this script is located -SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" -GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" -# Generate tests for 40 examples +TEST_DEF=040_zelta_tests.yml +SETUP_SPECS="test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh" -if ! "$GENERATE_TEST" \ - "$CONFIG_DIR/040_zelta_tests.yml" \ - "test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh"; then - - printf "\n ❌ Failed to generate 040 test\n" - exit 1 -fi \ No newline at end of file +./generate_test.sh $TEST_DEF "$SETUP_SPECS" \ No newline at end of file diff --git a/test/runners/test_generation/bin/generate_50_revert_test.sh b/test/runners/test_generation/bin/generate_50_revert_test.sh index 9fbbfdf..a104238 100755 --- a/test/runners/test_generation/bin/generate_50_revert_test.sh +++ b/test/runners/test_generation/bin/generate_50_revert_test.sh @@ -1,16 +1,6 @@ #!/usr/bin/env bash -# Get the directory where this script is located -SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" -GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" -# Generate tests for 50 examples +TEST_DEF=050_zelta_revert_test.yml +SETUP_SPECS="test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh" -if ! "$GENERATE_TEST" \ - "$CONFIG_DIR/050_zelta_revert_test.yml" \ - "test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh"; then - - printf "\n ❌ Failed to generate 050 test\n" - exit 1 -fi \ No newline at end of file +./generate_test.sh $TEST_DEF "$SETUP_SPECS" diff --git a/test/runners/test_generation/bin/generate_60_clone_test.sh b/test/runners/test_generation/bin/generate_60_clone_test.sh index ee296b0..ba30dea 100755 --- a/test/runners/test_generation/bin/generate_60_clone_test.sh +++ b/test/runners/test_generation/bin/generate_60_clone_test.sh @@ -1,16 +1,6 @@ #!/usr/bin/env bash -# Get the directory where this script is located -SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" -GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" -# Generate tests for 60 examples +TEST_DEF=060_zelta_clone_test.yml +SETUP_SPECS="test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh|test/050_*_spec.sh" -if ! "$GENERATE_TEST" \ - "$CONFIG_DIR/060_zelta_clone_test.yml" \ - "test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh|test/050_*_spec.sh"; then - - printf "\n ❌ Failed to generate 060 test\n" - exit 1 -fi \ No newline at end of file +./generate_test.sh $TEST_DEF "$SETUP_SPECS" diff --git a/test/runners/test_generation/bin/generate_70_prune_test.sh b/test/runners/test_generation/bin/generate_70_prune_test.sh index 611ef73..ccf4baf 100755 --- a/test/runners/test_generation/bin/generate_70_prune_test.sh +++ b/test/runners/test_generation/bin/generate_70_prune_test.sh @@ -1,16 +1,6 @@ #!/usr/bin/env bash -# Get the directory where this script is located -SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" -GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" -# Generate tests for 70 examples +TEST_DEF=070_zelta_prune_test.yml +SETUP_SPECS="test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh|test/050_*_spec.sh|test/060_*_spec.sh" -if ! "$GENERATE_TEST" \ - "$CONFIG_DIR/070_zelta_prune_test.yml" \ - "test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh|test/050_*_spec.sh|test/060_*_spec.sh"; then - - printf "\n ❌ Failed to generate 070 test\n" - exit 1 -fi +./generate_test.sh $TEST_DEF "$SETUP_SPECS" diff --git a/test/runners/test_generation/bin/generate_80_policy_test.sh b/test/runners/test_generation/bin/generate_80_policy_test.sh index 6c6b4e6..725b69a 100755 --- a/test/runners/test_generation/bin/generate_80_policy_test.sh +++ b/test/runners/test_generation/bin/generate_80_policy_test.sh @@ -1,16 +1,6 @@ #!/usr/bin/env bash -# Get the directory where this script is located -SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" -GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" -# Generate tests for 80 examples +TEST_DEF=080_zelta_policy_test.yml +SETUP_SPECS="test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh|test/050_*_spec.sh|test/060_*_spec.sh|test/070_*_spec.sh" -if ! "$GENERATE_TEST" \ - "$CONFIG_DIR/080_zelta_policy_test.yml" \ - "test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh|test/040_*_spec.sh|test/050_*_spec.sh|test/060_*_spec.sh|test/070_*_spec.sh"; then - - printf "\n ❌ Failed to generate 080 test\n" - exit 1 -fi +./generate_test.sh $TEST_DEF "$SETUP_SPECS" diff --git a/test/runners/test_generation/bin/generate_all_tests.sh b/test/runners/test_generation/bin/generate_all_tests.sh index 41c3e0d..eb412eb 100755 --- a/test/runners/test_generation/bin/generate_all_tests.sh +++ b/test/runners/test_generation/bin/generate_all_tests.sh @@ -2,6 +2,8 @@ # Get the directory where this script is located SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +set -e + "$SCRIPT_DIR/generate_40_divergent_test.sh" "$SCRIPT_DIR/generate_50_revert_test.sh" "$SCRIPT_DIR/generate_60_clone_test.sh" diff --git a/test/runners/test_generation/bin/generate_test.sh b/test/runners/test_generation/bin/generate_test.sh new file mode 100755 index 0000000..0d7e9bf --- /dev/null +++ b/test/runners/test_generation/bin/generate_test.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# Check if arguments were provided +if [ $# -ne 2 ]; then + echo "Invalid number of arguments!" + echo "Usage: $0 (test yml file) (setup specs)" + exit 1 +fi + +TEST_DEF=$1 +SETUP_SPECS=$2 + +# Get the directory where this script is located +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +TEST_GEN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG_DIR="$TEST_GEN_DIR/config/test_defs" +GENERATE_TEST="$TEST_GEN_DIR/lib/orchestration/generate_test.sh" + + +printf "\n***\n*** Generating test for %s\n***\n" "$CONFIG_DIR/$TEST_DEF" +if ! "$GENERATE_TEST" "$CONFIG_DIR/$TEST_DEF" "$SETUP_SPECS"; then + printf "\n ❌ Failed to generate test for %s\n" "$TEST_DEF" + exit 1 +fi + diff --git a/test/runners/test_generation/bin/setup_debug_state.bash b/test/runners/test_generation/bin/setup_debug_state.bash index 2646017..04ed7b9 100644 --- a/test/runners/test_generation/bin/setup_debug_state.bash +++ b/test/runners/test_generation/bin/setup_debug_state.bash @@ -1,13 +1,5 @@ #!/usr/bin/env bash -# Check if arguments were provided -if [ $# -lt 1 ] || [ $# -gt 2 ]; then - echo "Invalid number of arguments!" - echo "Usage: $0 'pattern specs list' 'selector specs list" - return 1 -fi - -#VAR="${VAR-default_value}" # *** only source this script in bash, do not execute it # This is a helper for setting up a test environment with the datasets @@ -43,6 +35,13 @@ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then return 1 fi +# Check if arguments were provided +if [ $# -lt 1 ] || [ $# -gt 2 ]; then + echo "Invalid number of arguments!" + echo "Usage: $0 'pattern specs list' 'selector specs list" + return 1 +fi + # tree setup utility . "$TEST_GEN_DIR/lib/orchestration/setup_tree.sh" diff --git a/test/runners/test_generation/config/zelta_test_policy.conf b/test/runners/test_generation/config/zelta_test_policy.conf index e21d2ec..738d987 100644 --- a/test/runners/test_generation/config/zelta_test_policy.conf +++ b/test/runners/test_generation/config/zelta_test_policy.conf @@ -1,6 +1,6 @@ -# shellspec auto generated test zelta policy file at: (2026-03-12_04.50.29) +# shellspec auto generated test zelta policy file at: (2026-03-12_06.57.38) # NOTE: any modification will be lost BACKUP_SITE: localhost: datasets: - - apool/treetop: bpool/backups/zelta_policy_backup_2026-03-12_04.50.29 + - apool/treetop: bpool/backups/zelta_policy_backup_2026-03-12_06.57.38 From 50239429e69868b02e6b971e258590822ec4155a Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Fri, 13 Mar 2026 08:11:44 +0000 Subject: [PATCH 09/35] improve error handling, prevent shell exit from source files --- .../lib/orchestration/setup_tree.sh | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/test/runners/test_generation/lib/orchestration/setup_tree.sh b/test/runners/test_generation/lib/orchestration/setup_tree.sh index 37a5580..b616e27 100644 --- a/test/runners/test_generation/lib/orchestration/setup_tree.sh +++ b/test/runners/test_generation/lib/orchestration/setup_tree.sh @@ -10,11 +10,27 @@ setup_tree() { selector_specs=$2 trace_options=$3 - cd "$REPO_ROOT" || exit 1 - . ./test/test_helper.sh - . ./test/runners/env/helpers.sh - setup_env "1" # setup debug environment - clean_ds_and_pools # reset tree + cd "$REPO_ROOT" || return 1 + + if ! . ./test/test_helper.sh; then + echo "source ./test/test_helper.sh failed" + return 1 + fi + + if ! . ./test/runners/env/helpers.sh; then + echo "source ./test/runners/env/helpers.sh failed" + return 1 + fi + + if ! setup_env "1"; then + echo "setup_env failed" + return 1 + fi + + if ! clean_ds_and_pools; then + echo "clean_ds_and_pools failed" + return 1 + fi cmd1=() if [ -n "$pattern_specs" ]; then From 09dfd5ff5e6aaaee0df8fd6e8e09845f6b814e08 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Fri, 13 Mar 2026 22:37:18 -0400 Subject: [PATCH 10/35] regenerate all tests with matcher function printf fix --- test/040_zelta_tests_spec.sh | 26 +++++++++++++++++--------- test/050_zelta_revert_spec.sh | 26 +++++++++++++++++--------- test/060_zelta_clone_spec.sh | 8 +++++--- test/070_zelta_prune_spec.sh | 14 +++++++++----- test/080_zelta_policy_spec.sh | 11 ++++++----- 5 files changed, 54 insertions(+), 31 deletions(-) diff --git a/test/040_zelta_tests_spec.sh b/test/040_zelta_tests_spec.sh index 10e2be4..9528644 100644 --- a/test/040_zelta_tests_spec.sh +++ b/test/040_zelta_tests_spec.sh @@ -1,12 +1,13 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-12 02:42:58 -0400 +# Generated at: 2026-03-13 13:27:43 -0400 # Source: 040_zelta_tests_spec # WARNING: This file was automatically generated. Manual edits may be lost. output_for_match_after_divergence() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s +' 11 total datasets compared | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "DS_SUFFIX MATCH SRC_LAST TGT_LAST INFO"|\ "[treetop] @start @start @start up-to-date"|\ @@ -24,7 +25,8 @@ output_for_match_after_divergence() { "11 total datasets compared") ;; *) - printf "Unexpected line format: %s\n" "$line" >&2 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized_line" >&2 return 1 ;; esac @@ -35,7 +37,8 @@ output_for_match_after_divergence() { output_for_rotate_after_divergence() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s +' 36K sent, 10 streams received in 0.38 seconds | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "source is written; snapshotting: @zelta_"*""|\ "renaming '${SANDBOX_ZELTA_TGT_DS}' to '${SANDBOX_ZELTA_TGT_DS}_start'"|\ @@ -44,7 +47,8 @@ output_for_rotate_after_divergence() { ""*" sent, 10 streams received in "*" seconds") ;; *) - printf "Unexpected line format: %s\n" "$line" >&2 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized_line" >&2 return 1 ;; esac @@ -55,7 +59,8 @@ output_for_rotate_after_divergence() { output_for_match_after_rotate() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s +' 10 total datasets compared | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "DS_SUFFIX MATCH SRC_LAST TGT_LAST INFO"|\ "[treetop] @zelta_"*" @zelta_"*" @zelta_"*" up-to-date"|\ @@ -72,7 +77,8 @@ output_for_match_after_rotate() { "10 total datasets compared") ;; *) - printf "Unexpected line format: %s\n" "$line" >&2 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized_line" >&2 return 1 ;; esac @@ -83,14 +89,16 @@ output_for_match_after_rotate() { output_for_backup_after_rotate() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s +' 1K sent, 3 streams received in 0.06 seconds | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "syncing 10 datasets"|\ "10 datasets up-to-date"|\ ""*" sent, 3 streams received in "*" seconds") ;; *) - printf "Unexpected line format: %s\n" "$line" >&2 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized_line" >&2 return 1 ;; esac diff --git a/test/050_zelta_revert_spec.sh b/test/050_zelta_revert_spec.sh index 9a279fd..2e7f952 100644 --- a/test/050_zelta_revert_spec.sh +++ b/test/050_zelta_revert_spec.sh @@ -1,17 +1,19 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-12 02:43:45 -0400 +# Generated at: 2026-03-13 13:28:30 -0400 # Source: 050_zelta_revert_spec # WARNING: This file was automatically generated. Manual edits may be lost. output_for_snapshot() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s +' snapshot created 'apool/treetop@manual_test' | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "snapshot created '${SANDBOX_ZELTA_SRC_DS}@manual_test'") ;; *) - printf "Unexpected line format: %s\n" "$line" >&2 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized_line" >&2 return 1 ;; esac @@ -22,14 +24,16 @@ output_for_snapshot() { output_for_backup_after_delta() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s +' 37K sent, 22 streams received in 0.47 seconds | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "source is written; snapshotting: @zelta_"*""|\ "syncing 12 datasets"|\ ""*" sent, 22 streams received in "*" seconds") ;; *) - printf "Unexpected line format: %s\n" "$line" >&2 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized_line" >&2 return 1 ;; esac @@ -40,12 +44,14 @@ output_for_backup_after_delta() { output_for_snapshot_again() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s +' snapshot created 'apool/treetop@another_test' | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "snapshot created '${SANDBOX_ZELTA_SRC_DS}@another_test'") ;; *) - printf "Unexpected line format: %s\n" "$line" >&2 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized_line" >&2 return 1 ;; esac @@ -56,7 +62,8 @@ output_for_snapshot_again() { output_for_rotate_after_revert() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s +' 6K sent, 10 streams received in 0.38 seconds | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "renaming '${SANDBOX_ZELTA_TGT_DS}' to '${SANDBOX_ZELTA_TGT_DS}_zelta_"*"'"|\ "to ensure target is up-to-date, run: zelta backup ${SANDBOX_ZELTA_SRC_EP} ${SANDBOX_ZELTA_TGT_EP}"|\ @@ -65,7 +72,8 @@ output_for_rotate_after_revert() { ""*" sent, 10 streams received in "*" seconds") ;; *) - printf "Unexpected line format: %s\n" "$line" >&2 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized_line" >&2 return 1 ;; esac diff --git a/test/060_zelta_clone_spec.sh b/test/060_zelta_clone_spec.sh index b8b0bac..a8eaa86 100644 --- a/test/060_zelta_clone_spec.sh +++ b/test/060_zelta_clone_spec.sh @@ -1,19 +1,21 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-12 02:44:46 -0400 +# Generated at: 2026-03-13 13:29:30 -0400 # Source: 060_zelta_clone_spec # WARNING: This file was automatically generated. Manual edits may be lost. output_for_zfs_list_for_clone() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s +' apool/treetop/copy_of_sub2/orphan apool/treetop/sub2/orphan@zelta_2026-03-13_17.29.21 | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "NAME ORIGIN"|\ "${SANDBOX_ZELTA_SRC_DS}/copy_of_sub2 ${SANDBOX_ZELTA_SRC_DS}/sub2@zelta_"*""|\ "${SANDBOX_ZELTA_SRC_DS}/copy_of_sub2/orphan ${SANDBOX_ZELTA_SRC_DS}/sub2/orphan@zelta_"*"") ;; *) - printf "Unexpected line format: %s\n" "$line" >&2 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized_line" >&2 return 1 ;; esac diff --git a/test/070_zelta_prune_spec.sh b/test/070_zelta_prune_spec.sh index 81e1c45..150c5b1 100644 --- a/test/070_zelta_prune_spec.sh +++ b/test/070_zelta_prune_spec.sh @@ -1,19 +1,21 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-12 02:45:52 -0400 +# Generated at: 2026-03-13 13:30:37 -0400 # Source: 070_zelta_prune_spec # WARNING: This file was automatically generated. Manual edits may be lost. output_for_backup_with_snapshot() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s +' 31K sent, 12 streams received in 0.24 seconds | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "snapshotting: @zelta_"*""|\ "syncing 12 datasets"|\ ""*" sent, 12 streams received in "*" seconds") ;; *) - printf "Unexpected line format: %s\n" "$line" >&2 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized_line" >&2 return 1 ;; esac @@ -24,7 +26,8 @@ output_for_backup_with_snapshot() { output_for_prune_check() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s +' apool/treetop/sub4/zvol@zelta_2026-03-13_17.30.23 | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "${SANDBOX_ZELTA_SRC_DS}@zelta_"*""|\ "${SANDBOX_ZELTA_SRC_DS}/sub1@zelta_"*""|\ @@ -38,7 +41,8 @@ output_for_prune_check() { "${SANDBOX_ZELTA_SRC_DS}/sub4/zvol@zelta_"*"") ;; *) - printf "Unexpected line format: %s\n" "$line" >&2 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized_line" >&2 return 1 ;; esac diff --git a/test/080_zelta_policy_spec.sh b/test/080_zelta_policy_spec.sh index 7c6221c..bbfd32c 100644 --- a/test/080_zelta_policy_spec.sh +++ b/test/080_zelta_policy_spec.sh @@ -1,18 +1,19 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-12 02:47:09 -0400 +# Generated at: 2026-03-13 13:31:48 -0400 # Source: 080_zelta_policy_spec # WARNING: This file was automatically generated. Manual edits may be lost. output_for_policy_check() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s +' [BACKUP_SITE: dever@uvm1:bpool/backups] dever@uvm1:apool/treetop: 12 datasets up-to-date | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in - "[BACKUP_SITE: ${SANDBOX_ZELTA_TGT_EP}/zelta_policy_backup_"*"] ${SANDBOX_ZELTA_SRC_EP}: syncing 12 datasets"|\ - "[BACKUP_SITE: ${SANDBOX_ZELTA_TGT_EP}/zelta_policy_backup_"*"] ${SANDBOX_ZELTA_SRC_EP}: "*" sent, 22 streams received in "*" seconds") + "[BACKUP_SITE: ${SANDBOX_ZELTA_TGT_EP}] ${SANDBOX_ZELTA_SRC_EP}: 12 datasets up-to-date") ;; *) - printf "Unexpected line format: %s\n" "$line" >&2 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized_line" >&2 return 1 ;; esac From d8bdd182606c58c751f0a1137653b71068e33f05 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Fri, 13 Mar 2026 22:38:09 -0400 Subject: [PATCH 11/35] use printf instead of echo in normalization --- .../test_generation/scripts/awk/generate_case_stmt_func.awk | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk b/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk index 82793ed..ea2ffc3 100644 --- a/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk +++ b/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk @@ -21,7 +21,8 @@ END { print func_name "() {" print " while IFS= read -r line; do" print " # normalize whitespace, remove leading/trailing spaces" - print " normalized=$(echo \"$line\" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" + print " normalized=$(printf '%s\n' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" + print " case \"$normalized\" in" line_continue = "\"|\\" @@ -34,7 +35,8 @@ END { print " ;;" print " *)" - print " printf \"Unexpected line format: %s\\n\" \"$line\" >&2" + print " printf \"Unexpected line format : %s\\n\" \"$line\" >&2" + print " printf \"Comparing to normalized: %s\\n\" \"$normalized_line\" >&2" print " return 1" print " ;;" print " esac" From f6d62bc471ea80619975e3e70b9b2fe88be3abe6 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Fri, 13 Mar 2026 22:38:47 -0400 Subject: [PATCH 12/35] handle localhost when generating policy --- .../test_generation/bin/generate_zelta_policy_config.sh | 2 +- test/runners/test_generation/config/zelta_test_policy.conf | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/runners/test_generation/bin/generate_zelta_policy_config.sh b/test/runners/test_generation/bin/generate_zelta_policy_config.sh index 9851b5e..1c60cc8 100755 --- a/test/runners/test_generation/bin/generate_zelta_policy_config.sh +++ b/test/runners/test_generation/bin/generate_zelta_policy_config.sh @@ -35,5 +35,5 @@ cat < $ZELTA_TEST_POLICY_CONFIG_FILE BACKUP_SITE: ${src_host}: datasets: - - ${SANDBOX_ZELTA_SRC_DS}: ${tgt_host}${SANDBOX_ZELTA_TGT_DS}/${BACKUP_NAME} + - ${SANDBOX_ZELTA_SRC_DS}: ${SANDBOX_ZELTA_TGT_EP} EOF diff --git a/test/runners/test_generation/config/zelta_test_policy.conf b/test/runners/test_generation/config/zelta_test_policy.conf index 738d987..6243eae 100644 --- a/test/runners/test_generation/config/zelta_test_policy.conf +++ b/test/runners/test_generation/config/zelta_test_policy.conf @@ -1,6 +1,6 @@ -# shellspec auto generated test zelta policy file at: (2026-03-12_06.57.38) +# shellspec auto generated test zelta policy file at: (2026-03-14_02.33.22) # NOTE: any modification will be lost BACKUP_SITE: - localhost: + dever@lusvr01: datasets: - - apool/treetop: bpool/backups/zelta_policy_backup_2026-03-12_06.57.38 + - apool/treetop: dever@uvm1:bpool/backups From 528a5a0b63ea993dd6bf9447129c9333d3dcef03 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sat, 14 Mar 2026 18:04:54 -0400 Subject: [PATCH 13/35] add vm helpers for mimicign github testing, first version of github workflow --- .github/workflows/shellspec.yml | 43 +++++++++++++ .gitignore | 4 +- test/runners/vm/VM-README.md | 111 ++++++++++++++++++++++++++++++++ test/runners/vm/vm-setup.sh | 60 +++++++++++++++++ 4 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/shellspec.yml create mode 100644 test/runners/vm/VM-README.md create mode 100644 test/runners/vm/vm-setup.sh diff --git a/.github/workflows/shellspec.yml b/.github/workflows/shellspec.yml new file mode 100644 index 0000000..c584fd3 --- /dev/null +++ b/.github/workflows/shellspec.yml @@ -0,0 +1,43 @@ +name: ShellSpec Tests + +on: + push: + branches: [main, dev, "feature/**"] + pull_request: + branches: [main, dev] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y zfsutils-linux man-db + + - name: Install ShellSpec + run: curl -fsSL https://git.io/shellspec | sh -s -- --yes --prefix /usr/local + + - name: Create test user + run: | + sudo useradd -m -s /bin/bash testuser + echo 'testuser ALL=(ALL) NOPASSWD: /usr/bin/dd *, /usr/bin/rm -f /tmp/*, /usr/bin/truncate *, /usr/sbin/zpool *, /usr/sbin/zfs *' \ + | sudo tee /etc/sudoers.d/testuser + + - name: Copy repo for test user + run: | + sudo cp -r "$GITHUB_WORKSPACE" /home/testuser/zelta + sudo chown -R testuser:testuser /home/testuser/zelta + + - name: Run ShellSpec tests + run: | + sudo -u testuser env \ + SANDBOX_ZELTA_SRC_POOL=apool \ + SANDBOX_ZELTA_TGT_POOL=bpool \ + SANDBOX_ZELTA_SRC_DS=apool/treetop \ + SANDBOX_ZELTA_TGT_DS=bpool/backups \ + bash -c 'cd ~/zelta && shellspec' diff --git a/.gitignore b/.gitignore index db73fa3..83d79af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ +.* !.gitignore !.shellspec +!.github/ *.swp *.swo -.* doc/man? tmp hide.* @@ -14,3 +15,4 @@ A*d retired/ logs log +ci/* \ No newline at end of file diff --git a/test/runners/vm/VM-README.md b/test/runners/vm/VM-README.md new file mode 100644 index 0000000..bee7584 --- /dev/null +++ b/test/runners/vm/VM-README.md @@ -0,0 +1,111 @@ +# Local CI VM for ShellSpec Testing + +A lightweight Ubuntu 24.04 VM for running ShellSpec tests locally, matching the GitHub Actions `ubuntu-latest` environment. + +## Prerequisites + +- QEMU/KVM with libvirt (`virt-install`, `virsh`) +- A working bridge network (e.g., `br0`) or the default NAT network + +## Creating the VM + +Download the Ubuntu Server 24.04 cloud image: + +```bash +cd /var/lib/libvirt/images +sudo wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img +``` + +Create the VM: + +```bash +sudo virt-install \ + --name zelta-ci \ + --ram 2048 \ + --vcpus 2 \ + --disk path=/var/lib/libvirt/images/zelta-ci.qcow2,backing_store=/var/lib/libvirt/images/noble-server-cloudimg-amd64.img,size=10 \ + --os-variant ubuntu24.04 \ + --network bridge=br0 \ + --cloud-init root-password-generate=on \ + --noautoconsole +``` + +Save the generated root password from the output. + +## Initial Setup + +Connect to the VM console to get networking up: + +```bash +sudo virsh console zelta-ci # Ctrl+] to disconnect +``` + +### Configure VM with setup script from GitHub +From the VM console as root, pull and run the setup script: +```bash +curl -fsSL https://raw.githubusercontent.com/bell-tower/zelta/main/test/runners/vm/vm-setup.sh | bash +``` + +### configure VM via scp +Find the VM's IP: + +```bash +sudo virsh domifaddr zelta-ci +``` + +SSH in and run the setup script: + +```bash +ssh root@ +# Copy vm-setup.sh to the VM, then: +bash vm-setup.sh +``` + +Copy the zelta repo to the VM: + +```bash +scp -r /path/to/zelta root@:/home/testuser/zelta +# Or from inside the VM: +su - testuser -c 'git clone ~/zelta' +``` + +Make sure ownership is correct: + +```bash +chown -R testuser:testuser /home/testuser/zelta +``` + +## Running Tests + +```bash +su - testuser -c ~testuser/run_test.sh +``` + +## VM Management + +Snapshot after setup (while running): + +```bash +sudo virsh snapshot-create-as zelta-ci clean-baseline --description "Fresh setup with ShellSpec and ZFS" +``` + +Revert to snapshot: + +```bash +sudo virsh snapshot-revert zelta-ci clean-baseline +``` + +Start / stop / check status: + +```bash +sudo virsh start zelta-ci +sudo virsh shutdown zelta-ci +sudo virsh dominfo zelta-ci +``` + +Delete the VM entirely: + +```bash +sudo virsh destroy zelta-ci +sudo virsh undefine zelta-ci --remove-all-storage +``` diff --git a/test/runners/vm/vm-setup.sh b/test/runners/vm/vm-setup.sh new file mode 100644 index 0000000..98dc01e --- /dev/null +++ b/test/runners/vm/vm-setup.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Local CI VM setup script for zelta ShellSpec tests +# Run as root on a fresh Ubuntu 24.04 VM + +set -euo pipefail + +echo "==> Installing system packages" +apt-get update && apt-get install -y \ + zfsutils-linux \ + sudo \ + curl \ + git \ + man-db \ + openssh-server + +echo "==> Enabling SSH" +sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config +sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config +# Remove cloud-init drop-in that may override password auth +rm -f /etc/ssh/sshd_config.d/60-cloudimg-settings.conf +systemctl enable --now ssh + +echo "==> Installing ShellSpec" +if command -v shellspec &>/dev/null; then + echo " ShellSpec already installed, skipping" +else + curl -fsSL https://git.io/shellspec | sh -s -- --yes --prefix /usr/local +fi + +echo "==> Creating test user" +if id testuser &>/dev/null; then + echo " testuser already exists, skipping" +else + useradd -m -s /bin/bash testuser +fi + +echo "==> Configuring sudoers" +echo 'testuser ALL=(ALL) NOPASSWD: /usr/bin/dd *, /usr/bin/rm -f /tmp/*, /usr/bin/truncate *, /usr/sbin/zpool *, /usr/sbin/zfs *' \ + > /etc/sudoers.d/testuser + +echo "==> Creating test runner script" +cat > /home/testuser/run_test.sh << 'EOF' +#!/bin/bash +cd ~/zelta +export SANDBOX_ZELTA_SRC_POOL=apool +export SANDBOX_ZELTA_TGT_POOL=bpool +export SANDBOX_ZELTA_SRC_DS=apool/treetop +export SANDBOX_ZELTA_TGT_DS=bpool/backups +shellspec +EOF +chmod +x /home/testuser/run_test.sh +chown testuser:testuser /home/testuser/run_test.sh + +echo "" +echo "==> Setup complete!" +echo "" +echo "Next steps:" +echo " 1. Clone or copy the zelta repo to /home/testuser/zelta" +echo " 2. chown -R testuser:testuser /home/testuser/zelta" +echo " 3. Run tests: su - testuser -c ~testuser/run_test.sh" From 3daffb30b0415d939275562a66b0f21b899d56bb Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sat, 14 Mar 2026 18:48:37 -0400 Subject: [PATCH 14/35] run shellspec install with sudo --- .github/workflows/shellspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/shellspec.yml b/.github/workflows/shellspec.yml index c584fd3..8af360c 100644 --- a/.github/workflows/shellspec.yml +++ b/.github/workflows/shellspec.yml @@ -20,7 +20,7 @@ jobs: sudo apt-get install -y zfsutils-linux man-db - name: Install ShellSpec - run: curl -fsSL https://git.io/shellspec | sh -s -- --yes --prefix /usr/local + run: curl -fsSL https://git.io/shellspec | sudo sh -s -- --yes --prefix /usr/local - name: Create test user run: | From a3430b592560104b1072009a2e8df02dcf2a8b8d Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 00:07:10 -0400 Subject: [PATCH 15/35] add ShellSpec Tests status badge before what's new --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index efcd9b5..157072f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,12 @@ ![Zelta Logo](https://zelta.space/index/zelta-banner.svg) - # The Zelta Backup and Recovery Suite *Version 1.1, 2026-01-20* --- - +![ShellSpec Tests](https://github.com/bell-tower/zelta/actions/workflows/shellspec.yml/badge.svg) > - **What's New:** Check [CHANGELOG.md](CHANGELOG.md) for the latest changes > - **Found a Bug?** Please [open an issue](https://github.com/bellhyve/zelta/issues) > - **Previous Release:** [March 2024, Zelta v1.0](https://github.com/bellhyve/zelta/tree/release/1.0) - --- [zelta.space](https://zelta.space) | [Documentation](https://zelta.space/en/home) | [GitHub](https://github.com/bellhyve/zelta) From 4198bc5c9d6163ba1122eb0384f971acb349c1ad Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 00:12:07 -0400 Subject: [PATCH 16/35] reposition the ShellSpec Tests badge --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 157072f..c211102 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,11 @@ *Version 1.1, 2026-01-20* --- -![ShellSpec Tests](https://github.com/bell-tower/zelta/actions/workflows/shellspec.yml/badge.svg) > - **What's New:** Check [CHANGELOG.md](CHANGELOG.md) for the latest changes > - **Found a Bug?** Please [open an issue](https://github.com/bellhyve/zelta/issues) > - **Previous Release:** [March 2024, Zelta v1.0](https://github.com/bellhyve/zelta/tree/release/1.0) +> +>      ![ShellSpec Tests](https://github.com/bell-tower/zelta/actions/workflows/shellspec.yml/badge.svg) --- [zelta.space](https://zelta.space) | [Documentation](https://zelta.space/en/home) | [GitHub](https://github.com/bellhyve/zelta) From b93b7bd8d320cc64d6905cb3aba67eb16504fae9 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 03:18:57 -0400 Subject: [PATCH 17/35] regenerated test with matcher func fix --- test/040_zelta_tests_spec.sh | 22 +++++++++------------- test/050_zelta_revert_spec.sh | 22 +++++++++------------- test/060_zelta_clone_spec.sh | 7 +++---- test/070_zelta_prune_spec.sh | 12 +++++------- test/080_zelta_policy_spec.sh | 7 +++---- 5 files changed, 29 insertions(+), 41 deletions(-) diff --git a/test/040_zelta_tests_spec.sh b/test/040_zelta_tests_spec.sh index 9528644..983bd97 100644 --- a/test/040_zelta_tests_spec.sh +++ b/test/040_zelta_tests_spec.sh @@ -1,13 +1,12 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-13 13:27:43 -0400 +# Generated at: 2026-03-15 02:59:07 -0400 # Source: 040_zelta_tests_spec # WARNING: This file was automatically generated. Manual edits may be lost. output_for_match_after_divergence() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(printf '%s -' 11 total datasets compared | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "DS_SUFFIX MATCH SRC_LAST TGT_LAST INFO"|\ "[treetop] @start @start @start up-to-date"|\ @@ -26,7 +25,7 @@ output_for_match_after_divergence() { ;; *) printf "Unexpected line format : %s\n" "$line" >&2 - printf "Comparing to normalized: %s\n" "$normalized_line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac @@ -37,8 +36,7 @@ output_for_match_after_divergence() { output_for_rotate_after_divergence() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(printf '%s -' 36K sent, 10 streams received in 0.38 seconds | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "source is written; snapshotting: @zelta_"*""|\ "renaming '${SANDBOX_ZELTA_TGT_DS}' to '${SANDBOX_ZELTA_TGT_DS}_start'"|\ @@ -48,7 +46,7 @@ output_for_rotate_after_divergence() { ;; *) printf "Unexpected line format : %s\n" "$line" >&2 - printf "Comparing to normalized: %s\n" "$normalized_line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac @@ -59,8 +57,7 @@ output_for_rotate_after_divergence() { output_for_match_after_rotate() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(printf '%s -' 10 total datasets compared | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "DS_SUFFIX MATCH SRC_LAST TGT_LAST INFO"|\ "[treetop] @zelta_"*" @zelta_"*" @zelta_"*" up-to-date"|\ @@ -78,7 +75,7 @@ output_for_match_after_rotate() { ;; *) printf "Unexpected line format : %s\n" "$line" >&2 - printf "Comparing to normalized: %s\n" "$normalized_line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac @@ -89,8 +86,7 @@ output_for_match_after_rotate() { output_for_backup_after_rotate() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(printf '%s -' 1K sent, 3 streams received in 0.06 seconds | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "syncing 10 datasets"|\ "10 datasets up-to-date"|\ @@ -98,7 +94,7 @@ output_for_backup_after_rotate() { ;; *) printf "Unexpected line format : %s\n" "$line" >&2 - printf "Comparing to normalized: %s\n" "$normalized_line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac diff --git a/test/050_zelta_revert_spec.sh b/test/050_zelta_revert_spec.sh index 2e7f952..6d32785 100644 --- a/test/050_zelta_revert_spec.sh +++ b/test/050_zelta_revert_spec.sh @@ -1,19 +1,18 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-13 13:28:30 -0400 +# Generated at: 2026-03-15 02:59:54 -0400 # Source: 050_zelta_revert_spec # WARNING: This file was automatically generated. Manual edits may be lost. output_for_snapshot() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(printf '%s -' snapshot created 'apool/treetop@manual_test' | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "snapshot created '${SANDBOX_ZELTA_SRC_DS}@manual_test'") ;; *) printf "Unexpected line format : %s\n" "$line" >&2 - printf "Comparing to normalized: %s\n" "$normalized_line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac @@ -24,8 +23,7 @@ output_for_snapshot() { output_for_backup_after_delta() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(printf '%s -' 37K sent, 22 streams received in 0.47 seconds | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "source is written; snapshotting: @zelta_"*""|\ "syncing 12 datasets"|\ @@ -33,7 +31,7 @@ output_for_backup_after_delta() { ;; *) printf "Unexpected line format : %s\n" "$line" >&2 - printf "Comparing to normalized: %s\n" "$normalized_line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac @@ -44,14 +42,13 @@ output_for_backup_after_delta() { output_for_snapshot_again() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(printf '%s -' snapshot created 'apool/treetop@another_test' | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "snapshot created '${SANDBOX_ZELTA_SRC_DS}@another_test'") ;; *) printf "Unexpected line format : %s\n" "$line" >&2 - printf "Comparing to normalized: %s\n" "$normalized_line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac @@ -62,8 +59,7 @@ output_for_snapshot_again() { output_for_rotate_after_revert() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(printf '%s -' 6K sent, 10 streams received in 0.38 seconds | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "renaming '${SANDBOX_ZELTA_TGT_DS}' to '${SANDBOX_ZELTA_TGT_DS}_zelta_"*"'"|\ "to ensure target is up-to-date, run: zelta backup ${SANDBOX_ZELTA_SRC_EP} ${SANDBOX_ZELTA_TGT_EP}"|\ @@ -73,7 +69,7 @@ output_for_rotate_after_revert() { ;; *) printf "Unexpected line format : %s\n" "$line" >&2 - printf "Comparing to normalized: %s\n" "$normalized_line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac diff --git a/test/060_zelta_clone_spec.sh b/test/060_zelta_clone_spec.sh index a8eaa86..fb661b2 100644 --- a/test/060_zelta_clone_spec.sh +++ b/test/060_zelta_clone_spec.sh @@ -1,13 +1,12 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-13 13:29:30 -0400 +# Generated at: 2026-03-15 03:00:54 -0400 # Source: 060_zelta_clone_spec # WARNING: This file was automatically generated. Manual edits may be lost. output_for_zfs_list_for_clone() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(printf '%s -' apool/treetop/copy_of_sub2/orphan apool/treetop/sub2/orphan@zelta_2026-03-13_17.29.21 | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "NAME ORIGIN"|\ "${SANDBOX_ZELTA_SRC_DS}/copy_of_sub2 ${SANDBOX_ZELTA_SRC_DS}/sub2@zelta_"*""|\ @@ -15,7 +14,7 @@ output_for_zfs_list_for_clone() { ;; *) printf "Unexpected line format : %s\n" "$line" >&2 - printf "Comparing to normalized: %s\n" "$normalized_line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac diff --git a/test/070_zelta_prune_spec.sh b/test/070_zelta_prune_spec.sh index 150c5b1..04d9564 100644 --- a/test/070_zelta_prune_spec.sh +++ b/test/070_zelta_prune_spec.sh @@ -1,13 +1,12 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-13 13:30:37 -0400 +# Generated at: 2026-03-15 03:01:59 -0400 # Source: 070_zelta_prune_spec # WARNING: This file was automatically generated. Manual edits may be lost. output_for_backup_with_snapshot() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(printf '%s -' 31K sent, 12 streams received in 0.24 seconds | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "snapshotting: @zelta_"*""|\ "syncing 12 datasets"|\ @@ -15,7 +14,7 @@ output_for_backup_with_snapshot() { ;; *) printf "Unexpected line format : %s\n" "$line" >&2 - printf "Comparing to normalized: %s\n" "$normalized_line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac @@ -26,8 +25,7 @@ output_for_backup_with_snapshot() { output_for_prune_check() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(printf '%s -' apool/treetop/sub4/zvol@zelta_2026-03-13_17.30.23 | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "${SANDBOX_ZELTA_SRC_DS}@zelta_"*""|\ "${SANDBOX_ZELTA_SRC_DS}/sub1@zelta_"*""|\ @@ -42,7 +40,7 @@ output_for_prune_check() { ;; *) printf "Unexpected line format : %s\n" "$line" >&2 - printf "Comparing to normalized: %s\n" "$normalized_line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac diff --git a/test/080_zelta_policy_spec.sh b/test/080_zelta_policy_spec.sh index bbfd32c..5db0fdc 100644 --- a/test/080_zelta_policy_spec.sh +++ b/test/080_zelta_policy_spec.sh @@ -1,19 +1,18 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-03-13 13:31:48 -0400 +# Generated at: 2026-03-15 03:03:10 -0400 # Source: 080_zelta_policy_spec # WARNING: This file was automatically generated. Manual edits may be lost. output_for_policy_check() { while IFS= read -r line; do # normalize whitespace, remove leading/trailing spaces - normalized=$(printf '%s -' [BACKUP_SITE: dever@uvm1:bpool/backups] dever@uvm1:apool/treetop: 12 datasets up-to-date | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case "$normalized" in "[BACKUP_SITE: ${SANDBOX_ZELTA_TGT_EP}] ${SANDBOX_ZELTA_SRC_EP}: 12 datasets up-to-date") ;; *) printf "Unexpected line format : %s\n" "$line" >&2 - printf "Comparing to normalized: %s\n" "$normalized_line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac From 411e6b436e9b3473637812bfc43b1c0f37e58b84 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 03:20:14 -0400 Subject: [PATCH 18/35] fix line evaluation in matcher function --- .../test_generation/scripts/awk/generate_case_stmt_func.awk | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk b/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk index ea2ffc3..a750a6d 100644 --- a/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk +++ b/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk @@ -21,8 +21,7 @@ END { print func_name "() {" print " while IFS= read -r line; do" print " # normalize whitespace, remove leading/trailing spaces" - print " normalized=$(printf '%s\n' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" - + print " normalized=\$(printf '%s' \"\$line\" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*\$//')" print " case \"$normalized\" in" line_continue = "\"|\\" @@ -36,7 +35,7 @@ END { print " ;;" print " *)" print " printf \"Unexpected line format : %s\\n\" \"$line\" >&2" - print " printf \"Comparing to normalized: %s\\n\" \"$normalized_line\" >&2" + print " printf \"Comparing to normalized: %s\\n\" \"$normalized\" >&2" print " return 1" print " ;;" print " esac" From d9c2566898d532e7d74ec0a6e942ae78f3999bc0 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 15:49:18 -0400 Subject: [PATCH 19/35] update bellhye to bell-tower in urls --- README.md | 14 +++++++------- contrib/install-from-git.sh | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c211102..d408d8b 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ --- > - **What's New:** Check [CHANGELOG.md](CHANGELOG.md) for the latest changes -> - **Found a Bug?** Please [open an issue](https://github.com/bellhyve/zelta/issues) -> - **Previous Release:** [March 2024, Zelta v1.0](https://github.com/bellhyve/zelta/tree/release/1.0) +> - **Found a Bug?** Please [open an issue](https://github.com/bell-tower/zelta/issues) +> - **Previous Release:** [March 2024, Zelta v1.0](https://github.com/bell-tower/zelta/tree/release/1.0) > >      ![ShellSpec Tests](https://github.com/bell-tower/zelta/actions/workflows/shellspec.yml/badge.svg) --- -[zelta.space](https://zelta.space) | [Documentation](https://zelta.space/en/home) | [GitHub](https://github.com/bellhyve/zelta) +[zelta.space](https://zelta.space) | [Documentation](https://zelta.space/en/home) | [GitHub](https://github.com/bell-tower/zelta) **Zelta** provides bulletproof backups that meet strict compliance requirements while remaining straightforward to deploy and operate. It transforms complex backup and recovery operations into safe, auditable commandsβ€”protecting your data without requiring specialized expertise. @@ -40,7 +40,7 @@ Written in portable Bourne shell and AWK, Zelta runs anywhere ZFS runs. No packa ### From Source (Recommended for Zelta 1.1) ```sh -git clone https://github.com/bellhyve/zelta.git +git clone https://github.com/bell-tower/zelta.git cd zelta sudo ./install.sh # The installer will guide you through setup. @@ -58,12 +58,12 @@ pkg install zelta The following command clones Zelta from the `main` branch and launches the installer. Run this as a personal or backup user for a local, non-root installation. If you prefer a system-wide installation, run the command as root or via `sudo/doas`. ```sh -curl -fsSL https://raw.githubusercontent.com/bellhyve/zelta/main/contrib/install-from-git.sh | sh +curl -fsSL https://raw.githubusercontent.com/bell-tower/zelta/main/contrib/install-from-git.sh | sh ``` The installer will detect your privileges and guide you through adding the necessary environment variables to your shell profile. -*Security Note: As with any script piped from the internet, we encourage you to [inspect the installer source](https://github.com/bellhyve/zelta/blob/main/contrib/install-from-git.sh) before execution.* +*Security Note: As with any script piped from the internet, we encourage you to [inspect the installer source](https://github.com/bell-tower/zelta/blob/main/contrib/install-from-git.sh) before execution.* --- @@ -186,7 +186,7 @@ We welcome contributors who are passionate about data protection and recovery. B ### Contact -We welcome questions, bug reports, and feature requests at [GitHub Issues](https://github.com/bellhyve/zelta/issues). +We welcome questions, bug reports, and feature requests at [GitHub Issues](https://github.com/bell-tower/zelta/issues). For other inquiries including business questions, you can reach the Zelta team at Bell Tower via our [contact form](https://belltower.it/contact/). diff --git a/contrib/install-from-git.sh b/contrib/install-from-git.sh index 1d9d8da..6d3a25d 100644 --- a/contrib/install-from-git.sh +++ b/contrib/install-from-git.sh @@ -2,7 +2,7 @@ # Zelta One-Shot Installer # Downloads latest Zelta from GitHub and runs install.sh # -# Usage: curl -fsSL https://raw.githubusercontent.com/bellhyve/zelta/main/contrib/install-from-git.sh | sh +# Usage: curl -fsSL https://raw.githubusercontent.com/bell-tower/zelta/main/contrib/install-from-git.sh | sh # Or specify branch: curl ... | sh -s -- --branch=develop set -e From 2793eff40e6b1864b45c8c444b1788fdfe16c4f8 Mon Sep 17 00:00:00 2001 From: rlogwood Date: Sun, 15 Mar 2026 15:53:21 -0400 Subject: [PATCH 20/35] fix typo fo -> of Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- test/runners/doc/README_AliasHelpers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runners/doc/README_AliasHelpers.md b/test/runners/doc/README_AliasHelpers.md index 130f512..5e379fe 100644 --- a/test/runners/doc/README_AliasHelpers.md +++ b/test/runners/doc/README_AliasHelpers.md @@ -49,7 +49,7 @@ alias zrenv="zcd && . $ZELTA_ENV/reset_env.sh" # setup pools, datasets and remotes env vars alias ztenv="zcd && . $ZELTA_ENV/test_env.sh" -# access zelta man pages, relies on prior setting fo ZELTA_DOC env var +# access zelta man pages, relies on prior setting of ZELTA_DOC env var # which is the location of the ZELTA man pages alias zman='man -M "$ZELTA_DOC"' ``` From f9a66da2de80dae5bc4acaed0f98ac98d82a777c Mon Sep 17 00:00:00 2001 From: rlogwood Date: Sun, 15 Mar 2026 15:57:20 -0400 Subject: [PATCH 21/35] better defaults for local runs, leave remotes commented out by default Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- test/runners/env/test_env.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/runners/env/test_env.sh b/test/runners/env/test_env.sh index fb84a41..d0e314b 100644 --- a/test/runners/env/test_env.sh +++ b/test/runners/env/test_env.sh @@ -16,8 +16,8 @@ unset SANDBOX_ZELTA_TGT_REMOTE # * leave these undefined if you're running locally # * the endpoints are defined automatically and are REMOTE + DS -#export SANDBOX_ZELTA_SRC_REMOTE=dever@uvm1 # Ubuntu source -#export SANDBOX_ZELTA_TGT_REMOTE=dever@uvm1 # Ubuntu remote - -export SANDBOX_ZELTA_SRC_REMOTE=dever@lusvr01 # FreeBSD source -export SANDBOX_ZELTA_TGT_REMOTE=dever@lusvr01 # FreeBSD remote +# Examples: uncomment and customize these if you want to run against remotes. +#export SANDBOX_ZELTA_SRC_REMOTE=user@example-host # e.g. Ubuntu source +#export SANDBOX_ZELTA_TGT_REMOTE=user@example-host # e.g. Ubuntu remote +#export SANDBOX_ZELTA_SRC_REMOTE=user@freebsd-host # e.g. FreeBSD source +#export SANDBOX_ZELTA_TGT_REMOTE=user@freebsd-host # e.g. FreeBSD remote From ef296449887210d93ae09305360ca629bfc94e90 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 16:01:13 -0400 Subject: [PATCH 22/35] clean up comments --- test/runners/env/test_env.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/runners/env/test_env.sh b/test/runners/env/test_env.sh index d0e314b..bbed31a 100644 --- a/test/runners/env/test_env.sh +++ b/test/runners/env/test_env.sh @@ -10,14 +10,14 @@ export SANDBOX_ZELTA_TGT_DS=bpool/backups # remotes setup -# unset the current remotes +# always unset the current remotes first, then override as desired below unset SANDBOX_ZELTA_SRC_REMOTE unset SANDBOX_ZELTA_TGT_REMOTE -# * leave these undefined if you're running locally -# * the endpoints are defined automatically and are REMOTE + DS +# * leave these undefined if you're running locally +# * the endpoints are defined automatically and are REMOTE + DS # Examples: uncomment and customize these if you want to run against remotes. #export SANDBOX_ZELTA_SRC_REMOTE=user@example-host # e.g. Ubuntu source #export SANDBOX_ZELTA_TGT_REMOTE=user@example-host # e.g. Ubuntu remote -#export SANDBOX_ZELTA_SRC_REMOTE=user@freebsd-host # e.g. FreeBSD source -#export SANDBOX_ZELTA_TGT_REMOTE=user@freebsd-host # e.g. FreeBSD remote +#export SANDBOX_ZELTA_SRC_REMOTE=user@freebsd-host # e.g. FreeBSD source +#export SANDBOX_ZELTA_TGT_REMOTE=user@freebsd-host # e.g. FreeBSD remote From 2c4f3370a1ab4a87e16ef852a9de2a36d48b1cb1 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 16:22:19 -0400 Subject: [PATCH 23/35] do not add empty env vars to the map --- test/runners/test_generation/lib/ruby/env_substitutor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runners/test_generation/lib/ruby/env_substitutor.rb b/test/runners/test_generation/lib/ruby/env_substitutor.rb index f306795..986a8e9 100644 --- a/test/runners/test_generation/lib/ruby/env_substitutor.rb +++ b/test/runners/test_generation/lib/ruby/env_substitutor.rb @@ -31,7 +31,7 @@ def substitute(line) def build_sorted_env_map(env_var_names) # Parse and sort env vars by value length (descending) env_map = env_var_names.split(':').each_with_object({}) do |name, hash| - hash[name] = ENV[name] if ENV[name] + hash[name] = ENV[name] if ENV[name]&.length&.positive? end # Sort by value length descending to replace longest matches first From 521da0f1f98f4bb08d376d64f02ba8688b9d2579 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 16:26:44 -0400 Subject: [PATCH 24/35] remove debug info --- test/runners/test_generation/lib/ruby/test_generator.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/runners/test_generation/lib/ruby/test_generator.rb b/test/runners/test_generation/lib/ruby/test_generator.rb index 30b8f54..f43dfc6 100755 --- a/test/runners/test_generation/lib/ruby/test_generator.rb +++ b/test/runners/test_generation/lib/ruby/test_generator.rb @@ -248,12 +248,9 @@ def clean_up_output_line(line) end def normalize_output_line(line) - result = @env_substitutor.substitute( + @env_substitutor.substitute( clean_up_output_line(line) ) - puts("line subs; before: #{line}") - puts("line subs; before: #{result}") - result end def read_stderr_file(stderr_file) From ceeabc79e78c9fe5ae41807569458fc80d3e9556 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 16:31:47 -0400 Subject: [PATCH 25/35] run under bash shell --- test/runners/test_generation/lib/orchestration/generate_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runners/test_generation/lib/orchestration/generate_test.sh b/test/runners/test_generation/lib/orchestration/generate_test.sh index 36ef186..7112105 100755 --- a/test/runners/test_generation/lib/orchestration/generate_test.sh +++ b/test/runners/test_generation/lib/orchestration/generate_test.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/env bash # Check for required arguments if [ $# -lt 2 ]; then From 3868d437e5472b600d09f459480b69f89ad7b83a Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 16:48:01 -0400 Subject: [PATCH 26/35] fix !# path for env --- test/runners/test_generation/lib/orchestration/generate_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runners/test_generation/lib/orchestration/generate_test.sh b/test/runners/test_generation/lib/orchestration/generate_test.sh index 7112105..9c3072a 100755 --- a/test/runners/test_generation/lib/orchestration/generate_test.sh +++ b/test/runners/test_generation/lib/orchestration/generate_test.sh @@ -1,4 +1,4 @@ -#!/bin/env bash +#!/usr/bin/env bash # Check for required arguments if [ $# -lt 2 ]; then From 50b234c2273556588450a8e5377b926e2fc3a6ed Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 16:53:54 -0400 Subject: [PATCH 27/35] make testuser sudoer file 0440 --- test/runners/vm/vm-setup.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/runners/vm/vm-setup.sh b/test/runners/vm/vm-setup.sh index 98dc01e..7256e2e 100644 --- a/test/runners/vm/vm-setup.sh +++ b/test/runners/vm/vm-setup.sh @@ -38,6 +38,8 @@ echo "==> Configuring sudoers" echo 'testuser ALL=(ALL) NOPASSWD: /usr/bin/dd *, /usr/bin/rm -f /tmp/*, /usr/bin/truncate *, /usr/sbin/zpool *, /usr/sbin/zfs *' \ > /etc/sudoers.d/testuser +chmod 0440 /etc/sudoers.d/testuser + echo "==> Creating test runner script" cat > /home/testuser/run_test.sh << 'EOF' #!/bin/bash From 17a87c69987b29d77e6c5b4490599e19ed05a67e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:05:07 -0400 Subject: [PATCH 28/35] Replace real user/host identifiers with fake placeholders in zelta_test_policy.conf (#65) * Initial plan * Replace real user/host identifiers with fake placeholders in zelta_test_policy.conf Co-authored-by: rlogwood <3377419+rlogwood@users.noreply.github.com> * Update zelta_test_policy.conf generation notes --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rlogwood <3377419+rlogwood@users.noreply.github.com> Co-authored-by: rlogwood --- .../test_generation/config/zelta_test_policy.conf | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/runners/test_generation/config/zelta_test_policy.conf b/test/runners/test_generation/config/zelta_test_policy.conf index 6243eae..c6da12e 100644 --- a/test/runners/test_generation/config/zelta_test_policy.conf +++ b/test/runners/test_generation/config/zelta_test_policy.conf @@ -1,6 +1,7 @@ -# shellspec auto generated test zelta policy file at: (2026-03-14_02.33.22) -# NOTE: any modification will be lost +# shellspec auto generated test zelta policy file at: (0000-00-00_00.00.00) +# NOTE: this file is dynamically generated during shellspec runs, do not modify. +# NOTE: shellspec will create a local version of this file from your environment. BACKUP_SITE: - dever@lusvr01: + user@source-host: datasets: - - apool/treetop: dever@uvm1:bpool/backups + - srcpool/data: user@backup-host:dstpool/backups From 75e2cecfecf5552f46c97781c1a9f7d1501847ee Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 18:06:12 -0400 Subject: [PATCH 29/35] fix argument evaluation to allow optional arg passing --- .../lib/orchestration/setup_tree.sh | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/test/runners/test_generation/lib/orchestration/setup_tree.sh b/test/runners/test_generation/lib/orchestration/setup_tree.sh index b616e27..cbec50f 100644 --- a/test/runners/test_generation/lib/orchestration/setup_tree.sh +++ b/test/runners/test_generation/lib/orchestration/setup_tree.sh @@ -32,22 +32,34 @@ setup_tree() { return 1 fi + # Split trace_options into array (if not empty) + trace_opts=() + if [ -n "$trace_options" ]; then + read -ra trace_opts <<< "$trace_options" + fi + + # Split selector_specs into array (if not empty) + selector_opts=() + if [ -n "$selector_specs" ]; then + read -ra selector_opts <<< "$selector_specs" + fi + cmd1=() if [ -n "$pattern_specs" ]; then cmd1=(shellspec) - if [ -n "$trace_options" ]; then - cmd1+=("$trace_options") + if [ ${#trace_opts[@]} -gt 0 ]; then + cmd1+=("${trace_opts[@]}") fi cmd1+=(--pattern "$pattern_specs") fi cmd2=() - if [ -n "$selector_specs" ]; then + if [ ${#selector_opts[@]} -gt 0 ]; then cmd2=(shellspec) - if [ -n "$trace_options" ]; then - cmd2+=("$trace_options") + if [ ${#trace_opts[@]} -gt 0 ]; then + cmd2+=("${trace_opts[@]}") fi - cmd2+=("$selector_specs") + cmd2+=("${selector_opts[@]}") fi set -x @@ -65,5 +77,4 @@ setup_tree() { set +x printf "\n βœ… setup succeeded\n" - } From 9f5d244e79d07273818ac3fd9a4dcedbf31a3083 Mon Sep 17 00:00:00 2001 From: rlogwood Date: Sun, 15 Mar 2026 18:13:58 -0400 Subject: [PATCH 30/35] update ownership and perm explicitly on testuser sudoers and verify. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/shellspec.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/shellspec.yml b/.github/workflows/shellspec.yml index 8af360c..99bac70 100644 --- a/.github/workflows/shellspec.yml +++ b/.github/workflows/shellspec.yml @@ -27,6 +27,9 @@ jobs: sudo useradd -m -s /bin/bash testuser echo 'testuser ALL=(ALL) NOPASSWD: /usr/bin/dd *, /usr/bin/rm -f /tmp/*, /usr/bin/truncate *, /usr/sbin/zpool *, /usr/sbin/zfs *' \ | sudo tee /etc/sudoers.d/testuser + sudo chown root:root /etc/sudoers.d/testuser + sudo chmod 0440 /etc/sudoers.d/testuser + sudo visudo -cf /etc/sudoers.d/testuser - name: Copy repo for test user run: | From a378ae2e9a0e324973a44c729f9c6977aad61676 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 20:33:36 -0400 Subject: [PATCH 31/35] git ignore shellspec generated policy file, rename current file to .example for documentation --- .gitignore | 1 + ...zelta_test_policy.conf => zelta_test_policy.conf.example} | 5 +++++ 2 files changed, 6 insertions(+) rename test/runners/test_generation/config/{zelta_test_policy.conf => zelta_test_policy.conf.example} (59%) diff --git a/.gitignore b/.gitignore index 83d79af..5aede50 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ !.gitignore !.shellspec !.github/ +test/runners/test_generation/config/zelta_test_policy.conf *.swp *.swo doc/man? diff --git a/test/runners/test_generation/config/zelta_test_policy.conf b/test/runners/test_generation/config/zelta_test_policy.conf.example similarity index 59% rename from test/runners/test_generation/config/zelta_test_policy.conf rename to test/runners/test_generation/config/zelta_test_policy.conf.example index c6da12e..bc1b810 100644 --- a/test/runners/test_generation/config/zelta_test_policy.conf +++ b/test/runners/test_generation/config/zelta_test_policy.conf.example @@ -1,3 +1,8 @@ +# This is an example file to show the format of the automatically generated +# shellspec test policy file. The generated file will be +# written to test/runners/test_generation/config/zelta_test_policy.conf +# and is ignored by git. + # shellspec auto generated test zelta policy file at: (0000-00-00_00.00.00) # NOTE: this file is dynamically generated during shellspec runs, do not modify. # NOTE: shellspec will create a local version of this file from your environment. From 4c59980b710493933910ca5e9e0db82c1785f805 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 20:43:19 -0400 Subject: [PATCH 32/35] chown, chmod, verify sudoers file --- test/runners/vm/vm-setup.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/runners/vm/vm-setup.sh b/test/runners/vm/vm-setup.sh index 7256e2e..2a430d5 100644 --- a/test/runners/vm/vm-setup.sh +++ b/test/runners/vm/vm-setup.sh @@ -38,7 +38,9 @@ echo "==> Configuring sudoers" echo 'testuser ALL=(ALL) NOPASSWD: /usr/bin/dd *, /usr/bin/rm -f /tmp/*, /usr/bin/truncate *, /usr/sbin/zpool *, /usr/sbin/zfs *' \ > /etc/sudoers.d/testuser +chown root:root /etc/sudoers.d/testuser chmod 0440 /etc/sudoers.d/testuser +visudo -cf /etc/sudoers.d/testuser echo "==> Creating test runner script" cat > /home/testuser/run_test.sh << 'EOF' From 82e475330627f01d7dfc7f5fdf1b8f68c97b3926 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 21:05:09 -0400 Subject: [PATCH 33/35] improve setup and test instructions --- test/runners/vm/vm-setup.sh | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test/runners/vm/vm-setup.sh b/test/runners/vm/vm-setup.sh index 2a430d5..e70c4a2 100644 --- a/test/runners/vm/vm-setup.sh +++ b/test/runners/vm/vm-setup.sh @@ -52,13 +52,20 @@ export SANDBOX_ZELTA_SRC_DS=apool/treetop export SANDBOX_ZELTA_TGT_DS=bpool/backups shellspec EOF + chmod +x /home/testuser/run_test.sh chown testuser:testuser /home/testuser/run_test.sh +echo "created test runner: /home/testuser/run_test.sh" + +git clone https://github.com/bell-tower/zelta.git /home/testuser/zelta +chown -R testuser:testuser /home/testuser/zelta +echo "cloned zelta to: /home/testuser/zelta" echo "" echo "==> Setup complete!" echo "" -echo "Next steps:" -echo " 1. Clone or copy the zelta repo to /home/testuser/zelta" -echo " 2. chown -R testuser:testuser /home/testuser/zelta" -echo " 3. Run tests: su - testuser -c ~testuser/run_test.sh" +echo "To run tests:" +echo " su - testuser" +echo " cd zelta" +echo " git checkout (branchname)" +echo " ~/run_test.sh" From 89bc1681f6c3d11e03082a2d66c0f7228b757c51 Mon Sep 17 00:00:00 2001 From: rlogwood Date: Sun, 15 Mar 2026 21:21:50 -0400 Subject: [PATCH 34/35] ensure no dependency on ~ evaluation Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/shellspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/shellspec.yml b/.github/workflows/shellspec.yml index 99bac70..0c91e40 100644 --- a/.github/workflows/shellspec.yml +++ b/.github/workflows/shellspec.yml @@ -43,4 +43,4 @@ jobs: SANDBOX_ZELTA_TGT_POOL=bpool \ SANDBOX_ZELTA_SRC_DS=apool/treetop \ SANDBOX_ZELTA_TGT_DS=bpool/backups \ - bash -c 'cd ~/zelta && shellspec' + bash -c 'cd /home/testuser/zelta && shellspec' From 5a0977683456699d87dd3d54978f00efb27dcb18 Mon Sep 17 00:00:00 2001 From: Richard Logwood Date: Sun, 15 Mar 2026 22:06:15 -0400 Subject: [PATCH 35/35] avoid awk implementation specific treatment of \$ escapes --- .../scripts/awk/generate_case_stmt_func.awk | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk b/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk index a750a6d..2791479 100644 --- a/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk +++ b/test/runners/test_generation/scripts/awk/generate_case_stmt_func.awk @@ -21,7 +21,12 @@ END { print func_name "() {" print " while IFS= read -r line; do" print " # normalize whitespace, remove leading/trailing spaces" - print " normalized=\$(printf '%s' \"\$line\" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*\$//')" + + # Use printf with %s format specifier to avoid \$ escapes + dollar = "$" + printf " normalized=%s(printf '%%s' \"%sline\" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*%s//')\n", dollar, dollar, dollar + + print " # check line against expected output" print " case \"$normalized\" in" line_continue = "\"|\\"