diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index 0fe930f..7310277 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,16 +10,37 @@ on: workflow_call: jobs: - build: + test: + uses: ./.github/workflows/test.yaml + + package: runs-on: ubuntu-latest + needs: [test] + if: always() && needs.test.outputs.packages != '{"package":[]}' strategy: - matrix: ${{fromJson(needs.setup.outputs.matrix)}} - needs: [setup] + matrix: ${{ fromJson(needs.test.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 "❌ 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 + 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 +48,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 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..edfb6de --- /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: Packages 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) + exitCode=$? + echo "$output" + + 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.function }} (exit code: $exitCcode)" + echo "${{ matrix.function.package }}=false" >> $GITHUB_OUTPUT + fi 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..52fe80a --- /dev/null +++ b/bashpt @@ -0,0 +1,63 @@ +#!/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=(--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 + 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..87864a9 --- /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 preparation 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..7c4a0bc --- /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}s" + shift + elif [[ -z "$timeout" ]]; then + timeout=5s + fi + + if timeout "${timeout}" bash </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..b691115 --- /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 exitCode + if ! "$@" &>/dev/null; then + exitCode=$? + echo "✅ Assertion passed: command '$*' exited with status $exitCode" + return 0 + else + exitCode=$? + echo "⛔ Assertion failed: command '$*' exited with status $exitCode (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..6c77cc9 --- /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 exitCode + if "$@" &>/dev/null; then + exitCode=0 + echo "✅ Assertion passed: command '$*' exited with status $exitCode" + return 0 + else + exitCode=$? + echo "⛔ Assertion failed: command '$*' exited with status $exitCode (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..b912147 --- /dev/null +++ b/libs/core/test/prep_step @@ -0,0 +1,57 @@ +# 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 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, 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. +# +# 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() { + local output + output=$("$@" 2>&1) + local exitCode=$? + if [ $exitCode -eq 0 ]; then + echo "⚙️ Preparation step succeeded: '$*'" + return 0 + else + 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 +} + 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 +}