diff --git a/.github/workflows/shellspec.yml b/.github/workflows/shellspec.yml new file mode 100644 index 0000000..0c91e40 --- /dev/null +++ b/.github/workflows/shellspec.yml @@ -0,0 +1,46 @@ +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 | sudo 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 + 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: | + 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 /home/testuser/zelta && shellspec' diff --git a/.gitignore b/.gitignore index db73fa3..5aede50 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ +.* !.gitignore !.shellspec +!.github/ +test/runners/test_generation/config/zelta_test_policy.conf *.swp *.swo -.* doc/man? tmp hide.* @@ -14,3 +16,4 @@ A*d retired/ logs log +ci/* \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b70ca5c..2cab86f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to Zelta will be documented in this file. - **Commands**: `zelta revert` for in-place rollbacks via rename and clone. - **Commands**: `zelta rotate` for divergent version handling, evolved from original `--rotate` flag. - **Commands**: (Experimental) `zelta prune` identifies snapshots in `zfs destroy` range syntax based on replication state and a sliding window for exclusions. +- **Installer**: (Experimental) Added a one-liner/pipe to shell installer option. - **Uninstaller**: Added `uninstall.sh` for clean removal of Zelta installations, including legacy paths from earlier betas. - **Core**: `zelta-args.awk` added as a separate data-driven argument preprocessor. - **Core**: `zelta-common.awk` library for centralized string and logging functions. @@ -19,6 +20,7 @@ All notable changes to Zelta will be documented in this file. - **Docs**: `zelta.env` expanded with comprehensive inline documentation and examples for all major configuration categories. - **Docs**: New man pages: `zelta-options(7)`, `zelta-revert(8)`, `zelta-rotate(8)`, `zelta-prune(8)`. - **Docs**: Added tool to sync man pages with the zelta.space wiki. +- **Testing**: Added new advanced Shellspec-based testing suite. ### Changed - **Architecture**: Refactored all core scripts for maintainability and simpler logic. diff --git a/README.md b/README.md index a986cd2..d408d8b 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ ![Zelta Logo](https://zelta.space/index/zelta-banner.svg) - # The Zelta Backup and Recovery Suite *Version 1.1, 2026-01-20* --- - > - **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. @@ -41,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. @@ -55,11 +54,16 @@ pkg install zelta ``` ### Experimental: One-Shot Install + +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 | sudo sh +curl -fsSL https://raw.githubusercontent.com/bell-tower/zelta/main/contrib/install-from-git.sh | sh ``` -**Note:** This is experimental. For production use, we recommend cloning the repository and reviewing `install.sh` before running it. +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/bell-tower/zelta/blob/main/contrib/install-from-git.sh) before execution.* --- @@ -182,17 +186,17 @@ 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/). ### Conference Talks -**BSDCan 2024: Zelta: A Safe and Powerful Approach to ZFS Replication** -By Daniel J. Bell +**BSDCan 2024: Zelta: A Safe and Powerful Approach to ZFS Replication** +By Daniel J. Bell [Watch on YouTube](https://www.youtube.com/watch?v=_nmgQTs8wgE) -**OpenZFS Summit 2025: Responsible Replication with Zelta** +**OpenZFS Summit 2025: Responsible Replication with Zelta** [Watch on YouTube](https://www.youtube.com/watch?v=G3weooQqcXw) ### Bell Tower Services 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 diff --git a/doc/zelta-backup.md b/doc/zelta-backup.md index 86dc06c..754cfd6 100644 --- a/doc/zelta-backup.md +++ b/doc/zelta-backup.md @@ -73,7 +73,7 @@ _target_ : Increase verbosity. Specify once for operational detail, twice (`-vv`) for debug output. **-q, \--quiet** -: Quiet output. Specify once to suppress warnings, twice (`-qq`) to suppress errors. +: Decrease log level. Specify once to suppress notices, twice (`-qq`) to suppress warnings. **-j, \--json** : Output results in JSON format. See **zelta-options(7)** for details. diff --git a/doc/zelta-clone.md b/doc/zelta-clone.md index 85c6bc6..3663495 100644 --- a/doc/zelta-clone.md +++ b/doc/zelta-clone.md @@ -36,7 +36,7 @@ _target_ : Increase verbosity. Specify once for operational detail, twice (`-vv`) for debug output. **-q, \--quiet** -: Quiet output. Specify once to suppress warnings, twice (`-qq`) to suppress errors. +: Decrease log level. Specify once to suppress notices, twice (`-qq`) to suppress warnings. **-n, \--dryrun, \--dry-run** : Display `zfs` commands without executing them. diff --git a/doc/zelta-match.md b/doc/zelta-match.md index 77766d6..6542bd5 100644 --- a/doc/zelta-match.md +++ b/doc/zelta-match.md @@ -18,7 +18,7 @@ : Increase verbosity. Specify once for operational detail and twice (-vv) for debug output. **-q, \--quiet** -: Quiet output. Specify once to suppress warnings and twice (-qq) to suppress errors. +: Decrease log level. Specify once to suppress notices, twice (-qq) to suppress warnings. **\--log-level** : Specify a log level value 0-4: errors (0), warnings (1), notices (2, default), info (3, verbose), and debug (4). diff --git a/doc/zelta-prune.md b/doc/zelta-prune.md index d85baf1..9898b6a 100644 --- a/doc/zelta-prune.md +++ b/doc/zelta-prune.md @@ -46,7 +46,7 @@ _target_ : Increase verbosity. Specify once for operational detail, twice (`-vv`) for debug output. **-q, \--quiet** -: Quiet output. Specify once to suppress warnings, twice (`-qq`) to suppress errors. +: Decrease log level. Specify once to suppress notices, twice (`-qq`) to suppress warnings. **-n, \--dryrun, \--dry-run** : Display `zfs` commands without executing them. diff --git a/doc/zelta-revert.md b/doc/zelta-revert.md index 00a614f..0743184 100644 --- a/doc/zelta-revert.md +++ b/doc/zelta-revert.md @@ -45,7 +45,7 @@ _endpoint_ : Increase verbosity. Specify once for operational detail, twice (`-vv`) for debug output. **-q, \--quiet** -: Quiet output. Specify once to suppress warnings, twice (`-qq`) to suppress errors. +: Decrease log level. Specify once to suppress notices, twice (`-qq`) to suppress warnings. **-n, \--dryrun, \--dry-run** : Display `zfs` commands without executing them. diff --git a/doc/zelta-rotate.md b/doc/zelta-rotate.md index e27607b..fb0b828 100644 --- a/doc/zelta-rotate.md +++ b/doc/zelta-rotate.md @@ -56,7 +56,7 @@ _target_ : Increase verbosity. Specify once for operational detail, twice (`-vv`) for debug output. **-q, \--quiet** -: Quiet output. Specify once to suppress warnings, twice (`-qq`) to suppress errors. +: Decrease log level. Specify once to suppress notices, twice (`-qq`) to suppress warnings. **-n, \--dryrun, \--dry-run** : Display `zfs` commands without executing them. diff --git a/doc/zelta-snapshot.md b/doc/zelta-snapshot.md index 1d6b24f..1537ee0 100644 --- a/doc/zelta-snapshot.md +++ b/doc/zelta-snapshot.md @@ -36,7 +36,7 @@ _endpoint_ : Increase verbosity. Specify once for operational detail, twice (`-vv`) for debug output. **-q, \--quiet** -: Quiet output. Specify once to suppress warnings, twice (`-qq`) to suppress errors. +: Decrease log level. Specify once to suppress notices, twice (`-qq`) to suppress warnings. **-n, \--dryrun, \--dry-run** : Display `zfs` commands without executing them. diff --git a/test/040_zelta_tests_spec.sh b/test/040_zelta_tests_spec.sh index 930ef66..983bd97 100644 --- a/test/040_zelta_tests_spec.sh +++ b/test/040_zelta_tests_spec.sh @@ -1,12 +1,12 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-02-12 13:28:34 -0500 +# 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=$(echo "$line" | 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"|\ @@ -24,7 +24,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" >&2 return 1 ;; esac @@ -35,16 +36,17 @@ 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' "$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'"|\ "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 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac @@ -55,7 +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=$(echo "$line" | 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"|\ @@ -72,7 +74,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" >&2 return 1 ;; esac @@ -83,14 +86,15 @@ 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' "$line" | 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") + ""*" 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" >&2 return 1 ;; esac diff --git a/test/050_zelta_revert_spec.sh b/test/050_zelta_revert_spec.sh index 99d0fac..6d32785 100644 --- a/test/050_zelta_revert_spec.sh +++ b/test/050_zelta_revert_spec.sh @@ -1,17 +1,18 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-02-12 13:29:24 -0500 +# 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=$(echo "$line" | 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 "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac @@ -22,14 +23,15 @@ 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' "$line" | 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") + ""*" 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" >&2 return 1 ;; esac @@ -40,31 +42,13 @@ 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' "$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 - return 1 - ;; - esac - done - 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 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac @@ -75,16 +59,17 @@ output_for_revert() { 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' "$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}"|\ "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 + printf "Unexpected line format : %s\n" "$line" >&2 + printf "Comparing to normalized: %s\n" "$normalized" >&2 return 1 ;; esac @@ -118,20 +103,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..fb661b2 100644 --- a/test/060_zelta_clone_spec.sh +++ b/test/060_zelta_clone_spec.sh @@ -1,35 +1,20 @@ # Auto-generated ShellSpec test file -# Generated at: 2026-02-12 13:30:29 -0500 +# 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_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 - normalized=$(echo "$line" | 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_"*""|\ "${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" >&2 return 1 ;; esac @@ -40,11 +25,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..04d9564 --- /dev/null +++ b/test/070_zelta_prune_spec.sh @@ -0,0 +1,67 @@ +# Auto-generated ShellSpec test file +# 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' "$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 + printf "Comparing to normalized: %s\n" "$normalized" >&2 + return 1 + ;; + esac + done + return 0 +} + +output_for_prune_check() { + while IFS= read -r line; do + # normalize whitespace, remove leading/trailing spaces + 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_"*""|\ + "${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 + printf "Comparing to normalized: %s\n" "$normalized" >&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..5db0fdc --- /dev/null +++ b/test/080_zelta_policy_spec.sh @@ -0,0 +1,38 @@ +# Auto-generated ShellSpec test file +# 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' "$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" >&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" + + 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 + The output should satisfy output_for_policy_check + The status should be success + End + +End 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 diff --git a/test/runners/doc/README_AliasHelpers.md b/test/runners/doc/README_AliasHelpers.md index 0421576..5e379fe 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 of ZELTA_DOC env var +# which is the location of the ZELTA man pages +alias zman='man -M "$ZELTA_DOC"' ``` 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..bbed31a 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 -# * 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 + +# 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 +# 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 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/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_40_divergent_test.sh b/test/runners/test_generation/bin/generate_40_divergent_test.sh new file mode 100755 index 0000000..0de67d5 --- /dev/null +++ b/test/runners/test_generation/bin/generate_40_divergent_test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +TEST_DEF=040_zelta_tests.yml +SETUP_SPECS="test/01*_spec.sh|test/01*_spec.sh|test/02*_spec.sh" + +./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 new file mode 100755 index 0000000..a104238 --- /dev/null +++ b/test/runners/test_generation/bin/generate_50_revert_test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +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" + +./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 new file mode 100755 index 0000000..ba30dea --- /dev/null +++ b/test/runners/test_generation/bin/generate_60_clone_test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +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" + +./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 new file mode 100755 index 0000000..ccf4baf --- /dev/null +++ b/test/runners/test_generation/bin/generate_70_prune_test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +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" + +./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 new file mode 100755 index 0000000..725b69a --- /dev/null +++ b/test/runners/test_generation/bin/generate_80_policy_test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +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" + +./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 new file mode 100755 index 0000000..eb412eb --- /dev/null +++ b/test/runners/test_generation/bin/generate_all_tests.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# 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" +"$SCRIPT_DIR/generate_70_prune_test.sh" +"$SCRIPT_DIR/generate_80_policy_test.sh" diff --git a/test/runners/test_generation/bin/generate_new_tests.sh b/test/runners/test_generation/bin/generate_new_tests.sh deleted file mode 100755 index 31a0efd..0000000 --- a/test/runners/test_generation/bin/generate_new_tests.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh - -# Get the directory where this script is located -SCRIPT_DIR="$(cd "$(dirname "$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/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 - - -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 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/generate_zelta_policy_config.sh b/test/runners/test_generation/bin/generate_zelta_policy_config.sh new file mode 100755 index 0000000..1c60cc8 --- /dev/null +++ b/test/runners/test_generation/bin/generate_zelta_policy_config.sh @@ -0,0 +1,39 @@ +#!/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" + + +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" + +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: + ${src_host}: + datasets: + - ${SANDBOX_ZELTA_SRC_DS}: ${SANDBOX_ZELTA_TGT_EP} +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..04ed7b9 --- /dev/null +++ b/test/runners/test_generation/bin/setup_debug_state.bash @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +# *** 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 + 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" + +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" "$@" + 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 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 new file mode 100644 index 0000000..c63a5c1 --- /dev/null +++ b/test/runners/test_generation/config/test_defs/070_zelta_prune_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" 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..5ad2eff --- /dev/null +++ b/test/runners/test_generation/config/test_defs/080_zelta_policy_test.yml @@ -0,0 +1,17 @@ +output_dir: tmp +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" + +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 + 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.example b/test/runners/test_generation/config/zelta_test_policy.conf.example new file mode 100644 index 0000000..bc1b810 --- /dev/null +++ b/test/runners/test_generation/config/zelta_test_policy.conf.example @@ -0,0 +1,12 @@ +# 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. +BACKUP_SITE: + user@source-host: + datasets: + - srcpool/data: user@backup-host:dstpool/backups diff --git a/test/runners/test_generation/lib/orchestration/generate_test.sh b/test/runners/test_generation/lib/orchestration/generate_test.sh index 36ef186..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/sh +#!/usr/bin/env bash # Check for required arguments if [ $# -lt 2 ]; then diff --git a/test/runners/test_generation/lib/orchestration/setup_tree.sh b/test/runners/test_generation/lib/orchestration/setup_tree.sh index cb1dbce..cbec50f 100644 --- a/test/runners/test_generation/lib/orchestration/setup_tree.sh +++ b/test/runners/test_generation/lib/orchestration/setup_tree.sh @@ -6,19 +6,75 @@ REPO_ROOT=${REPO_ROOT:=$(git rev-parse --show-toplevel)} echo "REPO ROOT: $REPO_ROOT" setup_tree() { - setup_specs=$1 - trace_options=$2 + 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 + cd "$REPO_ROOT" || return 1 - 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 + 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 + + # 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 [ ${#trace_opts[@]} -gt 0 ]; then + cmd1+=("${trace_opts[@]}") + fi + cmd1+=(--pattern "$pattern_specs") + fi + + cmd2=() + if [ ${#selector_opts[@]} -gt 0 ]; then + cmd2=(shellspec) + if [ ${#trace_opts[@]} -gt 0 ]; then + cmd2+=("${trace_opts[@]}") + fi + cmd2+=("${selector_opts[@]}") + 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..986a8e9 --- /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]&.length&.positive? + 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..f43dfc6 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,17 @@ 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) + @env_substitutor.substitute( + clean_up_output_line(line) + ) + end + def read_stderr_file(stderr_file) File.readlines(stderr_file).map(&:chomp) rescue StandardError => e @@ -309,7 +261,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 +277,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 +289,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 +332,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..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 @@ -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) @@ -33,7 +21,12 @@ 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:]]*$//')" + + # 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 = "\"|\\" @@ -46,7 +39,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\" >&2" print " return 1" print " ;;" print " esac" 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 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..e70c4a2 --- /dev/null +++ b/test/runners/vm/vm-setup.sh @@ -0,0 +1,71 @@ +#!/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 + +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' +#!/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 "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 "To run tests:" +echo " su - testuser" +echo " cd zelta" +echo " git checkout (branchname)" +echo " ~/run_test.sh"