diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..311c2b9 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,19 @@ +# Copyright 2026 Hewlett Packard Enterprise Development LP +--- +name: unit-tests + +'on': + pull_request: + +jobs: + jenkins-library-guard-unit-test: + name: Jenkins @Library guard unit test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Run Jenkins @Library guard unit test + run: ./tests/guard-jenkins-library-pin-test.sh diff --git a/checks/guard-jenkins-library-pin.sh b/checks/guard-jenkins-library-pin.sh index 2f54ed7..e79167e 100755 --- a/checks/guard-jenkins-library-pin.sh +++ b/checks/guard-jenkins-library-pin.sh @@ -58,7 +58,7 @@ if [[ ${#JENKINS_FILES[@]} -eq 0 ]]; then exit 0 fi -library_regex='@Library[[:space:]]*\(' +library_regex='@Library[[:space:]]*[(]' guard_failed=0 for rel_path in "${JENKINS_FILES[@]}"; do diff --git a/tests/guard-jenkins-library-pin-test.sh b/tests/guard-jenkins-library-pin-test.sh new file mode 100755 index 0000000..1db3a54 --- /dev/null +++ b/tests/guard-jenkins-library-pin-test.sh @@ -0,0 +1,233 @@ +#!/usr/bin/env bash +# Copyright 2026 Hewlett Packard Enterprise Development LP +set -euo pipefail + +# Unit tests for checks/guard-jenkins-library-pin.sh +# +# Each case builds a minimal temporary git repository with a fixture +# Jenkinsfile, runs the guard, and verifies: +# - expected exit code +# - expected stdout/stderr content +# +# Usage: ./tests/guard-jenkins-library-pin-test.sh + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +GUARD="${REPO_ROOT}/checks/guard-jenkins-library-pin.sh" + +pass_count=0 +fail_count=0 + +WORK_DIR="" +cleanup_all() { + if [[ -n "${WORK_DIR}" ]]; then + # Some git-created paths can be non-writable (files and/or dirs, + # platform-dependent); make the tree writable so recursive cleanup is + # reliable across environments. + chmod -R u+w "${WORK_DIR}" 2>/dev/null || true + rm -rf "${WORK_DIR}" 2>/dev/null || true + fi +} +trap cleanup_all EXIT + +WORK_DIR="$(mktemp -d)" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +pass() { echo "[PASS] $1"; pass_count=$(( pass_count + 1 )); } +fail() { echo "[FAIL] $1" >&2; fail_count=$(( fail_count + 1 )); } + +# make_repo DIR +# Initialize a throwaway git repo with a committed state ready for ls-files. +make_repo() { + local dir="$1" + mkdir -p "${dir}" + git -C "${dir}" init -q + git -C "${dir}" config user.email "test@test.local" + git -C "${dir}" config user.name "Test" +} + +# commit_file REPO_DIR REL_PATH +# Stage and commit the file at REL_PATH inside REPO_DIR. +commit_file() { + local dir="$1" + local rel="$2" + git -C "${dir}" add "${rel}" + git -C "${dir}" commit -qm "test fixture: ${rel}" +} + +# run_case NAME WORKDIR EXPECTED_EXIT [STDOUT_SUBSTR] [STDERR_SUBSTR] +# Runs the guard and reports pass/fail. +run_case() { + local name="$1" + local workdir="$2" + local expected_exit="$3" + local stdout_substr="${4:-}" + local stderr_substr="${5:-}" + + local out_file="${WORK_DIR}/${name}.out" + local err_file="${WORK_DIR}/${name}.err" + local actual_exit=0 + + "${GUARD}" --target-root "${workdir}" \ + >"${out_file}" 2>"${err_file}" || actual_exit=$? + + local ok=1 + + # --- exit code check --- + if [[ "${actual_exit}" -ne "${expected_exit}" ]]; then + echo " [${name}] expected exit ${expected_exit}, got ${actual_exit}" >&2 + echo " stdout: $(cat "${out_file}")" >&2 + echo " stderr: $(cat "${err_file}")" >&2 + ok=0 + fi + + # --- stdout content check --- + if [[ -n "${stdout_substr}" ]] && \ + ! grep -qF "${stdout_substr}" "${out_file}"; then + echo " [${name}] stdout missing: ${stdout_substr}" >&2 + echo " stdout was: $(cat "${out_file}")" >&2 + ok=0 + fi + + # --- stderr content check --- + if [[ -n "${stderr_substr}" ]] && \ + ! grep -qF "${stderr_substr}" "${err_file}"; then + echo " [${name}] stderr missing: ${stderr_substr}" >&2 + echo " stderr was: $(cat "${err_file}")" >&2 + ok=0 + fi + + if [[ "${ok}" -eq 1 ]]; then + pass "${name}" + else + fail "${name}" + fi +} + +# --------------------------------------------------------------------------- +# Case 1 — no Jenkinsfile in repo: guard exits 0, reports no candidates +# --------------------------------------------------------------------------- +{ + d="${WORK_DIR}/case_no_jenkinsfile" + make_repo "${d}" + touch "${d}/README.md" + commit_file "${d}" README.md + + run_case \ + "no_jenkinsfile" "${d}" 0 \ + "no Jenkinsfile candidates found" +} + +# --------------------------------------------------------------------------- +# Case 2 — Jenkinsfile with no @Library: guard exits 0, reports ok +# --------------------------------------------------------------------------- +{ + d="${WORK_DIR}/case_clean" + make_repo "${d}" + cat > "${d}/Jenkinsfile" <<'GROOVY' +// clean fixture: no active @Library reference +GROOVY + commit_file "${d}" Jenkinsfile + + run_case \ + "clean_jenkinsfile" "${d}" 0 \ + "no active @Library references found" +} + +# --------------------------------------------------------------------------- +# Case 3 — active @Library reference: guard exits 1, reports blocked +# --------------------------------------------------------------------------- +{ + d="${WORK_DIR}/case_active_library" + make_repo "${d}" + cat > "${d}/Jenkinsfile" <<'GROOVY' +@Library("my-shared-lib") _ +GROOVY + commit_file "${d}" Jenkinsfile + + run_case \ + "active_library_blocked" "${d}" 1 \ + "" \ + "blocked: active @Library reference" +} + +# --------------------------------------------------------------------------- +# Case 4 — @Library in a // line comment: must be ignored, guard exits 0 +# --------------------------------------------------------------------------- +{ + d="${WORK_DIR}/case_line_comment" + make_repo "${d}" + cat > "${d}/Jenkinsfile" <<'GROOVY' +// @Library("my-shared-lib") _ +GROOVY + commit_file "${d}" Jenkinsfile + + run_case \ + "library_in_line_comment_ignored" "${d}" 0 \ + "no active @Library references found" +} + +# --------------------------------------------------------------------------- +# Case 5 — @Library inside a /* */ block comment: must be ignored, exits 0 +# --------------------------------------------------------------------------- +{ + d="${WORK_DIR}/case_block_comment" + make_repo "${d}" + cat > "${d}/Jenkinsfile" <<'GROOVY' +/* + * Example usage: @Library("my-shared-lib") _ + */ +GROOVY + commit_file "${d}" Jenkinsfile + + run_case \ + "library_in_block_comment_ignored" "${d}" 0 \ + "no active @Library references found" +} + +# --------------------------------------------------------------------------- +# Case 6 — @Library with whitespace before paren (spacing variant): blocked +# --------------------------------------------------------------------------- +{ + d="${WORK_DIR}/case_spaced_paren" + make_repo "${d}" + cat > "${d}/Jenkinsfile" <<'GROOVY' +@Library ("my-shared-lib") _ +GROOVY + commit_file "${d}" Jenkinsfile + + run_case \ + "library_spaced_paren_blocked" "${d}" 1 \ + "" \ + "blocked: active @Library reference" +} + +# --------------------------------------------------------------------------- +# Case 7 — Jenkinsfile in a subdirectory: guard must detect it +# --------------------------------------------------------------------------- +{ + d="${WORK_DIR}/case_subdir" + make_repo "${d}" + mkdir -p "${d}/jobs" + cat > "${d}/jobs/Jenkinsfile" <<'GROOVY' +@Library("my-shared-lib") _ +GROOVY + commit_file "${d}" jobs/Jenkinsfile + + run_case \ + "active_library_in_subdir_blocked" "${d}" 1 \ + "" \ + "blocked: active @Library reference" +} + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +echo "" +echo "Results: ${pass_count} passed, ${fail_count} failed" + +if [[ "${fail_count}" -ne 0 ]]; then + exit 1 +fi