From bedad5187eaf1f15025c2816c304c72a6b326407 Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 15:01:44 +0100 Subject: [PATCH 01/18] =?UTF-8?q?=E2=9C=A8=20Add=20test=20suite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 +++ bashpt | 58 ++++++++++ docs/TESTING.md | 160 +++++++++++++++++++++++++++ libs/core/test/assert_command_exists | 42 +++++++ libs/core/test/assert_contains | 46 ++++++++ libs/core/test/assert_equals | 44 ++++++++ libs/core/test/assert_eventually | 60 ++++++++++ libs/core/test/assert_exit_code | 51 +++++++++ libs/core/test/assert_failure | 45 ++++++++ libs/core/test/assert_not_contains | 46 ++++++++ libs/core/test/assert_output | 49 ++++++++ libs/core/test/assert_success | 44 ++++++++ libs/core/test/prep_step | 50 +++++++++ 13 files changed, 712 insertions(+) create mode 100755 bashpt create mode 100644 docs/TESTING.md create mode 100644 libs/core/test/assert_command_exists create mode 100644 libs/core/test/assert_contains create mode 100644 libs/core/test/assert_equals create mode 100644 libs/core/test/assert_eventually create mode 100644 libs/core/test/assert_exit_code create mode 100644 libs/core/test/assert_failure create mode 100644 libs/core/test/assert_not_contains create mode 100644 libs/core/test/assert_output create mode 100644 libs/core/test/assert_success create mode 100644 libs/core/test/prep_step diff --git a/README.md b/README.md index 994d359..d85f214 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ functionalities with ease. advanced features. - **Preprocessing**: A preprocessing step that allows for dynamic inclusion of scripts and packages, supporting multiple package sources and enhancing modularity. +- **Built-in Testing Framework**: Simple, convention-based testing with container isolation and + visual feedback (✅ ⛔ ⚙️ ⚠️). [Learn more →](docs/TESTING.md) - **Git Integration**: Automatically adds included functions to `.gitignore` when pulling from external sources, preventing unintended commits. @@ -87,6 +89,21 @@ This script demonstrates three ways to include and use functions from external l in your Bash+ scripts. The third option shows how to use nested package paths for better organization of your code. +### Testing Your Code + +Bash+ includes a built-in testing framework that makes it easy to test your functions: + +```bash +# Test a specific function +bashpt user::get_uid + +# Run tests locally without container isolation +BASHPT_LOCAL=true bashpt user::get_uid +``` + +Tests are automatically discovered based on naming conventions and run in isolated Docker containers +by default. For more details, see the [Testing Documentation](docs/TESTING.md). + ### Package Path Resolution Bash+ supports nested package paths, allowing for better organization of your code. For example: diff --git a/bashpt b/bashpt new file mode 100755 index 0000000..6f82aea --- /dev/null +++ b/bashpt @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# +# Copyright 2025 The Linux Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +set -eo pipefail + +functionName=${1:?function to be tested must be set} +path=libs/${functionName//::/\/} +testPath=$(dirname "$path")/tests/${functionName##*::} +testFunction=${functionName%::*}::tests::${functionName##*::}::test + +if [[ -d "$testPath" && -f "$testPath/test" ]]; then + test=$(bashpc <<<"set -euo pipefail; $testFunction") || { + echo "⛔ Failed to prepare test script for '$testFunction'" >&2 + exit 2 + } + + returnCode=0 + if [[ "${BASHPT_LOCAL:-false}" == "true" ]]; then + # Run tests locally + bash -c "$test" || returnCode=$? + else + # Run tests in Docker container + arguments=(-ti --rm) + image="${BASHPT_IMAGE:-ubuntu:latest}" + + # Add user override if specified + if [[ -n "${BASHPT_USER:-}" ]]; then + arguments+=(-u "$BASHPT_USER") + fi + + docker run "${arguments[@]}" "$image" bash -c "$test" || returnCode=$? + fi + + if [[ $returnCode -eq 0 ]]; then + echo "✅ All assertions passed: '$testFunction' passed" + exit 0 + else + echo "⛔ Some assertions failed: '$testFunction' failed" >&2 + exit 1 + fi +else + echo "🚫 No test $testFunction defined for $functionName, skipping." + exit 127 +fi + diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 0000000..a97fc3d --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,160 @@ +# Testing in Bash+ + +Bash+ includes a lightweight testing framework designed with the same philosophy as the framework +itself: simple, modular, and convention-based. Tests are easy to write, run in isolated +environments, and provide clear visual feedback. + +## Quick Start + +To run tests for any Bash+ function: + +```bash +bashpt user::get_uid +``` + +This automatically finds and runs the test for the `user::get_uid` function. + +## Test Organization + +Tests follow a predictable convention based on function names: +- Function: `user::get_uid` +- Test location: `libs/user/tests/get_uid/test` +- Test function: `user::tests::get_uid::test` + +This convention eliminates configuration and makes test discovery automatic. + +## Writing Tests + +Here's a simple test example: + +```bash +# shellcheck shell=bash + +#include user/get_home +#include core/test/assert_equals +#include core/test/assert_success +#include core/test/assert_failure +#include core/test/assert_contains +#include core/test/prep_step + +function user::tests::get_home::test() { + local sudo="" + if [[ $EUID -ne 0 ]] && command -v sudo &>/dev/null; then + sudo="sudo" + fi + + # Test with explicit username + core::test::assert_success user::get_home root + core::test::assert_equals /root "$(user::get_home root)" + core::test::assert_success test -d /root + + # Test with no argument (should default to current user) + local current_user expected_home + current_user=$(whoami) + expected_home=$(user::get_home "$current_user") + core::test::assert_success user::get_home + core::test::assert_equals "$expected_home" "$(user::get_home)" + + # Test with a custom user + core::test::prep_step $sudo useradd -m testuser + core::test::assert_success user::get_home testuser + + local home_dir + home_dir=$(user::get_home testuser) + core::test::assert_equals /home/testuser "$home_dir" + core::test::assert_contains "$home_dir" "testuser" + + core::test::prep_step $sudo userdel -r testuser + + # Test failure case + core::test::assert_failure user::get_home non_existent_user +} +``` + +## Assertion Functions + +Bash+ provides a comprehensive set of assertion functions with visual feedback: + +### Basic Assertions +- `core::test::assert_success` - Verifies a command exits with status 0 +- `core::test::assert_failure` - Verifies a command exits with non-zero status +- `core::test::assert_exit_code` - Verifies a command exits with a specific exit code +- `core::test::assert_equals` - Compares expected and actual values + +### String Assertions +- `core::test::assert_contains` - Verifies a string contains a substring +- `core::test::assert_not_contains` - Verifies a string does not contain a substring +- `core::test::assert_output` - Verifies a command produces expected output + +### System Assertions +- `core::test::assert_command_exists` - Verifies a command exists in PATH + +### Async Assertions +- `core::test::assert_eventually` - Retries a condition until it succeeds or times out (useful for async operations) + +### Test Preparation +- `core::test::prep_step` - Executes preparational steps (like creating test users or files) with distinct visual feedback + +Visual indicators: +- ✅ Passed assertions +- ⛔ Failed assertions +- ⚙️ Successful preparation steps +- ⚠️ Failed preparation steps + +## Container Execution + +By default, tests run in an isolated Docker container to ensure consistency across environments. +This is particularly useful for: +- System-level testing (user management, file permissions) +- Testing across different Linux distributions +- Ensuring a clean environment for each test run + +### Configuration Options + +Control test execution with environment variables: + +```bash +# Run tests locally instead of in a container +BASHPT_LOCAL=true bashpt user::get_uid + +# Use a specific container image +BASHPT_IMAGE=alpine:latest bashpt user::get_uid + +# Run tests as a specific user +BASHPT_USER=testuser bashpt user::get_uid +``` + +### Default Behavior + +- Container: `ubuntu:latest` +- User: `root` (configurable via `BASHPT_USER`) +- Automatic cleanup after test execution + +## Test Independence + +Each test is self-contained and handles its own setup and teardown. This explicit approach makes +tests: +- Easy to understand +- Simple to debug +- Independent from other tests +- Reproducible + +## Best Practices + +1. **Keep tests focused**: Test one function at a time +2. **Use prep_step for setup**: Distinguish test preparation from actual assertions +3. **Clean up resources**: Remove any users, files, or processes created during testing +4. **Use meaningful assertions**: Test both success and failure cases +5. **Leverage containers**: Use container isolation for system-level tests +6. **Follow conventions**: Place tests in the expected location for automatic discovery + +## Philosophy + +The Bash+ testing framework embodies the same principles as Bash+ itself: +- **Convention over configuration**: Tests are discovered automatically +- **Simplicity**: Minimal abstraction, maximum clarity +- **Modularity**: Reusable assertion functions +- **Practicality**: Visual feedback and straightforward debugging + +You don't need complex test runners or elaborate frameworks. Just write functions that test your +code, and `bashpt` handles the rest. diff --git a/libs/core/test/assert_command_exists b/libs/core/test/assert_command_exists new file mode 100644 index 0000000..5ba3959 --- /dev/null +++ b/libs/core/test/assert_command_exists @@ -0,0 +1,42 @@ +# shellcheck shell=bash +# +# Copyright 2025 The Linux Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# core::test::assert_command_exists +# Asserts that a command exists in the PATH. +# +# Description: +# This function checks if the specified command is available in the system PATH. +# +# Usage: +# core::test::assert_command_exists +# +# Parameters: +# commandName - The name of the command to check (e.g., 'ls', 'bash'). +# +# Example: +# core::test::assert_command_exists ls +function core::test::assert_command_exists() { + local command=${1:-${command:?must be set}} + + if command -v "$command" &>/dev/null; then + echo "✅ Assertion passed: command '$command' exists" + return 0 + else + echo "⛔ Assertion failed: command '$command' does not exist" >&2 + return 1 + fi +} diff --git a/libs/core/test/assert_contains b/libs/core/test/assert_contains new file mode 100644 index 0000000..8783cbb --- /dev/null +++ b/libs/core/test/assert_contains @@ -0,0 +1,46 @@ +# shellcheck shell=bash +# +# Copyright 2025 The Linux Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# core::test::assert_contains +# Asserts that a string contains a substring. +# +# Description: +# This function checks if the first string contains the second string as a +# substring. +# +# Usage: +# core::test::assert_contains +# +# Parameters: +# haystack - The string to search in. +# needle - The substring to search for. +# +# Example: +# core::test::assert_contains "hello world" "world" +function core::test::assert_contains() { + local haystack=${1:-${haystack:?must be set}} + local needle=${2:-${needle:?must be set}} + + if [[ "$haystack" == *"$needle"* ]]; then + echo "✅ Assertion passed: '$haystack' contains '$needle'" + return 0 + else + echo "⛔ Assertion failed: '$haystack' does not contain '$needle'" >&2 + return 1 + fi +} + diff --git a/libs/core/test/assert_equals b/libs/core/test/assert_equals new file mode 100644 index 0000000..abc9472 --- /dev/null +++ b/libs/core/test/assert_equals @@ -0,0 +1,44 @@ +# shellcheck shell=bash +# +# Copyright 2025 The Linux Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# core::test::assert_equals +# Asserts that two values are equal. +# +# Description: +# This function compares two values and verifies that they are equal. +# +# Usage: +# core::test::assert_equals +# +# Parameters: +# expected - The expected value. +# actual - The actual value to compare. +# +# Example: +# core::test::assert_equals "hello" "hello" +function core::test::assert_equals() { + local expected=${1:-${expected:?must be set}} + local actual=${2:-${actual:?must be set}} + + if [[ "$expected" != "$actual" ]]; then + echo "⛔ Assertion failed: expected '$expected', got '$actual'" >&2 + return 1 + else + echo "✅ Assertion passed: expected '$expected', got '$actual'" + return 0 + fi +} diff --git a/libs/core/test/assert_eventually b/libs/core/test/assert_eventually new file mode 100644 index 0000000..f5cd122 --- /dev/null +++ b/libs/core/test/assert_eventually @@ -0,0 +1,60 @@ +# shellcheck shell=bash +# +# Copyright 2025 The Linux Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# core::test::assert_eventually +# Asserts that a condition becomes true by retrying a command until it succeeds or times out. +# +# Description: +# This function repeatedly executes a given command (with optional arguments) +# until it succeeds (returns zero) or until the timeout expires. +# This is useful for testing asynchronous operations or conditions that may become +# true over time. +# The function uses the `timeout` command to ensure the actual wait time matches +# the specified timeout in seconds, rather than timing out in the middle of retries. +# +# Usage: +# core::test::assert_eventually [timeout] [args...] +# +# Parameters: +# timeout - Optional. Timeout in seconds (default: 5). +# command - The command or function to execute. +# args... - Optional arguments to pass to the command. +# +# Example: +# core::test::assert_eventually 10 test -f /tmp/file +function core::test::assert_eventually() { + local timeout=${timeout:-} + + if [[ -z "$timeout" && -n "$1" && "$1" =~ ^[0-9]+$ ]]; then + timeout=$1 + shift + elif [[ -z "$timeout" ]]; then + timeout=5 + fi + + if timeout "${timeout}" bash -s -- "$@" <<'EOF' &>/dev/null; then +until "$@"; do + sleep 1 +done +EOF + echo "✅ Eventually succeeded: $*" + return 0 + else + echo "⛔ Eventually failed: '$*' failed after ${timeout}s" >&2 + return 1 + fi +} diff --git a/libs/core/test/assert_exit_code b/libs/core/test/assert_exit_code new file mode 100644 index 0000000..4c75a4b --- /dev/null +++ b/libs/core/test/assert_exit_code @@ -0,0 +1,51 @@ +# shellcheck shell=bash +# +# Copyright 2025 The Linux Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# core::test::assert_exit_code +# Asserts that a command exits with a specific exit code. +# +# Description: +# This function executes a command and verifies that it exits with the +# expected exit code. +# +# Usage: +# core::test::assert_exit_code [args...] +# +# Parameters: +# expectedCode - The expected exit code (0-255). +# command - The command or function to execute. +# args... - Optional arguments to pass to the command. +# +# Example: +# core::test::assert_exit_code 0 ls /tmp +# core::test::assert_exit_code 1 ls /nonexistent +function core::test::assert_exit_code() { + local expectedCode=${1:-${expectedCode:?must be set}} + shift + + "$@" &>/dev/null + local actualCode=$? + + if [[ $actualCode -eq $expectedCode ]]; then + echo "✅ Assertion passed: command '$*' exited with status $actualCode" + return 0 + else + echo "⛔ Assertion failed: command '$*' exited with status $actualCode (expected $expectedCode)" >&2 + return 1 + fi +} + diff --git a/libs/core/test/assert_failure b/libs/core/test/assert_failure new file mode 100644 index 0000000..178e207 --- /dev/null +++ b/libs/core/test/assert_failure @@ -0,0 +1,45 @@ +# shellcheck shell=bash +# +# Copyright 2025 The Linux Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# core::test::assert_failure +# Asserts that a command exits with a non-zero exit code. +# +# Description: +# This function executes a command and verifies that it exits with a non-zero +# status code (indicating failure). +# +# Usage: +# core::test::assert_failure [args...] +# +# Parameters: +# command - The command or function to execute. +# args... - Optional arguments to pass to the command. +# +# Example: +# core::test::assert_failure ls /nonexistent +function core::test::assert_failure() { + local exit_code + if ! "$@" &>/dev/null; then + exit_code=$? + echo "✅ Assertion passed: command '$*' exited with status $exit_code" + return 0 + else + exit_code=$? + echo "⛔ Assertion failed: command '$*' exited with status $exit_code (expected non-zero)" >&2 + return 1 + fi +} diff --git a/libs/core/test/assert_not_contains b/libs/core/test/assert_not_contains new file mode 100644 index 0000000..52fa5b0 --- /dev/null +++ b/libs/core/test/assert_not_contains @@ -0,0 +1,46 @@ +# shellcheck shell=bash +# +# Copyright 2025 The Linux Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# core::test::assert_not_contains +# Asserts that a string does not contain a substring. +# +# Description: +# This function checks if the first string does not contain the second string +# as a substring. +# +# Usage: +# core::test::assert_not_contains +# +# Parameters: +# haystack - The string to search in. +# needle - The substring that should not be found. +# +# Example: +# core::test::assert_not_contains "hello world" "foo" +function core::test::assert_not_contains() { + local haystack=${1:-${haystack:?must be set}} + local needle=${2:-${needle:?must be set}} + + if [[ "$haystack" != *"$needle"* ]]; then + echo "✅ Assertion passed: '$haystack' does not contain '$needle'" + return 0 + else + echo "⛔ Assertion failed: '$haystack' contains '$needle'" >&2 + return 1 + fi +} + diff --git a/libs/core/test/assert_output b/libs/core/test/assert_output new file mode 100644 index 0000000..7616727 --- /dev/null +++ b/libs/core/test/assert_output @@ -0,0 +1,49 @@ +# shellcheck shell=bash +# +# Copyright 2025 The Linux Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# core::test::assert_output +# Asserts that a command produces the expected output. +# +# Description: +# This function executes a command and verifies that its stdout output matches +# the expected value exactly. +# +# Usage: +# core::test::assert_output [args...] +# +# Parameters: +# expected - The expected stdout output. +# command - The command or function to execute. +# args... - Optional arguments to pass to the command. +# +# Example: +# core::test::assert_output "hello" echo "hello" +function core::test::assert_output() { + local expected=${1:-${expected:?must be set}} + shift + local actual + actual=$("$@" 2>/dev/null) + + if [[ "$actual" == "$expected" ]]; then + echo "✅ Assertion passed: output matches '$expected'" + return 0 + else + echo "⛔ Assertion failed: expected output '$expected', got '$actual'" >&2 + return 1 + fi +} + diff --git a/libs/core/test/assert_success b/libs/core/test/assert_success new file mode 100644 index 0000000..60a3631 --- /dev/null +++ b/libs/core/test/assert_success @@ -0,0 +1,44 @@ +# shellcheck shell=bash +# +# Copyright 2025 The Linux Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# core::test::assert_success +# Asserts that a command exits successfully (exit code 0). +# +# Description: +# This function executes a command and verifies that it exits with status code 0. +# +# Usage: +# core::test::assert_success [args...] +# +# Parameters: +# command - The command or function to execute. +# args... - Optional arguments to pass to the command. +# +# Example: +# core::test::assert_success ls /tmp +function core::test::assert_success() { + local exit_code + if "$@" &>/dev/null; then + exit_code=0 + echo "✅ Assertion passed: command '$*' exited with status $exit_code" + return 0 + else + exit_code=$? + echo "⛔ Assertion failed: command '$*' exited with status $exit_code (expected 0)" >&2 + return 1 + fi +} diff --git a/libs/core/test/prep_step b/libs/core/test/prep_step new file mode 100644 index 0000000..359541a --- /dev/null +++ b/libs/core/test/prep_step @@ -0,0 +1,50 @@ +# shellcheck shell=bash +# +# Copyright 2025 The Linux Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# core::test::prep_step +# Executes a preparational step and reports success/failure with distinct formatting. +# +# Description: +# This function executes a command intended for test setup/preparation (e.g., creating users, +# setting up files, etc.). Unlike assertions, preparational steps are not test validations +# themselves, but are necessary for the test to run. Failures are reported with a distinct +# visual indicator to differentiate them from assertion failures. +# +# Usage: +# core::test::prep_step [args...] +# +# Parameters: +# command - The command or function to execute. +# args... - Optional arguments to pass to the command. +# +# Returns: +# Returns 0 if the command succeeds, non-zero if it fails. +# +# Example: +# core::test::prep_step useradd -m testuser +# core::test::prep_step sudo mkdir -p /tmp/testdir +function core::test::prep_step() { + if "$@" &>/dev/null; then + echo "⚙️ Preparational step succeeded: '$*'" + return 0 + else + local exitCode=$? + echo "⚠️ Preparational step failed: command '$*' exited with status $exitCode" >&2 + return $exitCode + fi +} + From ff5cb51d6269bc0a02e6a690550b7fefc51a6ded Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 15:02:16 +0100 Subject: [PATCH 02/18] =?UTF-8?q?=E2=9C=85=20Add=20test=20for=20user::get?= =?UTF-8?q?=5Fhome?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/user/tests/get_home/test | 61 +++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 libs/user/tests/get_home/test diff --git a/libs/user/tests/get_home/test b/libs/user/tests/get_home/test new file mode 100644 index 0000000..54b87d4 --- /dev/null +++ b/libs/user/tests/get_home/test @@ -0,0 +1,61 @@ +# shellcheck shell=bash +# +# Copyright 2025 The Linux Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +#include user/get_home +#include core/test/assert_equals +#include core/test/assert_success +#include core/test/assert_failure +#include core/test/prep_step + +function user::tests::get_home::test() { + local sudo="" + if [[ $EUID -ne 0 ]] && command -v sudo &>/dev/null; then + sudo="sudo" + fi + + # Test with explicit username + core::test::assert_success user::get_home root + core::test::assert_equals /root "$(user::get_home root)" + + # Test with no argument (should default to current user) + local current_user + current_user=$(whoami) + local expected_home + expected_home=$(user::get_home "$current_user") + core::test::assert_success user::get_home + core::test::assert_equals "$expected_home" "$(user::get_home)" + + # Test with $user environment variable set (should use $user instead of whoami) + local test_user="root" + user=$test_user core::test::assert_success user::get_home + user=$test_user core::test::assert_equals /root "$(user::get_home)" + + # Test with a custom user + core::test::prep_step $sudo useradd -m testuser >/dev/null + core::test::assert_success user::get_home testuser + core::test::assert_equals /home/testuser "$(user::get_home testuser)" + core::test::prep_step $sudo userdel -r testuser >/dev/null + + # Test with a custom home directory + core::test::prep_step $sudo useradd -m -d /tmp/testuser testuser >/dev/null + core::test::assert_success user::get_home testuser + core::test::assert_equals /tmp/testuser "$(user::get_home testuser)" + core::test::prep_step $sudo userdel -r testuser >/dev/null + + # Test with a non-existent user + core::test::assert_failure user::get_home non_existent_user +} From fcfffd754698be5ee98c53808a30be24b89125f3 Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 15:12:35 +0100 Subject: [PATCH 03/18] =?UTF-8?q?=F0=9F=94=A7=20Conditionally=20add=20-ti?= =?UTF-8?q?=20flag=20based=20on=20TTY=20availability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bashpt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bashpt b/bashpt index 6f82aea..e6390cd 100755 --- a/bashpt +++ b/bashpt @@ -33,7 +33,13 @@ if [[ -d "$testPath" && -f "$testPath/test" ]]; then bash -c "$test" || returnCode=$? else # Run tests in Docker container - arguments=(-ti --rm) + arguments=(--rm) + + # Only add -ti if we have a TTY (not in CI) + if [ -t 0 ]; then + arguments+=(-ti) + fi + image="${BASHPT_IMAGE:-ubuntu:latest}" # Add user override if specified From 527212817e23802bee425902975f6ab55a0cda7e Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 15:03:16 +0100 Subject: [PATCH 04/18] =?UTF-8?q?=F0=9F=91=B7=20Test=20functions,=20don't?= =?UTF-8?q?=20publish=20packages=20with=20failed=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Ruthardt --- .github/workflows/package.yaml | 145 ++++++++++++++++++++++++++------- 1 file changed, 116 insertions(+), 29 deletions(-) diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index 0fe930f..d3929c8 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -10,16 +10,124 @@ on: workflow_call: jobs: - build: + setup: runs-on: ubuntu-latest - strategy: - matrix: ${{fromJson(needs.setup.outputs.matrix)}} + outputs: + packages: ${{ steps.packages.outputs.matrix }} + functions: ${{ steps.functions.outputs.matrix }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get all packages + id: packages + shell: bash + run: | + json=$( + ( + for directory in $(find libs/ -mindepth 1 -maxdepth 1 -type d); do + echo -n '"'"${directory#*/}"'",' + done + echo '""' + ) | sed -E 's/,?""$//' + ) + echo "matrix={\"package\":[$json]}" >> $GITHUB_OUTPUT + + - name: Get all functions + id: functions + shell: bash + run: | + json=$( + ( + for directory in $(find libs/ -mindepth 1 -maxdepth 1 -type d); do + package="${directory#*/}" + + for file in $(find "libs/$package" -type f -and \ + ! -name '\*' -and \ + ! -wholename '*/tests/*'); do + directory=$(dirname "$file") + fileName=$(basename "$file") + if [[ ! -d "$directory/tests/$fileName" || ! -f "$directory/tests/$fileName/test" ]]; then + continue + fi + + functionName=${file#libs/*} + functionName=${functionName//\//::} + + echo -n '{"package":"'"$package"'","function":"'"$functionName"'"},' + done + done + echo '""' + ) | sed -E 's/,?""$//' + ) + + echo "matrix={\"function\":[$json]}" >> $GITHUB_OUTPUT + + test: + runs-on: ubuntu-latest + outputs: + results: ${{ toJson(steps.test.outputs) }} needs: [setup] + if: needs.setup.outputs.functions != '{"include":[]}' + strategy: + matrix: ${{ fromJson(needs.setup.outputs.functions) }} + fail-fast: false steps: - name: Checkout code uses: actions/checkout@v4 + - name: Set up Docker + uses: docker/setup-buildx-action@v3 + + - name: Test function + id: test + shell: bash + run: | + export PATH=".:$PATH" + export BASHP_INCLUDE_DIRS="libs" + + set +e + output=$(bashpt "${{ matrix.function.function }}" 2>&1) + exitCcode=$? + echo "$output" + + if [[ $exitCode -eq 0 ]]; then + echo "✅ Test passed for ${{ matrix.function }}" + elif [[ $exitCode -eq 127 ]]; then + echo "🔍 No test defined for ${{ matrix.function }} (acceptable)" + else + echo "❌ Test failed for ${{ matrix.function }} (exit code: $exitCode)" + echo "${{ matrix.function.package }}=false" >> $GITHUB_OUTPUT + fi + + package: + runs-on: ubuntu-latest + needs: [setup, test] + if: always() && needs.setup.outputs.packages != '{"include":[]}' + strategy: + matrix: ${{ fromJson(needs.setup.outputs.packages) }} + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check if all tests passed for this package + id: check + run: | + if ! jq -e '.${{ matrix.package }} == "false"' <<<'${{ needs.test.outputs.results }}'; then + echo "✅ All tests passed for package ${{ matrix.package }}" + echo "package=true" >> $GITHUB_OUTPUT + else + echo "❌ Some tests failed for package ${{ matrix.package }}" + echo "package=false" >> $GITHUB_OUTPUT + fi + + - name: Set up Docker + if: steps.check.outputs.package == 'true' + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + if: steps.check.outputs.package == 'true' uses: docker/login-action@v3 with: registry: ghcr.io @@ -27,43 +135,22 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Create temporary Dockerfile + if: steps.check.outputs.package == 'true' run: | cat >libs/Dockerfile <<'EOF' FROM scratch LABEL org.opencontainers.image.source https://github.com/${{ github.repository }} - + ARG PACKAGE ADD ${PACKAGE} /${PACKAGE}/ EOF - name: Build and push Docker image + if: steps.check.outputs.package == 'true' uses: docker/build-push-action@v5 with: context: ./libs file: ./libs/Dockerfile - build-args: PACKAGE=${{ matrix.item }} + build-args: PACKAGE=${{ matrix.package }} push: true - tags: ghcr.io/lf-certification/bashp-${{ matrix.item }}:latest - - setup: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: List all directories in libs and create matrix - id: set-matrix - run: | - json=$( - ( - for directory in $(find libs/ -mindepth 1 -maxdepth 1 -type d); do - echo -n '"'"${directory#*/}"'",' - done - echo '""' - ) | sed -E 's/,?""$//' - ) - - echo "matrix={\"item\":[$json]}" >> $GITHUB_OUTPUT - shell: bash + tags: ghcr.io/lf-certification/bashp-${{ matrix.package }}:latest From be144ca60a16148baccd27fe85f9b0cfe3a36113 Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 19:59:19 +0100 Subject: [PATCH 05/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Split=20into=20two?= =?UTF-8?q?=20workflows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Ruthardt --- .github/workflows/package.yaml | 97 ++--------------------------- .github/workflows/test.yaml | 109 +++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 92 deletions(-) create mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index d3929c8..42664c9 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -1,4 +1,4 @@ -name: Build and Push Package Images +name: Build and Push Packages on: workflow_dispatch: @@ -10,102 +10,15 @@ on: workflow_call: jobs: - setup: - runs-on: ubuntu-latest - outputs: - packages: ${{ steps.packages.outputs.matrix }} - functions: ${{ steps.functions.outputs.matrix }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Get all packages - id: packages - shell: bash - run: | - json=$( - ( - for directory in $(find libs/ -mindepth 1 -maxdepth 1 -type d); do - echo -n '"'"${directory#*/}"'",' - done - echo '""' - ) | sed -E 's/,?""$//' - ) - echo "matrix={\"package\":[$json]}" >> $GITHUB_OUTPUT - - - name: Get all functions - id: functions - shell: bash - run: | - json=$( - ( - for directory in $(find libs/ -mindepth 1 -maxdepth 1 -type d); do - package="${directory#*/}" - - for file in $(find "libs/$package" -type f -and \ - ! -name '\*' -and \ - ! -wholename '*/tests/*'); do - directory=$(dirname "$file") - fileName=$(basename "$file") - if [[ ! -d "$directory/tests/$fileName" || ! -f "$directory/tests/$fileName/test" ]]; then - continue - fi - - functionName=${file#libs/*} - functionName=${functionName//\//::} - - echo -n '{"package":"'"$package"'","function":"'"$functionName"'"},' - done - done - echo '""' - ) | sed -E 's/,?""$//' - ) - - echo "matrix={\"function\":[$json]}" >> $GITHUB_OUTPUT - test: - runs-on: ubuntu-latest - outputs: - results: ${{ toJson(steps.test.outputs) }} - needs: [setup] - if: needs.setup.outputs.functions != '{"include":[]}' - strategy: - matrix: ${{ fromJson(needs.setup.outputs.functions) }} - fail-fast: false - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker - uses: docker/setup-buildx-action@v3 - - - name: Test function - id: test - shell: bash - run: | - export PATH=".:$PATH" - export BASHP_INCLUDE_DIRS="libs" - - set +e - output=$(bashpt "${{ matrix.function.function }}" 2>&1) - exitCcode=$? - echo "$output" - - if [[ $exitCode -eq 0 ]]; then - echo "✅ Test passed for ${{ matrix.function }}" - elif [[ $exitCode -eq 127 ]]; then - echo "🔍 No test defined for ${{ matrix.function }} (acceptable)" - else - echo "❌ Test failed for ${{ matrix.function }} (exit code: $exitCode)" - echo "${{ matrix.function.package }}=false" >> $GITHUB_OUTPUT - fi + uses: ./.github/workflows/test.yaml package: runs-on: ubuntu-latest - needs: [setup, test] - if: always() && needs.setup.outputs.packages != '{"include":[]}' + needs: [test] + if: always() && needs.test.outputs.packages != '{"package":[]}' strategy: - matrix: ${{ fromJson(needs.setup.outputs.packages) }} + matrix: ${{ fromJson(needs.test.outputs.packages) }} fail-fast: false steps: - name: Checkout code diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..816227e --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,109 @@ +name: Test Functions + +on: + workflow_dispatch: + push: + paths: + - 'libs/**' + workflow_call: + outputs: + packages: + description: All packages + value: ${{ jobs.setup.outputs.packages }} + functions: + description: All functions with tests + value: ${{ jobs.setup.outputs.functions }} + results: + description: Packges with some failed tests + value: ${{ jobs.test.outputs.results }} + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + packages: ${{ steps.packages.outputs.matrix }} + functions: ${{ steps.functions.outputs.matrix }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get all packages + id: packages + shell: bash + run: | + json=$( + ( + for directory in $(find libs/ -mindepth 1 -maxdepth 1 -type d); do + echo -n '"'"${directory#*/}"'",' + done + echo '""' + ) | sed -E 's/,?""$//' + ) + echo "matrix={\"package\":[$json]}" >> $GITHUB_OUTPUT + + - name: Get all functions + id: functions + shell: bash + run: | + json=$( + ( + for directory in $(find libs/ -mindepth 1 -maxdepth 1 -type d); do + package="${directory#*/}" + + for file in $(find "libs/$package" -type f -and \ + ! -name '\*' -and \ + ! -wholename '*/tests/*'); do + directory=$(dirname "$file") + fileName=$(basename "$file") + if [[ ! -d "$directory/tests/$fileName" || ! -f "$directory/tests/$fileName/test" ]]; then + continue + fi + + functionName=${file#libs/*} + functionName=${functionName//\//::} + + echo -n '{"package":"'"$package"'","function":"'"$functionName"'"},' + done + done + echo '""' + ) | sed -E 's/,?""$//' + ) + + echo "matrix={\"function\":[$json]}" >> $GITHUB_OUTPUT + + test: + runs-on: ubuntu-latest + outputs: + results: ${{ toJson(steps.test.outputs) }} + needs: [setup] + if: needs.setup.outputs.functions != '{"function":[]}' + strategy: + matrix: ${{ fromJson(needs.setup.outputs.functions) }} + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker + uses: docker/setup-buildx-action@v3 + + - name: Test function + id: test + shell: bash + run: | + export PATH=".:$PATH" + export BASHP_INCLUDE_DIRS="libs" + + set +e + output=$(bashpt "${{ matrix.function.function }}" 2>&1) + exitCcode=$? + echo "$output" + + if [[ $exitCode -eq 0 ]]; then + echo "✅ Test passed for ${{ matrix.function }}" + elif [[ $exitCode -eq 127 ]]; then + echo "🔍 No test defined for ${{ matrix.function }} (acceptable)" + else + echo "❌ Test failed for ${{ matrix.function }} (exit code: $exitCode)" + echo "${{ matrix.function.package }}=false" >> $GITHUB_OUTPUT + fi From 5c394a18169239495cfb0852f982566b64be931d Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 20:33:21 +0100 Subject: [PATCH 06/18] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 816227e..0c58e53 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,7 +14,7 @@ on: description: All functions with tests value: ${{ jobs.setup.outputs.functions }} results: - description: Packges with some failed tests + description: Packages with some failed tests value: ${{ jobs.test.outputs.results }} jobs: From 57b3664b87e31d34407d7554de06bcac2320753d Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 20:33:58 +0100 Subject: [PATCH 07/18] =?UTF-8?q?=F0=9F=90=9B=20Fix=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/test.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0c58e53..fa7ae04 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -99,11 +99,11 @@ jobs: exitCcode=$? echo "$output" - if [[ $exitCode -eq 0 ]]; then + if [[ $exitCcode -eq 0 ]]; then echo "✅ Test passed for ${{ matrix.function }}" - elif [[ $exitCode -eq 127 ]]; then + elif [[ $exitCcode -eq 127 ]]; then echo "🔍 No test defined for ${{ matrix.function }} (acceptable)" else - echo "❌ Test failed for ${{ matrix.function }} (exit code: $exitCode)" + echo "❌ Test failed for ${{ matrix.function }} (exit code: $exitCcode)" echo "${{ matrix.function.package }}=false" >> $GITHUB_OUTPUT fi From 4caad6172966fd449c9854efc08b1d4016f77210 Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 20:35:07 +0100 Subject: [PATCH 08/18] =?UTF-8?q?=F0=9F=92=AC=20Fix=20language=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/TESTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index a97fc3d..87864a9 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -93,7 +93,7 @@ Bash+ provides a comprehensive set of assertion functions with visual feedback: - `core::test::assert_eventually` - Retries a condition until it succeeds or times out (useful for async operations) ### Test Preparation -- `core::test::prep_step` - Executes preparational steps (like creating test users or files) with distinct visual feedback +- `core::test::prep_step` - Executes preparation steps (like creating test users or files) with distinct visual feedback Visual indicators: - ✅ Passed assertions From 5cf90b508ff282187705eb5a88fbee82d3b2ea62 Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 20:39:38 +0100 Subject: [PATCH 09/18] =?UTF-8?q?=F0=9F=92=AC=20Fix=20language=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/core/test/prep_step | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/core/test/prep_step b/libs/core/test/prep_step index 359541a..17e0d07 100644 --- a/libs/core/test/prep_step +++ b/libs/core/test/prep_step @@ -39,11 +39,11 @@ # core::test::prep_step sudo mkdir -p /tmp/testdir function core::test::prep_step() { if "$@" &>/dev/null; then - echo "⚙️ Preparational step succeeded: '$*'" + echo "⚙️ Preparation step succeeded: '$*'" return 0 else local exitCode=$? - echo "⚠️ Preparational step failed: command '$*' exited with status $exitCode" >&2 + echo "⚠️ Preparation step failed: command '$*' exited with status $exitCode" >&2 return $exitCode fi } From a156da20743368afd8c4e7a887ca7ca0627d1830 Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 20:42:23 +0100 Subject: [PATCH 10/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20camelCase=20fo?= =?UTF-8?q?r=20variables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/core/test/assert_failure | 10 +++++----- libs/core/test/assert_success | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/libs/core/test/assert_failure b/libs/core/test/assert_failure index 178e207..b691115 100644 --- a/libs/core/test/assert_failure +++ b/libs/core/test/assert_failure @@ -32,14 +32,14 @@ # Example: # core::test::assert_failure ls /nonexistent function core::test::assert_failure() { - local exit_code + local exitCode if ! "$@" &>/dev/null; then - exit_code=$? - echo "✅ Assertion passed: command '$*' exited with status $exit_code" + exitCode=$? + echo "✅ Assertion passed: command '$*' exited with status $exitCode" return 0 else - exit_code=$? - echo "⛔ Assertion failed: command '$*' exited with status $exit_code (expected non-zero)" >&2 + exitCode=$? + echo "⛔ Assertion failed: command '$*' exited with status $exitCode (expected non-zero)" >&2 return 1 fi } diff --git a/libs/core/test/assert_success b/libs/core/test/assert_success index 60a3631..6c77cc9 100644 --- a/libs/core/test/assert_success +++ b/libs/core/test/assert_success @@ -31,14 +31,14 @@ # Example: # core::test::assert_success ls /tmp function core::test::assert_success() { - local exit_code + local exitCode if "$@" &>/dev/null; then - exit_code=0 - echo "✅ Assertion passed: command '$*' exited with status $exit_code" + exitCode=0 + echo "✅ Assertion passed: command '$*' exited with status $exitCode" return 0 else - exit_code=$? - echo "⛔ Assertion failed: command '$*' exited with status $exit_code (expected 0)" >&2 + exitCode=$? + echo "⛔ Assertion failed: command '$*' exited with status $exitCode (expected 0)" >&2 return 1 fi } From 9561066546e23c5e238b37f1987e24a9445064f5 Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 20:44:51 +0100 Subject: [PATCH 11/18] =?UTF-8?q?=F0=9F=92=AC=20Fix=20language=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/core/test/prep_step | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/core/test/prep_step b/libs/core/test/prep_step index 17e0d07..cdb36b0 100644 --- a/libs/core/test/prep_step +++ b/libs/core/test/prep_step @@ -16,11 +16,11 @@ # # core::test::prep_step -# Executes a preparational step and reports success/failure with distinct formatting. +# Executes a preparation step and reports success/failure with distinct formatting. # # Description: # This function executes a command intended for test setup/preparation (e.g., creating users, -# setting up files, etc.). Unlike assertions, preparational steps are not test validations +# setting up files, etc.). Unlike assertions, preparation steps are not test validations # themselves, but are necessary for the test to run. Failures are reported with a distinct # visual indicator to differentiate them from assertion failures. # From 46d20472caa176efc4bbe413a1e0603c36afa3c5 Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 20:53:06 +0100 Subject: [PATCH 12/18] =?UTF-8?q?=F0=9F=A9=B9=20Ensure=20we=20are=20using?= =?UTF-8?q?=20seconds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/core/test/assert_eventually | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/core/test/assert_eventually b/libs/core/test/assert_eventually index f5cd122..a3415b5 100644 --- a/libs/core/test/assert_eventually +++ b/libs/core/test/assert_eventually @@ -40,10 +40,10 @@ function core::test::assert_eventually() { local timeout=${timeout:-} if [[ -z "$timeout" && -n "$1" && "$1" =~ ^[0-9]+$ ]]; then - timeout=$1 + timeout="${1}s" shift elif [[ -z "$timeout" ]]; then - timeout=5 + timeout=5s fi if timeout "${timeout}" bash -s -- "$@" <<'EOF' &>/dev/null; then From b24d7f303f329d130d11b80ca97d3974ae3a6e2b Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 20:53:24 +0100 Subject: [PATCH 13/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/core/test/assert_eventually | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/core/test/assert_eventually b/libs/core/test/assert_eventually index a3415b5..7c4a0bc 100644 --- a/libs/core/test/assert_eventually +++ b/libs/core/test/assert_eventually @@ -46,7 +46,7 @@ function core::test::assert_eventually() { timeout=5s fi - if timeout "${timeout}" bash -s -- "$@" <<'EOF' &>/dev/null; then + if timeout "${timeout}" bash </dev/null; then until "$@"; do sleep 1 done From d569ccde359998186984f2ddc5df9460671ad0a0 Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 20:55:28 +0100 Subject: [PATCH 14/18] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/test.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fa7ae04..0acbcb1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -96,14 +96,14 @@ jobs: set +e output=$(bashpt "${{ matrix.function.function }}" 2>&1) - exitCcode=$? + exitCode=$? echo "$output" - if [[ $exitCcode -eq 0 ]]; then + if [[ $exitCode -eq 0 ]]; then echo "✅ Test passed for ${{ matrix.function }}" - elif [[ $exitCcode -eq 127 ]]; then + elif [[ $exitCode -eq 127 ]]; then echo "🔍 No test defined for ${{ matrix.function }} (acceptable)" else - echo "❌ Test failed for ${{ matrix.function }} (exit code: $exitCcode)" + echo "❌ Test failed for ${{ matrix.function }} (exit code: $exitCode)" echo "${{ matrix.function.package }}=false" >> $GITHUB_OUTPUT fi From aba3d5938050905e874fceb74f42e349b1c4c0d5 Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 20:56:56 +0100 Subject: [PATCH 15/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20reading?= =?UTF-8?q?=20the=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/package.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index 42664c9..7310277 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -27,12 +27,12 @@ jobs: - name: Check if all tests passed for this package id: check run: | - if ! jq -e '.${{ matrix.package }} == "false"' <<<'${{ needs.test.outputs.results }}'; then - echo "✅ All tests passed for package ${{ matrix.package }}" - echo "package=true" >> $GITHUB_OUTPUT - else + if jq -e '.${{ matrix.package }} == "false"' <<<'${{ needs.test.outputs.results }}'; then echo "❌ Some tests failed for package ${{ matrix.package }}" echo "package=false" >> $GITHUB_OUTPUT + else + echo "✅ All tests passed for package ${{ matrix.package }}" + echo "package=true" >> $GITHUB_OUTPUT fi - name: Set up Docker From 6cb74b9641881a4bd9445d3e81b17dc1a56ac971 Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 21:03:38 +0100 Subject: [PATCH 16/18] =?UTF-8?q?=F0=9F=92=9A=20Use=20the=20correct=20key/?= =?UTF-8?q?value,=20it=20is=20an=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0acbcb1..edfb6de 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -99,11 +99,11 @@ jobs: exitCode=$? echo "$output" - if [[ $exitCode -eq 0 ]]; then - echo "✅ Test passed for ${{ matrix.function }}" - elif [[ $exitCode -eq 127 ]]; then - echo "🔍 No test defined for ${{ matrix.function }} (acceptable)" + if [[ $exitCcode -eq 0 ]]; then + echo "✅ Test passed for ${{ matrix.function.function }}" + elif [[ $exitCcode -eq 127 ]]; then + echo "🔍 No test defined for ${{ matrix.function.function }} (acceptable)" else - echo "❌ Test failed for ${{ matrix.function }} (exit code: $exitCode)" + echo "❌ Test failed for ${{ matrix.function.function }} (exit code: $exitCcode)" echo "${{ matrix.function.package }}=false" >> $GITHUB_OUTPUT fi From 4f4184920be02fdc84c25ca093c50922f9c1ecc9 Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 21:11:13 +0100 Subject: [PATCH 17/18] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20trailing=20newlin?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bashpt | 1 - 1 file changed, 1 deletion(-) diff --git a/bashpt b/bashpt index e6390cd..52fe80a 100755 --- a/bashpt +++ b/bashpt @@ -61,4 +61,3 @@ else echo "🚫 No test $testFunction defined for $functionName, skipping." exit 127 fi - From 7c3702aca0795a99a6f32ec3083f9b323394dc6a Mon Sep 17 00:00:00 2001 From: Daniel Ruthardt Date: Thu, 13 Nov 2025 21:13:45 +0100 Subject: [PATCH 18/18] =?UTF-8?q?=F0=9F=94=8A=20Capture=20and=20provide=20?= =?UTF-8?q?output=20when=20prep=20step=20fails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- libs/core/test/prep_step | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/libs/core/test/prep_step b/libs/core/test/prep_step index cdb36b0..b912147 100644 --- a/libs/core/test/prep_step +++ b/libs/core/test/prep_step @@ -38,12 +38,19 @@ # core::test::prep_step useradd -m testuser # core::test::prep_step sudo mkdir -p /tmp/testdir function core::test::prep_step() { - if "$@" &>/dev/null; then + local output + output=$("$@" 2>&1) + local exitCode=$? + if [ $exitCode -eq 0 ]; then echo "⚙️ Preparation step succeeded: '$*'" return 0 else - local exitCode=$? echo "⚠️ Preparation step failed: command '$*' exited with status $exitCode" >&2 + if [ -n "$output" ]; then + echo "---- Command output ----" >&2 + echo "$output" >&2 + echo "-----------------------" >&2 + fi return $exitCode fi }